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:
2026-09-07 18:19:39 +07:00
parent 3bc1b99376
commit 3601748881
2 changed files with 44 additions and 104 deletions
+20 -75
View File
@@ -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