From 51bac2770d860dab0b391e21dadcac8bc4dc8382 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Fri, 28 Aug 2026 11:38:46 +0700 Subject: [PATCH] docs: JUCE latency plan - thay heuristic PDC bang AudioProcessorGraph (G0-G5) --- JUCE_LATENCY_PLAN.md | 125 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 JUCE_LATENCY_PLAN.md diff --git a/JUCE_LATENCY_PLAN.md b/JUCE_LATENCY_PLAN.md new file mode 100644 index 0000000..d4eb8a7 --- /dev/null +++ b/JUCE_LATENCY_PLAN.md @@ -0,0 +1,125 @@ +# 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. Loop wrap sample-accurate trong engine (không restart, không clear_queue → hết seam click gốc). +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 + + AudioTransportSource / custom position + + loop region [selLeft, selRight] set qua SHM ctrl + + getLatencySamples() sau prepareToPlay → REPORT_LATENCY (giữ SHM FxLatReport) +``` + +Giữ nguyên SHM `FxRealtimeIPC` làm ranh giới (không đổi Python/JS transport). Thay đổi nội bộ bridge. + +### 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. +- 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ở. +- [ ] 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}`. +- 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 — 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. + +### 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 | +| Tăng latency thêm 1 buffer nội bộ (G3 buffer toàn track) | Buffer vòng 2 pass loop + pre-roll; đo lại FILL | +| 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 | + +## 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 mới đổi contract SHM. +- 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.