299 add bpm management - #349
Merged
Merged
Conversation
Packets 1 and 4 of the BPM management plan. Packet 1 — tempo editing core: - new pure src/timeline/tempo/tempoEdits.ts owning every invariant (sort, dedupe by time, pinned first event, clamps, id backfill/repair) - TempoEvent.id, required at runtime and optional in the project tier; normalizeRulerLaneState now normalizes instead of passing through, so pre-#299 projects gain ids at the single load seam - editable BPM range [20, 999] — deliberately NOT the 60/200 detection bins from beatGridEstimation, which would reject 40 and 240 BPM - new tempoSlice (setProjectTempo / add / update / removeTempoChange), capturing history AFTER the mutation like midiClipSlice - tempoMap threaded through all 12 history sites; restore falls back to the live map so a pre-#299 entry cannot clobber it on undo Packet 4 — tempo-driven body grid and snapping: - new pure src/timeline/tempo/barsGrid.ts: explicit bar/beat/subdivision line times plus the pixel thresholds the bars ruler also uses, so ruler ticks and grid lines can never disagree - an ENABLED Bars+Beats ruler lane wins the grid (not activeRulerLaneId, which is retired as a grid seam); bars replace the time/frame grid - grid snapping in both snap paths with a pixel-derived threshold, since a 1/16 at 120 BPM is narrower than the fixed 0.15 s window - timelineGridSubdivision view preference (localStorage) with the picker in the Rulers menu Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Packet 2 of the BPM management plan, plus a Rulers menu UX fix. Content follows tempo: - new pure src/timeline/tempo/tempoRemap.ts — remapAcrossMaps, trackFollowsTempo, remapMidiClip, remapTimelineClipsForTempo - MIDI clips, notes and the four CC automation lanes are remapped through old map -> bar/beat -> new map; video/audio/image clips keep their seconds and their object identity - durations are never scaled by a factor: ends are remapped and the remapped start subtracted, which is the only correct answer across a tempo boundary - everything is anchored on the clip's content origin, so noteAbsoluteStart stays exact and the window cannot drift from its notes - tempoSlice commits the remapped clips and the new map in one set under one snapshot, so a single undo reverts tempo and notes together TempoMap fix: - barBeatToSeconds now extrapolates through segment 0 below the first segment. With more than one segment every range test failed and it fell through to the LAST segment, returning nonsense for bar <= 0 — reachable through a left-extended MIDI clip's negative inPoint - new barBeatToSecondsAt object wrapper Rulers menu: - clicking a lane row toggles the lane again (checkmark + highlight); the grid resolution moved behind a + expander on the Bars + Beats row and is indented under it, so one click never means two things Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Packet 3 plus two model fixes found while testing it in the app. Tempo lane (Rulers -> Tempo, stacked under Bars + Beats, never selectable as the active lane): - flags read "4/4 - BPM = 120"; editing swaps only the value being changed for a field, so the rest of the label stays readable - right-click the lane: Add tempo change / Add time signature change, inserted at the nearest bar with the relevant editor already armed and selected - right-click a flag: Change tempo / Change time signature / Ramp toggle / Delete (Delete disabled on the project tempo) - drag to move, snapped to bars, Alt for free placement; one undo per gesture - the menu portals to document.body: the lane lives inside .time-ruler, which has a transform and two overflow:hidden wrappers, so a fixed-position menu was being positioned against the wrong box and clipped away - dismissal listens in the CAPTURE phase, since the flags themselves stop mousedown propagation and a bubble listener never fired Ramps (plan 3.3 said out of scope; now supported): - TempoEvent.curve = 'jump' | 'ramp'; 'ramp' reaches the tempo by interpolating from the previous event across the interval leading into it - TempoMap integrates a linearly varying tempo: elapsed beats are quadratic in time and the inverse solves that quadratic in closed form, so 60 -> 120 over 8 s covers 12 beats (the average), not 8 - dashed sloped indicator across the ramped interval plus an arrow on the flag Model fixes: - content is anchored to its QUARTER-NOTE position, not to (bar, beat). Bar and beat made a pure meter change (4/4 -> 3/4 at the same BPM) drag every note sideways even though no note's duration changed - tempo marks are re-anchored on every edit so they stay on their BAR. They are stored in seconds, so a ramp changing the length of the preceding interval used to slide a flag placed on bar 11 to bar 11.5. An explicitly set position (insert or drag) still wins, so a dragged flag lands where it was dropped Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A read-only mirror of the timeline's tempo lane, stacked between the Time and Bars rulers so tempo sits directly above the bars it drives. - follows the timeline's Rulers -> Tempo toggle, so one setting drives both views instead of a second hidden one (and it is already project-persisted through rulerLanes, from #257) - same labels, ramp arrow and dashed sloped indicator as the timeline lane; flags are pointer-events:none so they never swallow ruler scrubbing - built as a separate inline-styled renderer rather than reusing TempoRulerLane: the piano roll is a detached popup, and in dev the app CSS is injected as <style> tags that are not mirrored into it, so Timeline.css classes do not exist there - PIANO_ROLL_RULER_H replaced by pianoRollRulerHeight(laneCount), since the ruler is no longer a fixed two lanes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selecting a MIDI track defaulted to Track controls. The instrument is what you actually reach for on a MIDI track — volume/pan already live in the mixer — so it now opens there. Audio tracks still open on Track effects and every other track type on Track controls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Packets 5 and 6 of the BPM management plan. Engine: - src/engine/audio/metronomeVoice.ts — one oscillator + gain with a ~40 ms exponential decay; 1000 Hz at full level on the downbeat, 800 Hz at 0.7 otherwise. No assets, no sample loading. A click whose time has already passed fires immediately rather than being dropped - src/services/audio/metronomeScheduler.ts — the look-ahead pattern from midiPlaybackScheduler: 25 ms timer, 0.12 s window, timeline<->context anchor, re-anchor on a >0.25 s seek, silence at non-1x speed, dedup so a beat inside two windows fires once - beat times come from iterateBarBeatLines, so the click tracks meter, mid-window tempo changes and RAMPS with no new math - routing per plan 3.4: its own GainNode straight to AudioContext.destination, never a registered node route, so it cannot reach master metering, the FX/limiter chain or an export; an isExporting guard sits on top UI: - MetronomeButton after the Rulers menu: the button toggles, a separate caret opens a popover with volume and Every beat / Bars only - metronomeEnabled / metronomeVolume / metronomeMode are per-user localStorage view state like snappingEnabled, never project content (plan 3.6). Pulled forward from Packet 6 into Packet 5 because the engine needs something to read Count-in is deliberately NOT included. A real pre-count sounds the click for N bars while content stays silent, which needs a new transport phase rather than a playhead roll-back; play() has nowhere for "running but not advancing". The plan allows shipping the toggle and volume first rather than bending playback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Packet 7, plus three architecture-ratchet fixes the full chain surfaced from the metronome packets (which were fast-committed without checks). Docs: - new docs/Features/Tempo-And-Metronome.md — what you can do, then the three ideas the feature rests on, each written around WHY, since each was wrong at least once while building: BPM sets duration while the meter only groups; MIDI is musical and media is linear; a tempo mark is a musical object - Timeline-Rulers.md gets a superseded-in-part banner and its two now-false claims corrected in place: it no longer "ships no grid/snap behavior", and the click-to-select active-lane seam was retired in favour of "an enabled bars lane wins the grid" - Features/README.md indexes the new page; ongoing/README.md marks the plan complete - the plan doc gains per-packet landing notes and a new section 11 recording where the shipped feature diverges from it (ramps, quarter-note anchoring, musical re-anchoring, the smaller deviations, and the two bugs that mutation-testing our own tests caught) Ratchet fixes: - the metronome bootstrap moved from useLayerSync (which is under a hard LOC cap it pushed over) into editorBoot, where the other app-wide services start. A global audio service was never part of layer sync - metronomeScheduler registered in the getState access policy at 4 hits, the same class as midiPlaybackScheduler; baselines bumped with the reason - kept the registry file inside its own 300-line budget Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The architecture registry budget test measures split(/\r?\n/).length, so a newline-terminated file scores content lines + 1. Both staging and this branch sat at 299 content lines = exactly 300, so the merge of the two (staging's muscriptor adapter grant + this branch's metronomeScheduler entry) scored 301 and failed CI, even though neither branch fails alone. Drop the per-packet bookkeeping comments left over from the closed Complete Refactor (packets 231/267/291/306/307/342/343/345, the dockStore-folder note, and the multi-line ratchet log in the baselines); that history lives in git and in docs/completed/refactor/. No entries, paths or baseline numbers change, so the file is 281 lines with real headroom instead of zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflict in src/architecture/getStateAccessPolicy.ts resolved by keeping this branch's trimmed baseline block and combining both sides' numbers: staging's allowedAdapterPathCount 26 (muscriptor adapter grant) plus this branch's metronomeScheduler entry on top of staging's ledger (237 -> 238 files, 674 -> 678 hits). Co-Authored-By: Claude Opus 5 <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.
#299