JUCE 7.0.9で追加されたMIDI-CIサポートについて

JUCE Advent Calendar 2023、2日目の記事です。

2023年にはほとんどJUCEらしいコードをいじる機会が無かったatsushienoです。今年はJUCE 7.0.9にやや唐突に追加されたMIDI-CI (Capability Inquiry) サポートについて解説します。これならちょっと詳しい程度で書けるっ

そもそもMIDI-CIとは

MIDI-CIは、「MIDIバイス」同士でリクエスト/レスポンス方式の通信を行えるユニバーサル・システムエクスクルーシブ (SysEx) メッセージです(SysExでやり取りできればいいので、デバイスMIDI 2.0である必要はありません)。MIDI 1.0の時代は単方向だったので、MIDI-CIが想定する双方向通信を実現するには、相互にinとoutが繋がっている必要があります。

MIDIバイス」と括弧書きにしてあるのは、実際には物理デバイスではなくソフトウェアMIDI等もこの中に含まれるので、抽象的な概念だと理解したほうが適切です。プラットフォームMIDIアクセスAPIにおける「デバイス」の概念があれば、それに近いと考えてよいでしょう(「MIDIポート」のほうが近い環境もあるでしょう)。

実際には、メッセージの送受信さえ出来ていれば、その間のトランスポートが何であるかは問わないので、たとえばMIDI 1.0のinとoutが繋がっている「必要」があるわけではありません。とはいえ、現実的にはプラットフォームMIDIアクセスAPIMIDIバイスと認識されているものに接続するというのが汎用的でしょう。他にもBonjour (mDNS) プロトコルなどを用いてデバイスを探索するという方法も考えられます。

MIDI-CIの具体的な機能の内容を説明する前に言及しておくべきことがあります。実はMIDI 2.0の仕様は2023年6月に大幅な改訂を加えていて、この時にMIDI-CIも「バージョン1.2」に更新されています。以前のMIDI-CIについて解説されているものでは不十分だと思ったほうがよいです。MIDI-CI関連記事の新旧を見極める基準のひとつとしては、MIDI 2.0を構成する「3つのP (Three Ps)」としてProfile Configuration, Protocol Negotiation, Property Exchangeを挙げているものは、バージョン1.2で「廃止された」Protocol Negotiationが含まれているので、古いと思ってよいでしょう(根本的に仕様が変わったので、それについて言及していて然るべきではあります)。バージョン1.2ではProcess Inquiryが新たに追加されたので(「3つのP」は首をすげ替えられて生きているといえるでしょう)、それについて言及していれば新しいです。またMIDI-CI仕様の詳細に踏み込むのであれば無視できない概念として機能ブロック(Function Block)というコンセプトも追加されています。

MIDI-CIバージョン1.2では、以下のカテゴリーに分類できるメッセージが規定されています。

  • Profile Configuration: デバイスに問い合わせを送って、そのデバイスがどのような「プロファイル」に準拠したものであるかを知ることができます。「ピアノだ」「フルートだ」「ギターペダルだ」「GM総合音源だ」「ソフトウェアシンセだ」といった返答が来ることになります。具体的な楽器プロファイルの仕様はまだありません。
  • Property Exchange: デバイスの「プロパティ」の情報を取得/設定するためのものです。メッセージの内容はデフォルトではJSONでやり取りされます(独自のMIME typeで送受信することも論理的には可能です)。最近KORGKeyStage(未発売)が世界で初めてPEをサポートするMIDIキーボードとして話題になりました。
  • Process Inquiry: デバイスの現在のMIDIメッセージの保持された状態があれば、それを一括して取得する等の機能を含むメッセージ群です。MIDI dumpの類を規定したと考えられます。

MIDI-CI APIがカバーすべき機能

MIDI-CIの機能をサポートするライブラリ」が実装すべきものは、MIDI-CIが規定する各種メッセージのSysExバイナリのシリアライゼーション/デシリアライゼーションと、その送受信に基づいてデバイスの情報を保持する機構ということになります。それぞれのメッセージはUniversal SysExのフォーマットに準拠するかたちで詳細データフォーマットが定められており、それらを抽象化するオブジェクトが有用です。たとえばDiscoveryMessageDiscoveryResponseEndpointInquiryEndpointInquiryResponse…といった構造体が規定されるでしょう(これらの擬似的な名前はjuce_midi_ciをもとにしています)。

MIDI-CI 1.2仕様では、問い合わせを送信するMIDIバイスInitiator、応答を返すMIDIバイスResponderと呼んでおり、これらを表現するクラスにイベントハンドラーとして「デバイス検出の問い合わせが来た」「デバイス検出の応答が返ってきた」等をもち、またメッセージの処理フロー(たとえば「デバイス検出リクエストがResponderに届いたらInitiatorに返信を送る」等)を実装できていれば、基本的な送受信の仕組みはできていることになります。MIDI-CIにはタイムアウト等の規定も含まれているので、場合によっては実装でカバーする必要があるかもしれません。

実際の送受信はプラットフォームMIDIアクセスAPI等に丸投げできるので、自前で実装する必要はないでしょう。Bonjour等で独自に実装できるように、メッセージの送受信部分は外部から設定できることが望ましいです。

SysExバイナリデータの内容の取得・生成

MIDI-CIが前提とするSysExデータは、MIDI 1.0バイトストリームとMIDI 2.0 UMPの各トランスポートでバイナリデータフォーマットが異なります。

  • MIDI 1.0 SysEx: F0h ... F7h、7-bit encoded
  • MIDI 2.0 SysEx7 UMP: 3Gh xxh xxh ...、7-bit encoded
  • MIDI 2.0 SysEx8 UMP: 5Gh xxh xxh ...、8-bit (not encoded)

Gはgroup)

MIDI-CIの主要な用途がMIDI 2.0システムであることを考えると、MIDI-CIライブラリはMIDI 2.0を想定してこれらを全てサポートしていることが望ましく、MIDI-CIのSysExバイナリデータを解析したり生成したりするときには、これらをデコードした8-bitバイナリデータで処理できることが望ましいでしょう。

余談: プラットフォームMIDIアクセスAPIのサポートは?

2023年はLinuxALSA APIMIDI 2.0サポートが追加された年であり(2023年6月の更新版に基づいています)、すでに対応APIが追加されていたmacOS/iOS群のCoreMIDIと合わせて、基本的なMIDI-CIサポートをクロスプラットフォームで利用できる状態だったといえます。Windowsでは2024年まで出ない予定です(開発者がほぼ1人しかいないし、現実的に対応できるのかどうかは不透明です)。Androidでの対応は未公表です(やる気はあるみたいですが)。

とはいえ、プラットフォームMIDIアクセスAPIにおけるMIDI 2.0対応は、MIDI-CIの機能を実現する上で必須というわけではありません。前述の通り、MIDI-CIはMIDI 1.0のトランスポートのI/Oのペアの上でも構築できます。実のところ、JUCE 7.0.9のMIDI-CIサポートでは、プラットフォームMIDIアクセスAPIを必要としていません。MIDI 2.0対応が遅れているWindowsAndroidでも、仕様上は安心してMIDI-CIを使用できます。

MIDI-CIをどこまで高レベルでサポートするかは、API次第です。たとえばMIDI-CIをサポートしているとされるCoreMIDIでも、Property ExchangeについてはほとんどAPIがなく、「なまのSysExをやり取りする程度なら可能」というレベルでしかありません。これはProperty Exchangeで必要とされるパケットの分割に対応するのが面倒くさいという問題と、リアルタイムシステムを前提としたMIDIの世界においてProperty Exchangeの実装で必要となるJSONをどのようにサポートするかという厄介な問題を含んでいるためかもしれません。筆者もMIDI-CIをKotlinで途中まで実装してあるのですが(2023年6月アップデート未対応)、Property Exchangeは面倒なので未着手です。

