diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index 8359a82..bb68e95 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -141,7 +141,15 @@ if (!_useScriptNode) { try { - _workletNode = new AudioWorkletNode(_audioCtx, 'fluidsynth-bridge'); + // Force stereo output regardless of the device's + // channel count — FluidSynth renders stereo, and a + // mono output would crash the worklet (out[1] undefined). + _workletNode = new AudioWorkletNode(_audioCtx, 'fluidsynth-bridge', { + numberOfOutputs: 1, + outputChannelCount: [2], + channelCount: 2, + channelCountMode: 'explicit' + }); _workletNode.connect(_gainNode); console.log("[SonicSF] AudioWorklet node connected via gain"); _startRenderLoop(); diff --git a/app/static/js/worklets/fluidsynth-bridge.js b/app/static/js/worklets/fluidsynth-bridge.js index 3d4a18d..ce0d33e 100644 --- a/app/static/js/worklets/fluidsynth-bridge.js +++ b/app/static/js/worklets/fluidsynth-bridge.js @@ -15,17 +15,24 @@ class FluidSynthBridge extends AudioWorkletProcessor { process(inputs, outputs) { const out = outputs[0]; - if (!out) return true; + if (!out || out.length === 0) return true; this.called++; + const numCh = out.length; const len = out[0].length; const qL = this.leftQ; const qR = this.rightQ; let fi = 0; let si = 0; + // Handle any output channel count (mono devices produce 1 channel, so + // out[1] may be undefined — never write into a missing channel). for (let i = 0; i < len; i++) { - if (fi >= qL.length) { out[0][i] = 0; out[1][i] = 0; continue; } - out[0][i] = qL[fi][si]; - out[1][i] = qR[fi][si]; + if (fi >= qL.length) { + for (let c = 0; c < numCh; c++) out[c][i] = 0; + continue; + } + for (let c = 0; c < numCh; c++) { + out[c][i] = c % 2 === 0 ? qL[fi][si] : qR[fi][si]; + } si++; if (si >= qL[fi].length) { fi++; si = 0; } } diff --git a/app/templates/index.html b/app/templates/index.html index 5d90e5c..bd887e8 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -16,7 +16,7 @@ - + diff --git a/wiki.md b/wiki.md index 098d098..bae5153 100644 --- a/wiki.md +++ b/wiki.md @@ -1229,3 +1229,8 @@ - **Tóm tắt thay đổi:** (1) **ReferenceError `assignTrackMidiChannel is not defined`**: allocator channel (trackMidiChannelsRef/ensureTrackMidiChannel/assignTrackMidiChannel) đặt bên trong `App`, nhưng các sub-component piano roll (`PianoRollTabEditor` — canvas preview, click note, draw/paint brush, keybed) là module-level → không thấy hàm → lỗi khi render/play. Chuyển allocator lên **module-level** (đầu file, trước PianoRollTabEditor), App dùng chung bản đó (bỏ khai báo `useRef` cục bộ trong App; reset `trackMidiChannelsRef.current = {}` khi load dự án vẫn dùng chung object). (2) **Deprecation ScriptProcessorNode**: bật **AudioWorklet** `fluidsynth-bridge` làm mặc định (`_useScriptNode = false`), chỉ fallback ScriptProcessor khi `new AudioWorkletNode`/`addModule` thất bại → hết warning deprecation; `_startRenderLoop` (queue 16 block × 512 sample ~186ms headroom, interval 2× frame rate) vốn đã được thiết kế cho worklet. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html` - **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK, bundle load smoke test (stub React/ReactDOM/window) OK. Harness `test_sonicsf_fix.js` với mock AudioWorkletNode + performance: `workletCreated=1 scriptProcCreated=0` (worklet được dùng), S1-S5 vẫn PASS; trước đó (thiếu mock performance) xác nhận fallback ScriptProcessor hoạt động. 4 harness còn lại vẫn PASS. Hard refresh. + +### [2026-08-03 12:30] Task: Fix crash AudioWorklet - "Cannot set properties of undefined (setting '0')" khi output mono +- **Tóm tắt thay đổi:** `FluidSynthBridge.process` luôn ghi stereo `out[0]`/`out[1]`; khi thiết bị/context xuất mono (chỉ 1 channel), `out[1]` là `undefined` → `out[1][i] = ...` ném TypeError, dừng audio. Fix: (1) `fluidsynth-bridge.js` xử lý mọi số channel output (vòng lặp theo `out.length`, chẵn=lấy L, lẻ=lấy R, trường hợp cạn queue ghi 0 cho từng channel); (2) `soundfontPlayer.js` tạo `AudioWorkletNode` với `outputChannelCount: [2]`, `channelCount: 2`, `channelCountMode: 'explicit'` để ép stereo (FluidSynth render stereo) thay vì để device quyết định. +- **Các file ảnh hưởng:** `app/static/js/worklets/fluidsynth-bridge.js`, `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html` +- **Ghi chú/Test (nếu có):** Harness `node /tmp/kilo/test_worklet.js` mô phỏng process(): (1) output mono 1 channel → mirror left, không crash; (2) stereo → L/R đúng; (3) queue cạn → silence. Tất cả PASS. 5 harness khác vẫn PASS. Worklet + soundfontPlayer `node --check` OK. Không cần build bundle (2 file served trực tiếp). Hard refresh.