Skip to content

feat(editor): notes carry a notated rhythm value (note-value model foundation)#360

Open
ChrisBeWithYou wants to merge 1 commit into
mainfrom
feat/rhythm-value-foundation
Open

feat(editor): notes carry a notated rhythm value (note-value model foundation)#360
ChrisBeWithYou wants to merge 1 commit into
mainfrom
feat/rhythm-value-foundation

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What this is

The foundation slice of the note-value / rhythm-duration model — the missing rhythmic layer under the editor. Today a note is onset + sustain (both seconds) with no notated value, and the live Tab view fakes durations off a 16th grid. This adds notated value as a first-class, symbolic quantity so the editor can eventually engrave, round-trip, and teach rhythm honestly.

The model (three decoupled quantities on one beat coordinate)

  • Onset (note.time) — where it starts.
  • Notated value (note.rhythm, new) — a quarter / dotted-eighth / triplet member. Symbolic, optional, best-fit-derived when absent.
  • Sounding length (note.sustain) — the free ring-out.

A staccato quarter and a legato quarter are the same value, different ring — so value and sustain both persist and neither overwrites the other.

note.rhythm = {
  base:  1|2|4|8|16|32|64|128,          // value denominator (power of two)
  dots:  0|1|2,                          // augmentation dots
  tuplet: null | [{ n, m, group }, ],   // outer→inner nesting; "n in the time of m"
  grace:  null | 'acciaccatura' | 'appoggiatura',
}

What this PR lands (no UI yet)

  • src/rhythm-value.js (pure tier): whole-note magnitude (base × dots × nested tuplet; grace = 0), the beat conversion, best-fit with an honest null past tolerance, greedy tie-decomposition, and the GP/MIDI/MusicXML tick projection with a lossy flag.
  • SetRhythmValueCmd: set/clear value on a selection as one undoable edit; snapshots the absent-vs-present distinction with a sentinel; clones the value per note (no aliasing).

Load-bearing invariants (each has a test that fails without the fix)

  • Never on the wire. rhythm is a runtime field like note.beat_stripBeat peels it off the seconds-only note wire, so a value-free pack stays byte-identical and the arrangement content signature is untouched. Authored values will persist later via the feedpak §7.6 notation side-file, never the note dict.
  • Survives chord reconstruction. reconstructChords rebuilds chord members through an explicit field-mapper that would otherwise silently drop rhythm; it now carries it (omitting the key when absent).
  • Honest gap over confident wrong. Best-fit returns null past tolerance rather than a plausible-but-wrong value.

Tests

tests/rhythm_value.test.mjs (pure) + tests/rhythm_value_cmd.test.mjs drive the real EditHistory / reconstructChords / save-strip (not stubs): exec→rollback→redo round-trips including absent-vs-present, per-note clone independence, chord-member survival, and the off-wire strip. Full JS suite green (317), lint clean (0 errors).

What's next

This is slice 1 of the model. Follow-ups: honest live-Tab from authored value, triplet/tie authoring UI, value-aware entry, import value lift/inference, and persistence + honest export. Persisting engraving-grade values (grace/nested tuplets) requires an FEP to extend feedpak §7.6 — a parallel spec track that gates only the persistence slice; this PR and the authoring slices don't depend on it.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for authored rhythm values as a first-class part of notes.
    • Supports straight, dotted, graced, and tuplet rhythms, with conversion between rhythmic spans and playback timing.
    • Added undoable controls for setting, replacing, and clearing rhythm values.
  • Bug Fixes

    • Rhythm values are preserved when rebuilding chords.
    • Runtime rhythm metadata is excluded from timing-only and legacy save data to maintain compatibility.
  • Tests

    • Added coverage for rhythm calculations, editing, undo/redo, chord preservation, and saving.

…undation)

