feat: embed native VST GUI in mastering + fx rack panels (bridge --fx-gui frame capture + input relay)
Linux parts (0L1-4L1 per TASKS_LINUX_WINDOWS.md): - native_bridge: FxGuiServer.cpp HTTP server (Winsock) — /frame BitBlt + PrintWindow fallback -> JPEG (libjpeg-turbo q70), /input PostMessage relay (mouse/wheel/key, coords scaled), /close, /ping; RenderFxJob fxGuiSetup + run_fx_gui_server (offscreen); main.cpp --fx-gui dispatch; vcpkg + CMakeLists WIN32-only + JPEG. - api: open_fx_gui embed flag spawn --fx-gui, parse SF_FXGUI_PORT -> embed_url, fx-gui/close endpoint; 4 unit tests. - ui: VstGuiEmbed component (frame refresh 100ms, ping 2s, input relay, scale coords) shared by MasteringModal + FXRackModal; swap graph<->GUI, cleanup on unmount, dead-bridge toast fallback. - docs: TASKS_LINUX_WINDOWS.md, WALKTHROUGH_WINDOWS.md, PLAN status. Windows tasks 0W1-4W1 remain (build + E2E real VST).
This commit is contained in:
+58
-1
@@ -503,6 +503,14 @@ class FxGuiRequest(BaseModel):
|
||||
"""Mở native GUI của VST FX qua bridge (Windows desktop)."""
|
||||
path: str
|
||||
name: Optional[str] = None
|
||||
# embed=True → bridge --fx-gui (off-screen + HTTP frame/input server,
|
||||
# PLAN_MASTERBUS_VST_GUI_EMBED.md); embed=False → cửa sổ rời cũ.
|
||||
embed: bool = False
|
||||
|
||||
|
||||
class FxGuiCloseRequest(BaseModel):
|
||||
"""Đóng bridge fx-gui đã mở cho plugin (embed mode)."""
|
||||
path: str
|
||||
|
||||
|
||||
class PreviewRequest(BaseModel):
|
||||
@@ -717,12 +725,41 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
||||
except Exception:
|
||||
os.close(fd)
|
||||
raise HTTPException(status_code=500, detail="Không ghi được job file cho bridge.")
|
||||
cmd = [exe, "--open-fx-gui", job_path]
|
||||
cmd = [exe, "--fx-gui" if req.embed else "--open-fx-gui", job_path]
|
||||
try:
|
||||
if req.embed:
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True,
|
||||
close_fds=os.name != "nt")
|
||||
# Bridge prints SF_FXGUI_PORT=<port> after binding its HTTP server.
|
||||
embed_url = None
|
||||
try:
|
||||
for line in proc.stdout:
|
||||
line = (line or "").strip()
|
||||
if line.startswith("SF_FXGUI_PORT="):
|
||||
port = int(line.split("=", 1)[1])
|
||||
embed_url = f"http://127.0.0.1:{port}"
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not embed_url:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
raise HTTPException(status_code=500, detail="Bridge không báo port fx-gui (mở GUI thất bại).")
|
||||
proc._sf_plugin_path = plugin_path
|
||||
proc._sf_embed_url = embed_url
|
||||
_register_fx_gui_process(proc)
|
||||
return {"success": True, "started": True, "already_running": False,
|
||||
"embed_url": embed_url, "cmd": cmd}
|
||||
proc = subprocess.Popen(cmd, close_fds=os.name != "nt")
|
||||
proc._sf_plugin_path = plugin_path
|
||||
_register_fx_gui_process(proc)
|
||||
return {"success": True, "started": True, "already_running": False, "cmd": cmd}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
try:
|
||||
os.remove(job_path)
|
||||
@@ -731,6 +768,26 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
||||
raise HTTPException(status_code=500, detail=f"Không mở được GUI của VST: {e}")
|
||||
|
||||
|
||||
@router.post("/fx-gui/close")
|
||||
async def close_fx_gui(req: FxGuiCloseRequest, current_user: dict = Depends(get_current_user)):
|
||||
"""Đóng bridge fx-gui đang chạy cho plugin (embed mode) — frontend gọi khi
|
||||
user đóng panel embed. Không lỗi nếu plugin chưa mở (closed=False)."""
|
||||
enforce_password_changed(current_user)
|
||||
plugin_path = (req.path or "").strip()
|
||||
if not plugin_path:
|
||||
raise HTTPException(status_code=400, detail="Thiếu đường dẫn VST plugin.")
|
||||
for proc in _FX_GUI_PROCESSES:
|
||||
if proc is not None and proc.poll() is None and getattr(proc, "_sf_plugin_path", None) == plugin_path:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
_prune_fx_gui_processes()
|
||||
return {"success": True, "closed": True}
|
||||
return {"success": True, "closed": False}
|
||||
|
||||
|
||||
_FX_GUI_PROCESSES = [] # list[subprocess.Popen] — các bridge fx-gui do app spawn
|
||||
|
||||
def _register_fx_gui_process(proc):
|
||||
|
||||
+103
-64
@@ -11289,8 +11289,7 @@ const TRACK_FX_META = {
|
||||
compressor: { name: 'Bus Compressor', icon: 'compress', color: '#fbbf24', sub: 'Glue & Punch' },
|
||||
limiter: { name: 'Brickwall Limiter', icon: 'shield-half', color: '#f43f5e', sub: 'True-Peak 20:1' },
|
||||
exciter: { name: 'Harmonic Exciter', icon: 'wand-2', color: '#c084fc', sub: 'Saturation & Air' },
|
||||
rebalance: { name: 'Master Rebalance', icon: 'sliders-horizontal', color: '#38bdf8', sub: 'M/S Balance' },
|
||||
carla: { name: 'Carla Bridge (VST FX)', icon: 'sliders', color: '#14b8a6', sub: 'Native VST audio processing' }
|
||||
rebalance: { name: 'Master Rebalance', icon: 'sliders-horizontal', color: '#38bdf8', sub: 'M/S Balance' }
|
||||
};
|
||||
const TRACK_FX_DEFAULTS = {
|
||||
eq: { g1: 0, g2: 0, g3: 0, g4: 0 },
|
||||
@@ -11298,8 +11297,7 @@ const TRACK_FX_DEFAULTS = {
|
||||
compressor: { threshold: -16, ratio: 3, makeup: 0 },
|
||||
limiter: { ceiling: -1.0 },
|
||||
exciter: { drive: 40 },
|
||||
rebalance: { mid: 0, side: 0 },
|
||||
carla: { plugin: '', plugin_path: '' }
|
||||
rebalance: { mid: 0, side: 0 }
|
||||
};
|
||||
|
||||
// EQ Pro canvas frame renderer (graphic_EQ_interactive_module.md §I-II): grid,
|
||||
@@ -11754,7 +11752,66 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
);
|
||||
};
|
||||
|
||||
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen, openGui) => (
|
||||
// --- Embedded VST GUI (PLAN_MASTERBUS_VST_GUI_EMBED.md) --------------------
|
||||
// Frame-capture + input relay: hiển thị native GUI qua HTTP /frame (~10fps),
|
||||
// gửi pointer/key về bridge qua /input. Dùng chung MasteringModal + FXRackModal.
|
||||
// url = embed_url từ API fx-gui open (embed:true); onClose khi user đóng;
|
||||
// onDead khi bridge chết (ping fail) → parent trả graph + toast.
|
||||
const VstGuiEmbed = ({ url, onClose, onDead }) => {
|
||||
const imgRef = React.useRef(null);
|
||||
const boxRef = React.useRef(null);
|
||||
const deadRef = React.useRef(false);
|
||||
const [dead, setDead] = React.useState(false);
|
||||
const post = (payload) => { try { fetch(url + '/input', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).catch(() => {}); } catch (e) {} };
|
||||
React.useEffect(() => {
|
||||
deadRef.current = false;
|
||||
setDead(false);
|
||||
const tick = setInterval(() => { const img = imgRef.current; if (img) img.src = url + '/frame?t=' + Date.now(); }, 100);
|
||||
const ping = setInterval(() => {
|
||||
fetch(url + '/ping').then(r => { if (!r.ok) throw new Error('bad'); }).catch(() => {
|
||||
if (!deadRef.current) { deadRef.current = true; setDead(true); onDead && onDead(); }
|
||||
});
|
||||
}, 2000);
|
||||
return () => { clearInterval(tick); clearInterval(ping); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url]);
|
||||
const pt = (e) => {
|
||||
const box = boxRef.current, img = imgRef.current;
|
||||
if (!box || !img || !img.naturalWidth || !img.naturalHeight) return null;
|
||||
const r = box.getBoundingClientRect();
|
||||
if (r.width === 0 || r.height === 0) return null;
|
||||
return {
|
||||
x: Math.round((e.clientX - r.left) / r.width * img.naturalWidth),
|
||||
y: Math.round((e.clientY - r.top) / r.height * img.naturalHeight)
|
||||
};
|
||||
};
|
||||
const handle = (e) => {
|
||||
const p = pt(e); if (!p) return;
|
||||
post({ type: e.type === 'mousedown' ? 'mousedown' : e.type === 'mouseup' ? 'mouseup' : 'mousemove', x: p.x, y: p.y, button: e.button });
|
||||
};
|
||||
const close = () => { try { fetch(url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} onClose && onClose(); };
|
||||
return (
|
||||
<div className="relative w-full h-full flex flex-col">
|
||||
<div className="flex items-center justify-between px-3 py-1.5 bg-slate-900/80 border border-slate-800 rounded-t-lg shrink-0">
|
||||
<span className="text-[10px] text-violet-300 font-mono">VST GUI — nhúng (frame-capture · ~10fps)</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{dead && <span className="text-[9px] text-red-400 font-mono">bridge mất kết nối</span>}
|
||||
<button onClick={close} className="px-2 py-0.5 bg-red-900/60 hover:bg-red-800 text-red-100 text-[10px] font-semibold rounded transition" title="Đóng GUI và trả về graph">✕ Đóng GUI</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={boxRef} tabIndex={0} className="flex-1 relative bg-slate-950 border border-slate-800 rounded-b-lg overflow-hidden outline-none"
|
||||
onMouseDown={handle} onMouseUp={handle} onMouseMove={handle}
|
||||
onMouseLeave={() => post({ type: 'mouseup', x: -1, y: -1, button: 0 })}
|
||||
onWheel={e => { const p = pt(e); if (p) post({ type: 'wheel', x: p.x, y: p.y, deltaY: e.deltaY }); }}
|
||||
onKeyDown={e => { e.preventDefault(); post({ type: 'keydown', keyCode: e.keyCode || e.which }); }}
|
||||
onKeyUp={e => { e.preventDefault(); post({ type: 'keyup', keyCode: e.keyCode || e.which }); }}>
|
||||
<img ref={imgRef} alt="VST GUI" className="w-full h-full object-contain select-none pointer-events-none" draggable={false} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen, openGui, showCarla = true) => (
|
||||
<div key={idx} className="flex items-center gap-2 bg-slate-900/60 border border-slate-800 rounded-lg px-2 py-1.5 flex-wrap">
|
||||
<button onClick={() => 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>
|
||||
@@ -11765,9 +11822,9 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
{(fxl || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}
|
||||
</select>
|
||||
<span className={`text-[9px] font-mono ${s.preset_b64 ? 'text-emerald-400' : 'text-slate-600'}`}>{s.preset_b64 ? '✓ preset' : 'no preset'}</span>
|
||||
<button onClick={() => 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 xuất từ Carla (Save preset)">Preset</button>
|
||||
<button onClick={() => 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={() => openGui(s)} 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>}
|
||||
<button onClick={() => { 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>
|
||||
{showCarla && <button onClick={() => { 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={() => 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={() => 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={() => 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>
|
||||
@@ -11789,14 +11846,6 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
const scopeMeterRRef = React.useRef(null);
|
||||
const eqCurveRef = React.useRef(null);
|
||||
const scopeStateRef = React.useRef({ L: null, R: null, head: 0, len: 0, tmpL: null, tmpR: null, freq: null });
|
||||
// Carla Bridge (VST FX) — danh sách VST để load vào Carla
|
||||
const [fxCarlaVsts, setFxCarlaVsts] = React.useState(null); // null = chưa load
|
||||
React.useEffect(() => {
|
||||
if (track && window.SonicAPI && window.SonicAPI.listPlugins) {
|
||||
window.SonicAPI.listPlugins().then(d => setFxCarlaVsts((d && d.vst_instruments) || [])).catch(() => setFxCarlaVsts([]));
|
||||
}
|
||||
}, [track && track.id]);
|
||||
|
||||
// VST FX (native_bridge --render-fx) — list VST3 effect plugins + preview
|
||||
const [fxList, setFxList] = React.useState([]);
|
||||
React.useEffect(() => {
|
||||
@@ -11815,10 +11864,16 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
// ── VST FX chain (native_bridge --render-fx, áp dụng khi Export/Offline render) ──
|
||||
const vstChain = (track.vstFxChain || []);
|
||||
const setVstChain = (next) => { if (onUpdateTrack) onUpdateTrack(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };
|
||||
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 3)
|
||||
const [fxGuiOpen, setFxGuiOpen] = React.useState(null); // { path, url } | null
|
||||
const closeFxGui = () => {
|
||||
if (fxGuiOpen) { try { fetch(fxGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} setFxGuiOpen(null); }
|
||||
};
|
||||
React.useEffect(() => () => { if (fxGuiOpen) { try { fetch(fxGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
|
||||
const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||
const removeVst = (idx) => setVstChain(vstChain.filter((_, i) => i !== idx));
|
||||
const moveVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= vstChain.length) return; const next = [...vstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setVstChain(next); };
|
||||
const openVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path }).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở GUI của ' + (s.name || s.path) + ' — chỉnh xong đóng cửa sổ plugin', 'success'); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn', 'info'); else window.showToast && window.showToast('Không mở được GUI', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const openVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.success && r.embed_url) setFxGuiOpen({ path: s.path, url: r.embed_url }); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; setVstChain([...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
||||
openVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
||||
const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
@@ -12141,7 +12196,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
</div>
|
||||
</div>
|
||||
{vstChain.length === 0 && <div className="text-[10px] text-slate-500 font-mono">Chưa có VST FX — chọn plugin bên dưới để thêm vào insert chain (WebAudio chain → VST FX → Fader).</div>}
|
||||
{vstChain.map((s, idx) => vstRow(s, idx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length, openVstGui))}
|
||||
{vstChain.map((s, idx) => vstRow(s, idx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length, openVstGui, false))}
|
||||
<select value={addVstSel} onChange={e => { setAddVstSel(e.target.value); if (e.target.value) { addVst(e.target.value); setAddVstSel(''); } }} className="w-full bg-slate-950 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-1.5 text-[11px] outline-none" title="Thêm VST FX">
|
||||
<option value="">+ Thêm VST FX...</option>
|
||||
{(fxList || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}
|
||||
@@ -12149,6 +12204,13 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
</div>
|
||||
|
||||
{/* WORKSPACE CONTROLS */}
|
||||
{fxGuiOpen ? (
|
||||
<div className="p-4">
|
||||
<VstGuiEmbed url={fxGuiOpen.url} onClose={closeFxGui}
|
||||
onDead={() => { window.showToast && window.showToast('Bridge VST GUI đã mất kết nối — trả về graph', 'error'); closeFxGui(); }} />
|
||||
</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">
|
||||
<div className="h-8 bg-slate-900/80 border-b border-slate-800/80 px-3 flex items-center justify-between text-xs font-mono rounded-t-lg shrink-0">
|
||||
<span className="text-slate-400">Active Module: <strong className="text-cyan-400">{activeMod ? (TRACK_FX_META[activeMod.type]?.name || activeMod.type) : '—'}</strong></span>
|
||||
@@ -12232,52 +12294,14 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
)}
|
||||
|
||||
{activeMod && activeMod.type === 'carla' && activeIdx >= 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[9px] text-teal-400 font-mono uppercase tracking-widest">CARLA BRIDGE — VST FX CHỈNH SỬA ÂM THANH</span>
|
||||
<span className="text-[9px] text-slate-500 font-mono">Carla chạy ngoài (user tự cài) · native GUI</span>
|
||||
</div>
|
||||
<div className="bg-slate-900/80 border border-teal-900/60 p-3 rounded-lg 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 font-mono mb-1">CHỌN VST FX (đã scan trên máy)</div>
|
||||
<select
|
||||
value={ap.plugin || ''}
|
||||
onChange={e => setParams(activeIdx, { 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>
|
||||
{(fxCarlaVsts || []).map(v => <option key={v.id || v.name} value={v.id || v.name}>{v.name || v.id}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ap.plugin) { window.showToast && window.showToast('Chọn VST FX trước khi Load Carla', 'warning'); return; }
|
||||
window.SonicAPI.openInCarla(ap.plugin, ap.plugin_path).then(r => {
|
||||
if (r && r.success) { window.showToast && window.showToast('Đã mở Carla với ' + ap.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();
|
||||
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 font-mono leading-relaxed">
|
||||
{ap.plugin
|
||||
? <>Đã chọn: <span className="text-teal-300">{ap.plugin}</span> — bấm <b>Load Carla Bridge</b> để mở native GUI VST và chỉnh sửa âm thanh.</>
|
||||
: 'Chọn VST FX từ danh sách đã scan, rồi bấm Load Carla Bridge để mở Carla (native GUI).'}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -12329,6 +12353,8 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ADD MODULE POPUP */}
|
||||
@@ -12374,11 +12400,17 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
|
||||
// ── VST FX masterbus (native_bridge --render-fx, áp dụng khi Export/Offline render) ──
|
||||
const masterVstChain = (ozState.vstFxChain || []);
|
||||
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 2)
|
||||
const [masterGuiOpen, setMasterGuiOpen] = React.useState(null); // { path, url } | null
|
||||
const closeMasterGui = () => {
|
||||
if (masterGuiOpen) { try { fetch(masterGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} setMasterGuiOpen(null); }
|
||||
};
|
||||
React.useEffect(() => () => { if (masterGuiOpen) { try { fetch(masterGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
|
||||
const setMasterVstChain = (next) => setOzState(prev => ({ ...prev, vstFxChain: next }));
|
||||
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||
const removeMasterVst = (idx) => setMasterVstChain(masterVstChain.filter((_, i) => i !== idx));
|
||||
const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };
|
||||
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path }).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở GUI của ' + (s.name || s.path) + ' — chỉnh xong đóng cửa sổ plugin', 'success'); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn', 'info'); else window.showToast && window.showToast('Không mở được GUI', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.success && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url }); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
||||
openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
||||
const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
@@ -13159,6 +13191,12 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
<main className="flex-1 flex overflow-hidden min-h-0">
|
||||
|
||||
{/* LEFT: MODULE VIEWS */}
|
||||
{masterGuiOpen ? (
|
||||
<div className="flex-1 flex flex-col bg-slate-950 relative overflow-y-auto oz-scrollbar p-4">
|
||||
<VstGuiEmbed url={masterGuiOpen.url} onClose={closeMasterGui}
|
||||
onDead={() => { window.showToast && window.showToast('Bridge VST GUI đã mất kết nối — trả về graph', 'error'); closeMasterGui(); }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col bg-slate-950 relative overflow-y-auto oz-scrollbar">
|
||||
|
||||
{/* SUB HEADER TOOLBAR */}
|
||||
@@ -13568,6 +13606,7 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RIGHT SIDEBAR: I/O METERS */}
|
||||
<div className="w-64 bg-slate-900 border-l border-slate-800/80 p-3 flex flex-col justify-between shadow-2xl oz-font-mono text-xs z-20 shrink-0">
|
||||
|
||||
+1980
-1972
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user