and one of the writes to the socket, down in play-current, fails. Since the HANDLER-CASE is outside the LOOP, handling the error will break out of the loop, allowing play-songs to return.

(defun play-songs (stream song-source metadata-interval)

(handler-case

(loop

for next-metadata = metadata-interval

then (play-current

stream

song-source

next-metadata

metadata-interval)

while next-metadata)

(error (e) (format *trace-output* 'Caught error in play-songs: ~a' e))))

Finally, you're ready to implement play-current, which actually sends the Shoutcast data. The basic idea is that you get the current song from the song source, open the song's file, and then loop reading data from the file and writing it to the socket until either you reach the end of the file or the current song is no longer the current song.

There are only two complications: One is that you need to make sure you send the metadata at the correct interval. The other is that if the file starts with an ID3 tag, you want to skip it. If you don't worry too much about I/O efficiency, you can implement play-current like this:

(defun play-current (out song-source next-metadata metadata-interval)

(let ((song (current-song song-source)))

(when song

(let ((metadata (make-icy-metadata (title song))))

(with-open-file (mp3 (file song))

(unless (file-position mp3 (id3-size song))

(error 'Can't skip to position ~d in ~a' (id3-size song) (file song)))

(loop for byte = (read-byte mp3 nil nil)

while (and byte (still-current-p song song-source)) do

(write-byte byte out)

(decf next-metadata)

when (and (zerop next-metadata) metadata-interval) do

(write-sequence metadata out)

(setf next-metadata metadata-interval))

(maybe-move-to-next-song song song-source)))

next-metadata)))

This function gets the current song from the song source and gets a buffer containing the metadata it'll need to send by passing the title to make-icy-metadata. Then it opens the file and skips past the ID3 tag using the two-argument form of FILE-POSITION. Then it commences reading bytes from the file and writing them to the request stream.[303]

It'll break out of the loop either when it reaches the end of the file or when the song source's current song changes out from under it. In the meantime, whenever next-metadata gets to zero (if you're supposed to send metadata at all), it writes metadata to the stream and resets next- metadata. Once it finishes the loop, it checks to see if the song is still the song source's current song; if it is, that means it broke out of the loop because it read the whole file, in which case it tells the song source to move to the next song. Otherwise, it broke out of the loop because someone changed the current song out from under it, and it just returns. In either case, it returns the number of bytes left before the next metadata is due so it can be passed in the next call to play-current.[304]

The function make-icy-metadata, which takes the title of the current song and generates an array of bytes containing a properly formatted chunk of ICY metadata, is also straightforward.[305]

(defun make-icy-metadata (title)

(let* ((text (format nil 'StreamTitle='~a';' (substitute #Space #' title)))

(blocks (ceiling (length text) 16))

(buffer (make-array (1+ (* blocks 16))

:element-type '(unsigned-byte 8)

:initial-element 0)))

(setf (aref buffer 0) blocks)

(loop

for char across text

for i from 1

do (setf (aref buffer i) (char-code char)))

buffer))

Depending on how your particular Lisp implementation handles its streams, and also how many MP3 clients you want to serve at once, the simple version of play-current may or may not be efficient enough.

The potential problem with the simple implementation is that you have to call READ- BYTE and WRITE-BYTE for every byte you transfer. It's possible that each call may result in a relatively expensive system call to read or write one byte. And even if Lisp implements its own streams with internal buffering so not every call to READ- BYTE or WRITE-BYTE results in a system call, function calls still aren't free. In particular, in implementations that provide user-extensible streams using so-called Gray Streams, READ-BYTE and WRITE-BYTE may result in a generic function call under the covers to dispatch on the class of the stream argument. While generic function dispatch is normally speedy enough that you don't have to worry about it, it's a bit more expensive than a nongeneric function call and thus not something you necessarily want to do several million times in a few minutes if you can avoid it.

A more efficient, if slightly more complex, way to implement play-current is to read and write multiple bytes at a time using the functions READ-SEQUENCE and WRITE-SEQUENCE. This also gives you a chance to match your file reads with the natural block size of the file system, which will likely give you the best disk throughput. Of course, no matter what buffer size you use, keeping track of when to send the metadata becomes a bit more complicated. A more efficient version of play-current that uses READ-SEQUENCE and WRITE-SEQUENCE might look like this:

(defun play-current (out song-source next-metadata metadata-interval)

(let ((song (current-song song-source)))

(when song

(let ((metadata (make-icy-metadata (title song)))

(buffer (make-array size :element-type '(unsigned-byte 8))))

(with-open-file (mp3 (file song))

(labels ((write-buffer (start end)

(if metadata-interval

(write-buffer-with-metadata start end)

(write-sequence buffer out :start start :end end)))

(write-buffer-with-metadata (start end)

(cond

Вы читаете Practical Common Lisp
Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату