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でまともに動作することは必須