FIX: Carla sử dụng bridge có midikeyboard

This commit is contained in:
2026-08-10 08:53:05 +07:00
parent aa32272f36
commit 250b94cd88
5 changed files with 163 additions and 25 deletions
+84 -12
View File
@@ -472,11 +472,15 @@ class PreviewRequest(BaseModel):
@router.post("/open-in-carla")
async def open_in_carla(req: OpenInCarlaRequest, current_user: dict = Depends(get_current_user)):
"""Mở Carla (native GUI host) trên máy hiện tại — Windows desktop mode.
"""Mở Carla với VSTi đã chọn — TỰ ĐỘNG load plugin (native GUI + keyboard).
Carla là app ngoài do user tự cài (GPL-2.0+ → không bundle/nhúng). App chỉ
spawn tiến trình; user chỉnh preset trong GUI rồi Save → .vstpreset → upload
vào thư viện preset → gán vào track → render engine tải preset tương ứng."""
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()
@@ -497,15 +501,12 @@ async def open_in_carla(req: OpenInCarlaRequest, current_user: dict = Depends(ge
plugin_path = plugins[req.plugin_name]
except Exception:
plugin_path = ""
cmd = [carla]
carxs_path = ""
if plugin_path:
# carla-single: mở thẳng 1 plugin thành app standalone có native GUI
single = os.path.join(
os.path.dirname(carla),
"carla-single" + (".exe" if os.name == "nt" else ""),
)
if os.path.isfile(single):
cmd = [single, plugin_path]
carxs_path = _write_carla_project(req.plugin_name or os.path.basename(plugin_path), plugin_path)
cmd = [carla]
if carxs_path:
cmd.append(carxs_path)
try:
cwd = os.path.dirname(carla) or None
subprocess.Popen(cmd, cwd=cwd, close_fds=os.name != "nt")
@@ -514,12 +515,83 @@ async def open_in_carla(req: OpenInCarlaRequest, current_user: dict = Depends(ge
"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}")
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)
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"
"</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)."""
+46 -1
View File
@@ -5294,6 +5294,8 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
const [pmSfLoading, setPmSfLoading] = React.useState({});
// Force re-render sau khi đnh v Carla (capabilities đi)
const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0);
// Khai báo trc tiếp thư mc cha carla.exe (nhp tay, không cn picker)
const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState('');
React.useEffect(() => {
if (isOpen) {
window.SonicAPI.listPlugins()
@@ -5416,6 +5418,24 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
}
} catch (err) { showToast('Lỗi định vị Carla: ' + (err.message || err), 'error'); }
};
// Khai báo thư mc Carla trc tiếp (nhp tay) tương t Đnh v nhưng
// không cn hp thoi chn thư mc.
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ư mc refresh list + catalog.
const saveAndScanDirs = async () => {
setPmScanning(true);
@@ -5667,6 +5687,20 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
onClick: () => { window.SonicAPI.openInCarla().then(function (r) { if (r && r.success) showToast('Đã mở Carla', 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition flex items-center gap-1'
}, React.createElement('i', { 'data-lucide': 'play', className: 'w-3 h-3' }), 'Mở Carla')
),
React.createElement('div', { className: 'flex gap-2 mt-2' },
React.createElement('input', {
type: 'text',
placeholder: 'Hoặc nhập thư mục chứa carla.exe (VD: D:/Tools/Carla)',
value: pmCarlaPathInput,
onChange: e => setPmCarlaPathInput(e.target.value),
onKeyDown: e => { if (e.key === 'Enter') saveCarlaInput(); },
className: 'flex-1 bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-teal-600'
}),
React.createElement('button', {
onClick: saveCarlaInput,
className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition shrink-0'
}, 'Lưu')
)
),
// Plugin directories section (folder picker + save + scan)
@@ -30099,7 +30133,18 @@ STRICT CONSTRAINTS:
className: "flex items-stretch"
},
/*#__PURE__*/React.createElement("button", {
onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id); },
onClick: () => {
setInstrumentDropdownTrackId(null);
setInstrumentDropdownBtnRect(null);
setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id);
// T ĐNG m Carla vi VSTi va chn (desktop + Carla local)
// Carla load sn plugin, native GUI + keyboard o đ preview realtime.
if (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) {
window.SonicAPI.openInCarla(v.id).then(function (r) {
if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id) + ' — chọn preset, bấm keyboard để preview', 'success');
}).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
}
},
className: "flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST")),
window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", {
File diff suppressed because one or more lines are too long