10月の開発記録(2022)

M3 2022秋の出展

10/30のM3 2022秋 (50回目だったらしい)に「オーディオプラグイン研究所」として出展参加しました。技術書典13で発行した「CLAPオーディオプラグイン開発ガイド」に加筆修正を加えた「正式版」の印刷版・電子版と、「Linux DTMガイドブック」の電子版が新刊ということになります。前者はboothで取扱開始しました。技術書典のときに「紙版がほしかった」という方はこちらからどうぞ。本当は無償配布にしたいところですが、印刷代だけでも「売れば売るほど赤字になる」設定なので、ひとつよしなに…

xamaritans.booth.pm

Linux DTMガイドブックも同様に「完成版」を発行したかったのですが(たとえば、サンプラーなど「器」だけ言及していて楽器サンプルへの言及がゼロなのを何とかしたい)、時間的に厳しくあきらめました。M3では新刊の頒布に合わせてAndroidプラグインの展示も行う計画で開発を進めていて、そこまで手が回らなかったためです。(Androidプラグインの展示に合わせて最新版を安定させる計画は間に合わず、少し古いバージョンにしたのですが…!)

このLinux DTM本と既刊の「MIDI 2.0エコシステム構築術」「MML to MIDI 2.0 to DAW」も含め、展示では幻の「印刷版」を見本として用意しましたが、これらの印刷版はありません。デジタル版でお求めください。(この2冊は割と「自由研究をまとめてみた」性格の本で、現状ではそれなりに時代とともに価値が薄れていく過渡的な内容だと思っています。)

DroidKaigi2022(遊びに行っただけ)

10月上旬にはDroidKaigi2022もありましたね。今回はトーク応募すらしなかったので、単純に東京ドームシティまで遊びに行きました。開催地が遠くなってしまったこともあって、宗教上の理由で午後からの参加のみでしたが、いつものDroidKaigiのふいんきがだいぶ戻ってきていて(ランチとかコーヒーとか夜のパーティが戻ってくるのはまだ無理そう)、開催してくれてありがたい〜という感じでした。年単位で会っていなかった知人のみなさんとも久々に顔合わせできたし、新しく開発者のみなさんとも知り合いになれました。

Gradle Managed Virtual Devicesで変化するエミュレータ活用術」の外山さんに「GitHub ActionsのLinuxホストで使えるようになってほしい(KVMのnested virtualizationをサポートしてほしい)んだよ〜」とか詮無きことを言ったり、「Jetpack Composeを用いて、Canvasを直接触るようなコンポーネントを作成する方法」を見て自分がComposeで作っていたWaveform描画コードを直したり、「Android "を" ビルドしてAndroid Systemを覗いてみよう]」みたいなそこそこ低レイヤーのセッションが聞けるのうれしい〜とか言いながらAOSPアプリのビルドに手を加えるのを眺めたり、「Considerate App Update Delivery」で相変わらず多種多様な他人のアプリをビルドしないといけない仕事やべえ知見がたまりそう〜とか思いながら眺めたり、「人の声を可視化する」でオーディオAPIの話したい〜と思ってask the speakersでお話ししてみたらここの読者の方だと判明したり(そんなことあるんだ…!ってなりました)、「Android アプリの内と外をつなぐ UI」を見ながら自分のアプリのonBackPressed()onBackPressedDispatcherで書き換えたり、あと他の人と雑談していて気がついたらセッションを見逃したり()みたいないつもの体験を楽しみました。

全部かはわからないけどYouTubeセッション動画も公開されているようなので、裏番組とかで見られなかったやつを少しずつ追っかけようと思います(昨日まではさすがにそんな余裕なかった)。

ktmidi 0.4.0

今月は先月から続いていたktmidiの実装を直す作業がひと段落して、新しくバージョン0.4.0を公開しました。 Kotlin Native環境用にRtMidiのcinteropバインディングを作って、RtMidiNativeAccessという実装を追加して(JNAを使ったKotlin/JVMバインディングもあることを考えるとだいぶ二度手間感…)、Midi2Playerをネイティブでも意味あるものにしたという変更が大きいですが、UMPベースの楽曲データのフォーマットやAPIに破壊的変更を加えたからバージョンを0.1上げたというのが建前上の理由です。

楽曲データの問題はlong-standing issueで、元々トラックヘッダーにUMPの件数を指定していたのですが、これでは読み飛ばしができないのでバイト数に変更したものです。楽曲のMETAイベント…に対応するSysex8データ…も間違っていて、テンポ指定が適切に反映できなかった問題があり、今回新たに追加したUMP to MIDI1 translatorの実装と合わせて、ようやくMIDI 1.0と同等のMidi2Playerの動作がreal-worldなやつとして確認できました。mugene-ngもこれを反映しているので、MMLからMIDI 2.0楽曲を引き続き生成できます。

この過程でAPIドキュメントジェネレーターのdokkaがビルドエラーを引き起こす問題にしばらく悩まされて、結局Kotlin Slackで開発者にいろいろ助けてもらって何とかしました。自分の中でdokkaの評価がだいぶうなぎのぼりに上がりました(!?)

AAP: プロトコル変更"V2"の再設計

8月にオーディオプラグインのパラメーター変更にMIDI 2.0 UMPを使ってプロトコルを再設計する話を書いたのですが、その手助けとなるはずだった上記のMidi2Playerをプラグイン試用のアプリに組み込もうとしたものの、ネイティブコード側の実装には使えず、デバッグに使うにも微妙なので、そっちの路線は一旦留め置いて、まずmidi2プロトコルで従来どおりの固定のメッセージを処理できるPoCを(今度はUMPで)作って、それをaap-lv2やaap-juceに適用していこうと考えました。

それで、最初は従来の「midi1ポートもmidi2ポートもサポートする」方向で実装を練っていたのですが、早晩「これは開発体験が悪い」と気づきました。これまではむしろ「MIDI2ポートはいらない。UMPをサポートしたければ、MIDI1ポートでMIDI-CIのSet New Protocolを送って切り替えればよい」と考えていたのですが、プラグイン開発者が「どっちで来るかわからないから両方サポートする」ことになるのは理不尽ですし、「MIDI2を処理できるプラグインは現状ほぼ無い」にしても、「パラメーター変更はMIDI2でしか受け付けられない」、「パラメーター変更をサポートしないプラグインはほぼ無い」、「MIDI2をMIDI1に(可能な範囲で)変換するのは難しくない」…という状況を鑑みて、むしろ「MIDI2ポートのみ存在すれば良い」という考えに至りました(ここまでで何日かかかった)。

これは相互運用性の観点ではドラスティックな変更になる(完全に別物になる)ので、AAPのプロトコルのバージョンを上げて対応すべきところですが、現行バージョン0.7.4までで "V2" として作り直しが進んでいるので、そのままV2プロトコルがこの設計を反映したものになるでしょう。

一方でAndroidAudioPluginMidiDeviceServiceがUMPを前提にするわけにはいかないので、MidiDeviceServiceからの入力を8月に実装したUMP Translatorを使ってUMPに変換し、逆にプラグイン実装側ではUMPからMIDI1にダウンコンバートするTranslatorの真面目な実装を新たに作って使ったり、プラグインプレビューのアプリではパラメーター変更入力をポートへの直接入力から新しいUMPのAssignable Controllersメッセージに変更したり(これでようやくCLAPやAtom Sequenceのような新しいイベント処理のスキームに切り替えられたといえます)、同じダウンコンバーターをaap-lv2の実装にも適用したり…といったことをやっていました。実のところまだ完了しておらず、LV2 portとAAP portのマッピングどうしよう…みたいなところで現在でも泥沼にハマっています。先にaap-juceで対応したほうが早かったかも…

11月の予定

11月は正直まだどうするか決めていません。「どうする」というのは具体的には例年11月にロンドンでやっているアレです…多分オンライン参加で妥協すると思います。去年より自由になったとはいえ、まだ一生後遺症が残り得るLong-Covidの問題が続いていることに変わりはなく、現地で自己アッピール()に使えるようなソフトウェアはまだ完成していない状態なので、今行くと「オーディオクラスタにしては妙にAndroid開発に詳しいやつ」というビミョい位置付けで固まってしまう気がします(正しいのですが)。まあ何か理由があれば現地参加する可能性もあります。

あと11月のうちに1回オンライン勉強会をやっておきたいですね。ネタはあるといえばあるのですが、状況次第です。何か準備できたら改めてTwitter(いつまで使うかわからないけど)などで告知します。

9月の活動記録(2022)

9月がいつの間にか過ぎていました(忘れてた)。9月はまだ半分以上台湾で過ごしていたんですよね。いつの間にかすっかり日本で落ち着いてしまいました。9月は表立った活動としては技術書を2冊書いたのが主だったところです。

CLAPオーディオプラグイン開発者ガイド

9月には技術書典13がありました。経験上「最終日に新刊が出ていれば大丈夫間に合う」という見込みで8月には何を出すかも決めていなかったのですが、9月になってさすがに何も計画が立っていないのはヤバい…ということで、いくつか候補を考えましたが、まず着実に書けそうなものを具体的に書き進めることにしました。その1冊目がCLAPです。

techbookfest.org

書き終わってから、サークル名を「オーディオプラグイン研究所」に変更していたことを思い出したのですが(!?)、結果的には完全にサークル名にふさわしいトピックになったと思います。なお9/11のオフライン開催には参加していません。台湾にいたし。

一方で、内容についてはだいぶ荒削りで、特に拡張機能まわりはここに以前書いた文章をそのまま使いまわしている部分も多いです。今回の執筆には、(コピペに手を加えた部分がそれなりに多いので)1週間くらいしかかかっておらず、途中から「なんだか初日に間に合いそうだしこのまま出せるスピードで初版を出しちゃえ」というノリになっていた側面はあります。実際、技術書典13の初日(9/10)には間に合いました。

今回は同人誌を「書き上げた」というよりは、開発に必要・有用そうな自分の過去記事を断片的に繋いでまとめた感じです。本当は30ページもいけば十分だと思っていたのですが、それで結果的に50ページ弱ものボリュームになりました。同人誌を完成させる作業のうちそれなりに悩ませられる要素である表紙のデザインについては、過去にLV2開発者ガイドを書いていたやつを色違いにして出すというやっつけ仕事で済ませました(!) 成り立ちがそんな感じですし、同人誌のちまちました収益に期待するよりは広く読んでもらったほうがいいだろうと考えて(仮に1冊1000円で100冊売れても10万円の売上にしかならんですし、現実には10冊売れるかどうかというレベルだと思います。トピックがCLAPだぞ??)、今回は無料配布しています。(前作「MML to MIDI 2.0」の本も実用性はほぼゼロの自由研究レベルだったので500円にしましたが、それと比較したら1000円でもよかったかも…?)

ついでにGitHubリポジトリも公開していますが、これは改訂版の作業を完了できたら有料販売のために閉じるかもしれません。

Linux DTMガイドブック

CLAP開発者ガイドブックは無難に書けるものを書いた(新刊として出せる安牌のネタを拾った)だけでしたが、別口でLinuxユーザー向けに「LinuxでもDTMはそんなに難しくない…!」って言えるような、DTMソフトをあれこれ紹介する本を出したいとは前々から思っていたところでした。サウンド系のコミュニティでそんな話が出てきたので、じゃあ書けそうな機運があるうちに1冊作っておこうと思って、CLAP本を出して落ち着いた後で、カジュアルに書き始めました。今度こそ技術書典最終日(9/25)が最終締切のやつ…!

techbookfest.org

最初はほぼ「Linuxで動くソフト」の紹介本のつもりで、一方で自分が特にプラグイン側の説明を滔々と書けるほどのトラックメイカーではないので(一方でDAW開発者でもプラグイン開発者でもないし、自分の立ち位置が何なのか今でもよくわからない)、大半がテキスト数行の紹介 + スクショで済むだろうと雑に目論んでいました。が、実際にDAWの使用感を説明しようと思ったらALSAやJACKをどう制御するかを全く説明しなかったら、多分DAWを全く使えないし、Wine経由でWindows用のプラグインを使える技術についても言及しておくべきだし…という感じでyabridgeの紹介を書いたり、そのために動作検証したり…といった感じで、作業量と内容が膨らんでいったところがあり、最終的にはソフトの紹介は半分ちょっとくらいになったんじゃないかと思います(数えてない)。

あと、この本のベースになった構想は草稿レベルでいくつか貯まっていて、最終的にこの本にまとめる時間がなかったネタとして「サウンドフォントやサンプラーの類をいろいろ紹介していく」などがあります。これもCLAP本と同じような感じで「最終的な完成版は改めて出す」ことにして、今回は無料で配布しています。技術書典主催の🐑も「これが無料なのはありえん」って感じで紹介してくれていましたが、ちょっとサービスしすぎたかも…まあ、仮に売れたとしても利益はせいぜい5桁なので、それよりは読まれてくれたほうがいいです。完成版は運が良ければM3 2022秋に間に合うかもしれません。

ちなみにこちらの本は技術書典13では最後の4日間だけ配布というかたちになってしまったわけですが(予定通りではある)、CLAP本は150件くらい、Linux DTM本は200件くらいダウンロードされており、トピック的にLinuxのほうが面白かったようです。CLAPが面白いって言える音楽技術関係者やプログラマーは多くないはずなので、これはまあ予想されてしかるべきところですね。

MidiPlayerのMIDI 2.0サポート用のMIDI-CI実装

8月はAAPでプラグインのサンプルクライアントを書き換えたいという話を書きましたが、そのためには現状の決め打ちのMIDI入力・オーディオ入力を渡すだけでなく任意のMIDIシーケンスを演奏できるようにしたほうが後々テストが楽になります。そのため、ktmidiのMidiPlayerを取り込んで演奏できるようにしたいと考えました。

ただ、ktmidiの特にMidi2Playerは、実際に演奏できるMIDI出力デバイスが存在しないため、実装の動作確認が何も出来ていない状態でした。Midi2Playerがきちんと動作するか確認するためには、デスクトップではMIDI2のUMPストリームからMIDI1のバイトストリームに変換して(8月にここで書いたやつです)、それを直接MidiOutputに出力するモードが必要で、その辺を実装したりしていました。AndroidではAAPのMidiDeviceServiceに垂れ流せば済むのですが、デスクトップで動作確認してからのほうが楽です。

Midi2Playerの動作確認に有用なのはMIDIプレイヤーアプリの実装なのですが、実はmanaged-midiから移行できていない最後のひとつがMIDIプレイヤーxmdspで、これをMidiPlayer基盤の動作確認が必要なレベルで作り始めるのはちとチャレンジングすぎます。そういうわけでコンソールプレイヤーのサンプルを追加したり、そのためにKotlin Multiplatformのプロジェクト構成を見直したり、あとMaven Centralへのパッケージ発行で罠にハマって(新バージョンのreleaseが失敗してもこちらに何も報告されない問題とか…)問い合わせで数日使ったり、あとサンプルデータを生成するためにmugene-ng(MMLコンパイラ)のリポジトリをビルドし直そうとして、むしろビルドを通すためのリグレッションに追われたりなど、地味な作業が進んでいるところです。

ktmidiのこれまでのMIDI2サポートは、基本的にMIDI-CIで調整された後のストリームの送受信を前提としていたり、あるいはMIDI-CIのSet New Protocolメッセージを盲目的に受信して切り替えられるようにしていましたが、本来ならそこにMIDI-CIにおけるsource/destination MUIDのチェックやTest New Protocolのやり取りが必要なわけで、その辺も実装しておかないと他のMIDI 2.0システムとの相互運用は難しいところでしょう。これは先月たまたま発見したのですが、ALSAでもMIDI 2.0サポートに目が向き始めている様子があるので、MIDI-CIも少し整備しておきたいところです。そういうわけで、MIDI-CIのプロトコルに基づく状態管理に使えるクラスを作ったりしていました。ただ、まだプリミティブな実装になっていて、同じMIDI-CIを実装しているAppleのCoreMIDIのAPIと比べると使いにくいです。HTTP/3をリクエスト/レスポンス別々にユーザーが扱わなければならないような状態なので、たぶんKotlinのsuspendを使った非同期APIにすると思います。

作っていて気付いたのですが、MIDI 2.0における3つのPと言われているやつのひとつProperty Exchangeは、最新のCoreMIDIでもサポートされていませんでした。JSONを使ってプロパティをやり取りするやつで、これをサポートするにはJSONリアライザーを統合しなければならないわけですが、その辺でどういうAPIにするのが適切なのか、たぶんAppleも決めあぐねているところがあるのでしょう。わたしにもいいアイディアが無いというか現状そこまで考えるメリットがなさすぎるので(この仕様もしかしたらGeneral MIDI 2みたいに使われずに死ぬんじゃない?くらいの印象)、とりあえず様子見です。

10月の予定

9月末からずっとゲームばかりやっていて忙しくて他のことに手を付けていられず、今週もDroidKaigiオフラインを眺めに行く予定なので、まだ見通しが立たないところですが、9月の積み残しのMidi2Playerを片付けて、それをもとに8月の積み残しのAndroidプラグインクライアントを片付けて、あと↑の同人誌の穴埋めが出来たらM3 2022秋に少数部でも紙版とか持っていきたいところではあります。最近の新刊はずっと電子版ばかりでカタログ上の存在しかなかったので、ちょっとグッズを持っていきたい気持ちがあるんですよね。個人的にはもう紙版と電子版があったら後者しか買っていないのですが。まあそんな感じです。

8月の開発記録 (2022)

ちょっとだけ台湾生活のようす

台湾で日常生活を送りながら日暮しPCに向かいて過ごしています。日本も医療崩壊がひどいことになっていますが、安全に過ごせる度合いは台湾とどっこいどっこいなところがあるなあと感じています。公共交通機関でマスクを外す人間は皆無に近いので(これはきちんと規制があるためでしょう)、日本とは違って安全にMRTやバスで移動できています(日本では2ヶ月に1回くらいしか載っていなかった)。MRTはもともと飲食禁止で、日本みたいにホームに自販機も売店も設置されていないですし。

