【SuperCollider】インパルス応答からリバーブをかける方法

# はじめに

一般的にデジタルリバーブをかける場合、選択肢は二つあります。

- アルゴリズミックリバーブ
- コンボリューションリバーブ

## アルゴリズミックリバーブ

Schroeder Reverbなどで検索するといろいろ出てきますが、デジタルで残響音をつくり出すためのアルゴリズムです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437488/81a2a6da-e9b6-e513-0a94-3117086f4c36.png)
出典: https://maxforlive.com/library/device/9181/schroeder-reverb


Schroeder Reverbは最初に考案されたアルゴリズミックリバーブですが、少ないマシンパワーでもリアルタイムにエフェクトをかけられるため現在でも使われています。
もちろんSuperColliderでも実装できます。
https://doc.sccode.org/Tutorials/Mark_Polishook_tutorial/Japanese_version/13.html#リバーブレーション

## コンボリューションリバーブ

アルゴリズミックリバーブが現実の残響ではなく機械的に作ったものであるのに対して、コンボリューションリバーブは現実の残響を利用するものです。

### インパルス応答

たとえば一瞬だけ鳴る破裂音があるとします。これを空間の中で再生すると、その空間の響き方によって残響が変わります。
このときこの残響を含めた音をインパルス応答といって、これがあればどんな音にでも同じ残響音を付与することができます。(元の音にインパルス応答の波形の値を時間方向にかけてあげるだけ)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437488/8e026f34-aa6f-136b-e0f3-718d69fbb87f.png)
出典: http://www.ari-web.com/service/soft/reverb-4.htm

この計算方法は感覚的にもわかりやすいですが、実際には計算回数が多いためこのままだとリアルタイム処理には向きません。

### フーリエ変換

そこで活躍するのがフーリエ変換(FFT)で、サンプルの数だけ乗算しないといけなかったのがFFT後の世界では一度の乗算で計算が完了してしまいます。あとはこれを逆フーリエ変換(IFFT)するだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437488/1b023ce6-a3f7-54ae-d790-be21acecd276.png)
出典: http://www.ari-web.com/service/soft/reverb-4.htm

## インパルス応答データ

ちなみにインパルス応答を録音するためには、静寂な環境で理想的なインパルス音源(周波数に偏りがなく、音も限りなく短い大音量なノイズ)である必要があります。
もちろん手を叩いた音を録音することでもある程度代用できますが、高品質なインパルス応答を得るのは少し難しいです。
そこでOpenAirというWebサイトでは高品質なインパルス応答が無料で公開されているため、こちらをダウンロードして使ってみるのが最初はいいかもしれません。
https://www.openair.hosted.york.ac.uk/?page_id=36

# コンボリューションリバーブの実装

SuperColliderで実装します。

デフォルトのパッケージだと畳み込み演算の処理が遅いので、[EZConv](https://github.com/davidgranstrom/EZConv)というクラスをインストールして使っていきます。

## EZConvのインストール方法

SuperColliderのエディタを開き、

```supercollider
Quarks.install("https://github.com/davidgranstrom/EZConv.git");
```

を実行するだけでインストールできます。

実行後はSuperColliderを再起動するか、Recompile Class Libraryすれば`EZConv`クラスが使用できるようになると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437488/d655b096-f1a9-8673-b0a6-e53f162f0af1.png)

## EZConvの使い方

こんなフォルダ構成になっているものとします。

```terminal
$ tree
.
├── IRs
│   └── residential_area.wav
└── test.scd
```

