2017/05/25

SuperColliderのクラス

クラスを記述したファイルは拡張子を.scとして systemExtensionDir あるいは userExtensionDir に保存します。以下でパスを確認できます。

Platform.systemExtensionDir.postln;
Platform.userExtensionDir.postln;

ここでは userExtensionDirClassTest というディレクトリを作ってその中に.scファイルを置くことにします。まずは以下の内容で classTest.sc というファイルを作ります。

Superclass {
  var <>x;
  var <>y = 10;

  init {
    x = 5;
  }
}

MyClass : Superclass {
  init {
    super.init;
    "hi".postln;
  }
}

var の後に続く <> はゲッタとセッタを意味します。 var <>x;x のゲッタとセッタを定義しています。

クラス名 : スーパークラス名 {} で継承を行います。スーパークラスのメソッドは super で参照します。

ファイルを保存したらCtrl+Shift+Iでクラスライブラリを再コンパイルします。続いて新しいファイルを開いて以下の内容を入力します。

{
  var me = MyClass.new;
  me.init;
  me.x.postln;
  me.y.postln;

  me.x = 100;
  me.x.postln;

}.value;

実行して次の出力が得られれば成功です。

hi
5
10
100
-> 100

SuperColliderでディレイとフェイザ

ディレイ

ヘルプではUGen > Delaysとして分類されています。エフェクトとしてのディレイには CombC などを使います。 DelayC などはただの遅延素子なのでフィードバックはかかりません。

exception in real time: alloc failed が出るときは s.options.memSize で確保するメモリを増やしてサーバを再起動します。デフォルトでは8192KBが確保されます。

s.options.memSize = 1000000; // 1GBのメモリを確保。

SynthDef(\delay,
  { | out(0), in(0), delayTime(0.2), decaytime(16), wet(0.5) |
    var input = In.ar(in, 2);
    var dry = input * (1 - wet);
    var delay = CombC.ar(input, 4, delayTime, decaytime, wet, dry);
    OffsetOut.ar(out, delay);
  }
).add;

TempoClock.tempo = 140 / 60;

~seq = Pbind(
  \degree, Pseq([0, 2.2, 4.35, 6.67, 8.81], inf),
  \dur, 0.5,
  \amp, 0.3,
  \out, 10
);
~delay = Pfx(~seq, \delay, \delayTime, 0.63, \in, 10).play;

以下はステレオディレイの例です。 Pfxb を使う場合は SynthDef の出力チャンネル数を s.options.numOutputBusChannels 以下にしないとエラーがでるようなので注意してください。

SynthDef(\delayStereo,
  { | out(0), in(0), delayTimeL(0.2), delayTimeR(0.15), decaytime(16)
    , wet(0.5) |
    var input = In.ar(in, 2);
    var dry = input * (1 - wet);
    var delayL = CombC.ar(input[0], 4, delayTimeL, decaytime, wet, dry[0]);
    var delayR = CombC.ar(input[1], 4, delayTimeR, decaytime, wet, dry[1]);
    OffsetOut.ar(out, [delayL, delayR]);
  }
).add;

~seq1 = Pbind(
  \out, 10,
  \degree, Pseq([[0, 3], [0, 5], [2, 5], [2, 7]], inf),
  \amp, 0.2
);

~seq2 = Pbind(
  \out, 10,
  \degree, Pseq([
    Pseq([Rest], 4),
    Pseq([14, 11, 8, Rest])
  ], inf),
  \dur, 4,
  \amp, 0.2
);

~seq3 = Pbind(
  \out, 10,
  \degree, Pseq([
    Pseq([Rest], 8),
    Pseq([-10, -9, -11, -10, -9, -11, -10, -14])
  ], inf),
  \dur, 8,
  \amp, 0.4
);

~par = Ppar([~seq1, ~seq2, ~seq3]);

TempoClock.tempo = 100 / 60;

~delay = Pfxb(~par, \delayStereo,
  \in, 10,
  \delayTimeL, TempoClock.beatDur * 0.78,
  \delayTimeR, TempoClock.beatDur * 0.666,
).play;

