FIX: Carla sử dụng bridge

This commit is contained in:
2026-08-10 07:57:26 +07:00
parent fbaac1f673
commit aa32272f36
17 changed files with 1372 additions and 34 deletions
+131 -1
View File
@@ -1,10 +1,12 @@
import os, sys, uuid, json, tempfile, subprocess, time as _time
import numpy as np
import soundfile as sf
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks
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
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.soundfont_inspector import SoundFontInspector
from app.core.soundfont_converter import SoundFontConverter
@@ -446,6 +448,134 @@ class RenderRequest(BaseModel):
output_filename: Optional[str] = "render_output.wav"
class OpenInCarlaRequest(BaseModel):
"""Mở native GUI của VSTi trong Carla (chỉ khả dụng khi runtime=desktop
và Carla được cài trên cùng máy — tự phát hiện qua runtime profile)."""
plugin_name: Optional[str] = None
plugin_path: Optional[str] = None
class PreviewRequest(BaseModel):
"""Quick-render preview: pedalboard render clip ngắn bằng ĐÚNG plugin +
preset (cùng code path với export) → trả wav để browser phát.
Âm preview = âm export (khác Preview Synth WASM hiện tại)."""
instrument_id: str
notes: list = []
bpm: float = 120.0
sample_rate: int = 44100
soundfont_bank: Optional[int] = 0
soundfont_program: Optional[int] = 0
preset_id: Optional[str] = None
preset_path: Optional[str] = None
preset_data: Optional[str] = None # base64 bytes .vstpreset (project nhúng)
@router.post("/open-in-carla")
async def open_in_carla(req: OpenInCarlaRequest, current_user: dict = Depends(get_current_user)):
"""Mở Carla (native GUI host) trên máy hiện tại — Windows desktop mode.
Carla là app ngoài do user tự cài (GPL-2.0+ → không bundle/nhúng). App chỉ
spawn tiến trình; user chỉnh preset trong GUI rồi Save → .vstpreset → upload
vào thư viện preset → gán vào track → render engine tải preset tương ứng."""
enforce_password_changed(current_user)
from app.core.runtime import find_carla
carla = find_carla()
if not carla:
raise HTTPException(
status_code=409,
detail="Không tìm thấy Carla trên máy này. Hãy giải nén bản Carla "
"portable (zip, miễn phí) từ https://github.com/falkTX/Carla/releases, "
"rồi vào Plugin Manager → Carla Bridge → Định vị Carla... để chọn "
"thư mục chứa carla.exe (bản portable không dùng PATH).",
)
plugin_path = req.plugin_path or ""
if not plugin_path and req.plugin_name:
try:
pm = PluginManager()
plugins = pm._scan_plugins()
if req.plugin_name in plugins:
plugin_path = plugins[req.plugin_name]
except Exception:
plugin_path = ""
cmd = [carla]
if plugin_path:
# carla-single: mở thẳng 1 plugin thành app standalone có native GUI
single = os.path.join(
os.path.dirname(carla),
"carla-single" + (".exe" if os.name == "nt" else ""),
)
if os.path.isfile(single):
cmd = [single, plugin_path]
try:
cwd = os.path.dirname(carla) or None
subprocess.Popen(cmd, cwd=cwd, close_fds=os.name != "nt")
return {
"success": True,
"started": True,
"carla_path": carla,
"plugin_path": plugin_path,
"cmd": cmd,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Không mở được Carla: {e}")
@router.post("/preview")
async def preview_instrument(req: PreviewRequest):
"""Quick-render preview VSTi (âm thật, cùng code path với export)."""
if not HAS_PEDALBOARD:
raise HTTPException(status_code=501, detail="pedalboard không khả dụng trên máy này")
if not req.notes:
raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để preview")
try:
pm = PluginManager()
vst = pm.load_vst(req.instrument_id)
if vst is None:
raise HTTPException(status_code=404, detail=f"Không tìm thấy VSTi: {req.instrument_id}")
apply_preset_to_plugin(
vst,
preset_id=req.preset_id,
preset_path=req.preset_path,
preset_data_b64=req.preset_data,
)
from pedalboard import Pedalboard
midi_events = []
for n in req.notes:
midi_events.append({
"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),
})
midi_messages = PluginManager.midi_events_to_messages(
midi_events, req.bpm, req.sample_rate,
bank=req.soundfont_bank, program=req.soundfont_program,
)
total_needed = 0
beat_sec = 60.0 / max(30.0, req.bpm)
for ev in midi_events:
end_sec = (ev["start_beat"] + ev["duration_beats"]) * beat_sec
if int(end_sec * req.sample_rate) > total_needed:
total_needed = int(end_sec * req.sample_rate)
total_needed = max(total_needed, 1024)
silent = np.zeros((2, total_needed), dtype=np.float32)
board = Pedalboard([vst])
buf = board(silent, sample_rate=req.sample_rate, midi_messages=midi_messages)
fname = f"preview_{uuid.uuid4().hex[:10]}.wav"
out_path = os.path.join(settings.PROCESSED_DIR, fname)
sf.write(out_path, buf.T, req.sample_rate)
return {
"success": True,
"url": f"/static/audio/processed/{fname}",
"path": out_path,
"duration_sec": round(buf.shape[1] / float(req.sample_rate), 3),
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Preview thất bại: {e}")
@router.post("/render")
async def render_project(
req: RenderRequest,