DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Resampler.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
44#include "AudioBuffer.h"
45#include "AudioSpec.h"
46#include "SimdOps.h"
47
48#include <algorithm>
49#include <cassert>
50#include <cmath>
51#include <cstddef>
52#include <cstdint>
53#include <limits>
54#include <numbers>
55#include <type_traits>
56#include <utility>
57#include <vector>
58
59namespace dspark {
60
84template <typename T>
86{
87public:
88 enum class Quality
89 {
90 Draft,
91 Normal,
92 High,
93 Ultra
94 };
95
97 static constexpr int kMaxTaps = 8192;
98
108 void prepare(double sourceRate, double targetRate,
109 Quality quality = Quality::Normal)
110 {
111 prepareTransactional(sourceRate, targetRate, quality,
112 channelStates_.size());
113 }
114
124 void prepare(const AudioSpec& spec, double targetRate,
125 Quality quality = Quality::Normal)
126 {
127 const size_t requestedChannels = spec.numChannels > 0
128 ? static_cast<size_t>(spec.numChannels)
129 : size_t(0);
130 prepareTransactional(spec.sampleRate, targetRate, quality,
131 std::max(channelStates_.size(), requestedChannels));
132 }
133
140 void reset() noexcept
141 {
142 resetChannelState(mono_);
143 for (auto& cs : channelStates_)
144 resetChannelState(cs);
145 }
146
158 [[nodiscard]] std::vector<T> process(const T* input, int inputLength)
159 {
160 if (table_.empty()) return {}; // not prepared
161
162 // Clamp through double before the int cast: huge ratios could
163 // otherwise overflow the cast itself (undefined behaviour).
164 const double outLenD =
165 std::ceil(static_cast<double>(inputLength) * ratio_);
166 const int outputLength = static_cast<int>(
167 std::min(outLenD, static_cast<double>(std::numeric_limits<int>::max())));
168
169 std::vector<T> output(static_cast<size_t>(outputLength));
170 for (int outIdx = 0; outIdx < outputLength; ++outIdx)
171 {
172 int64_t intPos;
173 int64_t phase = 0;
174 double frac = 0.0;
175 if (exact_)
176 {
177 // Integer position arithmetic: output k reads input k*M/L.
178 const int64_t num = static_cast<int64_t>(outIdx) * stepM_;
179 intPos = num / phasesL_;
180 phase = num % phasesL_;
181 }
182 else
183 {
184 const double srcPos = static_cast<double>(outIdx) / ratio_;
185 intPos = static_cast<int64_t>(srcPos);
186 frac = srcPos - static_cast<double>(intPos);
187 }
188 if (intPos >= inputLength) break;
189 output[static_cast<size_t>(outIdx)] =
190 interpolateOffline(input, inputLength, static_cast<int>(intPos), phase, frac);
191 }
192 return output;
193 }
194
208 [[nodiscard]] std::vector<T> processRange(const T* input, int inputLength,
209 int64_t firstOutput, int64_t count)
210 {
211 if (table_.empty() || input == nullptr || inputLength <= 0 || count <= 0) return {};
212 std::vector<T> output(static_cast<size_t>(count));
213 for (int64_t i = 0; i < count; ++i)
214 {
215 const int64_t outIdx = firstOutput + i;
216 int64_t intPos;
217 int64_t phase = 0;
218 double frac = 0.0;
219 if (exact_)
220 {
221 // Floor division: negative positions are ordinary here.
222 const int64_t num = outIdx * stepM_;
223 intPos = num / phasesL_;
224 if (num % phasesL_ != 0 && num < 0) --intPos;
225 phase = num - intPos * phasesL_;
226 }
227 else
228 {
229 const double srcPos = static_cast<double>(outIdx) / ratio_;
230 const double fl = std::floor(srcPos);
231 intPos = static_cast<int64_t>(fl);
232 frac = srcPos - fl;
233 }
234 // A window that misses the input entirely is exactly zero.
235 const int64_t firstTap = intPos - halfTaps_ + 1;
236 if (firstTap + taps_ <= 0 || firstTap >= inputLength) continue;
237 output[static_cast<size_t>(i)] =
238 interpolateOffline(input, inputLength, static_cast<int>(intPos), phase, frac);
239 }
240 return output;
241 }
242
249 [[nodiscard]] int64_t getReach() const noexcept
250 {
251 return static_cast<int64_t>(std::ceil((taps_ - halfTaps_ + 1) * ratio_)) + 1;
252 }
253
262 int processBlock(const T* input, int inputLength, T* output) noexcept
263 {
264 return processChannel(input, inputLength, output, mono_);
265 }
266
277 {
278 int numCh = std::min(input.getNumChannels(), output.getNumChannels());
279 int inLen = input.getNumSamples();
280
281 assert(static_cast<int>(channelStates_.size()) >= numCh &&
282 "Resampler channels not allocated! Call prepare(spec...) first.");
283 // Release-safe: never index channelStates_ past what prepare() allocated
284 // (the assert above flags the missing prepare(spec) in debug builds).
285 numCh = std::min(numCh, static_cast<int>(channelStates_.size()));
286
287 int outCount = 0;
288 for (int ch = 0; ch < numCh; ++ch)
289 {
290 outCount = processChannel(input.getChannel(ch), inLen,
291 output.getChannel(ch),
292 channelStates_[static_cast<size_t>(ch)]);
293 }
294
295 return outCount;
296 }
297
303 [[nodiscard]] int getMaxOutputSamples(int inputLength) const noexcept
304 {
305 const double maxOut =
306 std::ceil(static_cast<double>(inputLength) * ratio_) + 2.0;
307 return static_cast<int>(
308 std::min(maxOut, static_cast<double>(std::numeric_limits<int>::max())));
309 }
310
312 [[nodiscard]] double getRatio() const noexcept { return ratio_; }
313
315 [[nodiscard]] int getFilterLength() const noexcept { return taps_; }
316
325 [[nodiscard]] int getLatency() const noexcept
326 {
327 return static_cast<int>(std::round(static_cast<double>(halfTaps_) * ratio_));
328 }
329
330private:
332 static constexpr int kTablePhases = 512;
335 static constexpr int64_t kMaxExactPhases = 4096;
336 static constexpr int64_t kMaxExactCoefficients = int64_t(1) << 21;
337
338 struct ChannelState
339 {
340 std::vector<T> history;
341 int writePos = 0;
342 double fractionalPos = 0.0;
343 int64_t phase = 0;
344 };
345
346 struct Spec { double passband; double attenuationDb; };
347
348 static_assert(std::is_nothrow_copy_assignable_v<T>,
349 "Resampler sample storage must be reset without throwing");
350 static_assert(std::is_nothrow_swappable_v<ChannelState>,
351 "Resampler state commit must be no-throw");
352 static_assert(std::is_nothrow_swappable_v<std::vector<T>>,
353 "Resampler table commit must be no-throw");
354 static_assert(std::is_nothrow_swappable_v<std::vector<ChannelState>>,
355 "Resampler channel commit must be no-throw");
356
357 static Spec qualitySpec(Quality q) noexcept
358 {
359 switch (q)
360 {
361 case Quality::Draft: return { 0.80, 60.0 };
362 case Quality::Normal: return { 0.90, 100.0 };
363 case Quality::High: return { 0.91, 140.0 };
364 case Quality::Ultra: return { 0.915, 210.0 };
365 }
366 return { 0.90, 100.0 };
367 }
368
370 [[nodiscard]] static double kaiserBeta(double a) noexcept
371 {
372 if (a > 50.0) return 0.1102 * (a - 8.7);
373 if (a >= 21.0) return 0.5842 * std::pow(a - 21.0, 0.4) + 0.07886 * (a - 21.0);
374 return 0.0;
375 }
376
378 [[nodiscard]] static double besselI0(double x) noexcept
379 {
380 double sum = 1.0, term = 1.0;
381 for (int k = 1; k <= 200; ++k)
382 {
383 const double half = x / (2.0 * k);
384 term *= half * half;
385 sum += term;
386 if (term < 1e-17 * sum) break;
387 }
388 return sum;
389 }
390
392 struct Design
393 {
394 int taps = 2;
395 double cutoff = 1.0;
396 double beta = 0.0;
397 bool identity = false;
398 };
399
400 [[nodiscard]] static Design design(double ratio, Quality quality) noexcept
401 {
402 Design d;
403 if (ratio == 1.0)
404 {
405 d.identity = true;
406 return d;
407 }
408 const Spec spec = qualitySpec(quality);
409 const double scale = std::min(1.0, ratio); // lower Nyquist / input Nyquist
410 // Band edges in cycles per input sample: the passband ends at
411 // passband * lower Nyquist, the stopband begins at the lower Nyquist.
412 const double transition = 0.5 * scale * (1.0 - spec.passband);
413 d.cutoff = scale * 0.5 * (1.0 + spec.passband);
414 d.beta = kaiserBeta(spec.attenuationDb);
415 // Kaiser's length estimate, N = (A - 7.95) / (14.36 * df).
416 const double n = (spec.attenuationDb - 7.95) / (14.36 * transition);
417 const double capped = std::min(n, static_cast<double>(kMaxTaps));
418 d.taps = 2 * static_cast<int>(std::ceil(0.5 * capped));
419 d.taps = std::max(d.taps, 4);
420 return d;
421 }
422
432 static void buildPhase(T* dst, const Design& d, double frac)
433 {
434 const int half = d.taps / 2;
435 if (d.identity)
436 {
437 for (int j = 0; j < d.taps; ++j)
438 dst[j] = (j == half - 1) ? T(1) : T(0);
439 return;
440 }
441 constexpr double kPi = std::numbers::pi;
442 const double i0Beta = besselI0(d.beta);
443 double sum = 0.0;
444 // Accumulate in double; the table stores T.
445 for (int j = 0; j < d.taps; ++j)
446 {
447 const double t = static_cast<double>(j - half + 1) - frac;
448 const double x = t * d.cutoff;
449 const double sincVal = (std::abs(x) < 1e-12)
450 ? d.cutoff
451 : d.cutoff * std::sin(kPi * x) / (kPi * x);
452 const double wx = t / static_cast<double>(half);
453 const double win = (std::abs(wx) >= 1.0)
454 ? 0.0
455 : besselI0(d.beta * std::sqrt(1.0 - wx * wx)) / i0Beta;
456 dst[j] = static_cast<T>(sincVal * win);
457 sum += sincVal * win;
458 }
459 if (std::abs(sum) > 1e-12)
460 {
461 const double inv = 1.0 / sum;
462 for (int j = 0; j < d.taps; ++j)
463 dst[j] = static_cast<T>(static_cast<double>(dst[j]) * inv);
464 }
465 }
466
471 [[nodiscard]] static std::pair<int64_t, int64_t> rationalRatio(
472 double sourceRate, double targetRate) noexcept
473 {
474 // Integer rates (every standard one) reduce exactly.
475 const double rs = std::round(sourceRate), rt = std::round(targetRate);
476 if (std::abs(rs - sourceRate) < 1e-9 && std::abs(rt - targetRate) < 1e-9
477 && rs < 1e12 && rt < 1e12)
478 {
479 int64_t a = static_cast<int64_t>(rt), b = static_cast<int64_t>(rs);
480 int64_t x = a, y = b;
481 while (y != 0) { const int64_t r = x % y; x = y; y = r; }
482 if (x > 0) return { a / x, b / x };
483 }
484 // Otherwise a continued-fraction approximation that is exact to
485 // double precision.
486 const double r = targetRate / sourceRate;
487 int64_t h0 = 0, h1 = 1, k0 = 1, k1 = 0;
488 double v = r;
489 for (int it = 0; it < 64; ++it)
490 {
491 const double fl = std::floor(v);
492 if (fl > 1e12) break;
493 const auto an = static_cast<int64_t>(fl);
494 const int64_t h2 = an * h1 + h0, k2 = an * k1 + k0;
495 if (k2 > (int64_t(1) << 32) || h2 > (int64_t(1) << 32)) break;
496 h0 = h1; h1 = h2; k0 = k1; k1 = k2;
497 if (std::abs(static_cast<double>(h1) / static_cast<double>(k1) - r) <= 1e-15 * r)
498 return { h1, k1 };
499 const double rem = v - fl;
500 if (rem < 1e-15) break;
501 v = 1.0 / rem;
502 }
503 return { 0, 0 };
504 }
505
506 static void initialiseChannelState(ChannelState& state, int taps)
507 {
508 state.history.assign(static_cast<size_t>(taps * 2), T(0));
509 state.writePos = 0;
510 state.fractionalPos = 0.0;
511 state.phase = 0;
512 }
513
514 void prepareTransactional(double sourceRate, double targetRate,
515 Quality quality, size_t channelCount)
516 {
517 // Derive the complete candidate configuration without touching the
518 // live object. The std::max behavior intentionally preserves the
519 // established treatment of non-positive rates.
520 const double stagedSourceRate = std::max(sourceRate, 1.0);
521 const double stagedTargetRate = std::max(targetRate, 1.0);
522 const double stagedRatio = stagedTargetRate / stagedSourceRate;
523 const Design d = design(stagedRatio, quality);
524
525 auto [l, m] = rationalRatio(stagedSourceRate, stagedTargetRate);
526 const bool exact = d.identity
527 || (l > 0 && l <= kMaxExactPhases
528 && l * static_cast<int64_t>(d.taps) <= kMaxExactCoefficients);
529 if (d.identity) { l = 1; m = 1; }
530
531 // Every potentially throwing allocation belongs to local state. A
532 // failure therefore destroys only the candidate and leaves the live
533 // conversion, histories and streaming positions untouched.
534 std::vector<T> stagedTable;
535 if (exact)
536 {
537 // Phase p is the kernel for fractional position p / L.
538 stagedTable.resize(static_cast<size_t>(l * d.taps));
539 for (int64_t p = 0; p < l; ++p)
540 buildPhase(stagedTable.data() + p * d.taps, d,
541 static_cast<double>(p) / static_cast<double>(l));
542 }
543 else
544 {
545 // kTablePhases + 3 phases: one guard phase before position 0 and
546 // two after the last, so cubic interpolation across phases never
547 // reads outside the table.
548 stagedTable.resize(static_cast<size_t>((kTablePhases + 3) * d.taps));
549 for (int p = 0; p < kTablePhases + 3; ++p)
550 buildPhase(stagedTable.data() + static_cast<size_t>(p) * static_cast<size_t>(d.taps), d,
551 static_cast<double>(p - 1) / static_cast<double>(kTablePhases));
552 }
553 ChannelState stagedMono;
554 initialiseChannelState(stagedMono, d.taps);
555 std::vector<ChannelState> stagedChannels(channelCount);
556 for (auto& state : stagedChannels)
557 initialiseChannelState(state, d.taps);
558
559 // Scalar assignment and the mechanically checked swaps below cannot
560 // throw. Once commit starts, observers can only see the complete new
561 // state (prepare itself remains a setup-thread operation).
562 sourceRate_ = stagedSourceRate;
563 targetRate_ = stagedTargetRate;
564 ratio_ = stagedRatio;
565 srcStep_ = 1.0 / stagedRatio;
566 taps_ = d.taps;
567 halfTaps_ = d.taps / 2;
568 exact_ = exact;
569 phasesL_ = exact ? l : 0;
570 stepM_ = exact ? m : 0;
571 table_.swap(stagedTable);
572 using std::swap;
573 swap(mono_, stagedMono);
574 channelStates_.swap(stagedChannels);
575 }
576
578 T interpolateContiguous(const T* src, int64_t phase, double frac) const noexcept
579 {
580 if (exact_)
581 return simd::dotProduct(table_.data() + phase * taps_, src, taps_);
582
583 // Cubic (Lagrange) interpolation across the four phases around the
584 // position: error ~ (pi / (2 * kTablePhases))^4, below every stopband.
585 const double exactPhase = frac * static_cast<double>(kTablePhases);
586 int p0 = static_cast<int>(exactPhase);
587 if (p0 > kTablePhases - 1) p0 = kTablePhases - 1; // frac is < 1 by contract
588 const double u = exactPhase - static_cast<double>(p0);
589 const T* k = table_.data() + static_cast<size_t>(p0) * static_cast<size_t>(taps_);
590 // Table phase index p0 + 1 holds position p0 / P (one guard phase first).
591 const T sm = simd::dotProduct(k, src, taps_);
592 const T s0 = simd::dotProduct(k + taps_, src, taps_);
593 const T s1 = simd::dotProduct(k + 2 * taps_, src, taps_);
594 const T s2 = simd::dotProduct(k + 3 * taps_, src, taps_);
595 const double wm = -u * (u - 1.0) * (u - 2.0) / 6.0;
596 const double w0 = (u + 1.0) * (u - 1.0) * (u - 2.0) / 2.0;
597 const double w1 = -(u + 1.0) * u * (u - 2.0) / 2.0;
598 const double w2 = (u + 1.0) * u * (u - 1.0) / 6.0;
599 return static_cast<T>(wm * sm + w0 * s0 + w1 * s1 + w2 * s2);
600 }
601
603 T interpolateOffline(const T* data, int length, int intPos,
604 int64_t phase, double frac)
605 {
606 // Tap j weighs data[intPos - halfTaps + 1 + j] (see buildPhase).
607 const int firstSrc = intPos - halfTaps_ + 1;
608 if (firstSrc >= 0 && firstSrc + taps_ <= length)
609 return interpolateContiguous(data + firstSrc, phase, frac);
610
611 // Edge path: gather into a zero-padded window. The kernel is read
612 // exactly as in the fast path, so both agree bit for bit.
613 std::vector<T>& w = edgeScratch_;
614 w.assign(static_cast<size_t>(taps_), T(0));
615 for (int j = 0; j < taps_; ++j)
616 {
617 const int srcIdx = firstSrc + j;
618 if (srcIdx >= 0 && srcIdx < length)
619 w[static_cast<size_t>(j)] = data[srcIdx];
620 }
621 return interpolateContiguous(w.data(), phase, frac);
622 }
623
624 static void resetChannelState(ChannelState& cs) noexcept
625 {
626 std::fill(cs.history.begin(), cs.history.end(), T(0));
627 cs.writePos = 0;
628 cs.fractionalPos = 0.0;
629 cs.phase = 0;
630 }
631
632 int processChannel(const T* input, int inputLength, T* output,
633 ChannelState& state) noexcept
634 {
635 if (state.history.empty()) return 0; // not prepared
636
637 // Hot state lives in locals for the duration of the block: this both
638 // tells the compiler the fields cannot alias the output writes (no
639 // per-sample reloads) and keeps them in registers. The window
640 // [writePos, writePos + n) holds the last n inputs, oldest first; the
641 // kernel peaks at tap halfTaps-1+frac, so the output stream is
642 // delayed by exactly halfTaps input samples (see getLatency()).
643 T* const hist = state.history.data();
644 const int n = taps_;
645 int writePos = state.writePos;
646 int outIdx = 0;
647
648 if (exact_)
649 {
650 const int64_t l = phasesL_, m = stepM_;
651 int64_t phase = state.phase;
652 for (int i = 0; i < inputLength; ++i)
653 {
654 // Double-buffered push: the mirror write keeps the read
655 // window contiguous (no modulo).
656 const T x = input[i];
657 hist[writePos] = x;
658 hist[writePos + n] = x;
659 if (++writePos >= n)
660 writePos = 0;
661
662 while (phase < l)
663 {
664 output[outIdx++] = interpolateContiguous(hist + writePos, phase, 0.0);
665 phase += m;
666 }
667 phase -= l;
668 }
669 state.phase = phase;
670 }
671 else
672 {
673 const double step = srcStep_;
674 double frac = state.fractionalPos;
675 for (int i = 0; i < inputLength; ++i)
676 {
677 const T x = input[i];
678 hist[writePos] = x;
679 hist[writePos + n] = x;
680 if (++writePos >= n)
681 writePos = 0;
682
683 while (frac < 1.0)
684 {
685 output[outIdx++] = interpolateContiguous(hist + writePos, 0, frac);
686 frac += step;
687 }
688 frac -= 1.0;
689 }
690 state.fractionalPos = frac;
691 }
692
693 state.writePos = writePos;
694 return outIdx;
695 }
696
697 double sourceRate_ = 44100.0;
698 double targetRate_ = 48000.0;
699 double ratio_ = 1.0;
700 double srcStep_ = 1.0;
701
702 int taps_ = 2;
703 int halfTaps_ = 1;
704 bool exact_ = true;
705 int64_t phasesL_ = 0;
706 int64_t stepM_ = 0;
707
708 std::vector<T> table_;
709 std::vector<T> edgeScratch_;
710 ChannelState mono_;
711 std::vector<ChannelState> channelStates_;
712};
713
714} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Windowed-sinc sample rate converter optimized for real-time DSP.
Definition Resampler.h:86
int processBlock(AudioBufferView< T > input, AudioBufferView< T > output) noexcept
Resamples multi-channel audio using AudioBufferView (streaming).
Definition Resampler.h:276
@ High
Passband 0.91 of Nyquist (20 kHz at 44.1), 140 dB stopband.
@ Ultra
Passband 0.915 of Nyquist, 210 dB stopband: mastering.
@ Normal
Passband 0.90 of Nyquist, 100 dB stopband.
@ Draft
Passband 0.80 of Nyquist, 60 dB stopband: previews.
int64_t getReach() const noexcept
How far, in output samples, the kernel reaches before an input sample and after it: processRange() ov...
Definition Resampler.h:249
int getLatency() const noexcept
Returns the streaming latency in output samples.
Definition Resampler.h:325
static constexpr int kMaxTaps
Longest kernel, in input samples (see the class note).
Definition Resampler.h:97
double getRatio() const noexcept
Returns the conversion ratio (targetRate / sourceRate).
Definition Resampler.h:312
int getFilterLength() const noexcept
Kernel length in input samples (1 at a ratio of exactly 1).
Definition Resampler.h:315
std::vector< T > processRange(const T *input, int inputLength, int64_t firstOutput, int64_t count)
Offline, time-aligned conversion of any span of output samples.
Definition Resampler.h:208
void reset() noexcept
Resets the internal state (delay lines, all channels) to zero.
Definition Resampler.h:140
std::vector< T > process(const T *input, int inputLength)
Resamples an entire buffer (offline batch processing).
Definition Resampler.h:158
int processBlock(const T *input, int inputLength, T *output) noexcept
Resamples a block of audio in single-channel streaming mode.
Definition Resampler.h:262
void prepare(const AudioSpec &spec, double targetRate, Quality quality=Quality::Normal)
Prepares the resampler using AudioSpec (unified API).
Definition Resampler.h:124
void prepare(double sourceRate, double targetRate, Quality quality=Quality::Normal)
Prepares the resampler for a given rate conversion.
Definition Resampler.h:108
int getMaxOutputSamples(int inputLength) const noexcept
Returns the maximum number of output samples for a given input length.
Definition Resampler.h:303
float dotProduct(const float *DSPARK_RESTRICT a, const float *DSPARK_RESTRICT b, int count) noexcept
Computes the dot product of two arrays.
Definition SimdOps.h:419
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
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45