The VCA compressor: the dbx/Blackmer lineage, as a true-RMS, log-domain, single-time-constant feedforward design. 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 <vector>Go to the source code of this file.
Classes | |
| class | pulp::signal::VcaCompressorT< SampleType > |
| True-RMS, log-domain, single-time-constant feedforward compressor. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
Typedefs | |
| using | pulp::signal::VcaCompressor = VcaCompressorT<float> |
| using | pulp::signal::VcaCompressor64 = VcaCompressorT<double> |
The VCA compressor: the dbx/Blackmer lineage, as a true-RMS, log-domain, single-time-constant feedforward design.
This is NOT the transparent modern compressor with a different colour stage bolted on. FeedforwardCompressorT and this class differ in WHERE the ballistics live, and that one difference is the whole lineage:
Here the gain computer is strictly memoryless. Every sample of attack and release "feel" is produced once, by the detector's single one-pole running on the INSTANTANEOUS MEAN SQUARE, and nothing downstream re-smooths it. Change the threshold mid-render and the gain moves on the very next sample, because there is no post-gain-computer filter left to ramp through. The modern design cannot do that, and a test asserts the difference rather than leaving it as prose.
True RMS, in the log domain. Blackmer's detector patent (David E. Blackmer, US 3,681,618, "RMS Circuits with Bipolar Logarithmic Converter", filed 1971, granted 1972-08-01; expired 1989-08-01 under the pre-GATT 17-years-from-grant term) replaces the resistor of a conventional RC averager with a diode at a fixed idle current, so the averaging time constant is set by a current and — driven through a log/antilog pair — the network averages in the LOG domain. That is mathematically true RMS, not peak with a fudge factor. The digital reformulation is the one-pole on x² below, read out as 10·log10. Cited for topology and concept only; no component values, SPICE models or trim data from any commercial part are used here.
One time constant, not two. The averaging RC is a single physical network; it does not know "rising" from "falling". So the user surface here is ONE time_ms control, not an attack knob and a release knob. It is not literally symmetric — a real diode conducts forward and leaks in reverse at different rates, and Blackmer's network is documented as fast-attack / slow-decay LOCKED TOGETHER — so the direction split survives as a single fixed internal ratio k (release τ = k · attack τ), not as a second knob. The falsifiable form of "the dbx feel" is therefore release_time / attack_time == k, and that is what the suite measures.
A dB-linear VCA. The Blackmer gain cell's control law is exponential — gain in dB is linear in control voltage (0.33 dB/mV at 300 K for the four-transistor cell). THAT Corporation's 2180-series ICs, the licensed modern continuation, carry it forward (>120 dB dynamic range, >130 dB gain range, <0.01 % THD; THAT Corporation, "2180-Series Pre-Trimmed Blackmer® VCA" datasheet). In a digital build that collapses to 10^(g/20) with no free parameters — but it is WHY the whole chain stays in dB from the detector's 10·log10 to that single conversion, with no linear-domain divide-by-ratio anywhere.
The static characteristic itself is the published feedforward hard/soft-knee gain computer of Giannoulis, Massberg & Reiss, "Digital Dynamic Range Compressor Design — A Tutorial and Analysis", JAES 60(6), pp. 399–408, 2012. It is written out here rather than borrowed from FeedforwardCompressorT for two reasons that are not stylistic: that class clamps ratio to ≥ 1, so it structurally cannot express the negative-ratio mode below, and its accessor is bound to its own parameter set.
"OverEasy" is dbx's trademark for a continuous soft knee (dbx 160, 1976). It is documented, non-numeric product language, so it is treated here as a NAMED MODE — knee width > 0 — whose shape comes from the cited paper, never from a proprietary curve.
gain_reduction_db() here is SIGNED and ≤ 0: it is the number added to the makeup gain before the dB→linear conversion, exactly as §3.2/§3.3 of the spec define it. FeedforwardCompressorT::gain_reduction_db() returns the opposite convention — a positive magnitude — because its decoupled detector runs a max() that is only correct on positives. The two classes disagree deliberately; a caller metering both must not assume one convention. Nothing in THIS class runs a max() over the gain signal, so there is no ordering hazard here to justify flipping the sign, and matching the spec's own equations is worth more than matching the sibling class.
Because k > 1, the detector charges faster than it discharges, so on a signal whose instantaneous power VARIES it settles ABOVE the true mean square. On a sine at equilibrium the offset c = (ȳ − m)/m, with m the true mean square, solves
which at the default k = 4 puts the reading ~1.5 dB above true RMS. That is a property of the topology, not a defect: a constant-power signal (DC, square wave) still reads its exact true RMS, and it is what makes this detector's number differ from an unbiased RMS average. The suite asserts the measured offset against that closed form, because a criterion demanding true-RMS accuracy from a k > 1 detector is not achievable by any correct implementation of this topology.
Not applicable. The only nonlinearity is the static gain computer, and it operates on the CONTROL-rate level_db signal, never on the audio waveform. The audio path is one per-sample linear multiply. There is no audio-rate nonlinearity to alias, so no oversampling and no ADAA.
Feedforward: the detector reads the INPUT, never this class's own output. There is no loop, so there is no stability argument and no small-signal gain to compensate. The gain command is bounded above by makeup_db alone and below by −ceiling_db; both bounds are asserted by the suite, not assumed.
RT contract: prepare() may allocate — it sizes the lookahead ring buffer for the maximum lookahead_ms of the declared range, which is what lets set_lookahead_ms() be audio-thread safe. set_*, process*, reset() and every accessor never allocate, never lock, never throw. process() is a pure function of (state, input) with no hidden globals. All state is POD; a zero-initialised instance is a valid silent-passthrough instance at unity gain before prepare() is called.
Control-domain values are double throughout, matching the house reference feedforward_compressor.hpp. The spec sketch writes them as SampleType, which would round a float instantiation's threshold and time constants through float for no benefit — the detector's one-pole coefficient at 500 ms and 192 kHz is ~1e-5, where float starts to matter.