Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

making something happen in real time

by bcrowell2 (Friar)
on Nov 06, 2005 at 18:54 UTC ( [id://506136]=perlquestion: print w/replies, xml ) Need Help??

bcrowell2 has asked for the wisdom of the Perl Monks concerning the following question:

O Monks,

I have a simple perl script I've written that acts as an accelerating musical metronome. The idea is that if I need to practice, say, a few bars out of a piece of music to be able to play it faster, the metronome clicks at faster and faster tempos, while I'm sitting away from the computer.

Currently I'm using code like this:

select undef,undef,undef,$t;
to wait the appropriate amount of time between clicks. (The sleep function didn't have high enough resolution.) Also, there's a little bit of processing time required in order to play the sound, which I estimate on the fly using calls to `date +%s` and `date +%N`. In fact, this processing time is normally very small on my system (less than a millisecond).

OK, so my software is working great, but there's one problem: when xscreensaver comes on, it starts gobbling up cpu like crazy. My on-the-fly correction detects this, and tries to compensate for it, but it only succeeds on the average, so the clicks become very uneven. The silly part of all this is that there's really no need for my script and xscreensaver to be competing like crazy for cpu, because my script is using less than 1% of the time-averaged cpu.

All I really want is to be able to make sure that a shell command ("play foo.wav") happens at some well-defined time in the future, even if a cpu-hog process starts butting in. Is there any way to do this? (I also need to get the return value back from the shell command, because when the user hits control-C during the execution of that child process, the signal goes to the child, not the parent.)

This is all on Linux, and I'm not particularly concerned about portability to non-Unix systems, although I'd like it to avoid gratuitous Linuxisms. (I'm also wondering if there's some more portable way to do the equivalent of `date +%s`, which is a GNU extension to the date command.)

Of course I can temporarily kill the xscreensaver daemon, but that's a hassle, and I'll forget to do it. Likewise I could log in as root and run my script as nice --18, but that's obviously a kludge.

TIA!

Since people have requested it, here's my code:

#!/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.og +g"; 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 efficie +ncy; # 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 upd +ated later, based on real-time # data about how fast we're actually going; on a modern system, putt +ing 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 tim +es $t = clock(); if (defined $last_t) { my $real_dt = $t-$last_t; # amount of time that actually elaps +ed #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=$co +rrection, 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 sho +rt 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 = <STDIN>; 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;

Replies are listed 'Best First'.
Re: making something happen in real time
by lidden (Curate) on Nov 06, 2005 at 19:31 UTC
    Seems to me you should use Time::HiRes. I use that one combined with Tk's after metod and it works well for me but my prog can have larger errors then yours.
      Seems to me you should use Time::HiRes.
      Ah, thanks, that's helpful! That fixes the portability problem with using `date`. Calling Time::HiRes::usleep instead of using select didn't fix the latency problem, though.
Re: making something happen in real time
by Zaxo (Archbishop) on Nov 06, 2005 at 19:03 UTC

    It's obviously crazy to have xscreensaver walking on any unattended processes. Not a kluge (it's what nice is for), modify the xscreensaver startup command to run niced +19.

    A few years ago there was a lot of work on improving linux scheduling for real-time applications (google real time linux), but I don't know the state of those projects these days. They were also motivated by music and video applications for the most part.

    After Compline,
    Zaxo

      Hmm, good point, but xscreensaver runs at nice +10 by default. I think it's more of a problem with latency. My script spends 99% of its time asleep, so while it's sleeping the scheduler says, "OK, nobody's using any cpu. Even though xscreensaver is running at nice +10, we'll give it a chunk of cpu, because nobody else wants any." Then when my higher-priority process pops back up and demands some cpu time, there's a few hundred milliseconds of latency before its request is granted.
Re: making something happen in real time
by bageler (Hermit) on Nov 06, 2005 at 20:55 UTC
    you could select more frequently and have it check for $t intervals to play the tone, that way your script would be the cpu hog and demand more attention from the system.
      Well, I could just spin in a tight loop instead of using select, but I'd like to avoid having my program be a cpu hog when it shouldn't have any need to be. I don't think sleeping for shorter intervals will help, because it just seems to require several hundred milliseconds to get the cpu back from xscreensaver, and that's a lot longer than the timing resolution I need.
        Well, it sounds like that's what you should do to me. If there's a swinging blade (xscreensaver) and you want to dodge between it (beep) you have to have better time resolution than the period of the pendulum. Maybe you could increase the resolution only near the time when the screensaver should be kicking in? Then, for the majority of the time, you can be less of a cpu hog. That would still leave you vulnerable to anything else that might be going on in your system that also has a high resolution.
Re: making something happen in real time
by sgifford (Prior) on Nov 07, 2005 at 01:05 UTC
    It might be faster to find a module to play the sound, instead of running an external process. It takes more time to start up a new process than for your program to do something itself.

    You could try waking up slightly before you actually want to play the sound, then reading the sound into memory, going into a tight loop briefly, or doing something else that will make sure your program is all ready to go by the time it needs to be.

    You could also try to shut off, pause, or kill xscreensaver from your script.

      It might be faster to find a module to play the sound, instead of running an external process. It takes more time to start up a new process than for your program to do something itself.
      The time to start up a new process is short and predictable. My problem is with latency, which is a long and unpredictable time. (I've also had a lot of problems with Audio::Play in the past, one of which is that it doesn't compile on my Debian system, and it's not available as a precompiled Debian package.)

      You could try waking up slightly before you actually want to play the sound, then reading the sound into memory, going into a tight loop briefly, or doing something else that will make sure your program is all ready to go by the time it needs to be.
      Sure, I'm just trying to find a way to make my program not be a cpu hog.

        If latenecy is your problem, you probably want to patch your kernel to be low latency. Go to Con Kolivas kernel patch , and do groups.google searches for discussion about it. It is widely used for people using their computers for audio work.

        But I also agree with sgifford that your should try to streamline your playing of the audio. I would try to put the audio in a format that you can store in memory and write directly to /dev/dsp, the Audio::DSP module works well.


        I'm not really a human, but I play one on earth. flash japh
Re: making something happen in real time
by ambrus (Abbot) on Nov 06, 2005 at 19:35 UTC

    Before every tick, get the current time, and wait whatever time is left till the next tick.

    Here's a simple example.

    perl -we 'use Time::HiRes qw"time sleep"; use IO::Handle; $length = 60 +/$ARGV[0]; my $next = time; for (;;) { $next += $length; $now = time; + sleep($next - $now); printflush STDOUT "\a."; }' 80

    Of course, this can still be inexact if the load is high (especially if the system is doing io heavily).

    On the +%s thing, there's perl -we 'print time() . "\n";'.

      Before every tick, get the current time, and wait whatever time is left till the next tick.
      That's essentially what I'm already doing.

      Of course, this can still be inexact if the load is high (especially if the system is doing io heavily).
      And that's why it doesn't work :-)

      On the +%s thing, there's perl -we 'print time() . "\n";'
      But I need better resolution than one second.

        But I need better resolution than one second.

        Oh, sorry, didn't listen carefully. Then use Time::HiRes, which is a core module on newer perls:

        perl -we 'use Time::HiRes "time"; print time, "\n";'
        or the gettimeoftheday function from the same module, or (on linux)
        perl -we 'defined(syscall 78, ($d = pack "x100"), 0) or die "panic: ge +ttimeofday failed $!"; ($s, $u) = unpack "l!l!", $d; printf "%d.%06d\ +n", $s, $u;'
        but 156 instead of 78 on solaris, and 116 (untested) instead of 78 on freebsd, (Update:) 96 instead of 78 on linux-x86_64.

        Update:

        Before every tick, get the current time, and wait whatever time is left till the next tick.
        That's essentially what I'm already doing.
        My point is that you have to ask for the time before every sleep. I don't see your code, so you might already be doing that. Of course, if you spawn an external process for querying the time, then that's slow, so don't do that.
Re: making something happen in real time
by GrandFather (Saint) on Nov 08, 2005 at 01:10 UTC

    A completely different way to do it is to set up code to play looped sound. I don't know Linux but I bet there is a play equivelent module that will let you do that. Then you set up a sound buffer with the tick and an appropriate amount of silence for the rate you require initially. Then, every update period, reduce the padding and update the sound buffer.

    Now your script can perform a much less frequent update and can be sure that the tick rate will be precise, albeit with a glitch if you can't manage a synchronous update of the sound buffer.


    Perl is Huffman encoded by design.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://506136]
Approved by Tanktalus
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others taking refuge in the Monastery: (5)
As of 2024-04-18 15:07 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found