The transparent, modern compressor: a log-domain feedforward design with a soft-knee gain computer and a decoupled detector. More...
#include <pulp/signal/denormal.hpp>#include <pulp/signal/dynamics_core.hpp>#include <pulp/signal/dynamics_contract.hpp>#include <pulp/signal/units.hpp>#include <algorithm>#include <cmath>#include <cstddef>#include <cstdint>#include <vector>Go to the source code of this file.
Classes | |
| class | pulp::signal::FeedforwardCompressorT< SampleType > |
| Log-domain feedforward compressor with a soft-knee gain computer. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
Typedefs | |
| using | pulp::signal::FeedforwardCompressor = FeedforwardCompressorT<float> |
| using | pulp::signal::FeedforwardCompressor64 = FeedforwardCompressorT<double> |
Enumerations | |
| enum class | pulp::signal::CompressorDetector : std::uint8_t { pulp::signal::peak , pulp::signal::rms } |
| What the sidechain measures. More... | |
The transparent, modern compressor: a log-domain feedforward design with a soft-knee gain computer and a decoupled detector.
"Feedforward" is the whole topology in one word — the sidechain reads the INPUT, never the compressor's own output. There is no loop, so there is no stability story to tell and no small-signal gain to compensate; the gain trajectory is a pure function of the input.
Every equation in the signal path comes from Giannoulis, Massberg & Reiss, "Digital Dynamic Range Compressor Design — A Tutorial and Analysis", JAES 60(6), pp. 399–408, 2012. Anything not from that paper is tagged [design parameter] with a default and a range.
The obvious detector is one pole whose coefficient switches between attack and release depending on whether the level is rising. That design has a discontinuous derivative exactly at the crossover, and material sitting near the threshold crosses it constantly — the artefact is audible as distortion on precisely the signals a compressor is most often asked to handle. The paper's fix, used here from the outset, is to decouple the two:
Stage 1 already picks the faster of "follow immediately" and "release toward it", so stage 2 always has a continuous input and there is no branch for the signal to chatter across.
Sign convention, stated because it is easy to get backwards. r[n] above is the gain reduction as a POSITIVE magnitude in dB — the paper's convention. The gain computer naturally produces g_c = y_L − x_L ≤ 0, so this class negates once, at the boundary, and the detector works in positives throughout. Running the same max() on the negative quantity would make the detector snap instantly toward LESS reduction and ease into more, i.e. swap attack and release. That version still compresses, still sounds like a compressor, and fails a step-response test immediately — which is why there is one.
Feedback topology is a different design with a different static-curve mapping, not a mode switch. Multiband is N instances plus the existing crossovers. An external sidechain key is a graph-layer routing concern. Analog saturation is explicitly excluded by the clean/modern brief. Oversampling does not apply (series law 4, stated rather than silently omitted): there is no waveshaping nonlinearity in the audio path — the only nonlinearity is a smoothed multiplicative gain trajectory, band-limited by the detector's own smoothing at any musically sensible attack time. True-peak (inter-sample) detection would need an oversampled estimator; peak mode here is sample-peak only, flagged rather than folded in silently.
This ships ALONGSIDE compressor.hpp rather than replacing it: the two do not share a parameter layout, so repointing existing callers is a migration with its own compatibility audit, not a drop-in swap.
RT contract: prepare() may allocate — it sizes the lookahead ring buffer to the worst-case max_lookahead_ms at the prepared sample rate, so no later set_lookahead_ms() can allocate. set_*, process*, and reset() never allocate, never lock, never throw. process() is a pure function of (state, input) with no hidden globals. The stereo entry points read both channels before writing either, which the link path requires.