261 lines
8.0 KiB
Python
261 lines
8.0 KiB
Python
# 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()))}
|