FIX: Carla sử dụng bridge
This commit is contained in:
+131
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user