#!/usr/bin/perl # dependencies: # sox ("play" command) # festival (text to speech) use strict; use POSIX; use Time::HiRes; our $ogg_file = "/usr/share/apps/accelerando/sounds/metronome_click.ogg"; our $click_length = .0507; # seconds, the length of the ogg file our $decompressed_sound_file = POSIX::tmpnam().'.wav'; # ...temporary decompressed copy of the ogg file, for efficiency; # cleaned up in the END block my $cmd = "sox $ogg_file $decompressed_sound_file"; system($cmd)==0 or die "Error executing this command: $cmd"; # The following only really works if a child process isn't running. If it is, the child gets the signal, # and we just have to test the return value. $SIG{INT} = sub{ clean_up()}; $SIG{QUIT} = sub{ clean_up()}; my $player_delay = 0; # estimated time to play the wave file, in seconds; this will be updated later, based on real-time # data about how fast we're actually going; on a modern system, putting this at zero seems to # have no observable effect on the tempo my $initial = ask("Initial tempo",60); my $timesig = ask("Number of beats per bar",4); my $bars = ask("Number of bars before increasing the tempo",999999); my $add = ask("Amount to add to the tempo after the first time",0); my $between = ask("Beats between changes of tempo",1); if (1) { time_delay(7); # time for the musician to get ready, in seconds text_to_speech("ready")==0 or clean_up(); time_delay(1); text_to_speech("go")==0 or clean_up(); time_delay(1); } my $frac = 1+$add/$initial; print "The tempo will be increased by a factor of $frac each time.\n"; for (my $tempo=$initial; $tempo<500; $tempo*=$frac) { print int($tempo)." beats per minute\n"; text_to_speech(int($tempo))==0 or clean_up(); my $dt = 60/$tempo; my ($t,$last_t); for (my $bar=1; $bar<=$bars; $bar++) { for (my $beat=1; $beat<=$timesig; $beat++) { click()==0 or clean_up(); my $delay = $dt - $click_length - $player_delay; # seconds time_delay($delay); # because sleep() doesn't work for short times $t = clock(); if (defined $last_t) { my $real_dt = $t-$last_t; # amount of time that actually elapsed #print "real_dt=$real_dt\n"; my $correction = $real_dt-$dt; # positive if we fell behind if ($correction>.05) {$correction=.05} # don't let it go crazy if the cpu just got busy for a second $correction *= .5; # avoid undamped oscillations, etc. $player_delay += $correction; if ($player_delay<0) {$player_delay=0} #print "real_dt=$real_dt, player_delay=$player_delay, corr=$correction, player_delay=$player_delay\n"; } $last_t = $t; } } time_delay($dt*$between); # seconds } exit; # automatically does a clean_up(), in END{} block #---------------------------------------------------------------------------------- sub time_delay { my $t = shift; # seconds #select undef,undef,undef,$t; # because sleep() doesn't work for short times Time::HiRes::usleep($t*1_000_000); } sub ask { my $prompt = shift; my $default = ''; my $show_default = ''; if (@_) {$default=shift; $show_default=" ($default)"} print "$prompt${show_default}?\n"; my $answer = ; chomp $answer; if ($answer eq '') {$answer=$default} return $answer; } sub click { my $cmd = "play --silent $decompressed_sound_file"; # --silent means not to print anything to stdout my $r= system($cmd); if ($r!=0) {print "Return code $r from $cmd\n"} return $r; } BEGIN { my $startup_time = seconds_since_epoch(); sub clock { my $s = seconds_since_epoch(); return sprintf "%d.%09d",($s-$startup_time),time_nanoseconds(); } sub seconds_since_epoch { # return `date +%s`; # GNU only my ($s,$usec) = Time::HiRes::gettimeofday(); return $s; } sub time_nanoseconds { # the nanoseconds part of the time since the epoch # return `date +%N`; my ($s,$usec) = Time::HiRes::gettimeofday(); return $usec*1000; } } sub text_to_speech { my $text = shift; my $cmd = "echo '$text' | festival --tts"; my $r= system($cmd); if ($r!=0) {print "Return code $r from $cmd\n"} return $r; } sub clean_up { unlink $decompressed_sound_file; exit; } END { clean_up(); } 1;