JUCE 7.0.9におけるMIDI-CIのサポート

juce_midi_ciモジュール

JUCE 7.0.9でMIDI-CIのサポートが追加されました。JUCEのlead developerの手によるもので、相応に本気度の高いコードになっています。これは現時点ではjuce_midi_ciという新しいモジュールに含まれています。juce_audio_devicesjuce_audio_basicsではありません。具体的にはISCライセンスではなくGPLv3ライセンスという大きな違いが出てきます。

juce_midi_ciは、プラットフォームのMIDIアクセスAPIからは独立して実装されています。バイナリストリームの処理とMIDI-CIの処理フローを体現したInitiatorとResponderの構成が中心です。

MIDI-CIの対応バージョンも1.2なのでしっかり最新版に追従しています。さらに(これはMIDI-CI仕様で要求されていて面倒くさいのですが)バージョン1.1以前のメッセージも正しく解析できるようになっています(生成のサポートは不要です)。一部のMessageクラスフィールドは、MIDI-CIバージョン1.2のメッセージでのみ有効です。

juce_midi_ciモジュールの主な役割は次のカテゴリーに分けられます。全てjuce::midi_ci namespaceに含まれます。

  • Message (namespace) : MIDI CIのデータ生成と解析
  • Device : MIDI-CI Initiatorの実装
  • Responder : MIDI-CI Responderの実装
    • ProfileHost, PropertyHostなどが個別のホスト機能を分散して担当している
  • Encodings : 7-bit encodedのストリームからの8-bitストリームへの変換実装や、Mcoded7の実装など
  • ResponderOutput : MIDI-CIバイトストリームの出力を抽象化したもの

MIDI-CIメッセージは以下の表のように定義されています。

機能 Initiator Responder
バイスの検出 Discovery DiscoveryResponse
エンドポイントの探索 EndpointInquiry EndpointInquiryResponse
MUIDの無効化 InvalidateMUID -
通知類 ACK, NAK
プロファイル問い合わせ ProfileInquiry ProfileInquiryResponse
プロファイル通知 ProfileAdded, ProfileRemoved
プロファイル詳細取得 ProfileDetails ProfileDetailsResponse
プロファイル有効化 ProfileOn ProfileEnabledReport
プロファイル無効化 ProfileOff ProfileDisabledReport
プロファイル固有詳細 ProfileSpecificData
プロパティリスト取得 PropertyExchangeCapabilities PropertyExchangeCapabilitiesResponse
プロパティ取得 PropertyGetData PropertyGetDataResponse
プロパティ設定 PropertySetData PropertySetDataResponse
プロパティ通知登録 PropertySubscribe PropertySubscribeResponse
プロセス問い合わせ ProcessInquiry ProcessInquiryResponse
プロセスMIDIレポート ProcessMidiMessageReport ProcessMidiMessageReportResponse, ProcessEndMidiMessageReport

ちなみに、筆者がコードリーディングした限りでは、2023年6月に新しく仕様に追加されたProcess Inquiry関連のメッセージ処理を実装しているコードはありませんでした。

CapabilityInquiryDemo

juce_midi_ciモジュールでどんなことができるようになるかは、JUCE/examples/Audio/CapabilityInquiryDemo.hというデモアプリで試せます。5000行近くあるそれなりに大規模なサンプルです。JUCEのPIPになっているので、PIPとしてビルドすれば足りそうですが、JUCEトップディレクトリからcmakeでビルドするのが簡単でしょう:

cmake . -B cmake-build -DJUCE_BUILD_EXAMPLES=ON -DJUCE_BUILD_EXTRAS=ON
cmake --build cmake-build --target CapabilityInquiryDemo

-DJUCE_BUILD_EXAMPLES=ON -DJUCE_BUILD_EXTRAS=ONをcmakeの引数として追加すればCLion NovaみたいなIDEからデバッグも可能です。./cmake-build/examples/Audio/CapabilityInquiryDemo_artefacts/CapabilityInquiryDemoなどの実行ファイルがビルドされるはずなので、これを起動して、MIDI-CIデモの動作を確認してみましょう。

CapabilityInquiryDemo

このアプリではプラットフォームのMIDIバイスが列挙されますが、これはjuce_midi_ciの機能ではなく、デモアプリのコードとして実装されています。

CapabilityInquiryDemoアプリの中では、MIDI-CIのメッセージは、この"MIDI I/O"のタブで選択されたMidiInputMidiOutputの間で送受信されることが前提になります。最初にDevice Discoveryメッセージのやり取りが成功すると、"Discovery"タブの内容が更新されます。

Discoveryの結果が反映されている様子

Profilesのリストは空白ですが、これは今回テストで稼働していたMIDI-CIクライアントがProfile Inquiryに空白で応答していたためです。あと、このスクショでは見えませんが、下の方にはさらにProperty Exchangeのためのリストも含まれています(これもProperty Exchangeをサポートする実装をもっていないので今回は何も出ていません)。リスト項目が出ていればSet Profile On/OffとかProperty Get/Setみたいな操作も可能になるでしょう。

"Logging"タブを開くと、このアプリが送受信したMIDI-CIメッセージのログを参照できます。

Loggingタブの内容

CapabilityInquiryDemoアプリでは、DiscoveryResponseが適宜処理されると、続いてProfileInquiryを投げて、その結果はこのタブの"Profiles"の項目に反映されるようですが、筆者はProfile Configurationを処理できるMIDI-CI実装をもっていないので、とりあえず実際に動かしてみたものはここまでとします。Device(とResponder)のAPIの使い方次第なので、これ以上はCapabilityInquiryDemoでこれらのクラスがどのように使われているかを追及してみてください。

誰がこのAPIを使う必要があるのか

MIDI-CI APIは、MIDI 2.0の機能を活用できるMIDIバイスMIDIシステムの開発者が使用するものであって、現状では利用する意味のある開発者はかなり限定的です。たとえばまだMIDI 1.0しかサポートされていないJUCEプラグインAPIを使っている開発者にとってはほぼ無意味です。AudioUnitのようにMIDI 2.0プロトコルを利用できるとされている場合は、MIDI-CIに対する問い合わせに応答することにはそれなりの意味があるでしょう。

スタンドアローンアプリケーションでMIDI入力を送受信する場合は、このAPIが便利である可能性は高いです。たとえば楽器メーカーがMIDI 2.0対応デバイスを開発するとき、PC側ホストでMIDI 2.0サポートを実装する必要があるなら、MIDI-CIを使用する必要性が高いでしょう。そういうときはこのjuce_midi_ciが役に立つかもしれません。

筆者は、この記事を書くために、CapabilityInquiryDemoと相互運用できるか試してみる目的で、atsushieno/ktmidiに含まれるMIDI-CI実装を2023年6月仕様に部分的に追従させてみましたが、CapabilityInquiryDemoはMIDI-CI開発のヘルプツールとしてなかなか有用でした。JUCEを利用できない場合(ktmidiのようにC++でない場合など)でも、利用する場面はあるかもしれません。

筆者が1年くらい前にktmidiで作っていたMidiCIInitiatorMidiCIResponderというクラスが、それぞれJUCEのDeviceResponderに近い構成なので(プラットフォームAPIから独立している部分まで含めて類似)、誰が作ってもおよそ似たような構成になるのだろうと思います。CoreMIDIはもう少しエンドユーザー向けのAPIになっていたと記憶していますが、2023年6月のアップデートを受けて何かしら変更が加えられたかもしれません(未確認)。

(本稿執筆時点で筆者の環境でALSAサポートが期待通りに動いていないので稼働サンプルが出せませんが、上記MidiCIResponderを使ったシンプルなコードです。)

11月の開発記録 (2023)

