why cant I understand Reverb?

As the title suggests, I’ve been trying to build a reverb. of course, first (and nearly only), I came across Schroeder. I then got to build first and second order allpass filters of sorta unknown quality, as well as a simpleish comb filter delay. Those look like this:

//single ringbuffer allpass(first order) w/ a 'lowest' cutoff of S samples before it loops back on itself in time
template <typename T = float, size_t S = 2048>
struct AllPass1st {
	std::atomic<size_t> ind;
	T Buf[S]; //main  buffer
	T blok[8]; //small block for smoothing when time is changed
	std::atomic<size_t> blkhd;
	T Foffset; //fractional sample offset (delay/frequency)
	T Gcoeff; //gain
	void reset() {
		std::memset(Buf, 0.f, sizeof(T) * S);
		ind = 0;
		blkhd = 0;
	}
	AllPass1st() {
		this->reset();
	}
    //run every once in a while(every 8 samples atm) 
	void setCoeffs(T freq, T gain, float samplerate) {
		this->Gcoeff = gain;
		this->Foffset = FreqToSampleF(freq, samplerate); //just converts Hz to samples
	}
    //run every sample
	inline T process(T in) {
		size_t idx = wraparound((this->ind % S) - (int)this->Foffset, S);
		T del = this->Buf[idx];  
		this->blok[blkhd % 8] = del;
        //lerp 4 output from 2 samples in the past to smooth time changes
		T out = cubicLerp(this->blok, (blkhd - 2) % 8, this->Foffset, 8);
        this->Buf[this->ind % S] = in - (out * this->Gcoeff); //negative feedback to current moment
        out += (in * this->Gcoeff); //feed forward by gain
		
		++this->ind;
		++this->blkhd;
		return out;
	}
	
};

template <typename T = float>
//a sorta kinda biqaud allpass
struct AllPass2nd {
	/*
	wh = current sample out of 3 in buffer
	X[n]  = dry input buffer
	Y[n] = final output
	d = break frequency coefficient
	c = bandwidth coefficient
    a & b = polynomial coefficients
	*/
	int wh = 0;
	T X[3] = { 0, 0, 0 };
	T Y[3] = { 0, 0, 0 };
	T d = 0.2;
	T c = 0.2;
	T a[3] = { 0, 0, 0 };
	T b[3] = { 0, 0, 0 };


	void reset() {
		//std::memset(V, (T)0, sizeof(T) * 3);
		std::memset(this->X, (T)0, sizeof(T) * 3);
		std::memset(this->Y, (T)0, sizeof(T) * 3);
		std::memset(this->a, (T)0, sizeof(T) * 3);
		std::memset(this->b, (T)0, sizeof(T) * 3);
		wh = 0;

	}

	AllPass2nd() {
		this->reset();
	}

	//equations found at TheWolfSound
	//cutoff and band  given in Hz
	void setCoeffs(T cutoff, T band, T samplerate) {
		T tang = tanapp((_PI * band) / samplerate); //tanapp is andrewkay approx of tan
		this->c = (tang - 1.f) / (tang + 1.f);
		this->d = -cos((_PI * cutoff) / samplerate); //should use another approx here
		this->a[0] = -this->c;
		this->a[1] = this->d * (1.f - this->c);
		this->a[2] = 1.f;
		this->b[0] = 0;
		this->b[1] = this->a[1];
		this->b[2] = this->a[0];
	}
	//wraparound treats the array as a circular buffer, fixing negative number issues of simple %
    //i should maybe instead structure this like vcvs biquad process
	inline T process(T in) {
		this->X[wh] = in;
		this->Y[wh] = (this->a[0] * this->X[this->wh]) + (this->a[1] * this->X[wraparound(this->wh - 1, 3)]) + (this->a[2] * this->X[wraparound(this->wh - 2, 3)])
			- (this->b[1] * this->Y[wraparound(this->wh - 1, 3)]) - (this->b[2] * this->Y[wraparound(this->wh - 2, 3)]);
		auto hh = this->wh;
		++this->wh;
		this->wh %= 3;
		return this->Y[hh];
	}
	
};
//this is technically like a doubleringbuffer in a way
template <typename T = float, size_t S = 2048>
struct DelayComb {
	std::atomic<size_t> wh;

	T Buf[S * 2];
	T blok[8];
	std::atomic<size_t> blkhd;
	void reset() {
		std::memset(this->Buf, (T)0, sizeof(T) * S * 2);
		this->wh = 0;
		this->blkhd = 0;
		std::memset(this->blok, (T)0, sizeof(T) * 8);
	}
	DelayComb() {
		this->reset();
	}

	inline T process(T in, T offset, T FB) {
		size_t idx = wraparound((this->wh % S) - (int)offset, S);
		this->blok[this->blkhd % 8] = this->Buf[idx]; // do that same cubic lerping thing
		T out = cubicLerp(this->blok, (this->blkhd - 2) % 8, offset, 8);
		this->Buf[this->wh % S] = in - (out * FB); //negative feedback
		++this->wh;
		++this->blkhd;
		return out;
	}
};

