_playNoteFluid called program_change synchronously at
call time, not at scheduled note time. startTrackPlayback
processes all MIDI items first, then all sections -> each
note's program_change overwritten by the last type processed.
All notes played with section instrument regardless of
actual time position. Moved program_change into doNote
(setTimeout callback) so it fires at correct time.
_playNoteFluid no-instrument branch previously called
_playNoteFallback (sine wave oscillator). Changed to
silent return. User explicitly requested no default sound.
When a track has no instrument configured, _playNoteFluid called
program_change(ch, 0). FluidSynth searches ALL loaded SoundFonts
to resolve program 0 — if only another track's SoundFont is loaded,
that SF's program 0 gets applied to the wrong track.
Fix: use oscillator fallback directly instead of program_change.
_playNoteFluid: when no synthEngine and no program, fallback to
program 0 on channel (default piano) so unconfigured tracks still
produce sound via FluidSynth
soundfontPlayer.js:
- selectInstrument: respect passed channel (don't allocate new one)
- _playNoteFluid: only use _engineChMap when channel is undefined
ghostNoteExtractor.js:
- Include ALL notes from overlapping items (not clipped)
- relative_start_beat can be negative (notes before window)
- Keep original duration instead of clamping
Changed itemEndBeat <= windowStartBeat → < and
noteAbsEnd <= windowStartBeat → < so abutting items
(Item 1 ends at bar 1, Item 2 starts at bar 1) show
ghost notes from the adjacent item.
- MIDI ghost notes: all non-selected items rendered at 25% opacity
- Dropdown at tab title: switch active edit target across all tracks
- Session Sync mode (default): viewport aligned to session bars
- Isolated mode toggle: bar 0, no ghost notes
- Ghost toggle: show/hide ghost layer (only in session mode)
- Auto-scroll to session position in session sync mode
- Bar labels show absolute session bar numbers
Khi load SF2, preset lookup của program_change dùng search order
(lần lượt qua các SF đã load). Track 1 load SF2→SF2 presets override
SF1 trên tất cả channel, track 2 chọn nhầm preset của SF2.
Fix: program_select(synth, ch, sfHandle, bank, prog) chỉ định rõ
SoundFont ID, không phụ thuộc search order.
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.
- 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
- 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