Frank Cox is a professional programmer living in the North San Francisco Bay Area. He can be reached at [email protected]
I commute into San Francisco by ferry and think it's great. I get beautiful views, fresh air, full bar, bathrooms, tables, and electrical outlets. The only way it could be better (other than WiFi) is if they picked me up at home and dropped me off at work. I have one of those jobs where I sit and type most of the day, and it seems like the rest of the time I sit in meetings. Luckily, some of what I sit and type is Perl.
I started carrying a portable CD player years ago, which added a musical sound track to my commute. Later, I graduated to listening to mp3 files on my palmtop computer. The convenience of digital music revolutionized my musical life. Now, with the size of affordable memory increasing rapidly, I have dozens of CD's worth of music available at once in a portable device. Better yet, I have the space to encode them at a much higher quality.
At home I rarely play a CD any more (except when I haven't ripped it yet). It's mostly mp3's now and the best speakers in the house are connected to my workstation. That's where I tend to be sitting anyway, since half my hobbies involve typing, too.
So Many Songs... How to Find One to Play
Over the years I've built a sizable collection of music files. It's not huge by contemporary standards. Still, I have a few thousand files in a couple hundred directories. In the past, if I was sitting at my desktop and I wanted to play a particular song, I had to navigate several levels of file system to find it. Most ways to do this involved a lot of clicking and maybe some dragging.
One of the best methods I found was searching for my song with a GUI find utility and dragging the result to my player. That's when I discovered that it's really interesting to just search on a word and play everything that comes up. This technique digs way down into the depths and brings up a selection of gems with something in common. I was having a lot of fun with this but it still involved at least one too many steps. I simply had to build a better solution.
The Strange Roots of waPlay in Perl
This article is about a Perl solution, but I came to build it via a convoluted route. When I decided to do a GUI music find app, I thought I'd try an experiment. I'd write it in Perl/Tk, Mono, and Konfabulator. Mono is the open source .Net and C# for unix's and Windows. Konfabulator is a rich Javascript toolkit for building desktop widgets on Mac and Windows.
I figured that it would be a simple enough project, and one where I could justify using a GUI interface. I'd get some experience with these new systems, and as an added bonus, I'd end up with something I really needed.
httpQ
First, I had to find a way to control my music player from a program. I use the freeware Winamp player on Windows and it has a documented plug-in interface. I looked at the plug-ins available and quickly discovered httpQ. httpQ is an open source plug-in that takes Winamp control commands over the network using the HTTP protocol.
Since all I needed for this project was to control a music player on my local box, I installed httpQ and configured it. Plus, having a loosely coupled system like this can open the door to future experiments.
httpQ uses HTTP GET for all transactions so I can type commands in to the address bar of my browser and see all the results returned. Most httpQ commands look similar to one of these two examples:
http://localhost:4800/play?p=pass http://localhost:4800/playfile?p=pass&a="c:/tunes/I_love_Perl.mp3"
The httpQ defaults are port 4800, the plain text password "pass," and the address of the host "localhost." These can be configured in the Winamp Preferences for the httpQ plug-in. There are about 40 commands defined in the httpQ documentation. Some of the commands take one additional argument like the path to a file in the second example above. The playfile command simply puts the specified song at the end of the current playlist. The play command in the first example will play the song at the current index in the playlist. The httpQ documentation and a little experimentation will clear up any questions about the commands. httpQ is a complex product but reasonably consistent.
The Perl Version First
I decided to work out the algorithms in a Perl program first and then do my GUI
experiments in Perl/Tk. I figured that it would be a lot easier than prototyping
while struggling with an new language. Perl is easier for me in general so it was almost like
cheating. I wrote the meat of the program as an object oriented Perl file,
waPlay.pm
. I used a simple Perl program to drive it for testing and then wrote
the Tk part as a GUI driver.
Object Oriented Perl
I used Perl object syntax for this program partly because this was a prototype meant to be re-implemented in languages that, shall we just say, embrace the Object Oriented way of doing things. But, I use object oriented Perl for many programs when I think they will be even moderately complex. I particularly like the promiscuous access to all the object data. It's almost like global variables without the mess or guilt.
waPlay.pm
For this new module, I want to have a "find" function and a way to communicate with httpQ.
For the find funciton, I need to know the location of my music files. I have all my music under one
directory so this is just one path as a string. To communicate with httpQ, I need
the host name or address, the port number, and the password. For the first experimental
version I set everything in the init()
method. In the current version, shown here,
these values are set at object creation in the driver program.
package waPlay; $VERSION = .9; sub new { my $class = shift; my $self = {}; bless $self, $class; $self->{HOST} = shift; $self->{PORT} = shift; $self->{PASS} = shift; $self->{TUNES_DIR} = shift; $self->init(); return $self; } sub init { my $self = shift; $self->{SONG_TYPES} = [qw/.mp3 .wav .ogg .wma/]; } sub showAll { my $self = shift; use Data::Dumper; print Dumper($self); }
The only thing set in init()
is the music file extensions. My music
directories have cover art images, some metadata about the music, and a few
random Perl programs. Setting SONG_TYPES allows the find function to skip non-music
files by looking only at the file extensions that I set.
I like to start all of my object oriented Perl programs with new()
, init()
,
and showAll()
. showAll()
dumps the whole structure of the object and it's great
for debugging or just for reminding me of what I'm doing. The output of showAll()
for my current
version of waPlay looks like this:
$VAR1 = bless( { 'HOST' => 'localhost', 'HTTP_RESPONSE' => '7. Tom Waits - Downtown Train<br>', 'DEBUG' => '1', 'TUNES_DIR' => 'R:/Big Stripe/tunes', 'PASS' => 'pass', 'SONG_TYPES' => [ '.mp3', '.wav', '.ogg', '.wma' ], 'CURRENT_SONG' => '7. Tom Waits - Downtown Train', 'PORT' => '4800' }, 'waPlay' );
This dump tells me that I've opened the program with an existing Winamp playlist and I'm playing song seven. If I had done a find, there would be a SONGS_FOUND array with the full paths to the music files - which would be kind of a mess to show here.
wpFind()
My find
method is fairly simple thanks to the standard modules File::Find
and
File::Basename
.
sub wpFind { my $self = shift; use File::Find; use File::Basename; find(\&wanted, $self->{TUNES_DIR}); sub wanted { my ($b, $p, $base); if (-f and $_ =~ /$self->{FIND}/i) { ($b, $p, $base) = fileparse($_, @{$self->{SONG_TYPES}}); next unless $base; push @{$self->{SONGS_FOUND}}, $File::Find::name; } } }
The $self->{FIND}
string is set in another method called
wpFindPlay()
which, in turn, gets the string I am searching for
through a callback in the GUI. I'll get to both of these subjects shortly.
File::Find
takes care of the tricky task of recursing through
directories and subdirectories. It calls the wanted()
function on each
item it finds. If it's a file, and it matches the FIND string, it is tested to see
if it has one of the extensions in the SONG_TYPES array. If it passes all of these
tests, it gets pushed into the SONGS_FOUND array.
There are some other things to notice about wpFind()
. One thing is that I'm using
the i
option to do a case insensitive search. A case insensitive search is
really a necessity for this application. Another thing is that the FIND string typed into the GUI is
treated as a regular expression. This adds a lot of extra power for us
programmer typesand it might surprise us every once in a while. For example, I was
searching for songs with "U.S.A" in the title. I got some unexpected matches
including "Russian Lullaby" (since /U.S.A/i
matches ussia). Instead, I needed to use
U\.S\.A
waFindPlay(search_string)
waFindPlay()
is called by the GUI and stores the string being searched for into the FIND
data item. waFindPlay()
clears the SONGS_FOUND array and calls wpFind()
. It then
uses the httpQ command delete to clear the Winamp playlist. Finally, it uses the
playfile command in a loop to push the songs found into the current Winamp
playlist. It is called FindPlay
because it originally started playing the new
playlist right away. However, I found that I didn't always like interrupting the song
that's currently playing. So, I removed that feature, but the name stuck.
sub wpFindPlay { my $self = shift; $self->{FIND} = shift; @{$self->{SONGS_FOUND}} = (); $self->wpFind(); $self->waSend('delete'); foreach (@{$self->{SONGS_FOUND}}) { $self->waSend('playfile', $_); } }
There are two calls here that use waSend()
to talk to httpQ.
Here is what gets sent for these two calls in wpFindPlay()
:
http://localhost:4800/delete?p=pass http://localhost:4800/playfile?p=pass&a="c:/tunes/I_love_Perl.mp3"
A method to construct these URI's would only need the command and optional argument. Everything else it needs is available as object variables.
sub waSend { my $self = shift; my $cmd = shift; my $arg = shift; my $url = "http://$self->{HOST}:$self->{PORT}/$cmd?p=$self->{PASS}"; $url .= "&a=$arg" if defined $arg; $self->_getHTTPQ($url); }
The method that actually sends these URI's to httpQ is called _getHTTPQ()
. I use the convention of
prepending an underscore to a method when I don't intend it to be called directly.
sub _getHTTPQ { my $self = shift; my $url = shift; use LWP::Simple; $self->{HTTP_RESPONSE} = get($url); }
I used LWP::Simple
to take care of the HTTP transaction and "simple" is a good
description for it. The one thing to notice here is that whatever is returned by get()
is stored in the object variable HTTP_RESPONSE. This could be a 1 or 0 from httpQ
for success or fail. Or, it might be a string returned by httpQ in response to a
request. It could also be an error message from somewhere else, or undef, or some
other unexpected value. I haven't needed to implement any error checking yet,
but this would be one good place to do it.
Before we leave this section we should look at the currentSongName()
method. The
GUI has the current song name in the title bar and this is how it gets it:
sub currentSongName { my $self = shift; $self->waSend('getlistpos'); my $pos = $self->{HTTP_RESPONSE}; $self->waSend('getplaylisttitle', $pos); $self->{CURRENT_SONG} = $self->{HTTP_RESPONSE}; $self->{CURRENT_SONG} =~ s/<br>//gi; return $self->{CURRENT_SONG}; }
It uses the getlistpos
command to get the position of the current song in the
playlist and then uses getplaylisttitle
to get the song name. The name is
returned with <br> in it for some reason, so we have to remove this. The
CURRENT_SONG object variable is also set and then I return the object to the caller.
waPlayGUI.pl
I started working on the GUI once I had a full collection of working methods. As GUI applications go, this is a pretty basic one. There is one box to enter text and a row of buttons. Here is what the finished GUI looks like:
The GUI part of my program starts out like this:
Tk is included in current distributions from Activestate and that is what I used.
I set the default values for my object variables and then call
I originally had the size fixed with
The box to enter text is called an Entry widget in Tk. We also have six Button
widgets. The five on the far right are all simple calls to
The example above is the play button. The Button widget has dozens of other options
but I am happy with most of the defaults. For the
All the other player control buttons are like this. The httpQ commands from left
to right are: prev, play, pause, stop, next.
The find button is just slightly different in that it calls
The Entry widget isn't any more complicated:
This is where the
I like my GUI applications to be keyboard friendly too, so later I'll call
One more little extra is this bit of code:
Every 3000ms, or once every three seconds, the
Using 3000ms gives good average response while keeping the application
lightweight. I tried setting it to 1000 but the CPU usage at idle was up between
Firefox's and Photoshop's.
After all the widgets are defined, I
I have a short, wide window. I'm starting at the far right with next (>|) and
adding the buttons from right to left, packing them all to the right side. Then,
I put in the entry box packed to the left. This way when I resize the window all
the buttons stay together on the right and the entry stays on the left.
In the line before the
So that's it. I have a neat little desktop music tool
with a unique find function that I use almost every day. Most of my other Perl
programs are tools and utilities that run on the command line or in the
background on servers. So, it's been extra nice to have a Perl application
sitting on my Windows desktop.
In case you're wondering about the rest of my project: I have a Mono version.
It's not 100% there yet but it works. It wasn't much harder to write then the
Perl/Tk version except I haven't found an equivalent of Perl's standard
TPJ
use strict;
use waPlay;
use Tk;
$| = 1;
my $HOST = "localhost";
my $PORT = "4800";
my $PASS = "pass";
my $TUNES_DIR = "c:/tunes";
my $DEBUG = 0;
getInit();
my $wap = new waPlay($HOST, $PORT, $PASS, "$TUNES_DIR", $DEBUG);
$wap->showAll() if $wap->{DEBUG};
my $main = new MainWindow(-height => 30, -width => 300);
$main->resizable(1, 0);
getInit()
.
getInit()
is an ordinary subroutine that reads an .ini file to get the default
settings. Next, I create my waPlay
object named $wap
with new()
.
I also create my main window and give it a size of 300 pixels wide by 30 pixels high.
Then, I make it resizable horizontally but not vertically.
resizable(0,0)
but sometimes
the song titles are too long to fit in the title bar. This way I can make the
interface wider by dragging one of the edges.
waSend()
that look
like to this:
my $play = $main->Button(-text => ">",
-command => sub {$wap->waSend('play')});
-text
option, I'm using '>' for play. If I
wanted to put a picture of a play symbol on the button face I could have used
the -imaged
option instead. I might do this in the future, but for now I like
the nothin' fancy look. The -command
option will call code when the button is
clicked. As you can see, I'm using an inline subroutine to send a play command
to httpQ using waSend()
. It really is as easy as that!
wpFindPlay()
and passes it the find string:
my $findB = $main->Button(-text => "find...",
-command => sub {$wap->wpFindPlay($text)});
my $entry = $main->Entry(-width => 30,
-textvariable => \$text);
$text
variable gets the find string.
focus()
on
$entry
. This will let me start typing into the entry box when my window has
focus without first clicking in it. I also want the option of hitting the Enter
key after typing my find string instead of having to always click the Find...
button. That's what I do with the bind
method here.
$main->bind('<KeyPress-Return>' => sub {
$wap->wpFindPlay($text)
});
$main->repeat(3000 => sub {
$title = $wap->currentSongName(); $main->title("$title")});
currentSongName()
method is
called and the title is put into the window title bar. I wanted to have a very
compact remote with only the entry box and buttons but I haven't found a
workable way to do this in Perl/Tk. The title bar makes the interface bigger so
I figured that I would fill the extra space.
pack
them into the main window all at once
like this:
$next->pack(-side => 'right');
$stop->pack(-side => 'right');
$pause->pack(-side => 'right');
$play->pack(-side => 'right');
$prev->pack(-side => 'right');
$findB->pack(-side => 'right');
$entry->pack(-side => 'left');
$entry->focus();
MainLoop;
MainLoop
call, I give focus to the entry
widget. Putting
it at the end like this will give the Entry field focus when the GUI itself gets focus.
MainLoop
is the last logical statement in a Perl/Tk program, so we are done!
So, What About the Other Versions?
File::Find
. My alternative solutions haven't been very satisfactory so far.
I've lost some of my enthusiasm for Konfabulator, but I have some buttons that send httpQ
commands. On the bright side, Konfabulator comes with a set of Unix utilities,
including find, to ensure compatability with the original Mac OS X version. So
I'm not very far from a working version there either. The Perl version works so
well though, that it is the one I'm going to keep using.