#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>)
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 id
s of those frames, which you can get with a simple MAPCAR
like this:
ID3V2> (mapcar #'id (frames *id3*))
('TT2' 'TP1' 'TAL' 'TRK' 'TPA' 'TYE' 'TCO' 'TEN' 'COM' 'COM' 'COM')
If you look up these identifiers in the ID3v2.2 spec, you'll discover that all the frames with identifiers starting with
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 MAPCAR
expression with a call to read-id3
and wraps the whole thing in a DELETE- DUPLICATES
to keep things tidy. You'll have to use a :test
argument of #'string=
to DELETE-DUPLICATES
to specify that you want two elements considered the same if they're the same string.
(defun frame-types (file)
(delete-duplicates (mapcar #'id (frames (read-id3 file))) :test #'string=))
This should give the same answer except with only one of each identifier when passed the same filename.
ID3V2> (frame-types '/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3')
('TT2' 'TP1' 'TAL' 'TRK' 'TPA' 'TYE' 'TCO' 'TEN' 'COM')
Then you can use Chapter 15's walk-directory
function along with mp3-p
to find every MP3 file under a directory and combine the results of calling frame-types
on each file. Recall that NUNION
is the recycling version of the UNION
function; since frame-types
makes a new list for each file, this is safe.
(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))
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:
ID3V2> (frame-types-in-dir '/usr2/mp3/')
('TCON' 'COMM' 'TRCK' 'TIT2' 'TPE1' 'TALB' 'TCP' 'TT2' 'TP1' 'TCM'
'TAL' 'TRK' 'TPA' 'TYE' 'TCO' 'TEN' 'COM')
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.
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.
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, define-binary-class
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.
As long as you're defining such a type, you can also define it to take two parameters, :length
and :terminator
, 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.
(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)))
Then string-args
uses the encoding byte, the length, and the terminator to determine several of the arguments to be passed to read-value
and write-value
by the :reader
and :writer
of id3-encoded-string
. One of the length and terminator arguments to string-args
should always be NIL
.
(defun string-args (encoding length terminator)
(cond
(length
(values (non-terminated-type encoding) :length length))
(terminator
(values (terminated-type encoding) :terminator terminator))))
With those helpers, the definition of id3-encoded-string
is simple. One detail to note is that the keyword—either :length
or :terminator
—used in the call to read- value
and write-value
is just another piece of data returned by string- args
. Although keywords in arguments lists are almost always literal keywords, they don't have to be.
(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)))