Yes, I’m aware of the nested allpass strategies, I just never used GFIS for that.
FWIW, here are some variants derived from the Schroeder ideas:
//////////////////////////
REVERB WITH ALLPASS-FILTER
//////////////////////////
Interesting comment on the 1962 paper by Manfred SCHROEDER from 1962:
https://valhalladsp.com/2009/05/30/schroeder-reverbs-the-forgotten-algorithm/
1.) Comb and Allpass
// Idea: by chosen params a Comb-Delay can be transformed to an Allpass-Delay:
// frequency response with comb delay -> multiples of 500 Hz 500 Hz
s.freqscope;
(
x = {
var in = WhiteNoise.ar(0.05);
var delayTime = 0.002; // ~500 Hz
var decayTime = 0.1;
var comb = CombL.ar(in, 0.2, delayTime, decayTime);
comb ! 2;
}.play
)
x.release
// flat frequency response ! (= Allpassfilter)
(
x = {
var in = WhiteNoise.ar(0.05);
var delayTime = 0.002; // ~500 Hz
var decayTime = 0.1;
var comb = CombL.ar(in, 0.2, delayTime, decayTime);
// calculate feedback gain
var fbGain = 0.001 ** (delayTime / decayTime).poll(0, fbGain);
// mix with correctl chosen params -> flat frequency response
comb * (1 - (fbGain ** 2)) - (in * fbGain) ! 2;
}.play
)
x.release
// compare with AllpassL -> identical (silence) !
(
x = {
var in = WhiteNoise.ar(0.05);
var delayTime = 0.002; // ~500 Hz
var decayTime = 0.1;
var comb = CombL.ar(in, 0.2, delayTime, decayTime);
// calculate feedback gain
var fbGain = 0.001 ** (delayTime / decayTime).poll(0, fbGain);
// difference = 0 !
comb * (1 - (fbGain ** 2)) - (in * fbGain) - AllpassL.ar(in, 0.2, delayTime, decayTime);
}.play
)
x.release
// allpass + source -> audible frequency (1/delaytime)
(
x = {
var in = WhiteNoise.ar(0.05);
var delayTime = 0.002; // ~500 Hz
var decayTime = 0.1;
var comb = CombL.ar(in, 0.2, delayTime, decayTime);
// calculate feedback gain
var fbGain = 0.001 ** (delayTime / decayTime).poll(0, fbGain);
// allpass + source
comb * (1 - (fbGain ** 2)) - (in * fbGain) + in ! 2;
// identical
// AllpassL.ar(in, 0.2, delayTime, decayTime) + in ! 2;
}.play
)
x.release
/////////////////////////////////////////////
2.) Schroeder Reverb I: serial allpass (P.221, 222)
(
s.options.blockSize = 1;
s.reboot;
)
(
// number of allpas filters in sequence
~num = 5;
~fxBus = Bus.audio(s, 2);
// force nicely distributed random numbers for deviation of allpassDelayFactor
// this seed worked ok for me, try others
~minCombDelayTime = 30;
thisThread.randSeed = 121;
~allpassDelayFactorDeviationMax = 0.1;
~allpassDelayFactorDeviations = { rand2(~allpassDelayFactorDeviationMax) } ! ~num + 1;
"~allpassDelayFactorDeviations: ".postln;
~allpassDelayFactorDeviations.do(_.postln);
SynthDef(\schroeder_I, { |outBus, amp = 1, initDelay = 30, cutoff = 7000, overallGain = 0.89|
var out, in = In.ar(~fxBus, 2);
var sig = in;
var maxDelay = 0.2;
// Schroeder suggestions
var allpassGain = 0.7;
var firstAllpassDelay = 30;
var allpassDelayFactor = 1/3;
var allpassDelay = firstAllpassDelay * 0.001;
var fb = LocalIn.ar(2);
sig = DelayL.ar(fb + sig, 0.2, initDelay * 0.001 - ControlDur.ir);
{ |i|
var decay;
(i != 0).if { allpassDelay = allpassDelay * allpassDelayFactor * ~allpassDelayFactorDeviations[i] };
decay = allpassDelay * log(0.001) / log(allpassGain);
sig = AllpassL.ar(sig, maxDelay, allpassDelay, decay);
} ! ~num;
// variant: damping of high frequencies
LocalOut.ar(overallGain * BHiShelf.ar(sig, cutoff, 1, -18));
out = (1 - (overallGain ** 2)) * sig - (in * overallGain);
Out.ar(outBus, out * amp)
}, metadata: (
specs: (
amp: [0, 1, \db, 0, 0.5],
initDelay: [2, 1000, 3, 0, 30], // in ms
overallGain: [0, 1, \lin, 0, 0.89],
cutoff: [200, 16000, \exp, 0, 7000]
)
)
).add;
SynthDef(\sawPerc, { |out, freq = 400, att = 0.01, rel = 0.1, amp = 0.1|
var env = EnvGen.ar(Env.perc(att, rel), doneAction: 2);
Out.ar(out, Saw.ar(freq * [1, 1.02], amp) * env)
}).add;
)
// needs miSCellaneous_lib
\schroeder_I.sVarGui.gui(sliderWidth: 320, labelWidth: 120)
// alternative:
SynthDescLib.global[\schroeder_I].makeGui
(
x = Pbind(
\instrument, \sawPerc,
\dur, 0.2,
\note, Prand([0, 2, 4, 7, 9], inf),
\octave, 4,
\amp, 0.3,
\out, ~fxBus
).play
)
x.stop
// stop reverb in GUI
/////////////////////////////////////////////
2.) Schroeder Reverb II: 4 parallele Combs + 2 Allpass in Serie (S. 223)
(
~combNum = 4;
~allpassNum = 2; // if you change this, you must adapt allpassDelayTimes and allpassGains
~fxBus = Bus.audio(s, 2);
// force nicely distributed random numbers for comb delaytimes
// this seed worked ok for me, try others
thisThread.randSeed = 121;
~combDelayTimeSpread = 1.5;
~combDelayTimeFactors = { |i|
((~combDelayTimeSpread - 1 * i) + rand(~combDelayTimeSpread - 1)) / ~combNum + 1
} ! ~combNum;
"minCombDelayTime: ".post;
~minCombDelayTime.post;
" ms".postln;
"combDelayTimeFactors: ".postln;
~combDelayTimeFactors.do(_.postln);
SynthDef(\schroeder_II_vs_1, { |outBus, amp = 0.7, mix = 0.12, minCombDelay = 30, revTime = 1|
var minCombDelayTime = 30;
var combDelayTimes = ~combDelayTimeFactors * minCombDelay * 0.001;
// calculate from desired reverb time according to Schroeder
var combGains = 10 ** (-3 * combDelayTimes / revTime);
// fix allpass delaytime choices by Schroeder
var allpassDelayTimes = [5, 1.7] * 0.001;
var allpassGains = [0.7, 0.7];
var sig, out;
var in = In.ar(~fxBus, 2);
var maxDelay = 0.2;
var combDecayTimes = combDelayTimes * log(0.001) / log(combGains);
var allpassDecayTimes = allpassDelayTimes * log(0.001) / log(allpassGains);
// core:
// ~combNum parallel Combs for L and R ...
sig = { |i| CombL.ar(in[i], maxDelay, combDelayTimes, combDecayTimes).sum } ! 2;
// ... followed by 2 sequential Allpasses
{ |i| sig = AllpassL.ar(sig, maxDelay, allpassDelayTimes[i], allpassDecayTimes[i]) } ! ~allpassNum;
out = mix * sig - ((1 - mix) * in);
Out.ar(outBus, out * amp)
}, metadata: (
specs: (
amp: [0, 1, \db, 0, 0.7],
mix: [0, 1, 3, 0, 0.12],
minCombDelay: [1, 150, \lin, 0, 30],
revTime: [0, 15, 3, 0, 1]
)
)
).add;
SynthDef(\sawPerc, { |out, freq = 400, att = 0.01, rel = 0.1, amp = 0.1|
var env = EnvGen.ar(Env.perc(att, rel), doneAction: 2);
Out.ar(out, Saw.ar(freq * [1, 1.02], amp) * env)
}).add;
)
// needs miSCellaneous_lib
\schroeder_II_vs_1.sVarGui.gui(sliderWidth: 320, labelWidth: 120)
// alternative:
SynthDescLib.global[\schroeder_II_vs_1].makeGui
// minCombDelay should be 30 (recommended by S.)
(
x = Pbind(
\instrument, \sawPerc,
\dur, 0.2,
\note, Prand([0, 2, 4, 7, 9], inf),
\octave, 4,
\amp, 0.3,
\out, ~fxBus
).play
)
x.stop
// stop reverb in GUI
(
~combNum = 4;
~fxBus = Bus.audio(s, 2);
// force nicely distributed random numbers for comb delaytimes
// this seed worked ok for me, try others
~minCombDelayTime = 30;
thisThread.randSeed = 121;
~combDelayTimeSpread = 1.5;
~combDelayTimeFactors = { |i|
((~combDelayTimeSpread - 1 * i) + rand(~combDelayTimeSpread - 1)) / ~combNum + 1
} ! ~combNum;
"minCombDelayTime: ".post;
~minCombDelayTime.post;
" ms".postln;
"combDelayTimeFactors: ".postln;
~combDelayTimeFactors.do(_.postln);
SynthDef(\schroeder_II_vs_2, { |outBus, amp = 0.7, mix = 0.12, revTime = 1, revCorr = 0.5, srcCorr = 0.2, cutoff = 3500|
var minCombDelayTime = 30;
var combDelayTimes = ~combDelayTimeFactors * minCombDelayTime * 0.001;
// calculate from desired reverb time according to Schroeder
var combGains = 10 ** (-3 * combDelayTimes / revTime);
// fix allpass delaytime choices by Schroeder
var allpassDelayTimes = [5, 1.7] * 0.001;
var allpassGains = [0.7, 0.7];
var sig, out;
var in = In.ar(~fxBus, 2);
var maxDelay = 0.2;
var combDecayTimes = combDelayTimes * log(0.001) / log(combGains);
var allpassDecayTimes = allpassDelayTimes * log(0.001) / log(allpassGains);
// core:
// dampened input, 4 parallel Combs for L and R ...
sig = { |i| CombL.ar(BHiShelf.ar(in[i], cutoff, 1, -18), maxDelay, combDelayTimes, combDecayTimes).sum } ! 2;
// ... followed by 2 sequential Allpasses
{ |i| sig = AllpassL.ar(sig, maxDelay, allpassDelayTimes[i], allpassDecayTimes[i]) } ! ~allpassNum;
// correlations for src and reverb
in = [
XFade2.ar(in[0], in[1], srcCorr - 1),
XFade2.ar(in[0], in[1], 1 - srcCorr)
];
// better: SelectX.ar(srcCorr, [in, in.sum ! 2])
sig = [
XFade2.ar(sig[0], sig[1], revCorr - 1),
XFade2.ar(sig[0], sig[1], 1 - revCorr)
];
// better: SelectX.ar(revCorr, [sig, sig.sum ! 2])
out = mix * sig - ((1 - mix) * in);
Out.ar(outBus, out * amp)
}, metadata: (
specs: (
amp: [0, 1, \db, 0, 0.7],
mix: [0, 1, 3, 0, 0.12],
revTime: [0, 15, 3, 0, 1],
revCorr: [0, 1, \lin, 0, 0.5],
srcCorr: [0, 1, \lin, 0, 0.2],
cutoff: [200, 16000, \exp, 0, 3500]
)
)
).add;
SynthDef(\sawPerc_2, { |out, freq = 400, att = 0.01, rel = 0.1, pan = 0, amp = 0.1|
var env = EnvGen.ar(Env.perc(att, rel), doneAction: 2);
Out.ar(out, Pan2.ar(Saw.ar(freq, amp), pan) * env)
}).add
)
// needs miSCellaneous_lib
\schroeder_II_vs_2.sVarGui.gui(sliderWidth: 320, labelWidth: 120)
// alternative:
SynthDescLib.global[\schroeder_II_vs_2].makeGui
(
x = Pbind(
\instrument, \sawPerc_2,
\dur, 0.2,
\note, Prand([0, 2, 4, 7, 9], inf) + [0, 16],
\octave, 4,
\pan, [-1, 1],
\amp, 0.3,
\out, ~fxBus
).play
)
x.stop
// stop reverb in GUI