Swift CoreMIDI Callbacks

The following was tested with Swift 2.1.1.  I am far from a Swift expert.  I do use “;” and “()” in Swift sometimes even though not necessary.  This is because I develop in many languages and the code flows from my fingers this way.

Suppose you have a Cocoa app with a ViewController and you want to update the UI when a MIDI source or destination is added or removed, or a MIDI packet is received. There are a few snags along the way that you may encounter. CoreMIDI provides references for the application to pass in that can be used in the callbacks. But converting between Swift and C structs may not be intuitively obvious. Another difficulty is casting MIDINotification to MIDIObjectAddRemoveNotification. If this isn’t done properly the bits will get converted but the actual data in the MIDIObjectAddRemoveNotification struct will be trash. This is how I handled callbacks in Swift.

First I created a protocol that my ViewController implemented so that casting would be a little cleaner.

protocol MIDICallbackHandler {
    func processMidiPacket(packet: MIDIPacket,  msgSrc: MIDIEndpointRef) -> Void;
    func processMidiObjectChange(message: MIDIObjectAddRemoveNotification) -> Void;
}

Then I implemented the callbacks as global Swift functions. The bridgeMutable() and unbridgeMutable() utility functions were based on information I found on stackoverflow.com

func bridgeMutable(obj : T) -> UnsafeMutablePointer {
    return UnsafeMutablePointer(Unmanaged.passUnretained(obj).toOpaque())
}

func unbridgeMutable(ptr : UnsafeMutablePointer) -> T {
    return Unmanaged.fromOpaque(COpaquePointer(ptr)).takeUnretainedValue()
}

func MIDIUtil_MIDIReadProc(pktList: UnsafePointer,
        readProcRefCon: UnsafeMutablePointer, srcConnRefCon: UnsafeMutablePointer) -> Void
{
    let src:MIDIEndpointRef = UnsafeMutablePointer(COpaquePointer(srcConnRefCon)).memory;

    let packetList:MIDIPacketList = pktList.memory;
    
    let h:AnyObject = unbridgeMutable(readProcRefCon);
    let handler:MIDICallbackHandler = h as! MIDICallbackHandler;
    
    var packet:MIDIPacket = packetList.packet;
    for _ in 1...packetList.numPackets
    {
        handler.processMidiPacket(packet, msgSrc: src);
        packet = MIDIPacketNext(&packet).memory;
    }
}

func MIDIUtil_MIDINotifyProc(message: UnsafePointer, refCon: UnsafeMutablePointer) -> Void
{
    let notification:MIDINotification = message.memory;
    
    if (notification.messageID == .MsgObjectAdded || notification.messageID == .MsgObjectRemoved)
    {
        let msgPtr:UnsafePointer = UnsafePointer(message);
        let changeMsg:MIDIObjectAddRemoveNotification = msgPtr.memory;
        let h:AnyObject = unbridgeMutable(refCon);
        let handler:MIDICallbackHandler = h as! MIDICallbackHandler;
        handler.processMidiObjectChange(changeMsg);
    }
}

Finally the view controller was implemented using the following code snippets. Note that another utility function invokeLater is used so that the UI can be updated off of the high priority CoreMIDI event thread.


func invokeLater(codeBlock: dispatch_block_t) {
    dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
        dispatch_async(dispatch_get_main_queue(),codeBlock)
    })
}

class ViewController: NSViewController, MIDICallbackHandler {
...
    var midiClient: MIDIClientRef = 0;
    var inPort:MIDIPortRef = 0;
...
    override func viewDidLoad() {
        super.viewDidLoad()
...         
        // These Functions use the global callbacks and pass a reference to this class instance
        MIDIClientCreate("MidiTest1Client", MIDIUtil_MIDINotifyProc, bridgeMutable(self), &midiClient);
        MIDIInputPortCreate(midiClient, "MidiTest1_InPort", MIDIUtil_MIDIReadProc, bridgeMutable(self), &inPort);
    }
...

    // MIDI Callback Handler for packet receipt
    // In this case I had an NSTableView named monitorTable I was updating
    func processMidiPacket(packet: MIDIPacket, msgSrc: MIDIEndpointRef) -> Void {
        dataSource.append((packet: packet, msgSrc: msgSrc));
        
        // Don't Update UI on this thread!
        invokeLater({
            self.monitorTable.reloadData();
            self.monitorTable.scrollToEndOfDocument(self);
        });
    }
    
