232 improve midi clip#248
Merged
Merged
Conversation
Add a shared --midi-color token and derive both the MIDI clip body and the MIDI track header from it so audio and MIDI tracks are distinguishable at a glance. Header uses a flat blue base (no vertical gradient) keeping the standard top sheen; the default tint is suppressed when a custom label color is set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lane gated its muted class on track.type === 'audio', so muting a MIDI track only dimmed the header, not its clips. Use the shared isAudioSectionTrack predicate (audio + midi) to match the header's mixer-track mute logic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
useMidiClipDraw mapped the cursor to time against the outer .timeline-track-stack, which includes the trackHeaderWidth header column, so every new clip was shifted later by trackHeaderWidth / zoom seconds. Measure against the lane's .track-clip-row (time-zero origin, scroll already baked in) like the existing right-click handler, re-reading the row rect on mouseup for scroll robustness. Drops the now-unused trackLanesRef/scrollX props. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ClipMidiPreview, a canvas mini note view drawn inside MIDI clips: X = note time, Y = pitch, with the pitch axis fit to the clip's own min..max range (DAW "fit notes to view") so the used register fills the height. Windowed canvas + DPR scaling like ClipWaveform so long/zoomed clips stay cheap. Wire it into the clip body, hide the now-redundant duration number, and shorten the label to "MIDI" (full clip rename to come later). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Right-click a MIDI clip → Rename enters an in-place inline edit where the label shows. Adds store state clipRenameId/setClipRenameId and a history-tracked renameMidiClip action; the clip swaps its label for an autofocused input (Enter/blur commits, Escape cancels). Default "MIDI Clip" name renders as "MIDI"; a renamed clip shows its custom name. Documents the #232 timeline clip presentation (color, note preview, label, rename, draw fix) in the MIDI plan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework the in-clip note preview to map clip-local time 1:1 (x = note.start *
zoom) across the full clip width, so it lines up with the timeline ruler and
shows the whole clip; tails past the clip end are clamped and notes starting
past it are skipped, mirroring the playback scheduler (clip = window onto the
notes). Drops the audio-waveform render-window sizing that was clipping notes
("missing material") and the inPoint offset.
Make the piano roll exactly the clip's real duration (contentWidth =
clipDuration * PX_PER_SEC) instead of a 4s minimum, so the editor and the clip
preview represent the same [0, duration] span; resize the clip for more room.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Overlapping clips on the same track used to trim/delete the clip underneath. For MIDI this is wrong — overlapping clips must cohabitate and both sound. Add a centralized getTrackOverlapPolicy(track) helper: MIDI tracks use 'stack' (overlap allowed, never trimmed), every other type keeps the existing 'trim' behavior, so non-MIDI editing is byte-identical. - getPositionWithResistance: stack tracks drop at the exact requested position with forcingOverlap=false (no red state, no track bounce). - trimOverlappingClips: no-op on stack tracks (safety line). - moveClip needs no change — its trim/bounce key off the now-falsy flags. Edge snapping is untouched, so MIDI keeps its magnetism; pushing past it lands the clip overlapping. The scheduler already plays all clips on a track, so both overlapping clips sound; manual silencing uses track mute. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A piano-roll window is bound to one active (editable) clip. Now it also shows, read-only, the notes of every other MIDI clip whose timeline span overlaps the active clip — so the user can read what coexists without leaving the editor. - computeGhostNotes(activeClip, allClips): pure helper that converts overlapping notes into the active clip's local time and clamps them to its window. Note data only (pitch + timing), velocity dropped. - PianoRoll renders ghosts beneath the real notes as flat grey bars with pointerEvents:none and no handlers, so they can't be selected/edited. - Active vs ghost is decided per window, so two open piano rolls each ghost the other's overlapping notes. Tests: tests/unit/ghostNotes.test.ts (local-time conversion, clamping, exclusion of active/non-overlapping/non-MIDI clips). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A MIDI clip is now an unbounded note canvas plus a movable window. Notes keep their absolute timeline position while the clip is resized — only the window (in/out point) moves, not the data. Model (single source of truth: services/midi/midiClipTiming.ts): - note.start is content time (fixed; changes only when the note is edited) - [inPoint, outPoint] is the visible/playable window (outPoint = inPoint + duration) - a note at content time t plays at startTime + (t - inPoint) - inPoint === 0 for existing clips, so this reduces to the old behavior (backward compatible) Routed every consumer through the model: scheduler, piano roll (render + draw/move/resize), clip mini-preview (new inPoint prop), ghost notes, and the offline export renderer. Notes outside the window are hidden but preserved, so shrink-then-enlarge brings them back. Both edges enlarge freely: MIDI is now an infinite trim source. The infinite-source list was duplicated in three places (trim commit, handle affordances, and the live on-timeline resize preview) and only two had midi — so the clip resized on commit but did not visibly grow/shrink during the drag. Consolidated into one shared utils/infiniteTrimSource.ts used by all three, fixing the live-resize feedback. Also fixes a latent bug where left-shrinking shifted notes (scheduler read startTime + note.start while trim moved startTime/inPoint). Tests: tests/unit/midiClipTiming.test.ts (window slide stability, round-trip, negative in-point pre-roll, hidden-not-moved). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A media split keeps both halves as windows onto one immutable source file. A MIDI clip has no external source — the note data IS the content — so splitClip now special-cases source.type === 'midi' and partitions the notes into two standalone clips via partitionMidiNotesAtCut instead of sharing one note array behind complementary windows. Each half owns only its own notes, rebased to inPoint = 0 (left notes shift by inPoint, right by the cut), with absolute timeline positions unchanged. Notes are assigned WHOLE by where their start falls (same rule as isNoteStartInWindow) and are never sliced: a note starting just before the cut stays in the left clip with its full duration and rings out past the cut, as before. Notes outside the visible window are dropped (already silent — a standalone clip keeps no ghosts). Because the data is genuinely separated, the clip preview, piano roll and scheduler each show/fit only that half's notes with no special-casing. This is deliberately distinct from a resize, which still uses the reveal-on- enlarge window model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Cubase-style Glue tool (Cut tool group, school-glue-bottle icon), the inverse of the MIDI cut. Click any MIDI clip to merge its contiguous run of touching/overlapping clips — expanding both directions, so clicking either side of an adjacent pair glues the same run, and a gap breaks the run (glue never jumps a space). Alt-click force-merges every following clip on the track, gaps included. Non-MIDI/locked clicks are no-ops. Notes are combined via mergeMidiNotes (the inverse of partitionMidiNotesAtCut): every note keeps its absolute timeline position, only in-window notes are taken (no resurrected ghosts), overlapping clips contribute polyphony, and gaps stay silent. So cut -> glue round-trips cleanly. - mergeMidiNotes + MidiMergeSegment in services/midi/midiClipTiming.ts - merge-midi-clips edit operation + applyMergeMidiClipsOperation (contiguousRunContaining resolver) in stores/timeline/editOperations - glue tool wired through the registry, defaults, icons, cursor, shortcuts, pointer dispatcher, and guided-replay tool mapping - GlueBottleIcon custom icon + matching cursor - docs + unit tests (mergeMidiNotes round-trip, resolver run/gap/overlap/alt/no-op, cut-group cycle order) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#232