Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
oversampling_fir.hpp
Go to the documentation of this file.
1#pragma once
2
5
8#include <pulp/simd/simd.hpp>
9
10#include <algorithm>
11#include <array>
12#include <cmath>
13#include <cstddef>
14#include <vector>
15
16namespace pulp::signal {
17
21inline constexpr std::size_t kHouseHalfBandTaps = 65;
22inline constexpr double kHouseHalfBandStopbandDb = 81.3; // Kaiser beta ~= 8
23inline constexpr double kHouseHalfBandStageOnePassband = 0.45;
24inline constexpr double kHouseHalfBandStageTwoPassband = 0.225;
25
26namespace detail {
27
28// Fixed-storage linear-phase half-band stage for nonlinear processors whose
29// configuration and processing paths must both remain allocation-free. The
30// general LinearPhaseOversamplingStage2x below supports selectable quality and
31// tap counts; this stage deliberately fixes the 65-tap prototype so its state
32// can live entirely inline.
33//
34// Half-band structure: apart from the centre, only odd-offset taps are
35// non-zero, and an odd offset always reaches a sample of the opposite time
36// parity. The stage therefore keeps one linear history per parity; each
37// output is the centre tap on its own-parity history plus one contiguous
38// 32-tap pulp::simd::dot over the other parity's history. Histories move
39// their newest samples back to the front once every kCapacity writes.
41 public:
42 FixedHalfBandFir65() noexcept {
43 // Force the shared coefficient tables to initialize off the audio path.
44 static_cast<void>(coefficients());
45 static_cast<void>(odd_taps_oldest_first());
46 }
47
48 double process(double input) noexcept {
49 auto& own = parity_[phase_];
50 auto& other = parity_[phase_ ^ 1u];
51 if (own.head == kHistory)
52 own.compact();
53 own.samples[own.head++] = input;
54 // x[n - 32] is 16 own-parity samples back; x[n - 1 - 2j] for
55 // j = 0..31 are the other parity's newest 32, oldest first.
56 const double centre = coefficients()[kCentre] * own.samples[own.head - 1 - kCentre / 2];
57 const double odd = pulp::simd::dot(other.samples.data() + other.head - kOddTaps,
58 odd_taps_oldest_first().data(), kOddTaps);
59 phase_ ^= 1u;
60 return centre + odd;
61 }
62
63 void reset() noexcept {
64 for (auto& p : parity_) {
65 p.samples.fill(0.0);
66 p.head = kKeep;
67 }
68 phase_ = 0;
69 }
70
72 static const std::array<double, 65>& coefficients() noexcept {
73 static const std::array<double, kTaps> result = design();
74 return result;
75 }
76
77 private:
78 static constexpr std::size_t kTaps = 65;
79 static constexpr std::size_t kCentre = (kTaps - 1) / 2;
80 static constexpr std::size_t kOddTaps = (kTaps - 1) / 2;
81 // Each parity history must reach back kOddTaps samples (the dot) and
82 // kCentre / 2 + 1 samples (the centre tap).
83 static constexpr std::size_t kKeep = kOddTaps;
84 static constexpr std::size_t kCapacity = 64;
85 static constexpr std::size_t kHistory = kKeep + kCapacity;
86 static constexpr double kBeta = 8.0;
87
88 static double sinc(double x) noexcept {
89 if (std::abs(x) < 1.0e-15)
90 return 1.0;
91 constexpr double kPi = 3.141592653589793238462643383279502884;
92 const double pix = kPi * x;
93 return std::sin(pix) / pix;
94 }
95
96 static std::array<double, kTaps> design() noexcept {
97 std::array<double, kTaps> result{};
98 const double denominator = bessel_i0(kBeta);
99 double sum = 0.0;
100 for (std::size_t tap = 0; tap < kTaps; ++tap) {
101 const int offset =
102 static_cast<int>(tap) - static_cast<int>(kCentre);
103 if (offset != 0 && (offset & 1) == 0) {
104 result[tap] = 0.0;
105 continue;
106 }
107 const double position =
108 static_cast<double>(offset) / static_cast<double>(kCentre);
109 const double window =
110 bessel_i0(kBeta * std::sqrt(
111 std::max(0.0, 1.0 - position * position))) /
112 denominator;
113 result[tap] =
114 0.5 * sinc(0.5 * static_cast<double>(offset)) * window;
115 sum += result[tap];
116 }
117 for (double& coefficient : result)
118 coefficient /= sum;
119 return result;
120 }
121
122 // Odd-offset taps t = 63, 61, ..., 1 (oldest sample first).
123 static const std::array<double, kOddTaps>& odd_taps_oldest_first() noexcept {
124 static const std::array<double, kOddTaps> result = [] {
125 std::array<double, kOddTaps> taps{};
126 for (std::size_t j = 0; j < kOddTaps; ++j)
127 taps[j] = coefficients()[kTaps - 2 - 2 * j];
128 return taps;
129 }();
130 return result;
131 }
132
133 struct ParityHistory {
134 // [0, head) holds past same-parity input, oldest first; head >= kKeep.
135 std::array<double, kHistory> samples{};
136 std::size_t head = kKeep;
137
138 void compact() noexcept {
139 std::copy(samples.begin() + static_cast<std::ptrdiff_t>(head - kKeep),
140 samples.begin() + static_cast<std::ptrdiff_t>(head), samples.begin());
141 head = kKeep;
142 }
143 };
144
145 std::array<ParityHistory, 2> parity_{};
146 unsigned phase_ = 0;
147};
148
153inline std::vector<double> linear_phase_oversampling_prototype(double passband_edge_fraction,
154 double stopband_db,
155 std::size_t taps) {
156 const double passband = 0.5 * passband_edge_fraction;
157 constexpr double stopband = 0.25;
158 const double transition = stopband - passband;
159 const double cutoff = passband + 0.5 * transition;
160 return design_windowed_sinc(taps, cutoff, kaiser_beta_for_stopband(stopband_db));
161}
162
163template <typename SampleType> class LinearPhaseOversamplingStage2x {
164 public:
165 void configure(double passband_edge_fraction, double stopband_db, std::size_t taps) {
166 auto prototype =
167 linear_phase_oversampling_prototype(passband_edge_fraction, stopband_db, taps);
168
169 std::vector<SampleType> up_even;
170 std::vector<SampleType> up_odd;
171 std::vector<SampleType> down_even;
172 std::vector<SampleType> down_odd;
173 up_even.reserve((prototype.size() + 1) / 2);
174 up_odd.reserve(prototype.size() / 2);
175 down_even.reserve((prototype.size() + 1) / 2);
176 down_odd.reserve(prototype.size() / 2);
177 for (std::size_t i = 0; i < prototype.size(); ++i) {
178 const auto coefficient = static_cast<SampleType>(prototype[i]);
179 auto& up_phase = (i & 1u) == 0u ? up_even : up_odd;
180 auto& down_phase = (i & 1u) == 0u ? down_even : down_odd;
181 up_phase.push_back(SampleType{2} * coefficient);
182 down_phase.push_back(coefficient);
183 }
184 up_even_.set_coefficients(std::move(up_even));
185 up_odd_.set_coefficients(std::move(up_odd));
186 down_even_.set_coefficients(std::move(down_even));
187 down_odd_.set_coefficients(std::move(down_odd));
188 taps_ = taps;
189 }
190
191 void reset() {
192 up_even_.reset();
193 up_odd_.reset();
194 down_even_.reset();
195 down_odd_.reset();
196 previous_odd_ = SampleType{0};
197 }
198
199 void upsample(SampleType input, SampleType& even, SampleType& odd) {
200 even = up_even_.process(input);
201 odd = up_odd_.process(input);
202 }
203
204 SampleType downsample(SampleType even, SampleType odd) {
205 const SampleType output = down_even_.process(even) + down_odd_.process(previous_odd_);
206 previous_odd_ = odd;
207 return output;
208 }
209
210 std::size_t taps() const noexcept {
211 return taps_;
212 }
213
214 private:
215 FirFilterT<SampleType> up_even_;
217 FirFilterT<SampleType> down_even_;
218 FirFilterT<SampleType> down_odd_;
219 SampleType previous_odd_ = SampleType{0};
220 std::size_t taps_ = 0;
221};
222
234#if !defined(PULP_SIGNAL_HARDWARE_FMA)
235#if defined(__aarch64__) || defined(_M_ARM64)
236#define PULP_SIGNAL_HARDWARE_FMA 1
237#else
238#define PULP_SIGNAL_HARDWARE_FMA 0
239#endif
240#endif
241
246template <typename SampleType>
247inline SampleType fir_multiply_add(SampleType a, SampleType b, SampleType acc) noexcept {
248#if PULP_SIGNAL_HARDWARE_FMA
249 return std::fma(a, b, acc);
250#else
251 return acc + a * b;
252#endif
253}
254
266template <typename SampleType> class FixedOrderFirBranch {
267 public:
268 static constexpr std::size_t kBlock = 256;
269
270 void set_coefficients(const std::vector<SampleType>& time_order) {
271 reversed_.assign(time_order.rbegin(), time_order.rend());
272 history_.assign(reversed_.empty() ? 0 : reversed_.size() - 1 + kBlock, SampleType{0});
273 reset();
274 }
275
276 void reset() {
277 std::fill(history_.begin(), history_.end(), SampleType{0});
278 }
279
280 std::size_t taps() const noexcept {
281 return reversed_.size();
282 }
283
285 void process(const SampleType* input, SampleType* output, std::size_t n) noexcept {
286 const std::size_t taps = reversed_.size();
287 const std::size_t keep = taps - 1;
288 SampleType* history = history_.data();
289 std::copy_n(input, n, history + keep);
290 const SampleType* h = reversed_.data();
291 constexpr std::size_t kLanes = 16;
292 std::size_t i = 0;
293 for (; i + kLanes <= n; i += kLanes) {
294 SampleType acc[kLanes] = {};
295 const SampleType* x = history + i;
296 for (std::size_t k = 0; k < taps; ++k) {
297 const SampleType c = h[k];
298 for (std::size_t lane = 0; lane < kLanes; ++lane)
299 acc[lane] = fir_multiply_add(c, x[k + lane], acc[lane]);
300 }
301 std::copy_n(acc, kLanes, output + i);
302 }
303 for (; i < n; ++i) {
304 SampleType acc = SampleType{0};
305 const SampleType* x = history + i;
306 for (std::size_t k = 0; k < taps; ++k)
307 acc = fir_multiply_add(h[k], x[k], acc);
308 output[i] = acc;
309 }
310 std::copy(history + n, history + n + keep, history);
311 }
312
313 private:
314 std::vector<SampleType> reversed_;
315 std::vector<SampleType> history_;
316};
317
322template <typename SampleType> class LinearPhaseOversamplingBlockStage2x {
323 public:
324 static constexpr std::size_t kBlock = FixedOrderFirBranch<SampleType>::kBlock;
325
326 void configure(double passband_edge_fraction, double stopband_db, std::size_t taps) {
327 const auto prototype =
328 linear_phase_oversampling_prototype(passband_edge_fraction, stopband_db, taps);
329 std::vector<SampleType> up_even, up_odd, down_even, down_odd;
330 for (std::size_t i = 0; i < prototype.size(); ++i) {
331 const auto coefficient = static_cast<SampleType>(prototype[i]);
332 ((i & 1u) == 0u ? up_even : up_odd).push_back(SampleType{2} * coefficient);
333 ((i & 1u) == 0u ? down_even : down_odd).push_back(coefficient);
334 }
335 up_even_.set_coefficients(up_even);
336 up_odd_.set_coefficients(up_odd);
337 down_even_.set_coefficients(down_even);
338 down_odd_.set_coefficients(down_odd);
339 taps_ = taps;
340 reset();
341 }
342
343 void reset() {
344 up_even_.reset();
345 up_odd_.reset();
346 down_even_.reset();
347 down_odd_.reset();
348 previous_odd_ = SampleType{0};
349 }
350
351 std::size_t taps() const noexcept {
352 return taps_;
353 }
354
357 void upsample(const SampleType* input, SampleType* output, std::size_t n) noexcept {
358 for (std::size_t done = 0; done < n; done += kBlock) {
359 const std::size_t count = std::min(kBlock, n - done);
360 up_even_.process(input + done, even_.data(), count);
361 up_odd_.process(input + done, odd_.data(), count);
362 SampleType* out = output + 2 * done;
363 for (std::size_t i = 0; i < count; ++i) {
364 out[2 * i] = even_[i];
365 out[2 * i + 1] = odd_[i];
366 }
367 }
368 }
369
372 void downsample(const SampleType* input, SampleType* output, std::size_t n) noexcept {
373 for (std::size_t done = 0; done < n; done += kBlock) {
374 const std::size_t count = std::min(kBlock, n - done);
375 const SampleType* in = input + 2 * done;
376 for (std::size_t i = 0; i < count; ++i) {
377 split_even_[i] = in[2 * i];
378 split_odd_[i] = i == 0 ? previous_odd_ : in[2 * i - 1];
379 }
380 previous_odd_ = in[2 * count - 1];
381 down_even_.process(split_even_.data(), even_.data(), count);
382 down_odd_.process(split_odd_.data(), odd_.data(), count);
383 for (std::size_t i = 0; i < count; ++i)
384 output[done + i] = even_[i] + odd_[i];
385 }
386 }
387
388 private:
393 std::array<SampleType, kBlock> split_even_{};
394 std::array<SampleType, kBlock> split_odd_{};
395 std::array<SampleType, kBlock> even_{};
396 std::array<SampleType, kBlock> odd_{};
397 SampleType previous_odd_ = SampleType{0};
398 std::size_t taps_ = 0;
399};
400
401} // namespace detail
402
403} // namespace pulp::signal
FIR filter with arbitrary coefficients.
Definition fir_filter.hpp:39
double process(double input) noexcept
Definition oversampling_fir.hpp:48
static const std::array< double, 65 > & coefficients() noexcept
The 65 prototype taps (symmetric; centre index 32; even offsets zero).
Definition oversampling_fir.hpp:72
void reset() noexcept
Definition oversampling_fir.hpp:63
FixedHalfBandFir65() noexcept
Definition oversampling_fir.hpp:42
One polyphase branch of a block 2x stage: a direct-form FIR whose every output is the taps summed old...
Definition oversampling_fir.hpp:266
std::size_t taps() const noexcept
Definition oversampling_fir.hpp:280
void process(const SampleType *input, SampleType *output, std::size_t n) noexcept
n <= kBlock. output must not alias input.
Definition oversampling_fir.hpp:285
void reset()
Definition oversampling_fir.hpp:276
void set_coefficients(const std::vector< SampleType > &time_order)
Definition oversampling_fir.hpp:270
static constexpr std::size_t kBlock
Definition oversampling_fir.hpp:268
The block form of LinearPhaseOversamplingStage2x, for OversamplerT: the same prototype and the same p...
Definition oversampling_fir.hpp:322
std::size_t taps() const noexcept
Definition oversampling_fir.hpp:351
void configure(double passband_edge_fraction, double stopband_db, std::size_t taps)
Definition oversampling_fir.hpp:326
void downsample(const SampleType *input, SampleType *output, std::size_t n) noexcept
2n interleaved inputs to n outputs, the odd branch one input sample behind.
Definition oversampling_fir.hpp:372
void reset()
Definition oversampling_fir.hpp:343
void upsample(const SampleType *input, SampleType *output, std::size_t n) noexcept
n input samples to 2n interleaved outputs.
Definition oversampling_fir.hpp:357
static constexpr std::size_t kBlock
Definition oversampling_fir.hpp:324
Definition oversampling_fir.hpp:163
SampleType downsample(SampleType even, SampleType odd)
Definition oversampling_fir.hpp:204
std::size_t taps() const noexcept
Definition oversampling_fir.hpp:210
void reset()
Definition oversampling_fir.hpp:191
void configure(double passband_edge_fraction, double stopband_db, std::size_t taps)
Definition oversampling_fir.hpp:165
void upsample(SampleType input, SampleType &even, SampleType &odd)
Definition oversampling_fir.hpp:199
Finite Impulse Response filter with configurable order and coefficients.
Definition design_adherence.hpp:32
Round-2 effect-wave vocabulary.
Definition analysis_frontends.hpp:50
SampleType fir_multiply_add(SampleType a, SampleType b, SampleType acc) noexcept
acc + a * b for the block FIR: one fused multiply-add where the hardware has it, a separate multiply ...
Definition oversampling_fir.hpp:247
std::vector< double > linear_phase_oversampling_prototype(double passband_edge_fraction, double stopband_db, std::size_t taps)
The Kaiser-windowed lowpass prototype both linear-phase 2x stage forms split into polyphase branches.
Definition oversampling_fir.hpp:153
Definition additive_bank.hpp:218
float sinc(float x)
Sinc function: sin(pi*x) / (pi*x), with sinc(0) = 1.
Definition special_functions.hpp:17
constexpr double kHouseHalfBandStopbandDb
Definition oversampling_fir.hpp:22
@ c
Definition spectrum_trace.hpp:35
constexpr std::size_t kHouseHalfBandTaps
Pulp's low-latency linear-phase half-band design.
Definition oversampling_fir.hpp:21
constexpr double kHouseHalfBandStageTwoPassband
Definition oversampling_fir.hpp:24
float bessel_i0(float x)
Modified Bessel function of the first kind, order 0 Used in Kaiser window design.
Definition special_functions.hpp:25
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
constexpr double kHouseHalfBandStageOnePassband
Definition oversampling_fir.hpp:23
std::vector< double > design_windowed_sinc(std::size_t taps, double cutoff, double beta)
Definition windowed_sinc_design.hpp:58
double kaiser_beta_for_stopband(double stopband_db)
Definition windowed_sinc_design.hpp:27
float sum(const float *data, std::size_t count) noexcept
@ j
Definition input_events.hpp:40
Shared Kaiser-windowed-sinc FIR design helpers.