Merge main (V45.1-32 fixes: ReferenceError guard, crackle/solo/seek fixes) vao standalone-shm-bridge (giu FILL 96)
This commit is contained in:
+525
-138
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user