http://www.perlmonks.org?node_id=1203062

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

Hello!

I'm making a little game using Tk, and now I'm trying to add music to it. It's surprisingly difficult.

I would like to be able to start, stop, and change tracks at any point in the program. Ideally also play two sounds at the same time (music and sound effects), but let's take it one step at a time.

I've tried using a somewhat complicated set of forks, which exec() a system tool for playing sounds (afplay, on the Mac). But that's quite clumsy, system-dependent, and doesn't stop playing when the main program quits, which would be pretty annoying.

I've also tried using Audio::Play::MPG123, which sounded promising, but the very first line
$player = Audio::Play::MPG123->new;
gives an error I don't understand:
open3: exec of mpg123 -R --aggressive  failed

Now I'm starting to consider calling an external Applescript, which is basically the highest level of desperation. Any other ideas?

Replies are listed 'Best First'.
Re: Playing sounds -- Tk
by Discipulus (Canon) on Nov 09, 2017 at 17:43 UTC
    Ok if it feasible with Tk zentara has already done it.. but since he seems absorbed in some deep meditation..

    I'll link to some of his posts: one using SDL Tk Game Sound demo and another using Audio::DSP (that seems a bit aged module) Re^5: sound and gamepad support in Perl?

    PS I modified the title of my answer to metion Tk, and please share your improvements!

    PPS succesfully tested the first demo ( updated version ) with Perl 5.24 and SDL 2.546 incredible mixing capacities!!

    L*

    There are no rules, there are no thumbs..
    Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
      Hi, I have been temporarily awakened from my meditation to concur with Discipulus, SDL is the way to go. SDL is designed to play sounds, and combined with Tk keypress controls, you can get very good responsiveness. I would be concerned that other methods mentioned would result in noticeable time delays in starting/stopping sounds. I return to the Eternal 0m now...... :-)

      I'm not really a human, but I play one on earth. ..... an animated JAPH
Re: Playing sounds
by 1nickt (Canon) on Nov 09, 2017 at 17:07 UTC

    Hi,

    $player = Audio::Play::MPG123->new;
    gives an error I don't understand:
    open3: exec of mpg123 -R --aggressive failed

    That shows that the perl wrapper Audio::Play::MPG123 is attempting to launch the system binary mpg123 and failing. Dollars gets you donuts it's because you don't have it installed.

    On my Linux the command is:

    $ sudo apt-get install mpg123 libmpg123-dev
    ... you'll have to find the equivalent for MacPorts or whatever package manger you are using.

    When you read the docs for a module and it says something like:

    "This is a frontend to the mpg123 player. It works by starting an external mpg123 process with the -R option and feeding commands to it."
    ... it's pretty likely that you'll need a system application.

    Hope this helps!


    The way forward always starts with a minimal test.
