Skip to content

midi2kit/MIDI2Kit-SDK

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

74 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MIDI2Kit

A Swift library for MIDI 2.0 / MIDI-CI / Property Exchange on Apple platforms.

Features

  • MIDI-CI Device Discovery - Automatically discover MIDI 2.0 capable devices
  • Property Exchange - Get and set device properties via PE protocol
  • Advanced SET Operations - Payload validation, batch SET, and pipeline workflows
  • UMP SysEx7 Bidirectional Conversion - MIDI 1.0 SysEx ↔ UMP Data 64 packet conversion
  • Multi-Packet SysEx7 Reassembly - Actor-based UMPSysEx7Assembler for fragmented messages
  • RPN/NRPN → MIDI 1.0 CC Conversion - UMP RPN/NRPN to MIDI 1.0 Control Change approximation
  • High-Level API - Simple MIDI2Client actor for common use cases
  • Responder API - MIDI2ResponderClient for creating MIDI-CI Responders with typed resources (v1.0.17+)
  • Connection Policy - MIDI2ConnectionPolicy for selective device filtering (v1.0.17+)
  • KORG Optimization - 99% faster PE operations with KORG devices (v1.0.8+)
  • Adaptive Warm-Up - Automatic connection optimization with device learning
  • Swift Concurrency - Built with async/await and Sendable types

Requirements

  • iOS 17.0+ / macOS 14.0+ / tvOS 17.0+ / watchOS 10.0+ / visionOS 1.0+
  • Xcode 16.0+
  • Swift 6.0+

Installation

Swift Package Manager

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/hakaru/MIDI2Kit.git", from: "1.0.0")
]

Or in Xcode: File → Add Package Dependencies → Enter the repository URL.

Local Release Validation (No GitHub Actions)

If GitHub Actions cannot run (for example due to billing limits), you can run the same consumer smoke test locally for every release tag:

git clone https://github.com/midi2kit/MIDI2Kit-SDK.git
cd MIDI2Kit-SDK
chmod +x Scripts/consumer-smoke-local.sh
Scripts/consumer-smoke-local.sh 1.0.14

You can also pass a tag with v prefix and/or a custom repository URL:

Scripts/consumer-smoke-local.sh v1.0.14 https://github.com/midi2kit/MIDI2Kit-SDK.git

Success ends with:

OK: MIDI2Kit-SDK consumer smoke passed

Quick Start

import MIDI2Kit

// Create and start the client
let client = try MIDI2Client(name: "MyApp")
try await client.start()

// Listen for device discovery
Task {
    for await event in await client.makeEventStream() {
        switch event {
        case .deviceDiscovered(let device):
            print("Found: \(device.displayName)")
            
            // Get device info if PE is supported
            if device.supportsPropertyExchange {
                let info = try await client.getDeviceInfo(from: device.muid)
                print("Product: \(info.productName ?? "Unknown")")
            }
            
        case .deviceLost(let muid):
            print("Lost: \(muid)")
            
        default:
            break
        }
    }
}

// Later: clean shutdown
await client.stop()

Configuration

Customize behavior with MIDI2ClientConfiguration:

var config = MIDI2ClientConfiguration()

// Discovery settings
config.discoveryInterval = .seconds(5)
config.deviceTimeout = .seconds(30)

// PE settings
config.peTimeout = .seconds(10)
config.maxRetries = 3

// Create client with custom config
let client = try MIDI2Client(name: "MyApp", configuration: config)

Or use presets:

// KORG BLE MIDI devices (warm-up enabled, longer timeouts)
let client = try MIDI2Client(name: "MyApp", preset: .korgBLEMIDI)

// Standard MIDI 2.0 devices (default settings)
let client = try MIDI2Client(name: "MyApp", preset: .standard)

Available presets:

  • .korgBLEMIDI - Optimized for KORG Module Pro and BLE MIDI devices
  • .standard - Default settings for standard MIDI 2.0 devices

UMP Conversion

