feat: embed native VST GUI in mastering + fx rack panels (bridge --fx-gui frame capture + input relay)

Linux parts (0L1-4L1 per TASKS_LINUX_WINDOWS.md):
- native_bridge: FxGuiServer.cpp HTTP server (Winsock) — /frame BitBlt +
  PrintWindow fallback -> JPEG (libjpeg-turbo q70), /input PostMessage relay
  (mouse/wheel/key, coords scaled), /close, /ping; RenderFxJob fxGuiSetup +
  run_fx_gui_server (offscreen); main.cpp --fx-gui dispatch; vcpkg +
  CMakeLists WIN32-only + JPEG.
- api: open_fx_gui embed flag spawn --fx-gui, parse SF_FXGUI_PORT -> embed_url,
  fx-gui/close endpoint; 4 unit tests.
- ui: VstGuiEmbed component (frame refresh 100ms, ping 2s, input relay,
  scale coords) shared by MasteringModal + FXRackModal; swap graph<->GUI,
  cleanup on unmount, dead-bridge toast fallback.
- docs: TASKS_LINUX_WINDOWS.md, WALKTHROUGH_WINDOWS.md, PLAN status.
Windows tasks 0W1-4W1 remain (build + E2E real VST).
This commit is contained in:
2026-08-17 22:37:55 +07:00
parent 38f4cbd8b5
commit c382afe2a6
13 changed files with 3098 additions and 2051 deletions
+58 -1
View File
@@ -503,6 +503,14 @@ class FxGuiRequest(BaseModel):
"""Mở native GUI của VST FX qua bridge (Windows desktop)."""
path: str
name: Optional[str] = None
# embed=True → bridge --fx-gui (off-screen + HTTP frame/input server,
# PLAN_MASTERBUS_VST_GUI_EMBED.md); embed=False → cửa sổ rời cũ.
embed: bool = False
class FxGuiCloseRequest(BaseModel):
"""Đóng bridge fx-gui đã mở cho plugin (embed mode)."""
path: str
class PreviewRequest(BaseModel):
@@ -717,12 +725,41 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
except Exception:
os.close(fd)
raise HTTPException(status_code=500, detail="Không ghi được job file cho bridge.")
cmd = [exe, "--open-fx-gui", job_path]
cmd = [exe, "--fx-gui" if req.embed else "--open-fx-gui", job_path]
try:
if req.embed:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True,
close_fds=os.name != "nt")
# Bridge prints SF_FXGUI_PORT=<port> after binding its HTTP server.
embed_url = None
try:
for line in proc.stdout:
line = (line or "").strip()
if line.startswith("SF_FXGUI_PORT="):
port = int(line.split("=", 1)[1])
embed_url = f"http://127.0.0.1:{port}"
break
except Exception:
pass
if not embed_url:
proc.terminate()
try:
proc.wait(timeout=5)
except Exception:
proc.kill()
raise HTTPException(status_code=500, detail="Bridge không báo port fx-gui (mở GUI thất bại).")
proc._sf_plugin_path = plugin_path
proc._sf_embed_url = embed_url
_register_fx_gui_process(proc)
return {"success": True, "started": True, "already_running": False,
"embed_url": embed_url, "cmd": cmd}
proc = subprocess.Popen(cmd, close_fds=os.name != "nt")
proc._sf_plugin_path = plugin_path
_register_fx_gui_process(proc)
return {"success": True, "started": True, "already_running": False, "cmd": cmd}
except HTTPException:
raise
except Exception as e:
try:
os.remove(job_path)
@@ -731,6 +768,26 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
raise HTTPException(status_code=500, detail=f"Không mở được GUI của VST: {e}")
@router.post("/fx-gui/close")
async def close_fx_gui(req: FxGuiCloseRequest, current_user: dict = Depends(get_current_user)):
"""Đóng bridge fx-gui đang chạy cho plugin (embed mode) — frontend gọi khi
user đóng panel embed. Không lỗi nếu plugin chưa mở (closed=False)."""
enforce_password_changed(current_user)
plugin_path = (req.path or "").strip()
if not plugin_path:
raise HTTPException(status_code=400, detail="Thiếu đường dẫn VST plugin.")
for proc in _FX_GUI_PROCESSES:
if proc is not None and proc.poll() is None and getattr(proc, "_sf_plugin_path", None) == plugin_path:
proc.terminate()
try:
proc.wait(timeout=5)
except Exception:
proc.kill()
_prune_fx_gui_processes()
return {"success": True, "closed": True}
return {"success": True, "closed": False}
_FX_GUI_PROCESSES = [] # list[subprocess.Popen] — các bridge fx-gui do app spawn
def _register_fx_gui_process(proc):