yoppa.org


Blog

SuperCollider自主練 – 基礎編その4

1.9 Synthの定義

これまでのサンプルは、音を再生する際に Synth(“temp__0” : 1000) というようなメッセージが出力されていた。

内部的に繋ぎあわされたUGenのセットは、SynthDef(Synthの定義)にまとめられる、そこではどのUgenを用いるのか、また、どのように繋りあうのかが記述されている。SCのサーバーはそれらの定義を解釈して音響を生成する。

実はこれまでの例で用いてきた、{}.play という書式を用いた際には、SCはこれらの処理を目に見えないところで自動的に行い、一時的な名前をつけてSynthを生成し、自動処理していた。

より明白な方法でこの処理を行うことが可能。作成したパッチをSynthDefという形式で包みこんで、名前をつけて特定する。また、この際に名前をつけるだけでなく、出力するバスをOut.arメッセージを使用して指定しなければならない。

{SinOsc.ar}.play // 自動的に一時的なSynth名を生成

このサンプルをSynthDefを使用して書くと、

SynthDef("sine", {Out.ar(0, SinOsc.ar)}).play

となる。右チャンネルに出力する場合は、

SynthDef("sine", {Out.ar(1, SinOsc.ar)}).play

もしくは、下記のように変数を用いて記述も可能。

(
SynthDef("one_tone_only", {
    var out, freq = 440;
    out = SinOsc.ar(freq);
    Out.ar(0, out)
}).add
)

これは下記の命令で再生する。

Synth("one_tone_only");

SynthDefで定義された楽器を呼び出すには2つの方法があることに注意。一つは play メッセージを使用する方法。これは {}.play で音を生成する方法に似ている。もう一つは add メッセージを使用してSynthDefをサーバーに追加する方法。この他に .send(s) や WiteDefFile を使用する方法などがあるが、ここでは割愛。

この例であげた”one_tone_only”は、440Hzのサイン波しか生成できない。たとえば、このSinOscの周波数のように、SynthDefの外部から変化させたい場合は、引数(arg)を用いる。

(
SynthDef("different_tones", {
    arg freq = 440; // 引数freqを宣言し、その初期値を440にする
    var out; 
    out = SinOsc.ar(freq)*0.3;
    Out.ar(0, out)
}).play
)

Synthは、第1引数にはSynthDefで指定した名前、第2引数にはSynthDefで定義された引数(arg)を配列にして渡す。このやり方によって複数の引数を同時に渡すことができる。

["arg1", 10, "arg2", 111 …]

実際に”different_tones”を使用してみる。

Synth("different_tones", ["freq", 550]);
Synth("different_tones", [\freq, 660]); // "freq" と同じ
Synth("different_tones", ["freq", 880]);

// もし引数を指定しなければ、初期値の440となる
Synth("different_tones")

一度の複数の”different_tones”を生成することもできる。

a = Synth("different_tones", ["freq", 64.midicps]);
b = Synth("different_tones", ["freq", 67.midicps]);
c = Synth("different_tones", ["freq", 72.midicps]);

a.set("freq", 65.midicps);
c.set("freq", 71.midicps);
a.set("freq", 64.midicps);  c.set("freq", 72.midicps);

a.free; 
b.free; 
c.free;

PMOscを使用したより実践的なSynthDefの例

(
// まずこのブロックを実行
SynthDef("PMCrotale", {
arg midi = 60, tone = 3, art = 1, amp = 0.8, pan = 0; 
var env, out, mod, freq;

freq = midi.midicps;
env = Env.perc(0, art);
mod = 5 + (1/IRand(2, 6));

out = PMOsc.ar(freq, mod*freq, 
    pmindex: EnvGen.kr(env, timeScale: art, levelScale: tone), 
    mul: EnvGen.kr(env, timeScale: art, levelScale: 0.3));

out = Pan2.ar(out, pan);

out = out * EnvGen.kr(env, timeScale: 1.3*art, 
    levelScale: Rand(0.1, 0.5), doneAction:2);  
Out.ar(0, out); //Out.ar(bus, out);

}).add;
)

// 次に以下の行を実行
Synth("PMCrotale", ["midi", rrand(48, 72).round(1), "tone", rrand(1, 6)])

1.10 バス、バッファー、ノード

バス(Buses)は、オーディオ信号やコントロール信号のルーティングに使用される。SCには128のオーディオバスと4096のコントロールバスが用意されていて、またこの数値は設定で変更することも可能。

デフォルトのオーディオ出力は最初の0番のバスから出力される。Out.ar(0)で使用された方法。また、オーディオ入力は通常は8番のバスが使用される。その他のバスは「プライベートな」バスとして内部のルーティングのために確保されている。

例えば5人のメンバーのバンドがいて、それぞれがリバーブのエフェクトを使用するより、通常はミキサーでルーティングして1つに統合してリバーブをかけようと思うだろう。SCのバスもこれと同じような働きをする。

ここで新たなUGenであるPlayBufを例にして、バスの使用方法を検討している。PlayBufはオーディオファイルを読み込んで再生する。サウンドファイルの場所は、SuperCollider.appからみた相対パスで指定する。

