💡

Cmajor first impression

2022/12/30に公開

11月末頃に書きかけてあったc-majorネタですが、当時多忙で煮詰まっておらず、その後出すタイミングをすっかり失っていたので(忘れていたともいう)、お蔵入りにならないように出しておきます。主題は完全にaikeさんの「Cmajorによるオーティオプログラミング」とかぶるのですが、こっちの内容はあんましユーザー = オーディオ開発者の視点で書いていないので、全然違うものとして読めます(たぶん)。


毎年11月頃にロンドンで開催されるAudio Developer Conferenceが、今年も11/14-11/16で開催されました。昨年に引き続き今年もハイブリッド開催で、筆者もオンライン参加していました(オフライン参加にするか迷っていたのですが、オフライン参加チケットは10月いっぱいで売り切れてしまいました)。

例年このADCに合わせて新規プロダクトやバージョンのリリース告知を出してくるところがいくつか出てきます。たとえばCycling '74のRNBOなんかは直近のリリースで、実際ADC22のエンディングキーノートの主要なトピックのひとつでもあり、ADCで大いに話題になったといえるでしょう。MicrosoftがMIDI 2.0サポート計画を告知したのも同様です(今回はMS、Google、Appleの3社が合同でMIDI 2.0サポートに関するセッションをひとつもっていました)。

そういうわけで、今回はADC22で新たに公開されたオーディオ開発用言語Cmajorの話です。なおCmajorは現時点でソースコード非公開であり、ランタイムがWindows/Mac用バイナリしか公開されていないので、Linuxユーザーの筆者は試していません(ソースツリーからcmakeでビルドまではできても実行は出来ない)。

Cmajorの目的

Cmajorはある種のオーディオアプリケーションの全体あるいは一部、たとえばオーディオプラグインを手早く開発できるように作られた言語です。Cmajorのアプリケーションは「パッチ」であり、MAX/MSPやPure Data、Super Colliderなどのユーザーが作るものと概ね同じといえます。

開発者のJulian Storerは2018年にはROLI在籍時にSOUL (SOUnd Language) というざっくり同様の言語を開発・公開していましたが、ROLIが破産してIPの関係で自ら開発を継続できなくなったので、新たにSoundwide(Native InstrumentsやiZotopeやPlugin Allianceをまとめている会社)の傘下でSound Stacksという独立した会社を立ちあげて開発したのがCmajorということになります。

Cmajorの特徴

オーディオ処理に求められるリアルタイム性を損なわない基本言語仕様、ビルド処理に時間がかからないようにホットリロード可能なWeb技術に基づくGUI、実行環境に合わせてCPU/GPU/TPU等の利用を最適化できる実行エンジンとJITエンジンなどが特徴です。ADC22のセッションを見られればそれが一番一次情報に近いのですが、どうせしばらく公開されないので、5月にロンドンで行われたAudio Programmer MeetupでのJulian Storer自身のCmajor公開前の公演動画を出しておきます。

https://www.youtube.com/watch?v=2smLOcflZbY

Cmajorの言語仕様は、C言語ライクな構造を「自然」だと思う開発者に最適化されています。前置型、静的型(基本的な型推論はあり)、?:を使う三項条件演算子、セミコロン必須の文法などが採用されています。async/awaitのような構文要素はありません。Cmajorは主にオーディオスレッド上で動作するためのコードを書くための言語です。

null safety: 存在しないメモリアドレスを指すNullポインターはなく、0で埋められたオブジェクト型がNullリテラルと呼ばれています(実際に言語リファレンスで登場するnullはこのNull Literalのみです)。

atomicity: 型にはプリミティブ型と非プリミティブ型があり、プリミティブ型には数値型のint32 int64, float32, float64, complex32, complex64、論理型のboolのみがあります。int32の代わりにint, float32の代わりにfloatcomplex32の代わりにcomplexという識別子も使えます。unsigned integerはありません。複素数型がプリミティブとして存在するのが特徴といえるでしょう。複素数型はオーディオ処理では波形データをfrequency domain(周波数領域)に変換したデータを保持するためによく使われるもので、これがプリミティブ型としてatomic operationの対象となっている(と考えられる)ことが重要です。

わたしの理解では、complex64は16ビットが必要になり、これがプリミティブ型としてアトミックに操作できる必要があるということは、i386は設計上サポートできないということになります。

文字列

string型がありますが、(SOULとは異なり)stringはプリミティブ型ではなく、プリミティブ型に許される各種の言語機能は利用できません(たとえばvectorの型引数として使えません)。文字列操作のAPIもごく限られています。Cmajorの機能は基本的にリアルタイムオーディオスレッドで実行できるものに特化しており、動的メモリ確保を要する機能はサポートしません。

配列の扱い

配列型があり、不可変固定長で、型名の後に[x]を付けます。xは長さで、推論出来ない場合は必ず求められます。サイズ0の配列は宣言できません。

array bound safety: 配列の長さは、定数である必要はありませんが、配列境界エラーを引き起こすようなコードは書けません。定数でない長さの配列の要素にアクセスする必要がある場面では、wrap<x>という型を使います。

references: 配列型の変数は参照ではなく値を保持するものです。変数に配列型の値を代入すると、値によるコピーが行われ、すなわちその配列のクローンが生成されます。ひとつの配列のメモリ領域を参照して利用するものとして、sliceが使えます。Rustなどでおなじみの概念です。

