The Spec Is Beautiful. Reality Is Chaos.

I want to be fair to the people who wrote the MIDI 2.0 specification. The documents published by MMA and AMEI are genuinely good. UMP packet structure, MIDI-CI message formats, Property Exchange request/response flows — it’s all there, clearly organized, with diagrams. You can sit down and read through it and come away feeling like you understand the system.

The problem is how much the spec simply doesn’t cover.

I found this out the hard way while building MIDI2Kit. Every time I thought I had a solid implementation, some real device would do something the spec didn’t mention — and suddenly I was debugging protocol behavior that had no documentation to reference. Let me walk through the specific places where the spec and reality diverge.

The Encoding Problem: KORG Ignores Mcoded7

Property Exchange data travels over SysEx. SysEx has a 7-bit constraint — the high bit of every byte must be zero. So when you need to send arbitrary 8-bit data through a SysEx channel, you have to encode it first. The spec specifies an encoding called Mcoded7 for exactly this purpose.

KORG’s implementation sends raw UTF-8 without any Mcoded7 encoding.

If you faithfully implement the spec and run incoming KORG data through your Mcoded7 decoder, the result is garbage. If you skip the decoder and treat it as plain UTF-8, it works fine. The KORG device is technically speaking outside the spec, but it’s a real shipping product and you have to handle it. This is the first lesson of real-world MIDI 2.0 development: working code beats written spec.

The Type Mismatch That Silently Drops Data

This one is subtle enough to burn a lot of time before you notice it.

In the Property Exchange ResourceList response, each resource entry has canGet and canSet fields. The spec says these are booleans. Swift’s Codable is happy to decode booleans. Everything looks fine.

KORG sends strings instead:

// What the spec says
{"resource": "DeviceInfo", "canGet": true, "canSet": false}

// What KORG actually sends
{"resource": "DeviceInfo", "canGet": "full", "canSet": "none"}

The insidious part: Codable doesn’t throw an error when it can’t decode a field into the expected type. It just skips the entire object. KORG has two proprietary resources in their ResourceList, and both of them were silently disappearing — no crash, no warning, no indication anything was wrong. I only noticed because I was wondering why those resources never showed up.

The fix was a custom CanValue type that accepts either a boolean or a string:

enum CanValue: Decodable {
    case bool(Bool)
    case string(String)

    var isEnabled: Bool {
        switch self {
        case .bool(let v): return v
        case .string(let s): return s == "full"
        }
    }
}

Not a complicated fix, but you have to know it’s needed first. And the spec gives you no reason to expect it.

The Schema Field Type Mismatch

While I was in that neighborhood, I found another one. The schema field in a ResourceList entry is documented as a string in the spec — a reference to a schema name. KORG sends an inline JSON object instead. Same field name, completely different type.

At this point I had stopped being surprised.

Request ID Exhaustion

Property Exchange request IDs are 7-bit values, so you get 128 of them per device. The spec says you can have up to 128 simultaneous in-flight requests. What it doesn’t say is anything about when those IDs get released.

In practice: when a PE transaction fails — no NAK comes back, timeout fires, your request object gets cleaned up — there are situations where the ID doesn’t actually get recycled. Run enough failed transactions and you hit 128 outstanding IDs. New requests fail immediately with “no available request IDs,” and you have no way to tell from the outside whether the device is broken or your ID pool is exhausted.

The spec doesn’t define when IDs should be released. It doesn’t define what to do when they run out. Implementers are left to figure this out on their own, which means every implementation handles it differently, which means the behavior you observe varies by device.

PE Notify: The Subtype That Changed Between Versions

MIDI-CI v1.1 used subtype 0x38 for PE Notify messages. MIDI-CI v1.2 changed it to 0x3F.

These are the same message doing the same thing — the subtype just changed. If you send a v1.2 Notify (0x3F) to a device that only understands v1.1, it ignores it. If you send a v1.1 Notify (0x38) to a v1.2 device, it might also ignore it.

The right approach is to check which MIDI-CI version each device advertised during Discovery, then use the correct subtype per device. This is manageable but tedious, and it’s the kind of implementation detail that’s easy to miss if you’re only testing against modern hardware.

Apple’s Undocumented CI Participant

macOS 15 added MIDIUMPEndpointManager and MIDICIDeviceManager. These classes exist in the SDK headers. They don’t appear in any WWDC session. The documentation is minimal at best.

When you use MIDICIDeviceManager to enumerate discovered CI participants, one of them carries Apple’s manufacturer SysEx ID (0x11 0x00 0x00). It responds to Discovery broadcasts — the protocol handshake works. But send it any Property Exchange request and it goes silent. No response, no error, no NAK. You just wait for the timeout.

There’s no documentation explaining this behavior. I only understood what was happening after enough timeouts to recognize the pattern. The solution was to blacklist this participant — detect the Apple manufacturer ID and skip PE requests entirely. But I wouldn’t have known to do that without observing it on real hardware first.

The Apple CI participant responds to Discovery but ignores all PE requests. It appears to be an internal system component that’s registered on the CI bus but not intended for third-party interaction.

The Real Specification Is Running Code

After enough of these encounters, I arrived at a conclusion that’s a bit uncomfortable: the authoritative specification for MIDI 2.0 behavior isn’t the published document. It’s the aggregate behavior of shipping devices.

The spec represents what the protocol is supposed to do. But interoperability — the actual goal of any protocol — is defined by what real implementations do. When KORG ships a device that ignores Mcoded7, the practical spec for “how to talk to KORG devices” now includes “don’t use Mcoded7.” That fact lives nowhere except in the heads of people who have debugged it.

This is what MIDI2Kit is trying to address. Every quirk, every undocumented behavior, every type mismatch gets encoded into the library so that the next developer doesn’t have to rediscover it. The spec is where you start. The library is where you end up.

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
← All Posts