updateActiveTracksRef checks sessionTabs by currentActiveTab.
When activeTab is a PIANO_ROLL sub-tab (midi_xxx), none matches
-> falls to setTracks (main). Instrument changes are lost.
Fix: also check subTabs for PIANO_ROLL type, resolve parent
session tab via parent_tab_id, route to setSessionTabs.
Ctrl+Click handler toggled selection + set pending drag.
Once selected, drag system treated it as 'move selection'
instead of 'copy'. Removed selection toggle - Ctrl+Click
now only sets pending drag for copy operation.
When PIANO_ROLL sub-tab is opened from SECTION-TAB,
activeTab switches to midi_xxx, so activeTracks fell
back to main project tracks. Section trackIds don't
exist in main tracks -> dropdown empty, ghost layers
empty, instrument data wrong.
Fix: check subTabs for PIANO_ROLL type, resolve parent
section tab from parent_tab_id, return its tracks.
handleEditSectionInTab overwrote midiItems: [] when cloning
section.tracks, stripping all MIDI data from section context.
PianoRollTabEditor uses activeTracks which resolves section
tracks -- midiItems must be preserved for dropdown switcher,
ghost note extraction, and instrument settings to work.
Scanner: new instance with existing sf_scan_state.json
did not populate in-memory _catalog. Now re-inspects
unchanged files to fill catalog on first scan.
Frontend: left column reads instrumentSelectorData.
soundfonts instead of sfPresets (null when no presets).
Click SF in left column triggers on-demand fetch.
Modal max-w-md -> max-w-4xl. Font 14px (text-sm).
Left column: soundfont list. Right column: instruments
of selected SF. All Instruments view when none selected.
Search filters both columns.
Background daemon (30s interval) tracks scanned files via
sf_scan_state.json (size+mtime). Only inspects new/changed SFs.
Catalog served from scanner cache, no full re-scan per request.
Upload/delete trigger immediate re-scan.
Ctrl+click toggle selection THEN drag started -> selIds.has(itemId)
returned false because the item was just removed from snapshot.
Fix: snapshot selectedItemIds BEFORE toggle, pass to pendingDrag.
Drag always copies the original group; toggle is only visual.
Same bug as copy: return statement only updated
targetTrackId/drag.trackId, discarding items from other tracks.
Fix: always return { sections: midSections, midiItems: midMidis }
for ALL tracks in the map (arrays are sliced per-track).
The updateActiveTracks return statement was:
return t.id === trackId ? updated : t;
This discarded duplicate items that belong to other tracks.
Fix: always return updated{ sections, midiItems, clips }
for EVERY track in the map (already sliced from originals,
so unchanged tracks are no-ops).
React batches setSelectedItemIds, so when handleSetPendingDrag
snapshots selectedItemIds, the just-toggled item isn't included
yet. Fix: pass wasAlreadySelected boolean from TimelineTrack
mousedown; handleSetPendingDrag manually applies the toggle
(add/delete) to the snapshot so drag uses correct selection.
The useEffect with [] deps captured handleSectionItemDragStart
from initial render, which read an empty selectedItemIds.
Added handleSectionItemDragStartRef that stays current across
renders; the effect calls ref.current instead of the closure.
- Ctrl+click on selected item: deselect that item only
- Ctrl+click on empty space (via sweep start): deselect all items
- Ctrl+drag on empty space BETWEEN or BELOW tracks: creates
sweep overlay scanning ALL tracks for intersecting items
- onDeselectItem handler + prop wired through WaveformLane
- Global onMouseDown on tracks container div for empty-area sweep
- Removed sweepTrackIdRef filter in mouseup handler:
all tracks are scanned for intersecting items.
- Removed track ID check from sweep overlay rendering:
overlay appears on every track (not just drag-start track).
- Fixed stale closure by adding sweepSelectRef.
handleMouseUp captured stale sweepSelect state from render
closure. Added sweepSelectRef updated on every mousemove
so mouseup reads the latest sweep range correctly.
- Ctrl+drag on empty space: sweep range selection overlay
- Mouseup selects all items (MIDI, sections, clips) in sweep
- Selected items rendered with amber border highlight
- Ctrl+drag on section/MIDI item: duplicates the item
- Drag selected items: moves entire selected group together
- Ctrl+drag selected items: duplicates the entire group
handleRulerMouseDown's Ctrl+click branch previously only
cleared selection. Now it sets selectionMode='global',
records drag start, and enables isDraggingRulerRef so the
existing document-level mousemove handler updates the
selection range in real-time.
useEffect watching [activeTool, activeTab] runs createIcons()
but contextMenu is NOT in deps. On first right-click, if
activeTool/activeTab also change (e.g. timeline click), the
effect fires and icons appear. Subsequent right-clicks don't
change activeTool/activeTab, so createIcons() never runs for
the new context menu DOM.
Fix: add contextMenu to useEffect dependency array.
- Use filter(t => t.isArmed) instead of find() to route
MIDI input to ALL armed tracks simultaneously
- Use getTrackMidiChannel per track instead of raw
MIDI hardware channel (msg.data[0] & 0x0F)
- NoteOff, CC, PitchBend also routed to each armed
track's dedicated channel
- Previous code sent to channel 0 + first armed track only
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.
Line 5273 called playNote with undefined program + no channel,
defaulting to channel 0 with last-set instrument (may be Track 2's).
Fix: resolve parent track from st.trackId, pass correct channel
and synth_engine/instrumentProgram.
6 call sites were missing channel parameter, causing FluidSynth
to use ch=0 for all tracks -> program_change on channel 0
overwrote instrument across tracks during playback.
Fixes: startTrackPlayback, section sub-track MIDI, playMidiPreviewNote,
piano roll Alt+scroll preview, note click preview, keybed preview.
All 4 rules confirmed working:
1. MIDI item inherits track instrument
2. Section item does NOT inherit
3. Track isolation preserved
4. Piano roll instrument change propagates back to track
When midiChannel is undefined (no instrument set), compute channel
from track index to avoid defaulting to 0 and clashing with
configured tracks that use channel 0.
- setTrackInstrumentWithProgram stores midiChannel on the track
- schedulePianoRollMidi reads track.midiChannel instead of
recomputing from index (can be inconsistent across calls)
- selectInstrument also stores midiChannel if not already set
- isLooping defaults to true on subTab creation (auto-loop when
range selection is set)
- Per-track instrument confirmed: selectInstrument respects passed
channel, _playNoteFluid bypasses _engineChMap when channel is
explicit, no-instrument fallback sets program 0 (default piano)
instead of inheriting another track's program
_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
- When activePlayTrackIds is null (no button selected): no ghost play
- When activePlayTrackIds is set (one button red): only that track's
ghost notes play, with the track's own instrument on its own channel
- Removed st.currentTime >= 0 check from playhead rendering
- handleEditMidiInTab: currentTime = -beatOff * beatSec so phBeat=0
- handleSwitchMidiItem: same logic for dropdown switches
- Playhead draws at x=0 (bar 0) when item opens, moves right during
playback from the initial negative position
When selectedNoteIds.length > 0, shift+scroll on any note changes
velocity for all selected notes. Also fixed hit-test to subtract
renderBeatOffset for correct positioning in session mode.
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.
handleEditMidiInTab now sets currentTime relative to item start:
currentTime = Math.max(0, mainTimelineCurrentTime - midiItem.startTime)
so playhead reflects the main timeline position instead of bar 0.
handleSwitchMidiItem now computes new currentTime from the delta
between old and new renderBeatOffset, keeping the playhead at
the same session-absolute time instead of resetting to bar 0.