Search code examples
iosswiftcoremidi

Observe MIDI device connection notification on iOS


Is there an API that will notify me when a MIDI device has been connected to iOS device? I have been trying to find it among the CoreMidi APIs, but I wasn't successful. I am only able to list all connected devices at a given moment.

I would like to avoid polling if possible, so this would be beneficial.


Solution

  • Yes, you can use the MIDIClient API. Specifically, here's a simple, self-contained program that will print messages when devices are added, removed, or have their properties changed:

    import Cocoa
    import CoreMIDI
    
    var client = MIDIClientRef()
    let clientName = "MyMIDIClient" as CFString
    let err = MIDIClientCreateWithBlock(clientName, &client) { (notificationPtr: UnsafePointer<MIDINotification>) in
        let notification = notificationPtr.pointee
        switch notification.messageID {
            case .msgSetupChanged: // Can ignore, really
                break
    
            case .msgObjectAdded:
                let rawPtr = UnsafeRawPointer(notificationPtr)
                let message = rawPtr.assumingMemoryBound(to: MIDIObjectAddRemoveNotification.self).pointee
                print("MIDI \(message.childType) added: \(message.child)")
    
            case .msgObjectRemoved:
                let rawPtr = UnsafeRawPointer(notificationPtr)
                let message = rawPtr.assumingMemoryBound(to: MIDIObjectAddRemoveNotification.self).pointee
                print("MIDI \(message.childType) removed: \(message.child)")
    
            case .msgPropertyChanged:
                let rawPtr = UnsafeRawPointer(notificationPtr)
                let message = rawPtr.assumingMemoryBound(to: MIDIObjectPropertyChangeNotification.self).pointee
                print("MIDI \(message.object) property \(message.propertyName.takeUnretainedValue()) changed.")
    
            case .msgThruConnectionsChanged:
                fallthrough
            case .msgSerialPortOwnerChanged:
                print("MIDI Thru connection was created or destroyed")
    
            case .msgIOError:
                let rawPtr = UnsafeRawPointer(notificationPtr)
                let message = rawPtr.assumingMemoryBound(to: MIDIIOErrorNotification.self).pointee
                print("MIDI I/O error \(message.errorCode) occurred")
    
            default:
                break
        }
    }
    
    if err != noErr {
        print("Error creating MIDI client: \(err)")
    }
    
    let rl = RunLoop.current
    while true { 
        rl.run(mode: .default, before: .distantFuture)
    }
    

    A few notes:

    • Of all the Apple system APIs, CoreMIDI may just be the worst one to use from Swift. It's a pure-C API, makes heavy use of pointers, CoreFoundation types, callback procs, structs whose types are only known at runtime, etc. You see that here in the use of Unsafe(Raw)Pointers, rebinding memory, etc. I think it still makes more sense to just use it from Objective-C, and do so in my own projects.
    • Not all MIDI devices actually present as a MIDI Device (ie. MIDIDeviceRef). Some use a driver that creates virtual endpoints, and those just show up as (unrelated) source and destination endpoints. You have to figure out how to deal with these on your own. Native Instruments' devices are one common example of this behavior.
    • You might check out MIKMIDI, which makes all of this a lot easier. Specifically, you'd just do:
    var midiDevicesObserver: NSKeyValueObservation?
    let deviceManager = MIKMIDIDeviceManager.shared
    midiDevicesObserver = deviceManager.observe(\.availableDevices) { (dm, _) in
        print("Available MIDI devices changed: \(dm.availableDevices)")
    }
    

    or use the also-available MIKMIDIDeviceWasAddedNotification and related notifications. It also handles automatically coalescing source/endpoint pairs into devices, allows you to KVO device properties (name, etc.), and a bunch of other stuff.

    Disclaimer: I'm the primary author and maintainer of MIKMIDI.