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

ciqiのかたわれのotonotrixです。

初回はSuperCollider Japanの運営もされている、umbrella_processさんのコードを紹介します。

コードはここから。

今回は前半部分に当たるSynthDefについて。
SynthDefの書き方や、SinOsc, EnvGen, Pan2などのよく使うUnit Generatorの機能については、SuperColliderのHelpファイルを参照してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
(
SynthDef("up-piano-20", {
arg freq=440, gate=1, amp=1, pan=0, downRate=20000, mix=0.25, room=0.15, damp=0.5;
var x, y, env;
env = Env.asr(5,1,5, -3);
x = SinOsc.ar(freq,0,amp);
y = LFNoise2.ar(0.2,downRate/100,downRate);
x = Latch.ar(x, Impulse.ar(y));
x = FreeVerb.ar(x,mix,room,damp);
x = EnvGen.kr(env,gate,doneAction: 2) * x;
Out.ar(0, Pan2.ar(x,pan));
}).store;
)


7行目からいきましょう。見やすいように引数を渡した状態で、
y = LFNoise2.ar(0.2,200,20000);

LFNoiseはランダム値を出すUGenですが、1秒間に出す値(-1~1)の数を決めることができます。

ここでは、0.2となっているので10秒に二つ(1秒間に0.2つ)の値を出します。
そして第二、第三引数が、-1~1の値に対してそれぞれ乗算、加算なので、この場合、
「19800~20200の間の値を10秒に二つランダムに出す」ということです。

LFNoiseにはいくつか種類がありまして、大雑把にいうと、生み出された値同士を繋ぐ(補間)か繋がないかで分けられます。LFNoise2は後者で、値が滑らかに変化していきます。
「19800~20200の間の値を10秒に二つランダムに出す。次の値へは滑らかに変化する」です。

8行目は、
x = Latch.ar(SinOsc.ar(freq,0,amp), Impulse.ar(y));ですね。
Helpにも書いてありますが、LatchはSample & Holdという手法で、次のtriggerがあるまで値を保持するということです。試しに一つコードを。

{ Latch.ar( SinOsc.ar(440), Impulse.ar(5000)) }.scope; (要 internal server起動)



滑らかだったsin波がでこぼこになってますね。次のtrigger(Impulseが1を出す)まで前の値は維持されるから、Stepのようになるのです。波形が変わるので結果として音色が変わります。internal serverを起動した状態で{上のコード}.freqscopeでスペクトルを確認してください。

ん、ちょっと待ってください。triggerの数の分だけ値が検出されるって、何だかサンプリングレートみたいですね。 事実、Impulse.ar(5000)は値を5000回検出してるわけですし。そう考えるとですね、これはもともと44100回(44.1kHz)検出していたのを、5000回(5kHz)に変更すること、つまりサンプリングレートを下げていると考えられないでしょうか。下げるとどうなるか。

この場合だと、だいたい8~9分の1にレートが下がっているので、44.1kHzと比べて一回のtriggerごとに8~9個のサンプルを見逃していると考えられるのです。その結果、不完全なsin波が生まれ、音が歪むのです。

よく見たらSynthDefの引数がdownRateになってますね。

7行目のコードと合わせると、
「sin波をダウンサンプリングするが、そのレートは19800(19.8kHz)~20200(20.2kHz)の間をランダムにしかも滑らかに変化する、その結果としてsin波が変化しスペクトルが変化する」
ということですね。刺激的な音色になりそうですね。

しかし、Impulseは主役ではないのに、何故「kr」ではなく「ar」を使うのでしょう?controlするだけだし、krで十分ではないか?

そもそも、audio rate(ar)でsampleを生み出す時、一つ一つ処理するのではなく、64個をひとまとめにしています。このまとまりをblock sizeといいます。それに対してcontrol rate(kr)は、arのblock size一つの間に一つのsampleを生み出すだけなのです。つまりsample数は1/64なのです。
具体的に見ましょう。

{ Impulse.kr(200) }.scope;



Impulse.arとはえらい違いですね。
(64sampleごとに生み出される値間は直線(線形)で補間されています)
ImpulseというよりもTriangleっぽいですね。一つの値に対して64sampleですので、0から1までの間は64sample、1から0までも64sampleで、つまり一つのImpulseには128sample必要ということですね。
ということは、1秒間に出せるImpulseの最大数は44100/128でだいたい344といえます。

{ Impulse.kr(44100/128) }.scope;



ますますTriangleですね。
これ以上Impulseの数を増やしたら、Impulseとして成り立ちませんね。
ナイキストレートにより、最大周波数は検出レートの半分の周波数が望ましいので、44100/128/2でだいたい172Hzですね。

{ Latch.ar(SinOsc.ar(44100/128/2),Impulse.kr(44100/128)) }.scope;



Pulse波になりましたね。このくらいまでならばkrで対応出来るわけです。

というわけで、Impulse.kr(20200)なんかは、不可能(可能ではあるがImpulseとしての体をなさない)なんですね。だからarを使わなくてはならない。

9行以下も基本的なUGenを使ってますので、Helpで確認してください。

10、11行目の、
x = EnvGen.kr(env,gate,doneAction: 2) * SinOsc.ar(freq,0,amp);
Out.ar(0, Pan2.ar(x,pan));
についてですが、エンベロープをsin波に乗算してからPan2に入れるのは、CPUの負荷軽減になります。

Pan2.ar(signal*envelope)よりも、Pan2.ar(signal)*envelopeの方が負荷が大きいのです。
Pan2でsignalをステレオにする際、[signal,signal]と配列にしているので、前者だとenvelopeはsignal一つだけに使っていますが、後者だと、[signal,signal]*envelope、[signal*envelope,signal*envelope]となり、envelopeの乗算処理を一つ余計に使うことになり、これがCPUに負担をかけます。画面左下の、ServerのGUIのUGensが増えているのがわかります。

後半のsequencerの部分に話が及びますが、rand2などのmath operationをSynthDefには使用せず、language側で計算させてserverに計算結果を渡すのもCPUの負荷軽減になります。serverに計算させるということは、それだけ負担をかけるということですから。

アイデアと深い思索が効率よく詰めこまれた素晴らしいコードですね。

以上で前半のSynthDefを終わります。間違いや勘違いがあるかもしれませんので、ご指摘、ご教示いただけると幸いです。

2 Comments

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

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

Post Comment