Category: | Audio Related Programs |
Author/Contact Info | Written by Tom Pfau, pfaut on PerlMonks |
Description: | This program is an MP3 player with a Tk interface. It uses a database to keep track of how recently songs have been played so that it can play songs at random selecting from the least recently played songs. Merry Christmas! |
#!/usr/bin/perl =head1 TkMP3 - Random MP3 player with a Tk display and controls I wrote this because I didn't like playing from fixed playlists. After going through the list a few times, you start anticipating the next song. Also, the 'random' mode of many players doesn't quite cut it. When you stop and restart the player, it forgets what it recently played. You return from lunch to hear the same songs you just listened to in the morning but in a slightly different order. This player selects songs from a database and records the time they were played when the song finishes. The song selection algorithm picks a random song from the least recently played 10% of the collection. This keeps the order random and unpredictable and doesn't repeat songs after restarting the player. =cut use Carp; # move to the mp3 directory chdir "$ENV{HOME}/mp3"; # create the visual and attach button handlers my $vis = new Visual; $vis->onPause(\&btn_pause); $vis->onStop(\&btn_stop); $vis->onPlay(\&btn_play); $vis->onNext(\&btn_next); $vis->onExit(\&btn_exit); # catch termination requests $SIG{INT} = \&btn_exit; $SIG{TERM} = \&btn_exit; # create a new player, attach the handler for end of song, and # start playing my $player = new Player; $player->atEnd(\&song_over); my $fnm = $player->play; $vis->setTitle($fnm,"TkMP3 - $fnm"); $vis->start; # the pause button was pressed sub btn_pause { alarm(0); $player->pause; } # the stop button was pressed sub btn_stop { alarm(0); $player->stop; $vis->setTitle('<stopped>'); } # the play button was pressed sub btn_play { alarm(0); my $fnm = $player->play; $vis->setTitle($fnm,"TkMP3 - $fnm"); } # the next button was pressed sub btn_next { alarm(0); $player->stop; sleep 1; $player->select_next; my $fnm = $player->play; $vis->setTitle($fnm,"TkMP3 - $fnm"); } # the power button was pressed sub btn_exit { $SIG{INT} = 'IGNORE'; $SIG{TERM} = 'IGNORE'; $player->stop; $vis->shutdown; } # the player finished playing the current song # pause for two seconds and start another song # use alarm instead of sleep so that the window # remains responsive to user input sub song_over { $vis->setTitle(''); $SIG{ALRM} = \&sig_alarm; alarm 2; } # restart the player sub sig_alarm { $SIG{ALRM} = 'IGNORE'; my $fnm = $player->play; $vis->setTitle($fnm,"TkMP3 - $fnm"); } ###################################################################### +## # # this package provides the Tk interface package Visual; use Tk; # Bitmaps use constant Pause_Bitmap => <<BITMAP; #pause.xbm #define pause_width 16 #define pause_height 16 static unsigned char pause_bits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x06, 0x60, 0x06, 0x60, 0 +x06, 0x60, 0x06, 0x60, 0x06, 0x60, 0x06, 0x60, 0x06, 0x60, 0x06, 0x60, 0 +x06, 0x60, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; BITMAP ; use constant Stop_Bitmap => <<BITMAP; #stop.xbm #define stop_width 16 #define stop_height 16 static unsigned char stop_bits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x0f, 0xf0, 0x0f, 0x30, 0 +x0c, 0x30, 0x0c, 0x30, 0x0c, 0x30, 0x0c, 0x30, 0x0c, 0x30, 0x0c, 0xf0, 0 +x0f, 0xf0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; BITMAP ; use constant Play_Bitmap => <<BITMAP; #play.xbm #define play_width 16 #define play_height 16 static unsigned char play_bits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x98, 0x01, 0x98, 0x03, 0x98, 0 +x07, 0x98, 0x0f, 0x98, 0x1f, 0x98, 0x1f, 0x98, 0x0f, 0x98, 0x07, 0x98, 0 +x03, 0x98, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; BITMAP ; use constant Next_Bitmap => <<BITMAP; #next.xbm #define next_width 16 #define next_height 16 static unsigned char next_bits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x1c, 0x07, 0x38, 0 +x0e, 0x70, 0x1c, 0xe0, 0x38, 0xe0, 0x38, 0x70, 0x1c, 0x38, 0x0e, 0x1c, 0 +x07, 0x0c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; BITMAP ; use constant Power_Bitmap => <<BITMAP; #power.xbm #define power_width 16 #define power_height 16 static unsigned char power_bits[] = { 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x88, 0 +x11, 0x84, 0x21, 0x84, 0x21, 0x84, 0x21, 0x84, 0x21, 0x88, 0x11, 0x10, 0 +x08, 0x20, 0x04, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00}; BITMAP ; # create a new interface object sub new { my ($class) = shift; my $self = bless {}, $class; $self->_init(); return $self; } # initialize the interface sub _init { my ($self) = @_; $self->{main} = MainWindow->new(); $self->{main}->configure( -title => 'MP3 Player', -background => 'blue', -width => 250, -height => 50 ); $self->{bm_pause} = $self->{main}->Bitmap( -data => Pause_Bitmap ) +; $self->{bm_stop} = $self->{main}->Bitmap( -data => Stop_Bitmap ); $self->{bm_play} = $self->{main}->Bitmap( -data => Play_Bitmap ); $self->{bm_next} = $self->{main}->Bitmap( -data => Next_Bitmap ); $self->{bm_power} = $self->{main}->Bitmap( -data => Power_Bitmap ) +; $self->{form} = $self->{main}->Frame(); $self->{form}->pack(); $self->{title} = $self->{form}->Label( -text => 'Song Title', -relief => 'sunken' ); $self->{title}->pack(-side => 'top', -fill => 'x'); $self->{pause} = $self->{form}->Button( -image => $self->{bm_pause +}, -width => 32, -height => 20); $self->{pause}->pack( -side => 'left', -after => $self->{title} ); $self->{stop} = $self->{form}->Button( -image => $self->{bm_stop}, -width => 32, -height => 20); $self->{stop}->pack( -side => 'left', -after => $self->{pause} ); $self->{play} = $self->{form}->Button( -image => $self->{bm_play}, -width => 32, -height => 20); $self->{play}->pack( -side => 'left', -after => $self->{stop} ); $self->{fwd} = $self->{form}->Button( -image => $self->{bm_next}, -width => 32, -height => 20); $self->{fwd}->pack( -side => 'left', -after => $self->{play} ); $self->{power} = $self->{form}->Button( -image => $self->{bm_power +}, -width => 32, -height => 20); $self->{power}->pack( -side => 'left', -after => $self->{fwd} ); } # The following methods set handlers for the buttons. Each method # should be passed a subroutine reference. # set the pause button handler sub onPause { my ($self,$rtn) = @_; $self->{pause}->configure( -command => $rtn ); } # set the play button handler sub onPlay { my ($self,$rtn) = @_; $self->{play}->configure( -command => $rtn ); } # set the stop button handler sub onStop { my ($self,$rtn) = @_; $self->{stop}->configure( -command => $rtn ); } # set the next button handler sub onNext { my ($self,$rtn) = @_; $self->{fwd}->configure( -command => $rtn ); } # set the exit button handler sub onExit { my ($self,$rtn) = @_; $self->{power}->configure( -command => $rtn ); } # Set the title text. If two arguments are provided, also set the # title bar text. sub setTitle { my ($self,$title,$bar) = @_; $self->{title}->configure( -text => $title ); $self->{main}->configure( -title => $bar ) if $bar; } # shutdown the app sub shutdown { my ($self) = @_; $self->{main}->destroy; } # start the app sub start { MainLoop(); } ###################################################################### +## # # This package implements the player. It forks off a copy of the mp3 # player program and passes it the name of a file to play. It catches # SIGCHLD when the player finishes and sends notification to the main # program. package Player; use DBI; use POSIX "sys_wait_h"; # player state constants use constant ST_STOPPED => 0; use constant ST_PLAYING => 1; use constant ST_PAUSED => 2; sub new { my ($class) = @_; my $self = bless { state=>ST_STOPPED, pid=>undef, index=>undef, filename=>undef, date=>undef }, $class; # setup database connection and statement handles $self->{dbh} = DBI->connect("dbi:Pg:dbname=pfau","pfau","xyzzy", {AutoCommit=>true}) or die "Unable to connect to database: $DBI::errstr\n"; $self->{count} = $self->{dbh}->prepare(q(SELECT count(*) FROM mp3 WHERE autoplay IS NOT NULL)) or die "Prepare: $dbh->errstr\n"; $self->{files} = $self->{dbh}->prepare(q(SELECT idx,file_name,last_play FROM mp3 WHERE autoplay IS NOT NULL ORDER BY last_play LIMIT 1 OFFSET ?)) or die "Prepare: $dbh->errstr\n"; $self->{log} = $self->{dbh}->prepare(q(UPDATE mp3 SET last_play=CURRENT_TIMESTAMP WHERE idx = ?)) or die "Prepare: $dbh->errstr\n"; return $self; } # disconnect from the database sub DESTROY { $dbh->disconnect; } # start playing or pause if playing sub play { my ($self) = @_; if ($self->{state} == ST_PAUSED || $self->{state} == ST_PLAYING) { $self->pause; } elsif ($self->{state} == ST_STOPPED) { $self->select_next unless $self->{filename}; my $fnm = $self->{filename}; $SIG{CHLD} = sub { $self->sig_child(@_) }; if ($self->{pid} = fork) { $self->{state} = ST_PLAYING; } elsif (defined $self->{pid}) { exec 'kmp3player','-q','-b','256',$fnm; } else { die "Can't fork: $!\n"; } } return $self->{filename}; } # pause and resume sub pause { my ($self) = @_; if ($self->{state} == ST_PAUSED) { kill 'CONT', $self->{pid}; $self->{state} = ST_PLAYING; $SIG{CHLD} = sub { $self->sig_child(@_) }; } elsif ($self->{state} == ST_PLAYING) { $SIG{CHLD} = 'IGNORE'; kill 'STOP', $self->{pid}; $self->{state} = ST_PAUSED; } } # stop player sub stop { my ($self) = @_; if ( $self->{state} == ST_PLAYING || $self->{state} == ST_PAUSED ) { $SIG{CHLD} = 'IGNORE'; kill 'TERM', $self->{pid}; $self->{pid} = undef; $self->{state} = ST_STOPPED; } } # pick the next song to play sub select_next { my ($self) = @_; my $rv = $self->{count}->execute; if ( ! $rv ) { die "Execute 1: ", $sh_self->{count}->errstr, "\n"; } my $row = $self->{count}->fetch; if ( ! $row ) { die "Fetch 1: ", $self->{count}->errstr, "\n"; } my $cnt = $row->[0]; $self->{count}->finish; my $rn = int(rand(int($cnt/10)) + 1); $rv = $self->{files}->execute($rn); if ( ! $rv ) { die "Execute 2: ", $self->{files}->errstr, "\n"; } $row = $self->{files}->fetch; if ( ! $row ) { die "Fetch 2: ", $self->{files}->errstr, "\n"; } $self->{index} = $row->[0]; $self->{filename} = $row->[1]; $self->{date} = $row->[2]; $self->{files}->finish; } sub reap_child { my ($self,@args) = @_; my $wsts = waitpid($self->{pid},WNOHANG); my $sts = $?; return if $wsts == 0; die "Child exited abnormally, exiting\n" if $sts != 0; } # SIGCHLD handler sub sig_child { my ($self,@args) = @_; $self->reap_child; $SIG{CHLD} = sub { $self->sig_child(@_) }; $self->{state} = ST_STOPPED; $self->log_play(); $self->{atend}->() if $self->{atend}; } # set a handler for end of song sub atEnd { my ($self,$rtn) = @_; $self->{atend} = $rtn; } # call database logging routine sub log_play { my ($self) = @_; my $rv = $self->{log}->execute($self->{index}); if ( ! $rv ) { die "Execute 3: ", $self->{log}->errstr, "\n"; } $self->{log}->finish; $self->{filename} = undef; $self->{date} = undef; $self->{index} = undef; } __END__ Database definitions (postgresql) # available mp3 files # idx = unique id generated from a sequence # file_name is the filename of the mp3 file relative to the mp3 # directory or the absolule path to the file # last_play is the timestamp of the last time the song was played # autoplay = 0 to remove from consideration, non-zero to allow # the song to be played CREATE TABLE "mp3" ( "idx" serial, "file_name" text, "last_play" timestamp with time zone default '1980-1-1 0:0:0', "autoplay" integer default 1 ); To add songs: INSERT INTO mp3 (file_name) VALUES ('song.mp3'); |
|
---|