Files
SonicForgeStudio/PLAN_MASTERBUS_VST_GUI_EMBED.md
3dtours c382afe2a6 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).
2026-08-17 22:37:55 +07:00

9.4 KiB

PLAN: Nhúng native VST FX GUI vào Mastering Panel + FX Rack Panel

Ngày: 2026-08-17 Trạng thái: LINUX PHẦN HOÀN THÀNH (0L1, 0L2, 1L1, 2L1, 3L1, 4L1) — commit <pending>; WINDOWS PHẦN CÒN LẠI (0W1, 0W2, 1W1, 2W1, 3W1, 4W1) — làm theo WALKTHROUGH_WINDOWS.md. Tách task: TASKS_LINUX_WINDOWS.md. Kế thừa: PLAN_MASTERBUS_FX_RACK_VST.md Phase 0-3 HOÀN THÀNH (commit b261eb0 render FX, 38f4cbd mở native VST GUI dạng cửa sổ rời qua --open-fx-gui).

1. Mục tiêu

Hoàn thiện vòng đời VST FX trong MASTERING PANEL (MasteringModal) và FX RACK PANEL (FXRackModal):

  1. Nút X trên slot plugin → xóa plugin khỏi ô slot. → ĐÃ CÓ (removeVst/removeMasterVst + <button> X trong vstRow, app/static/js/app.jsx:11757). Chỉ xác minh, không code lại.
  2. Nút + → thêm VST trực tiếp vào panel VÀ load native VST GUI ngay trong panel — thay vì cửa sổ rời như hiện tại (SonicAPI.openFxGui → bridge --open-fx-gui mở window 800x600 tách biệt).
  3. Đóng GUI → panel trở về graph GUI. Click slot VST đã load → mở lại GUI nhúng, thay thế vùng graph GUI của panel bằng native VST GUI ở đó.

2. Hiện trạng

Hạng mục Hiện tại Delta cần
Slot VST masterbus masterVstChain (vstFxChain) + vstRow giữ
Slot VST per-track track.vstFxChain + vstRow giữ
Nút X xóa slot có (removeV*) xác minh
Nút + thêm + mở GUI có (addVst/addMasterVst → openVstGui/openMasterVstGui) đổi từ cửa sổ rời → GUI nhúng trong panel
GUI mở bridge --open-fx-gui (RenderFxJob.cpp:889+, FxGuiWndProc, fxGuiAttachSafe, IPlugView attach HWND) giữ làm nền, thêm capture + input relay
Graph GUI của panel MasteringModal: vùng canvas EQ/imager/meters (eqCanvasRef…); FXRackModal: scopeCanvasRef/eqCurveRef vùng này bị thay thế khi GUI mở
Tauri src-tauri v2 có, app cũng chạy qua LAN browser GUI nhúng phải chạy cả 2 môi trường → không dùng overlay native window

Quyết định kiến trúc: app là web (browser/Tauri webview) → không thể nhúng HWND trực tiếp. Dùng frame-capture + input relay (kiểu VNC cho plugin window): bridge giữ native editor window, chụp bitmap → HTTP, frontend hiển thị trong panel và gửi pointer/key về bridge → SendMessage tới HWND plugin.

3. Kiến trúc đích

3.1. Bridge: mode --fx-gui <job.json> (C++, Windows, MIT)

Mở rộng từ --open-fx-gui hiện có (giữ nguyên phần attach IPlugView):

  • Sau khi attach view (đã có fxGuiAttachSafe), chạy HTTP server mini (port ngẫu nhiên, ghi vào job output/stdout để Python đọc):
    • GET /frame → PNG/JPEG chụp từ HWND plugin bằng BitBlt (fallback PrintWindow với PW_RENDERFULLCONTENT) — trả image/jpeg, chất lượng ~70 để giảm băng thông LAN.
    • POST /input { type: "mousedown|mousemove|mouseup|wheel|key", x, y, button, deltaY, keyCode } → PostMessage(hwnd, WM_...) theo tọa độ đã scale (window plugin có thể lớn hơn vùng hiển thị → scale client rect).
    • POST /close → detach view, thoát tiến trình (exit 0).
    • GET /ping → alive check.
  • Giữ nguyên SEH guard + registry chống mở 2 GUI (_FX_GUI_PROCESSES phía Python).
  • Vị trí: native_bridge/src/RenderFxJob.cpp (hoặc file mới FxGuiServer.cpp cạnh nó — theo kích thước, tách nếu >300 dòng).

3.2. Python API (app/api/v1/plugins.py — mở rộng open_fx_gui)

  • POST /api/v1/plugins/fx-gui/open { path, name } → spawn bridge --fx-gui, đọc port, trả { success, embed_url: "http://127.0.0.1:<port>", already_running }. Registry _FX_GUI_PROCESSES như hiện có.
  • POST /api/v1/plugins/fx-gui/close { path } → kill process (đã có luồng cleanup ở _register_fx_gui_process).
  • Không cần proxy qua FastAPI: frontend gọi thẳng embed_url (CORS: plugins.py đã có middleware — kiểm tra thêm Access-Control-Allow-Origin cho port động).

