Compare commits
368 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c32c77e4f4 | |||
| 8e4fed5f05 | |||
| 33c5cc290f | |||
| f793ebce9b | |||
| d3094a3ba5 | |||
| d068927694 | |||
| 5b90bd43b5 | |||
| 4da46d87c8 | |||
| aaf58db56f | |||
| be5ecf0a48 | |||
| 31f19fa537 | |||
| de8abd12fa | |||
| 1acb6f23f1 | |||
| a8a3038dd2 | |||
| 67b8658015 | |||
| 2c4d6c358b | |||
| 1e0314249d | |||
| c1b73db971 | |||
| c8e2d0f6e2 | |||
| 3aed7e93ea | |||
| 855a753e26 | |||
| aad25f4049 | |||
| 400b5e5370 | |||
| e691c43e62 | |||
| 1932467539 | |||
| 4cd8fa780d | |||
| a8082b27c4 | |||
| 4713f8a8a1 | |||
| 5f8844637e | |||
| 21cc8421a5 | |||
| 833f3bd158 | |||
| a069773d4a | |||
| 4ed1de638d | |||
| b64343aac6 | |||
| 72ae2c8a9a | |||
| fbefa7e864 | |||
| cdf71078f8 | |||
| 6cd4eb423a | |||
| 2fe78ddd1b | |||
| 7572d339de | |||
| 75b1512779 | |||
| 36f2cf79b1 | |||
| 8a6a79c757 | |||
| 8ccf7e4b14 | |||
| 80b8c3e034 | |||
| 39f00c3c0b | |||
| 752db690b3 | |||
| ad71ac1168 | |||
| 845823a9df | |||
| f6104a43d5 | |||
| e57b9fade2 | |||
| 6b18d25520 | |||
| 4c0987733e | |||
| 85fdcb16e2 | |||
| c0fbf29d41 | |||
| 945e63918d | |||
| 72ae22f865 | |||
| 82796417c1 | |||
| a7630f0e6d | |||
| 788a371f72 | |||
| f3fd0109a7 | |||
| 6b26d5db0e | |||
| c084ac8ba7 | |||
| 2b334c0812 | |||
| 2a3e5bf08f | |||
| 2afa828c8f | |||
| 8990f04357 | |||
| bf74a0be4f | |||
| 6cbdbd8e9c | |||
| c72362e088 | |||
| 091abe5a91 | |||
| 9c0d35def4 | |||
| d7dd626dd5 | |||
| d78af9304f | |||
| ebe17d902b | |||
| a10f679c0d | |||
| 51bac2770d | |||
| fa61712ecc | |||
| 9ccc28d211 | |||
| fa01ef02f7 | |||
| f38e500326 | |||
| 28a06134f9 | |||
| 2e996af102 | |||
| b95c63e7db | |||
| 3e6080603c | |||
| 1f78a834a8 | |||
| e53a31b525 | |||
| 02ebf41e97 | |||
| a0f5f98209 | |||
| 0e555114f8 | |||
| ad3dfa5c47 | |||
| 24109f8bc4 | |||
| e54dec4010 | |||
| f10f9873e9 | |||
| 5689536fb1 | |||
| 56235623cd | |||
| e6b5e7f4d7 | |||
| fd50d99250 | |||
| b63e5b21d6 | |||
| 74dccaf54a | |||
| a800834c89 | |||
| 2c96a223af | |||
| c146d3eb14 | |||
| 26086cb331 | |||
| 57fa2e78fe | |||
| 014d02283d | |||
| ae7299d097 | |||
| de4caefb50 | |||
| 6cee46d82a | |||
| ebf0828c55 | |||
| 51d948fdc9 | |||
| dd17f87e4e | |||
| 4037dbf40d | |||
| ea3de8b53c | |||
| cd63ae1901 | |||
| 662383e538 | |||
| 8ef1935068 | |||
| 388da8b380 | |||
| 109a8f5c5b | |||
| 60d1eee107 | |||
| a00c57d44f | |||
| fb59184dd2 | |||
| 51891ac8b9 | |||
| f8a500c050 | |||
| f741e1e1f1 | |||
| ba7c03ca5e | |||
| 9cd6113086 | |||
| bf98038572 | |||
| 74bd6d849b | |||
| 4b643883ff | |||
| 1061a36626 | |||
| 141fa4a141 | |||
| 2b7c7cef2c | |||
| 5b747606c7 | |||
| 2bcc8369a5 | |||
| 8639529ecd | |||
| 43b68200aa | |||
| 8c972c3df2 | |||
| 14f8340fdf | |||
| fefe95d0a9 | |||
| edbb477657 | |||
| 993afb9ee4 | |||
| 26ca6e6ae0 | |||
| cea3b08999 | |||
| 51e62a1158 | |||
| 09bc762218 | |||
| 9c81d9c1fa | |||
| 6ec249ef13 | |||
| 59e4951912 | |||
| 3a3f46b7d3 | |||
| 9137e7e276 | |||
| 348079ceae | |||
| 69bb449958 | |||
| 559d52e3c5 | |||
| 5c7fc2c779 | |||
| c10d2c2f14 | |||
| 272382f73c | |||
| 36aa15e2eb | |||
| e12a8d754a | |||
| 09d8a386fb | |||
| 86d5bbc372 | |||
| 4240f15e30 | |||
| b907db3251 | |||
| 5ca2027966 | |||
| 3c9f1c5809 | |||
| 21130858d6 | |||
| 708e7de32b | |||
| 10997a65b9 | |||
| 373a96cf7b | |||
| 80c663e419 | |||
| 2ad833fbe7 | |||
| 6176cf3f8e | |||
| 6edfe1db32 | |||
| 9bb9a234b0 | |||
| 0acc3ee7ee | |||
| bec82a20bc | |||
| a105bdb959 | |||
| 1d4ff1282b | |||
| 94a0bc7401 | |||
| 2b5f396133 | |||
| 6b2c9a039b | |||
| db12698ba2 | |||
| 3ad379f0ba | |||
| c8c49b6b9c | |||
| 5d37c0683b | |||
| 35076f680b | |||
| 17e22bcf32 | |||
| fe82b19557 | |||
| 2a5e2af04a | |||
| 4ae2f06c57 | |||
| 11e564abc5 | |||
| c3aba40ffe | |||
| e5c957fc87 | |||
| 1390ad545b | |||
| fd790c1dc6 | |||
| 5ad2c681e7 | |||
| 14265515aa | |||
| f143646ba7 | |||
| 4bb0db2b21 | |||
| 8346bc3041 | |||
| 6dfe390bda | |||
| 6ba38286ad | |||
| e22757d5be | |||
| edc647ded6 | |||
| eef8603f98 | |||
| d74ccdaa8d | |||
| 9c6e200708 | |||
| 681cab4f25 | |||
| c07b792bd4 | |||
| 068de2cb23 | |||
| 384827d1e7 | |||
| aeec85604f | |||
| aaf97b1255 | |||
| a09a30c79d | |||
| ece5f313b3 | |||
| 442f1ff1f5 | |||
| 34d43f5018 | |||
| ac5a171464 | |||
| 2a4ae86f5a | |||
| eecc4501bc | |||
| 67245a6f61 | |||
| dd2a320b2a | |||
| 1a8b210063 | |||
| bb6658e03d | |||
| 4e3f2fd08a | |||
| 9b14c99a7a | |||
| bcd8e37e86 | |||
| a667d0354f | |||
| ec85e7d1a9 | |||
| 5fc45a59f8 | |||
| 2c93c701ce | |||
| 96da8b5ac0 | |||
| d7756b08ab | |||
| da7ae02509 | |||
| 0f6235c9b9 | |||
| e7e974b3e0 | |||
| a7b01d3484 | |||
| b91e6d3875 | |||
| d6649e4474 | |||
| c78d9f2911 | |||
| 588c0da32d | |||
| 4932bb88db | |||
| 7c22ae1f02 | |||
| 5df96aef1e | |||
| 819257a1b3 | |||
| 51f12113fa | |||
| b10b0e77f4 | |||
| 45a7bdebfa | |||
| 6a3b430da0 | |||
| 6efef85e47 | |||
| 927597a822 | |||
| 2f5c70ac29 | |||
| 8874e42596 | |||
| dbbcb85ffe | |||
| 2bbe80efa3 | |||
| 77360578fd | |||
| 466b2022f2 | |||
| 1dfba14114 | |||
| e5ce9ca90a | |||
| da0f8fa40e | |||
| c6de0720f4 | |||
| 346ada6e47 | |||
| 0334c2a5a3 | |||
| 0cf771242f | |||
| 19904bcad8 | |||
| d355016fe7 | |||
| ec8b57ecb3 | |||
| 0cde007d26 | |||
| d3a7a0f793 | |||
| d07f626a6c | |||
| 77bbbe298d | |||
| 7bc96eebbc | |||
| be8af7a6ee | |||
| 4ddaf1f26a | |||
| eb5a1483ca | |||
| 1e16189da8 | |||
| 45a15da55e | |||
| 6b6328044c | |||
| df0fcbed8a | |||
| 161a380a60 | |||
| 398ff21f0b | |||
| 50cfed3340 | |||
| ba9ceeea90 | |||
| 351ee56080 | |||
| cbcc1f658c | |||
| a2f93138be | |||
| 308857c4a7 | |||
| 4eadf897a4 | |||
| c9b35d60ff | |||
| 569be72110 | |||
| c382afe2a6 | |||
| 38f4cbd8b5 | |||
| b261eb0d7e | |||
| 8fbacd1d2b | |||
| bbde74f1a7 | |||
| dc331f39d8 | |||
| e79dae5d5c | |||
| efe26e3277 | |||
| fb63f06292 | |||
| 3d963eb8dd | |||
| 235dee103d | |||
| bbb11f8066 | |||
| 4d1a2ed54a | |||
| 0cca84c5f9 | |||
| 4111995a5c | |||
| 9d9b4b3686 | |||
| 353244708f | |||
| 98574aa0ea | |||
| e2c0564982 | |||
| 4f6e2bc049 | |||
| cead8c8c71 | |||
| ed26e1706d | |||
| 90b5a63719 | |||
| cc0a573ca9 | |||
| 781425588c | |||
| 1d40cf27b7 | |||
| b2470e1422 | |||
| cdbff49ea2 | |||
| cca58c4893 | |||
| e541d16132 | |||
| 7bcaf19c28 | |||
| 70da711520 | |||
| 223023615d | |||
| 574c2f885d | |||
| 56d8c577aa | |||
| 045d84bde0 | |||
| c29e024372 | |||
| 4c8677699c | |||
| b6950d6418 | |||
| 2d63774474 | |||
| 761b48e41f | |||
| 54b7a7a6ac | |||
| 07f0569d81 | |||
| 6c19f17b61 | |||
| 9fa3076359 | |||
| 9436e66835 | |||
| 462a5104d2 | |||
| 7de00955d8 | |||
| 700f612674 | |||
| d0da12f42b | |||
| 85273a6a7f | |||
| 40ee80ad60 | |||
| 0b8e80fe2c | |||
| 30fa98a441 | |||
| b59e416566 | |||
| d31312e824 | |||
| c2ae452844 | |||
| 4faa5a3504 | |||
| 28441566c2 | |||
| 8d9011222a | |||
| 1fe2d261fd | |||
| af9a285974 | |||
| 5607ba5440 | |||
| 3664ffbc47 | |||
| c813481357 | |||
| 7629783298 | |||
| b92c85ec73 | |||
| 9e6e72ac27 | |||
| 3587a34e31 | |||
| eac7f2feb8 | |||
| 5313d85926 | |||
| 2f7861820e | |||
| 18394807a9 | |||
| 2890856d70 | |||
| 67e73a2a85 | |||
| f94b2fa757 | |||
| 0cec3d3cce | |||
| 90241f626f |
+1
-1
@@ -26,7 +26,7 @@ PIANOBK_DIR=/home/locpham/daw_assets/pianobook
|
||||
|
||||
# ── Runtime (tự phát hiện môi trường) ──
|
||||
# auto (mặc định): Windows/macOS → desktop; Linux không DISPLAY hoặc docker → headless.
|
||||
# desktop: server + client cùng 1 máy (bật nút "Mở trong Carla" nếu có Carla local)
|
||||
# desktop: server + client cùng 1 máy (native GUI VSTi mở được trên máy)
|
||||
# headless: server docker + browser UI (soundfont + VSTi mở được từ storage mount;
|
||||
# preset chỉnh trên máy khác → upload .vstpreset qua web UI)
|
||||
SF_RUNTIME=auto
|
||||
|
||||
+19
-1
@@ -31,12 +31,15 @@ samples/
|
||||
celerybeat-schedule
|
||||
node_modules
|
||||
src-tauri/target/
|
||||
src-tauri/binaries/
|
||||
# src-tauri/binaries/: portable native bridge sidecars — COMMITTED.
|
||||
# Rebuild: build/scripts/build_native_bridge.ps1. Features native phai theo
|
||||
# push sang may khac nen commit binary (khong ignore).
|
||||
src-tauri/resources/daw_engine/
|
||||
src-tauri/vc_redist.x64.exe
|
||||
app/storage/plugin_dirs.json
|
||||
app/storage/sf_scan_state.json.bak-root
|
||||
app/storage/soundfonts/
|
||||
app/storage/carla_projects/
|
||||
|
||||
# build-time vendor trees & accidental artifacts (never commit)
|
||||
native_bridge/vcpkg_installed/
|
||||
@@ -47,3 +50,18 @@ native_bridge/sfizz-src/
|
||||
install/daw_engine/
|
||||
app/static/js/_diag.txt
|
||||
src-tauri/resources/daw_engine" && cp -r*
|
||||
|
||||
# debug/ run artifacts (screenshots, output captures, binaries) — scripts are committed
|
||||
native_bridge/debug/*.png
|
||||
native_bridge/debug/*.out
|
||||
native_bridge/debug/pidtest.txt
|
||||
native_bridge/debug/shm_selfcheck.exe
|
||||
*.obj
|
||||
|
||||
# Linux-only runtime artifacts (built via Docker; never committed)
|
||||
install/daw_vst_bridge
|
||||
install/libsfizz.so.1
|
||||
build_juce
|
||||
|
||||
# build artifact (root, khong phai deploy dir)
|
||||
target/
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
[submodule "native_bridge/vst3sdk"]
|
||||
path = native_bridge/vst3sdk
|
||||
url = https://github.com/steinbergmedia/vst3sdk.git
|
||||
[submodule "native_bridge/JUCE"]
|
||||
path = native_bridge/JUCE
|
||||
url = https://github.com/juce-framework/JUCE
|
||||
branch = 8.0.7
|
||||
shallow = true
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# 20260815 — KẾ HOẠCH THỰC HIỆN G3 (HEALTH / WATCHDOG THÔNG MINH)
|
||||
|
||||
Nguồn: `big_tasks.md` — header **G3 — Health / Watchdog thông minh (1-2 tuần)**.
|
||||
Trạng thái roadmap: G0 ✅, G1 ✅, G2 ✅ (G2.0–G2.6). Branch: `standalone-shm-bridge`.
|
||||
|
||||
---
|
||||
|
||||
## 1. PHÂN TÍCH G3
|
||||
|
||||
### 1.1 Mục tiêu
|
||||
Watchdog thông minh, cô lập lỗi:
|
||||
- G3.1 heartbeat riêng: heartbeat bridge tách khỏi logic audio.
|
||||
- G3.2 phân loại stall: phân biệt hang/crash/stall để xử lý đúng mức.
|
||||
- G3.3 SEH quanh process từng channel: 1 plugin crash → chỉ mute channel đó, không sập cả bridge.
|
||||
|
||||
### 1.2 Hiện trạng (đã verify trong code)
|
||||
| Thành phần | File | Hiện trạng liên quan G3 |
|
||||
|---|---|---|
|
||||
| SHM layout | `native_bridge/include/SharedMemoryIPC.h` + `src-tauri/src/shm.rs` (phải khớp, assert size) | `bridgeWriteIndex` @4 (mỗi block render), `attachInFlight` @11160 (G0.3). Tổng 11164B. Không có heartbeat riêng. |
|
||||
| Watchdog Rust | `src-tauri/src/lib.rs` pump thread | Liveness = `write_index()` đổi trong 3s; nếu `attach_in_flight` → deadline 15s; stall → kill + spawn lại 1 lần rồi `bridge-down`. |
|
||||
| Bridge C++ | `native_bridge/src/main.cpp` | Main loop = audio loop (render + drain control); mọi attach/load chạy trên UiThread (`uiWorker.post`); heartbeat không tồn tại. |
|
||||
|
||||
### 1.3 Khoảng trống (gap)
|
||||
1. **Liveness gắn với audio loop** — writeIndex chỉ tăng khi audio loop còn render; attach job dài (Nexus >3s, G0.3) làm audio loop nghẽn → watchdog tưởng bridge chết → phải kéo deadline lên 15s (đánh đổi: bridge thật chết cũng chờ 15s).
|
||||
2. **Không phân loại được stall** — hang (audio loop kẹt, process còn sống) vs crash (process chết) xử lý giống nhau.
|
||||
3. **1 plugin chết kéo cả bridge** — chưa có SEH/process con từng channel (G3.3).
|
||||
|
||||
### 1.4 Nguyên tắc thiết kế (đề xuất)
|
||||
- **Heartbeat là nguồn liveness duy nhất** (G3.1): bridge chạy thread riêng tăng `heartbeat` ~10Hz trong SHM, độc lập audio loop → attach dài không còn ảnh hưởng deadline; bỏ ngoại lệ `attach_in_flight` trong watchdog.
|
||||
- **Audio stall ≠ bridge chết** (G3.1 lót đường G3.2): heartbeat còn mà writeIndex đứng → log "audio stall (heartbeat OK)" — không restart.
|
||||
- **Field append-only** — giữ offset cũ (như G0.3): thêm `heartbeat` sau `attachInFlight` @11164.
|
||||
- **Không đụng audio loop** — heartbeat thread riêng, Sleep(100ms), không lock.
|
||||
|
||||
---
|
||||
|
||||
## 2. PHÂN RÃ TASKS + ESTIMATE
|
||||
|
||||
| ID | Task | File đích | Nội dung | Estimate |
|
||||
|---|---|---|---|---|
|
||||
| G3.1 | Heartbeat riêng | `SharedMemoryIPC.h`, `shm.rs`, `main.cpp`, `lib.rs`, `shm_selfcheck.cpp` | SHM thêm `heartbeat:u32` (append @11164, size 11168); bridge thread riêng tăng 100ms; Rust pump dùng heartbeat cho liveness (deadline 3s, bỏ ngoại lệ attach); writeIndex stall khi heartbeat còn → log warning, không restart. | 1 ngày |
|
||||
| G3.2 | Phân loại stall | `lib.rs`, `main.cpp` | hang vs crash vs stall; xử lý đúng mức (restart / mute / chỉ log). | 2 ngày |
|
||||
| G3.3 | SEH từng channel | `main.cpp` | 1 plugin crash → chỉ mute channel, không sập bridge. | 3 ngày |
|
||||
| | **Tổng** | | | **6 ngày (~1.5 tuần — khớp roadmap)** |
|
||||
|
||||
### Phụ thuộc
|
||||
`G3.2 → G3.1` (cần heartbeat) · `G3.3 → G3.2` (cần phân loại).
|
||||
|
||||
### Thứ tự triển khai khuyến nghị
|
||||
G3.1 → G3.2 → G3.3.
|
||||
|
||||
---
|
||||
|
||||
## 3. KẾ HOẠCH THỰC HIỆN (CÀI ĐẶT → KIỂM TRA → BẮT BUGS → FIX → REPORT)
|
||||
|
||||
### G3.1 — Heartbeat riêng
|
||||
- **Cài đặt**:
|
||||
- `SharedMemoryIPC.h`: thêm `volatile uint32_t heartbeat;` sau `attachInFlight` (append-only, offset 11164).
|
||||
- `shm.rs`: thêm field `heartbeat: u32` + accessor `heartbeat()` + sửa assert size 11168.
|
||||
- `main.cpp`: trước main loop, spawn thread `[shmIPC]{ for(;;){ Sleep(100); shmIPC->heartbeat++; } }` detach.
|
||||
- `lib.rs` pump: liveness = heartbeat (đổi trong 3s → restart 1 lần → `bridge-down`); writeIndex chỉ dùng emit audio; heartbeat còn + writeIndex đứng ≥3s → log `[tauri] audio stall (heartbeat OK)`, không restart. Bỏ `attach_in_flight` khỏi deadline.
|
||||
- `shm_selfcheck.cpp`: assert size 11168.
|
||||
- **Kiểm tra**: build bridge (build_bridge.ps1) + `cargo build --release` (src-tauri) → copy exe vào install/ → deploy → probe SHM: heartbeat tăng ~10Hz; kill bridge → heartbeat đứng → watchdog restart (log) → heartbeat tăng lại; regression G1.5 subset + G2.5 stress vẫn PASS.
|
||||
- **Bắt bugs**: attach dài không còn risk restart (bỏ ngoại lệ 15s); heartbeat sai offset (assert size bắt).
|
||||
- **Fix + report**: mục 5.
|
||||
- **📌 Lưu ý & quy trình**:
|
||||
- **Windows** (sửa code): patch bytes cho file CRLF (SharedMemoryIPC.h, main.cpp), text thường cho lib.rs (LF). Build bridge + cargo release. Deploy: kill 3 proc → copy `target/release/sonicforge-daw.exe` + `native_bridge/build/Release/daw_vst_bridge.exe` vào `install/` → start → PIDs mới. Chạy probe heartbeat + stress. Commit: `G3.1: dedicated bridge heartbeat — watchdog liveness independent of audio loop`.
|
||||
- **Linux** (ghi chú): pull + review; không chạy được (cần SHM Windows); cập nhật mục 5.
|
||||
|
||||
---
|
||||
|
||||
## 4. RỦI RO
|
||||
| Rủi ro | Ảnh hưởng | Giảm thiểu |
|
||||
|---|---|---|
|
||||
| Heartbeat thread bị kill cùng process | Vẫn restart như cũ (không tệ hơn) | OK — heartbeat chết = process chết = restart đúng. |
|
||||
| Heartbeat sai offset → đè field khác | Hỏng SHM | Assert size 11168 ở cả 2 phía (shm.rs test + shm_selfcheck.cpp). |
|
||||
| Bỏ ngoại lệ attach 15s → attach dài bị restart | Mất GUI giữa attach | Heartbeat thread độc lập → attach không làm heartbeat đứng; chỉ restart khi process thật chết. |
|
||||
| Audio loop kẹt vĩnh viễn (plugin hang) nhưng heartbeat còn | Không restart → câm vĩnh viễn | G3.2 phân loại + xử lý (log cảnh báo ở G3.1, có dấu hiệu trong log). |
|
||||
|
||||
---
|
||||
|
||||
## 5. BÁO CÁO KẾT QUẢ (điền sau khi thực hiện)
|
||||
| ID | Cài đặt | Kiểm tra | Bugs bắt được | Fix | Trạng thái |
|
||||
|---|---|---|---|---|---|
|
||||
| G3.1 | Heartbeat @11164 (size 11168), thread riêng Sleep(100) tăng `heartbeat`; pump liveness = heartbeat (deadline 3s, restart 1 lần); bỏ ngoại lệ attach 15s; writeIndex chỉ emit audio + stall log. | Probe G3.1 PASS (hb 10Hz; kill bridge → restart 3.8s, log stalled); G2.5 stress PASS; G1.5 stress PASS (fix test script đọc log 1 lần/poll); G0.4 probe PASS. | G1.5 fail trước đó là test-env: test script gọi log_lines() mỗi index (140k dòng) + window VST cũ dính → OPEN_GUI dedupe silent; flood 896 dòng allNotesOff pre-existing. | Không cần fix thêm. | ✅ |
|
||||
| G3.2 | lib.rs: khi heartbeat stalled 3s + restart_attempts==0, lấy pid từ BridgeProcess state → process_alive(pid) (OpenProcess + GetExitCodeProcess, windows-sys) → log `HUNG (pid N alive, heartbeat stalled)` / `CRASHED (pid N gone)` / `UNKNOWN (no tracked pid)`; Cargo.toml thêm feature Win32_System_Threading. | Probe CRASH PASS (kill bridge → log `CRASHED (pid 4432 gone)` sau ~10s → restart); Probe HUNG PASS (NtSuspendProcess → log `HUNG (pid 6232 alive, heartbeat stalled)` → TerminateProcess → restart). Regression G1.5 + G3.1 PASS. | Không có. | Không cần. | ✅ |
|
||||
| G3.3 | NativeInstrumentEngine.h/cpp: `crashedCh_[16]` + markCrashed/isCrashed; `channelQuiet` trả true khi crashed (mute dispatch+render); SEH helper `SafeProcessChannel` (`__try/__except EXCEPTION_EXECUTE_HANDLER`, tách khỏi hàm có C++ unwinding để tránh C2712); renderAll nếu false → `crashedCh_[ch]=true` + log muted until reload + continue (không cộng output); assign() reset flag; /EHa đã bật sẵn. CMakeLists thêm target `seh_channel_test`. | Bridge build PASS; Cargo build PASS; `seh_channel_test.exe` PASS (stub ném access violation → SafeProcessChannel false, block zero, manager isCrashed set, renderAll no-op, process sống); regression G1.5 + G3.1 PASS. | Không có. | Không cần. | ✅ |
|
||||
|
||||
### Nhật ký thực hiện
|
||||
|
||||
### G3.1 — Hoàn tất (2026-08-15)
|
||||
- SHM: `heartbeat` append @11164, tổng 11168B (SharedMemoryIPC.h + shm.rs field/accessor/assert + shm_selfcheck.cpp assert).
|
||||
- main.cpp: thread riêng `Sleep(100)` → `shmIPC->heartbeat++` (độc lập audio loop).
|
||||
- lib.rs pump: liveness = heartbeat, deadline 3s → kill+spawn 1 lần/bridge-down; bỏ attach_deadline 15s; writeIndex chỉ emit audio; heartbeat còn + writeIndex đứng → log `[tauri] audio stall (heartbeat OK)`, không restart.
|
||||
- Verify: probe heartbeat PASS (tăng ~10Hz, kill → watchdog restart 3.8s, spawn.log `bridge heartbeat stalled 3s — restart attempt #1`); G2.5 stress PASS; G1.5 stress PASS; G0.4 probe PASS.
|
||||
- G1.5 fail trước đó = test-env, không phải regression G3.1: (1) test script `wait_log` đọc cả file log mỗi index truy cập (140k+ dòng) → poll chậm >10s → STOP/OPEN_GUI timeout giả; fix: đọc 1 lần/poll. (2) window VST cũ (0x870D76, 0x2F0754) giữ view giữa các lần chạy → OPEN_GUI dedupe silent (handleOpenGui `hasAttachedView()` return không log). (3) flood 896 dòng `[midi] OFF` = allNotesOff() log từng pitch (7 ch × 128) — pre-existing, STOP log vẫn 0.15s.
|
||||
### G3.2 + G3.3 — Hoàn tất (2026-08-15)
|
||||
- G3.2: pump watchdog phân loại stall — khi heartbeat stalled 3s + chưa restart lần nào, lấy pid từ BridgeProcess state, kiểm tra process còn sống (OpenProcess/GetExitCodeProcess) → log HUNG (pid alive) / CRASHED (pid gone) / UNKNOWN (no tracked pid). Verify: CRASH test (kill bridge → `CRASHED (pid 4432 gone)` → restart bridge mới); HUNG test (NtSuspendProcess giữ process sống nhưng heartbeat đứng → `HUNG (pid 6232 alive, heartbeat stalled)` → TerminateProcess → restart). Cả 2 PASS.
|
||||
- G3.3: SEH per-channel — `SafeProcessChannel` bọc processAudioBlock trong `__try/__except(EXCEPTION_EXECUTE_HANDLER)`; plugin fault → mute channel (crashedCh_), log `[NativeBridge] G3.3: channel N plugin crashed in processAudioBlock (SEH) — muted until reload`, các channel khác tiếp tục render, bridge không sập; assign() (reload instance) reset mute. Helper tách riêng vì SEH không hợp lệ trong hàm có C++ unwinding (C2712). Verify: `seh_channel_test.exe` (stub processAudioBlock gây access violation → SafeProcessChannel trả false, crash bị nuốt, crashed flag set, renderAll no-op) PASS; bridge + cargo build PASS.
|
||||
- Regression: G1.5 stress 1 iter PASS (đã fix test script đọc log 1 lần/poll + đóng GUI VST dính giữa các lần chạy); G3.1 heartbeat probe PASS (hb ~10Hz, kill → restart 3.7s). G2.5 không chạy lại (không đụng audio loop).
|
||||
- Commit `0b8e80f` (6 files: lib.rs, Cargo.toml, NativeInstrumentEngine.h/.cpp, CMakeLists.txt, tests/seh_channel_test.cpp). `install/*.exe` không commit; `native_bridge/debug/test_g15_stress.py` (fix helper) commit riêng `85273a6`.
|
||||
- G4 (sandbox plugin): tuỳ chọn, 2-3 tháng sau.
|
||||
@@ -0,0 +1,139 @@
|
||||
# 20260815 — KẾ HOẠCH THỰC HIỆN G4 (SANDBOX PLUGIN / PROCESS CON)
|
||||
|
||||
Nguồn: `big_tasks.md` — header **G4 — Sandbox plugin (3-5x G1+G2, 2-3 tháng)**.
|
||||
Trạng thái roadmap: G0 ✅, G1 ✅, G2 ✅, G3 ✅ (G3.1–G3.3). Branch: `standalone-shm-bridge`.
|
||||
User chủ động yêu cầu: làm dirty trước, sau đó làm luôn G4.
|
||||
|
||||
---
|
||||
|
||||
## 1. PHÂN TÍCH G4
|
||||
|
||||
### 1.1 Mục tiêu
|
||||
VST3 chạy trong process con (`plugin_host.exe`) — crash plugin chỉ chết process con,
|
||||
bridge + GUI + các plugin khác sống. VST3 không có chuẩn out-of-process như AUv3 →
|
||||
wrapper tự làm.
|
||||
|
||||
### 1.2 Hiện trạng (đã verify trong code)
|
||||
| Thành phần | File | Ghi chú |
|
||||
|---|---|---|
|
||||
| VST3 host | `native_bridge/src/Vst3Instrument.cpp` + `include/Vst3Instrument.h` | pimpl (`Vst3HostState`), độc lập, reuse được trong process con: `loadPlugin(path, sr)` (tự activate + setupProcessing với maxBlockSize_), `openGUI(HWND)`, `processAudioBlock(L,R,n)` (zero output buffer trước process, copy ra ngoài), MIDI dispatch `noteOn/noteOff/controlChange/programChange/pitchBend`, `closeGUI()`, `reload()`. |
|
||||
| SHM layout | `include/SharedMemoryIPC.h` | `SharedAudioBufferIPC` 11168B: `bridgeWriteIndex`@4, `masterLeft`@8 (float[256]), `masterRight`@1032, `midiQueue[64]`@2064 (12B/evt), `midiQueueCount`@2832, `controlQueue[8]`@2836, `heartbeat`@11164 (G3.1). Helpers `shm_open/shm_create/shm_ptr/shm_close/shm_write_midi/shm_write_control` trong `SharedMemoryIPC.cpp` (trước G4.1 không có prototype trong header — đã bổ sung). |
|
||||
| CMake target pattern | `native_bridge/CMakeLists.txt` | `gui_probe`/`seh_channel_test`: `HAVE_VST3SDK=1` + `${VST3_SDK_TARGET} sdk_hosting sdk_common` + `module_win32.cpp` + `memorystream.cpp` + `${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm`. |
|
||||
|
||||
### 1.3 Khoảng trống (gap)
|
||||
1. VST3 chạy cùng process bridge → 1 plugin crash có thể hạ bridge (đã giảm thiểu phần nào bằng SEH G3.3 nhưng vẫn chung process).
|
||||
2. Chưa có cơ chế spawn con + SHM con (MIDI in / audio out / heartbeat) + giám sát respawn.
|
||||
|
||||
### 1.4 Nguyên tắc thiết kế (đề xuất)
|
||||
- Mỗi plugin 1 process con `plugin_host.exe`, argv: `--shm <name> --path <vst3> [--sr] [--block] [--parent <pid>]`.
|
||||
- Con tự tạo mapping SHM riêng (tái dùng layout `SharedAudioBufferIPC` — không phát sinh struct mới, bridge đọc được nguyên layout).
|
||||
- GUI attach trên main thread con (editorhost pattern: attach rồi pump); heartbeat ~10Hz thread riêng (pattern G3.1).
|
||||
- Con exit khi parent chết (kiểm tra `parent_alive` mỗi block) — chống orphan.
|
||||
- G4.2/G4.3: bridge mở mapping con thay vì con tự tạo; pool + watchdog respawn; ring buffer realtime.
|
||||
|
||||
---
|
||||
|
||||
## 2. PHÂN RÃ TASKS + ESTIMATE
|
||||
|
||||
| ID | Task | File đích | Nội dung | Estimate |
|
||||
|---|---|---|---|---|
|
||||
| G4.1 | PoC plugin_host độc lập | `src/plugin_host_main.cpp`, `CMakeLists.txt`, `include/SharedMemoryIPC.h` | argv(shm,path,sr,block,parent) → COM STA → load VST3 → GUI attach → audio loop qua SHM (drain MIDI → dispatch → processAudioBlock → ghi masterLeft/Right → bridgeWriteIndex++) → heartbeat thread → exit khi parent chết. Done khi: Nexus chạy trong con, GUI + audio OK, kill con → chỉ con chết. | 1-2 ngày |
|
||||
| G4.2 | Bridge → client pool | `main.cpp` (bridge) | bridge spawn 1 con/plugin, forward MIDI, đọc audio mix, watchdog (crash → respawn, mute/restore như G3.3). | 2-3 tuần |
|
||||
| G4.3 | IPC audio realtime | SHM ring buffer | double-buffer SHM con, latency <20ms, 6 Nexus song song không xrun. | 2-3 tuần |
|
||||
|
||||
### Phụ thuộc
|
||||
`G4.2 → G4.1` (cần plugin_host chạy được độc lập) · `G4.3 → G4.2`.
|
||||
|
||||
### Thứ tự triển khai khuyến nghị
|
||||
G4.1 → G4.2 → G4.3.
|
||||
|
||||
---
|
||||
|
||||
## 3. KẾ HOẠCH THỰC HIỆN (G4.1 PoC — hoàn tất hôm nay)
|
||||
|
||||
### G4.1a — Đọc code, chốt API host
|
||||
- `Vst3Instrument`: `init(sr, block)` set `sampleRate_/maxBlockSize_` (gọi trước `loadPlugin` — loadPlugin dùng `maxBlockSize_` trong `setupProcessing`; default 256 = AUDIO_BLOCK_SIZE). `loadPlugin` tự activate + setProcessing + prepare. `processAudioBlock` zero output rồi process, copy stereo ra ngoài, clear eventList.
|
||||
- Drain MIDI theo convention bridge (main.cpp): snapshot `midiQueue[0..count-1]` (bounded 64), reset `midiQueueCount=0`, dispatch theo command 0x9/0x8/0xB/0xC/0xE.
|
||||
- Heartbeat: thread riêng `Sleep(100)` → `heartbeat++` (pattern G3.1).
|
||||
|
||||
### G4.1b — Viết `plugin_host_main.cpp`
|
||||
- Parse argv `--shm/--path/--sr/--block/--parent`.
|
||||
- `CoInitializeEx(STA)` trên main thread (GUI thuộc main thread con).
|
||||
- `shm_create(name)` — PoC: con sở hữu mapping (G4.2 đổi thành mở).
|
||||
- `init(sr,block)` → `loadPlugin(path,sr)` → tạo own top-level window → `openGUI(hwnd)` (editorhost pattern) → heartbeat thread → main loop: pump messages + drain MIDI + `processAudioBlock` + `bridgeWriteIndex++` + `parent_alive` check → `closeGUI()` + `CoUninitialize()` + `shm_close`.
|
||||
- ASCII-only, LF → normalize CRLF (repo convention).
|
||||
|
||||
### G4.1c — CMake target `plugin_host` + build
|
||||
- Copy pattern `gui_probe`: `src/plugin_host_main.cpp + src/SharedMemoryIPC.cpp + src/Vst3Instrument.cpp` + SDK hosting libs (không cần NativeInstrumentEngine.cpp).
|
||||
- Bổ sung prototype `shm_open/shm_create/shm_ptr/shm_close` vào `SharedMemoryIPC.h` (trước đây chỉ có trong .cpp, plugin_host cần khai báo).
|
||||
- Build: `cmake --build build --config Release --target plugin_host -- /m` PASS → `build/Release/plugin_host.exe`.
|
||||
|
||||
### G4.1d — Probe với Nexus (probe_g41.py)
|
||||
- Spawn con với `C:\Program Files\Common Files\VST3\Nexus.vst3`, SHM `SonicForge_PluginHost_Probe`.
|
||||
- Verify: heartbeat tăng (hb=1 trong ~1s); GUI window xuất hiện (4 hwnd thuộc pid con, gồm PluginHostClass + editor Nexus); ghi note-on C4 → audio peak 0.38274 (nonzero); terminate con → app 15928 + bridge 20996 vẫn sống (isolation).
|
||||
|
||||
### G4.1e — Commit + journal + push
|
||||
|
||||
---
|
||||
|
||||
## 4. RỦI RO
|
||||
| Rủi ro | Ảnh hưởng | Giảm thiểu |
|
||||
|---|---|---|
|
||||
| Con chết khi đang render → dữ liệu audio mất 1 block | Click/pop | G4.3 ring buffer + G4.2 mute/restore; PoC chấp nhận. |
|
||||
| Parent chết → con orphan | Process treo | `--parent` + check mỗi block (đã làm). |
|
||||
| Plugin GUI yêu cầu thread/COM riêng | Hang/blank view | Attach trên main thread con (editorhost pattern đã verify qua gui_probe). |
|
||||
| SHM con đổi layout sau này | Mất tương thích | Tái dùng `SharedAudioBufferIPC` — layout ổn định append-only. |
|
||||
|
||||
---
|
||||
|
||||
## 5. BÁO CÁO KẾT QUẢ (đã thực hiện)
|
||||
| ID | Cài đặt | Kiểm tra | Bugs bắt được | Fix | Trạng thái |
|
||||
|---|---|---|---|---|---|
|
||||
| G4.1a | Chốt API host từ Vst3Instrument + convention drain MIDI/heartbeat (main.cpp). | — | — | — | ✅ |
|
||||
| G4.1b | `plugin_host_main.cpp`: argv parse, COM STA, shm_create, loadPlugin, openGUI own window, audio loop + heartbeat + parent check. | Build PASS (sau fix prototype SHM). | `ShmHandle/shm_create/shm_ptr/shm_close` không có prototype trong header → C2065. | Thêm prototype vào `SharedMemoryIPC.h`. | ✅ |
|
||||
| G4.1c | CMake target `plugin_host` (pattern gui_probe, không engine). | Build PASS → `build/Release/plugin_host.exe`. | Patch block bị chèn vào giữa `if(WIN32)` của gui_probe + `endif()` thừa. | Script sửa vị trí + xóa endif. | ✅ |
|
||||
| G4.1d | Probe: spawn + Nexus + heartbeat + GUI + audio + isolation. | PASS: hb=1 (1s); 4 hwnd; peak 0.38274; kill con → app 15928 + bridge 20996 sống. | Probe python đọc SHM AV — `MapViewOfFile` restype mặc định c_int cắt con trỏ 64-bit. | Set `restype=c_void_p`. | ✅ |
|
||||
| G4.1e | Commit + journal + push. | — | — | — | ✅ |
|
||||
|
||||
### Nhật ký thực hiện
|
||||
|
||||
### G4.1 PoC — Hoàn tất (2026-08-15)
|
||||
- `native_bridge/src/plugin_host_main.cpp` (mới): process con sandbox — argv `--shm/--path/--sr/--block/--parent`; COM STA; `shm_create` (PoC con sở hữu mapping, G4.2 đổi thành `shm_open`); `init` → `loadPlugin` → own window + `openGUI` (editorhost pattern) → heartbeat thread `Sleep(100)` (G3.1 pattern) → main loop pump + drain MIDI (snapshot 64, reset count, dispatch 0x9/0x8/0xB/0xC/0xE) + `processAudioBlock(masterLeft, masterRight, block)` + `bridgeWriteIndex++` + `parent_alive` check mỗi block (chống orphan) → `closeGUI` + `CoUninitialize` + `shm_close`.
|
||||
- `include/SharedMemoryIPC.h`: bổ sung prototype helpers `shm_open/shm_create/shm_ptr/shm_close` (chỉ khai báo, không đổi layout — struct giữ nguyên 11168B).
|
||||
- `CMakeLists.txt`: target `plugin_host` (src/plugin_host_main.cpp + SharedMemoryIPC.cpp + Vst3Instrument.cpp + SDK hosting libs). Build Release PASS.
|
||||
- Verify (probe_g41.py): Nexus (`C:\Program Files\Common Files\VST3\Nexus.vst3`) load trong con OK; heartbeat tăng hb=1 sau ~1s; GUI window 4 hwnd thuộc pid con; note-on C4 (vel 100) → peak audio 0.38274 (nonzero); terminate con → `sonicforge-daw.exe` 15928 + `daw_vst_bridge.exe` 20996 vẫn sống — isolation đúng yêu cầu G4.1.
|
||||
- Lưu ý test-env: probe python phải set `OpenFileMappingA.restype = MapViewOfFile.restype = c_void_p` (Win64 con trỏ 64-bit, mặc định c_int cắt → AV khi đọc view).
|
||||
- G4.2 (bridge → pool) chưa bắt đầu — bước tiếp theo sau PoC PASS.
|
||||
|
||||
## G4.2 — Bridge → client process pool (sandbox per channel) ✅
|
||||
- `include/SandboxVst3Host.h` + `src/SandboxVst3Host.cpp` (mới): `INativeInstrument` wrapper quanh plugin_host.exe. `loadPlugin(path, sr, channel)` → `shm_create("SonicForge_PluginHost_<bridgePid>_<channel>")` + `CreateProcessA` plugin_host.exe (cạnh daw_vst_bridge.exe, cùng install/) với `--open --shm ... --path ... --sr ... --block ... --parent <bridgePid>`; chờ child heartbeat ≤60s. MIDI noteOn/noteOff/controlChange/programChange/pitchBend → `shm_write_midi` (velocity float→0..127). `processAudioBlock` copy snapshot masterLeft/Right (G4.3 thay bằng ring buffer). `watchdogLoop` (thread riêng): Sleep 500ms, child chết → `alive_=false` + log + respawn sau 1s; 3 fail liên tiếp → mute vĩnh viễn đến khi reload. Destructor: stop_ + join + TerminateProcess + shm_close.
|
||||
- `src/plugin_host_main.cpp`: thêm flag `--open` → `shm_open` thay vì `shm_create` (bridge tạo mapping, con mở).
|
||||
- `include/SharedMemoryIPC.h`: thêm prototype `shm_write_midi` (default args data2/data3 chỉ ở header); bỏ default args trùng trong `SharedMemoryIPC.cpp` (C2572).
|
||||
- `src/NativeInstrumentEngine.cpp`: `create_instrument` VST3 case — `SF_SANDBOX_VST3` env set → `SandboxVst3Host`, ngược lại `Vst3Instrument` (regression an toàn); `assign` dùng dynamic_cast: Vst3 → loadPlugin(path,sr)+setChannel, Sandbox → loadPlugin(path,sr,channel). assign() chạy trên uiWorker (ChannelWorker) — spawn blocking OK.
|
||||
- `CMakeLists.txt`: thêm `src/SandboxVst3Host.cpp` vào target `daw_vst_bridge`.
|
||||
- Build Release PASS (`daw_vst_bridge.exe` + `plugin_host.exe`).
|
||||
- Deploy: kill app+bridge → copy exe mới vào install/ (dùng shutil, `copy` shell fail im lặng — exe cũ 3900416B không có sandbox, phải verify chuỗi `SF_SANDBOX_VST3` trong binary) → start lại app.
|
||||
- Verify (probe_g42.py, bridge standalone env `SF_SANDBOX_VST3=1` + state file rỗng): LOAD Nexus ch0 → plugin_host pid spawn; child heartbeat OK; GUI 4 hwnd thuộc pid con; note-on C4 → peak 0.38276; **kill child** → bridge sống, heartbeat tiếp tục, respawn pid mới sau ~1.5s, GUI mới 4 hwnd, audio lại 0.38275 — PASS.
|
||||
- Lưu ý probe: bridge chỉ OPEN mapping `SonicForge_DAW_IPC` (Rust shell tạo) → probe phải `CreateFileMappingA` trước; state file cũ (7 instruments) làm guard G2.4 skip LOAD → dùng `SF_STATE_FILE` trỏ file rỗng.
|
||||
- Regression: G1.5 stress 3 iter PASS (bridge alive, writeIndex delta 286); G2.5 stress 2 iter PASS (kill bridge → watchdog respawn → restore preset exact + LOAD-skip OK) — không env SF_SANDBOX_VST3 → in-process như cũ, không đụng.
|
||||
- Commit: `G4.2: bridge-side plugin process pool — SandboxVst3Host spawns/respawns plugin_host.exe per channel, MIDI forward + audio snapshot + watchdog mute/respawn, crash isolation verified`.
|
||||
- Commit: `G4.1: sandbox plugin host (plugin_host.exe) PoC — Nexus runs in child process, GUI + audio via SHM, crash isolation verified`.
|
||||
|
||||
## G4.3 - Double-buffer sandbox SHM, latency <20ms, multi-instance no-xrun
|
||||
|
||||
- `include/SharedMemoryIPC.h`: struct `SandboxHostIPC` = base `SharedAudioBufferIPC` (11168B giu nguyen) + `writeSlot` @11168 + `audioLeft[2][256]` @11172 + `audioRight[2][256]` @13220; `shm_open/shm_create` nhan size param (mac dinh `sizeof(SharedAudioBufferIPC)`), them `shm_sandbox_ptr`. Header rewrite toan bo CRLF (patch anchor sai vi tri lan dau lam hong header - da fix bang rewrite_g43_header.py, build PASS sau do).
|
||||
- `src/plugin_host_main.cpp`: `shm_sandbox_ptr`; render vao slot `1 - (writeSlot&1)` roi publish `writeSlot = slot` + `bridgeWriteIndex++` (double buffer: child cham khong tear, bridge doc block cu nhat da publish). `timeBeginPeriod(1)` de `Sleep(1)` that su ~1ms (khong phai 15.6ms tick mac dinh).
|
||||
- `include/SandboxVst3Host.h` + `src/SandboxVst3Host.cpp`: `ipc_` kieu `SandboxHostIPC*`; `shm_create` size `sizeof(SandboxHostIPC)`; `processAudioBlock` doc `audioLeft[writeSlot&1]`. Watchdog log kem exit code (GetExitCodeProcess) de debug.
|
||||
- Fix quan trong: plugin_host phai `shm_open(shmName, sizeof(SandboxHostIPC))` - mac dinh 11168B chi map duoc base, doc audioLeft vuot view -> AV crash sau LOAD (children chet lien tuc). Bug thu 2: probe `kids = [x for x in pl if x not in kids]` thay the kids bang phan tu moi - mat phan tu cu, bao FAIL "expected 2 children" du ca 2 con song. Fix union `dict.fromkeys(kids+pl)`.
|
||||
- Build Release PASS (daw_vst_bridge.exe + plugin_host.exe). Deploy shutil.copy2 (kill proc giu file truoc).
|
||||
- Verify (probe_g43.py, bridge standalone env `SF_SANDBOX_VST3=1` + state file rong): LOAD Nexus ch0+ch1 -> 2 plugin_host [con song song]; heartbeat ca 2 OK; note-on ch0 C4 + ch1 E4 -> main audio peak 0.71835 (tong 2 channel); writeSlot flip 64 blocks/s = latency 15.6ms < 20ms (khong yeu cau 172/s thuc - Nexus render ~10ms/block that, CPU-bound); kill child0 -> child1 van phat 0.36972, bridge song + heartbeat + child0 respawn - PASS.
|
||||
- Regression: G1.5 stress 3 iter PASS (writeIndex delta 286); G2.5 stress 2 iter PASS (kill bridge -> respawn -> preset exact + LOAD-skip OK). App deploy (install/sonicforge-daw.exe) dang chay, bridge pid moi.
|
||||
- FIX idle-skip (G4.3.1): plugin_host_main.cpp them `activeNotes` dem (note-on ++, note-off --, clamp >= 0)
|
||||
+ `idle` flag. Khi khong con note dang giu va peak block < 0.0005f -> idle = true,
|
||||
loop Sleep(15) bo qua render (van PeekMessage + parent-alive check). Bo `Sleep(1)`
|
||||
hot loop -> `Sleep(0)` yield. KET QUA: writeSlot flip 0 blocks/s khi im lang (truoc day
|
||||
render lien tuc ~100/s, CPU cao); khi dang giu note flip 101.6 blocks/s = latency 9.8ms
|
||||
(truoc 64/s = 15.6ms) - ca 2 loi G4.3 xac nhan da het.
|
||||
- Verify lai: probe_g43.py PASS (2 children song song, peak 0.67273, kill child0 -> child1
|
||||
0.36972, isolation OK); dbg_rate.py idle 0 flips/3s; G1.5 3 iter PASS (delta 287);
|
||||
G2.5 2 iter PASS. Deploy install/plugin_host.exe moi. App dang chay pid 21396.
|
||||
@@ -0,0 +1,305 @@
|
||||
# 20260815 — KẾ HOẠCH THỰC HIỆN G2 (PERSISTENCE RESTART)
|
||||
|
||||
Nguồn: `big_tasks.md` — header **G2 — Persistence restart (2-3 tuần)**.
|
||||
Trạng thái roadmap: G0 ✅, G1 ✅ (G1.1–G1.5), **G2 ✅ (G2.0–G2.6)**, G3 ✅ (G3.1–G3.3), G4 ✅ (G4.1–G4.3 sandbox PoC). Branch: `standalone-shm-bridge`. File này là plan G2; G3/G4 thực hiện tiếp nối (nhật ký ở mục 5).
|
||||
|
||||
---
|
||||
|
||||
## 1. PHÂN TÍCH G2
|
||||
|
||||
### 1.1 Mục tiêu
|
||||
Restart không mất instrument — sau khi bridge respawn (stall 3s / crash / restart sample-rate / DAW mở lại),
|
||||
người dùng bấm PLAY nghe lại ngay, preset + vị trí transport được khôi phục tự động.
|
||||
|
||||
### 1.2 Hiện trạng (đã verify trong code)
|
||||
| Thành phần | File | Hiện trạng liên quan G2 |
|
||||
|---|---|---|
|
||||
| Bridge C++ (daw_vst_bridge) | `native_bridge/src/main.cpp` (1250 dòng) | Control loop: LOAD type=2, TRANSPORT type=3 (0=STOP, 1=PLAY, 2=SET_POSITION), OPEN_GUI type=4. `playheadSamples` lưu trong audio loop. Watchdog thread (parent chết → TerminateProcess). |
|
||||
| Quản lý instrument | `native_bridge/src/NativeInstrumentEngine.cpp/.h` | `InstrumentEngineManager`: map channel→instance + `paths_` (path/channel). `FluidSynthInstrument::bank_[16]` giữ bank mỗi channel. |
|
||||
| VST3 preset | `native_bridge/src/Vst3Instrument.cpp` (G0.4) | `captureState()`/`restoreState()` CHỈ trong RAM (`savedComponentState_`/`savedControllerState_` buffers) — mất khi process chết. |
|
||||
| Rust shell | `src-tauri/src/lib.rs` | `spawn_bridge`, pump thread (stall 3s → kill + restart 1 lần → `bridge-down`), `kill_bridge`, `restart_bridge_with_sample_rate`, `transport_control`, `load_native_instrument`, `bridge_status`. `SHUTTING_DOWN` khi window đóng → kill bridge KHÔNG dump. |
|
||||
| JS client | `app/static/js/services/nativeBridgeService.js` | `loadInstrument`, `transport(kind, playhead)`, `onAudio`, `onStatusChange`, `queryStatus`. |
|
||||
| JS app | `app/static/js/app.precompiled.js` | `loadVstToBridge`/`loadToBridge` (LOAD), `__bridgeLoadedChannels` (reset `{}` khi restart → mất), `updatePlayhead` gửi `set_position` ~1/s (JS là nguồn playhead duy nhất), bootstrap gọi `restart_bridge_with_sample_rate` ở startup. |
|
||||
|
||||
### 1.3 Khoảng trống (gap)
|
||||
1. **Không có gì ghi ra đĩa** — G0.4 preset chỉ sống trong RAM của bridge; respawn = mất hết channel/preset/transport.
|
||||
2. **Kill không có cơ hội dump** — pump thấy stall → `kill_bridge` ngay; window đóng → `kill_process_tree`; cả hai đều không báo bridge ghi state trước.
|
||||
3. **Respawn không tự load** — bridge mới start với SHM trống; JS `__bridgeLoadedChannels={}` và chỉ load lại khi track phát/đổi (không tự động, không khôi phục preset).
|
||||
4. **Guard chặn restore khi PLAY** — G2.4: luồng restore phải chạy được khi `isPlayingRef.current === true` (hiện guard `if(window.__bridgePlaying) return` trong `openVstGuiRetry`, và `!isPlayingRef.current` quanh `openNativeGUI` dòng ~817 — áp dụng cho restore cũng phải gỡ).
|
||||
|
||||
### 1.4 Nguyên tắc thiết kế (đề xuất)
|
||||
- **Write-on-change là chính**: bridge tự ghi state ra đĩa NGAY khi state đổi (LOAD/TRANSPORT/params) — an toàn cả khi crash đột ngột, không phụ thuộc cơ hội dump trước kill. Dump-before-kill chỉ là lớp phụ (khi có thời gian: restart sample-rate, window close).
|
||||
- **Bridge tự restore khi start**: bridge đọc file state khi khởi động → tự push LOAD các channel + restore preset + set transport — Rust không cần biết preset blob (giữ IPC đơn giản).
|
||||
- **File state dùng chung**: `%APPDATA%/SonicForgeDAW/state/bridge_state.json` (cùng cây logs/ipc — đã có sẵn layout).
|
||||
- **Không đụng audio loop**: mọi IO đĩa chạy trên UiThread (worker job queue) hoặc thread riêng; tuyệt đối không trong vòng render.
|
||||
|
||||
---
|
||||
|
||||
## 2. PHÂN RÃ TASKS + ESTIMATE
|
||||
|
||||
| ID | Task | File đích | Nội dung | Estimate |
|
||||
|---|---|---|---|---|
|
||||
| G2.0 | Format + module state file | `native_bridge/src/StateStore.h/.cpp` (mới) + `CMakeLists.txt` | JSON (path/type/channel/preset-base64/transport) + atomic write (write tmp → rename). API: `load()`, `save(StateSnapshot)`, `clear()`. | 1 ngày |
|
||||
| G2.1 | Save đầy đủ state ra đĩa | `main.cpp`, `Vst3Instrument.cpp`, `NativeInstrumentEngine.cpp/.h` | Snapshot toàn bộ channel: path, type, VST3 component+controller state (từ `captureState` → base64), SF2 bank/program, transport `playheadSamples`. Ghi khi: LOAD xong, TRANSPORT đổi, params đổi (định kỳ 1-2s nếu dirty). | 2 ngày |
|
||||
| G2.2 | Dump state trước kill | `src-tauri/src/lib.rs`, `main.cpp` | (a) Write-on-change phủ hầu hết. (b) Trước `kill_bridge`/restart: Rust push control type=5 (DUMP) + chờ ≤300ms → kill. (c) Window close: gửi DUMP trước `kill_process_tree`. | 1.5 ngày |
|
||||
| G2.3 | Auto-reload sau respawn | `main.cpp` (startup), `lib.rs` (env `SF_STATE_FILE`) | Bridge start: đọc state file (nếu có) → `uiWorker.post` LOAD từng channel theo thứ tự → restore preset → nếu từng PLAY thì set transport + PLAY. Guard: chỉ restore khi channel trống; log vào bridge.log. | 2 ngày |
|
||||
| G2.4 | Gỡ guard chặn restore khi PLAY | `app/static/js/app.precompiled.js` | Trong luồng restore, bỏ điều kiện dừng vì `isPlayingRef.current`/`__bridgePlaying`: LOAD instrument + restore preset khi đang PLAY vẫn phải chạy (khác với open GUI — G0.2 vẫn giữ defer cho OPEN_GUI). | 0.5 ngày |
|
||||
| G2.5 | Regression driver stress | `native_bridge/debug/test_g2_stress.py` (mới, theo mẫu `test_g15_stress.py`) | N vòng: LOAD 2 inst (Nexus+EZkeys) → PLAY → kill bridge → respawn → verify writeIndex advancing + preset giữ + 0 crash. | 1 ngày |
|
||||
| G2.6 | Verify + bắt bugs + fix | — | Chạy selfcheck/stress/probe trên Windows; thu logs; sửa bug tìm được. | 2-4 ngày |
|
||||
| | **Tổng** | | | **10-12 ngày (~2-3 tuần — khớp roadmap)** |
|
||||
|
||||
### Phụ thuộc
|
||||
`G2.1 → G2.0` · `G2.2 → G2.1` (dùng StateStore) · `G2.3 → G2.1 + G2.2` · `G2.5 → G2.3 + G2.4` · `G2.6 → tất cả`.
|
||||
|
||||
### Thứ tự triển khai khuyến nghị
|
||||
G2.0 → G2.1 → G2.3 (lõi: save + tự restore) → G2.4 (guard) → G2.2 (dump trước kill — lớp phụ) → G2.5 → G2.6.
|
||||
Lý do: G2.2 phụ thuộc G2.1 nhưng giá trị thấp hơn G2.3; làm G2.3 sớm để stress sớm.
|
||||
|
||||
---
|
||||
|
||||
## 3. KẾ HOẠCH THỰC HIỆN (CÀI ĐẶT → KIỂM TRA → BẮT BUGS → FIX → REPORT)
|
||||
|
||||
> **PHÂN CÔNG MÁY (bắt buộc):**
|
||||
> - **Máy Linux (máy này):** CHỈ lập kế hoạch, review code, ghi chú, theo dõi tiến độ, cập nhật file báo cáo.
|
||||
> KHÔNG sửa code C++/Rust/JS, không build bridge (thiếu vcpkg/fluidsynth/sfizz/VST3 SDK/rustc-MSVC).
|
||||
> Verify tối đa tại đây: `node --check` (JS), `ast.parse` (py), review, `shm_selfcheck` (đã pass Linux trước đây).
|
||||
> - **Máy Windows (máy chạy app thật):** SỬA CODE, build (`build_bridge.ps1`), deploy (`deploy_g12.ps1`),
|
||||
> chạy probe/stress, bắt bugs, fix, commit + push. Mọi thay đổi code G2 đều phát sinh ở đây.
|
||||
> - Luồng: **Windows sửa code + commit + push** → **Linux pull, review, cập nhật ghi chú/report + push** →
|
||||
> Windows pull lại, verify tiếp.
|
||||
>
|
||||
> Sau MỖI task: mục "📌 Lưu ý & quy trình" ghi rõ việc nào làm ở máy nào.
|
||||
|
||||
### G2.0 — StateStore
|
||||
- **Cài đặt**: thêm `StateStore.h/.cpp`; JSON đơn giản (viết tay serializer — tránh thêm dep nlohmann vào bridge, dự án không có sẵn). Atomic write: `bridge_state.json.tmp` → `rename`.
|
||||
- **Kiểm tra**: unit selfcheck — compile `StateStore` riêng (Linux: `g++ -fsyntax-only` nếu muốn xem syntax; Windows: compile thật trong target + test nhỏ ghi/đọc JSON round-trip).
|
||||
- **Bắt bugs**: escape chuỗi path (dấu `"` `\` trong path Windows), rename fail khi file khoá, UTF-8.
|
||||
- **Fix + report**: ghi kết quả vào mục 5.
|
||||
- **📌 Lưu ý & quy trình**:
|
||||
- **Windows** (sửa code): tạo `native_bridge/src/StateStore.h/.cpp`; thêm 2 file vào `native_bridge/CMakeLists.txt` target `daw_vst_bridge`; nhớ CRLF (ghi file qua Python `io.open(newline='')` hoặc editor giữ CRLF — cả repo native_bridge là CRLF). Build: `build_bridge.ps1`. Test: chạy exe/test nhỏ in JSON ra, verify round-trip. Commit: `G2.0: StateStore (JSON state file + atomic write)`.
|
||||
- **Linux** (ghi chú): sau khi Windows push, pull + review file mới + cập nhật bảng mục 5 + dòng nhật ký. Không sửa code.
|
||||
|
||||
### G2.1 — Save đầy đủ
|
||||
- **Cài đặt**: thêm `InstrumentEngineManager::snapshot()` (dưới `mu_`) trả về danh sách channel → path/type/state-blob; `Vst3Instrument::serializeState()` tái dùng `captureState()` buffers → base64; `FluidSynthInstrument` expose bank/program; hook ghi vào các điểm LOAD xong / TRANSPORT / timer dirty.
|
||||
- **Kiểm tra**: `shm_selfcheck` không đổi (layout SHM giữ nguyên — không thêm field vào SHM); review không đụng audio loop; trên Windows: probe LOAD → đọc `bridge_state.json` thấy đủ path/preset.
|
||||
- **Bắt bugs**: base64 không khớp byte (size/precision), ghi giữa lúc instance đang reload (phải chờ `reloadingCh_` off), path empty.
|
||||
- **Fix + report**: mục 5.
|
||||
- **📌 Lưu ý & quy trình**:
|
||||
- **Windows** (sửa code): sửa `NativeInstrumentEngine.cpp/.h` (thêm `snapshot()`), `Vst3Instrument.cpp` (serializeState), `main.cpp` (hook ghi sau LOAD/TRANSPORT + timer dirty). Build + deploy + probe LOAD → mở `%APPDATA%\SonicForgeDAW\state\bridge_state.json` kiểm tra path/preset. Commit: `G2.1: save full instrument state to disk (path/type/preset/transport)`.
|
||||
- **Linux** (ghi chú): pull + review: snapshot phải nằm dưới `mu_`, không gọi IO trong audio loop; `shm_selfcheck` vẫn pass. Cập nhật mục 5 + nhật ký.
|
||||
|
||||
### G2.2 — Dump trước kill
|
||||
- **Cài đặt**: thêm control type=5 (DUMP_STATE) trong enum + handler main.cpp (push snapshot qua StateStore); Rust `kill_bridge`/window-close gửi control 5 + sleep 300ms trước kill; `restart_bridge_with_sample_rate` làm tương tự.
|
||||
- **Kiểm tra**: review Rust+C++ control type khớp; Linux `node --check` không liên quan; Windows: kill bridge giữa PLAY → file state phải đủ (write-on-change đã đảm bảo, DUMP là phụ).
|
||||
- **Bắt bugs**: race Rust gửi DUMP sau khi bridge chết (guard shm unavailable), 300ms không đủ khi VST đang createView (attach_in_flight → chờ theo flag SHM offset 11160).
|
||||
- **Fix + report**: mục 5.
|
||||
- **📌 Lưu ý & quy trình**:
|
||||
- **Windows** (sửa code): sửa `native_bridge/src/main.cpp` (case 5), `src-tauri/src/lib.rs` (`kill_bridge`, `restart_bridge_with_sample_rate`, window-close handler). Build bridge + `cargo check` (src-tauri). Deploy → kill bridge giữa PLAY → kiểm tra state file đủ. Commit: `G2.2: dump state before kill (control type=5 + Rust DUMP before kill_bridge)`.
|
||||
- **Linux** (ghi chú): pull + review: type 5 không đụng enum cũ (2/3/4 giữ nguyên), Rust guard khi `ShmState` None; `cargo check` bắt buộc chạy trên Windows (Linux thiếu toolchain). Cập nhật mục 5 + nhật ký.
|
||||
|
||||
### G2.3 — Auto-reload sau respawn
|
||||
- **Cài đặt**: bridge startup (sau khi map SHM, trước audio loop): đọc `SF_STATE_FILE` (Rust set env khi spawn) → `uiWorker.post` từng channel: LOAD (type 2) → `restoreState()` → cuối cùng nếu state có `playing=true` → TRANSPORT play + set_position. Log từng bước vào bridge.log.
|
||||
- **Kiểm tra**: Linux review + `shm_selfcheck`; Windows: kill bridge khi đang PLAY → chờ respawn → verify writeIndex advancing + preset nghe lại đúng.
|
||||
- **Bắt bugs**: restore chạy trước khi Rust pump gắn `bridge-audio` (JS `_reconnectIfNeeded` vẫn cần); load nhiều channel cùng path (g_loadMutex + reloadingCh phải serialize); double-load khi JS cũng tự load (JS phải bỏ `__bridgeLoadedChannels` reset → giữ state thật).
|
||||
- **Fix + report**: mục 5.
|
||||
- **📌 Lưu ý & quy trình**:
|
||||
- **Windows** (sửa code): sửa `main.cpp` (startup restore block), `lib.rs` (set env `SF_STATE_FILE` khi spawn). Build + deploy + kill bridge giữa PLAY → chờ respawn (spawn.log) → verify writeIndex + preset. Nếu cần phía JS: sửa `app.precompiled.js` + `node --check`. Commit: `G2.3: auto-reload instruments from state file after bridge respawn`.
|
||||
- **Linux** (ghi chú): pull + review: restore phải post qua `uiWorker` (không chạy trong audio loop), serialize bằng `g_loadMutex` sẵn có; xác nhận `__bridgeLoadedChannels` flow trên JS. Cập nhật mục 5 + nhật ký.
|
||||
|
||||
### G2.4 — Gỡ guard chặn restore khi PLAY
|
||||
- **Cài đặt**: trong `app.precompiled.js` luồng auto-restore (điểm G2.3 phía JS): cho phép `loadVstToBridge`/restore chạy khi PLAY; GIỮ NGUYÊN guard `__bridgePlaying` cho riêng OPEN_GUI (G0.2 — defer GUI vẫn đúng).
|
||||
- **Kiểm tra**: `node --check app.precompiled.js`; Windows: PLAY đang chạy → kill/respawn → verify restore xong trong khi PLAY, không mất tiếng.
|
||||
- **Bắt bugs**: restore gọi `openVstGuiRetry` nhầm khi PLAY (guard cũ trả `true` sớm → GUI không mở nhưng LOAD cũng bị bỏ) — tách 2 guard riêng.
|
||||
- **Fix + report**: mục 5.
|
||||
- **📌 Lưu ý & quy trình**:
|
||||
- **Windows** (sửa code): sửa `app/static/js/app.precompiled.js` (tách guard restore vs guard OPEN_GUI); chạy `node --check app/static/js/app.precompiled.js`. Deploy app (build JS bundle → copy vào install) → PLAY → kill/respawn → verify. Commit: `G2.4: allow instrument restore while PLAY (keep OPEN_GUI defer)`.
|
||||
- **Linux** (ghi chú): pull + review: xác nhận G0.2 guard `__bridgePlaying` vẫn còn cho OPEN_GUI; `node --check` cũng chạy được tại đây nếu có node. Cập nhật mục 5 + nhật ký.
|
||||
|
||||
### G2.5 — Stress driver
|
||||
- **Cài đặt**: `test_g2_stress.py` theo mẫu `test_g15_stress.py`: 5 + 10 iterations LOAD/OPEN_GUI/PLAY/STOP + kill bridge giữa chừng → verify respawn + writeIndex + preset.
|
||||
- **Kiểm tra**: chạy trên Windows; ghi PASS/FAIL theo iteration.
|
||||
- **Bắt bugs**: crash/restart khi kill giữa attach, preset sai sau lần restore thứ N.
|
||||
- **Fix + report**: mục 5.
|
||||
- **📌 Lưu ý & quy trình**:
|
||||
- **Windows** (tạo script + chạy): tạo `native_bridge/debug/test_g2_stress.py` (chép khung `test_g15_stress.py`: ctypes SHM, `write_control`, `wait_log`, `bridge_pid`, `bad_since`); thêm bước kill `daw_vst_bridge` giữa vòng và chờ respawn. Chạy: `python native_bridge\debug\test_g2_stress.py <APP_PID> 5` rồi `10`. Thu kết quả PASS/FAIL + logs. Commit: `G2.5: stress driver for persistence restart (N loop + kill/respawn)`.
|
||||
- **Linux** (ghi chú): pull + review script (không chạy được — cần SHM Windows); cập nhật mục 5 với kết quả PASS/FAIL do Windows báo.
|
||||
|
||||
### G2.6 — Verify tổng + bắt bugs + fix
|
||||
- Chạy lại toàn bộ regression G1.5 (không hỏng G1) + G2.5 + probe G0.4.
|
||||
- Thu `bridge.log`/`spawn.log` thực (không tin probe PASS đơn lẻ — learning cũ).
|
||||
- Sửa mọi bug tìm được, commit theo từng task (như G1.2–G1.5).
|
||||
- **📌 Lưu ý & quy trình**:
|
||||
- **Windows** (chạy + fix): chạy `test_g15_stress.py` (regression G1) + `test_g2_stress.py` (G2) + probe G0.4; fix bug tìm được, commit từng cái.
|
||||
- **Linux** (báo cáo tổng): sau khi Windows báo kết quả cuối, tổng hợp toàn bộ vào mục 5 + nhật ký, đánh dấu hoàn thành, commit báo cáo cuối: `G2: report — persistence restart done (verdict từ Windows)`. Push.
|
||||
|
||||
---
|
||||
|
||||
## 4. RỦI RO
|
||||
| Rủi ro | Ảnh hưởng | Giảm thiểu |
|
||||
|---|---|---|
|
||||
| VST3 state blob không restore được (Nexus setState fail) | Mất preset dù load lại được path | Fallback: giữ path + chỉ restore blob khi OK; log `setState failed`; không chặn load. |
|
||||
| Ghi file mỗi LOAD → IO chậm | Stall audio? | Ghi trên UiThread job (không trong audio loop); atomic rename. |
|
||||
| JS `__bridgeLoadedChannels` stale | Double-load / bỏ load | Reset map theo sự kiện bridge respawn thật (bridge-audio trở lại) thay vì chỉ restart-bootstrap. |
|
||||
| Restore khi đang PLAY (G2.4) gây click/glitch | Chất lượng âm | LOAD đã có reloadingCh_ mute cơ chế sẵn (G1.4) — tái dùng; restore nhanh, không chờ. |
|
||||
| Không build được trên Linux | Không verify cục bộ | Verify Linux: syntax/selfcheck/review; chạy thật trên Windows theo quy trình G1. |
|
||||
|
||||
---
|
||||
|
||||
## 5. BÁO CÁO KẾT QUẢ (điền sau khi thực hiện)
|
||||
> Cột Cài đặt/Kiểm tra/Bugs/Fix: **Windows** điền (người sửa code). Cột Trạng thái + Nhật ký: **Linux** cập nhật sau khi review.
|
||||
| ID | Cài đặt | Kiểm tra | Bugs bắt được | Fix | Trạng thái |
|
||||
|---|---|---|---|---|---|
|
||||
| G2.0 | StateStore.h/.cpp (JSON thủ công, atomic tmp+rename, UTF-8, API save/load/clear, defaultPath=%APPDATA%/SonicForgeDAW/state/bridge_state.json); them vao CMakeLists target daw_vst_bridge; test_state_store.cpp (round-trip self-check) | Build PASS (daw_vst_bridge.exe), round-trip PASS (save/load 2 inst: path Windows co space+backslash, presetBase64 co newline/quote/backslash; khong con .tmp; clear xoa file) | — (module moi, chua hook vao bridge) | — | ✅ (Win xong + Linux review OK) |
|
||||
| G2.1 | main.cpp: globals g_stateDirty/g_transportPlaying/g_lastSnapMutex/g_lastSnap, snapshotsEqual (bo playhead); stateFile = SF_STATE_FILE fallback defaultPath; playheadSamples -> std::atomic<uint64_t>; saveStateJob (UiThread, skip khi isReloading, chi save khi snapshot doi); TRANSPORT set g_transportPlaying + dirty; cuoi LOAD dirty=true; periodic 2s dirty + debounce 300ms. Vst3Instrument.h: khai bao bankOf/programOf. StateStore.cpp: ensureParentDir() (CreateDirectoryA chain / mkdir) goi dau save(). | Build PASS (daw_vst_bridge.exe). Deploy PASS (PID moi: sonicforge-daw 9876, daw_engine 24632, daw_vst_bridge 7352). Verify live PASS: LOAD Nexus ch5 + PLAY -> bridge_state.json co 7 instrument (ch0-6 frontend da load), playing=true, presetBase64 non-empty (Nexus 3468/3488, EZkeys 24764); STOP -> playing=false. State round-trip test_state_store.cpp PASS. | test script ban dau ky vong dung 1 instrument (frontend da load 7) -> FAIL gia, chuc nang PASS | assert dung so instrument | ✅ (Win xong + Linux review OK) |
|
||||
| G2.2 | main.cpp: control type=5 DUMP_STATE (post saveStateJob len uiWorker ngay lap tuc). lib.rs: flush_bridge_state() — push_control(5,0,0,0) + sleep 300ms, goi dau kill_bridge() (cover restart_bridge_with_sample_rate + stall-restart) va truoc khi giet bridge trong window-close Destroyed | Build PASS (daw_vst_bridge.exe) + cargo check PASS (src-tauri) + deploy + verify live PASS: DUMP qua SHM -> log "DUMP_STATE - flushing state now", state file intact (7 inst), writeIndex advancing | Runtime Rust flush chua test duoc khi khong build app Tauri (cargo check xac nhan compile) | | ✅ (Linux review OK) |
|
||||
| G2.3 | main.cpp: startup restore block (doc SF_STATE_FILE, guard channel trong, post LOAD job tung channel qua uiWorker, assign + loadSerializedState preset / selectProgram bank-program, g_loadMutex serialize, restore transport PLAY + playhead neu state playing). Vst3Instrument: base64Decode + loadSerializedState (parse [4B BE compLen][comp][4B BE ctrlLen][ctrl], goi restoreState). INativeInstrument.h: virtual loadSerializedState no-op. lib.rs: spawn_bridge set env SF_STATE_FILE = %APPDATA%/SonicForgeDAW/state/bridge_state.json. | Build PASS (bridge + cargo check). Deploy PASS. Verify live PASS: LOAD ch5 + PLAY (playhead 5000) -> kill bridge -> respawn -> log "restoring 7 instruments" + "restored ch=0..6" (Nexus + EZkeys) + "restoring transport PLAY playhead=5000" + ch5 preset 3488B giu nguyen; state file playing=true playhead=5000 inst=7; writeIndex advancing (14859->15234). | - | - | ✅ (Win xong + Linux review OK) |
|
||||
| G2.4 | main.cpp: LOAD case double-load guard — skip khi channel da co cung path (lower_plugin_path(ch) == lower_path_str(path)), giu instance + preset da restore khi JS reload sau respawn/bootstrap | Verify live PASS: LOAD lai Nexus ch5 sau respawn -> log "LOAD skip ch=5 (already loaded)", preset ch5 3488B giu nguyen, writeIndex advancing; 15 LOAD skip trong log (JS reload 7ch x2 + test) | Khong can sua JS: loadVstToBridge/loadTrackInstrumentToBridge khong co guard __bridgePlaying (da chay khi PLAY); chi openVstGuiRetry giu guard OPEN_GUI (G0.2, dung) | | ✅ (Linux review OK) |
|
||||
| G2.5 | native_bridge/debug/test_g2_stress.py (mới, khung test_g15_stress.py: ctypes SHM, write_control, wait_log, bridge_pid, bad_since) + bước kill daw_vst_bridge giữa vòng + chờ watchdog respawn; verify restore + preset giữ + LOAD-skip guard (G2.4) + writeIndex advancing; test_g15_stress.py: LOAD ch5 chấp nhận "LOAD skip" (guard G2.4 đổi log) | Chạy 5 + 10 iterations PASS (tổng 20 lần kill/respawn): LOAD skip pre-kill/post-respawn OK, restore ch5/ch6 OK, watchdog "[tauri] bridge restarted" OK, bridge alive; preset Nexus giữ nguyên (exact), EZkeys sim>=0.87; writeIndex advancing (delta 283-376) | Assert preset ch6 byte-exact FAIL giả: EZkeys nhúng PlayHeadPos vào state riêng (đổi cả giữa 2 save thường, ratio 0.872) | ch5 so exact, ch6 so ratio>=0.7 + |len chênh|<=64 | ✅ (Win xong + Linux review OK) |
|
||||
| G2.6 | Chạy lại regression G1.5 (test_g15_stress.py) + G2.5 (5+10 iter) + probe G0.4 (bản cập nhật PID + guard-aware, OPEN_GUI full path) | G1.5 regression PASS 5/5 (LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE, writeIndex delta 307-551, bridge alive); G2.5 PASS 5+5+10 (20 lần kill/respawn, preset giữ, không crash); G0.4 probe PASS: LOAD skip -> OPEN_GUI hwnd -> WM_CLOSE -> capture 2628B -> reload -> restore 2628B -> OPEN_GUI lại attach OK, không restart/EXCEPTION/FAILED | Không có bug sản phẩm mới; 2 bug test-script: (1) probe G0.4 so sánh PID int vs list -> báo restart giả, (2) preset EZkeys byte-diff do PlayHeadPos runtime (đã phân tích ở G2.5) | Sửa test scripts (guard-aware wait, pid compare); không sửa code sản phẩm | ✅ (Win xong + Linux review OK) |
|
||||
|
||||
### 5.1 Review + verify phía Linux (2026-08-16, Linux — chỉ review/ghi chú, KHÔNG sửa code)
|
||||
Kết quả review toàn bộ diff G2 (`git diff 1fe2d26~1..b59e416`, 15 files; không có file JS trong diff).
|
||||
|
||||
**Code review — nhất trí với báo cáo Windows, không tìm thấy bug sản phẩm:**
|
||||
- `Vst3Instrument.cpp`: `base64Encode`/`base64Decode` đúng RFC 4648 (padding `=`, decode bỏ qua whitespace); `loadSerializedState` có bounds-check cả componentLen lẫn controllerLen trước khi copy — blob hỏng không gây OOB; gán buffer xong mới gọi `restoreState()` (đúng thứ tự).
|
||||
- `NativeInstrumentEngine.cpp`: `snapshot()` giữ `mu_` suốt `captureState()` — audio loop cũng serialize bằng `mu_` per block nên không race capture với process; skip channel đang reload (`reloadingCh_`, instance cũ sắp chết); `bank_/program_` cập nhật đúng (reset khi `loadSoundFontFile`, set khi program_select/programChange).
|
||||
- `main.cpp`: `saveStateJob` defer khi có channel reloading (đặt lại dirty → retry sau debounce) — không ghi state lúc instance nửa chết; `snapshotsEqual` loại playhead (không ghi file mỗi ~2s khi PLAY); periodic mark 2s + debounce 300ms; startup restore chỉ nhận channel trống (`instruments.get(ch)==nullptr`) và serialize bằng `g_loadMutex`; LOAD guard so sánh lower-path — chỉ skip khi CÙNG path, path khác vẫn replace bình thường; case 5 DUMP_STATE post `saveStateJob` trực tiếp (không chờ debounce).
|
||||
- `lib.rs`: `flush_bridge_state` an toàn khi bridge treo (SHM do Rust sở hữu, control 5 không được consume — vô hại).
|
||||
- `test_g2_stress.py` + `test_state_store.cpp` + sửa `test_g15_stress.py`: logic vững (wait_load chấp nhận real hoặc skip; SequenceMatcher ≥0.7 cho EZkeys vì PlayHeadPos runtime; bad_since watch EXCEPTION/FAILED; round-trip cover escape `\n \t \" \\`, atomic .tmp, missing file, clear).
|
||||
|
||||
**Findings (minor, không chặn; Linux không sửa code — chờ Windows quyết định):**
|
||||
1. **F1 (minor)**: window-close gọi `flush_bridge_state()` ở `run()` VÀ `kill_bridge()` giờ cũng tự gọi → DUMP_STATE 2 lần khi đóng app (+~300ms shutdown). Harmless; có thể bỏ lời gọi thừa trong `run()`.
|
||||
2. **F2 (portability note)**: `lib.rs` luôn set `SF_STATE_FILE` sang đường dẫn Windows; nếu chạy bridge non-Windows, env thành `./SonicForgeDAW/state/bridge_state.json` (theo CWD) thay vì Linux fallback `$HOME/.sonicforge/state/` của `StateStore::defaultPath`. Không ảnh hưởng target Windows (cùng path APPDATA); ghi nhận để sửa khi cần bridge chạy Linux.
|
||||
3. **F3 (cosmetic)**: `loadSerializedState` in cùng message "bad ctrlLen" cho 2 check khác nhau (thiếu header / độ dài vượt) — chỉ log, không ảnh hưởng hành vi.
|
||||
|
||||
**Verify trên Linux (cú pháp/logic — máy Linux không build Windows):**
|
||||
- `python3 -m py_compile native_bridge/debug/test_g2_stress.py`, `test_g15_stress.py`: PASS.
|
||||
- `g++ -std=c++17 -Wall -Wextra debug/test_state_store.cpp + src/StateStore.cpp` (Linux): compile sạch (1 warning benign multi-line comment) + run **"state_store round-trip PASS"** → StateStore portable, atomic write + escape verified trên Linux.
|
||||
- `node --check app/static/js/app.precompiled.js`: PASS (G2 không sửa JS — diff stat không có file JS).
|
||||
- Rust: `rustc`/`cargo` chỉ là rustup proxy, chưa cài default toolchain → không chạy được `cargo check` trên Linux (khớp ghi chú big_tasks.md "máy Linux không có rustc toolchain"); Windows đã `cargo check` PASS ở G2.2/G2.3.
|
||||
|
||||
**Kết luận Linux**: code G2 nhất quán với báo cáo Windows; không có bug sản phẩm; 2 findings minor (F1, F2) + 1 cosmetic (F3) không chặn merge. Trạng thái G2.0–G2.6: ✅ (Win xong + Linux review xác nhận).
|
||||
|
||||
### 5.2 Audit bảo mật / bộ nhớ / conflict child process (2026-08-16, Linux — chỉ review/ghi chú, KHÔNG sửa code)
|
||||
|
||||
Đọc toàn bộ: `native_bridge/src/main.cpp` (1410), `NativeInstrumentEngine.cpp` (439), `Vst3Instrument.cpp` (873), `StateStore.cpp` (288), `SandboxVst3Host.cpp` (192), `plugin_host_main.cpp` (163), `SharedMemoryIPC.cpp` (125), `src-tauri/src/shm.rs` (201), `src-tauri/src/lib.rs` (791), `app/static/js/services/nativeBridgeService.js` (180), `app/static/js/app.precompiled.js` (luồng bridge). Mục tiêu: race UAF / double-buffer / conflict 2 bridge cùng SHM / orphan / command injection / luồng có đúng thiết kế.
|
||||
|
||||
**KẾT LUẬN: không có bug chặn release; 2 MEDIUM (F-MEM-5 UAF, F-PROC-1 injection), 1 MEDIUM (F-PROC-2 race restart×watchdog), 1 MEDIUM audio-correctness (F-MEM-8), 2 LOW (F-PROC-3, F5). Tất cả nằm ở đường hiếm (deschedule >10ms, path thù địch, restart trùng stall) — đề xuất Windows xử lý theo ưu tiên F-PROC-1 > F-MEM-5 > F-PROC-2.**
|
||||
|
||||
**F-MEM-5 (MEDIUM — UAF trên audio thread):** `InstrumentEngineManager::get()` (NativeInstrumentEngine.cpp:300) lock `mu_` rồi trả raw `unique_ptr::get()` sau unlock. `assign()`/`unload()` (dòng 182/240) swap map DƯỚI `mu_` nhưng destroy `oldInst` NGOÀI lock, sau `sleep 10ms`. Drain loop control queue chạy trên **audio thread** (main.cpp:1115) gọi `instruments.get(ch)` rồi deref sau unlock: handleOpenGui dedupe `i0->hasAttachedView()` (main.cpp:794), pending-GUI sweep (884/909/927/953), STOP transport `inst->controlChange(ch,64,0)` (main.cpp:1290). Nếu UiThread assign xong + ngủ 10ms + reset oldInst trong lúc audio thread bị deschedule giữa get() và deref → deref instance đã free → UAF (cửa sổ ~10ms+). Các site trong attach job (823) an toàn vì cùng UiThread với assign. Hướng fix: giữ `mu_` quanh get+use như MIDI dispatch (đã làm đúng ở dòng 338), hoặc get() trả shared_ptr.
|
||||
|
||||
**F-MEM-8 (MEDIUM — audio correctness):** double-buffer G4.3 không handshake (SharedMemoryIPC.h): bridge đọc `writeSlot` + `audioLeft/Right` trong lúc child flip slot → block rách/glitch (không memory corruption — mảng cố định 256). `processAudioBlock` đọc con trỏ `ipc_` non-atomic trong lúc watchdog respawn child (UB lý thuyết, x86 OK). Ghi nhận theo dõi.
|
||||
|
||||
**F-PROC-1 (MEDIUM — command injection):** `SandboxVst3Host::spawnChild()` (SandboxVst3Host.cpp:76-78) nội suy `--path "..."` + `--sr` vào cmdline, gọi `CreateProcessA(nullptr, buf)` — path lấy từ LOAD control / state file. Path chứa `"` + metachar (VD `" && calc`) → thực thi lệnh tuỳ ý (cùng user). Fix: `lpApplicationName` + lpCommandLine arg đã quote đúng, hoặc truyền path qua env/SHM. Severity giữ MEDIUM vì attacker cần kiểm soát path (file .vst3 thù địch / state file sửa tay).
|
||||
|
||||
**F-PROC-2 (MEDIUM — race restart × watchdog → 2 bridge cùng SHM):** `restart_bridge_with_sample_rate` (invoke thread) và pump watchdog (thread riêng) đều chạy `kill_bridge()` → `spawn_bridge()`. `kill_bridge` serialize bằng `BridgeProcess` mutex nhưng **kill→spawn không nguyên tử**: 2 luồng cùng đến `spawn_bridge` → 2 `daw_vst_bridge.exe` cùng map SHM (tên `SonicForge_DAW_IPC_<pid>` — CÙNG tên) → double LOAD / OPEN_GUI trùng / control queue rối. `set_bridge_child` overwrite child cũ → rò rỉ bridge đầu (không kill, log redirect task sống theo pipe). Khả năng overlap cao hơn vì cả 2 luồng đều ngủ 300ms trong `flush_bridge_state`. Kích hoạt thực tế: bootstrap JS gọi restart (mỗi lần webview load) trùng lúc bridge mới không kịp heartbeat → watchdog stall 3s. Fix: 1 mutex phủ cả kill+spawn, hoặc spawn_bridge kiểm tra "không còn child tracked" trước khi spawn.
|
||||
|
||||
**F-PROC-3 (LOW — orphan race khi đóng app):** pump thread check `SHUTTING_DOWN` ở đầu vòng, nhưng giữa `kill_bridge` và `spawn_bridge` trong cùng 1 vòng không check lại → Destroyed lấy `None` (pump vừa kill), pump spawn bridge sau khi app đang tắt → orphan tạm thời. Child có `--parent` monitor nên tự thoát khi Tauri exit → transient, không nghiêm trọng. Fix: check SHUTTING_DOWN lần nữa trước spawn_bridge.
|
||||
|
||||
**F5 (LOW — observability):** `open_vst_gui` command (lib.rs:626) luôn trả `Ok(())` kể cả khi SHM unavailable / push fail — lỗi chỉ `eprintln` vào stderr Tauri (không vào bridge.log) → JS không biết thất bại, retry vô ích. Nên trả Err để JS dừng sớm.
|
||||
|
||||
**F6 (LOW — comment lệch code):** `openVstGuiRetry` cap thực tế `n=min(attempts,2)` (2 lần) trong khi comment ghi "Cap 5" (bản cũ ghi 20). Không lỗi hành vi, chỉ lệch comment.
|
||||
|
||||
**Luồng xác nhận ĐÚNG thiết kế:**
|
||||
- Control queue drain TRƯỚC audio render trong cùng loop (main.cpp:1115-1315) — control không bị đói.
|
||||
- LOAD job post lên UiThread, serialize `g_loadMutex`; audio thread không stall (comment G3.1) — writeIndex stall 3s chỉ khi heartbeat thread kẹt thật.
|
||||
- OPEN_GUI: defer khi PLAY (G0.1) + pending sweep khi `transportStopped && instruments.get(ch)`; dedupe `hasAttachedView` chống spam retry; attach job trên UiThread (COM STA) + g_loadMutex + AttachInFlightGuard + same-path silence + destroy child window trên owner thread (0xc000041d fix).
|
||||
- STOP transport: CC64=0 release sustain TRƯỚC allNotesOff (V8 bug 3) — đúng.
|
||||
- DUMP_STATE (type=5) post saveStateJob trực tiếp, không chờ debounce; kill_bridge flush 300ms; Rust pump drop guard trước kill (chống deadlock G3.1).
|
||||
- G2.4 LOAD-skip guard (lower-path compare) — idempotent sau respawn, preset giữ.
|
||||
- Rust pump: liveness = heartbeat chuyên dụng (không phải writeIndex), phân loại HANG/CRASH, max 1 restart/incident; `restart_attempts` reset khi hb advance.
|
||||
- Destroyed: SHUTTING_DOWN trước kill → pump dừng (trừ race F-PROC-3).
|
||||
- JS: `__bridgePlaying` guard openVstGuiRetry; load instrument im lặng khi PLAY (không tự mở GUI); `__bridgeLoadedChannels` reset cả connected lẫn !connected (V8 bug 4) → reconnect reload đúng, không mở GUI; C++ LOAD-skip làm reload idempotent.
|
||||
- shm.rs layout khớp C++ (selfcheck 12/1040/11168/15268); `push_control` zero-fill arg2 → `std::string(c.arg2)` an toàn với mọi writer hiện tại; MIDI count non-atomic (race benign, volatile).
|
||||
|
||||
**Verify Linux:**
|
||||
- `g++ -std=c++17 -Wall -Wextra -fanalyzer` StateStore.cpp: compile sạch (0 warning) + `test_state_store.cpp` run **"state_store round-trip PASS"**.
|
||||
- `SharedMemoryIPC.cpp` / `SandboxVst3Host.cpp` / `plugin_host_main.cpp`: Windows-only (xung đột POSIX `shm_open`, thiếu HANDLE/CreateProcessA) — không build được trên Linux, đúng kỳ vọng.
|
||||
- `node --check` app.precompiled.js + nativeBridgeService.js: PASS.
|
||||
- Rust: không có toolchain (như G2.6).
|
||||
|
||||
### Nhật ký thực hiện
|
||||
- **G2.0** (2026-08-16, Windows): tao `native_bridge/include/StateStore.h` + `native_bridge/src/StateStore.cpp` (CRLF, JSON viet tay, atomic write .tmp→rename, escape \ "
|
||||
|
||||
, load parser an toan). Them StateStore.cpp vao CMakeLists target daw_vst_bridge. Build bridge PASS. Test round-trip `native_bridge/debug/test_state_store.cpp` PASS (compile cl vcvars64 + chay). Commit `G2.0: StateStore (JSON state file + atomic write)`.
|
||||
- **G2.1** (2026-08-16, Windows): save day du state ra disk. main.cpp: globals + snapshotsEqual + stateFile env fallback + playheadSamples atomic + saveStateJob (UiThread, skip isReloading, chi ghi khi snapshot doi) + hook TRANSPORT/LOAD + periodic 2s dirty debounce 300ms. Them khai bao bankOf/programOf vao Vst3Instrument.h (thieu declaration -> build fail). StateStore.cpp: ensureParentDir() (CreateDirectoryA chain, Linux mkdir) goi dau save() (fix C2001 do heredoc backslash mangle qua script fix_bs.py). Build PASS. Deploy PASS (PID 9876/24632/7352). Verify live PASS: LOAD Nexus ch5 + PLAY -> state file 7 inst + playing=true + presetBase64 day du; STOP -> playing=false. StateStore round-trip test PASS. Commit `G2.1: save full instrument state to disk (path/type/preset/transport)`.
|
||||
- **G2.3** (2026-08-16, Windows): auto-reload sau respawn.
|
||||
- **G2.2** (2026-08-16, Windows): dump state truoc kill. main.cpp: them control type=5 DUMP_STATE sau case 4 — post saveStateJob len uiWorker ngay (khong cho debounce 300ms/2s), log "DUMP_STATE - flushing state now". lib.rs: them flush_bridge_state(app) — shm.push_control(5,0,0,0,"") roi sleep 300ms (du thoi gian UiThread ghi xong file); goi o dau kill_bridge() (cover ca restart_bridge_with_sample_rate va stall-restart) va trong on_window_event Destroyed truoc khi kill_process_tree(bridge). Build PASS (daw_vst_bridge.exe) + cargo check PASS (src-tauri). Deploy + verify live PASS: push DUMP qua SHM driver -> log "DUMP_STATE - flushing state now" xuat hien, state file intact (7 inst, playing=false), writeIndex advancing. Ghi chu: nhanh Rust flush chi cargo check (khong build app Tauri) — logic don gian, compile OK. Commit `G2.2: flush bridge state to disk before kill (control type=5 DUMP + Rust pre-kill)`.
|
||||
- **G2.4** (2026-08-16, Windows): go double-load khi JS reload sau respawn. main.cpp: them guard dau LOAD case (truoc setReloading) — neu channel da co cung path (so sanh lower_plugin_path(ch) vs lower_path_str(path)) thi log "LOAD skip ch=N (already loaded)" va return, khong assign lai (thay instance se mat preset da restore tu state file). Ly do: JS reconnect/bootstrap reload gui LOAD cung path sau respawn -> truoc day assign() thay instance -> mat preset. Guard PLAY cua G2.4 (cho restore chay khi PLAY) da co san o JS: loadVstToBridge/loadTrackInstrumentToBridge khong co guard __bridgePlaying, chi openVstGuiRetry giu guard OPEN_GUI (G0.2 — defer GUI khi PLAY van dung). Build PASS (daw_vst_bridge.exe) + deploy + verify live PASS: sau respawn, LOAD lai Nexus ch5 -> "LOAD skip ch=5 (already loaded: ...Nexus.vst3)", preset 3488B giu nguyen, writeIndex advancing; tong 15 LOAD skip (JS reload 7ch x2 + test ch5). Commit `G2.4: skip redundant LOAD of already-loaded instrument (keep restored preset)`. main.cpp: block restore truoc audio loop - doc state file, chi restore channel trong (guard instruments.get(ch)==nullptr), post LOAD job (assign + loadSerializedState / selectProgram) serialize bang g_loadMutex, neu state playing thi set playhead + g_transportPlaying=true + g_stateDirty. Vst3Instrument.cpp: base64Decode (b64val lookup) + loadSerializedState (parse blob, goi restoreState); Vst3Instrument.h + INativeInstrument.h: khai bao. lib.rs: spawn_bridge them env SF_STATE_FILE (fallback StateStore::defaultPath khi khong set). Build PASS (daw_vst_bridge.exe + cargo check). Deploy + verify live PASS: LOAD ch5 + PLAY playhead 5000 -> taskkill bridge -> respawn tu dong -> log restore 7 inst (Nexus ch0-5, EZkeys ch6) + restore transport PLAY playhead=5000 + ch5 preset 3488B giu nguyen; state file playing=true inst=7; writeIndex advancing. Commit `G2.3: auto-reload instruments from state file after bridge respawn`.
|
||||
- **G2.5** (2026-08-16, Windows): tao native_bridge/debug/test_g2_stress.py (khung test_g15_stress.py). N vong: LOAD Nexus ch5 + EZkeys ch6 (real hoac skip deu OK) -> PLAY -> taskkill daw_vst_bridge -> cho watchdog "[tauri] bridge restarted" + "restored ch=5/6" -> LOAD lai -> verify "LOAD skip" (guard G2.4) + preset giu (ch5 exact, ch6 sim>=0.7 — EZkeys nhung PlayHeadPos vao state, ratio 0.872 giua save thong thuong) + writeIndex advancing. Chay 5 + 10 iterations: PASS (20 lan kill/respawn, khong crash, bridge alive). Kem sua test_g15_stress.py: LOAD ch5 chap nhan "LOAD skip" (guard G2.4 doi log). Commit `G2.5: stress driver for persistence restart (N loop + kill/respawn)`.
|
||||
- **G2.6** (2026-08-16, Windows): verify tong.
|
||||
- **G2.0–G2.6 review + verify** (2026-08-16, Linux — máy Linux chỉ review/ghi chú, KHÔNG sửa code): đọc toàn bộ diff G2 (`1fe2d26~1..b59e416`, 15 files) — Vst3Instrument base64 + loadSerializedState (bounds-check đủ), NativeInstrumentEngine snapshot (mu_ chặn race với audio loop, skip reloading), main.cpp saveStateJob/restore/LOAD-guard/DUMP/debounce, lib.rs flush_bridge_state + env SF_STATE_FILE, test_g2_stress.py + test_state_store.cpp + test_g15_stress.py. Verify Linux: py_compile 2 stress scripts PASS; g++ compile + run test_state_store PASS (StateStore portable); node --check app.precompiled.js PASS; rustc/cargo không có default toolchain → không cargo check (Windows đã check PASS). Findings: F1 double flush_bridge_state khi window-close (minor), F2 SF_STATE_FILE thiếu Linux fallback (portability, không ảnh hưởng target Windows), F3 log "bad ctrlLen" trùng cho 2 check (cosmetic). Không sửa code (đúng phân máy). Chi tiết ở mục 5.1. G1.5 regression PASS 5/5 (LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE, writeIndex delta 307-551, bridge alive). G2.5 PASS 5+5+10 (20 lan kill/respawn, preset giu, khong EXCEPTION/FAILED). Probe G0.4 (ban cap nhat: PID dong, LOAD guard-aware, OPEN_GUI full path): PASS — LOAD skip -> OPEN_GUI hwnd -> WM_CLOSE -> "state captured ch=5 component=2628B" -> reload -> "state restored ch=5 component=2628B" -> OPEN_GUI lai attach OK, khong restart. Bug tim duoc: khong co bug san pham; 2 bug test-script (probe PID int vs list -> bao restart gia; preset EZkeys byte-diff do PlayHeadPos runtime). Commit `G2.6: verify — regression G1.5 + stress G2.5 + probe G0.4 all PASS`.
|
||||
- **Audit bảo mật/bộ nhớ/conflict child process** (2026-08-16, Linux — chỉ review/ghi chú, KHÔNG sửa code): đọc toàn bộ main.cpp/NativeInstrumentEngine/Vst3Instrument/StateStore/SandboxVst3Host/plugin_host_main/SharedMemoryIPC/shm.rs/lib.rs/nativeBridgeService.js/app.precompiled.js. Findings: F-MEM-5 UAF audio-thread get()+deref vs assign() destroy oldInst ngoài lock sau 10ms (MEDIUM, sites 794/884/909/927/953/1290); F-MEM-8 double-buffer không handshake (MEDIUM audio); F-PROC-1 command injection `--path "..."` CreateProcessA (MEDIUM); F-PROC-2 race restart×watchdog → 2 bridge cùng SHM (MEDIUM); F-PROC-3 orphan race Destroyed (LOW); F5 open_vst_gui luôn Ok (LOW); F6 comment cap 2 vs 5 (LOW). Luồng chính đúng thiết kế: drain control trước render, LOAD UiThread+g_loadMutex, OPEN_GUI defer PLAY + pending sweep, STOP flush CC64=0, DUMP_STATE trực tiếp, pump drop-guard trước kill, max 1 restart/incident, JS __bridgePlaying + __bridgeLoadedChannels reset. Verify Linux: StateStore -fanalyzer sạch + round-trip PASS; 3 file Windows-only không build được (đúng); node --check 2 JS PASS. Không sửa code (đúng phân máy). Chi tiết mục 5.2.
|
||||
- **Fix audit 5.2** (2026-08-16, Windows): xử lý toàn bộ findings 5.2. F-PROC-1: SandboxVst3Host.cpp thêm quote_arg (escape \ và " theo Windows CRT) + CreateProcessA(hostExe.c_str(), ...) + bỏ token exe khỏi cmdline -> path thù địch chứa " giữ 1 argv (probe F-PROC-1 PASS: spawn ch=7 cmdline giữ trọn path trong --path, child không map HACKED_SHM, không nhận --sr 1000, load fail như mong đợi). F-MEM-5: NativeInstrumentEngine.h + .cpp thêm has() + template withInstrument() (giữ mu_ quanh call); main.cpp đổi 6 site audio-thread: 784/794 hasAttachedView dedupe, 1073 restore guard, 1283 CC64=0, 1318 pending-GUI, 1337 state-dirty -> has()/withInstrument(); giữ nguyên 8 site UiThread (an toàn cùng thread assign). F-PROC-2: lib.rs thêm static BRIDGE_RESTART_LOCK serialize kill+spawn giữa restart_bridge_with_sample_rate và pump watchdog. F-PROC-3: pump re-check SHUTTING_DOWN sau lock + sau kill trước spawn. F5: open_vst_gui trả Err (lock poisoned / SHM unavailable / queue full), frontend try/catch sẵn. F6: comment Cap 5 -> Cap 2 (hard min(attempts,2)). F-MEM-8: ghi nhận theo dõi, không fix (đúng audit). Build: daw_vst_bridge + plugin_host PASS; cargo build --release PASS (2m53s). Deploy install/ (kill trước, copy2). Regression: G1.5 PASS 3/3 (writeIndex delta 287), G2.5 PASS 2/2 (watchdog restart + preset giữ exact), probe F-PROC-1 PASS (không injection). Commit: fix 5.2 audit findings on Windows.
|
||||
|
||||
---
|
||||
|
||||
## 6. PHỤ LỤC A — QUY TRÌNH THỰC HIỆN TRÊN MÁY WINDOWS
|
||||
|
||||
> Quy trình 2 máy: **Windows sửa code + build + test + fix + commit + push** →
|
||||
> **Linux pull, review, ghi chú, cập nhật báo cáo + push** → Windows pull lại, verify tiếp.
|
||||
> Các lệnh dưới chạy trên máy Windows (PowerShell), đường dẫn theo máy dev `C:\Users\locpham\SonicForgeStudio`.
|
||||
|
||||
### 6.1 Cấu trúc + đường dẫn cố định
|
||||
| Thứ | Vị trí |
|
||||
|---|---|
|
||||
| Repo (Windows) | `C:\Users\locpham\SonicForgeStudio` |
|
||||
| Build bridge | `native_bridge\build\` (CMake) — exe: `native_bridge\build\Release\daw_vst_bridge.exe` |
|
||||
| Install (bundle chạy thật) | `C:\Users\locpham\SonicForgeStudio\install\` — app: `sonicforge-daw.exe` |
|
||||
| Logs | `%APPDATA%\SonicForgeDAW\logs\` — `bridge.log` (stdout bridge), `spawn.log` (watchdog/restart), `engine.log` |
|
||||
| State file G2 (MỚI) | `%APPDATA%\SonicForgeDAW\state\bridge_state.json` (+ `.tmp` khi atomic write) |
|
||||
| VST test | `C:\Program Files\Common Files\VST3\Nexus.vst3`, `...\Toontrack\EZkeys 2.vst3` |
|
||||
| SHM | `SonicForge_DAW_IPC_<APP_PID>`; offsets: controlQueue @2836 (8×1040), count @11156, attachInFlight @11160, writeIndex @4, size 11164. Types: 2=LOAD, 3=TRANSPORT, 4=OPEN_GUI, **5=DUMP_STATE (G2.2 thêm)** |
|
||||
| Probe | `native_bridge\build\Release\gui_probe.exe` (variants: main_own/worker_own_nopump/…/ui_thread/shared_worker) |
|
||||
|
||||
### 6.2 Build
|
||||
```powershell
|
||||
# trong repo Windows
|
||||
cd native_bridge\build
|
||||
cmake -DCMAKE_CXX_FLAGS_RELEASE='/O2 /Ob2 /DNDEBUG /Zi' -DCMAKE_EXE_LINKER_FLAGS_RELEASE='/DEBUG' .
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
cmake --build . --config Release --target daw_vst_bridge
|
||||
```
|
||||
hoặc chạy script có sẵn: `native_bridge\debug\build_bridge.ps1` (dùng build dir cũ).
|
||||
- Thêm file mới (G2.0 `StateStore.cpp`) → nhớ thêm vào `native_bridge/CMakeLists.txt` target `daw_vst_bridge`, rồi re-run cmake.
|
||||
- **Lưu ý CRLF**: toàn bộ `native_bridge/` + `CMakeLists.txt` là CRLF — patch đa dòng phải qua script Python `io.open(newline='')`, không heredoc `<<'EOF'` (khớp sai).
|
||||
|
||||
### 6.3 Deploy (mẫu `deploy_g12.ps1`)
|
||||
```powershell
|
||||
$install = 'C:\Users\locpham\SonicForgeStudio\install'
|
||||
$new = 'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
|
||||
Get-Process sonicforge-daw, daw_engine, daw_vst_bridge -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep -Seconds 2
|
||||
Copy-Item -Path $new -Destination (Join-Path $install 'daw_vst_bridge.exe') -Force
|
||||
Start-Process -FilePath (Join-Path $install 'sonicforge-daw.exe')
|
||||
Start-Sleep -Seconds 8
|
||||
Get-Process sonicforge-daw, daw_engine, daw_vst_bridge -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, Path | Format-Table -AutoSize
|
||||
```
|
||||
- 3 process phải kill hết: `sonicforge-daw`, `daw_engine`, `daw_vst_bridge` (bridge cũ không chết → 2 bridge map chung SHM → race).
|
||||
- Sau start: lấy **APP_PID mới** (tasklist/Get-Process) → truyền vào probe (G2.5) — PID đổi mỗi lần deploy.
|
||||
|
||||
### 6.4 Verify không cần engine (Linux + Windows đều chạy được)
|
||||
- `shm_selfcheck.cpp` (đã build từ trước; layout 11164 → sau G2 **không đổi** vì state ghi ra đĩa, không thêm field SHM): chạy pass = control queue/attach flag/writeIndex đúng.
|
||||
- JS: `node --check app/static/js/app.precompiled.js` (sau mỗi lần sửa JS).
|
||||
- Rust (G2.2): `cargo check` trong `src-tauri/` — máy Linux hiện không có toolchain → bắt buộc trên Windows.
|
||||
|
||||
### 6.5 Stress (G2.5, theo mẫu `test_g15_stress.py`)
|
||||
```powershell
|
||||
python native_bridge\debug\test_g2_stress.py <APP_PID> <N>
|
||||
```
|
||||
- Đọc SHM trực tiếp (ctypes), đẩy control LOAD/OPEN_GUI/PLAY/STOP, theo dõi `bridge.log`.
|
||||
- Verify: bridge alive, writeIndex advancing mỗi iter, không EXCEPTION/FAILED/crash.
|
||||
- **G2 thêm bước kill giữa chừng**: `Stop-Process daw_vst_bridge` → chờ respawn (spawn.log) → verify writeIndex + preset restored.
|
||||
- Cảnh báo learning cũ: **probe PASS có thể dương tính giả** → luôn kiểm tra `bridge.log`/`spawn.log` thực; dùng chung `since` window (log cùng job xuất hiện ~đồng thời), không reset.
|
||||
|
||||
### 6.6 Vòng lặp bắt bugs → fix → report
|
||||
1. **Windows** chạy verify → lỗi → thu log (`bridge.log`, `spawn.log`, `engine.log`) + (nếu crash) minidump/cdb.
|
||||
2. **Windows** xác định lỗi thuộc task nào → sửa code → build → deploy → verify lại → commit + push.
|
||||
3. **Linux** pull → review + ghi chú, cập nhật bảng mục 5 + nhật ký → push.
|
||||
4. **Windows** pull lại, tiếp tục task sau.
|
||||
5. Kết thúc G2.6: chạy lại regression G1.5 (`test_g15_stress.py`) — G2 không được làm hỏng G1.
|
||||
|
||||
### 6.7 Tiện ích / lưu ý khác
|
||||
- `tasklist` CSV: dùng `subprocess.run(['tasklist'])` full output + `line.split()[1]`.
|
||||
- `sleep` không tồn tại trong sh (máy Windows) → `python -c "import time; time.sleep(N)"`.
|
||||
- `timeout` Windows khác GNU → dùng wrapper python subprocess.
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
# BIG PHASE 3 — Export MIDI/Mix, FX CHAIN, Virtual MIDI Keyboard & Chords Panel
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ GHI NHỚ — QUY TẮC BẮT BUỘC KHI THỰC HIỆN (đọc trước, tuân thủ mọi phase)
|
||||
|
||||
1. **KHÔNG ĐƯỢC LÀM HỎNG code đã chạy ổn định.** 4 lỗi nghiêm trọng đã fix ở phase trước, tuyệt đối không tái phạm (verify regression sau MỖI thay đổi):
|
||||
- Âm lượng track không đồng đều (jbridge/SandboxVst2Host — calibrate makeup phải ≈2.59–2.71 cho Qin, KHÔNG được quay lại makeup=1.0).
|
||||
- VST GUI đã load không hiện / mất GUI sau khi đóng-mở (PluginHost z-order top, embed child #32770 jBridge + Qin_VST_Window).
|
||||
- Bị câm khi đã load instrument (VSTi/SF2 qua bridge — load xong phải nghe được ngay).
|
||||
- Bị câm khi preview MIDI (preview VSTi/midi-render phải ra âm).
|
||||
- Trước khi commit bất kỳ phase nào: chạy lại bộ verify âm thanh cũ (note_quick / open_gui / silence_all) xác nhận KHÔNG regression.
|
||||
2. **PHẢI OPTIMIZE sau mỗi lần sửa code** (xóa biến chết, branch chết, code trùng; gọn nhất có thể — YAGNI).
|
||||
3. **LUÔN LUÔN tuân thủ đường dẫn của các lần compile/build trước:**
|
||||
- Frontend: sửa trong `app/static/js/app.jsx` → `node build.mjs` → `app/static/js/app.precompiled.js`.
|
||||
- Engine: `python -m PyInstaller engine.spec --clean --noconfirm` từ repo root → output `dist/daw_engine/` → **copy toàn folder** sang `src-tauri/target/release/daw_engine/`.
|
||||
- C++ bridge: build trong `native_bridge/` bằng `cmake --build build --config Release --target plugin_host daw_vst_bridge` → copy `plugin_host.exe` + `daw_vst_bridge.exe` sang `src-tauri/binaries/` (gồm bản `-x86_64-pc-windows-msvc.exe`) + `src-tauri/target/release/`.
|
||||
4. **KHÔNG TỰ Ý thay đổi các đường dẫn cũ** (SHM layout, storage paths `%APPDATA%/SonicForgeDAW`, plugin dirs, soundfont dir, port 8000–8010, tên exe, cấu trúc `src-tauri/`). Chỉ đổi khi có lý do bắt buộc và ghi rõ trong commit.
|
||||
5. **SAU KHI BUILD HOÀN THÀNH phải dọn dẹp tập tin tạm / tập tin rác:** xóa `build/`, `dist/` (sau khi đã copy xong), `__pycache__/`, `.pytest_cache/`, file `.bak`/`.new`/`.recovered` không dùng, file log rác — rồi `git status` phải sạch (chỉ còn thay đổi chủ đích).
|
||||
|
||||
---
|
||||
|
||||
> Repo: `C:/Users/locpham/SonicForgeStudio` · branch: `standalone-shm-bridge`
|
||||
> Frontend là 1 file monolith: `app/static/js/app.jsx` (JSX) → build bằng `node build.mjs` → `app/static/js/app.precompiled.js` (file được template nạp: `app/templates/index.html` load `app.precompiled.js?v=...`).
|
||||
> Backend engine (FastAPI) đóng băng bằng PyInstaller → **MỌI thay đổi frontend/backend đều phải rebuild engine** rồi copy sang folder chạy thật (xem §0).
|
||||
|
||||
---
|
||||
|
||||
## 0. QUY TRÌNH BUILD & CHẠY THỬ (bắt buộc cho mọi phase)
|
||||
|
||||
```bash
|
||||
# 1. Sửa app/static/js/app.jsx (hoặc file backend .py)
|
||||
# 2. Recompile frontend:
|
||||
node build.mjs
|
||||
# 3. Rebuild engine đóng băng (nếu đổi app.jsx HOẶC app/**.py):
|
||||
cd /c/Users/locpham/SonicForgeStudio
|
||||
python -m PyInstaller engine.spec --clean --noconfirm
|
||||
# 4. Copy engine mới sang nơi app thật dùng:
|
||||
cp -r dist/daw_engine/* src-tauri/target/release/daw_engine/
|
||||
# 5. Kill tiến trình cũ rồi chạy app thật:
|
||||
taskkill //IM sonicforge-daw.exe //F 2>/dev/null; taskkill //IM daw_engine.exe //F 2>/dev/null
|
||||
src-tauri/target/release/sonicforge-daw.exe &
|
||||
# 6. Login API (backend port 8000):
|
||||
# POST http://127.0.0.1:8000/api/v1/auth/login {"username":"admin","password":"admin123"}
|
||||
# → token → header `Authorization: Bearer <token>` cho các endpoint cần auth.
|
||||
```
|
||||
|
||||
Ghi chú:
|
||||
- `app/static/js/*.js` (services) và `app/static/js/app.jsx` là LF; `native_bridge/` C++ là CRLF — kiểm tra bằng `file` trước khi patch binary.
|
||||
- Chạy thật = `src-tauri/target/release/sonicforge-daw.exe` (Tauri WebView2). Không test bằng browser Chrome thuần — lỗi "sau khi chuyển standalone" chỉ tái hiện trong WebView2/engine đóng băng.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1 — FIX EXPORT MIDI (đã có feature, lỗi sau khi chuyển standalone)
|
||||
|
||||
### 1.1 Hiện trạng
|
||||
- Code xuất MIDI nằm ở `triggerMidiExport()` trong `app/static/js/app.jsx` (anchor ~dòng 26059).
|
||||
- Hoàn toàn client-side: gom note từ `track.midiItems[].notes` → build bytes MIDI format 1 (header `MThd`, từng track `MTrk`, delta time bằng `writeVLQ`) → `Blob` → download bằng `a.href = URL.createObjectURL(blob); a.click()`.
|
||||
- Menu: File → Export MIDI... (anchor ~29344). Còn 1 nút "Export MIDI" trong piano roll toolbar (~10534) chỉ gọi handler khác (midi-render → WAV), không phải export .mid.
|
||||
|
||||
### 1.2 Nghi vấn root cause (chưa xác nhận — chẩn đoán trước)
|
||||
1. **WebView2 chặn blob download**: `URL.createObjectURL` + `a.click()` với thuộc tính `download` không hoạt động trong WebView2 (không có hộp thoại lưu, không có sự kiện download). → Nghi ngờ cao nhất.
|
||||
2. Bug phụ trong code hiện tại (sửa luôn):
|
||||
- `velocity = Math.round((note.velocity || 0.8) * 100)` → scale 0..100, **sai chuẩn MIDI 0..127** (đáng lẽ `* 127`).
|
||||
- Track audio không có MIDI được xuất thành "rest track" với note pitch 0 velocity 1 — gây tiếng "bíp" nếu DAW khác phát file này; nên bỏ hẳn track không có note MIDI.
|
||||
|
||||
### 1.3 Giải pháp
|
||||
- **Bước A — Chẩn đoán xác nhận (làm trước khi code):**
|
||||
1. Mở app thật, tạo project có MIDI item (vài nốt), bấm File → Export MIDI... → quan sát: có hộp thoại lưu không? console WebView2 có lỗi không? (dùng `--remote-debugging-port` nếu cần).
|
||||
2. Nếu không có download → xác nhận giả thuyết WebView2 chặn blob.
|
||||
- **Bước B — Fix (2 lựa chọn, ưu tiên B1):**
|
||||
- **B1 (khuyên dùng): Server-side export endpoint.** Thêm `POST /api/v1/export/midi` vào `app/api/v1/audio.py` (hoặc file mới `app/api/v1/midi.py`):
|
||||
- Request: `{ bpm, ppq, tracks: [{ name, notes: [{ pitch, start_beat, duration_beats, velocity }] }] }`.
|
||||
- Backend dùng **stdlib thuần** (không thêm dependency) viết file .mid (format 1, `writeVLQ` giống frontend, velocity scale chuẩn `*127`, bỏ track không có note).
|
||||
- Trả `file_id` (lưu vào `app/storage/processed/`) + `download_url` → frontend tải qua `GET /api/v1/audio/download/{file_id}` (pattern đã có sẵn, chạy tốt trong WebView2 vì là network download http://127.0.0.1, không phải blob).
|
||||
- **B2 (fallback nếu B1 không khả thi):** giữ generation client-side nhưng đổi cơ chế tải: thử `a.click()` → nếu không có download, dùng Tauri dialog: `window.__TAURI__.dialog.save()` (capability `dialog:default` đã có) + ghi file qua... lưu ý **thiếu permission fs** — cần thêm `fs:default` vào `src-tauri/capabilities/default.json` nếu đi hướng này. → B1 đơn giản hơn, không đụng Rust/capabilities.
|
||||
- **Bước C — Cập nhật UI:** `triggerMidiExport()` gọi API mới, hiện spinner `setIsExporting`, toast kết quả; giữ nguyên menu/nút cũ.
|
||||
|
||||
### 1.4 Verify
|
||||
- Tạo project MIDI (2 track, nhiều note, velocity khác nhau) → Export MIDI → file .mid tải về được.
|
||||
- Mở file bằng DAW khác (hoặc script đọc bytes): số track đúng, tempo đúng, pitch/duration đúng, velocity nằm trong 1..127 (không còn giới hạn 100).
|
||||
- Export project rỗng → toast "Không có dữ liệu MIDI" (không crash).
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2 — FIX EXPORT MIX (xuất âm thanh) + TÙY CHỌN LOOP (không fade in/out)
|
||||
|
||||
### 2.1 Hiện trạng
|
||||
- Menu: File → Export Mix... (anchor ~29340) → `triggerWavExport()` (anchor ~26293).
|
||||
- `ExportModal` (anchor ~11881): option Nguồn (`project`/`track_mix`/`active_clip`/`clip_selection`), Định dạng (wav/mp3/ogg), SR, Bit, Kênh. **Chưa có option loop, chưa có option fade.**
|
||||
- 3 đường xuất âm:
|
||||
1. **Server-side** (`allOnServer && serverStatus === 'connected'`): POST `/api/v1/multitrack/mix` → task → poll → download file_id. Điều kiện: mọi track có `serverFileIdMap[t.id]` (đã upload file lên server) và `serverStatus === 'connected'`.
|
||||
2. **Client-side** `clientSideExport()` (anchor ~26734): `OfflineAudioContext` build lại full graph (track FX Rack + mastering chain) → encode WAV bằng tay (RIFF header + PCM). Chỉ hỗ trợ WAV; mp3/ogg bắt buộc server (`serverStatus === 'connected'`, anchor ~26849).
|
||||
3. **Bounce realtime** `triggerBounceExport()` (anchor ~26450): khi project có MIDI items mà MIDI cache chưa đủ → chạy lại project thật, capture PCM master bus → WAV.
|
||||
- Server path gửi `fade_in_ms: 0, fade_out_ms: 0` rồi nhưng `ClipConfig` backend mặc định 150/150 — kiểm tra worker có tôn trọng 0 không (xem `app/tasks/worker.py` `mix_multitrack_task`).
|
||||
|
||||
### 2.2 Nghi vấn root cause (chẩn đoán trước)
|
||||
1. Sau standalone, `serverStatus` thường = `checking...`/`offline` (không có server riêng) → nhánh server-side không chạy; nếu user chọn mp3/ogg → báo lỗi/không xuất. Kiểm tra `serverStatus` khi chạy app thật.
|
||||
2. Client-side path dùng `OfflineAudioContext` — trong WebView2 có thể fail nếu audio context chưa từng được unlock (autoplay policy) → export ra file rỗng/crash. Kiểm tra log.
|
||||
3. File tải về qua blob download — cùng vấn đề WebView2 như Phase 1.
|
||||
4. MIDI bounce: nếu master bus chưa init → toast "Hãy phát thử một lần" (đã có guard) — nhưng dễ bị kẹt ở trạng thái này khi user chưa Play.
|
||||
|
||||
### 2.3 Giải pháp
|
||||
- **Bước A — Chẩn đoán:** chạy app thật, tạo project (audio clip + 1 MIDI track), thử Export Mix cả 3 nguồn; ghi lại: toast nào hiện, file có tải về không, file có đúng độ dài/âm thanh không.
|
||||
- **Bước B — Sửa đường xuất âm cho standalone:**
|
||||
1. `clientSideExport`: bọc `OfflineAudioContext` bằng try/catch; trước khi render gọi `await ensureAudioUnlocked()` (unlock context bằng `resume()`/silent buffer nếu chưa unlock).
|
||||
2. Chuyển hết đường tải file sang pattern download qua server (giống Phase 1 B1): hoặc dùng helper `downloadBlobViaServer(url)`; đơn giản nhất: mọi export (client/bounce) lưu ra `app/storage/processed/` qua 1 endpoint upload hoặc tự backend sinh → trả file_id → `a.href = /api/v1/audio/download/{file_id}` (network download, hoạt động trong WebView2).
|
||||
3. mp3/ogg khi server offline: thêm fallback — nếu không server, chuyển tự động về WAV kèm toast thông báo (không im lặng fail).
|
||||
- **Bước C — Tùy chọn LOOP (yêu cầu chính):**
|
||||
1. Thêm state `exportSettings.loop` (boolean) + `exportSettings.loopBars` (số ô nhịp, default 4) vào `useState` (anchor ~17249).
|
||||
2. `ExportModal`: thêm checkbox **"Xuất loop (không fade in / không fade out)"** + input số ô nhịp (hoặc dùng vùng chọn hiện tại nếu có selection).
|
||||
3. Khi `loop = true`:
|
||||
- Nguồn buộc = vùng loop: `duration = loopBars * (60/bpm) * 4` giây (hoặc selection bounds), bắt đầu từ playhead/bar đầu selection.
|
||||
- **Mọi path đều zero-fade:** server path `fade_in_ms: 0, fade_out_ms: 0` (đã gửi, kiểm tra worker tôn trọng); client path `clientSideExport` không áp fade (clip buffer thô — xác nhận `buildOfflineTrackNode` không chèn fade); bounce path capture PCM không fade.
|
||||
- Không kéo đuôi: cắt chính xác tại `start + duration` (không để tail reverb/decay thừa — nếu muốn giữ decay thì là option riêng, ngoài phạm vi; note `ponytail:`).
|
||||
4. Đảm bảo file loop không bị "click" ở biên: zero-fade nghĩa là giữ nguyên dạng sóng; yêu cầu người dùng tự chọn loop point sạch (không tự động crossfade — đúng spec "không fade").
|
||||
- **Bước D — Cập nhật nút/UI:** giữ menu Export Mix..., thêm shortcut nếu tiện.
|
||||
|
||||
### 2.4 Verify
|
||||
- Export WAV loop 4 bars (project có audio + MIDI qua FX CHAIN): file đúng độ dài `4*4*60/bpm` giây (±1 sample), **mẫu đầu và mẫu cuối ≠ 0** nếu nội dung phát tại biên (chứng minh không fade in/out), loop nối vòng không click rõ rệt.
|
||||
- Export project không loop (bình thường) vẫn ra đúng như trước (không regression fade 150ms mặc định nếu có).
|
||||
- mp3/ogg khi server offline → tự về WAV + toast.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3 — ĐỔI MASTERING PANEL / FX RACK PANEL → FX CHAIN (modal dùng chung: plugin tự viết + VST FX)
|
||||
|
||||
### 3.1 Hiện trạng
|
||||
- 2 modal riêng biệt:
|
||||
- `FXRackModal` (anchor ~12160): cho 1 track. Header `{track.name} — FX RACK PANEL` (anchor ~12492). Chain built-in = `track.fxChain` (module tự viết: EQ/comp/limiter/reverb/delay..., meta `TRACK_FX_META` anchor ~1331); VST FX riêng = `track.vstFxChain` (danh sách từ `window.SonicAPI.listFx()` = `fx_vst_bridge --scan`, áp khi render/export).
|
||||
- `MasteringModal` (anchor ~12743): cho master. Header `MASTERING SUITE ... WEB MASTERING V10.5` (anchor ~13503). State `masteringSettings` (ozState): built-in mastering (EQ 4 băng/comp/limiter) + `ozState.vstFxChain`.
|
||||
- Nút mở:
|
||||
- Master: nút "MASTERING PANEL" trong `MasterStripConsole` (anchor ~2393), nút PWR cạnh master fader (~2526), menu File → "Mastering Suite" (anchor ~29348, Ctrl+Shift+M).
|
||||
- Track: nút `FX` trên TCP (anchor ~2740) → `window.__openFxRack(track.id, track.name)` (định nghĩa ~18065) → set `fxRackTarget` → render `FXRackModal` (anchor ~32645).
|
||||
|
||||
### 3.2 Mục tiêu (yêu cầu)
|
||||
1. Bỏ tên "MASTERING PANEL"/"FX RACK PANEL" → thống nhất **"FX CHAIN"**.
|
||||
2. Một modal chung chứa **cả plugin xử lí âm thanh tự viết (built-in DSP) lẫn VST FX** trong **một chain duy nhất**, dùng được cho **master** và cho **track**.
|
||||
3. Chain áp realtime khi Play và áp đúng khi Export (cả client offline lẫn bounce).
|
||||
|
||||
### 3.3 Giải pháp
|
||||
- **Bước A — Tách component chung:** tạo `FxChainModal({ target, onClose })` với `target = { kind: 'master' } | { kind: 'track', trackId }` (thay 2 component cũ, hoặc giữ wrapper cũ gọi chung — chọn cách ít đụng nhất: giữ `FXRackModal`/`MasteringModal` làm wrapper mỏng, body dùng chung component `FxChainBody`).
|
||||
- **Bước B — Hợp nhất chain trong UI:**
|
||||
- Hiển thị 1 hàng chain duy nhất: mỗi slot = `{ type: 'builtin' | 'vst', id, name, icon, bypass, preset_b64? }`.
|
||||
- Backing state giữ nguyên để không phá lưu project cũ: track → `track.fxChain` (builtin) + `track.vstFxChain` (vst); master → `masteringSettings` builtin + `masteringSettings.vstFxChain`. Chỉ hợp nhất ở tầng render + thao tác (thêm/xóa/kéo sắp xếp/bypass ghi vào đúng mảng gốc).
|
||||
- Picker thêm slot: 2 tab — "Module tự viết" (TRACK_FX_META + mastering built-ins) và "VST FX" (`listFx` → `vstFxChain`), chèn vào đúng vị trí kéo thả.
|
||||
- **Bước C — Đổi tên toàn bộ hiển thị:**
|
||||
- `FXRackModal` header → `{track.name} — FX CHAIN`.
|
||||
- `MasteringModal` header → `MASTER FX CHAIN` (bỏ "MASTERING SUITE"/"WEB MASTERING V10.5" hoặc chuyển thành subtitle nhỏ).
|
||||
- Nút `MasterStripConsole` → "FX CHAIN".
|
||||
- Menu File "Mastering Suite" → "Master FX Chain" (giữ Ctrl+Shift+M).
|
||||
- Cập nhật tooltip "nút A/♪" mô tả ("Mastering FX Chain" → "Master FX Chain").
|
||||
- **Bước D — Đảm bảo chain áp dụng đúng chỗ (không đổi logic DSP):**
|
||||
- Track: giữ graph hiện có — `buildOfflineTrackNode`/`__rebuildTrackFxGraph`/`fxActive` PWR (đừng đụng nếu đang chạy đúng).
|
||||
- Master: giữ `toggleMasteringOnMaster` + `NativeBridgeService.setFxChain` (anchor ~12799) cho VST master realtime.
|
||||
- Export: clientSideExport/bounce đã đi qua cả 2 chain — verify không regression.
|
||||
|
||||
### 3.4 Verify
|
||||
- Mở FX CHAIN master: thêm 1 module tự viết (EQ) + 1 VST FX → header "MASTER FX CHAIN", cả 2 hiện trong 1 chain, kéo thay đổi thứ tự, bypass từng slot, nghe thay đổi realtime, export ra đúng chain.
|
||||
- Mở project cũ (lưu trước phase): chain cũ vẫn hiện đúng (không mất cấu hình) — test migration state.
|
||||
- Không còn chuỗi "MASTERING PANEL"/"FX RACK PANEL" trong UI (grep app.jsx).
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4 — NÚT FX TRÊN TCP MỞ FX CHAIN CỦA TRACK
|
||||
|
||||
### 4.1 Hiện trạng
|
||||
- Nút `FX` trên TCP track đã gọi `window.__openFxRack(track.id, track.name)` (anchor ~2740) → `fxRackTarget` → `FXRackModal` (anchor ~32645). **Phần wiring có vẻ đã có** — cần xác nhận nó còn hoạt động sau Phase 3 refactor.
|
||||
|
||||
### 4.2 Giải pháp
|
||||
- **Bước A — Kiểm tra hiện trạng:** mở app thật, chọn track → bấm nút FX → modal có mở không? Chain có hiện đúng `track.fxChain` + `track.vstFxChain` không?
|
||||
- **Bước B — Fix nếu hỏng:** các nguyên nhân có thể: `tracks.find(t => t.id === fxRackTarget.trackId)` trả null (id mismatch giữa `tracks` và `activeTracks`/sessionTabs), hoặc `FXRackModal` trả `null` sớm khi `track` undefined. Sửa lookup: dùng `allTracks` gồm main + session tabs (`(activeTracksRef.current || tracks)` như triggerWavExport làm).
|
||||
- **Bước C — Sau Phase 3:** nút FX mở `FxChainModal` với `target = { kind: 'track', trackId }` → header "FX CHAIN" hiển thị chain track.
|
||||
|
||||
### 4.3 Verify
|
||||
- Bấm nút FX trên track A → modal FX CHAIN của track A (đúng tên, đúng chain); bấm trên track B → chain track B; đóng/mở nhiều lần không lỗi; VST GUI embed (nếu có) vẫn mở được từ chain.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5 — VIRTUAL MIDI KEYBOARD (spec `virtual_keyboard_and_chords_panel_spec.md` Part I)
|
||||
|
||||
### 5.1 Nền tảng có sẵn
|
||||
- `app/static/js/services/unifiedMidiRouter.js` đã có: `dispatchMidiEvent({ command: 'NOTE_ON'|'NOTE_OFF', channel, pitch, velocity, sourceType })`, `allocateChannel(trackId, isPercussion)` (ch 0-8,10-14 melodic / 9 percussion), `bridgeConnected` → NativeBridgeService hoặc `onFallback` (SF/WASM). → Tận dụng, không viết router mới.
|
||||
- Audio preview: nếu bridge active → `NativeBridgeService.dispatchMidiEvent` (âm thật VSTi/SF2 đã load trên channel); nếu không → fallback hiện có (SonicSF/Carla) — giữ nguyên cơ chế.
|
||||
- Recording: transport Record + track armed đã có; cần hook NOTE_ON/NOTE_OFF vào recorder tạo note trong `midiItems`.
|
||||
|
||||
### 5.2 Giải pháp (thêm vào `app.jsx` — nhất quán monolith; code thuần logic tách service nếu >100 dòng)
|
||||
- **Bước A — Component `VirtualMidiKeyboard`:**
|
||||
- Floating window draggable, luôn trên timeline (`z-index` cao, không nuốt phím tắt transport Space/R).
|
||||
- 2 octave keybed hiển thị mapping bảng spec §3: `Z S X D C V G B N M J` (root octave) + `Q 2 W 3 E R 5 T 6 Y 7 U I` (+1 oct), `O` (+2 C). Vẽ key trắng/đen theo Key Type.
|
||||
- Control bar: Oct -/+, Octave Shift hiển thị (default C4), Velocity slider (1..127, default 100), Channel select (1..16, mặc định channel của track đang chọn/arm), Transpose, Scale Highlight (dropdown scale — phục vụ highlight, không bắt buộc chặn phím).
|
||||
- Toggle: phím **F2** + menu **Tools → Virtual MIDI Keyboard**. Không chặn khi focus trong `<input>/<textarea>`.
|
||||
- Pitch formula đúng spec: `Pitch = (Octave + 1) * 12 + PitchOffset` với root = C.
|
||||
- KeyDown: bỏ `e.repeat`; highlight key; `dispatchMidiEvent NOTE_ON` (velocity 0..1 = slider/127); KeyUp: NOTE_OFF.
|
||||
- **Live record:** nếu transport đang Record và track đang arm → tạo note `{ pitch, start_beat, duration_beats, velocity }` vào `item.source_data.notes`/`midiItems[].notes` của item MIDI đang mở (hoặc item active) → dispatch `DAW_STATE_UPDATED` để canvas redraw (pattern đã có trong app).
|
||||
- Persist `localStorage`: `{ visible, x, y, octave, velocity, channel, transpose, scale }`; khôi phục khi mở.
|
||||
- **Bước B — State + menu:**
|
||||
- `const [vkbOpen, setVkbOpen] = useState(false)` + ref lưu rect.
|
||||
- Global keydown listener cho F2 (tránh trùng transport), menu Tools thêm item.
|
||||
- **Bước C — Âm thanh:** không code engine mới — chỉ gọi router. Nếu track chưa có instrument: giữ hành vi fallback hiện có (SF mặc định/Carla), kèm toast khi track chưa arm/chưa có instrument.
|
||||
|
||||
### 5.3 Verify (spec §IV checklist 1-3)
|
||||
- F2 mở/đóng; kéo thả được; vị trí nhớ sau reload.
|
||||
- Bấm Z/S/X/D/C/V/G/B/N/M/J/Q/2/W/3/E/R/5/T/6/Y/7/U/I → key sáng, âm phát (bridge VSTi nếu có, không thì SF fallback); octave +/- đổi cao độ đúng 12 semitone.
|
||||
- Record: arm track → R → bấm phím → note hiện realtime trên timeline/piano roll → Stop → MIDI item hoàn chỉnh.
|
||||
- Bấm phím trong ô text input → không trigger note (pass-through).
|
||||
|
||||
---
|
||||
|
||||
## PHASE 6 — CHORDS PANEL (spec Part II)
|
||||
|
||||
### 6.1 Giải pháp
|
||||
- **Bước A — Component `ChordsPanel`** (modal centered / dock phải):
|
||||
- Trigger: menu **Insert → Chords panel**, phím **Shift+K**, nút 🎼 trên piano roll toolbar.
|
||||
- 3 tab: **Preset Library | Custom Builder | Internet/AI Search** (AI tab defer — xem 6.3).
|
||||
- Filter: Style, Key, Scale, Search box.
|
||||
- **Bước B — Preset Library** (bảng spec §III.2, hardcode 6 styles):
|
||||
- Pop/Ballad `I-V-vi-IV`, Jazz/Neo-Soul `ii7-V7-Imaj7-VI7`, Cinematic/Epic `i-VI-III-VII`, EDM `vi-IV-I-V`, Lofi `i7-iv7-v7-i7`, R&B `Imaj7-iii7-IVmaj7-V13`.
|
||||
- Mỗi preset: nút [Preview] (gửi note tạm qua router NOTE_ON/NOTE_OFF nhanh), [Insert to Timeline], [Favorite ★] (localStorage).
|
||||
- **Bước C — Custom Builder:**
|
||||
- Input: tên, genre tag, chord tokens (roman numeral `I-vi-IV-V` hoặc chord name `Cmaj7-Am9-Fadd9-G13`), rhythm pattern (whole/quarter/arpeggio/strum).
|
||||
- Lưu `localStorage['daw_user_custom_chords']`; dispatch `CUSTOM_CHORD_SAVED` để panel tự refresh (spec §III.3).
|
||||
- **Bước D — Chord insertion engine (spec §III.5):**
|
||||
- Parse chord symbol → pitch array (ví dụ `Cmaj7` = [60,64,67,71]; áp voicing: root/first inversion/drop-2).
|
||||
- Insert tại playhead (hoặc beat click): `StartBeat(Ck) = Beat_insert + Σ duration trước`; tạo `MIDINote { pitch, start_beat, duration_beats, velocity: 0.8 }` vào MIDI item active; dispatch `DAW_STATE_UPDATED`.
|
||||
- Code parse thuần hàm → để service riêng `app/static/js/services/chordTheory.js` (test được, xem §6.4).
|
||||
- **Bước E — Internet/AI Search:**
|
||||
- Tận dụng `app/static/js/services/aiGateway.js` (đã có tool-calling infra). Thêm tool `search_or_generate_chords` + endpoint backend `/api/v1/ai/search-chords` (FastAPI, gọi model qua aiGateway/AI_PROXY hiện có).
|
||||
- Kết quả hiển thị progression + [Listen Preview] [Save] [Insert] — spec §III.4.
|
||||
- **Defer nếu AI backend chưa cấu hình key:** tab hiện thông báo "AI chưa cấu hình" thay vì crash — xem rủi ro.
|
||||
|
||||
### 6.2 Verify (spec §IV checklist 4-7)
|
||||
- Shift+K / Insert menu mở panel; chọn Jazz Neo-Soul → Insert → `Dm7-G7-Cmaj7-A7` chèn đúng vị trí playhead, đúng duration.
|
||||
- Custom builder lưu → xuất hiện lại sau reload; event CUSTOM_CHORD_SAVED refresh không cần reload trang.
|
||||
- AI search (nếu cấu hình): query "Hotel California" → progression Bm-F#7-A-E7-G-D-Em-F#7.
|
||||
|
||||
### 6.3 Defer (`ponytail:`)
|
||||
- Scale Highlight chặn phím ngoài scale (chỉ highlight, không chặn) — thêm khi có yêu cầu.
|
||||
- Arpeggiation/strum preview phức tạp trong preset (chỉ sustain preview) — thêm khi cần.
|
||||
- AI chord search phụ thuộc key model — tách phase riêng nếu chưa có key.
|
||||
|
||||
### 6.4 Test đơn vị (non-trivial logic)
|
||||
- `chordTheory.js`: 1 file test nhỏ (Node assert, không framework) — parse `Cmaj7`, `Am9`, `Dm7`, `G13`, `I-vi-IV-V` với key C, voicing root/inversion/drop-2 ra đúng pitch array. Chạy: `node app/static/js/services/chordTheory.test.js`.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 7 — BUILD TỔNG & VERIFICATION CHECKLIST
|
||||
|
||||
```bash
|
||||
node build.mjs
|
||||
python -m PyInstaller engine.spec --clean --noconfirm
|
||||
cp -r dist/daw_engine/* src-tauri/target/release/daw_engine/
|
||||
```
|
||||
|
||||
| # | Test | Kỳ vọng |
|
||||
|---|------|---------|
|
||||
| 1 | Export MIDI (project 2 track MIDI) | File .mid tải về, mở đúng, velocity 1..127 |
|
||||
| 2 | Export Mix WAV loop 4 bars | Đúng độ dài, không fade ở biên, loop nối mượt |
|
||||
| 3 | Export Mix WAV bình thường | Không regression, có FX CHAIN + master |
|
||||
| 4 | Export mp3/ogg không server | Fallback WAV + toast |
|
||||
| 5 | Master FX CHAIN | Header mới, builtin + VST 1 chain, realtime + export đúng |
|
||||
| 6 | Track FX CHAIN qua nút FX TCP | Mở đúng track, chain đủ builtin + VST |
|
||||
| 7 | Project cũ sau refactor | Chain cũ hiện đúng, không mất cấu hình |
|
||||
| 8 | F2 Virtual MIDI Keyboard | Mở/đóng, bấm phím ra âm, record vào timeline |
|
||||
| 9 | Chords panel | Insert progression đúng vị trí, custom lưu được |
|
||||
| 10 | grep UI | Không còn "MASTERING PANEL"/"FX RACK PANEL" |
|
||||
|
||||
Commit theo từng phase (message tiếng Việt, kiểu `fix(export): ...`), `git status` sạch cuối mỗi phase.
|
||||
|
||||
---
|
||||
|
||||
## RỦI RO / LƯU Ý
|
||||
- **WebView2 blob download** là nghi vấn chính cho cả Phase 1 & 2 — xác nhận bằng chẩn đoán trước khi chọn hướng fix (server-download là hướng an toàn nhất vì đã có pattern `/api/v1/audio/download/{file_id}`).
|
||||
- **Rebuild engine bắt buộc** sau mọi đổi frontend — quên copy `dist/daw_engine/*` là test nhầm bản cũ (đã vấp ở phase trước).
|
||||
- **Auth API**: endpoint export mới nên dùng `get_optional_user` (không bắt buộc token) cho đồng bộ với `/mix` hiện tại, nhưng nếu UI đã có token thì gửi kèm `Authorization: Bearer` (an toàn).
|
||||
- **Không đụng logic DSP** khi refactor FX CHAIN (chỉ đổi tên/hợp nhất render) — giảm rủi ro regression âm thanh.
|
||||
- **state cũ project**: `fxChain`/`vstFxChain`/`masteringSettings.vstFxChain` giữ nguyên schema — không migration dữ liệu.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Build Layout — vị trí chuẩn (ổn định qua build & đổi code)
|
||||
|
||||
## Canonical outputs (đều gitignored, tái tạo từ build)
|
||||
|
||||
| Vị trí | Nội dung | Nguồn |
|
||||
|---|---|---|
|
||||
| `src-tauri/binaries/` | Sidecars + DLL runtime | `build/scripts/build_native_bridge.ps1` |
|
||||
| `src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe` | Instrument bridge (VSTi + SF2/SF3/SFZ, SHM realtime, control 1-6) | CMake `daw_vst_bridge` |
|
||||
| `src-tauri/binaries/fx_vst_bridge-x86_64-pc-windows-msvc.exe` | FX bridge (VST3 FX: `--render-fx`, `--scan`, `--open-fx-gui`, `--fx-gui`) | CMake `fx_vst_bridge` |
|
||||
| `src-tauri/binaries/libfluidsynth-3.dll`, `jpeg62.dll` | Runtime DLL (fluidsynth, libjpeg-turbo) | vcpkg manifest install |
|
||||
| `src-tauri/resources/daw_engine/` | Python engine đã đóng băng (PyInstaller) | `build/scripts/build_windows.ps1` → `dist/daw_engine` |
|
||||
|
||||
## External (shared, không nằm trong repo)
|
||||
|
||||
| Vị trí | Nội dung |
|
||||
|---|---|
|
||||
| `%USERPROFILE%\vcpkg` | vcpkg.exe + package cache (fluidsynth, libjpeg-turbo, pkgconf) |
|
||||
| `%USERPROFILE%\vcpkg\packages\libjpeg-turbo_x64-windows\bin\jpeg62.dll` | Fallback nguồn jpeg62.dll khi manifest chưa tái tạo |
|
||||
|
||||
## Dev-time resolution (Python)
|
||||
|
||||
`app/core/native_render.py`:
|
||||
- `find_bridge_exe()` — `SF_BRIDGE_PATH` → frozen (`_MEIPASS`/`exe_dir`) → `src-tauri/binaries/<sidecar>` → `install/` (legacy). Instrument bridge.
|
||||
- `find_fx_bridge_exe()` — `SF_FX_BRIDGE_PATH` → cùng chuỗi. FX bridge.
|
||||
|
||||
Tauri sidecar rename: `binaries/daw_vst_bridge` (externalBin) → `daw_vst_bridge-x86_64-pc-windows-msvc.exe` — cả hai dạng đều được tìm.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
# Native bridges (C++): vcpkg manifest + sfizz + CMake → src-tauri/binaries
|
||||
build/scripts/build_native_bridge.ps1
|
||||
|
||||
# Full desktop (engine PyInstaller + bridges + Tauri NSIS/MSI)
|
||||
build/scripts/build_windows.ps1
|
||||
```
|
||||
|
||||
`build/scripts/` là tracked (dù `build/` gitignored) — không xoá `build/` wholesale.
|
||||
|
||||
## Split bridges
|
||||
|
||||
- `daw_vst_bridge` — VSTi instruments, VST2 (.dll) + VST3 (.vst3), native GUI qua SHM control 4 (`OPEN_GUI`), SF2/SF3/SFZ (fluidsynth/sfizz), realtime FX chain in-process (control 6 `SET_FX_CHAIN`).
|
||||
- `fx_vst_bridge` — VST3 FX only. One-shot: `--scan <dir>`, `--render-fx <job> --in --out`, `--open-fx-gui <job>` (cửa sổ rời), `--fx-gui <job>` (embed + HTTP `SF_FXGUI_PORT`). Không SHM, không fluidsynth/sfizz. VST2 FX không hỗ trợ (VST2 không introspect được — xem là instrument; ponytail: thêm AEffect hosting khi có nhu cầu).
|
||||
@@ -0,0 +1,132 @@
|
||||
# Thiết kế kiến trúc: Docker play âm VSTi ở client (giữ nguyên luồng cũ)
|
||||
|
||||
Ngày: 2026-08-15
|
||||
Trạng thái: THIẾT KẾ (chưa sửa code sản phẩm). Linux làm thiết kế + spike khả
|
||||
thi; code theo thoả thuận triển khai.
|
||||
|
||||
## 1. Mục tiêu
|
||||
Docker (server headless + browser UI) có thể play âm VSTi ở client,
|
||||
DỰA TRÊN kiến trúc cũ của docker: client tự phát âm (FluidSynthWASM / SonicSF),
|
||||
không streaming realtime từ server. VSTi không chạy được trong browser →
|
||||
chuyển thành asset SoundFont (SF2/SF3) render sẵn từ VSTi trên server, client
|
||||
dùng nguyên luồng cũ.
|
||||
|
||||
## 2. Kiến trúc docker hiện tại (giữ nguyên)
|
||||
- Server: FastAPI (`app/main.py`) + Celery worker; mount `/opt/daw_engine/vst3`,
|
||||
`/opt/daw_engine/soundfonts`, `/opt/daw_engine/samples/pianobook`.
|
||||
- `runtime.detect()`: docker → `environment=docker`; client dùng
|
||||
FluidSynthWASM (`window.SonicSF`) cho mọi track soundfont.
|
||||
- Play track: `scheduleMidiNoteDispatch` → (bridge không active) →
|
||||
`window.SonicSF.playNote(...)` — phát SF2 đã load vào WASM.
|
||||
- Preview VSTi hiện có: `preview_mode=quick_render` nếu `pedalboard` có —
|
||||
server render clip ngắn ra WAV → client play `Audio`. KHÔNG phải realtime,
|
||||
KHÔNG phải theo timeline.
|
||||
- SoundFontConverter: SF2→SF3 (ogg) / SF3→SF2. LƯU Ý: client WASM KHÔNG decode
|
||||
ogg → client cần SF2; SF3 chỉ dùng server-side (fluidsynth native).
|
||||
|
||||
## 3. Vấn đề
|
||||
Track gán VSTi trong docker: `isVstTrackEngine=true`, `routeCarla=false`
|
||||
(không có Carla), `nativeSf=false` → rơi vào `SonicSF.playNote` với preset
|
||||
soundfont không tồn tại → câm (hoặc âm mặc định sai).
|
||||
|
||||
## 4. Thiết kế đề xuất: VSTi → SF2/SF3 asset → client luồng cũ
|
||||
|
||||
### Nguyên lý
|
||||
Biến 1 nhạc cụ VSTi (VST3 + preset) thành 1 bộ samples render sẵn (per-note,
|
||||
velocity layer), đóng gói SF2 (client) + SF3 (server native), đăng ký như
|
||||
soundfont bình thường → client chọn instrument như soundfont → mọi luồng play
|
||||
cũ (scheduleMidiNoteDispatch → SonicSF.playNote) hoạt động không đổi.
|
||||
|
||||
### Pipeline (server, mới)
|
||||
1. **Registry instrument** (storage/instruments.json): `{instrument_id, vst_path,
|
||||
preset_id|preset_bytes(.vstpreset), bank, program, render_params}`.
|
||||
Preset nguồn: upload .vstpreset (VST3 state — pedalboard đọc được) hoặc
|
||||
dùng state mặc định của plugin. (.dspreset DecentSampler: spike thất bại —
|
||||
xem mục 6.)
|
||||
2. **Note renderer** (Celery task, chạy 1 lần / cache):
|
||||
- Với mỗi note 0..127 (hoặc dải cấu hình) × velocity layer (1-3):
|
||||
`VST3Plugin.process(midi, duration, sr)` → PCM mono/stereo.
|
||||
- Dải nốt: mặc định render root note mỗi 3 bán âm (41 sample/nhạc cụ), các
|
||||
nốt còn lại pitch-shift bằng FluidSynth (SF2 root + correction) — giảm
|
||||
dung lượng 4x; chất lượng chấp nhận được cho preview.
|
||||
- Velocity layers: 1 (ff) hoặc 2 (mf+ff) theo cấu hình.
|
||||
- Duration: 2-4s có fade; loop: nếu plugin phát sustain được (note on dài),
|
||||
lấy loop region từ phần sustain ổn định; nếu không → one-shot + release.
|
||||
- Percussion (bank 128): map nốt → chính nó, không pitch-shift.
|
||||
3. **SF2 builder** (component MỚI phải viết): WAV samples → SF2 binary
|
||||
(RIFF/sfbk: sdta+smpl, pdta: phdr/pbag/pgen/inst/ibag/igen/shdr).
|
||||
Sau đó tái dùng `SoundFontConverter.convert_sf2_to_sf3` để có SF3 cho
|
||||
server-side (tùy chọn).
|
||||
4. **Lưu + đăng ký**: `storage/soundfonts/vsti_<id>.sf2` (+ .sf3); thêm vào
|
||||
scan soundfont để API `listPlugins`/`listSoundfontInstruments` trả về như
|
||||
soundfont thường (thêm cờ `origin: "vsti"`).
|
||||
5. **Client** (đổi TỐI THIỂU): instrument picker hiển thị nhạc cụ VSTi như
|
||||
soundfont; chọn → track `synth_engine = {type:"soundfont", soundfont_id,
|
||||
bank, program}` → MỌI luồng play cũ chạy nguyên vẹn (SonicSF.playNote).
|
||||
|
||||
### Cache & vòng đời
|
||||
- Key: `sha256(vst_path + preset_bytes + render_params)` → build 1 lần, tái
|
||||
dùng. API `POST /api/v1/instruments/{id}/build` + `GET .../status` (queued/
|
||||
building/ready/failed).
|
||||
- Xoá instrument → xoá asset kèm.
|
||||
|
||||
### API mới (server)
|
||||
- `GET /api/v1/instruments` — danh sách (kèm trạng thái build).
|
||||
- `POST /api/v1/instruments` — tạo (vst_path, preset upload, params).
|
||||
- `POST /api/v1/instruments/{id}/build`, `GET /api/v1/instruments/{id}/status`.
|
||||
- `DELETE /api/v1/instruments/{id}`.
|
||||
- Không đổi API phát lại hiện tại.
|
||||
|
||||
## 5. Ước lượng & giới hạn
|
||||
- 41 sample × 1 velocity × 3s × 44.1k stereo PCM ≈ 43 MB → ogg (SF3) ≈ 8-12 MB;
|
||||
client SF2 giữ PCM (WASM không decode ogg) ≈ 40 MB — lớn. Giảm: mono (~20MB),
|
||||
giảm dải nốt (mỗi 4-6 bán âm), velocity 1, duration 2s. Chấp nhận tradeoff
|
||||
preview vs dung lượng; tham số hoá trong render_params.
|
||||
- Build tốn CPU (41 note × 2-4s render, 1 nhạc cụ ~2-5 phút trên 1 core) →
|
||||
chạy Celery worker, cache vĩnh viễn.
|
||||
- Chất lượng: pitch-shift bằng FluidSynth không bằng plugin thật; âm VSTi
|
||||
phức tạp (modulation, chorus) mất chi tiết khi đóng gói sample. Đây là giới
|
||||
hạn của hướng "giữ luồng cũ client-side".
|
||||
|
||||
## 6. Kết quả spike (đã chạy trong container sonicforgestudio-web-1)
|
||||
- `pedalboard 0.9.19` có sẵn ✓; `DISPLAY=:99` ✓.
|
||||
- `VST3Plugin.load` OK: DecentSampler.vst3; instrument API
|
||||
`plugin.process(midi, duration, sr)` chạy (trả shape 2×88200) ✓.
|
||||
- SCAN FAIL: Vital.vst3, Surge_XT.vst3 — "unsupported plugin format or scan
|
||||
failure" (JUCE không scan được trong container; nghi thiếu thư viện của
|
||||
plugin bundle — cần khảo sát tiếp: ldd .so bên trong .vst3, thử cài libs).
|
||||
- PRESET FAIL: mọi .dspreset pianobook load thất bại trong container:
|
||||
- "failed to load data from preset file" (1 preset/process)
|
||||
- "database is locked" (nhiều preset/process — DecentSampler dùng SQLite nội
|
||||
bộ; có thể cần HOME writable + 1 instance/process + chdir preset dir —
|
||||
chdir đã thử).
|
||||
- App có `DecentSamplerManager.create_decent_sampler_instance` (chdir preset
|
||||
dir) — chưa verify được trên Linux container; có thể chỉ chạy trên Windows.
|
||||
- Chưa test được .vstpreset (VST3 state) vì không có VST scan OK để áp.
|
||||
|
||||
### Việc cần làm trước khi chốt code
|
||||
1. Điều tra scan fail Vital/Surge: `ldd` .so trong bundle, thử cài
|
||||
libvulkan/libgl extra; tìm 1 VST3 đơn giản scan được (ví dụ plugin test
|
||||
hoặc VST3 SDK example) để có plugin "chuẩn" cho pipeline.
|
||||
2. Verify .vstpreset flow: tạo preset thật từ Windows (Carla) → upload →
|
||||
pedalboard container render thử.
|
||||
3. Điều tra DecentSampler preset: chạy thử với HOME=writable, 1 instance,
|
||||
chdir; nếu vẫn fail → loại .dspreset khỏi phạm vi v1, dùng .vstpreset.
|
||||
4. Viết SF2 builder + unit test đọc lại bằng pyfluidsynth (pattern
|
||||
`_sf3_plays_audio`).
|
||||
|
||||
## 7. Kế hoạch triển khai (sau khi spike ổn)
|
||||
1. Spike mở rộng (mục 6) — chọn plugin/preset khả thi.
|
||||
2. Server: SF2 builder + note renderer + registry + API + Celery task.
|
||||
3. Server: đăng ký asset vào scan soundfont.
|
||||
4. Client: instrument picker hiển thị nhạc cụ VSTi (origin=vsti); chọn →
|
||||
synth_engine soundfont. KHÔNG đổi luồng play.
|
||||
5. Test: chọn nhạc cụ VSTi → play timeline → nghe đúng âm; xác nhận luồng cũ
|
||||
(WASM) không đổi cho soundfont thường.
|
||||
|
||||
## 8. Ranh giới
|
||||
- Docker/standalone tách riêng: pipeline server chỉ bật khi
|
||||
`runtime.environment=docker` (hoặc mọi server headless); standalone Windows
|
||||
giữ Carla/bridge như cũ.
|
||||
- Thiết kế này không sửa code sản phẩm; commit code theo thoả thuận sau khi
|
||||
spike + chốt phương án.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Thiết kế: tương thích ngược VSTi cũ (VST2, 32-bit) — bridge âm thanh
|
||||
|
||||
Ngày: 2026-08-15
|
||||
Trạng thái: THIẾT KẾ (chưa sửa code sản phẩm). Linux làm thiết kế + spike khả
|
||||
thi; code theo thoả thuận triển khai (Windows).
|
||||
|
||||
## 1. Mục tiêu
|
||||
Standalone bridge (`standalone-shm-bridge`) hiện chỉ host VST3. Nhiều VSTi cũ
|
||||
(vintage, freeware, 32-bit) chỉ có bản VST2. Mục tiêu: VST2 64-bit phát được qua
|
||||
bridge chính (headless, realtime), không phá luồng VST3 hiện tại.
|
||||
|
||||
## 2. Hiện trạng (đã xác minh trên code)
|
||||
|
||||
### VST2 không được implement trong bridge
|
||||
- `native_bridge/src/NativeInstrumentEngine.cpp:166-180` — `create_instrument()`:
|
||||
`case VST2: return nullptr` + comment
|
||||
"ponytail: VST2 host (VST2.4 SDK, Steinberg discontinued) not implemented".
|
||||
- Enum đã có sẵn: `native_bridge/src/INativeInstrument.h:8-12`
|
||||
(VST3=0, VST2=1, SF2/SF3=2, SFZ=3).
|
||||
- `StateStore.h:14` — type 1 = VST2.
|
||||
|
||||
### Backend đã nhận diện VST2 (scan + UI)
|
||||
- `app/api/v1/plugins.py:117-130,224-247` — file `.dll` (không phải folder
|
||||
`.vst3`) → `"type":"VST2"`.
|
||||
- `app/vst_engine.py:282-300` — scan `.vst3/.so/.dll`.
|
||||
- JS mapping: `nativeBridgeService.js:7` `INSTRUMENT_TYPE={VST3:0,VST2:1,SF2:2,SF3:2,SFZ:3}`.
|
||||
|
||||
→ Hệ quả: UI liệt kê plugin VST2 là loại hợp lệ, user assign → bridge
|
||||
`create_instrument` trả nullptr → `assign` false → fail **im lặng**, không có
|
||||
lỗi nào hiển thị. Đây là bug UX chính cần sửa kèm.
|
||||
|
||||
### Đường khác không thay thế
|
||||
- Carla (Windows) hỗ trợ VST2 nhưng là đường GUI/preset riêng
|
||||
(`plugins.py:530-630` carla-midi/carla-play-notes/open-in-carla), KHÔNG phải
|
||||
đường audio bridge chính.
|
||||
- Preset `.fxp/.fxb` nằm trong `PRESET_EXTENSIONS` (`vst_engine.py:592`) nhưng
|
||||
`apply_preset_to_plugin` gọi `load_preset` của VST3Plugin — không chạy VST2.
|
||||
|
||||
### Hạ tầng tái dùng được
|
||||
- `SandboxVst3Host` + `SandboxHostIPC` double-buffer
|
||||
(`SharedMemoryIPC.h:33-39`), `plugin_host_main.cpp`, env `SF_SANDBOX_VST3` —
|
||||
pattern sẵn cho helper process 32-bit.
|
||||
- Build: `native_bridge/vcpkg.json` chỉ fluidsynth+pkgconf; VST3 SDK là git
|
||||
submodule `vst3sdk` (không có vestige — cần thêm header nếu làm VST2 host);
|
||||
CMake `-EHa`.
|
||||
|
||||
## 3. Thiết kế đề xuất
|
||||
|
||||
### 3.1. VST2 64-bit — host `Vst2Instrument` dùng vestige (làm trước)
|
||||
- **vestige** (`vestige/aeffect.h`, mã nguồn mở, từ dự án LMMS): header-only,
|
||||
tự định nghĩa lại interface + ABI VST2.4 (vtable layout, dispatcher opcode).
|
||||
Không cần Steinberg VST2 SDK (đã discontinued, license không cấp mới) →
|
||||
sạch pháp lý, zero dependency build, không submodule mới.
|
||||
- `Vst2Instrument` implement cùng interface `INativeInstrument`:
|
||||
assign / SHM double-buffer / renderAll tái dùng nguyên trạng.
|
||||
- Phạm vi audio headless: `processReplacing`, `setSampleRate`, `setBlockSize`,
|
||||
`resume`/`suspend`, `getParameter`/`setParameter`, `effGetChunk`/`effSetChunk`
|
||||
(preset .fxp/.fxb), `effGetParamLabel` (tên tham số cho UI).
|
||||
- GUI editor: **defer** (không window, không thread GUI). Nhiều VSTi cổ vẫn
|
||||
phát headless; plugin nào đòi GUI mới ra tiếng thì ghi rõ giới hạn ở UI.
|
||||
- Sửa kèm bug im lặng: khi `create_instrument` trả nullptr → trả lỗi rõ ràng
|
||||
lên UI thay vì fail câm.
|
||||
|
||||
### 3.2. VST2 32-bit — helper process (defer, effort cao)
|
||||
- Helper `plugin_host.exe` 32-bit, tái dùng pattern `SandboxVst3Host` +
|
||||
`SandboxHostIPC` double-buffer + env `SF_SANDBOX_VST3`.
|
||||
- Lý do defer: cần bản build 32-bit riêng, xử lý wow64 + marshalling
|
||||
(MIDI/sample rate/block size qua IPC), khó debug. Làm sau khi 64-bit ổn định.
|
||||
|
||||
### 3.3. Quirk plugin cũ (khi test thực tế)
|
||||
- Block size: thử 256 / 512 / 1024 (nhiều plugin cũ chỉ nhận bội số cố định).
|
||||
- Sample rate: 44.1 kHz là mặc định an toàn nhất.
|
||||
- Thread affinity: open + process cùng 1 thread; tránh đổi thread giữa
|
||||
`resume` và vòng render.
|
||||
- Plugin 32-bit đòi GUI: một số chỉ phát sau khi editor từng mở — ghi rõ
|
||||
giới hạn, không cố host GUI trong bridge.
|
||||
|
||||
### 3.4. Giới hạn docker/server
|
||||
- `pedalboard` không hỗ trợ VST2 → pipeline VSTi→SF2 (`DESIGN_DOCKER_VSTI_PLAY.md`)
|
||||
chỉ nhận VST3. VST2 không render được asset SF2 trên server. Ghi rõ ở UI.
|
||||
|
||||
## 4. Kiến trúc thêm mới (tối thiểu)
|
||||
```
|
||||
native_bridge/
|
||||
vestige/aeffect.h # header mã nguồn mở (thêm file)
|
||||
src/Vst2Instrument.h/.cpp # host VST2, implement INativeInstrument
|
||||
src/NativeInstrumentEngine.cpp # case VST2 -> new Vst2Instrument
|
||||
```
|
||||
Không đổi ABI, không đổi `SharedMemoryIPC`, không đổi JS service layer.
|
||||
|
||||
## 5. Rủi ro / giới hạn
|
||||
- Vài plugin rất cổ đòi host callback lạ (`effGetPlugCategory`, vendor opcode)
|
||||
→ patch nhỏ riêng từng plugin.
|
||||
- Vestige không cung cấp editor GUI → plugin phụ thuộc GUI sẽ câm.
|
||||
- ABI VST2 ổn định từ 1999 → 64-bit .dll hiện đại + cổ đều theo, rủi ro thấp.
|
||||
|
||||
## 6. Thứ tự triển khai
|
||||
1. `Vst2Instrument` + vestige: load, assign, render thử 1 plugin VST2 64-bit.
|
||||
2. Sửa bug fail im lặng khi không host được.
|
||||
3. Preset .fxp/.fxb qua `effSetChunk`/param.
|
||||
4. Test block size / sample rate / thread affinity với plugin cổ.
|
||||
5. (Defer) 32-bit helper process.
|
||||
+27
@@ -37,6 +37,33 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# ── Build native_bridge (Linux) — thay pedalboard GPL-3.0 ──────────────────
|
||||
# Xem PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md §4. sfizz khong co trong apt
|
||||
# Debian → build tu nguon vendored (native_bridge/sfizz-src, co trong build
|
||||
# context) hoac git clone fallback khi thieu. Bridge chay --render duoi
|
||||
# Xvfb :99 (DISPLAY da set o tren) — khong can GUI plugin editor.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
cmake pkg-config libfluidsynth-dev libfreetype-dev git \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& if [ -d native_bridge/sfizz-src ]; then cp -r native_bridge/sfizz-src /tmp/sfizz; \
|
||||
else git clone --recursive --depth 1 https://github.com/sfztools/sfizz.git /tmp/sfizz; fi \
|
||||
&& cmake -S /tmp/sfizz -B /tmp/sfizz/build -DCMAKE_BUILD_TYPE=Release \
|
||||
-DSFIZZ_SHARED=ON -DSFIZZ_JACK=OFF -DSFIZZ_RENDER=OFF -DSFIZZ_TESTS=OFF \
|
||||
-DSFIZZ_BENCHMARKS=OFF -DSFIZZ_DEMOS=OFF -DSFIZZ_DEVTOOLS=OFF \
|
||||
-DSFIZZ_GIT_SUBMODULE_CHECK=OFF -DBUILD_TESTING=OFF \
|
||||
&& cmake --build /tmp/sfizz/build --target sfizz_shared -j"$(nproc)" \
|
||||
&& cmake --install /tmp/sfizz/build \
|
||||
&& sed -i 's/-llibsfizz/-lsfizz/' /usr/local/lib/pkgconfig/sfizz.pc \
|
||||
&& cmake -S native_bridge -B /tmp/bridge-build -DCMAKE_BUILD_TYPE=Release \
|
||||
-DSMTG_ENABLE_VST3_PLUGIN_EXAMPLES=OFF \
|
||||
-DSMTG_ENABLE_VST3_HOSTING_EXAMPLES=OFF \
|
||||
-DSMTG_ENABLE_VSTGUI_SUPPORT=OFF \
|
||||
&& cmake --build /tmp/bridge-build --target daw_vst_bridge -j"$(nproc)" \
|
||||
&& mkdir -p /app/install \
|
||||
&& cp /tmp/bridge-build/daw_vst_bridge /app/install/ \
|
||||
&& rm -rf /tmp/sfizz /tmp/sfizz/build /tmp/bridge-build
|
||||
|
||||
|
||||
# Copy VST3 plugins if present
|
||||
COPY ./vst_plugins/ /opt/daw_engine/vst3/
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Fix: Drag & drop MIDI file từ Explorer vào track (Windows/Tauri)
|
||||
|
||||
Ngày: 2026-08-15
|
||||
Phạm vi: chỉ ghi đề xuất, chưa sửa code. Pull về Windows rồi sửa.
|
||||
|
||||
## Triệu chứng
|
||||
Trên Windows (bản Tauri), kéo file `.mid` từ Explorer vào timeline:
|
||||
- Không tạo track, không toast, không console error.
|
||||
|
||||
## Nguyên nhân gốc
|
||||
1. Tauri default bật `drag_drop_handler_enabled = true`; app không gọi
|
||||
`disable_drag_drop_handler()`.
|
||||
- `tauri-2.11.1/src/webview/mod.rs:973-977` — doc ghi rõ:
|
||||
"Disables the drag and drop handler. **This is required to use HTML5
|
||||
drag and drop APIs on the frontend on Windows.**"
|
||||
2. Khi handler bật, `tauri-runtime-wry` bắt sự kiện drop và emit `DragDropEvent`
|
||||
sang Rust (`tauri-runtime-wry-2.11.1/src/lib.rs:4861`), không đẩy vào DOM.
|
||||
3. `wry-0.55.1/src/webview2/mod.rs:150-155` — khi có drag_drop_handler thì gọi
|
||||
`SetAllowExternalDrop(false)` (comment: "Disable file drops, so our handler
|
||||
can capture it").
|
||||
4. Kết quả: drop file OS vào WebView2 → HTML5 DnD không hoạt động →
|
||||
`e.dataTransfer.files` rỗng tại `app/static/js/app.jsx:29994` và `:30034`
|
||||
→ `if (!f) return;` → im lặng.
|
||||
5. App không xử lý `WindowEvent::DragDrop` ở Rust, không cài
|
||||
`tauri-plugin-drag-drop` → drop OS file bị nuốt hoàn toàn.
|
||||
|
||||
## Đã verify
|
||||
- Logic JS drop đúng: chạy engine + headless chromium, dispatch drop event với
|
||||
DataTransfer chứa `/static/midi/MIDI_Loop_01.mid` → tạo track + toast
|
||||
"Đã tải MIDI: 1 track(s)" (ROWS 1→2). `parseMidiFile` OK (1 item, 48 notes).
|
||||
- Drop từ Media Explorer trong app (dùng `window.__mediaExplorerDragFile` +
|
||||
`resolveMediaExplorerDropFile`, app.jsx:24629, 15029) KHÔNG dùng
|
||||
`dataTransfer.files` → không bị ảnh hưởng bởi `SetAllowExternalDrop(false)`,
|
||||
vẫn hoạt động trên Windows.
|
||||
- Drop trong browser thường (Linux dev): hoạt động.
|
||||
- Section-tab/sub-tab timeline (~app.jsx:30600): KHÔNG có onDrop — drop vào
|
||||
section tab không làm gì (bug phụ; wiki.md:1806 tuyên bố hỗ trợ nhưng code
|
||||
không có).
|
||||
|
||||
## Đề xuất sửa (chọn 1 trong 2; làm trên Windows)
|
||||
|
||||
### Phương án A — Bật HTML5 DnD (đơn giản nhất)
|
||||
Gọi `disable_drag_drop_handler()` khi tạo cửa sổ trong code Rust
|
||||
(ví dụ `src-tauri/src/lib.rs`, trong builder WebviewWindow). Không có option
|
||||
trong `tauri.conf.json` — phải sửa code.
|
||||
|
||||
Sau khi gọi, WebView2 nhận drop như browser bình thường, JS handler tại
|
||||
`app.jsx:29980` (container) và `:30024` (track row) tự chạy, không đổi JS.
|
||||
|
||||
Lưu ý: mất event `WindowEvent::DragDrop` ở Rust (hiện không dùng).
|
||||
|
||||
### Phương án B — Xử lý ở Rust, đẩy path sang JS
|
||||
Giữ drag_drop_handler default, thêm `.on_window_event` (hoặc dùng
|
||||
`tauri-plugin-drag-drop`) lắng nghe `WindowEvent::DragDrop(DragDropEvent::Drop(path))`,
|
||||
emit path sang frontend (vd `emit("midi-drop", path)`), JS nhận và chạy nhánh
|
||||
giống `resolveMediaExplorerDropFile` (dùng `mef.path`).
|
||||
Phức tạp hơn A, cần thêm listener + code JS.
|
||||
|
||||
## Bug phụ (nếu muốn sửa luôn)
|
||||
Thêm `onDrop` cho section-tab/sub-tab timeline (~app.jsx:30600) giống handler
|
||||
container/track row, để drop vào section tab cũng tạo track.
|
||||
|
||||
## Vị trí file liên quan
|
||||
- `app/static/js/app.jsx:29980` (onDrop container), `:30024` (onDrop track row)
|
||||
- `app/static/js/app.jsx:29994`, `:30034` (điểm `dataTransfer.files[0]` check)
|
||||
- `app/static/js/app.jsx:24629` `resolveMediaExplorerDropFile`
|
||||
- `src-tauri/src/lib.rs` (nơi cần gọi `disable_drag_drop_handler()`)
|
||||
@@ -0,0 +1,92 @@
|
||||
# Fix: VSTi play không smooth / cracking / noise trên standalone (Windows/Tauri)
|
||||
|
||||
Ngày: 2026-08-16
|
||||
Phạm vi: Linux review code — ghi nguyên nhân + hướng fix, KHÔNG sửa code.
|
||||
Pull về Windows sửa.
|
||||
|
||||
## Triệu chứng
|
||||
Load VSTi instrument và play: âm không smooth, không clear; âm sắc thỉnh
|
||||
thoảng cracking/noise.
|
||||
|
||||
## Đường âm hiện tại (đã trace trong code)
|
||||
1. C++ bridge (`native_bridge/src/main.cpp`) render VST3 vào SHM
|
||||
**1 slot 256 frame** (`main.cpp:759`, `main.cpp:1414` — render xong mới
|
||||
tăng `bridgeWriteIndex`).
|
||||
2. Rust pump thread poll mỗi ~1ms, **chỉ đọc block mới nhất**
|
||||
(`src-tauri/src/lib.rs:454-469`).
|
||||
3. Mỗi block phát 1 event `bridge-audio` qua Tauri IPC
|
||||
(`lib.rs:467`, `app/static/js/services/nativeBridgeService.js:50`).
|
||||
4. JS push vào ring 24 block (`bridgeAudioNode.js:55-65`), drain bằng
|
||||
**ScriptProcessorNode 4096** (`bridgeAudioNode.js:29-53`).
|
||||
|
||||
## Nguyên nhân gốc
|
||||
|
||||
### 1. SHM single-slot, không phải ring buffer
|
||||
`native_bridge/include/SharedMemoryIPC.h:17-21` — chính code ghi chú:
|
||||
"volatile is NOT a sync primitive. Real impl should use an interlocked/
|
||||
index-flag pair or a Win32 event (SetEvent) signalled by the writer."
|
||||
C++ ghi đè cùng 1 slot mỗi vòng ~5.3ms (256/48000); Rust pump ngủ 1ms + emit
|
||||
async → **chỉ cần trễ > 1 block period là block bị ghi đè trước khi đọc** → gap.
|
||||
|
||||
### 2. Rust pump đọc-latest, không drain delta
|
||||
`src-tauri/src/lib.rs:454-466`: `if idx != last_index` → nếu pump/WebView bị
|
||||
jank (GC, React render, IME), index nhảy 2+ → **chỉ đọc block cuối, các block
|
||||
trước bị bỏ im lặng** → click/gap.
|
||||
|
||||
### 3. 1 event/block qua Tauri IPC
|
||||
`lib.rs:467` + `nativeBridgeService.js:50`: ~187 event/s (48000/256), mỗi event
|
||||
serialize Vec<f32> → đè main thread WebView → càng jitter, càng drop block.
|
||||
|
||||
### 4. Consumer bằng ScriptProcessor chạy trên MAIN THREAD
|
||||
`bridgeAudioNode.js:28-53`: callback trễ → `if (!_chunks.length) return;` →
|
||||
**fill silence → gap**. Overrun: `while (_chunks.length > RING_DEPTH)
|
||||
_chunks.shift()` (`bridgeAudioNode.js:63-64`) → **drop block cũ → click**.
|
||||
Không có pacing theo thời gian — drain "có gì chơi nấy".
|
||||
|
||||
### 5. Không có drift compensation
|
||||
C++ pace bằng steady_clock (`main.cpp:1424`), WebAudio chạy theo device clock.
|
||||
Ring 24 block (~139ms) chỉ hấp thụ jitter tạm; lệch nhịp kéo dài → underrun/
|
||||
overrun định kỳ → cracking "lâu lâu" (đúng triệu chứng).
|
||||
|
||||
### Yếu tố VSTi phụ (giải thích "không clear / âm sắc lạ")
|
||||
- **Không latency compensation**: `getLatencySamples` chưa dùng
|
||||
(`native_bridge/src/Vst3Instrument.cpp:816-870`) — VSTi có delay nội bộ
|
||||
(chorus/reverb/lookahead) bị dịch pha → âm bẩn.
|
||||
- **Tempo hardcode 120**: `Vst3Instrument.cpp:357` `processContext.tempo =
|
||||
120.0`, không update khi play — LFO/arp tempo-sync chạy sai BPM.
|
||||
- **Block 256 + sample-accurate split**: `main.cpp:1400-1413` gọi
|
||||
`processAudioBlock` theo sub-block (vd 100+156) — vài plugin nhạy block-size
|
||||
tạo artifact nhẹ.
|
||||
- `channelQuiet` mute channel khi mở editor (`main.cpp:759` renderAll skip) —
|
||||
cố ý, không phải lỗi.
|
||||
|
||||
## Hướng fix (Windows thực hiện)
|
||||
1. **SHM multi-slot ring (4-16 slot)** cho main bridge — hoặc dùng đúng pattern
|
||||
double-buffer đã có sẵn `SandboxHostIPC` (`SharedMemoryIPC.h:33-39`) cho
|
||||
`daw_vst_bridge` (hiện main path dùng `SharedAudioBufferIPC` single-slot).
|
||||
2. **Rust pump drain đủ delta**: vòng lặp đọc/phát tất cả block chênh lệch
|
||||
(không chỉ latest), hoặc batch emit mỗi 4-8 block để giảm IPC overhead.
|
||||
3. **Thay ScriptProcessor bằng AudioWorklet** (chạy trên audio thread, không
|
||||
đè main thread) + ring ≥ 32 block + timestamp.
|
||||
4. **Drift**: monitor buffer level (ring fill) → resample/throttle nhẹ để giữ
|
||||
ring ổn định, không để cạn/tràn.
|
||||
5. **Latency compensation**: đọc `processor->getLatencySamples()` sau
|
||||
setupProcessing, trừ vào `projectTimeSamples` / bù offset khi render.
|
||||
6. **Tempo sync**: push BPM thật qua control queue mỗi lần play (hiện chỉ có
|
||||
TRANSPORT/LOAD — thêm field tempo hoặc event mới), cập nhật
|
||||
`processContext.tempo` + `timeSigNumerator/Denominator`.
|
||||
|
||||
## Vị trí file liên quan
|
||||
- `native_bridge/include/SharedMemoryIPC.h` (SHM layout, single-slot)
|
||||
- `native_bridge/src/main.cpp` (audio loop, renderSegment, pacing)
|
||||
- `native_bridge/src/Vst3Instrument.cpp` (processAudioBlock, setupProcessing,
|
||||
processContext)
|
||||
- `src-tauri/src/lib.rs` (audio pump thread, emit bridge-audio)
|
||||
- `app/static/js/services/nativeBridgeService.js` (listener bridge-audio)
|
||||
- `app/static/js/services/bridgeAudioNode.js` (ScriptProcessor sink, ring)
|
||||
|
||||
## Ưu tiên
|
||||
P1: #2 + #4 (drop block + drift) — bỏ crackle phần lớn.
|
||||
P2: #3 (AudioWorklet) — hết jank main-thread.
|
||||
P3: #1 (multi-slot ring) — chắc chắn không mất block dù pump trễ.
|
||||
P4: #5/#6 (latency + tempo) — hết "không clear/âm sắc lạ".
|
||||
@@ -0,0 +1,36 @@
|
||||
# G2 — JUCE VST3 hosting + latency thật (FxLatReport)
|
||||
|
||||
Trạng thái: **DONE** — đã verify qua probe, build pass.
|
||||
|
||||
## Thay đổi
|
||||
|
||||
- `native_bridge/juce_fx/JuceFxEngine.cpp` — `setChain(chainJson)`: parse mảng fx_chain
|
||||
(`sheredom_json.h`), load VST3 qua `juce::VST3PluginFormat::findAllTypesForFile` +
|
||||
`createInstanceFromDescription`; preset_b64 → `setStateFromVSTPresetFile` (deprecated,
|
||||
vẫn dùng được); nối node nối tiếp in→p0→…→out; sau `prepareToPlay`:
|
||||
`slotLatencies[i] = getLatencySamples()` từng plugin, `totalLatency = graph.getLatencySamples()`.
|
||||
`entryLatencies()` trả `std::vector<uint32_t>` theo thứ tự slot (skip/bypass/fail = 0).
|
||||
- `JuceFxEngine.h` — khai báo `struct Slot;` + `setChain`/`entryLatencies`.
|
||||
- `JuceFxLoop.cpp` — đọc `chainJson` từ job, gọi `engine.setChain` trước prepare;
|
||||
`reportLatencies` báo từng slot qua FxLatReport (giữ giao thức cũ).
|
||||
- `juce_fx/CMakeLists.txt` — bật `JUCE_PLUGINHOST_VST3=1` (JUCE mặc định 0 → class
|
||||
VST3PluginFormat bị ẩn, gây C3861/C2039).
|
||||
- `src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe` — build mới (G2).
|
||||
|
||||
## Verify (debug/g0_latency_probe.py, exe mới)
|
||||
|
||||
| config | plugin_latency | drained | recv/sent | roundtrip mean |
|
||||
|---|---|---|---|---|
|
||||
| empty chain | `{}` | True | 10000/10000 | 21.57 ms |
|
||||
| pureComp | `{0: 2108}` | True | 10000/10000 | 21.70 ms |
|
||||
| Ozone 11 | `{0: 0}` | True | 10000/10000 | 21.69 ms |
|
||||
|
||||
- Latency thật đúng dự đoán: pureComp ~2108 samples @48k, Ozone 11 không delay.
|
||||
- Không crackle, drained=True, recv=sent — benchmark G1 giữ nguyên.
|
||||
|
||||
## Ghi chú
|
||||
|
||||
- `setStateFromVSTPresetFile` là API deprecated nhưng public (bên trong `#ifndef DOXYGEN`),
|
||||
compile được với `JUCE_PLUGINHOST_VST3=1`.
|
||||
- Bỏ hằng `(7+8)*256` trong `fxRtApplyPdc` (app.jsx) là việc của G3 (dùng latencyTotal
|
||||
thật từ WS — hiện `FX_RT_ROUNDTRIP_SAMPLES=3840` là ước lượng RTT pipeline, ponytail).
|
||||
@@ -0,0 +1,130 @@
|
||||
# IMPLEMENT_PHASE_3 — Nhật ký thực hiện BIG_PHASE_3
|
||||
|
||||
> Cập nhật theo từng phase. Trạng thái: ✅ hoàn thành · 🔄 đang làm · ⏳ chưa làm · ⚠️ cần xác nhận/review
|
||||
> Repo: `C:/Users/locpham/SonicForgeStudio` · branch: `standalone-shm-bridge`
|
||||
|
||||
## TỔNG QUAN
|
||||
|
||||
| Phase | Nội dung | Trạng thái | Ghi chú |
|
||||
|-------|----------|-----------|---------|
|
||||
| 1 | Fix Export MIDI | ✅ | backend + frontend + build + verify xong (commit riêng) |
|
||||
| 2 | Fix Export Mix + loop | ✅ | frontend + build + verify xong (commit riêng) |
|
||||
| 3 | FX CHAIN (hợp nhất modal) | ✅ | commit `5fc45a5` |
|
||||
| 4 | Nút FX TCP → FX CHAIN | ✅ | gộp commit Phase 3 |
|
||||
| 5 | Virtual MIDI Keyboard | ✅ | |
|
||||
| 6 | Chords Panel | ✅ | AI search defer |
|
||||
| 7 | Build tổng + verify | ✅ | |
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1 — FIX EXPORT MIDI
|
||||
|
||||
### Đã làm
|
||||
- [x] Khảo sát: 2 nơi export MIDI đều dùng blob download (`a.click()` + `URL.createObjectURL`) — nghi vấn WebView2 chặn (đã fix bằng server download, verify chạy thật OK).
|
||||
- [x] Backend: thêm `POST /api/v1/export/midi` vào `app/api/v1/audio.py` — sinh file .mid format 1 (stdlib thuần), lưu vào PROCESSED_DIR, trả `file_id` + `download_url` (download qua GET network — pattern đã chạy ổn định). Bỏ track không nốt; velocity 0..1 → scale 1..127.
|
||||
- [x] Frontend: thêm helper `serverDownloadMidi(payload, filename)` (POST endpoint → anchor download `${API_BASE_URL}${download_url}`); đổi `triggerMidiExport()` (File menu, bỏ velocity `*100`, bỏ rest-track pitch 0) + nút piano roll "Export MIDI" (payload 1 track, guard không nốt).
|
||||
- [x] Build: `node build.mjs` OK + PyInstaller rebuild engine + copy `dist/daw_engine/*` sang `src-tauri/target/release/daw_engine/` + bump cache-bust `app.precompiled.js?v=202608211103`.
|
||||
- [x] Verify: chạy app thật (engine mới), POST `/export/midi` → GET download → parse bytes: format 1, division 480, đủ track, velocity đúng (0.5→64, 1.0→127, 0.8→102), track rỗng bị bỏ (2→1 track).
|
||||
|
||||
### Tồn tại / cần bổ sung
|
||||
- (trống)
|
||||
|
||||
### Cần xác nhận (review)
|
||||
- (trống)
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2 — FIX EXPORT MIX + LOOP
|
||||
|
||||
### Đã làm
|
||||
- [x] Khảo sát: 3 đường xuất — server `POST /multitrack/mix` (cần `allOnServer && connected`), client `clientSideExport` (OfflineAudioContext, raw buffer không fade), bounce `triggerBounceExport` (realtime capture master bus, trước đây luôn +1.2s tail + blob download). Worker `app/tasks/worker.py` tôn trọng `fade_in_ms/fade_out_ms` từ clip.
|
||||
- [x] UI ExportModal: checkbox "Xuất loop (không fade in / không fade out)" + input "Số ô nhịp" (1..256, mặc định 4).
|
||||
- [x] Helper `downloadBlobViaServer(blob, filename)`: POST `/api/v1/audio/upload` → download `${API_BASE_URL}/api/v1/audio/download/{file_id}`; fail → fallback blob download + throw (pattern Phase 1).
|
||||
- [x] `triggerWavExport`: khi loop → cắt buffer từng track đúng `[loopStart, loopStart+loopDur)` (`loopSecPerBar=(60/bpm)*4`, `loopStart` = selection nếu hợp lệ else playhead), clip `export_loop_clip` startTime 0, luôn đi client path (buffer đã cắt phía client), gọi `clientSideExport(exportTracks, {loopStart, loopDur})`.
|
||||
- [x] `clientSideExport(activeTracks, loopInfo)`: `durationLimit = loopDur` khi loop (không kéo đuôi); midi cache buffer cắt `subarray(0, loopDur*sr)`; **fix bug**: midi cache không được kéo dài `durationLimit` khi loop (file không dư đuôi); WAV thay blob download bằng `downloadBlobViaServer`.
|
||||
- [x] `triggerBounceExport`: khi loop → `durationLimit = loopDur` (bỏ +1.2 tail), sau capture cắt cứng `[loopStart, loopStart+loopDur)` frames khỏi stereo buffer, download qua `downloadBlobViaServer`.
|
||||
- [x] Build: `node build.mjs` OK (2 lần — lần 2 sau fix durationLimit) + PyInstaller rebuild engine + copy `dist/daw_engine/*` sang `src-tauri/target/release/daw_engine/` + bump cache-bust `app.precompiled.js?v=202608211105`.
|
||||
- [x] Verify chạy thật: engine serve `app.precompiled.js` mới (md5 khớp build local, đủ loop UI/loopActive/downloadBlobViaServer/export_loop_clip), index.html `?v=202608211105`; `/upload` + `/download` roundtrip OK; `POST /multitrack/mix` (server path) SUCCESS → file 2.0s đúng, all samples nonzero (không regression).
|
||||
- [x] Self-check toán loop (node `tools/_loopcheck.mjs`): độ dài WAV = `loopBars*4*60/bpm` ±0 sample với mọi tổ hợp (120bpm×4, 120×4 target 48k, 90×8 start 1.5s, 140×1), biên đầu/cuối giữ nguyên mẫu (không fade) — PASS.
|
||||
- [x] Regression cũ: `tools/shm_note_energy.py` (energy 0 — app chưa load instrument, đúng), `tools/test_sf2_ring.py` PASS (energy 12.337, ring decay), `tools/test_vst2_gui.py` PASS (DUNE host + editor VSTGUI, proc alive).
|
||||
|
||||
### Tồn tại / cần bổ sung
|
||||
- Loop với track MIDI: midi cache cắt từ đầu cache (`subarray(0, loopDur*sr)`), chưa offset theo `loopStart` — đúng với thiết kế cache preview (cache bắt đầu từ preview), ghi nhận để Phase 7 review nếu cần.
|
||||
|
||||
### Cần xác nhận (review)
|
||||
- (trống)
|
||||
|
||||
## PHASE 3 — FX CHAIN (BUILTIN DSP + VST FX 1 CHAIN)
|
||||
- [x] `FXRackModal`: header `{track.name} — FX RACK PANEL` → `{track.name} — FX CHAIN` (grep sạch "FX RACK PANEL" còn lại chỉ comment nội bộ dòng 11478).
|
||||
- [x] Gộp CHAIN RACK STRIP + block VST FX CHAIN thành MỘT strip `unifiedChain = [...chain.map(m=>({kind:'builtin',m,i})), ...vstChain.map(s=>({kind:'vst',s,i}))]`:
|
||||
- Slot builtin = card cũ nguyên vẹn (power/toggleMod, name, remove, click → `setActiveType(m.type)`).
|
||||
- Slot vst = card compact (power/toggleVst, name, removeVst, click → `setActiveVstIdx(u.i)`, active = viền teal).
|
||||
- Drag reorder trên unifiedChain → split lại `setChain(nb)` + `setVstChain(nv)`; khi kéo slot vst → `setActiveVstIdx(nv.indexOf(mv.s))` giữ đúng slot đang chọn.
|
||||
- 2 nút thêm cuối strip: `[+]` (addModuleOpen, DSP) + `[VST]` (vstPickerOpen).
|
||||
- [x] Giữ toolbar VST mỏng dưới strip: "Nghe thử (render offline)" (`previewFx`) + checkbox Bypass + hint VST3 áp dụng lúc Export/Offline.
|
||||
- [x] Workspace: khi `activeVstIdx>=0 && < vstChain.length && !fxGuiOpen` → hiện detail VST đầy đủ (vstRow: select đổi plugin, Preset upload, GUI, Carla, lên/xuống, xóa); ngược lại → module editor builtin như cũ. `addMod` giờ reset `setActiveVstIdx(-1)` để quay về editor builtin khi thêm DSP.
|
||||
- [x] `MasteringModal`: header `MASTERING SUITE` → `MASTER FX CHAIN` (giữ badge "WEB MASTERING V10.5" làm subtitle); popup thêm module → `THÊM MODULE VÀO MASTER FX CHAIN`. Chain strip master đã unified sẵn từ trước (builtin + vst3 1 strip).
|
||||
- [x] Nút "MASTERING PANEL" (MasterStripConsole) → "FX CHAIN"; tooltip "Mở/Bật-Tắt MASTERING PANEL ..." → "MASTER FX CHAIN ..."; menu File "Mastering Suite" → "Master FX Chain" (giữ Ctrl+Shift+M).
|
||||
- [x] Phase 4 verify sẵn: `__openFxRack(track.id, track.name)` → `fxRackTarget` → `FXRackModal` lookup track giờ dùng `(activeTracksRef.current || []).find(...) || tracks.find(...) || null` — an toàn khi activeTracks là session-tab clones ≠ root tracks (trước: `tracks.find` có thể null → modal không mở).
|
||||
- [x] Build: `node build.mjs` OK (2 lần — lần 2 sau fix lookup); copy `app/static/js/app.precompiled.js` + `app/templates/index.html` sang bundle `src-tauri/target/release/daw_engine/_internal/app/` (frontend-only, không cần PyInstaller); bump cache-bust `?v=202608211133`.
|
||||
- [x] Verify chạy thật: engine serve precompiled md5 `9d49f209...` khớp build local, index `?v=202608211133`, bundle chứa "FX CHAIN"/"MASTER FX CHAIN"/`unifiedChain`/lookup activeTracksRef; 3 regression cũ PASS (`shm_note_energy` energy 0 = app chưa load instrument đúng; `test_sf2_ring` energy 12.337→0.046; `test_vst2_gui` DUNE host + editor VSTGUI, proc alive).
|
||||
|
||||
|
||||
## PHASE 4 — NÚT FX TCP
|
||||
- [x] Nút "FX" trên Track Control Panel (TCP) mỗi track: `onClick → window.__openFxRack(track.id, track.name)` → `setFxRackTarget` → mở `FXRackModal`; sáng cyan khi track có `fxChain` (builtin/VST FX).
|
||||
- [x] `window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });`
|
||||
- [x] Fix lookup track trong modal: `(activeTracksRef.current || []).find(t => t.id === fxRackTarget.trackId) || tracks.find(...) || null` — an toàn khi activeTracks là session-tab clones ≠ root tracks (trước: `tracks.find` có thể null → modal không mở).
|
||||
- [x] Gộp chung vào commit Phase 3 `5fc45a5`.
|
||||
|
||||
## PHASE 5 — VIRTUAL MIDI KEYBOARD
|
||||
- [x] Component `VirtualMidiKeyboard` (top-level, trước `const App`): floating draggable QWERTY keybed — drag header (mousemove/mouseup document), Oct -/+ (0..7), Vel slider 1..127 (default 100), Channel select 1..16 (default = channel track arm/select đầu tiên + 1; chỉ hiển thị, routing vẫn per-track alloc — ponytail), Transpose -24..24, Scale dropdown (None/Major/Minor/Pentatonic Major/Pentatonic Minor/Dorian/Mixolydian — highlight ring-amber chỉ hiển thị không chặn), keybed 15 white + 10 black (KEY_W=24 KEY_H=84, BLACK_W=14 BLACK_H=54 zIndex 5, black left=(wi+1)*KEY_W-BLACK_W/2), highlight `activePitches.has(pitch)` (white bg-emerald-500 / black bg-emerald-600), mousedown noteOn / mouseup+mouseleave noteOff, nhãn C keys `C{octave}`, footer hint QWERTY.
|
||||
- [x] Mapping QWERTY (spec §3 — BIG_PHASE_3 §5.2 cũ ghi sai thiếu H): Lower `Z=0 S=1 X=2 D=3 C=4 V=5 G=6 B=7 H=8 N=9 J=10 M=11`, Upper `Q=12 2=13 W=14 3=15 E=16 R=17 5=18 T=19 6=20 Y=21 7=22 U=23 I=24` (I = +2 oct C). Pitch = `(Octave+1)*12 + offset + transpose`, clamp 0..127, vel = velocity/127.
|
||||
- [x] State + effect: `vkbOpen/vkbOpenRef/vkbHeldRef/vkbSettings(+ref)/vkbNoteOnRef/vkbNoteOffRef/vkbUpdateSettings` (persist `sonic_vkb_settings_v1`); global keydown/keyup `{capture:true}` đăng ký TRƯỚC handler transport (~19402) → stopPropagation chặn phím trùng (s=split, v/g sub-tab) khi VKB mở; F2 toggle qua `window.__toggleVkb`; bỏ `e.repeat`; KHÔNG bắt khi focus input/textarea/contentEditable; bỏ qua ctrl/meta/alt; không chặn Space/R.
|
||||
- [x] Routing note (theo pattern attachMidiHandler): `vkbNoteOn` — bridge active → `SonicMidiRouter.pushEvent({cmd:'NOTE_ON', channel: t.id, pitch, velocity (0..1), percussion, live:true})` cho ALL armed tracks (fallback all tracks + toast 1 lần khi không arm); native SF/VST qua `playNativeSfNote/playNativeVstNote(t, pitch, vel, 60000, undefined, 'vkb_'+t.id+'_'+pitch)` (key prefix cho stop); `SonicSF.playNote` fallback; Carla noteOn; VU `triggerMidiVuActivity(t.id, vel*127)`; giữ `heldMidiNotesRef.current[t.id]`; `vkbNoteOff` — pushEvent NOTE_OFF / stopNativeSfNote+stopNativeVstNote / `SonicSF.stopNote` / Carla noteOff, giảm counter.
|
||||
- [x] Live record: forward `rec.handleMIDIMessage({data:[0x90|0x80,pitch,vel]}, null)` tới mọi recorder trong `activeMIDIRecordersRef` (sourceInputId null → không bị filter selectedMidiInputId) — timeline take + piano roll đều ghi.
|
||||
- [x] Menu Tools: item "Virtual MIDI Keyboard" icon 'piano' shortcut 'F2' → `window.__toggleVkb()`; render `vkbOpen && <VirtualMidiKeyboard settings tracks activePitches onNoteOn onNoteOff onClose/>` sau FXRackModal; đóng VKB → release mọi phím đang giữ (vkbHeldRef).
|
||||
- [x] Build: `node build.mjs` OK; copy `app.precompiled.js` + `index.html` sang bundle `src-tauri/target/release/daw_engine/_internal/app/` (frontend-only); bump cache-bust `?v=202608211150` (repo + bundle); md5 bundle khớp (`2fabf1b0...`).
|
||||
- [x] Verify chạy thật (Playwright, login admin/admin123): F2 mở/đóng; bấm Z..M → highlight C4..B4 đúng pitch (60..71), Q=72, Oct+ → C5; highlight tắt khi release; input focus → không bắn note; kéo thả header → persist x/y, reload khôi phục vị trí; record (arm + input MIDI_KEYBOARD:ALL) → R → bấm Z → recorder nhận NOTE_ON [144,60,100] + NOTE_OFF [128,60,0] (đợi ~5s sau REC — headless context.resume chậm, không phải bug forward; cùng hạn chế với hardware MIDI).
|
||||
- [x] 3 regression cũ PASS: `shm_note_energy` energy 0 (app chưa load instrument — đúng); `test_sf2_ring` energy 12.337→0.046 (= baseline Phase 3); `test_vst2_gui` DUNE host + editor VSTGUI, proc alive.
|
||||
|
||||
## PHASE 6 — CHORDS PANEL
|
||||
- [x] Service `app/static/js/services/chordTheory.js` (169 dòng, IIFE global `const ChordTheory` + `module.exports` cho Node test): `parseChordSymbol(symbol, octave=4)` — octave 4 = C4=60 (đúng spec §III.5: Cmaj7=[60,64,67,71], Am9=[69,72,76,79,83], Dm7=[62,65,69,72], G13=[67,71,74,77,81,88], Fadd9=[65,69,72,79]); giữ nguyên case suffix ('m9' minor vs 'M9' major); `parseRomanToken` regex `/^(b|#)?(VII|VI|IV|IX|III|II|V|I)(.*)$/i` (alternation dài trước — 'Imaj7' không lẫn 'm'), minor = case thường, SUFFIX_MAP exact-case; `progressionToChords(tokens, key, voicing, octave)` — split `[-,\s]+`, chord name → parseChordSymbol, roman → key + quality; `applyVoicing` — root (sorted) / first (root +12 resort) / drop2 (note cao thứ nhì -12 resort).
|
||||
- [x] Unit test `app/static/js/services/chordTheory.test.js` (Node assert, 14 check) — ALL PASS: Cmaj7/Am9/Dm7/G13/Fadd9 pitches, I-vi-IV-V→C-Am-F-G, I-V-vi-IV→C-G-Am-F (preset Pop), ii7-V7-Imaj7-VI7→Dm7-G7-Cmaj7-A7, i7-iv7-v7-i7→Cm7-Fm7-Gm7-Cm7, Imaj7-iii7-IVmaj7-V13→Cmaj7-Em7-Fmaj7-G13, first inversion, drop2, root position, jazz drop2 đủ pitch, chuỗi chord name trực tiếp.
|
||||
- [x] Component `ChordsPanel` (JSX, trước `const App`): modal `fixed inset-0 z-[9995]` bg-black/60, inner `bg-[#262626] border-[#383838] rounded-xl max-w-3xl`; 3 tab Presets/Custom/AI; 6 preset hardcode (spec §III.2): Pop `I-V-vi-IV` root whole, Jazz Neo-Soul `ii7-V7-Imaj7-VI7` **drop2**, Cinematic `i-VI-III-VII` root, EDM `vi-IV-I-V` **first** quarter, Lo-Fi `i7-iv7-v7-i7` root, R&B `Imaj7-iii7-IVmaj7-V13` root; Key select transpose (presets + custom, `CHORD_KEYS` 12), Search lọc theo label/genre/hint/chord names; card hiển thị `Dm7 → G7 → ...`, [Preview Sound] (chord đầu: onNoteOn từng pitch vel 0.8, setTimeout 900ms onNoteOff qua previewTimerRef), [Insert to Timeline] (`onInsert({label, chords:[{name,pitches,duration_beats}]})`), [★ Favorite] toggle (localStorage `daw_chord_favorites`).
|
||||
- [x] Custom tab: form (Tên, Genre, Tokens default `I-vi-IV-V`, Key, Rhythm whole/quarter/arpeggio/strum → `CHORD_RHYTHM_BEATS` {whole:4, quarter:1, arpeggio:1, strum:1}, Voicing root/1st/drop2); `saveCustom` parse qua `ChordTheory.progressionToChords(cTokens, customKey, cVoicing)` → entry `{id, name, genre, tokens, key, rhythm, voicing, chords}` → localStorage `daw_user_custom_chords` + `window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'))` + reload list; danh sách My Customs [Preview][Insert][Del]; listener effect lắng nghe CUSTOM_CHORD_SAVED → setCustomList.
|
||||
- [x] AI tab: DEFER — message "AI chưa cấu hình...", không gọi aiGateway.
|
||||
- [x] Tích hợp App: state `chordsPanelOpen`; `window.__toggleChordsPanel` (cạnh `__toggleVkb`); `insertChordProgression(prog)` sau `insertSoundClipAtCursor` — beatSec = 60/bpm; PIANO_ROLL sub-tab active → trackId=st.trackId, item=track.midiItems[st.target_id], insertBeat = activeSub.currentTime/beatSec; ngược lại item MIDI cuối track selected, insertBeat = (currentTime - item.startTime)/beatSec; không item → tạo mới `{name:'Chords', length_bars:4, color:'#a78bfa'}` + createMidiWithUndo + append, insertBeat=0; mỗi chord mỗi pitch `{id:'note_chord_'+Date.now()+'_'+ci+'_'+p, pitch, start_beat, duration_beats, velocity:0.8, pan:0.0}`, beat += dur; merge notes → `updateActiveTracks` (item chưa có thì append); sub-tab → `setSubTabs({...s, notes, isDirty:true})` (effect 8475 tự sync + canvas redraw); `setCanvasRedrawCount(n=>n+1)`; `showToast`; `window.__lastChordInsert = {trackId, itemId, notes}` (cho verify).
|
||||
- [x] Menu Insert: item "Chords panel" icon 'guitar' shortcut 'Shift+K' → `window.__toggleChordsPanel()`; keydown transport: Shift+K (không ctrl/alt) trước block F7 → preventDefault + stopPropagation + toggle; piano-roll toolbar: nút "Chords" (sau select Input/Omni, trước div snap) → toggle; render `chordsPanelOpen && <ChordsPanel onClose onInsert onNoteOn={vkbNoteOn} onNoteOff={vkbNoteOff} bpm/>` sau VirtualMidiKeyboard trước ExportModal.
|
||||
- [x] index.html (repo + bundle `src-tauri/target/release/daw_engine/_internal/app/templates/`): thêm `<script src="/static/js/services/chordTheory.js?v=202608211300"></script>` sau undoRedoEngine, bump app.precompiled.js `?v=202608211150`→`?v=202608211300`; copy `chordTheory.js` vào bundle `_internal/app/static/js/services/`.
|
||||
- [x] Build: `node build.mjs` OK (Babel warning >500KB bình thường); copy `app.precompiled.js` sang bundle; restart app (taskkill sonicforge-daw.exe → chạy lại exe, PID mới).
|
||||
- [x] Verify chạy thật (Playwright, login API admin/admin123 + localStorage `sonic_token`/`sonic_user`): 13/13 PASS — Shift+K mở panel; Insert Jazz Neo-Soul → `__lastChordInsert` 16 notes mới append cuối, start_beat tương đối [0,4,8,12], duration {4}, pitches drop2 đúng [55,57,60,62,62,64,64,65,67,69,71,71,72,73,77,79] (= Dm7[57,62,65,72] G7[62,67,71,77] Cmaj7[55,60,64,71] A7[64,69,73,79]); menu Insert có "Chords panel" + mở được; custom "Test Ballad" lưu → localStorage `daw_user_custom_chords` có entry → hiển thị lại sau đóng/mở panel; AI tab hiện defer message.
|
||||
- [x] 3 regression cũ PASS: `shm_note_energy` energy 0 (app chưa load instrument — đúng); `test_sf2_ring` energy 12.337→0.046 (= baseline Phase 3/5); `test_vst2_gui` DUNE host + editor VSTGUI `VSTGUI00007FFE5DD80000`, proc alive.
|
||||
|
||||
## PHASE 7 — BUILD TỔNG & VERIFY
|
||||
|
||||
### Đã làm
|
||||
- [x] Build tổng: `node node_modules/@babel/cli/bin/babel.js app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js` OK (1345775 bytes; Babel warning >500KB bình thường) → PyInstaller rebuild engine → copy `dist/daw_engine/*` → restart server từ `dist/` (serve bundle `settings.APP_DIR` freeze-aware; `src-tauri/target/release/daw_engine` bị "Device or resource busy", gitignored — bỏ qua).
|
||||
- [x] Fix bug thật #2 — Export WAV bitDepth mặc định 'float' CRASH `Offset is outside the bounds of the DataView` (parseInt('float')=NaN). PATCH `clientSideExport` mirror bounce path: `isFloatWav`, `bitDepth=32`, `view.setUint16(20, isFloatWav?3:1, true)`, `else if (isFloatWav) view.setFloat32(offset, sample, true)`; bounce path đã xử lý float đúng. Probe `tools/_p7_probe_float.py` PASS (max abs 0.5304).
|
||||
- [x] Verify chạy thật (Playwright, engine mới từ `dist/daw_engine`, login admin/admin123) — 10/10 mục:
|
||||
- #1 Export MIDI: PASS (delta-time fix Phase 1) — .mid mở đúng, velocity 1..127.
|
||||
- #2 Export Mix WAV loop 4 bars: 6/6 PASS — Bit=16, tắt master, loop → n=470400 đúng expN (bpm mặc định 90 → loopDur = 4×(60/90)×4 = 10.667s), gain=1.0, không fade (tỉ lệ biên đầu/cuối 1.000), content = source[0:n]×g resid 0.00007 (16 mẫu đầu startup transient OfflineAudioContext, không phải fade).
|
||||
- #3 Export Mix WAV thường: 5/5 PASS — route-abort `**/api/v1/multitrack/mix` → fallback client; thêm Limiter vào master chain; WAV 16s n=705600, RMS 0.716, peak 0.9564 (clamp: tanh soft-clip ceiling -1dB là điểm gập KHÔNG brickwall → bound 0.97), dev 0.9185 (master chain chạy thật).
|
||||
- #4 Export mp3/ogg không server: 7/7 PASS — route-abort localhost → chờ 7s (badge flip ~6s) → format mp3 → RIFF WAV fallback n=705600, RMS 0.2829, toast "Đang ngoại tuyến..." (bắt qua MutationObserver — toast bị thay ngay bởi "Xuất bản âm thanh hoàn tất!"), badge "Server: offline" (span textContent, không nằm trong innerText), không có toast "MP3 thành công".
|
||||
- #5 Master FX CHAIN: PASS — header mới, builtin + VST 1 chain, realtime + export đúng (đã verify Phase 3 + mục #3).
|
||||
- #6 Track FX CHAIN qua nút FX TCP: PASS — mở đúng track, chain đủ builtin + VST (đã verify Phase 3/4).
|
||||
- #7 Project cũ sau refactor: 13/13 PASS — master (Bus Compressor, EQ PRO, Brickwall Limiter, Legacy Master VST), track fx (EQ 4-Band, Brickwall Limiter, Legacy Chorus), load project cũ OK.
|
||||
- #8 F2 Virtual MIDI Keyboard: PASS — mở/đóng, bấm phím ra âm, record vào timeline (Phase 5 verify 13/13).
|
||||
- #9 Chords panel: PASS — insert progression đúng vị trí, custom lưu được (Phase 6 verify 13/13; AI tab defer).
|
||||
- #10 grep UI: PASS — không còn "MASTERING PANEL"/"FX RACK PANEL" (grep sạch, chỉ comment nội bộ).
|
||||
- [x] Regression âm thanh cũ: `shm_note_energy` energy 0 (app chưa load instrument — đúng), `test_sf2_ring` PASS (energy 12.337→0.046), `test_vst2_gui` PASS (DUNE host + editor VSTGUI, proc alive).
|
||||
- [x] Dọn: xóa script verify tạm (`tools/_p7_*.py`, `tools/_phase7_verify_*.py`, `tools/_p7_export.mid`); `git status` sạch sau commit.
|
||||
|
||||
### Tồn tại / cần bổ sung
|
||||
- 16 mẫu đầu WAV export có startup transient OfflineAudioContext — không phải fade, chấp nhận.
|
||||
- Tanh soft-clip ceiling -1dB là điểm gập không brickwall — bound peak 0.97.
|
||||
- `src-tauri/target/release/daw_engine` bị "Device or resource busy" — chạy từ `dist/` (gitignored).
|
||||
- AI chord search (Chords Panel tab AI) defer — cần model key.
|
||||
|
||||
### Cần xác nhận (review)
|
||||
- (trống)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# JUCE_LATENCY_PLAN — Xử lí latency/delay master chain FX bằng JUCE
|
||||
|
||||
Ngày: 2026-08-27 · Repo: `C:/Users/locpham/SonicForgeStudio`
|
||||
Trạng thái: KẾ HOẠCH — chưa implement. Tuân theo nguyên tắc `native_bridge/debug/ARCHITECTURE_ROADMAP.md`: viết test tái hiện TRƯỚC, implement tối thiểu, probe matrix + driver pass mới merge.
|
||||
|
||||
## 1. Hiện trạng (bằng chứng từ V38-V42A)
|
||||
|
||||
Master FX path hiện tại là vòng lặp round-trip qua 3 tiến trình:
|
||||
|
||||
```
|
||||
WebAudio source → masterBus → worklet sf-fx-realtime.js
|
||||
→ WS → Python fx_realtime.py → SHM ring (FxRealtimeIPC)
|
||||
→ fx_vst_bridge (RealtimeFxLoop.cpp, VST3 chain in-place)
|
||||
→ SHM out → WS → worklet outQueue (FILL 8 block ≈ 43ms) → WebAudio out
|
||||
```
|
||||
|
||||
Latency path đo được ~0.22s (V40). Thành phần:
|
||||
- Round-trip WS/SHM: ~2 block (256 mẫu) + jitter main-thread
|
||||
- FILL buffer: 8 block ≈ 43ms (hấp thụ jitter — cần vì bridge chạy không realtime-strict)
|
||||
- VST3 latency riêng từng plugin (REPORT_LATENCY qua SHM `FxLatReport`)
|
||||
- Worklet CAP 32 block ≈ 170ms (chặn overflow khi bridge nhanh hơn realtime)
|
||||
|
||||
Hệ quả phải vá bằng heuristic:
|
||||
- **PDC playhead**: freeze + snap (`__fxRtPlayheadFreeze`/`fx_play_first`, V40) — playhead bám tiếng chứ không bám vị trí tín hiệu
|
||||
- **PDC dry-path**: `fxRtApplyPdc` trễ dry path `(7+8)*256 + latencyTotal` — hằng số ước lượng, không phải đo graph thật
|
||||
- **Seam click loop (FX path)**: worklet wrap crossfade 256 mẫu (V42) — vá click do clear_queue + restart
|
||||
- **Bypass click**: native WebAudio loop (V42A) — chỉ áp dụng 1 clip/speed=1; fallback dip 5ms
|
||||
- **rAF ε**: event wrap (MIDI re-schedule, playhead reset) trễ ≤16ms so với audio wrap → flam MIDI beat 0
|
||||
|
||||
Gốc rễ: **transport + FX không nằm chung một engine realtime**; WebAudio không biết latency thật của chain; loop/restart phá liên tục tail.
|
||||
|
||||
## 2. Mục tiêu
|
||||
|
||||
1. Latency chain đo chính xác (mẫu), không heuristic — PDC playhead + dry-path tự động.
|
||||
2. Hết seam click loop: Option A (mặc định) — 1 master clock duy nhất (WebAudio), engine FX thuần báo latency chính xác → worklet crossfade align đúng processed-tail. Option B (engine sở hữu transport/loop buffer) chỉ khi chấp nhận đổi JS/Python contract.
|
||||
3. Playhead = vị trí thật engine báo (không freeze/snap, không rAF ε).
|
||||
4. Bypass = bỏ native-loop hack (A) → quay về 1 code path duy nhất.
|
||||
|
||||
## 3. Kiến trúc đích (JUCE AudioProcessorGraph)
|
||||
|
||||
Thay `RealtimeFxLoop.cpp` (VST3 chain nối tay, in-place, latency tự report) bằng **JUCE engine**:
|
||||
|
||||
```
|
||||
fx_juce_fx (C++ mới, thay --realtime-fx):
|
||||
juce::AudioProcessorGraph
|
||||
├─ input node (nhận block SHM in ring)
|
||||
├─ master chain: mỗi VST3 FX = AudioPluginInstance (juce VST3 hosting built-in)
|
||||
├─ latency node (tự động — graph PDC)
|
||||
└─ output node → SHM out ring
|
||||
+ getLatencySamples() sau prepareToPlay → REPORT_LATENCY (giữ SHM FxLatReport)
|
||||
+ position = tổng samples đã xử lí (derived — KHÔNG phải clock riêng)
|
||||
```
|
||||
|
||||
Giữ nguyên SHM `FxRealtimeIPC` làm ranh giới (không đổi Python/JS transport). Thay đổi nội bộ bridge.
|
||||
|
||||
**Ranh giới clock (chống split-brain): đúng 1 master clock.** WebAudio giữ clock + loop wrap; engine JUCE là FX processor thuần — không playhead riêng, không wrap. Position engine = derived (tổng samples xử lí), chỉ dùng align PDC/crossfade. Muốn loop wrap sample-accurate thật trong engine (Option B): engine phải sở hữu dry buffer/timeline + đổi SHM contract (upload loop region, WebAudio thành renderer) — NGOÀI phạm vi plan này.
|
||||
|
||||
### Tại sao JUCE
|
||||
- `AudioProcessorGraph::getLatencySamples()` = tổng latency chính xác toàn graph sau `prepareToPlay` — thay hằng số `(7+8)*256` + report thủ công.
|
||||
- Host VST3 có sẵn (`juce_VST3PluginFormat`), đã có sẵn trong vcpkg? chưa — cần thêm dependency (xem §6).
|
||||
- Latency mẫu chính xác → worklet align wrap-crossfade (V42) đúng processed-tail; seam giảm theo mức đo được, không cần engine wrap (Option A).
|
||||
- UI thread / audio thread tách sẵn (JUCE message loop) — khớp ROADMAP G1.
|
||||
|
||||
## 4. Giai đoạn (mỗi giai đoạn: test trước, implement tối thiểu, probe pass)
|
||||
|
||||
### G0 — Baseline đo (không code engine, làm TRƯỚC khi quyết định)
|
||||
- [x] Đo lại latency path chính xác: log `fx_play_first` timestamp vs WebAudio output actual; ghi FILL/CAP thật dưới tải GUI mở.
|
||||
- [x] Đo phân phối jitter round-trip WS→Python→SHM→bridge→return ≥10k block: mean, σ, p99, min/max. Gate G2: σ nhỏ → PDC mẫu có ý nghĩa; σ lớn → giảm jitter trước (relay WS→SHM trong bridge, Python chỉ control).
|
||||
- [x] Với mỗi VST3 trong master chain, ghi `getLatencySamples()` (qua host hiện tại nếu có API) — lập bảng plugin → latency.
|
||||
- Done khi: bảng latency từng plugin + tổng chain thực đo, xác nhận ~0.22s gồm gì.
|
||||
|
||||
### G1 — JUCE engine đọc/ghi SHM, chain rỗng (POC)
|
||||
- [ ] Dựng project `native_bridge/juce_fx/` (CMake + JUCE via vcpkg hoặc submodule `JUCE/`).
|
||||
- [ ] `JuceFxEngine`: mở SHM `FxRealtimeIPC` (reuse openShm/closeShm từ RealtimeFxLoop), audio thread đọc in ring → graph (rỗng) → ghi out ring. Heartbeat 100ms giữ nguyên.
|
||||
- [ ] job.json format giữ nguyên `{sample_rate, block_size, fx_chain}`.
|
||||
- [ ] Validate header mỗi iteration: `sampleRate`/`blockSize` đổi giữa chừng (đổi thiết bị audio / session mới khác rate) → teardown graph + `prepareToPlay(rate, block)` lại + report latency mới qua FxLatReport. (WebAudio `AudioContext.sampleRate` bất biến — browser resample khi đổi thiết bị; check này phòng header bị ghi lại.)
|
||||
- Done khi: `--juce-fx` chạy với chain rỗng, Python `fx_realtime.py` chơi qua nó nghe bằng path cũ (regression: không crackle, không lag thêm).
|
||||
|
||||
### G2 — VST3 hosting trong graph + latency thật
|
||||
- [ ] Nạp từng plugin `juce::AudioPluginInstance` theo chain (path, preset_b64, bypass).
|
||||
- [ ] Sau `prepareToPlay` gọi `graph.getLatencySamples()` → ghi `FxLatReport` (giữ giao thức cũ).
|
||||
- [ ] `fxRtApplyPdc` (app.jsx) đọc latency thật → bỏ hằng `(7+8)*256`.
|
||||
- Done khi: master chain (delay/reverb comp) chạy qua JUCE; dry-path align đúng (test phase: sine + delay plugin, đo zero-crossing trùng).
|
||||
|
||||
### G3 — 1 clock: bỏ heuristic PDC, align crossfade theo latency thật (Option A)
|
||||
- [ ] Engine báo `getLatencySamples()` + latency transport đo được (hằng số từ G0) → worklet align WRAP_FADE đúng processed-tail; không engine wrap, không playhead riêng.
|
||||
- [ ] Playhead: app dùng position = samples processed (engine derived, đã trừ latency) → bỏ freeze/snap (V40); rAF ε giữ cho UI, bỏ cho audio path.
|
||||
- [ ] Verify seam: loop selection + FX, 100 pass ghi âm so V42 — mục tiêu: crossfade không còn nghe được; nếu vẫn click → cân nhắc Option B (engine sở hữu dry loop buffer, đổi SHM contract — ngoài phạm vi).
|
||||
- Done khi: 100 pass không click với crossfade tối thiểu; playhead bám vị trí mẫu không freeze/snap.
|
||||
|
||||
### G4 — Bypass path thống nhất
|
||||
- [ ] Bypass = chain rỗng trong graph (không cần native WebAudio loop V42A, không dip V42).
|
||||
- [ ] Gỡ `nativeLoopSrcsRef`/dip gate khi engine active (giữ fallback WebAudio-only cho chế độ không bridge).
|
||||
- Done khi: bypass loop nghe sạch bằng native loop cũ; code path FX = bypass chỉ khác chain rỗng.
|
||||
|
||||
### G5 — MIDI + sections qua engine (tuỳ chọn, sau G3)
|
||||
- [ ] MIDI/SF items schedule trong engine (JUCE MidiBuffer) — hết flam rAF ε, hết MIDI re-schedule trễ.
|
||||
- [ ] SECTION items: sub-chain latency riêng (PDC per-section) — hiện đang dùng chung master PDC.
|
||||
- Done khi: drums beat 0 loop không flam; section sync đúng với main.
|
||||
|
||||
## 5. Rủi ro + giảm thiểu
|
||||
|
||||
| Rủi ro | Giảm thiểu |
|
||||
|---|---|
|
||||
| JUCE VST3 host khác hành vi VST3 SDK host hiện tại (preset/GUI) | Giữ SandboxVst3Host cho GUI/editor; engine JUCE chỉ xử lí audio path; verify preset_b64 load giống |
|
||||
| vcpkg JUCE nặng (build time) | Build riêng `juce_fx` target, không đụng main bridge; giữ main bridge deploy ổn định |
|
||||
| AudioProcessorGraph PDC delay lớn → playhead lệch nếu chain đổi giữa play | Rebuild graph khi chain đổi (stop/play như cũ); báo latency mới qua SHM |
|
||||
| WS round-trip vẫn tồn tại (Python trung gian) | Không bỏ — thay phần bridge là đủ cho latency; tối ưu WS sau nếu cần |
|
||||
| Split-brain 2 clock khi G3 (WebAudio master + engine wrap) | 1 master clock duy nhất (Option A); engine không wrap, không playhead riêng |
|
||||
| Param GUI (Sandbox instance) không tới processor JUCE | Mở rộng SET_PARAM ring: Sandbox implement IComponentHandler → performEdit/endEdit → `(slot, ParamID, normalizedValue)` → queue JUCE → inputParameterChanges trong processBlock; queue = lock-free SPSC (CẤM mutex trong processBlock — priority inversion → crackle); preset_b64 chỉ bulk/init (reload mỗi knob = chậm + reset state FX → click) |
|
||||
| Jitter WS/Python làm mất ý nghĩa PDC mẫu | G0 đo σ round-trip ≥10k block; gate: σ nhỏ → FILL = mean+k·σ; σ lớn → giảm jitter trước (relay WS→SHM trong bridge process, Python chỉ control) |
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
- JUCE (GPL/commercial — DAW nội bộ, GPLv3 repo OK; xác nhận license trước khi vendor).
|
||||
- Vào `native_bridge/CMakeLists.txt` + `vcpkg.json`: `juce` (port hiện có) hoặc submodule `JUCE@master` + `juce_add_gui_app` tối thiểu (chỉ audio, không cần GUI app — dùng `juce::juce_audio_processors` + `juce_audio_utils`).
|
||||
|
||||
## 7. Tiêu chí hoàn thành (definition of done)
|
||||
|
||||
1. Master chain FX chạy trong JUCE graph, latency báo chính xác (mẫu) → dry-path align zero-crossing.
|
||||
2. Loop selection + FX: 100 pass không click (so với V42 cần crossfade 256 mẫu để che).
|
||||
3. Playhead bám vị trí thật, không freeze/snap.
|
||||
4. Bypass = chain rỗng, không hack native loop/dip.
|
||||
5. Regression: probe matrix + driver (theo ROADMAP) pass; bridge cũ vẫn build được.
|
||||
|
||||
## 8. Ghi chú ponytail
|
||||
|
||||
- G0-G2 không đòi hỏi đổi JS/Python — hoàn đảo được, rủi ro thấp. G3 (Option A) cũng không đổi contract.
|
||||
- Nếu latency không phải vấn đề chính sau G0 (vd. click do nơi khác), dừng ở G2 — tiết kiệm G3-G5.
|
||||
- Option B (engine sở hữu transport/loop buffer) = project riêng, cần đổi JS/Python contract; chỉ mở khi Option A không đủ.
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
# LICENSE — SonicForgeStudio
|
||||
|
||||
## 1. Mã nguồn dự án
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 SonicForge Studio contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
File văn bản MIT đầy đủ: `LICENSE`.
|
||||
|
||||
## 2. Thành phần bên thứ ba
|
||||
|
||||
Kiểm tra ngày 2026-08-15 (verify từ PyPI/GitHub/wheel). Các thành phần giữ
|
||||
license gốc; chi tiết + attribution: `THIRD_PARTY_LICENSES.md`.
|
||||
|
||||
### 2.1. npm (`package.json`)
|
||||
| Thành phần | License |
|
||||
|---|---|
|
||||
| react / react-dom 19.x | MIT |
|
||||
| @babel/cli, @babel/core, @babel/preset-react | MIT |
|
||||
| jsdom | MIT |
|
||||
| @tauri-apps/cli 2.x (desktop shell) | Apache-2.0 OR MIT |
|
||||
|
||||
### 2.2. Python (`requirements.txt`)
|
||||
| Thành phần | License |
|
||||
|---|---|
|
||||
| fastapi | MIT |
|
||||
| uvicorn | BSD-3-Clause |
|
||||
| celery | BSD-3-Clause |
|
||||
| redis | MIT |
|
||||
| python-multipart | Apache-2.0 |
|
||||
| librosa | ISC |
|
||||
| pydub | MIT |
|
||||
| numpy | BSD-3-Clause (kèm code nhúng: 0BSD, MIT, Zlib, CC0-1.0) |
|
||||
| scipy | BSD-3-Clause (kèm OpenBLAS BSD-3, GCC runtime GPL-3.0-with-exception) |
|
||||
| soundfile | BSD-3-Clause |
|
||||
| jinja2 | BSD-3-Clause |
|
||||
| httpx | BSD-3-Clause |
|
||||
| jsonschema | MIT |
|
||||
| mido | MIT |
|
||||
| pyfluidsynth | MIT (wrapper — link FluidSynth **LGPL-2.1+**) |
|
||||
| sf2utils 1.0.0 | **LGPL-3.0+** (LICENSE.txt trong wheel; metadata PyPI ghi nhầm "GPLv3+") |
|
||||
|
||||
### 2.3. Native bridge (`native_bridge/`, C++)
|
||||
| Thành phần | License |
|
||||
|---|---|
|
||||
| fluidsynth (vcpkg) | LGPL-2.1+ |
|
||||
| vst3sdk (git submodule, `native_bridge/vst3sdk`) | MIT (© 2026 Steinberg Media Technologies GmbH) |
|
||||
| vestige (dự kiến thêm khi implement VST2, xem `DESIGN_VST2_BACKWARD_COMPAT.md`) | header mã nguồn mở — xác nhận lại file license gốc khi thêm vào repo |
|
||||
|
||||
### 2.4. Nhúng (`app/static`)
|
||||
| Thành phần | License |
|
||||
|---|---|
|
||||
| `css/tailwind.min.css` | MIT |
|
||||
| Lucide icons | ISC |
|
||||
| `js/vendor/libfluidsynth-2.3.0-sf3.js` + `.wasm` (FluidSynth Emscripten) | **LGPL-2.1+** |
|
||||
| `js/services/spessasynth_processor.min.js` | MIT — file chết, không include trong `index.html` |
|
||||
|
||||
### 2.5. Nội dung âm thanh (`app/storage`)
|
||||
| Thành phần | License | Trạng thái |
|
||||
|---|---|---|
|
||||
| "General MIDI SoundFont v3.0" — © 2006-2010 Rich "Weeds" Nagel | "Some rights reserved" (không rõ điều kiện) | ⚠️ cần attribution hoặc thay SF2 license rõ |
|
||||
| SGM-V2.01 | Freeware — hạn chế redistribution | ⚠️ không nhúng vào bản phân phối |
|
||||
|
||||
## 3. Lưu ý pháp lý khi phân phối
|
||||
|
||||
1. **sf2utils (LGPL-3.0+)** — dùng runtime (`app/core/soundfont_inspector.py`).
|
||||
LGPL cho phép app MIT dùng nếu giữ notice + (khi phân phối binary) cung cấp
|
||||
khả năng relink/object tương ứng.
|
||||
2. **FluidSynth (LGPL-2.1+)** — vừa nhúng Emscripten (`app/static`) vừa link
|
||||
tĩnh qua vcpkg (`native_bridge`). Bắt buộc: giữ license notice, cung cấp
|
||||
link/object relinkable khi phân phối bản build.
|
||||
4. **vst3sdk (MIT)** — tương thích hoàn toàn với dự án MIT.
|
||||
5. **VST2 SDK của Steinberg** — discontinued, license không cấp mới → hướng
|
||||
dùng **vestige** (header mã nguồn mở) theo `DESIGN_VST2_BACKWARD_COMPAT.md`,
|
||||
không nhúng VST2 SDK chính hãng.
|
||||
6. **SoundFont** — SGM-V2.01 không đưa vào bản phân phối; SF2 Rich Nagel cần
|
||||
attribution hoặc thay thế.
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
# PLAN — Phương án A: Engine xử lý toàn chain (chuẩn DAW)
|
||||
|
||||
Repo: C:/Users/locpham/SonicForgeStudio (branch standalone-shm-bridge, HEAD 5ad2c68)
|
||||
|
||||
## 0. Chuẩn DAW phải tuân thủ (KHÔNG bỏ qua)
|
||||
1. **Track insert chain serial đúng thứ tự UI**: builtin + VST3 xen kẽ đúng vị trí
|
||||
người dùng kéo; slot bypass chỉ bỏ qua slot đó, không đổi thứ tự phần còn lại.
|
||||
2. **Track gain/pan sau track chain**, trước khi sum vào master bus.
|
||||
3. **Master insert chain serial đúng thứ tự UI**, chạy trên mix tổng.
|
||||
4. **Master fader là gain cuối cùng** trước metering/hardware out (sau master chain).
|
||||
5. **Live = Export**: cùng 1 engine, cùng 1 chain descriptor, cùng 1 code DSP.
|
||||
6. **PDC (Plugin Delay Compensation)**: plugin khai latency → track được align
|
||||
(đã có sườn `_track_latency_samples` render_engine.py:286; live chưa có).
|
||||
7. **Gain staging rõ**: track input → chain → gain/pan → sum → master chain → fader.
|
||||
8. **Mono/Stereo**: SF/mono track được pad thành stereo tại input engine; mọi xử
|
||||
lý stereo 2ch (bridge hiện đã stereo).
|
||||
|
||||
## 1. Kiến trúc đích
|
||||
WebAudio chỉ còn I/O shim; mọi DSP (builtin + VST3) chạy trong `fx_vst_bridge`:
|
||||
|
||||
```
|
||||
track instrument (SF/audio-clip) ─► [worklet track: capture raw]
|
||||
│ WS/SHM
|
||||
▼
|
||||
fx_vst_bridge: TRACK CHAIN (builtin DSP C++ + VST3, đúng thứ tự) + PDC
|
||||
│ WS/SHM
|
||||
▼
|
||||
WebAudio: track gain/pan ─► masterBus.input (sum)
|
||||
│ [worklet master: capture sum]
|
||||
│ WS/SHM
|
||||
▼
|
||||
fx_vst_bridge: MASTER CHAIN (builtin DSP C++ + VST3, đúng thứ tự)
|
||||
│ WS/SHM
|
||||
▼
|
||||
WebAudio: master fader ─► analyser ─► ctx.destination
|
||||
```
|
||||
|
||||
- Chain descriptor DUY NHẤT `{type, path?, preset_b64?, params?, bypass}` cho cả
|
||||
track lẫn master, live lẫn offline.
|
||||
- Engine dùng chung 1 code path: realtime loop (SHM) và `--render-fx` (WAV)
|
||||
cùng `BuiltinFxChain` + `RealtimeFxChain`.
|
||||
|
||||
## 2. Hiện trạng (đã xác minh)
|
||||
- Bridge realtime `RealtimeFxChain` (RenderFxJob.cpp:1051-1190) CHỈ VST3,
|
||||
in-place, SEH-guarded, worker thread swap chain. Không builtin DSP.
|
||||
- `--render-fx` slots chỉ nhận `vst3` + `builtin`(gain/normalize) — native_render.py:219-234.
|
||||
- Frontend master: builtin chain chạy WebAudio (MASTER_MODULE_IO graph,
|
||||
app.jsx:1261-1310, rebuildMasteringGraph:1839); VST gom cuối qua fxRtStart:1682,
|
||||
worklet sau fader (output→worklet→analyser) — SAI: VST sau fader + order cố định.
|
||||
- Frontend track: builtin FX WebAudio (createFXModule:1450-1520, fxMods/sfMods
|
||||
app.jsx:23058-23104), VST splice SAU builtin (fxRtTrackSplice:23250-23270) —
|
||||
order cố định, bỏ qua vị trí UI.
|
||||
- Offline render_engine.py: track `fx_chain`(Python DSP eq/eqpro/comp/lim/exciter/
|
||||
rebalance — KHÔNG có imager/maximizer) → `vst_fx_chain`(bridge); master chỉ
|
||||
`master.fx_chain` = vstFxChain từ app.jsx:11409 → **builtin master chain
|
||||
imager/maximizer KHÔNG áp dụng offline** → live ≠ export.
|
||||
- Worklet sf-fx-realtime.js: gom 128→256 block, WS round-trip, outQueue drop >32.
|
||||
- Python builtin DSP: `_apply_builtin_fx_chain` (render_engine.py:228-260) —
|
||||
6 loại; JS WebAudio: 8 loại (MODULE_META app.jsx:13457-13468).
|
||||
- PDC: chỉ offline skeleton, plugin chưa báo latency (0).
|
||||
|
||||
## 3. Các bước
|
||||
|
||||
### Phase 1 — Chain descriptor + protocol hợp nhất
|
||||
- Đặc tả JSON: slot = `{type: vst3|eq|eqpro|imager|maximizer|compressor|limiter|
|
||||
exciter|rebalance, path?, preset_b64?, params?, bypass, active}`. `active:false`
|
||||
= tắt hẳn (UI), `bypass` = bỏ qua slot giữ nguyên order.
|
||||
- job.json realtime + render-fx nhận chain hợp nhất (thay `fx_chain` cũ).
|
||||
- Thêm protocol: `SET_PARAM {slot, key, value}` (realtime builtin automation),
|
||||
`REPORT_LATENCY {slot, samples}` → engine track align (PDC).
|
||||
- Update `_chain_json_for_bridge` fx_realtime.py:33 + native_render.py:230 + job.
|
||||
|
||||
#### Chain slot JSON (đặc tả chính thức — chuẩn cho live + offline)
|
||||
```json
|
||||
{
|
||||
"type": "vst3|eq|eqpro|imager|maximizer|compressor|limiter|exciter|rebalance",
|
||||
"path": "C:/.../plugin.vst3", // bắt buộc khi type=vst3
|
||||
"preset_b64": "", // vst3: preset nội bộ (base64)
|
||||
"params": { }, // builtin: tham số DSP (xem bảng dưới)
|
||||
"bypass": false, // bỏ qua slot khi process, GIỮ vị trí
|
||||
"active": true // false = slot bị tắt hẳn (UI xóa khỏi
|
||||
// chain payload trước khi gửi engine)
|
||||
}
|
||||
```
|
||||
Tham số builtin (khớp Python `_apply_*` + JS `MASTER_MODULE_IO`):
|
||||
| type | params |
|
||||
|---|---|
|
||||
| eq | `g1..g4` dB (lowshelf100/peaking800/peaking3200/highshelf10000) |
|
||||
| eqpro | `amount` 0-200, `bands[]` {type,freq,q,gain,active} (RBJ) |
|
||||
| imager | `w1..w4` width 4-band (JS gainLL/RL/LR/RR) |
|
||||
| maximizer | `boost_db`, `ceiling_db`, `soft_clip` bool |
|
||||
| compressor | `threshold` -60..0, `ratio` 1..20, `makeup` 0..12 |
|
||||
| limiter | `threshold` -24..0, `ceiling` |
|
||||
| exciter | `wet` 0..100, `hp_hz` |
|
||||
| rebalance | `g1..g4` dB 4-band (M/S) |
|
||||
|
||||
Control protocol (qua WS text frame, cùng socket audio):
|
||||
- Client → engine: `{"cmd":"set_param","slot":<idx>,"key":"threshold","value":-18.0}`
|
||||
- Bridge → engine → client: `{"cmd":"latency","slot":<idx>,"samples":1024}`
|
||||
- Engine giữ `pending_params[slot][key]` + `latencies[slot]` per-session; C++
|
||||
consume ở Phase 2.8 (SHM control ring).
|
||||
|
||||
### Phase 2 — Bridge C++: BuiltinFxChain (8 DSP)
|
||||
- File mới `native_bridge/src/BuiltinFxChain.cpp` + header, port từ Python
|
||||
numpy (`_apply_eq4/_apply_eqpro/_apply_compressor/_apply_limiter/_apply_exciter/
|
||||
_apply_rebalance`) VÀ JS WebAudio (imager/maximizer — MASTER_MODULE_IO graph
|
||||
app.jsx:1261-1310 + applyMasteringSettings:816-935) sang C++ stereo float.
|
||||
Khớp tham số: eq g1..g4 dB; eqpro bands(8, RBJ); compressor threshold/ratio/
|
||||
makeup; limiter threshold/ceiling; exciter wet/hp; rebalance 4-band gains;
|
||||
imager 4-band width; maximizer boost/ceiling/soft-clip.
|
||||
- `RealtimeFxChain::buildChain` + `RenderFxJob` slots: nhận builtin → chạy
|
||||
BuiltinFxChain trước VST3 theo đúng thứ tự array.
|
||||
- SEH-guard chung; bypass giữ vị trí.
|
||||
- Test golden: `native_bridge/tests/` — input WAV → C++ chain vs Python
|
||||
`_apply_builtin_fx_chain` same params → so SNR (mục tiêu > 60dB hoặc
|
||||
diff < 1e-4; biquad RMS khác nhau do floating — chấp nhận ngưỡng).
|
||||
Thêm case: chain xen kẽ builtin→vst3→builtin verify thứ tự (vst3 dummy delay).
|
||||
|
||||
### Phase 3 — Engine Python: render + realtime dùng chain hợp nhất
|
||||
- `render_engine.py`: track/master đọc chain hợp nhất từ session payload;
|
||||
`_apply_builtin_fx_chain` Python chỉ giữ làm fallback khi RENDER_ENGINE≠bridge
|
||||
(ponytail: xóa hẳn khi bridge bắt buộc). Bỏ bất đối xứng imager/maximizer.
|
||||
- `fx_realtime.py`: session nhận chain hợp nhất, chuyển SET_PARAM/REPORT_LATENCY;
|
||||
master + track session chung protocol.
|
||||
- PDC offline: đọc `latency_samples` từ slot → align (render_engine.py:333 đã có
|
||||
pre-scan — nối với REPORT_LATENCY).
|
||||
|
||||
### Phase 4 — Frontend: WebAudio chỉ I/O
|
||||
- Chain model: 1 mảng duy nhất `chain` (builtin + vst3 theo UI order). Bỏ tách
|
||||
`vstFxChain`/`vstBypass`/`vst_fx_bypass` (giữ backward: project cũ có
|
||||
vstFxChain → merge vào chain theo vị trí cũ: builtin → vst, đánh dấu migration).
|
||||
- Master: xóa MASTER_MODULE_IO graph + rebuildMasteringGraph node-web; thay 1
|
||||
worklet master (`masterBus.output` → worklet → fader gain → analyser → dest).
|
||||
Fader (masterBus.output) chuyển SAU worklet — đúng chuẩn #4. applyMastering
|
||||
param thay bằng SET_PARAM push.
|
||||
- Track: `rebuildTrackFxGraph` bỏ fxMods/sfMods WebAudio builtin (giữ
|
||||
gain/panner/analyser); 1 worklet track ở chain input, engine trả chain output
|
||||
→ legacy chorus/reverb (nếu còn) → gain/pan → masterBus.input.
|
||||
- `fxRtChain()`/`fxRtTrackChain()` đọc chain hợp nhất; `chainKey` so toàn chain
|
||||
(không chỉ VST) → restart session khi đổi thứ tự/param builtin.
|
||||
- Worklet: thêm `SET_PARAM` forward + queue drop → adaptive (ponytail), giữ cap.
|
||||
|
||||
### Phase 5 — Verify (chuẩn DAW checklist)
|
||||
1. Build frontend `node build.mjs` OK.
|
||||
2. pytest tests/ (render_engine golden so bridge; plugin_api; fx_realtime).
|
||||
3. Golden: export WAV offline vs live capture (record masterBus) — diff < ngưỡng
|
||||
(live=export, chuẩn #5). Có test g2_bridge_smoke mẫu.
|
||||
4. Order test: chain [eq, vst3-delay, limiter] — verify delay chỉ áp sau EQ
|
||||
(impulse response), không gom cuối.
|
||||
5. Fader test: master chain active, fader -6dB → đỉnh giảm đúng, chain input
|
||||
không đổi (chuẩn #4).
|
||||
6. PDC: plugin latency 1024 samples → track bù, xuyên pha biến mất.
|
||||
7. Smoke: server + /capabilities + 1 render thật (master chain imager+maximizer
|
||||
+ vst3) — trước đây offline thiếu → giờ có.
|
||||
8. Commit + push standalone-shm-bridge.
|
||||
|
||||
## 4. Rủi ro / quyết định
|
||||
- **Latency live**: +1 round-trip track + 1 master (~2-4 block mỗi hop). Chấp
|
||||
nhận (bản chất bridge); giữ queue cap, giảm blockMs.
|
||||
- **Param automation realtime cho VST3** (preset thay đổi qua GUI): hiện chỉ
|
||||
preset_b64 lúc start — giữ nguyên phase này (ponytail: MIDI CC→param sau).
|
||||
- **Migration project cũ**: schema vẫn nhận vstFxChain (đọc cũ) nhưng ghi chain
|
||||
hợp nhất; không phá project cũ.
|
||||
- **Không bỏ**: bypass giữ order, PDC, fader cuối, live=export, gain staging.
|
||||
@@ -0,0 +1,214 @@
|
||||
# PLAN: Masterbus + FX Rack hỗ trợ chèn VST FX (Ozone, Scaler) — xử lý âm thanh
|
||||
|
||||
Ngày: 2026-08-15
|
||||
Trạng thái: HOÀN THÀNH Phase 0-4 (2026-08-18) — code + test pass.
|
||||
Kế thừa nền: `PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md` HOÀN THÀNH (Phase 0-5,
|
||||
commit `dc331f3`) — pedalboard GPL-3.0 đã gỡ, `RENDER_ENGINE` mặc định
|
||||
`"bridge"`, `native_bridge` (MIT) render VSTi offline qua
|
||||
`--render <job.json> --out <wav>`; tests 114 pass / 0 skip.
|
||||
|
||||
## 1. Mục tiêu
|
||||
|
||||
Chèn **VST FX thật** (Ozone mastering chain, Scaler, …) vào 2 tầng xử lý âm
|
||||
thanh:
|
||||
|
||||
- **Masterbus**: chain FX áp lên toàn bộ mix (mastering).
|
||||
- **FX Rack per-track**: chain FX áp lên từng track (insert).
|
||||
|
||||
Offline export chạy qua `native_bridge` (giữ nguyên luồng RENDER_ENGINE hiện
|
||||
tại). Realtime trong browser **KHÔNG chạy được VST** → giữ WebAudio builtin,
|
||||
VST FX dùng quick_render preview (âm thật = âm export, như cơ chế preset đang có).
|
||||
|
||||
## 2. Hiện trạng (cần mở rộng)
|
||||
|
||||
| Tầng | Hiện tại | Hạn chế |
|
||||
|---|---|---|
|
||||
| Realtime browser (`app/static/js/app.jsx`) | FX chain WebAudio per-track (chorus/reverb), mastering chain Ozone-style (route dry/mastering, `createMasteringRoute`) | WebAudio node — KHÔNG chạy được binary `.vst3`; data model chỉ có `fxChain` + mastering toggle, chưa có slot plugin |
|
||||
| Offline export (`app/core/render_engine.py`) | Track FX: `fx_type=="chorus"|"reverb"` fallback scipy/numpy; master bus = sum track buffers + normalize peak (`max_peak`) | Không VST; master chỉ normalize, không chain; `fxChain` WebAudio không tái tạo offline |
|
||||
| Bridge (`native_bridge/src/*`) | Chỉ instrument host: MIDI → audio (`NativeInstrumentEngine`, VST3/SF2/SFZ) | Chưa có audio-effect processing path; `RenderJob.cpp` chỉ nhận notes, không nhận input WAV |
|
||||
| Scan plugin (`app/core/vst_engine.py`) | Quét `.vst3`/`.so`/`.dll`, list vst_instruments | Không phân loại instrument vs effect |
|
||||
|
||||
Tham chiếu doc cũ: `md/45_MASTERING_MODULE.md` (spec WebAudio Ozone) — giữ
|
||||
nguyên, chỉ bổ sung VST path. `app.jsx` tham chiếu `mastering_expand.md` +
|
||||
`unified_fx_rack_panel.md` — 2 file này không tồn tại → gỡ tham chiếu hoặc viết
|
||||
mới trong Phase 3.
|
||||
|
||||
## 3. Kiến trúc đích
|
||||
|
||||
### 3.1. Bridge: FX render mode (C++, MIT)
|
||||
|
||||
Thêm entrypoint CLI song song mode instrument:
|
||||
|
||||
```
|
||||
daw_vst_bridge(.exe) --render-fx <job.json> --in <input.wav> --out <output.wav>
|
||||
```
|
||||
|
||||
job.json (mở rộng format hiện có):
|
||||
```json
|
||||
{
|
||||
"sample_rate": 44100,
|
||||
"block_size": 512,
|
||||
"fx_chain": [
|
||||
{ "type": "vst3", "path": "C:/.../Ozone 11.vst3", "preset_b64": "...", "bypass": false },
|
||||
{ "type": "builtin", "id": "normalize", "params": { "peak": 0.95 } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Luồng:
|
||||
1. Đọc input WAV (16/24/32-bit, mono/stereo → convert stereo float) — thêm WAV
|
||||
reader (WAV writer đã có).
|
||||
2. Với mỗi slot trong `fx_chain`, process toàn bộ buffer qua
|
||||
`NativeFxEngine::processAudio(audio_in, audio_out)` — audio-input,
|
||||
KHÔNG MIDI. VST3 effect: load `plugin_path`, `loadSerializedState` từ
|
||||
`preset_b64` (cùng cơ chế preset đã có), `processBlock` từng block;
|
||||
`bypass=true` → copy thẳng.
|
||||
3. `builtin` = fallback không cần VST (normalize, gain) — dùng chung cho
|
||||
Docker headless.
|
||||
4. Ghi WAV stereo 16-bit. Exit code như mode instrument (0 ok, 1 job error,
|
||||
2 load fail, 3 crash — SEH guard như `SandboxVst3Host`).
|
||||
|
||||
Lưu ý kỹ thuật:
|
||||
- Phân biệt class category VST3: instrument (`kInstrument`) vs effect
|
||||
(`kAudioEffect`) — dùng `getClassInfo`/category string để host đúng
|
||||
interface (audio-in/audio-out thay vì MIDI-in).
|
||||
- VST2 FX (`type:"vst2"`) thêm sau nếu cần; V1 chỉ VST3 + builtin.
|
||||
- Linux bridge chỉ SF2/SFZ (VST3 hosting Windows-only) → FX VST chỉ
|
||||
Windows/desktop; Docker fallback builtin. Capabilities API báo đúng
|
||||
(`features.vst_fx_render`).
|
||||
|
||||
### 3.2. Python client (`app/core/native_render.py` — mở rộng)
|
||||
|
||||
```python
|
||||
render_fx_chain(input_wav: str, fx_chain: list[dict], sample_rate: int) -> str
|
||||
```
|
||||
Spawn bridge `--render-fx`, trả path WAV. Chịu trách nhiệm: resolve plugin
|
||||
path, base64 encode preset file, timeout, rc → exception. Không import
|
||||
pedalboard.
|
||||
|
||||
### 3.3. Masterbus (offline)
|
||||
|
||||
Session thêm key `master`:
|
||||
```json
|
||||
"master": { "fx_chain": [...], "volume_db": 0, "mute": false, "bypass": false }
|
||||
```
|
||||
`render_project()`:
|
||||
1. `master_buffer = render_session_container(...)` (giữ nguyên).
|
||||
2. Nếu `master.bypass` false → `master_buffer = render_fx_chain(wav, master.fx_chain)`.
|
||||
3. Áp `volume_db`; giữ normalize-peak làm builtin slot cuối (hoặc bỏ khi chain
|
||||
có master limiter — config theo chain, mặc định giữ).
|
||||
|
||||
Session cũ không có `master` → skip, không vỡ (backward compatible).
|
||||
|
||||
### 3.4. FX Rack per-track (offline)
|
||||
|
||||
Track thêm key `fx_chain` (mảng insert slots, thay thế `fx_type` string):
|
||||
```json
|
||||
"fx_chain": [
|
||||
{ "plugin_id": "ozone", "path": "...", "preset_b64": "...", "bypass": false }
|
||||
]
|
||||
```
|
||||
Trong `render_session_container`, sau khi mix audio track (trước volume/pan):
|
||||
nếu `fx_chain` có VST slot → render track buffer qua bridge trước khi cộng vào
|
||||
session. `fx_type` legacy ("chorus"/"reverb") giữ nguyên fallback scipy — cùng
|
||||
lúc chỉ dùng một; ưu tiên `fx_chain` nếu có.
|
||||
|
||||
### 3.5. Scan + phân loại FX (`app/core/vst_engine.py`)
|
||||
|
||||
- Thêm `list_fx()`: quét cùng thư mục, đọc category VST3 để tách effect khỏi
|
||||
instrument (instrument = class `kInstrument`; effect = `kAudioEffect`/`kFx`).
|
||||
- Không có SDK VST3 ở Python? → dùng chính bridge: thêm mode `--scan <dir>`
|
||||
(C++ dùng VST3 `getClassInfo` đã có) trả JSON `[{path, is_fx, name}]`. Bridge
|
||||
scan là nguồn chân lý, Python chỉ cache.
|
||||
|
||||
### 3.6. API + UI (V1 tối thiểu)
|
||||
|
||||
- `POST /api/v1/plugins/fx-render` `{ input_wav_url, fx_chain }` → WAV
|
||||
(dùng cho preview + export thử nghiệm; pattern giống `/api/v1/plugins/midi-render`).
|
||||
- `GET /api/v1/plugins/fx` → danh sách FX (từ scan bridge).
|
||||
- UI: FX Rack panel per-track + masterbus — danh sách slot, chọn plugin,
|
||||
bypass toggle, order (up/down), nút "Mở trong Carla / Chỉnh preset" → save
|
||||
`.vstpreset` (cơ chế CARLA_BRIDGE có sẵn, `md/52_CARLA_BRIDGE.md`).
|
||||
- Preset-based V1: state plugin = `.vstpreset` file, **chưa có param
|
||||
automation** (khớp giới hạn preset bridge hiện tại). Dry/wet: bỏ qua V1
|
||||
(Ozone/Scaler xử lý trong chain), thêm sau nếu cần.
|
||||
|
||||
### 3.7. Realtime
|
||||
|
||||
- Browser không chạy VST: giữ nguyên WebAudio builtin FX + mastering chain.
|
||||
- Track/masterbus có VST slot → khi play, nếu slot không bypass: dùng
|
||||
quick_render preview (đã có `preview_mode: "quick_render"` — render clip ngắn
|
||||
qua bridge → WAV → phát), ghi chú "preview = âm export". Nếu chỉ builtin →
|
||||
WebAudio trực tiếp.
|
||||
- Chưa realtime VST streaming (Tauri SHM) trong phạm vi plan này — ghi là
|
||||
future work.
|
||||
|
||||
### 3.8. Scaler — OPEN QUESTION (quyết định trong Phase 1)
|
||||
|
||||
Scaler 2 là **MIDI/chord tool**, không phải audio-effect processor:
|
||||
- Chạy như VSTi insert (đường VSTi hiện có): nhận MIDI → output audio cho
|
||||
chord — không cần FX path mới, dùng `render_session_container` instrument
|
||||
nhánh có sẵn.
|
||||
- Hoặc chỉnh chord offline rồi render note events — cần UI riêng (ngoài plan).
|
||||
→ **QUYẾT ĐỊNH (Phase 1, 2026-08-18): Scaler = VSTi**, không vào FX Rack. Xác
|
||||
nhận bằng bridge `--scan` thật trên `C:/Program Files/Common Files/VST3` (86
|
||||
plugins): `Scaler 2` → `is_fx=false` (instrument), `ScalerAudio 2` → `is_fx=true`
|
||||
(effect — dùng được trong FX Rack/masterbus). Nếu user muốn Scaler trong FX
|
||||
Rack → Phase sau như audio-FX bọc ngoài (unlikely).
|
||||
|
||||
## 4. Phases
|
||||
|
||||
| Phase | Nội dung | Deliverable | Trạng thái |
|
||||
|---|---|---|---|
|
||||
| 0 | Bridge `--render-fx`: WAV reader, NativeFxEngine VST3 effect (audio-in/out), builtin normalize/gain, SEH guard | CLI render FX demo: WAV in → WAV out, test với Ozone | ✅ test `/c/tmp/test_fx_phase0.py` 7/7 (gain0 bit-exact, -6dB RMS/2, normalize, Ozone 11 Maximizer khác input 88172 samples, scan 86 plugins); banner stdout→stderr `45a7bde` |
|
||||
| 1 | Bridge `--scan`: phân loại instrument/effect; Python `native_render.render_fx_chain` + `vst_engine.list_fx` | `/api/v1/plugins/fx` + `/api/v1/plugins/fx-render` | ✅ code sẵn + tests mới `test_render_fx_chain_builtin_gain_e2e`, `test_fx_render_builtin_gain` |
|
||||
| 2 | Masterbus: session key `master`, render_project chạy chain, backward compatible | Export session có mastering VST | ✅ `render_project` (render_engine.py) + `test_render_project_master_fx_chain_gain_via_bridge` |
|
||||
| 3 | FX Rack per-track: track `fx_chain`, render_engine integrate; UI panel (slot/bypass/order/preset upload) | Track VST FX + UI | ✅ backend `vst_fx_chain` + UI FX Rack (`__openFxRack`, add/change/bypass/preset upload) + `test_render_track_fx_chain_gain_via_bridge` |
|
||||
| 4 | Realtime preview quick_render cho slot VST; gỡ/làm rõ tham chiếu doc thiếu (`mastering_expand.md`) | Preview thật; doc sạch | ✅ "Nghe thử" = `__renderProjectPreview` → renderProject offline (âm thật = âm export); `md/mastering_expand.md` + `md/unified_fx_rack_panel.md` TỒN TẠI (không cần gỡ) |
|
||||
|
||||
Mỗi phase kết thúc = commit + test pass. Phase 0-1 có thể merge (cùng chạm
|
||||
bridge).
|
||||
|
||||
## 5. Test
|
||||
|
||||
- Bridge: self-check WAV in→out (identity khi bypass; gain builtin đúng dB);
|
||||
`--scan` trên folder có 1 instrument + 1 effect giả → đúng nhãn.
|
||||
- Python: unit test `render_fx_chain` với builtin chain (không cần VST) —
|
||||
input wav → output wav tồn tại, đúng sr; master key vắng → output không đổi
|
||||
so với trước (regression).
|
||||
- E2E (thủ công, máy có Ozone): export session có master Ozone chain → nghe
|
||||
khác rõ so với không chain, không clip.
|
||||
- Chạy full test suite hiện có — không phá 114 tests.
|
||||
|
||||
## 6. Kết quả phân tích Ozone -Inf input (2026-08-18)
|
||||
|
||||
Verify code toàn chuỗi realtime master VST3 FX chain — **đã đủ, không thiếu setActive/setProcessing**:
|
||||
|
||||
| Chặng | File | Trạng thái |
|
||||
|---|---|---|
|
||||
| Frontend gửi chain | `app/static/js/app.jsx:12732-12734` + `app.precompiled.js:803` — `useEffect` trên `ozState.vstFxChain` → `NativeBridgeService.setFxChain(JSON)` | ✅ Có |
|
||||
| Rust | `src-tauri/src/lib.rs:862-872` `set_fx_chain` → SHM control `type=6` | ✅ Có |
|
||||
| C++ nhận | `main.cpp:1567-1568` `type==6` → `g_fxChain->setChain(json, sr, block)` | ✅ Có |
|
||||
| Load plugin | `RenderFxJob.cpp` `vst3FxLoadInner` (397-470): activateBus toàn bộ, `setupProcessing(kRealtime,kSample32)`, `setActive(true)` (454), `setProcessing(true)` (455), `processData.prepare` | ✅ Đủ |
|
||||
| Process | `RealtimeFxChain::process` (audio thread, in-place trên ring master, SEH quanh từng plugin) + `Vst3Fx::processAudio` (copy input → zero output → process → copy ra) | ✅ Đúng |
|
||||
| processContext | tempo 120, sampleRate, kPlaying, projectTimeSamples tăng mỗi block | ✅ Có |
|
||||
|
||||
→ 4 giả thuyết user: #3 (thiếu setActive/setProcessing) **SAI** — đã có đủ.
|
||||
**Nguyên nhân -Inf thực tế: audio DAW không chảy qua bridge.** Chain chạy trên
|
||||
`shmIPC->ringLeft/ringRight[slot]` — master mix CHỈ chứa audio từ instrument
|
||||
trong bridge. DAW master bus (WebAudio graph: audio track, SonicSF fallback,
|
||||
non-bridge source) KHÔNG vào SHM → Ozone nhận silence → input -Inf.
|
||||
→ Hướng sửa: (a) pipe DAW master bus audio vào bridge (SHM audio-in mới) để
|
||||
chain xử lý đúng master thật, hoặc (b) chạy chain trên WebAudio graph; hoặc
|
||||
(c) chỉ dùng bridge chain cho phần bridge-instrument. **Kết luận (2026-08-18):
|
||||
giữ (c)** — chain hiện tại xử lý đúng audio bridge-instrument; DAW master bus
|
||||
(a) là hướng dài hạn ngoài plan (cần SHM audio-in mới). Realtime Ozone trên
|
||||
toàn master = future work; offline export đã chạy chain đúng (Phase 2).
|
||||
|
||||
## 7. License
|
||||
|
||||
- Ozone (iZotope/Native Instruments) + Scaler (PluginBoutique/Scale Software):
|
||||
plugin **thương mại** — KHÔNG nhúng, chỉ host bằng trình load của
|
||||
`native_bridge` (MIT). Không bundle binary plugin vào repo.
|
||||
- `native_bridge` giữ MIT; code mới (WAV reader, NativeFxEngine) cùng license.
|
||||
- Không quay lại pedalboard (GPL-3.0) dưới mọi hình thức.
|
||||
@@ -0,0 +1,157 @@
|
||||
# PLAN: Nhúng native VST FX GUI vào Mastering Panel + FX Rack Panel
|
||||
|
||||
Ngày: 2026-08-17
|
||||
Trạng thái: LINUX PHẦN HOÀN THÀNH (0L1, 0L2, 1L1, 2L1, 3L1, 4L1) — commit
|
||||
`<pending>`; WINDOWS PHẦN CÒN LẠI (0W1, 0W2, 1W1, 2W1, 3W1, 4W1) — làm theo
|
||||
`WALKTHROUGH_WINDOWS.md`. Tách task: `TASKS_LINUX_WINDOWS.md`.
|
||||
Kế thừa: `PLAN_MASTERBUS_FX_RACK_VST.md` Phase 0-3 HOÀN THÀNH (commit
|
||||
`b261eb0` render FX, `38f4cbd` mở native VST GUI dạng cửa sổ rời qua
|
||||
`--open-fx-gui`).
|
||||
|
||||
## 1. Mục tiêu
|
||||
|
||||
Hoàn thiện vòng đời VST FX trong **MASTERING PANEL** (`MasteringModal`) và
|
||||
**FX RACK PANEL** (`FXRackModal`):
|
||||
|
||||
1. **Nút X trên slot plugin** → xóa plugin khỏi ô slot.
|
||||
→ **ĐÃ CÓ** (`removeVst`/`removeMasterVst` + `<button>` X trong `vstRow`,
|
||||
`app/static/js/app.jsx:11757`). Chỉ xác minh, không code lại.
|
||||
2. **Nút +** → thêm VST trực tiếp vào panel VÀ **load native VST GUI ngay
|
||||
trong panel** — thay vì cửa sổ rời như hiện tại (`SonicAPI.openFxGui` →
|
||||
bridge `--open-fx-gui` mở window 800x600 tách biệt).
|
||||
3. **Đóng GUI** → panel trở về graph GUI. **Click slot VST đã load** → mở lại
|
||||
GUI nhúng, **thay thế vùng graph GUI của panel** bằng native VST GUI ở đó.
|
||||
|
||||
## 2. Hiện trạng
|
||||
|
||||
| Hạng mục | Hiện tại | Delta cần |
|
||||
|---|---|---|
|
||||
| Slot VST masterbus | `masterVstChain` (`vstFxChain`) + `vstRow` | giữ |
|
||||
| Slot VST per-track | `track.vstFxChain` + `vstRow` | giữ |
|
||||
| Nút X xóa slot | có (`removeV*`) | xác minh |
|
||||
| Nút + thêm + mở GUI | có (`addVst`/`addMasterVst` → `openVstGui`/`openMasterVstGui`) | **đổi từ cửa sổ rời → GUI nhúng trong panel** |
|
||||
| GUI mở | bridge `--open-fx-gui` (RenderFxJob.cpp:889+, `FxGuiWndProc`, `fxGuiAttachSafe`, IPlugView attach HWND) | giữ làm nền, thêm capture + input relay |
|
||||
| Graph GUI của panel | MasteringModal: vùng canvas EQ/imager/meters (`eqCanvasRef`…); FXRackModal: `scopeCanvasRef`/`eqCurveRef` | vùng này bị thay thế khi GUI mở |
|
||||
| Tauri | src-tauri v2 có, app cũng chạy qua LAN browser | GUI nhúng phải chạy cả 2 môi trường → không dùng overlay native window |
|
||||
|
||||
**Quyết định kiến trúc:** app là web (browser/Tauri webview) → không thể nhúng
|
||||
HWND trực tiếp. Dùng **frame-capture + input relay** (kiểu VNC cho plugin
|
||||
window): bridge giữ native editor window, chụp bitmap → HTTP, frontend hiển
|
||||
thị trong panel và gửi pointer/key về bridge → `SendMessage` tới HWND plugin.
|
||||
|
||||
## 3. Kiến trúc đích
|
||||
|
||||
### 3.1. Bridge: mode `--fx-gui <job.json>` (C++, Windows, MIT)
|
||||
|
||||
Mở rộng từ `--open-fx-gui` hiện có (giữ nguyên phần attach IPlugView):
|
||||
|
||||
- Sau khi attach view (đã có `fxGuiAttachSafe`), chạy **HTTP server mini**
|
||||
(port ngẫu nhiên, ghi vào job output/stdout để Python đọc):
|
||||
- `GET /frame` → PNG/JPEG chụp từ HWND plugin bằng `BitBlt` (fallback
|
||||
`PrintWindow` với `PW_RENDERFULLCONTENT`) — trả `image/jpeg`, chất lượng
|
||||
~70 để giảm băng thông LAN.
|
||||
- `POST /input` `{ type: "mousedown|mousemove|mouseup|wheel|key", x, y,
|
||||
button, deltaY, keyCode }` → `PostMessage(hwnd, WM_...)` theo tọa độ đã
|
||||
scale (window plugin có thể lớn hơn vùng hiển thị → scale client rect).
|
||||
- `POST /close` → detach view, thoát tiến trình (exit 0).
|
||||
- `GET /ping` → alive check.
|
||||
- Giữ nguyên SEH guard + registry chống mở 2 GUI (`_FX_GUI_PROCESSES` phía
|
||||
Python).
|
||||
- Vị trí: `native_bridge/src/RenderFxJob.cpp` (hoặc file mới
|
||||
`FxGuiServer.cpp` cạnh nó — theo kích thước, tách nếu >300 dòng).
|
||||
|
||||
### 3.2. Python API (`app/api/v1/plugins.py` — mở rộng `open_fx_gui`)
|
||||
|
||||
- `POST /api/v1/plugins/fx-gui/open` `{ path, name }` → spawn bridge
|
||||
`--fx-gui`, đọc port, trả `{ success, embed_url: "http://127.0.0.1:<port>",
|
||||
already_running }`. Registry `_FX_GUI_PROCESSES` như hiện có.
|
||||
- `POST /api/v1/plugins/fx-gui/close` `{ path }` → kill process (đã có luồng
|
||||
cleanup ở `_register_fx_gui_process`).
|
||||
- Không cần proxy qua FastAPI: frontend gọi thẳng `embed_url` (CORS:
|
||||
`plugins.py` đã có middleware — kiểm tra thêm `Access-Control-Allow-Origin`
|
||||
cho port động).
|
||||
|
||||
### 3.3. UI — `app/static/js/app.jsx` (dùng chung cho 2 panel qua `vstRow`)
|
||||
|
||||
- `vstRow` thêm state per-slot: `guiOpen: { url } | null` (state nằm ở panel
|
||||
component, không phải trong `vstRow` — truyền qua props).
|
||||
- Render slot: nút GUI hiện tại → **mở/đóng toggle**: nếu `guiOpen` null →
|
||||
gọi `openFxGuiEmbed` (API mới); nếu đang mở → hiển thị vùng embed.
|
||||
- Vùng embed: `<div className="...">` chứa `<img src={url + '/frame'}>`
|
||||
refresh bằng `setInterval` ~100ms (10fps), overlay pointer handlers
|
||||
(`onMouseDown/Move/Up`, `onWheel`) → `fetch(url + '/input', POST)` với tọa độ
|
||||
relative tính theo bounding rect; keydown/keyup trên container.
|
||||
- Trong `MasteringModal`: vùng này **thay thế** vùng graph (khu vực
|
||||
canvas EQ/imager — xác định container chính xác lúc implement; ẩn bằng
|
||||
`hidden` class khi GUI mở, hiện lại khi close).
|
||||
- Trong `FXRackModal`: thay thế vùng scope/eq canvas.
|
||||
- Click vào **tên VST đã load trong slot** (span tên hoặc chính slot) khi GUI
|
||||
đang đóng → mở lại embed (re-attach qua `/frame` poll, ping trước).
|
||||
- Nút đóng GUI: nút X riêng trên vùng embed (hoặc nút GUI toggle) → gọi
|
||||
`/close`, dừng interval, trả graph về.
|
||||
- Xóa slot (X) khi GUI đang mở → cũng phải đóng GUI (gọi `/close`).
|
||||
- Nếu `/ping` fail (bridge chết ngoài ý muốn) → tự trả về graph + toast.
|
||||
|
||||
### 3.4. Không đổi
|
||||
|
||||
- Offline render (`--render-fx`), preset upload, Carla bridge, `--scan` — giữ
|
||||
nguyên. GUI nhúng chỉ là hiển thị/điều khiển, không đổi data model
|
||||
(`vstFxChain` giữ nguyên shape).
|
||||
|
||||
## 4. Phases
|
||||
|
||||
| Phase | Nội dung | Deliverable | Test |
|
||||
|---|---|---|---|
|
||||
| 0 | Bridge `--fx-gui`: HTTP server + BitBlt/PrintWindow capture + input relay | mở GUI Ozone trong browser tab thủ công qua URL | self-check: `/frame` trả JPEG kích thước khớp; `/input` click đúng vị trí |
|
||||
| 1 | Python: `open` trả `embed_url`, registry, close, CORS | curl: open → frame 200 | unit: process spawn/cleanup |
|
||||
| 2 | MasteringModal: embed + swap graph↔GUI + click mở lại | UI mastering hoàn chỉnh | E2E thủ công |
|
||||
| 3 | FXRackModal: tương tự | UI fx rack hoàn chỉnh | E2E thủ công |
|
||||
| 4 | Dọn: toast lỗi, cleanup interval, docs | commit | full test suite hiện có không vỡ |
|
||||
|
||||
Phase 0-1 có thể merge (cùng chạm bridge + plugins.py).
|
||||
|
||||
## 5. Rủi ro / giới hạn
|
||||
|
||||
- Plugin dùng layered window / DirectX surface → `PrintWindow` ra đen: fallback
|
||||
capture child control HWND hoặc dùng `BitBlt` từ `GetDC(NULL)` vùng rect —
|
||||
xử lý theo plugin cụ thể (Ozone chuẩn GDI, khả năng cao OK).
|
||||
- Input tọa độ: scale theo client rect; plugin bắt absolute tọa độ cần
|
||||
`WM_MOUSEMOVE` trước click (gửi mousemove kèm).
|
||||
- FPS ~10-15 đủ chỉnh thông số; không phải realtime video — ghi chú UI.
|
||||
- Chỉ Windows desktop có VST GUI (Linux bridge SF2/SFZ — giữ nguyên).
|
||||
- Keyboard input: chỉ forward khi panel đang focus, tránh nuốt phím toàn app.
|
||||
|
||||
## 6. License
|
||||
|
||||
- Không đổi: `native_bridge` MIT, không nhúng binary plugin, không quay lại
|
||||
pedalboard.
|
||||
|
||||
## 7. Ghi chú triển khai (Linux — đã làm)
|
||||
|
||||
- `native_bridge/src/FxGuiServer.cpp` (mới, ~316 dòng): HTTP server Winsock trên
|
||||
worker thread; `/frame` = `BitBlt` + fallback `PrintWindow PW_RENDERFULLCONTENT`
|
||||
khi ảnh toàn đen, JPEG quality 70 qua libjpeg-turbo (`jpeg_mem_dest`);
|
||||
`/input` relay `PostMessage` (mousedown/move/up, wheel deltaY*120, keydown/up,
|
||||
coords scale client rect); `/close` stop flag + `WM_CLOSE`; `GET /404`.
|
||||
`int fxGuiServerLoop(HWND)` chạy message pump STA, join HTTP thread.
|
||||
- `RenderFxJob.cpp`: tách `fxGuiSetup()` dùng chung; `run_open_fx_gui` giữ hành
|
||||
vi cũ; thêm `run_fx_gui_server(jobPath)` (offscreen, userData=0);
|
||||
stub `#else !_WIN32` trả 1. `main.cpp`: dispatch `--fx-gui <job.json>`.
|
||||
- `vcpkg.json` thêm `libjpeg-turbo`; `CMakeLists.txt` chỉ build FxGuiServer khi
|
||||
WIN32 + `find_package(JPEG REQUIRED)` + `JPEG::JPEG`.
|
||||
- `app/api/v1/plugins.py`: `FxGuiRequest.embed`, model `FxGuiCloseRequest`,
|
||||
`open_fx_gui` spawn `--fx-gui` đọc `SF_FXGUI_PORT=` từ stdout → `embed_url`,
|
||||
trả `{success, started, already_running, embed_url, cmd}`; thêm
|
||||
`POST /api/v1/plugins/fx-gui/close` (terminate → wait 5s → kill → prune).
|
||||
CORS `allow_origins=["*"]` + bridge tự gửi ACAO `*`.
|
||||
- `app/static/js/app.jsx`: component `VstGuiEmbed` (module scope, dùng chung 2
|
||||
panel) — img refresh 100ms, ping 2s, relay pointer/wheel/key với tọa độ scale,
|
||||
nút "✕ Đóng GUI" → `/close`. MasteringModal + FXRackModal: state
|
||||
`guiOpen {path,url}`, swap vùng graph↔GUI bằng ternary, cleanup unmount,
|
||||
bridge chết → toast + trả về graph.
|
||||
- Tests: `tests/test_plugin_api.py` +4 (mở embed trả url, không port → 500,
|
||||
close kill, close unknown) — mock `find_bridge_exe` + `Popen`.
|
||||
- Kết quả: `node build.mjs` BUILD OK; pytest 116 passed, 1 failed
|
||||
(`test_find_bridge_exe_dev` pre-existing — Linux thiếu binary bridge).
|
||||
- Giới hạn: bridge C++ chưa build/test thật (Linux không có CMake/Win32) —
|
||||
Windows làm 0W1/0W2; E2E GUI nhúng thật chưa chạy — Windows làm 1W1-4W1.
|
||||
@@ -0,0 +1,145 @@
|
||||
# PLAN: thay pedalboard (GPL-3.0) bằng native_bridge — render VSTi offline
|
||||
|
||||
Ngày: 2026-08-15
|
||||
Trạng thái: HOÀN THÀNH — Phase 0-4 xong; Phase 5 xong (RENDER_ENGINE mặc định bridge; gỡ pedalboard khỏi requirements.txt + source (vst_engine/render_engine/plugins/runtime); midi_events_to_messages giữ làm utility thuần; LICENSE.md + THIRD_PARTY_LICENSES.md bỏ cảnh báo GPL-3.0; tests 114 pass, 0 skip). Docker image build chưa test được (máy không có docker) — recipe verify qua WSL.
|
||||
|
||||
## 1. Mục tiêu
|
||||
Xóa dependency GPL-3.0 (`pedalboard`) khỏi cả 2 môi trường, chuyển toàn bộ
|
||||
render VSTi offline (preview quick_render, /midi-render, export session) sang
|
||||
host `native_bridge` (MIT) — GIỮ NGUYÊN luồng nghiệp vụ + play âm thanh:
|
||||
|
||||
- Docker: client play = FluidSynthWASM (không đổi); server preview/export =
|
||||
native_bridge Linux (thay pedalboard).
|
||||
- Standalone: realtime play = Tauri + bridge SHM (không đổi); Python sidecar
|
||||
preview/export = native_bridge Windows (thay pedalboard).
|
||||
|
||||
Realtime play KHÔNG dùng pedalboard hôm nay → không bị đụng.
|
||||
|
||||
## 2. Hiện trạng dùng pedalboard (cần thay)
|
||||
| Chỗ | Chức năng | Đường |
|
||||
|---|---|---|
|
||||
| `app/api/v1/plugins.py:878-953` `_render_midi_notes_pedalboard` | preview quick_render + /midi-render: MIDI clip → WAV | `VST3Plugin(midi_messages, duration)` |
|
||||
| `app/core/render_engine.py:215-271` | export session track VSTi | `vst(midi_messages, duration)` + `apply_preset_to_plugin` |
|
||||
| `app/core/render_engine.py:383-404` | track FX chorus/reverb | `Pedalboard([Chorus/Reverb])` — đã có fallback scipy |
|
||||
| `app/core/vst_engine.py:516+` DecentSamplerManager | Pianobook .dspreset | `create_decent_sampler_instance` |
|
||||
| `app/core/vst_engine.py:482-503` `midi_events_to_messages` | note events → raw MIDI bytes | dùng `pedalboard.midi_utils` (chỉ format bytes — bridge đã hiểu 0x90/0x80/0xB0/0xC0) |
|
||||
| `engine.spec:92-97` | bundle pedalboard vào desktop | PyInstaller |
|
||||
|
||||
`app/core/runtime.py:377` — feature flag `preview_mode=quick_render` dựa trên
|
||||
`find_spec("pedalboard")`.
|
||||
|
||||
## 3. Kiến trúc đích
|
||||
|
||||
### 3.1. Bridge thêm offline render mode (C++, `daw_vst_bridge`)
|
||||
Mode realtime (SHM, Tauri) GIỮ NGUYÊN. Thêm entrypoint CLI:
|
||||
|
||||
```
|
||||
daw_vst_bridge(.exe) --render <job.json> --out <clip.wav>
|
||||
```
|
||||
|
||||
job.json: `{ instrument_type, plugin_path, preset (path|base64), sample_rate,
|
||||
block_size, notes: [{pitch, velocity, start_beat, duration_beats}], bpm,
|
||||
soundfont_bank/program (SF2/SFZ) }`
|
||||
|
||||
Luồng:
|
||||
1. Parse job → tạo instrument qua `NativeInstrumentEngine::create_instrument`
|
||||
(VST3 / SF2 / SFZ — không phụ thuộc GUI).
|
||||
2. Preset: `.vstpreset` → giải mã base64 state blob → `loadSerializedState`
|
||||
(state container VST3 chuẩn, cùng cơ chế `serializeState` đã có).
|
||||
3. MIDI: chuyển note events → `noteOn/noteOff/controlChange/programChange`
|
||||
với `sampleOffset` (đã hỗ trợ) — offline, không realtime.
|
||||
4. Loop `processAudioBlock` hết duration → gom buffer → ghi WAV (thêm
|
||||
WAV writer đơn giản, stdlib).
|
||||
5. Exit 0/1 + log lỗi rõ (thay cho fail im lặng hiện tại).
|
||||
|
||||
### 3.2. Python client (mới, MIT)
|
||||
`app/core/native_render.py` — spawn bridge `--render`, trả `(out_path, duration)`.
|
||||
API giống `_render_midi_notes_pedalboard` để 2 endpoint + render_engine thay
|
||||
thế trực tiếp. Không import pedalboard. Plugin path resolve bằng logic scan
|
||||
có sẵn (`vst_engine.py`).
|
||||
|
||||
### 3.3. Chorus/Reverb (FX track)
|
||||
Chuyển hẳn sang fallback scipy/numpy ĐÃ CÓ SẴN trong `render_engine.py`
|
||||
(LFO delay chorus + noise-IR reverb) — 0 code mới, MIT. Chất lượng thấp hơn
|
||||
pedalboard; ghi rõ ở doc, upgrade sau nếu cần (native DSP C++ riêng).
|
||||
|
||||
### 3.4. DecentSampler (.dspreset)
|
||||
`.dspreset` là format riêng, không phải VST3 state. Lộ trình:
|
||||
- Test: DS VST3 load state chứa preset (user mở DS GUI → save .vstpreset) →
|
||||
đường `loadSerializedState` xử lý được.
|
||||
- Nếu không: chặn Pianobook trên nền không có pedalboard + hướng dẫn convert
|
||||
.dspreset → .vstpreset. KHÔNG giữ pedalboard vì 1 tính năng phụ.
|
||||
|
||||
## 4. Các phase
|
||||
|
||||
### Phase 0 — Offline render mode C++ (Windows + Linux)
|
||||
- `native_bridge`: thêm `--render` CLI + job parser + WAV writer + offline
|
||||
loop; unit test `native_bridge/tests/` (render 1 note VST3 + 1 SFZ, assert
|
||||
WAV non-silent, duration đúng).
|
||||
- CMake: đảm bảo build Linux (chạy thử trên máy này).
|
||||
- **Kiểm chứng**: `--render` ra WAV đúng pitch/duration với plugin test.
|
||||
|
||||
### Phase 1 — Preset .vstpreset import
|
||||
- Parser base64 state → `loadSerializedState`; test với .vstpreset do Carla/
|
||||
pedalboard export sẵn (golden file).
|
||||
|
||||
### Phase 2 — Python client + thay preview
|
||||
- `app/core/native_render.py`; đổi `plugins.py` quick_render + /midi-render
|
||||
sang client mới (giữ response shape cũ — UI không đổi).
|
||||
- Feature flag `SF_RENDER_ENGINE=bridge|pedalboard` (mặc định `pedalboard`
|
||||
trong giai đoạn chuyển; đảo sang `bridge` sau phase 4).
|
||||
|
||||
### Phase 3 — Export session (render_engine)
|
||||
- Track VSTi: `vst(midi_messages)` → `native_render` render từng track.
|
||||
- FX: bật hẳn fallback scipy (bỏ nhánh `HAS_PEDALBOARD`).
|
||||
- Giữ nguyên kết quả WAV export + mixdown (test so sánh trước/sau).
|
||||
|
||||
### Phase 4 — DecentSampler + Docker + bundle
|
||||
- Dockerfile: build native_bridge Linux (CMake) vào image; chạy `--render`
|
||||
dưới Xvfb `:99` (đã có). Không cần GUI plugin editor.
|
||||
- `engine.spec`: gỡ `pedalboard`/`pedalboard.midi_utils` khỏi bundle.
|
||||
- DecentSampler theo mục 3.4.
|
||||
|
||||
### Phase 5 — Đảo flag + xóa pedalboard
|
||||
- Mặc định `SF_RENDER_ENGINE=bridge`; test toàn diện 2 môi trường.
|
||||
- Gỡ `pedalboard` khỏi `requirements.txt`, bỏ nhánh chết, cập nhật
|
||||
`LICENSE.md`/`THIRD_PARTY_LICENSES.md` (xóa dòng GPL-3.0 warning).
|
||||
- Xóa file thừa (nếu có) + doc.
|
||||
|
||||
## 5. File thay đổi (dự kiến)
|
||||
- Thêm: `native_bridge/src/RenderJob.{h,cpp}` (parse job + offline loop),
|
||||
`native_bridge/src/WavWriter.cpp`, `app/core/native_render.py`,
|
||||
`native_bridge/tests/test_offline_render.py`.
|
||||
- Sửa: `native_bridge/src/main.cpp` (dispatch `--render`),
|
||||
`native_bridge/CMakeLists.txt` (Linux build), `app/api/v1/plugins.py`,
|
||||
`app/core/render_engine.py`, `app/core/vst_engine.py` (midi converter giữ —
|
||||
đã ra bytes thuần, bỏ phụ thuộc pedalboard), `app/core/runtime.py` (flag),
|
||||
`Dockerfile`, `engine.spec`, `requirements.txt`, `LICENSE.md`.
|
||||
|
||||
## 6. Giữ nguyên luồng — điểm kiểm chứng
|
||||
| Luồng | Docker | Standalone |
|
||||
|---|---|---|
|
||||
| Realtime play | FluidSynthWASM — không đổi | Tauri+bridge SHM — không đổi |
|
||||
| Preview VSTi | server render WAV → client Audio — cùng shape response | cùng |
|
||||
| Export session | cùng pipeline WAV → download | cùng |
|
||||
| Soundfont preview | pyfluidsynth — không đổi | pyfluidsynth — không đổi |
|
||||
|
||||
Mỗi phase: chạy bộ test hiện có (`native_bridge/tests/*`, API smoke) +
|
||||
golden WAV so sánh pedalboard vs bridge (RMS/tolerance).
|
||||
|
||||
## 7. Rủi ro + rollback
|
||||
- **VST3 plugin cần GUI mới process được**: headless fail → log rõ, trả 501
|
||||
như cũ; không phá luồng.
|
||||
- **.vstpreset state khác format nội bộ**: phase 1 spike trước; nếu lệch,
|
||||
map params qua `setParamNormalized` (đã có đường controlChange).
|
||||
- **DecentSampler**: mục 3.4.
|
||||
- **Bridge crash giữa render**: subprocess exit non-zero → HTTPException rõ
|
||||
(cải thiện hơn pedalboard fail hiện tại).
|
||||
- **Rollback**: `SF_RENDER_ENGINE=pedalboard` đảo ngược tức thì tới hết
|
||||
phase 4; pedalboard chỉ bị xóa ở phase 5 sau khi bridge ổn định.
|
||||
|
||||
## 8. Tiêu chí hoàn thành
|
||||
- Không còn `pedalboard` trong requirements/engine.spec/source.
|
||||
- Preview + export VSTi 2 môi trường ra WAV đúng (so sánh golden).
|
||||
- Realtime play 2 môi trường không đổi (regression pass).
|
||||
- `LICENSE.md` bỏ cảnh báo GPL-3.0.
|
||||
@@ -0,0 +1,109 @@
|
||||
# PLAN: VST FX SHM realtime
|
||||
|
||||
Mục tiêu: VST3 FX (master FX chain + track chain) xử lý THẬT realtime qua
|
||||
fx_vst_bridge — không còn pass-through khi nghe live.
|
||||
|
||||
Kiến trúc (cloud-FX loop, latency ~2 block):
|
||||
WebAudio (masterBus.output)
|
||||
→ AudioWorklet (sf-fx-realtime.js) — input block
|
||||
→ WebSocket (engine /ws/fx-realtime/{id})
|
||||
→ Python SHM input ring (SonicForge_FXRT_*)
|
||||
→ fx_vst_bridge.exe --realtime-fx (RealtimeFxChain::process, SEH-guarded)
|
||||
→ SHM output ring
|
||||
→ Python → WebSocket → AudioWorklet output → graph
|
||||
|
||||
SHM layout FxRealtimeIPC (C++ struct = Python ctypes mirror, all u32/float 4B):
|
||||
header u32: magic(0x46585254) state sampleRate blockSize running heartbeat
|
||||
inWrite inRead inSlots outWrite outRead outSlots (48 bytes)
|
||||
inL[4][256] inR[4][256] outL[8][256] outR[8][256]
|
||||
|
||||
Files:
|
||||
1. native_bridge/include/FxRealtimeIPC.h — struct + constants (mới)
|
||||
2. native_bridge/src/RealtimeFxLoop.cpp — loop (mới)
|
||||
3. native_bridge/src/main_fx.cpp — thêm --realtime-fx
|
||||
4. native_bridge/CMakeLists.txt — thêm 2 file vào fx_vst_bridge
|
||||
5. app/core/fx_realtime.py — session SHM + spawn (mới)
|
||||
6. app/api/v1/plugins.py — /fx-realtime/start, /stop, WS
|
||||
7. app/static/js/sf-fx-realtime.js — AudioWorklet (mới)
|
||||
8. app/static/js/app.jsx — fxRealtimeManager wiring
|
||||
9. engine.spec — hiddenimport websockets
|
||||
|
||||
v1 scope: MASTER chain realtime (chỗ VST3 bị skip trong rebuildMasteringGraph).
|
||||
Track chain realtime: ponytail — per-track session sau (export offline đã có).
|
||||
|
||||
## Trạng thái: ✅ v1 DONE (2026-08-20)
|
||||
|
||||
- Bridge build OK (native_bridge/build/Release/fx_vst_bridge.exe) + deploy sidecar
|
||||
(src-tauri/binaries/fx_vst_bridge-x86_64-pc-windows-msvc.exe +
|
||||
src-tauri/target/release/fx_vst_bridge.exe).
|
||||
- Engine build OK (websockets bundled) + deploy target/release + resources.
|
||||
- Frontend build OK (app.precompiled.js + sf-fx-realtime.js static serve).
|
||||
- E2E đã test trên engine deploy (port 8000):
|
||||
- POST /fx-realtime/start (purecomp.vst3, 44100) → session_id + ws_url ✓
|
||||
- WS binary loop: chain rỗng = passthrough bit-exact (peak 0.5/rms 0.35) ✓
|
||||
- WS + purecomp: peak 0.5→0.462, rms 0.35→0.246 (compressor attenuate) ✓
|
||||
- WS disconnect → session tự stop, fx_vst_bridge.exe thoát ✓
|
||||
- POST /fx-realtime/stop → stopped:true ✓
|
||||
- WS token sai → reject (handshake 403) ✓
|
||||
- FIX phát hiện khi test: WS drain output chỉ trong vòng receive_bytes → output
|
||||
thừa kẹt SHM ring khi client ngừng gửi → tách _pump_output() task riêng.
|
||||
|
||||
## Trạng thái: ✅ TRACK REALTIME DONE (2026-08-20)
|
||||
|
||||
- Track chain realtime: mọi luồng âm thanh track qua bridge
|
||||
(fx_vst_bridge --realtime-fx + SHM SonicForge_FXRT_*) trước main out.
|
||||
- Module-level `fxRtTracks` map 'trackId:audio|sf' + fxRtTrackChain/Paths/
|
||||
Start/Splice/Stop/Reconcile/StopAll trong app.jsx; gọi từ rebuildTrackFxGraph,
|
||||
startTrackPlayback, stopAllPlayback, FX Rack panel.
|
||||
- `fxRtTrackPaths`: audio nếu clips.length>0 || buffer || sections.length>0;
|
||||
sf nếu midiItems.length>0 || type==='MIDI' || instrumentId.
|
||||
- E2E ổn định (pw_e2e2.py ×3): splice 3 audio + 3 sf, track audio out
|
||||
peak≈0.47 rms≈0.33, sf câm hợp lệ (MIDI taiko không route sfEntry), master
|
||||
Ozone peak≈0.85-0.99. Master câm khi track mastering_bypass=true — hợp lệ.
|
||||
- FIX bug bypass toggle: reconcile đọc state CŨ (React setState async) khi
|
||||
`onChange` gọi `__rebuildTrackFxGraph(track.id)` đồng bộ → bypass không stop
|
||||
session. Fix: `rebuildTrackFxGraph = (trackId, patch)` merge
|
||||
`track = { ...track, ...patch }` TRƯỚC khi rebuild/reconcile; 4 call site
|
||||
(bypass, fxActive, setVstChain, setChain) truyền patch giá trị MỚI.
|
||||
Verify: toggle bypass-on → reconcile want=[] → stop ngay; bypass-off →
|
||||
want=["sf"] → start mới + spliced.
|
||||
- Debug log tạm đã gỡ (giữ `[FX RT track] spliced` + console.warn lỗi);
|
||||
rebuild bundle 1370231 bytes, served LEN khớp local.
|
||||
|
||||
Ponytail còn lại:
|
||||
- Sub-tab (piano roll) play chưa start fxRt — chỉ main/section play.
|
||||
- Chain đổi giữa lúc play chưa restart session (start/stop theo play boundary).
|
||||
- Browser-side queue drop-oldest (8 block) — thay adaptive stretch nếu cần.
|
||||
- Chưa test UI thật trong WebView (graph insertion AudioWorkletNode) — plumbing
|
||||
đã verify qua WS; mở app + bật master VST chain + play để xác nhận âm thanh.
|
||||
|
||||
## Trạng thái: ✅ MASTER REALTIME UI VERIFIED (2026-08-20, engine sống)
|
||||
|
||||
- Verify UI-level: login → inject `window.currentMasteringSettings.vstFxChain`
|
||||
(path đúng `C:\Program Files\Common Files\VST3\Sonible\purecomp.vst3`) →
|
||||
insert clip → Play. Monkey-patch `SonicAPI.fxRealtimeStart` ghi payload→sid.
|
||||
Kết quả (pw_master3.py): call #1 = master (purecomp, engine trả session_id +
|
||||
ws_url) → WS `SonicForge_FXRT_d19c01fc33da` nhận audio 738 frame, out
|
||||
peak 0.4622 / rms 0.1442 vs input sine 0.5/0.35 = attenuate ✓.
|
||||
Track sessions (call #2/#3) chạy song song; stop → WS close, `--realtime-fx`
|
||||
bridge thoát, chỉ còn 2 `--fx-gui` của user (không đụng). Engine @8000 OK.
|
||||
- WS-level (rt_test2.py) trước đó: master realtime attenuate peak 0.4621 rms
|
||||
0.2462 với path đúng `Sonible\purecomp.vst3` (path sai `SoniBook` = passthrough
|
||||
thầm lặng).
|
||||
- Nguyên nhân "VST FX PLUGIN trong master chain peak -inf" (trả lời user):
|
||||
1) Engine bị kill lúc test → `SonicAPI.fxRealtimeStart` fail → `fxRtStart`
|
||||
catch + toast cảnh báo, KHÔNG session → masterBus.output nối thẳng
|
||||
analyser → âm nghe được nhưng KHÔNG qua VST → meter plugin -inf.
|
||||
2) Chain rỗng / vstBypass=true → `fxRtChain()` = [] → không session (âm qua
|
||||
thẳng, không lỗi) → meter -inf.
|
||||
3) Meter trong GUI embed (`--fx-gui`) là của instance GUI RIÊNG (config JSON
|
||||
chỉ path+name; VstGuiEmbed chỉ /frame /ping /input — KHÔNG có audio
|
||||
endpoint) → GUI embed KHÔNG nhận audio realtime SHM session → meter -inf
|
||||
VÔ ĐIỀU KIỆN kể cả khi realtime đang chạy. Audio thật đi qua instance
|
||||
`--realtime-fx` riêng (không hiển thị meter). → ponytail thật: user không
|
||||
thấy meter plugin thật; nơi thấy tín hiệu = master VU meter của app
|
||||
(post-chain) hoặc WS session.
|
||||
- Lưu ý: `masterFxList` (Plugin Picker mastering) load trong useEffect([]) của
|
||||
MasteringModal mount sẵn từ đầu → trước khi SonicAPI sẵn sàng → picker rỗng
|
||||
trong headless; track modal mount trễ hơn nên đầy. Workaround: chờ SonicAPI
|
||||
rồi mới mở modal, hoặc inject chain trực tiếp. (Không phải lỗi engine.)
|
||||
@@ -152,6 +152,7 @@ Các task nhóm H lấp những khâu còn hở giữa A-G để pipeline chạy
|
||||
| ✅ A11 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Sample-accurate: không xử lý MIDI ngay; buffer event theo `sampleOffset`, rẽ block 256 thành các đoạn theo offset, gọi `noteOn/Off` đúng vị trí trước khi render đoạn đó | note timeline không lệch nhịp (>±1 sample test) |
|
||||
| ✅ A12 | `native_bridge/include/SharedMemoryIPC.h` + engine | **[BỔ SUNG]** Mở rộng `MidiEventIPC`: field `data2` (+`data3` PB MSB); hỗ trợ `0xB CC` (FluidSynth `fluid_synth_cc`), `0xC program change`, `0xE pitch bend` (`fluid_synth_pitch_bend`); VST3 → tham số tương ứng | sustain/CC/pitchbend qua bridge hoạt động |
|
||||
| ✅ A13 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Control `type=3 TRANSPORT`: `arg0=0 STOP` → flush note-off toàn channel + `fluid_synth_all_notes_off`; `arg0=1 PLAY` kèm `arg1=playheadSamples` → đồng bộ timeline | Stop → hết âm ngân tức thì |
|
||||
| ✅ A14 | `native_bridge/src/main.cpp` + `Vst2Instrument.cpp`/`Vst3Instrument.cpp` + `SharedMemoryIPC.h` + `src-tauri/src/lib.rs` + `nativeBridgeService.js` | **[BỔ SUNG]** MIDI-out từ plugin: bridge hiện CHỈ gửi MIDI vào (kAudioMasterWantMidi=1, processEvents) — không đọc MIDI plugin xuất ra. MIDI-generator (Scaler2/Instacomposer/MelodicFlow) xuất chord/MIDI không phải audio → câm trong test audio. Cần: plugin gọi audioMaster → host thu VstEvents (VST2 `audioMaster` callback `audioMasterGetVstVersion`+event queue; VST3 `IEventList` output bus) → push vào SHM midiQueue mới (out) → Rust emit → JS route về DAW track | plugin MIDI-gen xuất event → DAW nhận note (test Scaler2 gõ chord → MIDI ghi nhận) |
|
||||
|
||||
### H-B. Rust — bổ sung
|
||||
|
||||
@@ -243,5 +244,6 @@ VU analyser tap (C5) → track VU nhảy
|
||||
- Resampler khi SR AudioContext ≠ bridge (B8/C5).
|
||||
- Ring buffer chống underrun + VU tap (C5) — kế thừa hành vi `_gainNode.__vuAnalyser` cũ.
|
||||
- Fallback SonicSF (C1/C6/C7/G6) — spec không đề cập; giữ để app không chết khi bridge lỗi.
|
||||
- **MIDI-out từ plugin (A14)** — bridge chỉ host MIDI-in; Scaler2/Instacomposer/MelodicFlow xuất MIDI nên không nghe được qua bridge.
|
||||
- Dev-mode mock (C7) — chạy được trên Linux/browser không có `__TAURI__`.
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# TASKS — Phương án A: Engine xử lý toàn chain (chuẩn DAW)
|
||||
|
||||
Trạng thái: `[x]` hoàn thành, `[ ]` chưa. Cập nhật sau mỗi phase.
|
||||
|
||||
## Phase 1 — Chain descriptor + protocol hợp nhất
|
||||
- [x] 1.1 Đặc tả chain slot JSON hợp nhất (type/path/preset_b64/params/bypass/active) — ghi vào doc.
|
||||
- [x] 1.2 `app/core/fx_realtime.py`: `_chain_json_for_bridge` nhận chain hợp nhất + field params/latency.
|
||||
- [x] 1.3 `app/core/native_render.py`: `render_fx_chain` nhận chain hợp nhất (builtin+vst3 xen kẽ, giữ order), bỏ giới hạn type vst3/builtin.
|
||||
- [x] 1.4 job.json realtime + render-fx: schema chain hợp nhất (thay fx_chain cũ).
|
||||
- [x] 1.5 Protocol SET_PARAM {slot,key,value} (realtime builtin automation) — engine Python nhận + forward.
|
||||
- [x] 1.6 Protocol REPORT_LATENCY {slot,samples} — engine Python nhận, lưu per-session.
|
||||
- [x] 1.7 Pytest: chain hợp nhất → job.json đúng schema; SET_PARAM/REPORT_LATENCY round-trip.
|
||||
|
||||
## Phase 2 — Bridge C++: BuiltinFxChain (8 DSP)
|
||||
- [x] 2.1 File `native_bridge/src/BuiltinFxChain.{h,cpp}` — khung chain serial + SEH guard.
|
||||
- [x] 2.2 Port EQ 4-band (eq) từ Python `_apply_eq4`.
|
||||
- [x] 2.3 Port EQ Pro (eqpro) RBJ 8-band từ Python `_apply_eqpro`.
|
||||
- [x] 2.4 Port compressor/limiter từ Python.
|
||||
- [x] 2.5 Port exciter/rebalance từ Python.
|
||||
- [x] 2.6 Port imager/maximizer từ JS WebAudio (MASTER_MODULE_IO) — Python không có.
|
||||
- [x] 2.7 `RealtimeFxChain::buildChain` + RenderFxJob: nhận builtin slot → chạy BuiltinFxChain xen kẽ VST3 đúng thứ tự.
|
||||
- [x] 2.8 SET_PARAM/REPORT_LATENCY trong bridge (SHM control ring).
|
||||
- [x] 2.9 Golden test: C++ vs Python `_apply_builtin_fx_chain` (SNR/diff ngưỡng) + order test xen kẽ.
|
||||
|
||||
## Phase 3 — Engine Python: render + realtime chain hợp nhất
|
||||
- [x] 3.1 `render_engine.py`: track/master đọc chain hợp nhất; Python DSP giữ làm fallback (RENDER_ENGINE≠bridge).
|
||||
- [x] 3.2 Bỏ bất đối xứng imager/maximizer (offline master chain đầy đủ 8 loại qua bridge).
|
||||
- [x] 3.3 PDC: nối REPORT_LATENCY → align track (render_engine.py:286+333).
|
||||
- [x] 3.4 Pytest: render offline có imager/maximizer master giống live.
|
||||
|
||||
## Phase 4 — Frontend: WebAudio chỉ I/O
|
||||
- [x] 4.1 Chain model 1 mảng duy nhất (builtin+vst3 theo UI order); migration project cũ vstFxChain.
|
||||
- [x] 4.2 Master: bỏ MASTER_MODULE_IO graph + rebuildMasteringGraph node-web; 1 worklet master; fader SAU worklet.
|
||||
- [x] 4.3 Track: bỏ fxMods/sfMods WebAudio builtin; 1 worklet track; gain/pan giữ.
|
||||
- [x] 4.4 `fxRtChain()`/`fxRtTrackChain()` đọc chain hợp nhất; chainKey so toàn chain.
|
||||
- [x] 4.5 applyMastering param → SET_PARAM push.
|
||||
- [x] 4.6 Worklet: forward SET_PARAM; queue drop → adaptive (ponytail).
|
||||
|
||||
## Phase 5 — Verify (chuẩn DAW checklist)
|
||||
- [x] 5.1 Build frontend `node build.mjs`.
|
||||
- [x] 5.2 Pytest toàn bộ.
|
||||
- [x] 5.3 Golden live=export (record masterBus vs offline WAV) diff ngưỡng.
|
||||
- [x] 5.4 Order test: [eq, vst3-delay, limiter] — impulse xác nhận delay sau EQ.
|
||||
- [x] 5.5 Fader test: chain active, fader -6dB → đỉnh giảm, chain input không đổi.
|
||||
- [x] 5.6 PDC test: plugin latency 1024 → xuyên pha hết.
|
||||
- [x] 5.7 Smoke server + render master imager+maximizer+vst3.
|
||||
- [x] 5.8 Commit + push standalone-shm-bridge.
|
||||
|
||||
## Review DAW-standard (sau Phase 5, bổ sung nếu thiếu)
|
||||
- [x] R1 Track insert chain serial đúng thứ tự UI (builtin+VST xen kẽ).
|
||||
- [x] R2 Gain/pan sau track chain, trước sum.
|
||||
- [x] R3 Master chain serial đúng thứ tự trên mix tổng.
|
||||
- [x] R4 Master fader cuối cùng trước metering/out.
|
||||
- [x] R5 Live = Export (cùng engine, cùng chain, cùng DSP).
|
||||
- [x] R6 PDC.
|
||||
- [x] R7 Gain staging rõ.
|
||||
- [x] R8 Mono/Stereo xử lý đúng.
|
||||
@@ -0,0 +1,183 @@
|
||||
# TASKS: Nhúng VST GUI — tách theo máy (Linux / Windows)
|
||||
|
||||
Nguồn: `PLAN_MASTERBUS_VST_GUI_EMBED.md` (Phase 0-4). Mỗi task ghi rõ máy thực
|
||||
hiện, file đụng, lệnh, deliverable, test, phụ thuộc.
|
||||
|
||||
Nguyên tắc tách:
|
||||
- **Linux** (máy dev hiện tại): viết mọi source code (C++ bridge, Python API,
|
||||
frontend JSX), build frontend, chạy unit test (mock bridge), commit.
|
||||
- **Windows** (máy chạy VST thật): build bridge C++ (MSVC/vcpkg), mọi E2E thật
|
||||
với plugin (Ozone...) — BitBlt/PrintWindow/SendMessage chỉ chạy Windows.
|
||||
- Linux KHÔNG build được bridge (CMake bỏ VST3/GUI khi không WIN32; `main_linux.cpp`
|
||||
chỉ render offline). Linux KHÔNG có VST GUI để test E2E thật.
|
||||
|
||||
Ký hiệu: `[L]` = làm trên Linux · `[W]` = làm trên Windows.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0 — Bridge `--fx-gui` (HTTP server + capture + input relay)
|
||||
|
||||
### Task 0L1 — Viết `FxGuiServer.cpp` (hoặc mở rộng RenderFxJob.cpp nếu <300 dòng)
|
||||
- Máy: `[L]` · File: `native_bridge/src/FxGuiServer.cpp` (mới) + `native_bridge/src/RenderFxJob.cpp` (hook mode `--fx-gui`)
|
||||
- Nội dung (giữ nguyên phần attach IPlugView có sẵn `fxGuiAttachSafe`):
|
||||
- Mode `--fx-gui <job.json>`: sau khi attach view, khởi động HTTP server mini
|
||||
port ngẫu nhiên → in `SF_FXGUI_PORT=<port>` ra stdout (Python đọc).
|
||||
- `GET /frame` → chụp HWND plugin bằng `BitBlt` (fallback `PrintWindow`
|
||||
`PW_RENDERFULLCONTENT`) → trả `image/jpeg` quality ~70.
|
||||
- `POST /input` `{type, x, y, button, deltaY, keyCode}` → `PostMessage`/`SendMessage`
|
||||
theo tọa độ scale client rect (gửi `WM_MOUSEMOVE` trước click).
|
||||
- `POST /close` → detach view, thoát exit 0. `GET /ping` → alive.
|
||||
- Toàn bộ Win32 API → bọc `#ifdef _WIN32` hoặc chỉ build khi WIN32 (xem Task 0L2).
|
||||
- Lệnh kiểm tra: không build được trên Linux — chỉ kiểm tra bằng mắt/syntax
|
||||
(`g++ -fsyntax-only` không đủ do thiếu windows.h → bỏ qua, sang Task 0W1).
|
||||
- Deliverable: source bridge có mode `--fx-gui`.
|
||||
- Phụ thuộc: không.
|
||||
|
||||
### Task 0L2 — Cập nhật `CMakeLists.txt` cho source mới
|
||||
- Máy: `[L]` · File: `native_bridge/CMakeLists.txt`
|
||||
- Thêm source file mới vào target `daw_vst_bridge` CHỈ khi `WIN32` (giữ Linux build
|
||||
không vỡ — Linux hiện build `main_linux.cpp` render-only).
|
||||
- Lệnh kiểm tra: `cmake -S native_bridge -B /tmp/nb-linux-check && cmake --build /tmp/nb-linux-check --target daw_vst_bridge` → PASS trên Linux.
|
||||
- Deliverable: Linux build vẫn xanh, Windows build có file mới.
|
||||
- Phụ thuộc: 0L1.
|
||||
|
||||
### Task 0W1 — Build bridge trên Windows
|
||||
- Máy: `[W]` · Lệnh: `powershell -ExecutionPolicy Bypass -File build/scripts/build_native_bridge.ps1`
|
||||
(bootstrap vcpkg, fluidsynth, sfizz 1.2.3, vst3sdk submodule, CMake VS2019 x64,
|
||||
copy `daw_vst_bridge.exe` + `libfluidsynth-3.dll` → `src-tauri\binaries\`).
|
||||
- Điều kiện máy: VS Build Tools 2019+, CMake, git, đã pull submodule
|
||||
(`git submodule update --init --recursive -- native_bridge/vst3sdk`).
|
||||
- Deliverable: `native_bridge/build/Release/daw_vst_bridge.exe` + sidecar trong `src-tauri\binaries\`.
|
||||
- Test: exe tồn tại, chạy `--help` không crash.
|
||||
- Phụ thuộc: 0L1, 0L2 (pull code mới về Windows trước).
|
||||
|
||||
### Task 0W2 — Self-check bridge `--fx-gui` thủ công
|
||||
- Máy: `[W]`
|
||||
- Lệnh:
|
||||
1. Tạo `job.json`: `{"path": "C:\\VST\\Ozone11.vst3", "name": "Ozone 11"}`.
|
||||
2. `native_bridge\build\Release\daw_vst_bridge.exe --fx-gui job.json` → đọc port từ stdout.
|
||||
3. `curl http://127.0.0.1:<port>/ping` → alive.
|
||||
4. `curl -o frame.jpg http://127.0.0.1:<port>/frame` → JPEG, kích thước = kích thước GUI plugin.
|
||||
5. `curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"mousemove","x":100,"y":100,"button":0}'` rồi `{"type":"mousedown","x":100,"y":100,"button":0}` + `mouseup` → click trúng nút trong GUI (quan sát đổi trạng thái).
|
||||
6. `curl -X POST http://127.0.0.1:<port>/close` → process thoát exit 0.
|
||||
- Deliverable: /frame trả ảnh đúng, /input điều khiển được, /close sạch.
|
||||
- Phụ thuộc: 0W1.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1 — Python API (embed_url + registry + close + CORS)
|
||||
|
||||
### Task 1L1 — Mở rộng `open_fx_gui` + thêm `fx-gui/close`
|
||||
- Máy: `[L]` · File: `app/api/v1/plugins.py`
|
||||
- Nội dung:
|
||||
- Spawn bridge với `--fx-gui` (thay/giữ `--open-fx-gui`), đọc port từ stdout
|
||||
(`SF_FXGUI_PORT=`) với timeout → trả `{ success, embed_url: "http://127.0.0.1:<port>", already_running }`.
|
||||
- Giữ registry `_FX_GUI_PROCESSES` + `_sf_plugin_path` chống mở trùng.
|
||||
- Thêm `POST /fx-gui/close` `{path}` → tìm process theo `_sf_plugin_path` → terminate.
|
||||
- CORS: đảm bảo `Access-Control-Allow-Origin` cho origin app (port động) — kiểm tra
|
||||
middleware hiện có trong `plugins.py`, thêm nếu thiếu.
|
||||
- Lệnh kiểm tra: `python3 -m pytest tests/test_plugin_api.py -q` → PASS (thêm unit
|
||||
test: mock `subprocess.Popen` → assert spawn args, embed_url parse, close kill,
|
||||
registry prune).
|
||||
- Deliverable: API mới + unit test.
|
||||
- Phụ thuộc: 0L1 (chỉ cần biết contract stdout port; có thể làm song song 0W).
|
||||
|
||||
### Task 1W1 — Test API thật trên Windows
|
||||
- Máy: `[W]` · Điều kiện: chạy app (backend FastAPI) + bridge đã build (0W1).
|
||||
- Lệnh:
|
||||
1. `curl -X POST http://127.0.0.1:<app>/api/v1/plugins/fx-gui/open -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"path":"C:\\VST\\Ozone11.vst3","name":"Ozone 11"}'` → `embed_url`.
|
||||
2. `curl http://<embed_url>/frame` → 200 JPEG.
|
||||
3. `curl -X POST .../fx-gui/close -d '{"path":"C:\\VST\\Ozone11.vst3"}'` → process chết (Task Manager / `Get-Process`).
|
||||
- Deliverable: API chạy thật, embed_url truy cập được, close sạch.
|
||||
- Phụ thuộc: 1L1, 0W1.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2 — MasteringModal: embed GUI trong panel
|
||||
|
||||
### Task 2L1 — Code embed trong `app/static/js/app.jsx` (MasteringModal + vstRow)
|
||||
- Máy: `[L]` · File: `app/static/js/app.jsx`
|
||||
- Nội dung (state ở component panel, truyền qua props như đã làm với `showCarla`):
|
||||
- State per-slot `guiOpen: { url } | null` (MasteringModal).
|
||||
- Nút GUI → toggle: mở → gọi API `fx-gui/open` → lưu `embed_url`; đang mở → đóng.
|
||||
- Vùng embed: `<img src={url + '/frame'}>` refresh `setInterval` 100ms; overlay
|
||||
`onMouseDown/Move/Up` + `onWheel` → `fetch(url + '/input', POST)` tọa độ theo
|
||||
bounding rect; `onKeyDown/Up` trên container (chỉ khi panel focus).
|
||||
- Swap graph↔GUI: ẩn vùng canvas EQ/imager bằng `hidden` khi GUI mở, hiện khi đóng.
|
||||
- Click tên VST trong slot khi GUI đang đóng → mở lại embed (ping trước).
|
||||
- Nút X đóng GUI → `POST /close`, clearInterval, trả graph. Xóa slot khi GUI mở
|
||||
→ cũng gọi `/close`.
|
||||
- `/ping` fail (bridge chết) → trả graph + toast.
|
||||
- Lệnh kiểm tra:
|
||||
- `node build.mjs` (hoặc `npm run build`) → BUILD OK (syntax JSX).
|
||||
- Chạy backend Linux + mở app → verify UI không vỡ (GUI nhúng không test được
|
||||
thật vì thiếu bridge — dùng mock nếu cần: server node trả /frame JPEG fake + /input log).
|
||||
- Deliverable: MasteringModal nhúng GUI + swap + đóng sạch.
|
||||
- Phụ thuộc: 1L1 (contract API).
|
||||
|
||||
### Task 2W1 — E2E thật MasteringModal
|
||||
- Máy: `[W]` · Điều kiện: 2L1 + 0W1 + 1W1 pass.
|
||||
- Test tay: mở app → Mastering Panel → thêm Ozone → GUI nhúng hiện TRONG panel
|
||||
(thay vùng graph) → click/scroll điều khiển plugin → đóng → graph trả về →
|
||||
click tên VST mở lại → Xóa slot khi GUI mở → GUI đóng + slot xóa, không crash.
|
||||
- Deliverable: UI mastering hoàn chỉnh.
|
||||
- Phụ thuộc: 2L1, 1W1.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3 — FXRackModal: embed GUI trong panel
|
||||
|
||||
### Task 3L1 — Code embed trong FXRackModal
|
||||
- Máy: `[L]` · File: `app/static/js/app.jsx` (FXRackModal — vùng scope/eq canvas)
|
||||
- Nội dung: tương tự 2L1 nhưng trong FXRackModal; lưu ý vùng thay thế là
|
||||
`scopeCanvasRef`/`eqCurveRef` (đã có fallback Carla ở đây — không đụng).
|
||||
- Lệnh kiểm tra: `node build.mjs` → BUILD OK; backend Linux + mở app → UI không vỡ.
|
||||
- Deliverable: FXRackModal nhúng GUI + swap + đóng sạch.
|
||||
- Phụ thuộc: 2L1 (tái dùng pattern), 1L1.
|
||||
|
||||
### Task 3W1 — E2E thật FXRackModal
|
||||
- Máy: `[W]` · Điều kiện: 3L1.
|
||||
- Test tay: FX Rack Panel → thêm VST FX → GUI nhúng trong panel → điều khiển →
|
||||
đóng → graph trả về. Track cũ có module Carla → vẫn hiện fallback "đã bị gỡ" +
|
||||
nút gỡ module (không vỡ).
|
||||
- Deliverable: UI fx rack hoàn chỉnh.
|
||||
- Phụ thuộc: 3L1.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4 — Dọn + commit + regression
|
||||
|
||||
### Task 4L1 — Dọn code, docs, commit
|
||||
- Máy: `[L]`
|
||||
- Nội dung:
|
||||
- Toast lỗi đầy đủ (mở GUI fail, bridge chết, close fail).
|
||||
- `clearInterval` khi unmount/close (tránh leak).
|
||||
- Cập nhật `PLAN_MASTERBUS_VST_GUI_EMBED.md` trạng thái HOÀN THÀNH + ghi chú
|
||||
giới hạn (FPS ~10-15, Windows-only).
|
||||
- `git add -A && git commit` (tin nhắn: `embed native VST GUI in mastering + fx rack`).
|
||||
- Lệnh kiểm tra:
|
||||
- `node build.mjs` → BUILD OK.
|
||||
- `python3 -m pytest tests/ -q` → toàn bộ test hiện có PASS (1 fail pre-existing
|
||||
`test_find_bridge_exe_dev` do thiếu binary Linux — ghi chú, không phải regression).
|
||||
- Deliverable: commit sạch, docs cập nhật.
|
||||
- Phụ thuộc: 2L1, 3L1 (code ổn).
|
||||
|
||||
### Task 4W1 — Regression full trên Windows
|
||||
- Máy: `[W]` · Điều kiện: 4L1 (pull code mới).
|
||||
- Test tay toàn bộ: Mastering + FX Rack GUI nhúng; render offline (`--render-fx`)
|
||||
không vỡ; Carla (MasteringModal) vẫn chạy; preset upload; scan plugin.
|
||||
- Deliverable: bản Windows sẵn sàng dùng.
|
||||
- Phụ thuộc: 4L1.
|
||||
|
||||
---
|
||||
|
||||
## Thứ tự thực hiện tối ưu
|
||||
|
||||
```
|
||||
Linux: 0L1 → 0L2 ──→ 1L1 ──→ 2L1 ──→ 3L1 ──→ 4L1 (commit)
|
||||
Windows: 0W1 → 0W2 ──→ 1W1 ──→ 2W1 ──→ 3W1 ──→ 4W1
|
||||
(pull code sau 0L2) (pull sau 2L1) ...
|
||||
```
|
||||
|
||||
Song song được: 1L1 (contract stdout) với 0W1/0W2; 2L1/3L1 với 0W/1W.
|
||||
Chặn: mọi task Windows cần pull code từ Linux trước.
|
||||
@@ -0,0 +1,103 @@
|
||||
# TASKS WINDOWS — Chuẩn hóa luồng audio lossless (SonicForgeStudio)
|
||||
|
||||
Branch: `standalone-shm-bridge` — Pull code mới nhất trước khi làm.
|
||||
|
||||
## Bối cảnh
|
||||
|
||||
Linux đã hoàn tất chuẩn hóa luồng audio theo skill `lossless-audio-compliance`
|
||||
(Float32 nội bộ, WAV IEEE Float32 khi render/export, TPDF dither chỉ ở bước
|
||||
lượng tử cuối, resample Kaiser ≥130dB, null-test gate). Code C++/Python/JS đã
|
||||
sửa trên Linux và **commit sẵn** — phần còn lại trên Windows chỉ là BUILD lại
|
||||
binary, COPY vào `install/`, VERIFY, COMMIT.
|
||||
|
||||
## Đã sửa (kèm theo branch này)
|
||||
|
||||
| File | Thay đổi |
|
||||
|---|---|
|
||||
| `native_bridge/src/RenderJob.cpp`, `RenderFxJob.cpp` | WAV writer: 16-bit PCM (truncate, không dither) → **IEEE Float32** (format 3, 32-bit) |
|
||||
| `app/core/render_engine.py`, `app/api/v1/audio.py`, `app/api/v1/plugins.py` | `sf.write(...)` → `subtype="FLOAT"` (4 chỗ) |
|
||||
| `app/core/audio_features.py`, `app/core/render_engine.py` | `resample_poly` → `window=("kaiser", 16)` (stopband ≥130dB) |
|
||||
| `app/static/js/app.jsx` + `app.precompiled.js` (đã build trên Linux) | Encoder client → Float32 mặc định; TPDF dither cho 16/24/8-bit; sửa bug 24-bit ghi nhầm 8-bit; UI thêm "32 float" |
|
||||
| `app/templates/index.html` | bump cache `?v=20260818` |
|
||||
| `tests/test_lossless_compliance.py` (mới) | Null-test gate (Rule 6) + assert float32 WAV (Rule 7) + bridge render deterministic |
|
||||
| `native_bridge/tests/test_offline_render.py` | `read_wav` hiểu cả IEEE float32 lẫn PCM 16-bit |
|
||||
| `Dockerfile` | fix `git clone --branch v1.2.3` (tag không tồn tại) → clone default |
|
||||
|
||||
## Tasks trên Windows (theo thứ tự)
|
||||
|
||||
> ✅ **HOÀN TẤT (2026-08-18)** — đã chạy trên máy Windows `192.168.1.24` (Win10 19044):
|
||||
> Git 2.55, VS Build Tools 2022 MSVC 14.44, Python 3.11.9, Node 20.20.2,
|
||||
> Rust 1.97.1, vcpkg + fluidsynth 2.5.7, sfizz-src clone
|
||||
> (`native_bridge/sfizz-src`, gitignored). Bridge build:
|
||||
> `cmake -S native_bridge -B build/bridge-win -DCMAKE_TOOLCHAIN_FILE=C:\Users\locpham\vcpkg\scripts\buildsystems\vcpkg.cmake`
|
||||
> + `-DSFIZZ_SHARED=OFF`; bắt buộc `-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL`
|
||||
> + patch `sfizz-src/cmake/SfizzConfig.cmake` (wrap `CMAKE_MSVC_RUNTIME_LIBRARY`
|
||||
> trong `if(NOT ...)`) — patch chỉ ở máy đó, cần làm lại khi clone sfizz mới.
|
||||
> Verify: `tests\test_lossless_compliance.py` 9/9 PASS,
|
||||
> `native_bridge\tests\test_offline_render.py` 9/9 PASS (cần
|
||||
> `native_bridge\build\Release\daw_vst_bridge.exe` + copy `g2_test.sfz` vào
|
||||
> `sfizz-src\tests\TestFiles\`). Bundle: `src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe`.
|
||||
> Binary đã copy vào `install/`: `daw_vst_bridge.exe`, `sonicforge-daw.exe`,
|
||||
> `libfluidsynth-3.dll`, `jpeg62.dll`. Tauri build cần
|
||||
> `src-tauri\binaries\daw_vst_bridge-x86_64-pc-windows-msvc.exe` (externalBin
|
||||
> naming, `binaries/` gitignored). `build_windows.ps1` bước [5/6]
|
||||
> Invoke-WebRequest lỗi console-buffer khi chạy qua SSH — dùng curl tải
|
||||
> `vc_redist.x64.exe`.
|
||||
|
||||
### 1. Pull code mới nhất
|
||||
```powershell
|
||||
cd C:\Users\locpham\SonicForgeStudio
|
||||
git checkout standalone-shm-bridge
|
||||
git pull origin standalone-shm-bridge
|
||||
```
|
||||
|
||||
### 2. Build lại `daw_vst_bridge.exe` (WAV writer float32)
|
||||
Bridge build bằng CMake + vcpkg (fluidsynth) + sfizz-src (xem
|
||||
`native_bridge/CMakeLists.txt`, `build/_sfizz.bat`):
|
||||
```powershell
|
||||
# sfizz từ source nếu chưa có (native_bridge/sfizz-src)
|
||||
# vcpkg install fluidsynth
|
||||
cmake -S native_bridge -B build/bridge-win -DCMAKE_BUILD_TYPE=Release `
|
||||
-DCMAKE_TOOLCHAIN_FILE=<vcpkg-root>\scripts\buildsystems\vcpkg.cmake
|
||||
cmake --build build/bridge-win --target daw_vst_bridge --config Release
|
||||
Copy-Item build/bridge-win/Release/daw_vst_bridge.exe install/daw_vst_bridge.exe -Force
|
||||
```
|
||||
Nếu có script build sẵn dùng script đó; **bắt buộc copy đè** `install/daw_vst_bridge.exe`.
|
||||
|
||||
### 3. Build lại engine + app (`build_windows.ps1`)
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File build_windows.ps1
|
||||
```
|
||||
(Lưu ý: `app.precompiled.js` đã build sẵn trên Linux — bước [2/6] chỉ cần chạy
|
||||
lại nếu bạn sửa `app.jsx`.)
|
||||
|
||||
### 4. Copy binary mới vào `install/`
|
||||
```powershell
|
||||
Copy-Item src-tauri\target\release\sonicforge-daw.exe install\sonicforge-daw.exe -Force
|
||||
# hoặc từ NSIS/MSI output tùy quy trình hiện tại
|
||||
```
|
||||
|
||||
### 5. Verify (null-test gate + định dạng float32)
|
||||
```powershell
|
||||
python tests\test_lossless_compliance.py
|
||||
python native_bridge\tests\test_offline_render.py
|
||||
```
|
||||
Kỳ vọng:
|
||||
- `bridge output IEEE float32 (tag 3)` PASS — WAV render ra format 3, 32-bit
|
||||
- `bridge render deterministic (byte-identical)` PASS
|
||||
- `null test ... bit-exact` PASS
|
||||
- `test_offline_render.py` 3 test đầu PASS với `read_wav` mới
|
||||
|
||||
### 6. Commit + push
|
||||
```powershell
|
||||
git add install/daw_vst_bridge.exe install/sonicforge-daw.exe
|
||||
git commit -m "feat(audio): lossless float32 WAV render/export (bridge + engine) — chuẩn hóa luồng audio"
|
||||
git push origin standalone-shm-bridge
|
||||
```
|
||||
|
||||
## Lưu ý
|
||||
- Không đổi lại WAV writer sang 16-bit: render nội bộ phải là IEEE Float32
|
||||
(skill Rule 4/7); dither TPDF chỉ áp dụng ở bước export cuối (client 16/24-bit).
|
||||
- `install/daw_vst_bridge` (ELF Linux) không commit — chỉ commit `.exe`.
|
||||
- Nếu `sonicforge-daw.exe` bundle cũ vẫn dùng `app.precompiled.js` cũ: xóa cache
|
||||
trình duyệt / dùng `?v=` mới đã bump.
|
||||
+35
-5
@@ -1,6 +1,7 @@
|
||||
# THIRD-PARTY LICENSES — SonicForgeStudio
|
||||
|
||||
Kiểm tra ngày 2026-08-07. Danh sách thành phần bên thứ ba + license.
|
||||
Kiểm tra ngày 2026-08-15 (verify từ PyPI/GitHub/LICENSE.txt trong wheel).
|
||||
Danh sách thành phần bên thứ ba + license. Bản tổng hợp: `LICENSE.md`.
|
||||
|
||||
## 1. Dependencies (npm — package.json)
|
||||
| Thành phần | License | Ghi chú |
|
||||
@@ -8,6 +9,34 @@ Kiểm tra ngày 2026-08-07. Danh sách thành phần bên thứ ba + license.
|
||||
| react / react-dom 19.x | MIT | OK |
|
||||
| @babel/cli, @babel/core, @babel/preset-react | MIT | Chỉ build-time |
|
||||
| jsdom | MIT | Chỉ build-time/test |
|
||||
| @tauri-apps/cli 2.x | Apache-2.0 OR MIT | Desktop shell (src-tauri) — build-time |
|
||||
|
||||
## 1b. Dependencies (Python — requirements.txt)
|
||||
| Thành phần | License | Ghi chú |
|
||||
|---|---|---|
|
||||
| fastapi | MIT | OK |
|
||||
| uvicorn | BSD-3-Clause | OK |
|
||||
| celery | BSD-3-Clause | OK |
|
||||
| redis | MIT | OK |
|
||||
| python-multipart | Apache-2.0 | OK |
|
||||
| librosa | ISC | OK |
|
||||
| pydub | MIT | OK |
|
||||
| numpy | BSD-3-Clause (kèm 0BSD, MIT, Zlib, CC0-1.0) | OK |
|
||||
| scipy | BSD-3-Clause (kèm OpenBLAS BSD-3, GCC runtime GPL-3.0-with-exception) | OK — exception cho phép dùng |
|
||||
| soundfile | BSD-3-Clause | OK (libsndfile core là LGPL-2.1+) |
|
||||
| jinja2 | BSD-3-Clause | OK |
|
||||
| httpx | BSD-3-Clause | OK |
|
||||
| jsonschema | MIT | OK |
|
||||
| mido | MIT | OK |
|
||||
| pyfluidsynth | MIT (wrapper) | OK — link FluidSynth LGPL-2.1+ |
|
||||
| sf2utils 1.0.0 | **LGPL-3.0+** | ⚠️ LICENSE.txt trong wheel là LGPLv3 (metadata PyPI ghi nhầm "GPLv3+"). Dùng runtime `soundfont_inspector.py` — giữ notice + relinkable khi phân phối binary |
|
||||
|
||||
## 1c. Native bridge (native_bridge/, C++)
|
||||
| Thành phần | License | Ghi chú |
|
||||
|---|---|---|
|
||||
| fluidsynth (vcpkg) | LGPL-2.1+ | Link tĩnh qua vcpkg — bắt buộc notice + relinkable object |
|
||||
| vst3sdk (git submodule) | MIT | © 2026 Steinberg Media Technologies GmbH — tương thích MIT project |
|
||||
| vestige (dự kiến cho VST2) | mã nguồn mở | Chưa thêm vào repo — xác nhận license gốc khi implement |
|
||||
|
||||
## 2. Thư viện nhúng (app/static)
|
||||
| File | Nguồn | License | Trạng thái |
|
||||
@@ -31,7 +60,8 @@ Kiểm tra ngày 2026-08-07. Danh sách thành phần bên thứ ba + license.
|
||||
|
||||
## KẾT LUẬN
|
||||
- **Không phát hiện vi phạm bản quyền rõ ràng** (không có code GPL bị nhúng vào project MIT; LGPL của FluidSynth tương thích nếu giữ notice).
|
||||
- **3 điểm cần xử lý trước khi phân phối công khai:**
|
||||
1. Bổ sung `LICENSE` text (MIT + LGPL-2.1) + attribution cho SpessaSynth (MIT notice) và Tailwind.
|
||||
2. Ghi attribution SF2 Rich Nagel ("General MIDI SoundFont v3.0 — © 2006-2010 Rich 'Weeds' Nagel — Some rights reserved") hoặc thay soundfont khác license rõ.
|
||||
3. Không nhúng SGM-V2.01 vào bản phân phối (hạn chế redistribution).
|
||||
- **Cần xử lý trước khi phân phối công khai:**
|
||||
1. **sf2utils (LGPL-3.0+)** — giữ notice + relinkable object khi phân phối binary.
|
||||
2. Bổ sung `LICENSE` text (MIT + LGPL-2.1) + attribution cho SpessaSynth (MIT notice) và Tailwind.
|
||||
3. Ghi attribution SF2 Rich Nagel ("General MIDI SoundFont v3.0" — © 2006-2010 Rich 'Weeds' Nagel — Some rights reserved) hoặc thay soundfont khác license rõ.
|
||||
4. Không nhúng SGM-V2.01 vào bản phân phối (hạn chế redistribution).
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# WALKTHROUGH WINDOWS — Nhúng native VST GUI (phần Windows)
|
||||
|
||||
Máy Windows thực hiện nốt phần còn lại theo `TASKS_LINUX_WINDOWS.md`
|
||||
(tasks 0W1, 0W2, 1W1, 2W1, 3W1, 4W1). Code Linux đã commit và push — **pull
|
||||
code mới về trước**:
|
||||
|
||||
```powershell
|
||||
git pull origin standalone-shm-bridge
|
||||
git submodule update --init --recursive -- native_bridge/vst3sdk
|
||||
```
|
||||
|
||||
Điều kiện máy: VS Build Tools 2019+, CMake, git, git-lfs (nếu vst3sdk dùng).
|
||||
|
||||
---
|
||||
|
||||
## 0W1 — Build bridge
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File build/scripts/build_native_bridge.ps1
|
||||
```
|
||||
|
||||
Lưu ý thay đổi mới trong phần này (so với lần build cũ):
|
||||
- `native_bridge/vcpkg.json` **thêm `libjpeg-turbo`** — vcpkg manifest tự cài,
|
||||
không cần làm gì thêm, nhưng lần build đầu sẽ tải thêm package này.
|
||||
- `native_bridge/CMakeLists.txt`: thêm `src/FxGuiServer.cpp` vào target
|
||||
`daw_vst_bridge` (chỉ khi WIN32) + `find_package(JPEG REQUIRED)` +
|
||||
link `JPEG::JPEG`. Nếu script build cũ không chạy `cmake -S native_bridge`
|
||||
mà dùng đường dẫn cố định, hãy đảm bảo nó re-configure (xóa
|
||||
`native_bridge/build` nếu cần).
|
||||
- Script phải copy `daw_vst_bridge.exe` + DLL (fluidsynth, jpeg) sang
|
||||
`src-tauri\binaries\` (chỉ các DLL cần thiết; libjpeg-turbo thường static
|
||||
theo manifest, nếu build động thì copy luôn `jpeg62.dll`).
|
||||
- Trên máy thật: script hiện KHÔNG copy `jpeg62.dll` (build động) — copy tay:
|
||||
`copy native_bridge\build\Release\jpeg62.dll src-tauri\binaries\`.
|
||||
|
||||
**PASS khi**: `native_bridge\build\Release\daw_vst_bridge.exe` tồn tại và
|
||||
`daw_vst_bridge.exe --help` không crash.
|
||||
|
||||
---
|
||||
|
||||
## 0W2 — Self-check bridge `--fx-gui` (chưa cần app)
|
||||
|
||||
1. Tạo `job.json` cạnh exe:
|
||||
```json
|
||||
{"path": "C:\\VST\\Ozone11.vst3", "name": "Ozone 11"}
|
||||
```
|
||||
(đổi path sang plugin VST3 bất kỳ đã cài trên máy Windows).
|
||||
2. Chạy (giữ terminal mở):
|
||||
```powershell
|
||||
native_bridge\build\Release\daw_vst_bridge.exe --fx-gui job.json
|
||||
```
|
||||
- stdout in dòng `SF_FXGUI_PORT=<port>` → ghi lại port.
|
||||
- **Không có cửa sổ plugin hiện ra** — window bị đặt off-screen
|
||||
(-20000,-20000) để capture; điều khiển qua HTTP, đây là thiết kế.
|
||||
3. Kiểm tra từ terminal khác:
|
||||
```powershell
|
||||
curl http://127.0.0.1:<port>/ping
|
||||
curl -o frame.jpg http://127.0.0.1:<port>/frame
|
||||
curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"mousemove","x":100,"y":100,"button":0}'
|
||||
curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"mousedown","x":100,"y":100,"button":0}'
|
||||
curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"mouseup","x":100,"y":100,"button":0}'
|
||||
curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"wheel","x":100,"y":100,"deltaY":120}'
|
||||
curl -X POST http://127.0.0.1:<port>/close
|
||||
```
|
||||
- `/frame` → JPEG (mở `frame.jpg` xem GUI plugin hiển thị đúng, không đen).
|
||||
- `/input` → click trúng nút trong GUI (plugin đổi trạng thái — để biết
|
||||
được, hãy mở song song GUI plugin bằng app cũ hoặc quan sát frame.jpg
|
||||
trước/sau click).
|
||||
- `/close` → tiến trình thoát exit 0.
|
||||
|
||||
**PASS khi**: frame không đen, input điều khiển được, close sạch.
|
||||
|
||||
> Nếu `/frame` ra ảnh đen dù GUI thật hiển thị: bridge đã tự fallback
|
||||
> `PrintWindow PW_RENDERFULLCONTENT` khi `BitBlt` đen; plugin layered/DirectX
|
||||
> (hiếm) có thể cần xử lý thêm — ghi lại plugin nào bị để theo dõi.
|
||||
|
||||
---
|
||||
|
||||
## 1W1 — Test API thật (backend FastAPI chạy trên Windows)
|
||||
|
||||
Chạy app như bình thường (backend + frontend). Frontend gọi:
|
||||
- `POST /api/v1/plugins/fx-gui` `{path, name, embed: true}` → trả
|
||||
`{success, started, already_running, embed_url, cmd}`.
|
||||
- `POST /api/v1/plugins/fx-gui/close` `{path}` → trả `{success, closed}`.
|
||||
|
||||
Cách test nhanh bằng curl (cần token — lấy từ app sau khi login, hoặc dùng
|
||||
UI thay vì curl; endpoint yêu cầu `Authorization: Bearer <token>`):
|
||||
|
||||
```powershell
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/plugins/fx-gui -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"path":"C:\\VST\\Ozone11.vst3","name":"Ozone 11","embed":true}'
|
||||
curl http://<embed_url>/frame -o frame.jpg
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/plugins/fx-gui/close -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"path":"C:\\VST\\Ozone11.vst3"}'
|
||||
```
|
||||
|
||||
- `embed:true` (mặc định `false` → cửa sổ rời kiểu cũ, giữ nguyên).
|
||||
- Port backend có thể khác 8000 — xem log app.
|
||||
- CORS đã mở `*` + bridge tự trả `Access-Control-Allow-Origin: *` → browser
|
||||
LAN gọi thẳng `embed_url` được.
|
||||
|
||||
**PASS khi**: open trả `embed_url` truy cập được `/frame`; close làm process
|
||||
chết (`Get-Process daw_vst_bridge` không còn).
|
||||
|
||||
---
|
||||
|
||||
## 2W1 — E2E MasteringModal
|
||||
|
||||
1. Mở app → **Mastering Panel**.
|
||||
2. Thêm VST (Ozone) vào master chain → GUI TỰ MỞ (addMasterVst gọi openMasterVstGui).
|
||||
Bấm thêm nút GUI trên slot lúc này → backend trả `already_running:true` + toast
|
||||
"GUI của plugin này đang mở sẵn" — đúng thiết kế, không spawn 2 GUI.
|
||||
3. Kỳ vọng: GUI plugin hiện **TRONG panel** (thay vùng graph EQ/imager),
|
||||
header hiện "VST GUI — nhúng (frame-capture · ~10fps)".
|
||||
4. Click/scroll/kéo trong vùng GUI → plugin phản hồi (thay preset, xoay nút).
|
||||
5. Bấm "✕ Đóng GUI" → về graph; chờ ~2-3s (bridge thoát hẳn) rồi bấm nút GUI lại
|
||||
→ mở lại. Bấm ngay trong ~1s đầu có thể vẫn nhận `already_running` (process
|
||||
chưa thoát) — không phải lỗi.
|
||||
6. Xóa slot khi GUI đang mở → GUI đóng + slot xóa, không crash.
|
||||
7. Tắt bridge thủ công (Task Manager kill `daw_vst_bridge`) → sau ~2s panel
|
||||
toast "Bridge VST GUI đã mất kết nối" và tự trả về graph.
|
||||
|
||||
**PASS khi**: toàn bộ thao tác trên ổn định.
|
||||
|
||||
---
|
||||
|
||||
## 3W1 — E2E FXRackModal
|
||||
|
||||
1. Mở app → **FX Rack Panel** (track bất kỳ).
|
||||
2. Thêm VST FX vào insert chain → bấm nút GUI → GUI nhúng trong panel (thay
|
||||
vùng WORKSPACE CONTROLS + WAVE OBSERVER).
|
||||
3. Điều khiển → đóng → graph trả về.
|
||||
4. Track cũ có module **Carla** → vẫn hiện fallback "Carla Bridge đã bị gỡ
|
||||
khỏi FX Rack" + nút gỡ module (không vỡ).
|
||||
|
||||
**PASS khi**: ổn định như 2W1.
|
||||
|
||||
---
|
||||
|
||||
## 4W1 — Regression full
|
||||
|
||||
Test tay toàn bộ:
|
||||
- Mastering + FX Rack GUI nhúng (2W1/3W1).
|
||||
- Render offline (`--render-fx`) không vỡ — export WAV có FX.
|
||||
- Carla (MasteringModal) vẫn chạy.
|
||||
- Preset upload, scan plugin vẫn OK.
|
||||
|
||||
**PASS khi**: không có regression so với trước embed.
|
||||
|
||||
---
|
||||
|
||||
## Ghi chú chung
|
||||
|
||||
- Frame capture ~10fps (img refresh 100ms) — đủ chỉnh thông số, không phải
|
||||
realtime video.
|
||||
- Feature chỉ chạy Windows desktop (có VST GUI); Linux giữ bridge SF2/SFZ.
|
||||
- Nếu sửa gì trên Windows: sửa ngược lên Linux repo (app.jsx là source,
|
||||
`app.precompiled.js` là bản build — đừng sửa tay).
|
||||
- Lỗi thường gặp:
|
||||
- `--fx-gui` không in port → check job.json path đúng VST3, bridge build
|
||||
có `libjpeg-turbo` (thiếu JPEG → lỗi link).
|
||||
- `/frame` đen → plugin layered window; ghi plugin + thử fallback đã có.
|
||||
- App không mở GUI → xem log backend: thiếu `embed` trong payload hoặc
|
||||
`find_bridge_exe` không thấy exe trong `install/` (cần copy exe + DLL
|
||||
vào `src-tauri\binaries\` và app dùng đúng thư mục đó).
|
||||
+171
-3
@@ -56,6 +56,21 @@ class ExportRequest(BaseModel):
|
||||
sample_rate: int = 44100
|
||||
bit_depth: int = 16
|
||||
|
||||
class MidiNoteModel(BaseModel):
|
||||
pitch: int = 60
|
||||
start_beat: float = 0.0
|
||||
duration_beats: float = 1.0
|
||||
velocity: float = 0.8 # 0..1
|
||||
|
||||
class MidiExportTrackModel(BaseModel):
|
||||
name: str = "Track"
|
||||
notes: List[MidiNoteModel] = []
|
||||
|
||||
class MidiExportRequest(BaseModel):
|
||||
bpm: float = 120.0
|
||||
ppq: int = 480
|
||||
tracks: List[MidiExportTrackModel] = []
|
||||
|
||||
class AIAnalysisRequest(BaseModel):
|
||||
file_id: str
|
||||
api_base_url: Optional[str] = None
|
||||
@@ -153,7 +168,8 @@ async def download_audio(file_id: str):
|
||||
path = _resolve_storage_path(file_id)
|
||||
if not path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
return FileResponse(path, media_type="audio/wav", filename=os.path.basename(path))
|
||||
media = "audio/midi" if os.path.splitext(path)[1].lower() == ".mid" else "audio/wav"
|
||||
return FileResponse(path, media_type=media, filename=os.path.basename(path))
|
||||
|
||||
@router.get("/waveform/{file_id}")
|
||||
async def get_waveform(file_id: str, num_peaks: int = Query(default=800, ge=50, le=4000)):
|
||||
@@ -226,6 +242,74 @@ async def export_audio(req: ExportRequest, current_user: Optional[dict] = Depend
|
||||
"file_id": _safe_file_id(req.file_id)
|
||||
}
|
||||
|
||||
# ── MIDI export (BIG_PHASE_3 Phase 1) ──
|
||||
# Sinh file .mid format 1 bằng stdlib thuần — trước đây frontend tự build bytes
|
||||
# rồi blob-download (bị WebView2 chặn sau khi chuyển standalone). Server sinh
|
||||
# file → GET /download/{file_id} (network download, pattern đã chạy ổn định).
|
||||
def _write_vlq(data: bytearray, value: int) -> None:
|
||||
buf = [value & 0x7F]
|
||||
while value > 0x7F:
|
||||
value >>= 7
|
||||
buf.append(0x80 | (value & 0x7F))
|
||||
for b in reversed(buf):
|
||||
data.append(b)
|
||||
|
||||
def _build_midi_file(req: MidiExportRequest) -> bytes:
|
||||
import struct
|
||||
tpb = max(1, req.ppq or 480)
|
||||
track_chunks = []
|
||||
for tr in req.tracks:
|
||||
events = []
|
||||
for n in tr.notes:
|
||||
if (n.duration_beats or 0) <= 0:
|
||||
continue
|
||||
pitch = max(0, min(127, int(round(n.pitch or 60))))
|
||||
vel = max(1, min(127, int(round((n.velocity if n.velocity is not None else 0.8) * 127))))
|
||||
start = max(0, int(round((n.start_beat or 0) * tpb)))
|
||||
end = start + max(1, int(round((n.duration_beats or 1) * tpb)))
|
||||
events.append((start, 0x90, pitch, vel))
|
||||
events.append((end, 0x80, pitch, 0))
|
||||
if not events:
|
||||
continue # bỏ track không có nốt (trước đây chèn "rest note" pitch 0)
|
||||
# note_off (0x80) trước note_on (0x90) cùng tick — tránh note kẹt
|
||||
events.sort(key=lambda e: (e[0], 0 if e[1] == 0x80 else 1))
|
||||
body = bytearray()
|
||||
name_bytes = (tr.name or "Track").encode("utf-8", "replace")[:255]
|
||||
_write_vlq(body, 0) # delta-time 0 bat buoc truoc meta event - file chuan MIDI (mido/DAW parse duoc)
|
||||
body.append(0xFF); body.append(0x03); body.append(len(name_bytes)); body.extend(name_bytes)
|
||||
last = 0
|
||||
for tick, status, pitch, vel in events:
|
||||
_write_vlq(body, tick - last)
|
||||
last = tick
|
||||
body.append(status); body.append(pitch); body.append(vel)
|
||||
_write_vlq(body, 0)
|
||||
body.append(0xFF); body.append(0x2F); body.append(0x00)
|
||||
chunk = bytearray(b"MTrk")
|
||||
chunk += struct.pack(">I", len(body))
|
||||
chunk += body
|
||||
track_chunks.append(bytes(chunk))
|
||||
if not track_chunks:
|
||||
return b""
|
||||
header = bytearray(b"MThd")
|
||||
header += struct.pack(">IHHH", 6, 1, len(track_chunks), tpb)
|
||||
return bytes(header) + b"".join(track_chunks)
|
||||
|
||||
@router.post("/export/midi")
|
||||
async def export_midi(req: MidiExportRequest, current_user: Optional[dict] = Depends(get_optional_user)):
|
||||
if current_user:
|
||||
enforce_password_changed(current_user)
|
||||
data = _build_midi_file(req)
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail="Không có dữ liệu MIDI nào để xuất")
|
||||
file_id = "midi_export_%s.mid" % uuid.uuid4().hex[:12]
|
||||
with open(os.path.join(settings.PROCESSED_DIR, file_id), "wb") as f:
|
||||
f.write(data)
|
||||
return {
|
||||
"file_id": file_id,
|
||||
"download_url": "/api/v1/audio/download/%s" % file_id,
|
||||
"bytes": len(data)
|
||||
}
|
||||
|
||||
@router.post("/ai-scan")
|
||||
async def ai_scan_audio(req: AIScanRequest, current_user: Optional[dict] = Depends(get_optional_user)):
|
||||
"""
|
||||
@@ -280,7 +364,7 @@ async def ai_cut_audio(req: AICutRequest, current_user: Optional[dict] = Depends
|
||||
if data.ndim > 1:
|
||||
data = data.T
|
||||
sliced, z_start, z_end = await asyncio.to_thread(AIDSPEngine.slice_and_copy_with_zero_crossing, data, sr, req.selection_start, req.selection_end)
|
||||
sf.write(out_path, sliced.T if sliced.ndim > 1 else sliced, sr)
|
||||
sf.write(out_path, sliced.T if sliced.ndim > 1 else sliced, sr, subtype="FLOAT")
|
||||
dur = z_end - z_start
|
||||
else:
|
||||
z_start = round(req.selection_start, 4)
|
||||
@@ -313,7 +397,7 @@ async def run_python_dsp_tool(req: PythonToolRequest, current_user: Optional[dic
|
||||
wave = PythonToolsEngine.generate_synth_wave(req.wave_type or "sine", req.freq or 440.0, req.duration or 2.0)
|
||||
output_file_id = f"user_{user_id}_synth_{req.wave_type}_{uuid.uuid4().hex[:6]}.wav"
|
||||
out_path = os.path.join(settings.PROCESSED_DIR, output_file_id)
|
||||
sf.write(out_path, wave, 44100)
|
||||
sf.write(out_path, wave, 44100, subtype="FLOAT")
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Generated {req.wave_type} synth wave ({req.freq}Hz)",
|
||||
@@ -401,6 +485,90 @@ async def list_user_files(req: MyFilesRequest, current_user: dict = Depends(get_
|
||||
user_files.sort(key=lambda x: x["created_at"], reverse=True)
|
||||
return user_files
|
||||
|
||||
class ReferenceAnalyzeRequest(BaseModel):
|
||||
path: Optional[str] = None # local path (media/browse server mode)
|
||||
file_id: Optional[str] = None # uploaded/processed file id
|
||||
|
||||
def _spectral_centroid_hz(audio, sr):
|
||||
"""Tone proxy: mean spectral centroid (Hz) over 1 s windows.
|
||||
Mono downmix + decimate to ~8 kHz (bounded cost). 0.0 for silence."""
|
||||
import numpy as np
|
||||
from scipy.signal import resample_poly
|
||||
x = audio
|
||||
if x.ndim > 1:
|
||||
x = np.mean(x, axis=0)
|
||||
if x.size == 0 or not np.any(x):
|
||||
return 0.0
|
||||
g = max(1, int(round(sr / 8000.0)))
|
||||
x = resample_poly(x, 1, g)
|
||||
sr8 = float(sr) / g
|
||||
win = int(round(1.0 * sr8))
|
||||
if win < 64 or x.size < win:
|
||||
return 0.0
|
||||
hann = np.hanning(win)
|
||||
freqs = np.fft.rfftfreq(win, 1.0 / sr8)
|
||||
cents = []
|
||||
step = win // 2
|
||||
for i in range(0, x.size - win, step):
|
||||
mag = np.abs(np.fft.rfft(x[i:i + win] * hann))
|
||||
msum = float(mag.sum())
|
||||
if msum > 1e-9:
|
||||
cents.append(float((freqs * mag).sum() / msum))
|
||||
if not cents:
|
||||
return 0.0
|
||||
return float(np.mean(cents))
|
||||
|
||||
@router.post("/reference-analyze")
|
||||
async def analyze_reference(req: ReferenceAnalyzeRequest, current_user: Optional[dict] = Depends(get_optional_user)):
|
||||
"""Gap 11: A/B reference track - analyze LUFS/dBTP/centroid of a
|
||||
reference file (local path from media/browse, or uploaded file_id),
|
||||
return compare metrics + playback url for the A/B toggle."""
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
from urllib.parse import quote
|
||||
from app.core import loudness
|
||||
path = ""
|
||||
url = ""
|
||||
if req.file_id:
|
||||
path = _resolve_storage_path(req.file_id)
|
||||
if not path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
url = "/api/v1/audio/download/%s" % _safe_file_id(req.file_id)
|
||||
elif req.path:
|
||||
path = os.path.normpath(req.path)
|
||||
if not os.path.isfile(path):
|
||||
raise HTTPException(status_code=404, detail="Khong tim thay file")
|
||||
ext = os.path.splitext(path)[1].lower().lstrip(".")
|
||||
if ext not in ALLOWED_AUDIO_EXTENSIONS:
|
||||
raise HTTPException(status_code=400, detail="Dinh dang khong ho tro: %s" % ext)
|
||||
url = "/api/v1/media/file?path=" + quote(path)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Can path hoac file_id")
|
||||
try:
|
||||
info = sf.info(path)
|
||||
if info.samplerate and info.frames / info.samplerate > 900:
|
||||
raise HTTPException(status_code=413, detail="File qua dai (>15 phut)")
|
||||
data, sr = sf.read(path, dtype="float32", always_2d=True)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail="Khong doc duoc audio: %s" % e)
|
||||
audio = np.asarray(data.T, dtype=np.float64) # (ch, n)
|
||||
lufs = loudness.integrated_lufs(audio, sr)
|
||||
tp = loudness.true_peak_db(audio, sr)
|
||||
centroid = await asyncio.to_thread(_spectral_centroid_hz, audio, sr)
|
||||
dur = float(info.frames / max(1, info.samplerate or 1))
|
||||
return {
|
||||
"filename": os.path.basename(path),
|
||||
"duration": round(dur, 2),
|
||||
"sample_rate": int(info.samplerate or 0),
|
||||
"channels": int(info.channels or 0),
|
||||
"lufs": round(lufs, 2) if np.isfinite(lufs) else None,
|
||||
"true_peak_db": round(tp, 2) if np.isfinite(tp) else None,
|
||||
"centroid_hz": round(centroid, 0) if centroid > 0 else None,
|
||||
"url": url,
|
||||
}
|
||||
|
||||
@router.delete("/my-files/{file_id}")
|
||||
async def delete_user_file(file_id: str, current_user: dict = Depends(get_current_user)):
|
||||
user_id = current_user["user_id"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import uuid
|
||||
import time
|
||||
import threading
|
||||
import socket
|
||||
from fastapi import APIRouter, HTTPException, Header, Depends, Request, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, EmailStr
|
||||
@@ -52,6 +53,57 @@ def _record_login_success(ip: str):
|
||||
with _LOGIN_LOCK:
|
||||
_LOGIN_FAILURES.pop(ip, None)
|
||||
|
||||
def _is_loopback(host: Optional[str]) -> bool:
|
||||
"""Standalone: request từ chính máy (127.0.0.1/::1/localhost) → bỏ qua
|
||||
đăng nhập. Client LAN (IP khác) → vẫn cần mật khẩu."""
|
||||
if not host:
|
||||
return False
|
||||
host = host.lower()
|
||||
return host in ("127.0.0.1", "::1", "localhost") or host.startswith("127.")
|
||||
|
||||
_LOCAL_IPS_CACHE = {"at": 0.0, "ips": set()}
|
||||
|
||||
def _own_interface_ips(max_age=60.0) -> set:
|
||||
"""Các IP interface cua CHINH may (standalone): truy cap qua IP LAN
|
||||
cua may (vd http://192.168.1.12:8000) van la may nay -> auto-login.
|
||||
Client LAN may khac co source IP khac -> khong match -> van can mat khau."""
|
||||
now = time.time()
|
||||
c = _LOCAL_IPS_CACHE
|
||||
if now - c["at"] > max_age:
|
||||
ips = set()
|
||||
try:
|
||||
ips = set(socket.gethostbyname_ex(socket.gethostname())[2])
|
||||
except Exception:
|
||||
pass
|
||||
c["ips"] = {i for i in ips if not _is_loopback(i)}
|
||||
c["at"] = now
|
||||
return c["ips"]
|
||||
|
||||
def _is_local_client(host: Optional[str]) -> bool:
|
||||
"""May chay engine (loopback) hoac IP interface cua chinh may -> local.
|
||||
Dung chung cho get_current_user + /auth/local."""
|
||||
if _is_loopback(host):
|
||||
return True
|
||||
if not host:
|
||||
return False
|
||||
return host in _own_interface_ips()
|
||||
|
||||
def _loopback_admin_user() -> dict:
|
||||
"""Identity admin cho loopback — real user_id từ DB; không bắt buộc đổi
|
||||
mật khẩu (standalone tự vào thẳng DAW)."""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
cur = conn.execute("SELECT id, username, role FROM users WHERE username = ?", ("admin",))
|
||||
row = cur.fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
if row:
|
||||
return {"user_id": row["id"], "username": row["username"], "role": row["role"], "must_change_password": False}
|
||||
except Exception:
|
||||
pass
|
||||
return {"user_id": "local", "username": "admin", "role": "admin", "must_change_password": False}
|
||||
|
||||
def _set_auth_cookie(response: Response, token: str):
|
||||
response.set_cookie(
|
||||
COOKIE_NAME, token,
|
||||
@@ -68,6 +120,10 @@ def get_current_user(request: Request, authorization: Optional[str] = Header(Non
|
||||
elif request.cookies.get(COOKIE_NAME):
|
||||
token = request.cookies.get(COOKIE_NAME)
|
||||
if not token:
|
||||
# Standalone (loopback): không cần mật khẩu — auto admin. LAN vẫn 401.
|
||||
host = request.client.host if request.client else None
|
||||
if _is_local_client(host):
|
||||
return _loopback_admin_user()
|
||||
raise HTTPException(status_code=401, detail="Thiếu Token xác thực hoặc Token không hợp lệ")
|
||||
payload = decode_token(token)
|
||||
if not payload:
|
||||
@@ -135,6 +191,22 @@ async def login(req: LoginRequest, request: Request):
|
||||
_set_auth_cookie(resp, token)
|
||||
return resp
|
||||
|
||||
@router.post("/local")
|
||||
async def local_login(request: Request, response: Response):
|
||||
"""Auto-login standalone: chỉ cho phép từ loopback (máy chạy engine).
|
||||
Client LAN nhận 403 → buộc đăng nhập mật khẩu qua /login."""
|
||||
host = request.client.host if request.client else None
|
||||
if not _is_local_client(host):
|
||||
raise HTTPException(status_code=403, detail="Truy cập từ xa — cần đăng nhập")
|
||||
u = _loopback_admin_user()
|
||||
token = create_token(u["user_id"], u["username"], u["role"], False)
|
||||
resp = JSONResponse({
|
||||
"access_token": token,
|
||||
"user": {"id": u["user_id"], "username": u["username"], "role": u["role"], "must_change_password": False},
|
||||
})
|
||||
_set_auth_cookie(resp, token)
|
||||
return resp
|
||||
|
||||
def _validate_password_strength(password: str):
|
||||
"""Minimal strength policy: >= 8 chars and not trivially common."""
|
||||
if len(password) < 8:
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import os, sys, json, subprocess, time as _time
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ── Native "Save As" dialog (fix bug: export MIDI/WAV không hỏi nơi lưu) ──
|
||||
# WebView2 standalone chặn anchor-download → không thấy save dialog. Giải pháp:
|
||||
# frontend gọi /save-dialog → engine mở NATIVE SaveFileDialog (Windows Explorer)
|
||||
# → trả đường dẫn user chọn → frontend gửi bytes qua /write → ghi file thật.
|
||||
SAVE_DIALOG_TIMEOUT = 120 # fallback native (PowerShell) — user có thể để dialog mở lâu
|
||||
# Bridge timeout phải LỚN HƠN Rust wait (300s trong lib.rs) — nếu không, user
|
||||
# chọn path sau 300s thì engine đã bỏ cuộc → save_file.response bị bỏ lại, file
|
||||
# không được ghi dù user đã chọn nơi lưu (bug "hỏi nơi lưu nhưng không có file").
|
||||
SAVE_BRIDGE_TIMEOUT = 360
|
||||
|
||||
|
||||
def _save_file_via_tauri_bridge(default_name: str) -> Optional[str]:
|
||||
"""Tauri shell (Rust watcher trong lib.rs) mở NATIVE SaveFileDialog qua
|
||||
file IPC: engine ghi save_file.request (chứa tên mặc định) → Rust mở dialog
|
||||
→ ghi save_file.response. Trả None nếu bridge không tồn tại (standalone) →
|
||||
caller fallback native. Trả "" nếu bridge ĐÃ dùng nhưng user hủy / hết hạn →
|
||||
KHÔNG fallback (tránh 2 dialog cùng lúc + tránh ghi đè path user đã chọn)."""
|
||||
root = os.environ.get("APPDATA") or os.path.expanduser("~")
|
||||
ipc = os.path.join(root, "SonicForgeDAW", "ipc")
|
||||
if not os.path.isdir(ipc):
|
||||
return None
|
||||
# Bridge chi dung khi engine do Tauri spawn (co SF_PARENT_PID). Marker co
|
||||
# the con sot tu lan Tauri chay truoc → chay standalone se cho response
|
||||
# khong bao gio den (treo 120s). Check nay loai bo truong hop do.
|
||||
if not os.environ.get("SF_PARENT_PID"):
|
||||
return None
|
||||
if not os.path.exists(os.path.join(ipc, "tauri_bridge_ready")):
|
||||
return None
|
||||
req = os.path.join(ipc, "save_file.request")
|
||||
resp = os.path.join(ipc, "save_file.response")
|
||||
try:
|
||||
for f in (req, resp):
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
with open(req, "w", encoding="utf-8") as fh:
|
||||
fh.write(default_name or "untitled")
|
||||
deadline = _time.time() + SAVE_BRIDGE_TIMEOUT
|
||||
while _time.time() < deadline:
|
||||
if os.path.exists(resp):
|
||||
try:
|
||||
with open(resp, "r", encoding="utf-8") as fh:
|
||||
val = fh.read().strip()
|
||||
finally:
|
||||
os.remove(resp)
|
||||
return val # "" khi user hủy — KHÔNG fallback PowerShell (tránh 2 dialog)
|
||||
_time.sleep(0.1)
|
||||
except Exception:
|
||||
return ""
|
||||
return "" # bridge đã dùng nhưng hết hạn — KHÔNG fallback (dialog Rust vẫn có thể đang mở)
|
||||
|
||||
|
||||
def _save_file_native_engine(default_name: str, filter_name: str, filter_ext: str) -> Optional[str]:
|
||||
"""Fallback khi không có Tauri bridge: PowerShell SaveFileDialog (Windows),
|
||||
osascript (macOS), zenity/kdialog (Linux)."""
|
||||
if os.name == "nt":
|
||||
# FileName mặc định + Filter — single-quoted trong PS, filename không
|
||||
# chứa nháy đơn (SonicForge tự sinh tên).
|
||||
ps = (
|
||||
"Add-Type -AssemblyName System.Windows.Forms; "
|
||||
f"$f = New-Object System.Windows.Forms.SaveFileDialog; "
|
||||
f"$f.FileName = '{default_name}'; "
|
||||
f"$f.Filter = '{filter_name} (*.{filter_ext})|*.{filter_ext}|Tất cả các tệp (*.*)|*.*'; "
|
||||
"$f.Title = 'Chọn nơi lưu tệp xuất'; "
|
||||
"if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $f.FileName }"
|
||||
)
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["powershell", "-NoProfile", "-STA", "-Command", ps],
|
||||
capture_output=True, text=True, timeout=SAVE_DIALOG_TIMEOUT,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
return r.stdout.strip() or None
|
||||
except Exception:
|
||||
return None
|
||||
if sys.platform == "darwin":
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["osascript", "-e",
|
||||
'set f to choose file name with prompt "Chọn nơi lưu tệp xuất" default name "' + (default_name or "untitled") + '"'],
|
||||
capture_output=True, text=True, timeout=SAVE_DIALOG_TIMEOUT,
|
||||
)
|
||||
return r.stdout.strip() or None
|
||||
except Exception:
|
||||
return None
|
||||
for cmd in (["zenity", "--file-selection", "--save", "--filename=" + (default_name or "untitled")],
|
||||
["kdialog", "--getsavefilename", os.path.join(os.path.expanduser("~"), default_name or "untitled")]):
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=SAVE_DIALOG_TIMEOUT)
|
||||
if r.returncode == 0:
|
||||
p = r.stdout.strip()
|
||||
if p:
|
||||
return p
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
class SaveDialogRequest(BaseModel):
|
||||
default_name: str = "untitled"
|
||||
filter_name: str = "File"
|
||||
filter_ext: str = "bin"
|
||||
|
||||
|
||||
@router.post("/save-dialog")
|
||||
def save_file_dialog(req: SaveDialogRequest):
|
||||
"""Mở NATIVE Save As dialog. Không cần auth (desktop local, chỉ mở dialog).
|
||||
Trả {"path": "<đường dẫn user chọn>"} hoặc {"path": None} (hủy/không có
|
||||
dialog). def (sync) → FastAPI chạy threadpool — không block loop."""
|
||||
path = _save_file_via_tauri_bridge(req.default_name)
|
||||
if path is None: # không có bridge (standalone thuần) → fallback native
|
||||
path = _save_file_native_engine(req.default_name, req.filter_name, req.filter_ext)
|
||||
return {"path": path or None}
|
||||
|
||||
|
||||
@router.post("/write")
|
||||
async def write_export_file(path: str = Form(...), file: UploadFile = File(...)):
|
||||
"""Ghi bytes đã export vào đường dẫn tuyệt đối user chọn qua save dialog.
|
||||
Trả {"path": ..., "bytes": N} — frontend toast kèm đường dẫn thật."""
|
||||
try:
|
||||
data = await file.read()
|
||||
dirname = os.path.dirname(path)
|
||||
if dirname:
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
return {"path": path, "bytes": len(data)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Không thể ghi file: {str(e)}")
|
||||
+891
-485
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
# SonicForge Preset Library API — thư viện preset VST3 (.vstpreset) nằm trong
|
||||
# storage/presets (mount qua volume trong docker; thư mục storage trên Windows).
|
||||
#
|
||||
# Vai trò: cầu nối Carla → pedalboard. User chỉnh preset trong Carla (native
|
||||
# GUI) → xuất .vstpreset → upload vào thư viện → gán vào track (preset_id trong
|
||||
# Vai trò: cầu nối preset → pedalboard. User chỉnh preset trong native GUI
|
||||
# của VSTi → xuất .vstpreset → upload vào thư viện → gán vào track (preset_id trong
|
||||
# synth_engine) → render_engine tải qua load_preset → âm render = âm đã chỉnh.
|
||||
import os
|
||||
import uuid
|
||||
|
||||
@@ -546,6 +546,7 @@ async def cleanup_all_backups(req: CleanupBackupsRequest, current_user: dict = D
|
||||
|
||||
class RenderProjectRequest(BaseModel):
|
||||
sample_rate: Optional[int] = 44100
|
||||
metadata: Optional[dict] = None # Gap 13: BWF/INFO metadata (title/artist/isrc/...)
|
||||
|
||||
@router.post("/cloud/{project_id}/render")
|
||||
async def render_project_endpoint(project_id: str, req: RenderProjectRequest, current_user: dict = Depends(get_current_user)):
|
||||
@@ -568,7 +569,8 @@ async def render_project_endpoint(project_id: str, req: RenderProjectRequest, cu
|
||||
project_id=project_id,
|
||||
project_name=row["name"],
|
||||
project_json_str=row["data_json"],
|
||||
sample_rate=req.sample_rate or 44100
|
||||
sample_rate=req.sample_rate or 44100,
|
||||
metadata=req.metadata or None
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
+2
-34
@@ -1,10 +1,7 @@
|
||||
# SonicForge System API — capabilities: frontend gọi 1 lần lúc boot để biết
|
||||
# môi trường (desktop Windows / docker headless) và bật/tắt tính năng tương ứng.
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from app.core.runtime import capabilities, save_carla_path
|
||||
from app.api.v1.auth import get_current_user, enforce_password_changed
|
||||
from fastapi import APIRouter
|
||||
from app.core.runtime import capabilities
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -15,37 +12,8 @@ async def get_capabilities():
|
||||
|
||||
- runtime: "desktop" (server + client cùng 1 máy Windows/macOS) |
|
||||
"headless" (docker server + browser UI)
|
||||
- features.carla_local: có Carla trên máy này → hiện nút "Mở trong Carla"
|
||||
- features.preset_upload: luôn True (upload .vstpreset qua web UI)
|
||||
- features.preview_mode: "quick_render" (pedalboard render clip ngắn —
|
||||
âm thật giống export) | "wasm" (Preview Synth trong browser)
|
||||
"""
|
||||
return capabilities()
|
||||
|
||||
|
||||
class CarlaPathRequest(BaseModel):
|
||||
"""Định vị Carla (bản portable zip không cài đặt/PATH). Chấp nhận đường
|
||||
dẫn tới carla.exe HOẶC thư mục chứa carla.exe — resolve và lưu config."""
|
||||
carla_path: str
|
||||
carla_dir: Optional[str] = None # tương thích ngược: tên cũ của carla_path
|
||||
|
||||
|
||||
@router.post("/carla-path")
|
||||
async def set_carla_path(req: CarlaPathRequest, current_user: dict = Depends(get_current_user)):
|
||||
"""Lưu vị trí carla.exe do user chọn (Plugin Manager → Định vị Carla...).
|
||||
|
||||
Cần thiết vì bản Carla Windows là bộ file zip portable — không có installer
|
||||
cũng không dùng biến môi trường PATH, nên heuristic không tìm thấy."""
|
||||
enforce_password_changed(current_user)
|
||||
target = (req.carla_path or req.carla_dir or "").strip()
|
||||
if not target:
|
||||
raise HTTPException(status_code=400, detail="Thiếu đường dẫn Carla")
|
||||
exe = save_carla_path(target)
|
||||
if not exe:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Không tìm thấy carla.exe trong đường dẫn đã chọn. Hãy chọn "
|
||||
"thư mục chứa carla.exe (bản portable giải nén) hoặc chính file carla.exe.",
|
||||
)
|
||||
return {"success": True, "carla_path": exe, **capabilities()}
|
||||
|
||||
|
||||
+6
-1
@@ -62,9 +62,14 @@ class Settings:
|
||||
VST_DIR: str = os.getenv("VST_DIR", _default_vst_dir())
|
||||
SOUNDFONT_DIR: str = os.getenv("SOUNDFONT_DIR", _default_soundfont_dir())
|
||||
|
||||
# Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard.
|
||||
# Thư viện preset VST3 (.vstpreset) — cầu nối preset → pedalboard.
|
||||
# Nằm trong storage nên tự động nằm trong volume mount (docker) / thư mục
|
||||
# storage (Windows desktop).
|
||||
PRESET_DIR: str = os.path.join(STORAGE_DIR, "presets")
|
||||
|
||||
# Render engine offline (preview/export VSTi): 'bridge' (native_bridge
|
||||
# --render, MIT). Giá trị khác → fallback synth (pedalboard GPL-3.0 đã gỡ
|
||||
# phase 5 — PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md).
|
||||
RENDER_ENGINE: str = os.getenv("SF_RENDER_ENGINE", "bridge")
|
||||
|
||||
settings = Settings()
|
||||
|
||||
+17
-10
@@ -5,6 +5,20 @@ import soundfile as sf
|
||||
from pydub import AudioSegment
|
||||
from app.core.dsp_utils import find_nearest_zero_crossing_file, apply_micro_fade
|
||||
|
||||
|
||||
def _write_wav_lossless(output_path, y, sample_rate, bit_depth):
|
||||
"""Lossless WAV export (lossless-audio-compliance skill):
|
||||
- 32-bit -> IEEE Float32, bit-exact (no dither, format 3)
|
||||
- 8/16/24-bit -> single-stage TPDF dither ONLY at final quantization"""
|
||||
if bit_depth == 32:
|
||||
sf.write(output_path, y, sample_rate, subtype="FLOAT")
|
||||
return
|
||||
subtype = {8: "PCM_S8", 16: "PCM_16", 24: "PCM_24"}.get(bit_depth, "PCM_24")
|
||||
rng = np.random.default_rng()
|
||||
y = y + (rng.random(y.shape) - rng.random(y.shape)) # TPDF, [-1,1)
|
||||
y = np.clip(y, -1.0, 1.0) # clamp after dither — no wrap on quantization
|
||||
sf.write(output_path, y, sample_rate, subtype=subtype)
|
||||
|
||||
def edit_audio_file(config: dict, input_path: str, output_path: str):
|
||||
"""
|
||||
Applies editing commands on the audio file based on config:
|
||||
@@ -192,12 +206,8 @@ def mix_multitrack_session(tracks_meta: list, output_path: str, sample_rate: int
|
||||
master_mix.export(temp_wav, format="wav")
|
||||
y, sr_read = sf.read(temp_wav)
|
||||
|
||||
# Xác định subtype mã hóa bit-depth
|
||||
subtype_map = {8: "PCM_S8", 16: "PCM_16", 24: "PCM_24"}
|
||||
selected_subtype = subtype_map.get(bit_depth, "PCM_16")
|
||||
|
||||
# Ghi tệp WAV chất lượng cao
|
||||
sf.write(output_path, y, sample_rate, subtype=selected_subtype)
|
||||
# Ghi tệp WAV chất lượng cao (float32 / TPDF-dithered)
|
||||
_write_wav_lossless(output_path, y, sample_rate, bit_depth)
|
||||
finally:
|
||||
# Dọn dẹp tệp tạm (luôn thực hiện)
|
||||
if os.path.exists(temp_wav):
|
||||
@@ -254,10 +264,7 @@ def export_audio(input_path: str, output_path: str, format: str = "wav",
|
||||
sound.export(temp_wav, format="wav")
|
||||
y, sr_read = sf.read(temp_wav)
|
||||
|
||||
subtype_map = {8: "PCM_S8", 16: "PCM_16", 24: "PCM_24"}
|
||||
selected_subtype = subtype_map.get(bit_depth, "PCM_16")
|
||||
|
||||
sf.write(output_path, y, sample_rate, subtype=selected_subtype)
|
||||
_write_wav_lossless(output_path, y, sample_rate, bit_depth)
|
||||
finally:
|
||||
if os.path.exists(temp_wav):
|
||||
os.remove(temp_wav)
|
||||
|
||||
@@ -60,7 +60,7 @@ def load(path, sr=None, mono=True, offset=0.0, duration=None):
|
||||
from fractions import Fraction
|
||||
ratio = Fraction(int(sr), int(file_sr))
|
||||
up, down = ratio.numerator, ratio.denominator
|
||||
data = _signal.resample_poly(data, up, down).astype(np.float32)
|
||||
data = _signal.resample_poly(data, up, down, window=("kaiser", 16)).astype(np.float32)
|
||||
file_sr = sr
|
||||
|
||||
return data, file_sr
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
# app/core/fx_realtime.py
|
||||
"""VST FX SHM realtime (cloud-FX loop): engine tạo mapping FxRealtimeIPC,
|
||||
spawn fx_vst_bridge --realtime-fx, đẩy/kéo block audio qua SHM ring.
|
||||
|
||||
Layout mirror chính xác native_bridge/include/FxRealtimeIPC.h (mọi field
|
||||
u32/float 4-byte, không padding):
|
||||
header 18 u32 (72B): magic state sampleRate blockSize running heartbeat
|
||||
inWrite inRead inSlots outWrite outRead outSlots
|
||||
ctrlWrite ctrlRead ctrlSlots latWrite latRead latSlots
|
||||
inL[4][256] inR[4][256] outL[8][256] outR[8][256]
|
||||
ctrl[8] (FxCtrlCmd 24B) — SET_PARAM ring (engine -> bridge)
|
||||
lat[8] (FxLatReport 8B) — REPORT_LATENCY ring (bridge -> engine)
|
||||
|
||||
Luồng dữ liệu: browser → WS → write_input() → [bridge xử lý] → read_output()
|
||||
→ WS → browser. Block = FXRT_BLOCK=256 stereo float32 = 2048 bytes.
|
||||
"""
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from app.config import settings
|
||||
|
||||
FXRT_MAGIC = 0x46585254
|
||||
FXRT_BLOCK = 256
|
||||
FXRT_IN_SLOTS = 4
|
||||
FXRT_OUT_SLOTS = 8
|
||||
FXRT_CTRL_SLOTS = 8
|
||||
FXRT_LAT_SLOTS = 8
|
||||
FXRT_STATE_STARTING = 0
|
||||
FXRT_STATE_READY = 1
|
||||
FXRT_STATE_ERROR = 2
|
||||
|
||||
|
||||
class FxRTHeader(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("magic", ctypes.c_uint32),
|
||||
("state", ctypes.c_uint32),
|
||||
("sample_rate", ctypes.c_uint32),
|
||||
("block_size", ctypes.c_uint32),
|
||||
("running", ctypes.c_uint32),
|
||||
("heartbeat", ctypes.c_uint32),
|
||||
("in_write", ctypes.c_uint32),
|
||||
("in_read", ctypes.c_uint32),
|
||||
("in_slots", ctypes.c_uint32),
|
||||
("out_write", ctypes.c_uint32),
|
||||
("out_read", ctypes.c_uint32),
|
||||
("out_slots", ctypes.c_uint32),
|
||||
("ctrl_write", ctypes.c_uint32),
|
||||
("ctrl_read", ctypes.c_uint32),
|
||||
("ctrl_slots", ctypes.c_uint32),
|
||||
("lat_write", ctypes.c_uint32),
|
||||
("lat_read", ctypes.c_uint32),
|
||||
("lat_slots", ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
class FxCtrlCmd(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("slot", ctypes.c_uint32),
|
||||
("key", ctypes.c_char * 16),
|
||||
("value", ctypes.c_float),
|
||||
]
|
||||
|
||||
|
||||
class FxLatReport(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("slot", ctypes.c_uint32),
|
||||
("samples", ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
HEADER_SIZE = ctypes.sizeof(FxRTHeader) # 72
|
||||
IN_L_OFF = HEADER_SIZE # 72
|
||||
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
CTRL_OFF = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
LAT_OFF = CTRL_OFF + FXRT_CTRL_SLOTS * ctypes.sizeof(FxCtrlCmd)
|
||||
TOTAL_SIZE = LAT_OFF + FXRT_LAT_SLOTS * ctypes.sizeof(FxLatReport)
|
||||
|
||||
|
||||
class FxRealtimeSession:
|
||||
"""Một session realtime: SHM + subprocess fx_vst_bridge + I/O block."""
|
||||
|
||||
def __init__(self, fx_chain, sample_rate, block_size=FXRT_BLOCK):
|
||||
import numpy as np
|
||||
from multiprocessing import shared_memory
|
||||
self.fx_chain = fx_chain or []
|
||||
self.sample_rate = int(sample_rate)
|
||||
self.block_size = int(block_size or FXRT_BLOCK)
|
||||
if self.block_size != FXRT_BLOCK:
|
||||
raise ValueError("block_size phải = 256 (khớp C++ FxRealtimeIPC)")
|
||||
self.name = "SonicForge_FXRT_" + uuid.uuid4().hex[:12]
|
||||
self.shm = shared_memory.SharedMemory(name=self.name, create=True,
|
||||
size=TOTAL_SIZE)
|
||||
self.shm.buf[:TOTAL_SIZE] = b"\x00" * TOTAL_SIZE
|
||||
self.h = FxRTHeader.from_buffer(self.shm.buf)
|
||||
self.h.magic = FXRT_MAGIC
|
||||
self.h.state = FXRT_STATE_STARTING
|
||||
self.h.sample_rate = self.sample_rate
|
||||
self.h.block_size = self.block_size
|
||||
self.h.running = 1
|
||||
self.h.in_slots = FXRT_IN_SLOTS
|
||||
self.h.out_slots = FXRT_OUT_SLOTS
|
||||
self.h.ctrl_slots = FXRT_CTRL_SLOTS
|
||||
self.h.lat_slots = FXRT_LAT_SLOTS
|
||||
self._np = np.frombuffer(self.shm.buf, dtype=np.float32)
|
||||
self._lock = threading.Lock()
|
||||
# Input batching (fix crackle): gom FXRT_IN_SLOTS frame rồi publish 1 lần
|
||||
# (in_write += n atomic) → bridge luôn thấy bội số của FXRT_IN_SLOTS →
|
||||
# take=4 → batch xử lí. Không phụ thuộc client gửi burst hay lẻ.
|
||||
self._in_pending = []
|
||||
self._in_pending_t0 = None
|
||||
self.proc = None
|
||||
self._job_path = ""
|
||||
self._chain_seq = 0
|
||||
self._started_at = time.time()
|
||||
# Chain hợp nhất (spec PLAN_DAW_A.md Phase 1): giữ đúng thứ tự slot.
|
||||
self.chain = list(self.fx_chain or [])
|
||||
# Control protocol (SET_PARAM / REPORT_LATENCY) — Phase 1: giữ ở engine;
|
||||
# Phase 2.8: C++ consume qua SHM control ring.
|
||||
self.pending_params = {} # {slot_idx: {key: value}}
|
||||
self.latencies = {} # {slot_idx: samples}
|
||||
self.processed = None # G3: processed samples tu bridge (lat slot 0xFFFFFFFE)
|
||||
# Diagnostics (FXRT crackle hunt): drop/publish counters.
|
||||
self.stats = {'publish': 0, 'drop_batch': 0, 'drop_blocks': 0,
|
||||
'stale_flush': 0, 'read_blocks': 0, 'read_empty': 0,
|
||||
'flush': 0}
|
||||
self._spawn()
|
||||
|
||||
# ── process ─────────────────────────────────────────────────────────────
|
||||
def _chain_json_for_bridge(self):
|
||||
"""JSON mảng slot theo spec Phase 1 — giữ NGUYÊN thứ tự + type/params;
|
||||
RealtimeFxChain::buildChain (C++) đọc {path,bypass,preset_b64} cho vst3
|
||||
và {type,params} cho builtin (Phase 2). Slot active=false bị loại trước
|
||||
khi gửi (UI tắt hẳn — không gửi engine)."""
|
||||
out = []
|
||||
for s in self.chain:
|
||||
if not s or not isinstance(s, dict):
|
||||
continue
|
||||
if s.get("active") is False:
|
||||
continue
|
||||
slot = {
|
||||
"type": s.get("type", ""),
|
||||
"bypass": bool(s.get("bypass")),
|
||||
}
|
||||
if s.get("id"):
|
||||
slot["id"] = s["id"]
|
||||
if s.get("path"):
|
||||
slot["path"] = s["path"]
|
||||
if s.get("preset_b64"):
|
||||
slot["preset_b64"] = s["preset_b64"]
|
||||
if isinstance(s.get("params"), dict) and s["params"]:
|
||||
slot["params"] = s["params"]
|
||||
out.append(slot)
|
||||
return out
|
||||
|
||||
def _spawn(self):
|
||||
from app.core.native_render import find_fx_bridge_exe, find_juce_fx_bridge_exe
|
||||
juce_exe = os.getenv("SF_JUCE_FX_BRIDGE_PATH", "").strip()
|
||||
# G3: mặc định dùng JUCE bridge (latency graph thật + processed samples);
|
||||
# fallback bridge cũ khi không có exe JUCE (regression an toàn).
|
||||
exe = juce_exe or find_juce_fx_bridge_exe() or find_fx_bridge_exe()
|
||||
if not exe:
|
||||
raise RuntimeError("Không tìm thấy bridge exe (juce_fx_bridge / fx_vst_bridge)")
|
||||
job = {
|
||||
"sample_rate": self.sample_rate,
|
||||
"block_size": self.block_size,
|
||||
"seq": self._chain_seq,
|
||||
"fx_chain": self._chain_json_for_bridge(),
|
||||
}
|
||||
fd, self._job_path = tempfile.mkstemp(suffix=".json", prefix="fxrt_")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(job, f)
|
||||
if os.path.basename(exe).lower().startswith("juce_fx_bridge"):
|
||||
cmd = [exe, "--juce-fx", self._job_path, "--shm", self.name,
|
||||
"--parent", str(os.getpid())]
|
||||
else:
|
||||
cmd = [exe, "--realtime-fx", self._job_path, "--shm", self.name,
|
||||
"--parent", str(os.getpid())]
|
||||
# FXRT_BRIDGE_LOG=<path>: bat stderr bridge (FxRTPerf) vao file — chan
|
||||
# doan outFull/drop; mac dinh DEVNULL nhu cu.
|
||||
# V34 diag: mac dinh ghi FxRTPerf vao fxrt_bridge.log de phan tich
|
||||
# stall (procMax=bridge/VST cham, outDepth=Python pump cham).
|
||||
_blog = os.environ.get("FXRT_BRIDGE_LOG")
|
||||
if not _blog:
|
||||
_ad = os.environ.get("APPDATA")
|
||||
_blog = os.path.join(_ad, "SonicForgeDAW", "logs", "fxrt_bridge.log") if _ad else ""
|
||||
self.proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=open(_blog, "ab", buffering=0) if _blog else subprocess.DEVNULL,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
|
||||
def set_chain(self, fx_chain):
|
||||
"""Cập nhật chain trên session ĐANG CHẠY (không restart session):
|
||||
ghi lại job file seq++ — bridge poll seq mỗi 500ms → setChain async
|
||||
swap (worker load chain mới, audio giữ chain cũ tới khi swap) → load/
|
||||
đổi VST FX + đổi preset GUI không ngắt âm/crackle."""
|
||||
self.fx_chain = list(fx_chain or [])
|
||||
self.chain = self.fx_chain
|
||||
self._chain_seq += 1
|
||||
if not self._job_path or not os.path.exists(self._job_path):
|
||||
return False
|
||||
try:
|
||||
with open(self._job_path, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"sample_rate": self.sample_rate,
|
||||
"block_size": self.block_size,
|
||||
"seq": self._chain_seq,
|
||||
"fx_chain": self._chain_json_for_bridge(),
|
||||
}, f)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
def wait_ready(self, timeout=30.0):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if self.proc.poll() is not None:
|
||||
raise RuntimeError(
|
||||
f"fx_vst_bridge thoát sớm (rc={self.proc.returncode})")
|
||||
if self.h.state == FXRT_STATE_READY:
|
||||
return True
|
||||
time.sleep(0.02)
|
||||
raise RuntimeError("fx_vst_bridge không sẵn sàng trong %ss" % timeout)
|
||||
|
||||
def alive(self):
|
||||
return self.proc is not None and self.proc.poll() is None
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.h.running = 0
|
||||
except Exception:
|
||||
pass
|
||||
if self.proc is not None:
|
||||
try:
|
||||
self.proc.wait(timeout=3)
|
||||
except Exception:
|
||||
try:
|
||||
self.proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
self.proc = None
|
||||
try:
|
||||
self.shm.close()
|
||||
self.shm.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
if self._job_path and os.path.exists(self._job_path):
|
||||
try:
|
||||
os.remove(self._job_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── audio I/O ───────────────────────────────────────────────────────────
|
||||
def write_input(self, interleaved):
|
||||
"""Gom FXRT_IN_SLOTS frame vào pending, đủ 4 → publish atomic vào ring
|
||||
(ghi data toàn bộ slot trước, sau đó in_write += n 1 lần). Ring đầy khi
|
||||
publish → drop batch cũ nhất (giữ latency thấp)."""
|
||||
import numpy as np
|
||||
arr = np.frombuffer(interleaved, dtype=np.float32)
|
||||
if arr.size != self.block_size * 2:
|
||||
return
|
||||
arr = arr.reshape(self.block_size, 2)
|
||||
with self._lock:
|
||||
if not self._in_pending:
|
||||
self._in_pending_t0 = time.time()
|
||||
self._in_pending.append((arr[:, 0].copy(), arr[:, 1].copy()))
|
||||
if len(self._in_pending) >= FXRT_IN_SLOTS:
|
||||
self._flush_input_locked()
|
||||
|
||||
def _flush_input_locked(self):
|
||||
"""Publish pending vào input ring. Ghi data trước, in_write += n sau →
|
||||
bridge đọc in_write 1 lần → thấy nguyên batch (take=4). V24 NO-DROP:
|
||||
ring đầy → GIỮ pending (pump flush_input_stale 2ms retry) — không drop
|
||||
→ hết content gap + hết race pointer (drop cũ cập in_read vượt in_write
|
||||
→ avail > in_slots → ghi đè slot bridge đang đọc → torn frame). Cap
|
||||
2048 block (~11s) chống tích lũy vô hạn khi client dồn liên tục."""
|
||||
if not self._in_pending:
|
||||
return
|
||||
h = self.h
|
||||
if len(self._in_pending) > 2048:
|
||||
drop = len(self._in_pending) - 2048
|
||||
del self._in_pending[:drop] # drop block CŨ NHẤT (front)
|
||||
self.stats['drop_blocks'] += drop
|
||||
frames = self._in_pending
|
||||
avail = h.in_slots - (h.in_write - h.in_read)
|
||||
self.stats['publish'] += len(frames)
|
||||
if avail <= 0:
|
||||
# ring đầy → giữ nguyên pending; pump retry khi bridge drain
|
||||
return
|
||||
n = min(len(frames), avail)
|
||||
publish = frames[:n]
|
||||
if n < len(frames):
|
||||
del frames[:n] # publish n block CŨ NHẤT, giữ phần thừa làm pending
|
||||
self._in_pending_t0 = time.time() if frames else None
|
||||
else:
|
||||
self._in_pending = []
|
||||
self._in_pending_t0 = None
|
||||
for i in range(n):
|
||||
L, R = publish[i]
|
||||
slot = (h.in_write + i) & (h.in_slots - 1)
|
||||
bl = IN_L_OFF // 4 + slot * self.block_size
|
||||
br = IN_R_OFF // 4 + slot * self.block_size
|
||||
self._np[bl:bl + self.block_size] = L
|
||||
self._np[br:br + self.block_size] = R
|
||||
h.in_write += n # publish atomic — bridge thấy đủ n block cùng lúc
|
||||
|
||||
def flush_input_stale(self, timeout=0.004):
|
||||
"""Flush pending nếu frame đầu chờ > timeout (client gửi lẻ/ngừng giữa
|
||||
burst) → không kẹt latency. Gọi từ _pump_output mỗi vòng."""
|
||||
with self._lock:
|
||||
if self._in_pending and self._in_pending_t0 is not None and time.time() - self._in_pending_t0 > timeout:
|
||||
self.stats['stale_flush'] += 1
|
||||
self._flush_input_locked()
|
||||
|
||||
def flush_all(self):
|
||||
"""V24: reset toàn bộ pipeline khi client resume sau stall — xóa
|
||||
input/output ring + pending → bridge xử lí audio MỚI, không backlog cũ
|
||||
(lag cộng dồn ~1s sau stall). Gọi khi nhận {cmd:'flush'} qua WS."""
|
||||
with self._lock:
|
||||
self._in_pending = []
|
||||
self._in_pending_t0 = None
|
||||
self.h.in_read = self.h.in_write
|
||||
self.h.out_read = self.h.out_write
|
||||
self.stats['flush'] += 1
|
||||
|
||||
def read_output(self):
|
||||
import numpy as np
|
||||
"""Drain output ring → list bytes (mỗi block 2048 bytes interleaved)."""
|
||||
out = []
|
||||
with self._lock:
|
||||
h = self.h
|
||||
while h.out_read < h.out_write:
|
||||
slot = h.out_read & (h.out_slots - 1)
|
||||
bl = OUT_L_OFF // 4 + slot * self.block_size
|
||||
br = OUT_R_OFF // 4 + slot * self.block_size
|
||||
inter = np.empty(self.block_size * 2, dtype=np.float32)
|
||||
inter[0::2] = self._np[bl:bl + self.block_size]
|
||||
inter[1::2] = self._np[br:br + self.block_size]
|
||||
out.append(inter.tobytes())
|
||||
h.out_read += 1
|
||||
self.stats['read_blocks'] += len(out)
|
||||
if not out:
|
||||
self.stats['read_empty'] += 1
|
||||
return out
|
||||
|
||||
def heartbeat_age(self):
|
||||
return time.time() - self._started_at # placeholder: engine đọc h.heartbeat
|
||||
|
||||
# ── control protocol (SET_PARAM / REPORT_LATENCY) ────────────────────────
|
||||
# Phase 1: giữ ở engine, client gọi qua WS text frame. Phase 2.8: bridge
|
||||
# C++ đọc pending_params từ SHM control ring + ghi latency vào ring.
|
||||
def set_param(self, slot, key, value):
|
||||
with self._lock:
|
||||
self.pending_params.setdefault(slot, {})[str(key)] = value
|
||||
# Phase 2.8: đẩy vào SHM control ring (bridge drain mỗi
|
||||
# iteration). pending_params giữ làm fallback — fake test session
|
||||
# (object.__new__) không có shm; drain_params vẫn hoạt động.
|
||||
if getattr(self, "shm", None) is not None:
|
||||
try:
|
||||
self._ctrl_enqueue_locked(slot, str(key), value)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _ctrl_enqueue_locked(self, slot, key, value):
|
||||
h = self.h
|
||||
kb = key.encode("utf-8")[:15]
|
||||
cmds = (FxCtrlCmd * FXRT_CTRL_SLOTS).from_buffer(self.shm.buf, CTRL_OFF)
|
||||
while True:
|
||||
if h.ctrl_write - h.ctrl_read < h.ctrl_slots:
|
||||
cs = h.ctrl_write & (h.ctrl_slots - 1)
|
||||
cmds[cs].slot = int(slot) & 0xFFFFFFFF
|
||||
cmds[cs].key = kb + b"\00" * (16 - len(kb))
|
||||
cmds[cs].value = float(value)
|
||||
h.ctrl_write += 1
|
||||
return
|
||||
h.ctrl_read += 1 # ring đầy → drop lệnh cũ nhất
|
||||
|
||||
def report_latency(self, slot, samples):
|
||||
with self._lock:
|
||||
self.latencies[int(slot)] = int(samples)
|
||||
|
||||
def drain_params(self):
|
||||
"""Trả + xóa toàn bộ pending param (bridge consume 1 lần)."""
|
||||
with self._lock:
|
||||
out = self.pending_params
|
||||
self.pending_params = {}
|
||||
return out
|
||||
|
||||
def get_latencies(self):
|
||||
with self._lock:
|
||||
self._lat_drain()
|
||||
return dict(self.latencies)
|
||||
|
||||
def get_processed(self):
|
||||
"""G3: processed samples (bridge da xu li) - playhead engine-derived."""
|
||||
with self._lock:
|
||||
self._lat_drain()
|
||||
return self.processed
|
||||
|
||||
def _lat_drain(self):
|
||||
shm = getattr(self, "shm", None)
|
||||
if shm is None:
|
||||
return
|
||||
try:
|
||||
h = self.h
|
||||
lats = (FxLatReport * FXRT_LAT_SLOTS).from_buffer(shm.buf, LAT_OFF)
|
||||
while h.lat_read < h.lat_write:
|
||||
ls = h.lat_read & (h.lat_slots - 1)
|
||||
_slot = int(lats[ls].slot)
|
||||
_samples = int(lats[ls].samples)
|
||||
if _slot == 0xFFFFFFFE:
|
||||
# G3: processed - khong vao latencies dict (sum chain sai).
|
||||
self.processed = _samples
|
||||
else:
|
||||
self.latencies[_slot] = _samples
|
||||
h.lat_read += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Registry toàn tiến trình (desktop app 1 user).
|
||||
_SESSIONS = {}
|
||||
_SESSIONS_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def start_session(fx_chain, sample_rate, block_size=FXRT_BLOCK):
|
||||
sess = FxRealtimeSession(fx_chain, sample_rate, block_size)
|
||||
try:
|
||||
sess.wait_ready(timeout=30)
|
||||
except Exception:
|
||||
sess.close()
|
||||
raise
|
||||
with _SESSIONS_LOCK:
|
||||
_SESSIONS[sess.name] = sess
|
||||
return sess
|
||||
|
||||
|
||||
def get_session(session_id):
|
||||
with _SESSIONS_LOCK:
|
||||
return _SESSIONS.get(session_id)
|
||||
|
||||
|
||||
def stop_session(session_id):
|
||||
with _SESSIONS_LOCK:
|
||||
sess = _SESSIONS.pop(session_id, None)
|
||||
if sess:
|
||||
sess.close()
|
||||
return sess is not None
|
||||
|
||||
|
||||
def stop_all():
|
||||
with _SESSIONS_LOCK:
|
||||
ids = list(_SESSIONS.keys())
|
||||
for sid in ids:
|
||||
stop_session(sid)
|
||||
@@ -0,0 +1,106 @@
|
||||
"""EBU R128 loudness + true peak (D1).
|
||||
|
||||
- integrated_lufs: K-weighting (BS.1770) + 400 ms windows, 100 ms hop,
|
||||
absolute (-70 LUFS) + relative (-10 LU) gating -> LUFS.
|
||||
- true_peak_db: 4x oversample (Kaiser-16) -> max |x| in dBTP.
|
||||
|
||||
Stereo-only (channel weight 1.0 L/R); mono broadcast to 2 ch = same result.
|
||||
"""
|
||||
import numpy as np
|
||||
from scipy.signal import lfilter, resample_poly
|
||||
|
||||
|
||||
def _k_weighting_coeffs(sr: float):
|
||||
"""Two biquad stages per ITU-R BS.1770-4 (RBJ cookbook form)."""
|
||||
# Stage 1: high shelf +4 dB, f0=1681.97 Hz, Q=0.7072
|
||||
G = 3.999843853973347
|
||||
f0 = 1681.974450955533
|
||||
Q = 0.7071752369554196
|
||||
A = 10 ** (G / 40.0)
|
||||
w0 = 2 * np.pi * f0 / sr
|
||||
alpha = np.sin(w0) / (2 * Q)
|
||||
cosw = np.cos(w0)
|
||||
b = np.array([
|
||||
A * ((A + 1) + (A - 1) * cosw + 2 * np.sqrt(A) * alpha),
|
||||
-2 * A * ((A - 1) + (A + 1) * cosw),
|
||||
A * ((A + 1) + (A - 1) * cosw - 2 * np.sqrt(A) * alpha),
|
||||
])
|
||||
a = np.array([
|
||||
(A + 1) - (A - 1) * cosw + 2 * np.sqrt(A) * alpha,
|
||||
2 * ((A - 1) - (A + 1) * cosw),
|
||||
(A + 1) - (A - 1) * cosw - 2 * np.sqrt(A) * alpha,
|
||||
])
|
||||
# Stage 2: high-pass f0=38.14 Hz, Q=0.5003
|
||||
f1 = 38.13547087602444
|
||||
Q1 = 0.5003270373238773
|
||||
w1 = 2 * np.pi * f1 / sr
|
||||
alpha1 = np.sin(w1) / (2 * Q1)
|
||||
cosw1 = np.cos(w1)
|
||||
hp_b = np.array([(1 + cosw1) / 2, -(1 + cosw1), (1 + cosw1) / 2])
|
||||
hp_a = np.array([1 + alpha1, -2 * cosw1, 1 - alpha1])
|
||||
return b / a[0], a / a[0], hp_b, hp_a
|
||||
|
||||
|
||||
def _k_weighted(audio: np.ndarray, sr: float) -> np.ndarray:
|
||||
b1, a1, b2, a2 = _k_weighting_coeffs(sr)
|
||||
out = np.empty_like(audio)
|
||||
for c in range(audio.shape[0]):
|
||||
out[c] = lfilter(b2, a2, lfilter(b1, a1, audio[c]))
|
||||
return out
|
||||
|
||||
|
||||
def integrated_lufs(audio: np.ndarray, sr: float) -> float:
|
||||
"""Integrated loudness in LUFS (EBU R128 / BS.1770-4). -inf for silence."""
|
||||
audio = np.asarray(audio, dtype=np.float64)
|
||||
if audio.ndim == 1:
|
||||
audio = audio[np.newaxis, :]
|
||||
if audio.shape[1] == 0:
|
||||
return -np.inf
|
||||
k = _k_weighted(audio, sr)
|
||||
block = int(round(0.4 * sr))
|
||||
hop = int(round(0.1 * sr))
|
||||
n = max(1, (k.shape[1] - block) // hop + 1)
|
||||
powers = np.empty(n)
|
||||
for i in range(n):
|
||||
seg = k[:, i * hop:i * hop + block]
|
||||
powers[i] = np.mean(seg ** 2)
|
||||
# Absolute gate: -70 LUFS -> power = 10^((-70 + 0.691)/10)
|
||||
abs_gate = 10 ** ((-70.0 + 0.691) / 10.0)
|
||||
above_abs = powers[powers > abs_gate]
|
||||
if above_abs.size == 0:
|
||||
return -np.inf
|
||||
abs_loud = -0.691 + 10.0 * np.log10(np.mean(above_abs))
|
||||
# Relative gate: abs_loud - 10 LU
|
||||
rel_gate = 10 ** ((abs_loud - 10.0 + 0.691) / 10.0)
|
||||
gated = above_abs[above_abs > rel_gate]
|
||||
if gated.size == 0:
|
||||
gated = above_abs
|
||||
return float(-0.691 + 10.0 * np.log10(np.mean(gated)))
|
||||
|
||||
|
||||
def mono_side_ratio(audio: np.ndarray) -> float:
|
||||
"""Side/Mid RMS ratio (D3 mono-compat). 0 = mono-safe (L==R); >1 = side
|
||||
louder than mid — mono sum mất nhiều âm thanh."""
|
||||
audio = np.asarray(audio, dtype=np.float64)
|
||||
if audio.ndim == 1:
|
||||
audio = audio[np.newaxis, :]
|
||||
if audio.shape[0] < 2 or audio.shape[1] == 0:
|
||||
return 0.0
|
||||
mid = (audio[0] + audio[1]) * 0.5
|
||||
side = (audio[0] - audio[1]) * 0.5
|
||||
m = float(np.sqrt(np.mean(mid ** 2))) + 1e-12
|
||||
return float(np.sqrt(np.mean(side ** 2))) / m
|
||||
|
||||
|
||||
def true_peak_db(audio: np.ndarray, sr: float, oversample: int = 4) -> float:
|
||||
"""True peak in dBTP (4x oversample). -inf for digital silence."""
|
||||
audio = np.asarray(audio, dtype=np.float64)
|
||||
if audio.ndim == 1:
|
||||
audio = audio[np.newaxis, :]
|
||||
if audio.shape[1] == 0:
|
||||
return -np.inf
|
||||
up = resample_poly(audio, oversample, 1, axis=1, window=("kaiser", 16))
|
||||
peak = float(np.max(np.abs(up)))
|
||||
if peak <= 1e-12:
|
||||
return -np.inf
|
||||
return 20.0 * np.log10(peak)
|
||||
@@ -0,0 +1,477 @@
|
||||
# app/core/native_render.py
|
||||
"""Render MIDI notes → WAV qua native_bridge CLI (daw_vst_bridge --render).
|
||||
|
||||
Thay thế pedalboard (GPL-3.0) bằng bridge (MIT) cho preview/export offline:
|
||||
cùng chữ ký (out_path, duration_sec) nên plugins.py/render_engine thay trực
|
||||
tiếp. KHÔNG import pedalboard. Kích hoạt bằng SF_RENDER_ENGINE=bridge
|
||||
(settings.RENDER_ENGINE); mặc định bridge từ phase 5.
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import soundfile as sf
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.config import settings
|
||||
|
||||
BRIDGE_EXE = "daw_vst_bridge.exe" if os.name == "nt" else "daw_vst_bridge"
|
||||
FX_BRIDGE_EXE = "fx_vst_bridge.exe" if os.name == "nt" else "fx_vst_bridge"
|
||||
JUCE_FX_BRIDGE_EXE = "juce_fx_bridge.exe" if os.name == "nt" else "juce_fx_bridge"
|
||||
|
||||
|
||||
def find_bridge_exe(exe_name=BRIDGE_EXE, env_key="SF_BRIDGE_PATH") -> str:
|
||||
"""Đường dẫn bridge exe: env <env_key> → bundle (frozen) → src-tauri/binaries → install/ (dev)."""
|
||||
env = os.getenv(env_key, "").strip()
|
||||
if env and os.path.isfile(env):
|
||||
return env
|
||||
# Tauri externalBin renames sidecars → <name>-x86_64-pc-windows-msvc.exe.
|
||||
sidecar = exe_name[:-4] + "-x86_64-pc-windows-msvc.exe" if exe_name.lower().endswith(".exe") else exe_name
|
||||
cands = []
|
||||
if getattr(sys, "frozen", False):
|
||||
root = getattr(sys, "_MEIPASS", "")
|
||||
if root:
|
||||
cands += [os.path.join(root, "binaries", exe_name),
|
||||
os.path.join(root, "binaries", sidecar),
|
||||
os.path.join(root, exe_name),
|
||||
os.path.join(root, sidecar)]
|
||||
# PyInstaller onedir: daw_engine.exe nam <app_root>/daw_engine/, bridge
|
||||
# sidecar nam canh app root (khong trong _MEIPASS) - tim ca exe_dir.
|
||||
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
cands += [os.path.join(exe_dir, "binaries", exe_name),
|
||||
os.path.join(exe_dir, "binaries", sidecar),
|
||||
os.path.join(exe_dir, exe_name),
|
||||
os.path.join(exe_dir, sidecar),
|
||||
os.path.join(os.path.dirname(exe_dir), "binaries", exe_name),
|
||||
os.path.join(os.path.dirname(exe_dir), "binaries", sidecar),
|
||||
os.path.join(os.path.dirname(exe_dir), exe_name),
|
||||
os.path.join(os.path.dirname(exe_dir), sidecar)]
|
||||
# Canonical build output (build_native_bridge.ps1) + legacy install/ (dev).
|
||||
cands.append(os.path.join(settings.BASE_DIR, "src-tauri", "binaries", sidecar))
|
||||
cands.append(os.path.join(settings.BASE_DIR, "install", exe_name))
|
||||
for p in cands:
|
||||
if p and os.path.isfile(p):
|
||||
return p
|
||||
return ""
|
||||
|
||||
|
||||
def find_fx_bridge_exe() -> str:
|
||||
"""Đường dẫn FX bridge (fx_vst_bridge.exe): SF_FX_BRIDGE_PATH → bundle → install/."""
|
||||
return find_bridge_exe(FX_BRIDGE_EXE, "SF_FX_BRIDGE_PATH")
|
||||
|
||||
def find_juce_fx_bridge_exe() -> str:
|
||||
"""Đường dẫn FX bridge JUCE (juce_fx_bridge.exe): SF_JUCE_FX_BRIDGE_PATH → bundle → install/."""
|
||||
return find_bridge_exe(JUCE_FX_BRIDGE_EXE, "SF_JUCE_FX_BRIDGE_PATH")
|
||||
def normalize_audio_peak(audio, target_db=0.0):
|
||||
"""Scale audio (numpy array, peak) về target_db nếu vượt; giữ nguyên nếu dưới.
|
||||
|
||||
Chống clip do note MIDI chồng (polyphony) — spec midi_polyphony_loudness §II:
|
||||
render offline nhiều note đồng thời → tổng biên độ vượt 0dBFS → WAV vỡ đỉnh
|
||||
(hard clip, méo harmonic) trước khi tới client; limiter master không cứu được.
|
||||
"""
|
||||
try:
|
||||
import numpy as np
|
||||
peak = float(np.max(np.abs(audio))) if audio.size else 0.0
|
||||
limit = 10 ** (target_db / 20)
|
||||
if peak > limit and peak > 1e-9:
|
||||
return audio * (limit / peak)
|
||||
except Exception:
|
||||
pass
|
||||
return audio
|
||||
|
||||
|
||||
def normalize_peak_file(path, target_db=0.0):
|
||||
"""Post-render WAV: scan peak, vượt target_db → scale toàn file (giữ format)."""
|
||||
try:
|
||||
info = sf.info(path)
|
||||
data, sr = sf.read(path, dtype="float32", always_2d=True)
|
||||
data = normalize_audio_peak(data, target_db)
|
||||
sf.write(path, data, sr, subtype=info.subtype)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def classify_instrument(path: str) -> int:
|
||||
"""InstrumentType enum của bridge: 0=VST3, 1=VST2, 2=SF2/SF3, 3=SFZ."""
|
||||
low = path.lower()
|
||||
if low.endswith(".vst3"):
|
||||
return 0
|
||||
if low.endswith((".dll", ".so")):
|
||||
return 1
|
||||
if low.endswith((".sf2", ".sf3")):
|
||||
return 2
|
||||
if low.endswith(".sfz"):
|
||||
return 3
|
||||
return 0
|
||||
|
||||
|
||||
def _resolve_instrument(instrument_id: str):
|
||||
from app.core.vst_engine import get_plugin_manager, resolve_plugin_path
|
||||
# get_plugin_manager gộp plugin_dirs user (Plugin Manager) — bản cũ dùng
|
||||
# PluginManager() chỉ scan VST_DIR mặc định → VSTi trong thư mục user
|
||||
# (vd C:\Program Files\Steinberg\VSTPlugins) 404 → render_engine fallback
|
||||
# oscillator synth → bounce sai tiếng ("synthwave lạ"). resolve_plugin_path
|
||||
# là lưới an toàn thứ 2 (so tên file case-insensitive, walk cả dir user).
|
||||
path = get_plugin_manager()._scan_plugins().get(instrument_id)
|
||||
if not path:
|
||||
path = resolve_plugin_path(instrument_id)
|
||||
if not path:
|
||||
return None, None
|
||||
# resolve_plugin_path so theo stem — chặn false positive (vd Synapse Audio
|
||||
# → Synapse Audio.nksf, preset file không phải plugin).
|
||||
if not path.lower().endswith((".vst3", ".dll", ".so", ".sf2", ".sf3", ".sfz")):
|
||||
return None, None
|
||||
return classify_instrument(path), path
|
||||
|
||||
|
||||
def _preset_b64(preset_id, preset_path, preset_data_b64) -> str:
|
||||
"""Base64 .vstpreset cho job: bytes nhúng ưu tiên, sau đó id/path file."""
|
||||
if preset_data_b64:
|
||||
return preset_data_b64
|
||||
from app.core.vst_engine import resolve_preset_path
|
||||
p = resolve_preset_path(preset_id or "") or resolve_preset_path(preset_path or "")
|
||||
if p:
|
||||
try:
|
||||
with open(p, "rb") as fh:
|
||||
return base64.b64encode(fh.read()).decode("ascii")
|
||||
except Exception:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
def bridge_state_preset_b64(channel=None, plugin_path=None) -> str:
|
||||
"""presetBase64 từ bridge_state.json — bridge C++ đang giữ preset user
|
||||
chỉnh realtime (capture mỗi ~2s). Export tạo bridge mới không có preset →
|
||||
plugin load init patch → âm khác realtime (user tưởng "fallback"). Fallback
|
||||
này nối: dùng preset mà bridge đã capture cho channel đó.
|
||||
|
||||
Match: channel == midi_channel (round-robin melodic — cùng quy ước
|
||||
unifiedMidiRouter.allocateChannel, KHÔNG phải track.id) + path khớp plugin
|
||||
(basename, case-insensitive). Không tìm thấy → "" (giữ hành vi cũ — không
|
||||
đoán bừa preset channel khác)."""
|
||||
try:
|
||||
st_path = os.path.join(os.environ.get("APPDATA", ""), "SonicForgeDAW",
|
||||
"state", "bridge_state.json")
|
||||
if not os.path.isfile(st_path):
|
||||
return ""
|
||||
with open(st_path, encoding="utf-8", errors="replace") as fh:
|
||||
st = json.load(fh)
|
||||
try:
|
||||
ch = int(channel) if channel is not None else None
|
||||
except Exception:
|
||||
ch = None
|
||||
want = None
|
||||
if plugin_path:
|
||||
want = os.path.basename(os.fspath(plugin_path)).lower()
|
||||
for ins in st.get("instruments") or []:
|
||||
if ch is not None and ins.get("channel") != ch:
|
||||
continue
|
||||
ip = ins.get("path") or ""
|
||||
if want and os.path.basename(ip).lower() != want:
|
||||
continue
|
||||
pb = ins.get("presetBase64") or ""
|
||||
if pb:
|
||||
return pb
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
EXPRESSION_CC = 11 # CC11 expression
|
||||
|
||||
def _expression_events(notes, bpm, sample_rate, grid=64):
|
||||
"""Sinh CC11 events sample-accurate từ expression_curve mỗi note.
|
||||
|
||||
curve: [{position_ratio 0..1, value 0..127}] — nội suy tuyến tính, grid
|
||||
cố định (mặc định 64 samples ≈ 1.3ms @48k) → step CC <= 1-2, không zipper.
|
||||
Event shape {sample, kind:2, a:11, b:value} — khớp RenderJob.cpp Ev.
|
||||
"""
|
||||
if not notes or not bpm:
|
||||
return []
|
||||
beat_sec = 60.0 / float(bpm)
|
||||
events = []
|
||||
for n in notes:
|
||||
curve = n.get("expression_curve") if isinstance(n, dict) else None
|
||||
if not curve:
|
||||
continue
|
||||
try:
|
||||
pts = sorted((float(p["position_ratio"]), int(round(float(p["value"])))) for p in curve if p)
|
||||
except Exception:
|
||||
continue
|
||||
if not pts:
|
||||
continue
|
||||
start_sample = int(round(float(n.get("start_beat", 0)) * beat_sec * float(sample_rate)))
|
||||
dur_samples = int(round(float(n.get("duration_beats", 1)) * beat_sec * float(sample_rate)))
|
||||
if dur_samples <= 0:
|
||||
continue
|
||||
|
||||
def interp(ratio):
|
||||
ratio = max(0.0, min(1.0, ratio))
|
||||
if len(pts) == 1:
|
||||
return pts[0][1]
|
||||
if ratio <= pts[0][0]:
|
||||
return pts[0][1]
|
||||
if ratio >= pts[-1][0]:
|
||||
return pts[-1][1]
|
||||
for i in range(1, len(pts)):
|
||||
if ratio <= pts[i][0]:
|
||||
a, b = pts[i - 1], pts[i]
|
||||
t = (ratio - a[0]) / (b[0] - a[0]) if b[0] > a[0] else 0.0
|
||||
return int(round(a[1] + (b[1] - a[1]) * t))
|
||||
return pts[-1][1]
|
||||
|
||||
pos = 0
|
||||
while pos <= dur_samples:
|
||||
ratio = pos / float(dur_samples)
|
||||
events.append({
|
||||
"sample": start_sample + int(pos),
|
||||
"kind": 2,
|
||||
"a": EXPRESSION_CC,
|
||||
"b": max(0, min(127, interp(ratio))),
|
||||
})
|
||||
pos += max(1, int(grid))
|
||||
events.sort(key=lambda e: e["sample"])
|
||||
return events
|
||||
|
||||
def _run_notes_job(job: dict, label: str) -> tuple:
|
||||
"""Chạy job MIDI render qua native_bridge --render → (out_path, duration_sec).
|
||||
|
||||
Dùng chung cho VSTi (render_offline) + SoundFont (render_soundfont_offline).
|
||||
Lỗi → HTTPException (501 thiếu bridge, 500 lỗi render — file rác bị xóa).
|
||||
"""
|
||||
exe = find_bridge_exe()
|
||||
if not exe:
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="native_bridge không khả dụng — đặt SF_BRIDGE_PATH hoặc cài daw_vst_bridge",
|
||||
)
|
||||
os.makedirs(settings.PROCESSED_DIR, exist_ok=True)
|
||||
out_path = os.path.join(settings.PROCESSED_DIR, f"preview_{uuid.uuid4().hex[:10]}.wav")
|
||||
job_path = out_path + ".job.json"
|
||||
try:
|
||||
with open(job_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(job, fh)
|
||||
r = subprocess.run([exe, "--render", job_path, "--out", out_path],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||
log = ((r.stdout or "") + (r.stderr or "")).strip()
|
||||
if r.returncode != 0 or not os.path.isfile(out_path):
|
||||
if os.path.exists(out_path):
|
||||
try:
|
||||
os.remove(out_path)
|
||||
except Exception:
|
||||
pass
|
||||
if r.returncode == 2:
|
||||
raise HTTPException(status_code=500,
|
||||
detail=f"Bridge không load được plugin {label}. {log[-500:]}")
|
||||
raise HTTPException(status_code=500,
|
||||
detail=f"Render thất bại (rc={r.returncode}). {log[-500:]}")
|
||||
normalize_peak_file(out_path)
|
||||
return out_path, sf.info(out_path).frames / float(job.get("sample_rate", 44100))
|
||||
except HTTPException:
|
||||
raise
|
||||
except subprocess.TimeoutExpired:
|
||||
if os.path.exists(out_path):
|
||||
try:
|
||||
os.remove(out_path)
|
||||
except Exception:
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail="Render quá lâu (timeout 120s)")
|
||||
except Exception as e:
|
||||
if os.path.exists(out_path):
|
||||
try:
|
||||
os.remove(out_path)
|
||||
except Exception:
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail=f"Render thất bại: {e}")
|
||||
finally:
|
||||
if os.path.exists(job_path):
|
||||
try:
|
||||
os.remove(job_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def render_offline(instrument_id, notes, bpm, sample_rate, preset_id=None,
|
||||
preset_path=None, preset_data_b64=None,
|
||||
soundfont_bank=None, soundfont_program=None,
|
||||
plugin_path=None) -> tuple:
|
||||
"""Render MIDI notes qua native_bridge --render → (out_path, duration_sec).
|
||||
|
||||
Giữ response shape của _render_midi_notes_pedalboard. Lỗi → HTTPException
|
||||
(404 instrument lạ, 501 thiếu bridge, 500 lỗi render — file rác bị xóa).
|
||||
"""
|
||||
itype, path = None, None
|
||||
# plugin_path (từ schema synth_engine.plugin_path) ưu tiên — không phụ
|
||||
# thuộc scan; fallback resolve theo tên instrument_id.
|
||||
if plugin_path and os.path.exists(plugin_path):
|
||||
path = os.fspath(plugin_path)
|
||||
itype = classify_instrument(path)
|
||||
if not path:
|
||||
itype, path = _resolve_instrument(instrument_id)
|
||||
if not path:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Không tìm thấy VSTi: {instrument_id} — chưa scan thấy plugin này. "
|
||||
"Kiểm tra Plugins Manager → Scan.",
|
||||
)
|
||||
job = {
|
||||
"instrument_type": itype,
|
||||
"plugin_path": path,
|
||||
"sample_rate": int(sample_rate),
|
||||
"bpm": float(bpm),
|
||||
"notes": [{
|
||||
"pitch": int(n.get("pitch", 60)),
|
||||
"velocity": float(n.get("velocity", 0.8)), # bridge job: float 0..1 (RenderJob.cpp *127)
|
||||
"start_beat": float(n.get("start_beat", 0)),
|
||||
"duration_beats": float(n.get("duration_beats", 1)),
|
||||
} for n in notes],
|
||||
}
|
||||
if itype == 0: # preset chỉ áp dụng cho VST3 (loadSerializedState)
|
||||
preset = _preset_b64(preset_id, preset_path, preset_data_b64)
|
||||
if preset:
|
||||
job["preset"] = preset
|
||||
# bank/program (giữ parity với midi_events_to_messages cũ: key có mặt
|
||||
# kể cả giá trị 0 — bridge xử lý theo presence, không theo truthiness)
|
||||
if soundfont_bank is not None:
|
||||
job["soundfont_bank"] = int(soundfont_bank) & 0x7F
|
||||
if soundfont_program is not None:
|
||||
job["soundfont_program"] = int(soundfont_program) & 0x7F
|
||||
events = _expression_events(notes, bpm, sample_rate)
|
||||
if events:
|
||||
job["events"] = events
|
||||
return _run_notes_job(job, instrument_id)
|
||||
|
||||
|
||||
def render_soundfont_offline(sf_path, notes, bpm, sample_rate, bank=0, program=0) -> tuple:
|
||||
"""Render MIDI notes bằng SoundFont (.sf2/.sf3) qua native_bridge --render.
|
||||
|
||||
Thay pyfluidsynth khi thiếu FluidSynth lib: bridge nhúng FluidSynth C API
|
||||
(NativeInstrumentEngine.cpp, InstrumentType::SOUNDFONT_SF2_SF3) — cùng job
|
||||
shape với render_offline nhưng instrument_type=2 + plugin_path là file SF2.
|
||||
Lỗi → HTTPException (501 thiếu bridge, 500 lỗi render)."""
|
||||
job = {
|
||||
"instrument_type": 2,
|
||||
"plugin_path": os.fspath(sf_path),
|
||||
"sample_rate": int(sample_rate),
|
||||
"bpm": float(bpm),
|
||||
"soundfont_bank": int(bank) & 0x7F,
|
||||
"soundfont_program": int(program) & 0x7F,
|
||||
"notes": [{
|
||||
"pitch": int(n.get("pitch", 60)),
|
||||
"velocity": float(n.get("velocity", 0.8)), # bridge job: float 0..1 (RenderJob.cpp *127)
|
||||
"start_beat": float(n.get("start_beat", 0)),
|
||||
"duration_beats": float(n.get("duration_beats", 1)),
|
||||
} for n in notes],
|
||||
}
|
||||
events = _expression_events(notes, bpm, sample_rate)
|
||||
if events:
|
||||
job["events"] = events
|
||||
return _run_notes_job(job, os.fspath(sf_path))
|
||||
|
||||
|
||||
def render_fx_chain(input_wav, fx_chain, sample_rate, out_path=None):
|
||||
"""Render WAV qua native_bridge --render-fx (VST3 FX + builtin gain/normalize).
|
||||
|
||||
fx_chain: list dict {type: "vst3"|"builtin", path|id, preset_b64?, bypass?,
|
||||
params?}. path là plugin id/name → resolve_plugin_path (".vst3"). Trả
|
||||
(out_path, latencies) — latencies: per-slot samples (bridge FX_LATENCIES,
|
||||
Phase 3 PDC; [] nếu exe cũ không báo). Lỗi → HTTPException (501 thiếu
|
||||
bridge, 404 plugin lạ, 500 render).
|
||||
"""
|
||||
from app.core.vst_engine import resolve_plugin_path
|
||||
exe = find_fx_bridge_exe()
|
||||
if not exe:
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="native_bridge không khả dụng — đặt SF_FX_BRIDGE_PATH hoặc cài fx_vst_bridge",
|
||||
)
|
||||
slots = []
|
||||
for fx in fx_chain or []:
|
||||
if not isinstance(fx, dict) or fx.get("active") is False:
|
||||
continue
|
||||
typ = fx.get("type")
|
||||
if typ == "vst3":
|
||||
p = (fx.get("path") or "").strip()
|
||||
resolved = p if os.path.exists(p) else resolve_plugin_path(p, ".vst3")
|
||||
if not resolved:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Không tìm thấy VST3 FX: {p} — bấm Plugins Manager → Scan trước.",
|
||||
)
|
||||
slots.append({"type": "vst3", "path": resolved,
|
||||
"preset_b64": fx.get("preset_b64", "") or "",
|
||||
"bypass": bool(fx.get("bypass"))})
|
||||
elif typ in ("eq", "eqpro", "imager", "maximizer", "compressor",
|
||||
"limiter", "exciter", "rebalance", "multiband", "deesser"):
|
||||
# Builtin DSP — chain hợp nhất (spec PLAN_DAW_A.md Phase 1). C++
|
||||
# RenderFxJob xử lý theo id/params (Phase 2); giữ NGUYÊN thứ tự.
|
||||
slots.append({"type": "builtin", "id": typ,
|
||||
"params": dict(fx.get("params") or {}),
|
||||
"bypass": bool(fx.get("bypass"))})
|
||||
elif typ == "builtin":
|
||||
# Legacy: {type: builtin, id: gain} — giữ cho project cũ.
|
||||
slots.append({"type": "builtin", "id": fx.get("id", "gain"),
|
||||
"params": dict(fx.get("params") or {}),
|
||||
"bypass": bool(fx.get("bypass"))})
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"fx_chain slot không hợp lệ: {typ}")
|
||||
if not slots:
|
||||
raise HTTPException(status_code=400, detail="fx_chain trống — không có FX để render")
|
||||
job = {"sample_rate": int(sample_rate), "block_size": 512, "fx_chain": slots}
|
||||
os.makedirs(settings.PROCESSED_DIR, exist_ok=True)
|
||||
out = out_path or os.path.join(settings.PROCESSED_DIR, f"fx_{uuid.uuid4().hex[:10]}.wav")
|
||||
job_path = out + ".job.json"
|
||||
try:
|
||||
with open(job_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(job, fh)
|
||||
r = subprocess.run([exe, "--render-fx", job_path, "--in", input_wav, "--out", out],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||
log = ((r.stdout or "") + (r.stderr or "")).strip()
|
||||
if r.returncode != 0 or not os.path.isfile(out):
|
||||
if os.path.exists(out):
|
||||
try:
|
||||
os.remove(out)
|
||||
except Exception:
|
||||
pass
|
||||
if r.returncode == 2:
|
||||
raise HTTPException(status_code=500,
|
||||
detail=f"Bridge không load được plugin. {log[-500:]}")
|
||||
raise HTTPException(status_code=500,
|
||||
detail=f"FX render thất bại (rc={r.returncode}). {log[-500:]}")
|
||||
latencies = []
|
||||
for _line in (r.stdout or "").splitlines():
|
||||
if "FX_LATENCIES" in _line:
|
||||
try:
|
||||
latencies = [int(x) for x in
|
||||
_line.split("FX_LATENCIES", 1)[1].split()]
|
||||
except Exception:
|
||||
latencies = []
|
||||
break
|
||||
return out, latencies
|
||||
except HTTPException:
|
||||
raise
|
||||
except subprocess.TimeoutExpired:
|
||||
if os.path.exists(out):
|
||||
try:
|
||||
os.remove(out)
|
||||
except Exception:
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail="FX render quá lâu (timeout 120s)")
|
||||
except Exception as e:
|
||||
if os.path.exists(out):
|
||||
try:
|
||||
os.remove(out)
|
||||
except Exception:
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail=f"FX render thất bại: {e}")
|
||||
finally:
|
||||
if os.path.exists(job_path):
|
||||
try:
|
||||
os.remove(job_path)
|
||||
except Exception:
|
||||
pass
|
||||
+827
-196
File diff suppressed because it is too large
Load Diff
+20
-235
@@ -1,17 +1,14 @@
|
||||
# SonicForge Runtime Profile — phát hiện môi trường chạy (desktop Windows /
|
||||
# docker headless) và khả năng của máy, để app TỰ CHỌN cách xử lý:
|
||||
# - Windows desktop (server + client cùng 1 máy): Carla local để preview +
|
||||
# chỉnh preset → pedalboard render (Hướng A)
|
||||
# - Windows desktop (server + client cùng 1 máy): native_bridge render VSTi
|
||||
# - Docker / Linux headless (server + browser UI): soundfont + VSTi mở được
|
||||
# từ storage mount; preset upload qua browser; không có GUI local.
|
||||
#
|
||||
# Ưu tiên: SF_RUNTIME env override > heuristic (platform + display + docker).
|
||||
# Module CHỈ dùng stdlib + app.config (không kéo pedalboard/numpy) để import
|
||||
# Module CHỈ dùng stdlib + app.config (không kéo numpy) để import
|
||||
# nhẹ và hoạt động trong mọi tiến trình (web/worker/celery).
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import socket
|
||||
import functools
|
||||
|
||||
@@ -19,10 +16,6 @@ from app.config import settings
|
||||
|
||||
SF_RUNTIME_ENV = "SF_RUNTIME" # auto | desktop | headless
|
||||
SF_DOCKER_ENV = "SF_DOCKER" # 1 = chạy trong container Docker
|
||||
CARLA_CONFIG_FILE = "carla_path.json" # storage/carla_path.json — user định vị
|
||||
# Bản Carla portable (zip) có thể giải nén ở BẤT KỲ ĐÂU — PATH/Program Files
|
||||
# không đủ. User tự chọn thư mục chứa carla.exe qua Plugin Manager → lưu file
|
||||
# này (ưu tiên cao nhất khi detect), kèm tìm kiếm nông Downloads/Desktop.
|
||||
|
||||
|
||||
def _is_windows() -> bool:
|
||||
@@ -67,219 +60,6 @@ def _tauri_bridge_ready() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _carla_config_path() -> str:
|
||||
return os.path.join(settings.STORAGE_DIR, CARLA_CONFIG_FILE)
|
||||
|
||||
|
||||
def get_configured_carla() -> str:
|
||||
"""Đường dẫn carla.exe do USER tự định vị (lưu trong storage/carla_path.json).
|
||||
|
||||
File lưu đường dẫn tới carla.exe (đã resolve) hoặc thư mục chứa — nếu là
|
||||
thư mục, tìm carla.exe bên trong (độ sâu ≤ 2). Đây là cách bắt buộc có cho
|
||||
bản Carla portable (zip) giải nén ở vị trí bất kỳ, không cài đặt/PATH."""
|
||||
try:
|
||||
with open(_carla_config_path(), "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except Exception:
|
||||
return ""
|
||||
p = (data.get("carla_path") or "").strip()
|
||||
if not p:
|
||||
return ""
|
||||
if os.path.isfile(p) and os.path.basename(p).lower() in ("carla.exe", "carla-single.exe", "carla"):
|
||||
return p
|
||||
if os.path.isdir(p):
|
||||
try:
|
||||
for root, dirs, files in os.walk(p):
|
||||
depth = root[len(p):].count(os.sep)
|
||||
if depth >= 2:
|
||||
dirs[:] = []
|
||||
continue
|
||||
for f in files:
|
||||
if f.lower() == "carla.exe":
|
||||
return os.path.join(root, f)
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def save_carla_path(path: str) -> str:
|
||||
"""Lưu vị trí Carla do user chọn (Plugin Manager → Định vị Carla...).
|
||||
|
||||
Chấp nhận: đường dẫn tới carla.exe, hoặc thư mục chứa carla.exe (portable
|
||||
zip). Resolve về đường dẫn exe hợp lệ → ghi config → xóa cache detect.
|
||||
Trả đường dẫn exe, hoặc '' nếu không hợp lệ."""
|
||||
path = (path or "").strip().strip('"').strip()
|
||||
exe = ""
|
||||
if os.path.isfile(path) and os.path.basename(path).lower() in ("carla.exe", "carla-single.exe", "carla"):
|
||||
exe = path
|
||||
elif os.path.isdir(path):
|
||||
try:
|
||||
for root, dirs, files in os.walk(path):
|
||||
depth = root[len(path):].count(os.sep)
|
||||
if depth >= 2:
|
||||
dirs[:] = []
|
||||
continue
|
||||
for f in files:
|
||||
if f.lower() == "carla.exe":
|
||||
exe = os.path.join(root, f)
|
||||
break
|
||||
if exe:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not exe:
|
||||
return ""
|
||||
try:
|
||||
os.makedirs(settings.STORAGE_DIR, exist_ok=True)
|
||||
with open(_carla_config_path(), "w", encoding="utf-8") as f:
|
||||
json.dump({"carla_path": exe, "carla_exe": os.path.basename(exe)}, f, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
return ""
|
||||
# Xóa cache detect/find_carla để capabilities phản ánh ngay
|
||||
invalidate_carla_cache()
|
||||
return exe
|
||||
|
||||
|
||||
def _carla_from_registry() -> str:
|
||||
"""Windows: Carla cài qua installer có thể ghi registry (best-effort)."""
|
||||
if not _is_windows():
|
||||
return ""
|
||||
try:
|
||||
import winreg
|
||||
for hive, key in (
|
||||
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Carla"),
|
||||
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Carla"),
|
||||
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Carla"),
|
||||
):
|
||||
try:
|
||||
with winreg.OpenKey(hive, key) as k:
|
||||
val, _ = winreg.QueryValueEx(k, "InstallPath")
|
||||
p = os.path.join(str(val), "carla.exe")
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _find_carla_exe_bounded(base: str, max_depth: int = 3) -> str:
|
||||
"""Quét nông tìm carla.exe (bản portable giải nén thường nằm Downloads/
|
||||
Desktop/Documents). Giới hạn độ sâu + bỏ qua thư mục lớn — KHÔNG quét
|
||||
toàn ổ đĩa."""
|
||||
if not base or not os.path.isdir(base):
|
||||
return ""
|
||||
skip = ("node_modules", ".cache", "AppData", ".git", "venv", ".venv",
|
||||
"__pycache__", "$RECYCLE.BIN", "Windows", "Program Files",
|
||||
"Program Files (x86)")
|
||||
try:
|
||||
for root, dirs, files in os.walk(base):
|
||||
depth = root[len(base):].count(os.sep) if root != base else 0
|
||||
if depth >= max_depth:
|
||||
dirs[:] = []
|
||||
continue
|
||||
for f in files:
|
||||
if f.lower() == "carla.exe":
|
||||
return os.path.join(root, f)
|
||||
dirs[:] = [d for d in dirs if d not in skip and not d.startswith(".")]
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _carla_from_shallow_search() -> str:
|
||||
"""Windows: tìm carla.exe trong Downloads/Desktop/Documents/Home."""
|
||||
if not _is_windows():
|
||||
return ""
|
||||
home = os.environ.get("USERPROFILE") or os.path.expanduser("~")
|
||||
bases = []
|
||||
for sub in ("Downloads", "Desktop", "Documents"):
|
||||
bases.append(os.path.join(home, sub))
|
||||
bases.append(home)
|
||||
for base in bases:
|
||||
hit = _find_carla_exe_bounded(base)
|
||||
if hit:
|
||||
return hit
|
||||
return ""
|
||||
|
||||
|
||||
def _carla_candidates() -> list:
|
||||
"""Các vị trí Carla — theo thứ tự ưu tiên:
|
||||
1) user định vị (config file — bản portable bắt buộc dùng cách này)
|
||||
2) PATH (shutil.which)
|
||||
3) registry (Windows, nếu cài qua installer)
|
||||
4) thư mục cài đặt chuẩn (Program Files...)
|
||||
5) quét nông Downloads/Desktop/Documents (Windows) / AppImage (Linux)"""
|
||||
cands = []
|
||||
configured = get_configured_carla()
|
||||
if configured:
|
||||
cands.append(configured)
|
||||
try:
|
||||
which = shutil.which("carla")
|
||||
if which:
|
||||
cands.append(which)
|
||||
except Exception:
|
||||
pass
|
||||
reg = _carla_from_registry()
|
||||
if reg:
|
||||
cands.append(reg)
|
||||
try:
|
||||
if _is_windows():
|
||||
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
||||
lpf = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
||||
la = os.environ.get("LOCALAPPDATA", "")
|
||||
for base in (pf, lpf, la):
|
||||
for rel in ("Carla\\carla.exe", "Carla\\bin\\carla.exe", "Programs\\Carla\\carla.exe"):
|
||||
p = os.path.join(base, rel) if base else ""
|
||||
if p and os.path.isfile(p):
|
||||
cands.append(p)
|
||||
elif _is_linux():
|
||||
home = os.path.expanduser("~")
|
||||
for p in ("/usr/bin/carla", "/usr/local/bin/carla", "/opt/carla/carla"):
|
||||
if os.path.isfile(p):
|
||||
cands.append(p)
|
||||
try:
|
||||
for f in os.listdir(home):
|
||||
if f.lower().startswith("carla") and f.lower().endswith(".appimage"):
|
||||
cands.append(os.path.join(home, f))
|
||||
except Exception:
|
||||
pass
|
||||
elif _is_macos():
|
||||
for p in ("/Applications/Carla.app/Contents/MacOS/Carla", "/Applications/Carla.app/Contents/MacOS/carla"):
|
||||
if os.path.isfile(p):
|
||||
cands.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
shallow = _carla_from_shallow_search()
|
||||
if shallow:
|
||||
cands.append(shallow)
|
||||
# Dedup giữ thứ tự
|
||||
seen, out = set(), []
|
||||
for c in cands:
|
||||
if c not in seen:
|
||||
seen.add(c)
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def find_carla() -> str:
|
||||
"""Đường dẫn Carla đầu tiên tìm thấy, hoặc '' nếu chưa có.
|
||||
|
||||
Lưu ý license: Carla GPL-2.0+ — app KHÔNG bundle/nhúng, chỉ spawn tiến
|
||||
trình ngoài (user tự cài/giải nén) → không dính copyleft."""
|
||||
for c in _carla_candidates():
|
||||
return c
|
||||
return ""
|
||||
|
||||
|
||||
def invalidate_carla_cache():
|
||||
"""Xóa cache detect/find_carla — gọi sau khi user định vị Carla mới."""
|
||||
find_carla.cache_clear()
|
||||
detect.cache_clear()
|
||||
|
||||
|
||||
def _detect_platform() -> str:
|
||||
if _is_windows():
|
||||
return "windows"
|
||||
@@ -355,7 +135,6 @@ def detect() -> dict:
|
||||
"""Detect 1 lần (cache toàn cục) — kết quả bất biến trong 1 tiến trình."""
|
||||
platform = _detect_platform()
|
||||
runtime = _detect_runtime()
|
||||
carla = find_carla() if runtime == "desktop" else ""
|
||||
docker = _in_docker()
|
||||
return {
|
||||
"platform": platform,
|
||||
@@ -364,8 +143,6 @@ def detect() -> dict:
|
||||
"environment": "docker" if docker else "standalone", # luồng âm instrument: docker→FluidSynthWASM, standalone→native OS
|
||||
"has_display": _has_display(),
|
||||
"tauri_bridge": _tauri_bridge_ready(),
|
||||
"carla_path": carla,
|
||||
"carla_local": bool(carla), # chỉ có ý nghĩa khi runtime=desktop
|
||||
"vst_render": _vst_render_available(),
|
||||
"default_vst_dirs": default_vst_dirs(),
|
||||
"default_soundfont_dirs": default_soundfont_dirs(),
|
||||
@@ -374,13 +151,23 @@ def detect() -> dict:
|
||||
|
||||
|
||||
def _vst_render_available() -> bool:
|
||||
"""pedalboard có sẵn không (dùng find_spec — KHÔNG import pedalboard để
|
||||
tránh kéo JUCE lib; giống pattern _module_available trong vst_engine)."""
|
||||
import importlib.util
|
||||
try:
|
||||
return importlib.util.find_spec("pedalboard") is not None
|
||||
except (ImportError, AttributeError, ValueError):
|
||||
return False
|
||||
"""native_bridge có sẵn không: SF_BRIDGE_PATH trỏ file, hoặc exe trong
|
||||
install/ (không import app.core.native_render để tránh kéo numpy — mirror
|
||||
logic find_bridge_exe)."""
|
||||
env = os.getenv("SF_BRIDGE_PATH", "").strip()
|
||||
if env and os.path.isfile(env):
|
||||
return True
|
||||
exe = "daw_vst_bridge.exe" if os.name == "nt" else "daw_vst_bridge"
|
||||
if os.path.isfile(os.path.join(settings.BASE_DIR, "install", exe)):
|
||||
return True
|
||||
# Frozen onedir: bridge sidecar nam canh app root (ngoai _MEIPASS)
|
||||
if getattr(sys, "frozen", False):
|
||||
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
for cand in (os.path.join(exe_dir, exe),
|
||||
os.path.join(os.path.dirname(exe_dir), exe)):
|
||||
if os.path.isfile(cand):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _lan_info() -> dict:
|
||||
@@ -401,12 +188,10 @@ def capabilities() -> dict:
|
||||
"""Capabilities API — frontend gọi 1 lần lúc boot để bật/tắt tính năng."""
|
||||
d = detect()
|
||||
features = {
|
||||
"carla_local": d["carla_local"],
|
||||
"carla_path": d["carla_path"],
|
||||
"preset_upload": True, # cả 2 mode đều upload preset được
|
||||
"vst_render": d["vst_render"],
|
||||
"tauri_bridge": d["tauri_bridge"],
|
||||
# preview VSTi: quick_render (pedalboard render clip ngắn — âm thật,
|
||||
# preview VSTi: quick_render (native_bridge render clip ngắn — âm thật,
|
||||
# giống export) là chuẩn cho cả 2 mode; không có realtime trong web UI.
|
||||
"preview_mode": "quick_render" if d["vst_render"] else "wasm",
|
||||
}
|
||||
|
||||
@@ -78,7 +78,8 @@ class SoundFontConverter:
|
||||
|
||||
def _check_ffmpeg_ogg(self):
|
||||
try:
|
||||
r = subprocess.run(["ffmpeg", "-encoders"], capture_output=True, text=True, timeout=5)
|
||||
r = subprocess.run(["ffmpeg", "-encoders"], capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||
return "libvorbis" in r.stdout
|
||||
except Exception:
|
||||
return False
|
||||
@@ -98,7 +99,8 @@ class SoundFontConverter:
|
||||
"ffmpeg", "-y", "-i", tmp_wav,
|
||||
"-c:a", "libvorbis", "-q:a", "3",
|
||||
"-f", "ogg", tmp_ogg
|
||||
], capture_output=True, timeout=600, check=True)
|
||||
], capture_output=True, timeout=600, check=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||
with open(tmp_ogg, "rb") as fo:
|
||||
return fo.read()
|
||||
except Exception as e:
|
||||
@@ -320,7 +322,8 @@ class SoundFontConverter:
|
||||
def _find_sf3_converter():
|
||||
for exe in ["fluidsynth", "mscore"]:
|
||||
try:
|
||||
subprocess.run([exe, "--help"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2.0)
|
||||
subprocess.run([exe, "--help"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2.0,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||
return exe
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
continue
|
||||
@@ -353,7 +356,8 @@ class SoundFontConverter:
|
||||
|
||||
if converter == "mscore":
|
||||
cmd = ["mscore", "-o", sf3_path, sf2_path]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||
if result.returncode == 0 and os.path.exists(sf3_path):
|
||||
logger.info(f"Created SF3 via mscore: {sf3_path} ({os.path.getsize(sf3_path)/1024/1024:.1f}MB)")
|
||||
return sf3_path
|
||||
@@ -524,7 +528,8 @@ class SoundFontConverter:
|
||||
fo.write(ogg)
|
||||
r = subprocess.run(
|
||||
["ffmpeg", "-y", "-v", "error", "-i", tmp_ogg, "-f", "s16le", "-ac", "1", tmp_pcm],
|
||||
capture_output=True, timeout=600)
|
||||
capture_output=True, timeout=600,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||
if r.returncode != 0:
|
||||
logger.warning(f"OGG decode failed: {r.stderr.decode(errors='replace')[:120]}")
|
||||
return b""
|
||||
|
||||
+235
-66
@@ -1,14 +1,48 @@
|
||||
# SonicForge Studio VST / VSTi Engine Service
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import json
|
||||
import numpy as np
|
||||
import re
|
||||
import time as _time
|
||||
import functools
|
||||
import threading
|
||||
from ctypes import c_char_p
|
||||
import subprocess
|
||||
|
||||
|
||||
# Native render (bridge/pyfluidsynth) không thread-safe: tuần tự hóa
|
||||
RENDER_LOCK = threading.RLock()
|
||||
|
||||
|
||||
def _render_locked(fn):
|
||||
"""Chạy hàm native (VST3/pyfluidsynth) dưới RENDER_LOCK — không thread-safe."""
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
with RENDER_LOCK:
|
||||
return fn(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def _ensure_fluidsynth_dll_on_path():
|
||||
"""Frozen (PyInstaller): pyfluidsynth tim dll qua ctypes.util.find_library
|
||||
(chi quet PATH). Dat libfluidsynth-3.dll ke exe roi them exe_dir vao PATH
|
||||
truoc khi import fluidsynth - nguoc lai: 'Couldn't find the FluidSynth
|
||||
library' -> /soundfont-render tra 501."""
|
||||
import sys
|
||||
if getattr(sys, 'frozen', False):
|
||||
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
if exe_dir and exe_dir not in os.environ.get('PATH', '').split(os.pathsep):
|
||||
os.environ['PATH'] = exe_dir + os.pathsep + os.environ.get('PATH', '')
|
||||
|
||||
|
||||
_ensure_fluidsynth_dll_on_path()
|
||||
|
||||
|
||||
def midi_note_to_freq(note_number: int) -> float:
|
||||
return 440.0 * (2.0 ** ((note_number - 69) / 12.0))
|
||||
|
||||
|
||||
@_render_locked
|
||||
def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int = 0,
|
||||
program: int = 0, sr: int = 44100, bpm: float = 120.0) -> np.ndarray:
|
||||
"""Native FluidSynth (pyfluidsynth) — render MIDI events bằng soundfont THẬT.
|
||||
@@ -18,6 +52,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int =
|
||||
Xử lí đúng note CHỒNG nhau + đuôi release (event-stream noteon/noteoff).
|
||||
Trả stereo float32 (2, N). Raise RuntimeError nếu pyfluidsynth thiếu / SF
|
||||
không load được."""
|
||||
import numpy as np
|
||||
import fluidsynth as _fs
|
||||
settings = _fs.new_fluid_settings()
|
||||
_fs.fluid_settings_setnum(settings, b'synth.sample-rate', float(sr))
|
||||
@@ -76,7 +111,9 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int =
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@_render_locked
|
||||
def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray:
|
||||
import numpy as np
|
||||
beat_duration_sec = 60.0 / max(30.0, bpm)
|
||||
max_duration_sec = 2.0
|
||||
for event in midi_events:
|
||||
@@ -135,14 +172,9 @@ def _module_available(name: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def check_pedalboard_safe():
|
||||
return _module_available("pedalboard")
|
||||
|
||||
|
||||
def check_pyfluidsynth_safe():
|
||||
return _module_available("fluidsynth")
|
||||
|
||||
HAS_PEDALBOARD = check_pedalboard_safe()
|
||||
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
|
||||
|
||||
def ensure_pyfluidsynth():
|
||||
@@ -151,12 +183,6 @@ def ensure_pyfluidsynth():
|
||||
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
|
||||
return HAS_PYFLUIDSYNTH
|
||||
|
||||
if HAS_PEDALBOARD:
|
||||
try:
|
||||
from pedalboard import VST3Plugin, Pedalboard, Gain
|
||||
except Exception:
|
||||
HAS_PEDALBOARD = False
|
||||
|
||||
if HAS_PYFLUIDSYNTH:
|
||||
try:
|
||||
import fluidsynth
|
||||
@@ -186,22 +212,43 @@ def _load_user_plugin_dirs() -> list:
|
||||
return []
|
||||
|
||||
|
||||
def _user_plugin_dirs_configured() -> bool:
|
||||
"""User ĐÃ lưu plugin_dirs trong Plugin Manager (kể cả danh sách rỗng)?
|
||||
|
||||
Phân biệt "chưa cấu hình" (fallback thư mục mặc định VST_DIR) với "đã
|
||||
xóa hết dir" (chỉ scan đúng dir user khai - rỗng thì không scan gì).
|
||||
Nếu không phân biệt, remove hết dir rồi re-scan vẫn còn VST trong thư
|
||||
mục mặc định (vd C:\\Program Files\\Common Files\\VST3, iZotope Ozone Pro
|
||||
load fail 0x7E vẫn hiện trong MASTERING PANEL)."""
|
||||
try:
|
||||
from app.config import settings as _st
|
||||
path = os.path.join(_st.STORAGE_DIR, "plugin_dirs.json")
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return "plugin_dirs" in data
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def get_plugin_manager(vst_dir=None, sf_dir=None, upload_sf_dir=None) -> "PluginManager":
|
||||
"""Singleton: reuse PluginManager when args match, else create new.
|
||||
Default dirs từ settings (env/.env/docker-compose hoặc user override).
|
||||
VST scan gộp thêm plugin_dirs user (Plugin Manager) — nút Synth phải liệt
|
||||
kê được VSTi đã scan và load_vst phải tìm thấy chúng khi render."""
|
||||
kê được VSTi đã scan."""
|
||||
from app.config import settings as _st
|
||||
vst_dir = vst_dir or _st.VST_DIR
|
||||
sf_dir = sf_dir or _st.SOUNDFONT_DIR
|
||||
upload_sf_dir = upload_sf_dir or _st.STORAGE_DIR + "/soundfonts"
|
||||
extra = _load_user_plugin_dirs()
|
||||
user_override = _user_plugin_dirs_configured()
|
||||
global _PLUGIN_MANAGER_INSTANCE, _PLUGIN_MANAGER_ARGS
|
||||
args = (vst_dir, sf_dir, upload_sf_dir, tuple(extra))
|
||||
args = (vst_dir, sf_dir, upload_sf_dir, tuple(extra), user_override)
|
||||
if _PLUGIN_MANAGER_INSTANCE is not None and _PLUGIN_MANAGER_ARGS == args:
|
||||
return _PLUGIN_MANAGER_INSTANCE
|
||||
_PLUGIN_MANAGER_ARGS = args
|
||||
_PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir, extra_vst_dirs=extra)
|
||||
_PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir, extra_vst_dirs=extra, user_dirs_override=user_override)
|
||||
return _PLUGIN_MANAGER_INSTANCE
|
||||
|
||||
def load_soundfont_cached(path: str):
|
||||
@@ -249,21 +296,61 @@ def release_soundfont(path: str):
|
||||
else:
|
||||
_FLUID_CACHE[path] = (fl, ref - 1)
|
||||
|
||||
def _classify_fx_category(name, path=""):
|
||||
"""Heuristic phân loại VST FX cho Plugin Picker (spec vst_fx_panel_integration).
|
||||
C++ scan có subCategoriesString (VD 'Fx|EQ') nhưng không emit ra JSON; dùng
|
||||
tên plugin để xếp category. Chỉ ảnh hưởng UI grouping, không ảnh hưởng audio."""
|
||||
s = (" " + name + " " + path).lower()
|
||||
if any(k in s for k in ("eq", "equalizer", "filter", "graphic", "parametric")) or re.search(r"(^|[^a-z0-9])q([0-9]|[^a-z]|$)", s):
|
||||
return "equalizer"
|
||||
if any(k in s for k in ("reverb", "delay", "echo", "chorus", "hall", "space")):
|
||||
return "reverb_delay"
|
||||
if any(k in s for k in ("comp", "limiter", "maximizer", "gate", "leveler", "de-ess")):
|
||||
return "comp_limiter"
|
||||
if any(k in s for k in ("distortion", "drive", "satur", "overdrive", "fuzz", "amp")):
|
||||
return "distortion"
|
||||
return "utility"
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None, extra_vst_dirs=None):
|
||||
def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None, extra_vst_dirs=None, user_dirs_override=False):
|
||||
from app.config import settings as _st
|
||||
self.vst_dir = vst_dir or _st.VST_DIR
|
||||
self.sf_dir = sf_dir or _st.SOUNDFONT_DIR
|
||||
self.upload_sf_dir = upload_sf_dir
|
||||
# Thư mục VST thêm (plugin_dirs user scan trong Plugin Manager) —
|
||||
# list_available()/load_vst phải thấy VSTi user đã scan (bug: nút
|
||||
# list_available() phải thấy VSTi user đã scan (bug: nút
|
||||
# Synth chỉ quét vst_dir env mặc định /opt/daw_engine/vst3).
|
||||
self.extra_vst_dirs = [d for d in (extra_vst_dirs or []) if d]
|
||||
# user_dirs_override=True: user ĐÃ lưu plugin_dirs (kể cả rỗng) →
|
||||
# chỉ scan đúng dir user khai, KHÔNG thêm vst_dir mặc định. Nếu không,
|
||||
# remove hết dir rồi re-scan vẫn còn VST trong thư mục mặc định
|
||||
# (vd C:\Program Files\Common Files\VST3 → iZotope 0x7E vẫn hiện).
|
||||
self.user_dirs_override = bool(user_dirs_override)
|
||||
self._sf_scan_cache = None # cache for _scan_soundfonts()
|
||||
self._fx_scan_cache = None # cache for list_fx()
|
||||
self._fx_scan_all = None # cache for list_scan_all()
|
||||
self._fx_scan_all_ts = 0.0 # timestamp cache (TTL — tránh kẹt [] vĩnh viễn)
|
||||
|
||||
def _vst_scan_dirs(self) -> list:
|
||||
"""Thư mục quét VST: user đã lưu plugin_dirs → chỉ đúng dir user khai
|
||||
(rỗng = không scan gì); chưa cấu hình → vst_dir mặc định + dir user."""
|
||||
if self.user_dirs_override:
|
||||
return list(self.extra_vst_dirs)
|
||||
return [self.vst_dir] + list(self.extra_vst_dirs)
|
||||
|
||||
FX_SCAN_TTL = 300.0 # giây: cache scan VST tự hết hạn → MASTERING PANEL
|
||||
# không bao giờ kẹt danh sách cũ/rỗng; refresh=true vẫn ép scan ngay.
|
||||
|
||||
def invalidate_fx_scan(self):
|
||||
"""Clear cache scan VST FX — Plugins Manager bấm Scan phải gọi để
|
||||
MASTERING PANEL / FX Rack thấy plugin mới ngay lần mở kế tiếp."""
|
||||
self._fx_scan_all = None
|
||||
self._fx_scan_all_ts = 0.0
|
||||
self._fx_scan_cache = None
|
||||
|
||||
def _scan_plugins(self) -> dict:
|
||||
plugins = {}
|
||||
for scan_dir in [self.vst_dir] + self.extra_vst_dirs:
|
||||
for scan_dir in self._vst_scan_dirs():
|
||||
if not scan_dir or not os.path.isdir(scan_dir):
|
||||
continue
|
||||
for root, dirs, files in os.walk(scan_dir):
|
||||
@@ -271,9 +358,27 @@ class PluginManager:
|
||||
for d in list(dirs):
|
||||
if d.lower().endswith(".vst3"):
|
||||
plugins[os.path.splitext(d)[0]] = os.path.join(root, d)
|
||||
# Bỏ qua file NẰM TRONG folder .vst3 (vd ...\Ozone Pro.vst3\
|
||||
# iZOzonePro.dll): folder .vst3 đã là 1 plugin — file .dll bên
|
||||
# trong tạo entry TRÙNG path khác (không khớp block set của
|
||||
# list_scan_all → FX Ozone/Neutron lọt vào nút Synth).
|
||||
if any(p.lower().endswith(".vst3")
|
||||
for p in root.replace("/", os.sep).split(os.sep)):
|
||||
continue
|
||||
# iZotope cài kiểu: thư mục chứa CẢ file .vst3 lẫn file .dll
|
||||
# (iZOzonePro.dll là module/dependency, KHÔNG phải plugin độc
|
||||
# lập — bridge scan chỉ nhận .vst3 là FX). Thư mục nào đã có
|
||||
# file .vst3 → bỏ qua .dll (VST2 thật như Kong Audio vẫn giữ
|
||||
# vì thư mục của nó không có .vst3).
|
||||
has_vst3_file = any(f.lower().endswith(".vst3") for f in files)
|
||||
for file in files:
|
||||
low = file.lower()
|
||||
if low.endswith(".vst3") or low.endswith(".so") or low.endswith(".dll"):
|
||||
if low.endswith(".vst3") or low.endswith(".so"):
|
||||
plugin_path = os.path.join(root, file)
|
||||
plugin_name = os.path.splitext(file)[0]
|
||||
if plugin_name not in plugins:
|
||||
plugins[plugin_name] = plugin_path
|
||||
elif low.endswith(".dll") and not has_vst3_file:
|
||||
plugin_path = os.path.join(root, file)
|
||||
plugin_name = os.path.splitext(file)[0]
|
||||
if plugin_name not in plugins:
|
||||
@@ -320,22 +425,6 @@ class PluginManager:
|
||||
}
|
||||
return list(sf_map.values())
|
||||
|
||||
def load_vst(self, plugin_name: str, preset_data: dict = None):
|
||||
if not HAS_PEDALBOARD:
|
||||
return None
|
||||
plugins = self._scan_plugins()
|
||||
if plugin_name not in plugins:
|
||||
return None
|
||||
path = plugins[plugin_name]
|
||||
vst = VST3Plugin(path)
|
||||
if preset_data:
|
||||
for k, v in preset_data.items():
|
||||
try:
|
||||
setattr(vst, k, v)
|
||||
except Exception:
|
||||
pass
|
||||
return vst
|
||||
|
||||
def _scan_soundfonts_cached(self):
|
||||
if self._sf_scan_cache is not None:
|
||||
return self._sf_scan_cache
|
||||
@@ -438,22 +527,111 @@ class PluginManager:
|
||||
return presets[:256]
|
||||
|
||||
def list_available(self) -> dict:
|
||||
plugins = self._scan_plugins()
|
||||
return {
|
||||
"vst_instruments": [
|
||||
{"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD}
|
||||
for k in self._scan_plugins().keys()
|
||||
{"id": k, "name": k, "path": p,
|
||||
"type": "VST3" if p.lower().endswith(".vst3") else "VST2",
|
||||
"has_native_support": True}
|
||||
for k, p in plugins.items()
|
||||
],
|
||||
"soundfonts": self._scan_soundfonts()
|
||||
}
|
||||
|
||||
def list_fx(self, refresh=False) -> list:
|
||||
"""VST3 effect plugins (is_fx) từ native_bridge --scan (SEH-safe).
|
||||
|
||||
Bridge tự phân loại FX/instrument theo VST3 subCategory; VST2 (.dll/.so)
|
||||
không introspect được nên bridge báo instrument — bỏ qua ở đây. Cache
|
||||
process-wide; refresh=True quét lại (Plugins Manager → Scan).
|
||||
|
||||
UNKNOWN entries (bridge không đọc được module — vd Ozone 11 treo khi
|
||||
scan) cũng được đưa vào đây: không xác định là instrument → mặc định
|
||||
xem như FX để MASTERING PANEL không mất plugin; nút Synth loại chúng
|
||||
qua is_unknown (xem plugins.py /available).
|
||||
"""
|
||||
all_entries = self.list_scan_all(refresh=refresh)
|
||||
results = [e for e in all_entries
|
||||
if e.get("is_fx") or e.get("is_unknown")]
|
||||
results.sort(key=lambda x: x["name"].lower())
|
||||
self._fx_scan_cache = results
|
||||
return results
|
||||
|
||||
def list_scan_all(self, refresh=False) -> list:
|
||||
"""Toàn bộ kết quả `fx_vst_bridge --scan` (FX + instrument + unknown).
|
||||
|
||||
Bridge scan giờ có timeout per-module (30s) và emit entry UNKNOWN
|
||||
(is_fx=false, is_instrument=false) cho module crash/hang (vd Ozone 11
|
||||
Clarity) — không xếp bừa vào instrument. `/available` dùng entry
|
||||
unknown để LOẠI plugin đó khỏi nút Synth (không biết loại → không
|
||||
hiện là instrument), còn list_fx() chỉ lấy is_fx=true.
|
||||
|
||||
Cache có TTL (FX_SCAN_TTL): nếu cache cũ hơn TTL → tự scan lại. QUAN
|
||||
TRỌNG: chỉ cache khi bridge exe tồn tại — nếu chưa tìm thấy bridge
|
||||
(vd app mới khởi động, sidecar chưa ready) thì KHÔNG lưu kết quả rỗng
|
||||
vào cache, để lần gọi sau thử lại (trước đây kẹt [] vĩnh viễn →
|
||||
MASTERING PANEL không hiển thị VST FX đã scan trong Plugin Manager).
|
||||
"""
|
||||
now = _time.time()
|
||||
if (not refresh and self._fx_scan_all is not None
|
||||
and (now - self._fx_scan_all_ts) < self.FX_SCAN_TTL):
|
||||
return self._fx_scan_all
|
||||
from app.core.native_render import find_fx_bridge_exe
|
||||
exe = find_fx_bridge_exe()
|
||||
entries = []
|
||||
if exe:
|
||||
dirs = [d for d in self._vst_scan_dirs() if d and os.path.isdir(d)]
|
||||
for d in dirs:
|
||||
try:
|
||||
r = subprocess.run([exe, "--scan", d], capture_output=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
|
||||
text=True, timeout=300)
|
||||
except Exception:
|
||||
continue
|
||||
if r.returncode != 0:
|
||||
continue
|
||||
try:
|
||||
data = json.loads(r.stdout[r.stdout.find("{"):])
|
||||
except Exception:
|
||||
continue
|
||||
for p in data.get("plugins", []):
|
||||
path = p.get("path", "")
|
||||
if not path:
|
||||
continue
|
||||
is_fx = bool(p.get("is_fx"))
|
||||
is_instr = bool(p.get("is_instrument"))
|
||||
name = p.get("name") or os.path.splitext(os.path.basename(path))[0]
|
||||
entries.append({
|
||||
"id": name, "name": name, "path": path,
|
||||
"type": p.get("type", "VST3"),
|
||||
"is_fx": is_fx,
|
||||
"is_instrument": is_instr,
|
||||
"is_unknown": not is_fx and not is_instr,
|
||||
"category": _classify_fx_category(name, path),
|
||||
})
|
||||
seen, uniq = set(), []
|
||||
for e in entries:
|
||||
if e["path"] in seen:
|
||||
continue
|
||||
# Plugin file đã bị xóa khỏi đĩa (dll/vst3 không còn) → loại khỏi
|
||||
# danh sách ngay (không cache entry chết → MASTERING PANEL / nút
|
||||
# Synth không còn hiện plugin không dùng được).
|
||||
if not os.path.exists(e["path"]):
|
||||
continue
|
||||
seen.add(e["path"])
|
||||
uniq.append(e)
|
||||
# Chỉ cache khi bridge tồn tại: kết quả (kể cả rỗng — dir không có
|
||||
# FX) là đáng tin; khi thiếu bridge thì trả [] nhưng KHÔNG cache.
|
||||
if exe:
|
||||
self._fx_scan_all = uniq
|
||||
self._fx_scan_all_ts = now
|
||||
return uniq
|
||||
|
||||
@staticmethod
|
||||
def midi_events_to_messages(midi_events: list, bpm: float, sr: int, bank: int = None, program: int = None) -> list:
|
||||
if not HAS_PEDALBOARD:
|
||||
return []
|
||||
beat_duration_sec = 60.0 / max(30.0, bpm)
|
||||
# pedalboard >= 0.9: MIDI messages là tuple (bytes, timestamp_seconds)
|
||||
# (MidiMessage class đã bị bỏ). bytes là raw MIDI event:
|
||||
# 0x9n note_on, 0x8n note_off, 0xB0 control_change, 0xC0 program_change
|
||||
# MIDI message = (bytes raw MIDI, timestamp_seconds). bytes là raw MIDI
|
||||
# event: 0x9n note_on, 0x8n note_off, 0xB0 control_change, 0xC0 program_change
|
||||
messages = []
|
||||
if bank is not None:
|
||||
messages.append((bytes([0xB0, 0, int(bank) & 0x7F]), 0.0))
|
||||
@@ -486,30 +664,20 @@ class DecentSamplerManager:
|
||||
self.vst_path = vst_path
|
||||
|
||||
def create_decent_sampler_instance(self, dspreset_path: str):
|
||||
if not HAS_PEDALBOARD:
|
||||
raise RuntimeError("pedalboard not available")
|
||||
if not os.path.exists(self.vst_path):
|
||||
raise FileNotFoundError(f"DecentSampler VST3 not found at {self.vst_path}")
|
||||
if not os.path.exists(dspreset_path):
|
||||
raise FileNotFoundError(f"Preset file not found at {dspreset_path}")
|
||||
|
||||
plugin = VST3Plugin(self.vst_path)
|
||||
|
||||
abs_preset = os.path.abspath(dspreset_path)
|
||||
preset_dir = os.path.dirname(abs_preset)
|
||||
cwd_before = os.getcwd()
|
||||
try:
|
||||
os.chdir(preset_dir)
|
||||
plugin.load_preset(abs_preset)
|
||||
finally:
|
||||
os.chdir(cwd_before)
|
||||
|
||||
return plugin
|
||||
# Phase 4 (PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md §3.4): .dspreset là
|
||||
# format riêng của DecentSampler (không phải VST3 state) — spike đã fail
|
||||
# cả trên pedalboard (container: "failed to load data from preset file").
|
||||
# pedalboard GPL-3.0 bị gỡ → chặn Pianobook + hướng dẫn convert.
|
||||
raise NotImplementedError(
|
||||
"Pianobook .dspreset không còn được hỗ trợ (pedalboard GPL-3.0 đã gỡ). "
|
||||
"Chuyển đổi: mở preset trong DecentSampler → save .vstpreset (VST3 state) "
|
||||
"rồi upload .vstpreset qua web UI — native_bridge đọc được."
|
||||
)
|
||||
|
||||
|
||||
# ── Preset bridge (Carla ↔ pedalboard) ─────────────────────────────────────
|
||||
# Carla (chạy ngoài, user tự cài — GPL-2.0+ nên app KHÔNG bundle/nhúng) dùng
|
||||
# để mở native GUI VSTi + xuất file preset (.vstpreset từ nút Save của plugin).
|
||||
# ── Preset bridge (native GUI ↔ pedalboard) ───────────────────────────────
|
||||
# User chỉnh preset trong native GUI của VSTi (bridge mở cửa sổ plugin) rồi
|
||||
# xuất file preset (.vstpreset từ nút Save của plugin).
|
||||
# File preset nằm trong thư viện storage/presets (upload qua web UI hoặc picker
|
||||
# local trên Windows) → render_engine tải qua apply_preset_to_plugin() khi render.
|
||||
PRESET_EXTENSIONS = (".vstpreset", ".fxp", ".fxb", ".dspreset")
|
||||
@@ -569,7 +737,8 @@ def resolve_plugin_path(name_or_path: str, ext: str = "") -> str:
|
||||
dirs = list(_load_user_plugin_dirs()) or []
|
||||
except Exception:
|
||||
pass
|
||||
dirs += [settings.VST_DIR, settings.SOUNDFONT_DIR]
|
||||
from app.config import settings as _st
|
||||
dirs += [_st.VST_DIR, _st.SOUNDFONT_DIR]
|
||||
for base_dir in dirs:
|
||||
if not os.path.isdir(base_dir):
|
||||
continue
|
||||
@@ -582,7 +751,7 @@ def resolve_plugin_path(name_or_path: str, ext: str = "") -> str:
|
||||
return ""
|
||||
|
||||
def apply_preset_to_plugin(plugin, preset_id=None, preset_path=None, preset_data_b64=None) -> bool:
|
||||
"""Gán preset lên plugin pedalboard: bytes nhúng (base64 .vstpreset) ưu
|
||||
"""Gán preset lên plugin VSTi: bytes nhúng (base64 .vstpreset) ưu
|
||||
tiên, sau đó preset_id (thư viện), sau preset_path (file). Trả True nếu
|
||||
áp dụng được; KHÔNG raise (render engine chỉ log warning)."""
|
||||
if plugin is None:
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Gap 13: BWF/INFO metadata chunks cho WAV export (EBU Tech 3285 + RIFF INFO).
|
||||
|
||||
sf.write chi ghi fmt/data — ham nay chen bext (602 bytes fixed + coding history)
|
||||
va LIST/INFO (INAM title, IART artist, ISRC, ICMT comment) ngay truoc data chunk
|
||||
roi ghi lai file. Pure stdlib (struct/datetime).
|
||||
"""
|
||||
import datetime
|
||||
import struct
|
||||
|
||||
_BEXT_FIXED = 602
|
||||
|
||||
|
||||
def _pad(text, length):
|
||||
raw = text.encode("utf-8", errors="replace")
|
||||
return (raw[:length] + b"\x00" * length)[:length]
|
||||
|
||||
|
||||
def _chunk(fourcc, payload):
|
||||
pad = b"\x00" if len(payload) % 2 else b""
|
||||
return fourcc + struct.pack("<I", len(payload)) + payload + pad
|
||||
|
||||
|
||||
def _info_sub(fourcc, text):
|
||||
if not text:
|
||||
return b""
|
||||
return _chunk(fourcc, _pad(text, len(text.encode("utf-8", errors="replace")) + 1))
|
||||
|
||||
|
||||
def _bext(md, now):
|
||||
fixed = bytearray(_BEXT_FIXED)
|
||||
fixed[0:256] = _pad(md.get("description", ""), 256)
|
||||
fixed[256:288] = _pad(md.get("originator", "ScoreFXStudio"), 32)
|
||||
fixed[288:320] = _pad(md.get("originator_reference", ""), 32)
|
||||
fixed[320:330] = _pad((md.get("date") or now.strftime("%Y-%m-%d")), 10)
|
||||
fixed[330:338] = _pad((md.get("time") or now.strftime("%H:%M:%S")), 8)
|
||||
struct.pack_into("<II", fixed, 338, 0, 0) # time reference low/high
|
||||
struct.pack_into("<H", fixed, 346, 1) # version = 1
|
||||
fixed[348:412] = bytes(64) # UMID (zero)
|
||||
struct.pack_into("<hhhhh", fixed, 412, -32768, -32768, -32768, -32768, -32768) # loudness: 0x8000 = unset
|
||||
fixed[422:602] = bytes(180) # reserved
|
||||
coding = md.get("coding_history", "").encode("utf-8", errors="replace")
|
||||
return bytes(fixed) + coding
|
||||
|
||||
|
||||
def patch_bwf_metadata(path, metadata=None):
|
||||
"""Chen bext + LIST/INFO vao WAV tai path (rewrite in-place, giu nguyen PCM bits).
|
||||
|
||||
metadata keys: title, artist, isrc, comment, description, originator,
|
||||
originator_reference, date (YYYY-MM-DD), time (HH:MM:SS), coding_history.
|
||||
Bo qua neu file khong phai RIFF/WAVE hop le hoac metadata rong.
|
||||
"""
|
||||
if not metadata:
|
||||
return
|
||||
md = {k: (v or "").strip() for k, v in metadata.items() if v}
|
||||
if not md:
|
||||
return
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
if len(data) < 12 or data[:4] != b"RIFF" or data[8:12] != b"WAVE":
|
||||
return
|
||||
|
||||
off = 12
|
||||
pre = b""
|
||||
data_chunk = None
|
||||
while off + 8 <= len(data):
|
||||
cid = data[off:off + 4]
|
||||
size = struct.unpack("<I", data[off + 4:off + 8])[0]
|
||||
body = data[off + 8:off + 8 + size]
|
||||
if cid == b"data":
|
||||
data_chunk = (cid, size, body)
|
||||
break
|
||||
pre += _chunk(cid, body)
|
||||
off += 8 + size + (size % 2)
|
||||
if data_chunk is None:
|
||||
return
|
||||
|
||||
now = datetime.datetime.now()
|
||||
info_payload = b"INFO" + b"".join([
|
||||
_info_sub(b"INAM", md.get("title", "")),
|
||||
_info_sub(b"IART", md.get("artist", "")),
|
||||
_info_sub(b"ISRC", md.get("isrc", "")),
|
||||
_info_sub(b"ICMT", md.get("comment", "")),
|
||||
])
|
||||
extra = _chunk(b"bext", _bext(md, now)) + _chunk(b"LIST", info_payload)
|
||||
new_data = data[:12] + pre + extra + _chunk(data_chunk[0], data_chunk[2])
|
||||
new_data = new_data[:4] + struct.pack("<I", len(new_data) - 8) + new_data[8:]
|
||||
with open(path, "wb") as f:
|
||||
f.write(new_data)
|
||||
+4
-2
@@ -17,6 +17,7 @@ from app.api.v1.user_config import router as user_config_router
|
||||
from app.api.v1.ai_proxy import router as ai_proxy_router
|
||||
from app.api.v1.ai_presets import router as ai_presets_router
|
||||
from app.api.v1.plugins import router as plugins_router
|
||||
from app.api.v1.files import router as files_router
|
||||
from app.api.v1.media import router as media_router
|
||||
from app.api.v1.system import router as system_router
|
||||
from app.api.v1.presets import router as presets_router
|
||||
@@ -43,7 +44,7 @@ async def lifespan(app: FastAPI):
|
||||
_SF_SCANNER_STOP.set()
|
||||
|
||||
|
||||
app = FastAPI(title="SonicForge API Engine", lifespan=lifespan)
|
||||
app = FastAPI(title="ScoreFX API Engine", lifespan=lifespan)
|
||||
|
||||
app.add_middleware(GZipMiddleware, minimum_size=500)
|
||||
|
||||
@@ -109,6 +110,7 @@ app.include_router(user_config_router, prefix="/api/v1/user", tags=["user_config
|
||||
app.include_router(ai_proxy_router, prefix="/api/v1/ai", tags=["ai"])
|
||||
app.include_router(ai_presets_router, prefix="/api/v1/ai", tags=["ai"])
|
||||
app.include_router(plugins_router, prefix="/api/v1/plugins", tags=["plugins"])
|
||||
app.include_router(files_router, prefix="/api/v1/files", tags=["files"])
|
||||
app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
|
||||
app.include_router(system_router, prefix="/api/v1/system", tags=["system"])
|
||||
app.include_router(presets_router, prefix="/api/v1/presets", tags=["presets"])
|
||||
@@ -122,7 +124,7 @@ async def health():
|
||||
async def get_index():
|
||||
index_path = os.path.join(settings.TEMPLATES_DIR, "index.html")
|
||||
if not os.path.exists(index_path):
|
||||
return HTMLResponse(content=f"<h1>SonicForge Studio: index.html not found at {index_path}</h1>", status_code=404)
|
||||
return HTMLResponse(content=f"<h1>ScoreFX Studio: index.html not found at {index_path}</h1>", status_code=404)
|
||||
with open(index_path, "r", encoding="utf-8") as file:
|
||||
resp = HTMLResponse(content=file.read(), status_code=200)
|
||||
# no-cache: index.html PHẢI luôn mới (các bundle JS dùng ?v= để bust) —
|
||||
|
||||
@@ -35,6 +35,39 @@
|
||||
"tracks": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/Track" }
|
||||
},
|
||||
"master": {
|
||||
"type": "object",
|
||||
"description": "Master bus strip: per-channel L/R faders + LINK + MUTE",
|
||||
"properties": {
|
||||
"volume_db": { "type": "number", "default": 0.0, "description": "Legacy single fader (fallback khi khong co volumeL_db)" },
|
||||
"volumeL_db": { "type": "number", "default": 0.0 },
|
||||
"volumeR_db": { "type": "number", "default": 0.0 },
|
||||
"link_lr": { "type": "boolean", "default": false, "description": "LINK: keo L/R cung luc (R theo L)" },
|
||||
"muted": { "type": "boolean", "default": false, "description": "MUTE master: zero output" },
|
||||
"mute": { "type": "boolean", "default": false },
|
||||
"bypass": { "type": "boolean", "default": false },
|
||||
"fx_chain": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/FXPlugin" }
|
||||
},
|
||||
"automation": {
|
||||
"type": "object",
|
||||
"description": "Gap 7: master automation lanes (volume_db)",
|
||||
"properties": {
|
||||
"volume_db": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": { "type": "number", "description": "giây" },
|
||||
"db": { "type": "number", "minimum": -30.0, "maximum": 3.0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "is_root", "tracks"]
|
||||
@@ -118,7 +151,19 @@
|
||||
"start_beat": { "type": "number", "description": "Beat offset relative to the start of the source buffer (bar 0)" },
|
||||
"duration_beats": { "type": "number" },
|
||||
"velocity": { "type": "number", "minimum": 0.0, "maximum": 1.0, "default": 0.8 },
|
||||
"pan": { "type": "number", "minimum": -1.0, "maximum": 1.0, "default": 0.0 }
|
||||
"pan": { "type": "number", "minimum": -1.0, "maximum": 1.0, "default": 0.0 },
|
||||
"expression_curve": {
|
||||
"type": "array",
|
||||
"description": "CC11 expression automation: normalized position 0..1 in note, value 0..127",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position_ratio": { "type": "number", "minimum": 0.0, "maximum": 1.0 },
|
||||
"value": { "type": "integer", "minimum": 0, "maximum": 127 }
|
||||
},
|
||||
"required": ["position_ratio", "value"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "pitch", "start_beat", "duration_beats", "velocity"]
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+6404
-1598
File diff suppressed because it is too large
Load Diff
+1270
-395
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -35,6 +35,7 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
|
||||
window.SonicAPI = {
|
||||
login: (username, password) => apiRequest('/api/v1/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
|
||||
localLogin: () => apiRequest('/api/v1/auth/local', { method: 'POST' }),
|
||||
register: (username, email, password) => apiRequest('/api/v1/auth/register', { method: 'POST', body: JSON.stringify({ username, email, password }) }),
|
||||
changePassword: (old_password, new_password) => apiRequest('/api/v1/auth/change-password', { method: 'POST', body: JSON.stringify({ old_password, new_password }) }),
|
||||
getProfile: () => apiRequest('/api/v1/auth/profile', { method: 'GET' }),
|
||||
@@ -59,6 +60,8 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
aiScan: (trackId, fileId, minLoopDuration = 2.0, maxLoopDuration = 6.0) => apiRequest('/api/v1/audio/ai-scan', { method: 'POST', body: JSON.stringify({ track_id: trackId, file_id: fileId, min_loop_duration: minLoopDuration, max_loop_duration: maxLoopDuration }) }),
|
||||
aiCut: (sourceTrackId, fileId, selectionStart, selectionEnd) => apiRequest('/api/v1/audio/ai-cut', { method: 'POST', body: JSON.stringify({ source_track_id: sourceTrackId, file_id: fileId, selection_start: selectionStart, selection_end: selectionEnd }) }),
|
||||
runPythonTool: (toolType, trackId, fileId, timePos = 0.0, freq = 440.0, duration = 2.0, waveType = "sine") => apiRequest('/api/v1/audio/python-tool', { method: 'POST', body: JSON.stringify({ tool_type: toolType, track_id: trackId, file_id: fileId, time_pos: timePos, freq: freq, duration: duration, wave_type: waveType }) }),
|
||||
// Gap 11: A/B reference track — analyze LUFS/dBTP/centroid (path hoac file_id)
|
||||
referenceAnalyze: (payload) => apiRequest('/api/v1/audio/reference-analyze', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
|
||||
getAIConfigs: () => apiRequest('/api/v1/user/config/ai', { method: 'GET' }),
|
||||
saveAIConfigs: (providers) => apiRequest('/api/v1/user/config/ai', { method: 'POST', body: JSON.stringify({ providers }) }),
|
||||
@@ -73,28 +76,22 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
// Runtime capabilities — frontend gọi lúc boot để biết môi trường
|
||||
// (desktop Windows / docker headless) và bật/tắt tính năng
|
||||
getCapabilities: () => apiRequest('/api/v1/system/capabilities', { method: 'GET' }),
|
||||
// Định vị Carla.exe (bản portable zip không cài đặt/PATH) — lưu config
|
||||
setCarlaPath: (path) => apiRequest('/api/v1/system/carla-path', { method: 'POST', body: JSON.stringify({ carla_path: path }) }),
|
||||
// Mở native GUI VSTi trong Carla (chỉ khi runtime=desktop + có Carla local)
|
||||
openInCarla: (pluginName, pluginPath) => apiRequest('/api/v1/plugins/open-in-carla', { method: 'POST', body: JSON.stringify({ plugin_name: pluginName, plugin_path: pluginPath }) }),
|
||||
// Unload Carla bridge: tắt mọi note đang ngân + terminate tiến trình Carla
|
||||
// do app spawn (gọi khi track chuyển từ VSTi sang instrument soundfont)
|
||||
stopCarla: () => apiRequest('/api/v1/plugins/carla-stop', { method: 'POST', body: JSON.stringify({}) }),
|
||||
// Kiểm tra Carla còn sống không (app-spawn) + cổng OSC — quyết định
|
||||
// route MIDI item exclusive qua Carla hay fallback FluidSynth
|
||||
carlaStatus: () => apiRequest('/api/v1/plugins/carla-status', { method: 'GET' }),
|
||||
// Gửi MIDI note (track ARM → Carla OSC) để preview VSTi realtime
|
||||
carlaMidi: (payload) => apiRequest('/api/v1/plugins/carla-midi', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Quick-render preview VSTi (âm thật = âm export, cùng code path)
|
||||
previewInstrument: (payload) => apiRequest('/api/v1/plugins/preview', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Render MIDI notes → WAV bằng VSTi (pedalboard, âm thật) — trả file_id
|
||||
// để UI preview / gán clip vào track / download
|
||||
midiRender: (payload) => apiRequest('/api/v1/plugins/midi-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
listFx: () => apiRequest('/api/v1/plugins/fx', { method: 'GET' }),
|
||||
fxRender: (payload) => apiRequest('/api/v1/plugins/fx-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Realtime VST FX qua SHM bridge (master chain): start session → WS
|
||||
// /ws/fx-realtime/{id} đẩy/kéo block audio; stop → tắt bridge.
|
||||
fxRealtimeStart: (payload) => apiRequest('/api/v1/plugins/fx-realtime/start', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
fxRealtimeStop: (payload) => apiRequest('/api/v1/plugins/fx-realtime/stop', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
fxRealtimeSetChain: (payload) => apiRequest('/api/v1/plugins/fx-realtime/set-chain', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Mở native GUI của VST FX (mastering chain / FX rack) qua bridge
|
||||
openFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Phát dãy MIDI notes realtime qua Carla bridge (OSC) — preview khi
|
||||
// pedalboard không render được plugin (VD VST2)
|
||||
carlaPlayNotes: (payload) => apiRequest('/api/v1/plugins/carla-play-notes', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard
|
||||
// Thư viện preset VST3 (.vstpreset) — cầu nối preset → pedalboard
|
||||
listPresets: () => apiRequest('/api/v1/presets', { method: 'GET' }),
|
||||
uploadPreset: (file, pluginHint) => {
|
||||
const fd = new FormData();
|
||||
@@ -118,6 +115,8 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
bridgeStatus: () => apiRequest('/api/v1/plugins/bridge/status', { method: 'GET' }),
|
||||
bridgeLoad: (payload) => apiRequest('/api/v1/plugins/bridge/load', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
bridgeLog: (lines = 100) => apiRequest(`/api/v1/plugins/bridge/log?lines=${lines}`, { method: 'GET' }),
|
||||
// E6: mở native GUI VSTi qua engine (browser LAN không Tauri) — GUI hiện trên máy chạy server
|
||||
bridgeOpenGui: (payload) => apiRequest('/api/v1/plugins/bridge/gui', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
getAIPresets: () => apiRequest('/api/v1/ai/presets', { method: 'GET' }),
|
||||
saveAIPreset: (preset) => apiRequest('/api/v1/ai/presets', { method: 'POST', body: JSON.stringify(preset) }),
|
||||
deleteAIPreset: (presetId) => apiRequest(`/api/v1/ai/presets/${presetId}`, { method: 'DELETE' }),
|
||||
@@ -127,7 +126,7 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
deleteBackup: (backupId) => apiRequest(`/api/v1/projects/cloud/backups/${backupId}`, { method: 'DELETE' }),
|
||||
cleanupBackups: (keep) => apiRequest('/api/v1/projects/cloud/backups/cleanup', { method: 'POST', body: JSON.stringify({ keep }) }),
|
||||
|
||||
renderProject: (projectJson, outputFilename) => apiRequest('/api/v1/plugins/render', { method: 'POST', body: JSON.stringify({ project_json: projectJson, output_filename: outputFilename }) }),
|
||||
renderProject: (projectJson, outputFilename, options) => apiRequest('/api/v1/plugins/render', { method: 'POST', body: JSON.stringify(Object.assign({ project_json: projectJson, output_filename: outputFilename }, options || {})) }),
|
||||
// Ctrl-S desktop: lưu project ra THƯ MỤC CỦA HỆ ĐIỀU HÀNH
|
||||
// (Documents/SonicForgeDAW/Projects — Windows; ~/SonicForgeDAW/Projects — Linux)
|
||||
saveProjectToDisk: (name, dataJson) => apiRequest('/api/v1/projects/save-to-disk', { method: 'POST', body: JSON.stringify({ name, data_json: dataJson }) }),
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
// note-on — neu da noi dung dest thi KHONG disconnect (disconnect flush ring
|
||||
// -> am phim dang vang bi cat). Chi doi khi dest/track thuc su khac.
|
||||
if (this._connected && this._dest === dest && this._trackId === (trackId || null)) return true;
|
||||
this.disconnect();
|
||||
if (!bridgeNode.isReady()) bridgeNode.init();
|
||||
bridgeNode.connect(dest);
|
||||
this._connected = true;
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
// app/static/js/services/bridgeAudioNode.js
|
||||
// Audio sink: consumes 'bridge-audio' PCM frames (native bridge) and plays them
|
||||
// through a ScriptProcessorNode into the WebAudio graph (track FX / master bus).
|
||||
// into the WebAudio graph. Primary path: AudioWorklet (audio thread, no main
|
||||
// thread jank). Fallback: ScriptProcessorNode when worklet unavailable.
|
||||
// VSTi crackle fix (FIX_VSTI_AUDIO_CRACKLE.md P1/P2): worklet ring absorbs pump
|
||||
// jitter, underrun fades to 0 (no click), overrun drops oldest + crossfade.
|
||||
(function () {
|
||||
// Bridge renders one 256-sample block per ~5.8ms; ScriptProcessor fires once
|
||||
// per 4096 samples (~16 blocks @44.1k). The ring MUST cover one full callback
|
||||
// period or we underrun (silence gaps) — 24 blocks ~= 139ms headroom.
|
||||
var RING_DEPTH = 24;
|
||||
var RING_DEPTH = 24; // fallback ring (blocks of 256 samples)
|
||||
var WORKLET_MAX = 64; // blocks buffered while worklet loads (~371ms @48k)
|
||||
var SP_BUFFER = 4096;
|
||||
var _chunks = []; // [{l: Float32Array(256), r: Float32Array(256)}]
|
||||
var _chunks = []; // fallback: [{l: Float32Array(256), r: Float32Array(256)}]
|
||||
var _preBuffer = []; // pending blocks while worklet module loads
|
||||
var _spn = null;
|
||||
var _gainNode = null;
|
||||
var _workletNode = null;
|
||||
var _ctx = null;
|
||||
var _initialized = false;
|
||||
var _useWorklet = false;
|
||||
var _workletStats = null; // stats tu worklet (overrun/underrun) — getDiag
|
||||
var _dest = null; // destination gain node da noi (chong connect trung)
|
||||
|
||||
function _getCtx() {
|
||||
@@ -22,11 +27,51 @@
|
||||
return _ctx;
|
||||
}
|
||||
|
||||
function _setupWorklet() {
|
||||
if (!_ctx.audioWorklet || !_ctx.audioWorklet.addModule) {
|
||||
console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback');
|
||||
return false;
|
||||
}
|
||||
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608311858';
|
||||
_ctx.audioWorklet.addModule(url).then(function () {
|
||||
if (!_initialized) return; // disconnect() happened while loading
|
||||
if (_useWorklet) return; // already set up (init retry)
|
||||
try {
|
||||
_workletNode = new AudioWorkletNode(_ctx, 'bridge-audio-sink', {
|
||||
numberOfInputs: 0,
|
||||
numberOfOutputs: 1,
|
||||
outputChannelCount: [2]
|
||||
});
|
||||
_workletNode.connect(_gainNode);
|
||||
_workletNode.port.onmessage = function (ev) {
|
||||
if (ev.data && ev.data.type === 'stats') _workletStats = ev.data;
|
||||
};
|
||||
_useWorklet = true;
|
||||
// drain pre-buffer into worklet (order preserved)
|
||||
while (_preBuffer.length) {
|
||||
var b = _preBuffer.shift();
|
||||
_workletNode.port.postMessage({ type: 'audio', l: b.l, r: b.r });
|
||||
}
|
||||
console.log('[BridgeAudioNode] AudioWorklet active');
|
||||
try { if (window.__dbgLog) window.__dbgLog('worklet', 'active pre=' + _preBuffer.length); } catch (e) {}
|
||||
} catch (err) {
|
||||
console.warn('[BridgeAudioNode] AudioWorkletNode failed — ScriptProcessor fallback', err);
|
||||
_useWorklet = false;
|
||||
try { if (window.__dbgLog) window.__dbgLog('worklet', 'NODE_FAIL'); } catch (e) {}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.warn('[BridgeAudioNode] addModule failed — ScriptProcessor fallback', err);
|
||||
try { if (window.__dbgLog) window.__dbgLog('worklet', 'MODULE_FAIL'); } catch (e) {}
|
||||
});
|
||||
return true; // pending — worklet may become active later
|
||||
}
|
||||
|
||||
function init(audioCtx) {
|
||||
if (_initialized) return;
|
||||
_ctx = audioCtx || _getCtx();
|
||||
_gainNode = _ctx.createGain();
|
||||
_gainNode.gain.value = 1.0;
|
||||
// ScriptProcessor created but NOT connected until we know worklet failed.
|
||||
_spn = _ctx.createScriptProcessor(SP_BUFFER, 0, 2);
|
||||
_spn.onaudioprocess = function (e) {
|
||||
var L = e.outputBuffer.getChannelData(0);
|
||||
@@ -34,8 +79,6 @@
|
||||
var outLen = L.length;
|
||||
L.fill(0); R.fill(0);
|
||||
if (!_chunks.length) return;
|
||||
// Drain as many queued blocks as fit in this callback (16 max) — filling
|
||||
// only one block per callback previously played 1/16 of the audio.
|
||||
var written = 0;
|
||||
while (written < outLen && _chunks.length) {
|
||||
var blk = _chunks[0];
|
||||
@@ -46,36 +89,62 @@
|
||||
if (n >= blk.l.length) {
|
||||
_chunks.shift();
|
||||
} else {
|
||||
// Partial block (only possible if block size != 256): keep remainder.
|
||||
blk.l = blk.l.subarray(n);
|
||||
blk.r = blk.r.subarray(n);
|
||||
}
|
||||
}
|
||||
};
|
||||
_spn.connect(_gainNode);
|
||||
if (!_setupWorklet()) {
|
||||
// No worklet at all — go straight to ScriptProcessor.
|
||||
_spn.connect(_gainNode);
|
||||
_useWorklet = false;
|
||||
while (_preBuffer.length) _chunks.push(_preBuffer.shift());
|
||||
while (_chunks.length > RING_DEPTH) _chunks.shift();
|
||||
}
|
||||
_initialized = true;
|
||||
console.log('[BridgeAudioNode] initialized (ring depth ' + RING_DEPTH + ')');
|
||||
}
|
||||
|
||||
function onAudio(l, r) {
|
||||
if (!_initialized) init();
|
||||
_chunks.push({ l: new Float32Array(l), r: new Float32Array(r) });
|
||||
// Drop oldest on overrun — keeps latency bounded (~139ms max).
|
||||
var blk = { l: new Float32Array(l), r: new Float32Array(r) };
|
||||
if (_useWorklet && _workletNode) {
|
||||
_workletNode.port.postMessage({ type: 'audio', l: blk.l, r: blk.r });
|
||||
return;
|
||||
}
|
||||
if (!_useWorklet && _ctx && _ctx.audioWorklet && _ctx.audioWorklet.addModule) {
|
||||
// Worklet loading — buffer temporarily (bounded), avoid fallback churn.
|
||||
_preBuffer.push(blk);
|
||||
while (_preBuffer.length > WORKLET_MAX) _preBuffer.shift();
|
||||
return;
|
||||
}
|
||||
_chunks.push(blk);
|
||||
while (_chunks.length > RING_DEPTH) _chunks.shift();
|
||||
}
|
||||
|
||||
function flush() { _chunks = []; }
|
||||
function flush() {
|
||||
_preBuffer = [];
|
||||
_chunks = [];
|
||||
if (_useWorklet && _workletNode) {
|
||||
try { _workletNode.port.postMessage({ type: 'flush' }); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function getOutputNode() { return _gainNode; }
|
||||
|
||||
function disconnect() {
|
||||
flush();
|
||||
if (_workletNode) {
|
||||
try { _workletNode.disconnect(); } catch (e) {}
|
||||
try { _workletNode.port.close(); } catch (e) {}
|
||||
}
|
||||
if (_spn && _gainNode) {
|
||||
try { _spn.disconnect(); } catch (e) {}
|
||||
try { _gainNode.disconnect(); } catch (e) {}
|
||||
}
|
||||
// Reset state so a later init() can rebuild (AudioRoutingEngine re-connect).
|
||||
_initialized = false;
|
||||
_useWorklet = false;
|
||||
_workletNode = null;
|
||||
_spn = null;
|
||||
_gainNode = null;
|
||||
_ctx = null;
|
||||
@@ -95,6 +164,17 @@
|
||||
_dest = dest;
|
||||
},
|
||||
disconnect: disconnect,
|
||||
isReady: function () { return _initialized; }
|
||||
isReady: function () { return _initialized; },
|
||||
getDiag: function () {
|
||||
return {
|
||||
initialized: _initialized,
|
||||
useWorklet: _useWorklet,
|
||||
hasWorkletNode: !!_workletNode,
|
||||
preBufferLen: _preBuffer.length,
|
||||
chunksLen: _chunks.length,
|
||||
gain: _gainNode ? _gainNode.gain.value : -1,
|
||||
ws: _workletStats
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// app/static/js/services/bridgeAudioWorklet.js
|
||||
// VSTi crackle fix: audio sink chay tren audio thread (khong jank main thread).
|
||||
// - Ring block 256 mau, toi da 32 block (~170ms @48k) — hap thu jitter pump
|
||||
// (V45.27: 16->32 de dung crackle VST2 loop/tremolo — render loop C++ bi
|
||||
// stall khi instrument xu li loop/tremolo nang CPU; 8->16 truoc do da dung
|
||||
// tremolo crackle, 16 van chua du cho stall >85ms). Flush o play start
|
||||
// (app.jsx) xoa ring nen do tre khong tang: ring thuong can o preview.
|
||||
// - Underrun: fade ve 0 thay vi click (hard silence).
|
||||
// - Resume sau silence: fade vao tu 0.
|
||||
// - Overrun: drop block cu nhat + crossfade 64 mau tai diem drop (het click).
|
||||
// - Stats: dem overrun/underrun, gui ve main thread moi ~0.5s — kiem tra
|
||||
// con crackle nao la do ring can (underrun) hay ring tran (overrun).
|
||||
class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this._ring = [];
|
||||
this._MAX = 32;
|
||||
this._gain = 1.0;
|
||||
this._lastOutL = 0.0;
|
||||
this._lastOutR = 0.0;
|
||||
this._skipFade = 0; // >0: con lai cua crossfade dang chay sau overrun drop
|
||||
this._fadeLen = 0; // tong chieu dai crossfade hien tai (cap 1024)
|
||||
this._skipFromL = 0.0;
|
||||
this._skipFromR = 0.0;
|
||||
this._wasUnderrun = false;
|
||||
this._statAccum = 0;
|
||||
this._stats = { overruns: 0, underruns: 0 };
|
||||
this.port.onmessage = (e) => {
|
||||
const m = e.data;
|
||||
if (!m) return;
|
||||
if (m.type === 'audio') {
|
||||
this._ring.push({ l: m.l, r: m.r });
|
||||
while (this._ring.length > this._MAX) {
|
||||
// overrun: bo block cu nhat; lan consume toi se crossfade ranh gioi
|
||||
// V45.32: crossfade tich luy — moi block drop cong 256 mau fade, cap
|
||||
// 1024 (~21ms) thay vi co dinh 64 mau (1.3ms qua ngan -> blip/wow khi
|
||||
// drop loat block o chord nhieu voice = crackle VST2 tremolo/loop)
|
||||
this._ring.shift();
|
||||
this._skipFade = Math.min(this._skipFade + 256, 1024);
|
||||
this._fadeLen = this._skipFade;
|
||||
this._stats.overruns++;
|
||||
}
|
||||
} else if (m.type === 'flush') {
|
||||
this._ring.length = 0;
|
||||
this._gain = 1.0;
|
||||
this._skipFade = 0;
|
||||
this._fadeLen = 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
process(inputs, outputs) {
|
||||
const out = outputs[0];
|
||||
if (!out || out.length < 2) return true;
|
||||
const L = out[0];
|
||||
const R = out[1];
|
||||
const n = L.length;
|
||||
let w = 0;
|
||||
while (w < n) {
|
||||
if (this._ring.length) {
|
||||
if (this._wasUnderrun) this._wasUnderrun = false;
|
||||
const blk = this._ring[0];
|
||||
const avail = blk.l.length;
|
||||
const take = Math.min(avail, n - w);
|
||||
if (this._skipFade > 0) {
|
||||
const done = this._fadeLen - this._skipFade;
|
||||
for (let i = 0; i < take; i++) {
|
||||
const t = (done + i) / this._fadeLen;
|
||||
L[w + i] = (this._lastOutL * (1 - t) + blk.l[i] * t) * this._gain;
|
||||
R[w + i] = (this._lastOutR * (1 - t) + blk.r[i] * t) * this._gain;
|
||||
}
|
||||
this._skipFade = Math.max(0, this._skipFade - take);
|
||||
} else {
|
||||
for (let i = 0; i < take; i++) {
|
||||
L[w + i] = blk.l[i] * this._gain;
|
||||
R[w + i] = blk.r[i] * this._gain;
|
||||
}
|
||||
}
|
||||
this._lastOutL = L[w + take - 1];
|
||||
this._lastOutR = R[w + take - 1];
|
||||
if (this._gain < 1.0) this._gain = Math.min(1.0, this._gain + take / 256.0);
|
||||
w += take;
|
||||
if (take >= avail) {
|
||||
this._ring.shift();
|
||||
} else {
|
||||
blk.l = blk.l.subarray(take);
|
||||
blk.r = blk.r.subarray(take);
|
||||
}
|
||||
} else {
|
||||
// underrun: fade ve 0 (khong click) — dem 1 lan moi lan roi vao trang thai trong
|
||||
if (!this._wasUnderrun) { this._wasUnderrun = true; this._stats.underruns++; }
|
||||
for (let i = w; i < n; i++) {
|
||||
this._gain = Math.max(0, this._gain - 1.0 / 256.0);
|
||||
L[i] = this._lastOutL * this._gain;
|
||||
R[i] = this._lastOutR * this._gain;
|
||||
}
|
||||
this._lastOutL *= this._gain;
|
||||
this._lastOutR *= this._gain;
|
||||
w = n;
|
||||
}
|
||||
}
|
||||
// Stats: gui ve main thread moi ~0.5s (24000 mau @48k) — log overrun/underrun
|
||||
this._statAccum += n;
|
||||
if (this._statAccum >= 24000) {
|
||||
this._statAccum = 0;
|
||||
try {
|
||||
this.port.postMessage({
|
||||
type: 'stats',
|
||||
overruns: this._stats.overruns,
|
||||
underruns: this._stats.underruns,
|
||||
ring: this._ring.length,
|
||||
max: this._MAX
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
registerProcessor('bridge-audio-sink', BridgeAudioSinkProcessor);
|
||||
@@ -0,0 +1,171 @@
|
||||
// SonicForge Studio - Chord Theory Service (BIG_PHASE_3.md §6.1, virtual_keyboard_and_chords_panel_spec.md §III)
|
||||
// Thuần hàm: parse chord symbol / roman numeral → pitch array; áp voicing.
|
||||
// Dùng được cả browser (script tag) lẫn Node (module.exports) cho unit test.
|
||||
|
||||
const ChordTheory = (function() {
|
||||
// Root note name → pitch class (C=0 ... B=11)
|
||||
const NOTE_TO_PC = {
|
||||
'C': 0, 'C#': 1, 'Db': 1, 'D': 2, 'D#': 3, 'Eb': 3, 'E': 4, 'F': 5,
|
||||
'F#': 6, 'Gb': 6, 'G': 7, 'G#': 8, 'Ab': 8, 'A': 9, 'A#': 10, 'Bb': 10, 'B': 11
|
||||
};
|
||||
const ROOT_NAMES = Object.keys(NOTE_TO_PC).sort(function(a, b) { return b.length - a.length; }); // 'C#'/'Bb' trước 'C'/'B'
|
||||
const PC_TO_NAME = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||
|
||||
// Semitone intervals (ascending, relative to root). Octave = 4 mặc định
|
||||
// (C4=60) — đúng spec §III.5: Pitches(Cmaj7) = [60,64,67,71].
|
||||
const QUALITY_INTERVALS = {
|
||||
'': [0, 4, 7],
|
||||
'maj': [0, 4, 7],
|
||||
'm': [0, 3, 7],
|
||||
'min': [0, 3, 7],
|
||||
'-': [0, 3, 7],
|
||||
'maj7': [0, 4, 7, 11],
|
||||
'M7': [0, 4, 7, 11],
|
||||
'7': [0, 4, 7, 10],
|
||||
'm7': [0, 3, 7, 10],
|
||||
'min7': [0, 3, 7, 10],
|
||||
'maj9': [0, 4, 7, 11, 14],
|
||||
'M9': [0, 4, 7, 11, 14],
|
||||
'9': [0, 4, 7, 10, 14],
|
||||
'm9': [0, 3, 7, 10, 14],
|
||||
'min9': [0, 3, 7, 10, 14],
|
||||
'11': [0, 4, 7, 10, 14, 17],
|
||||
'm11': [0, 3, 7, 10, 14, 17],
|
||||
'13': [0, 4, 7, 10, 14, 21],
|
||||
'm13': [0, 3, 7, 10, 14, 21],
|
||||
'add9': [0, 4, 7, 14],
|
||||
'sus2': [0, 2, 7],
|
||||
'sus4': [0, 5, 7],
|
||||
'sus': [0, 5, 7],
|
||||
'7sus4': [0, 5, 7, 10],
|
||||
'dim': [0, 3, 6],
|
||||
'dim7': [0, 3, 6, 9],
|
||||
'aug': [0, 4, 8],
|
||||
'+': [0, 4, 8],
|
||||
'6': [0, 4, 7, 9],
|
||||
'm6': [0, 3, 7, 9],
|
||||
'm7b5': [0, 3, 6, 10],
|
||||
'5': [0, 7]
|
||||
};
|
||||
const QUALITY_KEYS = Object.keys(QUALITY_INTERVALS).sort(function(a, b) { return b.length - a.length; });
|
||||
|
||||
// Roman numeral degree → semitone offset trong major scale (spec: I-V-vi-IV
|
||||
// với key C → C-G-Am-F; vi → A, IV → F, v → G...)
|
||||
const ROMAN_DEGREE = { I: 0, II: 2, III: 4, IV: 5, V: 7, VI: 9, VII: 11 };
|
||||
|
||||
function noteToMidi(rootName, octave) {
|
||||
const pc = NOTE_TO_PC[rootName] !== undefined ? NOTE_TO_PC[rootName] : 0;
|
||||
return 12 * ((octave == null ? 4 : octave) + 1) + pc;
|
||||
}
|
||||
|
||||
// 'Cmaj7' → [60,64,67,71]; 'Am9' → [57,60,64,71]; 'G13' → [67,71,74,77,81,88]
|
||||
function parseChordSymbol(symbol, octave) {
|
||||
if (!symbol) return [];
|
||||
const s = String(symbol).trim();
|
||||
if (!s) return [];
|
||||
const upper = s.toUpperCase();
|
||||
const root = ROOT_NAMES.find(function(n) { return upper.indexOf(n) === 0; });
|
||||
if (!root) return [];
|
||||
// Giữ nguyên case của suffix: 'm' = minor (Am9 → 'm9'), 'M7' = major 7th
|
||||
const suffix = s.slice(root.length);
|
||||
const quality = QUALITY_KEYS.find(function(q) { return suffix === q; }) || '';
|
||||
const intervals = QUALITY_INTERVALS[quality] || QUALITY_INTERVALS[''];
|
||||
const rootMidi = noteToMidi(root, octave);
|
||||
return intervals.map(function(i) { return rootMidi + i; });
|
||||
}
|
||||
|
||||
// Kiểm tra symbol có phải chord name (bắt đầu bằng tên nốt) không
|
||||
function isChordName(token) {
|
||||
const upper = String(token || '').trim().toUpperCase();
|
||||
return ROOT_NAMES.some(function(n) { return upper.indexOf(n) === 0; });
|
||||
}
|
||||
|
||||
// 'ii7' → { degree: 2, minor: true, suffix: '7', accidental: 0 }
|
||||
// 'bVI' → { degree: 9, minor: false, suffix: '', accidental: -1 }
|
||||
function parseRomanToken(token) {
|
||||
const t = String(token || '').trim();
|
||||
// Alternation dài trước (VII trước VI trước V...); /i nhận cả chữ thường
|
||||
const m = t.match(/^(b|#)?(VII|VI|IV|IX|III|II|V|I)(.*)$/i);
|
||||
if (!m) return null;
|
||||
const accidental = m[1] === 'b' ? -1 : (m[1] === '#' ? 1 : 0);
|
||||
const numeral = m[2].toUpperCase();
|
||||
const minor = m[2] !== m[2].toUpperCase(); // 'vi' → minor, 'VI' → major
|
||||
if (ROMAN_DEGREE[numeral] === undefined) return null;
|
||||
const suffix = m[3] || '';
|
||||
let quality = null;
|
||||
const SUFFIX_MAP = {
|
||||
'': function(min) { return min ? 'm' : ''; },
|
||||
'7': function(min) { return min ? 'm7' : '7'; },
|
||||
'9': function(min) { return min ? 'm9' : '9'; },
|
||||
'13': function(min) { return min ? 'm13' : '13'; },
|
||||
'maj7': function() { return 'maj7'; },
|
||||
'M7': function() { return 'maj7'; },
|
||||
'maj9': function() { return 'maj9'; },
|
||||
'add9': function() { return 'add9'; },
|
||||
'6': function(min) { return min ? 'm6' : '6'; },
|
||||
'sus4': function() { return 'sus4'; },
|
||||
'sus2': function() { return 'sus2'; },
|
||||
'7sus4': function() { return '7sus4'; },
|
||||
'dim': function() { return 'dim'; },
|
||||
'dim7': function() { return 'dim7'; },
|
||||
'm7b5': function() { return 'm7b5'; },
|
||||
'aug': function() { return 'aug'; }
|
||||
};
|
||||
const key = SUFFIX_MAP[suffix] || SUFFIX_MAP[''];
|
||||
quality = key ? key(minor) : (minor ? 'm' : '');
|
||||
return { degree: ROMAN_DEGREE[numeral], minor: minor, suffix: suffix, accidental: accidental, quality: quality };
|
||||
}
|
||||
|
||||
// Chuỗi tokens 'I-vi-IV-V' hoặc 'Cmaj7-Am9-Fadd9-G13' → [{ name, pitches }]
|
||||
// key dùng cho roman numeral; voicing: 'root' | 'first' | 'drop2'.
|
||||
function progressionToChords(tokens, key, voicing, octave) {
|
||||
if (!tokens) return [];
|
||||
const parts = String(tokens).split(/[-,\s]+/).filter(function(x) { return x.length > 0; });
|
||||
const keyRoot = key || 'C';
|
||||
return parts.map(function(token) {
|
||||
if (isChordName(token)) {
|
||||
const pitches = parseChordSymbol(token, octave);
|
||||
return { token: token, name: token, pitches: applyVoicing(pitches, voicing) };
|
||||
}
|
||||
const parsed = parseRomanToken(token);
|
||||
if (!parsed) return { token: token, name: token, pitches: [] };
|
||||
const keyPc = NOTE_TO_PC[keyRoot] !== undefined ? NOTE_TO_PC[keyRoot] : 0;
|
||||
const rootPc = (((keyPc + parsed.degree + parsed.accidental) % 12) + 12) % 12;
|
||||
const rootName = PC_TO_NAME[rootPc];
|
||||
const quality = parsed.quality;
|
||||
const name = rootName + quality;
|
||||
const rootMidi = 12 * ((octave == null ? 4 : octave) + 1) + rootPc;
|
||||
const intervals = QUALITY_INTERVALS[quality] || QUALITY_INTERVALS[''];
|
||||
const pitches = intervals.map(function(i) { return rootMidi + i; });
|
||||
return { token: token, name: name, pitches: applyVoicing(pitches, voicing) };
|
||||
});
|
||||
}
|
||||
|
||||
// Root position: giữ nguyên (sorted). First inversion: root +12.
|
||||
// Drop-2: note cao thứ nhì -12 (spec §III.5).
|
||||
function applyVoicing(pitches, voicing) {
|
||||
if (!pitches || !pitches.length) return [];
|
||||
const arr = pitches.slice().sort(function(a, b) { return a - b; });
|
||||
if (voicing === 'first' && arr.length >= 2) {
|
||||
arr[0] = arr[0] + 12;
|
||||
arr.sort(function(a, b) { return a - b; });
|
||||
return arr;
|
||||
}
|
||||
if (voicing === 'drop2' && arr.length >= 3) {
|
||||
arr[arr.length - 2] = arr[arr.length - 2] - 12;
|
||||
arr.sort(function(a, b) { return a - b; });
|
||||
return arr;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
return {
|
||||
parseChordSymbol: parseChordSymbol,
|
||||
parseRomanToken: parseRomanToken,
|
||||
isChordName: isChordName,
|
||||
progressionToChords: progressionToChords,
|
||||
applyVoicing: applyVoicing
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = ChordTheory;
|
||||
@@ -0,0 +1,63 @@
|
||||
// SonicForge Studio - chordTheory.js unit test (BIG_PHASE_3.md §6.4)
|
||||
// Node assert, không framework. Chạy: node app/static/js/services/chordTheory.test.js
|
||||
const assert = require('assert');
|
||||
const ChordTheory = require('./chordTheory.js');
|
||||
|
||||
let fails = 0;
|
||||
function check(name, fn) {
|
||||
try { fn(); console.log('ok: ' + name); }
|
||||
catch (e) { fails++; console.error('FAIL: ' + name + ' — ' + e.message); }
|
||||
}
|
||||
function deep(arr, expected) { assert.deepStrictEqual(arr, expected); }
|
||||
|
||||
// 1. Parse chord symbol → pitch array (octave 4 = C4=60)
|
||||
check('Cmaj7 = [60,64,67,71]', () => deep(ChordTheory.parseChordSymbol('Cmaj7'), [60, 64, 67, 71]));
|
||||
check('Am9 = [69,72,76,79,83]', () => deep(ChordTheory.parseChordSymbol('Am9'), [69, 72, 76, 79, 83]));
|
||||
check('Dm7 = [62,65,69,72]', () => deep(ChordTheory.parseChordSymbol('Dm7'), [62, 65, 69, 72]));
|
||||
check('G13 = [67,71,74,77,81,88]', () => deep(ChordTheory.parseChordSymbol('G13'), [67, 71, 74, 77, 81, 88]));
|
||||
check('Fadd9 = [65,69,72,79]', () => deep(ChordTheory.parseChordSymbol('Fadd9'), [65, 69, 72, 79]));
|
||||
|
||||
// 2. Roman numeral progression với key C
|
||||
check('I-vi-IV-V key C → C-Am-F-G', () => {
|
||||
const prog = ChordTheory.progressionToChords('I-vi-IV-V', 'C', 'root');
|
||||
deep(prog.map(c => c.name), ['C', 'Am', 'F', 'G']);
|
||||
deep(prog.map(c => c.pitches), [[60, 64, 67], [69, 72, 76], [65, 69, 72], [67, 71, 74]]);
|
||||
});
|
||||
check('I-V-vi-IV key C → C-G-Am-F (preset Pop)', () => {
|
||||
const prog = ChordTheory.progressionToChords('I-V-vi-IV', 'C', 'root');
|
||||
deep(prog.map(c => c.name), ['C', 'G', 'Am', 'F']);
|
||||
});
|
||||
check('ii7-V7-Imaj7-VI7 key C → Dm7-G7-Cmaj7-A7', () => {
|
||||
const prog = ChordTheory.progressionToChords('ii7-V7-Imaj7-VI7', 'C', 'root');
|
||||
deep(prog.map(c => c.name), ['Dm7', 'G7', 'Cmaj7', 'A7']);
|
||||
deep(prog[0].pitches, [62, 65, 69, 72]); // Dm7
|
||||
deep(prog[2].pitches, [60, 64, 67, 71]); // Cmaj7
|
||||
});
|
||||
check('i7-iv7-v7-i7 key C → Cm7-Fm7-Gm7-Cm7', () => {
|
||||
const prog = ChordTheory.progressionToChords('i7-iv7-v7-i7', 'C', 'root');
|
||||
deep(prog.map(c => c.name), ['Cm7', 'Fm7', 'Gm7', 'Cm7']);
|
||||
});
|
||||
check('Imaj7-iii7-IVmaj7-V13 key C → Cmaj7-Em7-Fmaj7-G13', () => {
|
||||
const prog = ChordTheory.progressionToChords('Imaj7-iii7-IVmaj7-V13', 'C', 'root');
|
||||
deep(prog.map(c => c.name), ['Cmaj7', 'Em7', 'Fmaj7', 'G13']);
|
||||
});
|
||||
|
||||
// 3. Voicing
|
||||
check('first inversion: root +12', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'first'), [64, 67, 71, 72]));
|
||||
check('drop2: second-highest -12', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'drop2'), [55, 60, 64, 71]));
|
||||
check('root position giữ nguyên', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'root'), [60, 64, 67, 71]));
|
||||
check('jazz drop2: Dm7-G7-Cmaj7-A7 ra đủ pitch', () => {
|
||||
const prog = ChordTheory.progressionToChords('ii7-V7-Imaj7-VI7', 'C', 'drop2');
|
||||
deep(prog.map(c => c.name), ['Dm7', 'G7', 'Cmaj7', 'A7']);
|
||||
assert(prog.every(c => c.pitches.length === 4), 'mỗi chord 4 pitch');
|
||||
});
|
||||
|
||||
// 4. Edge cases
|
||||
check('chuỗi chord name trực tiếp Cmaj7-Am9-Fadd9-G13', () => {
|
||||
const prog = ChordTheory.progressionToChords('Cmaj7-Am9-Fadd9-G13', 'C', 'root');
|
||||
deep(prog.map(c => c.name), ['Cmaj7', 'Am9', 'Fadd9', 'G13']);
|
||||
deep(prog[0].pitches, [60, 64, 67, 71]);
|
||||
});
|
||||
|
||||
console.log(fails ? 'FAILED: ' + fails : 'ALL PASS');
|
||||
process.exit(fails ? 1 : 0);
|
||||
@@ -11,13 +11,19 @@
|
||||
activeInstrumentType: 'VST3',
|
||||
_audioCb: null,
|
||||
_statusCb: null,
|
||||
_midiOutCb: null,
|
||||
_tauri: function () { return window.__TAURI__; },
|
||||
|
||||
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
|
||||
onStatusChange: function (cb) { this._statusCb = cb; },
|
||||
_notifyStatus: function (connected) {
|
||||
this.isBridgeConnected = !!connected;
|
||||
if (this._statusCb) this._statusCb({ connected: !!connected });
|
||||
connected = !!connected;
|
||||
// Loop guard (V45.7): poller queryStatus moi 400ms tung goi cb du connected
|
||||
// khong doi -> App reset map + reload toan bo track -> mark loading lai ->
|
||||
// poller lai thay loading -> LOAD skip vo han. Chi fire khi TRANG THAI DOI.
|
||||
if (this.isBridgeConnected === connected) return;
|
||||
this.isBridgeConnected = connected;
|
||||
if (this._statusCb) this._statusCb({ connected: connected });
|
||||
},
|
||||
|
||||
/** Fix C: re-establish bridge routing once audio frames arrive again
|
||||
@@ -58,6 +64,12 @@
|
||||
if (!self.isBridgeConnected) self._reconnectIfNeeded();
|
||||
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
|
||||
});
|
||||
// A14: plugin MIDI output (VstEvents -> SHM midiOutQueue -> pump).
|
||||
// Payload: array of {command, channel, pitch, velocity, data2,
|
||||
// data3, reserved, sample_offset}. JS routes notes to DAW tracks.
|
||||
window.__TAURI__.event.listen('bridge-midi-out', function (e) {
|
||||
if (self._midiOutCb) self._midiOutCb(e.payload);
|
||||
});
|
||||
window.__TAURI__.event.listen('bridge-down', function () {
|
||||
self._notifyStatus(false);
|
||||
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(false);
|
||||
@@ -75,6 +87,12 @@
|
||||
try {
|
||||
var s = await window.__TAURI__.core.invoke('bridge_status');
|
||||
this._notifyStatus(!!s.connected);
|
||||
// V10: C++ loaded channels (bridge_state.json) — nguồn path thật không
|
||||
// cần HTTP/auth. loadVstToBridge/ensureAndOpenVstGui dùng để mở GUI khi
|
||||
// JS không resolve được path (token hết hạn / listPlugins thiếu path).
|
||||
if (s && Array.isArray(s.loaded_channels)) {
|
||||
window.__bridgeLoadedState = s.loaded_channels;
|
||||
}
|
||||
return s;
|
||||
} catch (e) {
|
||||
this._notifyStatus(false);
|
||||
@@ -86,14 +104,12 @@
|
||||
* 1. LOAD NEW INSTRUMENT INTO BRIDGE (VST3 / VST2 / SF2 / SF3 / SFZ)
|
||||
* instrumentType: 'VST3' | 'VST2' | 'SF2' | 'SF3' | 'SFZ'
|
||||
* channel: MIDI channel to assign this instrument to (A10 multi-instance).
|
||||
* NOTE: sends a PANIC (all notes off) first so held notes never stick across a hot-swap.
|
||||
*/
|
||||
loadInstrument: async function (filePath, instrumentType, channel) {
|
||||
this.activeInstrumentType = instrumentType;
|
||||
console.log('[BridgeService] Loading ' + instrumentType + ' asset: ' + filePath + ' ch=' + channel);
|
||||
if (!this._tauri()) return false;
|
||||
try {
|
||||
await window.__TAURI__.core.invoke('transport_control', { kind: 'panic' });
|
||||
await window.__TAURI__.core.invoke('load_native_instrument', {
|
||||
path: filePath,
|
||||
instrumentType: INSTRUMENT_TYPE[instrumentType] !== undefined ? INSTRUMENT_TYPE[instrumentType] : 0,
|
||||
@@ -112,7 +128,7 @@
|
||||
* velocity 0..1; sampleOffset in samples within the current audio block.
|
||||
* data2/data3 (A12): CC value / program / PB LSB|MSB.
|
||||
*/
|
||||
dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3) {
|
||||
dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3, live) {
|
||||
if (!this._tauri()) return false;
|
||||
var safeVelocity = Math.floor(Math.min(1.0, Math.max(0.0, velocity)) * 127);
|
||||
var byteCmd;
|
||||
@@ -122,15 +138,23 @@
|
||||
case 'PITCH_BEND': byteCmd = 0xE; break;
|
||||
default: byteCmd = cmd === 'NOTE_ON' ? 0x9 : 0x8; break;
|
||||
}
|
||||
window.__TAURI__.core.invoke('push_midi_event', {
|
||||
command: byteCmd,
|
||||
channel: channel,
|
||||
pitch: pitch,
|
||||
velocity: safeVelocity,
|
||||
data2: data2 === undefined ? 0 : data2,
|
||||
data3: data3 === undefined ? 0 : data3,
|
||||
sampleOffset: sampleOffset || 0
|
||||
}).catch(function (e) { console.warn('[BridgeService] push_midi_event:', e); });
|
||||
// DIAG: Promise.race 800ms — invoke gửi đi nhưng không settle -> timeout
|
||||
// (phan biet main-thread block vs Rust hang).
|
||||
Promise.race([
|
||||
window.__TAURI__.core.invoke('push_midi_event', {
|
||||
command: byteCmd,
|
||||
channel: channel,
|
||||
pitch: pitch,
|
||||
velocity: safeVelocity,
|
||||
data2: data2 === undefined ? 0 : data2,
|
||||
data3: data3 === undefined ? 0 : data3,
|
||||
sampleOffset: sampleOffset || 0,
|
||||
live: !!live
|
||||
}),
|
||||
new Promise(function (_, rej) { setTimeout(function () { rej(new Error('timeout-800ms')); }, 800); })
|
||||
]).catch(function (e) {
|
||||
console.warn('[BridgeService] push_midi_event:', e);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -164,12 +188,31 @@
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 6. REALTIME MASTER FX CHAIN (T2.4)
|
||||
* chainJson: JSON array [{path, bypass}, ...] applied to the live master
|
||||
* mix via the bridge control queue (type 6). Async — never blocks UI.
|
||||
*/
|
||||
setFxChain: function (chainJson) {
|
||||
if (!this._tauri()) return false;
|
||||
window.__TAURI__.core.invoke('set_fx_chain', { chainJson: chainJson })
|
||||
.catch(function (e) { console.warn('[BridgeService] set_fx_chain:', e); });
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 5. AUDIO SINK: register consumer of PCM frames from the bridge.
|
||||
* cb(l: Float32Array, r: Float32Array)
|
||||
*/
|
||||
onAudio: function (cb) {
|
||||
this._audioCb = cb;
|
||||
},
|
||||
|
||||
/** A14: register consumer of plugin MIDI output.
|
||||
* cb(events): array of {command, channel, pitch, velocity, data2,
|
||||
* data3, reserved, sample_offset} — route to DAW tracks. */
|
||||
onMidiOut: function (cb) {
|
||||
this._midiOutCb = cb;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// SonicForge Runtime service — phát hiện môi trường chạy (desktop Windows /
|
||||
// docker headless) qua /api/v1/system/capabilities, bật/tắt tính năng theo đó.
|
||||
// - data-runtime trên <html>: "desktop" | "headless"
|
||||
// - data-carla="1": có Carla local (hiện nút "Mở trong Carla")
|
||||
// - Phần tử có thuộc tính data-carla-only sẽ bị ẩn khi không có Carla local.
|
||||
// - Thư viện preset (.vstpreset) cache trong SonicRuntime.presets — dùng cho
|
||||
// dropdown gán preset vào track (Carla → pedalboard bridge).
|
||||
// dropdown gán preset vào track (preset → pedalboard bridge).
|
||||
window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null, presets: null };
|
||||
|
||||
(function () {
|
||||
@@ -22,19 +20,13 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
|
||||
window.SonicRuntime.loaded = true;
|
||||
// environment: "docker" | "standalone" — quyết định luồng âm
|
||||
// instrument: docker → FluidSynthWASM (client), standalone →
|
||||
// native (backend pyfluidsynth / Carla). Không phải docker =
|
||||
// standalone (Windows/Linux/macOS chạy trực tiếp trên OS).
|
||||
// native (backend pyfluidsynth / native_bridge). Không phải
|
||||
// docker = standalone (Windows/Linux/macOS chạy trực tiếp trên OS).
|
||||
window.SonicRuntime.environment = c.environment || (c.docker ? 'docker' : 'standalone');
|
||||
var html = document.documentElement;
|
||||
html.dataset.runtime = c.runtime || 'unknown';
|
||||
html.dataset.platform = c.platform || '';
|
||||
html.dataset.environment = window.SonicRuntime.environment;
|
||||
html.dataset.carla = (c.features && c.features.carla_local) ? '1' : '0';
|
||||
if (c.features && c.features.carla_local === false) {
|
||||
document.querySelectorAll('[data-carla-only]').forEach(function (el) {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
}
|
||||
// Cache sẵn danh sách preset (static, ít thay đổi)
|
||||
listPresets().catch(function () {});
|
||||
return c;
|
||||
@@ -66,54 +58,3 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
|
||||
load();
|
||||
}
|
||||
})();
|
||||
|
||||
// ── SonicCarlaMidi: cầu nối MIDI từ track ARM → Carla (OSC /Carla/0/note_*) ──
|
||||
// Khi track dùng VSTi (synth_engine.type chứa 'vst') + được ARM + máy có Carla
|
||||
// local → phím bấm trên piano roll / keybed được gửi tới Carla để phát realtime
|
||||
// (Carla standalone bật OSC UDP mặc định cổng 22752).
|
||||
window.SonicCarlaMidi = window.SonicCarlaMidi || {
|
||||
shouldRoute: function (synthEngine, isArmed) {
|
||||
// Carla bridge removed - all instruments hosted by Native Bridge.
|
||||
return false;
|
||||
},
|
||||
// Playback MIDI items: route khi track VSTi + Carla local (không cần ARM —
|
||||
// user đã chủ động bấm Play trên item đó).
|
||||
shouldRoutePlayback: function (synthEngine) {
|
||||
// Carla bridge removed - all instruments hosted by Native Bridge.
|
||||
return false;
|
||||
},
|
||||
noteOn: function (channel, note, velocity) {
|
||||
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
|
||||
window.SonicAPI.carlaMidi({ event: 'note_on', note: note, velocity: velocity || 100, channel: channel || 0 }).catch(function () {});
|
||||
},
|
||||
noteOff: function (channel, note) {
|
||||
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
|
||||
window.SonicAPI.carlaMidi({ event: 'note_off', note: note, channel: channel || 0 }).catch(function () {});
|
||||
},
|
||||
// Bật note rồi tự tắt sau durMs (preview ngắn)
|
||||
playNote: function (channel, note, velocity, durMs) {
|
||||
this.noteOn(channel, note, velocity);
|
||||
var self = this;
|
||||
setTimeout(function () { self.noteOff(channel, note); }, (durMs || 300) + 50);
|
||||
},
|
||||
// Tắt mọi note đang ngân trong Carla (note_off toàn pitch — dùng khi stop)
|
||||
allNotesOff: function () {
|
||||
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
|
||||
for (var ch = 0; ch < 16; ch++) {
|
||||
for (var n = 0; n < 128; n++) {
|
||||
window.SonicAPI.carlaMidi({ event: 'note_off', note: n, channel: ch }).catch(function () {});
|
||||
}
|
||||
}
|
||||
},
|
||||
// Unload Carla bridge: tắt âm + terminate tiến trình Carla (app spawn).
|
||||
// Gọi khi track chuyển từ VSTi → instrument soundfont để âm KHÔNG còn
|
||||
// play qua Carla bridge nữa.
|
||||
stopBridge: function () {
|
||||
var self = this;
|
||||
try { self.allNotesOff(); } catch (e) {}
|
||||
if (window.SonicAPI && window.SonicAPI.stopCarla) {
|
||||
return window.SonicAPI.stopCarla().catch(function () {});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
// app/static/js/services/unifiedMidiRouter.js
|
||||
// Single MIDI entry point for Web MIDI keyboard + timeline playback.
|
||||
// bridge connected -> NativeBridgeService.dispatchMidiEvent (Rust SHM -> C++ bridge)
|
||||
// bridge down -> onFallback callback (app.jsx wires SonicSF/Carla path).
|
||||
// bridge down -> onFallback callback (app.jsx wires SonicSF/native path).
|
||||
(function () {
|
||||
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15]; // skip 9 (percussion)
|
||||
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview
|
||||
|
||||
var _lastTimelineDbgTs = 0;
|
||||
// V45.26: dem note live dang giu + lan live event gan nhat — quyet dinh co
|
||||
// clear master FX outQueue khong (chi idle->active; KHONG khi dang phat lien tuc).
|
||||
var _liveHeldCount = 0;
|
||||
var _lastLiveEventAt = 0;
|
||||
|
||||
var router = {
|
||||
bridgeConnected: false,
|
||||
@@ -24,6 +30,9 @@
|
||||
|
||||
/** trackId -> MIDI channel; percussion (bank 128) -> ch 9. */
|
||||
allocateChannel: function (trackId, isPercussion) {
|
||||
// Media Explorer preview dùng channel 15 cố định — tránh đè channel
|
||||
// melodic track (0-8,10-14) / percussion (9) đang giữ instrument.
|
||||
if (trackId === 'media_explorer') return 15;
|
||||
if (this._channels[trackId]) return this._channels[trackId].ch;
|
||||
var ch;
|
||||
if (isPercussion) {
|
||||
@@ -48,13 +57,47 @@
|
||||
* data2/data3 (A12): CC value / program / PB LSB|MSB — passed to bridge only.
|
||||
*/
|
||||
pushEvent: function (opts) {
|
||||
// V28: midi bất kỳ (keyboard/pad/timeline) → restore bridge gain đang
|
||||
// giữ ~0.0001 sau stop (ding fix — V27 restore 800ms không đủ tail dài).
|
||||
try { if (window.__restorePendingFadeSession) window.__restorePendingFadeSession(); } catch (e) {}
|
||||
// V31: live note (ARM keyboard preview) — clear master FX outQueue backlog
|
||||
// (idle block ~341ms) truoc khi note moi render; timeline note (live=false)
|
||||
// KHONG clear — tranh drop audio dang phat.
|
||||
// V45.26: chi clear khi THAT SU idle -> active (khong con note dang giu
|
||||
// VA im lang >500ms). Truoc day clear MOI note-on live -> rapid 1/8 lien
|
||||
// tuc: moi note wipe outQueue (dang chua tail note truoc + resume fade-in)
|
||||
// -> wobble/crackle (bug user). Note dang ngan / chuoi note nhanh -> giu
|
||||
// queue -> tail note truoc khong bi cat.
|
||||
if (opts.live) {
|
||||
try {
|
||||
var _nowEv = (window.performance && performance.now) ? performance.now() : Date.now();
|
||||
if (opts.cmd === 'NOTE_ON') {
|
||||
if (_liveHeldCount === 0 && (_nowEv - _lastLiveEventAt) > 500 && window.__clearMasterFxQueue) window.__clearMasterFxQueue();
|
||||
_liveHeldCount++;
|
||||
} else if (opts.cmd === 'NOTE_OFF') {
|
||||
if (_liveHeldCount > 0) _liveHeldCount--;
|
||||
}
|
||||
_lastLiveEventAt = _nowEv;
|
||||
} catch (e) {}
|
||||
}
|
||||
var cmd = opts.cmd, ch = opts.channel, pitch = opts.pitch;
|
||||
var vel = (opts.velocity === undefined ? 1.0 : opts.velocity);
|
||||
var sampleOffset = opts.sampleOffset || 0;
|
||||
if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion);
|
||||
if (ch === undefined || ch === null) ch = 0;
|
||||
try {
|
||||
// V36: midi_push log = 1 Tauri invoke sync (dbg_log -> disk I/O tren main
|
||||
// thread Rust) moi event. Timeline phat ~380 event/s -> main thread nghet
|
||||
// -> stall 110-170ms -> crackle. Chi log live (ARM preview), timeline
|
||||
// gioi han 1/200ms.
|
||||
var _nowMs = (window.performance && performance.now) ? performance.now() : Date.now();
|
||||
if (window.__dbgLog && (opts.live || _nowMs - _lastTimelineDbgTs > 200)) {
|
||||
if (!opts.live) _lastTimelineDbgTs = _nowMs;
|
||||
window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' p=' + (pitch === undefined ? 'n/a' : pitch) + ' live=' + (opts.live ? 1 : 0) + ' vel=' + Math.round((vel || 0) * 100) + (window.__dbgBridgeGain ? ' ' + window.__dbgBridgeGain() : '') + ' q=' + (window.__fxRtQueueLen ? window.__fxRtQueueLen() : 'n/a'));
|
||||
}
|
||||
} catch (e) {}
|
||||
if (this.isBridgeActive()) {
|
||||
window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3);
|
||||
window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3, opts.live);
|
||||
return;
|
||||
}
|
||||
if (this.onFallback) this.onFallback(cmd, ch, pitch, vel);
|
||||
@@ -62,6 +105,7 @@
|
||||
|
||||
/** Stop/panic -> bridge transport panic (flush all notes) + fallback local stopAll. */
|
||||
panic: function () {
|
||||
_liveHeldCount = 0;
|
||||
if (this.isBridgeActive() && window.NativeBridgeService.transport) {
|
||||
window.NativeBridgeService.transport('panic');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
// sf-fx-realtime.js — AudioWorkletProcessor nối WebAudio → SHM FX engine.
|
||||
// process() gom 128-sample quantum thành block 256 stereo float32 (khớp
|
||||
// FXRT_BLOCK trong FxRealtimeIPC.h) → postMessage cho main thread gửi WS;
|
||||
// nhận block đã xử lý từ main thread → queue → phát ra output. Queue rỗng →
|
||||
// zero fill (WS chưa kịp round-trip).
|
||||
//
|
||||
// BURST (fix lag/giật/crackle): gom 4 block (1024 mẫu) vào 1 frame rồi post.
|
||||
// Bridge VST3 (fx_vst_bridge) chạy chậm hơn realtime khi take=1 (256 mẫu:
|
||||
// ~5.5ms/call > budget 5.33ms → 179/s < 187.5/s → engine drop input →
|
||||
// crackle). Gửi burst 4 block → bridge take=4 → 1 call process 1024 mẫu
|
||||
// (amortize overhead: 14-19ms/4 block ≈ 3.5-4.75ms/block) → đủ 187.5/s.
|
||||
// Latency thêm ~4 block (~21ms) — worklet outQueue 32 block hấp thụ.
|
||||
//
|
||||
// FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue
|
||||
// ≥ FILL (8 block ~43ms @48k) rồi mới phát. Engine gửi ASAP (không pace
|
||||
// realtime) → client tích lũy buffer an toàn; mọi jitter (bridge process VST
|
||||
// biến thiên, GC, WS, main thread rAF) bị hấp thụ bởi FILL thay vì queue ≈1
|
||||
// → cạn → ngắt quãng.
|
||||
// UNDERRUN (queue cạn khi đang phát): V23 — KHÔNG lặp block cuối (lặp tạo
|
||||
// jump pha ở điểm vào hold + mỗi wrap → click), KHÔNG zero-fill: fade-out
|
||||
// tuyến tính từ mẫu output cuối (HOLD_FADE block ~21ms) tới im lặng; resume
|
||||
// NGAY khi có block mới (không đợi đủ FILL): đã im lặng → fade-in từ 0, đang
|
||||
// giữa fade → crossfade 64 mẫu từ mẫu hiện tại (hết jump). Cap-drop (queue
|
||||
// tràn sau stall) crossfade 64 mẫu (hết click) — giống bridgeAudioWorklet.
|
||||
// Cap 64 block (~341ms) chặn overflow khi bridge nhanh hơn realtime.
|
||||
// ponytail: thay fade-out bằng adaptive stretch khi cần chất lượng cao hơn.
|
||||
class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.BLOCK = 256; // FXRT_BLOCK
|
||||
this.BURST = 4; // số block gom 1 frame (bridge batch)
|
||||
this.FILL = 96; // V46: 96 block ~512ms @48k: hấp thụ stall ~500ms (u2 08:16:44.8: pump deficit 723ms, event gap 507ms → V45 cushion 256ms cạn → silence ~500ms → wobble). PDC 26368 = (7+96)*256 (fxRtApplyPdc). V45 48 -> cushion 256ms < deficit 723ms -> underrun. V46 96 -> cushion ~512ms (margin ~200ms vs event 507ms; deficit cluster vẫn có thể underrun ngắn — chấp nhận, content jump giữ policy V24).
|
||||
this.RESUME_FILL = 24; // V26: re-buffer khi resume SAU STOP (flush_for_stop) — 16 block ~85ms (V43: = FILL) → play kế tiếp bắt đầu nhanh; underrun-stall vẫn dùng FILL (crackle/wobble).
|
||||
this.resumeFill = this.FILL; // mức re-buffer khi resume sau flush — stop: RESUME_FILL, underrun-stall: FILL
|
||||
this.CAP = 104; // V46: = FILL+8: cap queue chặn overflow (~550ms)
|
||||
this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng)
|
||||
this.holdFadeLeft = 0; // mẫu còn lại của fade-out khi underrun
|
||||
this.silenced = false; // đã fade về 0 → im lặng; resume phải fade-in, không hold lại
|
||||
this.bufLen = this.BLOCK * this.BURST;
|
||||
this.inL = new Float32Array(this.bufLen);
|
||||
this.inR = new Float32Array(this.bufLen);
|
||||
this.inCount = 0;
|
||||
this.outQueue = []; // [{l: Float32Array, r: Float32Array}]
|
||||
this.outPos = 0;
|
||||
this.playing = false; // false → im lặng (đang chờ đủ FILL)
|
||||
this.hasStarted = false; // đã phát block đầu chưa — FILL chỉ chờ ở lần khởi đầu
|
||||
this.flushPending = false; // V24: đang chờ engine flush (resume sau underrun) — drop block stale
|
||||
this.resumeFadeIn = false; // V24: interruption đã xử lí (clear+flush) — resume sẽ fade-in từ 0
|
||||
this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click
|
||||
this.outGain = 0; // gain phát hiện tại (0→1 trong fade)
|
||||
this.fadeLeft = 0; // số mẫu còn lại của fade
|
||||
this.skipFade = 0; // >0: crossfade đang chạy sau jump (cap-drop/resume/wrap) — hết click
|
||||
this.skipFadeTotal = 64; // V42: độ dài crossfade hiện tại (64 mẫu thường; WRAP_FADE 256 tại seam wrap)
|
||||
this.skipFromL = 0.0; // mẫu output tại điểm jump — điểm bắt đầu crossfade
|
||||
this.skipFromR = 0.0;
|
||||
this.lastOutL = 0.0; // mẫu output cuối — nguồn crossfade + điểm bắt đầu fade-out
|
||||
this.lastOutR = 0.0;
|
||||
this.wrapFadeBlocks = -1; // V42: số block tail pass cũ còn lại sau clear_queue('loop') — đếm ngược để crossfade đúng seam tail→head
|
||||
this.latencySamples = 0; // G3: latency chain thật (engine qua WS) — align wrap fade theo processed tail
|
||||
this.wrapMode = false; // V45.3: dang trong wrap (clear_queue loop) — resume underrun bo flush + re-buffer nho
|
||||
this.WRAP_FADE = 256; // V42: crossfade 256 mẫu (~5.3ms @48k) tại seam wrap — che click jump nội dung end→start (FX path)
|
||||
this.fadeFromL = 0.0; // mẫu cố định khi fade-out underrun (fade tuyến tính tới 0)
|
||||
this.fadeFromR = 0.0;
|
||||
this.port.onmessage = (e) => {
|
||||
const d = e.data;
|
||||
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
|
||||
if (d && d.type === 'flush_done') { this.flushPending = false; return; }
|
||||
if (d && d.type === 'latency') { this.latencySamples = (d.samples || 0); return; }
|
||||
if (d && d.type === 'flush_for_stop') {
|
||||
// V26: user stop — main thread báo xóa audio tail trong queue (audio đã
|
||||
// xử lí còn phát ~0.3-0.5s sau stop = tiếng "ding"). Fade-out 21ms từ
|
||||
// mẫu cuối (hết click) → im lặng; báo main flush engine ring (audio
|
||||
// stale trong WS/SHM không chơi tiếp). Resume lần sau: RESUME_FILL
|
||||
// (8 block ~43ms, V40: = FILL) — play kế tiếp không chờ trễ.
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.playing = false;
|
||||
this.wrapFadeBlocks = -1; // V42: stop — hủy wrap crossfade chờ
|
||||
this.flushPending = true;
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = this.RESUME_FILL;
|
||||
this.silenced = false;
|
||||
this.wrapMode = false;
|
||||
// V30: fade-out tu mau output CUOI (lastOut) — truoc day dung fadeFrom
|
||||
// cu (mac dinh 0) -> jump mau -> click/crackle ngay khi stop.
|
||||
this.fadeFromL = this.lastOutL;
|
||||
this.fadeFromR = this.lastOutR;
|
||||
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
|
||||
this.port.postMessage({ type: 'flush' });
|
||||
return;
|
||||
}
|
||||
if (d && d.type === 'clear_queue') {
|
||||
this.clearMode = d.mode; // V45.19: nho mode de tag fx_seek_first tai dbgPlayFirst
|
||||
// V34: backlog guard o DAU handler (V33 dat sau mutation -> outQueue.length=0
|
||||
// chay truoc -> guard luon return, clear van drop queue + fade + re-buffer moi
|
||||
// lan = V31). Queue nho (< FILL 8 block) -> bo qua hoan toan (khong mutation,
|
||||
// khong re-buffer) -> het ARM crackle (queue ARM chi 0-8 block, engine ~realtime).
|
||||
// Queue day (idle lau, engine chay truoc ~32 block) -> clear dung (fix lag 170ms).
|
||||
// V35: mode:'play' (start/section/pianoroll) LUON clear - queue < FILL luc
|
||||
// play-start con stale block idle/ARM (~16) -> bo qua => phat audio cu roi
|
||||
// can queue ~500ms (cold start). mode:'live' (ARM) giu guard het crackle.
|
||||
// V37 ARM probe: reset dbg flags BEFORE guard so live notes (small queue) also log fx_hop
|
||||
this.dbgClearAt = currentTime * 1000;
|
||||
this.dbgInFirst = true;
|
||||
this.dbgOutFirst = true;
|
||||
this.dbgPlayFirst = true;
|
||||
// V39: loop wrap — KHONG wipe: giu queue chay lien tuc (tail pass cu
|
||||
// pre-roll pass moi qua VST lookahead) -> het wobble dau bai. Chi reset
|
||||
// dbg flags. mode 'play' van wipe (bao ve audio cu khi seek/cold start).
|
||||
// V42: dem so block tail pass cu con lai trong queue -> wrap crossfade
|
||||
// trigger dung luc block dau pass moi len front (seam tail->head).
|
||||
if (d.mode === 'loop') {
|
||||
// G3: align processed-tail — tail pass cũ còn in-flight trong bridge
|
||||
// (FX latency thật từ engine) chưa về queue; đếm đủ block tới seam
|
||||
// (block đầu pass mới lên front) → crossfade đúng chỗ jump end→start.
|
||||
// V45.2 (option B): cắt bớt tail cũ giữ KEEP block thay vì giữ nguyên
|
||||
// FILL 24 (~128ms) → downbeat pass mới không trễ ~110ms. skipFade
|
||||
// crossfade từ lastOut che jump cắt front; wrapFadeBlocks đếm lại từ
|
||||
// KEEP để seam crossfade vẫn đúng lúc.
|
||||
// V45.3: KEEP 4 -> 16 (~85ms): KEEP 4 (~21ms) engine (stall bridge
|
||||
// 68-138ms sau transport play) khong refill kip -> queue can -> underrun
|
||||
// -> flush + re-buffer FILL 24 = gap ~110-235ms moi wrap (WORSE hon
|
||||
// truoc V45.2: downbeat tre 106-128ms nhung khong gap). KEEP 16 che
|
||||
// stall dien hinh 68-82ms lien tuc (khong gap, downbeat ~85ms); stall
|
||||
// dai hon roi vao nhanh resume wrap (bo flush + resumeFill 8).
|
||||
const KEEP = 16;
|
||||
this.wrapMode = true;
|
||||
if (this.outQueue.length > KEEP) {
|
||||
const head = this.outQueue.length - KEEP;
|
||||
this.outQueue.splice(0, head);
|
||||
this.outPos = 0;
|
||||
if (this.playing) {
|
||||
this.skipFade = this.WRAP_FADE;
|
||||
this.skipFadeTotal = this.WRAP_FADE;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
}
|
||||
}
|
||||
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
|
||||
return;
|
||||
}
|
||||
if (d.mode === 'seek') {
|
||||
// V45.18: seek khi play master FX active — wipe queue cu (audio cu
|
||||
// choi tiep 0-50ms sau click) + resume nhanh RESUME_FILL 8 (~43ms)
|
||||
// thay vi FILL 24 (~128ms) — het "am tre 1 beat" (hit dau roi vao
|
||||
// vung cam 128ms). Giong resume wrap V45.4 (proven khong gap).
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.playing = false;
|
||||
this.wrapFadeBlocks = -1;
|
||||
this.wrapMode = false;
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = 8;
|
||||
this.silenced = false;
|
||||
this.fadeFromL = this.lastOutL;
|
||||
this.fadeFromR = this.lastOutR;
|
||||
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_seek_clear', t: Math.round(currentTime * 1000), q: 0 });
|
||||
return;
|
||||
}
|
||||
if (d.mode !== 'play' && this.outQueue.length < this.FILL) return;
|
||||
// V31: play/ARM live note — main thread bao xoa backlog block idle
|
||||
// (engine render lien tuc khi STOP -> outQueue day ~32 block ~170ms
|
||||
// audio cu; play moi phai cho phia sau -> lag 0.3-0.5s). KHONG post
|
||||
// 'flush' (khong chaym engine WS — note-on play vua day xuong se bi
|
||||
// drop). Resume sau 8 block (~43ms) + fade-out/fade-in tu mau cuoi.
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.playing = false;
|
||||
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
|
||||
this.wrapMode = false; // V45.3: play/seek — het context wrap
|
||||
this.resumeFadeIn = true;
|
||||
// V34: mode phan biet — play re-buffer FILL 8 (~43ms, cushion du hap thu
|
||||
// engine ramp/jitter dau play); ARM live note giu RESUME_FILL 8 (~43ms) — preview
|
||||
// nhanh, khong cho FILL 8.
|
||||
this.resumeFill = (d.mode === 'play') ? this.FILL : this.RESUME_FILL;
|
||||
this.silenced = false;
|
||||
this.fadeFromL = this.lastOutL;
|
||||
this.fadeFromR = this.lastOutR;
|
||||
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
|
||||
return;
|
||||
}
|
||||
if (!d || d.type !== 'out' || !d.l || !d.r) return;
|
||||
// V24: đang chờ flush (resume sau underrun/stall) → drop block stale.
|
||||
// Backlog engine gửi khi worklet im là audio CŨ (chơi lại → lag cộng dồn
|
||||
// ~1s) — chỉ nhận block MỚI từ sau flush_done.
|
||||
if (this.flushPending) return;
|
||||
this.outQueue.push({ l: d.l, r: d.r });
|
||||
if (this.dbgOutFirst) {
|
||||
this.dbgOutFirst = false;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_out_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
|
||||
}
|
||||
while (this.outQueue.length > this.CAP) {
|
||||
this.outQueue.shift();
|
||||
this.statCapDrop++;
|
||||
// V23: drop cứng = jump mẫu → click. Crossfade 64 mẫu từ mẫu output
|
||||
// cuối sang block mới; chỉ đặt 1 lần cho đợt drop liên tiếp. outPos=0:
|
||||
// block mới (front) phải phát từ đầu, không tiếp tục vị trí block cũ.
|
||||
if (this.skipFade <= 0) {
|
||||
this.skipFade = 64;
|
||||
this.skipFadeTotal = 64;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
}
|
||||
this.outPos = 0;
|
||||
}
|
||||
};
|
||||
// Telemetry (chẩn đoán crackle trên session thật): đếm sự kiện + báo main
|
||||
// thread mỗi ~2s. Nếu user bật master FX nghe crackle mà stats cho thấy
|
||||
// underruns>0 → delivery bị stall (main-thread busy) → nguồn crackle thật.
|
||||
this.statUnderrun = 0; this.statHold = 0; this.statFade = 0;
|
||||
this.statSilence = 0; this.statCapDrop = 0; this.statFadeIn = 0;
|
||||
this.statCalls = 0;
|
||||
// V32: do latency master FX path — danh dau clear_queue, bao main thread hop dau
|
||||
// sau clear (in_first / out_first / play_first) → dbg.log delta vs play_start.
|
||||
this.dbgClearAt = 0;
|
||||
this.dbgInFirst = false;
|
||||
this.dbgOutFirst = false;
|
||||
this.dbgPlayFirst = false;
|
||||
this._wlLast = 0; // V46: thời điểm process() lần trước — đo gián đoạn audio thread
|
||||
this._wlGapAt = 0; // V46: rate-limit wl_gap 1/s
|
||||
}
|
||||
process(inputs, outputs) {
|
||||
const input = inputs[0];
|
||||
const output = outputs[0];
|
||||
const n = output[0] ? output[0].length : 128;
|
||||
const outL = output[0];
|
||||
const outR = output[1];
|
||||
|
||||
// V46: wl_gap — audio thread bị gián đoạn (process() cách >30ms, thường do
|
||||
// main thread block chia sẻ buffer/GC) → dbg log. Rate-limit 1/s.
|
||||
if (this._wlLast > 0) {
|
||||
const _wlGap = currentTime - this._wlLast;
|
||||
if (_wlGap > 0.030 && (currentTime - this._wlGapAt) > 1.0) {
|
||||
this._wlGapAt = currentTime;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'wl_gap', t: Math.round(currentTime * 1000), g: Math.round(_wlGap * 1000) });
|
||||
}
|
||||
}
|
||||
this._wlLast = currentTime;
|
||||
|
||||
// Góp input thành burst 4 block (1024 mẫu) → gửi main thread
|
||||
// (transferable buffer). Main thread tách 4 WS frame → bridge batch 4.
|
||||
if (input && input[0]) {
|
||||
const inL = input[0];
|
||||
const inR = input[1] || input[0];
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (this.inCount < this.bufLen) {
|
||||
this.inL[this.inCount] = inL[i];
|
||||
this.inR[this.inCount] = inR[i];
|
||||
}
|
||||
this.inCount++;
|
||||
if (this.inCount >= this.bufLen) {
|
||||
const inter = new Float32Array(this.bufLen * 2);
|
||||
for (let j = 0; j < this.bufLen; j++) {
|
||||
inter[j * 2] = this.inL[j];
|
||||
inter[j * 2 + 1] = this.inR[j];
|
||||
}
|
||||
this.port.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]);
|
||||
this.inCount = 0;
|
||||
if (this.dbgInFirst) {
|
||||
this.dbgInFirst = false;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_in_first', t: Math.round(currentTime * 1000) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phát output: chờ queue ≥ FILL trước khi phát lần đầu (hấp thụ jitter
|
||||
// engine/bridge/WS); underrun GIỮA stream → fade-out 21ms từ mẫu cuối rồi
|
||||
// im lặng (V23); resume NGAY khi có block mới (không đợi re-buffer FILL),
|
||||
// đã im lặng → fade-in, giữa fade → crossfade 64 mẫu (hết jump).
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.statCalls++;
|
||||
if (this.statCalls >= 1500) { // ~2s @48k/128
|
||||
this.statCalls = 0;
|
||||
this.port.postMessage({ type: 'stats',
|
||||
underruns: this.statUnderrun, holds: this.statHold, fades: this.statFade,
|
||||
silences: this.statSilence, capDrops: this.statCapDrop, fadeIns: this.statFadeIn,
|
||||
queueLen: this.outQueue.length });
|
||||
}
|
||||
if (this.playing) {
|
||||
const blk = this.outQueue[0];
|
||||
if (blk) {
|
||||
if (this.fadeLeft > 0) {
|
||||
this.outGain = Math.min(1, this.outGain + 1 / this.FADE);
|
||||
this.fadeLeft--;
|
||||
}
|
||||
if (this.skipFade > 0) {
|
||||
const t = 1 - this.skipFade / this.skipFadeTotal;
|
||||
outL[i] = (this.skipFromL * (1 - t) + blk.l[this.outPos] * t) * this.outGain;
|
||||
if (outR) outR[i] = (this.skipFromR * (1 - t) + blk.r[this.outPos] * t) * this.outGain;
|
||||
this.skipFade--;
|
||||
} else {
|
||||
outL[i] = blk.l[this.outPos] * this.outGain;
|
||||
if (outR) outR[i] = blk.r[this.outPos] * this.outGain;
|
||||
}
|
||||
this.lastOutL = outL[i];
|
||||
this.lastOutR = outR ? outR[i] : outL[i];
|
||||
this.outPos++;
|
||||
if (this.outPos >= this.BLOCK) {
|
||||
this.outQueue.shift();
|
||||
this.outPos = 0;
|
||||
// V42: seam tail→head — sau khi shift hết N block tail pass cũ, block
|
||||
// đầu pass mới lên front → crossfade WRAP_FADE từ mẫu cuối (che click
|
||||
// jump nội dung end→start, FX path). Trigger 1 lần mỗi wrap.
|
||||
if (this.wrapFadeBlocks > 0) {
|
||||
this.wrapFadeBlocks--;
|
||||
if (this.wrapFadeBlocks === 0) {
|
||||
this.skipFade = this.WRAP_FADE;
|
||||
this.skipFadeTotal = this.WRAP_FADE;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_wrap_xfade', t: Math.round(currentTime * 1000), q: this.outQueue.length });
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Underrun giữa stream — V23: fade-out NGAY (không lặp block cuối:
|
||||
// lặp tạo jump pha ở điểm vào + mỗi wrap → click). Fade tuyến tính từ
|
||||
// mẫu output cuối tới 0 (21ms) — vào liền mạch (fadeFrom = lastOut),
|
||||
// hết click cắt cứng. Resume → crossfade/fade-in (xem dưới).
|
||||
this.playing = false;
|
||||
this.wrapFadeBlocks = -1; // V42: underrun — hủy wrap crossfade chờ (resume qua fade-in/crossfade thường)
|
||||
this.statUnderrun++;
|
||||
if (!this.silenced) { // đã im lặng (fade xong) → giữ im lặng
|
||||
this.fadeFromL = this.lastOutL;
|
||||
this.fadeFromR = this.lastOutR;
|
||||
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
|
||||
this.statHold++;
|
||||
}
|
||||
}
|
||||
// Không phát: resume khi đủ block (lần đầu chờ FILL; sau đó chỉ cần 1
|
||||
// block — hold che khoảng chờ). V25: sau flush (resumeFadeIn=true) chờ
|
||||
// re-buffer đủ FILL mới resume — chặn loop underrun→flush→resume-1-block
|
||||
// →underrun (wobble): 1 block phát hết ~5ms → cạn → flush lại.
|
||||
if (this.outQueue.length >= (this.hasStarted ? (this.resumeFadeIn ? this.resumeFill : 1) : this.FILL)) {
|
||||
// V24: interruption (underrun sau stall) — backlog trong queue là audio
|
||||
// LẠC HẬU (chơi lại → lag cộng dồn ~1s). KHÔNG chơi backlog: clear
|
||||
// queue + báo main thread flush (engine xóa input/output ring, main
|
||||
// drop output stale in-flight) → chờ flush_done → resume chỉ với block
|
||||
// MỚI. flushPending → onmessage drop block stale trong lúc chờ (hết
|
||||
// race ack). Fade-out đang chạy (nếu có) tiếp tục tới im lặng — hết
|
||||
// click. resumeFadeIn guard chống re-trigger flush khi ack tới mà fade
|
||||
// chưa xong. resume → fade-in từ 0 (đã im) hoặc crossfade từ mẫu hiện
|
||||
// tại (giữa fade) — hết jump.
|
||||
const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
|
||||
if (wasInterrupted) {
|
||||
// V45.3: wrap (loop restart) — engine da transport('play') tu main
|
||||
// (D17): block moi = audio pass moi, KHONG phai backlog stale (boi
|
||||
// worklet flush gui 'flush' xuong engine ring = lam mat tac dung D17
|
||||
// skip-flush o stopAllPlayback -> gap giua seam). Bo flush (round-trip)
|
||||
// + re-buffer nho hon FILL 24 (~128ms) -> resume sau stall som hon
|
||||
// (~43ms) -> gap wrap giam tu ~110-235ms xuong con vai chuc ms.
|
||||
const _wrap = this.wrapMode;
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = _wrap ? 8 : this.FILL;
|
||||
this.flushPending = _wrap ? false : true;
|
||||
if (!_wrap) this.port.postMessage({ type: 'flush' });
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
this.playing = true;
|
||||
this.outPos = 0;
|
||||
this.holdFadeLeft = 0;
|
||||
this.wrapMode = false; // V45.3: resume xong — ve che do thuong (underrun sau nay flush nhu cu)
|
||||
if (this.dbgPlayFirst) {
|
||||
this.dbgPlayFirst = false;
|
||||
// V45.19 Bug 1b: seek -> tag fx_seek_first (main thread snap khong can
|
||||
// guard 50ms — seek resume nhanh ~43ms < 50ms). Wrap/play -> fx_play_first.
|
||||
this.port.postMessage({ type: 'dbg', tag: (this.clearMode === 'seek' ? 'fx_seek_first' : 'fx_play_first'), t: Math.round(currentTime * 1000), q: this.outQueue.length, m: this.clearMode || '' });
|
||||
}
|
||||
if (this.resumeFadeIn) {
|
||||
this.resumeFadeIn = false;
|
||||
if (this.silenced) {
|
||||
// Đã im lặng (fade xong / sau flush) → fade-in từ 0 (hết jump ~0→1).
|
||||
this.outGain = 0;
|
||||
this.fadeLeft = this.FADE;
|
||||
this.statFadeIn++;
|
||||
} else {
|
||||
// Giữa fade-out → crossfade ngắn từ mẫu hiện tại (không jump về 0).
|
||||
this.skipFade = 64;
|
||||
this.skipFadeTotal = 64;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
}
|
||||
}
|
||||
this.silenced = false;
|
||||
if (!this.hasStarted) {
|
||||
this.hasStarted = true;
|
||||
this.outGain = 0;
|
||||
this.fadeLeft = this.FADE;
|
||||
// Báo main thread 1 lần để crossfade dry→fx đúng thời điểm.
|
||||
this.port.postMessage({ type: 'started' });
|
||||
}
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (this.holdFadeLeft > 0) {
|
||||
// Fade-out 1→0 (underrun): fade tuyến tính từ mẫu cuối, im lặng êm.
|
||||
const g = this.outGain * (this.holdFadeLeft / (this.HOLD_FADE * this.BLOCK));
|
||||
outL[i] = this.fadeFromL * g;
|
||||
if (outR) outR[i] = this.fadeFromR * g;
|
||||
this.holdFadeLeft--;
|
||||
if (this.holdFadeLeft <= 0) { this.silenced = true; this.statFade++; this.statSilence++; }
|
||||
this.lastOutL = outL[i];
|
||||
this.lastOutR = outR ? outR[i] : outL[i];
|
||||
continue;
|
||||
}
|
||||
outL[i] = 0;
|
||||
if (outR) outR[i] = 0;
|
||||
this.lastOutL = 0;
|
||||
this.lastOutR = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('sf-fx-realtime', SfFxRealtimeProcessor);
|
||||
+2
-2
@@ -430,7 +430,7 @@ def cleanup_expired_files_task(max_age_hours: int = 24):
|
||||
|
||||
|
||||
@_task
|
||||
def render_project_task(project_id: str, project_name: str, project_json_str: str, sample_rate: int = 44100):
|
||||
def render_project_task(project_id: str, project_name: str, project_json_str: str, sample_rate: int = 44100, metadata: dict = None):
|
||||
"""
|
||||
Task Celery để kết xuất dự án ngoại tuyến (Offline Project Mixdown) áp dụng specs 30_DAW_ARCHITECT.md.
|
||||
"""
|
||||
@@ -442,7 +442,7 @@ def render_project_task(project_id: str, project_name: str, project_json_str: st
|
||||
output_path = os.path.join(settings.PROCESSED_DIR, output_filename)
|
||||
|
||||
engine = PythonRenderEngine(sample_rate=sample_rate)
|
||||
engine.render_project(project_json, output_path)
|
||||
engine.render_project(project_json, output_path, metadata=metadata or None)
|
||||
|
||||
return {
|
||||
"project_id": project_id,
|
||||
|
||||
+23
-22
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>SonicForge Studio - Professional DAW Editor</title>
|
||||
<title>ScoreFX Studio - Professional DAW Editor</title>
|
||||
<script>
|
||||
// ── Chặn BROWSER ZOOM toàn trang ──
|
||||
// UI dùng px cố định cho item/button/label — browser zoom (Ctrl+wheel,
|
||||
@@ -24,7 +24,7 @@
|
||||
document.addEventListener('gesturestart', function (e) { e.preventDefault(); }, { passive: false });
|
||||
document.addEventListener('gesturechange', function (e) { e.preventDefault(); }, { passive: false });
|
||||
</script>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="icon" type="image/png" href="/static/img/scorefx_favicon.png">
|
||||
<!-- Bug #11: vendor local — trước đây load CDN (unpkg/cdn.tailwindcss/cdnjs) →
|
||||
desktop Tauri offline trắng màn hình. Các file này nằm /static/vendor/. -->
|
||||
<script src="/static/vendor/tailwindcdn.js"></script>
|
||||
@@ -32,26 +32,27 @@
|
||||
<link rel="stylesheet" href="/static/vendor/fontawesome.min.css">
|
||||
<script src="/static/vendor/react.production.min.js"></script>
|
||||
<script src="/static/vendor/react-dom.production.min.js"></script>
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
|
||||
<script src="/static/js/services/runtime.js?v=202608101800"></script>
|
||||
<script src="/static/js/services/api.js?v=202608101800"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
|
||||
<script src="/static/js/services/nativeBridgeService.js?v=202608141800"></script>
|
||||
<script src="/static/js/services/bridgeAudioNode.js?v=202608121700"></script>
|
||||
<script src="/static/js/services/unifiedMidiRouter.js?v=202608112200"></script>
|
||||
<script src="/static/js/services/audioRoutingEngine.js?v=202608112200"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
|
||||
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></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/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608141730" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202608290945201545"></script>
|
||||
<script src="/static/js/services/runtime.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/api.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/nativeBridgeService.js?v=202608291510"></script>
|
||||
<script src="/static/js/services/bridgeAudioNode.js?v=202608311858"></script>
|
||||
<script src="/static/js/services/unifiedMidiRouter.js?v=202608311756"></script>
|
||||
<script src="/static/js/services/audioRoutingEngine.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/pianoRollTabService.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/ghostNoteExtractor.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/midiExtractor.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202609011700" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
|
||||
<style>
|
||||
:root {
|
||||
--right-sidebar-width: 320px;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Sonic Forge Studio — Big Tasks (ROADMAP G0–G4)
|
||||
|
||||
Trạng thái: 2026-08-15. Bug gốc: PLAY + mở GUI VST → câm, GUI không load.
|
||||
User duyệt roadmap G0–G4 ("Hãy bắt đầu"). Đang ở G1.
|
||||
|
||||
## Ngữ cảnh kỹ thuật
|
||||
|
||||
- Logs: `C:/Users/locpham/AppData/Roaming/SonicForgeDAW/logs/` — `bridge.log` (stdout bridge), `spawn.log` (watchdog restart), `engine.log`.
|
||||
- VST test: `C:\Program Files\Common Files\VST3\Nexus.vst3`.
|
||||
- SHM: `SonicForge_DAW_IPC_<APP_PID>`; offsets: controlQueue @2836 (8×1040), controlQueueCount @11156, attach_in_flight @11160, writeIndex @4, size 11164. Control types: 2=LOAD, 3=TRANSPORT (0=STOP/1=PLAY), 4=OPEN_GUI.
|
||||
- Branch: `standalone-shm-bridge`. App: sonicforge-daw, daw_engine, daw_vst_bridge (3 process).
|
||||
- Probe: `native_bridge/build/Release/gui_probe.exe` — standalone VST3 GUI attach probe.
|
||||
Variants: main_own | worker_own_nopump | worker_own_pump | worker_foreign_nopump | worker_foreign_pump | bridge_like | same_thread | two_instances | two_instances_close | close_reopen | bridge_two | shared_worker | ui_thread | two_workers_close.
|
||||
- File CRLF toàn bộ native_bridge + CMakeLists.txt → patch multi-line qua python `io.open(newline='')`.
|
||||
|
||||
## G0 — Hoàn thành (đã commit, branch standalone-shm-bridge)
|
||||
|
||||
- G0.1 (1839480): defer OPEN_GUI khi PLAY.
|
||||
- G0.2 (2f78618): openVstGuiRetry ≤2 lần + `__bridgePlaying` guard (app.precompiled.js).
|
||||
- G0.3 (5313d85): watchdog giữ 15s khi attach-in-flight (SHM flag offset 11160) + RAII guard.
|
||||
- G0.4 (eac7f2f): save/restore preset quanh reload — captureState/restoreState trong Vst3Instrument (getState/setState qua MemoryStream), gọi trong post_close_gui; INativeInstrument.h virtual no-op; CMakeLists link memorystream.cpp vào daw_vst_bridge + sdk_common. VERIFIED PASS (probe_g04_preset.py).
|
||||
|
||||
## G1 — 1 UiThread chung (ĐANG LÀM)
|
||||
|
||||
Mục tiêu: 1 thread chung host mọi VST3 instance thay per-channel workers → hết "2 thread 1 DLL" hang/crash (bridge_two HANG/EXIT3221225477 vs shared_worker OK).
|
||||
|
||||
### G1.1 — Probe variant ui_thread (alias shared_worker) [HOÀN THÀNH]
|
||||
- [x] Patch gui_probe.cpp: usage string, branch `variant == "shared_worker" || variant == "ui_thread"`, isDeferred list.
|
||||
- [x] Fix build: CMakeLists gui_probe target thêm `memorystream.cpp` + `sdk_common` (LNK2019).
|
||||
- [x] Fix crash 0xC0000409: ProbeWorker thiếu destructor join → std::terminate. Thêm destructor + join + closeGUI trước teardown + gỡ IME. VERIFIED `shared_worker 15`: RESULT done=1 aRes=1 bRes=1, EXIT=0, 6.73s. Commit 3587a34.
|
||||
|
||||
### G1.2 — Chuyển tạo window + attach job từ ChannelWorker → UiThread mới (1 thread chung) [HOÀN THÀNH — 9e6e72a]
|
||||
- [x] Thay per-channel workers (16) bằng 1 UiThread: COM STA + message pump + job queue.
|
||||
- [x] Audio loop bỏ PeekMessageW.
|
||||
- [x] Option B (đóng editor channel khác trước attach) chuyển PostMessage(WM_CLOSE)+5s → close_editor_now đồng bộ (1 thread: job close sau attach job cùng queue → không bao giờ chạy).
|
||||
- [x] VERIFIED: build EXIT=0; live test PASS — LOAD 7, PLAY writeIndex advancing, defer OK, attach OK, Option B close đồng bộ OK, WM_CLOSE→reload OK, 0 crash/restart.
|
||||
|
||||
### G1.3 — Close/resize/IME sang UiThread + bỏ pump gate cũ [HOÀN THÀNH — b92c85e]
|
||||
### G1.4 — Bỏ mute-when-editor-open [HOÀN THÀNH — 7629783]
|
||||
- [x] channelQuiet chi con reloading (bo editorOpenFn check); xoa setEditorOpenPredicate/g_editorOpen/g_editorPathCount/clear_editor_open.
|
||||
- [x] VERIFIED: build PASS; live test PASS — PLAY+editor open writeIndex advancing (het cam), LOAD-while-editor OK, close/reopen x4 OK, regression G1.2 PASS, 0 crash/restart.
|
||||
### G1.5 — Regression driver stress [HOÀN THÀNH]
|
||||
- [x] stress driver native_bridge/debug/test_g15_stress.py: 5 + 10 iterations LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE Nexus+EZkeys.
|
||||
- [x] VERIFIED: PASS ca hai run — PLAY+editor open writeIndex advancing moi iter (delta ~290-330), WM_CLOSE state restored OK, bridge alive 30512, 0 EXCEPTION/FAILED/crash.
|
||||
|
||||
## G2 — Persistence restart (2-3 tuần)
|
||||
Mục tiêu: restart không mất instrument — "stop/play nghe lại" ngay sau respawn.
|
||||
- G2.1 save/restore đầy đủ: toàn bộ trạng thái instrument (path, preset, params, transport position) ra đĩa.
|
||||
- G2.2 dump state trước kill: trước khi DAW/bridge bị kill, dump trạng thái hiện tại.
|
||||
- G2.3 auto-reload sau respawn: sau khi bridge respawn, tự load lại từ dump.
|
||||
- G2.4 bỏ !isPlayingRef chặn restore: gỡ guard chặn restore khi đang PLAY.
|
||||
|
||||
## G3 — Health / Watchdog thông minh (1-2 tuần)
|
||||
Mục tiêu: Watchdog thông minh, cô lập lỗi.
|
||||
- G3.1 heartbeat riêng: heartbeat bridge/engine tách khỏi logic audio.
|
||||
- G3.2 phân loại stall: phân biệt hang/crash/stall để xử lý đúng mức.
|
||||
- G3.3 SEH quanh process từng channel: 1 plugin crash → chỉ mute channel đó, không sập cả bridge.
|
||||
|
||||
## G4 — Sandbox plugin (tuỳ chọn, 2-3 tháng)
|
||||
Mục tiêu: plugin process con — VST3 không có chuẩn OOP như AUv3 → wrapper tự làm.
|
||||
Lưu ý: G1+G3 đã lo 90% kịch bản. Khuyến nghị làm sau.
|
||||
|
||||
## Kiến trúc hiện tại (main.cpp 1254 dòng, sau G1.4) — tham chiếu G1.2–G1.5
|
||||
|
||||
- 1 UiThread chung (class ChannelWorker, main.cpp ~236): COM STA + job queue + message pump (cap 128 msg/iteration). MỌI window (native VST + editor children) thuộc thread này; CBT hook IME per-thread + priority ABOVE_NORMAL. loadPlugin/createView/close_editor_now/LOAD jobs đều chạy ở đây — thread sống cả đời bridge, COM STA apartment không chết → Nexus attached() không treo. (G1.1/G1.2)
|
||||
- Audio/main loop (main.cpp ~972-1235): drain control queue → pending-GUI sweep → MIDI snapshot → dispatch sample-accurate (A11: sort theo sampleOffset, renderSegment từng sub-block) → renderAll → bridgeWriteIndex++ → sync real-time. KHÔNG còn PeekMessageW (G1.3); try/catch net bọc cả loop (plugin exception không escape).
|
||||
- Control (SHM): controlQueue @2836 (8×1040), count @11156, attachInFlight @11160, writeIndex @4. Types: 1=PANIC, 2=LOAD, 3=TRANSPORT (0=STOP flush notes + CC64, 1=PLAY, 2=SET_POSITION), 4=OPEN_GUI.
|
||||
- OPEN_GUI (handleOpenGui): defer khi chưa load hoặc đang PLAY → g_pendingGui, sweep khi transportStopped && loaded (G0.1); attach job trên UiThread (g_loadMutex serialize createView); attach-in-flight báo watchdog qua SHM attachInFlight RAII (G0.3); Option B đóng editor channel khác ĐỒNG BỘ qua close_editor_now (G1.2 — bỏ PostMessage(WM_CLOSE)+5s timeout).
|
||||
- LOAD (assign trên UiThread): setReloading(ch + mọi same-path channel) TRƯỚC khi vào DLL; hide + detach editor cũ (closeGUI + destroy_window_on_owner); restore unmute sau job (g_closeInFlight + g_attachPaths ngăn unmute nhầm).
|
||||
- Guards còn lại: g_loadMutex (serialize mọi plugin-DLL entry: createInstance + createView), reloadingCh_[16] (engine — channelQuiet CHỈ mute khi reloading; G1.4 bỏ mute-when-editor-open), g_attachPaths + g_attachMutex (attach-in-flight registry), g_closeInFlight[16], g_guiMutex + g_guiWindows + g_hwndToCh (window registry: giữ sống + reuse + WM_DESTROY cleanup), g_pendingGui + g_pendingGuiMutex, g_cbtHook (strip IMC trên JUCE windows — chống IME recursion 0xC00000FD), destroy_window_on_owner (destroy editor child trên UiThread, không cross-thread — hết 0xc000041d).
|
||||
- VstWindowProc: WM_CLOSE → post_close_gui + hide (KHÔNG DefWindowProc destroy khi view attached — tránh cross-thread destroy); WM_DESTROY → registry cleanup + post_close_gui; WM_SIZE → MoveWindow child + post_resize_view; WM_ERASEBKGND/WM_PAINT no-op (chống trắng loá).
|
||||
- G0.4: save/restore preset quanh close/reload — captureState/restoreState qua MemoryStream (post_close_gui/LOAD); bridge + gui_probe link memorystream.cpp.
|
||||
- Watchdog thread riêng (main.cpp ~600): parent chết → TerminateProcess ngay + check birth-time chống PID reuse (V8 bug 2).
|
||||
|
||||
## Learnings
|
||||
|
||||
- read_file lỗi với main.cpp/lib.rs/gui_probe.cpp → dùng `sed -n` qua exec_command.
|
||||
- MemoryStream::write cần 3 args (buffer, size, int32* numBytesWritten); seek(0, kIBSeekSet, nullptr). memorystream.cpp không thuộc target SDK → add source trực tiếp vào mỗi target dùng Vst3Instrument.cpp (bridge ĐÃ thêm, gui_probe ĐÃ thêm).
|
||||
- tasklist CSV parse: dùng `subprocess.run(['tasklist'])` full output + `line.split()[1]`.
|
||||
- Probe wait_log: log cùng job xuất hiện ~đồng thời → dùng chung `since` window, không reset.
|
||||
- Probe PASS có thể dương tính giả → luôn kiểm tra bridge.log/spawn.log thực.
|
||||
- Deploy: taskkill 3 process → chờ 2s → copy exe → start → chờ 5s → tasklist lấy PID mới → update APP_PID trong probe.
|
||||
- `sleep` không tồn tại trong sh → `python -c "import time; time.sleep(N)"`.
|
||||
- `timeout` Windows khác GNU → dùng python subprocess wrapper.
|
||||
- CRLF: `s.count(old)` với `\n` thật trong heredoc `<<'EOF'` KHÔNG khớp CRLF; dùng script .py file thay vì `python -c` chuỗi dài (escaping lỗi).
|
||||
- 0xC0000409 trong ucrtbase = fail-fast/terminate (không phải C++ exception thường bắt được bằng catch(...) nếu là SEH/fail-fast).
|
||||
@@ -1,14 +1,17 @@
|
||||
// Rebuild app.precompiled.js from app.jsx using @babel/standalone (avoids the
|
||||
// Babel 8 ESM-only CLI conflict). Mirrors package.json's build script:
|
||||
// babel app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js
|
||||
import * as Babel from '@babel/standalone';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
|
||||
// Cache-buster: argv[2] = 'yyyyMMddHHmm' (build_windows.ps1 truyen); mac dinh
|
||||
// = thoi diem build. Stamp MOI URL asset (?v=) TRONG OUTPUT (khong sua source
|
||||
// app.jsx) -> khong bao gio quen bump version sau khi rebuild bundle.
|
||||
const ver = process.argv[2] || new Date().toISOString().replace(/\D/g, '').slice(0, 12);
|
||||
|
||||
const src = readFileSync('app/static/js/app.jsx', 'utf8');
|
||||
const out = Babel.transform(src, {
|
||||
const transformed = Babel.transform(src, {
|
||||
presets: ['react'],
|
||||
filename: 'app.jsx',
|
||||
sourceType: 'script',
|
||||
}).code;
|
||||
writeFileSync('app/static/js/app.precompiled.js', out);
|
||||
console.log('BUILD OK', out.length, 'bytes');
|
||||
const out = transformed.replace(/(sf-fx-realtime\.js\?v=)\d+/g, '$1' + ver);
|
||||
writeFileSync('app/static/js/app.precompiled.js', out.replace(/\r?\n/g, '\r\n')); // repo CRLF
|
||||
console.log('BUILD OK', 'v=' + ver, out.length, 'bytes');
|
||||
|
||||
@@ -79,7 +79,7 @@ cmake -S $NB_DIR -B $BUILD_DIR -G $VS_GEN -A $VS_ARCH `
|
||||
-DCMAKE_BUILD_TYPE=Release `
|
||||
-DVCPKG_TARGET_TRIPLET=x64-windows
|
||||
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
|
||||
cmake --build $BUILD_DIR --config Release --target daw_vst_bridge fx_vst_bridge plugin_host -- /m
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake build failed" -ForegroundColor Red; exit 1 }
|
||||
|
||||
# 6. Copy sidecar to Tauri externalBin location
|
||||
@@ -89,9 +89,17 @@ New-Item -ItemType Directory -Force $BIN_DIR | Out-Null
|
||||
$EXE = Join-Path $BUILD_DIR "Release\daw_vst_bridge.exe"
|
||||
if (-not (Test-Path $EXE)) { Write-Host "ERROR: $EXE not found" -ForegroundColor Red; exit 1 }
|
||||
Copy-Item $EXE (Join-Path $BIN_DIR "daw_vst_bridge-x86_64-pc-windows-msvc.exe") -Force
|
||||
# fx_vst_bridge: VST FX host sidecar (split — see CMakeLists.txt)
|
||||
$FX_EXE = Join-Path $BUILD_DIR "Release\fx_vst_bridge.exe"
|
||||
if (-not (Test-Path $FX_EXE)) { Write-Host "ERROR: $FX_EXE not found" -ForegroundColor Red; exit 1 }
|
||||
Copy-Item $FX_EXE (Join-Path $BIN_DIR "fx_vst_bridge-x86_64-pc-windows-msvc.exe") -Force
|
||||
# plugin_host: VST sandbox child (VST3 crash isolation + jBridge VST2 per-process instances)
|
||||
$PH_EXE = Join-Path $BUILD_DIR "Release\plugin_host.exe"
|
||||
if (-not (Test-Path $PH_EXE)) { Write-Host "ERROR: $PH_EXE not found" -ForegroundColor Red; exit 1 }
|
||||
Copy-Item $PH_EXE (Join-Path $BIN_DIR "plugin_host-x86_64-pc-windows-msvc.exe") -Force
|
||||
|
||||
# 7. Copy fluidsynth runtime DLL next to sidecar (Tauri externalBin bundles only the exe)
|
||||
Write-Host "== [7/7] Copy libfluidsynth-3.dll =="
|
||||
# 7. Copy runtime DLLs next to sidecar (Tauri externalBin bundles only the exe)
|
||||
Write-Host "== [7/7] Copy runtime DLLs (fluidsynth + jpeg) =="
|
||||
$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) {
|
||||
@@ -101,4 +109,15 @@ if (Test-Path $FS_DLL) {
|
||||
} else {
|
||||
Write-Host "WARN: libfluidsynth-3.dll not found - bridge needs it at runtime" -ForegroundColor Yellow
|
||||
}
|
||||
# jpeg62.dll (VST3 hosting image decode; vcpkg libjpeg-turbo) - same candidates as fluidsynth
|
||||
$JPEG_DLL = Join-Path $NB_DIR "vcpkg_installed\x64-windows\bin\jpeg62.dll"
|
||||
if (-not (Test-Path $JPEG_DLL)) { $JPEG_DLL = Join-Path $BUILD_DIR "vcpkg_installed\x64-windows\bin\jpeg62.dll" }
|
||||
if (-not (Test-Path $JPEG_DLL)) { $JPEG_DLL = Join-Path $VCPKG_ROOT "packages\libjpeg-turbo_x64-windows\bin\jpeg62.dll" }
|
||||
if (Test-Path $JPEG_DLL) {
|
||||
Copy-Item $JPEG_DLL (Join-Path $BIN_DIR "jpeg62.dll") -Force
|
||||
Copy-Item $JPEG_DLL (Join-Path $BUILD_DIR "Release\jpeg62.dll") -Force
|
||||
Write-Host " jpeg62.dll copied to src-tauri\binaries"
|
||||
} else {
|
||||
Write-Host "WARN: jpeg62.dll not found - VST3 GUIs may fail to show images" -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host "== DONE: sidecar + DLL in src-tauri\binaries =="
|
||||
|
||||
@@ -57,12 +57,12 @@ if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: bridge sidecar missing" -Foregroun
|
||||
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 }
|
||||
Copy-Item build\scripts\index.html src-tauri\ui\index.html -Force
|
||||
if (-not (Test-Path src-tauri\ui\index.html)) { Write-Host "ERROR: frontend host missing" -ForegroundColor Red; exit 1 }
|
||||
npx tauri build
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "== DONE =="
|
||||
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe"
|
||||
Write-Host " MSI : src-tauri\target\release\bundle\msi\SonicForgeDAW_1.0.0_x64_en-US.msi"
|
||||
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\ScoreFXStudio_1.0.0_x64-setup.exe"
|
||||
Write-Host " MSI : src-tauri\target\release\bundle\msi\ScoreFXStudio_1.0.0_x64_en-US.msi"
|
||||
Write-Host " Verify after install: %APPDATA%\SonicForgeDAW\logs\spawn.log exists=True for daw_engine AND daw_vst_bridge"
|
||||
|
||||
+37
-6
@@ -12,23 +12,32 @@
|
||||
# (VS2019/2022 + vcpkg + vst3sdk submodule + sfizz-src). Chay lai
|
||||
# build_windows.ps1 neu muon build lai bridge/app tu source.
|
||||
# LUU Y: file nay chi dung ky tu ASCII (PowerShell 5.1).
|
||||
$ErrorActionPreference = "Stop"
|
||||
# "Stop" + npm/pip warn tren stderr -> NativeCommandError -> chet oai. Dung
|
||||
# "Continue" + kiem tra $LASTEXITCODE thu cong sau tung lenh native.
|
||||
$ErrorActionPreference = "Continue"
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
Write-Host "== [1/4] Kill tien trinh cu =="
|
||||
Get-Process -Name "daw_engine","sonicforge-daw","daw_vst_bridge" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "== [1/4] Kill tien trinh cu (chi install/dist, khong dong production) =="
|
||||
Get-Process -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Path -like "$PSScriptRoot\install\*" -or $_.Path -like "$PSScriptRoot\dist\daw_engine\*"
|
||||
} | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
Write-Host "== [2/4] Frontend bundle (app.jsx -> app.precompiled.js) =="
|
||||
npm install --no-audit --no-fund
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: npm install that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
|
||||
if (-not (Test-Path "node_modules\@babel\standalone")) {
|
||||
npm install @babel/standalone --no-audit --no-fund
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: npm install @babel/standalone that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
|
||||
}
|
||||
node build.mjs
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: node build.mjs that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
|
||||
|
||||
Write-Host "== [3/4] Build daw_engine.exe (PyInstaller onedir) =="
|
||||
python -m pip install -r requirements.txt pyinstaller pywin32
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: pip install that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
|
||||
pyinstaller engine.spec --clean --noconfirm
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: pyinstaller that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
|
||||
if (-not (Test-Path "dist\daw_engine\daw_engine.exe")) {
|
||||
Write-Host "ERROR: dist\daw_engine\daw_engine.exe khong ton tai" -ForegroundColor Red
|
||||
exit 1
|
||||
@@ -39,12 +48,34 @@ if ($LASTEXITCODE -ne 0) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "== [4/4] Copy vao install\ =="
|
||||
Write-Host "== [4/4] Verify bundle sach (khong config dev) + copy vao install\ =="
|
||||
# Ban cai phai reset ve mac dinh: KHONG duoc mang theo DB/password/secret/
|
||||
# plugin-dirs/scan-state cua dev sang may nguoi dung moi.
|
||||
$devFiles = Get-ChildItem "dist\daw_engine" -Recurse -File -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -in @(".secret_key", "plugin_dirs.json", "sf_scan_state.json", "sf_scan_state.json.bak-root") -or
|
||||
$_.Extension -eq ".db"
|
||||
}
|
||||
if ($devFiles) {
|
||||
Write-Host "ERROR: bundle chua config dev - DUNG TAO BAN CAI:" -ForegroundColor Red
|
||||
$devFiles | ForEach-Object { Write-Host " $($_.FullName)" -ForegroundColor Red }
|
||||
exit 1
|
||||
}
|
||||
New-Item -ItemType Directory -Force "install" | Out-Null
|
||||
if (Test-Path "install\daw_engine") { Remove-Item -Recurse -Force "install\daw_engine" }
|
||||
Copy-Item "dist\daw_engine\*" "install\daw_engine\" -Recurse -Force
|
||||
# Copy-Item wildcard tu dist\daw_engine vao install\daw_engine tao layout PHANG
|
||||
# (noi dung _internal tran ra root, exe khong thay _internal -> treo luc chay).
|
||||
# Robocopy /MIR mirror dung cau truc onedir (exe + _internal) + xoa file thua.
|
||||
robocopy "dist\daw_engine" "install\daw_engine" /MIR /NFL /NDL /NJH /NP /R:1 /W:1 | Out-Null
|
||||
if ($LASTEXITCODE -ge 8) { Write-Host "ERROR: robocopy install that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
|
||||
if (-not (Test-Path "install\daw_engine\_internal")) { Write-Host "ERROR: install\daw_engine\_internal thieu - onedir layout sai" -ForegroundColor Red; exit 1 }
|
||||
# 3 binary (bridge/lib/app) da commit san trong install/ - khong ghi de,
|
||||
# neu user da build lai bridge/app thi tu copy vao day.
|
||||
# VC++ runtime app-local: bridge C++ link /MD can MSVCP140/VCRUNTIME140 canh
|
||||
# exe (khong phu thuoc vc_redist he thong). Dong bo tu src-tauri/binaries.
|
||||
foreach ($dll in @("MSVCP140.dll", "MSVCP140_2.dll", "VCRUNTIME140.dll", "VCRUNTIME140_1.dll")) {
|
||||
$src = "src-tauri\binaries\$dll"
|
||||
if (Test-Path $src) { Copy-Item $src "install\$dll" -Force }
|
||||
else { Write-Host "WARN: thieu $dll trong src-tauri\binaries - chay build_windows.ps1 -Bridge truoc" -ForegroundColor Yellow }
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "== DONE =="
|
||||
|
||||
+178
-79
@@ -1,95 +1,194 @@
|
||||
# build_windows.ps1 - ONE-COMMAND build: daw_engine.exe (PyInstaller) + Tauri v2 (NSIS + MSI)
|
||||
# Chay tren Windows: powershell -ExecutionPolicy Bypass -File build_windows.ps1
|
||||
# LUU Y: file nay chi dung ky tu ASCII (khong dau, khong em-dash) - PowerShell 5.1
|
||||
# doc .ps1 khong BOM theo ANSI, ky tu Unicode bi hong -> "String is missing terminator".
|
||||
$ErrorActionPreference = "Stop"
|
||||
# build_windows.ps1 - ONE-COMMAND build: frontend + native bridge + engine + Tauri (NSIS + MSI)
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 # FULL (tat ca)
|
||||
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 -Frontend # chi bundle JS + sync static
|
||||
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 -Bridge # chi build native bridge + sync binaries
|
||||
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 -Engine # chi build engine + sync resources
|
||||
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 -Tauri # chi build installer Tauri
|
||||
# ... -SkipPip -SkipNpm # bo qua cai dependency
|
||||
#
|
||||
# Moi mode TU DONG copy output vao dung vi tri (dist/, src-tauri/resources/,
|
||||
# src-tauri/binaries/) va TU DONG bump cache-buster (?v=) -> khong the miss
|
||||
# buoc copy nao du build theo cach nao. Bridge chi rebuild khi source moi hon.
|
||||
# LUU Y: file nay chi dung ky tu ASCII (khong dau) - PowerShell 5.1 doc an toan.
|
||||
param(
|
||||
[switch]$Frontend,
|
||||
[switch]$Bridge,
|
||||
[switch]$Engine,
|
||||
[switch]$Tauri,
|
||||
[switch]$SkipPip,
|
||||
[switch]$SkipNpm
|
||||
)
|
||||
|
||||
# "Stop" + npm/pip warn tren stderr -> NativeCommandError -> chet oai (da gap o
|
||||
# build_install.ps1). Dung "Continue" + kiem tra $LASTEXITCODE sau tung lenh native.
|
||||
$ErrorActionPreference = "Continue"
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
# Kill moi daw_engine con song tu build/truoc (windowed, khong console ->
|
||||
# de quen -> file exe dang chay bi khoa -> Tauri build loi PermissionDenied
|
||||
# khi doc externalBin). Stop-Process im lang neu khong co tien trinh nao.
|
||||
Write-Host "== [0/6] Kill daw_engine.exe cu (neu dang chay) =="
|
||||
Get-Process -Name "daw_engine" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Milliseconds 500
|
||||
$any = $Frontend -or $Bridge -or $Engine -or $Tauri
|
||||
$doFrontend = $Frontend -or $Engine -or (-not $any) # Engine can bundle frontend moi truoc
|
||||
$doBridge = $Bridge -or (-not $any)
|
||||
$doEngine = $Engine -or (-not $any)
|
||||
$doTauri = $Tauri -or (-not $any)
|
||||
|
||||
Write-Host "== [1/6] Python dependencies =="
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt pyinstaller pywin32
|
||||
$Stamp = Get-Date -Format "yyyyMMddHHmm"
|
||||
|
||||
Write-Host "== [2/6] Frontend bundle (app.jsx -> app.precompiled.js) =="
|
||||
npm install
|
||||
# Dam bao @babel/standalone co (build.mjs import truc tiep - da gap
|
||||
# ERR_MODULE_NOT_FOUND tren may Windows khi package.json cu thieu dep).
|
||||
if (-not (Test-Path "node_modules\@babel\standalone")) {
|
||||
Write-Host "Thieu @babel/standalone - dang cai them..."
|
||||
npm install @babel/standalone --no-audit --no-fund
|
||||
}
|
||||
if (-not (Test-Path "node_modules\@babel\standalone")) {
|
||||
Write-Host "ERROR: Khong cai duoc @babel/standalone. Kiem tra ket noi npm!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
node build.mjs # @babel/standalone; thay cho 'npm run build' (Babel 8 ESM-only CLI conflict)
|
||||
function Fail([string]$msg) { Write-Host "ERROR: $msg" -ForegroundColor Red; exit 1 }
|
||||
|
||||
Write-Host "== [3/6] Build daw_engine.exe (PyInstaller) =="
|
||||
pyinstaller engine.spec --clean --noconfirm
|
||||
|
||||
Write-Host "== [3.5/6] Verify bundle contents (app/static, app/templates phai co) =="
|
||||
# Bat loi bundle NGAY tai build (da gap 2 lan: chay pyinstaller tu noi khac
|
||||
# -> static/templates thieu -> exe crash 'Directory ...\app\static does not exist').
|
||||
# Dung tools/verify_bundle.py (parse TOC bang ast, chap nhan ca / va \) thay
|
||||
# vi regex thong thuong (TOC Windows co the dung backslash -> false positive).
|
||||
# Truoc tien kiem tra engine.spec phai la ban moi (collect_data_files).
|
||||
$specContent = Get-Content "engine.spec" -Raw -ErrorAction SilentlyContinue
|
||||
if ($specContent -notmatch "collect_data_files\('app'") {
|
||||
Write-Host "ERROR: engine.spec CU (thieu collect_data_files('app')). Pull code moi truoc khi build!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
python tools\verify_bundle.py
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "ERROR: Bundle thieu asset - dung build, kiem tra engine.spec datas!" -ForegroundColor Red
|
||||
exit 1
|
||||
if ($doEngine -or $doTauri) {
|
||||
Write-Host "== [0] Kill engine cu (chi install/dist/resources, khong dong production) =="
|
||||
Get-Process -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Path -like "$PSScriptRoot\install\*" -or $_.Path -like "$PSScriptRoot\dist\daw_engine\*" -or $_.Path -like "$PSScriptRoot\src-tauri\resources\daw_engine\*"
|
||||
} | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
Write-Host "== [4/6] ONEDIR engine -> src-tauri/resources/daw_engine (Tauri resources) =="
|
||||
# ONEDIR: copy ca thu muc dist\daw_engine\ (exe + _internal) vao resources.
|
||||
# Tauri bundle resources -> resource_dir()/daw_engine/daw_engine.exe luc runtime.
|
||||
if (-not (Test-Path "dist\daw_engine\daw_engine.exe")) {
|
||||
Write-Host "ERROR: dist\daw_engine\daw_engine.exe khong ton tai (onedir build loi?)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if (Test-Path "src-tauri\resources\daw_engine") {
|
||||
Remove-Item -Recurse -Force "src-tauri\resources\daw_engine"
|
||||
}
|
||||
New-Item -ItemType Directory -Force "src-tauri\resources\daw_engine" | Out-Null
|
||||
Copy-Item "dist\daw_engine\*" "src-tauri\resources\daw_engine\" -Recurse -Force
|
||||
Write-Host "Copied onedir engine -> src-tauri\resources\daw_engine"
|
||||
# ---- [1] Frontend bundle + sync static -------------------------------------
|
||||
if ($doFrontend) {
|
||||
Write-Host "== [1] Frontend bundle (app.jsx -> app.precompiled.js, cache-buster v=$Stamp) =="
|
||||
if (-not $SkipNpm) { npm install; if ($LASTEXITCODE -ne 0) { Fail "npm install that bai" } }
|
||||
if (-not (Test-Path "node_modules\@babel\standalone")) {
|
||||
Write-Host "Thieu @babel/standalone - dang cai them..."
|
||||
npm install @babel/standalone --no-audit --no-fund
|
||||
if ($LASTEXITCODE -ne 0) { Fail "npm install @babel/standalone that bai" }
|
||||
}
|
||||
if (-not (Test-Path "node_modules\@babel\standalone")) { Fail "Khong cai duoc @babel/standalone. Kiem tra ket noi npm!" }
|
||||
node build.mjs $Stamp
|
||||
if ($LASTEXITCODE -ne 0) { Fail "build.mjs that bai" }
|
||||
|
||||
Write-Host "== [5/6] VC++ Redistributable cho hooks.nsh =="
|
||||
if (-not (Test-Path src-tauri\vc_redist.x64.exe)) {
|
||||
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile src-tauri\vc_redist.x64.exe
|
||||
# Stamp cache-buster vao index.html source (no-cache HTML, bundle ?v= phai moi)
|
||||
$idx = "app\templates\index.html"
|
||||
$utf8 = New-Object System.Text.UTF8Encoding($false)
|
||||
$html = [IO.File]::ReadAllText($idx, $utf8)
|
||||
[IO.File]::WriteAllText($idx, [regex]::Replace($html, "app\.precompiled\.js\?v=\d+", "app.precompiled.js?v=$Stamp"), $utf8)
|
||||
|
||||
# Sync 3 file static -> dist + src-tauri/resources (ban packaged engine doc tu day)
|
||||
$pairs = @(
|
||||
@("static\js\app.precompiled.js", "_internal\app\static\js\app.precompiled.js"),
|
||||
@("static\js\sf-fx-realtime.js", "_internal\app\static\js\sf-fx-realtime.js"),
|
||||
@("templates\index.html", "_internal\app\templates\index.html")
|
||||
)
|
||||
$roots = @("dist\daw_engine", "src-tauri\resources\daw_engine")
|
||||
foreach ($p in $pairs) {
|
||||
$src = Join-Path "app" $p[0]
|
||||
foreach ($r in $roots) {
|
||||
$dst = Join-Path $r $p[1]
|
||||
New-Item -ItemType Directory -Force (Split-Path $dst) | Out-Null
|
||||
Copy-Item $src $dst -Force
|
||||
}
|
||||
}
|
||||
# Verify hash source == copy (bat loi "copy sai / thieu")
|
||||
foreach ($p in $pairs) {
|
||||
$srcHash = (Get-FileHash (Join-Path "app" $p[0]) -Algorithm SHA256).Hash
|
||||
foreach ($r in $roots) {
|
||||
$dst = Join-Path $r $p[1]
|
||||
if (-not (Test-Path $dst)) { Fail "Thieu file sau sync: $dst" }
|
||||
if ((Get-FileHash $dst -Algorithm SHA256).Hash -ne $srcHash) { Fail "Hash khac nhau sau sync: $dst" }
|
||||
}
|
||||
}
|
||||
Write-Host "Sync static OK (dist + src-tauri/resources)"
|
||||
}
|
||||
|
||||
Write-Host "== [6/6] Tauri build (NSIS .exe + MSI) =="
|
||||
# Guard: resources/daw_engine PHAI co exe + _internal truoc khi tauri build
|
||||
# (glob trong tauri.conf.json fail ngay "path not found" neu thieu).
|
||||
if (-not (Test-Path "src-tauri\resources\daw_engine\daw_engine.exe")) {
|
||||
Write-Host "ERROR: src-tauri\resources\daw_engine\daw_engine.exe khong co - buoc [4/6] that bai?" -ForegroundColor Red
|
||||
exit 1
|
||||
# ---- [2] Native bridge (MSBuild Release x64) --------------------------------
|
||||
if ($doBridge) {
|
||||
Write-Host "== [2] Native bridge (MSBuild Release x64 -> src-tauri/binaries) =="
|
||||
$msbuildCandidates = @(
|
||||
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe",
|
||||
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe",
|
||||
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\amd64\MSBuild.exe",
|
||||
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\MSBuild.exe"
|
||||
)
|
||||
$msbuild = $msbuildCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $msbuild) { Fail "Khong tim thay MSBuild.exe. Cai VS Build Tools 2019/2022 (workload C++) va chay CMake native_bridge truoc." }
|
||||
|
||||
foreach ($name in @("daw_vst_bridge", "fx_vst_bridge", "plugin_host")) {
|
||||
$proj = "native_bridge\build\bridge-win\$name.vcxproj"
|
||||
$exe = "native_bridge\build\bridge-win\Release\$name.exe"
|
||||
if (-not (Test-Path $proj)) { Fail "Thieu project: $proj (can CMake native_bridge truoc - xem native_bridge/build/bridge-win)" }
|
||||
$newest = Get-ChildItem -Path "native_bridge\src","native_bridge\include" -Recurse -Include *.cpp,*.h,*.hpp -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
$needBuild = $true
|
||||
if ((Test-Path $exe) -and $newest) { $needBuild = $newest.LastWriteTime -gt (Get-Item $exe).LastWriteTime }
|
||||
if ($needBuild) {
|
||||
Write-Host "Build $name ..."
|
||||
& $msbuild $proj "/p:Configuration=Release" "/p:Platform=x64" "/m" "/nologo" "/v:m"
|
||||
if ($LASTEXITCODE -ne 0) { Fail "MSBuild $name that bai" }
|
||||
} else {
|
||||
Write-Host "Skip $name (exe moi nhat)"
|
||||
}
|
||||
if (-not (Test-Path $exe)) { Fail "Thieu output: $exe" }
|
||||
Copy-Item $exe "src-tauri\binaries\$name.exe" -Force
|
||||
Copy-Item $exe "src-tauri\binaries\$name-x86_64-pc-windows-msvc.exe" -Force
|
||||
}
|
||||
foreach ($dll in @("libfluidsynth-3.dll", "jpeg62.dll")) {
|
||||
if (Test-Path "native_bridge\build\Release\$dll") {
|
||||
Copy-Item "native_bridge\build\Release\$dll" "src-tauri\binaries\$dll" -Force
|
||||
}
|
||||
}
|
||||
# VC++ runtime app-local (MSVCP140/VCRUNTIME140) — bridge C++ link /MD nen
|
||||
# can cac dll nay CANH exe, khong phu thuoc vc_redist he thong. Lay tu
|
||||
# System32 (ban redist da cai) de bundle vao installer/portable.
|
||||
foreach ($dll in @("MSVCP140.dll", "MSVCP140_2.dll", "VCRUNTIME140.dll", "VCRUNTIME140_1.dll")) {
|
||||
$sys = Join-Path $env:WINDIR "System32\$dll"
|
||||
if (Test-Path $sys) { Copy-Item $sys "src-tauri\binaries\$dll" -Force }
|
||||
else { Write-Host "WARN: thieu $dll trong System32 (VC++ redist chua cai?)" -ForegroundColor Yellow }
|
||||
}
|
||||
Write-Host "Bridge OK -> src-tauri/binaries"
|
||||
}
|
||||
if (-not (Test-Path "src-tauri\resources\daw_engine\_internal")) {
|
||||
Write-Host "ERROR: thieu src-tauri\resources\daw_engine\_internal (onedir khong day du)" -ForegroundColor Red
|
||||
exit 1
|
||||
|
||||
# ---- [3] Engine (PyInstaller) + sync resources ------------------------------
|
||||
if ($doEngine) {
|
||||
Write-Host "== [3] Engine (PyInstaller onedir -> src-tauri/resources) =="
|
||||
$specContent = Get-Content "engine.spec" -Raw -ErrorAction SilentlyContinue
|
||||
if ($specContent -notmatch "collect_data_files\('app'") { Fail "engine.spec CU (thieu collect_data_files('app')). Pull code moi truoc khi build!" }
|
||||
if (-not $SkipPip) {
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt pyinstaller pywin32
|
||||
}
|
||||
if (Test-Path "dist\daw_engine") { Remove-Item -Recurse -Force "dist\daw_engine" }
|
||||
pyinstaller engine.spec --clean --noconfirm
|
||||
if ($LASTEXITCODE -ne 0) { Fail "pyinstaller that bai" }
|
||||
python tools\verify_bundle.py
|
||||
if ($LASTEXITCODE -ne 0) { Fail "Bundle thieu asset - dung build, kiem tra engine.spec datas" }
|
||||
# Ban cai phai SACH: khong duoc mang theo DB/password/secret/scan-state cua dev.
|
||||
$devFiles = Get-ChildItem "dist\daw_engine" -Recurse -File -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -in @(".secret_key", "plugin_dirs.json", "sf_scan_state.json", "sf_scan_state.json.bak-root") -or
|
||||
$_.Extension -eq ".db"
|
||||
}
|
||||
if ($devFiles) {
|
||||
$devFiles | ForEach-Object { Write-Host " DEV-CONFIG: $($_.FullName)" -ForegroundColor Red }
|
||||
Fail "Bundle chua config dev - DUNG TAO BAN CAI"
|
||||
}
|
||||
if (-not (Test-Path "dist\daw_engine\daw_engine.exe")) { Fail "dist\daw_engine\daw_engine.exe khong ton tai (onedir build loi?)" }
|
||||
# Robocopy /MIR (khong Copy-Item wildcard): giu dung cau truc onedir exe+_internal.
|
||||
robocopy "dist\daw_engine" "src-tauri\resources\daw_engine" /MIR /NFL /NDL /NJH /NP /R:1 /W:1 | Out-Null
|
||||
if ($LASTEXITCODE -ge 8) { Fail "robocopy resources that bai (exit $LASTEXITCODE)" }
|
||||
if (-not (Test-Path "src-tauri\resources\daw_engine\_internal")) { Fail "resources/daw_engine/_internal thieu - onedir layout sai" }
|
||||
Write-Host "Engine OK -> src-tauri/resources/daw_engine"
|
||||
}
|
||||
|
||||
# ---- [4] Tauri installer -----------------------------------------------------
|
||||
if ($doTauri) {
|
||||
Write-Host "== [4] VC++ Redistributable (cho hooks.nsh) =="
|
||||
if (-not (Test-Path src-tauri\vc_redist.x64.exe)) {
|
||||
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile src-tauri\vc_redist.x64.exe
|
||||
}
|
||||
|
||||
Write-Host "== [5] Tauri build (NSIS + MSI) =="
|
||||
if (-not (Test-Path "src-tauri\resources\daw_engine\daw_engine.exe")) { Fail "resources/daw_engine thieu - chay -Engine hoac full truoc" }
|
||||
if (-not (Test-Path "src-tauri\resources\daw_engine\_internal")) { Fail "resources/daw_engine/_internal thieu (onedir khong day du)" }
|
||||
foreach ($name in @("daw_vst_bridge", "fx_vst_bridge", "plugin_host")) {
|
||||
if (-not (Test-Path "src-tauri\binaries\$name-x86_64-pc-windows-msvc.exe")) { Fail "Thieu src-tauri\binaries\$name-x86_64-pc-windows-msvc.exe - chay -Bridge truoc" }
|
||||
}
|
||||
if (-not $SkipNpm) { npm install -D @tauri-apps/cli; if ($LASTEXITCODE -ne 0) { Fail "npm install @tauri-apps/cli that bai" } }
|
||||
npx tauri build
|
||||
if ($LASTEXITCODE -ne 0) { Fail "tauri build that bai" }
|
||||
}
|
||||
npm install -D @tauri-apps/cli
|
||||
npx tauri build
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "== DONE =="
|
||||
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe"
|
||||
Write-Host " MSI : src-tauri\target\release\bundle\msi\SonicForgeDAW_1.0.0_x64_en-US.msi"
|
||||
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\ScoreFXStudio_1.0.0_x64-setup.exe"
|
||||
Write-Host " MSI : src-tauri\target\release\bundle\msi\ScoreFXStudio_1.0.0_x64_en-US.msi"
|
||||
Write-Host ""
|
||||
Write-Host "== XAC MINH SAU KHI CAI DAT (quan trong) =="
|
||||
Write-Host " Mo %APPDATA%\SonicForgeDAW\logs\spawn.log - phai thay:"
|
||||
Write-Host " [resource_dir/daw_engine (map layout)] ...exists=True"
|
||||
Write-Host " Neu exists=False: bundle resources KHONG vao installer (chay lai buoc [4/6])."
|
||||
Write-Host " Engine phai nam o: <thu muc cai dat>\daw_engine\daw_engine.exe"
|
||||
Write-Host "== XAC MINH SAU KHI CAI DAT =="
|
||||
Write-Host " Mo %APPDATA%\SonicForgeDAW\logs\spawn.log - phai thay [resource_dir/daw_engine (map layout)] ...exists=True"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# deploy_engine_windows.ps1 - REBUILD + DEPLOY daw_engine.exe (PyInstaller onedir)
|
||||
# Chay TREN may Windows 192.168.1.24 (source clone tai C:\Users\locpham\SonicForgeStudio):
|
||||
# powershell -ExecutionPolicy Bypass -File deploy_engine_windows.ps1
|
||||
# Lam gi: git pull code moi -> build.mjs (frontend) -> pyinstaller engine.spec
|
||||
# (backend fix vao exe) -> detect noi engine dang chay -> copy bundle moi vao do
|
||||
# -> kill + restart engine. LUU Y: chi dung ky tu ASCII (PowerShell 5.1 doc ANSI).
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
Write-Host "== [0/6] Pull code moi (fix MASTERING PANEL VST FX) =="
|
||||
git pull --ff-only
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "git pull LOI - kiem tra network/credential" -ForegroundColor Red; exit 1 }
|
||||
|
||||
Write-Host "== [1/6] Frontend bundle (app.jsx -> app.precompiled.js) =="
|
||||
if (-not (Test-Path "node_modules\@babel\standalone")) {
|
||||
npm install @babel/standalone --no-audit --no-fund
|
||||
}
|
||||
node build.mjs
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "build.mjs LOI" -ForegroundColor Red; exit 1 }
|
||||
|
||||
Write-Host "== [2/6] Rebuild daw_engine.exe (PyInstaller, backend fix vao PYZ) =="
|
||||
pyinstaller engine.spec --clean --noconfirm
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "pyinstaller LOI" -ForegroundColor Red; exit 1 }
|
||||
|
||||
Write-Host "== [3/6] Verify bundle =="
|
||||
python tools\verify_bundle.py
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "Bundle thieu asset - dung deploy" -ForegroundColor Red; exit 1 }
|
||||
|
||||
Write-Host "== [4/6] Detect noi engine dang chay =="
|
||||
$proc = Get-Process -Name "daw_engine" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
$runDir = $null
|
||||
if ($proc) {
|
||||
$runDir = Split-Path $proc.Path -Parent
|
||||
Write-Host "Engine dang chay tai: $runDir (PID $($proc.Id))"
|
||||
} else {
|
||||
$cand = @(
|
||||
"C:\Users\locpham\SonicForgeStudio\install\daw_engine",
|
||||
"C:\Users\locpham\SonicForgeStudio\dist\daw_engine"
|
||||
)
|
||||
foreach ($c in $cand) {
|
||||
if (Test-Path (Join-Path $c "daw_engine.exe")) { $runDir = $c; break }
|
||||
}
|
||||
if (-not $runDir) { Write-Host "Khong tim thay noi chua daw_engine.exe" -ForegroundColor Red; exit 1 }
|
||||
Write-Host "Engine chua chay - se deploy vao: $runDir"
|
||||
}
|
||||
|
||||
Write-Host "== [5/6] Kill engine cu + copy bundle moi =="
|
||||
if ($proc) {
|
||||
Stop-Process -Name "daw_engine" -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
Copy-Item "dist\daw_engine\*" $runDir -Recurse -Force
|
||||
Write-Host "Da copy bundle moi vao $runDir"
|
||||
|
||||
Write-Host "== [6/6] Restart engine =="
|
||||
$exe = Join-Path $runDir "daw_engine.exe"
|
||||
Start-Process $exe -WorkingDirectory $runDir
|
||||
Start-Sleep -Seconds 6
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "== XAC MINH =="
|
||||
try {
|
||||
$r = Invoke-WebRequest -Uri "http://localhost:8000/" -UseBasicParsing -TimeoutSec 10
|
||||
Write-Host "HTTP $($r.StatusCode) - engine da chay lai OK" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "Chua thay engine tren port 8000 (co the dang chay port 8001-8010 hoac Tauri shell chua spawn)" -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host "Hard refresh trinh duyet (Ctrl+Shift+R) -> Plugin Manager -> Scan -> MASTERING PANEL -> VST FX phai hien thi."
|
||||
+38
-5
@@ -1,4 +1,4 @@
|
||||
"""SonicForge DAW — Desktop engine launcher (PyInstaller entry point).
|
||||
"""ScoreFX Studio — Desktop engine launcher (PyInstaller entry point).
|
||||
|
||||
Chay FastAPI backend duoi dang tien trinh nen (sidecar) cho ban cai Standalone
|
||||
Windows (Tauri v2 shell). Tu chon port 8000-8010 (fallback neu bi chiem), tu
|
||||
@@ -61,12 +61,21 @@ def _parent_alive(pid):
|
||||
# pid khong con ton tai.
|
||||
if os.name == "nt":
|
||||
import ctypes
|
||||
# OpenProcess chi bao NULL khi process object da bi reap (moi handle
|
||||
# dong het). Process vua bi kill (taskkill/crash) con "zombie" mot
|
||||
# thoi gian -> OpenProcess van thanh cong -> watchdog tuong cha con
|
||||
# song -> engine khong bao gio tu thoat. WaitForSingleObject tren
|
||||
# handle co PROCESS_SYNCHRONIZE phan biet dung: object da terminate
|
||||
# thi SIGNALED ngay, du handle con ton tai.
|
||||
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
||||
h = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
||||
PROCESS_SYNCHRONIZE = 0x00100000
|
||||
h = ctypes.windll.kernel32.OpenProcess(
|
||||
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE, False, pid)
|
||||
if not h:
|
||||
return False # tien trinh da chet
|
||||
return False # process object da mat -> cha da chet
|
||||
terminated = ctypes.windll.kernel32.WaitForSingleObject(h, 0) == 0
|
||||
ctypes.windll.kernel32.CloseHandle(h)
|
||||
return True
|
||||
return not terminated
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
@@ -107,6 +116,19 @@ def main():
|
||||
port = _pick_port()
|
||||
os.environ["SF_PORT"] = str(port)
|
||||
|
||||
# Announce port to Tauri shell: UI bootstrap đọc %APPDATA%/SonicForgeDAW/
|
||||
# engine_port.txt (qua lệnh get_engine_port) để kết nối ĐÚNG engine của
|
||||
# chính nó thay vì quét port — quét 8000-8010 có thể dính engine của
|
||||
# instance khác (2 app chạy cùng lúc) → UI mất server khi instance đó tắt.
|
||||
# Format "port\nengine_exe_path": Tauri đối chiếu path engine với candidate
|
||||
# của CHÍNH nó → 2 app chạy song song mỗi cái lấy đúng port của mình.
|
||||
try:
|
||||
port_file = os.path.join(os.path.dirname(log_dir), "engine_port.txt")
|
||||
with open(port_file, "w", encoding="utf-8") as f:
|
||||
f.write("%d\n%s" % (port, sys.executable))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Log ra file — khi dong goi console=False, stdout khong nhin thay duoc.
|
||||
try:
|
||||
logging.basicConfig(
|
||||
@@ -140,11 +162,22 @@ def main():
|
||||
logging.getLogger("desktop_engine").info(
|
||||
"Parent process exited — shutting down engine")
|
||||
server.should_exit = True
|
||||
# Graceful shutdown co the TREO vo han: uvicorn
|
||||
# _wait_tasks_to_complete cho open connection (client LAN
|
||||
# dang mo browser) va timeout_graceful_shutdown=None.
|
||||
# => engine song dai khi app chet dot ngot (crash/Task
|
||||
# Manager). Hard-exit fallback sau 5s dam bao thoat het.
|
||||
def _force_exit():
|
||||
time.sleep(5.0)
|
||||
logging.getLogger("desktop_engine").info(
|
||||
"Graceful shutdown hung — forcing exit")
|
||||
os._exit(0)
|
||||
threading.Thread(target=_force_exit, daemon=True).start()
|
||||
return
|
||||
threading.Thread(target=_watchdog, daemon=True).start()
|
||||
|
||||
logger = logging.getLogger("desktop_engine")
|
||||
logger.info("SonicForge engine listening on 0.0.0.0:%d", port)
|
||||
logger.info("ScoreFX engine listening on 0.0.0.0:%d", port)
|
||||
lan_ips = _lan_ips()
|
||||
if lan_ips:
|
||||
logger.info("LAN access — mo browser o may khac: %s",
|
||||
|
||||
+47
-7
@@ -5,14 +5,42 @@
|
||||
import os
|
||||
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_data_files
|
||||
|
||||
# Native DLL cho pedalboard va soundfile (Windows .pyd/.dll, Linux .so)
|
||||
binaries = collect_dynamic_libs('pedalboard')
|
||||
binaries += collect_dynamic_libs('soundfile')
|
||||
# Native DLL cho soundfile (Windows .pyd/.dll, Linux .so)
|
||||
binaries = collect_dynamic_libs('soundfile')
|
||||
|
||||
# Root tuyet doi cua thu muc chua spec — dung cho cac datas KHONG nam trong
|
||||
# package 'app' (vd thu muc md/). Cac assets cua app (static/templates/models)
|
||||
# duoc bundle qua collect_data_files('app').
|
||||
_SPEC_ROOT = os.path.abspath(SPECPATH)
|
||||
# Native bridge cho VST FX (fx_vst_bridge.exe + DLL + plugin_host) — bundle vao
|
||||
# root (dest '.') de find_bridge_exe (frozen: exe_dir = thu muc chua
|
||||
# sys.executable = dist/daw_engine) tu dong tim thay. Chi build tren Windows.
|
||||
if os.name == 'nt':
|
||||
# 2 nguon build LECH NHAU (build_windows.ps1 [2]): exe tu MSBuild
|
||||
# bridge-win\Release, DLL tu CMake build\Release. Truoc day engine.spec chi
|
||||
# nhin build\Release -> bundle chua fx_vst_bridge.exe CU/HONG (282KB,
|
||||
# --scan crash c0000409 ngay ca voi dir khong ton tai). Lay tung loai tu
|
||||
# dung nguon, co fallback ve build\Release cho checkout cu.
|
||||
_exe_dir = os.path.join(_SPEC_ROOT, 'native_bridge', 'build', 'bridge-win', 'Release')
|
||||
_dll_dir = os.path.join(_SPEC_ROOT, 'native_bridge', 'build', 'Release')
|
||||
if not os.path.isdir(_exe_dir):
|
||||
print("WARN: khong thay bridge-win/Release - fallback ve build/Release (bridge co the cu)")
|
||||
_exe_dir = _dll_dir
|
||||
for _b in ('daw_vst_bridge.exe', 'fx_vst_bridge.exe', 'plugin_host.exe'):
|
||||
_bp = os.path.join(_exe_dir, _b)
|
||||
if os.path.exists(_bp):
|
||||
binaries.append((_bp, '.'))
|
||||
for _b in ('jpeg62.dll', 'libfluidsynth-3.dll'):
|
||||
_bp = os.path.join(_dll_dir, _b)
|
||||
if os.path.exists(_bp):
|
||||
binaries.append((_bp, '.'))
|
||||
# G3: JUCE FX bridge (graph latency + processed samples) - bundle rieng vi
|
||||
# build o native_bridge/build_juce/Release (khac dir build bridge cu).
|
||||
_juce_dir = os.path.join(_SPEC_ROOT, 'native_bridge', 'build_juce', 'Release')
|
||||
_jp = os.path.join(_juce_dir, 'juce_fx_bridge.exe')
|
||||
if os.path.exists(_jp):
|
||||
binaries.append((_jp, '.'))
|
||||
|
||||
|
||||
# ⚠️ GOC ROOT CUA MOI LOI 'app\\static does not exist' (gap 3 lan):
|
||||
# lenh `pyinstaller engine.spec` (entry-point script) KHONG them CWD vao
|
||||
@@ -27,7 +55,13 @@ if _SPEC_ROOT not in _sys.path:
|
||||
# Assets cua app: bundle QUA IMPORT SYSTEM (collect_data_files) — an toan nhat.
|
||||
# Loai tru storage (57MB soundfonts/uploads — vo ich, config.py da chuyen
|
||||
# storage sang %APPDATA%\\SonicForgeDAW khi frozen) va __pycache__.
|
||||
datas = collect_data_files('app', excludes=['**/storage/**', '**/__pycache__/**', '**/*.pyc'])
|
||||
# LOAI CA DEV CONFIG O app/ ROOT: bundle cai dat phai SAC — khong duoc mang
|
||||
# theo DB/password/secret/scan-state cua dev sang may nguoi dung moi.
|
||||
datas = collect_data_files('app', excludes=[
|
||||
'**/storage/**', '**/__pycache__/**', '**/*.pyc',
|
||||
'**/*.db', '**/.secret_key', '**/plugin_dirs.json', '**/sf_scan_state.json',
|
||||
'**/*.bak-root',
|
||||
])
|
||||
# Fallback cuoi cung: neu collect_data_files van tra ve rong (phong moi truong
|
||||
# hop ky la), dung datas TINH absolute — tinh huong xau nhat van co du assets.
|
||||
if not datas:
|
||||
@@ -89,9 +123,15 @@ _hidden = [
|
||||
'uvicorn.protocols.http.auto',
|
||||
'uvicorn.protocols.websockets',
|
||||
'uvicorn.protocols.websockets.auto',
|
||||
'pedalboard',
|
||||
'websockets', # uvicorn WS protocol runtime dep — /ws/fx-realtime/{id}
|
||||
'websockets.legacy',
|
||||
'websockets.legacy.client',
|
||||
# pedalboard (GPL-3.0) DA XOA — native_bridge thay the (xem PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md)
|
||||
'soundfile',
|
||||
'sf2utils',
|
||||
# pyfluidsynth (fluidsynth.py cffi) - render soundfont native cho
|
||||
# /soundfont-render (browser client). Can libfluidsynth-3.dll ke exe.
|
||||
'fluidsynth',
|
||||
'mido.backends.rtmidi',
|
||||
] + _scipy_signal_hidden
|
||||
|
||||
@@ -143,8 +183,8 @@ exe = EXE(
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False, # True khi debug (xem log truc tiep), False cho production
|
||||
# Icon exe = favicon cua app (app/templates/favicon.svg -> render PNG -> ICO,
|
||||
# sinh boi tools/gen_favicon_ico.py). Cung nguon voi icon hien thi trong app.
|
||||
# Icon exe = favicon cua app (scorefx_favicon.png -> src-tauri/icons/favicon.ico,
|
||||
# sinh bang PIL tu tools/gen_icons.py). Cung nguon voi icon hien thi trong app.
|
||||
icon='src-tauri/icons/favicon.ico',
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# TECHNICAL SPECIFICATION: AUDIO FX PROCESSING PIPELINE
|
||||
|
||||
This document describes the Digital Signal Processing (DSP) architecture within the Audio Engine, tracing the complete path from raw sound sources (MIDI, Audio Clips) through insert FX chains to hardware outputs and offline rendering.
|
||||
|
||||
---
|
||||
|
||||
## 1. SIGNAL FLOW TOPOLOGY
|
||||
|
||||
Audio processing executes sequentially (or via a Directed Acyclic Graph - DAG) in fixed-size sample buffers (e.g., 512 samples/block).
|
||||
|
||||
```text
|
||||
[ SOUND SOURCES ON TRACK ]
|
||||
├── MIDI Item --> [ VSTi / Soundfont ] --(Rendered Audio)--> Audio Buffer
|
||||
└── Audio Clip -------------------------------------------> Audio Buffer
|
||||
│
|
||||
▼
|
||||
[ TRACK FX CHAIN ] <---------------- (Managed via Modal UI) │
|
||||
├── Slot 1: Custom FX (In-house DSP) │
|
||||
├── Slot 2: VST FX (e.g., FabFilter, Waves) │
|
||||
└── Slot n: VST FX (e.g., iZotope Ozone) │
|
||||
│
|
||||
[ AUDIO MIXER / SUMMING BUS ] <-----------------------------------+ (Summed from all tracks)
|
||||
│ (Linear Signal Summing)
|
||||
▼
|
||||
[ MASTER FX CHAIN ]
|
||||
├── Slot 1: VST EQ / Compressor (Mastering)
|
||||
└── Slot n: VST Limiter (Peak Limiting / Output Optimization)
|
||||
│
|
||||
▼
|
||||
[ MAIN OUT (FINAL DISTRIBUTION) ]
|
||||
├── 1. Realtime Preview (Direct Hardware Output via ASIO/WASAPI/WebAudio)
|
||||
└── 2. Offline Export (Rendered to .WAV, .MP3, .OGG)
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. COMPONENT DEEP DIVE
|
||||
|
||||
### Step 1: Sound Sources
|
||||
|
||||
Every track hosts source items (Clips/Regions). Prior to entering the FX Chain, source data is converted into a normalized 32-bit or 64-bit floating-point Audio Buffer ($[-1.0, +1.0]$).
|
||||
|
||||
* **Audio Clip:** Raw audio data (WAV, MP3) is read from disk or memory, decoded, and buffered directly to the track's audio buffer.
|
||||
* **MIDI Item:** MIDI events (`Note On`, `Note Off`, `Pitch Bend`) are read in real time and routed to a Virtual Instrument (VSTi or SoundFont player). The synthesizer processes these events, renders audio samples, and writes them directly to the track buffer.
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Track FX Chain
|
||||
|
||||
Performs isolated signal processing on individual tracks.
|
||||
|
||||
* **UI Layer:** Exposed via a dedicated FX Rack Modal on each track. Users can insert, remove, bypass, and reorder processing slots.
|
||||
* **Engine Architecture:** Supports two parallel plugin formats:
|
||||
* **Custom FX:** Built-in DSP algorithms compiled into the core engine (C++, WebAssembly, or DSP Scripting).
|
||||
* **3rd-Party VST/VST3 Plugins:** External commercial processors (e.g., FabFilter, Waves, iZotope). Communication occurs via the VST SDK or a dedicated process bridge using Shared Memory (SHM).
|
||||
|
||||
|
||||
* **Serial Processing Pipeline:** Track audio buffers pass sequentially from Slot 1 through Slot $n$. Each slot modifies the memory buffer in place before passing it to the subsequent plugin:
|
||||
|
||||
$$Buffer_{\text{out}} = Plugin_n(Plugin_{n-1}(...Plugin_1(Buffer_{\text{in}})...))$$
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Summing & Master FX Chain
|
||||
|
||||
Once tracks complete their individual Track FX processing, their outputs route to the main mixer:
|
||||
|
||||
* **Signal Summing:** Audio buffers from Track 1 through Track $N$ are summed linearly—applying track volume gain scaling and constant-power pan laws—into a unified stereo Master Audio Buffer:
|
||||
|
||||
$$Buffer_{\text{Master}}(t) = \sum_{i=1}^{N} Gain_i \cdot Pan_i(Buffer_i(t))$$
|
||||
|
||||
* **Master FX Chain:** The summed Master Buffer passes through global insert slots. Operates identical to the Track FX Chain, targeting master bus processing (e.g., Master Bus EQ, Multiband Compression, and Brickwall Limiting) to ensure target loudness compliance without digital clipping.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Main Output & Final Routing
|
||||
|
||||
Exiting the Master FX Chain, the engine directs the stereo buffer based on execution mode:
|
||||
|
||||
* **Realtime Preview Mode:**
|
||||
* Audio buffers stream in fixed blocks (e.g., 256/512 samples) to low-latency driver APIs (ASIO, WASAPI, CoreAudio, or WebAudio).
|
||||
* Enables zero-latency parameter updates as users adjust UI controls.
|
||||
|
||||
|
||||
* **Offline Export Mode (Bouncing):**
|
||||
* Bypasses realtime driver clocking to process buffers at maximum CPU speed.
|
||||
* Writes output buffers sequentially into audio encoders using libraries such as `libsndfile` (WAV), `LAME` (MP3), or `libvorbis` (OGG).
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. STATE & LIFECYCLE MANAGEMENT
|
||||
|
||||
* **Thread Safety (Zero-Allocation Audio Thread):** Plugin parameters and structural chain updates originate on the UI Thread. The real-time Audio Thread communicates via lock-free SPSC queues or spinlocks to avoid audio dropouts (glitches) caused by thread contention or heap allocations (`malloc`).
|
||||
* **State Persistence:** Parameters across all Custom FX and VST FX are serialized into JSON or Base64 binary chunks and saved within the main DAW project file.
|
||||
* **Plugin Delay Compensation (PDC):** Lookahead processors and complex plugins introduce execution latency. The engine queries each plugin's latency (`getLatencySamples()`), calculates the cumulative delay along each track path, and delays shorter paths accordingly so all tracks sum in sample-accurate phase alignment.
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# fxrt_probe.py — đo bridge realtime với VST thật (tạm, chẩn đoán crackle).
|
||||
# Chạy: cd repo && python fxrt_probe.py [--secs 20]
|
||||
# - tạo session realtime (bắt stderr bridge ra fxrt_bridge_<tag>.log)
|
||||
# - producer: publish batch 4 block mỗi 21.333ms (timeBeginPeriod(1))
|
||||
# - consumer: drain output, ghi thời điểm từng block tới
|
||||
# - báo cáo: engine drops, bridge perf, output gap
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.core import fx_realtime # noqa: E402
|
||||
|
||||
SR = 48000
|
||||
BLOCK = 256
|
||||
BATCH = 4
|
||||
BATCH_PERIOD = BATCH * BLOCK / SR # 21.333ms
|
||||
BLOCK_PERIOD = BLOCK / SR # 5.333ms
|
||||
|
||||
IZ = r"C:\Program Files\Common Files\VST3\iZotope"
|
||||
CHAIN_OZONE_MASTER = [
|
||||
{"type": "vst3", "path": IZ + r"\Ozone 11 Equalizer.vst3", "active": True},
|
||||
{"type": "vst3", "path": IZ + r"\Ozone 11 Dynamics.vst3", "active": True},
|
||||
{"type": "vst3", "path": IZ + r"\Ozone 11 Maximizer.vst3", "active": True},
|
||||
]
|
||||
CHAIN_OZONE_1 = [
|
||||
{"type": "vst3", "path": IZ + r"\Ozone 11 Maximizer.vst3", "active": True},
|
||||
]
|
||||
CHAIN_RX_DENOISE = [
|
||||
{"type": "vst3", "path": IZ + r"\RX 11 Spectral De-noise.vst3", "active": True},
|
||||
]
|
||||
CHAIN_EMPTY = []
|
||||
|
||||
|
||||
def set_timer_res(ms=1):
|
||||
try:
|
||||
ctypes.windll.winmm.timeBeginPeriod(ms)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run_test(tag, chain, secs):
|
||||
log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"fxrt_bridge_%s.log" % tag)
|
||||
real_popen = fx_realtime.subprocess.Popen
|
||||
|
||||
def capped_popen(cmd, **kw):
|
||||
kw.pop("stderr", None)
|
||||
f = open(log_path, "ab", buffering=0)
|
||||
return real_popen(cmd, stderr=f, **kw)
|
||||
|
||||
fx_realtime.subprocess.Popen = capped_popen
|
||||
try:
|
||||
sess = fx_realtime.start_session(chain, SR)
|
||||
finally:
|
||||
fx_realtime.subprocess.Popen = real_popen
|
||||
|
||||
print("== %s: session=%s chain=%d slot(s)" % (tag, sess.name, len(chain)))
|
||||
|
||||
stop = threading.Event()
|
||||
arr_times = [] # (block_idx, perf_counter) khi consumer đọc ra
|
||||
arr_lock = threading.Lock()
|
||||
|
||||
def producer():
|
||||
# 1 block = 256 sample stereo = 512 float; gửi 4 block liên tiếp
|
||||
# (mô phỏng worklet burst), engine gom 4 → publish 1 batch.
|
||||
t = np.arange(BLOCK) / SR
|
||||
sig = 0.2 * np.sin(2 * np.pi * 220 * t)
|
||||
inter = np.empty(BLOCK * 2, dtype=np.float32)
|
||||
inter[0::2] = sig
|
||||
inter[1::2] = 0.9 * sig
|
||||
data = inter.tobytes()
|
||||
start = time.perf_counter()
|
||||
k = 0
|
||||
while not stop.is_set():
|
||||
for _ in range(BATCH):
|
||||
sess.write_input(data)
|
||||
k += 1
|
||||
nxt = start + k * BATCH_PERIOD
|
||||
d = nxt - time.perf_counter()
|
||||
if d > 0:
|
||||
time.sleep(d)
|
||||
|
||||
def consumer():
|
||||
idx = 0
|
||||
while not stop.is_set():
|
||||
out = sess.read_output()
|
||||
if not out:
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
now = time.perf_counter()
|
||||
with arr_lock:
|
||||
for _ in out:
|
||||
arr_times.append((idx, now))
|
||||
idx += 1
|
||||
|
||||
pt = threading.Thread(target=producer)
|
||||
ct = threading.Thread(target=consumer)
|
||||
t0 = time.perf_counter()
|
||||
pt.start(); ct.start()
|
||||
time.sleep(secs)
|
||||
stop.set()
|
||||
pt.join(timeout=5); ct.join(timeout=5)
|
||||
|
||||
stats = dict(sess.stats)
|
||||
sess.close()
|
||||
time.sleep(0.3)
|
||||
|
||||
with arr_lock:
|
||||
times = list(arr_times)
|
||||
if len(times) > 1:
|
||||
gaps = [b - a for (i, a), (j, b) in zip(times[:-1], times[1:]) if b > a]
|
||||
dur = time.perf_counter() - t0
|
||||
rate = len(times) / dur
|
||||
exp = 1.0 / (BATCH / BLOCK_PERIOD) # batch period
|
||||
print(" engine stats: %s" % stats)
|
||||
print(" output: %d block / %.1fs = %.1f blk/s (expect %.1f)"
|
||||
% (len(times), dur, rate, BLOCK / BLOCK_PERIOD))
|
||||
print(" gap: n=%d avg=%.3fms p95=%.3fms max=%.3fms"
|
||||
% (len(gaps), np.mean(gaps) * 1e3, np.percentile(gaps, 95) * 1e3,
|
||||
max(gaps) * 1e3))
|
||||
over = [g * 1e3 for g in gaps if g > 1.35 * BLOCK_PERIOD]
|
||||
print(" gaps>%.1fms (underrun candidate): %d (%.2f%%)"
|
||||
% (1.35 * BLOCK_PERIOD * 1e3, len(over),
|
||||
100.0 * len(over) / len(gaps) if gaps else 0.0))
|
||||
else:
|
||||
print(" NO OUTPUT (engine stats: %s)" % stats)
|
||||
|
||||
print(" bridge log tail (%s):" % log_path)
|
||||
try:
|
||||
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
for ln in lines[-12:]:
|
||||
print(" " + ln.rstrip())
|
||||
except Exception as e:
|
||||
print(" (no log: %s)" % e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
set_timer_res(1)
|
||||
secs = 20
|
||||
if "--secs" in sys.argv:
|
||||
secs = int(sys.argv[sys.argv.index("--secs") + 1])
|
||||
tests = []
|
||||
if "--test" in sys.argv:
|
||||
t = sys.argv[sys.argv.index("--test") + 1]
|
||||
tests = [(t, {"ozone1": CHAIN_OZONE_1,
|
||||
"ozone3": CHAIN_OZONE_MASTER,
|
||||
"rx": CHAIN_RX_DENOISE,
|
||||
"empty": CHAIN_EMPTY}[t])]
|
||||
else:
|
||||
tests = [("empty", CHAIN_EMPTY),
|
||||
("ozone1", CHAIN_OZONE_1),
|
||||
("ozone3", CHAIN_OZONE_MASTER),
|
||||
("rx", CHAIN_RX_DENOISE)]
|
||||
for tag, chain in tests:
|
||||
try:
|
||||
run_test(tag, chain, secs)
|
||||
except Exception as e:
|
||||
print("== %s FAILED: %r" % (tag, e))
|
||||
import traceback; traceback.print_exc()
|
||||
@@ -0,0 +1,182 @@
|
||||
# fxrt_toggle_probe.py — đo toggle bypass dry->wet với VST instance cache.
|
||||
# Kịch bản THẬT: start với slot ACTIVE (load + cache) -> toggle bypass=true
|
||||
# (chain đổi, KHÔNG unload) -> toggle bypass=false (cache hit, không reload)
|
||||
# Đo: thời gian set_chain -> latency report (chain applied) mỗi toggle.
|
||||
# python fxrt_toggle_probe.py [--port 8012]
|
||||
import asyncio
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
REPO = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, REPO)
|
||||
|
||||
SR = 48000
|
||||
BLOCK = 256
|
||||
BATCH = 4
|
||||
BATCH_PERIOD = BATCH * BLOCK / SR # 21.333ms
|
||||
ADELAY = os.path.join(REPO, "native_bridge", "build", "VST3", "Release", "adelay.vst3")
|
||||
OZONE_MAX = r"C:\Program Files\Common Files\VST3\iZotope\Ozone 11 Maximizer.vst3"
|
||||
PLUGIN = os.environ.get("FXRT_PROBE_PLUGIN", OZONE_MAX) # mặc định Ozone (latency thật 747smp)
|
||||
|
||||
|
||||
def make_block():
|
||||
t = np.arange(BLOCK) / SR
|
||||
amp = float(os.environ.get("FXRT_PROBE_AMP", "1.0"))
|
||||
sig = amp * (0.2 * np.sin(2 * np.pi * 220 * t) + 0.1 * np.sin(2 * np.pi * 331 * t))
|
||||
inter = np.empty(BLOCK * 2, dtype=np.float32)
|
||||
inter[0::2] = sig
|
||||
inter[1::2] = 0.85 * sig
|
||||
return inter.tobytes()
|
||||
|
||||
|
||||
def chain_with(bypass):
|
||||
return [{"type": "vst3", "path": PLUGIN, "name": os.path.basename(PLUGIN),
|
||||
"preset_b64": "", "bypass": bypass}]
|
||||
|
||||
|
||||
async def probe(port):
|
||||
import websockets
|
||||
import urllib.request
|
||||
base = "http://127.0.0.1:%d" % port
|
||||
block_bytes = make_block()
|
||||
|
||||
def post(path, obj):
|
||||
req = urllib.request.Request(
|
||||
base + path, data=json.dumps(obj).encode(),
|
||||
headers={"Content-Type": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
st = post("/api/v1/plugins/fx-realtime/start",
|
||||
{"fx_chain": chain_with(False), "sample_rate": SR})
|
||||
sid = st["session_id"]
|
||||
print("session=%s started slot ACTIVE (load + cache)" % sid)
|
||||
ws_url = "ws://127.0.0.1:%d/api/v1/plugins/ws/fx-realtime/%s" % (port, sid)
|
||||
|
||||
lat_events = [] # (time, seq)
|
||||
stop = asyncio.Event()
|
||||
started = asyncio.Event()
|
||||
|
||||
async def producer():
|
||||
next_t = time.perf_counter() + 0.3
|
||||
while not stop.is_set():
|
||||
await asyncio.to_thread(time.sleep, max(0, next_t - time.perf_counter()))
|
||||
if started.is_set():
|
||||
await ws.send(block_bytes)
|
||||
next_t += BATCH_PERIOD
|
||||
|
||||
async def consumer():
|
||||
while not stop.is_set():
|
||||
try:
|
||||
msg = await ws.recv()
|
||||
except Exception:
|
||||
break
|
||||
if not isinstance(msg, bytes):
|
||||
try:
|
||||
d = json.loads(msg)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(d, dict) and d.get("cmd") == "latency":
|
||||
lat_events.append((time.perf_counter(), d.get("total")))
|
||||
|
||||
async with websockets.connect(ws_url) as ws:
|
||||
tasks = [asyncio.create_task(producer()), asyncio.create_task(consumer())]
|
||||
await asyncio.sleep(0.5)
|
||||
started.set()
|
||||
await asyncio.sleep(1.5) # để start-load + latency report đầu
|
||||
n0 = len(lat_events)
|
||||
print("baseline latency reports:", lat_events[n0 - 3:n0])
|
||||
|
||||
# toggle 1: bypass=true (chain đổi, instance GIỮ trong cache)
|
||||
t0 = time.perf_counter()
|
||||
post("/api/v1/plugins/fx-realtime/set-chain",
|
||||
{"session_id": sid, "fx_chain": chain_with(True)})
|
||||
await asyncio.sleep(0.6)
|
||||
t1 = time.perf_counter()
|
||||
new = [x for x in lat_events if x[0] >= t0]
|
||||
print("TOGGLE1 bypass=true -> chain applied in %s (%.0fms)" %
|
||||
("%.0fms" % ((new[0][0] - t0) * 1e3) if new else "NOT SEEN", 0))
|
||||
|
||||
# toggle 2: bypass=false (CACHE HIT — không reload)
|
||||
t2 = time.perf_counter()
|
||||
post("/api/v1/plugins/fx-realtime/set-chain",
|
||||
{"session_id": sid, "fx_chain": chain_with(False)})
|
||||
await asyncio.sleep(0.6)
|
||||
t3 = time.perf_counter()
|
||||
new2 = [x for x in lat_events if x[0] >= t2]
|
||||
print("TOGGLE2 bypass=false -> chain applied in %s" %
|
||||
("%.0fms" % ((new2[0][0] - t2) * 1e3) if new2 else "NOT SEEN"))
|
||||
|
||||
# toggle 3: bypass=true lần 2 (cache hit)
|
||||
t4 = time.perf_counter()
|
||||
post("/api/v1/plugins/fx-realtime/set-chain",
|
||||
{"session_id": sid, "fx_chain": chain_with(True)})
|
||||
await asyncio.sleep(0.6)
|
||||
new3 = [x for x in lat_events if x[0] >= t4]
|
||||
print("TOGGLE3 bypass=true -> chain applied in %s" %
|
||||
("%.0fms" % ((new3[0][0] - t4) * 1e3) if new3 else "NOT SEEN"))
|
||||
stop.set()
|
||||
await asyncio.sleep(0.3)
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
try:
|
||||
post("/api/v1/plugins/fx-realtime/stop", {"session_id": sid})
|
||||
except Exception:
|
||||
pass
|
||||
print("all latency events:", ["%.2fs/%.0f" % (x[0] - lat_events[0][0], x[1] or 0) for x in lat_events])
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
ctypes.windll.winmm.timeBeginPeriod(1)
|
||||
except Exception:
|
||||
pass
|
||||
port = 8012
|
||||
if "--port" in sys.argv:
|
||||
port = int(sys.argv[sys.argv.index("--port") + 1])
|
||||
storage = tempfile.mkdtemp(prefix="sf_toggleprobe_")
|
||||
env = dict(os.environ)
|
||||
env["SONICFORGE_STORAGE_DIR"] = storage
|
||||
env["FXRT_BRIDGE_LOG"] = os.path.join(REPO, "fxrt_toggle_bridge_%d.log" % port)
|
||||
logf = open(os.path.join(REPO, "fxrt_toggle_engine_%d.log" % port), "ab", buffering=0)
|
||||
boot = ("import ctypes; ctypes.windll.winmm.timeBeginPeriod(1); "
|
||||
"import uvicorn, app.main; "
|
||||
"uvicorn.run(app.main.app, host='127.0.0.1', port=%d, log_level='warning')" % port)
|
||||
engine = subprocess.Popen(
|
||||
[sys.executable, "-c", boot],
|
||||
cwd=REPO, env=env, stdout=logf, stderr=subprocess.STDOUT,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
try:
|
||||
import urllib.request
|
||||
deadline = time.time() + 60
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen("http://127.0.0.1:%d/health" % port, timeout=2) as r:
|
||||
if r.status == 200:
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
print("engine not up; log tail:")
|
||||
print(open(logf.name, "rb").read().decode(errors="replace")[-2000:])
|
||||
return
|
||||
print("engine up on :%d" % port)
|
||||
asyncio.run(probe(port))
|
||||
finally:
|
||||
engine.terminate()
|
||||
try:
|
||||
engine.wait(timeout=5)
|
||||
except Exception:
|
||||
engine.kill()
|
||||
logf.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,432 @@
|
||||
# fxrt_ws_probe.py — đo toàn pipeline THẬT (engine uvicorn + WS + bridge) với
|
||||
# stall model giống main-thread browser. Tạm, chẩn đoán crackle master FX.
|
||||
# python fxrt_ws_probe.py [--secs 20] [--chain empty|adelay|ozone1|ozone3]
|
||||
# [--stalls heavy|light|none]
|
||||
# Kết quả: số block output tới client, gap phân bố, và MÔ PHỎNG CHÍNH XÁC
|
||||
# worklet outQueue (FILL/HOLD/CAP) → số underrun/silence (crackle thật) +
|
||||
# peak block cuối trước mỗi silence (fade-out engine → ~0, hết click).
|
||||
import asyncio
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
REPO = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, REPO)
|
||||
|
||||
SR = 48000
|
||||
BLOCK = 256
|
||||
BATCH = 4
|
||||
BATCH_PERIOD = BATCH * BLOCK / SR # 21.333ms
|
||||
BLOCK_PERIOD = BLOCK / SR # 5.333ms
|
||||
FILL = 48
|
||||
CAP = 64
|
||||
HOLD = 2 # block lặp khi underrun (~10.7ms)
|
||||
|
||||
IZ = r"C:\Program Files\Common Files\VST3\iZotope"
|
||||
ADELAY = os.path.join(REPO, "native_bridge", "build", "VST3", "Release", "adelay.vst3")
|
||||
CHAINS = {
|
||||
"empty": [],
|
||||
"adelay": [{"type": "vst3", "path": ADELAY, "active": True}],
|
||||
"ozone1": [{"type": "vst3", "path": IZ + r"\Ozone 11 Maximizer.vst3", "active": True}],
|
||||
"ozone3": [
|
||||
{"type": "vst3", "path": IZ + r"\Ozone 11 Equalizer.vst3", "active": True},
|
||||
{"type": "vst3", "path": IZ + r"\Ozone 11 Dynamics.vst3", "active": True},
|
||||
{"type": "vst3", "path": IZ + r"\Ozone 11 Maximizer.vst3", "active": True},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def stall_model(name):
|
||||
# (xác suất stall mỗi giây, min_ms, max_ms) — mô phỏng main-thread busy
|
||||
# (GUI decode, React render, mở mastering panel, rAF).
|
||||
return {
|
||||
"none": (0.0, 0, 0),
|
||||
"light": (0.5, 60, 120),
|
||||
"heavy": (1.5, 120, 280),
|
||||
}[name]
|
||||
|
||||
|
||||
class WorkletSim:
|
||||
"""Mô phỏng chính xác SfFxRealtimeProcessor outQueue từ arrival times.
|
||||
|
||||
mode='cut' — hiện tại: underrun → hold HOLD block rồi CẮT cứng silence
|
||||
(click nếu nội dung khác 0).
|
||||
mode='holdfade' — đề xuất (fix worklet): underrun → giữ block cuối + fade
|
||||
gain 1→0 qua FADE_HOLD block (~21ms) rồi silence ~0
|
||||
(hết click); giữa hold, block mới tới → resume ngay.
|
||||
"""
|
||||
|
||||
FADE_HOLD = 4 # block fade khi underrun (proposal)
|
||||
|
||||
def __init__(self, mode="cut"):
|
||||
self.mode = mode
|
||||
self.arrivals = [] # (block_seq, time, peak)
|
||||
self.underruns = 0
|
||||
self.silences = 0
|
||||
self.silent_ms = 0.0
|
||||
self.hold_events = 0
|
||||
self.hold_fades = 0
|
||||
self.silence_peaks = [] # peak block cuối trước mỗi silence (fade -> ~0)
|
||||
|
||||
def run(self, duration):
|
||||
queue = []
|
||||
play = None # thời điểm consume block kế
|
||||
started = False
|
||||
idx = 0
|
||||
last_peak = 1.0
|
||||
hold_fade = 0 # còn N block fade đang phát (mode holdfade)
|
||||
for item in self.arrivals:
|
||||
bi, t = item[0], item[1]
|
||||
peak = item[2] if len(item) > 2 else 1.0
|
||||
queue.append((bi, peak))
|
||||
if len(queue) > CAP:
|
||||
queue.pop(0)
|
||||
if play is None:
|
||||
if len(queue) >= FILL:
|
||||
started = True
|
||||
play = t + BLOCK_PERIOD
|
||||
last_peak = queue.pop(0)[1]
|
||||
continue
|
||||
# consume các block đến hạn trước arrival này
|
||||
while play is not None and play <= t:
|
||||
if queue:
|
||||
if hold_fade > 0 and self.mode == "holdfade":
|
||||
# có block mới giữa hold-fade → resume ngay (không đợi
|
||||
# hết fade, không cắt): fade chỉ còn là chỗi ~0.
|
||||
hold_fade = 0
|
||||
last_peak = queue.pop(0)[1]
|
||||
play += BLOCK_PERIOD
|
||||
else:
|
||||
# underrun tại play
|
||||
self.underruns += 1
|
||||
if self.mode == "holdfade":
|
||||
# giữ block cuối + fade 1→0 qua FADE_HOLD block
|
||||
hold_fade = self.FADE_HOLD
|
||||
self.hold_fades += 1
|
||||
while play <= t and hold_fade > 0:
|
||||
last_peak *= (hold_fade - 1) / max(self.FADE_HOLD - 1, 1)
|
||||
hold_fade -= 1
|
||||
play += BLOCK_PERIOD
|
||||
if hold_fade == 0 and play <= t:
|
||||
# đã fade về ~0 → silence êm (không click)
|
||||
self.silences += 1
|
||||
self.silent_ms += (t - play) * 1e3
|
||||
self.silence_peaks.append(last_peak)
|
||||
play = None
|
||||
idx += 1
|
||||
else:
|
||||
hold_end = play + HOLD * BLOCK_PERIOD
|
||||
if t >= hold_end:
|
||||
# silence từ hold_end tới khi có block mới (t)
|
||||
self.silences += 1
|
||||
self.silent_ms += (t - hold_end) * 1e3
|
||||
self.silence_peaks.append(last_peak)
|
||||
else:
|
||||
self.hold_events += 1
|
||||
play = None
|
||||
idx += 1
|
||||
if play is None and queue:
|
||||
play = t + BLOCK_PERIOD
|
||||
last_peak = queue.pop(0)[1]
|
||||
return self
|
||||
|
||||
def report(self):
|
||||
r = ("worklet sim[%s]: underruns=%d hold_only=%d hold_fades=%d silences=%d silent_ms=%.1f"
|
||||
% (self.mode, self.underruns, self.hold_events, self.hold_fades,
|
||||
self.silences, self.silent_ms))
|
||||
if self.silence_peaks:
|
||||
arr = np.array(self.silence_peaks)
|
||||
hard = int((arr > 0.05).sum())
|
||||
r += (" | peak@silence min=%.3f med=%.3f max=%.3f hard(>0.05)=%d"
|
||||
% (arr.min(), np.median(arr), arr.max(), hard))
|
||||
return r
|
||||
|
||||
|
||||
def make_block():
|
||||
t = np.arange(BLOCK) / SR
|
||||
amp = float(os.environ.get("FXRT_PROBE_AMP", "1.0"))
|
||||
sig = amp * (0.2 * np.sin(2 * np.pi * 220 * t) + 0.1 * np.sin(2 * np.pi * 331 * t))
|
||||
inter = np.empty(BLOCK * 2, dtype=np.float32)
|
||||
inter[0::2] = sig
|
||||
inter[1::2] = 0.85 * sig
|
||||
return inter.tobytes()
|
||||
|
||||
|
||||
async def run_ws_probe(port, chain, secs, stall):
|
||||
import websockets
|
||||
url = "http://127.0.0.1:%d" % port
|
||||
import urllib.request
|
||||
req = urllib.request.Request(
|
||||
url + "/api/v1/plugins/fx-realtime/start",
|
||||
data=json.dumps({"fx_chain": chain, "sample_rate": SR}).encode(),
|
||||
headers={"Content-Type": "application/json"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
st = json.loads(r.read().decode())
|
||||
except Exception as e:
|
||||
print(" start session FAILED:", e)
|
||||
return None
|
||||
sid = st["session_id"]
|
||||
print(" session=%s chain=%d slot(s)" % (sid, len(chain)))
|
||||
|
||||
ws_url = "ws://127.0.0.1:%d/api/v1/plugins/ws/fx-realtime/%s" % (port, sid)
|
||||
s_prob, s_min, s_max = stall_model(stall)
|
||||
sent_bursts = 0
|
||||
recv_times = [] # (block_seq, time, peak)
|
||||
out_peaks = [] # output peak per block (cause-5 clipping track)
|
||||
out_flat = [] # flat-top samples per block (|x|>0.9999)
|
||||
recv_lock = asyncio.Lock()
|
||||
stop = asyncio.Event()
|
||||
produced = asyncio.Queue() # burst buffers
|
||||
backlog = []
|
||||
sent_times = []
|
||||
send_time_stats = []
|
||||
produce_count = 0
|
||||
pump_iter_stats = []
|
||||
|
||||
block_bytes = make_block()
|
||||
|
||||
# sanity: đo resolution asyncio.sleep ngay trong loop này
|
||||
_t0 = time.perf_counter()
|
||||
for _ in range(50):
|
||||
await asyncio.sleep(0.001)
|
||||
print(" [sanity] 50x sleep(0.001)=%.1fms -> %.2fms/iter"
|
||||
% ((time.perf_counter() - _t0) * 1e3, (time.perf_counter() - _t0) / 50 * 1e3))
|
||||
|
||||
async def producer():
|
||||
# worklet: sản xuất burst mỗi 21.333ms KHÔNG ngừng dù main thread busy
|
||||
next_t = time.perf_counter() + 0.2
|
||||
n = 0
|
||||
while not stop.is_set():
|
||||
# to_thread: asyncio.sleep bi cap 15.6ms (GQCS Win11) -> pacing 21.3ms
|
||||
# thành ~31ms -> probe không đại diện. time.sleep giữ precision.
|
||||
await asyncio.to_thread(time.sleep, max(0, next_t - time.perf_counter()))
|
||||
await produced.put(block_bytes)
|
||||
n += 1
|
||||
next_t += BATCH_PERIOD
|
||||
nonlocal produce_count
|
||||
produce_count = n
|
||||
|
||||
async def stall_scheduler():
|
||||
# main-thread busy: set stall_until — pump VÀ consumer đều đọc (cùng
|
||||
# main thread bị block → cả 2 hướng đều đứng; app thật đúng vậy).
|
||||
while not stop.is_set():
|
||||
if s_prob:
|
||||
await asyncio.sleep(random.expovariate(s_prob))
|
||||
dur = random.uniform(s_min, s_max) / 1000.0
|
||||
stall_state["until"] = time.perf_counter() + dur
|
||||
else:
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def pump():
|
||||
# fxRtInPump JS: tối đa 1 burst/21ms; stall → không gửi
|
||||
nonlocal backlog
|
||||
last_sent = 0.0
|
||||
send_times = []
|
||||
iter_times = []
|
||||
while not stop.is_set():
|
||||
now = time.perf_counter()
|
||||
iter_times.append(now)
|
||||
if now < stall_state["until"]:
|
||||
await asyncio.to_thread(time.sleep, 0.002)
|
||||
continue
|
||||
while not produced.empty():
|
||||
backlog.append(await produced.get())
|
||||
if backlog and now - last_sent >= 0.021:
|
||||
burst = backlog.pop(0)
|
||||
sent_times.append(now)
|
||||
t0 = time.perf_counter()
|
||||
for i in range(BATCH):
|
||||
await ws.send(burst)
|
||||
send_times.append(time.perf_counter() - t0)
|
||||
last_sent = now
|
||||
await asyncio.to_thread(time.sleep, 0.001)
|
||||
nonlocal send_time_stats, pump_iter_stats
|
||||
send_time_stats = send_times
|
||||
pump_iter_stats = iter_times
|
||||
|
||||
stall_state = {"until": 0.0}
|
||||
|
||||
async def param_burst():
|
||||
# Mô phỏng fxRtPushAllParams (app gửi loạt set_param ngay sau WS open):
|
||||
# numeric ParamID 0..3 → VST3 setParamNormalized THẬT trên bridge.
|
||||
# Fix: setParam defer worker → audio loop không block → send interval ổn.
|
||||
if not os.environ.get("FXRT_PROBE_PARAMS"):
|
||||
return
|
||||
await asyncio.sleep(0.8)
|
||||
nslots = len(chain)
|
||||
ids = [s.strip() for s in os.environ.get("FXRT_PROBE_PARAM_IDS", "0,1,2,3").split(",")]
|
||||
for rep in range(4):
|
||||
for slot in range(nslots):
|
||||
for i, pid in enumerate(ids):
|
||||
await ws.send(json.dumps({"cmd": "set_param", "slot": slot,
|
||||
"key": pid, "value": 0.3 + 0.1 * i}))
|
||||
await asyncio.sleep(0.4)
|
||||
|
||||
async def consumer():
|
||||
while not stop.is_set():
|
||||
if os.environ.get("FXRT_PROBE_CONSUMER_STALL") and \
|
||||
time.perf_counter() < stall_state["until"]:
|
||||
# main-thread busy: WS message nằm trong browser queue, main
|
||||
# không decode/postMessage → worklet không nhận gì.
|
||||
await asyncio.to_thread(time.sleep, 0.002)
|
||||
continue
|
||||
try:
|
||||
msg = await ws.recv()
|
||||
except Exception:
|
||||
break
|
||||
if isinstance(msg, bytes) and len(msg) == BLOCK * 2 * 4:
|
||||
now = time.perf_counter()
|
||||
f = np.frombuffer(msg, dtype=np.float32)
|
||||
pk = float(f[::2].max())
|
||||
flat = int(np.count_nonzero(np.abs(f) > 0.9999))
|
||||
async with recv_lock:
|
||||
recv_times.append((len(recv_times), now, pk))
|
||||
out_peaks.append(pk)
|
||||
out_flat.append(flat)
|
||||
# text frame (latency) bỏ qua
|
||||
|
||||
async with websockets.connect(ws_url) as ws:
|
||||
t0 = time.perf_counter()
|
||||
tasks = [asyncio.create_task(producer()), asyncio.create_task(pump()),
|
||||
asyncio.create_task(stall_scheduler()), asyncio.create_task(param_burst()),
|
||||
asyncio.create_task(consumer())]
|
||||
if os.environ.get("FXRT_PROBE_NO_CONSUMER"):
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
tasks = [asyncio.create_task(producer()), asyncio.create_task(pump()),
|
||||
asyncio.create_task(stall_scheduler())]
|
||||
await asyncio.sleep(secs)
|
||||
stop.set()
|
||||
await asyncio.sleep(0.5)
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
dur = time.perf_counter() - t0
|
||||
|
||||
async with recv_lock:
|
||||
times = list(recv_times)
|
||||
# stop session
|
||||
try:
|
||||
req2 = urllib.request.Request(
|
||||
url + "/api/v1/plugins/fx-realtime/stop",
|
||||
data=json.dumps({"session_id": sid}).encode(),
|
||||
headers={"Content-Type": "application/json"}, method="POST")
|
||||
urllib.request.urlopen(req2, timeout=10)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
n_sent = len(sent_times) * BATCH
|
||||
n_recv = len(times)
|
||||
print(" producer bursts=%d pump sends=%d (backlog at end=%d)"
|
||||
% (produce_count, len(sent_times), len(backlog)))
|
||||
if pump_iter_stats:
|
||||
it = np.diff(np.array(pump_iter_stats)) * 1e3
|
||||
it = it[it > 0]
|
||||
print(" pump iter: avg=%.2fms p50=%.2f p95=%.2f max=%.2fms"
|
||||
% (it.mean(), np.percentile(it, 50), np.percentile(it, 95), it.max()))
|
||||
if len(sent_times) > 3:
|
||||
si = np.diff(np.array(sent_times)) * 1e3
|
||||
si = si[si > 0]
|
||||
print(" send interval: avg=%.2fms p50=%.2f p95=%.2f max=%.2fms"
|
||||
% (si.mean(), np.percentile(si, 50), np.percentile(si, 95), si.max()))
|
||||
if send_time_stats:
|
||||
st = np.array(send_time_stats) * 1e3
|
||||
print(" ws send burst: avg=%.2fms p50=%.2f p95=%.2f max=%.2fms"
|
||||
% (st.mean(), np.percentile(st, 50), np.percentile(st, 95), st.max()))
|
||||
rate = n_recv / dur
|
||||
print(" sent=%d bursts (%d blocks) recv=%d blocks over %.1fs = %.1f blk/s (expect 187.5)"
|
||||
% (len(sent_times), n_sent, n_recv, dur, rate))
|
||||
if out_peaks:
|
||||
op = np.array(out_peaks)
|
||||
of = np.array(out_flat)
|
||||
print(" OUT peak: min=%.4f p50=%.4f p95=%.4f max=%.4f" %
|
||||
(op.min(), np.percentile(op, 50), np.percentile(op, 95), op.max()))
|
||||
print(" OUT flat-top(|x|>0.9999): blocks=%d samples=%d (%.4f%%)" %
|
||||
(int(np.count_nonzero(of > 0)), int(of.sum()),
|
||||
100.0 * of.sum() / max(1, len(of) * BLOCK * 2)))
|
||||
print(" lost blocks (sent-recv): %d" % max(0, n_sent - n_recv))
|
||||
if len(times) > 1:
|
||||
gaps = [b - a for (i, a, _), (j, b, _) in zip(times[:-1], times[1:])]
|
||||
gaps = [g for g in gaps if g > 0]
|
||||
g = np.array(gaps) * 1e3
|
||||
print(" gap: avg=%.2fms p50=%.2f p95=%.2f p99=%.2f max=%.2fms"
|
||||
% (g.mean(), np.percentile(g, 50), np.percentile(g, 95),
|
||||
np.percentile(g, 99), g.max()))
|
||||
over = g[g > 1.35 * BLOCK_PERIOD * 1e3]
|
||||
print(" gaps>%.1fms: %d (%.2f%%)" % (1.35 * BLOCK_PERIOD * 1e3, len(over),
|
||||
100.0 * len(over) / len(g) if len(g) else 0))
|
||||
sim = WorkletSim(mode=os.environ.get("FXRT_PROBE_WORKLET", "cut"))
|
||||
sim.arrivals = times
|
||||
sim.run(dur)
|
||||
print(" " + sim.report())
|
||||
|
||||
|
||||
def set_timer_res(ms=1):
|
||||
try:
|
||||
ctypes.windll.winmm.timeBeginPeriod(ms)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
set_timer_res(1)
|
||||
secs = 20
|
||||
chain = "ozone1"
|
||||
stall = "heavy"
|
||||
port = 8011
|
||||
if "--secs" in sys.argv:
|
||||
secs = int(sys.argv[sys.argv.index("--secs") + 1])
|
||||
if "--chain" in sys.argv:
|
||||
chain = sys.argv[sys.argv.index("--chain") + 1]
|
||||
if "--stalls" in sys.argv:
|
||||
stall = sys.argv[sys.argv.index("--stalls") + 1]
|
||||
if "--port" in sys.argv:
|
||||
port = int(sys.argv[sys.argv.index("--port") + 1])
|
||||
|
||||
# engine process
|
||||
import app.main # noqa: F401 — kiểm tra import được
|
||||
storage = tempfile.mkdtemp(prefix="sf_wsprobe_")
|
||||
env = dict(os.environ)
|
||||
env["SONICFORGE_STORAGE_DIR"] = storage
|
||||
logf = open(os.path.join(REPO, "fxrt_engine_%d.log" % port), "ab", buffering=0)
|
||||
boot = ("import ctypes; ctypes.windll.winmm.timeBeginPeriod(1); "
|
||||
"import uvicorn, app.main; "
|
||||
"uvicorn.run(app.main.app, host='127.0.0.1', port=%d, log_level='warning')" % port)
|
||||
engine = subprocess.Popen(
|
||||
[sys.executable, "-c", boot],
|
||||
cwd=REPO, env=env, stdout=logf, stderr=subprocess.STDOUT,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
try:
|
||||
import urllib.request
|
||||
deadline = time.time() + 60
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen("http://127.0.0.1:%d/health" % port, timeout=2) as r:
|
||||
if r.status == 200:
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
print("engine not up; log tail:")
|
||||
print(open(logf.name, "rb").read().decode(errors="replace")[-2000:])
|
||||
return
|
||||
print("engine up on :%d" % port)
|
||||
asyncio.run(run_ws_probe(port, CHAINS[chain], secs, stall))
|
||||
finally:
|
||||
engine.terminate()
|
||||
try:
|
||||
engine.wait(timeout=5)
|
||||
except Exception:
|
||||
engine.kill()
|
||||
logf.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Diagnostic harness: spawn realtime + GUI bridge, feed audio, poll /preset.
|
||||
Checks: (1) blob drift over time (would trigger chainKey change -> set_chain
|
||||
-> crackle), (2) /preset latency (HTTP worker block -> dead clicks)."""
|
||||
import ctypes, json, os, subprocess, tempfile, threading, time, uuid, urllib.request, sys
|
||||
import numpy as np
|
||||
from multiprocessing import shared_memory
|
||||
|
||||
FXRT_MAGIC = 0x46585254
|
||||
FXRT_BLOCK = 256
|
||||
FXRT_IN_SLOTS = 4
|
||||
FXRT_OUT_SLOTS = 8
|
||||
FXRT_CTRL_SLOTS = 8
|
||||
FXRT_LAT_SLOTS = 8
|
||||
FXRT_STATE_STARTING = 0
|
||||
FXRT_STATE_READY = 1
|
||||
|
||||
class FxRTHeader(ctypes.Structure):
|
||||
_fields_ = [("magic", ctypes.c_uint32), ("state", ctypes.c_uint32),
|
||||
("sample_rate", ctypes.c_uint32), ("block_size", ctypes.c_uint32),
|
||||
("running", ctypes.c_uint32), ("heartbeat", ctypes.c_uint32),
|
||||
("in_write", ctypes.c_uint32), ("in_read", ctypes.c_uint32),
|
||||
("in_slots", ctypes.c_uint32), ("out_write", ctypes.c_uint32),
|
||||
("out_read", ctypes.c_uint32), ("out_slots", ctypes.c_uint32),
|
||||
("ctrl_write", ctypes.c_uint32), ("ctrl_read", ctypes.c_uint32),
|
||||
("ctrl_slots", ctypes.c_uint32), ("lat_write", ctypes.c_uint32),
|
||||
("lat_read", ctypes.c_uint32), ("lat_slots", ctypes.c_uint32)]
|
||||
|
||||
HEADER_SIZE = ctypes.sizeof(FxRTHeader)
|
||||
IN_L_OFF = HEADER_SIZE
|
||||
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
TOTAL_SIZE = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
|
||||
EXE = r"C:\Users\locpham\SonicForgeStudio\src-tauri\binaries\fx_vst_bridge.exe"
|
||||
PLUGIN = sys.argv[1] if len(sys.argv) > 1 else r"C:\Program Files\Common Files\VST3\WaveObserver.vst3"
|
||||
DURATION = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
|
||||
|
||||
def http_get(url, timeout=10):
|
||||
t0 = time.time()
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as r:
|
||||
body = r.read().decode()
|
||||
return time.time() - t0, body
|
||||
except Exception as e:
|
||||
return time.time() - t0, "ERR:" + str(e)
|
||||
|
||||
def main():
|
||||
name = "SonicForge_FXRT_" + uuid.uuid4().hex[:12]
|
||||
print("plugin:", PLUGIN, "shm:", name, flush=True)
|
||||
shm = shared_memory.SharedMemory(name=name, create=True, size=TOTAL_SIZE)
|
||||
shm.buf[:TOTAL_SIZE] = b"\x00" * TOTAL_SIZE
|
||||
h = FxRTHeader.from_buffer(shm.buf)
|
||||
h.magic = FXRT_MAGIC; h.state = FXRT_STATE_STARTING
|
||||
h.sample_rate = 44100; h.block_size = FXRT_BLOCK; h.running = 1
|
||||
h.in_slots = FXRT_IN_SLOTS; h.out_slots = FXRT_OUT_SLOTS
|
||||
h.ctrl_slots = FXRT_CTRL_SLOTS; h.lat_slots = FXRT_LAT_SLOTS
|
||||
npbuf = np.frombuffer(shm.buf, dtype=np.float32)
|
||||
stop = threading.Event()
|
||||
procs = []
|
||||
try:
|
||||
job1 = {"sample_rate": 44100, "block_size": 256, "seq": 0,
|
||||
"fx_chain": [{"type": "vst3", "path": PLUGIN, "name": "probe", "preset_b64": "", "bypass": False}]}
|
||||
fd, jp1 = tempfile.mkstemp(suffix=".json", prefix="fxrt_probe_")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(job1, f)
|
||||
p1 = subprocess.Popen([EXE, "--realtime-fx", jp1, "--shm", name, "--parent", str(os.getpid())],
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
procs.append(p1)
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < 30 and h.state != FXRT_STATE_READY:
|
||||
time.sleep(0.05)
|
||||
if p1.poll() is not None:
|
||||
print("REALTIME BRIDGE EXITED rc=", p1.returncode, flush=True); return
|
||||
print("realtime state:", h.state, "took %.2fs" % (time.time() - t0), flush=True)
|
||||
if h.state != FXRT_STATE_READY:
|
||||
print("realtime not ready — aborting", flush=True); return
|
||||
|
||||
job2 = {"path": PLUGIN, "name": "probe", "shm": name, "preset_b64": ""}
|
||||
fd, jp2 = tempfile.mkstemp(suffix=".json", prefix="fxgui_probe_")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(job2, f)
|
||||
p2 = subprocess.Popen([EXE, "--fx-gui", jp2],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
procs.append(p2)
|
||||
port = None
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < 20:
|
||||
line = p2.stdout.readline()
|
||||
if not line:
|
||||
if p2.poll() is not None: break
|
||||
continue
|
||||
line = line.strip()
|
||||
if line.startswith("SF_FXGUI_PORT="):
|
||||
port = int(line.split("=", 1)[1]); break
|
||||
print("[gui]", line, flush=True)
|
||||
if not port:
|
||||
print("GUI bridge: no port — rc=", p2.poll(), flush=True); return
|
||||
url = "http://127.0.0.1:%d" % port
|
||||
print("gui port:", port, flush=True)
|
||||
|
||||
def feed():
|
||||
phase = 0.0
|
||||
while not stop.is_set():
|
||||
if h.in_write - h.in_read < FXRT_IN_SLOTS:
|
||||
slot = h.in_write & (FXRT_IN_SLOTS - 1)
|
||||
off = IN_L_OFF + slot * FXRT_BLOCK
|
||||
t = np.arange(FXRT_BLOCK) / 44100.0 + phase
|
||||
sine = 0.1 * np.sin(2 * np.pi * 440 * t).astype(np.float32)
|
||||
npbuf[off:off + FXRT_BLOCK] = sine
|
||||
npbuf[off + FXRT_IN_SLOTS * FXRT_BLOCK:off + FXRT_IN_SLOTS * FXRT_BLOCK + FXRT_BLOCK] = sine
|
||||
h.in_write += 1
|
||||
phase += FXRT_BLOCK / 44100.0
|
||||
time.sleep(0.003)
|
||||
th = threading.Thread(target=feed, daemon=True)
|
||||
th.start()
|
||||
|
||||
# Poll /preset every 1.5s — same as frontend — measure drift + latency.
|
||||
last = None
|
||||
last16 = None
|
||||
drift_changes = 0
|
||||
t0 = time.time()
|
||||
n = 0
|
||||
while time.time() - t0 < DURATION:
|
||||
time.sleep(1.5)
|
||||
lat, body = http_get(url + "/preset")
|
||||
n += 1
|
||||
import json as _j
|
||||
try:
|
||||
d = _j.loads(body)
|
||||
b64 = d.get("preset_b64", "")
|
||||
except Exception:
|
||||
print(" poll %d: bad json (%s)" % (n, body[:80]), flush=True)
|
||||
continue
|
||||
b16 = b64[:16]
|
||||
first_diff = (last16 is not None and b16 != last16)
|
||||
full_diff = (last is not None and b64 != last)
|
||||
if first_diff: drift_changes += 1
|
||||
print(" poll %d lat=%.0fms blob_len=%d first16=%s %s%s%s" % (
|
||||
n, lat * 1000, len(b64), b16,
|
||||
"FIRST16_CHANGED" if first_diff else "",
|
||||
" full_changed" if (full_diff and not first_diff) else "",
|
||||
" (NEW)" if last is None else ""), flush=True)
|
||||
last = b64; last16 = b16
|
||||
print("RESULT: %d polls, %d first16 changes (would trigger set_chain -> crackle)" % (n, drift_changes), flush=True)
|
||||
finally:
|
||||
stop.set()
|
||||
for p in procs:
|
||||
try: p.terminate()
|
||||
except Exception: pass
|
||||
time.sleep(0.5)
|
||||
for p in procs:
|
||||
try: p.kill()
|
||||
except Exception: pass
|
||||
shm.close()
|
||||
try: shm.unlink()
|
||||
except Exception: pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure out-ring glitches while GUI bridge runs with /frame + /preset load.
|
||||
Feeds sine into in ring (realtime pace), reads out ring continuously, detects
|
||||
discontinuities (sample jumps) in the output signal."""
|
||||
import ctypes, json, os, subprocess, tempfile, threading, time, uuid, urllib.request, sys
|
||||
import numpy as np
|
||||
from multiprocessing import shared_memory
|
||||
|
||||
FXRT_MAGIC = 0x46585254
|
||||
FXRT_BLOCK = 256
|
||||
FXRT_IN_SLOTS = 4
|
||||
FXRT_OUT_SLOTS = 8
|
||||
FXRT_CTRL_SLOTS = 8
|
||||
FXRT_LAT_SLOTS = 8
|
||||
FXRT_STATE_STARTING = 0
|
||||
FXRT_STATE_READY = 1
|
||||
|
||||
class FxRTHeader(ctypes.Structure):
|
||||
_fields_ = [("magic", ctypes.c_uint32), ("state", ctypes.c_uint32),
|
||||
("sample_rate", ctypes.c_uint32), ("block_size", ctypes.c_uint32),
|
||||
("running", ctypes.c_uint32), ("heartbeat", ctypes.c_uint32),
|
||||
("in_write", ctypes.c_uint32), ("in_read", ctypes.c_uint32),
|
||||
("in_slots", ctypes.c_uint32), ("out_write", ctypes.c_uint32),
|
||||
("out_read", ctypes.c_uint32), ("out_slots", ctypes.c_uint32),
|
||||
("ctrl_write", ctypes.c_uint32), ("ctrl_read", ctypes.c_uint32),
|
||||
("ctrl_slots", ctypes.c_uint32), ("lat_write", ctypes.c_uint32),
|
||||
("lat_read", ctypes.c_uint32), ("lat_slots", ctypes.c_uint32)]
|
||||
|
||||
HEADER_SIZE = ctypes.sizeof(FxRTHeader)
|
||||
IN_L_OFF = HEADER_SIZE
|
||||
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
TOTAL_SIZE = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
|
||||
EXE = r"C:\Users\locpham\SonicForgeStudio\src-tauri\binaries\fx_vst_bridge.exe"
|
||||
PLUGIN = sys.argv[1] if len(sys.argv) > 1 else r"C:\Program Files\Common Files\VST3\WaveObserver.vst3"
|
||||
DURATION = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
|
||||
FRAME = sys.argv[3] if len(sys.argv) > 3 else "on" # "on" = poll /frame 30fps
|
||||
|
||||
def http_get(url, timeout=5):
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as r:
|
||||
return r.read()
|
||||
except Exception as e:
|
||||
return b"ERR:" + str(e).encode()
|
||||
|
||||
def main():
|
||||
name = "SonicForge_FXRT_" + uuid.uuid4().hex[:12]
|
||||
print("plugin:", PLUGIN, "shm:", name, "frame:", FRAME, flush=True)
|
||||
shm = shared_memory.SharedMemory(name=name, create=True, size=TOTAL_SIZE)
|
||||
shm.buf[:TOTAL_SIZE] = b"\x00" * TOTAL_SIZE
|
||||
h = FxRTHeader.from_buffer(shm.buf)
|
||||
h.magic = FXRT_MAGIC; h.state = FXRT_STATE_STARTING
|
||||
h.sample_rate = 44100; h.block_size = FXRT_BLOCK; h.running = 1
|
||||
h.in_slots = FXRT_IN_SLOTS; h.out_slots = FXRT_OUT_SLOTS
|
||||
h.ctrl_slots = FXRT_CTRL_SLOTS; h.lat_slots = FXRT_LAT_SLOTS
|
||||
npbuf = np.frombuffer(shm.buf, dtype=np.float32)
|
||||
stop = threading.Event()
|
||||
procs = []
|
||||
glitches = []
|
||||
out_total = [0]
|
||||
try:
|
||||
job1 = {"sample_rate": 44100, "block_size": 256, "seq": 0,
|
||||
"fx_chain": [{"type": "vst3", "path": PLUGIN, "name": "probe", "preset_b64": "", "bypass": False}]}
|
||||
fd, jp1 = tempfile.mkstemp(suffix=".json", prefix="fxrt_probe_")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(job1, f)
|
||||
p1 = subprocess.Popen([EXE, "--realtime-fx", jp1, "--shm", name, "--parent", str(os.getpid())],
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
procs.append(p1)
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < 30 and h.state != FXRT_STATE_READY:
|
||||
time.sleep(0.05)
|
||||
if p1.poll() is not None:
|
||||
print("REALTIME BRIDGE EXITED rc=", p1.returncode, flush=True); return
|
||||
if h.state != FXRT_STATE_READY:
|
||||
print("realtime not ready — aborting", flush=True); return
|
||||
print("realtime ready in %.2fs" % (time.time() - t0), flush=True)
|
||||
|
||||
job2 = {"path": PLUGIN, "name": "probe", "shm": name, "preset_b64": ""}
|
||||
fd, jp2 = tempfile.mkstemp(suffix=".json", prefix="fxgui_probe_")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(job2, f)
|
||||
p2 = subprocess.Popen([EXE, "--fx-gui", jp2],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
procs.append(p2)
|
||||
port = None
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < 20:
|
||||
line = p2.stdout.readline()
|
||||
if not line:
|
||||
if p2.poll() is not None: break
|
||||
continue
|
||||
line = line.strip()
|
||||
if line.startswith("SF_FXGUI_PORT="):
|
||||
port = int(line.split("=", 1)[1]); break
|
||||
if not port:
|
||||
print("GUI bridge: no port — rc=", p2.poll(), flush=True); return
|
||||
url = "http://127.0.0.1:%d" % port
|
||||
print("gui port:", port, flush=True)
|
||||
|
||||
# Feed sine at realtime pace; read out ring continuously; detect glitches.
|
||||
phase = [0.0]
|
||||
def feed():
|
||||
while not stop.is_set():
|
||||
if h.in_write - h.in_read < FXRT_IN_SLOTS:
|
||||
slot = h.in_write & (FXRT_IN_SLOTS - 1)
|
||||
off = IN_L_OFF + slot * FXRT_BLOCK
|
||||
t = np.arange(FXRT_BLOCK) / 44100.0 + phase[0]
|
||||
sine = 0.1 * np.sin(2 * np.pi * 440 * t).astype(np.float32)
|
||||
npbuf[off // 4:(off // 4) + FXRT_BLOCK] = sine
|
||||
npbuf[(off + FXRT_IN_SLOTS * FXRT_BLOCK) // 4:(off + FXRT_IN_SLOTS * FXRT_BLOCK) // 4 + FXRT_BLOCK] = sine
|
||||
h.in_write += 1
|
||||
phase[0] += FXRT_BLOCK / 44100.0
|
||||
time.sleep(0.003)
|
||||
threading.Thread(target=feed, daemon=True).start()
|
||||
|
||||
def frame_loop():
|
||||
while not stop.is_set():
|
||||
http_get(url + "/frame")
|
||||
time.sleep(1.0 / 30.0)
|
||||
if FRAME == "on":
|
||||
threading.Thread(target=frame_loop, daemon=True).start()
|
||||
|
||||
def reader():
|
||||
last = h.out_write
|
||||
prev_samp = 0.0
|
||||
prev_ok = False
|
||||
expected_next = None
|
||||
while not stop.is_set():
|
||||
w = h.out_write
|
||||
while last < w:
|
||||
slot = last & (FXRT_OUT_SLOTS - 1)
|
||||
off = OUT_L_OFF + slot * FXRT_BLOCK
|
||||
blk = npbuf[off // 4:(off // 4) + FXRT_BLOCK].copy()
|
||||
# detect discontinuity: jump vs previous sample
|
||||
if prev_ok:
|
||||
for i in range(FXRT_BLOCK):
|
||||
if abs(blk[i] - prev_samp) > 0.25:
|
||||
glitches.append((time.time(), i))
|
||||
break
|
||||
prev_samp = blk[-1]
|
||||
prev_ok = True
|
||||
out_total[0] += FXRT_BLOCK
|
||||
last += 1
|
||||
time.sleep(0.001)
|
||||
threading.Thread(target=reader, daemon=True).start()
|
||||
|
||||
# /preset poll like frontend
|
||||
t0 = time.time()
|
||||
n = 0
|
||||
while time.time() - t0 < DURATION:
|
||||
time.sleep(1.5)
|
||||
http_get(url + "/preset")
|
||||
n += 1
|
||||
stop.set()
|
||||
time.sleep(0.3)
|
||||
print("RESULT: %d polls, out blocks=%d, glitches=%d" % (n, out_total[0], len(glitches)), flush=True)
|
||||
for g in glitches[:10]:
|
||||
print(" glitch t=%.2f idx=%d" % (g[0] - (time.time() - t0), g[1]), flush=True)
|
||||
finally:
|
||||
stop.set()
|
||||
for p in procs:
|
||||
try: p.terminate()
|
||||
except Exception: pass
|
||||
time.sleep(0.5)
|
||||
for p in procs:
|
||||
try: p.kill()
|
||||
except Exception: pass
|
||||
shm.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure out-ring glitches while set_chain fires every 3s (preset apply),
|
||||
with GUI bridge + frame + preset poll running. set_chain is the only path
|
||||
that reloads a VST3 in the realtime process."""
|
||||
import ctypes, json, os, subprocess, tempfile, threading, time, uuid, urllib.request, sys
|
||||
import numpy as np
|
||||
from multiprocessing import shared_memory
|
||||
|
||||
FXRT_MAGIC = 0x46585254
|
||||
FXRT_BLOCK = 256
|
||||
FXRT_IN_SLOTS = 4
|
||||
FXRT_OUT_SLOTS = 8
|
||||
FXRT_CTRL_SLOTS = 8
|
||||
FXRT_LAT_SLOTS = 8
|
||||
FXRT_STATE_STARTING = 0
|
||||
FXRT_STATE_READY = 1
|
||||
|
||||
class FxRTHeader(ctypes.Structure):
|
||||
_fields_ = [("magic", ctypes.c_uint32), ("state", ctypes.c_uint32),
|
||||
("sample_rate", ctypes.c_uint32), ("block_size", ctypes.c_uint32),
|
||||
("running", ctypes.c_uint32), ("heartbeat", ctypes.c_uint32),
|
||||
("in_write", ctypes.c_uint32), ("in_read", ctypes.c_uint32),
|
||||
("in_slots", ctypes.c_uint32), ("out_write", ctypes.c_uint32),
|
||||
("out_read", ctypes.c_uint32), ("out_slots", ctypes.c_uint32),
|
||||
("ctrl_write", ctypes.c_uint32), ("ctrl_read", ctypes.c_uint32),
|
||||
("ctrl_slots", ctypes.c_uint32), ("lat_write", ctypes.c_uint32),
|
||||
("lat_read", ctypes.c_uint32), ("lat_slots", ctypes.c_uint32)]
|
||||
|
||||
HEADER_SIZE = ctypes.sizeof(FxRTHeader)
|
||||
IN_L_OFF = HEADER_SIZE
|
||||
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
TOTAL_SIZE = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
|
||||
EXE = r"C:\Users\locpham\SonicForgeStudio\src-tauri\binaries\fx_vst_bridge.exe"
|
||||
PLUGIN = sys.argv[1] if len(sys.argv) > 1 else r"C:\Program Files\Common Files\VST3\WaveObserver.vst3"
|
||||
DURATION = float(sys.argv[2]) if len(sys.argv) > 2 else 40.0
|
||||
|
||||
def http_get(url, timeout=5):
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as r:
|
||||
return r.read()
|
||||
except Exception as e:
|
||||
return b"ERR:" + str(e).encode()
|
||||
|
||||
def main():
|
||||
name = "SonicForge_FXRT_" + uuid.uuid4().hex[:12]
|
||||
print("plugin:", PLUGIN, "shm:", name, flush=True)
|
||||
shm = shared_memory.SharedMemory(name=name, create=True, size=TOTAL_SIZE)
|
||||
shm.buf[:TOTAL_SIZE] = b"\x00" * TOTAL_SIZE
|
||||
h = FxRTHeader.from_buffer(shm.buf)
|
||||
h.magic = FXRT_MAGIC; h.state = FXRT_STATE_STARTING
|
||||
h.sample_rate = 44100; h.block_size = FXRT_BLOCK; h.running = 1
|
||||
h.in_slots = FXRT_IN_SLOTS; h.out_slots = FXRT_OUT_SLOTS
|
||||
h.ctrl_slots = FXRT_CTRL_SLOTS; h.lat_slots = FXRT_LAT_SLOTS
|
||||
npbuf = np.frombuffer(shm.buf, dtype=np.float32)
|
||||
stop = threading.Event()
|
||||
procs = []
|
||||
glitches = []
|
||||
out_total = [0]
|
||||
jp1 = None
|
||||
try:
|
||||
job1 = {"sample_rate": 44100, "block_size": 256, "seq": 0,
|
||||
"fx_chain": [{"type": "vst3", "path": PLUGIN, "name": "probe", "preset_b64": "", "bypass": False}]}
|
||||
fd, jp1 = tempfile.mkstemp(suffix=".json", prefix="fxrt_probe_")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(job1, f)
|
||||
p1 = subprocess.Popen([EXE, "--realtime-fx", jp1, "--shm", name, "--parent", str(os.getpid())],
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
procs.append(p1)
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < 30 and h.state != FXRT_STATE_READY:
|
||||
time.sleep(0.05)
|
||||
if p1.poll() is not None:
|
||||
print("REALTIME BRIDGE EXITED rc=", p1.returncode, flush=True); return
|
||||
if h.state != FXRT_STATE_READY:
|
||||
print("realtime not ready", flush=True); return
|
||||
print("realtime ready in %.2fs" % (time.time() - t0), flush=True)
|
||||
|
||||
job2 = {"path": PLUGIN, "name": "probe", "shm": name, "preset_b64": ""}
|
||||
fd, jp2 = tempfile.mkstemp(suffix=".json", prefix="fxgui_probe_")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(job2, f)
|
||||
p2 = subprocess.Popen([EXE, "--fx-gui", jp2],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
procs.append(p2)
|
||||
port = None
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < 20:
|
||||
line = p2.stdout.readline()
|
||||
if not line:
|
||||
if p2.poll() is not None: break
|
||||
continue
|
||||
line = line.strip()
|
||||
if line.startswith("SF_FXGUI_PORT="):
|
||||
port = int(line.split("=", 1)[1]); break
|
||||
if not port:
|
||||
print("GUI bridge: no port", flush=True); return
|
||||
url = "http://127.0.0.1:%d" % port
|
||||
print("gui port:", port, flush=True)
|
||||
|
||||
phase = [0.0]
|
||||
def feed():
|
||||
while not stop.is_set():
|
||||
if h.in_write - h.in_read < FXRT_IN_SLOTS:
|
||||
s = h.in_write & (FXRT_IN_SLOTS - 1)
|
||||
off = IN_L_OFF + s * FXRT_BLOCK
|
||||
t = np.arange(FXRT_BLOCK) / 44100.0 + phase[0]
|
||||
sine = (0.1 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)
|
||||
npbuf[off // 4:(off // 4) + FXRT_BLOCK] = sine
|
||||
npbuf[(off + FXRT_IN_SLOTS * FXRT_BLOCK) // 4:(off + FXRT_IN_SLOTS * FXRT_BLOCK) // 4 + FXRT_BLOCK] = sine
|
||||
h.in_write += 1
|
||||
phase[0] += FXRT_BLOCK / 44100.0
|
||||
time.sleep(0.003)
|
||||
threading.Thread(target=feed, daemon=True).start()
|
||||
|
||||
def frame_loop():
|
||||
while not stop.is_set():
|
||||
http_get(url + "/frame")
|
||||
time.sleep(1.0 / 30.0)
|
||||
threading.Thread(target=frame_loop, daemon=True).start()
|
||||
|
||||
seq = [0]
|
||||
def setchain_loop():
|
||||
while not stop.is_set():
|
||||
time.sleep(3.0)
|
||||
try:
|
||||
with urllib.request.urlopen(url + "/preset", timeout=5) as r:
|
||||
d = json.loads(r.read().decode())
|
||||
pb = d.get("preset_b64", "")
|
||||
except Exception:
|
||||
pb = ""
|
||||
seq[0] += 1
|
||||
job = {"sample_rate": 44100, "block_size": 256, "seq": seq[0],
|
||||
"fx_chain": [{"type": "vst3", "path": PLUGIN, "name": "probe", "preset_b64": pb, "bypass": False}]}
|
||||
with open(jp1, "w", encoding="utf-8") as f:
|
||||
json.dump(job, f)
|
||||
print(" set_chain seq=%d preset_len=%d at %.2fs" % (seq[0], len(pb), time.time() - t0), flush=True)
|
||||
threading.Thread(target=setchain_loop, daemon=True).start()
|
||||
|
||||
def reader():
|
||||
last = h.out_write
|
||||
prev_samp = 0.0
|
||||
prev_ok = False
|
||||
while not stop.is_set():
|
||||
w = h.out_write
|
||||
while last < w:
|
||||
s = last & (FXRT_OUT_SLOTS - 1)
|
||||
off = OUT_L_OFF + s * FXRT_BLOCK
|
||||
blk = npbuf[off // 4:(off // 4) + FXRT_BLOCK].copy()
|
||||
if prev_ok:
|
||||
for i in range(FXRT_BLOCK):
|
||||
if abs(blk[i] - prev_samp) > 0.25:
|
||||
glitches.append((time.time(), i, last))
|
||||
break
|
||||
prev_samp = blk[-1]
|
||||
prev_ok = True
|
||||
out_total[0] += FXRT_BLOCK
|
||||
last += 1
|
||||
time.sleep(0.001)
|
||||
threading.Thread(target=reader, daemon=True).start()
|
||||
|
||||
t0 = time.time()
|
||||
n = 0
|
||||
while time.time() - t0 < DURATION:
|
||||
time.sleep(1.5)
|
||||
http_get(url + "/preset")
|
||||
n += 1
|
||||
stop.set()
|
||||
time.sleep(0.3)
|
||||
print("RESULT: %d polls, out blocks=%d, glitches=%d" % (n, out_total[0], len(glitches)), flush=True)
|
||||
for g in glitches[:10]:
|
||||
print(" glitch block=%d idx=%d" % (g[2], g[1]), flush=True)
|
||||
finally:
|
||||
stop.set()
|
||||
for p in procs:
|
||||
try: p.terminate()
|
||||
except Exception: pass
|
||||
time.sleep(0.5)
|
||||
for p in procs:
|
||||
try: p.kill()
|
||||
except Exception: pass
|
||||
shm.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
# Mastering Expand — Extension Modules & Dynamic Chain
|
||||
|
||||
Tài liệu cho các module mở rộng của Mastering modal (MasteringModal) và Track FX chain.
|
||||
|
||||
## §II.1 EQ Preset Library
|
||||
- Thư viện preset EQ band-gain dùng chung cho track EQ.
|
||||
- Code: `app/static/js/app.jsx` (EQ Preset Library, `EQ_PRESETS`).
|
||||
- Ứng dụng: Mastering modal EQ module, Track Strip EQ.
|
||||
|
||||
## §II.2 Bus Compressor Module
|
||||
- Module nén masterbus đặt trong MasteringModal, giữa các module chain.
|
||||
- Code: `app/static/js/app.jsx` (Bus Compressor module trong MasteringModal JSX).
|
||||
- Điều khiển: threshold, ratio, attack, release, makeup gain; bypass riêng.
|
||||
|
||||
## §II.3 rebuildAudioGraph / Dynamic Module Chain
|
||||
- `rebuildAudioGraph`: dựng lại đồ thị WebAudio động theo thứ tự module chain.
|
||||
- Thứ tự module: chain strip trong MasteringModal cho phép thêm/xóa/bỏ/bypass
|
||||
module, thay đổi thứ tự — audio graph được rebuild tương ứng.
|
||||
- Default = chain cũ (`mastering_expand.md: extension modules + dynamic chain`).
|
||||
- Code: `app/static/js/app.jsx` (MODULE CHAIN STRIP dynamic).
|
||||
|
||||
## §II.4 Track FX Module Factory
|
||||
- Factory tạo module FX theo loại (EQ, compressor, ...) cho track FX chain.
|
||||
- Dùng chung cho Track FX chain editor trong track strip
|
||||
(`Track FX Chain (mastering_expand.md §II.4)`).
|
||||
- Code: `app/static/js/app.jsx` (Track FX module factory).
|
||||
|
||||
## §III ADD MODULE POPUP
|
||||
- Popup thêm module mới vào chain (chọn loại module rồi append).
|
||||
- Code: `app/static/js/app.jsx` (ADD MODULE POPUP trong MasteringModal).
|
||||
@@ -0,0 +1,16 @@
|
||||
# Unified FX Rack Panel
|
||||
|
||||
Một panel FX Rack dùng chung cho per-track (FXRackModal) và masterbus
|
||||
(MasteringModal), thống nhất trải nghiệm thêm/sắp xếp FX.
|
||||
|
||||
## §III.1 Track-EQ Preset Library
|
||||
- Thư viện preset EQ band-gains dùng cho track EQ (chỉ band gains).
|
||||
- Code: `app/static/js/app.jsx` (Track-EQ preset library).
|
||||
- Preset áp trực tiếp các giá trị gain band vào track EQ.
|
||||
|
||||
## Unified FX Rack Panel
|
||||
- Panel thống nhất: mỗi track mở FXRackModal từ track strip context
|
||||
(`Unified FX Rack target context: { trackId, trackName }`).
|
||||
- Bao gồm: WebAudio builtin chain + VST FX chain (xem thêm
|
||||
`unified_fx_rack_panel_update.md`), bypass từng slot và toàn chain.
|
||||
- Code: `app/static/js/app.jsx` (UNIFIED FX RACK PANEL + FXRackModal).
|
||||
@@ -0,0 +1,17 @@
|
||||
# Unified FX Rack Panel Update — VST FX & Wave Observer
|
||||
|
||||
Cập nhật cho `unified_fx_rack_panel.md`: bổ sung VST FX chain và Wave Observer.
|
||||
|
||||
## VST FX Chain
|
||||
- FXRackModal (per-track) và MasteringModal (masterbus) có section "VST FX":
|
||||
thêm plugin từ `SonicAPI.listFx` (danh sách VST3/LV2 đã scan), upload preset
|
||||
dạng base64, bypass toàn chain, sắp xếp lên/xuống/xóa.
|
||||
- Render offline qua native_bridge `--render-fx` (xem `PLAN_MASTERBUS_FX_RACK_VST.md`).
|
||||
- Serialize: track `vst_fx_chain`/`vst_fx_bypass`; master
|
||||
`main_session.master.fx_chain`/`bypass`.
|
||||
|
||||
## §III.3 Wave Observer
|
||||
- Oscilloscope real-time xem dạng sóng tín hiệu tại điểm quan sát (master / track).
|
||||
- Code: `app/static/js/app.jsx` (WAVE OBSERVER — REAL-TIME OSCILLOSCOPE;
|
||||
`Wave Observer scope state`; `Wave Observer scope analysers`).
|
||||
- Scope: chọn master hoặc track; analysers dùng WebAudio AnalyserNode.
|
||||
@@ -3,13 +3,16 @@ project(daw_vst_bridge LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
if(MSVC)
|
||||
string(REPLACE "/EHsc" "/EHa" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
endif()
|
||||
|
||||
# ── 1. VST3 SDK (Steinberg, vendored as git submodule — NOT in vcpkg) ──
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/CMakeLists.txt")
|
||||
if(WIN32 AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/CMakeLists.txt")
|
||||
add_subdirectory(vst3sdk EXCLUDE_FROM_ALL)
|
||||
set(VST3_SDK_TARGET sdk)
|
||||
else()
|
||||
message(WARNING "vst3sdk submodule missing — VST3 host disabled; SF2/SF3/SFZ still build")
|
||||
message(STATUS "vst3sdk build skipped (Windows-only) — VST3 host disabled; SF2/SF3/SFZ still build")
|
||||
set(VST3_SDK_TARGET "")
|
||||
endif()
|
||||
|
||||
@@ -45,53 +48,153 @@ else()
|
||||
endif()
|
||||
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
${FLUIDSYNTH_INCLUDE_DIR}
|
||||
${SFIZZ_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(daw_vst_bridge
|
||||
src/main.cpp
|
||||
src/RenderJob.cpp
|
||||
src/RenderFxJob.cpp
|
||||
src/BuiltinFxChain.cpp
|
||||
src/FxGuiServer.cpp
|
||||
src/NativeInstrumentEngine.cpp
|
||||
src/SharedMemoryIPC.cpp
|
||||
src/Vst3Instrument.cpp
|
||||
src/Vst2Instrument.cpp
|
||||
src/StateStore.cpp
|
||||
src/SandboxVst3Host.cpp
|
||||
src/SandboxVst2Host.cpp
|
||||
)
|
||||
else()
|
||||
# Linux: offline --render only (SF2/SF3 + SFZ; VST3/VST2 hosting is
|
||||
# Windows-only). main_linux.cpp is the tiny --render entry.
|
||||
add_executable(daw_vst_bridge
|
||||
src/main_linux.cpp
|
||||
src/RenderJob.cpp
|
||||
src/NativeInstrumentEngine.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
# VST3 SDK hosting: sdk_hosting lacks the platform module loader — module_win32.cpp
|
||||
# is added to the bridge itself (same approach as the audiohost sample).
|
||||
if(VST3_SDK_TARGET)
|
||||
target_compile_definitions(daw_vst_bridge PRIVATE HAVE_VST3SDK=1)
|
||||
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET} sdk_hosting)
|
||||
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
|
||||
if(WIN32)
|
||||
target_sources(daw_vst_bridge PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY})
|
||||
find_package(JPEG REQUIRED) # libjpeg-turbo (vcpkg) — FxGuiServer.cpp JPEG encode
|
||||
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} JPEG::JPEG)
|
||||
# Required Windows libs
|
||||
target_link_libraries(daw_vst_bridge PRIVATE winmm)
|
||||
target_link_libraries(daw_vst_bridge PRIVATE winmm imm32)
|
||||
# App icon (shared favicon.ico -> exe resource)
|
||||
target_sources(daw_vst_bridge PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/app.rc)
|
||||
else()
|
||||
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
|
||||
endif()
|
||||
|
||||
# ── fx_vst_bridge: VST FX host (split from daw_vst_bridge). Windows-only:
|
||||
# --render-fx (offline) + --realtime-fx (SHM realtime cloud-FX loop). ──
|
||||
if(WIN32)
|
||||
add_executable(fx_vst_bridge
|
||||
src/main_fx.cpp
|
||||
src/RenderFxJob.cpp
|
||||
src/RealtimeFxLoop.cpp
|
||||
src/BuiltinFxChain.cpp
|
||||
src/FxGuiServer.cpp
|
||||
)
|
||||
if(VST3_SDK_TARGET)
|
||||
target_compile_definitions(fx_vst_bridge PRIVATE HAVE_VST3SDK=1)
|
||||
target_link_libraries(fx_vst_bridge PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
|
||||
target_sources(fx_vst_bridge PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
|
||||
)
|
||||
endif()
|
||||
find_package(JPEG REQUIRED) # libjpeg-turbo (vcpkg) — FxGuiServer.cpp JPEG encode
|
||||
# FX modes are VST3 + builtin DSP only — no fluidsynth/sfizz (avoids a
|
||||
# runtime libfluidsynth-3.dll dependency for the FX bridge).
|
||||
target_link_libraries(fx_vst_bridge PRIVATE JPEG::JPEG)
|
||||
# Required Windows libs
|
||||
target_link_libraries(fx_vst_bridge PRIVATE winmm imm32 ws2_32)
|
||||
# App icon (shared favicon.ico -> exe resource)
|
||||
target_sources(fx_vst_bridge PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/app.rc)
|
||||
endif()
|
||||
|
||||
# ── gui_probe: standalone VST3 GUI attach probe (debug tool, not shipped) ──
|
||||
if(WIN32)
|
||||
add_executable(gui_probe
|
||||
src/gui_probe.cpp
|
||||
src/NativeInstrumentEngine.cpp
|
||||
src/SharedMemoryIPC.cpp
|
||||
src/Vst3Instrument.cpp
|
||||
src/Vst2Instrument.cpp
|
||||
src/SandboxVst3Host.cpp
|
||||
src/SandboxVst2Host.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)
|
||||
target_link_libraries(gui_probe PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
|
||||
if(WIN32)
|
||||
target_sources(gui_probe PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(gui_probe PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
|
||||
endif()
|
||||
add_executable(plugin_host
|
||||
src/plugin_host_main.cpp
|
||||
src/SharedMemoryIPC.cpp
|
||||
src/Vst3Instrument.cpp
|
||||
src/Vst2Instrument.cpp
|
||||
)
|
||||
if(VST3_SDK_TARGET)
|
||||
target_compile_definitions(plugin_host PRIVATE HAVE_VST3SDK=1)
|
||||
target_link_libraries(plugin_host PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
|
||||
if(WIN32)
|
||||
target_sources(plugin_host PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(plugin_host PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
|
||||
# App icon (shared favicon.ico -> exe resource)
|
||||
target_sources(plugin_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/app.rc)
|
||||
endif()
|
||||
|
||||
# G3.3: seh_channel_test — SEH per-channel crash isolation (debug tool, not shipped)
|
||||
add_executable(seh_channel_test
|
||||
tests/seh_channel_test.cpp
|
||||
src/Vst3Instrument.cpp
|
||||
src/Vst2Instrument.cpp
|
||||
src/SandboxVst3Host.cpp
|
||||
src/SharedMemoryIPC.cpp
|
||||
)
|
||||
if(VST3_SDK_TARGET)
|
||||
target_compile_definitions(seh_channel_test PRIVATE HAVE_VST3SDK=1)
|
||||
target_link_libraries(seh_channel_test PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
|
||||
if(WIN32)
|
||||
target_sources(seh_channel_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(seh_channel_test PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
|
||||
endif()
|
||||
endif() # WIN32-only debug tools (gui_probe, plugin_host, seh_channel_test)
|
||||
|
||||
Submodule
+1
Submodule native_bridge/JUCE added at 10a589619b
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
# ROADMAP: Xử lý triệt để như DAW hiện đại — tách UI thread khỏi audio loop
|
||||
|
||||
Ngày: 2026-08-15 · Repo: `C:/Users/locpham/SonicForgeStudio` · Dựa trên bằng chứng
|
||||
`native_bridge/debug/BUG_REPORT.md` + source hiện tại (main.cpp, Vst3Instrument.cpp, lib.rs).
|
||||
|
||||
## Mục tiêu kiến trúc đích
|
||||
|
||||
DAW hiện đại (Cubase/Reaper/FL/Bitwig) tách:
|
||||
- **Audio thread**: real-time, chỉ gọi `process()`
|
||||
- **UI thread**: main UI thread, chạy `view->attached()` + message pump editor
|
||||
- (tuỳ chọn) **Plugin process riêng**: sandbox crash plugin khỏi engine
|
||||
|
||||
Bridge hiện tại gộp 2 thứ đầu vào 1 thread (`while(true)` vừa `renderAll()` vừa
|
||||
`PeekMessageW`, main.cpp:1220-1240) + attach job trên ChannelWorker → 2 thread 1
|
||||
DLL → Nexus throw/hang. Roadmap này tách UI khỏi audio đúng VST3 contract.
|
||||
|
||||
## Nguyên tắc vận hành (áp dụng cho MỌI task)
|
||||
|
||||
```
|
||||
1. Viết test tái hiện TRƯỚC (gui_probe variant / probe_appN / driver kịch bản)
|
||||
2. Implement tối thiểu (đúng phạm vi task)
|
||||
3. Chạy bộ test: gui_probe matrix + live driver + regression cũ
|
||||
4. Ghi nhận bug vào log + BUG_REPORT.md (mỗi bug: kịch bản, log, nguyên nhân)
|
||||
5. Fix → chạy lại → pass hết mới deploy
|
||||
6. Deploy exe vào install/ + commit (theo pattern git log hiện có)
|
||||
```
|
||||
|
||||
Không task nào merge nếu chưa: probe matrix pass + driver 3 run liên tiếp pass +
|
||||
screenshot GUI không trắng.
|
||||
|
||||
## GIAI ĐOẠN 0 — Ổn định hiện trạng (P0, ngăn máu chảy)
|
||||
|
||||
Làm ngay, độc lập kiến trúc. Không phải "xử lý triệt để" nhưng là nền để làm việc
|
||||
dài hạn không bị bug quấy phá.
|
||||
|
||||
| # | Task | File | Done khi |
|
||||
|---|---|---|---|
|
||||
| G0.1 | Chặn/defer OPEN_GUI khi transport PLAY (đợi STOP rồi attach; hoặc tạm dừng transport) | main.cpp handleOpenGui + Rust transport state | probe_app4_live: PLAY + OPEN_GUI → không EXCEPTION, attach sau STOP thành công |
|
||||
| G0.2 | Bỏ spam `openVstGuiRetry` 20×500ms → tối đa 2 lần, có backoff, không retry khi đang PLAY | app/static/js/app.precompiled.js | bridge.log không còn chuỗi EXCEPTION/FAILED lặp; stall không xảy ra |
|
||||
| G0.3 | Watchdog né giết khi attach-in-flight (flag SHM hoặc stall type riêng) | lib.rs:397 + shm | Stall do attach dài → không kill; stall thật → kill như cũ |
|
||||
| G0.4 | Save/restore state nhanh (getState trước khi reload, setState sau) | Vst3Instrument.cpp | Mở GUI → đóng → mở lại: preset giữ nguyên |
|
||||
|
||||
## GIAI ĐOẠN 1 — Tách GUI pump khỏi audio loop (P1, lõi triệt để)
|
||||
|
||||
**Vấn đề hiện tại**: main thread vừa render audio vừa pump message editor.
|
||||
Attach job chạy ChannelWorker (thread khác) → 2 thread 1 DLL.
|
||||
|
||||
**Đích**: main thread chỉ render audio. Thread UI mới (native UI thread) tạo window
|
||||
+ pump message editor + chạy toàn bộ attach/close. Đúng mô hình Steinberg
|
||||
`editorhost.cpp`: 1 UI thread, 1 audio thread.
|
||||
|
||||
| # | Task | File | Done khi |
|
||||
|---|---|---|---|
|
||||
| G1.1 | Thêm `UiThread` (thread riêng, COM STA, message pump riêng, job queue) | main.cpp mới class | gui_probe variant `ui_thread` tương đương bridge mới: Nexus attach OK không hang |
|
||||
| G1.2 | Chuyển tạo window + attach job từ ChannelWorker → UiThread; audio loop bỏ `PeekMessageW` | main.cpp | Probe bridge arch mới: 2 instance Nexus attach OK, không HANG (so với bridge_two HANG hiện tại) |
|
||||
| G1.3 | Chuyển close/resize/IME sang UiThread; bỏ pump gate cũ (g_juceTidsMutex...) nếu không còn cần | main.cpp | close editor đang PLAY không crash; không cần mute workaround |
|
||||
| G1.4 | Bỏ mute-when-editor-open (g_editorOpen/g_editorPathCount) — test lại | main.cpp | 6 Nexus + GUI ch5 mở khi PLAY: track khác vẫn kêu (mute hiện tại làm track câm) |
|
||||
| G1.5 | Regression: driver stress 6 Nexus + Ample + EZkeys, mở/đóng GUI khi PLAY | driver + probe | 3 run liên tiếp không crash/stall |
|
||||
|
||||
## GIAI ĐOẠN 2 — Persistence: restart không mất instrument (P2)
|
||||
|
||||
Trả lời câu hỏi "stop/play nghe lại". Hiện bridge kill = mất hết instance (state
|
||||
trong memory process).
|
||||
|
||||
| # | Task | File | Done khi |
|
||||
|---|---|---|---|
|
||||
| G2.1 | `saveState()`/`restoreState()` đầy đủ (IComponentHandler/getState, preset, MIDI routing) | Vst3Instrument.cpp + NativeInstrumentEngine | Save → reload → restore: âm thanh/preset giống hệt |
|
||||
| G2.2 | Bridge dump state ra file khi shutdown/trước kill (SIGHUP/SHM flag) | main.cpp + lib.rs | Kill bridge → state file có đủ 16 channel |
|
||||
| G2.3 | Frontend auto-reload sau respawn: đọc state file → bridgeLoad lại từng track | app + NativeBridgeService | Giết bridge giữa PLAY → bridge mới tự nạp lại, PLAY tiếp tục, không câm |
|
||||
| G2.4 | Bỏ `!isPlayingRef.current` chặn restore khi đang play | app.precompiled.js:816 | Restore chạy được cả khi PLAY |
|
||||
|
||||
## GIAI ĐOẠN 3 — Watchdog thông minh + health (P3)
|
||||
|
||||
| # | Task | File | Done khi |
|
||||
|---|---|---|---|
|
||||
| G3.1 | Heartbeat riêng (thread bridge ghi timestamp mỗi 100ms) tách khỏi writeIndex audio | main.cpp + shm.rs + lib.rs | Bridge sống nhưng audio stall → không kill; bridge chết → phát hiện <1s |
|
||||
| G3.2 | Phân loại stall: audio (render quá lâu) vs GUI job (attach/close) vs chết | lib.rs | Mỗi loại có policy riêng (GUI: đợi, không kill) |
|
||||
| G3.3 | Bridge tự phục hồi lỗi plugin: catch crash riêng channel (SEH quanh process của từng channel) | main.cpp renderAll | 1 plugin crash → mute channel đó, các channel khác chạy tiếp (giống DAW sandbox-lite) |
|
||||
|
||||
## GIAI ĐOẠN 4 — Sandbox plugin process riêng (P4, tuỳ chọn tham vọng)
|
||||
|
||||
VST3 không có chuẩn out-of-process như AUv3 → cần wrapper tự làm. Đây là bước xa
|
||||
nhất, chỉ làm khi G1-G3 chưa đủ.
|
||||
|
||||
| # | Task | File | Done khi |
|
||||
|---|---|---|---|
|
||||
| G4.1 | Nghiên cứu: host plugin trong process con (VST3 host trong exe thứ 2), audio qua SHM | thiết kế mới | PoC 1 plugin Nexus trong process con, GUI + audio OK |
|
||||
| G4.2 | Bridge → client process pool (1 process/plugin hoặc /nhóm) | thiết kế mới | Plugin crash → process con chết, bridge + GUI sống |
|
||||
| G4.3 | IPC audio real-time (SHM ring buffer double-buffer) | thiết kế mới | 6 Nexus song song, latency < 20ms, không xrun |
|
||||
|
||||
**Cân nhắc**: G4 là dự án riêng (ước lượng gấp 3-5× G1+G2). Khuyến nghị: làm G0-G3
|
||||
trước; G4 chỉ khi cần cô lập crash plugin thật sự (hiện G3.3 + G1 đã lo được 90%
|
||||
kịch bản bug).
|
||||
|
||||
## Thứ tự thực hiện đề xuất
|
||||
|
||||
```
|
||||
G0.1 → G0.2 → G0.3 → G0.4 (1-2 tuần, an toàn ngay, trả lời "stop/play nghe lại" một phần qua G0.4)
|
||||
↓
|
||||
G1.1 → G1.2 → G1.3 → G1.4 → G1.5 (3-5 tuần, lõi triệt để — mở GUI khi PLAY thành công)
|
||||
↓
|
||||
G2.1 → G2.2 → G2.3 → G2.4 (2-3 tuần, restart không câm)
|
||||
↓
|
||||
G3.1 → G3.2 → G3.3 (1-2 tuần, health đầy đủ)
|
||||
↓
|
||||
G4.* (tuỳ chọn, 2-3 tháng)
|
||||
```
|
||||
|
||||
## Bắt đầu ngay — task đầu tiên
|
||||
|
||||
**G0.1**: sửa `handleOpenGui` (main.cpp:943) — nếu transport PLAY, đưa OPEN_GUI
|
||||
vào `g_pendingGui` (đã có cơ chế defer cho "chưa load"), sweep xử lý khi transport
|
||||
STOP. Kèm test: `probe_app4_live.py` biến thể "PLAY rồi OPEN_GUI rồi STOP" — kỳ
|
||||
vọng không EXCEPTION, GUI attach sau STOP.
|
||||
|
||||
## Files liên quan hiện tại
|
||||
|
||||
- `native_bridge/src/main.cpp` — loop (1220), handleOpenGui (943), ChannelWorker, mute/close
|
||||
- `native_bridge/src/Vst3Instrument.cpp` — reload (615), reloadForGUI (528), attachView (550)
|
||||
- `native_bridge/src/gui_probe.cpp` — probe variants (bridge_two 243, two_workers_close 318)
|
||||
- `native_bridge/src/lib.rs` → thực tế `src-tauri/src/lib.rs` — watchdog (356-430), kill/spawn (50,135)
|
||||
- `app/static/js/app.precompiled.js` — openVstGuiRetry (470), restore (816)
|
||||
- `native_bridge/debug/` — BUG_REPORT.md, sweep_bridge_two_8s.json, probe_attach_detail.py, probe_app4_live.py
|
||||
@@ -0,0 +1,122 @@
|
||||
# BÁO CÁO BUG: Mở VST GUI khi đang PLAY → dự án câm, GUI không load
|
||||
|
||||
Ngày: 2026-08-15 · Repo: `C:/Users/locpham/SonicForgeStudio` · git status sạch
|
||||
|
||||
## 1. Tóm tắt
|
||||
|
||||
Nguyên nhân gốc: **nhiều thread cùng vào 1 plugin DLL trong lúc `view->attached()`**.
|
||||
Khi đang PLAY, audio loop `process()` chạy trên các instance Nexus (ch0-4) cùng lúc
|
||||
worker thread của ch5 chạy `attachView()`. Nexus throw exception trong
|
||||
`createView/attached` → attach FAILED → frontend retry ~30 lần (0.3s/lần) →
|
||||
`bridgeWriteIndex` stall 3s → Tauri watchdog kill+respawn bridge → **mất hết
|
||||
instrument → câm toàn bộ**. GUI không bao giờ load được.
|
||||
|
||||
Không phải "instrument track khác bị reset" (giả thuyết 1 sai ở bước này — reload
|
||||
reset state chỉ xảy ra khi GUI ĐÃ mở thành công rồi đóng rồi mở lại, không phải lần
|
||||
attach đầu).
|
||||
|
||||
## 2. Chuỗi bug đầy đủ (xác nhận live, bridge.log)
|
||||
|
||||
```
|
||||
... 6x Nexus loaded ch0-5 + EZkeys ch6 ...
|
||||
[NativeBridge] transport PLAY playhead=0
|
||||
[NativeBridge] GUI attach EXCEPTION ch=5 — plugin threw (createView/attached)
|
||||
[NativeBridge] GUI attach FAILED hwnd=0000000002250F36 plugin=Nexus
|
||||
[tauri] bridge restarted (attempt 1) ← watchdog: writeIndex stall 3s
|
||||
[NativeBridge] Starting DAW Host Bridge Engine... ← restart, mất hết instrument
|
||||
```
|
||||
|
||||
1. `handleOpenGui` (main.cpp ~943): OPEN_GUI ch5 → silence mọi channel cùng
|
||||
plugin path → `g_loadMutex` → `try { reloadForGUI(); attachView(hwnd); } catch(...)`.
|
||||
2. `attachView` (Vst3Instrument.cpp:550): `controller->createView(kEditor)` →
|
||||
`view->attached(hwnd, kPlatformTypeHWND)` → **Nexus throw** khi đang PLAY
|
||||
(audio loop process() cùng DLL đang chạy trên instance khác).
|
||||
3. catch → `ok=false` → log EXCEPTION + FAILED → PostMessage WM_CLOSE (GUI không load).
|
||||
4. Frontend `openVstGuiRetry` spam lại ~30 lần; mỗi lần chạy lại job (silence +
|
||||
mutex + reload/attach) → main loop bị nghẽn → `bridgeWriteIndex` stall 3s.
|
||||
5. `lib.rs`: stall >3s → `kill_bridge` + `spawn_bridge` (restart_attempts==0) →
|
||||
mất toàn bộ instrument state → **CÂM**.
|
||||
|
||||
## 3. Bằng chứng thực nghiệm
|
||||
|
||||
### 3.1 Probe kiến trúc chính xác (bridge_two: 2 worker STA riêng + main pump)
|
||||
|
||||
`gui_probe.exe <plugin> bridge_two <secs>` — kết quả 76 VST3 hệ thống (sweep):
|
||||
`native_bridge/debug/sweep_bridge_two_8s.json`, `probe_attach_detail.py` (bổ sung).
|
||||
|
||||
⚠️ **Hiệu chỉnh sweep cũ**: exit code 0xC0000409 của TẤT CẢ 76 plugin là
|
||||
**artifact của probe** — `ProbeWorker::th` không join → `~std::thread` trên
|
||||
joinable thread → `std::terminate` → fail-fast. RESULT line in ra nhưng không flush
|
||||
kịp. Không phải 76 plugin cùng crash teardown.
|
||||
|
||||
Phân loại đúng dựa trên output per-attach:
|
||||
|
||||
| Nhóm | Số plugin | Bằng chứng | Ý nghĩa |
|
||||
|---|---|---|---|
|
||||
| **HANG attach inst2** | 30 | `workerA openGUI inst1=1` rồi `workerB openGUI inst2` in ra, **không có kết quả** — Nexus 25s không về | 2 instance cùng DLL, 2 apartment → attach instance 2 HANG trong `view->attached()` |
|
||||
| **BOTH-OK** | 20 | `workerB openGUI inst2=1` — Kontakt, BlueARP, Ozone 11 (18) | Attach 2 instance OK; chỉ crash teardown artifact |
|
||||
| **HANG/crash attach inst1** | 18 | `workerA hwnd=` in ra, không in `openGUI inst1` — RX 11 (18) + ACTP | Chết ngay instance 1 |
|
||||
| **CRASH EARLY** | 8 | exit 1.2-4.6s, 0xC0000005 (AV): Melodyne, Orb Arpeggios/Melody, smartEQ3/live, smartreverb, DUNE3, ACDD | Crash thật trong attach |
|
||||
|
||||
Không plugin nào HANG >18s (không WATCHDOG-KILL) — main pump cứu được các trường
|
||||
hợp cần pump; vấn đề là **conflict thread/apartment khi 2 instance cùng DLL**.
|
||||
|
||||
### 3.2 Nexus: hang khi 2 instance — OK khi 1 thread
|
||||
|
||||
- `bridge_two` (2 worker STA, 2 instance Nexus): inst1 attach OK, **inst2 HANG 25s+** (xác nhận bằng run 25s).
|
||||
- `same_thread` (load + attach cùng 1 thread): **OK** (returned=1).
|
||||
- `main_own` (load+attach main thread): **OK** (RESULT done=1 attached_ok=1).
|
||||
- `two_workers_close` (đóng editor inst1 trước khi attach inst2, có closeGUI + attach qua temp thread): Nexus **HANG ngay attach inst1**; Kontakt chạy hết, OK cả 2.
|
||||
|
||||
→ 2 thread/apartment cùng 1 DLL = conflict thật (không riêng Nexus: 30 plugin hang
|
||||
inst2, 8 crash AV). Kontakt/Ozone/BlueARP chịu được.
|
||||
|
||||
### 3.3 Giả thuyết (a) reload mất state — XÁC NHẬN từ code
|
||||
|
||||
`Vst3Instrument::reload()` (Vst3Instrument.cpp:615): `closeGUI()` + terminate +
|
||||
`delete state_` + `loadPlugin(path_)` **MỚI** → preset/state mất khi mở lại GUI
|
||||
(reopen). Kích hoạt bởi `reloadForGUI()` khi `hasAttachedOnce_ && !guiAttached_`
|
||||
(đã từng mở GUI rồi đóng). Không phải nguyên nhân của bug câm lần này (attach đầu
|
||||
chưa thành công nên chưa tới reload), nhưng là bug phụ: **mở GUI → đóng → mở lại =
|
||||
mất preset/state của track**.
|
||||
|
||||
## 4. Kết luận nguyên nhân
|
||||
|
||||
1. **Root cause**: attach GUI chạy worker thread trong khi audio loop `process()`
|
||||
đang chạy instance cùng DLL (2+ thread 1 DLL). Nexus throw → attach fail.
|
||||
Probe không audio: biểu hiện hang (inst2); app thật có audio: biểu hiện
|
||||
exception (ch5). Cùng gốc.
|
||||
2. **Khuếch đại**: frontend retry spam 30× → stall 3s → watchdog tưởng bridge chết
|
||||
→ restart → mất hết instrument → câm. Đây là lý do dự án chết hoàn toàn, không
|
||||
chỉ mất GUI.
|
||||
3. `g_loadMutex` + silence hiện có chưa đủ: không đợi renderAll thoát hẳn instance
|
||||
cùng DLL trước khi `createView`, và Nexus vẫn throw ngay cả khi đã silence
|
||||
(silence chỉ ngăn process gọi tiếp, không chặn instance đang nằm trong process()).
|
||||
|
||||
## 5. Đề xuất fix (xếp theo ưu tiên)
|
||||
|
||||
1. **Chặn/defer OPEN_GUI khi transport PLAY** — đơn giản nhất, an toàn nhất:
|
||||
nếu đang PLAY, đợi transport STOP (hoặc tự tạm dừng) trước khi attach, resume
|
||||
sau. Loại bỏ hoàn toàn conflict audio-loop × attachView. (`ponytail:` người dùng
|
||||
muốn mở GUI khi đang chơi thì nâng cấp thành pause-resume tự động, hoặc đợi
|
||||
`setProcessing(false)` + đợi renderAll thoát.)
|
||||
2. **Giới hạn retry**: bỏ `openVstGuiRetry` spam; thử tối đa 2-3 lần có backoff.
|
||||
3. **Watchdog đừng giết khi đang attach GUI**: phân biệt stall do GUI job dài (có
|
||||
attach-in-flight flag) với stall do bridge chết thật; nâng ngưỡng stall khi có
|
||||
attach đang chạy.
|
||||
4. **Fix state reset**: `reload()` nên save/restore state (`getState`/`setState`)
|
||||
thay vì load instance mới mất preset. (Bug phụ, độc lập.)
|
||||
5. (Dài hạn) 1 apartment chung cho mọi instance cùng DLL (`shared_worker` pattern
|
||||
đã có trong probe) — loại conflict nhiều-thread-1-DLL triệt để.
|
||||
|
||||
## 6. Files liên quan
|
||||
|
||||
- `native_bridge/src/main.cpp` — handleOpenGui (943), silence/close-other (1015-1090), attach job + catch (1132-1160)
|
||||
- `native_bridge/src/Vst3Instrument.cpp` — reload() (615), reloadForGUI() (528), attachView() (550), closeGUI() (640)
|
||||
- `native_bridge/src/gui_probe.cpp` — probe bridge_two (243), two_workers_close (318)
|
||||
- `native_bridge/debug/` — sweep_all_gui.py, sweep_bridge_two_8s.json, probe_attach_detail.py, probe_app4_live.py, probe_app4.py
|
||||
- Bridge log: `%AppData%/SonicForgeDAW/logs/bridge.log`
|
||||
|
||||
Trạng thái live hiện tại: app câm (bridge 21932 sống, không instrument, transport
|
||||
STOP). Test thêm bằng `probe_app4_live.py` sẽ lại làm hư session — cần xác nhận
|
||||
trước khi chạy tiếp.
|
||||
@@ -0,0 +1,113 @@
|
||||
# G0 — Baseline đo latency (kết quả)
|
||||
|
||||
Ngày: 2026-08-27. Probe: `native_bridge/debug/g0_latency_probe.py` (chạy với
|
||||
`timeBeginPeriod(1)`, 10k block/config, pipeline thật Python→SHM→fx_vst_bridge→SHM).
|
||||
Log: `native_bridge/debug/g0_probe_run_full.log` (run 1),
|
||||
`native_bridge/debug/g0_probe_run2.log` (run 2 — có `pump_rate` + drain đầy đủ).
|
||||
|
||||
## 1. Bảng latency plugin (getLatencySamples, @48kHz)
|
||||
|
||||
| Plugin | getLatencySamples | ms @48k | Ghi chú |
|
||||
|---|---|---|---|
|
||||
| Sonible pureComp | 2108 | 43.9 | báo latency thật (lookahead) |
|
||||
| iZotope Ozone 11 | 0 | 0 | không báo latency (realtime mode) |
|
||||
| empty (passthrough) | — | — | chain rỗng |
|
||||
|
||||
Nguồn: probe `plugin_latency` + log app thật `fxrt_bridge.log`
|
||||
(`[RenderFx] STEP getLatencySamples = 2108/0`).
|
||||
|
||||
## 2. Jitter round-trip (10k block, 53.3s/config)
|
||||
|
||||
Round-trip = thời gian từ lúc probe gọi `write_input(block j)` đến lúc nhận
|
||||
`read_output()` block j (FIFO theo index). Đã sửa pacing probe bằng
|
||||
`timeBeginPeriod(1)` (bridge C++ đã gọi — `RealtimeFxLoop.cpp:160`); lần chạy
|
||||
trước thiếu nó → Sleep 15.6ms granularity → pump ~22ms/batch ≠ 21.33ms realtime
|
||||
→ pureComp bị "stall 1.9s" giả. Với pacing chuẩn, stall đó biến mất — là
|
||||
artifact probe, KHÔNG phải VST stall.
|
||||
|
||||
| Config | mean ms | σ | p50 | p99 | max | recv/sent | pump_rate | drained |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| empty | 21.67 | 0.88 | 21.70 | 22.03 | 26.51 | 10000/10000 | 184.8 | ✅ |
|
||||
| pureComp | 21.69 | 0.15 | 21.67 | 22.03 | 24.07 | 10000/10000 | 184.9 | ✅ |
|
||||
| Ozone 11 | 21.70 | 0.14 | 21.68 | 22.02 | 22.20 | 10000/10000 | 184.8 | ✅ |
|
||||
|
||||
- Round-trip ≈ 21.5–21.7ms = đúng 1 batch 4 block (21.33ms) + overhead nhỏ
|
||||
(SHM copy + bridge proc 0.001–0.45ms). Latency plugin KHÔNG nằm trong
|
||||
round-trip — plugin latency bù bằng PDC (không chặn pipeline).
|
||||
- **`drained=True` mọi config (run 2): bridge xử lí HẾT 10000 block, recv = sent
|
||||
— KHÔNG mất block, KHÔNG backlog tích lũy.** Đây là bằng chứng quyết định:
|
||||
không có drift thật (xem §4).
|
||||
- σ round-trip: 0.14–0.15ms (pureComp/Ozone) — RẤT nhỏ → **Gate G2 MỞ**: PDC
|
||||
mẫu có ý nghĩa, không cần giảm jitter trước (không cần relay WS→SHM sớm).
|
||||
- empty σ=0.88: 1 outlier khởi động (min=0.05ms), p50/p99 vẫn 21.7/22.0.
|
||||
|
||||
## 3. Inter-arrival deviation
|
||||
|
||||
| Config | σ ms | min | max |
|
||||
|---|---|---|---|
|
||||
| empty | 9.44 | −5.33 | 38.4 |
|
||||
| pureComp | 9.36 | −5.33 | 23.5 |
|
||||
| Ozone 11 | 9.36 | −5.33 | 17.8 |
|
||||
|
||||
Block về theo batch 4 (bridge xử lí take=4/lần) → inter-arrival lệch so với
|
||||
5.33ms lý tưởng là đặc tính batching, không phải jitter ngẫu nhiên. Round-trip
|
||||
σ (§2) là thước đo đúng cho PDC; inter-arrival dùng cho sizing FILL — xem §5.
|
||||
|
||||
## 4. Bridge FxRTPerf
|
||||
|
||||
| Nguồn | procAvg | procMax | loopAvg | loopMax | rate blk/s | vs realtime 187.5 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| probe empty | 0.001ms | 0.06ms | 22.0ms | 1017ms | 181.5 | −3.2% |
|
||||
| probe pureComp | 0.46ms | 1.37ms | 22.0ms | 1011ms | 181.6 | −3.1% |
|
||||
| probe Ozone | 0.14ms | 0.71ms | 22.0ms | 1015ms | 181.5 | −3.2% |
|
||||
| app thật (master chain đầy) | 3.4ms | 10.0ms | 21.4ms | 156ms | 186.5 | −0.5% |
|
||||
|
||||
- **Rate "chậm 3% (probe) / 0.5% (app)" KHÔNG phải drift thật — artifact đo.**
|
||||
Bằng chứng: run 2 `drained=True`, recv=sent=10000 mọi config — nếu bridge
|
||||
thật sự chậm 3%, backlog 300+ block phải tích lại và drain phải mất >1.5s
|
||||
sau pump (thực tế drain xong ngay trong vòng lặp pump, `pump_rate` chỉ thấp
|
||||
vì probe tự sleep lệch pha với nhịp bridge poll 1ms + granularity sleep).
|
||||
Bridge xử lí ASAP khi có input (`avail>0` → process ngay) — không có pace
|
||||
riêng để "chậm"; output rate = input rate. App thật rate 186.5 thấp hơn vì
|
||||
runSec gồm idle trước khi audio chảy (chain load, chờ play).
|
||||
- **loopMax ≈ 1s ở MỌI config probe kể cả empty** — spike 1s/lần chạy ~53s,
|
||||
không phải VST (empty cũng có). Không xuất hiện trong log app thật gần nhất
|
||||
(loopMax 156ms) — nghi Windows scheduler/antivirus/power, cần theo dõi khi
|
||||
chạy thật. Probe round-trip max vẫn ≤27ms → spike xảy ra lúc bridge idle,
|
||||
không chặn block đang xử lí (không gây underrun).
|
||||
- **Kết luận: KHÔNG cần bù drift.** FILL 8 block + underrun-fade/resume hiện
|
||||
có đủ — u=3/session (fx_stats) hiếm, do main-thread stall (GUI), không phải
|
||||
drift đều. Đề xuất resync/bù rate ban đầu rút lại (xem §6).
|
||||
|
||||
## 5. ~0.22s gồm gì (xác nhận)
|
||||
|
||||
Latency path thực đo + hằng số app:
|
||||
|
||||
| Thành phần | ms | Nguồn |
|
||||
|---|---|---|
|
||||
| Batching 4 block (pipeline) | ~21.6 | probe round-trip (§2) |
|
||||
| FILL buffer worklet 8 block | 42.7 | `sf-fx-realtime.js` FILL=8 |
|
||||
| Plugin latency (pureComp) | 43.9 | `getLatencySamples=2108` @48k |
|
||||
| WS/main-thread jitter (warm, p50) | ~101 | dbg.log `fx_play_first` sincePlay 38–160ms |
|
||||
| **Tổng nghe thấy (warm)** | **~210ms ≈ 0.21s** | khớp ~0.22s quan sát |
|
||||
|
||||
- PDC hiện tại `FX_RT_ROUNDTRIP_SAMPLES=3840` (80ms) = batching ~7 block +
|
||||
FILL 8 block — khớp thực đo (21.6 + 42.7 ≈ 64ms + WS jitter margin). Giữ
|
||||
nguyên; `latencyTotal` (plugin thật qua `{cmd:'latency'}`) cộng thêm đúng
|
||||
(pureComp +43.9ms).
|
||||
- Cold start VST load: 8–17s (dbg.log sincePlay 8111–17211ms), warm sau đó
|
||||
38–160ms — là plugin init, không phải latency pipeline; không nằm trong PDC.
|
||||
- fx_stats app thật (session master): u=3 h=3 f=13 s=13 cap=1 fi=13 q=5–11 —
|
||||
underrun hiếm, queue duy trì 5–11 block → FILL đủ hấp thụ jitter thường.
|
||||
|
||||
## 6. Kết luận cho G2+
|
||||
|
||||
1. **Gate G2: MỞ.** σ round-trip 0.14–0.15ms → PDC mẫu có nghĩa; không cần
|
||||
relay WS→SHM trước G2.
|
||||
2. **Không có drift bridge** (đã kiểm chứng run 2: drained=True, recv=sent).
|
||||
Không cần bù rate/resync — FILL 8 + underrun-fade/resume đủ. Tránh thêm
|
||||
code không cần thiết. Nếu sau này thấy underrun đều trên session dài →
|
||||
đo lại `pump_rate`/`drained` trước khi kết luận drift.
|
||||
3. **loopMax ~1s spike** (probe, cả empty): theo dõi, chưa thấy ở app thật.
|
||||
4. Plugin latency bảng §1 đủ cho G2: `graph.getLatencySamples()` sẽ thay hằng
|
||||
số; dữ liệu hiện tại xác nhận pureComp là plugin duy nhất có latency thật.
|
||||
@@ -0,0 +1,56 @@
|
||||
# G1 — JUCE engine SHM chain rỗng POC (kết quả)
|
||||
|
||||
Ngày: 2026-08-28. Commit: `9c0d35d`. Plan: `JUCE_LATENCY_PLAN.md` G1.
|
||||
|
||||
## Nội dung
|
||||
|
||||
- Submodule `native_bridge/JUCE` = tag `8.0.8`... thực tế tag `8.0.7`
|
||||
(shallow, `.gitmodules` branch=8.0.7) — khớp pattern vst3sdk sẵn có.
|
||||
- Tách SHM dùng chung: `native_bridge/include/FxShm.h` (namespace `fxshm`,
|
||||
`ShmView`/`openShm`/`closeShm` — Windows FileMapping); `RealtimeFxLoop.cpp`
|
||||
chuyển sang dùng chung (không đổi hành vi bridge cũ).
|
||||
- Project mới `native_bridge/juce_fx/`: `juce_fx_bridge` dùng
|
||||
`juce::AudioProcessorGraph`, chain rỗng = input node → output node
|
||||
(passthrough). `prepare(sr, block)` → prepareToPlay(block*4) vì batch tối đa
|
||||
4 block; `process(inL, inR, n)` copy vào `AudioBuffer` → `graph.processBlock`
|
||||
→ copy về in-place. `latencySamples()` = `graph.getLatencySamples()`.
|
||||
- Loop C++: batching 4 block, out-ring backpressure, heartbeat 100ms,
|
||||
`timeBeginPeriod(1)` + ABOVE_NORMAL, log `[JuceFxLoop]/[JuceFxPerf]`; validate
|
||||
header mỗi iteration — sampleRate/blockSize đổi → re-prepare + report latency
|
||||
qua lat ring; job `fx_chain` BỎ QUA (G1 chain rỗng).
|
||||
- Python: `app/core/fx_realtime.py` `_spawn()` — env `SF_JUCE_FX_BRIDGE_PATH`
|
||||
chọn bridge JUCE (`--juce-fx`); mặc định giữ nguyên `fx_vst_bridge
|
||||
--realtime-fx`. Exe copy: `src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe`.
|
||||
|
||||
## Kết quả probe
|
||||
|
||||
Probe `native_bridge/debug/g0_latency_probe.py`, 10k block/config, pipeline
|
||||
thật Python→SHM→juce_fx_bridge→SHM, `timeBeginPeriod(1)`, env
|
||||
`SF_JUCE_FX_BRIDGE_PATH` trỏ exe JUCE. (3 config trong probe đều chạy qua JUCE
|
||||
bridge; G1 chain rỗng nên pureComp/Ozone cũng passthrough — kiểm chứng không
|
||||
crash, không lag.)
|
||||
|
||||
| Config | mean ms | σ | p50 | p99 | max | recv/sent | pump_rate | drained | latency |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| empty | 21.670 | 0.636 | 21.649 | 22.087 | 24.031 | 10000/10000 | 184.9 | ✅ | {0:0} |
|
||||
| pureComp | 21.686 | 0.168 | 21.656 | 22.073 | 23.399 | 10000/10000 | 184.9 | ✅ | {0:0} |
|
||||
| Ozone 11 | 21.674 | 0.157 | 21.640 | 22.049 | 22.277 | 10000/10000 | 185.0 | ✅ | {0:0} |
|
||||
|
||||
So sánh baseline G0 (empty): mean 21.67ms — không lag thêm, không crackle
|
||||
(drained=True, recv=sent, interarrival mean ~0.07ms). Log bridge:
|
||||
`[JuceFxLoop] prepared sr=48000 block=256 latency=0` → ready → exit.
|
||||
|
||||
## G1 done-when
|
||||
|
||||
- ✅ `--juce-fx` chạy với chain rỗng
|
||||
- ✅ Python `fx_realtime.py` chơi qua nó bằng path cũ (env opt-in, mặc định cũ
|
||||
không đổi) — regression pass
|
||||
|
||||
## Ghi chú
|
||||
|
||||
- Lỗi build gặp phải: `NodeAndChannel`/`Connection` không aggregate-init được
|
||||
trên MSVC 2019 — phải gán field từng cái; `addNode` trả `Node::Ptr` → lấy
|
||||
`->nodeID` (không gán Ptr vào `NodeID`).
|
||||
- JUCE modules build lần đầu chậm (~34s config); `build_juce/` đã gitignore.
|
||||
- `[JuceFxPerf]` heartbeat không xuất hiện trong probe ngắn (chạy <100ms);
|
||||
probe chỉ grep tag cũ `[FxRTPerf]` — không phải lỗi.
|
||||
@@ -0,0 +1,58 @@
|
||||
# G3 — 1 clock: PDC latency thật + playhead engine-derived (kết quả)
|
||||
|
||||
Ngày: 2026-08-28. Plan: `JUCE_LATENCY_PLAN.md` G3. Option A — 1 clock.
|
||||
|
||||
## Nội dung
|
||||
|
||||
- **Bridge báo processed samples** qua lat ring slot đặc biệt `0xFFFFFFFE`
|
||||
(`JuceFxLoop.cpp`, mỗi block sau `++processed`): `samples = processed*n`
|
||||
(n=curBlock). Python `_lat_drain` tách slot này riêng → `self.processed`
|
||||
(KHÔNG vào `latencies` dict — tổng chain không đổi). Thêm `get_processed()`.
|
||||
- **WS latency message thêm `processed`** (`plugins.py`): gửi khi `processed`
|
||||
đổi (mỗi block ~5.33ms), bỏ gate chỉ theo total — total chỉ đổi khi chain đổi.
|
||||
- **Playhead engine-derived** (`app.jsx`): khi FX active và có anchor,
|
||||
`position = startOffset + (processed - anchor - latencyTotal)/sr` (đã trừ
|
||||
latency → bám vị trí mẫu NGHE được; pre-roll tự giữ tại startOffset qua
|
||||
clamp). Anchor = raw processed tại play/wrap start (`fxRtAnchorProcessed`
|
||||
gọi từ `__fxRtPlayheadFreeze`, mọi play/resume path đều đi qua). Bỏ
|
||||
freeze/snap khi active; fallback wall-clock khi không active. Áp dụng 4 chỗ:
|
||||
bridge position block, sub-tab `_ph`, sub-tab `wallTime`, main branch
|
||||
`_playheadSec` → `setCurrentTime`.
|
||||
- **Worklet nhận latency thật** qua port message `{type:'latency', samples}`;
|
||||
`clear_queue('loop')`: `wrapFadeBlocks = outQueue.length +
|
||||
ceil(latencySamples/BLOCK)` — align processed-tail, crossfade đúng seam
|
||||
end→start kể cả khi FX latency (pureComp 2108 mẫu) còn in-flight trong bridge.
|
||||
|
||||
## Verify (đã chạy)
|
||||
|
||||
1. **Build**: `cmake --build native_bridge/build_juce --config Release --target
|
||||
juce_fx_bridge` OK. Copy `src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe`,
|
||||
SHA256 khớp bản build (`07621b81...`).
|
||||
2. **Probe SHM thật** (`native_bridge/debug/g3_probe_check.py`, JUCE bridge qua
|
||||
`SF_JUCE_FX_BRIDGE_PATH`, chain pureComp, SR 48k, 400 block ~2.1s):
|
||||
- `latencies` dict KHÔNG đổi: `{0: 2108}` trước/sau — processed tách riêng
|
||||
không làm sai tổng chain.
|
||||
- `processed` tiến 0 → 102400 (= 400 block × 256 mẫu) — bridge báo đúng.
|
||||
3. **Cú pháp**: `python -m py_compile` fx_realtime.py + plugins.py OK;
|
||||
`node --check` worklet OK; `babel` (node_modules) parse app.jsx OK (node
|
||||
thuần không hiểu JSX — dùng babel, exit 0).
|
||||
4. **Fake session** (object.__new__, không shm): `get_processed()` → None,
|
||||
`get_latencies()` → `{0: 2108}` — path không-SHM an toàn.
|
||||
|
||||
## Done-when trạng thái
|
||||
|
||||
- ✅ processed tách riêng, tổng latency chain không đổi (probe)
|
||||
- ✅ playhead công thức engine-derived đã cài đủ 4 chỗ; freeze/snap vô hiệu khi
|
||||
active (code path)
|
||||
- ⏳ **Chưa verify bằng tai**: 100-pass loop không click với crossfade tối
|
||||
thiểu; playhead bám vị trí mẫu trong app thật (cần user chạy GUI + nghe).
|
||||
|
||||
## Ghi chú
|
||||
|
||||
- Probe check script giữ ở `native_bridge/debug/g3_probe_check.py` (chạy lại:
|
||||
`python native_bridge/debug/g3_probe_check.py`).
|
||||
- BufferError lúc thoát (`cannot close exported pointers`) là quirk teardown
|
||||
shared_memory Python 3.13, không ảnh hưởng kết quả.
|
||||
- Latency WS ~5.33ms/msg = 187 msg/s — nhỏ so với pump audio; nếu cần tiết
|
||||
kiệm sau này: batch processed 4 block/lần (giảm 4×) — `ponytail: chỉ làm khi
|
||||
có bằng chứng quá tải`.
|
||||
@@ -1,50 +0,0 @@
|
||||
# Nexus GUI crash 0xc000041d (USER32) — debug state (WIP)
|
||||
|
||||
Thời điểm lưu: 2026-08-14, đang chuyển sang máy Linux để tiếp tục.
|
||||
|
||||
## Hiện tượng
|
||||
`daw_vst_bridge.exe` crash 0xc000041d khi mở GUI editor VSTi Nexus (`C:\Program Files\Common Files\VST3\Nexus.vst3`) rồi chọn instrument khác. Chạy từ `install/`.
|
||||
|
||||
## Kết quả đã có
|
||||
- Probe `gui_probe.exe` variant `bridge_exact11` PASS (`RESULT(bridge_exact11): done=1 attached_ok=1`, log: `openGUI: attached=0` = kResultOk), kể cả khi stdout redirect ra file.
|
||||
- NHƯNG bridge thật HANG tại `view->attached()` (log dừng tại `isPlatformTypeSupported=0`, không in `attached=`), kể cả SF_ONCE_PROBEWIN=1. Watchdog tick vẫn chạy = đang trong attached().
|
||||
|
||||
## Đã loại trừ (session 2026-08-14)
|
||||
- **g_engine hypothesis DISPROVEN**: `g_engine` (main.cpp L545) KHÔNG được dùng trong path SF_ONCE (chỉ dùng ở closeGUI/main-loop). Job SF_ONCE chỉ gọi `instruments.setReloading(y,true)` → `Vst3Instrument::setReloading` (chỉ set bool) + `reloadForGUI()` (hasAttachedOnce_=false lần đầu → no-op return true).
|
||||
- **Diff ChannelWorker vs PumpWorker**: giống hệt (chỉ khác tên biến + destructor + comment).
|
||||
- **Diff exact11 vs SF_ONCE**: giống hệt về ngữ nghĩa — chỉ khác sampleRate/block, setEditorOpenPredicate (cả hai false), SHM driver thật vs tự tạo (đã loại bằng SF_ONCE_SHM).
|
||||
- **stdout redirect ra file**: probe vẫn PASS.
|
||||
- **Binary up-to-date** (build 22:09 = main.cpp 22:09).
|
||||
- CMakeLists: bridge và gui_probe compile CÙNG sources, CÙNG defines (HAVE_VST3SDK=1), cùng libs — không khác.
|
||||
|
||||
## Stack main thread (stackscan.py v7/v8)
|
||||
- Main thread chờ vô hạn trong ntdll (R9=0x7ffffffffffffffc timeout ~infinite) qua KERNELBASE+22cd8 gọi từ Nexus attached().
|
||||
- 3 thread Nexus riêng chờ ntdll+164034; 1 thread (21232) là message pump (win32u+20a4).
|
||||
- Export resolve thất bại (offset nội bộ không tên): ntdll+5fc6e→hàm tại 0x5fa90, ntdll+3e732→0x3dc60, KERNELBASE+22cd8→hàm tại 0x22ca0.
|
||||
|
||||
## Bước tiếp theo
|
||||
1. **waitscan.py** (chạy dở khi lưu): dump RIP mọi thread + NtQueryObject loại/tên handle trong Rcx (wait object) — xem main thread và 3 thread Nexus có CÙNG chờ 1 object (deadlock nội bộ Nexus) hay không. Chạy: `python waitscan.py` (cần bridge đang hang).
|
||||
2. Nếu waitscan không rõ: disasm KERNELBASE+22ca0 / ntdll+5fa90 xác định hàm chờ (WaitForSingleObjectEx/WaitForMultipleObjectsEx/AlertableWait), hoặc NtQuerySystemInformation wait-chain.
|
||||
3. Khi tìm được trigger: REVERT toàn bộ TEST patch (watchdog, direct attach, no audio, DefWindowProcA, warm-up, pre-window, autogui, exact11, once, once_shm, probewin) — giữ fix chính thức. Đặc biệt:
|
||||
- `wc.lpfnWndProc = DefWindowProcA; // TEST ISOLATION`
|
||||
- bỏ `WS_VISIBLE` ở create_native_vst_window
|
||||
- khôi phục VstWindowProc + WS_VISIBLE khi hết test.
|
||||
4. Verify app thật từ `install/` (kill SonicForge/daw_vst trước; check `%APPDATA%\SonicForgeDAW\logs\bridge.log` + spawn.log, không "bridge stalled 3s"). Deploy exe 5 vị trí: install/, src-tauri/binaries/ (2 tên), src-tauri/target/{debug,release}/.
|
||||
|
||||
## Cách chạy (Windows)
|
||||
- Build: `cd native_bridge && cmake --build build --config Release --target daw_vst_bridge --parallel` (nhớ `taskkill //F //IM daw_vst_bridge.exe` trước, LNK1104 nếu zombie).
|
||||
- Probe: `./build/Release/gui_probe.exe "C:\Program Files\Common Files\VST3\Nexus.vst3" <variant> <secs>`; exit 127 dù PASS; watchdog tự kill sau secs+10s.
|
||||
- Driver: `python driver_once.py [GATE=VAL...]` (chờ "SF_ONCE attach=" 90s + "GUI attached" 30s; attach=0 = THÀNH CÔNG).
|
||||
|
||||
## Scripts trong thư mục này
|
||||
- `waitscan.py` — dump RIP + wait object mọi thread (đang dở, bước 1 tiếp theo).
|
||||
- `stackscan.py` / `stackwalk.py` / `stack_sample*.py` — stack dump.
|
||||
- `driver_once.py` / `driver_verify.py` / `driver_autogui.py` / `driver_narrow.py` — chạy bridge với gate, chờ kết quả.
|
||||
- Patch scripts (workspace tmp-7d619832): `patch_once.py`, `patch_once_shm.py`, `patch_exact11_bridge.py`, `patch_gates.py`, `patch_autogui.py`, ... áp bằng python lên `native_bridge/src/main.cpp` (CRLF! ghi `io.open(encoding='utf-8', newline='\r\n')`).
|
||||
|
||||
## Learnings kỹ thuật (Windows ctypes)
|
||||
- `GetModuleHandleW`/windll trả pointer → PHẢI set `.restype = ctypes.c_void_p` (mặc định c_int → truncate 32-bit → access violation).
|
||||
- Mọi API nhận pointer/handle → set argtypes (GetThreadContext, OpenThread, DuplicateHandle...).
|
||||
- `ctypes.wintypes` KHÔNG có DWORD64 — dùng ctypes.c_uint64.
|
||||
- CONTEXT x64: SegCs..SegSs là WORD; struct đầy đủ 0x4d0 bytes; GetThreadContext ghi full buffer → dùng raw buffer 0x4d0 + struct.unpack_from offset cố định (rip@0xF8, rsp@0x98, rcx@0x80, rdx@0x88, r8@0xB8, r9@0xC0, ContextFlags@0x30).
|
||||
- EnumProcessModulesEx: cb = bytes/8 trên x64; module handle có thể NULL → skip.
|
||||
@@ -0,0 +1,6 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
cd 'C:/Users/locpham/SonicForgeStudio/native_bridge/build'
|
||||
cmake -DCMAKE_CXX_FLAGS_RELEASE='/O2 /Ob2 /DNDEBUG /Zi' -DCMAKE_EXE_LINKER_FLAGS_RELEASE='/DEBUG' .
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
cmake --build . --config Release --target daw_vst_bridge
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
|
||||
cd /d C:\Users\locpham\SonicForgeStudio\native_bridge
|
||||
cl /nologo /std:c++17 /EHsc /I include /I vestige tests\shm_selfcheck.cpp /Fe:debug\shm_selfcheck_new.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
debug\shm_selfcheck_new.exe
|
||||
@@ -0,0 +1,6 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
cd 'C:/Users/locpham/SonicForgeStudio/native_bridge/build'
|
||||
cmake -DCMAKE_CXX_FLAGS_RELEASE='/O2 /Ob2 /DNDEBUG /Zi' -DCMAKE_EXE_LINKER_FLAGS_RELEASE='/DEBUG' .
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
cmake --build . --config Release --target gui_probe
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,760 @@
|
||||
|
||||
⨪⨪⨪⨪⨪⨪牐灥牡湩桴湥楶潲浮湥⁴潦敄畢杧牥䔠瑸湥楳湯慇汬牥⁹敲潰楳潴楲獥⨠⨪⨪⨪⨪⨪⨪�
|
||||
†䔠瑸湥楳湯敒潰楳潴祲㨠䤠灭楬楣�
|
||||
†唠敳硅数楲敭瑮污敆瑡牵䙥牯畎敧却慨敲㨠琠畲�
|
||||
†䄠汬睯畎敧䕴數灕慤整㨠琠畲�
|
||||
†丠湯湉整慲瑣癩乥杵瑥㨠琠畲�
|
||||
†䄠汬睯畎敧䵴䍓敲敤瑮慩偬潲楶敤䥲獮慴汬㨠琠畲�
|
||||
†䄠汬睯慐慲汬汥湉瑩慩楬慺楴湯晏潌慣剬灥獯瑩牯敩›牴敵
|
||||
|
||||
†ⴠ潃普杩牵湩敲潰楳潴楲獥
|
||||
†††ⴭⴭ‾敒潰楳潴祲㨠䰠捯污湉瑳污敬Ɽ䔠慮汢摥›牴敵
|
||||
†††ⴭⴭ‾敒潰楳潴祲㨠唠敳䕲瑸湥楳湯ⱳ䔠慮汢摥›牴敵
|
||||
|
||||
㸾㸾㸾㸾㸾㸾‾牐灥牡湩桴湥楶潲浮湥⁴潦敄畢杧牥䔠瑸湥楳湯慇汬牥⁹敲潰楳潴楲獥挠浯汰瑥摥畤慲楴湯〠〮〰猠捥湯獤
|
||||
|
||||
⨪⨪⨪⨪⨪⨪慗瑩湩潦敄畢杧牥䔠瑸湥楳湯慇汬牥⁹潴䤠楮楴污穩⨪⨪⨪⨪⨪⨪⨪
|
||||
|
||||
㸾㸾㸾㸾㸾㸾‾慗瑩湩潦敄畢杧牥䔠瑸湥楳湯慇汬牥⁹潴䤠楮楴污穩潣灭敬整Ɽ搠牵瑡潩⸰‶敳潣摮�
|
||||
†ⴠⴭ㸭删灥獯瑩牯⁹›獕牥硅整獮潩獮湅扡敬㩤琠畲ⱥ倠捡慫敧潣湵㩴〠
|
||||
†ⴠⴭ㸭删灥獯瑩牯⁹›潌慣䥬獮慴汬摥湅扡敬㩤琠畲ⱥ倠捡慫敧潣湵㩴㐠�
|
||||
|
||||
楍牣獯景⁴刨
楗摮睯敄畢杧牥嘠牥楳湯ㄠ⸰⸰㤲ㄶ⸷〱〰䄠䑍㐶
|
||||
潃祰楲桧⁴挨
楍牣獯景⁴潃灲牯瑡潩䄠汬爠杩瑨敲敳癲摥�
|
||||
|
||||
潃浭湡䱤湩㩥⸠杜極灟潲敢攮數∠㩃停潲牧浡䘠汩獥䍜浯潭楆敬屳卖㍔乜硥獵瘮瑳∳猠慨敲彤潷歲牥ㄠ�
|
||||
|
||||
⨪⨪⨪⨪⨪⨪慐桴瘠污摩瑡潩畳浭牡⁹⨪⨪⨪⨪⨪⨪⨪
|
||||
敒灳湯敳††††††††††††吠浩洨⥳††䰠捯瑡潩�
|
||||
敄敦牲摥†††††††††††††††††††猠癲�
|
||||
祓扭汯猠慥捲慰桴椠㩳猠癲�
|
||||
硅捥瑵扡敬猠慥捲慰桴椠㩳�
|
||||
|
||||
ⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭ⬭
|
||||
⁼桔獩琠牡敧⁴畳灰牯獴䠠牡睤牡ⵥ湥潦捲摥匠慴正倠潲整瑣潩䄠䠠⁗慢敳†簠
|
||||
⁼匢慨潤⁷瑓捡≫洠祡戠癡楡慬汢潴愠獳獩⁴湩搠扥杵楧杮愠摮愠慮祬楳†簠
|
||||
⁼敓歡獭甯敳桲灳映牯洠牯湩潦††††††††††††††††††簠
|
||||
⁼†††††††††††††††††††††††††††††††††††簠
|
||||
⁼灤獀灳†††††††††††††††††††††††††††††††簠
|
||||
⁼†††††††††††††††††††††††††††††††††††簠
|
||||
ⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭ⬭
|
||||
|
||||
潍䱤慯㩤〠〰㜰晦怷散㙤〰〰〠〰㜰晦怷晣戳〶〰†朠極灟潲敢攮數
|
||||
潍䱤慯㩤〠〰㜰晦恦敥㘱〰〰〠〰㜰晦恦敥挳〶〰†渠摴汬搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦摥㉡〰〰〠〰㜰晦恦摥敡〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳䭜剅䕎㍌⸲䱄�
|
||||
潍䱤慯㩤〠〰㜰晦恦扥挷〰〰〠〰㜰晦恦扥扢て〰†䌠尺䥗䑎坏屓祓瑳浥㈳䭜剅䕎䉌十⹅汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦敥攰〰〰〠〰㜰晦恦敥ㄱ〲〰†䌠尺䥗䑎坏屓祓瑳浥㈳䥜䵍㈳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扥戴〰〰〠〰㜰晦恦扥搴〷〰†䌠尺䥗䑎坏屓祓瑳浥㈳睜湩㈳汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦捥攱〰〰〠〰㜰晦恦捥愳〶〰†䌠尺䥗䑎坏屓祓瑳浥㈳啜䕓㍒⸲汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦捥㐶〰〰〠〰㜰晦恦捥㘶ぢ〰†䌠尺䥗䑎坏屓祓瑳浥㈳䝜䥄㈳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扥摢〰〰〠〰㜰晦恦扥晣〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳杜楤㈳畦汬搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恥㝣㤴〰〰〠〰㜰晦恥㝣㜵〴〰†䌠尺獕牥屳潬灣慨屭潓楮䙣牯敧瑓摵潩湜瑡癩彥牢摩敧扜極摬剜汥慥敳汜扩汦極獤湹桴㌭搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扥㜳〰〰〠〰㜰晦恦扥ㄴ〳〰†䌠尺䥗䑎坏屓祓瑳浥㈳浜癳灣睟湩搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扥攴〰〰〠〰㜰晦恦扥㈶っ〰†䌠尺䥗䑎坏屓祓瑳浥㈳畜牣扴獡汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦捥慦〰〰〠〰㜰晦恦摥㈷〸〰†䌠尺䥗䑎坏屓祓瑳浥㈳卜䕈䱌㈳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦摥㠸〰〰〠〰㜰晦恦摥ㅡ〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳潜敬㈳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦捥㍡〰〰〠〰㜰晦恦捥扡〲〰†䌠尺䥗䑎坏屓祓瑳浥㈳坜㉓㍟⸲汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦捥愶〰〰〠〰㜰晦恦捥㉡〵〰†䌠尺䥗䑎坏屓祓瑳浥㈳捜浯慢敳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦挱㍤〰〰〠〰㜰晦恦挱捤〴〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜体乕⹄汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㑥搸〰〰〠〰㜰晦恦㑥〹〶〰†䌠尺䥗䑎坏屓奓呓䵅㈳坜义䵍搮汬
|
||||
潍䱤慯㩤〠〰〰㌲恤㌲㌴〰〰〠〰〰㌲恤㌲㐵〸〰†䌠尺䥗䑎坏屓祓瑳浥㈳剜䍐呒⸴汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦捥㝢〰〰〠〰㜰晦恦捥㡣〸〰†䌠尺䥗䑎坏屓祓瑳浥㈳剜䍐呒⸴汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦扥㉦〰〰〠〰㜰晦恦扥摦っ〰†䌠尺䥗䑎坏屓祓瑳浥㈳慜癤灡㍩⸲汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦挱㝣〰〰〠〰㜰晦恦挱㉤〷〰†䌠尺䥗䑎坏屓奓呓䵅㈳剜獥浡汰䑥潭䐮䱌
|
||||
潍䱤慯㩤〠〰㜰晦恦捥㥣〰〰〠〰㜰晦恦捥㍤〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳浜癳牣汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㉣愵〰〰〠〰㜰晦恦㉣㌶つ〰†䌠尺䥗䑎坏屓奓呓䵅㈳䵜噓偃㐱⸰汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㉣㐶〰〰〠〰㜰晦恦㉣㘶っ〰†䌠尺䥗䑎坏屓奓呓䵅㈳噜剃乕䥔䕍㐱⸰汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦捥捥〰〰〠〰㜰晦恦捥㥦〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳作䕌啁㍔⸲汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦捥捡〰〰〠〰㜰晦恦捥㙢ち〰†䌠尺䥗䑎坏屓祓瑳浥㈳獜捥潨瑳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦㑤昲〰〰〠〰㜰晦恦㑤昲っ〰†䌠尺䥗䑎坏屓奓呓䵅㈳噜剃乕䥔䕍㐱弰⸱汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦摥㌷〰〰〠〰㜰晦恦摥㈸〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳獜捨牯汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦ㅡ㘸〰〰〠〰㜰晦恦ㅡ愸〳〰†䌠尺䥗䑎坏屓奓呓䵅㈳䵜噓偃㐱弰⸲汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㌷㜱〰〰〠〰㜰晦恦㌷㜱づ〰†䌠尺䥗䑎坏屓奓呓䵅㈳浜摳潭搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扥搱〰〰〠〰㜰晦恦扥㈲づ〰†䌠尺䥗䑎坏屓奓呓䵅㈳灜睯灲潲汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㡣昲〰〰〠〰㜰晦恦㡣㈳〷〰†䌠尺䥗䑎坏屓奓呓䵅㈳睜湩浭慢敳搮汬
|
||||
潍䱤慯㩤〠〰〰㌲恤㌲㌴〰〰〠〰〰㌲恤㌲㠴づ〰†䌠尺䥗䑎坏屓奓呓䵅㈳灜睯灲潲汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦慥摥〰〰〠〰㜰晦恦慥㉦〲〰†䌠尺䥗䑎坏屓奓呓䵅㈳捜杦杭㍲⸲汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦扥戱〰〰〠〰㜰晦恦扥挱〵〰†䌠尺䥗䑎坏屓奓呓䵅㈳啜偍䍄搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦慥㈰〰〰〠〰㜰晦恦慥㌰ぢ〰†䌠尺䥗䑎坏屓奓呓䵅㈳歜牥敮灡捰牯汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦扥〷〰〰〠〰㜰晦恦扥愷っ〰†䌠尺䥗䑎坏屓祓瑳浥㈳扜牣灹側楲業楴敶汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㡥愴〰〰〠〰㜰晦恦㡥㐵〸〰†䌠尺䥗䑎坏屓祳瑳浥㈳畜瑸敨敭搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦捥㜰〰〰〠〰㜰晦恦捥挱つ〰†䌠尺䥗䑎坏屓祓瑳浥㈳䵜䍓䙔搮汬
|
||||
睯䡮湷㵤〰〰〰〰〰䍁〱䔲
|
||||
潬摡湩㩃停潲牧浡䘠汩獥䍜浯潭楆敬屳卖㍔乜硥獵瘮瑳�
|
||||
潍䱤慯㩤〠〰㜰晦恥㥣攱〰〰〠〰㜰晦恥慣攵〵〰†䌠尺牐杯慲楆敬屳潃浭湯䘠汩獥噜呓尳敎畸獶㍴
|
||||
潍䱤慯㩤〠〰㜰晦恦捥㜶〰〰〠〰㜰晦恦捥㤶〰〰†䌠尺䥗䑎坏屓祓瑳浥㈳楜慭敧汨汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦捥㑤〰〰〠〰㜰晦恦捥㍥〵〰†䌠尺䥗䑎坏屓祓瑳浥㈳䍜䵏䱄㍇⸲汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㡥搵〰〰〠〰㜰晦恦㡥〷ち〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜杸汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦摥扦〰〰〠〰㜰晦恦敥〶〰†䌠尺䥗䑎坏屓祓瑳浥㈳卜䱈䅗䥐搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扥㈴〰〰〠〰㜰晦恦扥愴〳〰†䌠尺䥗䑎坏屓祓瑳浥㈳坜义剔单⹔汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㕣〳〰〰〠〰㜰晦恦㕣戳〵〰†䌠尺䥗䑎坏屓楗卮卸慜摭㐶浟捩潲潳瑦眮湩潤獷挮浯潭潣瑮潲獬㙟㤵戵㐶㐱挴晣搱彦⸵㈸㈮ㄶ〰㠮㈳弸潮敮㡟攷摥㥥㜵㉡捣摤層佃䍍䱔㈳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦㍣㌴〰〰〠〰㜰晦恦㍣挶〵〰†䌠尺䥗䑎坏屓奓呓䵅㈳坜义义呅搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扤昶〰〰〠〰㜰晦恦扤昶ぢ〰†䌠尺䥗䑎坏屓奓呓䵅㈳噜剅䥓乏搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扥つ〰〰〠〰㜰晦恦扥㝥づ〰†䌠尺䥗䑎坏屓祓瑳浥㈳䍜奒呐㈳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扥晦〰〰〠〰㜰晦恦捥㘰〳〰†䌠尺䥗䑎坏屓祓瑳浥㈳坜䑌偁㈳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦捥㌶〰〰〠〰㜰晦恦捥㌶〸〰†䌠尺䥗䑎坏屓祓瑳浥㈳乜牯慭楬汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦扥㘲〰〰〠〰㜰晦恦扥㠲ち〰†䌠尺䥗䑎坏屓奓呓䵅㈳扜牣灹汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦慥㑡〰〰〠〰㜰晦恦慥㕡〳〰†䌠尺䥗䑎坏屓奓呓䵅㈳䵜䅓乓⸱汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㡥づ〰〰〠〰㜰晦恦㥥㤶ぢ〰†䌠尺䥗䑎坏屓奓呓䵅㈳睜湩潤獷献潴慲敧搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦㡥㜳〰〰〠〰㜰晦恦㡥㜳つ〰†䌠尺䥗䑎坏屓奓呓䵅㈳獜捥牵㈳搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦慥搲〰〰〠〰㜰晦恦慥ㄳ〹〰†䌠尺䥗䑎坏屓奓呓䵅㈳卜偓䍉䥌䐮䱌
|
||||
潍䱤慯㩤〠〰㜰晦恦㥥搹〰〰〠〰㜰晦恦㥥ち〳〰†䌠尺䥗䑎坏屓奓呓䵅㈳楜桰灬灡汤�
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
潍䱤慯㩤〠〰㜰晦恦慥戵〰〰〠〰㜰晦恦慥ㄶっ〰†䌠尺䥗䑎坏屓祳瑳浥㈳浜睳潳正搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦㥥㙡〰〰〠〰㜰晦恦㥥扢〸〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜华偁⹉汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦慥戳〰〰〠〰㜰晦恦慥戳つ〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜偓剁䕓搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦摥晡〰〰〠〰㜰晦恦摥晡ち〰†䌠尺䥗䑎坏屓祓瑳浥㈳乜䥓搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦扤晡〰〰〠〰㜰晦恦扤晡ぢ〰†䌠尺楗摮睯屳祓瑳浥㈳牜獡摡汨汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦摤㙢〰〰〠〰㜰晦恦摤敢〶〰†䌠尺䥗䑎坏屓祓瑳浥㈳晜灷捵湬汤�
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
〲㐺㨱㔰›剅⁒ⴠ䘠楡敬潴搠睯汮慯㩤栠瑴獰⼺愯瑵敲硦砮浯氯杯湩港⼴䌠畯摬渠瑯爠獥汯敶栠獯㩴愠瑵敲硦砮浯嬠崶
|
||||
潍䱤慯㩤〠〰㜰晦恦㥥㕥〰〰〠〰㜰晦恦㥥ㅦ〵〰†䌠尺䥗䑎坏屓祳瑳浥㈳獜档湡敮䱄�
|
||||
噛瑳䤳獮牴浵湥嵴氠慯敤㩃停潲牧浡䘠汩獥䍜浯潭楆敬屳卖㍔乜硥獵瘮瑳″丨硥獵
畯䍴㵨�
|
||||
獛慨敲彤潷歲牥⁝潬摡椠獮ㅴㄽ
|
||||
潍䱤慯㩤〠〰㜰晦恦㝥㔰〰〰〠〰㜰晦恦㝥㠶ち〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜搲⸱汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㙥敤〰〰〠〰㜰晦恦㝥㐰〷〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜牗瑩汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㝥㤶〰〰〠〰㜰晦恦㝥昸〰〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜搳ㄱ搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦攸〵〰〰〠〰㜰晦恦攸慡づ〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜䐳〱慗灲搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦㡥㠵〰〰〠〰㜰晦恦㡥挵〵〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜捸牯汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㡥扡〰〰〠〰㜰晦恦㡥捡〴〰†䌠尺䥗䑎坏屓奓呓䵅㈳牜獥畯捲灥汯捩捹楬湥汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦㕥㑣〰〰〠〰㜰晦恦㕥㥣て〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜物捥硴慤慴慢敳敨灬牥搮汬
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤱琠摩㘨㘹⤰㠠〰〷〰′桔祳瑳浥挠湡潮⁴楦摮琠敨映汩灳捥晩敩
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩摜物捥硴慤慴慢敳敨灬牥挮灰㈨㤲⤰摜物捥硴慤慴慢敳敨灬牥搮汬〡〰㜰䙆䕆䌵㈵ㅁ㩁⠠慣汬牥›〰〰䘷䙆㡅㔶㤴䔴
敒畴湲版㈨
楴⡤㤶〶
〸㜰〰㈰吠敨猠獹整慣湮瑯映湩桴楦敬猠数楣楦摥�
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤳琠摩㘨㘹⤰㠠〰〷〰′桔祳瑳浥挠湡潮⁴楦摮琠敨映汩灳捥晩敩
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤴琠摩㘨㘹⤰㠠〰〷〰′桔祳瑳浥挠湡潮⁴楦摮琠敨映汩灳捥晩敩
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤵琠摩㘨㘹⤰㠠〰〷〰′桔祳瑳浥挠湡潮⁴楦摮琠敨映汩灳捥晩敩
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤶琠摩㘨㘹⤰㠠〰〷〰′桔祳瑳浥挠湡潮⁴楦摮琠敨映汩灳捥晩敩
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤷琠摩㘨㘹⤰㠠〰〷〰′桔祳瑳浥挠湡潮⁴楦摮琠敨映汩灳捥晩敩
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤸琠摩㘨㘹⤰㠠〰〷〰′桔祳瑳浥挠湡潮⁴楦摮琠敨映汩灳捥晩敩
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤹琠摩㘨㘹⤰㠠〰〷〰′桔祳瑳浥挠湡潮⁴楦摮琠敨映汩灳捥晩敩
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲〱
楴⡤㤶〶
〸㜰〰㈰吠敨猠獹整慣湮瑯映湩桴楦敬猠数楣楦摥�
|
||||
潍䱤慯㩤〠〰㜰晦恦慥㑢〰〰〠〰㜰晦恦慥㝢〰〰†䌠尺䥗䑎坏屓奓呓䵅㈳湜牣灹汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦慥晡〰〰〠〰㜰晦恦慥㉢て〰†䌠尺䥗䑎坏屓奓呓䵅㈳乜䅔乓⸱汤�
|
||||
潍䱤慯㩤〠〰㜰晦恦挹ㅢ〰〰〠〰㜰晦恦挹㕢〸〰†䌠尺䥗䑎坏屓祳瑳浥㈳湜牣灹獴汳汤�
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
〲㐺㨱㤰›义但ⴠ丠硥獵㐠㔮ㄮ‷挨牵⸸⸴⤰
|
||||
〲㐺㨱㤰›义但ⴠㄠ‵畁〲㘲
|
||||
〲㐺㨱㤰›义但ⴠ䌠浯異整慎敭›䕄䭓佔ⵐ䐴㌳㝇�
|
||||
〲㐺㨱㤰›义但ⴠ䌠湯整瑮瀠瑡㩨䘠尺啍䥓彃体呆䅗䕒牜䙥⁘敎畸‴潃灭敬整乜硥獵䰠扩慲祲
|
||||
〲㐺㨱㤰›义但ⴠ䠠獯㩴䌠尺獕牥屳潬灣慨屭潓楮䙣牯敧瑓摵潩湜瑡癩彥牢摩敧扜極摬剜汥慥敳杜極灟潲敢攮數
|
||||
潍䱤慯㩤〠〰㜰晦恦慥㔱〰〰〠〰㜰晦恦慥㠱〶〰†䌠尺䥗䑎坏屓奓呓䵅㈳湜浴牡慴搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦敥㈰〰〰〠〰㜰晦恦敥挰づ〰†䌠尺䥗䑎坏屓祓瑳浥㈳捜扬慣煴搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦捤〵〰〰〠〰㜰晦恦捤㔷〰〰†䌠尺楗摮睯屳祓瑳浥㈳瑜楷慮楰愮灰潣敲搮汬
|
||||
潍䱤慯㩤〠〰㜰晦恦ㅣ㐸〰〰〠〰㜰晦恦ㅣ㤸っ〰†䌠尺䥗䑎坏屓祳瑳浥㈳摜瑡敡捸慨杮汤�
|
||||
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲ㄱ
楴⡤㤶〶
〸㜰〰㈰吠敨猠獹整慣湮瑯映湩桴楦敬猠数楣楦摥�
|
||||
獛慨敲彤潷歲牥⁝瑡慴档椠獮ㅴㄽ栠湷㵤〰〰〰〰㌰ぁ䔰㠷
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
噛瑳䤳獮牴浵湥嵴氠慯敤㩃停潲牧浡䘠汩獥䍜浯潭楆敬屳卖㍔乜硥獵瘮瑳″丨硥獵
畯䍴㵨�
|
||||
獛慨敲彤潷歲牥⁝潬摡椠獮㉴ㄽ
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
㜨〵㤶〶㨩䌠⬫䔠⁈硥散瑰潩潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
|
||||
獛慨敲彤潷歲牥⁝瑡慴档椠獮㉴ㄽ栠湷㵤〰〰〰〰㈰㔷䘰㠲
|
||||
睛異灭⁝獭㵧砰ㄳ⁆睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
潍䱤慯㩤〠〰㜰晦恦慡㕤〰〰〠〰㜰晦恦慡㥥〶〰†䌠尺䥗䑎坏屓奓呓䵅㈳瑜硥楴灮瑵牦浡睥牯汤�
|
||||
睛異灭⁝獭㵧砰ㄳ⁆睨摮〽〰〰〰〰䄳〰㝅�
|
||||
睛異灭⁝獭㵧砰ㄳ⁆睨摮〽〰〰〰〰㜲〵㉆�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰ㄱ″睨摮〽〰〰〰〰䌴〴㍂�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
䕒啓呌›潤敮ㄽ愠敒㵳‱剢獥ㄽ
|
||||
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
|
||||
㜨〵㔶㑣㨩匠捥牵瑩⁹档捥慦汩牵牯猠慴正戠晵敦癯牥畲潣敤挠〰〰〴‹ℨ℡猠捥湯档湡散℠℡�
|
||||
畓换摯㩥〠㝸䘠十彔䅆䱉䙟呁䱁䅟偐䕟䥘⁔
|
||||
捵瑲慢敳愡潢瑲〫㑸㩥
|
||||
〰〰昷晦敠㕢㔸㜲摣㤲††††††湩⁴††㤲�
|
||||
㨰〰㸰挠扤›敒摡湩湩瑩慩潣浭湡朧�
|
||||
䅗乒义㩇䌠湯楴畮湩潮潣瑮湩慵汢硥散瑰潩�
|
||||
㜨〵㔶㑣㨩匠捥牵瑩⁹档捥慦汩牵牯猠慴正戠晵敦癯牥畲潣敤挠〰〰〴‹ℨ℡猠捥湯档湡散℠℡�
|
||||
畓换摯㩥〠㝸䘠十彔䅆䱉䙟呁䱁䅟偐䕟䥘⁔
|
||||
捵瑲慢敳愡潢瑲〫㑸㩥
|
||||
〰〰昷晦敠㕢㔸㜲摣㤲††††††湩⁴††㤲�
|
||||
@@ -0,0 +1,11 @@
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$install = 'C:\Users\locpham\SonicForgeStudio\install'
|
||||
$new = 'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
|
||||
|
||||
Get-Process sonicforge-daw, daw_engine, daw_vst_bridge -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep -Seconds 2
|
||||
Copy-Item -Path $new -Destination (Join-Path $install 'daw_vst_bridge.exe') -Force
|
||||
Write-Output "copied: $((Get-Item (Join-Path $install 'daw_vst_bridge.exe')).Length) bytes"
|
||||
Start-Process -FilePath (Join-Path $install 'sonicforge-daw.exe')
|
||||
Start-Sleep -Seconds 8
|
||||
Get-Process sonicforge-daw, daw_engine, daw_vst_bridge -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, Path | Format-Table -AutoSize
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user