Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

1587 lines (1329 sloc) 112.126 kb
\chapter{Практика: разбор ID3}
\label{ch:25}
\thispagestyle{empty}
Имея библиотеку для разбора двоичных данных, вы уже готовы к созданию кода для чтения и
записи в каком-то реальном двоичном формате, например формате тегов ID3. Теги ID3
используются для хранения дополнительной информации в звуковых файлах MP3. Работа с тегами
ID3 будет хорошей проверкой для библиотеки для работы с двоичными данными, потому что
формат ID3~--- это настоящий, используемый на практике формат~--- смесь инженерных
компромиссов и характерных решений, которые тем не менее выполняют своё назначение. На
тот случай, если вы случайно пропустили революцию свободного обмена данными, вот краткий
обзор того, что представляют собой теги ID3 и как они относятся к файлам MP3.
MP3, или звуковой слой 3 для MPEG\pclfootnote{MPEG Audio Layer 3.},~--- это формат для хранения
сжатых звуковых данных, разработанный исследователями из Фраунгоферовского института
ин\-тег\-раль\-ных схем и стандартизованный <<Группой экспертов кино>>\pclfootnote{Moving Picture
Experts Group.}, объединённым комитетом организаций ISO\pclfootnote{International
Organization for Standardization.} и IEC\pclfootnote{International Electrotechnical
Commission.}. Однако формат MP3 сам по себе определяет только то, как хранить звуковые
данные. Это не страшно до тех пор, пока все ваши звуковые файлы обрабатываются каким-то
одним приложением, которое может хранить эти метаданные вне звуковых файлов, сохраняя их
связь со звуковыми файлами. Однако как только люди стали обмениваться файлами MP3 через
Интернет через такие файлообменные системы, как Napster, они быстро обнаружили, что нужно
как-то вставлять метаданные внутрь самих файлов MP3.
Поскольку стандарт MP3 уже был оформлен и значительная часть программного обеспечения и
оборудования уже была написана и спроектирована, причём так, что они знали, как
декодировать существующий формат файлов MP3, то любая схема внедрения информации в файл
MP3 была бы такой, что эта информация вынужденно была бы невидима декодерам файлов MP3. Вот
тут и появился ID3.
Первоначально формат ID3, изобретённый программистом Эриком Кэмпом (Eric Kemp),
представлял собой 128 байт, прилепленных в конце файла MP3, где их не замечало бы
большинство программ, работающих с MP3. Эта информация состояла из четырёх
тридцатибуквенных полей, являвшихся названием песни, названием альбома, именем исполнителя
и комментарием, одного четырёхбайтового поля года и одного однобайтового поля кода жанра
произведения. Кэмп придумал стандартные значения первых 80 кодов жанров. Nullsoft,
производитель программы Winamp, очень популярного MP3-плеера, позже добавили в этот список
ещё что-то около 60 жанров.
Этот формат было легко разбирать, но он был достаточно ограничен. Не было способа
сохранить названия более чем в 30 символов, было ограничение в 256 жанров, и значения
кодов жанров должны были одинаково восприниматься всеми пользователями, использующими
ID3. Не было даже способа сохранить номер дорожки на исходном диске до тех пор, пока
другой программист, Микаэль Мутшлер (Michael Mutschler), не предложил вставлять
номер дорожки в поле комментария, отделяя его от остального комментария нулевым байтом,
так чтобы существующее ПО, использующее ID3, которое предположительно читало бы до
первого нулевого символа в каждом текстовом поле, игнорировало бы его. Версия Кэмпа теперь
называется <<ID3 версия 1>> (ID3v1), а версия Мутшлера~--- <<ID3 версия 1.1>> (ID3v1.1).
Предложения первой версии, как бы ограничены не были, являлись хотя бы частичным
решением проблемы хранения метаданных, так что они были применены многими программами
копирования музыки\pclfootnote{Выдирание (ripping)~--- процесс, при помощи которого аудио-CD
преобразуется в MP3-файл на вашем жёстком диске. В~наши дни большинство таких программ
ещё и автоматически получают информацию о песнях, которые они выдирают, из онлайновых
баз данных, таких так Gracenote (the Compact Disc Database [CDDB]) или FreeDB, которую
они затем встраивают в MP3-файлы и ID3-теги.}, сохранявшими теги ID3 в файлах MP3, и
MP3-плеерами, вытаскивавшими эту информацию из тегов ID3 и показывавшими их пользователю.
Однако к 1998 году все эти ограничения стали совсем уже раздражающими, и новая группа
разработчиков, возглавляемая Мартином Нильсоном (Martin Nilsson), начала работу
над совершенно новой схемой хранения метаданных, которую назвали ID3v2. Формат ID3v2 крайне гибок,
разрешает включать много видов информации практически без ограничения длины. Также он
берёт на вооружение некоторые особенности формата MP3-файла, для того чтобы разместить
теги ID3v2 в начале файла MP3.
Однако разбирать теги в формате ID3v2~--- задача, значительно более сложная, чем теги в
формате версии~1. В~этой главе мы будем использовать библиотеку разбора бинарных данных из
предыдущей главы, для того чтобы разработать код, который сможет читать и писать теги в
формате ID3v2. Ну или, по крайней мере, сделаем какое-то приемлемое начало, поскольку если
ID3v1 достаточно прост, то ID3v2 порой причудлив до невозможности. Реализация всех
закоулков и потаённых уголков спецификации была бы порядочно сложной работой, особенно
если бы вы хотели поддержать все три версии, которые были документированы. На самом деле
вы можете игнорировать многие возможности в этих спецификациях, поскольку они очень редко
используются в <<дикой природе>>. В~качестве закуски вы можете опустить поддержку всей
версии~2.4, поскольку она не была широко воспринята и в основном всего лишь добавляла
некую вовсе не нужную гибкость, по сравнению с версией~2.3. Я сконцентрируюсь на версиях~2.2
и~2.3, потому что обе они широко используются и достаточно сильно отличаются друг от
друга, чтобы сделать нашу работу интересной.
\section{Структура тега ID3v2}
До того, как начать кодировать, вам нужно познакомиться с общей структурой тегов
ID3v2. Каждый тег начинается с заголовка, содержащего информацию о теге в общем. Первые
три байта заголовка содержат строку <<ID3>> в кодировке ISO-8859-1. То есть это байты с
кодами 73, 68 и~51. Затем идут два байта, которые кодируют <<старшую версию>> и ревизию
спецификации ID3, которой тег намеревается соответствовать. Далее идёт один байт, биты
которого интерпретируются как различные флаги. Значение каждого из флагов зависит от
версии спецификации. Некоторые из флагов могут влиять на то, как обрабатывается весь тег
целиком. Байты <<старшей версии>> на самом деле используются для записи младшей версии
спецификации, в то время как ревизия используется для хранения подверсии
спецификации. Таким образом, поле <<старшая версия>> тега, соответствующего спецификации
версии 2.3.0, будет 3. Поле ревизии всег\-да равно нулю, поскольку каждая новая спецификация
ID3v2 увеличивала младшую версию, оставляя подверсию нулём. Значение, хранимое в поле
старшей версии тега, как вы увидите, имеет сильное влияние на то, как надо разбирать всю
оставшуюся часть тега.
Последнее поле в заголовке тега~--- это число, закодированное в четырёх байтах, в каждом из
которых используется лишь по семь бит, содержащее размер всего тега без учёта заголовка.
В~тегах версии 2.3 в заголовке может быть ещё несколько дополнительных полей; все
остальное~--- это данные, разделённые на фреймы. Разные типы фреймов хранят разные виды
информации: от простого текста вроде названия песни до встроенного изображения. Каждый
фрейм начинается с заголовка, содержащего строковый идентификатор и размер. В~версии 2.3
заголовок фрейма также содержит два байта флагов и~--- при выставленном флаге~---
дополнительный однобайтовый код, указывающий, как закодирован остаток фрейма.
Фреймы~--- идеальный пример помеченных структур данных: чтобы распарсить тело
фрейма, надо прочитать заголовок и использовать идентификатор для определения того, какой вид
данных вы читаете.
Заголовок ID3 не указывает прямо, сколько фреймов в теге,~--- он говорит, насколько тот
большой, но раз фреймы могут быть разной длины, единственным способом узнать количество
фреймов будет прочитать их данные. К тому же размер, записанный в заголовке, может быть
больше, чем реальное количество байт в данных фреймов; после фреймов могут идти нули для
выравнивания под указанный размер. Это позволяет программам изменять тег без
переписывания всего MP3-файла\pclfootnote{Почти все файловые системы предоставляют
возможность перезаписывать существующие байты файла, но немноие~--- если вообще такие
есть~--- дают возможность добавлять или удалять данные в начало или середину файла без
необходимости перезаписать остаток файла. Так как теги ID3 обычно хранятся в начале
файла, чтобы перезаписать тег ID3, не трогая оставшуюся часть файла, вы должны заменить
старый тег новым точно такой же длины. Записывая теги ID3 с некоторым количеством
заполнения, вы получаете лучшие шансы сделать так: если в новом теге будет больше
данных, чем в первоначальном, вы используете меньше заполнителя, а если короче~---
больше.}.
Итак, наши главные задачи: чтение заголовка ID3; определение версии, 2.2 или 2.3; чтение
данных всех фреймов до конца тега или до блока выравнивания.
\section{Определение пакета}
Как и с другими библиотеками, которые мы разработали ранее, тот код, который мы напишем в
этой главе, стоит поместить в отдельный пакет. Нам надо будет обращаться к функциям
из библиотек binary и pathname из глав~\ref{ch:15} и~\ref{ch:24} и надо экспортировать
имена функций, которые составляют API этого пакета. Определим его так:
\begin{myverb}
(defpackage :com.gigamonkeys.id3v2
(:use :common-lisp
:com.gigamonkeys.binary-data
:com.gigamonkeys.pathnames)
(:export
:read-id3
:mp3-p
:id3-p
:album
:composer
:genre
:encoding-program
:artist
:part-of-set
:track
:song
:year
:size
:translated-genre))
\end{myverb}
Как обычно, вы можете, и наверное, вам даже следует, заменить <<com.gigamonkeys>> в имени
пакета на ваш собственный домен.
\section{Типы целых}
Можно начать с определения бинарных типов для чтения и записи некоторых примитивов,
использующихся в формате ID3,
нескольких целочисленных типов разного размера и четырёх видов строк.
ID3 использует беззнаковые целые, закодированные в одном, двух, трёх или четырёх байтах.
Если вы сначала напишете обобщённый двоичный тип \lstinline{unsigned-integer},
который принимает в качестве параметра количество читаемых байтов, то затем
с помощью короткой формы \lstinline{define-binary-type} можно будет определять
конкретные типы. Обобщённый тип \lstinline{unsigned-integer} выглядит следуюшим образом:
\begin{myverb}
(define-binary-type unsigned-integer (bytes)
(:reader (in)
(loop with value = 0
for low-bit downfrom (* 8 (1- bytes)) to 0 by 8 do
(setf (ldb (byte 8 low-bit) value) (read-byte in))
finally (return value)))
(:writer (out value)
(loop for low-bit downfrom (* 8 (1- bytes)) to 0 by 8
do (write-byte (ldb (byte 8 low-bit) value) out))))
\end{myverb}
Теперь можно пользоваться короткой формой define-binary-type для определения типов для
каждого размера целого из формата ID3:
\begin{myverb}
(define-binary-type u1 () (unsigned-integer :bytes 1))
(define-binary-type u2 () (unsigned-integer :bytes 2))
(define-binary-type u3 () (unsigned-integer :bytes 3))
(define-binary-type u4 () (unsigned-integer :bytes 4))
\end{myverb}
Еще один тип, который надо уметь читать и писать,~--- это 28-битное значение из заголовка.
Это размер, закодированный не как обычно~--- количеством бит, кратным 8, таким как 32, а
28-ю, потому что тег ID3 не может содержать байт \lstinline!#xff!, за
которым идут три включённых бита~--- такая последовательность для
MP3-декодеров имеет особое значение. В~принципе, ни одно поле в заголовке ID3 не может содержать такую
последовательность байтов, но если бы размер тега был закодирован обычным беззнаковым
целым, то были бы проблемы. Чтобы исключить такую возможность, размер кодируется в семи
младших битах каждого байта, все старшие всегда нули\pclfootnote{Данные фреймов, идущих за
заголовком ID3, также потенциально могут содержать эту незаконную
последовательность. Это предотвращается использованием специальной схемы, которая
включается при помощи одного из флагов в заголовке тега. Код из этой главы не принимает
в расчёт возможность установки этого флага, он редко используется на практике.}.
Таким образом, оно может быть считано и записано во многом как беззнаковое целое, только
размер байта, который передаётся в LDB, должен быть 7, а не 8. Это сходство наводит на
мысль, что если добавить параметр bits-per-byte к существующему бинарному типу
unsigned-integer, тогда можно определить новый тип id3-tag-size, используя короткую форму
define-binary-type. Новая версия unsigned-integer такая же, как старая, только
bits-per-byte заменяет прописанную везде в старой восьмёрку. Выглядит так:
\begin{myverb}
(define-binary-type unsigned-integer (bytes bits-per-byte)
(:reader (in)
(loop with value = 0
for low-bit downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte do
(setf (ldb (byte bits-per-byte low-bit) value) (read-byte in))
finally (return value)))
(:writer (out value)
(loop for low-bit downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte
do (write-byte (ldb (byte bits-per-byte low-bit) value) out))))
\end{myverb}
Теперь определение id3-tag-size становится тривиальным:
\begin{myverb}
(define-binary-type id3-tag-size () (unsigned-integer :bytes 4 :bits-per-byte 7))
\end{myverb}
Также надо изменить определения u1-u4 для указания, что там 8 бит в байте:
\begin{myverb}
(define-binary-type u1 () (unsigned-integer :bytes 1 :bits-per-byte 8))
(define-binary-type u2 () (unsigned-integer :bytes 2 :bits-per-byte 8))
(define-binary-type u3 () (unsigned-integer :bytes 3 :bits-per-byte 8))
(define-binary-type u4 () (unsigned-integer :bytes 4 :bits-per-byte 8))
\end{myverb}
\section{Типы строк}
Еще один из примитивных типов, который повсеместен в теге ID3,~--- это строка.
В~предыдущей главе мы обсудили некоторые вещи, на которые надо обратить
внимание, когда имеешь дело со строками в бинарных файлах, такие как разница между кодом
знака и кодировкой.
ID3 использует две разные кодировки: ISO 8859-1 и Unicode. ISO 8859-1, также известный как
Latin-1,~--- это 8-битная кодировка, которая дополняет ASCII буквами
из языков Восточной Европы. Другими словами, одни и те же коды от 0 до 127 указывают на
одни и те же знаки ASCII и ISO 8859-1, но ISO 8859-1 содержит также символы с кодами до 255.
Unicode~--- это кодировка, сделанная, чтобы обеспечить кодом практически каждый
знак всех на свете языков. Unicode~--- надмножество ISO 8859-1, так же
как ISO 8859-1~--- надмножество ASCII: коды 0--255 отображаются на одни и те же знаки ISO
8859-1 и Unicode. (Таким образом, Unicode ещё и надмножество ASCII.)
Поскольку ISO 8859-1 является 8-битной кодировкой, она использует один байт на
знак. Для Unicode-строк ID3 использует кодировку UCS-2 с меткой порядка байтов\pclfootnote
ID3v2.4 UCS-2 заменили на почти идентичную ей UTF-16 и добавили дополнительные кодировки
UTF-16BE и UTF-8.}. Через пару мгновений я расскажу, что это такое.
Чтение и запись этих двух кодировок не является проблемой~--- это всего лишь вопрос чтения
и записи беззнаковых чисел в разных форматах, и мы только что написали код для этого.
Трюк в том, чтобы перевести эти числовые значения в объекты знаков языка Lisp.
Ваша реализация Lisp, возможно, использует или Unicode, или ISO 8859-1 в качестве внутренней
кодировки. И раз все значения от 0 до 255 отображаются на одни и те же знаки в ISO 8859-1
и Unicode, то можно использовать функции \lstinline{CODE-CHAR} и \lstinline{СHAR-CODE} для их транслирования в обе
кодировки. Однако, если ваш Lisp поддерживает только ISO 8859-1, тогда можно будет
в качестве символов Lisp использовать только первые 255 символов Unicode. Другими словами, в
такой реализации Lisp, если вы попробуете обработать тег ID3, который использует строки
Unicode, и любая из этих строк содержит знак с кодом, большим 255, то вы получите ошибку,
когда попытаетесь перевести этот код в символ Lisp. Пока будем считать, что мы
или используем Lisp, поддерживающий Unicode, или не будем работать с файлами, содержащими
знаки вне досягаемости ISO 8859-1.
Ещё один момент с кодированием строк состоит в том, что необходимо выяснить, какое количество
байт следует интерпретировать как символьные данные.
ID3 использует две стратегии, рассмотренные в предыдущей главе: некоторые
строки заканчиваются нулевым символом, тогда как другие встречаются на позициях, по
которым можно определить количество байт для считывания: или когда строка в том
расположении всегда одной длины, или когда она в конце составной структуры, чей размер
известен. Тем не менее обратите внимание, что количество байт не обязательно совпадает с
количеством знаков в строке.
Складывая все эти варианты вместе, получим, что формат ID3 использует четыре способа
чтения и записи строк: два вида знаков на два вида разграничения строковых данных.
Очевидно, значительная часть логики чтения и записи строк будет полностью совпадать. Так
что можно начать с определения двух бинарных типов: один для чтения строк фиксированной длины
(в знаках) и другой для чтения строк, заканчивающихся заданным символом.
Оба пользуются тем, что тип, передаваемый в \lstinline{read-value} и \lstinline{write-value},~---
это такие же данные; тип читаемого символа можно сделать параметром
этих типов. Этой техникой мы будем пользоваться в данной главе довольно часто.
\begin{myverb}
(define-binary-type generic-string (length character-type)
(:reader (in)
(let ((string (make-string length)))
(dotimes (i length)
(setf (char string i) (read-value character-type in)))
string))
(:writer (out string)
(dotimes (i length)
(write-value character-type out (char string i)))))
(define-binary-type generic-terminated-string (terminator character-type)
(:reader (in)
(with-output-to-string (s)
(loop for char = (read-value character-type in)
until (char= char terminator) do (write-char char s))))
(:writer (out string)
(loop for char across string
do (write-value character-type out char)
finally (write-value character-type out terminator))))
\end{myverb}
С этими типами несложно будет прочитать строки ISO 8859-1. Поскольку \lstinline{character-type},
который передаётся в \lstinline{read-value} и \lstinline{write-value}, должен быть именем бинарного типа, то надо
определить \lstinline{iso-8859-1-char}. Здесь же неплохо организовать проверку корректности читаемых
и записываемых кодов символов.
\begin{myverb}
(define-binary-type iso-8859-1-char ()
(:reader (in)
(let ((code (read-byte in)))
(or (code-char code)
(error "Character code ~d not supported" code))))
(:writer (out char)
(let ((code (char-code char)))
(if (<= 0 code #xff)
(write-byte code out)
(error "Illegal character for iso-8859-1 encoding: character: ~c with code: ~d"
char code)))))
\end{myverb}
Теперь определение строк ISO 8859-1 становится тривиальным:
\begin{myverb}
(define-binary-type iso-8859-1-string (length)
(generic-string :length length :character-type 'iso-8859-1-char))
(define-binary-type iso-8859-1-terminated-string (terminator)
(generic-terminated-string :terminator terminator :character-type 'iso-8859-1-char))
\end{myverb}
Чтение строк UCS-2 лишь немногим сложнее. Трудности возникают из-за того, что можно
кодировать UCS-2 двумя способами: в порядке байтов от старшего к младшему (big-endian) или
от младшего к старшему (little-endian). Поэтому строки UCS-2 начинаются с двух
дополнительных байтов, которые называются меткой порядка байтов, состоящих из числового
значения \lstinline!#xfeff!, закодированных или в порядке big-endian, или в little-endian.
При чтении строки UCS-2 надо прочитать метку порядка байтов, а потом, в зависимости от её
значения, читать знаки в порядке big-endian или в little-endian. Так что понадобятся два
разных типа знаков UCS-2. Но нужна только одна версия кода проверки корректности. Значит,
можно определить параметризованный бинарный тип:
\begin{myverb}
(define-binary-type ucs-2-char (swap)
(:reader (in)
(let ((code (read-value 'u2 in)))
(when swap (setf code (swap-bytes code)))
(or (code-char code) (error "Character code ~d not supported" code))))
(:writer (out char)
(let ((code (char-code char)))
(unless (<= 0 code #xffff)
(error "Illegal character for ucs-2 encoding: ~c with char-code: ~d" char code))
(when swap (setf code (swap-bytes code)))
(write-value 'u2 out code))))
\end{myverb}
\noindent{}где функция \lstinline{swap-bytes} определена ниже, с использованием преимущества функции LDB, с
которой можно делать SETF и, соответственно, ROTATEF.
\begin{myverb}
(defun swap-bytes (code)
(assert (<= code #xffff))
(rotatef (ldb (byte 8 0) code) (ldb (byte 8 8) code))
code)
\end{myverb}
Используя ucs-2-char, определим два типа знаков, которые будут применяться в качестве
аргумента \lstinline{character-type} функций обобщённых строк.
\begin{myverb}
(define-binary-type ucs-2-char-big-endian () (ucs-2-char :swap nil))
(define-binary-type ucs-2-char-little-endian () (ucs-2-char :swap t))
\end{myverb}
Затем нужна функция, которая возвращает тип знаков, которые будут использоваться в
зависимости от метки порядка байтов.
\begin{myverb}
(defun ucs-2-char-type (byte-order-mark)
(ecase byte-order-mark
(#xfeff 'ucs-2-char-big-endian)
(#xfffe 'ucs-2-char-little-endian)))
\end{myverb}
Теперь можно определить оба строковых типа для строк UCS-2,
которые читают метку порядка байтов и определяют, какой вариант знаков UCS-2 передавать в
качестве аргумента \lstinline{character-type} в \lstinline{read-value} и \lstinline{write-value}. Остаётся только учесть,
что надо переводить аргумент length, который дан в байтах, в количество
читаемых знаков, учитывая при этом метку порядка байтов.
\begin{myverb}
(define-binary-type ucs-2-string (length)
(:reader (in)
(let ((byte-order-mark (read-value 'u2 in))
(characters (1- (/ length 2))))
(read-value
'generic-string in
:length characters
:character-type (ucs-2-char-type byte-order-mark))))
(:writer (out string)
(write-value 'u2 out #xfeff)
(write-value
'generic-string out string
:length (length string)
:character-type (ucs-2-char-type #xfeff))))
(define-binary-type ucs-2-terminated-string (terminator)
(:reader (in)
(let ((byte-order-mark (read-value 'u2 in)))
(read-value
'generic-terminated-string in
:terminator terminator
:character-type (ucs-2-char-type byte-order-mark))))
(:writer (out string)
(write-value 'u2 out #xfeff)
(write-value
'generic-terminated-string out string
:terminator terminator
:character-type (ucs-2-char-type #xfeff))))
\end{myverb}
\section{Заголовок тега ID3}
Закончив с основными примитивными типами, мы готовы перейти к более общей картине и начать
определять бинарные классы для представления сначала тега ID3 в целом, а потом и отдельных
фреймов.
Если заглянуть в спецификацию ID3v2.2, то мы увидим, что в основе структуры тега такой заголовок:
\begin{myverb}
ID3/file identifier "ID3"
ID3 version $02 00
ID3 flags %xx000000
ID3 size 4 * %0xxxxxxx
\end{myverb}
%$
за которым идут данные фреймов и выравнивание. Поскольку мы уже определили типы для
чтения и записи всех полей в этом заголовке, определение класса, который сможет читать
заголовок ID3,~--- это всего лишь вопрос их объединения.
\begin{myverb}
(define-binary-class id3-tag ()
((identifier (iso-8859-1-string :length 3))
(major-version u1)
(revision u1)
(flags u1)
(size id3-tag-size)))
\end{myverb}
Если у вас под рукой есть какой-нибудь MP3-файл, вы можете проверить всю эту кучу кода и
заодно посмотреть, какую версию тега ID3 он содержит. Для начала напишем функцию, которая
считывает только что определённый id3-tag из начала файла. Надо понимать тем не менее,
что тег ID3 не обязан находиться в начале файла, хотя в наши дни он почти всегда там.
Чтобы найти тег ID3 где-то ещё в файле, последний можно просканировать в поисках
последовательности байтов 73, 68, 51 (другими словами, это строка <<ID3>>)\pclfootnote{Версия
2.4 формата ID3 также поддерживает размещение похожего окончания в конце тега, что
позволяет проще находить тег, присоединённый к концу файла.}. Правда, сейчас уже,
наверное, можно считать, что файлы начинаются с тегов.
\begin{myverb}
(defun read-id3 (file)
(with-open-file (in file :element-type '(unsigned-byte 8))
(read-value 'id3-tag in)))
\end{myverb}
На основе этой функции можно написать другую, которая получает имя файла и печатает
информацию из заголовка тега вместе с именем файла.
\begin{myverb}
(defun show-tag-header (file)
(with-slots (identifier major-version revision flags size) (read-id3 file)
(format t "~a ~d.~d ~8,'0b ~d bytes -- ~a~%"
identifier major-version revision flags size (enough-namestring file))))
\end{myverb}
Она выдаст примерно следующее:
\begin{myverb}
ID3V2> (show-tag-header "/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3")
ID3 2.0 00000000 2165 bytes -- Kitka/Wintersongs/02 Byla Cesta.mp3
NIL
\end{myverb}
Конечно, чтобы определить, какая версия ID3 встречается чаще всего в вашей библиотеке,
лучше бы иметь функцию, которая выдаёт сводку по всем MP3-файлам в директории. Такую легко
реализовать с помощью функции walk-directory из главы~\ref{ch:15}. Для начала определим
вспомогательную функцию, которая проверяет, что у файла расширение MP3.
\begin{myverb}
(defun mp3-p (file)
(and
(not (directory-pathname-p file))
(string-equal "mp3" (pathname-type file))))
\end{myverb}
Затем соединим \lstinline{show-tag-header}, \lstinline{mp3-p} с \lstinline{walk-directory}, чтобы
печатать сводку по заголовкам ID3 в файлах в заданном каталоге.
\begin{myverb}
(defun show-tag-headers (dir)
(walk-directory dir #'show-tag-header :test #'mp3-p))
\end{myverb}
Однако, если у вас много MP3-файлов, вы можете пожелать просто посчитать, сколько тегов
ID3 каждой версии у вас в MP3-коллекции. Для получения этой информации можно было бы
написать такую функцию:
\begin{myverb}
(defun count-versions (dir)
(let ((versions (mapcar #'(lambda (x) (cons x 0)) '(2 3 4))))
(flet ((count-version (file)
(incf (cdr (assoc (major-version (read-id3 file)) versions)))))
(walk-directory dir #'count-version :test #'mp3-p))
versions))
\end{myverb}
Другая функция, которая понадобится в главе~\ref{ch:29}, для проверки, что файл
действительно начинается с тега ID3, которую можно определить вот так:
\begin{myverb}
(defun id3-p (file)
(with-open-file (in file :element-type '(unsigned-byte 8))
(string= "ID3" (read-value 'iso-8859-1-string in :length 3))))
\end{myverb}
\section{Фреймы ID3}
Как уже обсуждалось ранее, основная часть тега ID3 разделена на фреймы. Каждый фрейм
имеет структуру, похожую на структуру всего тега. Каждый фрейм начинается с заголовка,
указывающего вид фрейма и размер фрейма в байтах. Структура заголовка фрейма немного
разная у версий 2.2 и 2.3 формата ID3, и так получилось, что нам придётся работать с
обеими формами. Для начала сфокусируемся на разборе версии 2.2.
Заголовок в версии 2.2 состоит из трёх байт, которые кодируют трёхбуквенную ISO 8859-1
строку, за которой идёт трёхбайтовое беззнаковое число, задающее размер
фрейма в байтах без шестибайтового заголовка. Строка указывает тип фрейма, что
определяет, как мы будем разбирать данные. Это как раз та ситуация, для которой мы
определили макрос \lstinline{define-tagged-binary-class}. Мы можем определить помеченный класс,
который читает заголовок фрейма и затем подбирает подходящий конкретный класс, используя
функцию, которая отображает ID на имя класса.
\begin{myverb}
(define-tagged-binary-class id3-frame ()
((id (iso-8859-1-string :length 3))
(size u3))
(:dispatch (find-frame-class id)))
\end{myverb}
Теперь мы готовы начать строить реализацию конкретных классов фреймов. Однако
спецификация определяет достаточно большое количество типов фреймов~--- 63 в версии 2.2 и
еще больше в более поздних версиях. Даже считая типы фреймов, которые имеют общую
структуру, эквивалентными, мы все ещё получим 24 уникальных типа в версии 2.2. Но только
несколько из них используются на практике. Так что, вместо того чтобы сразу приступить к
определению классов для каждого из типа фреймов, вы можете начать с написания обобщенного
класса фреймов, который позволит вам читать фреймы в тег без разбора самих данных. Это
даст вам возможность определить, какие фреймы в самом деле присутствуют в файлах MP3,
которые вы хотите обрабатывать. Вам все равно понадобится этот класс, поскольку
спецификация разрешает включение эксперементальных фреймов, которые вам нужно будет уметь
читать без разбора данных в них.
Так как поле размера из заголовка фрейма точно говорит вам, какова длина фрейма в байтах,
вы можете определить класс \lstinline{generic-frame} (обобщённый фрейм), который расширяет \lstinline{id3-frame} и
добавляет единственное поле, data, которое будет содержать массив байт.
\begin{myverb}
(define-binary-class generic-frame (id3-frame)
((data (raw-bytes :size size))))
\end{myverb}
Тип поля data, \lstinline{raw-bytes}, должен просто содержать массив байт. Вы можете определить его
вот так:
\begin{myverb}
(define-binary-type raw-bytes (size)
(:reader (in)
(let ((buf (make-array size :element-type '(unsigned-byte 8))))
(read-sequence buf in)
buf))
(:writer (out buf)
(write-sequence buf out)))
\end{myverb}
На данный момент нам нужно, чтобы все фреймы читались как \lstinline{generic-frame}, так что можно
определить функцию \lstinline{find-frame-class}, которая используется в выражении \lstinline{:dispatch} в классе
\lstinline{id3-frame}, так чтобы она всегда возвращала \lstinline{generic-frame}, не обращая внимания на
индентификатор фрейма.
\begin{myverb}
(defun find-frame-class (id)
(declare (ignore id))
'generic-frame)
\end{myverb}
Вам придётся модифицицировать \lstinline{id3-tag} так, что он будет читать фреймы после полей
заголовка. Есть только одна маленькая трудность в чтении данных фреймов: несмотря на то
что заголовок тега указывает, каков его размер, в это число включён и заполнитель,
который может идти за данными фреймов. Так как заголовок тега не говорит вам, сколько
фреймов содержит тег, единственный способ определить, что вы натолкнулись на
заполнитель,~--- найти нулевой байт там, где вы ожидали идентификатор фрейма.
Чтобы управиться с этим, можно определить бинарный тип \lstinline{id3-frames}, который будет
ответственен за чтение остатка тега, создание объектов фреймов для представления всех
найденных фреймов и пропуск заполнителя. Этот тип будет принимать как параметр размер
тега, который он сможет использовать, чтобы избежать чтения за концом тега. Но читающему
коду ещё и придётся определять начало заполнителя, который может следовать за данными
фрейма в теге. Вместо того чтобы вызывать \lstinline{read-value} прямо в форме \lstinline{:reader} типа
\lstinline{id3-frames}, лучше использовать функцию \lstinline{read-frame}, определив её так, чтобы она возвращала
\lstinline{NIL}, когда обнаружит заполнитель, иначе возвращая объект \lstinline{id3-frame}, прочитанный через
\lstinline{read-value}. Предпологая, что \lstinline{read-frame} определена так, что она читает только один байт
после конца предыдущего фрейма для обнаружения заполнителя, можно определить бинарный тип
\lstinline{id3-frames} так:
\begin{myverb}
(define-binary-type id3-frames (tag-size)
(:reader (in)
(loop with to-read = tag-size
while (plusp to-read)
for frame = (read-frame in)
while frame
do (decf to-read (+ 6 (size frame)))
collect frame
finally (loop repeat (1- to-read) do (read-byte in))))
(:writer (out frames)
(loop with to-write = tag-size
for frame in frames
do (write-value 'id3-frame out frame)
(decf to-write (+ 6 (size frame)))
finally (loop repeat to-write do (write-byte 0 out)))))
\end{myverb}
Следующим кодом мы добавим слот frames в id3-tag.
\begin{myverb}
(define-binary-class id3-tag ()
((identifier (iso-8859-1-string :length 3))
(major-version u1)
(revision u1)
(flags u1)
(size id3-tag-size)
(frames (id3-frames :tag-size size))))
\end{myverb}
\section{Обнаружение заполнителя тега}
Теперь всё, что осталось доделать,~--- реализовать \lstinline{read-frame}. Это потребует немного
сноровки, так как код, который на самом деле читает байты из потока, лежит на несколько
уровней ниже \lstinline{read-frame}.
То, что вам бы действительно хотелось делать в \lstinline{read-frame},~--- прочитать один байт и,
если он нулевой, вернуть \lstinline{NIL}, в противном случае прочитать фрейм при помощи
\lstinline{read-value}. К несчастью, если вы прочитаете байт в \lstinline{read-frame}, то он не
сможет быть заново прочитан \lstinline{read-value}\footnote{Символьные потоки поддерживают две
функции: \lstinline{peek-char} и \lstinline{unread-char}, каждая из которых помогла бы решить
описанную задачу, но в бинарных потоках эквивалентных функций нет.}\hspace{\footnotenegspace}.
Выходит, это прекрасная возможность использовать систему условий~--- вы можете устроить
проверку на нулевые байты в коде нижнего уровня, читающем поток, и сигнализировать
условие, когда прочитан ноль; \lstinline{read-frame} сможет затем обработать условие, размотав
стек до того, как будут прочитаны следующие байты. В~дополнение к тому, что это аккуратное
решение проблемы обнаружения начала заполнителя тега, это также и пример, как можно
использовать условия для целей, отличных от обработки ошибок.
Можно начать с определения типа условия, который будет сигнализирован кодом нижнего уровня
и обработан кодом верхнего уровня. Этому условию не нужны слоты~--- вам просто нужен
отдельный класс условия, чтобы знать, что другой код не будет сигнализировать или
обрабатывать его.
\begin{myverb}
(define-condition in-padding () ())
\end{myverb}
Затем вам нужно определить бинарный тип, чей \lstinline{:reader} читает данное число байт,
сначала читая один байт и сигнализируя условие \lstinline{in-padding}, если он нулевой, и,
иначе, читая оставшиеся байты как \lstinline{iso-8859-1-string} и соединяя их с первым
прочитанным.
\begin{myverb}
(define-binary-type frame-id (length)
(:reader (in)
(let ((first-byte (read-byte in)))
(when (= first-byte 0) (signal 'in-padding))
(let ((rest (read-value 'iso-8859-1-string in :length (1- length))))
(concatenate
'string (string (code-char first-byte)) rest))))
(:writer (out id)
(write-value 'iso-8859-1-string out id :length length)))
\end{myverb}
Если переопределить \lstinline{id3-frame} так, чтобы тип его слота \lstinline{id} был
\lstinline{frame-id}, а не \lstinline{iso-8859-1-string}, условие будет сигнализировано, когда метод
\lstinline{read-value} класса \lstinline{id3-frame} прочтёт нулевой байт вместо начала фрейма.
\begin{myverb}
(define-tagged-binary-class id3-frame ()
((id (frame-id :length 3))
(size u3))
(:dispatch (find-frame-class id)))
\end{myverb}
Теперь все, что нужно сделать \lstinline{read-frame},~--- это обернуть вызов \lstinline{read-value} в
\lstinline{HANDLER-CASE}, который обработает условие \lstinline{in-padding}, просто вернув
\lstinline{NIL}.
\begin{myverb}
(defun read-frame (in)
(handler-case (read-value 'id3-frame in)
(in-padding () nil)))
\end{myverb}
Определив \lstinline{read-frame}, вы можете прочитать ID3 тег версии 2.2 целиком, представляя
фреймы экземплярами \lstinline{generic-frame}. В~разделе <<Какие фреймы на самом деле нужны?>>
вы проведёте несколько экспериментов в REPL, чтобы определить, какие классы фреймов вам
нужно реализовать. Но сначала давайте добавим поддержку для тегов ID3 версии~2.3.
\section{Поддержка нескольких версий ID3}
На данный момент \lstinline{id3-tag} определён с помощью \lstinline{define-binary-class}, но, если
вы хотите поддерживать различные версии ID3, больше смысла в использовании
\lstinline{define-tagged-binary-class}, который диспетчеризует значение
\lstinline{major-version}. Как выясняется, все версии ID3v2 имеют одну и ту же структуру вплоть
до поля \lstinline{size}. Итак, вы можете определить помеченный бинарный класс, как в следующем
коде, который определяет базовую структуру и потом передаёт управление подходящему
подклассу, специфичному для данной версии:
\begin{myverb}
(define-tagged-binary-class id3-tag ()
((identifier (iso-8859-1-string :length 3))
(major-version u1)
(revision u1)
(flags u1)
(size id3-tag-size))
(:dispatch
(ecase major-version
(2 'id3v2.2-tag)
(3 'id3v2.3-tag))))
\end{myverb}
Теги версий 2.2 и 2.3 различаются в двух местах. Во-первых, заголовок тега версии~2.3
может содержать вплоть до четырёх необязательных дополнительных полей заголовка, что
определяется значениями в поле \lstinline{flags}. Во-вторых, формат фрейма сменился между
версией~2.2 и версией~2.3, что означает, что вам придётся использовать различные классы
для представления фреймов версии~2.2 и фреймов, соответствующих версии~2.3.
Так как новый класс \lstinline{id3-tag} основан на том классе, который вы первоначально
напи\-сали для представления тега версии~2.2, неудивительно, что новый класс
\lstinline{id3v2.2-tag} тривиален, наследуя большую часть слотов от нового класса
\lstinline{id3-tag} и добавляя один недостающий слот, \lstinline{frames}. Так как теги версий~2.2
и~2.3 используют различные форматы фреймов, вам придётся изменить тип \lstinline{id3-frames}
так, чтобы он параметризовался типом фрейма для чтения. Но сейчас предположим, что вы это
сделаете, и добавим аргумент \lstinline{:frame-type} к дескриптору типов \lstinline{id3-frames} так:
\begin{myverb}
(define-binary-class id3v2.2-tag (id3-tag)
((frames (id3-frames :tag-size size :frame-type 'id3v2.2-frame))))
\end{myverb}
Класс \lstinline{id3v2.3-tag} немого более сложен из-за необязательных полей. Первые три из
четырёх необязательных полей добавляются, когда установлен шестой бит в поле
\lstinline{flags}. Они представляют собой четырёхбайтовое целое, указывающее размер
расширенного заголовка, два байта флагов и ещё одно четырёхбайтовое целое, указывающее,
сколько байт заполнителя включено в тег\pclfootnote{Если в теге есть расширенный заголовок,
вы можете использовать это значение, чтобы определить, где должны заканчиваться
данные. Однако, если расширенный заголовок не используется, вам всё равно придётся
использовать старый алгоритм, так что не стоит добавлять код, делающий это
по-другому.}. Четвёртое необязательное поле добавляется, когда установлен пятнадцатый
бит дополнительных флагов заголовка~--- четырёхбайтовая циклическая избыточностная проверка
(CRC) оставшейся части тега.
Библиотека двоичных данных не предоставляет никакой специальной поддержки для
необязательных полей в двоичном классе, но выходит так, что хватает обычных
параметризованных двоичных типов. Вы можете определить тип, параметризованный именем типа
и значением, который указывает, должно ли значение этого типа быть действительно
прочитано или записано.
\begin{myverb}
(define-binary-type optional (type if)
(:reader (in)
(when if (read-value type in)))
(:writer (out value)
(when if (write-value type out value))))
\end{myverb}
Использование \lstinline{if} как имени параметра кажется немного странным в этом коде, но оно
делает дескрипторы необязательных типов волне читаемыми.
\begin{myverb}
(define-binary-class id3v2.3-tag (id3-tag)
((extended-header-size (optional :type 'u4 :if (extended-p flags)))
(extra-flags (optional :type 'u2 :if (extended-p flags)))
(padding-size (optional :type 'u4 :if (extended-p flags)))
(crc (optional :type 'u4 :if (crc-p flags extra-flags)))
(frames (id3-frames :tag-size size :frame-type 'id3v2.3-frame))))
\end{myverb}
\noindent{}где \lstinline{extended-p} и \lstinline{crc-p}~--- вспомогательные функции, которые проверяют
соответствующий бит флагов, переданных им. Чтобы определить, выставлен отдельный бит в
целом числе или нет, можно использовать \lstinline{LOGBITP}, ещё одну жонглирующую битами
функцию. Она принимает индекс и целое и возвращает истину, если указанный бит установлен в
числе.
\begin{myverb}
(defun extended-p (flags) (logbitp 6 flags))
(defun crc-p (flags extra-flags)
(and (extended-p flags) (logbitp 15 extra-flags)))
\end{myverb}
Как и в классе тега версии 2.2, слот \lstinline{frames} определяется с типом \lstinline{id3-frames},
передавая имя типа фрейма как параметр. Вам, однако, придётся сделать незначительные
изменения в \lstinline{id3-frames} и \lstinline{read-frame} для поддержки дополнительного параметра
\lstinline{frame-type}.
\begin{myverb}
(define-binary-type id3-frames (tag-size frame-type)
(:reader (in)
(loop with to-read = tag-size
while (plusp to-read)
for frame = (read-frame frame-type in)
while frame
do (decf to-read (+ (frame-header-size frame) (size frame)))
collect frame
finally (loop repeat (1- to-read) do (read-byte in))))
(:writer (out frames)
(loop with to-write = tag-size
for frame in frames
do (write-value frame-type out frame)
(decf to-write (+ (frame-header-size frame) (size frame)))
finally (loop repeat to-write do (write-byte 0 out)))))
(defun read-frame (frame-type in)
(handler-case (read-value frame-type in)
(in-padding () nil)))
\end{myverb}
Изменения заключены в вызовах \lstinline{read-frame} и \lstinline{write-value}, где вам нужно
передать аргумент \lstinline{frame-type}, и в вычислении размера фрейма, где нужно использовать
функцию \lstinline{frame-header-size}, а не прописать значение~6, так как размер заголовка
изменился между версиями~2.2 и~2.3. Поскольку различие в результате этой функции основано на
классе фрейма, имеет смысл определить обобщённую функцию так:
\begin{myverb}
(defgeneric frame-header-size (frame))
\end{myverb}
Вы определите необходимые методы для этой обобщённой функции в следующем разделе, после
того как определите новые классы фреймов.
\section{Базовые классы для фреймов разных версий}
Раньше вы определили один базовый класс для всех фреймов, но теперь у вас два класса,
\lstinline{id3v2.2-frame} и \lstinline{id3v2.3-frame}. Класс \lstinline{id3v2.2-frame} будет, по сути,
таким же, как и первоначальный класс \lstinline{id3-frame}.
\begin{myverb}
(define-tagged-binary-class id3v2.2-frame ()
((id (frame-id :length 3))
(size u3))
(:dispatch (find-frame-class id)))
\end{myverb}
с другой стороны, \lstinline{id3v2.3-frame} требует больших изменений. Идентификатор
фрейма и поле размера были расширены в версии~2.3 с трёх до четырёх байт каждое, и были
добавлены два байта с флагами. Дополнительно фрейм, как и тег версии~2.3, может содержать
необязательные поля, зависящие от значений трёх флагов фрейма\footnote{Эти флаги не только
контролируют, включены ли необязательные поля, но и могут влиять на оставшуюся часть
тега. В~частности, если установлен седьмой бит флага, данные шифруются. На практике эти
возможности применяются редко, если вообще где-нибудь применяются, так что пока вы
можете просто проигнорировать их. Но к этой задаче вам пришлось бы обратиться, чтобы
качество вашего кода соответствовало промышленным стандартам. Одним простым половинчатым
решением было бы поменять \lstinline{find-frame-class} так, чтобы он принимал второй
аргумент, и передавать ему флаги; если фрейм зашифрован, вы могли бы создать экземпляр
обобщённого фрейма и положить в него данные фрейма.}\hspace{\footnotenegspace}. Держа
эти изменения в уме, вы можете определить базовый класс фрейма версии 2.3 вместе с
несколькими вспомогательными функциями, например так:
\begin{myverb}
(define-tagged-binary-class id3v2.3-frame ()
((id (frame-id :length 4))
(size u4)
(flags u2)
(decompressed-size (optional :type 'u4 :if (frame-compressed-p flags)))
(encryption-scheme (optional :type 'u1 :if (frame-encrypted-p flags)))
(grouping-identity (optional :type 'u1 :if (frame-grouped-p flags))))
(:dispatch (find-frame-class id)))
(defun frame-compressed-p (flags) (logbitp 7 flags))
(defun frame-encrypted-p (flags) (logbitp 6 flags))
(defun frame-grouped-p (flags) (logbitp 5 flags))
\end{myverb}
Определив эти два класса, вы можете реализовать методы обобщённой функции
\lstinline{frame-header-size}.
\begin{myverb}
(defmethod frame-header-size ((frame id3v2.2-frame)) 6)
(defmethod frame-header-size ((frame id3v2.3-frame)) 10)
\end{myverb}
Необязательные поля в фрейме версии 2.3 в этом вычислении не считаются частью заголовка,
так как они уже включены в значение размера фрейма.
\section{Конкретные классы для фреймов разных версий}
При первоначальном определении класс \lstinline{generic-frame} наследовал \lstinline{id3-frame}. Но
сейчас \lstinline{id3-frame} заменён двумя специфичными для версий базовыми классами,
\lstinline{id3v2.2-frame} и \lstinline{id3v2.3-frame}. Так что вам надо определить две новые версии
\lstinline{generic-frame}, по каждой для своего базового класса. Один из способов определить
эти классы таков:
\begin{myverb}
(define-binary-class generic-frame-v2.2 (id3v2.2-frame)
((data (raw-bytes :size size))))
(define-binary-class generic-frame-v2.3 (id3v2.3-frame)
((data (raw-bytes :size size))))
\end{myverb}
Однако немного раздражает то, что эти два класса одинаковы, за исключением их
суперклассов. Это не очень плохо в данном случае, так как здесь только одно дополнительное
поле. Но если вы выберете этот подход для других конкретных классов фреймов, таких,
которые имеют более сложную внутреннюю структуру, идентичную для двух версий ID3,
дублирование будет более раздражающим.
Другой подход, тот, который вам на самом деле следует использовать,~--- определить класс
\lstinline{generic-frame} как <<примесь>> (mixin): класс, который предполагается для
использования как суперкласс с одним из специфичных для версии базовых классов для
получения конкретного, специфичного для версии класса фрейма. В~этом способе только один
хитрый момент: \lstinline{generic-frame} не расширяет любой из базовых классов фрейма, так что
вы не сможете обращаться к слоту \lstinline{size} в определении. Вместо этого вы должны
использовать функцию \lstinline{current-binary-object}, которая обсуждалась в конце предыдущей
части, для доступа к объекту, в процессе чтения или записи которого находитесь, и передать
его в \lstinline{size}. И вам нужно учесть разницу в числе байт полного размера фрейма, которые
будут отложены, если любое из необязательных полей будет включено во фрейм. Так что вы
должны определить обобщённую функцию \lstinline{data-bytes} и методамы, которые делают
правильные действия, и для фреймов версии 2.2, и для версии 2.3.
\begin{myverb}
(define-binary-class generic-frame ()
((data (raw-bytes :size (data-bytes (current-binary-object))))))
(defgeneric data-bytes (frame))
(defmethod data-bytes ((frame id3v2.2-frame))
(size frame))
(defmethod data-bytes ((frame id3v2.3-frame))
(let ((flags (flags frame)))
(- (size frame)
(if (frame-compressed-p flags) 4 0)
(if (frame-encrypted-p flags) 1 0)
(if (frame-grouped-p flags) 1 0))))
\end{myverb}
После этого вы можете определить конкретные классы, которые расширяют один из специфичных
для версий классов, и класс \lstinline{generic-frame} для определения специфичного для версии класса
фрейма.
\begin{myverb}
(define-binary-class generic-frame-v2.2 (id3v2.2-frame generic-frame) ())
(define-binary-class generic-frame-v2.3 (id3v2.3-frame generic-frame) ())
\end{myverb}
Определив эти классы, вы можете переопределить функцию \lstinline{find-frame-class} так, чтобы
она возвращала правильный класс для версии, основываясь на длине идентификатора.
\begin{myverb}
(defun find-frame-class (id)
(ecase (length id)
(3 'generic-frame-v2.2)
(4 'generic-frame-v2.3)))
\end{myverb}
\section{Какие фреймы на самом деле нужны?}
Имея возможность читать теги и версии 2.2, и версии 2.3, используя обобщённые фреймы, вы
готовы начать реализацию классов для представления специфичных фреймов, которые вам
нужны. Однако, перед тем как нырнуть в это, вам следует набрать воздуха и выяснить, какие
фреймы вам на самом деле нужны, так как я уже упомянул ранее, что спецификация ID3
содержит множество фреймов, которые почти никогда не используются. Конечно, то, какие
фреймы вас заботят, зависит от того, какие приложения вы хотите написать. Если вы более
заинтересованы в извлечении информации из существующих ID3-тегов, тогда вам надо
реализовать только классы, представляющие информацию, до которой вам есть дело. С другой
стороны, если вы хотите написать редактор тегов ID3, вам может понадобиться поддержка всех
фреймов.
Чем угадывать, какие фреймы будут наиболее полезными, вы можете использовать код, который
вы уже написали, чтобы немного поковыряться в REPL и узнать, какие фреймы действительно
используютcя в ваших MP3. Для начала вам понадобится экземпляр \lstinline{id3-tag}, который вы
можете получить с помощью функции \lstinline{read-id3}.
\begin{myverb}
ID3V2> (read-id3 "/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3")
#<ID3V2.2-TAG @ #x727b2912>
\end{myverb}
Так как нам захочется немного поиграть с этим объектом, вам нужно сохранить его в переменную.
\begin{myverb}
ID3V2> (defparameter *id3* (read-id3 "/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3"))
*ID3*
\end{myverb}
Теперь вы можете узнать, например, сколько в нем фреймов:
\begin{myverb}
ID3V2> (length (frames *id3*))
11
\end{myverb}
Не слишком много~--- давайте посмотрим, что они из себя представляют.
\begin{myverb}
ID3V2> (frames *id3*)
(#<GENERIC-FRAME-V2.2 @ #x72dabdda> #<GENERIC-FRAME-V2.2 @ #x72dabec2>
#<GENERIC-FRAME-V2.2 @ #x72dabfa2> #<GENERIC-FRAME-V2.2 @ #x72dac08a>
#<GENERIC-FRAME-V2.2 @ #x72dac16a> #<GENERIC-FRAME-V2.2 @ #x72dac24a>
#<GENERIC-FRAME-V2.2 @ #x72dac32a> #<GENERIC-FRAME-V2.2 @ #x72dac40a>
#<GENERIC-FRAME-V2.2 @ #x72dac4f2> #<GENERIC-FRAME-V2.2 @ #x72dac632>
#<GENERIC-FRAME-V2.2 @ #x72dac7b2>)
\end{myverb}
Ладно, это не очень информативно. То, что вы действительно хотите знать,~--- это какие типы
фреймов там содержатся. Другими словами, вам нужны идентификаторы этих фреймов, которые вы
можете получить простым \lstinline{MAPCAR}, например так:
\begin{myverb}
ID3V2> (mapcar #'id (frames *id3*))
("TT2" "TP1" "TAL" "TRK" "TPA" "TYE" "TCO" "TEN" "COM" "COM" "COM")
\end{myverb}
Если вы посмотрите эти идентификаторы в спецификации ID3v2.2, вы обнаружите, что все
фреймы с идентификаторами, начинающимися с T, являются текстовой информацией и имеют
похожую структуру. А COM~--- это идентификатор для фреймов с комментариями, структура
которых схожа со структурой текстовых. В~частности, фреймы с текстовой информацией здесь,
оказывается, представляют название песни, исполнителя, альбом, дорожку, часть набора, год,
жанр и кодировавшую программу.
Конечно, это только один MP3-файл. Возможно, в других файлах используются другие
фреймы. Это достаточно просто определить. Для начала определим функцию, которая
комбинирует выражение \lstinline{MAPCAR} с вызовом \lstinline{read-id3} и заворачивает всё это в
\lstinline{DELETE-DUPLICATES}, чтобы поддерживать чистоту. Вам придётся использовать
\lstinline!#'string=! как аргумент \lstinline{:test} у \lstinline{DELETE-DUPLICATES}, чтобы указать,
что два элемента считаются одинаковыми, если это одна и та же строка.
\begin{myverb}
(defun frame-types (file)
(delete-duplicates (mapcar #'id (frames (read-id3 file))) :test #'string=))
\end{myverb}
Это должно давать тот же результат для такого же имени файла, за исключением того, что
каждый идентификатор встречается один раз.
\begin{myverb}
ID3V2> (frame-types "/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3")
("TT2" "TP1" "TAL" "TRK" "TPA" "TYE" "TCO" "TEN" "COM")
\end{myverb}
Теперь вы можете использовать функцию \lstinline{walk-directory} из главы~\ref{ch:15} для
нахождения всех MP3-файлов в директории и комбинирования результатов вызова frame-types на
каждом файле. Вспомните, что \lstinline{NUNION}~--- это деструктивная версия функции
\lstinline{UNION}, но, так как \lstinline{frame-types} делает новый список для каждого файла, она
безопасна.
\begin{myverb}
(defun frame-types-in-dir (dir)
(let ((ids ()))
(flet ((collect (file)
(setf ids (nunion ids (frame-types file) :test #'string=))))
(walk-directory dir #'collect :test #'mp3-p))
ids))
\end{myverb}
Теперь передайте ей имя каталога, и она выдаст вам набор идентификаторов, используемых во
всех MP3-файлах этого каталога и его подкаталогов. Это может занять несколько секунд в
зависимости от количества ваших MP3-файлов, но вы, вероятно, получите что-то вроде
следующего:
\begin{myverb}
ID3V2> (frame-types-in-dir "/usr2/mp3/")
("TCON" "COMM" "TRCK" "TIT2" "TPE1" "TALB" "TCP" "TT2" "TP1" "TCM"
"TAL" "TRK" "TPA" "TYE" "TCO" "TEN" "COM")
\end{myverb}
Четырёхбуквенные идентификаторы версии 2.3~--- эквиваленты идентификаторов версии 2.2,
которые я обсуждал ранее. Так как информация, хранимая в этих фреймах, в точности та,
которая понадобится вам в главе~\ref{ch:27}, имеет смысл реализовать классы только для тех
фреймов, которые на самом деле используются, а именно фреймов текстовой информации и
комментариев, что вы и сделаете в следующих двух разделах. Если позже вы решите, что
хотите поддерживать другие типы фреймов, то это больше вопрос преобразования спецификаций
ID3 в подходящие определения бинарных классов.
\section{Фреймы текстовой информации}
Все фреймы с текстовой информацией состоят из двух полей: одного байта, указывающего,
какая кодировка строк используется во фрейме, и строки, закодированной в оставшихся байтах
строки. Если кодирующий байт равен нулю, строка закодирована в ISO 8859-1; если он равен
единице, строка в кодировке UCS-2.
Вы уже определили бинарные типы для представления двух типов строк~--- двух типов
кодировок, каждой с двумя различными методами определения границ строки. Однако
\lstinline{define-binary-class} не предоставляет прямую возможность определить тип значения
для чтения, основываясь на других значениях в объекте. Вместо этого вы можете определить
бинарный тип, которому вы передадите значение байта кодировки, и после этого он будет
читать или писать подходящий вид строки.
Когда вы будете определять этот тип, вы можете определить его так, чтобы он принимал два
параметра, \lstinline{:length} и \lstinline{:terminator}, и выбирал правильный тип строки,
основанный на том, какой аргумент подан. Для реализации этого нового типа вы должны для
начала определить некоторые вспомогательные функции. Первая из двух возвращает имя
подходящего строкового типа, основываясь на байте кодировки.
\begin{myverb}
(defun non-terminated-type (encoding)
(ecase encoding
(0 'iso-8859-1-string)
(1 'ucs-2-string)))
(defun terminated-type (encoding)
(ecase encoding
(0 'iso-8859-1-terminated-string)
(1 'ucs-2-terminated-string)))
\end{myverb}
Затем \lstinline{string-args} использует этот байт кодировки, длину и \lstinline{terminator} для
определения нескольких аргументов для передачи их \lstinline{read-value} и \lstinline{write-value} с помощью
\lstinline{:reader} и \lstinline{:writer} в \lstinline{id3-encoded-string}. Один из аргументов \lstinline{string-args}~--- либо
\lstinline{length}, либо \lstinline{terminator}~--- всегда дожен быть \lstinline{NIL}.
\begin{myverb}
(defun string-args (encoding length terminator)
(cond
(length
(values (non-terminated-type encoding) :length length))
(terminator
(values (terminated-type encoding) :terminator terminator))))
\end{myverb}
С этими помощниками определить \lstinline{id3-encoded-string} просто. Одна деталь, которую
нужно отметить,~--- это то, что ключ~--- или \lstinline{:length}, или \lstinline{:terminator},~---
используемый в вызове \lstinline{read-value} и \lstinline{write-value}, является просто ещё одной
частью данных, возвращённых \lstinline{string-arts}. Даже если ключевые символы в списке
аргументов практически всегда вписаны в текст программы, они не обязаны быть вписаны туда
всегда.
\begin{myverb}
(define-binary-type id3-encoded-string (encoding length terminator)
(:reader (in)
(multiple-value-bind (type keyword arg)
(string-args encoding length terminator)
(read-value type in keyword arg)))
(:writer (out string)
(multiple-value-bind (type keyword arg)
(string-args encoding length terminator)
(write-value type out string keyword arg))))
\end{myverb}
Теперь можно определить примесный класс \lstinline{text-info} точно так же, как был определён
\lstinline{generic-frame} ранее.
\begin{myverb}
(define-binary-class text-info-frame ()
((encoding u1)
(information (id3-encoded-string :encoding encoding :length (bytes-left 1)))))
\end{myverb}
Как и при определении \lstinline{generic-frame}, вам нужно получить доступ к размеру фрейма, в
данном случае для того, чтобы вычислить аргумент \lstinline{:length} для передачи
\lstinline{id3-encoded-string}. Так как вам понадобится похожее вычисление в следующем определяемом
вами классе, вы можете пойти дальше и определить вспомогательную функцию \lstinline{bytes-left},
которая использует \lstinline{current-binary-object} для получения размера фрейма.
\begin{myverb}
(defun bytes-left (bytes-read)
(- (size (current-binary-object)) bytes-read))
\end{myverb}
Теперь вы можете определить два индивидуальных для каждой версии конкретных класса с
примесью дублируемого кода, так же, как вы сделали это с примесью \lstinline{generic-frame}.
\begin{myverb}
(define-binary-class text-info-frame-v2.2 (id3v2.2-frame text-info-frame) ())
(define-binary-class text-info-frame-v2.3 (id3v2.3-frame text-info-frame) ())
\end{myverb}
Чтобы запрячь эти классы за работу, вам нужно подправить \lstinline{find-frame-class}, чтобы он
возвращал правильное имя класса, когда \lstinline{ID} указывает, что фрейм является текстовым, а
именно всегда, когда \lstinline{ID} начинается с T и не является TXX или TXXX.
\begin{myverb}
(defun find-frame-class (name)
(cond
((and (char= (char name 0) #\bslash{}T)
(not (member name '("TXX" "TXXX") :test #'string=)))
(ecase (length name)
(3 'text-info-frame-v2.2)
(4 'text-info-frame-v2.3)))
(t
(ecase (length name)
(3 'generic-frame-v2.2)
(4 'generic-frame-v2.3)))))
\end{myverb}
\section{Фреймы комментариев}
Другим часто используемым фреймом является фрейм с комментариями, который похож на фрейм
текстовой информации с несколькими дополнительными полями. Как и фрейм текстовой
информации, он начинается с единственного байта, означающего кодировку строки,
используемую во фрейме. За этим байтом следует трёхбуквенная строка ISO 8859-1 (вне
зависимости от значения байта кодировки), которая указывает, каков язык комментария,
используя код ISO-639-2, например <<eng>> для английского или <<jpn>> для японского. За ним
следуют две строки, закодированные, как указано в первом байте. Первая завершаемая нулём
строка содержит описание комментария в кодировке, указанной первым байтом. Вторая строка,
занимающая остаток фрейма,~--- сам комментарий.
\begin{myverb}
(define-binary-class comment-frame ()
((encoding u1)
(language (iso-8859-1-string :length 3))
(description (id3-encoded-string :encoding encoding :terminator +null+))
(text (id3-encoded-string
:encoding encoding
:length (bytes-left
(+ 1 ; encoding
3 ; language
(encoded-string-length description encoding t)))))))
\end{myverb}
Как и в определении примеси text-inf, вы можете использовать bytes-left для вычисления
размера последней строки. Однако, так так поле описания~--- строка переменной длины, число
байт, прочитанных до начала текста, не является постоянным. Чтобы запутать всё ещё больше,
число байт, используемых для кодирования описания, зависит от кодировки. Итак, вам нужно
определить вспомогательную функцию, которая возвращает число байт, использованных для
кодирования строки, принимающую строку, код кодировки и логический индикатор того,
завершается строка дополнительным знаком или нет.
\begin{myverb}
(defun encoded-string-length (string encoding terminated)
(let ((characters (+ (length string) (if terminated 1 0))))
(* characters (ecase encoding (0 1) (1 2)))))
\end{myverb}
И, как и раньше, вы можете определить индивидуальные для каждой версии классы фреймов и
включить их в \lstinline{find-frame-class}.
\begin{myverb}
(define-binary-class comment-frame-v2.2 (id3v2.2-frame comment-frame) ())
(define-binary-class comment-frame-v2.3 (id3v2.3-frame comment-frame) ())
(defun find-frame-class (name)
(cond
((and (char= (char name 0) #\bslash{}T)
(not (member name '("TXX" "TXXX") :test #'string=)))
(ecase (length name)
(3 'text-info-frame-v2.2)
(4 'text-info-frame-v2.3)))
((string= name "COM") 'comment-frame-v2.2)
((string= name "COMM") 'comment-frame-v2.3)
(t
(ecase (length name)
(3 'generic-frame-v2.2)
(4 'generic-frame-v2.3)))))
\end{myverb}
\section{Извлечение информации из тега ID3}
Теперь у вас есть базовая возможность для чтения и записи тегов ID3 и есть много путей,
по которым можно развивать ваш код. Если вы хотите разработать полный редактор ID3-тегов,
вам нужно реализовать индивидуальные классы для всех типов фреймов. Вам также необходимо
будет определить методы для манипулирования объектами тегов и фреймов согласованным
образом (например, если вы измените значение строки в \lstinline{text-info-frame}, вам, вероятнее всего,
придётся поменять и размер); при нынешнем состоянии кода нельзя быть уверенным в том, что
это произойдёт\footnote{Гарантия таких согласований между полями~--- отличное применение
для методов :after обобщённой функции доступа. Например, вы могли бы определить этот
метод :after, чтобы держать размер синхронизированным со строкой информации:
\begin{myverb}
(defmethod (setf information) :after (value (frame text-info-frame))
(declare (ignore value))
(with-slots (encoding size information) frame
(setf size (encoded-string-length information encoding nil))))
\end{myverb}
}\hspace{\footnotenegspace}.
Или если вам нужна только определённая часть информации о MP3-файле из его ID3-тега~---
например, как вам, когда вы будете разрабатывать потоковый сервер MP3 в
главах~\ref{ch:27}, \ref{ch:28} и~\ref{ch:29},~--- то нужно написать функции, которые
находят подходящие фреймы и извлекают из них желаемую информацию.
Наконец, чтобы сделать ваш код готовым к внедрению в реальные приложения, вам придётся
покорпеть над спецификациями ID3 и иметь дело с деталями, которые я опустил ради экономии
места. В~частности, некоторые флаги как в теге, так и во фрейме могут влиять на способ
чтения содержимого тега; если вы не напишете некоторый код, который выполняет правильные
действия, когда установлены эти флаги, могут существовать ID3-теги, которые ваш код не
будет способен прочитать правильно. Но код из этой главы должен быть способен разобрать
почти все MP3, которые вы можете встретить в действительности.
На данный момент вы можете закончить, написав несколько функций для извлечения отдельных
частей информации из тега ID3. Эти функции вам понадобятся в главе~\ref{ch:26} и,
возможно, в другом коде, который использует эту библиотеку. Они входят в эту библиотеку
потому, что зависят от деталей формата ID3, о которых пользователям этой библиотеки не
следует волноваться.
Чтобы получить, скажем, имя песни для MP3, из которого извлечён тег ID3, вам надо найти
ID3-фрейм со специальным идентификатором и потом извлечь поле информации. А некоторые
части информации, такие как жанр, могут потребовать дальнейшего декодирования. К счастью,
все фреймы, содержащие информацию, до которой вам есть дело,~--- это фреймы текстовой
информации, так что извлечение конкретного кусочка информации сводится к использованию
правильного идентификатора для поиска подходящего фрейма. Конечно, авторы ID3 решили
сменить все идентификаторы при переходе от ID3v2.2 к ID3v2.3, так что вам придётся принять
это в расчёт.
Ничего слишком сложного~--- вам просто надо разыскать правильный путь для получения
различных частей информации. Это прекрасный кусок кода для интерактивной разработки, очень
похожей на тот способ, которым вы выяснили, какие классы фреймов вам нужно
реализовать. Для начала вам нужен объект класса \lstinline{id3-tag} для экспериментов. Предполагая,
что где-то рядом с вами лежит какой-нибудь MP3-файл, вы можете воспользоваться \lstinline{read-id3}
вот так:
\begin{myverb}
ID3V2> (defparameter *id3* (read-id3 "Kitka/Wintersongs/02 Byla Cesta.mp3"))
*ID3*
ID3V2> *id3*
#<ID3V2.2-TAG @ #x73d04c1a>
\end{myverb}
Замените \texttt{Kitka/Wintersongs/02 Byla Cesta.mp3} на имя вашего MP3-файла. Как только
у вас появится объект id3-tag, вы сможете начать копаться в нём. Например, вы можете
проверить список объектов фреймов с функцией frames.
\begin{myverb}
ID3V2> (frames *id3*)
(#<TEXT-INFO-FRAME-V2.2 @ #x73d04cca>
#<TEXT-INFO-FRAME-V2.2 @ #x73d04dba>
#<TEXT-INFO-FRAME-V2.2 @ #x73d04ea2>
#<TEXT-INFO-FRAME-V2.2 @ #x73d04f9a>
#<TEXT-INFO-FRAME-V2.2 @ #x73d05082>
#<TEXT-INFO-FRAME-V2.2 @ #x73d0516a>
#<TEXT-INFO-FRAME-V2.2 @ #x73d05252>
#<TEXT-INFO-FRAME-V2.2 @ #x73d0533a>
#<COMMENT-FRAME-V2.2 @ #x73d0543a>
#<COMMENT-FRAME-V2.2 @ #x73d05612>
#<COMMENT-FRAME-V2.2 @ #x73d0586a>)
\end{myverb}
Теперь предположим, что вы хотите извлечь название песни. Возможно, оно в одном из этих
фреймов, но для того, чтобы найти его, вам нужно найти фрейм с идентификатором
<<TT2>>. Итак, вы можете достаточно легко проверить, содержит ли тег такой фрейм, вытащив
все идентификаторы наружу, например так:
\begin{myverb}
ID3V2> (mapcar #'id (frames *id3*))
("TT2" "TP1" "TAL" "TRK" "TPA" "TYE" "TCO" "TEN" "COM" "COM" "COM")
\end{myverb}
Ага, вот он, первый фрейм. Однако нет гарантии, что он всегда будет первым, так что, возможно,
вам следует искать его не по позиции, а по идентификатору. Это тоже просто, используйте
функцию FIND.
\begin{myverb}
ID3V2> (find "TT2" (frames *id3*) :test #'string= :key #'id)
#<TEXT-INFO-FRAME-V2.2 @ #x73d04cca>
\end{myverb}
Теперь, чтобы получить саму информацию из фрейма, сделайте следующее:
\begin{myverb}
ID3V2> (information (find "TT2" (frames *id3*) :test #'string= :key #'id))
"Byla Cesta^@"
\end{myverb}
Опаньки. Этот \lstinline!^@!~--- то, как емакс печатает нулевой символ. В~ходе манёвра,
напоминающего клудж, который превратил спецификацию ID3v1 в ID3v1.1, информационная ячейка
фрейма текстовой информации, которая официально не является обрываемой нулём строкой,
может содержать нуль, и предпологается, что считыватели ID3 будут игнорировать любой знак
после нуля. Так что вам нужна функция, которая принимает строку и возвращает её
содержимое вплоть до первого нулевого знака, если он есть. Используя константу +null+ из
библиотеки бинарных данных, сделать это достаточно просто.
\begin{myverb}
(defun upto-null (string)
(subseq string 0 (position +null+ string)))
\end{myverb}
Теперь можно просто получить имя.
\begin{myverb}
ID3V2> (upto-null (information (find "TT2" (frames *id3*) :test #'string= :key #'id)))
"Byla Cesta"
\end{myverb}
Вы могли бы просто обернуть этот код в функцию с именем song, принимающую экземпляр
id3-tag как аргумент, и дело с концом. Однако единственная разница между этим кодом и
кодом, который бы вы использовали для извлечения других кусочков информации, которые вам
нужны (таких как название альбома, исполнитель и жанр), в идентификаторе. Так что лучше
немного разделить этот код. Для начала вы можете написать функцию, которая просто находит
фрейм для данных экземпляра id3-tag и идентификатора, вроде этой:
\begin{myverb}
(defun find-frame (id3 id)
(find id (frames id3) :test #'string= :key #'id))
\end{myverb}
\begin{myverb}
ID3V2> (find-frame *id3* "TT2")
#<TEXT-INFO-FRAME-V2.2 @ #x73d04cca>
\end{myverb}
Тогда другой кусочек кода, часть, извлекающая информацию из text-info-frame, может отойти
в другую функцию.
\begin{myverb}
(defun get-text-info (id3 id)
(let ((frame (find-frame id3 id)))
(when frame (upto-null (information frame)))))
\end{myverb}
\begin{myverb}
ID3V2> (get-text-info *id3* "TT2")
"Byla Cesta"
\end{myverb}
Теперь определение song~--- просто дело передачи правильного идентификатора.
\begin{myverb}
(defun song (id3) (get-text-info id3 "TT2"))
\end{myverb}
\begin{myverb}
ID3V2> (song *id3*)
"Byla Cesta"
\end{myverb}
Однако это определение song работает только с тегами версии 2.2, так как идентификатор
поменялся с <<TT2>> в версии 2.2 на <<TIT2>> в версии 2.3. И все остальные теги поменялись
тоже. Так как пользователь этой библиотеки не должен обязательно знать о различных
версиях формата ID3 для того, чтобы сделать такую простую вещь, как получение названия
песни, вам, наверное, лучше иметь дело с этими деталями за него. Простой способ состоит в
таком изменении find-frame, что она не просто принимает один идентификатор, а список
идентификаторов вроде этого:
\begin{myverb}
(defun find-frame (id3 ids)
(find-if #'(lambda (x) (find (id x) ids :test #'string=)) (frames id3)))
\end{myverb}
Теперь слегка поменяем get-text-info, чтобы она могла принимать один идентификатор и
более, используя параметр \lstinline!&rest!.
\begin{myverb}
(defun get-text-info (id3 &rest ids)
(let ((frame (find-frame id3 ids)))
(when frame (upto-null (information frame)))))
\end{myverb}
Теперь изменение, позволяющее song поддерживать теги и версии 2.2, и версии 2.3,~--- просто
вопрос добавления идентификатора из версии 2.3.
\begin{myverb}
(defun song (id3) (get-text-info id3 "TT2" "TIT2"))
\end{myverb}
После этого вам просто нужно найти подходящие идентификаторы версий 2.2 и 2.3 для каждого
поля, к которому вы хотите предоставить функцию доступа. Вот те функции, которые вам
понадобятся в главе~\ref{ch:27}:
\begin{myverb}
(defun album (id3) (get-text-info id3 "TAL" "TALB"))
(defun artist (id3) (get-text-info id3 "TP1" "TPE1"))
(defun track (id3) (get-text-info id3 "TRK" "TRCK"))
(defun year (id3) (get-text-info id3 "TYE" "TYER" "TDRC"))
(defun genre (id3) (get-text-info id3 "TCO" "TCON"))
\end{myverb}
Последняя трудность в том, что жанр хранится в фреймах TCO и TCON нечи\-тае\-мым человеком
способом. Вспомните, что в ID3v1 жанры хранились как один байт, который кодировал
определённый жанр из фиксированного списка. К несчастью, эти коды продолжают жить и в
ID3v2: если текст жанрового фрейма~--- число в круглых скобках, это число обязано быть
интерпретировано как код жанра из ID3v1. Но опять пользователи этой библиотеки, вероятно,
не будут заботиться об этой древней истории. Так что вам следует предоставить им функцию,
которая автоматически перекодирует жанр. Следующая функция использует функцию genre,
определённую лишь для того, чтобы извлекать сам жанр как текст, затем проверять,
начинается ли он с левой круглой скобки, и если это так, то раскодировать код жанра версии
1 при помощи функции, которую мы определим через пару мнгновений.
\begin{myverb}
(defun translated-genre (id3)
(let ((genre (genre id3)))
(if (and genre (char= #\bslash{}( (char genre 0)))
(translate-v1-genre genre)
genre)))
\end{myverb}
Так как код жанра версии 1, в сущности, просто индекс в массиве стандартных имён, самый
простой способ реализовать translate-v1-genre~--- извлечь число из строки жанра и
воспользоваться им как индексом в настоящем массиве.
\begin{myverb}
(defun translate-v1-genre (genre)
(aref *id3-v1-genres* (parse-integer genre :start 1 :junk-allowed t)))
\end{myverb}
Теперь всё, что вам нужно,~--- это определить массив имён. Следующий массив имён включает
80 официальных жанров версии 1 плюс жанры, созданные авторами Winamp:
\begin{myverb}
(defparameter *id3-v1-genres*
#(
;; These are the official ID3v1 genres.
"Blues" "Classic Rock" "Country" "Dance" "Disco" "Funk" "Grunge"
"Hip-Hop" "Jazz" "Metal" "New Age" "Oldies" "Other" "Pop" "R&B" "Rap"
"Reggae" "Rock" "Techno" "Industrial" "Alternative" "Ska"
"Death Metal" "Pranks" "Soundtrack" "Euro-Techno" "Ambient"
"Trip-Hop" "Vocal" "Jazz+Funk" "Fusion" "Trance" "Classical"
"Instrumental" "Acid" "House" "Game" "Sound Clip" "Gospel" "Noise"
"AlternRock" "Bass" "Soul" "Punk" "Space" "Meditative"
"Instrumental Pop" "Instrumental Rock" "Ethnic" "Gothic" "Darkwave"
"Techno-Industrial" "Electronic" "Pop-Folk" "Eurodance" "Dream"
"Southern Rock" "Comedy" "Cult" "Gangsta" "Top 40" "Christian Rap"
"Pop/Funk" "Jungle" "Native American" "Cabaret" "New Wave"
"Psychadelic" "Rave" "Showtunes" "Trailer" "Lo-Fi" "Tribal"
"Acid Punk" "Acid Jazz" "Polka" "Retro" "Musical" "Rock & Roll"
"Hard Rock"
;; These were made up by the authors of Winamp but backported into
;; the ID3 spec.
"Folk" "Folk-Rock" "National Folk" "Swing" "Fast Fusion"
"Bebob" "Latin" "Revival" "Celtic" "Bluegrass" "Avantgarde"
"Gothic Rock" "Progressive Rock" "Psychedelic Rock" "Symphonic Rock"
"Slow Rock" "Big Band" "Chorus" "Easy Listening" "Acoustic" "Humour"
"Speech" "Chanson" "Opera" "Chamber Music" "Sonata" "Symphony"
"Booty Bass" "Primus" "Porn Groove" "Satire" "Slow Jam" "Club"
"Tango" "Samba" "Folklore" "Ballad" "Power Ballad" "Rhythmic Soul"
"Freestyle" "Duet" "Punk Rock" "Drum Solo" "A capella" "Euro-House"
"Dance Hall"
;; These were also invented by the Winamp folks but ignored by the
;; ID3 authors.
"Goa" "Drum & Bass" "Club-House" "Hardcore" "Terror" "Indie"
"BritPop" "Negerpunk" "Polsk Punk" "Beat" "Christian Gangsta Rap"
"Heavy Metal" "Black Metal" "Crossover" "Contemporary Christian"
"Christian Rock" "Merengue" "Salsa" "Thrash Metal" "Anime" "Jpop"
"Synthpop"))
\end{myverb}
Ещё раз, возможно, вы чувствуете, что написали в этой главе тонну кода. Но если вы положите
его в один файл или если скачаете его версию с сайта этой книги, вы увидите, что строк там
не настолько много~--- большая часть проблем с написанием этой библиотеки происходит от
необходимости понять сложности самого формата ID3. В~любом случае, теперь у вас есть
существенная часть того, что вы превратите в потоковый MP3-сервер в главах~\ref{ch:27},
\ref{ch:28} и~\ref{ch:29}. Другая крупная часть инфраструктуры, которая вам
понадобится,~--- способ написания веб-программ со стороны сервера~--- является темой следующей
главы.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End:
Jump to Line
Something went wrong with that request. Please try again.