Updated:
Tested on: macOS 27.0; iConnectivity mioXM (DIN loopback), Roland A-88MKII (MIDI 2.0 mode), test programs (2026-09-24, 2026-09-28)

We sent a UMP Endpoint Discovery to a USB MIDI 1.0 device six times. The send function reported success all six times, and nothing ever came back.

Looking into it, we found at least four places in Core MIDI where return values and API descriptions alone make it hard to tell what is actually happening. A success code does not mean the message arrived, and not appearing in a list does not mean something does not exist, so you need a second way to check. What follows was checked on macOS 27.0; where the cause is not confirmed, we say so.

1. Stream Messages sent to a USB MIDI 1.0 device report success but do not come back

What happens. Send a UMP Stream Message (Message Type F, such as Endpoint Discovery) to the output of a USB MIDI 1.0 device, and MIDISendEventList returns success (status 0), but the message could not be received through the DIN loopback. There is no error and no notification.

How we checked. We used a port of an iConnectivity mioXM. The mioXM’s USB MIDIStreaming interface opened by MIDIServer is USB MIDI 1.0, with only the MIDI 1.0 setting (Alternate Setting 0). Core MIDI also reported the port’s protocol (kMIDIPropertyProtocolID) as 1, but that value describes the MIDI protocol the endpoint uses, not the USB transfer format. We sent to this port, whose DIN OUT and IN were connected with a cable as a loopback, and received on the same port’s input.

SentCame back
Endpoint Discovery (6 times)nothing
SysEx F0 7D 01 02 F7after about 2 ms

To find where it vanished, we compared the cumulative count of memory descriptors used for transfers on the mioXM USB interface opened by MIDIServer (the IOMemoryDescriptor in UsbUserClientBufferStatistics, readable with ioreg -l) before and after each send. It did not move while idle, went up by 3 for every SysEx, and never moved for Endpoint Discovery. We take this value to increase with each USB transfer; if so, the Endpoint Discovery was most likely dropped on the Mac side. We did not capture the USB traffic itself, though, so where it was dropped is not confirmed.

Sent instead to a MIDI 1.0 virtual port created on the same Mac, the Endpoint Discovery arrived unchanged. Core MIDI’s protocol conversion (MIDI 2.0 to 1.0) does not drop Stream Messages.

Cause (our guess). Neither USB MIDI 1.0 nor DIN has any format that can carry a Stream Message. We believe the USB MIDI 1.0 driver running inside MIDIServer drops Stream Messages when converting UMP to USB MIDI 1.0, since they have nothing to map to. We cannot see inside the driver, so this is not confirmed. The undefined System Real Time byte FD likewise did not move the value and did not come back through the loopback; where it was lost is not confirmed either.

Workaround. Exchange Stream Messages (Endpoint Discovery, Function Block Discovery, Stream Configuration and so on) over paths that can carry UMP Stream Messages, such as USB MIDI 2.0 devices and virtual ports. Stream Messages are a valid format in UMP even with the MIDI 1.0 protocol. Over a USB MIDI 1.0 device or DIN, you can only test as far as MIDI-CI, which travels as SysEx.

2. MIDIUMPEndpointManager leaves out devices connected before launch

What happens. MIDIUMPEndpointManager.shared.umpEndpoints does not include UMP Endpoints registered with the system before your app started. Even a USB MIDI 2.0 device that answers UMP 1.1 Endpoint Discovery gives an empty list if it was connected before your app. Only endpoints registered while your app is running appear, along with MIDIUMPEndpointWasAddedNotification. The SDK header says the manager keeps a local copy of the system-wide list, which does not match this behavior.

How we checked. With both a Roland A-88MKII in MIDI 2.0 mode and an endpoint created with the public MIDIUMPMutableEndpoint API, we swapped the order in which processes were started:

ConditionList
Process started after the device was connected (waited 5–10 s)empty
Process already running before the device was power-cycledadded notification after about 0.3 s, and listed
Separate process started after the MIDIUMPMutableEndpoint was createdempty
Separate process already running before the MIDIUMPMutableEndpoint was createdlisted

Whether an endpoint is listed depends on whether the process was running when it was registered, not on the device type or driver. The MIDI-CI device list (MIDICIDeviceManager.shared.discoveredCIDevices), on the other hand, returned the A-88MKII even to a process started later.

Cause (our guess). We believe the manager’s local list is built only from notifications after launch and never loads the endpoints that already exist. This is inside Core MIDI, so it is not confirmed. We have not checked other macOS versions.

