Exploring Into SuperCollider

少々前よりSuperColliderをいじっている。SuperColliderというのは、あえて説明することもないぐらいの有名どころなのだけど、主に音楽・音響記述に長けたプログラミング言語環境だ。音響系といってもDSPなどのストリームプロセスを書くというよりは、むしろ、ノードの接続を記述してシンセサイザーをつくり、それをコントロールするロジックを記述して動かすというのがコアな部分のようだ(もちろんそれ以外が出来ないというわけではない)。

Maxなどともコンセプトでは似ているところもあるのだけど、決定的に違うのはそれがビジュアルプログラムではなくテキストコーディングで行うものという点ではないかと思う。

わたしがSuperColliderに興味を持ったそもそものきっかけは、SC140というTwitterに書けるだけの分量でSuperColliderで音楽を作ろうという試みがあるということを知ったときだった。そのミニマムな魅力にくらくらっときて自分でもやってみたいと思ったのだった。

SuperColliderは少々取っつきにくいところがあるのだけど、コミュニティのパワーが補って余る。わたしもとても助けられている。特に、@umbrellaprocess氏(on twitter)にはわたしの初歩の初歩な質問にもきちんとレスポンスをいただき、大変感謝している。

SuperColliderのTutorialは、公式のIntroductory Tutorialがよくまとまっていると思う。今見返してみると、よく読めばちゃんと書いてあるということも多い・・・。

わたしはSuperColliderを学ぶ上でまずターゲットに決めたのはUGenというクラスだった。その部分がSuperColliderでシンセサイザ(音響処理も含む)を扱うクラスの基底クラスだったというのがその理由だった。

今回、とりあえず今までに分かったことをまとめてみた。しかし、まだSuperColliderへの冒険の道半ばなので、間違っていることが含まれているかもしれないということは注記しておこうと思う。

アーキテクチャ

SuperColliderの特徴的なアーキテクチャとしてあげられるのは、それがクライアントとサーバに分かれているということではないかと思う。SuperColliderの機能のうちUGen関係はサーバ側におかれている。クライアントとサーバはtcpもしくはudpで通信を行っていて、そのプロトコルはほぼ(bundleのネスト以外)OSC(Open Sound Controll)に準拠している。面白いのはOSC準拠なので別にサーバをSuperCollider以外からアクセスしてもかまわないということだ。

UGenを組み合わせたものがノードであり、ノードは通常Nodeクラスの派生クラス、Synthクラスを扱う。以下、Synthクラスのインスタンスをシンセサイザノードと表現している。

UGenを組み合わせ構成したシンセサイザはSynthDefによって明示的に、もしくは暗黙のうちに(play methodを使った場合など)SynthDef binary formatに変換される。これをサーバにロードしたとき、サーバの内部でシンセサイザノードとして実体化する。

関数と引数

さて、ここで一度SuperColliderの言語のシンタックスに触れておこうと思う。SuperColliderの関数は{}で囲まれたブロックで記述し、クロージャも可能であり多用される。引数はブロックの先頭にargに続いて宣言する(Rubyタイプの||記述もできる)。引数のデフォルト値も設定できる。

f = { arg a,b; a + b;  };

関数はFunctionクラスのインスタンスで、その実行にはvalueメソッドを使う.。引数の指定の仕方はCスタイルの方法や、Rubyのようにキーワード指定での方法、もしくはその二つをミックスした方法が可能だ(ただしキーワード指定の方が実行速度的にオーバーヘッドがあるという注意書きがある)。

f.value(1,2);
f.value(a:2, b:2);
f.value(1,b:2);

他にも関数の実行方法がある。SuperColliderにはシンボルというリテラルがあるのだけど、それを使用した実行方法だ。

v = ();
v.put(\a,1);
v.put(\b,1);
f.valueWithEnvir(v);

シンボルを利用した実行方法は以前はもっと簡単なメソッド(performKeyValuePairs)があったのだけど、無くなったみたいだ。この機能はSynthDefの第2引数、ugenGraphFuncへの引数がSysthクラスのコンストラクタでシンボル、値のペアで渡す事への整合性の名残だと思うのだけど。現在の実装ではSynthクラスのコンストラクタではペアの配列をOSCのデータに直接変換してサーバに投げている(setメソッドの実装も同様)。

