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

In my quest to create the MP3 daemon that can handle multiple song queues (and thus manage multiple instances of MPG321), I've ended up writing two modules to aid me. Audio::Play::MPG321 and Audio::Daemon::MPG321 are on CPAN and are needed for this script to work. Basically this program launches MPG321 every time a 'play' request comes in. Anyway, here's the code.
#!/usr/bin/perl ################### # Title # # By Da-Breegster # ################### use strict; use warnings; use Audio::Daemon::MPG321; use Config::Auto; use MP3::Info; use POSIX ":sys_wait_h"; use List::Util "shuffle"; use Cwd; our (@Players, $MixerPID, %Config, $Title); $| = 1; mkdir "/tmp/jmd" unless -d "/tmp/jmd"; open LOCK, "/var/lock/jmd" and do { kill 0, <LOCK> and die "JMD already running!"; }; open LOCK, ">/var/lock/jmd"; print LOCK $$; close LOCK; %Config = %{Config::Auto::parse("/etc/jmd")}; do { do { $MixerPID = fork() or exec("esd -nobeeps"); system("echo $MixerPID >> /root/esd"); system("esdctl unlock"); } if system("esdctl standbymode >> /dev/null") == 256; } if $Config{mixer} eq "esd"; $SIG{ALRM} = sub { return unless -s "/tmp/jmd/input"; open INPUT, "/tmp/jmd/input"; chomp(my $Input = <INPUT>); close INPUT; my ($Action, @Args); ($Action, @Args) = split(/\s+/, $Input); if ($Action eq "play" or $Action eq "add") { my @Songs = map { FullPath($_) } @Args; Play(@Songs) if $Action eq "play"; Add(@Songs) if $Action eq "add"; } elsif ($Action eq "rplay" or $Action eq "radd") { my @Songs = shuffle(@Args); @Songs = map { FullPath($_) } @Songs; Play(@Songs) if $Action eq "rplay"; Add(@Songs) if $Action eq "radd"; } else { Next() if $Action eq "next"; Prev() if $Action eq "prev"; Restart() if $Action eq "restart"; Forward(@Args) if $Action eq "forward"; Backward(@Args) if $Action eq "backward"; Pause() if $Action eq "pause"; Resume() if $Action eq "resume"; Toggle() if $Action eq "toggle"; Stop() if $Action eq "stop"; List() if $Action eq "ls"; $SIG{QUIT}->() if $Action eq "quit"; } }; $SIG{CHLD} = sub { my $Stiff; while (($Stiff = waitpid(-1, &WNOHANG)) > 0) { pop @Players; sleep($Config{lag}); Resume() if @Players; } }; $SIG{QUIT} = sub { unlink "/var/lock/jmd"; Stop() until (! @Players); kill "INT", $MixerPID if $MixerPID; open SONG, ">/tmp/jmd/song"; print SONG "[0]"; close SONG; open TIMES, ">/tmp/jmd/times"; print TIMES "0:00 (0:00)"; close TIMES; exit 0; }; $SIG{INT} = $SIG{QUIT}; sub FullPath { my $Song = shift; return $Song if $Song =~ m/^\//; my $CWD = getcwd; return "$CWD/$Song" if -f "$CWD/$Song"; return $Config{dir} . "/$Song" if -f $Config{dir} . "/$Song"; return; } sub Play { my @Songs = @_; Pause() if @Players; push @Players, new Audio::Daemon::MPG321 (@Songs); } sub Add { return unless @Players; $Players[-1]->add(@_); } sub Next { return unless @Players; $Players[-1]->next(); } sub Prev { return unless @Players; $Players[-1]->prev(); } sub Restart { return unless @Players; $Players[-1]->restart(); } sub Forward { return unless @Players; $Players[-1]->forward(shift); } sub Backward { return unless @Players; $Players[-1]->backward(shift); } sub Pause { return unless @Players; $Players[-1]->pause(); } sub Resume { return unless @Players; $Players[-1]->resume(); } sub Toggle { return unless @Players; $Players[-1]->toggle(); } sub Stop { return unless @Players; $Players[-1]->stop(); } sub List { open REPLY, ">/tmp/jmd/reply"; print REPLY "START\n"; foreach (@{$Players[-1]->{queue}}) { my $Prelude; if ($_ eq $Players[-1]->{queue}->[$Players[-1]->{pointer}]) { $Prelude = "> "; } else { $Prelude = " "; } print REPLY $Prelude . $_ . "\n"; } print REPLY "END\n"; close REPLY; } while (1) { select(undef, undef, undef, 0.5); next unless @Players; until ($Players[-1]->{player}->state() == 0) { $Players[-1]->{player}->poll(); if (my $Info = get_mp3tag($Players[-1]->{queue}->[$Players[-1]->{poi +nter}])) { $Title = $Info->{TITLE}; } else { $Title = $Players[-1]->{queue}->[$Players[-1]->{pointer}]; } open SONG, ">/tmp/jmd/song"; print SONG "[", $#Players + 1, "] ", $Title, "\n"; close SONG; open TIMES, ">/tmp/jmd/times"; print TIMES $Players[-1]->{player}->{sofar}, " (", $Players[-1]->{player}->{remains}, ")\n"; close TIMES; } $Players[-1]->{pointer}++; unless ($Players[-1]->{queue}->[$Players[-1]->{pointer}]) { Stop(); } else { $Players[-1]->load(); } }
Oh yeah, you'll also need mpg321, obviously. And set /etc/jmd to something like this: mixer: esd dir: /home/dabreegster/mp3 lag: 0 forward: 15 backward: 15 And you'll need esd to handle the whole /dev/dsp locking issue. Another mixer should work as long as mpg321 supports it. And here's the client script:
#!/usr/bin/perl ################### # Title # # By Da-Breegster # ################### use strict; use warnings; open PID, "/var/lock/jmd" or do { unless ($ARGV[0] eq "start") { print "JMD not running!\n"; exit 1; } }; no warnings; our $PID = <PID>; use warnings; close PID; my $Cmd = shift @ARGV; if ($Cmd eq "start") { system("jmd &"); exit 1; } elsif ($Cmd eq "ls") { # Dunno } elsif ($Cmd =~ m/(add|play)/) { my $Send = "$Cmd " . join(" ", @ARGV); $Send =~ s/\s$//; open CMD, ">/tmp/jmd/input"; print CMD $Send; close CMD; kill "ALRM", $PID; exit 1; } else { my $Send = "$Cmd " . join(" ", @ARGV); $Send =~ s/\s$//; open CMD, ">/tmp/jmd/input"; print CMD $Send; close CMD; kill "ALRM", $PID; exit 1; }
Just call it like this: 'jmc start; jmc play mp3/foo.mp3'. Anyway, it mostly works, except for some bugs, which I can't seem to locate the source of.

