Thursday, September 17, 2009

NAudio Tutorial 7 – The Basics of MIDI Files

Preamble

After the invigorating ride with the MIDI interface, I've done what I didn't originally set out to do and fallen for MIDI. It's been a bit of an arms length association for me; I actually started developing OpenSebJ (and BeatIt before that) many years ago because I didn't want to buy a MIDI keyboard and because I admittedly wasn't impressed with what what I associated with MIDI – that tinny sound that streams through your speakers when you started browsing the internet, after founding some ones home page on a free hosting site, that thought it would be wonderful to share with you a piece of music that could only be pitifully rendered through some inbuilt wave table on your Sound Blaster 16 (if you were so fortunate).

I digress; however that history is somewhat important as my focus has shifted since those humble beginnings to now understanding that MIDI does have a role in my future, for two primary reasons

1) It's the industry standard for interfacing Audio Equipment with a computer

2) It's a standard file format that can be read and written by most audio applications and means that layout's and scores using this information are almost universally transferable.

Don't get me wrong, I'm still a sucker for samples and that's where I'll end up targeting all of my development and time any way but MIDI in and of itself, is certainly an assisting means to that end.

This NAudio tutorial will be focusing on the MIDI File Format; we will start with the basics before moving on to the more intricate elements within the format. If you haven't had a chance to review the other posts in the NAudio Tutorial series yet, you can find them here:

http://opensebj.blogspot.com/2009/02/introduction-to-using-naudio.html
http://opensebj.blogspot.com/2009/02/naudio-tutorial-2-mixing-multiple-wave.html
http://opensebj.blogspot.com/2009/03/naudio-tutorial-3-sample-properties.html
http://opensebj.blogspot.com/2009/03/naudio-tutorials-minor-note.html
http://opensebj.blogspot.com/2009/03/naudio-tutorial-4-sample-reversing.html
http://opensebj.blogspot.com/2009/04/naudio-tutorial-5-recording-audio.html
http://opensebj.blogspot.com/2009/08/naudio-tutorial-6-midi-interfacing_27.html

The Format of Events

We can basically think of a MIDI file as a collection of events. These events are the same type of events which were introduced in the previous tutorial. The NoteOnEvent is arguably the most important and it is made up of:

AbsoluteTime – The time when this event will occur, in milliseconds
Channel – The channel (or you can think of it as the instrument), which this event relates to
NoteNumber – The number for the note; basically each note is assigned a number and this is how we work out which note on the scale will be played. Have a look at this nice SVG on Wikipedia which explains it.
Velocity – How hard we want to play the note
NoteLength (Duration) – How long the note is to be played for

So to put this together and create an event:

int AbsoluteTime = 1000; // 1 Second in on the track
int Channel = 1; // Channel needs to be between 1 and 16
int NoteNumber = 54;
int Velocity = 127; // Velocity is from 0 which is considered off, to 127 which is the maximum
int Duration = 250;

NoteOnEvent note1On = new NoteOnEvent(AbsoluteTime, Channel, NoteNumber, Velocity, Duration);

NoteOnEvent note1Off = new NoteOnEvent(AbsoluteTime + Duration, Channel, NoteNumber, 0, 0); // This is in effect a note off event – letting us know that the note can stop playing now.

Each NoteOn needs a corresponding NoteOff. A note off is defined by the Velocity == 0. If we don't have a corresponding NoteOff for a NoteOn event we will get a lovely exception thrown informing us of our civic duty to add a NoteOff for every NoteOn.

One note on and note off event by itself is interesting but not very useful. If we want to keep a set of events together then we should use the MidiEventCollection.

The Collection of Events

A MidiEventCollection is exactly what the name suggests, a collection of MIDI events. However it is a very sophisticated collection and is structured in such a way that allows for easy translation to a Midi file when required. If we have a look at the constructor we have the following:

MidiEventCollection events = new MidiEventCollection(FileType, DeltaTicksPerQuarterNote);

The file type is referring to what format we will be using for the MIDI File – we can set this to one for the purposes of this demonstration.

DeltaTicksPerQuarterNote is what it implies but we wont be going in to detail on this item in this tutorial, for now you can just set it to a value of 120.

Tracks

The MIDI specification can contain a number of tracks (think separate instruments) within the one file. Therefore each Event needs to be associated to a Track. In the version of the MIDI file we are working with in this example, Track 0 is used to store basic meta data about the composition. We add tracks to the MidiEventCollection like so:

int outputTrackCount = 2;
for (int track = 0; track < outputTrackCount; track++)
{
      events.AddTrack();
}

Add Events to the Collection

To add an event to a track all we need to use the Add method of the MidiEventCollection class. The Track is used as the array position identifier and the method then stores the events on that track – like so:

events[1].Add(noteOn1);
events[1].Add(noteOff1);

Export the Collection to a file (Save MIDI File)

Quick recap, we now have a single note being played defined, which is made up of 2 events, a NoteOn event and a corresponding NoteOff event. We have added 2 tracks to the MidiEventCollection, Track 0 & Track 1 and finally we have added the 2 events to Track 1. Before we export our lone playing note composition EndMarkers need to be appended to each Track. Fortunately there is a pre-supplied function for this which makes it rather straight forward, you will need to add it to your class though:

private void AppendEndMarker(IList<MidiEvent> eventList)
{
    long absoluteTime = 0;
    if (eventList.Count > 0)
        absoluteTime = eventList[eventList.Count - 1].AbsoluteTime;
    eventList.Add(new MetaEvent(MetaEventType.EndTrack, 0, absoluteTime));
}

Then it's just a matter of calling the method:

AppendEndMarker(events[0]);
AppendEndMarker(events[1]);

After this it's matter of calling the Export function and passing in the file name where the file is to be saved and the MidiEventCollection storing all of the events, aka:

MidiFile.Export(filename, events);

That's seriously it. We have saved our Midi file to some location. Go play it and hear a single note, exciting.

Other NAudio Tutorials

For more tutorials in this series, please see the following:

http://opensebj.blogspot.com/2009/02/introduction-to-using-naudio.html
http://opensebj.blogspot.com/2009/02/naudio-tutorial-2-mixing-multiple-wave.html
http://opensebj.blogspot.com/2009/03/naudio-tutorial-3-sample-properties.html
http://opensebj.blogspot.com/2009/03/naudio-tutorials-minor-note.html
http://opensebj.blogspot.com/2009/03/naudio-tutorial-4-sample-reversing.html
http://opensebj.blogspot.com/2009/04/naudio-tutorial-5-recording-audio.html
http://opensebj.blogspot.com/2009/08/naudio-tutorial-6-midi-interfacing_27.html