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:
2026-08-22 18:39:49 +07:00
parent 14265515aa
commit 5ad2c681e7
16 changed files with 163 additions and 1295 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ PIANOBK_DIR=/home/locpham/daw_assets/pianobook
# ── Runtime (tự phát hiện môi trường) ──
# auto (mặc định): Windows/macOS → desktop; Linux không DISPLAY hoặc docker → headless.
# desktop: server + client cùng 1 máy (bật nút "Mở trong Carla" nếu có Carla local)
# desktop: server + client cùng 1 máy (native GUI VSTi mở được trên máy)
# headless: server docker + browser UI (soundfont + VSTi mở được từ storage mount;
# preset chỉnh trên máy khác → upload .vstpreset qua web UI)
SF_RUNTIME=auto
+7 -437
View File
@@ -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,
+2 -2
View File
@@ -1,8 +1,8 @@
# SonicForge Preset Library API — thư viện preset VST3 (.vstpreset) nằm trong
# storage/presets (mount qua volume trong docker; thư mục storage trên Windows).
#
# Vai trò: cầu nối Carla → pedalboard. User chỉnh preset trong Carla (native
# GUI) → xuất .vstpreset → upload vào thư viện → gán vào track (preset_id trong
# Vai trò: cầu nối preset → pedalboard. User chỉnh preset trong native GUI
# của VSTi → xuất .vstpreset → upload vào thư viện → gán vào track (preset_id trong
# synth_engine) → render_engine tải qua load_preset → âm render = âm đã chỉnh.
import os
import uuid
+2 -34
View File
@@ -1,10 +1,7 @@
# SonicForge System API — capabilities: frontend gọi 1 lần lúc boot để biết
# môi trường (desktop Windows / docker headless) và bật/tắt tính năng tương ứng.
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional
from app.core.runtime import capabilities, save_carla_path
from app.api.v1.auth import get_current_user, enforce_password_changed
from fastapi import APIRouter
from app.core.runtime import capabilities
router = APIRouter()
@@ -15,37 +12,8 @@ async def get_capabilities():
- runtime: "desktop" (server + client cùng 1 máy Windows/macOS) |
"headless" (docker server + browser UI)
- features.carla_local: có Carla trên máy này → hiện nút "Mở trong Carla"
- features.preset_upload: luôn True (upload .vstpreset qua web UI)
- features.preview_mode: "quick_render" (pedalboard render clip ngắn —
âm thật giống export) | "wasm" (Preview Synth trong browser)
"""
return capabilities()
class CarlaPathRequest(BaseModel):
"""Định vị Carla (bản portable zip không cài đặt/PATH). Chấp nhận đường
dẫn tới carla.exe HOẶC thư mục chứa carla.exe — resolve và lưu config."""
carla_path: str
carla_dir: Optional[str] = None # tương thích ngược: tên cũ của carla_path
@router.post("/carla-path")
async def set_carla_path(req: CarlaPathRequest, current_user: dict = Depends(get_current_user)):
"""Lưu vị trí carla.exe do user chọn (Plugin Manager → Định vị Carla...).
Cần thiết vì bản Carla Windows là bộ file zip portable — không có installer
cũng không dùng biến môi trường PATH, nên heuristic không tìm thấy."""
enforce_password_changed(current_user)
target = (req.carla_path or req.carla_dir or "").strip()
if not target:
raise HTTPException(status_code=400, detail="Thiếu đường dẫn Carla")
exe = save_carla_path(target)
if not exe:
raise HTTPException(
status_code=400,
detail="Không tìm thấy carla.exe trong đường dẫn đã chọn. Hãy chọn "
"thư mục chứa carla.exe (bản portable giải nén) hoặc chính file carla.exe.",
)
return {"success": True, "carla_path": exe, **capabilities()}
+1 -1
View File
@@ -62,7 +62,7 @@ class Settings:
VST_DIR: str = os.getenv("VST_DIR", _default_vst_dir())
SOUNDFONT_DIR: str = os.getenv("SOUNDFONT_DIR", _default_soundfont_dir())
# Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard.
# Thư viện preset VST3 (.vstpreset) — cầu nối preset → pedalboard.
# Nằm trong storage nên tự động nằm trong volume mount (docker) / thư mục
# storage (Windows desktop).
PRESET_DIR: str = os.path.join(STORAGE_DIR, "presets")
+1 -1
View File
@@ -227,7 +227,7 @@ def _apply_rebalance(buf, params, sr):
def _apply_builtin_fx_chain(buf, chain, sr):
"""Serial pipeline qua các builtin custom FX slots (spec §2 Step 2: Slot 1..n
in-place). Slot inactive/bypass → bỏ qua. vst3/carla slots = xử lý ngoài
in-place). Slot inactive/bypass → bỏ qua. vst3 slots = xử lý ngoài
(bridge) → không thêm DSP ở đây."""
for slot in chain or []:
if isinstance(slot, str):
+1 -226
View File
@@ -1,7 +1,6 @@
# 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): Carla local để preview +
# chỉnh preset → native_bridge render (Hướng A)
# - 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.
#
@@ -10,8 +9,6 @@
# nhẹ và hoạt động trong mọi tiến trình (web/worker/celery).
import os
import sys
import json
import shutil
import socket
import functools
@@ -19,10 +16,6 @@ from app.config import settings
SF_RUNTIME_ENV = "SF_RUNTIME" # auto | desktop | headless
SF_DOCKER_ENV = "SF_DOCKER" # 1 = chạy trong container Docker
CARLA_CONFIG_FILE = "carla_path.json" # storage/carla_path.json — user định vị
# Bản Carla portable (zip) có thể giải nén ở BẤT KỲ ĐÂU — PATH/Program Files
# không đủ. User tự chọn thư mục chứa carla.exe qua Plugin Manager → lưu file
# này (ưu tiên cao nhất khi detect), kèm tìm kiếm nông Downloads/Desktop.
def _is_windows() -> bool:
@@ -67,219 +60,6 @@ def _tauri_bridge_ready() -> bool:
return False
def _carla_config_path() -> str:
return os.path.join(settings.STORAGE_DIR, CARLA_CONFIG_FILE)
def get_configured_carla() -> str:
"""Đường dẫn carla.exe do USER tự định vị (lưu trong storage/carla_path.json).
File lưu đường dẫn tới carla.exe (đã resolve) hoặc thư mục chứa — nếu là
thư mục, tìm carla.exe bên trong (độ sâu ≤ 2). Đây là cách bắt buộc có cho
bản Carla portable (zip) giải nén ở vị trí bất kỳ, không cài đặt/PATH."""
try:
with open(_carla_config_path(), "r", encoding="utf-8") as f:
data = json.load(f)
except Exception:
return ""
p = (data.get("carla_path") or "").strip()
if not p:
return ""
if os.path.isfile(p) and os.path.basename(p).lower() in ("carla.exe", "carla-single.exe", "carla"):
return p
if os.path.isdir(p):
try:
for root, dirs, files in os.walk(p):
depth = root[len(p):].count(os.sep)
if depth >= 2:
dirs[:] = []
continue
for f in files:
if f.lower() == "carla.exe":
return os.path.join(root, f)
except Exception:
pass
return ""
def save_carla_path(path: str) -> str:
"""Lưu vị trí Carla do user chọn (Plugin Manager → Định vị Carla...).
Chấp nhận: đường dẫn tới carla.exe, hoặc thư mục chứa carla.exe (portable
zip). Resolve về đường dẫn exe hợp lệ → ghi config → xóa cache detect.
Trả đường dẫn exe, hoặc '' nếu không hợp lệ."""
path = (path or "").strip().strip('"').strip()
exe = ""
if os.path.isfile(path) and os.path.basename(path).lower() in ("carla.exe", "carla-single.exe", "carla"):
exe = path
elif os.path.isdir(path):
try:
for root, dirs, files in os.walk(path):
depth = root[len(path):].count(os.sep)
if depth >= 2:
dirs[:] = []
continue
for f in files:
if f.lower() == "carla.exe":
exe = os.path.join(root, f)
break
if exe:
break
except Exception:
pass
if not exe:
return ""
try:
os.makedirs(settings.STORAGE_DIR, exist_ok=True)
with open(_carla_config_path(), "w", encoding="utf-8") as f:
json.dump({"carla_path": exe, "carla_exe": os.path.basename(exe)}, f, ensure_ascii=False, indent=2)
except Exception:
return ""
# Xóa cache detect/find_carla để capabilities phản ánh ngay
invalidate_carla_cache()
return exe
def _carla_from_registry() -> str:
"""Windows: Carla cài qua installer có thể ghi registry (best-effort)."""
if not _is_windows():
return ""
try:
import winreg
for hive, key in (
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Carla"),
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Carla"),
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Carla"),
):
try:
with winreg.OpenKey(hive, key) as k:
val, _ = winreg.QueryValueEx(k, "InstallPath")
p = os.path.join(str(val), "carla.exe")
if os.path.isfile(p):
return p
except Exception:
continue
except Exception:
pass
return ""
def _find_carla_exe_bounded(base: str, max_depth: int = 3) -> str:
"""Quét nông tìm carla.exe (bản portable giải nén thường nằm Downloads/
Desktop/Documents). Giới hạn độ sâu + bỏ qua thư mục lớn — KHÔNG quét
toàn ổ đĩa."""
if not base or not os.path.isdir(base):
return ""
skip = ("node_modules", ".cache", "AppData", ".git", "venv", ".venv",
"__pycache__", "$RECYCLE.BIN", "Windows", "Program Files",
"Program Files (x86)")
try:
for root, dirs, files in os.walk(base):
depth = root[len(base):].count(os.sep) if root != base else 0
if depth >= max_depth:
dirs[:] = []
continue
for f in files:
if f.lower() == "carla.exe":
return os.path.join(root, f)
dirs[:] = [d for d in dirs if d not in skip and not d.startswith(".")]
except Exception:
pass
return ""
def _carla_from_shallow_search() -> str:
"""Windows: tìm carla.exe trong Downloads/Desktop/Documents/Home."""
if not _is_windows():
return ""
home = os.environ.get("USERPROFILE") or os.path.expanduser("~")
bases = []
for sub in ("Downloads", "Desktop", "Documents"):
bases.append(os.path.join(home, sub))
bases.append(home)
for base in bases:
hit = _find_carla_exe_bounded(base)
if hit:
return hit
return ""
def _carla_candidates() -> list:
"""Các vị trí Carla — theo thứ tự ưu tiên:
1) user định vị (config file — bản portable bắt buộc dùng cách này)
2) PATH (shutil.which)
3) registry (Windows, nếu cài qua installer)
4) thư mục cài đặt chuẩn (Program Files...)
5) quét nông Downloads/Desktop/Documents (Windows) / AppImage (Linux)"""
cands = []
configured = get_configured_carla()
if configured:
cands.append(configured)
try:
which = shutil.which("carla")
if which:
cands.append(which)
except Exception:
pass
reg = _carla_from_registry()
if reg:
cands.append(reg)
try:
if _is_windows():
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
lpf = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
la = os.environ.get("LOCALAPPDATA", "")
for base in (pf, lpf, la):
for rel in ("Carla\\carla.exe", "Carla\\bin\\carla.exe", "Programs\\Carla\\carla.exe"):
p = os.path.join(base, rel) if base else ""
if p and os.path.isfile(p):
cands.append(p)
elif _is_linux():
home = os.path.expanduser("~")
for p in ("/usr/bin/carla", "/usr/local/bin/carla", "/opt/carla/carla"):
if os.path.isfile(p):
cands.append(p)
try:
for f in os.listdir(home):
if f.lower().startswith("carla") and f.lower().endswith(".appimage"):
cands.append(os.path.join(home, f))
except Exception:
pass
elif _is_macos():
for p in ("/Applications/Carla.app/Contents/MacOS/Carla", "/Applications/Carla.app/Contents/MacOS/carla"):
if os.path.isfile(p):
cands.append(p)
except Exception:
pass
shallow = _carla_from_shallow_search()
if shallow:
cands.append(shallow)
# Dedup giữ thứ tự
seen, out = set(), []
for c in cands:
if c not in seen:
seen.add(c)
out.append(c)
return out
@functools.lru_cache(maxsize=1)
def find_carla() -> str:
"""Đường dẫn Carla đầu tiên tìm thấy, hoặc '' nếu chưa có.
Lưu ý license: Carla GPL-2.0+ — app KHÔNG bundle/nhúng, chỉ spawn tiến
trình ngoài (user tự cài/giải nén) → không dính copyleft."""
for c in _carla_candidates():
return c
return ""
def invalidate_carla_cache():
"""Xóa cache detect/find_carla — gọi sau khi user định vị Carla mới."""
find_carla.cache_clear()
detect.cache_clear()
def _detect_platform() -> str:
if _is_windows():
return "windows"
@@ -355,7 +135,6 @@ 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()
carla = find_carla() if runtime == "desktop" else ""
docker = _in_docker()
return {
"platform": platform,
@@ -364,8 +143,6 @@ def detect() -> dict:
"environment": "docker" if docker else "standalone", # luồng âm instrument: docker→FluidSynthWASM, standalone→native OS
"has_display": _has_display(),
"tauri_bridge": _tauri_bridge_ready(),
"carla_path": carla,
"carla_local": bool(carla), # chỉ có ý nghĩa khi runtime=desktop
"vst_render": _vst_render_available(),
"default_vst_dirs": default_vst_dirs(),
"default_soundfont_dirs": default_soundfont_dirs(),
@@ -411,8 +188,6 @@ 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 = {
"carla_local": d["carla_local"],
"carla_path": d["carla_path"],
"preset_upload": True, # cả 2 mode đều upload preset được
"vst_render": d["vst_render"],
"tauri_bridge": d["tauri_bridge"],
+3 -3
View File
@@ -673,9 +673,9 @@ class DecentSamplerManager:
)
# ── Preset bridge (Carla ↔ pedalboard) ─────────────────────────────────────
# Carla (chạy ngoài, user tự cài — GPL-2.0+ nên app KHÔNG bundle/nhúng) dùng
# để mở native GUI VSTi + xuất file preset (.vstpreset từ nút Save của plugin).
# ── Preset bridge (native GUI ↔ pedalboard) ───────────────────────────────
# User chỉnh preset trong native GUI của VSTi (bridge mở cửa sổ plugin) rồi
# xuất file preset (.vstpreset từ nút Save của plugin).
# File preset nằm trong thư viện storage/presets (upload qua web UI hoặc picker
# local trên Windows) → render_engine tải qua apply_preset_to_plugin() khi render.
PRESET_EXTENSIONS = (".vstpreset", ".fxp", ".fxb", ".dspreset")
+69 -359
View File
@@ -108,11 +108,7 @@ const ensureSonicInstrument = (ctx) => {
}
};
// ── Carla bridge MIDI scheduling helper ────────────────────────────────────
// Schedule note_on/note_off tới Carla (OSC qua backend) cho MIDI items khi
// track dùng VSTi + có Carla local. Dùng chung cho mọi đường playback
// (main timeline, local loop, piano roll, ghost notes) để MIDI item PHẢI play
// qua Carla bridge khi VSTi được loaded.
// ── MIDI note-off scheduling helper (pending map chống note chồng) ─────────
// FIX (Bug 2): NOTE_OFF trễ (dur+30ms) của note cùng pitch bắn SAU note-on kế
// tiếp (bar liền nhau cùng hợp âm, piano roll note chồng) -> cắt note mới →
// câm. Map pending theo key channel:pitch — schedule NOTE_OFF mới cùng key →
@@ -147,65 +143,10 @@ const clearAllPendingNoteOffs = () => {
delete pendingNoteOffMap[k];
});
};
const fireCarlaNote = (ch, pitch, vel, delay, durMs) => {
setTimeout(function () { window.SonicCarlaMidi.noteOn(ch, pitch, vel); }, delay);
scheduleNoteOff('C:' + ch + ':' + pitch, function () { window.SonicCarlaMidi.noteOff(ch, pitch); }, delay, durMs);
};
// ⚠️ FIX (Bug 2): Carla mở ASYNC lúc play (ensureCarlaForPlayback) — nốt gửi
// trước khi OSC engine bind cổng bị mất → item câm dù keybed kêu. Nốt khi
// chưa ready được nhét queue, flush khi bridge OSC ready (giữ đúng thứ tự).
const flushCarlaNoteQueue = () => {
const q = window.__carlaNoteQueue || [];
if (!q.length) return;
window.__carlaNoteQueue = [];
q.forEach(item => {
const remaining = item.delay - (Date.now() - item.scheduledAt);
if (remaining <= 0) fireCarlaNote(item.ch, item.pitch, item.vel, 0, item.durMs);
else fireCarlaNote(item.ch, item.pitch, item.vel, remaining, item.durMs);
});
};
const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime, durMs) => {
try {
if (!window.SonicCarlaMidi || !window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) return;
const ctx = getAudioContext();
const delay = Math.max(0, (startWallTime - ctx.currentTime) * 1000);
const carlaVel = Math.round((velocity || 0.8) * 127);
const carlaCh = (synthEngine && synthEngine.midi_channel !== undefined) ? synthEngine.midi_channel : (channel || 0);
// Chẩn đoán: note MIDI item → Carla (bật console.log để verify route)
if (window.__carlaNoteLog === undefined) window.__carlaNoteLog = (window.__carlaNoteLog || 0) + 1;
console.log('[Carla] item note ch=' + carlaCh + ' pitch=' + pitch + ' vel=' + carlaVel + ' delay=' + delay.toFixed(0) + 'ms dur=' + (durMs || 300) + 'ms');
const dur = durMs || 300;
if (window.__carlaRunning !== true) {
window.__carlaNoteQueue = window.__carlaNoteQueue || [];
window.__carlaNoteQueue.push({ ch: carlaCh, pitch, vel: carlaVel, scheduledAt: Date.now(), delay, durMs: dur });
return;
}
fireCarlaNote(carlaCh, pitch, carlaVel, delay, dur);
} catch (e) { console.warn('[Carla] scheduleCarlaNote error:', e); }
};
// ── Carla bridge alive tracking + auto-open ────────────────────────────────
// window.__carlaRunning: undefined = chưa biết | true = đang chạy | false = đã chết.
// window.__carlaNoteQueue: nốt chờ flush khi Carla chưa ready (cold start).
// Quyết định route MIDI item EXCLUSIVE qua Carla hay fallback FluidSynth —
// tránh "câm toàn phần" khi Carla bị đóng (route chỉ-Carla mà Carla chết = im lặng).
const refreshCarlaStatus = () => {
try {
if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return;
window.SonicAPI.carlaStatus().then(st => {
window.__carlaRunning = !!(st && st.running);
if (window.__carlaRunning) flushCarlaNoteQueue();
}).catch(() => { window.__carlaRunning = false; });
} catch (e) {}
};
// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track rồi CHỜ OSC ready (poll
// carla-status tối đa 10s) — nốt chỉ gửi khi bridge thực sự nhận được.
// Có gate __carlaOpening chống spawn trùng (mỗi play chỉ mở 1 lần).
// ── Môi trường chạy: docker vs standalone ─────────────────────────────────
// environment: "docker" → âm instrument qua FluidSynthWASM (client); "standalone"
// → xử lí trực tiếp trên OS (backend native pyfluidsynth / Carla). Quy tắc:
// → xử lí trực tiếp trên OS (backend native pyfluidsynth). Quy tắc:
// KHÔNG phải docker = standalone (Windows/Linux/macOS chạy trực tiếp).
const sfEnv = () => {
try {
@@ -223,9 +164,6 @@ const isSfTrackEngine = (se) => {
return !!(se.soundfont_id || String(se.type || '').indexOf('soundfont') !== -1 || String(se.type || '').indexOf('sf3') !== -1);
};
const isVstTrackEngine = (se) => !!se && String(se.type || '').indexOf('vst') !== -1;
// Track dùng VSTi + Carla local → route Carla (native GUI, realtime).
const shouldRouteCarla = (se) => !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(se));
// ── Native soundfont preview (standalone) ─────────────────────────────────
// Render 1 note bằng backend native FluidSynth (pyfluidsynth) → play WAV.
// Mỗi key (thường = track.id) một Audio element — note mới stop note cũ;
@@ -1550,10 +1488,10 @@ function createTrackFxModule(type, ctx, params) {
nodes = { gLL, gRL, gLR, gRR };
} else if (type === 'eqpro') {
return createEqProModule(ctx, params);
} else if (type === 'carla' || type === 'vst3') {
// Carla Bridge / VST FX = xử lý NGOÀI (Carla standalone / fx_vst_bridge
// --render-fx khi export) — trong WebAudio graph chỉ là pass-through
// (không thêm DSP): âm track đi thẳng, đúng vị trí chain.
} else if (type === 'vst3') {
// VST FX = xử lý NGOÀI (fx_vst_bridge --render-fx khi export) — trong
// WebAudio graph chỉ là pass-through (không thêm DSP): âm track đi thẳng,
// đúng vị trí chain.
input.connect(output);
nodes = { passthrough: input };
} else {
@@ -1935,9 +1873,9 @@ function rebuildMasteringGraph(activate, chainArray) {
Object.keys(eqProStore).forEach(k => { try { eqProStore[k].destroy && eqProStore[k].destroy(); } catch (e) { } });
Object.keys(eqProStore).forEach(k => delete eqProStore[k]);
activeMods.forEach(mod => {
if (mod.type === 'carla' || mod.type === 'vst3') {
// Carla Bridge = VST FX chạy NGOÀI (Carla standalone) — không có node
// WebAudio trong master chain: pass-through, prev giữ nguyên.
if (mod.type === 'vst3') {
// VST FX chạy NGOÀI (fx_vst_bridge) — không có node WebAudio trong
// master chain: pass-through, prev giữ nguyên.
return;
}
if (mod.type === 'eqpro') {
@@ -6172,10 +6110,6 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
const [pmSfExpanded, setPmSfExpanded] = React.useState({});
const [pmSfInstruments, setPmSfInstruments] = React.useState({});
const [pmSfLoading, setPmSfLoading] = React.useState({});
// Force re-render sau khi định vị Carla (capabilities đổi)
const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0);
// Khai báo trực tiếp thư mục chứa carla.exe (nhập tay, không cần picker)
const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState('');
// D7: trạng thái Native Bridge (query khi mở modal) — badge header.
const [bridgeStatus, setBridgeStatus] = React.useState(null);
React.useEffect(() => {
@@ -6287,52 +6221,6 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
}
}
};
// Định vị Carla.exe — bản Windows là zip portable: KHÔNG cài đặt, KHÔNG dùng
// biến môi trường PATH nên heuristic không tìm thấy → user tự chọn thư mục
// chứa carla.exe (folder picker native) → lưu config phía server.
const locateCarla = async () => {
try {
let picked = null;
try {
const d = await window.SonicAPI.pickPluginDir();
if (d && typeof d.path === 'string' && d.path) picked = d.path;
} catch (e) { /* fallthrough */ }
if (!picked && window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
try {
const sel = await window.__TAURI__.core.invoke('plugin:dialog|open', { options: { directory: true, multiple: false } });
if (typeof sel === 'string' && sel) picked = sel;
} catch (e) { /* fallthrough */ }
}
if (!picked) { showToast('Không mở được hộp thoại chọn thư mục', 'error'); return; }
const r = await window.SonicAPI.setCarlaPath(picked);
if (r && r.success && r.carla_path) {
window.SonicRuntime.capabilities = r;
document.documentElement.dataset.carla = r.features && r.features.carla_local ? '1' : '0';
setPmCarlaVersion(v => v + 1);
showToast('Đã định vị Carla: ' + r.carla_path, 'success');
} else {
showToast('Không tìm thấy carla.exe trong thư mục đã chọn', 'error');
}
} catch (err) { showToast('Lỗi định vị Carla: ' + (err.message || err), 'error'); }
};
// Khai báo thư mục Carla trực tiếp (nhập tay) — tương tự Định vị nhưng
// không cần hộp thoại chọn thư mục.
const saveCarlaInput = async () => {
const p = (pmCarlaPathInput || '').trim();
if (!p) { showToast('Nhập đường dẫn thư mục chứa carla.exe', 'warning'); return; }
try {
const r = await window.SonicAPI.setCarlaPath(p);
if (r && r.success && r.carla_path) {
window.SonicRuntime.capabilities = r;
document.documentElement.dataset.carla = r.features && r.features.carla_local ? '1' : '0';
setPmCarlaVersion(v => v + 1);
setPmCarlaPathInput('');
showToast('Đã lưu Carla: ' + r.carla_path, 'success');
} else {
showToast('Không tìm thấy carla.exe trong đường dẫn đã nhập', 'error');
}
} catch (err) { showToast('Lỗi: ' + (err.message || err), 'error'); }
};
// Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog.
// dirsOverride: mảng dirs mới nhất (fix stale-closure: setTimeout giữ
// pmDirs cũ nên add/remove xong tự scan lại vẫn lưu dir cũ).
@@ -9055,11 +8943,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
ensureSonicInstrument(pvCtx);
playing.forEach(n => {
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
return;
}
if (isStandaloneSf() && isVstTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
if (isStandaloneSf() && isVstTrackEngine(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
return;
}
@@ -9429,9 +9317,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
ensureSonicInstrument(clCtx);
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
} else if (isStandaloneSf() && isVstTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
} else if (isStandaloneSf() && isVstTrackEngine(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
} else {
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
@@ -9695,9 +9583,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
} else if (isStandaloneSf() && isVstTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
} else if (isStandaloneSf() && isVstTrackEngine(dwTrk && dwTrk.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
const ctx = getAudioContext();
@@ -9798,9 +9686,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
} else if (isStandaloneSf() && isVstTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
} else if (isStandaloneSf() && isVstTrackEngine(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
var pvCtx = getAudioContext();
@@ -9809,11 +9697,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
scheduleMidiNoteDispatch(pvTrk, p, brushVelocityRef.current || 0.8, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
previewPitchRef.current = p;
}
// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(pvTrk && pvTrk.synth_engine, st.isArmed)) {
var pvVel2 = Math.round((brushVelocityRef.current || 0.8) * 127);
window.SonicCarlaMidi.playNote(pvTrk && pvTrk.synth_engine ? (pvTrk.synth_engine.midi_channel || 0) : 0, p, pvVel2, durMs);
}
}
if (pitchChanged) {
@@ -10192,8 +10075,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
}
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const kbNativeVst = isStandaloneSf() && isVstTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const kbNativeVst = isStandaloneSf() && isVstTrackEngine(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (kbNative) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
} else if (kbNativeVst) {
@@ -10208,12 +10091,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
}
// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine, st.isArmed)) {
window.SonicCarlaMidi.noteOn(kbCtx.ch, pitch, 100);
if (window.__carlaKeybedTimer) clearTimeout(window.__carlaKeybedTimer);
window.__carlaKeybedTimer = setTimeout(function () { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); }, 650);
}
} catch (err) {
console.error('playNote error:', err);
}
@@ -10224,23 +10101,18 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
}
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
} else if (isStandaloneSf() && isVstTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
} else if (isStandaloneSf() && isVstTrackEngine(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {}
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && !(isStandaloneSf() && (isSfTrackEngine(kbCtx.synthEngine) || isVstTrackEngine(kbCtx.synthEngine)) && !shouldRouteCarla(kbCtx.synthEngine))) {
} else if (window.SonicSF && !(isStandaloneSf() && (isSfTrackEngine(kbCtx.synthEngine) || isVstTrackEngine(kbCtx.synthEngine)))) {
// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
}
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine, st.isArmed)) {
window.SonicCarlaMidi.noteOn(kbCtx.ch, pitch, 100);
if (window.__carlaKeybedTimer) clearTimeout(window.__carlaKeybedTimer);
window.__carlaKeybedTimer = setTimeout(function () { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); }, 650);
}
} catch (err) { console.error('playNote error:', err); }
}
},
@@ -10253,8 +10125,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {}
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
},
onMouseLeave: () => {
if (!keybedMouseDownRef.current) return;
@@ -10265,7 +10135,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {}
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
}
}, showLabel && label)
);
@@ -10763,8 +10632,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
}, React.createElement("i", { "data-lucide": "file-down", className: "w-3 h-3" }), "Export MIDI"), React.createElement("button", {
onClick: async () => {
// Preview/export MIDI notes với âm VSTi: midi-render (pedalboard) →
// play wav ngay; nếu pedalboard không render được plugin (VST2/state
// GUI) fallback carla-play-notes (realtime qua Carla bridge, OSC).
// play wav ngay.
const se = st.synth_engine || {};
const instrumentId = se.plugin_id || st.instrumentId || null;
if (!instrumentId || String(se.type || '').indexOf('vst') === -1) {
@@ -10788,17 +10656,11 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
audio.play().catch(() => {});
showToast(`Preview VSTi (${(res.duration_sec || 0).toFixed(1)}s) — ${se.plugin_id || instrumentId}`, 'success');
} catch (err) {
// midi-render fail (404 plugin không load / 501 pedalboard) → Carla
try {
await window.SonicAPI.carlaPlayNotes({ notes: payload.notes, bpm: payload.bpm, channel: se.midi_channel || 0 });
showToast('Preview qua Carla (realtime) — nghe loa hệ thống', 'success');
} catch (err2) {
showToast('Preview VSTi thất bại: ' + ((err && err.message) || 'lỗi'), 'error');
}
showToast('Preview VSTi thất bại: ' + ((err && err.message) || 'lỗi'), 'error');
}
},
className: "px-2.5 py-1 bg-violet-700 hover:bg-violet-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold",
title: "Render MIDI notes bằng VSTi (pedalboard) và phát — fallback qua Carla realtime"
title: "Render MIDI notes bằng VSTi (pedalboard) và phát"
}, React.createElement("i", { "data-lucide": "play", className: "w-3 h-3" }), "Preview VSTi"), React.createElement("button", {
onClick: () => {
const se = st.synth_engine || {};
@@ -12335,7 +12197,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
);
};
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen, openGui, showCarla = true, activeIdx = -1, onSelect = null) => (
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen, openGui, activeIdx = -1, onSelect = null) => (
<div key={idx} onClick={() => onSelect && onSelect(s, idx)} className={`flex items-center gap-2 bg-slate-900/60 border rounded-lg px-2 py-1.5 flex-wrap cursor-pointer transition-all ${activeIdx === idx ? 'border-teal-400 bg-slate-800/80' : 'border-slate-800 hover:border-slate-600'}`}>
<button onClick={e => { e.stopPropagation(); toggleV(idx); }} className={`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass ? 'bg-teal-500 text-slate-950' : 'bg-slate-700 text-slate-300'}`} title={s.bypass ? 'Bypass' : 'Active'}>
<i data-lucide="power" className="w-2.5 h-2.5"></i>
@@ -12348,7 +12210,6 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
<span className={`text-[9px] font-mono ${s.preset_b64 ? 'text-emerald-400' : 'text-slate-600'}`}>{s.preset_b64 ? '✓ preset' : 'no preset'}</span>
<button onClick={e => { e.stopPropagation(); uploadV(idx); }} className="px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition" title="Upload .vstpreset (Save preset)">Preset</button>
{openGui && <button onClick={e => { e.stopPropagation(); openGui(s, idx); }} disabled={!s.path} className="px-2 py-1 bg-violet-800 hover:bg-violet-700 disabled:opacity-30 text-violet-100 text-[10px] font-semibold rounded transition" title="Mở GUI của VST (chỉnh thông số trực tiếp)"><i data-lucide="monitor" className="w-3 h-3"></i> GUI</button>}
{showCarla && <button onClick={e => { e.stopPropagation(); if (!s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } window.SonicAPI.openInCarla(s.name || s.path, s.path).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở Carla với ' + (s.name || s.path) + ' — chỉnh âm trong Carla rồi Save preset', 'success'); else window.showToast && window.showToast('Không mở được Carla', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }} className="px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition" title="Mở native GUI trong Carla">Carla</button>}
<button onClick={e => { e.stopPropagation(); moveV(idx, -1); }} disabled={idx === 0} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Lên"><i data-lucide="chevron-up" className="w-3 h-3"></i></button>
<button onClick={e => { e.stopPropagation(); moveV(idx, 1); }} disabled={idx === chainLen - 1} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Xuống"><i data-lucide="chevron-down" className="w-3 h-3"></i></button>
<button onClick={e => { e.stopPropagation(); removeV(idx); }} className="text-slate-600 hover:text-red-400 text-xs px-0.5" title="Xóa slot"><i data-lucide="x" className="w-3 h-3"></i></button>
@@ -12843,7 +12704,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
<span className="text-slate-400">Active VST: <strong className="text-teal-400">{vstChain[activeVstIdx].name || vstChain[activeVstIdx].path}</strong></span>
<span className="text-[9px] text-slate-500">Slot {activeVstIdx + 1}/{vstChain.length} — bấm card DSP trong CHAIN để chỉnh module builtin</span>
</div>
{vstRow(vstChain[activeVstIdx], activeVstIdx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length, openVstGui, true, activeVstIdx, null)}
{vstRow(vstChain[activeVstIdx], activeVstIdx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length, openVstGui, activeVstIdx, null)}
</div>
) : (
<div className="flex-1 bg-slate-950 border border-slate-800 rounded-xl p-4 flex flex-col gap-4 min-h-[240px] overflow-y-auto oz-scrollbar">
@@ -12928,17 +12789,6 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
</div>
)}
{activeMod && activeMod.type === 'carla' && activeIdx >= 0 && (
<div className="bg-amber-950/40 border border-amber-800/60 rounded-lg p-4 space-y-3">
<div className="text-[10px] text-amber-300 font-mono">Carla Bridge đã bị gỡ khỏi FX Rack — track cũ vẫn giữ module để không mất cấu hình.</div>
<button
onClick={() => removeMod(activeIdx)}
className="px-3 py-1.5 bg-amber-800 hover:bg-amber-700 text-amber-50 text-xs font-semibold rounded transition"
>
Gỡ module Carla khỏi track
</button>
</div>
)}
</div>
)}
@@ -13597,14 +13447,6 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
// stable hook count across renders (error #310 otherwise).
const dragChainIndexRef = React.useRef(null);
const [addModuleOpen, setAddModuleOpen] = React.useState(false);
// Carla Bridge (VST FX) — danh sách VST để load vào Carla (master chain)
const [masterCarlaVsts, setMasterCarlaVsts] = React.useState(null); // null = chưa load
React.useEffect(() => {
if (isOpen && window.SonicAPI && window.SonicAPI.listPlugins) {
window.SonicAPI.listPlugins().then(d => setMasterCarlaVsts((d && d.vst_instruments) || [])).catch(() => setMasterCarlaVsts([]));
}
}, [isOpen]);
if (!isOpen) return null;
const switchModule = (name) => setOzState(prev => ({ ...prev, activeModule: name }));
@@ -13619,10 +13461,9 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
limiter: { name: 'Brickwall Limiter', sub: 'True-Peak 20:1', icon: 'shield-half', color: '#f43f5e' },
exciter: { name: 'Harmonic Exciter', sub: 'Saturation & Air', icon: 'wand-2', color: '#c084fc' },
rebalance: { name: 'Master Rebalance', sub: 'M/S Balance', icon: 'sliders-horizontal', color: '#38bdf8' },
carla: { name: 'Carla Bridge (VST FX)', sub: 'Native VST audio processing', icon: 'sliders', color: '#14b8a6' },
vst3: { name: 'VST FX', sub: 'VST3 plugin', icon: 'plug', color: '#2dd4bf' }
};
const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : type === 'carla' ? 'carlaBridgeActive' : type === 'vst3' ? 'vstFxActive' : 'rebalActive';
const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : type === 'vst3' ? 'vstFxActive' : 'rebalActive';
const chainActive = (type) => !!ozState[chainFlag(type)];
const toggleChainModule = (modId) => {
@@ -13674,7 +13515,6 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
const id = 'mod_' + type + '_' + Date.now();
const entry = { id, type, name: meta.name, active: true };
if (type === 'eqpro') entry.params = { amount: 100, bands: JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS)) };
if (type === 'carla') entry.params = { plugin: '', plugin_path: '' };
setOzState(prev => ({
...prev,
chain: [...(prev.chain || []), entry],
@@ -14147,62 +13987,6 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
</div>
</div>
{/* VIEW: CARLA BRIDGE (VST FX) — load carla bridge để dùng VST chỉnh sửa âm thanh */}
<div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'carla' ? '' : 'hidden'}`}>
<div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
<div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
<span className="text-xs font-bold text-teal-400 uppercase oz-font-mono mb-3">Carla Bridge (VST FX)</span>
<button onClick={() => toggleChainModule((ozState.chain || []).find(m => m.type === 'carla')?.id)}
className={`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.carlaBridgeActive ? 'bg-teal-700 border-teal-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300'}`}>
{ozState.carlaBridgeActive ? 'ON' : 'OFF'}
</button>
</div>
<div className="col-span-8 space-y-3">
<div className="flex items-end gap-2 flex-wrap">
<div className="flex-1 min-w-[220px]">
<div className="text-[9px] text-slate-500 oz-font-mono mb-1">CHỌN VST FX (đã scan trên máy)</div>
<select
value={(ozState.carlaBridge && ozState.carlaBridge.plugin) || ''}
onChange={e => setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), plugin: e.target.value, plugin_path: '' } }))}
className="w-full bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1.5 text-xs outline-none focus:border-teal-500"
>
<option value="">— Chọn VST FX —</option>
{(masterCarlaVsts || []).map(v => <option key={v.id || v.name} value={v.id || v.name}>{v.name || v.id}</option>)}
</select>
</div>
<button
onClick={() => {
const plugin = (ozState.carlaBridge && ozState.carlaBridge.plugin) || '';
if (!plugin) { window.showToast && window.showToast('Chọn VST FX trước khi Load Carla', 'warning'); return; }
window.SonicAPI.openInCarla(plugin, ozState.carlaBridge.plugin_path).then(r => {
if (r && r.success) {
setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), connected: true } }));
window.showToast && window.showToast('Đã mở Carla với ' + plugin + ' (VST FX) — chỉnh âm thanh trong Carla', 'success');
} else { window.showToast && window.showToast('Không mở được Carla', 'error'); }
}).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error'));
}}
className="px-4 py-1.5 bg-teal-700 hover:bg-teal-600 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
>
<i data-lucide="play" className="w-3 h-3"></i> Load Carla Bridge
</button>
<button
onClick={() => {
if (window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) window.SonicCarlaMidi.stopBridge();
setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), connected: false } }));
window.showToast && window.showToast('Đã ngắt kết nối Carla Bridge', 'info');
}}
className="px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-red-300 text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
>
<i data-lucide="square" className="w-3 h-3"></i> Stop / Unload
</button>
</div>
<div className="text-[10px] text-slate-500 oz-font-mono leading-relaxed">
Mở Carla với VST FX để chỉnh sửa âm thanh master (native GUI). Module này là pass-through trong master chain (không thêm DSP WebAudio).
</div>
</div>
</div>
</div>
{/* WAVE OBSERVER INTEGRATION */}
<div className="border-t border-slate-800 bg-slate-900/60 p-3 flex flex-col shrink-0">
{/* Header */}
@@ -17181,20 +16965,17 @@ const App = () => {
var mt = activeTracksRef.current || tracks;
var curTrk = null;
for (var ci = 0; ci < mt.length; ci++) { if (mt[ci].id === trackId) { curTrk = mt[ci]; break; } }
// ── Unload Carla bridge khi chuyển từ VSTi sang instrument KHÔNG phải VST ──
// (soundfont/GM/default). Nếu không, Carla vẫn chạy với VSTi cũ → MIDI vẫn
// play qua Carla bridge (âm sai instrument + âm kẹt không dừng được).
// ── Panic khi chuyển từ VSTi sang instrument KHÔNG phải VST ──
// (soundfont/GM/default) — dừng mọi note đang vang từ VSTi cũ.
try {
const _hasInst = !!instrumentId;
const _wasVst = curTrk && curTrk.synth_engine && String(curTrk.synth_engine.type || '').indexOf('vst') !== -1;
const _nowVst = _hasInst && !isSfInstrument;
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
window.SonicCarlaMidi.stopBridge();
if (_wasVst && !_nowVst) {
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes(); stopAllNativeVstNotes();
console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST');
}
} catch (e) { console.warn('[Instrument] carla-stop on instrument switch error:', e); }
} catch (e) {}
var mch = curTrk ? assignTrackMidiChannel(curTrk, mt) : (sfBank === 128 ? 9 : 0);
updateActiveTracks(prev => prev.map(t => {
if (t.id !== trackId) return t;
@@ -17434,7 +17215,7 @@ const App = () => {
// Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F)
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
// D1: bridge active -> 1 cổng router (per-track channel alloc) →
// Rust SHM → C++ bridge; giữ SonicSF/Carla fallback ở nhánh else.
// Rust SHM → C++ bridge; giữ SonicSF fallback ở nhánh else.
var bTracks = activeTracksRef.current || [];
bTracks.forEach(function (bt) {
if (!bt.isArmed) return;
@@ -17465,18 +17246,13 @@ const App = () => {
try {
heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1;
} catch (err) { }
if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
if (isStandaloneSf() && isSfTrackEngine(asSe)) {
playNativeSfNote(asTrk, pitch, scaledVel / 127, 60000, undefined, 'midi_' + as.trackId + '_' + pitch);
} else if (isStandaloneSf() && isVstTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
} else if (isStandaloneSf() && isVstTrackEngine(asSe)) {
playNativeVstNote(asTrk, pitch, scaledVel / 127, 60000, undefined, 'midi_' + as.trackId + '_' + pitch);
} else {
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
}
// MIDI Keyboard → Carla bridge (piano roll sub-tab ARM):
// track VSTi của sub-tab phát VSTi realtime giống keybed.
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(asSe, true)) {
try { window.SonicCarlaMidi.noteOn(asCh, pitch, scaledVel); } catch (e) {}
}
});
}
// Route to ALL armed tracks (not just the first one)
@@ -17492,20 +17268,13 @@ const App = () => {
try {
heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1;
} catch (err) { }
if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
if (isStandaloneSf() && isSfTrackEngine(atSe)) {
playNativeSfNote(at, pitch, scaledVel / 127, 60000, undefined, 'midi_' + at.id + '_' + pitch);
} else if (isStandaloneSf() && isVstTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
} else if (isStandaloneSf() && isVstTrackEngine(atSe)) {
playNativeVstNote(at, pitch, scaledVel / 127, 60000, undefined, 'midi_' + at.id + '_' + pitch);
} else {
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
}
// MIDI Keyboard → Carla bridge (track VSTi + ARM + Carla
// local): phát VSTi realtime — giống hệt keybed ảo. Trước
// đây chỉ keybed gọi SonicCarlaMidi; keyboard hardware bị
// bỏ sót → track VSTi không kêu khi bấm đàn thật.
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(atSe, at.isArmed)) {
try { window.SonicCarlaMidi.noteOn(atCh, pitch, scaledVel); } catch (e) {}
}
});
}
} else if (cmd === 0x8 || (cmd === 0x9 && rawVel === 0)) {
@@ -17543,11 +17312,6 @@ const App = () => {
var stCh = assignTrackMidiChannel(st, stopTracks);
if (isStandaloneSf()) stopNativeSfNote('midi_' + st.id + '_' + pitch); stopNativeVstNote('midi_' + st.id + '_' + pitch);
window.SonicSF.stopNote(stCh, pitch);
// MIDI Keyboard → Carla bridge: note-off khi thả phím — tránh
// kẹt âm VSTi (giống keybed ảo).
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(st.synth_engine, st.isArmed)) {
try { window.SonicCarlaMidi.noteOff(stCh, pitch); } catch (e) {}
}
// Giảm counter note đang giữ — hết note → VU được phép decay/tắt
// (tick sẽ thấy counter = 0 và không còn giữ peak nữa)
try {
@@ -18195,9 +17959,9 @@ const App = () => {
setAiPrompt(newText);
};
// Gán preset VST3 (.vstpreset từ thư viện storage/presets — cầu nối
// Carla → pedalboard) vào track: preset_id nằm trong synth_engine → render
// engine tải qua load_preset khi render → âm render = âm đã chỉnh trong Carla.
// Gán preset VST3 (.vstpreset từ thư viện storage/presets) vào track:
// preset_id nằm trong synth_engine → render engine tải qua load_preset khi
// render → âm render = âm đã chỉnh trong preset.
const setTrackPreset = (trackId, presetId) => {
if (!trackId || !presetId) return;
var mt = activeTracksRef.current || tracks;
@@ -19224,7 +18988,6 @@ const App = () => {
limActive: false, limThreshold: -1.0,
excActive: false, excDrive: 40,
rebalActive: false, rebalMid: 0, rebalSide: 0,
carlaBridgeActive: false, carlaBridge: { plugin: '', plugin_path: '', connected: false },
});
}
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
@@ -22425,7 +22188,7 @@ const App = () => {
// ── Virtual MIDI Keyboard note routing (Phase 5) ────────────────────────
// NOTE_ON/NOTE_OFF theo pattern attachMidiHandler (hardware MIDI): bridge →
// per-track channel alloc; native SF/VST; SonicSF fallback; Carla; VU + giữ
// per-track channel alloc; native SF/VST; SonicSF fallback; VU + giữ
// counter (heldMidiNotesRef); forward recorder (live record vào midiItems).
const vkbNoteOn = (pitch, vel) => {
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
@@ -22448,16 +22211,13 @@ const App = () => {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(t.id, ctx.program, ctx.synthEngine); } catch (er) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: t.id, pitch: pitch, velocity: vel, percussion: percussion, live: true }); } catch (e) {}
} else if (isStandaloneSf() && (isSfTrackEngine(ctx.synthEngine) || isVstTrackEngine(ctx.synthEngine)) && !shouldRouteCarla(ctx.synthEngine)) {
} else if (isStandaloneSf() && (isSfTrackEngine(ctx.synthEngine) || isVstTrackEngine(ctx.synthEngine))) {
if (isSfTrackEngine(ctx.synthEngine)) playNativeSfNote(t, pitch, vel, 60000, undefined, 'vkb_' + t.id + '_' + pitch);
else playNativeVstNote(t, pitch, vel, 60000, undefined, 'vkb_' + t.id + '_' + pitch);
} else if (window.SonicSF) {
const dest = (activeTrackNodesRef.current[t.id] && activeTrackNodesRef.current[t.id].gainNode) || null;
window.SonicSF.playNote(pitch, Math.max(1, Math.round(vel * 127)), 60000, undefined, ctx.program, dest, ctx.ch, ctx.synthEngine);
}
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(ctx.synthEngine, t.isArmed)) {
try { window.SonicCarlaMidi.noteOn(ctx.ch, pitch, Math.max(1, Math.round(vel * 127))); } catch (e) {}
}
});
// Live record: forward tới mọi recorder đang chạy (timeline take / piano roll)
if (activeMIDIRecordersRef.current) {
@@ -22481,9 +22241,6 @@ const App = () => {
} else if (window.SonicSF && window.SonicSF.stopNote) {
if (isStandaloneSf()) { stopNativeSfNote('vkb_' + t.id + '_' + pitch); stopNativeVstNote('vkb_' + t.id + '_' + pitch); }
window.SonicSF.stopNote(ctx.ch, pitch);
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(t.synth_engine, t.isArmed)) {
try { window.SonicCarlaMidi.noteOff(ctx.ch, pitch); } catch (e) {}
}
}
try {
if (heldMidiNotesRef.current[t.id] !== undefined) {
@@ -23885,7 +23642,6 @@ const App = () => {
const destNode = getOrCreateTrackNode(track, context);
const program = track ? track.instrumentProgram : undefined;
var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
const routeCarla = shouldRouteCarla(track && track.synth_engine);
// D7: bridge active → preview qua bridge (channel router allocate — đúng
// instrument đã load khi chèn vào track).
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
@@ -23898,11 +23654,11 @@ const App = () => {
}, (durationMs || 500) + 30);
return;
}
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
if (isStandaloneSf() && isSfTrackEngine(track && track.synth_engine)) {
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
return;
}
if (isStandaloneSf() && !routeCarla && isVstTrackEngine(track && track.synth_engine)) {
if (isStandaloneSf() && isVstTrackEngine(track && track.synth_engine)) {
playNativeVstNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
return;
}
@@ -23983,10 +23739,9 @@ const App = () => {
// loop restart (rAF callback → React commit trễ hơn setTimeout 0) bị drop
// → loop pass 2+ câm. Set NGAY tại scheduler.
isPlayingRef.current = true;
// ⚠️ FIX: đồng bộ mastering + Carla status NGAY khi play — MIDI item phải
// qua mastering FX (khi bật) và qua Carla bridge (khi VSTi loaded).
// ⚠️ FIX: đồng bộ mastering NGAY khi play — MIDI item phải qua mastering
// FX (khi bật).
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
refreshCarlaStatus();
const allPlayTracks = activeTracksRef.current && activeTracksRef.current.length ? activeTracksRef.current : activeTracks;
// Capture items signature at schedule time — updatePlayhead so sánh với
// signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule).
@@ -24048,10 +23803,9 @@ const App = () => {
// MIDI items playback
const midiItems = track.midiItems || [];
const routeCarla = shouldRouteCarla(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const nativeVst = isStandaloneSf() && !routeCarla && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf || nativeVst)) {
const nativeSf = isStandaloneSf() && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const nativeVst = isStandaloneSf() && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || nativeSf || nativeVst)) {
if (nativeSf || nativeVst) {
scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
} else {
@@ -24076,16 +23830,10 @@ const App = () => {
if (offsetTime < noteEndSec) {
const durationMs = noteDurSec * 1000;
const program = track._isSectionClone ? track.instrumentProgram : (track.instrumentProgram !== undefined ? track.instrumentProgram : 0);
// Track VSTi + Carla local → MIDI item PHẢI play qua Carla bridge
// (chỉ route Carla — KHÔNG play qua FluidSynth GM nữa, tránh âm
// sai instrument chồng lên). Không có Carla → FluidSynth như cũ.
// ⚠️ FIX: item route giống hệt keybed (không gate __carlaRunning — Carla mở
// thủ công/cũ vẫn phải route; gate cũ làm "keybed kêu, item câm").
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));
if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay;
if (!routeToCarla && window.SonicSF) {
if (window.SonicSF) {
scheduleMidiNoteDispatch(
track, note.pitch || 60,
note.velocity || 0.8,
@@ -24097,9 +23845,6 @@ const App = () => {
track.synth_engine
);
}
// MIDI items → Carla bridge (track VSTi + Carla local): phát VSTi
// realtime — yêu cầu: MIDI item PHẢI play qua Carla khi VSTi loaded.
if (routeToCarla) scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs);
// Trigger VU meter flash when the note starts playing
setTimeout(() => {
// ⚠️ Guard: stop → setTimeout sót không được fire (VU nhảy
@@ -24113,7 +23858,7 @@ const App = () => {
} else {
const playOffset = offsetTime - noteStartSec;
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
if (!routeToCarla && window.SonicSF) {
if (window.SonicSF) {
scheduleMidiNoteDispatch(
track, note.pitch || 60,
note.velocity || 0.8,
@@ -24125,8 +23870,6 @@ const App = () => {
track.synth_engine
);
}
// MIDI items → Carla bridge (track VSTi + Carla local)
if (routeToCarla) scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
// Trigger VU meter flash instantly
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
@@ -24212,13 +23955,12 @@ const App = () => {
// 2. Play MIDI items in subTrack
const subMidiItems = subTrack.midiItems || [];
const subRouteCarla = shouldRouteCarla(subTrack.synth_engine);
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const subNativeVst = isStandaloneSf() && !subRouteCarla && isVstTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf || subNativeVst)) {
const subNativeSf = isStandaloneSf() && isSfTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const subNativeVst = isStandaloneSf() && isVstTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (subMidiItems.length > 0 && (window.SonicSF || subNativeSf || subNativeVst)) {
if (subNativeSf || subNativeVst) {
scheduleNativeTrack(subTrack, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
return; // native render → audio buffer (không qua WASM/Carla per-note)
return; // native render → audio buffer (không qua WASM per-note)
}
subMidiItems.forEach(item => {
const notes = item.notes || [];
@@ -24234,15 +23976,12 @@ const App = () => {
const notePlayEndMain = Math.min(noteEndMain, secEnd);
const program = subTrack.instrumentProgram;
var subCh = assignTrackMidiChannel(subTrack, secTracks);
// MIDI item trong SECTION → Carla bridge khi VSTi loaded
// (chỉ route Carla — không play FluidSynth GM sai âm)
const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));
if (offsetTime < noteStartMain) {
const delay = noteStartMain - offsetTime;
const startTime = context.currentTime + delay;
const playDurMs = (notePlayEndMain - noteStartMain) * 1000;
if (!subRouteCarla && window.SonicSF) {
if (window.SonicSF) {
scheduleMidiNoteDispatch(
subTrack, note.pitch || 60,
note.velocity || 0.8,
@@ -24254,7 +23993,6 @@ const App = () => {
subTrack.synth_engine
);
}
if (subRouteCarla) scheduleCarlaNote(subTrack.synth_engine, subCh, note.pitch || 60, note.velocity || 0.8, startTime, playDurMs);
// Trigger VU meter flash đúng nhịp (cả subTrack — VU
// section tab + parent track — VU MAIN): MIDI section đi
// SF → masterBus.input (không qua sub-node analyser) →
@@ -24272,7 +24010,7 @@ const App = () => {
}, delay * 1000);
} else {
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
if (!subRouteCarla && window.SonicSF) {
if (window.SonicSF) {
scheduleMidiNoteDispatch(
subTrack, note.pitch || 60,
note.velocity || 0.8,
@@ -24284,7 +24022,6 @@ const App = () => {
subTrack.synth_engine
);
}
if (subRouteCarla) scheduleCarlaNote(subTrack.synth_engine, subCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
if (window.triggerMidiVuActivity) {
// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20)
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + subTrack.id, note.velocity || 0.8);
@@ -24318,7 +24055,6 @@ const App = () => {
}
isPlayingRef.current = true;
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
refreshCarlaStatus();
const track = tracks.find(t => t.id === trackId);
if (!track) return;
const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
@@ -24358,13 +24094,12 @@ const App = () => {
// MIDI items playback
const midiItems = track.midiItems || [];
const routeCarla = shouldRouteCarla(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const nativeVst = isStandaloneSf() && !routeCarla && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf || nativeVst)) {
const nativeSf = isStandaloneSf() && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const nativeVst = isStandaloneSf() && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || nativeSf || nativeVst)) {
if (nativeSf || nativeVst) {
scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
return; // native render → audio buffer (không qua WASM/Carla per-note)
return; // native render → audio buffer (không qua WASM per-note)
}
var lcCh = assignTrackMidiChannel(track, tracks);
const bpmVal = parseInt(bpm) || 120;
@@ -24378,13 +24113,10 @@ const App = () => {
if (offsetTime < noteEndSec) {
const durationMs = noteDurSec * 1000;
const program = track.instrumentProgram !== undefined ? track.instrumentProgram : 0;
// MIDI item → Carla bridge khi VSTi loaded (chỉ route Carla — không
// play FluidSynth GM sai âm chồng lên)
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));
if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay;
if (!routeToCarla && window.SonicSF) {
if (window.SonicSF) {
scheduleMidiNoteDispatch(
track, note.pitch || 60,
note.velocity || 0.8,
@@ -24396,11 +24128,9 @@ const App = () => {
track.synth_engine
);
}
// MIDI items → Carla bridge (track VSTi + Carla local)
if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs);
} else {
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
if (!routeToCarla && window.SonicSF) {
if (window.SonicSF) {
scheduleMidiNoteDispatch(
track, note.pitch || 60,
note.velocity || 0.8,
@@ -24412,8 +24142,6 @@ const App = () => {
track.synth_engine
);
}
// MIDI items → Carla bridge (track VSTi + Carla local)
if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
}
}
});
@@ -24439,7 +24167,6 @@ const App = () => {
// delay=0 của sub-tab loop restart không bị drop.
isPlayingRef.current = true;
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
refreshCarlaStatus();
const midiNotes = notesOverride || st.notes || [];
const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal;
@@ -24463,10 +24190,9 @@ const App = () => {
// items placed later in the project play `item.startTime` seconds in the
// future (silence when pressing play). Ghost notes are already relative to
// the item window, so no absolute-session offset is applied anywhere here.
const routeCarla = shouldRouteCarla(synthEngine);
if (isStandaloneSf() && !routeCarla && (isSfTrackEngine(synthEngine) || isVstTrackEngine(synthEngine)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
if (isStandaloneSf() && (isSfTrackEngine(synthEngine) || isVstTrackEngine(synthEngine)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
scheduleNativeTrack(track, offsetSeconds, context, destNode, bpm, { notesOverride: midiNotes, playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
return; // native render cả tab → audio buffer (không per-note WASM/Carla)
return; // native render cả tab → audio buffer (không per-note WASM)
}
midiNotes.forEach(note => {
const noteOnBeat = note.start_beat || 0;
@@ -24478,22 +24204,9 @@ const App = () => {
const effectiveDur = noteDurSec - Math.max(0, offsetSeconds - noteStartSec);
const scheduledTime = startWallTime + effectiveStart;
const durMs = effectiveDur * 1000;
// Track VSTi + Carla local → note PHẢI play qua Carla bridge (chỉ route
// Carla — không play FluidSynth GM sai âm chồng lên)
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));
if (!routeToCarla && window.SonicSF) {
if (window.SonicSF) {
scheduleMidiNoteDispatch(track, note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
}
// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime
// (schedule theo audio clock bằng setTimeout — preview, timing gần đúng).
if (routeToCarla) {
const carlaDelay = Math.max(0, (scheduledTime - context.currentTime) * 1000);
const carlaVel = Math.round((note.velocity || 0.8) * 127);
const carlaCh = (synthEngine && synthEngine.midi_channel !== undefined) ? synthEngine.midi_channel : mainCh;
const carlaPitch = note.pitch || 60;
setTimeout(function () { window.SonicCarlaMidi.noteOn(carlaCh, carlaPitch, carlaVel); }, carlaDelay);
scheduleNoteOff('C:' + carlaCh + ':' + carlaPitch, function () { window.SonicCarlaMidi.noteOff(carlaCh, carlaPitch); }, carlaDelay, durMs);
}
}
});
// Play ghost notes from active tracks
@@ -24515,12 +24228,9 @@ const App = () => {
var effDur = durSec - Math.max(0, offsetSeconds - startSec);
var schedTime = startWallTime + effStart;
var durMs = effDur * 1000;
// Ghost note → Carla bridge khi ghost track VSTi (chỉ route Carla)
var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));
if (!gRouteCarla && window.SonicSF) {
if (window.SonicSF) {
scheduleMidiNoteDispatch(ghostTrack, note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
}
if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
}
});
});
@@ -34084,8 +33794,8 @@ STRICT CONSTRAINTS:
inp.click();
},
className: "w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 border-t border-zinc-700",
title: "Upload preset .vstpreset xuất từ Carla (native GUI)"
}, "\u2B06 Upload preset (t\u1EEB Carla...)"),
title: "Upload preset .vstpreset xuất từ native GUI"
}, "\u2B06 Upload preset (t\u1EEB native GUI...)"),
(!filteredInstruments.soundfonts.length && !filteredInstruments.vst.length) && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 py-4 text-center" }, "Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o")
)));
};
File diff suppressed because one or more lines are too long
+1 -16
View File
@@ -73,18 +73,6 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
// Runtime capabilities — frontend gọi lúc boot để biết môi trường
// (desktop Windows / docker headless) và bật/tắt tính năng
getCapabilities: () => apiRequest('/api/v1/system/capabilities', { method: 'GET' }),
// Định vị Carla.exe (bản portable zip không cài đặt/PATH) — lưu config
setCarlaPath: (path) => apiRequest('/api/v1/system/carla-path', { method: 'POST', body: JSON.stringify({ carla_path: path }) }),
// Mở native GUI VSTi trong Carla (chỉ khi runtime=desktop + có Carla local)
openInCarla: (pluginName, pluginPath) => apiRequest('/api/v1/plugins/open-in-carla', { method: 'POST', body: JSON.stringify({ plugin_name: pluginName, plugin_path: pluginPath }) }),
// 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 soundfont)
stopCarla: () => apiRequest('/api/v1/plugins/carla-stop', { method: 'POST', body: JSON.stringify({}) }),
// Kiểm tra Carla còn sống không (app-spawn) + cổng OSC — quyết định
// route MIDI item exclusive qua Carla hay fallback FluidSynth
carlaStatus: () => apiRequest('/api/v1/plugins/carla-status', { method: 'GET' }),
// Gửi MIDI note (track ARM → Carla OSC) để preview VSTi realtime
carlaMidi: (payload) => apiRequest('/api/v1/plugins/carla-midi', { method: 'POST', body: JSON.stringify(payload) }),
// Quick-render preview VSTi (âm thật = âm export, cùng code path)
previewInstrument: (payload) => apiRequest('/api/v1/plugins/preview', { method: 'POST', body: JSON.stringify(payload) }),
// Render MIDI notes → WAV bằng VSTi (pedalboard, âm thật) — trả file_id
@@ -99,10 +87,7 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
// Mở native GUI của VST FX (mastering chain / FX rack) qua bridge
openFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui', { method: 'POST', body: JSON.stringify(payload) }),
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
// Phát dãy MIDI notes realtime qua Carla bridge (OSC) — preview khi
// pedalboard không render được plugin (VD VST2)
carlaPlayNotes: (payload) => apiRequest('/api/v1/plugins/carla-play-notes', { method: 'POST', body: JSON.stringify(payload) }),
// Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard
// Thư viện preset VST3 (.vstpreset) — cầu nối preset → pedalboard
listPresets: () => apiRequest('/api/v1/presets', { method: 'GET' }),
uploadPreset: (file, pluginHint) => {
const fd = new FormData();
+3 -62
View File
@@ -1,10 +1,8 @@
// SonicForge Runtime service — phát hiện môi trường chạy (desktop Windows /
// docker headless) qua /api/v1/system/capabilities, bật/tắt tính năng theo đó.
// - data-runtime trên <html>: "desktop" | "headless"
// - data-carla="1": có Carla local (hiện nút "Mở trong Carla")
// - Phần tử có thuộc tính data-carla-only sẽ bị ẩn khi không có Carla local.
// - Thư viện preset (.vstpreset) cache trong SonicRuntime.presets — dùng cho
// dropdown gán preset vào track (Carla → pedalboard bridge).
// dropdown gán preset vào track (preset → pedalboard bridge).
window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null, presets: null };
(function () {
@@ -22,19 +20,13 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
window.SonicRuntime.loaded = true;
// environment: "docker" | "standalone" — quyết định luồng âm
// instrument: docker → FluidSynthWASM (client), standalone →
// native (backend pyfluidsynth / Carla). Không phải docker =
// standalone (Windows/Linux/macOS chạy trực tiếp trên OS).
// native (backend pyfluidsynth / native_bridge). Không phải
// docker = standalone (Windows/Linux/macOS chạy trực tiếp trên OS).
window.SonicRuntime.environment = c.environment || (c.docker ? 'docker' : 'standalone');
var html = document.documentElement;
html.dataset.runtime = c.runtime || 'unknown';
html.dataset.platform = c.platform || '';
html.dataset.environment = window.SonicRuntime.environment;
html.dataset.carla = (c.features && c.features.carla_local) ? '1' : '0';
if (c.features && c.features.carla_local === false) {
document.querySelectorAll('[data-carla-only]').forEach(function (el) {
el.style.display = 'none';
});
}
// Cache sẵn danh sách preset (static, ít thay đổi)
listPresets().catch(function () {});
return c;
@@ -66,54 +58,3 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
load();
}
})();
// ── SonicCarlaMidi: cầu nối MIDI từ track ARM → Carla (OSC /Carla/0/note_*) ──
// Khi track dùng VSTi (synth_engine.type chứa 'vst') + được ARM + máy có Carla
// local → phím bấm trên piano roll / keybed được gửi tới Carla để phát realtime
// (Carla standalone bật OSC UDP mặc định cổng 22752).
window.SonicCarlaMidi = window.SonicCarlaMidi || {
shouldRoute: function (synthEngine, isArmed) {
// Carla bridge removed - all instruments hosted by Native Bridge.
return false;
},
// Playback MIDI items: route khi track VSTi + Carla local (không cần ARM —
// user đã chủ động bấm Play trên item đó).
shouldRoutePlayback: function (synthEngine) {
// Carla bridge removed - all instruments hosted by Native Bridge.
return false;
},
noteOn: function (channel, note, velocity) {
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
window.SonicAPI.carlaMidi({ event: 'note_on', note: note, velocity: velocity || 100, channel: channel || 0 }).catch(function () {});
},
noteOff: function (channel, note) {
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
window.SonicAPI.carlaMidi({ event: 'note_off', note: note, channel: channel || 0 }).catch(function () {});
},
// Bật note rồi tự tắt sau durMs (preview ngắn)
playNote: function (channel, note, velocity, durMs) {
this.noteOn(channel, note, velocity);
var self = this;
setTimeout(function () { self.noteOff(channel, note); }, (durMs || 300) + 50);
},
// Tắt mọi note đang ngân trong Carla (note_off toàn pitch — dùng khi stop)
allNotesOff: function () {
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
for (var ch = 0; ch < 16; ch++) {
for (var n = 0; n < 128; n++) {
window.SonicAPI.carlaMidi({ event: 'note_off', note: n, channel: ch }).catch(function () {});
}
}
},
// Unload Carla bridge: tắt âm + terminate tiến trình Carla (app spawn).
// Gọi khi track chuyển từ VSTi → instrument soundfont để âm KHÔNG còn
// play qua Carla bridge nữa.
stopBridge: function () {
var self = this;
try { self.allNotesOff(); } catch (e) {}
if (window.SonicAPI && window.SonicAPI.stopCarla) {
return window.SonicAPI.stopCarla().catch(function () {});
}
return Promise.resolve();
}
};
+1 -1
View File
@@ -1,7 +1,7 @@
// app/static/js/services/unifiedMidiRouter.js
// Single MIDI entry point for Web MIDI keyboard + timeline playback.
// bridge connected -> NativeBridgeService.dispatchMidiEvent (Rust SHM -> C++ bridge)
// bridge down -> onFallback callback (app.jsx wires SonicSF/Carla path).
// bridge down -> onFallback callback (app.jsx wires SonicSF/native path).
(function () {
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview
+1 -1
View File
@@ -102,7 +102,7 @@ try:
logf.flush()
print("bridge out:", open(logpath, encoding="utf-8").read()[-1500:])
# VST3 (native hosting, no Carla): mda-vst3 bundle from SDK build
# VST3 (native hosting): mda-vst3 bundle from SDK build
VST3 = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "VST3", "Release", "mda-vst3.vst3"))
load_instrument(0, VST3, ch=2)
# wait until bridge reports the load (poll log), up to 10s
+1 -1
View File
@@ -15,7 +15,7 @@
// Frontend host (Tauri asset page). KHÔNG location.href redirect: chuyển
// sang http://127.0.0.1:8000 sẽ rời Tauri context -> mất window.__TAURI__
// -> NativeBridgeService vô hiệu (bridge-audio/load_instrument không dùng
// được; MIDI rơi về fallback WASM/Carla). Thay vào đó: quét port engine
// được; MIDI rơi về fallback WASM/native). Thay vào đó: quét port engine
// (8000-8010), nạp toàn bộ UI engine qua document.write trên CÙNG document
// (origin tauri:// giữ nguyên -> __TAURI__ sống), base href + API_BASE_URL
// trỏ engine HTTP (CORS * đã bật).
+1 -24
View File
@@ -190,7 +190,7 @@ class TestPluginAPI:
assert g.json()["plugin_dirs"] == [d1]
class TestMidiRenderVSTi:
"""Preview/export MIDI notes với âm VSTi — feature Carla bridge → pedalboard."""
"""Preview/export MIDI notes với âm VSTi — feature preset bridge → pedalboard."""
def test_midi_render_requires_auth(self):
client.cookies.clear() # TestClient giữ cookie login từ test trước
@@ -216,29 +216,6 @@ class TestMidiRenderVSTi:
assert resp.status_code == 404
assert "VSTi" in resp.json()["detail"]
def test_carla_play_notes_requires_auth(self):
client.cookies.clear() # TestClient giữ cookie login từ test trước
resp = client.post("/api/v1/plugins/carla-play-notes",
json={"notes": [{"pitch": 60}], "bpm": 120})
assert resp.status_code == 401
def test_carla_play_notes_no_notes(self):
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
resp = client.post("/api/v1/plugins/carla-play-notes", headers={"Authorization": f"Bearer {token}"},
json={"notes": [], "bpm": 120})
assert resp.status_code == 400
def test_carla_play_notes_no_carla(self):
# Máy test không có Carla local → 409 hướng dẫn định vị/mở Carla.
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
resp = client.post("/api/v1/plugins/carla-play-notes", headers={"Authorization": f"Bearer {token}"},
json={"notes": [{"pitch": 60, "start_beat": 0, "duration_beats": 1}], "bpm": 120})
assert resp.status_code == 409
# --- fx-gui embed (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 1) --------------
def test_fx_gui_open_embed_returns_embed_url(self):
token = get_admin_token()