Skip to content

feat(editor): tempo range operations — half/double/flatten/re-fit (tempo PR 8) - #232

Merged
byrongamatos merged 5 commits into
mainfrom
feat/editor-tempo-range-ops
Jul 12, 2026
Merged

feat(editor): tempo range operations — half/double/flatten/re-fit (tempo PR 8)#232
byrongamatos merged 5 commits into
mainfrom
feat/editor-tempo-range-ops

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What & why

Tempo-track PR 8 (charrette arch 2 / trans P5). Three operations over a selected barline range in Tempo Map mode — the tools for reshaping a whole passage's grid at once. Each declares its time contract (the charrette's declared-contract rule).

⚠ Two-parent stack. Depends on #226 (5a — the barline multi-selection this operates on) and #231 (PR 7 — the hardened _suggestFitPure the range re-fit extends with opts.toIdx). This PR is based on #231 with #226 merged in, so #226's selection code shows in the diff — review the final commit (tempo range operations …) for PR-8-only changes. After #226 and #231 squash-merge, rebase onto main: the diff becomes PR-8-only.

The three ops

  • Half-time / Double-time the rangeTempoGridCmd, so times HOLD (the audio positions and note seconds don't move; only the barline grid changes → the tempo halves or doubles).
    • _tempoHalveRangePure demotes every second downbeat (an odd bar count leaves the last bar unpaired — reported).
    • _tempoDoubleRangePure promotes the interior beat nearest each bar's time midpoint (a one-beat bar has nothing to split — skipped + reported).
  • Flatten range to a steady tempoTempoMapCmd, so times MOVE and the notes follow. _tempoFlattenRangePure re-times the beats between the two ends to uniform per-beat spacing, both ends pinned (no tail shift); interior locked barlines hold their time (the sub-range runs through _respaceWithLocksPure).
  • Re-fit the range to onsets_suggestFitPure gains opts.toIdx: the onset march stops cleanly at the range's last downbeat (stopDetail: 'bound'), never proposing past it. Pressing G with a multi-selection anchors at the range's first downbeat and bounds at its last; the accept path is unchanged.

Wired into the tempo-map context menu (the three rows appear when 2+ barlines are selected) and the Suggest (G) handler.

Verification

  • npm test120 pass. New tests/tempo_range_ops.test.mjs (12): the range resolver; half/double keep every second fixed; half→double is identity on an even range; a one-beat bar is skipped; flatten's uniform interior + pinned ends + untouched tail + interior-lock hold; each command round-trips exec → undo → redo (flatten's note rides the retimed grid); the bounded re-fit never proposes past the range (and the unbounded one does).
  • npm run lint — 0 errors (3 pre-existing ratchet warnings).
  • Live on AC/DC — Back In Black: range re-fit → "Suggested a fit for the selected range (96 barlines)"; right-click ▸ Half-time → "merged 52 barlines — audio positions hold"; all three rows present; no page errors.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Half-time, Double-time, and Flatten to steady tempo actions for selected tempo ranges.
    • Tempo suggestions can now be limited to a selected barline range.
    • Status messages indicate when suggestions apply only to the selected range.
  • Bug Fixes
    • Improved consistency when regenerating bounded tempo suggestions.
  • Tests
    • Added coverage for range operations, undo/redo behavior, note retiming, and bounded suggestions.

ChrisBeWithYou and others added 4 commits July 12, 2026 15:06
Charrette UX P5 / arch 1 (first half). Select and delete multiple Tempo Map
barlines at once, without touching the single-focus model (S.tempoSel — which
inspector / tap / lock / modulate / suggest all key on).

- New S.tempoSelMulti: Set<downbeatIdx> (the S.drumSel pattern). Shift+click a
  pole extends the contiguous downbeat range; a drag on empty grid arms a
  tempo-marquee (the drum editor's deferred-3px `moved` idiom) that box-selects
  downbeats in its swept X range (plain replaces, Shift unions); Ctrl+A in mode
  selects every downbeat. The set is index-based, so it is CLEARED — never
  remapped — in TempoGridCmd exec/rollback and on Tempo Map exit.
- Render: a light amber wash across the selected range, and selected poles read
  amber (the focus keeps its unique halo). Rides the existing draw pass.
- Bulk delete = ONE TempoGridCmd: _tempoDeleteBarlinesPure demotes every selected
  INTERIOR downbeat (never the first/last — the existing guard, generalized to a
  set) + one renumber. Reachable via Del and right-click "Delete N barlines".
- Escape clears the selection, layered UNDER #215's suggest-dismiss (ghosts own
  Escape while showing).

