[incise.org] pymp

Writing an mp3 Player in Python

This page is old. pymp changed to thump. This page is still a good read, though.

Lately I have been working on writing my own mp3 player in Python, because I found most mp3 players too stuffy and inflexible. I also enjoy coding things for my own use simply for the fact that I understand them better and I can make them work how I want, without the accompanying annoying features of software others have written.

Most of the code has not gone to the actual playing of mp3's, that can be done in perhaps 10-15 lines of Python. The bulk of the code is providing the actual player with features that I want.

Disclaimer: I am not an advanced programmer, in Python or in general. I have been using Python for about 4-5 months as of this writing (mid-May 2003), and programming regularly for perhaps 1.25 years. If you see something idiotic in this page, please let me know, instead of immediately deciding that I am an idiot : )

Goals

The goal is to have an mp3 player that is:

I also have some goals for the code itsef:

When you get down to it, the main goal is to have the perfect balance of power, flexibility, and simplicity. The unix ideal, basically. : )

Decoding

The actual decoding of the mp3's is done by the mad library, which is purportedly the highest quality mp3 decoder around. It is available as a python module (pymad). The sound device access is done by libao (pyao). For linux-specific use you can save yourself a download and use the python module linuxaudiodev (which is included in the linux python distribution), but I will be using pyao (see goal #6).

Just from these two modules we can build the core of our player, the part which actually decodes the mp3 and sends it to the sound card. An extremely simple example (derived from an example in the pymad distribution) would be:

#!/usr/bin/python
import mad, ao, sys

madfile = mad.MadFile(sys.argv[1])
dsp = ao.AudioDevice("oss", bits=16, rate=44100)
while 1:
  buf = madfile.read()
  if buf is None: break
  dsp.play(buf, len(buf))

You can copy/paste this into a file, pass an mp3 filename as the first argument when you execute it, and it will play your mp3! Just playing the file is a bit inadequete though, we want a framework that will interoperate all kinds of ways. Let's make something a bit more organized and modular.


class PympDecoder:

  def __init__(self, output_type="oss", bits=16, rate=44100):
    self.dsp = ao.AudioDevice(output_type, bits, rate)
  
  def bitrate(self, filename):
    return mad.MadFile(filename).bitrate()
  
  def _getBuf(self):
    self.buf = self.madfile.read()
    if self.buf is None:
      return 0
    else:
      return 1
  
  def _outputFrame(self):
    self.dsp.play(self.buf, len(self.buf))
  
  def _setMadFile(self, filename):
    self.madfile = mad.MadFile(filename)
  
  def playFile(self, filename):
    self._setMadFile(filename)
  
    while self._getBuf():
      self._decodeFrame()
    else:
      print
      return 1
      # we made it!

Now we have a class with which we can play, or retrieve the bitrate, of arbitrary files. We also provide the individual methods needed for iterating through an mp3, so, for example, if we want to perform some other actions while looping through our mp3, we can do so. There are other attributes like mpeg layer and sampling rate which you may want, but I don't care all that much. : ) The pymad example script covers all of that.

So basically what we have here is a class that we can instantiate, and then call either playFile() to play a file, or bitrate() to check the bitrate of a file. There are a few other internal functions for the sake of dividing up logic.

Interface

I have decided against creating a dedicated user interface, for two reasons:

I currently plan to have two user interface components. The original concept was an irc-like interface, with one line at the bottom for entering commands, and the rest of the screen displaying information about files that have played / are playing. This concept remains, and I have realized that I can already implement such a thing with zero programming, while leaving people the ability to choose their own solution appropriate to their situation.

Enter Window

Yes, Window(1). With window I can split the screen into two, with a single line at the bottom running one program, and the remaining top portion running another. So far this looks like:

-1------------------------------------------------------------------------------
Escape character is ^P.
death@incise ~ %




















-2-----------------------------------------------------------------------------
death@incise ~ %

I can customize the behavior of window(1) in my ~/.windowrc should I desire to do so. I believe screen(1) can also do this, I know it can split the screen into windows, but I haven't seen how to control the size of these windows yet. I suppose this is an example of the power in flexibility, if there is something you know of that you'd rather use than window(1), then you can use it and lose no functionality whatsoever. : )

Top part

