fix: GUI VST native window qua bridge + audio path bridge (SF2 preview/play câm)
- open_vst_gui: bo WebviewWindowBuilder/thread, chi push_control type=4 (hwnd=0 -> bridge tao window) - main.cpp: create_native_vst_window (class SonicForge_Native_VST3_Class, 800x600, khong TOPMOST), tao trong ChannelWorker job, map guiWindows, capture arg2 by value (fix dangling) - app.jsx: guard isBridgeActive() 8 cho -> bridge active thi moi note di router -> pushEvent -> bridge (truoc day SF2 cam vi HAS_PYFLUIDSYNTH=FALSE -> /soundfont-render 501; VST3 path cu dung nativeSf/Carla) - E2E: SF2 NOTE_ON qua dispatchMidiEvent -> SHM peak 0.029745; Nexus GUI native hwnd OK (license/preset) - docs: TASKS.md + TEST_NOTES.md ghi batch fix + ket qua; gitignore vendor/junk
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
|
||||
#include <fluidsynth.h>
|
||||
#include <sfizz.hpp>
|
||||
#include "Vst3Instrument.h"
|
||||
|
||||
// void* members keep fluid types out of the public header; cast here.
|
||||
#define FS_SYNTH (static_cast<fluid_synth_t*>(synth))
|
||||
#define FS_SETTINGS (static_cast<fluid_settings_t*>(settings))
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
@@ -14,24 +19,24 @@ FluidSynthInstrument::FluidSynthInstrument()
|
||||
: settings(nullptr), synth(nullptr), sfontId(-1) {}
|
||||
|
||||
FluidSynthInstrument::~FluidSynthInstrument() {
|
||||
if (synth) delete_fluid_synth(synth);
|
||||
if (settings) delete_fluid_settings(settings);
|
||||
if (synth) delete_fluid_synth(FS_SYNTH);
|
||||
if (settings) delete_fluid_settings(FS_SETTINGS);
|
||||
}
|
||||
|
||||
bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sampleRate) {
|
||||
if (synth) { delete_fluid_synth(synth); synth = nullptr; }
|
||||
if (settings) { delete_fluid_settings(settings); settings = nullptr; }
|
||||
if (synth) { delete_fluid_synth(FS_SYNTH); synth = nullptr; }
|
||||
if (settings) { delete_fluid_settings(FS_SETTINGS); settings = nullptr; }
|
||||
settings = new_fluid_settings();
|
||||
fluid_settings_setnum(settings, "synth.sample-rate", sampleRate);
|
||||
fluid_settings_setint(settings, "synth.polyphony", 256);
|
||||
fluid_settings_setint(settings, "synth.verbose", 0);
|
||||
synth = new_fluid_synth(settings);
|
||||
fluid_settings_setnum(FS_SETTINGS, "synth.sample-rate", sampleRate);
|
||||
fluid_settings_setint(FS_SETTINGS, "synth.polyphony", 256);
|
||||
fluid_settings_setint(FS_SETTINGS, "synth.verbose", 0);
|
||||
synth = new_fluid_synth(FS_SETTINGS);
|
||||
if (!synth) return false;
|
||||
sfontId = fluid_synth_sfload(synth, path.c_str(), 1);
|
||||
sfontId = fluid_synth_sfload(FS_SYNTH, path.c_str(), 1);
|
||||
if (sfontId == -1) return false;
|
||||
// Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano)
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
fluid_synth_program_select(synth, ch, sfontId, 0, 0);
|
||||
fluid_synth_program_select(FS_SYNTH, ch, sfontId, 0, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -42,35 +47,35 @@ bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) {
|
||||
|
||||
void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
|
||||
if (!synth) return;
|
||||
fluid_synth_bank_select(synth, channel, bank);
|
||||
fluid_synth_program_change(synth, channel, program);
|
||||
fluid_synth_bank_select(FS_SYNTH, channel, bank);
|
||||
fluid_synth_program_change(FS_SYNTH, channel, program);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
|
||||
if (!synth) return;
|
||||
int velInt = static_cast<int>(velocity * 127.0f);
|
||||
fluid_synth_noteon(synth, channel, pitch, velInt);
|
||||
fluid_synth_noteon(FS_SYNTH, channel, pitch, velInt);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
|
||||
if (!synth) return;
|
||||
fluid_synth_noteoff(synth, channel, pitch);
|
||||
fluid_synth_noteoff(FS_SYNTH, channel, pitch);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
|
||||
if (!synth) return;
|
||||
fluid_synth_cc(synth, channel, cc, value);
|
||||
fluid_synth_cc(FS_SYNTH, channel, cc, value);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||
if (!synth) return;
|
||||
fluid_synth_program_change(synth, channel, program);
|
||||
fluid_synth_program_change(FS_SYNTH, channel, program);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
if (!synth) return;
|
||||
// fluid_synth_pitch_bend takes the raw 14-bit value (center 8192).
|
||||
fluid_synth_pitch_bend(synth, channel, bend14);
|
||||
fluid_synth_pitch_bend(FS_SYNTH, channel, bend14);
|
||||
}
|
||||
|
||||
bool FluidSynthInstrument::openGUI(void* parentWindowHandle) {
|
||||
@@ -81,7 +86,7 @@ void FluidSynthInstrument::closeGUI() {}
|
||||
|
||||
void FluidSynthInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
||||
if (!synth) return;
|
||||
fluid_synth_write_float(synth, numSamples, outputL, 0, 1, outputR, 0, 1);
|
||||
fluid_synth_write_float(FS_SYNTH, numSamples, outputL, 0, 1, outputR, 0, 1);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
@@ -109,18 +114,15 @@ void SfizzInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleO
|
||||
}
|
||||
|
||||
void SfizzInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
|
||||
// ponytail: non-delayed cc() is stable across sfizz versions; switch to
|
||||
// hdCC(cc, value) for sample-accurate CC when the installed sfizz has it.
|
||||
sfizzSynth.cc(static_cast<int>(cc), static_cast<float>(value));
|
||||
sfizzSynth.cc(0, static_cast<int>(cc), static_cast<int>(value));
|
||||
}
|
||||
|
||||
void SfizzInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||
// TODO(A12): sfizz program-change API differs by version (hdProgramChange
|
||||
// in >=0.6). Rarely used by SFZ instruments — no-op until verified.
|
||||
sfizzSynth.programChange(0, static_cast<int>(program));
|
||||
}
|
||||
|
||||
void SfizzInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
sfizzSynth.pitchWheel(static_cast<int>(bend14));
|
||||
sfizzSynth.pitchWheel(0, static_cast<int>(bend14));
|
||||
}
|
||||
|
||||
bool SfizzInstrument::openGUI(void* parentWindowHandle) { return false; }
|
||||
@@ -128,7 +130,7 @@ void SfizzInstrument::closeGUI() {}
|
||||
|
||||
void SfizzInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
||||
float* channels[2] = { outputL, outputR };
|
||||
sfizzSynth.renderBlock(channels, numSamples);
|
||||
sfizzSynth.renderBlock(channels, numSamples, 1); // numOutputs=1 = stereo L/R pair (2 ch)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
@@ -138,9 +140,8 @@ std::unique_ptr<INativeInstrument> InstrumentEngineManager::create_instrument(In
|
||||
switch (type) {
|
||||
case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique<FluidSynthInstrument>();
|
||||
case InstrumentType::SFZ: return std::make_unique<SfizzInstrument>();
|
||||
// ponytail: VST3/VST2 host needs vst3sdk + Steinberg APIs — wired in
|
||||
// Giai doan 2 (A6-A8); returns nullptr so the bridge degrades gracefully.
|
||||
case InstrumentType::VST3:
|
||||
case InstrumentType::VST3: return std::make_unique<Vst3Instrument>();
|
||||
// ponytail: VST2 host (VST2.4 SDK, Steinberg discontinued) not implemented.
|
||||
case InstrumentType::VST2:
|
||||
default: return nullptr;
|
||||
}
|
||||
@@ -152,30 +153,41 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
||||
if (channel >= 16) return false;
|
||||
auto inst = create_instrument(type);
|
||||
if (!inst) return false;
|
||||
if (!inst->init(sampleRate, blockSize)) return false;
|
||||
bool loaded = false;
|
||||
if (type == InstrumentType::SOUNDFONT_SF2_SF3)
|
||||
loaded = static_cast<FluidSynthInstrument*>(inst.get())->loadSoundFontFile(path, sampleRate);
|
||||
else if (type == InstrumentType::SFZ)
|
||||
loaded = static_cast<SfizzInstrument*>(inst.get())->loadSfzFile(path, sampleRate);
|
||||
else if (type == InstrumentType::VST3)
|
||||
loaded = static_cast<Vst3Instrument*>(inst.get())->loadPlugin(path, sampleRate);
|
||||
if (!loaded) return false;
|
||||
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
|
||||
if (!inst->init(sampleRate, blockSize)) return false;
|
||||
// Replacing an existing instrument drops its voices with the old engine.
|
||||
channels_[channel] = std::move(inst);
|
||||
// Load may run on a detached thread (VST3 init is slow): only the map
|
||||
// write is under the mutex so renderAll on the audio loop never stalls.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
channels_[channel] = std::move(inst);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
auto it = channels_.find(channel);
|
||||
return it == channels_.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
void InstrumentEngineManager::allNotesOff() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
for (auto& [ch, inst] : channels_) {
|
||||
for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
std::memset(outputL, 0, numSamples * sizeof(float));
|
||||
std::memset(outputR, 0, numSamples * sizeof(float));
|
||||
if (channels_.empty()) return;
|
||||
|
||||
@@ -23,6 +23,19 @@ ShmHandle* shm_open(const char* name) {
|
||||
if (!h->view) { CloseHandle(h->map); delete h; return nullptr; }
|
||||
return h;
|
||||
}
|
||||
// Creates the mapping (used by tests/self-check; the DAW normally owns it).
|
||||
ShmHandle* shm_create(const char* name) {
|
||||
ShmHandle* h = new ShmHandle();
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, name, -1, nullptr, 0);
|
||||
std::wstring wname(wlen, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, name, -1, &wname[0], wlen);
|
||||
h->map = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
|
||||
sizeof(SharedAudioBufferIPC), wname.c_str());
|
||||
if (!h->map) { delete h; return nullptr; }
|
||||
h->view = MapViewOfFile(h->map, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
|
||||
if (!h->view) { CloseHandle(h->map); delete h; return nullptr; }
|
||||
return h;
|
||||
}
|
||||
void shm_close(ShmHandle* h) {
|
||||
if (!h) return;
|
||||
if (h->view) UnmapViewOfFile(h->view);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// native_bridge/src/Vst3Instrument.cpp
|
||||
// native_bridge/src/Vst3Instrument.cpp
|
||||
// VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/).
|
||||
//
|
||||
// Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every
|
||||
@@ -10,16 +10,25 @@
|
||||
#include "Vst3Instrument.h"
|
||||
|
||||
#ifdef HAVE_VST3SDK
|
||||
#include "public.sdk/source/main/pluginfactory.h"
|
||||
#include "pluginterfaces/base/ibstream.h"
|
||||
#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 "public.sdk/source/common/pluginview.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#endif
|
||||
|
||||
@@ -27,60 +36,155 @@
|
||||
// With-SDK implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
#ifdef HAVE_VST3SDK
|
||||
#include "public.sdk/source/main/module.h"
|
||||
|
||||
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::IEventList;
|
||||
using Steinberg::Vst::Event;
|
||||
using Steinberg::Vst::NoteOnEvent;
|
||||
using Steinberg::Vst::NoteOffEvent;
|
||||
using Steinberg::Vst::DataEvent;
|
||||
using Steinberg::Vst::kMidiCC;
|
||||
using Steinberg::Vst::kMidiPitchBend;
|
||||
using Steinberg::Vst::kMidiProgramChange;
|
||||
using Steinberg::IPlugView;
|
||||
using Steinberg::tresult;
|
||||
using Steinberg::kResultOk;
|
||||
using Steinberg::kResultFalse;
|
||||
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::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:
|
||||
Steinberg::tresult queryInterface(const Steinberg::TUID&, void** v) override {
|
||||
tresult queryInterface(const char*, void** v) override {
|
||||
*v = nullptr;
|
||||
return Steinberg::kNoInterface;
|
||||
return kNoInterface;
|
||||
}
|
||||
Steinberg::uint32 addRef() override { return 1; }
|
||||
Steinberg::uint32 release() override { return 1; }
|
||||
Steinberg::tresult beginEdit(Steinberg::Vst::ParamID) override { return Steinberg::kResultOk; }
|
||||
Steinberg::tresult performEdit(Steinberg::Vst::ParamID, Steinberg::Vst::ParamValue) override {
|
||||
return Steinberg::kResultOk;
|
||||
}
|
||||
Steinberg::tresult endEdit(Steinberg::Vst::ParamID) override { return Steinberg::kResultOk; }
|
||||
Steinberg::tresult restartComponent(Steinberg::int32) override { return Steinberg::kResultOk; }
|
||||
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; }
|
||||
};
|
||||
|
||||
// Bundle both components in one factory entry so module.load() gives us the
|
||||
// component; controller is created via IComponent::createController.
|
||||
// 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;
|
||||
ParameterChanges paramChanges;
|
||||
ProcessContext processContext;
|
||||
IPtr<IPlugView> view;
|
||||
HostPlugFrame plugFrame;
|
||||
HostComponentHandler componentHandler;
|
||||
int32 outputChannels = 2;
|
||||
bool controllerIsComponent = false; // single-component plugin: controller == component
|
||||
};
|
||||
|
||||
// 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()
|
||||
: module_(nullptr),
|
||||
processor_(nullptr),
|
||||
controller_(nullptr),
|
||||
view_(nullptr),
|
||||
: state_(nullptr),
|
||||
path_(),
|
||||
sampleRate_(44100.0),
|
||||
maxBlockSize_(256),
|
||||
loaded_(false),
|
||||
guiAttached_(false) {}
|
||||
|
||||
Vst3Instrument::~Vst3Instrument() { closeGUI(); }
|
||||
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
|
||||
@@ -88,15 +192,159 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
|
||||
return false; // vst3sdk submodule missing — VST3 disabled
|
||||
#else
|
||||
if (loaded_) return true;
|
||||
// ponytail: vst3sdk module loading is platform-specific
|
||||
// (Module::create on Windows .vst3 bundle / macOS .vst3 framework). Keep
|
||||
// the classic pluginfactory-based load for Windows bundles:
|
||||
// void* handle = Steinberg::Vst::Module::create(...)
|
||||
// and use module->getFactory().createInstance<...>(cid).
|
||||
// Verified on Windows in A6 (G3 Vital.vst3). Until then VST3 returns false
|
||||
// so SF2/SFZ keep working.
|
||||
(void)path; (void)sampleRate;
|
||||
return false;
|
||||
using namespace VST3::Hosting;
|
||||
|
||||
std::string err;
|
||||
std::cerr << "[dbg] loadPlugin: Module::create ..." << std::endl;
|
||||
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();
|
||||
std::cerr << "[dbg] loadPlugin: Module::create OK" << std::endl;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
std::cerr << "[dbg] loadPlugin: createInstance ..." << std::endl;
|
||||
std::cerr << "[dbg] loadPlugin: chosen name=" << chosen.name() << " category=" << chosen.category()
|
||||
<< " subcat=" << chosen.subCategoriesString() << " idbytes=";
|
||||
{
|
||||
const unsigned char* cid2 = (const unsigned char*)chosen.ID().data();
|
||||
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)cid2[b] << ' ';
|
||||
std::cerr << std::dec << std::endl;
|
||||
}
|
||||
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());
|
||||
std::cerr << "[dbg] loadPlugin: initialize ..." << std::endl;
|
||||
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);
|
||||
std::cerr << "[dbg] loadPlugin: getControllerClassId=" << (int)cidRes
|
||||
<< " cid=";
|
||||
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)(unsigned char)cid[b] << ' ';
|
||||
std::cerr << std::dec << std::endl;
|
||||
if (cidRes == kResultTrue || cidRes == kResultOk) {
|
||||
controller = factory.createInstance<IEditController>(VST3::UID(cid));
|
||||
std::cerr << "[dbg] loadPlugin: controller from factory=" << (controller ? 1 : 0) << std::endl;
|
||||
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);
|
||||
}
|
||||
|
||||
// Audio processor: setup → activate → start processing (audioclient.cpp).
|
||||
FUnknownPtr<IAudioProcessor> processor(component);
|
||||
if (!processor) {
|
||||
std::cerr << "[Vst3Instrument] no IAudioProcessor" << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::cerr << "[dbg] loadPlugin: setupProcessing ..." << std::endl;
|
||||
ProcessSetup setup{kRealtime, kSample32, (int32)maxBlockSize_, sampleRate};
|
||||
if (processor->setupProcessing(setup) != kResultOk) {
|
||||
std::cerr << "[Vst3Instrument] setupProcessing failed" << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::cerr << "[dbg] loadPlugin: setActive ..." << std::endl;
|
||||
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).
|
||||
std::cerr << "[dbg] loadPlugin: processData.prepare ..." << std::endl;
|
||||
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;
|
||||
}
|
||||
|
||||
std::cerr << "[dbg] loadPlugin: prepare OK, wiring state" << std::endl;
|
||||
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
|
||||
}
|
||||
|
||||
@@ -116,10 +364,21 @@ void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, ui
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
if (!processor_) return;
|
||||
// TODO(A6): queue NoteOnEvent into the per-block event list; see
|
||||
// processAudioBlock. Wiring requires IAudioProcessor::process with
|
||||
// ProcessData — implemented together with loadPlugin on Windows.
|
||||
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 = (int16)channel;
|
||||
e.noteOn.pitch = (int16)pitch;
|
||||
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
|
||||
s->eventList.addEvent(e);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -128,8 +387,20 @@ void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOf
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
if (!processor_) return;
|
||||
// TODO(A6): queue NoteOffEvent (see noteOn)
|
||||
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 = (int16)channel;
|
||||
e.noteOff.pitch = (int16)pitch;
|
||||
e.noteOff.velocity = 0.f;
|
||||
e.noteOff.noteId = -1;
|
||||
e.noteOff.tuning = 0.f;
|
||||
s->eventList.addEvent(e);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -138,8 +409,16 @@ void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
if (!controller_) return;
|
||||
// TODO(A6): controller_->setParamNormalized(kMidiCC | cc, value/127.0)
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
if (!s || !s->controller) return;
|
||||
ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)cc,
|
||||
kHostMidiCC | (ParamID)(cc & 0x7F));
|
||||
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
|
||||
}
|
||||
|
||||
@@ -148,8 +427,15 @@ void Vst3Instrument::programChange(uint32_t channel, uint32_t program) {
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
if (!controller_) return;
|
||||
// TODO(A6): setParamNormalized(kMidiProgramChange | program, ...)
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
if (!s || !s->controller) return;
|
||||
ParamID tag = kHostMidiProgramChange | (ParamID)(channel & 0xF);
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -158,8 +444,14 @@ void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
if (!controller_) return;
|
||||
// TODO(A6): setParamNormalized(kMidiPitchBend, bend14/16383.0)
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
if (!s || !s->controller) return;
|
||||
ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)kPitchBend,
|
||||
kHostMidiPitchBend | (ParamID)(channel & 0xF));
|
||||
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
|
||||
}
|
||||
|
||||
@@ -168,11 +460,54 @@ bool Vst3Instrument::openGUI(void* parentWindowHandle) {
|
||||
(void)parentWindowHandle;
|
||||
return false;
|
||||
#else
|
||||
if (!processor_ || !controller_ || !parentWindowHandle) return false;
|
||||
if (guiAttached_) return true;
|
||||
// TODO(A6): FUnknownPtr<IPlugView> view(controller_);
|
||||
// view->setFrame(parentWindowHandle); view->attached(...); view_ = view;
|
||||
return false;
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
std::cerr << "[dbg] openGUI hwnd=" << (void*)(uintptr_t)parentWindowHandle
|
||||
<< " controller=" << (s ? (s->controller ? 1 : 0) : -1) << std::endl;
|
||||
if (!s || !s->controller || !parentWindowHandle) return false;
|
||||
if (s->view && guiAttached_) return true;
|
||||
IPlugView* rawView = nullptr;
|
||||
tresult qi = s->controller->queryInterface(IPlugView::iid, (void**)&rawView);
|
||||
{
|
||||
const unsigned char* iidb = (const unsigned char*)&IPlugView::iid;
|
||||
std::cerr << "[dbg] openGUI: IPlugView::iid bytes=";
|
||||
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)iidb[b] << ' ';
|
||||
std::cerr << std::dec << std::endl;
|
||||
const unsigned char* eidb = (const unsigned char*)&IEditController::iid;
|
||||
std::cerr << "[dbg] openGUI: IEditController::iid bytes=";
|
||||
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)eidb[b] << ' ';
|
||||
std::cerr << std::dec << std::endl;
|
||||
}
|
||||
std::cerr << "[dbg] openGUI: controller qi IPlugView=" << (int)qi << " raw=" << (void*)rawView << " isSingle=" << (s->controllerIsComponent ? 1 : 0) << std::endl;
|
||||
FUnknownPtr<IPlugView> view(rawView);
|
||||
if (!view) {
|
||||
// Mot so plugin khong expose IPlugView tren edit controller; thu component.
|
||||
std::cerr << "[dbg] openGUI: controller no IPlugView, trying component" << std::endl;
|
||||
IPlugView* rawViewC = nullptr;
|
||||
tresult qic = s->component->queryInterface(IPlugView::iid, (void**)&rawViewC);
|
||||
std::cerr << "[dbg] openGUI: component qi IPlugView=" << (int)qic << " raw=" << (void*)rawViewC << std::endl;
|
||||
view = FUnknownPtr<IPlugView>(rawViewC);
|
||||
}
|
||||
if (!view) {
|
||||
// Official editorhost.cpp pattern: IEditController::createView(kEditor).
|
||||
// JUCE-based plugins (Scaler2) expose the editor only this way.
|
||||
std::cerr << "[dbg] openGUI: qi failed, trying controller->createView(kEditor)" << std::endl;
|
||||
view = owned(s->controller->createView(Steinberg::Vst::ViewType::kEditor));
|
||||
std::cerr << "[dbg] openGUI: createView view=" << (view ? "ok" : "null") << std::endl;
|
||||
}
|
||||
if (!view) { std::cerr << "[dbg] openGUI: no IPlugView" << std::endl; return false; }
|
||||
view->setFrame(&s->plugFrame);
|
||||
tresult ts = view->isPlatformTypeSupported(kPlatformTypeHWND);
|
||||
std::cerr << "[dbg] openGUI: isPlatformTypeSupported=" << (int)ts << std::endl;
|
||||
if (ts != kResultTrue && ts != kResultOk) return false;
|
||||
tresult ta = view->attached(parentWindowHandle, kPlatformTypeHWND);
|
||||
std::cerr << "[dbg] openGUI: attached=" << (int)ta << std::endl;
|
||||
if (ta != kResultOk) return false;
|
||||
s->view = view;
|
||||
guiAttached_ = 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
|
||||
}
|
||||
|
||||
@@ -180,10 +515,9 @@ void Vst3Instrument::closeGUI() {
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
if (view_ && guiAttached_) {
|
||||
// view_->removed(); view_->setFrame(nullptr);
|
||||
}
|
||||
view_ = nullptr;
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
if (s && s->view && guiAttached_) s->view->removed();
|
||||
if (s) s->view = nullptr;
|
||||
guiAttached_ = false;
|
||||
#endif
|
||||
}
|
||||
@@ -193,7 +527,66 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
// TODO(A6): ProcessData with 2 output buffers + event list; called by
|
||||
// InstrumentEngineManager::renderAll via processAudioBlock.
|
||||
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);
|
||||
|
||||
static uint32_t dbgN = 0;
|
||||
static int dbgMaxShown = 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 == kResultOk && 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));
|
||||
}
|
||||
uint32_t evc = s->eventList.getEventCount();
|
||||
int evt = -1;
|
||||
if (evc > 0) {
|
||||
const Event* e0 = s->eventList.getEventByIndex(0);
|
||||
evt = e0 ? (int)e0->type : -2;
|
||||
}
|
||||
if ((++dbgN % 250) == 0 || (evc > 0 && dbgMaxShown < 30)) {
|
||||
if (evc > 0) ++dbgMaxShown;
|
||||
std::cerr << "[dbg] 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;
|
||||
}
|
||||
s->eventList.clear();
|
||||
s->paramChanges.clearQueue();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// native_bridge/src/gui_probe.cpp — standalone VST3 GUI attach probe.
|
||||
// Debug tool (NOT shipped with the DAW): loads a VST3 with Vst3Instrument and
|
||||
// tests openGUI under different thread/pump/parent-window setups so we can
|
||||
// find why view->attached() hangs inside the bridge.
|
||||
#include "Vst3Instrument.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
static void pump_for(int secs) {
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
while (std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::steady_clock::now() - t0).count() < secs) {
|
||||
MSG msg;
|
||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
Sleep(5);
|
||||
}
|
||||
}
|
||||
|
||||
static void sleep_for(int secs) { Sleep((DWORD)(secs * 1000)); }
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 2) {
|
||||
printf("usage: gui_probe <plugin.vst3> [variant] [secs] [hwnd]\n"
|
||||
"variants: main_own | worker_own_nopump | worker_own_pump | worker_foreign_nopump | worker_foreign_pump | bridge_like | same_thread\n");
|
||||
return 2;
|
||||
}
|
||||
std::string path = argv[1];
|
||||
std::string variant = argc > 2 ? argv[2] : "main_own";
|
||||
int secs = argc > 3 ? atoi(argv[3]) : 20;
|
||||
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
|
||||
// Watchdog: never let a stuck plugin hang this probe forever.
|
||||
std::thread([secs]() {
|
||||
Sleep((DWORD)(secs + 10) * 1000);
|
||||
printf("WATCHDOG-KILL\n");
|
||||
fflush(stdout);
|
||||
TerminateProcess(GetCurrentProcess(), 0);
|
||||
}).detach();
|
||||
|
||||
HWND ownHwnd = 0;
|
||||
{
|
||||
WNDCLASSEX wc = {};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.lpfnWndProc = DefWindowProc;
|
||||
wc.hInstance = GetModuleHandle(nullptr);
|
||||
wc.lpszClassName = "GuiProbeClass";
|
||||
RegisterClassEx(&wc);
|
||||
ownHwnd = CreateWindowEx(0, "GuiProbeClass", "GuiProbe", WS_OVERLAPPEDWINDOW,
|
||||
0, 0, 800, 600, nullptr, nullptr, wc.hInstance, nullptr);
|
||||
printf("ownHwnd=%p\n", (void*)ownHwnd);
|
||||
}
|
||||
|
||||
void* targetHwnd = (void*)ownHwnd;
|
||||
if (argc > 4) targetHwnd = (void*)(uintptr_t)strtoull(argv[4], nullptr, 0);
|
||||
|
||||
printf("loading %s\n", path.c_str());
|
||||
fflush(stdout);
|
||||
Vst3Instrument inst;
|
||||
bool isDeferred = (variant == "bridge_like" || variant == "same_thread");
|
||||
bool ok = true;
|
||||
if (!isDeferred) {
|
||||
ok = inst.loadPlugin(path, 44100.0);
|
||||
printf("load=%d\n", ok ? 1 : 0);
|
||||
fflush(stdout);
|
||||
if (!ok) return 1;
|
||||
}
|
||||
|
||||
std::atomic<bool> done{false};
|
||||
std::atomic<bool> result{false};
|
||||
|
||||
auto attach_job = [&]() {
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
printf("[%s] openGUI start hwnd=%p\n", variant.c_str(), targetHwnd);
|
||||
fflush(stdout);
|
||||
bool r = inst.openGUI(targetHwnd);
|
||||
printf("[%s] openGUI returned=%d\n", variant.c_str(), r ? 1 : 0);
|
||||
fflush(stdout);
|
||||
result = r;
|
||||
done = true;
|
||||
};
|
||||
|
||||
if (variant == "main_own") {
|
||||
attach_job(); // editorhost pattern: attached on main thread, pump after
|
||||
pump_for(secs);
|
||||
} else if (variant == "worker_own_nopump" || variant == "worker_foreign_nopump") {
|
||||
std::thread t(attach_job);
|
||||
sleep_for(secs);
|
||||
if (!done.load()) { printf("RESULT: TIMEOUT (no pump)\n"); return 3; }
|
||||
} else if (variant == "worker_own_pump" || variant == "worker_foreign_pump") {
|
||||
std::thread t(attach_job);
|
||||
pump_for(secs);
|
||||
if (!done.load()) { printf("RESULT: TIMEOUT (pump concurrent)\n"); return 3; }
|
||||
} else if (variant == "bridge_like") {
|
||||
// Bridge pattern: plugin loaded on thread A which then EXITS (COM
|
||||
// apartment dies), openGUI on separate thread B, main pumps.
|
||||
std::thread loadA([&]() {
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
printf("[bridge_like] loadPlugin on thread A\n");
|
||||
fflush(stdout);
|
||||
bool r = inst.loadPlugin(path, 44100.0);
|
||||
printf("[bridge_like] load=%d (thread A exiting)\n", r ? 1 : 0);
|
||||
fflush(stdout);
|
||||
CoUninitialize();
|
||||
});
|
||||
loadA.join();
|
||||
std::thread t(attach_job); // thread B, after A exited
|
||||
pump_for(secs);
|
||||
if (!done.load()) { printf("RESULT: TIMEOUT (apartment dead)\n"); return 3; }
|
||||
} else if (variant == "same_thread") {
|
||||
// Load and openGUI on the SAME worker thread, kept alive; main pumps.
|
||||
std::thread t([&]() {
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
printf("[same_thread] loadPlugin on worker\n");
|
||||
fflush(stdout);
|
||||
bool r = inst.loadPlugin(path, 44100.0);
|
||||
printf("[same_thread] load=%d\n", r ? 1 : 0);
|
||||
fflush(stdout);
|
||||
if (r) attach_job();
|
||||
});
|
||||
pump_for(secs);
|
||||
if (!done.load()) { printf("RESULT: TIMEOUT (same_thread)\n"); return 3; }
|
||||
} else {
|
||||
printf("unknown variant %s\n", variant.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
printf("RESULT: done=%d attached_ok=%d\n", done.load() ? 1 : 0, result.load() ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
+190
-10
@@ -9,6 +9,7 @@
|
||||
#include <windows.h>
|
||||
#include <mmsystem.h>
|
||||
#include <process.h>
|
||||
#include <thread>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <cstdlib>
|
||||
@@ -17,9 +18,14 @@
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
// --- platform helpers -------------------------------------------------------
|
||||
@@ -42,6 +48,98 @@ static void sleep_ms(uint32_t ms) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// B9: native Win32 window for the VST editor (replaces the WebView2 surface —
|
||||
// the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the
|
||||
// ChannelWorker thread so the worker's idle message pump services its messages.
|
||||
static void* create_native_vst_window(const char* title) {
|
||||
#ifdef _WIN32
|
||||
static const char* kWndClass = "SonicForge_Native_VST3_Class";
|
||||
static bool registered = false;
|
||||
if (!registered) {
|
||||
WNDCLASSA wc = {};
|
||||
wc.lpfnWndProc = DefWindowProcA;
|
||||
wc.hInstance = GetModuleHandleA(nullptr);
|
||||
wc.lpszClassName = kWndClass;
|
||||
RegisterClassA(&wc);
|
||||
registered = true;
|
||||
}
|
||||
HWND hwnd = CreateWindowExA(0, kWndClass, title ? title : "VST",
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
|
||||
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
|
||||
return (void*)hwnd;
|
||||
#else
|
||||
(void)title;
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Per-channel persistent worker: ONE thread owns the COM STA apartment for that
|
||||
// channel's instrument for its whole lifetime. loadPlugin and openGUI MUST run
|
||||
// on the same thread — if the loading thread exits, its apartment dies and
|
||||
// VST3 plugins that marshal internally (Nexus) hang forever in
|
||||
// view->attached(). Verified with gui_probe: `bridge_like` (load thread exits,
|
||||
// openGUI on another) hangs; `same_thread` (load+openGUI on one alive thread)
|
||||
// returns attached=OK.
|
||||
class ChannelWorker {
|
||||
public:
|
||||
ChannelWorker() {
|
||||
th_ = std::thread([this] {
|
||||
#ifdef _WIN32
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
#endif
|
||||
std::unique_lock<std::mutex> lk(mu_);
|
||||
for (;;) {
|
||||
if (stop_ && jobs_.empty()) break;
|
||||
if (!jobs_.empty()) {
|
||||
auto job = std::move(jobs_.front());
|
||||
jobs_.pop_front();
|
||||
lk.unlock();
|
||||
job();
|
||||
lk.lock();
|
||||
continue;
|
||||
}
|
||||
cv_.wait_for(lk, std::chrono::milliseconds(5));
|
||||
// Pump THIS thread's message queue while idle — VST editor
|
||||
// windows are created on this thread, their messages must be
|
||||
// dispatched here or the editor freezes after attach.
|
||||
lk.unlock();
|
||||
MSG msg;
|
||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
lk.lock();
|
||||
}
|
||||
#ifdef _WIN32
|
||||
CoUninitialize();
|
||||
#endif
|
||||
});
|
||||
}
|
||||
~ChannelWorker() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
stop_ = true;
|
||||
}
|
||||
cv_.notify_all();
|
||||
if (th_.joinable()) th_.join();
|
||||
}
|
||||
void post(std::function<void()> job) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
jobs_.push_back(std::move(job));
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
private:
|
||||
std::thread th_;
|
||||
std::mutex mu_;
|
||||
std::condition_variable cv_;
|
||||
std::deque<std::function<void()>> jobs_;
|
||||
bool stop_ = false;
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
|
||||
|
||||
@@ -73,6 +171,12 @@ int main(int argc, char* argv[]) {
|
||||
#endif
|
||||
|
||||
InstrumentEngineManager instruments;
|
||||
// Per-channel persistent workers: loadPlugin + openGUI run on the SAME
|
||||
// thread whose COM STA apartment stays alive for the channel's lifetime
|
||||
// (see ChannelWorker comment — a dead apartment hangs Nexus attached()).
|
||||
std::map<uint32_t, std::unique_ptr<ChannelWorker>> workers;
|
||||
// B9: native editor windows per channel — keep alive (HWND outlives the job).
|
||||
std::map<uint32_t, void*> guiWindows;
|
||||
// B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE).
|
||||
// Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) — SF_BLOCK_SIZE
|
||||
// is accepted but must match, otherwise warned and ignored.
|
||||
@@ -95,13 +199,16 @@ int main(int argc, char* argv[]) {
|
||||
if (!inst) return; // channel chưa gán instrument → silent (A10)
|
||||
switch (evt.command) {
|
||||
case 0x9:
|
||||
// sampleOffset LUON LUON = 0 khi den day: events duoc dispatch ngay
|
||||
// truoc segment chua no (A11 splitting), nen offset tuong doi la 0.
|
||||
// Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre.
|
||||
if (evt.velocity > 0)
|
||||
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, evt.sampleOffset);
|
||||
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, 0);
|
||||
else
|
||||
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
|
||||
inst->noteOff(evt.channel, evt.pitch, 0);
|
||||
break;
|
||||
case 0x8:
|
||||
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
|
||||
inst->noteOff(evt.channel, evt.pitch, 0);
|
||||
break;
|
||||
case 0xB: // CC: controller number in pitch, value in data2
|
||||
inst->controlChange(evt.channel, evt.pitch, evt.data2);
|
||||
@@ -124,23 +231,50 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
|
||||
while (true) {
|
||||
#ifdef _WIN32
|
||||
// Message pump: VST editors (Nexus, JUCE-based...) block inside
|
||||
// view->attached() until the host dispatches messages — openGUI runs
|
||||
// on a worker thread, so THIS loop must pump concurrently (verified
|
||||
// with gui_probe: worker-thread openGUI + concurrent pump → attached
|
||||
// returns kResultOk; without it → hangs forever).
|
||||
MSG msg;
|
||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
#endif
|
||||
// A. Control events — non-rt safe, drained first
|
||||
for (uint32_t i = 0; i < shmIPC->controlQueueCount; ++i) {
|
||||
const auto& c = shmIPC->controlQueue[i];
|
||||
if (c.type == 2) { // LOAD_INSTRUMENT (A10: assign per MIDI channel)
|
||||
// Robust path length: do not trust arg1 (Rust passes 0 for LOAD).
|
||||
// Chay tren persistent worker thread cua channel: VST3 init
|
||||
// (loadPlugin) co the mat giay — chay dong bo tren audio loop
|
||||
// lam writeIndex stall > 3s -> Rust tuong bridge chet va restart
|
||||
// nham (2 bridge cung map SHM -> race control queue / double
|
||||
// load). assign() chi giu mutex khi ghi map nen renderAll khong
|
||||
// bao gio stall. openGUI sau nay chay tren CUNG thread nay
|
||||
// (ChannelWorker) — thread khong bao gio exit nen COM STA
|
||||
// apartment cua plugin con song (xem ChannelWorker comment).
|
||||
size_t plen = 0;
|
||||
while (plen < sizeof(c.arg2) && c.arg2[plen]) ++plen;
|
||||
std::string path(c.arg2, plen);
|
||||
InstrumentType t = (InstrumentType)c.arg0;
|
||||
uint32_t ch = c.channel & 0xF;
|
||||
if (instruments.assign(ch, t, path, sampleRate, block)) {
|
||||
std::cout << "[NativeBridge] instrument loaded ch=" << ch
|
||||
<< " type=" << (int)c.arg0 << " " << path << std::endl;
|
||||
} else {
|
||||
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
|
||||
<< " type=" << (int)c.arg0 << " " << path << std::endl;
|
||||
}
|
||||
if (!workers[ch]) workers[ch] = std::make_unique<ChannelWorker>();
|
||||
workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() {
|
||||
std::cerr << "[dbg] load thread start ch=" << ch
|
||||
<< " type=" << (int)t << " path=" << path << std::endl;
|
||||
bool ok = instruments.assign(ch, t, path, sampleRate, block);
|
||||
std::cerr << "[dbg] assign returned ch=" << ch << " ok=" << (ok ? 1 : 0) << std::endl;
|
||||
if (ok) {
|
||||
std::cout << "[NativeBridge] instrument loaded ch=" << ch
|
||||
<< " type=" << (int)t << " " << path << std::endl;
|
||||
} else {
|
||||
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
|
||||
<< " type=" << (int)t << " " << path << std::endl;
|
||||
}
|
||||
});
|
||||
} else if (c.type == 1) { // PANIC
|
||||
instruments.allNotesOff();
|
||||
std::cout << "[NativeBridge] PANIC — all notes off" << std::endl;
|
||||
@@ -154,6 +288,52 @@ int main(int argc, char* argv[]) {
|
||||
} else if (c.arg0 == 2) { // SET_POSITION (seek while stopped)
|
||||
playheadSamples = c.arg1;
|
||||
}
|
||||
} else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id
|
||||
// ponytail: per-plugin channel mapping chua co — gan GUI cho
|
||||
// instrument dau tien duoc load (smoke test = 1 instrument).
|
||||
// Chay tren CUNG ChannelWorker da load instrument: thread rieng
|
||||
// cho openGUI lai tao COM apartment moi, con plugin thi song o
|
||||
// apartment cu da chet (load thread exit) -> Nexus attached()
|
||||
// hang (gui_probe: bridge_like treo, same_thread OK).
|
||||
uint32_t guiCh = 16;
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
if (instruments.get(ch)) { guiCh = ch; break; }
|
||||
}
|
||||
if (guiCh == 16) {
|
||||
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << c.arg1
|
||||
<< " plugin=" << c.arg2 << " (no instrument loaded)" << std::endl;
|
||||
} else {
|
||||
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
|
||||
workers[guiCh]->post([&instruments, &guiWindows, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() {
|
||||
std::cerr << "[dbg] openGUI thread start hwnd=" << arg1
|
||||
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
||||
void* hwnd = (void*)(uintptr_t)arg1;
|
||||
#ifdef _WIN32
|
||||
if (arg1 == 0) {
|
||||
// B9: bridge tự tạo native window — editor VST3 đính
|
||||
// vào đây; message pump bởi ChannelWorker idle loop.
|
||||
hwnd = create_native_vst_window(arg2.c_str());
|
||||
if (!hwnd) {
|
||||
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
|
||||
return;
|
||||
}
|
||||
guiWindows[guiCh] = hwnd; // keep window alive
|
||||
}
|
||||
#else
|
||||
(void)guiWindows;
|
||||
#endif
|
||||
if (auto* inst = instruments.get(guiCh)) {
|
||||
if (inst->openGUI(hwnd))
|
||||
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
||||
else
|
||||
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << std::endl;
|
||||
}
|
||||
// Editor windows song tren thread nay — ChannelWorker
|
||||
// pump message queue khi idle (xem class comment).
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
shmIPC->controlQueueCount = 0;
|
||||
|
||||
Reference in New Issue
Block a user