Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
loop_eq.hpp
Go to the documentation of this file.
1#pragma once
2
3// In-loop tone shaping: a 10-band EQ whose coefficients are shared across the
4// 16 lines, and the per-channel "flux" peak that wanders.
5//
6// Two properties make this safe to put INSIDE a recursion:
7//
8// * Shared coefficients, per-channel state. Sixteen identical filters would
9// otherwise mean sixteen coefficient recomputes per control tick, and the
10// recompute (pow, cos, sqrt) dwarfs the three multiply-adds that actually
11// filter. One design, sixteen state pairs.
12// * The flux peak is ABSORPTIVE ONLY. A boost re-applied on every pass is a
13// per-pass gain above 1, which the stability normalization would have to pay
14// for by pulling the loop gain down — and that would shorten the tail
15// whenever flux happened to be peaking, an audible decay pumping. Letting
16// the wandering peak only ever CUT keeps its worst-case contribution at
17// exactly 1, so it colours the tail without lengthening or shortening it.
18// (A boost compensated by an equal broadband trim would be worse than
19// either: the trim is itself a per-pass loss on everything else.)
20//
21// The static EQ has no such normalization on purpose: it is an explicit
22// authoring choice, and a user who dials +6 dB into the loop should get a
23// longer tail at that frequency — bounded by the stability normalization
24// trading broadband decay for it. That trade is the honest one; the flux
25// motion layer is the one that must be free.
26//
27// RT contract: configure() runs the filter design (control rate, never
28// per-sample); process/reset allocate nothing.
29
33#include <pulp/signal/detail/simd_lanes.hpp>
38
39#include <algorithm>
40#include <array>
41#include <cmath>
42#include <cstdint>
43
44namespace pulp::signal::fdn {
45
46namespace lanes = signal::detail::lanes;
47
48// One EQ band as authored. Bands are set through the engine's API rather than
49// through baked params: they are a voicing decision baked into a mode or a
50// preset, not a knob the host automates.
51struct EqBand {
52 double freq_hz = 1000.0;
53 double gain_db = 0.0;
54 double q = 0.7;
55};
56
57// Default layout: one low shelf, eight peaks spread logarithmically, one high
58// shelf — all flat until authored.
59inline std::array<EqBand, kNumEqBands> default_eq_bands() {
60 std::array<EqBand, kNumEqBands> bands{};
61 bands[0] = {120.0, 0.0, 0.7};
62 for (int i = 1; i <= 8; ++i) {
63 // 250 Hz .. 8 kHz, geometric.
64 const double t = static_cast<double>(i - 1) / 7.0;
65 bands[static_cast<std::size_t>(i)] = {250.0 * std::pow(32.0, t), 0.0, 1.0};
66 }
67 bands[9] = {9000.0, 0.0, 0.7};
68 return bands;
69}
70
71// The 10-band bank. Coefficients live once; every channel carries only its two
72// state words per band.
73template <typename SampleType = float>
74class LoopEq {
75public:
76 LoopEq() : bands_(default_eq_bands()) {}
77
78 void set_band(int index, const EqBand& band) {
79 if (index < 0 || index >= kNumEqBands) return;
80 bands_[static_cast<std::size_t>(index)] = band;
81 }
82
83 const EqBand& band(int index) const {
84 return bands_[static_cast<std::size_t>(std::clamp(index, 0, kNumEqBands - 1))];
85 }
86
87 // Re-design every band for the tank rate. Centre frequencies are clamped
88 // below 0.45 x fs_tank: at 16 kHz an 8 kHz band would otherwise sit on
89 // Nyquist, where the bilinear transform's warping makes the section wild.
90 void configure(double tank_rate) {
91 const double max_hz = kAaCutoffFraction * tank_rate;
92 worst_case_boost_ = 1.0;
93 num_active_ = 0;
94 bool shelf_can_overshoot = false;
95 BiquadT<double> designer;
96 for (int b = 0; b < kNumEqBands; ++b) {
97 const EqBand& band = bands_[static_cast<std::size_t>(b)];
98 if (std::abs(band.gain_db) < kFlatBandDb) continue;
99 const auto type = (b == 0) ? BiquadT<double>::Type::low_shelf
100 : (b == kNumEqBands - 1)
103 designer.set_coefficients(type, std::min(band.freq_hz, max_hz),
104 std::max(band.q, 0.05), tank_rate,
105 band.gain_db);
106 coeffs_[static_cast<std::size_t>(num_active_)] = designer.coefficients();
107 ++num_active_;
108 if (band.gain_db > 0.0)
109 worst_case_boost_ *= std::pow(10.0, band.gain_db / 20.0);
110 if (type != BiquadT<double>::Type::peaking && std::max(band.q, 0.05) > kMonotonicShelfQ)
111 shelf_can_overshoot = true;
112 }
113 // A peaking band's maximum is its gain, so the product above bounds a
114 // bank of peaks and plain shelves. A shelf with Q above 1/sqrt(2)
115 // overshoots its own gain near the corner (and a cut shelf rises above
116 // unity), so then the bound also takes the cascade's measured peak.
117 if (shelf_can_overshoot)
118 worst_case_boost_ = std::max(worst_case_boost_, measured_peak(tank_rate));
119 reset();
120 }
121
122 void reset() {
123 for (auto& s : state_) s = {};
124 }
125
126 // Conservative upper bound on the bank's magnitude response: the product of
127 // every active band's linear boost, raised to the cascade's measured peak
128 // when a shelf's Q lets it overshoot. The stability normalization that
129 // consumes it is never optimistic.
130 double worst_case_boost() const { return worst_case_boost_; }
131
132 // Bands that are not flat — the ones process() actually runs.
133 int active_bands() const {
134 return num_active_;
135 }
136
137 SampleType process(int channel, SampleType x) {
138 for (int k = 0; k < num_active_; ++k) {
139 const auto& c = coeffs_[static_cast<std::size_t>(k)];
140 x = static_cast<SampleType>(
141 state_[state_index(k, channel)].process(c, static_cast<double>(x)));
142 }
143 return x;
144 }
145
146private:
147 // A band is treated as flat (and skipped entirely) below this, so an
148 // untouched EQ costs nothing per sample.
149 static constexpr double kFlatBandDb = 0.01;
150 // The steepest RBJ shelf whose response stays monotonic (slope S = 1).
151 static constexpr double kMonotonicShelfQ = 0.7071067811865476;
152
153 // Peak |H| of the active cascade on [0, Nyquist]: a log grid, every band
154 // centre, then a golden-section refinement around the largest sample. The
155 // refinement and a 0.1% margin keep a narrow peak between grid points from
156 // making the bound optimistic. Control rate only; allocates nothing.
157 double measured_peak(double tank_rate) const {
158 const std::span<const BiquadCoefficientsT<double>> sos(
159 coeffs_.data(), static_cast<std::size_t>(num_active_));
160 auto mag = [&](double omega) { return cascade_magnitude(sos, omega); };
161 constexpr int kGrid = 512;
162 constexpr double kLowest = 1.0e-4; // radians/sample, about 0.5 Hz at 32 kHz
163 const double pi = 3.14159265358979323846;
164 double best = std::max(mag(0.0), mag(pi));
165 double best_omega = 0.0;
166 double step = std::pow(pi / kLowest, 1.0 / (kGrid - 1));
167 double omega = kLowest;
168 for (int i = 0; i < kGrid; ++i, omega *= step) {
169 const double m = mag(omega);
170 if (m > best) {
171 best = m;
172 best_omega = omega;
173 }
174 }
175 for (int b = 0; b < kNumEqBands; ++b) {
176 const double centre =
177 angular_frequency(bands_[static_cast<std::size_t>(b)].freq_hz, tank_rate);
178 const double m = mag(centre);
179 if (m > best) {
180 best = m;
181 best_omega = centre;
182 }
183 }
184 if (best_omega > 0.0) {
185 double lo = std::max(best_omega / step, 0.0);
186 double hi = std::min(best_omega * step, pi);
187 constexpr double kRatio = 0.6180339887498949;
188 for (int i = 0; i < 40; ++i) {
189 const double a = hi - kRatio * (hi - lo);
190 const double c = lo + kRatio * (hi - lo);
191 if (mag(a) > mag(c))
192 hi = c;
193 else
194 lo = a;
195 }
196 best = std::max(best, mag(0.5 * (lo + hi)));
197 }
198 return best * 1.001;
199 }
200
201 static std::size_t state_index(int active_slot, int channel) {
202 return static_cast<std::size_t>(active_slot) *
203 static_cast<std::size_t>(kNumChannels) +
204 static_cast<std::size_t>(channel);
205 }
206
207 std::array<EqBand, kNumEqBands> bands_{};
208 std::array<BiquadCoefficientsT<double>, kNumEqBands> coeffs_{};
209 std::array<SanitizedBiquadState,
210 static_cast<std::size_t>(kNumEqBands) * kNumChannels> state_{};
211 int num_active_ = 0;
212 double worst_case_boost_ = 1.0;
213};
214
215// The flux layer: one extra peaking biquad per channel whose centre frequency
216// and depth both wander. Coefficients are recomputed at the control rate and
217// the filter ALWAYS runs, even at zero depth — switching a biquad in and out of
218// a feedback path pops, and the pop recirculates.
219//
220// The peak is ABSORPTIVE ONLY: its gain wanders in [-kFluxMaxDb, 0], never
221// above flat. That is what lets it contribute exactly 1 to the worst-case
222// bound, which in turn is what keeps the Jot decay law exact — see the header.
223// It also means flux never lengthens a tail; it takes narrow, wandering bites
224// out of it, and because each of the 16 lines bites in its own register the
225// aggregate decay is unchanged while the colour never sits still.
226//
227// Coefficients and state are stored per field across the sixteen channels so
228// both the per-tick redesign and the per-sample filter run as one pass over all
229// channels: the redesign evaluates the RBJ peaking formula with polynomial
230// sin/cos/exp that are exact to double rounding over the argument ranges the
231// bank can produce, instead of three libm calls per channel per tick.
232template <typename SampleType = float>
233class FluxBank {
234public:
236
237 void reset() {
238 for (int i = 0; i < kNumChannels; ++i) {
239 rng_[i].reset(kSeedBase + static_cast<std::uint32_t>(i) * kSeedStride);
240 target_hz_[i] = base_hz_[i];
241 current_hz_[i] = base_hz_[i];
242 target_db_[i] = 0.0;
243 current_db_[i] = 0.0;
244 countdown_[i] = interval_samples(i);
245 s1_[i] = 0.0;
246 s2_[i] = 0.0;
247 }
248 redesign();
249 }
250
251 // Base frequencies are spread logarithmically across the channels, so the
252 // sixteen lines each shimmer in their own register rather than all pumping
253 // the same band.
254 void configure(double tank_rate, double depth_db) {
255 tank_rate_ = tank_rate;
256 depth_db_ = std::clamp(depth_db, 0.0, kFluxMaxDb);
257 const double max_hz = kAaCutoffFraction * tank_rate;
258 for (int i = 0; i < kNumChannels; ++i) {
259 const double t = (kNumChannels > 1)
260 ? static_cast<double>(i) /
261 static_cast<double>(kNumChannels - 1)
262 : 0.0;
263 base_hz_[i] = std::min(
264 kFluxBaseMinHz * std::pow(kFluxBaseMaxHz / kFluxBaseMinHz, t), max_hz);
265 target_hz_[i] = std::clamp(target_hz_[i], kFluxBaseMinHz * 0.5, max_hz);
266 current_hz_[i] = std::clamp(current_hz_[i], kFluxBaseMinHz * 0.5, max_hz);
267 }
268 // One-pole glide toward the re-target, in the log domain.
269 glide_ = std::exp(-1.0 / std::max(1.0, kFluxGlideMs * 0.001 * tank_rate /
271 redesign();
272 }
273
274 // One control-rate tick: re-target the channels whose timer expired, glide
275 // the rest, and redesign.
276 void tick_control(int samples_elapsed) {
277 for (int i = 0; i < kNumChannels; ++i) {
278 countdown_[i] -= samples_elapsed;
279 if (countdown_[i] <= 0) {
280 const double octaves = rng_[i].next_bipolar() * kFluxSpreadOctaves;
281 target_hz_[i] = base_hz_[i] * std::pow(2.0, octaves);
282 target_db_[i] = -rng_[i].next_unit() * depth_db_;
283 countdown_[i] = interval_samples(i);
284 }
285 }
286 for (int i = 0; i < kNumChannels; ++i) {
287 current_hz_[i] += (target_hz_[i] - current_hz_[i]) * (1.0 - glide_);
288 current_db_[i] += (target_db_[i] - current_db_[i]) * (1.0 - glide_);
289 }
290 redesign();
291 }
292
293 // Always exactly 1: the wandering peak is absorptive only, so no pass can
294 // ever be louder than flat (see the class note).
295 double worst_case_boost() const { return 1.0; }
296
297 // The designed section for one channel, normalized to a0 == 1.
299 const auto c = static_cast<std::size_t>(channel);
300 return {b0_[c], b1_[c], b2_[c], a1_[c], a2_[c]};
301 }
302
303 // One channel, one sample, with every state word sanitized — the
304 // defensive scalar path.
305 SampleType process(int channel, SampleType x) {
306 const auto c = static_cast<std::size_t>(channel);
307 const double in = finite_or_zero(static_cast<double>(x));
308 const double out = finite_or_zero(b0_[c] * in + s1_[c]);
309 s1_[c] = finite_or_zero(snap_to_zero(b1_[c] * in - a1_[c] * out + s2_[c]));
310 s2_[c] = finite_or_zero(snap_to_zero(b2_[c] * in - a2_[c] * out));
311 return static_cast<SampleType>(out);
312 }
313
314 // The bank's coefficients and state as two-lane vectors (channels 2i and
315 // 2i + 1 in lane pair i), for a block kernel to hold in local storage
316 // across a block: load_lanes() before, store_lanes() after.
317 struct Lanes {
318 lanes::D2 b0[kNumChannels / 2];
319 lanes::D2 b1[kNumChannels / 2];
320 lanes::D2 b2[kNumChannels / 2];
321 lanes::D2 a1[kNumChannels / 2];
322 lanes::D2 a2[kNumChannels / 2];
323 lanes::D2 s1[kNumChannels / 2];
324 lanes::D2 s2[kNumChannels / 2];
325 };
326
327 void load_lanes(Lanes& l) const {
328 for (std::size_t p = 0; p < static_cast<std::size_t>(kNumChannels / 2); ++p) {
329 l.b0[p] = lanes::load(&b0_[2 * p]);
330 l.b1[p] = lanes::load(&b1_[2 * p]);
331 l.b2[p] = lanes::load(&b2_[2 * p]);
332 l.a1[p] = lanes::load(&a1_[2 * p]);
333 l.a2[p] = lanes::load(&a2_[2 * p]);
334 l.s1[p] = lanes::load(&s1_[2 * p]);
335 l.s2[p] = lanes::load(&s2_[2 * p]);
336 }
337 }
338
339 void store_lanes(const Lanes& l) {
340 for (std::size_t p = 0; p < static_cast<std::size_t>(kNumChannels / 2); ++p) {
341 lanes::store(&s1_[2 * p], l.s1[p]);
342 lanes::store(&s2_[2 * p], l.s2[p]);
343 }
344 }
345
346 // Every channel, one sample, in place, in the tank's lane layout: v[j]
347 // holds channels 4j .. 4j + 3. The tank's block path. Input is finite and
348 // bounded by the tank's construction (the damping stage clamps it), so the
349 // recursion runs bare and heal() snaps and sanitizes the state once per
350 // block (after store_lanes()).
351 static void process_lanes(Lanes& l, lanes::F4 (&v)[kNumChannels / 4]) {
352 using lanes::D2;
353 for (std::size_t j = 0; j < static_cast<std::size_t>(kNumChannels / 4); ++j) {
354 const D2 in[2] = {lanes::widen_low(v[j]), lanes::widen_high(v[j])};
355 D2 out[2];
356 for (std::size_t h = 0; h < 2; ++h) {
357 const std::size_t p = 2 * j + h;
358 const D2 y = l.b0[p] * in[h] + l.s1[p];
359 l.s1[p] = l.b1[p] * in[h] - l.a1[p] * y + l.s2[p];
360 l.s2[p] = l.b2[p] * in[h] - l.a2[p] * y;
361 out[h] = y;
362 }
363 v[j] = lanes::narrow(out[0], out[1]);
364 }
365 }
366
367 // Snap decayed state to zero and zero any channel whose state left the
368 // finite range. Once per block of at most a control period.
369 void heal() {
370 for (std::size_t c = 0; c < static_cast<std::size_t>(kNumChannels); ++c) {
371 if (!std::isfinite(s1_[c]) || !std::isfinite(s2_[c])) {
372 s1_[c] = 0.0;
373 s2_[c] = 0.0;
374 }
375 s1_[c] = snap_to_zero(s1_[c]);
376 s2_[c] = snap_to_zero(s2_[c]);
377 }
378 }
379
380private:
381 static constexpr std::uint32_t kSeedBase = 0x5EED1u;
382 static constexpr std::uint32_t kSeedStride = 0x85EBCA6Bu;
383
384 int interval_samples(int channel) {
385 const double ms = kFluxIntervalMinMs +
386 rng_[channel].next_unit() *
388 return std::max(1, static_cast<int>(ms * 0.001 * tank_rate_));
389 }
390
391 // sin and cos of w on [0, pi]: shift to y = w - pi/2 in [-pi/2, pi/2] and
392 // evaluate the Taylor series of cos(y) and sin(y) through degree 18 and 17
393 // (truncation below 5e-14 there), so sin(w) = cos(y), cos(w) = -sin(y).
394 static void sin_cos_0_pi(double w, double& s, double& c) {
395 const double y = w - 1.5707963267948966;
396 const double y2 = y * y;
397 double cy = 1.0 / 6402373705728000.0; // 1/18!
398 cy = cy * y2 - 1.0 / 20922789888000.0; // 1/16!
399 cy = cy * y2 + 1.0 / 87178291200.0; // 1/14!
400 cy = cy * y2 - 1.0 / 479001600.0; // 1/12!
401 cy = cy * y2 + 1.0 / 3628800.0; // 1/10!
402 cy = cy * y2 - 1.0 / 40320.0; // 1/8!
403 cy = cy * y2 + 1.0 / 720.0; // 1/6!
404 cy = cy * y2 - 1.0 / 24.0;
405 cy = cy * y2 + 0.5;
406 cy = 1.0 - cy * y2;
407 double sy = 1.0 / 355687428096000.0; // 1/17!
408 sy = sy * y2 - 1.0 / 1307674368000.0; // 1/15!
409 sy = sy * y2 + 1.0 / 6227020800.0; // 1/13!
410 sy = sy * y2 - 1.0 / 39916800.0; // 1/11!
411 sy = sy * y2 + 1.0 / 362880.0; // 1/9!
412 sy = sy * y2 - 1.0 / 5040.0; // 1/7!
413 sy = sy * y2 + 1.0 / 120.0;
414 sy = sy * y2 - 1.0 / 6.0;
415 sy = y + y * y2 * sy;
416 s = cy;
417 c = -sy;
418 }
419
420 // 10^(gain_db / 40) for gain_db in [-kFluxMaxDb, 0]: the exponent is at
421 // most 0.18 in magnitude, where the Taylor series of exp through degree 10
422 // is exact to double rounding.
423 static double peaking_amplitude(double gain_db) {
424 const double x = gain_db * (2.302585092994046 / 40.0);
425 double e = 1.0 / 3628800.0;
426 e = e * x + 1.0 / 362880.0;
427 e = e * x + 1.0 / 40320.0;
428 e = e * x + 1.0 / 5040.0;
429 e = e * x + 1.0 / 720.0;
430 e = e * x + 1.0 / 120.0;
431 e = e * x + 1.0 / 24.0;
432 e = e * x + 1.0 / 6.0;
433 e = e * x + 0.5;
434 e = e * x + 1.0;
435 return e * x + 1.0;
436 }
437
438 // The RBJ peaking section (BiquadT::Type::peaking), normalized.
439 void redesign() {
440 const double max_hz = kAaCutoffFraction * tank_rate_;
441 const double to_w = 6.283185307179586 / tank_rate_;
442 for (std::size_t i = 0; i < static_cast<std::size_t>(kNumChannels); ++i) {
443 const double w0 = std::clamp(current_hz_[i], 20.0, max_hz) * to_w;
444 double sin_w0 = 0.0;
445 double cos_w0 = 0.0;
446 sin_cos_0_pi(w0, sin_w0, cos_w0);
447 const double alpha = sin_w0 / (2.0 * kFluxQ);
448 const double A = peaking_amplitude(std::clamp(current_db_[i], -kFluxMaxDb, 0.0));
449 const double inv_a0 = 1.0 / (1.0 + alpha / A);
450 b0_[i] = (1.0 + alpha * A) * inv_a0;
451 b1_[i] = -2.0 * cos_w0 * inv_a0;
452 b2_[i] = (1.0 - alpha * A) * inv_a0;
453 a1_[i] = b1_[i];
454 a2_[i] = (1.0 - alpha / A) * inv_a0;
455 }
456 }
457
458 std::array<XorShift32, kNumChannels> rng_{};
459 std::array<double, kNumChannels> b0_{};
460 std::array<double, kNumChannels> b1_{};
461 std::array<double, kNumChannels> b2_{};
462 std::array<double, kNumChannels> a1_{};
463 std::array<double, kNumChannels> a2_{};
464 std::array<double, kNumChannels> s1_{};
465 std::array<double, kNumChannels> s2_{};
466 std::array<double, kNumChannels> base_hz_{};
467 std::array<double, kNumChannels> target_hz_{};
468 std::array<double, kNumChannels> current_hz_{};
469 std::array<double, kNumChannels> target_db_{};
470 std::array<double, kNumChannels> current_db_{};
471 std::array<int, kNumChannels> countdown_{};
472 double tank_rate_ = 48000.0;
473 double depth_db_ = kFluxMaxDb;
474 double glide_ = 0.0;
475};
476
477} // namespace pulp::signal::fdn
Definition biquad.hpp:53
@ peaking
Definition biquad.hpp:58
@ high_shelf
Definition biquad.hpp:58
@ low_shelf
Definition biquad.hpp:58
void set_coefficients(const BiquadCoefficientsT< U > &c)
Definition biquad.hpp:69
Coefficients coefficients() const
Definition biquad.hpp:81
void store_lanes(const Lanes &l)
Definition loop_eq.hpp:339
SampleType process(int channel, SampleType x)
Definition loop_eq.hpp:305
void tick_control(int samples_elapsed)
Definition loop_eq.hpp:276
BiquadCoefficientsT< double > coefficients(int channel) const
Definition loop_eq.hpp:298
void configure(double tank_rate, double depth_db)
Definition loop_eq.hpp:254
void load_lanes(Lanes &l) const
Definition loop_eq.hpp:327
FluxBank()
Definition loop_eq.hpp:235
void heal()
Definition loop_eq.hpp:369
static void process_lanes(Lanes &l, lanes::F4(&v)[kNumChannels/4])
Definition loop_eq.hpp:351
double worst_case_boost() const
Definition loop_eq.hpp:295
void reset()
Definition loop_eq.hpp:237
void reset()
Definition loop_eq.hpp:122
double worst_case_boost() const
Definition loop_eq.hpp:130
void configure(double tank_rate)
Definition loop_eq.hpp:90
SampleType process(int channel, SampleType x)
Definition loop_eq.hpp:137
void set_band(int index, const EqBand &band)
Definition loop_eq.hpp:78
LoopEq()
Definition loop_eq.hpp:76
int active_bands() const
Definition loop_eq.hpp:133
const EqBand & band(int index) const
Definition loop_eq.hpp:83
Evaluate what a biquad (or a cascade of them) does to the spectrum.
constexpr state::ParamID kRatio
Definition forge_dynamics_catalog.hpp:83
Definition config.hpp:15
constexpr int kControlRateSamples
Definition config.hpp:187
constexpr double kFluxDefaultDb
Definition config.hpp:120
T finite_or_zero(T value)
Definition sanitize.hpp:12
constexpr double kFluxBaseMinHz
Definition config.hpp:121
constexpr double kFluxQ
Definition config.hpp:130
constexpr int kNumEqBands
Definition config.hpp:20
constexpr double kFluxBaseMaxHz
Definition config.hpp:122
constexpr int kNumChannels
Definition config.hpp:18
@ tank_rate
Definition config.hpp:226
constexpr double kAaCutoffFraction
Definition config.hpp:170
constexpr double kFluxIntervalMinMs
Definition config.hpp:124
constexpr double kFluxSpreadOctaves
Definition config.hpp:123
constexpr double kFluxGlideMs
Definition config.hpp:126
constexpr double kFluxMaxDb
Definition config.hpp:115
constexpr double kFluxIntervalMaxMs
Definition config.hpp:125
std::array< EqBand, kNumEqBands > default_eq_bands()
Definition loop_eq.hpp:59
double cascade_magnitude(std::span< const BiquadCoefficientsT< SampleType >, Extent > sos, double omega)
Linear magnitude |H| of a cascade at angular frequency ω.
Definition frequency_response.hpp:80
@ c
Definition spectrum_trace.hpp:35
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
double angular_frequency(double freq_hz, double sample_rate)
Angular frequency (radians/sample) of freq_hz at sample_rate, clamped to [0, π].
Definition frequency_response.hpp:97
Definition biquad.hpp:24
Definition loop_eq.hpp:51
double freq_hz
Definition loop_eq.hpp:52
double q
Definition loop_eq.hpp:54
double gain_db
Definition loop_eq.hpp:53
Definition loop_eq.hpp:317
lanes::D2 s2[kNumChannels/2]
Definition loop_eq.hpp:324
lanes::D2 a2[kNumChannels/2]
Definition loop_eq.hpp:322
lanes::D2 a1[kNumChannels/2]
Definition loop_eq.hpp:321
lanes::D2 s1[kNumChannels/2]
Definition loop_eq.hpp:323
lanes::D2 b1[kNumChannels/2]
Definition loop_eq.hpp:319
lanes::D2 b0[kNumChannels/2]
Definition loop_eq.hpp:318
lanes::D2 b2[kNumChannels/2]
Definition loop_eq.hpp:320