An Erlang MIDI File Reader/Writer
I’ve been learning Erlang. (See Learning a New Programming Language.) As part of that process, I’ve written three programs so far: a Boids flock simulation, a simple Web application using Erlyweb, and a MIDI file reader/writer.
One point that I might not have made clearly in my previous post is the reasons that I learn a new language: to learn new ways to think about and solve coding problems, to see what’s out there that’s better than what I’m using (I try not to be a Blub programmer), to keep my brain sharp, and because it’s fun.
Coding the MIDI file library helped me learn about Erlang’s file I/O, the binary data type, and a bit more about pattern matching. (The Boids simulation has much more pattern matching goodness; more about that in another post.) The code isn’t distributed, nor does it really take advantage of many of Erlang’s strengths such as distributed process (Boids does). It does make use of the binary data type and pattern matching quite heavily.
I was also forced to think about data representation in Erlang: how would I represent a sequence, a track, and a single MIDI event? I’m not convinced that I have come up with the best representations, because I have not yet had the time to use this library for any MIDI file manipulation. When I get to that point, I fully expect that the data representation will change.
Having written this code in a few different languages before, I knew where
to start: with the easy stuff! I first defined the constants.
midi_consts.hrl
contains all of the define statements for all the
constants I might need. It was while creating this file that I learned how
to write hex numbers in Erlang. (That sounds like a small, silly thing but I
was quite annoyed for a few minutes before I figure out how to do that.)
Next, I dove into reading a MIDI file. Since I only deal with MIDI type 1
files which contain multiple tracks in a single file, that’s all I bothered
writing. I knew I’d have to read the header and one or more tracks. Erlang
code tends to use atoms, tuples, lists, and records to represent many types
of data, so I came up with a proposed data format for my sequence data. From
the comment for midifile:read/1
, which takes the path to a MIDI file and
returns a seq tuple:
After writing some code that read one byte at a time, I went back to the
“Programming with Files” chapter in
Programming Erlang by Joe
Armstrong and realized that I could slurp all of the MIDI data into memory
and randomly access it. This made my code cleaner and faster. It also made
pattern matching much easier, because I could write midifile:read_event
with many different pattern-matched arguments but the same arity. Here’s the
code that reads an event list within a track:
Let’s start at the end: event_list/3
is recursive. It builds the list of
events by reading a single event, then calling itself with the remaining
bytes to read in the track. If the remaning number of bytes is 0, the first
clause (the first two lines above) matches and an empty list is returned.
The second, longer clause starts by reading a variable length integer (more
about that below). It then reads the next three bytes of the file (again,
randomly accessing an in-memory copy of the file). Why three bytes? Because
most MIDI events are two or three bytes long. If I need fewer bytes, no harm
done. If I need more, I read more. Those three bytes are passed on to
read_event/4
, which is a function that is made up of a really long series
of clauses, each of which matches a different MIDI event.
Here are the first few read_event/4
clauses. The first three arguments are
the file, current file position, and delta time of the event. These clauses
all return an array consisting of the event tuple and the number of bytes
used by the event (excepting the length of the delta time).
The put
calls store the status and channel in the process dictionary, one
of the few places in Erlang that you can modify values. To handle running
status bytes, we need to remember the status and channel. I picked the
process dictionary as the place to store that.
/Added 2007-05-23:/ “Running status bytes” are a way to reduce the size of
MIDI files. If the status byte is the same as the previous status byte, you
can omit it. Also, as a special case a note-on value with a velocity of zero
is considered to be a note-off message. I needed code that would recognize
that the next byte was not a status byte, and use the previous status byte.
I made that an additional read_event/4
clause, like this:
We read the status and channel from the process dictionary and pass them
back to read_event/4
, which will use Erlang’s pattern matching to go back
and find the proper code for that status byte. End of additions.
?DPRINT is a macro that I defined to output when DEBUG is defined and do nothing when it isn’t:
MIDI encodes certain multi-byte integer values by “variable length encoding”
it: splitting it into seven bit chunks and outputting them with the
highest-order chunk first. The last, lowest-bit chunk has the high bit set
to zero, the earlier, higher chunks have the high bit set to one. Thus zero
is encoded as 00000000
and 129 is encoded as 10000001 00000001
. Notice
that the code in event_list/1
always reads the next four bytes; that’s
because the MIDI spec guarantees that var length numbers are at most four
bytes long. Here’s the code for read_var_len/1
, which returns a list
containing the value and the number of bytes it uses:
bsl
means “bit-shift left”.
Only after finishing the MIDI file reader code did I attack the MIDI file
writer. For that, I created an in-memory “IO list”. An IO list is a list
that consts of other IO lists, binaries, or bytes (integers between 0 and
155). The Erlang library method file:write_file/2
writes an IO list with
one call.
Here is the code that writes a MIDI var len value:
This code makes use of guard clauses: expressions that determine whether or not to execute a function clause. Note that the final clause exits. This matches Erlang’s philosophy that you should code for the normal case and let errors happen. It might have been more in the Erlang style to leave off the last case entirely, and let the program fail with a “bad match”. Perhaps I’ll remove it.
After finishing the writer, I read in a file with the reader and output the Erlang representation using the writer. The result: a few differences, but all due to decisions about running status byte values. The code isn’t perfect; it has a long way to go. I’ve learned a lot, though.
Next: a Boids simulation in Erlang.