11月、いつも31日まであると思ってしまうのですが、これは一生治らないのかもしれん…

aap-core 0.8.0, almost there

2ヶ月以上前から取り掛かっていたAAP拡張機能の再編成ですが、今月初頭にようやくひと段落して、ABIに影響するAPIの変更はほぼ発生しなくなってきたので、バージョンを新たに0.8.0とする準備ができました。これに合わせて、AAP拡張機能のバージョンも全て v3 に統一されています。

(まだ最終リリースを出していないのは、Android Gradle PluginがAAPのAARをimplementation project("...")ではなくMavenLocalからパッケージとして解決したとき「だけ」各種の不具合が発生するという問題を解決してから、各種プラグイン移植のコードに影響を及ぼさずにリリースしたいという思いがあるためなのですが、これはAGPの深淵を掘り下げないと直せない気がするので、たぶん早々に諦めて現状でリリースすると思います。すでにaap-lv2プラグインはほぼ全てこのバグに対処済です。aap-juceプラグインは…特にProjucerベースのやつを直したくない…)

先月リストアップしたAAP拡張機能(とAAPXS = AAP拡張機能サービス機構)の課題はこんな感じでした:

  • 拡張機能APIが同期メソッド前提なので、非同期前提にする
  • リアルタイム処理時に文字列比較しなくてすむようにURIDを導入する
  • プラグインコードもホストコードも直接認識していないような拡張機能操作のメッセージをやり取りする仕組み(AAPXS untyped runtime)
  • Kotlinからの拡張機能のuntypedな呼び出し
  • 動的にロード可能なAAPXS
  • ホスト拡張機能
  • 独立性の高いAAPXS実装用ヘルパーAPI

このうちまだ実現していないのはKotlinからの呼び出しのみです。まあ現状「呼び出せる可能性」さえあれば十分かなと思っています。

まだ拡張機能APIの非同期化がホストアプリケーション(androidaudioplugin-manager)のレベルまで浸透しているわけではないのですが(JNI越しに非同期呼び出しを実現するのはやや面倒)、リアルタイムAAPXSで拡張機能の呼び出し時にロック機構を導入せざるを得なくなってからaap-juce-byodでプリセット一覧の取得に何秒もかかっていたのが、v0.8.0ではミリ秒単位で完了するようになっています。

気が向いたらARA2サポートとか実装してみたいところですが、さすがに時間がかかりそうな気がします(APIそのものをあまり把握していない)。ARACLAP.hと同程度のことができればいいのであれば、もしかしたらすぐできるのかも…?

alsakt 0.3.3

先月ALSA 1.2.10対応として0.2.0を出したばかりのalsaktですが、JavaCPP力でライブラリをバンドルする方法を調べて、JavaCPPランタイムが自動的にjarから展開してロードできるようになりました。ローカルにインストールされているlibasound.soが1.2.10でなくても、Linux kernelが6.5以降であればUMPエンドポイントを構築できるようです。

ktmidi最新版(未リリース)のktmidi-jvm-desktopモジュールにはAlsaMidiAccessというクラスがありますが、このvirtual portを作るときにPortCreatorContext.midiProtocol1にするとMIDI 1.0 UMPで、2にするとMIDI 2.0 UMPで、ALSA sequencerのポートが作成されます。

作成されたUMPポートではMIDI 1.0バイトストリームではなくUMPストリームを受け取れるようになっていると思うのですが、まだatsushieno/kmmkに手を加えたものしか手持ちのUMPアプリがなく、十分に試せていない状態です。

ktmidiのMIDI-CIアップデート

Twitterを見ないので最近まですっかり忘れていたのですが、12月はAdvent Calendarの季節…ということで、JUCE Advent Calendar 2023に参加します。実のところ今年はほとんどJUCEでコードを書いていないのでネタが無い…と思っていたのですが、最近そういえばMIDI-CIサポートが追加されたとJUCE開発者から聞いて眺めていたので、つい先日7.0.9で正式リリースもされたし、これがちょうどいいだろうと思って書くことにしました。

それで、その実験をやるためにはMIDI-CIの実装が必要だな?となって、ちょうどalsaktでUMP対応もしたしMIDI 2.0まわりの実験には向いているのではないかと思って、ktmidiに1年前に仕込んでいたMIDI-CIサポートを活用してみることにしました。当然ながら2023年6月アップデートには未対応だったので、そこから着手しています。一応繋がってパケットのやり取りは一部できていたようですが、まだ安定的に相互運用できる状態になっていない状態です。まあ詳しくはAdvent Calendar当日の記事を見てもらえればと思います(記事は作成済)。

ADC23(オフライン参加)

4年ぶりにaudio developers conferenceに現地参加してきました。10月の5回目ワクチンが効いたのか、混雑しているカンファレンスからも無傷で帰還しました(ロンドンの感染状況も悪くないですし)。

ADCオンラインでのコネクションは、正直3年間で全く増えなかったので、初めてリアルで会った知人はほとんどがTAP Discord等での繋がりだったと思います。旧知の知人も割と戻ってきているようでしたが、まだまだ回復しきっていない様子でした。まあ4年も開いたらその間に業界が変わった人もいるだろうし(?)

今年参加して気づいたのは、以前なら日本の楽器メーカー(Roland/YAMAHA/KORGあたり)から人が来ていたのですが、流石にまだ戻ってきていない感じでした。今年はDreamtonicsがSynthesizer Vの展示を出していたので、日本人参加者の大半はその方面だったと思います(SynthV関連のセッションもありました)。

自分の関連技術方面では、CLAPの開発者が来ていたので前日からいろいろ話を聞いたり(自分もプラグインフォーマットを作ってるんだ、みたいな話もしたり)、ask the JUCE teamでいろいろ相談したりはしましたが、主に他の分野の話を聞いてきた感じにはなりました。またADCの動画が公開されてきたらいろいろ話せるかもしれません。

12月の予定

AAP 0.8.0リリースがひとつ完了予定のタスクですね。多分その後にGradle/AGPまわりを調査することになって、コードの追加は控えめになると予想されます。

ktmidiのMIDI-CIサポートは、もう少し時間をかけて使い物になるようにするか、他の作業を進めるか(↑のGradle/AGPまわりの調査もかなり気合が必要)、迷っているところです。MIDI-CIそのものは、特にMIDI-CIからProtocol Negotiationが消えた今、自分のコードではほぼ不要になったので、優先度は低いです。とはいえ一般的なMIDI 2.0アプリケーションを書くには必須の技術とはいえそうです(だからこそJUCEでも内部的なMIDI 2.0サポートに先駆けて実装しているわけで)。まあ12月の方向性はそんな感じです。

MIDI 2.0 UMPガイドブック 第2版のリリース

11/11から始まる技術書典15に合わせて、「MIDI 2.0 UMPガイドブック」の第2版を発行しました。技術書典Webおよびオーディオプラグイン研究所のboothで入手できます。既刊のアップデートとして登録してあるので、初版を購入済みの方は追加費用なしでダウンロードできます。

fedibird.com

xamaritans.booth.pm

MIDI 2.0のJune 2023 Updatesに合わせた内容のアップデートであり、大部分はここの過去エントリからのコピーに手を加えたものです。MIDI Clip Fileについての解説も付録として含めてあります。

atsushieno.hatenablog.com

atsushieno.hatenablog.com

UMP stream messageなどの解説が殆ど入っていないのですが、これは気が向いたら追加するかもしれません(エンドユーザー的にはだいぶニッチなのとMIDI-CIまわりの補助的な解説がだいぶ必要になるので、今のところ不要かなと思っています)。

