Книга: Practical Common Lisp
Supporting Multiple Versions of ID3
Supporting Multiple Versions of ID3
Currently, id3-tag
is defined using define-binary-class
, but if you want to support multiple versions of ID3, it makes more sense to use a define-tagged-binary-class
that dispatches on the major-version
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:
(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))))
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.
Since the new id3-tag
class is based on the one you originally wrote to represent version 2.2 tags, it's not surprising that the new id3v2.2-tag
class is trivial, inheriting most of its slots from the new id3-tag
class and adding the one missing slot, frames
. Because version 2.2 and version 2.3 tags use different frame formats, you'll have to change the id3-frames
type to be parameterized with the type of frame to read. For now, assume you'll do that and add a :frame-type
argument to the id3-frames
type descriptor like this:
(define-binary-class id3v2.2-tag (id3-tag)
((frames (id3-frames :tag-size size :frame-type 'id3v2.2-frame))))
The id3v2.3-tag
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 flags
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.[277] 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.
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.
(define-binary-type optional (type if)
(:reader (in)
(when if (read-value type in)))
(:writer (out value)
(when if (write-value type out value))))
Using if
as the parameter name looks a bit strange in that code, but it makes the optional
type descriptors quite readable. For instance, here's the definition of id3v2.3-tag
using optional
slots:
(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))))
where extended-p
and crc-p
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 LOGBITP
, another bit-twiddling function. It takes an index and an integer and returns true if the specified bit is set in the integer.
(defun extended-p (flags) (logbitp 6 flags))
(defun crc-p (flags extra-flags)
(and (extended-p flags) (logbitp 15 extra-flags)))
As in the version 2.2 tag class, the frames slot is defined to be of type id3-frames
, passing the name of the frame type as a parameter. You do, however, need to make a few small changes to id3-frames
and read-frame
to support the extra frame-type
parameter.
(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)))
The changes are in the calls to read-frame
and write-value
, where you need to pass the frame-type
argument and, in computing the size of the frame, where you need to use a function frame-header-size
instead of the literal value 6
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:
(defgeneric frame-header-size (frame))
You'll define the necessary methods on that generic function in the next section after you define the new frame classes.
- Structure of an ID3v2 Tag
- Defining a Package
- Integer Types
- String Types
- ID3 Tag Header
- ID3 Frames
- Detecting Tag Padding
- Supporting Multiple Versions of ID3
- Versioned Frame Base Classes
- Versioned Concrete Frame Classes
- What Frames Do You Actually Need?
- Text Information Frames
- Comment Frames
- Extracting Information from an ID3 Tag
- 25. Practical: An ID3 Parser
- An Example of Conversions in Action
- Perl Versions
- Multiple Inheritance
- Multiple Terminals
- Kernel Versions
- 4.1.1. Kernel Versions
- Multiple Associative Container
- 15.4. Debugging Multiple Tasks
- 15.4.1. Debugging Multiple Processes
- PROJECT 6.6 — Two-Digit Multiplexed 7-Segment LED
- PROJECT 6.7 — Two-Digit Multiplexed 7-Segment LED Counter with Timer Interrupt