    // MIDI Callback Handler for source or destination changes
    func processMidiObjectChange(message: MIDIObjectAddRemoveNotification) -> Void {
        // print("MIDI Notification ChildObjectType: \(message.childType.rawValue) ");

        switch (message.messageID)
        {
            case .MsgObjectAdded:
                if (message.childType == MIDIObjectType.Destination || message.childType == MIDIObjectType.Source)
                {
                    let src:MIDIEndpointRef = message.child;
                    print("MIDI Source/Destination Added: \(MIDIUtil.getDisplayName(src))");
                }
                else
                {
                    print("MIDI Notification Object Added MsgObjectType: \(message.childType.rawValue) ");
                }
            case .MsgObjectRemoved:
                if (message.childType == MIDIObjectType.Destination || message.childType == MIDIObjectType.Source)
                {
                    let x:MIDIEndpointRef = message.child;
                    // Note: you can't get displayname if object is removed!
                    if (x == currDest)
                    {
                        print("Current Destination Removed")
                        invokeLater( {
                            self.showAlert("MIDI Destination Change", infoText:"The current MIDI destination has been removed!")
                        });
                    }
                }
                else
                {
                    print("MIDI Notification Object Removed MsgObjectType: \(message.childType.rawValue) ");
                }
           
            default:
               print("processMidiNotification messageID:\(message.messageID.rawValue)");
               break;
        }
        
    }

}

Swift CoreMIDI without Objective-C (Updated for Swift 3)

Update: Code has been added for Swift 3 and XCode 8

These examples do little to no error checking and it is assumed your system has at least one source and one destination. Run them in a macOS Playground.

List the MIDI sources and destinations (Swift 3)

//
// Swift MIDI Playground : Matt Grippaldi 10/17/2016
//
// Sources & Destinations
//
// Updated for Swift 3 / XCode 8
//
import Cocoa
import CoreMIDI
import PlaygroundSupport

func getDisplayName(_ obj: MIDIObjectRef) -> String
{
    var param: Unmanaged<CFString>?
    var name: String = "Error"
    
    let err: OSStatus = MIDIObjectGetStringProperty(obj, kMIDIPropertyDisplayName, &param)
    if err == OSStatus(noErr)
    {
        name =  param!.takeRetainedValue() as String
    }
    
    return name
}

func getDestinationNames() -> [String]
{
    var names:[String] = [];
    
    let count: Int = MIDIGetNumberOfDestinations();
    for i in 0..<count {
        let endpoint:MIDIEndpointRef = MIDIGetDestination(i);
        
        if (endpoint != 0)
        {
            names.append(getDisplayName(endpoint));
        }
    }
    return names;
}

func getSourceNames() -> [String]
{
    var names:[String] = [];
    
    let count: Int = MIDIGetNumberOfSources();
    for i in 0..<count {
        let endpoint:MIDIEndpointRef = MIDIGetSource(i);
        if (endpoint != 0)
        {
            names.append(getDisplayName(endpoint));
        }
    }
    return names;
}

let destNames = getDestinationNames();

print("Number of MIDI Destinations: \(destNames.count)");
for destName in destNames
{
    print("  Destination: \(destName)");
}

let sourceNames = getSourceNames();

print("\nNumber of MIDI Sources: \(sourceNames.count)");
for sourceName in sourceNames
{
    print("  Source: \(sourceName)");
}

 

List the MIDI sources and destinations (Swift 2)

//
// Swift MIDI Playground : Matt Grippaldi 1/1/2016
//
import Cocoa
import CoreMIDI

func getDisplayName(obj: MIDIObjectRef) -> String
{
    var param: Unmanaged?
    var name: String = "Error";
    
    let err: OSStatus = MIDIObjectGetStringProperty(obj, kMIDIPropertyDisplayName, &param)
    if err == OSStatus(noErr)
    {
        name =  param!.takeRetainedValue() as String
    }
    
    return name;
}

