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:
@@ -28,6 +28,7 @@ public:
|
|||||||
bool reloadForGUI() override;
|
bool reloadForGUI() override;
|
||||||
bool attachView(void* parentWindowHandle) override;
|
bool attachView(void* parentWindowHandle) override;
|
||||||
void closeGUI() override;
|
void closeGUI() override;
|
||||||
|
void setChannel(uint32_t ch) { channel_ = ch; }
|
||||||
bool needsReload() const override { return hasAttachedOnce_ && !guiAttached_; }
|
bool needsReload() const override { return hasAttachedOnce_ && !guiAttached_; }
|
||||||
void setReloading(bool on) override { reloading_ = on; }
|
void setReloading(bool on) override { reloading_ = on; }
|
||||||
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
|
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
|
||||||
@@ -42,6 +43,7 @@ private:
|
|||||||
bool guiAttached_;
|
bool guiAttached_;
|
||||||
bool hasAttachedOnce_;
|
bool hasAttachedOnce_;
|
||||||
bool reloading_; // guarded by InstrumentEngineManager::mu_ (set via setReloading)
|
bool reloading_; // guarded by InstrumentEngineManager::mu_ (set via setReloading)
|
||||||
|
uint32_t channel_ = 0;
|
||||||
bool reload();
|
bool reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
|||||||
loaded = static_cast<SfizzInstrument*>(inst.get())->loadSfzFile(path, sampleRate);
|
loaded = static_cast<SfizzInstrument*>(inst.get())->loadSfzFile(path, sampleRate);
|
||||||
else if (type == InstrumentType::VST3)
|
else if (type == InstrumentType::VST3)
|
||||||
loaded = static_cast<Vst3Instrument*>(inst.get())->loadPlugin(path, sampleRate);
|
loaded = static_cast<Vst3Instrument*>(inst.get())->loadPlugin(path, sampleRate);
|
||||||
|
if (type == InstrumentType::VST3)
|
||||||
|
static_cast<Vst3Instrument*>(inst.get())->setChannel(channel);
|
||||||
if (!loaded) return false;
|
if (!loaded) return false;
|
||||||
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
|
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
|
||||||
if (!inst->init(sampleRate, blockSize)) return false;
|
if (!inst->init(sampleRate, blockSize)) return false;
|
||||||
|
|||||||
@@ -133,7 +133,11 @@ struct Vst3HostState {
|
|||||||
IPtr<IEditController> controller;
|
IPtr<IEditController> controller;
|
||||||
IPtr<HostApplication> hostApp;
|
IPtr<HostApplication> hostApp;
|
||||||
HostProcessData processData;
|
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;
|
ParameterChanges paramChanges;
|
||||||
ProcessContext processContext;
|
ProcessContext processContext;
|
||||||
IPtr<IPlugView> view;
|
IPtr<IPlugView> view;
|
||||||
@@ -141,6 +145,14 @@ struct Vst3HostState {
|
|||||||
HostComponentHandler componentHandler;
|
HostComponentHandler componentHandler;
|
||||||
int32 outputChannels = 2;
|
int32 outputChannels = 2;
|
||||||
bool controllerIsComponent = false; // single-component plugin: controller == component
|
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
|
// 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;
|
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->module = std::move(module);
|
||||||
s->component = std::move(component);
|
s->component = std::move(component);
|
||||||
s->controller = std::move(controller);
|
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.tuning = 0.f;
|
||||||
e.noteOn.velocity = velocity;
|
e.noteOn.velocity = velocity;
|
||||||
e.noteOn.length = 0;
|
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);
|
s->eventList.addEvent(e);
|
||||||
|
std::cerr << "[midi] pid=" << (int)GetCurrentProcessId() << " ON ch=" << channel
|
||||||
|
<< " p=" << pitch << " id=" << id << " v=" << velocity << std::endl;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
|
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
|
#ifndef HAVE_VST3SDK
|
||||||
return;
|
return;
|
||||||
#else
|
#else
|
||||||
auto* s = static_cast<Vst3HostState*>(state_);
|
auto* s = static_cast<Vst3HostState*>(state_);
|
||||||
if (!s || !s->component) return;
|
if (!s || !s->component) return;
|
||||||
|
// 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 = {};
|
Event e = {};
|
||||||
e.busIndex = 0;
|
e.busIndex = 0;
|
||||||
e.sampleOffset = (int32)sampleOffset;
|
e.sampleOffset = (int32)sampleOffset;
|
||||||
e.ppqPosition = 0;
|
e.ppqPosition = 0;
|
||||||
e.flags = Event::kIsLive;
|
e.flags = Event::kIsLive;
|
||||||
e.type = Event::kNoteOffEvent;
|
e.type = Event::kNoteOffEvent;
|
||||||
e.noteOff.channel = 0; // Force to MIDI Channel 1 (0) for VSTi compatibility
|
e.noteOff.channel = 0;
|
||||||
e.noteOff.pitch = (int16)pitch;
|
e.noteOff.pitch = (int16)pitch;
|
||||||
e.noteOff.velocity = 0.f;
|
e.noteOff.velocity = 0.f;
|
||||||
// noteId phai MATCH noteOn (0): Nexus/DUNE3 track notes bang id, -1
|
e.noteOff.noteId = id;
|
||||||
// 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;
|
e.noteOff.tuning = 0.f;
|
||||||
s->eventList.addEvent(e);
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,7 +697,7 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
|
|||||||
(s->processContext.tempo / 60.0);
|
(s->processContext.tempo / 60.0);
|
||||||
|
|
||||||
static uint32_t dbgN = 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
|
// VST3: host buffers must be zeroed (silence) before process — plugins
|
||||||
// that skip output leave garbage otherwise (EZkeys 2 -> huge noise).
|
// that skip output leave garbage otherwise (EZkeys 2 -> huge noise).
|
||||||
if (s->processData.numOutputs > 0) {
|
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);
|
const Event* e0 = s->eventList.getEventByIndex(0);
|
||||||
evt = e0 ? (int)e0->type : -2;
|
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;
|
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
|
<< " pr=" << (int)pr << " nOut=" << s->processData.numOutputs
|
||||||
<< " mx=" << mx << " ts=" << s->processContext.projectTimeSamples
|
<< " mx=" << mx << " ts=" << s->processContext.projectTimeSamples
|
||||||
<< " nch=" << (s->processData.numOutputs > 0 ? s->processData.outputs[0].numChannels : -1) << std::endl;
|
<< " nch=" << (s->processData.numOutputs > 0 ? s->processData.outputs[0].numChannels : -1) << std::endl;
|
||||||
|
|||||||
Reference in New Issue
Block a user