7月の開発記録 (2021)

恒例の自分用メモです。今月は大して進捗がないのですが、文章だけ見ると多いな…

MML-to-MIDI2.0 compiler on vscodium-extension

3月にMMLコンパイラmugeneをKotlinに移植したmugene-ngには、まだ.NETから移植が済んでいないプロジェクトがいくつかありました。そのうちの一番重要なやつがvscodium拡張(vscode拡張)です。これが無いとvscodiumでMMLを書いてその場でコンパイルできない…!

というわけでこれをやっつけました。それなりに手間がかかりました。

vscodium拡張のコーディングのためには、Kotlin/JSでMMLコンパイラのJSライブラリを何らかの再利用可能な形式にまとめて、それをライブラリとしてvscodium拡張のpackage.jsonで解決してコード上でモジュールとしてimportできるようにする必要があります。Kotlin/JSのプロジェクトではnpmパッケージを生成できるnpm-publishというGradleプラグインが使えるので、これを使います。

Kotlin/JSにはnodeとbrowserの2種類のプラットフォームがあり、その選択次第で採れる技術的手段が変わってきたりします。そして多分そのサポート状況はKotlin 1.5の現在でも刻々と変わっていて、Kotlin 1.5系列でも1〜2ヶ月前に試してできなかったことが今ではできるようになったりとかしています。そこまで流動的な課題でなくとも、たとえばファイルシステムからMMLソースファイルをロードするのは明らかにnodeでしかできないのでnode専用にしたり、でも実行可能なJSを作る方法がなぜかwebpack前提のドキュメントしか見当たらなかったりとか(多分慣れれば探し方の幅が広がるやつ)、いろいろよくわからない壁にぶち当たります。

2021年7月現在、Kotlin/JSにはLegacyIRという2種類のコード生成エンジンがあります。IRは新しいKotlinコンパイラの共通IRバックエンドに基づくJSコード生成エンジンで新しいのですが、まだ安定版ではありません。実際にはLegacyとIRの違いはこれだけでは済まない部分があります。IRはTypeScriptのd.tsを生成できます。これはいいこともあれば、生成コードの中で型解決エラーが発生する原因にもなります。strongly-typedでなければエラーにならず実行時にもエラーにならないことがあり得るわけです。

今回mugene-ngのJSコードは、Legacyで生成しています。IRでコード生成したらビルドが通らないような問題が実際に発生したのでそうなったわけですが(だいぶ前のことすぎて覚えていない)、現時点でJsIrは回避策を深く追及したところで次のバージョンで直ったりしそうなので、自分がそこまで深入りする価値のあるトピックには思えません。Kotlinに人生かけてる方面に任せたい。

Kotlin/JSをやっていて面白いのは、TypeScriptの存在感が全然無いことですね。Kotlinでコードを書いているのだから当然とも言えるのですが、ReactがそうであったようにKotlin/JSでも別にいらない存在と思われていそうです。実のところオーディオ界隈でJSが使われているときもTypeScriptは全く出て来ないことが多く(というか見たこと無いかも)、TypeScriptが当たり前のように使われている世界に住んでいた頃はいかに自分のいる世界しか見えていなかったのか、という気持ちにさせられます。

余談はさておき、コレを作っていた頃にvscode拡張にKotlin/JSを使った例を探して参考にしようとしたのですが、全然見つからなかったので、おそらくコレが初めて作られたやつなんじゃないかと思います。mugene-ng自体はnpmパッケージとして公開したので、理屈の上では誰でも使えると思いますが、まあわたしがvscodium拡張に使うくらいでしょう。

mugene-ngなのでMIDI 2.0 UMPを使った楽曲データにもコンパイルできます。(.umpxというファイルはSMFではないのでktmidiのMidi2Playerを使うしか再生手段がありませんが。)

mugene-ngなのでMIDI 2.0楽曲データにもコンパイルできる様子

fluidsynthのイヤホン抜け対応(やっつけ)

fluidsynthのAndroid対応の暗黙的なメンテナーっぽい立ち位置になっていて、Androidユーザーの質問が自分に振られるようになっているのですが、いわゆるイヤホン抜けになったときにオーディオ生成が止まるのを何とかしてほしいという話が出てきたので、先月のAndroid用テスト対応に続いてオートリカバリーを実装していました(masterに反映済み)。

ただこれは(わたしのPRコメントを見ると分かるのですが)あくまでやっつけなんですよね。普通の音楽アプリなら音楽再生そのものが止まるわけです。どうやって再生を止めるかを決めるのはあくまでアプリであって、FluidsynthでいえばMIDIシーケンサーがコールバックを受けて対応すべきところです。そのコールバックの仕組みがFluidsynthには用意されていない(ついでにいえばwin32やwasapiなどプラットフォームAPIのレベルで用意されていない)ので、現状どうしようもないよね、みたいな話になって、結局自動エラーリカバリーにするか停止にするかのオプションだけが実装されました。やっつけ対応…!

プラットフォーム別オーディオデバイス変更のコールバックについては、zennのスクラップにまとめたやつがあります。

https://zenn.dev/atsushieno/scraps/3f6a694db67e37

スクラップの中でも書いていますが、クロスプラットフォームのオーディオアクセスAPIではどうなっているんだろうと思って一番モダンっぽいminiaudio(デスクトップとiOS/Androidまで対応しているつよいやつ)の中を見てみましたが、デバイス変更コールバックのAPIを実装しているのはCoreAudioとbela(組み込みLinux用オーディオデバイス)だけでした。

fluidsynth-midi-device-service-jの復活…から危機へ

FluidsynthのAndroidビルドまわりを見ていたのは(直したのは自分ではない)、オーディオプラグインとして使うためにビルドした結果が不安定すぎたせいなのですが、Androidビルドとして安定的に使えるかどうかはMidiDeviceServiceとしてビルドしていたfluidsynth-midi-service-jという古いプロジェクトでも確認できたわけです。たまに他の開発者から問い合わせが来ることもあって、最新のfluidsynthに合わせてビルドをoverhaulする必要があると考えました。

ただ、このプロジェクトが死んでいたのはFluidsynthのビルドが死んでいたからというだけではなく、FluidsynthのCヘッダからJNAバインディングを自動生成するJNAeratorというツールがもう5年近くメンテされていなくて(そもそもGoogle codeからGitHubに移転していたのが貴重だったレベル)、古いJDKやら古いMavenやらで問題を起こしていたということがありました。何人かの開発者がforkして独自に対応していましたが、それでも自分の手元ではビルドが通せない状態でした。

そういうわけでしばらく諦めていたのですが、先月になってJNAeratorをgradleから呼び出せるgradle-jnaerator-pluginというものを発見して、これならビルドできるようになるかもしれないと思って試してみたら、生成されるコードがずいぶん変わったものの、jarは何とかビルドできる状態になりました。

ただ、結果的には、生成されるコードから要求されるjnaerator-runtimeが参照として要求するjna-runtimeがデスクトップ用ビルドのJarを要求して、こちらが使いたいjna-runtimeのAndroid用のAARとAPIが競合してビルドできないという問題が生じたため、回避策を求めていろいろ探し回った結果、opendesignflow/gradle-jnaerator-pluginというforkを使えばうまくいくということが分かって、そちらを使うことにしました。この辺の事情はdesign documentとしてリポジトリに含めてあります。

https://github.com/atsushieno/fluidsynth-midi-service-j/blob/main/docs/design/fluidsynthjna.md

後になって考えてみると、おそらくこのgradleプラグイン本家のオプションはJNAeratorのjnaerator-runtimeを必要とするstrongly-typedなコード生成モードをデフォルトとしていて、それで先の競合が生じたのでしょう。デフォルトで生成されるAPIではjnaerator-runtimeを必要とせず(確かNativeSizeクラスだけ必要になって適当に自作したはず)、実際に支障なく動作しています。

これでしばらく開発も安泰だとしばらく思っていたのですが、先週になって致命的なトラブルが…何とこのopendesignflow/gradle-jnaerator-pluginの関連ドメインが証明書を更新しなくなっていて、Gradle Pluginのパッケージ解決が不可能になってしまいました。パッケージがMaven Centralに発行されていれば問題なかったのですが、所詮は独自forkなのでこういう問題が発生するのは不可避…

というわけで、いたしかたなく自分でさらにforkして、全く経験のないgradle.orgアカウント作成からGradle Plugin登録までやっているのですが、Gradle Pluginがpending approvalのまま全く先に進まないというまさかの問題が発生して先に進めない…みたいな状態になっています。Gradle Pluginを使うこと自体が割と時間リスクになるんだなということがわかりました。耐えきれなくなったらGradleは捨ててMakefile等に戻ろうと思います。

