# 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 12: Reverberation: Feedback Delay Networks - Musical Sound Design In Supercollider - YouTube
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)

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)
)

(
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 @nathan already helped me out with this 4th-order FDN. thanks a lot (
{
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);
)


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.

(

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

[ 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 = 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