residential_area.wavは住宅街で録音した簡易インパルス応答ですが、[こちら](https://github.com/mynkit/SuperDirt/raw/v1.7.3/dev/miyano/synths/residential_area.wav)からダウンロードできます。

下記のような`test.scd`を用意して、それぞれCmd+Enter(WindowsはCtrl+Enter)して実行してみてください。

```supercollider:test.scd
// Load your IR.
(
~path=thisProcess.nowExecutingPath.dirname ++ "/IRs/residential_area.wav";
~ezConv=EZConv(~path, fftSize: 2048);
)

// Unload the IR. You might want to do this before loading a new one.
// ~ezConv.free;

// 以下をCmd+Enterで実行すると残響音付きのカエルのような声がでます
// 1行目のwetが残響音、dryが原音なので調整すると遊べます。
(
SynthDef("frog", { arg out, pan=0, variation=0.9, wet=0.9, dry=0.1;
	var dt, n, freq, mul, t, u, p, amp=0.1;
	n = Rand(7, 35);
	dt = 25.0 + Rand(-1.7, 1.7);
	dt = dt + LFNoise2.kr(2, variation) * 0.001;
	freq = 901 + Rand(0, 65);
	t = Impulse.ar(dt.reciprocal, 0, 100);
	mul = PulseCount.ar(t) < n;
	u = BPF.ar(mul * t, freq, 0.1);
	u = BPF.ar(u, freq, 0.2);
	u = u!2 * 5;
	// コンボリューションリバーブ
    // *0.03部分は、インパルス応答音源のボリューム調整
	p=~ezConv.ar([u[0],u[1]],0,1)*0.03; //(signals, leak, mult)
	u=(p*wet)+(u*dry);
	// 残響音がなくなってからnodeを解放するためDetectSilenceを使用
	DetectSilence.ar(u, doneAction:2);
	Out.ar(out, u*amp);
}).play;
)
```

# SuperDirt, TidalCyclesのためのリバーブエフェクト追加

自分はこのコンボリューションリバーブをグローバルエフェクトとして定義したかったので、以下のようにSuperDirtを変更しています。

- residential_area.wavを下記ディレクトリに配置(Mac)
`~/Library/Application Support/SuperCollider/downloaded-quarks/SuperDirt/synths/`

- `~/Library/Application Support/SuperCollider/downloaded-quarks/SuperDirt/classes/DirtOrbit.sc`を以下のように編集

```diff_super_collider:DirtOrbit.sc
...

	initDefaultGlobalEffects {
		this.globalEffects = [
			// all global effects sleep when the input is quiet for long enough and no parameters are set.
			GlobalDirtEffect(\dirt_delay, [\delaytime, \delayfeedback, \delaySend, \delayAmp, \lock, \cps]),
			GlobalDirtEffect(\dirt_reverb, [\size, \room, \dry]),
+			GlobalDirtEffect(\residential_area_reverb, [\resi]),
			GlobalDirtEffect(\dirt_leslie, [\leslie, \lrate, \lsize]),
			GlobalDirtEffect(\dirt_rms, [\rmsReplyRate, \rmsPeakLag]).alwaysRun_(true),
			GlobalDirtEffect(\dirt_monitor, [\limitertype]).alwaysRun_(true),
		]
	}

...
```

- `~/Library/Application Support/SuperCollider/downloaded-quarks/SuperDirt/synths/core-synths-global.scd`を以下のように編集

```diff_super_collider:core-synths-global.scd
...

{
	var numChannels = ~dirt.numChannels;
+   var residentialAreaIRPath=thisProcess.nowExecutingPath.dirname ++ "/residential_area.wav";
+   var residentialAreaConv=EZConv(residentialAreaIRPath, fftSize: 2048);

...

SynthDef("dirt_reverb" ++ numChannels, { |dryBus, effectBus, gate = 1, room = 0, size = 0.1, dry = 0|
		var in, snd, loop, depth;

		in = In.ar(dryBus, numChannels).asArray.sum;

		in = in * room.lag(LFNoise1.kr(1).range(0.01, 0.02)); // regulate input

		4.do { in = AllpassN.ar(in, 0.03, { Rand(0.005, 0.02) }.dup(numChannels), 1) };

		depth = size.lag(0.02).linexp(0, 1, 0.01, 0.98); // change depth between 0.1 and 0.98
		loop = LocalIn.ar(numChannels) * { depth + Rand(0, 0.05) }.dup(numChannels);
		loop = OnePole.ar(loop, 0.5);  // 0-1

		loop = AllpassN.ar(loop, 0.05, { Rand(0.01, 0.05) }.dup(numChannels), 2);

		loop = DelayN.ar(loop, 0.3, [0.19, 0.26] + { Rand(-0.003, 0.003) }.dup(2));
		loop = AllpassN.ar(loop, 0.05, { Rand(0.03, 0.15) }.dup(numChannels), 2);

		loop = loop + in;
		loop = LeakDC.ar(loop);

		LocalOut.ar(loop);

		snd = loop;
		snd = snd * (1 - dry).lag(LFNoise1.kr(1).range(0.01, 0.02));

		DirtPause.ar(snd, graceTime:4);

		snd = snd * EnvGen.kr(Env.asr, gate, doneAction:2);

		Out.ar(effectBus, snd);

	}, [\ir, \ir]).add;

+   SynthDef("residential_area_reverb" ++ ~dirt.numChannels, { |dryBus, effectBus, resi|
+		var signal = In.ar(dryBus, ~dirt.numChannels);
+		var process, gate = 1;
+
+		process=residentialAreaConv.ar([signal[0],signal[1]],0,1)*0.03; //(signals, leak, mult)
+		signal=(process*resi)+(signal*(1-resi));
+
+		signal = signal * EnvGen.kr(Env.asr, gate, doneAction:2);
+
+		DirtPause.ar(signal, graceTime:4);
+
+		Out.ar(effectBus, signal);
+	}, [\ir, \ir]).add;

...

```

TidalCyclesでresiエフェクトを使いたいので、BootTidal.hsに以下を追加

```diff_haskell:BootTidal.hs
...

+ :{
+ resi = pF "resi"
+ :}

...
```

これで、

```haskell:sample.tidal
d1
	$ s "bd"
 	# resi 1
```

のようにすればresiエフェクトが使えるようになると思います。