fix(voice): CC 120 hard-stops voices (incl. Trigger); Mono sizes voices_ to 1; comments corrected
CC 120 -> allSoundsOff/hardStop (immediate silence, stops Trigger one-shots); CC 123 -> allNotesOff/releaseAll (release, unchanged). Mono VoiceEngine sizes voices_ to 1 structurally. Legato same-note re-press edge documented. Three new tests.
This commit is contained in:
@@ -1793,6 +1793,65 @@ static void testPreviewCardReleaseAll() {
|
||||
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.
|
||||
static void testAllSoundsOffStopsTriggerOneShot() {
|
||||
// A Trigger sample with a very long play length (all-1 DC, flat velocity). After noteOn the
|
||||
// voice is active and ringing; allSoundsOff must silence it immediately.
|
||||
SampleData s = dcLevelSample(200000, 1.0f, 60);
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
s.play.trigger.lengthFraction = 1.0; // full length — would ring for 200000 frames
|
||||
Keymap km = Keymap::singleSampleChromatic(std::move(s));
|
||||
VoiceEngine eng(1, km);
|
||||
eng.noteOn(60, 127);
|
||||
CHECK(eng.activeVoiceCount() == 1);
|
||||
// CC 123 (release) must be a NO-OP on a Trigger voice — the one-shot plays through.
|
||||
eng.allNotesOff();
|
||||
CHECK(eng.activeVoiceCount() == 1); // still ringing (Trigger ignores release)
|
||||
std::vector<AudioSample> out;
|
||||
eng.render(out, 1);
|
||||
CHECK(out[0] > 0.5f); // still sounding
|
||||
// CC 120 (hard-stop) must silence it instantly.
|
||||
eng.allSoundsOff();
|
||||
CHECK(eng.activeVoiceCount() == 0); // immediately idle
|
||||
out.clear();
|
||||
eng.render(out, 1);
|
||||
CHECK(approx(out[0], 0.0, 1e-9)); // silent
|
||||
}
|
||||
|
||||
// CC 123 (allNotesOff) still releases Gate voices — the existing release behavior is unchanged.
|
||||
static void testAllNotesOffStillReleasesGateVoices() {
|
||||
SampleData s = dcLevelSample(200000, 1.0f, 60);
|
||||
// Default Gate mode, instant release (releaseFrames 0).
|
||||
Keymap km = Keymap::singleSampleChromatic(std::move(s));
|
||||
VoiceEngine eng(4, km);
|
||||
eng.noteOn(60, 127);
|
||||
eng.noteOn(62, 127);
|
||||
CHECK(eng.activeVoiceCount() == 2);
|
||||
eng.allNotesOff();
|
||||
std::vector<AudioSample> out;
|
||||
eng.render(out, 1);
|
||||
CHECK(approx(out[0], 0.0, 1e-9)); // Gate with 0-release: instant silence
|
||||
CHECK(eng.activeVoiceCount() == 0);
|
||||
}
|
||||
|
||||
// MONO LEGATO same-note re-press (one-held-note edge case): with only that note on the
|
||||
// stack, heldCount_ after the re-push is 1 (not >= 2), so it falls through to re-attack
|
||||
// rather than retune. This is the correct fresh-phrase behavior documented in the comment.
|
||||
static void testMonoLegatoSameNoteRepressReattacks() {
|
||||
SampleData s = rampSample(200000, 60);
|
||||
Keymap km = Keymap::singleSampleChromatic(std::move(s));
|
||||
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato);
|
||||
eng.noteOn(60, 127); // first press; read starts at 0
|
||||
std::vector<AudioSample> out;
|
||||
eng.render(out, 10); // advance the read head to ~10
|
||||
// Re-press the SAME note while it is the only held note: heldCount_ after removeHeld+push = 1
|
||||
// -> does NOT satisfy heldCount_ >= 2 -> re-attack (not a legato retune).
|
||||
eng.noteOn(60, 127);
|
||||
CHECK(approx(probeFrame(eng), 0.0, 1e-4)); // read RESTARTED at 0 (re-attack, not retune)
|
||||
}
|
||||
|
||||
// GREEN: out-of-range notes are rejected at BOTH mono entry points. The held stack stores
|
||||
// uint8, so an unguarded off for note 256 (== 0 mod 256) would alias-evict held note 0 —
|
||||
// losing its fallback. Note-ons out of [0,127] are a defined no-play.
|
||||
@@ -1970,6 +2029,9 @@ int main() {
|
||||
testAllNotesOffReleasesPolyVoices();
|
||||
testAllNotesOffClearsMonoHeldStack();
|
||||
testPreviewCardReleaseAll();
|
||||
testAllSoundsOffStopsTriggerOneShot();
|
||||
testAllNotesOffStillReleasesGateVoices();
|
||||
testMonoLegatoSameNoteRepressReattacks();
|
||||
testMonoOutOfRangeNotesRejected();
|
||||
testVoiceCountBoundsPolyphony();
|
||||
testPreviewCardIsolatedFromPool();
|
||||
|
||||
Reference in New Issue
Block a user