func getDestinationNames() -> [String]
{
    var names:[String] = [String]();
    
    let count: Int = MIDIGetNumberOfDestinations();
    for (var i=0; i < count; ++i) { let endpoint:MIDIEndpointRef = MIDIGetDestination(i); if (endpoint != 0) { names.append(getDisplayName(endpoint)); } } return names; } func getSourceNames() -> [String]
{
    var names:[String] = [String]();
    
    let count: Int = MIDIGetNumberOfSources();
    for (var i=0; i < count; ++i)
    {
        let endpoint:MIDIEndpointRef = MIDIGetSource(i);
        if (endpoint != 0)
        {
            names.append(getDisplayName(endpoint));
        }
    }
    return names;
}

let destNames = getDestinationNames();
for destName in destNames
{
     print("Destination: \(destName)");
}

let sourceNames = getSourceNames();
for sourceName in sourceNames
{
    print("Source: \(sourceName)");
}

 

Create a client, connect a destination, and play a note (Swift 3)

//
//
// Swift MIDI Playground : Matt Grippaldi 10/17/2016
//
// Sources & Destinations
//
// Updated for Swift 3 / XCode 8
// Added code to display destinations (11/25/2016)
//
import Cocoa
import CoreMIDI
import PlaygroundSupport

func getDisplayName(_ obj: MIDIObjectRef) -> String
{
    var param: Unmanaged?
    var name: String = "Error";
    
    let err: OSStatus = MIDIObjectGetStringProperty(obj, kMIDIPropertyDisplayName, &param)
    if err == OSStatus(noErr)
    {
        name =  param!.takeRetainedValue() as String
    }
    
    return name;
}

func getDestinationNames() -> [String]
{
    var names:[String] = [String]();
    
    let count: Int = MIDIGetNumberOfDestinations();
    for i in 0 ..< count
    {
        let endpoint:MIDIEndpointRef = MIDIGetDestination(i);
        if (endpoint != 0)
        {
            names.append(getDisplayName(endpoint));
        }
    }
    return names;
}

var midiClient: MIDIClientRef = 0;
var outPort:MIDIPortRef = 0;

MIDIClientCreate("MidiTestClient" as CFString, nil, nil, &midiClient);
MIDIOutputPortCreate(midiClient, "MidiTest_OutPort" as CFString, &outPort);

var packet1:MIDIPacket = MIDIPacket();
packet1.timeStamp = 0;
packet1.length = 3;
packet1.data.0 = 0x90 + 0; // Note On event channel 1
packet1.data.1 = 0x3C; // Note C3
packet1.data.2 = 100; // Velocity

var packetList:MIDIPacketList = MIDIPacketList(numPackets: 1, packet: packet1);

let destinationNames = getDestinationNames()
for (index,destName) in destinationNames.enumerated()
{
    print("Destination #\(index): \(destName)")
}

let destNum = 0
print("Using destination #\(destNum)")

var dest:MIDIEndpointRef = MIDIGetDestination(destNum);
print("Playing note for 1 second on channel 1")
MIDISend(outPort, dest, &packetList);
packet1.data.0 = 0x80 + 0; // Note Off event channel 1
packet1.data.2 = 0; // Velocity
sleep(1);
packetList = MIDIPacketList(numPackets: 1, packet: packet1);
MIDISend(outPort, dest, &packetList);
print("Note off sent")

 

Create a client, connect a destination, and play a note (Swift 2)

//
// Swift MIDI Playground : Matt Grippaldi 1/1/2016
//
import Cocoa
import CoreMIDI

var midiClient: MIDIClientRef = 0;
var outPort:MIDIPortRef = 0;

MIDIClientCreate("MidiTestClient", nil, nil, &midiClient);
MIDIOutputPortCreate(midiClient, "MidiTest_OutPort", &outPort);

var packet1:MIDIPacket = MIDIPacket();
packet1.timeStamp = 0;
packet1.length = 3;
packet1.data.0 = 0x90 + 0; // Note On event channel 1
packet1.data.1 = 0x3C; // Note C3
packet1.data.2 = 100; // Velocity

var packetList:MIDIPacketList = MIDIPacketList(numPackets: 1, packet: packet1);

// Get the first destination
var dest:MIDIEndpointRef = MIDIGetDestination(0);
MIDISend(outPort, dest, &packetList);
packet1.data.0 = 0x80 + 0; // Note Off event channel 1
packet1.data.2 = 0; // Velocity
sleep(1);
packetList = MIDIPacketList(numPackets: 1, packet: packet1);
MIDISend(outPort, dest, &packetList);

 

Connect an input source and receive data via a callback (Swift 3)

