Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
dynamics_core.hpp File Reference

The published compressor equations, once. More...

#include <algorithm>
#include <cmath>

Go to the source code of this file.

Namespaces

namespace  pulp
namespace  pulp::signal
namespace  pulp::signal::dynamics

Functions

double pulp::signal::dynamics::retain_finite (double candidate, double current) noexcept
 Control setters retain their last valid value when automation supplies a non-finite sample.
double pulp::signal::dynamics::amplitude_db (double amplitude, double epsilon)
 An amplitude → dB, floored by an ADDEND rather than a max().
double pulp::signal::dynamics::power_db (double mean_square, double epsilon)
 A mean square → dB, floored by an addend.
double pulp::signal::dynamics::one_pole_retain (double tau_seconds, double sample_rate)
 α = exp(−1/(τ·fs)) — the RETAIN coefficient (see the file doc block).
double pulp::signal::dynamics::knee_term_db (double over_db, double knee_db, double slope)
 The knee branch's quadratic term: the dB the characteristic bends away from unity inside the knee, (slope − 1)·(over + W/2)² / (2W).
double pulp::signal::dynamics::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.
double pulp::signal::dynamics::soft_knee_reduction_db (double over_db, double knee_db, double slope)
 The same characteristic in the GAIN-REDUCTION domain: dB over threshold → reduction in dB, SIGNED and ≤ 0 for a compressing slope.

Variables

constexpr double pulp::signal::dynamics::kLevelEpsilon = 1e-12
 Level-conversion floor, guarding log10(0).

Detailed Description

The published compressor equations, once.

Four lineages in this catalog — the transparent feedforward design, the Blackmer/VCA one, the 1176-style FET one and the diode-bridge one — differ in topology, in gain element and in where their ballistics live. What they do NOT differ in is the static characteristic they all compute, which is the soft-knee gain computer of

Giannoulis, D., Massberg, M. & Reiss, J.D., "Digital Dynamic Range Compressor Design — A Tutorial and Analysis", Journal of the Audio Engineering Society 60(6), pp. 399–408, 2012.

Written per module it became four transcriptions of one published equation, each with its own branch conditions and its own idea of which domain the answer comes back in. This header is the one transcription.

What it owns: the knee's quadratic term, the two domain forms of the characteristic that wrap it, the add-then-log level conversions, and the one-pole RETAIN coefficient. All free functions over double, all pure.

What it does NOT own — and this is the more important half:

  • Detectors. All four have one and no two are the same equation. The feedforward design runs the paper's DECOUPLED two-stage smoother; the VCA lineage runs a single direction-switched pole on the instantaneous mean square; the FET lineage runs the paper's BRANCHING filter at the oversampled rate inside a feedback loop; the diode-bridge lineage runs a dual follower blended on a sustain indicator. Those differences are what the modules exist to express. A base class with a mode switch would be strictly worse than four detectors that each say what they are.
  • Gain elements. A VCA multiply, a FET voltage divider and a diode-bridge attenuator share nothing but their position in the chain.
  • The subtraction y_L − x_L. It is a subtraction, not an equation, and the three sign conventions below mean a shared wrapper would have to be parameterised by its own sign — which is how the convention gets lost.
  • Floor disciplines other than add-then-log. The VCA lineage floors its mean square with a max() rather than an addend, deliberately: max keeps the detector's own state out of the denormal range, which an addend applied at read-out does not do. The diode-bridge lineage composes units::linear_to_db, the house conversion, and should keep doing so. Collapsing three floor disciplines into one would change three modules' arithmetic to save one line each.

The sign convention, and why there is more than one

This is the trap in the whole subsystem, so it is written down here rather than in one module's doc block where the other three cannot see it. Gain reduction is expressed three ways across the catalog, all correct in their own module and none interchangeable:

Module gain_reduction_db() Why
feedforward POSITIVE magnitude its decoupled detector runs a max(), which is only correct on positives
VCA SIGNED, ≤ 0 it is added to makeup before one dB→linear conversion, matching the paper's equations
FET POSITIVE magnitude same max()-free branching detector, but the divider is calibrated in positive dB of attenuation
diode-bridge SIGNED, ≤ 0 its static curve returns reduction directly

The specific hazard, carried here from the feedforward module's doc block because it applies to any future member of this family: running a max() on the NEGATIVE gain-computer output silently swaps attack and release. The detector snaps toward LESS reduction and eases into more. That version still compresses, still sounds like a working compressor, and fails a step-response test immediately — which is why every module in this family has one.

soft_knee_reduction_db returns the SIGNED form (≤ 0 for a compressing ratio). A caller wanting the positive magnitude negates once, at its own boundary, and then stays in positives.

Why the characteristic ships in two domains

soft_knee_output_db returns an output LEVEL; soft_knee_reduction_db returns a gain REDUCTION. They are the same equation and algebraically output = input + reduction — but they are NOT equal in floating point on the linear branch, where one computes T + over/R and the other computes (1/R − 1)·over. Deriving either from the other would perturb three modules' results in the last bits, which is a behaviour change made to save a function. So both ship, adjacent, sharing the knee term — which IS bit-exact between them — and a module picks the domain its own public contract is stated in.

The two also differ in which branch they take exactly ON the knee's lower edge (2·over == −W): the level form enters the quadratic branch, where the term evaluates to zero, and the reduction form returns zero directly. Same value, different path — preserved as-is so each module's arithmetic is untouched.

Retain, not coefficient

one_pole_retain returns α = exp(−1/(τ·fs)), the α in y = α·y + (1−α)·x. That is the COMPLEMENT of units::ms_to_onepole_coef, which returns the a in y += a·(x − y). Both conventions are in use in this family and they are one subtraction apart, so the name says which one this is. τ is the 63.2 % time constant in SECONDS; the 10–90 % rise is τ·ln 9 and the 60 dB decay is τ·ln 1000, per units.hpp.

Note also that BallisticsFilterT's coefficient is built on an exp(−2.2/·) 10–90 % convention. Composing it where a τ is meant redefines the user's time by a factor of ln 9.

RT contract: everything here is pure, stateless scalar arithmetic over POD. Nothing allocates, locks, or performs I/O, so all of it is safe per sample on the audio thread. amplitude_db/power_db cost one log10, one_pole_retain one exp, and the characteristic none — it is arithmetic and branches only.