c382afe2a6
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).
158 lines
9.4 KiB
Markdown
158 lines
9.4 KiB
Markdown
# 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.
|