ns_audinterface.pm

#    NS_AUDINTERFACE --> signal output control for Nightsteps software 
#    Copyright (C) 2022 Cliff Hammett
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.

package ns_audinterface{
#the audio interface layer translates data structures into audio parameters;
    use strict;
    use warnings;
    use lib ".";
    use Switch;
    use ns_testtools;
#    use ns_audlibrary;
    use Math::Round qw(round);
    use Data::Dumper;

    sub new {
        my ($class, $son, $thres, $option) = @_;
        my $ra_rules = $son->{rules};
        if ($son->{optionSpecificRules}->{$option}){
          push @{$ra_rules}, @{$son->{optionSpecificRules}->{$option}};
        }
        my $rh_effects = $son->{effectSets};
        if ($son->{optionSpecificEffects}->{$option}){
          $rh_effects = {$rh_effects, $son->{optionSpecificEffects}->{$option}};
        }
        my $this  = {
#            _mode => shift,
            _sonicspace=>1152,
            _scorelines=>24,
            _linedur=>48,
            _standardbeatdur=>4,
            _thres => $thres,
            _sonification => $son,
            _rules => $ra_rules,
            _effects=> $rh_effects,
            _testtools => ns_testtools->new(0),
#            _audlib => ns_audlibrary->new,
            _outputs => [{path=>'/home/pi/nsdata/gpio/dsig_l.o', field=>'detected_left', ra_sig=>[]}, 
                         {path=>'/home/pi/nsdata/gpio/dsig_r.o', field=>'detected_right', ra_sig=>[]}],
            _gpoutpath => '/home/pi/nsdata/gpio/'
        };
        bless $this, $class;
        return $this;
    }

    sub physSendInstructions{
        my ($this, $file, $ra_instr) = @_;
        open (FO, ">", $file);
        foreach my $l (@{$ra_instr}){
            print FO "$l\n";
        }
        close FO;
    }

######################################## NEWNEWNEW SONIC LOGIC ##############################################
#############################################################################################################

  sub createScore{
    my ($this, $rah_do) = @_;
    my $number = @{$rah_do};
    $this->{_testtools}->outputText("creating score from $number records\n");
    foreach my $out (@{$this->{_outputs}}){
      $out->{ra_sig} = [];
    }

    foreach my $rh_do (@{$rah_do}){
      my $o = 0;
      foreach my $out (@{$this->{_outputs}}){
        if ($rh_do->{$out->{field}}){
          $o++;
          $this->{_testtools}->outputText("$out->{field} is true\n");
        }
      }
      foreach my $out (@{$this->{_outputs}}){
        my $ra_instr = [];
        if ($rh_do->{$out->{field}}){
          my $distthres;
          my $rh_attr = {elecount =>16, repeats=>0, solstr=>0, rhythmstr=>0};
          if ($o == 2){
            $distthres = $this->{_thres}->{_distanceBands}->{centreThresholds};
          }else{
            $distthres = $this->{_thres}->{_distanceBands}->{offcentreThresholds};
          }      
          if ($rh_do->{$out->{field}}){
            $ra_instr = $this->generateScoreColumn($rh_do, $rh_attr, $out, $distthres);
          }
        }else{
          $ra_instr = $this->generateEmptyColumn;
        }
        push @{$out->{ra_sig}}, $ra_instr;
      }
    }
    $this->outputScore;
    return $this->{_outputs};
  }

  sub createEmptyScore{
    my $this = shift;
    $this->{_testtools}->outputText("creating empty score\n");
    foreach my $out (@{$this->{_outputs}}){
      $out->{ra_sig} = [];
      my $ra_instr = $this->generateEmptyColumn;
      push @{$out->{ra_sig}}, $ra_instr;
    }
    $this->outputScore;
    return $this->{_outputs};
  }

  sub generateScoreColumn{
    my ($this, $rh_do, $rh_attr, $out, $distthres) = @_;
    #print "generating Score column\n";
    my $ra_instr = [];
    #print Dumper($distthres);
    foreach my $t (@{$distthres}){
      $this->{_testtools}->outputText("$rh_do->{distance} <> $t->{maxdist}\n");
      if ($rh_do->{distance} < $t->{maxdist}){
        $rh_attr->{repeats} = $t->{repeats};
        $rh_attr->{solstr} = $t->{solstr};
      }
    }
    foreach my $b (@{$this->{_thres}->{_pcBands}}){
      my $solbonus = 0;
      #print Dumper($rh_do->{$this->{_thres}->{_pcBands}});
      $this->{_testtools}->outputText("MINVAL:  $b->{minval}\n");
      $this->{_testtools}->outputText("Field: $this->{_thres}->{_pcField}\n");
      $this->{_testtools}->outputText("Act Val: $rh_do->{$this->{_thres}->{_pcField}}\n");
      if ($rh_do->{$this->{_thres}->{_pcField}} > $b->{minval}){
        $rh_attr->{rhythmstr} = $b->{rhythmstr};
        $solbonus = $b->{solstr};
      }
      $rh_attr->{solstr} += $solbonus; 
    }
    #print Dumper($rh_attr);
    if ($rh_attr->{repeats} > 0){
      my $base = $this->generateBase($rh_do->{descr}, $rh_attr->{rhythmstr});
      my $ra_rhythm = $this->processBaseIntoRhythm($base, $rh_attr->{rhythmstr});
      $ra_instr = $this->convertRhythmIntoInstr($rh_attr, $ra_rhythm);
    }else{
      #print "no repeats\n";
      $ra_instr = $this->generateEmptyColumn;
    }
    #print "pre returned instr:\n";
    return $ra_instr;
  }

  sub generateEmptyColumn{
    my $this = shift;
    my @instr = ();
    $this->{_testtools}->outputText("generating Empty Column\n");
    for(my $i=0; $i<$this->{_scorelines}; $i++){
      my $l = {line=>"d$this->{_linedur}\@f0", hasstrike=>0};
      my $ra_l = $l;
      push @instr, $ra_l;
    }
    return \@instr;
  }

  sub outputScore{
    my $this = shift;
    foreach my $out (@{$this->{_outputs}}){
      my @lines = ();
      $this->{_testtools}->outputText("Outputting to $out->{path}\n");
      #foreach my $ra (@{$ra_sig}){
      my $ra = $out->{ra_sig};
      my $sizerows = @{$ra->[0]};
      my $sizecolumns = @{$ra};
      for (my $i=0; $i<$sizerows; $i++){
        my $l = "";
        for (my $k=0; $k<$sizecolumns; $k++){
          if ($k > 0){ $l .= "|";}
          $l .= $ra->[$k]->[$i]->{line};
        }
        push @lines, $l;
      }
      $this->physSendInstructions($out->{path}, \@lines);
    }
  }

  sub generateBase{
    my ($this, $s, $v) = @_;
    $this->{_testtools}->outputText("generating base from: $s\n");
    my $substr = substr $s, 0, 31;
    my @char = split //, $s;
    my $size = @char;
    my @prebaserhythm;
    my @baserhythm;
    for (my $i=0; $i<32; $i+=2){
      my $cn = 0;
      if ($i < $size){
       $cn = ord($char[$i]);
      }
      $this->{_testtools}->outputText("$cn\n");
      push @prebaserhythm, $cn;
    }
    my @percentile = sort {$a <=> $b} @prebaserhythm;
    my $pp = $this->makePercentileThres($v);
    my $small = $percentile[$pp->[0]];
    my $medium = $percentile[$pp->[1]];
    my $large = $percentile[$pp->[2]];
    foreach my $n (@prebaserhythm){
      if ($n <= $small)    { push @baserhythm,0}
      elsif ($n <= $medium){ push @baserhythm,1}
      elsif ($n <= $large) { push @baserhythm,2}
      else                { push @baserhythm,3};
    }
    return \@baserhythm;
  }

  sub makePercentileThres{
      my ($this, $v) = @_;
      my @thres = ([9,12,15],
                   [8,11,14],
                   [7,10,13],
                   [6,9,12],
                   [4,8,11]);
      return $thres[$v];
  }


  sub processBaseIntoRhythm{
      my ($this, $ra, $v) = @_;
      $this->{_testtools}->outputText("processing base into rhythm\n");
      my $laststate = "";
      my $lastval = 0;
      my $size = @{$ra};
      my @rhythm = ();
      #my $st = $this->makeStrengthValues($v); #v must be between 0 and 4
      my $st = {strongcrit=> 3, strongthres => 2, restcrit =>0};
      my $p = -1;
      for (my $i=0; $i<$size; $i++){
        if ($lastval == $st->{strongcrit} && $ra->[$i] >= $st->{strongthres} && $laststate eq "littlebeat"){
          $rhythm[$p]->{type} = "strongbeat";
          $rhythm[$p]->{ebLength} += 1;
          $laststate = "strongbeat";
        }elsif ($lastval < $st->{strongcrit} && $lastval > $st->{restcrit} &&  $ra->[$i] == 1 && $laststate eq "littlebeat"){
          $rhythm[$p]->{ebLength} += 1;
        }elsif ($ra->[$i] <= $st->{restcrit} && $laststate eq "rest"){
          $rhythm[$p]->{ebLength} += 1;
        }elsif ($ra->[$i] <= $st->{restcrit}){
          my $rh = {type => "rest", ebLength => 1};
          push @rhythm, $rh;
          $p++;
          $laststate = "rest";
        }else{
          my $rh = {type => "littlebeat", ebLength => 1};
          push @rhythm, $rh;
          $p++;
          $laststate = "littlebeat";
        }
        $lastval = $ra->[$i];
      }
      return \@rhythm;
  }

  sub makeStrengthValues{
    my ($this, $i) = @_;
    my @val = ({strongcrit=> 4, strongthres => 2, restcrit =>1},
               {strongcrit=> 3, strongthres => 2, restcrit =>1},
               {strongcrit=> 3, strongthres => 1, restcrit =>1},
               {strongcrit=> 3, strongthres => 1, restcrit =>0},
               {strongcrit=> 2, strongthres => 1, restcrit =>0});
    return $val[$i];
  }

  sub convertRhythmIntoInstr{
    my ($this, $rh, $ra_rhythm) = @_;
    $this->{_testtools}->outputText("converting rhythm into instr\n");
    my $rhylength = @{$ra_rhythm};
    my $blockdur = $this->{_sonicspace} / ($rh->{elecount} * $rh->{repeats});
    $this->{_testtools}->outputText("Blockdur is $blockdur\n");
    my @scorepart = ();
    my $curdur = 0;
    my $linesadded = 0;
    my $uf = 0;
    my $rh_attr = { blockdur=>$blockdur,
                    baseStrength=>$rh->{solstr},
                    unfinishedLine=>$uf };
    for(my $i=0;$linesadded < $this->{_scorelines}; $i++){
      if ($i == $rhylength){
        $this->{_testtools}->outputText("resetting\n");
        $i=0;
      }
      $rh_attr->{type} = $ra_rhythm->[$i]->{type};
      $rh_attr->{eleBlockLength} = $ra_rhythm->[$i]->{ebLength};
      my $ra_curBeat = $this->resolveBeat($rh_attr);
      for my $c (@{$ra_curBeat}){
        #print Dumper($c);
        if ($c->{ttldur} >= $this->{_linedur}){
          push @scorepart, $c;
          $rh_attr->{unfinishedLine} = 0;
          $linesadded++;
        }else{
          $rh_attr->{unfinishedLine} = $c;
        }
      }
    }
    return \@scorepart;
  }
  
  sub resolveBeat{
    my ($this, $rh_attr) = @_;
    my $ra_return = [];
    switch ($rh_attr->{type}){
      case "rest"{ $ra_return = $this->addRestBlock($rh_attr)}
      case "strongbeat"{ $ra_return = $this->addStrongBeatBlocks($rh_attr)}
      case "littlebeat"{ $ra_return = $this->addLittleBeatBlocks($rh_attr)}
      case "tap"{ $ra_return = $this->addTapBlocks($rh_attr)}
    }
    return $ra_return;
  }

  sub addRestBlock{
    my ($this, $rh_attr) = @_;
    my $rah = [{dur=>$rh_attr->{blockdur} * $rh_attr->{eleBlockLength},
                force=>0}
                ];
    my $ra_return = $this->composeBlock($rh_attr, $rah);
    return $ra_return;
  }

  sub addStrongBeatBlocks{
    my ($this, $rh_attr) = @_;
    my $dur = $rh_attr->{blockdur} * $rh_attr->{eleBlockLength}; 
    my $rah = [{dur=>($dur/2)-$this->{_standardbeatdur},
                force=>0},
               {dur=>$this->{_standardbeatdur} * 2,
                force=>$rh_attr->{baseStrength}},
               {dur=>($dur/2)-$this->{_standardbeatdur},
                force=>0}
                ];
    my $ra_return = $this->composeBlock($rh_attr, $rah);
    return $ra_return;
  }
  
  sub addLittleBeatBlocks{
    my ($this, $rh_attr) = @_;
    my $dur = $rh_attr->{blockdur} * $rh_attr->{eleBlockLength}; 
    my $rah = [{dur=>($dur/2)-($this->{_standardbeatdur}/2),
                force=>0},
               {dur=>$this->{_standardbeatdur},
                force=>$rh_attr->{baseStrength}},
               {dur=>($dur/2)-($this->{_standardbeatdur}/2),
                force=>0}
                ];
    my $ra_return = $this->composeBlock($rh_attr, $rah);
    return $ra_return;
  }

  sub addTapBlocks{
    my ($this, $rh_attr) = @_;
    my $dur = $rh_attr->{blockdur};
    my $reps = $rh_attr->{eleBlockLength}; 
    my $rah = [];
    for (my $i=0; $i<$reps; $i++){
      push @{$rah}, {dur=>($dur/2)-($this->{_standardbeatdur}/2),force=>0};
      push @{$rah}, {dur=>$this->{_standardbeatdur}, force=>$rh_attr->{baseStrength}};
      push @{$rah}, {dur=>($dur/2)-($this->{_standardbeatdur}/2),force=>0};
    }
    my $ra_return = $this->composeBlock($rh_attr, $rah);
    return $ra_return;
  }

  sub composeBlock{
    my ($this, $rh_attr, $rah) = @_;
    my $ra_return;
    if ($rh_attr->{unfinishedLine}){
      $ra_return = [$rh_attr->{unfinishedLine}]
    }else{
      $ra_return = [{line => "",
                    ttldur => 0,
                    hasstrike => 0}];
    }
    my $i = 0;
    for my $rh (@{$rah}){
      #print Dumper($rh);
      while ($rh->{dur} > 0){
        my $remains = $this->{_linedur} - $ra_return->[$i]->{ttldur};
        my $pref = "";
        if ($rh->{force} > 0){
          $ra_return->[$i]->{hasstrike} = 1;
        }
        if ($rh->{dur} > $remains){
          if (length($ra_return->[$i]->{line}) > 0){
            $pref = ",";
          }
          $ra_return->[$i]->{line} .= $pref . "d$remains\@f$rh->{force}";
          $ra_return->[$i]->{ttldur} += $remains;
          $rh->{dur} -= $remains;
          push @{$ra_return}, {line=>"", ttldur=> 0, hasstrike => 0};
          $i++;
        }else{
          if (length($ra_return->[$i]->{line}) > 0){
            $pref = ",";
          }
          $ra_return->[$i]->{line} .= $pref . "d$rh->{dur}\@f$rh->{force}";
          $ra_return->[$i]->{ttldur} += $rh->{dur};
          $rh->{dur} = 0;
        }
      }
    }
    return $ra_return;
  }

}
1;