Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
biquad.hpp
Go to the documentation of this file.
1#pragma once
2
5#include <pulp/simd/lanes.hpp>
6
7#include <algorithm>
8#include <array>
9#include <cmath>
10#include <cstddef>
11#include <type_traits>
12
13namespace pulp::signal {
14
15// Coefficients of one normalized second-order section (a0 == 1).
16//
17// This is the canonical exchange type between the three layers that all speak
18// biquads: design (FilterDesign / IirDesign produce it), runtime (BiquadT
19// consumes it via set_coefficients and hands it back via coefficients()), and
20// analysis (frequency_response.hpp evaluates H(z) from it). Keeping one shared
21// struct is what lets a UI ask a live filter what it looks like instead of
22// re-deriving — or approximating — the response itself.
23template <typename SampleType = float>
25 SampleType b0 = 1, b1 = 0, b2 = 0;
26 SampleType a1 = 0, a2 = 0;
27};
28
30
33template <typename SampleType>
35 // Jury criterion for z^2 + a1*z + a2 with real coefficients.
36 if (!(std::abs(c.a2) < SampleType{1}))
37 return false;
38
39 // FastTwoSum is exact here because |1| >= |a2|. Keep the residual so a
40 // rounded bound equal to |a1| still observes the strict real inequality.
41 const SampleType rounded_bound = SampleType{1} + c.a2;
42 const SampleType bound_error = c.a2 - (rounded_bound - SampleType{1});
43 const SampleType a1_magnitude = std::abs(c.a1);
44 return a1_magnitude < rounded_bound
45 || (a1_magnitude == rounded_bound && bound_error > SampleType{0});
46}
47
48// Biquad IIR filter — standard second-order section.
49// RT contract: coefficient calculation and process/reset paths allocate no
50// memory. Retune coefficients at a block boundary if parameter continuity
51// matters.
52template <typename SampleType = float>
53class BiquadT {
54 static_assert(std::is_floating_point_v<SampleType>,
55 "BiquadT requires a floating-point sample type");
56
57public:
59
61
62 BiquadT() = default;
63
64 // Load pre-designed coefficients (from FilterDesign, IirDesign, or any
65 // other source). Accepts a section of any floating-point precision so a
66 // float design table can drive a double-precision filter and vice versa.
67 // Assumes the section is already normalized to a0 == 1.
68 template <typename U>
70 b0_ = static_cast<SampleType>(c.b0);
71 b1_ = static_cast<SampleType>(c.b1);
72 b2_ = static_cast<SampleType>(c.b2);
73 a1_ = static_cast<SampleType>(c.a1);
74 a2_ = static_cast<SampleType>(c.a2);
75 update_block_form();
76 }
77
78 // Read back the normalized coefficients currently in force. Lets analysis
79 // and UI code evaluate this exact filter's response (see
80 // frequency_response.hpp) rather than guessing at its shape.
81 Coefficients coefficients() const { return {b0_, b1_, b2_, a1_, a2_}; }
82
83 // Configure from type, frequency, Q, and optional gain (for peaking/shelf)
85 SampleType freq_hz,
86 SampleType q,
87 SampleType sample_rate,
88 SampleType gain_db = SampleType{0}) {
89 // Guard the inputs that would produce inf/NaN or unstable coefficients
90 // and wedge the filter state forever: sample_rate ≤ 0 (w0 = ∞), q ≤ 0
91 // (alpha = ∞ at q=0, or a0 = 1+alpha crossing 0 for negative q),
92 // non-finite freq/gain, and freq at or above Nyquist (sin w0 ≤ 0 makes
93 // alpha ≤ 0, so a pole lands on or outside the unit circle). The
94 // frequency ceiling is 0.49 x sample_rate, the margin the EQ designers
95 // already clamp to. Any finite positive q, however tiny, yields large
96 // but finite coefficients, so every legitimate retune stays
97 // bit-identical. Mirrors the input hardening in NoiseGate::set_params.
98 if (!(std::isfinite(sample_rate) && sample_rate > SampleType{0}))
99 sample_rate = SampleType{44100};
100 if (!(std::isfinite(q) && q > SampleType{0}))
101 q = SampleType{1e-4f};
102 if (!std::isfinite(gain_db)) gain_db = SampleType{0};
103 if (!(std::isfinite(freq_hz) && freq_hz > SampleType{0}))
104 freq_hz = SampleType{1000};
105 freq_hz = std::min(freq_hz, sample_rate * SampleType{0.49});
106
107 SampleType w0 = SampleType{2} * pi * freq_hz / sample_rate;
108 SampleType cos_w0 = std::cos(w0);
109 SampleType sin_w0 = std::sin(w0);
110 SampleType alpha = sin_w0 / (SampleType{2} * q);
111
112 SampleType a0 = SampleType{1};
113 SampleType a1 = SampleType{0};
114 SampleType a2 = SampleType{0};
115 SampleType b0 = SampleType{1};
116 SampleType b1 = SampleType{0};
117 SampleType b2 = SampleType{0};
118
119 switch (type) {
120 case Type::lowpass:
121 b0 = (SampleType{1} - cos_w0) / SampleType{2};
122 b1 = SampleType{1} - cos_w0;
123 b2 = (SampleType{1} - cos_w0) / SampleType{2};
124 a0 = SampleType{1} + alpha;
125 a1 = -SampleType{2} * cos_w0;
126 a2 = SampleType{1} - alpha;
127 break;
128
129 case Type::highpass:
130 b0 = (SampleType{1} + cos_w0) / SampleType{2};
131 b1 = -(SampleType{1} + cos_w0);
132 b2 = (SampleType{1} + cos_w0) / SampleType{2};
133 a0 = SampleType{1} + alpha;
134 a1 = -SampleType{2} * cos_w0;
135 a2 = SampleType{1} - alpha;
136 break;
137
138 case Type::bandpass:
139 b0 = alpha;
140 b1 = SampleType{0};
141 b2 = -alpha;
142 a0 = SampleType{1} + alpha;
143 a1 = -SampleType{2} * cos_w0;
144 a2 = SampleType{1} - alpha;
145 break;
146
147 case Type::notch:
148 b0 = SampleType{1};
149 b1 = -SampleType{2} * cos_w0;
150 b2 = SampleType{1};
151 a0 = SampleType{1} + alpha;
152 a1 = -SampleType{2} * cos_w0;
153 a2 = SampleType{1} - alpha;
154 break;
155
156 case Type::allpass:
157 b0 = SampleType{1} - alpha;
158 b1 = -SampleType{2} * cos_w0;
159 b2 = SampleType{1} + alpha;
160 a0 = SampleType{1} + alpha;
161 a1 = -SampleType{2} * cos_w0;
162 a2 = SampleType{1} - alpha;
163 break;
164
165 case Type::peaking: {
166 SampleType A = std::pow(SampleType{10}, gain_db / SampleType{40});
167 b0 = SampleType{1} + alpha * A;
168 b1 = -SampleType{2} * cos_w0;
169 b2 = SampleType{1} - alpha * A;
170 a0 = SampleType{1} + alpha / A;
171 a1 = -SampleType{2} * cos_w0;
172 a2 = SampleType{1} - alpha / A;
173 break;
174 }
175
176 case Type::low_shelf: {
177 SampleType A = std::pow(SampleType{10}, gain_db / SampleType{40});
178 SampleType two_sqrt_a_alpha = SampleType{2} * std::sqrt(A) * alpha;
179 b0 = A * ((A + SampleType{1}) - (A - SampleType{1}) * cos_w0 + two_sqrt_a_alpha);
180 b1 = SampleType{2} * A * ((A - SampleType{1}) - (A + SampleType{1}) * cos_w0);
181 b2 = A * ((A + SampleType{1}) - (A - SampleType{1}) * cos_w0 - two_sqrt_a_alpha);
182 a0 = (A + SampleType{1}) + (A - SampleType{1}) * cos_w0 + two_sqrt_a_alpha;
183 a1 = -SampleType{2} * ((A - SampleType{1}) + (A + SampleType{1}) * cos_w0);
184 a2 = (A + SampleType{1}) + (A - SampleType{1}) * cos_w0 - two_sqrt_a_alpha;
185 break;
186 }
187
188 case Type::high_shelf: {
189 SampleType A = std::pow(SampleType{10}, gain_db / SampleType{40});
190 SampleType two_sqrt_a_alpha = SampleType{2} * std::sqrt(A) * alpha;
191 b0 = A * ((A + SampleType{1}) + (A - SampleType{1}) * cos_w0 + two_sqrt_a_alpha);
192 b1 = -SampleType{2} * A * ((A - SampleType{1}) + (A + SampleType{1}) * cos_w0);
193 b2 = A * ((A + SampleType{1}) + (A - SampleType{1}) * cos_w0 - two_sqrt_a_alpha);
194 a0 = (A + SampleType{1}) - (A - SampleType{1}) * cos_w0 + two_sqrt_a_alpha;
195 a1 = SampleType{2} * ((A - SampleType{1}) - (A + SampleType{1}) * cos_w0);
196 a2 = (A + SampleType{1}) - (A - SampleType{1}) * cos_w0 - two_sqrt_a_alpha;
197 break;
198 }
199 }
200
201 // Normalize
202 b0_ = b0 / a0;
203 b1_ = b1 / a0;
204 b2_ = b2 / a0;
205 a1_ = a1 / a0;
206 a2_ = a2 / a0;
207 update_block_form();
208 }
209
210 // Process a single sample (Direct Form II Transposed)
211 SampleType process(SampleType input) {
212 SampleType output = b0_ * input + s1_;
213 // Snap the recursive state to keep denormals out of the DF2T feedback
214 // path where subnormals are slow and no FTZ guard applies (see
215 // snap_recursion_state()). No-op for any state >= 1e-15.
216 s1_ = snap_recursion_state(b1_ * input - a1_ * output + s2_);
217 s2_ = snap_recursion_state(b2_ * input - a2_ * output);
218 history_valid_ = false;
219 return output;
220 }
221
222 // Process a buffer in-place.
223 //
224 // The block path runs the same per-pass form selection as process_chain()
225 // (a single filter is a one-section pass), so a filter computes the same
226 // output whether it is processed alone or as a one-section chain.
227 //
228 // For float that form is Direct Form I with its recursive state and
229 // arithmetic in double, written so the loop-carried dependency from one
230 // output to the next is a single multiply-add:
231 // y = -a1 y[n-1] + ((b0 x + b1 x[n-1]) + (b2 x[n-2] - a2 y[n-2]))
232 // (y[n-2] reaches y two samples later, so it is off the critical path).
233 // That is faster than the sample path's DF2T order, and with a double
234 // accumulator no less accurate in practice: across the RBJ types at 20 Hz to
235 // 8 kHz and Q 0.707 and 10, its RMS error against an exact (float64)
236 // filter stays within 3 dB of the sample path's (test_biquad.cpp). Block
237 // and sample outputs are therefore not bit-identical: they differ by the
238 // sample path's own float round-off. (Which compilers fuse multiply-adds
239 // is Pulp's contraction policy:
240 // tools/cmake/PulpFloatingPointContraction.cmake.)
241 //
242 // Double SampleType keeps a DF2T block path: the feedback-last order
243 // y = b0 x + s1; s1 = (-a1) y + (b1 x + s2); s2 = b2 x - a2 y
244 // for designs whose poles are at most kFeedbackLastMaxPoleRadius from the
245 // origin, and the sample path's own order above. That order is
246 // bit-identical to the sample path while the state stays above the
247 // 1e-15 snap threshold; below it the block path snaps once per chunk
248 // (snap_chunks()) where the sample path snaps every sample.
249 //
250 // State continuity: the sample path's DF2T state and the block path's DF1
251 // history describe the same filter. A block call that follows sample
252 // calls or a coefficient change derives an equivalent DF1 history from
253 // the DF2T state (same future output in exact arithmetic), and every
254 // block call leaves the DF2T state the sample path will continue from.
255 // Retuning therefore has the same (DF2T) transient on either path.
256 //
257 // Coefficients and state are held in locals: member state would otherwise
258 // be stored and reloaded every sample because the buffer may alias it.
259 // The denormal snap is applied to the recursive state once per
260 // kSnapChunkSamples chunk (see snap_chunks()) instead of after
261 // every sample; it only alters states below 1e-15. With both poles inside
262 // |z| < 0.44, or with subnormal input, the state can sit in the subnormal
263 // range (rounding can hold it in a one-ulp cycle) until the next snap, so
264 // subnormal residency is bounded by one chunk however long the buffer is.
265 void process(SampleType* buffer, int num_samples) {
266 if (num_samples <= 0)
267 return;
268 dispatch_fixed<1>(this, nullptr, buffer, nullptr, static_cast<std::size_t>(num_samples));
269 }
270
271 // Process two channels in place, each through its own filter, with the
272 // two recursions in the lanes of one vector so a stereo pair costs about
273 // as much as one channel. Each channel's output is bit-identical to
274 // calling that filter's block process(); the filters may have different
275 // coefficients. Both buffers must hold num_samples samples. Passing one
276 // filter as both channels runs the left buffer and then the right through
277 // it, as two block process() calls would.
278 static void process_stereo(BiquadT& left, BiquadT& right, SampleType* left_buffer,
279 SampleType* right_buffer, std::size_t num_samples) {
280 if (num_samples == 0)
281 return;
282 if (&left == &right) {
283 dispatch_fixed<1>(&left, nullptr, left_buffer, nullptr, num_samples);
284 dispatch_fixed<1>(&left, nullptr, right_buffer, nullptr, num_samples);
285 return;
286 }
287 // Below about 64 samples two separate calls already overlap in the
288 // out-of-order window and the lane path measured no faster. Filters
289 // whose passes take different forms cannot share lanes.
290 if (num_samples < kStereoLaneMinSamples || pass_form(&left, 1) != pass_form(&right, 1)) {
291 dispatch_fixed<1>(&left, nullptr, left_buffer, nullptr, num_samples);
292 dispatch_fixed<1>(&right, nullptr, right_buffer, nullptr, num_samples);
293 return;
294 }
295 dispatch_fixed<1>(&left, &right, left_buffer, right_buffer, num_samples);
296 }
297
320 static void process_chain(BiquadT* sections, std::size_t count, SampleType* buffer,
321 std::size_t num_samples) noexcept {
322 for (std::size_t first = 0; first < count; first += kChainSectionsPerPass) {
323 const std::size_t n = std::min(count - first, kChainSectionsPerPass);
324 dispatch_pass(n, sections + first, nullptr, buffer, nullptr, num_samples);
325 }
326 }
327
334 static void process_chain_stereo(BiquadT* left, BiquadT* right, std::size_t count,
335 SampleType* left_buffer, SampleType* right_buffer,
336 std::size_t num_samples) noexcept {
337 if (left == right) {
338 process_chain(left, count, left_buffer, num_samples);
339 process_chain(left, count, right_buffer, num_samples);
340 return;
341 }
342 for (std::size_t first = 0; first < count; first += kChainSectionsPerPass) {
343 const std::size_t n = std::min(count - first, kChainSectionsPerPass);
344 // The two channels' passes must take the same form to share
345 // lanes; otherwise they run one after the other.
346 if (pass_form(left + first, n) == pass_form(right + first, n)) {
347 dispatch_pass(n, left + first, right + first, left_buffer, right_buffer,
348 num_samples);
349 } else {
350 dispatch_pass(n, left + first, nullptr, left_buffer, nullptr, num_samples);
351 dispatch_pass(n, right + first, nullptr, right_buffer, nullptr, num_samples);
352 }
353 }
354 }
355
359 static constexpr std::size_t kChainSectionsPerPass = 16;
360
363 static constexpr std::size_t kWideStateMaxSections = 4;
364
365 // Shortest call process_stereo() runs in vector lanes.
366 static constexpr std::size_t kStereoLaneMinSamples = 64;
367
368 // Longer passes (and double SampleType) use the feedback-last DF2T order
369 // only when every section's largest pole radius is at most this. Near the
370 // unit circle that order's RMS error against an exact DF2T grows past the
371 // sample path's (low shelves, allpasses and notches near 20-100 Hz and
372 // high-Q bells), so those designs keep the sample path's order, which the
373 // tests hold bit-identical above the snap threshold.
374 static constexpr double kFeedbackLastMaxPoleRadius = 0.995;
375
376 // Reset state (call on discontinuities)
377 void reset() {
378 s1_ = SampleType{0};
379 s2_ = SampleType{0};
380 hx1_ = hx2_ = hy1_ = hy2_ = 0.0;
381 history_valid_ = true;
382 }
383
384 private:
385 enum class BlockForm { sample_order, feedback_last, wide_df1 };
386
387 // The form a pass of `count` sections takes; process() and
388 // process_chain() make the same choice per pass (see process_chain()).
389 static BlockForm pass_form(const BiquadT* sections, std::size_t count) noexcept {
390 if constexpr (std::is_same_v<SampleType, float>) {
391 if (count <= kWideStateMaxSections)
392 return BlockForm::wide_df1;
393 }
394 for (std::size_t k = 0; k < count; ++k)
395 if (sections[k].sample_path_order_in_block_)
396 return BlockForm::sample_order;
397 return BlockForm::feedback_last;
398 }
399
400 // Run one pass of `count` sections; `right` non-null runs a second
401 // channel's sections in the other lane.
402 static void dispatch_pass(std::size_t count, BiquadT* left, BiquadT* right,
403 SampleType* left_buffer, SampleType* right_buffer,
404 std::size_t num_samples) noexcept {
405 runtime::dispatch_count<kChainSectionsPerPass>(count, [&](auto sections) {
406 dispatch_fixed<decltype(sections)::value>(left, right, left_buffer, right_buffer,
407 num_samples);
408 });
409 }
410
411 template <std::size_t Count>
412 static void dispatch_fixed(BiquadT* left, BiquadT* right, SampleType* left_buffer,
413 SampleType* right_buffer, std::size_t num_samples) noexcept {
414 BlockForm form = pass_form(left, Count);
415 if constexpr (std::is_same_v<SampleType, float> && Count <= kWideStateMaxSections) {
416 if (form == BlockForm::wide_df1) {
417 // A history that cannot be derived (a degenerate section
418 // whose DF2T state no DF1 history reproduces) falls back to
419 // the sample path's order for this call, for that channel
420 // only: each channel still computes what it would alone.
421 const bool left_ok = prepare_histories<Count>(left);
422 const bool right_ok = right == nullptr || prepare_histories<Count>(right);
423 if (left_ok && right_ok) {
424 run_pass_lanes<Count, BlockForm::wide_df1>(left, right, left_buffer,
425 right_buffer, num_samples);
426 return;
427 }
428 if (right != nullptr) {
429 run_channel_df1_or_sample_order<Count>(left, left_buffer, num_samples, left_ok);
430 run_channel_df1_or_sample_order<Count>(right, right_buffer, num_samples,
431 right_ok);
432 return;
433 }
434 form = BlockForm::sample_order;
435 }
436 }
437 if (form == BlockForm::feedback_last)
438 run_pass_lanes<Count, BlockForm::feedback_last>(left, right, left_buffer, right_buffer,
439 num_samples);
440 else
441 run_pass_lanes<Count, BlockForm::sample_order>(left, right, left_buffer, right_buffer,
442 num_samples);
443 }
444
445 template <std::size_t Count> static bool prepare_histories(BiquadT* sections) noexcept {
446 bool ok = true;
447 for (std::size_t k = 0; k < Count; ++k)
448 ok = sections[k].prepare_history() && ok;
449 return ok;
450 }
451
452 template <std::size_t Count>
453 static void run_channel_df1_or_sample_order(BiquadT* sections, SampleType* buffer,
454 std::size_t num_samples, bool df1) noexcept {
455 if (df1)
456 run_pass_lanes<Count, BlockForm::wide_df1>(sections, nullptr, buffer, nullptr,
457 num_samples);
458 else
459 run_pass_lanes<Count, BlockForm::sample_order>(sections, nullptr, buffer, nullptr,
460 num_samples);
461 }
462
463 // One channel in a scalar, or two in the lanes of one vector; without
464 // vector lanes (simd::kHasVectorLanes false) the two channels run one
465 // after the other, with the same per-lane result.
466 template <std::size_t Count, BlockForm Form>
467 static void run_pass_lanes(BiquadT* left, BiquadT* right, SampleType* left_buffer,
468 SampleType* right_buffer, std::size_t num_samples) noexcept {
469 if (right == nullptr) {
470 BiquadT* filters[1] = {left};
471 SampleType* buffers[1] = {left_buffer};
472 run_pass<Count, Form, 1>(filters, buffers, num_samples);
473 return;
474 }
475 if constexpr (simd::kHasVectorLanes) {
476 BiquadT* filters[2] = {left, right};
477 SampleType* buffers[2] = {left_buffer, right_buffer};
478 run_pass<Count, Form, 2>(filters, buffers, num_samples);
479 } else {
480 BiquadT* filters[2] = {left, right};
481 SampleType* buffers[2] = {left_buffer, right_buffer};
482 for (std::size_t lane = 0; lane < 2; ++lane)
483 run_pass<Count, Form, 1>(filters + lane, buffers + lane, num_samples);
484 }
485 }
486
487 // The value a pass computes in: SampleType for the DF2T forms, double for
488 // DF1; `Width` channels side by side.
489 template <BlockForm Form>
490 using PassElement = std::conditional_t<Form == BlockForm::wide_df1, double, SampleType>;
491
492 template <typename E, std::size_t Width> struct PassLanes {
493 using type = E;
494 };
495#if PULP_SIMD_HAS_VECTOR_LANES
496 template <typename E> struct PassLanes<E, 2> {
497 using type = simd::Lanes<E, 2>;
498 };
499#endif
500
501 // One section's recurrence on a lane type V. DF2T state is (s1, s2) in
502 // (z0, z1):
503 // sample order: y = b0 x + s1; s1 = b1 x - a1 y + s2; s2 = b2 x - a2 y
504 // feedback last: the same with s1 = (-a1) y + (b1 x + s2), so the only
505 // operation after y is one multiply-add;
506 // DF1 (double) keeps (x1, x2, y1, y2) in (z0, z1, z2, z3):
507 // y = (-a1) y1 + ((b0 x + b1 x1) + (b2 x2 + (-a2) y2)).
508 // a1 and a2 arrive in the sign the form's recurrence uses (see
509 // run_pass()).
510 template <BlockForm Form, typename V>
511 static V step(V x, V b0, V b1, V b2, V a1, V a2, V& z0, V& z1, V& z2, V& z3) noexcept {
512 if constexpr (Form == BlockForm::wide_df1) {
513 const V feed = (b0 * x + b1 * z0) + (b2 * z1 + a2 * z3);
514 const V y = a1 * z2 + feed;
515 z1 = z0;
516 z0 = x;
517 z3 = z2;
518 z2 = y;
519 return y;
520 } else if constexpr (Form == BlockForm::feedback_last) {
521 (void)z2;
522 (void)z3;
523 const V y = b0 * x + z0;
524 const V t1 = b1 * x + z1;
525 const V t2 = b2 * x;
526 z0 = a1 * y + t1;
527 z1 = t2 - a2 * y;
528 return y;
529 } else {
530 (void)z2;
531 (void)z3;
532 const V y = b0 * x + z0;
533 z0 = b1 * x - a1 * y + z1;
534 z1 = b2 * x - a2 * y;
535 return y;
536 }
537 }
538
539 // Snap each lane of a recursive state value.
540 template <typename V> static void snap_lanes(V& v) noexcept {
541 for (std::size_t lane = 0; lane < simd::lane_count<V>; ++lane)
542 simd::set_lane(v, lane, snap_to_zero(simd::get_lane(v, lane)));
543 }
544
545 // A sample-major pass of Count sections over Width channels, coefficients
546 // and state in local arrays the compiler keeps in registers (separate
547 // arrays per field measured faster than an array of per-section structs
548 // once a stereo pass spills). Every lane runs the scalar operation
549 // sequence, so a channel's output does not depend on Width.
550 template <std::size_t Count, BlockForm Form, std::size_t Width>
551 static void run_pass(BiquadT* const* filters, SampleType* const* buffers,
552 std::size_t num_samples) noexcept {
553 using E = PassElement<Form>;
554 using V = typename PassLanes<E, Width>::type;
555 constexpr bool df1 = Form == BlockForm::wide_df1;
556 V b0[Count], b1[Count], b2[Count], a1[Count], a2[Count];
557 V z0[Count], z1[Count], z2[Count], z3[Count];
558 for (std::size_t k = 0; k < Count; ++k) {
559 for (std::size_t lane = 0; lane < Width; ++lane) {
560 const BiquadT& f = filters[lane][k];
561 simd::set_lane(b0[k], lane, static_cast<E>(f.b0_));
562 simd::set_lane(b1[k], lane, static_cast<E>(f.b1_));
563 simd::set_lane(b2[k], lane, static_cast<E>(f.b2_));
564 if constexpr (df1) {
565 simd::set_lane(a1[k], lane, -static_cast<double>(f.a1_));
566 simd::set_lane(a2[k], lane, -static_cast<double>(f.a2_));
567 simd::set_lane(z0[k], lane, f.hx1_);
568 simd::set_lane(z1[k], lane, f.hx2_);
569 simd::set_lane(z2[k], lane, f.hy1_);
570 simd::set_lane(z3[k], lane, f.hy2_);
571 } else {
572 simd::set_lane(a1[k], lane, Form == BlockForm::feedback_last ? -f.a1_ : f.a1_);
573 simd::set_lane(a2[k], lane, f.a2_);
574 simd::set_lane(z0[k], lane, f.s1_);
575 simd::set_lane(z1[k], lane, f.s2_);
576 z2[k] = z3[k] = V{};
577 }
578 }
579 }
580 for (const auto [begin, end] : snap_chunks(num_samples)) {
581 for (std::size_t i = begin; i < end; ++i) {
582 V v;
583 if constexpr (Width == 1)
584 v = static_cast<E>(buffers[0][i]);
585 else
586 v = V{static_cast<E>(buffers[0][i]), static_cast<E>(buffers[1][i])};
587 for (std::size_t k = 0; k < Count; ++k)
588 v = step<Form>(v, b0[k], b1[k], b2[k], a1[k], a2[k], z0[k], z1[k], z2[k],
589 z3[k]);
590 for (std::size_t lane = 0; lane < Width; ++lane)
591 buffers[lane][i] = static_cast<SampleType>(simd::get_lane(v, lane));
592 }
593 // The recursive state: (s1, s2) for DF2T, (y1, y2) for DF1.
594 for (std::size_t k = 0; k < Count; ++k) {
595 snap_lanes(df1 ? z2[k] : z0[k]);
596 snap_lanes(df1 ? z3[k] : z1[k]);
597 }
598 }
599 for (std::size_t k = 0; k < Count; ++k) {
600 for (std::size_t lane = 0; lane < Width; ++lane) {
601 BiquadT& f = filters[lane][k];
602 if constexpr (df1) {
603 f.store_history(simd::get_lane(z0[k], lane), simd::get_lane(z1[k], lane),
604 simd::get_lane(z2[k], lane), simd::get_lane(z3[k], lane));
605 } else {
606 f.s1_ = simd::get_lane(z0[k], lane);
607 f.s2_ = simd::get_lane(z1[k], lane);
608 f.history_valid_ = false;
609 }
610 }
611 }
612 }
613
614 // Largest ratio of a derived DF1 history to the DF2T state it reproduces
615 // that the block path accepts.
616 static constexpr double kMaxHistoryGain = 1.0e4;
617
618 // Make the DF1 history describe the current DF2T state: if the last
619 // writer was the sample path or a coefficient change, derive an
620 // equivalent history that produces the same future output in exact
621 // arithmetic. Returns false for a degenerate or ill-conditioned section;
622 // the caller then runs the sample order instead.
623 //
624 // The history (x1, x2, y1, y2) must satisfy two equations:
625 // s2 = b2 x1 - a2 y1
626 // s1 = b1 x1 - a1 y1 + b2 x2 - a2 y2
627 // The minimum-norm solution keeps the history on the scale of the state.
628 // Solving by dividing by one coefficient does not: a tiny nonzero a2
629 // makes the history huge, and its terms then cancel in the block output.
630 bool prepare_history() noexcept {
631 if (history_valid_)
632 return true;
633 const double b1 = b1_, b2 = b2_, a1 = a1_, a2 = a2_;
634 const double s1 = s1_, s2 = s2_;
635 const double m1 = std::max(std::abs(b2), std::abs(a2));
636 const double m2 = std::max({std::abs(b1), std::abs(b2), std::abs(a1), std::abs(a2)});
637 // Rows of the system, each scaled to a largest entry of 1.
638 const std::array<double, 4> q =
639 m2 > 0.0 ? std::array<double, 4>{b1 / m2, b2 / m2, -a1 / m2, -a2 / m2}
640 : std::array<double, 4>{};
641 const double t2 = m2 > 0.0 ? s1 / m2 : 0.0;
642 std::array<double, 4> v{};
643 if (m2 == 0.0) {
644 if (s1 != 0.0 || s2 != 0.0)
645 return false;
646 } else if (m1 == 0.0) {
647 if (s2 != 0.0)
648 return false;
649 const double qq = q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3];
650 for (std::size_t i = 0; i < 4; ++i)
651 v[i] = q[i] * (t2 / qq);
652 } else {
653 const std::array<double, 4> p{b2 / m1, 0.0, -a2 / m1, 0.0};
654 const double t1 = s2 / m1;
655 const double pp = p[0] * p[0] + p[2] * p[2];
656 const double qq = q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3];
657 const double pq = p[0] * q[0] + p[2] * q[2];
658 const double det = pp * qq - pq * pq;
659 // Nearly parallel rows: every solution is large, so the block
660 // output would lose precision to cancellation.
661 if (!(det > 1.0e-6 * pp * qq))
662 return false;
663 const double w1 = (qq * t1 - pq * t2) / det;
664 const double w2 = (pp * t2 - pq * t1) / det;
665 for (std::size_t i = 0; i < 4; ++i)
666 v[i] = p[i] * w1 + q[i] * w2;
667 }
668 // Even the minimum-norm history can be forced large: retuning to
669 // b2 = 0 with a tiny a2 leaves s2 = -a2 y1, so |y1| = |s2| / |a2| for
670 // every solution. A history far above the state it reproduces would
671 // lose the block output to cancellation, so that case runs the
672 // sample order too.
673 const double state = std::max(std::abs(s1), std::abs(s2));
674 const double largest =
675 std::max({std::abs(v[0]), std::abs(v[1]), std::abs(v[2]), std::abs(v[3])});
676 if (!std::isfinite(largest) || largest > kMaxHistoryGain * state)
677 return false;
678 hx1_ = v[0];
679 hx2_ = v[1];
680 hy1_ = v[2];
681 hy2_ = v[3];
682 history_valid_ = true;
683 return true;
684 }
685
686 // Record a DF1 block's history and the equivalent DF2T state the sample
687 // path continues from.
688 void store_history(double x1, double x2, double y1, double y2) noexcept {
689 hx1_ = x1;
690 hx2_ = x2;
691 hy1_ = y1;
692 hy2_ = y2;
693 const double b1 = b1_, b2 = b2_, a1 = a1_, a2 = a2_;
694 s2_ = snap_to_zero(static_cast<SampleType>(b2 * x1 - a2 * y1));
695 s1_ = snap_to_zero(static_cast<SampleType>(b1 * x1 - a1 * y1 + b2 * x2 - a2 * y2));
696 history_valid_ = true;
697 }
698
699 // Largest pole radius of z^2 + a1 z + a2, in double so the threshold
700 // decision does not depend on the sample type.
701 void update_block_form() {
702 const double a1 = static_cast<double>(a1_);
703 const double a2 = static_cast<double>(a2_);
704 const double disc = a1 * a1 - 4.0 * a2;
705 double radius;
706 if (disc < 0.0) {
707 radius = std::sqrt(a2);
708 } else {
709 const double root = std::sqrt(disc);
710 radius = std::max(std::abs(-a1 + root), std::abs(-a1 - root)) * 0.5;
711 }
712 // A NaN radius fails the comparison and keeps the sample-path order.
713 sample_path_order_in_block_ = !(radius <= kFeedbackLastMaxPoleRadius);
714 // New coefficients: the next DF1 block re-derives its history from
715 // the DF2T state, so a retune has the same transient on both paths.
716 history_valid_ = false;
717 }
718
719 static constexpr SampleType pi =
720 std::is_same_v<SampleType, float>
721 ? static_cast<SampleType>(3.14159265358979323846f)
722 : static_cast<SampleType>(3.141592653589793238462643383279502884L);
723
724 SampleType b0_ = SampleType{1};
725 SampleType b1_ = SampleType{0};
726 SampleType b2_ = SampleType{0};
727 SampleType a1_ = SampleType{0};
728 SampleType a2_ = SampleType{0};
729 SampleType s1_ = SampleType{0};
730 SampleType s2_ = SampleType{0};
731 bool sample_path_order_in_block_ = false;
732 // DF1 history (last two inputs and outputs) for the float block path,
733 // and whether it describes the current DF2T state.
734 double hx1_ = 0.0, hx2_ = 0.0, hy1_ = 0.0, hy2_ = 0.0;
735 bool history_valid_ = true;
736};
737
740
741} // namespace pulp::signal
Definition biquad.hpp:53
void process(SampleType *buffer, int num_samples)
Definition biquad.hpp:265
void reset()
Definition biquad.hpp:377
Type
Definition biquad.hpp:58
@ allpass
Definition biquad.hpp:58
@ peaking
Definition biquad.hpp:58
@ high_shelf
Definition biquad.hpp:58
@ notch
Definition biquad.hpp:58
@ low_shelf
Definition biquad.hpp:58
@ highpass
Definition biquad.hpp:58
@ bandpass
Definition biquad.hpp:58
@ lowpass
Definition biquad.hpp:58
void set_coefficients(Type type, SampleType freq_hz, SampleType q, SampleType sample_rate, SampleType gain_db=SampleType{0})
Definition biquad.hpp:84
BiquadCoefficientsT< SampleType > Coefficients
Definition biquad.hpp:60
static constexpr std::size_t kWideStateMaxSections
Definition biquad.hpp:363
static void process_chain_stereo(BiquadT *left, BiquadT *right, std::size_t count, SampleType *left_buffer, SampleType *right_buffer, std::size_t num_samples) noexcept
process_chain() for two channels at once, each through its own chain of count sections,...
Definition biquad.hpp:334
static constexpr double kFeedbackLastMaxPoleRadius
Definition biquad.hpp:374
static void process_stereo(BiquadT &left, BiquadT &right, SampleType *left_buffer, SampleType *right_buffer, std::size_t num_samples)
Definition biquad.hpp:278
void set_coefficients(const BiquadCoefficientsT< U > &c)
Definition biquad.hpp:69
static void process_chain(BiquadT *sections, std::size_t count, SampleType *buffer, std::size_t num_samples) noexcept
Run a chain of sections (a cascade: each section's output feeds the next) over a buffer in place.
Definition biquad.hpp:320
SampleType process(SampleType input)
Definition biquad.hpp:211
static constexpr std::size_t kStereoLaneMinSamples
Definition biquad.hpp:366
Coefficients coefficients() const
Definition biquad.hpp:81
static constexpr std::size_t kChainSectionsPerPass
Definition biquad.hpp:359
Turn a run-time count into a compile-time one.
Fixed-width register lanes for per-sample recursive kernels.
@ end
Definition editor_host.hpp:354
@ Count
Definition transport_fields.hpp:21
@ x1
Definition forge_distortion_catalog.hpp:48
@ x2
Definition forge_distortion_catalog.hpp:48
@ f
Definition pitch.hpp:25
@ alpha
Straight alpha: src * a + dst * (1 - a).
Definition gpu_pass.hpp:268
constexpr bool dispatch_count(std::size_t n, Fn &&fn)
Call fn(std::integral_constant<std::size_t, n>{}) for a run-time n in [1, Max], so a kernel can be in...
Definition dispatch_count.hpp:26
Definition additive_bank.hpp:218
BiquadT< float > Biquad
Definition biquad.hpp:738
@ buffer
A borrowed static buffer with a traversable playhead.
Definition granular.hpp:110
@ first
Definition dither.hpp:23
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ feed
Definition finite_stretch_builder.hpp:40
@ c
Definition spectrum_trace.hpp:35
bool biquad_is_stable(const BiquadCoefficientsT< SampleType > &c) noexcept
Return whether a normalized real biquad's poles are strictly inside the unit circle.
Definition biquad.hpp:34
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ i
Definition formant_filter_bank.hpp:20
BiquadT< double > Biquad64
Definition biquad.hpp:739
constexpr SnapChunks snap_chunks(std::size_t num_samples) noexcept
Definition denormal.hpp:173
@ ok
Definition breakpoint_envelope.hpp:36
BiquadCoefficientsT< float > BiquadCoefficients
Definition biquad.hpp:29
T snap_recursion_state(T x)
Snap a per-sample recursion's state: snap_to_zero() where subnormals are slow, the identity where kSu...
Definition denormal.hpp:84
void set_lane(V &v, std::size_t i, T x) noexcept
Set lane i of v to x; a scalar is its own lane 0.
Definition lanes.hpp:71
constexpr bool kHasVectorLanes
Definition lanes.hpp:48
auto get_lane(const V &v, std::size_t i) noexcept
Lane i of v; a scalar is its own lane 0.
Definition lanes.hpp:61
@ Width
Definition motion.hpp:45
@ y
Definition input_events.hpp:42
@ q
Definition input_events.hpp:41
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
Definition biquad.hpp:24
float b0
Definition biquad.hpp:25
float a2
Definition biquad.hpp:26
float a1
Definition biquad.hpp:26
float b2
Definition biquad.hpp:25
float b1
Definition biquad.hpp:25