feat: VST FX rack per-track + masterbus render via native_bridge
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user