Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
compressor.hpp
Go to the documentation of this file.
1#pragma once
2
8
9#include <algorithm>
10#include <cmath>
11#include <cstdint>
12#include <limits>
13#include <type_traits>
14
15namespace pulp::signal {
16
42enum class CompressorBallistics : std::uint8_t { branching, decoupled };
43
44// Feed-forward compressor with adjustable attack/release.
45//
46// RT contract: parameter setters and sample/buffer process paths allocate no
47// memory except `set_sample_rate()` / `set_lookahead_ms()` may allocate or
48// resize the lookahead delay line. Configure lookahead off the audio thread;
49// after that, process paths, `latency_samples()`, `gain_reduction_db()`, and
50// `reset()` allocate no memory.
51//
52// Sidechain HPF (item 2.4 of macOS plan): when `set_sidechain_hpf_hz`
53// is non-zero, the level-detection path runs the sidechain signal
54// through a high-pass biquad before envelope-following. This makes
55// the compressor less reactive to bass content — useful for
56// preventing pumping on a bass-heavy mix or for sidechain-from-kick
57// patterns where the kick's fundamental shouldn't trigger
58// compression on top of the snare.
59//
60// Lookahead (item 2.4): when `set_lookahead_ms` is non-zero, the
61// dry signal is delayed by that many ms so the envelope follower
62// "sees the future" — peak transients are caught before they hit
63// the output. Round-trip latency increases by the lookahead amount;
64// callers should report it via Processor::latency_samples().
65template <typename SampleType = float>
67public:
68 struct Params {
69 SampleType threshold_db = SampleType{-20.0f}; // Compression threshold
70 SampleType ratio = SampleType{4.0f}; // Compression ratio (4:1)
71 // Attack and release time constants (tau) in ms: a steady-level step
72 // moves 10% -> 90% in about 2.2x these (see CompressorBallistics).
73 SampleType attack_ms = SampleType{5.0f};
74 SampleType release_ms = SampleType{100.0f};
75 SampleType knee_db = SampleType{6.0f}; // Soft knee width in dB (0 = hard knee)
76 SampleType makeup_db = SampleType{0.0f}; // Makeup gain
77 };
78
79 void set_params(const Params& p) {
80 params_ = p;
81 update_ballistics();
82 }
83
87 // Selecting the current form again is a no-op, so a host may call
88 // this every block without disturbing the decoupled peak hold.
89 if (ballistics == ballistics_)
90 return;
91 ballistics_ = ballistics;
92 hold_db_ = envelope_db_;
93 }
95 return ballistics_;
96 }
97 void set_sample_rate(SampleType sr) {
98 sample_rate_ = sr;
99 update_ballistics();
100 configure_sidechain_filter();
101 configure_lookahead_buffer();
102 }
103
108 void set_sidechain_hpf_hz(SampleType hz) {
109 sidechain_hpf_hz_ = std::max(SampleType{0.0f}, hz);
110 configure_sidechain_filter();
111 }
112 SampleType sidechain_hpf_hz() const { return sidechain_hpf_hz_; }
113
119 void set_lookahead_ms(SampleType ms) {
120 lookahead_ms_ = std::clamp(ms, SampleType{0.0f}, SampleType{50.0f});
121 configure_lookahead_buffer();
122 }
123 SampleType lookahead_ms() const { return lookahead_ms_; }
124
126 int latency_samples() const {
127 return static_cast<int>(
128 std::round(lookahead_ms_ * SampleType{0.001f} * sample_rate_));
129 }
130
133 SampleType process(SampleType input) {
134 return process_with_sidechain(input, input);
135 }
136
140 SampleType process_with_sidechain(SampleType input, SampleType sidechain) {
141 // A non-finite program or detector sample is not meaningful audio. It
142 // must not enter the HPF, delay line, or recursive envelope because one
143 // poisoned state sample would otherwise make later finite audio and
144 // telemetry non-finite. Recover owned signal history, retain all user
145 // configuration, and emit finite silence for the rejected sample.
146 if (!std::isfinite(input) || !std::isfinite(sidechain)) {
147 reset();
148 return SampleType{0};
149 }
150
151 // Apply sidechain HPF if configured.
152 const SampleType detector = sidechain_hpf_active_
153 ? sidechain_hpf_.process(sidechain)
154 : sidechain;
155
156 const SampleType gain_db = gain_computer_db(detector_level_db(detector), params_);
157
158 // Snap the recursive gain-reduction state: when the signal sits above
159 // threshold it settles toward 0 dB and otherwise stalls in denormals
160 // with no FTZ guard. No-op above 1e-15.
161 if (ballistics_ == CompressorBallistics::decoupled) {
162 hold_db_ = snap_to_zero(decoupled_hold(hold_db_, gain_db, release_coeff_));
163 envelope_db_ = snap_to_zero(envelope_db_ + attack_coeff_ * (hold_db_ - envelope_db_));
164 } else {
165 envelope_db_ =
166 snap_to_zero(branching_step(envelope_db_, gain_db, attack_coeff_, release_coeff_));
167 }
168
169 const SampleType gain_linear = db_to_gain(envelope_db_ + params_.makeup_db);
170
171 // Lookahead: push input into the delay, read out the
172 // sample-from-the-past, apply the gain that the envelope
173 // followed off the un-delayed sidechain.
174 if (lookahead_samples_ > 0) {
175 lookahead_buffer_.push(input);
176 const SampleType delayed =
177 lookahead_buffer_.read(static_cast<SampleType>(lookahead_samples_));
178 return delayed * gain_linear;
179 }
180 return input * gain_linear;
181 }
182
185 void process(SampleType* buffer, int num_samples) {
186 run_chunks(buffer, buffer, num_samples);
187 }
188
198 void process_with_sidechain(SampleType* buffer, const SampleType* sidechain,
199 int num_samples) {
200 run_chunks(buffer, sidechain, num_samples);
201 }
202
206 SampleType attack_coefficient() const {
207 return attack_coeff_;
208 }
209 SampleType release_coefficient() const {
210 return release_coeff_;
211 }
212
213 SampleType gain_reduction_db() const { return envelope_db_; }
214
216 GainReduction gain_reduction() const noexcept {
217 return GainReduction::from_signed_db(static_cast<double>(envelope_db_));
218 }
219
220 void reset() {
221 envelope_db_ = SampleType{0.0f};
222 hold_db_ = SampleType{0.0f};
223 sidechain_hpf_.reset();
224 lookahead_buffer_.reset();
225 }
226
227 // ── The pipeline as pure functions ──────────────────────────────────
228 // Each stage process() runs, exposed so a widget or a test can draw or
229 // verify the gain computer and the ballistics without running audio.
230 // process() calls exactly these.
231
234 static SampleType detector_level_db(SampleType detector) {
235 const SampleType magnitude = std::max(std::abs(detector), SampleType{1e-10f});
236 if constexpr (std::is_same_v<SampleType, float>)
237 return FastMath::gain_to_db_accurate(magnitude);
238 else
239 return SampleType{20.0f} * std::log10(magnitude);
240 }
241
246 static SampleType gain_computer_db(SampleType input_db, const Params& params) {
247 const SampleType knee = params.knee_db;
248 const SampleType threshold = params.threshold_db;
249 const SampleType slope = SampleType{1.0f} / params.ratio - SampleType{1.0f};
250 const SampleType above = (input_db - threshold) * slope;
251
252 if (knee <= SampleType{0.0f}) {
253 // Hard knee
254 return input_db <= threshold ? SampleType{0.0f} : above;
255 }
256
257 // Soft knee
258 const SampleType half_knee = knee / SampleType{2.0f};
259 const SampleType x = input_db - threshold + half_knee;
260 const SampleType in_knee = slope * x * x / (SampleType{2.0f} * knee);
261 const SampleType upper = input_db > threshold + half_knee ? above : in_knee;
262 return input_db < threshold - half_knee ? SampleType{0.0f} : upper;
263 }
264
267 static SampleType ballistic_coefficient(SampleType time_ms, SampleType sample_rate) {
268 if (time_ms <= SampleType{0.0f})
269 return SampleType{1.0f};
270 return SampleType{1.0f} -
271 std::exp(SampleType{-1.0f} / (time_ms * SampleType{0.001f} * sample_rate));
272 }
273
277 static SampleType branching_step(SampleType envelope, SampleType target, SampleType attack,
278 SampleType release) {
279 const SampleType coeff = (target < envelope) ? attack : release;
280 return envelope + coeff * (target - envelope);
281 }
282
288 static SampleType decoupled_hold(SampleType hold, SampleType target, SampleType release) {
289 const SampleType released = hold + release * (target - hold);
290 return target < released ? target : released;
291 }
292
294 static SampleType db_to_gain(SampleType db) {
295 if constexpr (std::is_same_v<SampleType, float>)
297 else
298 return std::pow(SampleType{10.0f}, db / SampleType{20.0f});
299 }
300
301private:
302 Params params_;
303 SampleType sample_rate_ = SampleType{44100.0f};
304 SampleType envelope_db_ = SampleType{0.0f};
305 // Peak hold of the decoupled detector (gain change in dB, <= 0).
306 SampleType hold_db_ = SampleType{0.0f};
308
309 SampleType attack_coeff_ = ballistic_coefficient(params_.attack_ms, sample_rate_);
310 SampleType release_coeff_ = ballistic_coefficient(params_.release_ms, sample_rate_);
311
312 SampleType sidechain_hpf_hz_ = SampleType{0.0f};
313 bool sidechain_hpf_active_ = false;
314 BiquadT<SampleType> sidechain_hpf_{};
315
316 SampleType lookahead_ms_ = SampleType{0.0f};
317 int lookahead_samples_ = 0;
318 DelayLineT<SampleType> lookahead_buffer_{};
319
320 void configure_sidechain_filter() {
321 sidechain_hpf_active_ =
322 sidechain_hpf_hz_ > SampleType{0.0f} && sample_rate_ > SampleType{0.0f};
323 if (sidechain_hpf_active_) {
324 sidechain_hpf_.set_coefficients(
326 sidechain_hpf_hz_, /*Q=*/SampleType{0.7071068f}, sample_rate_);
327 sidechain_hpf_.reset();
328 }
329 }
330 void configure_lookahead_buffer() {
331 lookahead_samples_ = static_cast<int>(
332 std::round(lookahead_ms_ * SampleType{0.001f} * sample_rate_));
333 if (lookahead_samples_ > 0) {
334 // DelayLine wants a buffer length >= max delay + 1.
335 lookahead_buffer_.prepare(lookahead_samples_ + 1);
336 lookahead_buffer_.reset();
337 }
338 }
339
340 void update_ballistics() {
341 attack_coeff_ = ballistic_coefficient(params_.attack_ms, sample_rate_);
342 release_coeff_ = ballistic_coefficient(params_.release_ms, sample_rate_);
343 }
344
345 // The block path behind process(buffer) and process_with_sidechain(buffer,
346 // sidechain). Each kSnapChunkSamples chunk runs the detector and static
347 // curve over the whole chunk, then the envelope recursion, then the gain
348 // stage, so the transcendental calls in the outer passes are independent
349 // of one another and of the recursion.
350 void run_chunks(SampleType* buffer, const SampleType* sidechain, int num_samples) {
351 if (num_samples <= 0)
352 return;
353 SampleType level[kSnapChunkSamples];
354 for (const auto [begin, end] : snap_chunks(static_cast<std::size_t>(num_samples))) {
355 const int n = static_cast<int>(end - begin);
356 SampleType* x = buffer + begin;
357 const SampleType* sc = sidechain + begin;
358
359 // A non-finite sample resets signal history at that exact sample,
360 // which the per-sample path already implements. The scan is a
361 // branch-free OR so it vectorizes; NaN fails the comparison too.
362 constexpr SampleType kMaxFinite = std::numeric_limits<SampleType>::max();
363 unsigned non_finite = 0;
364 for (int i = 0; i < n; ++i)
365 non_finite |= static_cast<unsigned>(!(std::abs(x[i]) <= kMaxFinite)) |
366 static_cast<unsigned>(!(std::abs(sc[i]) <= kMaxFinite));
367 if (non_finite != 0) {
368 for (int i = 0; i < n; ++i)
369 x[i] = process_with_sidechain(x[i], sc[i]);
370 continue;
371 }
372
373 if (sidechain_hpf_active_) {
374 for (int i = 0; i < n; ++i)
375 level[i] = sidechain_hpf_.process(sc[i]);
376 for (int i = 0; i < n; ++i)
377 level[i] = gain_computer_db(detector_level_db(level[i]), params_);
378 } else {
379 for (int i = 0; i < n; ++i)
380 level[i] = gain_computer_db(detector_level_db(sc[i]), params_);
381 }
382
383 // The envelope is snapped once per chunk: it only alters a level
384 // below 1e-15 dB. With releases longer than about 0.05 ms the
385 // release pole is slow enough that a chunk-start value above that
386 // cannot reach the subnormal range before the next snap; with
387 // shorter ones the envelope decays through it within the chunk,
388 // so subnormal residency is still bounded by one chunk.
389 const SampleType attack = attack_coeff_;
390 const SampleType release = release_coeff_;
391 SampleType envelope = envelope_db_;
392 if (ballistics_ == CompressorBallistics::decoupled) {
393 SampleType hold = hold_db_;
394 for (int i = 0; i < n; ++i) {
395 hold = decoupled_hold(hold, level[i], release);
397 level[i] = envelope;
398 }
399 hold_db_ = snap_to_zero(hold);
400 } else {
401 for (int i = 0; i < n; ++i) {
402 envelope = branching_step(envelope, level[i], attack, release);
403 level[i] = envelope;
404 }
405 }
406 envelope_db_ = snap_to_zero(envelope);
407
408 const SampleType makeup = params_.makeup_db;
409 if (lookahead_samples_ > 0) {
410 const SampleType delay = static_cast<SampleType>(lookahead_samples_);
411 for (int i = 0; i < n; ++i)
412 level[i] = db_to_gain(level[i] + makeup);
413 for (int i = 0; i < n; ++i) {
414 lookahead_buffer_.push(x[i]);
415 x[i] = lookahead_buffer_.read(delay) * level[i];
416 }
417 } else {
418 for (int i = 0; i < n; ++i)
419 x[i] = x[i] * db_to_gain(level[i] + makeup);
420 }
421 }
422 }
423};
424
427
428// Brickwall limiter — hard limit at threshold with lookahead-free design.
429// RT contract: setters, process paths, and reset are scalar-only and allocate
430// no memory.
431template <typename SampleType = float>
432class LimiterT {
433public:
434 void set_threshold_db(SampleType db) {
435 threshold_ = std::pow(SampleType{10.0f}, db / SampleType{20.0f});
436 threshold_mute_ = std::isinf(db) && db < SampleType{0};
437 }
438 void set_release_ms(SampleType ms) { release_ms_ = ms; }
439 void set_sample_rate(SampleType sr) { sample_rate_ = sr; }
440
441 SampleType process(SampleType input) {
442 // Reject before touching the recursive peak envelope. reset() clears
443 // only signal history; threshold, release, and sample rate remain as
444 // configured. The rejected sample is represented by finite silence.
445 if (!std::isfinite(input)) {
446 reset();
447 return SampleType{0};
448 }
449
450 SampleType abs_input = std::abs(input);
451
452 // Envelope follower
453 if (abs_input > envelope_)
454 envelope_ = abs_input; // Instant attack
455 else {
456 SampleType coeff = SampleType{1.0f} -
457 std::exp(SampleType{-1.0f} /
458 (release_ms_ * SampleType{0.001f} * sample_rate_));
459 // Snap the release envelope so a decay into silence flushes to zero
460 // instead of stalling in denormals with no FTZ guard. No-op above
461 // 1e-15.
462 envelope_ = snap_to_zero(envelope_ + coeff * (abs_input - envelope_));
463 }
464
465 // Compute gain
466 SampleType gain =
467 (envelope_ > threshold_) ? threshold_ / envelope_ : SampleType{1.0f};
468 return input * gain;
469 }
470
471 void process(SampleType* buffer, int num_samples) {
472 for (int i = 0; i < num_samples; ++i)
473 buffer[i] = process(buffer[i]);
474 }
475
476 GainReduction gain_reduction() const noexcept {
477 // A zero threshold is the documented -infinity-dB mute setting. Its
478 // telemetry remains a mute even while the recovered envelope is zero;
479 // reporting unity here would falsely describe the configured result.
480 if (threshold_mute_)
482 std::numeric_limits<double>::infinity());
483 const SampleType gain =
484 (envelope_ > threshold_) ? threshold_ / envelope_ : SampleType{1};
485 if (!(gain > SampleType{0}))
487 std::numeric_limits<double>::infinity());
489 SampleType{20} * std::log10(gain));
490 }
491
492 void reset() { envelope_ = SampleType{0.0f}; }
493
494private:
495 SampleType threshold_ = SampleType{1.0f};
496 SampleType release_ms_ = SampleType{50.0f};
497 SampleType sample_rate_ = SampleType{44100.0f};
498 SampleType envelope_ = SampleType{0.0f};
499 bool threshold_mute_ = false;
500};
501
504
505} // namespace pulp::signal
@ highpass
Definition biquad.hpp:58
Definition compressor.hpp:66
SampleType process_with_sidechain(SampleType input, SampleType sidechain)
Process one sample with an explicit sidechain detector input.
Definition compressor.hpp:140
int latency_samples() const
Latency reported to the host = lookahead in samples (0 when off).
Definition compressor.hpp:126
void process_with_sidechain(SampleType *buffer, const SampleType *sidechain, int num_samples)
Process a block with a separate sidechain input.
Definition compressor.hpp:198
SampleType release_coefficient() const
Definition compressor.hpp:209
SampleType gain_reduction_db() const
Definition compressor.hpp:213
static SampleType db_to_gain(SampleType db)
Linear gain for a gain change in dB (FastMath for float).
Definition compressor.hpp:294
static SampleType gain_computer_db(SampleType input_db, const Params &params)
The static curve: gain change in dB (<= 0) for a detector level in dB under params (threshold,...
Definition compressor.hpp:246
static SampleType branching_step(SampleType envelope, SampleType target, SampleType attack, SampleType release)
One step of the branching ballistics: the envelope (gain change in dB) moves toward target with the a...
Definition compressor.hpp:277
void process(SampleType *buffer, int num_samples)
Process a block in place; the buffer is also the sidechain.
Definition compressor.hpp:185
SampleType process(SampleType input)
Process one sample.
Definition compressor.hpp:133
SampleType attack_coefficient() const
Envelope smoothing coefficients in force for the current parameters and sample rate: the per-sample s...
Definition compressor.hpp:206
SampleType lookahead_ms() const
Definition compressor.hpp:123
GainReduction gain_reduction() const noexcept
Canonical non-negative gain-reduction magnitude for shared meters.
Definition compressor.hpp:216
void set_sidechain_hpf_hz(SampleType hz)
Set the sidechain high-pass cutoff (Hz).
Definition compressor.hpp:108
void reset()
Definition compressor.hpp:220
void set_sample_rate(SampleType sr)
Definition compressor.hpp:97
static SampleType decoupled_hold(SampleType hold, SampleType target, SampleType release)
The decoupled ballistics' peak hold on gain changes (<= 0 dB): it jumps to any deeper reduction at on...
Definition compressor.hpp:288
void set_ballistics(CompressorBallistics ballistics) noexcept
Select how gain reduction is smoothed (see CompressorBallistics).
Definition compressor.hpp:86
static SampleType detector_level_db(SampleType detector)
Detector level in dB for one sample: 20 log10(|x|), floored at -200 dB.
Definition compressor.hpp:234
SampleType sidechain_hpf_hz() const
Definition compressor.hpp:112
CompressorBallistics ballistics() const noexcept
Definition compressor.hpp:94
void set_lookahead_ms(SampleType ms)
Set the lookahead in milliseconds.
Definition compressor.hpp:119
static SampleType ballistic_coefficient(SampleType time_ms, SampleType sample_rate)
One-pole smoothing coefficient for a time constant in ms: 1 - exp(-1 / (tau fs)), or 1 (instant) for ...
Definition compressor.hpp:267
void set_params(const Params &p)
Definition compressor.hpp:79
Canonical gain-reduction meter value.
Definition dynamics_contract.hpp:27
static GainReduction from_signed_db(double signed_db) noexcept
Adapt a signed gain value, where attenuation is negative dB.
Definition dynamics_contract.hpp:32
static GainReduction from_magnitude_db(double magnitude_db) noexcept
Adapt an attenuation magnitude, where reduction is positive dB.
Definition dynamics_contract.hpp:37
Definition compressor.hpp:432
void set_release_ms(SampleType ms)
Definition compressor.hpp:438
void set_threshold_db(SampleType db)
Definition compressor.hpp:434
void reset()
Definition compressor.hpp:492
GainReduction gain_reduction() const noexcept
Definition compressor.hpp:476
void set_sample_rate(SampleType sr)
Definition compressor.hpp:439
void process(SampleType *buffer, int num_samples)
Definition compressor.hpp:471
SampleType process(SampleType input)
Definition compressor.hpp:441
Shared envelope and gain-reduction vocabulary for dynamics processors.
Scalar math helpers for real-time audio DSP.
@ envelope
Definition plugin_state_io.hpp:26
@ end
Definition editor_host.hpp:354
Definition additive_bank.hpp:218
CompressorT< double > Compressor64
Definition compressor.hpp:426
@ buffer
A borrowed static buffer with a traversable playhead.
Definition granular.hpp:110
@ release
Definition envelope.hpp:62
@ delay
Definition envelope.hpp:57
@ hold
Definition envelope.hpp:59
@ attack
Definition envelope.hpp:58
constexpr std::size_t kSnapChunkSamples
Samples a block path runs between denormal snaps of its recursive state.
Definition denormal.hpp:113
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ i
Definition formant_filter_bank.hpp:20
LimiterT< float > Limiter
Definition compressor.hpp:502
constexpr SnapChunks snap_chunks(std::size_t num_samples) noexcept
Definition denormal.hpp:173
CompressorT< float > Compressor
Definition compressor.hpp:425
LimiterT< double > Limiter64
Definition compressor.hpp:503
CompressorBallistics
How CompressorT smooths the gain reduction its static curve asks for, in the log (dB) domain after th...
Definition compressor.hpp:42
@ branching
Definition compressor.hpp:42
@ decoupled
Definition compressor.hpp:42
@ non_finite
Definition formant_filter_bank.hpp:26
@ n
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
Definition compressor.hpp:68
SampleType ratio
Definition compressor.hpp:70
SampleType threshold_db
Definition compressor.hpp:69
SampleType attack_ms
Definition compressor.hpp:73
SampleType release_ms
Definition compressor.hpp:74
SampleType knee_db
Definition compressor.hpp:75
SampleType makeup_db
Definition compressor.hpp:76
static float db_to_gain_accurate(float db) noexcept
10^(dB / 20), within exp10_accurate()'s bound.
Definition fast_math.hpp:555
static float gain_to_db_accurate(float gain) noexcept
20 log10(gain) for a finite, positive, normal gain (clamp to a floor first); within log10_accurate()'...
Definition fast_math.hpp:550