M3 2021秋 サークル出展情報

もう明日になってしまいましたが、M3 2021秋のcircle gingaサークル参加情報です(GINGAレーベルとは無関係です。って毎回書いている気がする)。コロナウィルスが今のところ大人しくなっていてこれじゃ夜逃げの言い訳にできねー、当初予想していたよりは物理会場にも参加しやすくなっているかもしれません。入場制限はきちんと行われるはずですし、当サークルもノーマスク・鼻出しマスク等に警戒しつつサークル参加します。

というわけで、今日は出展情報です。10/31当日は circle ginga (G12) にてお待ちしています。

新刊: MML to DAW via MIDI 2.0: 次世代MMLコンパイラ開発研究

今回も音楽制作は着手できませんでした…が、4月にC#からKotlinに全面移行してMIDI 2.0サポートも追加したMMLコンパイラmugene-ngを、JUEC AudioPluginHostのオーディオルーティングを組み合わせてTracktion EngineというかWaveformの楽曲データを生成するツールaugene-ngとして発展させていて、現実の打ち込み作業で使えるところまで成果を出せるようになりました。この動画は最近ずっと打ち込んでいた…というか、いる…augene-ngのサンプル楽曲です。

youtu.be

映像は工夫がなくてTracktion Waveform上で再生しているだけですが、このファイルはWaveform上では全く手を加えていない自動生成物です。Waveformユーザーならわかると思いますが(滅多にいないとおもうけど)、こんな緑一色の画面なんてフツー見ることがありません。コンパイルされたファイルには色設定情報が含まれていないんです。どうやらデフォルトは緑みたいですね。

ともあれ、そういうわけで、自分で作曲する代わりに著作権切れの管弦楽曲を使って、とりあえずはワークフローとして現実に作業できることを示そう、というdogfoodingをやっていました。先月もその作業成果としていろいろ知見がたまってきたので、M3ではこれを音楽技術書の題材にしてまとめておこう、と考えました。それで出来上がったのがこちらです…!

表紙(暫定)

表紙が…まだ…できてない…!! 現在鋭意執筆中なので、完成品には何かしらの表紙がつくと思います…つくといいな…。内容は60ページ弱くらい書いてあるので、たぶん今日図版を追加したらもうちょっと膨らむと思います。

内容としては主に、DAW(今回はTracktion)の楽曲データには何を含められるのか、MMLMIDI 2.0命令を出すにはどうするのか、MIDI 2.0の何をDAWに持ち込めるのか…といった話題をまとめています。Kotlinの話も1章くらい書いていますが、どんなライブラリを使っているか、みたいな話がメインでコードは出てきません。(ツールの使い方の説明をしたいわけでもないので、一応含めてはいますが、あまり期待しないでください…)

M3会場では、先のデモ音源とMMLの展示(たぶんモバイルデバイス…もしかしたら紙かも)と合わせて、この新刊(電子版)を500円くらいで販売する予定です。

目次も以下に2ページだけサンプルとして出しておきますが、最終版は変更される可能性があります:

目次1 目次2

追記: 表紙も出来た…!

表紙

旧刊同人誌・音楽CD(物理)

circle gingaの旧刊同人誌および音楽CDも少数部ですが当日用意しておきます。これら以外も含め、旧刊の電子版は当日に限らずいつでもboothおよび技術書典オンラインで購入できます。

デモ楽曲について

誰でも知っていそうなホルストの火星です。昔から打ち込んでみようと思っていたのですが、先月発売されたActraiser Renaissanceで遊んでいて、やっぱりコレでいこう…!ってなったのでした。(買ったけどまだやっていない、フィルモアまでしかやっていないという人は、とりあえず「世界樹」まで進んでみてほしい…わたしが何を言っているかきっとわかると思います。)

MMLIMSLPで公開されているパブリックドメインのフルオケ楽譜から30ページぶん目コピです。先週もDroidKaigiを聞いたりしながら裏で画質の悪い譜面とにらめっこしていました(後から聴いて直したところがたくさん…)。これだけパート数があると全部チェックするのも大変ですね。全パート聴いておかしいところを指摘する指揮者の仕事が大変なのがよくわかるやつ…

デモ楽曲は、今回は完成版とは言い難いところがひとつあります。使用している音源がsf2(サウンドフォント)で、オーディオプラグインとしてももこれをVST3としてロードしているjuicysfpluginなのです。フリー音源を使うのはこのaugene-ngのデモ楽曲として作成している以上は維持しておきたい制約なのですが(Kontakt等に逃げたくない)、もう一歩進めて、Virtual Playing Orchestraのsfzをsfizzから使用するバージョンの作成にも着手しています(augene-ngの目的は「今のところDAWでしか作られていないレベルのものをMMLでも作り出す」ことにあるので…)。ただ、音量もオクターブもその他全体的に不明な変更量の作業が約30トラック分あるので、これは今回は無理だと判断しました。

とはいえ、sf2でもRealistic Soundfont V2: Libre Editionという割と音質の良い音源を使っているので、ところどころおもちゃ感は出てしまうものの、20世紀のMIDI音源モジュールよりはずっと「それっぽい」音になっていると思います。

作業が現在進行形のsfizz版も含めて、MMLとプロジェクトのソースは全てgithubで公開してあるので、ソーセージの中身が気になる人は見てみてください。

9月にはayumi-juceなどを作っていたので、OPNplugと合わせてFM音源で何かやろうと思っていたのですが(実際ゲーム曲のコピー打ち込みなどもあるのですが)、こちらは自作曲でもないとgithubリポジトリで公開できないので、今回は見送りました。

recap

そういうわけで、10/31当日M3に来られる方は東京流通センターでお会いしましょう。 circle ginga (G12) にてお待ちしています。