MIDI 2.0仕様と同様、本書も3年ぶりのアップデートとなります。3年前と比べると特に今年は状況が良くなってきた感がありますが、まだ各方面が「現在開発中」ステータスなので(CoreMIDIとALSAのみが安定している状況)、それらを使ったMIDI 2.0プログラミングの解説は含まれていません。libremidiやJUCE(最近MIDI-CIサポートがdevelopブランチに追加されましたね)などもそのうち安定して使えるようになる見込みはあります。JUCEのUMPサポートは入ったり消えたりでしたが…! Windows MIDI Servicesは正直わかりません(あんまし進捗が見えない)。

atsushieno/cmidi2(自作UMPサポートライブラリ)の解説はそのまま残してあります。最近もAAPでリアルタイムのSysEx8生成/解析が必要になって実装したばかりで、自分ではそこそこ現役で使うライブラリになっています。機能の完成度という意味ではktmidiのほうが進んでいるのですが、最近のエントリでも書いた通りまだalsaktが未完成のところがあるので特に言及を加えたりはしていません(まあKotlin開発者がMIDIライブラリを探せばすぐ出てくるし)。

第1版の印刷版はほぼ完売してboothでの販売もしていないのですが、第2版の印刷版は用意していません(ごめんなさい)。あと(前回エントリでも書きましたが)11/12の技術書典15オフラインはADC23とスケジュール的にかぶっているので不参加です。機会があれば技術書典16でお会いしましょう。

10月の開発記録 (2023)

例年であればスケジュール的に追い込まれている10月ですが、今年はM3にサークル申し込みし忘れ、11月の技術書典15にはオンラインのみ参加の予定で、新刊の準備もMIDI 2.0 UMPガイドブックの新版(June 2023 Updates対応)のみの予定(これは別途告知エントリーを書くつもりです)…というわけで、主に開発作業にかかっていました。

alsakt 0.2.0

今月ようやくLinuxデスクトップ環境をKernel 6.5にアップデートして、MIDI 2.0対応のALSAが組み込まれた環境にして、alsaktをMIDI 2.0対応の追加されたalsa-lib 1.2.10のAPIに追従させました。alsa-lib 1.2.10がインストールされた環境であればMIDI 2.0接続が可能になっていると思います。「思います」というのは、実はまだ動作確認できていないためです。

alsa-libについて調べた結果、どうやらlibasound.soをJavaCPPパッケージにバンドルしなければならないということが判明し、それがユニバーサルなビルドとして可能かどうか(Linux x86/x64のほかLinux arm64等も対応できるのか)も要調査…というステータスのまま今に至ります(他の作業のほうが優先度が高い…)。

ktmidiもこれに合わせて「MIDI 2.0の仮想デバイスを作成できる」つもりのところまではアップデートしたのですが、alsaktが動作未確認なので、こちらはバージョンすら上げていない状態です。ktmidiのレベルでMIDI 2.0サポートが確認できたら、次はkmmkのMIDI 2.0サポートの動作確認ということになるでしょうが(MIDI2サポート自体はAndroid + AAP用にすでにkmmkで実装済み)、この辺は気が向いたらやると思います。

AAP extensibility service enhancements

AAPの開発は、先月に引き続き拡張機能とAAPXS (AAP extensibility service) の再設計が中心になっています。先月はrealtime safeな拡張機構を実現するためにBinderの専用関数の代わりにMIDI 2.0のSysEx8メッセージをオーディオ処理と一緒に渡す仕組みを実装していました。今月はこの基本部分が動くようになったので、mainブランチに大量の変更を取り込んであります。

これは拡張機能サポートの大幅な変更になりましたが、まだ最初の一歩に過ぎなかったという状況が見えてきました。他にやるべきこととしては、以下のような変更が必要そうだというのが見えています:

(1) 既存の拡張機能サポートランタイムは、拡張機能の基本部分が同期メソッドを前提とした(非同期メソッドを前提としていない)設計に基づいているので、望まないブロッキングが発生します。根本的なAPIの再設計が必要になります。

(2) LV2 URID類似のindexed URIサポート: UMPのSysEx8に含まれるURIなどはパケット解析した結果得られる文字列バッファとなるため、non-interned stringを同値比較しなければならなくなってしまいました(interned string同士の比較であればポインタのアドレス比較だけで済んでいたわけです)。文字列の同値比較を回避する手段はあって、LV2のURIDという拡張機能が一例としてあります。URIに対応する整数をホストとプラグインの間で合意しておくことで、URIの代わりにint32_t等で識別できるようになります。

(3) AAPXS untyped runtime: 拡張機能のやり取りが、CのAPIではなくバイナリデータのプロトコルというレベルで規定されることになった結果、ホストやプラグインがそれぞれ認識しない拡張機能であっても、特に操作しないまま受け渡すことが可能になりました。ユーザーがある拡張機能を使用してプラグインにメッセージを送りたい場合、その拡張機能をサポートしないホストDAWを使っていても、任意のSysEx8メッセージを送信できるものであれば、それをプラグインに送ることがなお可能です。一方で認識できている拡張機能の操作については、バイナリメッセージを解析して然るべき処理に渡すことが求められます。

拡張機能の呼び出しをプロトコル指向で実現するというのは、拡張可能性を考慮したプラグインのフォーマットとしては割と画期的なのではないかと思います。プロトコル指向という意味ではLV2 Atomも同様のアプローチであると言えますが、LV2 Atomのシーケンスを編集できるDAWは普通はありえないでしょう(LV2に特化したものであればありえますが)。MIDI 2.0メッセージであれば将来的には可能性があります(1.0から変換してもいいですし)。

プロトコルは実装を伴う必要があるので、この仕組みをサポートするC/C++のuntyped runtimeが必要になります。

(4) KotlinからのAAPXS untyped runtimeの操作: 0.7.8時点でのAPIでは、拡張機能をサポートするメソッドは、全て個別具体的に定義・実装されたaap::PluginInstanceの関数を呼び出すことになります。これでは既知の拡張機能しかサポートできませんし、拡張機能が増えるたびにaap::PluginInstanceの対応コストが増大することになります。そして一番の問題はKotlinでクライアントを開発している場合です。C++ホスティングAPIにAAPXS untyped runtimeを用意した上で、そのAPIを直接呼び出せるKotlinのAPIが必要になります。

(5) 動的にロード可能なAAPXS: AAPXSの仕組みは、C++コンパイルして統合すれば十分機能するともいえますが、複数バージョンのホスト(のリファレンス実装)で問題なくロードできる拡張機能ライブラリをサポートしようと思ったら、CレベルのABIが必要になります。(拡張機能を動的にロードする場面は実用上は無いはずなのであまり重要な問題ではありませんが、できるならやっておいてもよい事項です。)

(6) ホスト拡張機能のサポート: AAPに拡張機能サポートが追加されたとき、それはプラグイン拡張機能を意味していました。ホストがプラグイン拡張機能APIを呼び出す、プラグイン拡張機能APIを実装する、という構図です。コレに対して、現在のAAPにはホスト拡張機能のコンセプトが実装されています。CLAPがプラグイン拡張機能とホスト拡張機能の両方を定義しているのと同様です。

このホスト拡張機能のサポートは、AAPXSの仕組みとしてはきちんと規定されておらず、ホストのリファレンス実装で独自に直接実装していました。Binderにコールバックオブジェクトを用意しておいて、そのhostExtension()というメソッドを呼び出すだけになっていました。AAPXSがリアルタイムモードでオーディオのprocess()のみ利用可能になるという状況には対応できていないので、新たにもう1系列のAAPXSサポートを追加する必要があります。

(7) 独立性の高いAAPXS実装手段の提供: AAPの現在のAAPXSサポートには、リファレンスモデルといえるような規範的なモデルがありません。既知の拡張機能についてはStandardExtensionsというクラスが用意されていて、C++の複雑なマクロとクラス階層構造の上に実装されていて、整理が追いついていません。

