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 => { try { eqProStore[k].destroy && eqProStore[k].destroy(); } catch (e) { } });
Object.keys(eqProStore).forEach(k => delete eqProStore[k]); Object.keys(eqProStore).forEach(k => delete eqProStore[k]);
activeMods.forEach(mod => { 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 // Carla Bridge = VST FX chạy NGOÀI (Carla standalone) — không có node
// WebAudio trong master chain: pass-through, prev giữ nguyên. // WebAudio trong master chain: pass-through, prev giữ nguyên.
return; return;
@@ -12360,7 +12360,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
{/* ADD MODULE POPUP */} {/* ADD MODULE POPUP */}
{addModuleOpen && ( {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="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"> <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> <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> <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' }, 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' }, 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' }, 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 chainActive = (type) => !!ozState[chainFlag(type)];
const toggleChainModule = (modId) => { const toggleChainModule = (modId) => {
@@ -12956,17 +12957,29 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
}); });
const flags = {}; const flags = {};
chain.forEach(m => { flags[chainFlag(m.type)] = !!m.active; }); 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) => { const removeChainModule = (modId) => {
setOzState(prev => { setOzState(prev => {
const removed = prev.chain.find(m => m.id === modId);
let chain = prev.chain.filter(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 if (chain.length === 0) chain = DEFAULT_MASTER_CHAIN.map(m => ({ ...m })); // keep ≥1
const flags = {}; const flags = {};
chain.forEach(m => { flags[chainFlag(m.type)] = !!m.active; }); chain.forEach(m => { flags[chainFlag(m.type)] = !!m.active; });
const activeModule = prev.activeModule; 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) => { const reorderChain = (fromIdx, toIdx) => {
@@ -12992,6 +13005,18 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
})); }));
setAddModuleOpen(false); 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) => { const updateChainEntryParams = (modId, patch) => {
setOzState(prev => ({ setOzState(prev => ({
...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> <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) => { {(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 meta = MODULE_META[mod.type] || { name: mod.type, sub: '', icon: 'circle', color: '#94a3b8' };
const isEditing = ozState.activeModule === mod.type; const isEditing = !isVst && ozState.activeModule === mod.type;
const isOn = chainActive(mod.type); const isOn = isVst ? mod.active !== false : chainActive(mod.type);
return ( return (
<div key={mod.id} <div key={mod.id}
draggable draggable
onDragStart={e => { dragChainIndexRef.current = idx; e.dataTransfer.effectAllowed = 'move'; }} onDragStart={e => { dragChainIndexRef.current = idx; e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = '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; }} 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'}`}> 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"> <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'}}> <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> <i data-lucide="power" className="w-2.5 h-2.5"></i>
</button> </button>
<div className="min-w-0"> <div className="min-w-0">
<div className="text-[11px] font-bold text-slate-200 truncate">{meta.name}</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}. {meta.sub}</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> </div>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
<i data-lucide={meta.icon} className="w-3 h-3 text-slate-500"></i> <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> <i data-lucide="x" className="w-3 h-3"></i>
</button> </button>
</div> </div>
@@ -13672,7 +13698,7 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
{/* ADD MODULE POPUP (mastering_expand.md §III) */} {/* ADD MODULE POPUP (mastering_expand.md §III) */}
{addModuleOpen && ( {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="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"> <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> <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> <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> </button>
))} ))}
</div> </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>
</div> </div>
)} )}
@@ -17597,6 +17636,8 @@ const App = () => {
const [aiPresetModalOpen, setAiPresetModalOpen] = useState(false); const [aiPresetModalOpen, setAiPresetModalOpen] = useState(false);
const [aiPresetVersion, setAiPresetVersion] = useState(0); const [aiPresetVersion, setAiPresetVersion] = useState(0);
const [showMasteringModal, setShowMasteringModal] = useState(false); const [showMasteringModal, setShowMasteringModal] = useState(false);
const masteringModalOpenRef = useRef(false);
masteringModalOpenRef.current = showMasteringModal;
// ── Help / About / Preferences (menu Help + Tools) ── // ── Help / About / Preferences (menu Help + Tools) ──
const [aboutModalOpen, setAboutModalOpen] = useState(false); const [aboutModalOpen, setAboutModalOpen] = useState(false);
const [helpModalOpen, setHelpModalOpen] = useState(false); const [helpModalOpen, setHelpModalOpen] = useState(false);
@@ -18284,7 +18325,10 @@ const App = () => {
setBpm((result.bpm || 120).toString()); setBpm((result.bpm || 120).toString());
if (result.sessionTabs && result.sessionTabs.length > 0) setSessionTabs(result.sessionTabs); if (result.sessionTabs && result.sessionTabs.length > 0) setSessionTabs(result.sessionTabs);
if (result.subTabs && result.subTabs.length > 0) setSubTabs(result.subTabs); 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'; const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
setProjectName(tmpName); setProjectName(tmpName);
localStorage.setItem('sonic_project_name', tmpName); localStorage.setItem('sonic_project_name', tmpName);
File diff suppressed because one or more lines are too long