MIDI2Kit provides bidirectional conversion between MIDI 1.0 SysEx and UMP SysEx7 (Data 64) packets, along with multi-packet reassembly and RPN/NRPN to MIDI 1.0 CC conversion.

MIDI 1.0 SysEx → UMP Data 64

import MIDI2Core

// Convert MIDI 1.0 SysEx to UMP Data 64 packets
let sysex: [UInt8] = [0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7]
let packets = UMPTranslator.fromMIDI1SysEx(sysex)
// Returns array of UMPMessages (automatically chunked into 6-byte payloads)

// Or use factory API
let packets2 = UMP.sysEx7.fromMIDI1(bytes: sysex)

UMP Data 64 → MIDI 1.0 SysEx

// Convert single UMP Data 64 packet to MIDI 1.0 SysEx
let ump: UMPMessage = // ... received UMP packet
if let sysexBytes = UMPTranslator.data64ToMIDI1SysEx(ump) {
    // sysexBytes includes 0xF0 start and 0xF7 end
    print("Converted SysEx: \(sysexBytes)")
}

Multi-Packet SysEx7 Reassembly

For fragmented SysEx messages spanning multiple UMP packets:

// Create assembler (actor-based, thread-safe)
let assembler = UMPSysEx7Assembler()

// Process incoming UMP packets
for packet in receivedPackets {
    if let completeSysEx = await assembler.process(packet) {
        // Received complete SysEx message (includes 0xF0 and 0xF7)
        print("Complete SysEx: \(completeSysEx)")
    }
}

// Timeout handling
if let timedOut = await assembler.popTimedOut() {
    print("Partial SysEx timed out: \(timedOut)")
}

Factory API

Create UMP Data 64 packets with the convenient factory API:

// Single complete packet
let singlePacket = UMP.sysEx7.complete(payload: [0x7E, 0x7F, 0x09, 0x01])

// Multi-packet sequence
let payload = Array(repeating: UInt8(0x42), count: 20) // 20 bytes → 4 packets
let packets = UMP.sysEx7.fromMIDI1(bytes: [0xF0] + payload + [0xF7])
// Returns array of Data 64 UMP messages with appropriate status (start/continue/end)

RPN/NRPN → MIDI 1.0 CC Conversion

Convert UMP RPN/NRPN messages to MIDI 1.0 Control Change approximations:

// Convert RPN to MIDI 1.0 CC sequence
let rpnMessage: UMPMessage = // ... UMP RPN message
let ccMessages = UMPTranslator.rpnToMIDI1ControlChange(rpnMessage)
// Returns array of MIDI 1.0 CC messages: [CC#101, CC#100, CC#6, CC#38]

// Convert NRPN to MIDI 1.0 CC sequence
let nrpnMessage: UMPMessage = // ... UMP NRPN message
let ccMessages = UMPTranslator.nrpnToMIDI1ControlChange(nrpnMessage)
// Returns array of MIDI 1.0 CC messages: [CC#99, CC#98, CC#6, CC#38]

Note: RPN/NRPN conversion is an approximation - UMP uses 32-bit resolution while MIDI 1.0 uses 7-bit (MSB) + 7-bit (LSB). The conversion preserves the 14-bit MSB value for compatibility.

API Reference

MIDI2Client

The main entry point for MIDI 2.0 operations.

Method Description
start() Start discovery and event processing
stop() Stop and clean up all resources
makeEventStream() Get an AsyncStream of events
getDeviceInfo(from:) Get DeviceInfo from a device
getResourceList(from:) Get available PE resources
get(_:from:) Get a PE resource
set(_:data:to:) Set a PE resource

MIDI2Device

Represents a discovered MIDI 2.0 device with cached property access.

Property/Method Description
muid Unique MIDI ID
displayName Human-readable name
supportsPropertyExchange PE capability
manufacturerName Manufacturer (if known)
deviceInfo Cached DeviceInfo (auto-fetched)
resourceList Cached resource list (auto-fetched)
getProperty<T>(_:as:) Type-safe property decoding
invalidateCache() Force fresh fetch on next access

Example:

