Exploring Into SuperCollider 5

前回は実際にサウンドの生成を行っているサーバscsynthを離れて、クライアントであるsclang側でのシンセサイザコントロールのさわりの部分を見てみた。

SuperColliderにはClockというクラスがあり、その機能はトリガーをかけて欲しい時間を登録しておき、トリガーのタイミングで行いたい処理を実行すれば良いという仕組みになっていた。トリガーは一度かかると解除されるので、繰り返し行いたい場合は処理の終了時に再度トリガーを登録しておく必要がある。

そのClockクラスの主要な派生クラスの一つがTempoClockであり、名前の通りテンポによる時間管理を行うクラスになっている。主要というのは、このクラスにはdefaultというクラス変数があり、クラスがロードされたときに暗黙にそこにインスタンスが生成される。そして、いろんな演奏に関するクラスがClockに関するパラメータのデフォルト引数(引数が明示的に指定された無かった場合も)としてTempoClock.defaultを使用しているからだ。

TempoClockにはプロパティtempoがあるのだけど、ではこのtempoのパラメータの単位はなんだろうか。メトロノームと同じくBPMでいいのか、はたまた違うのか。初期的に設定されているテンポはどれぐらいの速さなのだろうか。

今回はTempoClockに注目してみようと思う。

テンポの設定

まずはClock.scにあるTempoClockの定義の頭の部分を見てみよう。SuperColliderのクラス変数およびインスタンス変数の宣言は冒頭の部分でしか行うことが出来ない。つまり、頭の部分を見ればそのクラスがどんな変数を持っているかが分かる。

