SwiftでCore MIDI


![Untitled.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54852/eb50356e-7661-a840-bbc6-6e893884b7ec.gif)

週末急に思い立ってiPadでMIDIピアノの入力に反応するアプリを作ってみようと思ったのですが、Core MIDI周りの情報はObjective-C止まりでアップデートされていないものが多く、ましてSwiftとなると英語でもほとんど情報がないので結構困りました（そもそもAppleがMIDI関連の情報発信についてやる気ない）。ニーズが果たしてどれほどあるのかわかりませんが、一応メモとして残しておきます。

また、今回作成したバイナリはCatalyst経由でmacOS上でも動いたので、macの場合もSwiftを使う限りは同じやり方で行けると思います。

# 目標

MIDIキーボードから入力されたイベントに従ってiOSアプリ上で絵を出す。
iPadからMIDIピアノを鳴らすなどの送信系は今回扱っていません。

# 流れ
Swiftと言ってもCore MIDIをつつく以上、大きな流れはObjective-Cの場合と変わりません。下記のサイトは10年以上前のものですが、大いに参考にさせていただきました。

- [Core MIDI その１ MIDIObject](https://objective-audio.jp/2008/06/07/core-midi-midiobject/)
- [Core MIDI その２ MIDIPacketの受信](https://objective-audio.jp/2008/06/10/core-midi-midipacket/)

やることをまとめると、

1. MIDIデバイスの一覧を取得
2. 使用したいデバイスに対するMIDIClientを作成
3. MIDIClientで使うMIDIInputPort（データの受け口）を作成
4. 使用したいデバイスのMIDIEndpointと、MIDIInputPortを接続
5. 飛んでくるMIDIメッセージの処理

という感じです。
 
![midi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54852/b86c2692-3859-1803-8420-ff7c0ee14f04.png)



# 1. MIDIデバイスの一覧を取得
```swift:
import CoreMIDI
import os.log

var numberOfSources = 0
var sourceName = [String]()

func findMIDISources() {
    sourceName.removeAll()
    numberOfSources = MIDIGetNumberOfSources()
    os_log("%i Device(s) found", numberOfSources)
        
    for i in 0 ..< numberOfSources {
        let src = MIDIGetSource(i)
        var cfStr: Unmanaged<CFString>?
        let err = MIDIObjectGetStringProperty(src, kMIDIPropertyName, &cfStr)
        if err == noErr {
            if let str = cfStr?.takeRetainedValue() as String? {
                sourceName.append(str)
                os_log("Device #%i: %s", i, str)
            }
        }
    }
}
```

まず、`MIDIGetNumberOfSources()`関数を使用して、接続されているMIDIデバイス数を取得します。次にループ内で`MIDIGetSource()`を呼び、インデックス番号に対応したMIDIデバイスを取得します。

このデバイスに対して`MIDIObjectGetStringProperty()`を呼んであげることでデバイスの名称が取得できます。このデバイス名称は次のMIDIClientを作成する際に必要となります。

注意点はCore MIDIはCore Foundationを使用するC言語ベースのフレームワークなので、文字列は`String`ではなく`CFString`であり、文字列の取得は`CFString`のポインタを渡す形で行う点です。取得された`CFString`は自動でメモリ管理されないため、`takeRetainedValue()`を呼んだ上でSwiftの文字列に変換します。

- [参考] [Working with Core Foundation Types](https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/working_with_core_foundation_types)

#2. MIDIClientを作成

```swift:
    let name = NSString(string: sourceName[index])
    var client = MIDIClientRef()
    var err = MIDIClientCreateWithBlock(name, &client, onMIDIStatusChanged)
    if err != noErr {
        os_log(.error, "Failed to create client")
        return
    }
    os_log("MIDIClient created")

    func onMIDIStatusChanged(message: UnsafePointer<MIDINotification>) {
        os_log("MIDI Status changed!")
    }
```

次に取得したデバイス名から、MIDIClientを作成します。この際、ステータスが変化した際のコールバックを指定するのですが、これまでCore MIDIは`MIDIClientCreate()`という関数しかなく、コールバックもCの関数ポインタ形式で非常に面倒くさかったのですが、iOS 9.0から`MIDIClientCreateWithBlock()`という関数が追加され、コールバックにSwiftのクロージャを指定できるようになったのでめちゃめちゃ分かりやすくなりました。Core MIDI全然アップデートねえじゃねえかとか言ってすみません。ここでは`onMIDIStatusChanged()`という関数を別に定義して、それを指定しています。

# 3. MIDIInputPortを作成

```swift:
    let portName = NSString("inputPort")
    var port = MIDIPortRef()
    err = MIDIInputPortCreateWithBlock(client, portName, &port, onMIDIMessageReceived)
    if err != noErr {
        os_log("Failed to create input port")
        return
    }
    os_log("MIDIInputPort created")

    func onMIDIMessageReceived(message: UnsafePointer<MIDIPacketList>, srcConnRefCon: UnsafeMutableRawPointer?) {
        os_log("MIDI Message Received")
    }
```
MIDIInputPortの作成も、`MIDIInputPortCreateWithBlock()`関数ができたのでそちらを使います。実際にMIDIメッセージを受信した際に呼ばれるのはここで指定したコールバックです。（MIDIメッセージの解析はまた後ほどで行います）

# 4. MIDIEndpointとMIDIInputPortを接続

```swift
    let src = MIDIGetSource(index) // MIDIEndpointRef
    err = MIDIPortConnectSource(port, src, nil)
    if err != noErr {
        os_log("Failed to connect MIDIEndpoint")
        return
    }
    os_log("MIDIEndpoint connected to InputPort")
```

あとはそのままの流れで、最初に取得したMIDIEndpointと作成したMIDIInputPortを接続すればOKです。

# 5. MIDIメッセージの処理
最後にコールバックの中で受け取ったMIDIメッセージを解析し、内容に合わせた処理を行います。

```swift:
func onMIDIMessageReceived(message: UnsafePointer<MIDIPacketList>, srcConnRefCon: UnsafeMutableRawPointer?) {

    let packetList: MIDIPacketList = message.pointee
    let n = packetList.numPackets
    //os_log("%i MIDI Message(s) Received", n)
    
    var packet = packetList.packet
    for _ in 0 ..< n {
        // Handle MIDIPacket
        let mes: UInt8 = packet.data.0 & 0xF0
        let ch: UInt8 = packet.data.0 & 0x0F
        if mes == 0x90 && packet.data.2 != 0 {
            // Note On
            os_log("Note ON")
            let noteNo = packet.data.1
            let velocity = packet.data.2
            DispatchQueue.main.async {
                self.delegate?.noteOn(ch: ch, note: noteNo, vel: velocity)
            }
        } else if (mes == 0x80 || mes == 0x90) {
            // Note Off
            os_log("Note OFF")
            let noteNo = packet.data.1
            let velocity = packet.data.2
            DispatchQueue.main.async {
                self.delegate?.noteOff(ch: ch, note: noteNo, vel: velocity)
            }
        }
        let packetPtr = MIDIPacketNext(&packet)
        packet = packetPtr.pointee
    }
}
```
MIDIメッセージは`MIDIPacketList`というデータ型にまとめて入っており、`MIDIPacketNext()`を呼んでやることで次のデータへのポインタが返ってくるようになっています。Appleのページによれば以下のような使い方が正しいそうです（そもそもSwiftじゃないですが...）。

```objc
MIDIPacket *packet = &packetList->packet[0];
for (int i = 0; i < packetList->numPackets; ++i) {
    // ...
    packet = MIDIPacketNext (packet);
}
```
なんかこれだと最後に`MIDIPacketNext()`が一回余分に呼ばれてる感じがしますが、Appleがいいと言うのでたぶんいいのでしょう。

データの中身は`.data.[0-255]`でアクセスできます。最初の1バイトが0x9nの場合ノートオン、0x8nの場合はノートオフで、nはチャンネル番号を表します。またノートオン／オフの場合は続く2バイトでノートナンバー（音高）とベロシティ（強さ）が取得できるので、これらの情報を使って音の高さ、強さに合わせた処理を書けばよいでしょう。プロトコルを定義しておいてdelegateを設定できるようにしておくと楽かもしれません。

- [参考] [MIDIの学習](http://www1.plala.or.jp/yuuto/midi/p0201.html#note_on)

また、このコールバックですがメインスレッドとは別に呼ばれるっぽいので、UIやグラフィックに変更を加える際は、DispatchQueueなどを介してメインスレッドで実行されるように書いてあげるのが安全だと思われます。

# まとめ

これらの処理をクラスにまとめると以下のような感じになります。

```swift:MIDIManager.swift
import Foundation
import CoreMIDI
import os.log

protocol MIDIManagerDelegate {
    func noteOn(ch: UInt8, note: UInt8, vel: UInt8)
    func noteOff(ch: UInt8, note: UInt8, vel: UInt8)
}

class MIDIManager {
    
    var numberOfSources = 0
    var sourceName = [String]()
    var delegate: MIDIManagerDelegate?
    
    init() {
        findMIDISources()
    }
    
    func findMIDISources() {
        sourceName.removeAll()
        numberOfSources = MIDIGetNumberOfSources()
        os_log("%i Device(s) found", numberOfSources)
        
        for i in 0 ..< numberOfSources {
            let src = MIDIGetSource(i)
            var cfStr: Unmanaged<CFString>?
            let err = MIDIObjectGetStringProperty(src, kMIDIPropertyName, &cfStr)
            if err == noErr {
                if let str = cfStr?.takeRetainedValue() as String? {
                    sourceName.append(str)
                    os_log("Device #%i: %s", i, str)
                }
            }
        }
    }
    
    func connectMIDIClient(_ index: Int) {
        if 0 <= index && index < sourceName.count {
            // Create MIDI Client
            let name = NSString(string: sourceName[index])
            var client = MIDIClientRef()
            var err = MIDIClientCreateWithBlock(name, &client, onMIDIStatusChanged)
            if err != noErr {
                os_log(.error, "Failed to create client")
                return
            }
            os_log("MIDIClient created")
            
            // Create MIDI Input Port
            let portName = NSString("inputPort")
            var port = MIDIPortRef()
            err = MIDIInputPortCreateWithBlock(client, portName, &port, onMIDIMessageReceived)
            if err != noErr {
                os_log("Failed to create input port")
                return
            }
            os_log("MIDIInputPort created")

            // Connect MIDIEndpoint to MIDIInputPort
            let src = MIDIGetSource(index)
            err = MIDIPortConnectSource(port, src, nil)
            if err != noErr {
                os_log("Failed to connect MIDIEndpoint")
                return
            }
            os_log("MIDIEndpoint connected to InputPort")
        }
    }
    
    func onMIDIStatusChanged(message: UnsafePointer<MIDINotification>) {
        os_log("MIDI Status changed!")
    }

    func onMIDIMessageReceived(message: UnsafePointer<MIDIPacketList>, srcConnRefCon: UnsafeMutableRawPointer?) {

        let packetList: MIDIPacketList = message.pointee
        let n = packetList.numPackets
        //os_log("%i MIDI Message(s) Received", n)
        
        var packet = packetList.packet
        for _ in 0 ..< n {
            // Handle MIDIPacket
            let mes: UInt8 = packet.data.0 & 0xF0
            let ch: UInt8 = packet.data.0 & 0x0F
            if mes == 0x90 && packet.data.2 != 0 {
                // Note On
                os_log("Note ON")
                let noteNo = packet.data.1
                let velocity = packet.data.2
                DispatchQueue.main.async {
                    self.delegate?.noteOn(ch: ch, note: noteNo, vel: velocity)
                }
            } else if (mes == 0x80 || mes == 0x90) {
                // Note Off
                os_log("Note OFF")
                let noteNo = packet.data.1
                let velocity = packet.data.2
                DispatchQueue.main.async {
                    self.delegate?.noteOff(ch: ch, note: noteNo, vel: velocity)
                }
            }
            let packetPtr = MIDIPacketNext(&packet)
            packet = packetPtr.pointee
        }
    }
}
```

このクラスを使う側（ViewController）の処理としてはこんな感じです。

```swift:MyViewController.swift
class MyViewController: UIViewController, MIDIManagerDelegate {

    private var midi: MIDIManager?
    
    override func viewDidLoad() {
        super.viewDidLoad()

            midi = MIDIManager()
            if 0 < midi!.numberOfSources {
                midi!.connectMIDIClient(0)
                midi!.delegate = self
            }
    }

    func noteOn(ch: UInt8, note: UInt8, vel: UInt8) {
        // ノートオン時の処理
    }
    
    func noteOff(ch: UInt8, note: UInt8, vel: UInt8) {
        // ノートオフ時の処理
    }

```
不十分なところ多々あると思いますがご容赦ください！
AppleはMIDIのサポートちゃんと続けていってくれるのだろうか...。