3.3. UI — app/static/js/app.jsx (dùng chung cho 2 panel qua vstRow)

  • vstRow thêm state per-slot: guiOpen: { url } | null (state nằm ở panel component, không phải trong vstRow — truyền qua props).
  • Render slot: nút GUI hiện tại → mở/đóng toggle: nếu guiOpen null → gọi openFxGuiEmbed (API mới); nếu đang mở → hiển thị vùng embed.
  • Vùng embed: <div className="..."> chứa <img src={url + '/frame'}> refresh bằng setInterval ~100ms (10fps), overlay pointer handlers (onMouseDown/Move/Up, onWheel) → fetch(url + '/input', POST) với tọa độ relative tính theo bounding rect; keydown/keyup trên container.
    • Trong MasteringModal: vùng này thay thế vùng graph (khu vực canvas EQ/imager — xác định container chính xác lúc implement; ẩn bằng hidden class khi GUI mở, hiện lại khi close).
    • Trong FXRackModal: thay thế vùng scope/eq canvas.
  • Click vào tên VST đã load trong slot (span tên hoặc chính slot) khi GUI đang đóng → mở lại embed (re-attach qua /frame poll, ping trước).
  • Nút đóng GUI: nút X riêng trên vùng embed (hoặc nút GUI toggle) → gọi /close, dừng interval, trả graph về.
  • Xóa slot (X) khi GUI đang mở → cũng phải đóng GUI (gọi /close).
  • Nếu /ping fail (bridge chết ngoài ý muốn) → tự trả về graph + toast.

3.4. Không đổi

  • Offline render (--render-fx), preset upload, Carla bridge, --scan — giữ nguyên. GUI nhúng chỉ là hiển thị/điều khiển, không đổi data model (vstFxChain giữ nguyên shape).

4. Phases

Phase Nội dung Deliverable Test
0 Bridge --fx-gui: HTTP server + BitBlt/PrintWindow capture + input relay mở GUI Ozone trong browser tab thủ công qua URL self-check: /frame trả JPEG kích thước khớp; /input click đúng vị trí
1 Python: open trả embed_url, registry, close, CORS curl: open → frame 200 unit: process spawn/cleanup
2 MasteringModal: embed + swap graph↔GUI + click mở lại UI mastering hoàn chỉnh E2E thủ công
3 FXRackModal: tương tự UI fx rack hoàn chỉnh E2E thủ công
4 Dọn: toast lỗi, cleanup interval, docs commit full test suite hiện có không vỡ

Phase 0-1 có thể merge (cùng chạm bridge + plugins.py).

5. Rủi ro / giới hạn

  • Plugin dùng layered window / DirectX surface → PrintWindow ra đen: fallback capture child control HWND hoặc dùng BitBlt từ GetDC(NULL) vùng rect — xử lý theo plugin cụ thể (Ozone chuẩn GDI, khả năng cao OK).
  • Input tọa độ: scale theo client rect; plugin bắt absolute tọa độ cần WM_MOUSEMOVE trước click (gửi mousemove kèm).
  • FPS ~10-15 đủ chỉnh thông số; không phải realtime video — ghi chú UI.
  • Chỉ Windows desktop có VST GUI (Linux bridge SF2/SFZ — giữ nguyên).
  • Keyboard input: chỉ forward khi panel đang focus, tránh nuốt phím toàn app.

6. License

  • Không đổi: native_bridge MIT, không nhúng binary plugin, không quay lại pedalboard.

7. Ghi chú triển khai (Linux — đã làm)

  • native_bridge/src/FxGuiServer.cpp (mới, ~316 dòng): HTTP server Winsock trên worker thread; /frame = BitBlt + fallback PrintWindow PW_RENDERFULLCONTENT khi ảnh toàn đen, JPEG quality 70 qua libjpeg-turbo (jpeg_mem_dest); /input relay PostMessage (mousedown/move/up, wheel deltaY*120, keydown/up, coords scale client rect); /close stop flag + WM_CLOSE; GET /404. int fxGuiServerLoop(HWND) chạy message pump STA, join HTTP thread.
  • RenderFxJob.cpp: tách fxGuiSetup() dùng chung; run_open_fx_gui giữ hành vi cũ; thêm run_fx_gui_server(jobPath) (offscreen, userData=0); stub #else !_WIN32 trả 1. main.cpp: dispatch --fx-gui <job.json>.
  • vcpkg.json thêm libjpeg-turbo; CMakeLists.txt chỉ build FxGuiServer khi WIN32 + find_package(JPEG REQUIRED) + JPEG::JPEG.
  • app/api/v1/plugins.py: FxGuiRequest.embed, model FxGuiCloseRequest, open_fx_gui spawn --fx-gui đọc SF_FXGUI_PORT= từ stdout → embed_url, trả {success, started, already_running, embed_url, cmd}; thêm POST /api/v1/plugins/fx-gui/close (terminate → wait 5s → kill → prune). CORS allow_origins=["*"] + bridge tự gửi ACAO *.
  • app/static/js/app.jsx: component VstGuiEmbed (module scope, dùng chung 2 panel) — img refresh 100ms, ping 2s, relay pointer/wheel/key với tọa độ scale, nút "✕ Đóng GUI" → /close. MasteringModal + FXRackModal: state guiOpen {path,url}, swap vùng graph↔GUI bằng ternary, cleanup unmount, bridge chết → toast + trả về graph.
  • Tests: tests/test_plugin_api.py +4 (mở embed trả url, không port → 500, close kill, close unknown) — mock find_bridge_exe + Popen.
  • Kết quả: node build.mjs BUILD OK; pytest 116 passed, 1 failed (test_find_bridge_exe_dev pre-existing — Linux thiếu binary bridge).
  • Giới hạn: bridge C++ chưa build/test thật (Linux không có CMake/Win32) — Windows làm 0W1/0W2; E2E GUI nhúng thật chưa chạy — Windows làm 1W1-4W1.