Click here to Skip to main content
15,868,141 members
Articles / Programming Languages / C#

P/Invoke Jujitsu: Passing Variable Length Structs

Rate me:
Please Sign up or sign in to vote.
4.77/5 (9 votes)
4 Jul 2020MIT16 min read 11.1K   93   6   9
How to marshal structs that the .NET marshaller just can't handle without a lot of help
How to marshal structs that contain variable length, and/or fixed length arrays

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:

C++
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.
  • UINTs 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:

C#
[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 structs. 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:

C++
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]; // should be 8!!! for midiStreamOut()
} 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 DWORDs 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 structs 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.

C#
[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;
    // DWORD dwParams[] is marshalled manually
}

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:

C#
[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:

C++
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:

C#
[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.

C#
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:

C#
[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:

C++
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:

C#
[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():

C#
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));
        // cheap, but we're not using a callback here:
        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.

C#
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;
    // we always allocate 64k for our buffer. The alternative
    // would be to enumerate our events twice, the first time
    // to get the total buffer size. That's undesirable
    // 64kb of RAM is trivial.
    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) // short msg <= 24 bits
            {
                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;
                // pack 24 bits into dwEvent
                se.dwEvent = ((@event.Value[2] & 0x7F) << 16) + 
                             ((@event.Value[1] & 0x7F) << 8) + @event.Value[0];
                // this method is documented as "obsolete" but the alternative 
                // involves declaring a CopyMemory win32 API call 
                // and using that to copy memory pinned by a GCHandle.
                // yuck. Just do this instead. 
                // I wish MS would give you suitable replacements when
                // they deprecate something
                // note that we're copying the structure to 
                // our current working memory location
                Marshal.StructureToPtr
                   (se, new IntPtr(ptrOfs + eventPointer.ToInt64()), false);
                // increment our pointer by the size ofa MIDIEVENT
                ptrOfs += MIDIEVENT_SIZE;
            }
            else // probably a sysex - either way, greater than 24 bits.
            {
                // our MIDIEVENT is variable length but must be a multiple of 
                // 4 bytes. The following ensures that. dl will contain our
                // modified data length, padded as necessary
                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);
                // and once again we copy the structure 
                // to our current working memory location
                Marshal.StructureToPtr
                   (se, new IntPtr(ptrOfs + eventPointer.ToInt64()), false);
                ptrOfs += MIDIEVENT_SIZE; // increment our pointer
                // now copy our variable length portion
                Marshal.Copy(@event.Value, 0, 
                  new IntPtr(ptrOfs + eventPointer.ToInt64()), @event.Value.Length);
                // increment our pointer by the adj. variable length amount
                ptrOfs += dl;
            }
        }
        if (hasEvents)
        {
            var header = default(MIDIHDR);
            header.lpData = eventPointer;
            header.dwBufferLength = header.dwBytesRecorded = blockSize;
            // copy our MIDIHDR into the buffer
            Marshal.StructureToPtr(header, headerPointer, false);
            _Check(midiOutPrepareHeader(handle, headerPointer, MIDIHDR_SIZE));
            _Check(midiStreamOut(handle, headerPointer, MIDIHDR_SIZE));
            headerPointer = IntPtr.Zero;
        }
    }
    finally
    {
        // if headerPointer is non-zero and we got here, either an error occurred
        // or midiStreamOut was never called so free it.
        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:

C++
typedef struct {
  DWORD dwDeltaTime;
  DWORD dwStreamID;
  DWORD dwEvent;
  DWORD dwParms[0];
} MIDIEVENT;

Or in simpler terms:

C++
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:

C++
typedef struct {
  DWORD dwDeltaTime;
  DWORD dwStreamID;
  DWORD dwEvent;
  DWORD dwParms[1];
} MIDIEVENT;

or the equivalent:

C++
typedef struct {
  DWORD dwDeltaTime;
  DWORD dwStreamID;
  DWORD dwEvent;
  DWORD dwParm1;
} MIDIEVENT;

For data.Length = 5 through 8:

C++
typedef struct {
  DWORD dwDeltaTime;
  DWORD dwStreamID;
  DWORD dwEvent;
  DWORD dwParms[2];
} MIDIEVENT;

or the equivalent alternative:

C++
typedef struct {
  DWORD dwDeltaTime;
  DWORD dwStreamID;
  DWORD dwEvent;
  DWORD dwParm1;
  DWORD dwParm2;
} MIDIEVENT;

For data.Length = 9 through 12:

C++
typedef struct {
  DWORD dwDeltaTime;
  DWORD dwStreamID;
  DWORD dwEvent;
  DWORD dwParms[3];
} MIDIEVENT;

or the equivalent alternative:

C++
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.

C#
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.

C#
Marshal.StructureToPtr(se, new IntPtr(ptrOfs + eventPointer.ToInt64()), false);
ptrOfs += MIDIEVENT_SIZE; // increment our pointer
// now copy our variable length portion
Marshal.Copy(@event.Value, 0, 
             new IntPtr(ptrOfs + eventPointer.ToInt64()), @event.Value.Length);
// increment our pointer by the adj. variable length amount
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.

C#
static void Main()
{
    IntPtr handle=IntPtr.Zero;
    try
    {
        // open the first MIDI port
        _Check(midiOutOpen(out handle, 0, IntPtr.Zero, IntPtr.Zero, 0));
        Console.Error.WriteLine("Sending middle C. Press any key to continue...");
        // note ON
        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);
        // note OFF
        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));
    }

    // do streaming!

    var devId = 0; // use the first MIDI output device
    const int NOTE_LEN = 48; // arbitrary distance between the notes, in ticks
    // open the stream, specifying our callback
    _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...");
        // start the stream so it will play queued MIDI data
        _Check(midiStreamRestart(handle));
        // create a buffer of MIDI events
        var midiEvents = new KeyValuePair<int, byte[]>[]
        {
            // first one is a sysex message i just made up
            new KeyValuePair<int, byte[]>(0,new byte[] {0xF0,1,2,3,4,5,6,7,8,9,0xF7}),
            // note ons
            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}),
            // note offs
            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}),
            // note ons
            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}),
            // even more notes
            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}),
        };
        // send them
        _SendStream(handle, midiEvents);
        // wait for exit
        Console.ReadKey();
        // stop anything still playing
        _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

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
United States United States
Just a shiny lil monster. Casts spells in C++. Mostly harmless.

