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のほうも放置してこっちに注力しておこうと思っています。