Playing back incoming MIDI through sampler

Part of the AVFoundation framework, Apple’s Sampler Audio Unit allows us to play back musical notes out of preloaded sounds. Together with CoreMIDI we can use it as output to an external MIDI keyboard.

We set up a sampler by calling its default initializer and optionally loading a sound bank instrument:

var sampler = AVAudioUnitSampler()
let soundbank = Bundle.main.url(forResource: "FluidR3 GM2-2", withExtension: "SF2")
let melodicBank:UInt8 = UInt8(kAUSampler_DefaultMelodicBankMSB)
let gmHarpsichord:UInt8 = 6
try! sampler.loadSoundBankInstrument(at: soundbank!, program: gmHarpsichord, bankMSB: melodicBank, bankLSB: UInt8(kAUSampler_DefaultBankLSB))

For us to be able to hear it we attach the sampler to a sound engine, connect it to a mixer node and finally start the engine:

var engine = AVAudioEngine()
engine.attach(sampler)
engine.connect(sampler, to: engine.mainMixerNode, format: nil)
try! engine.start()

The sampler is itself a MIDI instrument and as such can receive arbitrary events like notes, pitch bend and channel pressure. But first we need to capture those messages from the external source.

The process for receiving MIDI is simple: we create an input port, discover the source and connect the two together:

var midiClient = MIDIClientRef()
var inputPort = MIDIPortRef()
let _ = MIDIClientCreateWithBlock("com.diegolavalle.SamplerOutput.MidiClient" as CFString, &midiClient, nil)
let _ = MIDIInputPortCreateWithBlock(midiClient, "com.diegolavalle.SamplerOutput.MidiInputPort" as CFString, &inputPort) { ... packet parsing }
for sourceIndex in 0 ..< MIDIGetNumberOfSources() {
let _ = MIDIPortConnectSource(inputPort, MIDIGetSource(sourceIndex), nil)
}

Unfortunatelly a MIDI instrument audio unit does not conform to the end-point interface based on packet lists so they it cannot be used directly as a destination. Instead we need to manually parse each packet before sending individual messages to the sampler.

First we process the packet list in our input port’s read block:

(packetsPointer, _) in
let packets = packetsPointer.pointee
var packetPointer = UnsafeMutablePointer<MIDIPacket>.allocate(capacity: 1)
packetPointer.initialize(to: packets.packet)
for _ in 0 ..< packets.numPackets {
self.parsePacket(packetPointer.pointee) // P
packetPointer = MIDIPacketNext(packetPointer)
}

Each packet requires special treatment as it can contain multiple events encoded in a fixed-size tuple:

// The next line converts a tuple into an array
let packetBytes = Mirror(reflecting: packet.data).children.map({$0.value}) as! [UInt8]
var previousStatus:UInt8! = nil
var messageData:[UInt8] = []
var i = 0 // Prepare to iterate the array and extract the MIDI messages
while i <= packetBytes.count {
if i == packet.length || isStatusByte(packetBytes[i]) {
... forward message to sampler
}
if i == packet.length {
break // We have reached the end of the packet and sent the last message
}
if isStatusByte(packetBytes[i]) {
previousStatus = packetBytes[i]
messageData = []
} else { // Is a data byte, save it
messageData.append(packetBytes[i])
}
i += 1
}

Once we have collected enough information for a discrete MIDI event we can forward the data to the sampler using the appropriate instance method:

switch messageData.count {
case 1:
sampler.sendMIDIEvent(previousStatus, data1: messageData[0])
case 2:
sampler.sendMIDIEvent(previousStatus, data1: messageData[0], data2: messageData[1])
default: break
}

We should be able to hear the result immediately without delay.

Full source code below.

Show your support

Clapping shows how much you appreciated Diego Lavalle’s story.