NOW LOADING

Best Practices and Performance Optimizations


Best practices for developers using the Windows MIDI Services SDK

Here’s a list of some best practices and performance optimizations for MIDI API-consuming applications.

Fast transmission of messages

For maximum compatibility across languages, and for safety, WinRT doesn’t allow pointers to be exposed by any properties or as parameters or return types for any function. In addition, the by-value and by-reference semantics for parameters are not always under the control of the API developer.

For those reasons, and to maximize ease of use across a number of languages and use-cases, we have multiple ways to send and receive messages.

You will want to do your own performance testing from your application and scenarios, but in general, the send/receives with the least overhead are those which send/receive individual 32 bit words, a single 128 bit structure, or the IMemoryBuffer. The word and struct methods do pass copies of data, but the amount of data, for most time critical messages, is still 64 bits or less (MIDI 1.0 channel voice messages are 32 bits, MIDI 2.0 channel voice messages are 64 bits).

The IMemoryBuffer approach is a more advanced way to transfer data to and from the API. This wraps a buffer of data which you can reuse between calls, including send/receive, as long as you manage and avoid any potential overlaps. Internally, the COM types used to access this ensures that only pointers are passed into the API. There’s a bit more ceremony to using this approach, so we recommend investing time there only if it better fits your app’s programming model. In addition, because IMemoryBuffer deals with bytes and not 32 bit words, you need to ensure you are correctly copying the data in, following the endianness rules for our internal MIDI 2.0 data representation.

The most flexible, but least performant approach, is to use the IMidiMessage interface and the methods which return strongly typed messages. These do involve additional type allocations either on the part of the caller or in the API code.

COM Extensions

For C++ and C++-like languages, we’ve added the COM extensions for fast allocation-free send and receive of messages. All the normal WinRT types are used for Session and Connection. But when you want to send and receive messages, and can ensure the data integrity of the messages being sent (most cross-platform apps already have code to do this), the COM Extensions are the way to go. There is a C++ example in the repo and examples in the documentation.

A connection uses either the COM Extensions or the WinRT message processing plugins, never both, because a registered messages received callback bypasses every listener and the connection’s own MessageReceived event. Decide which one a given connection uses before you write the receive path, and register the callback, or add the plugins, before calling Open(). The API reports the conflict rather than letting one silently disable the other, so check the result of AddMessageProcessingPlugin and the HRESULT from SetMessagesReceivedCallback. This matters most for virtual devices, which are themselves implemented as a message processing plugin.

Splitting large messages across transmissions

There is a limit to how many MIDI words may be sent in a single call. That limit is available through GetSupportedMaxMidiWordsPerTransmission, on both the connection and the COM extension interface. If a single call contains more words than the limit allows, the whole call is rejected and nothing is sent. Because the rejection is all-or-nothing, no data has reached the device, so retrying with a smaller buffer is safe.

System Exclusive is where this comes up. A SysEx7 UMP carries only six data bytes, so a 64 KB bulk dump is around 10,900 UMPs, far beyond what a single transmission can hold. Firmware updaters, patch librarians, and bulk dump tools all need to split their data and send it as a series of transmissions.

Query the limit for each connection rather than hard-coding it, split only on message boundaries so that no UMP ever spans two transmissions, and stop sending as soon as one transmission fails. Any chunks already accepted have reached the device, so decide in advance how your app recovers from a partial transfer. For System Exclusive, that normally means abandoning the transfer and starting it over.

If you are building a cross-platform framework or a language projection on top of this API, handle the splitting inside your own layer. Applications written against your abstraction usually cannot reach GetSupportedMaxMidiWordsPerTransmission, so leaving the job to them means they will hard-code a limit which is not guaranteed to remain correct. More guidance for library and framework authors is in Porting a MIDI Library or Framework to Windows MIDI Services.

For the rules and sample code, see MidiEndpointConnection and the COM Extensions.

Displaying connections to your app users

