- Originally published April 5, 2014.
Part 4 of a Series
Parsing a MIDI file track for its Events.
Sample MIDI File
I’ll be using the same simple piece of music as I did in part 3. It is Bach’s "Two-Part Invention No. 1". Here is a PDF of the sheet music and a MIDI file for the piece.
MIDI Events
MIDI is a streaming protocol. It was developed without files in mind at first. The model was to send small messages from a MIDI device, like a keyboard, to a computer. To make the messages as small and fast as possible the MIDI protocol is in a binary format. The complete MIDI specification is here:
A MIDI track is composed of MIDI Events. Once we have access to a track from a MIDI file we have to loop through the track and extract all the events. All events have the same structure.
MIDI Event | ||||
---|---|---|---|---|
Delta Time variable length: 1–4 bytes | Event Type 4 bits | MIDI Channel 4 bits | Parameter 1 1 byte | Parameter 2 1 byte |
- At the beginning of the MIDI Event is a Delta Timestamp. The first events all have a timestamp of 0.0. The timestamp is not in real time, it is all relative to notes in music. The default time for a quarter note is 1.0.
- The Event Type indicates one of seven of top level channel events.
Event Type (Value) | MIDI Channel | Parameter 1 | Parameter 2 |
---|---|---|---|
Note Off (0x8 – 8) | 0–15 | Note Number: 0–127 | Velocity: 0–127 |
Note On (0x9 – 9) | 0–15 | Note Number: 0–127 | Velocity: 0–127 |
Note Aftertouch (0xA – 10) | 0–15 | Note Number: 0–127 | Aftertouch Amount: 0–127 |
Controller (0xB – 11) | 0–15 | Controller Type: 0–127 | Controller Value: 0–127 |
Program Change (0xC – 12) | 0–15 | Program Number: 0–127 | Not Used |
Channel Aftertouch (0xD – 13) | 0–15 | Aftertouch Amount: 0–127 | Not Used |
Pitch Bend (0xE – 14) | 0–15 | Pitch Value (LSB): 0–127 | Pitch Value (MSB): 0–127 |
- The MIDI Channel usually corresponds to an instrument or stave in the music score. For example, in a piano score the treble and bass clefs will each be in a MIDI Channel.
- The two Parameters will contain data about the event. The most common event will be Note On and Parameter 1 is the note and Parameter 2 will be the velocity or how hard the key was pressed.
MIDI Note Events Example
Next we’ll look at some notes in a score and how they are represented in a MIDI file. We’ll focus on the highlighted notes. Here is the MIDI file for this score:

