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:
2026-08-12 22:12:43 +07:00
parent c3368d0a91
commit d8f8506077
37 changed files with 2170 additions and 419 deletions
+48 -10
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.20)
project(daw_vst_bridge LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ── 1. VST3 SDK (Steinberg, vendored as git submodule — NOT in vcpkg) ──
@@ -19,15 +19,24 @@ endif()
# Linux/macOS: pkg-config duoc dung (spec goc).
if(WIN32)
find_path(FLUIDSYNTH_INCLUDE_DIR fluidsynth.h)
find_library(FLUIDSYNTH_LIBRARY NAMES fluidsynth libfluidsynth)
find_library(FLUIDSYNTH_LIBRARY NAMES fluidsynth libfluidsynth libfluidsynth-3)
if(NOT FLUIDSYNTH_INCLUDE_DIR OR NOT FLUIDSYNTH_LIBRARY)
message(FATAL_ERROR "fluidsynth not found — cai qua vcpkg: vcpkg install fluidsynth")
endif()
# sfizz: header sfizz.hpp + lib sfizz (vcpkg export target sfizz::sfizz neu co)
find_path(SFIZZ_INCLUDE_DIR sfizz.hpp)
find_library(SFIZZ_LIBRARY NAMES sfizz)
if(NOT SFIZZ_INCLUDE_DIR OR NOT SFIZZ_LIBRARY)
message(FATAL_ERROR "sfizz not found — cai qua vcpkg: vcpkg install sfizz")
# sfizz: built from source (native_bridge/sfizz-src, see build/_sfizz.bat) —
# the sfizz port is NOT in vcpkg upstream. Fall back to vcpkg if present.
set(SFIZZ_SRC_BUILD "${CMAKE_CURRENT_SOURCE_DIR}/sfizz-src/build/library/lib/Release")
if(EXISTS "${SFIZZ_SRC_BUILD}/sfizz_static.lib")
file(GLOB SFIZZ_SRC_LIBS "${SFIZZ_SRC_BUILD}/*.lib")
set(SFIZZ_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/sfizz-src/src")
set(SFIZZ_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/sfizz-src/src")
set(SFIZZ_LIBRARY ${SFIZZ_SRC_LIBS})
else()
find_path(SFIZZ_INCLUDE_DIR sfizz.hpp)
find_library(SFIZZ_LIBRARY NAMES sfizz)
if(NOT SFIZZ_INCLUDE_DIR OR NOT SFIZZ_LIBRARY)
message(FATAL_ERROR "sfizz not found — build native_bridge/sfizz-src first")
endif()
endif()
else()
find_package(PkgConfig REQUIRED)
@@ -37,18 +46,27 @@ endif()
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/include
${FLUIDSYNTH_INCLUDE_DIRS}
${SFIZZ_INCLUDE_DIRS}
${FLUIDSYNTH_INCLUDE_DIR}
${SFIZZ_INCLUDE_DIR}
)
add_executable(daw_vst_bridge
src/main.cpp
src/NativeInstrumentEngine.cpp
src/SharedMemoryIPC.cpp
src/Vst3Instrument.cpp
)
# VST3 SDK hosting: sdk_hosting lacks the platform module loader — module_win32.cpp
# is added to the bridge itself (same approach as the audiohost sample).
if(VST3_SDK_TARGET)
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET})
target_compile_definitions(daw_vst_bridge PRIVATE HAVE_VST3SDK=1)
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET} sdk_hosting)
if(WIN32)
target_sources(daw_vst_bridge PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
)
endif()
endif()
if(WIN32)
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY})
@@ -57,3 +75,23 @@ if(WIN32)
else()
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
endif()
# ── gui_probe: standalone VST3 GUI attach probe (debug tool, not shipped) ──
add_executable(gui_probe
src/gui_probe.cpp
src/NativeInstrumentEngine.cpp
src/SharedMemoryIPC.cpp
src/Vst3Instrument.cpp
)
if(VST3_SDK_TARGET)
target_compile_definitions(gui_probe PRIVATE HAVE_VST3SDK=1)
target_link_libraries(gui_probe PRIVATE ${VST3_SDK_TARGET} sdk_hosting)
if(WIN32)
target_sources(gui_probe PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
)
endif()
endif()
if(WIN32)
target_link_libraries(gui_probe PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
endif()
@@ -7,6 +7,7 @@
#include <sfizz.hpp>
#include <map>
#include <mutex>
#include <memory>
#include <string>
#include <vector>
@@ -53,7 +54,7 @@ public:
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
private:
sfizz::Synth sfizzSynth;
sfz::Sfizz sfizzSynth;
};
// Multi-instrument host (A10): one engine instance per MIDI channel, so two
@@ -74,11 +75,12 @@ public:
// Zero L/R then sum each assigned instrument into it.
void renderAll(float* outputL, float* outputR, uint32_t numSamples);
size_t count() const { return channels_.size(); }
size_t count() const { std::lock_guard<std::mutex> lock(mu_); return channels_.size(); }
private:
static std::unique_ptr<INativeInstrument> create_instrument(InstrumentType type);
mutable std::mutex mu_;
std::map<uint32_t, std::unique_ptr<INativeInstrument>> channels_;
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
std::vector<float> scratchL_, scratchR_;
+4
View File
@@ -2,6 +2,10 @@
#pragma once
#include <cstdint>
#ifdef _WIN32
#include <windows.h>
#endif
#define AUDIO_BLOCK_SIZE 256
// WARNING: volatile is NOT a sync primitive. Real impl should use an
+2 -4
View File
@@ -29,10 +29,8 @@ public:
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
private:
void* module_; // Steinberg::IPluginFactory* (owned by module)
void* processor_; // Steinberg::Vst::IComponent*
void* controller_; // Steinberg::Vst::IEditController*
void* view_; // Steinberg::IPlugView*
// pimpl: all Steinberg SDK types live in Vst3Instrument.cpp (Vst3HostState).
void* state_;
std::string path_;
double sampleRate_;
uint32_t maxBlockSize_;
+42 -30
View File
@@ -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;
+13
View File
@@ -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);
+458 -65
View File
@@ -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
}
+141
View File
@@ -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
View File
@@ -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;
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
# G2 end-to-end smoke: DAW side creates SHM (mirrors Rust shm.rs), spawns the
# real bridge, pushes LOAD_INSTRUMENT + MIDI note, asserts non-zero audio.
# Usage: python tests/g2_bridge_smoke.py
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SF2 = r"C:\Users\locpham\SonicForgeStudio\app\storage\soundfonts\518e850f-a5d3-4790-b1f9-0c90c203c524.sf2"
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h:
sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v:
sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
ML, MR = 8, 1032
MIDI_Q, MIDI_Q_COUNT = 2064, 2832
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
def peak():
p = 0.0
for i in range(256):
l = struct.unpack_from("<f", buf, ML + i * 4)[0]
r = struct.unpack_from("<f", buf, MR + i * 4)[0]
p = max(p, abs(l), abs(r))
return p
def note_on(ch, pitch, vel):
w32(MIDI_Q_COUNT, 0)
base = MIDI_Q
buf[base] = 0x9; buf[base+1] = ch; buf[base+2] = pitch; buf[base+3] = vel
buf[base+4] = 0; buf[base+5] = 0; buf[base+6] = 0; buf[base+7] = 0
struct.pack_into("<I", buf, base + 8, 0)
w32(MIDI_Q_COUNT, 1)
def poll_peak(seconds=1.5):
# SFZ one-shots (g2_test.sfz) decay to silence in ~200ms; poll fast and
# keep the max so the audible window is not missed by a sparse sleep.
mx = 0.0
t0 = time.time()
while time.time() - t0 < seconds:
p = peak()
if p > mx: mx = p
time.sleep(0.01)
return mx
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "g2_bridge_stdout.txt")
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
results = []
try:
# SF2: load, note, expect non-silent audio
load_instrument(2, SF2)
time.sleep(0.3)
note_on(0, 60, 100)
p = poll_peak()
ok = p > 0.001
results.append(("SF2 note audio", ok, "peak=%.5f" % p))
print("SF2 PEAK=%.5f" % p)
if not ok:
logf.flush()
print("bridge out:", open(logpath, encoding="utf-8").read()[-1500:])
# SFZ on channel 1
load_instrument(3, SFZ, ch=1)
time.sleep(0.5)
note_on(1, 60, 100)
p2 = poll_peak()
ok2 = p2 > 0.001
results.append(("SFZ note audio", ok2, "peak=%.5f" % p2))
print("SFZ PEAK=%.5f" % p2)
if not ok2:
logf.flush()
print("bridge out:", open(logpath, encoding="utf-8").read()[-1500:])
# VST3 (native hosting, no Carla): mda-vst3 bundle from SDK build
VST3 = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "VST3", "Release", "mda-vst3.vst3"))
load_instrument(0, VST3, ch=2)
# wait until bridge reports the load (poll log), up to 10s
deadline = time.time() + 10
while time.time() < deadline:
logf.flush()
if "instrument loaded ch=2" in open(logpath, encoding="utf-8").read():
break
if proc.poll() is not None:
print("BRIDGE DIED rc=", proc.poll())
break
time.sleep(0.2)
note_on(2, 60, 100)
p3 = poll_peak(2.0)
ok3 = p3 > 0.001
results.append(("VST3 note audio", ok3, "peak=%.5f" % p3))
print("VST3 PEAK=%.5f" % p3)
logf.flush()
print("bridge out tail:", open(logpath, encoding="utf-8").read()[-2500:])
if not ok3:
print("bridge out:", open(logpath, encoding="utf-8").read()[-2500:])
finally:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
failed = [r for r in results if not r[1]]
print("G2 RESULT:", "FAIL" if failed else "PASS")
for r in results: print(" %-20s %s %s" % (r[0], "PASS" if r[1] else "FAIL", r[2]))
sys.exit(1 if failed else 0)
+1
View File
@@ -0,0 +1 @@
<region> sample=dummy1.wav key=60 lokey=0 hikey=127
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
# Sequence matrix: which load order crashes?
# usage: repro_seq.py <seq> e.g. "sfz,vst3" "sf2,vst3" "vst3,sfz" "sf2,sfz,vst3"
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SF2 = r"C:\Users\locpham\SonicForgeStudio\app\storage\soundfonts\518e850f-a5d3-4790-b1f9-0c90c203c524.sf2"
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
VST3 = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "VST3", "Release", "mda-vst3.vst3"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h: sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v: sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
seq = sys.argv[1] if len(sys.argv) > 1 else "sfz,vst3"
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "seq_%s.txt" % seq.replace(",", "_"))
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
try:
for i, name in enumerate(seq.split(",")):
if name == "sf2": load_instrument(2, SF2, ch=0)
elif name == "sfz": load_instrument(3, SFZ, ch=1)
elif name == "vst3": load_instrument(0, VST3, ch=2)
time.sleep(0.6)
if proc.poll() is not None:
print("DIED after %s rc=%s" % (name, proc.poll()))
break
else:
time.sleep(2)
print("ALIVE after %s rc=%s" % (seq, proc.poll()))
logf.flush()
print("log:")
print(open(logpath, encoding="utf-8").read()[:1500])
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# Reproduce SFZ-only crash. Load SFZ ch=1, no notes, poll every 0.2s.
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
ML, MR = 8, 1032
MIDI_Q, MIDI_Q_COUNT = 2064, 2832
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h: sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v: sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
def note_on(ch, pitch, vel):
w32(MIDI_Q_COUNT, 0)
base = MIDI_Q
buf[base] = 0x9; buf[base+1] = ch; buf[base+2] = pitch; buf[base+3] = vel
buf[base+4] = 0; buf[base+5] = 0; buf[base+6] = 0; buf[base+7] = 0
struct.pack_into("<I", buf, base + 8, 0)
w32(MIDI_Q_COUNT, 1)
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "repro_sfz.txt")
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
try:
load_instrument(3, SFZ, ch=1)
t0 = time.time()
last_line = ""
while time.time() - t0 < 8:
if proc.poll() is not None:
print("DIED after %.2fs rc=%s" % (time.time() - t0, proc.poll()))
break
time.sleep(0.2)
else:
print("ALIVE after 8s")
logf.flush()
print("log:", open(logpath, encoding="utf-8").read())
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# SFZ: load ch=1, note_on, poll peak for 3s — see if any audio comes out.
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
ML, MR = 8, 1032
MIDI_Q, MIDI_Q_COUNT = 2064, 2832
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h: sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v: sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
def note_on(ch, pitch, vel):
w32(MIDI_Q_COUNT, 0)
base = MIDI_Q
buf[base] = 0x9; buf[base+1] = ch; buf[base+2] = pitch; buf[base+3] = vel
buf[base+4] = 0; buf[base+5] = 0; buf[base+6] = 0; buf[base+7] = 0
struct.pack_into("<I", buf, base + 8, 0)
w32(MIDI_Q_COUNT, 1)
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
def peak():
p = 0.0
for i in range(256):
l = struct.unpack_from("<f", buf, ML + i * 4)[0]
r = struct.unpack_from("<f", buf, MR + i * 4)[0]
p = max(p, abs(l), abs(r))
return p
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "repro_sfz_note.txt")
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
try:
load_instrument(3, SFZ, ch=1)
time.sleep(0.5)
note_on(1, 60, 100)
t0 = time.time()
while time.time() - t0 < 3:
p = peak()
print("t=%.2f peak=%.5f alive=%s" % (time.time() - t0, p, proc.poll() is None))
if proc.poll() is not None:
print("DIED rc=%s" % proc.poll())
break
time.sleep(0.25)
logf.flush()
print("log:", open(logpath, encoding="utf-8").read())
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
# SFZ -> VST3 sequential: does VST3 load after SFZ? dump log periodically.
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
VST3 = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "VST3", "Release", "mda-vst3.vst3"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
ML, MR = 8, 1032
MIDI_Q, MIDI_Q_COUNT = 2064, 2832
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h: sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v: sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
def note_on(ch, pitch, vel):
w32(MIDI_Q_COUNT, 0)
base = MIDI_Q
buf[base] = 0x9; buf[base+1] = ch; buf[base+2] = pitch; buf[base+3] = vel
buf[base+4] = 0; buf[base+5] = 0; buf[base+6] = 0; buf[base+7] = 0
struct.pack_into("<I", buf, base + 8, 0)
w32(MIDI_Q_COUNT, 1)
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
def peak():
p = 0.0
for i in range(256):
l = struct.unpack_from("<f", buf, ML + i * 4)[0]
r = struct.unpack_from("<f", buf, MR + i * 4)[0]
p = max(p, abs(l), abs(r))
return p
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "sfz_then_vst3_v2.txt")
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
try:
load_instrument(3, SFZ, ch=1)
time.sleep(1.0)
load_instrument(0, VST3, ch=2)
t0 = time.time()
while time.time() - t0 < 8:
logf.flush()
log = open(logpath, encoding="utf-8").read()
print("t=%.1f alive=%s vst3_loaded=%s lines=%d" % (
time.time() - t0, proc.poll() is None, "loaded ch=2" in log, log.count("\n")))
if proc.poll() is not None:
print("DIED rc=%s" % proc.poll())
break
time.sleep(1.0)
logf.flush()
print("--- log ---")
print(open(logpath, encoding="utf-8").read()[:2000])
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
+5
View File
@@ -11,6 +11,11 @@
int main() {
// Use a test-only name to avoid clashing with a live DAW bridge.
ShmHandle* h = shm_open("SonicForge_SelfCheck_IPC");
#ifdef _WIN32
// On Windows the DAW (Rust) creates the mapping; standalone self-check
// creates it here when absent.
if (!h) h = shm_create("SonicForge_SelfCheck_IPC");
#endif
assert(h != nullptr);
SharedAudioBufferIPC* ipc = shm_ptr(h);
-1
View File
@@ -3,7 +3,6 @@
"version-string": "1.0.0",
"dependencies": [
"fluidsynth",
"sfizz",
"pkgconf"
]
}