Proposal
Simple API to facilitate piping MIDI events between objects.
Initially, this would help simplify the syntax required to attach MIDI endpoints from the I/O layer to MIDIKit Extensions abstraction classes (HUI, MTC, etc.).
This would likely take one of two possible implementation approaches:
- synthesize the required handlers to pass MIDI Events in and out of modules (internally using weak references to the module classes being attached), removing the need for the library consumer to implement manual handler or closure code
or
- act as a mechanism to store weak references to the actual class being attached
API
Protocols
public protocol ReceivesMIDIEvents {
func midiIn(event: MIDI.Event)
func midiIn(events: [MIDI.Event])
}
extension ReceivesMIDIEvents {
public func midiIn(events: [MIDI.Event]) {
for event in events {
midiIn(event: event)
}
}
}
public protocol SendsMIDIEvents {
/// Handler used when calling `midiOut()` methods.
typealias MIDIOutHandler = ((_ events: [MIDI.Event]) -> Void)
/// Handler used when calling `midiOut()` methods.
var midiOutHandler: MIDIOutHandler? { get set }
}
extension SendsMIDIEvents {
internal func midiOut(_ event: MIDI.Event) {
midiOutHandler?([event])
}
internal func midiOut(_ events: [MIDI.Event]) {
midiOutHandler?(events)
}
}
Protocol Adoptions
extension MIDI.IO.Input: ReceivesMIDIEvents { }
extension MIDI.IO.Output: SendsMIDIEvents { }
extension MIDI.IO.InputConnection: ReceivesMIDIEvents { }
extension MIDI.IO.OutputConnection: SendsMIDIEvents { }
Implementation
extension ReceivesMIDIEvents { // protocol
public func attach(to module: ReceivesMIDIEvents) {
// ...
}
public func removeAttachments() {
// ...
}
}
extension SendsMIDIEvents { // protocol
public func attach(to module: SendsMIDIEvents) {
// ...
}
public func removeAttachments() {
// ...
}
}
Changes to MIDI.IO.Manager
.addInput()
and .addInputConnection()
API would change slightly:
- The method parameter
receiveHandler: MIDI.IO.ReceiveHandler
would become an Optional. If passed nil
, no receive handler would initially be instantiated and received MIDI packets would not be handled (at that point).
- A new method parameter
attachTo: ReceivesMIDIEvents
would be added which would facilitate the creation of the managed Input/InputConnection as usual, but also attach a receiver object at the same time.
Considerations
Multiple MIDI pipes in a module
It's conceivable that a future MIDIKit module (or custom module developed by a library user) may make use of more than one input or output pipe. A possible future-proofing solution would be to add a property to specify which endpoint of a module to attach to when invoking the .attach()
method.
Solution 1
A simple index system, starting at 0. If there is only one applicable endpoint, 0 or nil could be passed:
// SendsMIDIEvents protocol
func attach(to module: ReceivesMIDIEvents, index: Int? = nil)
Solution 2
A a new value type MIDIAttachment
, giving type-safe names to each internal MIDI pipe.
public struct MIDIAttachment {
public static var `default`: Self { MIDIAttachment("default") }
}
// SendsMIDIEvents protocol
func attach(to module: ReceivesMIDIEvents, port: MIDIAttachment = .default)
Ideally there would be a mechanism to strongly-type these identifiers to each specific MIDIKit module and not have them share the same global MIDIAttachment
namespace.
Solution 3
Modules contain as many properties representing each internal MIDI "port" that they require.
Therefore, existing proposal logic could still apply without requiring an index or key system to identify them when calling .attach()
And because they are properties, they have carry with them the ability to have descriptive names and are strongly-typed.
class SomeModule {
var port1 // conforms to ReceivesMIDIEvents
var port2 // conforms to ReceivesMIDIEvents
}
This could, however, introduce more complexity than desired, as reference semantics would be required between SomeModule
and those properties, which will be less than desirable if the module's logic exists in the module (which it invariably will).