T15: drop WebAudio master for tracks - native master gain + track gain/pan IPC (SF/VST2/VST3 bridges), NativeAudioService + /api/v1/native router, native-first TrackInstrument, test matrix 8 passed
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
# 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
|
||||
|
||||
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: str
|
||||
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 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()
|
||||
|
||||
|
||||
@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):
|
||||
try:
|
||||
return _svc().ensure_vst2(req.track_id, req.plugin_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("/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()))}
|
||||
Reference in New Issue
Block a user