874 lines
34 KiB
C++
874 lines
34 KiB
C++
// 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 <windows.h>
|
|
#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 "public.sdk/source/common/memorystream.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 <cstring>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
#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<IComponent> component;
|
|
IPtr<IEditController> controller;
|
|
IPtr<HostApplication> 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<IPlugView> 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<int32> 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<IEditController>& controller, int16 channel,
|
|
int16 ctrlNumber, ParamID legacyTag) {
|
|
if (controller) {
|
|
FUnknownPtr<IMidiMapping> 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<Vst3HostState*>(state_);
|
|
if (s->component) {
|
|
FUnknownPtr<IAudioProcessor> 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<IComponent> component = factory.createInstance<IComponent>(chosen.ID());
|
|
if (!component) {
|
|
std::cerr << "[Vst3Instrument] createInstance<IComponent> failed" << std::endl;
|
|
return false;
|
|
}
|
|
IPtr<HostApplication> hostApp = owned(new HostApplication());
|
|
FUnknownPtr<IPluginBase> 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<IEditController> 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<IEditController>(VST3::UID(cid));
|
|
if (controller) {
|
|
FUnknownPtr<IPluginBase> 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<Vst3HostState> s(new Vst3HostState());
|
|
s->controllerIsComponent = isSingle;
|
|
controller->setComponentHandler(&s->componentHandler);
|
|
// Connect component <-> controller for parameter sync (both directions).
|
|
FUnknownPtr<IConnectionPoint> compCP(component);
|
|
FUnknownPtr<IConnectionPoint> 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<IAudioProcessor> 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<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::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<Vst3HostState*>(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<Vst3HostState*>(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<Vst3HostState*>(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<Vst3HostState*>(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
|
|
}
|
|
|
|
// G0.4: capture the plugin's full state (component + edit controller) into
|
|
// buffers so a GUI reopen can restore the preset after reload().
|
|
void Vst3Instrument::captureState() {
|
|
#ifndef HAVE_VST3SDK
|
|
return;
|
|
#else
|
|
auto* s = static_cast<Vst3HostState*>(state_);
|
|
if (!s) return;
|
|
savedComponentState_.clear();
|
|
savedControllerState_.clear();
|
|
Steinberg::MemoryStream stream;
|
|
if (s->component && s->component->getState(&stream) == kResultOk && stream.getSize() > 0) {
|
|
savedComponentState_.assign(stream.getData(), stream.getData() + stream.getSize());
|
|
std::cout << "[Vst3Instrument] state captured ch=" << channel_
|
|
<< " component=" << savedComponentState_.size() << "B" << std::endl;
|
|
}
|
|
if (s->controller && !s->controllerIsComponent &&
|
|
s->controller->getState(&stream) == kResultOk && stream.getSize() > 0) {
|
|
savedControllerState_.assign(stream.getData(), stream.getData() + stream.getSize());
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// G2.1: base64 encode (RFC 4648). Small, dependency-free.
|
|
static std::string base64Encode(const uint8_t* data, size_t len) {
|
|
static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
std::string out;
|
|
out.reserve(((len + 2) / 3) * 4);
|
|
for (size_t i = 0; i < len; i += 3) {
|
|
uint32_t n = (uint32_t)data[i] << 16;
|
|
if (i + 1 < len) n |= (uint32_t)data[i + 1] << 8;
|
|
if (i + 2 < len) n |= (uint32_t)data[i + 2];
|
|
out += tbl[(n >> 18) & 63];
|
|
out += tbl[(n >> 12) & 63];
|
|
out += (i + 1 < len) ? tbl[(n >> 6) & 63] : '=';
|
|
out += (i + 2 < len) ? tbl[n & 63] : '=';
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string Vst3Instrument::serializeState() const {
|
|
#ifndef HAVE_VST3SDK
|
|
return std::string();
|
|
#else
|
|
if (savedComponentState_.empty() && savedControllerState_.empty())
|
|
return std::string();
|
|
std::vector<uint8_t> blob;
|
|
auto append32 = [&blob](uint32_t v) {
|
|
blob.push_back((uint8_t)(v >> 24));
|
|
blob.push_back((uint8_t)(v >> 16));
|
|
blob.push_back((uint8_t)(v >> 8));
|
|
blob.push_back((uint8_t)v);
|
|
};
|
|
append32((uint32_t)savedComponentState_.size());
|
|
blob.insert(blob.end(), savedComponentState_.begin(), savedComponentState_.end());
|
|
append32((uint32_t)savedControllerState_.size());
|
|
blob.insert(blob.end(), savedControllerState_.begin(), savedControllerState_.end());
|
|
return base64Encode(blob.data(), blob.size());
|
|
#endif
|
|
}
|
|
|
|
// G2.3: base64 decode (RFC 4648). Empty / invalid -> false.
|
|
static int b64val(unsigned char c) {
|
|
if (c >= 'A' && c <= 'Z') return c - 'A';
|
|
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
|
if (c >= '0' && c <= '9') return c - '0' + 52;
|
|
if (c == '+') return 62;
|
|
if (c == '/') return 63;
|
|
return -1;
|
|
}
|
|
static bool base64Decode(const std::string& in, std::vector<uint8_t>& out) {
|
|
out.clear();
|
|
out.reserve((in.size() / 4) * 3);
|
|
uint32_t acc = 0;
|
|
int bits = 0;
|
|
for (unsigned char ch : in) {
|
|
if (ch == '=' || ch == '\n' || ch == '\r' || ch == ' ') continue;
|
|
int v = b64val(ch);
|
|
if (v < 0) return false;
|
|
acc = (acc << 6) | (uint32_t)v;
|
|
bits += 6;
|
|
if (bits >= 8) {
|
|
bits -= 8;
|
|
out.push_back((uint8_t)((acc >> bits) & 0xFF));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// G2.3: restore a preset previously saved by serializeState() (base64 of
|
|
// [4B BE componentLen][component][4B BE controllerLen][controller]).
|
|
// Decodes into the saved-state buffers and applies them to the live instance.
|
|
void Vst3Instrument::loadSerializedState(const std::string& base64) {
|
|
#ifndef HAVE_VST3SDK
|
|
(void)base64;
|
|
return;
|
|
#else
|
|
std::vector<uint8_t> blob;
|
|
if (!base64Decode(base64, blob) || blob.size() < 8) {
|
|
std::cerr << "[Vst3Instrument] loadSerializedState: bad base64 ch=" << channel_ << std::endl;
|
|
return;
|
|
}
|
|
auto read32 = [&blob](size_t off) -> uint32_t {
|
|
return ((uint32_t)blob[off] << 24) | ((uint32_t)blob[off + 1] << 16) |
|
|
((uint32_t)blob[off + 2] << 8) | (uint32_t)blob[off + 3];
|
|
};
|
|
size_t off = 0;
|
|
uint32_t clen = read32(off); off += 4;
|
|
if (off + clen > blob.size()) { std::cerr << "[Vst3Instrument] loadSerializedState: bad compLen" << std::endl; return; }
|
|
savedComponentState_.assign(blob.begin() + off, blob.begin() + off + clen);
|
|
off += clen;
|
|
if (off + 4 > blob.size()) { std::cerr << "[Vst3Instrument] loadSerializedState: bad ctrlLen" << std::endl; return; }
|
|
uint32_t klen = read32(off); off += 4;
|
|
if (off + klen > blob.size()) { std::cerr << "[Vst3Instrument] loadSerializedState: bad ctrlLen" << std::endl; return; }
|
|
savedControllerState_.assign(blob.begin() + off, blob.begin() + off + klen);
|
|
std::cout << "[Vst3Instrument] loaded serialized state ch=" << channel_
|
|
<< " component=" << savedComponentState_.size()
|
|
<< " controller=" << savedControllerState_.size() << std::endl;
|
|
restoreState();
|
|
#endif
|
|
}
|
|
|
|
// G0.4: restore the captured state onto the freshly loaded instance.
|
|
void Vst3Instrument::restoreState() {
|
|
#ifndef HAVE_VST3SDK
|
|
return;
|
|
#else
|
|
auto* s = static_cast<Vst3HostState*>(state_);
|
|
if (!s) return;
|
|
if (!savedComponentState_.empty()) {
|
|
Steinberg::MemoryStream stream;
|
|
stream.write(savedComponentState_.data(), (int32)savedComponentState_.size(), nullptr);
|
|
stream.seek(0, Steinberg::IBStream::kIBSeekSet, nullptr);
|
|
bool okComp = s->component && s->component->setState(&stream) == kResultOk;
|
|
if (!okComp) {
|
|
std::cerr << "[Vst3Instrument] setState(component) failed ch=" << channel_ << std::endl;
|
|
} else {
|
|
std::cout << "[Vst3Instrument] state restored ch=" << channel_
|
|
<< " component=" << savedComponentState_.size() << "B" << std::endl;
|
|
}
|
|
}
|
|
if (!savedControllerState_.empty()) {
|
|
Steinberg::MemoryStream stream;
|
|
stream.write(savedControllerState_.data(), (int32)savedControllerState_.size(), nullptr);
|
|
stream.seek(0, Steinberg::IBStream::kIBSeekSet, nullptr);
|
|
if (s->controller && s->controller->setState(&stream) != kResultOk) {
|
|
std::cerr << "[Vst3Instrument] setState(controller) failed ch=" << channel_ << std::endl;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool Vst3Instrument::reloadForGUI() {
|
|
#ifndef HAVE_VST3SDK
|
|
return false;
|
|
#else
|
|
auto* s = static_cast<Vst3HostState*>(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).
|
|
// G0.4: reload() terminates + recreates the instance — capture the
|
|
// preset first, restore it after, so the GUI reopen keeps the sound.
|
|
captureState();
|
|
if (!reload()) return false;
|
|
s = static_cast<Vst3HostState*>(state_);
|
|
if (!s || !s->controller) return false;
|
|
restoreState();
|
|
}
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool Vst3Instrument::attachView(void* parentWindowHandle) {
|
|
#ifndef HAVE_VST3SDK
|
|
(void)parentWindowHandle;
|
|
return false;
|
|
#else
|
|
auto* s = static_cast<Vst3HostState*>(state_);
|
|
if (!s || !s->controller || !parentWindowHandle) return false;
|
|
IPlugView* rawView = nullptr;
|
|
s->controller->queryInterface(IPlugView::iid, (void**)&rawView);
|
|
FUnknownPtr<IPlugView> 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<IPlugView>(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<Vst3HostState*>(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<Vst3HostState*>(state_);
|
|
if (s->component) {
|
|
FUnknownPtr<IAudioProcessor> 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<Vst3HostState*>(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<Vst3HostState*>(state_);
|
|
if (!s || !s->component || numSamples == 0) return;
|
|
FUnknownPtr<IAudioProcessor> 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
|
|
}
|