Comments and Discussions

 
SuggestionPadding Pin
Ronald M. Martin6-Jul-20 11:18
Ronald M. Martin6-Jul-20 11:18 
Here's a coding tip:

The following code:
var dl = @event.Value.Length;
if (0 != (dl % 4))
    dl += 4 - (dl % 4);

Can be replaced by:
var dl = (@event.Value.Length + 3) & (~3);

This is an example of what you might call maximally-biased rounding. It is applicable to any situation in which you want to round up to the nearest multiple of a specific power of two. If n = 2^m, then replacing both threes in the line above with the value of n - 1 will round up to the nearest multiple of 2^m.

Ron Martin
cpuwzd
GeneralRe: Padding Pin
honey the codewitch6-Jul-20 19:19
mvahoney the codewitch6-Jul-20 19:19 
QuestionThank you very much for a lot of work with code and article Pin
LightTempler4-Jul-20 23:13
LightTempler4-Jul-20 23:13 
AnswerRe: Thank you very much for a lot of work with code and article Pin
honey the codewitch5-Jul-20 2:04
mvahoney the codewitch5-Jul-20 2:04 
QuestionWell done :-) Pin
Garth J Lancaster4-Jul-20 13:51
professionalGarth J Lancaster4-Jul-20 13:51 
AnswerRe: Well done :-) Pin
honey the codewitch4-Jul-20 13:54
mvahoney the codewitch4-Jul-20 13:54 
GeneralRe: Well done :-) Pin
Jon McKee4-Jul-20 14:29
professionalJon McKee4-Jul-20 14:29 
GeneralMy vote of 5 Pin
User 110609794-Jul-20 7:06
User 110609794-Jul-20 7:06 
GeneralRe: My vote of 5 Pin
honey the codewitch4-Jul-20 7:10
mvahoney the codewitch4-Jul-20 7:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.