Phase 3: engine render chain hop nhat — track/master doc mastering_settings.chain (imager/maximizer), PDC probe FX_LATENCIES, fix daw_vst_bridge link BuiltinFxChain, pytest 3.2/3.3
This commit is contained in:
+128
-27
@@ -293,6 +293,84 @@ def _track_latency_samples(track):
|
||||
total += int(slot["latency_samples"])
|
||||
return total
|
||||
|
||||
def _merge_track_fx_chain(track):
|
||||
"""Chain hợp nhất track cho bridge (Phase 3.1): builtin fx_chain + vst3
|
||||
vst_fx_chain, giữ NGUYÊN thứ tự UI. vst_fx_bypass → bỏ hẳn slot vst3
|
||||
(builtin vẫn chạy)."""
|
||||
merged = []
|
||||
for slot in (track.get("fx_chain") or []):
|
||||
if isinstance(slot, str):
|
||||
slot = {"type": slot}
|
||||
if isinstance(slot, dict):
|
||||
merged.append(slot)
|
||||
if not track.get("vst_fx_bypass"):
|
||||
merged += list(track.get("vst_fx_chain") or [])
|
||||
return merged
|
||||
|
||||
# Param mapping master chain entry → C++ BuiltinFx (Phase 3.2). UI chain entry
|
||||
# chỉ lưu params cho eqpro; imager/maximizer/eq/... giữ knob ở flat mastering
|
||||
# state (maxGain, w1..w4, ...). Offline fill từ flat state để khớp live
|
||||
# (MASTER_MODULE_IO WebAudio đọc cùng state đó).
|
||||
_BUILTIN_PARAM_MAP = {
|
||||
"eq": [("g1", "eqLowGain"), ("g2", "eqMid1Gain"),
|
||||
("g3", "eqMid2Gain"), ("g4", "eqHighGain")],
|
||||
"compressor": [("threshold", "compThreshold"), ("ratio", "compRatio"),
|
||||
("makeup", "compMakeup")],
|
||||
"limiter": [("ceiling", "limThreshold")],
|
||||
"exciter": [("drive", "excDrive")],
|
||||
"rebalance": [("mid", "rebalMid"), ("side", "rebalSide")],
|
||||
"imager": [("w1", "w1"), ("w2", "w2"), ("w3", "w3"), ("w4", "w4")],
|
||||
"maximizer": [("boost_db", "maxGain"), ("soft_clip", "maxSoftClip"),
|
||||
("upward", "maxUpward"), ("ceiling_db", "ceiling")],
|
||||
}
|
||||
|
||||
def _master_chain_entry(ms, entry):
|
||||
"""Entry master chain cho bridge: params entry (eqpro) + fill từ flat
|
||||
mastering state (knob imager/maximizer/eq/...)."""
|
||||
typ = entry.get("type")
|
||||
if typ not in _BUILTIN_PARAM_MAP:
|
||||
return entry
|
||||
params = dict(entry.get("params") or {})
|
||||
for cpp_key, js_key in _BUILTIN_PARAM_MAP[typ]:
|
||||
v = ms.get(js_key)
|
||||
if v is not None and cpp_key not in params:
|
||||
params[cpp_key] = v
|
||||
out = dict(entry)
|
||||
out["params"] = params
|
||||
return out
|
||||
|
||||
def _probe_track_latency(track, sample_rate):
|
||||
"""Latency thật (samples) của track từ bridge --render-fx (Phase 3.3):
|
||||
chạy chain qua probe silence ngắn, parse FX_LATENCIES. 0 nếu không đo
|
||||
được. Chỉ chạy khi track có slot vst3 (builtin DSP latency = 0)."""
|
||||
if settings.RENDER_ENGINE != "bridge":
|
||||
return 0
|
||||
merged = _merge_track_fx_chain(track)
|
||||
if not any(isinstance(s, dict) and s.get("type") == "vst3" for s in merged):
|
||||
return 0
|
||||
from app.core.native_render import render_fx_chain
|
||||
os.makedirs(settings.PROCESSED_DIR, exist_ok=True)
|
||||
probe_in = os.path.join(
|
||||
settings.PROCESSED_DIR,
|
||||
f"latprobe_in_{os.getpid()}_{np.random.randint(100000)}.wav")
|
||||
probe_out = probe_in.replace("latprobe_in_", "latprobe_out_")
|
||||
try:
|
||||
sf.write(probe_in, np.zeros((2, 64), dtype=np.float32).T,
|
||||
sample_rate, subtype="FLOAT")
|
||||
_, lats = render_fx_chain(input_wav=probe_in, fx_chain=merged,
|
||||
sample_rate=sample_rate, out_path=probe_out)
|
||||
return int(sum(lats)) if lats else 0
|
||||
except Exception as e:
|
||||
logger.warning("[RenderEngine] PDC probe failed: %s", e)
|
||||
return 0
|
||||
finally:
|
||||
for f in (probe_in, probe_out):
|
||||
if os.path.exists(f):
|
||||
try:
|
||||
os.remove(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
class PythonRenderEngine:
|
||||
def __init__(self, sample_rate=44100):
|
||||
self.sample_rate = sample_rate
|
||||
@@ -332,12 +410,16 @@ class PythonRenderEngine:
|
||||
|
||||
# PDC pre-scan (spec §3): latency mỗi track từ chain slots; track ngắn
|
||||
# hơn bị delay khi mix để mọi track sum sample-accurate phase-aligned.
|
||||
# Phase 3.3: slot chưa khai latency_samples → probe bridge --render-fx
|
||||
# (FX_LATENCIES) đo latency thật của VST3 (getLatencySamples).
|
||||
_track_lat = {}
|
||||
_max_lat = 0
|
||||
for _t in tracks:
|
||||
if solo_ids and _t.get("id") not in solo_ids:
|
||||
continue
|
||||
_lat = _track_latency_samples(_t)
|
||||
if not _lat:
|
||||
_lat = _probe_track_latency(_t, self.sample_rate)
|
||||
_track_lat[_t.get("id")] = _lat
|
||||
_max_lat = max(_max_lat, _lat)
|
||||
|
||||
@@ -595,31 +677,17 @@ class PythonRenderEngine:
|
||||
continue
|
||||
|
||||
# ── Track FX chain (spec §2 Step 2): serial custom FX → VST FX ──
|
||||
# Builtin DSP slots (fx_chain) → legacy fx_type (chorus/reverb) →
|
||||
# VST FX (vst_fx_chain) qua native_bridge. Mỗi slot sửa buffer
|
||||
# in-place trước khi chuyển tới slot kế — đúng Buffer_out =
|
||||
# Plugin_n(...Plugin_1(Buffer_in)).
|
||||
# Phase 3.1: RENDER_ENGINE=bridge → chain HỢP NHẤT (builtin
|
||||
# fx_chain + vst3 vst_fx_chain xen kẽ đúng thứ tự UI) qua 1 lần
|
||||
# native_bridge --render-fx. Mỗi slot sửa buffer in-place đúng
|
||||
# Buffer_out = Plugin_n(...Plugin_1(Buffer_in)). Legacy
|
||||
# chorus/reverb (fx_type cũ) chỉ có Python DSP → áp trước bridge;
|
||||
# `ponytail:` track có cả fx_chain + fx_type sẽ chạy legacy trước
|
||||
# chain (track cũ không có chain hiện đại — order không quan trọng).
|
||||
fx_type = track.get("fx_type")
|
||||
if track.get("fx_active", True):
|
||||
try:
|
||||
track_buffer = _apply_builtin_fx_chain(
|
||||
track_buffer, track.get("fx_chain"), self.sample_rate)
|
||||
except Exception as e:
|
||||
logger.warning("[RenderEngine] Builtin FX chain failed: %s", e)
|
||||
fx_type = track.get("fx_type")
|
||||
if fx_type == "chorus":
|
||||
try:
|
||||
track_buffer = _apply_legacy_chorus(track_buffer, self.sample_rate)
|
||||
except Exception as e:
|
||||
logger.warning("[RenderEngine] Fallback Chorus failed: %s", e)
|
||||
elif fx_type == "reverb":
|
||||
try:
|
||||
track_buffer = _apply_legacy_reverb(track_buffer, self.sample_rate)
|
||||
except Exception as e:
|
||||
logger.warning("[RenderEngine] Fallback Reverb failed: %s", e)
|
||||
|
||||
track_fx_chain = track.get("vst_fx_chain") or []
|
||||
if track_fx_chain and not track.get("vst_fx_bypass"):
|
||||
if settings.RENDER_ENGINE == "bridge":
|
||||
merged_fx = _merge_track_fx_chain(track)
|
||||
if merged_fx and settings.RENDER_ENGINE == "bridge":
|
||||
try:
|
||||
from app.core.native_render import render_fx_chain
|
||||
tmp_in = os.path.join(
|
||||
@@ -627,8 +695,12 @@ class PythonRenderEngine:
|
||||
f"trackfx_in_{os.getpid()}_{np.random.randint(100000)}.wav")
|
||||
tmp_out = tmp_in.replace("trackfx_in_", "trackfx_out_")
|
||||
try:
|
||||
if fx_type == "chorus":
|
||||
track_buffer = _apply_legacy_chorus(track_buffer, self.sample_rate)
|
||||
elif fx_type == "reverb":
|
||||
track_buffer = _apply_legacy_reverb(track_buffer, self.sample_rate)
|
||||
sf.write(tmp_in, track_buffer.T, self.sample_rate, subtype="FLOAT")
|
||||
render_fx_chain(input_wav=tmp_in, fx_chain=track_fx_chain,
|
||||
render_fx_chain(input_wav=tmp_in, fx_chain=merged_fx,
|
||||
sample_rate=self.sample_rate, out_path=tmp_out)
|
||||
data, _ = sf.read(tmp_out, dtype="float32", always_2d=True)
|
||||
if data.shape[1] >= 2:
|
||||
@@ -645,7 +717,26 @@ class PythonRenderEngine:
|
||||
except Exception as e:
|
||||
logger.warning("[RenderEngine] Track FX chain skipped: %s", e)
|
||||
else:
|
||||
logger.warning("[RenderEngine] Track FX chain cần RENDER_ENGINE=bridge — bỏ qua")
|
||||
# Fallback Python DSP (RENDER_ENGINE≠bridge) — giữ luồng cũ:
|
||||
# builtin fx_chain → legacy fx_type. VST3 cần bridge.
|
||||
try:
|
||||
track_buffer = _apply_builtin_fx_chain(
|
||||
track_buffer, track.get("fx_chain"), self.sample_rate)
|
||||
except Exception as e:
|
||||
logger.warning("[RenderEngine] Builtin FX chain failed: %s", e)
|
||||
if fx_type == "chorus":
|
||||
try:
|
||||
track_buffer = _apply_legacy_chorus(track_buffer, self.sample_rate)
|
||||
except Exception as e:
|
||||
logger.warning("[RenderEngine] Fallback Chorus failed: %s", e)
|
||||
elif fx_type == "reverb":
|
||||
try:
|
||||
track_buffer = _apply_legacy_reverb(track_buffer, self.sample_rate)
|
||||
except Exception as e:
|
||||
logger.warning("[RenderEngine] Fallback Reverb failed: %s", e)
|
||||
if (track.get("vst_fx_chain") and not track.get("vst_fx_bypass")
|
||||
and settings.RENDER_ENGINE != "bridge"):
|
||||
logger.warning("[RenderEngine] Track VST FX cần RENDER_ENGINE=bridge — bỏ qua")
|
||||
|
||||
# Process track volume (spec §2 Step 3: Gain_i scaling)
|
||||
gain_linear = 10 ** (vol_db / 20.0)
|
||||
@@ -694,8 +785,18 @@ class PythonRenderEngine:
|
||||
# Masterbus FX chain (VST3 FX / builtin) — render qua native_bridge
|
||||
# --render-fx, rồi volume_db master; backward compatible (không có
|
||||
# master.fx_chain → giữ nguyên luồng cũ).
|
||||
# Phase 3.2: ưu tiên mastering_settings.chain (chain hợp nhất UI —
|
||||
# builtin imager/maximizer + vst3 xen kẽ, params qua entry + flat
|
||||
# mastering state) → bỏ bất đối xứng offline thiếu imager/maximizer;
|
||||
# fallback master.fx_chain (project cũ, vst3-only).
|
||||
master = main_session.get("master") or {}
|
||||
fx_chain = master.get("fx_chain") or []
|
||||
_ms = project_json.get("mastering_settings") or {}
|
||||
_ms_chain = _ms.get("chain") if isinstance(_ms.get("chain"), list) else None
|
||||
if _ms_chain:
|
||||
fx_chain = [_master_chain_entry(_ms, s) for s in _ms_chain
|
||||
if isinstance(s, dict)]
|
||||
else:
|
||||
fx_chain = master.get("fx_chain") or []
|
||||
if fx_chain and not master.get("bypass"):
|
||||
if settings.RENDER_ENGINE == "bridge":
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user