My Midi library now includes MIDI output device enumeration, MIDI stream support, and more. You can now do background playback of in memory streams much more efficiently.
I provided my MIDI Library as part of a larger project that included a MIDI file slicer and a simple drum machine. These worked, but they required you to stop playing the output before any changes were made, and they also took quite a bit of CPU to preview.
Fortunately Windows provides an efficient hardware accelerated MIDI streaming API you can use to send MIDI events to the output. The native API is not easy to use from C#, but I have wrapped it to make it much easier.
The upshot of this is background playback of an in-memory MIDI sequence without stealing a bunch of CPU cycles or creating another thread.
I have updated the MIDI library to reflect this. I have also updated the MIDI Slicer and FourByFour drum machine apps to allow you to edit in a more real time manner, reflecting the new capabilities of the library.
Conceptualizing this Mess
For the basics on using my MIDI library, see this article. Mostly I'll be covering the additional features I've added here.
As mentioned Windows provides a streaming API for sending MIDI events out to a device. The events, like a standard MIDI event, are stamped with the delta in ticks so that multiple events at different times can be sent at once. There are some limitations to this API, such that it's not always hardware accelerated, but more importantly the send buffer is only 64kb, meaning you can only queue up 64kb worth of events for playback at any given time.
What we do with this is feature is we queue up events as we go, so that when a user changes a setting that can be reflected in our event stream almost right away.
We've got some new classes to explore:
MidiDevice is the base class for MIDI devices and contains accessors to get the available output devices and streams. It has
Streams properties which enumerate each respectively.
MidiOutputDevice is a specialized
MidiDevice that contains features specific to MIDI output devices. You can get the associated
MidiStream for the output device by retrieving
MidiOutputDevice.Stream. Each device has a
Index which identify it.
Both streams and devices must be opened before being used. However, when opening them be aware that you cannot have both a MIDI output device and its associated stream open at the same time. If you need the features of both,
MidiStream allows you to send messages immediately, like the output device, plus it allows you to queue up events.
MidiOutputDevice is simply a matter of opening it using
Open() and then using
Send() to send
MidiMessage objects. Unlike the previous versions of this libarary, this one should be able to send sysex messages if the underlying device supports them.
MidiStream you can do the same thing as above, which is simple, or to use the streaming features, you have to set some things up first. You typically need a
SendComplete event handler to tell you when the queued up events have all been played. In addition to using
Open(), you'll also typically need to set the
TimeBase and finally you'll use
Start() to make the queued events begin playing.
Coding this Mess
The scratch project contains code to stream a file to the output 100 events at a time.
var mf = MidiFile.ReadFrom(@"..\..\Bohemian-Rhapsody-1.mid");
const int EVENT_COUNT = 100;
int pos = 0;
var seq = MidiSequence.Merge(mf.Tracks);
int len = seq.Events.Count;
var eventList = new List<MidiEvent>(EVENT_COUNT);
using (var stm = MidiDevice.Streams)
stm.TimeBase = mf.TimeBase;
stm.SendComplete += delegate (object sender,EventArgs eargs)
var next = pos+EVENT_COUNT;
if (len <= pos)
pos = 0;
for(pos = 0;pos<EVENT_COUNT;++pos)
if (len <= pos)
pos = 0;
Console.WriteLine("Press a key...");
As you can see this is a little bit involved. That's the price you pay for streaming. However, once you strip away all the comments the core isn't that complicated. Basically what we're doing is taking a MIDI file, and merging all the tracks into a single sequence for playback. We then get the
Events off of that
MidiSequence and we start iterating through them, at a maximum of 100 at a time. The less events you use, the more real time you can alter them, but the more CPU intensive playback will be. It's a tradeoff. If we reach the end we start over so we can loop. For each batch of events we add them to
eventList for playback. We then queue those events for playback using
Send(). Note how we're doing this inside the
SendComplete handler, and also once at the beginning to kick things off. Finally we simply wait for a key. Closing the stream will stop the playback and stop the events from firing. Remember that
Send() can take either
MidiMessage message objects, but the former will be queued for playback while the latter will not.
We do the above technique in our demo projects as well, except instead of loading the file from disk, we create it, or load it and preprocess it depending on the settings in the UI.
- 27th June, 2020 - Initial submission