tests/tempo_multiselect.test.mjs (6): marquee hit math, the delete transform
(demote+renumber, first/last guard), the bulk-delete round-trip through the
command, the exec/rollback set-clearing contract, and range selection. Verified
live: Ctrl+A → "105 barlines selected" (amber render), Escape clears, a marquee
box-selects, Ctrl+A+Del → "Deleted 103 barlines" (first/last kept), no errors.
npm test 115 green, lint 0 errors.

Independent of the in-review queue (deps only on merged #215); keep-both seams
with #220 (_tempoMapDraw) and #218/#225 on the shared tempo.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
The onset-fit march (Suggest ▸ G) now reads music the way a transcriber
would — it stops guessing where the audio stops agreeing instead of
snapping the grid onto syncopations or a single bad transient. All in
the pure fence (src/tempo-suggest.js); _suggestFitPure's signature is
unchanged (opts bag), so PR 8 can still build on it.

- c1 beat-relative window: the snap window is now a fraction of the
  BEAT (gridInt/beatsInBar), not the whole bar — the old ±12%-of-bar was
  ±0.48 beat in 4/4 and scaled with meter, so a syncopation could be
  snapped as the downbeat. New _suggestBeatWinPure; still floored at 25ms.
- c2 one-bar comb corroboration (_suggestCombPure): a candidate downbeat
  is scored by the onset support of ALL the bar's implied beats, not the
  single downbeat onset.
- c6 confidence is a PRODUCT of comb × continuity × consistency, so a
  bare-downbeat or off-tempo bar reads as less certain than a fully-
  played, steady one.
- c3 median stretch (_suggestMedianPure) resists one bad snap the way a
  plain EMA (α=0.35) could not; a single correction > 25% of a bar STOPS
  (stopDetail 'tempo-jump') rather than dragging the whole grid.
- c4 phase: a snap implying ~half/double the bar is a metric-phase
  ambiguity (halftime backbeat, double-time hat) — stop ('phase').
- c5 miss classification (_suggestAnyOnsetInPure): a bar with NO onset is
  silence — stop ('silence'); a bar whose onsets just miss the downbeat is
  sustained — keep marching. Trailing-drop now keys on a miss flag, not
  confidence, so a real low-comb hit survives.
- c7 named refusals: out-of-range tempo (outside 40-300 BPM) stops
  ('bpm-range'), and _suggestHudTextPure names each stop reason in the HUD
  (_suggestStopDetail wired through tempo.js).

tests/tempo_suggest.test.mjs: +7 fixtures, one per failure mode, each
verified to FAIL on the pre-hardening engine (c1 grabs the syncopation,
c2 scores equal confidence, c3-c7 lack the stops). The existing drift
fixture was updated to a fully-played onset stream so the new comb
confidence is exercised (deliberate pin update). 118 JS green, lint
0-err. Verified live: Suggest on AC/DC proposed 96 barlines, ghosts
render, no errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…mpo PR 8)

Three operations over the barline multi-selection (5a), each a small pure
+ one command + a right-click row, each declaring its time contract.

- Half-time / Double-time the range (TempoGridCmd — times HOLD, beats
  re-lift, so audio positions hold): _tempoHalveRangePure demotes every
  second downbeat inside the range (odd bar count → remainder reported);
  _tempoDoubleRangePure promotes the interior beat nearest each bar's time
  midpoint (a one-beat bar has nothing to split → skipped + reported).
- Flatten range to a steady tempo (TempoMapCmd — times MOVE, notes follow):
  _tempoFlattenRangePure re-times the beats between the range ends to
  uniform per-beat spacing, both ends pinned (no tail shift), interior
  locks honoured by running the sub-range through _respaceWithLocksPure.
- Re-fit the range to onsets: _suggestFitPure gains opts.toIdx — the march
  stops cleanly at the range's last downbeat (stopDetail 'bound'), never
  proposing past it. G with a multi-selection anchors at the range's first
  downbeat and bounds at its last; the accept path is unchanged.

Wired into the tempo-map context menu (the three rows show when 2+
barlines are selected) and the Suggest (G) handler. _suggestCompute
remembers the opts so a forward-regenerate after an accept stays in range.