ランタイム

実行環境はクローズドソースなので筆者らが実装を覗くことはできませんが、Cmajorはスクリプティング言語に近い環境であるようです。(筆者のデスクトップ環境はLinuxであり、12月になってLinux用のコンソールツールcmajおよびlibCmajPerformer.soは提供されるようになったものの、Linux用のCmajor VSCode Extensionは相変わらずリリースされていないので、ごく限られた機能しか試せない状態で書いています。)

実行エンジンはJITであると説明されます。JITとリアルタイム処理というのは一般的には排他的ですが、JITがNGなのは一般的には処理時間を保証できない「実行時」コード生成処理を伴うためであり、Cmajorでコンパイルされるコードが(一般的に想定される限り)そこまで長大ではなくロード時に全体をネイティブコードにコンパイルするものであると考えれば、この点ではリアルタイム処理の適性が無いとはいえません。

Cmajorプログラムがスクリプトとして実行できるということは、コードに変更を加えてそのままhot reloadすることも可能ということです。全体的に巨大なコードベースになるとは考えられないので、スクリプト全体をリロードしてJITコンパイルする程度で十分でしょう。この意味ではJITといってもほぼAOTと変わりません。オーディオプラグインのCmajor loaderであれば、いったんパススルー状態にして、リロードしてJITコンパイルして、パススルー状態を解除する、といった感じでしょうか。公式ドキュメントによると、スクリプトを動的にロードできるプラグインはcmaj::JUCEPluginBase<cmaj::JUCEPluginType_DynamicJIT>で、動的なパラメーターの変更を期待通りにサポートしないDAWが多いのである種の妥協が施されているようです。

ソースプログラムをCmajorと同様のコンテキストで「JITコンパイルする」オーディオプログラミング言語としてはExtemporeなども挙げられます(AOTとして2014年には実現しています)。ReaperのJSFXなどもおそらく同様と筆者は推測します。この方式であれば、C++の長大な静的コンパイルは必要とせず、既にバイナリコードとしてビルドされたCmajorランタイムを共有ライブラリとしてロードするか、プラグインのリリースビルドを公開する場合は静的にリンクする(これは複数の異なるCmajorランタイムが同一のDAWオーディオプロセスでロードされないために必要です)だけでよいことになります。JUCEアプリケーションが一般的にJUCE本体を含め全てをC++ソースからコンパイルして作られることを考えると、デバッグビルドの速度は劇的に向上するといえるでしょう(ビルドというのも当てはまらない気がしますが)。

CmajorはC++経由でネイティブコードを生成するのを避け、LLVM IR経由で直接ネイティブコードを生成するようです。C++コードを生成する選択肢も提供されていますが、C++コンパイラはさまざまな機能に対応するために余計なネイティブコードを生成する必要があり、Cmajorのパッチに対応するネイティブコードはC++ソースからのコードに比べてずっと最適なものが生成できると開発チームは述べています。

もちろん動的にネイティブコードを生成する仕組みはiOSのようなプラットフォームでは実現不能なので、今後iOS上で動作させるようになったら、iOS用には事前コンパイルすることになるでしょう(この意味ではCmajorの仕組みはJITであってAOTではありません)。インタープリターの実行エンジンが開発される可能性もゼロではありませんが、Cmajorのメリットはゼロコストでリアルタイムのオーディオ処理を自然に記述できることにあるので、そのメリットを殺すインタープリターをサポートするメリットはほぼないでしょう。

Cmajorのパッチは直接生成されたネイティブコードで実行されますが、Cmajorのホスト/ランタイムをC++ APIで制御することは可能です。

wasmサポート

Cmajorではネイティブコードだけでなくwasmもサポートされるのですが、これをWebアプリケーションから呼び出す方法については筆者はまだ把握していません。CmajorランタイムをEmscriptenでコンパイルできるのであれば、このAPIを呼び出すコードでJavaScript側でFFIを実現するEM_ASM_*のAPIの何かしらを利用するアプローチが考えられるでしょう(wasmは動的に呼び出せるという前提)。

ちなみにcmaj play --engine=wasmでコンソールから実行する時には何らかのwasmランタイムが使われているはずですが、それが何であるかは不明です。たぶんSoundStacksでforkしているwasm3ではないです。インタープリターだし、1月にforkに修正を加えてから現在までupstreamに100件以上あるcommitsに全く追従していないので、実験してこの路線はナシということになったんだと思います。binaryenもforkしていて、こっちも300commitsくらい追従していないのですが、8月頃までは追従していた様子があるので、バイナリ直接生成モードに相当するようなwasmコードジェネレーターを、binaryenで自前実装している可能性はまあまあなくはないと思っています(LLVM IRからのコード生成に自前コード生成のフックを仕掛けられるのかどうか、筆者には十分な知見がありません)。

とりあえずここまで

前述の通り、cmajorはLinuxサポートが中途半端な状態のままなので、今回試すのはここまでです。WebViewを用いたGUIサポートやプロジェクトファイルの生成についても調べたかったところですが、現状クローズドソースのCmajorに筆者はそこまで価値を見出していないので、もう少し状況が良くなって興味が出てきたら続きを書くかもしれません。

Discussion