feat: VST FX rack per-track + masterbus render via native_bridge

This commit is contained in:
2026-08-17 21:31:57 +07:00
parent 8fbacd1d2b
commit b261eb0d7e
18 changed files with 1805 additions and 14 deletions
+82
View File
@@ -192,3 +192,85 @@ def render_offline(instrument_id, notes, bpm, sample_rate, preset_id=None,
os.remove(job_path)
except Exception:
pass
def render_fx_chain(input_wav, fx_chain, sample_rate, out_path=None) -> str:
"""Render WAV qua native_bridge --render-fx (VST3 FX + builtin gain/normalize).
fx_chain: list dict {type: "vst3"|"builtin", path|id, preset_b64?, bypass?,
params?}. path là plugin id/name → resolve_plugin_path (".vst3"). Trả
out_path; lỗi → HTTPException (501 thiếu bridge, 404 plugin lạ, 500 render).
"""
from app.core.vst_engine import resolve_plugin_path
exe = find_bridge_exe()
if not exe:
raise HTTPException(
status_code=501,
detail="native_bridge không khả dụng — đặt SF_BRIDGE_PATH hoặc cài daw_vst_bridge",
)
slots = []
for fx in fx_chain or []:
typ = fx.get("type")
if typ == "vst3":
p = (fx.get("path") or "").strip()
resolved = p if os.path.isfile(p) else resolve_plugin_path(p, ".vst3")
if not resolved:
raise HTTPException(
status_code=404,
detail=f"Không tìm thấy VST3 FX: {p} — bấm Plugins Manager → Scan trước.",
)
slots.append({"type": "vst3", "path": resolved,
"preset_b64": fx.get("preset_b64", "") or "",
"bypass": bool(fx.get("bypass"))})
elif typ == "builtin":
slots.append({"type": "builtin", "id": fx.get("id", "gain"),
"params": dict(fx.get("params") or {}),
"bypass": bool(fx.get("bypass"))})
else:
raise HTTPException(status_code=400, detail=f"fx_chain slot không hợp lệ: {typ}")
if not slots:
raise HTTPException(status_code=400, detail="fx_chain trống — không có FX để render")
job = {"sample_rate": int(sample_rate), "block_size": 512, "fx_chain": slots}
os.makedirs(settings.PROCESSED_DIR, exist_ok=True)
out = out_path or os.path.join(settings.PROCESSED_DIR, f"fx_{uuid.uuid4().hex[:10]}.wav")
job_path = out + ".job.json"
try:
with open(job_path, "w", encoding="utf-8") as fh:
json.dump(job, fh)
r = subprocess.run([exe, "--render-fx", job_path, "--in", input_wav, "--out", out],
capture_output=True, text=True, timeout=120)
log = ((r.stdout or "") + (r.stderr or "")).strip()
if r.returncode != 0 or not os.path.isfile(out):
if os.path.exists(out):
try:
os.remove(out)
except Exception:
pass
if r.returncode == 2:
raise HTTPException(status_code=500,
detail=f"Bridge không load được plugin. {log[-500:]}")
raise HTTPException(status_code=500,
detail=f"FX render thất bại (rc={r.returncode}). {log[-500:]}")
return out
except HTTPException:
raise
except subprocess.TimeoutExpired:
if os.path.exists(out):
try:
os.remove(out)
except Exception:
pass
raise HTTPException(status_code=500, detail="FX render quá lâu (timeout 120s)")
except Exception as e:
if os.path.exists(out):
try:
os.remove(out)
except Exception:
pass
raise HTTPException(status_code=500, detail=f"FX render thất bại: {e}")
finally:
if os.path.exists(job_path):
try:
os.remove(job_path)
except Exception:
pass