Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
plate_reverb.hpp
Go to the documentation of this file.
1// sbl/dsp/modules/plate_reverb.hpp — Scalable Dattorro reverb (Audio Stack — Modules)
2//
3// Classic Dattorro plate topology, templated on a scale factor:
4// Input → Pre-delay → Bandwidth LP → 4x Input Diffusion (allpass)
5// → Cross-fed Tank (2 halves, each: AP → long delay → damp → AP → long delay)
6// → Multi-tap stereo output
7//
8// Based on Jon Dattorro, "Effect Design Part 1" (JAES, 1997).
9//
10// ScalePercent controls the acoustic space size. All delay lengths and output
11// tap positions are multiplied by ScalePercent/100, preserving the Dattorro
12// geometry (echo density pattern) while changing the perceived room size:
13// - PlateReverb = AlgoReverb<100> (~161 KB, tight plate character)
14//
15// Base delay values are scaled from Dattorro's 29.76 kHz reference rate to
16// 48 kHz (factor 50/31), then further scaled by ScalePercent.
17//
18// Tank modulation uses a 4-phase quadrature oscillator (zero-cost, no LUT)
19// to modulate all 4 tank delay lines with 90° offsets. Modulation depth
20// scales with tank size to keep comb teeth dispersed at all scales.
21//
22// Parameter smoothing: All parameters are smoothed per-sample via
23// ParameterSmoother IIR with category-appropriate settling times (structural
24// 50ms, timbral 20ms, gain 15ms) to prevent pops/clicks from abrupt
25// parameter changes. Predelay uses ModulatedDelayLine for interpolated
26// read-position gliding.
27//
28// Float-domain processing — STM32H7 (Cortex-M7) has hardware FPU.
29//
30// Usage:
31// // Plate (original 1x size)
32// float pool[PlateReverb::BUFFER_SIZE] = {};
33// PlateReverb reverb;
34// reverb.init(pool, PlateReverb::BUFFER_SIZE);
35//
36// // Hall (3x larger tank — place in SDRAM)
37
38#ifndef SBL_DSP_MODULES_PLATE_REVERB_HPP_
39#define SBL_DSP_MODULES_PLATE_REVERB_HPP_
40
41#include <cmath>
42#include <cstdint>
43#include <cstring>
44
52
53namespace sbl::dsp::modules {
54
55// ============================================================================
56// Base Dattorro delay values at 48 kHz (scaled from 29.76 kHz reference)
57// Scale factor: 48000 / 29760 = 50/31 ≈ 1.6129
58// ============================================================================
59
60namespace reverb {
61
62// Input diffusion allpass delays (scaled from 142, 107, 379, 277)
63constexpr uint32_t BASE_INPUT_AP1 = 229;
64constexpr uint32_t BASE_INPUT_AP2 = 173;
65constexpr uint32_t BASE_INPUT_AP3 = 611;
66constexpr uint32_t BASE_INPUT_AP4 = 447;
67
68// Tank left: AP1 → Delay1 → (damp) → AP2 → Delay2
69// Scaled from 672, 4453, 1800, 3720.
70constexpr uint32_t BASE_TANK_L_AP1 = 1084;
71constexpr uint32_t BASE_TANK_L_DELAY1 = 7182;
72constexpr uint32_t BASE_TANK_L_AP2 = 2903;
73constexpr uint32_t BASE_TANK_L_DELAY2 = 6000;
74
75// Tank right: AP1 → Delay1 → (damp) → AP2 → Delay2
76// Scaled from 908, 4217, 2656, 3163.
77constexpr uint32_t BASE_TANK_R_AP1 = 1465;
78constexpr uint32_t BASE_TANK_R_DELAY1 = 6802;
79constexpr uint32_t BASE_TANK_R_AP2 = 4284;
80constexpr uint32_t BASE_TANK_R_DELAY2 = 5102;
81
82// Pre-delay (max 100ms @ 48kHz at 1x scale)
83constexpr uint32_t BASE_PREDELAY_MAX = 4800;
84
85// LFO modulation parameters (base values at 1x scale)
86constexpr float BASE_MOD_RATE_HZ = 1.5f;
87constexpr float BASE_MOD_DEPTH = 16.0f;
88
89// Output tap positions (scaled from 29.76 kHz to 48 kHz)
90constexpr uint32_t BASE_TAP_L_AP1_A = 429;
91constexpr uint32_t BASE_TAP_L_DL1_A = 569;
92constexpr uint32_t BASE_TAP_L_DL1_B = 5850;
93constexpr uint32_t BASE_TAP_L_AP2_A = 2161;
94constexpr uint32_t BASE_TAP_L_DL2_A = 2258;
95constexpr uint32_t BASE_TAP_L_DL2_B = 4302;
96
97constexpr uint32_t BASE_TAP_R_AP1_A = 429;
98constexpr uint32_t BASE_TAP_R_DL1_A = 569;
99constexpr uint32_t BASE_TAP_R_DL1_B = 5592;
100constexpr uint32_t BASE_TAP_R_AP2_A = 1129;
101constexpr uint32_t BASE_TAP_R_DL2_A = 1981;
102constexpr uint32_t BASE_TAP_R_DL2_B = 4105;
103
104// Input diffusion feedback coefficients (Dattorro standard)
105constexpr float INPUT_DIFFUSION1 = 0.75f; // AP1, AP2
106constexpr float INPUT_DIFFUSION2 = 0.625f; // AP3, AP4
107
108// Tank diffusion — first AP is fixed, second AP tracks decay
109constexpr float DECAY_DIFFUSION1 = 0.7f;
110
111// Decay diffusion 2: dd2 = BASE * (1 - SCALE * decay)
112// Range: 0.5 (zero decay) → ~0.2 (full decay). From Dattorro (1997).
113constexpr float DECAY_DIFFUSION2_BASE = 0.5f;
114constexpr float DECAY_DIFFUSION2_SCALE = 0.6f;
115
116// Input bandwidth LP frequency (Dattorro "bandwidth" parameter)
117// Lower = warmer/darker reverb, higher = brighter/more transparent
118constexpr float DEFAULT_BANDWIDTH_HZ = 10000.0f;
119
120// Multi-tap output scaling: 7 taps sum to ~10x amplitude at unity decay.
121// Normalizes the mix bus to approximately [-1, 1].
122constexpr float TAP_SUM_SCALE = 0.10f;
123
124} // namespace reverb
125
126// Forward declaration for friend access
127struct AlgoReverbTestAccess;
128
129// ============================================================================
130// AlgoReverb<ScalePercent> — Dattorro reverb with configurable space size
131// ============================================================================
132
133template<uint32_t ScalePercent = 100>
135 // Scale a base delay value by the configured percentage
136 static constexpr uint32_t S(uint32_t base) {
137 return base * ScalePercent / 100;
138 }
139
140 // Modulation depth scales with tank size
141 static constexpr float MOD_DEPTH =
142 reverb::BASE_MOD_DEPTH * static_cast<float>(ScalePercent) / 100.0f;
143 static constexpr uint32_t MOD_MARGIN =
144 static_cast<uint32_t>(MOD_DEPTH) + 2;
145
146 // Scaled delay values (compile-time constants)
147 static constexpr uint32_t INPUT_AP1 = S(reverb::BASE_INPUT_AP1);
148 static constexpr uint32_t INPUT_AP2 = S(reverb::BASE_INPUT_AP2);
149 static constexpr uint32_t INPUT_AP3 = S(reverb::BASE_INPUT_AP3);
150 static constexpr uint32_t INPUT_AP4 = S(reverb::BASE_INPUT_AP4);
151
152 static constexpr uint32_t TANK_L_AP1 = S(reverb::BASE_TANK_L_AP1);
153 static constexpr uint32_t TANK_L_DELAY1 = S(reverb::BASE_TANK_L_DELAY1);
154 static constexpr uint32_t TANK_L_AP2 = S(reverb::BASE_TANK_L_AP2);
155 static constexpr uint32_t TANK_L_DELAY2 = S(reverb::BASE_TANK_L_DELAY2);
156
157 static constexpr uint32_t TANK_R_AP1 = S(reverb::BASE_TANK_R_AP1);
158 static constexpr uint32_t TANK_R_DELAY1 = S(reverb::BASE_TANK_R_DELAY1);
159 static constexpr uint32_t TANK_R_AP2 = S(reverb::BASE_TANK_R_AP2);
160 static constexpr uint32_t TANK_R_DELAY2 = S(reverb::BASE_TANK_R_DELAY2);
161
162 static constexpr uint32_t PREDELAY_MAX = S(reverb::BASE_PREDELAY_MAX);
163
164 // Scaled output tap positions
165 static constexpr uint32_t TAP_L_AP1_A = S(reverb::BASE_TAP_L_AP1_A);
166 static constexpr uint32_t TAP_L_DL1_A = S(reverb::BASE_TAP_L_DL1_A);
167 static constexpr uint32_t TAP_L_DL1_B = S(reverb::BASE_TAP_L_DL1_B);
168 static constexpr uint32_t TAP_L_AP2_A = S(reverb::BASE_TAP_L_AP2_A);
169 static constexpr uint32_t TAP_L_DL2_A = S(reverb::BASE_TAP_L_DL2_A);
170 static constexpr uint32_t TAP_L_DL2_B = S(reverb::BASE_TAP_L_DL2_B);
171
172 static constexpr uint32_t TAP_R_AP1_A = S(reverb::BASE_TAP_R_AP1_A);
173 static constexpr uint32_t TAP_R_DL1_A = S(reverb::BASE_TAP_R_DL1_A);
174 static constexpr uint32_t TAP_R_DL1_B = S(reverb::BASE_TAP_R_DL1_B);
175 static constexpr uint32_t TAP_R_AP2_A = S(reverb::BASE_TAP_R_AP2_A);
176 static constexpr uint32_t TAP_R_DL2_A = S(reverb::BASE_TAP_R_DL2_A);
177 static constexpr uint32_t TAP_R_DL2_B = S(reverb::BASE_TAP_R_DL2_B);
178
179 // Signal-unit parameter ranges
180 static constexpr float kMaxDecay = 0.99f;
181 static constexpr float kMinBandwidthHz = 100.0f;
182 static constexpr float kMaxBandwidthHz = 20000.0f;
183 static constexpr float kBandwidthOctaves = 7.644f; // log2(20000/100)
184 static constexpr float kMaxTankHpHz = 500.0f;
185 static constexpr float kMinTankHpFilterHz = 1.0f; // OnePoleHP range min
186 static constexpr float kMaxDiffusion1 = 0.95f;
187 static constexpr float kMaxDiffusion2 = 0.95f * reverb::INPUT_DIFFUSION2
188 / reverb::INPUT_DIFFUSION1; // ≈ 0.7917
189 static constexpr float kMinOutputScale = 0.001f;
190
191public:
192 /// @note All public methods are ISR-safe — bounded computation, no I/O.
193
194 /// Total float buffer size required for init()
195 static constexpr uint32_t BUFFER_SIZE =
196 INPUT_AP1 + INPUT_AP2 + INPUT_AP3 + INPUT_AP4 +
197 TANK_L_AP1 + (TANK_L_DELAY1 + MOD_MARGIN) +
198 TANK_L_AP2 + (TANK_L_DELAY2 + MOD_MARGIN) +
199 TANK_R_AP1 + (TANK_R_DELAY1 + MOD_MARGIN) +
200 TANK_R_AP2 + (TANK_R_DELAY2 + MOD_MARGIN) +
201 PREDELAY_MAX;
202
203 AlgoReverb() = default;
204
205 /**
206 * @brief Initialize with a pre-allocated float buffer pool
207 * @param pool Float buffer pool (must outlive the reverb)
208 * @param pool_size Pool size in floats (must be >= BUFFER_SIZE)
209 */
210 void init(float* pool, uint32_t pool_size) {
211 if (pool_size < BUFFER_SIZE) return;
212
213 // Zero the pool — NOLOAD sections may contain garbage from previous firmware
214 memset(pool, 0, pool_size * sizeof(float));
215
216 float* p = pool;
217
218 predelay_.init(p, PREDELAY_MAX); p += PREDELAY_MAX;
219
220 input_ap_[0].init(p, INPUT_AP1); p += INPUT_AP1;
221 input_ap_[1].init(p, INPUT_AP2); p += INPUT_AP2;
222 input_ap_[2].init(p, INPUT_AP3); p += INPUT_AP3;
223 input_ap_[3].init(p, INPUT_AP4); p += INPUT_AP4;
224
229
230 tank_l_ap1_buf_ = p;
231 tank_l_ap1_.init(p, TANK_L_AP1); p += TANK_L_AP1;
232 tank_l_dl1_buf_ = p;
233 tank_l_dl1_.init(p, TANK_L_DELAY1 + MOD_MARGIN);
234 p += TANK_L_DELAY1 + MOD_MARGIN;
235 tank_l_ap2_buf_ = p;
236 tank_l_ap2_.init(p, TANK_L_AP2); p += TANK_L_AP2;
237 tank_l_dl2_buf_ = p;
238 tank_l_dl2_.init(p, TANK_L_DELAY2 + MOD_MARGIN);
239 p += TANK_L_DELAY2 + MOD_MARGIN;
240
242
243 tank_r_ap1_buf_ = p;
244 tank_r_ap1_.init(p, TANK_R_AP1); p += TANK_R_AP1;
245 tank_r_dl1_buf_ = p;
246 tank_r_dl1_.init(p, TANK_R_DELAY1 + MOD_MARGIN);
247 p += TANK_R_DELAY1 + MOD_MARGIN;
248 tank_r_ap2_buf_ = p;
249 tank_r_ap2_.init(p, TANK_R_AP2); p += TANK_R_AP2;
250 tank_r_dl2_buf_ = p;
251 tank_r_dl2_.init(p, TANK_R_DELAY2 + MOD_MARGIN);
252 p += TANK_R_DELAY2 + MOD_MARGIN;
253
255
256 pool_ = pool;
257 pool_size_ = pool_size;
258
259 reset();
260 }
261
262 // =====================================================================
263 // Signal-unit inputs (_su API)
264 // =====================================================================
265
266 /// Set decay [0,1] → [0, 0.99] linear (structural, 50ms settling)
267 void set_decay_su(float su) {
268 if (su < 0.0f) su = 0.0f;
269 if (su > 1.0f) su = 1.0f;
270 decay_ = su * kMaxDecay;
271 }
272
273 /// Set damping [0,1] → [0, 1.0] linear (structural, 50ms settling)
274 void set_damping_su(float su) {
275 if (su < 0.0f) su = 0.0f;
276 if (su > 1.0f) su = 1.0f;
277 damping_ = su;
278 }
279
280 /// Set pre-delay [0,1] → [0, max] samples linear (structural, 50ms settling)
281 void set_predelay_su(float su) {
282 if (su < 0.0f) su = 0.0f;
283 if (su > 1.0f) su = 1.0f;
284 target_predelay_ = su * static_cast<float>(PREDELAY_MAX - 1);
285 }
286
287 /// Set stereo width [0,1] → [0, 1.0] linear (timbral, 20ms settling)
288 void set_width_su(float su) {
289 if (su < 0.0f) su = 0.0f;
290 if (su > 1.0f) su = 1.0f;
291 width_ = su;
292 }
293
294 /// Set wet/dry mix [0,1] → [0, 1.0] linear (gain, 15ms settling)
295 void set_mix_su(float su) {
296 if (su < 0.0f) su = 0.0f;
297 if (su > 1.0f) su = 1.0f;
298 mix_ = su;
299 }
300
301 /// Set input bandwidth LP [0,1] → [100, 20000] Hz exponential (structural, 50ms)
302 /// Exponential mapping is handled by OnePole::set_cutoff_su() using the
303 /// configured range.
304 void set_bandwidth_su(float su) {
305 if (su < 0.0f) su = 0.0f;
306 if (su > 1.0f) su = 1.0f;
307 bandwidth_su_ = su;
308 }
309
310 /// Set tank HP cutoff [0,1] → [0, 500] Hz (structural, 50ms)
311 /// su = 0 disables the HP filter. OnePoleHP maps [0,1] exponentially
312 /// across the configured range.
313 void set_tank_hp_su(float su) {
314 if (su < 0.0f) su = 0.0f;
315 if (su > 1.0f) su = 1.0f;
316 tank_hp_su_ = su;
317 }
318
319 /// Set input diffusion [0,1] → d1=[0, 0.95], d2=[0, 0.79] (structural, 50ms)
320 /// Preserves the Dattorro d2/d1 ratio (0.625/0.75) across the range.
321 void set_input_diffusion_su(float su) {
322 if (su < 0.0f) su = 0.0f;
323 if (su > 1.0f) su = 1.0f;
324 diffusion_d1_ = su * kMaxDiffusion1;
325 diffusion_d2_ = su * kMaxDiffusion2;
326 }
327
328 /// Set output tap scale [0,1] → [0.001, 1.0] linear (gain, 15ms)
329 void set_output_scale_su(float su) {
330 if (su < 0.0f) su = 0.0f;
331 if (su > 1.0f) su = 1.0f;
332 output_scale_ = kMinOutputScale + su * (1.0f - kMinOutputScale);
333 }
334
335 // =====================================================================
336 // Accessors
337 // =====================================================================
338
339 float decay() const { return decay_; }
340 float damping() const { return damping_; }
341 float mix() const { return mix_; }
342 float width() const { return width_; }
343
344 /// Maximum pre-delay in milliseconds for this scale
345 float max_predelay_ms() const {
346 return static_cast<float>(PREDELAY_MAX) * 1000.0f
348 }
349
350 /**
351 * @brief Process a stereo block in-place (float)
352 */
353 void process(float* left, float* right, uint16_t frames) {
354 // Auto-configure smoothers on first process() call
355 if (!smoothers_configured_) configure_smoothers();
356
357 for (uint16_t i = 0; i < frames; ++i) {
358 // Per-sample parameter smoothing via OnePole
359 float sd = op_decay_.process(decay_);
360 float sdamp = op_damping_.process(damping_);
361 float smix = op_mix_.process(mix_);
362 float swidth = op_width_.process(width_);
363 float spd = op_predelay_.process(target_predelay_);
364 float soscale = op_output_scale_.process(output_scale_);
365
366 // Smooth bandwidth _su and update input LP filter cutoff
367 float sbw_su = op_bandwidth_su_.process(bandwidth_su_);
368 bandwidth_.set_cutoff_su(sbw_su);
369
370 // Smooth tank HP _su
371 float sthp_su = op_tank_hp_su_.process(tank_hp_su_);
372
373 // Smooth input diffusion and update allpass feedback
374 float sd1 = op_diffusion_d1_.process(diffusion_d1_);
375 float sd2 = op_diffusion_d2_.process(diffusion_d2_);
376 input_ap_[0].set_feedback(sd1);
377 input_ap_[1].set_feedback(sd1);
378 input_ap_[2].set_feedback(sd2);
379 input_ap_[3].set_feedback(sd2);
380
381 // Decay diffusion: derive from smoothed decay (avoids discrete
382 // allpass coefficient steps inside the recirculating tank)
384 * (1.0f - reverb::DECAY_DIFFUSION2_SCALE * sd);
385 tank_l_ap2_.set_feedback(dd2);
386 tank_r_ap2_.set_feedback(dd2);
387
388 // Damping: smooth coefficient update
389 float damp_coeff = 1.0f - sdamp;
390 tank_l_damp_.set_coefficient(damp_coeff);
391 tank_r_damp_.set_coefficient(damp_coeff);
392
393 const float wet = smix;
394 const float dry = 1.0f - wet;
395
396 float in_l = left[i];
397 float in_r = right[i];
398
399 float input = (in_l + in_r) * 0.5f;
400
401 // === Pre-delay (via ModulatedDelayLine for smooth gliding) ===
402 float predelayed;
403 if (spd < 1.0f) {
404 predelayed = input;
405 } else {
406 predelayed = predelay_.read(spd);
407 }
408 predelay_.write(input);
409
410 // === Bandwidth limiter (Dattorro input LP) ===
411 float limited = bandwidth_.process(predelayed);
412
413 // === Input diffusion ===
414 float diffused = limited;
415 for (int ap = 0; ap < 4; ++ap) {
416 diffused = input_ap_[ap].process(diffused);
417 }
418
419 // === Quadrature LFO tick (4-phase for all tank delays) ===
420 mod_sin_ += mod_inc_ * mod_cos_;
421 mod_cos_ -= mod_inc_ * mod_sin_;
422 float mod_dl1_l = mod_sin_ * MOD_DEPTH; // 0°
423 float mod_dl2_l = mod_cos_ * MOD_DEPTH; // 90°
424 float mod_dl1_r = -mod_sin_ * MOD_DEPTH; // 180°
425 float mod_dl2_r = -mod_cos_ * MOD_DEPTH; // 270°
426
427 // === Tank processing (uses smoothed decay for gain) ===
428 float fb_l = tank_r_out_;
429 float fb_r = tank_l_out_;
430 if (sthp_su > 0.0f) {
431 tank_hp_l_.set_cutoff_su(sthp_su);
432 tank_hp_r_.set_cutoff_su(sthp_su);
433 fb_l = tank_hp_l_.process(fb_l);
434 fb_r = tank_hp_r_.process(fb_r);
435 }
436 float tank_l_in = diffused + sd * fb_l;
437 float tank_r_in = diffused + sd * fb_r;
438
439 // --- Tank Left ---
440 float tl = tank_l_ap1_.process(tank_l_in);
441 float tl_dl1 = tank_l_dl1_.read_cubic(
442 static_cast<float>(TANK_L_DELAY1) + mod_dl1_l);
443 tank_l_dl1_.write(tl);
444 tl = tank_l_ap2_.process(tank_l_damp_.process(tl_dl1) * sd);
445 float tl_dl2 = tank_l_dl2_.read_cubic(
446 static_cast<float>(TANK_L_DELAY2) + mod_dl2_l);
447 tank_l_dl2_.write(tl);
448 tank_l_out_ = tl_dl2;
449
450 // --- Tank Right ---
451 float tr = tank_r_ap1_.process(tank_r_in);
452 float tr_dl1 = tank_r_dl1_.read_cubic(
453 static_cast<float>(TANK_R_DELAY1) + mod_dl1_r);
454 tank_r_dl1_.write(tr);
455 tr = tank_r_ap2_.process(tank_r_damp_.process(tr_dl1) * sd);
456 float tr_dl2 = tank_r_dl2_.read_cubic(
457 static_cast<float>(TANK_R_DELAY2) + mod_dl2_r);
458 tank_r_dl2_.write(tr);
459 tank_r_out_ = tr_dl2;
460
461 // === Multi-tap stereo output ===
462 float out_l = tap_from(tank_r_ap1_buf_, TANK_R_AP1,
463 tank_r_ap1_.write_pos(), TAP_R_AP1_A)
464 + tap_from(tank_r_dl1_buf_,
465 TANK_R_DELAY1 + MOD_MARGIN,
466 tank_r_dl1_.write_pos(), TAP_R_DL1_A)
467 - tap_from(tank_r_ap2_buf_, TANK_R_AP2,
468 tank_r_ap2_.write_pos(), TAP_R_AP2_A)
469 + tap_from(tank_r_dl2_buf_,
470 TANK_R_DELAY2 + MOD_MARGIN,
471 tank_r_dl2_.write_pos(), TAP_R_DL2_A)
472 - tap_from(tank_l_dl1_buf_,
473 TANK_L_DELAY1 + MOD_MARGIN,
474 tank_l_dl1_.write_pos(), TAP_L_DL1_B)
475 - tap_from(tank_l_ap2_buf_, TANK_L_AP2,
476 tank_l_ap2_.write_pos(), TAP_L_AP2_A)
477 - tap_from(tank_l_dl2_buf_,
478 TANK_L_DELAY2 + MOD_MARGIN,
479 tank_l_dl2_.write_pos(), TAP_L_DL2_B);
480
481 float out_r = tap_from(tank_l_ap1_buf_, TANK_L_AP1,
482 tank_l_ap1_.write_pos(), TAP_L_AP1_A)
483 + tap_from(tank_l_dl1_buf_,
484 TANK_L_DELAY1 + MOD_MARGIN,
485 tank_l_dl1_.write_pos(), TAP_L_DL1_B)
486 - tap_from(tank_l_ap2_buf_, TANK_L_AP2,
487 tank_l_ap2_.write_pos(), TAP_L_AP2_A)
488 + tap_from(tank_l_dl2_buf_,
489 TANK_L_DELAY2 + MOD_MARGIN,
490 tank_l_dl2_.write_pos(), TAP_L_DL2_A)
491 - tap_from(tank_r_dl1_buf_,
492 TANK_R_DELAY1 + MOD_MARGIN,
493 tank_r_dl1_.write_pos(), TAP_R_DL1_B)
494 - tap_from(tank_r_ap2_buf_, TANK_R_AP2,
495 tank_r_ap2_.write_pos(), TAP_R_AP2_A)
496 - tap_from(tank_r_dl2_buf_,
497 TANK_R_DELAY2 + MOD_MARGIN,
498 tank_r_dl2_.write_pos(), TAP_R_DL2_B);
499
500 out_l *= soscale;
501 out_r *= soscale;
502
503 float mono = (out_l + out_r) * 0.5f;
504 out_l = mono + swidth * (out_l - mono);
505 out_r = mono + swidth * (out_r - mono);
506
507 left[i] = dry * in_l + wet * out_l;
508 right[i] = dry * in_r + wet * out_r;
509 }
510 }
511
512 void reset() {
513 if (pool_) {
514 for (uint32_t i = 0; i < pool_size_; ++i) pool_[i] = 0.0f;
515 }
516
517 for (int i = 0; i < 4; ++i) input_ap_[i].reset();
518 tank_l_ap1_.reset();
519 tank_l_ap2_.reset();
520 tank_r_ap1_.reset();
521 tank_r_ap2_.reset();
522
523 tank_l_dl1_.reset();
524 tank_l_dl2_.reset();
525 tank_r_dl1_.reset();
526 tank_r_dl2_.reset();
527
528 predelay_.reset();
529
530 tank_l_out_ = 0.0f;
531 tank_r_out_ = 0.0f;
532 tank_l_damp_.reset();
533 tank_r_damp_.reset();
534 bandwidth_.reset();
535 tank_hp_l_.reset();
536 tank_hp_r_.reset();
537
538 mod_sin_ = 0.0f;
539 mod_cos_ = 1.0f;
540
541 // Reset parameter smoothers to current targets
542 op_decay_.reset(decay_);
543 op_damping_.reset(damping_);
544 op_mix_.reset(mix_);
545 op_width_.reset(width_);
546 op_predelay_.reset(target_predelay_);
547 op_bandwidth_su_.reset(bandwidth_su_);
548 op_tank_hp_su_.reset(tank_hp_su_);
549 op_diffusion_d1_.reset(diffusion_d1_);
550 op_diffusion_d2_.reset(diffusion_d2_);
551 op_output_scale_.reset(output_scale_);
552 }
553
554private:
555 friend struct AlgoReverbTestAccess;
556
557 // =====================================================================
558 // Engineering-unit setters (private — use _su API or TestAccess)
559 // =====================================================================
560
561 void set_decay(float decay) {
562 if (decay > 0.99f) decay = 0.99f;
563 if (decay < 0.0f) decay = 0.0f;
564 decay_ = decay;
565 op_decay_.reset(decay_);
566 }
567
568 void set_damping(float damp) {
569 if (damp > 1.0f) damp = 1.0f;
570 if (damp < 0.0f) damp = 0.0f;
571 damping_ = damp;
572 op_damping_.reset(damping_);
573 }
574
575 void set_predelay(float ms) {
576 float samples = ms * types::SAMPLE_RATE_F / 1000.0f;
577 if (samples < 0.0f) samples = 0.0f;
578 if (samples >= static_cast<float>(PREDELAY_MAX)) {
579 samples = static_cast<float>(PREDELAY_MAX - 1);
580 }
581 target_predelay_ = samples;
582 op_predelay_.reset(target_predelay_);
583 }
584
585 void set_width(float width) {
586 if (width > 1.0f) width = 1.0f;
587 if (width < 0.0f) width = 0.0f;
588 width_ = width;
589 op_width_.reset(width_);
590 }
591
592 void set_mix(float mix) {
593 if (mix > 1.0f) mix = 1.0f;
594 if (mix < 0.0f) mix = 0.0f;
595 mix_ = mix;
596 op_mix_.reset(mix_);
597 }
598
599 /**
600 * @brief Set tank high-pass frequency to prevent bass buildup
601 * @param freq_hz HP cutoff in Hz (0 = disabled, typical 60–80 Hz)
602 */
603 void set_tank_hp(float freq_hz) {
604 // Convert Hz to _su via inverse exponential mapping
605 float su = 0.0f;
606 if (freq_hz > 0.0f) {
607 su = log2f(freq_hz / kMinTankHpFilterHz)
608 / log2f(kMaxTankHpHz / kMinTankHpFilterHz);
609 if (su < 0.0f) su = 0.0f;
610 if (su > 1.0f) su = 1.0f;
611 }
612 tank_hp_su_ = su;
613 op_tank_hp_su_.reset(su);
614 if (su > 0.0f) {
615 tank_hp_l_.set_cutoff_su(su);
616 tank_hp_r_.set_cutoff_su(su);
617 }
618 }
619
620 /**
621 * @brief Set input bandwidth LP frequency
622 * @param freq_hz LP cutoff in Hz (default 10000). Lower = darker/warmer input.
623 */
624 void set_bandwidth(float freq_hz) {
625 if (freq_hz < kMinBandwidthHz) freq_hz = kMinBandwidthHz;
626 // Convert Hz to _su via inverse exponential mapping
627 float su = log2f(freq_hz / kMinBandwidthHz) / kBandwidthOctaves;
628 if (su > 1.0f) su = 1.0f;
629 bandwidth_su_ = su;
630 bandwidth_.set_cutoff_su(su);
631 op_bandwidth_su_.reset(su);
632 }
633
634 /**
635 * @brief Set input diffusion coefficients (allpass feedback gains)
636 * @param d1 First pair (AP1, AP2) feedback [0, 1]. Default 0.75.
637 * @param d2 Second pair (AP3, AP4) feedback [0, 1]. Default 0.625.
638 */
639 void set_input_diffusion(float d1, float d2) {
640 if (d1 < 0.0f) d1 = 0.0f;
641 if (d1 > 0.95f) d1 = 0.95f;
642 if (d2 < 0.0f) d2 = 0.0f;
643 if (d2 > 0.95f) d2 = 0.95f;
644 diffusion_d1_ = d1;
645 diffusion_d2_ = d2;
646 input_ap_[0].set_feedback(d1);
647 input_ap_[1].set_feedback(d1);
648 input_ap_[2].set_feedback(d2);
649 input_ap_[3].set_feedback(d2);
650 op_diffusion_d1_.reset(d1);
651 op_diffusion_d2_.reset(d2);
652 }
653
654 /**
655 * @brief Set output tap scaling factor
656 * @param scale Output multiplier (default 0.10). Higher = louder wet output.
657 */
658 void set_output_scale(float scale) {
659 if (scale < 0.001f) scale = 0.001f;
660 if (scale > 1.0f) scale = 1.0f;
661 output_scale_ = scale;
662 op_output_scale_.reset(scale);
663 }
664
665 // =====================================================================
666 // Smoother configuration
667 // =====================================================================
668
669 void configure_smoothers() {
670 float sr = types::SAMPLE_RATE_F;
671 // LFO modulation increment
672 mod_inc_ = math::TWO_PI * reverb::BASE_MOD_RATE_HZ / sr;
673 // Configure bandwidth LP filter range
674 bandwidth_.set_range(kMinBandwidthHz, kMaxBandwidthHz);
675 bandwidth_.set_cutoff_su(bandwidth_su_);
676 // Configure tank HP filter range
677 tank_hp_l_.set_range(kMinTankHpFilterHz, kMaxTankHpHz);
678 tank_hp_r_.set_range(kMinTankHpFilterHz, kMaxTankHpHz);
679 // Structural params (50ms settling)
680 op_decay_.set_time_ms(50.0f, sr);
681 op_damping_.set_time_ms(50.0f, sr);
682 op_predelay_.set_time_ms(50.0f, sr);
683 op_bandwidth_su_.set_time_ms(50.0f, sr);
684 op_tank_hp_su_.set_time_ms(50.0f, sr);
685 op_diffusion_d1_.set_time_ms(50.0f, sr);
686 op_diffusion_d2_.set_time_ms(50.0f, sr);
687 // Timbral params (20ms settling)
688 op_width_.set_time_ms(20.0f, sr);
689 // Gain params (15ms settling)
690 op_mix_.set_time_ms(15.0f, sr);
691 op_output_scale_.set_time_ms(15.0f, sr);
692 // Reset all to current targets
693 op_decay_.reset(decay_);
694 op_damping_.reset(damping_);
695 op_mix_.reset(mix_);
696 op_width_.reset(width_);
697 op_predelay_.reset(target_predelay_);
698 op_bandwidth_su_.reset(bandwidth_su_);
699 op_tank_hp_su_.reset(tank_hp_su_);
700 op_diffusion_d1_.reset(diffusion_d1_);
701 op_diffusion_d2_.reset(diffusion_d2_);
702 op_output_scale_.reset(output_scale_);
703 smoothers_configured_ = true;
704 }
705
706 // =====================================================================
707 // Private helpers
708 // =====================================================================
709
710 static float tap_from(const float* buf, uint32_t buf_size,
711 uint32_t write_pos, uint32_t tap_offset) {
712 if (tap_offset >= buf_size) tap_offset = buf_size - 1;
713 uint32_t pos = (write_pos >= tap_offset)
714 ? write_pos - tap_offset
715 : write_pos + buf_size - tap_offset;
716 return buf[pos];
717 }
718
719 // =====================================================================
720 // Member state
721 // =====================================================================
722
723 float* pool_ = nullptr;
724 uint32_t pool_size_ = 0;
725
726 float* tank_l_ap1_buf_ = nullptr;
727 float* tank_l_dl1_buf_ = nullptr;
728 float* tank_l_ap2_buf_ = nullptr;
729 float* tank_l_dl2_buf_ = nullptr;
730
731 float* tank_r_ap1_buf_ = nullptr;
732 float* tank_r_dl1_buf_ = nullptr;
733 float* tank_r_ap2_buf_ = nullptr;
734 float* tank_r_dl2_buf_ = nullptr;
735
736 primitives::AllpassFilter input_ap_[4];
737 primitives::AllpassFilter tank_l_ap1_;
738 primitives::AllpassFilter tank_l_ap2_;
739 primitives::AllpassFilter tank_r_ap1_;
740 primitives::AllpassFilter tank_r_ap2_;
741
742 primitives::DelayLine tank_l_dl1_;
743 primitives::DelayLine tank_l_dl2_;
744 primitives::DelayLine tank_r_dl1_;
745 primitives::DelayLine tank_r_dl2_;
746
747 primitives::DelayLine predelay_;
748
749 float tank_l_out_ = 0.0f;
750 float tank_r_out_ = 0.0f;
751
752 primitives::OnePole tank_l_damp_;
753 primitives::OnePole tank_r_damp_;
754 primitives::OnePole bandwidth_;
755
756 primitives::OnePoleHP tank_hp_l_;
757 primitives::OnePoleHP tank_hp_r_;
758
759 float mod_sin_ = 0.0f;
760 float mod_cos_ = 1.0f;
761 float mod_inc_ = 0.0f;
762
763 // Parameter targets
764 float decay_ = 0.7f;
765 float damping_ = 0.3f;
766 float width_ = 1.0f;
767 float mix_ = 0.3f;
768 float output_scale_ = reverb::TAP_SUM_SCALE;
769 float target_predelay_ = 0.0f;
770 float bandwidth_su_ = 0.869f; // [0,1] — default 10 kHz (Dattorro bandwidth)
771 float tank_hp_su_ = 0.0f; // [0,1] — 0 = disabled
772 float diffusion_d1_ = reverb::INPUT_DIFFUSION1;
773 float diffusion_d2_ = reverb::INPUT_DIFFUSION2;
774
775 // Per-sample parameter smoothers (ParameterSmoother)
776 primitives::ParameterSmoother op_decay_;
777 primitives::ParameterSmoother op_damping_;
778 primitives::ParameterSmoother op_mix_;
779 primitives::ParameterSmoother op_width_;
780 primitives::ParameterSmoother op_predelay_;
781 primitives::ParameterSmoother op_bandwidth_su_;
782 primitives::ParameterSmoother op_tank_hp_su_;
783 primitives::ParameterSmoother op_diffusion_d1_;
784 primitives::ParameterSmoother op_diffusion_d2_;
785 primitives::ParameterSmoother op_output_scale_;
786 bool smoothers_configured_ = false;
787};
788
789// Convenient type aliases
790using PlateReverb = AlgoReverb<100>; // ~161 KB — original plate
791
792} // namespace sbl::dsp::modules
793
794#endif // SBL_DSP_MODULES_PLATE_REVERB_HPP_
Schroeder allpass filter.
void set_predelay_su(float su)
Set pre-delay [0,1] → [0, max] samples linear (structural, 50ms settling)
float max_predelay_ms() const
Maximum pre-delay in milliseconds for this scale.
void set_damping_su(float su)
Set damping [0,1] → [0, 1.0] linear (structural, 50ms settling)
void process(float *left, float *right, uint16_t frames)
Process a stereo block in-place (float)
void set_decay_su(float su)
Set decay [0,1] → [0, 0.99] linear (structural, 50ms settling)
void set_mix_su(float su)
Set wet/dry mix [0,1] → [0, 1.0] linear (gain, 15ms settling)
static constexpr uint32_t BUFFER_SIZE
Total float buffer size required for init()
void set_output_scale_su(float su)
Set output tap scale [0,1] → [0.001, 1.0] linear (gain, 15ms)
void init(float *pool, uint32_t pool_size)
Initialize with a pre-allocated float buffer pool.
void set_width_su(float su)
Set stereo width [0,1] → [0, 1.0] linear (timbral, 20ms settling)
uint32_t write_pos() const
Current write position (for external tap reads)
void set_feedback(float g)
Set feedback coefficient.
void init(float *buffer, uint32_t delay)
Initialize after default construction.
float process(float x)
Process a single sample.
void reset()
Zero the buffer and reset write position.
float read(float delay_samples) const
Read at fractional delay with linear interpolation.
void write(float sample)
Write a sample to the delay line.
float read_cubic(float delay_samples) const
Read at fractional delay with 4-point Hermite cubic interpolation.
void init(float *buffer, uint32_t max_delay)
Initialize after default construction.
uint32_t write_pos() const
Current write position (for external tap reads)
void reset()
Zero all samples in the buffer and reset write position.
void set_range(float min_hz, float max_hz)
Definition one_pole.hpp:146
void set_coefficient(float a)
Set filter coefficient directly.
Definition one_pole.hpp:73
void reset()
Reset filter state to zero.
Definition one_pole.hpp:103
void set_range(float min_hz, float max_hz)
Set frequency range for _su mapping (configuration, not a signal)
Definition one_pole.hpp:51
float process(float x)
Process a single sample.
Definition one_pole.hpp:80
void set_cutoff_su(float su)
Set cutoff frequency via signal unit.
Definition one_pole.hpp:64
void set_time_ms(float ms, float rate_hz)
Compute coefficient from settling time in milliseconds.
float process(float target)
Process one sample toward target.
The numbers every layer reaches for.
Circular buffer delay line.
Fast analytical approximations (Audio Stack — Atoms)
Fixed-point constants and audio sample types.
constexpr float TWO_PI
Definition constants.hpp:12
constexpr float BASE_MOD_RATE_HZ
constexpr uint32_t BASE_TAP_R_DL1_A
constexpr float BASE_MOD_DEPTH
constexpr uint32_t BASE_TANK_L_AP2
constexpr uint32_t BASE_TANK_R_AP2
constexpr uint32_t BASE_INPUT_AP3
constexpr float DECAY_DIFFUSION1
constexpr uint32_t BASE_TAP_R_AP2_A
constexpr uint32_t BASE_TANK_L_AP1
constexpr float TAP_SUM_SCALE
constexpr float DECAY_DIFFUSION2_BASE
constexpr uint32_t BASE_TANK_L_DELAY2
constexpr uint32_t BASE_INPUT_AP1
constexpr float INPUT_DIFFUSION1
constexpr uint32_t BASE_TANK_R_DELAY2
constexpr uint32_t BASE_PREDELAY_MAX
constexpr uint32_t BASE_TAP_L_DL1_A
constexpr uint32_t BASE_INPUT_AP4
constexpr float DECAY_DIFFUSION2_SCALE
constexpr uint32_t BASE_TAP_L_DL2_B
constexpr uint32_t BASE_TANK_R_DELAY1
constexpr float INPUT_DIFFUSION2
constexpr uint32_t BASE_TAP_L_DL2_A
constexpr uint32_t BASE_TAP_L_DL1_B
constexpr uint32_t BASE_TAP_L_AP2_A
constexpr float DEFAULT_BANDWIDTH_HZ
constexpr uint32_t BASE_TAP_L_AP1_A
constexpr uint32_t BASE_TAP_R_DL2_B
constexpr uint32_t BASE_TANK_R_AP1
constexpr uint32_t BASE_TAP_R_AP1_A
constexpr uint32_t BASE_INPUT_AP2
constexpr uint32_t BASE_TANK_L_DELAY1
constexpr uint32_t BASE_TAP_R_DL1_B
constexpr uint32_t BASE_TAP_R_DL2_A
Complete musical tools.
Definition ladder.hpp:59
float SAMPLE_RATE_F
Definition fixed.hpp:17
Single-pole IIR filters (LP and HP)
One-pole parameter smoother.