Files
SonicForgeStudio/app/api/v1/native.py
T

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()))}