Let's Write A Reverb

Geraint Luff
@Signalsmith
Signalsmith Audio

No magic numbers, no tricky tuning: a clean and flexible approach to designing a smooth high-quality reverb, using a variation on the classic feedback-delay network (FDN) structure.

Reverbs are one of my favourite effects, both to use and to write, and feedback-delay networks are a great way to play around and try things.

Reverbs sometimes have a bad reputation for being tricky to tune, so this article goes through one possible reverb design which I hope is simple and intuitive to understand, and I also think is friendly and robust (as in, easy to configure correctly and get a good sound).

So let's get to it!

What do we want from a reverb?

We'll start by laying out what we're looking for. Artificial reverberation is quite a wide field, covering a few different use-cases, but let's take a fairly simple definition:

An effect which creates a sense of space, by generating a diffuse longer-lasting sound from a short input

Approaches

There are many ways to achieve this, but two common approaches are convolution and feedback delay networks. There are advantages and disadvantages to both:

Convolution

❌ complicated
❌ high CPU
❌ hard to modulate
✅ very customisable
✅ can imitate real spaces

Feedback delay network (FDN)

❌ tricky to make realistic
❌ easy to get wrong
✅ very efficient
✅ easy to get right 😉

We're going to take the FDN approach, because you can make a decent-sounding reverb with low CPU and a fairly simple design.

The feedback-delay loop

Here's a simple feedback loop:

The input signal is sent through a delay, which then goes to the output - but it's also fed back into the delay input. This means the signal travels round in a loop, producing a series of echoes from a single input pulse.

There are a few variations on this, like where in the loop we take the output from, or where the gain is placed. The essential part is the feedback which produces an infinitely-looping output, with a gain to make it decay over time.

The echoes repeat at regular intervals, producing a predictable pattern.

A multi-channel feedback loop

Now let's look at a multi-channel version of the same loop:

Rather than draw out a separate feedback loop for each channel (which makes for cluttered diagrams), I find it neater to consider them as multi-channel signals/blocks. I've used double-borders to indicate multi-channel processing.

An important property here is that each channel gets delayed by a different amount, so they each produce a different echo pattern. If we mix all the output channels together, the result still has repeating patterns, but it's more complex than the single-channel version:

Adding a mixing matrix

That more complex sound is good - we don't want identifiable repeating patterns in our reverb. But in the loop above, each channel only repeats its own echoes. Could we get an even more complex result if they picked up each other's echoes as well?

We can achieve this by putting a mixing matrix inside the feedback loop. This is an N \times N orthogonal matrix, where N is the number of channels.

Matrices, and orthogonal matrices

If you aren't familiar with matrices, for this article, we can just say: a matrix is a function which takes in multiple inputs, and adds those together in different ways to produce multiple outputs:

A simple example would be a mid-side encoder, which takes two input channels, and returns them in a linear combination:

For a mixing matrix, we treat each simultaneous set of input samples (from the N channels) as the inputs to the function, and use that to generate one sample for each output channel.

Orthogonal Matrix

An orthogonal matrix is a matrix where the total energy of the output always equals the total energy of the input.

You don't need to know how to construct these, we're going to use some off-the-shelf orthogonal mixing matrices. However, this property where the output energy matches the input energy is going to be really useful later.

Because of this cross-mixing, as the signal travels around the loop, the echoes appear more and more often, giving an increasingly chaotic result:

The tail end of that is even starting to sound like a reverb, right?

Caution

At this point, it's tempting to mix the channels as much as possible, or put a bunch of other things inside the feedback loop to speed up diffusion.

