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
+30
View File
@@ -2063,6 +2063,35 @@ static void testSameBlockDoubleTakeoverKeepsDeclickSeed() {
CHECK(approx(post.back(), 1.0, 1e-3));
}
// Declick with ZERO-ATTACK takeover: the new voice reaches full level on frame 0 (amp == 1),
// so the gated compensation adds nothing (gate = 1 - 1 = 0). Output on the boundary frame is
// exactly the new voice's level, never exceeding full scale. Without the (1-amp) gate a zero-
// attack takeover from a sustained voice produced newOnset + oldLevel -> up to 2x (+6 dB).
static void testZeroAttackTakeoverNeverExceedsFullScale() {
SampleData s = dcSample(200000, 60);
s.play.adsr.attackFrames = 0; // zero-attack: amp == 1 on the very first frame
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 0;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
eng.noteOn(60, 127);
std::vector<AudioSample> pre;
eng.render(pre, 200); // sustained at 1.0
CHECK(approx(pre.back(), 1.0, 1e-6));
eng.noteOn(64, 127); // zero-attack takeover: amp hits 1 on frame 0
std::vector<AudioSample> post;
eng.render(post, 400);
// Every output frame must stay within [-1, 1]: no +6 dB blip.
for (AudioSample v : post) {
CHECK(v <= 1.0f + 1e-4f && v >= -1.0f - 1e-4f);
}
// The zero-attack note settles at sustain 1.0 immediately.
CHECK(approx(post[0], 1.0, 1e-4));
}
// GA-VoiceSteal repro (DAW bug): voiceCount 3, a triad note-on'd at the SAME sample time
// (three note-ons in one block, no render between), then a 4th note. The steal must take
// EXACTLY ONE voice (the oldest, none releasing) and leave the other two RINGING — the DAW
@@ -2265,6 +2294,7 @@ int main() {
testMonoDeclickOnlyOnTakeover();
testPolyStealDeclicksRestart();
testSameBlockDoubleTakeoverKeepsDeclickSeed();
testZeroAttackTakeoverNeverExceedsFullScale();
testPreviewCardIsolatedFromPool();
testPreviewCardReplaceStaleOffAndOutOfZone();