diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js
index 57d6763..9936d34 100644
--- a/app/static/js/services/soundfontPlayer.js
+++ b/app/static/js/services/soundfontPlayer.js
@@ -173,14 +173,11 @@
stopNote: function (channel, pitch) {
if (channel < 0 || channel > 15) return;
if (_initialized && _synthInstance) {
- // Ensure sustain is OFF before noteOff (SpessaSynth uses >=8192 threshold)
try { _synthInstance.controllerChange(channel, 64, 0); } catch (e) {}
- // Standard MIDI noteOff for the specific pitch — enters SoundFont release envelope
try { _synthInstance.noteOff(channel, pitch); } catch (e) {}
- // Force immediate voice kill: CC 120 (All Sound Off) sets isActive=false instantly,
- // bypassing the release envelope. CC 123 (All Notes Off) merely enters the release
- // phase which respects long release tails (2-5s for sustained instruments).
+ try { _synthInstance.noteOn(channel, pitch, 0); } catch (e) {}
try { _synthInstance.controllerChange(channel, 120, 0); } catch (e) {}
+ try { _synthInstance.post({ channelNumber: channel, type: "stopAll", data: 1 }); } catch (e) {}
}
},
@@ -321,7 +318,10 @@
stopAll: function () {
if (_initialized && _synthInstance) {
- try { for (let ch = 0; ch < 16; ch++) _synthInstance.controllerChange(ch, 120, 0); } catch (e) {}
+ for (let ch = 0; ch < 16; ch++) {
+ try { _synthInstance.controllerChange(ch, 120, 0); } catch (e) {}
+ try { _synthInstance.post({ channelNumber: ch, type: "stopAll", data: 1 }); } catch (e) {}
+ }
}
// Cancel all scheduled future notes
while (_scheduledNotes.length > 0) {
diff --git a/app/templates/index.html b/app/templates/index.html
index cdfb5c1..3143ec0 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -27,7 +27,7 @@
-
+
diff --git a/wiki.md b/wiki.md
index 2fedf6a..d0e47f7 100644
--- a/wiki.md
+++ b/wiki.md
@@ -426,3 +426,8 @@
- **Tóm tắt thay đổi:** Root cause: `stopNote()` used CC 123 (All Notes Off) which merely enters SoundFont's release envelope. Sustained instruments like Tremolo Strings (GM#44) have 2-5s release tails. Fix: replaced CC 123 with CC 120 (All Sound Off) in `stopNote()` and `stopAll()`. CC 120 sets `voice.isActive = false` immediately, bypassing the release envelope entirely, so the note stops instantly on key release.
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`
- **Ghi chú/Test (nếu có):** Test với MIDI keyboard + Tremolo Strings instrument: note tắt ngay khi release key. CC 120 tác dụng lên toàn bộ channel (kill all voices) — phù hợp với keyboard preview use-case.
+
+### [2026-07-27 12:15] Task: Fix SpessaSynth loop voice not releasing (layer 2)
+- **Tóm tắt thay đổi:** CC 120 vẫn không đủ vì SpessaSynth 4.3.1 AudioWorklet có bug: looped voices trong MIDI message pipeline xử lý CC 120 sai (`processMessage`). Fix: thêm `noteOn(ch, pitch, 0)` (MIDI noteOff alternate path) + `_synthInstance.post({channelNumber:ch, type:"stopAll", data:1})` gửi lệnh trực tiếp đến worklet qua `handleMessage` — bypass hoàn toàn MIDI pipeline.
+- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
+- **Ghi chú/Test (nếu có):** Cần clear cache browser (index.html cache-bust param updated).