Exploring Into SuperCollider 3

前回、前々回でUGenという出口から始まり、ようやくクライアント側からサーバ側への橋がみつかった。いままでの道のりを図示すると左図のようになるだろうか。

今回見ていくのはもう一つの橋、Synthクラスだ。

シンセサイザノード

SynthDefによりUGenの組み合わせによるシンセサイザーの設計図をSynthDef binaryにしてサーバに送り込んだら、次はそれをシンセサイザノードに実体化しないといけない。

サーバにはこのためのコマンドがあり、それが/s_newだ。/s_newの説明はSuperCollider Server Synth Engine Command Referenceに詳しく解説があるのだけど、

/s_new						create a new synth
string - synth definition name
int - synth ID
int - add action (0,1,2, 3 or 4 see below)
int - add target ID
[
int or string - a control index or name
float or int - a control value
] * N

最初のsynth definition nameはSynthDefで指定したシンセサイザの名前だ。名前は一つのSuperColliderサーバ空間内でユニークである必要がある。ネームスペースなどはないため、うっかりとバッティングすることもあるかもしれない。

では、さて、シンセサイザの名前がバッティングした場合どういうジャッジが行われるのだろうか?

これは簡単に予想できる通り、新しいSynthDef binaryで古いSynthDef binaryが上書きされる。後着優先ルールだ。

ただし、SynthDef binaryは言うなればシンセサイザノードのメタクラスなので、それが変わったからといって既に生成されているインスタンスが変わってしまうということはない(既に作成されたシンセサイザノードは変更されない)。

synthIDには作成するシンセサイザノードのIDを指定する。この値が-1だった場合にはサーバが自動的にIDを生成する。ここで一つサーバ内にお約束があって、シンセサイザノードを操作する他のコマンドで対象のIDが-1だった場合、一番最後に作成されたシンセサイザノードのIDを参照するということだ。

さきほどSynthDef binaryの名前がバッティングした場合はどうなるかというのを見たのだけど、synth IDがバッティングした場合はどうだろうか。それは、サーバのソースを見れば分かる。SC_Node.cppのNode_Newの先頭、

