Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
tank.hpp
Go to the documentation of this file.
1#pragma once
2
3// The tank: sixteen delay lines cross-coupled through a Hadamard matrix, with
4// the whole in-loop stack (diffusion, damping, EQ, flux, shimmer, saturation)
5// between the read and the write.
6//
7// Processing order inside one pass, per channel, is load-bearing and matches
8// the engine's block diagram:
9//
10// read (modulated) -> in-loop diffusion -> damping -> EQ -> flux
11// -> [OUTPUT TAP] -> + shimmer -> saturation -> x decay gain -> Hadamard
12//
13// The tap sits BEFORE the shimmer injection and the saturator so the dry tail
14// the listener hears is the tail the decay law describes; what is fed back is
15// the tail plus everything that must be energy-accounted for.
16//
17// BLOCK PROCESSING. Every stage between one line read and the matching write
18// is either per line (read, diffusion) or per sample across lines (damping,
19// flux, the matrix). The shortest loop is far longer than a control period, so
20// the tank runs a whole period at a time: each line reads and diffuses its span
21// along time, the cross-line stages then run one sample at a time over all
22// sixteen lines as one vector, and the writes are committed last. Every sample
23// is computed by the same expression it would be one at a time, so the block
24// partition is unobservable — process() is simply a block of one.
25//
26// RT contract: prepare() allocates; configure/update/process/reset allocate
27// nothing, so a live tank-rate change never touches the allocator.
28
33#include <pulp/signal/detail/simd_lanes.hpp>
38
39#include <algorithm>
40#include <array>
41#include <cmath>
42#include <cstring>
43#include <limits>
44#include <type_traits>
45
46namespace pulp::signal::fdn {
47
48namespace lanes = signal::detail::lanes;
49
50// Everything the tank re-derives at the control rate. The engine owns the
51// smoothing; the tank owns what the smoothed values mean.
53 double decay = 2.5;
54 double size = 0.5;
55 double damp_hi = 0.4;
56 double damp_lo = 0.1;
57 double diffusion = 0.7;
58 double mod = 0.35;
59 double shimmer = 0.0;
60 double drive = 0.0;
61 double bloom = 0.0;
62 bool flutter = false;
63};
64
65// How much authority Bloom has at a given decay. It approaches 1 only at long
66// decays: at short decays Bloom fattens the tail slightly, at long decays it
67// lifts the loop to the ceiling and the tank sits just under unity — the
68// freeze regime. Decay always keeps final authority, which is what makes
69// "bloom 1 with a 0.5 s decay still decays" true by construction.
70inline double bloom_authority(double decay_seconds) {
71 if (decay_seconds <= kBloomAuthorityMinSeconds) return 0.0;
72 if (decay_seconds >= kBloomAuthorityMaxSeconds) return 1.0;
73 const double t = std::log(decay_seconds / kBloomAuthorityMinSeconds) /
75 return t * t * (3.0 - 2.0 * t);
76}
77
78// In-place 16-point Hadamard butterfly, O(N log N). Hadamard maximizes
79// inter-line scattering per pass at trivial cost, and being unitary it is
80// energy-preserving — which is the foundation every stability claim here rests
81// on. No matrix morphing: the motion comes from delay modulation and flux.
82template <typename T>
83inline void hadamard16(std::array<T, kNumChannels>& v) {
84 for (int stride = 1; stride < kNumChannels; stride *= 2) {
85 for (int i = 0; i < kNumChannels; i += 2 * stride) {
86 for (int j = i; j < i + stride; ++j) {
87 const T a = v[static_cast<std::size_t>(j)];
88 const T b = v[static_cast<std::size_t>(j + stride)];
89 v[static_cast<std::size_t>(j)] = a + b;
90 v[static_cast<std::size_t>(j + stride)] = a - b;
91 }
92 }
93 }
94 for (auto& x : v) x *= T{0.25}; // 1/sqrt(16)
95}
96
97// The matrix is the last point where one corrupt line can spread into all
98// sixteen. Guard it before input fan-out: non-finite state is killed locally,
99// and a finite overflow is contained below the tank's wider emergency ceiling.
100template <typename T>
101inline T post_hadamard_guard(T value) {
102 if (!std::isfinite(static_cast<double>(value))) return T{0};
103 return snap_to_zero(
104 std::clamp(value, static_cast<T>(-kPostHadamardCeil),
105 static_cast<T>(kPostHadamardCeil)));
106}
107
108inline double damping_state_step(double input, double coefficient,
109 double& state) {
110 state = snap_to_zero(input + (state - input) * coefficient);
111 if (!std::isfinite(state)) state = 0.0;
113 std::clamp(state, -static_cast<double>(kSanityCeil),
114 static_cast<double>(kSanityCeil)));
115 return state;
116}
117
118template <typename SampleType = float>
119class FdnTank {
120 static_assert(std::is_same_v<SampleType, float>,
121 "the tank's block kernels are single precision");
122
123 public:
124 // Sizes every allocation for the worst case: the longest line at the
125 // highest tank rate. configure() only re-derives lengths and coefficients.
126 void prepare(double max_tank_rate) {
127 const int max_line = static_cast<int>(kDelayMaxSeconds * kSizeScaleMax *
128 max_tank_rate) +
130 // The sixteen lines, and the sixteen cascades' stages, advance in
131 // lockstep: stagger every ring so none shares low address bits with
132 // another (FracDelayT::prepare).
133 for (std::size_t i = 0; i < lines_.size(); ++i)
134 lines_[i].prepare(max_line, i * kRingStaggerSamples);
135 for (std::size_t i = 0; i < loop_diffusion_.size(); ++i)
136 loop_diffusion_[i].prepare(kLoopDiffusionBaseMs, max_tank_rate, kLoopDiffusionStages,
138 shimmer_.prepare(max_tank_rate);
139 for (int i = 0; i < kNumChannels; ++i) {
140 flutter_lfo_[i].reset(static_cast<double>(i) /
141 static_cast<double>(kNumChannels));
142 }
143 }
144
145 void configure(double tank_rate) {
146 tank_rate_ = tank_rate;
147 modulation_.configure(tank_rate);
148 eq_.configure(tank_rate);
149 flux_.configure(tank_rate, flux_depth_db_);
150 shimmer_.configure(tank_rate);
151 for (int i = 0; i < kNumChannels; ++i) {
152 // Stagger each channel's in-loop cascade by its own prime so no two
153 // channels share a diffusion stage length.
154 loop_diffusion_[i].configure(kLoopDiffusionBaseMs, tank_rate,
155 kChannelPrimes[static_cast<std::size_t>(i)]);
156 const double frac = (kNumChannels > 1)
157 ? static_cast<double>(i) /
158 static_cast<double>(kNumChannels - 1)
159 : 0.0;
160 flutter_lfo_[i].set_rate(
162 tank_rate);
163 }
164 glide_ = std::exp(-static_cast<double>(kControlRateSamples) /
165 std::max(1.0, kSmoothingMs * 0.001 * tank_rate));
166 derive_lengths(controls_.size);
167 for (int i = 0; i < kNumChannels; ++i) current_delay_[i] = target_delay_[i];
168 // Sentinels no legal parameter can take, so the next tick re-derives
169 // every rate-dependent coefficient rather than trusting a stale cache.
170 cached_.size = -1.0;
171 cached_.damp_hi = -1.0;
172 cached_.damp_lo = -1.0;
173 update_controls(controls_);
174 }
175
176 void reset() {
177 for (auto& line : lines_) line.reset();
178 for (auto& cascade : loop_diffusion_) cascade.reset();
179 modulation_.reset();
180 eq_.reset();
181 flux_.reset();
182 shimmer_.reset();
183 for (int i = 0; i < kNumChannels; ++i) {
184 lp_state_[i] = 0.0f;
185 hp_state_[i] = 0.0f;
186 current_delay_[i] = target_delay_[i];
187 flutter_lfo_[i].reset(static_cast<double>(i) /
188 static_cast<double>(kNumChannels));
189 }
190 // Reset snaps the size glide too. Recompute every length-dependent
191 // coefficients now so direct tank users cannot observe target delays
192 // paired with stale decay or damping state.
193 recompute_derived_state(controls_, true);
194 cached_ = controls_;
195 heal_phase_ = 0;
196 }
197
199 active_n_ = std::clamp(n, 2, kNumChannels);
200 // Unused lines are zeroed before the matrix; the taps and the input
201 // fan-out renormalize so the wet level is independent of the count.
202 tap_scale_ = static_cast<SampleType>(
203 1.0 / std::sqrt(static_cast<double>(active_n_)));
204 in_gain_ = static_cast<SampleType>(
205 1.0 / std::sqrt(static_cast<double>(active_n_) * 0.5));
206 for (int j = 0; j < kNumChannels / 4; ++j)
207 active_[j] = lanes::make(
208 4 * j < active_n_ ? 1.0f : 0.0f, 4 * j + 1 < active_n_ ? 1.0f : 0.0f,
209 4 * j + 2 < active_n_ ? 1.0f : 0.0f, 4 * j + 3 < active_n_ ? 1.0f : 0.0f);
210 derive_safe_block();
211 }
212 int active_channels() const { return active_n_; }
213
214 LoopEq<SampleType>& eq() { return eq_; }
215 const LoopEq<SampleType>& eq() const { return eq_; }
216
217 // Flux depth, in dB of absorption. Voicing, not a host-automatable knob.
218 void set_flux_depth_db(double db) {
219 flux_depth_db_ = std::clamp(db, 0.0, kFluxMaxDb);
220 flux_.configure(tank_rate_, flux_depth_db_);
221 }
222
223 // The realized per-pass gain of one line, and the worst-case boost it was
224 // normalized against. Exposed so the acceptance suite can assert the
225 // stability bound DIRECTLY — gain x boost <= kGainCeil — rather than only
226 // observing that the tail happened to decay on the vectors it tried.
227 double applied_gain(int channel) const {
228 return gain_[static_cast<std::size_t>(channel)];
229 }
230 // Structural telemetry for the acceptance suite: the public reverb drive
231 // must reach the in-loop saturator independently of output makeup/limiting.
232 double loop_drive() const { return drive_; }
233 double worst_case_boost() const { return worst_case_boost_; }
234 // Structural telemetry for the acceptance suite: delay construction is a
235 // normative equation, so expose its realized sample count rather than
236 // asking a spectral test to infer rounding from a dense 16-line response.
237 double target_delay_samples(int channel) const {
238 return target_delay_[static_cast<std::size_t>(
239 std::clamp(channel, 0, kNumChannels - 1))];
240 }
241
242 // One control-rate tick: re-derive lengths, decay gains, damping, and the
243 // stability normalization, then advance the walk and flux sources.
245 controls_ = c;
246 if (c.size != cached_.size) derive_lengths(c.size);
247 for (int i = 0; i < kNumChannels; ++i) {
248 current_delay_[i] += (target_delay_[i] - current_delay_[i]) * (1.0 - glide_);
249 }
250 recompute_derived_state(c, false);
251
252 modulation_.step_walks();
253 flux_.tick_control(kControlRateSamples);
254 cached_ = c;
255 }
256
257 // One tank sample. `in_l` / `in_r` are already pre-diffused and ducked;
258 // `out_l` / `out_r` are the wet taps, before width and the output stage.
259 void process(SampleType in_l, SampleType in_r, SampleType& out_l, SampleType& out_r) {
260 process_block(&in_l, &in_r, &out_l, &out_r, 1);
261 }
262
263 // A block of tank samples with the controls held, as between two
264 // update_controls() calls. Any length; it is split internally so no read
265 // ever reaches a sample written inside the same piece.
266 void process_block(const SampleType* in_l, const SampleType* in_r, SampleType* out_l,
267 SampleType* out_r, int n) {
268 int offset = 0;
269 while (offset < n) {
270 const int chunk =
271 std::min({n - offset, safe_block_, kHealIntervalSamples - heal_phase_});
272 process_chunk(in_l + offset, in_r + offset, out_l + offset, out_r + offset, chunk);
273 offset += chunk;
274 heal_phase_ += chunk;
275 if (heal_phase_ == kHealIntervalSamples) {
276 heal();
277 heal_phase_ = 0;
278 }
279 }
280 }
281
282 private:
283 using Lane = std::array<SampleType, kNumChannels>;
284
285 // One piece of at most safe_block_ samples. See the file note.
286 void process_chunk(const SampleType* in_l, const SampleType* in_r, SampleType* out_l,
287 SampleType* out_r, int n) {
288 // Sample-major work area: x[k] holds every line's value at sample k.
289 alignas(64) std::array<Lane, kMaxBlockSamples> x;
290 // Line-major spans: span[i] is line i's read, then its diffusion.
291 alignas(64) SampleType span[kNumChannels][kMaxBlockSamples];
292 read_lines(span, n);
293
294 // In-loop diffusion per line, along time. Its sign alternates by
295 // channel parity so neighbouring lines never diffuse alike.
296 diffuse_lines(span, n);
297 // Back to sample-major, four samples of four lines per transpose
298 // while every line is active; the tail and reduced line counts go
299 // value by value.
300 int k0 = 0;
301 if (active_n_ == kNumChannels) {
302 for (; k0 + 4 <= n; k0 += 4) {
303 for (int j = 0; j < kNumChannels / 4; ++j) {
304 lanes::F4 a = lanes::load(&span[4 * j][k0]);
305 lanes::F4 b = lanes::load(&span[4 * j + 1][k0]);
306 lanes::F4 c = lanes::load(&span[4 * j + 2][k0]);
307 lanes::F4 d = lanes::load(&span[4 * j + 3][k0]);
308 lanes::transpose(a, b, c, d);
309 const auto col = 4 * static_cast<std::size_t>(j);
310 lanes::store(&x[static_cast<std::size_t>(k0)][col], a);
311 lanes::store(&x[static_cast<std::size_t>(k0) + 1][col], b);
312 lanes::store(&x[static_cast<std::size_t>(k0) + 2][col], c);
313 lanes::store(&x[static_cast<std::size_t>(k0) + 3][col], d);
314 }
315 }
316 }
317 for (int k = k0; k < n; ++k) {
318 for (int i = 0; i < active_n_; ++i)
319 x[static_cast<std::size_t>(k)][static_cast<std::size_t>(i)] = span[i][k];
320 for (int i = active_n_; i < kNumChannels; ++i)
321 x[static_cast<std::size_t>(k)][static_cast<std::size_t>(i)] = SampleType{0};
322 }
323
324 // Across lines, one sample at a time, as four 4-lane vectors.
325 using lanes::F4;
326 constexpr int kVectors = kNumChannels / 4;
327 F4 lp_state[kVectors];
328 F4 hp_state[kVectors];
329 for (int j = 0; j < kVectors; ++j) {
330 lp_state[j] = lanes::load(&lp_state_[4 * static_cast<std::size_t>(j)]);
331 hp_state[j] = lanes::load(&hp_state_[4 * static_cast<std::size_t>(j)]);
332 }
333 const F4 ceiling = lanes::splat(static_cast<float>(kSanityCeil));
334 const F4 floor = lanes::splat(-static_cast<float>(kSanityCeil));
335 const bool shimmer = shimmer_weight_ > 0.0;
336 const bool eq = eq_.active_bands() > 0;
337 typename FluxBank<SampleType>::Lanes flux;
338 flux_.load_lanes(flux);
339 // Each line's input lands straight in its ring: the sixteen rings
340 // share one write position, and the mirror past each ring's end takes
341 // a span that runs over it (FracDelayT::advance).
342 const std::size_t write = lines_[0].write_index();
343 SampleType* ring[kNumChannels];
344 for (std::size_t i = 0; i < static_cast<std::size_t>(kNumChannels); ++i)
345 ring[i] = lines_[i].data() + write;
346 for (int k = 0; k < n; ++k) {
347 float* row = x[static_cast<std::size_t>(k)].data();
348 F4 v[kVectors];
349 for (int j = 0; j < kVectors; ++j) {
350 // Damping: one-pole LP then one-pole HP, both scaled per line
351 // so the frequency-dependent decay is consistent across
352 // lengths. The input is clamped to the tank's sanity ceiling,
353 // and each one-pole is a convex mix of its input and its own
354 // previous state, so both states stay inside that ceiling with
355 // no clamp on the recursion; heal() snaps and sanitizes them
356 // once per chunk.
357 const F4 in = lanes::clamp(lanes::load(row + 4 * j), floor, ceiling);
358 const F4 lp = in + (lp_state[j] - in) * lp_coeff_[j];
359 lp_state[j] = lp;
360 const F4 y = lanes::select(lp_on_[j], lp, in);
361 const F4 hp = y + (hp_state[j] - y) * hp_coeff_[j];
362 hp_state[j] = hp;
363 v[j] = lanes::select(hp_on_[j], y - hp, y);
364 }
365 if (eq) {
366 for (int j = 0; j < kVectors; ++j)
367 lanes::store(row + 4 * j, v[j]);
368 for (int i = 0; i < active_n_; ++i)
369 row[i] = eq_.process(i, row[i]);
370 for (int j = 0; j < kVectors; ++j)
371 v[j] = lanes::load(row + 4 * j);
372 }
374 for (int j = 0; j < kVectors; ++j)
375 v[j] *= active_[j];
376
377 // Output tap: even channels feed left, odd feed right.
378 const F4 taps = (v[0] + v[1]) + (v[2] + v[3]);
379 out_l[k] = (lanes::lane(taps, 0) + lanes::lane(taps, 2)) * tap_scale_;
380 out_r[k] = (lanes::lane(taps, 1) + lanes::lane(taps, 3)) * tap_scale_;
381
382 // Shimmer injects into the FEEDBACK, which is what makes it bloom.
383 if (shimmer) {
384 for (int j = 0; j < kVectors; ++j)
385 lanes::store(row + 4 * j, v[j]);
386 for (int i = 0; i < active_n_; ++i)
387 row[i] += static_cast<SampleType>(
388 shimmer_weight_ * static_cast<double>(shimmer_.process(i, row[i])));
389 for (int j = 0; j < kVectors; ++j)
390 v[j] = lanes::load(row + 4 * j);
391 }
392
393 // 1-Lipschitz crossfade: both terms have slope <= 1 everywhere, so
394 // the saturator can never raise the loop gain at any drive.
395 if (drive_ > 0.0) {
396 const F4 drive = lanes::splat(static_cast<float>(drive_));
397 for (int j = 0; j < kVectors; ++j)
398 v[j] += drive * (lipschitz_tanh_lanes(v[j]) - v[j]);
399 }
400
401 // Per-line decay gain (with the matrix's 1/4 normalization folded
402 // in), then the 16-point Hadamard: two butterfly stages across the
403 // vectors, two inside each.
404 for (int j = 0; j < kVectors; ++j)
405 v[j] *= matrix_gain_[j];
406 const F4 a0 = v[0] + v[1];
407 const F4 a1 = v[0] - v[1];
408 const F4 a2 = v[2] + v[3];
409 const F4 a3 = v[2] - v[3];
410 v[0] = lanes::hadamard4(a0 + a2);
411 v[1] = lanes::hadamard4(a1 + a3);
412 v[2] = lanes::hadamard4(a0 - a2);
413 v[3] = lanes::hadamard4(a1 - a3);
414
415 // Input fan-out: left feeds the even lines, right the odd, so
416 // stereo separation is built into the topology rather than bolted
417 // on after. The matrix output is clamped to its narrower ceiling
418 // before the fan-out, and the line input is fully sanitized after
419 // it (non-finite to zero, clamped, snapped).
420 const F4 feed = lanes::make(in_gain_ * in_l[k], in_gain_ * in_r[k], in_gain_ * in_l[k],
421 in_gain_ * in_r[k]);
422 const F4 matrix_ceiling = lanes::splat(static_cast<float>(kPostHadamardCeil));
423 for (int j = 0; j < kVectors; ++j) {
424 const F4 w =
425 clean(lanes::clamp(v[j], -matrix_ceiling, matrix_ceiling) + feed, kSanityCeil);
426 for (int lane = 0; lane < 4; ++lane)
427 ring[4 * j + lane][k] = lanes::lane(w, lane);
428 }
429 }
430 flux_.store_lanes(flux);
431 for (int j = 0; j < kVectors; ++j) {
432 lanes::store(&lp_state_[4 * static_cast<std::size_t>(j)], lp_state[j]);
433 lanes::store(&hp_state_[4 * static_cast<std::size_t>(j)], hp_state[j]);
434 }
435
436 // Every ring advances, inactive lines included, so all sixteen stay
437 // in lockstep for read_lines(); an inactive line's contents are
438 // zeroed by the active mask before they can reach the matrix.
439 for (auto& line : lines_)
440 line.advance(n);
441 }
442
443 // Modulated reads for every line, one sample at a time across lines. All
444 // sixteen rings share one size and advance in lockstep, so one write index
445 // and mask serve them all; each read's four taps are one 16-byte load
446 // (FracDelayT::taps_at), and the interpolation runs four lines per vector.
447 // Same delays, taps, fractions, and curve as FracDelayT::read_hermite_at.
448 void read_lines(SampleType (&span)[kNumChannels][kMaxBlockSamples], int n) {
449 using lanes::D2;
450 using lanes::F4;
451 using lanes::I2;
452 constexpr int kPairs = kNumChannels / 2;
453 const std::size_t write = lines_[0].write_index();
454 const SampleType* ring[kNumChannels];
455 for (std::size_t i = 0; i < static_cast<std::size_t>(kNumChannels); ++i)
456 ring[i] = lines_[i].taps_at(0);
457 D2 length[kPairs];
458 for (int p = 0; p < kPairs; ++p)
459 length[p] = lanes::load(&current_delay_[2 * static_cast<std::size_t>(p)]);
460 const D2 depth = lanes::splat(mod_depth_);
461 // Each line's delay as integer part and fraction, exactly as
462 // FracDelayT::read_hermite_at derives them.
463 const D2 one = lanes::splat(1.0);
464 const D2 lowest = lanes::splat(2.0);
465 const D2 highest = lanes::splat(static_cast<double>(lines_[0].max_delay()));
466 const I2 mask = lanes::splat_i(static_cast<std::int64_t>(lines_[0].mask()));
467 const F4 half = lanes::splat(0.5f);
468 const F4 two_and_half = lanes::splat(2.5f);
469 const F4 two = lanes::splat(2.0f);
470 const F4 one_and_half = lanes::splat(1.5f);
471 const F4 onef = lanes::splat(1.0f);
472 // out[j] is lines 4j..4j+3 at one sample. Four consecutive samples'
473 // results are transposed so each vector holds one line across those
474 // samples and lands in the line-major span as one store; a tail of
475 // fewer than four samples is stored lane by lane.
476 F4 pending[4][kNumChannels / 4];
477 int held = 0;
478 const auto flush = [&](int last) {
479 if (held == 4) {
480 for (int j = 0; j < kNumChannels / 4; ++j) {
481 F4 a = pending[0][j], b = pending[1][j], c = pending[2][j], d = pending[3][j];
482 lanes::transpose(a, b, c, d);
483 const int first = last - 3;
484 lanes::store(&span[4 * j][first], a);
485 lanes::store(&span[4 * j + 1][first], b);
486 lanes::store(&span[4 * j + 2][first], c);
487 lanes::store(&span[4 * j + 3][first], d);
488 }
489 } else {
490 for (int h = 0; h < held; ++h)
491 for (int j = 0; j < kNumChannels / 4; ++j)
492 for (int lane = 0; lane < 4; ++lane)
493 span[4 * j + lane][last - held + 1 + h] =
494 lanes::lane(pending[h][j], lane);
495 }
496 held = 0;
497 };
498 for (int k = 0; k < n; ++k) {
499 alignas(16) double m[kNumChannels];
500 modulation_.tick(m);
501 const I2 origin =
502 lanes::splat_i(static_cast<std::int64_t>(write + static_cast<std::size_t>(k)) - 2);
503 alignas(16) std::int64_t start[kNumChannels];
504 F4 frac[kNumChannels / 4];
505 for (int j = 0; j < kNumChannels / 4; ++j) {
506 D2 f[2];
507 for (int h = 0; h < 2; ++h) {
508 const int p = 2 * j + h;
509 const D2 d = lanes::clamp(length[p] * (one + depth * lanes::load(m + 2 * p)),
510 lowest, highest);
511 const I2 whole = lanes::to_int(d);
512 f[h] = d - lanes::to_double(whole);
513 const I2 at = (origin - whole) & mask;
514 start[2 * p] = lanes::lane(at, 0);
515 start[2 * p + 1] = lanes::lane(at, 1);
516 }
517 frac[j] = lanes::narrow(f[0], f[1]);
518 }
519 for (int j = 0; j < kNumChannels / 4; ++j) {
520 F4 ym1 = lanes::load(ring[4 * j] + start[4 * j]);
521 F4 y0 = lanes::load(ring[4 * j + 1] + start[4 * j + 1]);
522 F4 y1 = lanes::load(ring[4 * j + 2] + start[4 * j + 2]);
523 F4 y2 = lanes::load(ring[4 * j + 3] + start[4 * j + 3]);
524 lanes::transpose(ym1, y0, y1, y2);
525 const F4 t = onef - frac[j];
526 const F4 c1 = half * (y1 - ym1);
527 const F4 c2 = ym1 - two_and_half * y0 + two * y1 - half * y2;
528 const F4 c3 = half * (y2 - ym1) + one_and_half * (y0 - y1);
529 pending[held][j] = ((c3 * t + c2) * t + c1) * t + y0;
530 }
531 if (++held == 4)
532 flush(k);
533 }
534 flush(n - 1);
535 }
536
537 // The in-loop cascades, each through AllpassCascade::run_spans4 with no
538 // setup beyond its eight span pointers.
539 void diffuse_lines(SampleType (&span)[kNumChannels][kMaxBlockSamples], int n) {
540 if (controls_.flutter) {
541 alignas(64) double lfo[kMaxBlockSamples];
542 for (int i = 0; i < active_n_; ++i) {
543 const auto ui = static_cast<std::size_t>(i);
544 for (int k = 0; k < n; ++k)
545 lfo[k] = flutter_lfo_[ui].tick();
546 const SampleType g = (i & 1) ? -loop_g_ : loop_g_;
547 loop_diffusion_[ui].process_block_fluttered(span[i], n, g, lfo, kFlutterDepth);
548 }
549 advance_inactive_cascades(n);
550 return;
551 }
552 // Every line's stage s ring has one size and advances in lockstep
553 // with the others, so the write position, mask, and the decision
554 // whether mirror upkeep is due are shared per stage; only each ring's
555 // base and its stage delay differ.
556 constexpr int kStages = kLoopDiffusionStages;
557 static_assert(kStages == 4, "run_spans4 is the four-stage kernel");
558 if (n > loop_diffusion_[0].min_delay()) {
559 for (int i = 0; i < active_n_; ++i)
560 loop_diffusion_[static_cast<std::size_t>(i)].process_block(
561 span[i], n, (i & 1) ? -loop_g_ : loop_g_);
562 advance_inactive_cascades(n);
563 return;
564 }
565 std::size_t write[kStages];
566 std::size_t mask[kStages];
567 bool upkeep[kStages];
568 for (int s = 0; s < kStages; ++s) {
569 auto& line = loop_diffusion_[0].stage(s).line();
570 write[s] = line.write_index();
571 mask[s] = line.mask();
572 upkeep[s] = write[s] < line.mirror_samples() ||
573 write[s] + static_cast<std::size_t>(n) > line.size();
574 }
575 for (int i = 0; i < active_n_; ++i) {
576 auto& cascade = loop_diffusion_[static_cast<std::size_t>(i)];
577 SampleType* delayed[kStages];
578 SampleType* stored[kStages];
579 for (int s = 0; s < kStages; ++s) {
580 auto& stage = cascade.stage(s);
581 SampleType* base = stage.line().data();
582 delayed[s] =
583 base + ((write[s] - static_cast<std::size_t>(stage.delay())) & mask[s]);
584 stored[s] = base + write[s];
585 }
586 AllpassCascade<SampleType>::run_spans4(span[i], n, (i & 1) ? -loop_g_ : loop_g_,
587 delayed, stored);
588 }
589 // Inactive lines' rings advance too, so all sixteen stay in lockstep.
590 for (int s = 0; s < kStages; ++s) {
591 for (int i = 0; i < kNumChannels; ++i) {
592 auto& line = loop_diffusion_[static_cast<std::size_t>(i)].stage(s).line();
593 if (upkeep[s])
594 line.advance(n);
595 else
596 line.skip(n);
597 }
598 }
599 }
600
601 // Inactive lines' cascades advance with the active ones so every stage
602 // ring stays in lockstep (diffuse_lines() shares write positions).
603 void advance_inactive_cascades(int n) {
604 for (int i = active_n_; i < kNumChannels; ++i)
605 for (int s = 0; s < loop_diffusion_[static_cast<std::size_t>(i)].num_stages(); ++s)
606 loop_diffusion_[static_cast<std::size_t>(i)].stage(s).line().advance(n);
607 }
608
609 // Every kHealIntervalSamples: snap decayed recursive state to zero (the
610 // interval is far too short for a state at the snap threshold to reach the
611 // denormal range), and zero any state that went non-finite, so one corrupt
612 // value can never persist in a recursion.
613 void heal() {
614 for (std::size_t i = 0; i < static_cast<std::size_t>(kNumChannels); ++i) {
615 lp_state_[i] = std::isfinite(lp_state_[i]) ? snap_to_zero(lp_state_[i]) : 0.0f;
616 hp_state_[i] = std::isfinite(hp_state_[i]) ? snap_to_zero(hp_state_[i]) : 0.0f;
617 }
618 flux_.heal();
619 }
620
621 // post_hadamard_guard / sanitize on four lanes: non-finite to zero, finite
622 // values clamped to +/-ceiling and snapped.
623 static lanes::F4 clean(lanes::F4 x, double ceiling) {
624 const auto c = lanes::splat(static_cast<float>(ceiling));
625 const lanes::F4 bounded = lanes::clamp(x, -c, c);
626 const lanes::F4 magnitude = lanes::abs(x);
627 const lanes::M4 keep = lanes::both(
628 lanes::less_equal(magnitude, lanes::splat(std::numeric_limits<float>::max())),
629 lanes::less_equal(lanes::splat(snap_threshold<float>()), magnitude));
630 return lanes::select(keep, bounded, lanes::splat(0.0f));
631 }
632
633 static lanes::F4 lipschitz_tanh_lanes(lanes::F4 v) {
634 const lanes::F4 x = lanes::clamp(v, lanes::splat(-3.0f), lanes::splat(3.0f));
635 const lanes::F4 x2 = x * x;
636 return x * (lanes::splat(27.0f) + x2) / (lanes::splat(27.0f) + lanes::splat(9.0f) * x2);
637 }
638
639 // The longest piece process_chunk() may take: every line read and every
640 // diffusion stage must reach only samples written before the piece began.
641 void derive_safe_block() {
642 int bound = kMaxBlockSamples;
643 for (int i = 0; i < active_n_; ++i) {
644 const auto ui = static_cast<std::size_t>(i);
645 // |modulation| <= 1, and the read is clamped at a delay of 2. The
646 // Hermite window's newest tap is one sample newer than the floor.
647 const double shortest = std::max(2.0, current_delay_[ui] * (1.0 - mod_depth_));
648 bound = std::min(bound, static_cast<int>(shortest) - 1);
649 int stage = loop_diffusion_[ui].min_delay();
650 if (controls_.flutter)
651 stage = static_cast<int>(static_cast<double>(stage) * (1.0 - kFlutterDepth)) - 1;
652 bound = std::min(bound, stage);
653 }
654 safe_block_ = std::max(bound, 1);
655 }
656
657 void recompute_derived_state(const TankControls& c, bool force_damping) {
658 for (int i = 0; i < kNumChannels; ++i) {
659 loop_delay_[i] =
660 current_delay_[i] + loop_diffusion_[static_cast<std::size_t>(i)].total_delay();
661 }
662
663 // Worst-case per-pass boost, from every stage that can add energy.
664 // Flux is absorptive-only and contributes exactly 1; the shimmer term
665 // is the injection weight, which is already 1/sqrt(active_n) scaled.
666 shimmer_weight_ = ShimmerBank<SampleType>::injection_weight(c.shimmer, active_n_);
667 worst_case_boost_ =
668 eq_.worst_case_boost() * flux_.worst_case_boost() * (1.0 + shimmer_weight_);
669
670 const double ceiling = kGainCeil / worst_case_boost_;
671 const double authority = c.bloom * bloom_authority(c.decay);
672 const double t60 = std::max(c.decay, 0.01);
673 // Once the controls and the size glide settle, every input below is
674 // unchanged from tick to tick; the per-line pow() is then skipped.
675 if (!force_damping && ceiling == gain_key_.ceiling && authority == gain_key_.authority &&
676 t60 == gain_key_.t60 && tank_rate_ == gain_key_.tank_rate &&
677 loop_delay_ == gain_key_.loop_delay) {
678 finish_derived_state(c, force_damping);
679 return;
680 }
681 gain_key_ = {ceiling, authority, t60, tank_rate_, loop_delay_};
682 for (int i = 0; i < kNumChannels; ++i) {
683 // Jot's absorptive law: every line reaches -60 dB at the same time
684 // regardless of its length. The length that matters is the whole
685 // LOOP — the delay line plus the in-loop diffusion cascade, which is
686 // lossless but very much not delay-free. Using the bare line length
687 // here computes the gain for a loop that does not exist and stretches
688 // the realized decay by the cascade's share of the round trip.
689 const double jot =
690 std::pow(10.0, -3.0 * loop_delay_[i] / (t60 * tank_rate_));
691 double g = std::min(jot, ceiling);
692 g += (ceiling - g) * authority;
693 gain_[i] = g;
694 }
695 for (int j = 0; j < kNumChannels / 4; ++j) {
696 const auto u = static_cast<std::size_t>(4 * j);
697 // The matrix's 1/sqrt(16) normalization rides on the decay gain.
698 matrix_gain_[j] = lanes::make(
699 static_cast<float>(gain_[u] * 0.25), static_cast<float>(gain_[u + 1] * 0.25),
700 static_cast<float>(gain_[u + 2] * 0.25), static_cast<float>(gain_[u + 3] * 0.25));
701 }
702
703 finish_derived_state(c, force_damping);
704 }
705
706 void finish_derived_state(const TankControls& c, bool force_damping) {
707 if (force_damping || c.damp_hi != cached_.damp_hi ||
708 c.damp_lo != cached_.damp_lo || c.size != cached_.size)
709 derive_damping(c.damp_hi, c.damp_lo);
710
711 mod_depth_ = c.mod * kModMaxFraction * mod_depth_shrink(c.decay);
712 loop_g_ = static_cast<SampleType>(c.diffusion * kLoopDiffusionG);
713 drive_ = c.drive;
714 derive_safe_block();
715 }
716
717 // Defense in depth. One guard at the output is not enough in a 16-line
718 // recursive structure: a single corrupted channel is inaudible at the sum
719 // until the matrix has already spread it into every other line.
720 static SampleType sanitize(SampleType x) {
721 if (!std::isfinite(static_cast<double>(x))) return SampleType{0};
722 // Denormals matter more here than anywhere else in the engine: a 60 s
723 // tail spends its last seconds in exactly the range where a
724 // denormal-stalling CPU pays for every one of sixteen recirculating
725 // lines at once.
726 return snap_to_zero(std::clamp(x, static_cast<SampleType>(-kSanityCeil),
727 static_cast<SampleType>(kSanityCeil)));
728 }
729
730 void derive_lengths(double size) {
731 const double scale =
732 kSizeScaleMin + std::clamp(size, 0.0, 1.0) * (kSizeScaleMax - kSizeScaleMin);
733 const double dmin = kDelayMinSeconds * scale;
734 const double dmax = kDelayMaxSeconds * scale;
735 for (int i = 0; i < kNumChannels; ++i) {
736 const double frac = static_cast<double>(i) /
737 static_cast<double>(kNumChannels - 1);
738 const double base = dmin + (dmax - dmin) * std::pow(frac, kDelayExponent);
739 // ADDITIVE prime offsets, not multiplicative co-prime ratios:
740 // multiplying near-equal base lengths diverges wildly when the
741 // requested range is narrow (a small room), while adding distinct
742 // primes guarantees pairwise-distinct lengths with a bounded
743 // distortion of the intended distribution.
744 target_delay_[i] =
745 std::round(base * tank_rate_) +
746 static_cast<double>(kChannelPrimes[static_cast<std::size_t>(i)]);
747 const double cap = static_cast<double>(lines_[static_cast<std::size_t>(i)]
748 .max_delay());
749 target_delay_[i] = std::clamp(target_delay_[i], 4.0, cap);
750 }
751 median_delay_ = target_delay_[kNumChannels / 2] +
752 loop_diffusion_[kNumChannels / 2].total_delay();
753 }
754
755 void derive_damping(double damp_hi, double damp_lo) {
756 const double lp_hz =
758 std::pow(1.0 - std::clamp(damp_hi, 0.0, 1.0), kDampCurve));
759 const double hp_hz =
761 std::pow(std::clamp(damp_lo, 0.0, 1.0), kDampCurve));
762 const double lp_med = std::exp(-6.283185307179586 * lp_hz / tank_rate_);
763 for (int i = 0; i < kNumChannels; ++i) {
764 const double ratio =
765 std::clamp(loop_delay_[i] / std::max(median_delay_, 1.0), 0.05, 20.0);
766 // Lowpass: the Jot-proportional exponent applies to the filter's
767 // LOSS, not to the pole. A one-pole lowpass passes (1 - a) in its
768 // stopband, so (1 - a) is what gets raised to D_i / D_med, and a
769 // longer line ends up filtering proportionally harder.
770 lp_a_[i] = lp_med <= 0.0
771 ? 0.0
772 : lerp_uniform(lp_med, 1.0 - std::pow(1.0 - lp_med, ratio));
773 // Highpass: no exact analogue exists. A one-pole highpass has no
774 // flat stopband — its attenuation is 20*log10(f / fc), which
775 // depends on frequency — so raising a gain to D_i / D_med is not
776 // defined for it. Scaling the CUTOFF by the length ratio gives the
777 // same monotone behaviour (longer lines filter harder) with a
778 // bounded, documented departure from exact proportionality.
779 const double hp_hz_i = hp_hz * ratio;
780 hp_a_[i] = lerp_uniform(
781 std::exp(-6.283185307179586 * hp_hz / tank_rate_),
782 std::exp(-6.283185307179586 * hp_hz_i / tank_rate_));
783 }
784 for (int j = 0; j < kNumChannels / 4; ++j) {
785 const auto u = static_cast<std::size_t>(4 * j);
786 lp_coeff_[j] =
787 lanes::make(static_cast<float>(lp_a_[u]), static_cast<float>(lp_a_[u + 1]),
788 static_cast<float>(lp_a_[u + 2]), static_cast<float>(lp_a_[u + 3]));
789 hp_coeff_[j] =
790 lanes::make(static_cast<float>(hp_a_[u]), static_cast<float>(hp_a_[u + 1]),
791 static_cast<float>(hp_a_[u + 2]), static_cast<float>(hp_a_[u + 3]));
792 lp_on_[j] = lanes::mask_of(lp_a_[u] > 0.0, lp_a_[u + 1] > 0.0, lp_a_[u + 2] > 0.0,
793 lp_a_[u + 3] > 0.0);
794 hp_on_[j] = lanes::mask_of(hp_a_[u] > 0.0, hp_a_[u + 1] > 0.0, hp_a_[u + 2] > 0.0,
795 hp_a_[u + 3] > 0.0);
796 }
797 }
798
799 // Blend the uniform coefficient against the length-proportional one.
800 // kDampingScale 1 is textbook-correct; 0 is uniform, which is "wrong" but
801 // sometimes sounds better on small bright rooms — the choice is data.
802 static double lerp_uniform(double uniform, double proportional) {
804 }
805
806 std::array<FracDelayT<SampleType>, kNumChannels> lines_{};
807 std::array<AllpassCascade<SampleType>, kNumChannels> loop_diffusion_{};
808 std::array<PhaseOsc, kNumChannels> flutter_lfo_{};
809 DelayModulationBank modulation_{};
810 LoopEq<SampleType> eq_{};
811 FluxBank<SampleType> flux_{};
812 ShimmerBank<SampleType> shimmer_{};
813
814 std::array<double, kNumChannels> target_delay_{};
815 std::array<double, kNumChannels> current_delay_{};
816 std::array<double, kNumChannels> loop_delay_{};
817 std::array<double, kNumChannels> gain_{};
818 std::array<double, kNumChannels> lp_a_{};
819 std::array<double, kNumChannels> hp_a_{};
820 std::array<float, kNumChannels> lp_state_{};
821 std::array<float, kNumChannels> hp_state_{};
822 // The cross-line stage's per-line constants, as four 4-lane vectors.
823 lanes::F4 matrix_gain_[kNumChannels / 4] = {};
824 lanes::F4 lp_coeff_[kNumChannels / 4] = {};
825 lanes::F4 hp_coeff_[kNumChannels / 4] = {};
826 lanes::M4 lp_on_[kNumChannels / 4] = {};
827 lanes::M4 hp_on_[kNumChannels / 4] = {};
828 lanes::F4 active_[kNumChannels / 4] = {lanes::splat(1.0f), lanes::splat(1.0f),
829 lanes::splat(1.0f), lanes::splat(1.0f)};
830
831 TankControls controls_{};
832 TankControls cached_{};
833 double tank_rate_ = 20000.0;
834 double flux_depth_db_ = kFluxDefaultDb;
835 double median_delay_ = 1.0;
836 double glide_ = 0.0;
837 double mod_depth_ = 0.0;
838 double drive_ = 0.0;
839 double shimmer_weight_ = 0.0;
840 double worst_case_boost_ = 1.0;
841 SampleType loop_g_ = 0;
842 SampleType tap_scale_ = static_cast<SampleType>(0.25);
843 SampleType in_gain_ = static_cast<SampleType>(0.35355339059327373);
844 int active_n_ = kNumChannels;
845 int safe_block_ = 1;
846 int heal_phase_ = 0;
847
848 // The inputs the per-line decay gains were last derived from.
849 struct GainKey {
850 double ceiling = -1.0;
851 double authority = -1.0;
852 double t60 = -1.0;
853 double tank_rate = -1.0;
854 std::array<double, kNumChannels> loop_delay{};
855 } gain_key_{};
856};
857
858} // namespace pulp::signal::fdn
static void run_spans4(SampleType *x, int n, SampleType g, SampleType *const *d, SampleType *const *o)
Definition diffusion.hpp:262
Definition tank.hpp:119
void process(SampleType in_l, SampleType in_r, SampleType &out_l, SampleType &out_r)
Definition tank.hpp:259
LoopEq< SampleType > & eq()
Definition tank.hpp:214
void update_controls(const TankControls &c)
Definition tank.hpp:244
double loop_drive() const
Definition tank.hpp:232
const LoopEq< SampleType > & eq() const
Definition tank.hpp:215
double applied_gain(int channel) const
Definition tank.hpp:227
void set_flux_depth_db(double db)
Definition tank.hpp:218
double worst_case_boost() const
Definition tank.hpp:233
double target_delay_samples(int channel) const
Definition tank.hpp:237
void process_block(const SampleType *in_l, const SampleType *in_r, SampleType *out_l, SampleType *out_r, int n)
Definition tank.hpp:266
void configure(double tank_rate)
Definition tank.hpp:145
int active_channels() const
Definition tank.hpp:212
void prepare(double max_tank_rate)
Definition tank.hpp:126
void reset()
Definition tank.hpp:176
void set_active_channels(int n)
Definition tank.hpp:198
static void process_lanes(Lanes &l, lanes::F4(&v)[kNumChannels/4])
Definition loop_eq.hpp:351
Definition loop_eq.hpp:74
static double injection_weight(double shimmer, int active_n)
Definition shimmer.hpp:122
@ x2
Definition forge_distortion_catalog.hpp:48
MidiEvent at(MidiEvent event, std::int32_t sample_offset) noexcept
Definition utility_contract.hpp:140
@ d
Definition pitch.hpp:22
@ f
Definition pitch.hpp:25
@ b
Definition pitch.hpp:31
@ g
Definition pitch.hpp:27
@ proportional
Definition rhythm_relationship.hpp:20
@ uniform
Definition gpu_pass.hpp:142
@ pending
Definition gpu_pass.hpp:556
@ ring
Definition string.hpp:18
Definition config.hpp:15
double mod_depth_shrink(double decay_seconds)
Definition modulation.hpp:189
constexpr double kSmoothingMs
Definition config.hpp:186
constexpr int kControlRateSamples
Definition config.hpp:187
constexpr double kDampCurve
Definition config.hpp:110
constexpr double kFluxDefaultDb
Definition config.hpp:120
constexpr int kHealIntervalSamples
Definition config.hpp:195
void hadamard16(std::array< T, kNumChannels > &v)
Definition tank.hpp:83
constexpr double kDampingScale
Definition config.hpp:98
constexpr double kDelayMinSeconds
Definition config.hpp:51
constexpr double kGainCeil
Definition config.hpp:142
constexpr double kFlutterDepth
Definition config.hpp:72
constexpr double kSizeScaleMax
Definition config.hpp:56
constexpr double kSanityCeil
Definition config.hpp:183
constexpr double kBloomAuthorityMaxSeconds
Definition config.hpp:146
constexpr int kNumChannels
Definition config.hpp:18
T post_hadamard_guard(T value)
Definition tank.hpp:101
constexpr double kLoopDiffusionBaseMs
Definition config.hpp:68
constexpr double kDampHiMinHz
Definition config.hpp:99
constexpr int kLoopDiffusionStages
Definition config.hpp:64
@ drive
Definition config.hpp:223
@ damp_lo
Definition config.hpp:219
@ shimmer
Definition config.hpp:222
@ damp_hi
Definition config.hpp:218
@ size
Definition config.hpp:216
@ tank_rate
Definition config.hpp:226
constexpr std::array< int, kNumChannels > kChannelPrimes
Definition config.hpp:27
constexpr double kFlutterRateMinHz
Definition config.hpp:73
constexpr double kFlutterRateMaxHz
Definition config.hpp:74
constexpr double kLoopDiffusionG
Definition config.hpp:66
constexpr double kDampLoMinHz
Definition config.hpp:106
constexpr double kSizeScaleMin
Definition config.hpp:55
constexpr std::size_t kRingStaggerSamples
Definition config.hpp:199
constexpr double kDelayMaxSeconds
Definition config.hpp:52
constexpr int kHermiteGuard
Definition config.hpp:171
double bloom_authority(double decay_seconds)
Definition tank.hpp:70
constexpr int kMaxBlockSamples
Definition config.hpp:190
double damping_state_step(double input, double coefficient, double &state)
Definition tank.hpp:108
constexpr double kBloomAuthorityMinSeconds
Definition config.hpp:145
constexpr double kFluxMaxDb
Definition config.hpp:115
constexpr double kDampLoMaxHz
Definition config.hpp:107
constexpr double kPostHadamardCeil
Definition config.hpp:182
constexpr double kDelayExponent
Definition config.hpp:59
constexpr double kDampHiMaxHz
Definition config.hpp:105
constexpr double kModMaxFraction
Definition config.hpp:82
void write(std::uint8_t *b, std::size_t p, std::uint64_t v, unsigned n) noexcept
Definition spectral_frame_codec.hpp:117
@ whole
Definition units.hpp:287
@ half
Definition units.hpp:290
@ first
Definition dither.hpp:23
constexpr T snap_threshold()
Threshold in absolute value below which a sample is treated as zero.
Definition denormal.hpp:36
@ feed
Definition finite_stretch_builder.hpp:40
@ bounded
Definition fm_operator_engine.hpp:35
@ c
Definition spectrum_trace.hpp:35
@ c3
Full chorus. The lush one.
Definition scanner_vibrato.hpp:28
@ c2
Medium chorus.
Definition scanner_vibrato.hpp:27
@ c1
Shallow chorus — scanner plus dry.
Definition scanner_vibrato.hpp:26
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
@ line
Definition web_event_translate.hpp:116
@ s
Definition input_events.hpp:41
@ h
Definition input_events.hpp:39
@ j
Definition input_events.hpp:40
@ y
Definition input_events.hpp:42
@ n
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
@ t
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
@ row
Definition design_ir.hpp:53
@ start
Definition geometry.hpp:370
Definition tank.hpp:52
double drive
Definition tank.hpp:60
double diffusion
Definition tank.hpp:57
double bloom
Definition tank.hpp:61
double decay
Definition tank.hpp:53
double damp_hi
Definition tank.hpp:55
double size
Definition tank.hpp:54
bool flutter
Definition tank.hpp:62
double shimmer
Definition tank.hpp:59
double damp_lo
Definition tank.hpp:56
double mod
Definition tank.hpp:58