Γ-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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user