MIDI2Transport
Transport ModuleMIDI I/O abstraction layer. Production CoreMIDI implementation and MockTransport for testing.
MIDITransport Protocol
The core protocol that abstracts MIDI I/O operations. Both production and test implementations conform to this protocol.
public protocol MIDITransport: Sendable
Required Methods
| Method | Description |
|---|---|
send(_:to:) | Send MIDI data to a destination |
connect(to:) | Connect to a MIDI source |
disconnect(from:) | Disconnect from a source |
connectToAllSources() | Connect to all available sources |
findMatchingDestination(for:) | Find destination for a source (same entity) |
shutdown() | Shut down and finish all streams |
Required Properties
| Property | Type | Description |
|---|---|---|
received | AsyncStream<MIDIReceivedData> | Stream of incoming MIDI data |
sources | [MIDISourceInfo] | Available MIDI sources |
destinations | [MIDIDestinationInfo] | Available MIDI destinations |
setupChanged | AsyncStream<Void> | Setup change notifications |
CoreMIDITransport
Production implementation using Apple's CoreMIDI framework.
public class CoreMIDITransport: MIDITransport
// Create transport
let transport = try CoreMIDITransport(clientName: "MyApp")
// Connect to all sources
try await transport.connectToAllSources()
// Send data
let data: [UInt8] = [0xF0, 0x7E, 0x7F, ...]
try await transport.send(data, to: destinationID)
// Receive data
for await received in transport.received {
print("Received \(received.data.count) bytes from \(received.sourceID)")
}
// Handle setup changes (device connected/disconnected)
for await _ in transport.setupChanged {
print("MIDI setup changed")
// Refresh device list
}
⚠️ Important: MIDISourceID and MIDIDestinationID are session-scoped handles, not persistent IDs. They may change across reboots or device reconnections. For persistent identification, use uniqueID from endpoint info.
LoopbackTransport
In-process loopback transport for testing initiator-responder communication without CoreMIDI. New in v1.0.5.
public actor LoopbackTransport: MIDITransport
// Create a loopback pair
let (initiatorTransport, responderTransport) = LoopbackTransport.createPair()
// Messages sent on one transport are received on the other
try await initiatorTransport.send(data, to: destinationID)
// → responderTransport.received will emit this data
try await responderTransport.send(replyData, to: destinationID)
// → initiatorTransport.received will emit this data
Use Cases
| Use Case | Description |
|---|---|
| MockDevice Testing | Test MIDI-CI initiator ↔ responder without hardware |
| Unit Tests | Fast, deterministic MIDI message testing |
| Integration Tests | Test full discovery → PE flow in isolation |
With MockDevice
let (initiatorTransport, responderTransport) = LoopbackTransport.createPair()
// Create mock device on responder side
let mockDevice = MockDevice(transport: responderTransport, preset: .korgModulePro)
try await mockDevice.start()
// Create managers on initiator side
let ciManager = CIManager(transport: initiatorTransport, ...)
let peManager = PEManager(transport: initiatorTransport, ...)
try await ciManager.start()
// Discovery and PE operations work seamlessly
await ciManager.startDiscovery()
let response = try await peManager.get("DeviceInfo", from: mockDevice.handle)
MockMIDITransport
Mock implementation for unit testing without real MIDI hardware.
public actor MockMIDITransport: MIDITransport
// Create mock transport
let mockTransport = MockMIDITransport()
// Add mock endpoints
await mockTransport.addMockSource(MIDISourceInfo(
sourceID: MIDISourceID(1),
name: "Test Device",
uniqueID: 12345
))
await mockTransport.addMockDestination(MIDIDestinationInfo(
destinationID: MIDIDestinationID(1),
name: "Test Device",
uniqueID: 12345
))
// Simulate receiving data
await mockTransport.simulateReceive(
[0xF0, 0x7E, 0x7F, ...],
from: MIDISourceID(1)
)
// Check sent messages
let sent = await mockTransport.sentMessages
XCTAssertEqual(sent.count, 1)
XCTAssertEqual(sent[0].data, expectedData)
Test Helpers
| Method | Description |
|---|---|
simulateReceive(_:from:) | Simulate incoming MIDI data |
addMockSource(_:) | Add a mock MIDI source |
addMockDestination(_:) | Add a mock MIDI destination |
sentMessages | Array of all sent messages for verification |
clearSentMessages() | Clear the sent messages array |
Endpoint Types
MIDISourceID / MIDIDestinationID
Type-safe wrappers for CoreMIDI endpoint references.
public struct MIDISourceID: Sendable, Hashable {
public let value: UInt32
}
public struct MIDIDestinationID: Sendable, Hashable {
public let value: UInt32
}
MIDISourceInfo / MIDIDestinationInfo
Endpoint metadata including name, manufacturer, and persistent ID.
| Property | Type | Description |
|---|---|---|
sourceID / destinationID | MIDISourceID / MIDIDestinationID | Session-scoped handle |
name | String | Endpoint display name |
manufacturer | String? | Manufacturer name |
isOnline | Bool | True if currently available |
uniqueID | Int32? | Persistent ID across sessions |
MIDIReceivedData
Incoming MIDI data with source information.
public struct MIDIReceivedData: Sendable {
public let data: [UInt8] // Raw MIDI bytes
public let sourceID: MIDISourceID? // Source endpoint
public let timestamp: UInt64 // CoreMIDI timestamp
}
Error Types
public enum MIDITransportError: Error, Sendable
| Case | Description |
|---|---|
notInitialized | Transport not initialized |
clientCreationFailed(Int32) | Failed to create MIDI client |
portCreationFailed(Int32) | Failed to create MIDI port |
sendFailed(Int32) | Failed to send MIDI data |
connectionFailed(Int32) | Failed to connect to source |
destinationNotFound(UInt32) | Destination endpoint not found |
sourceNotFound(UInt32) | Source endpoint not found |