Every Future has a Founding

SD-Reverb signal path

Every stage from an input sample to an output sample written as arithmetic, with the constants, the smoothing, the sample-rate dependences and a measured check.

46 minutes read

9635 words

Contents30

This page follows a stereo signal through SD-Reverb, from the sample the host hands in to the sample the host takes back. Every operation on the way is written down as arithmetic. Each stage is described twice, first in words and then in symbols. The symbols are the specification and the words are what the symbols mean.

Everything here is derived from the source code as it stands at commit 4ec3060, dated 4 September 2026. No documentation, website text or memory of earlier versions was used. Where a number is quoted, the file it comes from is given. A handful of the predictions are checked at the end against an impulse response, rendered by the repository’s own offline tool.

The mathematics assumes only the ground covered by the Fundamentals section, and each page of it is named where it is first needed. Building a morphing reverb covers the same plugin as a route in, from what a room does to sound through to the design decisions. Reading it first is optional.

The path in outline

A reverb takes a sound and adds to it the reflections a room would have added. SD-Reverb builds those reflections from delayed copies of the input. Those copies are fed back on themselves, mixed together, and made a little quieter and a little duller on every trip round the loop. The input reaches that loop through a short chain of preparation stages, and the result is blended with the untouched input at the end.

stage what it does equations
dry copy keeps the input untouched for the mix at the end (1)
low cut one-pole high-pass, 20 to 500 Hz, default 80 Hz (2) to (4)
pre-delay 0 to 200 ms, fractional, glided per sample (5) to (8)
level follower produces the morph velocity scale and no audio (9), (10)
early stage 3 delay lines, feedback 0.4, mixed by a 3 by 3 reflection (11) to (15)
diffusion 8 all-pass stages per channel, blended in by the Diffusion knob (16) to (20)
late field 6 delay lines in a feedback loop (21) to (41)
mixing matrix a 6 by 6 reflection that moves along a closed loop (42) to (48)
Entry Drift a moving injection that keeps each channel’s level (49), (50)
assembly early tap, width, equal-power mix, trim (51) to (54)
engine crossfade two complete engines, swapped over 40 ms (55), (56)

The dry signal is scaled at the very end and nowhere else. No stage filters it, delays it or widens it. Everything upstream of the mix is the wet signal.

The two stages that do most of the work are the early stage and the late field. Both are the same kind of object, a feedback delay network. That is a set of delay lines whose outputs are mixed by a matrix and written back into the lines. The early stage is a small one, with three lines and heavy loss per trip, and it produces a handful of distinct echoes. The late field is a larger one, with six lines and small loss per trip, and it produces the dense, slowly fading wash.

Samples, blocks and chunks

A digital audio signal is a list of numbers. Each number is the air pressure at one instant, and the instants are equally spaced. At a sample rate of fs=48000f_s = 48\,000 samples per second, consecutive numbers are 1/480001/48\,000 of a second apart. Signals and samples explains this from the start. This page needs only two ideas. A signal is a sequence x[n]x[n] indexed by an integer nn , and a delay by DD samples means using x[nD]x[n-D] in place of x[n]x[n] .

SD-Reverb works on two such sequences at once, a left channel xL[n]x_L[n] and a right channel xR[n]x_R[n] . The numbers are 32-bit floating point, nominally in the range 1-1 to +1+1 . The host does not deliver them one at a time but in blocks of some hundreds or thousands of samples (Reverb.cpp). Inside, the reverb cuts each block into chunks of at most 64 samples. Some things happen once per chunk, such as parameter smoothing and advancing the slow modulations. The audio arithmetic itself happens once per sample.

When the host changes a parameter in the middle of a block, the shells split the block at that sample and call the reverb twice. A change therefore lands exactly where the host asked (src/clap/Plugin.cpp). The reverb never sees this. It only ever sees a run of samples and the current parameter values.

All arithmetic on audio is done in single precision. Coefficients are computed in double precision when a parameter changes and then stored as single precision.

Notation

symbol meaning
fsf_s sample rate, samples per second
nn sample index, so x[n]x[n] is the value of signal xx at sample nn
xL[n],xR[n]x_L[n], x_R[n] the input channels
aL,aRa_L, a_R the input after the low cut
pL,pRp_L, p_R after the pre-delay, which is what the early stage receives
eL,eRe_L, e_R the early stage’s output
fL,fRf_L, f_R what the late field receives, meaning the early output, diffused
wL,wRw_L, w_R the late field’s output
yL,yRy_L, y_R the plugin’s output
u\lfloor u \rfloor the integer part of uu
round(u)\operatorname{round}(u) the nearest integer to uu
v\lVert v \rVert the length of vector vv , which is v12++vN2\sqrt{v_1^2 + \dots + v_N^2}
II the identity matrix
vTv^{\mathsf T} the transpose of vv
aba \leftarrow b replace the stored value aa by bb

Parameters are written by their panel names. All of them except Mode, Decay, Predelay, Low Cut, Morph Rate and Trim are plain numbers from 0 to 1. Decay is in seconds, Predelay in milliseconds, Low Cut in hertz and Morph Rate in hertz. Tone Tilt runs from 1-1 to +1+1 and Trim is in decibels. Mode is one of Hall, Chamber, Cathedral and Drift, numbered 0 to 3. The host sees some of these through a curve, and the section on parameter smoothing lists them. The reverb itself only ever receives the real units.

parameter default parameter default
Mode Hall Morph Rate 0.02 Hz
Size 0.50 Morph Depth 0.40
Decay 2.4 s Entry Drift 0, off
Predelay 20 ms Entry Rate 0.45
Early Level 0.35 Morph Adapt 0.50
Diffusion 1.00 Air Level 0.30
Tone Tilt 0.00 Air Motion 0.30
Low Cut 80 Hz Width 0.80
Mix 0.30 Trim 0 dB
Enabled on

The front end

The source is Reverb::process in src/dsp/Reverb.cpp.

The dry copy

The first thing the reverb does with each input sample is keep a copy of it.

dL[n]=xL[n],dR[n]=xR[n](1) d_L[n] = x_L[n], \qquad d_R[n] = x_R[n] \tag{1}

That copy is what dry means in the mix at the end. Nothing below touches it.

Low cut

Very low frequencies build up in a reverb and turn to mud. The wet path therefore starts with a filter that removes what lies below a chosen frequency and leaves everything above it. The dry signal keeps its low end, and only what enters the room is thinned.

The filter is a one-pole high-pass. The page on one-pole filters, shelves and all-passes builds it from the smoothing recurrence. Per channel, with a state s[n]s[n] that starts at 0,

sL[n]=sL[n1]+clc(xL[n]sL[n1]),aL[n]=xL[n]sL[n],(2) s_L[n] = s_L[n-1] + c_{\mathrm{lc}}\,\big(x_L[n] - s_L[n-1]\big), \qquad a_L[n] = x_L[n] - s_L[n], \tag{2}

and the same for the right channel with its own state. The state ss is a running average of the input that follows slow changes and ignores fast ones. Subtracting it from the input leaves the fast changes. The coefficient sets where slow ends and fast begins.

clc=1e2πflc/fs(3) c_{\mathrm{lc}} = 1 - e^{-2\pi f_{\mathrm{lc}}/f_s} \tag{3}

Here flcf_{\mathrm{lc}} is the Low Cut frequency, clamped to 20 to 500 Hz (Reverb.cpp). At the default 80 Hz and fs=48000f_s = 48\,000 , clc=0.010417c_{\mathrm{lc}} = 0.010417 . The response to a steady sine of frequency ff has magnitude

Hlc(f)=(1c)1eiω1(1c)eiω,ω=2πf/fs,(4) \big|H_{\mathrm{lc}}(f)\big| = \frac{(1-c)\,\big|1 - e^{-i\omega}\big|}{\big|1 - (1-c)\,e^{-i\omega}\big|}, \qquad \omega = 2\pi f / f_s, \tag{4}

which is 0 at f=0f = 0 , about 1/21/\sqrt 2 at f=flcf = f_{\mathrm{lc}} , and rises towards 1 above it. The coefficient rather than the frequency is what the parameter smoother glides.

Pre-delay

In a real hall the first reflection arrives some milliseconds after the direct sound, because the walls are further away than the source. Predelay holds the wet signal back by 0 to 200 ms before any of the reflections are built. That keeps the reverb from smearing the attack of the dry sound. The delay can be a fraction of a sample. When the control is moved the delay length glides rather than jumps, so there is no click.

