MIDIFrameworkを使ってMacとシンセで送受信をしてみる

## ここまでの流れ
前回の投稿で、Standard MIDI Fileを選択するための[ファイルブラウザ](https://qiita.com/tunaingot/items/f0a1a683fde8ef40a0a5)について書きました。
書いた記事はiOSデバイスのものですが、macOSは```NSOutlineView```を使えば簡単に作れるため、他のネット記事や、Qiitaメンバーの記事を参考にしてもらうとして...

MIDIに関連する機能はmacOSとiOSで共通なので、まずは私の開発環境でUSBで常にシンセと繋がっているmacOSで**MIDIの送受信**をやってみます。

## MIDIハードウェア環境
超シンセマニアの私でしたが(笑)、住環境などの問題から徐々に機材が減り、現在はRolandの**FANTOM 8**だけが鎮座しています。

![IMG_0673.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/a137c0c3-ab57-4ba8-9739-6cba496a180f.jpeg)

MacはiMac (Apple Silicon M1)を使っています。

Macとは**USB直結**で使っています。
デバイスドライバのタイプは**Vender**で、MIDIだけでなく、オーディオインターフェースとしても利用できる様になっています。

動作確認の結果、デバイスドライバは**Vender**でも**Generic**のどちらでも動作しました。
GenericはmacOS標準ドライバで動作するモードです。

## サンプルプロジェクト
GitHubで公開しています。
[こちら](https://github.com/tunaingot/LMIDISample)からダウンロードできます。

### MIDIFramework
サンプルプロジェクトには```MIDIFramework```というフレームワークを取り込んでいます。
これは私がC++, Objective-Cの時代から最新のSwiftまで、長年育ててきたMIDI関連処理をフレームワーク化したものです。
ソースコードで公開するより、このほうがシンプルに使えると思いました。

MIDIの処理は```LMIDI```というクラスが担当します。

### Macに繋がっているMIDIデバイスを調べる
以下の二つのLMIDIクラスプロパティで調べられます。
```swift
public static var sourceNames: [String]
public static var destinationNames: [String]
```
サンプルプログラムでは、
```swift
print(LMIDI.sourceNames)
print(LMIDI.destinationNames)
```
と記述していて、デバッグコンソールにMacに接続されているMIDIデバイスを表示します。

### MIDI処理のインスタンスを作る
上記のプロパティでMIDIデバイスを調べ、接続したいデバイスを決めます。
サンプルプログラムでは、MIDI処理のインスタンス名は```midiDevice```という名前にし、ひとまずリスト上一番上のデバイスに接続します。

LMIDIのイニシャライザは送信デバイス、受信デバイスを個別に指定できる様になっています。
```swift
private var midiDevice: LMIDI?

midiDevice = LMIDI(sourceName: LMIDI.sourceNames.first!,
              destinationName: LMIDI.destinationNames.first!)
```
これですぐに送受信できる状態になっています！
### 受信データの受け取り
サンプルではNotificationを使っています。
MIDIFrameworkは、MIDIデータを受信するとNotificationで通知します。
サンプルでは、
* すべてのメッセージの受信通知
* コントロールチェンジの受信通知

を受け取るようにobserverを指定しています。
```swift
NotificationCenter.default.addObserver(self,
                    selector: #selector(messageReceived(_:)),
                    name: LMIDI.messageReceivedNotification,
                    object: midiDevice)
NotificationCenter.default.addObserver(self,
                    selector: #selector(controlChangeReceived(_:)),
                    name: LMIDI.controlChangeReceivedNotification,
                    object: midiDevice)
```
この通知で受け取ったuserInfoの中に、MIDIで受信したデータが格納されています。
サンプルでは、print文を使ってその中身をデバッグコンソールに表示しています。

![スクリーンショット 2025-04-28 21.44.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/b4878807-5a94-4cdb-8e60-f03acba56461.png)

もう一つの受信方法は、```LMIDIDelegate```というプロトコルを適用すると、
```swift
public protocol LMIDIDelegate {
    func messageReceived(receiveData: Data, MIDITimeStamp: MIDITimeStamp)
}
```
というデリゲートメソッドで受信データを受け取ることができます。
このデリゲートメソッドは受信した時に呼ばれるので、MIDIレコーダーのような受信データを素早く処理したいときに便利です。

タイミング的にはデリゲートメソッドの方がNotificationより早く呼ばれますが、NotificationのUserInfoにもMIDITimeStampを入れてあるので、MIDIデータ受信タイミングはどちらの方法でも正しく得ることができまs。


### 送信
```send```メソッドを使います。
sendメソッドは二つあります。

```swift
public func send(packet: Data, timeStamp: MIDITimeStamp)
public func send(packet: Data)
```
引数にMIDITimeStampがあるメソッドは、指定したタイムスタンプのタイミングでMIDIデータが送信されます。
引数がないメソッドは即時送信されます。

```1秒後にノートオフを送る```という処理を考えた場合、
* 引数にタイムスタンプあり〜1秒後のタイムスタンプを計算して、引数に与える
* 引数にタイムスタンプなし〜タイマーを使って1秒後に送信

という方法が考えられます。
単発でMIDIデータを送信したい場合はタイマーがわかりやすいでしょう。

大量のデータを決まった時間に送るなら、MIDITimeStampを検討するのもいいでしょう。
使う側はMIDITimeStampを指定して送信処理するだけで、あとはmacOSがそのタイミングが来たら外部へ送信してくれます。

## 送受信ができたら、次はいよいよSMF再生
Standard MIDI Fileを再生するには、基本的に送信処理ができればOKです。
受信処理は必須ではないのですが、受信処理も組み込んでみます。

## 後日談
iOSのサンプルプロジェクトも追加しました。
macOSとiOSのサンプルは同じフォルダに入れましたので、一緒にダウンロードできます。