#!/usr/bin/env perl

# Test script for communicating with audacity via mod-script-pipe
# Audacity should be running first, with the scripting plugin loaded.
#
# Note that currently, some menu commands require the project to be focused for
# them to work.  Further information and a list of known problems is available
# on the 'Scripting' page of the Audacity wiki.

use strict;
use warnings;
use Time::HiRes qw( gettimeofday tv_interval );
use List::Util qw( max );

# Where should screenshots and exported tracks be saved?
our $home = $ENV{HOME};
our $screenshotDir = $home.'/pipetest/';
our $effectTestDir = $home.'/pipetest/';

# Variables for pipe names
our $Name;
our $UID;
our $ToSrvName;
our $FromSrvName;

# For timing
our $t0;

# TODO: Maybe get the pipe names from audacity?
if ($^O eq 'MSWin32') {
   $Name = 'Srv';
   $ToSrvName = '\\\\.\\pipe\\To'.$Name.'Pipe';
   $FromSrvName = '\\\\.\\pipe\\From'.$Name.'Pipe';
} elsif ($^O eq 'linux') {
   $UID = $<;
   $ToSrvName = '/tmp/audacity_script_pipe.to.'.$UID;
   $FromSrvName = '/tmp/audacity_script_pipe.from.'.$UID;
} elsif ($^O eq 'darwin') {
   $UID = $<;
   $ToSrvName = '/tmp/audacity_script_pipe.to.'.$UID;
   $FromSrvName = '/tmp/audacity_script_pipe.from.'.$UID;
}

# Open pipes
sub startUp{
   open( TO_SRV, "+<$ToSrvName" )
      or die "Could not open $ToSrvName";
   open( FROM_SRV, "+<$FromSrvName" )
      or die "Could not open $FromSrvName";

   # The next 'magic incantation' causes TO_SRV to be flushed every time we
   # write something to it.
   select((select(TO_SRV),$|=1)[0]);
}

# Close pipes
sub finish{
   print "Done. Press return to end.";
   <>;
   close TO_SRV;
   close FROM_SRV;
}

# Subroutines for measuring how long a command takes to complete
sub startTiming{
   $t0 = [gettimeofday];
}
sub stopTiming{
   my $elapsed = tv_interval ( $t0, [gettimeofday] );
   print "[Total time for command: $elapsed seconds.]\n";
}

# Write a command to the pipe
sub sendCommand{
   my $command = shift;
   if ($^O eq 'MSWin32') {
      print TO_SRV "$command

\r\n\0";
   } else {
      # Don't explicitly send \0 on Linux or reads after the first one fail...
      print TO_SRV "$command\n";
   }
   print "[$command]\n";
}

# Send (and time) a command, and print responses
sub doCommand{
   startTiming();
   sendCommand(shift);

   my @resps = getResponses();
   map { print "$_\n"; } @resps;

   stopTiming();
   print "\n";
   return @resps;
}

# Return an array of all responses
sub getResponses{
   my $resp;
   my @responses;

   while($resp = <FROM_SRV>) {
      chomp($resp);
      last if ($resp eq '');
      push(@responses, $resp);
   }

   return @responses;
}

# Get the value of a preference
sub getPref{
   my $name = shift;
   sendCommand("GetPreference: PrefName=$name");
   my @resps = getResponses();
   return shift(@resps);
}

# Set the value of a preference
sub setPref{
   my $name = shift;
   my $val = shift;
   doCommand("SetPreference: PrefName=$name PrefValue=$val");
}

# Send a screenshot command
sub screenshot{
   my $filePath    = shift;
   my $captureMode = shift;
   my $background  = shift;
   doCommand("Screenshot: FilePath=$filePath CaptureMode=$captureMode Background=$background");
}

# Send a menu command
sub menuCommand{
   my $commandName = shift;
   doCommand("MenuCommand: CommandName=$commandName");
}