10月は、これらの問題を割りと試行錯誤を繰り返しつつ洗い出しながら、包括的に解決できる仕組みを再構築しており、だんだん形になりつつはありますが、新しいブランチにのみ変更が溜まっていくような状況が続いています。

11月の予定

10月で開発に区切りが出た様子はまったくないので、11月も引き続きこの拡張機能まわりのサポートを設計しつつ実装していくことになるでしょう。あとは上記のalsa-libまわりの問題を解決してktmidiのMidiAccessにMIDI 2.0サポートを追加したものが出てくる可能性もなくはないです。あと、4年ぶりにADC23にオフライン参加する予定なので、その間は何も進展しないだろうし、内容次第でまた開発の方向性も多少影響を受ける可能性があるな、と思っています。ADC23があるので技術書典15はオンラインサークル参加のみの予定です。

9月の開発記録(2023)

近況: 多忙です

mastodon.cloud

9月はAAPの開発以外ではちょっとsfizzにパッチを作る程度の活動しかしていないので、AAPの話だけを書きます。

AAPは現時点で開発中のGUIサポートもMIDI 2.0出力を使った通知機能も拡張機能のリアルタイムサポートも完成していない状態ですが、新しいプラグインマネージャーを含み、かつ0.7.7と互換性を維持しているバージョンとして、0.7.8リリースをリリースしました。

github.com

0.7.8の主な機能はv0.7.9あるいはv0.8.0に向けて後方互換性を破壊できることなのですが、新プラグインマネージャーと、部分的な拡張機能のリアルタイムサポートが中心です(これについてはすぐ説明します)。

今月はまだ上記の各種タスクが仕掛中ステータスのままなのですが、その設計の話をまとめます。今月はその過程でState拡張とPresets拡張の設計にかかる話を少しだけ切り出してまとめたものも書きました。

atsushieno.hatenablog.com

拡張機能のリアルタイムサポート 前提: AAPXS

AAPでは、モダンなオーディオプラグインフォーマットで一般的に見られる拡張機能の仕組みをサポートしていますが、Androidのようなモバイル環境ではホストでプラグインを動的ロードできず、拡張機能の呼び出し = プロセス境界越え、となるので、単に「他所のプラグインフォーマットAPIを真似して決め打ちで定義する」だけでは実現しません。(いや、まあ最初はそのレベルの認識で始めたわけですが…!)

もしCヘッダーレベルのAPIだけを定義して「実装は実装者の責任」ということになると、各プラグインと各DAWがそれぞれAAPのようなBinder IPCを実装することになって、実装コストがえらいことになりますし、それはおそらく相互運用できないレベルのものになるでしょう。そういうわけで、プラグインDAWの開発者には、一般的なプラグインフォーマットにおけるCヘッダーレベルのAPIの実装を求め、それより先のIPC部分は「拡張機能の開発者がホストとプラグインの橋渡し部分を実装する」というかたちで責務を分散しています。

そのおかげで、AAPの実装はその後2, 3年ほど変遷を経ていますが、拡張機能のIPCにかかる部分は、aap-lv2、aap-juceとも、個別のプラグイン実装を変更する必要が無かっただけでなく、LV2拡張機能へのマッピングやJUCEの機能の実装をフレームワークの変更に追従させるという意味での変更も必要ありませんでした。拡張機能にかかる変更は、全てその下のレイヤーで吸収していた、ということになります。

AAPの標準拡張機能の開発者は自分1人なので(そもそもAAPプラグインもホストも自分しか開発していませんが…!)、実質的に「自分が実装する」とイコールだけど、たとえばLV2のexternal UIみたいな独自拡張を作ろうと思ったら、その開発者はatsushienoと同じ作業をすることになります。

この橋渡し部分は、AAPXS (extension service) と呼ばれ、AAPにはAAPXSの実装をロードするためのC APIがホスト側・プラグイン側の両方についてあります。この部分の舵取りで難しいのは、拡張機能の「実装」は、自分が実装しているAAPの「リファレンス実装」とは切り離されていなければならない、というところです。そうしないと、リファレンス実装のバージョンアップがあって実装の非互換が生じた時に、サードパーティ拡張機能が古いライブラリへの依存関係を引きずってロードできなくなってしまいます。

また、C++ APIではなくC APIとしておくことで、将来コンパイラツールチェインの変更などに伴ってABIに破壊的変更が生じたとしても、その影響を受けなくなります(これに失敗してmingwが必須になったのがC++APIを規定するVCVRackだと言われています)。AAPのリファレンス実装におけるAAPXS実装では、C++が使われています。

AAPXS and RT safety

現在開発中ではない、従来のAAPXSを支えるのは、Android Binderを用いたIPCの仕組みです。Binderそのものはrealtime safetyを実現しているものの、アプリケーションには解放されていないというのが現状で、vendor libraryを作れるなら対応可能、みたいなレベルではあります。それはそれで問題ですが、いずれにせよ、その上で動作するRT safeな仕組みを作ることが重要で、それが無いとベンダーサポートやAOSPへの変更を交渉するとしても単なる画餅になります。

拡張機能の呼び出しは、共有メモリ上に引数と戻り値を配置したうえでextension()というAIDL定義されたメソッドを呼び出す、というかたちで実現しています。このメモリ配置のレイアウトは拡張機能の開発者が自分で規定すれば良いので、ホストやプラグイン開発者、あるいはフレームワーク開発者(atsushieno)が関知することはありません。

ホストがいつ呼び出すかわからない拡張機能の関数から呼び出されるこのメソッドを呼び出すのは、オーディオスレッドというわけにはいかないので、非オーディオスレッドで呼び出すことになります。そのため、AAP 0.7.7までは、拡張機能のサポートは本質的にnon-RT safeということになっています。

また、仮にextension()がnon-RT safeで良いとしても、プラグインがアクティブな状態でオーディオ処理がオーディオスレッド上でリアルタイムで動作しているときに、non-RT-safeなextension()の呼び出しスレッドがそのBinderの制御を奪い取ると、リアルタイムで動作しているオーディオスレッドもブロックされてしまうことになります。これを防ぐには、リアルタイム処理中はオーディオ処理のprocess()のみを許容し、extension()その他すべてのBinderメソッド呼び出しを禁止しなければなりません。

AAPのMIDI 2.0チャンネルの活用

モダンなプラグインフォーマットでは、オーディオ処理において、パラメータの変更などをsample accurateにプラグインが処理できるようなかたちで、イベントリストをオーディオバッファと合わせて渡してやる仕組みが一般的です(イベントリストを渡す仕組みが無い場合は、オーディオ処理とは別のサイクルでパラメーターを設定する拡張機能などを呼び出すことになります)。

この、イベントリストでの拡張機能の呼び出し等を可能にするために、LV2にはAtomが存在します。CLAPにはLV2のような「エレガントな」設計はありませんが、clap_event_*_tの各構造体をclap_plugin_tprocess関数(関数ポインタの呼び出し)に渡せるようになっていて、同様の目的を果たせています。

AAPでも、MIDIのノートイベントなどは、オーディオ処理中のMIDI 2.0入出力チャンネルで、UMPとして送受信することになっています。このチャンネル上でLV2 Atomのような構造を実現すれば、ある程度の拡張機能の呼び出しがRT safeなかたちで実現できることになります。AAPの場合、パラメーターの変更はAAPのMIDI拡張を定義するaap/ext/midi.h上でParameter SysEx8メッセージとして定義されていますが(他にもNRPNやCCを使うことも設定次第で可能です)、これと同様のSysEx8メッセージによって、任意の拡張機能の呼び出しを可能にできるはずです。拡張機能の引数と戻り値は共有メモリ上に書き込まれているメモリブロックなので、これをそのままSysEx8などのデータにしてしまえばよいわけです。この仕組みは、ラベルとしてわかりやすくするためにAAPXS SysEx8と呼ばれています。

