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
+68
View File
@@ -197,3 +197,71 @@ class TestMidiRenderVSTi:
resp = client.post("/api/v1/plugins/carla-play-notes", headers={"Authorization": f"Bearer {token}"},
json={"notes": [{"pitch": 60, "start_beat": 0, "duration_beats": 1}], "bpm": 120})
assert resp.status_code == 409
# --- fx-gui embed (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 1) --------------
def test_fx_gui_open_embed_returns_embed_url(self):
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
fake_proc = MagicMock()
fake_proc.stdout = iter(["some log\n", "SF_FXGUI_PORT=54321\n"])
with patch("app.core.native_render.find_bridge_exe",
return_value="C:\\bridge\\daw_vst_bridge.exe"), \
patch("app.api.v1.plugins.subprocess.Popen", return_value=fake_proc):
resp = client.post("/api/v1/plugins/fx-gui",
headers={"Authorization": f"Bearer {token}"},
json={"path": "C:\\VST\\Ozone11.vst3", "name": "Ozone 11", "embed": True})
assert resp.status_code == 200
data = resp.json()
assert data["embed_url"] == "http://127.0.0.1:54321"
assert data["started"] is True
# registry có process + path để close tìm thấy
from app.api.v1 import plugins as plugins_mod
assert any(getattr(p, "_sf_plugin_path", None) == "C:\\VST\\Ozone11.vst3"
for p in plugins_mod._FX_GUI_PROCESSES)
plugins_mod._FX_GUI_PROCESSES.clear()
def test_fx_gui_open_no_port_is_error(self):
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
fake_proc = MagicMock()
fake_proc.stdout = iter(["never prints port\n"])
with patch("app.core.native_render.find_bridge_exe",
return_value="C:\\bridge\\daw_vst_bridge.exe"), \
patch("app.api.v1.plugins.subprocess.Popen", return_value=fake_proc):
resp = client.post("/api/v1/plugins/fx-gui",
headers={"Authorization": f"Bearer {token}"},
json={"path": "C:\\VST\\Ozone11.vst3", "embed": True})
assert resp.status_code == 500
assert "port" in resp.json()["detail"]
fake_proc.terminate.assert_called_once()
def test_fx_gui_close_kills_process(self):
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
from app.api.v1 import plugins as plugins_mod
proc = MagicMock()
proc.poll.return_value = None
proc._sf_plugin_path = "C:\\VST\\Ozone11.vst3"
plugins_mod._FX_GUI_PROCESSES.append(proc)
try:
resp = client.post("/api/v1/plugins/fx-gui/close",
headers={"Authorization": f"Bearer {token}"},
json={"path": "C:\\VST\\Ozone11.vst3"})
finally:
plugins_mod._FX_GUI_PROCESSES.clear()
assert resp.status_code == 200
assert resp.json()["closed"] is True
proc.terminate.assert_called_once()
def test_fx_gui_close_unknown_plugin(self):
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
resp = client.post("/api/v1/plugins/fx-gui/close",
headers={"Authorization": f"Bearer {token}"},
json={"path": "C:\\VST\\NoSuchPlugin.vst3"})
assert resp.status_code == 200
assert resp.json()["closed"] is False