DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
MinBlepTable.h
Go to the documentation of this file.
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
19#include "FFT.h"
20#include "WindowFunctions.h"
21
22#include <array>
23#include <cmath>
24#include <cstddef>
25#include <numbers>
26#include <type_traits>
27#include <vector>
28
29namespace dspark {
30
70template <typename T>
72{
73 static_assert(std::is_floating_point_v<T>, "MinBlepTable requires float or double");
74
75public:
77 static constexpr int kTaps = 64;
80 static constexpr int kOversample = 64;
82 static constexpr int kTableSize = kTaps * kOversample + 1;
83
90 static const MinBlepTable& instance() noexcept
91 {
92 static const MinBlepTable table;
93 return table;
94 }
95
105 [[nodiscard]] T residual(T t) const noexcept
106 {
107 const T pos = t * static_cast<T>(kOversample);
108 const auto idx = static_cast<int>(pos);
109 if (idx < 0 || idx >= kTableSize - 1)
110 return T(0);
111 const T frac = pos - static_cast<T>(idx);
112 const T a = residual_[static_cast<size_t>(idx)];
113 const T b = residual_[static_cast<size_t>(idx) + 1];
114 return a + frac * (b - a);
115 }
116
117private:
118 MinBlepTable() noexcept { build(); }
119
120 void build() noexcept
121 {
122 constexpr int n = kTableSize; // 4097 oversampled points
123 constexpr size_t fftSize = 65536; // ~16x n keeps cepstral aliasing low
124 constexpr double pi = std::numbers::pi_v<double>;
125
126 // -- 1. Linear-phase BLIT: sinc cutting at the base-rate Nyquist
127 // (zeros every kOversample points), Blackman-Harris windowed
128 // (-92 dB stopband), normalised to unit DC gain so the
129 // integrated step settles at 1.
130 std::vector<double> x(fftSize, 0.0);
131 std::vector<double> win(n);
132 WindowFunctions<double>::blackmanHarris(win.data(), n, false);
133 const double center = 0.5 * (n - 1);
134 double dc = 0.0;
135 for (int i = 0; i < n; ++i)
136 {
137 const double t = (static_cast<double>(i) - center) / kOversample;
138 const double s = (std::abs(t) < 1e-9) ? 1.0 : std::sin(pi * t) / (pi * t);
139 x[static_cast<size_t>(i)] = s * win[static_cast<size_t>(i)];
140 dc += s * win[static_cast<size_t>(i)];
141 }
142 for (int i = 0; i < n; ++i)
143 x[static_cast<size_t>(i)] /= dc;
144
145 // -- 2. Real cepstrum. log|X| is a real, even function of frequency,
146 // so running it through the real inverse FFT (imaginary parts
147 // zero) yields the even cepstrum directly.
148 FFTReal<double> fft(fftSize);
149 std::vector<double> spec(fftSize + 2);
150 std::vector<double> cep(fftSize);
151 fft.forward(x.data(), spec.data());
152
153 const size_t bins = fftSize / 2 + 1;
154 for (size_t k = 0; k < bins; ++k)
155 {
156 const double re = spec[2 * k];
157 const double im = spec[2 * k + 1];
158 // Soft floor at -100 dB, blended in power. The floor must sit just
159 // below the window's -92 dB stopband, not far below it: hard, deep
160 // corners in log|X| make the cepstrum decay like 1/q^2 and alias
161 // through the causal fold, which comes back as percent-level error
162 // in the reconstructed step (measured, not hypothetical).
163 constexpr double floorMag = 1e-5;
164 const double mag = std::sqrt(re * re + im * im + floorMag * floorMag);
165 spec[2 * k] = std::log(mag);
166 spec[2 * k + 1] = 0.0;
167 }
168 fft.inverse(spec.data(), cep.data());
169
170 // -- 3. Fold onto the causal part (Hilbert relation between log
171 // magnitude and minimum phase): keep q=0 and q=N/2, double
172 // 1..N/2-1, zero the anticausal half.
173 for (size_t q = 1; q < fftSize / 2; ++q)
174 {
175 cep[q] *= 2.0;
176 cep[fftSize - q] = 0.0;
177 }
178
179 // -- 4. Back to the spectrum, exponentiate, back to time: the
180 // minimum-phase BLIT, energy packed at the front.
181 fft.forward(cep.data(), spec.data());
182 for (size_t k = 0; k < bins; ++k)
183 {
184 const double m = std::exp(spec[2 * k]);
185 const double ph = spec[2 * k + 1];
186 spec[2 * k] = m * std::cos(ph);
187 spec[2 * k + 1] = m * std::sin(ph);
188 }
189 fft.inverse(spec.data(), x.data());
190
191 // -- 5. Integrate into a step and store the residual. Pinning the
192 // table end exactly at step==1 closes the truncated residual
193 // at zero (no leftover micro-step when the kernel expires).
194 double acc = 0.0;
195 std::vector<double> step(n);
196 for (int i = 0; i < n; ++i)
197 {
198 acc += x[static_cast<size_t>(i)];
199 step[static_cast<size_t>(i)] = acc;
200 }
201 const double settle = step[static_cast<size_t>(n - 1)];
202 for (int i = 0; i < n; ++i)
203 residual_[static_cast<size_t>(i)] =
204 static_cast<T>(step[static_cast<size_t>(i)] / settle - 1.0);
205 }
206
207 std::array<T, kTableSize> residual_{};
208};
209
210} // namespace dspark
Fast Fourier Transform (Cooley-Tukey radix-2) with SIMD acceleration.
Standard window functions for spectral analysis and FIR filter design.
Shared minimum-phase band-limited step (minBLEP) residual table.
T residual(T t) const noexcept
Residual of the minimum-phase band-limited step at position t.
static const MinBlepTable & instance() noexcept
Returns the process-wide shared table, building it on first call.
static constexpr int kOversample
static constexpr int kTableSize
Total table entries (one guard point at the end for interpolation).
static constexpr int kTaps
Correction span in base-rate samples (power of two, ring-buffer friendly).
Main namespace for the DSPark framework.
constexpr T pi
Pi (3.14159...) for the given floating-point type.
Definition DspMath.h:36
static void blackmanHarris(T *output, int size, bool periodic=true) noexcept
Blackman-Harris window (4-term, -92 dB side lobes).