1098 lines
75 KiB
HTML
1098 lines
75 KiB
HTML
|
<HTML><HEAD><TITLE>Practical: An ID3 Parser</TITLE><LINK REL="stylesheet" TYPE="text/css" HREF="style.css"/></HEAD><BODY><DIV CLASS="copyright">Copyright © 2003-2005, Peter Seibel</DIV><H1>25. Practical: An ID3 Parser</H1><P>With a library for parsing binary data, you're ready to write some
|
|||
|
code for reading and writing an actual binary format, that of ID3
|
|||
|
tags. ID3 tags are used to embed metadata in MP3 audio files. Dealing
|
|||
|
with ID3 tags will be a good test of the binary data library because
|
|||
|
the ID3 format is a true real-world format--a mix of engineering
|
|||
|
trade-offs and idiosyncratic design choices that does, whatever else
|
|||
|
might be said about it, get the job done. In case you missed the
|
|||
|
file-sharing revolution, here's a quick overview of what ID3 tags are
|
|||
|
and how they relate to MP3 files.</P><P>MP3, also known as MPEG Audio Layer 3, is a format for storing
|
|||
|
compressed audio data, designed by researchers at Fraunhofer IIS and
|
|||
|
standardized by the Moving Picture Experts Group, a joint committee
|
|||
|
of the International Organization for Standardization (ISO) and the
|
|||
|
International Electrotechnical Commission (IEC). However, the MP3
|
|||
|
format, by itself, defines only how to store audio data. That's fine
|
|||
|
as long as all your MP3 files are managed by a single application
|
|||
|
that can store metadata externally and keep track of which metadata
|
|||
|
goes with which files. However, when people started passing around
|
|||
|
individual MP3 files on the Internet, via file-sharing systems such
|
|||
|
as Napster, they soon discovered they needed a way to embed metadata
|
|||
|
in the MP3 files themselves.</P><P>Because the MP3 standard was already codified and a fair bit of
|
|||
|
software and hardware had already been written that knew how to
|
|||
|
decode the existing MP3 format, any scheme for embedding information
|
|||
|
in an MP3 file would have to be invisible to MP3 decoders. Enter ID3.</P><P>The original ID3 format, invented by programmer Eric Kemp, consisted
|
|||
|
of 128 bytes stuck on the end of an MP3 file where it'd be ignored by
|
|||
|
most MP3 software. It consisted of four 30-character fields, one each
|
|||
|
for the song title, the album title, the artist name, and a comment;
|
|||
|
a four-byte year field; and a one-byte genre code. Kemp provided
|
|||
|
standard meanings for the first 80 genre codes. Nullsoft, the makers
|
|||
|
of Winamp, a popular MP3 player, later supplemented this list with
|
|||
|
another 60 or so genres.</P><P>This format was easy to parse but obviously quite limited. It had no
|
|||
|
way to encode names longer than 30 characters; it was limited to 256
|
|||
|
genres, and the meaning of the genre codes had to be agreed upon by
|
|||
|
all users of ID3-aware software. There wasn't even a way to encode
|
|||
|
the CD track number of a particular MP3 file until another
|
|||
|
programmer, Michael Mutschler, proposed embedding the track number in
|
|||
|
the comment field, separated from the rest of the comment by a null
|
|||
|
byte, so existing ID3 software, which tended to read up to the first
|
|||
|
null in each of the text fields, would ignore it. Kemp's version is
|
|||
|
now called ID3v1, and Mutschler's is ID3v1.1.</P><P>Limited as they were, the version 1 proposals were at least a partial
|
|||
|
solution to the metadata problem, so they were adopted by many MP3
|
|||
|
ripping programs (which had to put the ID3 tag into the MP3 files)
|
|||
|
and MP3 players (which would extract the information in the ID3 tag
|
|||
|
to display to the user).<SUP>1</SUP></P><P>By 1998, however, the limitations were really becoming annoying, and
|
|||
|
a new group, led by Martin Nilsson, started work on a completely new
|
|||
|
tagging scheme, which came to be called ID3v2. The ID3v2 format is
|
|||
|
extremely flexible, allowing for many kinds of information to be
|
|||
|
included, with almost no length limitations. It also takes advantage
|
|||
|
of certain details of the MP3 format to allow ID3v2 tags to be placed
|
|||
|
at the beginning of an MP3 file.</P><P>ID3v2 tags are, however, more of a challenge to parse than version 1
|
|||
|
tags. In this chapter, you'll use the binary data parsing library
|
|||
|
from the previous chapter to develop code that can read and write
|
|||
|
ID3v2 tags. Or at least you'll make a reasonable start--where ID3v1
|
|||
|
was too simple, ID3v2 is baroque to the point of being completely
|
|||
|
overengineered. Implementing every nook and cranny of the
|
|||
|
specification, especially if you want to support all three versions
|
|||
|
that have been specified, would be a fair bit of work. However, you
|
|||
|
can ignore many of the features in those specifications since they're
|
|||
|
rarely used "in the wild." For starters, you can ignore, for now, a
|
|||
|
whole version, 2.4, since it has not been widely adopted and mostly
|
|||
|
just adds more needless flexibility compared to version 2.3. I'll
|
|||
|
focus on versions 2.2 and 2.3 because they're both widely used and
|
|||
|
are different enough from each other to keep things interesting.</P><A NAME="structure-of-an-id3v2-tag"><H2>Structure of an ID3v2 Tag</H2></A><P>Before you can start cutting code, you'll need to be familiar with
|
|||
|
the overall structure of an ID3v2 tag. A tag starts with a header
|
|||
|
containing information about the tag as a whole. The first three
|
|||
|
bytes of the header encode the string "ID3" in ISO-8859-1 characters.
|
|||
|
In other words, they're the bytes 73, 68, and 51. Then comes two
|
|||
|
bytes that encode the major version and revision of the ID3
|
|||
|
specification to which the tag purports to conform. They're followed
|
|||
|
by a single byte whose individual bits are treated as flags. The
|
|||
|
meanings of the individual flags depend on the version of the spec.
|
|||
|
Some of the flags can affect the way the rest of the tag is parsed.
|
|||
|
The "major version" is actually used to record the minor version of
|
|||
|
the spec, while the "revision" is the subminor version of the spec.
|
|||
|
Thus, the "major version" field for a tag conforming to the 2.3.0
|
|||
|
spec is 3. The revision field is always zero since each new ID3v2
|
|||
|
spec has bumped the minor version, leaving the subminor version at
|
|||
|
zero. The value stored in the major version field of the tag has, as
|
|||
|
you'll see, a dramatic effect on how you'll parse the rest of the
|
|||
|
tag.</P><P>The last field in the tag header is an integer, encoded in four bytes
|
|||
|
but using only seven bits from each byte, that gives the total size
|
|||
|
of the tag, not counting the header. In version 2.3 tags, the header
|
|||
|
may be followed by several <I>extended header</I> fields; otherwise, the
|
|||
|
remainder of the tag data is divided into <I>frames</I>. Different types
|
|||
|
of frames store different kinds of information, from simple textual
|
|||
|
information, such as the song name, to embedded images. Each frame
|
|||
|
starts with a header containing a string identifier and a size. In
|
|||
|
version 2.3, the frame header also contains two bytes worth of flags
|
|||
|
and, depending on the value of one the flags, an optional one-byte
|
|||
|
code indicating how the rest of the frame is encrypted.</P><P>Frames are a perfect example of a tagged data structure--to know how
|
|||
|
to parse the body of a frame, you need to read the header and use the
|
|||
|
identifier to determine what kind of frame you're reading.</P><P>The ID3 tag header contains no direct indication of how many frames
|
|||
|
are in a tag--the tag header tells you how big the tag is, but since
|
|||
|
many frames are variable length, the only way to find out how many
|
|||
|
frames the tag contains is to read the frame data. Also, the size
|
|||
|
given in the tag header may be larger than the actual number of bytes
|
|||
|
of frame data; the frames may be followed with enough null bytes to
|
|||
|
pad the tag out to the specified size. This makes it possible for tag
|
|||
|
editors to modify a tag without having to rewrite the whole MP3
|
|||
|
file.<SUP>2</SUP></P><P>So, the main issues you have to deal with are reading the ID3 header;
|
|||
|
determining whether you're reading a version 2.2 or 2.3 tag; and
|
|||
|
reading the frame data, stopping either when you've read the complete
|
|||
|
tag or when you've hit the padding bytes.</P><A NAME="defining-a-package"><H2>Defining a Package</H2></A><P>Like the other libraries you've developed so far, the code you'll
|
|||
|
write in this chapter is worth putting in its own package. You'll
|
|||
|
need to refer to functions from both the binary data and pathname
|
|||
|
libraries developed in Chapters 24 and 15 and will also want to
|
|||
|
export the names of the functions that make up the public API to this
|
|||
|
package. The following package definition does all that:</P><PRE>(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))</PRE><P>As usual, you can, and probably should, change the
|
|||
|
<CODE>com.gigamonkeys</CODE> part of the package name to your own domain.</P><A NAME="integer-types"><H2>Integer Types</H2></A><P>You can start by defining binary types for reading and writing
|
|||
|
several of the primitive types used by the ID3 format, various sizes
|
|||
|
of unsigned integers, and four kinds of strings.</P><P>ID3 uses unsigned integers encoded in one, two, three, and four
|
|||
|
bytes. If you first write a general <CODE>unsigned-integer</CODE> binary
|
|||
|
type that takes the number of bytes to read as an argument, you can
|
|||
|
then use the short form of <CODE>define-binary-type</CODE> to define the
|
|||
|
specific types. The general <CODE>unsigned-integer</CODE> type looks like
|
|||
|
this:</P><PRE>(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))))</PRE><P>Now you can use the short form of <CODE>define-binary-type</CODE> to define
|
|||
|
one type for each size of integer used in the ID3 format like this:</P><PRE>(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))</PRE><P>Another type you'll need to be able to read and write is the 28-bit
|
|||
|
value used in the header. This size is encoded using 28 bits rather
|
|||
|
than a multiple of 8, such as 32 bits, because an ID3 tag can't
|
|||
|
contain the byte <CODE>#xff</CODE> followed by a byte with the top 3 bits
|
|||
|
on because that pattern has a special meaning to MP3 decoders. None
|
|||
|
of the other fields in the ID3 header could possibly contain such a
|
|||
|
byte sequence, but if you encoded the tag size as a regular
|
|||
|
<CODE>unsigned-integer</CODE>, it might. To avoid that possibility, the
|
|||
|
size is encoded using only the bottom seven bits of each byte, with
|
|||
|
the top bit always zero.<SUP>3</SUP></P><P>Thus, it can be read and written a lot like an
|
|||
|
<CODE>unsigned-integer</CODE> except the size of the byte specifier you
|
|||
|
pass to <CODE><B>LDB</B></CODE> should be seven rather than eight. This similarity
|
|||
|
suggests that if you add a parameter, <CODE>bits-per-byte</CODE>, to the
|
|||
|
existing <CODE>unsigned-integer</CODE> binary type, you could then define a
|
|||
|
new type, <CODE>id3-tag-size</CODE>, using a short-form
|
|||
|
<CODE>define-binary-type</CODE>. The new version of <CODE>unsigned-integer</CODE>
|
|||
|
is just like the old version except with <CODE>bits-per-byte</CODE> used
|
|||
|
everywhere the old version hardwired the number eight. It looks like
|
|||
|
this:</P><PRE>(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))))</PRE><P>The definition of <CODE>id3-tag-size</CODE> is then trivial.</P><PRE>(define-binary-type id3-tag-size () (unsigned-integer :bytes 4 :bits-per-byte 7))</PRE><P>You'll also have to change the definitions of <CODE>u1</CODE> through
|
|||
|
<CODE>u4</CODE> to specify eight bits per byte like this:</P><PRE>(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))</PRE><A NAME="string-types"><H2>String Types</H2></A><P>The other kinds of primitive types that are ubiquitous in the ID3
|
|||
|
format are strings. In the previous chapter I discussed some of the
|
|||
|
issues you have to consider when dealing with strings in binary files,
|
|||
|
such as the difference between character codes and character
|
|||
|
encodings.</P><P>ID3 uses two different character codes, ISO 8859-1 and Unicode. ISO
|
|||
|
8859-1, also known as Latin-1, is an eight-bit character code that
|
|||
|
extends ASCII with characters used by the languages of Western
|
|||
|
Europe. In other words, the code points from 0-127 map to the same
|
|||
|
characters in ASCII and ISO 8859-1, but ISO 8859-1 also provides
|
|||
|
mappings for code points up to 255. Unicode is a character code
|
|||
|
designed to provide a code point for virtually every character of all
|
|||
|
the world's languages. Unicode is a superset of ISO 8859-1 in the
|
|||
|
same way that ISO 8859-1 is a superset of ASCII--the code points from
|
|||
|
0-255 map to the same characters in both ISO 8859-1 and Unicode.
|
|||
|
(Thus, Unicode is also a superset of ASCII.)</P><P>Since ISO 8859-1 is an eight-bit character code, it's encoded using
|
|||
|
one byte per character. For Unicode strings, ID3 uses the UCS-2
|
|||
|
encoding with a leading <I>byte order mark</I>.<SUP>4</SUP> I'll discuss what a byte order
|
|||
|
mark is in a moment.</P><P>Reading and writing these two encodings isn't a problem--it's just a
|
|||
|
question of reading and writing unsigned integers in various formats,
|
|||
|
and you just finished writing the code to do that. The trick is how
|
|||
|
you translate those numeric values to Lisp character objects.</P><P>The Lisp implementation you're using probably uses either Unicode or
|
|||
|
ISO 8859-1 as its internal character code. And since all the values
|
|||
|
from 0-255 map to the same characters in both ISO 8859-1 and Unicode,
|
|||
|
you can use Lisp's <CODE><B>CODE-CHAR</B></CODE> and <CODE><B>CHAR-CODE</B></CODE> functions to
|
|||
|
translate those values in both character codes. However, if your Lisp
|
|||
|
supports only ISO 8859-1, then you'll be able to represent only the
|
|||
|
first 255 Unicode characters as Lisp characters. In other words, in
|
|||
|
such a Lisp implementation, if you try to process an ID3 tag that
|
|||
|
uses Unicode strings and if any of those strings contain characters
|
|||
|
with code points higher than 255, you'll get an error when you try to
|
|||
|
translate the code point to a Lisp character. For now I'll assume
|
|||
|
either you're using a Unicode-based Lisp or you won't process any
|
|||
|
files containing characters outside the ISO 8859-1 range.</P><P>The other issue with encoding strings is how to know how many bytes
|
|||
|
to interpret as character data. ID3 uses two strategies I mentioned
|
|||
|
in the previous chapter--some strings are terminated with a null
|
|||
|
character, while other strings occur in positions where you can
|
|||
|
determine the number of bytes to read, either because the string at
|
|||
|
that position is always the same length or because the string is at
|
|||
|
the end of a composite structure whose overall size you know. Note,
|
|||
|
however, that the number of bytes isn't necessarily the same as the
|
|||
|
number of characters in the string.</P><P>Putting all these variations together, the ID3 format uses four ways
|
|||
|
to read and write strings--two characters crossed with two ways of
|
|||
|
delimiting the string data.</P><P>Obviously, much of the logic of reading and writing strings will be
|
|||
|
quite similar. So, you can start by defining two binary types, one
|
|||
|
for reading strings of a specific length (in characters) and another
|
|||
|
for reading terminated strings. Both types take advantage of that the
|
|||
|
type argument to <CODE>read-value</CODE> and <CODE>write-value</CODE> is just
|
|||
|
another piece of data; you can make the type of character to read a
|
|||
|
parameter of these types. This is a technique you'll use quite a few
|
|||
|
times in this chapter.</P><PRE>(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))))</PRE><P>With these types available, there's not much to reading ISO 8859-1
|
|||
|
strings. Because the <CODE>character-type</CODE> argument you pass to
|
|||
|
<CODE>read-value</CODE> and <CODE>write-value</CODE> of a <CODE>generic-string</CODE>
|
|||
|
must be the name of a binary type, you need to define an
|
|||
|
<CODE>iso-8859-1-char</CODE> binary type. This also gives you a good place
|
|||
|
to put a bit of sanity checking on the code points of characters you
|
|||
|
read and write.</P><PRE>(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)))))</PRE><P>Now defining the ISO 8859-1 string types is trivial using the short
|
|||
|
form of <CODE>define-binary-type</CODE> as follows:</P><PRE>(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))</PRE><P>Reading UCS-2 strings is only slightly more complex. The complexity
|
|||
|
arises because you can encode a UCS-2 code point in two ways: most
|
|||
|
significant byte first (big-endian) or least significant byte first
|
|||
|
(little-endian). UCS-2 strings therefore start with two extra bytes,
|
|||
|
called the <I>byte order mark</I>, made up of the numeric value
|
|||
|
<CODE>#xfeff</CODE> encoded in either big-endian form or little-endian
|
|||
|
form. When reading a UCS-2 string, you read the byte order mark and
|
|||
|
then, depending on its value, read either big-endian or little-endian
|
|||
|
characters. Thus, you'll need two different UCS-2 character types.
|
|||
|
But you need only one version of the sanity-checking code, so you can
|
|||
|
define a parameterized binary type like this:</P><PRE>(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))))</PRE><P>where the <CODE>swap-bytes</CODE> function can be defined as follows,
|
|||
|
taking advantage of <CODE><B>LDB</B></CODE> being <CODE><B>SETF</B></CODE>able and thus
|
|||
|
<CODE><B>ROTATEF</B></CODE>able:</P><PRE>(defun swap-bytes (code)
|
|||
|
(assert (<= code #xffff))
|
|||
|
(rotatef (ldb (byte 8 0) code) (ldb (byte 8 8) code))
|
|||
|
code)</PRE><P>Using <CODE>ucs-2-char</CODE>, you can define two character types that will
|
|||
|
be used as the <CODE>character-type</CODE> arguments to the generic string
|
|||
|
functions.</P><PRE>(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))</PRE><P>Then you need a function that returns the name of the character type
|
|||
|
to use based on the value of the byte order mark.</P><PRE>(defun ucs-2-char-type (byte-order-mark)
|
|||
|
(ecase byte-order-mark
|
|||
|
(#xfeff 'ucs-2-char-big-endian)
|
|||
|
(#xfffe 'ucs-2-char-little-endian)))</PRE><P>Now you can define length- and terminator-delimited string types for
|
|||
|
UCS-2-encoded strings that read the byte order mark and use it to
|
|||
|
determine which variant of UCS-2 character to pass as the
|
|||
|
<CODE>character-type</CODE> argument to <CODE>read-value</CODE> and
|
|||
|
<CODE>write-value</CODE>. The only other wrinkle is that you need to
|
|||
|
translate the <CODE>length</CODE> argument, which is a number of bytes, to
|
|||
|
the number of characters to read, accounting for the byte order mark.</P><PRE>(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))))</PRE><A NAME="id3-tag-header"><H2>ID3 Tag Header</H2></A><P>With the basic primitive types done, you're ready to switch to a
|
|||
|
high-level view and start defining binary classes to represent first
|
|||
|
the ID3 tag as a whole and then the individual frames.</P><P>If you turn first to the ID3v2.2 specification, you'll see that the
|
|||
|
basic structure of the tag is this header:</P><PRE>ID3/file identifier "ID3"
|
|||
|
ID3 version $02 00
|
|||
|
ID3 flags %xx000000
|
|||
|
ID3 size 4 * %0xxxxxxx</PRE><P>followed by frame data and padding. Since you've already defined
|
|||
|
binary types to read and write all the fields in the header, defining
|
|||
|
a class that can read the header of an ID3 tag is just a matter of
|
|||
|
putting them together.</P><PRE>(define-binary-class id3-tag ()
|
|||
|
((identifier (iso-8859-1-string :length 3))
|
|||
|
(major-version u1)
|
|||
|
(revision u1)
|
|||
|
(flags u1)
|
|||
|
(size id3-tag-size)))</PRE><P>If you have some MP3 files lying around, you can test this much of
|
|||
|
the code and also see what version of ID3 tags your MP3s contain.
|
|||
|
First you can write a function that reads an <CODE>id3-tag</CODE>, as just
|
|||
|
defined, from the beginning of a file. Be aware, however, that ID3
|
|||
|
tags aren't required to appear at the beginning of a file, though
|
|||
|
these days they almost always do. To find an ID3 tag elsewhere in a
|
|||
|
file, you can scan the file looking for the sequence of bytes 73, 68,
|
|||
|
51 (in other words, the string "ID3").<SUP>5</SUP> For now
|
|||
|
you can probably get away with assuming the tags are the first thing
|
|||
|
in the file.</P><PRE>(defun read-id3 (file)
|
|||
|
(with-open-file (in file :element-type '(unsigned-byte 8))
|
|||
|
(read-value 'id3-tag in)))</PRE><P>On top of this function you can build a function that takes a
|
|||
|
filename and prints the information in the tag header along with the
|
|||
|
name of the file.</P><PRE>(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))))</PRE><P>It prints output that looks like this:</P><PRE>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</PRE><P>Of course, to determine what versions of ID3 are most common in your
|
|||
|
MP3 library, it'd be handier to have a function that returns a
|
|||
|
summary of all the MP3 files under a given directory. You can write
|
|||
|
one easily enough using the <CODE>walk-directory</CODE> function defined in
|
|||
|
Chapter 15. First define a helper function that tests whether a given
|
|||
|
filename has an <CODE>mp3</CODE> extension.</P><PRE>(defun mp3-p (file)
|
|||
|
(and
|
|||
|
(not (directory-pathname-p file))
|
|||
|
(string-equal "mp3" (pathname-type file))))</PRE><P>Then you can combine <CODE>show-tag-header</CODE> and <CODE>mp3-p</CODE> with
|
|||
|
<CODE>walk-directory</CODE> to print a summary of the ID3 header in each
|
|||
|
file under a given directory.</P><PRE>(defun show-tag-headers (dir)
|
|||
|
(walk-directory dir #'show-tag-header :test #'mp3-p))</PRE><P>However, if you have a lot of MP3s, you may just want a count of how
|
|||
|
many ID3 tags of each version you have in your MP3 collection. To get
|
|||
|
that information, you might write a function like this:</P><PRE>(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))</PRE><P>Another function you'll need in Chapter 29 is one that tests whether
|
|||
|
a file actually starts with an ID3 tag, which you can define like
|
|||
|
this:</P><PRE>(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))))</PRE><A NAME="id3-frames"><H2>ID3 Frames</H2></A><P>As I discussed earlier, the bulk of an ID3 tag is divided into
|
|||
|
frames. Each frame has a structure similar to that of the tag as a
|
|||
|
whole. Each frame starts with a header indicating what kind of frame
|
|||
|
it is and the size of the frame in bytes. The structure of the frame
|
|||
|
header changed slightly between version 2.2 and version 2.3 of the
|
|||
|
ID3 format, and eventually you'll have to deal with both forms. To
|
|||
|
start, you can focus on parsing version 2.2 frames.</P><P>The header of a 2.2 frame consists of three bytes that encode a
|
|||
|
three-character ISO 8859-1 string followed by a three-byte unsigned
|
|||
|
integer, which specifies the size of the frame in bytes, excluding
|
|||
|
the six-byte header. The string identifies what type of frame it is,
|
|||
|
which determines how you parse the data following the size. This is
|
|||
|
exactly the kind of situation for which you defined the
|
|||
|
<CODE>define-tagged-binary-class</CODE> macro. You can define a tagged
|
|||
|
class that reads the frame header and then dispatches to the
|
|||
|
appropriate concrete class using a function that maps IDs to a class
|
|||
|
names.</P><PRE>(define-tagged-binary-class id3-frame ()
|
|||
|
((id (iso-8859-1-string :length 3))
|
|||
|
(size u3))
|
|||
|
(:dispatch (find-frame-class id)))</PRE><P>Now you're ready to start implementing concrete frame classes.
|
|||
|
However, the specification defines quite a few--63 in version 2.2 and
|
|||
|
even more in later specs. Even considering frame types that share a
|
|||
|
common structure to be equivalent, you'll still find 24 unique frame
|
|||
|
types in version 2.2. But only a few of these are used "in the wild."
|
|||
|
So rather than immediately setting to work defining classes for each
|
|||
|
of the frame types, you can start by writing a generic frame class
|
|||
|
that lets you read the frames in a tag without parsing the data
|
|||
|
within the frames themselves. This will give you a way to find out
|
|||
|
what frames are actually present in the MP3s you want to process.
|
|||
|
You'll need this class eventually anyway because the specification
|
|||
|
allows for experimental frames that you'll need to be able to read
|
|||
|
without parsing.</P><P>Since the size field of the frame header tells you exactly how many
|
|||
|
bytes long the frame is, you can define a <CODE>generic-frame</CODE> class
|
|||
|
that extends <CODE>id3-frame</CODE> and adds a single field, <CODE>data</CODE>,
|
|||
|
that will hold an array of bytes.</P><PRE>(define-binary-class generic-frame (id3-frame)
|
|||
|
((data (raw-bytes :size size))))</PRE><P>The type of the data field, <CODE>raw-bytes</CODE>, just needs to hold an
|
|||
|
array of bytes. You can define it like this:</P><PRE>(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)))</PRE><P>For the time being, you'll want all frames to be read as
|
|||
|
<CODE>generic-frame</CODE>s, so you can define the <CODE>find-frame-class</CODE>
|
|||
|
function used in <CODE>id3-frame</CODE>'s <CODE>:dispatch</CODE> expression to
|
|||
|
always return <CODE>generic-frame</CODE>, regardless of the frame's
|
|||
|
<CODE>id</CODE>.</P><PRE>(defun find-frame-class (id)
|
|||
|
(declare (ignore id))
|
|||
|
'generic-frame)</PRE><P>Now you need to modify <CODE>id3-tag</CODE> so it'll read frames after the
|
|||
|
header fields. There's only one tricky bit to reading the frame data:
|
|||
|
although the tag header tells you how many bytes long the tag is,
|
|||
|
that number includes the padding that can follow the frame data.
|
|||
|
Since the tag header doesn't tell you how many frames the tag
|
|||
|
contains, the only way to tell when you've hit the padding is to look
|
|||
|
for a null byte where you'd expect a frame identifier.</P><P>To handle this, you can define a binary type, <CODE>id3-frames</CODE>, that
|
|||
|
will be responsible for reading the remainder of a tag, creating
|
|||
|
frame objects to represent all the frames it finds, and then skipping
|
|||
|
over any padding. This type will take as a parameter the tag size,
|
|||
|
which it can use to avoid reading past the end of the tag. But the
|
|||
|
reading code will also need to detect the beginning of the padding
|
|||
|
that can follow the tag's frame data. Rather than calling
|
|||
|
<CODE>read-value</CODE> directly in <CODE>id3-frames</CODE> <CODE>:reader</CODE>, you
|
|||
|
should use a function <CODE>read-frame</CODE>, which you'll define to
|
|||
|
return <CODE><B>NIL</B></CODE> when it detects padding, otherwise returning an
|
|||
|
<CODE>id3-frame</CODE> object read using <CODE>read-value</CODE>. Assuming you
|
|||
|
define <CODE>read-frame</CODE> so it reads only one byte past the end of
|
|||
|
the last frame in order to detect the start of the padding, you can
|
|||
|
define the <CODE>id3-frames</CODE> binary type like this:</P><PRE>(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)))))</PRE><P>You can use this type to add a <CODE>frames</CODE> slot to <CODE>id3-tag</CODE>.</P><PRE>(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))))</PRE><A NAME="detecting-tag-padding"><H2>Detecting Tag Padding</H2></A><P>Now all that remains is to implement <CODE>read-frame</CODE>. This is a bit
|
|||
|
tricky since the code that actually reads bytes from the stream is
|
|||
|
several layers down from <CODE>read-frame</CODE>.</P><P>What you'd really like to do in <CODE>read-frame</CODE> is read one byte
|
|||
|
and return <CODE><B>NIL</B></CODE> if it's a null and otherwise read a frame with
|
|||
|
<CODE>read-value</CODE>. Unfortunately, if you read the byte in
|
|||
|
<CODE>read-frame</CODE>, then it won't be available to be read by
|
|||
|
<CODE>read-value</CODE>.<SUP>6</SUP></P><P>It turns out this is a perfect opportunity to use the condition
|
|||
|
system--you can check for null bytes in the low-level code that reads
|
|||
|
from the stream and signal a condition when you read a null;
|
|||
|
<CODE>read-frame</CODE> can then handle the condition by unwinding the
|
|||
|
stack before more bytes are read. In addition to turning out to be a
|
|||
|
tidy solution to the problem of detecting the start of the tag's
|
|||
|
padding, this is also an example of how you can use conditions for
|
|||
|
purposes other than handling errors.</P><P>You can start by defining a condition type to be signaled by the
|
|||
|
low-level code and handled by the high-level code. This condition
|
|||
|
doesn't need any slots--you just need a distinct class of condition
|
|||
|
so you know no other code will be signaling or handling it.</P><PRE>(define-condition in-padding () ())</PRE><P>Next you need to define a binary type whose <CODE>:reader</CODE> reads a
|
|||
|
given number of bytes, first reading a single byte and signaling an
|
|||
|
<CODE>in-padding</CODE> condition if the byte is null and otherwise reading
|
|||
|
the remaining bytes as an <CODE>iso-8859-1-string</CODE> and combining it
|
|||
|
with the first byte read.</P><PRE>(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)))</PRE><P>If you redefine <CODE>id3-frame</CODE> to make the type of its <CODE>id</CODE>
|
|||
|
slot <CODE>frame-id</CODE> instead of <CODE>iso-8859-1-string</CODE>, the
|
|||
|
condition will be signaled whenever <CODE>id3-frame</CODE>'s
|
|||
|
<CODE>read-value</CODE> method reads a null byte instead of the beginning
|
|||
|
of a frame.</P><PRE>(define-tagged-binary-class id3-frame ()
|
|||
|
((id (frame-id :length 3))
|
|||
|
(size u3))
|
|||
|
(:dispatch (find-frame-class id)))</PRE><P>Now all <CODE>read-frame</CODE> has to do is wrap a call to
|
|||
|
<CODE>read-value</CODE> in a <CODE><B>HANDLER-CASE</B></CODE> that handles the
|
|||
|
<CODE>in-padding</CODE> condition by returning <CODE><B>NIL</B></CODE>.</P><PRE>(defun read-frame (in)
|
|||
|
(handler-case (read-value 'id3-frame in)
|
|||
|
(in-padding () nil)))</PRE><P>With <CODE>read-frame</CODE> defined, you can now read a complete version
|
|||
|
2.2 ID3 tag, representing frames with instances of
|
|||
|
<CODE>generic-frame</CODE>. In the "What Frames Do You Actually Need?"
|
|||
|
section, you'll do some experiments at the REPL to determine what
|
|||
|
frame classes you need to implement. But first let's add support for
|
|||
|
version 2.3 ID3 tags.</P><A NAME="supporting-multiple-versions-of-id3"><H2>Supporting Multiple Versions of ID3</H2></A><P>Currently, <CODE>id3-tag</CODE> is defined using
|
|||
|
<CODE>define-binary-class</CODE>, but if you want to support multiple
|
|||
|
versions of ID3, it makes more sense to use a
|
|||
|
<CODE>define-tagged-binary-class</CODE> that dispatches on the
|
|||
|
<CODE>major-version</CODE> value. As it turns out, all versions of ID3v2
|
|||
|
have the same structure up to the size field. So, you can define a
|
|||
|
tagged binary class like the following that defines this basic
|
|||
|
structure and then dispatches to the appropriate version-specific
|
|||
|
subclass:</P><PRE>(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))))</PRE><P>Version 2.2 and version 2.3 tags differ in two ways. First, the
|
|||
|
header of a version 2.3 tag may be extended with up to four optional
|
|||
|
extended header fields, as determined by values in the flags field.
|
|||
|
Second, the frame format changed between version 2.2 and version 2.3,
|
|||
|
which means you'll have to use different classes to represent version
|
|||
|
2.2 frames and the corresponding version 2.3 frames.</P><P>Since the new <CODE>id3-tag</CODE> class is based on the one you originally
|
|||
|
wrote to represent version 2.2 tags, it's not surprising that the new
|
|||
|
<CODE>id3v2.2-tag</CODE> class is trivial, inheriting most of its slots
|
|||
|
from the new <CODE>id3-tag</CODE> class and adding the one missing slot,
|
|||
|
<CODE>frames</CODE>. Because version 2.2 and version 2.3 tags use different
|
|||
|
frame formats, you'll have to change the <CODE>id3-frames</CODE> type to be
|
|||
|
parameterized with the type of frame to read. For now, assume you'll
|
|||
|
do that and add a <CODE>:frame-type</CODE> argument to the
|
|||
|
<CODE>id3-frames</CODE> type descriptor like this:</P><PRE>(define-binary-class id3v2.2-tag (id3-tag)
|
|||
|
((frames (id3-frames :tag-size size :frame-type 'id3v2.2-frame))))</PRE><P>The <CODE>id3v2.3-tag</CODE> class is slightly more complex because of the
|
|||
|
optional fields. The first three of the four optional fields are
|
|||
|
included when the sixth bit in <CODE>flags</CODE> is set. They're a four-
|
|||
|
byte integer specifying the size of the extended header, two bytes
|
|||
|
worth of flags, and another four-byte integer specifying how many
|
|||
|
bytes of padding are included in the tag.<SUP>7</SUP> The fourth optional field, included when
|
|||
|
the fifteenth bit of the extended header flags is set, is a four-byte
|
|||
|
cyclic redundancy check (CRC) of the rest of the tag.</P><P>The binary data library doesn't provide any special support for
|
|||
|
optional fields in a binary class, but it turns out that regular
|
|||
|
parameterized binary types are sufficient. You can define a type
|
|||
|
parameterized with the name of a type and a value that indicates
|
|||
|
whether a value of that type should actually be read or written.</P><PRE>(define-binary-type optional (type if)
|
|||
|
(:reader (in)
|
|||
|
(when if (read-value type in)))
|
|||
|
(:writer (out value)
|
|||
|
(when if (write-value type out value))))</PRE><P>Using <CODE>if</CODE> as the parameter name looks a bit strange in that
|
|||
|
code, but it makes the <CODE>optional</CODE> type descriptors quite
|
|||
|
readable. For instance, here's the definition of <CODE>id3v2.3-tag</CODE>
|
|||
|
using <CODE>optional</CODE> slots:</P><PRE>(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))))</PRE><P>where <CODE>extended-p</CODE> and <CODE>crc-p</CODE> are helper functions that
|
|||
|
test the appropriate bit of the flags value they're passed. To test
|
|||
|
whether an individual bit of an integer is set, you can use
|
|||
|
<CODE><B>LOGBITP</B></CODE>, another bit-twiddling function. It takes an index and
|
|||
|
an integer and returns true if the specified bit is set in the
|
|||
|
integer.</P><PRE>(defun extended-p (flags) (logbitp 6 flags))
|
|||
|
|
|||
|
(defun crc-p (flags extra-flags)
|
|||
|
(and (extended-p flags) (logbitp 15 extra-flags)))</PRE><P>As in the version 2.2 tag class, the frames slot is defined to be of
|
|||
|
type <CODE>id3-frames</CODE>, passing the name of the frame type as a
|
|||
|
parameter. You do, however, need to make a few small changes to
|
|||
|
<CODE>id3-frames</CODE> and <CODE>read-frame</CODE> to support the extra
|
|||
|
<CODE>frame-type</CODE> parameter.</P><PRE>(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)))</PRE><P>The changes are in the calls to <CODE>read-frame</CODE> and
|
|||
|
<CODE>write-value</CODE>, where you need to pass the <CODE>frame-type</CODE>
|
|||
|
argument and, in computing the size of the frame, where you need to
|
|||
|
use a function <CODE>frame-header-size</CODE> instead of the literal value
|
|||
|
<CODE>6</CODE> since the frame header changed size between version 2.2 and
|
|||
|
version 2.3. Since the difference in the result of this function is
|
|||
|
based on the class of the frame, it makes sense to define it as a
|
|||
|
generic function like this:</P><PRE>(defgeneric frame-header-size (frame))</PRE><P>You'll define the necessary methods on that generic function in the
|
|||
|
next section after you define the new frame classes.</P><A NAME="versioned-frame-base-classes"><H2>Versioned Frame Base Classes</H2></A><P>Where before you defined a single base class for all frames, you'll
|
|||
|
now have two classes, <CODE>id3v2.2-frame</CODE> and <CODE>id3v2.3-frame</CODE>.
|
|||
|
The <CODE>id3v2.2-frame</CODE> class will be essentially the same as the
|
|||
|
original <CODE>id3-frame</CODE> class.</P><PRE>(define-tagged-binary-class id3v2.2-frame ()
|
|||
|
((id (frame-id :length 3))
|
|||
|
(size u3))
|
|||
|
(:dispatch (find-frame-class id)))</PRE><P>The <CODE>id3v2.3-frame</CODE>, on the other hand, requires more changes.
|
|||
|
The frame identifier and size fields were extended in version 2.3
|
|||
|
from three to four bytes each, and two bytes worth of flags were
|
|||
|
added. Additionally, the frame, like the version 2.3 tag, can contain
|
|||
|
optional fields, controlled by the values of three of the frame's
|
|||
|
flags.<SUP>8</SUP> With those changes in
|
|||
|
mind, you can define the version 2.3 frame base class, along with
|
|||
|
some helper functions, like this:</P><PRE>(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))</PRE><P>With these two classes defined, you can now implement the methods on
|
|||
|
the generic function <CODE>frame-header-size</CODE>.</P><PRE>(defmethod frame-header-size ((frame id3v2.2-frame)) 6)
|
|||
|
|
|||
|
(defmethod frame-header-size ((frame id3v2.3-frame)) 10)</PRE><P>The optional fields in a version 2.3 frame aren't counted as part of
|
|||
|
the header for this computation since they're already included in the
|
|||
|
value of the frame's <CODE>size</CODE>.</P><A NAME="versioned-concrete-frame-classes"><H2>Versioned Concrete Frame Classes</H2></A><P>In the original definition, <CODE>generic-frame</CODE> subclassed
|
|||
|
<CODE>id3-frame</CODE>. But now <CODE>id3-frame</CODE> has been replaced with the
|
|||
|
two version-specific base classes, <CODE>id3v2.2-frame</CODE> and
|
|||
|
<CODE>id3v2.3-frame</CODE>. So, you need to define two new versions of
|
|||
|
<CODE>generic-frame</CODE>, one for each base class. One way to define this
|
|||
|
classes would be like this:</P><PRE>(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))))</PRE><P>However, it's a bit annoying that these two classes are the same
|
|||
|
except for their superclass. It's not too bad in this case since
|
|||
|
there's only one additional field. But if you take this approach for
|
|||
|
other concrete frame classes, ones that have a more complex internal
|
|||
|
structure that's identical between the two ID3 versions, the
|
|||
|
duplication will be more irksome.</P><P>Another approach, and the one you should actually use, is to define a
|
|||
|
class <CODE>generic-frame</CODE> as a <I>mixin</I>: a class intended to be
|
|||
|
used as a superclass along with one of the version-specific base
|
|||
|
classes to produce a concrete, version-specific frame class. The only
|
|||
|
tricky bit about this approach is that if <CODE>generic-frame</CODE>
|
|||
|
doesn't extend either of the frame base classes, then you can't refer
|
|||
|
to the <CODE>size</CODE> slot in its definition. Instead, you must use the
|
|||
|
<CODE>current-binary-object</CODE> function I discussed at the end of the
|
|||
|
previous chapter to access the object you're in the midst of reading
|
|||
|
or writing and pass it to <CODE>size</CODE>. And you need to account for
|
|||
|
the difference in the number of bytes of the total frame size that
|
|||
|
will be left over, in the case of a version 2.3 frame, if any of the
|
|||
|
optional fields are included in the frame. So, you should define a
|
|||
|
generic function <CODE>data-bytes</CODE> with methods that do the right
|
|||
|
thing for both version 2.2 and version 2.3 frames.</P><PRE>(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))))</PRE><P>Then you can define concrete classes that extend one of the
|
|||
|
version-specific base classes and <CODE>generic-frame</CODE> to define
|
|||
|
version-specific generic frame classes.</P><PRE>(define-binary-class generic-frame-v2.2 (id3v2.2-frame generic-frame) ())
|
|||
|
|
|||
|
(define-binary-class generic-frame-v2.3 (id3v2.3-frame generic-frame) ())</PRE><P>With these classes defined, you can redefine the
|
|||
|
<CODE>find-frame-class</CODE> function to return the right versioned class
|
|||
|
based on the length of the identifier.</P><PRE>(defun find-frame-class (id)
|
|||
|
(ecase (length id)
|
|||
|
(3 'generic-frame-v2.2)
|
|||
|
(4 'generic-frame-v2.3)))</PRE><A NAME="what-frames-do-you-actually-need"><H2>What Frames Do You Actually Need?</H2></A><P>With the ability to read both version 2.2 and version 2.3 tags using
|
|||
|
generic frames, you're ready to start implementing classes to
|
|||
|
represent the specific frames you care about. However, before you
|
|||
|
dive in, you should take a breather and figure out what frames you
|
|||
|
actually care about since, as I mentioned earlier, the ID3 spec
|
|||
|
specifies many frames that are almost never used. Of course, what
|
|||
|
frames you care about depends on what kinds of applications you're
|
|||
|
interested in writing. If you're mostly interested in extracting
|
|||
|
information from existing ID3 tags, then you need implement only the
|
|||
|
classes representing the frames containing the information you care
|
|||
|
about. On the other hand, if you want to write an ID3 tag editor, you
|
|||
|
may need to support all the frames.</P><P>Rather than guessing which frames will be most useful, you can use
|
|||
|
the code you've already written to poke around a bit at the REPL and
|
|||
|
see what frames are actually used in your own MP3s. To start, you
|
|||
|
need an instance of <CODE>id3-tag</CODE>, which you can get with the
|
|||
|
<CODE>read-id3</CODE> function.</P><PRE>ID3V2> (read-id3 "/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3")
|
|||
|
#<ID3V2.2-TAG @ #x727b2912></PRE><P>Since you'll want to play with this object a bit, you should save it
|
|||
|
in a variable.</P><PRE>ID3V2> (defparameter *id3* (read-id3 "/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3"))
|
|||
|
*ID3*</PRE><P>Now you can see, for example, how many frames it has.</P><PRE>ID3V2> (length (frames *id3*))
|
|||
|
11</PRE><P>Not too many--let's take a look at what they are.</P><PRE>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>)</PRE><P>Okay, that's not too informative. What you really want to know are
|
|||
|
what kinds of frames are in there. In other words, you want to know
|
|||
|
the <CODE>id</CODE>s of those frames, which you can get with a simple
|
|||
|
<CODE><B>MAPCAR</B></CODE> like this:</P><PRE>ID3V2> (mapcar #'id (frames *id3*))
|
|||
|
("TT2" "TP1" "TAL" "TRK" "TPA" "TYE" "TCO" "TEN" "COM" "COM" "COM")</PRE><P>If you look up these identifiers in the ID3v2.2 spec, you'll discover
|
|||
|
that all the frames with identifiers starting with <I>T</I> are text
|
|||
|
information frames and have a similar structure. And <I>COM</I> is the
|
|||
|
identifier for comment frames, which have a structure similar to that
|
|||
|
of text information frames. The particular text information frames
|
|||
|
identified here turn out to be the frames for representing the song
|
|||
|
title, artist, album, track, part of set, year, genre, and encoding
|
|||
|
program.</P><P>Of course, this is just one MP3 file. Maybe other frames are used in
|
|||
|
other files. It's easy enough to discover. First define a function
|
|||
|
that combines the previous <CODE><B>MAPCAR</B></CODE> expression with a call to
|
|||
|
<CODE>read-id3</CODE> and wraps the whole thing in a <CODE><B>DELETE-DUPLICATES</B></CODE>
|
|||
|
to keep things tidy. You'll have to use a <CODE>:test</CODE> argument of
|
|||
|
<CODE>#'string=</CODE> to <CODE><B>DELETE-DUPLICATES</B></CODE> to specify that you want
|
|||
|
two elements considered the same if they're the same string.</P><PRE>(defun frame-types (file)
|
|||
|
(delete-duplicates (mapcar #'id (frames (read-id3 file))) :test #'string=))</PRE><P>This should give the same answer except with only one of each
|
|||
|
identifier when passed the same filename.</P><PRE>ID3V2> (frame-types "/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3")
|
|||
|
("TT2" "TP1" "TAL" "TRK" "TPA" "TYE" "TCO" "TEN" "COM")</PRE><P>Then you can use Chapter 15's <CODE>walk-directory</CODE> function along
|
|||
|
with <CODE>mp3-p</CODE> to find every MP3 file under a directory and
|
|||
|
combine the results of calling <CODE>frame-types</CODE> on each file.
|
|||
|
Recall that <CODE><B>NUNION</B></CODE> is the recycling version of the <CODE><B>UNION</B></CODE>
|
|||
|
function; since <CODE>frame-types</CODE> makes a new list for each file,
|
|||
|
this is safe.</P><PRE>(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))</PRE><P>Now pass it the name of a directory, and it'll tell you the set of
|
|||
|
identifiers used in all the MP3 files under that directory. It may
|
|||
|
take a few seconds depending how many MP3 files you have, but you'll
|
|||
|
probably get something similar to this:</P><PRE>ID3V2> (frame-types-in-dir "/usr2/mp3/")
|
|||
|
("TCON" "COMM" "TRCK" "TIT2" "TPE1" "TALB" "TCP" "TT2" "TP1" "TCM"
|
|||
|
"TAL" "TRK" "TPA" "TYE" "TCO" "TEN" "COM")</PRE><P>The four-letter identifiers are the version 2.3 equivalents of the
|
|||
|
version 2.2 identifiers I discussed previously. Since the information
|
|||
|
stored in those frames is exactly the information you'll need in
|
|||
|
Chapter 27, it makes sense to implement classes only for the frames
|
|||
|
actually used, namely, text information and comment frames, which
|
|||
|
you'll do in the next two sections. If you decide later that you want
|
|||
|
to support other frame types, it's mostly a matter of translating the
|
|||
|
ID3 specifications into the appropriate binary class definitions.</P><A NAME="text-information-frames"><H2>Text Information Frames</H2></A><P>All text information frames consist of two fields: a single byte
|
|||
|
indicating which string encoding is used in the frame and a string
|
|||
|
encoded in the remaining bytes of the frame. If the encoding byte is
|
|||
|
zero, the string is encoded in ISO 8859-1; if the encoding is one,
|
|||
|
the string is a UCS-2 string.</P><P>You've already defined binary types representing the four different
|
|||
|
kinds of strings--two different encodings each with two different
|
|||
|
methods of delimiting the string. However, <CODE>define-binary-class</CODE>
|
|||
|
provides no direct facility for determining the type of value to read
|
|||
|
based on other values in the object. Instead, you can define a binary
|
|||
|
type that you pass the value of the encoding byte and that then reads
|
|||
|
or writes the appropriate kind of string.</P><P>As long as you're defining such a type, you can also define it to
|
|||
|
take two parameters, <CODE>:length</CODE> and <CODE>:terminator</CODE>, and pick
|
|||
|
the right type of string based on which argument is supplied. To
|
|||
|
implement this new type, you must first define some helper functions.
|
|||
|
The first two return the name of the appropriate string type based on
|
|||
|
the encoding byte.</P><PRE>(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)))</PRE><P>Then <CODE>string-args</CODE> uses the encoding byte, the length, and the
|
|||
|
terminator to determine several of the arguments to be passed to
|
|||
|
<CODE>read-value</CODE> and <CODE>write-value</CODE> by the <CODE>:reader</CODE> and
|
|||
|
<CODE>:writer</CODE> of <CODE>id3-encoded-string</CODE>. One of the length and
|
|||
|
terminator arguments to <CODE>string-args</CODE> should always be <CODE><B>NIL</B></CODE>.</P><PRE>(defun string-args (encoding length terminator)
|
|||
|
(cond
|
|||
|
(length
|
|||
|
(values (non-terminated-type encoding) :length length))
|
|||
|
(terminator
|
|||
|
(values (terminated-type encoding) :terminator terminator))))</PRE><P>With those helpers, the definition of <CODE>id3-encoded-string</CODE> is
|
|||
|
simple. One detail to note is that the keyword--either <CODE>:length</CODE>
|
|||
|
or <CODE>:terminator</CODE>--used in the call to <CODE>read-value</CODE> and
|
|||
|
<CODE>write-value</CODE> is just another piece of data returned by
|
|||
|
<CODE>string-args</CODE>. Although keywords in arguments lists are almost
|
|||
|
always literal keywords, they don't have to be.</P><PRE>(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))))</PRE><P>Now you can define a <CODE>text-info</CODE> mixin class, much the way you
|
|||
|
defined <CODE>generic-frame</CODE> earlier.</P><PRE>(define-binary-class text-info-frame ()
|
|||
|
((encoding u1)
|
|||
|
(information (id3-encoded-string :encoding encoding :length (bytes-left 1)))))</PRE><P>As when you defined <CODE>generic-frame</CODE>, you need access to the size
|
|||
|
of the frame, in this case to compute the <CODE>:length</CODE> argument to
|
|||
|
pass to <CODE>id3-encoded-string</CODE>. Because you'll need to do a similar
|
|||
|
computation in the next class you define, you can go ahead and define
|
|||
|
a helper function, <CODE>bytes-left</CODE>, that uses
|
|||
|
<CODE>current-binary-object</CODE> to get at the size of the frame.</P><PRE>(defun bytes-left (bytes-read)
|
|||
|
(- (size (current-binary-object)) bytes-read))</PRE><P>Now, as you did with the <CODE>generic-frame</CODE> mixin, you can define
|
|||
|
two version-specific concrete classes with a minimum of duplicated
|
|||
|
code.</P><PRE>(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) ())</PRE><P>To wire these classes in, you need to modify <CODE>find-frame-class</CODE>
|
|||
|
to return the appropriate class name when the ID indicates the frame
|
|||
|
is a text information frame, namely, whenever the ID starts with
|
|||
|
<I>T</I> and isn't <I>TXX</I> or <I>TXXX</I>.</P><PRE>(defun find-frame-class (name)
|
|||
|
(cond
|
|||
|
((and (char= (char name 0) #\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)))))</PRE><A NAME="comment-frames"><H2>Comment Frames</H2></A><P>Another commonly used frame type is the comment frame, which is like a
|
|||
|
text information frame with a few extra fields. Like a text
|
|||
|
information frame, it starts with a single byte indicating the string
|
|||
|
encoding used in the frame. That byte is followed by a three-character
|
|||
|
ISO 8859-1 string (regardless of the value of the string encoding
|
|||
|
byte), which indicates what language the comment is in using an
|
|||
|
ISO-639-2 code, for example, "eng" for English or "jpn" for Japanese.
|
|||
|
That field is followed by two strings encoded as indicated by the
|
|||
|
first byte. The first is a null-terminated string containing a
|
|||
|
description of the comment. The second, which takes up the remainder
|
|||
|
of the frame, is the comment text itself.</P><PRE>(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)))))))</PRE><P>As in the definition of the <CODE>text-info</CODE> mixin, you can use
|
|||
|
<CODE>bytes-left</CODE> to compute the size of the final string. However,
|
|||
|
since the <CODE>description</CODE> field is a variable-length string, the
|
|||
|
number of bytes read prior to the start of <CODE>text</CODE> isn't a
|
|||
|
constant. To make matters worse, the number of bytes used to encode
|
|||
|
<CODE>description</CODE> is dependent on the encoding. So, you should
|
|||
|
define a helper function that returns the number of bytes used to
|
|||
|
encode a string given the string, the encoding code, and a boolean
|
|||
|
indicating whether the string is terminated with an extra character.</P><PRE>(defun encoded-string-length (string encoding terminated)
|
|||
|
(let ((characters (+ (length string) (if terminated 1 0))))
|
|||
|
(* characters (ecase encoding (0 1) (1 2)))))</PRE><P>And, as before, you can define the concrete version-specific comment
|
|||
|
frame classes and wire them into <CODE>find-frame-class</CODE>.</P><PRE>(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) #\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)))))</PRE><A NAME="extracting-information-from-an-id3-tag"><H2>Extracting Information from an ID3 Tag</H2></A><P>Now that you have the basic ability to read and write ID3 tags, you
|
|||
|
have a lot of directions you could take this code. If you want to
|
|||
|
develop a complete ID3 tag editor, you'll need to implement specific
|
|||
|
classes for all the frame types. You'd also need to define methods
|
|||
|
for manipulating the tag and frame objects in a consistent way (for
|
|||
|
instance, if you change the value of a string in a
|
|||
|
<CODE>text-info-frame</CODE>, you'll likely need to adjust the size); as
|
|||
|
the code stands, there's nothing to make sure that
|
|||
|
happens.<SUP>9</SUP></P><P>Or, if you just need to extract certain pieces of information about
|
|||
|
an MP3 file from its ID3 tag--as you will when you develop a
|
|||
|
streaming MP3 server in Chapters 27, 28, and 29--you'll need to write
|
|||
|
functions that find the appropriate frames and extract the
|
|||
|
information you want.</P><P>Finally, to make this production-quality code, you'd have to pore
|
|||
|
over the ID3 specs and deal with the details I skipped over in the
|
|||
|
interest of space. In particular, some of the flags in both the tag
|
|||
|
and the frame can affect the way the contents of the tag or frame is
|
|||
|
read; unless you write some code that does the right thing when those
|
|||
|
flags are set, there may be ID3 tags that this code won't be able to
|
|||
|
parse correctly. But the code from this chapter should be capable of
|
|||
|
parsing nearly all the MP3s you actually encounter.</P><P>For now you can finish with a few functions to extract individual
|
|||
|
pieces of information from an <CODE>id3-tag</CODE>. You'll need these
|
|||
|
functions in Chapter 27 and probably in other code that uses this
|
|||
|
library. They belong in this library because they depend on details
|
|||
|
of the ID3 format that the users of this library shouldn't have to
|
|||
|
worry about.</P><P>To get, say, the name of the song of the MP3 from which an
|
|||
|
<CODE>id3-tag</CODE> was extracted, you need to find the ID3 frame with a
|
|||
|
specific identifier and then extract the information field. And some
|
|||
|
pieces of information, such as the genre, can require further
|
|||
|
decoding. Luckily, all the frames that contain the information you'll
|
|||
|
care about are text information frames, so extracting a particular
|
|||
|
piece of information mostly boils down to using the right identifier
|
|||
|
to look up the appropriate frame. Of course, the ID3 authors decided
|
|||
|
to change all the identifiers between ID3v2.2 and ID3v2.3, so you'll
|
|||
|
have to account for that.</P><P>Nothing too complex--you just need to figure out the right path to
|
|||
|
get to the various pieces of information. This is a perfect bit of
|
|||
|
code to develop interactively, much the way you figured out what
|
|||
|
frame classes you needed to implement. To start, you need an
|
|||
|
<CODE>id3-tag</CODE> object to play with. Assuming you have an MP3 laying
|
|||
|
around, you can use <CODE>read-id3</CODE> like this:</P><PRE>ID3V2> (defparameter *id3* (read-id3 "Kitka/Wintersongs/02 Byla Cesta.mp3"))
|
|||
|
*ID3*
|
|||
|
ID3V2> *id3*
|
|||
|
#<ID3V2.2-TAG @ #x73d04c1a></PRE><P>replacing <CODE>Kitka/Wintersongs/02 Byla Cesta.mp3</CODE> with the
|
|||
|
filename of your MP3. Once you have your <CODE>id3-tag</CODE> object, you
|
|||
|
can start poking around. For instance, you can check out the list of
|
|||
|
frame objects with the <CODE>frames</CODE> function.</P><PRE>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>)</PRE><P>Now suppose you want to extract the song title. It's probably in one
|
|||
|
of those frames, but to find it, you need to find the frame with the
|
|||
|
"TT2" identifier. Well, you can check easily enough to see if the tag
|
|||
|
contains such a frame by extracting all the identifiers like this:</P><PRE>ID3V2> (mapcar #'id (frames *id3*))
|
|||
|
("TT2" "TP1" "TAL" "TRK" "TPA" "TYE" "TCO" "TEN" "COM" "COM" "COM")</PRE><P>There it is, the first frame. However, there's no guarantee it'll
|
|||
|
always be the first frame, so you should probably look it up by
|
|||
|
identifier rather than position. That's also straightforward using
|
|||
|
the <CODE><B>FIND</B></CODE> function.</P><PRE>ID3V2> (find "TT2" (frames *id3*) :test #'string= :key #'id)
|
|||
|
#<TEXT-INFO-FRAME-V2.2 @ #x73d04cca></PRE><P>Now, to get at the actual information in the frame, do this:</P><PRE>ID3V2> (information (find "TT2" (frames *id3*) :test #'string= :key #'id))
|
|||
|
"Byla Cesta^@"</PRE><P>Whoops. That <CODE>^@</CODE> is how Emacs prints a null character. In a
|
|||
|
maneuver reminiscent of the kludge that turned ID3v1 into ID3v1.1,
|
|||
|
the <CODE>information</CODE> slot of a text information frame, though not
|
|||
|
officially a null-terminated string, can contain a null, and ID3
|
|||
|
readers are supposed to ignore any characters after the null. So, you
|
|||
|
need a function that takes a string and returns the contents up to
|
|||
|
the first null character, if any. That's easy enough using the
|
|||
|
<CODE>+null+</CODE> constant from the binary data library.</P><PRE>(defun upto-null (string)
|
|||
|
(subseq string 0 (position +null+ string)))</PRE><P>Now you can get just the title.</P><PRE>ID3V2> (upto-null (information (find "TT2" (frames *id3*) :test #'string= :key #'id)))
|
|||
|
"Byla Cesta"</PRE><P>You could just wrap that code in a function named <CODE>song</CODE> that
|
|||
|
takes an <CODE>id3-tag</CODE> as an argument, and you'd be done. However,
|
|||
|
the only difference between this code and the code you'll use to
|
|||
|
extract the other pieces of information you'll need (such as the
|
|||
|
album name, the artist, and the genre) is the identifier. So, it's
|
|||
|
better to split up the code a bit. For starters, you can write a
|
|||
|
function that just finds a frame given an <CODE>id3-tag</CODE> and an
|
|||
|
identifier like this:</P><PRE>(defun find-frame (id3 id)
|
|||
|
(find id (frames id3) :test #'string= :key #'id))
|
|||
|
|
|||
|
ID3V2> (find-frame *id3* "TT2")
|
|||
|
#<TEXT-INFO-FRAME-V2.2 @ #x73d04cca></PRE><P>Then the other bit of code, the part that extracts the information
|
|||
|
from a <CODE>text-info-frame</CODE>, can go in another function.</P><PRE>(defun get-text-info (id3 id)
|
|||
|
(let ((frame (find-frame id3 id)))
|
|||
|
(when frame (upto-null (information frame)))))
|
|||
|
|
|||
|
ID3V2> (get-text-info *id3* "TT2")
|
|||
|
"Byla Cesta"</PRE><P>Now the definition of <CODE>song</CODE> is just a matter of passing the
|
|||
|
right identifier.</P><PRE>(defun song (id3) (get-text-info id3 "TT2"))
|
|||
|
|
|||
|
ID3V2> (song *id3*)
|
|||
|
"Byla Cesta"</PRE><P>However, this definition of <CODE>song</CODE> works only with version 2.2
|
|||
|
tags since the identifier changed from "TT2" to "TIT2" between
|
|||
|
version 2.2 and version 2.3. And all the other tags changed too.
|
|||
|
Since the user of this library shouldn't have to know about different
|
|||
|
versions of the ID3 format to do something as simple as get the song
|
|||
|
title, you should probably handle those details for them. A simple
|
|||
|
way is to change <CODE>find-frame</CODE> to take not just a single
|
|||
|
identifier but a list of identifiers like this:</P><PRE>(defun find-frame (id3 ids)
|
|||
|
(find-if #'(lambda (x) (find (id x) ids :test #'string=)) (frames id3)))</PRE><P>Then change <CODE>get-text-info</CODE> slightly so it can take one or more
|
|||
|
identifiers using a <CODE><B>&rest</B></CODE> parameter.</P><PRE>(defun get-text-info (id3 &rest ids)
|
|||
|
(let ((frame (find-frame id3 ids)))
|
|||
|
(when frame (upto-null (information frame)))))</PRE><P>Then the change needed to allow <CODE>song</CODE> to support both version
|
|||
|
2.2 and version 2.3 tags is just a matter of adding the version 2.3
|
|||
|
identifier.</P><PRE>(defun song (id3) (get-text-info id3 "TT2" "TIT2"))</PRE><P>Then you just need to look up the appropriate version 2.2 and version
|
|||
|
2.3 frame identifiers for any fields for which you want to provide an
|
|||
|
accessor function. Here are the ones you'll need in Chapter 27:</P><PRE>(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"))</PRE><P>The last wrinkle is that the way the <CODE>genre</CODE> is stored in the
|
|||
|
TCO or TCON frames isn't always human readable. Recall that in ID3v1,
|
|||
|
genres were stored as a single byte that encoded a particular genre
|
|||
|
from a fixed list. Unfortunately, those codes live on in ID3v2--if
|
|||
|
the text of the genre frame is a number in parentheses, the number is
|
|||
|
supposed to be interpreted as an ID3v1 genre code. But, again, users
|
|||
|
of this library probably won't care about that ancient history. So,
|
|||
|
you should provide a function that automatically translates the
|
|||
|
genre. The following function uses the <CODE>genre</CODE> function just
|
|||
|
defined to extract the actual genre text and then checks whether it
|
|||
|
starts with a left parenthesis, decoding the version 1 genre code
|
|||
|
with a function you'll define in a moment if it does:</P><PRE>(defun translated-genre (id3)
|
|||
|
(let ((genre (genre id3)))
|
|||
|
(if (and genre (char= #\( (char genre 0)))
|
|||
|
(translate-v1-genre genre)
|
|||
|
genre)))</PRE><P>Since a version 1 genre code is effectively just an index into an
|
|||
|
array of standard names, the easiest way to implement
|
|||
|
<CODE>translate-v1-genre</CODE> is to extract the number from the genre
|
|||
|
string and use it as an index into an actual array.</P><PRE>(defun translate-v1-genre (genre)
|
|||
|
(aref *id3-v1-genres* (parse-integer genre :start 1 :junk-allowed t)))</PRE><P>Then all you need to do is to define the array of names. The
|
|||
|
following array of names includes the 80 official version 1 genres
|
|||
|
plus the genres created by the authors of Winamp:</P><PRE>(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"))</PRE><P>Once again, it probably feels like you wrote a ton of code in this
|
|||
|
chapter. But if you put it all in a file, or if you download the
|
|||
|
version from this book's Web site, you'll see it's just not that many
|
|||
|
lines--most of the pain of writing this library stems from having to
|
|||
|
understand the intricacies of the ID3 format itself. Anyway, now you
|
|||
|
have a major piece of what you'll turn into a streaming MP3 server in
|
|||
|
Chapters 27, 28, and 29. The other major bit of infrastructure you'll
|
|||
|
need is a way to write server-side Web software, the topic of the
|
|||
|
next chapter.
|
|||
|
</P><HR/><DIV CLASS="notes"><P><SUP>1</SUP><I>Ripping</I> is the process by which a
|
|||
|
song on an audio CD is converted to an MP3 file on your hard drive.
|
|||
|
These days most ripping software also automatically retrieves
|
|||
|
information about the songs being ripped from online databases such
|
|||
|
as Gracenote (n<>e the Compact Disc Database [CDDB]) or FreeDB, which
|
|||
|
it then embeds in the MP3 files as ID3 tags.</P><P><SUP>2</SUP>Almost all file systems provide the ability to overwrite
|
|||
|
existing bytes of a file, but few, if any, provide a way to add or
|
|||
|
remove data at the beginning or middle of a file without having to
|
|||
|
rewrite the rest of the file. Since ID3 tags are typically stored at
|
|||
|
the beginning of a file, to rewrite an ID3 tag without disturbing the
|
|||
|
rest of the file you must replace the old tag with a new tag of
|
|||
|
exactly the same length. By writing ID3 tags with a certain amount of
|
|||
|
padding, you have a better chance of being able to do so--if the new
|
|||
|
tag has more data than the original tag, you use less padding, and if
|
|||
|
it's shorter, you use more.</P><P><SUP>3</SUP>The frame data following the ID3 header
|
|||
|
could also potentially contain the illegal sequence. That's prevented
|
|||
|
using a different scheme that's turned on via one of the flags in the
|
|||
|
tag header. The code in this chapter doesn't account for the
|
|||
|
possibility that this flag might be set; in practice it's rarely
|
|||
|
used.</P><P><SUP>4</SUP>In ID3v2.4, UCS-2
|
|||
|
is replaced by the virtually identical UTF-16, and UTF-16BE and UTF-8
|
|||
|
are added as additional encodings.</P><P><SUP>5</SUP>The 2.4 version of the
|
|||
|
ID3 format also supports placing a footer at the end of a tag, which
|
|||
|
makes it easier to find a tag appended to the end of a file.</P><P><SUP>6</SUP>Character streams support two functions,
|
|||
|
<CODE><B>PEEK-CHAR</B></CODE> and <CODE><B>UNREAD-CHAR</B></CODE>, either of which would be a
|
|||
|
perfect solution to this problem, but binary streams support no
|
|||
|
equivalent functions.</P><P><SUP>7</SUP>If a tag had an
|
|||
|
extended header, you could use this value to determine where the
|
|||
|
frame data should end. However, if the extended header isn't used,
|
|||
|
you'd have to use the old algorithm anyway, so it's not worth adding
|
|||
|
code to do it another way.</P><P><SUP>8</SUP>These flags, in addition to controlling whether the
|
|||
|
optional fields are included, can affect the parsing of the rest of
|
|||
|
the tag. In particular, if the seventh bit of the flags is set, then
|
|||
|
the actual frame data is compressed using the zlib algorithm, and if
|
|||
|
the sixth bit is set, the data is encrypted. In practice these
|
|||
|
options are rarely, if ever, used, so you can get away with ignoring
|
|||
|
them for now. But that would be an area you'd have to address to make
|
|||
|
this a production-quality ID3 library. One simple half solution would
|
|||
|
be to change <CODE>find-frame-class</CODE> to accept a second argument and
|
|||
|
pass it the flags; if the frame is compressed or encrypted, you could
|
|||
|
instantiate a generic frame to hold the data.</P><P><SUP>9</SUP>Ensuring that kind of interfield consistency would be a
|
|||
|
fine application for <CODE>:after</CODE> methods on the accessor generic
|
|||
|
functions. For instance, you could define this <CODE>:after</CODE> method
|
|||
|
to keep <CODE>size</CODE> in sync with the <CODE>information</CODE> string:</P><PRE>(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))))</PRE></DIV></BODY></HTML>
|