TempoClock : Clock {
classvar <all, <>default;
var <queue, ptr;
var <beatsPerBar=4.0, barsPerBeat=0.25;
var <baseBarBeat=0.0, <baseBar=0.0;
var <>permanent=false;

classvarで宣言されているものがクラス変数で、varで宣言されているものがインスタンス変数だ。変数名の頭についてる<および>はその変数の外部からのアクセス制限を表していて、<がread、>がwriteとなっている。<>の形でread/writeと覚えるのが覚えやすいかもしれない。

さて、TempoClockクラスの変数宣言は引用した部分のとおりなのだけど、ぱっとみたところtempoという名前の変数がないことに気付くのではないだろうか。無いとはいえ、たとえば、

TempoClock.default.tempo = 1.0;

などと実行してみても分かるようにtempoというプロパティは設定できる。これはSuperColliderにはプロパティのsetter/getterメソッドを定義することができるからだ。setterメソッドはプロパティ名+アンダースコアという命名規則がある。tempoはどうかというと、こんな風に実装されている。

tempo_ { arg newTempo;
this.setTempoAtBeat(newTempo, this.beats);
this.changed(\tempo);  // this line is added
}

そしてsetTempoAtBeat。

setTempoAtBeat { arg newTempo, beats;
_TempoClock_SetTempoAtBeat    // 1
^this.primitiveFailed
}

アンダースコアで始まる識別子(1)はおなじみのプリミティブであり、内部関数が呼ばれることになる。

実際に呼ばれるのはPyrSched.cppにあるprTempoClock_SetTempoAtBeatだ。この関数ではTempoClockクラスの実体であるC++のクラスTempoClockのSetTempoAtBeatを呼んでいる。ところでこのprTempoClock_SetTempoAtBeatの頭の方にこんな記述がある。

int prTempoClock_SetTempoAtBeat(struct VMGlobals *g, int numArgsPushed)
{
PyrSlot *a = g->sp - 2;
PyrSlot *b = g->sp - 1;
PyrSlot *c = g->sp;
TempoClock *clock = (TempoClock*)a->uo->slots[1].uptr;  // 1

SuperColliderのVMはメソッド呼び出し方法がCと同じく引数をスタックに積んでコールするという実装になっている。このメソッドの引数は2つなのだけど、先にレシーバをスタックに積んでるあたりはC++と同じといった方が良いかもしれない。

aはレシーバオブジェクトなので、TempoClockのインスタンスになる。そのオブジェクトのslotsの2番目がC++実装のTempoClockオブジェクトへのポインタとなってることになる(1)。

さて、SuperColliderのTempoClockの実装の冒頭のところを再び思い出してもらって、2つめのインスタンス変数はなんだったろうか。それはアクセス許可子なしのptrだ。ここに実は内部オブジェクトのポインタが格納されているというわけだ。

prTempoClock_SetTempoAtBeatは、最終的には内部C++クラスTempoClockのSetTempoAtBeatを呼び出していて、その実装はこんな風になっている。

void TempoClock::SetTempoAtBeat(double inTempo, double inBeats)
{
mBaseSeconds = BeatsToSecs(inBeats);  // 1
mBaseBeats = inBeats;                 // 2
mTempo = inTempo;                     // 3
mBeatDur = 1. / mTempo;               // 4
pthread_cond_signal (&mCondition);    // 5
}

mBaseSecondsというインスタンス変数に引数のテンポを変更する拍数を実際の秒数に変換したものを代入する(1)。これはテンポ単位(拍=beat)と実時間の変換を最適化するためだろう。

mBaseBeatsにはテンポを変える拍数を(2)、mTempoには引数のテンポをそのまま代入している(3)。そう、内部クラスの方にはtempoを保持するインスタンス変数が存在するのだ。

そしてmBeatDur。名前からして一拍あたりの長さだろう。これは1をテンポで割ったものとしている(4)。単位はmBaseSecondsという変数があるあたりからもおそらく秒だろう。そうすると、この値は1拍何秒かということになる。BPM=120の場合一秒間に2拍、1拍の長さは0.5秒となる。

つまり、最初の疑問、TempoClockのtempoプロパティに設定する値の単位は何かという事への答えはBPS(beats per second)ということになる。BPM=120の場合BPS=2、BPM=60の場合はBPS=1となる。

テンポスレッドへの変更の通知

SetTempoAtBeatの最後はシグナルを上げて実際に処理を行っているスレッドにテンポの変更を通知する(5)。スレッドの部分は前回見た部分なのだけど、こういう処理を行っている。

この「指定時間になるかシグナルが来るまで」の「シグナル」の処理になるわけだ。テンポが変更されているので待つべき時間を再計算して既に過ぎているようならトリガーをかけないといけない。

さて、一つ気付くことがある。SuperColliderのTempoClockのsetTempoAtBeatは2つの引数を取り、最初が変更するテンポ、次がいつからそのテンポを変えるかということになるのだけど、内部C++クラスのTempoClockのメソッドsetTempoAtBeatではmBeatDurがすぐに変更されてしまっている(4)。

つまりこういうことだ。

「フレーズの途中で次の拍からテンポを変えるというような指定をした場合、演奏中のフレーズのテンポも変わってしまう。」

tempoプロパティ経由で設定するときは「いつから」の引数が今の拍数(this.beats)になっているので問題は起こらない。もし予約設定のようにsetTempoAtBeatによってテンポを設定する場合は注意する必要があるだろう。

ところで、実は内部C++クラスTempoClockのクラス宣言部にこういう記述がある。

double mTempo; // beats per second
double mBeatDur; // 1/tempo
double mBeats; // beats

そう、ちゃんと(内部ソースの)コメントにテンポの単位はBPSと書いてあったのだった。

テンポの初期値

それではテンポの初期値はいくつなのだろうか。

内部クラスにしかテンポを保持するクラスがないのなら内部クラスのコンストラクタを見るのが早いのではないかと思うのだけど、内部クラスもコンストラクタでは引数としてテンポを受け取っているのだ。

TempoClock(VMGlobals *inVMGlobals, PyrObject* inTempoClockObj,
double inTempo, double inBaseBeats, double inBaseSeconds);

では、誰が初期テンポを設定しているのだろうか。TempoClock.defaultの生成を追ってみる。

*initClass {
default = this.new.permanent_(true);  // 1
CmdPeriod.add(this);
}

*initClassはクラスファイルがロードされたときに呼ばれるメソッドだ。ここではnewを引数無しで呼び出している(1)。

*new { arg tempo, beats, seconds, queueSize=256;
^super.new.init(tempo, beats, seconds, queueSize)
}

*newでは親クラスのnewを呼び出した後initを呼んでいる。ここでSuperColliderは引数がない場合、その値はnilとなることを思い出して欲しい。

init { arg tempo, beats, seconds, queueSize;
queue = Array.new(queueSize);
this.prStart(tempo, beats, seconds);  // 1
all = all.add(this);
}

tempoはnilのままprStartへ進む(1)。

prStart { arg tempo;
_TempoClock_New
^this.primitiveFailed
}

そして、nilのままプリミティブへ。_TempoClock_Newの実体はPyrSched.cppのprTempoClock_Newだ。そのprTempoClock_Newの冒頭にこんな記述がある。

double tempo;
int err = slotDoubleVal(b, &tempo);  // 1
if (err) tempo = 1.;                 // 2

double型の値を取り出そうとしている(1)のだけど、tempoはnilなのでエラー終了する。そしてエラー終了した場合tempo=1となるのだ(2)。

つまり、テンポの初期値はBPS=1(BPM=60)ということになる。

dependency

テンポ設定時の一つ忘れ物を片付けておこう。tempo_メソッドの最後にあるこの処理。

this.changed(\tempo);  // this line is added

changedメソッドの実装はObjectクラスにある。

changed { arg what ... moreArgs;
dependantsDictionary.at(this).do({ arg item;
item.update(this, what, *moreArgs);
});
}

changedが呼ばれるとdependantsDictionaryに自分自身(レシーバ)をキーとして登録されているオブジェクトにupdateのメッセージが投げられる。dependantsDictionaryはObjectクラスのクラス変数だ。*initClassでインスタンス生成が行われている。

*initClass { dependantsDictionary = IdentityDictionary.new(4); }

キーはレシーバ自身だとして、その値は何かというと、レシーバに依存関係オブジェクトを登録するメソッドaddDependantにその答えがある。

addDependant { arg dependant;
var theDependants;
theDependants = dependantsDictionary.at(this);           // 1
if(theDependants.isNil,{                                 // 2
theDependants = IdentitySet.new.add(dependant);  // 3
dependantsDictionary.put(this, theDependants);   // 4
},{
theDependants.add(dependant);                    // 5
});
}

dependantsDictionaryから自分自身をキーとして値を引き(1)、そのエントリーが存在しない場合(2)は新たにIdentitySetのインスタンスを生成し、依存関係オブジェクトをそこに足し(3)、最後にdependantsDictionaryに登録を行っている(4)。エントリーが存在する場合はそこに追加する(5)。

さて、updateはObjectで実装されているメソッドだ。

update { arg theChanged, theChanger;	// respond to a change in a model
}

これを見ても分かるように、デフォルトでは何もしない空のメソッドだ。

TempoClock.default.addDependant( 1 );

等としても何も起こらない(Integerクラス、及びその親クラスにはupdateは実装されていない=Objectクラスのupdateメソッドが実行される)。

DebugOut {
update { arg theChanged, theChanger;
theChanger.asString.postln;
}
}

例えば、このようなクラスを作ってそのインスタンスを注目するオブジェクトに設定しておけば値が変わる度(changedが呼ばれる度)それを表示するといったことも出来る。

コントロール用のオブジェクトを一つ作っておき、その状態が変わったらパターンを変えるなんていうことにも使うことが出来る(状態が変わったときにchangedを呼ぶ)。また、update時にさらにchangedを発生して連鎖的に変化させるといったことも出来るだろう。

オブジェクトの状態変更に合わせて何かをしたい場合に使える機能だ。

Objective-C(というよりそのフレームワークのCocoa)にもNotificationとして同じような機能が用意されている。