fluidsynth-midi-device-service-jのMIDI 2.0対応

4月のM3 2021春で公開したMIDI 2.0エコシステム構築術という同人誌でも少し書いたのですが、FluidsynthにはMIDI 2.0対応のissueが登録されており、それに向けてAPIが整備されていて、MIDI 2.0サポートに着手できる状態でした。そういうわけで、いったんfluisynth-midi-service-jが最新のktmidiと合わせてビルドできるようになった現在、MIDI 2.0対応版MidiDeviceServiceを作るのも現実的になったと言えるでしょう。

…と思ったのですが、実際にUMPの数値をもとにfluidsynthのfluid_synth_noteon()などを(JNA経由で)呼び出してみると、まだ実装そのものは7ビットのMIDIメッセージのデータが前提となっていて、16ビットのvelocityを送るとクラッシュする状態でした(今でもそう)。これはfluidsynthそのものを改造するしかないな…!と思って、少し手元で変更を加えたforkを作っています。velocityの内部的な変更だけ実装してみましたが、とりあえずはクラッシュせずに発音できているようです。音量加工部分をほとんどいじっていないのに問題なく音が出ていてある意味こわいのですが、たぶんvelocityの内部処理が未だに128段階なのでしょう…

とりあえずは、UMPとMIDI 2.0 Channel Messagesが正常に処理できれば十分なので(内部的に処理を精細にしてもSF2サウンドフォントの仕様は7bit前提なわけですし)、そのうち「MIDI 2.0に対応できた」などと適当に英語のほうに書いて公開しようと思います。

本家メンテナーは「MIDI2対応だけのAPIを追加したくない」でわたしは「MIDI2対応のAPI追加は必要になる」で、今のところスタンスが合っていないので(そりゃそうなると思う)、とりあえずある程度アプリが動作するようになるまではforkで進めることになりそうです。ただ現状いずれにしろビルドがgradle pluginでブロックされているのでしばらくは氷漬けかな…

kmmkのMIDI-CI対応など

fluidsynth-midi-service-jのMIDI 2.0対応と対になるのが、Androidとデスクトップで動作する仮想MIDIキーボードkmmkMIDI 2.0対応です…といっても、MIDI 2.0 UMPを送信するモードは既に4月の時点で実装済みでした。この当時はsender(仮想MIDIキーボード)とreceiver(仮想MIDIバイス)の間で、外部的にプロトコルの合意があれば、いきなりUMPを送りつけても問題ない、という前提で設計していました。

この時点では主な接続先アプリが自作オーディオプラグインから半自動生成されるMIDIバイスしかなく、これらはMIDI2のみが前提でした。そして4月以降もMIDIメッセージがきちんと処理できる状態のコードにはほとんどならず、MIDI2のみが前提となっているうちは他のMIDIキーボードアプリも問題の切り分けに使えませんでした。これは何とかしないといけない。

AndroidのMidiDeviceServiceはデフォルトでMIDI 1.0に対応するものであり、アプリケーションとMIDI 2.0を使う合意をやりとりするには、Android MIDI APIの規定されていないプロパティを独自に定義するか、MIDI-CIの定めるプロトコルに則ってMIDI 2.0に昇格するくらいしかありません。独自にプロパティを規定しても良いのですが、ある程度MIDI-CIに則ってやってしまったほうが、後々対応が楽になるでしょう。というわけで、先月実装したMIDI-CI対応を適用することにしました。

もっとも、現時点ではMIDI-CIが想定する双方向のメッセージングは実装していません。MIDI-CIのメッセージは全てUniversal SysExですが、プロトコルの設定はSet New Protocolというメッセージで規定されていて、これには特に対応するレスポンスメッセージがありません。その代わりにTest New Protocolというプロトコル変更を確認するメッセージがあります。このテストは必須というわけではないので、Set New Protocolだけ勝手に送りつけて、後はプロトコル変更が有効になったとみなしてUMPを送りつけても、正常に動作しているうちは問題なく動作するはずです…たぶんどんなMIDI 2.0楽器でも(!?)

あと、kmmkは本来C#で開発していたxmmkの置き換えになる予定なのですが、デスクトップではキー入力に一切対応していなかった上にタッチ(マウス)でも1秒音を鳴らすだけしかやっていなかったので、ちゃんとPointerDown/UpとKeyDown/Upに対応しました。pointerInput()からこれをきちんと処理する方法はドキュメントから拾えなかったのでコードにリンクしておきます

UI実装は基本的にお粗末だったのですが、これは別に自分がGUIに興味がないからだけではなくて、Compose for Desktopが本気で動き出すのがJetpack Compose 1.0リリース以降っぽかったからです(この辺のissueからも察してほしい…)。Jetpack Compose 1.0は今週リリースされたばかりですが、Multiplatformでパッケージを整備するCompose for Desktopのほうは、ビルドできる環境が整備されるのがもう少し先になるでしょうし、Compose for Desktopのfeature parityが実現するのはもっと先になるでしょう。

ktmidi rtmidi backend (in progress / stuck)

kmmkは自分では既にDAWと組み合わせて活用できるものになっているのですが、managed-midiとは異なりまだプラットフォーム別バックエンド実装ができておらず、AndroidALSAしかサポートできていません。MacWindowsでも使えるようにしたいのですが、Compose for DesktopなのでJVMアプリで使えることが重要で、CoreMIDI APIJVM上で呼び出せるようなライブラリは存在しません。さすがにRoboVM上で動かすみたいなのはしんどい上にComposeのビルドシステムと合わないんじゃね?と思わなくもないので(試してもいない)、ここはもう少し制御可能なやり方で済ませたいところです。

というわけで、managed-midiで当初やっていたように、まずrtmidiのJNAバインディングだな、と考えました。rtmidiを継承しているlibremidiはC++に全振りしているので、ここはC APIがある(というか自分で書いて公式に取り込んでもらった)rtmidiです。以前JavaCPPでも試したのですがその時うまくいかなかったのでJNAです。ただ、↑で書いた通りGradle Pluginがまともに使えない状態なので、これも足踏み状態です。

ComposeがKotlin/Nativeでも使えればObjective-C interopが使えるのでMacでは最適解なのですが、さすがにまだCompose for Nativeはまだまだ先になりそうなので、JVMでやっていくのが良さそうかなと思っています。

6月の開発記録 (2021)

2021年も半年が過ぎてしまいました。去年の今頃は「来年には正常化しているだろう…いるんじゃないかな…たぶん…」くらいに思っていたものですが、まあ段階的に正常化しつつありますよね。日本以外はな。台湾なんかもう50人/日くらいに収まっているので、渡航できるようになれば概ね安心して行けることでしょう(まあ渡航を受け付けていないわけですが)。

海外のカンファレンスもぼちぼちハイブリッド開催に移行しつつあるようです。ADC21とか。もうちょっとちゃんと動くものがあればCfP出したいところですが…まだその時ではなさそう… 最近は無職の副業…といえるのかわからんですが*1…でロンドンの音楽アプリ開発のヘルプに入ったりしているので、リアルで参加しておこうかな〜くらいに思っています。まあその時までやってるか分からんですが…*2

まあそんなわけで、6月も5月に引き続きクローズドソース案件があって更新は少なめです。

Fluidsynth tests on Android

OSSではFluidsynthのAndroidビルドで動作するテストを追加したりしていました。これが地味に面倒で、まずテストそのものがctestで書かれていて(たぶんCMakeでサポートされているからなんじゃないかと思います)、これがテストごとにexecutableをビルドして実行するスタイルなので、Android NDKを使ったビルド・実行のあり方と完全に相性が悪い。仕方ないのでexecutableになるテストコードをスクリプトで書き換えてshared libraryをビルドできるようにして、それをABI別にビルドして実行できるようにしました。

Fluidsynthはなぜか(?)Azure DevOpsをCIで使っているのですが、ADではAndroidの4ABI分のビルドを並列に走らせられないレベルのスペックしかなく、ビルドスクリプトがABI別に動作するようになっているので、ビルドスクリプトの構成がやや特殊です(完全に無駄なエンジニアリングをさせられていそう…)。AD側は管理権限が無いのでメンテナーに丸投げして、自分はローカルスクリプトでビルドできるところまで面倒を見る…というつもりでいたのですが、CIもいじれそうな開発チームに加えられてしまったので、またそっちに時間リソースが消えるかもしれません。GitHub Actionsを常用している身としては、Azure DevOpsをいじりたい気持ちはゼロというかマイナスなのですが、メンテナ氏にはAndroidビルドを助けてもらった恩もあるしな…

