ID3 Tags (#136)

This quiz was another idea I got out of the Erlang book. The author
uses a
similar example to show how smooth processing binary data in Erlang can
be. I’m
happy to say that I found the submitted Ruby solutions to be equally
smooth, if
not more so.

The secret to binary parsing in Ruby is generally the String.unpack()
method and
the majority of the solutions capitalized on this technique.
Technically, ID3
tags are mainly in plain text, with some null characters thrown in.
Still, I
think it’s a good idea to get into the unpack() mindset anytime you
slicing up binary data.

I want to take a look at Eugene K.'s code below. It’s a pretty
usage of unpack() to parse some data. It also includes a nicety when
the file that I’m ashamed to admit I didn’t think of. Let’s start with

def fileTail (file, offset),IO::SEEK_END)

In my own code, I read the whole file into memory and indexed out the
last 128
bytes. That’s almost always the wrong approach and Eugene shows the
strategy above. This code just opens the file, seek()s to offset bytes
the end, and read()s the needed data. That scales much better when the
sizes are significant.

As a quick aside, file_tail() would probably be a more Rubyish method

The code now builds a data structure class to hold the tag details. It
like this:

class ID3Tag
GENRES=[“Blues”,“Classic Rock”,“Country”,…,“Dance Hall”]
attr_reader :title, :artist, :album, :year, :comment, :genre, :track

# ...

You can see that this class is mainly just a data structure that defines
for all of the elements in a tag. I’ve trimmed the GENRES listing here,
but the
code included the full set.

I will say that some found more clever means to load the GENRES Array.
people did fancy heredoc manipulations, but the most clever pulled the
list out
of the quiz document using open-uri and hpricot. That was especially
wise this
time since I made so many mistakes in the quiz description.

We’re now ready for the actual parsing code:

# ...

def initialize fname
    fileTail(fname,128).unpack "A3A30A30A30A4A30C"
  raise "No ID3 Info" if tag!='TAG'
  s_com,flag,[email protected] "A28CC"
  if flag==0 and track!=0
  @genre="Unknown" if  !@genre


As you can see, the majority of the work is done on the first line with
a single
call to unpack(). The template fed to unpack() is the key to the whole
An “A” in the unpack() template instructs it to extract a String,
removing any
trailing spaces or null characters. By default the String is just one
long, but you can provide a number after the “A” to increase that count.
only other character used in the template is a “C” which is used to
extract one
character as an Integer. The unpack() call returns an Array which
Eugene just
mass-assigns to the relevant variables.

The rest is simple. The code checks the first chunk for the identifying
String and throws an error if it’s not there. Then another call to
with a template much like the first, pulls the track field out of the
The if statement makes sure that assignment only happens when it is
The final two lines are just a longhand form of:

@genre = GENRES[@genre] || “Unknown”

With all of the fields stored away in the proper variables, reader calls
can be
used to extract as needed. Eugene’s actual application code just punted
on that
point though:


My thanks to all who have helped me with my Erlang comparisons these
last two
weeks. I promise, we’re on to new topics now.

In fact, tomorrow we will tackle an interesting subproblem from this
year’s ICFP