Preview rides the real note path: mailbox drains into VoiceEngine noteOn/noteOff at the root note; PreviewCard retired; first-poll reopen heal reloads a silent-but-should-be-loaded instrument

This commit is contained in:
2026-07-28 10:30:51 -04:00
parent 6b85df8a8c
commit 81c4658711
5 changed files with 136 additions and 385 deletions
+40 -147
View File
@@ -1331,8 +1331,8 @@ static void testPreserveGateStereoLoopComposes() {
}
// --- Preserve voice cap: a Preserve note-on past the cap is dropped; Varispeed unaffected. ---
// Since the Phase S re-scope EVERY engine Preserve voice (root included) runs the shifter and
// counts toward the cap — the unity demotion is preview-card-only (see the Phase S section).
// EVERY engine Preserve voice (root included) runs the shifter and counts toward the cap —
// the FA1 unity demotion is gone (it was scoped to the retired PreviewCard).
static void testPreserveVoiceCap() {
SampleData s = dcSample(2000, 60);
s.play.pitchEngine = PitchEngine::Preserve; // held (Gate, no loop -> runs long enough)
@@ -1346,11 +1346,10 @@ static void testPreserveVoiceCap() {
}
// ---------------------------------------------------------------------------
// FA1 (re-scoped by Phase S) — the Preserve unity-Varispeed bypass now belongs to the PREVIEW
// CARD ONLY. The MIDI engine keeps the shifter at EVERY Preserve note. GA2 update: the primed
// shifter speaks on frame 0 at every ratio (the ring holds the first window of real source),
// so onset is uniformly IMMEDIATE across the keyboard and the bypass survives purely as a
// per-frame work-skip for the card.
// FA1 postscript — the Preserve unity-Varispeed bypass is REMOVED with the PreviewCard
// (preview is now a plain engine noteOn at the root). The engine keeps the shifter at
// EVERY Preserve note; GA2's primed ring speaks on frame 0 at every ratio, so onset is
// uniformly IMMEDIATE across the keyboard with no demotion path at all.
// ---------------------------------------------------------------------------
// The ENGINE'S root-note Preserve voice keeps the OLA path — and since the GA2 prime fix the
@@ -1385,54 +1384,6 @@ static void testPreserveUnityEngineVoiceSpeaksImmediately() {
CHECK(lateUp > 0.9); // and no gaps later either (splices land in real history)
}
// The PREVIEW CARD at unity speaks on frame ONE — the FA1 latency fix, now scoped to the card.
static void testPreviewCardUnitySpeaksImmediately() {
SampleData s = dcSample(2000, 60);
s.play.pitchEngine = PitchEngine::Preserve;
s.play.adsr = flatAdsr();
Keymap km = Keymap::singleSampleChromatic(std::move(s));
PreviewCard card(km, /*preserveWindowFrames=*/512);
card.noteOn(60, 127); // at root: unity shift -> demoted inside the card, zero onset delay
std::vector<AudioSample> buf(4, 0.0f);
card.render(buf.data(), buf.size());
CHECK(approx(buf[0], 1.0, 1e-6)); // the DC sample, on the very first frame
}
// keyTrack 0 collapses EVERY note to unity — an off-root preview also demotes, speaks at once.
static void testPreviewCardKeyTrackZeroAlsoSpeaksImmediately() {
SampleData s = dcSample(2000, 60);
s.play.pitchEngine = PitchEngine::Preserve;
s.play.adsr = flatAdsr();
Keymap km = Keymap::singleSampleChromatic(std::move(s));
km.zones[0].keyTrack = 0.0; // no tracking: all keys play root pitch (unity)
PreviewCard card(km, /*preserveWindowFrames=*/512);
card.noteOn(67, 127);
std::vector<AudioSample> buf(4, 0.0f);
card.render(buf.data(), buf.size());
CHECK(approx(buf[0], 1.0, 1e-6));
}
// A TRANSPOSED preview keeps the genuine OLA path — the card's demotion is unity-ONLY. Since
// the GA2 prime fix onset silence can no longer distinguish the paths (both speak on frame 0),
// so prove it by DURATION: a Preserve Trigger at 100% of a 1000-frame sample holds ~1000
// output frames at +12 st, where a Varispeed demotion would run off in ~500.
static void testPreviewCardTransposedKeepsShifter() {
SampleData s = preserveTriggerSample(1000, 1.0);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
PreviewCard card(km, /*preserveWindowFrames=*/512);
card.noteOn(72, 127); // +12 semitones: a real shift, NOT demoted
std::vector<AudioSample> buf(1, 0.0f);
std::size_t len = 0;
for (std::size_t f = 0; f < 2000; ++f) {
buf[0] = 0.0f;
card.render(buf.data(), 1);
if (card.active()) len = f + 1;
else break;
}
CHECK(len > 700); // duration held (Preserve) — a Varispeed demotion would stop near 500
CHECK(len < 1300); // ...and not doubled either (sanity)
}
// A TRANSPOSED Preserve note keeps the genuine OLA path — and since the GA2 prime fix that
// path has NO onset cost: the ring is primed with the first window of real source, so a
// transposed voice opens at full level on frame 0 (the DAW "zero-sample gaps in the first few
@@ -1829,19 +1780,6 @@ static void testAllNotesOffClearsMonoHeldStack() {
CHECK(eng.activeVoiceCount() == 0);
}
// MAJOR-2 companion: the preview card's unconditional releaseAll (the panic peer).
static void testPreviewCardReleaseAll() {
Keymap km = twoLevelKeymap();
PreviewCard card(km);
card.noteOn(70, 127);
CHECK(card.active());
card.releaseAll(); // no note argument: quiets whatever rings
std::vector<AudioSample> buf(1, 0.0f);
card.render(buf.data(), buf.size());
CHECK(approx(buf[0], 0.0, 1e-9));
CHECK(!card.active());
}
// CC 120 (allSoundsOff) hard-stops a ringing TRIGGER one-shot that would otherwise play to
// its bounded playEnd (minutes on a full-length capture). This is the primary repro: allNotesOff
// (CC 123) is a NO-OP on a Trigger voice — only allSoundsOff provides the actual hard stop.
@@ -2211,46 +2149,30 @@ static void testZeroAttackGateRetrigNoStep() {
CHECK(maxDeltaAcross(static_cast<double>(pre.back()), post) < 0.08);
}
// GA2 — the PREVIEW click: a preview fired over a RINGING preview replaces the card's single
// voice — the same hard cut as a mono takeover, previously entirely un-declicked (the card
// never passed the opt-in). With takeoverDeclick opted in at construction, the
// replace-restart runs the same difference-seeded ramp: boundary continuity + bounded slope.
static void testPreviewRetriggerDeclicksRestart() {
// PREVIEW re-audition declick (GA2, re-homed on the engine): the preview is now a plain
// engine noteOn at the root, so re-auditioning while the first preview still rings is a
// POLY AT-CAP STEAL when the pool is saturated — with takeoverDeclick opted in (the shell's
// product default), the restart runs the same difference-seeded ramp: boundary continuity
// + bounded slope. Same physics testPolyStealDeclicksRestart pins; this pins it at the
// preview's exact shape (same note, root, full pool of 1).
static void testPreviewReauditionDeclicksViaEngineSteal() {
SampleData s = sineSample(48000, 100.0, 60); // default ADSR: instant unity (worst case)
Keymap km = Keymap::singleSampleChromatic(std::move(s));
PreviewCard card(km, 0, /*takeoverDeclick=*/true);
VoiceEngine eng(1, km, 0, 0, VoiceMode::Poly, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
card.noteOn(60, 127);
std::vector<AudioSample> pre(120, 0.0f);
card.render(pre.data(), pre.size()); // ringing at ~ the sine peak
eng.noteOn(60, 127); // preview: root note through the pool
std::vector<AudioSample> pre;
eng.render(pre, 120); // ringing at ~ the sine peak
CHECK(pre.back() > 0.99f);
card.noteOn(60, 127); // audition again: replaces the ringing preview
std::vector<AudioSample> post(400, 0.0f);
card.render(post.data(), post.size());
eng.noteOn(60, 127); // audition again: at-cap steal restart
std::vector<AudioSample> post;
eng.render(post, 400);
CHECK(std::fabs(static_cast<double>(post[0]) - static_cast<double>(pre.back())) < 0.01);
CHECK(maxDeltaAcross(static_cast<double>(pre.back()), post) < 0.08);
}
// The preview declick is OPT-IN: a default-constructed card keeps the pre-fix hard cut
// byte-identical — the replace-restart's first frame is the new voice's raw onset (sin(0)
// == 0 here), pinning the pure-core regression baseline the shell layers the opt-in above.
static void testPreviewDefaultOffKeepsHardCutBaseline() {
SampleData s = sineSample(48000, 100.0, 60);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
PreviewCard card(km); // declick NOT opted in
card.noteOn(60, 127);
std::vector<AudioSample> pre(120, 0.0f);
card.render(pre.data(), pre.size());
CHECK(pre.back() > 0.99f);
card.noteOn(60, 127);
std::vector<AudioSample> post(1, 0.0f);
card.render(post.data(), post.size());
CHECK(approx(post[0], 0.0, 1e-4)); // the raw hard cut: new onset, no ramp
}
// 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
@@ -2309,50 +2231,26 @@ static void testOverCapChordStealsExactlyOne() {
CHECK(eng.activeVoiceCount() == 0); // the stolen-into 4th note releases last
}
// PREVIEW-CARD ISOLATION: the card never consumes a pool voice, a FULL pool never drops a
// preview, and pool stealing never touches the ringing preview. The two sum independently.
static void testPreviewCardIsolatedFromPool() {
// PREVIEW OBEYS VOICING (the PreviewCard-isolation reversal): a preview is a plain engine
// noteOn, so it is a REAL pool voice — a full pool STEALS for it (never a parallel voice on
// top), it counts toward activeVoiceCount, and its note-off releases through the normal
// path. This is the processor's mailbox-drain contract, pinned in the pure core.
static void testPreviewNoteObeysVoicing() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
VoiceEngine eng(2, km);
PreviewCard card(km);
eng.noteOn(60, 127);
eng.noteOn(62, 127); // the pool is now FULL
card.noteOn(64, 127); // preview fires anyway — its own voice
CHECK(eng.activeVoiceCount() == 2); // no pool voice consumed
CHECK(card.active());
std::vector<AudioSample> buf(4, 0.0f);
eng.render(buf.data(), buf.size()); // engine sums 2 voices...
card.render(buf.data(), buf.size()); // ...card ADDS its own on top
CHECK(approx(buf[0], 3.0, 1e-6));
eng.noteOn(64, 127); // pool steals INTERNALLY...
eng.noteOn(64, 127); // the preview note: steals — no third voice
CHECK(eng.activeVoiceCount() == 2);
CHECK(card.active()); // ...the preview is untouched
card.noteOff(64); // flat release: card gates off instantly
std::vector<AudioSample> buf(4, 0.0f);
eng.render(buf.data(), buf.size());
CHECK(approx(buf[0], 2.0, 1e-6)); // TWO voices sum — never 3 (no side-car)
eng.noteOff(64); // flat release: the preview gates off
std::vector<AudioSample> buf2(1, 0.0f);
card.render(buf2.data(), buf2.size());
CHECK(approx(buf2[0], 0.0, 1e-9));
CHECK(eng.activeVoiceCount() == 2); // and the pool never noticed
}
// The card is ONE voice: a new preview replaces the ringing one, a STALE note-off (for the
// replaced note) is a no-op, and an out-of-zone preview is a defined no-play.
static void testPreviewCardReplaceStaleOffAndOutOfZone() {
Keymap km = twoLevelKeymap(); // zones [40,59] + [60,80]
PreviewCard card(km);
card.noteOn(50, 127);
card.noteOn(70, 127); // replaces the first preview
std::vector<AudioSample> buf(1, 0.0f);
card.render(buf.data(), buf.size());
CHECK(approx(buf[0], 0.75, 1e-6)); // zone B is what rings
card.noteOff(50); // STALE off for the replaced note: no-op
CHECK(card.active());
card.noteOff(70); // the sounding note's off gates it (release 0)
std::vector<AudioSample> buf2(1, 0.0f);
card.render(buf2.data(), buf2.size());
CHECK(approx(buf2[0], 0.0, 1e-9));
card.noteOn(20, 127); // out of every zone: defined no-play
CHECK(!card.active());
eng.render(buf2.data(), buf2.size());
CHECK(eng.activeVoiceCount() == 1); // the surviving MIDI note still rings
CHECK(approx(buf2[0], 1.0, 1e-6));
}
// GA2 — bounded-blend overshoot regression: mid-ramp output must stay within full scale.
@@ -2591,12 +2489,9 @@ int main() {
testPreserveGateStereoLoopComposes();
testPreserveVoiceCap();
// FA1 (re-scoped by Phase S) — the unity bypass is preview-card-only; the engine keeps a
// uniform Preserve onset. Velocity under Preserve unchanged.
// FA1 postscript — the unity demotion is gone with the PreviewCard; the engine keeps a
// uniform Preserve onset at every note. Velocity under Preserve unchanged.
testPreserveUnityEngineVoiceSpeaksImmediately();
testPreviewCardUnitySpeaksImmediately();
testPreviewCardKeyTrackZeroAlsoSpeaksImmediately();
testPreviewCardTransposedKeepsShifter();
testPreserveTransposedVoiceSpeaksImmediately();
testPreserveUnityVoiceCountsTowardCap();
testVelocityCurveAppliesUnderPreserve();
@@ -2605,7 +2500,8 @@ int main() {
testPerZoneAdsrReachesVoiceEnvelope();
testZeroAdsrIsInstantSustain();
// Phase S — voice count, MONO mode (held stack + Retrigger/Legato), preview card.
// Phase S — voice count, MONO mode (held stack + Retrigger/Legato); preview as a real
// pool voice (PreviewCard retired — preview routes through the engine).
testMonoLastNotePriorityAndFallback();
testMonoReleaseOfLowerHeldNoteIsInaudible();
testMonoRepressHeldNoteMovesToTop();
@@ -2621,7 +2517,6 @@ int main() {
testMonoLegatoTriggerHeldKeyStillRetunes();
testAllNotesOffReleasesPolyVoices();
testAllNotesOffClearsMonoHeldStack();
testPreviewCardReleaseAll();
testAllSoundsOffStopsTriggerOneShot();
testAllNotesOffStillReleasesGateVoices();
testMonoLegatoSameNoteRepressReattacks();
@@ -2636,11 +2531,9 @@ int main() {
testZeroAttackTakeoverNeverExceedsFullScale();
testMonoRetrigTriggerZoneDeclicksRestart();
testZeroAttackGateRetrigNoStep();
testPreviewRetriggerDeclicksRestart();
testPreviewDefaultOffKeepsHardCutBaseline();
testPreviewReauditionDeclicksViaEngineSteal();
testDeclickBoundedBlendNoOvershoot();
testPreviewCardIsolatedFromPool();
testPreviewCardReplaceStaleOffAndOutOfZone();
testPreviewNoteObeysVoicing();
// GA3 — Preserve tail wind-down (writer freeze at source exhaustion).
testPreserveTailFinalWindowGapFree();