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
+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;