そういうわけで、今月は主にこのAAPXS SysEx8の設計作業を行っていました。コードでいうとこの辺:

github.com

(ちなみに、パラメーター変更をNRPNやCCにマッピングできるAAP MIDI拡張の機能を実現していて、残った課題が「プログラムチェンジのメッセージをset_preset()の呼び出しに差し替える」という項目で、それを実現するというのが今回のrealtime safeな拡張機能の呼び出しに繋がったわけですが、枝葉の話なので省略します。)

"set preset" in 0.7.8 plugin manager UI

AAP 0.7.8では、拡張機能の呼び出しを、「部分的に」このMIDI 2.0チャンネル上でシステムエクスクルーシブ メッセージとしてやり取りできる基盤が追加されています。8月に実装した新しいplugin manager UI上でプリセットを変更する処理は、リアルタイムモードの場合はAAP本体の拡張機能呼び出しの機能をスキップして、直接MIDIチャンネルにプリセット呼び出しのSysEx8を追加する仕組みを作って呼び出しています。

これはProof of Conceptをdogfoodingするためのやっつけ仕事でしかないのですが、それはこの仕組みをきちんと導入するとABI breakageが発生して従来のバージョンのAAPプラグインと互換性が損なわれてしまうために、妥協せざるを得なかった部分です。0.7.8リリースの主な機能は後方互換性を破壊…などと書いたのはこのためで、このリリースが終わった今、この部分を一般化する作業に着手できるわけです。

「破壊的変更」としたのは、Binderのextension()呼び出しでは明示的に要求していなかったリクエストとレスポンスのバイナリをUMPにシリアライズするときに必要になりそうだったからです(が、現状では必要なく回っています)。

Reply, and Async Reply

Binderの呼び出しをリアルタイムMIDI2 UMPメッセージで置き換える仕組みは作りましたが、ここに来て面倒な問題が残っていることに気づきました。AAPXSのホスト = プラグイン間のやり取りは同期的なAPI定義に基づいて実装されているということです。

AAPXSを呼び出すスレッドがオーディオスレッドと別であったときは、それぞれの呼び出しスレッドが同期的に呼び出されて結果を返せていればよかったのですが、オーディオスレッドでMIDI2 UMP化してリクエストが送信された結果を、(どれくらいかわからないけど)後になってレスポンスで返されるまで待機して、それから結果をコールバック等で返す、という仕組みにするためには、AAPXSのAPIが非同期呼び出し前提で設計されている必要があります。

この非同期APIの実装において重要なのはスレッディングモデルの切り替えです。非同期呼び出しの拡張機能のリクエストはリアルタイムに送信されて来ますが、そのスレッドで拡張機能をそのまま呼び出すわけにはいかないので、一度non-RTなワーカースレッドに処理を移譲する必要があります。それが完了したら、その結果をAAPXS SysEx8としてホスト側に送り返すことになります。

非同期処理はこのようなモデルになっていますが、一方でリアルタイム呼び出し1回で完結する前提の拡張機能もあって、それらは非同期呼び出しへのディスパッチと結果の収集を前提に設計すると、目的を果たせないことになります。そのため、AAPXSのAPIを非同期呼び出し「のみ」にすることもできません。どちらかを設定することになります。

こんな複雑な設計が求められる時点で、AAPの拡張機能の開発はだいぶ難易度が高くなったな(atsushieno にしかできなそう)と思いましたが(あと「AppleもAUv3で同じくらいややこしい問題をかかえて破綻してほしい〜」とか思いましたが)、まあリアルタイム拡張機能呼び出しを実現するためには超えないといけない壁なので、やるしかないですね。

とはいえ、冒頭に書いた通り、今は異世界の仕事をこなすので無職は手一杯なので、そっちの仕事が落ち着いたらこっちに戻ってきます。

分離プロセスのオーディオプラグインにおけるStateとPresetsに関する覚書

parameters, states, presets

オーディオプラグインの挙動は、主にパラメーターとそれ以外の状態 (state) に基づいている。GUI上で調整しているとこれらの違いをあまり意識しないが、パラメーターは主に32bit float、使い方によっては64bit floatひとつで設定するもので、それ以外の状態のデータ型や形式はプラグイン次第だ。これは一般的なオーディオプラグインフォーマットに共通する挙動といってよい。大抵は拡張機能の方式で規定されている。

32bit float(あるいは64bit float、面倒なので以降は言及しない)と決まっているパラメーターは、DAWによって、オートメーションで処理できたり、演奏処理中に動的に変更できることが多い。それ以外の状態を演奏中に動的にプログラマブルに変更することは、理論上は可能だが、状態の変更は多くの場合は全パラメーターの変更などを伴い、場合によってはデータファイルのロードも伴い、リアルタイムで処理できない程度に重いことが多いので、一般的ではない。

状態は基本的にユーザーがGUI上で(GUIが機能を提供する範囲で)自由に設定でき、セーブ・ロードが可能で、この中にはパラメーターの現在の値なども(一般的には)含まれる。これに対して、プラグイン側が最初から実用的な楽器・エフェクトの「状態」をプリセットとして提供していることがある。言い換えれば、プリセット機能は一般的には状態の機能の応用として提供されている。

VST3の場合、プリセットはvstpreset等のファイルに保存されるが、これは状態と同じデータを保存しているようだ。LV2の場合、プリセットはLV2で幅広く用いられるRDF Turtleのデータ形式で共有されることが多いが、Pluginオブジェクトにそのまま反映できるようにPluginオブジェクトに類する構造として保存されるようだ(これによってControlPortの内容などをそのまま適用できる)。

stateのセーブとロード

プラグインの状態の保存処理は、DAWで音楽を打ち込んで保存するとき、各トラックにおける各プラグインについて発生する。逆に楽曲をロードすると、それらのプラグインの状態が復元されることになるが、フリーズしたトラックにおけるプラグインなどを考慮すると、すべてがロード時に復元するとは限らない。

GUIの表示・非表示の切替時には、状態が変更されるわけではないが、プラグインの実装次第ではGUI表示処理から何らかのパラメーター変更処理が走る可能性はゼロではない(バグの可能性も高い)。パラメーターとは異なり、通常はホストから状態が直接操作されることはないが、プリセットの操作というかたちではホストから状態が操作されることがある。

また、DAWの操作次第では、プラグインが「複製」されることがある(たとえばトラックを複製した場合)。この場合、状態も全てプラグインの新しいインスタンスにコピーされることになる。

stateデータの内容

どのプラグインフォーマットでも、状態を保存した結果はバイナリblobになる。この内容が、もう少し具体的なセマンティクスに基づく、型情報や文脈のあるデータの集合体になっていることもある。

LV2のState拡張では、状態の全体をsave関数で保存しrestore関数でロードすることになっているが、その中では状態の各要素をkey-valueストアのようにkey、size、typeを明示してstore / retrieveすることになっている。型としてはLV2 Atomの使用が想定されている。LV2 AtomであればLV2の標準メタデータフォーマットであるRDF Turtleファイル(.ttlなど)で読み書きでき、プラグインが提供するプリセットのデータとしても有用だ。

AudioUnitにおける状態管理も似ていて、AUAudioUnitならfullStateあるいはfullStateForDocumentで、全プロパティと全パラメーターの情報が格納されることになる。それなりの大きさになることが想定されるので、たとえば分離プロセスで動作するプラグインの場合に、IPCのメッセージにこのバイナリを格納して送受信するというのは筋が悪い。ホストとプラグインの間に共有メモリのチャンネルを用意しておいてデータ本体はそこで受け渡し、IPCでは制御命令のみを送受信するというアプローチが妥当だろう。