tracking Android Studio

6月はAndroid Studio Canary (Bumblebee) もbeta (Arctic Fox) もちょいちょい更新されていたのですが、ついにBumblebeeのネイティブデバッガーにMemory Viewが搭載されたので、現実的なデバッグ作業が可能になりつつあります。CLionユーザーなら分かると思うのですが、IDEAではポインタの表示が配列みたいにはならないんですよね。なので内容を見ようと思ったらMemory Viewを見るしか無いわけです。構造化表示してくれるわけではないので、不親切なことに変わりはないのですが、無いよりはずっとマシなのです。

Memory View ただ、Bumblebee alpha02のプロダクションはかなりイマイチで、clangのパス設定が間違っていてコード補完などのC++編集機能がまともに動作しないとか、デバッガーが出鱈目な位置でブレークする(ので1行stepoverするごとに出鱈目な行にジャンプする…)など、使い物にならない状態です。仕方ないので、プロジェクトによって再現したりしなかったりするところから、問題ををそれぞれ調べて言語化してissuetrackerに登録する作業などをやっていたりしました。native toolchainまわりは特にユーザーが少ないのでわれわれ(ホントに複数形か…?)が藪を切り開くしか無いのじゃ…

BumblebeeがダメならArctic Foxでやればいいんじゃないか、と思うわけですが、Arctic FoxにするとC++ソースがAndroidプロジェクトビューに全然表示されない問題などにぶち当たったりして、そっちはそっちでうまく行かなかったりします。最近気づいた問題としては、.ideaディレクトリの内容がAFとBBで互換性が無さそう(なので切り替えるたびに全消しするしか無い)、みたいなものもありました。

まだ条件が判明しない類のバグがいくつかあって、たとえばデプロイメントに関して、インストールしたアプリのStorage & Dataをクリアすればコードがまともに動作するようになる、みたいな挙動を見るのですが、再現条件がわからずレポートにならないところです。Apply ChangesがおかしいのかAndroid Gradle Pluginがおかしいのか…

cmidi2とktmidiのUMP Byte Orderの調整

4月からAAPの開発の主なフォーカスがMidiDeviceServiceの応用にずっと集まっているところですが、MIDI 1.0アプリとMIDI 2.0アプリの相互運用がまだまだきちんと実現できておらず、その原因はさまざまな箇所に問題があった(ある)ためのようでした。そのひとつとして、まずcmidi2とktmidiの間でバイトオーダーの調整がきちんとなされていなかったことがあります。バイトオーダーが問題にならなかったMIDI 1.0とは異なり、MIDI 2.0では32ビット整数がデータの中心になっていることもあり、バイトオーダーがきちんと交通整理されている必要があります。

そういうわけで、今月はこの問題に対処する作業がいくつかのリポジトリ上で行われました。具体的な問題の内容については別途zennでまとめたものがあります。

zenn.dev

MIDI-CI generators in cmidi2/ktmidi

当初、AAPの内部プロトコルMIDI 2.0オンリーにするつもりでした。これはMIDI 1.0のメッセージをMIDI 2.0化するのは簡単で、それをMIDI 1.0に戻すのも現実的に可能だから、という目算があってのことだったのですが、aap-lv2やaap-juceも含めて少しusage scenariosを整備した結果、やはりMIDI 1.0アプリから接続したときはMIDI 1.0のままで、MIDI 2.0アプリから接続したときはMIDI 2.0で、といった切り替えをAndroid MIDI APIの枠内で実現するためには、外部から情報を与えるのには限界があるため、MIDI-CIで規定されているProtocol Negotiationを使ってMIDI 2.0モードへの切り替えを指示できるようにしよう、と考えるようになりました。

ひとつには、MIDI 1.0をサポートするAPIに自分がUMPを流し込もうとしていて、そのためにI/Oポートのデータ転送処理側で余計なバイトストリーム加工処理を走らせないでほしいと期待しているにもかかわらず、自分がむしろそういう期待を積極的に裏切りに行くという構図になっていて、少々忸怩たるものがある、という話があります。(まあそうはいってもタイムスタンプを加工する処理が必要になる以上、そのままのデータを送受信できる日は来ないわけですが。)

MIDI-CIは基本的にユニバーサルシステムエクスクルーシブなので、MIDI 1.0をサポートする出力ポート(Android MIDI APIではMidiReceiver)があれば、そこにsysexを送りつければ良いことになります。MIDI-CIメッセージの多くはinquiry/replyの双方向を前提としたものなので、MIDI 1.0の1ポート接続のみを前提としたら無理そうにも見えますが、Set New Protocolは一方的にプロトコルを設定するものとして送りつけることができ、その確認用には別途Test New Protocolメッセージが規定されています。MIDI 2.0に切り替えるSet New Protocolだけ一方的に送りつけて、後はUMPを送りつけるような仕組みにすれば、接続オプションを無理やりハックすることなくMIDI 2.0と1.0を切り替えることが可能そうです。

これを実現するため、cmidi2とktmidiにMIDI-CIのuniversal sysex generatorsを追加しました。ktmidiのほうはcmidi2に追加したAPIをそのまま持ってきただけなので、まだAPIの変更あるいは上モノの追加が想定されるところです。cmidi2のAPI追加がそれなりの作業量だったので、Kotlinコードは自動変換で対応しました。これもzennのスクラップで別途まとめてあります。

zenn.dev

ちなみに、これをAAPのMidiDeviceServiceに実際に組み込んで応用する部分は進んでいません。というのは、前述したAndroid Studioのデバッガがまともに使えない問題のせいでまともに先に進めないためです…そういうわけでこっちはBumblebeeの修正待ちです。

AAP Web UIの実装

MidiDeviceServiceの実装作業が先に進められないので、その間にできることを先に進めておこうと思い、最近は何度か段階的に着手していたGUI統合の作業を進めています。何度か書いていますが、プラグインとホストはアプリケーション プロセスが別々になるので、ホスト側にはプラグイン側が提供する…かもしれない…Web UIをインプロセスのWebViewでロードさせて、そこからアウトプロセスのプラグインを制御する、あるいは変更通知を受け取る、というやり方が必要になってきます。

Web UIをプラグイン側が実装する期待値は高くないので、デフォルトでポート全部を対象とするknobを出すUIを出そうと思っています。作りかけですが、MidiDeviceServiceのデモに上乗せしたらこんな感じです。

AAP Web UI

いや…まあさすがにknobの画像は差し替えるかもしれんな…? (あとキーボードもInstrument以外では不要だし) まあ機能的にはこのツマミをいじったらプラグイン側にパラメータ(ポート)変更命令が飛び、キーボードを押したらノート命令が飛ぶ、みたいな感じです。まだその受け口を実装というか調整していないので、この辺は来月の調整事項でしょうか。

*1:双方が雑で契約関係もろくに決めていなかったりとか…

*2:前回は夏の入りくらいに仕事を始めてM3直前に辞だった気がする

5月の開発記録 (2021)

恒例の自分用開発記録です。

と書き出しましたが、5月はほとんど何もやってない感じです。ひとつには、4月にM3向けにいろいろやっていた間にいろいろ先送りしていたのと、自宅近辺の騒音工事がうるさい間は寝不足になりながらコーディングを放棄してゲームで遊んでいたりとか*1Google I/Oの動画を眺めていたら終わった週があったりとか。

コーディング作業としては、なぜかフリーランス仕事してくれみたいなのが集中してやって来て、仕事ではやりたくないなぁ…とか返しつつその調査を手伝ったりとか、何だかんだで手伝うことになった感じの案件があったりとかで、自分のプロジェクトの開発はあんまり進展していないです。ここ数日もどちらかといえばFluidsynthのAndroidビルドでトラブっているのを直す手伝いで時間を使っている感じです(現在進行形)。

ktmidi and AAP on maven central

AndroidのオーディオプラグインのプロジェクトのMIDIまわりのコードをもう少しまともにしたいので、ktmidiを使いたいと思っていた(いる)のですが、パッケージ参照をsubmoduleのmavenLocal経由で解決し続けるのがしんどくなってきたので、いろいろな手抜き解決をあきらめてSonatype経由でMaven Centralに出すことにしました。AAPのほうは今日出したばかりなのでまだインデックスされていません。

mvnrepository.com

mvnrepository.com

