Android 15のMIDI 2.0サポートについて

2/16(PST)にAndroid 15 DP1が公開され、"Virtual MIDI 2.0 Devices" のサポートが追加されました。これについてMIDI 2.0の最新の状況(自分が知りうる公知のもの)を踏まえて解説します。この機能を要望したのは自分なので*1、たぶん自分が説明するのが一番正しいはず…

承前: Android 13のMIDI 2.0サポートからどうなった?

AndroidMIDI 2.0の機能が初めて追加されたのは2年前のAndroid 13です。正式版がリリースされたとき、このブログでその詳細を解説しました。

atsushieno.hatenablog.com

内容を箇条書きでまとめるとこんな感じです:

  • USB-MIDI 2.0プロトコルの規格が固まったので、Android 13でプラットフォームのレベルで実装された
  • MIDI 2.0のサポートは既存のMIDIポートの接続でもできる(UMPストリームをMIDI 1.0メッセージの代わりに流せばいいだけ)
  • OSが認識できる((MidiManager.getDevicesForTransport()で取得できる))ソフトウェアMIDI 2.0デバイスはまだ作成できない(のでissuetrackerで要望を出した)
    • 後方互換性の問題で単なる機能追加はできない
  • GoogleAndroidオーディオチームがこの辺の新機能を仕切り直してMIDI-CIサポートも追加してくると思う

ここから2年経って現在に至るわけですが、ソフトウェアMIDI 2.0デバイスのサポートが実際に追加された以外の部分は、半分くらいは「ハズレ」になった感じです。

え、そんなに外れるような予想を出したの??と思われそうですが、もちろん理由があります。2023年6月に、MIDI 2.0仕様は大きく変容したのです。これについては6月に詳しくまとめました。

atsushieno.hatenablog.com

ここで重要なのは「Protocol Negotiationがなくなった」という部分です。↑の話はProtocol Negotiationを使うことで論理的に1つのMIDIポートをMIDI 1.0 bytestreamとMIDI 2.0 UMPの両方で切り替えて使い回せる前提だったのが*2、そうではなくなってしまったわけです。代わりに導入されたUMP Stream ConfigurationメッセージはUMPとして規定されているので、MIDI 1.0でも2.0でもUniversal SysExであればよかったProtocol Negotiationとは異なり、そのポートが最初からUMPトランスポートであることが前提となりました。

2022年の時点では、MIDI 2.0を包括的にサポートしているプラットフォームAPIAppleのCoreMIDIとAndroidのUSB限定MIDI 2.0ポートのサポートしかなく、各プラットフォームでどうMIDI 2.0接続をデザインするかは不透明な状態でした。おそらく各社がMMAMIDI協会)で相談・総合接続の検証を行っていく過程で、「ポートはMIDI 1.0トランスポートかUMPポートか『あらかじめ』規定しておく」という合意が形成されたように(外からは)見えます。

ちなみにもうひとつ「外した」と思われるMIDI-CIサポートも追加してくると思う」ですが、ktmidiでMIDI-CI実装をゼロから作ったので、将来的には代わりにコレを使ってもらえればと思います。まだAPIが未整備…

MidiUmpDeviceService: 唯一のAPI変更点

上記のまとめでは「新機能を仕切り直してMIDI-CIサポートも追加してくると思う」と書いたわけですが、MIDI-CIの面倒なProtocol Negotiationがそもそもなくなったので、プラットフォームのAPIMIDI-CIは不要になりました。結果的に、Android 15で追加されたのは、新しいMidiUmpDeviceServiceという、MidiDeviceServiceの兄弟分のようなクラス1つのみです。使い方は基本的にMidiDeviceServiceとほぼ同じです。MidiUmpDeviceServiceから派生した実装クラスを作成し、AndroidManifest.xml<service>android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE"<meta-data>付きで登録し、対応するump_device_info.xmlのようなファイル(リソース名前は任意)をres/xmlに置くだけです。

特に、MidiDeviceServiceの中で使われるMidiReceiverMidiSenderMidiInputPortMidiOutputPortなどのクラスは全て共通です。なぜなら、(わたしが「外れた」予想の説明で書いた通り)MIDI 1.0のトランスポートの上にUMPを流しても、単なるByteArrayを送受信することにしかならないので、APIを変える必要が無いからです。

