Serum Reverb Filter in SC

hey, does someone know the reverb filter in serum?
Ive read somewhere that its basically a feedback delay network and researched feedback delay networks and found some code via SuperCollider Code
and this video https://www.youtube.com/watch?v=6ATQyEPUF7Q&t=363s
But its more operating like a resonator than a reverb when modulating the Cutoff Frequency at a low cutoff range and creating really great metallic sounds.

here you can hear what i mean:

This example from sccode https://sccode.org/1-5bL:

// Construct a circulant feedback matrix using the given eigenvalues
(
// 1 - The eigenvalues of the feedback coefficient matrix
d = [
	-1,
	Polar(1, -3pi / 4),
	Polar(1, -pi / 2),
	Polar(1, -pi / 6),
	1,
	Polar(1, pi / 6),
	Polar(1, pi / 2),
	Polar(1, 3pi / 4)
];

// 2 - Compute the feedback matrix from the given eigenvalues
n = d.size;
a = (Matrix.newIDFT(n) * Matrix.withFlatArray(n, 1, d)).real.flat / sqrt(n);
)

(
var primePowerDelays = {
	arg delays;
	(delays.collect { |delay, i|
		var prime = i.nthPrime;

		prime ** ((log(delay) / log(prime)) + 0.5).floor;
	}).asInteger / s.sampleRate;
};

var delayLengths = {
	arg n, dmin, dmax;
	var nm1 = n - 1;
	var d = dmin * ((dmax / dmin) ** ((0..nm1) / nm1));

	(d * s.sampleRate).round(1.0).asInteger;
};

SynthDef(\sine, {
	var freq = \freq.kr(440);
	var trigFreq = \trigFreq.kr(1);
	var sig, gainEnv, trig;
	trig = Impulse.ar(trigFreq);
	gainEnv = Decay.ar(trig, \decay.kr(0.2));
	sig = SinOsc.ar(freq);
	sig = sig * gainEnv * \amp.kr(0.5);
	Out.ar(\out.kr(0), sig)
}).add;

SynthDef(\fdn, {

	var a, fb, delayTime;
	var sig, inSig;

	fb = LocalIn.ar(n);

	a = \a.kr(0 ! n);
	delayTime = \delayTime.kr(primePowerDelays.(delayLengths.(n, 0.03, 0.06)));

	inSig = In.ar(\in.kr(0));

	sig = a.size.collect { |i|
		DelayN.ar(a.rotate(i).inject(inSig, { |input, coef|
			coef * fb[i] + input
		}), 1, (delayTime[i] * \scale.kr(1.0) - ControlDur.ir).fold(0.0, 1.0))
	};

	sig = OnePole.ar(sig, \coef.kr(0.5));
	sig = LeakDC.ar(sig);

	LocalOut.ar(sig);

	sig = sig.sum;
	sig = sig.tanh;

	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.75));
	ReplaceOut.ar(\out.kr(0), sig)
}).add;
)

(
s.makeBundle(nil, {
        x = Synth(\sine, [\out, 0, \trigFreq, 0.25, \amp, 0.1], 1, \addToHead);
        y = Synth(\fdn, [\in, 0, \out, 0, \a, a, \scale, 0.01, \coef, 0.5], 1, \addToTail);
});
)

isnt too bad with low settings for scale and coef for creating a more resonator like thing. but now sure about that.

Any ideas on how to implement the reverb filter from serum in SC? thanks :slight_smile:

@nathan already helped me out with this 4th-order FDN. thanks a lot :slight_smile:

(
{
	var snd, size, dampingFreq, feedback, dry;
	// Input signal: saw sweep from 100 to 1000 Hz
	snd = Saw.ar(XLine.kr(100, 1000, 0.1)) * Env.perc(0.001, 0.2).ar;
	dry = snd;
	snd = Mix(snd); // if input signal is stereo then make it mono

	size = 0.05;
	feedback = 0.9;
	dampingFreq = 16e3;

	snd = snd + LocalIn.ar(4);
	snd = DelayC.ar(snd, 0.5, [0.2, 0.34, 0.36, 0.38] * size - ControlDur.ir);
	snd = snd * feedback;
	snd = LPF.ar(snd, dampingFreq);
	snd = snd * [
		[1, 1, 1, 1],
		[1, -1, 1, -1],
		[1, 1, -1, -1],
		[1, -1, -1, 1],
	].flop / 2;
	snd = snd.sum;
	LocalOut.ar(snd);
	dry + Splay.ar(snd);
}.play(fadeTime: 0);
)

