Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
feedforward_compressor.hpp
Go to the documentation of this file.
1#pragma once
2
103
107#include <pulp/signal/units.hpp>
108
109#include <algorithm>
110#include <cmath>
111#include <cstddef>
112#include <cstdint>
113#include <vector>
114
115namespace pulp::signal {
116
118enum class CompressorDetector : std::uint8_t {
121};
122
124template <typename SampleType = float>
126public:
128
129 // ── Design parameters (the complete roster) ───────────────────────────
130
134 static constexpr double kLevelEpsilon = dynamics::kLevelEpsilon;
135
141 static constexpr double kSlowReleaseRatio = 4.0;
142
145 static constexpr double kSustainTauMs = 200.0;
146
149 static constexpr double kSustainThresholdDb = 3.0;
150
151 // Control ranges. These mirror the catalog node's parameter table; the
152 // table is the canonical declaration and these are the same numbers at the
153 // call site, not a second independent one.
154 static constexpr double kThresholdDbMin = -60.0;
155 static constexpr double kThresholdDbMax = 0.0;
156 static constexpr double kRatioMin = 1.0;
157 static constexpr double kRatioMax = 100.0;
158 static constexpr double kKneeDbMin = 0.0;
159 static constexpr double kKneeDbMax = 24.0;
160 static constexpr double kAttackMsMin = 0.05;
161 static constexpr double kAttackMsMax = 200.0;
162 static constexpr double kReleaseMsMin = 5.0;
163 static constexpr double kReleaseMsMax = 4000.0;
164 static constexpr double kRmsWindowMsMin = 1.0;
165 static constexpr double kRmsWindowMsMax = 50.0;
166 static constexpr double kMakeupDbMax = 24.0;
167
170 static constexpr double kMaxLookaheadMsCeiling = 50.0;
171
172 // ── Lifecycle ─────────────────────────────────────────────────────────
173
177 void prepare(double sample_rate, double max_lookahead_ms = 10.0) {
178 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
179 max_lookahead_ms_ = std::clamp(max_lookahead_ms, 0.0, kMaxLookaheadMsCeiling);
180
181 const auto capacity = static_cast<std::size_t>(
182 std::llround(units::ms_to_samples(max_lookahead_ms_, sample_rate_))) + 1;
183 for (auto& ring : lookahead_) ring.assign(capacity, SampleType{0});
184
185 update_coefficients();
186 update_lookahead();
187 update_makeup();
188 reset();
189 }
190
192 void reset() {
193 for (auto& ch : channels_) ch = ChannelState{};
194 for (auto& ring : lookahead_) std::fill(ring.begin(), ring.end(), SampleType{0});
195 write_index_[0] = 0;
196 write_index_[1] = 0;
197 lookahead_valid_[0] = 0;
198 lookahead_valid_[1] = 0;
199 }
200
201 // ── Controls (real units throughout) ──────────────────────────────────
202
203 void set_threshold_db(double db) {
204 threshold_db_ = std::clamp(dynamics::retain_finite(db, threshold_db_),
206 update_makeup();
207 }
208
209 void set_ratio(double r) {
210 ratio_ = std::clamp(dynamics::retain_finite(r, ratio_), kRatioMin, kRatioMax);
211 update_makeup();
212 }
213
214 void set_knee_width_db(double w) {
215 knee_db_ = std::clamp(dynamics::retain_finite(w, knee_db_),
217 update_makeup();
218 }
219
220 void set_attack_ms(double ms) {
221 attack_ms_ = std::clamp(dynamics::retain_finite(ms, attack_ms_),
223 update_coefficients();
224 }
225
226 void set_release_ms(double ms) {
227 release_ms_ = std::clamp(dynamics::retain_finite(ms, release_ms_),
229 update_coefficients();
230 }
231
232 void set_detector(Detector d) { detector_ = d; }
233 Detector detector() const { return detector_; }
234
235 void set_rms_window_ms(double ms) {
236 rms_window_ms_ = std::clamp(dynamics::retain_finite(ms, rms_window_ms_),
238 update_coefficients();
239 }
240
243 void set_lookahead_ms(double ms) {
244 lookahead_ms_ = std::clamp(dynamics::retain_finite(ms, lookahead_ms_),
245 0.0, max_lookahead_ms_);
246 update_lookahead();
247 }
248
249 void set_program_dependent_release(bool on) { program_dependent_ = on; }
250
251 void set_makeup_gain_db(double db) {
252 makeup_db_ = std::clamp(dynamics::retain_finite(db, makeup_db_),
254 update_makeup();
255 }
256
257 void set_auto_makeup(bool on) {
258 auto_makeup_ = on;
259 update_makeup();
260 }
261
264 void set_stereo_link(double amount) {
265 stereo_link_ = std::clamp(dynamics::retain_finite(amount, stereo_link_), 0.0, 1.0);
266 }
267
271 int latency_samples() const noexcept { return lookahead_samples_; }
272
276 double static_curve_db(double input_db) const {
277 return dynamics::soft_knee_output_db(input_db, threshold_db_, knee_db_, ratio_);
278 }
279
281 double gain_computer_db(double input_db) const {
282 return static_curve_db(input_db) - input_db;
283 }
284
287 double effective_makeup_db() const { return auto_makeup_ ? auto_makeup_db_ : makeup_db_; }
288
292 double gain_reduction_db(int channel = 0) const {
293 return channels_[static_cast<std::size_t>(channel & 1)].smoothed_reduction;
294 }
295
297 GainReduction gain_reduction(int channel = 0) const noexcept {
299 }
300
301 // ── Processing ────────────────────────────────────────────────────────
302
304 SampleType process(SampleType input) {
305 // A non-finite sample is not audio and must not become detector or
306 // lookahead history. Clear the owned recursive state so the very next
307 // finite sample is processed from a valid, deterministic state.
308 if (!std::isfinite(static_cast<double>(input))) {
309 recover_audio_fault();
310 return SampleType{0};
311 }
312 const double level_db = detect_level_db(0, static_cast<double>(input));
313 const double gain = channel_gain(0, level_db);
314 return static_cast<SampleType>(delayed(0, input) * gain * makeup_linear_);
315 }
316
319 void process_stereo(SampleType& left, SampleType& right) {
320 if (!std::isfinite(static_cast<double>(left)) ||
321 !std::isfinite(static_cast<double>(right))) {
322 recover_audio_fault();
323 left = SampleType{0};
324 right = SampleType{0};
325 return;
326 }
327 const double left_db = detect_level_db(0, static_cast<double>(left));
328 const double right_db = detect_level_db(1, static_cast<double>(right));
329
330 // The blend crosses the two DETECTOR INPUTS, before the detector, not
331 // two finished gain-reduction values after it.
332 const double shared_db = std::max(left_db, right_db);
333 const double left_used = left_db + stereo_link_ * (shared_db - left_db);
334 const double right_used = right_db + stereo_link_ * (shared_db - right_db);
335
336 const double left_gain = channel_gain(0, left_used);
337 const double right_gain = channel_gain(1, right_used);
338
339 const SampleType dry_left = delayed(0, left);
340 const SampleType dry_right = delayed(1, right);
341 left = static_cast<SampleType>(dry_left * left_gain * makeup_linear_);
342 right = static_cast<SampleType>(dry_right * right_gain * makeup_linear_);
343 }
344
345 void process_block(SampleType* io, int n) {
346 for (int i = 0; i < n; ++i) io[i] = process(io[i]);
347 }
348
349 void process_block_stereo(SampleType* left, SampleType* right, int n) {
350 for (int i = 0; i < n; ++i) process_stereo(left[i], right[i]);
351 }
352
353private:
354 void recover_audio_fault() noexcept {
355 for (auto& ch : channels_) ch = ChannelState{};
356 write_index_[0] = write_index_[1] = 0;
357 lookahead_valid_[0] = lookahead_valid_[1] = 0;
358 }
359
360 struct ChannelState {
361 double mean_square = 0.0;
362 double release_stage = 0.0;
363 double smoothed_reduction = 0.0;
364 double sustain = 0.0;
365 };
366
368 double detect_level_db(int channel, double x) {
369 auto& ch = channels_[static_cast<std::size_t>(channel)];
370 if (detector_ == Detector::peak) return dynamics::amplitude_db(x, kLevelEpsilon);
371
372 // `power_db` is `10·log10`, not 20, because the integrator already holds
373 // a squared quantity — see its note in `dynamics_core.hpp`.
374 ch.mean_square = snap_to_zero(rms_coef_ * ch.mean_square + (1.0 - rms_coef_) * x * x);
375 return dynamics::power_db(ch.mean_square, kLevelEpsilon);
376 }
377
379 double channel_gain(int channel, double level_db) {
380 auto& ch = channels_[static_cast<std::size_t>(channel)];
381
382 // Positive magnitude from here on; see the sign-convention note in the
383 // file doc block.
384 const double reduction = -gain_computer_db(level_db);
385
386 double release_coef = release_coef_;
387 if (program_dependent_) {
388 // The sustain detector is a one-pole over the indicator "is the
389 // CURRENT reduction deep enough to count", so it rises while
390 // compression is held and falls once it lets go.
391 const double sustained =
392 ch.smoothed_reduction > kSustainThresholdDb ? 1.0 : 0.0;
393 ch.sustain = sustain_coef_ * ch.sustain + (1.0 - sustain_coef_) * sustained;
394 release_coef = release_coef_ + ch.sustain * (slow_release_coef_ - release_coef_);
395 }
396
397 ch.release_stage = std::max(
398 reduction, release_coef * ch.release_stage + (1.0 - release_coef) * reduction);
399 ch.smoothed_reduction = snap_to_zero(
400 attack_coef_ * ch.smoothed_reduction + (1.0 - attack_coef_) * ch.release_stage);
401
402 return std::pow(10.0, -ch.smoothed_reduction / 20.0);
403 }
404
413 SampleType delayed(int channel, SampleType input) {
414 if (lookahead_samples_ <= 0) return input;
415 const auto index = static_cast<std::size_t>(channel);
416 auto& ring = lookahead_[index];
417 if (ring.empty()) return input;
418
419 const std::size_t size = ring.size();
420 std::size_t& write = write_index_[index];
421 const std::size_t read =
422 (write + size - static_cast<std::size_t>(lookahead_samples_)) % size;
423 const SampleType out = lookahead_valid_[index] >= static_cast<std::size_t>(lookahead_samples_)
424 ? ring[read]
425 : SampleType{0};
426 ring[write] = input;
427 write = (write + 1) % size;
428 if (lookahead_valid_[index] < size) ++lookahead_valid_[index];
429 return out;
430 }
431
432 void update_coefficients() {
433 attack_coef_ = one_pole(attack_ms_);
434 release_coef_ = one_pole(release_ms_);
435 slow_release_coef_ = one_pole(release_ms_ * kSlowReleaseRatio);
436 sustain_coef_ = one_pole(kSustainTauMs);
437 rms_coef_ = one_pole(rms_window_ms_);
438 }
439
442 double one_pole(double tau_ms) const {
443 return dynamics::one_pole_retain(tau_ms * 0.001, sample_rate_);
444 }
445
446 void update_lookahead() {
447 lookahead_samples_ = static_cast<int>(
448 std::llround(units::ms_to_samples(lookahead_ms_, sample_rate_)));
449 const auto capacity = lookahead_[0].size();
450 if (capacity > 0 && lookahead_samples_ >= static_cast<int>(capacity))
451 lookahead_samples_ = static_cast<int>(capacity) - 1;
452 if (lookahead_samples_ < 0) lookahead_samples_ = 0;
453 }
454
455 void update_makeup() {
456 // Exactly cancels the gain computer's own reduction at a 0 dBFS input —
457 // the "unity at the reference point" convention. Closed form, evaluated
458 // on a control change rather than per sample.
459 auto_makeup_db_ = -gain_computer_db(0.0);
460 makeup_linear_ = units::db_to_linear(effective_makeup_db());
461 }
462
463 double sample_rate_ = 44100.0;
464 double max_lookahead_ms_ = 10.0;
465
466 double threshold_db_ = -18.0;
467 double ratio_ = 4.0;
468 double knee_db_ = 6.0;
469 double attack_ms_ = 10.0;
470 double release_ms_ = 120.0;
471 double rms_window_ms_ = 10.0;
472 double lookahead_ms_ = 0.0;
473 double makeup_db_ = 0.0;
474 double stereo_link_ = 1.0;
475 Detector detector_ = Detector::peak;
476 bool program_dependent_ = true;
477 bool auto_makeup_ = true;
478
479 double attack_coef_ = 0.0;
480 double release_coef_ = 0.0;
481 double slow_release_coef_ = 0.0;
482 double sustain_coef_ = 0.0;
483 double rms_coef_ = 0.0;
484 double auto_makeup_db_ = 0.0;
485 double makeup_linear_ = 1.0;
486 int lookahead_samples_ = 0;
487
488 ChannelState channels_[2]{};
489 std::vector<SampleType> lookahead_[2]{};
490 std::size_t write_index_[2]{0, 0};
491 std::size_t lookahead_valid_[2]{0, 0};
492};
493
496
497} // namespace pulp::signal
Log-domain feedforward compressor with a soft-knee gain computer.
Definition feedforward_compressor.hpp:125
void set_detector(Detector d)
Definition feedforward_compressor.hpp:232
static constexpr double kRmsWindowMsMax
Definition feedforward_compressor.hpp:165
static constexpr double kRatioMin
Definition feedforward_compressor.hpp:156
static constexpr double kSustainTauMs
Definition feedforward_compressor.hpp:145
void set_makeup_gain_db(double db)
Definition feedforward_compressor.hpp:251
void set_lookahead_ms(double ms)
Lookahead in ms, clamped to the ceiling prepare() sized for — so this can never need an allocation.
Definition feedforward_compressor.hpp:243
void process_stereo(SampleType &left, SampleType &right)
One stereo pair, in place.
Definition feedforward_compressor.hpp:319
void set_auto_makeup(bool on)
Definition feedforward_compressor.hpp:257
static constexpr double kKneeDbMin
Definition feedforward_compressor.hpp:158
SampleType process(SampleType input)
One mono sample.
Definition feedforward_compressor.hpp:304
static constexpr double kAttackMsMin
Definition feedforward_compressor.hpp:160
static constexpr double kSustainThresholdDb
Definition feedforward_compressor.hpp:149
static constexpr double kRatioMax
Definition feedforward_compressor.hpp:157
static constexpr double kRmsWindowMsMin
Definition feedforward_compressor.hpp:164
void set_attack_ms(double ms)
Definition feedforward_compressor.hpp:220
static constexpr double kSlowReleaseRatio
Definition feedforward_compressor.hpp:141
void set_threshold_db(double db)
Definition feedforward_compressor.hpp:203
static constexpr double kAttackMsMax
Definition feedforward_compressor.hpp:161
void set_program_dependent_release(bool on)
Definition feedforward_compressor.hpp:249
double effective_makeup_db() const
Makeup gain currently in force, in dB — the auto value when auto-makeup is on, the manual value other...
Definition feedforward_compressor.hpp:287
void set_release_ms(double ms)
Definition feedforward_compressor.hpp:226
static constexpr double kMaxLookaheadMsCeiling
Definition feedforward_compressor.hpp:170
Detector detector() const
Definition feedforward_compressor.hpp:233
void set_ratio(double r)
Definition feedforward_compressor.hpp:209
static constexpr double kLevelEpsilon
Definition feedforward_compressor.hpp:134
void set_rms_window_ms(double ms)
Definition feedforward_compressor.hpp:235
CompressorDetector Detector
Definition feedforward_compressor.hpp:127
void reset()
Zeroes detector state and the lookahead buffer. Never allocates.
Definition feedforward_compressor.hpp:192
void prepare(double sample_rate, double max_lookahead_ms=10.0)
Sizes the lookahead ring buffer for the worst case.
Definition feedforward_compressor.hpp:177
static constexpr double kMakeupDbMax
Definition feedforward_compressor.hpp:166
GainReduction gain_reduction(int channel=0) const noexcept
Canonical non-negative gain-reduction magnitude for shared meters.
Definition feedforward_compressor.hpp:297
static constexpr double kKneeDbMax
Definition feedforward_compressor.hpp:159
static constexpr double kThresholdDbMin
Definition feedforward_compressor.hpp:154
void process_block_stereo(SampleType *left, SampleType *right, int n)
Definition feedforward_compressor.hpp:349
double gain_computer_db(double input_db) const
Gain reduction in dB (≤ 0) the static curve applies at input_db.
Definition feedforward_compressor.hpp:281
void set_stereo_link(double amount)
0 = fully independent per-channel detectors, 1 = one shared detector fed by the louder channel.
Definition feedforward_compressor.hpp:264
int latency_samples() const noexcept
The module's entire latency contribution: the lookahead delay, exactly.
Definition feedforward_compressor.hpp:271
static constexpr double kReleaseMsMin
Definition feedforward_compressor.hpp:162
void process_block(SampleType *io, int n)
Definition feedforward_compressor.hpp:345
double static_curve_db(double input_db) const
The static characteristic's output for an input level in dB — the gain computer alone,...
Definition feedforward_compressor.hpp:276
static constexpr double kReleaseMsMax
Definition feedforward_compressor.hpp:163
static constexpr double kThresholdDbMax
Definition feedforward_compressor.hpp:155
void set_knee_width_db(double w)
Definition feedforward_compressor.hpp:214
double gain_reduction_db(int channel=0) const
Smoothed gain reduction in dB (≥ 0, a magnitude) as of the last processed sample.
Definition feedforward_compressor.hpp:292
Canonical gain-reduction meter value.
Definition dynamics_contract.hpp:27
static GainReduction from_magnitude_db(double magnitude_db) noexcept
Adapt an attenuation magnitude, where reduction is positive dB.
Definition dynamics_contract.hpp:37
Shared envelope and gain-reduction vocabulary for dynamics processors.
The published compressor equations, once.
@ index
Definition gpu_pass.hpp:140
@ ring
Definition string.hpp:18
double soft_knee_output_db(double input_db, double threshold_db, double knee_db, double ratio)
The characteristic in the OUTPUT-LEVEL domain: input dB → output dB.
Definition dynamics_core.hpp:193
double one_pole_retain(double tau_seconds, double sample_rate)
α = exp(−1/(τ·fs)) — the RETAIN coefficient (see the file doc block).
Definition dynamics_core.hpp:155
double power_db(double mean_square, double epsilon)
A mean square → dB, floored by an addend.
Definition dynamics_core.hpp:146
double retain_finite(double candidate, double current) noexcept
Control setters retain their last valid value when automation supplies a non-finite sample.
Definition dynamics_core.hpp:117
double amplitude_db(double amplitude, double epsilon)
An amplitude → dB, floored by an ADDEND rather than a max().
Definition dynamics_core.hpp:135
constexpr double kLevelEpsilon
Level-conversion floor, guarding log10(0).
Definition dynamics_core.hpp:125
@ size
Definition config.hpp:216
std::uint64_t read(std::span< const std::uint8_t > b, std::size_t p, unsigned n) noexcept
Definition spectral_frame_codec.hpp:111
void write(std::uint8_t *b, std::size_t p, std::uint64_t v, unsigned n) noexcept
Definition spectral_frame_codec.hpp:117
T ms_to_samples(T ms, T sample_rate)
Milliseconds → samples at sample_rate Hz.
Definition units.hpp:147
T db_to_linear(T db)
Amplitude dB → linear gain. 0 dB → 1, −6.0206 dB → 0.5.
Definition units.hpp:71
Definition additive_bank.hpp:218
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
CompressorDetector
What the sidechain measures.
Definition feedforward_compressor.hpp:118
@ peak
Sample peak. Literal, fastest-responding.
Definition feedforward_compressor.hpp:119
@ rms
Running mean-square. Reads program loudness.
Definition feedforward_compressor.hpp:120
FeedforwardCompressorT< double > FeedforwardCompressor64
Definition feedforward_compressor.hpp:495
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
FeedforwardCompressorT< float > FeedforwardCompressor
Definition feedforward_compressor.hpp:494
@ i
Definition formant_filter_bank.hpp:20
The real-unit vocabulary shared by every DSP block in the catalog.