Running Property Exchange Over BLE: What Could Go Wrong?
BLE MIDI is genuinely convenient. No cables, and for simple note on/off messages the latency is acceptable. When I started building MIDI2Kit’s support for the KORG KeyStage, BLE was the obvious connection path — the whole point of the KeyStage’s wireless mode is to cut the cable clutter on your desk.
Simple MIDI 1.0 traffic over BLE? Fine. But Property Exchange is a different story. PE involves large JSON payloads, and large payloads get split into multiple SysEx chunks. Suddenly you’re playing a game of “did all the chunks arrive?” over a radio link that nobody designed for this use case.
Packet Loss With No Retry
Here’s the fundamental problem: BLE drops packets. Not often, but it happens. And the PE spec has no retry mechanism. If one chunk in a multi-chunk response gets lost, the entire response is garbage. You wait for a timeout, then try again from the beginning.
The ResourceList — the response that enumerates all the resources a device exposes — is a typical multi-chunk response. It’s also the thing you absolutely need before you can do anything useful with PE. So you’re in a situation where your most critical first request is also your most likely to fail.
The spec is silent on retries. I checked multiple times. There’s no defined behavior for “chunk N of a response was lost.” The closest thing to guidance is “wait for timeout.” That’s not great when your users are waiting for a connection indicator to turn green.
The iPad Bug I Could Never Explain
This one still bothers me. On iPad (specifically iPad14,10 running iOS 18.6.2), receiving chunk 2 of a ResourceList response produces corrupted data. Bytes 338–342 come out as binary garbage. Every single time. 100% reproducible.
On iPhone — same code, same KORG device, same BLE connection — it works perfectly.
The possible explanations I considered:
- A CoreMIDI BLE MIDI driver bug that only manifests on iPad hardware
- A KORG Module Pro bug specific to the iPad build
- MTU differences between iPad and iPhone affecting how chunks get framed
I couldn’t isolate which one it was. CoreMIDI is closed source. KORG’s firmware is closed source. There’s no way to instrument either side. I documented the reproduction steps, added it to Known Limitations, and moved on. Sometimes that’s the honest answer.
The Warm-Up Problem
Here’s a strange one. If you request ResourceList immediately after a BLE MIDI connection is established, it fails most of the time. But if you first request DeviceInfo (a small, single-chunk response), then request ResourceList, the success rate jumps dramatically.
I started calling this “warm-up.” My working theory is that the BLE connection parameters negotiation isn’t complete at the moment the MIDI stack reports “connected.” The DeviceInfo request buys enough time for the radio link to stabilize. Or maybe it’s something on KORG Module Pro’s side — their BLE stack needs a beat to initialize. I genuinely don’t know.
What I do know is that it works. The .korgBLEMIDI preset in MIDI2Kit bakes in this warm-up sequence automatically:
let client = try MIDI2Client(name: "MyApp", preset: .korgBLEMIDI)
// Internally: fetches DeviceInfo first, then requests ResourceList
// You don't have to think about it
If you’re building on top of MIDI2Kit, this detail is invisible to you. But it took me a while to discover it.
Timeout Design for an Unreliable Channel
The default PE timeout in MIDI2Kit is 5 seconds. For wired connections that’s plenty. Over BLE, a multi-chunk response can legitimately take longer than that under congested radio conditions — I’ve seen responses take over 3 seconds on an otherwise healthy connection.
The .korgBLEMIDI preset extends the timeout to 10 seconds. It also sets maxInflightPerDevice = 1, meaning requests are serialized rather than pipelined. Sending multiple PE requests simultaneously increases BLE link load, which increases packet loss, which defeats the purpose. One request at a time, wait for the response, then send the next.
These settings aren’t ideal — serialized requests with long timeouts make for slow connection setup. But they’re what actually works reliably in practice.
When All Else Fails, Skip ResourceList
There’s a final fallback for when ResourceList just won’t cooperate. Since I know KORG’s resource structure — hard won from SysEx sniffing sessions — I can skip the discovery step and go directly to known resources:
// If ResourceList times out, fall back to known KORG resources
let knownResources = ["DeviceInfo", "CMList", "ProgramList"]
This sacrifices generality for reliability. It only works because I reverse-engineered KORG’s specific resource layout. For a hypothetical future device from a different manufacturer, this fallback wouldn’t apply. But for the KORG case, it means users get a working connection even when the BLE environment is hostile.
The SysEx Send Path Trap
macOS 15 introduced MIDISendEventList, the UMP-native send path. New API, better design, obviously the right choice going forward.
Except KORG BLE MIDI devices don’t handle SysEx correctly when it comes through that path. You have to use the legacy MIDISend function — the MIDI 1.0 packet list API — for SysEx destined for KORG hardware over BLE.
I built a SysExSendStrategy enum to make this controllable:
enum SysExSendStrategy {
case legacyOnly // MIDISend — safe, works with all devices
case auto // MIDISendEventList for UMP-native, legacy otherwise
case umpOnly // Force MIDISendEventList
}
The .korgBLEMIDI preset forces .legacyOnly until I can confirm that the new send path works with KORG hardware. It’s a conservative choice, but “definitely works” beats “sometimes works” here.
Lessons From the BLE Trenches
Getting basic BLE MIDI working is easy. Running a sophisticated protocol like MIDI-CI Property Exchange on top of it — with its multi-chunk transactions, stateful sessions, and timing requirements — is a different problem entirely.
The KORG KeyStage actually does this in practice. Wireless, cable-free, full PE functionality. That’s impressive and it’s worth supporting. But the path to supporting it involved solving problems that probably weren’t in the original BLE MIDI spec authors’ minds.
All of MIDI2Kit’s BLE handling is heuristic. “This sequence of things tends to work” rather than “this is correct per the spec.” The warm-up trick, the serialized requests, the conservative timeouts — none of it is derived from first principles. It’s what survived contact with reality.
Get Started with MIDI2Kit
The MIDI 2.0 library that handles BLE quirks, device-specific workarounds, and all the messy reality of real-world MIDI. Open source, MIT licensed.