let device: MIDI2Device = // from .deviceDiscovered event

// Cached access (auto-fetches on first call)
if let info = try await device.deviceInfo {
    print("Product: \(info.productName ?? "Unknown")")
}

// Type-safe property access
struct CustomProperty: Codable {
    let value: String
}
if let prop = try await device.getProperty("X-Custom", as: CustomProperty.self) {
    print("Custom: \(prop.value)")
}

MIDI2ClientEvent

Events emitted by the client.

Event Description
.deviceDiscovered(device) New device found
.deviceLost(muid) Device disconnected
.deviceUpdated(device) Device info updated
.notification(notification) PE subscription notification
.started / .stopped Client lifecycle

Logging

MIDI2Kit uses os.Logger for efficient logging:

// Disable all logs
MIDI2Logger.isEnabled = false

// Enable verbose logging
MIDI2Logger.isVerbose = true

Filter logs in Console.app:

subsystem == "com.midi2kit"

Debugging & Diagnostics

MIDI2Client provides diagnostic tools for troubleshooting:

// Get comprehensive diagnostics
let diag = await client.diagnostics
print(diag)

// Check destination resolution details
if let destDiag = await client.lastDestinationDiagnostics {
    print("Tried destinations: \(destDiag.triedOrder)")
    print("Resolved to: \(destDiag.resolvedDestination)")
}

// View last communication trace
if let trace = await client.lastCommunicationTrace {
    print(trace.description)
    // Shows: operation, MUID, resource, duration, result, errors
}

Migration from Low-Level API

If you're using CIManager or PEManager directly, see the Migration Guide for step-by-step instructions to migrate to MIDI2Client.

Recent Updates

v1.0.17 (2026-02-13)

  • High-Level Responder API: MIDI2ResponderClient actor for easy creation of MIDI-CI Responders
  • Connection Policy: MIDI2ConnectionPolicy with MatchRule-based filtering (.contains(), .exact(), .prefix(), .suffix())
  • Notification Throttling: PEResponder.minNotifyInterval to prevent notification storms (default: 50ms)
  • ListResource auto-totalCount: Automatic totalCount header in list responses
  • ComputedResource typed initializers: Type-safe getTyped/setTyped Codable handlers
  • 602 Tests: Comprehensive test coverage (+38 from v1.0.15)
// Create responder with typed resources
let responder = try MIDI2ResponderClient(name: "MyResponder")
await responder.addResource("Temperature") {
    ComputedResource(getTyped: { _ in
        TemperatureReading(celsius: getCurrentTemp())
    })
}

// Filter connections
var config = MIDI2ClientConfiguration()
config.connectionPolicy = MIDI2ConnectionPolicy(
    allowedNames: [.prefix("iPad"), .contains("Pro")]
)
try await responder.start()

v1.0.16 (2026-02-12)

  • Superseded by v1.0.17 (rebase integration fix)

v1.1.0 (2026-02-07)

  • UMP SysEx7 Bidirectional Conversion: MIDI 1.0 SysEx ↔ UMP Data 64 packet conversion with UMPTranslator
  • Multi-Packet SysEx7 Reassembly: Actor-based UMPSysEx7Assembler for fragmented SysEx messages with timeout handling
  • RPN/NRPN → MIDI 1.0 CC Conversion: Convert UMP RPN/NRPN to MIDI 1.0 Control Change approximations
  • Factory API: Convenient UMP.sysEx7.* methods for creating Data 64 packets
  • 564 Tests: Comprehensive test coverage across 77 test suites

v1.0.9 (2026-02-06)

  • KORG ChannelList/ProgramList Auto-Conversion: Auto-convert KORG proprietary format (bankPC: [Int] array) to standard format
  • New APIs: getChannelList(), getProgramList() - Auto-detect vendor and select appropriate resource

v1.0.8 (2026-02-06)

  • KORG Optimization: Skip ResourceList, directly fetch X-ParameterList (99% faster, 16.4s → 144ms)
  • Adaptive WarmUp Strategy: Learn success/failure per device, auto-select optimal warmup strategy
  • KORG Extension APIs: getOptimizedResources(), getXParameterList(), getXProgramEdit()

