Integer Types
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.
ID3 uses unsigned integers encoded in one, two, three, and four bytes. If you first write a general unsigned-integer
binary type that takes the number of bytes to read as an argument, you can then use the short form of define-binary-type
to define the specific types. The general unsigned-integer
type looks like this:
(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))))
Now you can use the short form of define-binary-type
to define one type for each size of integer used in the ID3 format like this:
(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))
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 #xff
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 unsigned-integer
, 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.3
Thus, it can be read and written a lot like an unsigned-integer
except the size of the byte specifier you pass to **LDB**
should be seven rather than eight. This similarity suggests that if you add a parameter, bits-per-byte
, to the existing unsigned-integer
binary type, you could then define a new type, id3-tag-size
, using a short-form define-binary-type
. The new version of unsigned-integer
is just like the old version except with bits-per-byte
used everywhere the old version hardwired the number eight. It looks like this:
(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))))
The definition of id3-tag-size
is then trivial.
(define-binary-type id3-tag-size () (unsigned-integer :bytes 4 :bits-per-byte 7))
You’ll also have to change the definitions of u1
through u4
to specify eight bits per byte like this:
(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))