_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.