Phase 2: BuiltinFxChain C++ (8 DSP) + control ring SET_PARAM/REPORT_LATENCY
- native_bridge/src/BuiltinFxChain.{h,cpp}: 8 builtin DSP port 1:1 từ
Python _apply_builtin_fx_chain (eq/eqpro/compressor/limiter/exciter/rebalance)
và JS MASTER_MODULE_IO (imager/maximizer). Biquad RBJ DF2T, block-wise
stateful. createBuiltinFx trả nullptr cho gain/normalize (legacy).
- RenderFxJob: FxSlot thêm bfx, parse builtin 8 loại, runBuiltinSlotSafe SEH,
realtimeRunChain nhánh builtin, chainGen()/entryLatencies(), setParam
áp live lên bfx; Vst3Fx latencySamples từ getLatencySamples().
- RealtimeFxLoop: drain ctrl ring (SET_PARAM -> chain.setParam, drop cũ khi
đầy) + REPORT_LATENCY khi gen đổi (ipc->lat[] ring, lastGen).
- FxRealtimeIPC.h: header 18 u32 (72B), ctrl/lat slots 8, FxCtrlCmd 24B,
FxLatReport 8B, static_asserts.
- app/core/fx_realtime.py: mirror header 72B (ctrl_write/read, lat_write/read,
slots), set_param ghi ctrl ring qua _ctrl_enqueue_locked (guard shm),
get_latencies drain lat ring qua _lat_drain.
- tests/test_builtin_fx_golden.py: golden SNR C++ vs Python (8 cases, ngưỡng
30dB) + order test eq/compressor xen kẽ.
- TASKS_DAW_A.md: tick 2.1-2.9.
Test: 136 passed (4 fx_realtime_chain + 8 builtin golden + regression).
This commit is contained in:
+71
-4
@@ -4,9 +4,12 @@ spawn fx_vst_bridge --realtime-fx, đẩy/kéo block audio qua SHM ring.
|
||||
|
||||
Layout mirror chính xác native_bridge/include/FxRealtimeIPC.h (mọi field
|
||||
u32/float 4-byte, không padding):
|
||||
header 12 u32 (48B): magic state sampleRate blockSize running heartbeat
|
||||
header 18 u32 (72B): magic state sampleRate blockSize running heartbeat
|
||||
inWrite inRead inSlots outWrite outRead outSlots
|
||||
ctrlWrite ctrlRead ctrlSlots latWrite latRead latSlots
|
||||
inL[4][256] inR[4][256] outL[8][256] outR[8][256]
|
||||
ctrl[8] (FxCtrlCmd 24B) — SET_PARAM ring (engine -> bridge)
|
||||
lat[8] (FxLatReport 8B) — REPORT_LATENCY ring (bridge -> engine)
|
||||
|
||||
Luồng dữ liệu: browser → WS → write_input() → [bridge xử lý] → read_output()
|
||||
→ WS → browser. Block = FXRT_BLOCK=256 stereo float32 = 2048 bytes.
|
||||
@@ -29,6 +32,8 @@ FXRT_MAGIC = 0x46585254
|
||||
FXRT_BLOCK = 256
|
||||
FXRT_IN_SLOTS = 4
|
||||
FXRT_OUT_SLOTS = 8
|
||||
FXRT_CTRL_SLOTS = 8
|
||||
FXRT_LAT_SLOTS = 8
|
||||
FXRT_STATE_STARTING = 0
|
||||
FXRT_STATE_READY = 1
|
||||
FXRT_STATE_ERROR = 2
|
||||
@@ -48,15 +53,38 @@ class FxRTHeader(ctypes.Structure):
|
||||
("out_write", ctypes.c_uint32),
|
||||
("out_read", ctypes.c_uint32),
|
||||
("out_slots", ctypes.c_uint32),
|
||||
("ctrl_write", ctypes.c_uint32),
|
||||
("ctrl_read", ctypes.c_uint32),
|
||||
("ctrl_slots", ctypes.c_uint32),
|
||||
("lat_write", ctypes.c_uint32),
|
||||
("lat_read", ctypes.c_uint32),
|
||||
("lat_slots", ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
HEADER_SIZE = ctypes.sizeof(FxRTHeader) # 48
|
||||
IN_L_OFF = HEADER_SIZE # 48
|
||||
class FxCtrlCmd(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("slot", ctypes.c_uint32),
|
||||
("key", ctypes.c_char * 16),
|
||||
("value", ctypes.c_float),
|
||||
]
|
||||
|
||||
|
||||
class FxLatReport(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("slot", ctypes.c_uint32),
|
||||
("samples", ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
HEADER_SIZE = ctypes.sizeof(FxRTHeader) # 72
|
||||
IN_L_OFF = HEADER_SIZE # 72
|
||||
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
|
||||
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
TOTAL_SIZE = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
CTRL_OFF = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
|
||||
LAT_OFF = CTRL_OFF + FXRT_CTRL_SLOTS * ctypes.sizeof(FxCtrlCmd)
|
||||
TOTAL_SIZE = LAT_OFF + FXRT_LAT_SLOTS * ctypes.sizeof(FxLatReport)
|
||||
|
||||
|
||||
class FxRealtimeSession:
|
||||
@@ -81,6 +109,8 @@ class FxRealtimeSession:
|
||||
self.h.running = 1
|
||||
self.h.in_slots = FXRT_IN_SLOTS
|
||||
self.h.out_slots = FXRT_OUT_SLOTS
|
||||
self.h.ctrl_slots = FXRT_CTRL_SLOTS
|
||||
self.h.lat_slots = FXRT_LAT_SLOTS
|
||||
self._np = np.frombuffer(self.shm.buf, dtype=np.float32)
|
||||
self._lock = threading.Lock()
|
||||
self.proc = None
|
||||
@@ -225,6 +255,28 @@ class FxRealtimeSession:
|
||||
def set_param(self, slot, key, value):
|
||||
with self._lock:
|
||||
self.pending_params.setdefault(slot, {})[str(key)] = value
|
||||
# Phase 2.8: đẩy vào SHM control ring (bridge drain mỗi
|
||||
# iteration). pending_params giữ làm fallback — fake test session
|
||||
# (object.__new__) không có shm; drain_params vẫn hoạt động.
|
||||
if getattr(self, "shm", None) is not None:
|
||||
try:
|
||||
self._ctrl_enqueue_locked(slot, str(key), value)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _ctrl_enqueue_locked(self, slot, key, value):
|
||||
h = self.h
|
||||
kb = key.encode("utf-8")[:15]
|
||||
cmds = (FxCtrlCmd * FXRT_CTRL_SLOTS).from_buffer(self.shm.buf, CTRL_OFF)
|
||||
while True:
|
||||
if h.ctrl_write - h.ctrl_read < h.ctrl_slots:
|
||||
cs = h.ctrl_write & (h.ctrl_slots - 1)
|
||||
cmds[cs].slot = int(slot) & 0xFFFFFFFF
|
||||
cmds[cs].key = kb + b"\00" * (16 - len(kb))
|
||||
cmds[cs].value = float(value)
|
||||
h.ctrl_write += 1
|
||||
return
|
||||
h.ctrl_read += 1 # ring đầy → drop lệnh cũ nhất
|
||||
|
||||
def report_latency(self, slot, samples):
|
||||
with self._lock:
|
||||
@@ -239,8 +291,23 @@ class FxRealtimeSession:
|
||||
|
||||
def get_latencies(self):
|
||||
with self._lock:
|
||||
self._lat_drain()
|
||||
return dict(self.latencies)
|
||||
|
||||
def _lat_drain(self):
|
||||
shm = getattr(self, "shm", None)
|
||||
if shm is None:
|
||||
return
|
||||
try:
|
||||
h = self.h
|
||||
lats = (FxLatReport * FXRT_LAT_SLOTS).from_buffer(shm.buf, LAT_OFF)
|
||||
while h.lat_read < h.lat_write:
|
||||
ls = h.lat_read & (h.lat_slots - 1)
|
||||
self.latencies[int(lats[ls].slot)] = int(lats[ls].samples)
|
||||
h.lat_read += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Registry toàn tiến trình (desktop app 1 user).
|
||||
_SESSIONS = {}
|
||||
|
||||
Reference in New Issue
Block a user