# Send a command which requests a list of all available menu commands
sub getMenuCommands{
   doCommand("GetMenus: ShowStatus=0");
}

sub showMenuStatus{
   sendCommand("GetMenus: ShowStatus=1");
   my @resps = getResponses();
   map { print "$_\n"; } @resps;
}

# Send a string that should be a syntax error
sub syntaxError{
   doCommand("CommandWithNoColon foo bar");
}

# Send a command that doesn't exist
sub noSuchCommand{
   doCommand("NoSuchCommand: myParam=3");
}

sub parameterTest{
   # Set a non-existent parameter
   doCommand("GetMenus: blah=2");
   # Parameter with no '='
   doCommand("MenuCommand: CommandName");
}

# See what happens when commands have extra spaces in various places
sub extraSpaces{
   doCommand("Help: CommandName=Help");
   doCommand("Help : CommandName=Help");
   doCommand("Help: CommandName =Help");
   doCommand("Help: CommandName= Help");
   doCommand("Help: CommandName=Help ");
}

# Test whether we can fall back to batch mode
sub batchFallback{
   doCommand( "Echo: Delay=1.0 Decay=0.5" );
}

# Send lots of commands quickly
sub stressTest{
   my $n = 0;
   while($n < 600){
      getMenuCommands();
      ++$n;
   }
}

# Get help on a command
sub help{
   my $cmdName = shift;
   doCommand("Help: CommandName=$cmdName");
}

# Get help on all of the listed commands
sub fullHelp{
   my @cmds = qw(BatchCommand CompareAudio MenuCommand GetMenus GetTrackInfo Help Message Screenshot Select SetTrackInfo);
   foreach my $cmd (@cmds){
      help($cmd);
   }
}

# From script, this works like an 'echo'
sub message{
   my $msg = shift;
   doCommand("Message: MessageString=$msg");
}

# Send a CompareAudio command with a given threshold
sub compareAudio{
   my $threshold = shift;
   my @resps = doCommand("CompareAudio: Threshold=$threshold");
   shift(@resps);
   return @resps;
}

# Delete all tracks
sub deleteAll{
   doCommand("Select: Mode=All");
   menuCommand("RemoveTracks");
}

# A test of the CompareAudio command
sub compareTest{
   deleteAll();

   menuCommand("NewMonoTrack");
   doCommand("Chirp:");
   menuCommand("NewMonoTrack");
   doCommand("Chirp:");

   my $j = 0;
   while($j < 3)
   {
      my $i = 0;
      while($i < 6){
         doCommand("Select: Mode=Range StartTime=5.0 EndTime=8.0 FirstTrack=0 LastTrack=0");

         doCommand("Amplify: Ratio=0.95");
         doCommand("Select: Mode=All");
         compareAudio(0.4 - 0.1*$j);
         ++$i;
      }
      ++$j;
   }
}

# Print some info returned by the GetTrackInfo command
sub getTrackInfo{
   my $trackID = shift;
   sendCommand("GetTrackInfo: Type=Name TrackIndex=0");
   my @resps = getResponses();
   my $name = shift(@resps);
   sendCommand("GetTrackInfo: Type=StartTime TrackIndex=0");
   @resps = getResponses();
   my $startTime = shift(@resps);
   sendCommand("GetTrackInfo: Type=EndTime TrackIndex=0");
   @resps = getResponses();
   my $endTime = shift(@resps);
   sendCommand("GetTrackInfo: Type=Pan TrackIndex=0");
   @resps = getResponses();
   my $pan = shift(@resps);
   sendCommand("GetTrackInfo: Type=Gain TrackIndex=0");
   @resps = getResponses();
   my $gain = shift(@resps);
   sendCommand("GetTrackInfo: Type=Mute TrackIndex=0");
   @resps = getResponses();
   my $mute = shift(@resps);
   sendCommand("GetTrackInfo: Type=Solo TrackIndex=0");
   @resps = getResponses();
   my $solo = shift(@resps);
   sendCommand("GetTrackInfo: Type=Selected TrackIndex=0");
   @resps = getResponses();
   my $selected = shift(@resps);
   sendCommand("GetTrackInfo: Type=Focused TrackIndex=0");
   @resps = getResponses();
   my $focused = shift(@resps);
   sendCommand("GetTrackInfo: Type=Linked TrackIndex=0");
   @resps = getResponses();
   my $linked = shift(@resps);

   print "     Name: $name\n";
   print "StartTime: $startTime\n";
   print "  EndTime: $endTime\n";
   print "      Pan: $pan\n";
   print "     Gain: $gain\n";
   print "     Mute: $mute\n";
   print "     Solo: $solo\n";
   print " Selected: $selected\n";
   print "  Focused: $focused\n";
   print "   Linked: $linked\n";
}

