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.
Versioned Frame Base Classes
Where before you defined a single base class for all frames, you'll now have two classes, id3v2.2-frame and id3v2.3-frame. The id3v2.2-frame class will be essentially the same as the original id3-frame class.
(define-tagged-binary-class id3v2.2-frame ()
((id (frame-id :length 3))
(size u3))
(:dispatch (find-frame-class id)))
The id3v2.3-frame, 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.[278] With those changes in mind, you can define the version 2.3 frame base class, along with some helper functions, like this:
(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))
With these two classes defined, you can now implement the methods on the generic function frame-header-size.
(defmethod frame-header-size ((frame id3v2.2-frame)) 6)
(defmethod frame-header-size ((frame id3v2.3-frame)) 10)
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 size.
Versioned Concrete Frame Classes
In the original definition, generic-frame subclassed id3-frame. But now id3-frame has been replaced with the two version-specific base classes, id3v2.2-frame and id3v2.3-frame. So, you need to define two new versions of generic-frame, one for each base class. One way to define this classes would be like this:
(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))))
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.
Another approach, and the one you should actually use, is to define a class generic-frame as a mixin: 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 generic-frame doesn't extend either of the frame base classes, then you can't refer to the size slot in its definition. Instead, you must use the current-binary-object 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 size. 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 data-bytes with methods that do the right thing for both version 2.2 and version 2.3 frames.
(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))))
Then you can define concrete classes that extend one of the version-specific base classes and generic-frame to define version-specific generic frame classes.
(define-binary-class generic-frame-v2.2 (id3v2.2-frame generic-frame) ())
(define-binary-class generic-frame-v2.3 (id3v2.3-frame generic-frame) ())
With these classes defined, you can redefine the find-frame-class function to return the right versioned class based on the length of the identifier.
(defun find-frame-class (id)
(ecase (length id)
(3 'generic-frame-v2.2)
(4 'generic-frame-v2.3)))
What Frames Do You Actually Need?
278
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 find-frame-class 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.