Γ-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:
@@ -197,8 +197,10 @@ float Limiter::process(float* left, float* right, int frames) {
|
|||||||
left[i] = 0.f;
|
left[i] = 0.f;
|
||||||
if (stereo) right[i] = 0.f;
|
if (stereo) right[i] = 0.f;
|
||||||
}
|
}
|
||||||
const float effectiveGain = s >= 1.f ? gain : s * gain;
|
// `gain` is the limiter's own reduction, computed from the real input this sample
|
||||||
if (effectiveGain < blockMin) blockMin = effectiveGain;
|
// whether or not the mute is currently scaling it toward silence — publishing it
|
||||||
|
// unscaled is what lets the meter show "really limiting" and not "just muting".
|
||||||
|
if (gain < blockMin) blockMin = gain;
|
||||||
|
|
||||||
// A disengage is tested FIRST so a toggle-off arriving mid-engage abandons the prime
|
// A disengage is tested FIRST so a toggle-off arriving mid-engage abandons the prime
|
||||||
// instead of waiting it out in silence.
|
// instead of waiting it out in silence.
|
||||||
|
|||||||
@@ -79,10 +79,12 @@ public:
|
|||||||
bool enabled() const { return target_.load(std::memory_order_relaxed); }
|
bool enabled() const { return target_.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
// Applies the limiter in place over `frames` of `left` (and `right`, which may be null for
|
// 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
|
// a mono buffer). Returns the SMALLEST gain the LIMITER ITSELF computed this block —
|
||||||
// for a settled bypass, 0.0 anywhere the transition mute is at silence. The transition mute
|
// smoothGain's output against the real input, at every sample including a muted one — NOT
|
||||||
// counts because the contract is the gain that REACHED the output: the reported value and
|
// scaled by the transition mute. The mute is a switch, not limiting: scaling by it would
|
||||||
// the signal are scaled by the same factor, or the meter and the bus disagree.
|
// report 0.0 (full reduction) on every toggle regardless of program content, which is a
|
||||||
|
// meter defect, not a fact about the bus. 1.0 means no detected peak exceeded the ceiling,
|
||||||
|
// whether settled bypassed or mid-mute over quiet material.
|
||||||
float process(float* left, float* right, int frames);
|
float process(float* left, float* right, int frames);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ class ReaSamplerEmbed; // embedded TCP/MCP UI shell (owned below; see queryInte
|
|||||||
struct MasterBusMeter {
|
struct MasterBusMeter {
|
||||||
float peakL = 0.f; // max |x| this block
|
float peakL = 0.f; // max |x| this block
|
||||||
float peakR = 0.f;
|
float peakR = 0.f;
|
||||||
float minGain = 1.f; // smallest limiter gain applied this block; 1 = no reduction
|
// Smallest gain the LIMITER computed this block (Limiter::process) — deliberately NOT
|
||||||
|
// scaled by the transition mute, so a toggle over quiet material reads 1 (no reduction)
|
||||||
|
// rather than the mute's own weight. 1 = no reduction.
|
||||||
|
float minGain = 1.f;
|
||||||
bool clip = false; // LATCHED at a block peak >= 0 dBFS; only clearMasterBusClip lowers it
|
bool clip = false; // LATCHED at a block peak >= 0 dBFS; only clearMasterBusClip lowers it
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+45
-2
@@ -14,7 +14,9 @@
|
|||||||
// the unlimited input — never a fraction of the unlimited input, which is the leak the
|
// the unlimited input — never a fraction of the unlimited input, which is the leak the
|
||||||
// retired equal-gain crossfade admitted;
|
// retired equal-gain crossfade admitted;
|
||||||
// * the transition's only two discontinuities are the hard edges against silence, one per
|
// * 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"
|
#include "../src/core/instrument/engine/limiter.h"
|
||||||
|
|
||||||
@@ -143,11 +145,19 @@ static void testTruePeakDetectionEngagesWhereSamplePeakWouldNot() {
|
|||||||
static void testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle() {
|
static void testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle() {
|
||||||
Limiter lim;
|
Limiter lim;
|
||||||
lim.prepare(kRate);
|
lim.prepare(kRate);
|
||||||
|
const int latency = limiterLookaheadSamples(kRate);
|
||||||
const float ceiling = static_cast<float>(limiterCeilingLinear());
|
const float ceiling = static_cast<float>(limiterCeilingLinear());
|
||||||
const std::vector<float> src = pattern(48000, ceiling * 2.5f);
|
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
|
std::vector<float> l = src, r = src; // dual mono: L and R are the same signal
|
||||||
const int block = 64;
|
const int block = 64;
|
||||||
bool centered = true;
|
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)) {
|
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,
|
// 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.
|
// 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);
|
if (i >= (l.size() * 3) / 4 && lim.enabled()) lim.setEnabled(false);
|
||||||
const int n = static_cast<int>(
|
const int n = static_cast<int>(
|
||||||
std::min(static_cast<std::size_t>(block), l.size() - i));
|
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) {
|
for (std::size_t i = 0; i < l.size(); ++i) {
|
||||||
if (l[i] != r[i]) { centered = false; break; }
|
if (l[i] != r[i]) { centered = false; break; }
|
||||||
@@ -169,6 +180,37 @@ static void testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle() {
|
|||||||
}
|
}
|
||||||
CHECK(worstEngaged <= ceiling * (1.f + 1e-6f));
|
CHECK(worstEngaged <= ceiling * (1.f + 1e-6f));
|
||||||
CHECK(worstEngaged > 0.f);
|
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() {
|
static void testTheTransitionsOnlyEdgesAreTheTwoAgainstSilence() {
|
||||||
@@ -436,6 +478,7 @@ int main() {
|
|||||||
testEngagedHoldsTheCeilingOnProgramTwelveDbOver();
|
testEngagedHoldsTheCeilingOnProgramTwelveDbOver();
|
||||||
testTruePeakDetectionEngagesWhereSamplePeakWouldNot();
|
testTruePeakDetectionEngagesWhereSamplePeakWouldNot();
|
||||||
testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle();
|
testStereoLinkedGainKeepsDualMonoCenteredAcrossAToggle();
|
||||||
|
testToggleWithNothingOverCeilingPublishesNoReduction();
|
||||||
testTheTransitionsOnlyEdgesAreTheTwoAgainstSilence();
|
testTheTransitionsOnlyEdgesAreTheTwoAgainstSilence();
|
||||||
testUnlimitedSignalIsNeverEmittedAtAPartialWeight();
|
testUnlimitedSignalIsNeverEmittedAtAPartialWeight();
|
||||||
testALoudTransientInFlightAtTheToggleCannotSpike();
|
testALoudTransientInFlightAtTheToggleCannotSpike();
|
||||||
|
|||||||
Reference in New Issue
Block a user