Skip to content

feat(editor): GM roll — a piano-roll layout for the drum editor - #317

Merged
ChrisBeWithYou merged 2 commits into
mainfrom
feat/drum-gm-roll
Jul 19, 2026
Merged

feat(editor): GM roll — a piano-roll layout for the drum editor#317
ChrisBeWithYou merged 2 commits into
mainfrom
feat/drum-gm-roll

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Why

Kisscool's "no piano roll for drum MIDI" report (recorded in the dogfood handoff as a known design gap, and as view-modality open question 3) + Christian's 07-18 commitment to ship it for the nightly. Drummers arriving from DAWs read drums on General-MIDI percussion rows; the editor offered only the physical-kit piece order (Full) and the community 7-row family shape (Compact).

What

The Rows toggle gains a third density: GM roll.

  • One row per piece on its canonical GM note, pitch-descending top→bottom — the piano-roll convention (57 Crash R on top, 36 Kick near the bottom, the no-GM stack sinking last). Labels carry the GM number: 38 Snare, 42 HH cl.
  • Implemented exactly the way the view-modality design demands (V6: one grid, density presets, never a second data path): a new 'midi' branch in _drumLaneTablePure. Same grid, same piece-ids, same editing/selection/colors in every mode — only row order and labels change.
  • DRUM_PIECE_GM defines the canonical piece→note map in drum.js (primary notes only; the pad strip's GM_DRUM_MAP keeps the 35/40/41/47/59 aliases — and importing it from there would create a drum ← drum-pad-strip cycle).
  • Cycle is Full → Compact → GM roll → Full (_drumDensityNextPure); the preference round-trips 'midi' through localStorage like the existing modes; button text + status copy + User Guide updated.

Tests

drum_density.test.mjs extended — all new assertions fail on main: every piece appears exactly once; strict pitch-descending order (crash_r top, kick lowest GM row, stack last with gm: null); per-row gm agrees with the map; map hygiene (exactly the chart pieces, nothing extra); the 3-way cycle incl. junk recovery. npm test 209/209 · lint 0 errors.

Runtime verification (real host + Playwright)

Loaded a pack with a drum tab, entered the drum editor: Rows cycles Full → Compact → GM roll → Full; the GM rows render pitch-ordered with numbered labels and the fixture's hits land on their rows; the status line describes the mode; the preference persists (editorDrumDensity = midi).

🤖 Generated with Claude Code

https://claude.ai/code/session_01929LgKdJMyPGLf8N1WpEVW

Summary by CodeRabbit

  • New Features

    • Added a GM roll drum-editor layout with one row per General MIDI percussion piece.
    • Rows are ordered by pitch and labeled with their GM note numbers.
    • The Rows toggle now cycles through Full, Compact, and GM roll modes.
    • The selected layout persists between sessions.
  • Documentation

    • Updated the user guide and changelog with details about GM roll mode.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ChrisBeWithYou, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b934c24d-967d-4eed-9e75-7b2152c8e05d

📥 Commits

Reviewing files that changed from the base of the PR and between 8d8dcee and 3ee0940.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • docs/USER-GUIDE.md
  • src/drum.js
  • tests/drum_density.test.mjs
📝 Walkthrough

Walkthrough

The drum editor adds a persisted GM roll density mode with one General MIDI percussion piece per pitch-ordered row, GM-number labels, updated controls, documentation, changelog coverage, and tests for mapping and cycling.

Changes

Drum GM roll mode

Layer / File(s) Summary
GM lane mapping and ordering
src/drum.js, tests/drum_density.test.mjs
Adds GM note mappings and generates one midi-density lane per piece, ordered by descending pitch with stack unmapped; tests validate lane structure, ordering, and mapping coverage.
Density cycling and editor rendering
src/drum.js, docs/USER-GUIDE.md, CHANGELOG.md, tests/drum_density.test.mjs
Adds full → compact → midi → full cycling, persists valid density values, renders GM labels, updates control text, and documents the GM roll mode.

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

Sequence Diagram(s)

sequenceDiagram
  participant DensityButton
  participant DrumEditor
  participant LocalStorage
  DensityButton->>DrumEditor: Toggle drum density
  DrumEditor->>DrumEditor: Compute next density
  DrumEditor->>LocalStorage: Persist editorDrumDensity
  DrumEditor->>DrumEditor: Rebuild lane table and render labels
Loading

Possibly related PRs

Suggested reviewers: byrongamatos

🚥 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 change: adding GM roll piano-roll layout support for the drum 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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/drum-gm-roll

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.

@ChrisBeWithYou

Copy link
Copy Markdown
Contributor Author

Planned follow-up (Christian's call, 07-18): surface this in the view dropdown.

This PR puts the GM roll behind the Rows button (Full → Compact → GM roll). #316 separately turns the view switcher into a dropdown that, for drum tracks, offers Drum grid · Notation. So today a user checking the dropdown for "what layouts does this track have?" sees no piano roll, even though this PR ships one.

Once both this and #316 are merged, a small follow-up commit lands on whichever merged second: the drum dropdown becomes Drum grid · Piano roll · Notation (Piano roll = GM-roll density), while Full / Compact stay here on the Rows button — they're densities of the kit-ordered grid; GM roll is a different axis (pitch). Both controls read _drumDensityMode() so they can't drift, and returning to Drum grid restores the last non-GM density rather than resetting to Full.

Nothing to change in this PR — noting it so the two don't stay disconnected.

ChrisBeWithYou pushed a commit that referenced this pull request Jul 18, 2026
The drum 'Piano roll' dropdown entry needs GM-roll density to exist.
Merge #317 first, then this PR.

Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>

# Conflicts:
#	CHANGELOG.md
ChrisBeWithYou pushed a commit that referenced this pull request Jul 18, 2026
…tion

With the GM roll (#317, merged into this branch) and the view dropdown
both present, the drum piano roll was reachable only from the Rows
button while fretted tracks advertised "Piano roll" in the dropdown — so
a user checking the dropdown for "what layouts does this track have?"
concluded drums had none. Christian's call: fold the unification in
rather than defer it, since he controls the merge order.

- The drum dropdown becomes Drum grid · Piano roll · Notation, where
  Piano roll selects GM-roll density. Full/Compact stay on the Rows
  button: they are densities of the same KIT-ordered grid, whereas the
  GM roll is a different axis (pitch).
- Returning to Drum grid restores the density you came from
  (_drumKitDensityBack), so a Compact user isn't silently reset to Full
  by a trip through the roll.
- Both controls route through ONE setter (_editorSetDrumDensity, which
  _editorToggleDrumDensity now delegates to), so the Rows button and the
  dropdown cannot drift apart — the dropdown reads _drumDensityMode().
- No import cycle: key-view.js -> drum.js is one-directional.

Drums now match fretted tracks' vocabulary: instrument geometry (String
view / Drum grid) -> pitch-ordered (Piano roll / GM rows) -> engraved
(Tab-Notation / Notation).

Tests: +4 in view_switch_lens (density-driven value derivation, Piano
roll setting GM density with both controls agreeing, the Compact/Full
round-trip restore, and the Rows cycle still wrapping through the same
setter). Full suite 210/210, lint 0 errors.

Runtime-verified in the host: the dropdown lists all three; Rows ->
Compact then dropdown -> Piano roll flips the Rows button to GM roll;
returning to Drum grid restores COMPACT (not Full); Notation engraves
without disturbing the density; leaving it returns to the grid.

Merge order: #317 first, then this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01929LgKdJMyPGLf8N1WpEVW
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
@ChrisBeWithYou

Copy link
Copy Markdown
Contributor Author

Update — the follow-up landed early, in #316. Christian is merging these himself after a Codex review, which removes the merge-order risk that had me deferring this.

This PR is unchanged and still independently mergeable. #316 now has this branch merged into it and adds the unification on top: the drum dropdown becomes Drum grid · Piano roll · Notation, where Piano roll selects the GM-roll density from this PR. Full/Compact stay here on the Rows button, and both controls route through one setter so they can't drift.

Merge order: this PR first, then #316.

@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/drum_density.test.mjs (1)

106-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update test description to match assertion behavior.

The test description mentions junk recovers to compact-first, but the assertion _drumDensityNextPure('banana') === 'full' verifies that junk input correctly recovers to 'full'.

♻️ Proposed fix
-t('density cycle: Full → Compact → GM roll → Full; junk recovers to compact-first', () => {
+t('density cycle: Full → Compact → GM roll → Full; junk recovers to full', () => {
     assert.strictEqual(_drumDensityNextPure('full'), 'compact');
     assert.strictEqual(_drumDensityNextPure('compact'), 'midi');
🤖 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/drum_density.test.mjs` around lines 106 - 112, Update the test
description for the `_drumDensityNextPure` cycle test to state that junk input
recovers to “full,” matching the existing `'banana'` assertion. Leave the
assertions and test behavior 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/drum_density.test.mjs`:
- Around line 106-112: Update the test description for the
`_drumDensityNextPure` cycle test to state that junk input recovers to “full,”
matching the existing `'banana'` assertion. Leave the assertions and test
behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 57b45e27-0dc1-4ffd-90b3-3eb3d3cd6316

📥 Commits

Reviewing files that changed from the base of the PR and between 6243cf8 and 8d8dcee.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • docs/USER-GUIDE.md
  • src/drum.js
  • tests/drum_density.test.mjs

ChrisBeWithYou and others added 2 commits July 19, 2026 00:13
Tester ask ("no piano roll for drum MIDI"; view-modality open question
3): drummers coming from a DAW expect the GM percussion-row layout, not
only the physical-kit piece order. The Rows toggle gains a third density
after Full and Compact.

- DRUM_PIECE_GM: the canonical General-MIDI note per chart piece (the
  primary note only — the pad strip's GM_DRUM_MAP keeps the aliases;
  defining it here avoids a drum <- pad-strip import cycle).
- _drumLaneTablePure grows a 'midi' branch: one row per piece on its GM
  note, pitch-DESCENDING top->bottom (piano-roll convention), no-GM
  pieces (stack) sink last. Same grid, same piece-ids, same editing and
  colors — V6's one-data-path rule holds; only row order and labels
  change. Lane labels gain the GM number prefix ("38 Snare").
- _drumDensityNextPure: Full -> Compact -> GM roll -> Full; the pref
  round-trips 'midi' through localStorage; button + status copy updated.

Tests: drum_density suite extended (all new assertions fail on main) —
every piece exactly once, strict descending order with crash_r on top /
kick lowest / stack last, gm numbers agree with the map, map hygiene
(exactly the chart pieces), and the 3-way cycle with junk recovery.
Full suite 209/209, lint 0 errors. Runtime-verified through the host
with Playwright: Rows cycles Full -> Compact -> GM roll -> Full, the GM
rows render pitch-ordered with numbered labels, the pref persists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01929LgKdJMyPGLf8N1WpEVW
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
@ChrisBeWithYou
ChrisBeWithYou merged commit dcf35b6 into main Jul 19, 2026
4 checks passed
ChrisBeWithYou pushed a commit that referenced this pull request Jul 19, 2026
…tion

With the GM roll (#317, merged into this branch) and the view dropdown
both present, the drum piano roll was reachable only from the Rows
button while fretted tracks advertised "Piano roll" in the dropdown — so
a user checking the dropdown for "what layouts does this track have?"
concluded drums had none. Christian's call: fold the unification in
rather than defer it, since he controls the merge order.

- The drum dropdown becomes Drum grid · Piano roll · Notation, where
  Piano roll selects GM-roll density. Full/Compact stay on the Rows
  button: they are densities of the same KIT-ordered grid, whereas the
  GM roll is a different axis (pitch).
- Returning to Drum grid restores the density you came from
  (_drumKitDensityBack), so a Compact user isn't silently reset to Full
  by a trip through the roll.
- Both controls route through ONE setter (_editorSetDrumDensity, which
  _editorToggleDrumDensity now delegates to), so the Rows button and the
  dropdown cannot drift apart — the dropdown reads _drumDensityMode().
- No import cycle: key-view.js -> drum.js is one-directional.

Drums now match fretted tracks' vocabulary: instrument geometry (String
view / Drum grid) -> pitch-ordered (Piano roll / GM rows) -> engraved
(Tab-Notation / Notation).

Tests: +4 in view_switch_lens (density-driven value derivation, Piano
roll setting GM density with both controls agreeing, the Compact/Full
round-trip restore, and the Rows cycle still wrapping through the same
setter). Full suite 210/210, lint 0 errors.

Runtime-verified in the host: the dropdown lists all three; Rows ->
Compact then dropdown -> Piano roll flips the Rows button to GM roll;
returning to Drum grid restores COMPACT (not Full); Notation engraves
without disturbing the density; leaving it returns to the grid.

Merge order: #317 first, then this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01929LgKdJMyPGLf8N1WpEVW
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
ChrisBeWithYou added a commit that referenced this pull request Jul 19, 2026
)

* feat(editor): Tab and Notation join the view switcher

The live engraved score (tab-view-live.js) existed but was reachable
only through the view-cycle shortcut and the View menu — the top-right
switcher showed just String | Piano roll, and its active state ignored
the lens entirely (String stayed lit while the engraving was showing).

- The switcher becomes String · Piano roll · Tab · Notation. Tab opens
  the engraved tablature; Notation the standard-notation staff — both
  drive the existing lens (editorToggleTabView) with the matching staff
  profile (editorSetTabViewStaff).
- A user whose Score-staff preference is "notation + tab" keeps it:
  _tabStaffForClickPure preserves 'both' on either click, and
  _viewSwitchActivePure lights Tab AND Notation together for it (the
  view genuinely shows both).
- The active state is lens-aware; the roll-lock pill hides while the
  lens owns the timeline; Tab/Notation hide on drum tracks (which have
  no tab — same refusal the lens itself makes); keys stay piano-locked.
- Returning to String/Piano roll drops the lens before writing the
  per-part view pref (mirrors the cycle's ordering).
- User Guide: the view list documents all four + the switcher.

Tests: tests/view_switch_lens.test.mjs — active-set derivation
(including 'both' lighting two buttons and junk-staff degradation),
'both'-preserving staff-for-click, lens entry via
editorSetViewMode('tab'/'notation'), String exit dropping the lens, and
keys/drums refusal. All fail on main. Existing tab_view_cycle suite
unchanged and green; full suite 210/210, lint 0 errors.
Runtime-verified through the host with Playwright: 4 buttons, Tab
renders the engraving, Notation switches staff + persists the pref,
String hides the mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01929LgKdJMyPGLf8N1WpEVW
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>

* refactor(editor): view switcher becomes a dropdown (Christian's call)

The pill toggle from the previous commit becomes a per-track view
dropdown: String view · Piano roll · Tab · Notation · Notation + Tab.
With a dropdown every staff profile is an explicit option, so the
selected value is always a truthful readback of state (lens included)
and the 'preserve the both pref on click' dance disappears —
_tabStaffForClickPure is gone, _viewSwitchValuePure derives the single
value. Drum tracks lose the engraved options (disabled + hidden)
instead of buttons that refuse. Tests reworked to the value model;
'both' now pinned as an explicit choice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01929LgKdJMyPGLf8N1WpEVW
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>

* feat(editor): drum parity — Notation for drum tracks in the view dropdown

Christian's parity call: drums should get Notation too. In the drum
editor the same dropdown now offers Drum grid / Notation, and the live
lens engraves the drum tab on a real percussion staff.

- src/alphatex.js: _alphaTexFromDrumHitsPure, the percussion flavor of
  the generator. The bar walker (bucketing, gap durations, rest fill,
  beatMap) is factored into a shared _alphaTexAssemblePure so drums and
  fretted parts keep ONE quantization contract. DRUM_TEX_ARTICULATIONS
  maps each piece to its alphaTab articulation id; `stack` has none, so
  its hits are skipped and COUNTED, never silently dropped.
- Three things the browser taught us, each now pinned by a test:
  * every drum beat is parenthesized — a bare `36.4` lexes as the FLOAT
    36.4, not articulation 36 with a quarter duration (fretted notes
    dodge this because `3.6.4` has two dots). This was a hard parse
    error: the engraving came up blank.
  * `\clef neutral` is BAR metadata and load-bearing — without it
    alphaTab engraves the kit under a TREBLE clef.
  * the percussion staff renders under StaveProfile.Default; forcing
    Score (as fretted parts do) engraves it as a pitched staff.
- src/tab-view-live.js: a `_texSource` ('chart' | 'drums') selects the
  generator, keys the render + api-rebuild, and routes click-to-select
  into S.drumSel instead of S.sel. Entering the lens from the drum
  editor keeps S.drumEditMode ON underneath (the lens paints over it),
  so leaving Notation restores the grid with its state intact; the draw
  pass drops the lens if the drum tab or mode goes away.
- src/key-view.js + screen.html: the dropdown shows the drum pair in
  drum mode and the five standard views otherwise.

Tests: alphatex suite +5 (percussion header/clef/articulations, chord
grouping with same-piece dedupe, unmapped counting, clef only on bar 1,
and the fretted generator NEVER emitting a clef); view-switch suite +2
(drum value derivation, and drum-notation not leaving the drum editor).
All fail on main. Full suite 210/210, lint 0 errors.

Runtime-verified in the host: the dropdown offers Drum grid / Notation,
the engraving renders with a PERCUSSION clef, kick on the bottom space,
snare on the middle line, X noteheads for hats; switching back restores
the grid. Verified via alphaTab's parsed model too: isPercussion=true,
channel 9, articulations resolving to MIDI 36/38/42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01929LgKdJMyPGLf8N1WpEVW
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>

* feat(editor): drums reach view parity — Drum grid · Piano roll · Notation

With the GM roll (#317, merged into this branch) and the view dropdown
both present, the drum piano roll was reachable only from the Rows
button while fretted tracks advertised "Piano roll" in the dropdown — so
a user checking the dropdown for "what layouts does this track have?"
concluded drums had none. Christian's call: fold the unification in
rather than defer it, since he controls the merge order.

- The drum dropdown becomes Drum grid · Piano roll · Notation, where
  Piano roll selects GM-roll density. Full/Compact stay on the Rows
  button: they are densities of the same KIT-ordered grid, whereas the
  GM roll is a different axis (pitch).
- Returning to Drum grid restores the density you came from
  (_drumKitDensityBack), so a Compact user isn't silently reset to Full
  by a trip through the roll.
- Both controls route through ONE setter (_editorSetDrumDensity, which
  _editorToggleDrumDensity now delegates to), so the Rows button and the
  dropdown cannot drift apart — the dropdown reads _drumDensityMode().
- No import cycle: key-view.js -> drum.js is one-directional.

Drums now match fretted tracks' vocabulary: instrument geometry (String
view / Drum grid) -> pitch-ordered (Piano roll / GM rows) -> engraved
(Tab-Notation / Notation).

Tests: +4 in view_switch_lens (density-driven value derivation, Piano
roll setting GM density with both controls agreeing, the Compact/Full
round-trip restore, and the Rows cycle still wrapping through the same
setter). Full suite 210/210, lint 0 errors.

Runtime-verified in the host: the dropdown lists all three; Rows ->
Compact then dropdown -> Piano roll flips the Rows button to GM roll;
returning to Drum grid restores COMPACT (not Full); Notation engraves
without disturbing the density; leaving it returns to the grid.

Merge order: #317 first, then this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01929LgKdJMyPGLf8N1WpEVW
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>

---------

Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@ChrisBeWithYou
ChrisBeWithYou deleted the feat/drum-gm-roll branch July 19, 2026 05:22
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