【iOSDC2024 #LT】2. Core MIDIを使って、電子ピアノと連携したアプリを作る
この記事はiOSDC2024で発表したLTのセッション、我が家に電子ピアノがやってきたので、ピアノと連携するアプリを作ってみた!の発表のうち、CoreMIDIを使ったデバイスとの連携の方法についてまとめたものです
前回の記事は、「1. MIDIの規格について」でした
この記事では、今回実装した電子ピアノとiPadなどのデバイスと連携できるアプリの実装の中で、MIDIを使った電子ピアノとの接続に関する実装や、ピアノから取得したデータをアプリ内でどのようにハンドリングしているかをまとめます
今回実装したアプリは以下のようなUIになっています
接続した電子ピアノで鍵盤を叩くと、その情報、具体的には叩いた鍵盤の音名と叩いた強さが画面上部に表示されるようになっています
また叩いている間は、画面下の鍵盤のうち、実際に叩いている鍵盤の色が変わり、その色も叩いた強さに応じて濃さがかわるようになっています
MIDIKitを使った電子ピアノとの接続方法の概要
それではまずは電子ピアノとの接続の実装について、既述のMIDIKitを使った方法でまとめます
このアプリでは、MidiHelperというclassを実装し、MIDIを使った接続処理はここに集約しています
そしてこのclassはMidiHelperProtcolに準拠しているので、別途Mockなどに差し替えることも可能です
サンプルアプリ同様に、初期化時にView側で生成したObservableMIDIManagerをmidiManagerという変数でこのclassに渡すようにしています
基本的な接続処理やデータの送受信はこのmidiManagerを使って行います
接続処理は以下のようになっています
try midiManager?.start()
try midiManager?.addInputConnection(
to: .allOutputs,
tag: MidiHelper.Tags.midiIn,
receiver: .events { [weak self] events, timeStamp, source in
self?.received(timestamp: timeStamp, midiEvents: events)
}
)
try midiManager?.addOutputConnection(
to: .allInputs,
tag: MidiHelper.Tags.midiOut
)
ここではまずmidiManagerの開始処理を実行します
その後電子ピアノをinputとして登録したConnectionを作成して、追加します
そうすることで、電子ピアノの打鍵時の音名情報などをcallbackで受け取れるようになります
なお今回to:で指定している.allOutputsは接続することができるすべてのデバイスをinputとして登録してます
個別のデバイスと接続したい場合は、別途それらの情報を取得して、そのデバイスをinputとして指定する必要があります
また別途電子ピアノをoutputとして指定し、Connectionを作成しています
そうすることで、デバイス側から送ったMIDIの情報、例えばC4の鍵盤を100のvelocityで鳴らすというのを送信すると、接続されている電子ピアノ側でその音を鳴らすことができます
ちなみにデバイスから電子ピアノに出力する場合、「鍵盤を叩く」というイベントを送った後、適切に「鍵盤を離した」というイベントを送信しないと、電子ピアノ側でその鍵盤が解放されず、ピアノ側でその鍵盤を叩いても音が鳴らなくなるので、適切に両方のイベントを送ることが重要です
MidiHelper.Tagsのタグは、別途個別に接続する楽器やconnectionを選択するUIでも指定するために共通化しているもので、特にそちらの指定が不要であれば、自由な文字列で問題ありません
この辺りの実装は、MIDIKitのサンプルコードでも確認できます(コード)
特に凝った機能が不要であれば、これでピアノとのデータの送受信を行うことができます
なおBluetooth経由での接続の場合、Bluetoothデバイスの接続設定をする必要がありますが、そのUIはCoreMIDI側で提供されています
MIDIKitではそれをUIViewRepresentableでラップした画面のサンプルがあるので、SwiftUIでそれを利用したい場合はそれを参考にするのがいいと思います(コード)
ネットワーク経由での接続の場合、以下の関数を実行することでネットワーク経由での接続ができるようになります(参考)
なおこの関するはvisionOSがサポートされていない点には注意が必要です(ただし自分が動作確認した範囲では、ネットワーク経由でMacのGarageBandのピアノの鳴らすことができています)
setMIDINetworkSession(policy: .anyone)
アプリ内の音名などの定義実装
次にアプリ内での音名などの定義をどのようにしているかです
元々のCoreMIDIでは音名や鍵盤の叩く強さなどはもちろんenumなどでは定義されておらず、すべてポインターなどから自分で取得します
MIDIKit内ではそれらをenumなどにマッピングし、扱いやすい形にしてくれていますが、音名などの定義はMIDIとの接続だけでなく、ピアノのUIの実装でも利用するので、それらの実装がMIDIKitに依存しないようにするために、個別に実装しました
実際のコードは今回実装したPianoUIのPackageに実際のコードがありますが、概要をこちらにまとめます
音名
音名は例えば、第4オクターブのドといった音の種類で、ピアノで言うと鍵盤の位置がそれに該当します
これは、①オクターブ、②オクターブ内の音名の2つの要素に分けて定義しています
既述のNote番号では、0~127の範囲があり、オクターブは-1~9まであります
また日本では一般的に、「ドレミファソラシ」と言われますが、英語では「CDEFGAB」と呼ばれます
そのため今回はオクターブをOctave, ドレミファソラシをc~bに黒鍵分sharpを踏まえたものをKeyTypeというenumで定義し、それらを含めたPianoKeyというstructを作りました
これでOctaveとKeyTypeを参照すれば、一意の音名・鍵盤をしていできます(実装)
初期化の引数は、もちろんOctaveとKeyTypeを直接指定することもできるし、Note番号を指定することもできるようになっています
ちなみに細かい実装ですが、このアプリでは設定画面から音名の表記を切り替えられるようにしており、
以下の表記をサポートしています(設定は@AppStorageやUserDefaultsを使って、アプリ内に保持しています)
- 英語: CDEFGAB
- ドイツ語: CDEFGAH
- イタリア語: Do, Re, Mi, Fa, Sol, La, Si
- イタリア語(カタカナ): ドレミファソラシ
- 日本語: いろはにほへと
鍵盤を叩く強さなどの情報
実際にMIDI経由で電子ピアノから送られてくる情報は、既述の通り上記のPianoKeyの情報に加えて、タイムスタンプや鍵盤を叩く強さ、鍵盤を押しているか or 離しているかなどの情報があります
それらを扱うために以下のようなstructを定義しています
public struct PianoKeyStroke: Equatable, Identifiable, Hashable, Codable {
public let key: PianoKey
public let velocity: Int
public let timestampNanoSecond: UInt64
public let isOn: Bool
}
timestampNanoSecondがUInt64の型になっているのは、MIDIが扱うCoreMIDITimeStampという型がUInt64のtypealiasになっているので、UInt64型として定義しています
velocityは0~127の範囲で送られるので、扱いやすいようにこのstruct内には0~1(主にUI側でOpacity操作で利用)で扱う、もしくは%(主にTextで表示する時に利用)で扱うことができるgetterも定義しています
後述のPianoViewは初期化時の引数にこのPianoKeyStrokeの配列を受け取るようになっており、その値に応じて、鍵盤のUIを表示しています
このアプリでは、上記のMidiHelperでMIDIKitで受け取った値、またはUI側から電子ピアノに送るPianoKeyStrokeを変換し、相互にやり取りできるようにしています
PianoKeyの変換はNote番号への変換をかますと、簡単に行うことができます
ほぼ同時に叩いた鍵盤の情報を1つのグループとして扱う実装
このアプリで定めた仕様のうち、1つややこしいものが、同時に叩いた鍵盤は同じグループとして表示すると言うことです
もう一度アプリのスクショの上部を見ると、単独で叩いた鍵盤は1つのグループで区切られていますが、同時に叩いた鍵盤は3つで1つのグループになっています
ただこれはピアノを叩く人にとっては同時と感じますが、実際のMIDI上で記録されているタイムスタンプはUInt64で流れてくるので、正確に同時に叩くことは事実上不可能です
そのため流れてくる値のうち、ある音が流れてから一定時間の音は1つの配列にして流し、次の音は再び一定時間の単位で配列にまとめるという処理をしなければなりません
しかもその単位は30ミリ秒などの単位(設定画面から変更可能)の要望だったので、自前でその処理を実装するのはかなり難易度が高いです
そこで今回は元々ここのMIDIHelperからUI側への値の流し込みをCombineで行っていた(まだAsyncStreamに慣れていないので…)ので、そのCombineのOperatorを使いました
何かOperator自体を組み合わせて自作する必要があるかと思っていたら、なんとちょうどいいcollectというOperatorがありました
collectは指定時間or/and指定個数になったタイミングでそれまで保持した値を配列として流すというものです(なんと都合がいいのでしょう)
それを元に実装したのが以下の関数です
func bufferThrottle(for interval: DispatchQueue.SchedulerTimeType.Stride, scheduler: DispatchQueue) -> AnyPublisher<[Output], Failure> {
return self
.collect(.byTime(scheduler, interval))
.eraseToAnyPublisher()
}
今回は30ミリ秒などの指定時間が基準になるので、.byTimeを指定し、その引数に時間とschedulerを指定します
これで元々PianoKeyStrokeだったものをPianoKeyStrokeの配列に変換して流せるようになりました
まとめ
この記事ではCoreMIDIを使って、iPhoneなどのデバイスとMIDI対応の楽器と接続する実装やアプリ内での音楽情報の定義や処理方法についてまとめました
次の記事は、「3. Layout Protocolを使って、ピアノのUIを作ってみた」です