DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
WaveshapeTable.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
37#include "DspMath.h"
38#include "AudioSpec.h"
39#include "AudioBuffer.h"
40#include "Oversampling.h"
41#include "Interpolation.h"
42
43#include <algorithm>
44#include <cassert>
45#include <cmath>
46#include <functional>
47#include <memory>
48#include <vector>
49
50namespace dspark {
51
58template <FloatType T>
60{
61public:
66 {
67 buildFromFunction([](T x) { return x; }, 4);
68 }
69
84 void buildFromFunction(std::function<T(T)> func, int tableSize = 4096, T xMax = T(8))
85 {
86 assert(tableSize >= 4);
87 assert(xMax > T(0));
88
89 // Release-safe clamps: a tableSize below 4 makes process() compute
90 // negative table positions (out-of-bounds reads), and a huge one
91 // overflows the 32-bit size_t arithmetic on WASM. Invalid xMax
92 // (non-positive or NaN) falls back to the default half-range.
93 tableSize = std::clamp(tableSize, 4, 1 << 20);
94 if (!(xMax > T(0))) xMax = T(8);
95
96 tableSize_ = tableSize;
97 xMax_ = std::max(xMax, T(0.001));
98 invRange_ = T(1) / (T(2) * xMax_);
99
100 // Allocate tableSize + 3 to accommodate Hermite interpolation padding.
101 // This eliminates conditional branch bounds-checking in the DSP hot-path.
102 table_.resize(static_cast<size_t>(tableSize) + 3);
103
104 for (int i = 0; i < tableSize; ++i)
105 {
106 T x = -xMax_ + T(2) * xMax_ * static_cast<T>(i) / static_cast<T>(tableSize - 1);
107 table_[static_cast<size_t>(i + 1)] = func(x); // Offset by 1
108 }
109
110 // Pad boundaries for Hermite (mirroring endpoints)
111 table_[0] = table_[1];
112 table_[static_cast<size_t>(tableSize + 1)] = table_[static_cast<size_t>(tableSize)];
113 table_[static_cast<size_t>(tableSize + 2)] = table_[static_cast<size_t>(tableSize)];
114 }
115
121 void buildTanh(int tableSize = 4096)
122 {
123 buildFromFunction([](T x) -> T { return std::tanh(x); }, tableSize);
124 }
125
131 void buildHardClip(T threshold = T(0.8), int tableSize = 4096)
132 {
133 buildFromFunction([threshold](T x) -> T {
134 return std::clamp(x, -threshold, threshold);
135 }, tableSize);
136 }
137
142 void buildSoftClip(int tableSize = 4096)
143 {
144 buildFromFunction([](T x) -> T {
145 if (x > T(1)) return T(2.0 / 3.0);
146 if (x < T(-1)) return T(-2.0 / 3.0);
147 return x - (x * x * x) / T(3);
148 }, tableSize);
149 }
150
155 void buildAsymmetric(int tableSize = 4096)
156 {
157 buildFromFunction([](T x) -> T {
158 return x >= T(0) ? std::tanh(x * T(1.2)) : std::tanh(x * T(0.8));
159 }, tableSize);
160 }
161
170 [[nodiscard]] inline T process(T input, T preGain = T(1), T postGain = T(1)) const noexcept
171 {
172 // The table spans [-xMax, xMax] (default 8), so realistic drive values
173 // keep tracing the curve instead of flattening at the old +-1 boundary.
174 // min/max are ordered so that a NaN (input or preGain) resolves to the
175 // upper table edge instead of reaching the int cast below (undefined)
176 // and indexing out of bounds: the output stays finite.
177 T driven = std::max(-xMax_, std::min(xMax_, input * preGain));
178
179 // Map to index range [0, tableSize - 1]
180 T pos = (driven + xMax_) * invRange_ * static_cast<T>(tableSize_ - 1);
181
182 int idx = static_cast<int>(pos);
183 T frac = pos - static_cast<T>(idx);
184
185 // Base index offset by +1 due to left-side padding
186 const T* t = table_.data() + idx + 1;
187
188 // Catmull-Rom smoothing via the shared Hermite kernel (Interpolation.h)
189 return interpolateHermite(t[-1], t[0], t[1], t[2], frac) * postGain;
190 }
191
203 void process(T* __restrict data, int numSamples, T preGain = T(1), T postGain = T(1)) const noexcept
204 {
205 for (int i = 0; i < numSamples; ++i)
206 data[i] = process(data[i], preGain, postGain);
207 }
208
209 // -- Lifecycle & Oversampling ---------------------------------------------
210
215 void prepare(const AudioSpec& spec)
216 {
217 spec_ = spec;
218 if (oversampler_)
219 oversampler_->prepare(spec);
220 }
221
227 void setOversampling(int factor)
228 {
229 assert(factor >= 1 && (factor & (factor - 1)) == 0);
230
231 if (factor > 1)
232 {
233 oversampler_ = std::make_unique<Oversampling<T>>(factor);
234 // Oversampling normalises invalid factors release-safe (rounds
235 // down to a power of two); mirror the value it actually adopted
236 // so getOversamplingFactor() never lies about the running rate.
237 oversamplingFactor_ = oversampler_->getFactor();
238 if (spec_.sampleRate > 0)
239 oversampler_->prepare(spec_);
240 }
241 else
242 {
243 oversamplingFactor_ = 1;
244 oversampler_.reset();
245 }
246 }
247
248 [[nodiscard]] int getOversamplingFactor() const noexcept { return oversamplingFactor_; }
249
256 void processBlock(AudioBufferView<T> buffer, T preGain = T(1), T postGain = T(1)) noexcept
257 {
258 if (oversamplingFactor_ > 1 && oversampler_)
259 {
260 auto upView = oversampler_->upsample(buffer);
261 for (int ch = 0; ch < upView.getNumChannels(); ++ch)
262 {
263 process(upView.getChannel(ch), upView.getNumSamples(), preGain, postGain);
264 }
265 oversampler_->downsample(buffer);
266 }
267 else
268 {
269 for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
270 {
271 process(buffer.getChannel(ch), buffer.getNumSamples(), preGain, postGain);
272 }
273 }
274 }
275
276 void reset() noexcept
277 {
278 if (oversampler_) oversampler_->reset();
279 }
280
281 [[nodiscard]] int getTableSize() const noexcept { return tableSize_; }
282 [[nodiscard]] bool isReady() const noexcept { return tableSize_ > 0; }
283
285 [[nodiscard]] T getInputRange() const noexcept { return xMax_; }
286
292 [[nodiscard]] int getLatency() const noexcept
293 {
294 return (oversampler_ && oversamplingFactor_ > 1) ? oversampler_->getLatency() : 0;
295 }
296
297private:
298 std::vector<T> table_;
299 int tableSize_ = 0;
300 T xMax_ = T(8);
301 T invRange_ = T(1) / T(16);
302
303 AudioSpec spec_ {};
304 std::unique_ptr<Oversampling<T>> oversampler_;
305 int oversamplingFactor_ = 1;
306};
307
308} // namespace dspark
Owning audio buffer and non-owning view for real-time DSP processing.
Describes the audio processing environment (sample rate, block size, channels).
Core mathematical utilities for digital signal processing.
Fractional-position interpolation for delay lines, tables and buffers.
Power-of-two oversampling with polyphase half-band FIR filters.
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.
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 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:35
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43