Per-Note Expressionsサポートに関する覚書

調査の端緒

MIDIに変換するMMLで打ち込み作業をやっている時に、それぞれのノートの発音中に、それぞれのタイミングで音量を調整して打ち込みたいフレーズが出てきた。アルペジオ奏法みたいなことをシンセ音でやったことがあれば理解してもらえるかもしれない。たいていは単一のエンベロープで妥協しそうな気もするけど、特定のノートだけ違う力加減で発音したいような場合だ。

VelocityはMIDIノートオンのタイミングでしか指定できないから、VolumeかExpressionを使って実現するしかない(そのためのMML命令は実装してある)。しかしVolumeもExpressionもコントロールチェンジなのでチャンネルに帰属する。同じトラックで打ち込む以上は同じチャンネルで打ち込むことになる(理論的には別々のノートに別々のチャンネルを当てはめることは可能だけど、一般的なMML打ち込み作業でやるようなことではない)。

モダンなDAWとオーディオプラグインの世界では、MPE (MIDI Polyphonic Expression)とか、VSTのNote Expressionという、ノート毎にコントロールパラメーターを設定できる機能が存在している。MPEはMIDI 1.0で表現できる範囲内でこれを実現しているので、MPEをサポートする任意のMIDIバイスから、MPEをサポートする任意のDAWにデータを送ることが可能だし、その途中にどんなMIDI処理系が挟まっていても、メッセージを加工されない限りは大丈夫だ。AudioUnitでもAUAudioUnitsupportsMPEというプロパティが存在する。

そしてMIDI 2.0 UMPにはMPEの流れを汲むPer-Note ControllerやPer-Note Pitchbendといった機能が備わっている。これらを受信して処理できる「MIDIバイス」なら、これらのメッセージに対応して発音をノート別に調整できるはずだ。MPEで制御できるべきものとして規定されているのはピッチベンドとプレッシャー(アフター・タッチ)とCC 74(一般的にはカットオフ)くらいだが、MIDI 2.0 UMP仕様にそのような制限的な言及は無い(MPEでもその他をサポートするのは自由ではある。一部「推奨しない」ものもある。詳しくはここに書いた)。

VST3のNote Expressionは、MIDI 2.0 UMPにおけるPer-Note Controllerに近く、サポートされるノート別パラメーターについて具体的な制限は無く、プラグイン側が任意に規定し、ホスト側はそれをユーザーに提示するだけのようだ。

いずれにせよ、現時点でどこまで自作ツールで「実現」できる機能なのか把握しておきたかったので、関連仕様とサポートの実態を調べることにした…というのが半分で、残り半分くらいは日頃の調べ物を少しまとまったかたちで記録しておこうと思ってのことだ。

MPE入力の手法

ここでは話題の対象を既にDAWで実装例がいくつもあるMPEに限定するが、他のPer-Note Expressionについても当てはまる話ではある。

MPEは特定のノートに対してコントロールチェンジ等を送るもので、その入力は一般的なコントロールチェンジのようにトラック毎に入力させることはできない。ノート毎に入力させる必要がある。少なくとも、特定の音階に対して入力させる必要がある。常にあるノートでノートオンするたびに毎回同じコントロール値を期待するという状況は、ユースケースを考えてもあまり一般的ではなさそうだ。常に固定の相対ピッチで微分音作曲するなら、MTS (MIDI Tuning Standard) に則って設定したほうが適切で、オーディオプラグイン(インストゥルメント)の場合は微分音作曲に対応している場合はscalaファイルをサポートしているのが一般的だ。

自分が知っているところでは、Audio Developer Conference 2019でSteinbergの開発者がBitwig Studio、Cubase、ReaperでそれぞれどのようにMPEを入力するか紹介しているので、興味があれば参考にされたい。

youtu.be

この後ちょっと話に出てくるTracktion Waveformでもこんな編集画面がある:

MPE on Tracktion Waveform11

(ここでは何度も紹介しているけど、Tracktion Waveformは音楽編集・再生部分がtracktion_engineとしてGPLv3で公開されているので、次に言及する自作ツールでも活用している。)

TracktionのMPEサポート

自分のMML to Tracktionコンパイラaugene-ngでは、MMLコンパイラmugene-ngを使っていったんSMFに変換した上で、Tracktion Waveformで編集できる && Tracktion Engineで再生できるような楽曲データ(*.tracktionedit)に変換するものだった(過去形)。今回まとめている内容を実践する過程で、SMFからMIDI 2.0の楽曲データに移行している(mugene-ngでは既にMIDI 2.0楽曲データの生成をサポートしている)。そして、MIDI 2.0のPer-Note Pitchbendを、*.tracktionedit上ではMPEとして変換されるように実装した。

ただ前述の通り、MPEで標準的にサポートされているのは、ピッチベンドとアフター・タッチとCC #74(一般的にはカットオフ)のみであって、たとえばCC #07 (Volume) やCC #0B (Expression) に相当するものを、MIDI入力デバイスから送ることは出来ても、MPEのホスト側が受け付けないことはある。

今回は、VolumeやExpressionをPer-Note ExpressionとしてMMLで表現してUMPに変換して、それを*.tracktioneditに出力したいと考えた。しかし、Tracktionでは、MPEで送られてきた情報は、<NOTE>要素の子要素として、<PITCHBEND><PRESSURE><TIMBRE>のいずれかのみ保存されるようだ。つまり、Tracktion側にはNote Expressionを受け入れる場所がない。WaveformのGUI上にもこの3つの選択肢しか無いし、tracktion_engineの関連しそうなソースコードを見ても、この3種類しかサポートしていないようだ。

JUCEとNote Expressionサポート

