V45.41: fix 4 loi Mastering Panel tren may khac - xoa toolbar Delta Listen (IRC/EQ Presets/Learn Input Gain), xoa nut 'X Dong GUI' VstGuiEmbed, GUI embed chi mo theo click (bo auto-open khi vao panel + bo persist _persistMasterGuiOpen), fix flash VST GUI bang frame preload single-flight + throttle /show theo rect, fix chip 'Server' nhap nhay (hysteresis: offline sau 3 fail lien tiep, timeout 6s). Rebuild engine+installer v=202609021718.

This commit is contained in:
2026-09-02 17:40:07 +07:00
parent d8e5f52017
commit 328f3506bb
3 changed files with 77 additions and 79 deletions
+52 -57
View File
@@ -12522,8 +12522,8 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
// Merge cả vstFxChain + chain (entry vst3) để 2 nơi lưu đồng nhất.
vstFxChain: (masteringSettings.vstFxChain || []).map(s => ({ ...s, preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' })),
chain: (masteringSettings.chain || []).map(m => m && m.type === 'vst3' ? { ...m, preset_b64: _capturedPresetByPath[m.path] || m.preset_b64 || '' } : m),
gui_open_path: (_persistMasterGuiOpen && _persistMasterGuiOpen.path) ? _persistMasterGuiOpen.path : (masteringSettings && masteringSettings.gui_open_path) || null,
gui_open_name: (_persistMasterGuiOpen && _persistMasterGuiOpen.name) ? _persistMasterGuiOpen.name : (masteringSettings && masteringSettings.gui_open_name) || null
gui_open_path: (masteringSettings && masteringSettings.gui_open_path) || null,
gui_open_name: (masteringSettings && masteringSettings.gui_open_name) || null
} : null
};
};
@@ -13375,6 +13375,8 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
// onDead khi bridge chết (ping fail) → parent trả graph + toast.
const VstGuiEmbed = ({ url, onClose, onDead }) => {
const imgRef = React.useRef(null);
const preloadRef = React.useRef(null);
const lastShowRef = React.useRef(null);
const boxRef = React.useRef(null);
const deadRef = React.useRef(false);
const screenRectRef = React.useRef(null);
@@ -13385,7 +13387,17 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
deadRef.current = false;
setDead(false);
setFrameLoaded(false);
const tick = setInterval(() => { const img = imgRef.current; if (img && img.complete) img.src = url + '/frame?t=' + Date.now(); }, 100);
const tick = setInterval(() => {
const img = imgRef.current;
if (!img || deadRef.current) return;
const pr = preloadRef.current;
if (pr && !pr.complete) return; // frame đang tải dở → chờ, không spam /frame
const np = pr || new Image();
np.onload = () => { if (preloadRef.current === np) { preloadRef.current = null; img.src = np.src; } };
np.onerror = () => { if (preloadRef.current === np) preloadRef.current = null; }; // frame lỗi → giữ frame cũ, không flash/trắng
np.src = url + '/frame?t=' + Date.now();
preloadRef.current = np;
}, 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(); }
@@ -13396,9 +13408,9 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
// rect refresh below re-anchors it over the panel.
try { fetch(url + '/show', { method: 'POST' }).catch(() => {}); } catch (e) {}
return () => {
clearInterval(tick); clearInterval(ping);
clearInterval(tick); clearInterval(ping); preloadRef.current = null;
// Panel closed -> hide the native window (bridge stays alive so
// reopening is fast). The "Đóng GUI" button still POSTs /close (exit).
// reopening is fast); POST /hide chứ không /close.
try { fetch(url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -13411,9 +13423,9 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
const maybeShow = () => {
const r = screenRectRef.current;
if (r && r.w >= 4 && r.h >= 4) {
// Re-POST /show on every refresh (~500ms) so the native window
// re-anchors over the panel on scroll/drag/resize. Idempotent: the
// bridge skips SetWindowPos/resize when nothing moved.
const key = Math.round(r.x) + ',' + Math.round(r.y) + ',' + Math.round(r.w) + ',' + Math.round(r.h);
if (lastShowRef.current === key) return; // rect không đổi → không spam /show
lastShowRef.current = key;
try { fetch(url + '/show', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rect: r }) }).catch(() => {}); } catch (e) {}
}
};
@@ -13461,14 +13473,12 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
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, buttons: e.buttons, rect: screenRectRef.current || undefined });
};
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"
@@ -14194,9 +14204,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
// ──────────────────────────────────────────────
// MASTERING MODAL COMPONENT (from md/45_MASTERING_MODULE.md)
// ──────────────────────────────────────────────
// GUI embed state survives MasteringModal unmount so the native window
// re-shows automatically when the panel is reopened (close/reopen bug).
let _persistMasterGuiOpen = null;
const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettings }) => {
const ozState = masteringSettings;
const setOzState = setMasteringSettings;
@@ -14212,11 +14220,16 @@ 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, setMasterGuiOpenState] = React.useState(_persistMasterGuiOpen); // { path, url } | null
const setMasterGuiOpen = (v) => { if (_persistMasterGuiOpen && _persistMasterGuiOpen.url) _fxGuiUnregisterSource(_persistMasterGuiOpen.url); _persistMasterGuiOpen = v; if (v && v.url) _fxGuiRegisterSource(v.url, null); setMasterGuiOpenState(v); };
const [masterGuiOpen, setMasterGuiOpenState] = React.useState(null); // { path, url } | null — GUI CHỈ mở theo click
const [activeMasterVstPath, setActiveMasterVstPath] = React.useState(null);
const masterGuiOpenRef = React.useRef(null);
masterGuiOpenRef.current = masterGuiOpen;
const setMasterGuiOpen = (v) => {
const prev = masterGuiOpenRef.current;
if (prev && prev.url) _fxGuiUnregisterSource(prev.url);
if (v && v.url) _fxGuiRegisterSource(v.url, null);
setMasterGuiOpenState(v);
};
const closeMasterGui = () => {
const g = masterGuiOpenRef.current;
if (g) {
@@ -14242,16 +14255,22 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
return () => clearInterval(iv);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [masterGuiOpen]);
// Reopen the saved GUI when the panel is opened after a project restore.
// Modal đóng (modal luôn mounted, KHÔNG unmount): ẩn GUI + xoá persist để
// lần sau mở panel KHÔNG tự mở lại GUI (fix auto-open khi vào master chain).
// Chỉ POST /hide — bridge sống, click mở lại nhanh.
React.useEffect(() => {
if (!isOpen || !ozState || !ozState.gui_open_path || masterGuiOpen) return;
const s = (ozState.vstFxChain || []).find(x => x.path === ozState.gui_open_path);
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
window.SonicAPI.openFxGui({ path: s.path, name: ozState.gui_open_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 });
}).catch(() => {});
if (isOpen) return;
const g = masterGuiOpenRef.current;
if (!g) return;
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];
setMasterGuiOpen(null);
setActiveMasterVstPath(null);
setOzState(prev => { const n = { ...(prev || {}) }; delete n.gui_open_path; delete n.gui_open_name; return n; });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, ozState && ozState.gui_open_path]);
}, [isOpen]);
const setMasterVstChain = (next) => setOzState(prev => {
const chain = prev.chain || [];
const out = [];
@@ -15089,35 +15108,6 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
) : (
<div className="flex-1 flex flex-col bg-slate-950 relative overflow-y-auto oz-scrollbar">
{/* SUB HEADER TOOLBAR */}
<div className="h-9 bg-slate-900/60 border-b border-slate-800/80 px-4 flex items-center justify-between text-xs oz-font-mono shrink-0">
<div className="flex items-center gap-3">
<span className="text-slate-400 flex items-center gap-1.5"><i data-lucide="headphones" className="w-3 h-3 text-cyan-400"></i> Delta Listen</span>
<select className="bg-slate-950 border border-slate-800 rounded px-2 py-0.5 text-[11px] text-slate-300 outline-none">
<option value="irc4">IRC IV - Classic</option>
<option value="irc3">IRC III - Balanced</option>
<option value="irc2">IRC II - Crisp</option>
</select>
</div>
<div className="flex items-center gap-2 text-[11px]">
{ozState.activeModule === 'eq' && (
<div className="flex items-center gap-1.5">
<span className="text-slate-400">EQ Presets:</span>
<select
value={ozState.eqPreset || 'flat'}
onChange={e => applyEQPreset(e.target.value)}
className="bg-slate-950 border border-slate-700 text-cyan-300 rounded px-2 py-0.5 text-[11px] outline-none focus:border-cyan-500">
{Object.keys(EQ_PRESET_LIBRARY).map(k => (
<option key={k} value={k}>{EQ_PRESET_LIBRARY[k].name}</option>
))}
</select>
</div>
)}
<span className="text-slate-400">Learn Input Gain:</span>
<button className="bg-cyan-950 text-cyan-300 border border-cyan-800/80 px-2 py-0.5 rounded font-bold hover:bg-cyan-900 transition-colors">-11.0 LUFS</button>
</div>
</div>
{/* VIEW: DYNAMIC EQ */}
<div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'eq' ? '' : 'hidden'}`}>
<div className="relative w-full h-64 bg-slate-950 border border-slate-800/90 rounded-xl overflow-hidden shadow-inner cursor-crosshair">
@@ -23730,19 +23720,24 @@ const App = () => {
return () => observer.disconnect();
}, [timelineWrapperNode]);
useEffect(() => {
let cancelled = false, busy = false, iv = null;
let cancelled = false, busy = false, iv = null, fails = 0;
// Hysteresis: chỉ báo offline sau 3 lần fail LIÊN TIẾP (~12-18s), 1 lần
// trễ/timeout đơn lẻ (engine khởi động chậm máy khác) KHÔNG làm chip
// nhấp nháy connected/offline — fix "Server không stable".
const ok = () => { fails = 0; if (!cancelled) setServerStatus('connected'); };
const bad = () => { fails++; if (!cancelled && fails >= 3) setServerStatus('offline'); };
const check = () => {
if (busy || cancelled) return;
busy = true;
const ctl = new AbortController();
const t = setTimeout(() => ctl.abort(), 3000);
const t = setTimeout(() => ctl.abort(), 6000);
fetch(API_BASE_URL, { signal: ctl.signal })
.then(r => { if (!cancelled) setServerStatus(r.ok ? 'connected' : 'error'); })
.catch(() => { if (!cancelled) setServerStatus('offline'); })
.then(r => { if (r.ok) ok(); else bad(); })
.catch(() => bad())
.finally(() => { busy = false; clearTimeout(t); });
};
check();
iv = setInterval(() => { if (!cancelled) check(); }, 3000);
iv = setInterval(() => { if (!cancelled) check(); }, 4000);
return () => { cancelled = true; if (iv) clearInterval(iv); };
}, []);