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.