1) This is the major one. If you start up a new Player and let all the songs finish naturally, Stop() is called and it works fine. But if you call Stop() yourself, it'll pop @Players and stuff in $SIG{CHLD} and you can resume the queue you had playing before, but the while loop will totally freeze. I've inserted prints in there before and determined that it totally just stops. I have no clue what's going on.

2) When a queue finishes naturally or otherwise, Resume() is called on the appropriate element in @Players from $SIG{CHLD}, but it never actually resumes!

If I need to explain the code more, just tell me and I will. Thanks.

Replies are listed 'Best First'.
Re: Yet another music-related queue question
by thcsoft (Monk) on Jun 11, 2005 at 11:53 UTC
    err... are you really expecting us to debug your code?

    language is a virus from outer space.
      No, preferably provide some sort of clue as to why a while loop would stop if the player stops in a way other that naturally. I have a guess. Underneath, poll() is talking to mpg321 using IO::Open2 and stuff like that. If you close a program that has a IO::Handle object and stuff, could that make the while loop freeze? I'll test it out in a minute. Sorry, again. I didn't mean to sound like I was expecting somebody to just debug it for me. I was just looking for some clues. Besides, the program is usable (I run it all the time) and I thought somebody might be intersted.
        dabreegster,
        I have rewritten your code to hopefully make it more digestible. There is just too much there for someone to look at and guess what the problem might be. You don't check the return code of any of your open commands nor do you provide comments of how the external files interact with your code. IOW, even giving you "just a pointer" requires having a basic understanding of how the code works. As it stands - that would require a signficant investment of personal time. I have no idea if it will even run, but it might help someone else help you.

        Cheers - L~R

        I'm a big music head myself, and one thing you could do to get my interest in this module is to add more documentation. Aside from the title blocks, the only comment I see in there is '# Dunno'. Start by adding a Plain Old Documentation so I can perldoc this code. Give me a no-brainer description of what this does, and how to use it so I don't have to guess at it's function from reading the code.
        I fixed it. For some reason, making mpg321 exit in the middle of reading or something was making everything freeze. So I changed Stop() to set a global variable to 1 and in the until loop, I added 'last if $Stop'. Right outside that, I tested for a true $Stop value and set it to false and forced the player to exit there. I'll post the code later.