A MIDI 2.0 device doesn’t arrive fully described. It arrives, and then it tells us about itself. Windows MIDI Services publishes each piece of that description as it comes in, so an application watching endpoints sees a sequence of notifications for a single device being plugged in, not one.
That surprises people. Applications which assume a device is fully described the moment it appears, or the moment any single notification arrives, end up caching half-built information: blank function block names, a MIDI 1.0 port list with a port missing, an endpoint name which is later replaced by the one the device reports.
This article describes what to expect, and how to write a handler that stays correct.
If you haven’t used the watcher before, start with How to Enumerate UMP Endpoints with Add/Remove/Change Notification.
For a UMP-native device the rough order is:
Added handler runs. At this point the endpoint has the name and capabilities the transport knows about, and for a USB device, its group terminal blocks.Updated.IsEndpointDiscoveryComplete becomes true and Updated is raised with IsEndpointDiscoveryStateUpdated set.A device which isn’t UMP-native, such as a MIDI 1.0 USB device, skips step 2 and is largely described at step 1.
None of this is instantaneous, and none of it is synchronized with your process. A device the user switches on while your application is running will walk through the whole sequence.
MidiEndpointDeviceInformationUpdatedEventArgs carries a set of IsXxxUpdated and AreXxxUpdated properties so that you can rebuild only the part of your model which changed. The full list is on the reference page.
At least one flag is always set. Every property the watcher requests belongs to at least one group, so there’s no “something else changed” state to write code for. If you’re testing all the flags and finding none set, you’re running against a build from before this was true; re-read the properties you care about in that case rather than skipping the update.
The groups deliberately overlap. A user-assigned endpoint name sets both IsNameUpdated and IsUserMetadataUpdated, because it’s both of those things. Test the flag which matches what your code does, not the one which matches where the value is stored.
This is the one which catches most applications.
In the MIDI 2.0 UMP specification, a Function Block Info Notification and a Function Block Name Notification are different messages. A device commonly sends all of its block information first and its block names afterwards, so there’s a window in which a function block legitimately exists with no name yet.
Read the name every time you handle AreFunctionBlocksUpdated, and treat a blank name as “not yet”, not as “this block has no name”:
for (auto const& block : args.UpdatedDevice().GetDeclaredFunctionBlocks())
{
if (!block.Name().empty())
{
// update your cached name for this block
}
}
Don’t cache the blank and stop looking. If you need something to show in the meantime, the group terminal block name or the MIDI 1.0 port name for the same group is a reasonable placeholder.
Updated handlerIf you present MIDI 1.0-style ports to part of your application, get them from MidiLegacyPortDeviceWatcher, not by listing them inside an endpoint update.
The ports are separate device interfaces. The service creates, renames and removes them as function block information arrives, and Windows delivers their arrival notifications independently of the endpoint’s. There’s no ordering between “the endpoint update reached your process” and “the port that update implies exists”. Listing ports inside the endpoint handler is therefore a race you can lose in either direction, and you can lose it on the last update as easily as the first.
IsMidi1PortMappingUpdated tells you the mapping changed, which is a good reason to refresh your view. Take the actual list from the port watcher, which has its own Added, Updated and Removed events, and GetEnumeratedPortsForAssociatedEndpoint() to relate ports back to the endpoint they came from.
See the C++/WinRT watch-midi1-ports and C# watch-midi1-ports samples.
IsEndpointDiscoveryComplete is a hint, not a barrierMidiEndpointDeviceInformation.IsEndpointDiscoveryComplete becomes true when the service finishes gathering in-protocol information, or when it stops waiting for a device which didn’t answer. For an endpoint which doesn’t use in-protocol discovery it’s true from the start.
It’s genuinely useful: it’s a sensible moment to settle your user interface rather than redrawing on every intermediate update. But:
Watcher events are raised from Windows device enumeration, not from your thread. Depending on the apartment your application uses, handlers can run on a thread pool thread, and successive events for the same endpoint aren’t guaranteed to run on the same thread. Protect any state your handler touches, and don’t assume the handler is running where your user interface lives.
UpdatedDevice refers to the watcher’s live object for that endpoint, and the watcher keeps it up to date. If you need a stable picture to work from, take the values you need out of it at the top of your handler rather than reading it repeatedly while you build something.
Added, Updated and Removed are all handled, for the lifetime of the watcher, not just during startupAreFunctionBlocksUpdated, and a blank name is treated as “not yet”MidiLegacyPortDeviceWatcher, not from inside the endpoint handlerIsEndpointDiscoveryCompleteRemoved is handled, so an endpoint which is removed and re-added doesn’t leave a duplicate entry in your list