コンテンツへスキップ
ものがたり
戻る

1月の開発記録 (2026)

昨年12月に引き続き、1月もintensive uapmd hack monthでした。今週はだいぶ稼働していないので、成果の大半は最初の3週間半くらいで出てきたものです。

他ではKotlinのライブラリのパッケージメンテナンスをちょっと進めたくらいです。uapmdでMIDI 2.0音源が実用的に使えるようになってきたので、特に(現状Tracktion Engine前提の)augene-ngをMIDI 2.0中心にシフトして進めようとも思ったのですが、やはり単一のUMPデバイスだけではまだまだ楽曲の打ち込みができるとは言い難いので、もう少しuapmdを先に進める必要があります。

uapmd/midicciの進展

uapmdとmidicciは先月末の時点でほぼパッケージングまで完了していましたが、年頭に正式にv0.1をリリースしました。これに合わせてKVR product pageも作成・公開しています。

www.kvraudio.com

uapmdもmidicciも、ここ1ヶ月でいろいろ大きく進展しています。最初にどんな変更が加えられたかを列挙して、後から各論的に細かい部分を詳説しています。時間が無くて書ききれないので、細かい論点は別途切り出して覚書として別投稿するかもしれません。

uapmd improvements

midicci improvements

今月はmidicciにもさまざまな改善が施されました:

embedded JSスクリプティングのサポート

v0.1.0リリースを公開してからすぐに取り掛かったのがJSサポートのやり直しです。先月少し言及したのですが、先月までのJavaScriptサポートはuapmd APIの公開インターフェース全体にJS shimを自動生成して、JSコードだけでinstrumentationが可能になることを目指していました。これだとuapmdを起動して、GUIコンテキストを用意して、プラグインをスキャンしてリストを構築・管理して…といった作業をJSでやることになり(あるいは「できるようにする」ことになり)、使い勝手が悪く、実用性が無かったといえます。しかもJSバインディングはnodeを前提に作られていて、Webブラウザ上で動かしてWebCLAPみたいなものもサポートしたいとなった場合には使えません。そういうわけでこの方向性は無かったことにしました。

代わりに導入されたのは、一般的なDAWに見られるスクリプティングランタイムの仕組みの導入です。これはchocのQuickJSサポートを組み込んで、chocのJS関数登録の仕組みを活用して実現しています。APIは目下のところ「AIにuapmdのAppModelクラスに含まれていた関数に対応するものを全部作る」というレベルで自動生成させたものだけですが、これで「JS変数にちょっとリストアップしたプラグインのインスタンスを生成してプラグインUIを表示する」というスクリプトを自動生成させて、それが現状うまく動作しているので、この段階で満足しています。もう少しやる気が出たら、起動直後に自動実行するスクリプトをuapmd-appにコマンドライン引数として渡すとuapmd-appの初期化が完了したら自動実行する、みたいな機能を作れば、いろいろできることが増えるかもしれません。

組み込みスクリプティングのAPIがあればMCPサーバーの追加も難しくはないと思いますが、現状スクリプティングAPIがあれば、それをAIエージェントに食わせるだけで十分いろいろできるはずなので、どちらかといえば外部に接続できてembedded JSランタイムと通信できるWebSocketsなどのproxyサーバーを組み込むほうが有用そうです。最近の話題でいえば、iPlug3が(まだ机上の存在ですが)プラグインがMCPサーバーとして動作するものとして設計されているようですが、その動作環境において通信経路を自由に作れるとは限らないプラグインSDKとは異なり、uapmdは普通にライブラリとして利用できるので、一見して使い方が分かるようなAPIが公開されていれば、あとはAIのほうがよろしく解釈してくれるでしょう。

Linux GUIサポートの改善

uapmdの大きな特長のひとつは、uapmd自体はコーディングAIエージェントにプラグインフォーマットのAPIのインターフェースをガンガン実装させて先に進める、というアプローチを全面的に採用していることです。iPlug3が「われわれはAI時代を念頭に置いてフルスクラッチで設計している」みたいなことを宣言していますが、uapmdは(まあまあ抵抗感がありましたが)とっくに採用しています。

ただその弊害で、AI任せであんまし良くない設計のまま放置していた(いる)のがGUIサポート、特にVST3のIRunLoopIWaylandHostIWaylandFrameといった部分でした。ClaudeやCodexはAPIの意図を解釈して実装する分には問題ない仕事をするのですが、uapmdがイベントループのAPI(remidy::EventLoop)をどのように使うかは明示的な仕様が無いので、IWaylandHostを実装するよう指示した時にLinuxEventLoopなるクラスを独自に作成してその中でのみIWaylandHostをサポートする、みたいなことをやっており、自分も深く吟味せずに取り込む、みたいな状態になっていました。

