From ea0e4719fcb20db268f2d784b752855e6d713634 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:00:26 +0200 Subject: [PATCH] Release held VST3 notes on reset instead of discarding them Fixes #441 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 --- crates/vst3-host/src/plugin.rs | 8 +++++++- crates/vst3-host/tests/fixture_e2e.rs | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/vst3-host/src/plugin.rs b/crates/vst3-host/src/plugin.rs index 2975465f..20d5fa6f 100644 --- a/crates/vst3-host/src/plugin.rs +++ b/crates/vst3-host/src/plugin.rs @@ -213,8 +213,14 @@ impl Plugin for Vst3Plugin { } fn reset(&mut self) { - self.notes.clear(); + // Keep pending releases and release whatever is still held, so the processor's voices stop + // on the next block instead of sounding on while the host forgets them. + self.notes.retain(|e| matches!(e, NoteEvent::Off { .. })); + for e in &mut self.notes { + e.set_time(0); + } self.controls.clear(); + self.all_notes_off(); self.held = [false; 128]; } diff --git a/crates/vst3-host/tests/fixture_e2e.rs b/crates/vst3-host/tests/fixture_e2e.rs index 5efd33cc..9c87b8bb 100644 --- a/crates/vst3-host/tests/fixture_e2e.rs +++ b/crates/vst3-host/tests/fixture_e2e.rs @@ -144,6 +144,29 @@ fn synth_plays_notes_at_their_offsets() { assert!(mono[0].iter().all(|&x| (x - 1.0).abs() < 1e-6), "the note is still held"); } +#[test] +fn reset_after_all_notes_off_still_releases_held_voices() { + let mut p = soundcraft_vst3_host::create(&id_of("Fixture Synth")).unwrap(); + p.prepare(48_000.0, 64, 2); + p.note_on(0, 60, 127); + let mut io = vec![vec![0.0f32; 64], vec![0.0f32; 64]]; + p.process(&mut io, 64); + assert!((io[0][63] - 1.0).abs() < 1e-6, "the note sounds"); + // Mixer::reset calls these two in a row on an instrument (stop / seek). + p.all_notes_off(); + p.reset(); + let mut io = vec![vec![9.0f32; 64], vec![9.0f32; 64]]; + p.process(&mut io, 64); + assert!(io[0].iter().all(|x| x.abs() < 1e-6), "voice still sounding after reset: {}", io[0][63]); + // A reset alone also releases what the host still holds. + p.note_on(0, 61, 127); + p.process(&mut io, 64); + assert!((io[0][63] - 1.0).abs() < 1e-6); + p.reset(); + p.process(&mut io, 64); + assert!(io[0].iter().all(|x| x.abs() < 1e-6), "voice still sounding after reset: {}", io[0][63]); +} + #[test] fn many_instances_create_and_drop_cleanly() { let (g, s) = (id_of("Fixture Gain"), id_of("Fixture Synth"));