最初の1ヶ月はそこそこ他人と会うことがあったのですが、最近は外出する理由もなく、ほとんどYouBike(貸自転車)しか使っていません。いつの間にかYouBike 2.0とやらができて、大抵500mも歩かないうちにバイクスポットが見つかるくらい増えているので、ちょっとGoogle Mapで茶店(作業場)を探して空いていたら(空いているかどうかはざっくりGoogle Mapsアプリでわかる)行ってみるか…みたいなことは簡単に出来ています。昔は短時間なら完全無料でしたが、今もほぼ毎回5NTしか払ってないしほぼ無料です。昔は自転車を買っていましたが今は買わなくてもいいな…となっています。町中でもYouBike以外の自転車をだいぶ見なくなった気がします。

安全なのは移動くらいで、食・住は日本のほうが安全だなあと思います。もともと日本人は家庭持ち以外はたいがい一人暮らしだと思いますが、こっちではたいがい複数人でルームシェアリング(正確にはflat sharing)で暮らしていますし。わたしも7月は本当はそうなる一人暮らしになる予定だったのですが、airbnbのホストとのやり取りで完全に騙されて、いざ行ってみたら他の住人が…みたいなことになっていて、しかも初日から隣の部屋から止まらない咳が聞こえてくる…みたいな恐怖体験で、もう自室に入るたびにアルコール消毒するストレスフルな生活でした。家族や自分の同意のある知人ならともかく、どこで何をしているかもわからんどこの馬の骨とも知れぬ輩から伝染されるのはさすがに…。今は完全に自分1人しかいない部屋を借りているのですが、日本の自宅並みに安全です。ただ当然ながら家賃が高くて、今は日本並みに払っています。これはサステナブルではない…

食が安全ではないと思うのは、特に国レベルで圧倒的に自炊に向いていないためです。キッチンのない部屋も多いし(たとえば591で部屋探しの検索条件に「可開伙」 = 料理可能 を追加するとそこそこ絞り込まれます)、食材は日本以上に高いものも多いし、外食したほうが総合的に安いと言っていいでしょう。そんなわけで多くの人にとっては外食が基本になりますし、飲食店は混んでいます。持ち帰りが簡単にできるのはいいところですが、料理を作っている側も日本に比べたら感染対策は緩いですね。店員は休憩中に店内でマスクを外して過ごしていることが多く、何なら調理中も…みたいな様子を、日本より頻繁に見かけます。弁当類は東京のコンビニ同様に台北のコンビニでも買えるので、それで毎日過ごせるというのであれば、こっちは十分安全でしょう。

引越し前は住居付近に便當の店がたくさんあって毎日ローテーションで健康的なものを買えたのですが(共用のキッチンなんて使う気になれなかったし)、今はほとんどなくて代わりに夜市が近くにあるので、たまに油断してその辺で買って帰るか、数少ない便當の店まで足を延ばすか、ランチだけ外でちょっとお高いブランチの類(大抵200NT超え)を食うか、チェーン店(バーガーキングとか)で済ませるか、自宅にある大同電鍋やIHホットプレートを使ってできることだけやる(ただし調味料が何もない)…みたいな生活です。日本では最近ほぼ毎日自宅で食事を作っていたので、天地の差です。

AAP: port-config拡張(未了)と自動定義ポート

6月頃から始めたAAPクライアントの再構築(ネイティブコード中心化)を皮切りに、state拡張をextension化して、7月にはAIDLによる初期化プロセスに破壊的変更を加えましたが、8月にもまずこれを応用したプラグインインスタンスメタデータによらないポートの生成を実現しました。これはport-config拡張機能として実装される予定ですが、現状では大したことをやっておらず、この拡張機能が使われていたら<ports>要素をプラグインメタデータres/xml/aap_metadata.xmlに書かなくてもよろしくやってくれる、というものです。

本来はこれでオーディオポート生成も自動調整できることが期待されるのですが、ひとまずはこれでポートの定義を自動生成中心に置き換えることを目論んでいます。今後「ポートが自動生成されるように作成されたプラグインのであれば、新しいポート設計にも自動で対応する」ような変更を加えやすくなりますし(現状ではポートをハードコードすることになるので、コード上の後方互換性は間違いなく維持できませんが)。

port-configに関連する話でいうと、これ自体はオーディオバス(ポート)設定の自動調整を主眼においたものなのですが、プラグインのオーディオ処理の中心process()やその前段階としてのprepare()に渡すオーディオバッファポインターの集合体AndroidAudioPluginBuffer構造体の構成についても、再設計すべきか否か常日頃から悩んでいて、結論が出ません。CLAPのようにdata32data64を分けたりports x channelsの論理単位を分ける設計が魅力的なので模倣するのも良いと思っています。特にportごとにlatencyを設定できるのが美しいですね。ただこのノリでstrongly-typed APIを導入してコロコロ破壊的に変更し始めると多分癖になって良くない…という警戒心があります。これまでAAPは2年近くプラグインAPIレベルで破壊的変更を加えてこなかったので。6月からどんどん崩していますが(今だけ…というつもり)。

AAP UMP based parameters

今月はこの流れでいよいよパラメーター変更を1つのMIDIポートからすべて処理する仕組みを実装しました。LV2でいえばAtomポート、CLAPでいえばclap_process_tevents_inevents_outに相当するやつです。これでOdin2みたいに数百個のパラメーターがあるやつが数百個のパラメーターポートを生成してそれぞれにバッファが付く、みたいなことをする必要が原理的には不要になります。

原理的に…というのは、実のところこれがまだaap-juceやaap-lv2には適用されていないというのと、メタデータによらないポート生成との食い合わせが悪く、プラグインプレビュー( = テスト)のActivityでMIDIシーケンスを再生できていないためです。現状、MIDIシーケンスはMIDI1で生成されていて、MIDI1イベントとして送信されていますが、新しいプラグインパラメーター変更はMIDI2のAssignable Controllerを利用しており(bank/indexでパラメーター番号、値はfloatのビットパターンを32bit intとしてそのまま反映)、MIDI1に変換すると情報落ちが発生します。floatの列挙値で不一致が発生することを考えるとこれは容認できません。

MIDI1ポートとMIDI2ポート(あるいはパラメーター変更などに特化したポート)を並立させることも考えられますが、パラメーター変更イベントはもともとtimestampedでなければならないことを考えると、MIDI1ポートと並立していることは大きなデメリットになります。これはCLAPのWebサイトにあるMIDI & Event Queuesというページにある解説が詳しいですが、特にVST3のパラメーター変更とノートを別々のイベントキューに分岐するやり方ではイベントの処理順序が維持されないという問題があります。これを回避するためにsample accurateだったはずのイベントを1サンプルずつずらすみたいなdirty hackが用いられるわけです。

そういうわけで、MIDIサポートは、ホスト-プラグイン間のプロトコルとしては全て1本のMIDI2ポートでやらざるを得ない気がしています(プラグインAPIとしてはUMPのみ用いて、MIDI1への変換はプラグイン開発サポートライブラリみたいなものを作って対応する)。現行のMIDI1サポートもSMFのtimecodeをそのまま送るようなやっつけ仕様で、プラグイン側が複数のtimecode decoderを持つのは負担にしかならないはずですし。

最近CLAPのコミュニティでやり取りしていて知ったのですが、CLAPではMIDIなどのイベントポートを複数使えないわけではないんですね。ただホストとプラグインを繋ぐ論理ポートは1本でなければならず、その中で流れる各種イベントにはこれとは別の概念であるportが存在するようです。たとえば、Kontaktのport 1〜port 8に、あるいはSC-88ProのPort AとPort Bに(めっさ古い例えだ…)、同時にイベントを送信できるわけです。追記: イベント構造体にport_indexとして指定するスタイルです(clap_plugin_note_ports_t.get()で渡すindexに対応するはず)。

プラグインパラメーターの変更とプラグインからの変更通知に関してはUIが絡む場合も考えなければならず、特にAAPの設計ではホストからの変更(ホストプロセスUI)の他に、プラグイン本体からの変更(インプロセスUI)と、「プラグインUIアプリ」からの変更()純粋なアウトプロセスUI?)についても検討していたので、設計をいろいろ検討…というか再検討…する必要がありました。それで「オーディオプラグインのパラメーター変更と通知に関する覚書」を書いたりもしていました。

MIDI 2.0 UMP仕様書日本語版(未遂)

7月にCOSCUP 2022でMIDI 2.0のセッションをもったわけですが、MIDI 2.0の情報ってまだあまり無いんですよね。わたしは最大の癌はMMAの「ログインしないと仕様書がダウンロードできない」時代遅れの組織であることが原因だと思っていますが、共同使用策定者であるAMEIのサイトに行くとログインなしでダウンロードできます…という話をそのセッションでもしてきました。

