https://signalsmith-audio.co.uk/writing/2021/lets-write-a-reverb/ [square] Signalsmith Audio Blog Let's Write A Reverb 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. [preview] 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). I've presented this a couple of times, most recently at ADC 21, and before that at TAP's meetup. There are some slight differences between the versions, but they mostly follow the same structure. The ADC version comes with some example code. 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 very customisable high CPU can imitate real spaces hard to modulate Feedback delay network (FDN) tricky to make realistic very efficient easy to get wrong 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: [feedback-l] 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: [feedback-n] 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: [matrix-ish] A simple example would be a mid-side encoder, which takes two input channels, and returns them in a linear combination: [matrix-mid] 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. [matrix-ort] 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. If you're using 8 or more channels for your feedback loop, I'd recommend a Householder matrix. It's simple and fast to calculate, and doesn't do too much mixing - I briefly mention below why that actually a bad thing. [feedback-n] 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. [feedback-n] 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. In more technical terms: too much mixing inside the feedback loop can move the eigentones of your reverb closer together, leaving gaps which colour the sound for longer decays. 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: [two-stage-] 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. Echo density is the number of discrete echoes occurring per second, and is a reasonable metric for diffusion. As the echo density reaches 2000-4000 echoes/second, they blend into a continuous diffuse sound - so for a reverb, that's our target. 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: The Schroeder allpass shown here is the simplest single-channel case. There are also Gerzon allpasses which are kind of the multi-channel equivalent. [schroeder-] A classic Schroeder allpass filter For the technically-minded, here are some plots for the allpass filter (solid) and just the feedback loop on its own (dotted): [schroeder-] impulse-response (top), frequency-response (middle) and phase-response (bottom) for a Schroeder allpass with gain = 0.5 [schroeder-] 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: [allpass-ch] 10 Schroeder allpasses in series, with delays from 1-15ms, and a feedback gain of 40% Incidentally, if you want a "digital plate" sound, putting a bunch of allpass filters together in series is a classic method. This mostly goes to show that musicians will find a use for anything. 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. [schroeder-] *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 [multi-allp] 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: [multi-dela] 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: [matrix-had] As we mentioned above, an orthogonal matrix preserves the energy of a signal, which exactly meets our definition above. To use a matrix, we treat each simultaneous set of input samples (from the N channels) as a vector, and apply an orthogonal N \times N 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: [channel-sw] These can both be represented by orthoginal matrices, so you could group them together as just "orthogonal matrices". But when it comes to designing a diffuser, they're a bit different, so we've listed them separately. ... 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 I haven't found any good resources on this particular diffuser design. I found a couple of forum posts and a paragraph from a book mostly describing the 2-channel case, but it doesn't seem to be a widely-discussed approach. If you find something good to link to, let me know. 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: [diffusion-] 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). [allpass-mi] [allpass-mi] 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. [random-del] [velvet-pat] This is how the delay-times are selected for "velvet noise" multi-tap delays, although that's used for single channels at a time. It also doesn't really matter that the delay-times are ordered by channel, because we shuffle the channels around straight after the delay. 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: [diffusion-] 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: [diffusion-] This gives a smoother start/end, and a less noticeable "peak" in the middle. [diffusion-] 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: [total-desi] Everything between "split" and "mix" is a multi-channel signal. I would recommend using at least 8 internal channels for a high-quality output. We've used a Householder matrix in the feedback loop because it provides some mixing, but we don't want too much - we briefly mentioned why above. Development note: if you write/test the diffuser half first (or if you tweak this design), remember to consider whether the echoes times are aligned between channels or not. The diffuser outputs aligned echoes, so will need a different "mix" section (e.g. just taking the first channels) compared to the unaligned echoes coming out of the feedback loop. 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. [early-refl] 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: [delay-modu] When you modulate a delay line, you need to implement fractional delays (unlike fixed-length delays, which should be rounded to the nearest sample). This costs a little bit of CPU. Another advantage of having a mixing matrix in the feedback loop is that if you're modulating the feedback delay times, you can just modulate some of the channels, and the matrix will (over time) spread the detuned echoes to the rest. 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. [filtering-] 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! [total-desi] 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. Geraint