The first slice of the note-value / rhythm-duration model (the roadmap's "deep
foundation under everything"): a note can carry a symbolic `rhythm` — base
(whole…128th), dots, nested tuplets, grace — as a THIRD quantity beside its onset
and its sounding `sustain`, so a staccato quarter and a legato quarter are the
same value, different ring. Lands the pure engine + the undoable command; no UI.

- `src/rhythm-value.js` (pure tier): whole-note magnitude (base × dots × nested
  tuplet, grace = 0), the beat conversion, best-fit with an HONEST null past
  tolerance, greedy tie-decomposition, and the GP/MIDI/MusicXML tick projection
  with a lossy flag. Fully unit-tested.
- `SetRhythmValueCmd`: sets/clears value on a selection as one undoable edit,
  snapshotting the absent-vs-present distinction with a sentinel and cloning the
  value per note (no aliasing).
- value is a runtime field like `note.beat`: it SURVIVES chord reconstruction
  (the field-mapper vanish hazard) but never rides the seconds-only note wire
  (`_stripBeat`), so a value-free pack stays byte-identical and the content
  signature is untouched. Authored values persist later via the §7.6 notation
  side-file, not the note dict.
- tests drive the REAL EditHistory / reconstructChords / save-strip, not stubs:
  exec→rollback→redo round-trips, chord-member survival, off-wire strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xGPjDBF8NTwTK7VQvizix
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a first-class rhythm-value model with tuplet, beat, tick, and bar conversions; introduces an undoable rhythm-setting command; preserves rhythm through chord rebuilding while stripping it from serialized runtime data; and adds model and integration tests.

Changes

Rhythm value foundation

Layer / File(s) Summary
Rhythm model and conversions
src/rhythm-value.js, tests/rhythm_value.test.mjs, CHANGELOG.md
Adds rhythm identity, cloning, equality, magnitude, tuplet, beat-span, tick, and bar-accounting helpers with coverage for valid, invalid, lossy, and tied values.
Editing and runtime integration
src/commands.js, src/chords.js, src/tempo.js, tests/rhythm_value_cmd.test.mjs
Adds SetRhythmValueCmd, preserves rhythm during chord reconstruction, and strips runtime rhythm fields from wire and save-body data while testing undo/redo and cloning behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant SetRhythmValueCmd
  participant NoteModel
  participant SaveStrip
  Editor->>SetRhythmValueCmd: execute rhythm update
  SetRhythmValueCmd->>NoteModel: set or clear cloned rhythm
  NoteModel-->>SetRhythmValueCmd: retain prior state for rollback
  Editor->>SaveStrip: build serialized data
  SaveStrip-->>Editor: omit runtime rhythm fields
Loading

Possibly related PRs

Suggested reviewers: byrongamatos

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: editor notes gain a notated rhythm value as the foundation of the new note-value model.
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/rhythm-value-foundation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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)
src/rhythm-value.js (1)

31-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider freezing the exported constant arrays.

RHYTHM_BASES and COMMON_TUPLETS are exported as plain mutable arrays/objects. Given this module's own stated concern about shared-mutable-state bugs (the cloneRhythm "bend_values aliasing bug" comment), a consumer accidentally doing RHYTHM_BASES.push(...) or mutating a COMMON_TUPLETS entry would silently corrupt best-fit results for every caller.

🛡️ Proposed fix
-export const RHYTHM_BASES = [1, 2, 4, 8, 16, 32, 64, 128];
+export const RHYTHM_BASES = Object.freeze([1, 2, 4, 8, 16, 32, 64, 128]);

-export const COMMON_TUPLETS = [
-    { n: 3, m: 2 }, { n: 5, m: 4 }, { n: 6, m: 4 }, { n: 7, m: 4 }, { n: 9, m: 8 },
-];
+export const COMMON_TUPLETS = Object.freeze([
+    { n: 3, m: 2 }, { n: 5, m: 4 }, { n: 6, m: 4 }, { n: 7, m: 4 }, { n: 9, m: 8 },
+].map(Object.freeze));
🤖 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 `@src/rhythm-value.js` around lines 31 - 37, Freeze the exported RHYTHM_BASES
and COMMON_TUPLETS constants to prevent consumers from modifying shared search
configuration, and also freeze each tuple object within COMMON_TUPLETS. Preserve
their current values and exported names while making both the arrays and nested
entries immutable.
🤖 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 `@src/rhythm-value.js`:
- Around line 31-37: Freeze the exported RHYTHM_BASES and COMMON_TUPLETS
constants to prevent consumers from modifying shared search configuration, and
also freeze each tuple object within COMMON_TUPLETS. Preserve their current
values and exported names while making both the arrays and nested entries
immutable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d6cf4a0-7f5d-4e10-bd85-f9c4c4088543

📥 Commits

Reviewing files that changed from the base of the PR and between e6185e7 and 434cbb5.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/chords.js
  • src/commands.js
  • src/rhythm-value.js
  • src/tempo.js
  • tests/rhythm_value.test.mjs
  • tests/rhythm_value_cmd.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.

1 participant