TidalCyclesのSuperColliderのエンジンSuperDirtには、サンプルだけではなく独自のシンセを楽器として加えたり、エフェクトを追加したりはたまたカスタムイベントをブリッジしたりSuperColliderの柔軟性を活かした'Hack'が存在します。ライブラリパッケージの hacks/
フォルダには様々なハックの方法を紹介するドキュメントを見つけることができます。
その中のTidalのパラメータを追加して自前のエフェクトを作る方法を紹介します。
hacks/adding-effects.scd
というドキュメントに例としてspectral-delay
というエフェクトを追加するサンプルコードが紹介されています。
コメントで追加の手順が示されていますが、やることは三つです。
- add the desired parameters to Tidal, so it can be used
- add a module definition to Superdirt, so it can be found when the parameter is not nil
- add the synth def to SuperDirt, so it can be played
- Tidalでパラメータとして使用できるようにHaskellのコードを追加する。
- SuperDirt側にモジュール追加しパラメータが使用できるようにする。
- SuperDirtにエフェクトのSynthを定義する。
1 Tidalのパラメータを加える
Tidal側でHaskellコードでパラメータを追加します。
サンプルではライブラリ内 Sound/Tidal/Params.hs
に加えると指示がありますが、
編集後に再度ライブラリをコンパイルする必要が出てきてしまうのでその次のコメントアウトの // ... or you can run this in the interpreter:
の方法を今はお勧めします。
Tidalのブート後に以下のコードを実行します。
エディタでブート後let
で始まっている二つのブロックをそれぞれ実行します。
let tsdelay = make' VF tsdelay_p
tsdelay_p = F "tsdelay" Nothing
let xsdelay = make' VI xsdelay_p
xsdelay_p = I "xsdelay" Nothing
ライブラリを直接騙取する場合、TidalCyclesライブラリ内 Sound/Tidal/Params.hs
に以下のコードを加えます。末尾に追加するのがいいでしょう。その後cabalコマンドでパッケージを際コンパイルします。
tsdelay :: Pattern Double -> ParamPattern
tsdelay = make' VF tsdelay_p
tsdelay_p = F "tsdelay" Nothing
xsdelay :: Pattern Int -> ParamPattern
xsdelay = make' VI xsdelay_p
xsdelay_p = I "xsdelay" Nothing
Haskellコードの文法については今はまだ詳しく理解しなくて良いでしょう。
大事なのはTidalのパラメータとしてtsdelay
xsdelay
の二つをパラメータをここで追加していることです。
それぞれのパラメータに与える引数の値の型もここで指定されています。
Pattern Double
・make' VF
・I
/ Pattern Int
・make' VI
・F
という組み合わせに注目してください、前者は Double の浮動小数点数値のパターン、 後者は Int 整数値のパターンを引数に受けるパラメータを追加しています。
つまりtsdelay
は Pattern Double を xsdelay
は Pattern Int を引数に取るようになります。
これでTidalのコードからエフェクトのパラメータとして使えるようになります。
Tidalで使う時にはおそらく以下のようなコードになるでしょう
d1 $ sound "bd*4" |+| tsdelay "0.4"
d1 $ sound "bd*4" |+| xsdelay "1"
自分はライブラリを直接編集したくは無い&再コンパイルしたくないのでこの部分を.hsファイルとして分けておいて、Tidalのブート時にHaskellのモジュールをロードする方法で使っています。この方法についてはまた別の機会に解説したいです。
2 SuperDirtにモジュールを追加する
次にSuperDirt側でmoduleと呼ばれる設定を定義します。これによってTidalで生成されたパラメータがSuperDirでエフェクトのシンセに適切にマップされます。
このコードと次の3
の手順のコードはSCドキュメントで保存してブート時に実行するようにします。
とりあえず今はexternal_effect.scd
などとして保存しておきます。ブート時に実行する方法は後ほど解説します。
(
~dirt.addModule('spectral-delay', { |dirtEvent|
dirtEvent.sendSynth('spectral-delay' ++ ~dirt.numChannels,
// OPTIONAL
// passing this array of parameters could be left out,
// but it makes it clear what happens
[
xsdelay: ~xsdelay,
tsdelay: ~tsdelay,
sustain: ~sustain,
out: ~out
]
)
}, { ~sdelay.notNil or: { ~xsdelay.notNil } }); // play synth only if at least one of the two was given
)
~dirt
はSuperDirtをブートした時にSuperDirtのグローバルオブジェクトが割り当てられています。
ですのでこの手順と次の3
の手順のコードはSuperDirtをブートした後に実行しなければいけません。
重要なのは~dirt.addModule
の最初の引数、'spectral-delay'
、これは作成するモジュールの名前で後で使用します。任意の文字列でユーザーが定義できます。わかりやすい名前にしましょう。また.sendSynth()
関数での第一引数 'spectral-delay' ++ ~dirt.numChannels
にも同じ名前でシンセの名前を指定します。
もう一つは第二引数の中のリストです。ここに列挙したパラメータがSynthの引数に渡ってくるようになります。
[
xsdelay: ~xsdelay,
tsdelay: ~tsdelay,
sustain: ~sustain,
out: ~out
]
今回自分で定義するパラメータ以外にも、Synthの中で使用するものは加えておきます。例えばout
パラメータはデフォルトのパラメータで大体のエフェクトで使用することになります。sustain
もデフォルトのパラメータです。
内部的な処理の話になりますが、ここに列挙されているパラメータがOSC渡ってきます *1
.addModule
の第二引数にFunctionのブラケットでシンセが適用される条件を定義します。
この条件が揃った時に、でエフェクトのノードが楽器のシンセとグローバルミキサーとの間に差し込まれてエフェクトがかかります。
{ ~sdelay.notNil or: { ~xsdelay.notNil } }
サンプルでは上のようになっています。この場合 tsdelay
xsdelay
のどちらか一方でもパラメーターが渡ってきた場合にエフェクトが追加されます。
最後にモジュールの順番を指定します。この設定はオプショナルです。
// here you can see the effect order:
~dirt.modules;
// OPTIONAL: you can reorder the effects, if you want e.g. the lpf to come after the delay:
~dirt.orderModules(['spectral-delay', 'hpf', 'klm']);
楽器のエフェクターを思い出してください。エフェクターでは信号を通す順番が重要です。SuperDirtのオーディオ信号処理でも通ってくるエフェクターの順番で最終的な出音は全く違うものになります。ここでは.orderModules()
でリストを渡して順番を指定できます。
*1このリストは任意とコメントにあるので省略可能?リストにないと引数で渡って来なくなるのか要検証。
3 エフェクトシンセを追加する
最後にSuperDirt側でエフェクトのシンセを追加します。
// (3) make a synthdef (for more examples see core-synths)
(
var numChannels = ~dirt.numChannels;
SynthDef("spectral-delay" ++ numChannels, { |out, tsdelay, xsdelay = 1, sustain|
var signal, delayTime, delays, freqs, filtered;
var size = 16;
var maxDelayTime = 0.2;
signal = In.ar(out, numChannels);
delayTime = tsdelay * maxDelayTime;
filtered = (1..size).sum { |i|
var filterFreq = i.linexp(1, size, 40, 17000);
var sig = BPF.ar(signal, filterFreq, 0.005);
// the delay pattern is determined from xsdelay by bitwise-and:
DelayN.ar(sig, maxDelayTime, i & xsdelay * (1/size) * delayTime )
};
signal = signal * 0.2 + (filtered * 4); // this controls wet/dry
ReplaceOut.ar(out, signal)
}).add;
)
-
"spectral-delay" ++ numChannels
というシンセの名前が先ほどのモジュールの宣言と同じになっています。 -
先ほどリストで列挙したパラメータを引数に持っています。
-
ReplaceOut.ar
でアウトチャンネルをout
パラメータで指定しています。
守ることは以上で、他は自由にデザインできるようです(本質的には全て自由ですが)
変数 numChannels
は SuperDirt をブートした時のチャンネル数を参照しています。シンセの名前以外にも In.ar
のチャンネル数などに使われて重要そうなのでブート時に一度 SuperDirt のグローバルオブジェクトから取得するのが良さそうです。
bootする
それぞれ手順を踏んだら Tidal と SuperDirt の起動です。
Tidal はライブラリに手を加えた場合はそれぞれのエディタ環境でブートします。 パラメータを追加するコードを実行した場合はすでにブートできていると思います。
エラーなく boot できたら次は SuperCollider 側で SuperDirt を起動します。
その前に、ブート後に先ほどのscdに書いたコードを実行したいので、waitForBoot{}
でサーバのブート完了をフックして処理させるようにしておきましょう。
ファイルのパスの文字列に .load
コマンドでscdファイルを指定して実行できます。
その後 SuperDirt.start
でブートさせるようにします。
(
s.waitForBoot{
"/your/path/to/external_effect.scd".load;
};
SuperDirt.start(2, s);
)
("/your/path/to/external_effect.scd"
の部分をそれぞれ先ほど保存した scd のパスに)
もちろんファイルに保存しなくても、先ほどのコードをエディタで選択して実行して行っても同じです。
以上でエフェクトを使えるようになったと思います。あとは独自のエフェクトを追加して豊かなサウンドにしていってください。
以降は私が追加したエフェクトをいくつか紹介します。
Wah
wah
: ワウペダル風、引数の値はローパスフィルタのfreqにかけるノイズの頻度。cutoff
と違い一回のシンセの中で変化がある。
parameter :
wah :: Pattern Double -> ParamPattern
wah = make' VF wah_p
wah_p = F "wah" (Just 0.25)
module :
~dirt.addModule('wah', { |dirtEvent|
dirtEvent.sendSynth('wah' ++ numChannels,
[
out: ~out,
wah: ~wah
]
)
}, { ~wah.notNil });
synth :
var numChannels = ~dirt.numChannels;
SynthDef("wah" ++ numChannels, { arg out, pan = 0, wah = 0;
var index, in, mix, signal;
index = Amplitude.kr((wah.ceil).clip(0,1));
in = In.ar(out, 2);
mix = RLPF.ar(in, LFNoise2.kr(wah.linexp(0, 1.0, 0.8, 40), 40, 84).midicps, 0.2);
signal = Select.ar(index, [in, mix]);
ReplaceOut.ar(out, Pan2.ar(signal, pan));
}).store;
BinScramble
binscr
: FFTの PV_BinScramble
を使ったエフェクト、ちょっと音が荒れる。引数の値はミックスレート。
parameter :
binscr :: Pattern Double -> ParamPattern
binscr = make' VF binscr_p
binscr_p = F "binscr" (Just 0)
module :
~dirt.addModule('binscramble', { |dirtEvent|
dirtEvent.sendSynth('binscramble' ++ numChannels,
[
out: ~out,
bscr: ~binscr
]
)
}, { ~binscr.notNil });
synth :
var numChannels = ~dirt.numChannels;
SynthDef("binscramble" ++ numChannels, { arg out, pan = 0, binscr = 0;
var signal, chain, chain_r, lfo, lfo2, trig;
signal = In.ar(out, numChannels) * 1.2;
chain = FFT(LocalBuf(2048), signal[0]);
chain_r = FFT(LocalBuf(2048), signal[1]);
lfo = LFNoise2.kr(4).abs;
lfo2 = LFNoise2.kr(2).abs;
trig = Impulse.kr(32);
chain = PV_BinScramble(chain, lfo, lfo2, trig);
chain_r = PV_BinScramble(chain_r, lfo, lfo2, trig);
ReplaceOut.ar(out, Pan2.ar((signal * (1.0 - binscr)) + (binscr * [IFFT(chain), IFFT(chain_r)]), pan));
}).add;
BinShift
binshf
: FFTの PV_BinShift
によるピッチベンド。引数の値はミックスレート。stretch の値はとりあえず MouseX
にしている、パラメータ与えてもよいかも。
parameter :
binshf :: Pattern Double -> ParamPattern
binshf = make' VF binshf_p
binshf_p = F "binshf" (Just 0)
module :
~dirt.addModule('binshift', { |dirtEvent|
dirtEvent.sendSynth('binshift' ++ numChannels,
[
out: ~out,
binshf: ~binshf
]
)
}, { ~binshf.notNil });
synth :
var numChannels = ~dirt.numChannels;
SynthDef("binshift" ++ numChannels, { arg out, pan = 0, binshf = 0;
var signal, chain, chain_r, stretch;
signal = In.ar(out, numChannels) * 1.2;
chain = FFT(LocalBuf(2048), signal[1]);
chain_r = FFT(LocalBuf(2048), signal[0]);
stretch = MouseX.kr(0.25, 4, \exponential);
chain = PV_BinShift(chain, stretch);
chain_r = PV_BinShift(chain_r, stretch);
ReplaceOut.ar(out, Pan2.ar((signal * (1.0 - binshf)) + (binshf * [IFFT(chain), IFFT(chain_r)]), pan));
}).add;
BinFreeze
binfrz
: FFT の PV_MagFreeze
を使ったフリージング。引数の値はミックスレート。MouseY
でコントロール。とりあえず画面の上半分に行ったらフリーズするようになってる。
parameter :
binfrz :: Pattern Double -> ParamPattern
binfrz = make' VF binfrz_p
binfrz_p = F "binfrz" (Just 0)
module :
~dirt.addModule('binfreeze', { |dirtEvent|
dirtEvent.sendSynth('binfreeze' ++ numChannels,
[
out: ~out,
binfrz: ~binfrz
]
)
}, { ~binfrz.notNil });
synth :
var numChannels = ~dirt.numChannels;
SynthDef("binfreeze" ++ numChannels, { arg out, pan = 0, binfrz = 0;
var signal, chain, chain_r, freeze;
signal = In.ar(out, numChannels) * 1.2;
chain = FFT(LocalBuf(2048), signal[0]);
chain_r = FFT(LocalBuf(2048), signal[1]);
freeze = MouseY.kr;
chain = PV_MagFreeze(chain, freeze > 0.5);
chain_r = PV_MagFreeze(chain_r, freeze > 0.5);
ReplaceOut.ar(out, Pan2.ar((signal * (1.0 - binfrz)) + (binfrz * [IFFT(chain), IFFT(chain_r)]), pan));
}).add;
Convolution
conv
: Convolution を使ったフィルター。kernel に LFPulse
を使っているがここをもっと工夫して違う音色にしたいところ。
parameter :
conv :: Pattern Double -> ParamPattern
conv = make' VF conv_p
conv_p = F "conv" (Just 0)
module :
~dirt.addModule('convolution', { |dirtEvent|
dirtEvent.sendSynth('convolution' ++ numChannels,
[
out: ~out,
conv: ~conv
]
)
}, { ~conv.notNil });
synth :
var numChannels = ~dirt.numChannels;
SynthDef("convolution" ++ numChannels, { arg out, note, pan = 0, conv = 0;
var in, karnel, mix;
karnel = LFPulse.ar((note + 60).midicps, 0.0, 0.15, 0.2) + Impulse.ar((note + 60).midicps, 0.0, 0.4);
in = In.ar(out, numChannels);
mix = [Convolution.ar(in[0], karnel, 1024, 0.5), Convolution.ar(in[1], karnel, 1024, 0.5)];
ReplaceOut.ar(out, Pan2.ar((in * (1.0 - conv)) + (conv * mix), pan));
}).add;