docs: plan JUCE - phan bien split-brain (1 master clock, Option A), param pump GUI->JUCE, do jitter sigma G0

This commit is contained in:
2026-08-28 11:45:46 +07:00
parent 51bac2770d
commit a10f679c0d
+16 -11
View File
@@ -32,7 +32,7 @@ Gốc rễ: **transport + FX không nằm chung một engine realtime**; WebAudi
## 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. Loop wrap sample-accurate trong engine (không restart, không clear_queue → hết seam click gốc).
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.
@@ -47,23 +47,25 @@ fx_juce_fx (C++ mới, thay --realtime-fx):
├─ master chain: mỗi VST3 FX = AudioPluginInstance (juce VST3 hosting built-in)
├─ latency node (tự động — graph PDC)
└─ output node → SHM out ring
+ AudioTransportSource / custom position
+ loop region [selLeft, selRight] set qua SHM ctrl
+ 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).
- Loop wrap trong graph: nguồn là ring buffer nội bộ → wrap tại đúng sample boundary → không clear_queue, không restart source, không seam.
- 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)
- [ ] Đ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ở.
- [ ] Đ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).
- [ ] 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ì.
@@ -79,12 +81,11 @@ Giữ nguyên SHM `FxRealtimeIPC` làm ranh giới (không đổi Python/JS tran
- [ ] `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 — Transport + loop trong engine (hết seam click)
- [ ] SHM ctrl thêm: `{cmd:'transport', state: play|stop, position, loopStart, loopEnd}`.
- [ ] Engine giữ buffer nội bộ của toàn track master; loop wrap tại `loopEnd` sample-exact, không restart.
- [ ] Playhead: engine gửi `position` (samples processed, đã trừ latency) → app hiển thị trực tiếp; bỏ freeze/snap (V40) + rAF ε cho audio (giữ rAF chỉ cho UI).
- [ ] Worklet: bỏ `wrapFadeBlocks`/`WRAP_FADE` (V42) khi engine path active — seam không còn tồn tại.
- Done khi: loop selection với FX chain: 100 pass liên tục không click (ghi âm so sánh với V42); playhead bám đúng vị trí mẫu.
### 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).
@@ -105,6 +106,9 @@ Giữ nguyên SHM `FxRealtimeIPC` làm ranh giới (không đổi Python/JS tran
| 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; 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
@@ -121,5 +125,6 @@ Giữ nguyên SHM `FxRealtimeIPC` làm ranh giới (không đổi Python/JS tran
## 8. Ghi chú ponytail
- G0-G2 không đòi hỏi đổi JS/Python — hoàn đảo được, rủi ro thấp. G3 mới đổi contract SHM.
- 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 đủ.