# PLAN: Masterbus + FX Rack hỗ trợ chèn VST FX (Ozone, Scaler) — xử lý âm thanh Ngày: 2026-08-15 Trạng thái: HOÀN THÀNH Phase 0-4 (2026-08-18) — code + test pass. Kế thừa nền: `PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md` HOÀN THÀNH (Phase 0-5, commit `dc331f3`) — pedalboard GPL-3.0 đã gỡ, `RENDER_ENGINE` mặc định `"bridge"`, `native_bridge` (MIT) render VSTi offline qua `--render --out `; tests 114 pass / 0 skip. ## 1. Mục tiêu Chèn **VST FX thật** (Ozone mastering chain, Scaler, …) vào 2 tầng xử lý âm thanh: - **Masterbus**: chain FX áp lên toàn bộ mix (mastering). - **FX Rack per-track**: chain FX áp lên từng track (insert). Offline export chạy qua `native_bridge` (giữ nguyên luồng RENDER_ENGINE hiện tại). Realtime trong browser **KHÔNG chạy được VST** → giữ WebAudio builtin, VST FX dùng quick_render preview (âm thật = âm export, như cơ chế preset đang có). ## 2. Hiện trạng (cần mở rộng) | Tầng | Hiện tại | Hạn chế | |---|---|---| | Realtime browser (`app/static/js/app.jsx`) | FX chain WebAudio per-track (chorus/reverb), mastering chain Ozone-style (route dry/mastering, `createMasteringRoute`) | WebAudio node — KHÔNG chạy được binary `.vst3`; data model chỉ có `fxChain` + mastering toggle, chưa có slot plugin | | Offline export (`app/core/render_engine.py`) | Track FX: `fx_type=="chorus"|"reverb"` fallback scipy/numpy; master bus = sum track buffers + normalize peak (`max_peak`) | Không VST; master chỉ normalize, không chain; `fxChain` WebAudio không tái tạo offline | | Bridge (`native_bridge/src/*`) | Chỉ instrument host: MIDI → audio (`NativeInstrumentEngine`, VST3/SF2/SFZ) | Chưa có audio-effect processing path; `RenderJob.cpp` chỉ nhận notes, không nhận input WAV | | Scan plugin (`app/core/vst_engine.py`) | Quét `.vst3`/`.so`/`.dll`, list vst_instruments | Không phân loại instrument vs effect | Tham chiếu doc cũ: `md/45_MASTERING_MODULE.md` (spec WebAudio Ozone) — giữ nguyên, chỉ bổ sung VST path. `app.jsx` tham chiếu `mastering_expand.md` + `unified_fx_rack_panel.md` — 2 file này không tồn tại → gỡ tham chiếu hoặc viết mới trong Phase 3. ## 3. Kiến trúc đích ### 3.1. Bridge: FX render mode (C++, MIT) Thêm entrypoint CLI song song mode instrument: ``` daw_vst_bridge(.exe) --render-fx --in --out ``` job.json (mở rộng format hiện có): ```json { "sample_rate": 44100, "block_size": 512, "fx_chain": [ { "type": "vst3", "path": "C:/.../Ozone 11.vst3", "preset_b64": "...", "bypass": false }, { "type": "builtin", "id": "normalize", "params": { "peak": 0.95 } } ] } ``` Luồng: 1. Đọc input WAV (16/24/32-bit, mono/stereo → convert stereo float) — thêm WAV reader (WAV writer đã có). 2. Với mỗi slot trong `fx_chain`, process toàn bộ buffer qua `NativeFxEngine::processAudio(audio_in, audio_out)` — audio-input, KHÔNG MIDI. VST3 effect: load `plugin_path`, `loadSerializedState` từ `preset_b64` (cùng cơ chế preset đã có), `processBlock` từng block; `bypass=true` → copy thẳng. 3. `builtin` = fallback không cần VST (normalize, gain) — dùng chung cho Docker headless. 4. Ghi WAV stereo 16-bit. Exit code như mode instrument (0 ok, 1 job error, 2 load fail, 3 crash — SEH guard như `SandboxVst3Host`). Lưu ý kỹ thuật: - Phân biệt class category VST3: instrument (`kInstrument`) vs effect (`kAudioEffect`) — dùng `getClassInfo`/category string để host đúng interface (audio-in/audio-out thay vì MIDI-in). - VST2 FX (`type:"vst2"`) thêm sau nếu cần; V1 chỉ VST3 + builtin. - Linux bridge chỉ SF2/SFZ (VST3 hosting Windows-only) → FX VST chỉ Windows/desktop; Docker fallback builtin. Capabilities API báo đúng (`features.vst_fx_render`). ### 3.2. Python client (`app/core/native_render.py` — mở rộng) ```python render_fx_chain(input_wav: str, fx_chain: list[dict], sample_rate: int) -> str ``` Spawn bridge `--render-fx`, trả path WAV. Chịu trách nhiệm: resolve plugin path, base64 encode preset file, timeout, rc → exception. Không import pedalboard. ### 3.3. Masterbus (offline) Session thêm key `master`: ```json "master": { "fx_chain": [...], "volume_db": 0, "mute": false, "bypass": false } ``` `render_project()`: 1. `master_buffer = render_session_container(...)` (giữ nguyên). 2. Nếu `master.bypass` false → `master_buffer = render_fx_chain(wav, master.fx_chain)`. 3. Áp `volume_db`; giữ normalize-peak làm builtin slot cuối (hoặc bỏ khi chain có master limiter — config theo chain, mặc định giữ). Session cũ không có `master` → skip, không vỡ (backward compatible). ### 3.4. FX Rack per-track (offline) Track thêm key `fx_chain` (mảng insert slots, thay thế `fx_type` string): ```json "fx_chain": [ { "plugin_id": "ozone", "path": "...", "preset_b64": "...", "bypass": false } ] ``` Trong `render_session_container`, sau khi mix audio track (trước volume/pan): nếu `fx_chain` có VST slot → render track buffer qua bridge trước khi cộng vào session. `fx_type` legacy ("chorus"/"reverb") giữ nguyên fallback scipy — cùng lúc chỉ dùng một; ưu tiên `fx_chain` nếu có. ### 3.5. Scan + phân loại FX (`app/core/vst_engine.py`) - Thêm `list_fx()`: quét cùng thư mục, đọc category VST3 để tách effect khỏi instrument (instrument = class `kInstrument`; effect = `kAudioEffect`/`kFx`). - Không có SDK VST3 ở Python? → dùng chính bridge: thêm mode `--scan ` (C++ dùng VST3 `getClassInfo` đã có) trả JSON `[{path, is_fx, name}]`. Bridge scan là nguồn chân lý, Python chỉ cache. ### 3.6. API + UI (V1 tối thiểu) - `POST /api/v1/plugins/fx-render` `{ input_wav_url, fx_chain }` → WAV (dùng cho preview + export thử nghiệm; pattern giống `/api/v1/plugins/midi-render`). - `GET /api/v1/plugins/fx` → danh sách FX (từ scan bridge). - UI: FX Rack panel per-track + masterbus — danh sách slot, chọn plugin, bypass toggle, order (up/down), nút "Mở trong Carla / Chỉnh preset" → save `.vstpreset` (cơ chế CARLA_BRIDGE có sẵn, `md/52_CARLA_BRIDGE.md`). - Preset-based V1: state plugin = `.vstpreset` file, **chưa có param automation** (khớp giới hạn preset bridge hiện tại). Dry/wet: bỏ qua V1 (Ozone/Scaler xử lý trong chain), thêm sau nếu cần. ### 3.7. Realtime - Browser không chạy VST: giữ nguyên WebAudio builtin FX + mastering chain. - Track/masterbus có VST slot → khi play, nếu slot không bypass: dùng quick_render preview (đã có `preview_mode: "quick_render"` — render clip ngắn qua bridge → WAV → phát), ghi chú "preview = âm export". Nếu chỉ builtin → WebAudio trực tiếp. - Chưa realtime VST streaming (Tauri SHM) trong phạm vi plan này — ghi là future work. ### 3.8. Scaler — OPEN QUESTION (quyết định trong Phase 1) Scaler 2 là **MIDI/chord tool**, không phải audio-effect processor: - Chạy như VSTi insert (đường VSTi hiện có): nhận MIDI → output audio cho chord — không cần FX path mới, dùng `render_session_container` instrument nhánh có sẵn. - Hoặc chỉnh chord offline rồi render note events — cần UI riêng (ngoài plan). → **QUYẾT ĐỊNH (Phase 1, 2026-08-18): Scaler = VSTi**, không vào FX Rack. Xác nhận bằng bridge `--scan` thật trên `C:/Program Files/Common Files/VST3` (86 plugins): `Scaler 2` → `is_fx=false` (instrument), `ScalerAudio 2` → `is_fx=true` (effect — dùng được trong FX Rack/masterbus). Nếu user muốn Scaler trong FX Rack → Phase sau như audio-FX bọc ngoài (unlikely). ## 4. Phases | Phase | Nội dung | Deliverable | Trạng thái | |---|---|---|---| | 0 | Bridge `--render-fx`: WAV reader, NativeFxEngine VST3 effect (audio-in/out), builtin normalize/gain, SEH guard | CLI render FX demo: WAV in → WAV out, test với Ozone | ✅ test `/c/tmp/test_fx_phase0.py` 7/7 (gain0 bit-exact, -6dB RMS/2, normalize, Ozone 11 Maximizer khác input 88172 samples, scan 86 plugins); banner stdout→stderr `45a7bde` | | 1 | Bridge `--scan`: phân loại instrument/effect; Python `native_render.render_fx_chain` + `vst_engine.list_fx` | `/api/v1/plugins/fx` + `/api/v1/plugins/fx-render` | ✅ code sẵn + tests mới `test_render_fx_chain_builtin_gain_e2e`, `test_fx_render_builtin_gain` | | 2 | Masterbus: session key `master`, render_project chạy chain, backward compatible | Export session có mastering VST | ✅ `render_project` (render_engine.py) + `test_render_project_master_fx_chain_gain_via_bridge` | | 3 | FX Rack per-track: track `fx_chain`, render_engine integrate; UI panel (slot/bypass/order/preset upload) | Track VST FX + UI | ✅ backend `vst_fx_chain` + UI FX Rack (`__openFxRack`, add/change/bypass/preset upload) + `test_render_track_fx_chain_gain_via_bridge` | | 4 | Realtime preview quick_render cho slot VST; gỡ/làm rõ tham chiếu doc thiếu (`mastering_expand.md`) | Preview thật; doc sạch | ✅ "Nghe thử" = `__renderProjectPreview` → renderProject offline (âm thật = âm export); `md/mastering_expand.md` + `md/unified_fx_rack_panel.md` TỒN TẠI (không cần gỡ) | Mỗi phase kết thúc = commit + test pass. Phase 0-1 có thể merge (cùng chạm bridge). ## 5. Test - Bridge: self-check WAV in→out (identity khi bypass; gain builtin đúng dB); `--scan` trên folder có 1 instrument + 1 effect giả → đúng nhãn. - Python: unit test `render_fx_chain` với builtin chain (không cần VST) — input wav → output wav tồn tại, đúng sr; master key vắng → output không đổi so với trước (regression). - E2E (thủ công, máy có Ozone): export session có master Ozone chain → nghe khác rõ so với không chain, không clip. - Chạy full test suite hiện có — không phá 114 tests. ## 6. Kết quả phân tích Ozone -Inf input (2026-08-18) Verify code toàn chuỗi realtime master VST3 FX chain — **đã đủ, không thiếu setActive/setProcessing**: | Chặng | File | Trạng thái | |---|---|---| | Frontend gửi chain | `app/static/js/app.jsx:12732-12734` + `app.precompiled.js:803` — `useEffect` trên `ozState.vstFxChain` → `NativeBridgeService.setFxChain(JSON)` | ✅ Có | | Rust | `src-tauri/src/lib.rs:862-872` `set_fx_chain` → SHM control `type=6` | ✅ Có | | C++ nhận | `main.cpp:1567-1568` `type==6` → `g_fxChain->setChain(json, sr, block)` | ✅ Có | | Load plugin | `RenderFxJob.cpp` `vst3FxLoadInner` (397-470): activateBus toàn bộ, `setupProcessing(kRealtime,kSample32)`, `setActive(true)` (454), `setProcessing(true)` (455), `processData.prepare` | ✅ Đủ | | Process | `RealtimeFxChain::process` (audio thread, in-place trên ring master, SEH quanh từng plugin) + `Vst3Fx::processAudio` (copy input → zero output → process → copy ra) | ✅ Đúng | | processContext | tempo 120, sampleRate, kPlaying, projectTimeSamples tăng mỗi block | ✅ Có | → 4 giả thuyết user: #3 (thiếu setActive/setProcessing) **SAI** — đã có đủ. **Nguyên nhân -Inf thực tế: audio DAW không chảy qua bridge.** Chain chạy trên `shmIPC->ringLeft/ringRight[slot]` — master mix CHỈ chứa audio từ instrument trong bridge. DAW master bus (WebAudio graph: audio track, SonicSF fallback, non-bridge source) KHÔNG vào SHM → Ozone nhận silence → input -Inf. → Hướng sửa: (a) pipe DAW master bus audio vào bridge (SHM audio-in mới) để chain xử lý đúng master thật, hoặc (b) chạy chain trên WebAudio graph; hoặc (c) chỉ dùng bridge chain cho phần bridge-instrument. **Kết luận (2026-08-18): giữ (c)** — chain hiện tại xử lý đúng audio bridge-instrument; DAW master bus (a) là hướng dài hạn ngoài plan (cần SHM audio-in mới). Realtime Ozone trên toàn master = future work; offline export đã chạy chain đúng (Phase 2). ## 7. License - Ozone (iZotope/Native Instruments) + Scaler (PluginBoutique/Scale Software): plugin **thương mại** — KHÔNG nhúng, chỉ host bằng trình load của `native_bridge` (MIT). Không bundle binary plugin vào repo. - `native_bridge` giữ MIT; code mới (WAV reader, NativeFxEngine) cùng license. - Không quay lại pedalboard (GPL-3.0) dưới mọi hình thức.