5ad2c681e7
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).
228 lines
8.3 KiB
Python
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))
|