Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
536 lines (459 sloc) 41.7 KB
\chapter{Практика. Сервер Shoutcast}
\label{ch:28}
\thispagestyle{empty}
В~этой главе вы разработаете ещё одну важную часть веб-приложения для потокового вещания
музыки в формате MP3, а именно сервер, реализующий протокол Shoutcast, который
выполняет потоковое вещание в формате MP3 пользовательским клиентам, таким как iTunes,
XMMS\footnote{Версия XMMS, поставляемая с Red Hat 8.0, 9.0 и Fedora не понимает как
проигрывать файлы в формате MP3, поскольку сотрудники Red Hat были озабочены
лицензионными аспектами использования кодеков MP3. Для того, чтобы XMMS поддерживал MP3
в этих версиях Linux, вам необходимо взять исходные тексты с \url{http://www.xmms.org} и
собрать их самостоятельно. Или посетите \url{http://www.fedorafaq.org/#xmms-mp3} для
получения информации о других возможностях поддержки MP3.}\hspace{\footnotenegspace} или Winamp.
\section{Протокол Shoutcast}
Протокол Shoutcast был создан сотрудниками компании Nullsoft, создателя программы
Winamp. Он был спроектирован для поддержки потокового вещания в Интернете~-- Shoutcast DJ
отправляет аудиофайлы с персональных компьютеров на центральный сервер Shoutcast, который
затем отправляет эти данные в виде потока любому из подключённых слушателей.
Сервер, который вы напишите, в действительности реализует только половину функциональности
настоящего сервера Shoutcast~-- вы будете использовать протокол, который используют
серверы Shoutcast для потокового вещания MP3 слушателям, но ваш сервер будет способен
передавать только те песни, которые уже загружены на тот компьютер, на котором выполняется
ваш сервер.
Вам необходимо знать только две части протокола Shoutcast: формат запроса, который клиент
делает для того, чтобы начать получать поток данных, и формат ответа, включая механизм,
который используется для вставки данных о проигрываемой композиции в поток данных.
Начальный запрос от клиента MP3 к серверу Shoutcast выглядит так же, как обычный запрос
протокола HTTP. В~ответе сервер Shoutcast отправляет ответ ICY, который выглядит так же,
как и ответ HTTP, за исключением строки \lstinline{ICY}\footnote{Чтобы сделать вещи ещё более
запутанными, я упомяну, что есть ещё один потоковый протокол, называемы Icecast. Но
между заголовком \lstinline{ICY}, используемым протоколом Shoutcast и протоколом Icecast, не
существует никаких связей.}\hspace{\footnotenegspace} вместо обычной строки версии \lstinline{HTTP} и немного
отличающихся заголовков. После отправки заголовков и пустой строки сервер начинает
отправлять потенциально бесконечный поток данных в формате MP3.
Единственной сложной вещью в протоколе Shoutcast является способ вставки
информации о песне в данные, отправляемые клиенту. Проблема, с которой столкнулись
дизайнеры протокола Shoutcast, заключалась в нахождении возможности передачи клиенту новой
информации о песне сервером Shoutcast при начале проигрывания новой песни, так что клиент
мог бы отображать эту информацию в интерфейсе. (Возвращаясь к главе~\ref{ch:25},
вспоминаем, что формат MP3 не обеспечивает механизмов для кодирования метаданных.) Хотя
одной из целей создания ID3v2 было обеспечение лучшей совместимости с потоковой передачей
файлов MP3, сотрудники Nullsoft решили идти своим путём и изобрели новую схему, которую
проще реализовать и клиенту, и серверу. Это, конечно, было идеальным случаем, поскольку они
сами были авторами клиента для проигрывания MP3.
Их решение заключалось в простом игнорировании структуры данных MP3 и вставке метаданных
каждые \lstinline{n} байт. И клиент принимал на себя ответственность за удаление метаданных из
потока, так чтобы они не рассматривались как данные MP3. Поскольку отправка метаданных
клиенту, который не готов к их приёму, может вызывать проблемы с воспроизведением звука,
то сервер должен отправлять метаданные, только если запрос содержит специальный заголовок
\lstinline{Icy-Metadata}. И для того чтобы клиент знал, как часто метаданные будут
передваться, сервер должен отправить клиенту заголовок \lstinline{Icy-Metaint}, чьим значением
является число байт данных в формате MP3, которые будут переданы между двумя пакетами с
метаданными.
Основное содержание метаданных~-- строка вида \lstinline{StreamTitle='title'};, где
\lstinline{title} является заголовком текущей песни и не может содержать знак одинарной
кавычки. Это содержимое закодировано как массив байт, разделённый указателями длины:
сначала отправляет одиночный байт, показывающий, сколько 16-байтовых блоков будет
отправлено, за которым следуют эти блоки. Они содержат саму строку в кодировке ASCII, и
последний блок дополнен нулевыми байтами до 16-байтовой границы.
Таким образом, наименьшим допустимым блоком метаданных является единственный байт, равный
нулю, что означает, что за ним не следует ни одного блока. Если сервер не нуждается в
обновлении метаданных, то он может отправить такой пустой блок, но он должен отправить как
минимум один байт, так что клиент не будет отбрасывать данные MP3.
\section{Источники песен}
Поскольку сервер Shoutcast должен продолжать передавать поток данных клиенту все время,
пока он подключён к нему, то вам необходимо обеспечить ваш сервер источником песен, из
которых он сможет брать данные. В~веб-приложении каждый подключённый клиент будет иметь
список песен, с которым он сможет работать через веб-интерфейс. Но, для того чтобы
избежать излишней зависимости между модулями, вы должны определить интерфейс, который
сможет использовать сервер Shoutcast для получения списка проигрываемых песен. Вы можете
сейчас написать простую реа\-ли\-за\-цию этого интерфейса и заменить её на более сложную при
написании веб-приложения, которое вы будете создавать в главе~\ref{ch:29}.
\vfill{}
\pagebreak{}
\begin{lrbox}{\chtwoeightone}
\begin{minipage}{\linewidth}
\begin{myverb}
(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*))
\end{myverb}
\end{minipage}
\end{lrbox}
\textintable{Пакет}{Объявление разрабатываемого вами пакета будет выглядеть примерно так:\\[-3pt]
\noindent{}\usebox{\chtwoeightone}\\
}
Основной идеей для создания интерфейса является то, что сервер Shoutcast будет находить
источник песен, основываясь на идентификаторе, выделенном из объекта AllegroServe,
представляющего запрос. Затем можно сделать следующие три действия над выделенным
источником песен:
\begin{itemize}
\item получить текущую песню из источника песен;
\item сообщить источнику песен, что мы закончили работу над текущей песней;
\item запросить у источника песен о том, все ещё является ли текущей та песня, которую мы
запрашивали ранее.
\end{itemize}
Последняя операция необходима, поскольку могут быть такие случаи, и мы увидим их в
главе~\ref{ch:29}, когда мы работаем с источником песен вне сервера Shoutcast. Вы можете
выразить операции, необходимые серверу Shoutcast, с помощью следующих обобщённых фунций:
\begin{myverb}
(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."))
\end{myverb}
Функция \lstinline{maybe-move-to-next-song} определена таким способом, что за одну
операцию проверяется, является ли данная песня текущей, и если это так, то источник песен
перемещается к следующей песне. Это будет важным в следующей главе, когда вам нужно будет
реализовать источник песен, который будет доступен из двух потоков выполнения\pclfootnote
технической точки зрения, реализация, представленная в данной главе, также вызывается из
двух потоков выполнения~-- из потока AllegroServe, который выполняет сервер Shoutcast, а
также из интерактивной консоли ввода команд. Но пока вы можете допустить наличие гонки
за ресурсами (race condition). В~следующей главе мы будем обсуждать вопрос
использования блокировок для создания безопасного кода.}.
Для представления информации о песне, которая необходима серверу Shoutcast, вы можете
определить класс \lstinline{song} со слотами, которые будут хранить имя файла MP3, заголовок,
который будет отправлен в качестве метаданных Shoutcast, и размер тега ID3, так что он
может быть пропущен во время передачи файла.
\begin{myverb}
(defclass song ()
((file :reader file :initarg :file)
(title :reader title :initarg :title)
(id3-size :reader id3-size :initarg :id3-size)))
\end{myverb}
Значение, возвращённое \lstinline{current-song} (оно же и является первым аргументом функций
\lstinline{still-current-p} и\lstinline{maybe-move-to-next-song}), будет экземпляром класса
\lstinline{song}.
Вдобавок к этому вам необходимо определить обобщённую функцию, которую сервер сможет
использовать для нахождения источника песен, основываясь на желательном типе источника и
объекте, представляющем запрос. Методы будут специализировать параметр \lstinline{type}, для
того чтобы возвращать разные виды источников песен, и будут вытягивать различную
информацию, в которой они нуждаются, для определения, какой источник песен необходимо
возвращать, из объекта \lstinline{request}.
\begin{myverb}
(defgeneric find-song-source (type request)
(:documentation "Find the song-source of the given type for the given request."))
\end{myverb}
Однако в данной главе вы можете использовать самую простую реализацию этого интерфейса,
которая будет всегда возвращать один и тот же объект~-- простую очередь объектов
\lstinline{song}, которой вы сможете управлять через строку ввода команд. Вы можете начать эту
реализацию путём определения класса \lstinline{simple-song-queue} и глобальной переменной
\lstinline{*songs*}, которая содержит экземпляр данного класса.
\begin{myverb}
(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))
\end{myverb}
Затем вы можете определить метод \lstinline{find-song-source}, специализированный через
\lstinline{EQL} для символа \lstinline{singleton}, который будет возвращать экземпляр объекта,
хранимый в переменной \lstinline{*songs*}.
\begin{myverb}
(defmethod find-song-source ((type (eql 'singleton)) request)
(declare (ignore request))
*songs*)
\end{myverb}
Теперь вам всего лишь надо реализовать методы для трёх обобщённых функций, которые будут
использоваться сервером Shoutcast.
\begin{myverb}
(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))))
\end{myverb}
И в целях тестирования вам необходимо обеспечить возможность добавления песен в очередь.
\begin{myverb}
(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))))
\end{myverb}
\section{Реализация сервера Shoutcast}
Теперь вы готовы к реализации сервера Shoutcast. Поскольку протокол Shoutcast практически
основан на HTTP, вы можете реализовать сервер в виде функции внутри AllegroServe. Однако
поскольку вам нужно будет взаимодействовать с некоторыми низкоуровневыми функциями
AllegroServe, то вы не сможете использовать макрос \lstinline{define-url-function} из
главы~\ref{ch:26}. Вместо этого вам нужно написать обычную функцию, которая будет
выглядеть примерно так:
\begin{myverb}
(defun shoutcast (request entity)
(with-http-response
(request entity :content-type "audio/MP3" :timeout *timeout-seconds*)
(prepare-icy-response request *metadata-interval*)
(let ((wants-metadata-p (header-slot-value request :icy-metadata)))
(with-http-body (request entity)
(play-songs
(request-socket request)
(find-song-source *song-source-type* request)
(if wants-metadata-p *metadata-interval*))))))
\end{myverb}
Затем опубликуйте эту функцию для пути \lstinline{/stream.mp3}, например вот так\footnote{Еще
одна вещь, которую вы можете захотеть сделать во время работы над этим кодом,~--
выполнить выражение \lstinline{(net.aserve::debug-on :notrap)}. Оно заставляет AllegroServe
не перехватывать ошибки, выданные вашим кодом, что позволит вам использовать стандартный
отладчик Lisp. В~SLIME это приведёт к показу буфера отладчика SLIME, так же как и для
обычной ошибки.}\hspace{\footnotenegspace}:
\begin{myverb}
(publish :path "/stream.mp3" :function 'shoutcast)
\end{myverb}
В~вызове \lstinline{with-http-response}, в добавление к стандартным параметрам \lstinline{request} и
\lstinline{entity}, вам необходимо передать аргументы \lstinline{:content-type} и \lstinline{:timeout}.
Аргумент \lstinline{:content-type} сообщает AllegroServe, как установить значение заголовка
\lstinline{Content-Type}. А аргумент \lstinline{:timeout} указывает количество времени (в
секундах), которое даёт AllegroServe функции для генерации ответа. По умолчанию
AllegroServe отменяет каждый запрос через пять минут. Поскольку вы собираетесь передавать
поток практически бесконечно, то вам необходимо указать большее значение. Не существует
способа указать AllegroServe, чтобы он не отменял запроса, так что вы должны установить
подходящее большое значение в переменной \lstinline{*timeout-seconds*}, например 10 лет,
переведённые в секунды.
\begin{myverb}
(defparameter *timeout-seconds* (* 60 60 24 7 52 10))
\end{myverb}
Затем внутри тела \lstinline{with-http-response} и до вызова \lstinline{with-http-body}, который
выполнит отправку заголовков ответа, вам необходимо напрямую поработать с ответом, который
отправит AllegroServe. Функция \lstinline{prepare-icy-response} выполняет все необходимые
действия: изменение строки протокола со значения по умолчанию~-- \lstinline{"HTTP"} на
\lstinline{"ICY"}~-- и добавление заголовков, специфических для Shoutcast\footnote{Заголовки
Shoutcast обычно посылаются в виде строк с символами в нижнем регистре, так что вам
необходимо замаскировать имена именованных параметров, используемых для заголовков в
AllegroServe, чтобы предотвратить их преобразование в верхний регистр при чтении
исходного текста. Так что вам нужно писать \lstinline{:|icy-metaint|} вместо обычного
\lstinline{:icy-metaint}. Вы также можете записать эту строку как
\lstinline!:\i\c\y-\m\e\t\a\i\n\t!, но это было бы глупо.}\hspace{\footnotenegspace}. Вам также необходимо
добавить код для обхода ошибки в iTunes, который заставит AllegroServe не использовать
поблочную передачу данных (chunked transfer-encoding)\footnote{Функция
\lstinline{turn-off-chunked-transfer-encoding} является хаком. Не существует способа
отключить поблочную передачу данных (chunked transfer encoding), используя официальный API AllegroServe и не
указывая длину содержимого, поскольку подразумевается, что любой клиент, который
объявляет себя поддерживающим HTTP 1.1 (что и делает iTunes), понимает этот способ
кодирования.}\hspace{\footnotenegspace}. Функции \lstinline{request-reply-protocol-string}, \lstinline{request-uri} и
\lstinline{reply-header-slot-value} являются частью AllegroServe.
\begin{myverb}
(defun prepare-icy-response (request metadata-interval)
(setf (request-reply-protocol-string request) "ICY")
(loop for (k v) in (reverse
`((:|icy-metaint| ,(princ-to-string metadata-interval))
(:|icy-notice1| "<BR>This stream blah blah blah<BR>")
(:|icy-notice2| "More blah")
(:|icy-name| "MyLispShoutcastServer")
(:|icy-genre| "Unknown")
(:|icy-url| ,(request-uri request))
(:|icy-pub| "1")))
do (setf (reply-header-slot-value request k) v))
;; iTunes, despite claiming to speak HTTP/1.1, doesn't understand
;; chunked Transfer-encoding. Grrr. So we just turn it off.
(turn-off-chunked-transfer-encoding request))
(defun turn-off-chunked-transfer-encoding (request)
(setf (request-reply-strategy request)
(remove :chunked (request-reply-strategy request))))
\end{myverb}
Внутри выражения \lstinline{with-http-body} функции \lstinline{shoutcast} вы выполняете потоковое
вещание в формате MP3. Функция \lstinline{play-songs} берёт поток, в который вы должны писать
данные, источник песен и интервал передачи метаданных, или \lstinline{NIL}, если клиент не
хочет получать метаданных. Поток~-- это сокет, полученный из объекта \lstinline{request},
источник песен получается при помощи функции \lstinline{find-song-source}, а интервал передачи
метаданных берётся из глобальной переменной \lstinline{*metadata-interval*}. Тип источника
песен контролируется переменной \lstinline{*song-source-type*}, который сейчас должен быть
установлен в значение \lstinline{singleton}, для того чтобы использовать
\lstinline{simple-song-queue}, которую мы уже реа\-ли\-зо\-ва\-ли.
\begin{myverb}
(defparameter *metadata-interval* (expt 2 12))
(defparameter *song-source-type* 'singleton)
\end{myverb}
Сама функция \lstinline{play-songs} не делает ничего сложного~-- она в цикле вызывает функцию
\lstinline{play-current}, которая берёт на себя всю тяжесть задачи по отправке содержимого
отдельного файла MP3, пропускания тегов ID3 и вставки метаданных ICY. Единственной
трудностью является отслеживание момента отправки метаданных.
Поскольку вы должны отправлять блоки метаданных через фиксированные интервалы, независимо
от того, когда вы переключаетесь с отправки одного файла на другой, то каждый раз, когда вы
вызываете \lstinline{play-current}, вам необходимо указать, когда следующие метаданные должны
быть переданы, и при возврате эта функция должна вернуть аналогичное значение, так что вы
сможете передать эти данные в следующем вызове \lstinline{play-current}. Если
\lstinline{play-current} получает \lstinline{NIL} от источника песен, то она также вернёт
\lstinline{NIL}, что позволяет завершить цикл \lstinline{LOOP} внутри \lstinline{play-songs}.
В~дополнение к выполнению цикла \lstinline{play-songs} также использует \lstinline{HANDLER-CASE}
для перехвата ошибок, которые будут выданы, когда клиент MP3 отключится от сервера, и одна
из процедур записи в \lstinline{play-current} приведёт к выдаче ошибки. Поскольку
\lstinline{HANDLER-CASE} находится вне \lstinline{LOOP}, то обработка ошибки приведёт к прерыванию
цикла, позволяет выполнить выход из \lstinline{play-songs}.
\begin{myverb}
(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))))
\end{myverb}
И теперь вы готовы к реализации функции \lstinline{play-current}, которая выполняет отправку
данных Shoutcast. Основная идея заключается в том, что вы получаете текущую песню от
источника песен, открываете файл, содержащий её, и затем выполняете цикл, в котором читаете
данные из файла и записываете их в сокет, до тех пор, пока вы не достигнете конца файла,
или текущая песня не перестанет быть текущей.
Имеются только две трудности: одна из них заключается в том, что вы должны быть уверены,
что вы отправляете метаданные через заданный интервал. Другой является то, что если файл
начинается с тега ID3, то вам нужно пропустить его. Если вы не особо беспокоитесь об
эффективности ввода-вывода, то вы можете реализовать \lstinline{play-current} вот так:
\begin{myverb}
(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)))
\end{myverb}
Эта функция получает текущую песню из источника песен и затем получает буфер, содержащий
метаданные путём передачи названия песни функции \lstinline{make-icy-metadata}. Потом она
открывает файл и пропускает тег ID3, используя функцию \lstinline{FILE-POSITION} с двумя
аргументами. Затем она начинает читать байты из файла и записывать их в
сокет\footnote{Большинство проигрывателей MP3 будет отображать метаданные где-то в
пользовательском интерфейсе. Однако программа XMMS в Linux по умолчанию не делает
этого. Чтобы заставить XMMS отображать метаданные Shoutcast, нажмите \lstinline{Ctrl+P} для
открытия диалога <<Preferences>> (Настройки). Затем во вкладке <<Audio I/O Plugins>>
(крайняя левая в версии 1.2.10) выберите пункт <<MPEG Layer 1/2/3 Player>>
(\lstinline{libmpg123.so}) и нажмите кнопку <<Configure>>. Затем выберите вкладку
<<Streaming>> и внизу, в разделе <<SHOUTCAST/Icecast>>, отметьте <<Enable
SHOUTCAST/Icecast title streaming>> кнопку.}\hspace{\footnotenegspace}.
Эта функция прервёт цикл, когда достигнет конца файла или когда источник песен изменит
текущую песню. Между тем, когда \lstinline{next-metadata} будет равен нулю (если вы вообще
будете отправлять метаданные), эта функция записывает метаданные в поток и сбрасывает
\lstinline{next-metadata} в начальное значение. После завершения цикла она проверяет,
является ли песня все ещё текущей в источнике песен, и если это так, то это значит, что мы
вышли из цикла из-за того, что прочитали весь файл, и в этом случае она сообщает источнику
песен о необходимости перемещения к следующей песне. В~противном случае цикл прерван
из-за того, что кто-то изменил текущую песню, и функция просто выполняет возврат без
дополнительных действий. В~любом случае, она возвращает число байт, оставшихся до
отправки следующей порции метаданных, так что это значение может быть использовано при
следующем вызове \lstinline{play-current}\footnote{Люди перешедшие на Common Lisp со Scheme,
могут удивляться, почему \lstinline{play-current} не может просто вызывать себя рекурсивно. В
Scheme это будет работать, поскольку в спецификации Scheme требуется, чтобы реализации
поддерживали <<an unbounded number of active tail calls (неограниченное количество
хвостовых вызовов)>>. Для реализаций Common Lisp разрешено иметь такое свойство, но оно
не требуется стандартом языка. Так что в Common Lisp основным способом создания циклов
является использование соответствующих конструкций, а не рекурсии.}\hspace{\footnotenegspace}.
Реализация функции \lstinline{make-icy-metadata}, которая получает название текущей песни
и формирует массив байт, содержащий правильно отформатированный блок метаданных ICY, также
проста\footnote{Эта функция предполагает, так же как и другой код, который вы пишете, что
в вашей реализации Lisp внутренней кодировкой для знаков является ASCII или надмножество
ASCII, так что вы можете использовать функцию \lstinline{CHAR-CODE} для преобразования
объеков типа \lstinline{CHARACTER} в данные в кодировке ASCII.}\hspace{\footnotenegspace}.
\begin{myverb}
(defun make-icy-metadata (title)
(let* ((text (format nil "StreamTitle='~a';" (substitute #\bslash{}Space #\bslash{}' 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))
\end{myverb}
В~зависимости от того, как конкретная реализация Lisp работает с потоками, и от того,
сколько клиентов MP3 вы хотите обрабатывать одновременно, простая версия
\lstinline{play-current} может быть достаточно эффективной или нет.
Потенциальной проблемой простой реализации может быть то, что она использует
\lstinline{READ-BYTE} и \lstinline{WRITE-BYTE} для передачи каждого байта. Возможно, что каждый
вызов может приводить к относительно затратному системному вызову чтения или записи одного
байта. И даже если в вашем Lisp реализованы потоки с внутренней буферизацией, так что не
каждый вызов \lstinline{READ-BYTE} и \lstinline{WRITE-BYTE} будет приводить к системному вызову, то
все равно вызов функции не является дешёвой операцией. В~частности, в реализациях,
которые предоставляют потоки, расширяемые пользователем, используя так называемые <<серые
потоки>> (Gray Streams), вызовы \lstinline{READ-BYTE} и \lstinline{WRITE-BYTE} могут приводить к
вызову обобщённых функций, которые будут приводить к неявной диспатчеризации вызова в
зависимости от класса потока. Хотя диспатчеризация обобщённой функции является достаточно
быстрой операцией и вы можете сильно не волноваться об этом, но все равно её вызов более
затратен, чем вызов обычной функции, и это не та вещь, которую вы захотите выполнять
несколько миллионов раз за несколько минут, особенно если вы можете избежать этого.
Более эффективный, но чуть более сложный способ реализации \lstinline{play-current}~-- читать и
записывать данные блоками, используя функции \lstinline{READ-SEQUENCE} и \lstinline{WRITE-SEQUENCE}.
Это также даёт вам шанс привести чтение данных в соответствие с размером блока данных
файловой системы, что обеспечит вам лучшую производительность диска. Конечно, вне
зависимости от того, какой размер блока вы будете использовать, отслеживание точки
отправки метаданных станет более сложной задачей. Более эффективная версия
\lstinline{play-current}, использующая функции \lstinline{READ-SEQUENCE} и \lstinline{WRITE-SEQUENCE},
может выглядеть вот так:
\begin{myverb}
(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
((> next-metadata (- end start))
(write-sequence buffer out :start start :end end)
(decf next-metadata (- end start)))
(t
(let ((middle (+ start next-metadata)))
(write-sequence buffer out :start start :end middle)
(write-sequence metadata out)
(setf next-metadata metadata-interval)
(write-buffer-with-metadata middle end))))))
(multiple-value-bind (skip-blocks skip-bytes)
(floor (id3-size song) (length buffer))
(unless (file-position mp3 (* skip-blocks (length buffer)))
(error "Couldn't skip over ~d ~d byte blocks."
skip-blocks (length buffer)))
(loop for end = (read-sequence buffer mp3)
for start = skip-bytes then 0
do (write-buffer start end)
while (and (= end (length buffer))
(still-current-p song song-source)))
(maybe-move-to-next-song song song-source)))))
next-metadata)))
\end{myverb}
Теперь вы готовы собрать все части вместе. В~следующей главе вы напишете веб-интерфейс
для сервера Shoutcast, разработанного в данной главе и использующего базу данных MP3 из
главы~\ref{ch:27} в качестве источника песен.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End: