feat: VSTi live playback 100% client-side (không Carla) — autosample SF2 backend (pedalboard) → FluidSynth WASM qua masterBus → mastering → main out; SonicVstiAutosample ensure+dedup+cooldown; runtime gating __enableCarlaLivePlayback; âm bắt qua ensureMidiCapture → clientSideExport

This commit is contained in:
2026-08-11 14:28:42 +07:00
parent 7293d7ac7e
commit cadb5402a3
12 changed files with 676 additions and 44 deletions
+94
View File
@@ -1118,6 +1118,100 @@ async def soundfont_render(req: SoundfontRenderRequest, current_user: dict = Dep
raise HTTPException(status_code=500, detail=f"Render soundfont thất bại: {e}")
class AutosampleRequest(BaseModel):
"""Auto-sample VSTi preset -> SF2 (client FluidSynth WASM live playback).
Server goi tools.autosample_vsti.autosample_sf2 (pedalboard render tung
note voi DUNG plugin + preset nhu export) -> luu {uuid}.sf2 vao
UPLOAD_SF_DIR + .meta -> client tai qua /soundfonts/download/{uuid}.
SF2 only (SF3 can ffmpeg/libvorbis; client WASM khong decode SF3)."""
instrument_id: str
preset_id: Optional[str] = None
preset_path: Optional[str] = None
preset_data: Optional[str] = None # base64 bytes .vstpreset (project nhung)
low: int = 36
high: int = 96
step: int = 2
duration: float = 2.5
release: float = 1.0
velocity: int = 100
sample_rate: int = 44100
@router.post("/autosample")
async def autosample_vsti(req: AutosampleRequest, current_user: dict = Depends(get_current_user)):
"""Auto-sample VSTi -> SF2 cho live playback client-side (FluidSynth WASM).
Tra sf_id (bare uuid) de client tai SF2 va phat qua SonicSF nhu soundfont
thuong. Chi phi autosample mot lan -> cache o client (localStorage)."""
enforce_password_changed(current_user)
if not HAS_PEDALBOARD:
raise HTTPException(status_code=501, detail="pedalboard khong kha dung tren may nay")
# tools/ nam ngoai package app - import voi fallback path
try:
from tools.autosample_vsti import autosample_sf2 as _autosample_sf2
except ImportError:
_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if _root not in sys.path:
sys.path.insert(0, _root)
from tools.autosample_vsti import autosample_sf2 as _autosample_sf2
file_uuid = str(uuid.uuid4())
dest_path = os.path.join(UPLOAD_SF_DIR, file_uuid + ".sf2")
log_lines = []
def _log(msg):
log_lines.append(msg)
print(f"[autosample] {msg}")
try:
result = _autosample_sf2(
req.instrument_id, dest_path,
preset_id=req.preset_id,
preset_path=req.preset_path,
preset_data_b64=req.preset_data,
low=req.low, high=req.high, step=req.step,
duration=req.duration, release=req.release,
velocity=req.velocity, sample_rate=req.sample_rate,
log=_log,
)
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
except RuntimeError as e:
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
try:
if os.path.exists(dest_path):
os.remove(dest_path)
except Exception:
pass
raise HTTPException(status_code=500, detail=f"Auto-sample that bai: {e}")
meta_path = os.path.join(UPLOAD_SF_DIR, file_uuid + ".meta")
try:
with open(meta_path, "w", encoding="utf-8") as f:
json.dump({"original_name": f"autosample_{file_uuid[:8]}.sf2",
"uuid": file_uuid, "file": file_uuid + ".sf2",
"plugin_id": req.instrument_id, "kind": "autosampled"}, f)
except Exception:
pass
try:
get_scanner().scan_once()
except Exception as e:
print(f"[autosample] scan_once failed: {e}")
return {
"success": True,
"sf_id": file_uuid,
"file": file_uuid + ".sf2",
"name": f"autosample_{file_uuid[:8]}.sf2",
"url": f"/api/v1/plugins/soundfonts/download/{file_uuid}",
"size_bytes": result["size_bytes"],
"note_count": result["note_count"],
"plugin_id": req.instrument_id,
}
class CarlaPlayNotesRequest(BaseModel):
"""Phát dãy MIDI notes qua Carla bridge (OSC, realtime) — preview khi
pedalboard không render được plugin (VD VST2). Cần Carla đang chạy với