そもそも、Steinbergのスタンスとしては、Note Expressionサポートはオーディオプラグインホスト側で実装されている必要がある。Tracktion EngineはJUCEで実装されているので、juce_audio_plugin_client(オーディオプラグインホスティング用モジュール)がVST3のNote Expressionをサポートしていなければ、Tracktion Engineが独自にサポートしている可能性は限りなく低い。そして実際JUCEではNote Expressionをサポートしていない。一方でJUCEはMPEのサポートには積極的だ。MPEは基本的にMIDI 1.0をサポートしていればあとは送り手と受け手の問題(中間層はただ転送するだけ)ということもあって、幅広く受け入れられている。

JUCEがNote Expressionをサポートしないのは、Note ExpressionがVST3固有の機能でしかない(AudioUnitでサポートされていない)からだと言えそうだ。もっとも最新のCoreAudioに含まれるAudioUnitではMIDI 2.0をMIDI入力として受け付けられるようなAPIを持っているように見える。もしそうだとしたら、juce::AudioProcessor::processBlock()MidiBufferがUMPを受け渡しできるように機能追加できるかもしれない。いずれにせよJUCE本体とTracktion Engineの両方に手を加えないと実現出来ないレベルの話だ。

VST3 MIDI Outputの問題

仮にJUCEに手を入れてMIDI 2.0をホストからサポートしたとしても、MIDI 2.0をサポートするMIDI出力プラグインの扱いが割と厄介な問題になる。主としてVST3がMIDI出力をまともにサポートしていないということが批判的に言及されることが多いようだ。入力をVST3のIMidiMappingでVST3のイベントに置換する時点でもイベント順序がノートとコントロールの間で保持されないなど致命的な問題があったようだ。このKVR Forumのスレッドの1ページ目の最後によくまとめられている。

www.kvraudio.com

JUCE ForumでもMIDI to MIDIプラグインまともに機能しないの?というスレッドがある。VST3のMIDI出力プラグインはbastardなどと言われている。もちろんjuce::AudioProcessorにproducesMidiというプロパティが在る程度には一般的な機能だった。

forum.juce.com

VST3のNote ExpressionとMPEどっちがどう良い?みたいな議論は割と昔からあるようだ。ここではBitwig StudioのKVR Forumの議論をひとつ紹介するにとどめておこう。

www.kvraudio.com

MIDI 2.0はNote Expression以上にサポートされていないが、Steinbergに手綱を握られること無く幅広く互換性が期待できるという意味では(そしてすでにAudioUnitでサポートされているいそうな様子からも)、今後はMIDI 2.0サポートを介在してのNote Expressionサポートが期待できそうではある。

LV2とMPE

VSTではNote Expression、AUではMIDI2の採用によってPer-Note Expressionが実現していそうだが現実的にはMPEしか出来ていないっぽい、ということはわかったが、LV2ではどうだろうか。実のところLV2ではMPEサポートも危うく、現実的にはLV2をサポートしているホストでMPEをサポートしているものは無いかもしれない。Ardourの場合は「MPEをサポートしようと思ったら根本的な再設計が必要だ」というのでサポートしていないようだ。

discourse.ardour.org

QTractorも「MPEはMIDI 1.0に基づいて入力できるでしょ」というレベルでサポートしていないようだ。

www.rncbc.org

この意味ではLV2を後からサポートするようになったReaperなどのほうが期待値が高いかもしれない(ReaperのLV2サポートの現状はちょっと検証できていない)。

MPEとサンプラー

話をホスト側からプラグイン側に移そう。今回自分が使いたかったのはFluidsynthやsfizzなどのサンプラーだ。Fluidsynthは良くも悪くもSF2ファイルをもとにMIDIソフトウェア・シンセサイザーとして動作することだけが考慮事項で、ここでは何度か紹介しているがMIDI 2.0を部分的にサポートしている。ただ、今回機能してほしいのはPer-Note Controllersで、これはまだ実装されていないようだ

sfizzのようなSFZサンプラーの場合、話はもう少しだけややこしくなる。というのは、SFZはもう少し細かいサンプラーの制御をSFZファイル側で記述する側面があるので、sfizzだけが独自に機能拡張できる話ではないからだ。開発者コミュニティにおける議論をざっくり検索して眺めてみた限りでは、SFZでもノート別にコントロール・チェンジの挙動を細かく記述することは可能なようだ。ただし、SFZのポピュラーな商用サンプラーSforzandoも含めてそれらをサポートしているものは見当たらないようだ。

github.com

LV2にMPEサポートが(仕様面にしろ実装面にしろ)無さそうなこともあって、sfizzなどのSFZサンプラーでMPEやNote Expressionがサポートされる日はまだまだ遠いかもしれない。

とはいえ、おそらくsfizzの開発者もこの辺のサポートが手薄であることを気にしているのではないかと自分は想像している。sfizzの主要な開発者の1人が今年公開していたのが、MPEをサポートするJUNO-60クローンだった(JUCEで作られている)。これは既存のシンセサイザーにMPEを組み込むのを試してみた側面があるように見える(READMEの冒頭で言及している)。

github.com

仕様上はPer-Note Expressionの実現を妨げるものがないとしたら、MIDI 2.0をサポートするプラグインホストが出てくれば、サンプラーがPer-Note Expressionをサポートする動きが出てくるかもしれない。

ちなみにnkiフォーマットはKontaktがそもそもVST3に対応していないレベルなので無理ではないかと思う(出来てMPEサポートくらい?)。

9月の開発記録(2021)

月末恒例9月の自分用開発記録です。といっても今月はコーディング作業はそんなに注力していません。先月の終わりに

MMLでまた打ち込みできる環境を何とかしたい…

