DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Clipper.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
40#include "../Core/AudioBuffer.h"
41#include "../Core/AudioSpec.h"
42#include "../Core/DspMath.h"
43#include "../Core/DryWetMixer.h"
44#include "../Core/Oversampling.h"
45#include "../Core/StateBlob.h"
46
47#include <algorithm>
48#include <atomic>
49#include <bit>
50#include <cmath>
51#include <cstddef>
52#include <cstdint>
53#include <memory>
54#include <numbers>
55#include <vector>
56
57namespace dspark {
58
65template <FloatType T>
67{
68public:
70 enum class Mode
71 {
72 Hard,
73 Soft,
74 Analog,
76 };
77
78 // -- Lifecycle --------------------------------------------------------------
79
93 void prepare(const AudioSpec& spec)
94 {
95 if (!spec.isValid()) return;
96
97 prepared_.store(false, std::memory_order_relaxed);
98 spec_ = spec;
99 mixer_.prepare(spec);
100
101 const int osFactor = osFactor_.load(std::memory_order_relaxed);
102
103 if (osFactor > 1) {
104 oversampler_ = std::make_unique<Oversampling<T>>(
106 oversampler_->prepare(spec);
107 } else {
108 oversampler_.reset();
109 }
110
111 // Align the dry copy with the latent (oversampled) wet path so that a
112 // dry/wet blend below 100% does not comb-filter.
113 mixer_.setLatencyCompensation(oversampler_ ? oversampler_->getLatency() : 0);
114
115 for (int ch = 0; ch < kMaxChannels; ++ch)
116 slewPrev_[ch] = T(0);
117
118 prepared_.store(true, std::memory_order_relaxed);
119 }
120
127 void reset() noexcept
128 {
129 mixer_.reset();
130 if (oversampler_) oversampler_->reset();
131 for (int ch = 0; ch < kMaxChannels; ++ch)
132 slewPrev_[ch] = T(0);
133 gainReductionDb_.store(T(0), std::memory_order_relaxed);
134 }
135
136 // -- Processing -------------------------------------------------------------
137
146 void processBlock(AudioBufferView<T> buffer) noexcept
147 {
148 if (!prepared_.load(std::memory_order_relaxed)) return;
149 if (buffer.getNumSamples() == 0 || buffer.getNumChannels() == 0) return;
150
151 T mixVal = mix_.load(std::memory_order_relaxed);
152 mixer_.pushDry(buffer);
153
154 if (oversampler_ && oversampler_->getFactor() > 1)
155 {
156 auto upView = oversampler_->upsample(buffer);
157 processInternal(upView, spec_.sampleRate * oversampler_->getFactor());
158 oversampler_->downsample(buffer);
159 }
160 else
161 {
163 }
164
165 mixer_.mixWet(buffer, mixVal);
166 }
167
168 // -- Parameters (Thread-Safe Setters/Getters) -------------------------------
169
174 void setMode(Mode mode) noexcept
175 {
176 const int m = std::clamp(static_cast<int>(mode), 0,
177 static_cast<int>(Mode::GoldenRatio));
178 mode_.store(static_cast<Mode>(m), std::memory_order_relaxed);
179 }
180
186 void setCeiling(T dB) noexcept
187 {
188 if (!std::isfinite(dB)) return;
189 ceilingDb_.store(std::clamp(dB, T(-60), T(0)), std::memory_order_relaxed);
190 }
191
197 void setInputGain(T dB) noexcept
198 {
199 if (!std::isfinite(dB)) return;
200 inputGainDb_.store(std::clamp(dB, T(0), T(48)), std::memory_order_relaxed);
201 }
202
212 void setStages(int count) noexcept
213 {
214 stages_.store(std::clamp(count, 1, kMaxStages), std::memory_order_relaxed);
215 }
216
222 void setMix(T amount) noexcept
223 {
224 if (!std::isfinite(amount)) return;
225 mix_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
226 }
227
240 void setSlewLimit(T ms) noexcept
241 {
242 if (!std::isfinite(ms)) return;
243 slewLimitMs_.store(std::max(ms, T(0)), std::memory_order_relaxed);
244 }
245
254 void setOversampling(int factor) noexcept
255 {
256 factor = std::bit_ceil(static_cast<unsigned int>(std::max(1, factor)));
257 osFactor_.store(std::min(factor, 16), std::memory_order_relaxed);
258 }
259
260 [[nodiscard]] Mode getMode() const noexcept { return mode_.load(std::memory_order_relaxed); }
261 [[nodiscard]] T getCeiling() const noexcept { return ceilingDb_.load(std::memory_order_relaxed); }
262 [[nodiscard]] T getInputGain() const noexcept { return inputGainDb_.load(std::memory_order_relaxed); }
263 [[nodiscard]] int getStages() const noexcept { return stages_.load(std::memory_order_relaxed); }
264 [[nodiscard]] T getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
265 [[nodiscard]] T getSlewLimit() const noexcept { return slewLimitMs_.load(std::memory_order_relaxed); }
266
268 [[nodiscard]] int getOversampling() const noexcept { return osFactor_.load(std::memory_order_relaxed); }
269
271 [[nodiscard]] int getLatency() const noexcept { return oversampler_ ? oversampler_->getLatency() : 0; }
272
274 [[nodiscard]] T getGainReductionDb() const noexcept { return gainReductionDb_.load(std::memory_order_relaxed); }
275
276
278 [[nodiscard]] std::vector<uint8_t> getState() const
279 {
280 StateWriter w(stateId("CLIP"), 1);
281 w.write("mode", static_cast<int32_t>(mode_.load(std::memory_order_relaxed)));
282 w.write("ceiling", static_cast<float>(ceilingDb_.load(std::memory_order_relaxed)));
283 w.write("inputGain", static_cast<float>(inputGainDb_.load(std::memory_order_relaxed)));
284 w.write("stages", stages_.load(std::memory_order_relaxed));
285 w.write("mix", static_cast<float>(mix_.load(std::memory_order_relaxed)));
286 w.write("slewLimit", static_cast<float>(slewLimitMs_.load(std::memory_order_relaxed)));
287 w.write("oversampling", osFactor_.load(std::memory_order_relaxed));
288 return w.blob();
289 }
290
293 bool setState(const uint8_t* data, size_t size)
294 {
295 StateReader r(data, size);
296 if (!r.isValid() || r.processorId() != stateId("CLIP")) return false;
297 setMode(static_cast<Mode>(r.read("mode", 0)));
298 setCeiling(static_cast<T>(r.read("ceiling", 0.0f)));
299 setInputGain(static_cast<T>(r.read("inputGain", 0.0f)));
300 setStages(r.read("stages", 1));
301 setMix(static_cast<T>(r.read("mix", 1.0f)));
302 setSlewLimit(static_cast<T>(r.read("slewLimit", 0.0f)));
303 setOversampling(r.read("oversampling", 1));
304 return true;
305 }
306
307protected:
308 static constexpr int kMaxStages = 4;
309 static constexpr int kMaxChannels = 16;
310
312 static constexpr T kPhi = static_cast<T>(1.6180339887498948482);
313
317 void processInternal(AudioBufferView<T>& buffer, double currentSampleRate) noexcept
318 {
319 Mode modeVal = mode_.load(std::memory_order_relaxed);
320 T ceilDb = ceilingDb_.load(std::memory_order_relaxed);
321 T gainDb = inputGainDb_.load(std::memory_order_relaxed);
322 int numStages = stages_.load(std::memory_order_relaxed);
323 T slewMs = slewLimitMs_.load(std::memory_order_relaxed);
324
325 T ceiling = dbToLinear(ceilDb);
326 T totalGainLin = dbToLinear(gainDb);
327
328 // Split gain logarithmically across cascaded stages
329 T stageGain = (numStages > 1)
330 ? std::pow(totalGainLin, T(1) / static_cast<T>(numStages))
331 : totalGainLin;
332
333 // Calculate maximum allowed delta per sample based on the physical sample rate
334 T maxSlewDelta = T(0);
335 if (slewMs > T(0)) {
336 maxSlewDelta = (ceiling / (slewMs * T(0.001))) / static_cast<T>(currentSampleRate);
337 }
338
339 // The mode switch is resolved once per block, outside the sample loop.
340 switch (modeVal)
341 {
342 case Mode::Hard: dispatchClipping<Mode::Hard>(buffer, totalGainLin, stageGain, numStages, ceiling, maxSlewDelta); break;
343 case Mode::Soft: dispatchClipping<Mode::Soft>(buffer, totalGainLin, stageGain, numStages, ceiling, maxSlewDelta); break;
344 case Mode::Analog: dispatchClipping<Mode::Analog>(buffer, totalGainLin, stageGain, numStages, ceiling, maxSlewDelta); break;
345 case Mode::GoldenRatio: dispatchClipping<Mode::GoldenRatio>(buffer, totalGainLin, stageGain, numStages, ceiling, maxSlewDelta); break;
346 default: dispatchClipping<Mode::Hard>(buffer, totalGainLin, stageGain, numStages, ceiling, maxSlewDelta); break;
347 }
348 }
349
354 template <Mode M>
355 void dispatchClipping(AudioBufferView<T>& buffer, T totalGainLin, T stageGain, int numStages, T ceiling, T maxSlewDelta) noexcept
356 {
357 const int nCh = std::min(buffer.getNumChannels(), kMaxChannels);
358 const int nS = buffer.getNumSamples();
359
360 T peakIn = T(0);
361 T peakOut = T(0);
362
363 for (int ch = 0; ch < nCh; ++ch)
364 {
365 T* data = buffer.getChannel(ch);
366 for (int i = 0; i < nS; ++i)
367 {
368 T sample = data[i];
369 T driven = sample * totalGainLin;
370
371 // Compiler will unroll this loop for small bounded N
372 for (int s = 0; s < numStages; ++s)
373 {
374 sample *= stageGain;
375 sample = processSample<M>(sample, ceiling);
376 }
377
378 // Slew Limiter processing (branch highly predictable if static)
379 if (maxSlewDelta > T(0))
380 {
381 T delta = sample - slewPrev_[ch];
382 if (std::abs(delta) > maxSlewDelta)
383 sample = slewPrev_[ch] + std::copysign(maxSlewDelta, delta);
384 }
385
386 slewPrev_[ch] = sample;
387
388 peakIn = std::max(peakIn, std::abs(driven));
389 peakOut = std::max(peakOut, std::abs(sample));
390 data[i] = sample;
391 }
392 }
393
394 // Safe gain reduction calculation
395 if (peakIn > T(1e-6)) {
396 auto ratio = std::min(peakOut / peakIn, T(1));
397 gainReductionDb_.store(gainToDecibels(ratio, T(-100)), std::memory_order_relaxed);
398 } else {
399 gainReductionDb_.store(T(0), std::memory_order_relaxed);
400 }
401 }
402
406 template <Mode M>
407 [[nodiscard]] static inline T processSample(T sample, T ceiling) noexcept
408 {
409 if constexpr (M == Mode::Hard)
410 {
411 return std::clamp(sample, -ceiling, ceiling);
412 }
413 else if constexpr (M == Mode::Soft)
414 {
415 return ceiling * std::tanh(sample / ceiling);
416 }
417 else if constexpr (M == Mode::Analog)
418 {
419 // Unity-slope sine shaper: sin(x/ceiling) has derivative 1 at the
420 // origin, so quiet material passes at exactly 0 dB like the other
421 // modes (the old pre-scaled form had slope pi/2, a +3.9 dB jump
422 // when switching modes). The knee spans |x| in [~ceiling*0.5,
423 // ceiling*pi/2] and lands exactly on the ceiling.
424 constexpr T halfPi = static_cast<T>(std::numbers::pi * 0.5);
425 return ceiling * fastSin(std::clamp(sample / ceiling, -halfPi, halfPi));
426 }
427 else // Mode::GoldenRatio (the mode switch instantiates no other value)
428 {
429 // True mathematical Golden Ratio soft-knee
430 T threshold = ceiling / kPhi;
431 T absSample = std::abs(sample);
432
433 if (absSample <= threshold)
434 return sample;
435
436 // Rational asymptotic curve to the ceiling
437 T sign = std::copysign(T(1), sample);
438 T excess = absSample - threshold;
439 T range = ceiling - threshold;
440
441 return sign * (threshold + (range * excess) / (excess + range));
442 }
443 }
444
445 // Mathematical utility helpers
446 [[nodiscard]] static T dbToLinear(T dB) noexcept { return std::pow(T(10), dB / T(20)); }
447 [[nodiscard]] static T gainToDecibels(T linear, T minusInfinityDb) noexcept
448 {
449 return linear > T(1e-5) ? T(20) * std::log10(linear) : minusInfinityDb;
450 }
451
453 std::atomic<bool> prepared_ { false };
455 std::unique_ptr<Oversampling<T>> oversampler_;
456
457 // Lock-free parameter states
458 std::atomic<Mode> mode_ { Mode::Hard };
459 std::atomic<T> ceilingDb_ { T(0) };
460 std::atomic<T> inputGainDb_ { T(0) };
461 std::atomic<int> stages_ { 1 };
462 std::atomic<T> mix_ { T(1) };
463 std::atomic<T> slewLimitMs_ { T(0) };
464 std::atomic<int> osFactor_ { 1 }; // Default to 1 (off)
465
466 // History states per channel
468
469 // Metering state
470 std::atomic<T> gainReductionDb_ { T(0) };
471};
472
473} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Real-time audio clipper with analog modeling and anti-aliasing features.
Definition Clipper.h:67
std::unique_ptr< Oversampling< T > > oversampler_
Definition Clipper.h:455
std::atomic< T > inputGainDb_
Definition Clipper.h:460
std::atomic< int > osFactor_
Definition Clipper.h:464
AudioSpec spec_
Definition Clipper.h:452
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob. Oversampling factor applies on the next prepare() as usual.
Definition Clipper.h:293
std::atomic< T > gainReductionDb_
Definition Clipper.h:470
T slewPrev_[kMaxChannels]
Definition Clipper.h:467
std::atomic< T > ceilingDb_
Definition Clipper.h:459
static T processSample(T sample, T ceiling) noexcept
Compile-time resolution of the waveshaping math.
Definition Clipper.h:407
void setSlewLimit(T ms) noexcept
Enables slew limiting to soften clipping edges.
Definition Clipper.h:240
void setInputGain(T dB) noexcept
Sets the input drive/gain before clipping.
Definition Clipper.h:197
void processInternal(AudioBufferView< T > &buffer, double currentSampleRate) noexcept
Core DSP routing. Resolves atomics and branches to the per-mode template.
Definition Clipper.h:317
int getLatency() const noexcept
Returns latency in samples introduced by oversampling filters.
Definition Clipper.h:271
void dispatchClipping(AudioBufferView< T > &buffer, T totalGainLin, T stageGain, int numStages, T ceiling, T maxSlewDelta) noexcept
Per-mode processing loop (waveshaper inlined at compile time; the slew state and peak metering keep t...
Definition Clipper.h:355
static constexpr T kPhi
Mathematical Golden Ratio used for the GoldenRatio soft-knee transition.
Definition Clipper.h:312
static constexpr int kMaxStages
Definition Clipper.h:308
void prepare(const AudioSpec &spec)
Prepares the clipper for processing, allocating any necessary internal buffers.
Definition Clipper.h:93
Mode
Defines the harmonic waveshaping algorithm used for clipping.
Definition Clipper.h:71
@ Hard
Brickwall digital clipping. High odd harmonics.
@ Analog
Sine-based soft clipping. Transformer-like saturation.
@ Soft
Tanh soft clipping. Even/odd blend, tape-like.
@ GoldenRatio
Mathematical soft-knee using phi. Extremely transparent until heavy drive.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio buffer in-place through the clipping algorithm.
Definition Clipper.h:146
void setStages(int count) noexcept
Sets the number of cascaded clipping stages.
Definition Clipper.h:212
void setMode(Mode mode) noexcept
Sets the clipping algorithm.
Definition Clipper.h:174
T getCeiling() const noexcept
Definition Clipper.h:261
T getMix() const noexcept
Definition Clipper.h:264
std::atomic< T > mix_
Definition Clipper.h:462
void setMix(T amount) noexcept
Sets the dry/wet ratio of the processor.
Definition Clipper.h:222
void reset() noexcept
Resets the internal state of the clipper.
Definition Clipper.h:127
void setOversampling(int factor) noexcept
Sets the oversampling multiplier to mitigate aliasing.
Definition Clipper.h:254
T getGainReductionDb() const noexcept
Retrieves the maximum gain reduction applied during the last block (for UI metering).
Definition Clipper.h:274
DryWetMixer< T > mixer_
Definition Clipper.h:454
Mode getMode() const noexcept
Definition Clipper.h:260
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Clipper.h:278
static T gainToDecibels(T linear, T minusInfinityDb) noexcept
Definition Clipper.h:447
static constexpr int kMaxChannels
Definition Clipper.h:309
int getStages() const noexcept
Definition Clipper.h:263
void setCeiling(T dB) noexcept
Sets the absolute maximum output level.
Definition Clipper.h:186
std::atomic< bool > prepared_
Definition Clipper.h:453
T getInputGain() const noexcept
Definition Clipper.h:262
T getSlewLimit() const noexcept
Definition Clipper.h:265
int getOversampling() const noexcept
Returns the published oversampling factor (applied on the next prepare()).
Definition Clipper.h:268
std::atomic< Mode > mode_
Definition Clipper.h:458
static T dbToLinear(T dB) noexcept
Definition Clipper.h:446
std::atomic< T > slewLimitMs_
Definition Clipper.h:463
std::atomic< int > stages_
Definition Clipper.h:461
Pre-allocated, SIMD-friendly dry/wet blender for real-time audio.
Definition DryWetMixer.h:72
Power-of-two oversampling processor with polyphase anti-aliasing.
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
Main namespace for the DSPark framework.
T fastSin(T x) noexcept
Fast sine approximation (degree-9 odd minimax polynomial).
Definition DspMath.h:213
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
constexpr T halfPi
Pi / 2 (1.57079...). Quarter period; sin/cos phase offset.
Definition DspMath.h:45
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43