d8f8506077
- open_vst_gui: bo WebviewWindowBuilder/thread, chi push_control type=4 (hwnd=0 -> bridge tao window) - main.cpp: create_native_vst_window (class SonicForge_Native_VST3_Class, 800x600, khong TOPMOST), tao trong ChannelWorker job, map guiWindows, capture arg2 by value (fix dangling) - app.jsx: guard isBridgeActive() 8 cho -> bridge active thi moi note di router -> pushEvent -> bridge (truoc day SF2 cam vi HAS_PYFLUIDSYNTH=FALSE -> /soundfont-render 501; VST3 path cu dung nativeSf/Carla) - E2E: SF2 NOTE_ON qua dispatchMidiEvent -> SHM peak 0.029745; Nexus GUI native hwnd OK (license/preset) - docs: TASKS.md + TEST_NOTES.md ghi batch fix + ket qua; gitignore vendor/junk
278 lines
26 KiB
Markdown
278 lines
26 KiB
Markdown
# TASKS — TRIỂN KHAI NATIVE HOST BRIDGE THEO PLAN
|
||
|
||
Nguồn: `PLAN.md` + spec. Mỗi task: file đích, hành động cụ thể, điều kiện hoàn thành (DoD) để verify.
|
||
Ký hiệu: **[MỚI]** tạo mới, **[THAY]** thay thế/đổi luồng hiện có, **[BỔ SUNG]** thêm vào code hiện có.
|
||
|
||
> **TRẠNG THÁI 2026-08-11:** ✅ = đã làm xong. Task ✅ liên quan Rust/Windows (B1-B4, C2, C5, C7)
|
||
> mới verify ở mức code (node --check JS / review struct) — **cần `cargo check` + chạy thật trên máy
|
||
> Windows** trước khi coi là hoàn tất; máy này (Linux) không có rustc/CMake-MSVC.
|
||
> **D2/D9:** đã đổi timeline → `scheduleMidiNoteDispatch` (bridge active) + transport play/stop +
|
||
> `bridgePlayheadSampleRef`; sampleOffset/set_position chỉ gửi khi C++ A11/A13 sẵn sàng (nay gửi
|
||
> sampleOffset=0 qua setTimeout). **E1-E3:** syntax OK (ast.parse); máy này SIGILL khi import
|
||
> `app.core.vst_engine` (numpy/pedalboard pre-existing) → chưa chạy thử API tại đây.
|
||
> **BATCH A10-A13 + D5 (2026-08-11):** C++ multi-instance (InstrumentEngineManager), CC/program/pitch
|
||
> bend (MidiEventIPC + data2/data3), TRANSPORT (type=3 STOP flush/PLAY/SET_POSITION + playheadSamples),
|
||
> sample-accurate segment render, JS dispatch MỞ RỘNG (CC/PROGRAM/PITCH_BEND), load theo channel,
|
||
> transport(playhead). **Struct SHM ĐỔI: 10872 → 11160 bytes** — đã sync C (selfcheck pass Linux) ↔ Rust
|
||
> shm.rs ↔ selfcheck; **bắt buộc `cargo check` + chạy shm_selfcheck trên Windows**. Sửa bug pre-existing:
|
||
> Rust push_control không set pathlen → C++ load giờ dùng strnlen(arg2) thay vì tin arg1. C++ chưa compile
|
||
> ở máy này (thiếu fluidsynth/sfizz headers) — verify bằng shm_selfcheck (không cần engine) + review.
|
||
> **BATCH B8-B10 (cùng ngày):** spawn bridge kèm SF_SAMPLE_RATE/SF_BLOCK_SIZE (C++ main đọc env, block cố
|
||
> định 256), open_vst_gui tạo child WebviewWindow + HWND raw_window_handle → control type=4, health monitor
|
||
> stall 3s → respawn 1 lần → bridge-down (đếm vào bridge.log). Rust chưa compile được (rustup không có
|
||
> toolchain) — review tay; `cargo check` trên Windows là điều kiện hoàn tất.
|
||
> **BATCH D7/D10/E4/E5/F1-F7/B6/B7/A7 (2026-08-11, cuối):** D7 UI Plugin Manager (badge Bridge ON/OFF +
|
||
> nút "Load Bridge" trên VST/SoundFont → `/api/v1/bridge/load` → loadInstrument); D10 header indicator
|
||
> Bridge/WASM + nút "Ép WASM" (setForceWasm) + service `onStatusChange`; E4 watchdog verify (không đụng
|
||
> bridge); E5 `GET /api/v1/bridge/log`; F1-F3 3 ps1 copy vào repo build/scripts/; F4 verify_bundle
|
||
> `--check-bridge`; F5 hooks.nsh OK; F6 docs 2 sidecar; F7 CI yml; B6 externalBin; B7 capabilities OK;
|
||
> A7 tách Vst3Instrument.h/.cpp (guard HAVE_VST3SDK, stub no-op khi thiếu submodule — A6 host thật vẫn
|
||
> TODO chờ vst3sdk trên Windows). Bundle rebuild OK; selfcheck 11160 pass Linux.
|
||
> **BATCH 2026-08-12 (chạy thật Windows + GỠ CARLA):** E2 resolve path fix — `_resolve_bridge_asset`
|
||
> scan plugin_dirs + os.walk cho SF2/SF3/SFZ; `loadTrackInstrumentToBridge` luôn gửi `r.path` qua
|
||
> `bridgeLoad`. **GỠ CARLA khỏi luồng instrument**: xóa `ensureCarlaForPlayback` + 2 call site + nút 🎛
|
||
> PM scan VST + dropdown "Carla Bridge (mở Carla.exe)" + 2 auto-open instrumentSelector/dropdown + nút
|
||
> Carla Bridge riêng (app.jsx); `runtime.js shouldRoute/shouldRoutePlayback` → false vô điều kiện (bridge
|
||
> host mọi instrument; Carla KHÔNG còn được mở/route khi bridge connected). GIỮ: FX module "Load Carla
|
||
> Bridge" (VST FX ngoài, user tự cài). **E2E thật (Tauri UI + CDP):** chèn SF2 024_BigSymphny.sf2 từ
|
||
> Plugin Manager → bridge load FULL PATH (log `[NativeBridge] instrument loaded ch=0 type=2 ...`), NOTE_ON
|
||
> → peak −0.0213 (âm phát), NOTE_OFF → 0 (decay). **`tauri build` lại**: NSIS `SonicForgeDAW_1.0.0_x64-setup.exe`
|
||
> (363MB) + MSI `SonicForgeDAW_1.0.0_x64_en-US.msi` (598MB) — engine mới (hash MD5 khớp dist) verify trong
|
||
> **FIX VST GUI HANG (attached()):** nguyên nhân `view->attached()` treo = openGUI chạy thread mới trong khi
|
||
> plugin được load ở thread cũ đã EXIT → COM STA apartment của load thread chết → plugin marshal nội bộ
|
||
> (Nexus) hang. Xác minh bằng `gui_probe` (standalone, target CMake, không ship): `bridge_like` (load
|
||
> thread A exit → openGUI thread B) TREO; `same_thread` (load+openGUI cùng thread, pump song song) OK.
|
||
> **Fix**: `main.cpp` thêm `ChannelWorker` — 1 thread persistent mỗi channel, giữ COM STA sống, LOAD và
|
||
> OPEN_GUI đều post job vào thread đó, thread pump message queue khi idle. Kèm: message pump ở đầu
|
||
> vòng audio loop (PeekMessage/Translate/Dispatch) cho editor trong lúc attached(). **Kết quả E2E (CDP +
|
||
> SHM):** Nexus `attached=0` → `[NativeBridge] GUI attached`, child editor windows visible trong
|
||
> `vst-Nexus`; SF2 peak 0.0236; Scaler2 note peak 0.0062 (VST audio routing OK); Nexus note peak 0 =
|
||
> license/preset (không phải host bug, GUI là đường load preset). 1 bridge process, 0 restart.
|
||
> **BATCH 2026-08-12 (FIX UI E2E — GUI VST native + audio path bridge):**
|
||
> **Issue 2 — GUI plugin là HTML che, không phải cửa sổ thật:** `open_vst_gui` (src-tauri/src/lib.rs)
|
||
> bỏ WebviewWindowBuilder/thread → chỉ `shm.push_control(4,0,0,0,&plugin_id)` (hwnd=0 → bridge tự tạo).
|
||
> `main.cpp` thêm `create_native_vst_window(title)` (RegisterClassA `SonicForge_Native_VST3_Class`,
|
||
> CreateWindowExA WS_OVERLAPPEDWINDOW|WS_VISIBLE 800x600, DefWindowProcA, KHÔNG WS_EX_TOPMOST); case
|
||
> OPEN_GUI arg1==0 tạo window NGAY TRONG ChannelWorker job (giữ COM STA), giữ `std::map<uint32_t,void*>
|
||
> guiWindows`; fix dangling: lambda capture `arg2=std::string(c.arg2)`. Verify: win_enum hwnd 0x4BE0EC4,
|
||
> class `SonicForge_Native_VST3_Class`, title Nexus, pid=27272, child `JUCE_19ff6684f11` visible —
|
||
> KHÔNG HTML che. Nexus note peak vẫn 0 = license/preset (kỳ vọng, GUI là đường load preset; init 1-2 phút).
|
||
> **Issue 1 — SF2 preview/play câm:** root cause `HAS_PYFLUIDSYNTH=FALSE` trong bundled engine →
|
||
> `/api/v1/plugins/soundfont-render` 501; VST3 play path cũ dùng nativeSf/scheduleCarlaNote (Carla đã gỡ →
|
||
> shouldRouteCarla false). Fix `app/static/js/app.jsx`: thêm guard `&& !(window.SonicMidiRouter &&
|
||
> window.SonicMidiRouter.isBridgeActive())` vào 8 chỗ (kbNative, keybed mouseenter, pvCtx wheel preview,
|
||
> clCtx piano-roll click, dwTrk draw, pvCtxInst draw preview, nativeSf x2, subNativeSf) → bridge active thì
|
||
> MỌI đường note đi qua router → pushEvent → bridge. Build + copy app.precompiled.js vào 3 nơi
|
||
> (src-tauri/target/release/daw_engine/_internal/app/static/, src-tauri/resources, dist/daw_engine). Verify
|
||
> HTTP: guard count=2 trong bundle served. **E2E chính ĐẠT:** sf2_focus2 — loadInstrument SF2 orchestral
|
||
> ch0 → dispatchMidiEvent NOTE_ON 60 → SHM peak **0.029745** → NOTE_OFF → 0. GUI Nexus native window OK.
|
||
|
||
> installer. D7 + E2 coi là hoàn tất (E2E thật).
|
||
> **BATCH CUỐI (2026-08-11):** B5 ✅ (pump thread tồn tại trong lib.rs ~263 — review tay, cần cargo check
|
||
> Windows); F5 ✅ (hooks.nsh `customInstall` ExecWait vc_redist toàn máy — đủ cho cả 2 exe); F2 🟡
|
||
> (ps1 đã copy, chạy thật cần Windows); D9 bổ sung `transport('set_position', playhead)` ~mỗi giây trong
|
||
> `updatePlayhead` (JS là nguồn playhead duy nhất — C++ A13 không tự biết). Bundle rebuilt OK.
|
||
|
||
---
|
||
|
||
## A. NATIVE BRIDGE C++ (`native_bridge/`) — [MỚI]
|
||
|
||
Code skeleton đã chuẩn bị sẵn trong workspace `native_bridge/`; copy vào repo `/home/locpham/SonicForgeStudio/native_bridge/` rồi hoàn thiện từng task.
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| ✅ A1 | `native_bridge/` | Copy toàn bộ skeleton từ workspace vào repo | `native_bridge/CMakeLists.txt` + `include/*.h` + `src/*.cpp` tồn tại |
|
||
| ✅ A2 | `native_bridge/vst3sdk/` | `git submodule add https://github.com/steinbergmedia/vst3sdk.git` | thư mục `vst3sdk/CMakeLists.txt` tồn tại |
|
||
| ✅ A3 | `native_bridge/vcpkg.json` | Tạo manifest: `{"dependencies":["fluidsynth","sfizz","pkgconf"]}` | `vcpkg install` chạy từ manifest không lỗi |
|
||
| ✅ A4 | `native_bridge/include/SharedMemoryIPC.h` | Giữ struct spec + controlQueue (đã có); **bổ sung** field `uint64_t blockTimestamp` (đo latency IPC) | struct khớp layout Rust ở B3 |
|
||
| ✅ A5 | `native_bridge/src/main.cpp` | Hoàn thiện: đọc control LOAD (đã có), **thêm** xử lý `type=3 TRANSPORT_PLAY/STOP` (flush note-off khi Stop), log mỗi lần load instrument ra stdout | Stop timeline → bridge gửi note-off toàn pitch |
|
||
| A6 | `native_bridge/src/NativeInstrumentEngine.cpp` | Giữ FluidSynth + sfizz (đã có); **hoàn thiện Vst3Instrument**: host qua vst3sdk (`IAudioProcessor::process`, `IPlugView::attached` với HWND con), factory `make_instrument` trả về instance thật | load `.vst3` → noteon → PCM khác 0 |
|
||
| 🟡 A7 | `native_bridge/include/Vst3Instrument.h` + `src/Vst3Instrument.cpp` | Tách class VST3 riêng (hiện đang stub trong main.cpp); xử lý `openGUI(void* hwnd)` | compile OK khi có vst3sdk |
|
||
| A8 | `native_bridge/src/Vst2Instrument.cpp` | (Tùy chọn, GĐ sau) Host VST2 qua vestige | skip nếu không đủ thời gian; ghi TODO |
|
||
| ✅ A9 | `native_bridge/src/SharedMemoryIPC.cpp` | Bỏ no-op; thêm helper `open_shm(name)`, `write_midi`, `write_control` (tái dùng bởi test) | unit test nhỏ gọi helper pass |
|
||
|
||
---
|
||
|
||
## B. RUST / TAURI SHELL (`src-tauri/`) — [MỚI] + [BỔ SUNG]
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| ✅ B1 | `src-tauri/Cargo.toml` | **BỔ SUNG** dependency `windows-sys` (features `Win32_System_MemoryManagement`, `Win32_Foundation`) để CreateFileMapping/MapViewOfFile | `cargo check` pass |
|
||
| ✅ B2 | `src-tauri/src/shm.rs` | **[MỚI]** Module SHM: `create_shm(name) -> ShmHandle` tạo `SonicForge_DAW_IPC` (CreateFileMapping + MapViewOfFile, size = `sizeof(SharedAudioBufferIPC)`), expose `write_midi_event(evt)`, `read_audio_block()`, `reset_queue()` | `cargo check` pass; struct layout khớp A4 |
|
||
| ✅ B3 | `src-tauri/src/lib.rs` | **BỔ SUNG** `EngineProcess` thứ hai cho bridge: `bridge_candidates()` dò `binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe` (resource dir + exe dir), spawn với env `SF_SHM_NAME=SonicForge_DAW_IPC` + `SF_PARENT_PID`, redirect stdout→`%APPDATA%/SonicForgeDAW/logs/bridge.log`, kill khi window destroyed | spawn.log có dòng `[daw_vst_bridge] exists=True` |
|
||
| ✅ B4 | `src-tauri/src/lib.rs` | **BỔ SUNG** `invoke_handler` với commands: `push_midi_event(cmd,ch,pitch,vel,sampleOffset)`, `load_native_instrument(path,type)`, `open_vst_gui(pluginId)`, `bridge_status()`, `transport_control(kind)` | `window.__TAURI__.core.invoke(...)` trả về OK |
|
||
| ✅ B5 | `src-tauri/src/lib.rs` | **[MỚI]** Audio pump thread: mỗi ~5.8ms đọc SHM (`bridgeWriteIndex` tăng) → `app.emit("bridge-audio", {L: Float32Array, R: Float32Array})`; nếu index không tăng 1s → emit `bridge-down` | UI nhận event `bridge-audio` |
|
||
| B6 | `src-tauri/tauri.conf.json` | **BỔ SUNG** `bundle.externalBin: ["binaries/daw_vst_bridge"]` (giữ resources engine) | `npx tauri build` bundle chứa `daw_vst_bridge-x86_64-pc-windows-msvc.exe` |
|
||
| B7 | `src-tauri/capabilities/default.json` | Xác nhận không cần permission mới (commands app-level) — nếu dùng plugin window cho GUI VST thì thêm `core:window:allow-create` | `tauri dev` không cảnh báo permission |
|
||
|
||
---
|
||
|
||
## C. FRONTEND SERVICES MỚI (`app/static/js/services/`) — [MỚI]
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| ✅ C1 | `unifiedMidiRouter.js` | **[MỚI]** Class `UnifiedMidiRouter`: `pushEvent({cmd,ch,pitch,vel,sampleOffset})` normalize `UnifiedMidiEvent`; `subscribe(source)` cho Web MIDI + Timeline; khi `bridgeConnected` → gọi `nativeBridgeService.dispatchMidiEvent`, ngược lại → gọi `SonicSF.playNote/stopNote` (fallback); `panic()` → `transport_control('panic')` | 1 router dùng chung 2 nguồn |
|
||
| ✅ C2 | `nativeBridgeService.js` | **[MỚI]** Như spec NHƯNG `dispatchMidiEvent` = `window.__TAURI__.core.invoke('push_midi_event', evt)` (không có `__TAURI_IPC_SHM__`); `loadInstrument(filePath,type)` invoke `load_native_instrument`; `openNativeGUI(pluginId)` invoke `open_vst_gui`; lắng nghe `bridge-audio`/`bridge-down`, expose `onAudio(cb)` | MIDI keyboard → note chạy qua bridge |
|
||
| ✅ C3 | `audioRoutingEngine.js` | **[MỚI]** `connectNativeBridgeToTrackFX(bridgeNode, trackId)` theo spec §VI: nối node vào `trackContext.sfEntry` → FX chain → `masterBus.input`; `disconnect()` khi bridge down | âm bridge qua EQ track + master maximizer |
|
||
| ✅ C4 | `app/templates/index.html` | **BỔ SUNG** 3 script tag sau `soundfontPlayer.js` (dòng 41): `nativeBridgeService.js`, `unifiedMidiRouter.js`, `audioRoutingEngine.js` (kèm `?v=` mới) | reload page → `window.SonicMidiRouter` tồn tại |
|
||
|
||
---
|
||
|
||
## D. WIRING `app/static/js/app.jsx` — [THAY]
|
||
|
||
| ID | Vị trí (dòng hiện tại) | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| ✅ D1 | `onmidimessage` ~15587 | **[THAY]** Nhánh NOTE_ON/OFF gọi `SonicSF.playNote`/`SonicCarlaMidi.noteOn` → gọi `UnifiedMidiRouter.pushEvent` trước; giữ fallback khi `!bridgeConnected` | bấm keyboard → router log + âm qua bridge |
|
||
| ✅ D2 | `updatePlayhead` ~20771+ (nhánh dispatch midiItems) | **[THAY]** Dispatch note timeline → `pushEvent` kèm `sampleOffset` tính từ playhead (bỏ `setTimeout` drift); Stop/Play → `transport_control` | play piano roll đúng nhịp, Stop hết âm ngân |
|
||
| ✅ D3 | `playNativeSfNote` / `SonicSF.playNote` call sites (track armed, keybed, sub-tab) | **[THAY]** Đổi đích tới router khi bridge active (cờ `SF_BRIDGE_AVAILABLE`) | mọi đường note đi qua 1 cổng |
|
||
| ✅ D4 | `SonicCarlaMidi` (runtime.js ~74) | **[THAY]** `shouldRouteCarla` trả false khi bridge connected (VSTi do bridge host); giữ Carla khi không có bridge | không kép âm VST (bridge + Carla) |
|
||
| ✅ D5 | `updateSfRouting` ~21255 + `setOutputDestination` | **[BỔ SUNG]** Nhánh mới: khi bridge active, `audioRoutingEngine.connectNativeBridgeToTrackFX(node, trackId)` thay vì `SonicSF.setOutputDestination` | âm bridge vào đúng track FX chain |
|
||
| D6 | `panic`/`stopAll` (soundfontPlayer.js) + Stop handler app.jsx | **[BỔ SUNG]** Stop/panic → `transport_control('panic')` tới bridge + vẫn gọi fallback cục bộ | không note kẹt sau Stop |
|
||
| ✅ D7 | UI Plugin Manager / Synth dropdown | **[BỔ SUNG]** Hiển thị trạng thái bridge (`bridge_status`); nút "Load vào Native Bridge" gọi `C2.loadInstrument` với path resolve từ API plugins (Bổ sung E1) | chọn VSTi/SF2 → bridge load, GUI VST mở được |
|
||
|
||
---
|
||
|
||
## E. BACKEND PYTHON (`app/`) — [BỔ SUNG]
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| ✅ E1 | `app/api/v1/plugins.py` | **BỔ SUNG** endpoint `GET /api/v1/bridge/status` (đọc file `%APPDATA%/SonicForgeDAW/ipc/bridge_status` do Rust ghi, hoặc probe bridge.log) | `curl /api/v1/bridge/status` trả JSON có `connected` |
|
||
| ✅ E2 | `app/api/v1/plugins.py` | **BỔ SUNG** endpoint `POST /api/v1/bridge/load` — resolve path thật (sf2 trong storage/soundfonts, .vst3 trong plugin_dirs) rồi ghi file IPC `ipc/bridge_load.request` cho Rust đọc (giống cơ chế pick_dir hiện có) | Rust nhận request → bridge load → UI nhận `bridge-audio` |
|
||
| ✅ E3 | `app/core/vst_engine.py` | Giữ nguyên cho offline render; **thêm** comment + hàm `resolve_plugin_path(name)` tái dùng bởi E2 | không phá luồng offline hiện có |
|
||
| E4 | `desktop_engine.py` | Không đổi logic; kiểm tra watchdog không giết bridge (bridge do Rust spawn, engine chỉ quản lý chính nó) | chạy standalone vẫn OK |
|
||
|
||
---
|
||
|
||
## F. BUILD & ĐÓNG GÓI WINDOWS — [BỔ SUNG]
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| F1 | `build/scripts/build_native_bridge.ps1` | Copy từ workspace (đã sẵn): vcpkg bootstrap + install fluidsynth/sfizz + submodule vst3sdk + cmake MSVC + copy exe | chạy trên máy Windows → exe trong `src-tauri/binaries/` |
|
||
| 🟡 F2 | `build/scripts/build_windows.ps1` | Copy từ workspace: pipeline 7 bước (0. kill engine+bridge, 1. pip, 2. frontend, 3. pyinstaller, 4. copy engine, 5. build bridge, 6. vc_redist, 7. tauri build) | build 1 lệnh ra NSIS+MSI |
|
||
| F3 | `build/scripts/test_standalone.ps1` | Copy từ workspace: 8 bước test (process, spawn.log, SHM, health, bridge/status, load SF2, checklist tay) | pass #1-#5 |
|
||
| F4 | `tools/verify_bundle.py` | **BỔ SUNG** kiểm tra thêm: `src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe` tồn tại trước tauri build | build fail sớm nếu thiếu bridge |
|
||
| ✅ F5 | `src-tauri/hooks.nsh` | Xác nhận `vc_redist.x64.exe` được cài cho cả `daw_engine.exe` + `daw_vst_bridge.exe` (bridge cần VC++ runtime) | máy sạch cài app → bridge chạy |
|
||
|
||
---
|
||
|
||
## G. TEST — [BỔ SUNG]
|
||
|
||
| ID | Nội dung | DoD |
|
||
|---|---|---|
|
||
| G1 | Unit test C++ (A9 helper) | assert-based check trong `native_bridge/tests/` chạy pass |
|
||
| G2 | Test SHM: Rust tạo → bridge mở → ghi MIDI → đọc audio | `bridgeWriteIndex` tăng; PCM khác 0 khi noteon |
|
||
| G3 | Test load SF2/SF3 (SGM-V2.01) + SFZ (SalamanderPiano) + VST3 (Vital) | spec VII mục 2 pass |
|
||
| G4 | Test live MIDI + timeline cùng lúc (Nektar SE49) | spec VII mục 3 pass, latency <10ms |
|
||
| G5 | Test Track FX + Master FX tác động âm bridge | spec VII mục 4 pass |
|
||
| G6 | Test fallback: kill bridge → SonicSF WASM tiếp tục, không crash | spec VII mục 6 pass |
|
||
| G7 | Chạy `test_standalone.ps1` trên máy cài standalone | 5/5 bước tự động pass |
|
||
|
||
---
|
||
|
||
## H. BỔ SUNG LIỀN MẠCH (MẠCH DỮ LIỆU HOÀN CHỈNH)
|
||
|
||
Các task nhóm H lấp những khâu còn hở giữa A-G để pipeline chạy end-to-end không đứt đoạn.
|
||
Đánh số tiếp theo nhóm tương ứng.
|
||
|
||
### H-A. Bridge C++ — bổ sung
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| ✅ A10 | `native_bridge/src/main.cpp` + `NativeInstrumentEngine.h` | **[MỚI]** Thay `currentInstrument` (1 instance) bằng `InstrumentEngineManager`: `std::map<uint32_t, std::unique_ptr<INativeInstrument>>` keyed theo MIDI channel; `assign_instrument(channel, path, type)`; MIDI event → instance theo `evt.channel`; channel chưa gán → silent (không crash) | 2 track (ch 0 + ch 2) 2 instrument phát đồng thời |
|
||
| ✅ A11 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Sample-accurate: không xử lý MIDI ngay; buffer event theo `sampleOffset`, rẽ block 256 thành các đoạn theo offset, gọi `noteOn/Off` đúng vị trí trước khi render đoạn đó | note timeline không lệch nhịp (>±1 sample test) |
|
||
| ✅ A12 | `native_bridge/include/SharedMemoryIPC.h` + engine | **[BỔ SUNG]** Mở rộng `MidiEventIPC`: field `data2` (+`data3` PB MSB); hỗ trợ `0xB CC` (FluidSynth `fluid_synth_cc`), `0xC program change`, `0xE pitch bend` (`fluid_synth_pitch_bend`); VST3 → tham số tương ứng | sustain/CC/pitchbend qua bridge hoạt động |
|
||
| ✅ A13 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Control `type=3 TRANSPORT`: `arg0=0 STOP` → flush note-off toàn channel + `fluid_synth_all_notes_off`; `arg0=1 PLAY` kèm `arg1=playheadSamples` → đồng bộ timeline | Stop → hết âm ngân tức thì |
|
||
|
||
### H-B. Rust — bổ sung
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| ✅ B8 | `src-tauri/src/lib.rs` | **[BỔ SUNG]** Spawn bridge kèm env `SF_SAMPLE_RATE` + `SF_BLOCK_SIZE` (đọc từ `bridge_status`/cấu hình, mặc định 44100/256); nếu `AudioContext.sampleRate` khác → JS resampler (C5) — ghi chú trong code | SR khác 44100 vẫn nghe đúng cao độ |
|
||
| ✅ B9 | `src-tauri/src/lib.rs` | **[BỔ SUNG]** `open_vst_gui(pluginId)`: tạo Tauri child window (WebviewWindowBuilder, title = plugin, childOf main) → lấy HWND (windows-sys) → ghi control `type=4 OPEN_GUI` + hwnd vào SHM → bridge `openGUI(hwnd)`; window đóng → `close_gui` | VST GUI hiện trong cửa sổ con, đóng sạch |
|
||
| ✅ B10 | `src-tauri/src/lib.rs` | **[BỔ SUNG]** Health monitor: nếu `bridgeWriteIndex` đứng >3s và process chết → spawn lại 1 lần, vẫn fail → `emit("bridge-down")`; đếm số lần restart vào bridge.log | app tự phục hồi bridge 1 lần; UI fallback sau đó |
|
||
|
||
### H-C. Frontend — bổ sung
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| ✅ C5 | `app/static/js/services/bridgeAudioNode.js` | **[MỚI]** Sink phát âm: nhận `bridge-audio` (Float32Array L/R) → ring buffer depth 8 block (~46ms) chống underrun → ScriptProcessor/Worklet đẩy ra `AudioNode` (output chuẩn stereo); nút này là "bridgeNode" mà C3 nối; **VU analyser tap trên node** (`__vuAnalyser` như `_gainNode` cũ) để track VU nhảy; resampler tuyến tính nếu SR khác bridge | âm nghe được từ SHM; VU track hoạt động |
|
||
| ✅ C6 | `app/static/js/app.jsx` (useEffect khởi tạo ~gần `initMasterBus`) | **[BỔ SUNG]** Bootstrap bridge: gọi `bridge_status()` → set `SF_BRIDGE_AVAILABLE`; `nativeBridgeService.onAudio` nối vào `bridgeAudioNode`; subscribe `bridge-down` → ngắt node + quay về SonicSF | cờ đúng trước note đầu tiên; bridge chết → fallback tự động |
|
||
| ✅ C7 | `app/static/js/services/nativeBridgeService.js` | **[BỔ SUNG]** Dev-mode mock: không có `window.__TAURI__` (Linux dev / browser) → mọi invoke thành log + `onAudio` không emit → app chạy bằng SonicSF như cũ | dev trên Linux không crash |
|
||
|
||
### H-D. app.jsx — bổ sung
|
||
|
||
| ID | Vị trí | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| D8 | router (C1) | **[BỔ SUNG]** Channel allocation: bảng `trackId → channel 0-15` thay `_engineChMap`/`allocateChannel`; percussion (bank 128) → ch 9; gửi `CC0/CC32/program change` qua bridge khi đổi instrument (A12) | 2 track 2 instrument không trùng channel |
|
||
| ✅ D9 | `updatePlayhead` | **[BỔ SUNG]** Playhead sample counter: `playheadSample += block*(dt*sr/block)`; `sampleOffset = playheadSample - lastBlockStartSamples`; gửi `transport_control('set_position', playheadSample)` mỗi bar | timeline chính xác sample |
|
||
| D10 | UI header/status | **[BỔ SUNG]** Indicator bridge (connected/down) + nút "Ép dùng WASM" (debug): gọi `SonicMidiRouter.setForceWasm(true)` | user biết engine nào đang phát |
|
||
|
||
### H-E. Backend — bổ sung
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| E5 | `app/api/v1/plugins.py` | **[BỔ SUNG]** `GET /api/v1/bridge/log?lines=100` — trả tail `bridge.log` cho UI debug | UI xem được log bridge |
|
||
|
||
### H-F. Build — bổ sung
|
||
|
||
| ID | File | Hành động | DoD |
|
||
|---|---|---|---|
|
||
| F6 | `DESKTOP_INSTALL_PLAN.md` / `DEPLOYMENT_REPORT.md` | **[BỔ SUNG]** Cập nhật tài liệu: kiến trúc 2 sidecar (daw_engine + daw_vst_bridge), yêu cầu VC++ runtime, port/API mới | tài liệu khớp thực tế |
|
||
| F7 | `ci/build-windows.yml` | **[MỚI]** (Tùy chọn) GitHub Actions: runner windows-latest → build bridge + engine + tauri → artifact installer | pipeline CI ra NSIS setup |
|
||
|
||
### H-G. Test — bổ sung
|
||
|
||
| ID | Nội dung | DoD |
|
||
|---|---|---|
|
||
| G8 | Đo latency end-to-end: đọc `blockTimestamp` (A4) qua `bridge_status` + round-trip MIDI→loa (loopback/mic); so ngưỡng | IPC ≤0.5ms; end-to-end <10ms |
|
||
| G9 | Test multi-instance: track 1 SF2 (piano) + track 2 SFZ (string) chạy cùng lúc, đổi instrument live | không choke, đúng âm từng track |
|
||
| G10 | Test CC/sustain/pitchbend qua bridge (A12) | sustain pedal + pitch wheel + CC volume hoạt động |
|
||
|
||
---
|
||
|
||
## SƠ ĐỒ MẠCH DỮ LIỆU HOÀN CHỈNH (SAU KHI ĐỦ TASKS)
|
||
|
||
```
|
||
Web MIDI keyboard ──┐
|
||
Timeline playhead ──┴─► UnifiedMidiRouter (C1) ─ invoke push_midi_event (B4/C2)
|
||
(D8: track→channel) │ bridge down → SonicSF fallback (C1/C6/C7)
|
||
▼
|
||
Rust shm.rs write_midi_event (B2) ──► SHM "SonicForge_DAW_IPC"
|
||
▼
|
||
bridge main.cpp (A11 sampleOffset, A12 CC/PC/PB, A13 transport)
|
||
→ InstrumentEngineManager (A10) → FluidSynth / sfizz / VST3 (A6-A8)
|
||
▼
|
||
SHM masterLeft/Right + bridgeWriteIndex + blockTimestamp (A4)
|
||
▼
|
||
Rust audio pump (B5/B10 health) ─ emit 'bridge-audio' ─► bridgeAudioNode (C5)
|
||
▼
|
||
audioRoutingEngine (C3) → track sfEntry → track FX → masterBus → speakers
|
||
▼
|
||
VU analyser tap (C5) → track VU nhảy
|
||
```
|
||
|
||
---
|
||
|
||
## PHỤ THUỘC (thứ tự thực hiện — đã cập nhật)
|
||
|
||
1. **A1-A4 → B1-B4 → C1-C2 + C5-C7** (skeleton + IPC + router + audio sink) — nền tảng.
|
||
2. **A10-A13 → B5, B8 → C3, C6 → D1-D2, D8-D9** (multi-instance + sample-accurate + audio pump + routing + wiring).
|
||
3. **A5-A7, A12 → B9-B10 → D3-D7, D10 → E1-E5** (engine thật + VST GUI + gỡ Carla + UI + backend).
|
||
4. **F1-F7 → G1-G10** (build pipeline + test) — cần máy Windows.
|
||
|
||
## CÒN THIẾU SO VỚI SPEC (đã bổ sung vào tasks — bản đầy đủ)
|
||
|
||
- SHM tạo bởi Rust (B2) — spec chỉ có bridge mở.
|
||
- Đường MIDI JS→bridge (B4/C2) — spec dùng `__TAURI_IPC_SHM__` không tồn tại.
|
||
- Audio pump SHM→WebView (B5/C5) — spec nói zero-copy nhưng WebView không zero-copy được.
|
||
- Transport/panic + playhead position (A5/A13/B4/C1/D6/D9) — spec thiếu, cần cho timeline Stop/đồng bộ.
|
||
- Nhiều instrument/track (A10/D8) — spec chỉ 1 `currentInstrument`.
|
||
- VST3 GUI child window (B9/A7) — spec có `openGUI` nhưng thiếu cơ chế HWND con từ Tauri.
|
||
- CC/program/pitchbend (A12) — spec chỉ note on/off; app hiện có sustain/pitch wheel.
|
||
- Sample-accurate scheduling (A11/D9) — spec xử lý MIDI ngay lập tức, bỏ qua sampleOffset.
|
||
- Resampler khi SR AudioContext ≠ bridge (B8/C5).
|
||
- Ring buffer chống underrun + VU tap (C5) — kế thừa hành vi `_gainNode.__vuAnalyser` cũ.
|
||
- Fallback SonicSF (C1/C6/C7/G6) — spec không đề cập; giữ để app không chết khi bridge lỗi.
|
||
- Dev-mode mock (C7) — chạy được trên Linux/browser không có `__TAURI__`.
|
||
|