MIDI file support

By popular demand, two simple pieces of code have been added Field to support the writing and reading of MIDI files. MIDI’s complete and utter adequacy as a protocol combined with its first mover advantage (it was standardized in the early 80s) has meant that, despite its obvious problems, it is the interchange format for note-level descriptions of music that is read and written everywhere where note-level descriptions of music might be useful.

Saving

How to write some notes to a MIDI file? Here’s the code:

// secret invocation to import SaveMidi into your work
var SaveMidi = Java.type("trace.sound.SaveMidi")

var saver = new SaveMidi()
for(var i=0;i<60;i++)
{
    // add a note at time 'i' (in seconds)
    // of pitch 80-i (see below)
    // with 'velocity' of 0.1+i/100.0
    // with duration 0.1+i/30.0 seconds
	saver.note(i, 80-i, 0.1+i/100.0, 0.1+i/30.0)	
}

// actually write the midi file
// change this, unless you are called 'marc'
saver.save("/Users/marc/Desktop/something.mid")

That’s really all you need to know. The same information in a different form:

saver.note(time, pitch, velocity, duration) — adds a note to a SaveMidi object. The note starts at time (in seconds), is of pitch pitch (from 0-127), velocity (from 0.0 to 1.0) and lasts duration seconds long. Think of velocity as a keyboard-centric way of saying ‘loudness’. Pitch probably requires a little explanation. pitch=69 is, by convention, the pitch A that traditionally sounds at 440Hz. pitch=68 is then the A♭ immediately below that, pitch=70 is the B immediately above. That’s all the music theory I’m going to give here. There’s also a saver.note(time, pitch, velocity, duration, channel) where channel can go from 0-15 (inclusive) if you know what channels are in MIDI files.

However, the reality is that it’s up to the program that interprets this MIDI file as to what the notes (and velocities) mean (just as it’s up to the synthesizer you connect your keyboard to as to what sounds come out when you press the keys). If we just drag this .mid file into GarageBand we’ll get those notes on a “Steinway Grand”:

Take a moment to convince yourself that that shape (with pitch running from low to high / bottom to top, and time running left to right) is what you’d expect from the code above.

Two things to note:

  1. MIDI files don’t typically have much in the way of temporal resolution (in much the same way as music notation doesn’t have much in the way of temporal resolution. There are fixes for this, but they more application specific than I’d like. These MIDI files aren’t a whole lot more accurate than, say, 24 frames-per-second cinema.
  2. MIDI files, while polyphonic, can’t encode the same note playing twice at the same time. The first time a pitch ‘stops’ both ‘notes’ will stop. This comes from it’s legacy as a keyboard oriented format, where you can’t press a key that’s already being held down.

Realtime output (Mac Only)

Code for sending notes out of Field into any app that’s listening for MIDI works in very similar ways. First make sure you have turned on “IAC” midi (this stands for Inter-Application communication) in Audio/Midi setup. This app can be found in /Applications/Utilities. Window->Show Midi Studio shows:

Double click on the IAC Driver and check “device is online” before starting Field.

Then:

var OutMidi = Java.type("trace.sound.OutMidi")
var o = new OutMidi()
o.note(30, 1, 4, 0)

… sends a note. output.note(pitch, velocity, duration_in_seconds, channel) — with velocity 0-1, pitch 0-127 and channel 0-15. Note that GarageBand ignores ‘channel’ and only (seems?) to be willing to play midi if the first track is selected. Consider exploring Logic Pro…

Loading

Loading MIDI files is just as easy. Just because we can, let’s draw that GarageBand screen in Field:

var LoadMidi = Java.type("trace.sound.LoadMidi")

var loaded = new LoadMidi("/Users/marc/Desktop/something.mid")

var f = new FLine()

// loaded.notes is a list of notes from the midi file
for(var note of loaded.notes)
{
    // a note has '.time', '.duration', '.pitch' and '.velocity'
    // in the same units as the SaveMidi class
	f.moveTo(note.time*24, 127-note.pitch)
	f.lineTo((note.time+note.duration)*24, 127-note.pitch)
}

_.lines.f = f

Yields:

Being able to iterate over the whole MIDI file gives you access to everything. But it’s not always what you want, if you want to make an animation that responds to what’s happening in a moment of time. Three things help:

  1. loaded.notesAtTime(4.5) returns all (if any) notes that are sounding at time 4.5seconds.
  2. loaded.notesBetweenTimes(4.5, 4.6) returns all (if any) notes that are sounding at any point between time 4.5 and 4.6 seconds
  3. loaded.read(4.5) returns all (if any) notes that are sounding between the last time you called loaded.read( ... ) and 4.5 seconds. This useful for incrementally reading through a MIDI file (perhaps, say, based on _t()) and making sure you don’t miss any notes. It only works, though, when reading forwards in time (you won’t get anything if you skip backwards).