Re: Playing sounds (updated)
by haukex (Archbishop) on Nov 09, 2017 at 16:45 UTC
    which exec() a system tool for playing sounds (afplay, on the Mac). But that's quite clumsy, system-dependent,

    Unfortunately, any solution that runs an external command will most likely be system-dependent. Even without looking at Audio::Play::MPG123, I strongly suspect based on the error message that it's also just trying to run an external command. Update: After having looked into this a little more, let me rewrite that paragraph. Calling an external program of course feels a little clunky and inelegant, but at the moment I am not aware of cross-platform Perl modules for playing audio that don't make use of any external tools (perhaps I've just overlooked them, if someone else knows of one, please share - Update 3: see Discipulus's and zentara's comments about SDL here). So perhaps calling an external program is not such a bad choice, if you can run it in the background and control its execution. Audio::Play::MPG123 uses mpg123, which like SoX below also says it is cross-platform, so perhaps you just don't have it installed (Update 2: as 1nickt also pointed out in the meantime). Instead of that module, you can also use the solution I linked to below.

    Perhaps this will help in being a small piece in your puzzle: I showed some working code how to run a program that plays a sound while your script keeps running, and how to interrupt playback, using IPC::Run at Re^5: Killing a child process (the rest of the thread may be an interesting read too). The script uses the play tool from SoX, which according to its website is available for *NIX including Mac OS X, as well as Windows.

Re: Playing sounds
by Tux (Canon) on Nov 09, 2017 at 23:17 UTC
Re: Playing sounds
by karlgoethebier (Abbot) on Nov 09, 2017 at 19:04 UTC

    Taking a look at VLC might be an option. Regards, Karl

    «The Crux of the Biscuit is the Apostrophe»

    perl -MCrypt::CBC -E 'say Crypt::CBC->new(-key=>'kgb',-cipher=>"Blowfish")->decrypt_hex($ENV{KARL});'Help

Re: Playing sounds
by Chuma (Scribe) on Nov 09, 2017 at 18:48 UTC

    Thanks! That makes sense, I'd have to install mpg123 to get that to work. But to have any hope of running my program on other machines, never mind other systems, I'd rather avoid that. If it's running an external process anyway, I might as well do it with afplay, right?

    I'll show you what I have so far. It's a pretty primitive idea – the music script reads a line from a file, telling it which audio file to play, then waits for that to finish, and does it again.

    for(1..5){ # should be an infinite loop $pid=fork(); if($pid==0){ my $m=getmusic(); if($m){exec 'afplay "'.$m.'"' or die} else{exec 'sleep 1'} } while (wait() != -1){ my $m=getmusic(); unless($m){ # somehow stop the music currently playing }else{ sleep 1; } } } sub getmusic{ open MFILE, "$path/music.txt" or die; my $m=<MFILE>; chomp $m; close MFILE; return $m; }

    What I would need is: First, a way to call this from my main Perl program, and have it exit when that one does. Second, a way to get this script to kill the child process when it detects it's time to stop, in this example because the file is empty, as you see in the code.

    Some of the links you posted also look interesting, although I don't understand much of them. I'll continue looking there too.

      I'm not sure which node you are replying to (there are per-node "reply" links next to each post), but I'll reply anyway.

      But to have any hope of running my program on other machines, never mind other systems, I'd rather avoid that.

      AFAIK sound is always going to be an OS-dependent thing, so any Perl code would at some level have to access an external OS-dependent library or tool. Yes, running an external program just to play a sound is a bit wasteful, but you said this was for a game, and unless you're playing hundreds of sounds a second, or you need high precision in playback (precise timing down to the millisecond of when the sounds are played), or this is going to be run on very low-end machines, then IMHO using an external program is probably fine, especially for background music. (Update: On the other hand, see Discipulus's and zentara's comments about SDL here.)

      If it's running an external process anyway, I might as well do it with afplay, right?

      Personally I'd look at it differently: If I'm going to be running an external process anyway, I'd like to use the same one no matter which OS, then I will only have to handle one interface (command line arguments etc.). Then, the "only" issues I'd have to worry about are possibly pathname issues, which can be handled with a cross-platform tool like Path::Class or at least the core File::Spec, and making it as easy as possible for a user of my program to install that external tool. Perhaps you can try installing the previously mentioned mpg123 or SoX on different machines to see how easily their installers handle, how easy it is for your Perl script to call those external programs after their installation (potential PATH issues etc.), and so on.

      You might just simply document for your users, "if you want sound support, install package X and set configuration option Y to the installation path", and have your program gracefully handle the case of the sound playback tool not being installed (if having no sound is an option for you). Or, if your program has an installer, and you wanted to go this far, the installers of the audio software may support a "quiet" installation, which you can run during your program's installation, and depending on the license terms you might even distribute the audio package contained within yours.

      As for your code, I don't quite understand why you want to go through an external file - is that for interprocess communication, or will your user be editing that file? Note that both aforementioned players can play multiple files one after the other if given on the command line, and play a single file multiple times (e.g. play sound.mp3 repeat 2 and mpg123 --loop 3 sound.mp3 both play the same file three times). If you want to monitor the playback and re-start after it is finished, then yes, one way to do that would be to fork a child and have it block until the sound stops playing, but on the other hand, since I assume your code is probably based on an event loop, then regularly polling the state of the playback of the music and re-starting if necessary is also an option (that's what I do in my code below). Anyway, as a fun little project I whipped up an OO package based on the code I linked to earlier (only tested on Linux so far):

      { # ---8<--- put this in file Sound.pm if you like ---8<--- package Sound; use warnings; use strict; use Carp; use IPC::Run qw/start/; use Scalar::Util qw/weaken/; our $DEFAULT_PLAYER = ['play','-q']; # SoX my @all_sounds; my %NEW_KNOWN_ARGS = map {$_=>1} qw/file player/; sub new { my $class = shift; my %self = (player => $DEFAULT_PLAYER); $self{file} = shift if @_%2; my %args = @_; $NEW_KNOWN_ARGS{$_} or croak "new: unknown arg '$_'" for keys %args; carp "new: file specified twice (or odd number of arguments)" if defined $self{file} && defined $args{file}; defined $args{$_} and $self{$_} = $args{$_} for keys %args; croak "new: player must be a nonempty arrayref" if ref $self{player} ne 'ARRAY' || @{$self{player}}<1; croak "new: no file specified" unless defined $self{file}; my $self = bless \%self, $class; push @all_sounds, $self; weaken $all_sounds[-1]; return $self; } sub stop_all_Sounds { # package function, stops *all* Sounds globally! defined and $_->stop for @all_sounds; } sub play { my $self = shift; $self->stop if $self->is_playing; $self->{harness} = start [ @{$self->{player}}, ref $self->{file} eq 'ARRAY' ? @{$self->{file}} : $self->{file} ]; return $self; } sub is_playing { my $h = shift->{harness}; return unless defined $h; return 1 if $h->pumpable; $h->finish; return; } sub wait { my $h = shift->{harness}; return unless defined $h; return $h->finish; } sub stop { my $h = shift->{harness}; return unless defined $h; $h->signal('INT'); return $h->finish; } sub DESTROY { shift->stop; # take this opportunity to clean up the array a bit @all_sounds = grep {defined} @all_sounds; # need to re-weaken as per Scalar::Util docs weaken $_ for @all_sounds; } 1; } # --->8--- end package Sound --->8--- BEGIN { $INC{'Sound.pm'}++ } # ignore; just for inlining the package # ----- ----- Demo ----- ----- use warnings; use strict; use Sound; # Note: my "short" sound is ~2.3s long my $snd_short = Sound->new('/tmp/short.mp3'); my $snd_long = Sound->new('/tmp/long.mp3'); local $SIG{INT} = sub { print "Caught SIGINT, stopping and exiting...\n"; Sound->stop_all_Sounds(); exit }; print "Playing long sound in background, and sleeping 10s...\n"; $snd_long->play; sleep 10; print "Playing short sound, blocking until done...\n"; $snd_short->play->wait; print "Sleeping 3s...\n"; sleep 3; print "Playing short sound in background, not sleeping...\n"; $snd_short->play; print "Re-starting playback of long sound...\n"; $snd_long->play; print "Waiting for playback of short sound to finish...\n"; $snd_short->wait; print "Sleeping 5s (long sound playback will continue)...\n"; sleep 5; # $snd_long will stop playing when the variable goes out of scope # (in this case, when the script ends) my $snd_two = Sound->new( # multiple files played one after another file => ['/tmp/short.mp3', '/tmp/short.mp3'], # player can be specified with absolute path, or no path # if the binary is in the PATH environment variable player => ['/usr/bin/mpg123','-q'] ); print "Playing second short sound in background...\n"; $snd_two->play; for (1..10) { # loop for roughly 10 seconds print "Monitoring playback (loop $_, sleeping)...\n"; sleep 1; if (not $snd_two->is_playing) { print "Playback ended, re-starting with long sound...\n"; $snd_two = Sound->new('/tmp/long.mp3')->play; } } print "Stopping second sound...\n"; $snd_two->stop; print "Script done.\n";

      (BTW, yet another option for commandline playback is from the powerful FFmpeg or libav packages, the command line tools ffplay/avplay can be used like so: avplay -vn -nodisp -autoexit -nostats -loglevel quiet sound.mp3, I believe the options are identical for ffplay.)