DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
MinBlepTable.h
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
128 [[nodiscard]] T dcDelay() const noexcept { return dcDelay_; }
129
130private:
131 MinBlepTable() noexcept { build(); }
132
133 void build() noexcept
134 {
135 constexpr int n = kTableSize; // 4097 oversampled points
136 constexpr size_t fftSize = 65536; // ~16x n keeps cepstral aliasing low
137 constexpr double pi = std::numbers::pi_v<double>;
138
139 // -- 1. Linear-phase BLIT: sinc cutting at the base-rate Nyquist
140 // (zeros every kOversample points), Blackman-Harris windowed
141 // (-92 dB stopband), normalised to unit DC gain so the
142 // integrated step settles at 1.
143 std::vector<double> x(fftSize, 0.0);
144 std::vector<double> win(n);
145 WindowFunctions<double>::blackmanHarris(win.data(), n, false);
146 const double center = 0.5 * (n - 1);
147 double dc = 0.0;
148 for (int i = 0; i < n; ++i)
149 {
150 const double t = (static_cast<double>(i) - center) / kOversample;
151 const double s = (std::abs(t) < 1e-9) ? 1.0 : std::sin(pi * t) / (pi * t);
152 x[static_cast<size_t>(i)] = s * win[static_cast<size_t>(i)];
153 dc += s * win[static_cast<size_t>(i)];
154 }
155 for (int i = 0; i < n; ++i)
156 x[static_cast<size_t>(i)] /= dc;
157
158 // -- 2. Real cepstrum. log|X| is a real, even function of frequency,
159 // so running it through the real inverse FFT (imaginary parts
160 // zero) yields the even cepstrum directly.
161 FFTReal<double> fft(fftSize);
162 std::vector<double> spec(fftSize + 2);
163 std::vector<double> cep(fftSize);
164 fft.forward(x.data(), spec.data());
165
166 const size_t bins = fftSize / 2 + 1;
167 for (size_t k = 0; k < bins; ++k)
168 {
169 const double re = spec[2 * k];
170 const double im = spec[2 * k + 1];
171 // Soft floor at -100 dB, blended in power. The floor must sit just
172 // below the window's -92 dB stopband, not far below it: hard, deep
173 // corners in log|X| make the cepstrum decay like 1/q^2 and alias
174 // through the causal fold, which comes back as percent-level error
175 // in the reconstructed step (measured, not hypothetical).
176 constexpr double floorMag = 1e-5;
177 const double mag = std::sqrt(re * re + im * im + floorMag * floorMag);
178 spec[2 * k] = std::log(mag);
179 spec[2 * k + 1] = 0.0;
180 }
181 fft.inverse(spec.data(), cep.data());
182
183 // -- 3. Fold onto the causal part (Hilbert relation between log
184 // magnitude and minimum phase): keep q=0 and q=N/2, double
185 // 1..N/2-1, zero the anticausal half.
186 for (size_t q = 1; q < fftSize / 2; ++q)
187 {
188 cep[q] *= 2.0;
189 cep[fftSize - q] = 0.0;
190 }
191
192 // -- 4. Back to the spectrum, exponentiate, back to time: the
193 // minimum-phase BLIT, energy packed at the front.
194 fft.forward(cep.data(), spec.data());
195 for (size_t k = 0; k < bins; ++k)
196 {
197 const double m = std::exp(spec[2 * k]);
198 const double ph = spec[2 * k + 1];
199 spec[2 * k] = m * std::cos(ph);
200 spec[2 * k + 1] = m * std::sin(ph);
201 }
202 fft.inverse(spec.data(), x.data());
203
204 // -- 5. Integrate into a step and store the residual. Pinning the
205 // table end exactly at step==1 closes the truncated residual
206 // at zero (no leftover micro-step when the kernel expires).
207 double acc = 0.0;
208 std::vector<double> step(n);
209 for (int i = 0; i < n; ++i)
210 {
211 acc += x[static_cast<size_t>(i)];
212 step[static_cast<size_t>(i)] = acc;
213 }
214 const double settle = step[static_cast<size_t>(n - 1)];
215 double area = 0.0;
216 for (int i = 0; i < n; ++i)
217 {
218 const double r = step[static_cast<size_t>(i)] / settle - 1.0;
219 residual_[static_cast<size_t>(i)] = static_cast<T>(r);
220 // Trapezoidal integral of the stored residual (what residual()
221 // interpolates), in samples.
222 area += (i == 0 || i == n - 1) ? 0.5 * r : r;
223 }
224 dcDelay_ = static_cast<T>(-area / kOversample);
225 }
226
227 std::array<T, kTableSize> residual_{};
228 T dcDelay_ = T(0);
229};
230
231} // namespace dspark
Shared minimum-phase band-limited step (minBLEP) residual table.
T dcDelay() const noexcept
Low-frequency delay of the minimum-phase step, in samples.
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:45
static void blackmanHarris(T *output, int size, bool periodic=true) noexcept
Blackman-Harris window (4-term, -92 dB side lobes).