Sound Byte Libs 29c5ff3
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/widgets/proc/plate_reverb.hpp — Dattorro plate reverb (Audio Stack — Widgets)
2//
3// Classic Dattorro plate topology:
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// Uses the full Dattorro delay topology with proper long tank delays
10// (4453, 3720, 4217, 3163 samples at 29.76kHz) and short allpass delays
11// (672, 1800, 908, 2656). Total tank ~110KB at 48kHz.
12//
13// Tank modulation uses a 4-phase quadrature oscillator (zero-cost, no LUT)
14// to modulate all 4 tank delay lines with 90° offsets. This ensures
15// matching-length delays never have aligned comb teeth, eliminating the
16// metallic resonances common in simple plate algorithms.
17//
18// Float-domain processing — STM32H7 (Cortex-M7) has hardware FPU.
19//
20// Usage:
21// float pool[PLATE_REVERB_BUFFER_SIZE] = {};
22// sbl::widgets::proc::PlateReverb reverb;
23// reverb.init(pool, PLATE_REVERB_BUFFER_SIZE);
24// reverb.set_sample_rate(48000);
25// reverb.set_decay(0.7f);
26// reverb.set_damping(0.3f);
27// reverb.process(left, right, frames);
28
29#pragma once
30
31#include <cstdint>
32#include <cstring>
33
34#include <sbl/dsp/allpass.hpp>
36#include <sbl/dsp/one_pole.hpp>
37
38namespace sbl::widgets::proc {
39
40// ============================================================================
41// Dattorro delay values (at 29.76kHz reference rate)
42// ============================================================================
43
44namespace plate {
45
46// Input diffusion allpass delays
47constexpr uint32_t INPUT_AP1 = 142;
48constexpr uint32_t INPUT_AP2 = 107;
49constexpr uint32_t INPUT_AP3 = 379;
50constexpr uint32_t INPUT_AP4 = 277;
51
52// Tank left: AP1 → Delay1 → (damp) → AP2 → Delay2
53// APs use the short Dattorro allpass delays; standalone DLs use the long delays.
54// 4453 and 3720 are coprime (GCD=1) — no shared comb harmonics.
55constexpr uint32_t TANK_L_AP1 = 672;
56constexpr uint32_t TANK_L_DELAY1 = 4453;
57constexpr uint32_t TANK_L_AP2 = 1800;
58constexpr uint32_t TANK_L_DELAY2 = 3720;
59
60// Tank right: AP1 → Delay1 → (damp) → AP2 → Delay2
61// 4217 is prime, 3163 is prime — maximum comb decorrelation.
62constexpr uint32_t TANK_R_AP1 = 908;
63constexpr uint32_t TANK_R_DELAY1 = 4217;
64constexpr uint32_t TANK_R_AP2 = 2656;
65constexpr uint32_t TANK_R_DELAY2 = 3163;
66
67// Pre-delay (max 100ms @ 48kHz)
68constexpr uint32_t PREDELAY_MAX = 4800;
69
70// LFO modulation parameters
71constexpr float MOD_RATE_HZ = 1.0f;
72constexpr float MOD_DEPTH = 8.0f;
73
74// Extra buffer headroom for modulation excursion + interpolation margin
75constexpr uint32_t MOD_MARGIN = static_cast<uint32_t>(MOD_DEPTH) + 2;
76
77// Total buffer size needed (tank delay buffers include modulation margin)
78constexpr uint32_t BUFFER_SIZE =
85
86// Input diffusion feedback coefficients (Dattorro standard)
87constexpr float INPUT_DIFFUSION1 = 0.75f; // AP1, AP2
88constexpr float INPUT_DIFFUSION2 = 0.625f; // AP3, AP4
89
90// Tank diffusion
91constexpr float DECAY_DIFFUSION1 = 0.7f; // First AP in each half
92constexpr float DECAY_DIFFUSION2 = 0.5f; // Second AP in each half
93
94// --- Output tap positions (relative to start of each delay/AP buffer) ---
95// Taps from Dattorro paper, placed to maximize echo density across the
96// stereo field. Deep taps reach into the long delay lines for diffuse tails.
97constexpr uint32_t TAP_L_AP1_A = 266;
98constexpr uint32_t TAP_L_DL1_A = 353;
99constexpr uint32_t TAP_L_DL1_B = 3627;
100constexpr uint32_t TAP_L_AP2_A = 1340;
101constexpr uint32_t TAP_L_DL2_A = 1400;
102constexpr uint32_t TAP_L_DL2_B = 2667;
103
104constexpr uint32_t TAP_R_AP1_A = 266;
105constexpr uint32_t TAP_R_DL1_A = 353;
106constexpr uint32_t TAP_R_DL1_B = 3467;
107constexpr uint32_t TAP_R_AP2_A = 700;
108constexpr uint32_t TAP_R_DL2_A = 1228;
109constexpr uint32_t TAP_R_DL2_B = 2545;
110
111} // namespace plate
112
113// ============================================================================
114// PlateReverb widget
115// ============================================================================
116
117/// Total float buffer size required for PlateReverb::init()
119
121public:
122 /// @note All public methods are ISR-safe — bounded computation, no I/O.
123
124 PlateReverb() = default;
125
126 /**
127 * @brief Initialize with a pre-allocated float buffer pool
128 * @param pool Float buffer pool (must outlive the PlateReverb)
129 * @param pool_size Pool size in floats (must be >= PLATE_REVERB_BUFFER_SIZE)
130 */
131 void init(float* pool, uint32_t pool_size) {
132 if (pool_size < plate::BUFFER_SIZE) return;
133
134 // Zero the pool — NOLOAD sections may contain garbage from previous firmware
135 memset(pool, 0, pool_size * sizeof(float));
136
137 float* p = pool;
138
139 predelay_buf_ = p; p += plate::PREDELAY_MAX;
140
141 input_ap_[0].init(p, plate::INPUT_AP1); p += plate::INPUT_AP1;
142 input_ap_[1].init(p, plate::INPUT_AP2); p += plate::INPUT_AP2;
143 input_ap_[2].init(p, plate::INPUT_AP3); p += plate::INPUT_AP3;
144 input_ap_[3].init(p, plate::INPUT_AP4); p += plate::INPUT_AP4;
145
150
151 tank_l_ap1_buf_ = p;
152 tank_l_ap1_.init(p, plate::TANK_L_AP1); p += plate::TANK_L_AP1;
153 tank_l_dl1_buf_ = p;
156 tank_l_ap2_buf_ = p;
157 tank_l_ap2_.init(p, plate::TANK_L_AP2); p += plate::TANK_L_AP2;
158 tank_l_dl2_buf_ = p;
161
164
165 tank_r_ap1_buf_ = p;
166 tank_r_ap1_.init(p, plate::TANK_R_AP1); p += plate::TANK_R_AP1;
167 tank_r_dl1_buf_ = p;
170 tank_r_ap2_buf_ = p;
171 tank_r_ap2_.init(p, plate::TANK_R_AP2); p += plate::TANK_R_AP2;
172 tank_r_dl2_buf_ = p;
175
178
179 pool_ = pool;
180 pool_size_ = pool_size;
181
182 reset();
183 }
184
185 void set_sample_rate(uint32_t sr) {
186 sample_rate_ = sr;
187 // Quadrature oscillator increment: 2π × freq / sr
188 constexpr float TWO_PI = 6.283185307f;
189 mod_inc_ = TWO_PI * plate::MOD_RATE_HZ / static_cast<float>(sr);
190 // Bandwidth limiter: ~10kHz LP at reverb input (Dattorro spec)
191 bandwidth_.set_frequency(10000.0f, static_cast<float>(sr));
192 }
193
194 void set_decay(float decay) {
195 if (decay > 0.99f) decay = 0.99f;
196 if (decay < 0.0f) decay = 0.0f;
197 decay_ = decay;
198 }
199
200 void set_damping(float damp) {
201 if (damp > 1.0f) damp = 1.0f;
202 if (damp < 0.0f) damp = 0.0f;
203 damping_ = damp;
204 // damp=0.0 (bright) → coeff=1.0 (passthrough)
205 // damp=1.0 (dark) → coeff=0.0 (hold/max filtering)
206 float coeff = 1.0f - damp;
207 tank_l_damp_.set_coefficient(coeff);
208 tank_r_damp_.set_coefficient(coeff);
209 }
210
211 void set_predelay(float ms) {
212 uint32_t samples = static_cast<uint32_t>(
213 ms * static_cast<float>(sample_rate_) / 1000.0f);
214 if (samples >= plate::PREDELAY_MAX) {
215 samples = plate::PREDELAY_MAX - 1;
216 }
217 predelay_samples_ = samples;
218 }
219
220 void set_width(float width) {
221 if (width > 1.0f) width = 1.0f;
222 if (width < 0.0f) width = 0.0f;
223 width_ = width;
224 }
225
226 void set_mix(float mix) {
227 if (mix > 1.0f) mix = 1.0f;
228 if (mix < 0.0f) mix = 0.0f;
229 mix_ = mix;
230 }
231
232 float decay() const { return decay_; }
233 float damping() const { return damping_; }
234 float mix() const { return mix_; }
235 float width() const { return width_; }
236
237 /**
238 * @brief Process a stereo block in-place (float)
239 *
240 * @param left Left channel buffer (float, in-place)
241 * @param right Right channel buffer (float, in-place)
242 * @param frames Number of sample frames
243 */
244 void process(float* left, float* right, uint16_t frames) {
245 const float wet = mix_;
246 const float dry = 1.0f - wet;
247
248 for (uint16_t i = 0; i < frames; ++i) {
249 float in_l = left[i];
250 float in_r = right[i];
251
252 float input = (in_l + in_r) * 0.5f;
253
254 // === Pre-delay ===
255 float predelayed;
256 if (predelay_samples_ == 0) {
257 predelayed = input;
258 } else {
259 predelayed = predelay_read();
260 }
261 predelay_write(input);
262
263 // === Bandwidth limiter (Dattorro input LP) ===
264 float limited = bandwidth_.process(predelayed);
265
266 // === Input diffusion ===
267 float diffused = limited;
268 for (int ap = 0; ap < 4; ++ap) {
269 diffused = input_ap_[ap].process(diffused);
270 }
271
272 // === Quadrature LFO tick (4-phase for all tank delays) ===
273 mod_sin_ += mod_inc_ * mod_cos_;
274 mod_cos_ -= mod_inc_ * mod_sin_;
275 // 4-phase distribution: matching delay lengths (e.g. 672 in
276 // both halves) get 90° offset so their combs never align.
277 float mod_dl1_l = mod_sin_ * plate::MOD_DEPTH; // 0°
278 float mod_dl2_l = mod_cos_ * plate::MOD_DEPTH; // 90°
279 float mod_dl1_r = -mod_sin_ * plate::MOD_DEPTH; // 180°
280 float mod_dl2_r = -mod_cos_ * plate::MOD_DEPTH; // 270°
281
282 // === Tank processing ===
283 float tank_l_in = diffused + decay_ * tank_r_out_;
284 float tank_r_in = diffused + decay_ * tank_l_out_;
285
286 // --- Tank Left (read-before-write preserves Dattorro delays) ---
287 // Cubic interpolation on all modulated reads reduces HF artifacts
288 float tl = tank_l_ap1_.process(tank_l_in);
289 float tl_dl1 = tank_l_dl1_.read_cubic(
290 static_cast<float>(plate::TANK_L_DELAY1) + mod_dl1_l);
291 tank_l_dl1_.write(tl);
292 tl = tank_l_ap2_.process(tank_l_damp_.process(tl_dl1) * decay_);
293 float tl_dl2 = tank_l_dl2_.read_cubic(
294 static_cast<float>(plate::TANK_L_DELAY2) + mod_dl2_l);
295 tank_l_dl2_.write(tl);
296 tank_l_out_ = tl_dl2;
297
298 // --- Tank Right (read-before-write preserves Dattorro delays) ---
299 float tr = tank_r_ap1_.process(tank_r_in);
300 float tr_dl1 = tank_r_dl1_.read_cubic(
301 static_cast<float>(plate::TANK_R_DELAY1) + mod_dl1_r);
302 tank_r_dl1_.write(tr);
303 tr = tank_r_ap2_.process(tank_r_damp_.process(tr_dl1) * decay_);
304 float tr_dl2 = tank_r_dl2_.read_cubic(
305 static_cast<float>(plate::TANK_R_DELAY2) + mod_dl2_r);
306 tank_r_dl2_.write(tr);
307 tank_r_out_ = tr_dl2;
308
309 // === Multi-tap stereo output ===
310 // Dattorro output tapping: 7 taps per channel from the
311 // opposite tank half (cross-fed), using deep taps into the
312 // long delay lines for maximum echo density.
313 float out_l = tap_from(tank_r_ap1_buf_, plate::TANK_R_AP1,
314 tank_r_ap1_.write_pos(), plate::TAP_R_AP1_A)
315 + tap_from(tank_r_dl1_buf_,
317 tank_r_dl1_.write_pos(), plate::TAP_R_DL1_A)
318 - tap_from(tank_r_ap2_buf_, plate::TANK_R_AP2,
319 tank_r_ap2_.write_pos(), plate::TAP_R_AP2_A)
320 + tap_from(tank_r_dl2_buf_,
322 tank_r_dl2_.write_pos(), plate::TAP_R_DL2_A)
323 - tap_from(tank_l_dl1_buf_,
325 tank_l_dl1_.write_pos(), plate::TAP_L_DL1_B)
326 - tap_from(tank_l_ap2_buf_, plate::TANK_L_AP2,
327 tank_l_ap2_.write_pos(), plate::TAP_L_AP2_A)
328 - tap_from(tank_l_dl2_buf_,
330 tank_l_dl2_.write_pos(), plate::TAP_L_DL2_B);
331
332 float out_r = tap_from(tank_l_ap1_buf_, plate::TANK_L_AP1,
333 tank_l_ap1_.write_pos(), plate::TAP_L_AP1_A)
334 + tap_from(tank_l_dl1_buf_,
336 tank_l_dl1_.write_pos(), plate::TAP_L_DL1_B)
337 - tap_from(tank_l_ap2_buf_, plate::TANK_L_AP2,
338 tank_l_ap2_.write_pos(), plate::TAP_L_AP2_A)
339 + tap_from(tank_l_dl2_buf_,
341 tank_l_dl2_.write_pos(), plate::TAP_L_DL2_A)
342 - tap_from(tank_r_dl1_buf_,
344 tank_r_dl1_.write_pos(), plate::TAP_R_DL1_B)
345 - tap_from(tank_r_ap2_buf_, plate::TANK_R_AP2,
346 tank_r_ap2_.write_pos(), plate::TAP_R_AP2_A)
347 - tap_from(tank_r_dl2_buf_,
349 tank_r_dl2_.write_pos(), plate::TAP_R_DL2_B);
350
351 out_l *= 0.15f;
352 out_r *= 0.15f;
353
354 float mono = (out_l + out_r) * 0.5f;
355 out_l = mono + width_ * (out_l - mono);
356 out_r = mono + width_ * (out_r - mono);
357
358 left[i] = dry * in_l + wet * out_l;
359 right[i] = dry * in_r + wet * out_r;
360 }
361 }
362
363 void reset() {
364 if (pool_) {
365 for (uint32_t i = 0; i < pool_size_; ++i) pool_[i] = 0.0f;
366 }
367
368 for (int i = 0; i < 4; ++i) input_ap_[i].clear();
369 tank_l_ap1_.clear();
370 tank_l_ap2_.clear();
371 tank_r_ap1_.clear();
372 tank_r_ap2_.clear();
373
374 tank_l_dl1_.clear();
375 tank_l_dl2_.clear();
376 tank_r_dl1_.clear();
377 tank_r_dl2_.clear();
378
379 predelay_wp_ = 0;
380
381 tank_l_out_ = 0.0f;
382 tank_r_out_ = 0.0f;
383 tank_l_damp_.reset();
384 tank_r_damp_.reset();
385 bandwidth_.reset();
386
387 // Reset quadrature oscillator (sine=0, cosine=1)
388 mod_sin_ = 0.0f;
389 mod_cos_ = 1.0f;
390 }
391
392private:
393 void predelay_write(float sample) {
394 predelay_buf_[predelay_wp_] = sample;
395 ++predelay_wp_;
396 if (predelay_wp_ >= plate::PREDELAY_MAX) predelay_wp_ = 0;
397 }
398
399 float predelay_read() const {
400 if (predelay_samples_ == 0) {
401 return predelay_buf_[predelay_wp_];
402 }
403 uint32_t pos = (predelay_wp_ >= predelay_samples_)
404 ? predelay_wp_ - predelay_samples_
405 : predelay_wp_ + plate::PREDELAY_MAX - predelay_samples_;
406 return predelay_buf_[pos];
407 }
408
409 static float tap_from(const float* buf, uint32_t buf_size,
410 uint32_t write_pos, uint32_t tap_offset) {
411 if (tap_offset >= buf_size) tap_offset = buf_size - 1;
412 uint32_t pos = (write_pos >= tap_offset)
413 ? write_pos - tap_offset
414 : write_pos + buf_size - tap_offset;
415 return buf[pos];
416 }
417
418 float* pool_ = nullptr;
419 uint32_t pool_size_ = 0;
420
421 float* predelay_buf_ = nullptr;
422
423 float* tank_l_ap1_buf_ = nullptr;
424 float* tank_l_dl1_buf_ = nullptr;
425 float* tank_l_ap2_buf_ = nullptr;
426 float* tank_l_dl2_buf_ = nullptr;
427
428 float* tank_r_ap1_buf_ = nullptr;
429 float* tank_r_dl1_buf_ = nullptr;
430 float* tank_r_ap2_buf_ = nullptr;
431 float* tank_r_dl2_buf_ = nullptr;
432
433 sbl::dsp::AllpassFilter input_ap_[4];
434 sbl::dsp::AllpassFilter tank_l_ap1_;
435 sbl::dsp::AllpassFilter tank_l_ap2_;
436 sbl::dsp::AllpassFilter tank_r_ap1_;
437 sbl::dsp::AllpassFilter tank_r_ap2_;
438
443
444 uint32_t predelay_wp_ = 0;
445
446 float tank_l_out_ = 0.0f;
447 float tank_r_out_ = 0.0f;
448
449 dsp::OnePole tank_l_damp_;
450 dsp::OnePole tank_r_damp_;
451 dsp::OnePole bandwidth_; // Input bandwidth limiter (Dattorro input LP)
452
453 // Quadrature oscillator state
454 float mod_sin_ = 0.0f;
455 float mod_cos_ = 1.0f;
456 float mod_inc_ = 0.0f;
457
458 float decay_ = 0.7f;
459 float damping_ = 0.3f;
460 float width_ = 1.0f;
461 float mix_ = 0.3f;
462 uint32_t predelay_samples_ = 0;
463 uint32_t sample_rate_ = 48000;
464};
465
466} // namespace sbl::widgets::proc
uint32_t write_pos() const
Current write position (for external tap reads)
Definition allpass.hpp:96
void clear()
Zero the buffer and reset write position.
Definition allpass.hpp:85
void init(float *buffer, uint32_t delay)
Initialize after default construction.
Definition allpass.hpp:44
void set_feedback(float g)
Set feedback coefficient.
Definition allpass.hpp:79
float process(float x)
Process a single sample.
Definition allpass.hpp:59
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 clear()
Zero all samples in the buffer and reset write position.
float read_cubic(float delay_samples) const
Read at fractional delay with 4-point Hermite cubic interpolation.
void write(float sample)
Write a sample to the delay line.
void reset()
Reset filter state to zero.
Definition one_pole.hpp:82
float process(float x)
Process a single sample.
Definition one_pole.hpp:58
void set_coefficient(float a)
Set filter coefficient directly.
Definition one_pole.hpp:31
void set_frequency(float freq_hz, uint32_t sr=48000)
Compute coefficient from cutoff frequency (cold path)
Definition one_pole.hpp:41
void process(float *left, float *right, uint16_t frames)
Process a stereo block in-place (float)
void init(float *pool, uint32_t pool_size)
Initialize with a pre-allocated float buffer pool.
constexpr uint32_t TAP_R_DL2_B
constexpr uint32_t PREDELAY_MAX
constexpr uint32_t TAP_R_AP1_A
constexpr uint32_t INPUT_AP1
constexpr uint32_t TAP_L_AP1_A
constexpr uint32_t BUFFER_SIZE
constexpr uint32_t TANK_R_DELAY1
constexpr uint32_t TAP_R_AP2_A
constexpr uint32_t TANK_L_AP1
constexpr float DECAY_DIFFUSION1
constexpr uint32_t MOD_MARGIN
constexpr uint32_t TANK_R_AP1
constexpr float DECAY_DIFFUSION2
constexpr uint32_t INPUT_AP3
constexpr uint32_t TAP_L_DL1_B
constexpr uint32_t INPUT_AP2
constexpr float INPUT_DIFFUSION1
constexpr uint32_t TAP_L_DL1_A
constexpr uint32_t TAP_R_DL2_A
constexpr uint32_t TANK_R_AP2
constexpr uint32_t TAP_L_DL2_A
constexpr uint32_t TAP_L_DL2_B
constexpr uint32_t TANK_L_DELAY2
constexpr uint32_t INPUT_AP4
constexpr uint32_t TANK_L_DELAY1
constexpr uint32_t TANK_R_DELAY2
constexpr uint32_t TAP_R_DL1_A
constexpr uint32_t TAP_R_DL1_B
constexpr float INPUT_DIFFUSION2
constexpr uint32_t TAP_L_AP2_A
constexpr uint32_t TANK_L_AP2
constexpr float MOD_RATE_HZ
Signal processing widgets.
Definition delay.hpp:31
constexpr uint32_t PLATE_REVERB_BUFFER_SIZE
Total float buffer size required for PlateReverb::init()