SuperColliderコードリーディング 「Stacked Chord Loop by umbrella_process」 その2

どうも、ciqiの片一方のotonotrixです。

前半に引き続き、umbrella_processさんのコードを分析します。

コードはここから

今回は、sequenceです。SynthDefで作った楽器をどのように演奏するのかということです。
(SynthDefについての分析のページはこちら)

前回より少し長めですが、これで一つのまとまりです。
ArrayオブジェクトやTaskオブジェクト、loopメソッド等はよく使われるので、Helpやexampleでその機能を確認してください。
(条件文の範囲の明確化のため、日本語のコメントを追加してあります)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
(
// Stacked Chord Loop
 
var bpm = 80;
var nodeArray = Array.new();
var chordArray = Array.new();
var scaleArray = #[ 12, 14, 4, 5, 7, 9, 11];
var weightArray = #[ 0.1,0.05, 0.3, 0.1,0.05, 0.2, 0.2];
/**
* sequencers
*/
var sequencer = Task({
var note, node;
loop({
     if( 0.15.coin, {
         note = scaleArray.wchoose(weightArray) + 51;
         if( 0.2.coin, {
             note = note + 12;
         });
         while( { chordArray.includes(note) }, {
             note = scaleArray.wchoose(weightArray) + 51;
         });   // while文の終わり
         chordArray = chordArray.addFirst(note);
         node = Synth.tail( nil, "up-piano-20", [
              \freq, note.midicps,
              \amp, (0.05.rand2 + 0.1)*0.5,
              \pan, 0.8.rand2,
              \downRate, 10000.rand2 + 15000,
              \mix, 0.5.rand2 + 0.5
         ]);
         nodeArray = nodeArray.addFirst(node);
         if( chordArray.size > 5, {
             chordArray.pop;
             s.sendMsg( "/n_set", nodeArray.pop.nodeID, \gate, 0.0 );
      });
      chordArray.postln;
    }); // 最初のif文の終わり
    1.wait;
});
}, TempoClock(bpm/60));
 
/**
* music start
*/
SystemClock.sched(0,{sequencer.start});
)


12行目からの、Taskの内部にいきましょう。

簡単にすると、

1
2
3
4
5
6
Task( { 
       loop({処理;
             1.wait
           });
      },TempoClock(bpm/60)
     )


ですね。
1.waitは秒ではなく、clockで設定されたテンポのことで、1は四分音符一つ分のことです。

TempoClockのテンポは、BPM 60=1とした比ですので、使いたいテンポがあればそのテンポを60で割った値をTempoClockに渡せばいいわけですね。コードの通りTempoClock(bpm/60)と書いておけば、テンポ入力しやすいので便利です。

このコードの重要な部分は、
14行目の、loop( { 処理; 1.wait} )であり、メソッドの名の通り、loop内部の関数を延々evaluateし続けるということです。仮にBPM 60の場合では、「処理 → 1秒(四分音符分一つ分)待つ →処理 →1秒待つ…」が延々と続きます。

loop内部の処理がコード全体のコアといってもよいでしょう。
このことを念頭に、loop内部の処理を展開していきましょう。

15行目のifを見てみましょう。このifはloop処理の全体に渡ってますね。
簡潔化して、

1
2
3
4
loop({ if(0.15.coin~
         ~); 
     1.wait;
    })


ということですね。
このifはtrue処理しかありません。falseだとif内部の処理はまるまる無視されますので、何もせずに1.waitに行き、またloopの先頭から処理が始まります。ifの内部に0.15.coinというのがありますが、15%の確率でtrue(85%の確率でfalse)を返します。もちろん1.coinだと100% trueですね。
falseが無いので、15%の確率でifの内部が実行されるといえます。
(他のifもfalse処理がありませんね)

16行目、

note = scaleArray.wchoose(weightArray) + 51;

を見てみましょう。

wchooseメソッドというのは、weight chooseの略で、chooseメソッドは全くのランダムで配列の要素が選ばれるのに対し、wchooseは確率に偏りを持たせるメソッドです。weightArrayのindexと同じscaleArrayのindexの要素が確率的に選ばれます。12が選ばれるのは10%ということですね。その選ばれた値に51が足されます。鍵盤で表すとこうです。

グレーの部分が生成するMIDInoteです(オクターブは除いています)

