Files
SonicForgeStudio/JUCE_LATENCY_PLAN.md
T

131 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 đủ.