Historically, if you wanted to receive MIDI events using CoreMIDI, you would have to create a MIDI input port using a call to MIDIInputPortCreateWithBlock
and then parse the incoming MIDI packets in the block-based callback. This would get complicated quickly because you would have to write logic for parsing MIDI packets. For example, to extract “note on” events, you would have to look for events that start with the byte 0x90
and are also followed by two bytes for the note value and velocity, respectively. You would also have to handle situations where multiple MIDI events arrive in one packet, and so on.
As of macOS 12 and iOS 15, this became easier with the introduction of the little-known MIDIEventListForEachEvent
API. To use it, we create a MIDI 1.0 input port with the newer MIDIInputPortCreateWithProtocol
function (and MIDI 2.0 is also supported):
MIDIInputPortCreateWithProtocol(client,
name,
MIDIProtocolID._1_0,
&inputPort) { list, _ in
...
The block here gives us a UnsafePointer<MIDIEventList>
pointer to the event list (as opposed to a pointer to MIDIPacket
s in the older API). The MIDIEventListForEachEvent
function lets us iterate over the list by invoking a visitor function that will get called for every MIDI event. Unfortunately, it takes a C callback function that follows the C function pointer calling convention (public typealias MIDIEventVisitor = @convention(c) ...
). If you simply try capturing self
in a block passed to MIDIEventListForEachEvent
, you’ll get an error:
MIDIEventListForEachEvent(list, { _, timestamp, message in
^^^^ A C function pointer cannot be formed from a closure that captures context
self.doSomething(message, timestamp)
}, nil)
This makes it harder to use this in situations where we want to capture surrounding context, which is probably most cases, so we created a more Swift-friendly wrapper that makes this easier:
import CoreMIDI
import Foundation
typealias MIDIForEachBlock = (MIDIUniversalMessage, MIDITimeStamp) -> Void
private final class MIDIForEachContext {
var block: MIDIForEachBlock
init(block: @escaping MIDIForEachBlock) {
self.block = block
}
}
func MIDIEventListForEach(_ list: UnsafePointer<MIDIEventList>, _ block: MIDIForEachBlock) {
withoutActuallyEscaping(block) { escapingClosure in
let context = MIDIForEachContext(block: escapingClosure)
withExtendedLifetime(context) {
let contextPointer = Unmanaged.passUnretained(context).toOpaque()
MIDIEventListForEachEvent(list, { contextPointer, timestamp, message in
guard let contextPointer else {
return
}
let localContext = Unmanaged<MIDIForEachContext>.fromOpaque(contextPointer).takeUnretainedValue()
localContext.block(message, timestamp)
}, contextPointer)
}
}
}
In a nutshell, this code wraps a regular Swift closure in a class that gets passed as the “context” pointer to MIDIEventListForEachEvent
. Inside of the C callback function, we then unwrap the context and invoke our Swift closure; this one is not restricted with @convention(c)
, so we can capture surrounding context just fine. It will get invoked with two parameters for every MIDI event: a MIDIUniversalMessage
and MIDITimeStamp
. Using it is as simple as switching on the message type and looking at the nested union types. Here, we look for a message type of channelVoice1
and status of noteOn
.
MIDIEventListForEach(list) { message, timestamp in
switch message.type {
case .channelVoice1:
switch message.channelVoice1.status {
case .noteOn:
self.handleNoteOn(message.channelVoice1.note.number,
velocity: message.channelVoice1.note.velocity,
timestamp: timestamp)
...
We can now trivially obtain the MIDI note value as well as its velocity, all without having to parse MIDI packets ourselves.