fix(bridge): load dung VSTi theo track (V9) - bo early-return mui theo flag, load qua Tauri invoke khi co path, guard path sai (WaveObserver vs Nexus), chi mo GUI khi load OK; sync 6 ban JS + index.html ?v; kem cac fix bridge truoc (bugs 1-4, GUI restore)

This commit is contained in:
2026-08-18 16:56:05 +07:00
parent cbcc1f658c
commit 351ee56080
11 changed files with 2234 additions and 2019 deletions
+18 -1
View File
@@ -131,6 +131,21 @@ void sendResponse(SOCKET c, int code, const char* contentType,
if (len > 0) send(c, (const char*)data, (int)len, 0);
}
// CORS preflight reply: the embed page (origin 127.0.0.1:8000) POSTs JSON to
// this server (a different port), so the browser sends an OPTIONS request
// first. Without this handler the preflight fell into the /input JSON parser
// and failed with 400, so the embedded GUI looked frozen: frames streamed but
// clicks never reached the plugin.
void sendOptions(SOCKET c) {
const char* r =
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type\r\n"
"Content-Length: 0\r\nConnection: close\r\n\r\n";
send(c, r, (int)std::strlen(r), 0);
}
// --- frame capture -----------------------------------------------------------
// Capture the window client rect twice — BitBlt (GDI view) and PrintWindow
// with PW_RENDERFULLCONTENT (DWM composition, includes DirectX/GPU surfaces
@@ -288,7 +303,9 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
HttpReq req;
if (!readRequest(c, req)) { closesocket(c); continue; }
if (req.path == "/ping") {
if (req.method == "OPTIONS") {
sendOptions(c);
} else if (req.path == "/ping") {
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else if (req.path == "/frame") {
std::vector<unsigned char> jpg;
+97 -2
View File
@@ -46,6 +46,9 @@ bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sam
fluid_settings_setint(FS_SETTINGS, "synth.verbose", 0);
synth = new_fluid_synth(FS_SETTINGS);
if (!synth) return false;
// Unity baseline; auto-calibration trong InstrumentEngineManager::calibrate
// can bang peak ve -6 dBFS cho moi engine (VST2/VST3/SF2/SFZ).
fluid_synth_set_gain(FS_SYNTH, 1.0f);
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)
@@ -65,7 +68,12 @@ void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32
if (!synth || channel >= 16) return;
bank_[channel] = bank;
program_[channel] = program;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program);
// SF2 fallback: nhieu soundfont khong co bank 128 (GM drum) — neu select
// fail, thu lai bank 0 de khong cam (fluidsynth giu preset cu -> sai am).
if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program) != FLUID_OK && bank != 0) {
bank_[channel] = 0;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program);
}
}
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
@@ -92,7 +100,10 @@ void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
// Dung bank da nhan tu CC0/CC32 — bank hardcode 0 lam preset o bank != 0
// khong duoc chon (fluid giu preset cu -> ra piano sai).
program_[channel] = program;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program);
if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program) != FLUID_OK && bank_[channel] != 0) {
bank_[channel] = 0;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program);
}
}
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
@@ -224,6 +235,9 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
if (!loaded) return false;
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
if (!inst->init(sampleRate, blockSize)) return false;
// Calibrate output level (makeup) - probe render truoc khi publish vao map
// de renderAll ap dung ngay, khong block audio thread.
const float mk = calibrate(inst.get(), sampleRate, blockSize);
// Replacing an existing instrument drops its voices with the old engine.
// 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.
@@ -244,6 +258,7 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
// flows to it again. G3.3: a reload also clears the crash-mute.
reloadingCh_[channel] = false;
crashedCh_[channel] = false;
makeup_[channel] = mk;
}
if (oldInst) {
isBypassed_.store(true);
@@ -271,6 +286,7 @@ void InstrumentEngineManager::unload(uint32_t channel) {
channels_.erase(channel);
paths_.erase(channel);
types_.erase(channel);
makeup_.erase(channel);
reloadingCh_[channel] = false;
}
if (oldInst) {
@@ -459,6 +475,63 @@ static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR
}
#endif
// --- Auto-calibration (am luong on dinh moi engine VST2/VST3/SF2/SFZ) ---
// Probe-render 3 notes (60/64/67, velocity 0.8) ngay sau khi load, do peak,
// tinh makeup sao cho note don ra ~ -6 dBFS (0.5). Clamp: khong bu patch cam
// qua muc, khong tat patch to. renderAll ap dung makeup + peak-safety @1.0
// (chord/transient vuot 1.0 se bi keo ve 1.0 - khong clip, khong meo).
static constexpr float kCalTargetPeak = 0.5f;
static constexpr float kCalMinMakeup = 0.125f;
static constexpr float kCalMaxMakeup = 8.0f;
float InstrumentEngineManager::calibrate(INativeInstrument* inst, double sampleRate, uint32_t blockSize) {
if (!inst) return 1.0f;
const uint32_t bs = blockSize ? blockSize : 512;
const uint32_t body = (uint32_t)(sampleRate * 0.6);
const uint32_t tail = (uint32_t)(sampleRate * 0.25);
std::vector<float> L(bs + 64, 0.0f), R(bs + 64, 0.0f);
const int pitches[3] = { 60, 64, 67 };
float peak = 0.0f;
auto renderProbe = [&](uint32_t n) {
#ifdef _WIN32
DWORD code = 0;
#else
uint32_t code = 0;
#endif
if (!SafeProcessChannel(inst, L.data(), R.data(), n, &code)) return false;
for (uint32_t i = 0; i < n; ++i) {
float a = L[i]; if (a < 0.0f) a = -a; if (a > peak) peak = a;
float b = R[i]; if (b < 0.0f) b = -b; if (b > peak) peak = b;
}
return true;
};
for (int p : pitches) {
inst->noteOn(0, (uint32_t)p, 0.8f, 0);
uint32_t off = 0;
while (off < body) {
uint32_t n = bs; if (off + n > body) n = body - off;
if (!renderProbe(n)) { inst->noteOff(0, (uint32_t)p, 0); return 1.0f; }
off += n;
}
inst->noteOff(0, (uint32_t)p, 0);
off = 0;
while (off < tail) {
uint32_t n = bs; if (off + n > tail) n = tail - off;
if (!renderProbe(n)) return 1.0f;
off += n;
}
}
if (peak < 1e-4f) {
std::cerr << "[Calibrate] silent probe peak=" << peak << " makeup=1.0" << std::endl;
return 1.0f;
}
float g = kCalTargetPeak / peak;
if (g < kCalMinMakeup) g = kCalMinMakeup;
if (g > kCalMaxMakeup) g = kCalMaxMakeup;
std::cerr << "[Calibrate] peak=" << peak << " makeup=" << g << std::endl;
return g;
}
void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) {
if (isBypassed_.load()) {
std::memset(outputL, 0, numSamples * sizeof(float));
@@ -519,9 +592,31 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
}
continue;
}
// Auto-normalize: makeup calibrated (constant per-channel gain, no steps).
auto mit = makeup_.find(ch);
const float g = (mit != makeup_.end()) ? mit->second : 1.0f;
for (uint32_t i = 0; i < numSamples; ++i) { spL[i] *= g; spR[i] *= g; }
for (uint32_t i = 0; i < numSamples; ++i) {
outputL[i] += spL[i];
outputR[i] += spR[i];
}
}
// Smoothed brickwall limiter on the summed mix: fast attack (~2-3 samples)
// when the mix exceeds 1.0, slow release back to unity. Replaces the old
// per-block hard clamp whose gain step at block boundaries caused zipper
// crackle. limiterGain_ persists across renderAll calls.
const float kAttack = 0.4f;
const float kRelease = 0.0006f;
for (uint32_t i = 0; i < numSamples; ++i) {
float a = outputL[i] < 0.0f ? -outputL[i] : outputL[i];
float b = outputR[i] < 0.0f ? -outputR[i] : outputR[i];
const float m = a > b ? a : b;
const float target = (m * limiterGain_ > 1.0f) ? (1.0f / (m + 1e-12f)) : 1.0f;
if (target < limiterGain_)
limiterGain_ += (target - limiterGain_) * kAttack;
else
limiterGain_ += (1.0f - limiterGain_) * kRelease;
outputL[i] *= limiterGain_;
outputR[i] *= limiterGain_;
}
}
+7 -8
View File
@@ -1170,13 +1170,12 @@ int main(int argc, char* argv[]) {
}
});
}
// FIX auto-play: KHONG tu bat transport khi restore state -- bridge
// khong duoc tu phat am. Frontend la chu transport: page dang play
// that su se tu gui PLAY lai sau reconnect; page fresh thi im lang.
if (snap.playing) {
playheadSamples = (uint64_t)snap.playhead;
transportStopped = false;
g_transportPlaying = true;
g_stateDirty = true;
std::cout << "[NativeBridge] restoring transport PLAY playhead="
<< playheadSamples.load() << std::endl;
std::cout << "[NativeBridge] restore transport SKIPPED (snap.playing=true)"
<< std::endl;
}
} else {
std::cout << "[NativeBridge] no state file to restore (" << stateFile << ")" << std::endl;
@@ -1386,8 +1385,8 @@ int main(int argc, char* argv[]) {
{
std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) {
// G0.1: giu entry khi dang PLAY - handleOpenGui se defer lai
if (transportStopped && instruments.has(it->first)) {
// G0.1: fulfill as soon as instrument is ready, even while playing
if (instruments.has(it->first)) {
readyGui.push_back({ it->first, it->second });
it = g_pendingGui.erase(it);
} else ++it;