前回までのところでタイマーに対し演奏されるべき時間にトリガーをかけてもらう予約を行うところまで来ていた。あとはトリガーに対して演奏データを処理する、つまりは最終的にsclangのランドから、scsynthに対してコマンドを発行することになる。
ようやくUGenからみていったSuperColliderの処理の流れも、一周してきたというわけだ。
Responding to timer trigger
流れをもう一度確認してみよう。
同じような流れは以前にも出てきたのだけど、少々込み入っていて把握しづらい。図示してみた。
図の上段が前回の部分だ。そして、タイマーからのトリガーが下段だ。このawakeからの部分は、実は以前にも見ていて対象のクラスが違うので微妙に異なる部分があるのだけど、大体においては同じだ。
と、いうことで、今回はprNextからの流れを見ていくことにしよう。
EventStreamPlayerクラスのprNextの実装は次のようになっている。
prNext { arg inTime;
var nextTime;
var outEvent = stream.next(event.copy); // 1
if (outEvent.isNil) {
streamHasEnded = stream.notNil;
cleanup.clear;
this.removedFromScheduler;
^nil
}{
nextTime = outEvent.playAndDelta(cleanup, muteCount > 0); // 2
if (nextTime.isNil) { this.removedFromScheduler; ^nil };
nextBeat = inTime + nextTime; // inval is current logical beat
^nextTime
};
}
prNext Stream.sc
大まかにいうと演奏すべきイベントを取り出し(1)、実際に演奏する(2)という流れになっている。戻り値は次にトリガーを予約する時間で、次に演奏すべきデータがない場合はnilを返すようになっている。
stream.nextで取り出したoutEventを演奏するメソッド、playAndDeltaはEventクラスで実装されている。
playAndDelta { | cleanup, mute |
if (mute) { this.put(\freq, \rest) }; // 3
cleanup.update(this);
this.play; // 4
^this.delta;
}
playAndDelta Event.sc
ミュートが指定されている場合、3でなにやら設定しているようなのだけど、とりあえず飛ばして先に進んでみる。4でそのものずばりのメソッドplayが呼ばれる。
play {
if (parent.isNil) {
parent = defaultParentEvent;
};
this.use {
this[\play].value; // 5
};
// ^this.delta
}
play Event.sc
さっきのmuteのところでも気になったのだけど、5でなにやら自分に対してシンボル\playにひも付いた値を取り出そうとしているように見える。
Eventクラスの継承関係はどうなってるのだろうか。
EventクラスはDictionaryクラスの派生クラス(直接ではないけど)ということが見えてくる。保留にしていたmuteの時の処理(3)も\freqのというキーに\restというシンボルを結びつけているということがわかる。
さて、EventがDictionaryクラスのサブクラスなのはわかったのだけど、では[\play]、そしてそのオブジェクトに対してvalueというメソッドを呼び出している(5)の意味とはなんだろうか。その答えは、Eventクラスのクラス初期化を追っていくとわかる。
*initClass {
Class.initClassTree(Server);
Class.initClassTree(TempoClock);
this.makeParentEvents; // 6
StartUp.add {
Event.makeDefaultSynthDef;
};
}
initClass Event.sc
*initClassはクラスファイルが読み込まれたときに一度だけ実行されるメソッドだ。
このなかでmakeParentEventsというメソッドが呼ばれている(6)。
このmakeParentEventsというのはEventクラスのクラスメソッドで、こんな感じで実装されている。
*makeParentEvents {
// define useful event subsets.
partialEvents = (
makeParentEvents Event.sc
makeParentEventsはとても巨大なメソッドなので冒頭の部分だけの抜粋になるけど、ここでは演奏用のイベントデータのパターンをすべて登録しているのだ。そして、このメソッドの最後で、
defaultParentEvent = parentEvents.default;
と、クラス変数defaultParentEventにセットしている。
つまり、this[\play]で取り出されているものは\playに結びつけられた、つまり演奏用のイベント情報なわけだ。
value
さて、this[\play]に続くvalueだけど、これはもう何度も出てきているのでおなじみかもしれない。このメソッドはレシーバを評価するというメソッドだ。このメソッドはObjectに定義されている。
value { ^this }
デフォルトの挙動では自分自身を返す。もし、レシーバがFunctionクラスのインスタンスだった場合を見てみると、
value { arg ... args;
_FunctionValue // 7
// evaluate a function with args
^this.primitiveFailed
}
実装の実体は_FunctionValueで、これはおなじみのプリミティブ、つまりはネイティブに実装された内部ルーチンが呼び出されることになる。このようにレシーバがFunctionクラスの場合、その値はメソッドを実行した戻り値となる。
さて、Eventクラスのキー\playに対する値がどう定義されているのだろうか。
play: #{
var tempo, server;
~finish.value;
server = ~server ?? { Server.default };
tempo = ~tempo;
if (tempo.notNil) {
thisThread.clock.tempo = tempo;
};
~eventTypes[~type].value(server);
},
このようにのブロック(関数)となっている。valueメソッドで評価されるものは、このブロックがとなる(このブロックが実行されて値を返す)。つまり、このブロックこそがEventクラスが演奏を行うときのコアロジックとなるわけだ。
Environment Variable
ところで、~が頭につく変数というのはどういう変数なのだろうか。グローバル変数のように使用できるのだけど、グローバル変数というのはいまひとつ正確ではない。SuperColliderのドキュメントにはこのように記されている。
~ access an environment variable ~abc compiles to \abc.envirGet ~abc = value compiles to \abc.envirPut(value) supercollider.svn.sourceforge.net--SymbolicNotations.html
つまり~はenvironment variable(環境変数)へのアクセスを行うためのシンタクスシュガーなわけだ。環境(environment)は、そのものずばり、Environmentというクラスがある。環境変数というのはそのEnvironmentクラスのインスタンスのキー値となる。
さて、~abcみたいに、対象のインスタンスを指定しなくてもアクセスできているということは、the environmentと呼べるようなEnvironmentクラスのインスタンスがどこかに存在するはずだ。
どこかというと、やはりObjectクラスが思い浮かぶわけで、実際Objectクラスにはクラス変数としてcurrentEnvironmentというEnvironmentクラスの変数が定義されているのだ。
こんなコードで実験をしてみた。
~x = 'hello'; ~x.postln (上の2行を評価) hello hello currentEnvironment = Environment.new (上の行を評価) Environment[ ] ~x.println (上の行を評価) ERROR: Message 'println' not understood. RECEIVER: nil
途中でcurrentEnvironmentにあたらしいEnvironmentクラスのインスタンスをセットしている。それによって~xが未定義状態になっていることがわかる。新しいEnvironmentには\xの値をセットしていないからだ。
このように、環境変数はごっそりと環境ごと換えて(換えられて)しまうことが出来る。
Event Types
さて、寄り道が長くなってしまったが、Eventクラスのplayメソッドに戻ろう。playメソッドにはとてもさり気ない記述なのだけど重要な処理を行っている部分がある。
play {
if (parent.isNil) {
parent = defaultParentEvent;
};
this.use { // 8
this[\play].value;
};
// ^this.delta
}
play Event.sc
この(8)の部分だ。自身のuseメソッドをブロックを引数に呼び出している。useの実装はEnvironmentクラスで行われている。
use { arg function;
// temporarily replaces the currentEnvironment with this,
// executes function, returns the result of the function
var result, saveEnvir;
saveEnvir = currentEnvironment;
currentEnvironment = this; // 9
protect {
result = function.value(this);
}{
currentEnvironment = saveEnvir;
};
^result
}
use Environment.sc
(9)をみるとcurrentEnvironmentを自分自身で書き換えている。(9)で環境を取り替えた後、他のスレッドからは排他的に実行するためにprotectメソッドの中で引数の関数ブロックを実行する。そして、終了したらまた環境を元に戻すという処理を行っている、
さて、\playによって評価されるのは現在の環境の\playに結びつけられた値、今の場合ブロックなのでブロックが評価される(実行される)。
もう一度\playに結びつけられているブロックをふりかえってみよう。
play: #{
var tempo, server;
~finish.value;
server = ~server ?? { Server.default };
tempo = ~tempo; // 10
if (tempo.notNil) {
thisThread.clock.tempo = tempo;
};
~eventTypes[~type].value(server); // 11
},
(10)で自身に\tempoのキーで値が設定されていないかを調べて、設定されていた場合にはテンポを設定している。
このようにデフォルトの値が設定されていないキーもあれば設定されているキーもある。(11)では\eventTypesキーに結びつけられたオブジェクトのさらに\typeのキーに結びつけられたオブジェクトを引き、それを評価(value)している。
\typeは\playの定義のすぐ上でこんな風に定義されている。
type: \note,
何も指定しなかった場合、イベントのタイプは\noteとなるわけだ。
\eventTypeにはいろんなタイプの演奏イベントが定義されている。例えば\restはこうなっている。
rest: #{},
何もしないというのが一目瞭然だ。
さて、いま、とりあえずイベントのタイプは\noteだとして進もう。\noteはというと、こんな風に定義されている。
note: #{|server|
var freqs, lag, strum, strumTime, sustain;
var bndl, addAction, group, sendGate, ids;
var msgFunc, desc, synthLib, bundle, instrumentName, schedBundleArray, offset;
freqs = ~detunedFreq.value;
...
とても長いので、流れをざっとみていくことにする。
前半はソースのコメントにもあるとおり、OSCメッセージを組み立てている。そして、後半、処理は二つに分かれる。ひとつはそのまま組み立てたメッセージをまとめて送信するか、それともアルペジオのように間隔を空けて順次送信するかだ。
そしてどちらも最終的に到達するメソッドは同じで、server.sendBundleとなる。余談になるけど、その前段階に呼ばれる~schedBundleArray.valueも~schedStrummedNote.valueも、この巨大な辞書の中に定義されている。このノリは少しJavaScriptににているような気がする。
~schedBundleArrayの方を見てみると、以下のようになっている。
schedBundleArray: #{ | lag, offset, server, bundleArray |
thisThread.clock.sched ( offset, {
if (lag !=0 ) {
SystemClock.sched(lag, { server.sendBundle(server.latency, *bundleArray) }) //12
} {
server.sendBundle(server.latency, *bundleArray) // 13
}
})
},
offsetによってタイマーのトリガを予約して、トリガーがかかると、lagが0のときはすぐにserver.sendBundleを行い(13)、そうでない場合はタイマーにsendBundleの実行を予約している(12)。offsetもlagも0ではないときはoffset+lag分二回のタイマーのトリガを経てserver.sendBundleに到達することになる。
ここで、ようやくイベントはサーバに届き、音として再生されることとなるわけだ。