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:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user