fix(sampler_core): gate takeover declick by (1-amp) to remove zero-attack +6 dB blip and long-attack notch; clamp seed to +/-1; add zero-attack test

This commit is contained in:
2026-07-28 06:59:05 -04:00
parent 056ccd003e
commit 39ea6ce7db
2 changed files with 43 additions and 4 deletions
+13 -4
View File
@@ -272,8 +272,10 @@ void Voice::start(int note, int velocity, const SampleData& sample, int rootNote
// the same pre-cut output level — zeroing would drop the pending ramp and bring the
// click back on that edge. The next rendered frame overwrites lastOut anyway.
if (declickTakeover && active_) {
declickL_ = lastOutL_;
declickR_ = lastOutR_;
// Clamp the seed to ±1.0: closes the theoretical same-frame-repeat accumulation edge
// (successive starts before any frame is rendered cannot grow the seed above full scale).
declickL_ = (lastOutL_ > 1.0) ? 1.0 : (lastOutL_ < -1.0) ? -1.0 : lastOutL_;
declickR_ = (lastOutR_ > 1.0) ? 1.0 : (lastOutR_ < -1.0) ? -1.0 : lastOutR_;
declickActive_ = (declickL_ > kDeclickFloor || declickL_ < -kDeclickFloor ||
declickR_ > kDeclickFloor || declickR_ < -kDeclickFloor);
} else {
@@ -530,9 +532,16 @@ AudioSample Voice::advanceFrame(bool stereo, AudioSample& outR) {
// takeover/steal start() so the restart's hard cut has no step. Engine-agnostic — applied
// after either pitch-engine branch, on the shared epilogue. Inactive (the common case)
// costs one branch.
//
// Gate by (1 - amp): the compensation fills only the HOLE the new attack leaves. When amp
// is near 0 (slow attack, typical 3 ms) the gate is ~1 — full compensation, no change in
// feel. When amp is 1 (zero-attack, instant sustain) the gate is 0 — no compensation added,
// so there is no +6 dB blip. For long attacks the gate tapers the compensation proportionally,
// removing the notch that arose when both the old tail and the new level were present in full.
if (declickActive_) {
outL += declickL_;
if (stereo) outRlocal += declickR_;
const double gate = 1.0 - amp;
outL += declickL_ * gate;
if (stereo) outRlocal += declickR_ * gate;
declickL_ *= kDeclickDecay;
declickR_ *= kDeclickDecay;
if (declickL_ < kDeclickFloor && declickL_ > -kDeclickFloor &&