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でやっていくのが良さそうかなと思っています。