A note on efficiency: The above line would produce 6 multiplication BinaryOpUGens (* 1 is eliminated) and 16 division BinaryOpUGens – because ops are left-to-right, so it first expands the four channels of snd to 16 channels, then applies /2 to each channel separately.

Multiplication is faster than division so one might write * 0.5 instead of / 2.

a * b * c = a * (b * c) so it could also be snd * (array.flop * 0.5) and in this case, the second term consists entirely of constants, so that will be calculated in advance in the language, leaving 16 * units (losing the * 1 degenerate case optimization).

But we can go further:

	snd = snd * [
		[1, 1, 1, 1],
		[1, -1, 1, -1],
		[1, 1, -1, -1],
		[1, -1, -1, 1],
	].flop;
	snd = snd.sum * 0.5;

Written this way, you get the same result with 10 * units only (six * -1 and four * 0.5), rather than the 22 math ops initially. (The number of + units wouldn’t change.)

hjh

2 Likes

thank you very much.
in the end i would probably put the matrix outside of the SynthDef like this.
(still playing around with different matrices, delTimes and modalDensity here):

EDIT: the cutoff of the reverb filter in serum equals the reverb size here.

(
var hadamard_4x4, hadamard_8x8, householder, puckette, svd;

hadamard_4x4 = [
	[ 1,  1,  1,  1 ],
	[ 1, -1,  1, -1 ],
	[ 1,  1, -1, -1 ],
	[ 1, -1, -1,  1 ],
] * ( sqrt(2).reciprocal * sqrt(2).reciprocal );

hadamard_8x8 = [
	[ 1,  1,  1,  1,  1,  1,  1,  1 ],
	[ 1, -1,  1, -1,  1, -1,  1, -1 ],
	[ 1,  1, -1, -1,  1,  1, -1, -1 ],
	[ 1, -1, -1,  1,  1, -1, -1,  1 ],
	[ 1,  1,  1,  1, -1, -1, -1, -1 ],
	[ 1, -1,  1, -1, -1,  1, -1,  1 ],
	[ 1,  1, -1, -1, -1, -1,  1,  1 ],
	[ 1, -1, -1,  1, -1,  1,  1, -1 ],
] * ( sqrt(2).reciprocal * sqrt(2).reciprocal * sqrt(2).reciprocal );

householder = [
    [-0.5, 0.5, 0.5, 0.5],
    [ 0.5, -0.5, 0.5, 0.5],
    [ 0.5, 0.5, -0.5, 0.5],
    [ 0.5, 0.5, 0.5, -0.5],
];

puckette = [
	[0, 1, 1, 0],
	[-1, 0, 0, -1],
	[1, 0, 0, -1],
	[0, 1, -1, 0]
] * sqrt(2).reciprocal;

svd = [
	[-0.29780638, -0.34044128,  0.89169459,  0.01708377],
	[ 0.66830151, -0.59712235,  0.00372119, -0.44362613],
	[-0.6599292,  -0.27730799, -0.31432955, -0.62353081],
	[-0.17081542, -0.67130091, -0.32567445,  0.64351638]
];

SynthDef(\fdn, {
	var snd, size, dampingFreq, feedback, dry, delTimes, delTimesSec;
	var matrix, order;

	snd = Saw.ar(XLine.kr(100, 1000, 0.1)) * Env.perc(0.001, 0.2).ar;
	dry = snd;
	snd = Mix(snd); // if input signal is stereo then make it mono

	//matrix = hadamard_4x4;
	matrix = hadamard_8x8;
	//matrix = householder;
	//matrix = puckette;
	//matrix = svd;

	order = matrix.size;

	size = 0.1;
	feedback = 0.9;
	dampingFreq = 16e3;
	//delTimes = [8820.0, 14994.0, 15876.0, 16758.0]; // delaytimes in samples

	delTimes = [ 5813, 3547, 2797, 3613, 4003, 1657, 4007, 5711 ] * 1.82;

	//delTimes = order.collect({|i| rrand(1000, 4599).nextPrime }) * 2.5;

	( delTimes.sum / 44100 ).debug(\delTimes_sum_);	// should be at least 1 sec

	delTimesSec = delTimes / 44100;

	snd = snd + LocalIn.ar(order);
	snd = DelayC.ar(snd, 0.5, delTimesSec * size - ControlDur.ir);
	snd = snd * feedback;
	snd = LPF.ar(snd, dampingFreq);

	snd = snd * matrix.flop;

	snd = snd.sum;
	LocalOut.ar(snd);
	snd = dry + Splay.ar(snd);
	snd = snd * 0.3;
	snd = SafetyLimiter.ar(snd);
	Out.ar(\out.kr(0), snd);
}).play;
)
2 Likes