# SonicForge Native Audio API (T15) — track instrument đi thẳng vào engine # native (SF host / VST2 / VST3 bridge DLL), KHÔNG qua masterBus WebAudio. # Master fader + track gain/pan áp native qua NativeMixer — một engine duy nhất. from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import List, Optional import os from app.core.native_audio_service import get_service from app.core.render_engine import _find_sf2_path from app.core.vst_engine import get_plugin_manager router = APIRouter() class MasterGainRequest(BaseModel): gain_db: float = 0.0 class TrackGainPanRequest(BaseModel): track_id: str gain_db: float = 0.0 pan: float = 0.0 class SfEnsureRequest(BaseModel): track_id: str sf_path: Optional[str] = None sf_id: Optional[str] = None # resolve path server-side (JS chỉ có UUID) bank: int = 0 program: int = 0 channel: int = 0 live: bool = True class SfNoteRequest(BaseModel): track_id: str channel: int = 0 pitch: int velocity: int = 100 class SfNoteOffRequest(BaseModel): track_id: str channel: int = 0 pitch: int class Vst2EnsureRequest(BaseModel): track_id: str plugin_path: Optional[str] = None plugin_id: Optional[str] = None # resolve path server-side live: bool = True class Vst2NoteRequest(BaseModel): track_id: str channel: int = 0 pitch: int velocity: int = 100 class Vst2NoteOffRequest(BaseModel): track_id: str channel: int = 0 pitch: int class Vst3EnsureRequest(BaseModel): track_id: str plugin_path: Optional[str] = None plugin_id: Optional[str] = None # resolve path server-side live: bool = True class Vst3NoteRequest(BaseModel): track_id: str channel: int = 0 pitch: int velocity: int = 100 class Vst3NoteOffRequest(BaseModel): track_id: str channel: int = 0 pitch: int class RenderNote(BaseModel): note: int = 60 velocity: int = 100 start_beat: float = 0.0 duration_beats: float = 1.0 class RenderRequest(BaseModel): kind: str # "sf" | "vst2" path: str bank: int = 0 program: int = 0 notes: List[RenderNote] = [] bpm: float = 120.0 gain_db: float = 0.0 pan: float = 0.0 lim_active: bool = False threshold_db: float = -1.0 master_gain_db: float = 0.0 def _svc(): return get_service() def _resolve_plugin_path(plugin_id: str = None, plugin_path: str = None) -> str: """Resolve plugin path server-side: uu tien plugin_path truc tiep, con khong thi plugin_id = ten plugin trong registry scan (JS chi co plugin_id).""" path = (plugin_path or "").strip() if path: return path pid = (plugin_id or "").strip() if pid: try: plugins = get_plugin_manager()._scan_plugins() if pid in plugins: return plugins[pid] except Exception: pass return "" @router.get("/status") async def status(): return _svc().status() @router.post("/set_master_gain") async def set_master_gain(req: MasterGainRequest): return _svc().set_master_gain(req.gain_db) @router.post("/track_gain_pan") async def track_gain_pan(req: TrackGainPanRequest): return _svc().set_track_gain_pan(req.track_id, req.gain_db, req.pan) @router.post("/sf/ensure") async def sf_ensure(req: SfEnsureRequest): path = (req.sf_path or "").strip() if not path and req.sf_id: path = _find_sf2_path(req.sf_id) if not path or not os.path.exists(path): raise HTTPException(status_code=400, detail=f"không tìm thấy SF2/SF3 " f"(sf_path={req.sf_path!r} sf_id={req.sf_id!r})") try: return _svc().ensure_sf(req.track_id, path, req.bank, req.program, req.channel, req.live) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/sf/note_on") async def sf_note_on(req: SfNoteRequest): try: return _svc().sf_note_on(req.track_id, req.channel, req.pitch, req.velocity) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/sf/note_off") async def sf_note_off(req: SfNoteOffRequest): try: return _svc().sf_note_off(req.track_id, req.channel, req.pitch) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/sf/audio_stop") async def sf_audio_stop(req: SfNoteOffRequest): return _svc().sf_audio_stop(req.track_id) @router.post("/vst2/ensure") async def vst2_ensure(req: Vst2EnsureRequest): path = _resolve_plugin_path(req.plugin_id, req.plugin_path) if not path or not os.path.exists(path): raise HTTPException(status_code=400, detail=f"khong tim thay plugin " f"(plugin_id={req.plugin_id!r} plugin_path={req.plugin_path!r})") try: return _svc().ensure_vst2(req.track_id, path, req.live) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/vst2/note_on") async def vst2_note_on(req: Vst2NoteRequest): try: return _svc().vst2_note_on(req.track_id, req.channel, req.pitch, req.velocity) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/vst2/note_off") async def vst2_note_off(req: Vst2NoteOffRequest): try: return _svc().vst2_note_off(req.track_id, req.channel, req.pitch) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/vst3/ensure") async def vst3_ensure(req: Vst3EnsureRequest): path = _resolve_plugin_path(req.plugin_id, req.plugin_path) if not path or not os.path.exists(path): raise HTTPException(status_code=400, detail=f"khong tim thay plugin " f"(plugin_id={req.plugin_id!r} plugin_path={req.plugin_path!r})") try: return _svc().ensure_vst3(req.track_id, path, req.live) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/vst3/note_on") async def vst3_note_on(req: Vst3NoteRequest): try: return _svc().vst3_note_on(req.track_id, req.channel, req.pitch, req.velocity) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/vst3/note_off") async def vst3_note_off(req: Vst3NoteOffRequest): try: return _svc().vst3_note_off(req.track_id, req.channel, req.pitch) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/render") async def render(req: RenderRequest): """Offline render cho test matrix spec V. Trả về số liệu (không trả audio bytes) — peak/rms đủ để so sánh âm lượng/mastering.""" svc = _svc() notes = [n.model_dump() for n in req.notes] try: if req.kind == "sf": y = svc.render_sf_offline(req.path, req.bank, req.program, notes, req.bpm, gain_db=req.gain_db, pan=req.pan, lim_active=req.lim_active, threshold_db=req.threshold_db, master_gain_db=req.master_gain_db) elif req.kind == "vst2": y = svc.render_vst2_offline(req.path, notes, req.bpm, gain_db=req.gain_db, pan=req.pan, lim_active=req.lim_active, threshold_db=req.threshold_db, master_gain_db=req.master_gain_db) else: raise HTTPException(status_code=400, detail="kind phải là 'sf' hoặc 'vst2'") except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) import numpy as np return {"ok": True, "kind": req.kind, "samples": int(y.shape[1]), "peak": float(np.abs(y).max()), "rms": float(np.sqrt((y ** 2).mean()))}