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:
+1
-1
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
@@ -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"],
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user