Skip to content

feat(editor): metric modulation at the selected sync point - #96

Merged
byrongamatos merged 2 commits into
mainfrom
feat/editor-tempo-modulate
Jul 6, 2026
Merged

byrongamatos merged 2 commits into
mainfrom
feat/editor-tempo-modulate

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Prog and classical charts need real metric modulations — new tempo = old × ratio at a pivot. This adds an M command in Tempo Map mode (both shortcut profiles) plus a Modulate… button in the sync-point inspector: pick a pivot preset (♩=♩. ×2/3 · ♩.=♩ ×3/2 · ♩=♪ ×1/2 · ♪=♩ ×2) or type any ratio (3:2, 2/3, 0.75).

The key semantics:

  • The re-space stops at the next tempo change. The new tempo applies from the selected measure through its uniform run — the first measure whose BPM differs by more than 0.5% is treated as a hand-authored pole and is never crossed. Modulating a 120-BPM section that runs into a 90-BPM section touches only the 120 bars; the 90 section keeps its exact internal spacing and just rigid-shifts.
  • Proportional interior re-spacing — sub-beats keep their fractional positions, so swung or uneven grids keep their feel through the modulation.
  • Beat count is unchanged, so the whole edit rides the existing TempoMapCmd: one undoable command, and notes remap per the tempo-ride scope exactly like a BPM edit.
  • Ratios outside 0.2–5 and results that would produce measures below the minimum span are refused with a clear status, never applied.

No spec change — the beat grid already expresses any ratio; this is pure authoring surface (per the rhythm-meter seat's charrette ruling).

Verification

  • node --check screen.js clean
  • New tests/tempo_modulate.test.js — 7 cases via the @pure:tempo-modulate block (ratio parsing incl. presets/fractions/junk, run boundary stops at a tempo change with the downstream section's spacing pinned, span math + tail shift, swing-fraction preservation, mid-run selection scope, min-span refusal, invalid targets)
  • All 26 JS test files pass

🤖 Generated with Claude Code

https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu

Summary by CodeRabbit

  • New Features

    • Added Metric Modulation to Tempo Map mode, including a new Tempo Modulate command accessible via the M key and a Modulate… button in the tempo inspector for eligible points.
    • Modulation re-spaces beats proportionally within a uniform-tempo span and updates the resulting tempo display.
  • Bug Fixes

    • Refuses modulation when it would create an invalid/too-short measure span and strengthens handling of invalid targets and ratio inputs.
  • Documentation

    • Updated the changelog with usage details.
  • Tests

    • Added dedicated tempo modulation unit coverage.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3faf3111-164e-4a57-aee9-39d5cbb6692d

📥 Commits

Reviewing files that changed from the base of the PR and between c8bff22 and 7b78e30.

📒 Files selected for processing (2)
  • screen.js
  • tests/tempo_modulate.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/tempo_modulate.test.js

📝 Walkthrough

Walkthrough

Adds a Metric Modulation feature to Tempo Map mode: a new tempoModulate command bound to the M key, an inspector “Modulate…” button, pure helper functions for ratio parsing and proportional beat re-spacing, an editor entry point that commits changes via TempoMapCmd, a standalone test suite, and a changelog entry.

Changes

Metric Modulation Feature

Layer / File(s) Summary
Pure modulation helpers and editor entry point
screen.js
Adds _tempoModulationRatioPure, _tempoMeasureBpmAtPure, and _tempoModulateRunPure to parse ratios and proportionally re-space beats across a uniform-tempo run, plus _editorModulateTempoAtSelection() which prompts, validates, applies, and commits the change via TempoMapCmd.
Command registration, shortcut keys, and inspector button
screen.js
Registers tempoModulate in EDITOR_SHORTCUT_COMMANDS, binds the M key in EOF and Feedback Tempo Map profiles, dispatches it in _editorRunEofCommand, and adds a “Modulate…” button to the tempo sync inspector gated by the same conditions as BPM editing.
Test suite and changelog entry
tests/tempo_modulate.test.js, CHANGELOG.md
Adds a Node test script extracting and validating the pure helpers (ratio parsing, run boundaries, scaling, swing preservation, mid-run selection, minimum-measure refusal, invalid targets) and a changelog entry documenting the feature.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Editor
  participant PureHelpers as TempoModulate Helpers
  participant TempoMapCmd

  User->>Editor: Press M or click "Modulate…" in Tempo Map mode
  Editor->>Editor: _editorModulateTempoAtSelection()
  Editor->>User: Prompt for pivot/ratio
  User-->>Editor: Provide ratio input
  Editor->>PureHelpers: _tempoModulationRatioPure(input)
  PureHelpers-->>Editor: Parsed ratio or null
  Editor->>PureHelpers: _tempoModulateRunPure(S.beats, target, ratio)
  PureHelpers-->>Editor: Modulated beats or null (refused)
  Editor->>TempoMapCmd: Commit modulated beats
  TempoMapCmd-->>Editor: Apply change
  Editor->>User: Update BPM display, redraw, show 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 and concisely summarizes the main editor change: metric modulation at a selected sync point.
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-modulate

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)
screen.js

ast-grep timed out on this file


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

@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)
screen.js (1)