それで「そういえば割と長いこと待っているけど日本語仕様書出てこないな…?」「AMEIにはMIDI 2.0の日本語情報ページがあるんだけど半年くらい更新されていないな…?」となっていたのを思い出して、「UMP仕様書とか短いから2, 3日あれば本体(Appendix以外)は訳せるだろうし、適当に作ってみるか」と思って、8月のはじめの頃に3日くらいで結局全部(Appendixも含めて)全部翻訳しました。

翻訳できたはいいのですが、これどうしようかな…勝手に公開というわけにもいかんだろうし、とりあえずAMEIのMIDI規格委員会のページにはこうあるのを見てとりました。

私たちは、共にMIDI関連の楽器やプログラム、ガジェットの開発/研究を行っていただける仲間を求めています。音楽に関わるお仕事をされてている方々、MIDIに興味をお持ちの学生の方で「個人情報の取り扱いについて」に同意いただけましたら、お名前、メールメールアドレスを記入の上、ご意見、ご要望をお送り下さい。尚、コメント欄は空欄でも結構です。MIDI関連のイベント等について情報を発信すると共に、今後の活動に役立ててまいります。

訳文はAMEIの訳語統一方針とか何も知らない状態で作ってあるので(調べようと思えばMIDI 1.0仕様書から推論できるといえばできますが)、とりあえず仮訳とか参考訳とか、もし何も無ければベースに使えなくはないだろうと思ってprivate gistにmarkdownを貼って送りつけてみました。3週間後に返事が返ってきて(夏季休暇の時期だし「定例会があれば議題に取り上げてもらえるだろう」くらいの推測でひと月は完全に反応待ちのつもりだったので、適切に対応してもらえたと理解しています)、翻訳作業そのものがMMAと示し合わせて進んでいるような状態なので静観してほしい、という内容だったので、訳文は非公開のまま成果物が出てくるのを待とうと思います。

MIDI 1.0 to 2.0 Translator in cmidi2 and ktmidi

仕様書を翻訳していて気付いたのですが、UMPのMIDI 1.0 from/to MIDI 2.0の変換を規定するDefault Translationに対応する実装がまだktmidiでもcmidi2でも出来ていなかったので、前述のAAPポートのイベント変換まわりを実装するときにも必要になるなと思って、とりあえず1.0から2.0への変換機能を追加しました。2.0のUMPを1.0のバイトコードに変換する機能は、以前から部分的に実装してあったので、そこはそのままです(sysexだけ複雑になるのでやっていなかった)。cmidi2はsingle header, inline functionのみの実装を想定していたので、この辺までやってしまうとだんだんktmidi並に複雑になってしまうのでやりたくない…と思っているのですが、まあ変換くらいならいいでしょう。

この辺をがっつりやるのであれば、本来ならMIDI 1.0メッセージからMIDI 2.0 UMPへの変換だけでなく、MIDI 1.0 UMPからMIDI 2.0 UMPへの変換もできたほうがよいのでしょうが、MIDI 1.0 UMPのストリームを使う場面がまだ自分のユースケースで出てこないので、後で考えよう…となっています。JUCEだとこの辺をByteStreamInputHandlerU32InputHandlerで分けている感じで、これくらいなら複雑になってもいいか…と思わなくはないですが、なるべくシンプルで直感的なAPIにしたいところではあります。(まあJUCEのUMPのAPIがわかりにくいのは、この場合は過剰な共通化ではなく単なるドキュメント不足だと思いますが。)

9月の予定(のつもり)

パラメーターポートまわりの次世代実装のProof of Concept実装が使えそうだとわかったので、現在はPluginPreview(プラグインのテストプレビュー実装)とそのUI (Activity) まわりを改造しています。現状でもlv2applyの入力を固定したやつみたいな感じですが、もう少しMIDI playerに近いものを作りたいところです。

あと9月には技術書典13があって10月にはM3 2022秋もあるので(出展予定)、新刊を用意しないと…(技術書典は最終日が締切日くらいに思っているところがある) …というわけでコーディングは少なめになるかもしれません。

オーディオプラグインのパラメーター変更と通知に関する覚書

最近自分のプラグインフレームワークプラグインパラメーターのサポートを設計しているのだけど(パラメーターのサポート無かったの!?と思われそうだけど、これまでは古いLV2と同じでパラメーター毎に「ポート」を用意するスタイルで実現していたのを再設計している)、特に変更通知まわりは割と面倒なところがあるなと思ったのでちょっとまとめておきたい。

パラメーター操作の分類

オーディオプラグインのパラメーターを操作する主体は2つ、あるいは3つある。

(1) ホスト(DAW)のシーケンサープラグインprocess()とかrun()とかに相当するオーディオ処理関数が呼び出されたときに、MIDI…に類するイベント…としてパラメーター設定命令が(理想をいえばタイムスタンプ付きでsample accurateに)そのprocess()あるいはrun()のサイクルの分をまとめて渡される。データの元がMIDIマッピングされたMIDIシーケンスであろうと、オートメーションであろうと、APIとしてはこれらの関数に行き着く。これらの関数はMIDI…に類するイベント…のアウトプットも取得でき、ホストはこれを現在処理中のフィルターチェインにある次のプラグインに渡すことができる(これはおそらくは一度MIDIに類する抽象イベントに変換することになる…というのは、たとえば、VST3のチェインの次にAUが繋がっていたらVST3イベントそのものを渡すことはできないからだ)。

(2) プラグインのUI操作。一般的に、GUIはリアルタイムで処理できないので、リアルタイムのオーディオ処理ループからは切り離されていて、したがってスレッドコンテキストも分離しているので、GUI上で生じたパラメーター変更のイベントは、いったんプラグインの内部でキューに溜め込まれて、次のprocess()run()の中で(理想をいえばタイムスタンプ付きでsample accurateに)処理される。ホスト側でプラグインのパラメーターの状態(現在値)を把握しておきたいことを考えると、UIによるパラメーター変更はホスト側に通知されるのが理想的で、したがってプラグインがホストに通知イベントを送信する仕組みが必要になる。

(3) オーディオ処理そのもの。これはパラメーター変更の入力ではなく出力 = 通知の話になるが、パラメーターには他のプラグインパラメーターの変更によって計算される性質のものがあり得る。これらはオーディオ処理に直接影響するとは言い難い(その関連パラメーターの変更は上記のいずれかですでに渡されている)が、これによって生じたパラメーターの再計算は、その変更を通知されるホストにとっては、プラグイン上にに生じたパラメーター変更と同義だ。

ただ、これら(1)〜(3)の処理モデルは、リアルタイムのオーディオ処理ループが稼働していることを前提としている。もしオーディオループが回っていなければ、UIによる変更を反映する次のprocess()run()は回ってこないので、それに伴って生じるべき変更通知はずっとやってこない。オーディオループが回っていない状態でもイベント通知が滞りなく届くようにするには、オーディオ処理が回っていない状態でもパラメーター変更が処理される仕組みが必要だ。そうしないとプラグインGUIで設定したパラメーターの値が、ホストで必要な箇所に反映されなくなってしまう。

UIイベントをプラグインに蓄積させるAPIプラグインにイベントを処理させるAPI

上記のうち(2)に関しては、一般的にはGUIプラグインのプロセスの中で動作しているので、GUIが発生させるイベントは同じプラグインの内部でイベントキューを保持しておいて、オーディオ処理が走るたびに内部で(1)のイベントストリームとマージして処理することになると考えられる。この場合、外部にAPIとして表出させることはない。

一方で、LV2 UIのように、プラグイン規格そのものがGUIをきちんと分離して、外部から入力をデータとして渡す仕組み担っている場合、あるいはその他のプラグインでもUIをきちんと分離する仕組みにしている、あるいはプラグイン本体とプロセス分離せざるを得ない場合(AAPがUIをホスト側で生成するとそうなる)、イベントキューイングのためのAPIプラグイン側から表出している必要がある。LV2 UI拡張のLV2UI_Port_Subscribe LV2UI_Write_Functionはこれを体現したAPIと考えてよいだろう。このAPIはイベントのキューイングのためのAPIであって、イベントを処理するためのAPIではない。

一方で、前述の通り、リアルタイムのオーディオ処理が停止している状態でもパラメーター変更が処理される仕組みが必要で、これはprocess()run()と同様に、イベント出力を生成する必要がある。このAPIの具体例と言えるのがCLAPのparams拡張に含まれるclap_plugin_params_tに含まれるflush()だ。LV2のGUIに変更を反映させるための仕組みとは性格が異なる。(このflush()の位置付けは実のところ曖昧だというissueも立っているが、本質的には書き方の問題であって仕様の設計がぼやけているということではないはずだ。)

別ベクトルの解決策として、そもそもprocess()run()をリアルタイム処理中以外でも呼び出せるようにすれば良いのではないか、そうすればイベント処理の実装はこれらのオーディオ処理関数で一元化できるのではないか、とも考えられるが、それはそれできちんと入力を処理できないプラグイン(すなわちオフラインレンダリングに対応できないプラグイン)が存在する可能性があって、この路線を押し通すには事前に調整されて然るべきということになる(たとえばAAPではまだ仕様が策定中なのでそういう前提を作れる)。

