To use the program, give a directory with music files as the first command line argument.
#!/usr/bin/perl use strict; use warnings; use POE qw(Component::Player::Musicus Wheel::Curses); use Curses; my $dir = shift; $dir = "." unless defined($dir); die "$dir is not a directory" unless -d $dir; opendir(DIR, $dir) or die $!; my @music = grep { -f "$dir/$_" } sort grep /^[\x20-\x7E]+$/, readdir(DIR); closedir DIR; POE::Session->create( inline_states => { _start => \&start, musicus_play => \&musicus_play, send_getpos => \&send_getpos, play => \&play, stop => \&stop, pause => \&pause, unpause => \&unpause, getinfocurr => \&getinfocurr, getpos => \&getpos, setpos => \&setpos, error => \&error, getlength => \&getlength, quit => \&player_quit, update_all => \&curses_refresh, got_input => \&curses_input, update_song => \&update_song, update_list => \&update_list, update_time => \&update_time, }, ); POE::Kernel->run; sub start { my ($kernel, $heap) = @_[KERNEL, HEAP]; $kernel->alias_set('console'); # Initialize everything %$heap = ( musicus => POE::Component::Player::Musicus->new( alias => 'console', delay => 100000 ), songinfo => { length => 0, pos => 0, marker => 0, artist => '', title => '', album => '', track => '', year => '', date => '', genre => '', comment => '', }, list_info => { top => 0, cursor => 0, }, playing => 0, paused => 0, gotinfo => 0, ); $heap->{curses} = POE::Wheel::Curses->new(InputEvent => 'got_input'); $kernel->yield('update_all'); $kernel->yield('musicus_play', 0); } # Musicus event handlers sub play { my ($heap, $kernel, $plugin) = @_[HEAP, KERNEL, ARG0]; $heap->{playing} = 1; $heap->{paused} = 0; $kernel->yield('updatedisplaystatus'); # Start getting position almost immediately, but put in a delay in case # we get a bunch of new songs started right after each other, then # there (hopefully) won't be a lot of extra getpos's $kernel->delay('send_getpos', .1); } sub stop { my ($heap, $kernel) = @_[HEAP, KERNEL]; $kernel->delay('send_getpos'); # No need to get position while stopped $heap->{playing} = 0; } sub pause { my ($heap, $kernel) = @_[HEAP, KERNEL]; $kernel->delay('send_getpos'); # No need to get position while paused $heap->{paused} = 1; } sub unpause { my ($heap, $kernel) = @_[HEAP, KERNEL]; $heap->{paused} = 0; $kernel->yield('send_getpos'); # Start getting position again } sub getinfocurr { my ($heap, $kernel, $info) = @_[HEAP, KERNEL, ARG0]; unless ($info->{title}) { # If the title is the same as the file name, # get rid of the extension and dir $info->{title} = $info->{file}; $info->{title} =~ s/^$dir\///; $info->{title} =~ s/\.[^.]+$//; } # Some XMMS plugins give better info for this command than getinfocurr $heap->{musicus}->getlength; # Add in the new information while preserving what's already there $heap->{songinfo} = {%{$heap->{songinfo}}, %$info}; $kernel->yield('update_song'); $kernel->yield('update_time'); } sub getlength { my ($heap, $length, $kernel) = @_[HEAP, ARG0, KERNEL]; $heap->{songinfo}{length} = $length; $kernel->yield('update_time'); } sub getpos { my ($heap, $kernel, $pos) = @_[HEAP, KERNEL, ARG0]; # Don't attempt to get info until song is actually playing if (!$heap->{gotinfo} && $pos > 0) { $heap->{gotinfo} = 1; $heap->{musicus}->getinfocurr; } if ($pos >= 0) { # No use updating if it's not different if (int($pos / 1000) != int($heap->{songinfo}{pos} / 1000)) { $heap->{songinfo}{pos} = $pos; $heap->{songinfo}{marker} = int(($pos / $heap->{songinfo}{length}) * ($Curses::COLS - 1)); $kernel->yield('update_time'); } if ($heap->{playing} && !$heap->{paused}) { $kernel->delay('send_getpos', .1); } } else { # End of song $heap->{songinfo}{pos} = $heap->{songinfo}{length}; $heap->{songinfo}{marker} = $Curses::COLS - 1; $kernel->yield('update_time'); } } # Events to send commands to musicus sub send_getpos { my $heap = $_[HEAP]; $heap->{musicus}->getpos; } sub musicus_play { my ($heap, $index) = @_[HEAP, ARG0]; # No, we don't have the info yet, but here's something temporary # (also erases old info) $heap->{gotinfo} = 0; $heap->{songinfo} = { title => $music[$index], pos => 0, marker => 0, length => 0, artist => '', title => '', album => '', track => '', year => '', date => '', genre => '', comment => '', }; $heap->{musicus}->stop if $heap->{playing}; $heap->{musicus}->play($dir . '/' . $music[$index]); } # Events to handle Curses interaction # Update all parts of the display. sub curses_refresh { my $kernel = $_[KERNEL]; $kernel->yield('update_song'); $kernel->yield('update_time'); $kernel->yield('update_list'); } # Update the song list. This sort of handles the highlight bar and # scrolling, although it's really kind of cheezy. sub update_list { my $heap = $_[HEAP]; my $list_info = $heap->{list_info}; my $row = 15; if ($list_info->{cursor} < 0) { $list_info->{cursor} = 0; $list_info->{top}-- if $list_info->{top} > 0; } elsif ($row + $list_info->{cursor} >= $Curses::LINES) { $list_info->{cursor} = $Curses::LINES - $row - 1; if ($list_info->{top} + ($Curses::LINES - $row) < @music) { $list_info->{top}++; } } elsif ($list_info->{top} + $list_info->{cursor} > @music - 1) { $list_info->{cursor} = @music - 1 - $list_info->{top}; } my $offset = 0; my $index = $list_info->{top}; while ($row < $Curses::LINES) { move($row, 0); clrtoeol(); if ($index < @music) { attrset(A_REVERSE) if $offset == $list_info->{cursor}; addstr(sprintf("%5d: %s", $index, $music[$index])); } attrset(A_NORMAL) if $offset == $list_info->{cursor}; $row++; $offset++; $index++; } move($row + $list_info->{cursor}, 0); noutrefresh(); doupdate; } # Update the song information. Title, album, genre, etc. sub update_song { my $heap = $_[HEAP]; my $song = $heap->{songinfo}; move(0, 0); clrtoeol(); addstr("Artist : $song->{artist}"); move(1, 0); clrtoeol(); addstr("Album : $song->{album}"); move(2, 0); clrtoeol(); addstr("Genre : $song->{genre}"); move(3, 0); clrtoeol(); addstr("Year : $song->{year}"); move(4, 0); clrtoeol(); addstr("Date : $song->{date}"); move(5, 0); clrtoeol(); addstr("Title : $song->{title}"); move(6, 0); clrtoeol(); addstr("Comment : $song->{comment}"); move(7, 0); clrtoeol(); addstr("Track : $song->{track}"); move(8, 0); clrtoeol(); move(9, 0); clrtoeol(); move(10, 0); clrtoeol(); addstr("Controls : Up/Down to Select, Enter to Play, " . "Space to toggle Pause, Q to Quit"); noutrefresh(); doupdate; } # Update the time display. This also moves the hash mark across the # screen to show the song's progress. sub update_time { my $heap = $_[HEAP]; move(13, 0); clrtoeol(); addstr( sprintf( "Time info : seconds=\%.2f/\%.2f", $heap->{songinfo}{pos} / 1000, $heap->{songinfo}{length} / 1000 ) ); move(14, 0); addstr('-' x $Curses::COLS); move(14, $heap->{songinfo}->{marker}); addstr('#'); move(14, $heap->{songinfo}->{marker}); noutrefresh(); doupdate; } # Got console input. Don't just sit there, do something! sub curses_input { my ($kernel, $heap, $keystroke) = @_[KERNEL, HEAP, ARG0]; # Replace special keystrokes with the names they're known by to # Curses. $keystroke = uc(keyname($keystroke)) if $keystroke =~ /^\d{2,}$/; # Ctrl+L refreshes the display. if ($keystroke eq "\cL") { $kernel->yield('update_all'); return; } # Emergency exit on C-c. if (($keystroke eq "\cC") or (lc($keystroke) eq 'q')) { $heap->{musicus}->quit(); return; } # Navigate down the play list. if ($keystroke eq 'KEY_DOWN') { $heap->{list_info}->{cursor}++; $kernel->yield('update_list'); noutrefresh(); doupdate; return; } # Navigate up the play list. if ($keystroke eq 'KEY_UP') { $heap->{list_info}->{cursor}--; $kernel->yield('update_list'); noutrefresh(); doupdate; return; } # The "p" key plays. if (ord($keystroke) == 13) { $kernel->yield('musicus_play', $heap->{list_info}->{top} + $heap->{list_info}->{cursor}); return; } # The spacebar toggles pause. if ($keystroke eq ' ') { if ($heap->{paused}) { $heap->{musicus}->unpause(); } else { $heap->{musicus}->pause(); } return; } } # The musicus player quit. Shut down the console interface, which # triggers the program's exit. sub player_quit { my ($kernel, $heap) = @_[KERNEL, HEAP]; move($Curses::LINES - 1, 0); clrtoeol(); move($Curses::LINES - 2, 0); clrtoeol(); addstr("Player has quit. Bye!"); noutrefresh(); doupdate; $kernel->alias_remove("console"); delete $heap->{curses}; } # Written by Curtis "Mr_Person" Hawthorne # Based on the MPEG Player from the POE Cookbook