//
// Swift MIDI Playground : Matt Grippaldi 10/17/2016
//
// MIDI Callbacks
// 
// Updated for Swift 3 / XCode 8
//
import Cocoa
import CoreMIDI
import PlaygroundSupport

func getDisplayName(_ obj: MIDIObjectRef) -> String
{
    var param: Unmanaged<CFString>?
    var name: String = "Error"
    
    let err: OSStatus = MIDIObjectGetStringProperty(obj, kMIDIPropertyDisplayName, &param)
    if err == OSStatus(noErr)
    {
        name =  param!.takeRetainedValue() as String
    }
    
    return name
}

func MyMIDIReadProc(pktList: UnsafePointer<MIDIPacketList>,
                    readProcRefCon: UnsafeMutableRawPointer?, srcConnRefCon: UnsafeMutableRawPointer?) -> Void
{
    let packetList:MIDIPacketList = pktList.pointee
    let srcRef:MIDIEndpointRef = srcConnRefCon!.load(as: MIDIEndpointRef.self)

    print("MIDI Received From Source: \(getDisplayName(srcRef))")
    
    var packet:MIDIPacket = packetList.packet
    for _ in 1...packetList.numPackets
    {
        let bytes = Mirror(reflecting: packet.data).children
        var dumpStr = ""
        
        // bytes mirror contains all the zero values in the ridiulous packet data tuple
        // so use the packet length to iterate.
        var i = packet.length
        for (_, attr) in bytes.enumerated()
        {
            dumpStr += String(format:"$%02X ", attr.value as! UInt8)
            i -= 1
            if (i <= 0)
            {
                break
            }
        }
        
        print(dumpStr)
        packet = MIDIPacketNext(&packet).pointee
    }
}

var midiClient: MIDIClientRef = 0
var inPort:MIDIPortRef = 0
var src:MIDIEndpointRef = MIDIGetSource(0)

MIDIClientCreate("MidiTestClient" as CFString, nil, nil, &midiClient)
MIDIInputPortCreate(midiClient, "MidiTest_InPort" as CFString, MyMIDIReadProc, nil, &inPort)

MIDIPortConnectSource(inPort, src, &src)

// Keep playground running
PlaygroundPage.current.needsIndefiniteExecution = true

 

Connect an input source and receive data via a callback (Swift 2)

//
// Swift MIDI Playground : Matt Grippaldi 1/1/2016
//
import Cocoa
import CoreMIDI
import XCPlayground

func getDisplayName(obj: MIDIObjectRef) -> String
{
    var param: Unmanaged?
    var name: String = "Error";
    
    let err: OSStatus = MIDIObjectGetStringProperty(obj, kMIDIPropertyDisplayName, &param)
    if err == OSStatus(noErr)
    {
        name =  param!.takeRetainedValue() as String
    }
    
    return name;
}

func MyMIDIReadProc(pktList: UnsafePointer,
    readProcRefCon: UnsafeMutablePointer, srcConnRefCon: UnsafeMutablePointer) -> Void
{
    let packetList:MIDIPacketList = pktList.memory;
    let srcRef:MIDIEndpointRef = UnsafeMutablePointer(COpaquePointer(srcConnRefCon)).memory;
    print("MIDI Received From Source: \(getDisplayName(srcRef))");
    
    var packet:MIDIPacket = packetList.packet;
    for _ in 1...packetList.numPackets
    {
        let bytes = Mirror(reflecting: packet.data).children;
        var dumpStr = "";
        
        // bytes mirror contains all the zero values in the ridiulous packet data tuple
        // so use the packet length to iterate.
        var i = packet.length;
        for (_, attr) in bytes.enumerate()
        {
             dumpStr += String(format:"$%02X ", attr.value as! UInt8);
            --i;
            if (i <= 0)
            {
               break;
            }
        }

        print(dumpStr)
        packet = MIDIPacketNext(&packet).memory;
    }
}

var midiClient: MIDIClientRef = 0;
var inPort:MIDIPortRef = 0;
var src:MIDIEndpointRef = MIDIGetSource(0);

MIDIClientCreate("MidiTestClient", nil, nil, &midiClient);
MIDIInputPortCreate(midiClient, "MidiTest_InPort", MyMIDIReadProc, nil, &inPort);

MIDIPortConnectSource(inPort, src, &src);

// Keep playground running
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true;