fix(fx-gui): meter VST chạy đúng — bỏ điều kiện inRead chặn peek + chống race lúc load + ẩn cửa sổ native floating

This commit is contained in:
2026-08-22 17:36:31 +07:00
parent 8346bc3041
commit 4bb0db2b21
7 changed files with 60 additions and 90 deletions
+15 -1
View File
@@ -1,4 +1,4 @@
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging
import numpy as np import numpy as np
import soundfile as sf import soundfile as sf
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header, Query, WebSocket, WebSocketDisconnect from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header, Query, WebSocket, WebSocketDisconnect
@@ -17,6 +17,8 @@ from app.core.auth import decode_token
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__)
UPLOAD_SF_DIR = os.path.join(settings.STORAGE_DIR, "soundfonts") UPLOAD_SF_DIR = os.path.join(settings.STORAGE_DIR, "soundfonts")
os.makedirs(UPLOAD_SF_DIR, exist_ok=True) os.makedirs(UPLOAD_SF_DIR, exist_ok=True)
@@ -871,6 +873,18 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
except Exception: except Exception:
proc.kill() proc.kill()
raise HTTPException(status_code=500, detail="Bridge không báo port fx-gui (mở GUI thất bại).") raise HTTPException(status_code=500, detail="Bridge không báo port fx-gui (mở GUI thất bại).")
# Keep draining stdout in a daemon thread: the bridge logs to
# stdout/stderr while running; if the pipe fills (64KB) the bridge
# blocks on any later write -> GUI freezes/breaks.
def _drain_fx_gui_stdout(pp):
try:
for ln in pp.stdout:
ln = (ln or "").strip()
if ln:
logger.info("[fx-gui] %s", ln)
except Exception:
pass
threading.Thread(target=_drain_fx_gui_stdout, args=(proc,), daemon=True).start()
proc._sf_plugin_path = plugin_path proc._sf_plugin_path = plugin_path
proc._sf_embed_url = embed_url proc._sf_embed_url = embed_url
_register_fx_gui_process(proc) _register_fx_gui_process(proc)
+23 -77
View File
@@ -270,52 +270,14 @@ uint64_t frameHash(HWND hwnd) {
} }
void danceStart(HWND hwnd, HWND target, int x, int y) { void danceStart(HWND hwnd, HWND target, int x, int y) {
// Anchored mode: place the host window's client area exactly over the embed // Embedded mode: the native window NEVER comes on-screen — showing it
// panel rect (borderless + topmost) so real mouse events hit the plugin // topmost over the panel was the "floating always-on-top" bug. Real
// without moving the cursor, then ask the pump thread to size the plugin // hardware input would require the window on-screen, so it is disabled;
// view to the panel. No park deadline -- the window stays until /close. // relayInput always uses synthetic PostMessage.
// Re-runs on every click so a scrolled/resized panel re-anchors. // ponytail: re-enable real input only with a proper sibling placement
if (g_embedRect.right > g_embedRect.left && g_embedRect.bottom > g_embedRect.top) { // (owned window, WS_EX_NOACTIVATE, no WS_EX_TOPMOST) if a plugin rejects
const int cw = g_embedRect.right - g_embedRect.left; // synthetic clicks.
const int ch = g_embedRect.bottom - g_embedRect.top; (void)hwnd; (void)target; (void)x; (void)y;
LONG_PTR style = GetWindowLongPtrA(hwnd, GWL_STYLE);
if (style & (WS_CAPTION | WS_THICKFRAME | WS_SYSMENU |
WS_MINIMIZEBOX | WS_MAXIMIZEBOX)) {
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU |
WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
SetWindowLongPtrA(hwnd, GWL_STYLE, style);
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
SetWindowPos(hwnd, HWND_TOPMOST, g_embedRect.left, g_embedRect.top,
cw, ch, SWP_NOACTIVATE | SWP_SHOWWINDOW);
fxGuiRequestResize(hwnd, cw, ch);
Sleep(60); // let the compositor repaint so the editor sees itself active
g_dance = true;
g_anchored = true;
g_parkUntil = 0; // anchored: stay until /close
return;
}
if (g_dance) return;
// Dat window ngay tai vi tri cursor (dang nam tren panel embed trong DAW)
// chu khong nhay ra goc man hinh: shift window sao cho client point (x,y)
// cua editor trung voi screen point dang click -> GUI xuat hien dung cho
// panel, khong con floating ngoai cua so DAW.
POINT pt = { x, y };
ClientToScreen(target, &pt); // screen cua diem click (window con offscreen)
POINT cur;
GetCursorPos(&cur);
RECT wr = {0};
GetWindowRect(hwnd, &wr);
const int nx = wr.left + (cur.x - pt.x);
const int ny = wr.top + (cur.y - pt.y);
GetCursorPos(&g_savedCursor);
SetWindowPos(hwnd, HWND_TOP, nx, ny, 0, 0,
SWP_NOSIZE | SWP_SHOWWINDOW);
Sleep(60); // let the compositor repaint so the editor sees itself active
g_dance = true;
g_parkUntil = GetTickCount() + 3000;
} }
void danceEnd(HWND hwnd) { void danceEnd(HWND hwnd) {
@@ -382,12 +344,7 @@ void relayInput(HWND hwnd, const json_object_s* o) {
// responds (reset miss); two consecutive no-response clicks mean the // responds (reset miss); two consecutive no-response clicks mean the
// editor rejects synthetic input — switch to real hardware input. // editor rejects synthetic input — switch to real hardware input.
if (!g_realInput) { if (!g_realInput) {
if (g_embedRect.right > g_embedRect.left) { if (isDown) {
// Anchored mode exists for plugins that need real input; the window
// appears exactly over the panel, so probing PostMessage first only
// wastes clicks.
g_realInput = true;
} else if (isDown) {
g_preClickHash = frameHash(hwnd); g_preClickHash = frameHash(hwnd);
} else if (isUp && g_preClickHash) { } else if (isUp && g_preClickHash) {
Sleep(250); Sleep(250);
@@ -396,8 +353,7 @@ void relayInput(HWND hwnd, const json_object_s* o) {
if (resp) { if (resp) {
g_miss = 0; // PostMessage responded — keep it g_miss = 0; // PostMessage responded — keep it
} else if (++g_miss >= 2) { } else if (++g_miss >= 2) {
g_realInput = true; // synthetic input rejected — go real g_miss = 0; // real-input mode disabled: window must stay off-screen
g_miss = 0;
} }
g_preClickHash = 0; g_preClickHash = 0;
} }
@@ -556,29 +512,19 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
} }
} }
if (sroot) std::free(sroot); if (sroot) std::free(sroot);
if (rectOk) { // Embedded mode: the native window NEVER comes on-screen — it stays
const int cw = g_embedRect.right - g_embedRect.left; // parked off-screen at -20000,-20000 (GDI capture + synthetic input
const int ch = g_embedRect.bottom - g_embedRect.top; // work regardless of position). The panel rect is still recorded
RECT cur = {}; // for the (disabled) real-input fallback and to keep the API
GetWindowRect(hwnd, &cur); // contract; /hide hid the window, so re-show it off-screen so
const bool same = cur.left == g_embedRect.left && cur.top == g_embedRect.top && // /frame keeps capturing.
cur.right - cur.left == cw && cur.bottom - cur.top == ch; (void)rectOk;
// Frontend re-POSTs /show every ~500ms to re-anchor on scroll; if (!IsWindowVisible(hwnd))
// skip both when nothing moved so the plugin view is not SetWindowPos(hwnd, HWND_NOTOPMOST, -20000, -20000, 0, 0,
// re-laid-out in a loop. Re-show after /hide regardless. SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
if (!same || !IsWindowVisible(hwnd)) g_dance = false;
SetWindowPos(hwnd, HWND_TOPMOST, g_embedRect.left, g_embedRect.top, g_anchored = false;
cw, ch, SWP_NOACTIVATE | SWP_SHOWWINDOW); g_parkUntil = 0;
if (cur.right - cur.left != cw || cur.bottom - cur.top != ch)
fxGuiRequestResize(hwnd, cw, ch);
g_dance = true;
g_anchored = true;
g_parkUntil = 0;
} else {
ShowWindow(hwnd, SW_SHOW);
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11); sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else if (req.path == "/preset") { } else if (req.path == "/preset") {
std::string b64; std::string b64;
+22 -12
View File
@@ -1433,12 +1433,14 @@ static void fxGuiFeedLoop(Vst3Fx* fx, HWND hwnd, std::shared_ptr<FxGuiShmCtl> ct
if (name != curName) { if (name != curName) {
if (v) { fxGuiCloseShm(v); v = nullptr; ipc = nullptr; } if (v) { fxGuiCloseShm(v); v = nullptr; ipc = nullptr; }
curName = name; curName = name;
if (!name.empty()) v = fxGuiOpenShm(name, sizeof(FxRealtimeIPC));
ipc = v ? static_cast<FxRealtimeIPC*>(v->view) : nullptr;
lastWrite = 0; // session (re)start -> re-anchor lastWrite = 0; // session (re)start -> re-anchor
} }
if (!v && !name.empty()) { // retry open each pass: mapping may appear late
v = fxGuiOpenShm(name, sizeof(FxRealtimeIPC));
ipc = v ? static_cast<FxRealtimeIPC*>(v->view) : nullptr;
}
if (ipc && ipc->h.magic == FXRT_MAGIC && if (ipc && ipc->h.magic == FXRT_MAGIC &&
ipc->h.inWrite > lastWrite && ipc->h.inWrite > ipc->h.inRead) { ipc->h.inWrite > lastWrite) {
if (ipc->h.inSlots == FXRT_IN_SLOTS && ipc->h.blockSize == FXRT_BLOCK) { if (ipc->h.inSlots == FXRT_IN_SLOTS && ipc->h.blockSize == FXRT_BLOCK) {
const uint32_t slot = (ipc->h.inWrite - 1) & (ipc->h.inSlots - 1); const uint32_t slot = (ipc->h.inWrite - 1) & (ipc->h.inSlots - 1);
float L[FXRT_BLOCK], R[FXRT_BLOCK], oL[FXRT_BLOCK], oR[FXRT_BLOCK]; float L[FXRT_BLOCK], R[FXRT_BLOCK], oL[FXRT_BLOCK], oR[FXRT_BLOCK];
@@ -1453,15 +1455,21 @@ static void fxGuiFeedLoop(Vst3Fx* fx, HWND hwnd, std::shared_ptr<FxGuiShmCtl> ct
if (v) fxGuiCloseShm(v); if (v) fxGuiCloseShm(v);
} }
// Detached feeder thread. initialShm may be "" — the frontend pushes the // Split feeder setup: fxGuiShmSetName runs EARLY (before the server loop) so
// live session name later via /shm once the realtime session is up. // /shm pushes from the frontend are not lost, while the detached thread starts
static void fxGuiStartAudioFeeder(Vst3Fx* fx, HWND hwnd, const std::string& initialShm) { // only AFTER load+attach succeeded — a feeder calling processAudio() mid-load
// can crash/corrupt a heavy plugin and break the editor attach.
// ponytail: feeder feeds the raw live pre-chain mix; add a separate audio path
// if GUI processing of the full insert chain is wanted later.
static void fxGuiShmSetName(const std::string& initialShm) {
auto p = fxGuiShmCtlRef();
if (!p) { p = std::make_shared<FxGuiShmCtl>(); fxGuiShmCtlRef() = p; }
std::lock_guard<std::mutex> lk(p->m);
p->name = initialShm;
}
static void fxGuiStartFeederThread(Vst3Fx* fx, HWND hwnd) {
auto p = fxGuiShmCtlRef(); auto p = fxGuiShmCtlRef();
if (!p) { p = std::make_shared<FxGuiShmCtl>(); fxGuiShmCtlRef() = p; } if (!p) { p = std::make_shared<FxGuiShmCtl>(); fxGuiShmCtlRef() = p; }
{
std::lock_guard<std::mutex> lk(p->m);
p->name = initialShm;
}
std::thread([fx, hwnd, p]() { fxGuiFeedLoop(fx, hwnd, p); }).detach(); std::thread([fx, hwnd, p]() { fxGuiFeedLoop(fx, hwnd, p); }).detach();
} }
@@ -1560,7 +1568,8 @@ int run_open_fx_gui(const std::string& jobPath) {
const int rc = fxGuiSetup(jobPath, &fx, &hwnd, /*offscreen=*/false, (LONG_PTR)&ctx, shm); const int rc = fxGuiSetup(jobPath, &fx, &hwnd, /*offscreen=*/false, (LONG_PTR)&ctx, shm);
if (rc) return rc; if (rc) return rc;
fxGuiStartWatchdog(hwnd, fxGuiParentPid()); fxGuiStartWatchdog(hwnd, fxGuiParentPid());
fxGuiStartAudioFeeder(&fx, hwnd, shm); fxGuiShmSetName(shm);
fxGuiStartFeederThread(&fx, hwnd);
// 5. Pump messages until the window is destroyed. // 5. Pump messages until the window is destroyed.
MSG msg; MSG msg;
@@ -1598,11 +1607,12 @@ int run_fx_gui_server(const std::string& jobPath) {
if (rc) return rc; if (rc) return rc;
fxGuiStartWatchdog(hwnd, fxGuiParentPid()); fxGuiStartWatchdog(hwnd, fxGuiParentPid());
SetPropA(hwnd, "FXGUI_FX", &fx); // for WM_FXGUI_RESIZE on the pump thread SetPropA(hwnd, "FXGUI_FX", &fx); // for WM_FXGUI_RESIZE on the pump thread
fxGuiStartAudioFeeder(&fx, hwnd, shm); fxGuiShmSetName(shm); // EARLY — frontend may /shm push before load finishes
int loadRc = 0; int loadRc = 0;
const int srv = fxGuiServerLoop(hwnd, [&]() { const int srv = fxGuiServerLoop(hwnd, [&]() {
loadRc = fxGuiLoadAttach(&fx, path, name, hwnd); loadRc = fxGuiLoadAttach(&fx, path, name, hwnd);
if (loadRc == 0) fxGuiStartFeederThread(&fx, hwnd); // after load+attach only
if (loadRc != 0) PostMessageA(hwnd, WM_CLOSE, 0, 0); // exit; port already printed if (loadRc != 0) PostMessageA(hwnd, WM_CLOSE, 0, 0); // exit; port already printed
}); });
RemovePropA(hwnd, "FXGUI_FX"); RemovePropA(hwnd, "FXGUI_FX");
Binary file not shown.
Binary file not shown.