EventLoopはremidy「が」実装を規定すべきものではなく、プラグインホストが自身のGUIフレームワークに合わせて提供すべきものとして作られていて、実際uapmd-appはImGuiEventLoopで動作しているので、(その中ではIWaylandHostはサポートされておらず)、Waylandサポートが利用される場面は全く存在しなかったといえます。

Waylandサポートの問題はもう1つあって、プラグインのほとんどはWaylandをサポートしない(IPlugView::isPlatformTypeSupported(kPlatformTypeWaylandSurfaceID)がfalseを返す)ので、ホストのデスクトップ セッションがWaylandでも(XDG_SESSION_TYPE=waylandでも)引き続きX11のプラグインUIもサポートしないといけない、ということです。uapmd-appは「現在のデスクトップに合った1つだけのplatform type」のみをチェックしていたので、前述のEventLoopの問題を修正した直後は、(Waylandセッションで動作している場合はWayland UIとして動作するかどうかをチェックし、当然のようにほとんどのプラグインがそのチェックを通らずにUI生成に失敗するため)UI生成が何一つ成功しないという問題に陥っていました。

現在ではX11が通ればX11でUIを生成することにしていますが、これはもしかしたらWaylandオンリーの環境では(特にWaybackなども組み込んでいないため)失敗するかもしれません。とりあえず現時点ではuapmd-appは一般的なLinuxデスクトップのみをターゲットにしていて、あまり現実的な懸念ではないと思って放置しています。Waylandオンリーの環境でも動作するオーディオプラグインが有意に出現してきたら考えます。

AUv3サポートのネイティブ実装

AUv3プラグインはAUv2のホスティングAPIを利用してインスタンス生成が可能です。ただ、そのホストが利用できる機能は、当然ながらAUv2が前提とするものに限られます。今月になってノート別コントローラーの実装を作り込もうとして気付いたのですが、AUv2はAudioUnitGetPropertyというAPIの上ではノート別パラメーターを取得できるようになっているものの、実際にこれでパラメーターを取得できるように作られたプラグインは存在せず、AUv3になってから初めてAUParameterTreeのAPIによって使えるようになった機能という位置付けになっているようです。

こうなったらAUv2のAPIではなくAUv3のAPIを使用してAUホストを書き直すしかない…と思ってひと通り実装しました。 これまでは「AUv2もAUv3もAUv2のAPIからアクセスできるからそっちで統一的にやる」というスタンスでやっていましたし、最終的にはAUv2実装が不要になるはずだからAUv3実装に統一するか…と考えていたのですが、今度はper-note controllerなどがAUv3のAUParameterTree経由でないと取得できず、一方でAUv3 APIだけで実装するとAUv2のパラメーター変更が取得できないというややこしい問題に引っかかり(このcommitだけでも確か結局は解決せず)、最終的に2つの実装が併存しています。

Appleがこの辺をきちんとAUv2Bridgeで実装してくれていればAUv3 APIだけで問題なく統一できていたはずなのですが、現実はそうはなってはいなかった、ということで、自分もいろいろ原稿(とは)を書き換えないといけないな…と思っているところです。

Function Block管理機能の再編成

uapmdはここまで、SequenceProcessorがリストを保持するAudioPluginTrackが中でAudioPluginGraphをもち、そのノードであるAudioPluginNodeが保持するプラグインインスタンスのそれぞれについて、UMP Function Blockとしてプラットフォーム仮想MIDIデバイスを(libremidiを使って)構築する、という構成になっていました。

UMPメッセージは、uapmd-appのAppModelのAPIでは個別のプラグイン インスタンスに対して送られるようになっていた一方で、シーケンサーのトラック管理が進化して1つのトラックに複数のプラグインを直線で繋げるようになった時点で、UMP入力キューはトラックごとに編成されることになりました。プラグインへのUMPイベントの受け渡しは、最初は先頭ノードがUMPを受け取って処理し、次のノードは前のノードの出力を受け取って処理するような設計になっていました。これは失敗でした。MIDIメッセージの何がそのノードで処理され、何が次のノードに「受け継がれる」べきかはわかりません。MIDIイベントストリームの連結はユーザーのホスト操作次第であるべきで、この仕組みでは後続のエフェクトパラメーターすら操作できません。NRPNの番号が衝突するので、最初のUMP入力をそのまま次のプラグインにも渡すというわけにもいきません。

