fix(vst): reopen heal without the editor — bounded main-thread retry timer, re-armable first-poll heal, unconditional preview-off consume + Mono/drain preview tests
This commit is contained in:
@@ -2253,6 +2253,48 @@ static void testPreviewNoteObeysVoicing() {
|
||||
CHECK(approx(buf2[0], 1.0, 1e-6));
|
||||
}
|
||||
|
||||
// PREVIEW JOINS THE MONO HELD STACK: a preview routed through the real note path is a mono
|
||||
// stack entry like any host note — it TAKES the single voice on press (last-note priority)
|
||||
// and its release FALLS BACK to the still-held host note instead of cutting to silence.
|
||||
// Pins the processor's mailbox-drain contract for Mono the way testPreviewNoteObeysVoicing
|
||||
// pins it for Poly steal.
|
||||
static void testPreviewNoteJoinsMonoHeldStack() {
|
||||
Keymap km = twoLevelKeymap();
|
||||
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
|
||||
CHECK(eng.noteOn(50, 127) == 0); // the host-MIDI note: zone A sounds
|
||||
CHECK(approx(probeFrame(eng), 0.25, 1e-6));
|
||||
CHECK(eng.noteOn(70, 127) == 0); // the preview press: TAKES the voice
|
||||
CHECK(eng.activeVoiceCount() == 1); // still mono — the preview is no side-car
|
||||
CHECK(approx(probeFrame(eng), 0.75, 1e-6));
|
||||
eng.noteOff(70); // preview release: FALLBACK to the held note
|
||||
CHECK(approx(probeFrame(eng), 0.25, 1e-6));
|
||||
eng.noteOff(50); // host note up: gate off (flat release = instant)
|
||||
CHECK(approx(probeFrame(eng), 0.0, 1e-9));
|
||||
CHECK(eng.activeVoiceCount() == 0);
|
||||
}
|
||||
|
||||
// PREVIEW NOTE-OFF ROUTES TO THE DRAIN ENGINE: mirror of process()'s dual-engine off
|
||||
// routing. A preview held across a reload leaves its ringing voice in the DISPLACED
|
||||
// (draining) snapshot while the fresh live engine has no voice at that pitch. The off is
|
||||
// sent to BOTH — exactly what the mailbox drain does: the fresh engine must safely no-op,
|
||||
// the drain engine must release its voice (otherwise the old-snapshot preview would
|
||||
// sustain until the next reload hard-cut it).
|
||||
static void testPreviewNoteOffRoutesToDrainEngine() {
|
||||
Keymap km = twoLevelKeymap();
|
||||
VoiceEngine drainEng(2, km); // was live when the preview fired
|
||||
VoiceEngine liveEng(2, km); // the post-reload fresh snapshot: no voices
|
||||
CHECK(drainEng.noteOn(70, 127) != VoiceEngine::kNoVoice);
|
||||
CHECK(approx(probeFrame(drainEng), 0.75, 1e-6)); // the preview rings in the old snapshot
|
||||
CHECK(liveEng.activeVoiceCount() == 0);
|
||||
// The preview release, drained to BOTH engines like a host note-off:
|
||||
liveEng.noteOff(70);
|
||||
drainEng.noteOff(70);
|
||||
CHECK(approx(probeFrame(liveEng), 0.0, 1e-9)); // fresh engine: safe no-op, stays silent
|
||||
CHECK(liveEng.activeVoiceCount() == 0);
|
||||
CHECK(approx(probeFrame(drainEng), 0.0, 1e-9)); // flat release: gates off NOW
|
||||
CHECK(drainEng.activeVoiceCount() == 0); // the old-snapshot voice released
|
||||
}
|
||||
|
||||
// GA2 — bounded-blend overshoot regression: mid-ramp output must stay within full scale.
|
||||
//
|
||||
// Construction of the worst case (§1 reviewer finding): retrig a sine at a point where the
|
||||
@@ -2534,6 +2576,8 @@ int main() {
|
||||
testPreviewReauditionDeclicksViaEngineSteal();
|
||||
testDeclickBoundedBlendNoOvershoot();
|
||||
testPreviewNoteObeysVoicing();
|
||||
testPreviewNoteJoinsMonoHeldStack();
|
||||
testPreviewNoteOffRoutesToDrainEngine();
|
||||
|
||||
// GA3 — Preserve tail wind-down (writer freeze at source exhaustion).
|
||||
testPreserveTailFinalWindowGapFree();
|
||||
|
||||
Reference in New Issue
Block a user