fix: GUI VST native window qua bridge + audio path bridge (SF2 preview/play câm)

- open_vst_gui: bo WebviewWindowBuilder/thread, chi push_control type=4 (hwnd=0 -> bridge tao window)
- main.cpp: create_native_vst_window (class SonicForge_Native_VST3_Class, 800x600, khong TOPMOST),
  tao trong ChannelWorker job, map guiWindows, capture arg2 by value (fix dangling)
- app.jsx: guard isBridgeActive() 8 cho -> bridge active thi moi note di router -> pushEvent -> bridge
  (truoc day SF2 cam vi HAS_PYFLUIDSYNTH=FALSE -> /soundfont-render 501; VST3 path cu dung nativeSf/Carla)
- E2E: SF2 NOTE_ON qua dispatchMidiEvent -> SHM peak 0.029745; Nexus GUI native hwnd OK (license/preset)
- docs: TASKS.md + TEST_NOTES.md ghi batch fix + ket qua; gitignore vendor/junk
This commit is contained in:
2026-08-12 22:12:43 +07:00
parent c3368d0a91
commit d8f8506077
37 changed files with 2170 additions and 419 deletions
+6
View File
@@ -37,3 +37,9 @@ src-tauri/vc_redist.x64.exe
app/storage/plugin_dirs.json
app/storage/sf_scan_state.json.bak-root
app/storage/soundfonts/
# build-time vendor trees & accidental artifacts (never commit)
native_bridge/vcpkg_installed/
native_bridge/sfizz-src/
app/static/js/_diag.txt
src-tauri/resources/daw_engine" && cp -r*
+41 -1
View File
@@ -28,6 +28,46 @@ Ký hiệu: **[MỚI]** tạo mới, **[THAY]** thay thế/đổi luồng hiện
> `--check-bridge`; F5 hooks.nsh OK; F6 docs 2 sidecar; F7 CI yml; B6 externalBin; B7 capabilities OK;
> A7 tách Vst3Instrument.h/.cpp (guard HAVE_VST3SDK, stub no-op khi thiếu submodule — A6 host thật vẫn
> TODO chờ vst3sdk trên Windows). Bundle rebuild OK; selfcheck 11160 pass Linux.
> **BATCH 2026-08-12 (chạy thật Windows + GỠ CARLA):** E2 resolve path fix — `_resolve_bridge_asset`
> scan plugin_dirs + os.walk cho SF2/SF3/SFZ; `loadTrackInstrumentToBridge` luôn gửi `r.path` qua
> `bridgeLoad`. **GỠ CARLA khỏi luồng instrument**: xóa `ensureCarlaForPlayback` + 2 call site + nút 🎛
> PM scan VST + dropdown "Carla Bridge (mở Carla.exe)" + 2 auto-open instrumentSelector/dropdown + nút
> Carla Bridge riêng (app.jsx); `runtime.js shouldRoute/shouldRoutePlayback` → false vô điều kiện (bridge
> host mọi instrument; Carla KHÔNG còn được mở/route khi bridge connected). GIỮ: FX module "Load Carla
> Bridge" (VST FX ngoài, user tự cài). **E2E thật (Tauri UI + CDP):** chèn SF2 024_BigSymphny.sf2 từ
> Plugin Manager → bridge load FULL PATH (log `[NativeBridge] instrument loaded ch=0 type=2 ...`), NOTE_ON
> → peak 0.0213 (âm phát), NOTE_OFF → 0 (decay). **`tauri build` lại**: NSIS `SonicForgeDAW_1.0.0_x64-setup.exe`
> (363MB) + MSI `SonicForgeDAW_1.0.0_x64_en-US.msi` (598MB) — engine mới (hash MD5 khớp dist) verify trong
> **FIX VST GUI HANG (attached()):** nguyên nhân `view->attached()` treo = openGUI chạy thread mới trong khi
> plugin được load ở thread cũ đã EXIT → COM STA apartment của load thread chết → plugin marshal nội bộ
> (Nexus) hang. Xác minh bằng `gui_probe` (standalone, target CMake, không ship): `bridge_like` (load
> thread A exit → openGUI thread B) TREO; `same_thread` (load+openGUI cùng thread, pump song song) OK.
> **Fix**: `main.cpp` thêm `ChannelWorker` — 1 thread persistent mỗi channel, giữ COM STA sống, LOAD và
> OPEN_GUI đều post job vào thread đó, thread pump message queue khi idle. Kèm: message pump ở đầu
> vòng audio loop (PeekMessage/Translate/Dispatch) cho editor trong lúc attached(). **Kết quả E2E (CDP +
> SHM):** Nexus `attached=0` → `[NativeBridge] GUI attached`, child editor windows visible trong
> `vst-Nexus`; SF2 peak 0.0236; Scaler2 note peak 0.0062 (VST audio routing OK); Nexus note peak 0 =
> license/preset (không phải host bug, GUI là đường load preset). 1 bridge process, 0 restart.
> **BATCH 2026-08-12 (FIX UI E2E — GUI VST native + audio path bridge):**
> **Issue 2 — GUI plugin là HTML che, không phải cửa sổ thật:** `open_vst_gui` (src-tauri/src/lib.rs)
> bỏ WebviewWindowBuilder/thread → chỉ `shm.push_control(4,0,0,0,&plugin_id)` (hwnd=0 → bridge tự tạo).
> `main.cpp` thêm `create_native_vst_window(title)` (RegisterClassA `SonicForge_Native_VST3_Class`,
> CreateWindowExA WS_OVERLAPPEDWINDOW|WS_VISIBLE 800x600, DefWindowProcA, KHÔNG WS_EX_TOPMOST); case
> OPEN_GUI arg1==0 tạo window NGAY TRONG ChannelWorker job (giữ COM STA), giữ `std::map<uint32_t,void*>
> guiWindows`; fix dangling: lambda capture `arg2=std::string(c.arg2)`. Verify: win_enum hwnd 0x4BE0EC4,
> class `SonicForge_Native_VST3_Class`, title Nexus, pid=27272, child `JUCE_19ff6684f11` visible —
> KHÔNG HTML che. Nexus note peak vẫn 0 = license/preset (kỳ vọng, GUI là đường load preset; init 1-2 phút).
> **Issue 1 — SF2 preview/play câm:** root cause `HAS_PYFLUIDSYNTH=FALSE` trong bundled engine →
> `/api/v1/plugins/soundfont-render` 501; VST3 play path cũ dùng nativeSf/scheduleCarlaNote (Carla đã gỡ →
> shouldRouteCarla false). Fix `app/static/js/app.jsx`: thêm guard `&& !(window.SonicMidiRouter &&
> window.SonicMidiRouter.isBridgeActive())` vào 8 chỗ (kbNative, keybed mouseenter, pvCtx wheel preview,
> clCtx piano-roll click, dwTrk draw, pvCtxInst draw preview, nativeSf x2, subNativeSf) → bridge active thì
> MỌI đường note đi qua router → pushEvent → bridge. Build + copy app.precompiled.js vào 3 nơi
> (src-tauri/target/release/daw_engine/_internal/app/static/, src-tauri/resources, dist/daw_engine). Verify
> HTTP: guard count=2 trong bundle served. **E2E chính ĐẠT:** sf2_focus2 — loadInstrument SF2 orchestral
> ch0 → dispatchMidiEvent NOTE_ON 60 → SHM peak **0.029745** → NOTE_OFF → 0. GUI Nexus native window OK.
> installer. D7 + E2 coi là hoàn tất (E2E thật).
> **BATCH CUỐI (2026-08-11):** B5 ✅ (pump thread tồn tại trong lib.rs ~263 — review tay, cần cargo check
> Windows); F5 ✅ (hooks.nsh `customInstall` ExecWait vc_redist toàn máy — đủ cho cả 2 exe); F2 🟡
> (ps1 đã copy, chạy thật cần Windows); D9 bổ sung `transport('set_position', playhead)` ~mỗi giây trong
@@ -88,7 +128,7 @@ Code skeleton đã chuẩn bị sẵn trong workspace `native_bridge/`; copy và
| ✅ 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 |
| 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 |
---
+129 -18
View File
@@ -5,29 +5,35 @@ Máy phát triển hiện tại (Linux, không rustc/CMake-MSVC, Python SIGILL k
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] A5+A13: TRANSPORT type=3 (STOP flush / PLAY / SET_POSITION) + log mỗi load — code OK, chưa test thật (cần tauri run / G4)
- [x] A10: InstrumentEngineManager multi-channel — build + chạy thật Windows OK (G2 load SF2 ch0 + SFZ ch1 đồng thời, mix vào master)
- [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**)
- [x] A6: Vst3Instrument host thật qua vst3sdk — load mda DX10 (.vst3) → noteon → PCM peak 0.16096 (G2 3 PASS, 2026-08-12)
- [x] A7: Vst3Instrument tách class + openGUI(hwnd)**FIX GUI HANG 2026-08-12**: `attached()` treo do
openGUI chạy thread khác với load thread (load thread exit → COM STA apartment chết). Probe
`gui_probe` (bridge_like treo / same_thread OK) → fix `ChannelWorker` (1 thread persistent/channel,
load+openGUI cùng thread, pump khi idle) + message pump đầu audio loop. E2E: Nexus attached=0 → GUI
attached, child editor windows visible; Scaler2 GUI attach vẫn FAILED khi window `vst-<id>` đã tồn tại
(open_vst_gui build fail → không push control — test-script issue, không phải host bug); Scaler2 note
peak 0.0062 = VST audio routing OK
- [x] **Windows verify sfizz API names**: sfizz 1.2.3 (sfizz-src) dùng `hdNoteOn/hdNoteOff/cc/pitchWheel/programChange` (tham số `delay` đầu), `setSampleRate(float)`, `setSamplesPerBlock(int)`, `renderBlock(float**, size_t, int numOutputs)` — đã sửa đúng 4 call trong NativeInstrumentEngine.cpp (bỏ no-op); velocity normalize /127 ở main.cpp:99
- [x] **Windows verify**: `shm_selfcheck.exe` chạy OK trên Windows — "SHM self-check OK (sizeof struct = 11160 bytes)"; `cargo check` + `cargo test` (layout_matches_c_header 12/1040/11160) PASS
## 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
- [x] B1-B4: `cargo check` pass trên Windows (sau khi sửa Cargo.toml features `Win32_System_Memory`+`Win32_Security`, dep `raw-window-handle=0.6`, shm.rs `.Value`, lib.rs `ok_or_else`)
- [x] B2: SHM struct layout khớp C header — unit test `layout_matches_c_header` trong src-tauri/src/shm.rs PASS (12/1040/11160 bytes)
- [x] B2: signature commands mới đã sync JS — chưa test thật qua UI (cần tauri run)
- [ ] 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)
- [ ] B6: tauri.conf.json externalBin — ✅ code + sidecar thật `binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe` (1.34MB, build PASS từ native_bridge) đã copy; 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)
- [x] C1-C7: node --check OK + `npm run build` (Babel) OK trên Windows — bundle `app/static/js/app.precompiled.js` 1.2MB mới
- [ ] C5: ring buffer 8 block chống underrun thật trên Windows (âm nghe được, VU nhảy)
## D. app.jsx wiring
@@ -35,7 +41,7 @@ chạy từng mục trên máy Windows (hoặc OS native có đủ toolchain),
- [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)
- [x] D4: Carla không route khi bridge active — verified: runtime.js shouldRoute/shouldRoutePlayback return false khi NativeBridgeService.isBridgeConnected (bridge host native SF2/SF3/SFZ/VST3)
- [ ] 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)
@@ -43,24 +49,24 @@ chạy từng mục trên máy Windows (hoặc OS native có đủ toolchain),
- [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] E1-E3: syntax OK — 38 file ast.parse OK trên Windows
- [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
- [x] SIGILL: hết — import `app.core.vst_engine` OK trên máy Windows này (máy dev Linux cũ không liên quan)
## 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] F1-F3: 3 script đã copy vào repo `build/scripts/` (ASCII-only, PS5.1-safe, CRLF) — build_native_bridge (vcpkg manifest fluidsynth + sfizz source + 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
- [ ] **Chạy thật trên Windows**: build_windows.ps1 trọn pipeline ra NSIS+MSI (đã verify build_native_bridge.ps1 chạy end-to-end OK)
## 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
- [x] G1: shm_selfcheck pass trên Windows — "SHM self-check OK (sizeof struct = 11160 bytes)"
- [x] G2: bridge mở SHM → load SF2 + SFZ → MIDI note → audio ≠ 0 (script `native_bridge/tests/g2_bridge_smoke.py`, peak SF2=0.00109 / SFZ=0.00107) — G2 thật từ Tauri UI còn lại ở G9/G10
- [ ] 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
@@ -70,6 +76,29 @@ chạy từng mục trên máy Windows (hoặc OS native có đủ toolchain),
- [ ] 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-12 (chạy thật Windows - E2E + GỠ CARLA)
- **E2 (resolve path soundfont) fix + verify thật**: `_resolve_bridge_asset` scan `_load_user_plugin_dirs()`
+ `os.walk` cho SF2/SF3/SFZ; JS `loadTrackInstrumentToBridge` luôn gọi `bridgeLoad({name, path: r.path})`.
E2E: chèn `024_BigSymphny.sf2` (thư mục Downloads\Collections soundfont) từ Plugin Manager ->
`bridge.log` ghi `[NativeBridge] instrument loaded ch=0 type=2 C:\Users\locpham\Downloads\Collections soundfont\024_BigSymphny.sf2`
(FULL PATH - không còn FAILED). NOTE_ON -> master peak -0.021314 (âm phát), NOTE_OFF -> 0 (decay).
- **GỠ CARLA (bridge host mọi instrument)**: xóa khỏi `app.jsx` - `ensureCarlaForPlayback` (hàm + 2 call
site), nút 🎛 PM scan VST row, dropdown "Carla Bridge (mở Carla.exe)" (synth dropdown), auto-open
instrumentSelector + instrumentDropdown, nút "Carla Bridge" riêng. `runtime.js`
`shouldRoute`/`shouldRoutePlayback` -> `return false` vô điều kiện -> Carla KHÔNG mở, KHÔNG route khi
bridge connected. **GIỮ chủ đích**: FX module "Load Carla Bridge" (2 call site `openInCarla` ở FX Rack
+ Master - VST FX ngoài, user tự cài, không phải luồng instrument).
- **Verify UI**: grep app.jsx - PM không còn section Carla (state/functions `pmCarlaVersion`,
`locateCarla`, `saveCarlaInput` còn nhưng không còn JSX render), không còn chuỗi "Carla Bridge (mở
Carla.exe)", không còn call `ensureCarlaForPlayback` (chỉ comment). Các hàm chết còn lại
(`fireCarlaNote`, `scheduleCarlaNote`, `refreshCarlaStatus`, `shouldRouteCarla`) vô hại - runtime trả false.
- **`tauri build` (bản cài sạch)**: NSIS `SonicForgeDAW_1.0.0_x64-setup.exe` (363MB) + MSI
`SonicForgeDAW_1.0.0_x64_en-US.msi` (598MB). Verify: extract NSIS bằng 7z -> `daw_engine/daw_engine.exe`
MD5 `8ddc7463333a4c334d24dcb1c891546d` khớp `dist/daw_engine` (engine bản mới); `daw_vst_bridge.exe` +
`libfluidsynth-3.dll` có trong bundle.
- **Quyết định giữ FX "Load Carla Bridge"**: đây là VST FX ngoài (native GUI, user tự cài) - tính năng
riêng của FX Rack, không liên quan luồng load instrument. Nếu muốn xóa luôn, làm sau.
## 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).
@@ -78,3 +107,85 @@ chạy từng mục trên máy Windows (hoặc OS native có đủ toolchain),
- `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.
## Ghi chú batch 2026-08-12 (bridge build + G2 trên Windows)
- **Bridge build PASS** (`native_bridge/build/_bridge.bat`, VS2019 x64 + vcpkg + sfizz source):
1. CMakeLists include_directories dùng `${FLUIDSYNTH_INCLUDE_DIRS}` (số nhiều) — `find_path` chỉ set `${FLUIDSYNTH_INCLUDE_DIR}``fluidsynth.h: No such file`. Sửa dùng singular.
2. sfizz 1.2.3 class là **`sfz::Sfizz`** (KHÔNG phải `sfizz::Synth`) → sửa `NativeInstrumentEngine.h` line 56.
3. `void* synth/settings` (giữ fluid types khỏi public header) → macro `FS_SYNTH`/`FS_SETTINGS` cast `static_cast<fluid_synth_t*>` mỗi call.
4. **LNK2038 RuntimeLibrary mismatch**: sfizz ép `/MT` trong `sfizz-src/cmake/SfizzConfig.cmake` (`set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded...")`) nhưng vcpkg fluidsynth là `/MD` → patch bỏ dòng ép + `_sfizz.bat` thêm `-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL`.
5. **Bug assign()**: gọi `init()` TRƯỚC `loadSoundFontFile()` — FluidSynth tạo synth trong lúc load nên `init` thấy null → SF2 load FAIL. Sửa thứ tự load → init (cả SF2 lẫn SFZ).
- Runtime: fluidsynth là DLL động — copy `libfluidsynth-3.dll` cạnh `daw_vst_bridge.exe` khi chạy standalone/sidecar.
- G2 test script: `native_bridge/tests/g2_bridge_smoke.py` — python tạo SHM (mirror Rust shm.rs offsets), spawn bridge với `SF_PARENT_PID`, push LOAD_INSTRUMENT (type=2 SOUNDFONT / 3 SFZ) + note-on, assert peak > 0.001.
## Ghi chú batch 2026-08-12 (scripts build Windows - F)
- **build_native_bridge.ps1** viết lại 7 bước (chạy end-to-end PASS trên máy thật):
1. vcpkg bootstrap (skip nếu có).
2. `vcpkg install --triplet x64-windows` theo manifest `native_bridge/vcpkg.json` — chỉ fluidsynth + pkgconf (sfizz KHÔNG có port vcpkg).
3. sfizz 1.2.3: clone tag `1.2.3` + `git submodule update --init --recursive`; patch idempotent bỏ dòng ép `/MT` trong `SfizzConfig.cmake` (regex `MultiThreaded$<$<CONFIG:Debug>:Debug>`); configure VS16 x64 với đủ flag `SFIZZ_SHARED=OFF ... -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL`; build target `sfizz_static`. Skip nếu `build/library/lib/Release/sfizz_static.lib` đã có.
4. vst3sdk: `git submodule update --init --recursive -- native_bridge/vst3sdk` (submodule ĐÃ đăng ký trong repo — không dùng `submodule add`).
5. CMake configure thêm `-G "Visual Studio 16 2019" -A x64`; build `--target daw_vst_bridge -- /m` (không build ALL_BUILD).
6-7. Copy sidecar `daw_vst_bridge-x86_64-pc-windows-msvc.exe` + `libfluidsynth-3.dll` (từ `build/vcpkg_installed/x64-windows/bin/`) vào `src-tauri/binaries/` — externalBin chỉ bundle exe, DLL phải cạnh sidecar.
- **build_windows.ps1**: thêm check `src-tauri\binaries\libfluidsynth-3.dll` sau bước bridge build.
- **tauri.conf.json**: thêm `"binaries/libfluidsynth-3.dll": "libfluidsynth-3.dll"` vào `bundle.resources` → installer đặt DLL cạnh exe (cùng thư mục sidecar). LƯU Ý: `tauri-build` validate resource tồn tại lúc compile — phải chạy build_native_bridge.ps1 trước `cargo build`/`tauri build`.
- **lib.rs bridge_candidates**: thêm 2 candidate `res_dir/triple_name` + `exe_dir/triple_name` — externalBin đặt sidecar ở INSTALL ROOT (tên có hậu tố triple), không nằm dưới `binaries/` → trước đây bản cài đặt không tìm thấy bridge (WASM fallback).
- Verify: parse PS1 OK (3 script), cargo check OK, chạy thử build_native_bridge.ps1 PASS (sfizz skip nhanh, build lại bridge + copy exe/DLL).
- **BUG CWD leak (sửa 2026-08-12)**: `build_native_bridge.ps1``Set-Location $PSScriptRoot` mà không trả về — gọi từ `build_windows.ps1` bằng `&` làm CWD bị đổi sang `build/scripts` → check `src-tauri\binaries\daw_vst_bridge-x86_64-pc-windows-msvc.exe` (relative) fail dù file tồn tại: "DONE: sidecar + DLL ..." rồi "ERROR: bridge sidecar missing". Sửa: bỏ `Set-Location` trong build_native_bridge.ps1 (mọi path đã absolute từ $PSScriptRoot) + build_windows.ps1 `Set-Location (Join-Path $PSScriptRoot "..\..")` ngay sau lệnh `&` (phòng thủ). Đã verify: CWD giữ nguyên, check sidecar+DLL = True.
- LƯU Ý PS 5.1: native stderr + `$ErrorActionPreference="Stop"` + redirect (`2>&1`/`*>`) → PS abort "NativeCommandError" (vd cmake warning). Không redirect trong flow build_windows.ps1 nên không ảnh hưởng; chỉ ảnh hưởng khi debug redirect stderr.
## Ghi chu batch 2026-08-12 (verify installer + spawn.log)
- User bao spawn.log khong thay dau vet daw_vst_bridge -> log dang xem la log CU 2026-08-11 21:00 (%APPDATA%\SonicForgeDAW\logs\spawn.log) tu ban cai truoc lib.rs fix (bridge logging + candidates). Log cu chi co engine candidates, khong co [bridge ...] lines.
- Installer hien tai src-tauri/target/release/bundle/nsis/SonicForgeDAW_1.0.0_x64-setup.exe (mtime 09:35) build SAU lib.rs fix (08:58) voi src-tauri/binaries/ day du (daw_vst_bridge-x86_64-pc-windows-msvc.exe 1.3M + libfluidsynth-3.dll 515K) -> tauri-bundler fail neu thieu externalBin/resource luc bundle, nen installer chac chan chua ca 2. KHONG can rebuild setup.exe.
- Candidate ky vong exists=True sau khi cai moi: exe_dir (triple name, bundle) = <INSTALL_ROOT>\daw_vst_bridge-x86_64-pc-windows-msvc.exe (LocalAppData\SonicForgeDAW neu per-user, hoac Program Files).
- Ghi chu: msiexec /a (administrative extract) tre tren MSI 570MB nay (0 file sau ~15 phut, phai taskkill). Khong dung duoc de verify noi dung installer; grep ten file trong setup.exe cung khong ra vi NSIS nen ca script data.
- KET QUA (user cai setup.exe 09:35 + chay): bridge chay OK — 3 process: sonicforge-daw.exe, daw_engine.exe, daw_vst_bridge.exe (9.2MB RAM). Install dir LocalAppData\\SonicForgeDAW: daw_vst_bridge.exe 1.3M + libfluidsynth-3.dll 515.5K canh nhau. spawn.log co [bridge ...] lines, exists=true tai resource_dir (bundle root) va exe_dir (portable) = <root>\\daw_vst_bridge.exe. LUU Y: externalBin cua NSIS dat sidecar o install ROOT voi TEN PLAIN (daw_vst_bridge.exe), KHONG co hau to triple -> candidates triple name deu exists=false (vo hai, plain name bat duoc). engine.log 10:02:40 listening 0.0.0.0:8001 OK.
## Ghi chu batch 2026-08-12 (fix UI: mat __TAURI__ khi redirect - trieu chung VSTi/Carla + SF2 khong am)
- TRIEU CHUNG user: (1) goi VSTi -> app chay voi Carla bridge thay vi native host bridge; (2) goi instrument SF2 -> khong co am thanh.
- GOC RE: src-tauri/ui/index.html la bootstrap script location.href = http://127.0.0.1:<port>/ redirect sang engine origin -> ROI T context tauri:// -> mat window.__TAURI__ -> nativeBridgeService._init() thay khong __TAURI__ -> 'native bridge disabled', isBridgeConnected=false -> unifiedMidiRouter.isBridgeActive() false -> moi MIDI roi ve onFallback (SonicSF WASM cho SF2) / SonicCarlaMidi (runtime.js route Carla khi track VSTi + bridge connected=false). Rust van spawn bridge + SHM -> process song, nhung JS khong bao gio dung. Khop 100% 2 trieu chung.
- FIX src-tauri/ui/index.html: bo location.href redirect; thay bang script async quet /health 8000-8010 tim engine, fetch(base) lay HTML UI, inject <base href> + window.API_BASE_URL sau <head>, roi document.write tren CUNG document -> origin tauri:// giu nguyen -> __TAURI__ song -> NativeBridgeService active. (Engine chon port dong 8000-8010, engine.log thay 8001; api.js mac dinh window.location.origin -> override API_BASE_URL. Engine CORS allow_origins=[*] OK fetch cheo origin.)
- FIX app/static/js/services/runtime.js (patch python 2 cho shouldRoute + shouldRoutePlayback): thay `if (NativeBridgeService.isBridgeConnected) return false;` bang check activeInstrumentType - chi chan Carla khi bridge connected VA instrument KHONG phai VST (VSTi van route Carla vi bridge chua host VST). Copy sang src-tauri/resources/daw_engine/_internal/... + dist/daw_engine/_internal/... (installer nhung tu resources; dist la nguon PyInstaller). cmp OK.
- build/scripts/frontend_host_index.html = template = ban index.html moi; build_windows.ps1 buoc [7/7] them Copy-Item frontend_host_index.html -> src-tauri/ui/index.html truoc npx tauri build.
- REBUILD installer 2026-08-12 10:27: setup.exe (363M) + 10:30: MSI moi. Trong release: runtime.js co activeInstrumentType (2 occurrences) = ban da patch. LUU Y: bash khong chay duoc npx (khong tim extension) -> dung `cmd //c ..\node_modules\.bin\tauri.cmd build` tu src-tauri.
- USER CAN LAM: cai lai setup.exe moi -> test SF2 (phai phat qua bridge, toast 'Da load vao Native Bridge', MIDI route bridge) va VSTi (van route Carla).
## Ghi chú batch 2026-08-12 (G2 3 PASS + fix VST3 dangle + installer mới)
- G2 smoke 3 PASS trên binary sạch (md5 bb1380357c30ec931058c65c29beeb46): SF2 peak=0.04333, SFZ peak=0.02455, VST3 (mda DX10) peak=0.16096.
- "Không ra âm" SFZ là artifact đo: g2_test.sfz dùng dummy1.wav (tone suy giảm nghe được ~120-230ms), poll cũ 0.25-0.6s bỏ lỡ. Fix: g2_bridge_smoke.py poll_peak(1.5s) mỗi 10ms track max.
- BUG VST3 crash khi load sau instrument khác (0xC0000005): src/Vst3Instrument.cpp loadPlugin dùng const ClassInfo* trỏ vào temporary trả về by value từ factory.classInfos() (sdk module.h ClassInfos classInfos() const noexcept) → dangle sau range-for. Fix: copy ClassInfo chosen; by value trong scope { auto infos = factory.classInfos(); ... }. Verify 4 sequence sf2,vst3 / vst3,sfz / sf2,sfz,vst3 / sfz,vst3 đều ALIVE, log “[Vst3Instrument] loaded ... (mda DX10) outCh=2”.
- runtime.js (cả 2 bản: src-tauri/resources/daw_engine/_internal + app/static/js/services): shouldRoute/shouldRoutePlayback return false khi window.NativeBridgeService.isBridgeConnected → bridge host native SF2/SF3/SFZ/VST3, không bao giờ route Carla.
- Debug prints đã xóa khỏi NativeInstrumentEngine.cpp ([SFZ] loadSfzFile/noteOn/render peaks); cleanup: scratch_test target khỏi CMakeLists.txt + scratch_test.cpp + fix_*.py + dbg_* probes + seq_*.txt logs đã xóa.
- Installer mới: NSIS SonicForgeDAW_1.0.0_x64-setup.exe (11:44:12, 363307885 B) + MSI (11:47:02, 598618948 B). md5 3 binary giống hệt bb1380357c30ec931058c65c29beeb46 (build/Release == src-tauri/binaries/ == target/release). packaged runtime.js có isBridgeConnected check (PACKAGED-OK).
- CMakeLists sau khi bỏ scratch_test vẫn build OK (daw_vst_bridge.vcxproj relink, md5 giữ nguyên).
## Ghi chú batch 2026-08-12 (FIX LOGIN "Not Found" + reset pass admin + rebuild installer)
- TRIỆU CHỨNG user: login không được — "Not Found", "không tìm thấy database cũ standalone", pass mặc định không vào.
- NG.NHÂN "Not Found" (2 lớp):
1) app.jsx:10 `const API_BASE_URL = window.location.origin;` — trong Tauri origin=tauri://localhost → mọi fetch API resolve sai → "Not Found". Fix: `(window.API_BASE_URL || window.location.origin).replace(/\/+$/, '')`; rebuild bundle (app.precompiled.js), bump tag ?v=202608102202 → ?v=202608121215 trong 4 index.html (app/templates, installed _internal, dist/_internal, resources/_internal).
2) double-slash: shell Tauri inject API_BASE_URL có trailing slash ("http://127.0.0.1:8000/") + api.js nối /api/v1 → //api/v1 → FastAPI 404 "Not Found". Fix: base.replace(/\/$/,'') trong build/scripts/frontend_host_index.html (đã sync src-tauri/ui/index.html, diff IDENTICAL) + api.js:2 replace(/\/+$/,''). Probe xác nhận: double-slash=404, single-slash=400.
- DB thật: %APPDATA%\SonicForgeDAW\storage\sonicforge.db (WAL, 96KB) — KHÔNG có DB ở LocalAppData; user tìm nhầm chỗ. Chứa sẵn admin + 2 projects (dữ liệu cũ còn nguyên).
- Pass admin đã bị đổi (verify admin123/admin/password/... đều FALSE). seed_admin chỉ seed khi chưa có admin, KHÔNG reset. Không có endpoint reset. → Reset trực tiếp DB (engine tắt): hashed_password = hash_password("admin123") (PBKDF2-HMAC-SHA256 600000 iter, salt token_hex(32), format salt:hash — dùng đúng thuật toán app/core/auth.py). Verify: admin/admin123 login HTTP 200 (token + user), admin/wrong 400.
- REBUILD SẠCH (không hot-patch): `python -m PyInstaller engine.spec --clean --noconfirm` → dist/daw_engine mới (12:29, md5 3367007aaced6e3c32a7a940914850e0) → copy sang src-tauri/resources/daw_engine/ → `cmd //c ..\node_modules\.bin\tauri.cmd build` từ src-tauri → installer mới: NSIS setup.exe 363333650 B (12:44) + MSI 598651716 B (12:47). Bridge md5 giữ bb1380357c30ec931058c65c29beeb46 (release + binaries).
- Verify packaged (target/release/daw_engine/_internal): api.js fix=1, precompiled fix=1, index.html tag ?v=202608121215, runtime.js có isBridgeConnected, engine md5 == dist.
- PASS MẶC ĐỊNH SAU CÀI MỚI: admin / admin123 (user nên đổi lại sau khi vào).
## Ghi chú batch 2026-08-12 (FIX: GUI VST native window + audio path bridge — E2E ĐẠT)
- TRIỆU CHỨNG user: (1) chọn VSTi → mở Carla (KHÔNG đúng TASKS.md — bridge host mọi instrument);
(2) instrument SF2 câm; (3) GUI plugin mở ra nhưng là HTML che, không phải cửa sổ plugin thật.
- FIX Issue 2 (GUI native): src-tauri/src/lib.rs `open_vst_gui` bỏ WebviewWindowBuilder/thread → chỉ
`shm.push_control(4,0,0,0,&plugin_id)`; native_bridge/src/main.cpp thêm `create_native_vst_window(title)`
(class `SonicForge_Native_VST3_Class`, WS_OVERLAPPEDWINDOW|WS_VISIBLE 800x600, không TOPMOST), OPEN_GUI
arg1==0 tạo window trong ChannelWorker job, giữ `guiWindows` map, capture arg2 by value (fix dangling).
Verify: hwnd 0x4BE0EC4 class SonicForge_Native_VST3_Class title Nexus, child JUCE_19ff6684f11 visible.
- FIX Issue 1 (âm câm): root cause SF2 = `HAS_PYFLUIDSYNTH=FALSE` trong bundled engine →
`/api/v1/plugins/soundfont-render` 501; VST3 play path cũ nativeSf/scheduleCarlaNote không chạy vì Carla
đã gỡ. Fix app.jsx: guard `isBridgeActive()` vào 8 chỗ (kbNative 9306, keybed mouseenter 9335, pvCtx
wheel 8180, clCtx piano-roll 8550, dwTrk draw 8814, pvCtxInst draw 8915, nativeSf 21779+22075,
subNativeSf 21942) → bridge active thì mọi note đi router → pushEvent → bridge.
- E2E CHÍNH ĐẠT (CDP + SHM): sf2_focus2 — bridge active, loadInstrument SF2 orchestral ch0 ok=true,
dispatchMidiEvent NOTE_ON 60 → SHM peak 0.029745 (âm phát qua bridge), NOTE_OFF → pk 0.
- E2E UI còn lại: chèn SF2 024 từ PM → track 01 có instrument 024; PR track 01 vẫn mở track 02 (track 01
chưa có MIDI item — dblclick lane không mở PR; không phải bug); preset list modal Select Instrument
trống (bank list OK) — instrument data chưa expand, không chặn E2E chính.
- VERIFY thêm: `gui_test_nexus.py` native window OK; bridge.log 1 lần "Starting", 43 "instrument loaded",
load thread ch=1 path 024_BigSymphny.sf2 → assigned ok=1; transport STOP.
+16 -6
View File
@@ -1232,14 +1232,20 @@ def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str])
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]:
sf_dirs = [UPLOAD_SF_DIR, SYSTEM_SF_DIR]
try:
from app.core.vst_engine import _load_user_plugin_dirs
sf_dirs += _load_user_plugin_dirs() or []
except Exception:
pass
for base_dir in dict.fromkeys(d for d in sf_dirs if d):
if not os.path.isdir(base_dir):
continue
for fname in os.listdir(base_dir):
cand = os.path.join(base_dir, fname)
for root, _, files in os.walk(base_dir):
for fname in files:
if fname.lower().endswith((".sf2", ".sf3", ".sfz")) and \
(fname.lower() == base.lower() or os.path.splitext(fname)[0].lower() == base_noext):
return cand
return os.path.join(root, fname)
ext = (".vst3" if instrument_type.upper() == "VST3" else
".vst2" if instrument_type.upper() == "VST2" else
".sfz" if instrument_type.upper() == "SFZ" else None)
@@ -1249,8 +1255,12 @@ def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str])
dirs = _load_user_plugin_dirs() or []
except Exception:
dirs = []
if not dirs:
dirs = [settings.VST_DIR, settings.SOUNDFONT_DIR]
# Luon gop VST_DIR/SOUNDFONT_DIR: neu chi dung user plugin_dirs,
# VST trong C:/Program Files/Common Files/VST3 khong resolve
# duoc -> bridge load fail -> VSTi cam / khong mo GUI.
for d in (settings.VST_DIR, settings.SOUNDFONT_DIR):
if d and d not in dirs:
dirs.append(d)
for base_dir in dirs:
if not os.path.isdir(base_dir):
continue
+67 -130
View File
@@ -7,7 +7,7 @@ const {
} = React;
// FastAPI Backend Configuration
const API_BASE_URL = window.location.origin;
const API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\/+$/, '');
const API_AUDIO = `${API_BASE_URL}/api/v1/audio`;
const API_MULTITRACK = `${API_BASE_URL}/api/v1/multitrack`;
const API_TASKS = `${API_BASE_URL}/api/v1/audio/tasks`;
@@ -143,38 +143,6 @@ const refreshCarlaStatus = () => {
// Carla chưa chy T ĐNG m vi VSTi ca track ri CH OSC ready (poll
// carla-status ti đa 10s) nt ch gi khi bridge thc s nhn đưc.
// Có gate __carlaOpening chng spawn trùng (mi play ch m 1 ln).
const ensureCarlaForPlayback = (synthEngine) => {
try {
if (!window.SonicCarlaMidi || !window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) return;
if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return;
if (window.__carlaRunning === true) { flushCarlaNoteQueue(); return; }
if (window.__carlaOpening) return;
window.__carlaOpening = true;
const finish = (ok) => {
window.__carlaRunning = !!ok;
window.__carlaOpening = false;
flushCarlaNoteQueue();
};
window.SonicAPI.carlaStatus().then(st => {
if (st && st.running) { finish(true); return; }
const pid = synthEngine && synthEngine.plugin_id;
if (!pid) { finish(false); return; }
window.SonicAPI.openInCarla(pid).then(r => {
if (!r || !r.success) { finish(false); return; }
// Carla spawn (hoc đã chy) poll ti khi OSC ready, mi flush nt
const deadline = Date.now() + 10000;
const tick = () => {
window.SonicAPI.carlaStatus().then(s2 => {
if (s2 && s2.running) { finish(true); return; }
if (Date.now() > deadline) { finish(false); return; }
setTimeout(tick, 400);
}).catch(() => finish(false));
};
tick();
}).catch(() => finish(false));
}).catch(() => finish(false));
} catch (e) {}
};
// Môi trưng chy: docker vs standalone
@@ -5901,11 +5869,6 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
)
),
React.createElement('div', { className: 'flex items-center gap-2' },
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
onClick: (e) => { e.stopPropagation(); window.SonicAPI.openInCarla(v.id).then(function (r) { if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id), 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
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)'
}, '🎛 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',
@@ -5977,39 +5940,6 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
})
)
),
// Carla Bridge section (desktop) đnh v carla.exe portable
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.runtime === 'desktop') && React.createElement('div', { className: 'pt-4 mt-4 border-t border-[#383838]' },
React.createElement('h4', { className: 'text-xs font-bold text-teal-400 uppercase mb-2' }, 'Carla Bridge (VSTi native GUI)'),
React.createElement('p', { className: 'text-[10px] text-zinc-500 mb-2 break-all' },
(window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_path) ?
'Đã định vị: ' + window.SonicRuntime.capabilities.features.carla_path :
'Chưa tìm thấy Carla. Bản Windows là zip portable (không cài đặt, không dùng PATH) — nhấn "Định vị Carla..." và chọn thư mục chứa carla.exe.'
),
React.createElement('div', { className: 'flex gap-2' },
React.createElement('button', {
onClick: locateCarla,
className: 'px-3 py-1.5 bg-teal-800 hover:bg-teal-700 text-white text-xs font-semibold rounded transition flex items-center gap-1'
}, React.createElement('i', { 'data-lucide': 'folder-search', className: 'w-3 h-3' }), 'Định vị Carla...'),
(window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
onClick: () => { window.SonicAPI.openInCarla().then(function (r) { if (r && r.success) showToast('Đã mở Carla', 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition flex items-center gap-1'
}, React.createElement('i', { 'data-lucide': 'play', className: 'w-3 h-3' }), 'Mở Carla')
),
React.createElement('div', { className: 'flex gap-2 mt-2' },
React.createElement('input', {
type: 'text',
placeholder: 'Hoặc nhập thư mục chứa carla.exe (VD: D:/Tools/Carla)',
value: pmCarlaPathInput,
onChange: e => setPmCarlaPathInput(e.target.value),
onKeyDown: e => { if (e.key === 'Enter') saveCarlaInput(); },
className: 'flex-1 bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-teal-600'
}),
React.createElement('button', {
onClick: saveCarlaInput,
className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition shrink-0'
}, 'Lưu')
)
),
// Plugin directories section (folder picker + save + scan)
React.createElement('div', {
className: 'pt-4 mt-4 border-t border-[#383838]'
@@ -6068,11 +5998,6 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
React.createElement('span', { className: 'w-16 shrink-0 text-zinc-500 font-mono text-[9px] truncate' }, v.type || 'VST'),
React.createElement('span', { className: 'truncate' }, v.name),
React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, v.dir),
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
onClick: () => { window.SonicAPI.openInCarla(null, v.path).then(function (r) { if (r && r.success) showToast('Đã mở Carla với ' + v.name, 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
className: 'text-[9px] bg-teal-800 hover:bg-teal-700 text-white px-1.5 py-0.5 rounded transition shrink-0',
title: 'Mở trong Carla (native GUI)'
}, '🎛')
)
),
React.createElement('div', { className: 'text-[10px] font-bold text-amber-300 uppercase flex items-center gap-1 mt-2' },
@@ -8251,7 +8176,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
ensureSonicInstrument(pvCtx);
playing.forEach(n => {
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) {
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
return;
}
@@ -8621,7 +8546,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
ensureSonicInstrument(clCtx);
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
} else {
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
@@ -8885,7 +8810,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine)) {
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
const ctx = getAudioContext();
@@ -8986,7 +8911,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine)) {
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
var pvCtx = getAudioContext();
@@ -9378,12 +9303,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
}
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine);
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (kbNative) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
} else if (window.SonicSF && !kbNative) {
// FIX: gi note theo thi gian bm phím durationMs ln (5s)
// ch là auto-off phòng h; mouseup/mouseleave gi stopNote dng
@@ -9406,11 +9331,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
}
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) {
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
} 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
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
@@ -9429,7 +9354,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {}
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
@@ -9441,7 +9366,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {}
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
@@ -15030,11 +14955,6 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
<div className={`flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-blue-100 ${!synthInst ? 'bg-slate-200' : ''}`} onClick={() => selectSynthInst(null)}>
<i className="fa-solid fa-ban text-slate-400"></i> None (mặc định)
</div>
{window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local && (
<div className="flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-teal-100 font-semibold text-teal-700 border-t border-[#e0e0e0]" onClick={() => { setSynthOpen(false); window.SonicAPI.openInCarla().then(r => { if (r && r.success) showToast('Đã mở Carla — chỉnh preset rồi Upload trong app', 'success'); }).catch(err => showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }}>
<i className="fa-solid fa-sliders text-teal-500"></i> 🎛 Carla Bridge (mở Carla.exe)
</div>
)}
{!synthLoading && filteredSynthList && filteredSynthList.map(group => (
<div key={group.sf.id || group.sf.name}>
<div className="px-2 py-1 bg-[#f0f0f0] font-semibold text-slate-600 border-t border-[#e0e0e0] truncate">{group.sf.display || group.sf.name || group.sf.id}</div>
@@ -15422,6 +15342,37 @@ const App = () => {
"Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal",
"Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"
];
// D7: bridge active load instrument vào bridge ti channel router dùng cho
// track (SonicMidiRouter.allocateChannel cùng allocation scheduleMidiNoteDispatch/
// keybed dùng khi push NOTE_ON). Không load MIDI ti channel rng C++ silent.
const loadTrackInstrumentToBridge = (trackId, instrumentId, bank) => {
if (!window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive() || !instrumentId) return;
const isSf = typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
const bch = window.SonicMidiRouter.allocateChannel(trackId, bank === 128);
const btype = isSf ? 'SF2' : 'VST3';
let bpath = null;
if (instrumentSelectorData) {
if (isSf) {
const sid = instrumentId.replace('sf_', '');
const s0 = (instrumentSelectorData.soundfonts || []).find(s => String(s.id).replace('sf_', '') === sid);
bpath = s0 && (s0.file || s0.path);
} else {
const v0 = (instrumentSelectorData.vst_instruments || []).find(v => v.id === instrumentId || v.name === instrumentId);
bpath = v0 && v0.path;
}
}
(async () => {
let p = bpath;
try {
const r = await window.SonicAPI.bridgeLoad({ name: instrumentId, path: bpath || null, instrumentType: btype, channel: bch });
if (r && r.path) p = r.path;
} catch (e) { console.warn('[Bridge] resolve fail:', e); }
if (p) {
const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch);
console.log('[Bridge] track', trackId, '-> bridge', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p);
}
})();
};
const setTrackInstrumentWithProgram = (trackId, instrumentId, programNumber, displayName, bankNumber) => {
const isSfInstrument = instrumentId && typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
const sfBank = bankNumber !== undefined ? bankNumber : (isSfInstrument ? 0 : undefined);
@@ -15465,6 +15416,8 @@ const App = () => {
const sfId = instrumentId.replace('sf_', '');
window.SonicSF.selectInstrument(mch, sfBank || 0, sfProg || 0, sfId);
}
// D7: bridge active load instrument vào bridge ti channel router s dùng
loadTrackInstrumentToBridge(trackId, instrumentId, sfBank);
setSubTabs(prev => prev.map(s => {
if (s.trackId !== trackId) return s;
return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId };
@@ -15684,7 +15637,7 @@ const App = () => {
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) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: bt.id, pitch: pitch, velocity: scaledVel / 127, percussion: !!(bt.synth_engine && bt.synth_engine.soundfont_bank === 128) }); } catch (e) {}
});
} else if (window.SonicSF) {
var allTracks = activeTracksRef.current || [];
@@ -15759,7 +15712,7 @@ const App = () => {
// 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) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: bst.id, pitch: pitch, velocity: 0, percussion: !!(bst.synth_engine && bst.synth_engine.soundfont_bank === 128) }); } catch (e) {}
});
} else if (window.SonicSF && window.SonicSF.stopNote) {
var stopTracks = activeTracksRef.current || [];
@@ -21693,6 +21646,17 @@ const App = () => {
const program = track ? track.instrumentProgram : undefined;
var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
const routeCarla = shouldRouteCarla(track && track.synth_engine);
// D7: bridge active preview qua bridge (channel router allocate đúng
// instrument đã load khi chèn vào track).
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
const isPerc = !!(track && track.synth_engine && track.synth_engine.soundfont_bank === 128);
const tch = track ? track.id : 0;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: tch, pitch: pitch || 60, velocity: velocity, percussion: isPerc }); } catch (e) {}
setTimeout(function () {
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: tch, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {}
}, (durationMs || 500) + 30);
return;
}
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
return;
@@ -21721,24 +21685,26 @@ const App = () => {
// 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;
// Percussion (bank 128) router allocateChannel ch 9; melodic round-robin.
const isPerc = !!(synthEngine && synthEngine.soundfont_bank === 128);
// 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) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 0, velocity: 0, data2: 0, percussion: isPerc }); } catch (e) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 32, velocity: 0, data2: 0, percussion: isPerc }); } catch (e) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'PROGRAM', channel: trkId, pitch: 0, velocity: 0, data2: program, percussion: isPerc }); } 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) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8, percussion: isPerc }); } catch (e) {}
}, delayMs);
setTimeout(function () {
if (!guardPlay()) return;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0 }); } catch (e) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {}
}, delayMs + (durMs || 1000) + 30);
return;
}
@@ -21810,14 +21776,13 @@ const App = () => {
// MIDI items playback
const midiItems = track.midiItems || [];
const routeCarla = shouldRouteCarla(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
if (nativeSf) {
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
} else {
// Preview cache: capture the soundfont (pre track-FX) for fast offline export
ensureMidiCapture(track, activeTrackNodesRef.current[track.id]);
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) ensureCarlaForPlayback(track.synth_engine);
var trkCh = assignTrackMidiChannel(track, allPlayTracks);
// Ensure instrument is loaded in FluidSynth
if (window.SonicSF && track.synth_engine && track.synth_engine.type === 'soundfont' && track.synth_engine.soundfont_id) {
@@ -21974,7 +21939,7 @@ const App = () => {
// 2. Play MIDI items in subTrack
const subMidiItems = subTrack.midiItems || [];
const subRouteCarla = shouldRouteCarla(subTrack.synth_engine);
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine);
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf)) {
if (subNativeSf) {
subMidiItems.forEach(item => scheduleNativeSfItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
@@ -21997,7 +21962,6 @@ const App = () => {
// MIDI item trong SECTION Carla bridge khi VSTi loaded
// (ch route Carla không play FluidSynth GM sai âm)
const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));
if (subRouteCarla || window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine)) ensureCarlaForPlayback(subTrack.synth_engine);
if (offsetTime < noteStartMain) {
const delay = noteStartMain - offsetTime;
@@ -22108,7 +22072,7 @@ const App = () => {
// MIDI items playback
const midiItems = track.midiItems || [];
const routeCarla = shouldRouteCarla(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
if (nativeSf) {
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
@@ -31271,13 +31235,6 @@ STRICT CONSTRAINTS:
onClick: () => {
setTrackInstrumentWithUndo(instrumentSelectorTrackId, v.id, v.name || v.id);
closeInstrumentSelector();
// T ĐNG m Carla vi VSTi va chn (desktop + Carla local)
// Carla load sn plugin + keyboard o đ preview realtime.
if (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) {
window.SonicAPI.openInCarla(v.id).then(function (r) {
if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id) + ' — chọn preset, bấm keyboard để preview', 'success');
}).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
}
},
className: "w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-violet-800 text-zinc-300 flex items-center justify-between gap-2"
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[9px] text-cyan-400 shrink-0" }, v.type || "VST")))
@@ -31340,14 +31297,6 @@ STRICT CONSTRAINTS:
onClick: () => setTrackInstrumentWithUndo(instrumentDropdownTrackId, null),
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400"
}, "None (Default Synth)"),
window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", {
onClick: () => {
setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null);
window.SonicAPI.openInCarla().then(function (r) { if (r && r.success) { showToast('Đã mở Carla — chọn VSTi, chỉnh âm, Save preset (.vstpreset) rồi Upload trong app', 'success'); } }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
},
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 font-semibold flex items-center justify-between",
title: "Mở Carla.exe trên hệ thống (native GUI VSTi) — không cần scan VST trong app"
}, "\uD83C\uDF9B Carla Bridge (m\u1EDF Carla.exe)", /*#__PURE__*/React.createElement("span", { className: "text-[9px] text-teal-500 shrink-0 ml-1" }, "GUI")) : null,
filteredInstruments.soundfonts.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "SoundFonts"),
filteredInstruments.soundfonts.map((sf, i) => /*#__PURE__*/React.createElement("button", {
key: "sfd_" + i,
@@ -31364,21 +31313,9 @@ STRICT CONSTRAINTS:
setInstrumentDropdownTrackId(null);
setInstrumentDropdownBtnRect(null);
setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id);
// T ĐNG m Carla vi VSTi va chn (desktop + Carla local)
// Carla load sn plugin, native GUI + keyboard o đ preview realtime.
if (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) {
window.SonicAPI.openInCarla(v.id).then(function (r) {
if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id) + ' — chọn preset, bấm keyboard để preview', 'success');
}).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
}
},
className: "flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST")),
window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", {
onClick: (e) => { e.stopPropagation(); window.SonicAPI.openInCarla(v.id).then(function (r) { if (r && r.success) { showToast('Đã mở Carla: ' + (v.name || v.id), 'success'); } }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
className: "shrink-0 px-2 text-xs bg-zinc-800 hover:bg-teal-700 text-teal-300 border-l border-zinc-700",
title: "Mở trong Carla (native GUI)"
}, "\uD83C\uDF9B") : null
)),
filteredInstruments.vst.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "VST Presets (.vstpreset)"),
(window.SonicRuntime && window.SonicRuntime.presets || []).map((p, i) => /*#__PURE__*/React.createElement("button", {
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -1,5 +1,5 @@
// SonicForge Studio API Service
window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\/+$/, '');
(function() {
function getAuthToken() {
@@ -115,9 +115,9 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
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' }),
bridgeStatus: () => apiRequest('/api/v1/plugins/bridge/status', { method: 'GET' }),
bridgeLoad: (payload) => apiRequest('/api/v1/plugins/bridge/load', { method: 'POST', body: JSON.stringify(payload) }),
bridgeLog: (lines = 100) => apiRequest(`/api/v1/plugins/bridge/log?lines=${lines}`, { method: 'GET' }),
getAIPresets: () => apiRequest('/api/v1/ai/presets', { method: 'GET' }),
saveAIPreset: (preset) => apiRequest('/api/v1/ai/presets', { method: 'POST', body: JSON.stringify(preset) }),
deleteAIPreset: (presetId) => apiRequest(`/api/v1/ai/presets/${presetId}`, { method: 'DELETE' }),
@@ -20,6 +20,30 @@
if (this._statusCb) this._statusCb({ connected: !!connected });
},
/** Fix C: re-establish bridge routing once audio frames arrive again
* after bridge-down. Same wiring as the bootstrap block in app.jsx
* (getAudioContext -> BridgeAudioNode.init -> AudioRoutingEngine.connect). */
_reconnectInFlight: false,
_reconnectIfNeeded: function () {
if (this._reconnectInFlight) return;
this._reconnectInFlight = true;
var self = this;
var finish = function () { self._reconnectInFlight = false; };
try {
var ctx = (typeof getAudioContext === 'function') ? getAudioContext() : (window.__sharedAudioCtx || null);
if (ctx && window.BridgeAudioNode && window.AudioRoutingEngine) {
window.BridgeAudioNode.init(ctx);
window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
}
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(true);
this._notifyStatus(true);
console.warn('[BridgeService] bridge recovered — routing re-established.');
} catch (e) {
console.warn('[BridgeService] reconnect failed:', e);
}
finish();
},
_init: function () {
if (!this._tauri()) {
// Dev mode (Linux / plain browser): no Tauri -> log-only, app uses SonicSF.
@@ -29,6 +53,9 @@
var self = this;
try {
window.__TAURI__.event.listen('bridge-audio', function (e) {
// Bridge recovered after bridge-down (or never bootstrapped):
// audio frames flow again -> re-establish routing (Fix C).
if (!self.isBridgeConnected) self._reconnectIfNeeded();
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
});
window.__TAURI__.event.listen('bridge-down', function () {
+4 -18
View File
@@ -73,28 +73,14 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
// (Carla standalone bật OSC UDP mặc định cổng 22752).
window.SonicCarlaMidi = window.SonicCarlaMidi || {
shouldRoute: function (synthEngine, isArmed) {
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;
if (!c || !c.features || !c.features.carla_local) return false;
if (!isArmed) return false;
var se = synthEngine || {};
return String(se.type || '').indexOf('vst') !== -1 && !!se.plugin_id;
} catch (e) { return false; }
// Carla bridge removed - all instruments hosted by Native Bridge.
return false;
},
// Playback MIDI items: route khi track VSTi + Carla local (không cần ARM —
// user đã chủ động bấm Play trên item đó).
shouldRoutePlayback: function (synthEngine) {
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;
if (!c || !c.features || !c.features.carla_local) return false;
var se = synthEngine || {};
return String(se.type || '').indexOf('vst') !== -1 && !!se.plugin_id;
} catch (e) { return false; }
// Carla bridge removed - all instruments hosted by Native Bridge.
return false;
},
noteOn: function (channel, note, velocity) {
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
+1 -1
View File
@@ -50,7 +50,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608102202" defer></script>
<script src="/static/js/app.precompiled.js?v=202608121215" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+72 -20
View File
@@ -1,52 +1,104 @@
# 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.
# Run on Windows x64 with VS Build Tools 2019+ (C++ workload) installed.
# ASCII only (PowerShell 5.1 reads .ps1 without BOM as ANSI).
$ErrorActionPreference = "Stop"
Set-Location $PSScriptRoot
# NOTE: no Set-Location here - this script must not change the caller's CWD
# (build_windows.ps1 calls it with & and then checks relative paths).
# All paths below are absolute (derived from $PSScriptRoot).
$REPO_ROOT = Resolve-Path (Join-Path $PSScriptRoot "..\..") # ...\SonicForgeStudio
$NB_DIR = Join-Path $REPO_ROOT "native_bridge"
$VS_GEN = "Visual Studio 16 2019" # VS2019 Build Tools on this machine
$VS_ARCH = "x64"
# 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 =="
Write-Host "== [1/7] 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 }
# 2. fluidsynth via vcpkg manifest (native_bridge/vcpkg.json; pkgconf included)
Write-Host "== [2/7] vcpkg install fluidsynth (manifest) =="
Push-Location $NB_DIR
& (Join-Path $VCPKG_ROOT "vcpkg.exe") install --triplet x64-windows
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Host "ERROR: vcpkg install failed" -ForegroundColor Red; exit 1 }
Pop-Location
# 3. VST3 SDK submodule (Steinberg; NOT in vcpkg)
Write-Host "== [3/6] VST3 SDK (submodule) =="
# 3. sfizz 1.2.3 static lib (sfizz has NO vcpkg port: clone + build once)
Write-Host "== [3/7] sfizz 1.2.3 (clone + build static /MD, skip if built) =="
$SFIZZ_DIR = Join-Path $NB_DIR "sfizz-src"
$SFIZZ_BUILD = Join-Path $SFIZZ_DIR "build"
$SFIZZ_STATIC = Join-Path $SFIZZ_BUILD "library\lib\Release\sfizz_static.lib"
if (-not (Test-Path $SFIZZ_STATIC)) {
if (-not (Test-Path (Join-Path $SFIZZ_DIR "CMakeLists.txt"))) {
git clone --branch 1.2.3 --depth 1 https://github.com/sfztools/sfizz.git $SFIZZ_DIR
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: sfizz clone failed" -ForegroundColor Red; exit 1 }
}
Push-Location $SFIZZ_DIR
git submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Host "ERROR: sfizz submodules failed" -ForegroundColor Red; exit 1 }
Pop-Location
# Patch: sfizz forces /MT (static CRT) in SfizzConfig.cmake; vcpkg fluidsynth
# is /MD. Replace the forced line (idempotent; no-op if already patched).
$cfg = Join-Path $SFIZZ_DIR "cmake\SfizzConfig.cmake"
$cfgText = [System.IO.File]::ReadAllText($cfg)
$patched = $cfgText -replace '(?m)^\s*set\(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded\$<\$<CONFIG:Debug>:Debug>"\)\s*$', '# patched for local Windows build: caller picks runtime (/MD) to match vcpkg'
if ($patched -ne $cfgText) { [System.IO.File]::WriteAllText($cfg, $patched) }
# Configure + build static lib with /MD to match fluidsynth
cmake -S $SFIZZ_DIR -B $SFIZZ_BUILD -G $VS_GEN -A $VS_ARCH `
-DSFIZZ_SHARED=OFF -DSFIZZ_RENDER=OFF -DSFIZZ_TESTS=OFF -DSFIZZ_DEMOS=OFF `
-DSFIZZ_DEVTOOLS=OFF -DSFIZZ_BENCHMARKS=OFF -DSFIZZ_JACK=OFF `
-DSFIZZ_USE_SNDFILE=OFF -DSFIZZ_USE_SYSTEM_ABSEIL=OFF `
-DSFIZZ_USE_SYSTEM_KISS_FFT=OFF -DSFIZZ_USE_SYSTEM_PUGIXML=OFF `
-DSFIZZ_USE_SYSTEM_CATCH=OFF -DSFIZZ_USE_SYSTEM_CXXOPTS=OFF `
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF `
-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: sfizz cmake configure failed" -ForegroundColor Red; exit 1 }
cmake --build $SFIZZ_BUILD --config Release --target sfizz_static -- /m
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: sfizz build failed" -ForegroundColor Red; exit 1 }
} else {
Write-Host " sfizz_static.lib already present - skip"
}
# 4. VST3 SDK submodule (Steinberg; NOT in vcpkg; already registered in repo)
Write-Host "== [4/7] 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
Push-Location $REPO_ROOT
git submodule update --init --recursive -- native_bridge/vst3sdk
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Host "ERROR: vst3sdk submodule failed" -ForegroundColor Red; exit 1 }
Pop-Location
}
# 4. Configure with CMake + vcpkg toolchain (MSVC)
Write-Host "== [4/6] CMake configure =="
# 5. Configure + build with CMake + vcpkg toolchain (MSVC, bridge target only)
Write-Host "== [5/7] CMake configure + build (target daw_vst_bridge) =="
$BUILD_DIR = Join-Path $NB_DIR "build"
New-Item -ItemType Directory -Force $BUILD_DIR | Out-Null
cmake -S $NB_DIR -B $BUILD_DIR `
cmake -S $NB_DIR -B $BUILD_DIR -G $VS_GEN -A $VS_ARCH `
-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
cmake --build $BUILD_DIR --config Release --target daw_vst_bridge -- /m
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 =="
Write-Host "== [6/7] 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 =="
# 7. Copy fluidsynth runtime DLL next to sidecar (Tauri externalBin bundles only the exe)
Write-Host "== [7/7] Copy libfluidsynth-3.dll =="
$FS_DLL = Join-Path $BUILD_DIR "vcpkg_installed\x64-windows\bin\libfluidsynth-3.dll"
if (-not (Test-Path $FS_DLL)) { $FS_DLL = Join-Path $VCPKG_ROOT "installed\x64-windows\bin\libfluidsynth-3.dll" }
if (Test-Path $FS_DLL) {
Copy-Item $FS_DLL (Join-Path $BIN_DIR "libfluidsynth-3.dll") -Force
Copy-Item $FS_DLL (Join-Path $BUILD_DIR "Release\libfluidsynth-3.dll") -Force
Write-Host " libfluidsynth-3.dll copied to src-tauri\binaries"
} else {
Write-Host "WARN: libfluidsynth-3.dll not found - bridge needs it at runtime" -ForegroundColor Yellow
}
Write-Host "== DONE: sidecar + DLL in src-tauri\binaries =="
+8
View File
@@ -34,10 +34,14 @@ 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")
Set-Location (Join-Path $PSScriptRoot "..\..") # restore CWD (child script must not leak it)
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
}
if (-not (Test-Path "src-tauri\binaries\libfluidsynth-3.dll")) {
Write-Host "ERROR: libfluidsynth-3.dll missing (bridge runtime DLL)" -ForegroundColor Red; exit 1
}
Write-Host "== [6/7] VC++ Redistributable for hooks.nsh =="
if (-not (Test-Path src-tauri\vc_redist.x64.exe)) {
@@ -51,6 +55,10 @@ if (-not (Test-Path "src-tauri\binaries\daw_vst_bridge-x86_64-pc-windows-msvc.ex
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
# Frontend host: copy template -> src-tauri/ui/index.html (giu Tauri context,
# document.write UI engine; neu thieu -> window mat __TAURI__ -> bridge chet).
Copy-Item buildscriptsfrontend_host_index.html src-tauriuiindex.html -Force
if (-not (Test-Path src-tauriuiindex.html)) { Write-Host "ERROR: frontend host missing" -ForegroundColor Red; exit 1 }
npx tauri build
Write-Host ""
+45 -7
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.20)
project(daw_vst_bridge LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 1. VST3 SDK (Steinberg, vendored as git submodule NOT in vcpkg)
@@ -19,15 +19,24 @@ endif()
# Linux/macOS: pkg-config duoc dung (spec goc).
if(WIN32)
find_path(FLUIDSYNTH_INCLUDE_DIR fluidsynth.h)
find_library(FLUIDSYNTH_LIBRARY NAMES fluidsynth libfluidsynth)
find_library(FLUIDSYNTH_LIBRARY NAMES fluidsynth libfluidsynth libfluidsynth-3)
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)
# sfizz: built from source (native_bridge/sfizz-src, see build/_sfizz.bat)
# the sfizz port is NOT in vcpkg upstream. Fall back to vcpkg if present.
set(SFIZZ_SRC_BUILD "${CMAKE_CURRENT_SOURCE_DIR}/sfizz-src/build/library/lib/Release")
if(EXISTS "${SFIZZ_SRC_BUILD}/sfizz_static.lib")
file(GLOB SFIZZ_SRC_LIBS "${SFIZZ_SRC_BUILD}/*.lib")
set(SFIZZ_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/sfizz-src/src")
set(SFIZZ_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/sfizz-src/src")
set(SFIZZ_LIBRARY ${SFIZZ_SRC_LIBS})
else()
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")
message(FATAL_ERROR "sfizz not found — build native_bridge/sfizz-src first")
endif()
endif()
else()
find_package(PkgConfig REQUIRED)
@@ -37,18 +46,27 @@ endif()
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/include
${FLUIDSYNTH_INCLUDE_DIRS}
${SFIZZ_INCLUDE_DIRS}
${FLUIDSYNTH_INCLUDE_DIR}
${SFIZZ_INCLUDE_DIR}
)
add_executable(daw_vst_bridge
src/main.cpp
src/NativeInstrumentEngine.cpp
src/SharedMemoryIPC.cpp
src/Vst3Instrument.cpp
)
# VST3 SDK hosting: sdk_hosting lacks the platform module loader module_win32.cpp
# is added to the bridge itself (same approach as the audiohost sample).
if(VST3_SDK_TARGET)
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET})
target_compile_definitions(daw_vst_bridge PRIVATE HAVE_VST3SDK=1)
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET} sdk_hosting)
if(WIN32)
target_sources(daw_vst_bridge PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
)
endif()
endif()
if(WIN32)
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY})
@@ -57,3 +75,23 @@ if(WIN32)
else()
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
endif()
# gui_probe: standalone VST3 GUI attach probe (debug tool, not shipped)
add_executable(gui_probe
src/gui_probe.cpp
src/NativeInstrumentEngine.cpp
src/SharedMemoryIPC.cpp
src/Vst3Instrument.cpp
)
if(VST3_SDK_TARGET)
target_compile_definitions(gui_probe PRIVATE HAVE_VST3SDK=1)
target_link_libraries(gui_probe PRIVATE ${VST3_SDK_TARGET} sdk_hosting)
if(WIN32)
target_sources(gui_probe PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
)
endif()
endif()
if(WIN32)
target_link_libraries(gui_probe PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
endif()
@@ -7,6 +7,7 @@
#include <sfizz.hpp>
#include <map>
#include <mutex>
#include <memory>
#include <string>
#include <vector>
@@ -53,7 +54,7 @@ public:
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
private:
sfizz::Synth sfizzSynth;
sfz::Sfizz sfizzSynth;
};
// Multi-instrument host (A10): one engine instance per MIDI channel, so two
@@ -74,11 +75,12 @@ public:
// 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(); }
size_t count() const { std::lock_guard<std::mutex> lock(mu_); return channels_.size(); }
private:
static std::unique_ptr<INativeInstrument> create_instrument(InstrumentType type);
mutable std::mutex mu_;
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_;
+4
View File
@@ -2,6 +2,10 @@
#pragma once
#include <cstdint>
#ifdef _WIN32
#include <windows.h>
#endif
#define AUDIO_BLOCK_SIZE 256
// WARNING: volatile is NOT a sync primitive. Real impl should use an
+2 -4
View File
@@ -29,10 +29,8 @@ public:
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*
// pimpl: all Steinberg SDK types live in Vst3Instrument.cpp (Vst3HostState).
void* state_;
std::string path_;
double sampleRate_;
uint32_t maxBlockSize_;
+41 -29
View File
@@ -3,6 +3,11 @@
#include <fluidsynth.h>
#include <sfizz.hpp>
#include "Vst3Instrument.h"
// void* members keep fluid types out of the public header; cast here.
#define FS_SYNTH (static_cast<fluid_synth_t*>(synth))
#define FS_SETTINGS (static_cast<fluid_settings_t*>(settings))
#include <cstring>
#include <iostream>
@@ -14,24 +19,24 @@ FluidSynthInstrument::FluidSynthInstrument()
: settings(nullptr), synth(nullptr), sfontId(-1) {}
FluidSynthInstrument::~FluidSynthInstrument() {
if (synth) delete_fluid_synth(synth);
if (settings) delete_fluid_settings(settings);
if (synth) delete_fluid_synth(FS_SYNTH);
if (settings) delete_fluid_settings(FS_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; }
if (synth) { delete_fluid_synth(FS_SYNTH); synth = nullptr; }
if (settings) { delete_fluid_settings(FS_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);
fluid_settings_setnum(FS_SETTINGS, "synth.sample-rate", sampleRate);
fluid_settings_setint(FS_SETTINGS, "synth.polyphony", 256);
fluid_settings_setint(FS_SETTINGS, "synth.verbose", 0);
synth = new_fluid_synth(FS_SETTINGS);
if (!synth) return false;
sfontId = fluid_synth_sfload(synth, path.c_str(), 1);
sfontId = fluid_synth_sfload(FS_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);
fluid_synth_program_select(FS_SYNTH, ch, sfontId, 0, 0);
}
return true;
}
@@ -42,35 +47,35 @@ bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) {
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);
fluid_synth_bank_select(FS_SYNTH, channel, bank);
fluid_synth_program_change(FS_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);
fluid_synth_noteon(FS_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);
fluid_synth_noteoff(FS_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);
fluid_synth_cc(FS_SYNTH, channel, cc, value);
}
void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
if (!synth) return;
fluid_synth_program_change(synth, channel, program);
fluid_synth_program_change(FS_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);
fluid_synth_pitch_bend(FS_SYNTH, channel, bend14);
}
bool FluidSynthInstrument::openGUI(void* parentWindowHandle) {
@@ -81,7 +86,7 @@ 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);
fluid_synth_write_float(FS_SYNTH, numSamples, outputL, 0, 1, outputR, 0, 1);
}
// -----------------------------------------------------------------
@@ -109,18 +114,15 @@ void SfizzInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleO
}
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));
sfizzSynth.cc(0, static_cast<int>(cc), static_cast<int>(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.
sfizzSynth.programChange(0, static_cast<int>(program));
}
void SfizzInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
sfizzSynth.pitchWheel(static_cast<int>(bend14));
sfizzSynth.pitchWheel(0, static_cast<int>(bend14));
}
bool SfizzInstrument::openGUI(void* parentWindowHandle) { return false; }
@@ -128,7 +130,7 @@ void SfizzInstrument::closeGUI() {}
void SfizzInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
float* channels[2] = { outputL, outputR };
sfizzSynth.renderBlock(channels, numSamples);
sfizzSynth.renderBlock(channels, numSamples, 1); // numOutputs=1 = stereo L/R pair (2 ch)
}
// -----------------------------------------------------------------
@@ -138,9 +140,8 @@ std::unique_ptr<INativeInstrument> InstrumentEngineManager::create_instrument(In
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::VST3: return std::make_unique<Vst3Instrument>();
// ponytail: VST2 host (VST2.4 SDK, Steinberg discontinued) not implemented.
case InstrumentType::VST2:
default: return nullptr;
}
@@ -152,30 +153,41 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
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);
else if (type == InstrumentType::VST3)
loaded = static_cast<Vst3Instrument*>(inst.get())->loadPlugin(path, sampleRate);
if (!loaded) return false;
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
if (!inst->init(sampleRate, blockSize)) return false;
// Replacing an existing instrument drops its voices with the old engine.
// Load may run on a detached thread (VST3 init is slow): only the map
// write is under the mutex so renderAll on the audio loop never stalls.
{
std::lock_guard<std::mutex> lock(mu_);
channels_[channel] = std::move(inst);
}
return true;
}
INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
std::lock_guard<std::mutex> lock(mu_);
auto it = channels_.find(channel);
return it == channels_.end() ? nullptr : it->second.get();
}
void InstrumentEngineManager::allNotesOff() {
std::lock_guard<std::mutex> lock(mu_);
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::lock_guard<std::mutex> lock(mu_);
std::memset(outputL, 0, numSamples * sizeof(float));
std::memset(outputR, 0, numSamples * sizeof(float));
if (channels_.empty()) return;
+13
View File
@@ -23,6 +23,19 @@ ShmHandle* shm_open(const char* name) {
if (!h->view) { CloseHandle(h->map); delete h; return nullptr; }
return h;
}
// Creates the mapping (used by tests/self-check; the DAW normally owns it).
ShmHandle* shm_create(const char* name) {
ShmHandle* h = new ShmHandle();
int wlen = MultiByteToWideChar(CP_UTF8, 0, name, -1, nullptr, 0);
std::wstring wname(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, name, -1, &wname[0], wlen);
h->map = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
sizeof(SharedAudioBufferIPC), wname.c_str());
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);
+457 -64
View File
@@ -1,4 +1,4 @@
// native_bridge/src/Vst3Instrument.cpp
// native_bridge/src/Vst3Instrument.cpp
// VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/).
//
// Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every
@@ -10,16 +10,25 @@
#include "Vst3Instrument.h"
#ifdef HAVE_VST3SDK
#include "public.sdk/source/main/pluginfactory.h"
#include "pluginterfaces/base/ibstream.h"
#include "public.sdk/source/vst/hosting/module.h"
#include "public.sdk/source/vst/hosting/hostclasses.h"
#include "public.sdk/source/vst/hosting/processdata.h"
#include "public.sdk/source/vst/hosting/eventlist.h"
#include "public.sdk/source/vst/hosting/parameterchanges.h"
#include "pluginterfaces/vst/ivstaudioprocessor.h"
#include "pluginterfaces/vst/ivstcomponent.h"
#include "pluginterfaces/vst/ivsteditcontroller.h"
#include "pluginterfaces/vst/ivstmidicontrollers.h"
#include "pluginterfaces/vst/ivstprocesscontext.h"
#include "pluginterfaces/vst/ivstevents.h"
#include "pluginterfaces/vst/ivstmessage.h"
#include "pluginterfaces/gui/iplugview.h"
#include "public.sdk/source/common/pluginview.h"
#include <cstring>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#endif
@@ -27,60 +36,155 @@
// With-SDK implementation
// ---------------------------------------------------------------------------
#ifdef HAVE_VST3SDK
#include "public.sdk/source/main/module.h"
namespace {
using Steinberg::tresult;
using Steinberg::kResultOk;
using Steinberg::kResultTrue;
using Steinberg::kResultFalse;
using Steinberg::kNoInterface;
using Steinberg::FUnknownPtr;
using Steinberg::IPtr;
using Steinberg::owned;
using Steinberg::FIDString;
using Steinberg::IPlugView;
using Steinberg::kPlatformTypeHWND;
using Steinberg::int16;
using Steinberg::int32;
using Steinberg::uint32;
using Steinberg::uint16;
using Steinberg::Vst::IComponent;
using Steinberg::Vst::IEditController;
using Steinberg::Vst::IAudioProcessor;
using Steinberg::Vst::IComponentHandler;
using Steinberg::IPluginBase;
using Steinberg::Vst::IConnectionPoint;
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;
using Steinberg::Vst::ProcessSetup;
using Steinberg::Vst::ProcessContext;
using Steinberg::Vst::HostProcessData;
using Steinberg::Vst::EventList;
using Steinberg::Vst::ParameterChanges;
using Steinberg::Vst::HostApplication;
using Steinberg::Vst::ParamID;
using Steinberg::Vst::ParamValue;
using Steinberg::Vst::IMidiMapping;
using Steinberg::Vst::IParamValueQueue;
using Steinberg::Vst::kNoParamId;
using Steinberg::Vst::BusInfo;
using Steinberg::Vst::kAudio;
using Steinberg::Vst::kInput;
using Steinberg::Vst::kOutput;
using Steinberg::Vst::kRealtime;
using Steinberg::Vst::kSample32;
using Steinberg::Vst::kPitchBend;
using Steinberg::Vst::CtrlNumber;
// VST3 spec §MIDI: host-side tag scheme for MIDI CC / pitch bend / program
// change parameters. NOT provided by the SDK (verified 3.8.1) — the host
// defines them. Note: using-declaration of the real VST3 constants would not
// compile; these are the spec values.
constexpr ParamID kHostMidiCC = 0x1000; // + controller number
constexpr ParamID kHostMidiPitchBend = 0x2000; // + channel
constexpr ParamID kHostMidiProgramChange = 0x3000;
// 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 {
tresult queryInterface(const char*, void** v) override {
*v = nullptr;
return Steinberg::kNoInterface;
return 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; }
tresult beginEdit(ParamID) override { return kResultOk; }
tresult performEdit(ParamID, ParamValue) override { return kResultOk; }
tresult endEdit(ParamID) override { return kResultOk; }
tresult restartComponent(Steinberg::int32) override { return kResultOk; }
};
// Bundle both components in one factory entry so module.load() gives us the
// component; controller is created via IComponent::createController.
// Minimal IPlugFrame so plugins can resize their editor view.
class HostPlugFrame : public Steinberg::IPlugFrame {
public:
tresult queryInterface(const char*, void** v) override {
*v = nullptr;
return kNoInterface;
}
Steinberg::uint32 addRef() override { return 1; }
Steinberg::uint32 release() override { return 1; }
tresult resizeView(Steinberg::IPlugView* view, Steinberg::ViewRect* newSize) override {
if (view && newSize) view->onSize(newSize);
return kResultOk;
}
};
// All Steinberg SDK objects live here (pimpl — Vst3Instrument.h stays SDK-free).
struct Vst3HostState {
// module must be destroyed LAST: it owns the plugin factory that created
// component/controller (member order ⇒ destroyed in reverse).
VST3::Hosting::Module::Ptr module;
IPtr<IComponent> component;
IPtr<IEditController> controller;
IPtr<HostApplication> hostApp;
HostProcessData processData;
EventList eventList;
ParameterChanges paramChanges;
ProcessContext processContext;
IPtr<IPlugView> view;
HostPlugFrame plugFrame;
HostComponentHandler componentHandler;
int32 outputChannels = 2;
bool controllerIsComponent = false; // single-component plugin: controller == component
};
// Resolve the plugin's ParamID for a MIDI CC / pitch-bend, preferring the
// plugin's IMidiMapping assignment (audiohost/miditovst.h pattern), falling
// back to the VST3 legacy tag scheme. Returns kNoParamId when unmapped.
ParamID midiControllerTag(IPtr<IEditController>& controller, int16 channel,
int16 ctrlNumber, ParamID legacyTag) {
if (controller) {
FUnknownPtr<IMidiMapping> mm(controller.get());
ParamID tag = kNoParamId;
if (mm &&
mm->getMidiControllerAssignment(0, channel, (CtrlNumber)ctrlNumber, tag) ==
kResultTrue &&
tag != kNoParamId)
return tag;
}
return legacyTag;
}
} // namespace
#endif
Vst3Instrument::Vst3Instrument()
: module_(nullptr),
processor_(nullptr),
controller_(nullptr),
view_(nullptr),
: state_(nullptr),
path_(),
sampleRate_(44100.0),
maxBlockSize_(256),
loaded_(false),
guiAttached_(false) {}
Vst3Instrument::~Vst3Instrument() { closeGUI(); }
Vst3Instrument::~Vst3Instrument() {
#ifdef HAVE_VST3SDK
if (!state_) return;
closeGUI();
auto* s = static_cast<Vst3HostState*>(state_);
if (s->component) {
FUnknownPtr<IAudioProcessor> processor(s->component);
if (processor) processor->setProcessing(false);
s->component->setActive(false);
s->component->terminate();
}
// Single-component plugins: controller == component, already terminated.
if (s->controller && !s->controllerIsComponent) s->controller->terminate();
s->processData.unprepare();
delete s;
state_ = nullptr;
#endif
}
bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
#ifndef HAVE_VST3SDK
@@ -88,15 +192,159 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double 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;
using namespace VST3::Hosting;
std::string err;
std::cerr << "[dbg] loadPlugin: Module::create ..." << std::endl;
Module::Ptr module = Module::create(path, err);
if (!module) {
std::cerr << "[Vst3Instrument] Module::create failed: " << err << std::endl;
return false;
}
const PluginFactory& factory = module->getFactory();
std::cerr << "[dbg] loadPlugin: Module::create OK" << std::endl;
// Pick the first Audio Module class; prefer an Instrument subcategory.
// classInfos() returns a TEMPORARY vector (by value) — never keep a
// pointer into it; it dangles after the range-for and reading
// chosen->ID()/name() is UB (crashed 0xC0000005). Copy instead.
ClassInfo chosen;
bool haveChosen = false;
{
auto infos = factory.classInfos();
for (const ClassInfo& ci : infos) {
if (ci.category() != kVstAudioEffectClass) continue;
if (!haveChosen) { chosen = ci; haveChosen = true; }
if (ci.subCategoriesString().find("Instrument") != std::string::npos) {
chosen = ci;
break;
}
}
}
if (!haveChosen) {
std::cerr << "[Vst3Instrument] no Audio Module class in " << path << std::endl;
return false;
}
std::cerr << "[dbg] loadPlugin: createInstance ..." << std::endl;
std::cerr << "[dbg] loadPlugin: chosen name=" << chosen.name() << " category=" << chosen.category()
<< " subcat=" << chosen.subCategoriesString() << " idbytes=";
{
const unsigned char* cid2 = (const unsigned char*)chosen.ID().data();
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)cid2[b] << ' ';
std::cerr << std::dec << std::endl;
}
IPtr<IComponent> component = factory.createInstance<IComponent>(chosen.ID());
if (!component) {
std::cerr << "[Vst3Instrument] createInstance<IComponent> failed" << std::endl;
return false;
}
IPtr<HostApplication> hostApp = owned(new HostApplication());
std::cerr << "[dbg] loadPlugin: initialize ..." << std::endl;
FUnknownPtr<IPluginBase> plugBase(component.get());
if (!plugBase || plugBase->initialize(hostApp) != kResultOk) {
std::cerr << "[Vst3Instrument] component initialize failed" << std::endl;
return false;
}
// Edit controller: either the component itself (single-component) or a
// separate factory instance (plugprovider.cpp pattern).
IPtr<IEditController> controller;
bool isSingle = false;
if (component->queryInterface(IEditController::iid, (void**)&controller) == kResultTrue) {
isSingle = true;
} else {
Steinberg::TUID cid = {};
tresult cidRes = component->getControllerClassId(cid);
std::cerr << "[dbg] loadPlugin: getControllerClassId=" << (int)cidRes
<< " cid=";
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)(unsigned char)cid[b] << ' ';
std::cerr << std::dec << std::endl;
if (cidRes == kResultTrue || cidRes == kResultOk) {
controller = factory.createInstance<IEditController>(VST3::UID(cid));
std::cerr << "[dbg] loadPlugin: controller from factory=" << (controller ? 1 : 0) << std::endl;
if (controller) {
FUnknownPtr<IPluginBase> ctrlBase(controller.get());
if (!ctrlBase || ctrlBase->initialize(hostApp) != kResultOk) controller = nullptr;
}
}
}
if (!controller) {
std::cerr << "[Vst3Instrument] no edit controller for " << path << std::endl;
return false;
}
// State allocated up-front so the component handler (which the controller
// keeps a pointer to) lives in the final state object.
std::unique_ptr<Vst3HostState> s(new Vst3HostState());
s->controllerIsComponent = isSingle;
controller->setComponentHandler(&s->componentHandler);
// Connect component <-> controller for parameter sync (both directions).
FUnknownPtr<IConnectionPoint> compCP(component);
FUnknownPtr<IConnectionPoint> ctrlCP(controller);
if (compCP && ctrlCP) {
compCP->connect(ctrlCP);
ctrlCP->connect(compCP);
}
// Audio processor: setup → activate → start processing (audioclient.cpp).
FUnknownPtr<IAudioProcessor> processor(component);
if (!processor) {
std::cerr << "[Vst3Instrument] no IAudioProcessor" << std::endl;
return false;
}
std::cerr << "[dbg] loadPlugin: setupProcessing ..." << std::endl;
ProcessSetup setup{kRealtime, kSample32, (int32)maxBlockSize_, sampleRate};
if (processor->setupProcessing(setup) != kResultOk) {
std::cerr << "[Vst3Instrument] setupProcessing failed" << std::endl;
return false;
}
std::cerr << "[dbg] loadPlugin: setActive ..." << std::endl;
if (component->setActive(true) != kResultOk) {
std::cerr << "[Vst3Instrument] setActive failed" << std::endl;
return false;
}
processor->setProcessing(true);
// Build per-bus buffers sized maxBlockSize_ (HostProcessData owns them;
// we must NOT override channelBuffers with external pointers — unprepare
// would delete[] them).
std::cerr << "[dbg] loadPlugin: processData.prepare ..." << std::endl;
if (!s->processData.prepare(*component, (int32)maxBlockSize_, kSample32)) {
std::cerr << "[Vst3Instrument] processData.prepare failed" << std::endl;
return false;
}
// Count output channels of bus 0 (mono plugins duplicate ch0 → both outs).
int32 outChannels = 2;
if (component->getBusCount(kAudio, kOutput) > 0) {
BusInfo bi = {};
if (component->getBusInfo(kAudio, kOutput, 0, bi) == kResultOk && bi.channelCount > 0)
outChannels = bi.channelCount;
}
std::cerr << "[dbg] loadPlugin: prepare OK, wiring state" << std::endl;
s->module = std::move(module);
s->component = std::move(component);
s->controller = std::move(controller);
s->hostApp = std::move(hostApp);
s->outputChannels = outChannels;
s->controllerIsComponent = isSingle;
s->processContext.sampleRate = sampleRate;
s->processContext.tempo = 120.0;
s->processContext.timeSigNumerator = 4;
s->processContext.timeSigDenominator = 4;
s->processContext.state =
ProcessContext::kPlaying | ProcessContext::kTempoValid | ProcessContext::kTimeSigValid |
ProcessContext::kProjectTimeMusicValid;
state_ = s.release();
path_ = path;
sampleRate_ = sampleRate;
loaded_ = true;
std::cout << "[Vst3Instrument] loaded " << path << " (" << chosen.name()
<< ") outCh=" << outChannels << std::endl;
return true;
#endif
}
@@ -116,10 +364,21 @@ void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, ui
#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.
auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->component) return;
Event e = {};
e.busIndex = 0;
e.sampleOffset = (int32)sampleOffset;
e.ppqPosition = 0;
e.flags = Event::kIsLive;
e.type = Event::kNoteOnEvent;
e.noteOn.channel = (int16)channel;
e.noteOn.pitch = (int16)pitch;
e.noteOn.tuning = 0.f;
e.noteOn.velocity = velocity;
e.noteOn.length = 0;
e.noteOn.noteId = 0; // some plugins (DUNE3) track notes by id; -1 may be ignored
s->eventList.addEvent(e);
#endif
}
@@ -128,8 +387,20 @@ void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOf
#ifndef HAVE_VST3SDK
return;
#else
if (!processor_) return;
// TODO(A6): queue NoteOffEvent (see noteOn)
auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->component) return;
Event e = {};
e.busIndex = 0;
e.sampleOffset = (int32)sampleOffset;
e.ppqPosition = 0;
e.flags = Event::kIsLive;
e.type = Event::kNoteOffEvent;
e.noteOff.channel = (int16)channel;
e.noteOff.pitch = (int16)pitch;
e.noteOff.velocity = 0.f;
e.noteOff.noteId = -1;
e.noteOff.tuning = 0.f;
s->eventList.addEvent(e);
#endif
}
@@ -138,8 +409,16 @@ void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value
#ifndef HAVE_VST3SDK
return;
#else
if (!controller_) return;
// TODO(A6): controller_->setParamNormalized(kMidiCC | cc, value/127.0)
auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return;
ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)cc,
kHostMidiCC | (ParamID)(cc & 0x7F));
ParamValue v = value / 127.0;
// setParamNormalized covers single-component plugins; the param queue
// reaches split plugins that read inputParameterChanges inside process().
s->controller->setParamNormalized(tag, v);
int32 idx = 0;
if (IParamValueQueue* q = s->paramChanges.addParameterData(tag, idx)) q->addPoint(0, v, idx);
#endif
}
@@ -148,8 +427,15 @@ void Vst3Instrument::programChange(uint32_t channel, uint32_t program) {
#ifndef HAVE_VST3SDK
return;
#else
if (!controller_) return;
// TODO(A6): setParamNormalized(kMidiProgramChange | program, ...)
auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return;
ParamID tag = kHostMidiProgramChange | (ParamID)(channel & 0xF);
// ponytail: normalized value should be program/(count-1) from the program
// list param stepCount; 127ths is a reasonable approximation.
ParamValue v = (program & 0x7F) / 127.0;
s->controller->setParamNormalized(tag, v);
int32 idx = 0;
if (IParamValueQueue* q = s->paramChanges.addParameterData(tag, idx)) q->addPoint(0, v, idx);
#endif
}
@@ -158,8 +444,14 @@ void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
#ifndef HAVE_VST3SDK
return;
#else
if (!controller_) return;
// TODO(A6): setParamNormalized(kMidiPitchBend, bend14/16383.0)
auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return;
ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)kPitchBend,
kHostMidiPitchBend | (ParamID)(channel & 0xF));
ParamValue v = bend14 / 16383.0;
s->controller->setParamNormalized(tag, v);
int32 idx = 0;
if (IParamValueQueue* q = s->paramChanges.addParameterData(tag, idx)) q->addPoint(0, v, idx);
#endif
}
@@ -168,11 +460,54 @@ bool Vst3Instrument::openGUI(void* parentWindowHandle) {
(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;
auto* s = static_cast<Vst3HostState*>(state_);
std::cerr << "[dbg] openGUI hwnd=" << (void*)(uintptr_t)parentWindowHandle
<< " controller=" << (s ? (s->controller ? 1 : 0) : -1) << std::endl;
if (!s || !s->controller || !parentWindowHandle) return false;
if (s->view && guiAttached_) return true;
IPlugView* rawView = nullptr;
tresult qi = s->controller->queryInterface(IPlugView::iid, (void**)&rawView);
{
const unsigned char* iidb = (const unsigned char*)&IPlugView::iid;
std::cerr << "[dbg] openGUI: IPlugView::iid bytes=";
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)iidb[b] << ' ';
std::cerr << std::dec << std::endl;
const unsigned char* eidb = (const unsigned char*)&IEditController::iid;
std::cerr << "[dbg] openGUI: IEditController::iid bytes=";
for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)eidb[b] << ' ';
std::cerr << std::dec << std::endl;
}
std::cerr << "[dbg] openGUI: controller qi IPlugView=" << (int)qi << " raw=" << (void*)rawView << " isSingle=" << (s->controllerIsComponent ? 1 : 0) << std::endl;
FUnknownPtr<IPlugView> view(rawView);
if (!view) {
// Mot so plugin khong expose IPlugView tren edit controller; thu component.
std::cerr << "[dbg] openGUI: controller no IPlugView, trying component" << std::endl;
IPlugView* rawViewC = nullptr;
tresult qic = s->component->queryInterface(IPlugView::iid, (void**)&rawViewC);
std::cerr << "[dbg] openGUI: component qi IPlugView=" << (int)qic << " raw=" << (void*)rawViewC << std::endl;
view = FUnknownPtr<IPlugView>(rawViewC);
}
if (!view) {
// Official editorhost.cpp pattern: IEditController::createView(kEditor).
// JUCE-based plugins (Scaler2) expose the editor only this way.
std::cerr << "[dbg] openGUI: qi failed, trying controller->createView(kEditor)" << std::endl;
view = owned(s->controller->createView(Steinberg::Vst::ViewType::kEditor));
std::cerr << "[dbg] openGUI: createView view=" << (view ? "ok" : "null") << std::endl;
}
if (!view) { std::cerr << "[dbg] openGUI: no IPlugView" << std::endl; return false; }
view->setFrame(&s->plugFrame);
tresult ts = view->isPlatformTypeSupported(kPlatformTypeHWND);
std::cerr << "[dbg] openGUI: isPlatformTypeSupported=" << (int)ts << std::endl;
if (ts != kResultTrue && ts != kResultOk) return false;
tresult ta = view->attached(parentWindowHandle, kPlatformTypeHWND);
std::cerr << "[dbg] openGUI: attached=" << (int)ta << std::endl;
if (ta != kResultOk) return false;
s->view = view;
guiAttached_ = true;
// ponytail: the bridge loop is a worker thread without a Windows message
// pump — some editors may not repaint until the first native event; a
// future version can spin a dedicated UI thread + pump.
return true;
#endif
}
@@ -180,10 +515,9 @@ void Vst3Instrument::closeGUI() {
#ifndef HAVE_VST3SDK
return;
#else
if (view_ && guiAttached_) {
// view_->removed(); view_->setFrame(nullptr);
}
view_ = nullptr;
auto* s = static_cast<Vst3HostState*>(state_);
if (s && s->view && guiAttached_) s->view->removed();
if (s) s->view = nullptr;
guiAttached_ = false;
#endif
}
@@ -193,7 +527,66 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
#ifndef HAVE_VST3SDK
return;
#else
// TODO(A6): ProcessData with 2 output buffers + event list; called by
// InstrumentEngineManager::renderAll via processAudioBlock.
auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->component || numSamples == 0) return;
FUnknownPtr<IAudioProcessor> processor(s->component);
if (!processor) return;
s->processData.processMode = kRealtime;
s->processData.numSamples = (int32)numSamples;
s->processData.inputEvents = &s->eventList;
s->processData.inputParameterChanges = &s->paramChanges;
s->processData.processContext = &s->processContext;
s->processContext.projectTimeSamples += numSamples;
s->processContext.projectTimeMusic =
(double)s->processContext.projectTimeSamples / s->processContext.sampleRate *
(s->processContext.tempo / 60.0);
static uint32_t dbgN = 0;
static int dbgMaxShown = 0;
// VST3: host buffers must be zeroed (silence) before process — plugins
// that skip output leave garbage otherwise (EZkeys 2 -> huge noise).
if (s->processData.numOutputs > 0) {
for (int32 b = 0; b < s->processData.numOutputs; ++b) {
const Steinberg::Vst::AudioBusBuffers& ob = s->processData.outputs[b];
for (int32 c = 0; c < ob.numChannels; ++c)
if (ob.channelBuffers32[c])
std::memset(ob.channelBuffers32[c], 0, numSamples * sizeof(float));
}
}
tresult pr = processor->process(s->processData);
float mx = 0.f;
if (pr == kResultOk && s->processData.numOutputs > 0) {
const Steinberg::Vst::AudioBusBuffers& out = s->processData.outputs[0];
const float* buf0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr;
const float* buf1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr;
if (buf0) std::memcpy(outputL, buf0, numSamples * sizeof(float));
else std::memset(outputL, 0, numSamples * sizeof(float));
if (buf1) std::memcpy(outputR, buf1, numSamples * sizeof(float));
else if (buf0) std::memcpy(outputR, buf0, numSamples * sizeof(float)); // mono → stereo
else std::memset(outputR, 0, numSamples * sizeof(float));
for (uint32_t i = 0; i < numSamples; ++i) {
float v = outputL[i] < 0 ? -outputL[i] : outputL[i];
if (v > mx) mx = v;
}
} else {
std::memset(outputL, 0, numSamples * sizeof(float));
std::memset(outputR, 0, numSamples * sizeof(float));
}
uint32_t evc = s->eventList.getEventCount();
int evt = -1;
if (evc > 0) {
const Event* e0 = s->eventList.getEventByIndex(0);
evt = e0 ? (int)e0->type : -2;
}
if ((++dbgN % 250) == 0 || (evc > 0 && dbgMaxShown < 30)) {
if (evc > 0) ++dbgMaxShown;
std::cerr << "[dbg] n=" << numSamples << " ev=" << evc << " evt=" << evt
<< " pr=" << (int)pr << " nOut=" << s->processData.numOutputs
<< " mx=" << mx << " ts=" << s->processContext.projectTimeSamples
<< " nch=" << (s->processData.numOutputs > 0 ? s->processData.outputs[0].numChannels : -1) << std::endl;
}
s->eventList.clear();
s->paramChanges.clearQueue();
#endif
}
+141
View File
@@ -0,0 +1,141 @@
// native_bridge/src/gui_probe.cpp — standalone VST3 GUI attach probe.
// Debug tool (NOT shipped with the DAW): loads a VST3 with Vst3Instrument and
// tests openGUI under different thread/pump/parent-window setups so we can
// find why view->attached() hangs inside the bridge.
#include "Vst3Instrument.h"
#ifdef _WIN32
#include <windows.h>
#endif
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <thread>
static void pump_for(int secs) {
auto t0 = std::chrono::steady_clock::now();
while (std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - t0).count() < secs) {
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
Sleep(5);
}
}
static void sleep_for(int secs) { Sleep((DWORD)(secs * 1000)); }
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("usage: gui_probe <plugin.vst3> [variant] [secs] [hwnd]\n"
"variants: main_own | worker_own_nopump | worker_own_pump | worker_foreign_nopump | worker_foreign_pump | bridge_like | same_thread\n");
return 2;
}
std::string path = argv[1];
std::string variant = argc > 2 ? argv[2] : "main_own";
int secs = argc > 3 ? atoi(argv[3]) : 20;
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
// Watchdog: never let a stuck plugin hang this probe forever.
std::thread([secs]() {
Sleep((DWORD)(secs + 10) * 1000);
printf("WATCHDOG-KILL\n");
fflush(stdout);
TerminateProcess(GetCurrentProcess(), 0);
}).detach();
HWND ownHwnd = 0;
{
WNDCLASSEX wc = {};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = DefWindowProc;
wc.hInstance = GetModuleHandle(nullptr);
wc.lpszClassName = "GuiProbeClass";
RegisterClassEx(&wc);
ownHwnd = CreateWindowEx(0, "GuiProbeClass", "GuiProbe", WS_OVERLAPPEDWINDOW,
0, 0, 800, 600, nullptr, nullptr, wc.hInstance, nullptr);
printf("ownHwnd=%p\n", (void*)ownHwnd);
}
void* targetHwnd = (void*)ownHwnd;
if (argc > 4) targetHwnd = (void*)(uintptr_t)strtoull(argv[4], nullptr, 0);
printf("loading %s\n", path.c_str());
fflush(stdout);
Vst3Instrument inst;
bool isDeferred = (variant == "bridge_like" || variant == "same_thread");
bool ok = true;
if (!isDeferred) {
ok = inst.loadPlugin(path, 44100.0);
printf("load=%d\n", ok ? 1 : 0);
fflush(stdout);
if (!ok) return 1;
}
std::atomic<bool> done{false};
std::atomic<bool> result{false};
auto attach_job = [&]() {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
printf("[%s] openGUI start hwnd=%p\n", variant.c_str(), targetHwnd);
fflush(stdout);
bool r = inst.openGUI(targetHwnd);
printf("[%s] openGUI returned=%d\n", variant.c_str(), r ? 1 : 0);
fflush(stdout);
result = r;
done = true;
};
if (variant == "main_own") {
attach_job(); // editorhost pattern: attached on main thread, pump after
pump_for(secs);
} else if (variant == "worker_own_nopump" || variant == "worker_foreign_nopump") {
std::thread t(attach_job);
sleep_for(secs);
if (!done.load()) { printf("RESULT: TIMEOUT (no pump)\n"); return 3; }
} else if (variant == "worker_own_pump" || variant == "worker_foreign_pump") {
std::thread t(attach_job);
pump_for(secs);
if (!done.load()) { printf("RESULT: TIMEOUT (pump concurrent)\n"); return 3; }
} else if (variant == "bridge_like") {
// Bridge pattern: plugin loaded on thread A which then EXITS (COM
// apartment dies), openGUI on separate thread B, main pumps.
std::thread loadA([&]() {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
printf("[bridge_like] loadPlugin on thread A\n");
fflush(stdout);
bool r = inst.loadPlugin(path, 44100.0);
printf("[bridge_like] load=%d (thread A exiting)\n", r ? 1 : 0);
fflush(stdout);
CoUninitialize();
});
loadA.join();
std::thread t(attach_job); // thread B, after A exited
pump_for(secs);
if (!done.load()) { printf("RESULT: TIMEOUT (apartment dead)\n"); return 3; }
} else if (variant == "same_thread") {
// Load and openGUI on the SAME worker thread, kept alive; main pumps.
std::thread t([&]() {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
printf("[same_thread] loadPlugin on worker\n");
fflush(stdout);
bool r = inst.loadPlugin(path, 44100.0);
printf("[same_thread] load=%d\n", r ? 1 : 0);
fflush(stdout);
if (r) attach_job();
});
pump_for(secs);
if (!done.load()) { printf("RESULT: TIMEOUT (same_thread)\n"); return 3; }
} else {
printf("unknown variant %s\n", variant.c_str());
return 2;
}
printf("RESULT: done=%d attached_ok=%d\n", done.load() ? 1 : 0, result.load() ? 1 : 0);
return 0;
}
+186 -6
View File
@@ -9,6 +9,7 @@
#include <windows.h>
#include <mmsystem.h>
#include <process.h>
#include <thread>
#else
#include <unistd.h>
#include <cstdlib>
@@ -17,9 +18,14 @@
#endif
#include <algorithm>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <string>
// --- platform helpers -------------------------------------------------------
@@ -42,6 +48,98 @@ static void sleep_ms(uint32_t ms) {
#endif
}
// B9: native Win32 window for the VST editor (replaces the WebView2 surface —
// the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the
// ChannelWorker thread so the worker's idle message pump services its messages.
static void* create_native_vst_window(const char* title) {
#ifdef _WIN32
static const char* kWndClass = "SonicForge_Native_VST3_Class";
static bool registered = false;
if (!registered) {
WNDCLASSA wc = {};
wc.lpfnWndProc = DefWindowProcA;
wc.hInstance = GetModuleHandleA(nullptr);
wc.lpszClassName = kWndClass;
RegisterClassA(&wc);
registered = true;
}
HWND hwnd = CreateWindowExA(0, kWndClass, title ? title : "VST",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
return (void*)hwnd;
#else
(void)title;
return nullptr;
#endif
}
// Per-channel persistent worker: ONE thread owns the COM STA apartment for that
// channel's instrument for its whole lifetime. loadPlugin and openGUI MUST run
// on the same thread — if the loading thread exits, its apartment dies and
// VST3 plugins that marshal internally (Nexus) hang forever in
// view->attached(). Verified with gui_probe: `bridge_like` (load thread exits,
// openGUI on another) hangs; `same_thread` (load+openGUI on one alive thread)
// returns attached=OK.
class ChannelWorker {
public:
ChannelWorker() {
th_ = std::thread([this] {
#ifdef _WIN32
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
#endif
std::unique_lock<std::mutex> lk(mu_);
for (;;) {
if (stop_ && jobs_.empty()) break;
if (!jobs_.empty()) {
auto job = std::move(jobs_.front());
jobs_.pop_front();
lk.unlock();
job();
lk.lock();
continue;
}
cv_.wait_for(lk, std::chrono::milliseconds(5));
// Pump THIS thread's message queue while idle — VST editor
// windows are created on this thread, their messages must be
// dispatched here or the editor freezes after attach.
lk.unlock();
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
lk.lock();
}
#ifdef _WIN32
CoUninitialize();
#endif
});
}
~ChannelWorker() {
{
std::lock_guard<std::mutex> lk(mu_);
stop_ = true;
}
cv_.notify_all();
if (th_.joinable()) th_.join();
}
void post(std::function<void()> job) {
{
std::lock_guard<std::mutex> lk(mu_);
jobs_.push_back(std::move(job));
}
cv_.notify_all();
}
private:
std::thread th_;
std::mutex mu_;
std::condition_variable cv_;
std::deque<std::function<void()>> jobs_;
bool stop_ = false;
};
int main(int argc, char* argv[]) {
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
@@ -73,6 +171,12 @@ int main(int argc, char* argv[]) {
#endif
InstrumentEngineManager instruments;
// Per-channel persistent workers: loadPlugin + openGUI run on the SAME
// thread whose COM STA apartment stays alive for the channel's lifetime
// (see ChannelWorker comment — a dead apartment hangs Nexus attached()).
std::map<uint32_t, std::unique_ptr<ChannelWorker>> workers;
// B9: native editor windows per channel — keep alive (HWND outlives the job).
std::map<uint32_t, void*> guiWindows;
// 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.
@@ -95,13 +199,16 @@ int main(int argc, char* argv[]) {
if (!inst) return; // channel chưa gán instrument → silent (A10)
switch (evt.command) {
case 0x9:
// sampleOffset LUON LUON = 0 khi den day: events duoc dispatch ngay
// truoc segment chua no (A11 splitting), nen offset tuong doi la 0.
// Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre.
if (evt.velocity > 0)
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, evt.sampleOffset);
inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, 0);
else
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
inst->noteOff(evt.channel, evt.pitch, 0);
break;
case 0x8:
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
inst->noteOff(evt.channel, evt.pitch, 0);
break;
case 0xB: // CC: controller number in pitch, value in data2
inst->controlChange(evt.channel, evt.pitch, evt.data2);
@@ -124,23 +231,50 @@ int main(int argc, char* argv[]) {
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
while (true) {
#ifdef _WIN32
// Message pump: VST editors (Nexus, JUCE-based...) block inside
// view->attached() until the host dispatches messages — openGUI runs
// on a worker thread, so THIS loop must pump concurrently (verified
// with gui_probe: worker-thread openGUI + concurrent pump → attached
// returns kResultOk; without it → hangs forever).
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
#endif
// 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).
// Chay tren persistent worker thread cua channel: VST3 init
// (loadPlugin) co the mat giay — chay dong bo tren audio loop
// lam writeIndex stall > 3s -> Rust tuong bridge chet va restart
// nham (2 bridge cung map SHM -> race control queue / double
// load). assign() chi giu mutex khi ghi map nen renderAll khong
// bao gio stall. openGUI sau nay chay tren CUNG thread nay
// (ChannelWorker) — thread khong bao gio exit nen COM STA
// apartment cua plugin con song (xem ChannelWorker comment).
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)) {
if (!workers[ch]) workers[ch] = std::make_unique<ChannelWorker>();
workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() {
std::cerr << "[dbg] load thread start ch=" << ch
<< " type=" << (int)t << " path=" << path << std::endl;
bool ok = instruments.assign(ch, t, path, sampleRate, block);
std::cerr << "[dbg] assign returned ch=" << ch << " ok=" << (ok ? 1 : 0) << std::endl;
if (ok) {
std::cout << "[NativeBridge] instrument loaded ch=" << ch
<< " type=" << (int)c.arg0 << " " << path << std::endl;
<< " type=" << (int)t << " " << path << std::endl;
} else {
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
<< " type=" << (int)c.arg0 << " " << path << std::endl;
<< " type=" << (int)t << " " << path << std::endl;
}
});
} else if (c.type == 1) { // PANIC
instruments.allNotesOff();
std::cout << "[NativeBridge] PANIC — all notes off" << std::endl;
@@ -154,6 +288,52 @@ int main(int argc, char* argv[]) {
} else if (c.arg0 == 2) { // SET_POSITION (seek while stopped)
playheadSamples = c.arg1;
}
} else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id
// ponytail: per-plugin channel mapping chua co — gan GUI cho
// instrument dau tien duoc load (smoke test = 1 instrument).
// Chay tren CUNG ChannelWorker da load instrument: thread rieng
// cho openGUI lai tao COM apartment moi, con plugin thi song o
// apartment cu da chet (load thread exit) -> Nexus attached()
// hang (gui_probe: bridge_like treo, same_thread OK).
uint32_t guiCh = 16;
for (uint32_t ch = 0; ch < 16; ++ch) {
if (instruments.get(ch)) { guiCh = ch; break; }
}
if (guiCh == 16) {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << c.arg1
<< " plugin=" << c.arg2 << " (no instrument loaded)" << std::endl;
} else {
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
workers[guiCh]->post([&instruments, &guiWindows, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() {
std::cerr << "[dbg] openGUI thread start hwnd=" << arg1
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
void* hwnd = (void*)(uintptr_t)arg1;
#ifdef _WIN32
if (arg1 == 0) {
// B9: bridge tự tạo native window — editor VST3 đính
// vào đây; message pump bởi ChannelWorker idle loop.
hwnd = create_native_vst_window(arg2.c_str());
if (!hwnd) {
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
return;
}
guiWindows[guiCh] = hwnd; // keep window alive
}
#else
(void)guiWindows;
#endif
if (auto* inst = instruments.get(guiCh)) {
if (inst->openGUI(hwnd))
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
else
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
<< " plugin=" << arg2 << std::endl;
}
// Editor windows song tren thread nay — ChannelWorker
// pump message queue khi idle (xem class comment).
});
}
}
}
shmIPC->controlQueueCount = 0;
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
# G2 end-to-end smoke: DAW side creates SHM (mirrors Rust shm.rs), spawns the
# real bridge, pushes LOAD_INSTRUMENT + MIDI note, asserts non-zero audio.
# Usage: python tests/g2_bridge_smoke.py
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SF2 = r"C:\Users\locpham\SonicForgeStudio\app\storage\soundfonts\518e850f-a5d3-4790-b1f9-0c90c203c524.sf2"
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h:
sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v:
sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
ML, MR = 8, 1032
MIDI_Q, MIDI_Q_COUNT = 2064, 2832
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
def peak():
p = 0.0
for i in range(256):
l = struct.unpack_from("<f", buf, ML + i * 4)[0]
r = struct.unpack_from("<f", buf, MR + i * 4)[0]
p = max(p, abs(l), abs(r))
return p
def note_on(ch, pitch, vel):
w32(MIDI_Q_COUNT, 0)
base = MIDI_Q
buf[base] = 0x9; buf[base+1] = ch; buf[base+2] = pitch; buf[base+3] = vel
buf[base+4] = 0; buf[base+5] = 0; buf[base+6] = 0; buf[base+7] = 0
struct.pack_into("<I", buf, base + 8, 0)
w32(MIDI_Q_COUNT, 1)
def poll_peak(seconds=1.5):
# SFZ one-shots (g2_test.sfz) decay to silence in ~200ms; poll fast and
# keep the max so the audible window is not missed by a sparse sleep.
mx = 0.0
t0 = time.time()
while time.time() - t0 < seconds:
p = peak()
if p > mx: mx = p
time.sleep(0.01)
return mx
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "g2_bridge_stdout.txt")
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
results = []
try:
# SF2: load, note, expect non-silent audio
load_instrument(2, SF2)
time.sleep(0.3)
note_on(0, 60, 100)
p = poll_peak()
ok = p > 0.001
results.append(("SF2 note audio", ok, "peak=%.5f" % p))
print("SF2 PEAK=%.5f" % p)
if not ok:
logf.flush()
print("bridge out:", open(logpath, encoding="utf-8").read()[-1500:])
# SFZ on channel 1
load_instrument(3, SFZ, ch=1)
time.sleep(0.5)
note_on(1, 60, 100)
p2 = poll_peak()
ok2 = p2 > 0.001
results.append(("SFZ note audio", ok2, "peak=%.5f" % p2))
print("SFZ PEAK=%.5f" % p2)
if not ok2:
logf.flush()
print("bridge out:", open(logpath, encoding="utf-8").read()[-1500:])
# VST3 (native hosting, no Carla): mda-vst3 bundle from SDK build
VST3 = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "VST3", "Release", "mda-vst3.vst3"))
load_instrument(0, VST3, ch=2)
# wait until bridge reports the load (poll log), up to 10s
deadline = time.time() + 10
while time.time() < deadline:
logf.flush()
if "instrument loaded ch=2" in open(logpath, encoding="utf-8").read():
break
if proc.poll() is not None:
print("BRIDGE DIED rc=", proc.poll())
break
time.sleep(0.2)
note_on(2, 60, 100)
p3 = poll_peak(2.0)
ok3 = p3 > 0.001
results.append(("VST3 note audio", ok3, "peak=%.5f" % p3))
print("VST3 PEAK=%.5f" % p3)
logf.flush()
print("bridge out tail:", open(logpath, encoding="utf-8").read()[-2500:])
if not ok3:
print("bridge out:", open(logpath, encoding="utf-8").read()[-2500:])
finally:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
failed = [r for r in results if not r[1]]
print("G2 RESULT:", "FAIL" if failed else "PASS")
for r in results: print(" %-20s %s %s" % (r[0], "PASS" if r[1] else "FAIL", r[2]))
sys.exit(1 if failed else 0)
+1
View File
@@ -0,0 +1 @@
<region> sample=dummy1.wav key=60 lokey=0 hikey=127
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
# Sequence matrix: which load order crashes?
# usage: repro_seq.py <seq> e.g. "sfz,vst3" "sf2,vst3" "vst3,sfz" "sf2,sfz,vst3"
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SF2 = r"C:\Users\locpham\SonicForgeStudio\app\storage\soundfonts\518e850f-a5d3-4790-b1f9-0c90c203c524.sf2"
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
VST3 = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "VST3", "Release", "mda-vst3.vst3"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h: sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v: sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
seq = sys.argv[1] if len(sys.argv) > 1 else "sfz,vst3"
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "seq_%s.txt" % seq.replace(",", "_"))
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
try:
for i, name in enumerate(seq.split(",")):
if name == "sf2": load_instrument(2, SF2, ch=0)
elif name == "sfz": load_instrument(3, SFZ, ch=1)
elif name == "vst3": load_instrument(0, VST3, ch=2)
time.sleep(0.6)
if proc.poll() is not None:
print("DIED after %s rc=%s" % (name, proc.poll()))
break
else:
time.sleep(2)
print("ALIVE after %s rc=%s" % (seq, proc.poll()))
logf.flush()
print("log:")
print(open(logpath, encoding="utf-8").read()[:1500])
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# Reproduce SFZ-only crash. Load SFZ ch=1, no notes, poll every 0.2s.
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
ML, MR = 8, 1032
MIDI_Q, MIDI_Q_COUNT = 2064, 2832
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h: sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v: sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
def note_on(ch, pitch, vel):
w32(MIDI_Q_COUNT, 0)
base = MIDI_Q
buf[base] = 0x9; buf[base+1] = ch; buf[base+2] = pitch; buf[base+3] = vel
buf[base+4] = 0; buf[base+5] = 0; buf[base+6] = 0; buf[base+7] = 0
struct.pack_into("<I", buf, base + 8, 0)
w32(MIDI_Q_COUNT, 1)
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "repro_sfz.txt")
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
try:
load_instrument(3, SFZ, ch=1)
t0 = time.time()
last_line = ""
while time.time() - t0 < 8:
if proc.poll() is not None:
print("DIED after %.2fs rc=%s" % (time.time() - t0, proc.poll()))
break
time.sleep(0.2)
else:
print("ALIVE after 8s")
logf.flush()
print("log:", open(logpath, encoding="utf-8").read())
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# SFZ: load ch=1, note_on, poll peak for 3s — see if any audio comes out.
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
ML, MR = 8, 1032
MIDI_Q, MIDI_Q_COUNT = 2064, 2832
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h: sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v: sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
def note_on(ch, pitch, vel):
w32(MIDI_Q_COUNT, 0)
base = MIDI_Q
buf[base] = 0x9; buf[base+1] = ch; buf[base+2] = pitch; buf[base+3] = vel
buf[base+4] = 0; buf[base+5] = 0; buf[base+6] = 0; buf[base+7] = 0
struct.pack_into("<I", buf, base + 8, 0)
w32(MIDI_Q_COUNT, 1)
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
def peak():
p = 0.0
for i in range(256):
l = struct.unpack_from("<f", buf, ML + i * 4)[0]
r = struct.unpack_from("<f", buf, MR + i * 4)[0]
p = max(p, abs(l), abs(r))
return p
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "repro_sfz_note.txt")
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
try:
load_instrument(3, SFZ, ch=1)
time.sleep(0.5)
note_on(1, 60, 100)
t0 = time.time()
while time.time() - t0 < 3:
p = peak()
print("t=%.2f peak=%.5f alive=%s" % (time.time() - t0, p, proc.poll() is None))
if proc.poll() is not None:
print("DIED rc=%s" % proc.poll())
break
time.sleep(0.25)
logf.flush()
print("log:", open(logpath, encoding="utf-8").read())
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
# SFZ -> VST3 sequential: does VST3 load after SFZ? dump log periodically.
import ctypes, os, struct, subprocess, sys, time
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
VST3 = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "VST3", "Release", "mda-vst3.vst3"))
SHM_NAME = "SonicForge_DAW_IPC"
SIZE = 11160
ML, MR = 8, 1032
MIDI_Q, MIDI_Q_COUNT = 2064, 2832
CTRL_Q, CTRL_Q_COUNT = 2836, 11156
kern = ctypes.windll.kernel32
PAGE_READWRITE, FILE_MAP_ALL_ACCESS = 0x04, 0xF001F
kern.CreateFileMappingW.restype = ctypes.c_void_p
kern.CreateFileMappingW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_wchar_p]
kern.MapViewOfFile.restype = ctypes.c_void_p
kern.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
kern.UnmapViewOfFile.restype = ctypes.c_int
kern.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
kern.CloseHandle.restype = ctypes.c_int
kern.CloseHandle.argtypes = [ctypes.c_void_p]
h = kern.CreateFileMappingW(None, None, PAGE_READWRITE, 0, SIZE, SHM_NAME)
if not h: sys.exit("CreateFileMappingW failed")
v = kern.MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not v: sys.exit("MapViewOfFile failed")
buf = (ctypes.c_ubyte * SIZE).from_address(v)
def w32(o, x): struct.pack_into("<I", buf, o, x)
def set_str(o, s, cap):
b = s.encode("utf-8")[:cap - 1]
for i, x in enumerate(b): buf[o + i] = x
for i in range(len(b), cap): buf[o + i] = 0
def note_on(ch, pitch, vel):
w32(MIDI_Q_COUNT, 0)
base = MIDI_Q
buf[base] = 0x9; buf[base+1] = ch; buf[base+2] = pitch; buf[base+3] = vel
buf[base+4] = 0; buf[base+5] = 0; buf[base+6] = 0; buf[base+7] = 0
struct.pack_into("<I", buf, base + 8, 0)
w32(MIDI_Q_COUNT, 1)
def load_instrument(itype, path, ch=0):
w32(CTRL_Q_COUNT, 0)
set_str(CTRL_Q + 16, path, 1024)
w32(CTRL_Q + 0, 2); w32(CTRL_Q + 4, itype); w32(CTRL_Q + 8, 0); w32(CTRL_Q + 12, ch)
w32(CTRL_Q_COUNT, 1)
def peak():
p = 0.0
for i in range(256):
l = struct.unpack_from("<f", buf, ML + i * 4)[0]
r = struct.unpack_from("<f", buf, MR + i * 4)[0]
p = max(p, abs(l), abs(r))
return p
env = dict(os.environ, SF_PARENT_PID=str(os.getpid()), SF_SAMPLE_RATE="44100")
logpath = os.path.join(os.path.dirname(BRIDGE), "sfz_then_vst3_v2.txt")
logf = open(logpath, "w", encoding="utf-8")
proc = subprocess.Popen([BRIDGE], env=env, cwd=os.path.dirname(BRIDGE),
stdout=logf, stderr=subprocess.STDOUT, text=True)
try:
load_instrument(3, SFZ, ch=1)
time.sleep(1.0)
load_instrument(0, VST3, ch=2)
t0 = time.time()
while time.time() - t0 < 8:
logf.flush()
log = open(logpath, encoding="utf-8").read()
print("t=%.1f alive=%s vst3_loaded=%s lines=%d" % (
time.time() - t0, proc.poll() is None, "loaded ch=2" in log, log.count("\n")))
if proc.poll() is not None:
print("DIED rc=%s" % proc.poll())
break
time.sleep(1.0)
logf.flush()
print("--- log ---")
print(open(logpath, encoding="utf-8").read()[:2000])
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=3)
except subprocess.TimeoutExpired: proc.kill()
logf.close()
kern.UnmapViewOfFile(v)
kern.CloseHandle(h)
+5
View File
@@ -11,6 +11,11 @@
int main() {
// Use a test-only name to avoid clashing with a live DAW bridge.
ShmHandle* h = shm_open("SonicForge_SelfCheck_IPC");
#ifdef _WIN32
// On Windows the DAW (Rust) creates the mapping; standalone self-check
// creates it here when absent.
if (!h) h = shm_create("SonicForge_SelfCheck_IPC");
#endif
assert(h != nullptr);
SharedAudioBufferIPC* ipc = shm_ptr(h);
-1
View File
@@ -3,7 +3,6 @@
"version-string": "1.0.0",
"dependencies": [
"fluidsynth",
"sfizz",
"pkgconf"
]
}
+235
View File
@@ -13,6 +13,9 @@
"jsdom": "^30.0.1",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@tauri-apps/cli": "^2.11.4"
}
},
"node_modules/@asamuzakjp/css-color": {
@@ -608,6 +611,238 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz",
"integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
"tauri": "tauri.js"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.11.4",
"@tauri-apps/cli-darwin-x64": "2.11.4",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4",
"@tauri-apps/cli-linux-arm64-gnu": "2.11.4",
"@tauri-apps/cli-linux-arm64-musl": "2.11.4",
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.4",
"@tauri-apps/cli-linux-x64-gnu": "2.11.4",
"@tauri-apps/cli-linux-x64-musl": "2.11.4",
"@tauri-apps/cli-win32-arm64-msvc": "2.11.4",
"@tauri-apps/cli-win32-ia32-msvc": "2.11.4",
"@tauri-apps/cli-win32-x64-msvc": "2.11.4"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz",
"integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz",
"integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz",
"integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz",
"integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz",
"integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz",
"integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz",
"integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz",
"integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz",
"integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz",
"integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz",
"integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@types/gensync": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz",
+3
View File
@@ -12,5 +12,8 @@
"jsdom": "^30.0.1",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@tauri-apps/cli": "^2.11.4"
}
}
+4 -2
View File
@@ -12,15 +12,17 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri = { version = "2", features = ["webview-data-url"] }
tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Shared memory (SonicForge_DAW_IPC) for the Native Host Bridge
raw-window-handle = "0.6"
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_System_MemoryManagement",
"Win32_Security",
"Win32_System_Memory",
] }
[profile.release]
@@ -1 +0,0 @@
PLACEHOLDER - duoc thay boi build_windows.ps1 buoc [4/6] (copy dist\daw_engine)
+96 -35
View File
@@ -12,7 +12,7 @@
// và ghi đầy đủ diagnostic vào %APPDATA%/SonicForgeDAW/logs/spawn.log.
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_dialog::{DialogExt, FilePath};
use tauri_plugin_shell::process::CommandChild;
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
@@ -23,6 +23,30 @@ use shm::{Shm, ShmState};
struct EngineProcess(Mutex<Option<CommandChild>>);
struct BridgeProcess(Mutex<Option<CommandChild>>);
/// Kill the currently managed bridge child (if any) — a restart MUST never
/// leave the old bridge running, else two daw_vst_bridge.exe race on the same
/// SHM (double instrument load, double OPEN_GUI, garbled control queue).
fn kill_bridge(app: &AppHandle) -> bool {
let state = app.state::<BridgeProcess>();
let mut guard = match state.0.lock() {
Ok(g) => g,
Err(_) => return false,
};
match guard.take() {
Some(child) => match child.kill() {
Ok(()) => {
println!("Native Host Bridge killed (restart)");
true
}
Err(e) => {
println!("Bridge kill failed: {e}");
false
}
},
None => false,
}
}
/// Audio frame pushed to the WebView (bridge SHM -> `bridge-audio` event).
#[derive(Clone, serde::Serialize)]
struct AudioFrame {
@@ -65,6 +89,8 @@ fn bridge_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
(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()),
(res_dir.join(triple_name), "resource_dir (triple name, bundle)".into()),
(exe_dir.join(triple_name), "exe_dir (triple name, bundle)".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()),
@@ -90,6 +116,9 @@ fn spawn_bridge(
found = Some((path.clone(), label.clone()));
}
}
// B3 DoD: dòng tóm tắt trạng thái bridge trong spawn.log
log_line.push_str(&format!("[daw_vst_bridge] exists={}
", found.is_some()));
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
@@ -109,8 +138,32 @@ fn spawn_bridge(
.env("SF_BLOCK_SIZE", "256")
.spawn()
{
Ok((_rx, child)) => {
Ok((mut rx, child)) => {
app.manage(BridgeProcess(Mutex::new(Some(child))));
// B3: redirect bridge stdout/stderr → %APPDATA%/SonicForgeDAW/logs/bridge.log
// (tauri-shell pipe rồi vứt rx; đọc event ghi file — E5 UI tail được).
let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
let bridge_log = std::path::Path::new(&log_dir)
.join("SonicForgeDAW").join("logs").join("bridge.log");
if let Some(parent) = bridge_log.parent() {
let _ = std::fs::create_dir_all(parent);
}
tauri::async_runtime::spawn(async move {
use std::io::Write;
let mut f = match std::fs::OpenOptions::new().create(true).append(true).open(&bridge_log) {
Ok(f) => f,
Err(_) => return,
};
while let Some(ev) = rx.recv().await {
let bytes: Vec<u8> = match ev {
CommandEvent::Stdout(b) | CommandEvent::Stderr(b) => b,
_ => continue,
};
if let Ok(text) = String::from_utf8(bytes) {
let _ = writeln!(f, "{}", text.trim_end());
}
}
});
println!("Native Host Bridge started ({label}): {}", bridge_exe.display());
true
}
@@ -290,6 +343,9 @@ pub fn run() {
if advanced == Some(true) {
last_change = std::time::Instant::now();
down_emitted = false;
// Bridge đã sống lại sau restart — đóng "sự cố" để lần
// stall sau lại được hưởng 1 restart (mỗi incident 1 lần).
restart_attempts = 0;
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",
@@ -308,6 +364,9 @@ pub fn run() {
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");
// FIX: kill bridge cũ trước — không được để 2 bridge
// cùng map SHM (race control queue / double load).
kill_bridge(&pump_handle);
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)
@@ -436,7 +495,7 @@ fn push_midi_event(
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")
.ok_or_else(|| "midi queue full".to_string())
}
#[tauri::command]
@@ -446,41 +505,27 @@ fn load_native_instrument(app: AppHandle, path: String, instrument_type: u8, cha
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")
.ok_or_else(|| "control queue full".to_string())
}
#[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())?;
// B9: bridge tự tạo native Win32 window cho editor VST3 (không qua
// WebView2 — HTML window cũ vẽ ĐÈ lên GUI plugin). push_control(4,0,0,0)
// với hwnd=0 báo bridge tạo window riêng trên ChannelWorker thread.
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,
let lock = state.0.lock();
match lock {
Ok(guard) => match guard.as_ref() {
Some(shm) => {
let ok = shm.push_control(4, 0, 0, 0, &plugin_id);
eprintln!("open_vst_gui: push_control type=4 hwnd=0 (bridge-native) ok={}", ok);
}
};
#[cfg(not(windows))]
let hwnd = 0u32;
shm.push_control(4, 0, hwnd, 0, &plugin_id)
.then_some(())
.ok_or("control queue full")
None => eprintln!("open_vst_gui: bridge shm unavailable"),
},
Err(e) => eprintln!("open_vst_gui: shm lock poisoned: {}", e),
}
Ok(())
}
#[derive(serde::Serialize)]
@@ -494,11 +539,18 @@ struct BridgeStatus {
block_size: u32,
}
/// %APPDATA%/SonicForgeDAW/ipc — cùng dir engine (plugins.py) đọc cho
/// /api/v1/bridge/status; None khi APPDATA unset (non-Windows).
fn bridge_ipc_dir() -> Option<PathBuf> {
let appdata = std::env::var("APPDATA").ok()?;
Some(Path::new(&appdata).join("SonicForgeDAW").join("ipc"))
}
#[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() {
let status = match guard.as_ref() {
Some(shm) => BridgeStatus {
connected: true,
shm_name: shm::SHM_NAME,
@@ -517,7 +569,16 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
sample_rate: 0,
block_size: 0,
},
})
};
// E1: ghi JSON → engine /api/v1/bridge/status (badge Plugin Manager phải
// phản ánh đúng bridge thật — trước đây file không tồn tại → luôn OFF).
if let Some(dir) = bridge_ipc_dir() {
let _ = std::fs::create_dir_all(&dir);
if let Ok(json) = serde_json::to_string(&status) {
let _ = std::fs::write(dir.join("bridge_status"), json);
}
}
Ok(status)
}
#[tauri::command]
@@ -533,5 +594,5 @@ fn transport_control(app: AppHandle, kind: String, playhead: Option<u32>) -> Res
};
shm.push_control(3, arg0, arg1, 0, "")
.then_some(())
.ok_or("control queue full")
.ok_or_else(|| "control queue full".to_string())
}
+16 -4
View File
@@ -8,7 +8,7 @@ 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::{
use windows_sys::Win32::System::Memory::{
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE,
};
@@ -83,13 +83,13 @@ impl Shm {
return None;
}
let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) };
if view.is_null() {
if view.Value.is_null() {
unsafe { CloseHandle(handle) };
return None;
}
Some(Shm {
handle,
view: view as *mut SharedAudioBufferIPC,
view: view.Value as *mut SharedAudioBufferIPC,
})
}
@@ -156,7 +156,7 @@ impl Shm {
impl Drop for Shm {
fn drop(&mut self) {
if !self.view.is_null() {
unsafe { UnmapViewOfFile(self.view as *const c_void) };
unsafe { UnmapViewOfFile(windows_sys::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS { Value: self.view as *mut c_void }) };
}
unsafe { CloseHandle(self.handle) };
}
@@ -164,3 +164,15 @@ impl Drop for Shm {
/// Process-wide shared-memory state for Tauri.
pub struct ShmState(pub Mutex<Option<Shm>>);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_matches_c_header() {
// native_bridge/tests/shm_selfcheck.cpp asserts the same sizes.
assert_eq!(std::mem::size_of::<MidiEventIPC>(), 12);
assert_eq!(std::mem::size_of::<ControlEventIPC>(), 1040);
assert_eq!(std::mem::size_of::<SharedAudioBufferIPC>(), 11160);
}
}
+2 -1
View File
@@ -33,7 +33,8 @@
"icons/favicon.ico"
],
"resources": {
"resources/daw_engine": "daw_engine/"
"resources/daw_engine": "daw_engine/",
"binaries/libfluidsynth-3.dll": "libfluidsynth-3.dll"
},
"externalBin": ["binaries/daw_vst_bridge"],
"windows": {
+32 -10
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="utf-8">
@@ -12,23 +12,45 @@
<body>
<div id="msg">Đang khởi động SonicForge Engine…</div>
<script>
// Engine (sidecar) có thể cần vài giây để boot (import librosa/pedalboard).
// Thử health-check trên dải port 8000-8010, redirect tới port đầu tiên OK.
// Frontend host (Tauri asset page). KHÔNG location.href redirect: chuyển
// sang http://127.0.0.1:8000 sẽ rời Tauri context -> mất window.__TAURI__
// -> NativeBridgeService vô hiệu (bridge-audio/load_instrument không dùng
// được; MIDI rơi về fallback WASM/Carla). Thay vào đó: quét port engine
// (8000-8010), nạp toàn bộ UI engine qua document.write trên CÙNG document
// (origin tauri:// giữ nguyên -> __TAURI__ sống), base href + API_BASE_URL
// trỏ engine HTTP (CORS * đã bật).
(async function () {
var tries = 0;
while (tries < 90) { // ~60s tối đa
for (var p = 8000; p <= 8010; p++) {
var base = null;
for (var p = 8000; p <= 8010 && !base; p++) {
try {
var r = await fetch('http://127.0.0.1:' + p + '/health', { cache: 'no-store' });
if (r.ok) { location.href = 'http://127.0.0.1:' + p + '/'; return; }
if (r.ok) base = 'http://127.0.0.1:' + p + '/';
} catch (e) { /* port chưa mở */ }
}
tries++;
await new Promise(function (res) { setTimeout(res, 700); });
}
if (!base) {
document.getElementById('msg').textContent =
'Không tìm thấy SonicForge Engine (port 80008010). Hãy đóng và chạy lại ứng dụng. ' +
'Nếu lặp lại, xem log: %APPDATA%\\SonicForgeDAW\\logs\\engine.log';
return;
}
var html;
try {
var res = await fetch(base, { cache: 'no-store' });
html = await res.text();
} catch (e) {
document.getElementById('msg').textContent = 'Engine không phản hồi: ' + e;
return;
}
// base href để /static, /api resolve về engine; API_BASE_URL override
// location.origin (origin thật là tauri://). KHÔNG trailing slash trong
// API_BASE_URL: api.js nối `${API_BASE_URL}/api/...` -> double slash
// `//api/...` -> FastAPI 404 "Not Found" (login báo not found).
html = html.replace('<head>',
'<head>\n<base href="' + base + '">\n' +
'<script>window.API_BASE_URL = "' + base.replace(/\/$/, '') + '";<\/script>');
document.open();
document.write(html);
document.close();
})();
</script>
</body>