ID3 Tag Header

With the basic primitive types done, you’re ready to switch to a high-level view and start defining binary classes to represent first the ID3 tag as a whole and then the individual frames.

If you turn first to the ID3v2.2 specification, you’ll see that the basic structure of the tag is this header:

  1. ID3/file identifier "ID3"
  2. ID3 version $02 00
  3. ID3 flags %xx000000
  4. ID3 size 4 * %0xxxxxxx

followed by frame data and padding. Since you’ve already defined binary types to read and write all the fields in the header, defining a class that can read the header of an ID3 tag is just a matter of putting them together.

  1. (define-binary-class id3-tag ()
  2. ((identifier (iso-8859-1-string :length 3))
  3. (major-version u1)
  4. (revision u1)
  5. (flags u1)
  6. (size id3-tag-size)))

If you have some MP3 files lying around, you can test this much of the code and also see what version of ID3 tags your MP3s contain. First you can write a function that reads an id3-tag, as just defined, from the beginning of a file. Be aware, however, that ID3 tags aren’t required to appear at the beginning of a file, though these days they almost always do. To find an ID3 tag elsewhere in a file, you can scan the file looking for the sequence of bytes 73, 68, 51 (in other words, the string “ID3”).5 For now you can probably get away with assuming the tags are the first thing in the file.

  1. (defun read-id3 (file)
  2. (with-open-file (in file :element-type '(unsigned-byte 8))
  3. (read-value 'id3-tag in)))

On top of this function you can build a function that takes a filename and prints the information in the tag header along with the name of the file.

  1. (defun show-tag-header (file)
  2. (with-slots (identifier major-version revision flags size) (read-id3 file)
  3. (format t "~a ~d.~d ~8,'0b ~d bytes -- ~a~%"
  4. identifier major-version revision flags size (enough-namestring file))))

It prints output that looks like this:

  1. ID3V2> (show-tag-header "/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3")
  2. ID3 2.0 00000000 2165 bytes -- Kitka/Wintersongs/02 Byla Cesta.mp3
  3. NIL

Of course, to determine what versions of ID3 are most common in your MP3 library, it’d be handier to have a function that returns a summary of all the MP3 files under a given directory. You can write one easily enough using the walk-directory function defined in Chapter 15. First define a helper function that tests whether a given filename has an mp3 extension.

  1. (defun mp3-p (file)
  2. (and
  3. (not (directory-pathname-p file))
  4. (string-equal "mp3" (pathname-type file))))

Then you can combine show-tag-header and mp3-p with walk-directory to print a summary of the ID3 header in each file under a given directory.

  1. (defun show-tag-headers (dir)
  2. (walk-directory dir #'show-tag-header :test #'mp3-p))

However, if you have a lot of MP3s, you may just want a count of how many ID3 tags of each version you have in your MP3 collection. To get that information, you might write a function like this:

  1. (defun count-versions (dir)
  2. (let ((versions (mapcar #'(lambda (x) (cons x 0)) '(2 3 4))))
  3. (flet ((count-version (file)
  4. (incf (cdr (assoc (major-version (read-id3 file)) versions)))))
  5. (walk-directory dir #'count-version :test #'mp3-p))
  6. versions))

Another function you’ll need in Chapter 29 is one that tests whether a file actually starts with an ID3 tag, which you can define like this:

  1. (defun id3-p (file)
  2. (with-open-file (in file :element-type '(unsigned-byte 8))
  3. (string= "ID3" (read-value 'iso-8859-1-string in :length 3))))