The Right Tool for Undocumented Protocols
When documentation doesn’t exist, you go to the source. The fastest way to understand what KORG devices are actually doing over MIDI-CI was to watch them talk to each other.
MIDI2Kit has a debug-only PE sniffer mode built in. When active, it disables the PE Responder and switches to passive recording — every MIDI-CI SysEx message gets captured verbatim without sending any responses. I put a KORG KeyStage and a copy of KORG Module Pro on the same MIDI bus, fired up the sniffer, and started watching.
What follows is a tour of everything I found.
The manufacturerName Gate
This was the first surprise and, honestly, it’s kind of clever. When KORG KeyStage receives a DeviceInfo response from a connected device, it checks the manufacturerName field. Only when that field contains exactly "KORG" does KeyStage ask for KORG’s proprietary resources.
Translation: if you want your app to integrate fully with KeyStage — sound names on the LCD, parameter names on the knobs — you have to identify yourself as KORG.
// In the DeviceInfo response
{"manufacturerName":"KORG","productName":"M2DX DX7 Synthesizer"}
You also set manufacturerID: .korg in the MIDI-CI Device Identity. This isn’t technically a spec violation — manufacturerName is a free-text field and the spec doesn’t restrict what you put there. But it’s definitely not something you’d expect to be necessary. The MIDI-CI spec envisions capability negotiation based on what a device can do, not who made it.
KORG presumably implemented it this way because it was the simplest gate to build. Whatever the reason, you have to work with it.
KORG’s Proprietary Resources
Once you pass the manufacturerName check, KeyStage starts requesting resources that aren’t in the MIDI 2.0 spec at all.
X-ProgramEdit
This resource carries the current program name and CC values. It’s what powers the LCD display on the KeyStage — when you switch sounds in your app, KeyStage knows what to show because it reads this resource.
The format has some quirks. Program names must follow a specific NUMBER:NAME format: "1:E.PIANO 1". One-based numbering, colon separator, then the name. That format matches what KORG Module uses internally, and KeyStage expects the same format from any app that claims to be KORG. If you send the name in a different format, the display doesn’t update correctly.
CC values come back in a currentValues array — another non-standard field with no PE spec equivalent.
X-ParameterList
This resource defines the parameters available for CC control: names, CC numbers, default values. KeyStage uses this to label its hardware knobs and sliders. When your app provides this resource, the physical controls on the keyboard show meaningful parameter names instead of generic CC numbers.
JSONSchema
Before requesting either of the above, KeyStage fetches schema definitions: parameterListSchema and programEditSchema. These describe the expected data format for the resources that follow. Interestingly, KORG sends these schemas as inline JSON objects rather than schema name strings — which is another point where their implementation diverges from the spec (as covered in Part 3).
The PE Notify Echo Loop
This is the one that cost me real debugging time.
The scenario: KeyStage sends a CC value change over regular MIDI. My app receives it and updates the corresponding parameter. Because the parameter changed, my code sends a PE Notify to all subscribers — which includes KeyStage itself, since it’s subscribed to the resource.
KeyStage receives the PE Notify. Its internal PE processor tries to handle it. But the update it triggered was the same change it just sent, so it’s seeing its own action reflected back. When this happens fast enough or in enough volume, KeyStage’s PE processor falls behind and the LCD freezes.
The fix is straightforward once you understand the cause: track where each CC change originated. Changes that came in via MIDI don’t need a PE Notify echo — the sender already knows about the change. Only changes triggered by the user interface (or other non-MIDI sources) should generate PE Notify messages.
This required adding an input source flag to CC change events throughout the codebase. Not difficult, but it touched a lot of places, and the symptom (LCD freeze) was confusing enough that it took a while to trace back to the root cause.
The lesson: when you’re both a PE Initiator and a PE Responder on the same bus, you need to track the origin of every state change to avoid feedback loops.
PE Initiator-Only: The Mode Nobody Documents
The most surprising discovery in this whole investigation was about KORG Module Pro’s PE behavior.
The MIDI 2.0 PE spec describes Initiators (devices that send requests) and Responders (devices that answer them). It describes how sessions work. What it doesn’t describe is a device that only participates as an Initiator and never responds to incoming requests.
KORG Module Pro is exactly that. If you send a PE GET request to it, nothing comes back. Here’s the log output from my sniffer:
PE: All dests tried, no response. KORG likely PE Initiator-only.
PE: (KORG queries US, but doesn't respond to PE GET)
KORG Module queries your app for its resources. But if you turn it around and query KORG Module, you get silence. PE Capability Reply has no flag to indicate this — there’s no way to know in advance that a device is Initiator-only. You find out when your requests time out.
The defensive approach: maintain a per-device capability map. When PE requests to a device consistently produce no response (not NAK — just silence), mark it as Initiator-only and stop sending requests. This should really be something the spec allows devices to declare, but for now it has to be inferred.
The 200ms Discovery Re-Send Mystery
Here’s a protocol timing quirk that I could only find through trial and error.
When KeyStage sends a Discovery Inquiry (sub-ID2: 0x70), the right response is to send your own Discovery in return. But there’s a catch: if you respond immediately, KeyStage ignores you. The PE session never establishes.
The magic number is 200 milliseconds. Wait 200ms after receiving the Discovery Inquiry, then send your own Discovery. At that point, KeyStage responds correctly and the session proceeds.
Why 200ms? I genuinely don’t know. It’s not in any documentation. It’s not derivable from the spec. I found it by starting at 0ms (fails), trying 100ms (fails), 150ms (fails sometimes), 200ms (works consistently). It could be an internal initialization delay in KeyStage’s firmware, or it could be a debounce window, or it could be something else entirely. Whatever it is, you have to accommodate it.
Fixed MUID for Session Persistence
MIDI-CI assigns each participant a MUID — a 28-bit identifier used to address messages. The spec says MUIDs should be randomly generated. MIDI2Kit generates them randomly by default.
When building support for KORG KeyStage specifically, I discovered something useful: KeyStage remembers the MUID of the last device it connected to. If you reconnect with the same MUID, KeyStage skips the full Discovery negotiation and resumes the previous PE session directly. Reconnection is nearly instant.
With a random MUID, every connection looks like a new device. KeyStage runs through the full negotiation sequence every time. It works, but it’s noticeably slower.
M2DX uses a fixed MUID (0x5404629) for this reason. The trade-off is that if two instances of the app are running simultaneously, they share a MUID, which would cause routing problems. For a single-instance instrument app, the benefit of instant reconnection outweighs that risk.
What the Sniffer Made Possible
Every discovery I described above came from the PE sniffer — from reading raw hex dumps of SysEx traffic between devices that weren’t designed to be observed. None of it came from documentation.
The sniffer itself is a few hundred lines of #if DEBUG-gated code in MIDI2Kit. It decodes UMP MIDI 2.0 messages to human-readable form, hex-dumps raw CI SysEx, and logs everything with timestamps. It was the most valuable debugging tool I built during this project.
KORG devices are a solved problem in MIDI2Kit now. The manufacturerName gating, the proprietary resources, the Initiator-only constraint, the Discovery timing, the PE Notify echo suppression — all of it is handled automatically. Third-party Swift developers building apps that target KORG hardware don’t have to reverse-engineer any of this themselves. That’s the whole point of the library.
Get Started with MIDI2Kit
The MIDI 2.0 library that handles the messy reality of real-world devices. Open source, MIT licensed.