この問題を解決するために、各プラグイン インスタンスには途中からFunction Block ≒ groupがアサインされるようになり、uapmd-appはプラグインのインスタンスIDを指定してnoteOn()などの命令を呼び出すのに、途中でgroupがアサインされ、それを受け取ったAudioPluginTrackはそのgroupに対応するインスタンスに渡す…といったややこしい実装になっていました。

ややこしいのはともかく、根本的な問題として、AudioPluginTrackにUMP Function Blockを割り当てるという構図がある限り、UAPMDはほぼフル機能のDAWシーケンサーエンジンを実装しなければならないことになってしまいます。それはやりたくないので、Function Blockの管理をシーケンサーエンジンから切り離せるようにしました。この部分は設計が詰められていなかったので、APIから作り直しています。

過去にUapmdMidiDeviceという名前で存在していた仮想MIDIデバイスの実体は、その実態に合わせてUapmdFunctionBlockとなり、これらを複数まとめたものが新たにUapmdFunctionDeviceという名前になりました。プラットフォーム上のUMPエンドポイントはUapmdFunctionBlockごとに作成されます。uapmd-engineのAudioPluginTrackは1つのUapmdFunctionDeviceを持つことになるでしょう(プラグインが9件以上刺さって全てが有効となる事態は、現状では想定していません。Kontaktのmulti-outなんかも8つまででしたし)。

シーケンサーを導入するためのモジュールの切り分け

1月になってから、embedded JSランタイムを追加して、またオーディオトラックとMIDI2クリップをカバーするシーケンスエディター機能(トラック編成・編集機能)を構築しようとしたのですが、これらがどんどん複雑なタスクとなっていき、特にuapmd-appAppModelAudioPluginSequencerという2つのクラスが巨大な神クラスと化してきて、機能拡張を進める前にいろいろコードとモジュールの切り分けをしっかりしておくべきだと思うようになりました。特に以下の2点が大きな課題でした。

uapmdのリポジトリには、現状作りかけで開発が進んでいないuapmd-applyという小型ツールがあって、これは基本的にはプラグイン名とオーディオファイルやSMFファイルを渡すと、それを時系列に沿ってプラグインにそれらを入力として適用して、オフライン レンダリングでオーディオファイルを生成する、というものを意図しています。これを実現するにはembedded JSエンジンのようなものをheadlessで動かす必要があるので、たまにuapmdに破壊的変更を加えるとこれがビルドできなくなるので、シーケンサーAPIの切り離しと再編成においてsanity checkの役割を果たしています。

プラグインホスティング、Function Block(仮想デバイス)管理の機能をシーケンサーエンジンから切り離すというのは、uapmdというモジュールをremidyやuapmd-app固有のものではない、再利用可能なライブラリとして位置付け、uapmd-appが最終的にDAWに匹敵する機能を自ら作り込まなくても何とかなるようにする、という目的が込められています。1月末の時点でどのような切り分けが実現しているかをまとめると、以下のようになるでしょう。

AudioPluginInstanceAPIの抽象化はまだまだ不完全で、たとえばPluginParameterSupportのインスタンスがそのまま返されるような設計になっています。これではまだremidyから切り離されているとは言い難いところです。ただ現状remidy以外の実装たとえばjuce_audio_plugin_clientを組み込む優先度は高くないです。)

切り離しの一番大きな部分は、シーケンサーエンジンが前提とする「トラック」が、Function Blockを編成する主体ではなくなり、またトラックのオーディオグラフのノードがプラグイン インスタンスを保持(管理)する存在でもなくなった、という部分です。具体的には…

v0.2と来月の見通し

先月末にはシーケンサー機能を実装してAndroidサポートも追加して…みたいなことを考えていて、実際に年初にはv0.2 milestonesとしても列挙していたのですが、今月はだいぶ方向性の違う作業やシーケンサーのための下回り作業に追われていたこともあって、このままv0.3 milestones扱いにしてv0.2はそろそろ公開してもいいかな…と思っています。まあこの辺のナンバリングは単なるフィーリングですが。

2月も引き続き個人開発にフルコミットできそうな感じなので、シーケンサーを安心して作り込める体制をコード上で構築していくことになると思います。あまりDAWのフルエンジンを作り込むような作業ではなく、あくまでDAWのオーディオエンジンで使えるプラグインホスティング機能を実装するものとして進めていきたいところです。


この記事を共有:

前の記事
2月の開発記録 (2026)
次の記事
12月の開発記録 (2025)