V42+V42A: fix seam click khi loop master FX — worklet wrap crossfade (tail->head) + native WebAudio loop cho bypass (het click beat 0); dip bypass 5ms; PDC playhead freeze/snap

This commit is contained in:
2026-08-28 11:35:32 +07:00
parent 9ccc28d211
commit fa61712ecc
10 changed files with 690 additions and 108 deletions
+17
View File
@@ -1123,6 +1123,16 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
# tới worklet kịp khi main-thread stall chặn cả 2 hướng (fade nằm trong
# browser queue, tới sau khi worklet đã cạn buffer → click vẫn còn), và
# fade-in sau stall tạo dip âm lượng định kỳ = vibrato/wow&flutter.
# V34 diag: pump gap log - bridge khong san xuat block (SHM rong) > 30ms
# => fxrt_pump.log (phan biet bridge stall vs Python pump stall).
_plog = os.environ.get("FXRT_PUMP_LOG")
if not _plog:
_ad = os.environ.get("APPDATA")
_plog = os.path.join(_ad, "SonicForgeDAW", "logs", "fxrt_pump.log") if _ad else ""
_pf = open(_plog, "a", buffering=1) if _plog else None
_pump_last = None
if _pf is not None:
_pf.write("%d pump_start\n" % int(_time.time() * 1000))
_last_lat = None
try:
while True:
@@ -1147,6 +1157,13 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
# time.sleep dùng high-res waitable timer -> poll đúng 2ms.
await asyncio.to_thread(_time.sleep, 0.002)
continue
if _pf is not None:
_now = _time.time()
if _pump_last is not None:
_gap = (_now - _pump_last) * 1000.0
if _gap > 30.0:
_pf.write("%d pump_gap %.0f n=%d\n" % (int(_now * 1000), _gap, len(blks)))
_pump_last = _now
for _blk in blks:
await websocket.send_bytes(_blk)
except Exception:
+6 -1
View File
@@ -178,7 +178,12 @@ class FxRealtimeSession:
"--parent", str(os.getpid())]
# FXRT_BRIDGE_LOG=<path>: bat stderr bridge (FxRTPerf) vao file — chan
# doan outFull/drop; mac dinh DEVNULL nhu cu.
_blog = os.environ.get("FXRT_BRIDGE_LOG") or ""
# V34 diag: mac dinh ghi FxRTPerf vao fxrt_bridge.log de phan tich
# stall (procMax=bridge/VST cham, outDepth=Python pump cham).
_blog = os.environ.get("FXRT_BRIDGE_LOG")
if not _blog:
_ad = os.environ.get("APPDATA")
_blog = os.path.join(_ad, "SonicForgeDAW", "logs", "fxrt_bridge.log") if _ad else ""
self.proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
+331 -40
View File
@@ -240,6 +240,8 @@ let _cc11Epoch = 0;
let _pendingFadeSession = null;
const restorePendingFadeSession = () => {
if (!_pendingFadeSession) return;
let _bgBefore = -1;
try { if (_pendingFadeSession.bridgeGainNode) _bgBefore = _pendingFadeSession.bridgeGainNode.gain.value; } catch (e) {}
try {
const t = getAudioContext().currentTime;
_pendingFadeSession.fadedGains.forEach(g => {
@@ -249,8 +251,64 @@ const restorePendingFadeSession = () => {
try { _pendingFadeSession.bridgeGainNode.gain.cancelScheduledValues(t); _pendingFadeSession.bridgeGainNode.gain.setValueAtTime(1.0, t); } catch (e) {}
}
} catch (e) {}
_dbgLog('restore', 'gains=' + _pendingFadeSession.fadedGains.length + ' bg=' + _bgBefore.toFixed(4) + '->1.0');
_pendingFadeSession = null;
};
// V28: timing diagnostics -> %APPDATA%/SonicForgeDAW/logs/dbg.log (Rust dbg_log).
// Fire-and-forget invoke — khong chan main thread; tag+extra ghi kem unix_ms.
const _dbgLog = (tag, extra) => {
try {
if (window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
window.__TAURI__.core.invoke('dbg_log', { tag: tag, extra: extra === undefined ? '' : String(extra) }).catch(function () {});
}
} catch (e) {}
};
// V29: expose dbg hook cho BridgeAudioNode (worklet state) + helpers.
window.__dbgLog = _dbgLog;
if (window.PerformanceObserver) { try { new PerformanceObserver((list) => { for (const e of list.getEntries()) _dbgLog('longtask', 'd=' + Math.round(e.duration) + ' s=' + Math.round(e.startTime)); }).observe({ entryTypes: ['longtask'] }); } catch (e) {} }
const _dbgBridgeGain = () => {
try {
if (window.BridgeAudioNode && window.BridgeAudioNode.getOutputNode) {
const _g = window.BridgeAudioNode.getOutputNode();
if (_g && _g.gain) return 'bg=' + _g.gain.value.toFixed(4);
return 'bg=none';
}
} catch (e) {}
return 'bg=n/a';
};
const _dbgWorkletState = () => {
try {
if (window.BridgeAudioNode && window.BridgeAudioNode.getDiag) {
const _d = window.BridgeAudioNode.getDiag();
return 'wl=' + (_d.useWorklet ? 'y' : 'n') + ' pre=' + _d.preBufferLen + ' ch=' + _d.chunksLen;
}
} catch (e) {}
return 'wl=n/a';
};
window.__dbgBridgeGain = _dbgBridgeGain;
window.__dbgWorkletState = _dbgWorkletState;
// V28: ding fix — midi bat ky (keyboard/pad/timeline) restore bridge gain sau
// stop (gain giu ~0.0001 — V27 restore 800ms khong du cho VSTi release tail dai).
window.__restorePendingFadeSession = restorePendingFadeSession;
// V31: live note (ARM keyboard) — xoa backlog outQueue master FX truoc khi
// note preview render (idle block ~341ms lam ARM lag ~0.3s).
window.__clearMasterFxQueue = function () {
_dbgLastClearMs = performance.now();
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: 'live' }); } catch (e) {}
};
// V32: queueLen master FX worklet hien tai (midi_push log — ARM latency).
window.__fxRtQueueLen = function () {
try { return (fxRt && fxRt.stats) ? fxRt.stats.queueLen : 'n/a'; } catch (e) { return 'n/a'; }
};
// V28: do jank — play_start timestamp; play_rAF / play_first_audio log delta.
let _dbgPlayStartMs = 0;
let _dbgLastClearMs = 0;
let _dbgFirstAudioPending = false;
let _dbgFirstAudiblePending = false;
// V40: real PDC — freeze playhead theo latency đo được (fx_play_first self-calib).
let _fxRtPlayheadSnapPending = false;
let _fxRtFreezeTimer = null;
let _fxRtFreezeTries = 0;
const _nativeTrackUrls = {};
const _nativeTrackPending = {};
// ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─
@@ -1710,13 +1768,14 @@ function encodeFloatWavBlob(pcm, sampleRate, channels) {
// 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, latencyTotal: 0, fadedIn: false };
const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, lastInSent: 0, chainKey: null, latencyTotal: 0, fadedIn: false, pdcCalibMs: 0 };
// 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 /
// new-project / export-end → teardown bình thường.
let fxRtKeepForRestart = false;
let fxRtWrapRestart = false; // V38: loop wrap restart -- clear_queue mode 'loop' (always-wipe + RESUME_FILL)
// V21: user stop (fade=true) giữ master FX session warm — play kế tiếp có FX
// ngay, không chờ reload VST ~4s. Idle timeout: ngừng chơi lâu → teardown
// giải phóng CPU/VST (chấp nhận ~4s delay ở play sau khi idle lâu).
@@ -1848,7 +1907,7 @@ async function fxRtStart() {
fxRt.sessionId = st.session_id;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608231800`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608272300`);
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);
@@ -1897,8 +1956,8 @@ async function fxRtStart() {
if (fxRt.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);
let l = fxRt.outL, r = fxRt.outR;
if (!l || l.length !== half) { l = fxRt.outL = new Float32Array(half); r = fxRt.outR = new Float32Array(half); }
for (let i = 0; i < half; i++) { l[i] = f[i * 2]; r[i] = f[i * 2 + 1]; }
fxRt.node.port.postMessage({ type: 'out', l, r });
};
@@ -1909,12 +1968,13 @@ async function fxRtStart() {
// mẫu (~3.5-4.75ms/block) → đủ 187.5/s (take=1: 5.5ms > budget 5.33ms →
// drop → crackle). KHÔNG dùng f.subarray().buffer — nó gửi cả backing
// buffer 8192B; slice() copy đúng 2048B.
const fxRtChunks = [0, 1, 2, 3].map(() => new Float32Array(512));
const fxRtSendFrames = (d) => {
if (!fxRt.ws || fxRt.ws.readyState !== 1) return;
try {
const f = new Float32Array(d.data);
if (f.length > 512) {
for (let i = 0; i < f.length; i += 512) fxRt.ws.send(f.slice(i, i + 512).buffer);
for (let i = 0; i < f.length; i += 512) { const c = fxRtChunks[i / 512]; c.set(f.subarray(i, i + 512)); fxRt.ws.send(c.buffer); }
} else {
fxRt.ws.send(d.data);
}
@@ -1978,6 +2038,28 @@ async function fxRtStart() {
// Telemetry worklet (mỗi ~2s): chẩn đoán crackle session thật.
fxRt.stats = d;
console.debug('[FX RT stats]', d);
// V33: log dbg.log — diff giua 2 dong = so su kien trong ~2s (u=underrun,
// h=hold, f=fade, s=silence, cap=capDrop, fi=fadeIn, q=queueLen).
try { _dbgLog('fx_stats', 'u=' + d.underruns + ' h=' + d.holds + ' f=' + d.fades + ' s=' + d.silences + ' cap=' + d.capDrops + ' fi=' + d.fadeIns + ' q=' + d.queueLen); } catch (e) {}
return;
}
if (d.type === 'dbg') {
// V32: hop dau sau clear_queue (in/out/play) — do latency master FX path.
const _now = performance.now();
// V40: self-calib PDC — fx_play_first = block đầu tiên thực sự phát ra
// (đã qua FILL) → latency path thực đo được; snap playhead về đúng
// startOffset ngay mẫu đầu tiên nghe được (cả 2 chiều: hết chạy trước
// lẫn hết lag). Native-bridge path dùng _dbgFirstAudiblePending riêng —
// không đụng flag này.
// V41: guard ≥50ms — tại loop wrap (clear_queue mode 'loop' giữ queue)
// worklet phát lại OLD TAIL ~5ms → dbgPlayFirst tiêu thụ → trước đây snap
// playhead SỚM + calib nhiễu ~50ms → display chạy trước → wrap sớm → vấp
// beat 0:0. Pass mới thật ≥ ~80ms → snap/calib chỉ trên event hợp lệ.
if (d.tag === 'fx_play_first' && (_now - _dbgPlayStartMs) >= 50) {
fxRt.pdcCalibMs = Math.max(50, Math.min(3000, _now - _dbgPlayStartMs));
try { if (window.__fxRtSnapPlayhead) window.__fxRtSnapPlayhead(); } catch (e2) {}
}
_dbgLog('fx_hop_' + d.tag, 'sincePlay=' + Math.round(_now - _dbgPlayStartMs) + ' sinceClear=' + Math.round(_now - _dbgLastClearMs) + ' wlT=' + d.t + ' q=' + (d.q !== undefined ? d.q : ''));
return;
}
if (d.type !== 'in') return;
@@ -2068,10 +2150,12 @@ function fxRtStop(silent) {
// 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.
// RTT = batching 4 block (~7 block @48k ≈ 1792) + FILL buffer 24 block
// (6144) — worklet chỉ phát khi queue ≥ FILL → latency tĩnh thêm FILL → 31
// block ≈ 7936. (FILL 16 trước đây = 5888; giữ đồng bộ với sf-fx-realtime.js)
const FX_RT_ROUNDTRIP_SAMPLES = 7936;
// V40: FILL 24→8 (sf-fx-realtime.js) → RTT = batching 4 block (~7 block @48k
// ≈ 1792) + FILL buffer 8 block (2048) = 15 block ≈ 3840 (~80ms @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 = 3840;
const FX_RT_START_EST_MS = 450;
function fxRtApplyPdc() {
const ctx = getAudioContext();
const bus = masterBus;
@@ -2228,28 +2312,49 @@ function getAudioContext() {
var st = await window.NativeBridgeService.queryStatus();
var connected = !!(st && st.connected);
window.SonicMidiRouter.setBridgeConnected(connected);
_dbgLog('bootstrap', 'bridge_sr=' + (st && st.sample_rate) + ' ctx_sr=' + audioCtx.sampleRate + ' connected=' + connected);
console.log('[Bridge] bootstrap connected=', connected);
if (connected) {
try {
console.log('[Bridge] Requesting restart to match sampleRate:', audioCtx.sampleRate);
await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate', { sampleRate: audioCtx.sampleRate });
// Bridge restart → mất hết instrument → reset map để reconnect reload.
window.__bridgeLoadedChannels = {};
window.__bridgeLoadedPaths = {};
for (var retry = 0; retry < 10; retry++) {
await new Promise(r => setTimeout(r, 100));
var st2 = await window.NativeBridgeService.queryStatus();
if (st2 && st2.connected) {
console.log('[Bridge] Reconnected at sampleRate:', audioCtx.sampleRate);
break;
// V27: bridge_status trả sample_rate thật (BridgeSampleRate state) —
// nếu đã khớp audioCtx (48k mặc định) thì KHÔNG restart: trước đây
// luôn kill+respawn → first play sau launch im lặng ~0.5s (chờ bridge
// respawn + restore instrument từ bridge_state.json).
if (st.sample_rate && st.sample_rate !== audioCtx.sampleRate) {
try {
console.log('[Bridge] Requesting restart to match sampleRate:', audioCtx.sampleRate, '(bridge was', st.sample_rate + ')');
await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate', { sampleRate: audioCtx.sampleRate });
// Bridge restart → mất hết instrument → reset map để reconnect reload.
window.__bridgeLoadedChannels = {};
window.__bridgeLoadedPaths = {};
for (var retry = 0; retry < 10; retry++) {
await new Promise(r => setTimeout(r, 100));
var st2 = await window.NativeBridgeService.queryStatus();
if (st2 && st2.connected) {
console.log('[Bridge] Reconnected at sampleRate:', audioCtx.sampleRate);
break;
}
}
} catch (err) {
console.warn('[Bridge] restart error:', err);
}
} catch (err) {
console.warn('[Bridge] restart error:', err);
} else {
console.log('[Bridge] sampleRate match — skip restart:', audioCtx.sampleRate);
}
window.BridgeAudioNode.init(audioCtx);
window.NativeBridgeService.onAudio(function (l, r) {
try {
if (_dbgFirstAudioPending) {
_dbgFirstAudioPending = false;
_dbgLog('play_first_audio', 'delta_ms=' + Math.round(performance.now() - _dbgPlayStartMs));
}
if (_dbgFirstAudiblePending && l && r) {
var _nz = false;
for (var _i = 0; _i < l.length; _i++) { if (l[_i] !== 0 || r[_i] !== 0) { _nz = true; break; } }
if (_nz) {
_dbgFirstAudiblePending = false;
_dbgLog('play_first_audible', 'delta_ms=' + Math.round(performance.now() - _dbgPlayStartMs));
}
}
window.BridgeAudioNode.onAudio(l, r);
} catch (e) {
console.warn('[Bridge] audio callback error:', e && e.message || e);
@@ -14185,11 +14290,13 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
window.addEventListener('resize', resizeAll);
const fftData = new Uint8Array(1024);
let _peakScratch = new Float32Array(2048);
function getPeakLevel(analyser) {
if (!analyser) return 0;
const bufferLength = analyser.fftSize;
const dataArray = new Float32Array(bufferLength);
if (_peakScratch.length < bufferLength) _peakScratch = new Float32Array(bufferLength);
const dataArray = _peakScratch;
analyser.getFloatTimeDomainData(dataArray);
let maxVal = 0;
@@ -14353,15 +14460,17 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
// enough to gauge perceived loudness of the processed master).
const outAn = masterBus && masterBus.analyser;
if (outAn) {
const outBuf = new Float32Array(outAn.fftSize);
const outLen = outAn.fftSize;
if (_peakScratch.length < outLen) _peakScratch = new Float32Array(outLen);
const outBuf = _peakScratch;
outAn.getFloatTimeDomainData(outBuf);
let sq = 0;
for (let i = 0; i < outBuf.length; i++) sq += outBuf[i] * outBuf[i];
const rmsDb = 20 * Math.log10(Math.sqrt(sq / outBuf.length) + 1e-12);
for (let i = 0; i < outLen; i++) sq += outBuf[i] * outBuf[i];
const rmsDb = 20 * Math.log10(Math.sqrt(sq / outLen) + 1e-12);
const rmsEl = document.getElementById('outRmsText');
if (rmsEl) rmsEl.innerText = rmsDb < -90 ? '-inf' : rmsDb.toFixed(1) + ' dB';
if (!lufsAccRef.current) lufsAccRef.current = [];
lufsAccRef.current.push(sq / outBuf.length);
lufsAccRef.current.push(sq / outLen);
const accLen = 24; // ~0.4s at 60fps rAF
if (lufsAccRef.current.length >= accLen) {
const blocks = lufsAccRef.current.splice(0, accLen);
@@ -20722,9 +20831,51 @@ const App = () => {
const rulerRef = useRef(null);
const activeSourcesRef = useRef([]);
const activeTrackNodesRef = useRef({}); // { [trackId]: { gainNode, pannerNode } }
const nativeLoopSrcsRef = useRef(new Map()); // V42A: { [trackId+'::'+clipId]: { src, loopStart, loopEnd } } — native WebAudio loop qua wrap
const startOffsetTimeRef = useRef(0);
const startBufferOffsetRef = useRef(0);
const startAudioTimeRef = useRef(0);
// V40: real PDC — freeze playhead theo latency FX path (thay V39 freeze giả
// định 0.31s → under-estimate → playhead chạy trước ~0.66s). __fxRtPlayheadFreeze:
// gọi ở mọi play start (allowCold=true ở startTrackPlayback che cold-start VST
// pre-roll; section play không khởi động FX session → false). __fxRtSnapPlayhead:
// worklet fx_play_first → bỏ freeze đúng mẫu đầu tiên nghe được. Fail-safe:
// session không active trong 1500ms → hủy freeze (đừng đóng băng playhead vô hạn).
window.__fxRtPlayheadFreeze = function (allowCold) {
try { if (_fxRtFreezeTimer) { clearTimeout(_fxRtFreezeTimer); _fxRtFreezeTimer = null; } } catch (e3) {}
// V41: gate theo CHAIN (không chỉ active) — session FX warm (stop giữ warm,
// bypass toggle gọi rebuildMasteringGraph KHÔNG gọi fxRtStop) → fxRt.active vẫn
// true khi bypass → trước đây freeze nhầm → playhead chạy trước → wrap sớm → vấp.
const _fxWillRun = !!(fxRt && fxRtChain().length > 0 && (fxRt.active || allowCold));
if (!_fxWillRun) return;
const _estMs = (fxRt.pdcCalibMs || FX_RT_START_EST_MS);
const _wasActiveAtFreeze = !!(fxRt && fxRt.active);
startAudioTimeRef.current = getAudioContext().currentTime + _estMs / 1000;
_fxRtPlayheadSnapPending = true;
_fxRtFreezeTries = 0;
// V41: snap xác định bằng timer tại est — loop wrap warm không post fx_play_first
// cho pass mới (old-tail tiêu thụ dbgPlayFirst) nên không có event để snap. Cold
// start (fxRt.active=false) KHÔNG snap sớm: chờ fx_play_first thật (VST load 4s).
_fxRtFreezeTimer = setTimeout(function freezeSnapRetry() {
_fxRtFreezeTimer = null;
if (_wasActiveAtFreeze && _fxRtPlayheadSnapPending) {
_fxRtPlayheadSnapPending = false;
startAudioTimeRef.current = getAudioContext().currentTime;
return;
}
if (fxRt && fxRt.starting) {
if (_fxRtFreezeTries < 20) { _fxRtFreezeTries++; _fxRtFreezeTimer = setTimeout(freezeSnapRetry, 500); }
return;
}
if (!fxRt || !fxRt.active) { startAudioTimeRef.current = getAudioContext().currentTime; } // session fail → bỏ freeze
}, _estMs);
};
window.__fxRtSnapPlayhead = function () {
if (fxRt && fxRt.active && _fxRtPlayheadSnapPending) {
_fxRtPlayheadSnapPending = false;
startAudioTimeRef.current = getAudioContext().currentTime;
}
};
const animationFrameIdRef = useRef(null);
const toastTimeoutRef = useRef(null);
const rulerDragStartRef = useRef(null);
@@ -23701,6 +23852,13 @@ const App = () => {
// để master FX bị teardown giữa lúc section đang phát).
if (fxRtWarmIdleTimer) { clearTimeout(fxRtWarmIdleTimer); fxRtWarmIdleTimer = null; }
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc
// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu).
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong
// cho phia sau; flush ring bridge (V30) khong chaym queue nay.
const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {}
if (!st.buffer) {
st.buffer = context.createBuffer(1, 128, context.sampleRate);
}
@@ -23805,6 +23963,9 @@ const App = () => {
startOffsetTimeRef.current = offsetWallTime;
startBufferOffsetRef.current = offsetBuffer;
startAudioTimeRef.current = context.currentTime;
// V40: real PDC — freeze playhead theo latency FX path (đo được, không ước
// như V39). Section play không khởi động FX session → allowCold=false.
try { if (window.__fxRtPlayheadFreeze) window.__fxRtPlayheadFreeze(false); } catch (e) { }
};
const playMetronomeClick = (time, isDownbeat = false) => {
try {
@@ -24139,6 +24300,7 @@ const App = () => {
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
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
@@ -24166,6 +24328,7 @@ const App = () => {
return Math.max(maxEnd * beatSec, 16 * beatSec * 4) + 1.0;
})() : st.buffer.duration;
if (bufferPos >= effectiveDuration) {
fxRtWrapRestart = true; // V38: loop wrap restart
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
if (st.isLooping) {
@@ -24206,6 +24369,7 @@ const App = () => {
if (!selectionCleared && isLoopingSelection && selLeft !== null && selRight !== null) {
if (selRight > selLeft && updatedTime >= selRight) {
if (hasAnySolo || selectionMode === 'local') {
fxRtWrapRestart = true; // V38: loop wrap restart
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = selLeft;
@@ -24219,6 +24383,7 @@ const App = () => {
setCurrentTime(selLeft);
setIsPlaying(true);
} else {
fxRtWrapRestart = true; // V38: loop wrap restart
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = selLeft;
@@ -24241,6 +24406,7 @@ const App = () => {
return;
}
if (isLoopingSelection) {
fxRtWrapRestart = true; // V38: loop wrap restart
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = 0;
@@ -24502,7 +24668,7 @@ const App = () => {
fxRtTracks[key] = entry;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608231800`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608272300`);
const node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
entry.node = node;
const u = new URL(API_BASE_URL);
@@ -25125,8 +25291,23 @@ const App = () => {
const context = getAudioContext();
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms)
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc
// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu).
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong
// cho phia sau; flush ring bridge (V30) khong chaym queue nay.
const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {}
_dbgPlayStartMs = performance.now();
_dbgFirstAudioPending = true;
_dbgFirstAudiblePending = true;
_dbgLog('play_start', 'offset=' + offsetTime + ' epoch=' + _nativePlayEpoch + ' ' + _dbgBridgeGain() + ' ' + _dbgWorkletState() + ' fxrt=' + (fxRt.active ? 1 : 0) + ' node=' + (fxRt.node ? 1 : 0) + ' q=' + (fxRt.stats ? fxRt.stats.queueLen : 'n/a'));
// Realtime VST FX (master chain qua SHM bridge): bật khi play + chain active.
try { fxRtStart(); } catch (e) { console.warn('[FX RT] start error:', e); }
// V40: real PDC — freeze playhead theo latency FX path; allowCold=true che
// cold-start VST pre-roll (playhead không chạy trước khi có tiếng). Bỏ V39
// freeze giả định 0.31s (under-estimate → playhead chạy trước ~0.66s).
try { if (window.__fxRtPlayheadFreeze) window.__fxRtPlayheadFreeze(true); } catch (e) { }
// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng
// bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
@@ -25138,6 +25319,7 @@ const App = () => {
bridgeStopRetryTimerRef.current = null;
}
try { window.NativeBridgeService.transport('play'); } catch (e) {}
_dbgLog('play_transport', 'offset=' + offsetTime);
}
// V12: guardPlay (scheduleMidiNoteDispatch) đọc isPlayingRef.current — nếu
// để React render set (setIsPlaying(true) sau) thì note-on delay=0 của
@@ -25152,9 +25334,19 @@ const App = () => {
const allPlayTracks = activeTracksRef.current && activeTracksRef.current.length ? activeTracksRef.current : activeTracks;
// Capture items signature at schedule time — updatePlayhead so sánh với
// signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule).
scheduledItemsSigRef.current = buildItemsSignature(allPlayTracks, sessionTabs);
// V28: defer bookkeeping signature — tính đồng bộ chặn main thread trước
// first-audio (góp vào delay onset); signature chỉ so sánh ~10fps trong
// updatePlayhead → trễ 0ms là đủ (re-schedule phát hiện muộn không sao).
setTimeout(function () { try { scheduledItemsSigRef.current = buildItemsSignature(allPlayTracks, sessionTabs); } catch (e) {} }, 0);
console.log('[Play] offset=' + offsetTime + ' tracks=' + allPlayTracks.length + ' masterBus=' + !!masterBus + ' dest=' + (context.destination ? 'ok' : 'MISSING') + ' projectEnd=' + (projectEndRef.current || 0).toFixed(2));
const hasSolo = allPlayTracks.some(t => t.solo);
// V42A: native loop region — loop selection hợp lệ → [selLeft, selRight];
// isLoopingSelection không selection → [0, projectEnd] (project-end wrap 24407).
// Không loop → _loopActive=false → fallback dip V42.
const _loopSel = !selectionCleared && isLoopingSelection && selLeft !== null && selRight !== null && selRight > selLeft;
const _loopLs = _loopSel ? selLeft : 0;
const _loopLe = _loopSel ? selRight : projectEndRef.current;
const _loopActive = isLoopingSelection && _loopLe > _loopLs;
allPlayTracks.forEach(track => {
const isPlayable = hasSolo ? track.solo : !track.muted;
if (!isPlayable) return;
@@ -25178,9 +25370,7 @@ const App = () => {
const rb = routeBypassFor(track);
playNode.route.routeGain.gain.value = rb ? 0 : 1;
playNode.route.dryGain.gain.value = rb ? 1 : 0;
console.log('[Bypass] reapply track', track.id, 'contentType=' + (routeBypassFor._lastType ? 'midi' : 'audio'), 'bypass=' + rb, '→ routeGain=' + playNode.route.routeGain.gain.value + '/dryGain=' + playNode.route.dryGain.gain.value);
}
console.log('[Play] track', track.id, track.name, 'node ok:', !!(gainNode && pannerNode), 'muted:', track.muted);
const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
id: 'default',
buffer: track.buffer,
@@ -25190,13 +25380,34 @@ const App = () => {
}] : [];
clips.forEach(clip => {
if (!clip.buffer) return;
const clipStart = clip.startTime || 0;
const clipDuration = clip.buffer.duration / (clip.speed || 1.0);
const clipEnd = clipStart + clipDuration;
// V42A: native WebAudio loop — wrap mẫu-chính-xác tại loopEnd, không
// restart → không click (thay dip V42). Chỉ khi loop active + track 1
// clip + speed=1 + clip phủ đủ vùng loop. Source giữ nguyên qua wrap
// (doHardStop skip khi fxRtWrapRestart) → seam-free.
const _loopable = _loopActive && clips.length === 1 && Math.abs((clip.speed || 1.0) - 1) < 0.0001 && clipStart <= _loopLs + 0.001 && clipEnd >= _loopLe - 0.001;
const _key = track.id + '::' + (clip.id || 'default');
const _kept = nativeLoopSrcsRef.current.get(_key);
if (_loopable && _kept && _kept.src && _kept.src.playbackState !== 'ended') {
if (Math.abs(_kept.loopStart - _loopLs) < 0.001 && Math.abs(_kept.loopEnd - _loopLe) < 0.001) {
activeSourcesRef.current.push(_kept.src); // giữ source loop đang chạy qua wrap
return;
}
try { _kept.src.stop(); } catch (e) {}
nativeLoopSrcsRef.current.delete(_key);
}
const source = context.createBufferSource();
source.buffer = clip.buffer;
source.playbackRate.value = clip.speed || 1.0;
source.connect(gainNode);
const clipStart = clip.startTime || 0;
const clipDuration = clip.buffer.duration / (clip.speed || 1.0);
const clipEnd = clipStart + clipDuration;
if (_loopable) {
source.loop = true;
source.loopStart = (_loopLs - clipStart) / (clip.speed || 1.0);
source.loopEnd = (_loopLe - clipStart) / (clip.speed || 1.0);
nativeLoopSrcsRef.current.set(_key, { src: source, loopStart: _loopLs, loopEnd: _loopLe });
}
if (offsetTime < clipStart) {
const delay = clipStart - offsetTime;
source.start(context.currentTime + delay, 0);
@@ -25447,15 +25658,34 @@ const App = () => {
});
}
});
// V42A: sweep native-loop sources không được schedule pass này (track muted/
// solo-off) → dừng + xóa — tránh source loop chơi âm thầm mãi.
try {
nativeLoopSrcsRef.current.forEach((v, k) => {
if (!v || !v.src || v.src.playbackState === 'ended') { nativeLoopSrcsRef.current.delete(k); return; }
if (activeSourcesRef.current.indexOf(v.src) === -1) {
try { v.src.stop(); } catch (e) {}
nativeLoopSrcsRef.current.delete(k);
}
});
} catch (e) {}
updateSfRouting();
};
// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
const startLocalTrackPlayback = (trackId, offsetTime) => {
const context = getAudioContext();
// V42A: local/solo path khong dung native loop — dung + xoa src loop con lai
// tu main play (wrap local giu chung qua doHardStop -> double audio neu khong).
try {
nativeLoopSrcsRef.current.forEach(v => { try { if (v && v.src) v.src.stop(); } catch (e) {} });
nativeLoopSrcsRef.current.clear();
} catch (e) {}
// V24: solo/local path — gọi fxRtStart (idempotent) để âm solo qua master
// chain (trước đây path này bỏ qua → solo không qua master FX).
try { fxRtStart(); } catch (e) { console.warn('[FX RT] start error:', e); }
const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {}
// V12: parity với startTrackPlayback — loop restart (solo/local) sau
// stopAllPlayback(): hủy STOP#2 + báo PLAY (nếu không C++ transportStopped
// giữ true → mọi note-on bị drop → solo/local loop pass 2+ câm) + set
@@ -25569,6 +25799,12 @@ const App = () => {
const context = getAudioContext();
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc
// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu).
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong
// cho phia sau; flush ring bridge (V30) khong chaym queue nay.
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: 'play' }); } catch (e) {}
// V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 →
// bridge transportStopped=true) → mọi note-on bị drop nếu không PLAY lại.
// Hủy STOP#2 (150ms) + báo PLAY để bridge nhận note mới (loop liên tục).
@@ -25713,6 +25949,10 @@ const App = () => {
startAudioTimeRef.current = context.currentTime;
startTrackPlayback(currentTime);
setIsPlaying(true);
// V28: rAF kế tiếp sau setState → đo jank main thread (chênh với play_start).
requestAnimationFrame(function () {
_dbgLog('play_rAF', 'delta_ms=' + Math.round(performance.now() - _dbgPlayStartMs));
});
}
};
handlePlayPauseRef.current = handlePlayPause;
@@ -25764,6 +26004,19 @@ const App = () => {
};
const stopAllPlayback = (fade) => {
try {
if (fxRtWrapRestart && fxRtChain().length === 0 && nativeLoopSrcsRef.current.size === 0 && masterBus && masterBus.muteGain && masterBus.muteGain.gain) {
// V41: dip CHỈ khi bypass (chain rỗng). FX active → seam thật xảy ra sau
// latency (~80ms) tại _now+latency; dip tại _now chỉ tạo dropout 14ms giữa
// đuôi pass cũ (audible) mà không che được seam click.
// V42: rút ngắn dip 14ms → 5ms (2ms xuống + 3ms lên) — đủ che click seam
// nhưng dropout quá ngắn để nghe thấy (V39 14ms tạo dropout rõ).
const _actx = getAudioContext(); const _now = _actx.currentTime;
masterBus.muteGain.gain.cancelScheduledValues(_now);
masterBus.muteGain.gain.setValueAtTime(masterBus.muteGain.gain.value || 1, _now);
masterBus.muteGain.gain.linearRampToValueAtTime(0.0001, _now + 0.002);
masterBus.muteGain.gain.linearRampToValueAtTime(1, _now + 0.005);
} // V39: dip input truoc hard-stop
_dbgLog('stop_start', 'fade=' + !!fade + ' ' + _dbgBridgeGain());
isPlayingRef.current = false;
// Gap 7: xoa automation schedule — tra gain master ve 1.0 (fader rieng).
try { if (masterBus && masterBus.output && masterBus.output.gain) { const _actx = getAudioContext(); masterBus.output.gain.cancelScheduledValues(_actx.currentTime); masterBus.output.gain.setValueAtTime(1, _actx.currentTime); } } catch (e) { }
@@ -25807,7 +26060,9 @@ const App = () => {
const capturedSources = activeSourcesRef.current.slice();
const capturedNodes = Object.values(activeTrackNodesRef.current);
activeSourcesRef.current = [];
activeTrackNodesRef.current = {};
// V42A: wrap (fade=false) GIỮ track nodes — source native-loop kept vẫn
// routed (node cũ). User stop (fade=true) mới xóa nodes.
if (fade) activeTrackNodesRef.current = {};
const fadedGains = [];
// V16: stop NGAY clip source CHƯA phát (playbackState='scheduled') — nếu
// chờ tới doHardStop (sau FADE_MS) chúng đã bắt đầu phát trong lúc fade →
@@ -25848,8 +26103,12 @@ const App = () => {
const doHardStop = () => {
try {
const stale = _nativePlayEpoch !== stopEpoch; // play mới bắt đầu trong lúc fade
// V42A: wrap (fxRtWrapRestart, fade=false) — giữ source native-loop đang
// chạy qua restart (seam-free tại loopEnd); stop/seek thật → dừng + xóa.
const _wrapKeepLoop = !stale && fxRtWrapRestart && nativeLoopSrcsRef.current.size > 0;
const _loopSrcs = _wrapKeepLoop ? new Set(Array.from(nativeLoopSrcsRef.current.values(), v => v.src)) : null;
capturedSources.forEach(src => {
try { src.stop(); } catch (e) { }
try { if (_loopSrcs && _loopSrcs.has(src)) return; src.stop(); } catch (e) { }
});
capturedNodes.forEach(n => {
if (n.fxStopFn) { try { n.fxStopFn(); } catch (e) {} }
@@ -25857,16 +26116,42 @@ const App = () => {
// V17: restore gain LUON — play moi trong cua so fade (stale) truoc day bo qua
// restore -> track/bridge gain ket 0.0001 -> moi am sau bi fade-out dai dang.
// Chi skip khi co fade session MOI hon (session nay het pending — session moi tu restore).
if (_pendingFadeSession && _pendingFadeSession.stopEpoch === stopEpoch) {
restorePendingFadeSession();
if (stale) {
if (_pendingFadeSession && _pendingFadeSession.stopEpoch === stopEpoch) {
restorePendingFadeSession();
}
return; // play mới đang chạy — không dừng bridge/SF của nó
}
if (stale) return; // play mới đang chạy — không dừng bridge/SF của nó
if (!_wrapKeepLoop) nativeLoopSrcsRef.current.clear(); // V42A: stop/seek thật — quên native loop
// V26: user stop (fade) — dừng master FX tail: xóa outQueue worklet + flush
// engine ring (audio đã xử lí còn trong pipeline phát tiếp ~0.3-0.5s sau stop
// = "ding"). Worklet tự fade-out 21ms (hết click) rồi báo main flush engine;
// session giữ warm (play kế tiếp FX ngay, không reload VST).
try {
if (fade && fxRt && fxRt.node && fxRt.active) {
fxRt.node.port.postMessage({ type: 'flush_for_stop' });
}
} catch (e) { console.warn('[Stop] fx flush error:', e); }
stopMidiCapture();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết
// âm ngân tức thì; A13 C++ flush note-off).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
try { window.NativeBridgeService.transport('stop'); } catch (e) {}
_dbgLog('stop_transport', 'epoch=' + stopEpoch);
// V39: MIDI panic CC123/CC120 tai wrap — VSTi note treo (khong nhan note-off) phai tat ngay.
try {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive && window.SonicMidiRouter.isBridgeActive()) {
Object.keys(trackMidiChannelsRef.current || {}).forEach(function (_tid) {
var _ch = trackMidiChannelsRef.current[_tid];
window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 123, 0, 0, 0, 0, false);
window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 120, 0, 0, 0, 0, false);
});
}
} catch (e) { console.warn('[Stop] midi panic error:', e); }
// V27: xóa audio còn buffer trong pipeline (preBuffer/chunks + worklet
// ring) — nếu không, âm trước stop còn trong ring phát tiếp sau fade.
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) { console.warn('[Stop] bridge flush error:', e); }
// V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync
// qua React effect (cham) van bay toi bridge sau STOP → retrigger am
// treo. V10: timer lưu ref — startTrackPlayback (loop restart) hủy
@@ -25882,6 +26167,12 @@ const App = () => {
// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck)
if (window.SonicSF.panic) { try { window.SonicSF.panic(); } catch (e) { console.warn('[Stop] panic error:', e); } }
}
// V28: KHÔNG auto-restore gain sau stop — VSTi release tail có thể dài
// hơn 800ms (V27 vẫn còn "ding"). Gain giữ ~0.0001 vô thời hạn; restore
// khi: play mới (startTrackPlayback entry) hoặc midi bất kỳ (pushEvent →
// __restorePendingFadeSession — keyboard/pad preview). Tail vẫn render
// nhưng im lặng qua bridge gain.
_dbgLog('stop_hard', 'epoch=' + stopEpoch + ' ' + _dbgBridgeGain());
} catch (e) {
console.warn('[Stop] stopAllPlayback error:', e);
}
File diff suppressed because one or more lines are too long
+15 -2
View File
@@ -31,7 +31,7 @@
console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback');
return false;
}
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608152200';
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608272301';
_ctx.audioWorklet.addModule(url).then(function () {
if (!_initialized) return; // disconnect() happened while loading
if (_useWorklet) return; // already set up (init retry)
@@ -49,12 +49,15 @@
_workletNode.port.postMessage({ type: 'audio', l: b.l, r: b.r });
}
console.log('[BridgeAudioNode] AudioWorklet active');
try { if (window.__dbgLog) window.__dbgLog('worklet', 'active pre=' + _preBuffer.length); } catch (e) {}
} catch (err) {
console.warn('[BridgeAudioNode] AudioWorkletNode failed — ScriptProcessor fallback', err);
_useWorklet = false;
try { if (window.__dbgLog) window.__dbgLog('worklet', 'NODE_FAIL'); } catch (e) {}
}
}).catch(function (err) {
console.warn('[BridgeAudioNode] addModule failed — ScriptProcessor fallback', err);
try { if (window.__dbgLog) window.__dbgLog('worklet', 'MODULE_FAIL'); } catch (e) {}
});
return true; // pending — worklet may become active later
}
@@ -157,6 +160,16 @@
_dest = dest;
},
disconnect: disconnect,
isReady: function () { return _initialized; }
isReady: function () { return _initialized; },
getDiag: function () {
return {
initialized: _initialized,
useWorklet: _useWorklet,
hasWorkletNode: !!_workletNode,
preBufferLen: _preBuffer.length,
chunksLen: _chunks.length,
gain: _gainNode ? _gainNode.gain.value : -1
};
}
};
})();
+4 -2
View File
@@ -1,6 +1,8 @@
// app/static/js/services/bridgeAudioWorklet.js
// VSTi crackle fix: audio sink chay tren audio thread (khong jank main thread).
// - Ring block 256 mau, toi da 64 block (~371ms @48k) — hap thu jitter pump.
// - Ring block 256 mau, toi da 8 block (~42ms @48k) — hap thu jitter pump
// nhung gioi han backlog: 64 block (~371ms) tao tre ~0.3-0.4s khi play/
// keyboard preview (audio moi phai cho ring cu chay het).
// - Underrun: fade ve 0 thay vi click (hard silence).
// - Resume sau silence: fade vao tu 0.
// - Overrun: drop block cu nhat + crossfade 64 mau tai diem drop (het click).
@@ -8,7 +10,7 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
constructor() {
super();
this._ring = [];
this._MAX = 64;
this._MAX = 8;
this._gain = 1.0;
this._lastOutL = 0.0;
this._lastOutR = 0.0;
@@ -5,6 +5,8 @@
(function () {
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview
var _lastTimelineDbgTs = 0;
var router = {
bridgeConnected: false,
onFallback: null, // function(cmd, channel, pitch, velocity) — set by app.jsx
@@ -51,11 +53,29 @@
* data2/data3 (A12): CC value / program / PB LSB|MSB — passed to bridge only.
*/
pushEvent: function (opts) {
// V28: midi bất kỳ (keyboard/pad/timeline) → restore bridge gain đang
// giữ ~0.0001 sau stop (ding fix — V27 restore 800ms không đủ tail dài).
try { if (window.__restorePendingFadeSession) window.__restorePendingFadeSession(); } catch (e) {}
// V31: live note (ARM keyboard preview) — clear master FX outQueue backlog
// (idle block ~341ms) truoc khi note moi render; timeline note (live=false)
// KHONG clear — tranh drop audio dang phat.
if (opts.live && opts.cmd === 'NOTE_ON') { try { if (window.__clearMasterFxQueue) window.__clearMasterFxQueue(); } catch (e) {} }
var cmd = opts.cmd, ch = opts.channel, pitch = opts.pitch;
var vel = (opts.velocity === undefined ? 1.0 : opts.velocity);
var sampleOffset = opts.sampleOffset || 0;
if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion);
if (ch === undefined || ch === null) ch = 0;
try {
// V36: midi_push log = 1 Tauri invoke sync (dbg_log -> disk I/O tren main
// thread Rust) moi event. Timeline phat ~380 event/s -> main thread nghet
// -> stall 110-170ms -> crackle. Chi log live (ARM preview), timeline
// gioi han 1/200ms.
var _nowMs = (window.performance && performance.now) ? performance.now() : Date.now();
if (window.__dbgLog && (opts.live || _nowMs - _lastTimelineDbgTs > 200)) {
if (!opts.live) _lastTimelineDbgTs = _nowMs;
window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' live=' + (opts.live ? 1 : 0) + ' vel=' + Math.round((vel || 0) * 100) + (window.__dbgBridgeGain ? ' ' + window.__dbgBridgeGain() : '') + ' q=' + (window.__fxRtQueueLen ? window.__fxRtQueueLen() : 'n/a'));
}
} catch (e) {}
if (this.isBridgeActive()) {
window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3, opts.live);
return;
+113 -7
View File
@@ -9,10 +9,10 @@
// ~5.5ms/call > budget 5.33ms → 179/s < 187.5/s → engine drop input →
// crackle). Gửi burst 4 block → bridge take=4 → 1 call process 1024 mẫu
// (amortize overhead: 14-19ms/4 block ≈ 3.5-4.75ms/block) → đủ 187.5/s.
// Latency thêm ~4 block (~21ms) — worklet outQueue 64 block hấp thụ.
// Latency thêm ~4 block (~21ms) — worklet outQueue 32 block hấp thụ.
//
// FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue
// ≥ FILL (24 block ~128ms @48k) rồi mới phát. Engine gửi ASAP (không pace
// ≥ FILL (8 block ~43ms @48k) rồi mới phát. Engine gửi ASAP (không pace
// realtime) → client tích lũy buffer an toàn; mọi jitter (bridge process VST
// biến thiên, GC, WS, main thread rAF) bị hấp thụ bởi FILL thay vì queue ≈1
// → cạn → ngắt quãng.
@@ -29,8 +29,10 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
super();
this.BLOCK = 256; // FXRT_BLOCK
this.BURST = 4; // số block gom 1 frame (bridge batch)
this.FILL = 24; // 24 block ~128ms @48k: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, VST panel). PDC 7936 = (7+24)*256 (fxRtApplyPdc).
this.CAP = 64; // cap queue chặn overflow (~341ms)
this.FILL = 8; // V40: 8 block ~43ms @48k: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, VST panel). PDC 3840 = (7+8)*256 (fxRtApplyPdc). Lưu ý: cushion mỏng hơn (24→8) → jitter main-thread mạnh → nguy cơ underrun fade-out.
this.RESUME_FILL = 8; // V26: re-buffer khi resume SAU STOP (flush_for_stop) — 8 block ~43ms (V40: = FILL) → play kế tiếp bắt đầu nhanh; underrun-stall vẫn dùng FILL (crackle/wobble).
this.resumeFill = this.FILL; // mức re-buffer khi resume sau flush — stop: RESUME_FILL, underrun-stall: FILL
this.CAP = 32; // V40: cap queue chặn overflow (~170ms)
this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng)
this.holdFadeLeft = 0; // mẫu còn lại của fade-out khi underrun
this.silenced = false; // đã fade về 0 → im lặng; resume phải fade-in, không hold lại
@@ -47,23 +49,96 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click
this.outGain = 0; // gain phát hiện tại (0→1 trong fade)
this.fadeLeft = 0; // số mẫu còn lại của fade
this.skipFade = 0; // >0: crossfade 64 mẫu đang chạy sau jump (cap-drop/resume) — hết click
this.skipFade = 0; // >0: crossfade đang chạy sau jump (cap-drop/resume/wrap) — hết click
this.skipFadeTotal = 64; // V42: độ dài crossfade hiện tại (64 mẫu thường; WRAP_FADE 256 tại seam wrap)
this.skipFromL = 0.0; // mẫu output tại điểm jump — điểm bắt đầu crossfade
this.skipFromR = 0.0;
this.lastOutL = 0.0; // mẫu output cuối — nguồn crossfade + điểm bắt đầu fade-out
this.lastOutR = 0.0;
this.wrapFadeBlocks = -1; // V42: số block tail pass cũ còn lại sau clear_queue('loop') — đếm ngược để crossfade đúng seam tail→head
this.WRAP_FADE = 256; // V42: crossfade 256 mẫu (~5.3ms @48k) tại seam wrap — che click jump nội dung end→start (FX path)
this.fadeFromL = 0.0; // mẫu cố định khi fade-out underrun (fade tuyến tính tới 0)
this.fadeFromR = 0.0;
this.port.onmessage = (e) => {
const d = e.data;
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
if (d && d.type === 'flush_done') { this.flushPending = false; return; }
if (d && d.type === 'flush_for_stop') {
// V26: user stop — main thread báo xóa audio tail trong queue (audio đã
// xử lí còn phát ~0.3-0.5s sau stop = tiếng "ding"). Fade-out 21ms từ
// mẫu cuối (hết click) → im lặng; báo main flush engine ring (audio
// stale trong WS/SHM không chơi tiếp). Resume lần sau: RESUME_FILL
// (8 block ~43ms, V40: = FILL) — play kế tiếp không chờ trễ.
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1; // V42: stop — hủy wrap crossfade chờ
this.flushPending = true;
this.resumeFadeIn = true;
this.resumeFill = this.RESUME_FILL;
this.silenced = false;
// V30: fade-out tu mau output CUOI (lastOut) — truoc day dung fadeFrom
// cu (mac dinh 0) -> jump mau -> click/crackle ngay khi stop.
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.port.postMessage({ type: 'flush' });
return;
}
if (d && d.type === 'clear_queue') {
// V34: backlog guard o DAU handler (V33 dat sau mutation -> outQueue.length=0
// chay truoc -> guard luon return, clear van drop queue + fade + re-buffer moi
// lan = V31). Queue nho (< FILL 8 block) -> bo qua hoan toan (khong mutation,
// khong re-buffer) -> het ARM crackle (queue ARM chi 0-8 block, engine ~realtime).
// Queue day (idle lau, engine chay truoc ~32 block) -> clear dung (fix lag 170ms).
// V35: mode:'play' (start/section/pianoroll) LUON clear - queue < FILL luc
// play-start con stale block idle/ARM (~16) -> bo qua => phat audio cu roi
// can queue ~500ms (cold start). mode:'live' (ARM) giu guard het crackle.
// V37 ARM probe: reset dbg flags BEFORE guard so live notes (small queue) also log fx_hop
this.dbgClearAt = currentTime * 1000;
this.dbgInFirst = true;
this.dbgOutFirst = true;
this.dbgPlayFirst = true;
// V39: loop wrap — KHONG wipe: giu queue chay lien tuc (tail pass cu
// pre-roll pass moi qua VST lookahead) -> het wobble dau bai. Chi reset
// dbg flags. mode 'play' van wipe (bao ve audio cu khi seek/cold start).
// V42: dem so block tail pass cu con lai trong queue -> wrap crossfade
// trigger dung luc block dau pass moi len front (seam tail->head).
if (d.mode === 'loop') {
this.wrapFadeBlocks = this.outQueue.length;
return;
}
if (d.mode !== 'play' && this.outQueue.length < this.FILL) return;
// V31: play/ARM live note — main thread bao xoa backlog block idle
// (engine render lien tuc khi STOP -> outQueue day ~32 block ~170ms
// audio cu; play moi phai cho phia sau -> lag 0.3-0.5s). KHONG post
// 'flush' (khong chaym engine WS — note-on play vua day xuong se bi
// drop). Resume sau 8 block (~43ms) + fade-out/fade-in tu mau cuoi.
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
this.resumeFadeIn = true;
// V34: mode phan biet — play re-buffer FILL 8 (~43ms, cushion du hap thu
// engine ramp/jitter dau play); ARM live note giu RESUME_FILL 8 (~43ms) — preview
// nhanh, khong cho FILL 8.
this.resumeFill = (d.mode === 'play') ? this.FILL : this.RESUME_FILL;
this.silenced = false;
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
return;
}
if (!d || d.type !== 'out' || !d.l || !d.r) return;
// V24: đang chờ flush (resume sau underrun/stall) → drop block stale.
// Backlog engine gửi khi worklet im là audio CŨ (chơi lại → lag cộng dồn
// ~1s) — chỉ nhận block MỚI từ sau flush_done.
if (this.flushPending) return;
this.outQueue.push({ l: d.l, r: d.r });
if (this.dbgOutFirst) {
this.dbgOutFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_out_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
}
while (this.outQueue.length > this.CAP) {
this.outQueue.shift();
this.statCapDrop++;
@@ -72,6 +147,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// block mới (front) phải phát từ đầu, không tiếp tục vị trí block cũ.
if (this.skipFade <= 0) {
this.skipFade = 64;
this.skipFadeTotal = 64;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
@@ -84,6 +160,12 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.statUnderrun = 0; this.statHold = 0; this.statFade = 0;
this.statSilence = 0; this.statCapDrop = 0; this.statFadeIn = 0;
this.statCalls = 0;
// V32: do latency master FX path — danh dau clear_queue, bao main thread hop dau
// sau clear (in_first / out_first / play_first) → dbg.log delta vs play_start.
this.dbgClearAt = 0;
this.dbgInFirst = false;
this.dbgOutFirst = false;
this.dbgPlayFirst = false;
}
process(inputs, outputs) {
const input = inputs[0];
@@ -111,6 +193,10 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
}
this.port.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]);
this.inCount = 0;
if (this.dbgInFirst) {
this.dbgInFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_in_first', t: Math.round(currentTime * 1000) });
}
}
}
}
@@ -136,7 +222,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.fadeLeft--;
}
if (this.skipFade > 0) {
const t = 1 - this.skipFade / 64;
const t = 1 - this.skipFade / this.skipFadeTotal;
outL[i] = (this.skipFromL * (1 - t) + blk.l[this.outPos] * t) * this.outGain;
if (outR) outR[i] = (this.skipFromR * (1 - t) + blk.r[this.outPos] * t) * this.outGain;
this.skipFade--;
@@ -150,6 +236,19 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
if (this.outPos >= this.BLOCK) {
this.outQueue.shift();
this.outPos = 0;
// V42: seam tail→head — sau khi shift hết N block tail pass cũ, block
// đầu pass mới lên front → crossfade WRAP_FADE từ mẫu cuối (che click
// jump nội dung end→start, FX path). Trigger 1 lần mỗi wrap.
if (this.wrapFadeBlocks > 0) {
this.wrapFadeBlocks--;
if (this.wrapFadeBlocks === 0) {
this.skipFade = this.WRAP_FADE;
this.skipFadeTotal = this.WRAP_FADE;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
this.port.postMessage({ type: 'dbg', tag: 'fx_wrap_xfade', t: Math.round(currentTime * 1000), q: this.outQueue.length });
}
}
}
continue;
}
@@ -158,6 +257,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// mẫu output cuối tới 0 (21ms) — vào liền mạch (fadeFrom = lastOut),
// hết click cắt cứng. Resume → crossfade/fade-in (xem dưới).
this.playing = false;
this.wrapFadeBlocks = -1; // V42: underrun — hủy wrap crossfade chờ (resume qua fade-in/crossfade thường)
this.statUnderrun++;
if (!this.silenced) { // đã im lặng (fade xong) → giữ im lặng
this.fadeFromL = this.lastOutL;
@@ -170,7 +270,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// block — hold che khoảng chờ). V25: sau flush (resumeFadeIn=true) chờ
// re-buffer đủ FILL mới resume — chặn loop underrun→flush→resume-1-block
// →underrun (wobble): 1 block phát hết ~5ms → cạn → flush lại.
if (this.outQueue.length >= (this.hasStarted && !this.resumeFadeIn ? 1 : this.FILL)) {
if (this.outQueue.length >= (this.hasStarted ? (this.resumeFadeIn ? this.resumeFill : 1) : this.FILL)) {
// V24: interruption (underrun sau stall) — backlog trong queue là audio
// LẠC HẬU (chơi lại → lag cộng dồn ~1s). KHÔNG chơi backlog: clear
// queue + báo main thread flush (engine xóa input/output ring, main
@@ -186,6 +286,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.outPos = 0;
this.flushPending = true;
this.resumeFadeIn = true;
this.resumeFill = this.FILL; // underrun-stall: giữ cushion FILL đầy đủ (V25 wobble)
this.port.postMessage({ type: 'flush' });
i--;
continue;
@@ -193,6 +294,10 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.playing = true;
this.outPos = 0;
this.holdFadeLeft = 0;
if (this.dbgPlayFirst) {
this.dbgPlayFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_play_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
}
if (this.resumeFadeIn) {
this.resumeFadeIn = false;
if (this.silenced) {
@@ -203,6 +308,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
} else {
// Giữa fade-out → crossfade ngắn từ mẫu hiện tại (không jump về 0).
this.skipFade = 64;
this.skipFadeTotal = 64;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
+2 -2
View File
@@ -41,7 +41,7 @@
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608250700"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608250700"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608250700"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608271630"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608250700"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
<script src="/static/js/app.precompiled.js?v=202608262200" defer></script>
<script src="/static/js/app.precompiled.js?v=202608272300" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+23 -1
View File
@@ -293,7 +293,8 @@ pub fn run() {
bridge_status,
transport_control,
set_fx_chain,
restart_bridge_with_sample_rate
restart_bridge_with_sample_rate,
dbg_log
])
.setup(|app| {
// V10 (FIX_DRAG_DROP_WINDOWS): disable Tauri's built-in drag-drop
@@ -900,6 +901,27 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
Ok(status)
}
/// V28: JS timing diagnostics — append `{unix_ms} {tag} {extra}` to
/// %APPDATA%/SonicForgeDAW/logs/dbg.log (create dirs as needed). Fire-and-forget
/// từ app.jsx (_dbgLog) để định vị delay Play/Stop không cần devtools.
#[tauri::command]
fn dbg_log(tag: String, extra: String) -> Result<(), String> {
let Some(appdata) = std::env::var("APPDATA").ok() else { return Ok(()) };
let dir = Path::new(&appdata).join("SonicForgeDAW").join("logs");
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("dbg.log");
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let line = format!("{} {} {}\n", now_ms, tag, extra);
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) {
let _ = f.write_all(line.as_bytes());
}
Ok(())
}
#[tauri::command]
fn transport_control(app: AppHandle, kind: String, playhead: Option<u32>) -> Result<(), String> {
let state = app.state::<ShmState>();