Workaround. When you need the existing UMP Endpoints, do not rely on umpEndpoints. Send Endpoint Discovery and Function Block Discovery yourself and read the replies. In our test environment, the Core MIDI device and entity property dictionaries also had keys such as ump endpoint and, for Group Terminal Blocks, First Group and Number of Groups, and these could be read too. However, they are keys we observed, not public constants in the SDK, and we have not checked whether they work the same way on other macOS versions.

A small tip for investigating. To read MIDIServer’s logs, run /usr/bin/log with its full path. In zsh, the built-in log command takes precedence, and log show can print nothing without any error.

3. MIDI 2.0 virtual ports are not UMP Endpoints, and even with MIDIUMPMutableEndpoint your app answers the inquiries

What happens. Virtual ports created with MIDISourceCreateWithProtocol and MIDIDestinationCreateWithProtocol using MIDI 2.0 only send and receive UMP. They do not become UMP Endpoints (endpoint information, Function Blocks, answers to inquiries). This is the documented behavior in the SDK header.

Created with MIDIUMPMutableEndpoint (macOS 15 and later), an endpoint is registered with Core MIDI as a UMP Endpoint. But Core MIDI does not answer Endpoint Discovery or Function Block Discovery sent by other apps. The inquiries arrive unchanged in your app’s receive callback.

How we checked. In one process we created a plain MIDI 2.0 virtual port and a MIDIUMPMutableEndpoint, and sent inquiries from another process: Endpoint Discovery to both, and Function Block Discovery to the MIDIUMPMutableEndpoint. Every inquiry arrived unchanged in the receive callback, and Core MIDI sent no reply. We did not try Function Block Discovery on the plain virtual port. Only the MIDIUMPMutableEndpoint had endpoint properties such as ump endpoint = 1 and appeared in the MIDIUMPEndpointManager list (and, as in pitfall 2, only for processes running when it was registered).

Whether MIDIUMPMutableEndpoint not answering inquiries is intended or a bug is not stated in the header, so we cannot tell.

Workaround. If you only need to pass MIDI 2.0 Channel Voice messages to a DAW, a plain virtual port created with MIDI 2.0 is enough. To present an endpoint name and Function Blocks to other apps, a possible approach is to create a MIDIUMPMutableEndpoint and have your app handle the incoming inquiries in its receive callback and send the corresponding notifications: Endpoint Info, Device Identity, Endpoint Name, Product Instance ID, Stream Configuration, Function Block Info and Function Block Name. We have not yet tested implementing these replies, or whether other apps (such as DAWs) recognize them correctly.

Relatedly, when a MIDIUMPMutableEndpoint sent a Stream Configuration Notification with a protocol different from MIDIServer’s configuration, MIDIServer logged a NOTICE and sent back a Stream Configuration Request with its own configuration, though not always, for example when it had just sent one. To an Endpoint Discovery sent from the endpoint, it did not answer and only logged a NOTICE.

4. When an input is recreated with the same Unique ID, disconnecting the old reference returns -50

What happens. When a connected input (Source) is removed and recreated with the same Unique ID, its MIDIEndpointRef changes. MIDIPortDisconnectSource on the old reference returns -50 (invalid parameter), while connecting to the new reference succeeds.

How we checked. With test virtual ports, in this order:

  1. Create an input, get its Unique ID and connect it from an input port.
  2. Remove the input, create another one and give it the same Unique ID.
  3. MIDIObjectFindByUniqueID returns the new reference.
  4. Disconnecting the old reference returns -50; connecting the new one succeeds (0).

We have not confirmed whether this is an OS bug or something that changed in macOS 27.

Workaround. Do not ignore -50 across the board. When a disconnect returns -50, look up the current input by its Unique ID. If no input is found, it is gone; if a different input from the old reference is found, it was replaced. Either way you can go on with the cleanup. If the input of the old reference is still found, or the lookup itself fails, treat it as a real failure.

What these have in common

  • A success code does not mean it arrived. Do not rely on the send function’s return value alone. Check another way: receive it back through a loopback, compare with another path (a virtual port), or read transfer statistics the OS keeps.
  • Not being listed does not mean not existing. Swap the order processes are started, look from another process, and compare with other APIs (properties, the MIDI-CI device list).
  • Keep “documented” and “bug” apart. Even when behavior does not match the SDK header, anything the header does not cover cannot be called intended or a bug. Our records separate what we confirmed from what we guessed.

We will update this page if we learn more.

About MIDIFabric

When disconnecting a recreated input returns -50, MIDIFabric checks in the way described above whether it was removed or replaced before cleaning up. It does not use MIDIUMPEndpointManager for its device list. The behavior described here is in Core MIDI and happens without MIDIFabric too.

← All developer notes