I then route those like the pictures say, serial allpasses followed by parallel combs, with the inclusion of a mixing matrix shown in a picture in that video about designing the erbe verb (the first thing i tried was building that picture directly but it did NOT work well) inside the feedback loop for the comb delays. that ends up like this:

template <typename T = float, size_t S = 44100, size_t D = 2048>
struct SchroederLane {

	Filter::AllPass1st<T, D> AP1[2];
	Filter::AllPass2nd<T> AP2[2];
	Filter::DelayComb<T, S> Combs[4];
	T combFreq[4]; //expects samples
	T baseAP[4] = { 1137, 1627, 2347, 3113 }; //in Hz, these have been init as many things, not the issue here
	T baseComb[4] = { 687, 801, 1053, 1251 }; //in samples, same as above
	T sprdmlt[4] = { 0.23, 0.47, 0.73, 0.97 }; //multiply global changes to combfreqs by these to spread from each other
	T diffFeeds[4]; //feedback group to be matrix mixed
	T globalDecay;
	void clear() {
		for (int c = 0; c < 2; ++c) {
			AP1[c].reset();
			AP2[c].reset();
		}
		for (int c = 0; c < 4; ++c) {
			Combs[c].reset();
		}
	}
	SchroederLane() {
		this->clear();
	}
//run when you update parameter values
//material and cohesion just change allpass parameters atm, but i hope to involve filters later
	void setQualities(T room, T decay, T material, T cohesion, float samplerate) {
		globalDecay = decay;
		T matFreq = VoltToFreq(material, 0.f, 128.f);
		T precut = matFreq / samplerate;		
		for (int q = 0; q < 2; ++q) {
			T ap1Freq = baseAP[q] - (cohesion * sprdmlt[q]);
			AP1[q].setCoeffs(ap1Freq, globalDecay * 0.2f + 0.1f, samplerate);

			AP2[q].setCoeffs(baseAP[q + 2] + matFreq, cohesion, samplerate);
		}
		for (int c = 0; c < 4; ++c) {
			combFreq[c] = baseComb[c] + (MeterToSample(room * 0.2f, samplerate) * sprdmlt[c]);
		}
	}
	//dry input here, if you give this part feedback it may go crazy
	T Smear(T in) {
		//serial process allpasses
		T ap1 = AP1[0].process(in);
		T ap2 = AP2[0].process(ap1);
		T ap3 = AP1[1].process(ap2);
		T ap4 = AP2[1].process(ap3);
		T sme = ap4;
		return sme;
	}
	
	//give it the output from smear
	T Diffuse(T in) {

		//combs themselves get almost no feedback, as that is provided via the shuffling matrix
		T combs[4] = { 0, 0, 0, 0 };
		for (int p = 0; p < 4; ++p) {
			combs[p] = Combs[p].process((in + diffFeeds[p]), combFreq[p], (T)0.01);
		}
		//shuffling also outputs a mixdown, how convenient
		T diffMix = (ShuffleMix4Lane(combs, globalDecay, diffFeeds));
		return diffMix;
	}
};

//the mixing matrix looks like this
template <typename T = float>
T ShuffleMix4Lane(T* lanes, T FB, T* out = nullptr) {
	//rescale decay/feedback, 0-0.3 doesnt let enough thru, over 0.5 is too much
	T dec = lerp(0.35f, 0.5f, 0.f, 1.f, FB);
	T p1 = lanes[0] - lanes[1];
	T p2 = lanes[0] + lanes[1];
	T p3 = lanes[2] - lanes[3];
	T p4 = lanes[2] + lanes[3];
	T n1 = (p1 - p3) * dec;
	T n2 = (p1 + p3) * dec;
	T n3 = (p2 - p4) * dec;
	T n4 = (p2 + p4) * dec;
	T mix = ((n1 + n2 + n3 + n4) * 0.5);
	if (out) {
		out[0] = n1;
		out[1] = n3;
		out[2] = n2;
		out[3] = n4;
	}
	return mix;

}

these are objectively simple building blocks and a straightforward implementation, but just one of these reverberators is costing 3% CPU (for comparison all of Plateau runs at about 4%). There seems to be very little information that i can find that goes in depth on the guts of a reverb algorithm, and the plugins here whos code i can actually look at are written at a level that i simply cannot yet understand, and honestly dont sound much better than what this already puts out.

If you google the “Dattorro 1997” algorithm mentioned in the Plateau description you will find a block diagram of that algorithm. It is based on a plate reverb (hence the name Plateau I guess) algorithm by David Griesinger (Lexicon).

Now that is not the only way you can combine allpasses, filters and delay loops so the block diagram can serve as a starting point for your own experiments in combining these elements.

Plateau (as well as Lexicon) also use modulation of the delay lines to avoid “standing waves” that can lead to emphasized frequencies.

1 Like

Maybe get some insights from the master: Valhalla DSP

3 Likes

Great links!

Btw, the link to the “Dattoro 1997” paper mentioned by Plateau is at the end of the “Best papers” article.

This is by far the best resource I have found for creating a reverb algorithm from scratch:

Thank you! I think these (starting with dattoro of course ) may indeed get me there.

If I may ask as well, looking at the construction of my filters, are they particularly inefficient in some obvious way I’m not seeing due to inexperience? Or do they seem at least reasonable?