M6: fix 2-GUI khi mở VST FX native (mastering) — một plugin chỉ một GUI

Bug .24: mastering panel load VST FX → 1 floating native (FX slot N) + 1 embed
cùng lúc; floating không đóng được qua UI.

Backend (/fx-gui):
- native branch terminate bridge fx-gui fallback cũ cùng plugin_path trước
  ed.open (loại instance VST thứ 2 + window rời)
- fallback spawn chặn khi editor native đang mở ở session sống → trả native
  (không spawn embed/float thứ 2)
- _terminate_fx_gui_for_path dùng chung cho /fx-gui/close

Frontend (M6 — xử lý r.native):
- openMasterVstGui/openVstGui/auto-open/reopen: nhánh native → state
  {path, url:null, native:true} (không còn toast 'Không mở được GUI embed'),
  marker gui_open_path/vstGuiOpenPath giữ như embed
- closeMasterGui/closeFxGui native → SonicAPI.closeFxGui (engine capture preset
  + ed.close); unmount/hide panel → SonicAPI.hideFxGui
- panel render: GUI native mở = indicator + nút Đóng GUI (thay VstGuiEmbed)
- api.js: closeFxGui/hideFxGui/showFxGui endpoints
This commit is contained in:
2026-09-04 18:26:40 +07:00
parent cede51ae0c
commit ec2c925d09
3 changed files with 90 additions and 22 deletions
+38 -9
View File
@@ -722,6 +722,12 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
if _sess is not None and _sess.alive():
_slot = _sess.chain_slot_for_path(plugin_path)
if _slot is not None:
# Bug 2-GUI: fallback bridge fx-gui (embed/float) còn sống cho cùng
# plugin — instance VST THỨ 2 + window riêng; mở native phải loại nó
# trước, nếu không 1 plugin hiện 2 GUI (1 floating native + 1 embed)
# và floating không tắt được qua UI. Terminate proc cũ → frontend
# embed panel của nó onDead → tự đóng.
_terminate_fx_gui_for_path(plugin_path)
_already = fx_realtime.editor_find(plugin_path)
_same = _already is not None and _already[0] == _sess.name
_sess.send_editor_cmd("open", slot=_slot)
@@ -729,6 +735,15 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
return {"success": True, "started": not _same,
"already_running": bool(_same), "native": True,
"embed_url": None, "session_id": _sess.name, "slot": _slot}
# Tránh mở 2 GUI cùng plugin: editor NATIVE (instance DSP trong session
# fxrt) của plugin này đang mở ở session SỐNG → KHÔNG spawn instance 2 /
# bridge fx-gui nữa (nguồn CPU 100% + 2 GUI); trả native để frontend hiện
# đúng trạng thái cửa sổ ngoài (M6).
_nsess, _nslot = _fxrt_native_editor(plugin_path)
if _nsess is not None:
return {"success": True, "started": False, "already_running": True,
"native": True, "embed_url": None,
"session_id": _nsess.name, "slot": _nslot}
# Tránh mở 2 GUI cùng plugin: nếu tiến trình bridge fx-gui cho plugin này
# đang chạy → trả started=False (frontend có thể focus window cũ).
for proc in _FX_GUI_PROCESSES:
@@ -842,15 +857,8 @@ async def close_fx_gui(req: FxGuiCloseRequest, current_user: dict = Depends(get_
return {"success": True, "closed": True, "native": True,
"preset_b64": _preset}
# session đã chết — rơi xuống spawn fallback (đóng bridge fx-gui cũ nếu có)
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}
if _terminate_fx_gui_for_path(plugin_path):
return {"success": True, "closed": True}
return {"success": True, "closed": False}
@@ -908,6 +916,27 @@ def _prune_fx_gui_processes():
global _FX_GUI_PROCESSES
_FX_GUI_PROCESSES = [p for p in _FX_GUI_PROCESSES if p is not None and p.poll() is None]
def _terminate_fx_gui_for_path(plugin_path):
"""Terminate MỌI bridge fx-gui (embed/float fallback) còn sống cho plugin.
Dùng khi mở native editor thay thế fallback — tránh 2 instance VST + 2 GUI
cùng 1 plugin (Bug 2-GUI). Trả True nếu có proc đã bị chấm dứt."""
_prune_fx_gui_processes()
killed = False
for proc in list(_FX_GUI_PROCESSES):
if proc.poll() is None and getattr(proc, "_sf_plugin_path", None) == plugin_path:
try:
proc.terminate()
try:
proc.wait(timeout=5)
except Exception:
proc.kill()
killed = True
except Exception:
pass
if killed:
_prune_fx_gui_processes()
return killed
@router.post("/preview")
+46 -13
View File
@@ -13681,9 +13681,14 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
const closeFxGui = () => {
const g = fxGuiOpenRef.current;
if (g) {
// Capture preset CUỐI trước khi đóng (poll 1.5s có thể chưa kịp khi chỉnh
// xong đóng ngay) — gọi /preset TRƯỚC /close (close kill process bridge).
try { fetch(g.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}).then(() => { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} }); } catch (e) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e2) {} }
if (g.url) {
// Capture preset CUỐI trước khi đóng (poll 1.5s có thể chưa kịp khi chỉnh
// xong đóng ngay) — gọi /preset TRƯỚC /close (close kill process bridge).
try { fetch(g.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}).then(() => { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} }); } catch (e) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e2) {} }
} else if (g.native) {
// M6: GUI native (cửa sổ ngoài "FX slot N") — engine capture preset + đóng editor.
try { window.SonicAPI && window.SonicAPI.closeFxGui({ path: g.path }).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}); } catch (e) {}
}
}
setFxGuiOpen(null);
if (g && g.path) {
@@ -13693,7 +13698,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
}
if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: null, vstGuiOpenName: null });
};
React.useEffect(() => () => { const g = fxGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} const _tk = 'track:' + ((track && track.id) || '') + ':' + g.path; if (_presetRestartTimers[_tk]) { clearTimeout(_presetRestartTimers[_tk]); delete _presetRestartTimers[_tk]; } if (g.path) delete _fxGuiBaselineByPath[g.path]; } }, []);
React.useEffect(() => () => { const g = fxGuiOpenRef.current; if (g) { if (g.url) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } else if (g.native) { try { window.SonicAPI && window.SonicAPI.hideFxGui({ path: g.path }).catch(() => {}); } catch (e) {} } const _tk = 'track:' + ((track && track.id) || '') + ':' + g.path; if (_presetRestartTimers[_tk]) { clearTimeout(_presetRestartTimers[_tk]); delete _presetRestartTimers[_tk]; } if (g.path) delete _fxGuiBaselineByPath[g.path]; } }, []);
// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset).
React.useEffect(() => {
if (!fxGuiOpen || !fxGuiOpen.url) return;
@@ -13710,13 +13715,14 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRtTrackShmName(track && track.id), preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => {
if (r && r.embed_url) setFxGuiOpen({ path: s.path, url: r.embed_url });
else if (r && r.native) setFxGuiOpen({ path: s.path, url: null, name: s.name || s.path, native: true });
}).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [track && track.id, track && track.vstGuiOpenPath]);
const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
const removeVst = (idx) => { const rem = vstChain[idx]; if (rem && fxGuiOpen && fxGuiOpen.path === rem.path) closeFxGui(); if (rem) delete _capturedPresetByPath[rem.path]; if (idx === activeVstIdx) setActiveVstIdx(-1); 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, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen && fxGuiOpen.path === s.path) { if (idx !== undefined) setActiveVstIdx(idx); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); 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, shm: fxRtTrackShmName(track && track.id), preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => { if (r && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', '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 openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen && fxGuiOpen.path === s.path) { if (idx !== undefined) setActiveVstIdx(idx); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); 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, shm: fxRtTrackShmName(track && track.id), preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => { if (r && r.native) { setFxGuiOpen({ path: s.path, url: null, name: s.name || s.path, native: true }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', '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; const next = [...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]; setVstChain(next); setActiveVstIdx(next.length - 1); };
const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
const uploadVstPreset = (idx) => {
@@ -14083,7 +14089,15 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
</div>
{/* WORKSPACE CONTROLS */}
{fxGuiOpen ? (
{fxGuiOpen && !fxGuiOpen.url && fxGuiOpen.native ? (
<div className="flex-1 min-h-0 flex flex-col items-center justify-center gap-3 text-slate-400 select-none">
<i data-lucide="external-link" className="w-10 h-10 text-teal-400"></i>
<div className="text-sm font-bold text-slate-200">GUI native đang mở (cửa sổ ngoài)</div>
<div className="text-[11px] font-mono text-slate-500 max-w-md truncate">{fxGuiOpen.name || fxGuiOpen.path}</div>
<div className="text-[10px] text-slate-500">Chỉnh thông số trong cửa sổ "FX slot N" — bấm Đóng GUI để đóng editor (giữ DSP + preset).</div>
<button onClick={closeFxGui} className="px-4 py-2 bg-teal-700 hover:bg-teal-600 text-white text-xs font-bold rounded-lg transition">Đóng GUI</button>
</div>
) : fxGuiOpen ? (
<div className="flex-1 min-h-0">
<VstGuiEmbed url={fxGuiOpen.url} onClose={closeFxGui}
onDead={() => { window.showToast && window.showToast('Plugin VST GUI lỗi khi mở (bridge chết/treo — check %TEMP%\\sf_dumps\\fx_bridge_crash_*.dmp) — đã trả về graph', 'error'); closeFxGui(); }} />
@@ -14294,8 +14308,13 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
const closeMasterGui = () => {
const g = masterGuiOpenRef.current;
if (g) {
// Capture preset CUỐI trước khi đóng (giống closeFxGui) — /preset rồi /close.
try { fetch(g.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}).then(() => { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} }); } catch (e) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e2) {} }
if (g.url) {
// Capture preset CUỐI trước khi đóng (giống closeFxGui) — /preset rồi /close.
try { fetch(g.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}).then(() => { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} }); } catch (e) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e2) {} }
} else if (g.native) {
// M6: GUI native (cửa sổ ngoài "FX slot N") — engine capture preset + đóng editor.
try { window.SonicAPI && window.SonicAPI.closeFxGui({ path: g.path }).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}); } catch (e) {}
}
}
setMasterGuiOpen(null);
if (g && g.path) {
@@ -14306,7 +14325,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
setActiveMasterVstPath(null);
setOzState(prev => { const n = { ...(prev || {}) }; delete n.gui_open_path; delete n.gui_open_name; return n; });
};
React.useEffect(() => () => { const g = masterGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} const _mk = 'master:' + g.path; if (_presetRestartTimers[_mk]) { clearTimeout(_presetRestartTimers[_mk]); delete _presetRestartTimers[_mk]; } if (g.path) delete _fxGuiBaselineByPath[g.path]; } }, []);
React.useEffect(() => () => { const g = masterGuiOpenRef.current; if (g) { if (g.url) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } else if (g.native) { try { window.SonicAPI && window.SonicAPI.hideFxGui({ path: g.path }).catch(() => {}); } catch (e) {} } const _mk = 'master:' + g.path; if (_presetRestartTimers[_mk]) { clearTimeout(_presetRestartTimers[_mk]); delete _presetRestartTimers[_mk]; } if (g.path) delete _fxGuiBaselineByPath[g.path]; } }, []);
// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset).
React.useEffect(() => {
if (!masterGuiOpen || !masterGuiOpen.url) return;
@@ -14324,8 +14343,13 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
if (isOpen) return;
const g = masterGuiOpenRef.current;
if (!g) return;
try { if (g.path) { fetch(g.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}); } } catch (e) {}
try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {}
if (g.url) {
try { if (g.path) { fetch(g.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64 && g.path) _capturedPresetByPath[g.path] = d.preset_b64; }).catch(() => {}); } } catch (e) {}
try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {}
} else if (g.native) {
// Ẩn GUI native khi đóng panel (giữ editor+instance, mở lại panel nhanh).
try { window.SonicAPI && window.SonicAPI.hideFxGui({ path: g.path }).catch(() => {}); } catch (e) {}
}
const _mk = 'master:' + g.path;
if (_presetRestartTimers[_mk]) { clearTimeout(_presetRestartTimers[_mk]); delete _presetRestartTimers[_mk]; }
if (g.path) delete _fxGuiBaselineByPath[g.path];
@@ -14355,6 +14379,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
const _name = st.gui_open_name || s.name || _p;
window.SonicAPI.openFxGui({ path: _p, name: _name, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[_p] || s.preset_b64 || '' }).then(r => {
_autoOpenRef.current.inFlight = false;
if (r && r.native && !masterGuiOpenRef.current) { _autoOpenRef.current.attempt = 0; setMasterGuiOpen({ path: _p, url: null, name: _name, native: true }); return; }
if (r && r.embed_url && !masterGuiOpenRef.current) { _autoOpenRef.current.attempt = 0; setMasterGuiOpen({ path: _p, url: r.embed_url, name: _name }); return; }
if (r && r.already_running && r.embed_url && !masterGuiOpenRef.current) { _autoOpenRef.current.attempt = 0; setMasterGuiOpen({ path: _p, url: r.embed_url, name: _name }); return; }
if (r && r.already_running) { _autoOpenRef.current.attempt = 0; setActiveMasterVstPath(null); window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); return; }
@@ -14403,7 +14428,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
const removeMasterVst = (idx) => { const rem = masterVstChain[idx]; if (rem) delete _capturedPresetByPath[rem.path]; 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 (masterGuiOpen && masterGuiOpen.path === s.path) { setActiveMasterVstPath(s.path); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); 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, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => { if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', '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 openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen && masterGuiOpen.path === s.path) { setActiveMasterVstPath(s.path); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); 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, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => { if (r && r.native) { setMasterGuiOpen({ path: s.path, url: null, name: s.name || s.path, native: true }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', '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); delete _capturedPresetByPath[path]; setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
@@ -15200,7 +15225,15 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
<main className="flex-1 flex overflow-hidden min-h-0">
{/* LEFT: MODULE VIEWS */}
{masterGuiOpen ? (
{masterGuiOpen && !masterGuiOpen.url && masterGuiOpen.native ? (
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-slate-400 bg-slate-950 select-none min-h-0">
<i data-lucide="external-link" className="w-10 h-10 text-cyan-400"></i>
<div className="text-sm font-bold text-slate-200">GUI native đang mở (cửa sổ ngoài)</div>
<div className="text-[11px] font-mono text-slate-500 max-w-md truncate">{masterGuiOpen.name || masterGuiOpen.path}</div>
<div className="text-[10px] text-slate-500 text-center max-w-md">Chỉnh thông số trong cửa sổ "FX slot N" của Ozone — thay đổi tự lưu vào project khi bấm Đóng GUI.</div>
<button onClick={closeMasterGui} className="px-4 py-2 bg-cyan-700 hover:bg-cyan-600 text-white text-xs font-bold rounded-lg transition">Đóng GUI</button>
</div>
) : masterGuiOpen ? (
<div className="flex-1 flex flex-col bg-slate-950 relative min-h-0">
<VstGuiEmbed url={masterGuiOpen.url} onClose={closeMasterGui}
onDead={() => { window.showToast && window.showToast('Plugin VST GUI lỗi khi mở (bridge chết/treo — check %TEMP%\\sf_dumps\\fx_bridge_crash_*.dmp) — đã trả về graph', 'error'); closeMasterGui(); }} />
+6
View File
@@ -90,6 +90,12 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
fxRealtimeSetChain: (payload) => apiRequest('/api/v1/plugins/fx-realtime/set-chain', { method: 'POST', body: JSON.stringify(payload) }),
// 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) }),
// M6: đóng/ẩn/hiện GUI NATIVE (editor instance DSP trong session fxrt).
// Engine capture preset khi close → trả preset_b64. Embed cũ đóng thẳng
// qua HTTP của bridge (g.url + '/close') — chỉ dùng cho native.
closeFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui/close', { method: 'POST', body: JSON.stringify(payload) }),
hideFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui/hide', { method: 'POST', body: JSON.stringify(payload) }),
showFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui/show', { method: 'POST', body: JSON.stringify(payload) }),
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
// Thư viện preset VST3 (.vstpreset) — cầu nối preset → pedalboard
listPresets: () => apiRequest('/api/v1/presets', { method: 'GET' }),