Hey all,
I’ve been working on reverb design for a while now and checking out a lot of the literature, things are starting to go pretty well I think. Recently I was working on a kind of silly idea, using some of the literature on the Hagia Sophia in Istanbul I decided to try to recreate it with an algorithmic reverb in SC (I am aware that this is a terrible idea). I split the signal into 5 bands to fit approximately with the RT60 times detailed in this paper.
The code for this got quite complex and because I’m almost exclusively self-taught I figure I’m probably doing a fair few things either wrong or at least kind of sloppily. The reverb works ok - it’s not the best reverb in the world, but it’s fine as a fun experiment - but I would be very keen for any feedback on the actual coding style/technique (although feedback on the design itself is also very welcome as I still have a lot to learn).
One thing that I was struggling with is that there is a lot of processing which is very similar, but not quite similar enough that I could figure out how to iterate it (eg. I’m using different filters for the different bands).
Code is below, thanks in advance if anyone feels like taking the time.
(
SynthDef(\hs_reverb, {
|modulation = 0.5|
var env = Env.asr(0.001, 1, 10).ar(Done.freeSelf, \gate.kr(1));
var input = In.ar(0, 2);
var reflections = input.mean;
var late;
var early = 0;
var cd = ControlDur.ir;
var sig = Array.newClear(2);
var band = Array.newClear(10);
var tap = Array.newClear(20);
var fb = LocalIn.ar(2);
var earlyDelayTimes = [0.0243, 0.0341, 0.0511, 0.0463, 0.0237, 0.0331, 0.0387, 0.0434];
var earlyAllpassTimes = [0.0021, 0.0034, 0.00241, 0.00257, 0.00347, 0.00561, 0.00325, 0.00433];
var allpassTimes = [0.0251, 0.0254263, 0.0451, 0.0456863, 0.0271, 0.0274523, 0.0443, 0.0448759, 0.0365, 0.0369745, 0.0631, 0.0639203, 0.0378, 0.0382914, 0.0623, 0.0631099, 0.0281, 0.0284653, 0.0334, 0.0338342, 0.0294, 0.0297822, 0.0315, 0.0319095, 0.0295, 0.0298835, 0.0314, 0.0318082, 0.0301, 0.0304913, 0.0308, 0.0312004, 0.0421, 0.0426473, 0.0513, 0.0519669, 0.0447, 0.0452811, 0.0506, 0.0512578];
var allpassDecays = [ 0.9, 12.9, 0.909, 13.029, 0.95, 13.01, 0.9595, 13.1401, 0.9, 11.3, 0.909, 11.413, 0.9, 11.1, 0.909, 11.211, 0.9, 12.1, 0.909, 12.221, 0.85, 12.2, 0.8585, 12.322, 0.95, 10.8, 0.9595, 10.908, 0.89, 11.01, 0.8989, 11.1201, 0.87, 6, 0.8787, 6.06, 0.88, 5.4, 0.8888, 5.454 ]
;
var delayTimes = [ 0.020081942081451, 0.020566612482071, 0.026000893115997, 0.020242093801498, 0.029465512037277, 0.027917033433914, 0.026404478549957, 0.022379480600357, 0.025247086286545, 0.027848986387253, 0.029049491882324, 0.023493590354919, 0.020490299463272, 0.028556725978851, 0.021322796344757, 0.020029554367065, 0.025117723941803, 0.021801003217697, 0.028470935821533, 0.021489890813828 ]
;
// var mod = 20.collect { SinOsc.kr(rrand(0.20, 0.33), phase: rrand(0, 2pi)) * rrand(0.001, 0.0015) };
var mod = 20.collect { LFNoise2.kr(rrand(0.20, 0.33)) * modulation };
early = 4.collect { |i| AllpassC.ar(OnePole.ar(reflections, 0.9), 1, earlyAllpassTimes[i], 1); };
late = [early.sum + fb[0], early.sum + fb[1]];
// bands 0/1, very low frequencies, then split into two channels
band[0] = LPF.ar(LPF.ar(late[1], 75), 75);
band[0] = AllpassC.ar(band[0], allpassTimes[0], allpassTimes[0], allpassDecays[0]);
band[0] = AllpassC.ar(band[0], allpassTimes[1], allpassTimes[1], allpassDecays[1]);
tap[0] = DelayC.ar(band[0], delayTimes[0] + 1, delayTimes[0] + mod[0] - cd);
// same again for right channel
band[1] = LPF.ar(LPF.ar(late[0], 75), 75);
band[1] = AllpassC.ar(band[1], allpassTimes[2], allpassTimes[2], allpassDecays[2]);
band[1] = AllpassC.ar(band[1], allpassTimes[3], allpassTimes[3], allpassDecays[3]);
tap[1] = DelayC.ar(band[1], delayTimes[1] + 1, delayTimes[1] + mod[1] - cd);
// bands 2/3, 80-180hz
band[2] = HPF.ar(HPF.ar(late[1], 80), 80);
band[2] = LPF.ar(LPF.ar(band[2], 180), 180);
band[2] = AllpassC.ar(band[2], allpassTimes[4], allpassTimes[4], allpassDecays[4]);
band[2] = AllpassC.ar(band[2], allpassTimes[5], allpassTimes[5], allpassDecays[5]);
band[2] = BPeakEQ.ar(band[2], 150, 1, -2);
tap[2] = DelayC.ar(band[2], delayTimes[2] + 1, delayTimes[2] + mod[2] - cd);
// same for right channel
band[3] = HPF.ar(HPF.ar(late[0], 80), 80);
band[3] = LPF.ar(LPF.ar(band[3], 180), 180);
band[3] = AllpassC.ar(band[3], allpassTimes[6], allpassTimes[6], allpassDecays[6]);
band[3] = AllpassC.ar(band[3], allpassTimes[7], allpassTimes[7], allpassDecays[7]);
band[3] = BPeakEQ.ar(band[2], 150, 1, -2);
tap[3] = DelayC.ar(band[3], delayTimes[3] + 1, delayTimes[3] + mod[3] - cd);
// bands 4/5 - 180 - 700hz
band[4] = HPF.ar(HPF.ar(late[1], 180), 180);
band[4] = LPF.ar(LPF.ar(band[4], 700), 700);
band[4] = AllpassC.ar(band[4], allpassTimes[8], allpassTimes[8], allpassDecays[8]);
band[4] = AllpassC.ar(band[4], allpassTimes[9], allpassTimes[9], allpassDecays[9]);
band[4] = BPeakEQ.ar(band[4], 600, 1, -2);
tap[4] = DelayC.ar(band[4], delayTimes[4] + 1, delayTimes[4] + mod[4] - cd);
// same for right channel
band[5] = HPF.ar(HPF.ar(late[0], 180), 180);
band[5] = LPF.ar(LPF.ar(band[5], 700), 700);
band[5] = BPeakEQ.ar(band[5], 600, 1, -2);
band[5] = AllpassC.ar(band[5], allpassTimes[10], allpassTimes[10], allpassDecays[10]);
band[5] = AllpassC.ar(band[5], allpassTimes[11], allpassTimes[11], allpassDecays[11]);
tap[5] = DelayC.ar(band[5], delayTimes[5] + 1, delayTimes[5] + mod[5] - cd);
// bands 6/7 - 700 - 2.5k
band[6] = HPF.ar(HPF.ar(late[1], 700), 700);
band[6] = LPF.ar(LPF.ar(band[6], 2500), 2500);
band[6] = AllpassC.ar(band[6], allpassTimes[12], allpassTimes[12], allpassDecays[12]);
band[6] = AllpassC.ar(band[6], allpassTimes[13], allpassTimes[13], allpassDecays[13]);
band[6] = BPeakEQ.ar(band[6], 1000, 1, -3);
tap[6] = DelayC.ar(band[6], delayTimes[6] + 1, delayTimes[6] + mod[6] - cd);
// same for right channel
band[7] = HPF.ar(HPF.ar(late[0], 700), 700);
band[7] = LPF.ar(LPF.ar(band[7], 2500), 2500);
band[7] = AllpassC.ar(band[7], allpassTimes[14], allpassTimes[14], allpassDecays[14]);
band[7] = AllpassC.ar(band[7], allpassTimes[15], allpassTimes[15], allpassDecays[15]);
band[7] = BPeakEQ.ar(band[7], 1000, 1, -3);
tap[7] = DelayC.ar(band[7], delayTimes[7] + 1, delayTimes[7] + mod[7] - cd);
// bands 8/9 - everything above 2.5k, with bit of extra filtering built in- tweak the hicut later;
band[8] = HPF.ar(HPF.ar(late[1], 2500), 2500);
band[8] = BPeakEQ.ar(band[8], 13000, 1, -3);
band[8] = LPF.ar(band[8], 16000);
band[8] = AllpassC.ar(band[8], allpassTimes[16], allpassTimes[16], allpassDecays[16]);
band[8] = AllpassC.ar(band[8], allpassTimes[17], allpassTimes[17], allpassDecays[17]);
tap[8] = DelayC.ar(band[8], delayTimes[8] + 1, delayTimes[8] + mod[8] - cd);
// same for right channel
band[9] = HPF.ar(HPF.ar(late[0], 2500), 2500);
band[9] = BPeakEQ.ar(band[9], 13000, 1, -3);
band[9] = LPF.ar(band[9], 16000);
band[9] = AllpassC.ar(band[9], allpassTimes[18], allpassTimes[18], allpassDecays[18]);
band[9] = AllpassC.ar(band[9], allpassTimes[19], allpassTimes[19], allpassDecays[19]);
tap[9] = DelayC.ar(band[9], delayTimes[9] + 1, delayTimes[9] + mod[9] - cd);
// second round of processing
band[0] = AllpassC.ar(tap[0], allpassTimes[20], allpassTimes[20], allpassDecays[20]);
band[0] = AllpassC.ar(band[0], allpassTimes[21], allpassTimes[21], allpassDecays[21]);
tap[0] = DelayC.ar(band[0], delayTimes[10] + 1, delayTimes[10] + mod[10] - cd);
band[1] = AllpassC.ar(tap[1], allpassTimes[22], allpassTimes[22], allpassDecays[22]);
band[1] = AllpassC.ar(band[1], allpassTimes[23], allpassTimes[23], allpassDecays[23]);
tap[1] = DelayC.ar(band[1], delayTimes[11] + 1, delayTimes[11] + mod[11] - cd);
band[2] = AllpassC.ar(tap[2], allpassTimes[24], allpassTimes[24], allpassDecays[24]);
band[2] = AllpassC.ar(band[2], allpassTimes[25], allpassTimes[25], allpassDecays[25]);
band[2] = BPeakEQ.ar(band[2], 200, 1, -2);
tap[2] = DelayC.ar(band[2], delayTimes[12] + 1, delayTimes[12] + mod[12] - cd);
band[3] = AllpassC.ar(tap[3], allpassTimes[26], allpassTimes[26], allpassDecays[26]);
band[3] = AllpassC.ar(band[3], allpassTimes[27], allpassTimes[27], allpassDecays[27]);
band[3] = BPeakEQ.ar(band[3], 200, 1, -2);
tap[3] = DelayC.ar(band[3], delayTimes[13] + 1, delayTimes[13] + mod[13] - cd);
band[4] = AllpassC.ar(tap[4], allpassTimes[28], allpassTimes[28], allpassDecays[28]);
band[4] = AllpassC.ar(band[4], allpassTimes[29], allpassTimes[29], allpassDecays[29]);
band[4] = BPeakEQ.ar(band[4], 500, 1, -2);
tap[4] = DelayC.ar(band[4], delayTimes[14] + 1, delayTimes[14] + mod[14] - cd);
band[5] = AllpassC.ar(tap[5], allpassTimes[30], allpassTimes[30], allpassDecays[30]);
band[5] = AllpassC.ar(band[5], allpassTimes[31], allpassTimes[31], allpassDecays[31]);
band[5] = BPeakEQ.ar(band[5], 500, 1, -2);
tap[5] = DelayC.ar(band[5], delayTimes[15] + 1, delayTimes[15] + mod[15] - cd);
band[6] = AllpassC.ar(tap[6], allpassTimes[32], allpassTimes[32], allpassDecays[32]);
band[6] = AllpassC.ar(band[6], allpassTimes[33], allpassTimes[33], allpassDecays[33]);
band[6] = BPeakEQ.ar(band[6], 1100, 1, -3);
tap[6] = DelayC.ar(band[6], delayTimes[16] + 1, delayTimes[16] + mod[16] - cd);
band[7] = AllpassC.ar(tap[7], allpassTimes[34], allpassTimes[34], allpassDecays[34]);
band[7] = AllpassC.ar(band[7], allpassTimes[35], allpassTimes[35], allpassDecays[35]);
band[7] = BPeakEQ.ar(band[7], 1100, 1, -3);
tap[7] = DelayC.ar(band[7], delayTimes[17] + 1, delayTimes[17] + mod[17] - cd);
band[8] = AllpassC.ar(tap[8], allpassTimes[36], allpassTimes[36], allpassDecays[36]);
band[8] = AllpassC.ar(band[8], allpassTimes[37], allpassTimes[37], allpassDecays[37]);
band[8] = LPF.ar(band[8], 9000);
tap[8] = DelayC.ar(band[8], delayTimes[18] + 1, delayTimes[18] + mod[18] - cd);
band[9] = AllpassC.ar(tap[9], allpassTimes[38], allpassTimes[38], allpassDecays[38]);
band[9] = AllpassC.ar(band[9], allpassTimes[39], allpassTimes[39], allpassDecays[39]);
band[9] = LPF.ar(band[9], 9000);
tap[9] = DelayC.ar(band[9], delayTimes[19] + 1, delayTimes[19] + mod[19] - cd);
// not sure if it's better to have 10 localout channels here or to do it this way to include a bit of crossover between the channels - does this even work?
// currently only sending the second round of taps to fb to avoid too much actual feedback
fb = [
[tap[0].neg + tap[2] + tap[4].neg + tap[6] + tap[8].neg],
[tap[1] + tap[3].neg + tap[5] + tap[7].neg + tap[9]],
] * \feedback.kr(-3.dbamp);
fb = LPF.ar(fb, 6000);
fb = LeakDC.ar(fb);
LocalOut.ar(fb);
early = Splay.ar(early, 0.75);
sig = [
[
tap[0],
tap[2] * -1,
tap[4],
tap[6] * -1,
tap[8],
].sum,
[
tap[1] * -1,
tap[3],
tap[5] * -1,
tap[7],
tap[9] * -1,
].sum
];
sig = LeakDC.ar(sig);
sig = LPF.ar(sig, 9000);
sig = sig + early;
sig = input + (sig * \wet.kr(-10.dbamp));
sig = sig * env;
ReplaceOut.ar(0, sig);
}).add;
SynthDef(\test, {
var sig = Saw.ar(XLine.kr(ExpRand(1000, 4000), 50, 1)) * Env.perc(0.01, 1).ar(Done.freeSelf);
sig = LPF.ar(sig, 8000);
sig = sig ! 2 * \amp.kr(-20.dbamp);
Out.ar(0, sig);
}).add;
)
Cheers,
Jordan