// native_bridge/src/SandboxVst2Host.h // G4.4: VST2 instrument (jBridge stubs like Qin_RV) hosted in a CHILD process. // jBridge 1.6b3 allows only ONE instance of a given plugin per host process - // the second instance's shared audio buffer already exists, so it stays // silent. Each child process is the "first" Bridger64 instance for its own // process, so any number of Qin tracks produce audio. The child owns its GUI // window (opened on control 4 = OPEN_GUI). #ifndef SANDBOX_VST2_HOST_H #define SANDBOX_VST2_HOST_H #include "INativeInstrument.h" #include "SharedMemoryIPC.h" #include #include #include #ifdef _WIN32 #include #endif class SandboxVst2Host : public INativeInstrument { public: SandboxVst2Host(); ~SandboxVst2Host() override; bool loadPlugin(const std::string& path, double sampleRate, uint32_t channel); bool init(double sampleRate, uint32_t maxBlockSize) override; void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) override {} void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) override; void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) override; void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override; void programChange(uint32_t channel, uint32_t program) override; void pitchBend(uint32_t channel, uint32_t bend14) override; // The child owns its GUI window (plugin_host opens it on OPEN_GUI control). bool openGUI(void* parentWindowHandle) override; bool canOpenGUI() const override { return true; } bool ownsGuiWindow() const override { return true; } // child owns its top-level window // The child renders at consumer pace and sleeps 15ms while idle - the // calibration probe must run in real-time or it only sees silence/attack. bool probeNeedsRealtime() const override { return true; } bool attachView(void* parentWindowHandle) override; // bridge OPEN_GUI job calls this bool hasAttachedView() const override { return guiOpen_.load(); } void closeGUI() override; void guiIdle() override {} // child pumps effEditIdle itself // G4.5: preset state lives in the child. The child republishes its // chunk into presetOut (after load / presetIn apply / GUI edits); // serializeState reads it. loadSerializedState writes presetIn for // the child to apply on its next loop pass. void captureState() override {} void restoreState() override {} std::string serializeState() const override; void loadSerializedState(const std::string& base64) override; void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override; private: bool spawnChild(); void watchdogLoop(); void pushControl(uint32_t type, uint32_t arg0 = 0, uint32_t arg1 = 0); void seedDefaultPreset(); void queuePreset(const std::string& base64, bool waitApply = false); std::string path_; std::string shmName_; std::string lastPreset_; // last preset queued (seed or real patch) — re-queued on watchdog respawn // G4.7: local copy of the last consumed child block. The DAW may call // processAudioBlock in segments within one block; serve every segment // from ONE coherent child block (copied on slot change). lastWriteSlot_ // starts 0 == zeroed shm slot 0, so the first child publish (slot 1) // always triggers the copy; before that we serve silence. float lastLeft_[AUDIO_BLOCK_SIZE] = {}; float lastRight_[AUDIO_BLOCK_SIZE] = {}; uint32_t lastWriteSlot_ = 0; double sampleRate_ = 44100.0; uint32_t block_ = AUDIO_BLOCK_SIZE; uint32_t channel_ = 0; ShmHandle* shm_ = nullptr; SandboxHostIPC* ipc_ = nullptr; std::atomic alive_{false}; std::atomic stop_{false}; std::atomic guiOpen_{false}; std::thread watchdog_; #ifdef _WIN32 HANDLE childProc_ = nullptr; #endif }; #endif // SANDBOX_VST2_HOST_H