The target delay in samples is P=fstpd/1000P^\star = f_s \cdot t_{\mathrm{pd}} / 1000 for a Predelay of tpdt_{\mathrm{pd}} milliseconds, clamped to 0 to 200. The delay actually applied at sample nn follows it with a per-sample recurrence.

P[n]=P[n1]+0.0005(PP[n1])(5) P[n] = P[n-1] + 0.0005\,\big(P^\star - P[n-1]\big) \tag{5}

Each sample closes one two-thousandth of the distance still to go. A change therefore settles with a time constant of 2000 samples, or 41.7 ms at 48 kHz.

At P[n]<1P[n] < 1 the pre-delay line is bypassed entirely and pL[n]=aL[n]p_L[n] = a_L[n] , with the line neither read nor written. Otherwise the line is read at the fractional position P[n]P[n] , and the new input is then written at the head. Reading between samples uses four neighbours and a cubic Hermite curve through them (src/dsp/InterpolatedDelayLine.hpp). With D=P[n]D = \lfloor P[n] \rfloor and μ=P[n]D\mu = P[n] - D the fractional part, take the four stored samples around the target position,

y1=a[nD+1],y0=a[nD],y1=a[nD1],y2=a[nD2],(6) y_{-1} = a[n-D+1],\quad y_0 = a[n-D],\quad y_1 = a[n-D-1],\quad y_2 = a[n-D-2], \tag{6}

and evaluate the cubic from the inside out.

p[n]=c0+μ(c1+μ(c2+μc3))(7) p[n] = c_0 + \mu\big(c_1 + \mu\,(c_2 + \mu\,c_3)\big) \tag{7}
c0=y0,c1=12(y1y1),c2=y152y0+2y112y2,c3=12(y2y1)+32(y0y1)(8) c_0 = y_0,\quad c_1 = \tfrac12 (y_1 - y_{-1}),\quad c_2 = y_{-1} - \tfrac52 y_0 + 2 y_1 - \tfrac12 y_2,\quad c_3 = \tfrac12 (y_2 - y_{-1}) + \tfrac32 (y_0 - y_1) \tag{8}

At μ=0\mu = 0 this returns y0=a[nD]y_0 = a[n-D] exactly and at μ=1\mu = 1 it returns y1=a[nD1]y_1 = a[n-D-1] exactly. In between it draws a curve whose slope at each end matches the slope of the neighbouring samples. So p[n]p[n] is aa delayed by P[n]P[n] samples, to a close approximation, for any P[n]P[n] .1

The pre-delay’s memory is written with the low-cut signal aa , so the two stages compose. The early stage receives the input with its lows removed and then held back.

The level follower

The last thing the front end does is watch how loud the signal entering the reflection stages is. This changes no audio. It produces one number per chunk, the morph velocity scale σ\sigma (src/dsp/AdaptiveMorphRate.hpp). The section on the trajectory uses it to slow the movement of the late field while the input is loud.

Per sample the level is the larger of the two channels’ magnitudes, [n]=max(pL[n],pR[n])\ell[n] = \max(|p_L[n]|, |p_R[n]|) . An envelope ε\varepsilon follows it with a fast attack and a slow release.

