Channels ▼
RSS

Web Development

A Music Player Remote Control in Perl/Tk


Frank Cox is a professional programmer living in the North San Francisco Bay Area. He can be reached at frank.l.cox@gmail.com


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 types—and 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:

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);

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 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.

I originally had the size fixed with 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.

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 waSend() that look like to this:

my $play = $main->Button(-text => ">", 
                         -command => sub {$wap->waSend('play')});

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 -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!

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 wpFindPlay() and passes it the find string:

my $findB = $main->Button(-text => "find...", 
                          -command => sub {$wap->wpFindPlay($text)});

The Entry widget isn't any more complicated:

my $entry = $main->Entry(-width => 30, 
                          -textvariable => \$text);

This is where the $text variable gets the find string.

I like my GUI applications to be keyboard friendly too, so later I'll call 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)
});

One more little extra is this bit of code:

$main->repeat(3000 => sub {
   $title = $wap->currentSongName(); $main->title("$title")});

Every 3000ms, or once every three seconds, the 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.

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 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;

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 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 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.

So, What About the Other Versions?

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 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.

TPJ


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV