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:
@@ -37,3 +37,9 @@ src-tauri/vc_redist.x64.exe
|
|||||||
app/storage/plugin_dirs.json
|
app/storage/plugin_dirs.json
|
||||||
app/storage/sf_scan_state.json.bak-root
|
app/storage/sf_scan_state.json.bak-root
|
||||||
app/storage/soundfonts/
|
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*
|
||||||
|
|||||||
@@ -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;
|
> `--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
|
> 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.
|
> 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
|
> **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 🟡
|
> 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
|
> (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) |
|
| ✅ 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 |
|
| ✅ 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 |
|
| 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
@@ -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.
|
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/`)
|
## 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] 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 (std::map channel→instrument) — code OK, chưa compile Windows
|
- [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] 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
|
- [x] A12: CC/program/pitch bend (MidiEventIPC data2+data3) — code OK
|
||||||
- [ ] A6: Vst3Instrument host thật qua vst3sdk — load .vst3 → noteon → PCM ≠ 0
|
- [x] A6: Vst3Instrument host thật qua vst3sdk — load mda DX10 (.vst3) → noteon → PCM peak 0.16096 (G2 3 PASS, 2026-08-12)
|
||||||
- [ ] A7: Vst3Instrument tách class + openGUI(hwnd)
|
- [x] A7: Vst3Instrument tách class + openGUI(hwnd) — **FIX GUI HANG 2026-08-12**: `attached()` treo do
|
||||||
- [ ] **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)
|
openGUI chạy thread khác với load thread (load thread exit → COM STA apartment chết). Probe
|
||||||
- [ ] **Windows verify**: `cargo check` + chạy `shm_selfcheck.exe` (sizeof struct = **11160 bytes**)
|
`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/`)
|
## B. Rust / Tauri (`src-tauri/`)
|
||||||
- [ ] B1-B4: `cargo check` pass trên Windows (windows-sys, shm.rs, lib.rs commands)
|
- [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`)
|
||||||
- [ ] B2: SHM struct layout khớp C header (**11160 bytes** — đã verify C bằng selfcheck Linux; cần Rust)
|
- [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)
|
||||||
- [ ] 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] 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
|
- [ ] 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] 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] 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
|
- [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
|
- [ ] 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)
|
- [ ] 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
|
## 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)
|
- [ ] C5: ring buffer 8 block chống underrun thật trên Windows (âm nghe được, VU nhảy)
|
||||||
|
|
||||||
## D. app.jsx wiring
|
## 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] 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
|
- [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
|
- [ ] 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
|
- [ ] 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] D6: panic/Stop → `SonicMidiRouter.panic()` (bridge transport panic + fallback SonicSF.stopAll) — 4 call site app.jsx đã đổi
|
||||||
- [x] D8: track→channel map + gửi CC0/CC32/PROGRAM trước NOTE_ON khi đổi instrument (scheduleMidiNoteDispatch)
|
- [x] 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
|
- [x] D10: header indicator Bridge/WASM + nút "Ép WASM" (toggle `SonicMidiRouter.setForceWasm`); service thêm `onStatusChange(cb)` — code + bundle OK
|
||||||
|
|
||||||
## E. Backend Python
|
## 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] 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
|
- [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
|
- [ ] `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`
|
- [ ] `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
|
## 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] 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] 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] 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
|
- [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
|
## 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)
|
- [x] G1: shm_selfcheck pass trên Windows — "SHM self-check OK (sizeof struct = 11160 bytes)"
|
||||||
- [ ] G2: SHM Rust tạo → bridge mở → MIDI → audio
|
- [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)
|
- [ ] G3: load SGM-V2.01 (SF2) + SalamanderPiano (SFZ) + Vital (VST3)
|
||||||
- [ ] G4: live MIDI + timeline cùng lúc, latency <10ms
|
- [ ] G4: live MIDI + timeline cùng lúc, latency <10ms
|
||||||
- [ ] G5: Track FX + Master FX tác động âm bridge
|
- [ ] 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)
|
- [ ] 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)
|
- [ ] 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)
|
## Ghi chú batch 2026-08-11 (A10-A13 + D5)
|
||||||
- Struct SHM đổi 10872 → 11160: MidiEventIPC +data2/data3(+reserved) = 12B; ControlEventIPC +channel = 1040B.
|
- 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).
|
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)`.
|
- `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.
|
- `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.
|
- 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` có `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.
|
||||||
|
|||||||
+18
-8
@@ -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 = os.path.basename((path or name).replace("\\", "/"))
|
||||||
base_noext = os.path.splitext(base)[0].lower()
|
base_noext = os.path.splitext(base)[0].lower()
|
||||||
# sf2/sf3/sfz: storage/soundfonts + system + user plugin_dirs
|
# 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):
|
if not os.path.isdir(base_dir):
|
||||||
continue
|
continue
|
||||||
for fname in os.listdir(base_dir):
|
for root, _, files in os.walk(base_dir):
|
||||||
cand = os.path.join(base_dir, fname)
|
for fname in files:
|
||||||
if fname.lower().endswith((".sf2", ".sf3", ".sfz")) and \
|
if fname.lower().endswith((".sf2", ".sf3", ".sfz")) and \
|
||||||
(fname.lower() == base.lower() or os.path.splitext(fname)[0].lower() == base_noext):
|
(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
|
ext = (".vst3" if instrument_type.upper() == "VST3" else
|
||||||
".vst2" if instrument_type.upper() == "VST2" else
|
".vst2" if instrument_type.upper() == "VST2" else
|
||||||
".sfz" if instrument_type.upper() == "SFZ" else None)
|
".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 []
|
dirs = _load_user_plugin_dirs() or []
|
||||||
except Exception:
|
except Exception:
|
||||||
dirs = []
|
dirs = []
|
||||||
if not dirs:
|
# Luon gop VST_DIR/SOUNDFONT_DIR: neu chi dung user plugin_dirs,
|
||||||
dirs = [settings.VST_DIR, settings.SOUNDFONT_DIR]
|
# 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:
|
for base_dir in dirs:
|
||||||
if not os.path.isdir(base_dir):
|
if not os.path.isdir(base_dir):
|
||||||
continue
|
continue
|
||||||
|
|||||||
+68
-131
@@ -7,7 +7,7 @@ const {
|
|||||||
} = React;
|
} = React;
|
||||||
|
|
||||||
// ── FastAPI Backend Configuration ──
|
// ── 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_AUDIO = `${API_BASE_URL}/api/v1/audio`;
|
||||||
const API_MULTITRACK = `${API_BASE_URL}/api/v1/multitrack`;
|
const API_MULTITRACK = `${API_BASE_URL}/api/v1/multitrack`;
|
||||||
const API_TASKS = `${API_BASE_URL}/api/v1/audio/tasks`;
|
const API_TASKS = `${API_BASE_URL}/api/v1/audio/tasks`;
|
||||||
@@ -143,38 +143,6 @@ const refreshCarlaStatus = () => {
|
|||||||
// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track rồi CHỜ OSC ready (poll
|
// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track rồi CHỜ OSC ready (poll
|
||||||
// carla-status tối đa 10s) — nốt chỉ gửi khi bridge thực sự nhận được.
|
// carla-status tối đa 10s) — nốt chỉ gửi khi bridge thực sự nhận được.
|
||||||
// Có gate __carlaOpening chống spawn trùng (mỗi play chỉ mở 1 lần).
|
// Có gate __carlaOpening chống spawn trùng (mỗi play chỉ mở 1 lần).
|
||||||
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 (hoặc đã chạy) — poll tới khi OSC ready, mới flush nốt
|
|
||||||
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 chạy: docker vs standalone ─────────────────────────────────
|
// ── Môi trường chạy: docker vs standalone ─────────────────────────────────
|
||||||
@@ -5901,12 +5869,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
React.createElement('div', { className: 'flex items-center gap-2' },
|
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', {
|
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'); },
|
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',
|
className: 'text-[10px] bg-cyan-800 hover:bg-cyan-700 text-white px-2 py-1 rounded transition shrink-0',
|
||||||
title: 'Load vào Native Bridge (C++ engine)'
|
title: 'Load vào Native Bridge (C++ engine)'
|
||||||
@@ -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)
|
// Plugin directories section (folder picker + save + scan)
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
className: 'pt-4 mt-4 border-t border-[#383838]'
|
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: '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: 'truncate' }, v.name),
|
||||||
React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, v.dir),
|
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' },
|
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);
|
var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
||||||
ensureSonicInstrument(pvCtx);
|
ensureSonicInstrument(pvCtx);
|
||||||
playing.forEach(n => {
|
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);
|
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
|
||||||
return;
|
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 clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
|
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
|
||||||
ensureSonicInstrument(clCtx);
|
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);
|
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
|
||||||
} else {
|
} else {
|
||||||
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
||||||
@@ -8885,7 +8810,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
|
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
|
||||||
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
|
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
|
||||||
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
|
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);
|
playNativeSfNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
|
||||||
} else if (window.SonicSF && window.SonicSF.playNote) {
|
} else if (window.SonicSF && window.SonicSF.playNote) {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
@@ -8986,7 +8911,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
||||||
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
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);
|
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
|
||||||
} else if (window.SonicSF && window.SonicSF.playNote) {
|
} else if (window.SonicSF && window.SonicSF.playNote) {
|
||||||
var pvCtx = getAudioContext();
|
var pvCtx = getAudioContext();
|
||||||
@@ -9378,12 +9303,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
if (window.triggerMidiVuActivity) {
|
if (window.triggerMidiVuActivity) {
|
||||||
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
|
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) {
|
if (kbNative) {
|
||||||
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
||||||
}
|
}
|
||||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
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) {
|
} else if (window.SonicSF && !kbNative) {
|
||||||
// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
|
// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
|
||||||
// chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng
|
// chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng
|
||||||
@@ -9406,11 +9331,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
if (window.triggerMidiVuActivity) {
|
if (window.triggerMidiVuActivity) {
|
||||||
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
|
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);
|
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
||||||
}
|
}
|
||||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
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))) {
|
} else if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
|
||||||
// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
|
// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
|
||||||
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
|
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
|
||||||
@@ -9429,7 +9354,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
try {
|
try {
|
||||||
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
||||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
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);
|
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
|
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
|
||||||
@@ -9441,7 +9366,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
try {
|
try {
|
||||||
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
||||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
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);
|
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
|
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
|
||||||
@@ -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)}>
|
<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)
|
<i className="fa-solid fa-ban text-slate-400"></i> None (mặc định)
|
||||||
</div>
|
</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 => (
|
{!synthLoading && filteredSynthList && filteredSynthList.map(group => (
|
||||||
<div key={group.sf.id || group.sf.name}>
|
<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>
|
<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",
|
"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"
|
"Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"
|
||||||
];
|
];
|
||||||
|
// D7: bridge active → load instrument vào bridge tại channel router dùng cho
|
||||||
|
// track (SonicMidiRouter.allocateChannel — cùng allocation scheduleMidiNoteDispatch/
|
||||||
|
// keybed dùng khi push NOTE_ON). Không load → MIDI tới channel rỗng → 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 setTrackInstrumentWithProgram = (trackId, instrumentId, programNumber, displayName, bankNumber) => {
|
||||||
const isSfInstrument = instrumentId && typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
|
const isSfInstrument = instrumentId && typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
|
||||||
const sfBank = bankNumber !== undefined ? bankNumber : (isSfInstrument ? 0 : undefined);
|
const sfBank = bankNumber !== undefined ? bankNumber : (isSfInstrument ? 0 : undefined);
|
||||||
@@ -15465,6 +15416,8 @@ const App = () => {
|
|||||||
const sfId = instrumentId.replace('sf_', '');
|
const sfId = instrumentId.replace('sf_', '');
|
||||||
window.SonicSF.selectInstrument(mch, sfBank || 0, sfProg || 0, sfId);
|
window.SonicSF.selectInstrument(mch, sfBank || 0, sfProg || 0, sfId);
|
||||||
}
|
}
|
||||||
|
// D7: bridge active → load instrument vào bridge tại channel router sẽ dùng
|
||||||
|
loadTrackInstrumentToBridge(trackId, instrumentId, sfBank);
|
||||||
setSubTabs(prev => prev.map(s => {
|
setSubTabs(prev => prev.map(s => {
|
||||||
if (s.trackId !== trackId) return s;
|
if (s.trackId !== trackId) return s;
|
||||||
return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId };
|
return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId };
|
||||||
@@ -15684,7 +15637,7 @@ const App = () => {
|
|||||||
var bTracks = activeTracksRef.current || [];
|
var bTracks = activeTracksRef.current || [];
|
||||||
bTracks.forEach(function (bt) {
|
bTracks.forEach(function (bt) {
|
||||||
if (!bt.isArmed) return;
|
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) {
|
} else if (window.SonicSF) {
|
||||||
var allTracks = activeTracksRef.current || [];
|
var allTracks = activeTracksRef.current || [];
|
||||||
@@ -15759,7 +15712,7 @@ const App = () => {
|
|||||||
// D1: bridge note-off qua router (channel trùng NOTE_ON — track.id).
|
// D1: bridge note-off qua router (channel trùng NOTE_ON — track.id).
|
||||||
var bStopTracks = activeTracksRef.current || [];
|
var bStopTracks = activeTracksRef.current || [];
|
||||||
bStopTracks.forEach(function (bst) {
|
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) {
|
} else if (window.SonicSF && window.SonicSF.stopNote) {
|
||||||
var stopTracks = activeTracksRef.current || [];
|
var stopTracks = activeTracksRef.current || [];
|
||||||
@@ -21693,6 +21646,17 @@ const App = () => {
|
|||||||
const program = track ? track.instrumentProgram : undefined;
|
const program = track ? track.instrumentProgram : undefined;
|
||||||
var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
|
var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
|
||||||
const routeCarla = shouldRouteCarla(track && track.synth_engine);
|
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)) {
|
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
|
||||||
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
|
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
|
||||||
return;
|
return;
|
||||||
@@ -21721,24 +21685,26 @@ const App = () => {
|
|||||||
// Guard chống note-on trễ sau Stop: CHỈ khi là timeline (startTime thật).
|
// Guard chống note-on trễ sau Stop: CHỈ khi là timeline (startTime thật).
|
||||||
const guardPlay = startTime != null ? function () { return isPlayingRef.current; } : function () { return true; };
|
const guardPlay = startTime != null ? function () { return isPlayingRef.current; } : function () { return true; };
|
||||||
const trkId = track ? track.id : 0;
|
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 → gửi CC0/CC32 (bank 0) + program change qua
|
// D8: track đổi instrument → gửi CC0/CC32 (bank 0) + program change qua
|
||||||
// bridge (A12) một lần mỗi program mới mỗi channel.
|
// bridge (A12) một lần mỗi program mới mỗi channel.
|
||||||
if (program !== undefined && program !== null) {
|
if (program !== undefined && program !== null) {
|
||||||
const lastP = window.__bridgeLastProgram || (window.__bridgeLastProgram = {});
|
const lastP = window.__bridgeLastProgram || (window.__bridgeLastProgram = {});
|
||||||
if (lastP[trkId] !== program) {
|
if (lastP[trkId] !== program) {
|
||||||
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: 0, velocity: 0, data2: 0, percussion: isPerc }); } catch (e) {}
|
||||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 32, velocity: 0, data2: 0 }); } 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 }); } catch (e) {}
|
try { window.SonicMidiRouter.pushEvent({ cmd: 'PROGRAM', channel: trkId, pitch: 0, velocity: 0, data2: program, percussion: isPerc }); } catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
if (!guardPlay()) return;
|
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);
|
}, delayMs);
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
if (!guardPlay()) return;
|
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);
|
}, delayMs + (durMs || 1000) + 30);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -21810,14 +21776,13 @@ const App = () => {
|
|||||||
// MIDI items playback
|
// MIDI items playback
|
||||||
const midiItems = track.midiItems || [];
|
const midiItems = track.midiItems || [];
|
||||||
const routeCarla = shouldRouteCarla(track.synth_engine);
|
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 ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
|
||||||
if (nativeSf) {
|
if (nativeSf) {
|
||||||
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||||
} else {
|
} else {
|
||||||
// Preview cache: capture the soundfont (pre track-FX) for fast offline export
|
// Preview cache: capture the soundfont (pre track-FX) for fast offline export
|
||||||
ensureMidiCapture(track, activeTrackNodesRef.current[track.id]);
|
ensureMidiCapture(track, activeTrackNodesRef.current[track.id]);
|
||||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) ensureCarlaForPlayback(track.synth_engine);
|
|
||||||
var trkCh = assignTrackMidiChannel(track, allPlayTracks);
|
var trkCh = assignTrackMidiChannel(track, allPlayTracks);
|
||||||
// Ensure instrument is loaded in FluidSynth
|
// Ensure instrument is loaded in FluidSynth
|
||||||
if (window.SonicSF && track.synth_engine && track.synth_engine.type === 'soundfont' && track.synth_engine.soundfont_id) {
|
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
|
// 2. Play MIDI items in subTrack
|
||||||
const subMidiItems = subTrack.midiItems || [];
|
const subMidiItems = subTrack.midiItems || [];
|
||||||
const subRouteCarla = shouldRouteCarla(subTrack.synth_engine);
|
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 (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf)) {
|
||||||
if (subNativeSf) {
|
if (subNativeSf) {
|
||||||
subMidiItems.forEach(item => scheduleNativeSfItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
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
|
// MIDI item trong SECTION → Carla bridge khi VSTi loaded
|
||||||
// (chỉ route Carla — không play FluidSynth GM sai âm)
|
// (chỉ route Carla — không play FluidSynth GM sai âm)
|
||||||
const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));
|
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) {
|
if (offsetTime < noteStartMain) {
|
||||||
const delay = noteStartMain - offsetTime;
|
const delay = noteStartMain - offsetTime;
|
||||||
@@ -22108,7 +22072,7 @@ const App = () => {
|
|||||||
// MIDI items playback
|
// MIDI items playback
|
||||||
const midiItems = track.midiItems || [];
|
const midiItems = track.midiItems || [];
|
||||||
const routeCarla = shouldRouteCarla(track.synth_engine);
|
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 ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
|
||||||
if (nativeSf) {
|
if (nativeSf) {
|
||||||
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||||
@@ -31271,13 +31235,6 @@ STRICT CONSTRAINTS:
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackInstrumentWithUndo(instrumentSelectorTrackId, v.id, v.name || v.id);
|
setTrackInstrumentWithUndo(instrumentSelectorTrackId, v.id, v.name || v.id);
|
||||||
closeInstrumentSelector();
|
closeInstrumentSelector();
|
||||||
// TỰ ĐỘNG mở Carla với VSTi vừa chọn (desktop + Carla local) —
|
|
||||||
// Carla load sẵn 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"
|
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")))
|
}, /*#__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),
|
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"
|
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)"),
|
}, "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.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", {
|
filteredInstruments.soundfonts.map((sf, i) => /*#__PURE__*/React.createElement("button", {
|
||||||
key: "sfd_" + i,
|
key: "sfd_" + i,
|
||||||
@@ -31364,21 +31313,9 @@ STRICT CONSTRAINTS:
|
|||||||
setInstrumentDropdownTrackId(null);
|
setInstrumentDropdownTrackId(null);
|
||||||
setInstrumentDropdownBtnRect(null);
|
setInstrumentDropdownBtnRect(null);
|
||||||
setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id);
|
setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id);
|
||||||
// TỰ ĐỘNG mở Carla với VSTi vừa chọn (desktop + Carla local) —
|
|
||||||
// Carla load sẵn 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"
|
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")),
|
}, /*#__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)"),
|
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", {
|
(window.SonicRuntime && window.SonicRuntime.presets || []).map((p, i) => /*#__PURE__*/React.createElement("button", {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
// SonicForge Studio API Service
|
// 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() {
|
||||||
function getAuthToken() {
|
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' }),
|
listSoundfontInstruments: (sfId) => apiRequest(`/api/v1/plugins/soundfont-instruments/${sfId}`, { method: 'GET' }),
|
||||||
// Native Host Bridge (daw_vst_bridge C++): trạng thái + load asset +
|
// Native Host Bridge (daw_vst_bridge C++): trạng thái + load asset +
|
||||||
// tail bridge.log (E1/E2/E5).
|
// tail bridge.log (E1/E2/E5).
|
||||||
bridgeStatus: () => apiRequest('/api/v1/bridge/status', { method: 'GET' }),
|
bridgeStatus: () => apiRequest('/api/v1/plugins/bridge/status', { method: 'GET' }),
|
||||||
bridgeLoad: (payload) => apiRequest('/api/v1/bridge/load', { method: 'POST', body: JSON.stringify(payload) }),
|
bridgeLoad: (payload) => apiRequest('/api/v1/plugins/bridge/load', { method: 'POST', body: JSON.stringify(payload) }),
|
||||||
bridgeLog: (lines = 100) => apiRequest(`/api/v1/bridge/log?lines=${lines}`, { method: 'GET' }),
|
bridgeLog: (lines = 100) => apiRequest(`/api/v1/plugins/bridge/log?lines=${lines}`, { method: 'GET' }),
|
||||||
getAIPresets: () => apiRequest('/api/v1/ai/presets', { method: 'GET' }),
|
getAIPresets: () => apiRequest('/api/v1/ai/presets', { method: 'GET' }),
|
||||||
saveAIPreset: (preset) => apiRequest('/api/v1/ai/presets', { method: 'POST', body: JSON.stringify(preset) }),
|
saveAIPreset: (preset) => apiRequest('/api/v1/ai/presets', { method: 'POST', body: JSON.stringify(preset) }),
|
||||||
deleteAIPreset: (presetId) => apiRequest(`/api/v1/ai/presets/${presetId}`, { method: 'DELETE' }),
|
deleteAIPreset: (presetId) => apiRequest(`/api/v1/ai/presets/${presetId}`, { method: 'DELETE' }),
|
||||||
|
|||||||
@@ -20,6 +20,30 @@
|
|||||||
if (this._statusCb) this._statusCb({ connected: !!connected });
|
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 () {
|
_init: function () {
|
||||||
if (!this._tauri()) {
|
if (!this._tauri()) {
|
||||||
// Dev mode (Linux / plain browser): no Tauri -> log-only, app uses SonicSF.
|
// Dev mode (Linux / plain browser): no Tauri -> log-only, app uses SonicSF.
|
||||||
@@ -29,6 +53,9 @@
|
|||||||
var self = this;
|
var self = this;
|
||||||
try {
|
try {
|
||||||
window.__TAURI__.event.listen('bridge-audio', function (e) {
|
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);
|
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
|
||||||
});
|
});
|
||||||
window.__TAURI__.event.listen('bridge-down', function () {
|
window.__TAURI__.event.listen('bridge-down', function () {
|
||||||
|
|||||||
@@ -73,28 +73,14 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
|
|||||||
// (Carla standalone bật OSC UDP mặc định cổng 22752).
|
// (Carla standalone bật OSC UDP mặc định cổng 22752).
|
||||||
window.SonicCarlaMidi = window.SonicCarlaMidi || {
|
window.SonicCarlaMidi = window.SonicCarlaMidi || {
|
||||||
shouldRoute: function (synthEngine, isArmed) {
|
shouldRoute: function (synthEngine, isArmed) {
|
||||||
try {
|
// Carla bridge removed - all instruments hosted by Native Bridge.
|
||||||
// D4: bridge active → VSTi do native bridge host, KHÔNG route Carla
|
return false;
|
||||||
// (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; }
|
|
||||||
},
|
},
|
||||||
// Playback MIDI items: route khi track VSTi + Carla local (không cần ARM —
|
// Playback MIDI items: route khi track VSTi + Carla local (không cần ARM —
|
||||||
// user đã chủ động bấm Play trên item đó).
|
// user đã chủ động bấm Play trên item đó).
|
||||||
shouldRoutePlayback: function (synthEngine) {
|
shouldRoutePlayback: function (synthEngine) {
|
||||||
try {
|
// Carla bridge removed - all instruments hosted by Native Bridge.
|
||||||
// D4: bridge active → KHÔNG route Carla (bridge host VSTi).
|
return false;
|
||||||
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; }
|
|
||||||
},
|
},
|
||||||
noteOn: function (channel, note, velocity) {
|
noteOn: function (channel, note, velocity) {
|
||||||
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
|
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
<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/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></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">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -1,52 +1,104 @@
|
|||||||
# build_native_bridge.ps1 - Build daw_vst_bridge.exe (C++ Native Host Bridge)
|
# 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).
|
# ASCII only (PowerShell 5.1 reads .ps1 without BOM as ANSI).
|
||||||
$ErrorActionPreference = "Stop"
|
$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
|
$REPO_ROOT = Resolve-Path (Join-Path $PSScriptRoot "..\..") # ...\SonicForgeStudio
|
||||||
$NB_DIR = Join-Path $REPO_ROOT "native_bridge"
|
$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)
|
# 1. vcpkg (bootstrap once; skip if already present)
|
||||||
$VCPKG_ROOT = Join-Path $env:USERPROFILE "vcpkg"
|
$VCPKG_ROOT = Join-Path $env:USERPROFILE "vcpkg"
|
||||||
if (-not (Test-Path (Join-Path $VCPKG_ROOT "vcpkg.exe"))) {
|
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
|
git clone https://github.com/microsoft/vcpkg $VCPKG_ROOT
|
||||||
& (Join-Path $VCPKG_ROOT "bootstrap-vcpkg.bat") -disableMetrics
|
& (Join-Path $VCPKG_ROOT "bootstrap-vcpkg.bat") -disableMetrics
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. vcpkg deps: fluidsynth + sfizz (+ pkgconf so pkg_check_modules may work)
|
# 2. fluidsynth via vcpkg manifest (native_bridge/vcpkg.json; pkgconf included)
|
||||||
Write-Host "== [2/6] vcpkg install fluidsynth sfizz =="
|
Write-Host "== [2/7] vcpkg install fluidsynth (manifest) =="
|
||||||
& (Join-Path $VCPKG_ROOT "vcpkg.exe") install fluidsynth sfizz --triplet x64-windows
|
Push-Location $NB_DIR
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: vcpkg install failed" -ForegroundColor Red; exit 1 }
|
& (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)
|
# 3. sfizz 1.2.3 static lib (sfizz has NO vcpkg port: clone + build once)
|
||||||
Write-Host "== [3/6] VST3 SDK (submodule) =="
|
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"))) {
|
if (-not (Test-Path (Join-Path $NB_DIR "vst3sdk"))) {
|
||||||
Push-Location $NB_DIR
|
Push-Location $REPO_ROOT
|
||||||
git submodule add https://github.com/steinbergmedia/vst3sdk.git vst3sdk
|
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
|
Pop-Location
|
||||||
}
|
}
|
||||||
|
|
||||||
# 4. Configure with CMake + vcpkg toolchain (MSVC)
|
# 5. Configure + build with CMake + vcpkg toolchain (MSVC, bridge target only)
|
||||||
Write-Host "== [4/6] CMake configure =="
|
Write-Host "== [5/7] CMake configure + build (target daw_vst_bridge) =="
|
||||||
$BUILD_DIR = Join-Path $NB_DIR "build"
|
$BUILD_DIR = Join-Path $NB_DIR "build"
|
||||||
New-Item -ItemType Directory -Force $BUILD_DIR | Out-Null
|
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_TOOLCHAIN_FILE=(Join-Path $VCPKG_ROOT "scripts\buildsystems\vcpkg.cmake") `
|
||||||
-DCMAKE_BUILD_TYPE=Release `
|
-DCMAKE_BUILD_TYPE=Release `
|
||||||
-DVCPKG_TARGET_TRIPLET=x64-windows
|
-DVCPKG_TARGET_TRIPLET=x64-windows
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake configure failed" -ForegroundColor Red; exit 1 }
|
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake configure failed" -ForegroundColor Red; exit 1 }
|
||||||
|
cmake --build $BUILD_DIR --config Release --target daw_vst_bridge -- /m
|
||||||
# 5. Build Release
|
|
||||||
Write-Host "== [5/6] CMake build =="
|
|
||||||
cmake --build $BUILD_DIR --config Release
|
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake build failed" -ForegroundColor Red; exit 1 }
|
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake build failed" -ForegroundColor Red; exit 1 }
|
||||||
|
|
||||||
# 6. Copy sidecar to Tauri externalBin location
|
# 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"
|
$BIN_DIR = Join-Path $REPO_ROOT "src-tauri\binaries"
|
||||||
New-Item -ItemType Directory -Force $BIN_DIR | Out-Null
|
New-Item -ItemType Directory -Force $BIN_DIR | Out-Null
|
||||||
$EXE = Join-Path $BUILD_DIR "Release\daw_vst_bridge.exe"
|
$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 }
|
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
|
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 =="
|
||||||
|
|||||||
@@ -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) =="
|
Write-Host "== [5/7] Build native bridge (daw_vst_bridge.exe) =="
|
||||||
& (Join-Path $PSScriptRoot "build_native_bridge.ps1")
|
& (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 ($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")) {
|
if (-not (Test-Path "src-tauri\binaries\daw_vst_bridge-x86_64-pc-windows-msvc.exe")) {
|
||||||
Write-Host "ERROR: bridge sidecar missing" -ForegroundColor Red; exit 1
|
Write-Host "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 =="
|
Write-Host "== [6/7] VC++ Redistributable for hooks.nsh =="
|
||||||
if (-not (Test-Path src-tauri\vc_redist.x64.exe)) {
|
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
|
python tools\verify_bundle.py --check-bridge
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: bridge sidecar missing" -ForegroundColor Red; exit 1 }
|
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: bridge sidecar missing" -ForegroundColor Red; exit 1 }
|
||||||
npm install -D @tauri-apps/cli
|
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
|
npx tauri build
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
project(daw_vst_bridge LANGUAGES C CXX)
|
project(daw_vst_bridge LANGUAGES C CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
# ── 1. VST3 SDK (Steinberg, vendored as git submodule — NOT in vcpkg) ──
|
# ── 1. VST3 SDK (Steinberg, vendored as git submodule — NOT in vcpkg) ──
|
||||||
@@ -19,15 +19,24 @@ endif()
|
|||||||
# Linux/macOS: pkg-config duoc dung (spec goc).
|
# Linux/macOS: pkg-config duoc dung (spec goc).
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
find_path(FLUIDSYNTH_INCLUDE_DIR fluidsynth.h)
|
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)
|
if(NOT FLUIDSYNTH_INCLUDE_DIR OR NOT FLUIDSYNTH_LIBRARY)
|
||||||
message(FATAL_ERROR "fluidsynth not found — cai qua vcpkg: vcpkg install fluidsynth")
|
message(FATAL_ERROR "fluidsynth not found — cai qua vcpkg: vcpkg install fluidsynth")
|
||||||
endif()
|
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) —
|
||||||
find_path(SFIZZ_INCLUDE_DIR sfizz.hpp)
|
# the sfizz port is NOT in vcpkg upstream. Fall back to vcpkg if present.
|
||||||
find_library(SFIZZ_LIBRARY NAMES sfizz)
|
set(SFIZZ_SRC_BUILD "${CMAKE_CURRENT_SOURCE_DIR}/sfizz-src/build/library/lib/Release")
|
||||||
if(NOT SFIZZ_INCLUDE_DIR OR NOT SFIZZ_LIBRARY)
|
if(EXISTS "${SFIZZ_SRC_BUILD}/sfizz_static.lib")
|
||||||
message(FATAL_ERROR "sfizz not found — cai qua vcpkg: vcpkg install sfizz")
|
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 — build native_bridge/sfizz-src first")
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
else()
|
else()
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
@@ -37,18 +46,27 @@ endif()
|
|||||||
|
|
||||||
include_directories(
|
include_directories(
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
${FLUIDSYNTH_INCLUDE_DIRS}
|
${FLUIDSYNTH_INCLUDE_DIR}
|
||||||
${SFIZZ_INCLUDE_DIRS}
|
${SFIZZ_INCLUDE_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(daw_vst_bridge
|
add_executable(daw_vst_bridge
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/NativeInstrumentEngine.cpp
|
src/NativeInstrumentEngine.cpp
|
||||||
src/SharedMemoryIPC.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)
|
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()
|
endif()
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY})
|
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY})
|
||||||
@@ -57,3 +75,23 @@ if(WIN32)
|
|||||||
else()
|
else()
|
||||||
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
|
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
|
||||||
endif()
|
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 <sfizz.hpp>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -53,7 +54,7 @@ public:
|
|||||||
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
|
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
sfizz::Synth sfizzSynth;
|
sfz::Sfizz sfizzSynth;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Multi-instrument host (A10): one engine instance per MIDI channel, so two
|
// 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.
|
// Zero L/R then sum each assigned instrument into it.
|
||||||
void renderAll(float* outputL, float* outputR, uint32_t numSamples);
|
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:
|
private:
|
||||||
static std::unique_ptr<INativeInstrument> create_instrument(InstrumentType type);
|
static std::unique_ptr<INativeInstrument> create_instrument(InstrumentType type);
|
||||||
|
|
||||||
|
mutable std::mutex mu_;
|
||||||
std::map<uint32_t, std::unique_ptr<INativeInstrument>> channels_;
|
std::map<uint32_t, std::unique_ptr<INativeInstrument>> channels_;
|
||||||
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
|
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
|
||||||
std::vector<float> scratchL_, scratchR_;
|
std::vector<float> scratchL_, scratchR_;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#define AUDIO_BLOCK_SIZE 256
|
#define AUDIO_BLOCK_SIZE 256
|
||||||
|
|
||||||
// WARNING: volatile is NOT a sync primitive. Real impl should use an
|
// WARNING: volatile is NOT a sync primitive. Real impl should use an
|
||||||
|
|||||||
@@ -29,10 +29,8 @@ public:
|
|||||||
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
|
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void* module_; // Steinberg::IPluginFactory* (owned by module)
|
// pimpl: all Steinberg SDK types live in Vst3Instrument.cpp (Vst3HostState).
|
||||||
void* processor_; // Steinberg::Vst::IComponent*
|
void* state_;
|
||||||
void* controller_; // Steinberg::Vst::IEditController*
|
|
||||||
void* view_; // Steinberg::IPlugView*
|
|
||||||
std::string path_;
|
std::string path_;
|
||||||
double sampleRate_;
|
double sampleRate_;
|
||||||
uint32_t maxBlockSize_;
|
uint32_t maxBlockSize_;
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
|
|
||||||
#include <fluidsynth.h>
|
#include <fluidsynth.h>
|
||||||
#include <sfizz.hpp>
|
#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 <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -14,24 +19,24 @@ FluidSynthInstrument::FluidSynthInstrument()
|
|||||||
: settings(nullptr), synth(nullptr), sfontId(-1) {}
|
: settings(nullptr), synth(nullptr), sfontId(-1) {}
|
||||||
|
|
||||||
FluidSynthInstrument::~FluidSynthInstrument() {
|
FluidSynthInstrument::~FluidSynthInstrument() {
|
||||||
if (synth) delete_fluid_synth(synth);
|
if (synth) delete_fluid_synth(FS_SYNTH);
|
||||||
if (settings) delete_fluid_settings(settings);
|
if (settings) delete_fluid_settings(FS_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sampleRate) {
|
bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sampleRate) {
|
||||||
if (synth) { delete_fluid_synth(synth); synth = nullptr; }
|
if (synth) { delete_fluid_synth(FS_SYNTH); synth = nullptr; }
|
||||||
if (settings) { delete_fluid_settings(settings); settings = nullptr; }
|
if (settings) { delete_fluid_settings(FS_SETTINGS); settings = nullptr; }
|
||||||
settings = new_fluid_settings();
|
settings = new_fluid_settings();
|
||||||
fluid_settings_setnum(settings, "synth.sample-rate", sampleRate);
|
fluid_settings_setnum(FS_SETTINGS, "synth.sample-rate", sampleRate);
|
||||||
fluid_settings_setint(settings, "synth.polyphony", 256);
|
fluid_settings_setint(FS_SETTINGS, "synth.polyphony", 256);
|
||||||
fluid_settings_setint(settings, "synth.verbose", 0);
|
fluid_settings_setint(FS_SETTINGS, "synth.verbose", 0);
|
||||||
synth = new_fluid_synth(settings);
|
synth = new_fluid_synth(FS_SETTINGS);
|
||||||
if (!synth) return false;
|
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;
|
if (sfontId == -1) return false;
|
||||||
// Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano)
|
// Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano)
|
||||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
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;
|
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) {
|
void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
|
||||||
if (!synth) return;
|
if (!synth) return;
|
||||||
fluid_synth_bank_select(synth, channel, bank);
|
fluid_synth_bank_select(FS_SYNTH, channel, bank);
|
||||||
fluid_synth_program_change(synth, channel, program);
|
fluid_synth_program_change(FS_SYNTH, channel, program);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
|
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
|
||||||
if (!synth) return;
|
if (!synth) return;
|
||||||
int velInt = static_cast<int>(velocity * 127.0f);
|
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) {
|
void FluidSynthInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
|
||||||
if (!synth) return;
|
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) {
|
void FluidSynthInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
|
||||||
if (!synth) return;
|
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) {
|
void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||||
if (!synth) return;
|
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) {
|
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||||
if (!synth) return;
|
if (!synth) return;
|
||||||
// fluid_synth_pitch_bend takes the raw 14-bit value (center 8192).
|
// 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) {
|
bool FluidSynthInstrument::openGUI(void* parentWindowHandle) {
|
||||||
@@ -81,7 +86,7 @@ void FluidSynthInstrument::closeGUI() {}
|
|||||||
|
|
||||||
void FluidSynthInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
void FluidSynthInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
||||||
if (!synth) return;
|
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) {
|
void SfizzInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
|
||||||
// ponytail: non-delayed cc() is stable across sfizz versions; switch to
|
sfizzSynth.cc(0, static_cast<int>(cc), static_cast<int>(value));
|
||||||
// hdCC(cc, value) for sample-accurate CC when the installed sfizz has it.
|
|
||||||
sfizzSynth.cc(static_cast<int>(cc), static_cast<float>(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SfizzInstrument::programChange(uint32_t channel, uint32_t program) {
|
void SfizzInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||||
// TODO(A12): sfizz program-change API differs by version (hdProgramChange
|
sfizzSynth.programChange(0, static_cast<int>(program));
|
||||||
// in >=0.6). Rarely used by SFZ instruments — no-op until verified.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SfizzInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
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; }
|
bool SfizzInstrument::openGUI(void* parentWindowHandle) { return false; }
|
||||||
@@ -128,7 +130,7 @@ void SfizzInstrument::closeGUI() {}
|
|||||||
|
|
||||||
void SfizzInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
void SfizzInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
||||||
float* channels[2] = { outputL, outputR };
|
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) {
|
switch (type) {
|
||||||
case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique<FluidSynthInstrument>();
|
case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique<FluidSynthInstrument>();
|
||||||
case InstrumentType::SFZ: return std::make_unique<SfizzInstrument>();
|
case InstrumentType::SFZ: return std::make_unique<SfizzInstrument>();
|
||||||
// ponytail: VST3/VST2 host needs vst3sdk + Steinberg APIs — wired in
|
case InstrumentType::VST3: return std::make_unique<Vst3Instrument>();
|
||||||
// Giai doan 2 (A6-A8); returns nullptr so the bridge degrades gracefully.
|
// ponytail: VST2 host (VST2.4 SDK, Steinberg discontinued) not implemented.
|
||||||
case InstrumentType::VST3:
|
|
||||||
case InstrumentType::VST2:
|
case InstrumentType::VST2:
|
||||||
default: return nullptr;
|
default: return nullptr;
|
||||||
}
|
}
|
||||||
@@ -152,30 +153,41 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
|||||||
if (channel >= 16) return false;
|
if (channel >= 16) return false;
|
||||||
auto inst = create_instrument(type);
|
auto inst = create_instrument(type);
|
||||||
if (!inst) return false;
|
if (!inst) return false;
|
||||||
if (!inst->init(sampleRate, blockSize)) return false;
|
|
||||||
bool loaded = false;
|
bool loaded = false;
|
||||||
if (type == InstrumentType::SOUNDFONT_SF2_SF3)
|
if (type == InstrumentType::SOUNDFONT_SF2_SF3)
|
||||||
loaded = static_cast<FluidSynthInstrument*>(inst.get())->loadSoundFontFile(path, sampleRate);
|
loaded = static_cast<FluidSynthInstrument*>(inst.get())->loadSoundFontFile(path, sampleRate);
|
||||||
else if (type == InstrumentType::SFZ)
|
else if (type == InstrumentType::SFZ)
|
||||||
loaded = static_cast<SfizzInstrument*>(inst.get())->loadSfzFile(path, sampleRate);
|
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;
|
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.
|
// Replacing an existing instrument drops its voices with the old engine.
|
||||||
channels_[channel] = std::move(inst);
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
|
INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
auto it = channels_.find(channel);
|
auto it = channels_.find(channel);
|
||||||
return it == channels_.end() ? nullptr : it->second.get();
|
return it == channels_.end() ? nullptr : it->second.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InstrumentEngineManager::allNotesOff() {
|
void InstrumentEngineManager::allNotesOff() {
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
for (auto& [ch, inst] : channels_) {
|
for (auto& [ch, inst] : channels_) {
|
||||||
for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0);
|
for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) {
|
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(outputL, 0, numSamples * sizeof(float));
|
||||||
std::memset(outputR, 0, numSamples * sizeof(float));
|
std::memset(outputR, 0, numSamples * sizeof(float));
|
||||||
if (channels_.empty()) return;
|
if (channels_.empty()) return;
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ ShmHandle* shm_open(const char* name) {
|
|||||||
if (!h->view) { CloseHandle(h->map); delete h; return nullptr; }
|
if (!h->view) { CloseHandle(h->map); delete h; return nullptr; }
|
||||||
return h;
|
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) {
|
void shm_close(ShmHandle* h) {
|
||||||
if (!h) return;
|
if (!h) return;
|
||||||
if (h->view) UnmapViewOfFile(h->view);
|
if (h->view) UnmapViewOfFile(h->view);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// native_bridge/src/Vst3Instrument.cpp
|
// native_bridge/src/Vst3Instrument.cpp
|
||||||
// VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/).
|
// VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/).
|
||||||
//
|
//
|
||||||
// Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every
|
// Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every
|
||||||
@@ -10,16 +10,25 @@
|
|||||||
#include "Vst3Instrument.h"
|
#include "Vst3Instrument.h"
|
||||||
|
|
||||||
#ifdef HAVE_VST3SDK
|
#ifdef HAVE_VST3SDK
|
||||||
#include "public.sdk/source/main/pluginfactory.h"
|
#include "public.sdk/source/vst/hosting/module.h"
|
||||||
#include "pluginterfaces/base/ibstream.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/ivstaudioprocessor.h"
|
||||||
|
#include "pluginterfaces/vst/ivstcomponent.h"
|
||||||
#include "pluginterfaces/vst/ivsteditcontroller.h"
|
#include "pluginterfaces/vst/ivsteditcontroller.h"
|
||||||
#include "pluginterfaces/vst/ivstmidicontrollers.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 "pluginterfaces/gui/iplugview.h"
|
||||||
|
|
||||||
#include "public.sdk/source/common/pluginview.h"
|
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -27,60 +36,155 @@
|
|||||||
// With-SDK implementation
|
// With-SDK implementation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
#ifdef HAVE_VST3SDK
|
#ifdef HAVE_VST3SDK
|
||||||
#include "public.sdk/source/main/module.h"
|
|
||||||
|
|
||||||
namespace {
|
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::IComponent;
|
||||||
using Steinberg::Vst::IEditController;
|
using Steinberg::Vst::IEditController;
|
||||||
using Steinberg::Vst::IAudioProcessor;
|
using Steinberg::Vst::IAudioProcessor;
|
||||||
using Steinberg::Vst::IComponentHandler;
|
using Steinberg::Vst::IComponentHandler;
|
||||||
|
using Steinberg::IPluginBase;
|
||||||
|
using Steinberg::Vst::IConnectionPoint;
|
||||||
using Steinberg::Vst::IParameterChanges;
|
using Steinberg::Vst::IParameterChanges;
|
||||||
using Steinberg::Vst::IEventList;
|
|
||||||
using Steinberg::Vst::Event;
|
using Steinberg::Vst::Event;
|
||||||
using Steinberg::Vst::NoteOnEvent;
|
using Steinberg::Vst::ProcessSetup;
|
||||||
using Steinberg::Vst::NoteOffEvent;
|
using Steinberg::Vst::ProcessContext;
|
||||||
using Steinberg::Vst::DataEvent;
|
using Steinberg::Vst::HostProcessData;
|
||||||
using Steinberg::Vst::kMidiCC;
|
using Steinberg::Vst::EventList;
|
||||||
using Steinberg::Vst::kMidiPitchBend;
|
using Steinberg::Vst::ParameterChanges;
|
||||||
using Steinberg::Vst::kMidiProgramChange;
|
using Steinberg::Vst::HostApplication;
|
||||||
using Steinberg::IPlugView;
|
using Steinberg::Vst::ParamID;
|
||||||
using Steinberg::tresult;
|
using Steinberg::Vst::ParamValue;
|
||||||
using Steinberg::kResultOk;
|
using Steinberg::Vst::IMidiMapping;
|
||||||
using Steinberg::kResultFalse;
|
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.
|
// Minimal IComponentHandler so the plugin can inform the host of param edits.
|
||||||
class HostComponentHandler : public IComponentHandler {
|
class HostComponentHandler : public IComponentHandler {
|
||||||
public:
|
public:
|
||||||
Steinberg::tresult queryInterface(const Steinberg::TUID&, void** v) override {
|
tresult queryInterface(const char*, void** v) override {
|
||||||
*v = nullptr;
|
*v = nullptr;
|
||||||
return Steinberg::kNoInterface;
|
return kNoInterface;
|
||||||
}
|
}
|
||||||
Steinberg::uint32 addRef() override { return 1; }
|
Steinberg::uint32 addRef() override { return 1; }
|
||||||
Steinberg::uint32 release() override { return 1; }
|
Steinberg::uint32 release() override { return 1; }
|
||||||
Steinberg::tresult beginEdit(Steinberg::Vst::ParamID) override { return Steinberg::kResultOk; }
|
tresult beginEdit(ParamID) override { return kResultOk; }
|
||||||
Steinberg::tresult performEdit(Steinberg::Vst::ParamID, Steinberg::Vst::ParamValue) override {
|
tresult performEdit(ParamID, ParamValue) override { return kResultOk; }
|
||||||
return Steinberg::kResultOk;
|
tresult endEdit(ParamID) override { return kResultOk; }
|
||||||
}
|
tresult restartComponent(Steinberg::int32) override { return kResultOk; }
|
||||||
Steinberg::tresult endEdit(Steinberg::Vst::ParamID) override { return Steinberg::kResultOk; }
|
|
||||||
Steinberg::tresult restartComponent(Steinberg::int32) override { return Steinberg::kResultOk; }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bundle both components in one factory entry so module.load() gives us the
|
// Minimal IPlugFrame so plugins can resize their editor view.
|
||||||
// component; controller is created via IComponent::createController.
|
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
|
} // namespace
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Vst3Instrument::Vst3Instrument()
|
Vst3Instrument::Vst3Instrument()
|
||||||
: module_(nullptr),
|
: state_(nullptr),
|
||||||
processor_(nullptr),
|
path_(),
|
||||||
controller_(nullptr),
|
|
||||||
view_(nullptr),
|
|
||||||
sampleRate_(44100.0),
|
sampleRate_(44100.0),
|
||||||
maxBlockSize_(256),
|
maxBlockSize_(256),
|
||||||
loaded_(false),
|
loaded_(false),
|
||||||
guiAttached_(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) {
|
bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
|
||||||
#ifndef HAVE_VST3SDK
|
#ifndef HAVE_VST3SDK
|
||||||
@@ -88,15 +192,159 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
|
|||||||
return false; // vst3sdk submodule missing — VST3 disabled
|
return false; // vst3sdk submodule missing — VST3 disabled
|
||||||
#else
|
#else
|
||||||
if (loaded_) return true;
|
if (loaded_) return true;
|
||||||
// ponytail: vst3sdk module loading is platform-specific
|
using namespace VST3::Hosting;
|
||||||
// (Module::create on Windows .vst3 bundle / macOS .vst3 framework). Keep
|
|
||||||
// the classic pluginfactory-based load for Windows bundles:
|
std::string err;
|
||||||
// void* handle = Steinberg::Vst::Module::create(...)
|
std::cerr << "[dbg] loadPlugin: Module::create ..." << std::endl;
|
||||||
// and use module->getFactory().createInstance<...>(cid).
|
Module::Ptr module = Module::create(path, err);
|
||||||
// Verified on Windows in A6 (G3 Vital.vst3). Until then VST3 returns false
|
if (!module) {
|
||||||
// so SF2/SFZ keep working.
|
std::cerr << "[Vst3Instrument] Module::create failed: " << err << std::endl;
|
||||||
(void)path; (void)sampleRate;
|
return false;
|
||||||
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,10 +364,21 @@ void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, ui
|
|||||||
#ifndef HAVE_VST3SDK
|
#ifndef HAVE_VST3SDK
|
||||||
return;
|
return;
|
||||||
#else
|
#else
|
||||||
if (!processor_) return;
|
auto* s = static_cast<Vst3HostState*>(state_);
|
||||||
// TODO(A6): queue NoteOnEvent into the per-block event list; see
|
if (!s || !s->component) return;
|
||||||
// processAudioBlock. Wiring requires IAudioProcessor::process with
|
Event e = {};
|
||||||
// ProcessData — implemented together with loadPlugin on Windows.
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,8 +387,20 @@ void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOf
|
|||||||
#ifndef HAVE_VST3SDK
|
#ifndef HAVE_VST3SDK
|
||||||
return;
|
return;
|
||||||
#else
|
#else
|
||||||
if (!processor_) return;
|
auto* s = static_cast<Vst3HostState*>(state_);
|
||||||
// TODO(A6): queue NoteOffEvent (see noteOn)
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,8 +409,16 @@ void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value
|
|||||||
#ifndef HAVE_VST3SDK
|
#ifndef HAVE_VST3SDK
|
||||||
return;
|
return;
|
||||||
#else
|
#else
|
||||||
if (!controller_) return;
|
auto* s = static_cast<Vst3HostState*>(state_);
|
||||||
// TODO(A6): controller_->setParamNormalized(kMidiCC | cc, value/127.0)
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,8 +427,15 @@ void Vst3Instrument::programChange(uint32_t channel, uint32_t program) {
|
|||||||
#ifndef HAVE_VST3SDK
|
#ifndef HAVE_VST3SDK
|
||||||
return;
|
return;
|
||||||
#else
|
#else
|
||||||
if (!controller_) return;
|
auto* s = static_cast<Vst3HostState*>(state_);
|
||||||
// TODO(A6): setParamNormalized(kMidiProgramChange | program, ...)
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,8 +444,14 @@ void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
|||||||
#ifndef HAVE_VST3SDK
|
#ifndef HAVE_VST3SDK
|
||||||
return;
|
return;
|
||||||
#else
|
#else
|
||||||
if (!controller_) return;
|
auto* s = static_cast<Vst3HostState*>(state_);
|
||||||
// TODO(A6): setParamNormalized(kMidiPitchBend, bend14/16383.0)
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,11 +460,54 @@ bool Vst3Instrument::openGUI(void* parentWindowHandle) {
|
|||||||
(void)parentWindowHandle;
|
(void)parentWindowHandle;
|
||||||
return false;
|
return false;
|
||||||
#else
|
#else
|
||||||
if (!processor_ || !controller_ || !parentWindowHandle) return false;
|
auto* s = static_cast<Vst3HostState*>(state_);
|
||||||
if (guiAttached_) return true;
|
std::cerr << "[dbg] openGUI hwnd=" << (void*)(uintptr_t)parentWindowHandle
|
||||||
// TODO(A6): FUnknownPtr<IPlugView> view(controller_);
|
<< " controller=" << (s ? (s->controller ? 1 : 0) : -1) << std::endl;
|
||||||
// view->setFrame(parentWindowHandle); view->attached(...); view_ = view;
|
if (!s || !s->controller || !parentWindowHandle) return false;
|
||||||
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,10 +515,9 @@ void Vst3Instrument::closeGUI() {
|
|||||||
#ifndef HAVE_VST3SDK
|
#ifndef HAVE_VST3SDK
|
||||||
return;
|
return;
|
||||||
#else
|
#else
|
||||||
if (view_ && guiAttached_) {
|
auto* s = static_cast<Vst3HostState*>(state_);
|
||||||
// view_->removed(); view_->setFrame(nullptr);
|
if (s && s->view && guiAttached_) s->view->removed();
|
||||||
}
|
if (s) s->view = nullptr;
|
||||||
view_ = nullptr;
|
|
||||||
guiAttached_ = false;
|
guiAttached_ = false;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -193,7 +527,66 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
|
|||||||
#ifndef HAVE_VST3SDK
|
#ifndef HAVE_VST3SDK
|
||||||
return;
|
return;
|
||||||
#else
|
#else
|
||||||
// TODO(A6): ProcessData with 2 output buffers + event list; called by
|
auto* s = static_cast<Vst3HostState*>(state_);
|
||||||
// InstrumentEngineManager::renderAll via processAudioBlock.
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
+190
-10
@@ -9,6 +9,7 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <mmsystem.h>
|
#include <mmsystem.h>
|
||||||
#include <process.h>
|
#include <process.h>
|
||||||
|
#include <thread>
|
||||||
#else
|
#else
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -17,9 +18,14 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <condition_variable>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
// --- platform helpers -------------------------------------------------------
|
// --- platform helpers -------------------------------------------------------
|
||||||
@@ -42,6 +48,98 @@ static void sleep_ms(uint32_t ms) {
|
|||||||
#endif
|
#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[]) {
|
int main(int argc, char* argv[]) {
|
||||||
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
|
std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;
|
||||||
|
|
||||||
@@ -73,6 +171,12 @@ int main(int argc, char* argv[]) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
InstrumentEngineManager instruments;
|
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).
|
// 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
|
// Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) — SF_BLOCK_SIZE
|
||||||
// is accepted but must match, otherwise warned and ignored.
|
// 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)
|
if (!inst) return; // channel chưa gán instrument → silent (A10)
|
||||||
switch (evt.command) {
|
switch (evt.command) {
|
||||||
case 0x9:
|
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)
|
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
|
else
|
||||||
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
|
inst->noteOff(evt.channel, evt.pitch, 0);
|
||||||
break;
|
break;
|
||||||
case 0x8:
|
case 0x8:
|
||||||
inst->noteOff(evt.channel, evt.pitch, evt.sampleOffset);
|
inst->noteOff(evt.channel, evt.pitch, 0);
|
||||||
break;
|
break;
|
||||||
case 0xB: // CC: controller number in pitch, value in data2
|
case 0xB: // CC: controller number in pitch, value in data2
|
||||||
inst->controlChange(evt.channel, evt.pitch, evt.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
|
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
|
||||||
while (true) {
|
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
|
// A. Control events — non-rt safe, drained first
|
||||||
for (uint32_t i = 0; i < shmIPC->controlQueueCount; ++i) {
|
for (uint32_t i = 0; i < shmIPC->controlQueueCount; ++i) {
|
||||||
const auto& c = shmIPC->controlQueue[i];
|
const auto& c = shmIPC->controlQueue[i];
|
||||||
if (c.type == 2) { // LOAD_INSTRUMENT (A10: assign per MIDI channel)
|
if (c.type == 2) { // LOAD_INSTRUMENT (A10: assign per MIDI channel)
|
||||||
// Robust path length: do not trust arg1 (Rust passes 0 for LOAD).
|
// 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;
|
size_t plen = 0;
|
||||||
while (plen < sizeof(c.arg2) && c.arg2[plen]) ++plen;
|
while (plen < sizeof(c.arg2) && c.arg2[plen]) ++plen;
|
||||||
std::string path(c.arg2, plen);
|
std::string path(c.arg2, plen);
|
||||||
InstrumentType t = (InstrumentType)c.arg0;
|
InstrumentType t = (InstrumentType)c.arg0;
|
||||||
uint32_t ch = c.channel & 0xF;
|
uint32_t ch = c.channel & 0xF;
|
||||||
if (instruments.assign(ch, t, path, sampleRate, block)) {
|
if (!workers[ch]) workers[ch] = std::make_unique<ChannelWorker>();
|
||||||
std::cout << "[NativeBridge] instrument loaded ch=" << ch
|
workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() {
|
||||||
<< " type=" << (int)c.arg0 << " " << path << std::endl;
|
std::cerr << "[dbg] load thread start ch=" << ch
|
||||||
} else {
|
<< " type=" << (int)t << " path=" << path << std::endl;
|
||||||
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
|
bool ok = instruments.assign(ch, t, path, sampleRate, block);
|
||||||
<< " type=" << (int)c.arg0 << " " << path << std::endl;
|
std::cerr << "[dbg] assign returned ch=" << ch << " ok=" << (ok ? 1 : 0) << std::endl;
|
||||||
}
|
if (ok) {
|
||||||
|
std::cout << "[NativeBridge] instrument loaded ch=" << ch
|
||||||
|
<< " type=" << (int)t << " " << path << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
|
||||||
|
<< " type=" << (int)t << " " << path << std::endl;
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (c.type == 1) { // PANIC
|
} else if (c.type == 1) { // PANIC
|
||||||
instruments.allNotesOff();
|
instruments.allNotesOff();
|
||||||
std::cout << "[NativeBridge] PANIC — all notes off" << std::endl;
|
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)
|
} else if (c.arg0 == 2) { // SET_POSITION (seek while stopped)
|
||||||
playheadSamples = c.arg1;
|
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;
|
shmIPC->controlQueueCount = 0;
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<region> sample=dummy1.wav key=60 lokey=0 hikey=127
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -11,6 +11,11 @@
|
|||||||
int main() {
|
int main() {
|
||||||
// Use a test-only name to avoid clashing with a live DAW bridge.
|
// Use a test-only name to avoid clashing with a live DAW bridge.
|
||||||
ShmHandle* h = shm_open("SonicForge_SelfCheck_IPC");
|
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);
|
assert(h != nullptr);
|
||||||
|
|
||||||
SharedAudioBufferIPC* ipc = shm_ptr(h);
|
SharedAudioBufferIPC* ipc = shm_ptr(h);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"version-string": "1.0.0",
|
"version-string": "1.0.0",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"fluidsynth",
|
"fluidsynth",
|
||||||
"sfizz",
|
|
||||||
"pkgconf"
|
"pkgconf"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+235
@@ -13,6 +13,9 @@
|
|||||||
"jsdom": "^30.0.1",
|
"jsdom": "^30.0.1",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.11.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@asamuzakjp/css-color": {
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
@@ -608,6 +611,238 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@types/gensync": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz",
|
||||||
|
|||||||
@@ -12,5 +12,8 @@
|
|||||||
"jsdom": "^30.0.1",
|
"jsdom": "^30.0.1",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.11.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,17 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = ["webview-data-url"] }
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
# Shared memory (SonicForge_DAW_IPC) for the Native Host Bridge
|
# Shared memory (SonicForge_DAW_IPC) for the Native Host Bridge
|
||||||
|
raw-window-handle = "0.6"
|
||||||
windows-sys = { version = "0.59", features = [
|
windows-sys = { version = "0.59", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_System_MemoryManagement",
|
"Win32_Security",
|
||||||
|
"Win32_System_Memory",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
PLACEHOLDER - duoc thay boi build_windows.ps1 buoc [4/6] (copy dist\daw_engine)
|
|
||||||
+97
-36
@@ -12,7 +12,7 @@
|
|||||||
// và ghi đầy đủ diagnostic vào %APPDATA%/SonicForgeDAW/logs/spawn.log.
|
// và ghi đầy đủ diagnostic vào %APPDATA%/SonicForgeDAW/logs/spawn.log.
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
use tauri_plugin_dialog::{DialogExt, FilePath};
|
use tauri_plugin_dialog::{DialogExt, FilePath};
|
||||||
use tauri_plugin_shell::process::CommandChild;
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
@@ -23,6 +23,30 @@ use shm::{Shm, ShmState};
|
|||||||
struct EngineProcess(Mutex<Option<CommandChild>>);
|
struct EngineProcess(Mutex<Option<CommandChild>>);
|
||||||
struct BridgeProcess(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).
|
/// Audio frame pushed to the WebView (bridge SHM -> `bridge-audio` event).
|
||||||
#[derive(Clone, serde::Serialize)]
|
#[derive(Clone, serde::Serialize)]
|
||||||
struct AudioFrame {
|
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("binaries").join(exe_name), "resource_dir/binaries (bundle)".into()),
|
||||||
(res_dir.join(exe_name), "resource_dir (bundle root)".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("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(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("binaries").join(exe_name), "exe_dir/../resources/binaries (dev)".into()),
|
||||||
(exe_dir.join("..").join("resources").join(exe_name), "exe_dir/../resources (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()));
|
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()
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
@@ -109,8 +138,32 @@ fn spawn_bridge(
|
|||||||
.env("SF_BLOCK_SIZE", "256")
|
.env("SF_BLOCK_SIZE", "256")
|
||||||
.spawn()
|
.spawn()
|
||||||
{
|
{
|
||||||
Ok((_rx, child)) => {
|
Ok((mut rx, child)) => {
|
||||||
app.manage(BridgeProcess(Mutex::new(Some(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());
|
println!("Native Host Bridge started ({label}): {}", bridge_exe.display());
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -290,6 +343,9 @@ pub fn run() {
|
|||||||
if advanced == Some(true) {
|
if advanced == Some(true) {
|
||||||
last_change = std::time::Instant::now();
|
last_change = std::time::Instant::now();
|
||||||
down_emitted = false;
|
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 (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(
|
let _ = pump_handle.emit(
|
||||||
"bridge-audio",
|
"bridge-audio",
|
||||||
@@ -308,6 +364,9 @@ pub fn run() {
|
|||||||
let log_path = std::path::Path::new(&log_dir)
|
let log_path = std::path::Path::new(&log_dir)
|
||||||
.join("SonicForgeDAW").join("logs").join("spawn.log");
|
.join("SonicForgeDAW").join("logs").join("spawn.log");
|
||||||
let mut line = String::from("[tauri] bridge stalled 3s — restart attempt #1\n");
|
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) {
|
if spawn_bridge(&pump_handle, &res_dir, &exe_dir, &mut line, &log_path) {
|
||||||
// count restarts into bridge.log (same file as stdout redirect)
|
// count restarts into bridge.log (same file as stdout redirect)
|
||||||
let bridge_log = std::path::Path::new(&log_dir)
|
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")?;
|
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
|
||||||
shm.push_midi(command, channel, pitch, velocity, data2, data3, sample_offset)
|
shm.push_midi(command, channel, pitch, velocity, data2, data3, sample_offset)
|
||||||
.then_some(())
|
.then_some(())
|
||||||
.ok_or("midi queue full")
|
.ok_or_else(|| "midi queue full".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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")?;
|
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
|
||||||
shm.push_control(2, instrument_type as u32, 0, channel as u32, &path)
|
shm.push_control(2, instrument_type as u32, 0, channel as u32, &path)
|
||||||
.then_some(())
|
.then_some(())
|
||||||
.ok_or("control queue full")
|
.ok_or_else(|| "control queue full".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn open_vst_gui(app: AppHandle, plugin_id: String) -> Result<(), String> {
|
fn open_vst_gui(app: AppHandle, plugin_id: String) -> Result<(), String> {
|
||||||
// B9: child WebviewWindow acts as the VST GUI surface. The bridge receives
|
// B9: bridge tự tạo native Win32 window cho editor VST3 (không qua
|
||||||
// its HWND (control type=4, arg1) so the VST3 editor can be attached (A7).
|
// WebView2 — HTML window cũ vẽ ĐÈ lên GUI plugin). push_control(4,0,0,0)
|
||||||
let url = tauri::WebviewUrl::External(
|
// với hwnd=0 báo bridge tạo window riêng trên ChannelWorker thread.
|
||||||
tauri::Url::parse("data:text/html,<h2>VST GUI</h2><p>Editor attaches from daw_vst_bridge (A7).</p>")
|
|
||||||
.map_err(|e| e.to_string())?,
|
|
||||||
);
|
|
||||||
let win = tauri::WebviewWindowBuilder::new(&app, format!("vst-{}", plugin_id), url)
|
|
||||||
.title(format!("VST — {}", plugin_id))
|
|
||||||
.inner_size(800.0, 600.0)
|
|
||||||
.build()
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let state = app.state::<ShmState>();
|
let state = app.state::<ShmState>();
|
||||||
let guard = state.0.lock().map_err(|e| e.to_string())?;
|
let lock = state.0.lock();
|
||||||
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
|
match lock {
|
||||||
// ponytail: HWND truncated to u32 (HWNDs fit in practice); window-close →
|
Ok(guard) => match guard.as_ref() {
|
||||||
// close_gui wiring lands together with A7 on Windows.
|
Some(shm) => {
|
||||||
#[cfg(windows)]
|
let ok = shm.push_control(4, 0, 0, 0, &plugin_id);
|
||||||
let hwnd = {
|
eprintln!("open_vst_gui: push_control type=4 hwnd=0 (bridge-native) ok={}", ok);
|
||||||
use tauri::raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
}
|
||||||
match win.window_handle().map(|h| h.as_raw()) {
|
None => eprintln!("open_vst_gui: bridge shm unavailable"),
|
||||||
Ok(RawWindowHandle::Win32(w)) => w.hwnd as usize as u32,
|
},
|
||||||
_ => 0u32,
|
Err(e) => eprintln!("open_vst_gui: shm lock poisoned: {}", e),
|
||||||
}
|
}
|
||||||
};
|
Ok(())
|
||||||
#[cfg(not(windows))]
|
|
||||||
let hwnd = 0u32;
|
|
||||||
shm.push_control(4, 0, hwnd, 0, &plugin_id)
|
|
||||||
.then_some(())
|
|
||||||
.ok_or("control queue full")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@@ -494,11 +539,18 @@ struct BridgeStatus {
|
|||||||
block_size: u32,
|
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]
|
#[tauri::command]
|
||||||
fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
|
fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
|
||||||
let state = app.state::<ShmState>();
|
let state = app.state::<ShmState>();
|
||||||
let guard = state.0.lock().map_err(|e| e.to_string())?;
|
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 {
|
Some(shm) => BridgeStatus {
|
||||||
connected: true,
|
connected: true,
|
||||||
shm_name: shm::SHM_NAME,
|
shm_name: shm::SHM_NAME,
|
||||||
@@ -517,7 +569,16 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
|
|||||||
sample_rate: 0,
|
sample_rate: 0,
|
||||||
block_size: 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]
|
#[tauri::command]
|
||||||
@@ -533,5 +594,5 @@ fn transport_control(app: AppHandle, kind: String, playhead: Option<u32>) -> Res
|
|||||||
};
|
};
|
||||||
shm.push_control(3, arg0, arg1, 0, "")
|
shm.push_control(3, arg0, arg1, 0, "")
|
||||||
.then_some(())
|
.then_some(())
|
||||||
.ok_or("control queue full")
|
.ok_or_else(|| "control queue full".to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-4
@@ -8,7 +8,7 @@ use std::ffi::c_void;
|
|||||||
use std::ptr;
|
use std::ptr;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
|
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,
|
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,13 +83,13 @@ impl Shm {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) };
|
let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) };
|
||||||
if view.is_null() {
|
if view.Value.is_null() {
|
||||||
unsafe { CloseHandle(handle) };
|
unsafe { CloseHandle(handle) };
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(Shm {
|
Some(Shm {
|
||||||
handle,
|
handle,
|
||||||
view: view as *mut SharedAudioBufferIPC,
|
view: view.Value as *mut SharedAudioBufferIPC,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ impl Shm {
|
|||||||
impl Drop for Shm {
|
impl Drop for Shm {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if !self.view.is_null() {
|
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) };
|
unsafe { CloseHandle(self.handle) };
|
||||||
}
|
}
|
||||||
@@ -164,3 +164,15 @@ impl Drop for Shm {
|
|||||||
|
|
||||||
/// Process-wide shared-memory state for Tauri.
|
/// Process-wide shared-memory state for Tauri.
|
||||||
pub struct ShmState(pub Mutex<Option<Shm>>);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"icons/favicon.ico"
|
"icons/favicon.ico"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"resources/daw_engine": "daw_engine/"
|
"resources/daw_engine": "daw_engine/",
|
||||||
|
"binaries/libfluidsynth-3.dll": "libfluidsynth-3.dll"
|
||||||
},
|
},
|
||||||
"externalBin": ["binaries/daw_vst_bridge"],
|
"externalBin": ["binaries/daw_vst_bridge"],
|
||||||
"windows": {
|
"windows": {
|
||||||
|
|||||||
+38
-16
@@ -1,4 +1,4 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="vi">
|
<html lang="vi">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@@ -12,23 +12,45 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="msg">Đang khởi động SonicForge Engine…</div>
|
<div id="msg">Đang khởi động SonicForge Engine…</div>
|
||||||
<script>
|
<script>
|
||||||
// Engine (sidecar) có thể cần vài giây để boot (import librosa/pedalboard).
|
// Frontend host (Tauri asset page). KHÔNG location.href redirect: chuyển
|
||||||
// Thử health-check trên dải port 8000-8010, redirect tới port đầu tiên OK.
|
// 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 () {
|
(async function () {
|
||||||
var tries = 0;
|
var base = null;
|
||||||
while (tries < 90) { // ~60s tối đa
|
for (var p = 8000; p <= 8010 && !base; p++) {
|
||||||
for (var p = 8000; p <= 8010; p++) {
|
try {
|
||||||
try {
|
var r = await fetch('http://127.0.0.1:' + p + '/health', { cache: 'no-store' });
|
||||||
var r = await fetch('http://127.0.0.1:' + p + '/health', { cache: 'no-store' });
|
if (r.ok) base = 'http://127.0.0.1:' + p + '/';
|
||||||
if (r.ok) { location.href = 'http://127.0.0.1:' + p + '/'; return; }
|
} catch (e) { /* port chưa mở */ }
|
||||||
} catch (e) { /* port chưa mở */ }
|
|
||||||
}
|
|
||||||
tries++;
|
|
||||||
await new Promise(function (res) { setTimeout(res, 700); });
|
|
||||||
}
|
}
|
||||||
document.getElementById('msg').textContent =
|
if (!base) {
|
||||||
'Không tìm thấy SonicForge Engine (port 8000–8010). Hãy đóng và chạy lại ứng dụng. ' +
|
document.getElementById('msg').textContent =
|
||||||
'Nếu lặp lại, xem log: %APPDATA%\\SonicForgeDAW\\logs\\engine.log';
|
'Không tìm thấy SonicForge Engine (port 8000–8010). 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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user