フィードバック

フィードバックを作るときは LocalInLocalOut が使えます。

{
  var input = LocalIn.ar(2, 1) * 10;
  var osc = SinOsc.ar(input, input * 1000, MouseX.kr(0.001, 0.5, 1));
  LocalOut.ar(osc);
  OffsetOut.ar(0, [osc, osc])
}.play;

フェイザ

ノッチができるフィルタを直列につないでカットオフ周波数をLFOで揺らすとフェイザになります。

以下の例では DelayCコムフィルタを作っています。 DelayC のかわりに AllpassC などを使っても似たような効果になります。

s.options.memSize = 1000000;

SynthDef(\phaser,
  { | out(0), in(0), rate(0.2), minDelay(0.0001), maxDelay(0.001)
    , dry(1), feedback(0.5) |
    var input = In.ar(in, 2);
    var inputFeedback = DelayC.ar(LocalIn.ar(2), 0.01, 0.0001, feedback);
    var phaser = List[];

    var lfo = LFPar.ar(rate).exprange(minDelay, maxDelay);
    var temp = input.do({ |channel, index|
      var filter = channel + inputFeedback[index];
      4.do({
        filter = DelayC.ar(filter, 1, lfo, 2/7, 0-filter * 5/7)
        // filter = AllpassC.ar(filter, 1, lfo, 0.001, 2/7, 0-filter * 5/7)
      });
      phaser.add(filter)
    });

    var output = phaser + (input * dry * 5/7);
    LocalOut.ar(output);
    OffsetOut.ar(out, output)
  }
).add;

~seq = Pbind(
  \out, 10,
  \scale, Scale.chromatic,
  \degree, Pseq([
    [0, 5, 9, 14],
    [2.01, 9.01, 16, -2.5],
    [3, 6.98, 8.1, 10],
    [4.97, 9, 10, 12],
    [8, 9.99, 11.5, 19],
    [3, 7.02, 8.01, 10],
    [5, 8.96, 10, 12.01],
    [-2, 1.41, 3, 5]
  ], inf),
  \dur, 1,
);

TempoClock.tempo = 130 / 60;

~phaser = Pfx(~seq, \phaser, \in, 10, \feedback, 0.8).play;

2017/05/16

SuperColliderでエフェクト

SynthDef で定義したエフェクトを InOut でルーティングします。

Bus

SuperColliderではBusを指定することでルーティングを行います。

BusにはAudioBusとControlBusの2つがあります。AudioBusは.ar、ControlBusは.krの管理に使います。ここでは単にBusと言う場合、AudioBusを指すことにします。

Bus0からの数チャンネルはハードウェアへの出力となります。例えばステレオ環境ならBus0が左、Bus1が右チャンネルとなります。 ServerOptions から出力チャンネル数を調べることができます。

// s = Server.local;
s.options.numAudioBusChannels; // ハードウェアの出力チャンネル数

以下でAudioBusとControlBusの最大チャンネル数を調べます。デフォルトの最大チャンネル数はAudioBusが1024、ControlBusが16384となります。

s.options.numAudioBusChannels;
s.options.numControlBusChannels;

In, Out

In で入力Bus、 Out で出力Busを指定します。以下の例ではBus10に出力した SawIn で受け取り、 BMoog フィルタを適用してBus0に出力します。

~bus = 10;

{
  var numChannels = s.options.numOutputBusChannels;
  var input = In.ar(~bus, numChannels);
  var filter = BMoog.ar(input, 500, 0.991);
  Out.ar(0, filter)
}.play;

{
  var osc = Saw.ar(100, 0.2);
  Out.ar(~bus, Pan2.ar(osc))
}.play;

Pfxb

パターンにエフェクトをかけるときは Pfxb が使えます。

thisThread.randSeed = 200;

