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 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.
variable length: 1–4 bytes
- 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: air-tromb.mid
- The first note in the Bass Trombone stave is in our MIDI file like this:
00 94 39 7f
00is the relative timestamp. It is 0 as it is the first note in the piece.
94is the MIDI Event Type. This is a Note On event.
94is the MIDI Channel. They start with 0 so this is the fifth channel.
39is the hexadecimal value for the number 57. This is the note A3, assuming that middle C is C4.
7fis 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.
93is a Note On event.
93is channel 3 or the fourth channel.
32is decimal 50 and is the note D3.
- The Trombone II part:
00 92 42 7f
92is a Note On event.
92is channel 2 or the third channel.
42is decimal 66 and is the note F#4.
- The Trombone I Part:
00 91 3e 7f
91is a Note On event.
91is channel 2 or the second channel.
3eis 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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
Next we’ll add another function for parsing the rest of the tracks which contain all the music.
1 2 3 4
The first thing we need is how many tracks there are in the file. This count will not include the Tempo Track.
We need a variable to hold the tracks as we loop through the file.
Now we can loop through the tracks in the MIDI file. More details after the code snippet.
1 2 3 4 5 6
- 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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
1 2 3 4
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.
1 2 3 4 5
- Line 1:
MusicTimeStampis declared as
typedef Float64 MusicTimeStamp;, it’s just a float.
- Line 2:
MusicEventTypeis an enum that is listed in Part 3 of this series.
- Line 3:
eventDatawill be a pointer to the data for the MIDI Event.
- Line 4:
eventDataSizelets us know how big the data is being pointed to by
- Line 5:
hasNextis 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.
We now start the loop that will give us each event.
1 2 3
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.
iteratoris 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.
1 2 3
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:
1 2 3
MIDINoteMessage is a struct with the information about the music note. We finally get what we were looking for.
1 2 3 4 5 6 7
Next, we’ll just output the data for now. That’s a good start and is plenty for a demo tutorial like this.
1 2 3 4 5 6 7 8
After displaying a note we need to try to get the next one and check if there is one.
Here’s the full function.
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
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.
1 2 3 4 5 6 7 8 9 10 11
Here is the output for the first measure of the bass clef.
1 2 3 4 5 6 7
Here’s the file that contains all this code. – main.m
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,