ここで、オーディオを格納する変数にバッファー(Buffers)を使用していることに注意。バッファーはチルダ(~)を先頭につけて宣言される。宣言の際にvarが必要ないのはaからzまでの変数と同様。バッファーとして定義することで、その値はパッチのどこからでも参照できるだけでなく、他のパッチや、さらには他のウィンドウからでも参照できるようになる。

~houston = Buffer.read(s, "sounds/a11wlk01-44_1.aiff");
~chooston = Buffer.read(s, "sounds/a11wlk01.wav");
{PlayBuf.ar(1, ~houston)}.play;
{PlayBuf.ar(1, ~chooston)}.play;

一度バッファーに読みこまれたサウンドは、バッファー番号、チャンネル数、ファイルパス、フレーム数など様々な情報を取得できる。

[~houston.bufnum, ~houston.numChannels, ~houston.path, ~houston.numFrames];
[~chooston.bufnum, ~chooston.numChannels, ~chooston.path, ~chooston.numFrames];

これらの情報を活用して、ライヒの”Come out”のように、左右のチャンネルで少しずつ位相をずらしながらループ再生するというような効果も簡単に実装できる。

(  // 左右の位相をずらす
{
    var rate, trigger, frames;
    frames = ~houston.numFrames;

    rate = [1, 1.01];
    trigger = Impulse.kr(rate);
    PlayBuf.ar(1, ~houston, 1, trigger, frames * Line.kr(0, 1, 60)) * 
    EnvGen.kr(Env.linen(0.01, 0.96, 0.01), trigger) * rate;
}.play;
)

では、このようにして読みこんだ2つのサウンドを一箇所からコントロールするにはどうすれば良いのか? ひとつは、ビンテージのアナログシンセサイザーのようにソースをモジュールとして作成し、それぞれをバスで接続するという方法がある。これには、Out.ar と In.ar を用いる。それぞれの引数にバスの番号を指定すると入力と出力が接続される。

より一般的な方法は、バスオブジェクトを使用する方法だ。

(
// まだコントロールの使用が開始されていないので、0のままで静止している
~kbus1 = Bus.control; // コントロールバス
~kbus2 = Bus.control; // コントロールバス
{
    var speed, direction;
    speed = In.kr(~kbus1, 1) * 0.2 + 1;
    direction = In.kr(~kbus2);
    PlayBuf.ar(1, ~chooston, (speed * direction), loop: 1);
}.play;
)

// コントロールの開始  
(
{Out.kr(~kbus1, LFNoise0.kr(12))}.play;
{Out.kr(~kbus2, LFClipNoise.kr(1/4))}.play;
)

// 二番目のバッファーを、同じコントロールバスを利用して開始
// ただし、右チャンネルに出力
(
{
    var speed, direction;
    speed = In.kr(~kbus1, 1) * 0.2 + 1;
    direction = In.kr(~kbus2);
    Out.ar(1, PlayBuf.ar(1, ~houston, (speed * direction), loop: 1));
}.play;
)

このように、コントロールバスを用いることでインデックス番号などを気にせずにダイレクトにアサインすることができる。コードの再利用性も高まる。

オーディオバスを用いることで、エフェクトを数珠繋ぎにして、オーディオシグナルをルーティングすることができる。

まずは、オーディオバスを使わないサンプル。

(
{
    Out.ar(0,
        Pan2.ar( PlayBuf.ar(1, ~houston, loop: 1) * 
            SinOsc.ar(LFNoise0.kr(12, mul: 500, add: 600)),
        0.5)
    )
}.play
)

オーディオバッファを、ノイズを周波数にドライブしたサイン波で音量変化させている。

もう一つ別の例。オールパスフィルタを使用して、ディレイラインを生成している。

(
{
var source, delay; 
    source = PlayBuf.ar(1, ~chooston, loop: 1);
    delay =  AllpassC.ar(source, 2, [0.65, 1.15], 10);
    Out.ar(0,
    Pan2.ar(source) + delay
    )
}.play
)

これらの処理をオーディオバスを利用して書き直してみる。まずオーディオバスとコントロールバスの名前を決める。

~delay = Bus.audio(s, 2);
~mod = Bus.audio(s, 2);
~gate = Bus.audio(s, 2);
~k5 = Bus.control;

コントロールバスを開始

{Out.kr(~k5, LFNoise0.kr(4))}.play;

最後の出力に手前でディレイラインを開始

{Out.ar(0, AllpassC.ar(In.ar(~delay, 2), 2, [0.65, 1.15], 10))}.play

ディレイの手前で、音量のモジュレーションを開始

{Out.ar(~delay, In.ar(~mod, 2) * SinOsc.ar(In.kr(~k5)*500 + 1100))}.play    

モジュレーションの手前で、ゲートを設定

{Out.ar([0, ~mod], In.ar(~gate, 2) * max(0, In.kr(~k5)))}.play    

これで、エフェクトのチェーンの部分はできたので、ここに実際にオーディオバッファを再生してみる。

{Out.ar(~gate, Pan2.ar(PlayBuf.ar(1, ~houston, loop: 1), 0.5))}.play;

すると、3つのエフェクトをチェーンにして再生される。さらに別の音を追加することも可能。

{Out.ar(~gate, Pan2.ar(PlayBuf.ar(1, ~chooston, loop: 1), -0.5))}.play;

(こうしたオーディオバスを継いでいく方法は、ライブコーディングなどにも使えそう…)