ε[n]=[n]+κ(ε[n1][n]),κ={e1/(0.050fs)[n]>ε[n1]e1/(1.2fs)otherwise(9) \varepsilon[n] = \ell[n] + \kappa\,\big(\varepsilon[n-1] - \ell[n]\big), \qquad \kappa = \begin{cases} e^{-1/(0.050\,f_s)} & \ell[n] > \varepsilon[n-1] \\ e^{-1/(1.2\,f_s)} & \text{otherwise} \end{cases} \tag{9}

The first case is the attack, at 50 ms, and the second the release, at 1.2 s.

σ=1min(4ε, 1)Morph Adapt(10) \sigma = 1 - \min(4\,\varepsilon,\ 1)\cdot \textit{Morph Adapt} \tag{10}

So σ\sigma is 1 in silence and falls to 1Morph Adapt1 - \textit{Morph Adapt} once the envelope reaches 0.250.25 , which is about 12-12 dBFS. The value used for the chunk is the one computed at its last sample. With Morph Adapt at 0 the scale is exactly 1 whatever the input, which is the constant-rate morphing guarantee the tests pin.

The early reflections stage

The source is src/dsp/EarlyReflections.cpp.

The first reflections off the nearest walls arrive as a few distinct echoes, one after another, each a little quieter. SD-Reverb makes them with three short delay lines of 6 to 26 ms at the default Size, depending on Mode. The input is written into the lines, and what comes out of them after their delay is the stage’s output. That output is also scaled down to 40 per cent, mixed across the three lines, and written back in. Each echo therefore spawns a few fainter echoes before dying away after a few trips. The stage produces a handful of reflections rather than a tail, and the test at tests/test_early.cpp checks exactly that.

Call the three lines’ stored sequences q0,q1,q2q_0, q_1, q_2 and their delays E0,E1,E2E_0, E_1, E_2 samples. Per sample the stage reads the three delayed values vi=qi[nEi]v_i = q_i[n - E_i] and forms the stereo output from them.

eL[n]=13(v0+v2),eR[n]=13(v1v2)(11) e_L[n] = \tfrac{1}{\sqrt3}\,(v_0 + v_2), \qquad e_R[n] = \tfrac{1}{\sqrt3}\,(v_1 - v_2) \tag{11}

It then scales by the feedback, v0.4vv \leftarrow 0.4\,v , and mixes the three lines with a Householder reflection A3=I2uuTA_3 = I - 2\,u u^{\mathsf T} , where uu is a unit vector.

vv2u(uv)(12) v \leftarrow v - 2\,u\,(u \cdot v) \tag{12}

Last, it adds the input and writes the result back at the head of each line.

qi[n]=vi+(B3[pL[n]pR[n]])i,B3=12[100111](13) q_i[n] = v_i + \big(B_3\,\begin{bmatrix} p_L[n] \\ p_R[n] \end{bmatrix}\big)_i, \qquad B_3 = \tfrac12 \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ 1 & -1 \end{bmatrix} \tag{13}

Those five steps together say that line 0 receives half the left input, line 1 half the right, and line 2 half the difference. The left output listens to lines 0 and 2, and the right output to lines 1 and 2 with the sign of line 2 flipped. Written as one recurrence,

q[n]=0.4A3[q0[nE0]q1[nE1]q2[nE2]]+B3p[n],e[n]=C3[q0[nE0]q1[nE1]q2[nE2]],C3=13[101011](14) q[n] = 0.4\,A_3\, \begin{bmatrix} q_0[n-E_0] \\ q_1[n-E_1] \\ q_2[n-E_2] \end{bmatrix} + B_3\, p[n], \qquad e[n] = C_3 \begin{bmatrix} q_0[n-E_0] \\ q_1[n-E_1] \\ q_2[n-E_2] \end{bmatrix}, \qquad C_3 = \tfrac{1}{\sqrt3}\begin{bmatrix} 1 & 0 & 1 \\ 0 & 1 & -1 \end{bmatrix} \tag{14}

The output is read before the input is written, so there is no zero-delay path. The first thing the early stage emits is the first delay EminE_{\min} samples after the input arrived. Each further trip round the loop multiplies by 0.40.4 , which is about 8-8 dB, so after four trips a reflection is down by more than 30 dB.

Each Mode has a table of three lengths in samples, written for 48 kHz (src/dsp/tables/DelaySets.hpp), and Size scales them.

Ei=clamp ⁣(round(E^i(0.5+Size)), 1, 8000)(15) E_i = \operatorname{clamp}\!\Big(\operatorname{round}\big(\hat E_i \cdot (0.5 + \textit{Size})\big),\ 1,\ 8000\Big) \tag{15}
Mode E^\hat E (samples) at Size 0.5, 48 kHz
Hall 385, 653, 1152 8.0, 13.6, 24.0 ms
Chamber 289, 509, 768 6.0, 10.6, 16.0 ms
Cathedral 480, 683, 1249 10.0, 14.2, 26.0 ms
Drift 336, 541, 1055 7.0, 11.3, 22.0 ms

Size 0 halves every length and Size 1 multiplies it by 1.5. The tables are in samples rather than milliseconds, and the section on sample rate says what that means at other rates.

The mixing vector uu is not fixed. It moves slowly around the balanced direction (1,1,1)/3(1,1,1)/\sqrt3 , at a third of the late field’s Morph Rate and with 0.70.7 times its Morph Depth. The section on the trajectory gives the path. The early stage uses whatever vector it is handed at the start of each chunk.

Input diffusion

The source is src/dsp/AllpassDiffuser.hpp, with the blend in Reverb.cpp.

The early stage’s output is what feeds the late field. Before it gets there it passes through a chain of eight all-pass stages per channel. An all-pass filter changes when energy arrives without changing how much of each frequency there is. A single click going in comes out as a cluster of smaller clicks spread over some milliseconds. A steady tone comes out at exactly the same level. This smears the input in time, so the late field is fed something already dense and starts its wash sooner. The Diffusion knob blends between the plain early output at 0 and the fully smeared version at 1. The direct early output that goes to the wet mix is taken before this stage. The first reflections therefore stay crisp whatever Diffusion is set to.

One stage with delay MM samples and coefficient gg keeps a delay line ww .

w[n]=z[n]+gw[nM],z[n]=w[nM]gw[n](16) w[n] = z[n] + g\,w[n-M], \qquad z'[n] = w[n-M] - g\,w[n] \tag{16}

Its transfer function and magnitude response are

Hap(z)=zMg1gzM,Hap(eiω)=1  for every ω,(17) H_{\mathrm{ap}}(z) = \frac{z^{-M} - g}{1 - g\,z^{-M}}, \qquad \big|H_{\mathrm{ap}}(e^{i\omega})\big| = 1 \ \text{ for every } \omega, \tag{17}

the second because numerator and denominator on the unit circle are complex conjugates up to a factor of unit modulus. Its response to a single impulse is a first echo of g-g at once, then (1g2)(1-g^2) after MM samples, then g(1g2)g(1-g^2) after 2M2M . Each one is gg times the last. At g=0.7g = 0.7 that is 0.70, 0.51, 0.357, 0.250-0.70,\ 0.51,\ 0.357,\ 0.250 and onward, whose squares sum to exactly 1.

The eight stages are in series, each channel with its own delays, rounded from milliseconds.

Ms=max ⁣(1, round(fsms/1000))(18) M_s = \max\!\big(1,\ \operatorname{round}(f_s \cdot m_s / 1000)\big) \tag{18}
stage 1 2 3 4 5 6 7 8
left msm_s (ms) 1.7 2.3 3.1 4.1 5.3 6.7 8.1 9.3
right msm_s (ms) 1.9 2.7 3.5 4.5 5.7 7.1 8.5 9.7

The coefficient is g=0.7g = 0.7 for every stage, and the reverb never changes it. The chain’s transfer function is the product of the eight, still of unit magnitude. Its impulse response is the eight single responses convolved together, a cluster lasting some tens of milliseconds whose direct term is (0.7)8=0.0576(-0.7)^8 = 0.0576 .

With hLh_L the chain’s output and δ\delta the effective diffusion, the late field receives

fL[n]=eL[n]+δ(hL[n]eL[n]),(19) f_L[n] = e_L[n] + \delta\,\big(h_L[n] - e_L[n]\big), \tag{19}

and likewise on the right. For δ\delta strictly between 0 and 1 the combination (1δ)+δHap(1-\delta) + \delta\,H_{\mathrm{ap}} is not itself all-pass. Its magnitude varies with frequency between 12δ|1 - 2\delta| and 1. Only the two ends of the knob are exactly neutral in level.

The effective diffusion is the knob raised by a floor. A long decay with little diffusion lets individual delay lines ring as audible pitches, so the reverb raises a floor under the knob as Decay grows.

δ=max ⁣(Diffusion, clamp ⁣(T440.5, 0, 0.5))(20) \delta = \max\!\Big(\textit{Diffusion},\ \operatorname{clamp}\!\big(\tfrac{T - 4}{4}\cdot 0.5,\ 0,\ 0.5\big)\Big) \tag{20}

Here TT is the smoothed Decay in seconds. There is no floor up to 4 s, and it rises to 0.5 at 8 s and beyond. The same δ\delta also shapes the late field’s mixing vector.

The late field

The source is src/dsp/FDN.cpp, with src/dsp/MultibandDamping.cpp, src/dsp/FeedbackNonlinearity.hpp, src/dsp/RoomTone.hpp and src/dsp/HouseholderMatrix.hpp.

The late field is six delay lines in a loop, 15 to 180 ms long at the default Size depending on Mode. Over the whole Size range that is 7.5 to 270 ms. What comes out of each line is sent to the output, which is the reverb tail a listener hears. The same value is also made quieter, by a factor chosen so that the tail takes exactly Decay seconds to fall by 60 dB. The bass loses less per trip and the treble loses more. A gentle limiter that does nothing at normal levels comes next, then a barely audible whisper of filtered noise when Air Level is up. Last, a matrix that conserves the total energy mixes the value with the other five. The six results are written back into the lines together with the new input.

Six lines whose lengths share no common factor produce echoes that never line up, so the sum sounds smooth rather than fluttery. Every trip round the loop loses a fixed fraction, so the tail is an exponential decay. The fraction depends on frequency, so the tail’s colour decays too.

Call the lines’ stored sequences 0,,5\ell_0, \dots, \ell_5 and their delays D0,,D5D_0, \dots, D_5 . Per sample, the field runs seven steps in this order. Step one reads the lines.

vi=i[nDi],i=0,,5(21) v_i = \ell_i[n - D_i], \qquad i = 0, \dots, 5 \tag{21}

Step two takes the output taps, before anything else touches vv .

wL[n]=13(v0+v1+v2),wR[n]=13(v3+v4+v5)(22) w_L[n] = \tfrac{1}{\sqrt3}\,(v_0 + v_1 + v_2), \qquad w_R[n] = \tfrac{1}{\sqrt3}\,(v_3 + v_4 + v_5) \tag{22}

Step three applies the damping, per line, which is a frequency-dependent gain below 1.

viHi{vi}(23) v_i \leftarrow \mathcal{H}_i\{v_i\} \tag{23}

Step four applies the soft limit, per line.

viϕ(vi)(24) v_i \leftarrow \phi(v_i) \tag{24}

Step five adds the room tone, per line, and only when Air Level is above 0.

vivi+α[n]νi[n](25) v_i \leftarrow v_i + \alpha[n]\,\nu_i[n] \tag{25}

Step six mixes, with the Householder reflection A=I2rrTA = I - 2 r r^{\mathsf T} and r=1\lVert r \rVert = 1 .

vv2r(rv)(26) v \leftarrow v - 2\,r\,(r \cdot v) \tag{26}

Step seven injects the input and writes the lines back.

i[n]=vi+(B(t)[fL[n]fR[n]])i(27) \ell_i[n] = v_i + \big(B(t)\,\begin{bmatrix} f_L[n] \\ f_R[n] \end{bmatrix}\big)_i \tag{27}

When Entry Drift is off, B(t)B(t) is the fixed matrix

B0=12[100110011001].(28) B_0 = \tfrac12 \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ -1 & 0 \\ 0 & -1 \\ 1 & 0 \\ 0 & 1 \end{bmatrix}. \tag{28}

The left input enters lines 0, 2 and 4 with line 2 inverted, and the right enters lines 1, 3 and 5 with line 3 inverted, each at half amplitude. The output taps read lines 0, 1 and 2 for the left and 3, 4 and 5 for the right. Each output channel therefore hears two lines fed by one input channel and one fed by the other. That asymmetry is what makes the two outputs differ from each other while both channels share one network.

The delays

As for the early stage, each Mode has a table for 48 kHz scaled by Size.

Di=clamp ⁣(round(D^i(0.5+Size)), 1, 30000)(29) D_i = \operatorname{clamp}\!\Big(\operatorname{round}\big(\hat D_i \cdot (0.5 + \textit{Size})\big),\ 1,\ 30\,000\Big) \tag{29}
Mode D^\hat D (samples) at Size 0.5, 48 kHz (ms)
Hall 961, 1259, 2893, 3103, 3367, 4560 20.0, 26.2, 60.3, 64.6, 70.1, 95.0
Chamber 727, 1141, 1173, 1861, 2101, 2161 15.1, 23.8, 24.4, 38.8, 43.8, 45.0
Cathedral 1921, 2588, 4553, 5609, 8047, 8641 40.0, 53.9, 94.9, 116.9, 167.6, 180.0
Drift 720, 1151, 2213, 2543, 2939, 3361 15.0, 24.0, 46.1, 53.0, 61.2, 70.0

Every table is pairwise coprime, meaning no two entries share a factor, which keeps the echo pattern from repeating early. The rounding in equation (29) does not preserve that property at other Size settings. The longest possible delay is 8641×1.5=129628641 \times 1.5 = 12\,962 samples, inside the 32768-sample buffers each line allocates.

Damping and the three bands

Sound in a room loses energy at every wall and in the air, and it loses more of its high frequencies than its low ones. SD-Reverb copies this by giving each line a gain below 1 that differs in three frequency regions, below about 250 Hz, between 250 and 4000 Hz, and above 4000 Hz. The middle region’s gain is set so that the tail falls 60 dB in exactly Decay seconds. The low region decays 1.3 times slower than that. The high region decays faster, by a margin that grows with Size. Air absorbs treble per metre travelled, so a bigger room loses more of it. Tone Tilt leans the whole curve darker or brighter.

A sample circulating in line ii makes one trip every DiD_i samples, which is every Di/fsD_i / f_s seconds, and is multiplied by gig_i each trip. After TT seconds it has made Tfs/DiT f_s / D_i trips and is at giTfs/Dig_i^{\,T f_s / D_i} of its starting value. Requiring that to be 10310^{-3} , which is 60-60 dB, gives the sixty-decibel gain .

gi=103Di/(Tfs)(30) g_i = 10^{-3 D_i / (T f_s)} \tag{30}

Longer lines get a smaller gig_i so that all six decay at the same rate per second. At the Hall defaults, with T=2.4T = 2.4 s, the six values are 0.944,0.927,0.841,0.830,0.817,0.7610.944, 0.927, 0.841, 0.830, 0.817, 0.761 .

The three bands get three decay times, TT , TmloT m_{\mathrm{lo}} and TmhiT m_{\mathrm{hi}} .

mlo=clamp ⁣(1.3(10.25τ), 0.6, 2.0),mhi=clamp ⁣((0.90.35Size)(1+0.6τ), 0.15, 1.4)(31) m_{\mathrm{lo}} = \operatorname{clamp}\!\big(1.3\,(1 - 0.25\,\tau),\ 0.6,\ 2.0\big), \qquad m_{\mathrm{hi}} = \operatorname{clamp}\!\big((0.9 - 0.35\,\textit{Size})\,(1 + 0.6\,\tau),\ 0.15,\ 1.4\big) \tag{31}

Here τ\tau is Tone Tilt. With the tilt at 0, the highs decay in 0.9T0.9\,T at Size 0 and 0.55T0.55\,T at Size 1. From the three times come three gains by equation (30), and the filter is built from their ratios.

gimid=103Di/(Tfs),γilo=103Di/(Tmlofs)gimid,γihi=103Di/(Tmhifs)gimid(32) g_i^{\mathrm{mid}} = 10^{-3D_i/(T f_s)}, \qquad \gamma_i^{\mathrm{lo}} = \frac{10^{-3D_i/(T m_{\mathrm{lo}} f_s)}}{g_i^{\mathrm{mid}}}, \qquad \gamma_i^{\mathrm{hi}} = \frac{10^{-3D_i/(T m_{\mathrm{hi}} f_s)}}{g_i^{\mathrm{mid}}} \tag{32}

The ratio γlo\gamma^{\mathrm{lo}} is slightly above 1, holding the lows up relative to the mids, and γhi\gamma^{\mathrm{hi}} is slightly below.

The filter is two shelves , each built from one one-pole low-pass, followed by the mid gain. With two states silos^{\mathrm{lo}}_i and sihis^{\mathrm{hi}}_i per line,

silosilo+clo(visilo),u=vi+(γilo1)silo,(33) s^{\mathrm{lo}}_i \leftarrow s^{\mathrm{lo}}_i + c_{\mathrm{lo}}\,(v_i - s^{\mathrm{lo}}_i), \qquad u = v_i + (\gamma_i^{\mathrm{lo}} - 1)\, s^{\mathrm{lo}}_i, \tag{33}
sihisihi+chi(usihi),u=u+(γihi1)(usihi),(34) s^{\mathrm{hi}}_i \leftarrow s^{\mathrm{hi}}_i + c_{\mathrm{hi}}\,(u - s^{\mathrm{hi}}_i), \qquad u' = u + (\gamma_i^{\mathrm{hi}} - 1)\,(u - s^{\mathrm{hi}}_i), \tag{34}
Hi{vi}=gimidu.(35) \mathcal H_i\{v_i\} = g_i^{\mathrm{mid}}\, u'. \tag{35}

Equation (33) adds (γlo1)(\gamma^{\mathrm{lo}} - 1) times the low-passed signal. Below the corner the low-pass passes everything, so the gain there is γlo\gamma^{\mathrm{lo}} , and above it the low-pass passes nothing, so the gain is 1. Equation (34) adds (γhi1)(\gamma^{\mathrm{hi}} - 1) times the high-passed signal, which is the input minus the low-pass, so the gain is 1 below the corner and γhi\gamma^{\mathrm{hi}} above. The corners are 250 Hz and 4000 Hz, with coefficients as in equation (3).

clo=1e2π250/fs=0.03220,chi=1e2π4000/fs=0.40762(36) c_{\mathrm{lo}} = 1 - e^{-2\pi\cdot 250/f_s} = 0.03220, \qquad c_{\mathrm{hi}} = 1 - e^{-2\pi\cdot 4000/f_s} = 0.40762 \tag{36}

Both values are for fs=48000f_s = 48\,000 . As a transfer function, with Lc(z)=c/(1(1c)z1)L_c(z) = c / (1 - (1-c) z^{-1}) the one-pole low-pass,

Hi(z)=gimid[1+(γilo1)Lclo(z)][1+(γihi1)(1Lchi(z))].(37) H_i(z) = g_i^{\mathrm{mid}}\,\Big[1 + (\gamma_i^{\mathrm{lo}} - 1)\,L_{c_{\mathrm{lo}}}(z)\Big]\Big[1 + (\gamma_i^{\mathrm{hi}} - 1)\,\big(1 - L_{c_{\mathrm{hi}}}(z)\big)\Big]. \tag{37}

At f=0f = 0 this is exactly gimidγilog_i^{\mathrm{mid}}\gamma_i^{\mathrm{lo}} , the low band’s target. At the highest representable frequency, fs/2f_s/2 , the one-pole low-pass is not quite zero but c/(2c)c/(2-c) , which is 0.016 for the low corner and 0.256 for the high one. The high band’s gain is therefore approached rather than reached. For the longest Hall line the target is 0.686 and the value at fs/2f_s/2 is 0.706. The repository’s own test allows 5 per cent at the top of the band. The section on measurements shows what that does to a measured decay time.

Putting the three regions side by side at the Hall defaults, with Decay 2.4 s, Size 0.5 and tilt 0:

band multiplier decay time
below 250 Hz 1.3 3.12 s
250 to 4000 Hz 1 2.40 s
above 4000 Hz 0.90.35×0.5=0.7250.9 - 0.35 \times 0.5 = 0.725 1.74 s

The transitions are gradual, an octave or so wide on each side of the corner, because a one-pole filter is gentle.

The soft limit

If anything ever pushed the loop’s level far above normal, such as an extreme Decay with a very loud input, the values could grow without bound. A gentle curve applied to each line value makes that impossible. It is indistinguishable from a straight line for values below about 1, bends over for larger ones, and never lets anything through above 2. In normal use it does nothing measurable.

The threshold is Θ=3\Theta = 3 (src/dsp/FeedbackNonlinearity.hpp).

ϕ(x)={x(1x23Θ2)x<Θsign(x)2Θ3xΘ(38) \phi(x) = \begin{cases} x\,\Big(1 - \dfrac{x^2}{3\,\Theta^2}\Big) & |x| < \Theta \\[1.2ex] \operatorname{sign}(x)\,\dfrac{2\Theta}{3} & |x| \ge \Theta \end{cases} \tag{38}

Its slope is ϕ(x)=1x2/Θ2\phi'(x) = 1 - x^2/\Theta^2 , which is 1 at zero and falls to 0 at ±Θ\pm\Theta , where the curve meets the flat part with no kink. So ϕ\phi never amplifies, since ϕ(x)x|\phi(x)| \le |x| and 0ϕ10 \le \phi' \le 1 . That is what lets the stability argument below count it as harmless. At x=0.1x = 0.1 it returns 0.099960.09996 , at x=1x = 1 it returns 0.9630.963 , at x=2x = 2 it returns 1.7041.704 , and at x3x \ge 3 exactly 2.

Room tone

A real room carries a floor of quiet air movement. When Air Level is above 0, each line has a tiny amount of soft noise added to it inside the loop. The noise therefore circulates through the loop and picks up the room’s colour, in the way any other content of the loop does. At the default Air Level of 0.30 it sits about 80 dB below full scale, far under the tail it rides on. Air Motion makes its level breathe slowly, over a cycle of about 6.7 seconds. At Air Level 0 the step is skipped entirely.

Each line ii has its own noise generator νi\nu_i , from six different seeds, built in three parts (src/dsp/RoomTone.hpp). A 32-bit xorshift generator produces a pseudo-random integer SS each sample, and the top 24 bits are mapped to a number ξ\xi in [1,1)[-1, 1) . This is white noise, with every frequency equally present.

Three one-pole low-passes with different corners are then summed with a direct term, which tilts the spectrum towards pink .

b00.99765b0+0.0990460ξ,b10.96300b1+0.2965164ξ,b20.57000b2+1.0526913ξ(39) b_0 \leftarrow 0.99765\,b_0 + 0.0990460\,\xi,\quad b_1 \leftarrow 0.96300\,b_1 + 0.2965164\,\xi,\quad b_2 \leftarrow 0.57000\,b_2 + 1.0526913\,\xi \tag{39}
πk=0.12(b0+b1+b2+0.1848ξ)(40) \pi_{\mathrm{k}} = 0.12\,(b_0 + b_1 + b_2 + 0.1848\,\xi) \tag{40}

The three pole radii correspond to corners of about 18 Hz, 288 Hz and 4.3 kHz at 48 kHz. They scale with the sample rate. A one-pole low-pass at 8 kHz then takes the edge off, with νiνi+c8k(πkνi)\nu_i \leftarrow \nu_i + c_{8\mathrm k}\,(\pi_{\mathrm k} - \nu_i) and c8k=1e2π8000/fsc_{8\mathrm k} = 1 - e^{-2\pi\cdot 8000/f_s} .

The amplitude is common to all six lines and wanders with a sine at 0.15 Hz.

θ[n]=θ[n1]+2π0.15fs (mod 2π),α[n]=λ(1+0.9Air Motionsinθ[n]),λ=Air Level1040.30(41) \theta[n] = \theta[n-1] + \frac{2\pi \cdot 0.15}{f_s} \ (\bmod\ 2\pi), \qquad \alpha[n] = \lambda\,\big(1 + 0.9\,\textit{Air Motion}\cdot\sin\theta[n]\big), \qquad \lambda = \textit{Air Level}\cdot \frac{10^{-4}}{0.30} \tag{41}

So λ=104\lambda = 10^{-4} , which is 80-80 dBFS, at the default. With Air Motion at 1 the level swings between 0.1λ0.1\lambda and 1.9λ1.9\lambda .

The noise is added in step five, after damping and before the mixing matrix. Its first appearance in the output is therefore only after it has been mixed and delayed. From then on it decays and re-enters like any other content of the loop.

The moving mixing matrix

The source is src/dsp/HouseholderMatrix.hpp and src/dsp/MatrixMorph.hpp.

The matrix

After damping, the six line values are mixed, so that each line’s next input is a combination of all six. The rule has to conserve energy, or the loop would either die faster than Decay says or run away. SD-Reverb uses a reflection , which mirrors the six-dimensional vector of line values in the plane perpendicular to a chosen direction rr . A mirror image is exactly as long as the original, which is the conservation needed.

With a unit vector rr ,

A=I2rrT,Av=v2r(rv).(42) A = I - 2\,r\,r^{\mathsf T}, \qquad A\,v = v - 2\,r\,(r \cdot v). \tag{42}

The second form is how it is computed. It is one dot product and one scaled subtraction, twelve multiplications for six lines, and the 36 matrix entries are never written down. Three properties follow, each pinned by a test in tests/test_matrix.cpp. The length Av\lVert A v \rVert equals v\lVert v \rVert for every vv , so energy is conserved. Applying AA twice gives AA=IA\,A = I . The direction rr is flipped, with Ar=rA\,r = -r , and Av=vA\,v = v whenever vv is perpendicular to rr .

Any unit vector gives a valid matrix. The balanced vector r=(1,1,1,1,1,1)/6r = (1,1,1,1,1,1)/\sqrt6 gives A=I13JA = I - \tfrac13 J , where JJ is the all-ones matrix. Every line then receives 23\tfrac23 of its own value minus 13\tfrac13 of each other line, which is the maximum mixing. A vector concentrated on one line, r=(1,0,0,0,0,0)r = (1,0,0,0,0,0) , gives A=diag(1,1,1,1,1,1)A = \operatorname{diag}(-1,1,1,1,1,1) and no mixing at all. Each line then feeds only itself and the network is six independent comb filters.

The Diffusion knob, through δ\delta of equation (20), chooses a base vector between those two.

r0(δ)=normalise(1+δ(161), δ6, δ6, δ6, δ6, δ6)(43) r_0(\delta) = \operatorname{normalise}\Big( 1 + \delta\,\big(\tfrac{1}{\sqrt6} - 1\big),\ \tfrac{\delta}{\sqrt6},\ \tfrac{\delta}{\sqrt6},\ \tfrac{\delta}{\sqrt6},\ \tfrac{\delta}{\sqrt6},\ \tfrac{\delta}{\sqrt6} \Big) \tag{43}

The value at δ=1\delta = 1 is the balanced vector, and at δ=0\delta = 0 it is the concentrated one. Halfway between, at δ=0.5\delta = 0.5 , it reads (0.839,0.243,,0.243)(0.839, 0.243, \dots, 0.243) .

The trajectory

A fixed matrix gives a fixed set of resonances. A tail that lasts seconds can then reveal them as a faint metallic ring. SD-Reverb moves the vector rr slowly around a closed loop near the base vector. Morph Rate sets the cycles per second and Morph Depth sets the amount. Every point on the loop is a unit vector, so the matrix is exactly energy-conserving at every instant. The movement therefore changes which resonances are excited without changing how fast the tail decays. The measurement below confirms this, with the decay time varying by under 3 per cent across the loop.

The loop is built from KK anchor directions, with K=6K = 6 for the late field and K=4K = 4 for the early stage and Entry Drift.

ak,i=sin ⁣(2π(k+1)(i+1)N+0.7Mode+0.31k)aˉk,k=0..K1, i=0..N1(44) a_{k,i} = \sin\!\Big( \frac{2\pi\,(k+1)(i+1)}{N} + 0.7\,\textit{Mode} + 0.31\,k \Big) - \bar a_k, \qquad k = 0..K-1,\ i = 0..N-1 \tag{44}

Here aˉk\bar a_k is the mean over ii , subtracted so that each anchor sums to zero. Each anchor is pushed away from the base vector by a per-mode spread ρ\rho and normalised.

Lk=normalise(r0+ρak),ρ={0.35Hall0.50Chamber0.25Cathedral0.80Drift(45) L_k = \operatorname{normalise}\big( r_0 + \rho\,a_k \big), \qquad \rho = \begin{cases} 0.35 & \text{Hall} \\ 0.50 & \text{Chamber} \\ 0.25 & \text{Cathedral} \\ 0.80 & \text{Drift} \end{cases} \tag{45}

A phase φ\varphi in [0,1)[0,1) advances once per chunk of mm samples, gated by the velocity scale σ\sigma of equation (10).

φ(φ+fmorphmfsσ)mod1(46) \varphi \leftarrow \Big(\varphi + \frac{f_{\mathrm{morph}}\cdot m}{f_s}\cdot\sigma\Big) \bmod 1 \tag{46}

That happens only when both fmorph>0f_{\mathrm{morph}} > 0 and Morph Depth is above 0. Otherwise φ\varphi stays where it is. The current vector is then

j=Kφ,μ=Kφj,F=slerp(Lj, L(j+1)modK, μ),r=slerp(r0, F, Morph Depth).(47) j = \lfloor K\varphi \rfloor, \quad \mu = K\varphi - j, \quad F = \operatorname{slerp}\big(L_j,\ L_{(j+1) \bmod K},\ \mu\big), \quad r = \operatorname{slerp}\big(r_0,\ F,\ \textit{Morph Depth}\big). \tag{47}

The operation slerp\operatorname{slerp} is spherical linear interpolation , the way to move between two unit vectors along the sphere rather than along the chord. With Ω=arccos(ab)\Omega = \arccos(a \cdot b) ,

slerp(a,b,t)=sin((1t)Ω)sinΩa+sin(tΩ)sinΩb,(48) \operatorname{slerp}(a, b, t) = \frac{\sin\big((1-t)\Omega\big)}{\sin \Omega}\,a + \frac{\sin(t\,\Omega)}{\sin\Omega}\,b, \tag{48}

and at Ω<105\Omega < 10^{-5} the code uses ordinary straight-line interpolation followed by normalisation. Every result has unit length, so AA is always exactly a reflection.

The phase therefore walks round the polygon L0L1LK1L0L_0 \to L_1 \to \dots \to L_{K-1} \to L_0 on the sphere, and Morph Depth pulls the point back towards the base vector. Depth 0 means staying at r0r_0 and depth 1 means following the polygon. At Morph Depth exactly 0 the code returns r0r_0 without computing anything, which is why a rate change at depth 0 is bit-for-bit inaudible.

Two facts about the anchors follow from equation (44). Take k=K1k = K - 1 , the last anchor, with K=NK = N . The argument of the sine is then 2π(i+1)2\pi(i+1) plus a constant. The same holds for k=2k = 2 when N=3N = 3 and K=4K = 4 . The sine is then the same for every ii , and subtracting the mean leaves exactly zero. So one anchor of every loop is the base vector itself, and the loop passes through r0r_0 once per cycle. At the Hall spread of 0.35 with δ=1\delta = 1 , the other five late anchors sit 26 to 31 degrees from the base vector.

trajectory NN KK base rate depth spread
late field 6 6 r0(δ)r_0(\delta) Morph Rate Morph Depth by Mode
early stage 3 4 balanced Morph Rate / 3 0.7 times Morph Depth by Mode
Entry Drift 3 4 balanced 0.08+0.250.08 + 0.25\cdot Entry Rate Hz 0.4 times Entry Drift 0.35, mode 0 always

Both the late and early trajectories are advanced with the same σ\sigma . The Entry Drift trajectory always advances with σ=1\sigma = 1 . The late field’s base vector is rebuilt every chunk from the smoothed δ\delta , so turning Diffusion moves the whole loop.

Entry Drift

The source is src/dsp/InputMatrixMorph.hpp, with the crossfade in FDN.cpp.

With the injection matrix B0B_0 of equation (28) fixed, the same note played twice excites the room the same way twice. Entry Drift slowly changes which of the six lines the input enters, and by how much, so that the second note takes a slightly different door. It never changes how loud the input is, and it is kept from moving the stereo image. The three lines the left channel feeds are rotated among themselves, and the three the right channel feeds likewise, by the same 3 by 3 reflection.

A unit vector uB(t)u_B(t) on a K=4K = 4 trajectory of its own defines R3=I2uBuBTR_3 = I - 2 u_B u_B^{\mathsf T} . Given the input pair, the static injection ι=B0[fL,fR]T\iota = B_0\,[f_L, f_R]^{\mathsf T} is formed first, and the two triples are then reflected separately.

[ι0ι2ι4]R3[ι0ι2ι4],[ι1ι3ι5]R3[ι1ι3ι5](49) \begin{bmatrix} \iota_0 \\ \iota_2 \\ \iota_4 \end{bmatrix} \leftarrow R_3 \begin{bmatrix} \iota_0 \\ \iota_2 \\ \iota_4 \end{bmatrix}, \qquad \begin{bmatrix} \iota_1 \\ \iota_3 \\ \iota_5 \end{bmatrix} \leftarrow R_3 \begin{bmatrix} \iota_1 \\ \iota_3 \\ \iota_5 \end{bmatrix} \tag{49}

Because R3R_3 is a reflection, each triple keeps its length. The left channel’s total injection is 32fL\tfrac{\sqrt3}{2}|f_L| before and after, whatever uBu_B is, and tests/test_entrydrift.cpp checks this to 10410^{-4} .

One consequence of using a reflection is that R3R_3 is never the identity. Even at the smallest non-zero depth, where uBu_B is the balanced vector (1,1,1)/3(1,1,1)/\sqrt3 , the left triple (12,12,12)fL(\tfrac12, -\tfrac12, \tfrac12)\,f_L becomes (16,56,16)fL(\tfrac16, -\tfrac56, \tfrac16)\,f_L . Switching the feature on therefore moves the entry pattern to a different fixed pattern of the same strength, and the trajectory then wanders around that one. The switch itself is covered by a 30 ms linear crossfade aa between the static and the reflected injection.

injection=(1a)B0f+aR(t)B0f,a:01 over 0.03fs samples(50) \text{injection} = (1 - a)\,B_0 f + a\,R(t) B_0 f, \qquad a: 0 \to 1 \text{ over } 0.03\,f_s \text{ samples} \tag{50}

When Entry Drift returns to 0 the fade runs back to 0 and the static code path is taken again. With the feature at 0, Entry Rate has no effect on the output at all.

Stability of the loop

A feedback loop is safe when every trip round it loses energy. Three of the late field’s steps cannot add energy, and one always removes some. Together that is enough.

Look at what one pass of steps three to seven does to the vector vv of six line values. Ignore the input and the room tone for the moment. The reflection at step six leaves v\lVert v \rVert unchanged by equation (42). The soft limit at step four never increases any component, because ϕ(x)x|\phi(x)| \le |x| . The damping at step three is a linear filter per line whose gain at every frequency is below 1. From equation (37) the low shelf’s magnitude lies between γlo\gamma^{\mathrm{lo}} and about 1, and the high shelf’s between about 1 and γhi\gamma^{\mathrm{hi}} . So Higimidmax(γilo,1)<1|H_i| \le g_i^{\mathrm{mid}}\max(\gamma_i^{\mathrm{lo}}, 1) < 1 whenever Decay is finite. The product gimidγilog_i^{\mathrm{mid}}\gamma_i^{\mathrm{lo}} is the low band’s own gain 103Di/(Tmlofs)10^{-3D_i/(T m_{\mathrm{lo}} f_s)} , below 1 for any line length and any finite Decay, the largest of 50 s included. The delay lines themselves only store values.

The energy in the loop is therefore multiplied by a factor below 1 every circulation, and with no input it decays geometrically to zero. The tests hold the tail below 10610^{-6} after 10 s of silence with a 2 s Decay, and finite for a full-scale impulse train at Decay 50 s. With input, the loop’s content is the sum of a decaying copy of every input sample that ever entered. That is exactly what a reverb tail is. The room tone is a bounded input like any other. This argument does not depend on rr being fixed, which is why the morphing above is free.

Iterating a matrix is the general statement of this. A map that is a length-keeping matrix times a gain below 1 has orbits whose length follows that gain.

Wet signal, width, mix and trim

The early stage’s direct output and the late field’s output are summed, the former scaled by Early Level.

y~L[n]=Early LeveleL[n]+wL[n],y~R[n]=Early LeveleR[n]+wR[n](51) \tilde y_L[n] = \textit{Early Level}\cdot e_L[n] + w_L[n], \qquad \tilde y_R[n] = \textit{Early Level}\cdot e_R[n] + w_R[n] \tag{51}

If an engine crossfade is running, this sum is formed for both engines and blended. The stereo image is then adjusted in mid and side form.

m=12(y~L+y~R),s=12(y~Ly~R)Width,y~Lm+s,y~Rms(52) m = \tfrac12\,(\tilde y_L + \tilde y_R), \qquad s = \tfrac12\,(\tilde y_L - \tilde y_R)\cdot\textit{Width}, \qquad \tilde y_L \leftarrow m + s, \quad \tilde y_R \leftarrow m - s \tag{52}

Width 1 leaves the signal unchanged and 0 collapses it to mono, with both channels equal to mm . The default of 0.8 shrinks the difference between the channels to 80 per cent. This applies to the wet signal only.

Dry and wet are blended with an equal-power law .

Gdry=cos(π2Mix),Gwet=sin(π2Mix)η(53) G_{\mathrm{dry}} = \cos\big(\tfrac{\pi}{2}\,\textit{Mix}\big), \qquad G_{\mathrm{wet}} = \sin\big(\tfrac{\pi}{2}\,\textit{Mix}\big)\cdot \eta \tag{53}

Here η\eta is the smoothed Enabled value, which is 1 when on. Because cos2+sin2=1\cos^2 + \sin^2 = 1 , the total power stays constant across the knob when dry and wet are unrelated signals, which they are. At Mix 0.5 both gains are 0.7070.707 , and at the default 0.3 they are 0.8910.891 and 0.4540.454 .

yL[n]=Gtrim(GdrydL[n]+Gwety~L[n]),Gtrim=10Trim/20(54) y_L[n] = G_{\mathrm{trim}}\,\big( G_{\mathrm{dry}}\, d_L[n] + G_{\mathrm{wet}}\, \tilde y_L[n] \big), \qquad G_{\mathrm{trim}} = 10^{\,\textit{Trim}/20} \tag{54}

The same holds for the right. Trim is clamped to ±24\pm 24 dB, a factor of 15.85 either way, and is applied after the mix, so it scales the dry signal too.

When the Enabled switch is turned off, η\eta ramps towards 0 through the same per-chunk smoother as Mix. The wet signal fades out over about 22 chunks, which is 29 ms at 48 kHz. Once η104\eta \le 10^{-4} the wet path is not computed at all. The reverb’s memory is cleared and the output is GtrimxG_{\mathrm{trim}}\,x until the switch returns. Switching back on therefore starts from an empty room.

The meters are peak detectors on the input and output chunks. They read the audio and change nothing.

Changing the delay lengths

The source is Reverb::maybeReconfigure and FDN::adoptFieldFrom.

Most controls can be applied to a running reverb by changing a number. The delay lengths need more, because shortening a delay line while a tail is in it would skip part of the stored sound and click. So the reverb keeps two complete early-plus-late engines, of which only one is normally running. When Mode or Size changes enough to need new lengths, the idle engine is set up with them. It is also given a copy of the sounding engine’s stored audio, so that it continues the same tail re-read at the new lengths. The output then crossfades from the old engine to the new one over 40 ms. If nothing is sounding at that moment the switch is made instantly instead.

Let ς=0.5+Size\varsigma = 0.5 + \textit{Size} be the delay scale. A reconfiguration is needed when the mode differs from the one applied, or when ςςapplied>0.03|\varsigma - \varsigma_{\text{applied}}| > 0.03 . Size therefore moves the delays in steps of at least 0.03, while its effect on damping through equation (31) is continuous.

The wet level is tracked by a peak follower over chunks, ωmax(chunk peak, 0.99ω)\omega \leftarrow \max(\text{chunk peak},\ 0.99\,\omega) . If ω<105\omega < 10^{-5} , or the reverb has not yet processed anything, the active engine is reconfigured and cleared in place. Otherwise the standby engine is configured and a crossfade begins. It receives the active engine’s delay-line contents and filter states, but neither its gains nor its delays. Per sample, with χ\chi rising from 0 to 1 in steps of 1/(0.040fs)1/(0.040 f_s) ,

y~=(1χ)y~active+χy~standby.(55) \tilde y = (1 - \chi)\,\tilde y^{\text{active}} + \chi\,\tilde y^{\text{standby}}. \tag{55}

Both engines process the same input during the fade, and both are handed the same mixing vectors. When χ\chi reaches 1 the roles swap and the now-idle engine is cleared. A change requested during a fade is applied when the fade ends.

Parameter smoothing

A parameter change from the host is a step. Applied directly, a step in a gain is a click. So every continuous parameter glides towards its target, and the gliding is where the values the audio actually uses come from.

Each of the following moves 35 per cent of the way to its target once per chunk of 64 samples.

cc+0.35(cc)(56) c \leftarrow c + 0.35\,(c^\star - c) \tag{56}

Those parameters are Mix, Width, Enabled, Early Level, Decay, Tone Tilt, Morph Depth, Entry Drift and Air Level. Three more are smoothed in a converted form: Trim as a linear gain, Diffusion as δ\delta , and Low Cut as the coefficient clcc_{\mathrm{lc}} . After kk chunks the remaining distance is 0.65k0.65^k . One per cent is reached after 11 chunks, which is 14.7 ms at 48 kHz. Decay, Tone Tilt, Morph Depth, Entry Drift and Air Level are pushed into the engines only when they have moved by more than 10510^{-5} . A static parameter therefore costs nothing.

Predelay is smoothed per sample instead, by equation (5).

Morph Rate, Entry Rate, Morph Adapt, Air Motion, Mode and Size’s effect on delay lengths are not smoothed, by design. The first three set speeds rather than positions, so a step in them produces no step in the audio. Size’s effect on damping is applied immediately, because it only changes the decay gains, and a change of gain alters a running tail gradually.

On the very first block after preparation every smoothed value is set to its target directly. A freshly loaded plugin therefore does not spend its first chunks ramping. This is also what makes Morph Depth 0 and Entry Drift 0 exact from the first sample.

Four parameters reach the reverb through a curve from a host value tt in [0,1][0,1] (src/dsp/Parameters.cpp).

parameter real value default tt
Decay 0.2250t0.2 \cdot 250^{\,t} s, 0.2 to 50 s 0.450045, giving 2.4 s
Predelay 200t2200\,t^2 ms 0.316228, giving 20 ms
Low Cut 2025t20 \cdot 25^{\,t} Hz, 20 to 500 Hz 0.430677, giving 80 Hz
Morph Rate 0 for t<0.02t < 0.02 , else 0.002250(t0.02)/0.980.002 \cdot 250^{(t-0.02)/0.98} Hz 0.428690, giving 0.02 Hz

Sample rate dependences

Every time constant expressed in seconds or hertz is converted with fsf_s and therefore sounds the same at any rate. That covers the low cut and damping corners of equation (3), the pre-delay target, and the diffuser delays of equation (18). It covers the envelope follower of equation (9), the room tone’s 8 kHz roll-off and its 0.15 Hz wander. It covers the crossfade and Entry Drift fades, the morph phase increment of equation (46), and the decay gains of equation (30).

What is expressed in samples changes with the rate instead. The delay tables of equations (15) and (29) are sample counts for 48 kHz and are used as they are at any rate. At 96 kHz every early and late delay is half as long in time. The Hall’s 20 to 95 ms become 10 to 47.5 ms, the echo pattern compresses by two, and the room sounds smaller in that respect. The decay time is unaffected, because equation (30) compensates.

The per-chunk smoothing of equation (56) moves 35 per cent per 64 samples, so its 14.7 ms settling at 48 kHz is 7.3 ms at 96 kHz. The pre-delay glide of equation (5) has a time constant of 2000 samples, which is 41.7 ms at 48 kHz and 20.8 ms at 96 kHz.

The pink-noise coefficients of equation (39) are fixed pole radii, so the room tone’s spectrum also shifts up in proportion to the rate.

The plugin boundary

The three shells, for CLAP, AUv2 and AUv3, all present the same contract and hand the reverb the same two arrays.

Audio is stereo in, stereo out, 32-bit float and non-interleaved. The CLAP port is declared stereo and a block with fewer than two channels is refused. The AUv2 accepts only a two-channel float format. The AUv3 requires two output buffers and zeroes its input when none is connected or the source declares silence. A mono source has to be presented as stereo by the host.

Reported latency is zero. Pre-delay is a delay the user asked for, not one the host is told to compensate.

The reported tail is 1.51.5 times Decay plus Predelay seconds, so an offline bounce keeps rendering after the last note.

Every process call runs with the processor’s flush-to-zero and denormals-are-zero flags set and restores them on exit (src/util/DenormalGuard.hpp). A decaying tail otherwise reaches numbers so small that the processor slows down handling them, and with the flags set such numbers become exactly 0. The test at tests/test_realtime_safety.cpp confirms a 20-second tail contains none.

The AUv2 answers the host’s bypass property with a 5 ms linear crossfade between its output and the untouched input, then clears the reverb. That path is outside the reverb and independent of the Enabled parameter. The CLAP and AUv3 shells have no host-bypass path of their own, and Enabled is the switch.

Nothing else in the shells touches audio. Parameter events are converted through the curves above and passed to the setters between slices of the block.

The algorithm in order

Per block of nbn_{\mathrm b} samples, in chunks of m64m \le 64 , the reverb does the following once per chunk.

  1. Glide the smoothed parameters by equation (56), and compute GdryG_{\mathrm{dry}} , GwetG_{\mathrm{wet}} , GtrimG_{\mathrm{trim}} and δ\delta .
  2. If Enabled has settled at off, clear the room once, output GtrimxG_{\mathrm{trim}} x , and skip the rest.
  3. For each sample of the chunk, run the front end, equations (1) to (9), producing dd , pp and the last-sample σ\sigma of equation (10).
  4. Advance the late and early morph phases by equation (46) with σ\sigma . Compute rr and uu by equation (47) and hand them to the active engine, and to the standby one when fading.
  5. Run the early stage on pp , equations (11) to (13), giving ee .
  6. Run the diffusion chain on ee , equations (16) to (19), giving ff .
  7. Run the late field on ff , equations (21) to (27), giving ww . Entry Drift’s trajectory advances by equation (46) with σ=1\sigma = 1 at the start of the engine’s block.
  8. Repeat 5 to 7 for the standby engine if a crossfade is running.
  9. For each sample, apply equations (51) to (55), producing yy .
  10. Update the wet peak follower and raise the meters.

Inside the late field at step 7, each sample runs the same nine operations for six lines: read, tap, damp, limit, add tone, reflect, inject, write, advance.

Measurements against the equations

The repository’s offline tool renders a unit impulse through the reverb with Mix at 1 and Air Level at 0 and measures the result. Predictions below come from the equations above and measurements from the tool. The tool’s band measurements use one-pole probes at 150 Hz, at 500 to 2200 Hz and at 7 kHz. Those sit inside the shelf transitions rather than at the band extremes. That is why the low and high measurements land between the mid target and the band target.

The first sample a listener hears, at the Hall defaults with Predelay 20 ms, Early Level 0.35, Width 0.8 and Low Cut 80 Hz:

quantity predicted measured
time of first wet sample 20ms+385/48=28.0220\,\text{ms} + 385 / 48 = 28.02 ms 28.0 ms
its amplitude 0.351312(1clc)1+0.82=0.09000.35 \cdot \tfrac{1}{\sqrt3} \cdot \tfrac12 (1 - c_{\mathrm{lc}}) \cdot \tfrac{1 + 0.8}{2} = 0.0900 0.0900
same, Size 0 20+192.5/48=24.020 + 192.5/48 = 24.0 ms 24.0 ms
same, Size 1 20+577.5/48=32.020 + 577.5/48 = 32.0 ms 32.0 ms

The amplitude is the early tap 1/31/\sqrt3 of line 0, which holds half the left input after the low cut’s first-sample gain 1clc1 - c_{\mathrm{lc}} . That is scaled by Early Level, then narrowed by Width through equation (52) with the right channel still silent. The late field’s first contribution arrives 961 samples later and is far smaller, because the diffuser’s direct term is 0.780.7^8 .

Decay times follow. The mid band target equals Decay, the low band is 1.31.3 times it, and the high band is (0.90.35Size)(0.9 - 0.35\,\textit{Size}) times it at tilt 0.

setting band targets low, mid, high (s) measured (s)
Hall, 2.4 s, Size 0.5 3.12, 2.40, 1.74 2.90, 2.27, 1.93
Hall, 2.4 s, Size 0 3.12, 2.40, 2.16 2.95, 2.36, 2.24
Hall, 2.4 s, Size 1 3.12, 2.40, 1.32 2.93, 2.20, 1.60
Cathedral, 6 s, Size 0.5 7.80, 6.00, 4.35 7.36, 5.65, 4.80
Hall, 2.4 s, Tilt +1 2.34, 2.40, 2.78 2.35, 2.53, 2.65
Hall, 2.4 s, Tilt −1 3.90, 2.40, 0.70 3.48, 2.24, 1.09
2026-09-05T01:28:23.786730 image/svg+xml Matplotlib v3.11.1, https://matplotlib.org/
Every band at every setting of the table above, plotted against the decay time equation (30) asks for. The mid band lands between 8.3 per cent short and 5.4 per cent long. The low band lands between 10.8 per cent short and 0.4 per cent long. The high band is the outlier, reading up to 55.7 per cent long, because the 7 kHz probe sits inside the shelf transition rather than above it.

The mid band lands within about 8 per cent of Decay in every case, a little short in all but the Tilt +1 row. That is inside the tolerance the repository’s own test sets. The ordering of low above mid above high holds in every untilted case. The high band at Size 1 measures 1.60 s against a 1.32 s target. The 7 kHz probe lies where the high shelf of equation (34) is only part-way to its full effect, exactly as equation (37) predicts.

Normalised echo density in 20 ms windows from 0 to 120 ms, where 1 means fully dense:

Diffusion density per window time to reach 0.9
1.0 0.00, 0.01, 0.21, 0.72, 0.86, 0.91 100 ms
0.0 0.00, 0.01, 0.02, 0.04, 0.05, 0.04 beyond 120 ms

Freezing the late trajectory at eight phases and measuring the broadband decay time gives 2.05 to 2.06 s at the Hall defaults, a spread of 0 per cent. At Tilt 1-1 it gives 2.02 to 2.08 s, a spread of 3 per cent. The moving matrix does not move the decay, as the section on the trajectory claims.

Table of constants

constant value where
chunk length 64 samples Reverb.cpp
parameter smoothing per chunk 0.35 Reverb.cpp
pre-delay glide per sample 0.0005 Reverb.cpp
pre-delay range 0 to 200 ms Reverb.cpp
low cut range 20 to 500 Hz Reverb.cpp
envelope attack and release 50 ms and 1.2 s AdaptiveMorphRate.hpp
envelope level scale 4, full effect at 0.25 AdaptiveMorphRate.hpp
early lines and feedback 3 and 0.4 EarlyReflections.hpp
early and late tap gain 1/31/\sqrt3 EarlyReflections.cpp, FDN.cpp
injection gain 0.5 EarlyReflections.cpp, FDN.cpp
diffuser stages and coefficient 8 per channel, 0.7 AllpassDiffuser.hpp
diffusion floor 0 below 4 s, 0.5 at 8 s Reverb.cpp
late lines 6 FDN.hpp
damping corners 250 Hz, 4000 Hz FDN.cpp
low multiplier 1.3(10.25τ)1.3(1 - 0.25\tau) , clamp 0.6 to 2.0 FDN.cpp
high multiplier (0.90.35Size)(1+0.6τ)(0.9 - 0.35\,\textit{Size})(1 + 0.6\tau) , clamp 0.15 to 1.4 FDN.cpp
soft-limit threshold 3, ceiling 2 FeedbackNonlinearity.hpp
room tone level 10410^{-4} at Air Level 0.3 RoomTone.hpp
room tone roll-off and oscillator 8 kHz, 0.15 Hz, depth 0.9 RoomTone.hpp
morph anchors 6 late, 4 early, 4 entry Reverb.hpp, InputMatrixMorph.hpp
loop spread by mode 0.35, 0.50, 0.25, 0.80 MatrixMorph.hpp
early morph rate and depth 13\tfrac13 , 0.7 times Reverb.cpp
entry rate and depth 0.08+0.25x0.08 + 0.25\,x Hz, 0.4 times Reverb.cpp, InputMatrixMorph.hpp
entry activation fade 30 ms FDN.cpp
engine crossfade 40 ms, linear Reverb.cpp
reconfigure threshold scale change above 0.03 Reverb.cpp
silent-switch threshold wet peak below 10510^{-5} Reverb.cpp
wet peak follower 0.99 per chunk Reverb.cpp
trim range ±24\pm 24 dB Reverb.cpp
off threshold η104\eta \le 10^{-4} Reverb.cpp
line buffers 32768 late, 4096 early FDN.cpp, EarlyReflections.cpp
reported tail 1.5T1.5\,T plus pre-delay Plugin.cpp

Source map

stage file
block loop, front end, mix, engines src/dsp/Reverb.hpp, src/dsp/Reverb.cpp
late field loop src/dsp/FDN.hpp, src/dsp/FDN.cpp
per-line damping src/dsp/MultibandDamping.hpp, .cpp
soft limit src/dsp/FeedbackNonlinearity.hpp
room tone src/dsp/RoomTone.hpp
reflection matrix src/dsp/HouseholderMatrix.hpp
matrix trajectory src/dsp/MatrixMorph.hpp
Entry Drift src/dsp/InputMatrixMorph.hpp
velocity gate src/dsp/AdaptiveMorphRate.hpp
early stage src/dsp/EarlyReflections.hpp, .cpp
diffusion src/dsp/AllpassDiffuser.hpp
delay lines src/dsp/DelayLine.hpp, src/dsp/InterpolatedDelayLine.hpp
delay tables src/dsp/tables/DelaySets.hpp
parameter curves and defaults src/dsp/Parameters.hpp, .cpp
denormal guard src/util/DenormalGuard.hpp
shells src/clap/Plugin.cpp, src/auv2/Component.cpp, src/auv3/AudioUnit.mm, src/auv3/RenderCore.hpp
measurements tools/offline/main.cpp

Figure sources

One script draws the figure. It plots the measured decay times of the table above against the ones equation (30) asks for. It also prints the spread it draws, so the caption and the plot cannot drift apart. The measurements are typed in from that table rather than recomputed, because they come from the compiled plugin. It needs NumPy and Matplotlib.

decay.py
uv run --with numpy --with matplotlib python3 decay.py

  1. For a delay between 1 and 2 samples, D=1D = 1 . The newest neighbour y1=a[n]y_{-1} = a[n] is then read from the buffer slot that has not yet been written this sample. It therefore holds whatever was stored there one buffer length earlier. The pre-delay buffer holds at least 0.21fs+80.21 f_s + 8 samples, rounded up to a power of two. The stale value only enters through c1c_1 , c2c_2 and c3c_3 , scaled by μ\mu , for delays under two samples, which is 0.04 ms. It is recorded for completeness and is not audible. ↩︎

Tags