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:
+52
-57
@@ -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); };
|
||||
}, []);
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -51,7 +51,7 @@
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202609021627" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202609021718" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user