From fa6d09ee83e8ed797fdffb888e4d6855c3df38fb Mon Sep 17 00:00:00 2001 From: locphamtran Date: Mon, 7 Sep 2026 09:03:23 +0700 Subject: [PATCH] =?UTF-8?q?M20:=20diag=20Bug=20B=20=E2=80=94=20log=20meter?= =?UTF-8?q?=20dB=20master=20OUT=20(load/fx=5Fclick/panel=5Fopen=20+=20hist?= =?UTF-8?q?=208s=20+=20post=201/2/4/8s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 66 +++++++++++++++++++++++++++++++- app/static/js/app.precompiled.js | 19 ++++++--- app/templates/index.html | 2 +- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 848c952..923a6a7 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -272,6 +272,67 @@ const _dbgLog = (tag, extra) => { }; // V29: expose dbg hook cho BridgeAudioNode (worklet state) + helpers. window.__dbgLog = _dbgLog; +// M20 Bug B diag: log dB meter master OUT (masterBus.analyser — cùng nguồn +// outPeakText MASTERING PANEL, SAU chain FX) + fader gain state. Hist vòng ~8s +// @250ms (chỉ giữ RAM, không log cho tới khi có event); marker: load (initMasterBus), +// fx_click (nút FX master), panel_open + post 1/2/4/8s (MASTERING PANEL mở) → +// so dB trước/sau để tìm cơ chế Bug B (âm lượng giảm khi mở panel). +let _mdiagHist = []; +let _mdiagTimer = null; +let _mdiagBuf = null; +function _mdiagDb() { + try { + const b = window.masterBus; + if (!b || !b.analyser) return null; + const an = b.analyser; + const len = an.fftSize || 2048; + if (!_mdiagBuf || _mdiagBuf.length < len) _mdiagBuf = new Float32Array(len); + an.getFloatTimeDomainData(_mdiagBuf); + let pk = 0; + for (let i = 0; i < len; i++) { const v = _mdiagBuf[i]; const a = v < 0 ? -v : v; if (a > pk) pk = a; } + return pk < 0.00003 ? -120 : 20 * Math.log10(pk); + } catch (e) { return null; } +} +function _mdiagFader() { + try { + const b = window.masterBus; if (!b) return 'na'; + const f = (x) => { try { const n = x && x.gain ? x.gain.value : null; return n === null ? 'na' : n.toFixed(4); } catch (e) { return 'na'; } }; + return 'out=' + f(b.output) + ' mute=' + f(b.muteGain) + ' volL=' + f(b.volLGain) + ' volR=' + f(b.volRGain); + } catch (e) { return 'na'; } +} +function _mdiagSample(tag) { + const db = _mdiagDb(); + try { _dbgLog('mdiag_' + tag, 'db=' + (db === null ? 'na' : db.toFixed(1)) + ' ' + _mdiagFader()); } catch (e) {} +} +function _mdiagStart() { + if (_mdiagTimer) return; + _mdiagTimer = setInterval(() => { + try { + if (!window.masterBus) return; + _mdiagHist.push(_mdiagDb()); + if (_mdiagHist.length > 32) _mdiagHist.shift(); // ~8s @250ms + } catch (e) {} + }, 250); +} +function _mdiagDump() { + try { + for (let i = 0; i < _mdiagHist.length; i++) { + const v = _mdiagHist[i]; + _dbgLog('mdiag_hist', 'db=' + (v === null ? 'na' : v.toFixed(1))); + } + } catch (e) {} +} +let _mdiagPostTimer = null; +function _mdiagSchedulePost() { + if (_mdiagPostTimer) clearTimeout(_mdiagPostTimer); + const del = [1000, 2000, 4000, 8000]; + let k = 0; + const tick = () => { + if (k >= del.length) return; + _mdiagPostTimer = setTimeout(() => { _mdiagSample('post' + del[k]); k++; tick(); }, del[k]); + }; + tick(); +} if (window.PerformanceObserver) { try { new PerformanceObserver((list) => { for (const e of list.getEntries()) _dbgLog('longtask', 'd=' + Math.round(e.duration) + ' s=' + Math.round(e.startTime)); }).observe({ entryTypes: ['longtask'] }); } catch (e) {} } const _dbgBridgeGain = () => { try { @@ -1324,6 +1385,7 @@ function initMasterBus(ctx) { masterBus.analyser.connect(ctx.destination); window.masterBus = masterBus; + try { _mdiagStart(); setTimeout(() => _mdiagSample('load1'), 1000); setTimeout(() => _mdiagSample('load5'), 5000); } catch (e) {} // Apply mastering once when the chain is first created (and on the // masteringSettings effect for subsequent changes — see useEffect). if (window.currentMasteringSettings) { @@ -3610,7 +3672,7 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, masterVolL, setMast React.createElement("button", { id: "fxBtn", className: "btn-daw h-[18px] rounded font-extrabold text-[10px] transition-all", - onClick: function() { setShowMasteringModal(true); }, + onClick: function() { try { _mdiagSample('fx_click'); } catch (e) {} setShowMasteringModal(true); }, title: "Mở MASTER FX CHAIN để chỉnh sửa" }, "FX"), React.createElement("button", { @@ -14801,6 +14863,8 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin }, []); React.useEffect(() => { if (!isOpen) { _autoOpenRef.current.attempt = 0; return; } + // M20 Bug B diag: giá trị meter đúng lúc mở MASTERING PANEL + hist 8s trước + post 1/2/4/8s + try { _mdiagSample('panel_open'); _mdiagDump(); _mdiagSchedulePost(); } catch (e) {} if (_autoOpenRef.current.timer) { clearTimeout(_autoOpenRef.current.timer); _autoOpenRef.current.timer = null; } _autoTryOpen(); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 9c240d9..c70371e 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -69,7 +69,13 @@ let _cc11Epoch=0;// V17: fade session dang treo (gain ramp FADE_MS) — restore let _pendingFadeSession=null;const restorePendingFadeSession=()=>{if(!_pendingFadeSession)return;let _bgBefore=-1;try{if(_pendingFadeSession.bridgeGainNode)_bgBefore=_pendingFadeSession.bridgeGainNode.gain.value;}catch(e){}try{const t=getAudioContext().currentTime;_pendingFadeSession.fadedGains.forEach(g=>{try{g.node.gain.cancelScheduledValues(t);g.node.gain.setValueAtTime(g.prev,t);}catch(e){}});if(_pendingFadeSession.bridgeGainNode){try{_pendingFadeSession.bridgeGainNode.gain.cancelScheduledValues(t);_pendingFadeSession.bridgeGainNode.gain.setValueAtTime(1.0,t);}catch(e){}}}catch(e){}_dbgLog('restore','gains='+_pendingFadeSession.fadedGains.length+' bg='+_bgBefore.toFixed(4)+'->1.0');_pendingFadeSession=null;};// V28: timing diagnostics -> %APPDATA%/SonicForgeDAW/logs/dbg.log (Rust dbg_log). // Fire-and-forget invoke — khong chan main thread; tag+extra ghi kem unix_ms. const _dbgLog=(tag,extra)=>{try{if(window.__TAURI__&&window.__TAURI__.core&&window.__TAURI__.core.invoke){window.__TAURI__.core.invoke('dbg_log',{tag:tag,extra:extra===undefined?'':String(extra)}).catch(function(){});}}catch(e){}};// V29: expose dbg hook cho BridgeAudioNode (worklet state) + helpers. -window.__dbgLog=_dbgLog;if(window.PerformanceObserver){try{new PerformanceObserver(list=>{for(const e of list.getEntries())_dbgLog('longtask','d='+Math.round(e.duration)+' s='+Math.round(e.startTime));}).observe({entryTypes:['longtask']});}catch(e){}}const _dbgBridgeGain=()=>{try{if(window.BridgeAudioNode&&window.BridgeAudioNode.getOutputNode){const _g=window.BridgeAudioNode.getOutputNode();if(_g&&_g.gain)return'bg='+_g.gain.value.toFixed(4);return'bg=none';}}catch(e){}return'bg=n/a';};const _dbgWorkletState=()=>{try{if(window.BridgeAudioNode&&window.BridgeAudioNode.getDiag){const _d=window.BridgeAudioNode.getDiag();const _ws=_d.ws?' ov='+_d.ws.overruns+' un='+_d.ws.underruns+' ring='+_d.ws.ring:'';return'wl='+(_d.useWorklet?'y':'n')+' pre='+_d.preBufferLen+' ch='+_d.chunksLen+_ws;}}catch(e){}return'wl=n/a';};window.__dbgBridgeGain=_dbgBridgeGain;window.__dbgWorkletState=_dbgWorkletState;// V28: ding fix — midi bat ky (keyboard/pad/timeline) restore bridge gain sau +window.__dbgLog=_dbgLog;// M20 Bug B diag: log dB meter master OUT (masterBus.analyser — cùng nguồn +// outPeakText MASTERING PANEL, SAU chain FX) + fader gain state. Hist vòng ~8s +// @250ms (chỉ giữ RAM, không log cho tới khi có event); marker: load (initMasterBus), +// fx_click (nút FX master), panel_open + post 1/2/4/8s (MASTERING PANEL mở) → +// so dB trước/sau để tìm cơ chế Bug B (âm lượng giảm khi mở panel). +let _mdiagHist=[];let _mdiagTimer=null;let _mdiagBuf=null;function _mdiagDb(){try{const b=window.masterBus;if(!b||!b.analyser)return null;const an=b.analyser;const len=an.fftSize||2048;if(!_mdiagBuf||_mdiagBuf.lengthpk)pk=a;}return pk<0.00003?-120:20*Math.log10(pk);}catch(e){return null;}}function _mdiagFader(){try{const b=window.masterBus;if(!b)return'na';const f=x=>{try{const n=x&&x.gain?x.gain.value:null;return n===null?'na':n.toFixed(4);}catch(e){return'na';}};return'out='+f(b.output)+' mute='+f(b.muteGain)+' volL='+f(b.volLGain)+' volR='+f(b.volRGain);}catch(e){return'na';}}function _mdiagSample(tag){const db=_mdiagDb();try{_dbgLog('mdiag_'+tag,'db='+(db===null?'na':db.toFixed(1))+' '+_mdiagFader());}catch(e){}}function _mdiagStart(){if(_mdiagTimer)return;_mdiagTimer=setInterval(()=>{try{if(!window.masterBus)return;_mdiagHist.push(_mdiagDb());if(_mdiagHist.length>32)_mdiagHist.shift();// ~8s @250ms +}catch(e){}},250);}function _mdiagDump(){try{for(let i=0;i<_mdiagHist.length;i++){const v=_mdiagHist[i];_dbgLog('mdiag_hist','db='+(v===null?'na':v.toFixed(1)));}}catch(e){}}let _mdiagPostTimer=null;function _mdiagSchedulePost(){if(_mdiagPostTimer)clearTimeout(_mdiagPostTimer);const del=[1000,2000,4000,8000];let k=0;const tick=()=>{if(k>=del.length)return;_mdiagPostTimer=setTimeout(()=>{_mdiagSample('post'+del[k]);k++;tick();},del[k]);};tick();}if(window.PerformanceObserver){try{new PerformanceObserver(list=>{for(const e of list.getEntries())_dbgLog('longtask','d='+Math.round(e.duration)+' s='+Math.round(e.startTime));}).observe({entryTypes:['longtask']});}catch(e){}}const _dbgBridgeGain=()=>{try{if(window.BridgeAudioNode&&window.BridgeAudioNode.getOutputNode){const _g=window.BridgeAudioNode.getOutputNode();if(_g&&_g.gain)return'bg='+_g.gain.value.toFixed(4);return'bg=none';}}catch(e){}return'bg=n/a';};const _dbgWorkletState=()=>{try{if(window.BridgeAudioNode&&window.BridgeAudioNode.getDiag){const _d=window.BridgeAudioNode.getDiag();const _ws=_d.ws?' ov='+_d.ws.overruns+' un='+_d.ws.underruns+' ring='+_d.ws.ring:'';return'wl='+(_d.useWorklet?'y':'n')+' pre='+_d.preBufferLen+' ch='+_d.chunksLen+_ws;}}catch(e){}return'wl=n/a';};window.__dbgBridgeGain=_dbgBridgeGain;window.__dbgWorkletState=_dbgWorkletState;// V28: ding fix — midi bat ky (keyboard/pad/timeline) restore bridge gain sau // stop (gain giu ~0.0001 — V27 restore 800ms khong du cho VSTi release tail dai). window.__restorePendingFadeSession=restorePendingFadeSession;// V31: live note (ARM keyboard) — xoa backlog outQueue master FX truoc khi // note preview render (idle block ~341ms lam ARM lag ~0.3s). @@ -251,7 +257,7 @@ eqLowFilter.connect(eqMid1Filter);eqMid1Filter.connect(eqMid2Filter);eqMid2Filte // "state is bad" → CÂM + stuck. Mastering chain có comp/lim module riêng khi bật. masterBus.input.connect(masterBus.inputAnalyser);masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.outputAnalyser.connect(masterBus.masterFxDryGain);masterBus.masterFxDryGain.connect(masterBus.pdcDelay);// Wet bus: CHI nhan node fxRt (fxRtStart noi) — khong noi dry truc tiep vao // masterFxGain (no = dry + processed = play double khi PWR bat). -masterBus.masterFxGain.connect(masterBus.output);masterBus.muteGain.connect(masterBus.analyser);masterBus.muteGain.connect(masterBus.splitter);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;// Apply mastering once when the chain is first created (and on the +masterBus.masterFxGain.connect(masterBus.output);masterBus.muteGain.connect(masterBus.analyser);masterBus.muteGain.connect(masterBus.splitter);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;try{_mdiagStart();setTimeout(()=>_mdiagSample('load1'),1000);setTimeout(()=>_mdiagSample('load5'),5000);}catch(e){}// Apply mastering once when the chain is first created (and on the // masteringSettings effect for subsequent changes — see useEffect). if(window.currentMasteringSettings){try{// Reset sig-cache: chain MỚI (biquad/maximizer node mới) giữ giá trị // INIT (gain 0, width 100…) — applyMasteringSettings early-return vì @@ -409,7 +415,7 @@ if(fxRt.warm)fxRtStop(true);// Token chống race bypass: user bấm PWR/Bypass // node → bypass vô hiệu. Mỗi fxRtStop() bump startToken → mọi checkpoint // dưới đây thấy mismatch → hủy session vừa tạo, không nối node. const token=++fxRt.startToken;fxRt.starting=true;const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet||!masterBus||!masterBus.output||!masterBus.analyser||!masterBus.masterFxGain||!masterBus.masterFxDryGain)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT] start fail:',e);showToast('Không khởi động VST FX realtime: '+(e.message||e),'warning');fxRt.starting=false;return;}if(token!==fxRt.startToken){// Bypass/stop giữa lúc engine đang load chain — hủy session vừa tạo. -console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609070853`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong +console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609070903`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong // co listener IPv6 -> SYN hang ~2s) roi moi fallback 127.0.0.1. Dung // thang 127.0.0.1 (loopback) -> WS open ~5ms. Server chap nhan loopback. const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`;const ws=new WebSocket(wsUrl);fxRt.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{if(token!==fxRt.startToken){try{ws.close();}catch(e){}fxRt.starting=false;return;}fxRt.active=true;fxRt.flushing=false;fxRt.starting=false;fxRt.inQueue=[];// bỏ audio stale sản xuất trước khi WS open (~4s load VST) — hết lag khởi đầu @@ -589,7 +595,7 @@ const RefCompareModal=({open,onClose})=>{const[path,setPath]=React.useState(''); // roi play qua blob URL (Audio element khong gui duoc header). const token=localStorage.getItem('sonic_token')||'';const resp=await fetch((window.API_BASE_URL||'')+refStats.url,{headers:token?{'Authorization':'Bearer '+token}:{}});if(!resp.ok)throw new Error('HTTP '+resp.status);const blob=await resp.blob();const objUrl=URL.createObjectURL(blob);const au=new Audio(objUrl);au._objUrl=objUrl;audioRef.current=au;au.loop=true;au.play().then(()=>setAb('B')).catch(()=>{});}catch(e){window.showToast&&window.showToast('Play ref loi: '+(e.message||e),'error');}};const fmt=(v,suf)=>v===null||v===undefined?'--':v+suf;const dlt=(a,b)=>a===null||a===undefined||b===null||b===undefined?'--':(a-b>0?'+':'')+(a-b).toFixed(1);return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[115] bg-black/70 backdrop-blur-sm flex items-center justify-center p-4",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-md bg-slate-900 border border-slate-700 rounded-2xl p-4 space-y-3 shadow-2xl",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-white tracking-wider"},"A/B REFERENCE"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-500 hover:text-slate-300"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"flex gap-1.5"},/*#__PURE__*/React.createElement("input",{value:path,onChange:e=>setPath(e.target.value),placeholder:"Local path (media/browse) hoac file_id",className:"flex-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-300 focus:outline-none"}),/*#__PURE__*/React.createElement("button",{onClick:()=>path&&analyze({path}),disabled:busy,className:"px-2 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-xs font-bold"},busy?'...':'Analyze')),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-500 text-center"},"- hoac -"),/*#__PURE__*/React.createElement("button",{onClick:()=>fileInputRef.current&&fileInputRef.current.click(),className:"w-full py-1.5 bg-slate-800 hover:bg-slate-700 text-zinc-200 rounded text-xs font-bold border border-slate-700"},"Upload reference file"),/*#__PURE__*/React.createElement("input",{ref:fileInputRef,type:"file",accept:"audio/*,.wav,.mp3,.flac,.ogg,.aiff,.m4a,.aac,.opus",className:"hidden",onChange:e=>{const f=e.target.files&&e.target.files[0];onPickFile(f);e.target.value='';}}),(refStats||masterStats)&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-1.5 text-[10px] text-center"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded p-1.5"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold mb-1"},"METER"),/*#__PURE__*/React.createElement("div",{className:"text-emerald-400 font-bold"},"LUFS"),/*#__PURE__*/React.createElement("div",{className:"text-slate-300 font-bold"},"dBTP"),/*#__PURE__*/React.createElement("div",{className:"text-slate-400"},"Tone")),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded p-1.5"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold mb-1"},"MASTER"),/*#__PURE__*/React.createElement("div",{className:"text-cyan-300 font-bold"},fmt(masterStats&&masterStats.lufs,'')),/*#__PURE__*/React.createElement("div",{className:"text-cyan-300 font-bold"},fmt(masterStats&&masterStats.true_peak_db,'')),/*#__PURE__*/React.createElement("div",{className:"text-slate-400"},"-")),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded p-1.5"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold mb-1"},"REF"),/*#__PURE__*/React.createElement("div",{className:"text-fuchsia-300 font-bold"},fmt(refStats&&refStats.lufs,'')),/*#__PURE__*/React.createElement("div",{className:"text-fuchsia-300 font-bold"},fmt(refStats&&refStats.true_peak_db,'')),/*#__PURE__*/React.createElement("div",{className:"text-slate-400"},fmt(refStats&&refStats.centroid_hz,'Hz')))),refStats&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-400 text-center"},"Delta: LUFS ",dlt(masterStats&&masterStats.lufs,refStats.lufs)," / dBTP ",dlt(masterStats&&masterStats.true_peak_db,refStats.true_peak_db)," \xA0(+ = master louder)",/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},refStats.filename," \xB7 ",refStats.duration,"s \xB7 ",refStats.sample_rate,"Hz \xB7 ",refStats.channels,"ch")),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-1.5 pt-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>{stopRef();setAb('A');},className:`py-1.5 rounded text-[10px] font-bold border ${ab==='A'?'bg-cyan-600 border-cyan-500 text-white':'bg-slate-800 text-slate-300 border-slate-700'}`},"A \xB7 Master"),/*#__PURE__*/React.createElement("button",{onClick:playB,disabled:!refStats,className:`py-1.5 rounded text-[10px] font-bold border ${ab==='B'?'bg-fuchsia-600 border-fuchsia-500 text-white':'bg-slate-800 text-slate-300 border-slate-700'}`},"B \xB7 Reference"))));};// ── Master Strip Console Component (from md/47_MASTER_STRIP_CONSOLE.md) ── const MasterStripConsole=({masterVolume,setMasterVolume,masterVolL,setMasterVolL,masterVolR,setMasterVolR,masterLink,setMasterLink,masterMuted,setMasterMuted,masterAutoVol,setMasterAutoVol,masterAutoOn,setMasterAutoOn,showMasteringModal,setShowMasteringModal,masteringSettings,setMasteringSettings,isPlaying})=>{const[isTestPlaying,setIsTestPlaying]=React.useState(false);const[isMono,setIsMono]=React.useState(false);const[pan,setPan]=React.useState(0.0);const[panText,setPanText]=React.useState('center');const vuCanvasRef=React.useRef(null);const animFrameRef=React.useRef(null);const rmsValRef=React.useRef(null);const peakLRef=React.useRef(null);const peakRRef=React.useRef(null);const panPointerRef=React.useRef(null);const isMasterActive=masteringSettings?masteringSettings.masterConnected:false;const panStartYRef=React.useRef(0);const startPanValRef=React.useRef(0);const[refOpen,setRefOpen]=React.useState(false);const[autoOpen,setAutoOpen]=React.useState(false);const addAutoPoint=()=>{const pts=(masterAutoVol||[]).slice();pts.push({time:Math.round(pts.reduce((m,p)=>Math.max(m,p.time||0),0)+1),db:0});setMasterAutoVol(pts);};const updAutoPoint=(i,key,val)=>{const pts=(masterAutoVol||[]).slice();pts[i]=Object.assign({},pts[i],{[key]:val});setMasterAutoVol(pts);};const delAutoPoint=i=>setMasterAutoVol((masterAutoVol||[]).filter((p,j)=>j!==i));const ensureAudio=()=>{getAudioContext();};const handleFaderChangeL=val=>{val=Math.max(-60,Math.min(12,val));ensureAudio();setMasterVolL(val);if(masterLink)setMasterVolR(Math.max(-60,Math.min(12,val+(masterVolR-masterVolL))));};const handleFaderChangeR=val=>{val=Math.max(-60,Math.min(12,val));ensureAudio();setMasterVolR(val);if(masterLink)setMasterVolL(Math.max(-60,Math.min(12,val-(masterVolR-masterVolL))));};// Double-click reset: ca 2 ve 0 -const handleFaderChange=val=>{handleFaderChangeL(val);handleFaderChangeR(val);};React.useEffect(()=>{applyMasterVolumesTo(masterBus,masterVolL,masterVolR,masterMuted);},[masterVolL,masterVolR,masterMuted]);const masterVolDisplay=masterLink?masterVolL<=-50?'-inf':(masterVolL>0?'+':'')+masterVolL.toFixed(1):'L'+(masterVolL<=-50?'-inf':masterVolL.toFixed(1))+' R'+(masterVolR<=-50?'-inf':masterVolR.toFixed(1));const handlePanPointerDown=e=>{isPanDraggingRef.current=true;panStartYRef.current=e.clientY;startPanValRef.current=pan;e.currentTarget.setPointerCapture(e.pointerId);};const handlePanPointerMove=e=>{if(!isPanDraggingRef.current)return;const deltaY=panStartYRef.current-e.clientY;let newPan=startPanValRef.current+deltaY/80;newPan=Math.min(1.0,Math.max(-1.0,newPan));setPan(newPan);const angle=newPan*120;if(panPointerRef.current)panPointerRef.current.style.transform=`rotate(${angle}deg)`;if(newPan===0)setPanText('center');else if(newPan<0)setPanText('L'+Math.abs(Math.round(newPan*100)));else setPanText('R'+Math.round(newPan*100));};const handlePanPointerUp=e=>{isPanDraggingRef.current=false;try{e.currentTarget.releasePointerCapture(e.pointerId);}catch(err){}};React.useEffect(()=>{const canvas=vuCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');function render(){animFrameRef.current=requestAnimationFrame(render);canvas.width=canvas.clientWidth;canvas.height=canvas.clientHeight;const w=canvas.width;const h=canvas.height;ctx.clearRect(0,0,w,h);let levelL=0;let levelR=0;if(masterBus&&masterBus.leftAnalyser&&masterBus.rightAnalyser){const leftData=new Uint8Array(256);const rightData=new Uint8Array(256);masterBus.leftAnalyser.getByteTimeDomainData(leftData);masterBus.rightAnalyser.getByteTimeDomainData(rightData);let peakL=0;let peakR=0;for(let i=0;ipeakL)peakL=v;}for(let i=0;ipeakR)peakR=v;}levelL=peakL;levelR=peakR;}const padding=4;const gap=4;const barW=Math.max(4,(w-padding*2-gap)/2);const grad=ctx.createLinearGradient(0,h,0,0);grad.addColorStop(0,'#10b981');grad.addColorStop(0.7,'#f59e0b');grad.addColorStop(0.95,'#ef4444');ctx.fillStyle=grad;ctx.fillRect(padding,h-levelL*h,barW,levelL*h);ctx.fillRect(padding+barW+gap,h-levelR*h,barW,levelR*h);const maxLevel=Math.max(levelL,levelR);if(rmsValRef.current){rmsValRef.current.innerText=maxLevel>0?(20*Math.log10(maxLevel)-3.2).toFixed(1)+' dB':'-inf';}if(peakLRef.current){peakLRef.current.innerText=levelL>0?(20*Math.log10(levelL)).toFixed(1)+'dB':'-inf';}if(peakRRef.current){peakRRef.current.innerText=levelR>0?(20*Math.log10(levelR)).toFixed(1)+'dB':'-inf';}}render();return()=>{if(animFrameRef.current)cancelAnimationFrame(animFrameRef.current);};},[masterVolL,masterVolR,masterMuted,isMono]);return React.createElement(React.Fragment,null,React.createElement("div",{className:"flex flex-col items-stretch w-[300px] shrink-0 strip-bg rounded-lg p-2 text-slate-300 select-none shadow-2xl relative overflow-hidden"},React.createElement("div",{className:"flex flex-col items-center my-0.5"},React.createElement("span",{className:"text-[9px] text-slate-400 font-mono"},panText||'center'),React.createElement("div",{className:"flex items-center gap-1"},React.createElement("span",{className:"text-[8px] font-mono text-slate-500 w-5 text-right"},pan<0?'L'+Math.abs(Math.round(pan*100)):''),React.createElement("div",{id:"panDial",className:"w-6 h-6 rounded-full bg-slate-800 border-2 border-slate-600 relative flex items-center justify-center shadow-inner cursor-pointer",title:"Kéo chuột để chỉnh Pan (Left/Right)",onPointerDown:handlePanPointerDown,onPointerMove:handlePanPointerMove,onPointerUp:handlePanPointerUp,onWheel:function(e){e.preventDefault();var step=e.deltaY>0?-0.05:0.05;var newPan=Math.max(-1,Math.min(1,pan+step));newPan=Math.round(newPan*100)/100;setPan(newPan);if(panPointerRef.current)panPointerRef.current.style.transform='rotate('+newPan*120+'deg)';if(newPan===0)setPanText('center');else if(newPan<0)setPanText('L'+Math.abs(Math.round(newPan*100)));else setPanText('R'+Math.round(newPan*100));},onDoubleClick:function(){setPan(0);setPanText('center');if(panPointerRef.current)panPointerRef.current.style.transform='rotate(0deg)';}},React.createElement("div",{ref:panPointerRef,id:"panPointer",className:"w-0.5 h-2 bg-slate-200 rounded absolute top-0.5 transition-transform",style:{transform:'rotate('+pan*120+'deg)'}})),React.createElement("span",{className:"text-[8px] font-mono text-slate-500 w-5 text-left"},pan>0?'R'+Math.round(pan*100):''),React.createElement("span",{ref:React.createRef?null:null,className:"text-[10px] font-bold font-mono text-slate-200 ml-8",onDoubleClick:function(){handleFaderChange(0);}},masterVolDisplay))),React.createElement("div",{className:"flex gap-1 my-1 justify-between items-stretch min-h-0",style:{flex:'1 1 0%'}},React.createElement("div",{className:"flex-1 flex flex-col bg-black/80 p-1.5 rounded border border-slate-800/90 relative shadow-inner"},React.createElement("div",{className:"flex-1 flex items-stretch justify-between min-h-0"},React.createElement("div",{className:"flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pr-1 text-right border-r border-slate-800/60 overflow-hidden"},React.createElement("span",{className:"flex-1 flex items-center justify-end"},"+12"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"+6"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"0"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-6"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-12"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-18"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-24"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-30"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-36"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-42"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-54")),React.createElement("div",{className:"flex-1 relative bg-slate-950 rounded overflow-hidden mx-1.5 border border-slate-900"},React.createElement("canvas",{ref:vuCanvasRef,className:"w-[100px] h-full block"}),React.createElement("div",{className:"absolute bottom-0.5 inset-x-0 flex justify-around text-[7px] font-mono text-slate-500 font-bold pointer-events-none bg-black/60 py-0.5"},React.createElement("span",null,"L"),React.createElement("span",null,"R"))),React.createElement("div",{className:"flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pl-1 text-left border-l border-slate-800/60 overflow-hidden"},React.createElement("span",{className:"flex-1 flex items-center justify-start"},"+12"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"+6"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"0"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-6"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-12"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-18"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-24"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-30"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-36"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-42"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-54"))),React.createElement("div",{className:"flex justify-between text-[9px] font-mono text-slate-400 mt-0.5"},React.createElement("span",{ref:peakLRef},"-inf"),React.createElement("span",{ref:peakRRef},"-inf"))),React.createElement("div",{className:"w-36 flex items-stretch gap-0.5 bg-slate-900/60 p-1 rounded border border-slate-800"},React.createElement("div",{className:"flex-1 flex flex-col items-center justify-center relative fader-track rounded",onWheel:function(e){e.preventDefault();handleFaderChangeL(masterVolL+(e.deltaY>0?-0.5:0.5));}},React.createElement("div",{className:"w-0.5 h-full bg-slate-700 absolute left-1/2 -translate-x-1/2 top-0"}),React.createElement("input",{id:"masterFaderL",type:"range",min:"-60",max:"12",step:"0.5",value:masterVolL,className:"fader-slider w-full z-10",orient:"vertical",onChange:function(e){handleFaderChangeL(parseFloat(e.target.value));},onDoubleClick:function(){handleFaderChangeL(0);}}),React.createElement("span",{className:"text-[7px] font-mono text-slate-500 leading-none mt-0.5"},"L")),React.createElement("div",{className:"relative w-5 text-[7px] font-mono text-slate-500 select-none overflow-hidden"},React.createElement("span",{className:"absolute",style:{top:'0%',right:'2px'}},"+12"),React.createElement("span",{className:"absolute",style:{top:'8.3%',right:'2px'}},"+6"),React.createElement("span",{className:"absolute",style:{top:'16.7%',right:'2px'}},"0"),React.createElement("span",{className:"absolute",style:{top:'25%',right:'2px'}},"-6"),React.createElement("span",{className:"absolute",style:{top:'33.3%',right:'2px'}},"-12"),React.createElement("span",{className:"absolute",style:{top:'50%',right:'2px'}},"-24"),React.createElement("span",{className:"absolute",style:{top:'66.7%',right:'2px'}},"-36"),React.createElement("span",{className:"absolute",style:{top:'91.7%',right:'2px'}},"-54")),React.createElement("div",{className:"flex-1 flex flex-col items-center justify-center relative fader-track rounded",onWheel:function(e){e.preventDefault();handleFaderChangeR(masterVolR+(e.deltaY>0?-0.5:0.5));}},React.createElement("div",{className:"w-0.5 h-full bg-slate-700 absolute left-1/2 -translate-x-1/2 top-0"}),React.createElement("input",{id:"masterFaderR",type:"range",min:"-60",max:"12",step:"0.5",value:masterVolR,className:"fader-slider w-full z-10",orient:"vertical",onChange:function(e){handleFaderChangeR(parseFloat(e.target.value));},onDoubleClick:function(){handleFaderChangeR(0);}}),React.createElement("span",{className:"text-[7px] font-mono text-slate-500 leading-none mt-0.5"},"R"))),React.createElement("div",{className:"w-8 flex flex-col justify-between text-[10px] font-bold"},React.createElement("button",{id:"monoBtn",onClick:function(){setIsMono(function(p){return!p;});},className:"btn-daw h-[18px] rounded flex flex-col items-center justify-center text-[8px]"+(isMono?" btn-mono-active":""),title:"Mono Switch"},React.createElement("i",{className:"fa-solid fa-circle-half-stroke text-[9px]"}),React.createElement("span",null,"MONO")),React.createElement("button",{id:"muteBtn",onClick:function(){setMasterMuted(function(p){return!p;});},className:"btn-daw h-[18px] rounded text-amber-500 font-bold hover:text-amber-400"+(masterMuted?" btn-mute-active":""),title:"Mute Master Output (that: zero output)"},"M"),React.createElement("button",{id:"linkBtn",onClick:function(){setMasterLink(function(p){return!p;});},className:"btn-daw h-[18px] rounded text-xs font-bold"+(masterLink?" btn-teal-active":""),title:"LINK: keo L/R cung luc (giu delta)"},"LK"),React.createElement("button",{className:"btn-daw h-[18px] rounded text-slate-400 hover:text-slate-200",title:"Route Matrix"},React.createElement("i",{className:"fa-solid fa-diagram-project text-[9px]"})),React.createElement("button",{id:"fxBtn",className:"btn-daw h-[18px] rounded font-extrabold text-[10px] transition-all",onClick:function(){setShowMasteringModal(true);},title:"Mở MASTER FX CHAIN để chỉnh sửa"},"FX"),React.createElement("button",{id:"powerBtn",className:"btn-daw h-[18px] rounded text-xs transition-all"+(isMasterActive?" btn-teal-active":""),onClick:function(){if(setMasteringSettings){setMasteringSettings(function(prev){return Object.assign({},prev,{masterConnected:!prev.masterConnected,isBypassed:false});});}},title:"Bật/Tắt MASTER FX CHAIN Bypass"},[React.createElement("i",{className:"fa-solid fa-power-off"+(isMasterActive?" text-emerald-400":" text-slate-500"),key:"ico"}),React.createElement("span",{key:"lbl",className:"text-[7px] font-bold"+(isMasterActive?" text-emerald-300":" text-slate-400")},"PWR")]),React.createElement("button",{className:"btn-daw h-[18px] rounded text-slate-400 text-[8px]",title:"Trim Envelope"},"TRIM"),React.createElement("button",{className:"btn-daw h-[18px] rounded text-slate-400 text-[9px]",title:"Session Info"},React.createElement("i",{className:"fa-solid fa-info"})))),React.createElement("div",{className:"flex flex-col gap-0.5 my-1 shrink-0"},React.createElement("div",{className:"flex items-center justify-between gap-1"},React.createElement("button",{id:"automBtn",onClick:function(){setAutoOpen(function(p){return!p;});},className:"btn-daw rounded px-1 py-0.5 text-[8px] font-bold"+(autoOpen?" btn-teal-active":""),title:"Master Volume Automation (Gap 7)"},"AUTOM"),React.createElement("button",{onClick:function(){setMasterAutoOn(function(p){return!p;});},className:"text-[8px] px-1 py-0.5 rounded font-mono"+(masterAutoOn?" text-emerald-400 bg-emerald-500/10":" text-slate-500"),title:"Bat/tat master volume automation"},masterAutoOn?"ON":"OFF")),autoOpen&&React.createElement("div",{className:"flex flex-col gap-0.5 bg-slate-950 border border-slate-800 rounded p-1 max-h-32 overflow-y-auto"},(masterAutoVol||[]).map(function(pt,i){return React.createElement("div",{key:i,className:"flex items-center gap-1"},React.createElement("input",{type:"number",step:"0.1",min:"0",value:pt.time,onChange:function(e){updAutoPoint(i,'time',parseFloat(e.target.value)||0);},className:"w-12 bg-slate-900 border border-slate-700 rounded px-0.5 text-[8px] font-mono text-slate-200"}),React.createElement("span",{className:"text-[8px] text-slate-500"},"s"),React.createElement("input",{type:"number",step:"0.5",min:"-30",max:"3",value:pt.db,onChange:function(e){updAutoPoint(i,'db',parseFloat(e.target.value)||0);},className:"w-12 bg-slate-900 border border-slate-700 rounded px-0.5 text-[8px] font-mono text-slate-200"}),React.createElement("span",{className:"text-[8px] text-slate-500"},"dB"),React.createElement("button",{onClick:function(){delAutoPoint(i);},className:"text-[8px] text-red-400 hover:text-red-300 ml-auto"},"x"));}),React.createElement("button",{onClick:addAutoPoint,className:"text-[8px] text-cyan-400 hover:text-cyan-300 mt-0.5"},"+ ADD POINT"))),React.createElement("div",{className:"text-center text-[10px] font-bold font-mono text-slate-200 shrink-0"},masterVolDisplay),React.createElement("div",{className:"shrink-0 border-t border-slate-800 flex flex-col items-center"},React.createElement("div",{className:"flex justify-between w-full text-[9px] font-mono py-0.5"},React.createElement("span",{className:"text-emerald-400"},"RMS"),React.createElement("span",{ref:rmsValRef,className:"text-emerald-400 font-bold"},"-inf")),React.createElement("div",{className:"w-full text-center bg-black/80 py-0.5 rounded border border-slate-800 text-[10px] font-extrabold tracking-widest text-slate-100 uppercase"},React.createElement("span",null,"MAIN OUT")))),refOpen&&React.createElement(RefCompareModal,{open:refOpen,onClose:function(){setRefOpen(false);}}));};// ── Track Strip Console Component (from md/48_TRACK_STRIP_COMPONENT.md) ── +const handleFaderChange=val=>{handleFaderChangeL(val);handleFaderChangeR(val);};React.useEffect(()=>{applyMasterVolumesTo(masterBus,masterVolL,masterVolR,masterMuted);},[masterVolL,masterVolR,masterMuted]);const masterVolDisplay=masterLink?masterVolL<=-50?'-inf':(masterVolL>0?'+':'')+masterVolL.toFixed(1):'L'+(masterVolL<=-50?'-inf':masterVolL.toFixed(1))+' R'+(masterVolR<=-50?'-inf':masterVolR.toFixed(1));const handlePanPointerDown=e=>{isPanDraggingRef.current=true;panStartYRef.current=e.clientY;startPanValRef.current=pan;e.currentTarget.setPointerCapture(e.pointerId);};const handlePanPointerMove=e=>{if(!isPanDraggingRef.current)return;const deltaY=panStartYRef.current-e.clientY;let newPan=startPanValRef.current+deltaY/80;newPan=Math.min(1.0,Math.max(-1.0,newPan));setPan(newPan);const angle=newPan*120;if(panPointerRef.current)panPointerRef.current.style.transform=`rotate(${angle}deg)`;if(newPan===0)setPanText('center');else if(newPan<0)setPanText('L'+Math.abs(Math.round(newPan*100)));else setPanText('R'+Math.round(newPan*100));};const handlePanPointerUp=e=>{isPanDraggingRef.current=false;try{e.currentTarget.releasePointerCapture(e.pointerId);}catch(err){}};React.useEffect(()=>{const canvas=vuCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');function render(){animFrameRef.current=requestAnimationFrame(render);canvas.width=canvas.clientWidth;canvas.height=canvas.clientHeight;const w=canvas.width;const h=canvas.height;ctx.clearRect(0,0,w,h);let levelL=0;let levelR=0;if(masterBus&&masterBus.leftAnalyser&&masterBus.rightAnalyser){const leftData=new Uint8Array(256);const rightData=new Uint8Array(256);masterBus.leftAnalyser.getByteTimeDomainData(leftData);masterBus.rightAnalyser.getByteTimeDomainData(rightData);let peakL=0;let peakR=0;for(let i=0;ipeakL)peakL=v;}for(let i=0;ipeakR)peakR=v;}levelL=peakL;levelR=peakR;}const padding=4;const gap=4;const barW=Math.max(4,(w-padding*2-gap)/2);const grad=ctx.createLinearGradient(0,h,0,0);grad.addColorStop(0,'#10b981');grad.addColorStop(0.7,'#f59e0b');grad.addColorStop(0.95,'#ef4444');ctx.fillStyle=grad;ctx.fillRect(padding,h-levelL*h,barW,levelL*h);ctx.fillRect(padding+barW+gap,h-levelR*h,barW,levelR*h);const maxLevel=Math.max(levelL,levelR);if(rmsValRef.current){rmsValRef.current.innerText=maxLevel>0?(20*Math.log10(maxLevel)-3.2).toFixed(1)+' dB':'-inf';}if(peakLRef.current){peakLRef.current.innerText=levelL>0?(20*Math.log10(levelL)).toFixed(1)+'dB':'-inf';}if(peakRRef.current){peakRRef.current.innerText=levelR>0?(20*Math.log10(levelR)).toFixed(1)+'dB':'-inf';}}render();return()=>{if(animFrameRef.current)cancelAnimationFrame(animFrameRef.current);};},[masterVolL,masterVolR,masterMuted,isMono]);return React.createElement(React.Fragment,null,React.createElement("div",{className:"flex flex-col items-stretch w-[300px] shrink-0 strip-bg rounded-lg p-2 text-slate-300 select-none shadow-2xl relative overflow-hidden"},React.createElement("div",{className:"flex flex-col items-center my-0.5"},React.createElement("span",{className:"text-[9px] text-slate-400 font-mono"},panText||'center'),React.createElement("div",{className:"flex items-center gap-1"},React.createElement("span",{className:"text-[8px] font-mono text-slate-500 w-5 text-right"},pan<0?'L'+Math.abs(Math.round(pan*100)):''),React.createElement("div",{id:"panDial",className:"w-6 h-6 rounded-full bg-slate-800 border-2 border-slate-600 relative flex items-center justify-center shadow-inner cursor-pointer",title:"Kéo chuột để chỉnh Pan (Left/Right)",onPointerDown:handlePanPointerDown,onPointerMove:handlePanPointerMove,onPointerUp:handlePanPointerUp,onWheel:function(e){e.preventDefault();var step=e.deltaY>0?-0.05:0.05;var newPan=Math.max(-1,Math.min(1,pan+step));newPan=Math.round(newPan*100)/100;setPan(newPan);if(panPointerRef.current)panPointerRef.current.style.transform='rotate('+newPan*120+'deg)';if(newPan===0)setPanText('center');else if(newPan<0)setPanText('L'+Math.abs(Math.round(newPan*100)));else setPanText('R'+Math.round(newPan*100));},onDoubleClick:function(){setPan(0);setPanText('center');if(panPointerRef.current)panPointerRef.current.style.transform='rotate(0deg)';}},React.createElement("div",{ref:panPointerRef,id:"panPointer",className:"w-0.5 h-2 bg-slate-200 rounded absolute top-0.5 transition-transform",style:{transform:'rotate('+pan*120+'deg)'}})),React.createElement("span",{className:"text-[8px] font-mono text-slate-500 w-5 text-left"},pan>0?'R'+Math.round(pan*100):''),React.createElement("span",{ref:React.createRef?null:null,className:"text-[10px] font-bold font-mono text-slate-200 ml-8",onDoubleClick:function(){handleFaderChange(0);}},masterVolDisplay))),React.createElement("div",{className:"flex gap-1 my-1 justify-between items-stretch min-h-0",style:{flex:'1 1 0%'}},React.createElement("div",{className:"flex-1 flex flex-col bg-black/80 p-1.5 rounded border border-slate-800/90 relative shadow-inner"},React.createElement("div",{className:"flex-1 flex items-stretch justify-between min-h-0"},React.createElement("div",{className:"flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pr-1 text-right border-r border-slate-800/60 overflow-hidden"},React.createElement("span",{className:"flex-1 flex items-center justify-end"},"+12"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"+6"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"0"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-6"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-12"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-18"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-24"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-30"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-36"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-42"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-54")),React.createElement("div",{className:"flex-1 relative bg-slate-950 rounded overflow-hidden mx-1.5 border border-slate-900"},React.createElement("canvas",{ref:vuCanvasRef,className:"w-[100px] h-full block"}),React.createElement("div",{className:"absolute bottom-0.5 inset-x-0 flex justify-around text-[7px] font-mono text-slate-500 font-bold pointer-events-none bg-black/60 py-0.5"},React.createElement("span",null,"L"),React.createElement("span",null,"R"))),React.createElement("div",{className:"flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pl-1 text-left border-l border-slate-800/60 overflow-hidden"},React.createElement("span",{className:"flex-1 flex items-center justify-start"},"+12"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"+6"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"0"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-6"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-12"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-18"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-24"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-30"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-36"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-42"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-54"))),React.createElement("div",{className:"flex justify-between text-[9px] font-mono text-slate-400 mt-0.5"},React.createElement("span",{ref:peakLRef},"-inf"),React.createElement("span",{ref:peakRRef},"-inf"))),React.createElement("div",{className:"w-36 flex items-stretch gap-0.5 bg-slate-900/60 p-1 rounded border border-slate-800"},React.createElement("div",{className:"flex-1 flex flex-col items-center justify-center relative fader-track rounded",onWheel:function(e){e.preventDefault();handleFaderChangeL(masterVolL+(e.deltaY>0?-0.5:0.5));}},React.createElement("div",{className:"w-0.5 h-full bg-slate-700 absolute left-1/2 -translate-x-1/2 top-0"}),React.createElement("input",{id:"masterFaderL",type:"range",min:"-60",max:"12",step:"0.5",value:masterVolL,className:"fader-slider w-full z-10",orient:"vertical",onChange:function(e){handleFaderChangeL(parseFloat(e.target.value));},onDoubleClick:function(){handleFaderChangeL(0);}}),React.createElement("span",{className:"text-[7px] font-mono text-slate-500 leading-none mt-0.5"},"L")),React.createElement("div",{className:"relative w-5 text-[7px] font-mono text-slate-500 select-none overflow-hidden"},React.createElement("span",{className:"absolute",style:{top:'0%',right:'2px'}},"+12"),React.createElement("span",{className:"absolute",style:{top:'8.3%',right:'2px'}},"+6"),React.createElement("span",{className:"absolute",style:{top:'16.7%',right:'2px'}},"0"),React.createElement("span",{className:"absolute",style:{top:'25%',right:'2px'}},"-6"),React.createElement("span",{className:"absolute",style:{top:'33.3%',right:'2px'}},"-12"),React.createElement("span",{className:"absolute",style:{top:'50%',right:'2px'}},"-24"),React.createElement("span",{className:"absolute",style:{top:'66.7%',right:'2px'}},"-36"),React.createElement("span",{className:"absolute",style:{top:'91.7%',right:'2px'}},"-54")),React.createElement("div",{className:"flex-1 flex flex-col items-center justify-center relative fader-track rounded",onWheel:function(e){e.preventDefault();handleFaderChangeR(masterVolR+(e.deltaY>0?-0.5:0.5));}},React.createElement("div",{className:"w-0.5 h-full bg-slate-700 absolute left-1/2 -translate-x-1/2 top-0"}),React.createElement("input",{id:"masterFaderR",type:"range",min:"-60",max:"12",step:"0.5",value:masterVolR,className:"fader-slider w-full z-10",orient:"vertical",onChange:function(e){handleFaderChangeR(parseFloat(e.target.value));},onDoubleClick:function(){handleFaderChangeR(0);}}),React.createElement("span",{className:"text-[7px] font-mono text-slate-500 leading-none mt-0.5"},"R"))),React.createElement("div",{className:"w-8 flex flex-col justify-between text-[10px] font-bold"},React.createElement("button",{id:"monoBtn",onClick:function(){setIsMono(function(p){return!p;});},className:"btn-daw h-[18px] rounded flex flex-col items-center justify-center text-[8px]"+(isMono?" btn-mono-active":""),title:"Mono Switch"},React.createElement("i",{className:"fa-solid fa-circle-half-stroke text-[9px]"}),React.createElement("span",null,"MONO")),React.createElement("button",{id:"muteBtn",onClick:function(){setMasterMuted(function(p){return!p;});},className:"btn-daw h-[18px] rounded text-amber-500 font-bold hover:text-amber-400"+(masterMuted?" btn-mute-active":""),title:"Mute Master Output (that: zero output)"},"M"),React.createElement("button",{id:"linkBtn",onClick:function(){setMasterLink(function(p){return!p;});},className:"btn-daw h-[18px] rounded text-xs font-bold"+(masterLink?" btn-teal-active":""),title:"LINK: keo L/R cung luc (giu delta)"},"LK"),React.createElement("button",{className:"btn-daw h-[18px] rounded text-slate-400 hover:text-slate-200",title:"Route Matrix"},React.createElement("i",{className:"fa-solid fa-diagram-project text-[9px]"})),React.createElement("button",{id:"fxBtn",className:"btn-daw h-[18px] rounded font-extrabold text-[10px] transition-all",onClick:function(){try{_mdiagSample('fx_click');}catch(e){}setShowMasteringModal(true);},title:"Mở MASTER FX CHAIN để chỉnh sửa"},"FX"),React.createElement("button",{id:"powerBtn",className:"btn-daw h-[18px] rounded text-xs transition-all"+(isMasterActive?" btn-teal-active":""),onClick:function(){if(setMasteringSettings){setMasteringSettings(function(prev){return Object.assign({},prev,{masterConnected:!prev.masterConnected,isBypassed:false});});}},title:"Bật/Tắt MASTER FX CHAIN Bypass"},[React.createElement("i",{className:"fa-solid fa-power-off"+(isMasterActive?" text-emerald-400":" text-slate-500"),key:"ico"}),React.createElement("span",{key:"lbl",className:"text-[7px] font-bold"+(isMasterActive?" text-emerald-300":" text-slate-400")},"PWR")]),React.createElement("button",{className:"btn-daw h-[18px] rounded text-slate-400 text-[8px]",title:"Trim Envelope"},"TRIM"),React.createElement("button",{className:"btn-daw h-[18px] rounded text-slate-400 text-[9px]",title:"Session Info"},React.createElement("i",{className:"fa-solid fa-info"})))),React.createElement("div",{className:"flex flex-col gap-0.5 my-1 shrink-0"},React.createElement("div",{className:"flex items-center justify-between gap-1"},React.createElement("button",{id:"automBtn",onClick:function(){setAutoOpen(function(p){return!p;});},className:"btn-daw rounded px-1 py-0.5 text-[8px] font-bold"+(autoOpen?" btn-teal-active":""),title:"Master Volume Automation (Gap 7)"},"AUTOM"),React.createElement("button",{onClick:function(){setMasterAutoOn(function(p){return!p;});},className:"text-[8px] px-1 py-0.5 rounded font-mono"+(masterAutoOn?" text-emerald-400 bg-emerald-500/10":" text-slate-500"),title:"Bat/tat master volume automation"},masterAutoOn?"ON":"OFF")),autoOpen&&React.createElement("div",{className:"flex flex-col gap-0.5 bg-slate-950 border border-slate-800 rounded p-1 max-h-32 overflow-y-auto"},(masterAutoVol||[]).map(function(pt,i){return React.createElement("div",{key:i,className:"flex items-center gap-1"},React.createElement("input",{type:"number",step:"0.1",min:"0",value:pt.time,onChange:function(e){updAutoPoint(i,'time',parseFloat(e.target.value)||0);},className:"w-12 bg-slate-900 border border-slate-700 rounded px-0.5 text-[8px] font-mono text-slate-200"}),React.createElement("span",{className:"text-[8px] text-slate-500"},"s"),React.createElement("input",{type:"number",step:"0.5",min:"-30",max:"3",value:pt.db,onChange:function(e){updAutoPoint(i,'db',parseFloat(e.target.value)||0);},className:"w-12 bg-slate-900 border border-slate-700 rounded px-0.5 text-[8px] font-mono text-slate-200"}),React.createElement("span",{className:"text-[8px] text-slate-500"},"dB"),React.createElement("button",{onClick:function(){delAutoPoint(i);},className:"text-[8px] text-red-400 hover:text-red-300 ml-auto"},"x"));}),React.createElement("button",{onClick:addAutoPoint,className:"text-[8px] text-cyan-400 hover:text-cyan-300 mt-0.5"},"+ ADD POINT"))),React.createElement("div",{className:"text-center text-[10px] font-bold font-mono text-slate-200 shrink-0"},masterVolDisplay),React.createElement("div",{className:"shrink-0 border-t border-slate-800 flex flex-col items-center"},React.createElement("div",{className:"flex justify-between w-full text-[9px] font-mono py-0.5"},React.createElement("span",{className:"text-emerald-400"},"RMS"),React.createElement("span",{ref:rmsValRef,className:"text-emerald-400 font-bold"},"-inf")),React.createElement("div",{className:"w-full text-center bg-black/80 py-0.5 rounded border border-slate-800 text-[10px] font-extrabold tracking-widest text-slate-100 uppercase"},React.createElement("span",null,"MAIN OUT")))),refOpen&&React.createElement(RefCompareModal,{open:refOpen,onClose:function(){setRefOpen(false);}}));};// ── Track Strip Console Component (from md/48_TRACK_STRIP_COMPONENT.md) ── const TrackStripConsole=({track,index,onUpdateTrack,trackVuRefs,style})=>{var vol=track.volumeDb!=null?track.volumeDb:0;var trackColor=track.color||'#06b6d4';var isMuted=track.muted;var isSoloed=track.solo;var isBypassed=!!(track.audioBypass??track.masteringBypass);var isArmed=track.isArmed;var trackName=track.name||'Track '+(index+1);var isMicActive=track.inputSource?.deviceType==='MICROPHONE';const[pan,setPan]=React.useState(0.0);const[panLabel,setPanLabel]=React.useState('center');const[isPhaseInverted,setIsPhaseInverted]=React.useState(false);const panPointerRef=React.useRef(null);const setVuCanvas=React.useCallback(function(el){if(el)trackVuRefs.current[track.id+'_mixer']=el;else delete trackVuRefs.current[track.id+'_mixer'];},[track.id,trackVuRefs]);// Track FX chain editor (mastering_expand.md §II.4): reuse the same module // types as the mastering suite inside this track's FX chain. var FX_MODULE_TYPES=['compressor','limiter','exciter','rebalance','eq'];const updateFxChain=function(nextChain){if(onUpdateTrack)onUpdateTrack(track.id,{fxChain:nextChain});};const handlePanPointerDown=e=>{e.currentTarget._panStartY=e.clientY;e.currentTarget._startPan=pan;e.currentTarget.setPointerCapture(e.pointerId);function onMove(ev){if(!e.currentTarget)return;var deltaY=e.currentTarget._panStartY-ev.clientY;var newPan=Math.min(1.0,Math.max(-1.0,e.currentTarget._startPan+deltaY/80));setPan(newPan);var angle=newPan*120;if(panPointerRef.current)panPointerRef.current.style.transform='rotate('+angle+'deg)';if(newPan===0)setPanLabel('center');else if(newPan<0)setPanLabel('L'+Math.abs(Math.round(newPan*100)));else setPanLabel('R'+Math.round(newPan*100));}function onUp(){document.removeEventListener('pointermove',onMove);document.removeEventListener('pointerup',onUp);}document.addEventListener('pointermove',onMove);document.addEventListener('pointerup',onUp);};return React.createElement("div",{style:style||undefined,className:"flex flex-col items-stretch w-[145px] shrink-0 strip-bg-track rounded-md text-slate-300 select-none shadow-2xl border border-slate-900 overflow-y-auto"},/* 1. Top Track Color Accent Bar */React.createElement("div",{className:"h-1.5 w-full shrink-0 transition-colors",style:{backgroundColor:trackColor}}),/* 2. Pan Rotary Dial Area */React.createElement("div",{className:"h-[46px] shrink-0 py-1 px-2 flex flex-col items-center justify-center border-b border-slate-700/40",style:{backgroundColor:trackColor+'15'}},React.createElement("div",{className:"w-6 h-6 rounded-full bg-slate-800 border-2 border-slate-400 relative flex items-center justify-center cursor-pointer shadow-md",title:"Kéo chuột lên/xuống để chỉnh Pan",onPointerDown:handlePanPointerDown,onDoubleClick:function(){setPan(0);setPanLabel('center');if(panPointerRef.current)panPointerRef.current.style.transform='rotate(0deg)';},onWheel:function(e){e.preventDefault();var step=e.deltaY>0?-0.05:0.05;var newPan=Math.max(-1,Math.min(1,pan+step));newPan=Math.round(newPan*100)/100;setPan(newPan);if(panPointerRef.current)panPointerRef.current.style.transform='rotate('+newPan*120+'deg)';if(newPan===0)setPanLabel('center');else if(newPan<0)setPanLabel('L'+Math.abs(Math.round(newPan*100)));else setPanLabel('R'+Math.round(newPan*100));}},React.createElement("div",{ref:panPointerRef,className:"w-0.5 h-2 rounded absolute top-0.5 transition-transform",style:{backgroundColor:trackColor,transform:'rotate(0deg)'}})),React.createElement("span",{className:"text-[8px] font-mono mt-0.5 font-semibold",style:{color:trackColor}},panLabel)),/* 3. Center Area: Peak dB + Fader + VU + Button Stack */React.createElement("div",{className:"flex-1 p-1 flex gap-1 justify-between items-stretch min-h-[170px]"},/* Left Fader & VU Column */React.createElement("div",{className:"flex-1 flex flex-col justify-between items-center bg-black/60 p-1 rounded border border-slate-800/80"},React.createElement("div",{className:"w-full flex justify-center text-[8px] font-mono text-slate-400 h-4 items-center"},React.createElement("span",null,vol<=-50?'-inf':(vol>0?'+':'')+vol.toFixed(1)+'dB')),React.createElement("div",{className:"flex items-stretch justify-around w-full flex-1 relative py-1"},/* Fader Rail */React.createElement("div",{className:"relative fader-track-bg w-3 flex-1 rounded flex items-center justify-center overflow-hidden"},React.createElement("div",{className:"w-0.5 h-full bg-slate-700 absolute"}),React.createElement("input",{type:"range",min:"-60",max:"12",step:"0.5",value:vol,className:"fader-slider w-full z-10",onChange:function(e){var val=parseFloat(e.target.value);if(onUpdateTrack)onUpdateTrack(track.id,{volumeDb:val});},onWheel:function(e){e.preventDefault();var step=e.deltaY>0?-0.5:0.5;var newVol=Math.max(-60,Math.min(12,vol+step));if(onUpdateTrack)onUpdateTrack(track.id,{volumeDb:newVol});},onDoubleClick:function(){if(onUpdateTrack)onUpdateTrack(track.id,{volumeDb:0});}})),/* VU Meter */React.createElement("div",{className:"w-2.5 flex-1 bg-slate-950 rounded border border-slate-900 overflow-hidden relative",title:"Peak VU Meter"},React.createElement("canvas",{ref:setVuCanvas,className:"w-full h-full block"})))),/* Right Button Stack */React.createElement("div",{className:"w-7 flex flex-col justify-between text-[8px] font-bold shrink-0"},React.createElement("button",{onClick:function(e){e.stopPropagation();var next=!track.muted;if(onUpdateTrack)onUpdateTrack(track.id,{muted:next});if(window.__applyTrackMuteSolo)window.__applyTrackMuteSolo(track.id,{muted:next});},className:"btn-daw h-[24px] rounded text-amber-500 font-extrabold flex items-center justify-center"+(isMuted?" btn-mute-active":""),title:"Mute Track"},"M"),React.createElement("button",{onClick:function(e){e.stopPropagation();var next=!track.solo;if(onUpdateTrack)onUpdateTrack(track.id,{solo:next});if(window.__applyTrackMuteSolo)window.__applyTrackMuteSolo(track.id,{solo:next});},className:"btn-daw h-[24px] rounded text-yellow-400 font-extrabold flex items-center justify-center"+(isSoloed?" btn-solo-active":""),title:"Solo Track"},"S"),React.createElement("button",{onClick:function(e){e.stopPropagation();var next=!(track.audioBypass??track.masteringBypass);if(onUpdateTrack)onUpdateTrack(track.id,{audioBypass:next});// Live re-route: bypassed channel skips FX + mastering chain at Main out. @@ -1203,7 +1209,8 @@ try{window.SonicAPI&&window.SonicAPI.hideFxGui({path:g.path}).then(d=>{if(d&&d.p const _autoIsOpenRef=React.useRef(isOpen);_autoIsOpenRef.current=isOpen;const _autoOzRef=React.useRef(ozState);_autoOzRef.current=ozState;const _autoOpenRef=React.useRef({timer:null,attempt:0,inFlight:false});const _autoTryOpen=React.useCallback(async()=>{const st=_autoOzRef.current||{};const _p=st.gui_open_path;if(!_p)return;if(!_autoIsOpenRef.current||masterGuiOpenRef.current||_autoOpenRef.current.inFlight)return;const s=(st.vstFxChain||[]).find(x=>x&&x.path===_p);if(!s||!window.SonicAPI||!window.SonicAPI.openFxGui)return;_autoOpenRef.current.inFlight=true;setActiveMasterVstPath(_p);const _name=st.gui_open_name||s.name||_p;try{// C: GUI master luôn qua session realtime (PWR OFF vẫn warm) — bảo đảm // session trước khi spawn; nếu không backend từ chối (409) — fallback // embed đã ngưng cho master. -const _warm=await fxRtEnsureWarmForGui(_p);if(!_warm)throw new Error('chain chưa sẵn sàng (bật slot VST trong chain trước)');if(!_autoIsOpenRef.current||masterGuiOpenRef.current){_autoOpenRef.current.inFlight=false;return;}const r=await window.SonicAPI.openFxGui({path:_p,name:_name,embed:true,shm:fxRt.sessionId||'',preset_b64:_capturedPresetByPath[_p]||s.preset_b64||'',master:true});_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;}throw new Error(r&&r.detail||'Bridge không mở được GUI embed');}catch(err){_autoOpenRef.current.inFlight=false;console.warn('[MasterAutoOpen] openFxGui fail:',_p,err&&err.message||err);setActiveMasterVstPath(null);if(_autoIsOpenRef.current&&_autoOpenRef.current.attempt<4&&!masterGuiOpenRef.current){_autoOpenRef.current.attempt+=1;if(_autoOpenRef.current.timer)clearTimeout(_autoOpenRef.current.timer);_autoOpenRef.current.timer=setTimeout(()=>{_autoOpenRef.current.timer=null;_autoTryOpen();},3000);}else{_autoOpenRef.current.attempt=0;window.showToast&&window.showToast('Không tự mở được GUI VST: '+(err&&err.message||'lỗi')+' — bấm tên plugin trong chuỗi để mở tay.','error');}}},[]);React.useEffect(()=>{return()=>{if(_autoOpenRef.current.timer){clearTimeout(_autoOpenRef.current.timer);_autoOpenRef.current.timer=null;}};},[]);React.useEffect(()=>{if(!isOpen){_autoOpenRef.current.attempt=0;return;}if(_autoOpenRef.current.timer){clearTimeout(_autoOpenRef.current.timer);_autoOpenRef.current.timer=null;}_autoTryOpen();// eslint-disable-next-line react-hooks/exhaustive-deps +const _warm=await fxRtEnsureWarmForGui(_p);if(!_warm)throw new Error('chain chưa sẵn sàng (bật slot VST trong chain trước)');if(!_autoIsOpenRef.current||masterGuiOpenRef.current){_autoOpenRef.current.inFlight=false;return;}const r=await window.SonicAPI.openFxGui({path:_p,name:_name,embed:true,shm:fxRt.sessionId||'',preset_b64:_capturedPresetByPath[_p]||s.preset_b64||'',master:true});_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;}throw new Error(r&&r.detail||'Bridge không mở được GUI embed');}catch(err){_autoOpenRef.current.inFlight=false;console.warn('[MasterAutoOpen] openFxGui fail:',_p,err&&err.message||err);setActiveMasterVstPath(null);if(_autoIsOpenRef.current&&_autoOpenRef.current.attempt<4&&!masterGuiOpenRef.current){_autoOpenRef.current.attempt+=1;if(_autoOpenRef.current.timer)clearTimeout(_autoOpenRef.current.timer);_autoOpenRef.current.timer=setTimeout(()=>{_autoOpenRef.current.timer=null;_autoTryOpen();},3000);}else{_autoOpenRef.current.attempt=0;window.showToast&&window.showToast('Không tự mở được GUI VST: '+(err&&err.message||'lỗi')+' — bấm tên plugin trong chuỗi để mở tay.','error');}}},[]);React.useEffect(()=>{return()=>{if(_autoOpenRef.current.timer){clearTimeout(_autoOpenRef.current.timer);_autoOpenRef.current.timer=null;}};},[]);React.useEffect(()=>{if(!isOpen){_autoOpenRef.current.attempt=0;return;}// M20 Bug B diag: giá trị meter đúng lúc mở MASTERING PANEL + hist 8s trước + post 1/2/4/8s +try{_mdiagSample('panel_open');_mdiagDump();_mdiagSchedulePost();}catch(e){}if(_autoOpenRef.current.timer){clearTimeout(_autoOpenRef.current.timer);_autoOpenRef.current.timer=null;}_autoTryOpen();// eslint-disable-next-line react-hooks/exhaustive-deps },[isOpen,ozState.gui_open_path,ozState.gui_open_name,ozState.vstFxChain]);const setMasterVstChain=next=>setOzState(prev=>{const chain=prev.chain||[];const out=[];let vi=0;const mk=(s,i)=>({id:'mod_vst_'+Date.now()+'_'+i+'_'+Math.random().toString(36).slice(2,8),type:'vst3',path:s.path,name:s.name||'',preset_b64:s.preset_b64||'',bypass:!!s.bypass,active:true});chain.forEach(m=>{if(m.type==='vst3'){const s=next[vi];if(s){out.push(mk(s,vi));vi++;}}else out.push(m);});for(;vi{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609070853`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1 +const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609070903`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1 const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`);entry.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{entry.ready=true;entry.inQueue=[];// bỏ audio stale trước khi WS open — hết lag khởi đầu fxRtTrackSplice(trackId,path);try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}))));}catch(e){}};ws.onmessage=ev=>{if(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='flush_done'){entry.flushing=false;if(entry.flushTimer){clearTimeout(entry.flushTimer);entry.flushTimer=null;}if(entry.node)entry.node.port.postMessage({type:'flush_done'});}}catch(e){}return;}if(!(ev.data instanceof ArrayBuffer)||!entry.node)return;if(entry.flushing)return;// V24: output stale trong luc flush — drop const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;ifxRtTrackStop(trackId,path,true);ws.onerror=()=>{try{ws.close();}catch(e){}};entry.node.port.onmessage=ev=>{const d=ev.data;if(!d||!entry.ws)return;if(d.type==='flush'){// V24: nhu master — worklet underrun-resume → flush engine ring diff --git a/app/templates/index.html b/app/templates/index.html index ad85962..d75966e 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +