DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Interpolation.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
51#include "DspMath.h"
52#include "SimdOps.h"
53
54#include <algorithm>
55#include <cassert>
56#include <cmath>
57#include <cstddef>
58#include <cstdint>
59#include <vector>
60
61namespace dspark {
62
71template <FloatType T>
72[[nodiscard]] inline T interpolateLinear(T y0, T y1, T frac) noexcept {
73 return y0 + frac * (y1 - y0);
74}
75
90template <FloatType T>
91[[nodiscard]] inline T interpolateLinear(const T* buffer, int length, T position) noexcept {
92 assert(length > 0 && position >= T(0) && position < static_cast<T>(length));
93 if (length <= 0) return T(0);
94
95 // An out-of-range position (including NaN) would make the int cast below
96 // undefined and the buffer reads out of bounds: clamp before casting.
97 int idx0;
98 T frac;
99 if (!(position >= T(0))) { idx0 = 0; frac = T(0); }
100 else if (position >= static_cast<T>(length)) { idx0 = length - 1; frac = T(0); }
101 else {
102 idx0 = static_cast<int>(position);
103 frac = position - static_cast<T>(idx0);
104 }
105
106 int idx1 = idx0 + 1;
107 if (idx1 >= length) idx1 = 0;
108
109 return interpolateLinear(buffer[idx0], buffer[idx1], frac);
110}
111
125template <FloatType T>
126[[nodiscard]] inline T interpolateHermite(T y0, T y1, T y2, T y3, T frac) noexcept {
127 T c = (y2 - y0) * T(0.5);
128 T v = y1 - y2;
129 T w = c + v;
130 T a = w + v + (y3 - y1) * T(0.5);
131 T b = w + a;
132 return (((a * frac) - b) * frac + c) * frac + y1;
133}
134
150template <FloatType T>
151[[nodiscard]] inline T interpolateHermite(const T* buffer, int length, T position) noexcept {
152 assert(length > 0 && position >= T(0) && position < static_cast<T>(length));
153 if (length <= 0) return T(0);
154
155 // Same release-safe clamp as the linear overload (undefined int cast /
156 // out-of-bounds reads otherwise).
157 int idx1;
158 T frac;
159 if (!(position >= T(0))) { idx1 = 0; frac = T(0); }
160 else if (position >= static_cast<T>(length)) { idx1 = length - 1; frac = T(0); }
161 else {
162 idx1 = static_cast<int>(position);
163 frac = position - static_cast<T>(idx1);
164 }
165
166 int idx0 = (idx1 > 0) ? idx1 - 1 : length - 1;
167 int idx2 = idx1 + 1; if (idx2 >= length) idx2 -= length;
168 int idx3 = idx2 + 1; if (idx3 >= length) idx3 -= length;
169
170 return interpolateHermite(buffer[idx0], buffer[idx1], buffer[idx2], buffer[idx3], frac);
171}
172
177template <FloatType T>
178[[nodiscard]] inline T interpolateCubic(const T* buffer, int length, T position) noexcept {
179 return interpolateHermite(buffer, length, position);
180}
181
195template <FloatType T>
196[[nodiscard]] inline T interpolateLagrange(T y0, T y1, T y2, T y3, T frac) noexcept {
197 T d = frac;
198 T dm1 = d - T(1);
199 T dm2 = d - T(2);
200 T dp1 = d + T(1);
201
202 // Precomputed division multipliers: 1/6 ~ 0.16666667, 1/2 = 0.5
203 static constexpr T inv6 = T(1.0 / 6.0);
204 static constexpr T inv2 = T(0.5);
205
206 T l0 = -(d * dm1 * dm2) * inv6;
207 T l1 = (dp1 * dm1 * dm2) * inv2;
208 T l2 = -(dp1 * d * dm2) * inv2;
209 T l3 = (dp1 * d * dm1) * inv6;
210
211 return l0 * y0 + l1 * y1 + l2 * y2 + l3 * y3;
212}
213
229template <FloatType T>
230[[nodiscard]] inline T interpolateLagrange(const T* buffer, int length, T position) noexcept {
231 assert(length > 0 && position >= T(0) && position < static_cast<T>(length));
232 if (length <= 0) return T(0);
233
234 // Same release-safe clamp as the linear overload (undefined int cast /
235 // out-of-bounds reads otherwise).
236 int idx1;
237 T frac;
238 if (!(position >= T(0))) { idx1 = 0; frac = T(0); }
239 else if (position >= static_cast<T>(length)) { idx1 = length - 1; frac = T(0); }
240 else {
241 idx1 = static_cast<int>(position);
242 frac = position - static_cast<T>(idx1);
243 }
244
245 int idx0 = (idx1 > 0) ? idx1 - 1 : length - 1;
246 int idx2 = idx1 + 1; if (idx2 >= length) idx2 -= length;
247 int idx3 = idx2 + 1; if (idx3 >= length) idx3 -= length;
248
249 return interpolateLagrange(buffer[idx0], buffer[idx1], buffer[idx2], buffer[idx3], frac);
250}
251
278template <FloatType T>
279[[nodiscard]] inline T interpolateAllpass(T currentSample, T previousSample,
280 T frac, T& state) noexcept
281{
282 // frac == 0 would place the pole exactly at z = -1 (a marginally stable
283 // resonator at Nyquist) and frac == -1 would divide by zero: clamp away
284 // from both. The clamp changes the delivered delay only for out-of-range
285 // requests.
286 T safeFrac = (frac < T(0.001)) ? T(0.001) : frac;
287 T coeff = (T(1) - safeFrac) / (T(1) + safeFrac);
288
289 T output = coeff * (currentSample - state) + previousSample;
290
291 // Cut the recursive tail below ~-300 dBFS so a decaying state can never
292 // reach the denormal range (matters on targets without FTZ/DAZ).
293 if (std::abs(output) < T(1e-15)) output = T(0);
294
295 state = output;
296 return output;
297}
298
321template <FloatType T>
323{
324public:
325 static constexpr int kTaps = 32;
326 static constexpr int kBefore = kTaps / 2 - 1;
327 static constexpr int kAfter = kTaps / 2;
328 static constexpr int kPhases = 256;
329
332 : table_(static_cast<size_t>((kPhases + 1) * kTaps))
333 {
334 constexpr double kBeta = 10.0;
335 constexpr double kPi = 3.14159265358979323846;
336 const double invI0Beta = 1.0 / besselI0(kBeta);
337 const double halfSpan = static_cast<double>(kTaps) / 2.0;
338
339 for (int p = 0; p <= kPhases; ++p)
340 {
341 T* row = table_.data() + static_cast<size_t>(p) * kTaps;
342 const double phase = static_cast<double>(p) / kPhases;
343 if (p == 0 || p == kPhases)
344 {
345 // Integer positions: an exact identity (sin(pi * k) rounds
346 // to ~1e-16, not 0, so the sinc is not evaluated here).
347 std::fill(row, row + kTaps, T(0));
348 row[p == 0 ? kBefore : kBefore + 1] = T(1);
349 continue;
350 }
351 double h[kTaps];
352 double sum = 0.0;
353 for (int t = 0; t < kTaps; ++t)
354 {
355 const double x = static_cast<double>(t - kBefore) - phase;
356 const double r = x / halfSpan;
357 const double window = (r * r < 1.0)
358 ? besselI0(kBeta * std::sqrt(1.0 - r * r)) * invI0Beta
359 : 0.0;
360 h[t] = std::sin(kPi * x) / (kPi * x) * window;
361 sum += h[t];
362 }
363 for (int t = 0; t < kTaps; ++t)
364 row[t] = static_cast<T>(h[t] / sum); // unit DC gain
365 }
366 }
367
374 [[nodiscard]] T read(const T* x, double frac) const noexcept
375 {
376 const double p = frac * static_cast<double>(kPhases);
377 const int ip = std::clamp(static_cast<int>(p), 0, kPhases - 1);
378 const T t = static_cast<T>(p - static_cast<double>(ip));
379 const T* h0 = table_.data() + static_cast<size_t>(ip) * kTaps;
380 const T* h1 = h0 + kTaps; // the table holds kPhases + 1 rows
381 const T a = simd::dotProductT(h0, x, kTaps);
382 const T b = simd::dotProductT(h1, x, kTaps);
383 return a + t * (b - a);
384 }
385
394 [[nodiscard]] T readRing(const T* ring, int64_t mask, int64_t intPos, double frac) const noexcept
395 {
396 const int64_t first = intPos - kBefore;
397 const auto start = static_cast<size_t>(first & mask);
398 if (start + static_cast<size_t>(kTaps) <= static_cast<size_t>(mask) + 1)
399 return read(ring + start, frac); // contiguous: no gather
400 T window[kTaps];
401 for (int t = 0; t < kTaps; ++t)
402 window[t] = ring[static_cast<size_t>((first + t) & mask)];
403 return read(window, frac);
404 }
405
406private:
408 [[nodiscard]] static double besselI0(double x) noexcept
409 {
410 double sum = 1.0, term = 1.0;
411 const double halfX = x / 2.0;
412 for (int k = 1; k < 60; ++k)
413 {
414 const double f = halfX / static_cast<double>(k);
415 term *= f * f;
416 sum += term;
417 if (term < sum * 1e-17) break;
418 }
419 return sum;
420 }
421
422 std::vector<T> table_;
423};
424
445template <FloatType T>
447{
448public:
449 static constexpr int kSteps = 16;
450 static constexpr int kStepsPerOctave = 8;
451 static constexpr int kPhases = 64;
452 static constexpr int kMaxTaps = 128;
453
456 {
457 constexpr double kBeta = 10.0;
458 constexpr double kPi = 3.14159265358979323846;
459 const double invI0Beta = 1.0 / besselI0(kBeta);
460
461 size_t total = 0;
462 for (int k = 0; k < kSteps; ++k)
463 {
464 const double rate = std::exp2(static_cast<double>(k + 1) / kStepsPerOctave);
465 half_[k] = static_cast<int>(std::ceil(16.0 * rate));
466 offset_[k] = total;
467 total += static_cast<size_t>(kPhases + 1) * static_cast<size_t>(2 * half_[k]);
468 }
469 table_.assign(total, T(0));
470
471 std::vector<double> h;
472 for (int k = 0; k < kSteps; ++k)
473 {
474 const double rate = std::exp2(static_cast<double>(k + 1) / kStepsPerOctave);
475 const int taps = 2 * half_[k];
476 h.assign(static_cast<size_t>(taps), 0.0);
477 for (int p = 0; p <= kPhases; ++p)
478 {
479 const double phase = static_cast<double>(p) / kPhases;
480 double sum = 0.0;
481 for (int t = 0; t < taps; ++t)
482 {
483 const double u = (static_cast<double>(t - (half_[k] - 1)) - phase) / rate;
484 const double r = u / 16.0;
485 const double window = (r * r < 1.0)
486 ? besselI0(kBeta * std::sqrt(1.0 - r * r)) * invI0Beta
487 : 0.0;
488 const double sinc = (u == 0.0) ? 1.0 : std::sin(kPi * u) / (kPi * u);
489 h[static_cast<size_t>(t)] = sinc * window;
490 sum += h[static_cast<size_t>(t)];
491 }
492 T* row = table_.data() + offset_[k] + static_cast<size_t>(p) * static_cast<size_t>(taps);
493 for (int t = 0; t < taps; ++t)
494 row[t] = static_cast<T>(h[static_cast<size_t>(t)] / sum); // unit DC gain
495 }
496 }
497 }
498
505 [[nodiscard]] static int stepFor(double rate) noexcept
506 {
507 const double steps = std::ceil(std::log2(std::max(rate, 1.0)) * kStepsPerOctave - 1e-9);
508 return std::clamp(static_cast<int>(steps) - 1, 0, kSteps - 1);
509 }
510
522 [[nodiscard]] T readRing(const T* ring, int64_t mask, int64_t intPos, double frac, int step) const noexcept
523 {
524 const int half = half_[step];
525 const int taps = 2 * half;
526 const double p = frac * static_cast<double>(kPhases);
527 const int ip = std::clamp(static_cast<int>(p), 0, kPhases - 1);
528 const T t = static_cast<T>(p - static_cast<double>(ip));
529 const T* h0 = table_.data() + offset_[step] + static_cast<size_t>(ip) * static_cast<size_t>(taps);
530 const T* h1 = h0 + taps;
531
532 const int64_t first = intPos - (half - 1);
533 const auto start = static_cast<size_t>(first & mask);
534 if (start + static_cast<size_t>(taps) > static_cast<size_t>(mask) + 1) [[unlikely]]
535 {
536 // The window straddles the ring's end: gather it (value-initialised,
537 // so no compiler can see an unwritten element reach the dot product).
538 T window[kMaxTaps] {};
539 for (int k = 0; k < taps; ++k)
540 window[k] = ring[static_cast<size_t>((first + k) & mask)];
541 const T a = simd::dotProductT(h0, window, taps);
542 const T b = simd::dotProductT(h1, window, taps);
543 return a + t * (b - a);
544 }
545 const T* x = ring + start;
546 const T a = simd::dotProductT(h0, x, taps);
547 const T b = simd::dotProductT(h1, x, taps);
548 return a + t * (b - a);
549 }
550
552 [[nodiscard]] int reach(int step) const noexcept { return half_[step]; }
553
554private:
556 [[nodiscard]] static double besselI0(double x) noexcept
557 {
558 double sum = 1.0, term = 1.0;
559 const double halfX = x / 2.0;
560 for (int k = 1; k < 60; ++k)
561 {
562 const double f = halfX / static_cast<double>(k);
563 term *= f * f;
564 sum += term;
565 if (term < sum * 1e-17) break;
566 }
567 return sum;
568 }
569
570 std::vector<T> table_;
571 size_t offset_[kSteps] {};
572 int half_[kSteps] {};
573};
574
575} // namespace dspark
32-tap Kaiser-windowed sinc reader for transparent fractional reads.
T read(const T *x, double frac) const noexcept
Interpolates kTaps consecutive samples at x[kBefore] + frac.
static constexpr int kAfter
Samples needed after intPos.
T readRing(const T *ring, int64_t mask, int64_t intPos, double frac) const noexcept
Reads a power-of-two ring buffer at intPos + frac.
static constexpr int kPhases
Tabulated fractional phases.
static constexpr int kTaps
Kernel length.
SincInterpolator()
Builds the kernel table (allocates: construct on a setup thread).
static constexpr int kBefore
Samples needed before intPos.
Band-limited fractional reader for read rates from 1 to 4.
static constexpr int kSteps
Tabulated rates.
int reach(int step) const noexcept
static constexpr int kMaxTaps
Kernel length at rate 4.
T readRing(const T *ring, int64_t mask, int64_t intPos, double frac, int step) const noexcept
Reads a power-of-two ring buffer at intPos + frac with the kernel of a rate step. It needs the sample...
static int stepFor(double rate) noexcept
The table for a read rate: the nearest tabulated rate at or above it. Costs a log2,...
static constexpr int kPhases
Phases per rate.
static constexpr int kStepsPerOctave
Rate resolution.
StretchedSincReader()
Builds the kernel tables (allocates: construct on a setup thread).
T dotProductT(const T *DSPARK_RESTRICT a, const T *DSPARK_RESTRICT b, int count) noexcept
Definition SimdOps.h:1182
Main namespace for the DSPark framework.
T interpolateCubic(const T *buffer, int length, T position) noexcept
Alias of interpolateHermite (Catmull-Rom evaluated in Hermite form). Kept for backward compatibility.
T interpolateHermite(T y0, T y1, T y2, T y3, T frac) noexcept
4-point, 3rd-order Hermite interpolation (optimized x-form).
T interpolateLinear(T y0, T y1, T frac) noexcept
Linear interpolation between two adjacent samples.
T interpolateLagrange(T y0, T y1, T y2, T y3, T frac) noexcept
4-point Lagrange interpolation from discrete samples.
T interpolateAllpass(T currentSample, T previousSample, T frac, T &state) noexcept
Allpass interpolation (first-order Thiran) for fractional delay.