Introduction
Usually, the .NET marshaller can make those P/Invoke calls into Win32 and other platform native libraries work like magic, but things can sometimes get tricky, and in any case you have to know what you're doing, even then. Despite the power of the marshaller, even in skilled hands, there are some tasks it's simply not up to, leaving you to have to resort to some less pleasant methods of getting your point across with code. While they're best avoided, these are sometimes necessary. I'll cover handling tricky scenarios involving marshalling C structs with embedded fixed or variable length arrays.
Please note that this is far from a complete or real world demonstration of the MIDI API. That is not the goal of this article. The article is merely to show how to P/Invoke with difficult structures. The MIDI API is just a vehicle for that and nothing more. For a comprehensive treatment of the MIDI API download or view the code at this article.
The P/Invoke Declarations
I'm going to assume you've used P/Invoke before, and at the bare minimum have copied and pasted some P/Invoke code before, and know what the DllImportAttribute
and hopefully the StructLayoutAttribute
is.
We'll be calling the Windows 32-bit MIDI function, midiStreamOut()
and the associated midiOutPrepareHeader()
, midiOutUnprepareHeader()
, and midiOutLongMsg()
functions, plus using some callbacks and related support calls to demonstrate the concepts. The important thing about these functions are that they take a MIDIHDR
structure, itself containing a fixed length array of reserved data, and in some cases a variable length array of MIDIEVENT
variable length structs that must be a multiple of a DWORD
(4 bytes) in length. Whew! That's a tall order, but it also demonstrates all the problems that the article will demonstrate how to solve, and with some adaptation, this can also work for COM interop in the same way, although you're less likely to run into this there.
The first step is to get our C API stdcall
function prototypes handy:
MMRESULT midiOutPrepareHeader(HMIDIOUT hmo, LPMIDIHDR lpMidiOutHdr, UINT cbMidiOutHdr);
MMRESULT midiOutUnprepareHeader(HMIDIOUT hmo, LPMIDIHDR pmh, UINT cbmh);
MMRESULT midiStreamOut(HMIDISTRM hms, LPMIDIHDR pmh, UINT cbmh);
MMRESULT midiOutLongMsg(HMIDIOUT hmo, LPMIDIHDR pmh, UINT cbmh);
We can find information on the types therein in the Microsoft Win32 API documentation online and from pinvoke.net. I'll cover them now from left to right and top to bottom.
MMRESULT
is a 32 bit value that indicates 0 if the call succeeded or some non-zero error code if it failed. We'll represent it as an int
. HMIDIOUT
like any Win32 handle, is essentially a pointer. Consequently, we use IntPtr
to represent it. LPMIDIHDR
is a pointer to a MIDIHDR
structure, which we haven't gotten to yet. We can represent it in two ways. The most obvious - IntPtr
, isn't necessarily the best as it means manually copying the structure to and from the memory at the IntPtr
, but sometimes it's necessary. The other - often better option is to use ref
, like in this case ref MIDIHDR
which will marshal a pointer to the structure - it passes the structure by reference. This is safer, cleaner and more efficient than the IntPtr
method - a trifecta! so use it when you can get away with it. We'll need to call some of these functions both ways, so we'll be declaring the P/Invoke functions for them using two different overloads each, one for the IntPtr
, and one for the ref MIDIHDR
. UINT
s here are 32-bit unsigned integers. They are just for passing the size of MIDIHDR
but Marshal.SizeOf()
returns an int
, not a uint
so we'll use int
for these. HMIDISTRM
is another handle, so once again, we use IntPtr
.
As I said before, we'll have multiple overloads for some of the functions, leaving us with the following C# declarations for the above:
[DllImport("winmm.dll")]
static extern int midiOutPrepareHeader(IntPtr hmo, ref MIDIHDR lpMidiOutHdr, int cbMidiOutHdr);
[DllImport("winmm.dll")]
static extern int midiOutPrepareHeader(IntPtr hmo, IntPtr lpMidiOutHdr, int cbMidiOutHdr);
[DllImport("winmm.dll")]
static extern int midiOutUnprepareHeader(IntPtr hmo, ref MIDIHDR pmh, int cbmh);
[DllImport("winmm.dll")]
static extern int midiOutUnprepareHeader(IntPtr hmo, IntPtr pmh, int cbmh);
[DllImport("winmm.dll")]
static extern int midiStreamOut(IntPtr hms, IntPtr pmh, int cbmh);
[DllImport("winmm.dll")]
static extern int midiOutLongMsg(IntPtr hmo, ref MIDIHDR pmh, int cbmh);
Now we can cover the struct
s. The main struct
is MIDIHDR
. It can contain additional data allocated at the memory pointed to by the lpData
member. Sometimes, that data takes the form of an array of MIDIEVENT
structures. These structures themselves are variable length but each one must be a multiple of 4 bytes. Here are the prototypes:
typedef struct midihdr_tag {
LPSTR lpData;
DWORD dwBufferLength;
DWORD dwBytesRecorded;
DWORD_PTR dwUser;
DWORD dwFlags;
struct midihdr_tag *lpNext;
DWORD_PTR reserved;
DWORD dwOffset;
DWORD_PTR dwReserved[4]; } MIDIHDR, *LPMIDIHDR;
typedef struct {
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
DWORD dwParms[];
} MIDIEVENT;
From top to bottom, first MIDIHDR
:
Microsoft, in their infinite wisdom decided lpData
should be LPSTR
instead of LPVOID
. Presumably, there's a reason, but it escapes me. Treat it as an LPVOID
, and thus IntPtr
. It's never a string
.
The next two members are DWORD
s which are 32 bit and unsigned but we'll be using int
for them because it works better for us.
The next one is a pointer, so we use IntPtr
.
The next one is a DWORD
, and int
, or uint
doesn't really matter here. I just use int
. Whichever you use, just make sure your flag consts match the type.
After that, another IntPtr
. This can never be MIDIHDR
because struct
s can't contain references to their own types as members in C#. It would have to be a class, which is doable, but beyond the scope of this article. We don't need to use this field from our code anyway.
Another IntPtr
, this time reserved.
Next, an offset. We'll be using int
here.
Finally, something interesting, a fixed length array. Don't get excited yet. In order to make the struct
's memory layout compatible with the C equivalent, we must "expand" the array into 8 fields, each of IntPtr
. Just be thankful it's 8 and not 256! I use dwReserved1
, dwReserved2
, etc. Note that the definition says 4. 4 is sufficient for midiOutLongMsg()
but not for midiStreamOut()
. We do not need to declare it twice, however. We can declare the biggest one, and just use that everywhere. Extra space doesn't matter as long as it's at the end of the struct. Not enough does.
Now onto MIDIEVENT
:
The first three fields here can be int
.
dwParams
is a different story. At first, it doesn't seem complicated. It's an array of 32 bit unsigned values, sure. However, how long is it? More importantly, how does it impact the in-memory footprint of the struct? All that data needs to come immediately after those first 3 fields. There's no way to marshal that! We can still use this struct, but leave dwParams
out altogether. We'll be writing it in manually when we need it.
[StructLayout(LayoutKind.Sequential)]
private struct MIDIHDR {
public IntPtr lpData;
public int dwBufferLength;
public int dwBytesRecorded;
public IntPtr dwUser;
public int dwFlags;
public IntPtr lpNext;
public IntPtr reserved;
public int dwOffset;
public IntPtr dwReserved1;
public IntPtr dwReserved2;
public IntPtr dwReserved3;
public IntPtr dwReserved4;
public IntPtr dwReserved5;
public IntPtr dwReserved6;
public IntPtr dwReserved7;
public IntPtr dwReserved8;
}
[StructLayout(LayoutKind.Sequential)]
private struct MIDIEVENT {
public int dwDeltaTime;
public int dwStreamID;
public int dwEvent;
}
Yay, there are our primary P/Invoke declarations!
However, the dwReserved1
, dwReserved2
, dwReserved3
, and dwReserved4
, etc. fields must seem pretty cheesy. It is. In reality, there's another way to do it that's easier on the fingers and easier to use, but makes the marshaller do more work under the covers. We could have declared the field like this:
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public IntPtr[] dwReserved;
Here are the reasons we didn't. First of all, we'll never be using those fields ourselves so we don't have to care what they look like and work like. Secondly, there are only 8, not 256. I was just teasing earlier, I'd have never made you type that much. What kind of monster do you take me for? And finally, the marshaller has to allocate that array if we use the method just above. We don't need the overhead. That's useless. I know making the CPU do useless things is fine and even stylish these days, but let's limit the amount of unnecessary work we're making the poor CPU do at this level, kay? Kay. Woo. In practice, this struct is used frequently on time sensitive calls. It's best to code like you mean it. This is a multimedia app, not a business app and you can't scale this code out - if it's not fast enough, you can't just throw another webserver in the mix!
We also have a bunch of other supporting P/Invoke interop code but we'll just cover some here as much of it is beyond the scope to dive into it here, and frankly, if you understood the above, you'll understand the rest. We'll cover more briefly though, just to be thorough:
MMRESULT midiOutGetErrorText(MMRESULT mmrError, LPSTR pszText, UINT cchText);
This will get us a friendly-ish error message for an MMRESULT
returned from one of our earlier P/Invoke methods. We declare it like so:
[DllImport("winmm.dll")]
static extern int midiOutGetErrorText(int mmrError, StringBuilder pszText, int cchText);
Using StringBuilder
here tells the marshaller that this is a fixed length string buffer that will be filled by the caller. I know that's weird that the marshaller would gather that from this declaration but the reason is that filling a string like that is a common pattern with C stdcall
API calls and so Microsoft provides this facility as a convenience. The alternative would be much more difficult, as we'd have to marshal by hand. With this method, we just declare the StringBuilder
with the same capacity as the value of cchText
and then pass it along. It will be filled when the method returns.
MMRESULT midiOutOpen(LPHMIDIOUT phmo, UINT uDeviceID,
DWORD_PTR dwCallback, DWORD_PTR dwInstance, DWORD fdwOpen);
This one opens a MIDI output device. The call returns a handle through the first argument which is a pointer to a handle, or in .NET, a by reference IntPtr
- ref IntPtr
. However, since the function does not care about the value of the argument being passed in, we use out
instead of ref
. If you're ever not sure whether to use ref
or out
in such a situation, use ref
. We will not be using the callback pointer in this case so we just marshal it as an IntPtr
. Otherwise, here, we'd marshal a delegate. The following is our C# declaration:
[DllImport("winmm.dll")]
static extern int midiOutOpen(out IntPtr phmo, int uDeviceID,
IntPtr dwCallback, IntPtr dwInstance, int fdwOpen);
The next one we'll cover involves a callback.
MMRESULT midiStreamOpen(LPHMIDISTRM phms, LPUINT puDeviceID, DWORD cMidi,
DWORD_PTR dwCallback, DWORD_PTR dwInstance, DWORD fdwOpen);
Here's the callback function prototype in C. We'll be needing to pass this into dwCallback
above:
void CALLBACK MidiOutProc( HMIDIOUT hmo, UINT wMsg, DWORD_PTR dwInstance,
DWORD_PTR dwParam1, DWORD_PTR dwParam2);
We can ignore the CALLBACK macro above as it just resolves to stdcall
, which we're already using. Creating the delegate uses the same process we use for creating an imported P/Invoke method. Applying what we've covered so far yields:
[DllImport("winmm.dll")]
static extern int midiStreamOpen(out IntPtr phms, ref int puDeviceId,
int cMidi, MidiOutProc dwCallback, IntPtr dwInstance, int fdwOpen);
private delegate void MidiOutProc(IntPtr hmo, int wMsg, IntPtr dwInstance,
IntPtr dwParam1, IntPtr dwParam2);
A couple of things about our callback: First, and for any callback really, you must make sure your delegate is around for as long as your callback will be called. If not, you will get ghastly errors, or worse, your app will just close with no error. Do not set a match of chicken between the garbage collector and Win32. No matter who wins, you will lose. In this case, our callback can be called even after we've sent all our data to the device, so we must be careful to keep our delegate around for the app's lifetime. Your lifetime will be different depending on your needs, but always be aware of your callbacks. Second, in this case our dwParam1
instance is going to be a pointer to a MIDIHDR
struct. We might have declared ref MIDIHDR dwParam
, and in some situations that might be advisable, but for reasons that will become clear later, it's not what we're going to do here.
The Support Methods
Now that we have our P/Invoke declarations, it's time to put them to work.
We'll start with the easier way of using MIDIHDR
- midiOutLongMsg()
:
static void _SendLong(IntPtr handle,byte[] data,int startIndex,int length)
{
var header = default(MIDIHDR);
var dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
try
{
header.lpData = new IntPtr(dataHandle.AddrOfPinnedObject().ToInt64() + startIndex);
header.dwBufferLength = header.dwBytesRecorded = length;
header.dwFlags = 0;
_Check(midiOutPrepareHeader(handle, ref header, MIDIHDR_SIZE));
_Check(midiOutLongMsg(handle, ref header, MIDIHDR_SIZE));
while ((header.dwFlags & MHDR_DONE) != MHDR_DONE)
{
Thread.Sleep(1);
}
_Check(midiOutUnprepareHeader(handle, ref header, MIDIHDR_SIZE));
}
finally
{
dataHandle.Free();
}
}
Here, what we're doing is using a GCHandle
to pin our data array for us. We then build a MIDIHDR
structure. We're offsetting into the data
array by startIndex
. Note that we're not doing bounds checking here. Normally, in this case, it's especially important to because you can cause access violations if you don't, but for the sample, I've omitted it to keep the code uncluttered. Anyway, we set the fields in the header and then call midiOutPrepareHeader()
before calling midiOutLongMsg()
. For some MIDI hardware - depending on what the actual device is, there can be some delay in sending the note but we can't call midiOutUnprepareHeader()
nor free our pinned array until we're done sending. Since we aren't using a callback, we do a cheap wait in a loop. Most of the time, the sleep will never execute. There are less dirty ways to do this, but this works better than it looks like it would. The data bytes are just MIDI message bytes that correspond to MIDI operations like "note on" and "note off". The protocol for these is beyond the scope of this article, but see my MIDI library article for more about what they can consist of.
Now onto the fun stuff. This is what we've been building toward. We get to prepare a midiStreamOut()
call. This is for those of you that thought the above was too simple (it really is). Here, we get to handle a variable length structs of data. What we'll be doing here is doing some of the work the .NET marshaller has been doing for us ourselves instead. In MIDIHDR
, we need to fill the lpData
member with a pointer to a contiguous array of variable length MIDIEVENT
structs. That's where the standard marshaller falls down, so we'll be doing this ourselves. Each MIDIEVENT
instance's length must be a multiple of 4. The total size of all of the memory, including MIDIHDR
, as best as I can tell, cannot be greater than 65536 bytes - or 64kb. One other issue is we need a new buffer each time we want to call midiStreamOut()
, and we must free that buffer after the send is complete, which we get notified of via a callback. Because we have to handle allocating and freeing of MIDIHDR
ourselves, we're going to be using it differently than we did above.
One thing to note about the memory layout of the buffer we're making is that it's a single buffer that starts with the MIDIHDR
struct, and then is followed by the MIDIEVENT
structs, so basically the lpData
member always points to just after the last member of MIDIHDR
. While we didn't have to do it this way, the alternative would be to do two separate heap allocations - one for lpData
and one for MIDIHDR
itself. Unmanaged heap allocations are relatively costly and doing so more than necessary leads to heap fragmentation further degrading performance. We want to be conservative in how we use it, and to do it properly just requires a little forethought. I demonstrate the proper technique below. We could do even better by recycling allocations but that adds significant complexity and we're going to avoid that here.
static void _SendStream(IntPtr handle, IEnumerable<KeyValuePair<int,byte[]>> events)
{
if (null == events)
throw new ArgumentNullException("events");
if (IntPtr.Zero == handle)
throw new InvalidOperationException("The stream is closed.");
int blockSize = 0;
IntPtr headerPointer = Marshal.AllocHGlobal(EVENTBUFFER_MAX_SIZE+MIDIHDR_SIZE);
try
{
IntPtr eventPointer = new IntPtr(headerPointer.ToInt64() + MIDIHDR_SIZE);
var ptrOfs = 0;
var hasEvents = false;
foreach (var @event in events)
{
hasEvents = true;
if(4>@event.Value.Length)
{
blockSize += MIDIEVENT_SIZE;
if (EVENTBUFFER_MAX_SIZE <= blockSize)
throw new ArgumentException("There are too many events
in the event buffer - maximum size must be 64k", "events");
var se = default(MIDIEVENT);
se.dwDeltaTime = @event.Key;
se.dwStreamID = 0;
se.dwEvent = ((@event.Value[2] & 0x7F) << 16) +
((@event.Value[1] & 0x7F) << 8) + @event.Value[0];
Marshal.StructureToPtr
(se, new IntPtr(ptrOfs + eventPointer.ToInt64()), false);
ptrOfs += MIDIEVENT_SIZE;
}
else
{
var dl = @event.Value.Length;
if (0 != (dl % 4))
dl += 4 - (dl % 4);
blockSize += MIDIEVENT_SIZE + dl;
if (EVENTBUFFER_MAX_SIZE <= blockSize)
throw new ArgumentException("There are too many events
in the event buffer - maximum size must be 64k", "events");
var se = default(MIDIEVENT);
se.dwDeltaTime = @event.Key;
se.dwStreamID = 0;
se.dwEvent = MEVT_F_LONG | (@event.Value.Length);
Marshal.StructureToPtr
(se, new IntPtr(ptrOfs + eventPointer.ToInt64()), false);
ptrOfs += MIDIEVENT_SIZE;
Marshal.Copy(@event.Value, 0,
new IntPtr(ptrOfs + eventPointer.ToInt64()), @event.Value.Length);
ptrOfs += dl;
}
}
if (hasEvents)
{
var header = default(MIDIHDR);
header.lpData = eventPointer;
header.dwBufferLength = header.dwBytesRecorded = blockSize;
Marshal.StructureToPtr(header, headerPointer, false);
_Check(midiOutPrepareHeader(handle, headerPointer, MIDIHDR_SIZE));
_Check(midiStreamOut(handle, headerPointer, MIDIHDR_SIZE));
headerPointer = IntPtr.Zero;
}
}
finally
{
if (IntPtr.Zero != headerPointer)
Marshal.FreeHGlobal(headerPointer);
}
}
This routine processes "events" where an event is a key-value pair of a delta and some message bytes. The delta tells the stream "when" to play the message byte data. Each delta is relative to the one that preceded it. The length of a delta tick is dependent on the tempo and timebase, which we won't cover setting here. Like before, the message data bytes are MIDI bytes that correspond to different actions like "note on" or "note off". For each event, there are one of two major paths to be taken. The first is if the message is shorter than 4 bytes. If that path is taken, our effective MIDIEVENT
struct is simply:
typedef struct {
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
DWORD dwParms[0];
} MIDIEVENT;
Or in simpler terms:
typedef struct {
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
} MIDIEVENT;
Basically, we don't need dwParms
at all if data.Length/@event.Value.Length
is less than 4.
The second case is more complicated. Basically, we need to write a struct ourselves, and the struct must be a multiple of 4 bytes, so that means basically, it must be a struct with all DWORD
members in this case - at least in terms of the memory layout. I'll show you what I mean:
For data.Length
= 4:
typedef struct {
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
DWORD dwParms[1];
} MIDIEVENT;
or the equivalent:
typedef struct {
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
DWORD dwParm1;
} MIDIEVENT;
For data.Length
= 5 through 8:
typedef struct {
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
DWORD dwParms[2];
} MIDIEVENT;
or the equivalent alternative:
typedef struct {
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
DWORD dwParm1;
DWORD dwParm2;
} MIDIEVENT;
For data.Length
= 9 through 12:
typedef struct {
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
DWORD dwParms[3];
} MIDIEVENT;
or the equivalent alternative:
typedef struct {
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
DWORD dwParm1;
DWORD dwParm2;
DWORD dwParm3;
} MIDIEVENT;
And on we go, adding another field or array element each time we need more bytes.
Obviously, we didn't create all of these declarations. We theoretically could have gone that way, but it would have been ridiculous, and error prone, plus the result would be a maintenance nightmare.
What we've done instead is copy memory into the buffer at strategic locations such that we first write the 3 member/12 byte MIDIEVENT
base structure. We then advance our pointer to the end of that structure, and write out the data
/@event.Value
array padding the end until it's a multiple of 4 bytes. I hope that makes sense.
Here's the code from the above that computes the padding. Here "dl" is abbreviated for data length.
var dl = @event.Value.Length;
if (0 != (dl % 4))
dl += 4 - (dl % 4);
If there's a clearer way to do that computation, be my guest, but it works as is.
From there, we just copy at our current location: First, we do the MIDIEVENT
base structure, then we move the pointer and copy our data/@event.Value
bytes. It's refreshingly simple.
Marshal.StructureToPtr(se, new IntPtr(ptrOfs + eventPointer.ToInt64()), false);
ptrOfs += MIDIEVENT_SIZE;
Marshal.Copy(@event.Value, 0,
new IntPtr(ptrOfs + eventPointer.ToInt64()), @event.Value.Length);
ptrOfs += dl;
A few things to note: The first is we're using Marshal.StructureToPtr()
and that's technically obsolete. There's an alternative using GCHandle
but it's ugly, as in less readable, more error prone, and it requires yet another P/Invoke declaration besides. Microsoft did not provide a suitable replacement for this method. Someone correct me in the comments if I'm wrong. The second is we're not zeroing out the padded bytes. We don't have to because the driver never reads them. It's just more code to do so which means more opportunity for bugs, without any real benefit. Finally, we're using ToInt64()
to get our IntPtr
integral value we can do arithmetic on. We don't use ToInt32()
nor do we cast to int
in the alternative because while this code has only been tested on the 32-bit, I'd rather not create problems supporting 64-bit down the road. Doing it this way doesn't hurt, and makes the code slightly more future proof.
The Main() Code
Here's where we call everything we just made. The demos are very simple, just enough to exercise the above code a little and give you a little bit of feedback. I didn't want to take the focus away from the marshalling, which was the important bit of this article.
This isn't an article on how the MIDI protocol works, but my MIDI library article covers it some, and does all the stuff this article explores in its library code.
static void Main()
{
IntPtr handle=IntPtr.Zero;
try
{
_Check(midiOutOpen(out handle, 0, IntPtr.Zero, IntPtr.Zero, 0));
Console.Error.WriteLine("Sending middle C. Press any key to continue...");
var data = new byte[4];
data[0] = 0x90;
data[1] = 60;
data[2] = 0x7F;
data[3] = 0;
_SendLong(handle, data, 0, data.Length);
Console.ReadKey(true);
data[0] = 0x80;
data[1] = 60;
data[2] = 0x7F;
data[3] = 0;
_SendLong(handle, data, 0, data.Length);
}
finally
{
if (IntPtr.Zero != handle)
_Check(midiOutClose(handle));
}
var devId = 0;
const int NOTE_LEN = 48;
_Check(midiStreamOpen(out handle, ref devId, 1,
MidiOutProcHandler, IntPtr.Zero, CALLBACK_FUNCTION));
try
{
Console.Error.WriteLine("Streaming chords to output. Press any key to exit...");
_Check(midiStreamRestart(handle));
var midiEvents = new KeyValuePair<int, byte[]>[]
{
new KeyValuePair<int, byte[]>(0,new byte[] {0xF0,1,2,3,4,5,6,7,8,9,0xF7}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x90,60,127}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x90,64,127}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x90,67,127}),
new KeyValuePair<int,byte[]>(NOTE_LEN,new byte[] { 0x80,60,127}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x80,64,127}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x80,67,127}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x90,62,127}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x90,65,127}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x90,69,127}),
new KeyValuePair<int,byte[]>(NOTE_LEN,new byte[] { 0x90,64,127}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x90,67,127}),
new KeyValuePair<int,byte[]>(0,new byte[] { 0x90,72,127}),
};
_SendStream(handle, midiEvents);
Console.ReadKey();
_Check(midiStreamStop(handle));
}
finally
{
if (IntPtr.Zero != handle)
_Check(midiStreamClose(handle));
}
}
All we're doing here is sending some messages. I made up a sysex message for the stream playback to exercise the second codepath in _SendStream()
where it deals with messages longer than 24 bits. The sysex message doesn't do anything that I know of, though it's possible that a connected MIDI device could theoretically recognize it at which point who knows what would happen? On your sound card's MIDI wavetable synthesizer, it has no effect. Note that we really should be using midiOutShortMsg()
instead of midiOutLongMsg()
which is generally only for sysex messages that can't be packed into 24 bits, but it doesn't matter. The former is just an optimized version of the same thing, and wouldn't demonstrate any of the principles in the article so it was omitted.
The only other thing we're doing here which I won't get into is "unpreparing" and then freeing the MIDIHDR
buffers we allocated and prepared in _SendStream()
when the _MidiOutProc()
callback handler is invoked. We get the pointer passed into us by the API
Points of Interest
Using the MIDI API is a kind of fresh hell. There is almost no documentation for it and it's very finicky. The workarounds and kludges - such as sleeping in a loop after midiOutLongMsg()
- I've provided that appear throughout the article are pretty standard when working with this API, even from C. If I'm wrong about how any of it works, feel free to lodge a complaint and correct me in the comments.
History
- 4th July, 2020 - Initial submission