【SuperDirt】ピンポンディレイの実装

# はじめに

ピンポンディレイとは、音が一度鳴ったあとのディレイ音が左右のpanに交互に振れていくようなディレイのことです。

以前TidalCyclesの[stut'](https://tidalcycles.org/docs/reference/time/#stutwith)を用いてピンポンディレイを作った記事を書いたので、そちらもよければご確認ください。

https://qiita.com/s2hap/items/5fdc270e45427031fd65

最初に、

- 通常のディレイ
- TidalCyclesのエフェクト([stut'](https://tidalcycles.org/docs/reference/time/#stutwith))を用いたピンポンディレイ
- SuperDirtで自作したピンポンディレイ

それぞれの音を聞いていただければと思います。

<iframe width="100%" height="450" src="https://www.youtube.com/embed/bPfNF51a7tU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# stut'ピンポンディレイの課題

## CPU使用率が高い

さきほどの動画で、SuperCollider右下の緑文字の値を見ていただくと(一番左の平均CPU使用率)、通常2%台であるのに対してstut'でのピンポンディレイのときだけ稀に3%台になっています。

通常のディレイが、一度遅延した音を再度フィードバックさせて遅延音を重ねているというシンプルな機構になっているのに対し、stut'は全ての遅延音を再生させているということもありその分CPUに負荷がかかります。

今回のようなそこまで再生するものが多くないときは問題ないですが、ディレイをかける対象が増えたりすると処理落ちしたりします。

## 意図通り動作しないことがある

例えば、TidalCyclesで以下のコードを実行してみてください。

```haskell
do
  resetCycles
  d1
    $ seqP [
    (1, 3, (
      ((# delay 0.5).(# delayt (1/8)).(# delayfb 0.7).(# lock 1))
      $ s "notes:2 [~ notes:2]"
      # gain 0.6
    ))
    ]
```

[seqP](https://tidalcycles.org/docs/patternlib/tour/composition/#seqp)を用いて、1サイクル目から3サイクル目の手前(つまり2サイクル目)まで再生しています。当然ディレイがかかっているので3サイクル目以降も音がしばらく残ります。
(ちなみに[resetCycles](https://tidalcycles.org/docs/patternlib/howtos/startpattern/#resetcycles)は0サイクル目までリセットする関数ですが、遅延があって使いづらいのでこちらの[resetCyclesTo関数](https://qiita.com/s2hap/items/88ab4ebb45882f7b339d)を使ったほうが良いです。)

続いてこちらのコードを実行してみてください。

```haskell
do
  resetCycles
  d1
    $ seqP [
    (1, 3, (
      stut' 2 (1/8) ((|* gain 0.9).(|> pan (1)))
      $ stut' 8 (1/4) ((|* gain 0.8).(|> pan (0)))
      $ s "notes:2 [~ notes:2]"
      # gain 0.6
    ))
    ]
```

同様に1,2サイクル目のときのみ再生させてますが。こちらは2サイクル目再生が終わると再生がストップし、3サイクル以降は音が残りません。

前述したように、stut'は全ての遅延音を再生させているだけなので、サイクル指定した場合その範囲外の音は再生されない、ということになっているようです。

使い方によってはこれが良いときもあると思いますが、指定していないサイクル以降も音が残って欲しい場面も多いはずです。


# globalEffectsを定義する

そこで、通常のdelayエフェクトと同様な使い勝手のピンポンディレイを作ると良さそうです。

通常のdelayはSuperDirtの[synths/core-synths-global.scd](https://github.com/musikinformatik/SuperDirt/blob/7e245e87f8f08a3d55e33a360bc97912f32fe69b/synths/core-synths-global.scd#L77-L127)で定義されています。

 (ちなみにパラメータは`delaytime, delayfeedback, delaySend, delayAmp, lock`などで`delay`パラメータが存在しませんが、delayAmpがdelayパラメータにあたります。[ここ](https://github.com/musikinformatik/SuperDirt/blob/7e245e87f8f08a3d55e33a360bc97912f32fe69b/classes/DirtEvent.sc#L128)で定義されています。)

また、globalEffectsの追加方法は[hacks/adding-a-compressor.scd](https://github.com/musikinformatik/SuperDirt/blob/develop/hacks/adding-a-compressor.scd)あたりを参考にすれば出来そうです。

このコードたちを参考にピンポンディレイを自作してみましょう。

## コード

```supercollider
SuperDirt.start;
```
したあとに以下のコードをSuperCollider　上で実行すれば、ピンポンディレイが定義されるはずです。(※ちなみに、自分の好みとしてlock=1でしたdelayコマンドを使うことがないので、lock=1の前提でlockはパラメータに含めてません。lockを有効化したい方はご自身で試してみてください！)

```supercollider
// ここの値は https://github.com/musikinformatik/SuperDirt/blob/develop/classes/DirtOrbit.sc#L59-L66 を参考にした
(
~dirt.orbits.do { |x|
	x.globalEffects = [
		GlobalDirtEffect(\dirt_delay, [\delaytime, \delayfeedback, \delaySend, \delayAmp, \lock, \cps]),
		GlobalDirtEffect(\pingpong_delay, [\pingpongt, \pingpongfb, \pingpong, \cps]), // 追加
		GlobalDirtEffect(\dirt_reverb, [\size, \room, \dry]),
		GlobalDirtEffect(\dirt_leslie, [\leslie, \lrate, \lsize]),
		GlobalDirtEffect(\dirt_rms, [\rmsReplyRate, \rmsPeakLag]).alwaysRun_(true),
		GlobalDirtEffect(\dirt_monitor, [\limitertype]).alwaysRun_(true)
	]
};

SynthDef("pingpong_delay" ++ ~dirt.numChannels, { |dryBus, effectBus, gate = 1, pingpongt, pingpongfb, pingpong, cps = 1|
	var signal = In.ar(dryBus, ~dirt.numChannels);
	var maxDelayTime = 4;
	var delaytime, delayfeedback, delayAmp, decayTime, left, right, dry;

	delayAmp = pingpong;
	delaytime = pingpongt;
	delayfeedback = pingpongfb;

	delayfeedback = delayfeedback.clip(0, 0.99);
	delaytime = delaytime * reciprocal(cps);
	delaytime = delaytime.clip(0, maxDelayTime); // just to be sure
	decayTime = log2(-60.dbamp) / log2(delayfeedback) * delaytime;
	decayTime = decayTime.clip(0, 20);

	// ピンポンディレイのロジック部分
	dry = signal;
	right = CombL.ar(dry, maxDelayTime*2, delaytime.lag(0.01)*2, decayTime);
	left = DelayN.ar(dry + right, maxDelayTime, delaytime.lag(0.01));
	signal = NumChannels.ar(signal + [left*delayAmp, right*delayAmp], ~dirt.numChannels);
	
	signal = signal * EnvGen.kr(Env.asr, gate, doneAction:2);

	DirtPause.ar(signal, graceTime:4);

	Out.ar(effectBus, signal);
}, [\ir, \ir]).add;

// 再生されているシンセを解放する(これによってglobalEffectsの追加・変更が適用される)
Server.default.freeAll;
)
```

あとは、BootTidal.hsに下記を追記して、各パラメータをTidalCyclesでも使えるようにしてください。

```haskell
:{
pingpong = pF "pingpong"
pingpongt = pF "pingpongt"
pingpongfb = pF "pingpongfb"
:}
```

(BootTidal.hsを直接いじるのが好みでない方は、TidalCyclesのプロジェクトディレクトリ直下にBootTidal.hsをコピペしてから編集したり、.tidalファイルから直接実行してみたりしてください。BootTidalについては以下の記事も参考にしてみてください)

https://qiita.com/s2hap/items/10c32ff7cac1ed8a636d



以上でピンポンディレイが定義されたので、試しに.tidalファイルで以下を実行してみましょう。

```haskell
setcps (120/60/4)

d1
  $ ((# pingpong 0.7).(# pingpongt (3/16)).(# pingpongfb 0.7))
  $ slow 2
  $ s "notes:2 [~ ~ ~ [~ notes:2]] notes:2 ~" # up "0 [0 0 0 -4] -2 0"
  # pan 0.5
  # gain 1.0
```

無事最初の動画のような音が再生されたかと思います。