Architecture Overview
How MIDI2Kit's modules work together and the design principles behind the library.
Module Overview
┌─────────────────────────────────────────────────────────────┐
│ Your Application │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ MIDI2Kit │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ CIManager │──│ PEManager │ │ CIManagerPEExtension│ │
│ │ (Discovery)│ │ (Property │ │ (Convenience) │ │
│ └─────────────┘ │ Exchange) │ └─────────────────────┘ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ MIDI2CI │ │ MIDI2PE │ │ MIDI2Core │
│ Discovery │ │ Property │ │ UMP Types │
│ Messages │ │ Exchange │ │ MUID │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└─────────────────────┼─────────────────────┘
▼
┌─────────────────┐
│ MIDI2Transport │
│ (MIDITransport │
│ Protocol) │
└─────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ CoreMIDITransport│ │ MockMIDITransport│
│ (Production) │ │ (Testing) │
└─────────────────┘ └─────────────────┘
│
▼
┌───────────┐
│ CoreMIDI │
│ (Apple) │
└───────────┘
Layer Architecture
Layer 1: Foundation (MIDI2Core)
Zero dependencies. Contains pure data types and utilities:
- UMP Types: Message types, status codes, groups, channels
- MUID: MIDI Unique Identifier (28-bit)
- DeviceIdentity: Manufacturer, family, model, version
- Value Scaling: MIDI 1.0 ↔ 2.0 conversion
- Mcoded7: 8-bit to 7-bit encoding
Layer 2: Transport (MIDI2Transport)
Abstraction layer for MIDI I/O:
- MIDITransport Protocol: Common interface for all implementations
- CoreMIDITransport: Production implementation using Apple's CoreMIDI
- MockMIDITransport: Test implementation for unit testing
Layer 3: Protocol (MIDI2CI, MIDI2PE)
MIDI-CI protocol implementation:
- MIDI2CI: Discovery, message building/parsing
- MIDI2PE: Property Exchange, chunking, subscriptions
Layer 4: High-Level API (MIDI2Kit)
User-facing convenience layer:
- CIManager: Automatic device discovery and lifecycle
- PEManager: Async/await Property Exchange API
- CIManagerPEExtension: Convenience methods combining CI + PE
Data Flow
Outgoing (Send)
Application
│
│ peManager.get("DeviceInfo", from: muid)
▼
PEManager
│ Build PE Get Inquiry message
│ Allocate Request ID
│ Set up timeout
▼
CIMessageBuilder
│ Build SysEx bytes
▼
MIDITransport.send()
│
▼
CoreMIDI / Mock
Incoming (Receive)
CoreMIDI / Mock
│
│ MIDIReceivedData
▼
MIDITransport.received (AsyncStream)
│
▼
CIManager / PEManager
│ Parse message type
│ Route to handler
▼
CIMessageParser
│ Extract fields
│ Validate MUID
▼
Handler
│ Discovery: Update device list, emit event
│ PE Reply: Assemble chunks, resume continuation
▼
Application (via async/await or event stream)
Design Decisions
Actor-based Concurrency
Both CIManager and PEManager are actors, providing:
- Thread-safe state management without explicit locks
- Clear ownership of mutable state
- Natural integration with async/await
Protocol-based Transport
The MIDITransport protocol enables:
- Testing without real MIDI hardware
- Future support for other transports (USB, network)
- Clean separation of concerns
Destination Resolver Pattern
PEManager uses a resolver closure to decouple from CIManager:
- PEManager doesn't depend on CIManager directly
- Allows custom resolution strategies
- Enables testing with mock destinations
Event Streams vs. Delegates
We use AsyncStream instead of delegates because:
- Better integration with Swift's structured concurrency
- No callback hell or retain cycles
- Natural for-await-in syntax
Single Source of Truth
Responsibilities are clearly separated:
- PEManager: Timeouts, continuations, response delivery
- PETransactionManager: Request ID allocation, chunk assembly
- CIManager: Device lifecycle, MUID → destination mapping