(モバイルプラットフォームにおけるプラグインフォーマットの場合、そもそもプラグインは巨大なメモリ空間を占有すべきではなく、したがってStateデータも小さく抑えられるべきで、場合によっては共有メモリ上でStateを全展開するのはふさわしくないかもしれない。いずれにせよIPCでやり取りするのは適切ではない。)

筆者が開発しているAAPでは、stateデータのフォーマットをMIDI 2.0 UMPのストリームとしてしまって、パラメーターの状態はそのためのSysEx8メッセージ(MIDIメッセージとしてプラグインにリアルタイム送信するために別途規定してある)や、あるいはデフォルトでパラメーター番号に対応しているCCやAssignable Controller (NRPN) で保存し、それ以外のバイナリはSysEx8やMDSのblobとしてで保存してプラグインが自分で復元する、というやり方を計画している(すでにオーディオ処理で用いられているフォーマットなので実行に移すのは簡単だけど、破壊的変更になるのでバージョンを上げる機会を待っている)。Stateの内容をどの程度セマンティックにするかはプラグイン開発者次第だ。

YAMAHA DX7ではMIDI標準のSysExで音色パラメーターをダンプして復元できた。現代でも多くのエミュレーターがそれらを含むCartデータファイルをサポートしている。汎用的なメッセージフォーマットでデータのやり取りが可能になれば、将来的な再利用性も高まる。

Stateのロード方式とフォーマット

LV2のStateで使われるLV2_State_Flagsは面白い。Stateを保存するのは、データの永続化が目的である場合と、DAWプラグインのプロセス間あるいはプラグイン本体とUIの間で受け渡しする場合で異なり、また永続化する場合もきちんとクロスプラットフォームで読み書きできる形式とそうでないPOD (plain old data) の場合とがありうる。

CLAPの場合、state拡張には特徴的なものが何もなく、単純にバイナリを保存するだけになるが、LV2_State_Flagsに類似するstate-contextという拡張がdraft状態で存在している。ほぼstate拡張の代替として利用できるものであり、プラグインインスタンスの「複製」の際に使う場合や、プリセットの受け渡しのために使う場合は、それぞれ通常の場合とは異なるフラグが渡されることになるようだ。

分離プロセスにおけるStateのロードとパラメーター変更通知

Stateをロードすると、パラメーターの値も変更されることになる。このとき、パラメーターの変更内容がプラグインから(プラグインUIからの変更のようには)通知されないと仮定すると、状態バイナリはホストにとってはblobでしかないので、これを渡されてもホストはパラメーターの変更内容までは知ることができない。そうなると、ホストは明示的にパラメーターの値を取得する必要がある。そうしないと、ホストのUI上は、プリセット選択が名前としては反映されているのに古いパラメーターが残り続けることになるし、その後のオートメーション処理などにも支障をきたすことになる。

プラグインとホストが同一プロセスにある場合、「パラメーターを取得する」のは単にgetParameterValue()のような関数を一つ呼び出すだけで足りるが、別々のプロセスにある場合は、その内容が共有メモリ上にでも無い限り、明示的に「問い合わせ」を行う必要がある。別々のマシンにある場合は、共有メモリというシナリオもあり得ないので、パラメーター取得のリクエストをIPCなどの手段で発行しなければならない。デスクトップのプラグインと同じようにリアルタイムで数百件のIPC呼び出しを指示することはできない(AAPの場合は1回 = 1ブロックのオーディオ処理で1往復のIPC呼び出ししか想定していない)。何らかの最適な手段を見つけなければならない。

ホストからの問い合わせという手段を用いずに、Stateのロードによって生じたパラメーター変更をオーディオ処理の結果として(LV2 Atom output portやMIDI output channnelのようなかたちで)通知するというアプローチもあり得る。ただ、パラメーターが数百も存在するようなプラグインで、パラメーター変更をパラメーターごとに分解して通知するのは、いかにも非効率的だ。ホストの種類によっては、プラグイン上の現在のパラメーターの値が特に意味を持たない場合もあり得る(たとえば単なる音楽プレイヤーでパラメーターの現在値を何も表示しない場合や、オーディオファイルにレンダリングするだけのツールなど)。

解決策のひとつとしては、State拡張機能に「状態をロードしたときはホストとの共有メモリ上にパラメーター値のリストをダンプできる」という逃げ道を設けておく、というのが考えられる。「できる」と書いているけど、パラメーター数がごく少ないプラグイン以外では、実質的に「要件」となるだろう。これなら、ホストがプラグインからStateのロード完了通知(これ自体はいつ完了するかわからないロード処理の最後に必要)を受け取ったときに、パラメーター値のリクエストを大量に発行する必要はなくなる。バイナリサイズの文脈で言及したような共有メモリのチャンネルが存在していることが前提になる。

IPCを前提としたプラグインフォーマットにおいて、このような「要件」を設けるのは「問題」ではないが、全プラグイン開発者にこれを手動で実装してもらうのは筋が悪いので、AudioUnit SDKfullStateのようにデフォルト実装としてこれらの面倒を見る標準ライブラリが存在していたほうが良さそうだ。

このレイヤーの課題を解決することによって、ようやくjuce::HostedAudioProcessorParameterのような存在をobservableにできる(addListener()が実現可能になる)。

8月の開発記録 (2023)

8月は忙しかったわけではないのですが、やはり生活が変わると開発者生活に影響が出て、まあいいかとなって適当に遊んでいる時間が増えて、あんましアウトプットはありませんでした。今日もひと月暮らしていた部屋を出て夏休み旅行(とは)に出ながら書いています。そんなわけで軽くまとめていきます。

INSIDE [技術のヒミツ] @ TechBooster

TechBoosterのC102新刊で「新世代のリモートUIをSurfaceControlViewHostで構築する」というタイトルで寄稿しました。

booth.pm

内容はここで何度か言及してきたSurfaceControlViewHostについて、他のアプリケーションからUIを引っ張ってくる最先端の方法としてまとめてあります(Jetpack Glanceなんかも「最先端」っぽいふいんきを出していますが、アレは本稿でも言及しているAppWidgetなのでカウントしない)。システムとしては割と重要な機能になると思うのですが、情報源が海外ブログ1本しか無いので、日本語資料があるのは悪くないと思います。

上がってきた本を見たら半分くらいメンバーがやってきた活動のまとめみたいになっていて、わたしも無職エンジニア生活のはなしに差し替えるかと思いましたが思いとどまりました。

Resident MIDI Keyboardのリリース活動

先月末にopen testingまでやっていたResident MIDI Keyboardを、正式リリースとしました。その過程で、ようやくKVR Developer accountを作ったのですが、その時に「開発者公式サイト」とか「プロダクトのページ」とかいろいろ求められ、しかも「facebookページとかtwitterプロフィールとかはナシで」みたいになっていたので、しょうがないからこの際androidaudioplugin.orgのページをちゃんと作るか…となって、いろいろJAMStackなstatic site generatorsを調べて時間を溶かしました(まあまあ楽しい)。結局調べたことは何の役にも立てておらず(あんまりしっくり来なかった)、サイトは手書きHTML + bootstrapです。

RMKのKVRページもできました。ここにしかない情報のようなものはほぼありませんが、ここは一般ユーザー向け音楽ツールを出したら宣伝しておく場所としては定番ですね。

あと、英語ブログのほうをひさびさに更新して、RMKとComposeAudioControlsとついでに最近のAAP GUIの開発状況を、ざっくりまとめていました。

AAP: refresh plugin manager

AAPの現在の主要なマイルストーンは「プラグインの入出力を整備してちゃんとインタラクティブにする」ところにあって、具体的にはホストからのパラメーター等の変更がきちんとプラグインに届き、プラグインからのパラメーター変更通知などがホストにちゃんと渡ってくるようにしたいのですが、そのためにはまずホスト側がちゃんとインタラクティブに動くようになっている必要があります。

