Cmajor
概要
https://cmajor.dev/
https://github.com/SoundStacks/cmajor
SOUL言語の主要開発者であるJules StorerとCesare Ferrariが設立した新会社Sound Stacks社による新言語。
Julesは2004年頃からTracktion(現在の Waveform)のために、ほぼひとりでオーディオプログラミングフレームワークJUCEを開発したスーパープログラマー。2014年SeaboardのROLI社がJUCEを買収、JulesもROLI所属となる。その後ADC2018カンファレンスの基調講演で性能、移植性、開発容易性に優れたオーディオ記述言語SOULを発表。
2020年ROLIは経営不振により行政管理下扱いとなりLuminary社として再起を図る。一方JUCE事業はiLokのPACE Anti-Piracy社に買収された。その際Julesも離れており、それ以降JUCEにもSOULにもコントリビュートしていない。1
SOULの開発が止まって以降動向が心配されていた中、JulesはJUCEフォーラム等でSOULに代わる新プロジェクトを予告、2022年11月にADC2022で新言語Cmajorを正式発表し、同時にGitHubのリポジトリも公開された。
ISCライセンス。バイナリで提供されている再配布可能ライブラリはEULAに従う。表現は異なるもののライセンス内容は実質的にSOULとほぼ変わらないように見えます。
実装例
Cmajorの言語設計はSOUL直系であり、わずかな変更でSOULからCmajorになります。同じプログラムをSOULとCmajorで書いた例を示します。
サイン波生成
メイン処理がrun()からmain()になり、出力演算子が<<から<-に変更になった以外SOULと同じです。
processor Sine
{
output stream float out;
float gain = 0.3f;
float theta = 0.f;
float delta = float (440 * twoPi * processor.period);
void main()
{
loop
{
out <- sin(theta) * gain;
theta = addModulo2Pi(theta, delta);
advance();
}
}
}
processor Sine
{
output stream float out;
float gain = 0.3f;
float theta = 0.f;
float delta = float (440 * twoPi * processor.period);
void run()
{
loop
{
out << sin(theta) * gain;
theta = addModulo2Pi(theta, delta);
advance();
}
}
}
SOULと同様マニフェストファイルを用意する必要があります。JSON形式のテキストファイルにアプリケーション情報を記述して拡張子.cmajorpatchで保存します。
{
"CmajorVersion": 1,
"ID": "net.aikelab.sine",
"version": "1.0",
"name": "Sine",
"description": "Sine",
"category": "generator",
"manufacturer": "aikelab.net",
"isInstrument": true,
"source": "sine.cmajor"
}
コマンドラインから実行します。第2引数で指定するのはcmajorpatchファイルです。
> cmajor play sine.cmajorpatch
GUIもSOUL同様簡単です。
processor SineGui
{
output stream float out;
input value float gain [[ name: "Gain", min:0, max:1, init:0.3, step:0.01 ]];
float theta = 0;
float delta = float (440 * twoPi * processor.period);
void main()
{
loop
{
out <- sin(theta) * gain;
theta = addModulo2Pi(theta, delta);
advance();
}
}
}
processor SineGui
{
output stream float out;
input value float gain [[ name: "Gain", min:0, max:1, init:0.3, step:0.01 ]];
float theta = 0;
float delta = float (440 * twoPi * processor.period);
void run()
{
loop
{
out << sin(theta) * gain;
theta = addModulo2Pi(theta, delta);
advance();
}
}
}
実行すると下の画面が表示されGUIから音を操作できるようになります。
サイン波のオシレータやゲイン処理はライブラリとしても用意されています。それらを使ったプログラムは以下のようになります。ライブラリ名はSOULから少し変わっています。graph宣言は、複数のprocessorを部品として扱ってオーディオグラフを構築します。
graph SineLib
{
output stream float out;
node osc = std::oscillators::Sine(float, 440);
node gain = std::levels::ConstantGain(float, 0.3f);
connection osc -> gain -> out;
}
graph SineLib
{
output stream float out;
let osc = soul::oscillators::Sine(float, 440);
let gain = soul::gain::FixedGain(float, 0.3f);
connection osc -> gain -> out;
}
Delayエフェクト
Cmajorはリングバッファ用の機能がいくつか用意されています。たとえばwrap<bufferSize>とした場合、bufferSizeを超えてアクセスしようとすると実際には0~bufferSize-1の範囲にしてアクセスされます。
wavファイルのファイル名は.cmajorpatchの方に書いて、ソースコードではリソース名(今回はvoice)を指定します。
readLinearInterpolatedは、サンプリングデータの再生速度を調整するための補間関数です。
今回はアプリとwavファイルのサンプルレートが異なるときの調整に使っています。
processor Delay
{
output stream float out;
input event float DelayTime [[ name: "DelayTime", min:0, max:1000, init:400, step:1 ]];
input value float feedback [[ name: "Feedback", min:0, max:1, init:0.5, step:0.01 ]];
input value float wetLevel [[ name: "WetLevel", min:0, max:1, init:0.5, step:0.01 ]];
event DelayTime(float delayMs)
{
let delaySamples = max(1, int(processor.frequency * (delayMs / 1000.0f)));
readPos = wrap<bufferSize>(writePos - delaySamples);
}
let bufferSize = 100000;
float[bufferSize] buffer;
wrap<bufferSize> readPos, writePos;
external std::audio_data::Mono voice;
void main()
{
float64 playPos;
float64 addPos = voice.sampleRate / processor.frequency;
loop
{
let dry = voice.frames.readLinearInterpolated(playPos);
playPos += addPos;
if (playPos >= voice.frames.size)
addPos = 0;
buffer[writePos] = dry + buffer[readPos] * feedback;
out <- dry + buffer[readPos] * wetLevel;
++readPos;
++writePos;
advance();
}
}
}
{
"CmajorVersion": 1,
"ID": "net.aikelab.delay",
"version": "1.0",
"name": "Delay",
"description": "Delay",
"category": "generator",
"manufacturer": "aikelab.net",
"isInstrument": false,
"source": "delay.cmajor",
"externals": { "Delay::voice": "../voice.wav" }
}
SOUL言語との差分は以下のとおりです。
19c19
< external std::audio_data::Mono voice;
---
> external soul::audio_samples::Mono voice;
21c21
< void main()
---
> void run()
35c35
< out <- dry + buffer[readPos] * wetLevel;
---
> out << dry + buffer[readPos] * wetLevel;
JUCEを利用したプラグイン作成例
SOUL同様VST3やAUなど各種フォーマットへの出力がサポートされています。出力されたプロジェクトはライブラリとしてJUCEを使用します。
① C++ソースコードを生成
以下のようにすると、.\cppfiles\フォルダにプラグイン用のcppファイルとCMakeLists.txtが生成されます。
JUCEやCmajorインクルードファイルのパスは、Windowsの場合でも「\」ではなく「/」で指定します。
> cmaj generate sine.cmajorpatch --target=plugin --jucePath=C:/lib/JUCE --cmajorIncludePath=C:/lib/Cmajor/include --output=cppfiles
② プロジェクトファイルを生成
CMakeでプロジェクトファイルを生成します。
> cmake cppfiles
③ プロジェクトファイルを開いてビルド
Sine.slnが生成されているので、Visual Studioで開いてビルドします。うまくいけばSine_plugin_artefactsフォルダの下に各種ターゲット向けのバイナリができています。
④ デバッグ
現状(2022年11月時点)はコンパイルすると「C2665 cmaj::Patch::Patch': オーバーロードされた関数ですべての引数の型を変換できませんでした」のエラーが出るようです。
plugin/cmajor_plugin.cppを開いて最終行あたり
(省略)std::make_unique<cmaj::Patch> (true));
の記述をCmajor\include\cmajor\helpers\cmaj_PatchUtilities.hの定義にあわせて以下のように1引数から2引数に修正すればビルドが通ります。
(省略)std::make_unique<cmaj::Patch> (true, false));
⑤ GUIのカスタマイズについて
生成されたCPPプログラムではJUCEのエディタウィンドウを生成しますが、ウィンドウ内の実体はWebViewであり詳細なデザインのカスタマイズはJUCE流ではなくCmajor/choc2の流儀に従うか、もしくはエディタウィンドウをJUCEで全面的に書き換える必要があり現状では簡単ではありません。3 今後GUIエディタを提供予定という話もあるようです。
感想
ほぼほぼSOULです。一度頓挫しかけたプロジェクトを表面上はわずかな変更で再始動した数奇な運命の言語でもあります。権利的に流用できない部分は一から書き直したはずで、幸か不幸か内部実装や仕様はより洗練されたのではないかとも思います。コンパイラまわりのソースコードが公開されていないなど、SOULよりも少しOSS的から商用的な舵取りになったような雰囲気は感じます。
SOULの魂を引き継いだ、今後大注目の言語Cmajorに期待が高まります。
JUCE Advent Calendar 2022
オーディオプログラミング言語 Advent Calendar 2020
-
現在JUCEは名目上Raw Material Software社の管理となっている。Raw Material SoftwareはもともとJulesがTracktion開発のために創設した会社らしく、この会社ごとPACEに買われたようです ↩
-
デフォルトのGUIはcmaj_DefaultGUI.hに定義されています ↩