#! /usr/bin/perl -w # # $Header: /u/richardk/cvs/sound/playlist,v 1.9 2000/07/26 16:24:17 richard Exp $ # use Tk; use Tk::DirTree; use Tk::Adjuster; use strict; use IO::File; use POSIX; #require 'sys/ioctl.ph'; #require 'sys/soundcard.ph'; # *.ph's are broken, arguably by design... # these values are for Linux 2.2 sub SOUND_MIXER_READ_VOLUME { 2147765504 }; sub SOUND_MIXER_WRITE_VOLUME { 3221507328 }; my @directories = (); my $currentdir = ""; my $playing = ""; my $playerpid; my $playerpaused = 0; my $dirschanged = 0; my $localconfig = "$ENV{'HOME'}/.playlist.conf"; my $playlist; my @playlist = (); my $mw; my $dirmenu; my $dirmenubutton; my $dirtree; my $selectframe; my $dirtreeframe; my ($leftvol, $rightvol); my $pause; my $go; my $tied; my $dw; my $mixer; sub addtrack ($); sub stop (); sub setbuttonstate (); # read a configuration file sub readconfig ($) { my $path = shift; if(-e $path) { my $f = new IO::File $path, O_RDONLY; my @config; die "$0: error opening $path: $!\n" if !defined $f; chomp(@config = <$f>); die "$0: error reading $path: $!\n" if $f->error; $f->close; parseconfig($path, @config); } } # add a directory to the list sub directory (@) { push(@directories, @_); } # parse configuration sub parseconfig ($@) { my $path = shift; local $_; my $line = 0; for(@_) { ++$line; next if /^\#/ || /^\s*$/; if(/^directory\s+(\S+)$/) { directory $1; } else { die "$0: $path:$line: syntax error\n"; } } } # display an error window sub error ($) { my $text = shift; my $ew = $mw->Toplevel(-title => "Error"); $ew->Label(-text => $text)->pack(-side => 'top'); $ew->Button(-text => 'OK', -command => sub {$ew->destroy})->pack(-side => 'bottom'); } # save the configuration sub save_config ($) { my $path = $localconfig; my $tmp = "$path.$$"; my $f = new IO::File $tmp, O_WRONLY|O_CREAT|O_EXCL, 0666; if(!defined $f) { error "error creating $tmp: $!"; return; } ($f->print(map("directory $_\n", @directories))) || do { error "error writing to $tmp: $!"; return; }; $f->close || do { error "error closing $tmp: $!"; return; }; (rename $tmp, $path) || do { error "error renaming $tmp to $path: $!"; return; }; $dirschanged = 0; } # bring up a window to add or remove a directory sub edit_dirs () { if(defined $dw) { return; } $dw = $mw->Toplevel(-title => 'Directory list'); my $dl = $dw->Scrolled('Listbox'); $dl->insert(0, @directories); $dl->pack(-side => 'top', -expand => 1, -fill => 'both'); my $fr = $dw->Frame(); my $en = $fr->Entry(); $fr->Button(-text => 'Delete', -command => sub { my $n = $dl->index('active'); splice(@directories, $n, 1); $dl->delete('active'); $dirschanged = 1; })->pack(-side => 'left'); $fr->Button(-text => 'Add', -command => sub { my $dir = $en->get; $dl->insert('end', $dir); directory $dir; $dirschanged = 1; })->pack(-side => 'left'); $en->pack(-side => 'left'); $fr->Button(-text => 'Save', -command => sub { &save_config; })->pack(-side => 'left'); $fr->Button(-text => 'OK', -command => sub { if($dirschanged) { &save_config; } &reselect; $dw->destroy; undef $dw; })->pack(-side => 'left'); $fr->pack(-side => 'bottom'); } # redo the directory tree sub rescan () { if(defined $dirtree) { $dirtree->destroy; undef $dirtree; } $dirtree = $dirtreeframe->Scrolled('Tree', -selectmode => 'extended', -separator => '/', -itemtype => 'imagetext'); # populate the directory tree &populate($dirtree, $currentdir, "/"); $dirtree->pack(-side => 'top', -expand => 1, -fill => 'both'); } sub populate ($$) { my ($dirtree, $root, $path) = @_; if(-d "$root/$path") { opendir(DIR, "$root/$path") || do { print STDERR "$0: error opening directory $root/$path: $!\n"; return 0; }; my @contents = sort readdir DIR; closedir DIR; $dirtree->add($path, -text => ($path eq "/" ? $root : basename($path))); my $child; my $nchildren = 0; my $nsubdirs = 0; for $child (@contents) { next if($child =~ /^\./); $nsubdirs += populate($dirtree, $root, "$path/$child"); $nchildren++; } if($nsubdirs) { # if there are subdirectories, keep this directory open $dirtree->setmode($path, 'close'); } elsif($nchildren) { # if there are only regular files, keep it closed $dirtree->setmode($path, 'open'); } else { # empty directory $dirtree->setmode($path, 'none'); } return 1; } else { my $name = basename($path); return 0 unless $name =~ /\.mp3$/; $name =~ s/\.mp3//; $name =~ s/^\d+:\s*//; $dirtree->add($path, -data => "$root/$path", -text => $name); $dirtree->setmode($path, 'none'); $dirtree->hide('entry', $path); return 0; } } sub basename ($) { local $_ = shift; if(/\/([^\/]+)$/) { return $1; } return $_; } # redo the selection frame sub reselect () { my $dir; # configure the directory select button $dirmenubutton->menu->delete(0, 'end'); for $dir (@directories) { $dirmenubutton->command(-label => $dir, -command => sub { $currentdir = $dir; &rescan; $dirmenubutton->configure(-text => $currentdir); }); } $dirmenubutton->configure(-text => $currentdir); &rescan; } # play sub go () { if(defined $playerpid && $playerpaused) { kill CONT => -$playerpid; $playerpaused = 0; } setbuttonstate; } # stop playing sub stop () { if(defined $playerpid && !$playerpaused) { kill STOP => -$playerpid; $playerpaused = 1; } setbuttonstate; } # recursively add tracks sub addtrack ($) { my $path = shift; if(-d $path) { opendir(DIR, $path) || do { error "error opening directory $path: $!"; return; }; my @children = sort readdir DIR; closedir DIR; my $c; for $c (@children) { next if $c =~ /^\./; addtrack "$path/$c"; } } elsif(-f $path) { push(@playlist, $path); my $basename = $path; $basename =~ s/^.*\/(\d\d:\s*)?//; $basename =~ s/\.mp3$//; $playlist->insert('end', $basename); } } # add tracks sub addtracks () { my @tracks = $dirtree->info('selection'); my $t; for $t (@tracks) { addtrack("$currentdir/$t"); } $dirtree->selectionClear; } # delete track sub deletetrack () { my $track; for $track (sort { $b <=> $a } $playlist->curselection) { splice(@playlist, $track, 1); $playlist->delete($track); } } # poll for activity sub poll () { my $want; # see if the player has terminated if(defined $playerpid) { my $wait = waitpid($playerpid, WNOHANG); if($wait > 0) { undef $playerpid; if($#playlist >= 0 && $playing eq $playlist[0]) { shift @playlist; $playlist->delete(0); } $playing = ""; setbuttonstate; } } # if we're paused, then no change is required if($playerpaused) { return; } # determine the desired state if($#playlist < 0) { $want = ""; } else { $want = $playlist[0]; } # if we're in the desired state, stop now if($want eq $playing) { return; } # if we're playing the wrong thing, kill the player # (and return immediately - we'll start up the right thing # next time round) if($playing ne "") { kill TERM => -$playerpid; return; } # if we want something that isn't playing, start the player if($want ne "") { if(!defined($playerpid = fork)) { die "$0: fork failed: $!\n"; } if(!$playerpid) { setpgid($$, $$) || die "$0: setpgid: $!\n"; exec("mpg123", "-b", "1024", "-q", $want) || print STDERR "$0: exec mpg123: $!\n"; exit 1; } $playing = $want; setbuttonstate; } } sub setvol ($) { my $which = shift; my $l = $leftvol->get; my $r = $rightvol->get; if($tied) { if($which eq "left") { $rightvol->set($r = $l); } else { $leftvol->set($l = $r); } } my $encoded = pack('i', $l + 256 * $r); (defined ioctl($mixer, &SOUND_MIXER_WRITE_VOLUME, $encoded)) || die "$0: error calling ioctl SOUND_MIXER_WRITE_VOLUME: $!\n"; } sub setbuttonstate () { $go->configure(-state => (defined $playerpid && $playerpaused ? "normal" : "disabled")); $pause->configure(-state => (defined $playerpid && !$playerpaused ? "normal" : "disabled")); } if(-e $localconfig) { readconfig $localconfig; } elsif(-e "/etc/playlist.conf") { readconfig"/etc/playlist.conf"; } while($#ARGV >= 0) { my $opt = shift; if($opt eq "-d") { my $dir = shift; directory $dir; } else { die "$0: unknown option `$opt'\n"; } } $mw = MainWindow->new(-title => 'Playlist'); my $fframe = $mw->Frame; $selectframe = $fframe->Frame(-relief => 'sunken', -bd => 2); my $dirframe = $selectframe->Frame(); $dirframe->Button(-text => 'Edit', -command => \&edit_dirs)->pack(-side => 'left'); $dirmenubutton = $dirframe->Menubutton(-relief => 'raised', -bd => 2)->pack(-side => 'left'); $dirmenubutton->configure(-menu => $dirmenubutton->menu); $dirframe->pack(-side => 'top', -expand => 0, -fill => 'x'); $dirtreeframe = $selectframe->Frame(); $dirtreeframe->pack(-side => 'top', -expand => 1, -fill => 'both'); $selectframe->Button(-text => 'Add', -command => \&addtracks)->pack(-side => 'left'); $selectframe->Button(-text => 'Rescan', -command => \&rescan)->pack(-side => 'left'); $currentdir = $directories[0]; my $listframe = $fframe->Frame(-relief => 'sunken', -bd => 2); $playlist = $listframe->Scrolled('Listbox', -selectmode => 'extended'); $playlist->pack(-side => 'top', -expand => 1, -fill => 'both'); $listframe->Button(-text => 'Delete', -command => \&deletetrack)->pack(-side => 'left'); $pause = $listframe->Button(-text => 'Pause', -command => \&stop)->pack(-side => 'left'); $go = $listframe->Button(-text => 'Go', -command => \&go)->pack(-side => 'left'); $listframe->Button(-text => 'Quit', -command => sub { $mw->destroy; })->pack(-side => 'left'); my $volframe = $mw->Frame(-relief => 'sunken', -bd => 2); $volframe->Label(-text => 'Volume')->pack(-side => 'top'); my @scale = (-length => 200, -showvalue => 1, -sliderrelief => 'raised', -tickinterval => 20, -digits => 2, -from => 100, -to => 0); my $scaleframe = $volframe->Frame(); $leftvol = $scaleframe->Scale(@scale, -label => 'left', -command => [\&setvol, 'left']); $leftvol->pack(-side => 'left'); $rightvol = $scaleframe->Scale(@scale, -label => 'right', -command => [\&setvol, 'right']); $rightvol->pack(-side => 'left'); $scaleframe->pack(-side => 'top'); $volframe->Checkbutton(-text => 'Tied', -variable => \$tied)->pack(-side => 'top'); # set initial values ($mixer = new IO::File "/dev/mixer", O_RDWR, 0) || die "$0: cannot open /dev/mixer: $!\n"; my $result = pack("i", 0); (defined ioctl($mixer, &SOUND_MIXER_READ_VOLUME, $result)) || die "$0: error calling ioctl SOUND_MIXER_READ_VOLUME: $!\n"; $result = unpack("i", $result); my $l = $result & 0xff; my $r = ($result >> 8) & 0xff; $leftvol->set($l); $rightvol->set($r); $tied = ($l == $r); $listframe->pack(-side => 'right', -expand => 1, -fill => 'both'); $fframe->Adjuster->packAfter($listframe, -side => 'right'); $selectframe->pack(-side => 'right', -expand => 1, -fill => 'both'); $fframe->pack(-side => 'left', -expand => 1, -fill => 'both'); $volframe->pack(-side => 'left', -fill => 'y'); reselect; setbuttonstate; $mw->repeat(1000, \&poll); $mw->OnDestroy(sub { if($dirschanged) { &save_config; } if(defined $playerpid) { kill TERM => -$playerpid; } }); MainLoop;