これが多分難しいのは、DSPの処理がリアルタイム前提で時間計算するようになっていたら、オフラインレンダリングには対応できないということと、DSPは特定のプラグインフォーマットから独立して実装されていることが多く、既存のDSPコードがオフラインレンダリングに対応できていない場合、process()run()がオフラインレンダリング対応を前提にしていると、そのDSPそのものが利用不可能になってしまうかもしれない、とかもしれない、といったことが考えられる。

非同期イベントとしての通知出力と、同期イベントとしての通知出力

リアルタイムのオーディオ処理のバックグラウンドで動作するUIを経由したパラメーター変更は、一般的には非同期イベントとなる。これに対して、オーディオ処理を経てのパラメーター変更通知が非同期となるべきかどうかは議論の余地がある。一般的には、UIコードがパラメーター変更のAPIを呼び出しても、前述の通りいったんキューイングされるだけだ。一般的にはキューイングするだけの処理の中で、パラメーター変更通知に値するプラグインの処理が発生するとは考えられず、一般的にはオーディオ処理の「結果」として通知に値するイベントが生成されるはずだ。キューイングの時点で通知イベントが生成されることを期待するようなAPIにしていると、何がしたいのかよくわからないぼやけたAPIになってしまう。

前述のclap_plugin_params_tに含まれるflush()を取り上げると、この関数はリアルタイムオーディオ処理が走っていないときに呼び出されてclap_output_events_tにイベント出力を格納することが想定されており、これは同期的なAPIだ。逆に、LV2 UI拡張にあるLV2UI_Write_Functionport_eventのようなAPIでは、その関数呼び出しから何らかの通知イベントが生成されることは期待されていない非同期的なAPIだ。そもそもGUIからプラグインを経ずにイベントを生成することは推奨されない。

CLAPのparams拡張のような同期的にイベント処理を行う方式については、もうひとつ言及しておくことがある。CLAPの場合、パラメーターの変更はプラグイン側で発生するイベントだが、プラグイン側からホスト側に対してイベント出力を直接送信する仕組みにはなっていない。その代わり、プラグインはホストから事前に渡されているホスト側のコンテキストであるclap_host_params_tのメンバーrequest_flush()を呼び出して、あくまでホスト側からclap_plugin_params_tflush()を呼び出すことを期待している。こうすることで、通常のオーディオ処理とイベント通知による処理のモデル(特にスレッディングモデル)を統一できる。オーディオ処理がリアルタイムで動作している状態であれば、request_flush()を呼び出さなくても、次のサイクルで通知イベントを出力するだけでよい。

パラメーター拡張APIは誰のためのものか (8/31追記)

前回はパラメーター変更と通知のAPIの類型について解説したが、特にLV2とCLAPでの位置付けがだいぶ異なることが見て取れた。なぜそのような違いが生じるのかを、少し踏み込んで考えたい。

一番根本的な違いは、それらのAPIを「誰が」使うのかを明確にすると見えてくるだろう。

  • CLAPの場合、拡張プラグインAPIを呼び出すのは常にホストであり、拡張ホストAPIを呼び出すのは常にプラグインだ(と自分は理解している)。パラメーターのflush()はホストが呼び出すものだし、プラグイン側はプラグインAPIを呼び出すことはない。プラグインGUIコードがプラグインDSPコードで利用するパラメーターを設定するために使用するAPIは存在しない。それはCLAPのスタンスでいえば、プラグイン本体が内部的に解決(提供・実装)すべきものであって、プラグインとホストのインタラクションに影響しないなら規格化されない。オーディオプラグインデザインパターンのようなものは、CLAP全体として標準化されない傾向があるように見える。
  • LV2の場合、LV2 UIの実装はLV2 DSPの実装とは別の共有ライブラリになり、パラメーターの変更と通知は接続されたポート経由で行う。LV2 UIの実装とLV2 DSPの間でインターフェースすなわちAPIが必要になる。

ちなみに、この一連の覚書でCLAPに関してparams拡張の話だけしてgui拡張の話を一切しないのは、CLAPのgui拡張は本当にGUIを扱っているだけで、GUIから発生するであろうパラメーターの変更イベントに関しては一切関知していないからだ。この辺CLAPの切り分けは美しくできている。最初に見たとき「拡張機能が"UI"ではなく"GUI"なのはセンス無いな…」と思ってしまったものだけど、この拡張は本当にGUIのためのものであって、たとえばUIとDSPの分離などを目的としてはいない。

Android 13のMIDI 2.0サポートについて

Android 13正式版がリリースされてAOSPにcode dropされたようです。

Android 13には興味深い新機能がいくつかあって、個人的にはLE Audioサポートの追加で音楽ソフトの体験が変わりそうなのとAndroid 12Lで追加された(主に)タブレット向けの複数Activity並列動作のサポートが気になるところですが、それらと並んでMIDI 2.0サポートが追加されたことがAndroid Developers Blogに書かれているので、これを少し解説します。

MIDI 2.0 - Android 13 adds support for the new MIDI 2.0 standard, including the ability to connect MIDI 2.0 hardware through USB. This updated standard offers features such as increased resolution for controllers, better support for non-Western intonation, and more expressive performance using per-note controllers. More here.

MIDI 2.0プロトコルサポートの概要

MIDI 2.0サポートに対応するAPIとしては、android.media.midi.MidiDeviceInfoに追加されたgetDefaultProtocol())メソッドのように、新しいMIDIプロトコル定数を利用するものがいくつか増えた程度で、Android 10で追加されたNative MIDI APIのような目新しさはない(小さい)です。

Android 13でMIDI 2.0サポートが追加されたのはUSB MIDI 2.0デバイスのサポートのみです。しかも「USBプロトコルでネイティブにMIDI 2.0サポートに対応したもの」だけが新規にサポートされたことになります。

MIDI 2.0はMIDI 1.0との後方互換性を強く意識して設計されています。その成果のひとつとして、MIDI 2.0のメッセージをあらわすUMPのパケットは、MIDI-CIとMIDI 2.0 Protocol Negotiationを利用することで、MIDI 1.0な接続環境でも送受信できるように接続を調整できます(これを実現するためには、仕様上は、送受信の双方向接続が確立される必要があります)。5-PINのMIDIケーブルでの接続まではカバーできないような話がMIDI 2.0仕様書には書かれていますが、USBやソフトウェアによる仮想MIDIバイスに関してはMIDI 1.0の接続さえあれば、そこにMIDI 2.0 UMPを流し込めるというわけです。

Android 13にはMIDI 2.0 UMPを「扱う・処理する」APIは存在しません。そもそもMIDI 1.0を扱う・処理するAPIAndroidには存在しません。MIDIメッセージもUMPストリームもただのバイト配列であり、こういうのはアプリケーションで趣味に合わせて都合の良いものを使えばよく、プラットフォームAPIとして存在する必要はありません。筆者もそういう意識でMIDI 2.0 UMPを扱うライブラリktmidiを作ってGitHubで公開しています。C++ならJUCEのUMPサポートを使うのもいいでしょう。

100%手前味噌なのですが、この辺は先月COSCUP 2022でも話してきた話であり、また1年ちょっと前に発行した同人誌「MIDI 2.0エコシステム構築術」でもまとめています。

xamaritans.booth.pm

ネイティブMIDI 2.0プロトコルバイスのサポート

今回Android 13でサポートされたのは、このMIDI-CIとProtocol NegotiationによるMIDI 2.0プロトコルへの昇格ではありません。これはAndroid 12以前でも利用できたものである、と考えてよいのです。実際、筆者は上記ktmidiプロジェクトの一環として作成した仮想MIDIキーボードアプリkmmkで、MIDI 2.0プロトコルに昇格して…した気分になって(Protocol Negotiationの応答処理をすっ飛ばして)…UMPを送りつけるモードを実装して、別途Android用のオーディオプラグインフレームワーク(自作)から作り出した仮想MIDIデバイスに実際に送りつけて音を出すところまで作ってみたことがあります(今はちょっとまともに動作しない状態になっているので言及するのもちょっとアレですが…)。

Android 13でサポートされるようになったのは、MIDI 2.0プロトコルネイティブな接続です。MIDI 1.0の頃は、MIDIプロトコルMIDI 1.0によるものだけでした。MIDI 2.0の時代になって、このUMPのみを送受信するMIDIバイスという概念が新たに発生することになりました。すなわち、MIDI-CIによるプロトコル昇格を経るまでもなく、最初からUMPでやり取りするデバイスがあり得る、ということです。

あるUSB-MIDIバイスが最初からMIDI 2.0プロトコルしかサポートしていないかどうかは、USB-MIDI 2.0デバイスであるかどうかをUSBプロトコルに則って得られる情報から判断できます。そしてその情報が、冒頭で言及したAndroid 13の新しいAPIであるMidiDeviceInfo.getDefaultProtocol()などで取得できるというわけです。

もうひとつ、MIDIアプリケーション開発者にとって重要な機能としては、MidiManager.getDevicesForTransport())というメソッドを使うと、MIDI 1.0あるいはMIDI 2.0専用のデバイスに限定してリストを返すことができます。

