Cùng MIDI pitch 60, track 1→ch0, track 2→ch1 cùng key '0:60'
→ single value bị overwrite, stopNote chỉ stop 1 channel.
Fix: _activeNotes[key] = [ch1, ch2, ...], stopNote iterate all.
Line 378: 'if (channel===undefined) channel=ch' gán channel = FluidSynth
channel (2), sau đó _activeNotes key dùng channel (2) thay vì MIDI
channel (0). stopNote lookup bằng 0 → key mismatch.
Fix: _origChannel = channel ngay đầu hàm, dùng _origChannel cho key.
1. precompiled: find(t=>isArmed) → filter(t=>isArmed) loop all tracks
2. soundfontPlayer: _activeNotes[ch:pitch] = mappedCh lưu khi noteOn
3. stopNote lookup mappedCh từ _activeNotes thay vì dùng MIDI ch 0
→ Fix: đổi SF track → channel mới → noteOff vẫn tìm đúng voice
_engineChMap: key=sfId:bank:prog → allocated MIDI channel.
Mỗi track ARM riêng biệt có engKey khác nhau → allocated channel
riêng (0-8 round-robin). _playNoteFluid lookup channel từ map
thay vì dùng MIDI channel gốc (luôn 0).
Trước đây chỉ schedule noteOff khi delay > 0 (notes scheduled).
Virtual piano keyboard play note immediate (delay===0) với duration=500ms
nhưng không có noteOff → âm thanh play mãi không tắt.
Fix: schedule noteOff khi 0 < durationMs < 60000, không phụ thuộc delay.
app.precompiled.js được build từ app.jsx cũ (10:54), thiếu:
- SonicSF.stopNote() trong MIDI note-off handler → note không tắt
- duration 500ms (quá ngắn, nên 60000ms = hold)
Patch thủ công bằng Python replace trên minified code.
- Dùng _fluid_synth_all_sounds_off thay vì CC120
- Log return value của _fluid_synth_noteoff (0=OK, -1=fail)
- Log ch+pitch trong stopNote
- Sửa printErr filter (bỏ check 'stderr')
- synth.gain=2.0 (thay vì 0.5 + set_gain 1.0) để tăng volume
- Pre-zero HEAPF32 buffer trước khi gọi write_float
- Log peak, avg, và value từ _fluid_synth_get_gain để debug
- _fluid_synth_set_gain(1.0) sau khi tạo synth (đảm bảo gain > 0)
- Log noteOn(ch, pitch, vel) mỗi lần play
- Log peak frame đầu tiên khi render > 0.001
- Error log trong catch pushFrame
- Sửa _playNoteFluid: dùng usedBank để xác định ch khi undefined
process() gán Float32Array (cả frame 512 samples) vào out[0][i]
thay vì sample riêng lẻ → NaN → silence.
Fix: track fi (frame index) + si (sample index), đọc qL[fi][si].
- Gọi _fluid_synth_error để lấy error message từ C engine
- Fallback từ .sf3 → .sf2 nếu sfload thất bại
- TOTAL_MEMORY=256MB cho SoundFont 11MB
- unlink file cũ trước writeFile tránh MEMFS conflict
- Log MEMFS stat để debug
Module script (type=module) có document.currentScript === null,
nên libfluidsynth không thể tự locate .wasm file. Fix: pass
locateFile() trả về URL .wasm tương ứng với URL .js đã load.
fluidsynthLoader.js: expose __FluidSynthLocateWasm() trả về
FLUIDSYNTH_JS_URL.replace('.js', '.wasm'). Dùng polling
setInterval thay vì resolve callback cho đáng tin cậy hơn.
soundfontPlayer.js: pass locateFile factory khi gọi factory.
Root cause: SpessaSynth's CC123 (All Notes Off) calls stopAllNotes(false)
which enters the SoundFont release envelope. Tremolo Strings (GM#44) and
similar sustained instruments have 2-5s release tails, causing notes to
continue playing after MIDI key release.
Fix: Replace CC 123 with CC 120 (All Sound Off) in stopNote() and
stopAll(). CC 120 calls stopAllNotes(true) which sets voice.isActive=false
instantly, bypassing the release envelope entirely.
Removed redundant noteOff(ch,pitch,0) and resetAllControllers(CC121)
which unnecessarily reset volume/pan/expression on the channel.
- Track sustain state per channel via _sustainStates[]
- sustainActive(channel) returns boolean
- MIDI NoteOn: only send CC123 if !sustainActive(ch)
- Preserves sustained notes when playing new notes
- Scheduled notes (delay>0, timeline playback): still set auto noteOff
- Immediate notes (delay=0, MIDI keyboard): keep sounding until stopNote or stopAll
- Remove _scheduledNotes push/pop for immediate notes (no timeout to track)
- NoteOn: durationMs=60000 (1 phút, giữ cho đến khi NoteOff)
- NoteOff: stopNote(ch, pitch) gọi synthInstance.noteOff() ngay
- MIDI channel (msg.data[0] & 0x0F) truyền đúng vào playNote
- CC (sustain/modulation) + pitch bend forward qua SpessaSynth
- Add controllerChange forwarder for all CC messages (cmd 0xB)
- Add pitchBend method to SonicSF + MIDI handler (cmd 0xE)
- CCs routed to _synthInstance.controllerChange(ch, cc, val)
- Pitch bend routed to _synthInstance.pitchBend(ch, 14bit-val)
- Remove small Synth button (already done earlier)
- Large Synth button now calls openInstrumentSelector (instrument modal)
- Fix _initPromise stale cache: reset to null when init fails
- Module not loaded yet → retries on next select instead of stuck
- openInstrumentSelector builds flat list of all instrument presets
- Modal shows all instruments with search bar
- Each preset shows sf name + program name
- Remove old two-step (SF list -> presets) navigation
- Fix pre-existing closing paren mismatch in PluginManagerModal
- catalog keys are lowercased (soundfont_inspector: .lower())
- sf.id from _scan_soundfonts has original case (SGM_v2.01)
- Use catalog[sfId.toLowerCase()] || catalog[sfId] to match both