// fx-rt-worker.js — M25: WebSocket FX realtime chạy trên Web Worker. // Động lực: main thread bị stall (rAF/React/render, longtask 375ms lúc cold // start) làm trễ cả WS receive lẫn postMessage → worklet queue cạn → underrun // nghe được (u1/u10). Chuyển WS + deinterleave sang worker; binary output đi // THẲNG worker→worklet qua MessageChannel (directPort) — delivery không phụ // thuộc event loop main → stall main không còn tạo hole âm thanh. Text // (latency/flush_done) vẫn forward main (PDC/UI). Input: worklet gửi thẳng // qua directPort ngược chiều (cùng channel, 2 chiều). // // Protocol main→worker: // {kind:'init', wsUrl, port} port = MessagePort tới worklet (transfer) // {kind:'send', text|buf} send 1 frame (text cmd hoặc binary input) // {kind:'close'} // Protocol worker→main: // {kind:'open'} | {kind:'close'} | {kind:'text', text} (chỉ text; binary // không bao giờ chạm main) let ws = null; let directPort = null; let flushing = false; // từ lúc gửi cmd 'flush' tới flush_done — drop output stale (V24, cùng logic fxRt.flushing phía main cũ) self.onmessage = (ev) => { const d = ev.data; if (!d) return; if (d.kind === 'init') { directPort = d.port || null; if (directPort) { // Input burst 2048 floats (4 block) — tách 4 WS frame 512 floats như // fxRtSendFrames cũ (frame = 1 block 256 stereo; slice() copy đúng // 2048B, KHÔNG subarray().buffer = gửi cả backing 8192B). directPort.onmessage = (e2) => { const d2 = e2.data; if (!d2 || d2.type !== 'in' || !(d2.data instanceof ArrayBuffer)) return; if (!ws || ws.readyState !== 1) return; try { const f = new Float32Array(d2.data); for (let i = 0; i < f.length; i += 512) { const c = new Float32Array(512); c.set(f.subarray(i, i + 512)); ws.send(c.buffer); } } catch (e) {} }; } try { ws = new WebSocket(d.wsUrl); } catch (e) { self.postMessage({ kind: 'close' }); return; } ws.binaryType = 'arraybuffer'; ws.onopen = () => self.postMessage({ kind: 'open' }); ws.onmessage = (mev) => { if (typeof mev.data === 'string') { try { const c = JSON.parse(mev.data); if (c && c.cmd === 'flush_done') { flushing = false; // Xóa cờ worklet NGAY trên cùng channel (trước mọi 'out' kế — FIFO) // — không chờ main forward (main có thể bận/stall). if (directPort) directPort.postMessage({ type: 'flush_done' }); } } catch (e) {} self.postMessage({ kind: 'text', text: mev.data }); return; } if (!(mev.data instanceof ArrayBuffer) || flushing || !directPort) return; // Deinterleave stereo (giống fxRtWsOnMessage cũ) → gửi thẳng worklet. const f = new Float32Array(mev.data); const half = f.length / 2; const l = new Float32Array(half); const r = new Float32Array(half); for (let i = 0, j = 0; i < half; i++) { l[i] = f[j++]; r[i] = f[j++]; } directPort.postMessage({ type: 'out', l, r }); }; ws.onclose = () => self.postMessage({ kind: 'close' }); ws.onerror = () => { try { ws.close(); } catch (e) {} }; return; } if (d.kind === 'send') { if (!ws || ws.readyState !== 1) return; if (d.text !== undefined) { try { const c = JSON.parse(d.text); if (c && c.cmd === 'flush') flushing = true; } catch (e) {} try { ws.send(d.text); } catch (e) {} } else if (d.buf !== undefined) { try { ws.send(d.buf); } catch (e) {} } return; } if (d.kind === 'close') { try { if (ws) ws.close(); } catch (e) {} } };