Song Sources
Because a Shoutcast server has to keep streaming songs to the client for as long as it’s connected, you need to provide your server with a source of songs to draw on. In the Web-based application, each connected client will have a playlist that can be manipulated via the Web interface. But in the interest of avoiding excessive coupling, you should define an interface that the Shoutcast server can use to obtain songs to play. You can write a simple implementation of this interface now and then a more complex one as part of the Web application you’ll build in Chapter 29.
The Package
The package for the code you’ll develop in this chapter looks like this:
(defpackage :com.gigamonkeys.shoutcast
(:use :common-lisp
:net.aserve
:com.gigamonkeys.id3v2)
(:export :song
:file
:title
:id3-size
:find-song-source
:current-song
:still-current-p
:maybe-move-to-next-song
:*song-source-type*))
The idea behind the interface is that the Shoutcast server will find a source of songs based on an ID extracted from the AllegroServe request object. It can then do three things with the song source it’s given.
- Get the current song from the source
- Tell the song source that it’s done with the current song
- Ask the source whether the song it was given earlier is still the current song
The last operation is necessary because there may be ways—and will be in Chapter 29—to manipulate the songs source outside the Shoutcast server. You can express the operations the Shoutcast server needs with the following generic functions:
(defgeneric current-song (source)
(:documentation "Return the currently playing song or NIL."))
(defgeneric maybe-move-to-next-song (song source)
(:documentation
"If the given song is still the current one update the value
returned by current-song."))
(defgeneric still-current-p (song source)
(:documentation
"Return true if the song given is the same as the current-song."))
The function maybe-move-to-next-song
is defined the way it is so a single operation checks whether the song is current and, if it is, moves the song source to the next song. This will be important in the next chapter when you need to implement a song source that can be safely manipulated from two different threads.3
To represent the information about a song that the Shoutcast server needs, you can define a class, song
, with slots to hold the name of the MP3 file, the title to send in the Shoutcast metadata, and the size of the ID3 tag so you can skip it when serving up the file.
(defclass song ()
((file :reader file :initarg :file)
(title :reader title :initarg :title)
(id3-size :reader id3-size :initarg :id3-size)))
The value returned by current-song
(and thus the first argument to still-current-p
and maybe-move-to-next-song
) will be an instance of song
.
In addition, you need to define a generic function that the server can use to find a song source based on the type of source desired and the request object. Methods will specialize the type
parameter in order to return different kinds of song source and will pull whatever information they need from the request object to determine which source to return.
(defgeneric find-song-source (type request)
(:documentation "Find the song-source of the given type for the given request."))
However, for the purposes of this chapter, you can use a trivial implementation of this interface that always uses the same object, a simple queue of song objects that you can manipulate from the REPL. You can start by defining a class, simple-song-queue
, and a global variable, *songs*
, that holds an instance of this class.
(defclass simple-song-queue ()
((songs :accessor songs :initform (make-array 10 :adjustable t :fill-pointer 0))
(index :accessor index :initform 0)))
(defparameter *songs* (make-instance 'simple-song-queue))
Then you can define a method on find-song-source
that specializes type
with an **EQL**
specializer on the symbol singleton
and returns the instance stored in *songs*
.
(defmethod find-song-source ((type (eql 'singleton)) request)
(declare (ignore request))
*songs*)
Now you just need to implement methods on the three generic functions that the Shoutcast server will use.
(defmethod current-song ((source simple-song-queue))
(when (array-in-bounds-p (songs source) (index source))
(aref (songs source) (index source))))
(defmethod still-current-p (song (source simple-song-queue))
(eql song (current-song source)))
(defmethod maybe-move-to-next-song (song (source simple-song-queue))
(when (still-current-p song source)
(incf (index source))))
And for testing purposes you should provide a way to add songs to this queue.
(defun add-file-to-songs (file)
(vector-push-extend (file->song file) (songs *songs*)))
(defun file->song (file)
(let ((id3 (read-id3 file)))
(make-instance
'song
:file (namestring (truename file))
:title (format nil "~a by ~a from ~a" (song id3) (artist id3) (album id3))
:id3-size (size id3))))