fix(bridge): stop leaves sound looping — EventList maxSize=50 drops STOP sweep note-offs
Root cause: Steinberg EventList defaults to maxSize=50 events. The STOP all-notes-off sweep queues 128 note-offs per channel (16ch x 128 pitch), so addEvent beyond 50 fails silently and the plugin never receives the remaining note-offs — voices keep sustaining and the transport loops forever until the bridge process is killed. Fix: raise eventList.setMaxSize(4096) in loadPlugin. Also assign unique noteIds per overlapping note-on (stack per pitch) so note-off releases every voice at that pitch, and log addEvent result + channel in debug output for verification.
This commit is contained in:
@@ -168,6 +168,8 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
||||
loaded = static_cast<SfizzInstrument*>(inst.get())->loadSfzFile(path, sampleRate);
|
||||
else if (type == InstrumentType::VST3)
|
||||
loaded = static_cast<Vst3Instrument*>(inst.get())->loadPlugin(path, sampleRate);
|
||||
if (type == InstrumentType::VST3)
|
||||
static_cast<Vst3Instrument*>(inst.get())->setChannel(channel);
|
||||
if (!loaded) return false;
|
||||
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
|
||||
if (!inst->init(sampleRate, blockSize)) return false;
|
||||
|
||||
@@ -133,7 +133,11 @@ struct Vst3HostState {
|
||||
IPtr<IEditController> controller;
|
||||
IPtr<HostApplication> hostApp;
|
||||
HostProcessData processData;
|
||||
EventList eventList;
|
||||
EventList eventList; // SDK default maxSize=50 — allNotesOff sweep adds
|
||||
// 128 note-offs per channel per stop; beyond 50
|
||||
// addEvent FAILS silently and the notes never
|
||||
// reach the plugin (voices keep sustaining).
|
||||
// Raised here via setMaxSize in loadPlugin.
|
||||
ParameterChanges paramChanges;
|
||||
ProcessContext processContext;
|
||||
IPtr<IPlugView> view;
|
||||
@@ -141,6 +145,14 @@ struct Vst3HostState {
|
||||
HostComponentHandler componentHandler;
|
||||
int32 outputChannels = 2;
|
||||
bool controllerIsComponent = false; // single-component plugin: controller == component
|
||||
// noteId per active voice. VST3 plugins match note-off to note-on by
|
||||
// (pitch, noteId); a constant id (old code used 0) means two overlapping
|
||||
// note-ons of the same pitch share one id, and a single note-off only
|
||||
// releases ONE of them — the other voice sustains forever (the "treo
|
||||
// loop" bug: sound keeps looping after STOP). Track a stack per pitch so
|
||||
// every overlapping voice gets a unique id and note-off releases them all.
|
||||
int32 nextNoteId = 1;
|
||||
std::vector<int32> noteIdsByPitch[128];
|
||||
};
|
||||
|
||||
// Resolve the plugin's ParamID for a MIDI CC / pitch-bend, preferring the
|
||||
@@ -350,6 +362,10 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
|
||||
}
|
||||
|
||||
std::cerr << "[dbg] loadPlugin: prepare OK, wiring state" << std::endl;
|
||||
// SDK EventList default maxSize=50 is far too small for the STOP
|
||||
// all-notes-off sweep (128 note-offs per channel). addEvent beyond the
|
||||
// cap fails SILENTLY → notes never released → stuck sound loop.
|
||||
s->eventList.setMaxSize(4096);
|
||||
s->module = std::move(module);
|
||||
s->component = std::move(component);
|
||||
s->controller = std::move(controller);
|
||||
@@ -403,33 +419,68 @@ void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, ui
|
||||
e.noteOn.tuning = 0.f;
|
||||
e.noteOn.velocity = velocity;
|
||||
e.noteOn.length = 0;
|
||||
e.noteOn.noteId = 0; // some plugins (DUNE3) track notes by id; -1 may be ignored
|
||||
// Unique noteId per voice: overlapping note-ons of the same pitch MUST
|
||||
// carry distinct ids, otherwise one note-off cannot release all voices
|
||||
// (Nexus/DUNE3 keep the extra voice forever → sound loops after STOP).
|
||||
int32 id = s->nextNoteId++;
|
||||
if (id <= 0) { id = 1; s->nextNoteId = 2; } // wrap: never reuse 0
|
||||
if (pitch < 128) s->noteIdsByPitch[pitch].push_back(id);
|
||||
e.noteOn.noteId = id;
|
||||
s->eventList.addEvent(e);
|
||||
std::cerr << "[midi] pid=" << (int)GetCurrentProcessId() << " ON ch=" << channel
|
||||
<< " p=" << pitch << " id=" << id << " v=" << velocity << std::endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
|
||||
(void)channel; (void)pitch; (void)sampleOffset;
|
||||
(void)channel; (void)sampleOffset;
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
if (!s || !s->component) return;
|
||||
Event e = {};
|
||||
e.busIndex = 0;
|
||||
e.sampleOffset = (int32)sampleOffset;
|
||||
e.ppqPosition = 0;
|
||||
e.flags = Event::kIsLive;
|
||||
e.type = Event::kNoteOffEvent;
|
||||
e.noteOff.channel = 0; // Force to MIDI Channel 1 (0) for VSTi compatibility
|
||||
e.noteOff.pitch = (int16)pitch;
|
||||
e.noteOff.velocity = 0.f;
|
||||
// noteId phai MATCH noteOn (0): Nexus/DUNE3 track notes bang id, -1
|
||||
// khong khop voi note-on -> plugin giu note vo han (am thanh khong dung
|
||||
// cho den khi kill bridge).
|
||||
e.noteOff.noteId = 0;
|
||||
e.noteOff.tuning = 0.f;
|
||||
s->eventList.addEvent(e);
|
||||
// Release EVERY active voice on this pitch (each had a distinct noteId).
|
||||
// MIDI note-off semantics: all voices at that pitch stop. Without this,
|
||||
// a second overlapping voice (stack depth >1) keeps sustaining.
|
||||
if (pitch < 128 && !s->noteIdsByPitch[pitch].empty()) {
|
||||
for (int32 id : s->noteIdsByPitch[pitch]) {
|
||||
Event e = {};
|
||||
e.busIndex = 0;
|
||||
e.sampleOffset = (int32)sampleOffset;
|
||||
e.ppqPosition = 0;
|
||||
e.flags = Event::kIsLive;
|
||||
e.type = Event::kNoteOffEvent;
|
||||
e.noteOff.channel = 0;
|
||||
e.noteOff.pitch = (int16)pitch;
|
||||
e.noteOff.velocity = 0.f;
|
||||
e.noteOff.noteId = id;
|
||||
e.noteOff.tuning = 0.f;
|
||||
s->eventList.addEvent(e);
|
||||
}
|
||||
std::string ids;
|
||||
for (int32 id : s->noteIdsByPitch[pitch]) { if (!ids.empty()) ids += ","; ids += std::to_string(id); }
|
||||
std::cerr << "[midi] pid=" << (int)GetCurrentProcessId() << " OFF ch=" << channel
|
||||
<< " p=" << pitch << " ids=[" << ids << "]" << std::endl;
|
||||
s->noteIdsByPitch[pitch].clear();
|
||||
} else {
|
||||
// No tracked voice: send a best-effort note-off with the last id
|
||||
// (id 0 now always means "untracked" — plugins may still release by
|
||||
// pitch). Never blocks the audio thread.
|
||||
Event e = {};
|
||||
e.busIndex = 0;
|
||||
e.sampleOffset = (int32)sampleOffset;
|
||||
e.ppqPosition = 0;
|
||||
e.flags = Event::kIsLive;
|
||||
e.type = Event::kNoteOffEvent;
|
||||
e.noteOff.channel = 0;
|
||||
e.noteOff.pitch = (int16)pitch;
|
||||
e.noteOff.velocity = 0.f;
|
||||
e.noteOff.noteId = 0;
|
||||
e.noteOff.tuning = 0.f;
|
||||
tresult ar = s->eventList.addEvent(e);
|
||||
std::cerr << "[midi] pid=" << (int)GetCurrentProcessId() << " OFF ch=" << channel
|
||||
<< " p=" << pitch << " untracked(0) add=" << (int)ar << std::endl;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -646,7 +697,7 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
|
||||
(s->processContext.tempo / 60.0);
|
||||
|
||||
static uint32_t dbgN = 0;
|
||||
static int dbgMaxShown = 0;
|
||||
static int dbgMaxShown = 0; // ev>0 blocks printed (raised: sweep must be visible)
|
||||
// VST3: host buffers must be zeroed (silence) before process — plugins
|
||||
// that skip output leave garbage otherwise (EZkeys 2 -> huge noise).
|
||||
if (s->processData.numOutputs > 0) {
|
||||
@@ -682,9 +733,9 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
|
||||
const Event* e0 = s->eventList.getEventByIndex(0);
|
||||
evt = e0 ? (int)e0->type : -2;
|
||||
}
|
||||
if ((++dbgN % 250) == 0 || (evc > 0 && dbgMaxShown < 30)) {
|
||||
if ((++dbgN % 250) == 0 || (evc > 0 && dbgMaxShown < 5000)) {
|
||||
if (evc > 0) ++dbgMaxShown;
|
||||
std::cerr << "[dbg] n=" << numSamples << " ev=" << evc << " evt=" << evt
|
||||
std::cerr << "[dbg] pid=" << (int)GetCurrentProcessId() << " ch=" << channel_ << " n=" << numSamples << " ev=" << evc << " evt=" << evt
|
||||
<< " pr=" << (int)pr << " nOut=" << s->processData.numOutputs
|
||||
<< " mx=" << mx << " ts=" << s->processContext.projectTimeSamples
|
||||
<< " nch=" << (s->processData.numOutputs > 0 ? s->processData.outputs[0].numChannels : -1) << std::endl;
|
||||
|
||||
Reference in New Issue
Block a user