SynthDef(\hihat,
  { | out(0), amp(1), pan(0), dur(0.1), curve(-64) |
    var envAmp = Env.perc(0, dur, 1, curve);
    var envGenAmp = EnvGen.ar(envAmp, doneAction: 2);
    var noise = FBSineC.ar(40000, 4, 1.02, 1.05);
    var dckill = BHiPass4.ar(noise, 240, 4, amp * envGenAmp);
    OffsetOut.ar(out, dckill)
  }
).add;

SynthDef(\delay,
  { | out(0), in(0), freq(0.1) |
    var input = In.ar(in, 1);

    var filter = input;
    var delay = 0.1;
    8.do({
      filter = AllpassL.ar(filter, delay, delay.rand, delay.rand, 0.5, filter)
    });

    OffsetOut.ar(out, Pan2.ar(filter))
  }
).add;

TempoClock.default.tempo = 118 / 60;

~bind = Pbind(
  \instrument, \hihat,
  \out, 10, // bus 10 に出力。
  \dur, 0.25,
  \curve, Pseq([-39, -70, -2, -33], inf),
  \amp, 0.1
);

Pfxb(
  ~bind,
  \delay,
  \in, 10,  // bus 10 から入力。
  \out, 0,
  \freq, 0.1
).play;

2017/05/10

SuperColliderでSync

私が調べた範囲では汎用的にオシレータシンクを行う方法が見当たりませんでした。ここでは場当たり的な実装をいくつか紹介します。

正確にHard syncできるのはphaseを入力できるgeneratorを使う方法です。次点でPlaybufですが位相リセット時にクリックノイズが乗ります。EnvGenは波系が歪んでいます。LoopBufはBufferのループ回数にばらつきが出ます。

Deterministic generatorでsync

phase を入力できるgeneratorは周波数を0にして LFSaw などで位相を回すことでsyncできます。

次の例の slave は上のどれかを使う必要があります。

{
  var freq = MouseY.kr(30, 3000, 1);
  var ratio = MouseX.kr(1, 100, 1);
  var master = LFPulse.ar(freq);
  var syncPhase = LFSaw.ar(freq, 0, pi * ratio);
  var slave = SinOsc.ar(0, syncPhase % 2pi, 1);
  var osc = master.range(0, 1) * slave;
  OffsetOut.ar(0, Pan2.ar(osc, 0, 0.5))
}.play;

EnvGenでsync

EnvGen もゲートを使ってsyncできます。ゲートについてはヘルプに解説があります。IDEでは EnvGen にカーソルを合わせてCtrl+Dでヘルプを開けます。

{
  var freq = MouseY.kr(30, 3000, 1);
  var ratio = MouseX.kr(1, 100, 1);
  var gate = LFPulse.ar(freq, 0, 0.5, 2, -1);

  var levels = [0] ++ [1, 0.9, 0, -0.9, -1] ++ [0];
  var waveSize = levels.size - 3;
  var times = [0] ++ { 1 / (ratio * freq) / waveSize }.dup(waveSize) ++ [0];
  var env = Env(levels, times, \lin, levels.size - 2, 0);
  var envGen = EnvGen.ar(env, gate);

  OffsetOut.ar(0, Pan2.ar(envGen, 0, 0.5))
}.play;

levelstimes の前後に [0] を加えている理由を説明します。まずは Env のループで使われる引数の releaseNodeloopNode の挙動を確認します。それぞれ4番目と5番目の引数です。以下の例を順に再生してみてください。

Env([1, 0, -0.1, 0], [1, 1, 1], \lin, 2, 0).test(5).plot; // (1)
Env([1, 0, -0.1, 0], [1, 1, 1], \lin, 2, 1).test(5).plot; // (2)
Env([1, 0, -0.1, 0], [1, 1, 1], \lin, 3, 0).test(5).plot; // (3)

(1)と(2)を再生すると、ループ開始点の値は levels[loopNode + 1] 、ループ終了点の値は levels[releaseNode] であることがわかります。

(3)を再生するとループしません。 releaseNode の値に levels.size - 1 を指定するとリリース後のエンベロープが無くなってしまうのでループしないようです。