- The first note in the Bass Trombone stave is in our MIDI file like this:
00 94 39 7f
- The
00
is the relative timestamp. It is 0 as it is the first note in the piece. - The
9
in the94
is the MIDI Event Type. This is a Note On event. - The
4
in the94
is the MIDI Channel. They start with 0 so this is the fifth channel. - The
39
is the hexadecimal value for the number 57. This is the note A3, assuming that middle C is C4. - The
7f
is the hexadecimal value for the number 127. This is the velocity of the note. This is the maximum value and indicates that the note should be played at full volume.
- The Trombone III part:
00 93 32 7f
- The timestamp and velocity are the same as the above note.
- The
9
in the93
is a Note On event. - The
3
in the93
is channel 3 or the fourth channel. - The
32
is decimal 50 and is the note D3.
- The Trombone II part:
00 92 42 7f
- The
9
in the92
is a Note On event. - The
2
in the92
is channel 2 or the third channel. - The
42
is decimal 66 and is the note F#4.
- The Trombone I Part:
00 91 3e 7f
- The
9
in the91
is a Note On event. - The
2
in the91
is channel 2 or the second channel. - The
3e
is decimal 62 which is D4.
Each of these MIDI Note Events is just 4 bytes long and represent one note in a score. This is a very small amount of data and it transfers from one device to another very efficiently.
Parsing MIDI Events
Now let’s parse notes with Core MIDI! In the example from Part 3 of this series we had all the code in the main()
function of the demo project. The first thing to do in this part is to move the code for parsing the Tempo Track into its own function.
The new main()
function
int main(int argc, const char * argv[])
{
// Load the MIDI File
MusicSequence sequence;
NewMusicSequence(&sequence);
NSURL *midiFileURL
= [NSURL fileURLWithPath:@"/Users/<user>/Desktop/bach-invention-01.mid"];
MusicSequenceFileLoad(sequence, (__bridge CFURLRef)midiFileURL, 0, 0);
parseTempoTrack(sequence);
return 0;
}
The code in the main()
function has a small change. The call to MusicSequenceFileLoad()
no longer has the kMusicSequenceLoadSMF_ChannelsToTracks
flag in it. This flag causes all the meta data for channels, like their names, to be moved to the tempo track. We want to know where the these events belong so we use the default flag of 0
.
Next we’ll add another function for parsing the rest of the tracks which contain all the music.
Parse MIDI Events
void parseMIDIEventTracks(MusicSequence sequence)
{
...
}
The first thing we need is how many tracks there are in the file. This count will not include the Tempo Track.
Get the number of tracks
UInt32 trackCount;
MusicSequenceGetTrackCount(sequence, &trackCount);
We need a variable to hold the tracks as we loop through the file.
Local track variable
MusicTrack track = NULL;
Now we can loop through the tracks in the MIDI file. More details after the code snippet.
The track loop
for (UInt32 index = 0; index < trackCount; index++) {
MusicSequenceGetIndTrack(sequence, index, &track);
MusicEventIterator iterator = NULL;
NewMusicEventIterator(track, &iterator);
parseTrackForMIDIEvents(iterator);
}
- Line 2: The
MusicSequenceGetIndTrack()
function retrieves a track with the given index. - Line 3: A
MusicEventIterator
, and the functions that work with it, know how to iterate through a track. Since the MIDI events can be varying lengths we can’t use normal array indexing. This line creates the variable. - Line 4: This line initializes the iterator with the current track.
- Line 5: Let’s use a new function to parse all the events in each track.
Here’s the completed function.
parseMIDIEventTracks()
void parseMIDIEventTracks(MusicSequence sequence)
{
UInt32 trackCount;
MusicSequenceGetTrackCount(sequence, &trackCount);
MusicTrack track = NULL;
for (UInt32 index = 0; index < trackCount; index++) {
MusicSequenceGetIndTrack(sequence, index, &track);
MusicEventIterator iterator = NULL;
NewMusicEventIterator(track, &iterator);
parseTrackForMIDIEvents(iterator);
}
}
Parsing a Track for MIDI Events
Now we finally get to see some notes from the MIDI file. There are lots of different kinds of MIDI events in these tracks. To keep things simple we’ll just retrieve the music notes in this sample. This will be done in the new function parseTrackForMIDIEvents()
.
parseTrackForMIDIEvents()
void parseTrackForMIDIEvents(MusicEventIterator iterator)
{
}
To start the function we’ll need the same housekeeping variables we used parsing the tempo track. Let’s look at them with a little more detail after the snippet.
Housekeeping Variables
MusicTimeStamp timestamp = 0;
MusicEventType eventType = 0;
const void *eventData = NULL;
UInt32 eventDataSize = 0;
Boolean hasNext = YES;
- Line 1:
MusicTimeStamp
is declared astypedef Float64 MusicTimeStamp;
, it’s just a float. - Line 2:
MusicEventType
is an enum that is listed in Part 3 of this series. - Line 3:
eventData
will be a pointer to the data for the MIDI Event. - Line 4:
eventDataSize
lets us know how big the data is being pointed to byeventData
. - Line 5:
hasNext
is our indicator that we have another track to parse.
Next we have to test if we have a first event. We do this in case there are no events in this track.
Test for First Event
MusicEventIteratorHasCurrentEvent(iterator, &hasNext);
We now start the loop that will give us each event.
The MIDI Event Loop
while (hasNext) {
MusicEventIteratorGetEventInfo(iterator, ×tamp, &eventType, &eventData, &eventDataSize);
}
- The
MusicEventIteratorGetEventInfo()
function gives us each event. The hard work of knowing how long each event is has been taken care of for us by the Core MIDI team. - The
iterator
is the only input to the function. All the other arguments will be filled in by the function.
Next we determine if this is an event type we want. We’ll just look at one type for this demo, the MIDI Note Message. We do that with this if statement.
Determine Event Type
if (eventType == kMusicEventType_MIDINoteMessage) {
}
If the event is a music note we need to declare a variable that’s a pointer to a MIDINoteMessage
and cast the event data as in line 2:
Cast the Data
if (eventType == kMusicEventType_MIDINoteMessage) {
MIDINoteMessage *noteMessage = (MIDINoteMessage*)eventData;
}
The MIDINoteMessage
is a struct with the information about the music note. We finally get what we were looking for.
MIDINoteMessage
typedef struct MIDINoteMessage {
UInt8 channel;
UInt8 note;
UInt8 velocity;
UInt8 releaseVelocity;
Float32 duration;
} MIDINoteMessage;
Next, we’ll just output the data for now. That’s a good start and is plenty for a demo tutorial like this.
Display the Notes
printf("Note - timestamp: %6.3f, channel: %d, note: %d, velocity: %d, release velocity: %d, duration: %f\n",
timestamp,
noteMessage->channel,
noteMessage->note,
noteMessage->velocity,
noteMessage->releaseVelocity,
noteMessage->duration
);
After displaying a note we need to try to get the next one and check if there is one.
Retrieve the next event
MusicEventIteratorNextEvent(iterator);
MusicEventIteratorHasCurrentEvent(iterator, &hasNext);
Here’s the full function.
parseTrackForMIDIEvents()
void parseTrackForMIDIEvents(MusicEventIterator iterator)
{
MusicTimeStamp timestamp = 0;
MusicEventType eventType = 0;
const void *eventData = NULL;
UInt32 eventDataSize = 0;
Boolean hasNext = YES;
MusicEventIteratorHasCurrentEvent(iterator, &hasNext);
while (hasNext) {
MusicEventIteratorGetEventInfo(iterator, ×tamp, &eventType, &eventData, &eventDataSize);
if (eventType == kMusicEventType_MIDINoteMessage) {
MIDINoteMessage *noteMessage = (MIDINoteMessage*)eventData;
printf("Note - timestamp: %6.3f, channel: %d, note: %d, velocity: %d, release velocity: %d, duration: %f\n",
timestamp,
noteMessage->channel,
noteMessage->note,
noteMessage->velocity,
noteMessage->releaseVelocity,
noteMessage->duration
);
}
MusicEventIteratorNextEvent(iterator);
MusicEventIteratorHasCurrentEvent(iterator, &hasNext);
}
}
When we run this on the Bach Invention file we should see lots of output. Here are the notes from the first measure for the first channel. This is the treble clef.
Treble Clef
Note - timestamp: 0.250, channel: 0, note: 60, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 0.500, channel: 0, note: 62, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 0.750, channel: 0, note: 64, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 1.000, channel: 0, note: 65, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 1.250, channel: 0, note: 62, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 1.500, channel: 0, note: 64, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 1.750, channel: 0, note: 60, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 2.000, channel: 0, note: 67, velocity: 90, release velocity: 0, duration: 0.500
Note - timestamp: 2.500, channel: 0, note: 72, velocity: 90, release velocity: 0, duration: 0.500
Note - timestamp: 3.000, channel: 0, note: 71, velocity: 90, release velocity: 0, duration: 0.500
Note - timestamp: 3.500, channel: 0, note: 72, velocity: 90, release velocity: 0, duration: 0.500
Here is the output for the first measure of the bass clef.
Bass Clef
Note - timestamp: 2.250, channel: 1, note: 48, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 2.500, channel: 1, note: 50, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 2.750, channel: 1, note: 52, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 3.000, channel: 1, note: 53, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 3.250, channel: 1, note: 50, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 3.500, channel: 1, note: 52, velocity: 90, release velocity: 0, duration: 0.250
Note - timestamp: 3.750, channel: 1, note: 48, velocity: 90, release velocity: 0, duration: 0.250
Full Source
Here’s the file that contains all this code.
Wrap Up
I think I can hear some questions forming.
- How did I know those events were from the first measure?
- What does a duration of 0.250 or 0.5 mean?
- What about all the other events?
- How do I just play a MIDI file?
We’ll get there, little by little.
Just keep coding,
-Eric