DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Clipper.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
43#include "../Core/AudioBuffer.h"
44#include "../Core/AudioSpec.h"
45#include "../Core/DspMath.h"
46#include "../Core/DryWetMixer.h"
47#include "../Core/Oversampling.h"
48#include "../Core/SmoothedValue.h"
49#include "../Core/StateBlob.h"
50#include "../Core/detail/ClipperShape.h"
51
52#include <algorithm>
53#include <array>
54#include <atomic>
55#include <bit>
56#include <cmath>
57#include <cstddef>
58#include <cstdint>
59#include <memory>
60#include <numbers>
61#include <vector>
62
63namespace dspark {
64
71template <FloatType T>
73{
74public:
76 enum class Mode
77 {
78 Hard,
79 Soft,
80 Analog,
82 };
83
84 // -- Lifecycle --------------------------------------------------------------
85
99 void prepare(const AudioSpec& spec)
100 {
101 if (!spec.isValid()) return;
102
103 prepared_.store(false, std::memory_order_relaxed);
104 spec_ = spec;
105 mixer_.prepare(spec);
106
107 const int osFactor = osFactor_.load(std::memory_order_relaxed);
108
109 if (osFactor > 1) {
110 oversampler_ = std::make_unique<Oversampling<T>>(
112 oversampler_->prepare(spec);
113 } else {
114 oversampler_.reset();
115 }
116
117 // Align the dry copy with the latent (oversampled) wet path so that a
118 // dry/wet blend below 100% does not comb-filter.
119 mixer_.setLatencyCompensation(oversampler_ ? oversampler_->getLatency() : 0);
120
121 // Parameter glides run at the processing (possibly oversampled) rate.
122 const double procRate = spec.sampleRate * (oversampler_ ? oversampler_->getFactor() : 1);
123 for (auto* sm : { &stageGainSm_, &ceilingSm_ })
124 {
125 sm->prepare(procRate, kParamRampMs);
126 sm->setSmoothingType(SmoothedValue<T>::SmoothingType::Linear);
127 }
129
130 for (int ch = 0; ch < kMaxChannels; ++ch)
131 slewPrev_[ch] = T(0);
132
133 prepared_.store(true, std::memory_order_relaxed);
134 }
135
142 void reset() noexcept
143 {
144 mixer_.reset();
145 if (oversampler_) oversampler_->reset();
146 for (int ch = 0; ch < kMaxChannels; ++ch)
147 slewPrev_[ch] = T(0);
149 gainReductionDb_.store(T(0), std::memory_order_relaxed);
150 }
151
152 // -- Processing -------------------------------------------------------------
153
162 void processBlock(AudioBufferView<T> buffer) noexcept
163 {
164 if (!prepared_.load(std::memory_order_relaxed)) return;
165 if (buffer.getNumSamples() == 0 || buffer.getNumChannels() == 0) return;
166
167 T mixVal = mix_.load(std::memory_order_relaxed);
168 mixer_.pushDry(buffer);
169
170 if (oversampler_ && oversampler_->getFactor() > 1)
171 {
172 auto upView = oversampler_->upsample(buffer);
173 processInternal(upView, spec_.sampleRate * oversampler_->getFactor());
174 oversampler_->downsample(buffer);
175 }
176 else
177 {
179 }
180
181 mixer_.mixWet(buffer, mixVal);
182 }
183
184 // -- Parameters (Thread-Safe Setters/Getters) -------------------------------
185
190 void setMode(Mode mode) noexcept
191 {
192 const int m = std::clamp(static_cast<int>(mode), 0,
193 static_cast<int>(Mode::GoldenRatio));
194 mode_.store(static_cast<Mode>(m), std::memory_order_relaxed);
195 }
196
202 void setCeiling(T dB) noexcept
203 {
204 if (!std::isfinite(dB)) return;
205 ceilingDb_.store(std::clamp(dB, T(-60), T(0)), std::memory_order_relaxed);
206 }
207
213 void setInputGain(T dB) noexcept
214 {
215 if (!std::isfinite(dB)) return;
216 inputGainDb_.store(std::clamp(dB, T(0), T(48)), std::memory_order_relaxed);
217 }
218
228 void setStages(int count) noexcept
229 {
230 stages_.store(std::clamp(count, 1, kMaxStages), std::memory_order_relaxed);
231 }
232
238 void setMix(T amount) noexcept
239 {
240 if (!std::isfinite(amount)) return;
241 mix_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
242 }
243
256 void setSlewLimit(T ms) noexcept
257 {
258 if (!std::isfinite(ms)) return;
259 slewLimitMs_.store(std::max(ms, T(0)), std::memory_order_relaxed);
260 }
261
270 void setOversampling(int factor) noexcept
271 {
272 factor = std::bit_ceil(static_cast<unsigned int>(std::max(1, factor)));
273 osFactor_.store(std::min(factor, 16), std::memory_order_relaxed);
274 }
275
276 [[nodiscard]] Mode getMode() const noexcept { return mode_.load(std::memory_order_relaxed); }
277 [[nodiscard]] T getCeiling() const noexcept { return ceilingDb_.load(std::memory_order_relaxed); }
278 [[nodiscard]] T getInputGain() const noexcept { return inputGainDb_.load(std::memory_order_relaxed); }
279 [[nodiscard]] int getStages() const noexcept { return stages_.load(std::memory_order_relaxed); }
280 [[nodiscard]] T getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
281 [[nodiscard]] T getSlewLimit() const noexcept { return slewLimitMs_.load(std::memory_order_relaxed); }
282
284 [[nodiscard]] int getOversampling() const noexcept { return osFactor_.load(std::memory_order_relaxed); }
285
287 [[nodiscard]] int getLatency() const noexcept { return oversampler_ ? oversampler_->getLatency() : 0; }
288
290 [[nodiscard]] T getGainReductionDb() const noexcept { return gainReductionDb_.load(std::memory_order_relaxed); }
291
292
294 [[nodiscard]] std::vector<uint8_t> getState() const
295 {
296 StateWriter w(stateId("CLIP"), 1);
297 w.write("mode", static_cast<int32_t>(mode_.load(std::memory_order_relaxed)));
298 w.write("ceiling", static_cast<float>(ceilingDb_.load(std::memory_order_relaxed)));
299 w.write("inputGain", static_cast<float>(inputGainDb_.load(std::memory_order_relaxed)));
300 w.write("stages", stages_.load(std::memory_order_relaxed));
301 w.write("mix", static_cast<float>(mix_.load(std::memory_order_relaxed)));
302 w.write("slewLimit", static_cast<float>(slewLimitMs_.load(std::memory_order_relaxed)));
303 w.write("oversampling", osFactor_.load(std::memory_order_relaxed));
304 return w.blob();
305 }
306
309 bool setState(const uint8_t* data, size_t size)
310 {
311 StateReader r(data, size);
312 if (!r.isValid() || r.processorId() != stateId("CLIP")) return false;
313 setMode(static_cast<Mode>(r.read("mode", 0)));
314 setCeiling(static_cast<T>(r.read("ceiling", 0.0f)));
315 setInputGain(static_cast<T>(r.read("inputGain", 0.0f)));
316 setStages(r.read("stages", 1));
317 setMix(static_cast<T>(r.read("mix", 1.0f)));
318 setSlewLimit(static_cast<T>(r.read("slewLimit", 0.0f)));
319 setOversampling(r.read("oversampling", 1));
320 return true;
321 }
322
323protected:
324 static constexpr int kMaxStages = 4;
325 static constexpr int kMaxChannels = 16;
326
328 static constexpr T kPhi = static_cast<T>(1.6180339887498948482);
329
333 void processInternal(AudioBufferView<T>& buffer, double currentSampleRate) noexcept
334 {
335 Mode modeVal = mode_.load(std::memory_order_relaxed);
336 T ceilDb = ceilingDb_.load(std::memory_order_relaxed);
337 T gainDb = inputGainDb_.load(std::memory_order_relaxed);
338 int numStages = stages_.load(std::memory_order_relaxed);
339 T slewMs = slewLimitMs_.load(std::memory_order_relaxed);
340
341 ceilingSm_.setTargetValue(dbToLinear(ceilDb));
342 stageGainSm_.setTargetValue(stageGainFor(dbToLinear(gainDb), numStages));
343
344 // Maximum slew per sample is ceiling * slewScale (time-aware).
345 const T slewScale = (slewMs > T(0))
346 ? T(1) / (slewMs * T(0.001)) / static_cast<T>(currentSampleRate)
347 : T(0);
348
349 // The mode switch is resolved once per block, outside the sample loop.
350 switch (modeVal)
351 {
352 case Mode::Hard: dispatchClipping<Mode::Hard>(buffer, numStages, slewScale); break;
353 case Mode::Soft: dispatchClipping<Mode::Soft>(buffer, numStages, slewScale); break;
354 case Mode::Analog: dispatchClipping<Mode::Analog>(buffer, numStages, slewScale); break;
355 case Mode::GoldenRatio: dispatchClipping<Mode::GoldenRatio>(buffer, numStages, slewScale); break;
356 default: dispatchClipping<Mode::Hard>(buffer, numStages, slewScale); break;
357 }
358 }
359
361 [[nodiscard]] static T stageGainFor(T totalGainLin, int numStages) noexcept
362 {
363 return (numStages > 1) ? std::pow(totalGainLin, T(1) / static_cast<T>(numStages))
364 : totalGainLin;
365 }
366
368 void snapParameters() noexcept
369 {
370 ceilingSm_.reset(dbToLinear(ceilingDb_.load(std::memory_order_relaxed)));
371 stageGainSm_.reset(stageGainFor(dbToLinear(inputGainDb_.load(std::memory_order_relaxed)),
372 stages_.load(std::memory_order_relaxed)));
373 }
374
379 template <Mode M>
380 void dispatchClipping(AudioBufferView<T>& buffer, int numStages, T slewScale) noexcept
381 {
382 const int nCh = std::min(buffer.getNumChannels(), kMaxChannels);
383 const int nS = buffer.getNumSamples();
384
385 T peakIn = T(0);
386 T peakOut = T(0);
387
388 // Chunks: the parameter glides are computed once per sample and shared
389 // by every channel; settled, both are plain constants.
390 for (int start = 0; start < nS; start += kChunk)
391 {
392 const int n = std::min(kChunk, nS - start);
393 const bool gliding = stageGainSm_.isSmoothing() || ceilingSm_.isSmoothing();
394 if (gliding)
395 for (int j = 0; j < n; ++j)
396 {
397 gainRamp_[static_cast<size_t>(j)] = stageGainSm_.getNextValue();
398 ceilRamp_[static_cast<size_t>(j)] = ceilingSm_.getNextValue();
399 }
400 const T gainConst = stageGainSm_.getCurrentValue();
401 const T ceilConst = ceilingSm_.getCurrentValue();
402
403 for (int ch = 0; ch < nCh; ++ch)
404 {
405 T* data = buffer.getChannel(ch) + start;
406 for (int j = 0; j < n; ++j)
407 {
408 const T stageGain = gliding ? gainRamp_[static_cast<size_t>(j)] : gainConst;
409 const T ceiling = gliding ? ceilRamp_[static_cast<size_t>(j)] : ceilConst;
410 T sample = data[j];
411 T totalGain = stageGain;
412 for (int s = 1; s < numStages; ++s) totalGain *= stageGain;
413 const T driven = sample * totalGain;
414
415 // Compiler will unroll this loop for small bounded N
416 for (int s = 0; s < numStages; ++s)
417 {
418 sample *= stageGain;
419 sample = processSample<M>(sample, ceiling);
420 }
421
422 // Slew Limiter processing (branch highly predictable if static)
423 const T maxSlewDelta = ceiling * slewScale;
424 if (maxSlewDelta > T(0))
425 {
426 T delta = sample - slewPrev_[ch];
427 if (std::abs(delta) > maxSlewDelta)
428 sample = slewPrev_[ch] + std::copysign(maxSlewDelta, delta);
429 }
430
431 slewPrev_[ch] = sample;
432
433 peakIn = std::max(peakIn, std::abs(driven));
434 peakOut = std::max(peakOut, std::abs(sample));
435 data[j] = sample;
436 }
437 }
438 }
439
440 // Safe gain reduction calculation
441 if (peakIn > T(1e-6)) {
442 auto ratio = std::min(peakOut / peakIn, T(1));
443 gainReductionDb_.store(gainToDecibels(ratio, T(-100)), std::memory_order_relaxed);
444 } else {
445 gainReductionDb_.store(T(0), std::memory_order_relaxed);
446 }
447 }
448
452 template <Mode M>
453 [[nodiscard]] static inline T processSample(T sample, T ceiling) noexcept
454 {
455 if constexpr (M == Mode::Hard)
456 return detail::clipperShape<detail::ClipperCurve::Hard>(sample, ceiling);
457 else if constexpr (M == Mode::Soft)
458 return detail::clipperShape<detail::ClipperCurve::Tanh>(sample, ceiling);
459 else if constexpr (M == Mode::Analog)
460 return detail::clipperShape<detail::ClipperCurve::Sine>(sample, ceiling);
461 else // Mode::GoldenRatio (the mode switch instantiates no other value)
462 return detail::clipperShape<detail::ClipperCurve::GoldenRatio>(sample, ceiling);
463 }
464
465 // Mathematical utility helpers
466 [[nodiscard]] static T dbToLinear(T dB) noexcept { return std::pow(T(10), dB / T(20)); }
467 [[nodiscard]] static T gainToDecibels(T linear, T minusInfinityDb) noexcept
468 {
469 return linear > T(1e-5) ? T(20) * std::log10(linear) : minusInfinityDb;
470 }
471
473 std::atomic<bool> prepared_ { false };
475 std::unique_ptr<Oversampling<T>> oversampler_;
476
477 // Lock-free parameter states
478 std::atomic<Mode> mode_ { Mode::Hard };
479 std::atomic<T> ceilingDb_ { T(0) };
480 std::atomic<T> inputGainDb_ { T(0) };
481 std::atomic<int> stages_ { 1 };
482 std::atomic<T> mix_ { T(1) };
483 std::atomic<T> slewLimitMs_ { T(0) };
484 std::atomic<int> osFactor_ { 1 }; // Default to 1 (off)
485
486 // History states per channel
488
489 // Parameter glides (processing rate) and their per-chunk ramps.
490 static constexpr double kParamRampMs = 20.0;
491 static constexpr int kChunk = 256;
494 std::array<T, kChunk> gainRamp_ {};
495 std::array<T, kChunk> ceilRamp_ {};
496
497 // Metering state
498 std::atomic<T> gainReductionDb_ { T(0) };
499};
500
501} // 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:73
std::unique_ptr< Oversampling< T > > oversampler_
Definition Clipper.h:475
SmoothedValue< T > stageGainSm_
Definition Clipper.h:492
void snapParameters() noexcept
Definition Clipper.h:368
std::atomic< T > inputGainDb_
Definition Clipper.h:480
std::atomic< int > osFactor_
Definition Clipper.h:484
AudioSpec spec_
Definition Clipper.h:472
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:309
std::atomic< T > gainReductionDb_
Definition Clipper.h:498
T slewPrev_[kMaxChannels]
Definition Clipper.h:487
std::atomic< T > ceilingDb_
Definition Clipper.h:479
static T processSample(T sample, T ceiling) noexcept
Compile-time resolution of the waveshaping math.
Definition Clipper.h:453
void setSlewLimit(T ms) noexcept
Enables slew limiting to soften clipping edges.
Definition Clipper.h:256
void setInputGain(T dB) noexcept
Sets the input drive/gain before clipping.
Definition Clipper.h:213
void processInternal(AudioBufferView< T > &buffer, double currentSampleRate) noexcept
Core DSP routing. Resolves atomics and branches to the per-mode template.
Definition Clipper.h:333
int getLatency() const noexcept
Returns latency in samples introduced by oversampling filters.
Definition Clipper.h:287
static constexpr T kPhi
Mathematical Golden Ratio used for the GoldenRatio soft-knee transition.
Definition Clipper.h:328
static constexpr int kMaxStages
Definition Clipper.h:324
void prepare(const AudioSpec &spec)
Prepares the clipper for processing, allocating any necessary internal buffers.
Definition Clipper.h:99
Mode
Defines the harmonic waveshaping algorithm used for clipping.
Definition Clipper.h:77
@ 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:162
void setStages(int count) noexcept
Sets the number of cascaded clipping stages.
Definition Clipper.h:228
std::array< T, kChunk > gainRamp_
Definition Clipper.h:494
void setMode(Mode mode) noexcept
Sets the clipping algorithm.
Definition Clipper.h:190
T getCeiling() const noexcept
Definition Clipper.h:277
void dispatchClipping(AudioBufferView< T > &buffer, int numStages, T slewScale) noexcept
Per-mode processing loop (waveshaper inlined at compile time; the slew state and peak metering keep t...
Definition Clipper.h:380
T getMix() const noexcept
Definition Clipper.h:280
std::atomic< T > mix_
Definition Clipper.h:482
SmoothedValue< T > ceilingSm_
Definition Clipper.h:493
static constexpr double kParamRampMs
Definition Clipper.h:490
void setMix(T amount) noexcept
Sets the dry/wet ratio of the processor.
Definition Clipper.h:238
void reset() noexcept
Resets the internal state of the clipper.
Definition Clipper.h:142
void setOversampling(int factor) noexcept
Sets the oversampling multiplier to mitigate aliasing.
Definition Clipper.h:270
T getGainReductionDb() const noexcept
Retrieves the maximum gain reduction applied during the last block (for UI metering).
Definition Clipper.h:290
std::array< T, kChunk > ceilRamp_
Definition Clipper.h:495
static constexpr int kChunk
Definition Clipper.h:491
DryWetMixer< T > mixer_
Definition Clipper.h:474
Mode getMode() const noexcept
Definition Clipper.h:276
static T stageGainFor(T totalGainLin, int numStages) noexcept
Definition Clipper.h:361
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Clipper.h:294
static T gainToDecibels(T linear, T minusInfinityDb) noexcept
Definition Clipper.h:467
static constexpr int kMaxChannels
Definition Clipper.h:325
int getStages() const noexcept
Definition Clipper.h:279
void setCeiling(T dB) noexcept
Sets the absolute maximum output level.
Definition Clipper.h:202
std::atomic< bool > prepared_
Definition Clipper.h:473
T getInputGain() const noexcept
Definition Clipper.h:278
T getSlewLimit() const noexcept
Definition Clipper.h:281
int getOversampling() const noexcept
Returns the published oversampling factor (applied on the next prepare()).
Definition Clipper.h:284
std::atomic< Mode > mode_
Definition Clipper.h:478
static T dbToLinear(T dB) noexcept
Definition Clipper.h:466
std::atomic< T > slewLimitMs_
Definition Clipper.h:483
std::atomic< int > stages_
Definition Clipper.h:481
Pre-allocated, SIMD-friendly dry/wet blender for real-time audio.
Definition DryWetMixer.h:78
Power-of-two oversampling processor with polyphase anti-aliasing.
Zero-allocation parameter smoother for real-time audio.
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:161
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:204
bool isValid() const noexcept
Definition StateBlob.h:199
uint32_t processorId() const noexcept
Definition StateBlob.h:200
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:53
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:105
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:71
Main namespace for the DSPark framework.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:651
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45