Finding APIs That Don’t Exist (Until You Read the Headers)

I was auditing the macOS 15 CoreMIDI framework headers when I started noticing new class names. Not in the documentation. Not in any release notes. Not mentioned at WWDC. Just sitting there in the SDK headers like Apple forgot to announce them.

Three new APIs caught my attention:

  • MIDIUMPEndpointManager — enumerates UMP endpoints (MIDI 2.0-native devices) with function block info, MIDI version, and manufacturer name
  • MIDICIDeviceManager — returns a list of MIDI-CI capable devices that CoreMIDI knows about
  • MIDIUMPMutableEndpoint — lets your app register itself as a UMP endpoint with custom function blocks

MIDIUMPEndpointManager and MIDICIDeviceManager were added in macOS 15 (Sequoia) but didn’t really start working until macOS 26.4 (Tahoe). MIDIUMPMutableEndpoint has a longer story, which I’ll get to.

The Apple-Manufactured CI Participant That Breaks Everything

When you call MIDICIDeviceManager on a fresh macOS install with no external MIDI devices connected, you still get results back. There’s a participant in the list with the manufacturer SysEx ID 0x11 0x00 0x00.

What 0x11 0x00 0x00 Is

SysEx manufacturer ID 0x11 in the North America block is assigned to Apple Inc. by the MIDI Manufacturers Association. So this isn’t a phantom entry or a test artifact — macOS itself is participating in the MIDI-CI network as an Apple-manufactured device.

It makes conceptual sense. CoreMIDI has always had the IAC Driver (inter-app communication virtual bus) and Network MIDI sessions. This is the MIDI 2.0 version of that: the OS joining the CI network as a management node.

It Responds to Discovery

The MIDI-CI spec requires that any participant on the network respond to Discovery messages. Apple’s OS node does this correctly. Send a Discovery Inquiry, get a Discovery Reply back. MIDICIDeviceManager lists it. Everything looks like a well-behaved CI participant.

But It Silently Ignores All PE Requests

Here’s where things go wrong. Once you’ve discovered this Apple CI participant, you naturally want to send it a Property Exchange GET request. Maybe query DeviceInfo, maybe ResourceList. Standard stuff.

// What actually happens when you send PE to Apple's CI node:
PE GET → DeviceInfo   → ... 5 seconds ... → timeout
PE GET → ResourceList → ... 5 seconds ... → timeout

No response. Not even a NAK. Just silence, then timeout.

A NAK would at least be useful — it would tell you “this resource isn’t supported.” Total silence means you have no idea whether your request was received, rejected, or dropped somewhere in the stack. You just wait out the full timeout window. In audio/MIDI programming, a 5-second hang is catastrophic from a user experience standpoint. The app looks completely frozen.

Three Theories for Why

There’s no official explanation. CoreMIDI is closed source. But here are the scenarios I think are plausible:

Incomplete implementation. Apple shipped the Discovery Responder side but hasn’t finished the PE Responder. Messages arrive internally but get dropped before any response is generated. The fact that there’s zero response (not even an error path) suggests the PE handler just doesn’t exist yet.

Sandboxed for first-party apps only. PE gives direct read/write access to a device’s internal state via JSON. That’s powerful. Apple may be restricting PE access to processes with specific entitlements — Logic Pro, GarageBand, their own audio tools — and silently blocking everyone else. This would be very Apple-like behavior.

Internal management node, not a public endpoint. macOS might need to exist as a CI participant for internal routing and device management reasons, but the PE interface was never meant to be publicly accessible. The Discovery response is a side effect of being on the network, not an invitation.

Any of these could be true. Without source access there’s no way to verify.

The Blacklist Fix

Whatever the reason, the practical consequence is the same: every app built on MIDI2Kit would hang for 5+ seconds on every macOS launch while waiting for Apple’s CI node to respond. That’s unacceptable.

The fix is a manufacturer ID blacklist:

// Don't send PE requests to Apple's CI participant
private let blacklistedManufacturers: Set<ManufacturerID> = [
    .init(0x11, 0x00, 0x00)  // Apple Inc.
]

MIDI2Kit watches MIDICIDeviceManager’s device list for updates. When the list changes, the blacklist is re-evaluated dynamically. New Apple CI participants that appear get excluded automatically; if one disappears, the exclusion is removed. The list stays current without any manual intervention.