この辺は参考資料がいくつかあるので、わたしがまとめることもないでしょう。ktmidiはKotlin MPPだったのであまり資料が無く、主にJetBrainsの中の人が書いたらしいコレを参考にしています。(ちょいちょい改良を加えてフィードバックしていたり。)

dev.to

ちなみにMaven Centralはめんどくさい…って思って手抜き策としていろいろ手を打ってみたのですが…

  • Jitpack : 一時期ビルドできていたのですが、モジュールが複雑化してきてビルドできなくなったのと、alsaktでALSAまわりのヘッダファイルが必要になってビルドできなくなって、こっちで続けるのは無理があると思ってやめました
  • GitHub Packages : 使う側が認証を求められるのはやっぱり体験が悪いのでやめました
  • GCR Maven Package Registry : しばらく前にGCPでアルファユーザー登録していて、放置されっぱなしだったのが、5月に使えるようになったのでちょっと調べてみて、やっぱり使う側が認証を求められたりするのか調べるのが面倒だったのでやめました

そんなわけで現状Jitpackが使えないようならまあMaven Centralが一番楽だろうとは思っています(楽な度合いで言えばJitpackが圧倒的に楽です)。まあ今でもあんましやりたくないのですが、一応GitHubでsonatypeに送りつけるまでは自動化できるようになったので、とりあえずは使い続けると思います。

ちなみにGitHub Actionsのubuntu 20.04で自動ビルドするようになった結果、iOSビルドを維持できなくなって、iOSはbuild.gradle.ktsで無効化してあります。これは↓あたりを読むと実現できそうですが…

medium.com

