Eric's Homemade Stuff

An infrequent blog of stuff I make, including music.

MIDI Events

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 ChannelParameter 1Parameter 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: air-tromb.mid

Score Fragment

  • 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 the 94 is the MIDI Event Type. This is a Note On event.
    • The 4 in the 94 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 the 93 is a Note On event.
    • The 3 in the 93 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 the 92 is a Note On event.
    • The 2 in the 92 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 the 91 is a Note On event.
    • The 2 in the 91 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
1
2
3
4
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
1
2
UInt32 trackCount;
MusicSequenceGetTrackCount(sequence, &trackCount);

We need a variable to hold the tracks as we loop through the file.

Local track variable
1
MusicTrack track = NULL;

Now we can loop through the tracks in the MIDI file. More details after the code snippet.

The track loop
1
2
3
4
5
6
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()
1
2
3
4
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
1
2
3
4
5
MusicTimeStamp timestamp = 0;
MusicEventType eventType = 0;
const void *eventData = NULL;
UInt32 eventDataSize = 0;
Boolean hasNext = YES;
  • Line 1: MusicTimeStamp is declared as typedef 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 by eventData.
  • 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
1
MusicEventIteratorHasCurrentEvent(iterator, &hasNext);

We now start the loop that will give us each event.

The MIDI Event Loop
1
2
3
while (hasNext) {
    MusicEventIteratorGetEventInfo(iterator, &timestamp, &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
1
2
3
    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
1
2
3
    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
1
2
3
4
5
6
7
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
1
2
3
4
5
6
7
8
  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
1
2
    MusicEventIteratorNextEvent(iterator);
    MusicEventIteratorHasCurrentEvent(iterator, &hasNext);

Here’s the full function.

parseTrackForMIDIEvents()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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, &timestamp, &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
1
2
3
4
5
6
7
8
9
10
11
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
1
2
3
4
5
6
7
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. – main.m

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




Comments