Merge main (V45.1-32 fixes: ReferenceError guard, crackle/solo/seek fixes) vao standalone-shm-bridge (giu FILL 96)

This commit is contained in:
2026-09-01 16:03:28 +07:00
25 changed files with 1291 additions and 287 deletions
+525 -138
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+7 -2
View File
@@ -16,6 +16,7 @@
var _ctx = null;
var _initialized = false;
var _useWorklet = false;
var _workletStats = null; // stats tu worklet (overrun/underrun) — getDiag
var _dest = null; // destination gain node da noi (chong connect trung)
function _getCtx() {
@@ -31,7 +32,7 @@
console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback');
return false;
}
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608272301';
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608311858';
_ctx.audioWorklet.addModule(url).then(function () {
if (!_initialized) return; // disconnect() happened while loading
if (_useWorklet) return; // already set up (init retry)
@@ -42,6 +43,9 @@
outputChannelCount: [2]
});
_workletNode.connect(_gainNode);
_workletNode.port.onmessage = function (ev) {
if (ev.data && ev.data.type === 'stats') _workletStats = ev.data;
};
_useWorklet = true;
// drain pre-buffer into worklet (order preserved)
while (_preBuffer.length) {
@@ -168,7 +172,8 @@
hasWorkletNode: !!_workletNode,
preBufferLen: _preBuffer.length,
chunksLen: _chunks.length,
gain: _gainNode ? _gainNode.gain.value : -1
gain: _gainNode ? _gainNode.gain.value : -1,
ws: _workletStats
};
}
};
+39 -8
View File
@@ -1,22 +1,30 @@
// 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 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).
// - Ring block 256 mau, toi da 32 block (~170ms @48k) — hap thu jitter pump
// (V45.27: 16->32 de dung crackle VST2 loop/tremolo — render loop C++ bi
// stall khi instrument xu li loop/tremolo nang CPU; 8->16 truoc do da dung
// tremolo crackle, 16 van chua du cho stall >85ms). Flush o play start
// (app.jsx) xoa ring nen do tre khong tang: ring thuong can o preview.
// - 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).
// - Stats: dem overrun/underrun, gui ve main thread moi ~0.5s — kiem tra
// con crackle nao la do ring can (underrun) hay ring tran (overrun).
class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
constructor() {
super();
this._ring = [];
this._MAX = 8;
this._MAX = 32;
this._gain = 1.0;
this._lastOutL = 0.0;
this._lastOutR = 0.0;
this._skipFade = 0; // >0: crossfade dang chay sau overrun drop
this._skipFade = 0; // >0: con lai cua crossfade dang chay sau overrun drop
this._fadeLen = 0; // tong chieu dai crossfade hien tai (cap 1024)
this._skipFromL = 0.0;
this._skipFromR = 0.0;
this._wasUnderrun = false;
this._statAccum = 0;
this._stats = { overruns: 0, underruns: 0 };
this.port.onmessage = (e) => {
const m = e.data;
if (!m) return;
@@ -24,13 +32,19 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
this._ring.push({ l: m.l, r: m.r });
while (this._ring.length > this._MAX) {
// overrun: bo block cu nhat; lan consume toi se crossfade ranh gioi
// V45.32: crossfade tich luy — moi block drop cong 256 mau fade, cap
// 1024 (~21ms) thay vi co dinh 64 mau (1.3ms qua ngan -> blip/wow khi
// drop loat block o chord nhieu voice = crackle VST2 tremolo/loop)
this._ring.shift();
this._skipFade = 64;
this._skipFade = Math.min(this._skipFade + 256, 1024);
this._fadeLen = this._skipFade;
this._stats.overruns++;
}
} else if (m.type === 'flush') {
this._ring.length = 0;
this._gain = 1.0;
this._skipFade = 0;
this._fadeLen = 0;
}
};
}
@@ -43,12 +57,14 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
let w = 0;
while (w < n) {
if (this._ring.length) {
if (this._wasUnderrun) this._wasUnderrun = false;
const blk = this._ring[0];
const avail = blk.l.length;
const take = Math.min(avail, n - w);
if (this._skipFade > 0) {
const done = this._fadeLen - this._skipFade;
for (let i = 0; i < take; i++) {
const t = i / 64;
const t = (done + i) / this._fadeLen;
L[w + i] = (this._lastOutL * (1 - t) + blk.l[i] * t) * this._gain;
R[w + i] = (this._lastOutR * (1 - t) + blk.r[i] * t) * this._gain;
}
@@ -70,7 +86,8 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
blk.r = blk.r.subarray(take);
}
} else {
// underrun: fade ve 0 (khong click)
// underrun: fade ve 0 (khong click) — dem 1 lan moi lan roi vao trang thai trong
if (!this._wasUnderrun) { this._wasUnderrun = true; this._stats.underruns++; }
for (let i = w; i < n; i++) {
this._gain = Math.max(0, this._gain - 1.0 / 256.0);
L[i] = this._lastOutL * this._gain;
@@ -81,6 +98,20 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
w = n;
}
}
// Stats: gui ve main thread moi ~0.5s (24000 mau @48k) — log overrun/underrun
this._statAccum += n;
if (this._statAccum >= 24000) {
this._statAccum = 0;
try {
this.port.postMessage({
type: 'stats',
overruns: this._stats.overruns,
underruns: this._stats.underruns,
ring: this._ring.length,
max: this._MAX
});
} catch (e) {}
}
return true;
}
}
@@ -17,8 +17,13 @@
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
onStatusChange: function (cb) { this._statusCb = cb; },
_notifyStatus: function (connected) {
this.isBridgeConnected = !!connected;
if (this._statusCb) this._statusCb({ connected: !!connected });
connected = !!connected;
// Loop guard (V45.7): poller queryStatus moi 400ms tung goi cb du connected
// khong doi -> App reset map + reload toan bo track -> mark loading lai ->
// poller lai thay loading -> LOAD skip vo han. Chi fire khi TRANG THAI DOI.
if (this.isBridgeConnected === connected) return;
this.isBridgeConnected = connected;
if (this._statusCb) this._statusCb({ connected: connected });
},
/** Fix C: re-establish bridge routing once audio frames arrive again
+23 -2
View File
@@ -6,6 +6,10 @@
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;
// V45.26: dem note live dang giu + lan live event gan nhat — quyet dinh co
// clear master FX outQueue khong (chi idle->active; KHONG khi dang phat lien tuc).
var _liveHeldCount = 0;
var _lastLiveEventAt = 0;
var router = {
bridgeConnected: false,
@@ -59,7 +63,23 @@
// 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) {} }
// V45.26: chi clear khi THAT SU idle -> active (khong con note dang giu
// VA im lang >500ms). Truoc day clear MOI note-on live -> rapid 1/8 lien
// tuc: moi note wipe outQueue (dang chua tail note truoc + resume fade-in)
// -> wobble/crackle (bug user). Note dang ngan / chuoi note nhanh -> giu
// queue -> tail note truoc khong bi cat.
if (opts.live) {
try {
var _nowEv = (window.performance && performance.now) ? performance.now() : Date.now();
if (opts.cmd === 'NOTE_ON') {
if (_liveHeldCount === 0 && (_nowEv - _lastLiveEventAt) > 500 && window.__clearMasterFxQueue) window.__clearMasterFxQueue();
_liveHeldCount++;
} else if (opts.cmd === 'NOTE_OFF') {
if (_liveHeldCount > 0) _liveHeldCount--;
}
_lastLiveEventAt = _nowEv;
} 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;
@@ -73,7 +93,7 @@
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'));
window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' p=' + (pitch === undefined ? 'n/a' : pitch) + ' 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()) {
@@ -85,6 +105,7 @@
/** Stop/panic -> bridge transport panic (flush all notes) + fallback local stopAll. */
panic: function () {
_liveHeldCount = 0;
if (this.isBridgeActive() && window.NativeBridgeService.transport) {
window.NativeBridgeService.transport('panic');
}
+60 -4
View File
@@ -57,6 +57,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
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.latencySamples = 0; // G3: latency chain thật (engine qua WS) — align wrap fade theo processed tail
this.wrapMode = false; // V45.3: dang trong wrap (clear_queue loop) — resume underrun bo flush + re-buffer nho
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;
@@ -79,6 +80,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.resumeFadeIn = true;
this.resumeFill = this.RESUME_FILL;
this.silenced = false;
this.wrapMode = 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;
@@ -88,6 +90,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
return;
}
if (d && d.type === 'clear_queue') {
this.clearMode = d.mode; // V45.19: nho mode de tag fx_seek_first tai dbgPlayFirst
// 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,
@@ -110,9 +113,51 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// G3: align processed-tail — tail pass cũ còn in-flight trong bridge
// (FX latency thật từ engine) chưa về queue; đếm đủ block tới seam
// (block đầu pass mới lên front) → crossfade đúng chỗ jump end→start.
// V45.2 (option B): cắt bớt tail cũ giữ KEEP block thay vì giữ nguyên
// FILL 24 (~128ms) → downbeat pass mới không trễ ~110ms. skipFade
// crossfade từ lastOut che jump cắt front; wrapFadeBlocks đếm lại từ
// KEEP để seam crossfade vẫn đúng lúc.
// V45.3: KEEP 4 -> 16 (~85ms): KEEP 4 (~21ms) engine (stall bridge
// 68-138ms sau transport play) khong refill kip -> queue can -> underrun
// -> flush + re-buffer FILL 24 = gap ~110-235ms moi wrap (WORSE hon
// truoc V45.2: downbeat tre 106-128ms nhung khong gap). KEEP 16 che
// stall dien hinh 68-82ms lien tuc (khong gap, downbeat ~85ms); stall
// dai hon roi vao nhanh resume wrap (bo flush + resumeFill 8).
const KEEP = 16;
this.wrapMode = true;
if (this.outQueue.length > KEEP) {
const head = this.outQueue.length - KEEP;
this.outQueue.splice(0, head);
this.outPos = 0;
if (this.playing) {
this.skipFade = this.WRAP_FADE;
this.skipFadeTotal = this.WRAP_FADE;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
}
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
return;
}
if (d.mode === 'seek') {
// V45.18: seek khi play master FX active — wipe queue cu (audio cu
// choi tiep 0-50ms sau click) + resume nhanh RESUME_FILL 8 (~43ms)
// thay vi FILL 24 (~128ms) — het "am tre 1 beat" (hit dau roi vao
// vung cam 128ms). Giong resume wrap V45.4 (proven khong gap).
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1;
this.wrapMode = false;
this.resumeFadeIn = true;
this.resumeFill = 8;
this.silenced = false;
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.port.postMessage({ type: 'dbg', tag: 'fx_seek_clear', t: Math.round(currentTime * 1000), q: 0 });
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
@@ -123,6 +168,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
this.wrapMode = false; // V45.3: play/seek — het context wrap
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
@@ -300,21 +346,31 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// tại (giữa fade) — hết jump.
const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
if (wasInterrupted) {
// V45.3: wrap (loop restart) — engine da transport('play') tu main
// (D17): block moi = audio pass moi, KHONG phai backlog stale (boi
// worklet flush gui 'flush' xuong engine ring = lam mat tac dung D17
// skip-flush o stopAllPlayback -> gap giua seam). Bo flush (round-trip)
// + re-buffer nho hon FILL 24 (~128ms) -> resume sau stall som hon
// (~43ms) -> gap wrap giam tu ~110-235ms xuong con vai chuc ms.
const _wrap = this.wrapMode;
this.outQueue.length = 0;
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' });
this.resumeFill = _wrap ? 8 : this.FILL;
this.flushPending = _wrap ? false : true;
if (!_wrap) this.port.postMessage({ type: 'flush' });
i--;
continue;
}
this.playing = true;
this.outPos = 0;
this.holdFadeLeft = 0;
this.wrapMode = false; // V45.3: resume xong — ve che do thuong (underrun sau nay flush nhu cu)
if (this.dbgPlayFirst) {
this.dbgPlayFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_play_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
// V45.19 Bug 1b: seek -> tag fx_seek_first (main thread snap khong can
// guard 50ms — seek resume nhanh ~43ms < 50ms). Wrap/play -> fx_play_first.
this.port.postMessage({ type: 'dbg', tag: (this.clearMode === 'seek' ? 'fx_seek_first' : 'fx_play_first'), t: Math.round(currentTime * 1000), q: this.outQueue.length, m: this.clearMode || '' });
}
if (this.resumeFadeIn) {
this.resumeFadeIn = false;