current-song
if the song
object in current-song
no longer matches the file that the current-idx
slot says should be playing. Then, if you call this function after any manipulation of the playlist that could possibly put those two slots out of sync, you're sure to keep current-song
set properly. Here are update-current-if-necessary
and its helper functions:
(defun update-current-if-necessary (playlist)
(unless (equal (file (current-song playlist))
(file-for-current-idx playlist))
(reset-current-song playlist)))
(defun file-for-current-idx (playlist)
(if (at-end-p playlist)
nil
(column-value (nth-row (current-idx playlist) (songs-table playlist)) :file)))
(defun at-end-p (playlist)
(>= (current-idx playlist) (table-size (songs-table playlist))))
You don't need to add locking to these functions since they'll be called only from functions that will take care of locking the playlist first.
The function reset-current-song
introduces one more wrinkle: because you want the playlist to provide an endless stream of MP3s to the client, you don't want to ever set current-song
to NIL
. Instead, when a playlist runs out of songs to play—when songs- table
is empty or after the last song has been played and repeat
is set to :none
—then you need to set current-song
to a special song whose file is an MP3 of silence[308] and whose title explains why no music is playing. Here's some code to define two parameters, *empty-playlist-song*
and *end-of-playlist- song*
, each set to a song with the file named by *silence-mp3*
as their file and an appropriate title:
(defparameter *silence-mp3* ...)
(defun make-silent-song (title &optional (file *silence-mp3*))
(make-instance
'song
:file file
:title title
:id3-size (if (id3-p file) (size (read-id3 file)) 0)))
(defparameter *empty-playlist-song* (make-silent-song 'Playlist empty.'))
(defparameter *end-of-playlist-song* (make-silent-song 'At end of playlist.'))
reset-current-song
uses these parameters when the current-idx
doesn't point to a row in songs-table
. Otherwise, it sets current-song
to a song
object representing the current row.
(defun reset-current-song (playlist)
(setf
(current-song playlist)
(cond
((empty-p playlist) *empty-playlist-song*)
((at-end-p playlist) *end-of-playlist-song*)
(t (row->song (nth-row (current-idx playlist) (songs-table playlist)))))))
(defun row->song (song-db-entry)
(with-column-values (file song artist album id3-size) song-db-entry
(make-instance
'song
:file file
:title (format nil '~a by ~a from ~a' song artist album)
:id3-size id3-size)))
(defun empty-p (playlist)
(zerop (table-size (songs-table playlist))))
Now, at last, you can implement the method on maybe-move-to-next-song
that moves current-idx
to its next value, based on the playlist's repeat mode, and then calls update- current-if-necessary
. You don't change current-idx
when it's already at the end of the playlist because you want it to keep its current value, so it'll point at the next song you add to the playlist. This function must lock the playlist before manipulating it since it's called by the Shoutcast server code, which doesn't do any locking.
(defmethod maybe-move-to-next-song (song (playlist playlist))
(with-playlist-locked (playlist)
(when (still-current-p song playlist)
(unless (at-end-p playlist)
(ecase (repeat playlist)
(:song) ; nothing changes
(:none (incf (current-idx playlist)))
(:all (setf (current-idx playlist)
(mod (1+ (current-idx playlist))
(table-size (songs-table playlist)))))))
(update-current-if-necessary playlist))))
The rest of the playlist code is functions used by the Web interface to manipulate playlist
objects, including adding and deleting songs, sorting and shuffling, and setting the repeat mode. As in the helper functions in the previous section, you don't need to worry about locking in these functions because, as you'll see, the lock will be acquired in the Web interface function that calls these.
Adding and deleting is mostly a question of manipulating the songs-table
. The only extra work you have to do is to keep the current-song
and current-idx
in sync. For instance, whenever the playlist is empty, its current-idx
will be zero, and the current-song
will be the *empty-playlist-song*
. If you add a song to an empty playlist, then the index of zero is now in bounds, and you should change the current-song
to the newly added song. By the same token, when you've played all the songs in a playlist and current-song
is *end-of-playlist- song*
, adding a song should cause current-song
to be reset. All this really means, though, is that you need to call update-current-if-necessary
at the appropriate points.
Adding songs to a playlist is a bit involved because of the way the Web interface communicates which songs to add. For reasons I'll discuss in the next section, the Web interface code can't just give you a simple set of criteria to use in selecting songs from the database. Instead, it gives you the name of a column and a list of values, and you're supposed to add all the songs from the main database where the given column has a value in the list of