New tests/tempo_range_ops.test.mjs (12): the range resolver; half/double
keep every second fixed; half→double is identity on an even range; a
one-beat bar is skipped; flatten's uniform interior + pinned ends + tail
untouched + interior-lock hold; each command round-trips exec→undo→redo
(flatten's note rides); the bounded re-fit never proposes past the range
(and the unbounded one does). 120 JS green, lint 0-err. Verified live:
range re-fit "Suggested a fit for the selected range (96 barlines)",
right-click ▸ Half-time "merged 52 barlines — audio positions hold".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tempo Map multi-selection now supports half-time, double-time, and flatten operations. Tempo suggestion fitting can also be constrained to the selected downbeat range, with option persistence during regeneration and coverage from unit and integration tests.

Changes

Tempo range editing

Layer / File(s) Summary
Range operations and command wiring
src/tempo.js, tests/tempo_range_ops.test.mjs
Adds selected-range detection, half-time, double-time, and flatten transformations, context-menu actions, undoable command execution, UI updates, and round-trip tests.
Selection-bounded tempo suggestions
src/input.js, src/tempo-suggest.js, tests/tempo_range_ops.test.mjs, tests/tempo_suggest.test.mjs
Passes the selected downbeat range as toIdx, stops fitting at the bound, preserves options during regeneration, and validates fresh-compute option reset behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant TempoSelection
  participant SuggestionEngine
  participant TempoGrid
  Editor->>TempoSelection: obtain selected downbeat range
  Editor->>SuggestionEngine: compute suggestions with range bound
  SuggestionEngine->>SuggestionEngine: stop proposals at toIdx
  SuggestionEngine-->>Editor: return bounded suggestions
  Editor->>TempoGrid: apply selected-range tempo operation
  TempoGrid-->>Editor: refresh tempo map and status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: new tempo range operations and bounded re-fit behavior in the editor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-tempo-range-ops

Comment @coderabbitai help to get the list of available commands.

@ChrisBeWithYou
ChrisBeWithYou force-pushed the feat/editor-suggest-hardening branch from eb8fd3b to 5c6910b Compare July 12, 2026 22:54
Base automatically changed from feat/editor-suggest-hardening to main July 12, 2026 23:22
@byrongamatos
byrongamatos merged commit 3074c5f into main Jul 12, 2026
4 checks passed
@byrongamatos
byrongamatos deleted the feat/editor-tempo-range-ops branch July 12, 2026 23:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/tempo_range_ops.test.mjs (1)

138-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test title promises redo coverage the body doesn't exercise.

Title says "exec → undo → redo" but the test never calls S.history.doRedo() or asserts anything about it, unlike the half-time test above (125-136) which fully round-trips. Since the PR objective specifically claims "command undo/redo coverage," this gap should be closed to actually verify redo for TempoGridCmd via double-time.

✅ Proposed fix
     _tempoDoubleRange();
     assert.deepStrictEqual(dbTimes(S.beats), [0, 2, 4, 6, 8], 'split at midpoints');
     assert.strictEqual(S.history.undo.length, 1);
     S.history.doUndo();
     assert.deepStrictEqual(dbTimes(S.beats), [0, 4, 8], 'undo restores the barlines');
+    S.history.doRedo();
+    assert.deepStrictEqual(dbTimes(S.beats), [0, 2, 4, 6, 8], 'redo re-applies');
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/tempo_range_ops.test.mjs` around lines 138 - 146, Extend the test
titled “double-time round-trips exec → undo → redo” to call S.history.doRedo()
after verifying undo, then assert that S.beats returns to the doubled grid [0,
2, 4, 6, 8]. Keep the existing execution and undo assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/tempo_range_ops.test.mjs`:
- Around line 138-146: Extend the test titled “double-time round-trips exec →
undo → redo” to call S.history.doRedo() after verifying undo, then assert that
S.beats returns to the doubled grid [0, 2, 4, 6, 8]. Keep the existing execution
and undo assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 573799fc-b5cd-4f07-9a7a-02d11728df73

📥 Commits

Reviewing files that changed from the base of the PR and between 2716f97 and 2c05177.

📒 Files selected for processing (5)
  • src/input.js
  • src/tempo-suggest.js
  • src/tempo.js
  • tests/tempo_range_ops.test.mjs
  • tests/tempo_suggest.test.mjs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants