Files
SonicForgeStudio/JUCE_LATENCY_PLAN.md
T

8.4 KiB

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.