ただし、AndroidManifest.xml<service>MIDIバイスメタデータXMLの内容は微妙に異なっているので気をつける必要があります。具体的には、(1) MidiDeviceServiceではmeta-data属性で指定していましたが、MidiUmpDeviceServiceではproperty属性で指定し(この属性の存在を知らなかったのですが、API Level 31で追加されていました)、また(2) MidiDeviceServiceのときは<input-port><output-port>をそれぞれ記述していたのに対し、MIDI 2.0のUMPポートは双方向なので、<port>という要素を使うことになります。Android 15では、ここからinputとoutputのペアが自動的に作成されることになります。

特にproperty属性を使う点に注意してください。2/21現時点ではMidiUmpDeviceServiceのAPIリファレンスにある記述が正しく、android.media.midiパッケージのOverviewにある記述は間違っています(自分はここでハマりました)。

あとonGetInputPortReceivers()のreturnが地味にArray<MidiReceiver>ではなくList<MidiReceiver>になっています。わたしはこれがArrayだったのは良くないと思っているので、地味に良い変更だと思います。Arrayだとポート数がService生成時に固定になってしまうので、将来的に動的ポート生成をサポートしようとしても整合しなくなる可能性があります。(とはいえまだdeveloper previewなので、単なる実装での見落としとして「元に戻される」かもしれません。)

仮想MIDIバイスの可能性

MidiUmpDeviceServiceを使うと、これまでUSBに限られていたMIDI 2.0デバイスが、理論上はAndroid APIでリーチできる任意のデバイスまで広がったことになります。まあこれは半分は言い方の問題で、これまででも任意のAndroid Serviceを提供することで、android.hardware.usbAPIを使ってオレオレUSB MIDI 2.0サポートを実装することもできたでしょうが、公式にAPIを与えられたServiceならおよそ誰が作ったものでも相互接続できるようになるでしょう。

「仮想MIDIバイスを作れる」と言われても、ソフトウェアMIDIシンセサイザーを作る人以外は関係ないのでは…と思われるかもしれませんが、実際にはもう少し適用領域があります。

  • 筆者の事例のひとつとして、ktmidi-ci-toolでは仮想MIDIポートを公開しており、これ自体は他の「MIDI-CIデバイス」に接続して操作するためのアプリケーションですが、MIDI-CIのリファレンス実装として、他のMIDI-CIクライアントアプリケーションがこのアプリケーションに「接続」して使うことも可能になっています。(ktmidi-ci-toolはKotlin Multiplatformアプリであり、LinuxMacOSでも同様に動作します。)
  • MIDIキーボードアプリでたまに見られるのは、仮想MIDI入力ポートも作成しておいて、そこに届いたメッセージをそのまま自らがクライアントとして接続しているMIDI出力に送信するというものです(たとえばvmpk, kmmk
  • UMP用の仮想ポートを作成して、MIDI 1.0デバイスにUMPから変換して送信するような機能も実装可能でしょう。MidiDeviceServiceもMidiUmpDeviceServiceも動的にポートを追加することは出来ませんが、MIDI1のデバイスであればUMPのグループを別々に扱うことで1つのUMPポートから16デバイスまではアプリケーション側で対応可能でしょう(デバイス名等をクライアント側に認識させるにはUMP StreamメッセージのEndpoint Discoveryを送ってEndpoint Name Notificationを受け取る必要がありそうです)

MIDI 2.0 UMPのクライアントアプリケーションの作り方は、Android 13の頃と変わりません。MidiManager.getDevicesForTransport()でUMP対応デバイスを取得し、あとはMIDI 1.0の頃と同じAPIで、ただしMIDIメッセージのByteArrayにはMIDI 1.0バイトストリームではなくUMPを流す、というだけです。

MIDI 2.0アプリケーションを開発するのにMIDI-CIの機能は必須ではありませんが、MIDI-CIが必要であれば、C++アプリならJUCE 7.0.9から存在しているjuce_midi_ciモジュール(解説)、Kotlinなら上述のとおり筆者のktmidiでそのための機能を提供しています…と言いたいところですが、ktmidi-ci-toolの開発の過程でAPIはひっちゃかめっちゃかなので、API整備を待ったほうが良いでしょう。

*1:こういうのをアレオレ(「アレはオレがやった」)って言う界隈があるそうですね

*2:そういう目的で存在しているとは仕様上は書かれていないですが