つまり [0] を前に加えるのはループ開始点を正しく設定するため、後に加えるのは Env をループさせるため、ということです。

この方法の元ネタは DemandEnvGen のヘルプにあった例です。あわせて参照してみてください。

Bufferでsync

Buffer を用意して PlayBuf をトリガすることでsyncできます。

~buffer = Buffer.alloc(s, 512).sine1(1.0, true, false, true);
{
  var freq = MouseY.kr(30, 3000, 1);
  var rate = ~buffer.numFrames / SampleRate.ir * freq * MouseX.kr(1, 32, 1);
  var trigger = Impulse.ar(freq) - 0.5;
  var loopBuf = PlayBuf.ar(
    ~buffer.numChannels,
    ~buffer.bufnum,
    rate,
    trigger,
    0,
    1
  );
  var master = LFPulse.ar(freq).range(0, 1);
  OffsetOut.ar(0, Pan2.ar(master * loopBuf, 0, 0.5))
}.play;

// ~buffer.free;

LoopBuf でもsyncできます。 LoopBuf の利点はループ開始点を指定できる点です。

~buffer = Buffer.alloc(s, 512).sine1(1.0, true, false, true);
{
  var freq = MouseY.kr(30, 3000, 1);
  var rate = ~buffer.numFrames / SampleRate.ir * freq * MouseX.kr(1, 32, 1);
  var gate = LFPulse.ar(freq);
  var loopBuf = LoopBuf.ar(
    ~buffer.numChannels,
    ~buffer.bufnum,
    rate,
    gate,
    0,
    0,
    ~buffer.numFrames - 1,
    4
  );
  OffsetOut.ar(0, Pan2.ar(loopBuf, 0, 0.5))
}.play;

// ~buffer.free;

2017/05/09

SuperCollider - オシレータの位相

トーンクラスターを作るときなどは音が演奏されるたびに位相をリセットしたくない場合があります。以下の例ではパターンから現在の時刻をシンセに与えることで位相の進みを計算しています。

位相は普通 [0, 2pi) の範囲の値を指定しますが LFSaw では [0, 2) の範囲で位相を指定します。

SystemClockInterpreter 側で動作しているようで SynthDef の中で呼び出した場合は add でサーバに送られる時点で計算されて定数になるようです。パターンで指定する場合も {} で囲って関数にしないと play が呼び出された時点で定数になってしまいます。

SynthDef(\phase,
  { | out, freq, dur, pan, amp, seconds |
    var env = Env([1, 0], [dur], 6);
    var envGen = EnvGen.ar(env, doneAction: 2);
    var osc = LFSaw.ar(freq, (2 * freq * seconds) % 2);
    OffsetOut.ar(out, Pan2.ar(osc, pan, amp * envGen))
  }
).add;

SynthDef(\reset,
  { | out, freq, dur, pan, amp, seconds |
    var env = Env([1, 0], [dur], 6);
    var envGen = EnvGen.ar(env, doneAction: 2);
    var osc = LFSaw.ar(freq, 0);
    OffsetOut.ar(out, Pan2.ar(osc, pan, amp * envGen))
  }
).add;

TempoClock.default.tempo = 70 / 60;

~scale = Scale.chromatic(Tuning.chalmers);
~root = 0;

~seqPhase = Pbind(
  \instrument, Pseq([Pseq([\phase], 8), Pseq([\reset], 8)], inf),
  \scale, ~scale,
  \root, ~root,
  \degree, Pseq([[-7, -3.05, -2.98, 0, 1.98, 2, 4, 4.01, 4.02, 5, 5.04, 7]], inf),
  \dur, 0.25,
  \amp, 0.05,
  \seconds, {SystemClock.seconds}
).play;

2017/05/06

SuperColliderのスケールとパターン

SuperColliderの音律とスケール、パターン、テンポについてです。

2017/05/04

SuperColliderのエンベロープなど

SuperColliderのエンベロープ、式の評価順、マウスとキーボードの入力、arとkrについてです。