M26: fix pdc=wet-lag FILL (10240) + sync display/anchor - het upbeat khi PWR ON giua play
- FX_RT_ROUNDTRIP_SAMPLES (12032) -> FX_RT_WET_LAG_SAMPLES (10240 = FILL 40 block) - fxRtApplyPdc + OFF-rebase + wake-rebase + bridge pos + playhead (main/sub/PR): tru latencyTotal (VST report 17536~365ms, content-latency that ~0) -> wet-lag - xoa diag M26b wake + M26 OFF (ph_on_*/ph_echo), giu ph_* logs guarded
This commit is contained in:
+20
-75
@@ -2149,29 +2149,6 @@ async function fxRtStart() {
|
||||
fxRt.inQueue = [];
|
||||
fxRt.outL = null;
|
||||
fxRt.outR = null;
|
||||
// M26b diag: PWR ON wake — snap pdcDelay/gains + hoi worklet state luc
|
||||
// wake, luc 'started' (wet bat dau) + 180/400/800ms sau: do wet-lag that,
|
||||
// delay node co ve 0 truoc crossfade khong. Bo diag o build fix.
|
||||
let _m26onWlArmed = false;
|
||||
const _snapOn = (tag, _sched) => {
|
||||
let _pd='na',_gd='na',_gw='na',_ps='na',_pa='na';
|
||||
const _b=masterBus, _c=getAudioContext();
|
||||
try { if(_b&&_b.pdcDelay) _pd=(_b.pdcDelay.delayTime.value*1000).toFixed(1); } catch(e){}
|
||||
try { if(_b&&_b.masterFxDryGain) _gd=_b.masterFxDryGain.gain.value.toFixed(4); } catch(e){}
|
||||
try { if(_b&&_b.masterFxGain) _gw=_b.masterFxGain.gain.value.toFixed(4); } catch(e){}
|
||||
try { _ps=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na'; } catch(e){}
|
||||
try { _pa=typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na'; } catch(e){}
|
||||
_dbgLog(tag,'pdcMs='+_pd+' gDry='+_gd+' gWet='+_gw+' lat='+(fxRt.latencyTotal||0)+' ps='+_ps+' pa='+_pa+' act='+(fxRt.active?1:0)+' warm='+(fxRt.warm?1:0)+' faded='+(fxRt.fadedIn?1:0)+' ctx='+(_c?_c.currentTime.toFixed(3):'na'));
|
||||
try { if (fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'dbg_state', k: tag }); } catch(e){}
|
||||
if (_sched && _sched.length) _sched.forEach((_ms,_ix)=>setTimeout(()=>_snapOn('ph_on'+( _ix+1)),_ms));
|
||||
};
|
||||
try { if (fxRt.node && fxRt.node.port && !_m26onWlArmed) {
|
||||
_m26onWlArmed = true;
|
||||
const _wl = (ev) => { try { const d = ev.data; if (d && d.type === 'dbg_state_r') _dbgLog('ph_on_wl','k='+d.k+' t='+d.t+' q='+d.q+' play='+d.playing+' hs='+d.hasStarted+' sil='+d.silenced+' fp='+d.flushPending+' stp='+d.stopped+' wm='+d.wrapMode+' wfb='+d.wrapFadeBlocks+' lat='+d.latencySamples+' rfill='+d.resumeFill+' inC='+d.inCount+' outPos='+d.outPos+' fadeL='+d.fadeLeft+' holdL='+d.holdFadeLeft); } catch(e){} };
|
||||
fxRt.node.port.addEventListener('message', _wl);
|
||||
setTimeout(()=>{ try { if (fxRt.node && fxRt.node.port) fxRt.node.port.removeEventListener('message', _wl); } catch(e){} }, 2600);
|
||||
} } catch(e){}
|
||||
_snapOn('ph_on_a', null);
|
||||
// M19 Bug A fix: wake PWR ON sau warm-bypass. Trước đây chỉ flush engine
|
||||
// ring + fadedIn=true NGAY — nhưng worklet lúc bypass rời graph không
|
||||
// process: outQueue còn ~FILL block audio CŨ + hasStarted=true (không gửi
|
||||
@@ -2192,7 +2169,6 @@ async function fxRtStart() {
|
||||
try {
|
||||
if (ev.data && ev.data.type === 'started' && wToken === fxRt.startToken) {
|
||||
fxRt.fadedIn = true;
|
||||
try { _snapOn('ph_on_s', [180, 400, 800]); } catch (e) {}
|
||||
fxRtCrossfade(true, wToken);
|
||||
setTimeout(() => { if (wToken === fxRt.startToken) fxRtApplyPdc(); }, 250);
|
||||
}
|
||||
@@ -2658,13 +2634,15 @@ async function fxRtEnsureWarmForGui(path) {
|
||||
// Master chain (worklet → bridge VST3) trễ ~2 block round-trip + latency
|
||||
// plugin. Track bypass mastering đi dry path (dryInput→dryOutput→output) KHÔNG
|
||||
// qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng
|
||||
// để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng — ponytail: đo RTT thật
|
||||
// (tag seq trên block) khi cần sample-accurate.
|
||||
// M18: FILL 40 (sf-fx-realtime.js V48) → RTT = batching 4 block (~7 block @48k
|
||||
// ≈ 1792) + FILL buffer 40 block (10240) = 47 block ≈ 12032 (~251ms @48k). Giữ
|
||||
// đồng bộ với sf-fx-realtime.js. FX_RT_START_EST_MS: ước lượng khởi đầu khi
|
||||
// chưa có phép đo (fx_play_first) — dùng cho freeze playhead thay V39 0.31s.
|
||||
const FX_RT_ROUNDTRIP_SAMPLES = 12032;
|
||||
// de align. M26: pdc = wet-lag FILL - content nghe = S(t-FILL); M26b do onset
|
||||
// +208-221ms = FILL 40 block (10240 @48k ~213ms) + RTT nho. KHONG tin VST
|
||||
// report (Ozone bao 17536 ~365ms nhung content-latency that ~0). ponytail:
|
||||
// neu plugin content-latency that L>0 -> pdc = FILL + L (display-lat giu FILL
|
||||
// vi processed da gom L).
|
||||
// Dong bo voi sf-fx-realtime.js: FILL 40 block (10240 @48k). FX_RT_START_EST_MS:
|
||||
// uoc luong khoi dau khi chua co phep do (fx_play_first) - dung cho freeze
|
||||
// playhead thay V39 0.31s.
|
||||
const FX_RT_WET_LAG_SAMPLES = 10240;
|
||||
const FX_RT_START_EST_MS = 450;
|
||||
// G3: anchor processed — playhead = startOffset + (processed - anchor - latency)/sr
|
||||
// (da tru latency -> bam vi tri mau NGHE DUOC; pre-roll tu giu tai startOffset —
|
||||
@@ -2687,11 +2665,10 @@ 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;
|
||||
// M26: pdc = wet-lag FILL 10240 (~213ms) - content nghe = S(t-FILL); VST
|
||||
// report (17536) la plugin buffer, content-latency that ~0 (M26b do onset).
|
||||
// ponytail: neu content-latency that L>0 -> FX_RT_WET_LAG_SAMPLES + L.
|
||||
const l = (fxRt && fxRt.active) ? FX_RT_WET_LAG_SAMPLES : 0;
|
||||
try { bus.pdcDelay.delayTime.setTargetAtTime(l / (ctx.sampleRate || 44100), ctx.currentTime, 0.05); } catch (e) {}
|
||||
}
|
||||
|
||||
@@ -21022,10 +20999,10 @@ const App = () => {
|
||||
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)
|
||||
? Math.max(_off2, _off2 + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES) / _sr2)
|
||||
: null;
|
||||
const _wall2 = _off2 + (_c2.currentTime - _at2);
|
||||
const _content2 = (_eng2 !== null) ? _eng2 : (_wall2 - (fxRt.latencyTotal || 0) / _sr2);
|
||||
const _content2 = (_eng2 !== null) ? _eng2 : (_wall2 - FX_RT_WET_LAG_SAMPLES / _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');
|
||||
@@ -21034,38 +21011,6 @@ const App = () => {
|
||||
}
|
||||
} catch (e) { try { _dbgLog('ph_offhold', 'err ' + String(e)); } catch (e2) {} }
|
||||
fxRtWarmBypass();
|
||||
// M26 diag: PWR OFF mid-play — snap pdcDelay/gains + hoi worklet
|
||||
// state tai t0/+180/+400/+800ms de do: pdc thuc te khi dry mo,
|
||||
// wet tail song bao lau, gains ramp ra sao. Bo diag o build fix.
|
||||
if (_plNow) {
|
||||
const _busB = masterBus;
|
||||
const _ctxB = getAudioContext();
|
||||
const _snapB = (k) => {
|
||||
let _pdB = 'na', _gdB = 'na', _gwB = 'na', _psB = 'na', _paB = 'na';
|
||||
try { if (_busB && _busB.pdcDelay) _pdB = (_busB.pdcDelay.delayTime.value * 1000).toFixed(1); } catch (e) {}
|
||||
try { if (_busB && _busB.masterFxDryGain) _gdB = _busB.masterFxDryGain.gain.value.toFixed(4); } catch (e) {}
|
||||
try { if (_busB && _busB.masterFxGain) _gwB = _busB.masterFxGain.gain.value.toFixed(4); } catch (e) {}
|
||||
try { _psB = typeof fxRt.processedSamples === 'number' ? fxRt.processedSamples : 'na'; } catch (e) {}
|
||||
try { _paB = typeof fxRt.processedAnchor === 'number' ? fxRt.processedAnchor : 'na'; } catch (e) {}
|
||||
const _oB = startOffsetTimeRef.current || 0;
|
||||
const _aB = startAudioTimeRef.current || 0;
|
||||
const _tB = _ctxB ? _ctxB.currentTime : 0;
|
||||
_dbgLog('ph_echo' + k, 'pdcMs=' + _pdB + ' gDry=' + _gdB + ' gWet=' + _gwB + ' lat=' + (fxRt.latencyTotal || 0) + ' ps=' + _psB + ' pa=' + _paB + ' cur=' + (currentTimeRef.current == null ? 'na' : currentTimeRef.current.toFixed(3)) + ' off=' + _oB.toFixed(3) + ' wall=' + (_oB + _tB - _aB).toFixed(3) + ' ctx=' + _tB.toFixed(3));
|
||||
try { if (fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'dbg_state', k: k }); } catch (e) {}
|
||||
};
|
||||
const _wlB = (ev) => {
|
||||
try {
|
||||
const d = ev.data;
|
||||
if (d && d.type === 'dbg_state_r') {
|
||||
_dbgLog('ph_echo_wl', 'k=' + d.k + ' t=' + d.t + ' q=' + d.q + ' play=' + d.playing + ' hs=' + d.hasStarted + ' sil=' + d.silenced + ' fp=' + d.flushPending + ' stp=' + d.stopped + ' wm=' + d.wrapMode + ' wfb=' + d.wrapFadeBlocks + ' lat=' + d.latencySamples + ' rfill=' + d.resumeFill + ' inC=' + d.inCount + ' outPos=' + d.outPos + ' fadeL=' + d.fadeLeft + ' holdL=' + d.holdFadeLeft);
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
try { if (fxRt.node && fxRt.node.port) fxRt.node.port.addEventListener('message', _wlB); } catch (e) {}
|
||||
_snapB(0);
|
||||
[180, 400, 800].forEach((_msB, _iB) => setTimeout(() => _snapB(_iB + 1), _msB));
|
||||
setTimeout(() => { try { if (fxRt.node && fxRt.node.port) fxRt.node.port.removeEventListener('message', _wlB); } catch (e) {} }, 1600);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fxRtStop();
|
||||
@@ -22090,7 +22035,7 @@ const App = () => {
|
||||
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);
|
||||
if (_pa !== null) fxRt.processedAnchor = _pa - FX_RT_WET_LAG_SAMPLES;
|
||||
fxRtWarmPlayHold = false;
|
||||
} else {
|
||||
// (M22) rebase wall — khi khong OFF-giua-play (warm tu stop), off/at la
|
||||
@@ -25268,7 +25213,7 @@ const App = () => {
|
||||
if (fxRt && fxRt.active && typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null) {
|
||||
_posSamples = Math.max(
|
||||
Math.floor(startOffsetTimeRef.current * (_pctx.sampleRate || 44100)),
|
||||
Math.floor(startOffsetTimeRef.current * (_pctx.sampleRate || 44100) + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0)))
|
||||
Math.floor(startOffsetTimeRef.current * (_pctx.sampleRate || 44100) + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES))
|
||||
);
|
||||
} else {
|
||||
const _elapsed = _pctx.currentTime - (startAudioTimeRef.current || _pctx.currentTime);
|
||||
@@ -25534,7 +25479,7 @@ const App = () => {
|
||||
const context = getAudioContext();
|
||||
let _ph = startOffsetTimeRef.current + (context.currentTime - startAudioTimeRef.current);
|
||||
if (fxRt && fxRt.active && typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null) {
|
||||
_ph = startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0)) / (context.sampleRate || 44100);
|
||||
_ph = startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES) / (context.sampleRate || 44100);
|
||||
}
|
||||
let _local = _ph;
|
||||
const _itemInfo = getPianoRollItemInfo(st);
|
||||
@@ -25562,7 +25507,7 @@ const App = () => {
|
||||
let wallTime = startOffsetTimeRef.current + elapsed;
|
||||
const bufferPos = startBufferOffsetRef.current + elapsed * speedFactor;
|
||||
if (fxRt && fxRt.active && typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null) {
|
||||
wallTime = startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0)) / (context.sampleRate || 44100);
|
||||
wallTime = startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES) / (context.sampleRate || 44100);
|
||||
}
|
||||
|
||||
// Loop sub-tab selection (bufferPos is buffer-time)
|
||||
@@ -25640,7 +25585,7 @@ const App = () => {
|
||||
const _playheadSec = (_fxRtSeekFreeze && fxRt && fxRt.active)
|
||||
? startOffsetTimeRef.current
|
||||
: ((fxRt && fxRt.active && typeof fxRt.processedSamples === 'number' && fxRt.processedAnchor != null)
|
||||
? Math.max(startOffsetTimeRef.current, startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - (fxRt.latencyTotal || 0)) / (context.sampleRate || 44100))
|
||||
? Math.max(startOffsetTimeRef.current, startOffsetTimeRef.current + (fxRt.processedSamples - fxRt.processedAnchor - FX_RT_WET_LAG_SAMPLES) / (context.sampleRate || 44100))
|
||||
: updatedTime);
|
||||
|
||||
// Selection Loop - LOOP_MAKER.md + LOOP_EDITOR_2.md §4.2
|
||||
|
||||
@@ -390,7 +390,7 @@ else if(data instanceof ArrayBuffer)this._worker.postMessage({kind:'send',buf:da
|
||||
// SecurityError -> fxRtStart chet -> master FX khong hoat dong (M24 dung
|
||||
// new WebSocket, khong bi same-origin). Fetch text qua engine (CORS * da bat)
|
||||
// -> Blob URL same-origin -> worker chay duoc ca Tauri lan http dev.
|
||||
try{if(!fxRtWorkerUrl){const _r=await fetch(`${API_BASE_URL}/static/js/fx-rt-worker.js?v=202609071745`,{cache:'no-store'});if(!_r.ok)throw new Error('fx-rt-worker load '+_r.status);fxRtWorkerUrl=URL.createObjectURL(new Blob([await _r.text()],{type:'text/javascript'}));}shim._worker=new Worker(fxRtWorkerUrl);}catch(e){throw e;}// giong ws constructor that -> caller catch cu giu nguyen
|
||||
try{if(!fxRtWorkerUrl){const _r=await fetch(`${API_BASE_URL}/static/js/fx-rt-worker.js?v=202609071819`,{cache:'no-store'});if(!_r.ok)throw new Error('fx-rt-worker load '+_r.status);fxRtWorkerUrl=URL.createObjectURL(new Blob([await _r.text()],{type:'text/javascript'}));}shim._worker=new Worker(fxRtWorkerUrl);}catch(e){throw e;}// giong ws constructor that -> caller catch cu giu nguyen
|
||||
shim._worker.onmessage=ev=>{const d=ev.data;if(!d||shim._closed)return;if(d.kind==='open'){shim.readyState=1;if(shim.onopen){try{shim.onopen();}catch(e){}}return;}if(d.kind==='close'){_fireClose(shim);return;}if(d.kind==='text'){if(shim.onmessage){try{shim.onmessage({data:d.text});}catch(e){}}}};shim._worker.onerror=()=>_fireClose(shim);try{shim._worker.postMessage({kind:'init',wsUrl,port:directPort||undefined},directPort?[directPort]:[]);}catch(e){}return shim;}async function fxRtStart(){// V21: mọi play/chain-change gọi đây — hủy idle timer của lần stop trước
|
||||
// (không để session warm bị teardown giữa lúc đang chơi).
|
||||
if(fxRtWarmIdleTimer){clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=null;}const chain=fxRtChain();const ck=fxRtChainKey(chain);const ckStruct=fxRtChainStructKey(chain);if(fxRt.active){// masterBus bị rebuild (recovery/export) → node thuộc graph cũ → restart.
|
||||
@@ -419,10 +419,7 @@ return;}const wToken=++fxRt.startToken;fxRt.starting=false;fxRt.warm=false;// No
|
||||
const _wNodeOk=!!(fxRt.node&&fxRt.node.context===getAudioContext()&&masterBus&&masterBus.outputAnalyser&&masterBus.masterFxGain);if(!_wNodeOk){fxRtStop(true);// fallthrough -> cold start phía dưới (fxRtStop đã bump token)
|
||||
}else{if(fxRt.chainStructKey!==ckStruct){await fxRtSetChain(chain);if(wToken!==fxRt.startToken)return;fxRt.chainStructKey=ckStruct;}fxRt.chainKey=ck;// Reconnect edges: disconnect-then-connect — toggle nhanh <180ms bypass
|
||||
// timer chưa kịp cắt -> disconnect ở đây tránh duplicate edge.
|
||||
try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.connect(fxRt.node);}catch(e){}try{if(fxRt.node)fxRt.node.disconnect(masterBus.masterFxGain);}catch(e){}try{if(fxRt.node)fxRt.node.connect(masterBus.masterFxGain);}catch(e){}fxRt.active=true;fxRt.inQueue=[];fxRt.outL=null;fxRt.outR=null;// M26b diag: PWR ON wake — snap pdcDelay/gains + hoi worklet state luc
|
||||
// wake, luc 'started' (wet bat dau) + 180/400/800ms sau: do wet-lag that,
|
||||
// delay node co ve 0 truoc crossfade khong. Bo diag o build fix.
|
||||
let _m26onWlArmed=false;const _snapOn=(tag,_sched)=>{let _pd='na',_gd='na',_gw='na',_ps='na',_pa='na';const _b=masterBus,_c=getAudioContext();try{if(_b&&_b.pdcDelay)_pd=(_b.pdcDelay.delayTime.value*1000).toFixed(1);}catch(e){}try{if(_b&&_b.masterFxDryGain)_gd=_b.masterFxDryGain.gain.value.toFixed(4);}catch(e){}try{if(_b&&_b.masterFxGain)_gw=_b.masterFxGain.gain.value.toFixed(4);}catch(e){}try{_ps=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na';}catch(e){}try{_pa=typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na';}catch(e){}_dbgLog(tag,'pdcMs='+_pd+' gDry='+_gd+' gWet='+_gw+' lat='+(fxRt.latencyTotal||0)+' ps='+_ps+' pa='+_pa+' act='+(fxRt.active?1:0)+' warm='+(fxRt.warm?1:0)+' faded='+(fxRt.fadedIn?1:0)+' ctx='+(_c?_c.currentTime.toFixed(3):'na'));try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'dbg_state',k:tag});}catch(e){}if(_sched&&_sched.length)_sched.forEach((_ms,_ix)=>setTimeout(()=>_snapOn('ph_on'+(_ix+1)),_ms));};try{if(fxRt.node&&fxRt.node.port&&!_m26onWlArmed){_m26onWlArmed=true;const _wl=ev=>{try{const d=ev.data;if(d&&d.type==='dbg_state_r')_dbgLog('ph_on_wl','k='+d.k+' t='+d.t+' q='+d.q+' play='+d.playing+' hs='+d.hasStarted+' sil='+d.silenced+' fp='+d.flushPending+' stp='+d.stopped+' wm='+d.wrapMode+' wfb='+d.wrapFadeBlocks+' lat='+d.latencySamples+' rfill='+d.resumeFill+' inC='+d.inCount+' outPos='+d.outPos+' fadeL='+d.fadeLeft+' holdL='+d.holdFadeLeft);}catch(e){}};fxRt.node.port.addEventListener('message',_wl);setTimeout(()=>{try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.removeEventListener('message',_wl);}catch(e){}},2600);}}catch(e){}_snapOn('ph_on_a',null);// M19 Bug A fix: wake PWR ON sau warm-bypass. Trước đây chỉ flush engine
|
||||
try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.disconnect(fxRt.node);}catch(e){}try{if(masterBus&&masterBus.outputAnalyser)masterBus.outputAnalyser.connect(fxRt.node);}catch(e){}try{if(fxRt.node)fxRt.node.disconnect(masterBus.masterFxGain);}catch(e){}try{if(fxRt.node)fxRt.node.connect(masterBus.masterFxGain);}catch(e){}fxRt.active=true;fxRt.inQueue=[];fxRt.outL=null;fxRt.outR=null;// M19 Bug A fix: wake PWR ON sau warm-bypass. Trước đây chỉ flush engine
|
||||
// ring + fadedIn=true NGAY — nhưng worklet lúc bypass rời graph không
|
||||
// process: outQueue còn ~FILL block audio CŨ + hasStarted=true (không gửi
|
||||
// 'started' lại) → reconnect xong phát stale block ngay + crossfade dry→fx
|
||||
@@ -433,7 +430,7 @@ let _m26onWlArmed=false;const _snapOn=(tag,_sched)=>{let _pd='na',_gd='na',_gw='
|
||||
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;try{_snapOn('ph_on_s',[180,400,800]);}catch(e){}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
|
||||
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
|
||||
@@ -441,7 +438,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=202609071745`);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=202609071819`);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):''}`;// M25: WS chay tren Web Worker - binary output/input di THANG
|
||||
@@ -552,22 +549,23 @@ for(let _a=0;_a<3;_a++){const sid=await fxRtWarmEnsure(want);if(sid&&fxRt.sessio
|
||||
// Master chain (worklet → bridge VST3) trễ ~2 block round-trip + latency
|
||||
// plugin. Track bypass mastering đi dry path (dryInput→dryOutput→output) KHÔNG
|
||||
// qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng
|
||||
// để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng — ponytail: đo RTT thật
|
||||
// (tag seq trên block) khi cần sample-accurate.
|
||||
// M18: FILL 40 (sf-fx-realtime.js V48) → RTT = batching 4 block (~7 block @48k
|
||||
// ≈ 1792) + FILL buffer 40 block (10240) = 47 block ≈ 12032 (~251ms @48k). Giữ
|
||||
// đồng bộ với sf-fx-realtime.js. FX_RT_START_EST_MS: ước lượng khởi đầu khi
|
||||
// chưa có phép đo (fx_play_first) — dùng cho freeze playhead thay V39 0.31s.
|
||||
const FX_RT_ROUNDTRIP_SAMPLES=12032;const FX_RT_START_EST_MS=450;// G3: anchor processed — playhead = startOffset + (processed - anchor - latency)/sr
|
||||
// de align. M26: pdc = wet-lag FILL - content nghe = S(t-FILL); M26b do onset
|
||||
// +208-221ms = FILL 40 block (10240 @48k ~213ms) + RTT nho. KHONG tin VST
|
||||
// report (Ozone bao 17536 ~365ms nhung content-latency that ~0). ponytail:
|
||||
// neu plugin content-latency that L>0 -> pdc = FILL + L (display-lat giu FILL
|
||||
// vi processed da gom L).
|
||||
// Dong bo voi sf-fx-realtime.js: FILL 40 block (10240 @48k). FX_RT_START_EST_MS:
|
||||
// uoc luong khoi dau khi chua co phep do (fx_play_first) - dung cho freeze
|
||||
// playhead thay V39 0.31s.
|
||||
const FX_RT_WET_LAG_SAMPLES=10240;const FX_RT_START_EST_MS=450;// G3: anchor processed — playhead = startOffset + (processed - anchor - latency)/sr
|
||||
// (da tru latency -> bam vi tri mau NGHE DUOC; pre-roll tu giu tai startOffset —
|
||||
// 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;// 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
|
||||
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;// M26: pdc = wet-lag FILL 10240 (~213ms) - content nghe = S(t-FILL); VST
|
||||
// report (17536) la plugin buffer, content-latency that ~0 (M26b do onset).
|
||||
// ponytail: neu content-latency that L>0 -> FX_RT_WET_LAG_SAMPLES + L.
|
||||
const l=fxRt&&fxRt.active?FX_RT_WET_LAG_SAMPLES: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
|
||||
@@ -1740,10 +1738,7 @@ const _guiVst=masteringSettings.gui_open_path&&(masteringSettings.chain||[]).som
|
||||
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).
|
||||
let _plNow=false;try{_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();// M26 diag: PWR OFF mid-play — snap pdcDelay/gains + hoi worklet
|
||||
// state tai t0/+180/+400/+800ms de do: pdc thuc te khi dry mo,
|
||||
// wet tail song bao lau, gains ramp ra sao. Bo diag o build fix.
|
||||
if(_plNow){const _busB=masterBus;const _ctxB=getAudioContext();const _snapB=k=>{let _pdB='na',_gdB='na',_gwB='na',_psB='na',_paB='na';try{if(_busB&&_busB.pdcDelay)_pdB=(_busB.pdcDelay.delayTime.value*1000).toFixed(1);}catch(e){}try{if(_busB&&_busB.masterFxDryGain)_gdB=_busB.masterFxDryGain.gain.value.toFixed(4);}catch(e){}try{if(_busB&&_busB.masterFxGain)_gwB=_busB.masterFxGain.gain.value.toFixed(4);}catch(e){}try{_psB=typeof fxRt.processedSamples==='number'?fxRt.processedSamples:'na';}catch(e){}try{_paB=typeof fxRt.processedAnchor==='number'?fxRt.processedAnchor:'na';}catch(e){}const _oB=startOffsetTimeRef.current||0;const _aB=startAudioTimeRef.current||0;const _tB=_ctxB?_ctxB.currentTime:0;_dbgLog('ph_echo'+k,'pdcMs='+_pdB+' gDry='+_gdB+' gWet='+_gwB+' lat='+(fxRt.latencyTotal||0)+' ps='+_psB+' pa='+_paB+' cur='+(currentTimeRef.current==null?'na':currentTimeRef.current.toFixed(3))+' off='+_oB.toFixed(3)+' wall='+(_oB+_tB-_aB).toFixed(3)+' ctx='+_tB.toFixed(3));try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'dbg_state',k:k});}catch(e){}};const _wlB=ev=>{try{const d=ev.data;if(d&&d.type==='dbg_state_r'){_dbgLog('ph_echo_wl','k='+d.k+' t='+d.t+' q='+d.q+' play='+d.playing+' hs='+d.hasStarted+' sil='+d.silenced+' fp='+d.flushPending+' stp='+d.stopped+' wm='+d.wrapMode+' wfb='+d.wrapFadeBlocks+' lat='+d.latencySamples+' rfill='+d.resumeFill+' inC='+d.inCount+' outPos='+d.outPos+' fadeL='+d.fadeLeft+' holdL='+d.holdFadeLeft);}}catch(e){}};try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.addEventListener('message',_wlB);}catch(e){}_snapB(0);[180,400,800].forEach((_msB,_iB)=>setTimeout(()=>_snapB(_iB+1),_msB));setTimeout(()=>{try{if(fxRt.node&&fxRt.node.port)fxRt.node.port.removeEventListener('message',_wlB);}catch(e){}},1600);}}}else{fxRtStop();}}}}catch(e){console.warn('[FX RT] resync fail:',e);}// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
|
||||
let _plNow=false;try{_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-FX_RT_WET_LAG_SAMPLES)/_sr2):null;const _wall2=_off2+(_c2.currentTime-_at2);const _content2=_eng2!==null?_eng2:_wall2-FX_RT_WET_LAG_SAMPLES/_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 →
|
||||
@@ -1863,7 +1858,7 @@ window.__fxRtWakeRebase=function(){try{if(!fxRt||!fxRt.active)return;const _c=ge
|
||||
// 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
|
||||
const _contentNow=(startOffsetTimeRef.current||0)+(_c.currentTime-(startAudioTimeRef.current||_c.currentTime));startOffsetTimeRef.current=_contentNow;startAudioTimeRef.current=_c.currentTime;if(_pa!==null)fxRt.processedAnchor=_pa-FX_RT_WET_LAG_SAMPLES;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.
|
||||
@@ -2101,7 +2096,7 @@ const triggerRescheduleNow=()=>{try{lastRescheduleTimeRef.current=0;}catch(e){}}
|
||||
// timeline sample-accurate (scheduling theo sampleOffset).
|
||||
if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected&&(isPlaying||isPlayingRef.current)){try{const _pctx=getAudioContext();// G3: FX active -> position tu processed engine (da tru latency) — bam
|
||||
// vi tri mau NGHE duoc, khong freeze/snap; fallback wall-clock.
|
||||
let _posSamples;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){_posSamples=Math.max(Math.floor(startOffsetTimeRef.current*(_pctx.sampleRate||44100)),Math.floor(startOffsetTimeRef.current*(_pctx.sampleRate||44100)+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))));}else{const _elapsed=_pctx.currentTime-(startAudioTimeRef.current||_pctx.currentTime);_posSamples=Math.max(0,Math.floor((startOffsetTimeRef.current+_elapsed)*(_pctx.sampleRate||44100)));}bridgePlayheadSampleRef.current=_posSamples;// set_position ~mỗi giây: C++ A13 cập nhật timeline anchor, tối ưu cho
|
||||
let _posSamples;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){_posSamples=Math.max(Math.floor(startOffsetTimeRef.current*(_pctx.sampleRate||44100)),Math.floor(startOffsetTimeRef.current*(_pctx.sampleRate||44100)+(fxRt.processedSamples-fxRt.processedAnchor-FX_RT_WET_LAG_SAMPLES)));}else{const _elapsed=_pctx.currentTime-(startAudioTimeRef.current||_pctx.currentTime);_posSamples=Math.max(0,Math.floor((startOffsetTimeRef.current+_elapsed)*(_pctx.sampleRate||44100)));}bridgePlayheadSampleRef.current=_posSamples;// set_position ~mỗi giây: C++ A13 cập nhật timeline anchor, tối ưu cho
|
||||
// seek/loop (Rust pump + C++ không tự biết vị trí; JS là nguồn duy nhất).
|
||||
try{const _sr=_pctx.sampleRate||44100;if(!window.__bridgeLastPosSent)window.__bridgeLastPosSent=0;if(bridgePlayheadSampleRef.current-window.__bridgeLastPosSent>_sr){window.__bridgeLastPosSent=bridgePlayheadSampleRef.current;window.NativeBridgeService.transport('set_position',bridgePlayheadSampleRef.current);}}catch(e2){}}catch(e){}}// Realtime re-schedule: khi đang play (loop play) mà items (vị trí/speed/
|
||||
// duration — kể cả nội dung section tab) thay đổi → dừng + schedule lại từ
|
||||
@@ -2170,10 +2165,10 @@ if(!st.buffer&&st.type!=='PIANO_ROLL')return;if(!st.isPlaying){// PIANO ROLL / s
|
||||
// item (st.currentTime = playPos - item.startTime); playhead vượt ra
|
||||
// ngoài duration item → bắt đầu từ đầu (0). Không có play nào → đứng
|
||||
// yên (chế độ edit). Loop luôn tiếp tục (rAF).
|
||||
if(isPlaying){const context=getAudioContext();let _ph=startOffsetTimeRef.current+(context.currentTime-startAudioTimeRef.current);if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){_ph=startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))/(context.sampleRate||44100);}let _local=_ph;const _itemInfo=getPianoRollItemInfo(st);if(_itemInfo){if(_ph<_itemInfo.startTime)_local=0;else if(_ph>=_itemInfo.startTime+_itemInfo.duration)_local=0;// vượt duration item → đầu
|
||||
if(isPlaying){const context=getAudioContext();let _ph=startOffsetTimeRef.current+(context.currentTime-startAudioTimeRef.current);if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){_ph=startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-FX_RT_WET_LAG_SAMPLES)/(context.sampleRate||44100);}let _local=_ph;const _itemInfo=getPianoRollItemInfo(st);if(_itemInfo){if(_ph<_itemInfo.startTime)_local=0;else if(_ph>=_itemInfo.startTime+_itemInfo.duration)_local=0;// vượt duration item → đầu
|
||||
else _local=_ph-_itemInfo.startTime;}// Log chẩn đoán (rate-limited ~2 lần/giây): xác nhận đồng bộ MAIN/
|
||||
// SECTION ↔ PIANO ROLL — ph (vị trí play) → local (trong item).
|
||||
try{const _sf=window.__syncFrame=(window.__syncFrame||0)+1;if(_sf%45===0){console.log('[Sync] PR',st.id,'ph=',_ph.toFixed(2),'item.start=',_itemInfo?_itemInfo.startTime.toFixed(2):'-','dur=',_itemInfo?_itemInfo.duration.toFixed(2):'-','→ local=',_local.toFixed(2),'tab=',activeTabRef.current);}}catch(e){}setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:_local}:s));}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}const context=getAudioContext();const speedFactor=st.speed||1.0;const elapsed=context.currentTime-startAudioTimeRef.current;let wallTime=startOffsetTimeRef.current+elapsed;const bufferPos=startBufferOffsetRef.current+elapsed*speedFactor;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){wallTime=startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))/(context.sampleRate||44100);}// Loop sub-tab selection (bufferPos is buffer-time)
|
||||
try{const _sf=window.__syncFrame=(window.__syncFrame||0)+1;if(_sf%45===0){console.log('[Sync] PR',st.id,'ph=',_ph.toFixed(2),'item.start=',_itemInfo?_itemInfo.startTime.toFixed(2):'-','dur=',_itemInfo?_itemInfo.duration.toFixed(2):'-','→ local=',_local.toFixed(2),'tab=',activeTabRef.current);}}catch(e){}setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:_local}:s));}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}const context=getAudioContext();const speedFactor=st.speed||1.0;const elapsed=context.currentTime-startAudioTimeRef.current;let wallTime=startOffsetTimeRef.current+elapsed;const bufferPos=startBufferOffsetRef.current+elapsed*speedFactor;if(fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null){wallTime=startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-FX_RT_WET_LAG_SAMPLES)/(context.sampleRate||44100);}// Loop sub-tab selection (bufferPos is buffer-time)
|
||||
if(st.selectionStart!==null&&st.selectionEnd!==null&&st.selectionStart!==st.selectionEnd&&st.isLooping){const start=Math.min(st.selectionStart,st.selectionEnd);const end=Math.max(st.selectionStart,st.selectionEnd);if(bufferPos>=end){fxRtWrapRestart=true;// V38: loop wrap restart
|
||||
try{_dbgLog('wrap_info','solo='+(hasAnySolo?1:0)+' local='+(selectionMode==='local'?1:0)+' tracks='+(tracks?tracks.length:-1)+' nl='+nativeLoopSrcsRef.current.size);}catch(e){}fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime
|
||||
stopAllPlayback();setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),currentTime:start,isPlaying:true}:s));schedulePianoRollMidi(st,start);startSubTabPlayback(st,start);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}}const effectiveDuration=st.type==='PIANO_ROLL'?(()=>{const notes=st.notes||[];const bpmVal=parseInt(bpmRef?.current||bpm)||120;const beatSec=60.0/bpmVal;if(recordingStateRef.current==='RECORDING')return 600.0;// 10 min during recording
|
||||
@@ -2183,7 +2178,7 @@ stopAllPlayback();if(st.isLooping){setSubTabs(prev=>prev.map(s=>s.id===activeTab
|
||||
// nếu chỉ chạy silent buffer).
|
||||
if(st.type==='PIANO_ROLL')schedulePianoRollMidi(st,0);startSubTabPlayback(st,0);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}else{setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:0,isPlaying:false}:s));}return;}setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:wallTime}:s));animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}if(!isPlaying)return;const context=getAudioContext();const elapsed=context.currentTime-startAudioTimeRef.current;const updatedTime=startOffsetTimeRef.current+elapsed;// G3: playhead display engine-derived khi FX active (wrap/loop trigger giu
|
||||
// wall-clock — transport truth).
|
||||
const _playheadSec=_fxRtSeekFreeze&&fxRt&&fxRt.active?startOffsetTimeRef.current:fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null?Math.max(startOffsetTimeRef.current,startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))/(context.sampleRate||44100)):updatedTime;// Selection Loop - LOOP_MAKER.md + LOOP_EDITOR_2.md §4.2
|
||||
const _playheadSec=_fxRtSeekFreeze&&fxRt&&fxRt.active?startOffsetTimeRef.current:fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null?Math.max(startOffsetTimeRef.current,startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-FX_RT_WET_LAG_SAMPLES)/(context.sampleRate||44100)):updatedTime;// Selection Loop - LOOP_MAKER.md + LOOP_EDITOR_2.md §4.2
|
||||
// If selection cleared by user, play linearly (don't loop)
|
||||
if(!selectionCleared&&isLoopingSelection&&selLeft!==null&&selRight!==null){if(selRight>selLeft&&updatedTime>=selRight){if(hasAnySolo||selectionMode==='local'){fxRtWrapRestart=true;// V38: loop wrap restart
|
||||
try{_dbgLog('wrap_info','solo='+(hasAnySolo?1:0)+' local='+(selectionMode==='local'?1:0)+' tracks='+(tracks?tracks.length:-1)+' nl='+nativeLoopSrcsRef.current.size);}catch(e){}fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime
|
||||
@@ -2251,7 +2246,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=202609071745`);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=202609071819`);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
|
||||
|
||||
Reference in New Issue
Block a user