v1.0.7 (2026-02-06)

  • AsyncStream fixes: Race condition fixes in CoreMIDITransport, MockMIDITransport, LoopbackTransport, PESubscriptionManager

v1.0.6 (2026-02-06)

  • Critical bug fix: Fixed CIManager.events not firing (AsyncStream continuation race condition)

See CHANGELOG.md for details.

Changelog

See CHANGELOG.md for version history and recent updates.

Known Limitations

KORG Module Pro

  • DeviceInfo: ✅ Works reliably
  • ResourceList: ⚠️ May timeout due to chunk loss (physical layer limitation)
  • Auto-Retry: ✅ MIDI2Client automatically retries with fallback destinations

KORG devices use a non-standard PE format. MIDI2Kit handles this automatically, but multi-chunk responses (like ResourceList) may be unreliable over BLE MIDI due to packet loss.

Built-in optimizations (when using .korgBLEMIDI preset):

  • Warm-up request before ResourceList to establish stable BLE connection
  • Automatic destination fallback on timeout
  • Extended timeout for multi-chunk responses

Workaround (if ResourceList still fails):

// Access known resources directly instead of using ResourceList:
let response = try await client.get("CMList", from: device.muid)
let response = try await client.get("ChannelList", from: device.muid)

Architecture

MIDI2Kit is organized into 5 modular Swift Package Manager targets with clear dependency hierarchy:

MIDI2Core (Foundation - no dependencies)
    ↑
    ├─ MIDI2Transport (CoreMIDI abstraction)
    ├─ MIDI2CI (Capability Inquiry / Discovery)
    ├─ MIDI2PE (Property Exchange)
    └─ MIDI2Kit (High-Level API)

Module Details

Module Purpose Key Types
MIDI2Core Foundation types, UMP messages, constants, bidirectional conversion MUID, DeviceIdentity, UMPMessage, UMPTranslator, UMPSysEx7Assembler, Mcoded7
MIDI2Transport CoreMIDI integration with connection management CoreMIDITransport, MIDITransport, SysExAssembler
MIDI2CI MIDI Capability Inquiry protocol (device discovery) CIManager, DiscoveredDevice, CIMessageParser
MIDI2PE Property Exchange (GET/SET device properties) PEManager, PETransactionManager, PESubscriptionManager
MIDI2Kit High-Level API - unified client for common use cases MIDI2Client, MIDI2ResponderClient, MIDI2Device, MIDI2ConnectionPolicy

Actor-Based Concurrency: All managers are actor types for thread-safe isolation. All data types are Sendable. Swift Concurrency (async/await) is used throughout.

Request ID Management: PE supports max 128 concurrent requests (0-127) with automatic ID allocation, per-device inflight limiting, and request ID cooldown to prevent delayed response mismatches.

Testing

MIDI2Kit includes comprehensive tests covering:

  • Unit Tests: 602 tests across 77+ test suites for individual components
  • Integration Tests: End-to-end workflow tests including discovery, PE operations, error recovery
  • UMP Conversion Tests: Bidirectional SysEx conversion, multi-packet reassembly, RPN/NRPN conversion
  • Real Device Tests: Verified with KORG Module Pro and BLE MIDI devices

Run tests with:

swift test

Security

MIDI2Kit follows secure coding practices:

  • ✅ Swift 6 strict concurrency mode
  • ✅ Actor isolation for thread safety
  • ✅ Input validation (MUID, Mcoded7, PE requests)
  • ✅ Buffer size limits (DoS prevention)
  • ✅ Structured error handling with classification
  • ✅ Minimal external dependencies

See docs/security-audit-20260204.md for detailed security audit report.

License

MIT License - see LICENSE file for details.

Contributing

Contributions welcome! Please open an issue first to discuss proposed changes.

Additional Resources

About

Pre-built XCFramework binaries for MIDI2Kit - Swift MIDI 2.0 Library

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors