FFT-based crossfading convolution

In a recent project, I wanted to do convolution against a time-varying signal and be able to crossfade the amount of convolution. Convolution.ar is all-on, all the time, so I decided to do it with PV_Mul instead. The trick was to prepare a buffer with the frequency-domain equivalent of 1.0, so that when the convolution is “faded out,” the spectrum gets multiplied by this identity operand.

Sharing bc it shows something about using FFT buffers and FFTTrigger.

(
s.waitForBoot {
	var fftSize = 2048;  // makes a big difference to the end result!
	var cond = CondVar.new;
	// sendCollection + wait needs to fork
	fork {
		~unity = Buffer.sendCollection(s,
			[1, 1] ++ Array.fill(fftSize div: 2 - 1, [1, 0]).flat,
			action: { cond.signalAll }
		);
	};
	cond.wait;

	~srcBus = Bus.audio(s, 2);

	Pdef(\src, Pbind(
		\degree, Pwhite(-7, 14, inf),
		\dur, Pexprand(0.1, 0.8, inf),
		\legato, Pexprand(0.6, 4.0, inf),
		\amp, Pexprand(0.06, 0.18, inf),
		\amp, 1,
		\out, ~srcBus
	)).play;

	~fx = { |inbus, unity|
		var hop = 0.5;
		var sig = In.ar(inbus, 2);
		var fft = FFT(Array.fill(2, { LocalBuf(fftSize) }), sig, hop);
		// replace 'kernel' with any other signal
		// I'm just using a noisy FM pair here
		var kernel = SinOsc.ar(0,
			phase: (Phasor.ar(0, LFDNoise3.kr(0.8).exprange(10, 200) * SampleDur.ir, 0, 1) * 2pi
				+ (45 * SinOsc.ar(LFDNoise3.kr(0.8).exprange(30, 1000)))
			) % 2pi
		) * 0.2;  // empirically this needs to be scaled down
		var kFFT = FFT(LocalBuf(fftSize), kernel, hop);
		var unityFFT = FFTTrigger(unity, hop, polar: 0);
		// note, do not put unityFFT first, or it will get overwritten
		// xfade is reversed for this reason
		var xfadeKernel = PV_XFade(kFFT, unityFFT, MouseX.kr(1, 0));
		var convo = PV_Mul(fft, xfadeKernel);
		IFFT(convo) * 0.1
	}.play(s.defaultGroup, addAction: \addToTail, args: [
		unity: ~unity, inbus: ~srcBus
	]);

	~fx.onFree { Pdef(\src).stop; ~unity.free; ~srcBus.free };
};
)

hjh

4 Likes