…Composeで作った仮想MIDIキーボードアプリをKotlin 1.5でビルドできるようになってからでいいや、っていう感じで先延ばししています(何

今月はホントに外の調べものばかりで、特にFluidsynthのビルドまわりでさまざまな試行錯誤をしているだけで時間を取られているのがよくわかったので(自力でprefab作ろうとしたりcerberoビルドがうまく行かない調べものをしたり…)、あんまり泥沼化しないうちにどこかで手打ちにしようと思います。アレを最初にビルドできるところまで持っていった当時の自分はホントえらい…

*1:サガ某とかニーア某とか…モバイルはアプリの安定性がアレでダメな感じですがPS4のほうはとてもよかった

4月の開発記録 (2021)

4/25にはM3 2021春がありました。サークルとして申し込んでいたのですが、その時はある程度MML + tracktion_engine + オーディオプラグインの制作環境で音楽を演奏できるだろうくらいの軽い気持ちで、申込内容は「ソフトのデモと展示」という趣旨になっていました。しかし、開発体験の悪いC#で書かれた旧MMLコンパイラで打ち込み作業を進めたい気持ちにまったくなれなかったので*1、先月書いた通り、意を決してKotlinに移行したわけです。MMLコンパイラの移行はできたものの、ntracktiveのtracktionデータ生成部分までは手が付けられず、残った時間で音楽制作はちょっと無理そうでした(ブランクもできてしまったし、オーディオプラグインプレイヤーまで進められなかった)。

制作は無理そうだったので、諦めてソフトウェアの展示のみで打開を図りました。ソフトウェアの展示であれば何かしら目を引くものであったほうがいい。何かいいネタはないものか…やりやすいのは今作っているもの…すなわち…

ktmidi supports MIDI 2.0

…そういうわけで、ktmidiエコシステムをMIDI 2.0に対応させました。ktmidi単体ではなく、先月書いた基盤ライブラリktmidiMMLコンパイラmugene-ng、仮想MIDIキーボードkmmkはどれもMIDI 2.0対応となっています(MMLAPI化ライブラリnotium-ngだけ未着手です。これは自分のプロジェクトで実用していないので…)。Kotlinを前提にしたMIDIライブラリがそもそも皆無と言って良いのですが(たまに実験的に触ってみたという感じのプロジェクトが存在する程度)、MIDI 2.0関係でここまで作り込んでいるOSSは他のどの言語でも無いと思います。(たとえば、JUCEにあるMIDI 2.0サポートはフレームワークだけでアプリは無い感じです。)

MIDI 2.0サポートのAPIは、概ねcmidi2のUMP解析・生成APIのやり方を採用しています(ネイティブライブラリとして参照したりはしません)。今月気づいたのですが、cmidi2は割と先行実装としてRustのmidirとかjack2とかで参照されていて、そのうち似たようなAPIがこの辺からも出てくるかもしれません。それぞれのエコシステムに合ったものが出てほしいと思います。

MMLコンパイラmugene-ngがMIDI 2.0対応しているというのは少し補足説明が必要でしょう。この話は何度か書いているのですが、MIDI 1.0のSMFに相当する仕様がMIDI 2.0ではまだ存在しないので、独自フォーマットをでっち上げる必要があるのです。MIDI 2.0に対応しているというDAWとしてMultirackStudioというものがあるのですが(Win/Mac onlyなのでわたしは試していません)、mugene-ngでも同様にSMFとほぼ同様の内容を含むデータモデルを規定しています。

とはいえ、MIDI 1.0からの反省をふまえて規定されたMIDI 2.0の長所を殺したくはない、具体的には、MIDIイベントのイベント間の待ち時間(デルタタイム)をMIDI 1.0のように可変長にしてデータ長を不統一にしたくはないわけです。そういうわけで、UMPにおけるJR Timestampの値をMIDI 1.0相当の手法で計算した値で書き換えています。この辺りで実装の書き分けが生じているので、案外シンプルではなかったです。

そもそもUMPの場合はタイムスタンプイベントが1/31250秒固定なんだからそれで計算すればいいじゃないか、と(分かる人には)思われそうですし、自分もそう思っていたことがあるのですが、実際に生成されるデータを目視したときに圧倒的に分かりやすいのも加工しやすいのも従来型の「分解能(4分音符1つあたりのticks)に基づく長さ指定」で表されるデルタタイムです。この方向性はMMAで策定中の仕様でも採用するだろうと踏んでいます。

もうひとつ重要なのはメタイベントの扱いが決まっていないことなのですが、今回はとりあえずmanufacturer IDなどの無いsysex8で保存することにしました。

楽曲データだけ作っても再生するAPIが無いと意味がないので、ktmidiのMidiPlayerに相当する機能をMidi2Playerとして実装しました。kmmkにMMLから演奏する機能がほしいという話を先月の時点で書いていましたが、実際に実装したので、これもMIDI 2.0に対応させています。ただし、UMPを受け取って処理できるMIDIバイスが無い限り、具体的に確認出来る状態ではありません。

正確には、UMPを受け取って処理できるMIDIバイスが無くても、UMPを受け取って、それをMIDI 1.0のメッセージにダウンコンバートする処理系があれば、MIDI 1.0デバイスの上で限定的に再生することは可能です。そういうのを作っても良かったのですが、もうひとつ前々から時々構想していたプロジェクトがあったので、この機会に着手することにしました。ちなみにこの時点でM3まで残り1週間…果たして間に合うのか…(間に合いませんでした)

aap-midi-device-service

android-audio-plugin-frameworkのほうの大きな課題のひとつに「とりあえずまともにMIDI再生できるホストがない」というのがありました。android-audio-plugin-framework自体に含まれているサンプルは固定メッセージしかも静的データ生成であり、もうひとつのホスト実装はJUCE AudioPluginHostであり、動作はかなり不安定で頻繁にクラッシュするけどJUCEのせいなのか自分のせいなのかも十分に調べられない状態でした(です)。

当初はtracktion_engineを使ったオーディオプレイヤーを移植するつもりだったのですが(実際に移植は出来ているのですが)、これも(たびたび書いている気がしますが)プラグインのstateの互換性の問題があって、プラグイン処理に入る以前のところに壁があります。

そういうわけで、AAPをAndroidのMidiDeviceService経由で使えるInstrumentプラグインとして作ってみたら良いんじゃないか、と考えていました。これが新しいプロジェクトaap-midi-device-serviceです。実のところandroid-audio-plugin-frameworkにも取り込み可能なように依存関係を絞り込んであります。まだ(?)取り込んでいないのは、単に全くまともに動作していないためです。(その意味ではまともなホストがほしいという要件は全く満たしていないところです。)

これにはもうひとつアイディアが乗っかっていて、AAPのポートのデータ形式はLV2同様、好き勝手に定義できるので、MIDI 2.0 UMPを乗せるチャンネルを作れる…MIDI 2.0 UMPを受信しても処理できる(!)というわけです。まあそもそも自分が仕様自体をまだ好き勝手に書き換えているレベルなので、どうとでもなるというのが正直なところです。実のところ、MIDI 2.0データをやり取りするポートというのは、AAPの中で規定する予定でした。

ただし実装していたのはLV2プラグインをAAPでロードするaap-lv2のみです。実際にメッセージを受け取るのはプラグインであり、AAP本体で実装するものは何もなかったのです(何かしらの共通化コードが有用になるかもしれませんが、現状ではそこまでの段階ではないです)。aap-lv2には、UMPをMIDI 1.0メッセージにダウンコンバートした上でLV2 Atomに変換する処理が実装されていましたが、どのプラグインでも使っていなかった機能です。何しろクライアントが無かったので。

aap-midi-device-serviceがまともに動作していないのは、どちらかというとネイティブホスティングAPI・基盤コードの整備が中途半端だったことが原因で、現状でも再整備が必要だなと思っています。当時はKotlinのホスティングAPIも存在せず、それどころかJUCEプラグインの取り込みも始めていなかったので(AudioPluginHostだけ存在していた状態)、無分別に実装を拡張していたわけです。どちらかというと今回もそれに近い突貫工事だったので、むしろこれから再整備しないといけないところです。あとAndroid emulatorだとKotlin+JNIでAndroid Studioのデバッガーが落ちるバグが生産性を大きく下(ゲフンゲフン

一応、単音を送る実証コードは動作したので、少なくともメッセージ経路にMIDI 2.0を阻むルートはないということは確認できました。

このプロジェクトについてはまだまだ説明不足なところが多いのですが、実装が安定してきたらREADMEなどで情報を注ぎ足していこうと思っています。

その他の作業

Kotlin MPPプロジェクト構成の再整備

3月にKotlin MPPとJetpack Composeとの間の不整合からAndroidまで含めたMPPが整理できないという話を書いたのですが、内部的にはKotlinのmultiplatformプラグインに基づいているはずだから出来るはずだというコメントをもらったので、IntelliJのIDEA (IDEAとAndroid Studio) で平仄が合っていない部分をいくつかの既存プロジェクトや新規プロジェクトテンプレートをもとに、何とかビルドしてmaven-publishプラグインpublishToLocalMavenタスクを正常に動作させるところまでできています。

ハマりどころで覚えていることを書くと

  • IDEAのmultiplatformプロジェクトにandroidMainを追加しても全く認識されない: Androidビルドを「有効」にするためにはbuild.gradle(.kts)プラグインを有効にするだけでなく、android { ... }のセクションを含める必要があり、それが無いと特に警告も何も無く単に無視されるようです。
  • maven-publishと組み合わせる情報があてにならない: Kotlin MPPでmaven-publishを使う方法はさまざまなページで紹介されているのですが(日本語だとこの辺とか)、それらに従ってartifactIdを条件分岐で処理するようにしていると、現在のKotlin Gradle Pluginではxxx-kotlinMultiplatformというartifactIdが生成されるし、Android用のartifactはDebugとかReleaseとか命名されてしまって、これが禁止された文字(小文字と-などのシンボル以外)を含むためmavenLocal()以外のリポジトリに登録できません。kotlinMultiplatformに対応するartifactIdはプロジェクト名に何もsuffixを付けないものにする必要があります。そうしないとMPPアプリや派生MPPライブラリで参照した時に、それらのcommonMainでの参照解決ができなくてまたハマることになります。
    • デフォルトでMavenパッケージとして登録できないartifactIdが生成されるのはまずいのでYouTrackに登録したのですが、「artifactIdを条件分岐で書き分けて指定しているのが原因だ」と返ってきました。いろいろ調べて出てきた情報が、現在ではことごとく間違っていることになった…(!) 確かに全部消したらGradle Pluginが命名をいい感じに処理するようになっていて、何も指定する必要がなくなりました。でもそもそもmaven-publishで発行したパッケージで期待通りに参照解決しなかったからWebで情報を探すことになったわけで、この辺の変更はたぶんここ1ヶ月くらい(Gradle-7.0?)で出てきたものなんじゃないかって思います。
  • Androidビルド設定で `kotlin { android { publishLibraryVariantsGroupedByFlavor = true } } を設定する: これはJetBrainsが?Googleが?推奨しないと書いているのですが、こうしないとdebugとreleaseでartifactが分かれることになります。(分かれていてもパッケージ参照がきちんと解決されてMavenパッケージを登録するときも両方受け付けるようになったらそれでも良いと思いますが。)

Kotlin 1.5のリリースまでにIDEできちんと整合性の取れたアプリケーション構成が実現する予定があるのかはわかりませんが、手作業でいろいろ調整すれば何とかなりつつあるという感じです。

MIDI 2.0エコシステム構築術 (M3 2021春 新刊)

aap-midi-device-service開発開始時(M3当日の1週間前)に思いついたときの計画としては、MidiDeviceServiceの実装そのものは3日くらいで出来て、あとはMIDI 2.0対応とaap-lv2側のMIDI 2.0対応コードの変更で3日くらい、運が良ければPSGエミュレーターをLV2化したaap-ayumi(aap-lv2にサンプルとして含まれているやつ)でUMPを受信できるようにlv2-midi2を組み込んで対応…とするつもりだったのですが、前々日になってもMIDI 1.0でつなげるのがやっとという感じでした。

さすがに前々日の深夜になって、この展示は無理だ…と考えて、とりあえずここまでやってきたことを諸々同人誌としてまとめて電子版として販売して何とかしよう、と執筆し始めて(この辺の発想がだいたい技術書典レベル)、会場でもずっと販売しながら執筆していました。購入いただいた皆さんありがとうございました。

M3で販売していたPDFに表紙を加えて、発見した誤字などをわずかに修正したバージョンをboothで販売開始しました。

xamaritans.booth.pm

aap-midi-device-serviceは、その後いったんはアプリがクラッシュせずにaap-ayumi側のaap-lv2がUMPを受信できるところまで繋がったのですが、ビルドを調整したり他の問題を直しているうちにまた繋がらなくなって、これは今無理やりつなごうとしても意味がないと考えて止めました。

cmidi2のMixed Data Setサポートなど

aap-midi-device-serviceはMidiReceiver.onSend()から先をC++で実装しています。オーディオ再生にOboeを、プラグインによるリアルタイム処理にAAPのネイティブAPIを使うため必然的な構成です。その中でMIDI 2.0を扱うため、ここではktmidiではなくcmidi2を使っています。

ktmidiで作成したUMP前提の楽曲データにメタデータが無い問題を解決するために、Sysex8にするかMDS (mixed data set)にするかと迷っていた時期があって、その時にcmidi2にMDSサポートが実装されていなかったことに気づいたり、他にも問題がいくつかあったことに気づいたので、cmidi2にもいくつか改良が加えられています。これまでは書いただけで使っていなかったので。やっぱりUMPを生産して消費するエコシステムが出来ると、利用価値が出てくるというものです。

*1:先月も書きましたが、開発環境がLinuxでまともに動作することは必須

KotlinクロスプラットフォームMIDIエコシステムの構築

android-audio-plugin-frameworkMIDIまわりの機能をいろいろ追加したいのと、いいかげん音楽制作のためにC#で作ってしまっていたツールをLinuxデスクトップでも開発できる言語環境で開発を再開していきたいという気持ちが、ずっと燻っていました(います)。しかしまだそのための基盤が足りない状況です。

そういうわけで、2月から少し準備していたのですが、3月には重い腰を上げて、これまでC#で作っていたクロスプラットフォームMIDIライブラリやツールをKotlinに移植していました。まだAPI的に安定したものでは到底ありませんが、ある程度目処が立ってきたので一度まとめておきます。

【目次】

ktmidiプロジェクト

ktmidiはmanaged-midiのKotlin移植というべきもので、もともとはfluidsynth-midi-service-jという(これまたXamarin.Androidから移行した)プロジェクトの一部としてSMFの読み書き部分などを部分的に移植してあったものに、ネイティブMIDIアクセスAPIMIDIプレイヤーの移植を追加しているところです。

もっとも、現時点ではKotlinエコシステムには.NET Standardのようなまともなクロスプラットフォームのプロジェクト構築手法がありません。Kotlin MPPプロジェクトはJVM/JS/Nativeのみ、KMMプロジェクトはiOS/Androidのみ、デスクトップとモバイルで全てを共通化できる仕組みは無し(Jetpack ComposeはAndroidとデスクトップJVMのみで個別に対応)、といったバラバラな状態です。そのため、これらの移植はまだまだ安定性を期待できるものでは到底ありません。

ktmidiにMidiAccess APIを追加する、もともとのアイディアでは、デスクトップJavaではjavax.sound.midiAPIを使って、Androidではandroid.media.midiAPIを使う構想になっていました。しかし現状ではktmidiが採用しているMPPのプロジェクト構成ではAndroid個別対応が不可能なので(jvmMainの他にandroidMainを置くことが出来ない)、Android側は別途プロジェクトを用意するしかないという本末転倒の状態です(未着手)。

仮想MIDIポート機能

Javaにはjavax.sound.midiがあるし、Androidで互換APIを実装することもできるし、そもそもクロスプラットフォームMIDIアクセスAPIを用意する必要はあるのか?という疑問が出てくる人もいると思いますが(まず自分がそうでした)、javax.sound.midiでは「仮想MIDIポートの作成」ができないんですよね。Windowsで実現できないので、Windowsが足を引っ張っている感じです。javax.sound.midiだけでなく、Web MIDI APIにもこの機能はありません。

従来のクロスプラットフォームMIDI APIにもやはりこの機能は無いのですが、C++では最近RtMidiの後継としてlibremidiというプロジェクトが誕生しており、これはEmscriptenを使ったWeb MIDI APIのサポートも追加しているのですが(わたしが去年juce_emscriptenのためにRtMidiで実装したのと似たような感じです)、これが仮想MIDIポートの作成に対応しています。JUCEのjuce_audio_devicesにもMidiInput::createNewDevice()のようなAPIがあります。

仮想ポートが作成できないということは、仮想MIDIキーボードのアプリを作ったときに、そのアプリをMIDI入力デバイスとして使用できない(使用するためにルーティング作業が発生する)ということです。わたしが日頃から使っている自作の仮想MIDIキーボードアプリではこの機能が使われていて、DAWに接続してソフトウェアからあたかもMMLを演奏しているかのようにDAWMIDIメッセージを送信できて便利なので、この機能は必須です。

alsakt

javax.sound.midiでは足りないところとして、もうひとつ最近知ったことなのですが、javax.sound.midiLinux実装はALSA rawmidi APIで取得できるデバイスにしか対応していません。TimidityやFluidsynthのようなソフトウェアMIDIのポートや仮想MIDIポートはALSA sequencer APIでしか取得できないので(BLEも同様でしょう)、完全に使い物にならないレベルです。

無いなら作るしかない、というわけで、ALSA Sequencer APIのJNIバインディングを作りました。

https://github.com/atsushieno/alsakt

managed-midiを開発していたときにもalsa-sharpというプロジェクトを作って、そこでP/Invokeまわりを全て面倒見ていたという経緯があるのですが、ALSAAPIから手作業でバインディングを作るのはしんどいので、自分で作っていたnclangというClangの.NETバインディングを使って自動生成していました。*1

Java/KotlinでもJNIバインディングを手作業で構築したくはないので、自動化機構を選定しました。以前にfluidsynth-midi-service-jを作った時はJNAとJNAeratorを使ったのですが、JNAeratorはもともとBridJと両輪で開発されてきたプロジェクトで、BridJともども既に死んでいるので、今回は今でも開発が継続しているJavaCPPを採用しました。JavaCPPはAndroidでも使えるし以前に同人誌に記事を書いたこともあるのですが、今回はALSA SequencerなのでJVMしかもLinuxデスクトップだけです。ほぼ忘れていたのでゼロから調べ直して作りました。

JavaCPPはJNAとは異なりバインディングのビルド時にnative glueを生成する仕組みになっていて、それはjavacpp-presetsというリポジトリでいくつも例示されているように、mvnから呼び出されるように作られています。が、2020年になってgradle-javacppというプロジェクトが誕生し、IDEAやAndroid Studioからもgradleでシームレスにビルドできるようになりました。今回はこれを採用することでビルド過程をあらかた既存のワークフローに合わせることができ、maven-publishプラグインと合わせてほんの2日で他のプロジェクトからmavenLocal()で参照解決して利用できるレベルまで辿り着けました。

ktmidiでは、MidiAccessクラスのLinuxデスクトップ向け実装として、このalsaktを利用したAlsaMidiAccessというクラスを用意しています。実態としてはmanaged-midiAlsaMidiAccessをそのまま持ってきたというのに近いです。

(もしネイティブコードで使おうと思ったらKotlin/Nativeを使うことになるわけですが、JNIはKotlin/Nativeでは使えないのでcinteropを使って相当部分を実装することになるでしょう。JNIよりは楽なのではないかと思いますが、やってみないとわかりません。)

mugene-ng

今回一番何とかしたかったのは自作のMMLコンパイラmugeneです。このMMLコンパイラは単なるおもちゃではなく、MIDIで楽曲データを作成できるレベルまでマクロシステムを作り込んであって、これを生き延びさせるのが今回の主目的です。この開発が継続できるようになれば、MIDI 2.0を使った楽曲データの作成も現実的になりますし(そのためにはktmidiでMidiPlayerのMIDI 2.0サポートも必要になりますね)。

mugeneの用途としては、現状では仮想MIDIキーボードとvscodium拡張に組み込んで使うのと、MMLプラグイン定義からTracktion Waveformの楽曲データ(.tracktionedit)を生成するもの(augene)と、AndroidアプリとしてMMLからコンパイルして演奏できるようにするというのがあります(fluidsynth-midi-service-jの前身であるXamarin.Android版のfluidsynth-midi-serviceではMMLから演奏する機能もデモアプリに付けていました)。今後オーディオプラグインなどに組み込んで使うことを考えると、ネイティブコードとしても利用できるようにしたいところです。これらを全て自然に満たせそうなのは、現状ではC++とKotlin、あとはDartくらいかなと思っています(開発環境がLinuxでまともに動作することは当然必須)。MMLコンパイラはリアルタイムでは動作できないのでGCがあっても問題ありません。

MMLコンパイラ自体の機能要件は単なる文字列処理でしかないのでどんな言語でもすぐできるはずなのですが、構文解析をLALR(1)のjayで実装していたので、なるべくそのまま移植したいところです。今回は最終的にantlr-kotlinで実装しましたが、一筋縄ではいかず(その紆余曲折は途中まで別途まとめてあります。気が向いたら続きを書くかも)、一度はANTLR4/Javaを使って生成したコードをIDEAのnj2kを使って手作業でKotlinに変換してその結果をさらに修正したり(ついでにnj2kのバグを直したり)で寄り道しつつ、最終的に文法定義のレベルで対応してantlr-kotlinを使い続けられるようにしました。

プロジェクトはとりあえずはKotlin MPPで作って、JVM以外にJSでもNativeでもいけるようになっています。これもデフォルトで組み込むMMLファイルの解決にAndroid固有の処理が必要になりそうですが、外部からプラットフォーム固有の実装を繋ぎこめば何とかなるレベルです。JSがいけるはずなので、いずれvscodium拡張も書き換えようと思っています(JSはまだ試していない)。

あと、これは現状では全く使われていないのですが、MMLを書いてMIDIバイスを操作するのではなくAPIとしてプログラマブルに操作したいという場面で使えるように、mugeneのdefault_macro.mmlの命令に相当するAPIを実装したnotiumというライブラリを作ってあったのですが、これもついでにKotlinに移植しました。

あとaugeneも作り直したいところですが、現状のツールのアプローチが.tracktionedit全体を毎回生成する方式で、トラックが増えてくると毎回ロードするために数十秒待たなければならなくなって致命的に生産性が低いので、いったん全部仕切り直して、MML処理部分はKotlin/Nativeでネイティブライブラリ化してJUCEアプリに組み込むのが妥当かなと思っています。それなりに実装時間が必要になりそうなので氷漬けです。

kmmk

こっちはまだ全然出来ていない状態ですが、ALSA実装が組み込まれたktmidiを、Jetpack Composeで作り直し中の仮想MIDIキーボードkmmkで使っています。アプリケーションのUIはJetpack Composeで、モバイル前提の設計になるので(Androidでも動作しています)、キー入力イベントなども根本的に作り直しになる予定です。そもそもxmmkではボタンは単なる飾りでクリックにも反応しないので作り直しともちょっと違う…

仮想MIDIキーボードのアプリはいろいろあるのですが、今ほしい機能を仮想MIDI入力デバイス以外で挙げるとしたら…

  • 入力の記録(これはxmmkではMMLとして実装済
    • ついでにMMLからの簡易再生(xmmkでは実装済
  • 簡単に使えるプログラムチェンジ(xmmkではメインメニューで実装)
  • 多様なMIDIバイスの音色プリセットのサポート(xmmkでは実装済
  • 簡単に88鍵くらいにアクセスできるキーボード(PCキーボードに反応するのは一定範囲のオクターブだけで良い)
  • キースイッチ情報の適用

といったところでしょうか。キースイッチに関しては何も規格がない状態なので、動的に取得するのがまだ困難かもしれません(MIDI 2.0 Property Exchangeで取得できる時代になってほしい…)。いずれにしろ風呂敷を広げすぎても意味がないので、できるところから実装していこうと思っています。

*1:Microsoft/ClangSharpというプロジェクトが似たようなことをやっていて、当初はAPIが全然イケてなかったのですが、最近ではnclangのようなOOラッパーも追加してまともに使えるようになっていそうなので、nclangを使う必要はもう無いでしょう。

3月の活動記録(2021)

3月の開発記録です。長くなってしまったので分割して、こっちは「その他」編です。

VST3とSFZ探訪

2月にリリースされたWaveform 11.5がLinuxでVST3をサポートするようになりました。これによって、昨年10月で書いていた、プラグインをtracktion_engineとWaveform11.5の間で共有できなかった問題が、VST3によって解決したことになります。そういうわけで、3月はVST3プラグインをいろいろ試していました。これはまとまった時間ができたら何かしらアウトプットを出したいと思います。(まだ無い状態)

ただ、Waveform 11.5 (on Linux) はまだまだオーディオエンジンが不安定で、トラックが10本を超えた辺りから(多分あまりプラグインを問わず)頻繁にクラッシュするようになるので、打ち込み作業での実用性はまだまだ覚束ないところです。

3月にはもう一つ嬉しいニュースがあって、Linux版のReaperがなんとLV2プラグインを使えるようになりました。LV2はクロスプラットフォームで利用できるように設計されているので、これはいずれMacWindowsでも使えるようになるかもしれません。

2月にはVitalが公開されたこともあり、シンセプラグインはそれなりに選択肢が出てきたのではないかと思っています。サンプラーのほうもバラエティを広げておきたいところです。エンジンはsfizzが最有力候補だと思っていますが、こちらもVST3版の開発が進んでいて、Waveformからも直接ロードできるようになっています。半年くらい前からLV2 UIの開発が進んでいたのですが、これはvstguiで作られており(vstguiはGPLv3ではないので気軽に使えるのです)、VST3版でも自然に同じものが組み込まれています。

sfizzはv1.0まではSFZ v1を主なターゲットとしていますが、SFZ v2やARIA独自拡張命令なども一部実装されていて、最近のバージョンではkeyswitchなどもサポートされるようになりました。その結果、たとえばUI METAL GTXのようなkeyswitch前提の音源もロードしてある程度期待通りのサウンドが生成できるようになっています。実用性がどんどん上がっているので今後も楽しみなところです。

aap-lv2-dragonfly-reverb

Dragonfly-Reverbはgithubで公開されているリポジトリの中では人気の高いリバーブエフェクトで、LV2とVSTがサポートされています。dragonfly-reverbのもうひとつ大きな特徴としては、このプラグインDPFで実装されています。DPFはLV2をサポートしているので(そのために存在するようなものなので)、これをAAPに移植できればDPFもサポートできたということになります。DPFによるVSTサポートにはVST2SDKではなくVeSTigeが使われているので、2021年でも誰でもビルドできます。

VSTをいろいろ探す作業の一環として、利用可能なプラグインで楽曲のテンプレートを作っていたのですが、Dragonfly-Reverbはリバーブの中ではかなり有力な選択肢なので、Waveformで打ち込んだものをAAPでAndroid上で演奏しようと思った時に使えるようにしておきたいところです。

DPFはビルドシステムらしきものがMakefile前提で作られており、sfizzやfluidsynthのようにCMake前提でビルドすることはできません。そのため、mda-lv2やguitarixのようにandroid-native-audio-buildersリポジトリAndroid用にビルドするやり方で対応しました。ビルド自体はスムーズに実現できたのですが、プラグインの挙動自体がまだ期待通りではないので、いずれtweakが必要になるでしょう。

https://github.com/atsushieno/aap-lv2-dragonfly-reverb

AAP: Compose UI and audio preview in plugin apps

2月までの実装では、AAPのプラグインアプリに付属のMainActivityが立ち上がるとプラグインのリストと、それらを選択するとポートリストを表示する機能が伝統的なUIで実装されていました。3月はJetpack Composeのfirst betaがリリースされたこともあって、ComposeをAAPに適用するには悪くないタイミングです。というわけで、まずこのUIをJetpack Composeで実現するところから始めました。Composeはこの時点でmainブランチに取り込まれていなかったのですが、本格的にComposeにコミットしていく流れになったと思います。

もともとのUIも単なるプラグインとそのパラメーターのリストなので大したことはないので、ついでにホストアプリケーションのサンプルで実装していたオーディオ出力のプレビュー機能を、プラグイン側にも実装することにしました。もともとUIの大部分が再利用の産物だったので、ホスト側のオーディオ処理をモジュールとして独立させて共有してしまいました。

プラグインアプリ内で利用できる機能として実装したことで、デバッグ作業も少し楽になりました。(インプロセスでオーディオ処理できているわけではないので、あくまでデバッグが楽になった程度です。)

KotlinクロスプラットフォームMIDIエコシステム

これはまとめていたら長くなってしまったので別エントリにします。

2月の開発記録(2021)

今月は前のめりにまとめた完全自分用の開発作業記録です。今月は上旬にちょっとDSPまわりの勉強をしていた以外は、だいぶAndroidAudioPlugin関連の開発に時間をつぎ込めた感じです。

s:/aria2web/aqua/

割とどうでも良い前フリから。もう半年以上前ですが、aria2webというsfizzサンプラーARIA extension GUIをWebViewとしてロードできる仕組みを実現したLV2プラグインを作りました。

atsushieno.hatenablog.com

プロジェクト名がイマイチだったので*1、いずれAAPに取り込んでWeb UI化するときに名前を変えようと思っていたので、今月ようやく着手しようと思った時点でaquaに改名しました。

XML namespacesを適切に利用した拡張管理

そういうわけで、先月末にいよいよGUI統合に取り掛かりたいと書いていたやつですが、実は未だに何もない状態です。先に片付けるべき課題があったためです。具体的には、プラグイン個別のGUIに対応するより先に、GUIのないプラグインの汎用パラメーターコントローラーリストのようなUIを(特にWebUIで)作るべきだろう、と考えました。

そのために実際に必要になるのは、パラメーターの種別に応じたUIコントローラーであり(値の範囲が整数なのに小数値を入力したくはないわけです。あるいはそもそもLV2 Atomのように数値でない場合とか)、そのためにはまずパラメーターのプロパティが規定される必要がありました。LV2で言えばport propertiesというコンセプトです。LV2であればlv2:portPropertyとかunits:unitなどがこれに相当します。

ポートのプロパティは拡張可能でなければなりません。拡張可能な情報を素直に実装するとしたら、適切な名前解決が重要です。 さし当たって、それまでdefault, minimum, maximum属性として規定されていた<port>要素のプロパティはurn:org.androidaudioplugin.port名前空間に属することになりました。これまでAAPは自由に拡張可能としてきたわけですが、C APIレベルでの拡張性しか実装していなかったので、ポートのプロパティのようにメタデータaap_metadata.xml)を拡張するやり方が未整備でした。

この拡張を実装するにあたって問題になったのが、AAPの内部でtinyxml2を使ってメタデータを処理していたのですが、tinyxml2はXML namespacesを処理できないことでした。マジか…他に何が使えるんだ…と思って調べてみると、PugiXmlもRapidXMLもダメ、使えるのはexpat(APIが完全に古い), libxml2(glib依存が厳しい), xerces-c++(でかい)…といった感じでした。ここは20世紀か?という感じの古参ばかりですが、あまり深入りしたくなかったので、Android側でのメタデータ解析は全てAndroid APIでやることにして、デスクトップのみlibxml2にして回避しました。ちなみにJUCEに含まれるXMLAPIダメです

LV2みたいにTurtle文法を選択してパーサの選択肢が無くなるよりXMLにしたほうが絶対良いでしょ、と考えていた(いる)わけですが、C++がこんなにxmlnsをサポートしていないパーサばかりだとは思っていなかったので、自分がC#やらKotlinやら使ってコードを書いていたのはずいぶんスポイルされているんだなあ、となりました。しかし、うーん…さすがに2021年からC++XMLパーサを実装するのはどうかな…

ちなみに今月はこのポート プロパティの基盤を実装しただけで、それを反映するGUIの構築には着手していない状態です。

flatten LV2 dev. environment

aap-lv2が12月にリポジトリを分割して開発しやすくなったので、aria2webをaquaに変更する過程で気づいた、sfizzで期待通りにオーディオデータが生成できなかった問題に着手しました。が、中で使われているLV2 toolkit (serd + sord + lv2 + sratom + lilv)がバイナリ参照になっていて、デバッグ時に中まで追えずに苦労していました。その度にデバッグのためにリポジトリの構成を変更してLV2 toolkitを一緒にビルドしてはコミット前に戻す、みたいなことを繰り返していたのですが、こんなことをするくらいなら全部aap-lv2のリポジトリでビルドすべきだ、と考えるに至りました。

実際これを避けていたのは(1)公式のビルドスクリプトがwafだったので公式に可能な限り準拠したやり方にしたかったのと、(2)Androidの4アーキテクチャ分 x この数のネイティブライブラリビルドの増加はCIビルド時間に有意に悪影響が出ると考えていたからなのですが、wafをスキップして直接CMakeでまとめてビルドしてみると個々のライブラリのビルド時間はそれほど長くも無かったので、全部フラットにビルドすることにしました。これでデバッグ体験はかなり良くなっています(現状やるのはわたしだけだと思いますが)。

ちなみにこの作業自体は前述のaquaをAAPに取り込んでWebUIの実験台に使おうと思ってまずsfizzの動作確認…となってオーディオ側の問題に気づいて着手したのですが、前述の通りGUI統合に入る前にポートのプロパティを…となってこれも未着手です。

Hera (Juno 60 emulator) port to AAP

今月はRolandからJuno 60プラグインが出て話題になりましたが、ほぼ同時期にADLplugやsfizzの開発者がHeraという新しいJuno 60エミュレーターのJUCEのプロジェクトを公開していました。そういうつもりはないんだけど完全に開発者の追っかけみたいなムーブになっている…

github.com

既存のエミュレーターにJUCEのMPEサポートを加えて作ってみた、というコンセプトで作られているようです。RolandのほうはMPE対応はしていなさそうですね(featuresを見ているだけですが)。

f:id:atsushieno:20210226001343p:plain
Hera on Waveform 11.5

CMakeベースのJUCEプラグインだったので、ちょうどCMakeでInstrumentとして機能するやつがほしかったんだ…! と思って、その日のうちにAndroidに移植しました。

github.com

aaphostsampleで開くとちゃんと音が出るので、最近ではOB-Xdと並んで簡単に動作チェックする時に活用しています。移植作業もEqとChowPhaserを移植していたのを応用しただけです。Heraとは全然関係ないのですがChowPhaserの作者から「Androidに移植してるの?? 試せる??」みたいなメールをいただいて「すまん…JUCEのaudio inまわりがおかしいからまだだ…!」みたいなお返事を書くことに…

Helio workstation with AAP

AAPのプラグイン移植はずいぶんバリエーションが増えてきたのですが、ホスト側はさすがに使い回せるアプリがほとんど無く、自作のaaphostsample、JUCEのAudioPluginHost、あとtracktion_engineを使うaugeneが一度も演奏できたためしがない…という状況でした。

JUCEを使ったDAWのひとつにHelio Workstationというのがあるのですが、これがTabletのみを意識しているとはいえAndroidでも動作すると謳っているので、これにAAPサポートを追加したいというのがひとつのプロジェクト完成形の姿でした(任意のDAWで任意のプラグイン移植が使える状態といえるわけです)。

github.com

ただhelio-workstationのプロジェクトは、Android用のプロジェクトなどが既にProjucerで生成された後の構造をいじって作られているようなので、既存のaap-juce移植のモデルにはうまくフィットしませんでした。何回か失敗した後で、スクラッチからHelio向けに独自のアプローチでビルドプロセスを構築することで(といってもどっちも自分しかやっていないのですが)、ようやくAAPサポートを追加できました。

github.com

ほぼオリジナルのコードに手を加えること無く実現したので、無理にビルド手順を一般化しないほうがいいのかなあと思い始めています。昨日も公開されたばかりのVitalを移植しようとしたのですが、このプロジェクト構造も一般的ではないのでAndroidビルドを追加する時点でかなり無理っぽさが出ています。いずれやるとしても何日かかかりそう…(本家でやってほしさある)

ちなみに現時点で、プラグインの一覧が取得できるものの名前の表示がイマイチ(これはオリジナルがそう)、オーディオグラフの設定画面ではオーディオ出力ポートが出てこないので生成結果を一切音声出力できないっぽく見える、その割に実はプラグインインスタンスも生成できていてノートに合わせて音が生成できる、ただし出てくる音がノイズまみれの謎音…みたいな状態なので、実用性はまだ無いです。

f:id:atsushieno:20210226002110p:plain
Helio+AAP
f:id:atsushieno:20210226002135p:plain
Helio+AAP, plugin details

プラグインインスタンス生成の安定化

AAPで初期からずっと困っていた最大の不安要素のひとつが、プラグインインスタンスを複数回生成するとプラグイン側のサービスが落ちてホストも一緒に落ちる、という問題でした。これだとちょっと使うだけですぐホストが落ちるので使用体験を語れるほどのレベルにも至っていない感じだったので、port propertiesが落ち着いたところでがっつり取り掛かりました。

いろいろ試行してみてわかったのは、どうやらandroidaudioplugin.aarをbuild.gradle上でmavenLocal()から解決していると発生するが、デバッグするためにsettings.gradleをいじってプロジェクトの一部にandroidaudiopluginを追加してbuild.gradle上でproject(':androidaudioplugin')として参照すると発生しない(!)ということでした。

何でこの話を書こうと思ったのかというと、事実関係を確認する前提が割と罠だらけなので記録しておきたかったわけです(自分用メモなので!):

  • mavenLocal()でパッケージを解決する時は、ローカルでビルドして修正を加えているバージョンが参照されているかどうか、build.gradleの内容から念押しする必要がある(settings.gradleで追加しても自動的にproject(':...')で解決するわけではない)
  • CIでビルドしても通るようにsubmoduleのandroid-audio-plugin-frameworkmakeでビルドしているので(前述のaap-lv2リポジトリ分割について書いたエントリを参照)、別のソースツリーでandroidaudioplugin.aarやandroidaudioplugin-lv2.aarに手を加えたものをアプリで確認するなら、maven localのパッケージを上書きしないようにビルドする必要がある
  • debugビルドとreleaseビルドの両方でmaven-publishされる構成が変わるので、自分の期待通りのものがaarとして発行されているか、build.gradle内のpublishToMavenLocalタスク関連の記述をきちんと確認する

今回の問題は急場しのぎ的にreleaseビルドのaarをpublishToMavenLocal~/.m2/repository以下にデプロイしないように変更したら直りました。それ以上のビルドの細かいオプションのどこに違いがあったのかは、まあやる気が出たときに…。関連するネタとしては、JUCEプラグインAndroidに移植しているときにJNI_OnLoad()がreleaseビルドでだけ衝突する問題が発生したことがあって、その時はldに-fltoが渡されていたのが引き金になっていた、というところまで調べたりしました。これもそれ以上の原因は追及していなかったり…。

いずれにせよ、この処置を施した後は、プラグイン インスタンスの生成まわりで不定期的にクラッシュする場面はほぼ無くなったので、ようやく安心してある種のアプリでは使えるようになったんじゃないかと思います。

今後の方針

現時点で直しておきたいのは(1)Frequalizerを含むJUCEのエフェクタープラグインが全滅している問題(JUCEのバグ)と(2) HelioでプラグインをロードしたときにAudio Outが適切に認識されない問題(AudioPluginHostでは出てくるのでたぶんHelioのバグ)、の2点で、着手するならこの辺でしょう。

あとは今月ようやく2021年版ロードマップ(2021年中に全部やるとは言ってない)を作ったので、その中からおそらくGUIまわりを中心に消化していくことになるでしょう。

github.com

とはいえ4月にM3 2021春があってそっちに向けて何かしらまとまったものを作りたいので、開発はしばらく休止かもしれません。あと多分何かしら別の原稿を書くことになると思いますし。はーどうしよ…

*1:aria2webだとただの変換処理系みたいに見えるので。実際、最初はそうだったのですが…