ID3v1 tags via Erlang

I’ve been taking considerable doses of disorienting but only marginally effective phenylephrine-based cold medicine. This has made it hard to think about anything, yet I did manage to type in an Erlang program to find ID3v1 tags in MP3 files. This program represents about 0.5% of the work necessary to complete my vague goal of implementing a music server in Erlang, which is a pointless effort in any sense other than Erlang practice.

Here’s the routine that pulls a tag from a file:

load_id3v1(FileName) ->
  case file:open(FileName, [read, binary]) of
    {ok, Handle} ->
      Rv = case file:pread(Handle, {eof, -128}, 128) of
        eof -> missing;
        {ok, <<$T, $A, $G, ID3v1/binary>>} -> ID3v1;
        X -> {garbled, X}
      end,
      file:close(Handle),
      Rv;
    Whatever -> Whatever
  end.

All that does is read the last 128 bytes of the file and look for the ID3 “TAG” marker in the first three bytes. If it finds that, then it returns (as a binary) the remainder. The ability to work with what amounts to blocks of raw memory in an idiomatic fashion is one of the most practical aspects of Erlang. The syntax is a little painful.

The main matching expression in the function does its work with a single match to a binary pattern. The pattern lists out the three characters of the tag – in Erlang, the notation “$c” means the character code for character ‘c’. (Erlang seems a little quaint with respect to character encodings.) The default semantics of an integer value in a binary pattern like that is that they signify values to match in single bytes. (I could have written that “TAG” instead of $T, $A, $G, by the way.)

The tag contents are matched as whatever is sitting there in the 128-byte block after the tag. The unbound variable name “ID3v1” will be bound to that 125-byte region of memory, and its type will be binary. Of course, that match will only work when the last 128 bytes of a file actually does begin with “TAG”. If not, that matching expression would fail, and the “case” expression would fall through to the “X” pattern, which returns a lame error indicator.

That’s all pretty elementary stuff, but I notice that the binary data type doesn’t show up too often in Erlang blog posts I see.

To use that function for real, what I’d do in my fantasy Erlang server is build a couple of tables. One table would contain the actual filename, plus the tag attributes, plus probably a numeric key to make it easier to communicate the file identity back and forth to web clients. In a current Java implementation, I’ve found that the issue of maintaining the integrity of interesting file names – think Sigur Rós tracks – as they go from the file system, into Java (Unicode), out to the browser, and back, to be a real headache, even requiring explicitly adding ISO-8259-1 character encoding to my Ubuntu installation. The Java file handling libraries, in particular the code to return the contents of a directory, work in the C domain, so they can’t handle simply-encoded filenames without the platform being made to understand. Or something like that. The point is that nice identifiers would be easier to handle as the medium of exchange in a web app.

The second table would capture hierarchical grouping relationships, but I haven’t thought that through much.

To continue looking at interesting things one can do with binary stuff in Erlang, consider what would have to be done to use some of the ID3 attributes. The tag contents are broken up into 30-byte regions for artist name, album, track, and a comment. (Discussion of how pathetically lame the tag architecture is are irrelevant here.) Those are padded with null bytes, normally. Well if we want to turn those into Erlang strings, we need to avoid the nulls – just calling “binary_to_list” on one of those 30-byte blocks would give us a “string” with a bunch of nulls at the end.

We therefore need a “de_null” routine to get rid of those zero bytes:

de_null(B) -> de_null(B, []).
de_null(<<>>, RS) -> lists:reverse(RS);
de_null(<<0, _/binary>>, RS) -> lists:reverse(RS);
de_null(<<C:8/integer, Rest/binary>>, RS) -> de_null(Rest, [C | RS]).

That’s written in a pretty conventional Erlang tail-recursive loop. The routine checks the first byte of its binary argument, and when it’s a zero it returns the reverse of the list it’s accumulated (backwards) from all the previous bytes. This isn’t bad, as those bindings to successive values for that “Rest” variable don’t involve copying anything – it’s just moving the pointer through the binary. However, we could do this another way:

de_null(B) -> de_null(B, 0).
de_null(Binary, Offset) ->
  case Binary of
    <<Stuff:Offset/binary, 0, _/binary>> -> binary_to_list(Stuff);
    <<Stuff:Offset/binary, _:0/binary>> -> binary_to_list(Stuff);
    _ -> de_null(Binary, Offset + 1)
  end.


In this version, instead of accumulating a list, we’ll just keep a byte counter through the iterations. Each iteration checks to see whether one of two things is true: that there’s a zero byte at the given offset, or that we’ve run out of binary. It does this by using the counter as the size of the “head” of the binary block, with the notation “Stuff:Offset/binary”. That says that the variable “Stuff” should be bound to the binary subset of the block starting at the beginning and proceeding for “Offset” bytes.

In this case there’s no practical difference. Sometimes, however, the indexing approach can be a much more efficient alternative to using lists. Appending to the end of a list is expensive, while scooting an index forward by a byte is essentially free.

8,185 Responses to “ID3v1 tags via Erlang”