# Assortment of different tests
sub fullTest{
   syntaxError();
   extraSpaces();
   menuCommand("NewStereoTrack");
   #screenshot($screenshotDir, "window", "None"); # (Slow)
   doCommand("Select: Mode=All");
   getMenuCommands();
   menuCommand("NewMonoTrack");
   batchFallback();
   help("Screenshot");
   message("Hello!");
   getTrackInfo(0);
   deleteAll();
}

# Play for three seconds, then stop
sub playAndStop{
   menuCommand("Play");
   sleep(3.0);
   menuCommand("Stop");
}

# Select part of a stereo track
sub selectRegion{
   my $track = shift;
   my $start = shift;
   my $end = shift;
   my $t1 = $track + 1;
   doCommand("Select: Mode=Range FirstTrack=$track LastTrack=$t1 StartTime=$start EndTime=$end");
}

# Run testing on the effects that use the ClearAndPaste method
# Allows the user to check whether effects transform time correctly
sub testClearAndPasters{

   # Which effects to test, and with what parameters
   my @clearAndPasters = (
      "Unchanged:", # control: nonexistent command, so does nothing
                    # (so 'batch command not recognised' isn't an error)
      "Noise:",    # generate
      "NoiseRemoval:",                 # misc clear&paste
      "ChangeSpeed: Percentage=-10.0", # misc clear&paste
      "ChangeSpeed: Percentage=40.0", # misc clear&paste
      "ChangeTempo: Percentage=-20.0", # soundtouch
      "ChangeTempo: Percentage=80.0", # soundtouch
      "ChangePitch: Percentage=25.0", # soundtouch
      "ChangePitch: Percentage=-80.0", # soundtouch
      "TimeScale: RateStart=-80.0 RateEnd=150.0 HalfStepsStart=-5.0 HalfStepsEnd=8.0 PreAnalyze=no",                      # SBSMS
   ); # nyquist can't be called currently

   # Allow time for user to give the project window focus (workaround for menu
   # command problem)
   sleep(1.0);
   deleteAll();
   my $len = 20.0;

   # Since there aren't proper generator commands yet, we use the preferences
   # to control the duration.
   # This preferences is not read in Audacity 2.2.x where duration
   # is read from pluginsettings.cfg
   my $origDuration = getPref("/CsPresets/NoiseGen_Duration");
   setPref("/CsPresets/NoiseGen_Duration", $len);

   # For each effect to test:
   # * Create some stereo noise, and chop two pieces out of it
   # * Add some labels, then apply the effect
   # @splits determines where the splits are
   my @splits = map {$_ * $len} (0.999, 0.2, 0.5, 0.6, 0.8, 0.1, 0.9);
   my $trackNum = 0;
   foreach my $effect (@clearAndPasters) {
      menuCommand("NewStereoTrack");
      selectRegion($trackNum, 0.0, $splits[0]);
      doCommand("Noise:");
      selectRegion($trackNum, $splits[1], $splits[2]);
      menuCommand("SplitDelete");
      menuCommand("AddLabel");
      selectRegion($trackNum, $splits[3], $splits[4]);
      menuCommand("SplitDelete");
      menuCommand("AddLabel");

      # Apply the effect
      selectRegion($trackNum, $splits[5], $splits[6]);
      doCommand($effect);

      # Make and set the track name
      my @splat = split(':', $effect);
      my $name = $splat[0];
      doCommand("SetTrackInfo: TrackIndex=$trackNum Type=Name Name=$name");
      doCommand("Select: Mode=None");

      $trackNum = $trackNum + 3;
   }

   # Set duration back to what it was before
   setPref("/CsPresets/NoiseGen_Duration", $origDuration);
}


