G3: 1 clock - PDC latency that + playhead engine-derived
This commit is contained in:
@@ -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`.
|
||||
@@ -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()
|
||||
@@ -236,6 +236,15 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
|
||||
MemoryBarrier();
|
||||
ipc->h.outWrite++;
|
||||
++processed;
|
||||
// G3: báo processed samples (playhead engine-derived — app tính
|
||||
// position = (processed - anchor - latency)/sr) qua lat ring slot
|
||||
// đặc biệt 0xFFFFFFFE. Python _lat_drain tách riêng (không vào
|
||||
// latencies dict). Ring 8 slot, Python drain mỗi ~2ms — an toàn.
|
||||
const uint32_t pls = ipc->h.latWrite & (FXRT_LAT_SLOTS - 1);
|
||||
ipc->lat[pls].slot = 0xFFFFFFFEu;
|
||||
ipc->lat[pls].samples = (uint32_t)(processed * n);
|
||||
MemoryBarrier();
|
||||
ipc->h.latWrite++;
|
||||
}
|
||||
const auto perfIterT1 = std::chrono::steady_clock::now();
|
||||
const double perfLoopMs = std::chrono::duration<double, std::milli>(perfIterT1 - perfT0).count();
|
||||
|
||||
Reference in New Issue
Block a user