前回はUGenを中心としてSuperColliderの世界への探求を試みてみた。今回はその上位構造となるSynthDefを見ていこうと思う。
SuperColliderはクライアントサーバ型アーキテクチャであることは既出のことなのだけど、SynthDef、そしてSynthがこの二つの間にかかった橋だと言えるだろう。UGenは確実に向こう側の世界だ。
お約束だけど、まだSuperColliderへの冒険の道半ばなので、間違っていることが含まれているかもしれないということは注記しておこうと思う。
向こう側とこちら側
SuperColliderのサーバで出来ることは、簡単に言ってしまうとUGenを組み合わせてシンセサイザノードを作成/破棄することと、コントロールデータを受け取ってシンセサイザノードに設定することだけだ。
例えば、
SinOsc.ar(440.rand())
等と記述しても、オシレータの周波数がランダムに変更されつつ出力されるなんて事は起きない。このSinOscは0から440のどれかの周波数でつねに出力しつづけるだけだ。それは、クラスの定義的にこれがコンストラクタであり、rand()が毎回評価されるわけではない(インスタンス生成時に一度だけ評価される)という事とは別に、サーバはrand()というものを評価することが出来ないという事実がある。サーバはSuperColliderの言語的な仕様部分とは全く無関係な存在なのだ。
つまりSuperColliderの言語を用いてUGenのシグナルプロセッシングの部分を記述することは出来ない(サーバはその言語が理解できない)。逆(クライアント上でUGenを使用する)も基本的には使うことが出来ないのだけど、クラス内に同じロジックを記述することにより見かけ上動いてるようにすることは可能だ。
UGenというのはいったい何かというと、その実体はSuperColliderのPluginsディレクトリ内にあるscxというファイル群であり、それはSuperColliderではなく、OSのネイティブコードバイナリであるシェアードライブラリだ。
わかりやすく言うと、それはVSTやAUのプラグインと同じということだ。サーバはプラグインを指示通りにつなぎ合わせてそれをシンセサイザノードとする。VSTやAUホスト上でユーザが手でバスにプラグインを挿していってるようなことに似ている。
SuperColliderの言語側で定義されているUGenのクラスは、それをどうつなげるかを表現するためだけに存在する。それをつなげてインスタンス化したオブジェクトの中身は実のところ空なのだ。それはPluginsのどのプラグインであるかは指し示すのだけど、どういう処理をするかはそこにはない。
クライアントからサーバへ
ではクライアントで定義したシンセサイザの構造をどうやってサーバに理解させるかと言うことなのだけど、ここでSynth Definition File Formatというバイナリフォーマットが出てくる(以下SynthDef binary)。
ここに記述されてある情報はサーバに伝わるし、されていない情報は伝わらない。そんなに複雑なフォーマットでないので、ざっと見ても分かる通り、そこに記述されるのは、あらかじめ決まった定数、入力されうるパラメータの名前とそのデフォルトの値、構成するUGenの名前とその入力出力の接続関係。そのぐらいでしかない。
サーバはこのファイルを介してシンセサイザノードを構築するための設計図を手にする。
サーバには直接のこのバイナリを受け取るコマンド(/d_recv)、ファイルから読み込んでくる(/d_load)といったコマンドが実装されている。そのコマンドはOSC(Open Sound Control)プロトコルでサーバに送る。
さて、それではこのSynthDef binaryを誰が作るかと言うことなのだけど、それがSynthDefというクラスというわけだ。ずいぶん遠回りなってしまったけど。SynthDefはSynthDef binaryの作成者と言うよりは、SuperColliderの言語内でのSynthDef binaryを表現するものと考えた方が良さそうだ。
SynthDefインスタンスの生成
よく出てくるSynthDef()と表記するのは実はSynthDefのコンストラクタだ。*newが呼ばれるわけだけどSynthDef.newのnewは省略することが出来る。その実装を見てみると、
*new { arg name, ugenGraphFunc, rates, prependArgs, variants, metadata; ^this.prNew(name).variants_(variants).metadata_(metadata) .build(ugenGraphFunc, rates, prependArgs) }
prNewは実質Objectのコンストラクタ(new)を呼んで名前を設定しているだけだ。variants_みたいな表記はSuperCollider独特の表記法なんだけど、クラス/インスタンス変数の後にアンダースコアをつけた形のメソッドはその変数に対するセッターを表現するというお約束ごとがある。イコールによる代入と動作は同じだ。
渡された引数のうちインスタンス変数にセットするものは設定した後に、buildが呼ばれる。buildの実装はこうなっている。
build { arg ugenGraphFunc, rates, prependArgs; protect { this.initBuild; this.buildUgenGraph(ugenGraphFunc, rates, prependArgs); this.finishBuild; } { UGen.buildSynthDef = nil; } }
protectは制御構造ではなく、Function classのメソッドだ。SynthDefのbuildは全スレッドでただ1つしか実行できないという実装になっているようだ。
buildの流れはメソッド名を見れば分かればすぐ分かるように、初期化した後にUGenの接続グラフを構成し(buildUgenGraph)、そして終了処理を行う。ここでグラフの最適化や入出力関係のチェックも行っているようだ。
buildUgenGraph { arg func, rates, prependArgs; .... prependArgs = prependArgs.asArray; this.addControlsFromArgsOfFunc(func, rates, prependArgs.size); result = func.valueArray(prependArgs ++ this.buildControls); .... }
prependArgsはnewで渡された引数だ。流れとしてはfuncとして渡された関数の引数をコントロールとして登録し、その後にfuncを実行してUGenグラフを構成するということだ。
前回にも出てきたのだけど、SuperColliderにおける関数というものはFunctionクラスのインスタンスなのだけど、それは{}で囲まれたブロックとして表現される。関数の引数はブロックの先頭でargに続き宣言された変数か、rubyのブロックのように||で囲まれた中に宣言された変数だ。そして、それは文字列として取り出すことが出来る。
addControlsFromArgsOfFuncの先頭でもそういうことを行っている。
addControlsFromArgsOfFunc { arg func, rates, skipArgs=0; var def, names, values,argNames, specs; def = func.def; argNames = def.argNames;
funcはSynthDefのnewで渡されたugenGraphFuncなのだけど、その関数の引数がすなわちコントローラとして登録されるという処理が行われているわけだ。
さて、なぜSynthDefのbuildが全スレッドで一つしかダメかという理由なのだけど、UGenの最終的に呼び出されるコンストラクタであるnew1の実装、
*new1 { arg rate ... args; if (rate.isKindOf(Symbol).not) { Error("rate must be Symbol.").throw }; ^super.new.rate_(rate).addToSynth.init( *args ) }
および、そのメソッドが呼び出しているaddToSynth、
addToSynth { synthDef = buildSynthDef; if (synthDef.notNil, { synthDef.addUGen(this) }); }
buildSynthDefはUGenクラスの定義の先頭にある。
UGen : AbstractFunction { classvar <>buildSynthDef; // the synth currently under construction
実は、SynthDefのインスタンスメソッド、initBuildの先頭にそのものずばりな答えがあるのだけど、
initBuild { UGen.buildSynthDef = this;
つまり、build以降に生成されたUGenのインスタンスはUGenのクラス変数にあるbuildSynthDef(つまり呼び出した元のSynthDef)に登録されるのだ。網を張っておき、UGenグラフを構成する関数ブロックを実行する。すると、網にはそのブロックで生成したUGenのインスタンスがかかっているので、それぞれのUGenの接続具合を調べる。この処理を行うために同時に1つしかSynthDefのbuildが出来ないという規約が生じている。この実装はなかなか興味深い実装じゃないかと思う。
SynthDefを後から変更する
SynthDefはインスタンスを生成したときにbuildされて、すぐにSynthDef binaryとして使える状態になっているわけだけど、これはロックされた(C++でいうとconstなオブジェクト)状態かというとそうではないのが面白いところだ。SynthDefの実質的な意味はシンセサイザノードの設計図でしかない。build内で使用されるaddUGen以外にもこんなメソッドも用意されている。
replaceUGen { arg a, b; children.remove(b); children.do { arg item, i; if (item === a and: { b.isKindOf(UGen) }) { children[i] = b; }; item.inputs.do { arg input, j; if (input === a) { item.inputs[j] = b }; }; }; }
組まれたUGenグラフのうち、あるUGenを別のUGenに置き換えるというものだ。引数は最初が置き換えたい元々あるUGen、次が新しいUGenの順になる。
処理を見てみたら分かるように、ちゃんとinput/outputもつなぎ直してくれる。ただ、UGenのinput/outputは実は内部的にはその順番しか意味がないので、パラメータ順番が同じUGen同士でないと問題が起こることは容易に想像できる。
新しいUGenでreplaceUGenした場合にはしておかないといけないことがある。それは、まず始めに新しいUGenのオーナーをセットすることだ。新たなUGenのsynthDefというインスタンス変数に追加しようとしているSynthDefインスタンスをセットしておく必要がある。
もう一つがreplaceUGenの実装の部分。
children[i] = b;
元あったaの場所にbを代入しているわけだけど、そのときbのindexを操作していない。ここは、
children[i] = b; b.synthIndex = a.synthIndex;
とすべきなのではないかと思う(aはいなくなってしまうので)。なので、replaceUGenした時にbのsynthIndexにaの値を入れてやる必要がある。もしくは、SynthDefのインスタンスメソッドindexUGensを呼べばSynthDefインスタンス内のUGenのindexがすべて割り振り直される。indexの問題は、replaceUGenするまえに新しいUGenインスタンスをSynthDefインスタンスにaddUGenすることによっても回避できる。
この二点は、実は前出のUGen.buildSynthDefを新しいUGenのインスタンスを生成する前にセットしておけば自動的に行われる。ただ、この場合もbuildSynthDefがスレッドセーフでないこと、終わった後にnilを代入することを忘れないことに注意する必要がある。
そして、締めくくりは、新しいUGenインスタンスに対してメソッドcollectConstantsを呼ぶことだ。SynthDef binaryのフォーマット的な問題だけど、すべての定数を事前にリストアップする必要があるのだ。このメソッドは前二つの処理のあと、特にオーナーをセットした後でないといけない。
ずいぶんと手順が面倒なのだけど、それ以外に、SynthDefのインスタンスメソッドfinishBuildを呼ぶという手もある。これは一つ目のオーナーをセットするということ以外は全部処理してくれる。ついでに最後にUGen.buildSynthDefにnilを代入しているところが判断の分かれるところだ。
その操作が同時にほかのSynthDefのbuildが行われないという保証が出来るなら、
UGen.buildSynthDef = targetSynthDef; // UGenの生成・replaceなど targetSynthDef.finishBuild;
が最も簡単な方法になるんじゃないかと思う。
こちら側からあちら側へ
SynthDefのインスタンスが出来たということは、すなわちSuperCollider的表現のSynthDef binaryができたということだ。それをサーバ上で有効にするためにはサーバに送らないといけない。それはSynthDefのメソッドにそのものずばりなメソッドがある。
send { arg server,completionMsg; server.listSendBundle(nil,[["/d_recv", this.asBytes,completionMsg]]); }
見ての通り、自分自身(SynthDef binary)を/d_recvでサーバに送信するという処理に他ならない。
ほかにもwriteDefFileというメソッドがある。これはSynthDef binaryをファイルに書き出すというメソッドだ。書き出した後にサーバ側で/d_loadすればsend同様にサーバ側でSynthDef binaryを読み込むことが出来る。
その二つを同時に行うメソッドがloadだ。loadはwriteDefFileを呼び出した後に、サーバに対してそのファイルを/d_load、すなわちロードするように要求する。