12771-12826: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: extract the repeated "next downbeat" scan into a helper.

The forward search for the next measure > 0 index is duplicated four times (_tempoMeasureBpmAtPure, the run-collection loop, the apply loop, and the lastNdb IIFE). A single _nextDownbeatPure(beats, from) helper would remove the duplication and let you drop the redundant re-scan inside the apply loop by reusing indices already computed while building runStarts.

Correctness itself looks sound: runStarts only ever pushes indices whose BPM is non-null, which guarantees a valid ndb in the apply loop, and res.beats is a full clone so it can't alias S.beats.

🤖 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 `@screen.js` around lines 12771 - 12826, The forward scan for the next `measure
> 0` downbeat is duplicated across `_tempoMeasureBpmAtPure`,
`_tempoModulateRunPure`, and the `lastNdb` logic, and the apply loop re-finds
indices that were already discovered. Extract a shared `_nextDownbeatPure(beats,
from)` helper, then reuse it everywhere and carry the computed next-downbeat
index through `runStarts` so `_tempoModulateRunPure` doesn’t re-scan during the
apply phase.
🤖 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 `@screen.js`:
- Around line 12771-12826: The forward scan for the next `measure > 0` downbeat
is duplicated across `_tempoMeasureBpmAtPure`, `_tempoModulateRunPure`, and the
`lastNdb` logic, and the apply loop re-finds indices that were already
discovered. Extract a shared `_nextDownbeatPure(beats, from)` helper, then reuse
it everywhere and carry the computed next-downbeat index through `runStarts` so
`_tempoModulateRunPure` doesn’t re-scan during the apply phase.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5338255f-d335-4643-9a9c-1cda035c7b79

📥 Commits

Reviewing files that changed from the base of the PR and between e067f25 and c8bff22.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • screen.js
  • tests/tempo_modulate.test.js

ChrisBeWithYou and others added 2 commits July 6, 2026 21:43
Prog/classical charting needs real metric modulations (new tempo = old x
ratio at a pivot). Adds an M command in Tempo Map mode + a "Modulate..."
button in the sync-point inspector: pick a pivot preset (quarter=dotted
quarter x2/3, dotted=quarter x3/2, quarter=eighth x1/2, eighth=quarter
x2) or type any ratio (3:2, 2/3, 0.75).

The new tempo applies from the selected measure THROUGH ITS UNIFORM RUN,
stopping at the first measure whose BPM materially differs (>0.5%) — a
hand-authored downstream tempo change is a natural pole the re-space
never crosses, so verified sections stay untouched (design D18).
Interior beats re-space PROPORTIONALLY, preserving swung/uneven sub-beat
fractions; everything after the run rigid-shifts by the accumulated
delta; beat count is unchanged so the edit rides TempoMapCmd (notes
remap per the tempo-ride scope, exactly like a BPM edit) as one undoable
command. Results yielding measures below the minimum span are refused.

Tests: tests/tempo_modulate.test.js (7 cases: ratio parsing, run
boundary at a tempo change, span math + tail shift, swing preservation,
mid-run selection, min-span refusal, invalid targets). node --check
clean; all 26 JS test files pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu
The final parseFloat fallback in _tempoModulationRatioPure silently
accepted leading-numeric garbage: '3:2:1'/'2/3/4'/'3abc' fail the anchored
fraction regex, then parseFloat truncates them to 3/2/3 — plausible ratios
that land inside the caller's 0.2-5 bounds and get APPLIED, contradicting
the garbage-rejected contract. Require the whole trimmed string to be a
clean bare decimal before accepting; otherwise return null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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