DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Oversampling.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
33#include "AudioBuffer.h"
34#include "AudioSpec.h"
35#include "FIRFilter.h"
36#include "SimdOps.h"
37
38#include <algorithm>
39#include <array>
40#include <cassert>
41#include <cstring>
42#include <vector>
43
44namespace dspark {
45
70template <typename T, int MaxChannels = 16>
72{
73public:
77 enum class Quality
78 {
79 Low,
80 Medium,
81 High,
82 Maximum
83 };
84
93 explicit Oversampling(int factor = 2, Quality quality = Quality::High)
94 : quality_(quality)
95 {
96 assert(factor >= 1 && (factor & (factor - 1)) == 0 && "Factor must be a power of 2");
97 assert(factor <= (1 << kMaxStages) && "Factor must be <= 16 (kMaxStages stages)");
98 // Release-safe normalisation: clamp into [1, 2^kMaxStages], then derive
99 // the stage count and re-derive factor_ = 2^numStages_ so the pair stays
100 // coherent even for a non-power-of-two request. An incoherent pair
101 // (e.g. factor_ = 3 driving one 2x stage) would make downsample() emit
102 // factor_/2 samples per base sample -- overrunning the caller's output
103 // buffer -- and overflow the per-stage histories sized for 2^numStages_.
104 factor = std::clamp(factor, 1, 1 << kMaxStages);
105 numStages_ = 0;
106 while ((1 << (numStages_ + 1)) <= factor)
107 ++numStages_;
108 factor_ = 1 << numStages_;
109 }
110
116 void prepare(const AudioSpec& spec)
117 {
118 baseSpec_ = spec;
119 upBuffer_.resize(spec.numChannels, spec.maxBlockSize * factor_);
120
121 const int taps = tapsForQuality(quality_);
122 const T beta = betaForQuality(quality_);
123
124 for (int stage = 0; stage < numStages_; ++stage)
125 {
126 // The max block size entering a stage is baseSize * 2^stage
127 int stageMaxSamples = spec.maxBlockSize * (1 << stage);
128 filters_[stage].design(taps, beta, spec.numChannels, stageMaxSamples);
129 }
130
131 reset();
132 }
133
138 void reset() noexcept
139 {
140 upBuffer_.clear();
141 for (int i = 0; i < numStages_; ++i)
142 filters_[i].reset();
143 }
144
146 [[nodiscard]] int getFactor() const noexcept { return factor_; }
147
149 [[nodiscard]] Quality getQuality() const noexcept { return quality_; }
150
155 [[nodiscard]] int getLatency() const noexcept
156 {
157 if (numStages_ == 0) return 0;
158 const int halfOrder = (tapsForQuality(quality_) - 1) / 2;
159 // Exact up->down round-trip group delay at the base rate. Each 2x half-band
160 // stage contributes (halfOrder+1) high-rate samples on the way up and the
161 // same on the way down; cascaded over numStages_ and referred to the base
162 // rate this telescopes to 2*(halfOrder+1)*(factor-1)/factor. Verified
163 // sample-exact against an impulse round-trip for factors 2/4/8/16 and all
164 // quality presets. (halfOrder+1 and factor are powers of two -> exact.)
165 return 2 * (halfOrder + 1) * (factor_ - 1) / factor_;
166 }
167
179 {
180 const int nCh = std::min(input.getNumChannels(), upBuffer_.getNumChannels());
181 // Release-safe clamp: a block larger than prepare()'s maxBlockSize would
182 // overflow the per-stage histories and the internal high-rate buffer.
183 const int nS = std::min(input.getNumSamples(), baseSpec_.maxBlockSize);
184
185 if (numStages_ == 0)
186 {
187 for (int ch = 0; ch < nCh; ++ch)
188 std::memcpy(upBuffer_.getChannel(ch), input.getChannel(ch), static_cast<std::size_t>(nS) * sizeof(T));
189 return upBuffer_.toView().getSubView(0, nS);
190 }
191
192 // Stage 0: Read directly from input view
193 filters_[0].processUpsample(input, upBuffer_.toView(), nCh, nS);
194
195 // Subsequent stages: in-place upsampling inside upBuffer_
196 int currentLen = nS * 2;
197 for (int stage = 1; stage < numStages_; ++stage)
198 {
199 filters_[stage].processUpsampleInPlace(upBuffer_.toView(), nCh, currentLen);
200 currentLen *= 2;
201 }
202
203 return upBuffer_.toView().getSubView(0, nS * factor_);
204 }
205
216 void downsample(AudioBufferView<T> output) noexcept
217 {
218 const int nCh = std::min(output.getNumChannels(), upBuffer_.getNumChannels());
219 const int nS = std::min(output.getNumSamples(), baseSpec_.maxBlockSize);
220
221 if (numStages_ == 0)
222 {
223 for (int ch = 0; ch < nCh; ++ch)
224 std::memcpy(output.getChannel(ch), upBuffer_.getChannel(ch), static_cast<std::size_t>(nS) * sizeof(T));
225 return;
226 }
227
228 int currentLen = nS * factor_;
229
230 for (int stage = numStages_ - 1; stage > 0; --stage)
231 {
232 filters_[stage].processDownsampleInPlace(upBuffer_.toView(), nCh, currentLen);
233 currentLen /= 2;
234 }
235
236 // Stage 0: Write directly to final output
237 filters_[0].processDownsample(upBuffer_.toView(), output, nCh, currentLen);
238
239 // Level transparency note: no gain compensation is applied (or needed).
240 // An impulse round-trip peaks slightly below 1.0 only because the
241 // impulse contains supra-Nyquist energy the anti-alias filters must
242 // remove; in-band sine-wave gain is already ~1.000 (verified to better
243 // than 0.01 dB across all quality presets).
244 }
245
246private:
247 static constexpr int kMaxStages = 4;
248
249 // ========================================================================
250 // Polyphase Block Half-Band FIR
251 // ========================================================================
256 struct PolyphaseHalfBand
257 {
258 std::vector<T> evenTaps;
259 T centerTap = T(0);
260 int halfOrder = 0;
261 int delaySamples = 0;
262
267 std::vector<T> history;
268 std::vector<T> evenHist;
269 std::vector<T> oddHist;
270 };
271 std::vector<ChannelState> upChannels;
272 std::vector<ChannelState> downChannels;
273 std::vector<T> scratch;
274
282 void design(int taps, T beta, int numChannels, int maxBlockSamples)
283 {
284 halfOrder = (taps - 1) / 2;
285 // INVARIANT: the polyphase split below collects the array-even taps,
286 // which coincide with the non-zero (odd-from-centre) half-band taps
287 // ONLY when halfOrder is odd. All quality presets (taps 31/63/127/255
288 // -> halfOrder 15/31/63/127) satisfy this. If you add a preset, keep
289 // (taps-1)/2 odd or the filter degenerates to silence.
290 assert((halfOrder & 1) == 1 && "half-band polyphase requires odd halfOrder");
291 // Pure-delay alignment for the centre-tap (odd) phase. With the full
292 // even branch (halfOrder+1 taps, group delay (halfOrder)/2 = numTaps/2
293 // base samples) the centre sample must sit numTaps/2 = (halfOrder+1)/2
294 // samples into the window to stay phase-aligned with the even phase.
295 delaySamples = (halfOrder + 1) / 2;
296
297 auto fullCoeffs = FIRDesign<T>::lowPass(1.0, 0.25, taps, beta);
298 centerTap = fullCoeffs[static_cast<std::size_t>(halfOrder)];
299
300 // The non-zero taps: all even array indices (the centre sits at the
301 // odd index halfOrder, and the other odd indices are the half-band
302 // zeros).
303 evenTaps.clear();
304 for (int i = 0; i < taps; i += 2)
305 evenTaps.push_back(fullCoeffs[static_cast<std::size_t>(i)]);
306
307 // Up history: base-rate (numTaps + block). Down histories: halfOrder
308 // samples of prefix plus up to maxBlockSamples new samples per phase
309 // (a stage's decimator input is 2 * maxBlockSamples long).
310 const int upHistorySize = static_cast<int>(evenTaps.size()) + maxBlockSamples;
311 const int downHistorySize = halfOrder + maxBlockSamples;
312
313 upChannels.resize(static_cast<std::size_t>(numChannels));
314 downChannels.resize(static_cast<std::size_t>(numChannels));
315
316 for (int ch = 0; ch < numChannels; ++ch)
317 {
318 upChannels[ch].history.assign(static_cast<std::size_t>(upHistorySize), T(0));
319 downChannels[ch].evenHist.assign(static_cast<std::size_t>(downHistorySize), T(0));
320 downChannels[ch].oddHist.assign(static_cast<std::size_t>(downHistorySize), T(0));
321 }
322 scratch.assign(static_cast<std::size_t>(maxBlockSamples), T(0));
323 }
324
328 void reset() noexcept
329 {
330 for (auto& ch : upChannels)
331 std::fill(ch.history.begin(), ch.history.end(), T(0));
332 for (auto& ch : downChannels)
333 {
334 std::fill(ch.evenHist.begin(), ch.evenHist.end(), T(0));
335 std::fill(ch.oddHist.begin(), ch.oddHist.end(), T(0));
336 }
337 }
338
344 void upsampleChannel(const T* src, T* dst, ChannelState& state, int n) noexcept
345 {
346 constexpr int W = simd::kVecWidth<T>;
347 using O = simd::Vec<T, W>;
348 const int numTaps = static_cast<int>(evenTaps.size());
349 auto& hist = state.history;
350
351 // Append the new block after the fixed numTaps-sample history prefix.
352 std::memmove(hist.data() + numTaps, src, static_cast<std::size_t>(n) * sizeof(T));
353
354 simd::firCorrelate(hist.data(), evenTaps.data(), numTaps, scratch.data(), n, T(2));
355
356 const T* centre = hist.data() + numTaps - delaySamples;
357 const T centre2 = centerTap * T(2);
358 const auto c2 = O::set1(centre2);
359 int i = 0;
360 for (; i + W <= n; i += W)
361 O::storeInterleave2(dst + 2 * i, O::load(scratch.data() + i),
362 O::mul(O::load(centre + i), c2));
363 for (; i < n; ++i)
364 {
365 dst[2 * i] = scratch[static_cast<std::size_t>(i)];
366 dst[2 * i + 1] = centre[i] * centre2;
367 }
368
369 // Save the trailing numTaps input samples for the next block. Using
370 // the current n keeps this correct under variable block sizes.
371 std::memmove(hist.data(), hist.data() + n, static_cast<std::size_t>(numTaps) * sizeof(T));
372 }
373
380 void downsampleChannel(const T* src, T* dst, ChannelState& state, int len) noexcept
381 {
382 constexpr int W = simd::kVecWidth<T>;
383 using O = simd::Vec<T, W>;
384 const int outLen = len / 2;
385 const int numTaps = static_cast<int>(evenTaps.size());
386 T* even = state.evenHist.data();
387 T* odd = state.oddHist.data();
388
389 // Split the new samples after the halfOrder-sample prefixes.
390 int m = 0;
391 for (; m + W <= outLen; m += W)
392 {
393 typename O::V e, o;
394 O::loadDeinterleave(src + 2 * m, e, o);
395 O::store(even + halfOrder + m, e);
396 O::store(odd + halfOrder + m, o);
397 }
398 for (; m < outLen; ++m)
399 {
400 even[halfOrder + m] = src[2 * m];
401 odd[halfOrder + m] = src[2 * m + 1];
402 }
403
404 simd::firCorrelate(even, evenTaps.data(), numTaps, dst, outLen, T(1));
405
406 // The centre tap meets x[2k + halfOrder] = odd[k + (halfOrder - 1) / 2].
407 const T* centre = odd + (halfOrder - 1) / 2;
408 const auto c = O::set1(centerTap);
409 int k = 0;
410 for (; k + W <= outLen; k += W)
411 O::store(dst + k, O::madd(c, O::load(centre + k), O::load(dst + k)));
412 for (; k < outLen; ++k)
413 {
414 // Keep the vector multiply-add rounding at block tails too.
415 T lanes[W];
416 O::store(lanes, O::madd(c, O::set1(centre[k]), O::set1(dst[k])));
417 dst[k] = lanes[0];
418 }
419
420 std::memmove(even, even + outLen, static_cast<std::size_t>(halfOrder) * sizeof(T));
421 std::memmove(odd, odd + outLen, static_cast<std::size_t>(halfOrder) * sizeof(T));
422 }
423
424 void processUpsample(AudioBufferView<const T> input, AudioBufferView<T> output, int nCh, int nS) noexcept
425 {
426 for (int ch = 0; ch < nCh; ++ch)
427 upsampleChannel(input.getChannel(ch), output.getChannel(ch),
428 upChannels[static_cast<std::size_t>(ch)], nS);
429 }
430
431 void processUpsampleInPlace(AudioBufferView<T> buffer, int nCh, int currentLen) noexcept
432 {
433 for (int ch = 0; ch < nCh; ++ch)
434 upsampleChannel(buffer.getChannel(ch), buffer.getChannel(ch),
435 upChannels[static_cast<std::size_t>(ch)], currentLen);
436 }
437
438 void processDownsample(AudioBufferView<const T> input, AudioBufferView<T> output, int nCh, int currentLen) noexcept
439 {
440 for (int ch = 0; ch < nCh; ++ch)
441 downsampleChannel(input.getChannel(ch), output.getChannel(ch),
442 downChannels[static_cast<std::size_t>(ch)], currentLen);
443 }
444
445 void processDownsampleInPlace(AudioBufferView<T> buffer, int nCh, int currentLen) noexcept
446 {
447 for (int ch = 0; ch < nCh; ++ch)
448 downsampleChannel(buffer.getChannel(ch), buffer.getChannel(ch),
449 downChannels[static_cast<std::size_t>(ch)], currentLen);
450 }
451 };
452
453 static constexpr int tapsForQuality(Quality q) noexcept
454 {
455 switch (q)
456 {
457 case Quality::Low: return 31;
458 case Quality::Medium: return 63;
459 case Quality::High: return 127;
460 case Quality::Maximum: return 255;
461 }
462 return 127;
463 }
464
465 static constexpr T betaForQuality(Quality q) noexcept
466 {
467 switch (q)
468 {
469 case Quality::Low: return T(3.395);
470 case Quality::Medium: return T(5.653);
471 case Quality::High: return T(7.857);
472 case Quality::Maximum: return T(10.056);
473 }
474 return T(7.857);
475 }
476
477 int factor_;
478 int numStages_;
479 Quality quality_;
480 AudioSpec baseSpec_ {};
481 AudioBuffer<T, MaxChannels> upBuffer_;
482
483 std::array<PolyphaseHalfBand, kMaxStages> filters_;
484};
485
486} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
AudioBufferView< T, MaxChannels > toView() noexcept
Returns a non-owning mutable view of this buffer. The view's channel capacity is propagated from MaxC...
int getNumChannels() const noexcept
Returns the number of active channels.
T * getChannel(int ch) noexcept
Returns a pointer to the sample data.
void resize(int numChannels, int numSamples)
Allocates the buffer for the given dimensions.
void clear() noexcept
Clears all channels, including SIMD alignment padding. Ensures any SIMD over-read will consume pure z...
static std::vector< T > lowPass(double sampleRate, double cutoffHz, int numTaps, T beta=T(5))
Designs a low-pass FIR filter.
Definition FIRFilter.h:89
Power-of-two oversampling processor with polyphase anti-aliasing.
void downsample(AudioBufferView< T > output) noexcept
Downsamples the internal high-rate buffer back to base-rate.
void reset() noexcept
Flushes all internal delay lines and history buffers. Call this when resetting the transport or to cl...
int getFactor() const noexcept
Returns the current oversampling factor.
Quality getQuality() const noexcept
Returns the current quality preset.
AudioBufferView< T > upsample(AudioBufferView< const T > input) noexcept
Upsamples the input base-rate signal into the internal high-rate buffer.
int getLatency() const noexcept
Calculates the exact group delay latency of the oversampling chain.
Quality
Quality presets defining the steepness and rejection of the anti-aliasing filter.
@ Low
31 taps/stage, ~-40 dB stopband. For fast previewing.
@ High
127 taps/stage, ~-80 dB stopband. Professional standard.
@ Maximum
255 taps/stage, ~-100 dB stopband. Mastering grade.
@ Medium
63 taps/stage, ~-60 dB stopband. General purpose.
Oversampling(int factor=2, Quality quality=Quality::High)
Constructs the oversampling engine.
void prepare(const AudioSpec &spec)
Pre-allocates buffers and computes FIR filter coefficients.
void firCorrelate(const T *x, const T *h, int taps, T *DSPARK_RESTRICT y, int n, T gain) noexcept
Block FIR as a valid correlation: y[i] = gain * sum_j h[j] * x[i + j] for i in [0,...
Definition SimdOps.h:1549
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:53
std::vector< T > oddHist
Decimator: odd high-rate samples with prefix.
std::vector< T > evenHist
Decimator: even high-rate samples with prefix.
std::vector< T > history
Upsampler: base-rate input with its FIR prefix.