fix(mastering): VST chain edits no longer reverted by cross-device temp sync

Guard the 3s applyRemote sync so it does not clobber masteringSettings
while the Mastering panel is open (user actively editing chain). Another
connected session's temp snapshot (written every 30s) was reverting
chain add/remove/toggle of VST modules within ~3s.
This commit is contained in:
2026-08-18 09:36:24 +07:00
parent 569be72110
commit c9b35d60ff
2 changed files with 2040 additions and 1993 deletions
+58 -14
View File
@@ -1622,7 +1622,7 @@ function rebuildMasteringGraph(activate, chainArray) {
Object.keys(eqProStore).forEach(k => { try { eqProStore[k].destroy && eqProStore[k].destroy(); } catch (e) { } });
Object.keys(eqProStore).forEach(k => delete eqProStore[k]);
activeMods.forEach(mod => {
if (mod.type === 'carla') {
if (mod.type === 'carla' || mod.type === 'vst3') {
// Carla Bridge = VST FX chạy NGOÀI (Carla standalone) — không có node
// WebAudio trong master chain: pass-through, prev giữ nguyên.
return;
@@ -12360,7 +12360,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
{/* ADD MODULE POPUP */}
{addModuleOpen && (
<div className="fixed inset-0 z-[120] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onClick={() => setAddModuleOpen(false)}>
<div className="w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl" onClick={e => e.stopPropagation()}>
<div className="w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl max-h-[80vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<h3 className="text-xs font-bold text-white uppercase tracking-wider font-mono">THÊM MODULE VÀO FX CHAIN</h3>
<button onClick={() => setAddModuleOpen(false)} className="text-slate-400 hover:text-white"><i data-lucide="x" className="w-4 h-4"></i></button>
@@ -12942,9 +12942,10 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
limiter: { name: 'Brickwall Limiter', sub: 'True-Peak 20:1', icon: 'shield-half', color: '#f43f5e' },
exciter: { name: 'Harmonic Exciter', sub: 'Saturation & Air', icon: 'wand-2', color: '#c084fc' },
rebalance: { name: 'Master Rebalance', sub: 'M/S Balance', icon: 'sliders-horizontal', color: '#38bdf8' },
carla: { name: 'Carla Bridge (VST FX)', sub: 'Native VST audio processing', icon: 'sliders', color: '#14b8a6' }
carla: { name: 'Carla Bridge (VST FX)', sub: 'Native VST audio processing', icon: 'sliders', color: '#14b8a6' },
vst3: { name: 'VST FX', sub: 'VST3 plugin', icon: 'plug', color: '#2dd4bf' }
};
const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : type === 'carla' ? 'carlaBridgeActive' : 'rebalActive';
const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : type === 'carla' ? 'carlaBridgeActive' : type === 'vst3' ? 'vstFxActive' : 'rebalActive';
const chainActive = (type) => !!ozState[chainFlag(type)];
const toggleChainModule = (modId) => {
@@ -12956,17 +12957,29 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
});
const flags = {};
chain.forEach(m => { flags[chainFlag(m.type)] = !!m.active; });
return { ...prev, chain, ...flags };
const target = chain.find(m => m.id === modId);
let vstFxChain = prev.vstFxChain || [];
if (target && target.type === 'vst3') {
const vi = vstFxChain.findIndex(s => s.path === target.path);
if (vi >= 0) vstFxChain = vstFxChain.map((s, si) => si === vi ? { ...s, bypass: !target.active } : s);
}
return { ...prev, chain, ...flags, vstFxChain };
});
};
const removeChainModule = (modId) => {
setOzState(prev => {
const removed = prev.chain.find(m => m.id === modId);
let chain = prev.chain.filter(m => m.id !== modId);
if (chain.length === 0) chain = DEFAULT_MASTER_CHAIN.map(m => ({ ...m })); // keep ≥1
const flags = {};
chain.forEach(m => { flags[chainFlag(m.type)] = !!m.active; });
const activeModule = prev.activeModule;
return { ...prev, chain, ...flags, activeModule: chain.some(m => m.type === activeModule) ? activeModule : chain[chain.length - 1].type };
let vstFxChain = prev.vstFxChain || [];
if (removed && removed.type === 'vst3') {
const vi = vstFxChain.findIndex(s => s.path === removed.path);
if (vi >= 0) vstFxChain = [...vstFxChain.slice(0, vi), ...vstFxChain.slice(vi + 1)];
}
return { ...prev, chain, ...flags, vstFxChain, activeModule: chain.some(m => m.type === activeModule) ? activeModule : chain[chain.length - 1].type };
});
};
const reorderChain = (fromIdx, toIdx) => {
@@ -12992,6 +13005,18 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
}));
setAddModuleOpen(false);
};
const addVstToChain = (path) => {
const fx = (masterFxList || []).find(f => f.path === path);
if (!fx) return;
const id = 'mod_vst_' + Date.now();
const entry = { id, type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false, active: true };
setOzState(prev => ({
...prev,
chain: [...(prev.chain || []), entry],
vstFxChain: [...(prev.vstFxChain || []), { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]
}));
setAddModuleOpen(false);
};
const updateChainEntryParams = (modId, patch) => {
setOzState(prev => ({
...prev,
@@ -13128,29 +13153,30 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mr-1 oz-font-mono shrink-0">CHAIN:</span>
{(ozState.chain || []).map((mod, idx) => {
const isVst = mod.type === 'vst3';
const meta = MODULE_META[mod.type] || { name: mod.type, sub: '', icon: 'circle', color: '#94a3b8' };
const isEditing = ozState.activeModule === mod.type;
const isOn = chainActive(mod.type);
const isEditing = !isVst && ozState.activeModule === mod.type;
const isOn = isVst ? mod.active !== false : chainActive(mod.type);
return (
<div key={mod.id}
draggable
onDragStart={e => { dragChainIndexRef.current = idx; e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
onDrop={e => { e.preventDefault(); const from = dragChainIndexRef.current; if (from !== null && from !== idx) reorderChain(from, idx); dragChainIndexRef.current = null; }}
onClick={() => switchModule(mod.type)}
onClick={() => { if (mod.type === 'vst3') { openMasterVstGui({ path: mod.path, name: mod.name || mod.path }); } else switchModule(mod.type); }}
className={`w-40 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${isEditing ? 'oz-card-active' : 'oz-card'}`}>
<div className="flex items-center gap-2 min-w-0">
<button onClick={e => { e.stopPropagation(); toggleChainModule(mod.id); }} className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0" style={{backgroundColor: isOn ? '#38bdf8' : '#334155', color: isOn ? '#0f172a' : '#94a3b8'}}>
<i data-lucide="power" className="w-2.5 h-2.5"></i>
</button>
<div className="min-w-0">
<div className="text-[11px] font-bold text-slate-200 truncate">{meta.name}</div>
<div className="text-[9px] oz-font-mono truncate" style={{color: meta.color}}>{idx + 1}. {meta.sub}</div>
<div className="text-[11px] font-bold text-slate-200 truncate">{isVst ? (mod.name || meta.name) : meta.name}</div>
<div className="text-[9px] oz-font-mono truncate" style={{color: meta.color}}>{idx + 1}. {isVst ? (mod.path ? mod.path.split(/[\\/]/).pop() : 'VST3') : meta.sub}</div>
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<i data-lucide={meta.icon} className="w-3 h-3 text-slate-500"></i>
<button onClick={e => { e.stopPropagation(); removeChainModule(mod.id); }} className="text-slate-600 hover:text-red-400 text-xs px-0.5" title="Xóa module">
<button onClick={e => { e.stopPropagation(); removeChainModule(mod.id); }} className="text-slate-600 hover:text-red-400 text-xs w-4 h-4 flex items-center justify-center shrink-0" title="Xóa module">
<i data-lucide="x" className="w-3 h-3"></i>
</button>
</div>
@@ -13672,7 +13698,7 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
{/* ADD MODULE POPUP (mastering_expand.md §III) */}
{addModuleOpen && (
<div className="fixed inset-0 z-[110] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onClick={() => setAddModuleOpen(false)}>
<div className="w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl" onClick={e => e.stopPropagation()}>
<div className="w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl max-h-[80vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<h3 className="text-xs font-bold text-white uppercase tracking-wider oz-font-mono">THÊM MODULE VÀO MASTERING CHAIN</h3>
<button onClick={() => setAddModuleOpen(false)} className="text-slate-400 hover:text-white"><i data-lucide="x" className="w-4 h-4"></i></button>
@@ -13694,6 +13720,19 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
</button>
))}
</div>
{(masterFxList || []).length > 0 && (
<div className="border-t border-slate-800 pt-3">
<div className="text-[10px] font-bold text-teal-400 uppercase tracking-wider oz-font-mono mb-2">VST FX (Ozone...)</div>
<div className="grid grid-cols-2 gap-2">
{masterFxList.map(f => (
<button key={f.path} onClick={() => addVstToChain(f.path)} className="p-2 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-lg text-left space-y-0.5 transition-colors">
<div className="text-[11px] font-bold text-teal-300 flex items-center gap-1.5"><i data-lucide="plug" className="w-3 h-3"></i> {f.name || f.id}</div>
<div className="text-[9px] text-slate-500 font-mono truncate">{f.path}</div>
</button>
))}
</div>
</div>
)}
</div>
</div>
)}
@@ -17597,6 +17636,8 @@ const App = () => {
const [aiPresetModalOpen, setAiPresetModalOpen] = useState(false);
const [aiPresetVersion, setAiPresetVersion] = useState(0);
const [showMasteringModal, setShowMasteringModal] = useState(false);
const masteringModalOpenRef = useRef(false);
masteringModalOpenRef.current = showMasteringModal;
// ── Help / About / Preferences (menu Help + Tools) ──
const [aboutModalOpen, setAboutModalOpen] = useState(false);
const [helpModalOpen, setHelpModalOpen] = useState(false);
@@ -18284,7 +18325,10 @@ const App = () => {
setBpm((result.bpm || 120).toString());
if (result.sessionTabs && result.sessionTabs.length > 0) setSessionTabs(result.sessionTabs);
if (result.subTabs && result.subTabs.length > 0) setSubTabs(result.subTabs);
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
// Không đè masteringSettings khi user đang mở Mastering panel
// (modal đang chỉnh) — bản đồng bộ từ thiết bị khác
// không được revert chain đang sửa (ví dụ vừa xóa VST).
if (result.masteringSettings && !masteringModalOpenRef.current) setMasteringSettings(result.masteringSettings);
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
setProjectName(tmpName);
localStorage.setItem('sonic_project_name', tmpName);
File diff suppressed because one or more lines are too long