the server is supposed to send metadata only if the client's original request contains a special Icy-Metadata header. And in order for the client to know how often to expect metadata, the server must send back a header Icy-Metaint whose value is the number of bytes of MP3 data that will be sent between each chunk of metadata.
The basic content of the metadata is a string of the form 'StreamTitle='
Thus, the smallest legal metadata chunk is a single byte, zero, indicating zero subsequent blocks. If the server doesn't need to update the metadata, it can send such an empty chunk, but it must send at least the one byte so the client doesn't throw away actual MP3 data.
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:
|
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.[299]
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))