Progress Update: MTC Input, and a MIDI package for Swift
(Note: this post will be pretty code-heavy, because it's the first area where I've had to deal with anything outside the realm of reasonably idiomatic Swift. If you're not interested in code, have a random cat GIF instead, with my compliments.)
(Also note: if you are interested in code, and just want a tl;dr link to a Swift MIDI implementation, fine, here you go.)
Continuing on the theme of synchronization infrastructure, I've just finished the guts of the first bit of external sync infrastructure: an MTC reader class. I'm building this up from scratch, rather than using CoreAudio's built-in MTC capabilities, because I want to have somewhat more fine-grained control over things like differentiating between video speed and film speed (e.g. 23.976 vs 24), and to be able to dig in deeper when things don't behave exactly as I expect.
Of course, in order to write an MTC reader, I needed first to get MIDI I/O working. This has proven to be a more arduous task than I was expecting, because Core MIDI does not (currently) have a nice Swift interface; instead, it has only the default mappings from C to Swift. Whereas some of Apple's C APIs have gotten nice syntactic help (CoreFoundation types, for example, feel almost like native Swift classes, and even get managed by ARC), Core MIDI is a much more old-school API to start with, even when developing in C.
One of the first signs that Core MIDI is old-school, even by C standards, is the type of the MIDIPacket
struct. Here it is in the C header, MIDIServices.h:
struct MIDIPacket
{
MIDITimeStamp timeStamp;
UInt16 length;
Byte data[256];
};
typedef struct MIDIPacket MIDIPacket;
They've gone to the lengths of making data
a fixed-size array, to ensure that it avoids any heap allocation. Unfortunately, because Swift imports fixed C arrays as tuples, the imported interface looks like this in Swift:
public struct MIDIPacket {
public var timeStamp: MIDITimeStamp
public var length: UInt16
public var data: (
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
... yikes. 😱
This makes sense from a type safety standpoint, but boy is it annoying. Most grievously, accessing a particular byte within that tuple requires accessing the tuple element, as in packet.data.3
. It does not work to access them through subscripts, as in packet.data[3]
, which crucially means that there's no way to loop through them or determine which element to access programmatically. That's a drag, since a packet can (hypothetically) contain multiple sequential MIDI messages of varying lengths—for example, a 3-byte note-on starting at data.0
, followed by a 7-byte SysEx message starting at data.3
, and then a 1-byte MIDI Clock message at data.10
, etc. (This doesn't seem to happen in practice, as far as I can tell, but it's possible in theory.)
Apple has gone so far as to make a few new Swift-only types available starting in Catalina (e.g. MIDIPacket.Builder
, MIDIPacket.ByteSequence
, MIDIPacket.ByteCollection
), which appear to be aimed toward making those data bytes accessible through reasonably sane means such as subscripting. However, the documentation of these new types is scant, with just a few comments apparently copied and pasted from somewhere unrelated, and it's unclear how to actually access them—there's no byteSequence
property on MIDIPacket
, for example, and no initializer on MIDIPacket.ByteSequence
that can accept a MIDI packet or its data—so the promise of accessing data bytes with subscripting seems to be just a tease at this point. As far as I can make out, these types look like either a work in progress, or some types created for Apple's internal use that got accidentally included in the Swift interface.
So what to do instead? I need this to be as fast as possible, and the benefit of the C version is that the data is just a contiguous chunk of memory. So rather than constructing an array from packet.data.0, packet.data.1, ..., packet.data.255
, I ended up settling on a solution that steps briefly outside of Swift's memory safety and accesses the C type a bit more directly. Here's the bit that casts the giant tuple to a Swift array:
var d = packet.data
var bytes = [UInt8](UnsafeBufferPointer(start: &d.0, count: 256))
And back:
let data = withUnsafeBytes(of: &bytes[0]) { (rawPtr) -> LMIDIByteTuple256 in // Note: LMIDIByteTuple256 is a typealias for that ungodly tuple
let ptr = rawPtr.baseAddress!.assumingMemoryBound(to: LMIDIByteTuple256.self)
return ptr.pointee
}
Both of those use the memory address of just the first element, and blindly trust that there are exactly 256 elements. Thankfully, this is guaranteed by Core MIDI's API, and I could make the same guarantee from the Swift side by only doing this conversion in one place.
LMIDI
The fact that Core MIDI still comes across so clunkily leads me to suspect that Apple has something else in the works for a more Swift-y approach to MIDI I/O. In the hopes that such a thing is in the works, and in order to avoid polluting more code than necessary with insane types and unsafe pointers, I ended up building a small set of MIDI-related Swift types that enapsulate the MIDI functionality I need for basic input and output, which I have split out into an oepn-source package called LMIDI. LMIDI isn't a fully-fledged wrapper for Core MIDI, but it covers enough to be able to take input and produce output, with a reasonably succinct syntax:
guard let src = LMIDIConfig.shared(name: "Lapis").inputSources.first else { return }
self.input = LMIDIInput(source: src, portName: "Input 1")
self.input?.listen { messages in
print("Messages received: \(messages)")
}
print("Listening on \(src.name)...")
guard let dest = LMIDIConfig.shared(name: "Lapis").outputDestinations.first else { return }
self.output = LMIDIOutput(destination: dest, portName: "Output 1")
self.output?.send(.noteOn(channel: 4, note: 10, velocity: 75))
If, someday, Apple introduces a whole new Swift API for MIDI, then hopefully this encapsulation will make it possible to swap that in without needing much, if anything, in the way of changes elsewhere. For now, getting dead-simple MIDI I/O has been enough of a pain that I hope this turns out to be useful for somebody.