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
+76
View File
@@ -1002,6 +1002,82 @@ def _render_midi_notes_pedalboard(instrument_id: str, notes: list, bpm: float,
)
@router.get("/fx")
async def list_fx_plugins(current_user: dict = Depends(get_current_user),
refresh: bool = Query(False)):
"""List VST effect plugins (is_fx) — native_bridge --scan trên các dir.
Chỉ plugin VST3 effect (Ozone, Scaler Audio, ...); instrument/VST2 bị
bridge loại. refresh=true → quét lại (Plugins Manager → Scan)."""
from app.core.vst_engine import get_plugin_manager
pm = get_plugin_manager()
return {"success": True, "plugins": pm.list_fx(refresh=refresh)}
class FxRenderRequest(BaseModel):
"""Render WAV qua chuỗi FX (VST3 effect + builtin gain/normalize).
Dùng cho masterbus (render xong project → Ozone trên bus) và FX Rack
per-track (preview offline qua bridge; realtime vẫn WebAudio)."""
input_file_id: Optional[str] = None # file trong PROCESSED_DIR
input_path: Optional[str] = None # path tuyệt đối (test)
fx_chain: list = []
sample_rate: int = 44100
@router.post("/fx-render")
async def fx_render(req: FxRenderRequest, current_user: dict = Depends(get_current_user)):
"""Áp fx_chain lên WAV đầu vào → WAV mới trong PROCESSED_DIR."""
enforce_password_changed(current_user)
src = (req.input_path or "").strip()
if not src:
fid = (req.input_file_id or "").strip()
if not fid:
raise HTTPException(status_code=400,
detail="Cần input_file_id hoặc input_path")
src = os.path.join(settings.PROCESSED_DIR, os.path.basename(fid))
if not os.path.isfile(src):
raise HTTPException(status_code=404,
detail=f"File đầu vào không tồn tại: {src}")
user_id = current_user["user_id"]
out_name = f"user_{user_id}_fx_{uuid.uuid4().hex[:10]}.wav"
out_path = os.path.join(settings.PROCESSED_DIR, out_name)
try:
_render_fx_locked(src, req.fx_chain, req.sample_rate, out_path)
duration = sf.info(out_path).frames / max(1, req.sample_rate)
return {
"success": True,
"file_id": out_name,
"url": f"/static/audio/processed/{out_name}",
"path": out_path,
"duration_sec": round(duration, 3),
"render_mode": settings.RENDER_ENGINE,
}
except HTTPException:
try:
os.remove(out_path)
except Exception:
pass
raise
except Exception as e:
try:
os.remove(out_path)
except Exception:
pass
raise HTTPException(status_code=500, detail=f"FX render thất bại: {e}")
@_render_locked
def _render_fx_locked(input_path: str, fx_chain: list, sample_rate: int, out_path: str):
"""FX render qua native_bridge --render-fx (không thread-safe → lock)."""
if settings.RENDER_ENGINE != "bridge":
raise HTTPException(status_code=501,
detail="Chỉ hỗ trợ native_bridge render — set SF_RENDER_ENGINE=bridge")
from app.core.native_render import render_fx_chain
render_fx_chain(input_wav=input_path, fx_chain=fx_chain,
sample_rate=sample_rate, out_path=out_path)
class SoundfontRenderRequest(BaseModel):
"""Render MIDI notes → WAV bằng soundfont THẬT qua native FluidSynth
(pyfluidsynth) — luồng âm instrument cho môi trường STANDALONE (xử lí