diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index 8cf0c90..b5b873a 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -258,16 +258,16 @@ if (!ok) return; } var engKey = (sfId || '') + ':' + bank + ':' + program; - if (!_engineChMap[engKey]) { - if (channel === undefined || channel === null) { + if (channel === undefined || channel === null) { + if (!_engineChMap[engKey]) { var allocCh = this.allocateChannel(bank); _engineChMap[engKey] = allocCh; channel = allocCh; } else { - _engineChMap[engKey] = channel; + channel = _engineChMap[engKey]; } - } else { - channel = _engineChMap[engKey]; + } else if (!_engineChMap[engKey]) { + _engineChMap[engKey] = channel; } var sfHandle = _sfHandleMap.get(sfId); if (sfHandle !== undefined) { @@ -423,18 +423,33 @@ var finalBank = usedBank; var finalProg = usedProg; var finalSfId = synthEngine ? synthEngine.soundfont_id : undefined; - if (_channels[ch] && _channels[ch].program !== undefined) { - finalBank = _channels[ch].bank; - finalProg = _channels[ch].program; - if (_channels[ch].sfId !== undefined) { - finalSfId = _channels[ch].sfId; - } + var cachedCh = _channels[ch]; + // A channel explicitly configured with a soundfont (via + // selectInstrument) is authoritative. But the default channel + // state (bank 0, program 0, no soundfont) must NOT mask the + // track's synth engine or the note's program — otherwise notes + // on a freshly-picked instrument are silently dropped. + if (cachedCh && cachedCh.sfId !== undefined) { + finalBank = cachedCh.bank; + finalProg = cachedCh.program; + finalSfId = cachedCh.sfId; + } else if (!synthEngine && program !== undefined && cachedCh && cachedCh.program !== undefined && cachedCh.program !== 0 && cachedCh.program !== null) { + finalBank = cachedCh.bank; + finalProg = cachedCh.program; + } + // Ensure the soundfont is actually loaded before the note plays. + // Quick instrument pick on a track does not pre-load it, so load + // lazily here and retry the note once the font is ready. + if (finalSfId && !_sfHandleMap.has(finalSfId)) { + self.loadSoundFont(finalSfId).then(function (ok) { + if (ok) doNote(); + }); + return; } // Program change at note time, not call time — ensures correct // instrument for each item regardless of processing order. // Skip if the channel already has this exact instrument (avoids // per-note soundfont reloads that cause audible crackle/glitches). - var cachedCh = _channels[ch]; var progAlreadySet = cachedCh && cachedCh.program === finalProg && cachedCh.bank === finalBank && cachedCh.sfId === finalSfId; if ((synthEngine || program !== undefined) && !progAlreadySet) { var sfHandle = finalSfId ? _sfHandleMap.get(finalSfId) : undefined; diff --git a/app/templates/index.html b/app/templates/index.html index e4290d8..9817285 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -16,7 +16,7 @@ - + diff --git a/wiki.md b/wiki.md index 50d32cd..7a3b051 100644 --- a/wiki.md +++ b/wiki.md @@ -1194,3 +1194,8 @@ - **Tóm tắt thay đổi:** (1) Loop preview giờ chạy liên tục vô hạn cho đến khi nhấn Stop: `startCanvasClock` đọc refs (`isLoopingRef`/`selStartRef`/`selEndRef`) thay vì closure cũ nên việc bật loop giữa lúc đang play được phản ánh ngay, playhead wrap đúng theo `loopStartSec` (trừ offset gốc), không còn tự `stopMediaPlayback()` khi hết selection; `playMidiPreview` dùng `isLoopingRef.current` khi lập lịch interval (trước đây closure `isLooping` cũ → bật loop không tạo interval) và hủy interval khi tắt loop; `toggleLoop` sync `isLoopingRef` ngay + cập nhật `loopStart`/`loopEnd` cho audio đang phát theo selection hiện tại; `playSelected` dùng refs cho loop points/startOffset. (2) Container render canvas thêm `p-0.5` (2px) để quét chọn vùng không vượt ra ngoài khung preview. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` (babel) thành công. Smoke: chọn file audio → quét chọn 1 đoạn → bật Loop → phát liên tục vùng chọn đến khi nhấn Stop (playhead wrap đúng, selection overlay vẫn hiển thị khi rAF redraw nhờ `drawSelStart`/`drawSelEnd`). MIDI: bật loop khi đang preview → interval reschedule vùng chọn. + +### [2026-08-03 10:15] Task: Fix MIDI ARM không phát âm thanh sau khi load instrument +- **Tóm tắt thay đổi:** Sửa `soundfontPlayer.js` để phím MIDI trên track ARM phát đúng instrument: (1) `_playNoteFluid` không còn để state mặc định của channel (bank 0, program 0, không sfId) che mất `synth_engine` của track → `finalSfId` luôn đúng; (2) tự load soundfont lười (lazy) ngay trong đường phát note nếu font chưa vào `_sfHandleMap` (track pick nhanh từ dropdown không gọi `selectInstrument` → trước đây rơi vào `bank_select`/`program_change` trên synth không có soundfont → câm lặng); (3) `selectInstrument` không còn đổi hướng channel khi đã truyền channel tường minh (sửa lỗi 2 track dùng chung instrument bị giật channel). Bump version cache `soundfontPlayer.js` trong `index.html`. +- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html` +- **Ghi chú/Test (nếu có):** Test logic bằng harness `node /tmp/kilo/test_sonicsf_fix.js` (mô phỏng FluidSynth): S1 lazy-load + program_select + noteon, S2 channel cấu hình sẵn không sfload lại, S3 program-only fallback, S4 no-instrument vẫn silent, S5 2 track cùng instrument giữ channel riêng — ALL PASSED. Cần hard-refresh trình duyệt để nạp `soundfontPlayer.js` bản mới.