M23: fix Bug B master fader main-out moi luc + Bug C content lien tuc qua PWR toggle (giu PDC warm + rebase content wake)

This commit is contained in:
2026-09-07 11:25:45 +07:00
parent dc962c87cd
commit 821019324f
3 changed files with 137 additions and 25 deletions
+94 -9
View File
@@ -1405,6 +1405,9 @@ function initMasterBus(ctx) {
console.warn('initMasterBus apply mastering error:', e);
}
}
// M23 Bug B fix: bus vua tao/rebuild — ap lai master fader (state set tu luc
// project load khi bus chua ton tai).
try { if (typeof fxMasterVolApply === 'function') fxMasterVolApply(); } catch (e) {}
return masterBus;
}
@@ -1461,6 +1464,17 @@ function applyMasterVolumesTo(bus, volL, volR, muted, time) {
bus.muteGain.gain.setValueAtTime(muted ? 0 : 1, time);
}
}
// M23 Bug B fix: master fader state global + ap lai khi masterBus (re)create —
// volume L/R/MUTE phai LUON la MAIN OUT, khong chi khi Mixer panel mo.
window.__masterVolState = null;
function fxMasterVolApply() {
try {
const _st = window.__masterVolState;
if (!_st) return;
const _b = masterBus || window.masterBus;
if (_b && _b.volLGain) applyMasterVolumesTo(_b, _st.volL, _st.volR, !!_st.muted);
} catch (e) {}
}
let _lastMasteringActive = null;
let _lastMasteringSig = null;
@@ -1865,6 +1879,10 @@ function encodeFloatWavBlob(pcm, sampleRate, channels) {
// tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường
// chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out.
const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, lastInSent: 0, chainKey: null, chainStructKey: null, latencyTotal: 0, fadedIn: false, pdcCalibMs: 0, processedSamples: null, processedAnchor: null, warm: false };
// M23 Bug C fix: PWR OFF giua play — giu pdcDelay (content dry = content wet,
// khong nhay/scratch khi warm). Wake/stop ha co (__fxRtWakeRebase,
// stopAllPlayback, fxRtStop, fxRtStart chain-empty).
let fxRtWarmPlayHold = false;
// Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không
// teardown) → fxRtStart early-return → KHÔNG reload VST ~4s mỗi pass loop
// (hết bypass rồi mới load lại). Chỉ set TRƯỚC nhánh restart/resume; user
@@ -2041,7 +2059,11 @@ async function fxRtStart() {
// master native — KHÔNG teardown (giết editor + preset). Play đi dry path;
// PWR ON sau đó wake nối graph ở đây. Teardown khi vô chủ do settings
// effect (OP12) xử lý.
if (!chain.length) return;
if (!chain.length) {
fxRtWarmPlayHold = false;
try { fxRtApplyPdc(); } catch (e) {} // zero delay — play mới không FX
return;
}
const wToken = ++fxRt.startToken;
fxRt.starting = false;
fxRt.warm = false;
@@ -2103,7 +2125,11 @@ async function fxRtStart() {
return;
}
}
if (!chain.length) return;
if (!chain.length) {
fxRtWarmPlayHold = false;
try { fxRtApplyPdc(); } catch (e) {}
return;
}
// C: warm session còn nhưng không wake được (ws đóng/session mất) -> teardown
// trước cold-spawn (tránh 2 session song song).
if (fxRt.warm) fxRtStop(true);
@@ -2308,6 +2334,7 @@ function fxRtStop(silent) {
if (fxRt.flushTimer) { clearTimeout(fxRt.flushTimer); fxRt.flushTimer = null; }
fxRt.inQueue = [];
fxRt.flushing = false;
fxRtWarmPlayHold = false;
if (!fxRt.active && !fxRt.ws && !fxRt.node && !fxRt.sessionId) return;
let _fxStopNode = null;
try {
@@ -2383,7 +2410,9 @@ function fxRtWarmBypass() {
try { if (masterBus && masterBus.outputAnalyser) masterBus.outputAnalyser.disconnect(fxRt.node); } catch (e) {}
try { fxRt.node.disconnect(); } catch (e) {}
fxRt.fadedIn = false;
fxRtApplyPdc();
// M23 Bug C: OFF giua play — giu PDC (content lien tuc qua warm); zero
// delay chi khi stop han (stopAllPlayback clear hold + applyPdc).
if (!fxRtWarmPlayHold) fxRtApplyPdc();
}, 180);
} else if (fxRt.node) {
// Graph cũ (không crossfade gains) — hard switch + nối lại dry.
@@ -2392,7 +2421,7 @@ function fxRtWarmBypass() {
if (masterBus && masterBus.outputAnalyser && masterBus.output) {
try { masterBus.outputAnalyser.connect(masterBus.output); } catch (e) {}
}
fxRtApplyPdc();
if (!fxRtWarmPlayHold) fxRtApplyPdc();
}
} catch (e) {}
// Không null node/ws/onmessage/sessionId — wake cần node cũ để reconnect;
@@ -2570,7 +2599,11 @@ function fxRtApplyPdc() {
const ctx = getAudioContext();
const bus = masterBus;
if (!ctx || !bus || !bus.pdcDelay) return;
const l = (fxRt && fxRt.active) ? (FX_RT_ROUNDTRIP_SAMPLES + (fxRt.latencyTotal || 0)) : 0;
// M23 Bug C: bo +FX_RT_ROUNDTRIP_SAMPLES — latencyTotal (bridge 'latency'
// frame) DA gom round-trip that (log M21/M22: display active = wall - lat).
// +12032 double-count -> dry path delay > wet lag ~250ms -> crossfade lech pha
// (scratch). ponytail: neu engine pipeline doi RTT that -> do lai o day.
const l = (fxRt && fxRt.active) ? (fxRt.latencyTotal || 0) : 0;
try { bus.pdcDelay.delayTime.setTargetAtTime(l / (ctx.sampleRate || 44100), ctx.currentTime, 0.05); } catch (e) {}
}
@@ -19816,6 +19849,15 @@ const App = () => {
window.__masterAutoVol = masterAutoVol;
window.__masterAutoOn = masterAutoOn;
}, [masterAutoVol, masterAutoOn]);
// M23 Bug B fix: master fader ap NGAY moi luc (main out) — khong cho Mixer
// panel mount (MasterStripConsole). State global de initMasterBus ap lai.
React.useEffect(() => {
window.__masterVolState = { volL: masterVolL, volR: masterVolR, muted: !!masterMuted };
const _mb = masterBus || window.masterBus;
if (_mb && _mb.volLGain && (typeof audioCtx !== 'undefined' && audioCtx)) {
try { applyMasterVolumesTo(_mb, masterVolL, masterVolR, !!masterMuted); } catch (e) {}
}
}, [masterVolL, masterVolR, masterMuted]);
React.useEffect(() => {
if (isPlaying && typeof applyMasterAutomationSchedule === 'function') {
try { applyMasterAutomationSchedule(currentTimeRef.current || 0); } catch (e) { console.warn('[Auto] reschedule error:', e); }
@@ -20878,7 +20920,32 @@ const App = () => {
const _hasVst = !!(masteringSettings.gui_open_path && (masteringSettings.chain || []).some(x => x && x.type === 'vst3' && x.path === masteringSettings.gui_open_path && x.active !== false));
if (fxRt.active || fxRt.starting || fxRt.warm) {
if (_hasVst && fxRt.sessionId && (fxRt.active || fxRt.warm)) {
if (fxRt.active) fxRtWarmBypass();
if (fxRt.active) {
// M23 Bug C: PWR OFF giua play — giu PDC (dry = content, khong
// jump/scratch) + rebase display ve content de warm display lien
// tuc (khong nhay +lat nhu M22).
try {
const _plNow = !!(isPlayingRef && isPlayingRef.current) || !!(subTabsRef && subTabsRef.current && subTabsRef.current.some(s => s && s.isPlaying));
if (_plNow) {
fxRtWarmPlayHold = true;
const _c2 = getAudioContext();
const _sr2 = (_c2.sampleRate) || 44100;
const _off2 = startOffsetTimeRef.current || 0;
const _at2 = startAudioTimeRef.current || _c2.currentTime;
const _eng2 = (typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null)
? Math.max(_off2, _off2 + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0)) / _sr2)
: null;
const _wall2 = _off2 + (_c2.currentTime - _at2);
const _content2 = (_eng2 !== null) ? _eng2 : (_wall2 - (fxRt.latencyTotal || 0) / _sr2);
startOffsetTimeRef.current = _content2;
startAudioTimeRef.current = _c2.currentTime;
_dbgLog('ph_offhold', 'cur=' + (currentTimeRef.current == null ? 'na' : currentTimeRef.current.toFixed(3)) + ' wall=' + _wall2.toFixed(3) + ' content=' + _content2.toFixed(3) + ' off=' + startOffsetTimeRef.current.toFixed(3) + ' at=' + startAudioTimeRef.current.toFixed(3) + ' ctx=' + _c2.currentTime.toFixed(3) + ' lat=' + (fxRt.latencyTotal || 0) + ' hold=1');
} else {
_dbgLog('ph_offhold', 'cur=' + (currentTimeRef.current == null ? 'na' : currentTimeRef.current.toFixed(3)) + ' hold=0');
}
} catch (e) { try { _dbgLog('ph_offhold', 'err ' + String(e)); } catch (e2) {} }
fxRtWarmBypass();
}
} else {
fxRtStop();
}
@@ -21894,9 +21961,23 @@ const App = () => {
const _c = getAudioContext();
const _wall = (startOffsetTimeRef.current || 0) + (_c.currentTime - (startAudioTimeRef.current || _c.currentTime));
const _pa = (typeof fxRt.processedSamples === 'number') ? fxRt.processedSamples : null;
startOffsetTimeRef.current = _wall;
startAudioTimeRef.current = _c.currentTime;
if (_pa !== null) fxRt.processedAnchor = _pa;
if (fxRtWarmPlayHold) {
// M23 Bug C: wake tu OFF-giua-play — off/at DA rebase ve content tai OFF;
// rebase tiep toi content-hien-tai (off + elapsed-warm) roi anchor pa =
// ps - lat de engine-display noi lien content (het freeze ~0.37s + am
// resume tre 1/4 beat nhu M22). Ha co hold.
const _contentNow = (startOffsetTimeRef.current || 0) + (_c.currentTime - (startAudioTimeRef.current || _c.currentTime));
startOffsetTimeRef.current = _contentNow;
startAudioTimeRef.current = _c.currentTime;
if (_pa !== null) fxRt.processedAnchor = _pa - Math.round(fxRt.latencyTotal || 0);
fxRtWarmPlayHold = false;
} else {
// (M22) rebase wall — khi khong OFF-giua-play (warm tu stop), off/at la
// grid that nen giu hanh vi cu.
startOffsetTimeRef.current = _wall;
startAudioTimeRef.current = _c.currentTime;
if (_pa !== null) fxRt.processedAnchor = _pa;
}
if (_fxRtSeekFreeze) { _fxRtSeekFreeze = false; if (_fxRtSeekFreezeTimer) { clearTimeout(_fxRtSeekFreezeTimer); _fxRtSeekFreezeTimer = null; } }
try { _dbgLog('ph_rebase', 'wall=' + _wall.toFixed(3) + ' at=' + startAudioTimeRef.current.toFixed(3) + ' off=' + startOffsetTimeRef.current.toFixed(3) + ' ps=' + (typeof fxRt.processedSamples === 'number' ? fxRt.processedSamples : 'na') + ' pa=' + (typeof fxRt.processedAnchor === 'number' ? fxRt.processedAnchor : 'na') + ' lat=' + (fxRt.latencyTotal || 0)); } catch (e) {}
} catch (e) { try { _dbgLog('ph_rebase', 'err ' + String(e)); } catch (e2) {} }
@@ -27191,6 +27272,10 @@ const App = () => {
try { fxRtStop(); } catch (e) { console.warn('[FX RT] idle stop error:', e); }
}, FX_RT_WARM_IDLE_MS);
}
// M23 Bug C: dung han (fade) — ha hold + PDC ve 0 (am dang fade -> khong
// nghe jump; warm sau do dry delay = 0 nhu cu).
fxRtWarmPlayHold = false;
try { fxRtApplyPdc(); } catch (e) {}
}
_nativePlayEpoch++; // hủy mọi render/decode pending của lần play cũ
// V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer
+42 -15
View File
@@ -265,14 +265,18 @@ if(window.currentMasteringSettings){try{// Reset sig-cache: chain MỚI (biquad/
// INIT (gain 0, width 100…) — applyMasteringSettings early-return vì
// _lastMasteringSig không đổi (module-level, persist qua recreation) →
// chain FLAT ("spectrum hiển thị nhưng không xử lí âm thanh").
_lastMasteringSig=null;toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected,window.currentMasteringSettings.isBypassed);applyMasteringSettings(window.currentMasteringSettings);}catch(e){console.warn('initMasterBus apply mastering error:',e);}}return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}// Gap 7: master volume automation lane — schedule envelope tren master output
_lastMasteringSig=null;toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected,window.currentMasteringSettings.isBypassed);applyMasteringSettings(window.currentMasteringSettings);}catch(e){console.warn('initMasterBus apply mastering error:',e);}}// M23 Bug B fix: bus vua tao/rebuild — ap lai master fader (state set tu luc
// project load khi bus chua ton tai).
try{if(typeof fxMasterVolApply==='function')fxMasterVolApply();}catch(e){}return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}// Gap 7: master volume automation lane — schedule envelope tren master output
// gain (dB tuyen tinh, clamp -30..+3 nhu render_engine._apply_master_volume_automation).
// Trang thai tu React qua window.__masterAutoOn/__masterAutoVol (pattern
// window.__currentZoom). playheadSec = vi tri timeline giay luc bat dau nghe.
function applyMasterAutomationSchedule(playheadSec){const bus=masterBus;if(!bus||!bus.output||!bus.output.gain)return;const ctx=audioCtx||getAudioContext();const now=ctx.currentTime;const env=window.__masterAutoOn&&Array.isArray(window.__masterAutoVol)?window.__masterAutoVol.filter(p=>p&&typeof p.time==='number'&&typeof p.db==='number').slice().sort((a,b)=>a.time-b.time):[];bus.output.gain.cancelScheduledValues(now);if(!env.length){bus.output.gain.setValueAtTime(1,now);return;}const ph=playheadSec||0;let idx=0;while(idx<env.length&&env[idx].time<ph)idx++;let initDb;if(idx===0)initDb=env[0].db;else if(idx>=env.length)initDb=env[env.length-1].db;else{const a=env[idx-1],b=env[idx];const span=b.time-a.time||1;initDb=a.db+(b.db-a.db)*Math.max(0,Math.min(1,(ph-a.time)/span));}bus.output.gain.setValueAtTime(Math.pow(10,Math.max(-30,Math.min(3,initDb))/20),now);for(let k=idx;k<env.length;k++){const t=now+(env[k].time-ph);if(t>now)bus.output.gain.linearRampToValueAtTime(Math.pow(10,Math.max(-30,Math.min(3,env[k].db))/20),t);}}// Master L/R faders + LINK + MUTE - per-channel gain stage (output -> volSplit ->
// volLGain/volRGain -> volMerge -> muteGain -> analyser). time===undefined ->
// live (smoothing); co time -> offline (setValueAtTime). -50dB ~ -inf.
function applyMasterVolumesTo(bus,volL,volR,muted,time){if(!bus||!bus.volLGain||!bus.volRGain||!bus.muteGain)return;var gl=volL<=-50?0:Math.pow(10,volL/20);var gr=volR<=-50?0:Math.pow(10,volR/20);if(time===undefined){var now=audioCtx.currentTime;bus.volLGain.gain.setTargetAtTime(gl,now,0.01);bus.volRGain.gain.setTargetAtTime(gr,now,0.01);bus.muteGain.gain.setTargetAtTime(muted?0:1,now,0.01);}else{bus.volLGain.gain.setValueAtTime(gl,time);bus.volRGain.gain.setValueAtTime(gr,time);bus.muteGain.gain.setValueAtTime(muted?0:1,time);}}let _lastMasteringActive=null;let _lastMasteringSig=null;let _lastChainSig=null;function chainSignature(chainArray){return(chainArray||[]).map(m=>(m.type||'')+(m.active?'1':'0')).join(',');}// ── Track FX module factory (mastering_expand.md §II.4) ──
function applyMasterVolumesTo(bus,volL,volR,muted,time){if(!bus||!bus.volLGain||!bus.volRGain||!bus.muteGain)return;var gl=volL<=-50?0:Math.pow(10,volL/20);var gr=volR<=-50?0:Math.pow(10,volR/20);if(time===undefined){var now=audioCtx.currentTime;bus.volLGain.gain.setTargetAtTime(gl,now,0.01);bus.volRGain.gain.setTargetAtTime(gr,now,0.01);bus.muteGain.gain.setTargetAtTime(muted?0:1,now,0.01);}else{bus.volLGain.gain.setValueAtTime(gl,time);bus.volRGain.gain.setValueAtTime(gr,time);bus.muteGain.gain.setValueAtTime(muted?0:1,time);}}// M23 Bug B fix: master fader state global + ap lai khi masterBus (re)create —
// volume L/R/MUTE phai LUON la MAIN OUT, khong chi khi Mixer panel mo.
window.__masterVolState=null;function fxMasterVolApply(){try{const _st=window.__masterVolState;if(!_st)return;const _b=masterBus||window.masterBus;if(_b&&_b.volLGain)applyMasterVolumesTo(_b,_st.volL,_st.volR,!!_st.muted);}catch(e){}}let _lastMasteringActive=null;let _lastMasteringSig=null;let _lastChainSig=null;function chainSignature(chainArray){return(chainArray||[]).map(m=>(m.type||'')+(m.active?'1':'0')).join(',');}// ── Track FX module factory (mastering_expand.md §II.4) ──
// The SAME module DSP used in the mastering chain, instantiated per-track for
// the [FX] button on track strips. Returns { input, output, nodes, dispose }.
// ── Track FX module factory (mastering_expand.md §II.4 / unified_fx_rack_panel.md) ──
@@ -323,7 +327,10 @@ const len=Math.floor(pcm.length/channels);const bps=4;const dataBytes=len*bps*ch
// worklet → analyser (sau master fader, trước metering/destination — khớp thứ
// tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường
// chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out.
const fxRt={active:false,starting:false,startToken:0,sessionId:null,ws:null,node:null,inQueue:[],pumpTimer:null,lastInSent:0,chainKey:null,chainStructKey:null,latencyTotal:0,fadedIn:false,pdcCalibMs:0,processedSamples:null,processedAnchor:null,warm:false};// Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không
const fxRt={active:false,starting:false,startToken:0,sessionId:null,ws:null,node:null,inQueue:[],pumpTimer:null,lastInSent:0,chainKey:null,chainStructKey:null,latencyTotal:0,fadedIn:false,pdcCalibMs:0,processedSamples:null,processedAnchor:null,warm:false};// M23 Bug C fix: PWR OFF giua play — giu pdcDelay (content dry = content wet,
// khong nhay/scratch khi warm). Wake/stop ha co (__fxRtWakeRebase,
// stopAllPlayback, fxRtStop, fxRtStart chain-empty).
let fxRtWarmPlayHold=false;// Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không
// teardown) → fxRtStart early-return → KHÔNG reload VST ~4s mỗi pass loop
// (hết bypass rồi mới load lại). Chỉ set TRƯỚC nhánh restart/resume; user
// stop thật (fade=true) giữ master FX warm + idle timeout (V21); bridge-down /
@@ -389,7 +396,8 @@ if(fxRt.warm&&!fxRt.starting&&fxRt.sessionId&&fxRt.ws&&fxRt.ws.readyState===1){/
// master native — KHÔNG teardown (giết editor + preset). Play đi dry path;
// PWR ON sau đó wake nối graph ở đây. Teardown khi vô chủ do settings
// effect (OP12) xử lý.
if(!chain.length)return;const wToken=++fxRt.startToken;fxRt.starting=false;fxRt.warm=false;// Node/context/gains còn nguyên? Nếu masterBus rebuild (recovery/export) từ
if(!chain.length){fxRtWarmPlayHold=false;try{fxRtApplyPdc();}catch(e){}// zero delay — play mới không FX
return;}const wToken=++fxRt.startToken;fxRt.starting=false;fxRt.warm=false;// Node/context/gains còn nguyên? Nếu masterBus rebuild (recovery/export) từ
// lúc warm -> node thuộc graph cũ -> không nối được -> fallthrough cold start
// bên dưới. Edge hẹp: PWR OFF từ đầu + GUI mở + PWR ON chưa từng play ->
// warm-ensure chỉ tạo session (chưa node) -> editor native bị giết (session
@@ -409,7 +417,7 @@ try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(f
if(fxRt.ws.readyState===1){try{fxRt.ws.send(JSON.stringify({cmd:'flush'}));}catch(e){}}try{fxRtPushAllParams();}catch(e){}try{fxRt.node.port.postMessage({type:'clear_queue',mode:'play'});}catch(e){}fxRt.fadedIn=false;// port.onmessage hiện tại là closure của cold-start cũ — guard 'started'
// bằng token CŨ (≠ wToken) nên sẽ bỏ qua. Hook tạm bắt 'started' cho wake
// (crossfade + PDC đúng lúc), forward mọi message khác; restore sau 3s.
const _wOld=fxRt.node.port.onmessage;const _wHook=ev=>{try{if(ev.data&&ev.data.type==='started'&&wToken===fxRt.startToken){fxRt.fadedIn=true;fxRtCrossfade(true,wToken);setTimeout(()=>{if(wToken===fxRt.startToken)fxRtApplyPdc();},250);}}catch(e){}try{_wOld(ev);}catch(e){}};fxRt.node.port.onmessage=_wHook;setTimeout(()=>{try{if(fxRt.node&&fxRt.node.port.onmessage===_wHook)fxRt.node.port.onmessage=_wOld;}catch(e){}},3000);return;}}if(!chain.length)return;// C: warm session còn nhưng không wake được (ws đóng/session mất) -> teardown
const _wOld=fxRt.node.port.onmessage;const _wHook=ev=>{try{if(ev.data&&ev.data.type==='started'&&wToken===fxRt.startToken){fxRt.fadedIn=true;fxRtCrossfade(true,wToken);setTimeout(()=>{if(wToken===fxRt.startToken)fxRtApplyPdc();},250);}}catch(e){}try{_wOld(ev);}catch(e){}};fxRt.node.port.onmessage=_wHook;setTimeout(()=>{try{if(fxRt.node&&fxRt.node.port.onmessage===_wHook)fxRt.node.port.onmessage=_wOld;}catch(e){}},3000);return;}}if(!chain.length){fxRtWarmPlayHold=false;try{fxRtApplyPdc();}catch(e){}return;}// C: warm session còn nhưng không wake được (ws đóng/session mất) -> teardown
// trước cold-spawn (tránh 2 session song song).
if(fxRt.warm)fxRtStop(true);// Token chống race bypass: user bấm PWR/Bypass khi fxRtStart đang chờ
// fxRealtimeStart (~4s load VST). Lúc đó fxRt.active vẫn false → effect
@@ -417,7 +425,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=202609071038`);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=202609071125`);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
@@ -469,7 +477,7 @@ fxRtSendFrames(d);};// Pace input: tối đa 1 block/kỳ (~5.8ms). Gửi dồn
try{masterBus.outputAnalyser.disconnect(masterBus.output);}catch(e){}masterBus.outputAnalyser.connect(fxRt.node);fxRt.node.connect(masterBus.masterFxGain);fxRt.chainKey=ck;fxRt.chainStructKey=ckStruct;}catch(e){console.warn('[FX RT] setup fail:',e);fxRtStop(true);}}function fxRtStop(silent){if(fxRtWarmIdleTimer){clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=null;}// Phải bump token TRƯỚC guard early-return: lúc fxRtStart đang in-flight thì
// active/ws/node/sessionId đều chưa có → guard sẽ return, nhưng start vẫn
// phải bị hủy (token mismatch ở checkpoint).
fxRt.startToken++;fxRt.starting=false;if(fxRt.pumpTimer){clearInterval(fxRt.pumpTimer);fxRt.pumpTimer=null;}if(fxRt.flushTimer){clearTimeout(fxRt.flushTimer);fxRt.flushTimer=null;}fxRt.inQueue=[];fxRt.flushing=false;if(!fxRt.active&&!fxRt.ws&&!fxRt.node&&!fxRt.sessionId)return;let _fxStopNode=null;try{if(fxRt.node){_fxStopNode=fxRt.node;if(masterBus&&masterBus.masterFxGain&&masterBus.masterFxDryGain){// Crossfade deactivation: ramp dry 0->1 / fx 1->0 roi MOI disconnect
fxRt.startToken++;fxRt.starting=false;if(fxRt.pumpTimer){clearInterval(fxRt.pumpTimer);fxRt.pumpTimer=null;}if(fxRt.flushTimer){clearTimeout(fxRt.flushTimer);fxRt.flushTimer=null;}fxRt.inQueue=[];fxRt.flushing=false;fxRtWarmPlayHold=false;if(!fxRt.active&&!fxRt.ws&&!fxRt.node&&!fxRt.sessionId)return;let _fxStopNode=null;try{if(fxRt.node){_fxStopNode=fxRt.node;if(masterBus&&masterBus.masterFxGain&&masterBus.masterFxDryGain){// Crossfade deactivation: ramp dry 0->1 / fx 1->0 roi MOI disconnect
// (tranh click hard-switch nguoc). Dry path da noi san qua
// masterFxDryGain — khong can reconnect outputAnalyser->output. PDC giam
// ve 0 sau khi dry chiem cho (fx silent).
@@ -486,8 +494,10 @@ function fxRtWarmBypass(){if(fxRtWarmIdleTimer){clearTimeout(fxRtWarmIdleTimer);
// sớm khi dry chưa chiếm chỗ -> click. Giữ node/ws/session cho wake.
try{fxRtCrossfade(false);}catch(e){}setTimeout(()=>{// Guard toggle nhanh: PWR ON trong 180ms -> wake đã nối lại node —
// KHÔNG cắt (cắt ở đây làm đứt node đang chạy).
if(fxRt.active||fxRt.startToken!==_byToken)return;try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{fxRt.node.disconnect();}catch(e){}fxRt.fadedIn=false;fxRtApplyPdc();},180);}else if(fxRt.node){// Graph cũ (không crossfade gains) — hard switch + nối lại dry.
try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{fxRt.node.disconnect();}catch(e){}if(masterBus&&masterBus.outputAnalyser&&masterBus.output){try{masterBus.outputAnalyser.connect(masterBus.output);}catch(e){}}fxRtApplyPdc();}}catch(e){}// Không null node/ws/onmessage/sessionId — wake cần node cũ để reconnect;
if(fxRt.active||fxRt.startToken!==_byToken)return;try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{fxRt.node.disconnect();}catch(e){}fxRt.fadedIn=false;// M23 Bug C: OFF giua play — giu PDC (content lien tuc qua warm); zero
// delay chi khi stop han (stopAllPlayback clear hold + applyPdc).
if(!fxRtWarmPlayHold)fxRtApplyPdc();},180);}else if(fxRt.node){// Graph cũ (không crossfade gains) — hard switch + nối lại dry.
try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{fxRt.node.disconnect();}catch(e){}if(masterBus&&masterBus.outputAnalyser&&masterBus.output){try{masterBus.outputAnalyser.connect(masterBus.output);}catch(e){}}if(!fxRtWarmPlayHold)fxRtApplyPdc();}}catch(e){}// Không null node/ws/onmessage/sessionId — wake cần node cũ để reconnect;
// worklet rời graph (0 cạnh) -> không process -> không gửi frame -> engine
// idle an toàn (bridge vẫn sống cho GUI native).
fxRt.active=false;fxRt.warm=true;fxRt.latencyTotal=0;}// C: warm-ensure — tạo session realtime KHÔNG nối node (GUI native cần session
@@ -529,7 +539,11 @@ const FX_RT_ROUNDTRIP_SAMPLES=12032;const FX_RT_START_EST_MS=450;// G3: anchor p
// thay freeze/snap V40). Anchor moi play/wrap restart qua __fxRtPlayheadFreeze
// (moi play/resume path deu goi); cold start (chua active) -> null -> WS message
// dau tien anchor.
function fxRtAnchorProcessed(){const _phPrevAnchor=fxRt&&fxRt.processedAnchor;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'){fxRt.processedAnchor=fxRt.processedSamples;}else{fxRt.processedAnchor=null;}try{if(_phArmed())_dbgLog('ph_anchor','prevA='+(typeof _phPrevAnchor==='number'?_phPrevAnchor:'na')+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' act='+(fxRt&&fxRt.active?1:0));}catch(e){}}function fxRtApplyPdc(){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.pdcDelay)return;const l=fxRt&&fxRt.active?FX_RT_ROUNDTRIP_SAMPLES+(fxRt.latencyTotal||0):0;try{bus.pdcDelay.delayTime.setTargetAtTime(l/(ctx.sampleRate||44100),ctx.currentTime,0.05);}catch(e){}}// Crossfade master-FX switch: ramp masterFxGain/masterFxDryGain thay vi
function fxRtAnchorProcessed(){const _phPrevAnchor=fxRt&&fxRt.processedAnchor;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'){fxRt.processedAnchor=fxRt.processedSamples;}else{fxRt.processedAnchor=null;}try{if(_phArmed())_dbgLog('ph_anchor','prevA='+(typeof _phPrevAnchor==='number'?_phPrevAnchor:'na')+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' act='+(fxRt&&fxRt.active?1:0));}catch(e){}}function fxRtApplyPdc(){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.pdcDelay)return;// M23 Bug C: bo +FX_RT_ROUNDTRIP_SAMPLES — latencyTotal (bridge 'latency'
// frame) DA gom round-trip that (log M21/M22: display active = wall - lat).
// +12032 double-count -> dry path delay > wet lag ~250ms -> crossfade lech pha
// (scratch). ponytail: neu engine pipeline doi RTT that -> do lai o day.
const l=fxRt&&fxRt.active?fxRt.latencyTotal||0:0;try{bus.pdcDelay.delayTime.setTargetAtTime(l/(ctx.sampleRate||44100),ctx.currentTime,0.05);}catch(e){}}// Crossfade master-FX switch: ramp masterFxGain/masterFxDryGain thay vi
// hard-rewire (click). Hai gain nay duoc tao trong initMasterBus.
function fxRtCrossfade(toFx,token){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.masterFxGain||!bus.masterFxDryGain)return;if(token!==undefined&&token!==fxRt.startToken)return;const tc=0.025;try{bus.masterFxGain.gain.setTargetAtTime(toFx?1:0,ctx.currentTime,tc);bus.masterFxDryGain.gain.setTargetAtTime(toFx?0:1,ctx.currentTime,tc);}catch(e){}}// ── Realtime VST FX per-track (SHM bridge) — mọi luồng track qua bridge ─────
// Mỗi track: session riêng (1 WS/SHM) cho TỪNG đường chain có âm — audio-clips
@@ -1562,7 +1576,9 @@ const[masterLink,setMasterLink]=useState(true);// LINK L/R
const[masterMuted,setMasterMuted]=useState(false);// MUTE master
const[masterAutoVol,setMasterAutoVol]=useState([]);// Gap 7: master volume automation [{time s, db}]
const[masterAutoOn,setMasterAutoOn]=useState(false);// Gap 7: automation active
React.useEffect(()=>{window.__masterAutoVol=masterAutoVol;window.__masterAutoOn=masterAutoOn;},[masterAutoVol,masterAutoOn]);React.useEffect(()=>{if(isPlaying&&typeof applyMasterAutomationSchedule==='function'){try{applyMasterAutomationSchedule(currentTimeRef.current||0);}catch(e){console.warn('[Auto] reschedule error:',e);}}},[masterAutoVol,masterAutoOn,isPlaying]);const[masterVU,setMasterVU]=useState(0);// 0-1
React.useEffect(()=>{window.__masterAutoVol=masterAutoVol;window.__masterAutoOn=masterAutoOn;},[masterAutoVol,masterAutoOn]);// M23 Bug B fix: master fader ap NGAY moi luc (main out) — khong cho Mixer
// panel mount (MasterStripConsole). State global de initMasterBus ap lai.
React.useEffect(()=>{window.__masterVolState={volL:masterVolL,volR:masterVolR,muted:!!masterMuted};const _mb=masterBus||window.masterBus;if(_mb&&_mb.volLGain&&typeof audioCtx!=='undefined'&&audioCtx){try{applyMasterVolumesTo(_mb,masterVolL,masterVolR,!!masterMuted);}catch(e){}}},[masterVolL,masterVolR,masterMuted]);React.useEffect(()=>{if(isPlaying&&typeof applyMasterAutomationSchedule==='function'){try{applyMasterAutomationSchedule(currentTimeRef.current||0);}catch(e){console.warn('[Auto] reschedule error:',e);}}},[masterAutoVol,masterAutoOn,isPlaying]);const[masterVU,setMasterVU]=useState(0);// 0-1
const[masterMeterPeak,setMasterMeterPeak]=useState(0);const[rightSidebarWidth,setRightSidebarWidth]=useState(320);const[tcpWidth,setTcpWidth]=useState(320);const[mediaExplorerHeight,setMediaExplorerHeight]=useState(50);const[panelPositions,setPanelPositions]=useState({export:'bottom',ai:'right',python_tools:'bottom',selection:'bottom',media_explorer:'bottom',fx_rack:'bottom',midi_events:'bottom'});const[panelDropZone,setPanelDropZone]=useState(null);const[dragGhostPos,setDragGhostPos]=useState(null);const[dragGhostPanel,setDragGhostPanel]=useState(null);const panelDragRef=useRef(null);const trackVuRefs=useRef({});const workspaceRef=useRef(null);const colResizerRef=useRef(null);const rowResizerRef=useRef(null);const[aiConfig,setAiConfig]=useState({baseUrl:localStorage.getItem('ai_base_url')||`${API_BASE_URL}`,apiKey:localStorage.getItem('ai_api_key')||'',model:localStorage.getItem('ai_model')||'deepseek-chat'});const[aiProviders,setAiProviders]=useState([]);useEffect(()=>{(async()=>{try{const data=await window.SonicAPI.getAIConfigs();if(data&&data.providers){setAiProviders(data.providers);const active=data.providers.find(p=>p.is_active)||data.providers[0];if(active)setSelectedProviderId(active.id);}}catch(e){/* server may not have config endpoint */}})();},[]);const[analysisState,setAnalysisState]=useState({status:'Sẵn sàng. Chạy AI để phân tích nhịp.',data:null,isRunning:false});const[aiPrompt,setAiPrompt]=useState('');const[aiComposeMode,setAiComposeMode]=useState('SIMILAR_VARIATION');const[promptHistory,setPromptHistory]=useState([]);const[promptHistIdx,setPromptHistIdx]=useState(-1);const promptHistRef=useRef([]);const aiPromptUndoRef=useRef({stack:[],idx:-1,max:30});const aiPromptUndoPush=text=>{const u=aiPromptUndoRef.current;u.stack.push(text);if(u.stack.length>u.max)u.stack.shift();u.idx=u.stack.length-1;};const[aiProvider,setAiProvider]=useState('OpenAI');const[aiModel,setAiModel]=useState('GPT-4o');const[aiActionLog,setAiActionLog]=useState([]);const actionLogContainerRef=useRef(null);useEffect(()=>{if(actionLogContainerRef.current){actionLogContainerRef.current.scrollTop=actionLogContainerRef.current.scrollHeight;}},[aiActionLog]);const[aiProcessing,setAiProcessing]=useState(false);const[aiSuggestions,setAiSuggestions]=useState([]);const[showAiTypeahead,setShowAiTypeahead]=useState(false);const[showAISuggestions,setShowAISuggestions]=useState(true);const[showAIActionLog,setShowAIActionLog]=useState(false);const aiPromptMgrRef=useRef(null);const aiTypeaheadRef=useRef(null);const[selectedProviderId,setSelectedProviderId]=useState('');const[exportSettings,setExportSettings]=useState(()=>{let expDir='',remExpDir=false;try{expDir=localStorage.getItem('sonic_export_dir')||'';}catch(e){}try{remExpDir=localStorage.getItem('sonic_remember_export_dir')==='1';}catch(e){}return{sampleRate:'44100',bitDepth:'16',format:'wav',source:'project',quality:'44khz',channels:'stereo',loop:false,loopBars:4,dither:false,normalize:true,normalizeTarget:'-14lufs',monoCheck:true,realtime:false,// Gap 13: BWF/INFO metadata cho WAV mastering export
metaTitle:'',metaArtist:'',metaIsrc:'',// Export file + thư mục (ExportModal) — dir lưu localStorage nếu remember
fileName:'',exportDir:expDir,rememberExportDir:remExpDir};});// Persist export dir mặc định (chỉ khi user bật remember) — ExportModal
@@ -1697,7 +1713,10 @@ const _recNow=activeAudioRecordersRef&&activeAudioRecordersRef.current&&Object.k
const _guiVst=masteringSettings.gui_open_path&&(masteringSettings.chain||[]).some(x=>x&&x.type==='vst3'&&x.path===masteringSettings.gui_open_path&&x.active!==false)?masteringSettings.gui_open_path:null;if(!_guiVst){clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=setTimeout(()=>{fxRtWarmIdleTimer=null;try{fxRtStop();}catch(e){console.warn('[FX RT] idle stop error:',e);}},FX_RT_WARM_IDLE_MS);}else{clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=null;}}}else{// C: PWR OFF / bypass / chain rỗng. GUI marker còn + session sống ->
// giữ WARM (WebAudio bypass + PDC 0, session/node/ws sống cho GUI
// native); không marker -> teardown thật (hành vi cũ).
const _hasVst=!!(masteringSettings.gui_open_path&&(masteringSettings.chain||[]).some(x=>x&&x.type==='vst3'&&x.path===masteringSettings.gui_open_path&&x.active!==false));if(fxRt.active||fxRt.starting||fxRt.warm){if(_hasVst&&fxRt.sessionId&&(fxRt.active||fxRt.warm)){if(fxRt.active)fxRtWarmBypass();}else{fxRtStop();}}}}catch(e){console.warn('[FX RT] resync fail:',e);}// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
const _hasVst=!!(masteringSettings.gui_open_path&&(masteringSettings.chain||[]).some(x=>x&&x.type==='vst3'&&x.path===masteringSettings.gui_open_path&&x.active!==false));if(fxRt.active||fxRt.starting||fxRt.warm){if(_hasVst&&fxRt.sessionId&&(fxRt.active||fxRt.warm)){if(fxRt.active){// M23 Bug C: PWR OFF giua play — giu PDC (dry = content, khong
// jump/scratch) + rebase display ve content de warm display lien
// tuc (khong nhay +lat nhu M22).
try{const _plNow=!!(isPlayingRef&&isPlayingRef.current)||!!(subTabsRef&&subTabsRef.current&&subTabsRef.current.some(s=>s&&s.isPlaying));if(_plNow){fxRtWarmPlayHold=true;const _c2=getAudioContext();const _sr2=_c2.sampleRate||44100;const _off2=startOffsetTimeRef.current||0;const _at2=startAudioTimeRef.current||_c2.currentTime;const _eng2=typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null?Math.max(_off2,_off2+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))/_sr2):null;const _wall2=_off2+(_c2.currentTime-_at2);const _content2=_eng2!==null?_eng2:_wall2-(fxRt.latencyTotal||0)/_sr2;startOffsetTimeRef.current=_content2;startAudioTimeRef.current=_c2.currentTime;_dbgLog('ph_offhold','cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' wall='+_wall2.toFixed(3)+' content='+_content2.toFixed(3)+' off='+startOffsetTimeRef.current.toFixed(3)+' at='+startAudioTimeRef.current.toFixed(3)+' ctx='+_c2.currentTime.toFixed(3)+' lat='+(fxRt.latencyTotal||0)+' hold=1');}else{_dbgLog('ph_offhold','cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' hold=0');}}catch(e){try{_dbgLog('ph_offhold','err '+String(e));}catch(e2){}}fxRtWarmBypass();}}else{fxRtStop();}}}}catch(e){console.warn('[FX RT] resync fail:',e);}// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
// override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp
// ngay lên node đang phát (không chờ tracks effect).
try{const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(_n){if(_n.sfRouteGain&&_n.sfDryGain){const _b=effMidiBypass(_t);_n.sfRouteGain.gain.value=_b?0:1;_n.sfDryGain.gain.value=_b?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=routeBypassFor(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}}});// Sub-nodes (section tabs, key '<parent>_sub_<id>'): PWR master ON →
@@ -1813,7 +1832,13 @@ const mainResumeRef=useRef(null);currentTimeRef.current=currentTime;// M22 Bug C
// engine-derived nhảy LÙI về vị trí trước OFF (ph_jump d âm lớn). Rebase:
// dời startOffsetTimeRef/startAudioTimeRef về wall hiện tại + anchor=ps →
// engine clock nối lại đúng chỗ, playhead liên tục (lệch ~latencyTotal).
window.__fxRtWakeRebase=function(){try{if(!fxRt||!fxRt.active)return;const _c=getAudioContext();const _wall=(startOffsetTimeRef.current||0)+(_c.currentTime-(startAudioTimeRef.current||_c.currentTime));const _pa=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:null;startOffsetTimeRef.current=_wall;startAudioTimeRef.current=_c.currentTime;if(_pa!==null)fxRt.processedAnchor=_pa;if(_fxRtSeekFreeze){_fxRtSeekFreeze=false;if(_fxRtSeekFreezeTimer){clearTimeout(_fxRtSeekFreezeTimer);_fxRtSeekFreezeTimer=null;}}try{_dbgLog('ph_rebase','wall='+_wall.toFixed(3)+' at='+startAudioTimeRef.current.toFixed(3)+' off='+startOffsetTimeRef.current.toFixed(3)+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' lat='+(fxRt.latencyTotal||0));}catch(e){}}catch(e){try{_dbgLog('ph_rebase','err '+String(e));}catch(e2){}}};// M21 ph-diag hooks (Bug C — playhead nhay khi PWR master FX toggle)
window.__fxRtWakeRebase=function(){try{if(!fxRt||!fxRt.active)return;const _c=getAudioContext();const _wall=(startOffsetTimeRef.current||0)+(_c.currentTime-(startAudioTimeRef.current||_c.currentTime));const _pa=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:null;if(fxRtWarmPlayHold){// M23 Bug C: wake tu OFF-giua-play — off/at DA rebase ve content tai OFF;
// rebase tiep toi content-hien-tai (off + elapsed-warm) roi anchor pa =
// ps - lat de engine-display noi lien content (het freeze ~0.37s + am
// resume tre 1/4 beat nhu M22). Ha co hold.
const _contentNow=(startOffsetTimeRef.current||0)+(_c.currentTime-(startAudioTimeRef.current||_c.currentTime));startOffsetTimeRef.current=_contentNow;startAudioTimeRef.current=_c.currentTime;if(_pa!==null)fxRt.processedAnchor=_pa-Math.round(fxRt.latencyTotal||0);fxRtWarmPlayHold=false;}else{// (M22) rebase wall — khi khong OFF-giua-play (warm tu stop), off/at la
// grid that nen giu hanh vi cu.
startOffsetTimeRef.current=_wall;startAudioTimeRef.current=_c.currentTime;if(_pa!==null)fxRt.processedAnchor=_pa;}if(_fxRtSeekFreeze){_fxRtSeekFreeze=false;if(_fxRtSeekFreezeTimer){clearTimeout(_fxRtSeekFreezeTimer);_fxRtSeekFreezeTimer=null;}}try{_dbgLog('ph_rebase','wall='+_wall.toFixed(3)+' at='+startAudioTimeRef.current.toFixed(3)+' off='+startOffsetTimeRef.current.toFixed(3)+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' lat='+(fxRt.latencyTotal||0));}catch(e){}}catch(e){try{_dbgLog('ph_rebase','err '+String(e));}catch(e2){}}};// M21 ph-diag hooks (Bug C — playhead nhay khi PWR master FX toggle)
// __phArmPwr: user bam PWR OFF/ON -> arm cua so log 6s + snapshot ngay lap tuc.
// __phSnapAll: snapshot refs/fxRt tai diem goi (chi log khi cua so dang mo).
window.__phArmPwr=function(dir){try{_phDiagArmedUntil=Date.now()+6000;_dbgLog('ph_pwr','dir='+dir+' cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' off='+(startOffsetTimeRef.current==null?'na':startOffsetTimeRef.current.toFixed(3))+' at='+(startAudioTimeRef.current==null?'na':startAudioTimeRef.current.toFixed(3))+' ctx='+(getAudioContext().currentTime||0).toFixed(3)+' fx='+(fxRt?'act='+(fxRt.active?1:0)+' warm='+(fxRt.warm?1:0)+' start='+(fxRt.starting?1:0)+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' lat='+(fxRt.latencyTotal||0)+' pdc='+(fxRt.pdcCalibMs||0)+' sid='+(fxRt.sessionId||'na'):'na')+' freeze='+(_fxRtSeekFreeze?1:0)+' snap='+(_fxRtPlayheadSnapPending?1:0)+' wh='+(wrapHoldRef.current?wrapHoldRef.current.end.toFixed(3)+'/'+wrapHoldRef.current.v0.toFixed(3):'na'));}catch(e){try{_dbgLog('ph_pwr','err '+String(e));}catch(e2){}}};window.__phSnapAll=function(tag){try{if(!_phArmed())return;_dbgLog(tag,'cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' off='+(startOffsetTimeRef.current==null?'na':startOffsetTimeRef.current.toFixed(3))+' at='+(startAudioTimeRef.current==null?'na':startAudioTimeRef.current.toFixed(3))+' ctx='+(getAudioContext().currentTime||0).toFixed(3)+' fx='+(fxRt?'act='+(fxRt.active?1:0)+' warm='+(fxRt.warm?1:0)+' start='+(fxRt.starting?1:0)+' ps='+(typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na')+' pa='+(typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na')+' lat='+(fxRt.latencyTotal||0)+' pdc='+(fxRt.pdcCalibMs||0)+' sid='+(fxRt.sessionId||'na'):'na')+' freeze='+(_fxRtSeekFreeze?1:0)+' snap='+(_fxRtPlayheadSnapPending?1:0)+' wh='+(wrapHoldRef.current?wrapHoldRef.current.end.toFixed(3)+'/'+wrapHoldRef.current.v0.toFixed(3):'na'));}catch(e){try{_dbgLog(tag,'err '+String(e));}catch(e2){}}};// ── Keyboard Shortcuts ──
@@ -2199,7 +2224,7 @@ fxRtTrackReconcile(trackId,node,track);}catch(e){console.warn('rebuildTrackFxGra
// Cần activeTrackNodesRef/isPlayingRef. Session giống master (fxRtStart):
// fetch start → AudioWorkletNode 'sf-fx-realtime' → WS; WS open → splice
// worklet vào chain hiện tại (đọc mods MỚI NHẤT — an toàn với rebuild).
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=202609071038`);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=202609071125`);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;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}entry.node.port.postMessage({type:'out',l,r});};ws.onclose=()=>fxRtTrackStop(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
@@ -2490,7 +2515,9 @@ try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}}e
// C: GUI master native đang mở (marker + slot active trong chain) -> KHÔNG
// arm — session cần sống cho editor; teardown sẽ giết editor + mất preset.
// closeMasterGui xoá marker -> settings effect (OP12) arm lại khi dừng.
try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}const _stopGuiVst=masteringSettings&&masteringSettings.gui_open_path&&(masteringSettings.chain||[]).some(x=>x&&x.type==='vst3'&&x.path===masteringSettings.gui_open_path&&x.active!==false);clearTimeout(fxRtWarmIdleTimer);if(_stopGuiVst){fxRtWarmIdleTimer=null;}else{fxRtWarmIdleTimer=setTimeout(()=>{fxRtWarmIdleTimer=null;try{fxRtStop();}catch(e){console.warn('[FX RT] idle stop error:',e);}},FX_RT_WARM_IDLE_MS);}}_nativePlayEpoch++;// hủy mọi render/decode pending của lần play cũ
try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}const _stopGuiVst=masteringSettings&&masteringSettings.gui_open_path&&(masteringSettings.chain||[]).some(x=>x&&x.type==='vst3'&&x.path===masteringSettings.gui_open_path&&x.active!==false);clearTimeout(fxRtWarmIdleTimer);if(_stopGuiVst){fxRtWarmIdleTimer=null;}else{fxRtWarmIdleTimer=setTimeout(()=>{fxRtWarmIdleTimer=null;try{fxRtStop();}catch(e){console.warn('[FX RT] idle stop error:',e);}},FX_RT_WARM_IDLE_MS);}// M23 Bug C: dung han (fade) — ha hold + PDC ve 0 (am dang fade -> khong
// nghe jump; warm sau do dry delay = 0 nhu cu).
fxRtWarmPlayHold=false;try{fxRtApplyPdc();}catch(e){}}_nativePlayEpoch++;// hủy mọi render/decode pending của lần play cũ
// V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer
// cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref).
bridgeNoteEpochRef.current++;// D18e: seam ring-out - chup pitch dang keu (timelinePitchesRef) co NOTE_OFF
+1 -1
View File
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
<script src="/static/js/app.precompiled.js?v=202609070903202609070853202609070713202609071004202609071038" defer></script>
<script src="/static/js/app.precompiled.js?v=202609070903202609070853202609070713202609071004202609071038202609071125" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
<style>
:root {