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.

Offset Size Field ------ ---- ----- 0 1 Universal SysEx ID (0x7E) 1 1 Device ID (0x7F = broadcast) 2 1 Sub-ID#1 (0x0D = MIDI-CI) 3 1 Sub-ID#2 (0x34 = PE Get Inquiry) 4 1 MIDI-CI Version 5 4 Source MUID (28-bit, LSB first) 9 4 Destination MUID (28-bit, LSB first) 13 1 Request ID (7-bit, 0-127) 14 2 Header Size (14-bit, LSB first) 16 N Header Data (JSON)

PE Get Reply (0x35)

Response containing property data.

Offset Size Field ------ ---- ----- 0 1 Universal SysEx ID (0x7E) 1 1 Device ID (0x7F) 2 1 Sub-ID#1 (0x0D = MIDI-CI) 3 1 Sub-ID#2 (0x35 = PE Get Reply) 4 1 MIDI-CI Version 5 4 Source MUID (28-bit, LSB first) 9 4 Destination MUID (28-bit, LSB first) 13 1 Request ID (7-bit) 14 2 Header Size (14-bit, LSB first) 16 N Header Data (JSON) 16+N 2 Num Chunks (14-bit) 18+N 2 This Chunk (14-bit, 1-indexed) 20+N 2 Data Size (14-bit) 22+N M Property Data (JSON, Mcoded7)

📝 Note: Header Data immediately follows Header Size. Chunk fields (numChunks, thisChunk, dataSize) come after the header.

PE Message Types

Sub-IDNameDescription
0x34PE Get InquiryRequest to read property
0x35PE Get ReplyResponse with property data
0x36PE Set InquiryRequest to write property
0x37PE Set ReplyAcknowledge write
0x38PE SubscribeSubscribe to notifications
0x39PE Subscribe ReplySubscription response
0x3FPE NotifyProperty change notification

Multi-Chunk Responses

Large responses are split into multiple chunks. Each chunk contains:

// 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:

8-bit input: [B0] [B1] [B2] [B3] [B4] [B5] [B6] ↓ ↓ ↓ ↓ ↓ ↓ ↓ 7-bit output: [H ] [b0] [b1] [b2] [b3] [b4] [b5] [b6] Where: - H = High bits collected: B0[7] B1[7] B2[7] B3[7] B4[7] B5[7] B6[7] 0 - bN = BN with bit 7 cleared (BN & 0x7F)

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:

// 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
)