#!/usr/bin/perl -w # # $Id: musicd,v 1.17 2002/03/03 22:21:07 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 ':state'; 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; # whether or not we're currently paused my $paused = 0; # 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; $mp3->set_player_mode(&XA_PLAYER_MODE_OUTPUT_AUTO_CLOSE_ON_STOP); # 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(0); } } elsif ($input =~ m/^REMOVEGRP (\d+) (\d+)/) { my $sid = $1; my $count = $2; # remove the requested number of songs from the playlist starting # with the first song that matches the specified songid my $sidx = -1; my $i; my $yanked = 0; for ($i = 0; $i < @playlist; $i++) { if ($playlist[$i]->[1] == $sid) { $sidx = $i; splice(@playlist, $i, $count); # adjust the playing index if necessary if ($index < $i) { # nothing to do here } elsif ($index >= $i && $index < $i + $count) { # we removed the playing tune $yanked = 1; $index = $i-1; } else { # we removed tunes from before the currently playing # tune so we adjust index $index -= $count; } last; } } # now check to see if we've removed the playing tune if ($yanked) { play_next_tune(0); } if ($sidx >= 0) { print $cfh "200 Removed $count entries from position $sidx.\n"; } else { print $cfh "404 Unable to locate $sid in playlist.\n"; } } 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(1); } # 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 $sidx.\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 " . ($paused ? "paused" : "playing") . ": " . (defined $playing ? $playing->[1] : "") . "\n"; } elsif ($input =~ m/^STOP/) { $mp3->stop(); $paused = 0; # clear the paused flag undef $playing; 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(0); } # clear the paused flag $paused = 0; # 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(); $paused = 1; 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/^BACK/) { # stop this tune and skip to the previous one $mp3->stop(); play_next_tune(-1); print $cfh "200 Skipped current song.\n"; } elsif ($input =~ m/^SKIP/) { # stop this tune and skip to the next one $mp3->stop(); play_next_tune(1); 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 { my ($direction) = @_; my $pcount = @playlist; # bail if the playlist is empty if ($pcount == 0) { $index = -1; undef $playing; print "Empty playlist, twiddling.\n"; return; } # advance the playlist pointer either forward or backward (wrapping # around at the end because we always loop) $index = ($index + $pcount + $direction) % $pcount; $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(1); } 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; }