などと書いていたように、状況で言えば2019年1月とか2019年10月とか2020年10月くらいのノリで、M3に何か出せるものを作れる環境を構築しようとあくせくしている感じです(この時点で環境構築とか言ってるのは既に無理そうな気がする)。

ayumi-juce

先月augene-ng(相変わらずこの名前がいつまで続くのかは未定)でオーディオプラグイン + MMLの制作環境がある程度現実的になってきたので、ためしにOPNplugのYM-2203 (FM) + ayumiのYM-2149 (PSG) の組み合わせで打ち込みを試しています。ただ、これを現実にするためには、実はひとつ追加作業が必要でした。ayumi-lv2は作ってあってaugene-ngでも音は出せたのですが、製品版のTracktion Waveform11ではLV2はサポートされていないので、VST3版を作る必要が生じてしまいました。

augene-ngが前提とするAudioPluginHostがVST2をサポートできていればそれでもいいのですが、そうでない環境ではVST3のみがWaveform11との共通解になってしまっている状態です。ayumiは、別にわたしが手を出さなくても、zynayumiというプロジェクトがあって、そっちのほうが完成度がはるかに高いのですが、これは内部的にはDPFを使用していて、DPFはVST2(VeSTige)やLV2をサポートしている一方でVST3はまだ開発中のステータスで、VST3ではビルドできない状態です。そっちを何とかするよりはayumi-lv2をJUCEでビルドしたほうが早いだろうと判断しました。

そういうわけで新しいリポジトリが生えています。

github.com

今回はついでにVSTパラメーター経由でayumiの各種コントロールを調整できるようにして、ついでにソフトウェアエンベロープを実装したので、パラメーター数がだいぶ増えました。エンベロープはADSRではなく制御点の直接指定で、UIは用意しておらず、あくまでパラメーター設定UI(JUCEの汎用部品)で調整する感じです。正確にはrelease部分が未着手です。JUCEのSynthesizerに統合してJUCEべったりにしてしまうかどうかが悩みどころ…

compose-mpp

https://zenn.dev/atsushieno/articles/compose-mpp-library に書いたので詳しくは省きますが、kmmkやaugene-ngのaugene-editor-projectでいつまでもAlertDialogやDropdownMenuが使えないのは不便だったので、JetBrainsがやらないなら自分でやってしまえということで作りました。割とちゃんと調べないと作れないやつだったので、シンプルな実装の割には試行錯誤を重ねていたり…

github.com

MMLのオートメーショントラック対応

今月はほとんどaugene-ngで打ち込み作業をdogfoodingして過ごしていて、その過程でいろいろ旧C#版から存在していた問題にいくつか改善を施すことができました。

今月最初に着手していたのは、MMLからオーディオプラグインのパラメーターを更新する手段の実現です。 tracktion_engine ではオートメーション トラックを使用すると、そのトラックに紐付けられたプラグインのパラメーターの値を変更できるので、MIDIでコントロールチェンジを送るのと同じ感じでMMLに記述できるようにしたい、と考えました。

ただし、MIDIにはオーディオプラグインパラメーターという概念が存在しないので、何かしら別のコンセプトをMIDIメッセージの枠組みに割り当ててやる必要があります。いくつか候補を考えましたが、最終的には無難にMMLからはsysexイベントとしてコンパイルさせて、Midi2TracktionEditConverterでそれを検出してAutomationTrackのパラメーター設定あるいはパラメーター値変更としてXML要素を生成することにしました。

MMLとしてはどんな感じで記述するのかというと、READMEからのコピペになるのですが、こんな感じです:

#macro AUDIO_PLUGIN_USE nameLen:number, ident:string {  __MIDI #F0, #7D, "augene-ng", $nameLen, $ident, #F7 }
#macro AUDIO_PLUGIN_PARAMETER parameterID:number, val:number { \
    __MIDI #F0, #7D, "augene-ng", 0, \
    $parameterID % #80, $parameterID / #80, $val % #80, $val / #80 } 

#macro OPNPLUG { AUDIO_PLUGIN_USE 11, "-1472549978" }
#macro OPNPLUG_MASTER_VOLUME val { AUDIO_PLUGIN_PARAMETER 0, $val }
#macro OPNPLUG_EMULATOR val { AUDIO_PLUGIN_PARAMETER 1, $val }
#macro OPNPLUG_CHIP_COUNT val { AUDIO_PLUGIN_PARAMETER 2, $val }
#macro OPNPLUG_CHIP_TYPE val { AUDIO_PLUGIN_PARAMETER 3, $val }
#macro OPNPLUG__PART_\1__OPERATOR_\1_LEVEL val { AUDIO_PLUGIN_PARAMETER 4, $val }
...

ユーザーはMMLOPNPLUGと書くことで、そのMMLのトラックでこれから出力するオートメーションパラメーターがOPNplug(としてローカルシステム上にプラグインのUIDが-1472549978であるもの)のものであることを指定し、OPNPLUG_MASTER_VOLUME 8191 と記述することで、そのmaster volumeに8191を指定します。こうやって一度MMLマクロとして定義してしまえば、あとはさらに短く記述したMML命令(たとえばOPN_MVとかにしても良いです)を独自に定義できます。

これはローカルシステム上にインストールされているオーディオプラグインのそれぞれについて、ひと通りだけ定義してしまえば良いので、全てのプラグインをスキャンしてこれらのオートメーション用MML~/.config/augene-ngディレクトリ以下に自動生成するツールを作成して、ひと段落としました。

MMLコンパイルからのHot Reload

