1#!/usr/bin/env perl
2
3# Test script for communicating with audacity via mod-script-pipe
4# Audacity should be running first, with the scripting plugin loaded.
5#
6# Note that currently, some menu commands require the project to be focused for
7# them to work.  Further information and a list of known problems is available
8# on the 'Scripting' page of the Audacity wiki.
9
10use strict;
11use warnings;
12use Time::HiRes qw( gettimeofday tv_interval );
13use List::Util qw( max );
14
15# Where should screenshots and exported tracks be saved?
16our $home = $ENV{HOME};
17our $screenshotDir = $home.'/pipetest/';
18our $effectTestDir = $home.'/pipetest/';
19
20# Variables for pipe names
21our $Name;
22our $UID;
23our $ToSrvName;
24our $FromSrvName;
25
26# For timing
27our $t0;
28
29# TODO: Maybe get the pipe names from audacity?
30if ($^O eq 'MSWin32') {
31   $Name = 'Srv';
32   $ToSrvName = '\\\\.\\pipe\\To'.$Name.'Pipe';
33   $FromSrvName = '\\\\.\\pipe\\From'.$Name.'Pipe';
34} elsif ($^O eq 'linux') {
35   $UID = $<;
36   $ToSrvName = '/tmp/audacity_script_pipe.to.'.$UID;
37   $FromSrvName = '/tmp/audacity_script_pipe.from.'.$UID;
38} elsif ($^O eq 'darwin') {
39   $UID = $<;
40   $ToSrvName = '/tmp/audacity_script_pipe.to.'.$UID;
41   $FromSrvName = '/tmp/audacity_script_pipe.from.'.$UID;
42}
43
44# Open pipes
45sub startUp{
46   open( TO_SRV, "+<$ToSrvName" )
47      or die "Could not open $ToSrvName";
48   open( FROM_SRV, "+<$FromSrvName" )
49      or die "Could not open $FromSrvName";
50
51   # The next 'magic incantation' causes TO_SRV to be flushed every time we
52   # write something to it.
53   select((select(TO_SRV),$|=1)[0]);
54}
55
56# Close pipes
57sub finish{
58   print "Done. Press return to end.";
59   <>;
60   close TO_SRV;
61   close FROM_SRV;
62}
63
64# Subroutines for measuring how long a command takes to complete
65sub startTiming{
66   $t0 = [gettimeofday];
67}
68sub stopTiming{
69   my $elapsed = tv_interval ( $t0, [gettimeofday] );
70   print "[Total time for command: $elapsed seconds.]\n";
71}
72
73# Write a command to the pipe
74sub sendCommand{
75   my $command = shift;
76   if ($^O eq 'MSWin32') {
77      print TO_SRV "$command
78
79\r\n\0";
80   } else {
81      # Don't explicitly send \0 on Linux or reads after the first one fail...
82      print TO_SRV "$command\n";
83   }
84   print "[$command]\n";
85}
86
87# Send (and time) a command, and print responses
88sub doCommand{
89   startTiming();
90   sendCommand(shift);
91
92   my @resps = getResponses();
93   map { print "$_\n"; } @resps;
94
95   stopTiming();
96   print "\n";
97   return @resps;
98}
99
100# Return an array of all responses
101sub getResponses{
102   my $resp;
103   my @responses;
104
105   while($resp = <FROM_SRV>) {
106      chomp($resp);
107      last if ($resp eq '');
108      push(@responses, $resp);
109   }
110
111   return @responses;
112}
113
114# Get the value of a preference
115sub getPref{
116   my $name = shift;
117   sendCommand("GetPreference: PrefName=$name");
118   my @resps = getResponses();
119   return shift(@resps);
120}
121
122# Set the value of a preference
123sub setPref{
124   my $name = shift;
125   my $val = shift;
126   doCommand("SetPreference: PrefName=$name PrefValue=$val");
127}
128
129# Send a screenshot command
130sub screenshot{
131   my $filePath    = shift;
132   my $captureMode = shift;
133   my $background  = shift;
134   doCommand("Screenshot: FilePath=$filePath CaptureMode=$captureMode Background=$background");
135}
136
137# Send a menu command
138sub menuCommand{
139   my $commandName = shift;
140   doCommand("MenuCommand: CommandName=$commandName");
141}
142
143# Send a command which requests a list of all available menu commands
144sub getMenuCommands{
145   doCommand("GetMenus: ShowStatus=0");
146}
147
148sub showMenuStatus{
149   sendCommand("GetMenus: ShowStatus=1");
150   my @resps = getResponses();
151   map { print "$_\n"; } @resps;
152}
153
154# Send a string that should be a syntax error
155sub syntaxError{
156   doCommand("CommandWithNoColon foo bar");
157}
158
159# Send a command that doesn't exist
160sub noSuchCommand{
161   doCommand("NoSuchCommand: myParam=3");
162}
163
164sub parameterTest{
165   # Set a non-existent parameter
166   doCommand("GetMenus: blah=2");
167   # Parameter with no '='
168   doCommand("MenuCommand: CommandName");
169}
170
171# See what happens when commands have extra spaces in various places
172sub extraSpaces{
173   doCommand("Help: CommandName=Help");
174   doCommand("Help : CommandName=Help");
175   doCommand("Help: CommandName =Help");
176   doCommand("Help: CommandName= Help");
177   doCommand("Help: CommandName=Help ");
178}
179
180# Test whether we can fall back to batch mode
181sub batchFallback{
182   doCommand( "Echo: Delay=1.0 Decay=0.5" );
183}
184
185# Send lots of commands quickly
186sub stressTest{
187   my $n = 0;
188   while($n < 600){
189      getMenuCommands();
190      ++$n;
191   }
192}
193
194# Get help on a command
195sub help{
196   my $cmdName = shift;
197   doCommand("Help: CommandName=$cmdName");
198}
199
200# Get help on all of the listed commands
201sub fullHelp{
202   my @cmds = qw(BatchCommand CompareAudio MenuCommand GetMenus GetTrackInfo Help Message Screenshot Select SetTrackInfo);
203   foreach my $cmd (@cmds){
204      help($cmd);
205   }
206}
207
208# From script, this works like an 'echo'
209sub message{
210   my $msg = shift;
211   doCommand("Message: MessageString=$msg");
212}
213
214# Send a CompareAudio command with a given threshold
215sub compareAudio{
216   my $threshold = shift;
217   my @resps = doCommand("CompareAudio: Threshold=$threshold");
218   shift(@resps);
219   return @resps;
220}
221
222# Delete all tracks
223sub deleteAll{
224   doCommand("Select: Mode=All");
225   menuCommand("RemoveTracks");
226}
227
228# A test of the CompareAudio command
229sub compareTest{
230   deleteAll();
231
232   menuCommand("NewMonoTrack");
233   doCommand("Chirp:");
234   menuCommand("NewMonoTrack");
235   doCommand("Chirp:");
236
237   my $j = 0;
238   while($j < 3)
239   {
240      my $i = 0;
241      while($i < 6){
242         doCommand("Select: Mode=Range StartTime=5.0 EndTime=8.0 FirstTrack=0 LastTrack=0");
243
244         doCommand("Amplify: Ratio=0.95");
245         doCommand("Select: Mode=All");
246         compareAudio(0.4 - 0.1*$j);
247         ++$i;
248      }
249      ++$j;
250   }
251}
252
253# Print some info returned by the GetTrackInfo command
254sub getTrackInfo{
255   my $trackID = shift;
256   sendCommand("GetTrackInfo: Type=Name TrackIndex=0");
257   my @resps = getResponses();
258   my $name = shift(@resps);
259   sendCommand("GetTrackInfo: Type=StartTime TrackIndex=0");
260   @resps = getResponses();
261   my $startTime = shift(@resps);
262   sendCommand("GetTrackInfo: Type=EndTime TrackIndex=0");
263   @resps = getResponses();
264   my $endTime = shift(@resps);
265   sendCommand("GetTrackInfo: Type=Pan TrackIndex=0");
266   @resps = getResponses();
267   my $pan = shift(@resps);
268   sendCommand("GetTrackInfo: Type=Gain TrackIndex=0");
269   @resps = getResponses();
270   my $gain = shift(@resps);
271   sendCommand("GetTrackInfo: Type=Mute TrackIndex=0");
272   @resps = getResponses();
273   my $mute = shift(@resps);
274   sendCommand("GetTrackInfo: Type=Solo TrackIndex=0");
275   @resps = getResponses();
276   my $solo = shift(@resps);
277   sendCommand("GetTrackInfo: Type=Selected TrackIndex=0");
278   @resps = getResponses();
279   my $selected = shift(@resps);
280   sendCommand("GetTrackInfo: Type=Focused TrackIndex=0");
281   @resps = getResponses();
282   my $focused = shift(@resps);
283   sendCommand("GetTrackInfo: Type=Linked TrackIndex=0");
284   @resps = getResponses();
285   my $linked = shift(@resps);
286
287   print "     Name: $name\n";
288   print "StartTime: $startTime\n";
289   print "  EndTime: $endTime\n";
290   print "      Pan: $pan\n";
291   print "     Gain: $gain\n";
292   print "     Mute: $mute\n";
293   print "     Solo: $solo\n";
294   print " Selected: $selected\n";
295   print "  Focused: $focused\n";
296   print "   Linked: $linked\n";
297}
298
299# Assortment of different tests
300sub fullTest{
301   syntaxError();
302   extraSpaces();
303   menuCommand("NewStereoTrack");
304   #screenshot($screenshotDir, "window", "None"); # (Slow)
305   doCommand("Select: Mode=All");
306   getMenuCommands();
307   menuCommand("NewMonoTrack");
308   batchFallback();
309   help("Screenshot");
310   message("Hello!");
311   getTrackInfo(0);
312   deleteAll();
313}
314
315# Play for three seconds, then stop
316sub playAndStop{
317   menuCommand("Play");
318   sleep(3.0);
319   menuCommand("Stop");
320}
321
322# Select part of a stereo track
323sub selectRegion{
324   my $track = shift;
325   my $start = shift;
326   my $end = shift;
327   my $t1 = $track + 1;
328   doCommand("Select: Mode=Range FirstTrack=$track LastTrack=$t1 StartTime=$start EndTime=$end");
329}
330
331# Run testing on the effects that use the ClearAndPaste method
332# Allows the user to check whether effects transform time correctly
333sub testClearAndPasters{
334
335   # Which effects to test, and with what parameters
336   my @clearAndPasters = (
337      "Unchanged:", # control: nonexistent command, so does nothing
338                    # (so 'batch command not recognised' isn't an error)
339      "Noise:",    # generate
340      "NoiseRemoval:",                 # misc clear&paste
341      "ChangeSpeed: Percentage=-10.0", # misc clear&paste
342      "ChangeSpeed: Percentage=40.0", # misc clear&paste
343      "ChangeTempo: Percentage=-20.0", # soundtouch
344      "ChangeTempo: Percentage=80.0", # soundtouch
345      "ChangePitch: Percentage=25.0", # soundtouch
346      "ChangePitch: Percentage=-80.0", # soundtouch
347      "TimeScale: RateStart=-80.0 RateEnd=150.0 HalfStepsStart=-5.0 HalfStepsEnd=8.0 PreAnalyze=no",                      # SBSMS
348   ); # nyquist can't be called currently
349
350   # Allow time for user to give the project window focus (workaround for menu
351   # command problem)
352   sleep(1.0);
353   deleteAll();
354   my $len = 20.0;
355
356   # Since there aren't proper generator commands yet, we use the preferences
357   # to control the duration.
358   # This preferences is not read in Audacity 2.2.x where duration
359   # is read from pluginsettings.cfg
360   my $origDuration = getPref("/CsPresets/NoiseGen_Duration");
361   setPref("/CsPresets/NoiseGen_Duration", $len);
362
363   # For each effect to test:
364   # * Create some stereo noise, and chop two pieces out of it
365   # * Add some labels, then apply the effect
366   # @splits determines where the splits are
367   my @splits = map {$_ * $len} (0.999, 0.2, 0.5, 0.6, 0.8, 0.1, 0.9);
368   my $trackNum = 0;
369   foreach my $effect (@clearAndPasters) {
370      menuCommand("NewStereoTrack");
371      selectRegion($trackNum, 0.0, $splits[0]);
372      doCommand("Noise:");
373      selectRegion($trackNum, $splits[1], $splits[2]);
374      menuCommand("SplitDelete");
375      menuCommand("AddLabel");
376      selectRegion($trackNum, $splits[3], $splits[4]);
377      menuCommand("SplitDelete");
378      menuCommand("AddLabel");
379
380      # Apply the effect
381      selectRegion($trackNum, $splits[5], $splits[6]);
382      doCommand($effect);
383
384      # Make and set the track name
385      my @splat = split(':', $effect);
386      my $name = $splat[0];
387      doCommand("SetTrackInfo: TrackIndex=$trackNum Type=Name Name=$name");
388      doCommand("Select: Mode=None");
389
390      $trackNum = $trackNum + 3;
391   }
392
393   # Set duration back to what it was before
394   setPref("/CsPresets/NoiseGen_Duration", $origDuration);
395}
396
397
398###############################################################################
399#  Effect testing                                                             #
400###############################################################################
401
402# A list of effects to test (could be got from Audacity in future)
403sub getEffects{
404
405   # (These ones will need special handling)
406   # AutoDuck
407   # Bass and Treble
408   # Repair
409   # NoiseRemoval
410   # ClickRemoval
411   # Paulstretch
412
413   # TimeScale (disabled because it's so slow)
414
415   my @effects = qw(
416      Amplify
417      ChangePitch
418      ChangeSpeed
419      ChangeTempo
420      Compressor
421      Echo
422      Equalization
423      FadeIn
424      FadeOut
425      Invert
426      Normalize
427      Phaser
428      Repeat
429      Reverse
430      TruncateSilence
431      Wahwah
432   );
433   return @effects;
434}
435
436# Create a chirp for an effect to be applied to.
437# The duration setting does not work in Audacity 2.2.x where duration
438# is read from pluginsettings.cfg
439sub generateBase{
440   my $genCmd = "Chirp";
441   my $duration = 30.0;
442   menuCommand("NewMonoTrack");
443   doCommand("$genCmd:");
444   my $desc = $genCmd . "-" . $duration . "s";
445   return $desc;
446}
447
448# Apply an effect and save the results (for use as reference output)
449sub saveEffectResults{
450   my $dirname = shift;
451   my $effect = shift;
452   deleteAll();
453
454   my $filename = $dirname . "/" . generateBase() . "-" . $effect . ".wav";
455   doCommand($effect);
456
457   printHeading("Exporting to $filename\n");
458   doCommand("Export: Mode=All Filename=$filename Channels=1");
459}
460
461# Apply an effect and compare the result to reference output
462sub doEffectTest{
463   my $dirname = shift;
464   my $effect = shift;
465
466   deleteAll();
467   my $filename = $dirname . "/" . generateBase() . "-" . $effect . ".wav";
468   doCommand("SetTrackInfo: TrackIndex=0 Type=Name Name=$effect");
469   doCommand($effect);
470   doCommand("Import: Filename=$filename");
471   doCommand("Select: Mode=All");
472   my @result = compareAudio(0.001);
473   return @result;
474}
475
476# Export reference copies of the effects in the list
477sub exportEffects{
478   my $exportDir = shift;
479   my @effects = getEffects();
480   foreach my $effect (@effects) {
481      saveEffectResults($exportDir, $effect);
482   }
483}
484
485# Test each of the effects in the list
486sub testEffects{
487   my $referenceDir = shift;
488   my %results = ();
489
490   my @effects = getEffects();
491   foreach my $effect (@effects) {
492      printHeading("Testing effect: $effect");
493      my @res = doEffectTest($referenceDir, $effect);
494      $results{ $effect }[0] = $res[0];
495      $results{ $effect }[1] = $res[1];
496   }
497
498   # Print out table of results
499   my $padLength = max(map { length($_) } @effects);
500
501   printHeading("Test results");
502   print "Effect name\tSamples\tSeconds\n\n";
503   for my $effect (keys %results) {
504      my $padded = sprintf("%-${padLength}s", $effect);
505      my $badSamples = $results{ $effect }[0];
506      my $badSeconds = $results{ $effect }[1];
507      print "$padded\t$badSamples\t$badSeconds\n";
508   }
509}
510
511# Print text with ascii lines above and below
512sub printHeading{
513   my $msg = shift;
514   my $line = "-" x length($msg);
515   print "$line\n$msg\n$line\n\n";
516}
517
518###############################################################################
519
520startUp();
521
522# Send some test commands
523
524exportEffects($effectTestDir);
525testEffects($effectTestDir);
526
527finish();
528