Γ-W1-T2: the limiter toggle is a mute, not a crossfade — the ceiling holds across both transitions

The equal-gain dry/wet blend let a peak through at (1-m) of its level. Now the
fade rides only the limited path and the hard edge lands on silence.
This commit is contained in:
2026-08-01 20:53:36 -04:00
parent 0612abbddb
commit 6232851c6b
5 changed files with 219 additions and 65 deletions
+1 -1
View File
@@ -292,7 +292,7 @@ anything for a trigger shape.
- `time_stretch` — the TIME half beside `pitch_shift`'s PITCH half, header-only: `StretchCursor`, the per-output-frame source-feed schedule (a fractional cursor carrying its rate debt, loop-wrapped), plus the rate bounds and their clamp. Rate 1.0 is exactly one source frame per output frame with no residue, which is what makes the unity Preserve read bit-identical to the pre-stretch engine. The bounds are **measured**, not arbitrary — see the header.
- `velocity_curve` — THE monotone spline, shared by every consumer: the three velocity transfer curves and the three spline EGs. `VelocityCurve` is evaluated as ONE OR MORE FritschCarlson monotone cubic Hermite splines joined at its HARD points — a hard knot is a sub-curve boundary for tangent purposes (exactly what the point array's own ends already are), so the two adjacent segments meet at their natural angle instead of a shared derivative and the no-overshoot guarantee holds PER SEGMENT rather than globally. Points are smooth by default; the ceiling is `kMaxCurvePoints` = 128, a MUSICAL bound (long rhythmic phrases, ~two points per articulation event) and not a performance one — **do not lower it**. `eval(velocity)` is the COLD reader, called once per note-on or once per drawn pixel column; `SplineCursor` is the RT one, an indexed segment search plus one Hermite evaluation with the segment and its tangents cached across samples. Both share the same `segmentTangents`/`hermiteAt` free functions, so there is one spline and not two. It carries its own y `CurveDomain`: UNIPOLAR [0,1] is the amp's GAIN, defaulting to `flat()` (y=1, every velocity→unity — a deliberate non-back-compat replacement of the old fixed `velocity/127` path, Daniel-approved); BIPOLAR [1,1] is the signed modulation shape for pitch and filter, defaulting to `zero()` so velocity modulates neither until a curve is drawn. A bipolar curve does not imply the absence of a depth beside it: the filter keeps its `velAmount` knob and the two compose multiplicatively (`velAmount × curve.eval(v)`, `play_params.h`), while the pitch curve's throw is the fixed `kVelocityPitchRangeSemitones`.
- `master_gain` — pure dB↔linear taper math (FB1): normalized [0,1] ↔ dB ↔ linear for the post-mixer master gain control (−∞…+24 dB, norm 0 = true silence, unity ≈ 0.714). Shared by the editor knob and the processor multiply so the needle, persisted value, and audio multiply cannot drift.
- `limiter` — the master bus's lookahead brickwall limiter, the stage after `master_gain`'s multiply: a 4x-oversampled TRUE-PEAK detector in the SIDECHAIN ONLY (the signal path is never oversampled), one stereo-linked gain, a baked 0.3 dBTP ceiling and **no makeup gain of any kind**. The gain law is a sliding MINIMUM of the per-sample target over the lookahead window followed by a MOVING AVERAGE of the same width: every term of that average is a minimum whose own window contains the sample being gained, so the ceiling is held **structurally** rather than by a tuned attack, and the one-pole release only ever slows the RISE so that bound survives it. Bypassed and settled, `process()` returns without reading or writing a sample — the byte-identical at-rest path, on the same discipline as `live == nullptr` and the filter's exact skip at `modAmount == 0`. `prepare()` owns every allocation and every transcendental; the engage/disengage crossfade is the codebase's standing ramp-every-gain-path-change rule applied to a limiter switching in.
- `limiter` — the master bus's lookahead brickwall limiter, the stage after `master_gain`'s multiply: a 4x-oversampled TRUE-PEAK detector in the SIDECHAIN ONLY (the signal path is never oversampled), one stereo-linked gain, a baked 0.3 dBTP ceiling and **no makeup gain of any kind**. The gain law is a sliding MINIMUM of the per-sample target over the lookahead window followed by a MOVING AVERAGE of the same width: every term of that average is a minimum whose own window contains the sample being gained, so the ceiling is held **structurally** rather than by a tuned attack, and the one-pole release only ever slows the RISE so that bound survives it. Bypassed and settled, `process()` returns without reading or writing a sample — the byte-identical at-rest path, on the same discipline as `live == nullptr` and the filter's exact skip at `modAmount == 0`. `prepare()` owns every allocation and every transcendental. **Switching is a MUTE, never a blend:** unlimited signal is emitted at weight 1 (the untouched bypass buffer) or at weight 0 and never in between, because a fraction of an unlimited signal is a peak over the ceiling — so the fade always rides the limited path and the hard edge always lands on the bypassed side, against silence. Do not reintroduce an equal-gain dry/wet crossfade over the toggle.
- `meter_ballistics` — the output meter's UI-side ballistics and dB scale: instantaneous rise, 20 dB/s fall, the 1.5 s peak hold and its release at the same rate, the clip latch, and the dB → normalized map over 60…+6 dBFS. The audio thread publishes raw block peaks and converts nothing; this module is what turns them into what the bar draws.
### `map/`
+36 -27
View File
@@ -36,7 +36,7 @@ void Limiter::prepare(double sampleRate) {
ceiling_ = static_cast<float>(limiterCeilingLinear());
const double rate = sampleRate > 0.0 ? sampleRate : 48000.0;
releaseCoeff_ = static_cast<float>(1.0 - std::exp(-1.0 / (kLimiterReleaseSeconds * rate)));
mixStep_ = static_cast<float>(1.0 / (kLimiterCrossfadeSeconds * rate));
switchStep_ = static_cast<float>(1.0 / (kLimiterMuteSeconds * rate));
// Windowed-sinc polyphase interpolator, built here because it costs transcendentals.
// Phase 0's taps all land on sinc zeros except the centre, so it is an exact delay and is
@@ -77,7 +77,7 @@ void Limiter::clearState() {
void Limiter::reset() {
clearState();
active_ = target_.load(std::memory_order_relaxed);
mix_ = active_ ? 1.f : 0.f;
switchGain_ = active_ ? 1.f : 0.f;
primeRemaining_ = 0;
}
@@ -158,11 +158,13 @@ float Limiter::process(float* left, float* right, int frames) {
const bool want = target_.load(std::memory_order_relaxed);
if (!want && !active_) return 1.f; // settled bypass: not one sample read or written
if (want && !active_) {
// A live engage. Start dry, fill the delay line, then crossfade — so the wet path is
// never silence weighted above zero.
// A live engage. The dry path leaves circuit AT THIS SAMPLE rather than fading out:
// fading it would emit unlimited signal at a partial weight, which is a peak over the
// ceiling. Silence covers the delay line's prime, then the fade-in rides the limited
// path, every sample of which is already under the ceiling.
clearState();
active_ = true;
mix_ = 0.f;
switchGain_ = 0.f;
primeRemaining_ = latency_;
}
@@ -183,33 +185,40 @@ float Limiter::process(float* left, float* right, int frames) {
if (stereo) delayR_[slot] = dryR;
delayPos_ = (delayPos_ + 1 == latency_) ? 0 : delayPos_ + 1;
// The endpoints are branches rather than blend arithmetic so a settled state is exact:
// dry + (wet - dry) * 1.0f is not wet in floating point. At m <= 0 the buffer is left
// untouched, which is the dry sample already in it.
const float m = mix_;
// The reported minimum is the gain actually reaching the output, not the limiter's raw
// target — mid-crossfade only a fraction `m` of the reduction is audible, so the meter
// (whose contract is "smallest gain APPLIED") must blend the same way the signal does:
// unity at m=0, `gain` at m=1, linear between.
const float effectiveGain = 1.f - m + m * gain;
if (effectiveGain < blockMin) blockMin = effectiveGain;
if (m >= 1.f) {
// Settled engaged is a branch rather than `wet * 1.0f` so it is bit-exact.
const float s = switchGain_;
if (s >= 1.f) {
left[i] = wetL;
if (stereo) right[i] = wetR;
} else if (m > 0.f) {
left[i] = dryL + (wetL - dryL) * m;
if (stereo) right[i] = dryR + (wetR - dryR) * m;
}
if (primeRemaining_ > 0) {
--primeRemaining_;
} else if (want) {
mix_ = (mix_ + mixStep_ >= 1.f) ? 1.f : mix_ + mixStep_;
} else if (s > 0.f) {
left[i] = wetL * s;
if (stereo) right[i] = wetR * s;
} else {
mix_ = (mix_ - mixStep_ <= 0.f) ? 0.f : mix_ - mixStep_;
left[i] = 0.f;
if (stereo) right[i] = 0.f;
}
const float effectiveGain = s >= 1.f ? gain : s * gain;
if (effectiveGain < blockMin) blockMin = effectiveGain;
// A disengage is tested FIRST so a toggle-off arriving mid-engage abandons the prime
// instead of waiting it out in silence.
if (!want) {
switchGain_ = s - switchStep_;
if (switchGain_ <= 0.f) {
// The disengage completes HERE, sample-accurately: the delay leaves circuit and
// the rest of the block is the dry buffer, untouched. Resuming from silence is
// the accepted discontinuity; fading the dry path back in instead would put
// unlimited signal at a partial weight, which is the leak the ceiling forbids.
switchGain_ = 0.f;
active_ = false;
break;
}
} else if (primeRemaining_ > 0) {
--primeRemaining_;
} else if (s < 1.f) {
switchGain_ = (s + switchStep_ >= 1.f) ? 1.f : s + switchStep_;
}
}
if (!want && mix_ <= 0.f && primeRemaining_ == 0) active_ = false;
return blockMin;
}
+22 -13
View File
@@ -28,10 +28,11 @@ inline constexpr double kLimiterLookaheadSeconds = 0.002;
// no-overshoot bound survives it unchanged.
inline constexpr double kLimiterReleaseSeconds = 0.100;
// The engage/disengage crossfade. A limiter engaging is a gain-path change and this codebase
// ramps every gain-path change; it also covers the window before the host acts on the latency
// change, which is the plugin's to keep clean because the host schedules that, not us.
inline constexpr double kLimiterCrossfadeSeconds = 0.010;
// The transition mute. Long enough that the fade is not itself an edge and that it dwarfs the
// 2 ms delay-line prime it covers; short enough that the whole muted window (prime + fade) is
// ~12 ms rather than a gap. Linear in amplitude, not equal-power: this fades ONE leg to
// silence, it does not cross two.
inline constexpr double kLimiterMuteSeconds = 0.010;
// 4x true-peak oversampling (ITU-R BS.1770's floor at 48 kHz) over an 8-tap-per-phase
// polyphase interpolator. The 33-tap prototype's centre tap makes phase 0 an exact 4-sample
@@ -53,6 +54,15 @@ int limiterLookaheadSamples(double sampleRate);
// then a MOVING AVERAGE of the same width. Every term of that average is a minimum whose own
// window contains the sample being gained, so the smoothed gain is <= the target gain at every
// sample by construction — the ceiling is held structurally rather than by a tuned attack.
//
// SWITCHING IS A MUTE, NOT A BLEND. Unlimited signal is emitted at weight 1 (settled bypass,
// which is the untouched buffer) or at weight 0, never in between — a fraction of an unlimited
// signal is a peak above the ceiling, which is exactly the leak this design forbids. So the
// FADE always rides the limited path (any weight of it is already under the ceiling, since the
// mute only scales down) and the HARD EDGE always lands on the bypassed side, against silence:
// engaging mutes at once, holds while the delay line primes, then fades the limited path in;
// disengaging fades the limited path out and resumes the dry buffer from silence. That
// discontinuity is accepted; a spike is not.
class Limiter {
public:
// Sizes the delay line, the detector and the smoothers, and snaps to the current enable
@@ -60,7 +70,7 @@ public:
void prepare(double sampleRate);
// Clears the delay line and the detector and snaps to the current enable state, skipping
// the engage crossfade — an activation has nothing sounding to be continuous with.
// the transition mute — an activation has nothing sounding to be continuous with.
// Main/UI thread only (the host guarantees process() is stopped at both call sites).
void reset();
@@ -70,10 +80,9 @@ public:
// Applies the limiter in place over `frames` of `left` (and `right`, which may be null for
// a mono buffer). Returns the SMALLEST gain actually applied to the output this block — 1.0
// for none (a settled bypass, or wherever the engage/disengage crossfade sits at dry). Mid
// crossfade this is the target gain blended by the same fraction `mix_` blends the signal,
// not the limiter's raw target — the two must agree, or the meter over-reports reduction
// that is only partially audible.
// for a settled bypass, 0.0 anywhere the transition mute is at silence. The transition mute
// counts because the contract is the gain that REACHED the output: the reported value and
// the signal are scaled by the same factor, or the meter and the bus disagree.
float process(float* left, float* right, int frames);
private:
@@ -91,7 +100,7 @@ private:
int window_ = 0; // the minimum/average width, latency_ - kLimiterOsDelay + 1
float ceiling_ = 1.f;
float releaseCoeff_ = 1.f;
float mixStep_ = 1.f;
float switchStep_ = 1.f;
float osTaps_[kLimiterOversample][kLimiterOsTaps] = {}; // phase 0 is unused (exact delay)
// --- audio-thread state ---
@@ -110,9 +119,9 @@ private:
double avgSum_ = 0.0; // double: the running sum is added to and subtracted from forever
int avgPos_ = 0;
float releaseGain_ = 1.f;
bool active_ = false; // the limiter path is running (engaged, or mid-crossfade)
float mix_ = 0.f; // 0 = dry, 1 = limited
int primeRemaining_ = 0; // samples the crossfade waits on while the delay line fills
bool active_ = false; // the limited path is in circuit (engaged, or still fading out)
float switchGain_ = 0.f; // the transition mute; only ever scales the LIMITED path
int primeRemaining_ = 0; // samples held at silence while the delay line fills
};
} // namespace reasampler::instrument::engine
@@ -97,7 +97,7 @@ tresult PLUGIN_API ReaSamplerProcessor::setActive(TBool state) {
reloadInstrument();
// The host performs this deactivate/reactivate whenever it acts on a kLatencyChanged
// request, so the limiter starts each activation with an empty delay line and snapped
// to its persisted state — no crossfade, because there is nothing sounding to be
// to its persisted state — no transition mute, because there is nothing sounding to be
// continuous with once the block above has destroyed every voice.
limiter_.reset();
} else {
+159 -23
View File
@@ -10,7 +10,11 @@
// * the detection is TRUE-peak: a signal whose SAMPLES all clear the ceiling but whose
// inter-sample peak does not still engages;
// * the gain is stereo-linked, so a dual-mono signal stays centered across a full toggle;
// * the engage/disengage crossfade leaves no step larger than the signal's own.
// * across a toggle in EITHER direction, every output sample is under the ceiling or exactly
// the unlimited input — never a fraction of the unlimited input, which is the leak the
// retired equal-gain crossfade admitted;
// * the transition's only two discontinuities are the hard edges against silence, one per
// direction.
#include "../src/core/instrument/engine/limiter.h"
@@ -146,7 +150,7 @@ static void testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle() {
bool centered = true;
for (std::size_t i = 0; i < l.size(); i += static_cast<std::size_t>(block)) {
// Toggle on a quarter in and off three quarters in, so the run covers bypassed,
// the engage crossfade, fully engaged, the disengage crossfade, and bypassed again.
// the engage mute, fully engaged, the disengage fade, and bypassed again.
if (i >= l.size() / 4 && !lim.enabled()) lim.setEnabled(true);
if (i >= (l.size() * 3) / 4 && lim.enabled()) lim.setEnabled(false);
const int n = static_cast<int>(
@@ -167,42 +171,172 @@ static void testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle() {
CHECK(worstEngaged > 0.f);
}
static void testToggleEmitsNoStepLargerThanTheSignalsOwn() {
// A steady sine: the crossfade blends it with a copy of itself delayed by the lookahead,
// which at 440 Hz is nearly half a cycle out — switching hard instead of fading would step
// by up to twice the amplitude, so this assertion has real teeth.
const double freq = 440.0;
const double amp = 0.5; // under the ceiling: this measures the TRANSITION, not limiting
std::vector<float> x(48000);
for (std::size_t i = 0; i < x.size(); ++i) {
x[i] = static_cast<float>(
static void testTheTransitionsOnlyEdgesAreTheTwoAgainstSilence() {
// Replaces the retired crossfade's "no step larger than the signal's own", which no longer
// describes the design: the mute has exactly ONE hard edge per direction, both against
// silence, and everything between them is continuous. A steady sine well under the
// ceiling, so this measures the TRANSITION and not limiting. 375 Hz is one cycle per 128
// samples, so a block-aligned toggle lands on a phase the test can state rather than
// inherit — at a zero crossing the engage edge would be small for a reason that has
// nothing to do with the design.
const double freq = 375.0; // kRate / 128
const double amp = 0.5;
const int block = 32;
const int engageAt = 12064; // block-aligned AND one sample past the sine's peak
const int disengageAt = 36064;
std::vector<float> in(48000);
for (std::size_t i = 0; i < in.size(); ++i) {
in[i] = static_cast<float>(
amp * std::sin(2.0 * 3.14159265358979323846 * freq * static_cast<double>(i) / kRate));
}
std::vector<float> y = in;
const float naturalStep =
static_cast<float>(amp * 2.0 * 3.14159265358979323846 * freq / kRate);
// One fade step's worth of signal: the disengage's last emitted sample sits at most this
// far above zero, because the fade is stepped AFTER the sample it weighted.
const float silenceFloor =
static_cast<float>(amp / (kLimiterMuteSeconds * kRate)) * 1.01f;
CHECK(std::fabs(in[static_cast<std::size_t>(engageAt) - 1]) > 0.4f); // the edge has teeth
Limiter lim;
lim.prepare(kRate);
const int block = 32;
for (std::size_t i = 0; i < x.size(); i += static_cast<std::size_t>(block)) {
if (i >= x.size() / 4 && !lim.enabled()) lim.setEnabled(true);
if (i >= (x.size() * 3) / 4 && lim.enabled()) lim.setEnabled(false);
for (std::size_t i = 0; i < y.size(); i += static_cast<std::size_t>(block)) {
if (static_cast<int>(i) >= engageAt && !lim.enabled()) lim.setEnabled(true);
if (static_cast<int>(i) >= disengageAt && lim.enabled()) lim.setEnabled(false);
const int n = static_cast<int>(
std::min(static_cast<std::size_t>(block), x.size() - i));
lim.process(x.data() + i, nullptr, n);
std::min(static_cast<std::size_t>(block), y.size() - i));
lim.process(y.data() + i, nullptr, n);
}
// Engage: the dry path leaves circuit AT the toggle sample, in one step to silence — the
// sample before it is still the untouched dry buffer, never a partial weight of it.
CHECK(y[static_cast<std::size_t>(engageAt) - 1] == in[static_cast<std::size_t>(engageAt) - 1]);
CHECK(y[static_cast<std::size_t>(engageAt)] == 0.f);
// Disengage: one resume edge, out of near-silence straight into the untouched dry buffer,
// and nothing written after it.
std::size_t lastTouched = 0;
for (std::size_t i = 0; i < y.size(); ++i) {
if (y[i] != in[i]) lastTouched = i;
}
CHECK(static_cast<int>(lastTouched) > disengageAt);
CHECK(std::fabs(y[lastTouched]) <= silenceFloor);
bool dryAfterResume = true;
for (std::size_t i = lastTouched + 1; i < y.size(); ++i) {
if (y[i] != in[i]) { dryAfterResume = false; break; }
}
CHECK(dryAfterResume);
// Everything BETWEEN the two edges is continuous — both fades and the settled middle.
float worstStep = 0.f;
for (std::size_t i = 1; i < x.size(); ++i) {
worstStep = std::max(worstStep, std::fabs(x[i] - x[i - 1]));
for (std::size_t i = static_cast<std::size_t>(engageAt) + 1; i <= lastTouched; ++i) {
worstStep = std::max(worstStep, std::fabs(y[i] - y[i - 1]));
}
CHECK(worstStep <= naturalStep * 1.2f);
// And the run really was muted, so the continuity above is not an untouched buffer's.
bool sawSilenceOverSignal = false;
for (std::size_t i = 0; i < y.size(); ++i) {
if (y[i] == 0.f && std::fabs(in[i]) > 0.4f) { sawSilenceOverSignal = true; break; }
}
CHECK(sawSilenceOverSignal);
}
static void testCrossfadeSettlesToTheExactEngagedAndBypassedPaths() {
// The one rule the transition encodes: every output sample is EITHER under the ceiling OR
// exactly the unlimited input. A fraction of the unlimited input is neither, which is why the
// retired equal-gain crossfade could pass a peak over the ceiling mid-transition.
static bool underCeilingOrExactlyDry(float y, float x, float ceiling) {
return std::fabs(y) <= ceiling * (1.f + 1e-6f) || y == x;
}
static void testUnlimitedSignalIsNeverEmittedAtAPartialWeight() {
const float ceiling = static_cast<float>(limiterCeilingLinear());
// +12 dB over the ceiling for the WHOLE run, so the transition windows are driven, not
// merely crossed while quiet.
const std::vector<float> in = pattern(48000, ceiling * 3.98f);
std::vector<float> y = in;
Limiter lim;
lim.prepare(kRate);
const int block = 64;
for (std::size_t i = 0; i < y.size(); i += static_cast<std::size_t>(block)) {
if (i >= y.size() / 4 && !lim.enabled()) lim.setEnabled(true); // engage
if (i >= (y.size() * 3) / 4 && lim.enabled()) lim.setEnabled(false); // disengage
const int n = static_cast<int>(
std::min(static_cast<std::size_t>(block), y.size() - i));
lim.process(y.data() + i, nullptr, n);
}
bool held = true;
bool sawLimited = false, sawMuted = false, sawDry = false;
for (std::size_t i = 0; i < y.size(); ++i) {
if (!underCeilingOrExactlyDry(y[i], in[i], ceiling)) { held = false; break; }
if (y[i] != in[i] && y[i] != 0.f) sawLimited = true;
if (y[i] == 0.f && std::fabs(in[i]) > ceiling) sawMuted = true;
if (y[i] == in[i] && std::fabs(in[i]) > ceiling) sawDry = true;
}
CHECK(held);
// Each of the three states the rule distinguishes actually occurred, so `held` is not
// satisfied by a buffer that was only ever passed through.
CHECK(sawLimited);
CHECK(sawMuted);
CHECK(sawDry);
}
static void testALoudTransientInFlightAtTheToggleCannotSpike() {
// The toggle flipped while a transient 18 dB over the ceiling is in flight, swept across
// the whole transition window (the 2 ms prime, the 10 ms fade, and past both) in each
// direction. Nothing anywhere may land between silence and the unlimited input.
const float ceiling = static_cast<float>(limiterCeilingLinear());
const int latency = limiterLookaheadSamples(kRate);
const int fade = static_cast<int>(kLimiterMuteSeconds * kRate);
const int block = 32;
const int toggleAt = 3200; // a block boundary
const int offsets[] = {0, 1, latency - 1, latency, latency + 1, fade / 2,
fade, fade + latency, fade + 4 * latency};
for (bool engaging : {true, false}) {
for (int offset : offsets) {
std::vector<float> in(
static_cast<std::size_t>(toggleAt + 2 * fade + 8 * latency), 0.f);
in[static_cast<std::size_t>(toggleAt + offset)] = ceiling * 8.f;
std::vector<float> y = in;
Limiter lim;
lim.setEnabled(!engaging);
lim.prepare(kRate); // prepare snaps to the target: the run starts settled
for (std::size_t i = 0; i < y.size(); i += static_cast<std::size_t>(block)) {
if (static_cast<int>(i) >= toggleAt) lim.setEnabled(engaging);
const int n = static_cast<int>(
std::min(static_cast<std::size_t>(block), y.size() - i));
lim.process(y.data() + i, nullptr, n);
}
bool held = true;
float loudestLimited = 0.f;
for (std::size_t i = 0; i < y.size(); ++i) {
if (!underCeilingOrExactlyDry(y[i], in[i], ceiling)) { held = false; break; }
if (y[i] != in[i]) loudestLimited = std::max(loudestLimited, std::fabs(y[i]));
}
CHECK(held);
// The transient reached the LIMITED path rather than being muted away entirely,
// so `held` above is not satisfied by silence. The qualifying offset differs by
// direction because the fade opens at the end of an engage and closes at the
// start of a disengage.
if (engaging && offset >= fade + latency) {
CHECK(loudestLimited > ceiling * 0.9f);
}
if (!engaging && offset == 0) CHECK(loudestLimited > ceiling * 0.5f);
}
}
}
static void testTransitionSettlesToTheExactEngagedAndBypassedPaths() {
Limiter lim;
lim.prepare(kRate);
const int latency = limiterLookaheadSamples(kRate);
const int settle = static_cast<int>(kLimiterCrossfadeSeconds * kRate) + latency + 64;
// The engage costs a `latency`-sample prime, then the fade, then the delay itself.
const int settle =
static_cast<int>(kLimiterMuteSeconds * kRate) + 2 * latency + 64;
const std::vector<float> src = pattern(4 * settle, 0.3f); // under the ceiling throughout
std::vector<float> y = src;
@@ -302,8 +436,10 @@ int main() {
testEngagedHoldsTheCeilingOnProgramTwelveDbOver();
testTruePeakDetectionEngagesWhereSamplePeakWouldNot();
testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle();
testToggleEmitsNoStepLargerThanTheSignalsOwn();
testCrossfadeSettlesToTheExactEngagedAndBypassedPaths();
testTheTransitionsOnlyEdgesAreTheTwoAgainstSilence();
testUnlimitedSignalIsNeverEmittedAtAPartialWeight();
testALoudTransientInFlightAtTheToggleCannotSpike();
testTransitionSettlesToTheExactEngagedAndBypassedPaths();
testGainNeverRisesAboveUnity();
testAlignmentIdentityHoldsAtTheExactWindowEdge();
testBakedConstants();