Files
SonicForgeStudio/app/core/runtime.py
admin 5ad2c681e7 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).
2026-08-22 18:39:49 +07:00

228 lines
8.3 KiB
Python

# SonicForge Runtime Profile — phát hiện môi trường chạy (desktop Windows /
# docker headless) và khả năng của máy, để app TỰ CHỌN cách xử lý:
# - Windows desktop (server + client cùng 1 máy): native_bridge render VSTi
# - Docker / Linux headless (server + browser UI): soundfont + VSTi mở được
# từ storage mount; preset upload qua browser; không có GUI local.
#
# Ưu tiên: SF_RUNTIME env override > heuristic (platform + display + docker).
# Module CHỈ dùng stdlib + app.config (không kéo numpy) để import
# nhẹ và hoạt động trong mọi tiến trình (web/worker/celery).
import os
import sys
import socket
import functools
from app.config import settings
SF_RUNTIME_ENV = "SF_RUNTIME" # auto | desktop | headless
SF_DOCKER_ENV = "SF_DOCKER" # 1 = chạy trong container Docker
def _is_windows() -> bool:
return os.name == "nt"
def _is_macos() -> bool:
return sys.platform == "darwin"
def _is_linux() -> bool:
return not _is_windows() and not _is_macos()
def _in_docker() -> bool:
if os.environ.get(SF_DOCKER_ENV) == "1":
return True
# Marker chuẩn của Docker (không tồn tại trên máy host thường)
try:
return os.path.exists("/.dockerenv")
except Exception:
return False
def _has_display() -> bool:
"""Có môi trường GUI hiển thị được hay không.
Windows/macOS luôn có (desktop). Linux cần biến DISPLAY (X11) — nếu chạy
Docker không có DISPLAY → headless."""
if _is_windows() or _is_macos():
return True
return bool(os.environ.get("DISPLAY"))
def _tauri_bridge_ready() -> bool:
"""App desktop Tauri viết marker file lúc setup (xem plugins.py
_pick_dir_via_tauri_bridge) — dùng để nhận diện bản desktop app."""
try:
root = os.environ.get("APPDATA") or os.path.expanduser("~")
return os.path.exists(os.path.join(root, "SonicForgeDAW", "ipc", "tauri_bridge_ready"))
except Exception:
return False
def _detect_platform() -> str:
if _is_windows():
return "windows"
if _is_macos():
return "darwin"
return "linux"
def _detect_runtime() -> str:
"""auto → desktop nếu có GUI (Windows/macOS luôn; Linux cần DISPLAY và
không phải docker), ngược lại headless. SF_RUNTIME override thắng."""
override = (os.environ.get(SF_RUNTIME_ENV) or "auto").strip().lower()
if override in ("desktop", "headless"):
return override
if _is_windows() or _is_macos():
return "desktop"
if _in_docker() or not _has_display():
return "headless"
return "desktop"
def default_vst_dirs() -> list:
"""Thư mục VST mặc định theo platform (vẫn ưu tiên env VST_DIR).
Windows: thư mục chuẩn VST3 của hệ thống; user thường khai báo thêm qua
Plugins Manager (plugin_dirs.json)."""
if settings.VST_DIR and settings.VST_DIR != "/opt/daw_engine/vst3":
return [settings.VST_DIR]
if _is_windows():
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
return [os.path.join(pf, "Common Files", "VST3"), os.path.join(pf, "VSTPlugins")]
if _is_macos():
return ["/Library/Audio/Plug-Ins/VST3", os.path.expanduser("~/Library/Audio/Plug-Ins/VST3")]
return ["/opt/daw_engine/vst3", os.path.expanduser("~/.vst3")]
def default_soundfont_dirs() -> list:
if settings.SOUNDFONT_DIR and settings.SOUNDFONT_DIR != "/opt/daw_engine/soundfonts":
return [settings.SOUNDFONT_DIR]
if _is_windows():
root = os.environ.get("APPDATA") or os.path.expanduser("~")
return [os.path.join(root, "SonicForgeDAW", "soundfonts")]
if _is_macos():
return [os.path.expanduser("~/Music/SonicForgeDAW/soundfonts")]
return ["/opt/daw_engine/soundfonts", os.path.expanduser("~/.sf2")]
def _lan_ips() -> list:
"""IPv4 của máy trên mạng LAN (bỏ loopback) — để client browser ở máy
khác mở app qua địa chỉ này. Stdlib only (không kéo thư viện ngoài)."""
ips = []
try:
# IP ra mạng theo default route — không gửi gói tin thật
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
if ip and not ip.startswith("127.") and ip not in ips:
ips.append(ip)
except Exception:
pass
# Enumerate thêm các interface khác (multi-NIC)
try:
for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
ip = info[4][0]
if ip and not ip.startswith("127.") and ip not in ips:
ips.append(ip)
except Exception:
pass
return ips
@functools.lru_cache(maxsize=1)
def detect() -> dict:
"""Detect 1 lần (cache toàn cục) — kết quả bất biến trong 1 tiến trình."""
platform = _detect_platform()
runtime = _detect_runtime()
docker = _in_docker()
return {
"platform": platform,
"runtime": runtime, # "desktop" | "headless"
"docker": docker,
"environment": "docker" if docker else "standalone", # luồng âm instrument: docker→FluidSynthWASM, standalone→native OS
"has_display": _has_display(),
"tauri_bridge": _tauri_bridge_ready(),
"vst_render": _vst_render_available(),
"default_vst_dirs": default_vst_dirs(),
"default_soundfont_dirs": default_soundfont_dirs(),
"preset_dir": os.path.join(settings.STORAGE_DIR, "presets"),
}
def _vst_render_available() -> bool:
"""native_bridge có sẵn không: SF_BRIDGE_PATH trỏ file, hoặc exe trong
install/ (không import app.core.native_render để tránh kéo numpy — mirror
logic find_bridge_exe)."""
env = os.getenv("SF_BRIDGE_PATH", "").strip()
if env and os.path.isfile(env):
return True
exe = "daw_vst_bridge.exe" if os.name == "nt" else "daw_vst_bridge"
if os.path.isfile(os.path.join(settings.BASE_DIR, "install", exe)):
return True
# Frozen onedir: bridge sidecar nam canh app root (ngoai _MEIPASS)
if getattr(sys, "frozen", False):
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
for cand in (os.path.join(exe_dir, exe),
os.path.join(os.path.dirname(exe_dir), exe)):
if os.path.isfile(cand):
return True
return False
def _lan_info() -> dict:
"""Địa chỉ + port để client LAN mở app. Port: SF_PORT (desktop engine chọn
trước, 8000-8010) hoặc 8000 mặc định."""
try:
port = int(os.environ.get("SF_PORT") or "8000")
except ValueError:
port = 8000
ips = _lan_ips()
return {
"ips": ips,
"port": port,
"urls": [f"http://{ip}:{port}" for ip in ips],
}
def capabilities() -> dict:
"""Capabilities API — frontend gọi 1 lần lúc boot để bật/tắt tính năng."""
d = detect()
features = {
"preset_upload": True, # cả 2 mode đều upload preset được
"vst_render": d["vst_render"],
"tauri_bridge": d["tauri_bridge"],
# preview VSTi: quick_render (native_bridge render clip ngắn — âm thật,
# giống export) là chuẩn cho cả 2 mode; không có realtime trong web UI.
"preview_mode": "quick_render" if d["vst_render"] else "wasm",
}
return {
"success": True,
"runtime": d["runtime"],
"platform": d["platform"],
"docker": d["docker"],
"environment": d["environment"],
# LAN access (chỉ standalone): URL để client browser ở máy khác trên
# mạng mở app. Docker (container) → bỏ qua (IP container không dùng được).
**({"lan": _lan_info()} if not d["docker"] else {}),
"features": features,
"default_dirs": {
"vst": d["default_vst_dirs"],
"soundfont": d["default_soundfont_dirs"],
"preset": d["preset_dir"],
},
}
def is_desktop() -> bool:
return detect()["runtime"] == "desktop"
def is_headless() -> bool:
return detect()["runtime"] == "headless"
if __name__ == "__main__":
# python -m app.core.runtime → in capabilities để kiểm tra detect
import json
print(json.dumps(capabilities(), ensure_ascii=False, indent=2))