Beefy Boxes and Bandwidth Generously Provided by pair Networks
The stupid question is the question not asked
 
PerlMonks  

TkMP3

by pfaut (Priest)
on Dec 24, 2002 at 16:37 UTC ( [id://222108]=sourcecode: print w/replies, xml ) Need Help??
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');

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: sourcecode [id://222108]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others learning in the Monastery: (5)
As of 2024-04-19 17:23 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found