I expect to simply have tail running in the top window, tailing the pymp.tail file, which is a log of the songs played, possibly with bitrates. This could, and likely may change, but for now that's all I can really think of.

Bottom part

I expect this to simply be a python script running a loop of raw_input()s, with appropriate actions taken for commands.


pyao and pymad on NetBSD

I was disappointed that neither pyao nor pymad were in the pkgsrc, the NetBSD packaging system, however they are fairly trivial to install. First you will need to install audio/libao and audio/mad. Then you will need to grab the pyao and pymad tarballs (here, and here, respectively). Simply follow the instructions included with each (actually the directions for pymad are on the pymad webpage, but they are identical to pyao's), using --prefix /usr/pkg, or whatever other directory you install your packages to. No further tweaking should be needed.


Update, May 12 2003

Ok, quite a bit has been done. There are now 3 files. Current source is here.

Interface, again

Window turned out to be a bit uncivilized, curses does not agree with the terminal type it uses, changing $TERM results in junk being printed, resizing the terminal causes it to act very badly, etc. I suppose this stems from the fact that window is pretty old (1993 or older), but it is very nice in concept, so it makes me wonder why no one has had any motivation to fix it. If I was good with C I'd probably try to myself, but I'm not, so I won't. : P

So now I am using screen. First off, I put the following in my ~/.screenrc:

bind + resize +1
bind - resize -1

Now, I start up a screen session.

% screen -S musicd

In this screen session, I run play.py, and it begins playing my music. I now detach. ^A d. Now I start a second screen session.

% screen -S music

Now in this screen session, I do ^A S twice, this splits the terminal into 3 "windows". Using ^A ^I to switch between windows, ^A + and ^A - to resize them, I get my screen to a state where I have two one line windows at the very bottom, with the remainder of the screen taken up by the first window. Remember, ^A ^I switches between, ^A c creates new screens, ^A <number> goes to screen <number>, etc. ^A ? for help. I start up 3 screens, with screen 1 in the top window, 2 in the upper-lower window, and screen 3 at the very bottom. Now I have something like this:

Now, in the top window, I want a list of the files being played, as they are played. The player writes this info (including bitrate) to the file /tmp/pymp.tail, so I just do tail -n 20 -f /tmp/pymp.tail. In window two, I want to see the time counter, so I tail pymp.live. Since this file is only a single line, with CR's to update the line, I want to make sure all text is cleared from this single line of my terminal beforehand, so I do clear; tail -f /tmp/pymp.live. In the bottom window, I want my controller, so I run ~/pymp/Ctl.py. I now have a fairly irc-like interface, which is nice, IMO : )

Now, remember the player playing in that other screen session? Well, I can check up on it in this one if I want. I go to the top window, hit ^A c, then screen -dr musicd. Now I have two screens inside one another, I can hit ^A a to get back to my tail of filenames, or I can use ^A a to send ^A's to the musicd screen, to detach it, or whatever else I might want to do in it. Generally though, I want to leave it alone, that's why I have a controller and all of this other confusing stuff. : )

Now I have this:

Ok, that's it for now, I still need to do quite a bit of work, but it's usable now.

Unresolved Issues / TODO

  1. Interface is a bit of a kludge to get up and running.
  2. Is there any way to get rid of those "status" bars that screen puts there?
  3. Song searching and advanced commands need implemented - this includes the backend functions for adding and removing things from the playlist by various methods (directory, glob, string matching, regex?), resetting the playlist, saving (?) the playlist, etc.
  4. Stopping and playing needs implemented. Stopping right now just kills the player and you have to go start it again manually.
  5. I'm not sure that using a regular file and using CR's to go to the beginning of the line for the pymp.live is a great idea, it seems to choke if you stop tailing it, or if you try to open the file from somewhere else, it appears empty, etc. Not really sure how to go about this.
  6. play.py needs docstrings. Ctl.py does too, but it is going to be changing alot so it'd be a bit pointless right now.
  7. Ctl.py will probably need to be structured more properly as it grows, it's a bit of a quick hack right now, with no classes of its own.
  8. Needs a new name. It's pymp for now, but there is already a project called pymps.
  9. A ton of other little things that I can't think of right now.


Update, May 16 2003

More notes.


comments


Nick Welch <nick@incise.org>