Emojis aside, various well-known FDN reverb designs (such as rings, or Dattoro's figure-of-8 tank) can be understood and analysed as instances of this pattern.

There are some interesting designs here - and they work, but they often require careful tuning. As well as finding a good compromise for the delay time, using too much inter-channel mixing can lock the delays together so they act like a cohesive unit, which isn't great for longer tails.

Designing a feedback loop to produce a diffuse, long-lasting sound with no strong resonances is hard - but we don't have to do it!

Diffusion

There were two properties we said we wanted from the reverb tail:

  • diffuse
  • long-lasting
#reverbgoals ✌️

Even a basic feedback loop handles the "long-lasting" part nicely. Rather than trying to get the feedback loop to also produce a diffuse sound, we're going to separate the two concerns:

The two requirements are handled by two separate stages.

If the diffuser does its job well, we shouldn't really need to build up echo density in the feedback loop.

Now we just need to design a really good diffuser.

Single-channel allpass filters

We want our diffuser to smudge the sound without adding much of its own timbre. A useful concept for this is an allpass filter.

Allpass filters have a flat frequency response (meaning that whatever frequencies you put in, those frequencies have the same energy in the output), but they can have a varying phase response (meaning some frequencies are delayed more than others, providing the "smudging" that we want).

Schroeder allpass

The Schroeder allpass is an IIR filter which often comes up when discussing reverb diffusion. It's made from a (single-channel) feedback loop which produces a series of echoes, with an extra feed-forward part which gives it a flat frequency response:

A classic Schroeder allpass filter

For a reverb diffuser, the delay-time is typically set to a handlful of milliseconds, so one allpass on its own doesn't provide much diffusion - but you can put a bunch of them in a row. The result is definitely diffuse, but it has a slight metallic edge to it:

10 Schroeder allpasses in series, with delays from 1-15ms, and a feedback gain of 40%

More complex designs

You can make more sophisticated allpasses by replacing the inner delay by another allpass, or using second-order allpasses (which can be combined in series to make even higher orders). These can improve the sound by disrupting the very regular phase-response of the basic design.

*Inception horn noise*

There are some good designs made from just these basic elements - but choosing a design (or understanding why it's good) isn't very intuitive. It's a tricky tuning problem, from the allpass structures to the delay-times and feedback amounts.

Multi-channel allpasses

Things get a bit more varied if we look at multi-channel allpasses - but first, we need to define what that actually means.

This idea of a flat frequency-response which passes through all frequencies (and minimises colouration for a diffuser) is a good place to start. Let's phrase it slightly differently:

The total energy of the output must match the total energy of the input.
— Mark Twain and Albert Einstein, simultaneously
The energy of the signal is preserved, but it may occur in a different channel or time in the output

For single-channel allpasses, this is equivalent to the standard "flat frequency-response", but this definition generalises nicely into multi-dimensional/multi-channel situations.

Compared to the single-channel case, we have a slightly wider range of basic building-blocks available. Let's look at a few examples:

Multi-channel delay

Much like a single-channel delay is technically an allpass filter (because it has a flat frequency response), a multi-channel delay fits the bill:

It's more interesting than the single-channel case, because each channel can be delayed by a different amount.

This will be useful for our diffuser: if the input channels are somehow synchronised in time (e.g. an echo occuring in all channels simultaneously), a multi-channel delay can de-sychronise those echoes.

Mixing matrix

Another type of allpass (which only makes sense for the multi-channel case) is a mixing matrix:

Each output channel is a linear combination of the input channels. If some input feature (e.g. an echo at a certain time) appears in only one channel, this distributes that feature across the other channels as well.

Hadamard matrices
  • maximum inter-channel mixing
  • efficient: N \log_2(N) additions for an N-channel matrix.

The Hadamard family of matrices provides the maximum amount of inter-channel mixing, and can also be computed pretty efficiently for powers of 2 (N \log_2(N) additions for N channels) - here's some example code.

Shuffling channels and inverting polarity

OK, so it's not very interesting - but swapping channels around is (technically) a multi-channel allpass. We can also invert the polarity (multiply by -1) on some or all of the channels:

... and more

There are a host of other multi-channel allpass structures available, including:

  • IIR allpasses (e.g. Gerzon)
  • Single-channel allpasses applied to the channels independently
  • Lower-dimensional allpasses applied to subset of the channels

It's worth noting that in the single-channel case, IIR allpasses are pretty much the only tool we have.

In the multi-channel case, we highlighted some basic building-blocks which don't contain any feedback, but are still enough to design a great diffuser by just combining them in series - which exactly what we're going to do.

A multi-channel diffuser

So, here's the plan for our diffuser: we define a "diffusion step", made from a sequence of (multi-channel) operations:

Single diffusion step

  1. delay
  2. shuffle channels & invert (some) polarity
  3. Hadamard mixing matrix
Shuffling and polarity-flipping can technically be considered part of the Hadamard matrix, but I've listed it separately to make it clear that the shuffling/inversion should be different for each step.

The delay takes an input (containing some number of echoes) and un-aligns the channels. After some shuffling, the Hadamard matrix takes each unaligned echo, and redistributes it across all the channels:

The delay makes sure every echo is at a distinct time, and the Hadamard matrix make sure each distinct echo time is present in every channel.

Every time we apply a diffusion step like this, we end up with N times more echoes in the output than we had in the input (where N is the number of channels).

For a larger number of channels (e.g. 8+), this means we can get a very dense result from a small number of these diffusion steps.

Examples

Let's run some clicks through this diffuser, to see what it sounds like. Here's a 4-channel diffuser, with 3 diffusion steps, each with delays from 0-60ms:

4 channels, 3 stages, delays from 0-60ms.

To get smoother diffusion, you can either add more diffusion steps, or increase the number of channels. Here's an 8-channel, 4-step diffuser:

8 channels, 4 stages, delays from 0-60ms

You still need to choose the number of channels and diffusion steps, but it's pretty easy to get a good configuration out of this pattern.

Down-mixing

At some point in our final effect, we need to mix these multi-channel signals back down to a mono (or stereo) output.

When you do this downmixing, the system as a whole stops being an allpass (even if you're just mixing down to the same number of channels as you started with).

Ceci n'est pas un passe-tout.

This is fine as long as your phase-shifts don't have regular patterns which could produce an identifiable "comb" timbre. The diffuser design we've explored here gives a nicely irregular phase-response, particularly for larger numbers of channels (N).

Because the Hadamard matrix has mixed things already, we can just take the top 1 or 2 output channels to get a good result - this is what we did for the examples above.

In our final reverb design, the diffuser's multi-channel output will head straight into the multi-channel feedback loop, so we don't downmix until later.

Choosing delay times

Once you've decided what range of delay times you want for a particular diffusion step, you could pick each channel's delay time randomly. However, if you divide the range into equal segments, and use one sub-range for each channel, they end up approximately evenly-distributed while still being randomised.

Uneven diffusion steps

There's no reason the diffusion steps need to have similar delay-lengths. You could stack together short steps and long steps.

To show the difference, here's a really long diffuser: 8 channels, 5 diffusion steps, with delays from 0-300ms:

Even though the middle part is very diffuse, the start and end are slightly rough.

Here's a similar setup with the same total diffusion length, except each diffusion step is twice as long as the last:

This gives a smoother start/end, and a less noticeable "peak" in the middle.

Comparing the waveform of the two examples

So, it's not like you can't experiment and tweak this design - but both of the options sound pretty good. Varying the diffusion-step sizes like this just puts a bit of icing on the cake.

Putting it together

So: we have a multi-channel diffuser, and we have a multi-channel feedback-delay loop. All we have to do is duplicate our input, and mix down our output, and we're done!

Here's our combined design:

Everything between "split" and "mix" is a multi-channel signal. I would recommend using at least 8 internal channels for a high-quality output.

The input and output don't have to be mono - just choose something appropriate when converting to/from N channels, and you can have a stereo (or more) reverb.

Examples

We'll use a fairly long reverb configuration:

  • 8 channels for the diffuser and feedback-loop
  • 4 diffusion steps (20ms, 40ms, 80ms, 160ms)
  • feedback gain of 85%
  • feedback delays from 100-200ms

Click response

Here's what happens when we put a short "click" through it:

Pretty smooth! Let's try it on some more musical inputs:

input original reverb (wet) reverb (5% wet)
drums
piano

Drums

Here's a little drum riff:

And here's the output from our reverb:

Piano

Here's a little bit of piano:

And here's the output from our reverb:

Next steps

This is a very minimal reverb design, and there are loads of ways to extend it. I'm not going to go into much detail, just sketch out a few things you might want to add:

Early reflections

The delays in the feedback loop mean there's a gap between the original sound and the earliest echoes. To bridge this gap, we can use a separate delay path (taken from the diffused signal) to fill the time until the first echoes from the feedback loop come through.

The diffuser itself also delays the sound a bit - so we could even take some input from earlier in the diffuser, so the reverberation starts as sharply as possible (especially if the earlier diffusion steps have shorter delays as mentioned above).

Modulation

In a real space, small movements in the environment (even air currents) produce subtle variations in delay times. In an FDN reverb, this can be modeled by gently modulating some of the delay times - or cranked way past realism to get a supernaturally thick sound.

There are a couple of places where you could modulate the delays in this design, which will produce slightly different sounds:

Filtering

In real spaces, high frequencies are often absorbed faster, and have shorter decay times. We can achieve this in an FDN reverb by using shelving filters to get frequency-dependent decay gain.

If the decay gain inside your feedback loop is -1.5dB, then adding a -1.5dB high-shelf filter would make high frequencies die away twice as fast.

With those extra details in the design, you can get some a pretty good sounds:

An example made from piano, synth bass and reverb

That's it!

Everything between "split" and "mix" is a multi-channel signal. I would recommend using at least 8 internal channels for a high-quality output.

Hopefully it wasn't too complicated. You can look at some C++ example code to see these ideas in action.

I really wanted to write this article, because many of the better-known FDN designs include magic numbers or peculiar allpass structures which seem like they've been very carefuly tuned.

I didn't like this, and when one day someone asked me to write "a reverb" (no specifics), I sketched out a design which included no IIR allpasses, and was robust enough that I could throw a random-number generator at all the delay times and it would still sound good.

This is a distilled version of that design, and I hope it's a useful starting point from which people can try a bunch of interesting things. 🙂