G3: 1 clock - PDC latency that + playhead engine-derived

This commit is contained in:
2026-08-28 15:51:19 +07:00
parent c72362e088
commit 6cbdbd8e9c
8 changed files with 218 additions and 10 deletions
+58
View File
@@ -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`.
+62
View File
@@ -0,0 +1,62 @@
"""G3 check — processed slot 0xFFFFFFFE tach rieng, khong lam sai latencies dict.
Chay: python native_bridge/debug/g3_probe_check.py
"""
import ctypes
import os
import sys
import time
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
try:
_winmm = ctypes.windll.winmm
_winmm.timeBeginPeriod(1)
except Exception:
pass
import numpy as np # noqa: E402
from app.core.fx_realtime import FxRealtimeSession, FXRT_BLOCK # noqa: E402
# Chon JUCE bridge (processed slot chi co o JuceFxLoop.cpp).
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
os.environ["SF_JUCE_FX_BRIDGE_PATH"] = os.path.join(
_REPO, "native_bridge", "build_juce", "Release", "juce_fx_bridge.exe")
SR = 48000
PURECOMP = r"C:\Program Files\Common Files\VST3\Sonible\purecomp.vst3"
sess = FxRealtimeSession([{"type": "vst3", "path": PURECOMP, "bypass": False, "preset_b64": ""}], SR)
try:
sess.wait_ready(30)
time.sleep(1.0)
lat0 = dict(sess.get_latencies())
proc0 = sess.get_processed()
print("lat0:", lat0)
print("proc0:", proc0)
t = np.arange(FXRT_BLOCK, dtype=np.float32) / SR
s = (np.sin(2 * np.pi * 440 * t) * 0.1).astype(np.float32)
inter = np.empty(FXRT_BLOCK * 2, dtype=np.float32)
inter[0::2] = s
inter[1::2] = s
blk = inter.tobytes()
n_ok = 0
last = proc0
for _ in range(400): # ~2.1s
sess.write_input(blk)
sess.flush_input_stale(0.001)
sess.read_output()
time.sleep(0.002)
time.sleep(0.5)
lat1 = dict(sess.get_latencies())
proc1 = sess.get_processed()
print("lat1:", lat1)
print("proc1:", proc1)
assert lat0 == lat1, "latencies dict changed!"
assert isinstance(proc1, int) and proc1 >= (proc0 or 0), "processed not advancing"
print("OK: latencies dict unchanged, processed=%d->%d" % (proc0 or 0, proc1))
finally:
sess.close()