Property Exchange Internals
A deep dive into MIDI-CI Property Exchange: message format, chunking, Mcoded7 encoding, and vendor-specific formats.
Message Format
Property Exchange uses Universal SysEx messages with specific Sub-IDs for different operations.
PE Get Inquiry (0x34)
Request to read a property value.
PE Get Reply (0x35)
Response containing property data.
📝 Note: Header Data immediately follows Header Size. Chunk fields (numChunks, thisChunk, dataSize) come after the header.
PE Message Types
| Sub-ID | Name | Description |
|---|---|---|
0x34 | PE Get Inquiry | Request to read property |
0x35 | PE Get Reply | Response with property data |
0x36 | PE Set Inquiry | Request to write property |
0x37 | PE Set Reply | Acknowledge write |
0x38 | PE Subscribe | Subscribe to notifications |
0x39 | PE Subscribe Reply | Subscription response |
0x3F | PE Notify | Property change notification |
Multi-Chunk Responses
Large responses are split into multiple chunks. Each chunk contains:
- numChunks: Total number of chunks (1-16383)
- thisChunk: Current chunk number (1-indexed)
- dataSize: Size of data in this chunk
// PEManager handles chunking automatically
// You receive the complete assembled response
let response = try await peManager.get("LargeResource", from: device.muid)
// Internally, PETransactionManager assembles chunks:
// - Tracks expected chunk count
// - Assembles header and body data
// - Times out if chunks don't arrive
// - Releases Request ID when complete
Mcoded7 Encoding
Property Exchange data is encoded using Mcoded7 to ensure all bytes are 7-bit safe for MIDI transmission.
Encoding Algorithm
Every 7 bytes of 8-bit data become 8 bytes of 7-bit data:
Example
// Original data (8-bit)
let original = Data([0x00, 0xFF, 0x80, 0x7F, 0x01, 0x02, 0x03])
// Encoded (7-bit safe)
let encoded = Mcoded7.encode(original)
// encoded = [0x2A, 0x00, 0x7F, 0x00, 0x7F, 0x01, 0x02, 0x03]
// ^--- high bits: 0b00101010 (bits 7 of bytes 1,3,4)
// Decoded back
let decoded = Mcoded7.decode(encoded)
// decoded == original
💡 Tip: Use response.decodedBody instead of response.body when processing PE responses. The decodedBody property automatically handles Mcoded7 decoding.
14-bit Field Encoding
Two-byte fields in MIDI-CI use 14-bit encoding where each byte's high bit is always 0:
// Encode 14-bit value
func encode14bit(_ value: UInt16) -> (lsb: UInt8, msb: UInt8) {
let lsb = UInt8(value & 0x7F)
let msb = UInt8((value >> 7) & 0x7F)
return (lsb, msb)
}
// Decode 14-bit value
func decode14bit(lsb: UInt8, msb: UInt8) -> UInt16 {
return UInt16(lsb) | (UInt16(msb) << 7)
}
// Example: Header Size = 256
// LSB = 256 & 0x7F = 0 (0x00)
// MSB = (256 >> 7) & 0x7F = 2 (0x02)
// Wire format: [0x00, 0x02]
DeviceInfo Format
The DeviceInfo resource returns device identification. Different manufacturers may use slightly different JSON formats.
Standard MIDI-CI Format
{
"manufacturerName": "KORG Inc.",
"productName": "Module Pro",
"familyName": "KORG Module",
"softwareVersion": "5.0.10",
"productInstanceId": "KM-001234"
}
Alternative Format (e.g., KORG)
{
"manufacturerId": [66, 0, 0],
"manufacturer": "KORG Inc.",
"familyId": [118, 1],
"family": "KORG Module",
"modelId": [4, 0],
"model": "Module Pro",
"versionId": [9, 0, 5, 0],
"version": "5.0.10"
}
MIDI2Kit Handling
PEDeviceInfo handles both formats transparently:
public struct PEDeviceInfo: Sendable, Codable {
// Standard keys
public let manufacturerName: String?
public let productName: String?
public let softwareVersion: String?
// Decoded automatically from either format
public init(from decoder: Decoder) throws {
// Try standard key first, fall back to alternative
manufacturerName = try container.decodeIfPresent(String.self, forKey: .manufacturerName)
?? container.decodeIfPresent(String.self, forKey: .manufacturer)
productName = try container.decodeIfPresent(String.self, forKey: .productName)
?? container.decodeIfPresent(String.self, forKey: .model)
// ...
}
}
Request ID Management
Each PE transaction uses a Request ID (0-127). MIDI2Kit manages these automatically:
- IDs are allocated per-device to allow concurrent requests
- Max 2 concurrent requests per device by default
- IDs are released when response arrives or timeout occurs
PEError.requestIDExhaustedif all 128 IDs are in use
// PETransactionManager tracks:
// - Active transactions by Request ID
// - Chunk assembly state
// - Timeout scheduling
// - Request ID allocation/release
// Configuration
let peManager = PEManager(
transport: transport,
sourceMUID: muid,
maxInflightPerDevice: 2 // Limit concurrent requests
)