1 Commits

Author SHA1 Message Date
locpham c3368d0a91 feat: native host bridge integration — C++ bridge, Rust SHM, JS routing, build scripts, docs
- native_bridge/: InstrumentEngineManager multi-channel, sample-accurate, CC/program/pitchbend, transport, Vst3Instrument stub (HAVE_VST3SDK)
- src-tauri: shm.rs, bridge spawn + audio pump + health monitor, open_vst_gui, externalBin, commands
- app: UnifiedMidiRouter, NativeBridgeService, bridgeAudioNode, audioRoutingEngine, Plugin Manager UI, Bridge/WASM indicator, set_position sync
- build: 3 ps1 (force-added, build/ ignored), verify_bundle --check-bridge, CI workflow
- docs: TASKS.md, TEST_NOTES.md (Windows verify checklist), install/report updates
2026-08-11 23:37:29 +07:00
37 changed files with 2970 additions and 80 deletions
+42
View File
@@ -0,0 +1,42 @@
name: build-windows
on:
workflow_dispatch:
push:
tags: ["v*"]
jobs:
build:
runs-on: windows-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@stable
- name: Install MSVC + CMake deps
uses: ilammy/msvc-dev-cmd@v1
- name: Build one-command pipeline (engine + bridge + tauri)
shell: powershell
run: |
powershell -ExecutionPolicy Bypass -File build/scripts/build_windows.ps1
- name: Upload NSIS installer
uses: actions/upload-artifact@v4
with:
name: SonicForgeDAW-setup
path: |
src-tauri/target/release/bundle/nsis/*.exe
src-tauri/target/release/bundle/msi/*.msi
+3
View File
@@ -0,0 +1,3 @@
[submodule "native_bridge/vst3sdk"]
path = native_bridge/vst3sdk
url = https://github.com/steinbergmedia/vst3sdk.git
+28
View File
@@ -302,3 +302,31 @@ Tất cả 7 task trong kế hoạch đã hoàn thành:
7. ✅ Kiểm thử hệ thống 7. ✅ Kiểm thử hệ thống
**Hệ thống SonicForge Studio đã sẵn sàng sử dụng! 🎉** **Hệ thống SonicForge Studio đã sẵn sàng sử dụng! 🎉**
---
## 🆕 Bản 1.2 — Bản Desktop Windows (Tauri v2 + 2 sidecar)
> Report trên là môi trường **Docker/server** (dev). Từ bản 1.2, bản phát hành
> desktop là **Tauri v2 shell + 2 sidecar** — xem `DESKTOP_INSTALL_PLAN.md` §5.2.
- **Sidecar 1** `daw_engine.exe` (FastAPI/PyInstaller onedir, port 8000-8010):
bundle.resources → `src-tauri/resources/daw_engine/`; `SF_PARENT_PID` để
watchdog tự thoát khi Tauri đóng.
- **Sidecar 2** `daw_vst_bridge.exe` (C++ Native Host Bridge): vcpkg
(fluidsynth/sfizz) + vst3sdk submodule; `bundle.externalBin`
`src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe`; trao đổi qua
shared memory `SonicForge_DAW_IPC` (struct 11160 bytes — sync
SharedMemoryIPC.h ↔ src-tauri/src/shm.rs ↔ tests/shm_selfcheck.cpp).
- **IPC control**: LOAD/TRANSPORT(panic|play|set_position)/OPEN_GUI(HWND) qua
SHM control queue; MIDI 0x9/0x8/0xB/0xC/0xE + sampleOffset.
- **API mới**: `GET /api/v1/bridge/status`, `POST /api/v1/bridge/load`,
`GET /api/v1/bridge/log`.
- **Build 1 lệnh**: `build/scripts/build_windows.ps1` (7 bước: kill → pip →
frontend → engine → copy → bridge → vc_redist → tauri build NSIS+MSI);
verify: `tools/verify_bundle.py --check-bridge`.
- **YC runtime**: VC++ 2015-2022 Redistributable cho cả 2 sidecar (NSIS hook
tự cài; MSI không chạy hooks → cài thủ công).
- **Trạng thái**: code + selfcheck SHM pass trên Linux; chưa build trên
Windows (thiếu rustc/CMake-MSVC tại máy dev) — chạy `test_standalone.ps1`
trên máy Windows sau khi cài.
+34
View File
@@ -112,6 +112,40 @@ bash build_linux.sh
bash build_macos.sh bash build_macos.sh
``` ```
## 5.2 KIẾN TRÚC 2 SIDECAR (Native Host Bridge — bản 1.2)
Từ bản 1.2, bản desktop chạy **Tauri v2** shell + **2 sidecar** (không còn browser):
```
┌───────────────────────── SonicForgeDAW (Tauri v2) ─────────────────────────┐
│ WebView2 (UI HTML5/JS — app.precompiled.js) │
│ ▲ invoke ▲ bridge-audio (event) │
│ Rust shell (src-tauri) │
│ ├── spawn+watchdog ──► daw_engine.exe (FastAPI sidecar, port 8000) │
│ └── spawn+health ────► daw_vst_bridge.exe (C++ Native Host Bridge) │
│ │ SharedMemory "SonicForge_DAW_IPC" │
│ │ (MidiEventIPC / ControlEventIPC / audio) │
│ └─ FluidSynth / sfizz / VST3 host │
└────────────────────────────────────────────────────────────────────────────┘
```
- **MIDI → âm**: Web MIDI/timeline → `push_midi_event` (Rust ghi SHM) →
bridge render block 256 (FluidSynth/sfizz/VST3) → Rust audio pump emit
`bridge-audio` → WebView2 ScriptProcessor → track FX → master → loa.
- **Fallback**: bridge chết → `SonicSF` (FluidSynth WASM) tự bật, app không crash.
- **YC runtime**: cả `daw_engine.exe` lẫn `daw_vst_bridge.exe` cần VC++ 2015-2022
Redistributable — `hooks.nsh` (NSIS) cài `vc_redist.x64.exe`; MSI không chạy
hooks → máy sạch nên cài NSIS hoặc tự cài redist.
- **Binaries**: engine = `src-tauri/resources/daw_engine/` (bundle.resources),
bridge = `src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe`
(`bundle.externalBin`).
- **Build bridge**: `build/scripts/build_native_bridge.ps1` (vcpkg fluidsynth/
sfizz + submodule vst3sdk + CMake MSVC) → copy exe vào `src-tauri/binaries/`.
- **API mới**: `GET /api/v1/bridge/status`, `POST /api/v1/bridge/load`,
`GET /api/v1/bridge/log` (engine giữ trung gian IPC file
`%APPDATA%/SonicForgeDAW/ipc/`).
- **Logs**: `%APPDATA%/SonicForgeDAW/logs/spawn.log` (engine+bridge), `bridge.log`.
## 6. CẬP NHẬT ## 6. CẬP NHẬT
- **Version check**: khi mở app, gọi endpoint version (file `version.json` đóng kèm + so sánh remote) → thông báo bản mới + link tải installer. - **Version check**: khi mở app, gọi endpoint version (file `version.json` đóng kèm + so sánh remote) → thông báo bản mới + link tải installer.
- Cập nhật = chạy installer mới (ghi đè, GIỮ NGUYÊN `~/SonicForgeStudio/` — data + soundfonts không đụng). - Cập nhật = chạy installer mới (ghi đè, GIỮ NGUYÊN `~/SonicForgeStudio/` — data + soundfonts không đụng).
+237
View File
@@ -0,0 +1,237 @@
# 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 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()``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__`.
+80
View File
@@ -0,0 +1,80 @@
# TEST NOTES — NATIVE BRIDGE (test trên máy native/Windows)
Máy phát triển hiện tại (Linux, không rustc/CMake-MSVC, Python SIGILL khi import
`app.core.vst_engine`) KHÔNG chạy được pipeline. Danh sách này là "bộ nhớ" —
chạy từng mục trên máy Windows (hoặc OS native có đủ toolchain), đánh dấu ✅ khi pass.
## A. C++ bridge (`native_bridge/`)
- [x] A5+A13: TRANSPORT type=3 (STOP flush / PLAY / SET_POSITION) + log mỗi load — code OK, chưa chạy thật
- [x] A10: InstrumentEngineManager multi-channel (std::map channel→instrument) — code OK, chưa compile Windows
- [x] A11: sample-accurate segment render theo sampleOffset — code OK; JS hiện gửi sampleOffset=0 (best-effort)
- [x] A12: CC/program/pitch bend (MidiEventIPC data2+data3) — code OK
- [ ] A6: Vst3Instrument host thật qua vst3sdk — load .vst3 → noteon → PCM ≠ 0
- [ ] A7: Vst3Instrument tách class + openGUI(hwnd)
- [ ] **Windows verify sfizz API names**: `SfizzInstrument::controlChange` dùng `sfizzSynth.cc(cc,val)`, `pitchBend` dùng `sfizzSynth.pitchWheel(bend14)` — nếu bản sfizz vcpkg chỉ có `hdCC`/`hdPitchWheel` thì đổi; `programChange` đang no-op (TODO)
- [ ] **Windows verify**: `cargo check` + chạy `shm_selfcheck.exe` (sizeof struct = **11160 bytes**)
## B. Rust / Tauri (`src-tauri/`)
- [ ] B1-B4: `cargo check` pass trên Windows (windows-sys, shm.rs, lib.rs commands)
- [ ] B2: SHM struct layout khớp C header (**11160 bytes** — đã verify C bằng selfcheck Linux; cần Rust)
- [ ] B2: signature commands mới — `push_midi_event(..., data2, data3, sample_offset)`, `load_native_instrument(path, type, channel)`, `transport_control(kind, playhead: Option<u32>)` — JS `nativeBridgeService.js`/`unifiedMidiRouter.js` đã sync
- [ ] B5: audio pump thread emit `bridge-audio` ~5ms — ✅ code: pump thread trong lib.rs (~263-330): đọc bridgeWriteIndex → emit `bridge-audio`; stall 3s → B10 respawn; cần chạy thật Windows
- [x] B8: spawn bridge kèm `SF_SAMPLE_RATE=44100`/`SF_BLOCK_SIZE=256` + C++ main.cpp đọc env (block cố định 256, SR áp cho init engine) — code OK
- [x] B9: `open_vst_gui` tạo child WebviewWindow + HWND qua `raw_window_handle` → control type=4 arg1 — code OK; **close_gui khi window đóng chưa làm (chờ A7)**
- [x] B10: health monitor trong audio pump — stall 3s → respawn 1 lần → `bridge-down`; đếm restart vào bridge.log — code OK
- [ ] B6: tauri.conf.json externalBin chứa daw_vst_bridge — ✅ code: `["binaries/daw_vst_bridge"]` đã thêm (chưa `tauri build` verify)
- [ ] B7: capabilities — ✅ review: window VST tạo từ Rust (`WebviewWindowBuilder`), commands app-level, event listen nằm trong `core:default` — không cần permission mới
- [ ] B9 thật trên Windows: child window hiện + HWND hợp lệ (kiểm tra log bridge nhận type=4)
## C. Frontend services
- [x] C1-C7: node --check OK + bundle build OK (Linux)
- [ ] C5: ring buffer 8 block chống underrun thật trên Windows (âm nghe được, VU nhảy)
## D. app.jsx wiring
- [x] D1: onmidimessage NOTE_ON/OFF → router (code + bundle OK)
- [x] D2: timeline → scheduleMidiNoteDispatch + transport play/stop (code + bundle OK)
- [x] D9: bridgePlayheadSampleRef counter + `transport('set_position', playhead)` ~mỗi giây trong updatePlayhead (JS là nguồn playhead duy nhất; C++ A13 cập nhật anchor) — code + bundle OK
- [ ] D1-D2 thật: bấm keyboard + play piano roll → âm qua bridge, Stop hết âm ngân
- [x] D4: Carla không route khi bridge active (code, cần verify thật)
- [ ] D5: audio routing vào track FX chain
- [x] D6: panic/Stop → `SonicMidiRouter.panic()` (bridge transport panic + fallback SonicSF.stopAll) — 4 call site app.jsx đã đổi
- [x] D8: track→channel map + gửi CC0/CC32/PROGRAM trước NOTE_ON khi đổi instrument (scheduleMidiNoteDispatch)
- [x] D7: UI Plugin Manager — badge `● Bridge ON/OFF` (query `GET /api/v1/bridge/status`), nút "Load Bridge" trên mỗi VST + SoundFont → `POST /api/v1/bridge/load` (E2 resolve path) → `NativeBridgeService.loadInstrument(path, type, 0)`; code + bundle OK — chưa test thật trên Windows
- [x] D10: header indicator Bridge/WASM + nút "Ép WASM" (toggle `SonicMidiRouter.setForceWasm`); service thêm `onStatusChange(cb)` — code + bundle OK
## E. Backend Python
- [x] E1-E3: syntax OK (ast.parse)
- [x] E4: desktop_engine.py watchdog — verify: chỉ theo dõi `SF_PARENT_PID` (Tauri) để tự thoát; không spawn/kill gì khác → không đụng bridge (bridge do Rust spawn). Không cần sửa.
- [x] E5: `GET /api/v1/bridge/log?lines=N` tail bridge.log — thêm Query import; ast.parse OK
- [ ] `curl /api/v1/bridge/status` trả `connected` đúng — cần chạy trên máy không SIGILL
- [ ] `POST /api/v1/bridge/load` resolve sf2/vst3 → bridge load → UI nhận `bridge-audio`
- [ ] SIGILL khi import `app.core.vst_engine` trên máy dev (numpy/pedalboard pre-existing) — không phải do code mới
## F. Build Windows
- [x] F1-F3: 3 script đã copy vào repo `build/scripts/` (ASCII-only, PS5.1-safe) — build_native_bridge (vcpkg+submodule+cmake), build_windows (7 bước), test_standalone (8 bước)
- [x] F4: `tools/verify_bundle.py --check-bridge` — verify TOC + sidecar tồn tại; fail đúng cách trên Linux (chưa có exe) — test exit 1 OK
- [x] F5: hooks.nsh — vc_redist cài toàn máy (ExecWait /quiet) → đủ runtime cho cả daw_engine.exe + daw_vst_bridge.exe; MSI không chạy hooks → cài thủ công
- [x] F6: DESKTOP_INSTALL_PLAN.md §5.2 (kiến trúc 2 sidecar) + DEPLOYMENT_REPORT.md bản 1.2
- [x] F7: `.github/workflows/build-windows.yml` — windows-latest: MSVC + Rust + node + python → build_windows.ps1 → upload NSIS/MSI
- [ ] **Chạy thật trên Windows**: build_windows.ps1 trọn pipeline ra NSIS+MSI
## G. Test end-to-end
- [ ] G1: shm_selfcheck pass (đã pass trên Linux: "SHM self-check OK (**11160 bytes**)" — chạy lại trên Windows)
- [ ] G2: SHM Rust tạo → bridge mở → MIDI → audio
- [ ] G3: load SGM-V2.01 (SF2) + SalamanderPiano (SFZ) + Vital (VST3)
- [ ] G4: live MIDI + timeline cùng lúc, latency <10ms
- [ ] G5: Track FX + Master FX tác động âm bridge
- [ ] G6: kill bridge → SonicSF fallback, không crash
- [ ] G7: test_standalone.ps1 5/5
- [ ] G8: đo latency blockTimestamp IPC ≤0.5ms, end-to-end <10ms
- [ ] G9: multi-instance 2 track đồng thời (A10 — 2 SF2/SFZ khác nhau, đổi live)
- [ ] G10: sustain/CC/pitchbend qua bridge (A12 — CC64 sustain + pitch wheel + CC7 volume)
## Ghi chú batch 2026-08-11 (A10-A13 + D5)
- Struct SHM đổi 10872 → 11160: MidiEventIPC +data2/data3(+reserved) = 12B; ControlEventIPC +channel = 1040B.
Sync 3 nơi: `SharedMemoryIPC.h`, `shm.rs`, `shm_selfcheck.cpp` — đã khớp (selfcheck pass Linux).
- Bug pre-existing tìm thấy: `shm.rs push_control` không ghi pathlen vào arg1 → bridge cũ đọc path rỗng.
Sửa: C++ `main.cpp` load path dùng strnlen(arg2); helper C `shm_write_control` chỉ set arg1=pathlen khi type==2.
- `transport_control` Rust giờ nhận `playhead: Option<u32>`; JS `NativeBridgeService.transport(kind, playhead)`.
- `loadInstrument(filePath, type, channel)`; `dispatchMidiEvent(cmd,...,data2,data3)` hỗ trợ CC/PROGRAM/PITCH_BEND.
- Khi D8 (track→channel map) chưa xong: JS loadInstrument mặc định channel=0 — multi-instance chưa kích hoạt từ UI.
+118 -1
View File
@@ -1,7 +1,7 @@
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading import os, sys, uuid, json, tempfile, subprocess, time as _time, threading
import numpy as np import numpy as np
import soundfile as sf import soundfile as sf
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header, Query
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, Any from typing import Optional, Any
@@ -1178,3 +1178,120 @@ async def render_project(
return {"url": f"/static/audio/processed/{os.path.basename(result_path)}", "path": result_path} return {"url": f"/static/audio/processed/{os.path.basename(result_path)}", "path": result_path}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Render failed: {str(e)}") raise HTTPException(status_code=500, detail=f"Render failed: {str(e)}")
# ── E1/E2: NATIVE HOST BRIDGE (daw_vst_bridge) ──────────────────────────────
# IPC dir giống _pick_dir_via_tauri_bridge: %APPDATA%/SonicForgeDAW/ipc — Rust
# (src-tauri) tạo, engine chỉ đọc/ghi file request/response.
BRIDGE_IPC_DIR = os.path.join(os.environ.get("APPDATA") or os.path.expanduser("~"), "SonicForgeDAW", "ipc")
def _bridge_ipc_dir() -> Optional[str]:
if os.path.isdir(BRIDGE_IPC_DIR):
return BRIDGE_IPC_DIR
return None
@router.get("/bridge/status")
async def bridge_status(current_user: dict = Depends(get_current_user)):
"""E1: trạng thái native bridge. Rust ghi bridge_status (JSON) khi spawn/
health-check; nếu chưa có → probe tail bridge.log (dòng started/exists)."""
ipc = _bridge_ipc_dir()
if not ipc:
return {"connected": False, "reason": "no_ipc_dir"}
st = os.path.join(ipc, "bridge_status")
if os.path.exists(st):
try:
with open(st, "r", encoding="utf-8") as fh:
raw = fh.read() or "{}"
try:
data = json.loads(raw)
except Exception:
data = {}
data.setdefault("connected", True)
return data
except Exception:
pass
log = os.path.normpath(os.path.join(ipc, "..", "logs", "bridge.log"))
if os.path.exists(log):
try:
with open(log, "r", encoding="utf-8", errors="ignore") as fh:
tail = fh.read().splitlines()[-5:]
joined = "\n".join(tail).lower()
return {"connected": ("started" in joined or "exists=true" in joined), "log_tail": tail}
except Exception:
pass
return {"connected": False, "reason": "no_status_no_log"}
class BridgeLoadRequest(BaseModel):
name: str # tên asset hiển thị (sf_xxx / Vital.vst3 / ...)
path: Optional[str] = None # path tuyệt đối nếu đã biết
instrumentType: str = "SF2" # 'VST3'|'VST2'|'SF2'|'SF3'|'SFZ'
channel: Optional[int] = None
def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str]) -> Optional[str]:
if path and os.path.exists(path):
return path
base = os.path.basename((path or name).replace("\\", "/"))
base_noext = os.path.splitext(base)[0].lower()
# sf2/sf3/sfz: storage/soundfonts + system + user plugin_dirs
for base_dir in [UPLOAD_SF_DIR, SYSTEM_SF_DIR]:
if not os.path.isdir(base_dir):
continue
for fname in os.listdir(base_dir):
cand = os.path.join(base_dir, fname)
if fname.lower().endswith((".sf2", ".sf3", ".sfz")) and \
(fname.lower() == base.lower() or os.path.splitext(fname)[0].lower() == base_noext):
return cand
ext = (".vst3" if instrument_type.upper() == "VST3" else
".vst2" if instrument_type.upper() == "VST2" else
".sfz" if instrument_type.upper() == "SFZ" else None)
if ext:
try:
from app.core.vst_engine import _load_user_plugin_dirs
dirs = _load_user_plugin_dirs() or []
except Exception:
dirs = []
if not dirs:
dirs = [settings.VST_DIR, settings.SOUNDFONT_DIR]
for base_dir in dirs:
if not os.path.isdir(base_dir):
continue
for root, _, files in os.walk(base_dir):
for fn in files:
if fn.lower().endswith(ext) and \
(fn.lower() == base.lower() or os.path.splitext(fn)[0].lower() == base_noext):
return os.path.join(root, fn)
return None
@router.post("/bridge/load")
async def bridge_load(req: BridgeLoadRequest, current_user: dict = Depends(get_current_user)):
"""E2: resolve path thật của asset rồi (a) ghi bridge_load.request cho Rust
watcher (nếu có) và (b) trả path để JS gọi invoke load_native_instrument
trực tiếp — pipeline hoạt động ngay, không chờ watcher."""
resolved = _resolve_bridge_asset(req.name, req.instrumentType, req.path)
if not resolved:
raise HTTPException(status_code=404, detail=f"Không tìm thấy asset: {req.name}")
wrote_request = False
ipc = _bridge_ipc_dir()
if ipc:
try:
req_file = os.path.join(ipc, "bridge_load.request")
if os.path.exists(req_file):
os.remove(req_file)
with open(req_file, "w", encoding="utf-8") as fh:
json.dump({"path": resolved, "type": req.instrumentType, "channel": req.channel}, fh)
wrote_request = True
except Exception:
pass
return {"success": True, "path": resolved, "type": req.instrumentType, "ipc_request_written": wrote_request}
@router.get("/bridge/log")
async def bridge_log(lines: int = Query(100, ge=1, le=2000), current_user: dict = Depends(get_current_user)):
"""E5: tail bridge.log cho UI debug. Log Rust ghi tại %APPDATA%/SonicForgeDAW/logs/bridge.log."""
log = os.path.normpath(os.path.join(BRIDGE_IPC_DIR, "..", "logs", "bridge.log"))
if not os.path.exists(log):
return {"lines": [], "path": log, "exists": False}
try:
with open(log, "r", encoding="utf-8", errors="ignore") as fh:
all_lines = fh.read().splitlines()
return {"lines": all_lines[-lines:], "path": log, "exists": True}
except Exception as e:
return {"lines": [], "path": log, "exists": False, "error": str(e)}
+30
View File
@@ -551,6 +551,36 @@ def resolve_preset_path(preset_id_or_path: str) -> str:
return "" return ""
def resolve_plugin_path(name_or_path: str, ext: str = "") -> str:
"""E3: resolve path thật của plugin/soundfont cho Native Bridge (E2).
ext: '.vst3' | '.vst2' | '.sf2' | '.sf3' | '.sfz' — filter phần mở rộng
khi quét plugin_dirs. Trả '' nếu không tìm thấy. Chống path traversal:
chỉ tên file (không separator) hoặc path tồn tại được chấp nhận."""
if not name_or_path:
return ""
p = name_or_path
if os.path.isfile(p):
return p
base = os.path.basename(p.replace("\\", "/"))
base_noext = os.path.splitext(base)[0].lower()
want_ext = ext.lower() if ext else None
dirs = []
try:
dirs = list(_load_user_plugin_dirs()) or []
except Exception:
pass
dirs += [settings.VST_DIR, settings.SOUNDFONT_DIR]
for base_dir in dirs:
if not os.path.isdir(base_dir):
continue
for root, _, files in os.walk(base_dir):
for fn in files:
if want_ext and not fn.lower().endswith(want_ext):
continue
if fn.lower() == base.lower() or os.path.splitext(fn)[0].lower() == base_noext:
return os.path.join(root, fn)
return ""
def apply_preset_to_plugin(plugin, preset_id=None, preset_path=None, preset_data_b64=None) -> bool: def apply_preset_to_plugin(plugin, preset_id=None, preset_path=None, preset_data_b64=None) -> bool:
"""Gán preset lên plugin pedalboard: bytes nhúng (base64 .vstpreset) ưu """Gán preset lên plugin pedalboard: bytes nhúng (base64 .vstpreset) ưu
tiên, sau đó preset_id (thư viện), sau preset_path (file). Trả True nếu tiên, sau đó preset_id (thư viện), sau preset_path (file). Trả True nếu
+225 -35
View File
@@ -1432,6 +1432,32 @@ function getAudioContext() {
if (window.SonicSF && window.SonicSF.init) { if (window.SonicSF && window.SonicSF.init) {
window.SonicSF.init(audioCtx); window.SonicSF.init(audioCtx);
} }
// C6: bootstrap native bridge 1 ln query status, ni audio sink + router.
if (window.NativeBridgeService && window.SonicMidiRouter && !window.__bridgeBootstrapped) {
window.__bridgeBootstrapped = true;
window.SonicMidiRouter.onFallback = function (cmd, ch, pitch, vel) {
if (!window.SonicSF) return;
if (cmd === 'PANIC') { if (window.SonicSF.stopAll) window.SonicSF.stopAll(); return; }
if (cmd === 'NOTE_ON') window.SonicSF.playNote(pitch, vel, undefined, undefined, undefined, undefined, ch);
else window.SonicSF.stopNote(ch, pitch);
};
(async function () {
try {
var st = await window.NativeBridgeService.queryStatus();
var connected = !!(st && st.connected);
window.SonicMidiRouter.setBridgeConnected(connected);
console.log('[Bridge] bootstrap connected=', connected);
if (connected) {
window.BridgeAudioNode.init(audioCtx);
window.NativeBridgeService.onAudio(function (l, r) { window.BridgeAudioNode.onAudio(l, r); });
if (window.AudioRoutingEngine) window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
}
} catch (e) {
console.warn('[Bridge] bootstrap error:', e);
window.SonicMidiRouter.setBridgeConnected(false);
}
})();
}
return audioCtx; return audioCtx;
} }
const formatTime = secs => { const formatTime = secs => {
@@ -5525,6 +5551,13 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0); const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0);
// Khai báo trc tiếp thư mc cha carla.exe (nhp tay, không cn picker) // Khai báo trc tiếp thư mc cha carla.exe (nhp tay, không cn picker)
const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState(''); const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState('');
// D7: trng thái Native Bridge (query khi m modal) badge header.
const [bridgeStatus, setBridgeStatus] = React.useState(null);
React.useEffect(() => {
if (isOpen) {
window.SonicAPI.bridgeStatus().then(s => setBridgeStatus(s)).catch(() => setBridgeStatus(null));
}
}, [isOpen]);
React.useEffect(() => { React.useEffect(() => {
if (isOpen) { if (isOpen) {
window.SonicAPI.listPlugins() window.SonicAPI.listPlugins()
@@ -5715,6 +5748,21 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
setSfUploadStatus('Error: ' + err.message); setSfUploadStatus('Error: ' + err.message);
} }
}; };
// D7: Load asset vào Native Bridge backend resolve path tht (E2) JS
// invoke load_native_instrument (C2) bridge C++ load vào channel 0.
const loadToBridge = async (name, path, type) => {
try {
const r = await window.SonicAPI.bridgeLoad({ name: name, path: path || null, instrumentType: type, channel: 0 });
if (!r || !r.path) { window.showToast && window.showToast('Không resolve được asset: ' + name, 'error'); return false; }
const ok = await window.NativeBridgeService.loadInstrument(r.path, type, 0);
window.showToast && window.showToast(ok ? ('Đã load vào Native Bridge: ' + r.path) : 'Bridge không khả dụng (dev mode?)', ok ? 'success' : 'warning');
if (ok) window.SonicAPI.bridgeStatus().then(s => setBridgeStatus(s)).catch(() => {});
return ok;
} catch (err) {
window.showToast && window.showToast('Lỗi load bridge: ' + (err.message || err), 'error');
return false;
}
};
const [pmTab, setPmTab] = React.useState('soundfont'); const [pmTab, setPmTab] = React.useState('soundfont');
return React.createElement('div', { return React.createElement('div', {
className: 'fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm', className: 'fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm',
@@ -5799,11 +5847,16 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
React.createElement('h3', { React.createElement('h3', {
className: 'text-base font-bold text-cyan-400 flex items-center gap-2' className: 'text-base font-bold text-cyan-400 flex items-center gap-2'
}, React.createElement('i', { 'data-lucide': 'zap', className: 'w-4 h-4' }), 'Plugin Manager (SoundFont / VSTi)'), }, React.createElement('i', { 'data-lucide': 'zap', className: 'w-4 h-4' }), 'Plugin Manager (SoundFont / VSTi)'),
React.createElement('button', { React.createElement('div', { className: 'flex items-center gap-2' },
onClick: onClose, React.createElement('span', {
className: 'text-zinc-500 hover:text-zinc-200 transition' className: `text-[9px] font-bold uppercase tracking-wider px-2 py-0.5 rounded border ${bridgeStatus && bridgeStatus.connected ? 'text-emerald-400 border-emerald-700 bg-emerald-900/30' : 'text-zinc-500 border-zinc-700 bg-zinc-800'}`,
}, React.createElement('i', { 'data-lucide': 'x', className: 'w-4 h-4' })) title: 'Native Bridge (C++ engine) — ' + (bridgeStatus ? JSON.stringify(bridgeStatus).slice(0, 120) : 'chưa query')
), }, bridgeStatus && bridgeStatus.connected ? '● Bridge ON' : '● Bridge OFF'),
React.createElement('button', {
onClick: onClose,
className: 'text-zinc-500 hover:text-zinc-200 transition'
}, React.createElement('i', { 'data-lucide': 'x', className: 'w-4 h-4' }))
)),
// Left-right body // Left-right body
React.createElement('div', { React.createElement('div', {
className: 'flex flex-1 overflow-hidden', className: 'flex flex-1 overflow-hidden',
@@ -5853,6 +5906,11 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
className: 'text-[10px] bg-teal-800 hover:bg-teal-700 text-white px-2 py-1 rounded transition shrink-0', className: 'text-[10px] bg-teal-800 hover:bg-teal-700 text-white px-2 py-1 rounded transition shrink-0',
title: 'Mở trong Carla (native GUI)' title: 'Mở trong Carla (native GUI)'
}, '🎛 Carla'), }, '🎛 Carla'),
React.createElement('button', {
onClick: (e) => { e.stopPropagation(); loadToBridge(v.name || v.id, v.path, v.type || 'VST3'); },
className: 'text-[10px] bg-cyan-800 hover:bg-cyan-700 text-white px-2 py-1 rounded transition shrink-0',
title: 'Load vào Native Bridge (C++ engine)'
}, 'Load Bridge'),
React.createElement('span', { className: 'text-[10px] bg-violet-950/40 text-violet-400 px-2 py-0.5 rounded-full border border-violet-800/30' }, v.type || 'VST3') React.createElement('span', { className: 'text-[10px] bg-violet-950/40 text-violet-400 px-2 py-0.5 rounded-full border border-violet-800/30' }, v.type || 'VST3')
) )
) )
@@ -5883,6 +5941,11 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
) )
), ),
React.createElement('div', { className: 'flex items-center gap-2' }, React.createElement('div', { className: 'flex items-center gap-2' },
React.createElement('button', {
onClick: (e) => { e.stopPropagation(); loadToBridge(sf.id, sf.file || sf.path, 'SF2'); },
className: 'text-[10px] bg-cyan-800 hover:bg-cyan-700 text-white px-2 py-1 rounded transition shrink-0',
title: 'Load vào Native Bridge (C++ engine)'
}, 'Load Bridge'),
React.createElement('span', { className: 'text-[10px] text-zinc-500' }, expanded ? '▾' : '▸'), React.createElement('span', { className: 'text-[10px] text-zinc-500' }, expanded ? '▾' : '▸'),
React.createElement('button', { React.createElement('button', {
onClick: (e) => { e.stopPropagation(); setSfToDelete(sf); }, onClick: (e) => { e.stopPropagation(); setSfToDelete(sf); },
@@ -8192,7 +8255,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId); playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
return; return;
} }
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine); scheduleMidiNoteDispatch(pvTrk, n.pitch, n.velocity || 0.8, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
}); });
} }
} }
@@ -8561,7 +8624,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) { if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId); playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
} else { } else {
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine); scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
} }
} }
} }
@@ -8829,7 +8892,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// playNote (FluidSynth nhc c THT ca track). _playNoteFallback ch // playNote (FluidSynth nhc c THT ca track). _playNoteFallback ch
// là oscillator beep (sai âm vi percussion/soundfont user: note v // là oscillator beep (sai âm vi percussion/soundfont user: note v
// mi nghe nhc c track trưc). // mi nghe nhc c track trưc).
window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined); scheduleMidiNoteDispatch(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
} }
} }
}; };
@@ -8927,10 +8990,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId); playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) { } else if (window.SonicSF && window.SonicSF.playNote) {
var pvCtx = getAudioContext(); var pvCtx = getAudioContext();
var pvVel = Math.round(brushVelocityRef.current * 127);
// playNote (FluidSynth nhc c THT). _playNoteFallback = oscillator // playNote (FluidSynth nhc c THT). _playNoteFallback = oscillator
// beep sai âm (percussion/soundfont). // beep sai âm (percussion/soundfont).
window.SonicSF.playNote(p, pvVel, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine); scheduleMidiNoteDispatch(pvTrk, p, brushVelocityRef.current || 0.8, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
previewPitchRef.current = p; previewPitchRef.current = p;
} }
// MIDI Carla (track VSTi + ARM + Carla local): preview realtime // MIDI Carla (track VSTi + ARM + Carla local): preview realtime
@@ -9320,7 +9382,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (kbNative) { if (kbNative) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch); playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
} }
if (window.SonicSF && !kbNative) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
} else if (window.SonicSF && !kbNative) {
// FIX: gi note theo thi gian bm phím durationMs ln (5s) // FIX: gi note theo thi gian bm phím durationMs ln (5s)
// ch là auto-off phòng h; mouseup/mouseleave gi stopNote dng // ch là auto-off phòng h; mouseup/mouseleave gi stopNote dng
// NGAY (trưc đây 500ms note t tt gia chng khi gi phím). // NGAY (trưc đây 500ms note t tt gia chng khi gi phím).
@@ -9345,7 +9409,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) { if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch); playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
} }
if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
} else if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
// gi note khi kéo qua phím (mouse enter) dng bng mouseup/leave // gi note khi kéo qua phím (mouse enter) dng bng mouseup/leave
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine); window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
} }
@@ -9362,7 +9428,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Dng note khi th phím tránh kt âm (loop liên tc) vi soundfont // Dng note khi th phím tránh kt âm (loop liên tc) vi soundfont
try { try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch); if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch); if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {} } catch (e) {}
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; } if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} } if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
@@ -9372,7 +9440,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Kéo chut ra khi phím dng note ca phím đó // Kéo chut ra khi phím dng note ca phím đó
try { try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch); if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch); if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {} } catch (e) {}
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} } if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
} }
@@ -14063,7 +14133,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) { if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
selectTokenRef.current++; selectTokenRef.current++;
const token = selectTokenRef.current; const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {} try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes(); stopAllNativeSfNotes();
playMidiPreview(cur, token); playMidiPreview(cur, token);
} }
@@ -14539,7 +14609,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) { if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
selectTokenRef.current++; selectTokenRef.current++;
const token = selectTokenRef.current; const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {} try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes(); stopAllNativeSfNotes();
playMidiPreview(cur, token); playMidiPreview(cur, token);
} }
@@ -14710,7 +14780,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
if (cur && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) { if (cur && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
selectTokenRef.current++; selectTokenRef.current++;
const token = selectTokenRef.current; const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {} try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes(); stopAllNativeSfNotes();
playMidiPreview(cur, token); playMidiPreview(cur, token);
} }
@@ -15368,7 +15438,7 @@ const App = () => {
const _nowVst = _hasInst && !isSfInstrument; const _nowVst = _hasInst && !isSfInstrument;
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) { if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
window.SonicCarlaMidi.stopBridge(); window.SonicCarlaMidi.stopBridge();
try { if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {} try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes(); stopAllNativeSfNotes();
console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST'); console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST');
} }
@@ -15608,7 +15678,15 @@ const App = () => {
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {} try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
// Route MIDI input to ALL armed tracks on their dedicated channels // Route MIDI input to ALL armed tracks on their dedicated channels
// Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F) // Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F)
if (window.SonicSF) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
// D1: bridge active -> 1 cng router (per-track channel alloc)
// Rust SHM C++ bridge; gi SonicSF/Carla fallback nhánh else.
var bTracks = activeTracksRef.current || [];
bTracks.forEach(function (bt) {
if (!bt.isArmed) return;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: bt.id, pitch: pitch, velocity: scaledVel / 127 }); } catch (e) {}
});
} else if (window.SonicSF) {
var allTracks = activeTracksRef.current || []; var allTracks = activeTracksRef.current || [];
var arSubs = subTabsRef && subTabsRef.current ? subTabsRef.current.filter(function(s) { return s.type === 'PIANO_ROLL' && s.isArmed; }) : []; var arSubs = subTabsRef && subTabsRef.current ? subTabsRef.current.filter(function(s) { return s.type === 'PIANO_ROLL' && s.isArmed; }) : [];
var armedTracks = allTracks.filter(function(t) { return t.isArmed; }); var armedTracks = allTracks.filter(function(t) { return t.isArmed; });
@@ -15677,7 +15755,13 @@ const App = () => {
} }
// Stop the note on ALL tracks (not just armed) to prevent stuck notes // Stop the note on ALL tracks (not just armed) to prevent stuck notes
// when ARM is toggled off while a key is held // when ARM is toggled off while a key is held
if (window.SonicSF && window.SonicSF.stopNote) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
// D1: bridge note-off qua router (channel trùng NOTE_ON track.id).
var bStopTracks = activeTracksRef.current || [];
bStopTracks.forEach(function (bst) {
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: bst.id, pitch: pitch, velocity: 0 }); } catch (e) {}
});
} else if (window.SonicSF && window.SonicSF.stopNote) {
var stopTracks = activeTracksRef.current || []; var stopTracks = activeTracksRef.current || [];
stopTracks.forEach(function(st) { stopTracks.forEach(function(st) {
// Only stop channels that actually carry this track's notes // Only stop channels that actually carry this track's notes
@@ -16894,6 +16978,16 @@ const App = () => {
const [pluginManagerModalOpen, setPluginManagerModalOpen] = useState(false); const [pluginManagerModalOpen, setPluginManagerModalOpen] = useState(false);
const [pluginsData, setPluginsData] = useState(null); const [pluginsData, setPluginsData] = useState(null);
// D10: bridge indicator + "Ép dùng WASM" debug toggle.
const [bridgeUi, setBridgeUi] = useState({ connected: false, forceWasm: false });
React.useEffect(() => {
if (!window.NativeBridgeService) return;
setBridgeUi(s => ({ ...s, connected: !!window.NativeBridgeService.isBridgeConnected }));
window.NativeBridgeService.onStatusChange(function (st) {
setBridgeUi(s => ({ ...s, connected: !!st.connected }));
});
return () => window.NativeBridgeService.onStatusChange(null);
}, []);
const loadAudioBuffersForTracks = async (tracksList) => { const loadAudioBuffersForTracks = async (tracksList) => {
let hasLoadedAny = false; let hasLoadedAny = false;
@@ -17641,6 +17735,9 @@ const App = () => {
const isDraggingSubTabRef = useRef(false); const isDraggingSubTabRef = useRef(false);
const handlePlayPauseRef = useRef(null); const handlePlayPauseRef = useRef(null);
const currentTimeRef = useRef(currentTime); const currentTimeRef = useRef(currentTime);
// D9: playhead sample counter cho native bridge (A11/A13 dùng đ đng b
// timeline sample-accurate); cp nht trong updatePlayhead khi bridge active.
const bridgePlayheadSampleRef = useRef(0);
// Resume main/session play khi ri PIANO ROLL tab: m piano roll lúc main // Resume main/session play khi ri PIANO ROLL tab: m piano roll lúc main
// đang play bm Space (play piano roll) stopAllPlayback dng main // đang play bm Space (play piano roll) stopAllPlayback dng main
// quay li MAIN/SECTION CÂM. Lưu {offset, audioTime} lúc m tab khi // quay li MAIN/SECTION CÂM. Lưu {offset, audioTime} lúc m tab khi
@@ -20624,6 +20721,25 @@ const App = () => {
}; };
const updatePlayhead = () => { const updatePlayhead = () => {
// D9: playhead sample counter cho bridge C++ A11/A13 dùng đ đng b
// timeline sample-accurate (scheduling theo sampleOffset).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected && (isPlaying || isPlayingRef.current)) {
try {
const _pctx = getAudioContext();
const _elapsed = _pctx.currentTime - (startAudioTimeRef.current || _pctx.currentTime);
bridgePlayheadSampleRef.current = Math.max(0, Math.floor((startOffsetTimeRef.current + _elapsed) * (_pctx.sampleRate || 44100)));
// set_position ~mi giây: C++ A13 cp nht timeline anchor, ti ưu cho
// seek/loop (Rust pump + C++ không t biết v trí; JS là ngun duy nht).
try {
const _sr = _pctx.sampleRate || 44100;
if (!window.__bridgeLastPosSent) window.__bridgeLastPosSent = 0;
if (bridgePlayheadSampleRef.current - window.__bridgeLastPosSent > _sr) {
window.__bridgeLastPosSent = bridgePlayheadSampleRef.current;
window.NativeBridgeService.transport('set_position', bridgePlayheadSampleRef.current);
}
} catch (e2) {}
} catch (e) {}
}
// Realtime re-schedule: khi đang play (loop play) mà items (v trí/speed/ // Realtime re-schedule: khi đang play (loop play) mà items (v trí/speed/
// duration k c ni dung section tab) thay đi dng + schedule li t // duration k c ni dung section tab) thay đi dng + schedule li t
// playhead hin ti đ item mi phát đúng v trí mi (không phát ni dung // playhead hin ti đ item mi phát đúng v trí mi (không phát ni dung
@@ -21287,6 +21403,12 @@ const App = () => {
const _prSubKeys = Object.keys(activeTrackNodesRef.current).filter(k => k.endsWith('_sub_' + _activeSub.trackId)); const _prSubKeys = Object.keys(activeTrackNodesRef.current).filter(k => k.endsWith('_sub_' + _activeSub.trackId));
_prNode = _prSubKeys.length ? activeTrackNodesRef.current[_prSubKeys[0]] : null; _prNode = _prSubKeys.length ? activeTrackNodesRef.current[_prSubKeys[0]] : null;
} }
// D5: bridge active ni BridgeAudioNode vào node track (sfEntry
// FX chain mastering) thay vì SonicSF.setOutputDestination.
if (_prNode && window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
window.AudioRoutingEngine.connect(window.BridgeAudioNode, _prNode, _activeSub.trackId);
return;
}
if (_prNode && window.SonicSF && window.SonicSF.setOutputDestination) { if (_prNode && window.SonicSF && window.SonicSF.setOutputDestination) {
if (_prNode.sfEntry) { if (_prNode.sfEntry) {
window.SonicSF.setOutputDestination(_prNode.sfEntry); window.SonicSF.setOutputDestination(_prNode.sfEntry);
@@ -21333,6 +21455,11 @@ const App = () => {
// SF ALWAYS enters the track's own FX chain (sfEntry sfModules when // SF ALWAYS enters the track's own FX chain (sfEntry sfModules when
// PWR ON). The button only switches the post-FX route (sfRouteGain / // PWR ON). The button only switches the post-FX route (sfRouteGain /
// sfDryGain) to skip or include the Mastering FX Chain. // sfDryGain) to skip or include the Mastering FX Chain.
// D5: bridge active ni vào node track (sfEntry FX chain).
if (node && window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
window.AudioRoutingEngine.connect(window.BridgeAudioNode, node, t.id);
return;
}
if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) { if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) {
console.log('[SFRoute] dest=sfEntry node', t.id, 'sfRouteGain=' + (node.sfRouteGain ? node.sfRouteGain.gain.value : 'MISSING'), 'sfDryGain=' + (node.sfDryGain ? node.sfDryGain.gain.value : 'MISSING')); console.log('[SFRoute] dest=sfEntry node', t.id, 'sfRouteGain=' + (node.sfRouteGain ? node.sfRouteGain.gain.value : 'MISSING'), 'sfDryGain=' + (node.sfDryGain ? node.sfDryGain.gain.value : 'MISSING'));
window.SonicSF.setOutputDestination(node.sfEntry); window.SonicSF.setOutputDestination(node.sfEntry);
@@ -21362,7 +21489,11 @@ const App = () => {
return; return;
} }
} }
if (window.SonicSF && window.SonicSF.setOutputDestination) { // D5: bridge active + không track nào audible ngt khi graph (v
// masterBus qua updateSfRouting ln sau khi có track).
if (window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.AudioRoutingEngine.disconnect();
} else if (window.SonicSF && window.SonicSF.setOutputDestination) {
window.SonicSF.setOutputDestination(null); window.SonicSF.setOutputDestination(null);
} }
} catch (e) { } catch (e) {
@@ -21578,8 +21709,49 @@ const App = () => {
); );
}; };
// D2: timeline MIDI note -> native bridge (khi bridge active) thay vì
// SonicSF (FluidSynth WASM). Gi setTimeout scheduling (như SonicSF cũ);
// bridge t render note-off sau duration. A11 (sample-accurate) s thay
// setTimeout bng sampleOffset đy thng vào SHM.
const scheduleMidiNoteDispatch = (track, pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine) => {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
const ctx = getAudioContext();
const startAt = startTime || ctx.currentTime; // undefined/null = phát ngay
const delayMs = Math.max(0, (startAt - ctx.currentTime) * 1000);
// Guard chng note-on tr sau Stop: CH khi là timeline (startTime tht).
const guardPlay = startTime != null ? function () { return isPlayingRef.current; } : function () { return true; };
const trkId = track ? track.id : 0;
// D8: track đi instrument gi CC0/CC32 (bank 0) + program change qua
// bridge (A12) mt ln mi program mi mi channel.
if (program !== undefined && program !== null) {
const lastP = window.__bridgeLastProgram || (window.__bridgeLastProgram = {});
if (lastP[trkId] !== program) {
lastP[trkId] = program;
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 0, velocity: 0, data2: 0 }); } catch (e) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 32, velocity: 0, data2: 0 }); } catch (e) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'PROGRAM', channel: trkId, pitch: 0, velocity: 0, data2: program }); } catch (e) {}
}
}
setTimeout(function () {
if (!guardPlay()) return;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8 }); } catch (e) {}
}, delayMs);
setTimeout(function () {
if (!guardPlay()) return;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0 }); } catch (e) {}
}, delayMs + (durMs || 1000) + 30);
return;
}
window.SonicSF.playNote(pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine);
};
const startTrackPlayback = offsetTime => { const startTrackPlayback = offsetTime => {
const context = getAudioContext(); const context = getAudioContext();
// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đng
// b timeline; A13 C++ x lý arg1=playheadSamples sau này).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
try { window.NativeBridgeService.transport('play'); } catch (e) {}
}
// FIX: đng b mastering + Carla status NGAY khi play MIDI item phi // FIX: đng b mastering + Carla status NGAY khi play MIDI item phi
// qua mastering FX (khi bt) và qua Carla bridge (khi VSTi loaded). // qua mastering FX (khi bt) và qua Carla bridge (khi VSTi loaded).
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {} try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
@@ -21675,8 +21847,8 @@ const App = () => {
const delay = noteStartSec - offsetTime; const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay; const startTime = context.currentTime + delay;
if (!routeToCarla && window.SonicSF) { if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote( scheduleMidiNoteDispatch(
note.pitch || 60, track, note.pitch || 60,
note.velocity || 0.8, note.velocity || 0.8,
durationMs, durationMs,
startTime, startTime,
@@ -21703,8 +21875,8 @@ const App = () => {
const playOffset = offsetTime - noteStartSec; const playOffset = offsetTime - noteStartSec;
const remainingDurMs = (noteEndSec - offsetTime) * 1000; const remainingDurMs = (noteEndSec - offsetTime) * 1000;
if (!routeToCarla && window.SonicSF) { if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote( scheduleMidiNoteDispatch(
note.pitch || 60, track, note.pitch || 60,
note.velocity || 0.8, note.velocity || 0.8,
remainingDurMs, remainingDurMs,
context.currentTime, context.currentTime,
@@ -21832,8 +22004,8 @@ const App = () => {
const startTime = context.currentTime + delay; const startTime = context.currentTime + delay;
const playDurMs = (notePlayEndMain - noteStartMain) * 1000; const playDurMs = (notePlayEndMain - noteStartMain) * 1000;
if (!subRouteCarla && window.SonicSF) { if (!subRouteCarla && window.SonicSF) {
window.SonicSF.playNote( scheduleMidiNoteDispatch(
note.pitch || 60, subTrack, note.pitch || 60,
note.velocity || 0.8, note.velocity || 0.8,
playDurMs, playDurMs,
startTime, startTime,
@@ -21862,8 +22034,8 @@ const App = () => {
} else { } else {
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000; const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
if (!subRouteCarla && window.SonicSF) { if (!subRouteCarla && window.SonicSF) {
window.SonicSF.playNote( scheduleMidiNoteDispatch(
note.pitch || 60, subTrack, note.pitch || 60,
note.velocity || 0.8, note.velocity || 0.8,
remainingDurMs, remainingDurMs,
context.currentTime, context.currentTime,
@@ -21961,8 +22133,8 @@ const App = () => {
const delay = noteStartSec - offsetTime; const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay; const startTime = context.currentTime + delay;
if (!routeToCarla && window.SonicSF) { if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote( scheduleMidiNoteDispatch(
note.pitch || 60, track, note.pitch || 60,
note.velocity || 0.8, note.velocity || 0.8,
durationMs, durationMs,
startTime, startTime,
@@ -21977,8 +22149,8 @@ const App = () => {
} else { } else {
const remainingDurMs = (noteEndSec - offsetTime) * 1000; const remainingDurMs = (noteEndSec - offsetTime) * 1000;
if (!routeToCarla && window.SonicSF) { if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote( scheduleMidiNoteDispatch(
note.pitch || 60, track, note.pitch || 60,
note.velocity || 0.8, note.velocity || 0.8,
remainingDurMs, remainingDurMs,
context.currentTime, context.currentTime,
@@ -22043,7 +22215,7 @@ const App = () => {
// Carla không play FluidSynth GM sai âm chng lên) // Carla không play FluidSynth GM sai âm chng lên)
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)); const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));
if (!routeToCarla && window.SonicSF) { if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine); scheduleMidiNoteDispatch(track, note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
} }
// MIDI items Carla (track VSTi + Carla local): phát VSTi realtime // MIDI items Carla (track VSTi + Carla local): phát VSTi realtime
// (schedule theo audio clock bng setTimeout preview, timing gn đúng). // (schedule theo audio clock bng setTimeout preview, timing gn đúng).
@@ -22079,7 +22251,7 @@ const App = () => {
// Ghost note Carla bridge khi ghost track VSTi (ch route Carla) // Ghost note Carla bridge khi ghost track VSTi (ch route Carla)
var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth)); var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));
if (!gRouteCarla && window.SonicSF) { if (!gRouteCarla && window.SonicSF) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth); scheduleMidiNoteDispatch(ghostTrack, note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
} }
if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs); if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
} }
@@ -22210,6 +22382,11 @@ const App = () => {
try { heldMidiNotesRef.current = {}; } catch (e) { } try { heldMidiNotesRef.current = {}; } catch (e) { }
stopMidiCapture(); stopMidiCapture();
stopAllNativeSfNotes(); stopAllNativeSfNotes();
// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết âm
// ngân tc thì; A13 C++ flush note-off).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
try { window.NativeBridgeService.transport('stop'); } catch (e) {}
}
if (window.SonicSF) { if (window.SonicSF) {
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); } try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
// Dng trit đ: noteoff tng note + hy scheduled note-on (hết âm stuck) // Dng trit đ: noteoff tng note + hy scheduled note-on (hết âm stuck)
@@ -28515,6 +28692,19 @@ STRICT CONSTRAINTS:
className: "font-mono text-[20px] text-zinc-400 tabular-nums", className: "font-mono text-[20px] text-zinc-400 tabular-nums",
title: "Tổng thời gian dự án" title: "Tổng thời gian dự án"
}, formatTime(projectEnd)), /*#__PURE__*/React.createElement("div", { }, formatTime(projectEnd)), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1.5 ml-3",
title: bridgeUi.connected && !bridgeUi.forceWasm ? "Engine phát qua Native Bridge (C++ FluidSynth/sfizz/VST3)" : "Engine phát qua SonicSF WASM"
}, /*#__PURE__*/React.createElement("span", {
className: "text-[9px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded border " + (bridgeUi.connected && !bridgeUi.forceWasm ? "text-emerald-400 border-emerald-700 bg-emerald-900/30" : "text-zinc-500 border-zinc-700 bg-zinc-800")
}, bridgeUi.connected && !bridgeUi.forceWasm ? "Bridge" : "WASM"), /*#__PURE__*/React.createElement("button", {
onClick: () => {
const v = !bridgeUi.forceWasm;
setBridgeUi(s => ({ ...s, forceWasm: v }));
if (window.SonicMidiRouter && window.SonicMidiRouter.setForceWasm) window.SonicMidiRouter.setForceWasm(v);
},
className: `px-1.5 py-0.5 rounded text-[9px] font-bold border transition ${bridgeUi.forceWasm ? "bg-amber-600 text-black border-amber-500" : "bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200"}`,
title: "Debug: ép dùng SonicSF WASM bỏ qua bridge"
}, "Ép WASM")), /*#__PURE__*/React.createElement("div", {
className: "flex-1" className: "flex-1"
})),(() => { })),(() => {
const dockPanels = { const dockPanels = {
File diff suppressed because one or more lines are too long
+5
View File
@@ -113,6 +113,11 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
getSoundfontCatalog: () => apiRequest('/api/v1/plugins/soundfonts/catalog', { method: 'GET' }), getSoundfontCatalog: () => apiRequest('/api/v1/plugins/soundfonts/catalog', { method: 'GET' }),
listDefaultSoundfonts: () => apiRequest('/api/v1/plugins/default-soundfonts', { method: 'GET' }), listDefaultSoundfonts: () => apiRequest('/api/v1/plugins/default-soundfonts', { method: 'GET' }),
listSoundfontInstruments: (sfId) => apiRequest(`/api/v1/plugins/soundfont-instruments/${sfId}`, { method: 'GET' }), listSoundfontInstruments: (sfId) => apiRequest(`/api/v1/plugins/soundfont-instruments/${sfId}`, { method: 'GET' }),
// Native Host Bridge (daw_vst_bridge C++): trạng thái + load asset +
// tail bridge.log (E1/E2/E5).
bridgeStatus: () => apiRequest('/api/v1/bridge/status', { method: 'GET' }),
bridgeLoad: (payload) => apiRequest('/api/v1/bridge/load', { method: 'POST', body: JSON.stringify(payload) }),
bridgeLog: (lines = 100) => apiRequest(`/api/v1/bridge/log?lines=${lines}`, { method: 'GET' }),
getAIPresets: () => apiRequest('/api/v1/ai/presets', { method: 'GET' }), getAIPresets: () => apiRequest('/api/v1/ai/presets', { method: 'GET' }),
saveAIPreset: (preset) => apiRequest('/api/v1/ai/presets', { method: 'POST', body: JSON.stringify(preset) }), saveAIPreset: (preset) => apiRequest('/api/v1/ai/presets', { method: 'POST', body: JSON.stringify(preset) }),
deleteAIPreset: (presetId) => apiRequest(`/api/v1/ai/presets/${presetId}`, { method: 'DELETE' }), deleteAIPreset: (presetId) => apiRequest(`/api/v1/ai/presets/${presetId}`, { method: 'DELETE' }),
@@ -0,0 +1,40 @@
// app/static/js/services/audioRoutingEngine.js
// Routes the native bridge audio node into the DAW graph:
// bridge node -> track.sfEntry (FX rack chain) -> fader/pan -> masterBus
// Falls back to masterBus.input when no per-track sfEntry exists.
(function () {
var engine = {
_connected: false,
_trackId: null,
isConnected: function () { return this._connected; },
/** bridgeNode = window.BridgeAudioNode; trackCtx = track node ({ sfEntry, gainNode }). */
connect: function (bridgeNode, trackCtx, trackId) {
if (!bridgeNode || !bridgeNode.getOutputNode) return false;
this.disconnect();
var dest = null;
if (trackCtx && trackCtx.sfEntry) dest = trackCtx.sfEntry;
else if (trackCtx && trackCtx.gainNode) dest = trackCtx.gainNode;
else if (window.masterBus && window.masterBus.input) dest = window.masterBus.input;
if (!dest) {
console.warn('[AudioRoutingEngine] no destination (no trackCtx / masterBus)');
return false;
}
if (!bridgeNode.isReady()) bridgeNode.init();
bridgeNode.connect(dest);
this._connected = true;
this._trackId = trackId || null;
console.log('[AudioRoutingEngine] bridge audio -> ' + (trackId || 'masterBus'));
return true;
},
disconnect: function () {
if (window.BridgeAudioNode) window.BridgeAudioNode.disconnect();
this._connected = false;
this._trackId = null;
}
};
window.AudioRoutingEngine = engine;
})();
+72
View File
@@ -0,0 +1,72 @@
// app/static/js/services/bridgeAudioNode.js
// Audio sink: consumes 'bridge-audio' PCM frames (native bridge) and plays them
// through a ScriptProcessorNode into the WebAudio graph (track FX / master bus).
(function () {
var RING_DEPTH = 8; // 8 blocks * 256 samples ~= 46ms anti-underrun
var SP_BUFFER = 4096; // ScriptProcessor chunk (16 bridge blocks)
var _queue = [];
var _spn = null;
var _gainNode = null;
var _ctx = null;
var _initialized = false;
function _getCtx() {
if (_ctx) return _ctx;
if (typeof getAudioContext === 'function') _ctx = getAudioContext();
else if (window.__sharedAudioCtx) _ctx = window.__sharedAudioCtx;
else _ctx = new (window.AudioContext || window.webkitAudioContext)();
return _ctx;
}
function init(audioCtx) {
if (_initialized) return;
_ctx = audioCtx || _getCtx();
_gainNode = _ctx.createGain();
_gainNode.gain.value = 1.0;
_spn = _ctx.createScriptProcessor(SP_BUFFER, 0, 2);
_spn.onaudioprocess = function (e) {
var L = e.outputBuffer.getChannelData(0);
var R = e.outputBuffer.getChannelData(1);
L.fill(0); R.fill(0);
if (!_queue.length) return;
var f = _queue.shift();
L.set(f.l); R.set(f.r);
};
_spn.connect(_gainNode);
_initialized = true;
console.log('[BridgeAudioNode] initialized (ring depth ' + RING_DEPTH + ')');
}
function onAudio(l, r) {
if (!_initialized) init();
_queue.push({ l: new Float32Array(l), r: new Float32Array(r) });
if (_queue.length > RING_DEPTH) _queue.shift(); // drop oldest on overrun
}
function flush() { _queue = []; }
function getOutputNode() { return _gainNode; }
function disconnect() {
flush();
if (_spn && _gainNode) {
try { _spn.disconnect(); } catch (e) {}
try { _gainNode.disconnect(); } catch (e) {}
}
// Reset state so a later init() can rebuild (AudioRoutingEngine re-connect).
_initialized = false;
_spn = null;
_gainNode = null;
_ctx = null;
}
window.BridgeAudioNode = {
init: init,
onAudio: onAudio,
flush: flush,
getOutputNode: getOutputNode,
connect: function (dest) { if (_gainNode) _gainNode.connect(dest); },
disconnect: disconnect,
isReady: function () { return _initialized; }
};
})();
@@ -0,0 +1,150 @@
// app/static/js/services/nativeBridgeService.js
// JS client for the C++ Native Host Bridge (daw_vst_bridge.exe).
// All IPC goes through Tauri commands (Rust writes the shared memory) —
// WebView2 JS cannot map Windows shared memory directly.
(function () {
// Must match native_bridge/include/INativeInstrument.h enum InstrumentType
var INSTRUMENT_TYPE = { VST3: 0, VST2: 1, SF2: 2, SF3: 2, SFZ: 3 };
var service = {
isBridgeConnected: false,
activeInstrumentType: 'VST3',
_audioCb: null,
_statusCb: null,
_tauri: function () { return window.__TAURI__; },
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
onStatusChange: function (cb) { this._statusCb = cb; },
_notifyStatus: function (connected) {
this.isBridgeConnected = !!connected;
if (this._statusCb) this._statusCb({ connected: !!connected });
},
_init: function () {
if (!this._tauri()) {
// Dev mode (Linux / plain browser): no Tauri -> log-only, app uses SonicSF.
console.log('[BridgeService] Dev mode: no __TAURI__, native bridge disabled.');
return;
}
var self = this;
try {
window.__TAURI__.event.listen('bridge-audio', function (e) {
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
});
window.__TAURI__.event.listen('bridge-down', function () {
self._notifyStatus(false);
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(false);
if (window.AudioRoutingEngine) window.AudioRoutingEngine.disconnect();
console.warn('[BridgeService] bridge-down event received.');
});
} catch (e) {
console.warn('[BridgeService] event listen failed:', e);
}
this.queryStatus();
},
queryStatus: async function () {
if (!this._tauri()) return { connected: false };
try {
var s = await window.__TAURI__.core.invoke('bridge_status');
this._notifyStatus(!!s.connected);
return s;
} catch (e) {
this._notifyStatus(false);
return { connected: false };
}
},
/**
* 1. LOAD NEW INSTRUMENT INTO BRIDGE (VST3 / VST2 / SF2 / SF3 / SFZ)
* instrumentType: 'VST3' | 'VST2' | 'SF2' | 'SF3' | 'SFZ'
* channel: MIDI channel to assign this instrument to (A10 multi-instance).
*/
loadInstrument: async function (filePath, instrumentType, channel) {
this.activeInstrumentType = instrumentType;
console.log('[BridgeService] Loading ' + instrumentType + ' asset: ' + filePath + ' ch=' + channel);
if (!this._tauri()) return false;
try {
await window.__TAURI__.core.invoke('load_native_instrument', {
path: filePath,
instrumentType: INSTRUMENT_TYPE[instrumentType] !== undefined ? INSTRUMENT_TYPE[instrumentType] : 0,
channel: channel === undefined ? 0 : channel
});
return true;
} catch (e) {
console.warn('[BridgeService] loadInstrument failed:', e);
return false;
}
},
/**
* 2. UNIFIED MIDI EVENT DISPATCH
* cmd: 'NOTE_ON' | 'NOTE_OFF' | 'CC' | 'PROGRAM' | 'PITCH_BEND'
* velocity 0..1; sampleOffset in samples within the current audio block.
* data2/data3 (A12): CC value / program / PB LSB|MSB.
*/
dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3) {
if (!this._tauri()) return false;
var safeVelocity = Math.floor(Math.min(1.0, Math.max(0.0, velocity)) * 127);
var byteCmd;
switch (cmd) {
case 'CC': byteCmd = 0xB; break;
case 'PROGRAM': byteCmd = 0xC; break;
case 'PITCH_BEND': byteCmd = 0xE; break;
default: byteCmd = cmd === 'NOTE_ON' ? 0x9 : 0x8; break;
}
window.__TAURI__.core.invoke('push_midi_event', {
command: byteCmd,
channel: channel,
pitch: pitch,
velocity: safeVelocity,
data2: data2 === undefined ? 0 : data2,
data3: data3 === undefined ? 0 : data3,
sampleOffset: sampleOffset || 0
}).catch(function (e) { console.warn('[BridgeService] push_midi_event:', e); });
return true;
},
/**
* 3. OPEN FLOATING CHILD WINDOW NATIVE GUI
*/
openNativeGUI: async function (pluginId) {
if (!this._tauri()) return false;
try {
await window.__TAURI__.core.invoke('open_vst_gui', { pluginId: pluginId });
return true;
} catch (e) {
console.warn('[BridgeService] open_vst_gui:', e);
return false;
}
},
/**
* 4. TRANSPORT CONTROL ('play' | 'stop' | 'panic' | 'set_position')
* playhead: sample position (A13) — used by 'play' and 'set_position'.
*/
transport: function (kind, playhead) {
if (!this._tauri()) return false;
var args = { kind: kind };
if (playhead !== undefined) args.playhead = playhead;
window.__TAURI__.core.invoke('transport_control', args)
.catch(function (e) { console.warn('[BridgeService] transport_control:', e); });
return true;
},
/**
* 5. AUDIO SINK: register consumer of PCM frames from the bridge.
* cb(l: Float32Array, r: Float32Array)
*/
onAudio: function (cb) {
this._audioCb = cb;
}
};
window.NativeBridgeService = service;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { service._init(); });
} else {
service._init();
}
})();
+5
View File
@@ -74,6 +74,9 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
window.SonicCarlaMidi = window.SonicCarlaMidi || { window.SonicCarlaMidi = window.SonicCarlaMidi || {
shouldRoute: function (synthEngine, isArmed) { shouldRoute: function (synthEngine, isArmed) {
try { try {
// D4: bridge active → VSTi do native bridge host, KHÔNG route Carla
// (tránh kép âm).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) return false;
var c = window.SonicRuntime && window.SonicRuntime.capabilities; var c = window.SonicRuntime && window.SonicRuntime.capabilities;
if (!c || !c.features || !c.features.carla_local) return false; if (!c || !c.features || !c.features.carla_local) return false;
if (!isArmed) return false; if (!isArmed) return false;
@@ -85,6 +88,8 @@ window.SonicCarlaMidi = window.SonicCarlaMidi || {
// user đã chủ động bấm Play trên item đó). // user đã chủ động bấm Play trên item đó).
shouldRoutePlayback: function (synthEngine) { shouldRoutePlayback: function (synthEngine) {
try { try {
// D4: bridge active → KHÔNG route Carla (bridge host VSTi).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) return false;
var c = window.SonicRuntime && window.SonicRuntime.capabilities; var c = window.SonicRuntime && window.SonicRuntime.capabilities;
if (!c || !c.features || !c.features.carla_local) return false; if (!c || !c.features || !c.features.carla_local) return false;
var se = synthEngine || {}; var se = synthEngine || {};
@@ -0,0 +1,73 @@
// app/static/js/services/unifiedMidiRouter.js
// Single MIDI entry point for Web MIDI keyboard + timeline playback.
// bridge connected -> NativeBridgeService.dispatchMidiEvent (Rust SHM -> C++ bridge)
// bridge down -> onFallback callback (app.jsx wires SonicSF/Carla path).
(function () {
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15]; // skip 9 (percussion)
var router = {
bridgeConnected: false,
onFallback: null, // function(cmd, channel, pitch, velocity) — set by app.jsx
forceWasm: false, // debug: D10 "Ép dùng WASM"
_channels: {}, // trackId -> { ch, percussion }
_nextMelodicIdx: 0,
setBridgeConnected: function (flag) {
this.bridgeConnected = !!flag;
},
setForceWasm: function (flag) { this.forceWasm = !!flag; },
isBridgeActive: function () {
return this.bridgeConnected && !this.forceWasm && !!window.NativeBridgeService;
},
/** trackId -> MIDI channel; percussion (bank 128) -> ch 9. */
allocateChannel: function (trackId, isPercussion) {
if (this._channels[trackId]) return this._channels[trackId].ch;
var ch;
if (isPercussion) {
ch = 9;
} else {
ch = MELODIC_CHANNELS[this._nextMelodicIdx % MELODIC_CHANNELS.length];
this._nextMelodicIdx++;
}
this._channels[trackId] = { ch: ch, percussion: !!isPercussion };
return ch;
},
resetChannels: function () {
this._channels = {};
this._nextMelodicIdx = 0;
},
/**
* cmd: 'NOTE_ON' | 'NOTE_OFF' | 'CC' | 'PROGRAM' | 'PITCH_BEND'
* channel: MIDI channel (or trackId -> allocateChannel first)
* velocity: 0..1 (normalized); sampleOffset: samples within current block.
* data2/data3 (A12): CC value / program / PB LSB|MSB — passed to bridge only.
*/
pushEvent: function (opts) {
var cmd = opts.cmd, ch = opts.channel, pitch = opts.pitch;
var vel = (opts.velocity === undefined ? 1.0 : opts.velocity);
var sampleOffset = opts.sampleOffset || 0;
if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion);
if (ch === undefined || ch === null) ch = 0;
if (this.isBridgeActive()) {
window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3);
return;
}
if (this.onFallback) this.onFallback(cmd, ch, pitch, vel);
},
/** Stop/panic -> bridge transport panic (flush all notes) + fallback local stopAll. */
panic: function () {
if (this.isBridgeActive() && window.NativeBridgeService.transport) {
window.NativeBridgeService.transport('panic');
}
if (this.onFallback) this.onFallback('PANIC', 0, 0, 0);
}
};
window.SonicMidiRouter = router;
})();
+4
View File
@@ -39,6 +39,10 @@
<script src="/static/js/services/storage.js?v=202608038200"></script> <script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script> <script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script> <script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608112200"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608112200"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608112200"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608112200"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></script> <script src="/static/js/services/aiGateway.js?v=202608037200"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script> <script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script> <script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
+52
View File
@@ -0,0 +1,52 @@
# build_native_bridge.ps1 - Build daw_vst_bridge.exe (C++ Native Host Bridge)
# Run on Windows x64 with VS Build Tools 2022 (C++ workload) installed.
# ASCII only (PowerShell 5.1 reads .ps1 without BOM as ANSI).
$ErrorActionPreference = "Stop"
Set-Location $PSScriptRoot
$REPO_ROOT = Resolve-Path (Join-Path $PSScriptRoot "..\..") # ...\SonicForgeStudio
$NB_DIR = Join-Path $REPO_ROOT "native_bridge"
# 1. vcpkg (bootstrap once; skip if already present)
$VCPKG_ROOT = Join-Path $env:USERPROFILE "vcpkg"
if (-not (Test-Path (Join-Path $VCPKG_ROOT "vcpkg.exe"))) {
Write-Host "== [1/6] Bootstrap vcpkg =="
git clone https://github.com/microsoft/vcpkg $VCPKG_ROOT
& (Join-Path $VCPKG_ROOT "bootstrap-vcpkg.bat") -disableMetrics
}
# 2. vcpkg deps: fluidsynth + sfizz (+ pkgconf so pkg_check_modules may work)
Write-Host "== [2/6] vcpkg install fluidsynth sfizz =="
& (Join-Path $VCPKG_ROOT "vcpkg.exe") install fluidsynth sfizz --triplet x64-windows
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: vcpkg install failed" -ForegroundColor Red; exit 1 }
# 3. VST3 SDK submodule (Steinberg; NOT in vcpkg)
Write-Host "== [3/6] VST3 SDK (submodule) =="
if (-not (Test-Path (Join-Path $NB_DIR "vst3sdk"))) {
Push-Location $NB_DIR
git submodule add https://github.com/steinbergmedia/vst3sdk.git vst3sdk
Pop-Location
}
# 4. Configure with CMake + vcpkg toolchain (MSVC)
Write-Host "== [4/6] CMake configure =="
$BUILD_DIR = Join-Path $NB_DIR "build"
New-Item -ItemType Directory -Force $BUILD_DIR | Out-Null
cmake -S $NB_DIR -B $BUILD_DIR `
-DCMAKE_TOOLCHAIN_FILE=(Join-Path $VCPKG_ROOT "scripts\buildsystems\vcpkg.cmake") `
-DCMAKE_BUILD_TYPE=Release `
-DVCPKG_TARGET_TRIPLET=x64-windows
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake configure failed" -ForegroundColor Red; exit 1 }
# 5. Build Release
Write-Host "== [5/6] CMake build =="
cmake --build $BUILD_DIR --config Release
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake build failed" -ForegroundColor Red; exit 1 }
# 6. Copy sidecar to Tauri externalBin location
Write-Host "== [6/6] Copy to src-tauri\binaries =="
$BIN_DIR = Join-Path $REPO_ROOT "src-tauri\binaries"
New-Item -ItemType Directory -Force $BIN_DIR | Out-Null
$EXE = Join-Path $BUILD_DIR "Release\daw_vst_bridge.exe"
if (-not (Test-Path $EXE)) { Write-Host "ERROR: $EXE not found" -ForegroundColor Red; exit 1 }
Copy-Item $EXE (Join-Path $BIN_DIR "daw_vst_bridge-x86_64-pc-windows-msvc.exe") -Force
Write-Host "== DONE: daw_vst_bridge.exe copied to src-tauri\binaries =="
+60
View File
@@ -0,0 +1,60 @@
# build_windows.ps1 - ONE-COMMAND build: daw_engine.exe + daw_vst_bridge.exe + Tauri (NSIS+MSI)
# Run on Windows: powershell -ExecutionPolicy Bypass -File build_windows.ps1
# ASCII only (PowerShell 5.1 ANSI limitation).
$ErrorActionPreference = "Stop"
Set-Location (Join-Path $PSScriptRoot "..\..") # repo root
Write-Host "== [0/7] Kill old sidecars (daw_engine, daw_vst_bridge) =="
Get-Process -Name "daw_engine" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Get-Process -Name "daw_vst_bridge" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 500
Write-Host "== [1/7] Python dependencies =="
python -m pip install --upgrade pip
python -m pip install -r requirements.txt pyinstaller pywin32
Write-Host "== [2/7] Frontend bundle (app.jsx -> app.precompiled.js) =="
npm install
if (-not (Test-Path "node_modules\@babel\standalone")) { npm install @babel/standalone --no-audit --no-fund }
if (-not (Test-Path "node_modules\@babel\standalone")) { Write-Host "ERROR: @babel/standalone missing" -ForegroundColor Red; exit 1 }
node build.mjs
Write-Host "== [3/7] Build daw_engine.exe (PyInstaller ONEDIR) =="
pyinstaller engine.spec --clean --noconfirm
$specContent = Get-Content "engine.spec" -Raw -ErrorAction SilentlyContinue
if ($specContent -notmatch "collect_data_files\('app'") { Write-Host "ERROR: engine.spec too old" -ForegroundColor Red; exit 1 }
python tools\verify_bundle.py
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: bundle missing assets" -ForegroundColor Red; exit 1 }
Write-Host "== [4/7] Copy engine onedir -> src-tauri\resources\daw_engine =="
if (-not (Test-Path "dist\daw_engine\daw_engine.exe")) { Write-Host "ERROR: onedir engine missing" -ForegroundColor Red; exit 1 }
if (Test-Path "src-tauri\resources\daw_engine") { Remove-Item -Recurse -Force "src-tauri\resources\daw_engine" }
New-Item -ItemType Directory -Force "src-tauri\resources\daw_engine" | Out-Null
Copy-Item "dist\daw_engine\*" "src-tauri\resources\daw_engine\" -Recurse -Force
Write-Host "== [5/7] Build native bridge (daw_vst_bridge.exe) =="
& (Join-Path $PSScriptRoot "build_native_bridge.ps1")
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: native bridge build failed" -ForegroundColor Red; exit 1 }
if (-not (Test-Path "src-tauri\binaries\daw_vst_bridge-x86_64-pc-windows-msvc.exe")) {
Write-Host "ERROR: bridge sidecar missing" -ForegroundColor Red; exit 1
}
Write-Host "== [6/7] VC++ Redistributable for hooks.nsh =="
if (-not (Test-Path src-tauri\vc_redist.x64.exe)) {
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile src-tauri\vc_redist.x64.exe
}
Write-Host "== [7/7] Tauri build (NSIS + MSI) =="
if (-not (Test-Path "src-tauri\resources\daw_engine\daw_engine.exe")) { Write-Host "ERROR: engine resource missing" -ForegroundColor Red; exit 1 }
if (-not (Test-Path "src-tauri\resources\daw_engine\_internal")) { Write-Host "ERROR: engine _internal missing" -ForegroundColor Red; exit 1 }
if (-not (Test-Path "src-tauri\binaries\daw_vst_bridge-x86_64-pc-windows-msvc.exe")) { Write-Host "ERROR: bridge externalBin missing" -ForegroundColor Red; exit 1 }
python tools\verify_bundle.py --check-bridge
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: bridge sidecar missing" -ForegroundColor Red; exit 1 }
npm install -D @tauri-apps/cli
npx tauri build
Write-Host ""
Write-Host "== DONE =="
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe"
Write-Host " MSI : src-tauri\target\release\bundle\msi\SonicForgeDAW_1.0.0_x64_en-US.msi"
Write-Host " Verify after install: %APPDATA%\SonicForgeDAW\logs\spawn.log exists=True for daw_engine AND daw_vst_bridge"
+81
View File
@@ -0,0 +1,81 @@
# test_standalone.ps1 - Post-install verification for Native Host Bridge
# Run on the Windows machine AFTER installing the NSIS setup (or from dev tree).
# ASCII only (PowerShell 5.1 ANSI limitation).
$ErrorActionPreference = "Continue"
$APPDATA = Join-Path $env:APPDATA "SonicForgeDAW"
$LOG_DIR = Join-Path $APPDATA "logs"
$BRIDGE_LOG = Join-Path $LOG_DIR "bridge.log"
Write-Host "== [1/8] daw_vst_bridge.exe process =="
$proc = Get-Process -Name "daw_vst_bridge" -ErrorAction SilentlyContinue
if ($proc) { Write-Host " OK: running (PID $($proc.Id))" } else { Write-Host " FAIL: not running" }
Write-Host "== [2/8] daw_engine.exe process =="
$eng = Get-Process -Name "daw_engine" -ErrorAction SilentlyContinue
if ($eng) { Write-Host " OK: running (PID $($eng.Id))" } else { Write-Host " FAIL: not running" }
Write-Host "== [3/8] spawn.log (engine + bridge lines) =="
$spawn = Join-Path $LOG_DIR "spawn.log"
if (Test-Path $spawn) {
$content = Get-Content $spawn -Raw
if ($content -match "daw_vst_bridge.*exists=True") { Write-Host " OK: bridge found in spawn.log" }
else { Write-Host " WARN: no daw_vst_bridge line in spawn.log (old build?)" }
if ($content -match "daw_engine.*exists=True") { Write-Host " OK: engine found in spawn.log" }
else { Write-Host " FAIL: engine not found in spawn.log" }
} else { Write-Host " FAIL: spawn.log missing" }
Write-Host "== [4/8] Shared Memory mapping SonicForge_DAW_IPC =="
# Requires admin? Usually not for read. Use a tiny C# helper to OpenFileMapping.
$shmCheck = @'
using System;
using System.Runtime.InteropServices;
public static class ShmCheck {
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
static extern IntPtr OpenFileMapping(uint dwDesiredAccess, bool bInheritHandle, string lpName);
public static int Check() {
IntPtr h = OpenFileMapping(0x6, false, "SonicForge_DAW_IPC"); // FILE_MAP_READ|FILE_MAP_WRITE
if (h == IntPtr.Zero) return Marshal.GetLastWin32Error();
return 0;
}
}
'@
try {
Add-Type -TypeDefinition $shmCheck -Language CSharp -ErrorAction Stop
$r = [ShmCheck]::Check()
if ($r -eq 0) { Write-Host " OK: SonicForge_DAW_IPC exists" } else { Write-Host " FAIL: shared memory not found (win32 err $r)" }
} catch { Write-Host " SKIP: cannot check SHM (compile error: $($_.Exception.Message))" }
Write-Host "== [5/8] Engine HTTP health =="
$health = $null
for ($p = 8000; $p -le 8010 -and -not $health; $p++) {
try { $health = Invoke-RestMethod -Uri "http://127.0.0.1:$p/health" -TimeoutSec 2 } catch {}
}
if ($health) { Write-Host " OK: engine health on port $p -> $($health | ConvertTo-Json -Compress)" }
else { Write-Host " FAIL: no engine on 8000-8010" }
Write-Host "== [6/8] Bridge status via engine API =="
try {
$st = Invoke-RestMethod -Uri "http://127.0.0.1:$p/api/v1/bridge/status" -TimeoutSec 3
Write-Host " bridge_status: $($st | ConvertTo-Json -Compress)"
} catch { Write-Host " WARN: /api/v1/bridge/status not available yet (endpoint added in Giai doan 6)" }
Write-Host "== [7/8] Audio smoke test: load SF2 + note -> PCM in SHM =="
# Requires a test soundfont; adjust path to a real .sf2 on this machine.
$sf = "C:\SonicForgeDAW\soundfonts\SGM-V2.01.sf2"
if (Test-Path $sf) {
try {
$body = @{ path = $sf; type = "SF2" } | ConvertTo-Json
Invoke-RestMethod -Uri "http://127.0.0.1:$p/api/v1/bridge/load" -Method Post -Body $body -ContentType "application/json" -TimeoutSec 30 | Out-Null
Write-Host " OK: bridge load SF2 requested. Press keys on MIDI keyboard and watch VU meters."
} catch { Write-Host " WARN: bridge/load failed ($($_.Exception.Message))" }
} else { Write-Host " SKIP: $sf not found - set a real .sf2 path" }
Write-Host "== [8/8] Manual checklist (spec VII) =="
Write-Host " [ ] Load VST3 (Vital.vst3) -> open floating GUI -> tweak knobs"
Write-Host " [ ] Load SF2 (SGM-V2.01.sf2) bank 0 prog 0 -> piano"
Write-Host " [ ] Load SFZ (SalamanderPiano.sfz) -> samples play"
Write-Host " [ ] MIDI keyboard live + Timeline play together -> no choke/dropout"
Write-Host " [ ] Track EQ cutoff + Master maximizer affect bridge audio instantly"
Write-Host " [ ] Kill daw_vst_bridge.exe -> app falls back to SonicSF WASM (no crash)"
Write-Host "== DONE =="
+59
View File
@@ -0,0 +1,59 @@
cmake_minimum_required(VERSION 3.20)
project(daw_vst_bridge LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ── 1. VST3 SDK (Steinberg, vendored as git submodule — NOT in vcpkg) ──
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/CMakeLists.txt")
add_subdirectory(vst3sdk EXCLUDE_FROM_ALL)
set(VST3_SDK_TARGET sdk)
else()
message(WARNING "vst3sdk submodule missing — VST3 host disabled; SF2/SF3/SFZ still build")
set(VST3_SDK_TARGET "")
endif()
# ── 2. FluidSynth + sfizz ──
# Windows: vcpkg toolchain (-DCMAKE_TOOLCHAIN_FILE=.../vcpkg.cmake) cung cap
# headers/libs; pkg_check_modules khong co san tren MSVC.
# Linux/macOS: pkg-config duoc dung (spec goc).
if(WIN32)
find_path(FLUIDSYNTH_INCLUDE_DIR fluidsynth.h)
find_library(FLUIDSYNTH_LIBRARY NAMES fluidsynth libfluidsynth)
if(NOT FLUIDSYNTH_INCLUDE_DIR OR NOT FLUIDSYNTH_LIBRARY)
message(FATAL_ERROR "fluidsynth not found — cai qua vcpkg: vcpkg install fluidsynth")
endif()
# sfizz: header sfizz.hpp + lib sfizz (vcpkg export target sfizz::sfizz neu co)
find_path(SFIZZ_INCLUDE_DIR sfizz.hpp)
find_library(SFIZZ_LIBRARY NAMES sfizz)
if(NOT SFIZZ_INCLUDE_DIR OR NOT SFIZZ_LIBRARY)
message(FATAL_ERROR "sfizz not found — cai qua vcpkg: vcpkg install sfizz")
endif()
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(FLUIDSYNTH REQUIRED fluidsynth)
pkg_check_modules(SFIZZ REQUIRED sfizz)
endif()
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/include
${FLUIDSYNTH_INCLUDE_DIRS}
${SFIZZ_INCLUDE_DIRS}
)
add_executable(daw_vst_bridge
src/main.cpp
src/NativeInstrumentEngine.cpp
src/SharedMemoryIPC.cpp
)
if(VST3_SDK_TARGET)
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET})
endif()
if(WIN32)
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY})
# Required Windows libs
target_link_libraries(daw_vst_bridge PRIVATE winmm)
else()
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
endif()
+42
View File
@@ -0,0 +1,42 @@
// native_bridge/include/INativeInstrument.h
#ifndef I_NATIVE_INSTRUMENT_H
#define I_NATIVE_INSTRUMENT_H
#include <cstdint>
#include <string>
enum class InstrumentType {
VST3,
VST2,
SOUNDFONT_SF2_SF3,
SFZ
};
class INativeInstrument {
public:
virtual ~INativeInstrument() = default;
// Initialize Engine with Sample Rate and Buffer Size
virtual bool init(double sampleRate, uint32_t maxBlockSize) = 0;
// Select Bank and Program Change
virtual void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) = 0;
// Dispatch MIDI Note On / Note Off events
virtual void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) = 0;
virtual void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) = 0;
// MIDI continuous controllers (A12): CC value, program change, 14-bit pitch bend
virtual void controlChange(uint32_t channel, uint32_t cc, uint32_t value) = 0;
virtual void programChange(uint32_t channel, uint32_t program) = 0;
virtual void pitchBend(uint32_t channel, uint32_t bend14) = 0;
// Open/close Native GUI window
virtual bool openGUI(void* parentWindowHandle) = 0;
virtual void closeGUI() = 0;
// Real-time Audio PCM Float32 rendering loop
virtual void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) = 0;
};
#endif // I_NATIVE_INSTRUMENT_H
@@ -0,0 +1,87 @@
// native_bridge/include/NativeInstrumentEngine.h
#ifndef NATIVE_INSTRUMENT_ENGINE_H
#define NATIVE_INSTRUMENT_ENGINE_H
#include "INativeInstrument.h"
#include <sfizz.hpp>
#include <map>
#include <memory>
#include <string>
#include <vector>
// FluidSynth (.sf2 / .sf3)
class FluidSynthInstrument : public INativeInstrument {
public:
FluidSynthInstrument();
~FluidSynthInstrument() override;
bool loadSoundFontFile(const std::string& path, double sampleRate);
bool init(double sampleRate, uint32_t maxBlockSize) override;
void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) override;
void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) override;
void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) override;
void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override;
void programChange(uint32_t channel, uint32_t program) override;
void pitchBend(uint32_t channel, uint32_t bend14) override;
bool openGUI(void* parentWindowHandle) override;
void closeGUI() override;
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
private:
void* settings; // fluid_settings_t*
void* synth; // fluid_synth_t*
int sfontId;
};
// sfizz (.sfz)
class SfizzInstrument : public INativeInstrument {
public:
bool loadSfzFile(const std::string& path, double sampleRate);
bool init(double sampleRate, uint32_t maxBlockSize) override;
void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) override;
void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) override;
void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) override;
void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override;
void programChange(uint32_t channel, uint32_t program) override;
void pitchBend(uint32_t channel, uint32_t bend14) override;
bool openGUI(void* parentWindowHandle) override;
void closeGUI() override;
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
private:
sfizz::Synth sfizzSynth;
};
// Multi-instrument host (A10): one engine instance per MIDI channel, so two
// tracks can play two different soundfonts/VSTs simultaneously. All channels
// render into the same output block (mixed), keyed by evt.channel.
class InstrumentEngineManager {
public:
// Create (or replace) the instrument on `channel` for `path`.
// Returns false if the type is unsupported or the file fails to load.
bool assign(uint32_t channel, InstrumentType type, const std::string& path,
double sampleRate, uint32_t blockSize);
INativeInstrument* get(uint32_t channel);
// Flush every sounding note on every assigned channel.
void allNotesOff();
// Zero L/R then sum each assigned instrument into it.
void renderAll(float* outputL, float* outputR, uint32_t numSamples);
size_t count() const { return channels_.size(); }
private:
static std::unique_ptr<INativeInstrument> create_instrument(InstrumentType type);
std::map<uint32_t, std::unique_ptr<INativeInstrument>> channels_;
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
std::vector<float> scratchL_, scratchR_;
};
#endif // NATIVE_INSTRUMENT_ENGINE_H
+48
View File
@@ -0,0 +1,48 @@
// native_bridge/include/SharedMemoryIPC.h
#pragma once
#include <cstdint>
#define AUDIO_BLOCK_SIZE 256
// WARNING: volatile is NOT a sync primitive. Real impl should use an
// interlocked/index-flag pair or a Win32 event (SetEvent) signalled by the
// writer. This struct follows the spec layout so Rust/JS mapping stays in sync.
struct SharedAudioBufferIPC {
// Synchronization Flags
volatile uint32_t clientReadIndex;
volatile uint32_t bridgeWriteIndex;
// PCM Float32 Audio Buffers
float masterLeft[AUDIO_BLOCK_SIZE];
float masterRight[AUDIO_BLOCK_SIZE];
// Latency probe: bridge stamps every rendered block (QueryPerformanceCounter)
volatile uint64_t blockTimestamp;
// MIDI Event Exchange Queue
struct MidiEventIPC {
uint8_t command; // 0x9 note on / 0x8 note off / 0xB CC / 0xC program / 0xE pitch bend
uint8_t channel;
uint8_t pitch; // note number (0x9/0x8) or CC number (0xB)
uint8_t velocity; // 0-127 (0x9 + vel=0 == note off)
uint8_t data2; // CC value / program number / PB LSB
uint8_t data3; // PB MSB (0xE only)
uint8_t reserved[2]; // explicit padding — keeps C/Rust layout stable
uint32_t sampleOffset;
} midiQueue[64];
volatile uint32_t midiQueueCount;
// Transport / Control (added vs spec: Stop/Play flush, instrument switch)
struct ControlEventIPC {
uint32_t type; // 0 = NONE, 1 = PANIC/ALL_NOTES_OFF, 2 = LOAD_INSTRUMENT,
// 3 = TRANSPORT, 4 = OPEN_GUI
uint32_t arg0; // LOAD: instrument type (InstrumentType);
// TRANSPORT: 0=STOP, 1=PLAY, 2=SET_POSITION
uint32_t arg1; // LOAD: string length (bytes) of path;
// TRANSPORT: playheadSamples (for timeline sync)
uint32_t channel; // LOAD: MIDI channel to assign instrument to (A10)
char arg2[1024]; // LOAD: UTF-8 path
} controlQueue[8];
volatile uint32_t controlQueueCount;
};
+43
View File
@@ -0,0 +1,43 @@
// native_bridge/include/Vst3Instrument.h
// VST3 host instrument (A7). Compiled ALWAYS; the real vst3sdk wiring is
// inside #ifdef HAVE_VST3SDK (set by CMake when the vst3sdk submodule is
// present). Without the SDK the class degrades to a no-op stub so the bridge
// still builds for SF2/SF3/SFZ.
#ifndef VST3_INSTRUMENT_H
#define VST3_INSTRUMENT_H
#include "INativeInstrument.h"
#include <string>
class Vst3Instrument : public INativeInstrument {
public:
Vst3Instrument();
~Vst3Instrument() override;
bool loadPlugin(const std::string& path, double sampleRate);
bool init(double sampleRate, uint32_t maxBlockSize) override;
void selectProgram(uint32_t channel, uint32_t bank, uint32_t program) override;
void noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) override;
void noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) override;
void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override;
void programChange(uint32_t channel, uint32_t program) override;
void pitchBend(uint32_t channel, uint32_t bend14) override;
bool openGUI(void* parentWindowHandle) override;
void closeGUI() override;
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
private:
void* module_; // Steinberg::IPluginFactory* (owned by module)
void* processor_; // Steinberg::Vst::IComponent*
void* controller_; // Steinberg::Vst::IEditController*
void* view_; // Steinberg::IPlugView*
std::string path_;
double sampleRate_;
uint32_t maxBlockSize_;
bool loaded_;
bool guiAttached_;
};
#endif // VST3_INSTRUMENT_H
@@ -0,0 +1,195 @@
// native_bridge/src/NativeInstrumentEngine.cpp
#include "NativeInstrumentEngine.h"
#include <fluidsynth.h>
#include <sfizz.hpp>
#include <cstring>
#include <iostream>
// -----------------------------------------------------------------
// 1. SOUNDFONT ENGINE (.SF2 / .SF3) VIA FLUIDSYNTH C API
// -----------------------------------------------------------------
FluidSynthInstrument::FluidSynthInstrument()
: settings(nullptr), synth(nullptr), sfontId(-1) {}
FluidSynthInstrument::~FluidSynthInstrument() {
if (synth) delete_fluid_synth(synth);
if (settings) delete_fluid_settings(settings);
}
bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sampleRate) {
if (synth) { delete_fluid_synth(synth); synth = nullptr; }
if (settings) { delete_fluid_settings(settings); settings = nullptr; }
settings = new_fluid_settings();
fluid_settings_setnum(settings, "synth.sample-rate", sampleRate);
fluid_settings_setint(settings, "synth.polyphony", 256);
fluid_settings_setint(settings, "synth.verbose", 0);
synth = new_fluid_synth(settings);
if (!synth) return false;
sfontId = fluid_synth_sfload(synth, path.c_str(), 1);
if (sfontId == -1) return false;
// Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano)
for (uint32_t ch = 0; ch < 16; ++ch) {
fluid_synth_program_select(synth, ch, sfontId, 0, 0);
}
return true;
}
bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) {
return synth != nullptr;
}
void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
if (!synth) return;
fluid_synth_bank_select(synth, channel, bank);
fluid_synth_program_change(synth, channel, program);
}
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
if (!synth) return;
int velInt = static_cast<int>(velocity * 127.0f);
fluid_synth_noteon(synth, channel, pitch, velInt);
}
void FluidSynthInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
if (!synth) return;
fluid_synth_noteoff(synth, channel, pitch);
}
void FluidSynthInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
if (!synth) return;
fluid_synth_cc(synth, channel, cc, value);
}
void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
if (!synth) return;
fluid_synth_program_change(synth, channel, program);
}
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
if (!synth) return;
// fluid_synth_pitch_bend takes the raw 14-bit value (center 8192).
fluid_synth_pitch_bend(synth, channel, bend14);
}
bool FluidSynthInstrument::openGUI(void* parentWindowHandle) {
return false; // SoundFont uses Web GUI Manager / Reskinned Knobs
}
void FluidSynthInstrument::closeGUI() {}
void FluidSynthInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
if (!synth) return;
fluid_synth_write_float(synth, numSamples, outputL, 0, 1, outputR, 0, 1);
}
// -----------------------------------------------------------------
// 2. SFZ ENGINE (.SFZ) VIA SFIZZ C++ API
// -----------------------------------------------------------------
bool SfizzInstrument::loadSfzFile(const std::string& path, double sampleRate) {
sfizzSynth.setSampleRate(sampleRate);
return sfizzSynth.loadSfzFile(path);
}
bool SfizzInstrument::init(double sampleRate, uint32_t maxBlockSize) {
sfizzSynth.setSampleRate(sampleRate);
sfizzSynth.setSamplesPerBlock(maxBlockSize);
return true;
}
void SfizzInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {}
void SfizzInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
sfizzSynth.hdNoteOn(sampleOffset, pitch, velocity);
}
void SfizzInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
sfizzSynth.hdNoteOff(sampleOffset, pitch, 0.0f);
}
void SfizzInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
// ponytail: non-delayed cc() is stable across sfizz versions; switch to
// hdCC(cc, value) for sample-accurate CC when the installed sfizz has it.
sfizzSynth.cc(static_cast<int>(cc), static_cast<float>(value));
}
void SfizzInstrument::programChange(uint32_t channel, uint32_t program) {
// TODO(A12): sfizz program-change API differs by version (hdProgramChange
// in >=0.6). Rarely used by SFZ instruments — no-op until verified.
}
void SfizzInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
sfizzSynth.pitchWheel(static_cast<int>(bend14));
}
bool SfizzInstrument::openGUI(void* parentWindowHandle) { return false; }
void SfizzInstrument::closeGUI() {}
void SfizzInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
float* channels[2] = { outputL, outputR };
sfizzSynth.renderBlock(channels, numSamples);
}
// -----------------------------------------------------------------
// 3. MULTI-CHANNEL INSTRUMENT MANAGER (A10)
// -----------------------------------------------------------------
std::unique_ptr<INativeInstrument> InstrumentEngineManager::create_instrument(InstrumentType type) {
switch (type) {
case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique<FluidSynthInstrument>();
case InstrumentType::SFZ: return std::make_unique<SfizzInstrument>();
// ponytail: VST3/VST2 host needs vst3sdk + Steinberg APIs — wired in
// Giai doan 2 (A6-A8); returns nullptr so the bridge degrades gracefully.
case InstrumentType::VST3:
case InstrumentType::VST2:
default: return nullptr;
}
}
bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
const std::string& path, double sampleRate,
uint32_t blockSize) {
if (channel >= 16) return false;
auto inst = create_instrument(type);
if (!inst) return false;
if (!inst->init(sampleRate, blockSize)) return false;
bool loaded = false;
if (type == InstrumentType::SOUNDFONT_SF2_SF3)
loaded = static_cast<FluidSynthInstrument*>(inst.get())->loadSoundFontFile(path, sampleRate);
else if (type == InstrumentType::SFZ)
loaded = static_cast<SfizzInstrument*>(inst.get())->loadSfzFile(path, sampleRate);
if (!loaded) return false;
// Replacing an existing instrument drops its voices with the old engine.
channels_[channel] = std::move(inst);
return true;
}
INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
auto it = channels_.find(channel);
return it == channels_.end() ? nullptr : it->second.get();
}
void InstrumentEngineManager::allNotesOff() {
for (auto& [ch, inst] : channels_) {
for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0);
}
}
void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) {
std::memset(outputL, 0, numSamples * sizeof(float));
std::memset(outputR, 0, numSamples * sizeof(float));
if (channels_.empty()) return;
if (scratchL_.size() < numSamples) {
scratchL_.resize(numSamples);
scratchR_.resize(numSamples);
}
for (auto& [ch, inst] : channels_) {
std::memset(scratchL_.data(), 0, numSamples * sizeof(float));
std::memset(scratchR_.data(), 0, numSamples * sizeof(float));
inst->processAudioBlock(scratchL_.data(), scratchR_.data(), numSamples);
for (uint32_t i = 0; i < numSamples; ++i) {
outputL[i] += scratchL_[i];
outputR[i] += scratchR_[i];
}
}
}
+108
View File
@@ -0,0 +1,108 @@
// native_bridge/src/SharedMemoryIPC.cpp
#include "SharedMemoryIPC.h"
#include <cstring>
#include <string>
// ── Platform shared-memory helpers (used by main.cpp on Windows and by the
// self-check test on POSIX). The DAW (Rust) creates the mapping; the bridge
// opens it. Layout must match the Rust struct (see src-tauri/src/shm.rs).
#ifdef _WIN32
#include <windows.h>
struct ShmHandle {
HANDLE map = nullptr;
void* view = nullptr;
};
ShmHandle* shm_open(const char* name) {
ShmHandle* h = new ShmHandle();
h->map = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, name);
if (!h->map) { delete h; return nullptr; }
h->view = MapViewOfFile(h->map, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
if (!h->view) { CloseHandle(h->map); delete h; return nullptr; }
return h;
}
void shm_close(ShmHandle* h) {
if (!h) return;
if (h->view) UnmapViewOfFile(h->view);
if (h->map) CloseHandle(h->map);
delete h;
}
#else
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
struct ShmHandle {
int fd = -1;
void* view = nullptr;
std::string name;
};
ShmHandle* shm_open(const char* name) {
size_t sz = sizeof(SharedAudioBufferIPC);
int fd = ::shm_open(name, O_CREAT | O_RDWR, 0666);
if (fd < 0) return nullptr;
if (ftruncate(fd, (off_t)sz) != 0) { ::close(fd); return nullptr; }
void* view = mmap(nullptr, sz, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (view == MAP_FAILED) { ::close(fd); return nullptr; }
ShmHandle* h = new ShmHandle();
h->fd = fd;
h->view = view;
h->name = name;
return h;
}
void shm_close(ShmHandle* h) {
if (!h) return;
if (h->view) munmap(h->view, sizeof(SharedAudioBufferIPC));
if (h->fd >= 0) ::close(h->fd);
shm_unlink(h->name.c_str());
delete h;
}
#endif
ShmHandle* shm_open_default() { return shm_open("SonicForge_DAW_IPC"); }
SharedAudioBufferIPC* shm_ptr(ShmHandle* h) {
return h ? static_cast<SharedAudioBufferIPC*>(h->view) : nullptr;
}
bool shm_write_midi(ShmHandle* h, uint8_t cmd, uint8_t channel, uint8_t pitch,
uint8_t velocity, uint32_t sampleOffset, uint8_t data2 = 0, uint8_t data3 = 0) {
SharedAudioBufferIPC* ipc = shm_ptr(h);
if (!ipc) return false;
// Ring overwrite guard: keep at most queue capacity pending events.
if (ipc->midiQueueCount >= 64) return false;
uint32_t i = ipc->midiQueueCount++;
SharedAudioBufferIPC::MidiEventIPC& e = ipc->midiQueue[i];
e.command = cmd;
e.channel = channel;
e.pitch = pitch;
e.velocity = velocity;
e.data2 = data2;
e.data3 = data3;
e.sampleOffset = sampleOffset;
return true;
}
bool shm_write_control(ShmHandle* h, uint32_t type, uint32_t arg0, uint32_t arg1,
uint32_t channel, const char* path) {
SharedAudioBufferIPC* ipc = shm_ptr(h);
if (!ipc) return false;
if (ipc->controlQueueCount >= 8) return false;
uint32_t i = ipc->controlQueueCount++;
SharedAudioBufferIPC::ControlEventIPC& c = ipc->controlQueue[i];
c.type = type;
c.arg0 = arg0;
c.channel = channel;
std::memset(c.arg2, 0, sizeof(c.arg2));
if (path && type == 2 /*LOAD*/) {
c.arg1 = (uint32_t)std::strlen(path); // path length in arg1
if (c.arg1 < sizeof(c.arg2)) std::memcpy(c.arg2, path, c.arg1);
} else {
c.arg1 = arg1; // TRANSPORT playhead / OPEN_GUI hwnd etc.
}
return true;
}
+199
View File
@@ -0,0 +1,199 @@
// native_bridge/src/Vst3Instrument.cpp
// VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/).
//
// Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every
// method is a no-op so the bridge still builds for SF2/SF3/SFZ only.
//
// With the SDK: load the .vst3 module, create the component + edit controller,
// connect them, process MIDI events + audio blocks, and attach the editor
// view to a native HWND (openGUI) for the floating GUI (B9).
#include "Vst3Instrument.h"
#ifdef HAVE_VST3SDK
#include "public.sdk/source/main/pluginfactory.h"
#include "pluginterfaces/base/ibstream.h"
#include "pluginterfaces/vst/ivstaudioprocessor.h"
#include "pluginterfaces/vst/ivsteditcontroller.h"
#include "pluginterfaces/vst/ivstmidicontrollers.h"
#include "pluginterfaces/gui/iplugview.h"
#include "public.sdk/source/common/pluginview.h"
#include <cstring>
#include <vector>
#endif
// ---------------------------------------------------------------------------
// With-SDK implementation
// ---------------------------------------------------------------------------
#ifdef HAVE_VST3SDK
#include "public.sdk/source/main/module.h"
namespace {
using Steinberg::Vst::IComponent;
using Steinberg::Vst::IEditController;
using Steinberg::Vst::IAudioProcessor;
using Steinberg::Vst::IComponentHandler;
using Steinberg::Vst::IParameterChanges;
using Steinberg::Vst::IEventList;
using Steinberg::Vst::Event;
using Steinberg::Vst::NoteOnEvent;
using Steinberg::Vst::NoteOffEvent;
using Steinberg::Vst::DataEvent;
using Steinberg::Vst::kMidiCC;
using Steinberg::Vst::kMidiPitchBend;
using Steinberg::Vst::kMidiProgramChange;
using Steinberg::IPlugView;
using Steinberg::tresult;
using Steinberg::kResultOk;
using Steinberg::kResultFalse;
// Minimal IComponentHandler so the plugin can inform the host of param edits.
class HostComponentHandler : public IComponentHandler {
public:
Steinberg::tresult queryInterface(const Steinberg::TUID&, void** v) override {
*v = nullptr;
return Steinberg::kNoInterface;
}
Steinberg::uint32 addRef() override { return 1; }
Steinberg::uint32 release() override { return 1; }
Steinberg::tresult beginEdit(Steinberg::Vst::ParamID) override { return Steinberg::kResultOk; }
Steinberg::tresult performEdit(Steinberg::Vst::ParamID, Steinberg::Vst::ParamValue) override {
return Steinberg::kResultOk;
}
Steinberg::tresult endEdit(Steinberg::Vst::ParamID) override { return Steinberg::kResultOk; }
Steinberg::tresult restartComponent(Steinberg::int32) override { return Steinberg::kResultOk; }
};
// Bundle both components in one factory entry so module.load() gives us the
// component; controller is created via IComponent::createController.
} // namespace
#endif
Vst3Instrument::Vst3Instrument()
: module_(nullptr),
processor_(nullptr),
controller_(nullptr),
view_(nullptr),
sampleRate_(44100.0),
maxBlockSize_(256),
loaded_(false),
guiAttached_(false) {}
Vst3Instrument::~Vst3Instrument() { closeGUI(); }
bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
#ifndef HAVE_VST3SDK
(void)path; (void)sampleRate;
return false; // vst3sdk submodule missing — VST3 disabled
#else
if (loaded_) return true;
// ponytail: vst3sdk module loading is platform-specific
// (Module::create on Windows .vst3 bundle / macOS .vst3 framework). Keep
// the classic pluginfactory-based load for Windows bundles:
// void* handle = Steinberg::Vst::Module::create(...)
// and use module->getFactory().createInstance<...>(cid).
// Verified on Windows in A6 (G3 Vital.vst3). Until then VST3 returns false
// so SF2/SFZ keep working.
(void)path; (void)sampleRate;
return false;
#endif
}
bool Vst3Instrument::init(double sampleRate, uint32_t maxBlockSize) {
sampleRate_ = sampleRate;
maxBlockSize_ = maxBlockSize;
return loaded_;
}
void Vst3Instrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
(void)channel; (void)bank; (void)program;
// ponytail: needs IEditController::setParamNormalized on program list params
}
void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
(void)channel; (void)pitch; (void)velocity; (void)sampleOffset;
#ifndef HAVE_VST3SDK
return;
#else
if (!processor_) return;
// TODO(A6): queue NoteOnEvent into the per-block event list; see
// processAudioBlock. Wiring requires IAudioProcessor::process with
// ProcessData — implemented together with loadPlugin on Windows.
#endif
}
void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
(void)channel; (void)pitch; (void)sampleOffset;
#ifndef HAVE_VST3SDK
return;
#else
if (!processor_) return;
// TODO(A6): queue NoteOffEvent (see noteOn)
#endif
}
void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
(void)channel; (void)cc; (void)value;
#ifndef HAVE_VST3SDK
return;
#else
if (!controller_) return;
// TODO(A6): controller_->setParamNormalized(kMidiCC | cc, value/127.0)
#endif
}
void Vst3Instrument::programChange(uint32_t channel, uint32_t program) {
(void)channel; (void)program;
#ifndef HAVE_VST3SDK
return;
#else
if (!controller_) return;
// TODO(A6): setParamNormalized(kMidiProgramChange | program, ...)
#endif
}
void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
(void)channel; (void)bend14;
#ifndef HAVE_VST3SDK
return;
#else
if (!controller_) return;
// TODO(A6): setParamNormalized(kMidiPitchBend, bend14/16383.0)
#endif
}
bool Vst3Instrument::openGUI(void* parentWindowHandle) {
#ifndef HAVE_VST3SDK
(void)parentWindowHandle;
return false;
#else
if (!processor_ || !controller_ || !parentWindowHandle) return false;
if (guiAttached_) return true;
// TODO(A6): FUnknownPtr<IPlugView> view(controller_);
// view->setFrame(parentWindowHandle); view->attached(...); view_ = view;
return false;
#endif
}
void Vst3Instrument::closeGUI() {
#ifndef HAVE_VST3SDK
return;
#else
if (view_ && guiAttached_) {
// view_->removed(); view_->setFrame(nullptr);
}
view_ = nullptr;
guiAttached_ = false;
#endif
}
void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
(void)outputL; (void)outputR; (void)numSamples;
#ifndef HAVE_VST3SDK
return;
#else
// TODO(A6): ProcessData with 2 output buffers + event list; called by
// InstrumentEngineManager::renderAll via processAudioBlock.
#endif
}
+204
View File
@@ -0,0 +1,204 @@
// native_bridge/src/main.cpp
// Entry point of daw_vst_bridge.exe — opens shared memory created by the DAW
// (Rust/Tauri side), runs the real-time MIDI->audio loop, watches the parent.
#include "INativeInstrument.h"
#include "SharedMemoryIPC.h"
#include "NativeInstrumentEngine.h"
#ifdef _WIN32
#include <windows.h>
#include <mmsystem.h>
#include <process.h>
#else
#include <unistd.h>
#include <cstdlib>
#include <thread>
#include <chrono>
#endif
#include <algorithm>
#include <cstring>
#include <iostream>
#include <memory>
#include <string>
// --- platform helpers -------------------------------------------------------
static bool parent_alive(uint32_t pid) {
#ifdef _WIN32
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!h) return false;
CloseHandle(h);
return true;
#else
return pid == 0 || (kill(pid, 0) == 0);
#endif
}
static void sleep_ms(uint32_t ms) {
#ifdef _WIN32
Sleep(ms);
#else
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
#endif
}
int main(int argc, char* argv[]) {
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
// 1. Shared memory name: argv --shm <name> | env SF_SHM_NAME | default
std::string shmName = "SonicForge_DAW_IPC";
for (int i = 1; i + 1 < argc; ++i) {
if (std::strcmp(argv[i], "--shm") == 0) shmName = argv[i + 1];
}
if (const char* e = std::getenv("SF_SHM_NAME")) shmName = e;
// Parent watchdog PID (set by Tauri sidecar spawner)
uint32_t parentPid = 0;
if (const char* e = std::getenv("SF_PARENT_PID")) parentPid = (uint32_t)std::atoi(e);
#ifdef _WIN32
// Real-time-ish timing: 1ms scheduler resolution
timeBeginPeriod(1);
HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName.c_str());
if (!hMapFile) {
std::cerr << "[NativeBridge] Failed to open Shared Memory mapping: " << shmName << std::endl;
return 1;
}
auto* shmIPC = (SharedAudioBufferIPC*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
if (!shmIPC) { CloseHandle(hMapFile); return 1; }
#else
(void)shmName; // POSIX shm mapping (shm_open) added when porting off Windows
auto* shmIPC = (SharedAudioBufferIPC*)std::calloc(1, sizeof(SharedAudioBufferIPC));
if (!shmIPC) return 1;
#endif
InstrumentEngineManager instruments;
// B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE).
// Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) — SF_BLOCK_SIZE
// is accepted but must match, otherwise warned and ignored.
double sampleRate = 44100.0;
if (const char* e = std::getenv("SF_SAMPLE_RATE")) {
double sr = (double)std::atoi(e);
if (sr > 0) sampleRate = sr;
}
if (const char* e = std::getenv("SF_BLOCK_SIZE")) {
uint32_t b = (uint32_t)std::atoi(e);
if (b != AUDIO_BLOCK_SIZE)
std::cerr << "[NativeBridge] SHM block size fixed at " << AUDIO_BLOCK_SIZE
<< " (SF_BLOCK_SIZE=" << b << " ignored)" << std::endl;
}
const uint32_t block = AUDIO_BLOCK_SIZE;
uint64_t playheadSamples = 0;
auto dispatch = [&](const SharedAudioBufferIPC::MidiEventIPC& evt) {
auto* inst = instruments.get(evt.channel);
if (!inst) return; // channel chưa gán instrument → silent (A10)
switch (evt.command) {
case 0x9:
if (evt.velocity > 0)
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, evt.sampleOffset);
else
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
break;
case 0x8:
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
break;
case 0xB: // CC: controller number in pitch, value in data2
inst->controlChange(evt.channel, evt.pitch, evt.data2);
break;
case 0xC: // program change: program in data2
inst->programChange(evt.channel, evt.data2);
break;
case 0xE: // 14-bit pitch bend: data2 = LSB, data3 = MSB
inst->pitchBend(evt.channel, evt.data2 | (uint32_t(evt.data3) << 7));
break;
default: break;
}
};
// Render only [from, to) of the block — used by sample-accurate splitting.
auto renderSegment = [&](uint32_t from, uint32_t to) {
if (to <= from) return;
instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from);
};
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
while (true) {
// A. Control events — non-rt safe, drained first
for (uint32_t i = 0; i < shmIPC->controlQueueCount; ++i) {
const auto& c = shmIPC->controlQueue[i];
if (c.type == 2) { // LOAD_INSTRUMENT (A10: assign per MIDI channel)
// Robust path length: do not trust arg1 (Rust passes 0 for LOAD).
size_t plen = 0;
while (plen < sizeof(c.arg2) && c.arg2[plen]) ++plen;
std::string path(c.arg2, plen);
InstrumentType t = (InstrumentType)c.arg0;
uint32_t ch = c.channel & 0xF;
if (instruments.assign(ch, t, path, sampleRate, block)) {
std::cout << "[NativeBridge] instrument loaded ch=" << ch
<< " type=" << (int)c.arg0 << " " << path << std::endl;
} else {
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
<< " type=" << (int)c.arg0 << " " << path << std::endl;
}
} else if (c.type == 1) { // PANIC
instruments.allNotesOff();
std::cout << "[NativeBridge] PANIC — all notes off" << std::endl;
} else if (c.type == 3) { // TRANSPORT (A13)
if (c.arg0 == 0) { // STOP → flush every note immediately
instruments.allNotesOff();
std::cout << "[NativeBridge] transport STOP — all notes off" << std::endl;
} else if (c.arg0 == 1) { // PLAY
playheadSamples = c.arg1;
std::cout << "[NativeBridge] transport PLAY playhead=" << playheadSamples << std::endl;
} else if (c.arg0 == 2) { // SET_POSITION (seek while stopped)
playheadSamples = c.arg1;
}
}
}
shmIPC->controlQueueCount = 0;
// B. Snapshot queued MIDI events (bounded copy, queue reset immediately)
uint32_t nEvents = shmIPC->midiQueueCount > 64 ? 64 : shmIPC->midiQueueCount;
SharedAudioBufferIPC::MidiEventIPC evts[64];
for (uint32_t i = 0; i < nEvents; ++i) evts[i] = shmIPC->midiQueue[i];
shmIPC->midiQueueCount = 0;
// A11 sample-accurate: sort by sampleOffset, dispatch at each boundary,
// render the sub-block before the boundary. Events with offset 0 (live
// keyboard, current JS) are dispatched first and shape the whole block.
std::stable_sort(evts, evts + nEvents,
[](const SharedAudioBufferIPC::MidiEventIPC& a,
const SharedAudioBufferIPC::MidiEventIPC& b) {
return a.sampleOffset < b.sampleOffset;
});
uint32_t cursor = 0;
uint32_t ei = 0;
while (ei < nEvents && evts[ei].sampleOffset <= cursor) { dispatch(evts[ei]); ++ei; }
for (; ei < nEvents; ++ei) {
uint32_t off = evts[ei].sampleOffset < block ? evts[ei].sampleOffset : block;
if (off > cursor) { renderSegment(cursor, off); cursor = off; }
dispatch(evts[ei]);
}
if (cursor < block) renderSegment(cursor, block);
shmIPC->bridgeWriteIndex++;
// D. Parent died / window closed -> exit (no orphan process)
if (parentPid != 0 && !parent_alive(parentPid)) {
std::cout << "[NativeBridge] parent gone — exiting." << std::endl;
break;
}
// E. Sleep briefly for the next frame tick (block@44100 ≈ 5.8ms)
sleep_ms(1);
}
#ifdef _WIN32
UnmapViewOfFile(shmIPC);
CloseHandle(hMapFile);
timeEndPeriod(1);
#else
std::free(shmIPC);
#endif
return 0;
}
+67
View File
@@ -0,0 +1,67 @@
// native_bridge/tests/shm_selfcheck.cpp
// Self-check for SharedMemoryIPC helpers (assert-based, no framework).
// POSIX path runs on Linux/macOS; Win32 path guarded (runs on Windows build).
#include "../include/SharedMemoryIPC.h"
#include "../src/SharedMemoryIPC.cpp"
#include <cassert>
#include <cstdio>
#include <cstring>
int main() {
// Use a test-only name to avoid clashing with a live DAW bridge.
ShmHandle* h = shm_open("SonicForge_SelfCheck_IPC");
assert(h != nullptr);
SharedAudioBufferIPC* ipc = shm_ptr(h);
assert(ipc != nullptr);
// Reset state (POSIX shm may survive an aborted previous run).
std::memset(ipc, 0, sizeof(SharedAudioBufferIPC));
// 1. MIDI event write + read back (A12: data2/data3 for CC/program/pitch bend)
assert(shm_write_midi(h, 0x9, 2, 60, 100, 37));
assert(ipc->midiQueueCount == 1);
assert(ipc->midiQueue[0].command == 0x9);
assert(ipc->midiQueue[0].channel == 2);
assert(ipc->midiQueue[0].pitch == 60);
assert(ipc->midiQueue[0].velocity == 100);
assert(ipc->midiQueue[0].sampleOffset == 37);
assert(ipc->midiQueue[0].data2 == 0 && ipc->midiQueue[0].data3 == 0);
assert(shm_write_midi(h, 0xB, 3, 64, 0, 0, 127)); // CC64 sustain = 127
assert(shm_write_midi(h, 0xE, 3, 0, 0, 0, 0x00, 0x40)); // pitch bend MSB
assert(ipc->midiQueue[1].data2 == 127);
assert(ipc->midiQueue[2].data3 == 0x40);
assert(ipc->midiQueueCount == 3);
// 2. Control event with path + channel (A10)
assert(shm_write_control(h, 2, 1 /*SOUNDFONT_SF2_SF3*/, 0, 4, "C:\\sf\\SGM.sf2"));
assert(ipc->controlQueueCount == 1);
assert(ipc->controlQueue[0].type == 2);
assert(ipc->controlQueue[0].arg0 == 1);
assert(ipc->controlQueue[0].channel == 4);
assert(std::strcmp(ipc->controlQueue[0].arg2, "C:\\sf\\SGM.sf2") == 0);
// TRANSPORT (A13): STOP with playhead arg
assert(shm_write_control(h, 3, 0, 44100, 0, ""));
assert(ipc->controlQueue[1].type == 3 && ipc->controlQueue[1].arg0 == 0
&& ipc->controlQueue[1].arg1 == 44100);
// 3. Queue capacity guard
for (int i = 0; i < 70; ++i) shm_write_midi(h, 0x8, 0, 40, 0, 0);
assert(ipc->midiQueueCount == 64); // capped, not overflowed
// 4. Audio block round-trip
for (uint32_t i = 0; i < AUDIO_BLOCK_SIZE; ++i) ipc->masterLeft[i] = (float)i;
ipc->bridgeWriteIndex++;
ipc->blockTimestamp = 123456;
assert(ipc->masterLeft[255] == 255.0f);
assert(ipc->bridgeWriteIndex == 1);
assert(ipc->blockTimestamp == 123456);
// 5. Layout stability — MUST match Rust src-tauri/src/shm.rs
assert(sizeof(SharedAudioBufferIPC::MidiEventIPC) == 12);
assert(sizeof(SharedAudioBufferIPC::ControlEventIPC) == 1040);
std::printf("SHM self-check OK (sizeof struct = %zu bytes)\n", sizeof(SharedAudioBufferIPC));
shm_close(h);
return 0;
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "daw-vst-bridge",
"version-string": "1.0.0",
"dependencies": [
"fluidsynth",
"sfizz",
"pkgconf"
]
}
+5
View File
@@ -17,6 +17,11 @@ tauri-plugin-shell = "2"
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
# Shared memory (SonicForge_DAW_IPC) for the Native Host Bridge
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_System_MemoryManagement",
] }
[profile.release] [profile.release]
strip = true strip = true
+305 -1
View File
@@ -10,14 +10,25 @@
// "daw_engine/" — đúng layout lib.rs chờ. // "daw_engine/" — đúng layout lib.rs chờ.
// Ngoài ra lib.rs còn dò THÊM các vị trí fallback (legacy/portable/dev) // Ngoài ra lib.rs còn dò THÊM các vị trí fallback (legacy/portable/dev)
// và ghi đầy đủ diagnostic vào %APPDATA%/SonicForgeDAW/logs/spawn.log. // và ghi đầy đủ diagnostic vào %APPDATA%/SonicForgeDAW/logs/spawn.log.
use tauri::Manager; use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_dialog::{DialogExt, FilePath}; use tauri_plugin_dialog::{DialogExt, FilePath};
use tauri_plugin_shell::process::CommandChild; use tauri_plugin_shell::process::CommandChild;
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Mutex; use std::sync::Mutex;
mod shm;
use shm::{Shm, ShmState};
struct EngineProcess(Mutex<Option<CommandChild>>); struct EngineProcess(Mutex<Option<CommandChild>>);
struct BridgeProcess(Mutex<Option<CommandChild>>);
/// Audio frame pushed to the WebView (bridge SHM -> `bridge-audio` event).
#[derive(Clone, serde::Serialize)]
struct AudioFrame {
l: Vec<f32>,
r: Vec<f32>,
}
/// Các vị trí có thể chứa daw_engine, theo thứ tự ưu tiên. /// Các vị trí có thể chứa daw_engine, theo thứ tự ưu tiên.
fn engine_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> { fn engine_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
@@ -46,6 +57,78 @@ fn engine_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
] ]
} }
/// Các vị trí có thể chứa daw_vst_bridge (Native Host Bridge sidecar).
fn bridge_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
let exe_name = if cfg!(windows) { "daw_vst_bridge.exe" } else { "daw_vst_bridge" };
let triple_name = if cfg!(windows) { "daw_vst_bridge-x86_64-pc-windows-msvc.exe" } else { exe_name };
vec![
(res_dir.join("binaries").join(exe_name), "resource_dir/binaries (bundle)".into()),
(res_dir.join(exe_name), "resource_dir (bundle root)".into()),
(res_dir.join("binaries").join(triple_name), "resource_dir/binaries (triple name)".into()),
(exe_dir.join(exe_name), "exe_dir (portable)".into()),
(exe_dir.join("..").join("resources").join("binaries").join(exe_name), "exe_dir/../resources/binaries (dev)".into()),
(exe_dir.join("..").join("resources").join(exe_name), "exe_dir/../resources (dev)".into()),
]
}
/// Spawn daw_vst_bridge.exe sidecar (B3/B8) with SHM + watchdog + audio env.
/// Appends candidate diagnostics to spawn.log; manages `BridgeProcess`.
/// Returns true when the child started.
fn spawn_bridge(
app: &AppHandle,
res_dir: &Path,
exe_dir: &Path,
log_line: &mut String,
spawn_log_path: &Path,
) -> bool {
let candidates = bridge_candidates(res_dir, exe_dir);
let mut found: Option<(PathBuf, String)> = None;
for (path, label) in &candidates {
let exists = path.exists();
log_line.push_str(&format!(" [bridge {label}] {} exists={}\n", path.display(), exists));
if found.is_none() && exists {
found = Some((path.clone(), label.clone()));
}
}
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(spawn_log_path)
{
use std::io::Write;
let _ = f.write_all(log_line.as_bytes());
}
match found {
Some((bridge_exe, label)) => {
match app
.shell()
.command(&bridge_exe)
.env("SF_SHM_NAME", shm::SHM_NAME)
.env("SF_PARENT_PID", std::process::id().to_string())
.env("SF_SAMPLE_RATE", "44100") // B8: JS resampler handles mismatches (C5)
.env("SF_BLOCK_SIZE", "256")
.spawn()
{
Ok((_rx, child)) => {
app.manage(BridgeProcess(Mutex::new(Some(child))));
println!("Native Host Bridge started ({label}): {}", bridge_exe.display());
true
}
Err(e) => {
println!("Failed to spawn daw_vst_bridge {bridge_exe:?}: {e}");
app.manage(BridgeProcess(Mutex::new(None)));
false
}
}
}
None => {
println!("daw_vst_bridge binary not found — app will use WASM fallback");
app.manage(BridgeProcess(Mutex::new(None)));
false
}
}
}
fn list_dir_snippet(dir: &Path) -> String { fn list_dir_snippet(dir: &Path) -> String {
let mut s = String::new(); let mut s = String::new();
match std::fs::read_dir(dir) { match std::fs::read_dir(dir) {
@@ -73,6 +156,13 @@ pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![
push_midi_event,
load_native_instrument,
open_vst_gui,
bridge_status,
transport_control
])
.setup(|app| { .setup(|app| {
let res_dir = app let res_dir = app
.path() .path()
@@ -157,6 +247,91 @@ pub fn run() {
} }
} }
// ── Native Host Bridge (daw_vst_bridge.exe): SHM + sidecar spawn ──
let shm_created = Shm::create();
println!(
"SharedMemory {}: {}",
shm::SHM_NAME,
if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" }
);
app.manage(ShmState(Mutex::new(shm_created)));
// Append bridge diagnostics to spawn.log and spawn the sidecar (B3).
let app_handle = app.handle().clone();
let _ = spawn_bridge(&app_handle, &res_dir, &exe_dir, &mut log_line, &spawn_log_path);
// ── Audio pump: bridge SHM -> WebView `bridge-audio` events ──
// B10 health: if bridgeWriteIndex stalls for 3s the bridge is dead —
// respawn once, then emit `bridge-down` so the UI falls back to WASM.
let pump_handle = app.handle().clone();
std::thread::spawn(move || {
let mut last_index: u32 = 0;
let mut last_change = std::time::Instant::now();
let mut down_emitted = false;
let mut restart_attempts = 0u32;
loop {
let state = pump_handle.state::<ShmState>();
let guard = match state.0.lock() {
Ok(g) => g,
Err(_) => {
std::thread::sleep(std::time::Duration::from_millis(10));
continue;
}
};
let advanced = guard.as_ref().map(|shm| {
let idx = shm.write_index();
if idx != last_index {
last_index = idx;
true
} else {
false
}
});
if advanced == Some(true) {
last_change = std::time::Instant::now();
down_emitted = false;
let (l, r) = guard.as_ref().map(|s| s.read_audio()).unwrap_or(([0f32; shm::AUDIO_BLOCK_SIZE], [0f32; shm::AUDIO_BLOCK_SIZE]));
let _ = pump_handle.emit(
"bridge-audio",
AudioFrame { l: l.to_vec(), r: r.to_vec() },
);
} else if last_change.elapsed() >= std::time::Duration::from_secs(3) && !down_emitted {
down_emitted = true;
if restart_attempts == 0 {
restart_attempts += 1;
let res_dir = pump_handle.path().resource_dir().unwrap_or_default();
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.to_path_buf()))
.unwrap_or_default();
let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
let log_path = std::path::Path::new(&log_dir)
.join("SonicForgeDAW").join("logs").join("spawn.log");
let mut line = String::from("[tauri] bridge stalled 3s — restart attempt #1\n");
if spawn_bridge(&pump_handle, &res_dir, &exe_dir, &mut line, &log_path) {
// count restarts into bridge.log (same file as stdout redirect)
let bridge_log = std::path::Path::new(&log_dir)
.join("SonicForgeDAW").join("logs").join("bridge.log");
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true).append(true).open(&bridge_log)
{
use std::io::Write;
let _ = f.write_all(b"[tauri] bridge restarted (attempt 1)\n");
}
last_change = std::time::Instant::now();
down_emitted = false;
} else {
let _ = pump_handle.emit("bridge-down", ());
}
} else {
let _ = pump_handle.emit("bridge-down", ());
}
}
drop(guard);
std::thread::sleep(std::time::Duration::from_millis(5));
}
});
// ── Native folder picker bridge (folder picker cho Plugin Manager) ── // ── Native folder picker bridge (folder picker cho Plugin Manager) ──
// UI chay tren http://127.0.0.1:8000 (engine) — KHONG co __TAURI__ // UI chay tren http://127.0.0.1:8000 (engine) — KHONG co __TAURI__
// (WebView2 cung khong ho tro window.prompt) → engine goi qua file // (WebView2 cung khong ho tro window.prompt) → engine goi qua file
@@ -226,8 +401,137 @@ pub fn run() {
let _ = child.kill(); let _ = child.kill();
println!("daw_engine sidecar terminated."); println!("daw_engine sidecar terminated.");
} }
// Terminate Native Host Bridge khi DAW window dong
let bridge_child = window
.state::<BridgeProcess>()
.0
.lock()
.ok()
.and_then(|mut lock| lock.take());
if let Some(child) = bridge_child {
let _ = child.kill();
println!("daw_vst_bridge sidecar terminated.");
}
} }
}) })
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }
// ── Native Host Bridge Tauri commands ──────────────────────────────────────
#[tauri::command]
fn push_midi_event(
app: AppHandle,
command: u8,
channel: u8,
pitch: u8,
velocity: u8,
data2: u8,
data3: u8,
sample_offset: u32,
) -> Result<(), String> {
let state = app.state::<ShmState>();
let guard = state.0.lock().map_err(|e| e.to_string())?;
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
shm.push_midi(command, channel, pitch, velocity, data2, data3, sample_offset)
.then_some(())
.ok_or("midi queue full")
}
#[tauri::command]
fn load_native_instrument(app: AppHandle, path: String, instrument_type: u8, channel: u8) -> Result<(), String> {
let state = app.state::<ShmState>();
let guard = state.0.lock().map_err(|e| e.to_string())?;
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
shm.push_control(2, instrument_type as u32, 0, channel as u32, &path)
.then_some(())
.ok_or("control queue full")
}
#[tauri::command]
fn open_vst_gui(app: AppHandle, plugin_id: String) -> Result<(), String> {
// B9: child WebviewWindow acts as the VST GUI surface. The bridge receives
// its HWND (control type=4, arg1) so the VST3 editor can be attached (A7).
let url = tauri::WebviewUrl::External(
tauri::Url::parse("data:text/html,<h2>VST GUI</h2><p>Editor attaches from daw_vst_bridge (A7).</p>")
.map_err(|e| e.to_string())?,
);
let win = tauri::WebviewWindowBuilder::new(&app, format!("vst-{}", plugin_id), url)
.title(format!("VST — {}", plugin_id))
.inner_size(800.0, 600.0)
.build()
.map_err(|e| e.to_string())?;
let state = app.state::<ShmState>();
let guard = state.0.lock().map_err(|e| e.to_string())?;
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
// ponytail: HWND truncated to u32 (HWNDs fit in practice); window-close →
// close_gui wiring lands together with A7 on Windows.
#[cfg(windows)]
let hwnd = {
use tauri::raw_window_handle::{HasWindowHandle, RawWindowHandle};
match win.window_handle().map(|h| h.as_raw()) {
Ok(RawWindowHandle::Win32(w)) => w.hwnd as usize as u32,
_ => 0u32,
}
};
#[cfg(not(windows))]
let hwnd = 0u32;
shm.push_control(4, 0, hwnd, 0, &plugin_id)
.then_some(())
.ok_or("control queue full")
}
#[derive(serde::Serialize)]
struct BridgeStatus {
connected: bool,
shm_name: &'static str,
write_index: u32,
block_timestamp: u64,
shm_size_bytes: usize,
sample_rate: u32,
block_size: u32,
}
#[tauri::command]
fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
let state = app.state::<ShmState>();
let guard = state.0.lock().map_err(|e| e.to_string())?;
Ok(match guard.as_ref() {
Some(shm) => BridgeStatus {
connected: true,
shm_name: shm::SHM_NAME,
write_index: shm.write_index(),
block_timestamp: shm.block_timestamp(),
shm_size_bytes: std::mem::size_of::<shm::SharedAudioBufferIPC>(),
sample_rate: 44100,
block_size: shm::AUDIO_BLOCK_SIZE as u32,
},
None => BridgeStatus {
connected: false,
shm_name: shm::SHM_NAME,
write_index: 0,
block_timestamp: 0,
shm_size_bytes: 0,
sample_rate: 0,
block_size: 0,
},
})
}
#[tauri::command]
fn transport_control(app: AppHandle, kind: String, playhead: Option<u32>) -> Result<(), String> {
let state = app.state::<ShmState>();
let guard = state.0.lock().map_err(|e| e.to_string())?;
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
// arg0: 0=STOP (flush), 1=PLAY, 2=SET_POSITION (seek, no flush)
let (arg0, arg1) = match kind.as_str() {
"play" => (1, playhead.unwrap_or(0)),
"set_position" => (2, playhead.unwrap_or(0)),
_ => (0, playhead.unwrap_or(0)), // stop / panic
};
shm.push_control(3, arg0, arg1, 0, "")
.then_some(())
.ok_or("control queue full")
}
+166
View File
@@ -0,0 +1,166 @@
// src-tauri/src/shm.rs
// Shared memory `SonicForge_DAW_IPC` — created by the DAW, opened by the
// C++ bridge (native_bridge). Layout MUST match native_bridge/include/
// SharedMemoryIPC.h (verified by native_bridge/tests/shm_selfcheck.cpp).
#![cfg_attr(not(windows), allow(dead_code))]
use std::ffi::c_void;
use std::ptr;
use std::sync::Mutex;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
use windows_sys::Win32::System::MemoryManagement::{
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE,
};
pub const SHM_NAME: &str = "SonicForge_DAW_IPC";
pub const AUDIO_BLOCK_SIZE: usize = 256;
pub const MIDI_QUEUE_CAP: usize = 64;
pub const CONTROL_QUEUE_CAP: usize = 8;
pub const CONTROL_PATH_MAX: usize = 1024;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct MidiEventIPC {
pub command: u8,
pub channel: u8,
pub pitch: u8,
pub velocity: u8,
pub data2: u8, // CC value / program / PB LSB
pub data3: u8, // PB MSB (0xE only)
pub reserved: [u8; 2],
pub sample_offset: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ControlEventIPC {
pub ctype: u32,
pub arg0: u32,
pub arg1: u32,
pub channel: u32, // LOAD: MIDI channel to assign (A10)
pub arg2: [u8; CONTROL_PATH_MAX],
}
#[repr(C)]
pub struct SharedAudioBufferIPC {
pub client_read_index: u32,
pub bridge_write_index: u32,
pub master_left: [f32; AUDIO_BLOCK_SIZE],
pub master_right: [f32; AUDIO_BLOCK_SIZE],
pub block_timestamp: u64,
pub midi_queue: [MidiEventIPC; MIDI_QUEUE_CAP],
pub midi_queue_count: u32,
pub control_queue: [ControlEventIPC; CONTROL_QUEUE_CAP],
pub control_queue_count: u32,
}
pub struct Shm {
handle: HANDLE,
view: *mut SharedAudioBufferIPC,
}
unsafe impl Send for Shm {}
impl Shm {
/// Create (or open) the mapping. Returns None on non-Windows (dev fallback).
pub fn create() -> Option<Self> {
if !cfg!(windows) {
return None;
}
let name_wide: Vec<u16> = SHM_NAME.encode_utf16().chain(std::iter::once(0)).collect();
let size = std::mem::size_of::<SharedAudioBufferIPC>() as u64;
let handle = unsafe {
CreateFileMappingW(
HANDLE::default(),
ptr::null(),
PAGE_READWRITE,
(size >> 32) as u32,
size as u32,
name_wide.as_ptr(),
)
};
if handle == HANDLE::default() {
return None;
}
let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) };
if view.is_null() {
unsafe { CloseHandle(handle) };
return None;
}
Some(Shm {
handle,
view: view as *mut SharedAudioBufferIPC,
})
}
fn ipc(&self) -> &mut SharedAudioBufferIPC {
unsafe { &mut *self.view }
}
pub fn push_midi(&self, cmd: u8, channel: u8, pitch: u8, velocity: u8, data2: u8, data3: u8, sample_offset: u32) -> bool {
let ipc = self.ipc();
if ipc.midi_queue_count as usize >= MIDI_QUEUE_CAP {
return false;
}
let i = ipc.midi_queue_count as usize;
ipc.midi_queue[i] = MidiEventIPC {
command: cmd,
channel,
pitch,
velocity,
data2,
data3,
reserved: [0u8; 2],
sample_offset,
};
ipc.midi_queue_count += 1;
true
}
pub fn push_control(&self, ctype: u32, arg0: u32, arg1: u32, channel: u32, path: &str) -> bool {
let ipc = self.ipc();
if ipc.control_queue_count as usize >= CONTROL_QUEUE_CAP {
return false;
}
let mut ev = ControlEventIPC {
ctype,
arg0,
arg1,
channel,
arg2: [0u8; CONTROL_PATH_MAX],
};
let bytes = path.as_bytes();
let n = bytes.len().min(CONTROL_PATH_MAX - 1);
ev.arg2[..n].copy_from_slice(&bytes[..n]);
// LOAD relies on C++ side strnlen() — arg1 is free for TRANSPORT playhead.
let i = ipc.control_queue_count as usize;
ipc.control_queue[i] = ev;
ipc.control_queue_count += 1;
true
}
pub fn read_audio(&self) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) {
let ipc = self.ipc();
(ipc.master_left, ipc.master_right)
}
pub fn write_index(&self) -> u32 {
self.ipc().bridge_write_index
}
pub fn block_timestamp(&self) -> u64 {
self.ipc().block_timestamp
}
}
impl Drop for Shm {
fn drop(&mut self) {
if !self.view.is_null() {
unsafe { UnmapViewOfFile(self.view as *const c_void) };
}
unsafe { CloseHandle(self.handle) };
}
}
/// Process-wide shared-memory state for Tauri.
pub struct ShmState(pub Mutex<Option<Shm>>);
+1 -1
View File
@@ -35,7 +35,7 @@
"resources": { "resources": {
"resources/daw_engine": "daw_engine/" "resources/daw_engine": "daw_engine/"
}, },
"externalBin": [], "externalBin": ["binaries/daw_vst_bridge"],
"windows": { "windows": {
"nsis": { "nsis": {
"installerHooks": "hooks.nsh" "installerHooks": "hooks.nsh"
+19
View File
@@ -14,6 +14,11 @@ import re
import sys import sys
REQUIRED = ["app/static", "app/templates"] REQUIRED = ["app/static", "app/templates"]
# F4: bridge sidecar phai co truoc khi tauri build (externalBin). Kiem tra tai
# day de build fail SOM nhat (build_windows.ps1 goi verify nay truoc tauri build).
BRIDGE_SIDECAR = os.path.join(
"src-tauri", "binaries", "daw_vst_bridge-x86_64-pc-windows-msvc.exe"
)
def find_toc(build_dir: str) -> str: def find_toc(build_dir: str) -> str:
@@ -31,6 +36,7 @@ def find_toc(build_dir: str) -> str:
def main() -> int: def main() -> int:
check_bridge = "--check-bridge" in sys.argv
build_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build")) build_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build"))
toc = find_toc(build_dir) toc = find_toc(build_dir)
if not toc: if not toc:
@@ -54,6 +60,19 @@ def main() -> int:
print(f"ERROR: Bundle thieu: {', '.join(missing)}. Kiem tra engine.spec datas!") print(f"ERROR: Bundle thieu: {', '.join(missing)}. Kiem tra engine.spec datas!")
return 1 return 1
print("OK: app/static + app/templates co trong bundle.") print("OK: app/static + app/templates co trong bundle.")
# F4: bridge sidecar (externalBin) — kiem tra o repo root (khong phai trong
# PyInstaller bundle): thieu -> tauri build se im lang bo qua bridge.
# Chi chay khi --check-bridge (goi o buoc [7/7] build_windows.ps1 — sau
# khi da build bridge o buoc [5/7]).
if check_bridge:
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sidecar = os.path.join(repo_root, BRIDGE_SIDECAR)
if not os.path.exists(sidecar):
print(f"ERROR: Thieu bridge sidecar: {sidecar}")
print(" Chay build/scripts/build_native_bridge.ps1 truoc, hoac")
print(" build_windows.ps1 buoc [5/7] se build tu dong.")
return 1
print(f"OK: bridge sidecar co mat ({BRIDGE_SIDECAR}).")
return 0 return 0