インタラクティブなホストとしてはaap-juce-simple-hostが現在はそれなりに機能しているのですが、AAPのデフォルトUIをGPLv3なJUCEにするわけにはいかず、また「ホストはKotlinでも作れる」という部分を維持しておくために、AAPのコードだけで一応ちゃんとしたホスティング(↑で書いたような要件を満たすやつ)ができるようにしておく必要があります。

これはデフォルトActivityとしてandroidaudioplugin-ui-composeというUIモジュールとandroidaudioplugin-samples-host-engineという非UIモジュールが実装していたのですが、前者も2年半前に作ったものでだいぶ古くなり、後者はそもそもUIがJetpack Composeになる前から存在していたもので、最新の実装でもMIDIメッセージは決め打ち、オーディオファイルも固定WAVを前提としたKotlinコードでロード、オーディオ処理は静的にprocess()を適用…みたいな最低限の動作確認用でした。任意のMIDI2メッセージを受け取れたりオーディオファイルを他のものに差し替えても動くようなコードにしたいところですが、優先度が低かったのと、静的な処理を前提としていることでいろいろ無理が生じていたので、後回しにしていました。

そういうわけで、今月は主にプラグインの新しいデフォルトActivityになるUIと、そのバックグラウンドで動くオーディオ処理の再実装に取り掛かっていました(実際には7月には構想を練ったり試験的なコードを書いたりしていました)。昨日ようやくちゃんとした音が出るようになったので、mainブランチにマージされています。

androidaudioplugin-ui-compose-app

新しいCompose UIは、基本的なmaster-detailスタイルのプラグインマネージャーで、対象はこれまで通りアプリ内プラグインだったりシステム上のプラグインだったりですが、detailのほうは常にプラグインインスタンスが生成されて、オーディオ処理が有効になるかどうかはstart/stop(プラグインAPI的にはactivate() / deactivate())で決まります。

old new
old UI new UI

Waveform Viewは「何となくオーディオ処理の結果が正しそうに見える」以上の使い道が無いのと、リアルタイム処理に合った表示にはならない(そしてリアルタイム表示するようにしても正しさがわからない)ので、シンプルに削除しました。代わりにMIDI2イベントを送信できるキーボードを載せています。そしてこのUIからaudio inも取得できるようにしました(自分で全然使わないので未確認ですが)。"Play Audio"ボタンはオーディオファイル(現状相変わらず決め打ち)を再生するために存在します。その前にエフェクトプラグインを適用して動作確認するためのものです。

パラメーターリストはgeneric native UIと同じものを使いまわしていますが、イベントの送信コードはgeneric native UIはローカルプラグインプロセス用で使い回せないので、別途実装しています。スライダーは消えました。もともとcompose-audio-controlsを試験的に採用した時点で消してもよかったのですが、一応未成熟なライブラリだったのでまあ妥当な移行策だったと思います。

MidiDeviceService機能のテスト用に存在していた"MIDI"ボタン(と"Stop MIDI"ボタン)はなくなりました。この機能は純粋にAndroid MIDI APIを経由して実装していたものだったので、今後はResident MIDI Keyboardがその代替となるでしょう。そのために作ったものなので。

UIは相変わらずWeb UIとNative UIを表示できるようになっていますが、ある意味ここが最大の違いで、今回からこれらのUI上での変更がプラグインに反映されるようになっています。従来のUIはプラグインを静的に適用するためにローカルのパラメーターリストを持っていただけで、オーディオ処理を適用するときに初めて適用されるもので、GUI拡張のインスタンスとは一切関係がない存在でした。新UIではインスタンスの操作がパラメーター適用と連動しています。

androidaudioplugin-manager

今回のプラグインマネージャーにおいて、GUIの刷新は表面的なもので、主役は非GUI部分です。これは(現時点では)androidaudioplugin-managerという、主にC++でオーディオとMIDI2のイベントルーティング実装を含むモジュールです。Kotlin APIとしてはorg.androidaudioplugin.manager.PluginPlayerというクラスが存在するのみです。C++側には同名のaap::PluginPlayerを含め、もっとたくさんのクラスがあるのですが、どこまでユーザー向けのAPIにするかは未定です。

従来のマネージャーUIは、Kotlinでいろいろ実装しすぎていて(たとえばwavファイルをロードしてKotlinのバイト配列にしたり、Kotlinでオーディオ処理用のバッファを生成したり)、aap-juceで使える・C++のみで実装した機能がうまく噛み合わなかった場面が多いものでした。新モジュールでは全機能をC++で実装しています。従来のUIでも段階的にKotlin実装のAudioPluginInstanceクラスから単なるC++ポインターのハンドルでしかないNativeRemotePluginInstanceを使うように変えてきましたが、新UIでは古いKotlin APIを全く使わずに実装しています。

この"manager"と名乗っているモジュールですが、表向きにはプラグインを(動的に)適用できるPluginPlayerの機能しか含まれていません。現状ではandroidaudioplugin-ui-compose-appの一部としても違和感が無いものですが、独立分離しているのは、ライブラリの実態がオーディオグラフであって、MidiDeviceServiceの実装などでも使い回す可能性があるためです。

このオーディオグラフ部分が、今回の新UIの主要な実装部分です。といっても、実際にはオーディオグラフがまともに(DAGを使って)実装されているわけではなく、「単一のプラグインを対象に従来のUIよりは柔軟にオーディオ処理を適用できる」「柔軟すぎるオーディオグラフは実装しない」というスタンスで作られています。本当は自分で実装もしたくなくて、LabSoundやminiaudioなどを使いまわしたかったのですが、これらを使ってMIDIサポートなどを追加するコードを書くほうがむしろ煩雑になる(あとLabSoundはどこがnot RT-safeなのかわからなかった)という理由で自作しました。

このオーディオグラフでできることはシンプルで

  • バイスからオーディオ入力を受け取る(そしてオーディオ処理に流す)
  • オーディオファイルを再生する(オーディオ処理に渡す)
  • MIDI入力を受け取る(そしてオーディオ処理に流す)
  • 単一のプラグインを適用する
  • MIDI出力をイベントハンドラーに流す
  • バイスにオーディオ出力を流す

というだけのものです。設計ドキュメントを見たほうがわかりやすいかも(ちょっと名前が実装と不整合な部分がありますが)。

audio graph pipeline

SimpleLinearAudioGraphという、1本のstd::vectorに格納できるレベルのグラフのみ存在しています。もう少しちゃんとノードを接続できるBasicAudioGraphというクラスをAPIだけ規定してあるのですが、そこまで必要なかったので実装していません。グラフの編集機能も実装していません。複数のプラグインにも適用できるでしょうが、今実装に求められている機能ではないので試していません。そういうものがほしい場合はJUCEのAudioProcessorPlayerとか、tracktion_engineに含まれるtracktion_graphなどを使うのがいいでしょう。

このネイティブ実装はTracktion/chocを試験的に使っています。主にオーディオバッファのinterleavingとか(従来のUIではこれをKotlinで実装していたり)、オーディオファイルのロードとresampling(従来のUIではresampling未対応)といった部分で使っています。オーディオ処理は正直ちょっと重い気もしますが(iteratorとか無理に使う必要はないはず)、単一のプラグインを処理するだけならまあいいかな…と考えています。

Oboeの部分やAAPのオーディオバッファの部分を抽象化したり、オーディオコールバックからの処理をプラットフォーム非依存にするように整備したりで、APIはちまちまと調整してあります。モデルさえ合えば他のプラグインフォーマットでも使い回せる可能性が微レ存…(最初はデスクトップで作ろうとしていました)