feat: tách luồng âm instrument theo môi trường — docker dùng FluidSynthWASM, standalone dùng native pyfluidsynth (+Carla VSTi)

- runtime: detect()/capabilities() trả environment (docker|standalone)
- vst_engine: render_soundfont_midi_to_audio (pyfluidsynth native, event-stream, release tail)
- plugins: POST /soundfont-render — render MIDI notes -> WAV (auth, fallback default SF)
- frontend: SonicRuntime.environment + dataset.environment; app.jsx route preview/transport:
  standalone+SF -> soundfont-render WAV (playNativeSfNote/scheduleNativeSfItem),
  docker giữ WASM, VSTi standalone giữ Carla bridge
This commit is contained in:
2026-08-10 20:38:54 +07:00
parent e89a594b7c
commit a0d8725541
8 changed files with 447 additions and 48 deletions
+69 -2
View File
@@ -6,8 +6,8 @@ from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional, Any
from app.config import settings
from app.core.vst_engine import PluginManager, HAS_PEDALBOARD, HAS_PYFLUIDSYNTH, apply_preset_to_plugin
from app.core.render_engine import PythonRenderEngine
from app.core.vst_engine import PluginManager, HAS_PEDALBOARD, HAS_PYFLUIDSYNTH, apply_preset_to_plugin, render_soundfont_midi_to_audio
from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_default_sf2
from app.core.soundfont_inspector import SoundFontInspector
from app.core.soundfont_converter import SoundFontConverter
from app.core.soundfont_scanner import SoundFontAutoScanner
@@ -992,6 +992,73 @@ def _render_midi_notes_pedalboard(instrument_id: str, notes: list, bpm: float,
return out_path, buf.shape[1] / float(sample_rate)
class SoundfontRenderRequest(BaseModel):
"""Render MIDI notes → WAV bằng soundfont THẬT qua native FluidSynth
(pyfluidsynth) — luồng âm instrument cho môi trường STANDALONE (xử lí
trực tiếp trên OS; docker dùng FluidSynthWASM ở client)."""
soundfont_id: Optional[str] = None
soundfont_path: Optional[str] = None
bank: int = 0
program: int = 0
notes: list = []
bpm: float = 120.0
sample_rate: int = 44100
@router.post("/soundfont-render")
async def soundfont_render(req: SoundfontRenderRequest, current_user: dict = Depends(get_current_user)):
"""Preview/play MIDI notes bằng âm soundfont thật — native FluidSynth trên OS.
Khác FluidSynthWASM (docker): âm render server-side bằng pyfluidsynth, trả
WAV để client play. Dùng cho: preview/keyboard, play MIDI item khi chạy
transport ở môi trường standalone."""
enforce_password_changed(current_user)
if not req.notes:
raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để render")
if not HAS_PYFLUIDSYNTH:
raise HTTPException(status_code=501, detail="pyfluidsynth không khả dụng — không render được soundfont native")
# Resolve path: soundfont_path → soundfont_id (upload/user dirs) → default
sf_path = (req.soundfont_path or "").strip()
if not sf_path and req.soundfont_id:
sf_path = _find_sf2_path(req.soundfont_id)
if not sf_path or not os.path.exists(sf_path):
sf_path = _find_default_sf2()
if not sf_path or not os.path.exists(sf_path):
raise HTTPException(status_code=404, detail="SoundFont không tìm thấy — hãy thêm soundfont vào Plugin Manager")
midi_events = [{
"note": int(n.get("pitch", 60)),
"start_beat": float(n.get("start_beat", 0)),
"duration_beats": float(n.get("duration_beats", 1)),
"velocity": int(float(n.get("velocity", 0.8)) * 127),
} for n in req.notes]
out_path = os.path.join(settings.PROCESSED_DIR, f"sf_{uuid.uuid4().hex[:10]}.wav")
try:
audio = render_soundfont_midi_to_audio(
midi_events, sf_path,
bank=req.bank, program=req.program,
sr=req.sample_rate, bpm=req.bpm,
)
sf.write(out_path, audio.T, req.sample_rate)
return {
"success": True,
"file_id": os.path.basename(out_path),
"url": f"/static/audio/processed/{os.path.basename(out_path)}",
"path": out_path,
"duration_sec": round(audio.shape[1] / float(req.sample_rate), 3),
"render_mode": "native-fluidsynth",
"soundfont": os.path.basename(sf_path),
}
except HTTPException:
raise
except Exception as e:
try:
if os.path.exists(out_path):
os.remove(out_path)
except Exception:
pass
raise HTTPException(status_code=500, detail=f"Render soundfont thất bại: {e}")
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