Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
diffusion.hpp
Go to the documentation of this file.
1#pragma once
2
3// Diffusion character: a Schroeder allpass chain INSIDE the feedback loop.
4//
5// The placement is the whole point. A diffuser in series with the output would
6// smear the first repeat and every later one identically; in the loop, repeat k
7// has been through the chain k times, so the echo train melts progressively
8// from a discrete slap into a soft cloud that approaches reverb texture as it
9// recirculates.
10//
11// The base network is Dattorro's published input diffuser (Effect Design Part
12// 1, Table 1). Two things keep it from ringing metallically: each stage's
13// delay is rounded to an ODD sample count, so no two stages share a factor and
14// their comb nulls never align; and two of the four stages are slowly
15// modulated (Dattorro's own recommendation) through the same Lagrange
16// fractional read the main delay line uses.
17
20
21#include <algorithm>
22#include <array>
23#include <cmath>
24#include <cstddef>
25
27
30public:
31 void prepare(std::size_t capacity_samples) { line_.prepare(capacity_samples); }
32 void reset() { line_.reset(); }
33
34 double process(double x, double delay_samples, double gain) noexcept {
35 const double delayed = line_.read(delay_samples);
36 const double y = -gain * x + delayed;
37 line_.push(x + gain * y);
38 return y;
39 }
40
41 double max_delay() const noexcept { return line_.max_delay(); }
42
43private:
45};
46
64public:
65 void prepare(double fs) {
66 sample_rate_ = fs;
67 delay_slew_coefficient_ =
68 std::exp(-1.0 / (kDiffusionTankDelaySlewMs * 0.001 * sample_rate_));
69 const double longest = std::max({kDiffusionTankDelayAMs[1], kDiffusionTankDelayBMs[1],
71 const auto capacity = static_cast<std::size_t>(
72 std::ceil(longest * kDiffusionTankSizeScale.back() * 0.001 * fs +
73 kDiffusionTankModDepth.back()) + 8.0);
74 for (auto& line : delays_) line.prepare(capacity);
75 for (auto& ap : allpasses_) ap.prepare(capacity);
76 for (auto& g : low_guard_) g.set_cutoff(kDiffusionTankLowGuardHz, fs);
77 }
78
79 void reset() {
80 for (auto& line : delays_) line.reset();
81 for (auto& ap : allpasses_) ap.reset();
82 for (auto& d : damping_) d.reset();
83 for (auto& g : low_guard_) g.reset();
84 feedback_ = {0.0, 0.0};
85 mod_phase_ = {0.0, 0.0};
86 snap_delays_on_update_ = true;
87 }
88
89 void update(double character_amount) noexcept {
90 const double size =
92 decay_ = interpolate_knots(kDiffusionTankAxis, kDiffusionTankDecay, character_amount);
94 mod_depth_ =
96 const double damp_hz =
98 for (auto& d : damping_) d.set_cutoff(damp_hz, sample_rate_);
99
100 // Normalize the tank to roughly unity ENERGY before it is heard.
101 //
102 // Stability does not need this: the figure of eight recirculates
103 // through feedback_ unscaled, so decay_ < 1 already guarantees the
104 // tank converges, and output_scale_ affects level only — the tank is
105 // never in the delay's feedback path. Dividing by the settled DC gain
106 // (1 - decay) would assume steady state, but a repeat is a transient
107 // that never settles, and that estimate over-attenuated by the whole
108 // decay factor (a measured 17 dB loss at full character). A
109 // recirculating network with per-pass gain g carries impulse energy
110 // ~1/(1 - g^2), so the unity-energy scale is sqrt(1 - g^2); the tap
111 // sum adds its own energy on top, divided back out the same way.
112 output_scale_ = kTankHeadroom *
113 std::sqrt(std::max(1.0 - decay_ * decay_, 1e-4)) /
114 std::sqrt(1.0 + kTapGain * kTapGain *
115 static_cast<double>(kTapCount));
116
117 const double to_samples = 0.001 * sample_rate_ * size;
118 delay_len_target_[0] = kDiffusionTankDelayAMs[0] * to_samples;
119 delay_len_target_[1] = kDiffusionTankDelayAMs[1] * to_samples;
120 delay_len_target_[2] = kDiffusionTankDelayBMs[0] * to_samples;
121 delay_len_target_[3] = kDiffusionTankDelayBMs[1] * to_samples;
122 ap_len_target_[0] = kDiffusionTankApAMs[0] * to_samples;
123 ap_len_target_[1] = kDiffusionTankApAMs[1] * to_samples;
124 ap_len_target_[2] = kDiffusionTankApBMs[0] * to_samples;
125 ap_len_target_[3] = kDiffusionTankApBMs[1] * to_samples;
126 if (snap_delays_on_update_) {
127 delay_len_ = delay_len_target_;
128 ap_len_ = ap_len_target_;
129 snap_delays_on_update_ = false;
130 }
131 }
132
136 bool active() const noexcept { return mix_ > 0.0; }
137 double mix() const noexcept { return mix_; }
138
139 double process(double x, double decorrelation) noexcept {
140 advance_delay_slew();
141 // Both branches read the OTHER branch's previous output, which is what
142 // closes the figure of eight without a zero-delay loop.
143 const double in_a = x + feedback_[1] * decay_;
144 const double in_b = x + feedback_[0] * decay_;
145
146 double taps = 0.0;
147 const double a = run_branch(0, in_a, decorrelation, taps);
148 const double b = run_branch(1, in_b, decorrelation, taps);
149 feedback_[0] = a;
150 feedback_[1] = b;
151
152 // The branch ends carry the tail; the scattered taps carry the density.
153 return output_scale_ * (0.5 * (a + b) + kTapGain * taps);
154 }
155
156 void tick_modulation() noexcept {
157 for (std::size_t m = 0; m < mod_phase_.size(); ++m) {
158 mod_phase_[m] += kDiffusionTankModRatesHz[m] / sample_rate_;
159 if (mod_phase_[m] >= 1.0) mod_phase_[m] -= 1.0;
160 }
161 }
162
163private:
164 static constexpr double kTapGain = 0.30;
165 static constexpr std::size_t kTapCount = 4; // two per branch
166 static constexpr double kTankHeadroom = 1.0;
167
168 void advance_delay_slew() noexcept {
169 for (std::size_t i = 0; i < delay_len_.size(); ++i) {
170 delay_len_[i] = delay_len_target_[i] +
171 delay_slew_coefficient_ * (delay_len_[i] - delay_len_target_[i]);
172 ap_len_[i] =
173 ap_len_target_[i] + delay_slew_coefficient_ * (ap_len_[i] - ap_len_target_[i]);
174 }
175 }
176
179 double run_branch(std::size_t branch, double x, double decorrelation,
180 double& taps) noexcept {
181 const std::size_t i0 = branch * 2u;
182 const std::size_t i1 = i0 + 1u;
183
184 const double sweep = mod_depth_ * decorrelation *
185 std::sin(2.0 * kPi * mod_phase_[branch]);
186 double y = allpasses_[i0].process(
187 x, clamp_delay(ap_len_[i0] + sweep, allpasses_[i0].max_delay()),
189
190 delays_[i0].push(y);
191 const double len0 = clamp_delay(delay_len_[i0], delays_[i0].max_delay());
192 for (std::size_t t = 0; t < kDiffusionTankTapFraction.size(); ++t)
193 taps += delays_[i0].read(clamp_delay(len0 * kDiffusionTankTapFraction[t],
194 delays_[i0].max_delay()));
195 y = delays_[i0].read(len0);
196
197 y = damping_[branch].lowpass(y);
198 // The other half of the damping: every pass sheds low end too, so no
199 // sub-100 Hz mode can recirculate at unity while the rest decays.
200 y = low_guard_[branch].highpass(y);
201 y = allpasses_[i1].process(y, clamp_delay(ap_len_[i1], allpasses_[i1].max_delay()),
203 delays_[i1].push(y);
204 return delays_[i1].read(clamp_delay(delay_len_[i1], delays_[i1].max_delay()));
205 }
206
207 static double clamp_delay(double d, double max_delay) noexcept {
208 return std::clamp(d, 1.0, max_delay);
209 }
210
211 std::array<FractionalDelayLine, 4> delays_{};
212 std::array<ModulatedAllpass, 4> allpasses_{};
213 std::array<OnePole, 2> damping_{};
214 std::array<OnePole, 2> low_guard_{};
215 std::array<double, 4> delay_len_ = {1.0, 1.0, 1.0, 1.0};
216 std::array<double, 4> ap_len_ = {1.0, 1.0, 1.0, 1.0};
217 std::array<double, 4> delay_len_target_ = {1.0, 1.0, 1.0, 1.0};
218 std::array<double, 4> ap_len_target_ = {1.0, 1.0, 1.0, 1.0};
219 std::array<double, 2> feedback_ = {0.0, 0.0};
220 std::array<double, 2> mod_phase_ = {0.0, 0.0};
221 double sample_rate_ = 48000.0;
222 double decay_ = 0.0;
223 double mix_ = 0.0;
224 double output_scale_ = 0.0;
225 double mod_depth_ = 0.0;
226 double delay_slew_coefficient_ = 0.0;
227 bool snap_delays_on_update_ = true;
228};
229
232public:
233 void prepare(double fs) {
234 sample_rate_ = fs;
235 tank_.prepare(fs);
236 for (std::size_t i = 0; i < kStageCount; ++i) {
237 // Sized for the largest sizeScale knot plus the modulation swing
238 // and the Lagrange kernel's reach.
239 const double max_ms = kDiffusionStageMs[i] * kDiffusionSizeScale.back();
240 const auto capacity = static_cast<std::size_t>(
241 std::ceil(max_ms * 0.001 * fs + kDiffusionModDepthSamples) + 8.0);
242 stages_[i].prepare(capacity);
243 }
244 }
245
246 void reset() {
247 for (auto& s : stages_) s.reset();
248 tank_.reset();
249 mod_phase_ = {0.0, 0.0};
250 }
251
255 void update(double character_amount) noexcept {
256 const double size_scale =
258 const double gain_scale =
260 for (std::size_t i = 0; i < kStageCount; ++i) {
261 const double samples = kDiffusionStageMs[i] * 0.001 * sample_rate_ * size_scale;
262 delays_[i] = round_to_odd(samples);
263 gains_[i] = std::min(kDiffusionStageGain[i] * gain_scale, kDiffusionGainMax);
264 }
265 tank_.update(character_amount);
266 }
267
272 double process_diffuser(double x, double decorrelation) noexcept {
273 for (std::size_t i = 0; i < kStageCount; ++i) {
274 double delay = delays_[i];
275 if (i == 0 || i == 2) {
276 const std::size_t m = (i == 0) ? 0u : 1u;
277 delay += kDiffusionModDepthSamples * decorrelation *
278 std::sin(2.0 * kPi * mod_phase_[m]);
279 }
280 delay = std::clamp(delay, 1.0, stages_[i].max_delay());
281 x = stages_[i].process(x, delay, gains_[i]);
282 }
283 return x;
284 }
285
294 double process_cloud(double x, double decorrelation) noexcept {
295 if (!tank_.active()) return x;
296 const double cloud = tank_.process(x, decorrelation);
297 return x + tank_.mix() * (cloud - x);
298 }
299
301 double process(double x, double decorrelation) noexcept {
302 return process_cloud(process_diffuser(x, decorrelation), decorrelation);
303 }
304
306 void tick_modulation() noexcept {
307 tank_.tick_modulation();
308 for (std::size_t m = 0; m < mod_phase_.size(); ++m) {
309 mod_phase_[m] += kDiffusionModRatesHz[m] / sample_rate_;
310 if (mod_phase_[m] >= 1.0) mod_phase_[m] -= 1.0;
311 }
312 }
313
314private:
315 static constexpr std::size_t kStageCount = 4;
316
317 static double round_to_odd(double samples) noexcept {
318 auto n = static_cast<long long>(std::llround(samples));
319 if (n < 3) n = 3;
320 if ((n & 1LL) == 0LL) n += 1;
321 return static_cast<double>(n);
322 }
323
324 ReverbTank tank_{};
325 std::array<ModulatedAllpass, kStageCount> stages_{};
326 std::array<double, kStageCount> delays_ = {1.0, 1.0, 1.0, 1.0};
327 std::array<double, kStageCount> gains_ = {0.0, 0.0, 0.0, 0.0};
328 std::array<double, 2> mod_phase_ = {0.0, 0.0};
329 double sample_rate_ = 48000.0;
330};
331
332} // namespace pulp::signal::chardelay
The four-stage chain for one channel.
Definition diffusion.hpp:231
double process_diffuser(double x, double decorrelation) noexcept
The bare allpass diffuser — the only part safe to RECIRCULATE.
Definition diffusion.hpp:272
void prepare(double fs)
Definition diffusion.hpp:233
double process(double x, double decorrelation) noexcept
One-pass smear for the cross-character output blend: diffuser + cloud.
Definition diffusion.hpp:301
void reset()
Definition diffusion.hpp:246
void update(double character_amount) noexcept
Recompute stage delays and gains for a character amount.
Definition diffusion.hpp:255
void tick_modulation() noexcept
Advance the two anti-metallic LFOs by one sample.
Definition diffusion.hpp:306
double process_cloud(double x, double decorrelation) noexcept
The reverb tank, crossfaded in — OUTPUT ONLY, never in the loop.
Definition diffusion.hpp:294
One modulated Schroeder allpass: y[n] = −g·x[n] + v[n−d], v[n] = x[n] + g·y[n].
Definition diffusion.hpp:29
void reset()
Definition diffusion.hpp:32
double max_delay() const noexcept
Definition diffusion.hpp:41
double process(double x, double delay_samples, double gain) noexcept
Definition diffusion.hpp:34
void prepare(std::size_t capacity_samples)
Definition diffusion.hpp:31
Dattorro's figure-of-eight reverb tank, one per channel.
Definition diffusion.hpp:63
void prepare(double fs)
Definition diffusion.hpp:65
double process(double x, double decorrelation) noexcept
Definition diffusion.hpp:139
bool active() const noexcept
Zero mix means the tank contributes nothing at all — the character is then the bare allpass diffuser,...
Definition diffusion.hpp:136
void update(double character_amount) noexcept
Definition diffusion.hpp:89
void tick_modulation() noexcept
Definition diffusion.hpp:156
double mix() const noexcept
Definition diffusion.hpp:137
void reset()
Definition diffusion.hpp:79
Definition bbd.hpp:36
constexpr std::array< double, 4 > kDiffusionStageGain
Definition tables.hpp:248
constexpr std::array< double, 3 > kDiffusionTankAxis
Definition tables.hpp:279
constexpr std::array< double, 3 > kDiffusionGainScale
Definition tables.hpp:252
constexpr std::array< double, 3 > kDiffusionTankModDepth
Tank modulation — this is what makes the cloud granular rather than static.
Definition tables.hpp:311
constexpr std::array< double, 3 > kDiffusionTankSizeScale
Tank size scaler and recirculation decay by character amount.
Definition tables.hpp:295
constexpr double kDiffusionGainMax
Stability / metallicity guard.
Definition tables.hpp:256
constexpr std::array< double, 2 > kDiffusionTankTapFraction
Extra scattered taps read at their own modulated offsets, as a fraction of the branch delay.
Definition tables.hpp:315
constexpr std::array< double, 3 > kDiffusionTankMix
How much of the character's output is the tank rather than the bare diffuser.
Definition tables.hpp:301
constexpr double kPi
Definition primitives.hpp:38
constexpr std::array< double, 4 > kDiffusionStageMs
Definition tables.hpp:246
constexpr double kDiffusionTankDelaySlewMs
Live character automation moves tank taps toward their new positions over a short continuous glide in...
Definition tables.hpp:326
constexpr std::array< double, 2 > kDiffusionTankApAMs
The two decay-diffuser allpasses per branch. [design parameters].
Definition tables.hpp:286
constexpr std::array< double, 3 > kDiffusionTankDampHz
In-tank damping.
Definition tables.hpp:305
constexpr std::array< double, 2 > kDiffusionTankApBMs
Definition tables.hpp:287
constexpr double kDiffusionTankApGain1
Definition tables.hpp:288
double interpolate_knots(const std::array< double, N > &axis, const std::array< double, N > &values, double c) noexcept
Definition primitives.hpp:125
constexpr std::array< double, 2 > kDiffusionTankDelayBMs
Definition tables.hpp:284
constexpr double kDiffusionTankApGain2
Definition tables.hpp:289
constexpr std::array< double, 2 > kDiffusionTankModRatesHz
Definition tables.hpp:312
constexpr std::array< double, 3 > kDiffusionSizeScale
Definition tables.hpp:251
constexpr std::array< double, 2 > kDiffusionModRatesHz
Definition tables.hpp:261
constexpr std::array< double, 3 > kDiffusionAxis
Definition tables.hpp:250
constexpr double kDiffusionTankLowGuardHz
In-tank low cut.
Definition tables.hpp:322
constexpr double kDiffusionModDepthSamples
Anti-metallic modulation of stages 1 and 3 (Dattorro modulates his diffusers).
Definition tables.hpp:260
constexpr std::array< double, 3 > kDiffusionTankDecay
Definition tables.hpp:296
constexpr std::array< double, 2 > kDiffusionTankDelayAMs
Branch delays in ms, mutually prime-ish so their comb nulls never align.
Definition tables.hpp:283
@ delay
Definition envelope.hpp:57
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20