upmidi
左から、30%, 10%, 5%, 20%, 20%, 10%, 5%です。右から四番目が60のドです。
ふむ、Gの出現率が一番高いのですな。しかもベース部分に。

17行目は、

if(0.2.coin,{note = note + 12;});

ですので、20%の確率で変数noteに12が足されます。変数noteの数値はMIDInoteとして扱われているので、1オクターブ上ということです。

20行目、

while( { chordArray.includes(note) },
{ note = scaleArray.wchoose(weightArray) + 51; });

最初の条件がtrueであるかぎり以下の処理を実行するので、ここでは、
「chordArrayという配列の内部に、変数noteに格納されているMIDInoteと同じのがあれば、(MIDInoteを)選び直す」ということです。
ユニゾンにならないようにということですね。音の多様性のためと、ユニゾンばかりが選ばれる続けることもありますし、その防止のwhile文でしょう。
その後、chordArrayに変数noteの値を格納します。

どんどんいきましょう。

23行目、

chordArray = chordArray.addFirst(note);
chordArrayの先頭(indexナンバー0)に変数noteの数値を入れます。

引数に値を渡し、生成したSynthを変数nodeに格納します。このときにifやwhileを駆使して得られたnoteを、MIDInoteから周波数に変換し、引数freqに渡します。
そして変数nodeに格納されたSynthをnodeArrayに入れます。ここでも配列の先頭に配置。

ここでchordArrayの要素数に対する条件文ですね。
32行目、

