Synthesia Clone "Piano Hero": Parsing Midi Files

Summer Project Numero Uno: Creating a Synthesia Clone for Android

Background: Synthesia is a piano game and trainer written in C++ that builds a piano roll out of a Midi file. Synthesia was also originally named "Piano Hero" before Activision sent a cease and desist letter telling them to change their name.

Synthesia is extremely helpful for learning new songs quickly (especially if you're slow at reading sheet music like me). However, finding a decent position for a computer near/ontop of your keyboard is very troublesome. And with the recent hype over tablet computers, most of which run android?, getting Synthesia to fit on (the thingy that holds sheet music) is a must.

Midi Files: Midi files are composed of MidiEvents, which generally represents an action such as a Note On, and are organized into tracks, which represent separate streams of MidiEvents. Every event has an associated delta-time stamp, measured in ticks, which determines when it should occur relative to the previous event. In order to convert ticks to seconds, we need to know two more things: the resolution and tempo. The resolution is the number of ticks per quarter note, which I kind of think of as the quality of the midi, and can be found in the file header. The tempo is number of microseconds per quarter note, but most people appear to convert this to beats per minute. The tempo is a little more difficult as it can change during as song. Once we have all of this, converting is some pretty straight forward algebra:
ppqn = 480                   // ticks per quarter note, get from file header
bpm = 60000000 /tempo; // quarter notes per minute, get tempo from MidiEvents
mspt = 60000 /( bpm * ppqn ) // milliseconds per tick
Working with ticks in Java is a little different, because Java automatically converts the delta ticks to cumulative ticks. So events having the following ticks 10, 10, 10 respectively would become 0, 10, 20. Now here's some half-pseudo-code for parsing a single track in a Midi file in Java:
int bmp = 120; // default is 120
int tempo = 0;
int ppqn = 480; // get from file header
int last_tick = 0;

double ct = 0; // the cumulative time
double mspt = 60000.0 / ( (double)bpm * (double)ppqn );

for(int i = 0; i < track.size(); i++) {
MidiEvent event = track.get(i);
MidiMessage msg = event.getMessage();

if(msg instanceof ShortMessage)
switch( ((ShortMessage)msg).getCommand() )
case NOTE_ON:
ct += mstp * (event.getTick() - last_tick);
last_tick = event.getTick();
case NOTE_OFF:
ct += mstp * (event.getTick() - last_tick);
last_tick = event.getTick();
else if(msg instanceof MetaMessage)
switch( ((MetaMessage)msg).getType() )
case 0x51:
ct += mstp * (event.getTick() - last_tick);
last_tick = event.getTick();
tempo = getIntFromByteArray(msg.getData());
bpm = 60000000 / tempo
mstp = 60000.0 / ((double)bpm * (double)ppqn);
For the sake of a piano roll, we only need to worry about these three types of messages. Notice that NOTE_ON and NOTE_OFF are two separate events. This means that if you want to create some kind of Note object, you need to either keep an array of half complete notes or look ahead for the next NOTE_OFF event with the same key number.

One last precaution! The first track in Type 1 Midi files contain all of the tempo events for all of the other tracks and is called the tempo map.

There are three types of Midi files:

Type 0: Everything is saved in one track.
Type 1: Multiple tracks with individual parts on separate tracks.
Type 2: Multiple tracks which represent different patterns. (Not commonly found)

So what I did was to go through the first track and find all of the tempo events and create duplicate events in the rest of the tracks.

1 comment:

  1. This is good stuff man, appreciate you posting some code too. MIDI can handle soooo much making it such a bitch to actually write a quick interpreter when looking at official documents.

    ReplyDelete