Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
nonlin_ambience.hpp
Go to the documentation of this file.
1#pragma once
2
6
9
10namespace pulp::signal {
11
13template <typename SampleType = float>
15public:
19 static constexpr int kTopologyWorkPerSample = 4;
23 struct Tap {
24 int delay = 0;
25 float gain = 0.0f;
26 int segment = 0;
27 };
28
29 // ── Lifecycle ─────────────────────────────────────────────────────────
30
34 void prepare(double sample_rate,
35 double max_length_ms = nonlin_ambience::kMaxLengthMs,
36 double max_predelay_ms = nonlin_ambience::kMaxPredelayMs) {
37 namespace na = nonlin_ambience;
38
39 sample_rate_ = (std::isfinite(sample_rate) && sample_rate > 0.0) ? sample_rate
40 : 48000.0;
41 max_length_ms_ = std::isfinite(max_length_ms)
42 ? std::max(nonlin_ambience::kMinLengthMs, max_length_ms)
44 max_predelay_ms_ = std::isfinite(max_predelay_ms) ? std::max(0.0, max_predelay_ms)
46
47 const int max_len_samples = static_cast<int>(std::ceil(
48 units::ms_to_samples(max_length_ms_, sample_rate_)));
49 const int max_pre_samples = static_cast<int>(std::ceil(
50 units::ms_to_samples(max_predelay_ms_, sample_rate_)));
51
52 // Power-of-two ring: the tap read is the hot loop and a mask is one
53 // instruction where a modulo is a division. See the file note.
54 ring_mask_ = static_cast<int>(next_pow2(static_cast<std::size_t>(
55 max_len_samples + max_pre_samples + 2))) - 1;
56 ring_.assign(static_cast<std::size_t>(ring_mask_) + 1u, SampleType{0});
57 ring_write_ = 0;
58
59 // A grid step never advances less than fs/Nd_max samples, so the number
60 // of steps inside an L-sample window is at most L·Nd_max/fs — which is
61 // (max_length_ms/1000)·Nd_max, independent of the sample rate.
62 tap_capacity_ = static_cast<int>(std::ceil(
63 na::kNdMax * max_length_ms_ / 1000.0)) + na::kTapGuard;
64 for (int bank = 0; bank < 2; ++bank)
65 for (int ch = 0; ch < 2; ++ch) {
66 taps_[bank][ch].assign(static_cast<std::size_t>(tap_capacity_), Tap{});
67 tap_count_[bank][ch] = 0;
68 }
69
70 for (int i = 0; i < na::kNumAllpass; ++i) {
71 allpass_len_[i] = nearest_prime(std::max(2, static_cast<int>(std::lround(
72 units::ms_to_samples(na::kAllpassDelaysMs[i], sample_rate_)))));
73 allpass_[i].prepare(allpass_len_[i] + 1);
74 }
75
76 swap_fade_len_ = std::max(1, static_cast<int>(std::lround(
77 units::ms_to_samples(na::kSwapFadeMs, sample_rate_))));
78
79 const auto smooth_s = static_cast<SampleType>(na::kParamSmoothMs / 1000.0);
80 const auto fs = static_cast<SampleType>(sample_rate_);
81 mix_.set_ramp_time(smooth_s, fs);
82 width_.set_ramp_time(smooth_s, fs);
83 output_gain_.set_ramp_time(smooth_s, fs);
84 converter_.set_ramp_time(smooth_s, fs);
85
86 for (int ch = 0; ch < 2; ++ch) {
87 conv_hp_[ch].prepare(static_cast<SampleType>(sample_rate_));
88 conv_hp_[ch].set_cutoff(static_cast<SampleType>(na::kConverterFcLo));
89 conv_lp_[ch].prepare(static_cast<SampleType>(sample_rate_));
90 conv_lp_[ch].set_cutoff(static_cast<SampleType>(na::kConverterFcHi));
91 conv_dc_[ch].set_pole(static_cast<SampleType>(na::kConverterDcPole));
92 }
93
94 update_segment_coefficients();
95 rebuild_immediate();
96 reset();
97 }
98
101 void reset() {
102 std::fill(ring_.begin(), ring_.end(), SampleType{0});
103 ring_write_ = 0;
104 for (int i = 0; i < nonlin_ambience::kNumAllpass; ++i) allpass_[i].reset();
105 for (auto& bank : seg_state_)
106 for (auto& ch : bank) ch.fill(SampleType{0});
107 for (int ch = 0; ch < 2; ++ch) {
108 conv_hp_[ch].reset();
109 conv_lp_[ch].reset();
110 conv_dc_[ch].reset();
111 }
112 dither_.reset();
113 mix_.set_immediate(mix_.target());
114 width_.set_immediate(width_.target());
115 output_gain_.set_immediate(output_gain_.target());
116 converter_.set_immediate(converter_.target());
117 // A pending swap is resolved rather than carried across a reset: the
118 // crossfade is a signal-domain artifact and there is no signal now.
119 if (fading_) {
120 front_ = pending_;
121 fading_ = false;
122 }
123 fade_pos_ = 0;
124 }
125
128 static constexpr int latency_samples() { return 0; }
129
130 // ── Topology parameters (rebuild the FIR) ─────────────────────────────
131
133 if (program == program_) return;
134 program_ = program;
135 rebuild_and_swap();
136 }
137
140 void set_length_ms(double ms) {
141 if (!std::isfinite(ms)) return;
142 const double v = std::clamp(ms, nonlin_ambience::kMinLengthMs, max_length_ms_);
143 if (v == length_ms_) return;
144 length_ms_ = v;
145 rebuild_and_swap();
146 }
147
148 void set_predelay_ms(double ms) {
149 if (!std::isfinite(ms)) return;
150 const double v = std::clamp(ms, 0.0, max_predelay_ms_);
151 if (v == predelay_ms_) return;
152 predelay_ms_ = v;
153 rebuild_and_swap();
154 }
155
158 void set_density_pct(double pct) {
159 if (!std::isfinite(pct)) return;
160 const double v = std::clamp(pct, nonlin_ambience::kMinDensityPct, 100.0);
161 if (v == density_pct_) return;
162 density_pct_ = v;
163 rebuild_and_swap();
164 }
165
166 void set_density_growth(double gamma) {
167 if (!std::isfinite(gamma)) return;
168 const double v = std::clamp(gamma, 0.0, 2.0);
169 if (v == density_growth_) return;
170 density_growth_ = v;
171 rebuild_and_swap();
172 }
173
175 void set_gate_hold_pct(double pct) {
176 if (!std::isfinite(pct)) return;
177 const double v = std::clamp(pct, 10.0, 95.0);
178 if (v == gate_hold_pct_) return;
179 gate_hold_pct_ = v;
180 rebuild_and_swap();
181 }
182
185 void set_attack_pct(double pct) {
186 if (!std::isfinite(pct)) return;
187 const double v = std::clamp(pct, 5.0, 98.0);
188 if (v == attack_pct_) return;
189 attack_pct_ = v;
190 rebuild_and_swap();
191 }
192
196 void set_topology(NonlinProgram program, double length_ms, double predelay_ms,
197 double density_pct, double density_growth,
198 double gate_hold_pct, double attack_pct) {
199 const double length = std::isfinite(length_ms)
201 max_length_ms_)
202 : length_ms_;
203 const double predelay = std::isfinite(predelay_ms)
204 ? std::clamp(predelay_ms, 0.0, max_predelay_ms_)
205 : predelay_ms_;
206 const double density = std::isfinite(density_pct)
207 ? std::clamp(density_pct,
209 : density_pct_;
210 const double growth = std::isfinite(density_growth)
211 ? std::clamp(density_growth, 0.0, 2.0)
212 : density_growth_;
213 const double hold = std::isfinite(gate_hold_pct)
214 ? std::clamp(gate_hold_pct, 10.0, 95.0)
215 : gate_hold_pct_;
216 const double attack = std::isfinite(attack_pct)
217 ? std::clamp(attack_pct, 5.0, 98.0)
218 : attack_pct_;
219 if (program == program_ && length == length_ms_ && predelay == predelay_ms_ &&
220 density == density_pct_ && growth == density_growth_ &&
221 hold == gate_hold_pct_ && attack == attack_pct_)
222 return;
223 program_ = program;
224 length_ms_ = length;
225 predelay_ms_ = predelay;
226 density_pct_ = density;
227 density_growth_ = growth;
228 gate_hold_pct_ = hold;
229 attack_pct_ = attack;
230 rebuild_and_swap();
231 }
232
236 void request_topology(NonlinProgram program, double length_ms, double predelay_ms,
237 double density_pct, double density_growth,
238 double gate_hold_pct, double attack_pct) {
239 const double length = std::isfinite(length_ms)
241 max_length_ms_)
242 : length_ms_;
243 const double predelay = std::isfinite(predelay_ms)
244 ? std::clamp(predelay_ms, 0.0, max_predelay_ms_)
245 : predelay_ms_;
246 const double density = std::isfinite(density_pct)
247 ? std::clamp(density_pct,
249 : density_pct_;
250 const double growth = std::isfinite(density_growth)
251 ? std::clamp(density_growth, 0.0, 2.0)
252 : density_growth_;
253 const double hold = std::isfinite(gate_hold_pct)
254 ? std::clamp(gate_hold_pct, 10.0, 95.0)
255 : gate_hold_pct_;
256 const double attack = std::isfinite(attack_pct)
257 ? std::clamp(attack_pct, 5.0, 98.0)
258 : attack_pct_;
259 if (program == program_ && length == length_ms_ && predelay == predelay_ms_ &&
260 density == density_pct_ && growth == density_growth_ &&
261 hold == gate_hold_pct_ && attack == attack_pct_)
262 return;
263 program_ = program;
264 length_ms_ = length;
265 predelay_ms_ = predelay;
266 density_pct_ = density;
267 density_growth_ = growth;
268 gate_hold_pct_ = hold;
269 attack_pct_ = attack;
270 topology_dirty_ = true;
271 topology_build_.active = false; // restart from the newest atomic snapshot
272 }
273
274 std::uint64_t topology_rebuild_count() const { return topology_rebuild_count_; }
275 int topology_work_units_last_sample() const { return topology_work_units_last_sample_; }
276
279 void set_seed(std::uint32_t seed) {
280 if (seed == seed_) return;
281 seed_ = seed;
282 rebuild_and_swap();
283 }
284
287 void set_diffusion(double g) {
288 if (!std::isfinite(g)) return;
289 diffusion_ = std::clamp(g, 0.0, nonlin_ambience::kDiffusionMax);
290 }
291
292 // ── Continuous parameters (smoothed, never rebuild) ───────────────────
293
298 void set_tone(double tone) {
299 if (!std::isfinite(tone)) return;
300 const double v = std::clamp(tone, -1.0, 1.0);
301 if (v == tone_) return;
302 tone_ = v;
303 update_segment_coefficients();
304 }
305
306 void set_hf_damp_hz(double hz) {
307 if (!std::isfinite(hz)) return;
308 const double v = std::clamp(hz, 1000.0, 18000.0);
309 if (v == hf_damp_hz_) return;
310 hf_damp_hz_ = v;
311 update_segment_coefficients();
312 }
313
316 void set_width_pct(double pct) {
317 if (!std::isfinite(pct)) return;
318 width_.set_target(static_cast<SampleType>(std::clamp(pct, 0.0, 100.0) / 100.0));
319 }
320
322 void set_converter_amount(double amount) {
323 if (!std::isfinite(amount)) return;
324 converter_.set_target(static_cast<SampleType>(std::clamp(amount, 0.0, 1.0)));
325 }
326
327 void set_output_gain_db(double db) {
328 if (!std::isfinite(db)) return;
329 output_gain_.set_target(
330 units::db_to_linear(static_cast<SampleType>(std::clamp(db, -24.0, 24.0))));
331 }
332
334 void set_mix_pct(double pct) {
335 if (!std::isfinite(pct)) return;
336 mix_.set_target(static_cast<SampleType>(std::clamp(pct, 0.0, 100.0) / 100.0));
337 }
338
339 // ── Processing ────────────────────────────────────────────────────────
340
342 void process_sample(SampleType& left, SampleType& right) {
343 if (!std::isfinite(static_cast<double>(left)) ||
344 !std::isfinite(static_cast<double>(right))) {
345 left = SampleType{0};
346 right = SampleType{0};
347 return;
348 }
349 const SampleType dry_l = left;
350 const SampleType dry_r = right;
351
352 // Mono sum into the diffuser: the tap cloud is one shared field.
353 SampleType d = SampleType{0.5} * (dry_l + dry_r);
354 const auto g = static_cast<SampleType>(diffusion_);
355 for (int i = 0; i < nonlin_ambience::kNumAllpass; ++i) {
356 const SampleType delayed = allpass_[i].read(allpass_len_[i] - 1);
357 const SampleType w = snap_to_zero(d + g * delayed);
358 d = -g * w + delayed;
359 allpass_[i].push(w);
360 }
361
362 ring_write_ = (ring_write_ + 1) & ring_mask_;
363 ring_[static_cast<std::size_t>(ring_write_)] = d;
364
365 SampleType wet_l = SampleType{0};
366 SampleType wet_r = SampleType{0};
367
368 if (!fading_) {
369 wet_l = render_bank(front_, 0);
370 wet_r = render_bank(front_, 1);
371 } else {
372 SampleType old_gain{}, new_gain{};
373 const auto u = static_cast<SampleType>(fade_pos_) /
374 static_cast<SampleType>(swap_fade_len_);
376 old_gain, new_gain);
377 wet_l = old_gain * render_bank(front_, 0) + new_gain * render_bank(pending_, 0);
378 wet_r = old_gain * render_bank(front_, 1) + new_gain * render_bank(pending_, 1);
379 if (++fade_pos_ >= swap_fade_len_) {
380 // The retiring bank's tilt filters are cleared so a later swap
381 // back into it does not start from stale state.
382 seg_state_[front_][0].fill(SampleType{0});
383 seg_state_[front_][1].fill(SampleType{0});
384 front_ = pending_;
385 fading_ = false;
386 fade_pos_ = 0;
387 }
388 }
389
390 const SampleType conv = converter_.next();
391 if (conv > SampleType{0}) {
392 wet_l = converter_stage(0, wet_l, conv);
393 wet_r = converter_stage(1, wet_r, conv);
394 }
395
396 // Width: mid/side. Exactly mono at 0, exactly the two realizations at 1.
397 const SampleType w = width_.next();
398 const SampleType mid = SampleType{0.5} * (wet_l + wet_r);
399 wet_l = mid + w * (wet_l - mid);
400 wet_r = mid + w * (wet_r - mid);
401
402 const SampleType trim = output_gain_.next();
403 wet_l *= trim;
404 wet_r *= trim;
405
406 SampleType dry_gain{}, wet_gain{};
407 crossfade_gains(mix_.next(), CrossfadeGainLaw::EqualGain, dry_gain, wet_gain);
408 left = dry_gain * dry_l + wet_gain * wet_l;
409 right = dry_gain * dry_r + wet_gain * wet_r;
410 advance_topology_rebuild();
411 }
412
414 void process(SampleType* left, SampleType* right, int num_samples) {
415 for (int n = 0; n < num_samples; ++n) process_sample(left[n], right[n]);
416 }
417
418 // ── Introspection ─────────────────────────────────────────────────────
419 //
420 // The tap table IS the product of this module, so it is readable. The
421 // acceptance suite asserts the realized gains against an independent
422 // transcription of the envelope math, which is only possible because these
423 // are public.
424
426 int tap_count(int channel) const { return tap_count_[front_][channel & 1]; }
427
430 const Tap& tap(int channel, int index) const {
431 return taps_[front_][channel & 1][static_cast<std::size_t>(index)];
432 }
433
436 double tap_norm(int channel) const { return tap_norm_[front_][channel & 1]; }
437
445 int window_samples() const { return bank_window_[front_]; }
446 int predelay_samples() const { return bank_predelay_[front_]; }
447 int allpass_length(int index) const { return allpass_len_[index]; }
448
451 double worst_case_gain() const {
453 diffusion_, converter_.target() > SampleType{0});
454 }
455
456 NonlinProgram program() const { return program_; }
457 double length_ms() const { return length_ms_; }
458 double tone() const { return tone_; }
459 bool swap_in_progress() const { return fading_; }
460
464 double envelope(double tau) const {
465 return nonlin_ambience::program_envelope(program_, tau, gate_hold_pct_ / 100.0,
466 attack_pct_ / 100.0);
467 }
468
469private:
470 // ── Tap-cloud rendering ───────────────────────────────────────────────
471
472 SampleType render_bank(int bank, int channel) {
473 std::array<SampleType, nonlin_ambience::kSegments> acc{};
474 const Tap* taps = taps_[bank][channel].data();
475 const int count = tap_count_[bank][channel];
476 const SampleType* ring = ring_.data();
477 const int write = ring_write_;
478 const int mask = ring_mask_;
479 for (int k = 0; k < count; ++k)
480 acc[static_cast<std::size_t>(taps[k].segment)] +=
481 static_cast<SampleType>(taps[k].gain) *
482 ring[static_cast<std::size_t>((write - taps[k].delay) & mask)];
483
484 SampleType out{0};
485 auto& state = seg_state_[bank][channel];
486 for (int s = 0; s < nonlin_ambience::kSegments; ++s) {
487 const SampleType a = seg_pole_[static_cast<std::size_t>(s)];
488 state[static_cast<std::size_t>(s)] = snap_to_zero(
489 a * state[static_cast<std::size_t>(s)] +
490 (SampleType{1} - a) * acc[static_cast<std::size_t>(s)]);
491 out += state[static_cast<std::size_t>(s)];
492 }
493 return out;
494 }
495
498 SampleType converter_stage(int channel, SampleType x, SampleType amount) {
499 namespace na = nonlin_ambience;
500 SampleType y = conv_hp_[channel].process_highpass(x);
501 y = conv_lp_[channel].process_lowpass(y);
502
503 const int bits = na::kConverterBitsMax -
504 static_cast<int>(std::lround(
505 static_cast<double>(amount) * na::kConverterBitSweep));
506 const auto step = static_cast<SampleType>(
507 1.0 / static_cast<double>(1 << (std::max(2, bits) - 1)));
508 // TPDF dither, 1 LSB peak: the difference of two independent uniforms.
509 const SampleType d = tpdf_difference(dither_.next_unit<SampleType>(),
510 dither_.next_unit<SampleType>()) * step;
511 y = std::round((y + d) / step) * step;
512
513 return conv_dc_[channel].process(y);
514 }
515
516 // ── Tap-table construction ────────────────────────────────────────────
517
518 void rebuild_immediate() {
519 topology_dirty_ = false;
520 topology_build_.active = false;
521 build_bank(front_);
522 fading_ = false;
523 fade_pos_ = 0;
524 }
525
526 void rebuild_and_swap() {
527 if (ring_.empty()) return; // not prepared yet
528 topology_dirty_ = false;
529 topology_build_.active = false;
530 if (fading_) {
531 // A second topology change inside one fade collapses the first: the
532 // pending bank becomes live immediately so the back bank is free to
533 // be rewritten. Documented, and only reachable by changing topology
534 // twice within kSwapFadeMs.
535 front_ = pending_;
536 fading_ = false;
537 fade_pos_ = 0;
538 }
539 const int back = 1 - front_;
540 build_bank(back);
541 ++topology_rebuild_count_;
542 seg_state_[back][0].fill(SampleType{0});
543 seg_state_[back][1].fill(SampleType{0});
544 pending_ = back;
545 fade_pos_ = 0;
546 fading_ = true;
547 }
548
549 void build_bank(int bank) {
550 namespace na = nonlin_ambience;
551
552 const int window_samples = std::max(1, static_cast<int>(std::lround(
553 units::ms_to_samples(length_ms_, sample_rate_))));
554 const int predelay_samples = std::max(0, static_cast<int>(std::lround(
555 units::ms_to_samples(predelay_ms_, sample_rate_))));
556 bank_window_[bank] = window_samples;
557 bank_predelay_[bank] = predelay_samples;
558
559 const double gate_hold = gate_hold_pct_ / 100.0;
560 const double attack = attack_pct_ / 100.0;
561 for (int ch = 0; ch < 2; ++ch) {
562 const std::uint32_t seed = ch == 0 ? seed_ : (seed_ ^ na::kSeedRodd);
563 Tap* out = taps_[bank][ch].data();
564 int count = 0;
565 double l1 = 0.0;
566
567 double t = 0.0; // position inside the window, in samples
568 int index = 0;
569 while (t < static_cast<double>(window_samples) && count < tap_capacity_) {
570 const double u = t / static_cast<double>(window_samples);
571 const double nd = na::pulse_density(u, density_pct_, density_growth_);
572 const double grid = sample_rate_ / nd; // Td, in samples
573
574 const auto design = na::design_velvet_tap(
575 program_, t, grid, window_samples, predelay_samples,
576 gate_hold, attack,
577 velvet_noise_draw<double>(seed, static_cast<std::uint64_t>(index)));
578
579 // A tap whose envelope is exactly zero is past a hard gate. It
580 // is dropped rather than stored, which is what makes the "IR is
581 // zero after the last tap" property meaningful: the last stored
582 // tap is the last audible one.
583 if (design.audible) {
584 // sqrt(Td) density weighting — see the file note. It makes
585 // the short-time RMS envelope equal E(τ) regardless of how
586 // the density law varies underneath it.
587 const double magnitude = design.magnitude;
588 out[count].delay = design.delay;
589 out[count].gain = static_cast<float>(design.gain);
590 out[count].segment = design.segment;
591 l1 += magnitude;
592 ++count;
593 }
594
595 t += grid;
596 ++index;
597 }
598
599 // §4.4: solve `norm` so Σ|g_k| == G_L1 exactly. That single
600 // constraint sets both the loudness and the peak-gain bound.
601 const double norm = l1 > na::kNormFloor ? na::kL1Budget / l1 : 0.0;
602 for (int k = 0; k < count; ++k)
603 out[k].gain = static_cast<float>(static_cast<double>(out[k].gain) * norm);
604
605 tap_count_[bank][ch] = count;
606 tap_norm_[bank][ch] = norm;
607 }
608 }
609
610 struct IncrementalTopologyBuild {
611 bool active = false;
612 int bank = 0;
613 int channel = 0;
614 bool normalizing = false;
615 int window_samples = 1;
616 int predelay_samples = 0;
617 int index = 0;
618 int count = 0;
619 int normalize_index = 0;
620 double t = 0.0;
621 double l1 = 0.0;
622 double norm = 0.0;
624 double density_pct = nonlin_ambience::kDensityRefPct;
625 double density_growth = nonlin_ambience::kGammaDefault;
626 double gate_hold = nonlin_ambience::kGateHold;
627 double attack = nonlin_ambience::kRevRise;
628 };
629
630 void begin_incremental_topology_rebuild() {
631 auto& b = topology_build_;
632 b = {};
633 b.active = true;
634 b.bank = 1 - front_;
635 b.window_samples = std::max(1, static_cast<int>(std::lround(
636 units::ms_to_samples(length_ms_, sample_rate_))));
637 b.predelay_samples = std::max(0, static_cast<int>(std::lround(
638 units::ms_to_samples(predelay_ms_, sample_rate_))));
639 b.program = program_;
640 b.density_pct = density_pct_;
641 b.density_growth = density_growth_;
642 b.gate_hold = gate_hold_pct_ / 100.0;
643 b.attack = attack_pct_ / 100.0;
644 bank_window_[b.bank] = b.window_samples;
645 bank_predelay_[b.bank] = b.predelay_samples;
646 }
647
648 void finish_incremental_channel() {
649 auto& b = topology_build_;
650 tap_count_[b.bank][b.channel] = b.count;
651 tap_norm_[b.bank][b.channel] = b.norm;
652 if (++b.channel < 2) {
653 b.normalizing = false;
654 b.index = 0;
655 b.count = 0;
656 b.normalize_index = 0;
657 b.t = 0.0;
658 b.l1 = 0.0;
659 b.norm = 0.0;
660 return;
661 }
662 seg_state_[b.bank][0].fill(SampleType{0});
663 seg_state_[b.bank][1].fill(SampleType{0});
664 pending_ = b.bank;
665 fade_pos_ = 0;
666 fading_ = true;
667 b.active = false;
668 topology_dirty_ = false;
669 ++topology_rebuild_count_;
670 }
671
672 void advance_incremental_topology_unit() {
673 namespace na = nonlin_ambience;
674 auto& b = topology_build_;
675 Tap* out = taps_[b.bank][b.channel].data();
676 if (b.normalizing) {
677 if (b.normalize_index < b.count) {
678 auto& gain = out[b.normalize_index++].gain;
679 gain = static_cast<float>(static_cast<double>(gain) * b.norm);
680 } else {
681 finish_incremental_channel();
682 }
683 return;
684 }
685 if (b.t >= static_cast<double>(b.window_samples) || b.count >= tap_capacity_) {
686 b.norm = b.l1 > na::kNormFloor ? na::kL1Budget / b.l1 : 0.0;
687 b.normalizing = true;
688 b.normalize_index = 0;
689 return;
690 }
691 const double u = b.t / static_cast<double>(b.window_samples);
692 const double nd = na::pulse_density(u, b.density_pct, b.density_growth);
693 const double grid = sample_rate_ / nd;
694 const std::uint32_t channel_seed =
695 b.channel == 0 ? seed_ : (seed_ ^ na::kSeedRodd);
696 const auto design = na::design_velvet_tap(
697 b.program, b.t, grid, b.window_samples, b.predelay_samples,
698 b.gate_hold, b.attack,
699 velvet_noise_draw<double>(channel_seed, static_cast<std::uint64_t>(b.index)));
700 if (design.audible) {
701 const double magnitude = design.magnitude;
702 out[b.count].delay = design.delay;
703 out[b.count].gain = static_cast<float>(design.gain);
704 out[b.count].segment = design.segment;
705 b.l1 += magnitude;
706 ++b.count;
707 }
708 b.t += grid;
709 ++b.index;
710 }
711
712 void advance_topology_rebuild() {
713 topology_work_units_last_sample_ = 0;
714 if (fading_) return;
715 if (!topology_build_.active) {
716 if (!topology_dirty_) return;
717 begin_incremental_topology_rebuild();
718 }
719 for (int i = 0; i < kTopologyWorkPerSample && topology_build_.active; ++i) {
720 advance_incremental_topology_unit();
721 ++topology_work_units_last_sample_;
722 }
723 }
724
725 // ── Segment tilt ──────────────────────────────────────────────────────
726
727 void update_segment_coefficients() {
728 namespace na = nonlin_ambience;
729 constexpr double kTwoPi = 6.283185307179586476925286766559;
730 const double bright = na::kFcBright;
731 const double dark = hf_damp_hz_;
732 for (int s = 0; s < na::kSegments; ++s) {
733 const double q = std::clamp(
734 static_cast<double>(s) / static_cast<double>(na::kSegments - 1) -
735 0.5 * tone_,
736 0.0, 1.0);
737 const double fc = bright * std::pow(dark / bright, q);
738 // §4.5's stated coefficient: the impulse-invariant pole.
739 seg_pole_[static_cast<std::size_t>(s)] = static_cast<SampleType>(
740 std::exp(-kTwoPi * fc / sample_rate_));
741 }
742 }
743
744 // ── Small helpers ─────────────────────────────────────────────────────
745
746 static std::size_t next_pow2(std::size_t n) {
747 std::size_t p = 1;
748 while (p < n) p <<= 1;
749 return p;
750 }
751
752 static bool is_prime(int n) {
753 if (n < 2) return false;
754 if (n % 2 == 0) return n == 2;
755 for (int d = 3; d * d <= n; d += 2)
756 if (n % d == 0) return false;
757 return true;
758 }
759
762 static int nearest_prime(int n) {
763 for (int offset = 0; offset < 64; ++offset) {
764 if (is_prime(n + offset)) return n + offset;
765 if (n - offset > 1 && is_prime(n - offset)) return n - offset;
766 }
767 return n;
768 }
769
770 // ── State ─────────────────────────────────────────────────────────────
771
772 double sample_rate_ = 48000.0;
773 double max_length_ms_ = nonlin_ambience::kMaxLengthMs;
774 double max_predelay_ms_ = nonlin_ambience::kMaxPredelayMs;
775
777 double length_ms_ = 350.0;
778 double predelay_ms_ = 0.0;
779 double density_pct_ = nonlin_ambience::kDensityRefPct;
780 double density_growth_ = nonlin_ambience::kGammaDefault;
781 double diffusion_ = nonlin_ambience::kDiffusionDefault;
782 double gate_hold_pct_ = nonlin_ambience::kGateHold * 100.0;
783 double attack_pct_ = nonlin_ambience::kRevRise * 100.0;
784 std::uint32_t seed_ = nonlin_ambience::kDefaultSeed;
785 double tone_ = 0.0;
786 double hf_damp_hz_ = nonlin_ambience::kFcDark;
787
788 int tap_capacity_ = 0;
789
790 std::vector<Tap> taps_[2][2];
791 int tap_count_[2][2] = {{0, 0}, {0, 0}};
792 double tap_norm_[2][2] = {{0.0, 0.0}, {0.0, 0.0}};
793 // Geometry is stored PER BANK so the introspection accessors can describe
794 // the bank being heard rather than the most recent `set_*`.
795 int bank_window_[2] = {0, 0};
796 int bank_predelay_[2] = {0, 0};
797
798 std::vector<SampleType> ring_;
799 int ring_mask_ = 0;
800 int ring_write_ = 0;
801
802 DelayLineT<SampleType> allpass_[nonlin_ambience::kNumAllpass];
803 int allpass_len_[nonlin_ambience::kNumAllpass] = {0, 0};
804
805 std::array<SampleType, nonlin_ambience::kSegments> seg_pole_{};
806 std::array<std::array<SampleType, nonlin_ambience::kSegments>, 2> seg_state_[2]{};
807
808 int front_ = 0;
809 int pending_ = 0;
810 bool fading_ = false;
811 int fade_pos_ = 0;
812 std::uint64_t topology_rebuild_count_ = 0;
813 bool topology_dirty_ = false;
814 IncrementalTopologyBuild topology_build_{};
815 int topology_work_units_last_sample_ = 0;
816 int swap_fade_len_ = 1;
817
818 SmoothedValue<SampleType> mix_{SampleType{1}};
819 SmoothedValue<SampleType> width_{SampleType{1}};
820 SmoothedValue<SampleType> output_gain_{SampleType{1}};
821 SmoothedValue<SampleType> converter_{SampleType{0}};
822
823 TptFilterT<SampleType> conv_hp_[2];
824 TptFilterT<SampleType> conv_lp_[2];
825 DcBlocker<SampleType> conv_dc_[2];
826 Xorshift32 dither_{0x2C1B3A5Du};
827};
828
831
832} // namespace pulp::signal
The nonlin / gated ambience engine. Stereo, processed in place.
Definition nonlin_ambience.hpp:14
double tap_norm(int channel) const
The L1 normalization scalar solved for channel's live tap table, so a caller can predict |g_k| withou...
Definition nonlin_ambience.hpp:436
void set_width_pct(double pct)
0 % is mono (both channels exactly the mid signal), 100 % is the two independent velvet realizations.
Definition nonlin_ambience.hpp:316
double envelope(double tau) const
The normalized envelope this instance's current settings ask for.
Definition nonlin_ambience.hpp:464
void set_seed(std::uint32_t seed)
Seed for the L channel; R derives as seed ⊕ kSeedRodd.
Definition nonlin_ambience.hpp:279
void set_converter_amount(double amount)
0 disables the converter stage entirely.
Definition nonlin_ambience.hpp:322
int predelay_samples() const
Definition nonlin_ambience.hpp:446
void set_length_ms(double ms)
Window length in ms — the absolute time the dimensionless envelope is stretched across.
Definition nonlin_ambience.hpp:140
double worst_case_gain() const
The worst-case gain for the current settings, as a closed form of the shipped constants (series law 8...
Definition nonlin_ambience.hpp:451
void set_mix_pct(double pct)
Dry/wet, 0 .. 100 %. The default is 100 (send style).
Definition nonlin_ambience.hpp:334
int allpass_length(int index) const
Definition nonlin_ambience.hpp:447
int window_samples() const
Window length in samples — the denominator of every τ.
Definition nonlin_ambience.hpp:445
void process_sample(SampleType &left, SampleType &right)
One stereo frame, in place. Allocation-free, lock-free, branch-light.
Definition nonlin_ambience.hpp:342
void set_program(NonlinProgram program)
Definition nonlin_ambience.hpp:132
void request_topology(NonlinProgram program, double length_ms, double predelay_ms, double density_pct, double density_growth, double gate_hold_pct, double attack_pct)
RT-bounded counterpart used by hosted automation.
Definition nonlin_ambience.hpp:236
static constexpr int kTopologyWorkPerSample
Definition nonlin_ambience.hpp:19
void reset()
Clears every state.
Definition nonlin_ambience.hpp:101
void set_gate_hold_pct(double pct)
The Gated program's hold point, as a percent of the window.
Definition nonlin_ambience.hpp:175
NonlinProgram program() const
Definition nonlin_ambience.hpp:456
void set_topology(NonlinProgram program, double length_ms, double predelay_ms, double density_pct, double density_growth, double gate_hold_pct, double attack_pct)
Atomically applies the complete topology snapshot and regenerates the inactive tap bank at most once.
Definition nonlin_ambience.hpp:196
void set_attack_pct(double pct)
The Reverse program's rise point and the NonLin2 program's gate point, as a percent of the window.
Definition nonlin_ambience.hpp:185
void set_output_gain_db(double db)
Definition nonlin_ambience.hpp:327
void set_density_growth(double gamma)
Definition nonlin_ambience.hpp:166
void set_diffusion(double g)
Allpass coefficient.
Definition nonlin_ambience.hpp:287
double length_ms() const
Definition nonlin_ambience.hpp:457
std::uint64_t topology_rebuild_count() const
Definition nonlin_ambience.hpp:274
void set_hf_damp_hz(double hz)
Definition nonlin_ambience.hpp:306
const Tap & tap(int channel, int index) const
Tap index on channel.
Definition nonlin_ambience.hpp:430
int tap_count(int channel) const
Number of taps currently live on channel (0 = L, 1 = R).
Definition nonlin_ambience.hpp:426
int topology_work_units_last_sample() const
Definition nonlin_ambience.hpp:275
void set_predelay_ms(double ms)
Definition nonlin_ambience.hpp:148
void set_density_pct(double pct)
Scales Nd_min only; Nd_max is fixed so the late field stays above the smoothness floor no matter how ...
Definition nonlin_ambience.hpp:158
void process(SampleType *left, SampleType *right, int num_samples)
A block of stereo frames, in place.
Definition nonlin_ambience.hpp:414
bool swap_in_progress() const
Definition nonlin_ambience.hpp:459
void prepare(double sample_rate, double max_length_ms=nonlin_ambience::kMaxLengthMs, double max_predelay_ms=nonlin_ambience::kMaxPredelayMs)
Allocates the tap ring and both tap banks.
Definition nonlin_ambience.hpp:34
double tone() const
Definition nonlin_ambience.hpp:458
static constexpr int latency_samples()
Series law 5: exactly zero.
Definition nonlin_ambience.hpp:128
void set_tone(double tone)
Spectral tilt T ∈ [−1, +1]: positive keeps highs later (brighter), negative darkens.
Definition nonlin_ambience.hpp:298
Deterministic TPDF dither and bounded error-feedback quantization.
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
@ d
Definition pitch.hpp:22
@ b
Definition pitch.hpp:31
@ dark
Definition environment.hpp:38
@ index
Definition gpu_pass.hpp:140
@ back
Definition gpu_pass.hpp:283
constexpr double kTwoPi
Definition lfo_sampling.hpp:77
The calibration register: every number this module ships that is not cited to the literature lives he...
Definition nonlin_ambience_design.hpp:253
constexpr double kRevRise
Reverse program: normalized rise point r and swell exponent p (p < 1 concave, p > 1 convex).
Definition nonlin_ambience_design.hpp:305
constexpr double kMinDensityPct
Lower bound of the density control's documented 10 .. 100 % range.
Definition nonlin_ambience_design.hpp:379
constexpr double kMaxLengthMs
Definition nonlin_ambience_design.hpp:375
constexpr double kFcDark
Definition nonlin_ambience_design.hpp:328
double worst_case_gain(double diffusion=kDiffusionDefault, bool converter_on=false)
The shipped worst-case gain as a closed form of the shipped constants: Π_i (1 + 2·g_i) · G_L1,...
Definition nonlin_ambience_design.hpp:400
constexpr double kDensityRefPct
The density_pct value at which Nd_min equals kNdMin exactly, so the shipped default reproduces the sh...
Definition nonlin_ambience_design.hpp:280
constexpr double kGammaDefault
Density growth exponent γ.
Definition nonlin_ambience_design.hpp:286
constexpr double kGateHold
Gated program: normalized hold point h and fall width w.
Definition nonlin_ambience_design.hpp:299
constexpr double kMinLengthMs
Largest window and pre-delay prepare() sizes storage for, in ms.
Definition nonlin_ambience_design.hpp:374
constexpr double kMaxPredelayMs
Definition nonlin_ambience_design.hpp:376
constexpr std::uint32_t kDefaultSeed
Default seed_L.
Definition nonlin_ambience_design.hpp:348
constexpr double kDiffusionMax
Definition nonlin_ambience_design.hpp:267
constexpr int kNumAllpass
Number of pre-diffusion Schroeder allpasses.
Definition nonlin_ambience_design.hpp:257
double program_envelope(NonlinProgram program, double tau, double gate_hold=kGateHold, double attack=kRevRise)
The dimensionless program envelope E(τ), τ ∈ [0, 1] (series law 7 — scale invariant,...
Definition nonlin_ambience_design.hpp:413
constexpr int kSegments
Number of time segments the tap range is split into for spectral tilt.
Definition nonlin_ambience_design.hpp:323
constexpr double kDiffusionDefault
Allpass coefficient, exposed as diffusion.
Definition nonlin_ambience_design.hpp:266
T ms_to_samples(T ms, T sample_rate)
Milliseconds → samples at sample_rate Hz.
Definition units.hpp:147
T db_to_linear(T db)
Amplitude dB → linear gain. 0 dB → 1, −6.0206 dB → 0.5.
Definition units.hpp:71
Definition additive_bank.hpp:218
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ delay
Definition envelope.hpp:57
@ hold
Definition envelope.hpp:59
@ attack
Definition envelope.hpp:58
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ EqualPower
(cos(u·π/2), sin(u·π/2)) — power sum == 1.
Definition crossfade.hpp:43
@ EqualGain
(1−u, u) — amplitude sum == 1.
Definition crossfade.hpp:42
constexpr T crossfade_smoothstep(T t) noexcept
Smoothstep progress shaping: cubic 3u²−2u³ with zero slope at both ends, clamped to [0,...
Definition crossfade.hpp:50
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
NonlinAmbienceT< double > NonlinAmbience64
Definition nonlin_ambience.hpp:830
void crossfade_gains(T u, CrossfadeGainLaw law, T &old_gain, T &new_gain) noexcept
Old/new blend gains for an ALREADY-SHAPED progress u (the caller applies any smoothstep/clamp first).
Definition crossfade.hpp:61
NonlinProgram
The four programs.
Definition nonlin_ambience_design.hpp:243
@ ambience
Short falling cloud — the "natural" one.
Definition nonlin_ambience_design.hpp:244
VelvetNoiseDrawT< SampleType > velvet_noise_draw(std::uint64_t seed, std::uint64_t index) noexcept
Pure draw for one velvet grid cell.
Definition velvet_noise.hpp:26
NonlinAmbienceT< float > NonlinAmbience
Definition nonlin_ambience.hpp:829
constexpr SampleType tpdf_difference(SampleType first, SampleType second) noexcept
Converts two independent uniform draws in [0, 1) to unit-peak TPDF noise.
Definition dither.hpp:32
@ grid
Definition geometry.hpp:348
@ s
Definition input_events.hpp:41
@ y
Definition input_events.hpp:42
@ q
Definition input_events.hpp:41
@ n
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ t
Definition input_events.hpp:41
The nonlin / gated ambience engine — a reverb whose envelope is DESIGNED rather than decayed.
One velvet tap: where to read, how much to scale it, and which spectral segment it belongs to.
Definition nonlin_ambience.hpp:23
float gain
Definition nonlin_ambience.hpp:25
int delay
Definition nonlin_ambience.hpp:24
int segment
Definition nonlin_ambience.hpp:26