feat(fx): Phase 1 DAW chain hop nhat — fx_realtime chain JSON giu thu tu, SET_PARAM/REPORT_LATENCY qua WS text, native_render chain builtin+vst3 xen ke, pytest

This commit is contained in:
2026-08-22 19:26:43 +07:00
parent 5ad2c681e7
commit fd790c1dc6
6 changed files with 365 additions and 10 deletions
+16 -2
View File
@@ -1069,9 +1069,23 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
pump_task = asyncio.create_task(_pump_output())
try:
while True:
data = await websocket.receive_bytes()
if not sess.alive():
msg = await websocket.receive()
if msg.get("type") == "websocket.disconnect" or not sess.alive():
break
if msg.get("text") is not None:
# Control protocol Phase 1: SET_PARAM / REPORT_LATENCY qua WS text.
try:
cmd = json.loads(msg["text"])
except Exception:
continue
if cmd.get("cmd") == "set_param":
sess.set_param(cmd.get("slot"), cmd.get("key"), cmd.get("value"))
elif cmd.get("cmd") == "latency":
sess.report_latency(cmd.get("slot"), cmd.get("samples"))
continue
data = msg.get("bytes")
if data is None:
continue
sess.write_input(data)
except WebSocketDisconnect:
pass
+46 -8
View File
@@ -86,21 +86,37 @@ class FxRealtimeSession:
self.proc = None
self._job_path = ""
self._started_at = time.time()
# Chain hợp nhất (spec PLAN_DAW_A.md Phase 1): giữ đúng thứ tự slot.
self.chain = list(self.fx_chain or [])
# Control protocol (SET_PARAM / REPORT_LATENCY) — Phase 1: giữ ở engine;
# Phase 2.8: C++ consume qua SHM control ring.
self.pending_params = {} # {slot_idx: {key: value}}
self.latencies = {} # {slot_idx: samples}
self._spawn()
# ── process ─────────────────────────────────────────────────────────────
def _chain_json_for_bridge(self):
"""JSON mảng {path,bypass,preset_b64} — RealtimeFxChain::buildChain đọc
đúng các field này (type/name bỏ qua)."""
"""JSON mảng slot theo spec Phase 1 — giữ NGUYÊN thứ tự + type/params;
RealtimeFxChain::buildChain (C++) đọc {path,bypass,preset_b64} cho vst3
và {type,params} cho builtin (Phase 2). Slot active=false bị loại trước
khi gửi (UI tắt hẳn — không gửi engine)."""
out = []
for s in self.fx_chain:
if not s or not s.get("path"):
for s in self.chain:
if not s or not isinstance(s, dict):
continue
out.append({
"path": s["path"],
if s.get("active") is False:
continue
slot = {
"type": s.get("type", ""),
"bypass": bool(s.get("bypass")),
"preset_b64": s.get("preset_b64") or "",
})
}
if s.get("path"):
slot["path"] = s["path"]
if s.get("preset_b64"):
slot["preset_b64"] = s["preset_b64"]
if isinstance(s.get("params"), dict) and s["params"]:
slot["params"] = s["params"]
out.append(slot)
return out
def _spawn(self):
@@ -203,6 +219,28 @@ class FxRealtimeSession:
def heartbeat_age(self):
return time.time() - self._started_at # placeholder: engine đọc h.heartbeat
# ── control protocol (SET_PARAM / REPORT_LATENCY) ────────────────────────
# Phase 1: giữ ở engine, client gọi qua WS text frame. Phase 2.8: bridge
# C++ đọc pending_params từ SHM control ring + ghi latency vào ring.
def set_param(self, slot, key, value):
with self._lock:
self.pending_params.setdefault(slot, {})[str(key)] = value
def report_latency(self, slot, samples):
with self._lock:
self.latencies[int(slot)] = int(samples)
def drain_params(self):
"""Trả + xóa toàn bộ pending param (bridge consume 1 lần)."""
with self._lock:
out = self.pending_params
self.pending_params = {}
return out
def get_latencies(self):
with self._lock:
return dict(self.latencies)
# Registry toàn tiến trình (desktop app 1 user).
_SESSIONS = {}
+10
View File
@@ -232,6 +232,8 @@ def render_fx_chain(input_wav, fx_chain, sample_rate, out_path=None) -> str:
)
slots = []
for fx in fx_chain or []:
if not isinstance(fx, dict) or fx.get("active") is False:
continue
typ = fx.get("type")
if typ == "vst3":
p = (fx.get("path") or "").strip()
@@ -244,7 +246,15 @@ def render_fx_chain(input_wav, fx_chain, sample_rate, out_path=None) -> str:
slots.append({"type": "vst3", "path": resolved,
"preset_b64": fx.get("preset_b64", "") or "",
"bypass": bool(fx.get("bypass"))})
elif typ in ("eq", "eqpro", "imager", "maximizer", "compressor",
"limiter", "exciter", "rebalance"):
# Builtin DSP — chain hợp nhất (spec PLAN_DAW_A.md Phase 1). C++
# RenderFxJob xử lý theo id/params (Phase 2); giữ NGUYÊN thứ tự.
slots.append({"type": "builtin", "id": typ,
"params": dict(fx.get("params") or {}),
"bypass": bool(fx.get("bypass"))})
elif typ == "builtin":
# Legacy: {type: builtin, id: gain} — giữ cho project cũ.
slots.append({"type": "builtin", "id": fx.get("id", "gain"),
"params": dict(fx.get("params") or {}),
"bypass": bool(fx.get("bypass"))})