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
+157
View File
@@ -0,0 +1,157 @@
# 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.