815a6fa hid Synth when track has sections. But a track
can contain both section items AND midi items. Synth
applies to MIDI items regardless of sections. Section
instrument isolation is handled by section clone
nullifying all soundfont fields (9befc20).
...t in fallback branch copied soundfont_id, soundfont_bank,
soundfont_program, instrument_source from parent MAIN track
even though instrumentProgram and synth_engine were nulled.
FluidSynth picks up these fields and auto-loads the SoundFont.
Fix: override all soundfont/synth fields to null/undefined.
Previous fix f2ac70e inherited instrumentId/program/name
from parent MAIN track. This causes SECTION-TAB to auto-load
program 0 of whatever SoundFont is loaded on MAIN, even
though user didn't select any instrument for the section.
Section tracks now keep null instrument + synth_engine
undefined. User can still load instrument manually via
Synth button (routed by updateActiveTracks fix).
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.
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
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
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.
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
- 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.
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.
When activePlayTrackIds was null (default: all play), clicking a track
created an empty Set and deleted nothing. Now initializes with all
track IDs, so click toggles the clicked track off/on correctly.
- Remove dashed border from ghost notes (fill-only at 25% opacity)
- Track column: replaced div overlay with natural flex child; use
<button> elements with border-l-2 indicator; removed pointer-events
- Ghost playback: skip layers with no instrument assigned (silent)
instead of playing with default piano
- 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)