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.
Introduction
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 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 Outputs
and 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 Name
and 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.
Using the MidiOutputDevice
is simply a matter of opening it using Open()
and then using Send()
to send MidiMessage
objects. Unlike the previous versions of this library, this one should be able to send sysex messages if the underlying device supports them.
Using a 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[0])
{
stm.Open();
stm.Start();
stm.TimeBase = mf.TimeBase;
stm.SendComplete += delegate (object sender,EventArgs eargs)
{
eventList.Clear();
var next = pos+EVENT_COUNT;
for(;pos<next;++pos)
{
if (len <= pos)
{
pos = 0;
break;
}
eventList.Add(seq.Events[pos]);
}
stm.Send(eventList);
};
for(pos = 0;pos<EVENT_COUNT;++pos)
{
if (len <= pos)
{
pos = 0;
break;
}
eventList.Add(seq.Events[pos]);
}
stm.Send(eventList);
Console.WriteLine("Press a key...");
Console.ReadKey();
stm.Close();
}
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 MidiEvent
or 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.
History
- 27th June, 2020 - Initial submission