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を使う必要はもう無いでしょう。