Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
oversampling.hpp
Go to the documentation of this file.
1#pragma once
2
7
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <complex>
12#include <cstddef>
13#include <functional>
14#include <numbers>
15#include <utility>
16
17namespace pulp::signal {
18
19// Oversampling processor — runs a callback at 2x, 4x, 8x, or 16x sample rate
20// with anti-aliasing filters on input and output.
21//
22// Three filter `Kind`s are available:
23//
24// * `fir_biquad` — a Butterworth Biquad pair per 2x stage.
25// Lightweight and symmetric, but a single biquad cannot reject much near
26// the fold boundary: worst-case base-band alias rejection is only around
27// 7 dB, and the passband is about -1.9 dB at 0.3 of the base rate. Those
28// figures hold at every factor — raising the factor buys headroom for the
29// callback's own harmonics, not a cleaner filter. Reach for one of the
30// kinds below when the anti-aliasing itself has to be good.
31// * `polyphase_iir` (default) — elliptic polyphase half-band IIR per 2x
32// stage (`design_halfband_iir`). Minimum phase, so no constant latency,
33// and the cheapest of the three: about a sixth of the biquad lane's cost
34// at x2 while rejecting about 106 dB more. Its round trip delays the base band
35// by 3.79 samples near DC at x2 (4.90 at x4), rising toward the passband
36// edge, against the biquad lane's 0.97 (1.46). Both qualities keep the
37// base band flat (< 1e-8 dB droop) to 0.485 Fs and reject images and
38// aliases from 0.515 Fs: by 114.6 dB at `Quality::standard` and by
39// 153.2 dB at `Quality::pristine`, in the float
40// build too: the stages keep their state in double, so a float round trip
41// adds only the rounding of its float samples (about -150 dB). Upper
42// stages only guard the base band, so they use short designs.
43// `polyphase_stopband_db()` returns the per-quality figure,
44// `polyphase_stage_design()` each stage's design, and
45// `group_delay_samples()` the round trip's frequency-dependent delay.
46// * `linear_phase_fir` — Kaiser-windowed FIR with a transition that ends at
47// the base-rate Nyquist. `Quality::standard` is a 96 dB design flat to
48// 80% of Nyquist; `Quality::pristine` uses a 140 dB prototype flat to 90%.
49// Pristine is a deliberately stricter tier than the ~86-90 dB equiripple
50// half-band designs common elsewhere: its x2 stage is 385 taps (192
51// samples of latency), so it costs more than such a design and should be
52// compared with one only at a matched rejection. Standard is the tier to
53// compare against those designs on cost. Each branch filters a whole
54// chunk at a time (`detail::LinearPhaseOversamplingBlockStage2x`).
55//
56// A half-band IIR's transition is symmetric about base Nyquist, so content
57// in (0.5, 0.515) Fs folds onto (0.485, 0.5) Fs with only partial rejection:
58// above the passband and, at 44.1 kHz or more, above 21 kHz. The linear-phase
59// lane ends its transition at Nyquist instead and has no such region.
60//
61// Switch with `set_kind()`. All kinds share the same callback API so
62// callers don't need to know which filter is active. Configure factor,
63// sample rate, and kind outside the audio callback; `process()` and
64// `process_block()` do not allocate after construction/configuration as
65// long as the supplied callback is also allocation-free.
66//
67// For the polyphase IIR and linear-phase lanes, `process_block()` runs each
68// filter stage over a whole chunk of samples and calls the callback for every
69// oversampled sample of the chunk before any decimation. The callback sees
70// exactly the samples, in exactly the order, that per-sample `process()`
71// calls would hand it, and the output is identical — every stage is causal
72// and processes its input in time order either way. The one exception: the
73// polyphase stages flush state below ~1e-30 (float) to zero at the end of
74// each chunk so silence decays to exact zero instead of a subnormal limit
75// cycle, and per-sample calls flush every sample, so the two can differ by
76// that much while a signal is dying away.
77//
78template <typename SampleType = float> class OversamplerT {
79 public:
80 enum class Factor { x2 = 2, x4 = 4, x8 = 8, x16 = 16 };
81
88
89 enum class Quality {
92 };
93
94 struct Latency {
96 double exact_input_samples = 0.0;
97 bool constant = false;
98 };
99
101 configure_filters();
102 }
103
112 factor_ = f;
113 configure_filters();
114 reset();
115 }
116 void set_sample_rate(SampleType sr) {
117 sample_rate_ = sr;
118 configure_filters();
119 reset();
120 }
122 kind_ = kind;
123 configure_kind_filters();
124 reset();
125 }
127 quality_ = quality;
128 configure_kind_filters();
129 reset();
130 }
131 Kind kind() const {
132 return kind_;
133 }
134 Quality quality() const {
135 return quality_;
136 }
137 Factor factor() const {
138 return factor_;
139 }
140 int factor_value() const {
141 return static_cast<int>(factor_);
142 }
143
148 Latency latency() const noexcept {
149 if (kind_ != Kind::linear_phase_fir)
150 return {};
151 double exact = 0.0;
152 const int stages = stage_count();
153 for (int stage = 0; stage < stages; ++stage) {
154 exact += static_cast<double>(fir_stages_[stage].taps() - 1u) /
155 static_cast<double>(1u << (stage + 1));
156 }
157 return {static_cast<int>(exact), exact, true};
158 }
159
160 int latency_samples() const noexcept {
161 return latency().input_samples;
162 }
163
170 double group_delay_samples(double frequency) const noexcept {
171 if (kind_ == Kind::linear_phase_fir)
173 double total = 0.0;
174 const int stages = stage_count();
175 for (int stage = 0; stage < stages; ++stage) {
176 const double rate = static_cast<double>(1u << (stage + 1));
177 const double f = frequency / rate;
178 if (kind_ == Kind::polyphase_iir) {
179 // The interpolator is H on the zero-stuffed input; the
180 // decimator keeps H's odd-phase samples, one high-rate sample
181 // ahead of H's own delay.
182 const auto& design = iir_designs_[static_cast<std::size_t>(stage)];
183 total += (2.0 * halfband_iir_group_delay<SampleType>(design, f) - 1.0) / rate;
184 } else {
185 // Both biquads of a stage see the full high-rate stream and the
186 // decimator keeps even-phase samples.
187 total += 2.0 *
189 biquad_up_stages_[static_cast<std::size_t>(stage)].coefficients(),
190 2.0 * std::numbers::pi * f) /
191 rate;
192 }
193 }
194 return total;
195 }
196
202 // Stage 0 fixes the base-band edge; stage s only has to keep the same
203 // absolute band clean at 2^s times the rate, so its edge halves each
204 // stage and a much shorter design reaches the same floor. The base
205 // edge, 0.2425 of the 2x rate, is 0.485 Fs: a 0.03 Fs transition, so
206 // anything a callback creates between 0.515 Fs and the stage's own
207 // Nyquist meets the full stopband. Both qualities share it; pristine
208 // spends its extra sections on depth.
209 const bool pristine = quality == Quality::pristine;
210 constexpr std::array<std::size_t, kMaxStages> kStandardCounts = {12, 4, 3, 2};
211 constexpr std::array<std::size_t, kMaxStages> kPristineCounts = {16, 6, 4, 3};
212 constexpr double base_edge = 0.2425;
213 if (stage < 0 || stage >= static_cast<int>(kMaxStages))
214 return {};
215 const auto index = static_cast<std::size_t>(stage);
216 const double edge = base_edge / static_cast<double>(1u << index);
217 return design_halfband_iir(pristine ? kPristineCounts[index] : kStandardCounts[index],
218 edge);
219 }
220
226 static double polyphase_stopband_db(Quality quality) noexcept {
227 double worst = -1e300;
228 for (int stage = 0; stage < static_cast<int>(kMaxStages); ++stage)
229 worst =
231 return worst;
232 }
233
234 // Process a single sample with oversampled callback.
235 // The callback receives an upsampled sample and returns the processed
236 // output. The same callback fires N times per `process()` call for factor
237 // xN (the loop runs at the oversampled rate). Prefer this templated path
238 // for realtime use; it does not need to type-erase callbacks.
239 template <typename Callback> SampleType process(SampleType input, Callback&& callback) {
240 SampleType output;
241 process_chunk(&input, &output, 1, callback);
242 return output;
243 }
244
245 // Convenience overload for callers that already hold a std::function. Taken by
246 // const reference: a by-value parameter would copy the callable — and so heap
247 // -allocate, for any payload too large for the small-object buffer — on every
248 // sample, including once per sample underneath process_block(). Invoking a
249 // std::function still costs an indirect call; hot paths want the templated
250 // overload above.
251 SampleType process(SampleType input, const std::function<SampleType(SampleType)>& callback) {
252 SampleType output;
253 process_chunk(&input, &output, 1, callback);
254 return output;
255 }
256
257 // Process a contiguous block through the same callback. `input` and
258 // `output` may alias for in-place processing.
259 template <typename Callback>
260 void process_block(const SampleType* input, SampleType* output, std::size_t num_samples,
261 Callback&& callback) {
262 const std::size_t chunk = kScratchSamples / static_cast<std::size_t>(factor_value());
263 for (std::size_t offset = 0; offset < num_samples; offset += chunk) {
264 const std::size_t count = std::min(chunk, num_samples - offset);
265 process_chunk(input + offset, output + offset, count, callback);
266 }
267 }
268
269 void reset() {
270 for (auto& stage : biquad_up_stages_)
271 stage.reset();
272 for (auto& stage : biquad_down_stages_)
273 stage.reset();
274 for (auto& stage : iir_stages_)
275 stage.reset();
276 for (auto& stage : fir_stages_)
277 stage.reset();
278 }
279
280 private:
281 static constexpr std::size_t kMaxStages = 4;
282 // Oversampled samples per chunk; the base-rate chunk is this over the factor.
283 static constexpr std::size_t kScratchSamples = 512;
284 Factor factor_ = Factor::x2;
286 Quality quality_ = Quality::standard;
287 SampleType sample_rate_ = SampleType{44100.0f};
288
289 // Ping-pong buffers for the rate-changing stages.
290 std::array<SampleType, kScratchSamples> scratch_a_{};
291 std::array<SampleType, kScratchSamples> scratch_b_{};
292
293 static constexpr int factor_value(Factor factor) noexcept {
294 return static_cast<int>(factor);
295 }
296
297 int stage_count() const noexcept {
298 int count = 0;
299 for (int factor = factor_value(); factor > 1; factor >>= 1)
300 ++count;
301 return count;
302 }
303
304 // `count` must not exceed kScratchSamples / factor. `input` is fully
305 // consumed by the first stage before `output` is written, so they may
306 // alias.
307 template <typename Callback>
308 void process_chunk(const SampleType* input, SampleType* output, std::size_t count,
309 Callback& callback) {
310 if (kind_ == Kind::fir_biquad) {
311 for (std::size_t i = 0; i < count; ++i)
312 output[i] = process_biquad_sample(input[i], callback);
313 return;
314 }
315 const int stages = stage_count();
316 SampleType* buffers[2] = {scratch_a_.data(), scratch_b_.data()};
317 const SampleType* source = input;
318 std::size_t n = count;
319 int next = 0;
320 for (int stage = 0; stage < stages; ++stage) {
321 upsample_stage(stage, source, buffers[next], n);
322 source = buffers[next];
323 n *= 2;
324 next ^= 1;
325 }
326 SampleType* work = buffers[next ^ 1];
327 for (std::size_t i = 0; i < n; ++i)
328 work[i] = callback(work[i]);
329 for (int stage = stages; stage-- > 0;) {
330 n /= 2;
331 SampleType* destination = stage == 0 ? output : buffers[next];
332 downsample_stage(stage, work, destination, n);
333 work = destination;
334 next ^= 1;
335 }
336 }
337
338 void upsample_stage(int stage, const SampleType* input, SampleType* output, std::size_t n) {
339 const auto s = static_cast<std::size_t>(stage);
340 if (kind_ == Kind::polyphase_iir) {
341 iir_stages_[s].upsample(input, output, n);
342 return;
343 }
344 fir_stages_[s].upsample(input, output, n);
345 }
346
347 void downsample_stage(int stage, const SampleType* input, SampleType* output, std::size_t n) {
348 const auto s = static_cast<std::size_t>(stage);
349 if (kind_ == Kind::polyphase_iir) {
350 iir_stages_[s].downsample(input, output, n);
351 return;
352 }
353 fir_stages_[s].downsample(input, output, n);
354 }
355
356 // The biquad lane stays sample by sample. Each biquad is one serial
357 // recurrence, so running a stage over a whole chunk leaves the core waiting
358 // on a single dependency chain; interleaving every stage's up and down
359 // filters per input sample lets those independent chains overlap, and
360 // measured about twice as fast.
361 template <typename Callback>
362 SampleType process_biquad_sample(SampleType input, Callback& callback) {
363 constexpr auto kMax = static_cast<std::size_t>(Factor::x16);
364 std::array<SampleType, kMax> values; // only [0, count) is ever read
365 std::array<SampleType, kMax> expanded;
366 values[0] = input;
367 std::size_t count = 1;
368 const int stages = stage_count();
369 for (int stage = 0; stage < stages; ++stage) {
370 auto& up = biquad_up_stages_[static_cast<std::size_t>(stage)];
371 for (std::size_t i = 0; i < count; ++i) {
372 expanded[2 * i] = up.process(SampleType{2} * values[i]);
373 expanded[2 * i + 1] = up.process(SampleType{0});
374 }
375 count *= 2;
376 std::copy_n(expanded.begin(), count, values.begin());
377 }
378 for (std::size_t i = 0; i < count; ++i)
379 values[i] = callback(values[i]);
380 for (int stage = stages; stage-- > 0;) {
381 auto& down = biquad_down_stages_[static_cast<std::size_t>(stage)];
382 count /= 2;
383 for (std::size_t i = 0; i < count; ++i) {
384 values[i] = down.process(values[2 * i]);
385 static_cast<void>(down.process(values[2 * i + 1]));
386 }
387 }
388 return values[0];
389 }
390
391 void configure_kind_filters() {
392 if (kind_ == Kind::linear_phase_fir)
393 configure_linear_phase_filters();
394 else if (kind_ == Kind::polyphase_iir)
395 configure_polyphase_filters();
396 }
397
398 // ── fir_biquad lane ────────────────────────────────────────────────
399 std::array<BiquadT<SampleType>, kMaxStages> biquad_up_stages_;
400 std::array<BiquadT<SampleType>, kMaxStages> biquad_down_stages_;
401
402 void configure_filters() {
403 // A 2x stage decimates to 2^stage * Fs, so what it has to keep clean is that
404 // rate's Nyquist — half of it — not the base rate's. Putting each stage's
405 // cutoff at 0.8 of its own fold boundary hands every stage the same
406 // normalized filter, which is what stage 0's 0.4 * Fs already was. Reusing
407 // one absolute 0.4 * Fs cutoff at every stage instead would make each added
408 // stage lowpass the base band again, dragging the passband down with it.
409 const SampleType base_cutoff = sample_rate_ * SampleType{0.4f};
410 for (std::size_t stage = 0; stage < kMaxStages; ++stage) {
411 const SampleType stage_rate = sample_rate_ * static_cast<SampleType>(1u << (stage + 1));
412 const SampleType stage_cutoff = base_cutoff * static_cast<SampleType>(1u << stage);
413 biquad_up_stages_[stage].set_coefficients(BiquadT<SampleType>::Type::lowpass,
414 stage_cutoff, SampleType{0.707f}, stage_rate);
415 biquad_down_stages_[stage].set_coefficients(BiquadT<SampleType>::Type::lowpass,
416 stage_cutoff, SampleType{0.707f},
417 stage_rate);
418 }
419 configure_kind_filters();
420 }
421
422 void configure_linear_phase_filters() {
423 const bool pristine = quality_ == Quality::pristine;
424 fir_stages_[0].configure(pristine ? 0.45 : 0.40, pristine ? 140.0 : 96.0,
425 pristine ? 385u : 129u);
426 // Upper stages only keep distant images out of the next lower stage.
427 // Standard filters there preserve the base-band guarantee without
428 // multiplying the steepest filter's cost at high rates. These edges
429 // shrink as fractions because each stage's input rate doubles: 0.20 of
430 // 2Fs and 0.10 of 4Fs both preserve the same absolute 0.40Fs band.
431 if (pristine) {
432 fir_stages_[1].configure(0.225, 140.0, 69u);
433 fir_stages_[2].configure(0.1125, 140.0, 57u);
434 fir_stages_[3].configure(0.05625, 140.0, 49u);
435 } else {
436 fir_stages_[1].configure(0.20, 96.0, 49u);
437 fir_stages_[2].configure(0.10, 96.0, 33u);
438 fir_stages_[3].configure(0.05, 96.0, 33u);
439 }
440 }
441
442 // ── polyphase_iir lane ─────────────────────────────────────────────
443 std::array<HalfBandIirDesign, kMaxStages> iir_designs_{};
444 std::array<HalfBandIirStageT<SampleType>, kMaxStages> iir_stages_;
445
446 void configure_polyphase_filters() {
447 for (std::size_t stage = 0; stage < kMaxStages; ++stage) {
448 iir_designs_[stage] = polyphase_stage_design(quality_, static_cast<int>(stage));
449 iir_stages_[stage].configure(iir_designs_[stage]);
450 }
451 }
452
453 // ── linear_phase_fir lane ──────────────────────────────────────────
454 std::array<detail::LinearPhaseOversamplingBlockStage2x<SampleType>, kMaxStages> fir_stages_;
455};
456
459
460} // namespace pulp::signal
@ lowpass
Definition biquad.hpp:58
Definition oversampling.hpp:78
OversamplerT()
Definition oversampling.hpp:100
static double polyphase_stopband_db(Quality quality) noexcept
The polyphase lane's stopband floor at quality, in dB (negative): the worst equiripple level over eve...
Definition oversampling.hpp:226
void set_kind(Kind kind)
Definition oversampling.hpp:121
void set_sample_rate(SampleType sr)
Definition oversampling.hpp:116
int latency_samples() const noexcept
Definition oversampling.hpp:160
void reset()
Definition oversampling.hpp:269
Quality quality() const
Definition oversampling.hpp:134
int factor_value() const
Definition oversampling.hpp:140
SampleType process(SampleType input, Callback &&callback)
Definition oversampling.hpp:239
void set_factor(Factor f)
RT contract: set_factor(), set_sample_rate(), set_kind(), and reset() are setup/control operations th...
Definition oversampling.hpp:111
SampleType process(SampleType input, const std::function< SampleType(SampleType)> &callback)
Definition oversampling.hpp:251
void set_quality(Quality quality)
Definition oversampling.hpp:126
Latency latency() const noexcept
Linear-phase modes expose a constant delay suitable for host compensation.
Definition oversampling.hpp:148
void process_block(const SampleType *input, SampleType *output, std::size_t num_samples, Callback &&callback)
Definition oversampling.hpp:260
Kind kind() const
Definition oversampling.hpp:131
Quality
Definition oversampling.hpp:89
@ pristine
FIR: 140 dB, flat to 0.45 Fs. IIR: 153.2 dB, flat to 0.485 Fs.
Definition oversampling.hpp:91
@ standard
FIR: 96 dB, flat to 0.40 Fs. IIR: 114.6 dB, flat to 0.485 Fs.
Definition oversampling.hpp:90
double group_delay_samples(double frequency) const noexcept
Group delay of the round trip (upsample, identity callback, downsample) in base-rate samples at frequ...
Definition oversampling.hpp:170
static HalfBandIirDesign polyphase_stage_design(Quality quality, int stage) noexcept
The elliptic half-band design stage stage (0 to 3) uses in the polyphase_iir lane at quality.
Definition oversampling.hpp:201
Kind
Which anti-aliasing filter family the oversampler uses.
Definition oversampling.hpp:83
@ fir_biquad
Butterworth biquad lowpass on both sides; ~7 dB rejection.
Definition oversampling.hpp:84
@ linear_phase_fir
Linear-phase FIR with reported integer latency.
Definition oversampling.hpp:86
@ polyphase_iir
Elliptic half-band polyphase IIR (allpass network). Default.
Definition oversampling.hpp:85
Factor
Definition oversampling.hpp:80
@ x16
Definition oversampling.hpp:80
@ x2
Definition oversampling.hpp:80
@ x4
Definition oversampling.hpp:80
@ x8
Definition oversampling.hpp:80
Factor factor() const
Definition oversampling.hpp:137
Evaluate what a biquad (or a cascade of them) does to the spectrum.
Polyphase IIR allpass-network half-band filters for 2x oversampling.
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
Definition design_adherence.hpp:32
Definition additive_bank.hpp:218
double halfband_iir_group_delay(const HalfBandIirDesign &design, double frequency) noexcept
Group delay of H(z) in samples of the filter's own rate.
Definition halfband_iir.hpp:287
@ up
Upper sideband on both channels: f → f + Δf.
Definition frequency_shifter_ssb.hpp:315
@ down
Lower sideband on both channels: f → f − Δf.
Definition frequency_shifter_ssb.hpp:316
HalfBandIirDesign design_halfband_iir(std::size_t coefficient_count, double passband_edge) noexcept
Closed-form elliptic half-band design (see the file comment).
Definition halfband_iir.hpp:185
double section_group_delay(const BiquadCoefficientsT< SampleType > &c, double omega)
Group delay of one biquad section at omega (radians per sample), in samples: -d(arg H)/d(omega) for H...
Definition frequency_response.hpp:68
OversamplerT< double > Oversampler64
Definition oversampling.hpp:458
@ i
Definition formant_filter_bank.hpp:20
OversamplerT< float > Oversampler
Definition oversampling.hpp:457
@ exact
Definition fast_math.hpp:630
double halfband_iir_stopband_db(const HalfBandIirDesign &design) noexcept
Worst-case (largest) stopband magnitude in dB over [stopband_edge(), 0.5], sampled densely enough to ...
Definition halfband_iir.hpp:301
@ s
Definition input_events.hpp:41
@ n
Definition input_events.hpp:40
Linear-phase 2x FIR stage used by the realtime oversampler.
Coefficients of one elliptic polyphase half-band filter, ascending.
Definition halfband_iir.hpp:148
Definition oversampling.hpp:94
double exact_input_samples
Definition oversampling.hpp:96
bool constant
Definition oversampling.hpp:97
int input_samples
Definition oversampling.hpp:95