Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
multirate.hpp
Go to the documentation of this file.
1#pragma once
2
3// The multirate bridge: host rate in, tank rate out, and back.
4//
5// The tank runs at its OWN sample rate, chosen from eight pinned steps. Low
6// rates are darker and grittier by design — the band limit plus the resampler's
7// own character — and high rates are airier. Non-integer ratios (44.1 kHz host
8// into a 20 kHz tank) are the normal case, never the exception, so nothing here
9// may assume an integer decimation factor.
10//
11// HOW POSITIONS ARE TRACKED
12// Each leg keeps the ABSOLUTE position, in its source stream, of the next
13// sample it must produce, and advances it by a constant step. That is stronger
14// than a wrapped phase accumulator: there is no per-block reset, no rounding
15// that can accumulate, and the sequence of produced positions depends only on
16// how many samples have been seen — never on how they were divided into blocks.
17// Rendering one 4096-sample block and rendering the same audio as 8 x 512 give
18// bit-identical output, which is the property the determinism test pins.
19//
20// WHY THERE IS A LAG
21// A 4-point Hermite read at position p needs the samples at floor(p)-1 through
22// floor(p)+2, so each leg can only produce a sample once its source stream has
23// advanced two samples past it. The output leg's two-sample window is in TANK
24// samples, so in host time it is worth 2 * fs_host / fs_tank — six host samples
25// at a 16 kHz tank against a 48 kHz host. That is the whole of the engine's
26// reported-zero latency story: there is no block-scale buffering anywhere, only
27// this interpolation window, and it is ratio-dependent by construction.
28//
29// BOTH RAILS AT ONCE
30// The bridge carries a stereo pair through identical filters at identical
31// positions, so it stores the rails interleaved and runs each filter section
32// and each Hermite read on a two-lane double vector holding left and right.
33// Input is sanitized where it enters each leg; the recursive filter state is
34// snapped and sanitized every kHealIntervalSamples of the filter's own clock
35// rather than at every multiply-add.
36//
37// RT contract: prepare() allocates for the worst case (96 kHz tank at the
38// maximum block size). configure() re-derives every rate-dependent value and
39// allocates nothing, so a live tank-rate change is RT-safe.
40
46#include <pulp/signal/detail/simd_lanes.hpp>
47
48#include <algorithm>
49#include <array>
50#include <cmath>
51#include <cstdint>
52#include <type_traits>
53#include <vector>
54
55namespace pulp::signal::fdn {
56
57namespace lanes = signal::detail::lanes;
58
59// A ring addressed by ABSOLUTE stream position rather than by delay. Positions
60// before the start of the stream read as silence, which is what makes the
61// startup transient of both legs simply "the tail has not arrived yet".
62template <typename SampleType = float>
64public:
65 void prepare(int capacity) {
66 std::size_t size = 8;
67 while (size < static_cast<std::size_t>(std::max(capacity, 1) + 4)) size <<= 1;
68 buffer_.assign(size, SampleType{0});
69 mask_ = size - 1;
70 written_ = 0;
71 }
72
73 void reset() {
74 std::fill(buffer_.begin(), buffer_.end(), SampleType{0});
75 written_ = 0;
76 }
77
78 void push(SampleType x) {
79 buffer_[static_cast<std::size_t>(written_) & mask_] = x;
80 ++written_;
81 }
82
83 std::int64_t written() const { return written_; }
84
85 // Highest position that can be read with all four Hermite taps present.
86 double readable_through() const { return static_cast<double>(written_ - 3); }
87
88 double read(double position) const {
89 const double floor_pos = std::floor(position);
90 const auto i = static_cast<std::int64_t>(floor_pos);
91 const double t = position - floor_pos;
92 return hermite4(at(i - 1), at(i), at(i + 1), at(i + 2), t);
93 }
94
95private:
96 double at(std::int64_t index) const {
97 if (index < 0 || index >= written_) return 0.0;
98 return static_cast<double>(buffer_[static_cast<std::size_t>(index) & mask_]);
99 }
100
101 std::vector<SampleType> buffer_;
102 std::size_t mask_ = 0;
103 std::int64_t written_ = 0;
104};
105
106// Two cascaded 2-pole lowpasses with the 4-pole Butterworth Q pair. One
107// coefficient design serves both roles (anti-alias before decimation,
108// reconstruction after interpolation) because both always run in the FASTER of
109// the two domains at the same cutoff; only the state is per role and channel.
111public:
112 void design(double cutoff_hz, double sample_rate) {
113 BiquadT<double> designer;
115 kButterworthQ0, sample_rate);
116 section_[0] = designer.coefficients();
118 kButterworthQ1, sample_rate);
119 section_[1] = designer.coefficients();
120 }
121
122 const std::array<BiquadCoefficientsT<double>, 2>& sections() const {
123 return section_;
124 }
125
126private:
127 std::array<BiquadCoefficientsT<double>, 2> section_{};
128};
129
130// The state half of the pair: one instance per (role, channel).
132public:
133 void reset() { s_ = {}; }
134
135 double process(const std::array<BiquadCoefficientsT<double>, 2>& sections,
136 double x) {
137 for (int k = 0; k < 2; ++k)
138 x = s_[static_cast<std::size_t>(k)].process(
139 sections[static_cast<std::size_t>(k)], x);
140 return x;
141 }
142
143private:
144 std::array<SanitizedBiquadState, 2> s_{};
145};
146
147// An interleaved stereo ring addressed by absolute stream position, as
148// PositionRing is for one rail, and like it reading positions outside the
149// written stream as silence.
150template <typename SampleType = float> class StereoPositionRing {
151 public:
152 void prepare(int capacity) {
153 std::size_t size = 8;
154 while (size < static_cast<std::size_t>(std::max(capacity, 1) + 4))
155 size <<= 1;
156 buffer_.assign(size * 2, SampleType{0});
157 mask_ = size - 1;
158 written_ = 0;
159 }
160
161 void reset() {
162 std::fill(buffer_.begin(), buffer_.end(), SampleType{0});
163 written_ = 0;
164 }
165
166 void push(lanes::D2 x) {
167 const std::size_t slot = (static_cast<std::size_t>(written_) & mask_) * 2;
168 if constexpr (std::is_same_v<SampleType, float>) {
169 lanes::store_narrow2(&buffer_[slot], x);
170 } else {
171 buffer_[slot] = static_cast<SampleType>(lanes::lane(x, 0));
172 buffer_[slot + 1] = static_cast<SampleType>(lanes::lane(x, 1));
173 }
174 ++written_;
175 }
176
177 std::int64_t written() const {
178 return written_;
179 }
180 double readable_through() const {
181 return static_cast<double>(written_ - 3);
182 }
183
184 lanes::D2 read(double position) const {
185 const double floor_pos = std::floor(position);
186 const auto i = static_cast<std::int64_t>(floor_pos);
187 const lanes::D2 t = lanes::splat(position - floor_pos);
188 // Positions outside the written stream read as silence, as they do
189 // in PositionRing: before the start, and past the newest sample.
190 const bool inside = i >= 1 && i + 2 < written_;
191 const lanes::D2 y0 = inside ? at(i - 1) : guarded(i - 1);
192 const lanes::D2 y1 = inside ? at(i) : guarded(i);
193 const lanes::D2 y2 = inside ? at(i + 1) : guarded(i + 1);
194 const lanes::D2 y3 = inside ? at(i + 2) : guarded(i + 2);
195 // Interpolator::hermite, both rails at once.
196 const lanes::D2 c1 = lanes::splat(0.5) * (y2 - y0);
197 const lanes::D2 c2 =
198 y0 - lanes::splat(2.5) * y1 + lanes::splat(2.0) * y2 - lanes::splat(0.5) * y3;
199 const lanes::D2 c3 = lanes::splat(0.5) * (y3 - y0) + lanes::splat(1.5) * (y1 - y2);
200 return ((c3 * t + c2) * t + c1) * t + y1;
201 }
202
203 private:
204 lanes::D2 guarded(std::int64_t index) const {
205 return index < 0 || index >= written_ ? lanes::splat(0.0) : at(index);
206 }
207
208 lanes::D2 at(std::int64_t index) const {
209 const std::size_t slot = (static_cast<std::size_t>(index) & mask_) * 2;
210 if constexpr (std::is_same_v<SampleType, float>)
211 return lanes::load_widen2(&buffer_[slot]);
212 else
213 return lanes::make(static_cast<double>(buffer_[slot]),
214 static_cast<double>(buffer_[slot + 1]));
215 }
216
217 std::vector<SampleType> buffer_;
218 std::size_t mask_ = 0;
219 std::int64_t written_ = 0;
220};
221
222// A normalized biquad section rearranged so its recursion is one
223// multiply-add deep. Transposed direct form II computes y = b0 x + s1 and then
224// s1' = b1 x - a1 y + s2, which puts two dependent multiply-adds between one
225// s1 and the next. Substituting y gives
226// s1' = (b1 - a1 b0) x - a1 s1 + s2, s2' = (b2 - a2 b0) x - a2 s1,
227// the same state and the same transfer function, with y now off the
228// recursion — half the latency per sample for a filter that cannot be
229// vectorized along time.
231 double b0 = 1.0, c1 = 0.0, c2 = 0.0, a1 = 0.0, a2 = 0.0;
232
234 return {c.b0, c.b1 - c.a1 * c.b0, c.b2 - c.a2 * c.b0, c.a1, c.a2};
235 }
236};
237
238// One biquad cascade's state for both rails, two lanes per section. The state
239// is snapped and sanitized every kHealIntervalSamples samples it processes,
240// counted from reset(), so the cleanup never depends on the caller's blocks.
241// A StereoSection with every coefficient already broadcast to both lanes,
242// built once per block rather than at every sample.
244 lanes::D2 b0, c1, c2, a1, a2;
245
247 return {lanes::splat(s.b0), lanes::splat(s.c1), lanes::splat(s.c2), lanes::splat(s.a1),
248 lanes::splat(s.a2)};
249 }
250};
251
252template <int Sections> class StereoBiquadState {
253 public:
254 void reset() {
255 for (auto& s : s1_)
256 s = lanes::splat(0.0);
257 for (auto& s : s2_)
258 s = lanes::splat(0.0);
259 count_ = 0;
260 }
261
262 lanes::D2 process(const StereoSection* sections, lanes::D2 x) {
263 StereoSectionLanes l[Sections];
264 for (int k = 0; k < Sections; ++k)
265 l[k] = StereoSectionLanes::from(sections[k]);
266 return process(l, x);
267 }
268
269 lanes::D2 process(const StereoSectionLanes* sections, lanes::D2 x) {
270 for (int k = 0; k < Sections; ++k) {
271 const auto& c = sections[k];
272 const lanes::D2 s1 = s1_[k];
273 const lanes::D2 y = c.b0 * x + s1;
274 s1_[k] = c.c1 * x + s2_[k] - c.a1 * s1;
275 s2_[k] = c.c2 * x - c.a2 * s1;
276 x = y;
277 }
278 if (++count_ == kHealIntervalSamples) {
279 heal();
280 count_ = 0;
281 }
282 return x;
283 }
284
285 // Snap decayed state to zero and clear state that went non-finite.
286 void heal() {
287 for (int k = 0; k < Sections; ++k) {
288 s1_[k] = healed(s1_[k]);
289 s2_[k] = healed(s2_[k]);
290 }
291 }
292
293 private:
294 static lanes::D2 healed(lanes::D2 v) {
295 const double a = lanes::lane(v, 0);
296 const double b = lanes::lane(v, 1);
297 return lanes::make(std::isfinite(a) ? snap_to_zero(a) : 0.0,
298 std::isfinite(b) ? snap_to_zero(b) : 0.0);
299 }
300
301 public:
302 private:
303 lanes::D2 s1_[Sections] = {};
304 lanes::D2 s2_[Sections] = {};
305 int count_ = 0;
306};
307
308// Host <-> tank stereo bridge.
309template <typename SampleType = float>
311public:
312 static constexpr int kNumRails = 2;
313
314 void prepare(double host_rate, int max_block) {
315 host_rate_ = host_rate > 0.0 ? host_rate : 48000.0;
316 max_block_ = std::max(max_block, 1);
317 max_tank_samples_ =
318 static_cast<int>(std::ceil(static_cast<double>(max_block_) * kMaxTankRate /
319 host_rate_)) +
321 in_ring_.prepare(max_block_ * 2 + kHermiteGuard);
322 out_ring_.prepare(max_tank_samples_ * 2 + kHermiteGuard);
323 for (int c = 0; c < kNumRails; ++c) {
324 tank_in_[c].assign(static_cast<std::size_t>(max_tank_samples_), SampleType{0});
325 tank_out_[c].assign(static_cast<std::size_t>(max_tank_samples_), SampleType{0});
326 }
328 }
329
330 // Re-derive every rate-dependent value. Never allocates: a live tank-rate
331 // change is a hard, clean flush, not a re-prepare.
332 void configure(double tank_rate) {
333 tank_rate_ = tank_rate;
334 resampling_ = std::abs(tank_rate_ - host_rate_) > 1e-6;
335 host_per_tank_ = host_rate_ / tank_rate_;
336 tank_per_host_ = tank_rate_ / host_rate_;
337 if (resampling_) {
338 const double fast = std::max(host_rate_, tank_rate_);
339 const double slow = std::min(host_rate_, tank_rate_);
340 filter_.design(kAaCutoffFraction * slow, fast);
341 for (int k = 0; k < 2; ++k)
342 sections_[k] = StereoSection::from(filter_.sections()[static_cast<std::size_t>(k)]);
343 }
344 reset();
345 }
346
347 double tank_rate() const { return tank_rate_; }
348 bool resampling() const { return resampling_; }
349
350 // Full flush: lines, filter states, and every position accumulator. A
351 // tank-rate change drops the running tail rather than reinterpreting it at
352 // the new rate, so no pitch-shifted tail artifact is possible.
353 void reset() {
354 in_ring_.reset();
355 out_ring_.reset();
356 aa_in_.reset();
357 aa_out_.reset();
358 for (int c = 0; c < kNumRails; ++c) {
359 std::fill(tank_in_[c].begin(), tank_in_[c].end(), SampleType{0});
360 std::fill(tank_out_[c].begin(), tank_out_[c].end(), SampleType{0});
361 }
362 tank_count_ = 0;
363 host_count_ = 0;
364 next_tank_pos_ = 0.0;
365 // Start the output leg behind the tank stream by enough that its
366 // Hermite window is satisfied for every block size: three tank samples
367 // for its own taps, plus the input leg's three-host-sample window
368 // expressed in tank samples, plus a sample of slack for the floor() at
369 // the boundary.
370 host_origin_ = -(4.0 * tank_per_host_ + 4.0);
371 next_host_pos_ = host_origin_;
372 }
373
374 // Upper bound on the interpolation skew, in host samples, between an input
375 // impulse and the wet response it produces. The engine still reports zero
376 // host-compensated latency: this is a 4-point interpolation window, not
377 // bufferable delay, and the output leg's window is in TANK samples — which
378 // is why the bound grows as the tank rate falls below the host rate.
380 if (!resampling_) return 0.0;
381 return 3.0 + (4.0 * tank_per_host_ + 4.0) * host_per_tank_;
382 }
383
384 // The next read positions of the two legs, on their stream clocks: the
385 // host-sample position of the next tank sample, and the tank-sample
386 // position of the next host sample.
387 double input_read_position() const {
388 return next_tank_pos_;
389 }
390 double output_read_position() const {
391 return next_host_pos_;
392 }
393
394 SampleType* tank_input(int rail) { return tank_in_[rail].data(); }
395 SampleType* tank_output(int rail) { return tank_out_[rail].data(); }
396
397 // Leg 1: consume a host block, fill tank_input(), return the tank sample
398 // count. Filtering happens in the faster domain: at the host rate before
399 // decimation when downsampling, at the tank rate after interpolation when
400 // upsampling.
401 int host_to_tank(const SampleType* left, const SampleType* right, int n) {
402 SampleType* tl = tank_in_[0].data();
403 SampleType* tr = tank_in_[1].data();
404 if (!resampling_) {
405 for (int i = 0; i < n; ++i) {
406 tl[i] = finite_or_zero(left[i]);
407 tr[i] = finite_or_zero(right[i]);
408 }
409 return n;
410 }
411
412 const bool downsampling = tank_rate_ < host_rate_;
413 const StereoSectionLanes sections[2] = {StereoSectionLanes::from(sections_[0]),
414 StereoSectionLanes::from(sections_[1])};
415 int produced = 0;
416 // The filter state as a local for the loop: the stores to the tank
417 // buffers would otherwise force it through memory every sample.
418 StereoBiquadState<2> aa = aa_in_;
419 for (int i = 0; i < n; ++i) {
420 const lanes::D2 x = lanes::make(static_cast<double>(finite_or_zero(left[i])),
421 static_cast<double>(finite_or_zero(right[i])));
422 in_ring_.push(downsampling ? aa.process(sections, x) : x);
423 const double limit = in_ring_.readable_through();
424 while (next_tank_pos_ <= limit && produced < max_tank_samples_) {
425 lanes::D2 v = in_ring_.read(next_tank_pos_);
426 if (!downsampling)
427 v = aa.process(sections, v);
428 tl[produced] = static_cast<SampleType>(lanes::lane(v, 0));
429 tr[produced] = static_cast<SampleType>(lanes::lane(v, 1));
430 ++produced;
431 next_tank_pos_ = tank_position(++tank_count_);
432 }
433 }
434 aa_in_ = aa;
435 return produced;
436 }
437
438 // Leg 2: consume the tank's output stream and emit exactly host_n host
439 // samples. The lag chosen in reset() guarantees every position read below
440 // is already inside the tank stream's Hermite window.
441 void tank_to_host(int tank_n, SampleType* left, SampleType* right, int host_n) {
442 const SampleType* tl = tank_out_[0].data();
443 const SampleType* tr = tank_out_[1].data();
444 if (!resampling_) {
445 for (int i = 0; i < tank_n; ++i) {
446 left[i] = finite_or_zero(tl[i]);
447 right[i] = finite_or_zero(tr[i]);
448 }
449 return;
450 }
451 const bool downsampling = tank_rate_ < host_rate_;
452 const StereoSectionLanes sections[2] = {StereoSectionLanes::from(sections_[0]),
453 StereoSectionLanes::from(sections_[1])};
454 StereoBiquadState<2> aa = aa_out_;
455 for (int i = 0; i < tank_n; ++i) {
456 const lanes::D2 v = lanes::make(static_cast<double>(finite_or_zero(tl[i])),
457 static_cast<double>(finite_or_zero(tr[i])));
458 out_ring_.push(downsampling ? v : aa.process(sections, v));
459 }
460 for (int i = 0; i < host_n; ++i) {
461 lanes::D2 v = out_ring_.read(next_host_pos_);
462 if (downsampling)
463 v = aa.process(sections, v);
464 left[i] = static_cast<SampleType>(lanes::lane(v, 0));
465 right[i] = static_cast<SampleType>(lanes::lane(v, 1));
466 next_host_pos_ = host_position(++host_count_);
467 }
468 aa_out_ = aa;
469 }
470
471 private:
473 StereoPositionRing<SampleType> out_ring_{};
474 std::array<std::vector<SampleType>, kNumRails> tank_in_{};
475 std::array<std::vector<SampleType>, kNumRails> tank_out_{};
476 StereoBiquadState<2> aa_in_{};
477 StereoBiquadState<2> aa_out_{};
478 ButterworthPair filter_{};
479 StereoSection sections_[2] = {};
480
481 double host_rate_ = 48000.0;
482 double tank_rate_ = 20000.0;
483 double host_per_tank_ = 1.0;
484 double tank_per_host_ = 1.0;
485 // Read positions are derived from sample counts, not accumulated: adding
486 // an inexact rate ratio once per sample drifts by whole samples over a
487 // long session, until the output leg reads past what the tank wrote.
488 double tank_position(std::int64_t count) const {
489 return static_cast<double>(count) * host_rate_ / tank_rate_;
490 }
491 double host_position(std::int64_t count) const {
492 return host_origin_ + static_cast<double>(count) * tank_rate_ / host_rate_;
493 }
494
495 std::int64_t tank_count_ = 0;
496 std::int64_t host_count_ = 0;
497 double host_origin_ = 0.0;
498 double next_tank_pos_ = 0.0;
499 double next_host_pos_ = 0.0;
500 int max_block_ = 0;
501 int max_tank_samples_ = 0;
502 bool resampling_ = false;
503};
504
505} // namespace pulp::signal::fdn
Definition biquad.hpp:53
@ lowpass
Definition biquad.hpp:58
void set_coefficients(const BiquadCoefficientsT< U > &c)
Definition biquad.hpp:69
Coefficients coefficients() const
Definition biquad.hpp:81
Definition multirate.hpp:110
void design(double cutoff_hz, double sample_rate)
Definition multirate.hpp:112
const std::array< BiquadCoefficientsT< double >, 2 > & sections() const
Definition multirate.hpp:122
Definition multirate.hpp:131
double process(const std::array< BiquadCoefficientsT< double >, 2 > &sections, double x)
Definition multirate.hpp:135
void reset()
Definition multirate.hpp:133
Definition multirate.hpp:310
static constexpr int kNumRails
Definition multirate.hpp:312
SampleType * tank_input(int rail)
Definition multirate.hpp:394
double input_read_position() const
Definition multirate.hpp:387
double tank_rate() const
Definition multirate.hpp:347
void configure(double tank_rate)
Definition multirate.hpp:332
int host_to_tank(const SampleType *left, const SampleType *right, int n)
Definition multirate.hpp:401
SampleType * tank_output(int rail)
Definition multirate.hpp:395
bool resampling() const
Definition multirate.hpp:348
double output_read_position() const
Definition multirate.hpp:390
double interpolation_skew_samples() const
Definition multirate.hpp:379
void tank_to_host(int tank_n, SampleType *left, SampleType *right, int host_n)
Definition multirate.hpp:441
void prepare(double host_rate, int max_block)
Definition multirate.hpp:314
void reset()
Definition multirate.hpp:353
Definition multirate.hpp:63
std::int64_t written() const
Definition multirate.hpp:83
void reset()
Definition multirate.hpp:73
void prepare(int capacity)
Definition multirate.hpp:65
double read(double position) const
Definition multirate.hpp:88
void push(SampleType x)
Definition multirate.hpp:78
double readable_through() const
Definition multirate.hpp:86
Definition multirate.hpp:252
void reset()
Definition multirate.hpp:254
lanes::D2 process(const StereoSection *sections, lanes::D2 x)
Definition multirate.hpp:262
void heal()
Definition multirate.hpp:286
lanes::D2 process(const StereoSectionLanes *sections, lanes::D2 x)
Definition multirate.hpp:269
Definition multirate.hpp:150
double readable_through() const
Definition multirate.hpp:180
void prepare(int capacity)
Definition multirate.hpp:152
void reset()
Definition multirate.hpp:161
std::int64_t written() const
Definition multirate.hpp:177
lanes::D2 read(double position) const
Definition multirate.hpp:184
void push(lanes::D2 x)
Definition multirate.hpp:166
Definition config.hpp:15
T finite_or_zero(T value)
Definition sanitize.hpp:12
constexpr int kHealIntervalSamples
Definition config.hpp:195
constexpr double kMaxTankRate
Definition config.hpp:37
constexpr double kButterworthQ1
Definition config.hpp:169
@ count
Definition config.hpp:227
@ size
Definition config.hpp:216
constexpr double kAaCutoffFraction
Definition config.hpp:170
constexpr std::array< double, 8 > kTankRates
Definition config.hpp:34
constexpr int kHermiteGuard
Definition config.hpp:171
constexpr double kButterworthQ0
Definition config.hpp:168
double hermite4(double y0, double y1, double y2, double y3, double t)
Definition interp.hpp:27
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ 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
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
Definition biquad.hpp:24
Definition multirate.hpp:243
lanes::D2 a2
Definition multirate.hpp:244
static StereoSectionLanes from(const StereoSection &s)
Definition multirate.hpp:246
lanes::D2 c2
Definition multirate.hpp:244
lanes::D2 a1
Definition multirate.hpp:244
lanes::D2 b0
Definition multirate.hpp:244
lanes::D2 c1
Definition multirate.hpp:244
Definition multirate.hpp:230
static StereoSection from(const BiquadCoefficientsT< double > &c)
Definition multirate.hpp:233
double a1
Definition multirate.hpp:231
double b0
Definition multirate.hpp:231
double a2
Definition multirate.hpp:231
double c2
Definition multirate.hpp:231
double c1
Definition multirate.hpp:231