Louis Couka

A Python Implementation of the Datorro Reverb

Published: 30/10/2024 | Author: Louis Couka

If you’re interested in implementing high-quality reverb in your audio projects, the Dattorro Reverb1 is an excellent candidate to explore. The Dattorro Reverb, even though the algorithm was first introduced decades ago, still holds up remarkably well in terms of sound quality and versatility.

Background: Why the Dattorro Reverb?

When I set out to design a reverb for my audio processing project, I started by testing several state-of-the-art plugins and algorithms. One that particularly caught my attention was the Dattorro Reverb, which has somewhat “leaked” from professional environments. The algorithm produces a lush, immersive reverb sound.

As with most reverb algorithms, a lot of magic numbers are involved. These values, carefully chosen through experimentation, are essential for creating a chaotic yet natural sound, where no aspect of the reverb feels too predictable or rigid.

Understanding the Dattorro Reverb

The Dattorro Reverb is built around a feedback delay network (IIR) with various filtering stages, the signal flow is as follow:

  1. Pre-Delay: Adds a slight delay before the reverb starts.
  2. Input Filter: A low-pass filter shapes the signal.
  3. Input Diffusor: Four all-pass filters add diffusion.
  4. Reverberation Tank (split into two parts): • Cross Feedback: Each half feeds into the other, creating a dense reverb. • Decay Diffusors: Modulated and standard all-pass filters further diffuse the signal. • Damping: Low-pass filtering removes high frequencies from the reverb tail.
  5. Output: The reverb is built from multiple delay taps, creating a lush tail.

Sound examples

Here are sound examples of the Datarow reverberation applied to a drone sound from UVI Cinematic Shades, with a long decay setting of 0.9.

Antineutron - Dry
Antineutron - Dry+Reverb

Notable Limitation

One drawback is that L and R channels are summed at the input, meaning it’s not a “true stereo” reverb. You lose some signal information since the side is discarded early on.

Final Thoughts

Despite the mono summing issue, the Dattorro Reverb delivers exceptional sound quality and is worth experimenting with. It can also serve as a strong foundation for a true stereo reverb by modifying the feedback network.

You can find my Python implementation on GitHub here or in the code section below. The implementation has been thoroughly tested to ensure it closely follows the original algorithm.

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# Exact implementation of the Datorro figure of eight implemented in Python, according to the original Paper
# Author : Louis Couka
# Website : https://www.louiscouka.com/code/datorro-reverb-implementation/
# License : MIT

import numpy as np
from scipy import signal
import matplotlib.pyplot as plt
import sounddevice as sd
#import soundfile as sf

# Input
fs = 44100
x = signal.unit_impulse(fs*2)
x = np.array([x, x]).T
#x, fs = sf.read("/Users/louiscouka/Desktop/dry.flac")

# Parameters
decay = 0.5
decay_diffusion1 = 0.7
decay_diffusion2 = 0.5
input_diffusion1 = 0.75
input_diffusion2 = 0.625
bandwidth = 0.9995
damping = 0.0005
wet = 0.4
dry = 1

# Helper that convert the sample unit of the paper according to the original fs employed = 29761
def c(numSamples):
    original_fs = 29761
    return int(np.round(numSamples*fs/original_fs))

### Init
y = np.sum(x, axis = 1) / 2 # Sum L R as in the paper
pad = 10000 # Should be more than max tap index value
y = np.append(np.zeros(pad), y, axis = 0)

### Diffuser
def allpass_comb(x, d, i, diffusion):
    y = np.zeros(len(x))
    while i < len(x):
        s = min(d, len(x) - i) # Chunk the process so we can vectorize and it's fast
        y[i:][:s] = diffusion*(x[i:][:s] - y[i-d:][:s]) + x[i-d:][:s]
        i += s
    return y
y = allpass_comb(y, c(142), pad, input_diffusion1)
y = allpass_comb(y, c(107), pad, input_diffusion1)
y = allpass_comb(y, c(379), pad, input_diffusion2)
y = allpass_comb(y, c(277), pad, input_diffusion2)

### Tank
dl = np.zeros((8, len(y))) # All delaylines
dlt = [c(672), c(4453), c(1800), c(3720), c(908), c(4217), c(2656), c(3163)] # Tap time used for each delayline
i = pad
while i < len(x):
    s = min(min(dlt), len(y) - i) # Chunk the process so we can vectorize and it's fast
    for j in range(0, 8, 4):
        dl[j+0, i:][:s] = y[i:][:s] + decay * dl[j-1, i-dlt[j-1]:][:s] + decay_diffusion1 * dl[j+0, i-dlt[j+0]:][:s]
        dl[j+1, i:][:s] =                     dl[j+0, i-dlt[j+0]:][:s] - decay_diffusion1 * dl[j+0, i:][:s]
        for k in range(i-s, i):
            dl[j+1, k] = damping*dl[j+1, k-1] + (1-damping)*dl[j+1, k]
        dl[j+2, i:][:s] =             decay * dl[j+1, i-dlt[j+1]:][:s] - decay_diffusion2 * dl[j+2, i-dlt[j+2]:][:s]
        dl[j+3, i:][:s] =                     dl[j+2, i-dlt[j+2]:][:s] + decay_diffusion2 * dl[j+2, i:][:s]
    i += s

### Build the output
yL  = np.copy(dl[5, pad-dlt[5]+c(266):][:len(x)]) # node48_54
yL += dl[5, pad-dlt[5]+c(2974):][:len(x)] # node48_54
yL -= dl[6, pad-dlt[6]+c(1913):][:len(x)] # node55_59
yL += dl[7, pad-dlt[7]+c(1996):][:len(x)] # node59_63
yL -= dl[1, pad-dlt[1]+c(1990):][:len(x)] # node24_30
yL -= dl[2, pad-dlt[2]+c(187):][:len(x)] # node31_33
yL -= dl[3, pad-dlt[3]+c(1066):][:len(x)] # node33_39

yR  = np.copy(dl[1, pad-dlt[1]+c(353 ):][:len(x)]) # node24_30
yR += dl[1, pad-dlt[1]+c(3627):][:len(x)] # node24_30
yR -= dl[2, pad-dlt[2]+c(1228):][:len(x)] # node31_33
yR += dl[3, pad-dlt[3]+c(2673):][:len(x)] # node33_39
yR -= dl[5, pad-dlt[5]+c(2111):][:len(x)] # node48_54
yR -= dl[6, pad-dlt[6]+c(335):][:len(x)] # node55_59
yR -= dl[7, pad-dlt[7]+c(121):][:len(x)] # node59_63

y = np.array([yL, yR]).T
y *= 0.6 # Final mul

sd.play(dry*x + wet*y, fs)
#sf.write("/Users/louiscouka/Desktop/wet.flac", dry*x + wet*y, fs)