diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 43394f3..dbd0feb 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -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 soundfile as sf 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() +logger = logging.getLogger(__name__) + UPLOAD_SF_DIR = os.path.join(settings.STORAGE_DIR, "soundfonts") 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: proc.kill() 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_embed_url = embed_url _register_fx_gui_process(proc) diff --git a/native_bridge/src/FxGuiServer.cpp b/native_bridge/src/FxGuiServer.cpp index 74670db..e7f2afe 100644 --- a/native_bridge/src/FxGuiServer.cpp +++ b/native_bridge/src/FxGuiServer.cpp @@ -270,52 +270,14 @@ uint64_t frameHash(HWND hwnd) { } void danceStart(HWND hwnd, HWND target, int x, int y) { - // Anchored mode: place the host window's client area exactly over the embed - // panel rect (borderless + topmost) so real mouse events hit the plugin - // without moving the cursor, then ask the pump thread to size the plugin - // view to the panel. No park deadline -- the window stays until /close. - // Re-runs on every click so a scrolled/resized panel re-anchors. - if (g_embedRect.right > g_embedRect.left && g_embedRect.bottom > g_embedRect.top) { - const int cw = g_embedRect.right - g_embedRect.left; - const int ch = g_embedRect.bottom - g_embedRect.top; - 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; + // Embedded mode: the native window NEVER comes on-screen — showing it + // topmost over the panel was the "floating always-on-top" bug. Real + // hardware input would require the window on-screen, so it is disabled; + // relayInput always uses synthetic PostMessage. + // ponytail: re-enable real input only with a proper sibling placement + // (owned window, WS_EX_NOACTIVATE, no WS_EX_TOPMOST) if a plugin rejects + // synthetic clicks. + (void)hwnd; (void)target; (void)x; (void)y; } 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 // editor rejects synthetic input — switch to real hardware input. if (!g_realInput) { - if (g_embedRect.right > g_embedRect.left) { - // 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) { + if (isDown) { g_preClickHash = frameHash(hwnd); } else if (isUp && g_preClickHash) { Sleep(250); @@ -396,8 +353,7 @@ void relayInput(HWND hwnd, const json_object_s* o) { if (resp) { g_miss = 0; // PostMessage responded — keep it } else if (++g_miss >= 2) { - g_realInput = true; // synthetic input rejected — go real - g_miss = 0; + g_miss = 0; // real-input mode disabled: window must stay off-screen } g_preClickHash = 0; } @@ -556,29 +512,19 @@ void runHttpServer(HWND hwnd, std::atomic& stop) { } } if (sroot) std::free(sroot); - if (rectOk) { - const int cw = g_embedRect.right - g_embedRect.left; - const int ch = g_embedRect.bottom - g_embedRect.top; - RECT cur = {}; - GetWindowRect(hwnd, &cur); - const bool same = cur.left == g_embedRect.left && cur.top == g_embedRect.top && - cur.right - cur.left == cw && cur.bottom - cur.top == ch; - // Frontend re-POSTs /show every ~500ms to re-anchor on scroll; - // skip both when nothing moved so the plugin view is not - // re-laid-out in a loop. Re-show after /hide regardless. - if (!same || !IsWindowVisible(hwnd)) - SetWindowPos(hwnd, HWND_TOPMOST, g_embedRect.left, g_embedRect.top, - cw, ch, SWP_NOACTIVATE | SWP_SHOWWINDOW); - 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); - } + // Embedded mode: the native window NEVER comes on-screen — it stays + // parked off-screen at -20000,-20000 (GDI capture + synthetic input + // work regardless of position). The panel rect is still recorded + // for the (disabled) real-input fallback and to keep the API + // contract; /hide hid the window, so re-show it off-screen so + // /frame keeps capturing. + (void)rectOk; + if (!IsWindowVisible(hwnd)) + SetWindowPos(hwnd, HWND_NOTOPMOST, -20000, -20000, 0, 0, + SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + g_dance = false; + g_anchored = false; + g_parkUntil = 0; sendResponse(c, 200, "application/json", "{\"ok\":true}", 11); } else if (req.path == "/preset") { std::string b64; diff --git a/native_bridge/src/RenderFxJob.cpp b/native_bridge/src/RenderFxJob.cpp index 2e486c9..f8d7781 100644 --- a/native_bridge/src/RenderFxJob.cpp +++ b/native_bridge/src/RenderFxJob.cpp @@ -1433,12 +1433,14 @@ static void fxGuiFeedLoop(Vst3Fx* fx, HWND hwnd, std::shared_ptr ct if (name != curName) { if (v) { fxGuiCloseShm(v); v = nullptr; ipc = nullptr; } curName = name; - if (!name.empty()) v = fxGuiOpenShm(name, sizeof(FxRealtimeIPC)); - ipc = v ? static_cast(v->view) : nullptr; 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(v->view) : nullptr; + } 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) { 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]; @@ -1453,15 +1455,21 @@ static void fxGuiFeedLoop(Vst3Fx* fx, HWND hwnd, std::shared_ptr ct if (v) fxGuiCloseShm(v); } -// Detached feeder thread. initialShm may be "" — the frontend pushes the -// live session name later via /shm once the realtime session is up. -static void fxGuiStartAudioFeeder(Vst3Fx* fx, HWND hwnd, const std::string& initialShm) { +// Split feeder setup: fxGuiShmSetName runs EARLY (before the server loop) so +// /shm pushes from the frontend are not lost, while the detached thread starts +// 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(); fxGuiShmCtlRef() = p; } + std::lock_guard lk(p->m); + p->name = initialShm; +} +static void fxGuiStartFeederThread(Vst3Fx* fx, HWND hwnd) { auto p = fxGuiShmCtlRef(); if (!p) { p = std::make_shared(); fxGuiShmCtlRef() = p; } - { - std::lock_guard lk(p->m); - p->name = initialShm; - } 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); if (rc) return rc; fxGuiStartWatchdog(hwnd, fxGuiParentPid()); - fxGuiStartAudioFeeder(&fx, hwnd, shm); + fxGuiShmSetName(shm); + fxGuiStartFeederThread(&fx, hwnd); // 5. Pump messages until the window is destroyed. MSG msg; @@ -1598,11 +1607,12 @@ int run_fx_gui_server(const std::string& jobPath) { if (rc) return rc; fxGuiStartWatchdog(hwnd, fxGuiParentPid()); 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; const int srv = fxGuiServerLoop(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 }); RemovePropA(hwnd, "FXGUI_FX"); diff --git a/src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe b/src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe index e9c3fdc..0497d94 100644 Binary files a/src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe and b/src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe differ diff --git a/src-tauri/binaries/daw_vst_bridge.exe b/src-tauri/binaries/daw_vst_bridge.exe index e9c3fdc..0497d94 100644 Binary files a/src-tauri/binaries/daw_vst_bridge.exe and b/src-tauri/binaries/daw_vst_bridge.exe differ diff --git a/src-tauri/binaries/fx_vst_bridge-x86_64-pc-windows-msvc.exe b/src-tauri/binaries/fx_vst_bridge-x86_64-pc-windows-msvc.exe index b1d9e6b..ab5eb05 100644 Binary files a/src-tauri/binaries/fx_vst_bridge-x86_64-pc-windows-msvc.exe and b/src-tauri/binaries/fx_vst_bridge-x86_64-pc-windows-msvc.exe differ diff --git a/src-tauri/binaries/fx_vst_bridge.exe b/src-tauri/binaries/fx_vst_bridge.exe index b1d9e6b..ab5eb05 100644 Binary files a/src-tauri/binaries/fx_vst_bridge.exe and b/src-tauri/binaries/fx_vst_bridge.exe differ