This is one of those workarounds that feels slightly absurd to ship — you’re explicitly blacklisting the operating system you’re running on — but the alternative is every user thinking your app is broken. If Apple ships a fully working PE Responder in a future macOS release, I’ll remove the blacklist in a MIDI2Kit update.

MIDIUMPMutableEndpoint: The 16-Combination Experiment

MIDIUMPMutableEndpoint lets your app register itself as a UMP endpoint, complete with custom function blocks. Your app appears to other devices as a proper MIDI 2.0 native device.

On macOS 15, it throws Foundation._GenericObjCError code=0 for every single parameter combination. All 16 of them. Complete failure.

On macOS 26.4, the errors go away. Progress. But “no error” doesn’t mean “it works.” I tested all 16 combinations of the relevant parameters and checked whether the function block actually showed up in MIDIUMPEndpointManager.

Result: 1 out of 16 combinations works. The other 15 succeed without error but register zero function blocks. Silent failure.

The working combination:

Parameter Required Value
direction .bidirectional
maxSysEx8Streams 0
midi1Info .notMIDI1
uiHint .unknown
markAsStatic true
Timing register() before setEnabled(true)

That last row is the sneaky one. Call order matters. If you call setEnabled(true) before your function blocks are registered, they don’t appear in the endpoint manager. No error, just a silently empty endpoint. I only caught this by diffing the successful and unsuccessful call sequences.

For now, MIDI2Kit documents this as the “baseline configuration” and uses it exclusively. Full function block customization will have to wait until Apple either fixes the silent failures or documents what’s actually required.

Smart SysEx Routing: Solving the KORG BLE Problem

macOS 15+ exposes MIDISendEventList as a native UMP send path. For MIDI 2.0 devices, SysEx travels as Data 64 packets through this path, which is the right way to do it on modern hardware.

Except KORG BLE MIDI devices break when you use this path. Send Data 64 to a KORG BLE device and the data arrives corrupted or not at all. You have to use the legacy MIDISend API instead.

MIDI2Kit handles this with a send strategy enum:

// Auto-detects the right send path based on device capability
case .auto:
    if destination.isUMPNative {
        sendViaEventList(data)  // Data 64 via MIDISendEventList
    } else {
        sendViaLegacy(data)     // MIDISend fallback
    }

The .korgBLEMIDI preset forces .legacyOnly regardless of what the device reports. Erring on the safe side is the right call here — a working legacy path beats a broken native path every time.

MIDI-CI v1.2 Support

The macOS 15 timeframe also prompted adding MIDI-CI v1.2 support to MIDI2Kit. The key changes from v1.1:

  • PE Notify message type changed from 0x38 to 0x3F
  • Management Messages (ACK/NAK for management operations) added
  • CI version negotiation during Discovery

All message builders in MIDI2Kit now accept a ciVersion parameter. The default is .v1_1 for backward compatibility. When MIDI2Kit discovers a remote device, it reads the CI version from the Discovery Reply and switches automatically. You don’t have to think about it.

Network MIDI 2.0 on macOS 26.4

The only CoreMIDI change that Apple actually documented in the macOS 26.4 release notes is Network MIDI 2.0 support over UDP. Everything else I’ve described in this post? Not mentioned.

MIDI2Kit’s receive path uses MIDIInputPortCreateWithProtocol(._2_0), which should transparently handle Network MIDI 2.0 sessions. I haven’t been able to verify this yet because I don’t have a Network MIDI 2.0 peer device to test against. It’s on the list.

The macOS 26.4 Verdict: PARTIAL-GO

Here’s where things stand after all this testing:

Working:

  • MIDIUMPMutableEndpoint with the baseline (bidirectional, no SysEx8, notMIDI1, unknown hint, static, register-then-enable)
  • MIDICIDeviceManager device detection (with Apple CI node excluded via blacklist)
  • MIDIUMPEndpointManager UMP endpoint enumeration
  • MIDI-CI v1.2 version negotiation

Not yet verified:

  • Full function block customization (15 of 16 parameter combinations fail silently)
  • Data 64 SysEx reception via UMP receive path
  • PE over a Network MIDI 2.0 session

All the new macOS 15+ APIs in MIDI2Kit are wrapped in @available guards. If you’re targeting macOS 14, none of this code even compiles in — legacy CoreMIDI APIs handle everything, and they work fine. The new path is strictly additive.

Get Started with MIDI2Kit

The MIDI 2.0 library that handles the messy reality of real-world devices. Open source, MIT licensed.

View on GitHub Documentation

More in This Series

← Part 7: The Two-iPhone Problem All Posts →