augene-ngの実用上の最大の問題は、昨年書いたことなのですが、オーディオプラグインがいくつもロードされるような楽曲になってきたときに、augene-playerが楽曲をロードするのに数十秒〜分単位で時間がかかってしまうことでした。augene-ngのコンパイラ…というべきなのかビルダーとでもいうべきなのかはわかりませんが…は*.tracktioneditファイルを生成するので、これをシンプルにtracktion_engine::Editとしてロードしていると、そこでオーディオプラグインの再ロードが行われ、そこで時間を食う…というのが問題の原因です。

このAPIで楽曲をロードしている以上はこの問題は避けられないようなので、楽曲全体のリロードを避けて、差分だけ更新するというのを基本方針として改善を試みました。全体をデプロイしたら時間がかかりすぎるので、必要な差分だけをデプロイする…そう、Instant Run、Fast Deployment、Hot Reload、Apply Changesです…! まだまだ完全ではないのですが、とりあえずHot Reloadのオプションをaugene(-editor)に付けて、Hot Reload有効時にはMidiClipの内容を書き換えるだけ、とすることで、打ち込み作業中のロード時間を大幅に短縮できるようになりました。*.tracktioneditが更新されるだけで自動的にリロードするオプションもあるので、リロードまわりのストレスは大幅に減少したと思います(といっても現状使っているのは自分だけですが)。

audio plugin portaibilityのある音色バンク

もうひとつの大きな問題は、DAWで打ち込んだデータがローカルシステム上のプラグインのセットアップに依存するのが不可避であるというもので、これも昨年書いたやつです。去年は主としてAndroidとの移植性と楽曲データ(augene-ngでいえば.tracktioneditファイル)そのものの移植性の観点からの困難を論じましたが、これは課題として難易度が爆上がりなので、今年はまず異なるデスクトップ環境間での「ソース」移植性の課題として解決してはどうかと考えています。

