#!/usr/bin/perl -w # # $Id: musicd,v 1.11 2001/07/26 19:26:55 mdb Exp $ # # MP3 audio server. Manages playlists of mp3 files that live on a local # filesystem. The playlists can be manipulated by connecting to the server # on a port and issuing simple text commands. use strict; use IO::Socket; use IO::Select; use MPEG::MP3Play; use Data::Dumper; use Getopt::Long; my $usage = "Usage: $0 [--bind_addr x.x.x.x]\n"; # this is what's currently being played my $playing; # this is our primary playlist my @playlist; # this is our current index into the playlist my $index = -1; # this is our handle on the MP3 player object my $mp3; # get our options my $bind_addr; # bind address GetOptions("bind_addr:s" => \$bind_addr); # validate the bind address die $usage if (defined $bind_addr && $bind_addr !~ /\d+\.\d+\.\d+\.\d+/); main: { # create the server part of things my $sock; if (defined $bind_addr) { $sock = new IO::Socket::INET(LocalHost => "$bind_addr:2500", Proto => "tcp", Listen => SOMAXCONN, Reuse => 1); } else { $sock = new IO::Socket::INET(LocalPort => 2500, Proto => "tcp", Listen => SOMAXCONN, Reuse => 1); } $sock or die "Unable to create server socket: $!\n"; # create our MP3 interface $mp3 = new MPEG::MP3Play; # manage our various inputs my $sel = new IO::Select($sock); my $mph = $mp3->get_command_read_pipe(); $sel->add($mph); # do our main loop thang my (@ready, $fh); while (@ready = $sel->can_read()) { foreach $fh (@ready) { if ($fh == $mph) { $mp3->process_messages_nowait(); } elsif ($fh == $sock) { $sel->add($sock->accept()); } else { # if handle_client() returns false, we remove this client from # the select list and close the socket if (!handle_client($fh)) { $sel->remove($fh); $fh->close(); } } } } } # handles input from a client sub handle_client { my ($cfh) = @_; # see what the client has to say my $input = <$cfh>; if (defined $input) { # remove trailing cr/nl $input =~ s:[\r\n]+$::g; } else { # bail out if the socket is closed return 0; } # parse and process the command if ($input =~ m/^APPEND (\d+) (\d+) (.*)/) { # append this song to the playlist my $eid = $1; my $sid = $2; my $path = $3; # make sure the file they provided exists and is readable if (-r $path) { push @playlist, [ $eid, $sid, $path ]; print $cfh "200 Appended '$eid:$sid:$path' to playlist.\n"; } else { print $cfh "404 Unable to read playlist candidate: '$path'\n"; } # if we're not playing a song, start playing now that we have a # playlist with songs in it if (!defined $playing) { play_next_tune(); } } elsif ($input =~ m/^REMOVE (\d+)/) { my $sid = $1; # if we're playing the song to remove, skip to the next song if ($sid == $playing->[1]) { play_next_tune(); } # remove this songid from the playlist my $sidx = -1; my $i; for ($i = 0; $i < @playlist; $i++) { if ($playlist[$i]->[1] == $sid) { $sidx = $i; splice(@playlist, $i, 1); # adjust the playing index if necessary $index -= 1 if ($i < $index); last; } } if ($sidx >= 0) { print $cfh "200 Removed $sid from position $i.\n"; } else { print $cfh "404 Unable to locate $sid in playlist.\n"; } } elsif ($input =~ m/^CLEAR/) { # clear out the current playlist and reset the playing index @playlist = (); $index = -1; print $cfh "200 Playlist cleared.\n"; } elsif ($input =~ m/^PLAYLIST/) { print $cfh "200 Playlist songs: " . @playlist . " current: " . (defined $playing ? $playing->[1] : "") . "\n"; my $entry; foreach $entry (@playlist) { print $cfh join("\t", @{$entry}) . "\n"; } } elsif ($input =~ m/^PLAYING/) { print $cfh "200 Currently playing: " . (defined $playing ? $playing->[1] : "") . "\n"; } elsif ($input =~ m/^STOP/) { $mp3->stop(); print $cfh "200 Music stopped.\n"; } elsif ($input =~ m/^PLAY/) { # if we've already queued up a song, play it if (defined $playing) { $mp3->play(); } else { # otherwise queue one up to play play_next_tune(); } # and report back based on whether or not we successfully queued # up a song for playing if (defined $playing) { print $cfh "200 Music started.\n"; } else { print $cfh "501 Can't play: playlist empty.\n"; } } elsif ($input =~ m/^PAUSE/) { $mp3->pause(); print $cfh "200 Music paused (use PLAY to unpause).\n"; } elsif ($input =~ m/^SKIPTO (\d+)/) { my $sid = $1; # stop this tune and skip to the specified tune $mp3->stop(); if (play_this_tune($sid)) { print $cfh "200 Skipped to $sid.\n"; } else { print $cfh "404 Unable to locate song $sid.\n"; } } elsif ($input =~ m/^SKIP/) { # stop this tune and skip to the next one $mp3->stop(); play_next_tune(); print $cfh "200 Skipped current song.\n"; } elsif ($input =~ m/^CLOSE/) { print $cfh "200 Bye bye.\n"; return 0; } elsif ($input =~ m/^EXIT/) { print $cfh "200 Bye bye.\n"; exit(0); } else { print $cfh "500 Unknown command: '$input'\n"; } return 1; } sub play_next_tune { # bail if the playlist is empty if (@playlist == 0) { $index = -1; undef $playing; print "Empty playlist, twiddling.\n"; return; } # advance the playlist pointer (wrapping around at the end because we # always loop) $index = ($index + 1) % @playlist; $playing = $playlist[$index]; # open it $mp3->open($playing->[2]); # and play it $mp3->play(); } sub play_this_tune { my ($sid) = @_; my $i; # bail if the playlist is empty if (@playlist == 0) { $index = -1; undef $playing; return 0; } # advance the playlist pointer (wrapping around at the end because we # always loop) for ($i = 0; $i < @playlist; $i++) { $index = ($index + 1) % @playlist; $playing = $playlist[$index]; if ($playing->[1] == $sid) { last; } } # open it $mp3->open($playing->[2]); # and play it $mp3->play(); # let them know if we found the tune return ($playing->[1] == $sid); } # message handlers package MPEG::MP3Play; sub msg_notify_player_state { my ($mp3, $msg) = @_; # deeee-bug! # print "state: " . Dumper($msg) . "\n"; # play the next tune if we hit the end of this one if ($msg->{state} == &XA_PLAYER_STATE_EOF) { main::play_next_tune(); } return 1; } sub msg_notify_error { my ($mp3, $msg) = @_; print "error: " . Dumper($msg) . "\n"; return 1; } sub msg_notify_debug { my ($mp3, $msg) = @_; if ($msg->{code} == &XA_ERROR_NO_SUCH_FILE) { print "unable to load: $playing\n"; } else { print "debug: " . Dumper($msg) . "\n"; } print "nsf: " . &XA_ERROR_NO_SUCH_FILE . "\n"; return 1; }