Introduction to MIDI: Part 2

Body

This second part of my introduction to MIDI programming on (Open)BeOS is about timing. If you read Part 1 and tried out the demo app, you may have noticed that playback of the notes really lagged behind if you quickly pressed several keys in a row. That is bad, because we want MIDI processing to be as real-time as possible. This article explains where the lag comes from, and shows you how to prevent it. Be warned: some of this discussion goes into gory low-level details and the guts of the Midi Kit, but that is the price you pay for low latency MIDI performance. Actually, it ain't all that bad.

How events are consumed

As you'll recall, the BeOS Midi Kit lets you create two types of MIDI endpoints: producers that send out MIDI events (comparable to a MIDI Out socket) and consumers that receive these events (a la MIDI In). So how does a consumer, aka the BMidiConsumer class, handle incoming MIDI events? Using Kernel Kit ports, that is.

Each consumer object has a Kernel Kit port associated with it to receive MIDI events from connected producers. A port is a low-level communications device for sending data between different applications. They are also used for sending and receiving BMessages, for example. You don't really need to know how ports work to use the Midi Kit, but you do need to understand how they affect the flow and timing of MIDI events.

In addition to opening a port, each consumer also creates a separate thread whose only task is to read incoming MIDI events from the port, and to call one of BMidiLocalConsumer's hook functions in response. As long as nothing is sent to the port, this thread blocks. When some producer sends an event to the consumer, for example "note on", the thread wakes up and calls the corresponding NoteOn() hook. After it is done with the hook function, the thread tries to read from the port again and the cycle repeats itself.

Inside the hook function, a typical consumer will first snooze_until() the performance time of the event. When that time has come, the thread wakes up again and actually "performs" the event, whatever that means. The consumer thread has a real-time priority, so needless to say, the hook functions should deal with the events as quickly as possible. (This may sound contradictory; first I'm telling you that the consumer must snooze, and now it should hurry up? Snoozing is fine because it takes up no processor time, and the real-time thread won't bog down the rest of the system while it is sleeping. It is performing the actual event that should be as fast as possible.)

Lousy timing

Now, back to our problem: what causes the lag? Obviously, while the consumer is snoozing, it won't deal with any other MIDI events that arrive at its port. Its thread is asleep and won't wake up until it is time to perform the current event. This works fine if the other pending events are all scheduled to be performed after the current one, but in our case that isn't necessarily true.

(If you are wondering where these "pending events" go in the mean time, they don't go anywhere. Kernel ports have a first-in/first-out message queue, and if the queue is full, any subsequent senders will block until their messages can be delivered. BMidiConsumers have a queue that has room for only one message. This means that at most one producer at a time can put a MIDI event in the queue, and all the others will simply block. Our EchoFilter's producer, for example, performs several SprayNoteOn()'s in a row. But what really happens is that it blocks on the second call to SprayNoteOn() until the first event has been processed by the consumer. Then it blocks on the third call until the second event is performed, and so on.)

An example: Suppose we have hooked up a (virtual) MIDI keyboard to the input of our echo filter, and the BeOS softsynth to its output. You press the "C" key on the keyboard at time 1. This event arrives at the echo filter, which passes it on to the softsynth. The echo filter also schedules three additional echo notes at performance times 2, 3, and 4. The softsynth consumer now snoozes until time 1 and performs the first event. Then it snoozes until time 2 and performs the second event. So far so good.

Imagine what happens if we now press the "D" key on the keyboard. We are still at time 2, so that sends a new "note on" event to the synth with time 2, and the echo filter adds three more echo notes with times 3, 4, and 5. But the softsynth first has to work through the "old" events. So when the consumer is finally ready to perform the "D" note, it is already time 4. Et voila, there is the lag. The "D" note on event should be performed at time 2, but there is no way to tell the consumer this. It is happily snoozing away, and there is no way to interrupt it.

Now what!?

There are two possible solutions: either consumers queue up incoming events and sort them in time before performing, or producers sort outgoing events before actually sending them. By design, the Midi Kit places this burden on the producers. Not on all producers, mind you; just on those that may spray events in the "wrong order". It just so happens that our echo filter is one of them. (In my opinion, this is a deficiency in the design of the Midi Kit. Making sure that MIDI events arrive and are processed in the proper order is really something that the Midi Kit should automatically take care of. Something to keep in mind for a future revision of the Midi Kit.)

Fortunately for you, I already have written the code to do this. The class MidiQueue is a filter object that receives incoming events, puts them on a queue (a real queue, not the kernel port thing), snoozes until it is time to perform them, and finally sends them out to any connected consumers. Of course, it sorts the events as it puts them on the queue, because that is the whole point of this exercise. Now you can simply put this MidiQueue object between your producer and your consumer, and it will automatically take care of everything.

In the EchoDemo sample project, I hook up the output of the EchoFilter to the input of the MidiQueue. Unlike before, these two endpoints are not Register()'ed, so you can't connect anything else to them. Of course, the input of the EchoFilter and the output of the MidiQueue are published. The easiest way to try this out is to connect the input of the EchoFilter to the output of a MIDI keyboard and the output of the MidiQueue to InternalMidi. You'll find that you now can now hammer on the keyboard all you want and still get near-instant playback. Woohoo!

To be honest, my implementation of MidiQueue is not ideal. A more complete implementation would give the queue a finite length to prevent producers from running wild and generating thousands of events in advance. I also used a rather simplistic method to find the next event to perform. MidiQueue's producer launches a thread that sleeps for a bit, checks if it is already time to perform the first event in the queue, and repeats that ad infinitum. (The sleep is short enough not to be noticeable, and long enough not to drag the CPU too much.) A better solution would be to snooze_until() it is time to perform the top-most event on the queue. If in the mean time another event is received that needs to be performed earlier, the snooze could be interrupted by sending the thread a SIGCONT signal.

More goodies

I made several other changes to the source code as well. Last time I just wanted to introduce the various Midi Kit concepts without cluttering the code too much and scaring you off. Now that you already more-or-less know how the Midi Kit works, I have taken the liberty to add some goodies. First of all, the EchoFilter now extends from a base class called SimpleMidiFilter. This class provides several convenient facilities for making your own MIDI event filters. In addition, if you fire up PatchBay you will see that the EchoFilter's endpoints now have pretty little icons. Anything to please the eye.

Download the sample code for this article here. The various utility classes that are used by the sample project are part of MidiUtil, a package I put together especially for making MIDI programming easier. You can get it here. And with that I conclude this short introduction to MIDI programming on the BeOS. For more info, check out the (preliminary) midi2 kit documentation. Now go and make some noise ;-)