augene(C#版)開発時は、その末期に、SMFのMETAイベントのひとつInstrument Name(メタイベント04h)を使って、ここにAudioPluginHostのfiltergraphのエイリアスを指定する、という手法を編み出しました。これを実装した当時は、単にプロジェクトファイル上で<AudioGraph>要素のIDを<Track>要素で紐付けるのが面倒だったのと、複数のトラックで1つのプラグイン設定を使いまわしたかったというのが理由です。しかし、いったんmugeneのINSTRUMENTNAME命令(メタイベントを生成するマクロ)でプラグインを間接的に指定するというスタイルは、プラグインエイリアスを与えて、いったん環境依存のファイルパスなどから引き離している、という特徴もあることに気が付きました。

augene(C#版)には<Include>要素のサポートも実装してあって、これを使うと他のプロジェクトファイルをインクルードすることも可能です。このインクルードされるファイルとして、オーディオグラフの集合体を定義しておけば、楽曲ごとに自分のプリセットを定義してまわる必要がなくなります。そして、その中で指定される*.filtergraphの内容が、プラットフォームごとに and/or 環境ごとに調整されていれば、これをインクルードする側は何も変更しなくても移植性のあるプロジェクトになります。またそれらをINSTRUMENTNAMEマクロで参照しているmugene MMLの記述も環境からは完全に独立しています。

MMLによるオートメーションサポートも合わせて、これはもしかして、不完全ではあるかもしれないけど、割とプラットフォーム独立性を担保できたのではないか…!?と思っています。それでaugene-ngのリポジトリにサンプルとして手持ちのオーディオプラグインを使ったfiltergraphファイルから、フリー音源とフリーソフトウェアプラグインのものをいくつか、<Include>できる*.augeneプロジェクトとして放り込んでみました。実際にはvst3プラグイン*.sf2/*.sfzといったファイルを/opt/augene/みたいなパスに突っ込まないといけなそうですが(~/.vst3などを指定していると結局その/home/atsushiみたいなディレクトリにマッピングされている気がします)、このアプローチで誰でも使い回せるようになれば悪く無さそうです(Windows/opt/augeneを作るのは無理そうなので、別の定義ファイルが必要になりそうですが)。

この観点ではstudiorackプロジェクトがいくつかクロスプラットフォームでのオーディオプラグインセットアップを自動化する試みを開発しているようなので、もしかしたらそのうちこの辺を使い回すかたちでより具体化した実装が進められるようになるかもしれません(未検討)。

SF2やSFZのファイルからも、これらの*.filtergraphファイルを自動生成できると良いのですが、どうしてもVST3フォーマットのstateバイナリの生成が必要になってしまうので、それなりに込み入ったハックをしないと実現しなさそうです。

going forward

augene-ngには他にも自分が便利に使いたいために施した改善がいくつかあるのですが、総じて技術的可能性と実用性の検証のために行っているという感じです。以前よりはだいぶ地に足が付いた感じになってきて(いやまだまだふわっとしているのですが)、もうしばらくこの路線で遊んでみてもいいだろうという気持ちになっています。フツーこんな他に誰も使わなそうなPoCを続けることって無いと思うのですが、似たような感じで10年くらいずっと1人で続けていたっぽいBespokeSynthが(初見はたぶんtheaudioprogrammer meetupだったと思うのですが)ついに完成して各所で話題になっているのとかを見ていても、何か励みになりますよね。まあこちらはまだMIDI2の活用事例にもなりそうですし(まだMIDI1なのですが、今やっている打ち込み作業で16チャンネルを使い切る前に対応しないといけなくなりそうな気がする)、もうひと月くらいはAndroidのほうも放置してこっちに注力しておこうと思っています。

8月の開発記録 (2021)

8月は主にKotlinに移植してきたMIDIツール・ライブラリの機能を回復・強化する1ヶ月になりました。他の開発作業(Androidオーディオプラグインとか)はあんましやっていません。「回復」というのは.NET時代にmanaged-midiで作ってきて打ち込みで使ってきたものと同等以上の機能を埋める (filling the gap) 的な意味です。

技術的には無駄にKotlin MPPとKotlin Nativeの知見が貯まった気がします。先月はKotlin/JSの知見が貯まったし、音楽アプリ開発者志向のはずなのに無駄にKotlin開発者になりつつある気がする…

kmmk 0.2.2: Fully featured virtual MIDI 1.0/2.0 keyboard application using Kotlin and Compose for Desktop and Android

先月にも言及しましたが、7月末にJetpack Compose 1.0 stableが出たこともあって、8月のうちにはCompose for Desktopもそれなりに安定的に手を出せる状態になるだろうと思っていましたが、本家のリリース後ほどなくcompose-1.0-rc1-alpha1というアルファなのかRCなのかよくわからんバージョンが出て、とりあえず1.0ベースのパッケージ体系が早々に確立されていました。

そういうわけで、半年くらい前にざっくりCompose for Desktopで実験的に実装していた(まあまだ実験的なUIの色が強い)仮想MIDIキーボードkmmkですが、今月はだいぶ機能強化しました。もうxmmkを使う必要はなさそうです。

Compose for DesktopのGradleタスクもいろいろ強化されていて、./gradlew packageなどと実行するとrpm/deb/msi/dmgなどがビルドできるようになっています(こんな感じでbuild.gradle(.kts)に記述します)。デスクトップのライフサイクルなども考慮したDecomposeも安心して使えるようになりました*1

kmmk

アプリの機能面では、「とりあえず音が出せる」レベルだったものに、キーボード配列の調整機能(US ASCIIとかJIS 106とか)を選択できたり、音色(プログラムチェンジ)を選択できたり、あと作曲時には有用なクロマチック配列モードを選択できたりするようになりました。クロマチックモードは特にキーボードを適当に打っていても普段ピアノ配列のキーボードでは出ないようなコードがカジュアルに出てくるのと、指をそのまま移動するだけで調性を維持できるのが便利だったりします。

それからMML Padへのレコーディングモードをxmmk(C#版)から復元して、ついでに今回はテンポに合わせて音長を記録するモードも実装しました。すでにアルペジオ奏法を含む和音の入力をサポートしているところに追加したので、それなりに動作の調整を必要としましたが、概ね想定通りの動作になっています(まだ実際の打ち込みでは使っていませんが)。

UIに関しては、C#時代にはまともな複数行テキストボックスを入力できるコンポーネントが無かった(&& xwt/gtk3がまともに動作する状態にならなかった)ので、SwingベースのCompose for Desktopに移行するだけで基本機能が大改善しています。一方でCompose for Desktopは未だにJetpack Composeの標準コンポーネントにあるDropDownListが使えなかったりする状態なので*2、まだ真面目にアプリUIを作り込もうと思ったら避けたほうが良いでしょう。

キー配列のグリッドはクリック/タップ用に残しているのですが*3、見づらいのでキーボード表示を別途追加するかもしれません。ドラムパートの編集も可能になるように10ch.スイッチも追加してあります。Android Phone上でMMLテキストが扱いやすいとは考えにくいので、Androidでの用途は主にMIDI音源の動作確認になりそうです。

augene: ntracktive -> kotracktive -> missing-dot: a migration helper library from .NET to Kotlin MPP

managed-midiで作り上げてきたプロジェクトのひとつにmugene MMLコンパイラをtracktion engineと組み合わせてオーディオプラグインを使った楽曲を直接打ち込めるようにするaugeneというものがありました。今年の春先にktmidiプロジェクトを立ち上げたときに「ここまでは実装しておきたい」と思っていたプロジェクトで、4月まででは進められなかったものです。

これは原理的にはSMFからtracktion engineの.tracktioneditという拡張子のXMLを生成しているだけなので*4、Kotlinにも簡単に移植できるだろう…と思っていたのですが、これが意外にも大変でした。

C#版ではntracktiveというライブラリとして.tracktioneditファイル生成部分を実装していたのですが、データモデルはXMLのレコード構造に対応するシンプルなクラス、読み書きはそのクラスからリフレクションでメンバーを取得してXmlReader / XmlWriterで機械的に実現、というオレオレシリアライザもどきの実装でした。まずはこのライブラリを"kotracktive"としてKotlin MPPに移植しようと考えました。

これをKotlinで実現するには、.NETの2つのカテゴリのAPIが必要になります:

  • System.Xml(.ReaderWriter)
  • System.Reflection

これらを実現する手段が、実はKotlin MPPには全くありません(!!)

XML自体が技術的トレンドではないということはありますが、KotlinでXMLを使う場面では、いつもKotlin/JVMに基づいてXmlPullやらSAXやらDOMやらが使われています。シリアライザまで概念を高位にすると、MPPでもkotlinx.serializationでXMLを読み書きするという手段が登場するのですが、そのXMLフォーマットサポートとして使えるとされるpdvrieze/xmlutilではKotlin Nativeがサポートされていません。バックエンドもプラットフォーム側のネイティブ実装に無理やり合わせているフインキがそこはかとなくあって、ネイティブ密着型クロスプラットフォームUIツールキットを使うときのような不安感があります。

どうしたものかしばらく考えましたが、悩んでいる暇があったらXmlTextReaderとXmlTextWriterに相当する機能くらい、ざっくり実装してしまおうと思い立って、合わせて2日くらいで適当にやっつけました。その後この辺を抜き出して独立したライブラリとしてmissing-dotというパッケージで公開しています。MPPでjitpackから拾える親切設計…! *5

github.com

XmlTextReaderはちょっと面倒なところがありますが、それでもDTDと実態参照をサポート対象外にして、CDATA Sectionの]]>の対処を先送りしてしまえば、面倒はかなりなくなります。XmlTextWriterも、とりあえずWhitespaceノードやらインデント対応やらが未サポートですが、.NETの実装とは異なりXSLT 1.0のややこしい要求事項を先送りしたので、とりあえず無難に使えます。しかもnamespacesプロパティ対応も付けてあるので、JUCEで生成されたNamespaces in XML違反のXMLすら読み書きできるヤツになっています。XmlNamespaceManagerは実装し、XmlNameTableは実装しない、くらいのバランス感覚です。

ホントは最初XmlPullを適当にJVMから移植しようと思っていたのですが、リフレクション依存がしんどそうだったのでやめました。この話は実は次の話と続いています…

System.Reflection APIの物真似をkspで実現する

XMLの読み書きは実現できましたが、Reflectionのほうは大変です。まずリフレクションの基礎となる実行時型情報がKotlin MPPで使えるkotlin.reflectにはありません。実行時に操作できるオブジェクト (Any) からはクラス名が取得できる程度です*6。クラスのメンバーを取得できるKDeclarationContainer.membersなどはJVM onlyです。System.Type.GetType(String name)に相当する機能を実現するには、ビルド時に生成できるコードだけで型情報データベースを作っておく必要があります。

Kotlinの世界では、こういう時はリフレクションではなく補助的なコード生成によって、メタデータをもとにコードを生成してリフレクションの代わりに用いるのが一般的であるようです。それならばSystem.ReflectionAPIのような「ランタイム」を作って、それらを「実装」するコードを動的に生成すれば出来そうだ…と考えて、今年Kotlin compiler pluginとして登場したksp実装しました。KSP...Kontakt Script ProcessorではないKSP…(ややこしい)。生成されるコードはたとえばこんな感じです。

ランタイム的なMetaTypeTypeJVMとかでややこしくなるので改名)やらTypeCodeやらPropertyInfoやらの実装はこんな感じです。これは先のmissing-dotに移動すべきか迷って結局まだ入れていません。

kspそのものは難しいことはあまりないのですが、kspのモジュールをプロジェクトに組み込む過程で、プロジェクトの構成そのものをkspとksp適用モジュールだけのKotlin MPPプロジェクトだけに絞り込んで再構成する必要が生じてしまいました。Kotlin MPPのモジュールは、特に制約が無ければ1つのプロジェクトに複数組み込むことも出来なくはないのですが、やはりプロジェクト全体を見に行って無関係なモジュールにも影響を与えるようなプラグインなどが関わってくると、想定外のビルドエラーを起こすようになったりします。

この種のビルドエラーの一部は、他のライブラリやプラグインを追加してもよく起こります。同一の入力に基づくビルドは常に同一の結果を返さなければならないのですが、これらの一部は明らかにビルドキャッシュ管理の問題をかかえており、Gradleなりプラグインなりのバグでしかありません。ただこの原因を特定するのは難易度が高く生産性が低い(追及コストに見合わない)のが問題ですね。

MPPのモジュールが想定外のビルドエラーを起こすようになったら、だいたい次のいずれかを試しています(Android Studioがまともに動作しなくなった時とほぼ同じ)。

  • ./gradlew clean build (IDEAなら"Rebuild Project")
  • rm -rf (projectroot)/.idea
  • IDEAを終了してkillall -9 java

クリーンな状態でビルドできなければプロジェクトの構成を単純化して(kspなどを使う場合は対象モジュール+kspのモジュールのみにする、など)、プロジェクトの参照はmaven-publishプラグインpublishToMavenLocal~/.m2/repository/以下にパッケージを発行して、利用側のプロジェクトのbuild.gradle(.kts)mavenLocal()repositories { ... }に追加して対処します。

原因によっては、特定のプラットフォームを対象外としなければならなくなるかもしれません。今回のkotracktiveの場合は、kspを使っているとKotlin Nativeでのビルドに失敗するという問題があって、nativeビルドは無効にしています(後述の理由で割とダメージがでかい…)。JS Legacyは通るけどJS IRは通らない、みたいなのも典型的なトラブルです。

それから、この次に説明するaugene-ngリポジトリがそうなのですが、1つのリポジトリで分割したプロジェクトをGitHub Actionsなどでビルドする場合にも、このpublishToMavenLocalでいったんビルドサーバ上のローカルmavenリポジトリから参照解決できるように、ビルドスクリプトを仕込む必要があります(こんな感じです)。

…とまあ余談(?)はおいといて、System.Reflection依存のコードはこんな感じで静的コード生成で対処しました。これがreified genericsにがっつり依存してたら死んでた…(実はちょいちょい依存していたのですがロジックを書き換えて対処しました)。

Kotlin MPPで実行時型情報に基づくリフレクションを実装するのは難しいのか(!?)ということも考えたのですが、たぶんボトルネックはJS LegacyとNativeにあって(JS IRは何となく実現できそうな気がする)、NativeはIRがあるという意味では多分難易度が高くないけどバイナリ互換性がしんどそう…みたいなことを考えて深く追及しないことにしました。(そこまでいったらJetBrainsで給料払ってもらって実装するレベル…)

augene-ng: MML + MIDI + Tracktion Engine XML manipulation tool

↑のkotracktiveですっかり横道に逸れてしまった感がありますが、本来の目的はMMLからtracktioneditファイルを生成するaugeneの移植でした。kotracktiveも含めて、2週間くらい前から始めていたようです。

github.com

.NET版は実のところ4つのパートで成り立っていました。

  • ntracktive - tracktioneditのオブジェクトモデル
  • midi2tracktionedit - SMFからtracktioneditのモデルに変換するコンバーター
  • augene - midi2tracktioneditに、さらにJUCE AudioPluginHostのfiltergraphからの変換もサポートした楽曲データ生成。GUIxwt
  • augene-player - tracktion_engineを使った音楽プレイヤー

midi2tracktioneditはktmidiとkotracktiveの組み合わせなので単純でしたが、augeneアプリはxwtだったので移植性はなく、またローカルファイルに著しく依存していたり、AudioPluginHostやaugene-playerをProcess.Start()で呼び出したりと、Kotlin MPPでやるのは無理ゲー感が強かったので、まずJVM前提でCompose for Desktopに移植しました。kmmkのところでも書きましたが、Compose for Desktopの機能自体が割と未完成でしんどいところがありますが、最低限ファイルをロードして加工して保存、コンパイル、プレイヤーの起動、くらいの最低限の機能だけは動いている状態です(実用的とはとてもいえない)。

ちなみにAudioPluginHostのfiltergraphはXLinqで読み書き、augeneのプロジェクトファイル自体はXmlSerializerで読み書き…という、拷問かな?という感じの.NET API依存ぶりで無駄にしんどかったのですが、XLinqは自前で実装し、XmlSerializer依存はXmlReader/XmlWriterを使って単純に書き換えました。このXLinq実装も今はmissing-dotに移動しています。

その後、augeneの曲データモデルはfiltergraphのオブジェクトモデルと合わせてGUIから切り離して、もしKotlin Nativeで使えるようになれば(上記kspの問題でまだできないわけですが)、tracktion_engineを使ったaugene-playerに統合してin place editorにも使えるんじゃないかと考えました。1年前にも書きましたが、tracktioneditを毎回プレイヤーでロードする仕組みにしていると、オーディオプラグインの全ロードが頻繁に発生して、とてもじゃないけど編曲できたものではありません。プレイヤー上で差分更新する機能は実用化の上で避けては通れないところです。

Kotlin MPPに移行するにあたって問題になったのはだいたいこんな感じです:

  • System.IO.Fileまわり - okioを使って実装。ファイルの読み書きとパス加工が主な要求事項です。
  • IsolatedStorage - ~/.local 以下にディレクトリを掘る方向で自前実装して対処しました。
  • FileSystemWatcher - APIだけでっち上げて現状何もしていません(UIに変更を反映できなくて不便…)

一方でGUI側に残した機能もいくつかあります。Process.Start()は、Androidなどでどう実現したらいいのかわからないので、エディタに移植性は無いと判断しました(Android上ではファイルの読み書きも使い勝手が全然違うので、実質的にAndroidでの使い道は無いです)。

ktmidiとmugene-consoleのネイティブビルド

mugene-consoleはmugeneコンパイラコマンドライン上で利用する唯一の方法でしたが、これはJVMプロジェクトだったので、java -jar ...で起動するしかありませんでした。これは元々MPPのNativeビルドでコンソールツールをビルドする方法を知らなかったということが大きいのですが、build.gradle.ktsの記述方法をいろいろ調べた結果、linuxX64() { ... }などにbinaries.executable()と記述しておけば、拡張子が.kexeとなるnative executableをビルドできることがわかったので、これでビルドすることにしました(Windowsでは.exeとなるようですが未確認)。

これで、Kotlin/JSに基づくvscodium extensionだけでなくコンソール上でもKotlin/Nativeに基づくmugene-ngを実行できるようになったのですが、実際には実行可能ファイルと同じディレクトリに格納しているデフォルトマクロMMLを解決する必要があり、そのためには実行可能ファイルの位置を特定できなければならず、これが意外とネイティブコードではしんどい(!?)ということがわかったのがある意味面白かったです。どれくらいしんどいかというと、全プラットフォームで別々の実装が必要になってそれだけを実装するC++ライブラリが割と人気があるくらいです。

さいわいこの辺は何とかなって、現在はmugene-consoleはコンソールツールとして問題なく使えるようになっています。

また、これと関連して、ktmidiもC/C++などから参照して利用できるようにしたいと思って、build.gradle(.kts)で記述する方法を探して、binaries.staticLib() binaries.sharedLib()などを追加して、libktmidi.solibktmidi_api.hといったファイルが生成できるようになりました。Cヘッダファイルの内容など、なかなか趣があります。

その他

8月にもandroid-audio-plugin-frameworkのMIDIサポートを少しいじっていた時期があるのですが、これは主にkmmkからMIDI 1.0のメッセージをMidiDeviceService実装に送り込んだ時に、MIDI-CIのSet New Protocolが送られない限りはMIDI1のままでプラグインに送られるように調整したくらいです。やはりMIDI2が前提だと数多のJUCEプラグインを全部調整してビルドし直さなければならなくなるので、MIDI1のままで済むと楽になるのでした。

AAPは最近もAndroid DAWアプリの有名どころの人に「いつ使えるようになるんだ?」とか訊かれて「とりあえずMIDIバイスとして使えるようにしたい…」とか濁しているところですが、しばらくは↑のプロジェクトがメインになりそうなのでまだまだ先なんだろうな、という感じです。最近また他所のプロジェクトの仕事は手伝わなくなっているので、次のM3に向けて何かしらやっていこうと思います。MMLでまた打ち込みできる環境を何とかしたい…

*1:まだstateを使っている程度でほとんどDecomposeらしさの活用はしていませんが

*2:すでにcompose-jbリポジトリにissueがあって放置されている状態です

*3:特にAndroid Phoneではキーボードを使うとは考えにくく、タップがメインになるでしょう

*4:tracktion_engineのJUCEコードにも同様の機能があるはずなのですが、変拍子だらけの楽曲をまともにインポートできない問題があるので、自前で変換しています

*5:Maven Centralはめんどくさいのでやってません

*6:これは多分技術的には正確な記述ではなくて、たぶん型名はそもそもコンパイル時にしか取得できていない

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のほうはとてもよかった