なお、android.media.midiのAPIリファレンスには、USB-MIDI 2.0をサポートするデバイスでも、MIDI 1.0プロトコルによる接続もサポートするべきである、と書かれています("A MIDI 2.0 USB device should create two interfaces, one endpoint that accepts only MIDI 1.0 packets and one that accepts only UMP packets.")。

また、Android 13のAPIで追加された定数は、MIDI 2.0プロトコルのいくつかのオプション(たとえばJRタイムスタンプを伴うか否か等)に関連するものですが、これらについてはMIDI 2.0 UMP仕様書の"3. MIDI Protocls in UMP format"などを参照してください。

MIDI 2.0ネイティブのサポートはUSB-MIDI 2.0デバイスのみ

この意味では、Android 13に追加されたMIDI 2.0サポートはUSB-MIDIに限定されたものであるように見えますが、実のところそのとおりになっています。USB-MIDIMIDI 2.0プロトコルネイティブなものを作成できるようになっているのであれば、仮想MIDIバイスでもそうなっていてほしいと考えるのは自然なことだと思いますが(!)、現状ではMIDI 2.0ネイティブなMidiDeviceServiceは作成できません

MidiDeviceInfoクラス経由でわれわれMIDIアプリケーション開発者が取得できる情報は、別のわれわれMIDIバイス開発者がdevice_info.xmlなどの「デバイスマニフェスト」で公開できる情報に含めることができる情報であり、ここに「MIDI 2.0プロトコルネイティブだよ」と記述すればOKとなってほしいところです。そう考えてGoogleissuetrackerにfeature requestを出したのですが、先送りになってしまいました。

理由はこのissueに書かれているのですが、もしデバイスマニフェストプロトコル情報を追加するだけで対応してしまうと、MIDI 2.0プロトコルネイティブのMidiDeviceServiceはMIDI 1.0と後方互換性が無いにもかかわらず、MIDI 1.0デバイスに接続するアプリケーションからも接続可能なデバイスとして列挙されてしまいます。これではユーザーがちゃんと使えるデバイスを利用できないことになるので、恐らく別のIntent Actionが必要になることでしょう。

そもそもUSB-MIDI 2.0対応デバイスは現状ほぼ存在しない

前述のCOSCUP 2022のセッションでも話した(スライドにも書いた)のですが、2022年8月の時点で、MIDI 2.0に対応したMIDIキーボード等のMIDIバイスはほぼ何も存在しません。開発用の基板のようなものを作って売っている会社はあるようだ、というレベルです。MIDI 1.0インターフェース上でMIDI-CIのプロトコル昇格を利用してUMPをサポートするほうが現実的なので、MIDI 2.0ネイティブのデバイスというものがリリースされ、サポートされないと困る…!とまで言えるようになるのは、近くない将来のことになるでしょう。

そういうわけで、Android 13のMIDI 2.0対応は、現状ではほぼ無視してよいレベルであるといえます(断言)。一方で、USB-MIDI 2.0仕様はもう確定していて変わらないので、Android 13の現時点でUSB-MIDI 2.0に対応できているというのは良いことだといえます。MIDI 2.0ネイティブデバイスの普及期にはAndroid 13が普及機で動作しているというのが理想的な状態です。

MIDI 2.0サポートに向けてとるべき対応

われわれMidiDeviceServiceの開発者がMIDI 2.0に対応するには、ソフトウェアレベルでMIDI-CIのプロトコル昇格に対応してUMPを処理できるようにすることです。これをしっかり実装しようと思うと、それなりのMIDI-CI基盤を構築しなければならず、これは少し骨の折れる作業になるので(送受信のcorrelationが必要になる)、Androidオーディオチームではこの目的でみんなが使えるMIDI-CI実装を開発して公開するつもりであるようです…と先のissueでコメントされています。コメントしているのはAndroidオーディオチームのリーダー(Oboeの開発者でもある)なので、それなりの確度で出てくることでしょう。

MIDI 2.0をサポートするDAWなどのアプリケーションもまだまだAndroidでは存在していないので(完全にchicken and eggの問題)、われわれが「MIDI 2.0をサポートしてほしい」という機能要望を見るようになるまでにはしばらくかかることでしょう。その間にAndroidオーディオチームがMIDI-CI対応ライブラリやAPIを出してきたら、そこから対応を始めるくらいでも十分ではなかろうかと思います(個人的には自分でMIDI-CI対応機能を作ってしまうと思いますが)。

7月の開発記録 (2022)

COSCUP 2022に登壇するために台湾に来ています。

台湾はまだ観光ビザで訪問できないので、就業金卡というビジネスビザの一種で来ています。ある種の職能スペシャリストビザの一種で、いくつかの条件を満たせば申請が受理される見込みがまあまああるのですが、キャリア年数などのほか年収基準なども150万NTDくらいで、IT技術者(条件のひとつを満たす職種)ならそれなりの収入がある人も少なくないと思います…と書くつもりだったのですが、昨年自分が申請した時に比べて日本円の価値が暴落したので、割と厳しくなったと思います…

周りの外国人の知人は胡散臭い連中だらけだったのに割と漏れなく取得できていて、しかも3ヶ月くらいで取得できていたので、自分も…と思っていたら、承認が降りたのは1年前だったのにその頃にオリンピックのせいで感染爆発が起こって、手続窓口が今年の春先までずっと閉じていて、必要な手続は今頃になって進められたという…いま申請すればたぶん3ヶ月くらいでいけると思うので興味のある人は試してみましょう(滞在可能年数に応じた申請手数料がかかります)。

台湾は三+四天(天は日数)という入国者隔離政策があって、過去に比べるとずいぶん緩められていて、たとえば自宅隔離が許されていたり隔離タクシーでなくても家族が車で迎えに来たりできるので、わたしなんかは失策だと思うわけですが、今のところBA.5もほぼ阻止できているようです。以前に行き来していたときも事前準備していると割とナーバスになったものですが今回はさらに輪をかけていろいろ気にしながら進めました。特に就業金卡は日本では情報が少ないですし(わたしがもっと書けって話になりそうですが)、日本から出国する日本人のための情報は英語圏でも見つからないですしね…

オーディオプラグイン規格勉強会 (2022.7)

先月も書いたのですが、CLAPという新しい規格…いやホントは昔からあったのですが…が正式リリースされて脚光をあびるようになったので、CLAPの勉強会を開催…する前にそもそもオーディオプラグイン規格全般を調べながらみんなでわいわい討論できる勉強会をやりたい…ということで7/6に開催しました。これについては今月4回にわたってここに連載したものがあるので、それを見てもらえればと思います。これを勉強会のために下調べとして書いていたので(そうするとスライドもまとめやすいわけです)、それでそれなりの時間を使いました(といってもほとんど6月の話です)。

What for, Where and How to Adopt MIDI 2.0 @ COSCUP 2022

7/30に台湾の巨大OSSカンファレンスCOSCUP 2022にて表題のタイトルで「MIDI 2.0の技術はどこで応用できるのか」という話をしました。

coscup.org

MIDI 2.0は今使われているわけでもないし、デバイスもなければOSサポートも無く、MIDI 2.0が実現するのもオーディオプラグインの既存の技術で実現できていることが中心だけど、MIDI-CIを使ってMIDI 1.0のトランスポートの上に成り立つもので、オーディオプラグインがバラバラに実現してきたものがMIDI 2.0によってふたたび標準としてDAWの内部で音楽表現を担う可能性があるものとしての未来を見出してほしい、という内容です。スライドはspeakerdeckで公開してあります。

speakerdeck.com

申込み時に「まあ初歩的な話しか出来んやろ」と思って入門レベルとしていたので、特にコードもデモもなく表層的な話で終わっています。ただもしかしたら開発初心者ではなく打ち込み初心者が来ていた可能性がry(まあ「MPEとか使えばMIDI 1.0ベースの世界でもper-note pitchbendとかできるよ」みたいな話もしたので大丈夫なはず…)

本当はAndroidのオーディオプラグインにもMIDI 2.0サポートを組み込んであるよホラ見て、みたいな話をしたかったのですが、実装のバグつぶしに負われていて「無理はせんとこ…」ってなりました() こっちがうまく動いてもMIDI 2.0クライアント(ktmidi/kmmk)と協調できないと困るし…AAP本体もそうなのですがC/S系のコードになるので面倒くさいところがあります。デスクトップならまだ楽なんですが。

AAPクライアントの再構築

AAPの拡張機能を実装してから、AIDLベースのKotlinクライアント実装ではそれに対応できないのを全面的に再構築する必要があったわけで(先月の続き)、7月はそれを片付けました。

今主に進めているのは新しいポート設計の部分で、現状LV2のTTLのようにメタデータXMLに全部列挙しないといけなくなっているものを、プラグインのコード主体で設定できるようにするものです。LV2 dynamic manifestのように動的にポートを設定したいというよりは、(1)オーディオバスを柔軟に設定できたり、(2)MIDI-CI実現のためのポート動的生成を可能にしたり、(3)オーディオポートのメタデータ記述を不要にしたい(デフォルトでステレオ設定としたい)といった目的です。特に(2)でMIDI IN/OUTを自動生成できるようになると、拡張機能の実装にsysexを使えるようになるので、そこに期待しています。

