#!/usr/bin/perl use warnings; use strict; use Curses; use POE; use POE::Wheel::Curses; use POE::Component::MPG123; my $dir = shift; $dir = "." unless defined($dir); die "$dir is not a directory" unless -d $dir; opendir(DIR, $dir) or die $!; my @mp3s = grep { -f "$dir/$_" } sort grep /^[\x20-\x7E]+$/, readdir(DIR); closedir DIR; # Start mpg123 in a child process. It will send responses back to the # session with the alias "console". POE::Component::MPG123->spawn(alias => "console"); # This POE Session runs the console. It maps mpg123 and internal # events to the functions which will handle them. POE::Session->create( inline_states => { _start => \&console_initialize, # Events from the mpg123 process. player_quit => \&player_quit, status => \&player_status, song_info => \&player_song_info, file_info => \&player_file_info, song_paused => \&player_song_paused, song_resumed => \&player_song_resumed, song_stopped => \&player_song_stopped, # Internal events. update_all => \&curses_refresh, update_time => \&curses_show_time, show => \&curses_show, got_input => \&curses_input, } ); # Now that the mpg123 process and console interface are started, let's # run them until they're done. $poe_kernel->run(); exit 0; ### The console display is broken into separate regions. Each region ### can be updated by calling a plain functions. Event handlers, such ### as update_all() will call these directly. # Update the time display. This also moves the hash mark across the # screen to show the song's progress. sub update_time { my $time_info = shift; my ($fw, $tw) = @{$time_info}{'frames_width', 'time_width'}; move(13, 0); clrtoeol(); addstr( sprintf( "Time info : frames=\%${fw}d/\%${fw}d " . "seconds=\%${tw}.2f/\%${tw}.2f", @{$time_info}{'frames_played', 'total_frames', 'seconds_played', 'total_seconds'} ) ); move(14, 0); addstr('-' x $Curses::COLS); move(14, $time_info->{marker}); addstr('#'); move(14, $time_info->{marker}); } # Update the song information. Title, album, genre, etc. sub update_song { my $song = shift; 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("Title : $song->{title}"); move(5, 0); clrtoeol(); addstr("Comment : $song->{comment}"); } # File information comes back separate from song information. Since # I'm a geek at heart, I'll display it all. sub update_file { my $file = shift; move(6, 0); clrtoeol(); addstr("MPEG info : type=$file->{type} layer=$file->{layer} " . "bitrate=$file->{bitrate}"); move(7, 0); clrtoeol(); addstr(" : emphasis=$file->{emphasis} crc=$file->{crc} " . "copyright=$file->{copyrighted}"); move(8, 0); clrtoeol(); addstr(" : extension=$file->{extension}"); move(9, 0); clrtoeol(); addstr("Audio info : sample rate=$file->{samplerate} " . "mode=$file->{mode} (extension=$file->{mode_extension})"); move(10, 0); clrtoeol(); addstr("Frame info : bits per=$file->{bpf} channels=$file->{channels}"); move(11, 0); clrtoeol(); addstr("Flags : copyright=$file->{copyrighted} "); } # Update the song list. This sort of handles the highlight bar and # scrolling, although it's really kind of cheezy. sub update_list { my $list_info = shift; my $row = 15; if ($list_info->{cursor} < 0) { $list_info->{cursor} = 0; $list_info->{top}-- if $list_info->{top}; } elsif ($row + $list_info->{cursor} >= $Curses::LINES) { $list_info->{cursor} = $Curses::LINES - $row - 1; $list_info->{top}++ if $list_info->{top} + $row - 1 < @mp3s; } my $offset = 0; my $index = $list_info->{top}; while ($row < $Curses::LINES) { move($row, 0); clrtoeol(); if ($index < @mp3s) { attrset(A_REVERSE) if $offset == $list_info->{cursor}; addstr(sprintf("%5d: %s", $index, $mp3s[$index])); } attrset(A_NORMAL) if $offset == $list_info->{cursor}; $row++; $offset++; $index++; } move($row + $list_info->{cursor}, 0); } ### Now for the event handlers proper. # Initialize the player's console interface. Called automatically by # POE when the console session has started. sub console_initialize { my ($kernel, $heap) = @_[KERNEL, HEAP]; $kernel->alias_set("console"); # Attach to STDIN via Curses. Build a screen to display on. $heap->{curses} = POE::Wheel::Curses->new(InputEvent => 'got_input'); # These structures contain information we'll be displaying. Since # we aren't guaranteed to receive all of it from mpg123, we set it # up with defaults to avoid undef warnings. $heap->{list_info} = {map { ($_, 0) } qw(top cursor)}; $heap->{song_info} = {map { ($_, 0) } qw(artist album genre year title comment)}; $heap->{file_info} = { map { ($_, 0) } qw( type layer samplerate mode mode_extension bpf channels copyrighted crc emphasis bitrate extension ) }; $heap->{time_info} = { total_frames => 0, total_seconds => 0, marker => 0, frames_width => 1, time_width => 1, frames_played => 0, seconds_played => 0, }; $kernel->yield('update_all'); $heap->{current_directory} = '.'; } # Update all parts of the display. sub curses_refresh { my $heap = $_[HEAP]; update_song($heap->{song_info}); update_file($heap->{file_info}); update_time($heap->{time_info}); update_list($heap->{list_info}); noutrefresh(); doupdate; } # The mpg123 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}; } # Received a play status message from mpg123. Calculate the song's # progress for display later. sub player_status { my ($heap, $frames_played, $frames_left, $seconds_played, $seconds_left) = @_[HEAP, ARG0 .. ARG3]; unless ($frames_played) { $heap->{time_info} = { total_frames => $frames_left, total_seconds => $seconds_left, marker => 0, frames_width => length($frames_left), time_width => length($seconds_left), }; } $heap->{time_info}->{frames_played} = $frames_played; $heap->{time_info}->{seconds_played} = $seconds_played; if ($frames_played > $heap->{time_info}->{total_frames}) { $heap->{time_info}->{total_frames} = $frames_played; $heap->{time_info}->{total_seconds} = $seconds_played; } my $total_frames = $heap->{time_info}->{total_frames}; my $percent_done = ($frames_played / $total_frames) * 100; # This is the position of the wandering "#". my $marker = int(($frames_played / $total_frames) * ($Curses::COLS - 1)); # Update the time display if the marker has moved. if ($percent_done == 100 or $marker != $heap->{time_info}->{marker}) { $heap->{time_info}->{marker} = $marker; update_time($heap->{time_info}); noutrefresh(); doupdate; } } # Received song information from mpg123. Format and display it. We # can get back two kinds of information: abbreviated "filename" only, # or full song info. sub player_song_info { my ($heap, $info) = @_[HEAP, ARG0]; if ($info->{type} eq 'filename') { $heap->{song_info} = { artist => '', album => '', genre => '', year => '', title => $info->{filename}, comment => 'No ID3 information.', }; } else { $heap->{song_info} = {%$info}; } update_song($heap->{song_info}); noutrefresh(); doupdate; } # Received detailed MPEG information from mpg123. Format and show it. sub player_file_info { my ($heap, $info) = @_[HEAP, ARG0]; $heap->{file_info} = {%$info}; update_file($heap->{file_info}); noutrefresh(); doupdate; } # TODO: Confirmations about the song's playback status. sub player_song_paused { } sub player_song_resumed { } sub player_song_stopped { } # 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'); return; } # Emergency exit on C-c. if (($keystroke eq "\cC") or (lc($keystroke) eq 'q')) { $kernel->post(mpg123 => 'quit'); return; } # Navigate down the play list. if ($keystroke eq 'KEY_DOWN') { $heap->{list_info}->{cursor}++; update_list($heap->{list_info}); noutrefresh(); doupdate; return; } # Navigate up the play list. if ($keystroke eq 'KEY_UP') { $heap->{list_info}->{cursor}--; update_list($heap->{list_info}); noutrefresh(); doupdate; return; } # The "p" key toggles between "play" and "pause". if (lc($keystroke) eq 'p') { $kernel->post( mpg123 => play => ( $dir . '/' . $mp3s[$heap->{list_info}->{top} + $heap->{list_info}->{cursor}] ) ); return; } # The spacebar is definitely paus. if ($keystroke eq ' ') { $kernel->post(mpg123 => 'pause'); return; } # And "s" stops. if (lc($keystroke) eq 's') { $kernel->post(mpg123 => 'stop'); return; } }