DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
WaveshapeTable.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
41#include "DspMath.h"
42#include "AudioSpec.h"
43#include "AudioBuffer.h"
44#include "Oversampling.h"
45#include "Interpolation.h"
46
47#include <algorithm>
48#include <array>
49#include <cassert>
50#include <cmath>
51#include <functional>
52#include <memory>
53#include <vector>
54
55namespace dspark {
56
63template <FloatType T>
65{
66public:
71 {
72 buildFromFunction([](T x) { return x; }, 4);
73 }
74
89 void buildFromFunction(std::function<T(T)> func, int tableSize = 4096, T xMax = T(8))
90 {
91 assert(tableSize >= 4);
92 assert(xMax > T(0));
93
94 // Release-safe clamps: a tableSize below 4 makes process() compute
95 // negative table positions (out-of-bounds reads), and a huge one
96 // overflows the 32-bit size_t arithmetic on WASM. Invalid xMax
97 // (non-positive or NaN) falls back to the default half-range.
98 tableSize = std::clamp(tableSize, 4, 1 << 20);
99 if (!(xMax > T(0))) xMax = T(8);
100
101 tableSize_ = tableSize;
102 xMax_ = std::max(xMax, T(0.001));
103 invRange_ = T(1) / (T(2) * xMax_);
104
105 // Allocate tableSize + 3 to accommodate Hermite interpolation padding.
106 // This eliminates conditional branch bounds-checking in the DSP hot-path.
107 table_.resize(static_cast<size_t>(tableSize) + 3);
108
109 for (int i = 0; i < tableSize; ++i)
110 {
111 T x = -xMax_ + T(2) * xMax_ * static_cast<T>(i) / static_cast<T>(tableSize - 1);
112 table_[static_cast<size_t>(i + 1)] = func(x); // Offset by 1
113 }
114
115 // Pad boundaries for Hermite (mirroring endpoints)
116 table_[0] = table_[1];
117 table_[static_cast<size_t>(tableSize + 1)] = table_[static_cast<size_t>(tableSize)];
118 table_[static_cast<size_t>(tableSize + 2)] = table_[static_cast<size_t>(tableSize)];
119
120 // Antiderivative at every knot, integrating the interpolated curve
121 // segment by segment (exact for the Hermite cubic), in double: the
122 // ADAA difference quotient cancels most of its digits.
123 step_ = 2.0 * static_cast<double>(xMax_) / static_cast<double>(tableSize - 1);
124 integral_.assign(static_cast<size_t>(tableSize), 0.0);
125 for (int i = 0; i + 1 < tableSize; ++i)
126 integral_[static_cast<size_t>(i + 1)] = integral_[static_cast<size_t>(i)]
127 + step_ * segmentIntegral(i + 1, 1.0);
128 resetAntialiasing();
129 }
130
136 void buildTanh(int tableSize = 4096)
137 {
138 buildFromFunction([](T x) -> T { return std::tanh(x); }, tableSize);
139 }
140
146 void buildHardClip(T threshold = T(0.8), int tableSize = 4096)
147 {
148 buildFromFunction([threshold](T x) -> T {
149 return std::clamp(x, -threshold, threshold);
150 }, tableSize);
151 }
152
157 void buildSoftClip(int tableSize = 4096)
158 {
159 buildFromFunction([](T x) -> T {
160 if (x > T(1)) return T(2.0 / 3.0);
161 if (x < T(-1)) return T(-2.0 / 3.0);
162 return x - (x * x * x) / T(3);
163 }, tableSize);
164 }
165
170 void buildAsymmetric(int tableSize = 4096)
171 {
172 buildFromFunction([](T x) -> T {
173 return x >= T(0) ? std::tanh(x * T(1.2)) : std::tanh(x * T(0.8));
174 }, tableSize);
175 }
176
185 [[nodiscard]] inline T process(T input, T preGain = T(1), T postGain = T(1)) const noexcept
186 {
187 // The table spans [-xMax, xMax] (default 8), so realistic drive values
188 // keep tracing the curve instead of flattening at the old +-1 boundary.
189 // min/max are ordered so that a NaN (input or preGain) resolves to the
190 // upper table edge instead of reaching the int cast below (undefined)
191 // and indexing out of bounds: the output stays finite.
192 T driven = std::max(-xMax_, std::min(xMax_, input * preGain));
193
194 // Map to index range [0, tableSize - 1]
195 T pos = (driven + xMax_) * invRange_ * static_cast<T>(tableSize_ - 1);
196
197 int idx = static_cast<int>(pos);
198 T frac = pos - static_cast<T>(idx);
199
200 // Base index offset by +1 due to left-side padding
201 const T* t = table_.data() + idx + 1;
202
203 // Catmull-Rom smoothing via the shared Hermite kernel (Interpolation.h)
204 return interpolateHermite(t[-1], t[0], t[1], t[2], frac) * postGain;
205 }
206
218 void process(T* __restrict data, int numSamples, T preGain = T(1), T postGain = T(1)) const noexcept
219 {
220 for (int i = 0; i < numSamples; ++i)
221 data[i] = process(data[i], preGain, postGain);
222 }
223
224 // -- Lifecycle & Oversampling ---------------------------------------------
225
230 void prepare(const AudioSpec& spec)
231 {
232 spec_ = spec;
233 if (oversampler_)
234 oversampler_->prepare(spec);
235 }
236
242 void setOversampling(int factor)
243 {
244 assert(factor >= 1 && (factor & (factor - 1)) == 0);
245
246 if (factor > 1)
247 {
248 oversampler_ = std::make_unique<Oversampling<T>>(factor);
249 // Oversampling normalises invalid factors release-safe (rounds
250 // down to a power of two); mirror the value it actually adopted
251 // so getOversamplingFactor() never lies about the running rate.
252 oversamplingFactor_ = oversampler_->getFactor();
253 if (spec_.sampleRate > 0)
254 oversampler_->prepare(spec_);
255 }
256 else
257 {
258 oversamplingFactor_ = 1;
259 oversampler_.reset();
260 }
261 }
262
263 [[nodiscard]] int getOversamplingFactor() const noexcept { return oversamplingFactor_; }
264
271 void processBlock(AudioBufferView<T> buffer, T preGain = T(1), T postGain = T(1)) noexcept
272 {
273 const bool adaa = antialias_;
274
275 auto shape = [&](T* data, int n, int ch) {
276 if (adaa && ch < kAntialiasChannels)
277 processAntialiased(data, n, preGain, postGain, ch);
278 else
279 process(data, n, preGain, postGain);
280 };
281
282 if (oversamplingFactor_ > 1 && oversampler_)
283 {
284 auto upView = oversampler_->upsample(buffer);
285 for (int ch = 0; ch < upView.getNumChannels(); ++ch)
286 shape(upView.getChannel(ch), upView.getNumSamples(), ch);
287 oversampler_->downsample(buffer);
288 }
289 else
290 {
291 for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
292 shape(buffer.getChannel(ch), buffer.getNumSamples(), ch);
293 }
294 }
295
313 void setAntialiasing(bool enabled) noexcept
314 {
315 if (enabled && !antialias_) resetAntialiasing();
316 antialias_ = enabled;
317 }
318
320 [[nodiscard]] bool isAntialiasingEnabled() const noexcept { return antialias_; }
321
322 void reset() noexcept
323 {
324 if (oversampler_) oversampler_->reset();
325 resetAntialiasing();
326 }
327
328 [[nodiscard]] int getTableSize() const noexcept { return tableSize_; }
329 [[nodiscard]] bool isReady() const noexcept { return tableSize_ > 0; }
330
332 [[nodiscard]] T getInputRange() const noexcept { return xMax_; }
333
339 [[nodiscard]] int getLatency() const noexcept
340 {
341 return (oversampler_ && oversamplingFactor_ > 1) ? oversampler_->getLatency() : 0;
342 }
343
344private:
345 static constexpr int kAntialiasChannels = 16;
346
349 [[nodiscard]] double segmentIntegral(int i, double u) const noexcept
350 {
351 const double y0 = table_[static_cast<size_t>(i - 1)];
352 const double y1 = table_[static_cast<size_t>(i)];
353 const double y2 = table_[static_cast<size_t>(i + 1)];
354 const double y3 = table_[static_cast<size_t>(i + 2)];
355 // Same coefficients as interpolateHermite(): ((a u - b) u + c) u + y1.
356 const double c = (y2 - y0) * 0.5;
357 const double v = y1 - y2;
358 const double w = c + v;
359 const double a = w + v + (y3 - y1) * 0.5;
360 const double b = w + a;
361 return (((a * 0.25 * u - b / 3.0) * u + c * 0.5) * u + y1) * u;
362 }
363
366 [[nodiscard]] double antiderivative(double x) const noexcept
367 {
368 const double xMax = static_cast<double>(xMax_);
369 const int last = tableSize_ - 1;
370 if (x >= xMax)
371 return integral_[static_cast<size_t>(last)]
372 + static_cast<double>(table_[static_cast<size_t>(last + 1)]) * (x - xMax);
373 if (x <= -xMax)
374 return static_cast<double>(table_[1]) * (x + xMax);
375 const double pos = (x + xMax) / step_;
376 const int idx = std::min(static_cast<int>(pos), last - 1);
377 return integral_[static_cast<size_t>(idx)] + step_ * segmentIntegral(idx + 1, pos - idx);
378 }
379
380 void processAntialiased(T* data, int numSamples, T preGain, T postGain, int ch) noexcept
381 {
382 // Out-of-range drive keeps its finite linear continuation; NaN lands
383 // on the upper edge, as in process().
384 const double limit = 1.0e3 * static_cast<double>(xMax_);
385 const auto c = static_cast<size_t>(ch);
386 double x0 = adaaX_[c];
387 double f0 = adaaF_[c];
388 const double post = static_cast<double>(postGain);
389 for (int i = 0; i < numSamples; ++i)
390 {
391 const double x = std::max(-limit, std::min(limit, static_cast<double>(data[i] * preGain)));
392 const double f = antiderivative(x);
393 if (!adaaPrimed_[c]) // first input after a reset: nothing to average with yet
394 {
395 x0 = x;
396 f0 = f;
397 adaaPrimed_[c] = true;
398 }
399 const double dx = x - x0;
400 double y;
401 if (std::abs(dx) > 1.0e-6)
402 y = (f - f0) / dx;
403 else // ill-conditioned: the mean over a vanishing segment is its midpoint value
404 y = static_cast<double>(process(static_cast<T>(0.5 * (x + x0))));
405 data[i] = static_cast<T>(y * post);
406 x0 = x;
407 f0 = f;
408 }
409 adaaX_[c] = x0;
410 adaaF_[c] = f0;
411 }
412
413 void resetAntialiasing() noexcept { adaaPrimed_.fill(false); }
414
415 std::vector<T> table_;
416 std::vector<double> integral_;
417 double step_ = 1.0;
418 bool antialias_ = false;
419 std::array<double, kAntialiasChannels> adaaX_ {};
420 std::array<double, kAntialiasChannels> adaaF_ {};
421 std::array<bool, kAntialiasChannels> adaaPrimed_ {};
422 int tableSize_ = 0;
423 T xMax_ = T(8);
424 T invRange_ = T(1) / T(16);
425
426 AudioSpec spec_ {};
427 std::unique_ptr<Oversampling<T>> oversampler_;
428 int oversamplingFactor_ = 1;
429};
430
431} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
int getNumSamples() const noexcept
Returns the number of samples per channel.
int getNumChannels() const noexcept
Returns the number of channels in this view.
T * getChannel(int ch) const noexcept
Returns a pointer to the sample data for the given channel.
Zero-latency lookup-table waveshaper with RT-safe gain modulation.
void buildTanh(int tableSize=4096)
Builds a normalized tanh (soft clip) table.
bool isAntialiasingEnabled() const noexcept
True when processBlock() applies ADAA.
int getLatency() const noexcept
Reports the processing latency in samples.
void buildSoftClip(int tableSize=4096)
Builds a cubic soft-clip table.
int getTableSize() const noexcept
T process(T input, T preGain=T(1), T postGain=T(1)) const noexcept
Processes a single sample through the waveshaper with Hermite interpolation.
void buildAsymmetric(int tableSize=4096)
Builds an asymmetric clipping table for even-harmonic generation.
void prepare(const AudioSpec &spec)
Prepares the waveshaper for oversampled block processing.
void buildHardClip(T threshold=T(0.8), int tableSize=4096)
Builds a hard-clip table.
WaveshapeTable()
Constructor. Initializes a safe passthrough table to prevent RT crashes.
bool isReady() const noexcept
void buildFromFunction(std::function< T(T)> func, int tableSize=4096, T xMax=T(8))
Builds the lookup table from an arbitrary transfer function.
void setAntialiasing(bool enabled) noexcept
Enables first-order antiderivative anti-aliasing (ADAA) in processBlock().
void setOversampling(int factor)
Enables oversampling.
void processBlock(AudioBufferView< T > buffer, T preGain=T(1), T postGain=T(1)) noexcept
Processes a buffer view with optional oversampling.
void process(T *__restrict data, int numSamples, T preGain=T(1), T postGain=T(1)) const noexcept
Processes a buffer in-place.
T getInputRange() const noexcept
Returns the half-range of the table's input domain.
int getOversamplingFactor() const noexcept
Main namespace for the DSPark framework.
T interpolateHermite(T y0, T y1, T y2, T y3, T frac) noexcept
4-point, 3rd-order Hermite interpolation (optimized x-form).
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45