- Track column: centered 12px buttons, purple active / yellow highlight
- Ghost playback: ghostLayers synced to st.ghostPlayLayers via useEffect;
schedulePianoRollMidi plays ghost notes from active tracks
- Per-track MIDI channels: each track gets a unique channel based on
its index, enabling separate instruments per track
renderBeatOffset was in bars but used as beat offset in rendering.
Multiplying by timeSigNum=4 converts to beats so items appear at
correct session-absolute positions.
- handleSwitchMidiItem no longer saves/sets previous item as ghost
- Removed ghostTrackFilter/filteredGhostLayers
- Track column shows clickable track names that toggle play state
- activePlayTrackIds Set controls which tracks' MIDI are playable
- Context menu flips upward near viewport bottom edge
- Track column (120px) left of keybed: checkboxes per track to
filter ghost notes; active track highlighted
- Session mode: canvas extends to full session duration
- Filtered ghost layers via ghostTrackFilter state (Set of track IDs)
- Fixed bar label seek in session mode (was using wrong offset)
- 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
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
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.
- 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
- App startup fetches full catalog with all instrument presets
- Merges presets into instrumentSelectorData.soundfonts
- Synth button shows presets from cache (no per-SF API call)
- Fallback: if cache miss, fetch individual SF presets on demand
- Remove setSelectionStart/End from simple click (non-drag) path
- Only handleMouseMove (drag) creates/modifies selection
- Sub-tab already correct (no selection set on click)
- Time Ruler click → setSelectionFollowsTempo(false) → time-based
- Tempo Track Lane click → setSelectionFollowsTempo(true) → tempo-based
- Also handles sub-tab ruler (EDITOR tabs)
- Manual toggle button ♪T/⏱T still works for override
- WaveformLane bar grid: integer beat loop (bn*beatDuration) instead
of accumulating t += beatDuration — no floating point drift
- Add selectionFollowsTempo toggle button (♪T/⏱T) in transport bar
- ♪T (tempo): selection recalculates with BPM
- ⏱T (time): selection fixed in seconds regardless of BPM
- TimelineRuler draws bar lines at bn * barDuration positions
- Same integer beat loop as TempoTrackLane (bn from firstBarNum)
- Bar number labels at center of ruler
- Time markers still shown below bar markers
- All use same bpm prop → always aligned with tempo track
- Replace t += beatDuration loop (floating accumulation drifts)
- Iterate by integer beat number: t = bn * beatDuration
- Each bar position computed independently, no cumulative error
- Same fix for snapInterval loop
- Fixes unequal bar widths for BPM like 77, 81
- DOM traversal for scrollLeft stops at sticky wrapper (scrollLeft=0)
- Use scrollLeft prop directly instead of traversing parent DOM
- Affects both TimelineRuler and TempoTrackLane canvas effects
- Bars now correctly space when BPM changes
- Items without length_bars: compute bars from duration/secondsPerBar
- Round to nearest quarter bar for musical accuracy
- Recalculate duration = bars * new secondsPerBar for all items
- Maintains correct bar count for both legacy and new items
- Add length_bars field to midiItems and sections at creation
- useEffect on bpm recalculates duration = length_bars * secondsPerBar
- Fix section creation: use secondsPerBar instead of hardcoded 4s
- Handle existing items without length_bars (no-op)
- If click.y + 260 > window.innerHeight: position bottom instead of top
- Also clamp left to prevent right-edge overflow
- Menu now appears above the click point when there's no room below
- Revert AI item startTime to currentTime (playhead position)
- Context menu: maxHeight 60vh + overflow-y-auto, top clamped to
window.innerHeight - 200 to prevent bottom clipping
- Same fix for both main session and sub-tab context menus
- Replace currentTime with auto-calculated maxEnd from existing items
- Each AI generation starts right after the previous one
- Ensures tracks from different generations align in bar columns
- Build synth_engine object with type, plugin_id, soundfont_bank,
soundfont_program, soundfont_id
- Save to targetTrack.synth_engine so schedulePianoRollMidi reads it
- Pass synthEngine as 3rd arg to applyAITrackInstrument (was missing)
- Remove redundant separate loadSoundFont call (applyAITrackInstrument
chains to selectInstrument which already calls loadSoundFont)
- Wrap TimelineRuler + TempoTrackLane in sticky-top div
- Same sticky pattern for both main session and sub-tab views
- TCP column already has sticky header (top-0) and tempo row (top-10)
- Background color hides tracks scrolling behind ruler
- Transport Loop button now syncs isLooping with active sub-tab
- Auto-computes loopEnd from midi notes, section clips, or audio duration
- Sub-tab selectionStart=0, selectionEnd=computed boundary on loop enable
- Main timeline also auto-derives loop end from track items/clips
- All three tab types: MAIN SESSION, SECTION-TAB, PIANO ROLL
- Frontend serialize: use undefined instead of null for synth_engine
- Backend validate: strip None synth_engine from tracks before schema validation
- Prevents 'None is not of type object' schema error
- getAudioContext() calls SonicSF.init on every call (not just first)
- soundfontPlayer.init retries after 1s if SpessaSynth module not loaded yet
- Auto-loads sgm_v2.01 SF3 on successful init
- Fixes race condition: module script loads after regular scripts
- Add seekPlaybackTo(time) that stops, updates refs, reschedules
MIDI, and restarts playback for both sub-tab and main timeline
- 'Quay lại đầu' and 'Đầu vùng chọn' buttons use seekPlaybackTo
- Sub-tab selection loop only active when st.isLooping is true
- End-of-playback loop uses st.isLooping, not global isLoopingSelection
- SonicSF.stopAll() sets gain 0 and stops oscillators immediately
- Piano roll ruler click during playback now seeks and continues playing
- ADSR envelope uses linearRampToValueAtTime for crack-free release
- stopAll ramps gain to 0 in 20ms before stopping oscillators