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