if( chordArray.size > 5, {chordArray.pop; s.sendMsg( “/n_set”, nodeArray.pop.nodeID, \gate, 0.0 );

「chordArrayの要素の数が5よりも大きくなれば、chordArrayの最後尾の要素からSynthを一つ取り出す。そのSynthに対し引数gateに0を渡し、音量をゼロに向かわせ、そののちメモリーから解放する」ということです。
(SynthDefで見かけた、gate, メモリの解放(doneAction: 2)については、EnvGen,Env.asrなどのHelpファイルを確認してください)

ここで、34行目の、

s.sendMsg( “/n_set”, nodeArray.pop.nodeID, \gate, 0.0 );

をみてみましょう。
簡単に言うと、「Serverの特定のSynth(Node)に、引数gateに0を渡すよう、OSCメッセージを送る」ということです。
特定のSynthにアクセスするには、nodeIDを使います。生成されるSynthはnodeIDで管理されているのです。背番号が付いているようなものですね。
.nodeIDはメソッドではなく、Synthにinstance variableとして格納されているnodeIDの取得という意味です。

ちなみに、nodeIDを渡さずにs.sendMsg( “/n_set”, nodeArray.pop, \gate, 0.0 );でコード全体を動かしてみましたが、こんな風にSynthが全く解放せずにエラいことになりました。

いつまでもSynthが残っているのがわかります。


背番号を送らなければ、どのSynth(Node)がメッセージの対象なのかわかりませんからね。
(てか、ServerにSynthをそのまま渡して、何がしたいねんっちゅう話ですな)
( s.sendMsg( “/n_set”, nodeArray.pop.defName, \gate, 0.0 );)でも無理でした。Synthの名前を渡してもダメですな)

実際にSynthが生成する時に、ServerにnodeIDは本当に渡されてるの?と思ったのでソースコードを見てみました。(Synthを選んでcommad + yでソースへ)

Synthのソースのすぐ下の*newがSynth生成のクラスメソッドですね。
このメソッド中の関数の一番下らへんに、

(Node.scのSynthの部分)

1
2
3
server.sendMsg(9, //"s_new"
              defName, synth.nodeID, addNum, inTarget.nodeID,
              *(args.asOSCArgArray)


というのがありますね。
なんのことはない、メソッドの内部でOSCメッセージが使われてますね。送っている内容を見ると、対象とするSynthDefの名前とnodeIDを送っているではありませんか。Serverに対してどのSynthDefを使うかと、生成するSynthに対して背番号を付与しているのですね。
(nodeIDをどのように取得しているかについても、是非ソースコードを追ってみてください)

これ以外にSynthを解放する方法を考えてみました。

◉一つ目。
chordArray.pop.set(\gate, 0);

取り出したSynthに対し、setメソッドで引数を渡します。
ん?これはnodeIDとは関係ない処理か?
確認のため、再びソースへ。
Synthにはsetメソッドは見当らないですが、親クラスのNodeにありました。メソッドの継承ですね。

(Node.scのNodeの部分)

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


やはり、ここでも内部でOSCメッセーが動いていて、ServerにnodeIDを渡していますね。
なんのことはない、umbrellaさんは、直接OSCメッセージを書いているのですね。

◉二つ目。
chordArray.pop.release(5);

取り出したSynthに対し、音を減衰させていき5秒後に音量を0にするメソッド。こちらも親クラスのNodeにメソッドがあります。ソースをば。

(Node.scのNodeの部分)

1
2
3
release { arg releaseTime;
          server.sendMsg(*this.releaseMsg(releaseTime))
         }


ここでも、sendMsgが活躍。内部にさらにreleaseMsgというメソッド。releaseメソッドのすぐ下にありますね。

(Node.scのNodeの部分)

1
2
3
4
5
6
7
8
releaseMsg { arg releaseTime;
        //assumes a control called 'gate' in the synth
             if(releaseTime.isNil, {
                     releaseTime = 0.0;
                   },{ releaseTime = -1.0 - releaseTime;
               });
            ^[15, nodeID, \gate, releaseTime]
           }


Serverにはメッセージを送ってなく、返り値[15, nodeID, \gate, releaseTime]を見るに、OSCメッセージと連携するタイプのようですね。ここでもやはりnodeIDが送られていますね。
この手法も有効ですが、SynthDef内部にrelease時間を設定してますし、それに任せましょう。

以上で、二つのやり方を考えてみましたが、どちらのメソッドも結局は、内部でs.sendMsg()をしているので、最初からOSCメッセージを使えばいいことが分かりました。

というわけで、

Synthが生成する際、SynthDefの名前と、背番号としてのnodeID等がServerに渡され、これらは生成したSynth(Node)のinstance variableに格納されている。

生成したSynthにメッセージを送る時の区別はnodeIDでする。
(Synthの名前やnodeIDはSynthのinstance variableから取得)

実際、nodeArray.pop.nodeIDを使って、配列から取り出したSynthからnodeIDを取得しています。
(Serverやgroupもわかるようですね)

少し脱線してしまいましたが、まとめると、
  • 生成されるSynthは最大5つまでで、6つ目が生成させれると一番古いSynth(すなわち配列の先頭)が減衰していき、やがて消滅する。
  • 候補の七つの音高(scaleArray)はそれぞれ重み付け(weightArray)がされている。
  • 音高がオクターブになる確率は20%ですが、各音が選ばれ、さらにオクターブ上になる確率は、各音の生成確率×0.2(オクターブになる確率)。一番高い可能性はMIDInoteナンバー55(G)の0.3×0.2で0.06、6%である。(個々のMIDInoteで見るとかなり低い確率なんですよね)
  • なお、最初のif文でtrueになる確率が15%なので、仮にBPM 60とすると、次の音は1~6.7秒の間に生成されますね。
テンポや各音の重み付け、オクターブになる確率など変更してみると面白いと思います。

umbrellaさんがコメントでご指摘くださったように、フリギア旋法ぽいですね。MIDInoteを変更して別の旋法を使うのも楽しいと思います。

確率的には、Ⅳ-Ⅴ-Ⅰがありえますね。といっても、仮にAbの響きの次の音がBb(最低音として)ならば、Ab/Bb(Bb7sus4 add 9th)となったりと響きが時間をかけて推移します。といっても、Gが30%の出現率ですからねえ。ベースがGである確率が低いと、フリギア旋法ぽくなりませんから。。。

他にも進行が生まれそうですが、中々巡り会えないでしょう。

以上で、「Stacked Chord Loop by umbrella_process」の分析を終了します。

読んでくださった方、コードを投稿されたumbrella_processさんに感謝いたします。

3 Comments

  1. umbrella_process
    umbrella_process 2012年12月30日 at 3:52 AM . Reply

    とりあげてくださってありがとうございます。
    配列を無駄に変な並び順にして分かりにくくなってますが、音は G, A♭, B♭, C, D, E♭, F で、フリギア旋法っぽくしてます。根音のGと四度、五度のC, Dの出現確率が高くなってます。あんまり深く考えてないですw

  2. […] SuperColliderコードリーディング 「Stacked Chord Loop by umbrella_process」 その2 […]

Post Comment