Remove Carla native VSTi bridge entirely
Drop Carla integration across frontend and backend: - plugins.py: remove /carla-midi, /carla-status, /carla-stop, /open-in-carla, /carla-play-notes endpoints and Carla process management - runtime.py: remove find_carla / _carla_* helpers, detect and capabilities - app.jsx: remove Carla bridge UI, SonicCarlaMidi routing, shouldRouteCarla guards, VST row Carla button and preset fallback - system.py: remove /carla-path - Keep audio path unchanged (tracks -> masterBus -> output -> analyser), directOut only for offline render Build frontend, tests pass (124 passed).
This commit is contained in:
+7
-437
@@ -1,7 +1,7 @@
|
||||
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Any
|
||||
@@ -609,13 +609,6 @@ 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 FxGuiRequest(BaseModel):
|
||||
"""Mở native GUI của VST FX qua bridge (Windows desktop)."""
|
||||
path: str
|
||||
@@ -649,170 +642,6 @@ class PreviewRequest(BaseModel):
|
||||
preset_data: Optional[str] = None # base64 bytes .vstpreset (project nhúng)
|
||||
|
||||
|
||||
class CarlaMidiRequest(BaseModel):
|
||||
"""Gửi MIDI note từ track ARM → Carla (OSC /Carla/0/note_on|note_off).
|
||||
|
||||
Carla standalone bật OSC UDP mặc định cổng 22752 (source: CarlaEngineOsc,
|
||||
CarlaEngineData oscPortUDP=22752; override: env CARLA_OSC_UDP_PORT của
|
||||
Carla, hoặc SF_CARLA_OSC_PORT / osc_port trong carla_path.json của app).
|
||||
Plugin đầu tiên trong project .carxs do app sinh có pluginId = 0."""
|
||||
event: str # "note_on" | "note_off"
|
||||
note: int
|
||||
velocity: Optional[int] = 100
|
||||
channel: Optional[int] = 0
|
||||
|
||||
|
||||
@router.post("/carla-midi")
|
||||
async def carla_midi(req: CarlaMidiRequest):
|
||||
"""MIDI keyboard (piano roll / keybed) → Carla để preview VSTi realtime."""
|
||||
if req.event not in ("note_on", "note_off"):
|
||||
raise HTTPException(status_code=400, detail="event phải là note_on hoặc note_off")
|
||||
ok = _send_carla_osc(req.event, req.note, req.velocity if req.event == "note_on" else 0, req.channel)
|
||||
if not ok:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Không gửi được OSC tới Carla — Carla đã mở chưa? (cổng OSC UDP mặc định 22752; "
|
||||
"nếu đổi cổng trong Carla, đặt SF_CARLA_OSC_PORT hoặc osc_port trong carla_path.json)",
|
||||
)
|
||||
return {"success": True, "event": req.event, "note": req.note, "channel": req.channel}
|
||||
|
||||
|
||||
def _carla_osc_port() -> int:
|
||||
"""Cổng OSC UDP của Carla: SF_CARLA_OSC_PORT env → osc_port trong
|
||||
storage/carla_path.json → mặc định 22752 (CarlaEngineData)."""
|
||||
try:
|
||||
p = os.environ.get("SF_CARLA_OSC_PORT")
|
||||
if p:
|
||||
return int(p)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from app.core.runtime import _carla_config_path
|
||||
with open(_carla_config_path(), "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
p = data.get("osc_port")
|
||||
if p:
|
||||
return int(p)
|
||||
except Exception:
|
||||
pass
|
||||
return 22752
|
||||
|
||||
|
||||
def _carla_osc_ready() -> bool:
|
||||
"""Carla OSC engine đang lắng nghe cổng UDP chưa — thử bind socket vào đúng
|
||||
cổng. Bind THÀNH CÔNG → cổng trống (Carla chưa mở / OSC chưa sẵn sàng);
|
||||
EADDRINUSE → Carla đang giữ cổng (kể cả mở thủ công ngoài app, hoặc app
|
||||
spawn đang boot tới giai đoạn OSC). KHÔNG set SO_REUSEADDR (Linux/Win cho
|
||||
phép 2 UDP socket cùng port → mất tác dụng phát hiện)."""
|
||||
try:
|
||||
import socket
|
||||
port = _carla_osc_port()
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.bind(("127.0.0.1", port))
|
||||
s.close()
|
||||
return False
|
||||
except OSError:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _send_carla_osc(event: str, note: int, velocity: int, channel: int) -> bool:
|
||||
"""Gửi OSC UDP tới `/Carla/0/{event}` (plugin đầu tiên = plugin auto-load).
|
||||
|
||||
OSC message: path + typetag + int args, mỗi phần pad '\0' tới bội số 4.
|
||||
Đã xác minh từ source Carla: handleMsgNoteOn/NoteOff nhận `iii`/`ii` và
|
||||
tên client mặc định của app standalone là "Carla" (carla_host.py
|
||||
fClientName = CARLA_CLIENT_NAME or "Carla")."""
|
||||
try:
|
||||
import socket
|
||||
import struct
|
||||
port = _carla_osc_port()
|
||||
path = f"/Carla/0/{event}".encode("utf-8")
|
||||
typetag = b",iii" if event == "note_on" else b",ii"
|
||||
vals = [int(channel), int(note), int(velocity)] if event == "note_on" else [int(channel), int(note)]
|
||||
|
||||
def _pad(b: bytes) -> bytes:
|
||||
rem = len(b) % 4
|
||||
return b + b"\x00" * (4 - rem) if rem else b
|
||||
|
||||
msg = _pad(path) + _pad(typetag) + b"".join(struct.pack(">i", v) for v in vals)
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(0.5)
|
||||
s.sendto(msg, ("127.0.0.1", port))
|
||||
s.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@router.post("/open-in-carla")
|
||||
async def open_in_carla(req: OpenInCarlaRequest, current_user: dict = Depends(get_current_user)):
|
||||
"""Mở Carla với VSTi đã chọn — TỰ ĐỘNG load plugin (native GUI + keyboard).
|
||||
|
||||
Cơ chế: sinh file project .carxs (định dạng XML chính thức của Carla —
|
||||
`carla.exe [FILE]` nhận project file) chứa node <Plugin><Info><Type>VST3
|
||||
</Type><Binary>...</Binary></Info> → Carla mở lên là plugin đã load sẵn,
|
||||
kèm on-screen MIDI keyboard (PixmapKeyboard) để preview realtime.
|
||||
|
||||
Carla là app ngoài do user tự giải nén (GPL-2.0+ → không bundle/nhúng);
|
||||
app chỉ spawn tiến trình + trao đổi file preset."""
|
||||
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 = ""
|
||||
carxs_path = ""
|
||||
if plugin_path:
|
||||
carxs_path = _write_carla_project(req.plugin_name or os.path.basename(plugin_path), plugin_path)
|
||||
# ⚠️ FIX (Bug 2): Carla ĐÃ chạy (mở thủ công / app spawn từ trước, hoặc
|
||||
# đang boot) → KHÔNG spawn bản 2 (trùng OSC port 22752 + audio → bridge
|
||||
# hỏng; Carla là single-instance). Trả already_running — frontend poll
|
||||
# carla-status chờ OSC ready rồi mới gửi note.
|
||||
if _carla_osc_ready() or len(_CARLA_PROCESSES) > 0:
|
||||
return {
|
||||
"success": True,
|
||||
"started": False,
|
||||
"already_running": True,
|
||||
"carla_path": carla,
|
||||
"plugin_path": plugin_path,
|
||||
"project_file": carxs_path,
|
||||
"cmd": [],
|
||||
}
|
||||
cmd = [carla]
|
||||
if carxs_path:
|
||||
cmd.append(carxs_path)
|
||||
try:
|
||||
cwd = os.path.dirname(carla) or None
|
||||
proc = subprocess.Popen(cmd, cwd=cwd, close_fds=os.name != "nt",
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
|
||||
_register_carla_process(proc)
|
||||
return {
|
||||
"success": True,
|
||||
"started": True,
|
||||
"carla_path": carla,
|
||||
"plugin_path": plugin_path,
|
||||
"project_file": carxs_path,
|
||||
"cmd": cmd,
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Không mở được Carla: {e}")
|
||||
|
||||
@router.post("/fx-gui")
|
||||
async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_current_user)):
|
||||
"""Mở native GUI của VST FX (mastering chain / FX rack) qua bridge.
|
||||
@@ -941,199 +770,6 @@ def _prune_fx_gui_processes():
|
||||
|
||||
|
||||
|
||||
# ── Carla bridge lifecycle ─────────────────────────────────────────────────
|
||||
# App spawn Carla (open_in_carla) và PHẢI có khả năng dừng nó khi track chuyển
|
||||
# sang instrument KHÔNG phải VST (soundfont/GM) — nếu không, Carla vẫn chạy và
|
||||
# MIDI vẫn vào VSTi cũ → âm sai instrument + âm kẹt không dừng được.
|
||||
_CARLA_PROCESSES = [] # list[subprocess.Popen]
|
||||
|
||||
|
||||
def _register_carla_process(proc):
|
||||
"""Lưu handle tiến trình Carla do app spawn (để carla-stop terminate được)."""
|
||||
global _CARLA_PROCESSES
|
||||
_prune_carla_processes()
|
||||
_CARLA_PROCESSES.append(proc)
|
||||
|
||||
|
||||
def _prune_carla_processes():
|
||||
"""Bỏ các handle đã thoát (poll() trả code) — tránh list rác."""
|
||||
global _CARLA_PROCESSES
|
||||
_CARLA_PROCESSES = [p for p in _CARLA_PROCESSES if p is not None and p.poll() is None]
|
||||
|
||||
|
||||
def _send_carla_all_notes_off() -> bool:
|
||||
"""Gửi note_off TẤT CẢ pitch (0-127) trên mọi channel (0-15) tới Carla.
|
||||
|
||||
Dừng mọi âm đang ngân trong Carla (kể cả sustain) — dùng ngay trước khi
|
||||
terminate để không còn tiếng kẹt khi bridge bị unload."""
|
||||
ok = False
|
||||
try:
|
||||
for ch in range(16):
|
||||
for note in range(128):
|
||||
if _send_carla_osc("note_off", note, 0, ch):
|
||||
ok = True
|
||||
except Exception:
|
||||
pass
|
||||
return ok
|
||||
|
||||
|
||||
@router.get("/carla-status")
|
||||
async def carla_status():
|
||||
"""Kiểm tra Carla bridge còn sống không (OSC engine đang nghe cổng UDP —
|
||||
phát hiện CẢ Carla mở thủ công ngoài app, không chỉ app-spawn; chính xác
|
||||
hơn _CARLA_PROCESSES vì OSC bind khi Carla boot xong engine).
|
||||
|
||||
Frontend dùng để quyết định: route MIDI item EXCLUSIVE qua Carla (chỉ khi
|
||||
Carla đang chạy) hay fallback FluidSynth (luôn có âm) — tránh câm toàn
|
||||
phần khi Carla bị đóng. running=OSC ready → gửi note là tới được Carla."""
|
||||
_prune_carla_processes()
|
||||
return {
|
||||
"success": True,
|
||||
"running": _carla_osc_ready(),
|
||||
"app_spawned": len(_CARLA_PROCESSES) > 0,
|
||||
"osc_port": _carla_osc_port(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/carla-stop")
|
||||
async def carla_stop(authorization: Optional[str] = Header(None)):
|
||||
"""Unload Carla bridge: tắt mọi note đang ngân + terminate tiến trình Carla
|
||||
do app spawn. Gọi khi track chuyển từ VSTi sang instrument khác (soundfont)
|
||||
để âm KHÔNG còn play qua Carla bridge."""
|
||||
# Auth là optional (desktop app có thể chưa login) — chỉ cần decode nếu có
|
||||
try:
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
from app.core.auth import decode_token
|
||||
decode_token(authorization.split(" ")[1])
|
||||
except Exception:
|
||||
pass
|
||||
# 1. Tắt hết âm đang ngân trong Carla (trước khi giết tiến trình)
|
||||
try:
|
||||
_send_carla_all_notes_off()
|
||||
except Exception:
|
||||
pass
|
||||
# 2. Terminate tiến trình Carla đã spawn
|
||||
killed = 0
|
||||
_prune_carla_processes()
|
||||
for proc in list(_CARLA_PROCESSES):
|
||||
try:
|
||||
proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
# Chờ tiến trình thoát (tối đa ~2s) rồi kill mạnh nếu còn sống
|
||||
try:
|
||||
import time as _t
|
||||
deadline = _t.time() + 2.0
|
||||
for proc in list(_CARLA_PROCESSES):
|
||||
while proc.poll() is None and _t.time() < deadline:
|
||||
_t.sleep(0.05)
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
if proc.poll() is not None:
|
||||
killed += 1
|
||||
except Exception:
|
||||
pass
|
||||
_CARLA_PROCESSES.clear()
|
||||
return {
|
||||
"success": True,
|
||||
"stopped": True,
|
||||
"killed": killed,
|
||||
"all_notes_off": True,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def _write_carla_project(plugin_name: str, plugin_path: str) -> str:
|
||||
"""Sinh file project .carxs cho Carla load sẵn VSTi (chỉ VST3 — VST2 cần
|
||||
uniqueID không đoán được → mở Carla trống để user tự Add Plugin).
|
||||
|
||||
Định dạng theo source Carla (CarlaEngine::saveProjectInternal +
|
||||
CarlaStateSave::dumpToMemoryStream): root <CARLA-PROJECT VERSION='2.5'>,
|
||||
<Plugin><Info><Type>VST3</Type><Binary>path</Binary><Label>..</Label>
|
||||
</Info><Data><Active>Yes</Active><ControlChannel>1</ControlChannel>
|
||||
<Options>0x0</Options></Data></Plugin>."""
|
||||
if not plugin_path or not os.path.exists(plugin_path):
|
||||
return ""
|
||||
low = plugin_path.lower()
|
||||
is_vst3 = low.endswith(".vst3") or low.endswith(".vst3/") or "\\" in plugin_path and plugin_path.rstrip("\\/").lower().endswith(".vst3")
|
||||
if not is_vst3:
|
||||
# VST2 (.dll/.so ngoài .vst3): không auto-load được tin cậy → Carla trống
|
||||
return ""
|
||||
from xml.sax.saxutils import escape
|
||||
name = escape(plugin_name or os.path.splitext(os.path.basename(plugin_path))[0])
|
||||
binary = escape(plugin_path)
|
||||
# Patchbay: kết nối TỰ ĐỘNG MIDI input + audio output → default speaker.
|
||||
# Mặc định Carla KHÔNG connect khi load project (chỉ restore connection có
|
||||
# trong file) → lần đầu mở không có âm, không nhận MIDI (phải unload/load
|
||||
# lại mới auto-connect như thêm plugin thủ công). Connection nào trỏ tới
|
||||
# port không tồn tại sẽ bị Carla bỏ qua im lặng → thêm nhiều biến thể tên.
|
||||
pb_name = name
|
||||
patchbay = (
|
||||
" <Patchbay>\n"
|
||||
# Audio out plugin → loa mặc định (system:playback_1/2)
|
||||
f" <Connection><Source>{pb_name}:audio_out1</Source><Target>system:playback_1</Target></Connection>\n"
|
||||
f" <Connection><Source>{pb_name}:audio_out2</Source><Target>system:playback_2</Target></Connection>\n"
|
||||
# MIDI input → plugin midi_in (biến thể tên client: Carla / carla / system)
|
||||
f" <Connection><Source>Carla:midi_in</Source><Target>{pb_name}:midi_in</Target></Connection>\n"
|
||||
f" <Connection><Source>carla:midi_in</Source><Target>{pb_name}:midi_in</Target></Connection>\n"
|
||||
f" <Connection><Source>system:midi_capture_1</Source><Target>{pb_name}:midi_in</Target></Connection>\n"
|
||||
" </Patchbay>\n"
|
||||
)
|
||||
xml = (
|
||||
"<?xml version='1.0' encoding='UTF-8'?>\n"
|
||||
"<!DOCTYPE CARLA-PROJECT>\n"
|
||||
f"<CARLA-PROJECT VERSION='2.5'>\n"
|
||||
" <EngineSettings>\n"
|
||||
" <ForceStereo>false</ForceStereo>\n"
|
||||
" <PreferPluginBridges>false</PreferPluginBridges>\n"
|
||||
" <PreferUiBridges>false</PreferUiBridges>\n"
|
||||
" <UIsAlwaysOnTop>false</UIsAlwaysOnTop>\n"
|
||||
" <MaxParameters>100</MaxParameters>\n"
|
||||
" <UIBridgesTimeout>10000</UIBridgesTimeout>\n"
|
||||
" </EngineSettings>\n"
|
||||
" <Plugin>\n"
|
||||
" <Info>\n"
|
||||
f" <Type>VST3</Type>\n"
|
||||
f" <Name>{name}</Name>\n"
|
||||
f" <Binary>{binary}</Binary>\n"
|
||||
f" <Label>{name}</Label>\n"
|
||||
" </Info>\n"
|
||||
" <Data>\n"
|
||||
" <Active>Yes</Active>\n"
|
||||
" <ControlChannel>1</ControlChannel>\n"
|
||||
" <Options>0x0</Options>\n"
|
||||
" </Data>\n"
|
||||
" </Plugin>\n"
|
||||
+ patchbay +
|
||||
"</CARLA-PROJECT>\n"
|
||||
)
|
||||
try:
|
||||
proj_dir = os.path.join(settings.STORAGE_DIR, "carla_projects")
|
||||
os.makedirs(proj_dir, exist_ok=True)
|
||||
# Dọn project cũ (quá 1 ngày) — tránh rác
|
||||
try:
|
||||
now = _time.time()
|
||||
for f in os.listdir(proj_dir):
|
||||
fp = os.path.join(proj_dir, f)
|
||||
try:
|
||||
if os.path.isfile(fp) and now - os.path.getmtime(fp) > 86400:
|
||||
os.remove(fp)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
safe = "".join(c for c in plugin_name if c.isalnum() or c in " _-")[:40].strip() or "plugin"
|
||||
carxs = os.path.join(proj_dir, f"{safe}_{uuid.uuid4().hex[:8]}.carxs")
|
||||
with open(carxs, "w", encoding="utf-8") as fh:
|
||||
fh.write(xml)
|
||||
return carxs
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
async def preview_instrument(req: PreviewRequest):
|
||||
"""Quick-render preview VSTi (âm thật, cùng code path với export)."""
|
||||
@@ -1171,8 +807,8 @@ class MidiRenderRequest(BaseModel):
|
||||
"""Render MIDI notes → audio qua VSTi (âm thật, cùng code path với export).
|
||||
|
||||
instrument_id = plugin_id của track synth_engine (khớp key scan của
|
||||
PluginManager). Đây là cầu nối Carla → pedalboard: preset chỉnh trong
|
||||
Carla (.vstpreset) được áp vào pedalboard trước khi render."""
|
||||
PluginManager). Đây là cầu nối preset → pedalboard: preset chỉnh trong
|
||||
native GUI (.vstpreset) được áp vào pedalboard trước khi render."""
|
||||
instrument_id: str
|
||||
notes: list = []
|
||||
bpm: float = 120.0
|
||||
@@ -1187,7 +823,7 @@ async def midi_render(req: MidiRenderRequest, current_user: dict = Depends(get_c
|
||||
"""Export MIDI notes → WAV với âm của VSTi instrument (lưu vào processed).
|
||||
|
||||
Preview/export MIDI notes với âm VSTi trước đây KHÔNG thực hiện được:
|
||||
- preview realtime chỉ phát qua loa Carla (không vào audio graph DAW)
|
||||
- preview realtime chỉ phát qua loa hệ thống (không vào audio graph DAW)
|
||||
- clientSideExport chỉ render soundfont (midiCache = FluidSynth WASM)
|
||||
- /plugins/preview có sẵn nhưng frontend không gọi
|
||||
Endpoint này render offline bằng pedalboard (đúng plugin + preset như
|
||||
@@ -1233,9 +869,9 @@ async def midi_render(req: MidiRenderRequest, current_user: dict = Depends(get_c
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Render MIDI thất bại: {e}. Nếu plugin VSTi không load được "
|
||||
"qua native_bridge, hãy mở Carla Bridge (chọn VSTi, chỉnh âm, "
|
||||
"Save preset .vstpreset) rồi Upload preset vào track — render "
|
||||
"sẽ dùng đúng âm đã chỉnh.",
|
||||
"qua native_bridge, hãy chỉnh âm trong native GUI (Save preset "
|
||||
".vstpreset) rồi Upload preset vào track — render sẽ dùng "
|
||||
"đúng âm đã chỉnh.",
|
||||
)
|
||||
|
||||
|
||||
@@ -1517,72 +1153,6 @@ 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 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
|
||||
plugin đã load (open-in-carla)."""
|
||||
notes: list = []
|
||||
bpm: float = 120.0
|
||||
channel: Optional[int] = 0
|
||||
|
||||
|
||||
@router.post("/carla-play-notes")
|
||||
async def carla_play_notes(req: CarlaPlayNotesRequest, current_user: dict = Depends(get_current_user)):
|
||||
"""Phát toàn bộ dãy MIDI notes vào Carla (note_on/note_off đúng thời điểm).
|
||||
|
||||
Preview realtime qua đúng VSTi đang mở trong Carla — dùng khi pedalboard
|
||||
không load được plugin (VST2, plugin cần state GUI). Âm phát ra loa hệ
|
||||
thống (Carla), KHÔNG thu vào file — muốn file audio dùng /midi-render."""
|
||||
enforce_password_changed(current_user)
|
||||
if not req.notes:
|
||||
raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để phát")
|
||||
if not find_carla_local():
|
||||
raise HTTPException(status_code=409, detail="Carla chưa được định vị (Plugin Manager → Carla Bridge → Định vị Carla...)")
|
||||
if not _carla_bridge_running():
|
||||
raise HTTPException(status_code=409, detail="Carla chưa mở. Hãy mở Carla Bridge (nút Synth → VSTi) trước khi preview qua Carla.")
|
||||
beat_sec = 60.0 / max(30.0, req.bpm)
|
||||
channel = int(req.channel or 0)
|
||||
events = []
|
||||
for n in req.notes:
|
||||
pitch = int(n.get("pitch", 60))
|
||||
vel = int(float(n.get("velocity", 0.8)) * 127)
|
||||
start = float(n.get("start_beat", 0)) * beat_sec
|
||||
dur = float(n.get("duration_beats", 1)) * beat_sec
|
||||
events.append((start, "note_on", pitch, max(1, min(127, vel))))
|
||||
events.append((start + dur, "note_off", pitch, 0))
|
||||
events.sort(key=lambda e: (e[0], 0 if e[1] == "note_off" else 1))
|
||||
duration_sec = max((e[0] for e in events), default=0) + 0.3
|
||||
# Phát trong thread nền — endpoint trả ngay, không block tới hết bản nhạc
|
||||
def _player():
|
||||
import time as _t
|
||||
t0 = _t.monotonic()
|
||||
for ev_time, ev_type, pitch, vel in events:
|
||||
wait = (t0 + ev_time) - _t.monotonic()
|
||||
if wait > 0:
|
||||
_t.sleep(wait)
|
||||
_send_carla_osc(ev_type, pitch, vel, channel)
|
||||
threading.Thread(target=_player, daemon=True).start()
|
||||
return {"success": True, "mode": "carla_realtime", "duration_sec": round(duration_sec, 3), "events": len(events)}
|
||||
|
||||
|
||||
def find_carla_local() -> str:
|
||||
from app.core.runtime import find_carla as _fc
|
||||
try:
|
||||
return _fc() or ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _carla_bridge_running() -> bool:
|
||||
try:
|
||||
_prune_carla_processes()
|
||||
# OSC ready (mở thủ công / app spawn đã boot) HOẶC tiến trình app-spawn
|
||||
# còn sống (đang boot) — preview chấp nhận gửi sớm một chút.
|
||||
return _carla_osc_ready() or len(_CARLA_PROCESSES) > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@router.post("/render")
|
||||
def render_project(
|
||||
req: RenderRequest,
|
||||
|
||||
Reference in New Issue
Block a user