Skip to main content

MIDI Listener in Swift

·6 mins

A Practical Introduction to CoreMIDI #

This post is a programming tutorial on how to receive MIDI messages on your iPhone, iPad or Mac with your own code. It will provide a code walk-through and a downloadable Xcode project with a complete working example.

Background #

On the 26th of August 2013 I have bought a Roland A-88 MIDI controller. I honestly thought I had the time to teach myself to play the piano.

Being notoriously curious about things I have asked myself “What is MIDI and what comes out from that controller?”. Yes, I could have walked the easy way and just downloaded an app, but …

Just a year before I have switched from PC to Mac and was eager to try any macOS and iOS development.

A new project was born: MIDI Aid.

https://twissmueller.medium.com/midi-aid-ab4454cfee58

So much about learning the piano.

At that time, the app was all done in Objective-C and UIKit. It was hard to find any tutorials on CoreMIDI at that time, but I have managed to get it done. The first release of MIDI Aid was on the 8th of February 2014.

The world has changed with the introduction of Swift and SwiftUI. At least my world and certainly of many other Apple developers. CoreMIDI itself also got some nice updates which I wanted to use.

I have completely rewritten MIDI Aid using Swift and SwiftUI and released it on the 26th of November 2020.

It was always hard to find adequate tutorials on CoreMIDI and with all the new changes most of them got outdated overnight. There was a void. There were no complete code examples of how to receive simple MIDI message from a controller on an iOS, iPadOS or macOS device.

This post fills the void by providing a code walk-through and a fully working Xcode project.

Compile it for either macOS or iOS, plug in a USB MIDI device and start hitting the keys, or whatever triggers a MIDI message on your device.

When the app receives a MIDI message it will be logged. For now, it supports “note on” and “note off” events, but can easily be extended for other types as well.

https://youtu.be/I4xTyAwSt2E

The Xcode-workspace with the fully-working application can be downloaded from here.

Please note: I have not written the code with the so called SysEx-messages in mind, because with my controllers I have no means to test them.

Prerequisites #

A few things are needed to follow along with this tutorial.

Let’s assume you have some type of MIDI controller, you need a means to connect it to either your Mac, your iPhone or iPad.

There are three ways of how to connect a MIDI controller:

  • The classic MIDI cable
  • USB cable
  • Bluetooth.

The USB cable is the easiest one, because it can just be plugged into your Apple device. USB-A to Lightning or USB-C adapter might be required.

In case your device does not support USB, the classic MIDI ports need to be used with an adapter cable like e.g. the Roland UM-ONE mk2.

Bluetooth is also an option but requires some more coding, at least for iOS, which I did not include in this tutorial. There are devices like the Roland WM-1 which transmit all MIDI signals over Bluetooth.

Coming back to the prerequisites you also need Xcode of course. I have written and tested the code with Xcode 12.5.1.

Talking about Xcode, you might wonder on how to debug your code when running it on iOS with a MIDI device being plugged into your iOS device. There is a setting in Xcode to indicate that the app can be started over WiFi.

Just open Window -> Devices and Simulators, select your iOS device and active “Connect via Network”. From now on, no USB-cable is required to start the app on your iOS device from Xcode.

Connect via Network

Code Walkthrough #

Now, it is time for some code. I have provided the classes needed along with their links to Apples documentation and cited Apples description.

Everything starts with a

A client object derives from MIDIObjectRef. It doesn’t have an owning object.

var midiClient: MIDIClientRef = 0

We then create this client by calling

Creates a MIDI client with a callback block.

MIDIClientCreateWithBlock("Client" as CFString, &midiClient) { midiNotification in
   ...
}

This callback will be called when something in our MIDI setup changes. When a device gets plugged in it needs to be connected “internally”. This is explained at the end.

switch (notification.messageID) {
    ...
    case .msgObjectAdded:
         NSLog("msgObjectAdded")
         // connect the source, see below

The next step is to declare a

A port object derives from MIDIObjectRef, and its owning object is a MIDIDeviceRef. It represents an input or output port, and it provides the means to communicate with any number of MIDI sources or destinations.

var inputPort: MIDIPortRef = 0

Similar to above, the port object needs to be created. We do this by calling

Creates an input port through which the client may receive incoming MIDI messages from any MIDI source.

MIDIInputPortCreateWithProtocol(
            midiClient,
            "Input Port as CFString" as CFString,
            MIDIProtocolID._1_0,
            &inputPort) {  [weak self] eventList, srcConnRefCon in
    ...
}

We continue working with eventList which is of type UnsafePointer<MIDIEventList>. This is our MIDI data that we want to extract.

A variable-length list of MIDI event packets.

From there we can continue and parse the MIDI messages.

let midiEventList: MIDIEventList = unsafePointerMidiEventList.pointee
var packet = midiEventList.packet

(0 ..< midiEventList.numPackets).forEach { _ in
    ...
}

In the loop, we can then extract all the packets.

let words = Mirror(reflecting: packet.words).children
words.forEach { word in
    let uint32 = word.value as! UInt32
    guard uint32 > 0 else { return }
    midiPacket = MidiPacket(
        first: UInt8((uint32 & 0xFF000000) >> 24),
        second: UInt8((uint32 & 0x00FF0000) >> 16),
        third: UInt8((uint32 & 0x0000FF00) >> 8),
        fourth: UInt8(uint32 & 0x000000FF))
}

What is not clear to me, is the number of packets (numPackets). As per my observations, I hit a key its one packet, release a key, another packet and so on.

Maybe when one of those ominous SysEx-messages are being received, then there can be more packets. As far as I can remember those message can be of variable length.

Finally, we connect the sources.

First we get the number of sources with

Returns the number of sources in the system.

let sourceCount = MIDIGetNumberOfSources()

We iterate over every source index to retrieve the source with

Returns a source in the system.

var source = MIDIGetSource(sourceIndex)

Then every source gets connected by calling

Makes a connection from a source to a client input port. Availability

MIDIPortConnectSource(inputPort, source, &source)

I have put the code for connecting the sources in a separate function and call this twice. At first I call it on startup, right after initialisation and creating the MIDIClientRef and the MIDIPortRef. Then, I also call it whenever a new device has been plugged in.

Conclusion #

Hopefully, this tutorial will get you going implementing your own code based on CoreMIDI.

From connecting the MIDI controller to a Mac, iPhone or iPad, then setting up the right structures in code to finally displaying the MIDI message on screen, we went through all the steps required.

There are many more use cases than displaying MIDI messages. Of course, MIDI messages can also be generated from code to control other MIDI devices. Maybe in the future, there will be another tutorial, but maybe I should learn to play the piano first.

If anything is unclear or in case you have trouble implementing your specific use-case, please feel free to leave a comment.

Resources #