Most apps need to display device and endpoint connection information to their users. Here are some details related to that.

Use the MidiEndpointDeviceWatcher to respond to device changes

MIDI devices come and go based on connecting/disconnecting USB cables, or new network endpoints coming online. In addition, properties like Function Blocks and Endpoint Name are subject to change at any time. Use the Windows::Devices::Midi2::Enumeration::MidiEndpointDeviceWatcher class on a background thread to monitor these endpoints, and receive notifications when anything changes. This is a much more robust approach vs simply enumerating a snapshot of devices up-front.

There’s no API or service reason to require a customer to reboot or reload/restart a MIDI DAW or other application to see newly added endpoints when using Windows MIDI Services.

For more information, see the How to Watch Endpoints page.

Don’t include diagnostics endpoints for most apps

Unless the app is a utility / testing app, we recommend you do not display the UMP Loopback Endpoints to the user. These are for diagnostics and testing only. By default, they are excluded during enumeration.

Always display the Group Number, not the Group Index

Groups, like Channels, are indexed 0-15, but the actual number to present to the user is always 1-16. The Built-in MidiGroup and MidiChannel types in the SDK make it easy to ensure you are using the correct values for data or display.

Enable drill-down into Groups (functions)

A single function block may exist on multiple groups, and multiple groups may overlap function blocks. That is the nature of the MIDI 2.0 specification. In most cases, you’ll find that a function is associated with one or more groups and those groups do not span other function blocks.

We recommend that, when displaying a connection to the user, you connect them to the UMP Endpoint, but then enable some sort of drill-down to show the function block names and their associated groups. Remember that the ultimate address of most MIDI Messages is the Endpoint, Group, and Channel.

SynthCompany Foo Synth 5
- Synthesizer (Groups 1, 2, 3)
- Sequencer (Groups 3, 4, 5)
- MIDI DIN Out (Group 6)

or

SynthCompany Foo Synth 5
- Group 1 (Synthesizer)
- Group 2 (Synthesizer)
- Group 3 (Synthesizer, Sequencer)
- Group 4 (Sequencer)
- Group 5 (Sequencer)
- Group 6 (MIDI DIN Out)

Or similar based on the conventions of your application.

Note that a flat list, like what many apps used for MIDI 1.0 ports, is not as reasonable in a MIDI 2.0 world. Best practices for this will come out over time as various applications grapple with the increased address count in MIDI 2.0.

Use .AsEquivalentFunctionBlock() for Group Terminal Blocks

USB MIDI 1.0 devices and some USB MIDI 2.0 devices will not have Function Blocks. Per-spec, Function Blocks are optional. However, those USB devices will have Group Terminal Blocks. The preference is to use the Function Block when available. However, to keep your data model uniform, we project Function Blocks from Group Terminal Blocks using the .AsEquivalentFunctionBlock() function of the MidiGroupTerminalBlock type. Not all properties map cleanly, but we make a best-effort attempt here to provide the application with usable data that can be presented to the user.

When an endpoint has both kinds of block, use the Function Blocks and ignore the Group Terminal Blocks. They are two views of the same endpoint at different levels of authority, not two sets of ports, so listing both produces a doubled list in which the customer cannot tell which entry is real. Note also that Function Blocks are discovered in-protocol and arrive after the endpoint does, so an endpoint can legitimately have none yet. Rebuild your list when the Updated event reports that function blocks have changed, and keep doing so for as long as you hold the watcher.

Use the Function Block UI Hint to help you decide how to show functions

The UI Hint property of a Function Block was created to give the UI an indication of the intended direction of communication, as a user would see it, for a function block. This shouldn’t necessarily block functions from showing up in a list that contains, for example, input devices, but it may be that you want to prioritize the ones with an appropriate UI hint, and have a “see all” option or similar to display the rest.

Didn't find what you were looking for?

Windows MIDI Services is an open source project with all source available on GitHub. We have a great community on Discord as well. Between GitHub and Discord, you should find the information you are looking for.