Γ-W1-T2: the published GR meter reads the limiter, not the mute

Retire the effectiveGain blend so the meter's minimum tracks smoothGain's own
reduction against real input, unscaled by the transition mute — a toggle over
quiet material now reads no reduction instead of pinning to 0.
This commit is contained in:
2026-08-01 21:08:13 -04:00
parent 6232851c6b
commit 91c1b78d5e
4 changed files with 59 additions and 9 deletions
+45 -2
View File
@@ -14,7 +14,9 @@
// 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.
// direction;
// * the published minimum tracks the limiter's own reduction, not the mute weight: a toggle
// over content that never crosses the ceiling publishes exactly 1.0 all the way through.
#include "../src/core/instrument/engine/limiter.h"
@@ -143,11 +145,19 @@ static void testTruePeakDetectionEngagesWhereSamplePeakWouldNot() {
static void testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle() {
Limiter lim;
lim.prepare(kRate);
const int latency = limiterLookaheadSamples(kRate);
const float ceiling = static_cast<float>(limiterCeilingLinear());
const std::vector<float> src = pattern(48000, ceiling * 2.5f);
std::vector<float> l = src, r = src; // dual mono: L and R are the same signal
const int block = 64;
bool centered = true;
// The prime+fade window right after the engage point: the published minimum here is the
// case Daniel named — it must read the limiter's own reduction on this loud program, not
// the mute weight (which would read exactly 0 through the prime, old contract).
const std::size_t engageAt = l.size() / 4;
const std::size_t muteWindowEnd = engageAt + static_cast<std::size_t>(latency) +
static_cast<std::size_t>(kLimiterMuteSeconds * kRate);
float minGainDuringMute = 1.f;
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 mute, fully engaged, the disengage fade, and bypassed again.
@@ -155,7 +165,8 @@ static void testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle() {
if (i >= (l.size() * 3) / 4 && lim.enabled()) lim.setEnabled(false);
const int n = static_cast<int>(
std::min(static_cast<std::size_t>(block), l.size() - i));
lim.process(l.data() + i, r.data() + i, n);
const float g = lim.process(l.data() + i, r.data() + i, n);
if (i >= engageAt && i < muteWindowEnd && g < minGainDuringMute) minGainDuringMute = g;
}
for (std::size_t i = 0; i < l.size(); ++i) {
if (l[i] != r[i]) { centered = false; break; }
@@ -169,6 +180,37 @@ static void testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle() {
}
CHECK(worstEngaged <= ceiling * (1.f + 1e-6f));
CHECK(worstEngaged > 0.f);
CHECK(minGainDuringMute > 0.f); // never the mute's own zero weight
CHECK(minGainDuringMute < 1.f); // and it really is reduction, not a no-op read
}
static void testToggleWithNothingOverCeilingPublishesNoReduction() {
// Daniel's ruling: only show GR when it's really limiting, not just muting. Content that
// never exceeds the ceiling must publish exactly 1.0 through the WHOLE transition — the
// prime, both fades, and the settled stretches — because the old effectiveGain contract
// read 0.0 through the mute regardless of content.
Limiter lim;
lim.prepare(kRate);
const float ceiling = static_cast<float>(limiterCeilingLinear());
const std::vector<float> src = pattern(48000, ceiling * 0.5f); // comfortably under, always
std::vector<float> y = src;
const int block = 64;
float minGain = 1.f;
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);
if (i >= (y.size() * 3) / 4 && lim.enabled()) lim.setEnabled(false);
const int n = static_cast<int>(
std::min(static_cast<std::size_t>(block), y.size() - i));
const float g = lim.process(y.data() + i, nullptr, n);
if (g < minGain) minGain = g;
}
CHECK(minGain == 1.f);
// And the run really did mute, so `minGain == 1.f` is not vacuous over an untouched buffer.
bool sawSilenceOverSignal = false;
for (std::size_t i = 0; i < y.size(); ++i) {
if (y[i] == 0.f && std::fabs(src[i]) > 0.1f) { sawSilenceOverSignal = true; break; }
}
CHECK(sawSilenceOverSignal);
}
static void testTheTransitionsOnlyEdgesAreTheTwoAgainstSilence() {
@@ -436,6 +478,7 @@ int main() {
testEngagedHoldsTheCeilingOnProgramTwelveDbOver();
testTruePeakDetectionEngagesWhereSamplePeakWouldNot();
testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle();
testToggleWithNothingOverCeilingPublishesNoReduction();
testTheTransitionsOnlyEdgesAreTheTwoAgainstSilence();
testUnlimitedSignalIsNeverEmittedAtAPartialWeight();
testALoudTransientInFlightAtTheToggleCannotSpike();