###############################################################################
#  Effect testing                                                             #
###############################################################################

# A list of effects to test (could be got from Audacity in future)
sub getEffects{

   # (These ones will need special handling)
   # AutoDuck
   # Bass and Treble
   # Repair
   # NoiseRemoval
   # ClickRemoval
   # Paulstretch

   # TimeScale (disabled because it's so slow)

   my @effects = qw(
      Amplify
      ChangePitch
      ChangeSpeed
      ChangeTempo
      Compressor
      Echo
      Equalization
      FadeIn
      FadeOut
      Invert
      Normalize
      Phaser
      Repeat
      Reverse
      TruncateSilence
      Wahwah
   );
   return @effects;
}

# Create a chirp for an effect to be applied to.
# The duration setting does not work in Audacity 2.2.x where duration
# is read from pluginsettings.cfg
sub generateBase{
   my $genCmd = "Chirp";
   my $duration = 30.0;
   menuCommand("NewMonoTrack");
   doCommand("$genCmd:");
   my $desc = $genCmd . "-" . $duration . "s";
   return $desc;
}

# Apply an effect and save the results (for use as reference output)
sub saveEffectResults{
   my $dirname = shift;
   my $effect = shift;
   deleteAll();

   my $filename = $dirname . "/" . generateBase() . "-" . $effect . ".wav";
   doCommand($effect);

   printHeading("Exporting to $filename\n");
   doCommand("Export: Mode=All Filename=$filename Channels=1");
}

# Apply an effect and compare the result to reference output
sub doEffectTest{
   my $dirname = shift;
   my $effect = shift;

   deleteAll();
   my $filename = $dirname . "/" . generateBase() . "-" . $effect . ".wav";
   doCommand("SetTrackInfo: TrackIndex=0 Type=Name Name=$effect");
   doCommand($effect);
   doCommand("Import: Filename=$filename");
   doCommand("Select: Mode=All");
   my @result = compareAudio(0.001);
   return @result;
}

# Export reference copies of the effects in the list 
sub exportEffects{
   my $exportDir = shift;
   my @effects = getEffects();
   foreach my $effect (@effects) {
      saveEffectResults($exportDir, $effect);
   }
}

# Test each of the effects in the list
sub testEffects{
   my $referenceDir = shift;
   my %results = ();

   my @effects = getEffects();
   foreach my $effect (@effects) {
      printHeading("Testing effect: $effect");
      my @res = doEffectTest($referenceDir, $effect);
      $results{ $effect }[0] = $res[0];
      $results{ $effect }[1] = $res[1];
   }

   # Print out table of results
   my $padLength = max(map { length($_) } @effects);

   printHeading("Test results");
   print "Effect name\tSamples\tSeconds\n\n";
   for my $effect (keys %results) {
      my $padded = sprintf("%-${padLength}s", $effect);
      my $badSamples = $results{ $effect }[0];
      my $badSeconds = $results{ $effect }[1];
      print "$padded\t$badSamples\t$badSeconds\n";
   }
}

# Print text with ascii lines above and below
sub printHeading{
   my $msg = shift;
   my $line = "-" x length($msg);
   print "$line\n$msg\n$line\n\n";
}

###############################################################################

startUp();

# Send some test commands

exportEffects($effectTestDir);
testEffects($effectTestDir);

finish();