int Node_New(World *inWorld, NodeDef *def, int32 inID, Node** outNode)
{
if (inID < 0) {
if (inID == -1) { // -1 means generate an id for the event
HiddenWorld* hw = inWorld->hw;
inID = hw->mHiddenID = (hw->mHiddenID - 8) | 0x80000000;
} else {
return kSCErr_ReservedNodeID;
}
}
if (World_GetNode(inWorld, inID)) {      // 指定されたIDが既に存在するかをチェック
return kSCErr_DuplicateNodeID;   // 存在した場合はエラー終了
}

ノードを作成するときに指定IDが既に存在するかをチェックし、存在した場合はエラーを返すという実装になっている。前半部分に先ほどのIDに-1を指定した場合の処理が書かれている。

add target IDというのは、add actionの操作の対象ノードの指定だ。scserver内には初期的にTopGroupというグループノードのみが存在し、そのIDは0になっている(sclangの環境ではまた違うのだけど、それは後述する)。なので、ノードを追加したいけどそのためにはノードが必要という循環依存関係は回避されている(少なくともTopGroupのIDを指定することが出来る)。

add actionはどういう風に追加するかだ。追加方法は次の5種類ある。

0では指定されたグループの先頭に、1では指定されたグループの最後尾に、2では指定されたノードの直前に、3では指定されたノードの直後に、4では指定されたノードと入れ替えて、元あったノードは削除される。操作から見ても分かるように、0と1では指定するターゲットノードの種類はグループでないといけない。

前々回に見たとおり、ノードの順序は実行順序に関係があるということには注意しておく必要があるかもしれない。

/s_newコマンドの残りのパラメータはSynthDefで指定したコントローラに設定する初期値となる。もし必要無い場合は省略できる。

/s_newコマンドは4つ以上のパラメータを持つのだけど、実は最低限必要なのは最初の二つだけだ。これは/s_newのサーバでの実装、

SCErr meth_s_new(World *inWorld, int inSize, char *inData, ReplyAddress* /*inReply*/)
{
...
int32 nodeID = msg.geti();
int32 addAction = msg.geti(); // !!
GraphDef *def = World_GetGraphDef(inWorld, defname);
if (!def) {
scprintf("*** ERROR: SynthDef %s not found\n", (char*)defname);
return kSCErr_SynthDefNotFound;
}

msg.geti()でint型のパラメータを取得しているのだけど、この関数はOSCパケットにそれ以上データがない場合(例えばパラメータが2つしかないパケットに対して3つめを取得しようとしてる場合)には0が返ってくる仕様となっている。/s_newのコマンドの3つめ4つめにこの処理が適用されるとすれば、addAction=0でグループの先頭に追加、そしてターゲットグループID=0は前述のTopGroupになるからだ(ターゲットノードIDの取得部分のソースはこの範囲にはないが同じ処理を行っている)。

Synthクラス

サーバとのシンセサイザノード関係のやりとりをラップするSuperColliderの言語側でのクラスだ。このクラスに対して操作を行うと、それは透過的にサーバへのコマンドに変換され送信される。RPC(Remove Procedure Call)ということだ。

例えば、Synthオブジェクトにパラメータを設定するメソッド、setはSynthクラスの基底クラス、Nodeクラスでこんな風に実装されている。

set { arg ... args;
server.sendMsg(15, nodeID, *(args.asOSCArgArray));  //"/n_set"
}

Synthクラスはご存じのとおりとても簡単な手続きでシンセサイザノードを作成することが出来る。簡潔に書こうと思うとSynth(“sine”)のような記述で済んでしまうのだけど、その中ではどのような処理が行われているのだろうか。

前項で/s_newの動作を見てきた。/s_newには最低二つパラメータが必要だ。ひとつが新しく作るシンセサイザノードのID、もう一つがSynthDef binaryの名前となる。IDは-1だった場合自動的に割り振られるというお約束を思い出すと、ではSynthクラスのコンストラクタでIDを指定しない場合は-1を渡してるのではないか、と一瞬考えるのだけど、それではうまくいかない。

なぜなら、-1でサーバで自動的にIDを割り振った場合、クライアント側ではそれが何番に振られたか分からないので継続的使用が出来なくなってしまうのだ。もちろん、シンセサイザノードが1つしかない場合には、前述のお約束で-1でアクセスできる。しかしSynthクラスにはそういう制約はなさそうだ。ではどうやって実現してるのだろうか。

※実はSynthクラスにも作りっぱなしで後は放置しても良い(言語側からのアクセスは考えなくても良い)という用途のシンセサイザノードを作成するgrainというメソッドがある。Node.scにあるgrainの実装を見てもらえば分かるようにこのクラスメソッドの戻り値はnilで以降の継続操作を想定していない。

ということで、Synthクラスのコンストラクタを見てみる。

*new { arg defName, args, target, addAction=\addToHead;
var synth, server, addNum, inTarget;
inTarget = target.asTarget;
server = inTarget.server;
addNum = addActions[addAction];
synth = this.basicNew(defName, server);
if((addNum < 2), { synth.group = inTarget; }, { synth.group = inTarget.group; });
server.sendMsg(9, //"s_new"
defName, synth.nodeID, addNum, inTarget.nodeID,
*(args.asOSCArgArray)
);
^synth
}

おおざっぱに言って、いろいろと設定をして最後にサーバへ/s_newを送っている。

新しく作るノードのIDは/s_newの二つ目のパラメータなので、そこを見てみるとsynth.nodeIDとなっている。synthはというとその前にthis.basicNew()で作成されている。既にSynthクラスのインスタンスは生成されていて、IDもまた決まっているようだ。

basicNew()は最終的にSynthクラスの基底クラス、NodeクラスのbasicNewメソッドがよばれる(SynthクラスのbasicNewはスーパークラスのbasicNewを呼んだ後にインスタンス変数defNameを設定しているだけだ)。

*basicNew { arg server, nodeID;
server = server ? Server.default;
^super.newCopyArgs(nodeID ?? { server.nextNodeID }, server)
}

nodeIDが指定されていない(=nil)場合、ServerクラスのnextNodeIDというメソッドでIDを取得している。そして、どんどん辿る。ServerクラスのnextNodeIDの実装。

nextNodeID {
^nodeAllocator.alloc
}

nodeAllocatorとは、

initTree {
nodeAllocator = NodeIDAllocator(clientID, options.initialNodeID);

このNodeIDAllocatorというクラスが結局のところIDを割り振ってるようだ。

さて、このNodeIDAllocatorクラスの実装はEngine.scにある。IDを生成するメソッド、allocの実装はというと、

alloc {
var x;
x = temp;
temp = (x + 1).wrap(initTemp, 0x03FFFFFF);
^x | mask
}

tempは指定しない場合初期値1000で、毎回1ずつ足したものをIDとして返している。この番号は見覚えのあるものではないかと思う。最大値は0x3ffffff(67108863)でそれを超えるとラップアラウンドする。おそらくそんなにIDを使う事なんて無いだろうけど。

ここでなぜ最大値が0x7fffffffでないかというと、実はSuperColliderは最大32ユーザまでの同時操作を想定してるのだ(クライアント側の実装として)。符号を除いた31ビットのうち上位5ビット(0〜31)をユーザー識別子として最後にorしたものがIDとして返ってくる。allocの最後でorされているmaskというのがそれだ。

使うID空間を最後にorするフラグで分けて、1つのサーバで同時に32人までのプレーヤでセッションを行うことが出来るというわけだ。その指定はどこで行うかというと、Serverクラスのコンストラクタ、

*new { arg name, addr, options, clientID=0;
^super.new.init(name, addr, options, clientID)
}

この4番目のパラメータ、cliendIDに入れれば良い。32以上の値を入れるとNodeIDAllocatorのコンストラクタがエラー終了(nil)する。マイナスの値はチェックされてないのでやめておいた方が無難そうだ。

IDがどうやって取得できたか分かったところでSynthクラスのコンストラクタに戻る。

サーバに送っている/s_newのパラメータのうち3つめのaddNumはメソッドのパラメータとしてはデフォルトでは\addToHeadが指定されていて、これがaddActionsというDictionaryに格納されている対応よりadd actionの番号に変換される。ここには番号をキーとした値も格納されているので、引数としてはシンボルでも数字でも通るようになっている。

そして4つめのtarget IDの部分はinTarget.nodeIDとなっている。inTargetはtarget.asTargetなのだけど、これはデフォルトパラメータがないので指定しない場合はnilになっているはずだ。と、いうことで、NilクラスのメソッドasTargetを見てみる。このメソッドはasTarget.scにあって、後から追加されている形になっている。

asTarget { ^Group.basicNew(Server.default, 1) }

これはID=1のグループノードということになる。つまり、何も指定しなかった場合、シンセサイザノードはこのGroupノードの下にぶら下がることになる。

ところで、ID=1のグループノードなんて誰がいつ作ったのだろうか?サーバで初期的に作られるのはID=0のグループノード、TopGroupだけだったはずだ。

実はこれは、先ほど出てきたServerクラスのメソッドinitTreeの続き部分にその答えがある。

initTree {
nodeAllocator = NodeIDAllocator(clientID, options.initialNodeID);
this.sendMsg("/g_new", 1);
tree.value(this);
ServerTree.run(this);
}

見てのとおり、ID=1を指定してグループノードを作成している(サーバに/g_newコマンドを発行)。一つ注目すべきポイントは、/g_newの2つめ3つめのパラメータが省略されてることだ。ここも前述の省略された場合は0というお約束が生きている(add action=0、target ID=0)。

ではこのinitTreeはいつ呼ばれるのかというと、bootメソッドにおいて、

boot { arg startAliveThread=true, recover=false;
...
this.doWhenBooted({
...
serverBooting = false;
this.initTree;

サーバが起動したら呼ばれるようになっているのだ。サーバを起動すると(Server.boot)ID=1のグループノードを作成して、以降その下でシンセサイザノードを扱うというお約束になっているようだ。

さてSynthクラスだけど、そのsclangの環境外から、例えば別のユーザのsclangとかサーバに直接コマンドを送って作ったノードを操作したい場合はどうすればいいのだろうか(多クライアント対1サーバでセッションを行ってるときに他のプレーヤの作ったシンセサイザノードに乱入する、とか)。

それには通常のコンストラクタではなくbasicNewの方を使う。このメソッドはSynthクラスのインスタンス生成は行われるのだけど、サーバに/s_newの発行は行われない。ノードIDは整数でのIDを使用する。Synthクラスのメソッドを使う必要がなければ、直接Nodeクラスのインスタンスとして作成しても良いだろう。

ただ、このインスタンスはノードに対するアクセスを提供するだけで、そのノードがどういう関係性にあるかは再現されない(そのノードを作ったユーザの環境で、そのノードがどのノードの下にぶら下がってるか、など)。

※調べられないというわけではなく、自動的には再現されないということ。

環境外で作ったものでなくても、自分の環境で作ったシンセサイザノードでも、この方法でインスタンスを生成することが出来る。そしてそれは例え環境内にあったとしても(そのIDのSynthオブジェクトが存在したとしても)、元のオブジェクトとのマッチングは行われない(別オブジェクトとなる)。親子関係などが反映されないのも同じだ。

ところで、ID参照型の(/s_newを送信しない、basicNewで作成した)オブジェクトにデフォルトパラメータでfreeメソッドを呼ぶと、サーバに/n_freeが送信され、サーバ空間で解放されていなくなってしまうので注意が必要だ。freeの引数にfalseを渡してやれば/n_freeの送信は行われないという実装になっている。