これを実現するためには、KotlinクライアントのAPIもネイティブAPIメタデータではなくインスタンスからポートを取得する必要があり、そのためにはJetpack Composeで実装していたプラグインプレビューのアプリもメタデータではなくインスタンスを中心に構築する必要があり…と、いろいろ芋づる式に書き換えることになりました。これは不安定なクライアント(ホスティングAPIなので、まだ表に見えるメリットのある変更にはなっていない感じです。プレビュー機能もまあまあイマイチなので(ホントにテスト用でしかない)、もうちょっとまともな機能と実装がほしいところではあります。ただ、さすがに優先順位が低い…それとは別に、この辺の内部実装をいろいろ書き換えたことでaap-juceなどもさまざまな問題が表出するようになって、バグフィックスばかりやっています。もうちょっと楽しい機能追加やりたいなあ…

あと最近はAndroid Studioあるいはlldbまわりが不安定でデバッガが何も進められなくなったりとか、NDKのバージョン次第でscudoがエラーを返すようになったり(しかも意味のあるメッセージは何も出さない)とか、いろいろツールチェインの問題だらけで開発体験が非常に悪くなっているので、どうにかなってほしいところです。まあどうにか「する」にはオープンソースでないと無理なわけですが、オープンな部分でもまだビルドの全体像を把握しているわけでもないので、気が向いたら掘り下げようと思います(向かなそう)。

CLAP 1.0公開を機にオーディオプラグイン規格の何たるかを知る (4) 拡張機能・各論 ほか

一般的な拡張機能

状態(state)の保存・復元

DAWで音楽を打ち込んだ内容をセーブすると、各トラックに設定されたプラグインのパラメーター等をひと通り操作した状態(state)を保存することになります。逆に楽曲データをロードすると、プラグインのパラメーター等を保存データから復元することになります。

DAWはひとつのプロセスの中にさまざまなプラグインをロードするので、他のアプリケーションに比べると頻繁にクラッシュします。そのため、楽曲をセーブする前に予防的に状態を保存することがあります。

状態の保存先は大抵のプラグインフォーマットではただのバイトストリームですが、LV2では保存する項目と値を構造的に格納するスタイルの仕様になっています。LV2は全体的にSemantic Webの流儀に従っており、LV2 Stateの仕様もその影響を受けていると考えられます。

プリセットの利用

プラグインの中にはMIDIでいうところの「プログラムチェンジ」に相当する機能を実装しているものがあります。シンセの場合は、これは単純にパラメーターの設定値の集合だけで実現していることも多く、その場合はプリセットと呼ぶほうが適切ともいえます。この機能をどう呼ぶかはプラグインフォーマット次第です。ここでは便宜上プリセットと呼びます。JUCE AudioProcessorならProgramと呼ばれます。

パラメーター設定の集合であると考えるとピンとくるかもしれませんが、プリセットが実装すべき機能は実のところ状態の機能とほぼ重複します。プリセットのロードとは、プログラムナンバー、プリセットの番号といったものを状態の代名として指定して状態を復元するのとほぼ同じです。

プラグインフォーマットによっては、ユーザープリセットの保存のような機能を可能にすることも考えられます(JUCEにはそのような機能が存在します。AudioProcessor::ChangeProgramName()など)。

GUI

GUIはオーディオプラグインの重要な機能のひとつですが、「無くても良い」機能でもあります。GUIが無い場合でも、外部のエディターからパラメーターを操作できる「エディットコントローラー」(これはVST3の用語です)の機能があれば、DAWプラグインのパラメーター方法をもとに自前でパラメーター操作のUIを用意できるためです。とはいえ、それでもプラグインはユーザーが操作しやすいUIを提供するのが一般的です。

プラグインフォーマットで専用のGUIフレームワークを提供することは多くありません。汎用プラグインフォーマットでは皆無に近いでしょう。プラグインフォーマットで専用のGUIフレームワークを提供するということは、GUIはその仕組みの上に則って構築するということになります。しかし、一般的にはDAWが利用する言語・開発環境は決め打ちにできないので、プラグインGUIはそのGUIと密結合できません。GUIフレームワークを提供しないプラグインフォーマットにできることは、せいぜいGUI操作においてホストとなるDAWとその小窓で出現するプラグインの間で生じる(その必要がある)インタラクションを、呼び出しや通知コールバックのかたちで定義するくらいです。

GUIフレームワークを開発するというのは大規模な作業になりうるもので、実際大規模な作業を経ずに基本機能だけで済ませたGUIフレームワークではさまざまな問題が噴出します。日本人向けにわかりやすい例を挙げれば、日本語入力にまともに対応できないことが多いです。アクセシビリティ対応、HiDPI対応、マルチプラットフォーム対応など、さまざまな難題があるのです。SteinbergはVSTGUIというオーディオプラグイン向けの汎用フレームワーク(そう、VST専用ではないのです)を作りましたが、やはりデスクトップ向けの一般的なGUIフレームワークと比べたらさまざまな点で妥協の産物です(たとえば2022年現在でもCairoが使われていたり)。

プラグインUI開発に最適な銀の弾丸は存在せず、プラグイン開発者は、自分のプラグインの最適解に近い任意のGUIフレームワークを利用する、という以上の一般化はできないといえます。

オーディオプラグインのオーディオ処理はリアルタイムで完了する必要があります。このリアルタイムとは「必ず一定の時間以内に完了する」というものであり、よくhard realtimeともいわれるものです。一方でGUI処理には一般的に「UIスレッドで動作しなければならない」という制約があります。必然的に、オーディオ処理とGUI処理は別々のスレッドで動作することになります。

さて、一般的にプラグインのオーディオ処理とGUIは別々のスレッドで別々の処理ループによって動作することになりますが、プラグインフォーマットによってはGUIの分離がスレッドの分離より一層強く設計されていることがあります。LV2はこの意味では分離アプローチの最右翼で、UIのためのライブラリを別ファイル上で実装して、オーディオ処理部分とはコードを共有できないようにしています。オーディオ処理のコードを参照しなくても、TTLメタデータの情報をもとにUIを実装することが可能であるためです。もちろんそうはいっても、UIのライブラリを参照してそのAPIを利用するコードを書くのを妨げることはできません。

GUIサポートをクロスプラットフォームで一般化するのは、可能ではありますが、技術的にいくつかのアプローチがあり、これがまた一つ難しい要因です。プラグインフォーマットとして何か1つを規定しないわけにはいきません。

  • VST3ではプラグインIEditController::createView()からIPlugViewというインターフェースの実体としてプラットフォーム別のViewを生成して、それをホストに返します。ホストはGUIのViewを(一般的には)自前のウィンドウにreparentして使うことになります。
  • CLAPではホストがclap_plugin_gui_t.create()を呼び出すとプラグインが内部的にGUIを生成しますが、結果はboolでしか帰ってきません。それをホスト側のGUIに統合するには、reparentするウィンドウのハンドルをclap_plugin_gui_t.set_parent()で渡す必要があります。あるいはfloating windowとして扱うという選択肢もありますが、プラグインがサポートしていなければこれは利用できません。clap-juce-extensionsで(つまりJUCEで)構築したプラグインだとfloatingには対応していません。

CLAPでは、LV2のようなUIとDSPのコード分離ポリシーをAPIとして強制してはいません。これは意図的な設計であるとコミュニティでは説明されています。コードをどのように分離するかは各アプリケーションのアーキテクチャ次第ともいえます。

ホストから提供される「楽曲の」情報

オーディオプラグインは基本的にオーディオ処理関数(CLAPのprocess()関数など)に渡されるオーディオ入力やイベント入力をもとにオーディオ・イベント出力を出力するリアルタイムな処理であり、渡される時間情報は基本的にSMPTEに基づく時間(マイクロ秒など)の即値あるいはそれを変換したサンプル数となります。そこにテンポや拍子(time signature)に関する情報は一般的には不要ですが、プラグインによっては、テンポ等の値をもとに生成する音声やMIDIイベントを調整したいことがありえます。これを実現するためには、DAWからの情報提供機能が不可欠です。この情報はトランスポートとかプレイバックと呼ばれることがあります。各プラグインフォーマットでは、それぞれ次に示す型で実現しています。

  • VST3: ProcessContext
  • LV2: Time拡張機能
  • CLAP: clap_event_transport (events.h)

clap_event_transportは拡張ではなくオーディオ処理で渡されるイベントの種類で、トランスポート情報にアップデートがあったときにホストから渡されます。現在の小節位置なども含まれる = 更新の必要が頻繁に生じるので、このイベントをサポートするDAWからはprocess()で送られるclap_process_tin_eventsに含まれることが多いと考えて良いでしょう。

CLAPにはtrack-infoというトラック情報を取得できるAPIもありますが、これはDAW上の表示色など、だいぶ性質の異なる情報を取得するためのものです。

CLAPのユニークな拡張機能

複雑なノートイベント

CLAPのイベント機構は、イベントの種類をCLAPイベント、MIDIイベント、MIDI2イベントから選択できます(他のイベント体系も規定できますが、ホストとプラグインの両方が合意するものを規定する必要があります)。CLAPイベントには、ノート関連イベント、パラメーター関連イベント、トランスポート情報更新イベントなどがあります。パラメーターについては次の節で説明するとして、ノート関連イベントもCLAPに固有のものがあるので説明します。

CLAPのノート関連イベントは次の4つです:

  • CLAP_EVENT_NOTE_ON: ホストからのノートオン指示
  • CLAP_EVENT_NOTE_OFF: ホストからのノートオフ指示
  • CLAP_EVENT_NOTE_CHOKE: ホストからのノート即時停止指示
  • CLAP_EVENT_NOTE_END: プラグインからのノート完了通知

ノートオンとノートオフはMIDIのものと同様の機能です。MIDI 2.0のノートメッセージとは異なり、アーティキュレーションの指定はできません。

。CHOKEとENDは説明が必要でしょう。まず「チョーク」ですが、これはノートを即座に停止する用途で追加されたイベントです。通常、ノートオフとは「リリース」の開始を意味するものであり、すなわちまだ無音になるまではしばらく時間がかかることを意味します。これに対してチョークが指示されると、そのノートの発音は即座に終了します。これが具体的に用いられる例として、CLAP仕様では(1)ドラムマシンのハイハットなど排他的に発音するノートや(2)MIDIにおけるall notes offのような命令として利用することを想定しています。MIDI 2.0であればノートオフのアーティキュレーションに「これはチョークである」という情報を追加することができたでしょうが、CLAPにはそのようなフィールドが存在しないので、こういう命令を追加するしかなくなります。

「エンド」のほうは、ホストから指示する命令ではなく、プラグイン側からホストへの通知で、リリース処理なども終わって完全にノートが無音になったときに送られます。ホストは、もしこれを受け取ってプラグインが何の音も生成していないことが分かったら、プラグインの処理をスキップするなどの最適化が可能になります。これはtail-length拡張によって実現できていた機能と、目的が類似しています。

パラメーター設定関連イベント

CLAPのパラメーター設定イベントもある程度バリエーションがあります:

  • CLAP_EVENT_PARAM_VALUE: 単純なパラメーターの設定
  • CLAP_EVENT_PARAM_MOD: パラメーターのモジュレーション操作(変化率を指定): 開発チームが "non-destructive automation" と呼んでいるもので、モジュレーションが完了したらパラメーターの値を元に戻せる(オートメーションをかけ終わった後に当初のパラメーター設定がなくならない)ことになります
  • CLAP_EVENT_PARAM_GESTURE_BEGIN, CLAP_EVENT_PARAM_GESTURE_END: ユーザーがDAW上のツマミなどでパラメーター操作を開始したことをプラグインに通知するイベント: この間に呼び出されたパラメーター変更イベントは履歴の記録などで厳密にトラッキングする必要がない、と考えられます

モジュレーションとジェスチャーは、表現力を高めるためのものではなく、DAWを利用するときのUXを改善するためのものといえます。(他の規格にも同様の機能を実現するものがあるかもしれません。)

また、パラメーターではありませんが、ノートエクスプレッションもCLAP_EVENT_NOTE_EXPRESSIONで設定できます。対象パラメーターの代わりに以下のいずれかを「エクスプレッションID」として指定します:

enum {  
  // with 0 < x <= 4, plain = 20 * log(x)  
  CLAP_NOTE_EXPRESSION_VOLUME,  
  // pan, 0 left, 0.5 center, 1 right  
  CLAP_NOTE_EXPRESSION_PAN,  
  // relative tuning in semitone, from -120 to +120  
  CLAP_NOTE_EXPRESSION_TUNING,    
  // 0..1  
  CLAP_NOTE_EXPRESSION_VIBRATO,  
  CLAP_NOTE_EXPRESSION_EXPRESSION,  
  CLAP_NOTE_EXPRESSION_BRIGHTNESS,  
  CLAP_NOTE_EXPRESSION_PRESSURE,  
};

ボイス(発音)数の管理

まだ1.0正式仕様には含まれていませんが、CLAPにはプラグインの発音数を管理できるvoice-infoという拡張機能があります。これが使えると、ホストでプラグインの現在の発音総数や最大発音数を取得できます。といっても。出来ることで音声処理に影響があるとは考えられません(雰囲気でパフォーマンスのある種の指標を得られるといったところでしょうか)。

enum {  
CLAP_VOICE_INFO_SUPPORTS_OVERLAPPING_NOTES = 1 << 0,  
};    
typedef struct clap_voice_info {  
  uint32_t voice_count;  
  uint32_t voice_capacity;    
  uint64_t flags;
} clap_voice_info_t;
typedef struct clap_plugin_voice_info {  
  bool (*get)(const clap_plugin_t *plugin, clap_voice_info_t *info);  
} clap_plugin_voice_info_t;

リアルタイム並列処理の制御 (thread_pool拡張)

u-heで頻繁に主張しているCLAPのアドバンテージのひとつが「ホストによって制御されるスレッドプール」です。これについて筆者は「スレッドプールはLV2 Workerなどでも実装されているし、さすがにそれはおかしいんじゃないか」と思ってだいぶコミュニティで掘り下げて議論して分かったのですが、結論からいえば一般的な意味でのスレッドプールでは全くありませんプラグインが非同期実行を実現するための仕組みではありません。

では何なのかというと、CLAPのthread_pool拡張のAPIは、リアルタイム処理を並列で実行するためのAPIです。プラグインがオーディオスレッドで動作しているprocess()の中からホストの機能を呼び出すかたちで利用します。次のような流れになります:

  • プラグインclap_host_thread_pool_t型のホスト拡張をclap_host_t.get_extension()で取得し、これがnullptrなら並列処理ではなく逐次処理を行う
  • clap_host_thread_pool_tを取得できたら、プラグインは続けてrequest_exec(host, numTasks)メンバーを呼び出す
  • ホストのrequest_exec(host, numTasks)の実装では、もし現在そのホストが指定されたnumTasks本のタスクをOpenMPなどの並列実行機構を用いた並列化を試みる
    • できないようなら、それ以上は何も実行せずにfalseを返す
    • 並列化できるようなら、そのプラグインclap_plugin_thread_pool_t型の拡張機能clap_plugin_t.get_extension()で取得する。これがnullptrならfalseを返す
    • clap_plugin_thread_pool_tを取得できたら、ホストは続けてそのexec(plugin, task_index)numTasks回呼び出し、request_exec()の戻り値としてtrueを返す

exec()で呼び出されるプラグインのタスクは、process()のサイクルで完了しなければならないものなので、並列であれ逐次であれ、処理全体をリアルタイムで実行完了しなければなりません。

CLAPのthread_pool拡張とは、こういった機能を実現するためのものです。一般的な意味でのスレッドプールのAPIはありません。一般的なスレッドプールのAPIであれば、タスク/ジョブのオブジェクトを生成してハンドルを渡すようなAPIになっていないと意味を為さないところですが、CLAPの場合はnumTasksという並列実行スロットの本数を渡すのみで、プラグイン側のタスクの呼び出しも同期的です。「thread poolという名前がおかしい」というのは概ねコミュニティにおける共通理解だと思ってよさそうです。

tuning

tuningはmicrotonal(微分音)を実現するための拡張機能です。この機能がオーディオプラグインフォーマットの一部として規定されるのは珍しいといえるでしょう。一般的に、これが拡張機能として規定されないのは、MIDI 1.0に基づくMMAの仕様としてMTS (MIDI Tuning Standards)というものがあって、DAWはこれに沿ってMIDIメッセージを送信し、プラグインはこれを受け取ったらその内容に応じた周波数変換テーブルを適用すれば良いので、独自にイベントを規定する必要がなかったためです。

CLAPの場合、MIDIイベントではなくCLAPイベントで全てを処理するユースケースに対応することを考えると、MTSに相当するメッセージを規定する必要があるといえるでしょう。tuning.hにはclap_event_tuningというMTS相当のイベントが規定されています。

関連情報

あんまし宣伝エントリにしたくないのですが、多分CLAPの位置付け等を理解するうえでそれなりに参考になると思うので並べておきます。

DAWシーケンサーエンジンを支える技術(第2版)」では、DAWがどうやってプラグインを利用するのか、オーディオプラグインはざっくりどういう仕組みとして作られているのか、楽曲のシーケンスはどう作られて保存されているのか、といった話をふわっと書いています(「ふわっと」というのは主観的な表現ですが、コードが出てこない程度の抽象論に終始する内容です)。

https://xamaritans.booth.pm/items/2397203

「LV2オーディオプラグイン開発者ガイド」は日本語でまとまった情報が多くないLV2オーディオプラグインについて解説し、プラグインの開発方法を説明しています。プラグイン開発に携わっていないと難しく、プラグイン開発をやっていると物足りないレベルかもしれません。

https://xamaritans.booth.pm/items/2394242

最後になりましたが、7/6に開催した勉強会のスライドを公開しておきます。内容はこの連載と合わせて作成していたため、かぶる項目が多いと思います。

speakerdeck.com