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.
To handle this, you can define a binary type, id3-frames, 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 read-value directly in id3-frames :reader, you should use a function read-frame, which you'll define to return NIL when it detects padding, otherwise returning an id3-frame object read using read-value. Assuming you define read-frame 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 id3-frames binary type like this:
(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)))))
You can use this type to add a frames slot to id3-tag.
(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))))
Now all that remains is to implement read-frame. This is a bit tricky since the code that actually reads bytes from the stream is several layers down from read-frame.
What you'd really like to do in read-frame is read one byte and return NIL if it's a null and otherwise read a frame with read-value. Unfortunately, if you read the byte in read-frame, then it won't be available to be read by read-value.[276]
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; read-frame 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.
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.
(define-condition in-padding () ())
Next you need to define a binary type whose :reader reads a given number of bytes, first reading a single byte and signaling an in-padding condition if the byte is null and otherwise reading the remaining bytes as an iso-8859-1-string and combining it with the first byte read.
(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)))
If you redefine id3-frame to make the type of its id slot frame- id instead of iso-8859-1-string, the condition will be signaled whenever id3- frame's read-value method reads a null byte instead of the beginning of a frame.
(define-tagged-binary-class id3-frame ()
((id (frame-id :length 3))
(size u3))
(:dispatch (find-frame-class id)))
Now all read-frame has to do is wrap a call to read-value in a HANDLER-CASE that handles the in-padding condition by returning NIL.
(defun read-frame (in)
(handler-case (read-value 'id3-frame in)
(in-padding () nil)))
With read-frame defined, you can now read a complete version 2.2 ID3 tag, representing frames with instances of generic-frame. 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.
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)
