// native_bridge/src/Vst3Instrument.cpp // VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/). // // Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every // method is a no-op so the bridge still builds for SF2/SF3/SFZ only. // // With the SDK: load the .vst3 module, create the component + edit controller, // connect them, process MIDI events + audio blocks, and attach the editor // view to a native HWND (openGUI) for the floating GUI (B9). #include "Vst3Instrument.h" #ifdef HAVE_VST3SDK #ifdef _WIN32 #include #endif #include "public.sdk/source/vst/hosting/module.h" #include "public.sdk/source/vst/hosting/hostclasses.h" #include "public.sdk/source/vst/hosting/processdata.h" #include "public.sdk/source/vst/hosting/eventlist.h" #include "public.sdk/source/vst/hosting/parameterchanges.h" #include "pluginterfaces/vst/ivstaudioprocessor.h" #include "pluginterfaces/vst/ivstcomponent.h" #include "pluginterfaces/vst/ivsteditcontroller.h" #include "pluginterfaces/vst/ivstmidicontrollers.h" #include "pluginterfaces/vst/ivstprocesscontext.h" #include "pluginterfaces/vst/ivstevents.h" #include "pluginterfaces/vst/ivstmessage.h" #include "pluginterfaces/gui/iplugview.h" #include #include #include #include #include #endif // --------------------------------------------------------------------------- // With-SDK implementation // --------------------------------------------------------------------------- #ifdef HAVE_VST3SDK namespace { using Steinberg::tresult; using Steinberg::kResultOk; using Steinberg::kResultTrue; using Steinberg::kResultFalse; using Steinberg::kNoInterface; using Steinberg::FUnknownPtr; using Steinberg::IPtr; using Steinberg::owned; using Steinberg::FIDString; using Steinberg::IPlugView; using Steinberg::kPlatformTypeHWND; using Steinberg::int16; using Steinberg::int32; using Steinberg::uint32; using Steinberg::uint16; using Steinberg::Vst::IComponent; using Steinberg::Vst::IEditController; using Steinberg::Vst::IAudioProcessor; using Steinberg::Vst::IComponentHandler; using Steinberg::IPluginBase; using Steinberg::Vst::IConnectionPoint; using Steinberg::Vst::IParameterChanges; using Steinberg::Vst::Event; using Steinberg::Vst::ProcessSetup; using Steinberg::Vst::ProcessContext; using Steinberg::Vst::HostProcessData; using Steinberg::Vst::EventList; using Steinberg::Vst::ParameterChanges; using Steinberg::Vst::HostApplication; using Steinberg::Vst::ParamID; using Steinberg::Vst::ParamValue; using Steinberg::Vst::IMidiMapping; using Steinberg::Vst::IParamValueQueue; using Steinberg::Vst::kNoParamId; using Steinberg::Vst::BusInfo; using Steinberg::Vst::kAudio; using Steinberg::Vst::kEvent; using Steinberg::Vst::kInput; using Steinberg::Vst::kOutput; using Steinberg::Vst::kRealtime; using Steinberg::Vst::kSample32; using Steinberg::Vst::kPitchBend; using Steinberg::Vst::CtrlNumber; // VST3 spec §MIDI: host-side tag scheme for MIDI CC / pitch bend / program // change parameters. NOT provided by the SDK (verified 3.8.1) — the host // defines them. Note: using-declaration of the real VST3 constants would not // compile; these are the spec values. constexpr ParamID kHostMidiCC = 0x1000; // + controller number constexpr ParamID kHostMidiPitchBend = 0x2000; // + channel constexpr ParamID kHostMidiProgramChange = 0x3000; // Minimal IComponentHandler so the plugin can inform the host of param edits. class HostComponentHandler : public IComponentHandler { public: tresult queryInterface(const char*, void** v) override { *v = nullptr; return kNoInterface; } Steinberg::uint32 addRef() override { return 1; } Steinberg::uint32 release() override { return 1; } tresult beginEdit(ParamID) override { return kResultOk; } tresult performEdit(ParamID, ParamValue) override { return kResultOk; } tresult endEdit(ParamID) override { return kResultOk; } tresult restartComponent(Steinberg::int32) override { return kResultOk; } }; // Minimal IPlugFrame so plugins can resize their editor view. class HostPlugFrame : public Steinberg::IPlugFrame { public: tresult queryInterface(const char*, void** v) override { *v = nullptr; return kNoInterface; } Steinberg::uint32 addRef() override { return 1; } Steinberg::uint32 release() override { return 1; } tresult resizeView(Steinberg::IPlugView* view, Steinberg::ViewRect* newSize) override { if (view && newSize) view->onSize(newSize); return kResultOk; } }; // All Steinberg SDK objects live here (pimpl — Vst3Instrument.h stays SDK-free). struct Vst3HostState { // module must be destroyed LAST: it owns the plugin factory that created // component/controller (member order ⇒ destroyed in reverse). VST3::Hosting::Module::Ptr module; IPtr component; IPtr controller; IPtr hostApp; HostProcessData processData; 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 view; HostPlugFrame plugFrame; 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 noteIdsByPitch[128]; }; // Resolve the plugin's ParamID for a MIDI CC / pitch-bend, preferring the // plugin's IMidiMapping assignment (audiohost/miditovst.h pattern), falling // back to the VST3 legacy tag scheme. Returns kNoParamId when unmapped. ParamID midiControllerTag(IPtr& controller, int16 channel, int16 ctrlNumber, ParamID legacyTag) { if (controller) { FUnknownPtr mm(controller.get()); ParamID tag = kNoParamId; if (mm && mm->getMidiControllerAssignment(0, channel, (CtrlNumber)ctrlNumber, tag) == kResultTrue && tag != kNoParamId) return tag; } return legacyTag; } } // namespace #endif Vst3Instrument::Vst3Instrument() : state_(nullptr), path_(), sampleRate_(44100.0), maxBlockSize_(256), loaded_(false), guiAttached_(false), hasAttachedOnce_(false), reloading_(false) {} Vst3Instrument::~Vst3Instrument() { #ifdef HAVE_VST3SDK if (!state_) return; closeGUI(); auto* s = static_cast(state_); if (s->component) { FUnknownPtr processor(s->component); if (processor) processor->setProcessing(false); s->component->setActive(false); s->component->terminate(); } // Single-component plugins: controller == component, already terminated. if (s->controller && !s->controllerIsComponent) s->controller->terminate(); s->processData.unprepare(); delete s; state_ = nullptr; #endif } bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) { #ifndef HAVE_VST3SDK (void)path; (void)sampleRate; return false; // vst3sdk submodule missing — VST3 disabled #else if (loaded_) return true; using namespace VST3::Hosting; std::string err; Module::Ptr module = Module::create(path, err); if (!module) { std::cerr << "[Vst3Instrument] Module::create failed: " << err << std::endl; return false; } const PluginFactory& factory = module->getFactory(); // Pick the first Audio Module class; prefer an Instrument subcategory. // classInfos() returns a TEMPORARY vector (by value) — never keep a // pointer into it; it dangles after the range-for and reading // chosen->ID()/name() is UB (crashed 0xC0000005). Copy instead. ClassInfo chosen; bool haveChosen = false; { auto infos = factory.classInfos(); for (const ClassInfo& ci : infos) { if (ci.category() != kVstAudioEffectClass) continue; if (!haveChosen) { chosen = ci; haveChosen = true; } if (ci.subCategoriesString().find("Instrument") != std::string::npos) { chosen = ci; break; } } } if (!haveChosen) { std::cerr << "[Vst3Instrument] no Audio Module class in " << path << std::endl; return false; } IPtr component = factory.createInstance(chosen.ID()); if (!component) { std::cerr << "[Vst3Instrument] createInstance failed" << std::endl; return false; } IPtr hostApp = owned(new HostApplication()); FUnknownPtr plugBase(component.get()); if (!plugBase || plugBase->initialize(hostApp) != kResultOk) { std::cerr << "[Vst3Instrument] component initialize failed" << std::endl; return false; } // Edit controller: either the component itself (single-component) or a // separate factory instance (plugprovider.cpp pattern). IPtr controller; bool isSingle = false; if (component->queryInterface(IEditController::iid, (void**)&controller) == kResultTrue) { isSingle = true; } else { Steinberg::TUID cid = {}; tresult cidRes = component->getControllerClassId(cid); if (cidRes == kResultTrue || cidRes == kResultOk) { controller = factory.createInstance(VST3::UID(cid)); if (controller) { FUnknownPtr ctrlBase(controller.get()); if (!ctrlBase || ctrlBase->initialize(hostApp) != kResultOk) controller = nullptr; } } } if (!controller) { std::cerr << "[Vst3Instrument] no edit controller for " << path << std::endl; return false; } // State allocated up-front so the component handler (which the controller // keeps a pointer to) lives in the final state object. std::unique_ptr s(new Vst3HostState()); s->controllerIsComponent = isSingle; controller->setComponentHandler(&s->componentHandler); // Connect component <-> controller for parameter sync (both directions). FUnknownPtr compCP(component); FUnknownPtr ctrlCP(controller); if (compCP && ctrlCP) { compCP->connect(ctrlCP); ctrlCP->connect(compCP); } // Activate all audio and event buses (MIDI & Audio) - required by VST3 specification. { int32 numAudioInputs = component->getBusCount(kAudio, kInput); for (int32 i = 0; i < numAudioInputs; ++i) { component->activateBus(kAudio, kInput, i, true); } int32 numAudioOutputs = component->getBusCount(kAudio, kOutput); for (int32 i = 0; i < numAudioOutputs; ++i) { component->activateBus(kAudio, kOutput, i, true); } int32 numEventInputs = component->getBusCount(kEvent, kInput); for (int32 i = 0; i < numEventInputs; ++i) { component->activateBus(kEvent, kInput, i, true); } int32 numEventOutputs = component->getBusCount(kEvent, kOutput); for (int32 i = 0; i < numEventOutputs; ++i) { component->activateBus(kEvent, kOutput, i, true); } } // Audio processor: setup → activate → start processing (audioclient.cpp). FUnknownPtr processor(component); if (!processor) { std::cerr << "[Vst3Instrument] no IAudioProcessor" << std::endl; return false; } ProcessSetup setup{kRealtime, kSample32, (int32)maxBlockSize_, sampleRate}; if (processor->setupProcessing(setup) != kResultOk) { std::cerr << "[Vst3Instrument] setupProcessing failed" << std::endl; return false; } if (component->setActive(true) != kResultOk) { std::cerr << "[Vst3Instrument] setActive failed" << std::endl; return false; } processor->setProcessing(true); // Build per-bus buffers sized maxBlockSize_ (HostProcessData owns them; // we must NOT override channelBuffers with external pointers — unprepare // would delete[] them). if (!s->processData.prepare(*component, (int32)maxBlockSize_, kSample32)) { std::cerr << "[Vst3Instrument] processData.prepare failed" << std::endl; return false; } // Count output channels of bus 0 (mono plugins duplicate ch0 → both outs). int32 outChannels = 2; if (component->getBusCount(kAudio, kOutput) > 0) { BusInfo bi = {}; if (component->getBusInfo(kAudio, kOutput, 0, bi) == kResultOk && bi.channelCount > 0) outChannels = bi.channelCount; } // 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); s->hostApp = std::move(hostApp); s->outputChannels = outChannels; s->controllerIsComponent = isSingle; s->processContext.sampleRate = sampleRate; s->processContext.tempo = 120.0; s->processContext.timeSigNumerator = 4; s->processContext.timeSigDenominator = 4; s->processContext.state = ProcessContext::kPlaying | ProcessContext::kTempoValid | ProcessContext::kTimeSigValid | ProcessContext::kProjectTimeMusicValid; state_ = s.release(); path_ = path; sampleRate_ = sampleRate; loaded_ = true; std::cout << "[Vst3Instrument] loaded " << path << " (" << chosen.name() << ") outCh=" << outChannels << std::endl; return true; #endif } bool Vst3Instrument::init(double sampleRate, uint32_t maxBlockSize) { sampleRate_ = sampleRate; maxBlockSize_ = maxBlockSize; return loaded_; } void Vst3Instrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) { (void)channel; (void)bank; (void)program; // ponytail: needs IEditController::setParamNormalized on program list params } void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) { (void)channel; (void)pitch; (void)velocity; (void)sampleOffset; #ifndef HAVE_VST3SDK return; #else auto* s = static_cast(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::kNoteOnEvent; e.noteOn.channel = 0; // Force to MIDI Channel 1 (0) for VSTi compatibility e.noteOn.pitch = (int16)pitch; e.noteOn.tuning = 0.f; e.noteOn.velocity = velocity; e.noteOn.length = 0; // 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)sampleOffset; #ifndef HAVE_VST3SDK return; #else auto* s = static_cast(state_); 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 = {}; 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 } void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) { (void)channel; (void)cc; (void)value; #ifndef HAVE_VST3SDK return; #else auto* s = static_cast(state_); if (!s || !s->controller) return; ParamID tag = midiControllerTag(s->controller, 0, (int16)cc, kHostMidiCC); ParamValue v = value / 127.0; // setParamNormalized covers single-component plugins; the param queue // reaches split plugins that read inputParameterChanges inside process(). s->controller->setParamNormalized(tag, v); int32 idx = 0; if (IParamValueQueue* q = s->paramChanges.addParameterData(tag, idx)) q->addPoint(0, v, idx); #endif } void Vst3Instrument::programChange(uint32_t channel, uint32_t program) { (void)channel; (void)program; #ifndef HAVE_VST3SDK return; #else auto* s = static_cast(state_); if (!s || !s->controller) return; ParamID tag = kHostMidiProgramChange; // ponytail: normalized value should be program/(count-1) from the program // list param stepCount; 127ths is a reasonable approximation. ParamValue v = (program & 0x7F) / 127.0; s->controller->setParamNormalized(tag, v); int32 idx = 0; if (IParamValueQueue* q = s->paramChanges.addParameterData(tag, idx)) q->addPoint(0, v, idx); #endif } void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) { (void)channel; (void)bend14; #ifndef HAVE_VST3SDK return; #else auto* s = static_cast(state_); if (!s || !s->controller) return; ParamID tag = midiControllerTag(s->controller, 0, (int16)kPitchBend, kHostMidiPitchBend); ParamValue v = bend14 / 16383.0; s->controller->setParamNormalized(tag, v); int32 idx = 0; if (IParamValueQueue* q = s->paramChanges.addParameterData(tag, idx)) q->addPoint(0, v, idx); #endif } bool Vst3Instrument::openGUI(void* parentWindowHandle) { #ifndef HAVE_VST3SDK (void)parentWindowHandle; return false; #else if (!reloadForGUI()) return false; return attachView(parentWindowHandle); #endif } bool Vst3Instrument::reloadForGUI() { #ifndef HAVE_VST3SDK return false; #else auto* s = static_cast(state_); if (!s || !s->controller) return false; if (s->view && guiAttached_) return true; if (hasAttachedOnce_ && !guiAttached_) { // Reopen GUI sau khi dong: mot so plugin (Nexus...) hang o // view->attached() LAN 2 tren cung component instance. Tao lai inst // moi tren CUNG worker thread (COM STA con song) roi attach lai. // PHAN NAY PHAI chay tren worker thread — apartment cua channel song // o day; chay tren thread tam thi apartment moi chet ngay sau do // (reopen lan sau -> treo nhu bridge_like). if (!reload()) return false; s = static_cast(state_); if (!s || !s->controller) return false; } return true; #endif } bool Vst3Instrument::attachView(void* parentWindowHandle) { #ifndef HAVE_VST3SDK (void)parentWindowHandle; return false; #else auto* s = static_cast(state_); if (!s || !s->controller || !parentWindowHandle) return false; IPlugView* rawView = nullptr; s->controller->queryInterface(IPlugView::iid, (void**)&rawView); FUnknownPtr view(rawView); if (!view) { // Mot so plugin khong expose IPlugView tren edit controller; thu component. IPlugView* rawViewC = nullptr; s->component->queryInterface(IPlugView::iid, (void**)&rawViewC); view = FUnknownPtr(rawViewC); } if (!view) { // Official editorhost.cpp pattern: IEditController::createView(kEditor). // JUCE-based plugins (Scaler2) expose the editor only this way. view = owned(s->controller->createView(Steinberg::Vst::ViewType::kEditor)); } if (!view) return false; view->setFrame(&s->plugFrame); view->isPlatformTypeSupported(kPlatformTypeHWND); tresult ta = view->attached(parentWindowHandle, kPlatformTypeHWND); if (ta != kResultOk) return false; #ifdef _WIN32 HWND hwnd = (HWND)parentWindowHandle; Steinberg::ViewRect rect; if (view->getSize(&rect) == kResultOk) { int w = rect.right - rect.left; int h = rect.bottom - rect.top; RECT r = { 0, 0, w, h }; AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE); SetWindowPos(hwnd, nullptr, 0, 0, r.right - r.left, r.bottom - r.top, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); // Tell the plugin the client size right after attach — some editors // (Skia/OpenGL-based) skip their first paint until onSize arrives, // leaving a blank/white surface. view->onSize(&rect); } #endif s->view = view; guiAttached_ = true; hasAttachedOnce_ = true; // ponytail: the bridge loop is a worker thread without a Windows message // pump — some editors may not repaint until the first native event; a // future version can spin a dedicated UI thread + pump. return true; #endif } void Vst3Instrument::resizeView(int w, int h) { #ifdef HAVE_VST3SDK auto* s = static_cast(state_); if (s && s->view) { Steinberg::ViewRect rect(0, 0, w, h); s->view->onSize(&rect); } #endif } bool Vst3Instrument::hasAttachedView() const { return state_ != nullptr && guiAttached_; } bool Vst3Instrument::reload() { #ifndef HAVE_VST3SDK return false; #else if (!state_) return false; closeGUI(); auto* s = static_cast(state_); if (s->component) { FUnknownPtr processor(s->component); if (processor) processor->setProcessing(false); s->component->setActive(false); s->component->terminate(); } // Single-component plugins: controller == component, already terminated. if (s->controller && !s->controllerIsComponent) s->controller->terminate(); s->processData.unprepare(); delete s; state_ = nullptr; loaded_ = false; guiAttached_ = false; hasAttachedOnce_ = false; return loadPlugin(path_, sampleRate_); #endif } void Vst3Instrument::closeGUI() { #ifndef HAVE_VST3SDK return; #else auto* s = static_cast(state_); // Do NOT call s->view->removed(): Nexus's removed() enters an internal // modal wait for a message only this thread's message pump can dispatch // — but this thread is inside removed(), so it deadlocks forever // (observed: closeGUI job never returns; wdisp flood from the editor). // Nulling the view and letting the next terminate() (reload / destructor) // tear the editor windows down on this same thread is the deterministic // path out. Callers (post_close_gui / load job) additionally destroy the // editor's child windows right here on this thread so the plugin stops // pumping; the leaked editor object is inert once its windows are gone. if (s) s->view = nullptr; guiAttached_ = false; #endif } void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) { (void)outputL; (void)outputR; (void)numSamples; #ifndef HAVE_VST3SDK return; #else // GUI reopen rebuilds the instance on the worker thread; while the // teardown runs, the audio loop must not touch state_ (flag toggled under // InstrumentEngineManager::mu_ by setReloading — renderAll holds the same // mutex during processAudioBlock, so the check is race-free). if (reloading_) return; auto* s = static_cast(state_); if (!s || !s->component || numSamples == 0) return; FUnknownPtr processor(s->component); if (!processor) return; s->processData.processMode = kRealtime; s->processData.numSamples = (int32)numSamples; s->processData.inputEvents = &s->eventList; s->processData.inputParameterChanges = &s->paramChanges; s->processData.processContext = &s->processContext; s->processContext.projectTimeSamples += numSamples; s->processContext.projectTimeMusic = (double)s->processContext.projectTimeSamples / s->processContext.sampleRate * (s->processContext.tempo / 60.0); // 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) { for (int32 b = 0; b < s->processData.numOutputs; ++b) { const Steinberg::Vst::AudioBusBuffers& ob = s->processData.outputs[b]; for (int32 c = 0; c < ob.numChannels; ++c) if (ob.channelBuffers32[c]) std::memset(ob.channelBuffers32[c], 0, numSamples * sizeof(float)); } } tresult pr = processor->process(s->processData); float mx = 0.f; if (pr >= 0 && s->processData.numOutputs > 0) { const Steinberg::Vst::AudioBusBuffers& out = s->processData.outputs[0]; const float* buf0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr; const float* buf1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr; if (buf0) std::memcpy(outputL, buf0, numSamples * sizeof(float)); else std::memset(outputL, 0, numSamples * sizeof(float)); if (buf1) std::memcpy(outputR, buf1, numSamples * sizeof(float)); else if (buf0) std::memcpy(outputR, buf0, numSamples * sizeof(float)); // mono → stereo else std::memset(outputR, 0, numSamples * sizeof(float)); for (uint32_t i = 0; i < numSamples; ++i) { float v = outputL[i] < 0 ? -outputL[i] : outputL[i]; if (v > mx) mx = v; } } else { std::memset(outputL, 0, numSamples * sizeof(float)); std::memset(outputR, 0, numSamples * sizeof(float)); } s->eventList.clear(); s->paramChanges.clearQueue(); #endif }