*new { arg defName, args, target, addAction=\addToHead;
var synth, server, addNum, inTarget;
...
server.sendMsg(9, //"s_new"
defName, synth.nodeID, addNum, inTarget.nodeID,
*(args.asOSCArgArray)
);

オーディオレートとコントロールレート

再びUGenに話を戻して、このクラスの派生クラスには重要なクラスメソッドが実装されている。*arと*krというインスタンスを生成するクラスメソッドだ。基底クラス(UGen)にこのメソッドがないのはSuperColliderの言語仕様に純粋仮想関数というものが存在しないからかもしれない。

arはaudio rateの略でkrはcontrol(kontrol) rateの略のようだ。その違いは、

Control rate unit generators are used for low frequency or slowly changing control signals. Control rate unit generators produce only a single sample per control cycle and therefore use less processing power than audio rate unit generators.

コントロールレートのユニットジェネレータは低周波数もしくは低速変化なコントロール信号として使われる。コントロールレートのユニットジェネレータは1コントロールサイクル毎にただ1つだけのサンプル信号を生成し、それ故に使用するプロセッサパワーはオーディオレートのユニットジェネレータに比べて少ない。


Instantiation. Audio Rate, Control Rate / Unit Generators and Synths

というように説明されている。

それでは、具体的にコントロールレートのサンプリング周波数はどれぐらいのものなのだろうか?ざっと当たってみたところそれらしい記述がドキュメントに見あたらなかったため、SuperColliderのソースを当たってみた。

まず、SC_GraphDef.cppの218行目あたり

switch (unitSpec->mCalcRate)
{
case calc_ScalarRate :
unitSpec->mRateInfo = &inWorld->mBufRate;
break;
case calc_BufRate :
graphDef->mNumCalcUnits++;
unitSpec->mRateInfo = &inWorld->mBufRate;
break;
case calc_FullRate :
graphDef->mNumCalcUnits++;
unitSpec->mRateInfo = &inWorld->mFullRate;
break;
case calc_DemandRate :
unitSpec->mRateInfo = &inWorld->mBufRate;
break;
}

これを見ると、FullRate(つまりオーディオレート)以外はmBufRateというものが適用されるということが分かる。さて、ではmBufRateがどういったものなのかということなのだけど、SC_World.cppの757行目。

Rate_Init(&inWorld->mBufRate, inSampleRate / inWorld->mBufLength, 1);

inBufLengthはscsynthの引数でblock-sizeで指定されている値で、デフォルトでは64となっている。

つまり、コントロールレートはブロックサイズを指定していない通常の場合、オーディオレート(=デバイスのサンプリングレート)の64分の1となる。サンプリングレートを44.1KHzとした場合、コントロールレートは約689Hz、サンプリング定理により約344Hzがコントロールレートで使用できる周波数の上限値となる。LFOとして使う場合は足りないことがあるかもしれない。

要するに、引用したところに書いてある通り、1コントロールサイクル(=オーディオバッファのブロックサイズ)につき1つのサンプル値を算出するということだ。

UGenインスタンスの生成

UGen系のクラスのインスタンス生成(ar,kr)には一つ重要なお約束ごとがある。それは、「引数に配列を渡した場合、その数(チャンネル)分のユニットジェネレータが生成される。その場合、戻り値はユニットジェネレータの配列である」ということだ。それは最終的に呼び出されるUGenクラスのクラスメソッド、multiNewListの実装を見れば分かる。

*multiNewList { arg args;
var size = 0, newArgs, results;
args = args.asUGenInput(this);
args.do({ arg item;
(item.class == Array).if({ size = max(size, item.size) });
});

引数に渡された配列のうち一番大きいものの要素数分生成するということのようだ。

SinOsc : UGen {
*ar {
arg freq=440.0, phase=0.0, mul=1.0, add=0.0;
^this.multiNew('audio', freq, phase).madd(mul, add)
}

ユニットジェネレータの生成メソッドにおける引数は、左図のように、それぞれのユニットを接続するための接続ポイントと見なすことが出来る。それぞれの引数に他のユニットジェネレータを渡すことにより、ユニット間を接続する。それをビジュアル的に表現すると、QuartzComposerやMax的なものになる。

ユニットジェネレータ間の接続はインスタンスの生成時に行われるので、その処理手順上ループは発生しない(インスタンスが実体化したときにはその上位の接続が確定してしまう)。ループが作れないということはフィードバックが作れないということになってしまうのだけど、その点はIn/OutというUGenの派生クラスが可能にしている(他のUGenの派生クラスでも可能だろう)。

In/OutはSuperColliderのバスそのものだ。InとOutに分かれているのだけど、内部的に指しているバッファはIDが同じならば同じとなる。Outのオーディオバスは、つなげるとそのデータはオーディオデバイスに出力される。しかし、オーディオバスはオーディオデバイスの持っているチャンネル数以上のIDは出力されないという実装になっている。どこまでが出力されてどこからが出力されないというのはその環境による。ある程度上位のIDのIn/Outをフィードバックバッファとして使用することは可能だろう。SuperColliderではデフォルトでオーディオバスは128、コントロールバスは4096用意されている。

チュートリアルにあるBusesではIn/Outを介在してバス間のオーディオデータのやりとりをしているのだけど、実はSuperColliderはノードのID順に実行されるといった規約は存在しない。生成されたシンセサイザノードは他のノードと依存関係のないものはサーバ内部のWorldクラスのmTopGroupというGroupクラスのインスタンスの子ノードとして存在しているのだけど、Groupクラスのノード評価のコードは以下の通りで、リストとしてつながってるものをただ順に評価しているだけだ。

void Group_Calc(Group *inGroup)
{
Node *child = inGroup->mHead;
while (child) {
Node *next = child->mNext;
(*child->mCalcFunc)(child);
child = next;
}
}

一応生成された順には並ぶようだ。その点でいえば評価の順序は古いものからとなるかもしれない。ただし、そうでない実装(UGen Plugin等の)も可能となっている。

シンセサイザノード内のUGenの評価順序に関しては規則は明確で、必ず依存関係で上流になるものから評価される。