feat(editor): live Tab view — engraved tablature as a third view mode - #273
Conversation
The view cycle grows a third lens (String -> Piano roll -> Tab): the timeline area becomes live-engraved tablature of the current in-memory chart, re-rendered (debounced, editGen-keyed) on every committed edit — no save, no GP round-trip, unlike the saved-pack Tab preview modal. - src/alphatex.js: pure generator — current fretted arrangement -> alphaTex on a 16th grid in the BEAT domain (variable tempo maps engrave correctly), gap-based durations with greedy rest fill so bars always sum exactly, \ts on meter change, chord grouping, string-number flip, alphaTab's octave convention in \tuning (high E = e5), and a beatMap aligned 1:1 with emitted beats for click mapping. Pickup/tail notes are skipped and counted, never silently dropped. - src/tab-view-live.js: the lens — S.tabViewMode flag cleared by the other mode toggles, draw() pings visibility (single source of truth), fretted-only guard, click-to-select: alphaTab's beatMouseDown plus a CAPTURE-phase DOM fallback (alphaTab stops propagation on events it consumes) map the clicked beat through the beatMap to the source notes -> S.sel + seek. Renderer is the same pinned CDN alphaTab as the preview (shared loader + font dir; enablePlayer only for the interaction layer — no soundfont, no cursor, editor owns audio). - Wiring: view-cycle + shortcut registry row (toggleTabView), menu bar entry, teardown-registered api destroy, mount in screen.html. Ordering trap fixed en route: _render must call _ensureApi BEFORE assigning _beatMap — the api build path runs _destroyApi(), which nulls the map and silently disarmed click-to-select. Verified live on :8001 (AC/DC pak): full engraving, click selects the opening chord (3 notes) + seeks, re-click after re-render still selects, live-edit refresh keeps the view, exit hides the mount, zero page errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a live alphaTab tablature view with alphaTex generation, beat selection, synchronized redraws, view controls, mutually exclusive editor modes, lifecycle teardown, and related tests and documentation. ChangesLive Tab View
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Editor as Editor draw pass
participant TabView as Live Tab View
participant Generator as alphaTex generator
participant AlphaTab as alphaTab renderer
Editor->>TabView: ping while tab view is active
TabView->>Generator: generate current arrangement
Generator-->>TabView: return tex and beatMap
TabView->>AlphaTab: engrave tex
AlphaTab-->>TabView: emit beat click
TabView->>Editor: select notes and move playhead
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/key-view.js (1)
138-158: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winGuard drums in the cycle path too
viewFor(arr)can still return'piano'for a drums-named arrangement, and this branch then callseditorToggleTabView(true), which refuses drums without changing mode. The shortcut can loop here forever instead of advancing to Tab/String. Add the same/^drums/icheck here or skip straight toeditorSetViewMode('string').🤖 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/key-view.js` around lines 138 - 158, Update _editorCycleViewMode to explicitly guard arrangements whose names match /^drums/i before the viewFor(arr) piano branch. Ensure drums skip the Tab toggle and advance directly to the appropriate string view, preventing the cycle from getting stuck while preserving existing keys-track handling.
🤖 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.
Inline comments:
In `@src/main.js`:
- Around line 226-231: Update the S.tabViewMode branch in the main update flow
so BPM and time-signature displays are refreshed before returning from
_tabViewPing(). Ensure updateBPMDisplay() and updateTempoSigDisplay() still run
during Tab view updates while preserving the existing _tabViewPing() behavior
and non-Tab mode flow.
---
Outside diff comments:
In `@src/key-view.js`:
- Around line 138-158: Update _editorCycleViewMode to explicitly guard
arrangements whose names match /^drums/i before the viewFor(arr) piano branch.
Ensure drums skip the Tab toggle and advance directly to the appropriate string
view, preventing the cycle from getting stuck while preserving existing
keys-track handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1179ce0d-9b3d-4134-ac55-44c3c21f582a
📒 Files selected for processing (16)
CHANGELOG.mdscreen.htmlsrc/alphatex.jssrc/drum.jssrc/input.jssrc/key-view.jssrc/main.jssrc/menu-bar.jssrc/parts-view.jssrc/shortcuts.jssrc/state.jssrc/tab-preview.jssrc/tab-view-live.jssrc/tempo.jstests/alphatex.test.mjstests/screen_markup.test.mjs
…en.html overlays, input.js dispatch, main.js imports/drawNow, screen_markup sibling list) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…ip the Tab stop in the view cycle Two findings from the community review: - updateBPMDisplay/updateTempoSigDisplay now run before ANY mode fork in drawNow, so undo/redo changing tempo or meter refreshes the toolbar readouts even while the Tab lens owns the timeline. - The view cycle skips the Tab stop for drums tracks (the lens refuses drums without changing mode, which left the cycle stuck on the roll); new tests/tab_view_cycle.test.mjs pins the full cycle including the drums skip and the leave-restores-String path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
There was a problem hiding this comment.
🧹 Nitpick comments (3)
tests/tab_view_cycle.test.mjs (3)
34-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
seed()'sstoredPianoparam/return is dead code.Every call site (
seed('Lead'),seed('Drums')) passes only thenameargument, and no caller uses the return value — tests 2/3 setlocalStorage.getItemmanually right after callingseed()instead. The parameter andreturn storedPiano;don't do anything and make the comment above ("the piano case being driven by the caller below") harder to map to actual behavior.🧹 Proposed cleanup
-function seed(name, storedPiano) { +function seed(name) { Object.assign(S, { arrangements: [{ name, id: 'p1' }], currentArr: 0, tabViewMode: false, filename: '' }); calls.length = 0; - // no filename → no stored view pref, so viewFor falls back on the name; - // fake "currently on the roll" via the tab of stored prefs being empty - // and the piano case being driven by the caller below. - return storedPiano; + // no filename → no stored view pref, so viewFor falls back on the name; + // callers that need the "on the roll" state stub localStorage.getItem + // themselves right after calling seed(). }🤖 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/tab_view_cycle.test.mjs` around lines 34 - 41, Remove the unused storedPiano parameter and return statement from seed, and update its comment to describe only the state initialization and caller-controlled localStorage behavior. Keep all existing seed call sites and test behavior unchanged.
1-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocstring claims the keys refusal path is "pinned" here, but no test exercises it.
The comment states this file pins both refusal paths — drums skipping Tab and "a keys track never cycles at all" — but only the drums case (lines 59-70) has an actual
t(...)test. TheKEYS_PATTERNbranch in_editorCycleViewMode()(callingsetStatus(...)and returning without dispatching any view change) is untested here, so a regression on that path wouldn't be caught by this suite despite the file's stated intent.Consider adding a fifth test seeding a keys-named arrangement and asserting
callsstays empty (nosetView/tabdispatch).t('a keys track never cycles (always stays on the roll)', () => { seed('Keys'); _editorCycleViewMode(); assert.deepStrictEqual(calls, [], 'keys track must not dispatch any view change'); });🤖 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/tab_view_cycle.test.mjs` around lines 1 - 10, Add a test alongside the existing view-cycle tests that seeds a keys-named arrangement, invokes _editorCycleViewMode(), and asserts the calls collection remains empty, covering the KEYS_PATTERN refusal path without any view-change dispatch.
49-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
localStorage.getItemstub isn't restored if the assertion fails, risking leakage into later tests.Both tests stub
localStorage.getItemthen reset it on the line right afterassert.deepStrictEqual. If the assertion throws (caught byt()'s try/catch), the reset never runs and the stale stub carries into subsequent tests via the sharedglobalThis.localStorage. It happens not to break the current 4-test suite (test 4 takes thetabViewModebranch unconditionally), but it's fragile for any future test added after these two.🧹 Proposed fix using try/finally
t('a fretted track on the roll advances INTO the Tab lens', () => { seed('Lead'); Object.assign(S, { filename: 'y.feedpak' }); localStorage.getItem = (k) => k.startsWith('editorViewPref:') ? JSON.stringify({ p1: 'piano' }) : null; calls.length = 0; - _editorCycleViewMode(); - assert.deepStrictEqual(calls, [['tab', true]], 'roll → Tab lens'); - localStorage.getItem = () => null; + try { + _editorCycleViewMode(); + assert.deepStrictEqual(calls, [['tab', true]], 'roll → Tab lens'); + } finally { + localStorage.getItem = () => null; + } });Apply the same
try/finallywrapping to the drums test (lines 59-70).🤖 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/tab_view_cycle.test.mjs` around lines 49 - 70, Wrap the stubbed localStorage.getItem usage in both tests, “a fretted track on the roll advances INTO the Tab lens” and “a drums track on the roll SKIPS Tab and wraps to String,” with try/finally blocks, placing the reset to the original no-op behavior in finally so it always executes even when assertions fail.
🤖 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/tab_view_cycle.test.mjs`:
- Around line 34-41: Remove the unused storedPiano parameter and return
statement from seed, and update its comment to describe only the state
initialization and caller-controlled localStorage behavior. Keep all existing
seed call sites and test behavior unchanged.
- Around line 1-10: Add a test alongside the existing view-cycle tests that
seeds a keys-named arrangement, invokes _editorCycleViewMode(), and asserts the
calls collection remains empty, covering the KEYS_PATTERN refusal path without
any view-change dispatch.
- Around line 49-70: Wrap the stubbed localStorage.getItem usage in both tests,
“a fretted track on the roll advances INTO the Tab lens” and “a drums track on
the roll SKIPS Tab and wraps to String,” with try/finally blocks, placing the
reset to the original no-op behavior in finally so it always executes even when
assertions fail.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a4c9de3c-2f9c-4d79-8196-6311dd16004f
📒 Files selected for processing (11)
CHANGELOG.mdscreen.htmlsrc/input.jssrc/key-view.jssrc/main.jssrc/menu-bar.jssrc/shortcuts.jssrc/state.jssrc/tempo.jstests/screen_markup.test.mjstests/tab_view_cycle.test.mjs
🚧 Files skipped from review as they are similar to previous changes (8)
- src/state.js
- screen.html
- src/shortcuts.js
- src/key-view.js
- src/menu-bar.js
- tests/screen_markup.test.mjs
- src/main.js
- CHANGELOG.md
item 1) alphaTex durations are absolute (:8 is an eighth in any meter), but the generator hard-coded 4 ticks per ruler beat, so a 6/8 bar engraved six QUARTER durations (overfull). Bar tick capacity now derives from the meter's denominator (16/den ticks per beat, quarter fallback) in both the note-bucketing and emission passes; the 16th quantization grid and beatMap alignment are unchanged, and 4/4 markup is bit-identical. Adds 6/8 + 7/8 suites (notes, rest fill, 16th subdivisions, meter change, exact bar-total sums) that fail on the pre-fix code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/alphatex.test.mjs (1)
133-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
barWholesilently under-counts unmatched tokens.The regex encodes the current alphaTex token grammar (rest/chord/note). If a future token shape is introduced (dotted duration, tie, tuplet, grace note) and doesn't match, it contributes
0to the sum instead of failing — so a bar-total assertion could silently pass on an incomplete/incorrect parse rather than catching the regression.Consider asserting that the regex consumed every token (e.g., compare match count against
s.trim().split(/\s+/).length, or throw if leftover unmatched text remains) so grammar drift fails loudly instead of silently.♻️ Proposed defensive check
function barWhole(bar) { const s = bar.replace(/\\ts \d+ \d+ ?/, ''); let sum = 0, m; const re = /(?:r|\([^)]*\)|\d+\.\d+)\.(\d+)/g; - while ((m = re.exec(s))) sum += 1 / Number(m[1]); + let matched = ''; + while ((m = re.exec(s))) { sum += 1 / Number(m[1]); matched += m[0]; } + if (matched.replace(/\s+/g, '') !== s.trim().replace(/\s+/g, '')) { + throw new Error(`barWhole: unmatched tokens in "${s}"`); + } return sum; }🤖 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/alphatex.test.mjs` around lines 133 - 141, Update barWhole so its token-matching logic verifies that every non-whitespace token in the normalized bar string is consumed by the regex; if any token is unmatched, fail loudly instead of omitting it from the duration sum. Preserve the existing duration calculation for valid rest, chord, and note tokens.
🤖 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/alphatex.test.mjs`:
- Around line 133-141: Update barWhole so its token-matching logic verifies that
every non-whitespace token in the normalized bar string is consumed by the
regex; if any token is unmatched, fail loudly instead of omitting it from the
duration sum. Preserve the existing duration calculation for valid rest, chord,
and note tokens.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ea3ba4ac-fb68-4e67-9304-4f28a5df16c3
📒 Files selected for processing (2)
src/alphatex.jstests/alphatex.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
- src/alphatex.js
…build
The Score-staff switch made _ensureApi rebuild the alphaTab api on the
SAME mount node (staveProfile is a construction-time setting). But
_destroyApi only tore down alphaTab's own api — it never removed our
capture-phase mousedown fallback, which is our own closure. So every
staff toggle stacked another live listener on the surviving mount, and
each subsequent beat click fired select() / editorSeekToTime() / status
updates once per accumulated listener.
Root cause: the manual mount.addEventListener('mousedown', ..., true) in
_ensureApi had no matching removeEventListener. Name the handler, drop it
in _destroyApi before nulling _apiMount. In PR #273 this path never fired
on a surviving node (rebuild happened only on a NEW mount, GC'd the old
listener), so the leak is specific to the staff-switch guard this PR adds.
Regression test (tests/tab_view_staff.test.mjs): three staff switches on
one mount keep exactly one mousedown listener; fails pre-fix (2 !== 1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The entry toggle refuses keys/drums, but switching TO such a track via the arrangement dropdown while the lens was already on bypassed that guard (editorSelectArrangement never cleared S.tabViewMode), and the view-cycle's own keys short-circuit returns before it can un-toggle either. The user was left stuck: the generator engraved `undefined.NaN.*` from keys/drums notes (no .string/.fret), alphaTab errored, and cycling couldn't escape. Enforce at the draw pass — the single visibility source of truth — so every currentArr-change entry point is covered by one guard: if the current track isn't fretted, drop the lens and let its normal view (the roll) take over. Regression tests in tab_view_cycle.test.mjs pin the keys/drums exit and the fretted keep-on (both new cases fail pre-fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@tests/tab_view_cycle.test.mjs`:
- Around line 105-111: Update the test around _tabViewPing so the 150 ms
debounced render is cleaned up before the test completes. Use the existing
tab-view teardown to cancel or flush the pending callback, or control it with a
fake timer and restore the timer state afterward, while preserving the
synchronous assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2b5d19a1-7ce7-4401-8d3d-60715bb2a5b9
📒 Files selected for processing (2)
src/tab-view-live.jstests/tab_view_cycle.test.mjs
…build
The Score-staff switch made _ensureApi rebuild the alphaTab api on the
SAME mount node (staveProfile is a construction-time setting). But
_destroyApi only tore down alphaTab's own api — it never removed our
capture-phase mousedown fallback, which is our own closure. So every
staff toggle stacked another live listener on the surviving mount, and
each subsequent beat click fired select() / editorSeekToTime() / status
updates once per accumulated listener.
Root cause: the manual mount.addEventListener('mousedown', ..., true) in
_ensureApi had no matching removeEventListener. Name the handler, drop it
in _destroyApi before nulling _apiMount. In PR #273 this path never fired
on a surviving node (rebuild happened only on a NEW mount, GC'd the old
listener), so the leak is specific to the staff-switch guard this PR adds.
Regression test (tests/tab_view_staff.test.mjs): three staff switches on
one mount keep exactly one mousedown listener; fails pre-fix (2 !== 1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#288) * feat(editor): standard notation in the score view (View > Score staff) The live score view grows a staff preference: tablature only (default, per the v1 call), standard notation only, or both staves together — same generated alphaTex either way, alphaTab derives pitch from tuning + fret, so notation comes free of new generation code. - tab-view-live.js: _scoreStaffProfilePure maps the preference to alphaTab's StaveProfile key (unknown/legacy stored values degrade to tab, never throw into the renderer); the api tracks the staff it was built with and rebuilds on change (staveProfile is construction-time); editorSetTabViewStaff validates + persists (localStorage, a reading preference like loop-snap) and ENTERS the view when it's off. - menu-bar.js: a Score-staff radio trio in the View menu, same ctx/ dispatch pattern as the loop-snap trio; checkmarks resolve at open. - state.js: S.tabViewStaff ('tab' default). Live-verified on :8001 (AC/DC pak): all three staves engrave (tab frets / treble-clef pitches with accidentals / braced grand system), click-to-select works under 'both' (3 notes), the real View-menu radio renders + dispatches + enters the view, preference persists. 148/148, lint baseline, new tab_view_staff suite + menu-model radio pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q * fix(editor): remove stale tab-view click listener on staff-profile rebuild The Score-staff switch made _ensureApi rebuild the alphaTab api on the SAME mount node (staveProfile is a construction-time setting). But _destroyApi only tore down alphaTab's own api — it never removed our capture-phase mousedown fallback, which is our own closure. So every staff toggle stacked another live listener on the surviving mount, and each subsequent beat click fired select() / editorSeekToTime() / status updates once per accumulated listener. Root cause: the manual mount.addEventListener('mousedown', ..., true) in _ensureApi had no matching removeEventListener. Name the handler, drop it in _destroyApi before nulling _apiMount. In PR #273 this path never fired on a surviving node (rebuild happened only on a NEW mount, GC'd the old listener), so the leak is specific to the staff-switch guard this PR adds. Regression test (tests/tab_view_staff.test.mjs): three staff switches on one mount keep exactly one mousedown listener; fails pre-fix (2 !== 1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: ChrisBeWithYou <chris@rifflarr.local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What
The view cycle grows a third full mode — String → Piano roll → Tab: the timeline area becomes live-engraved tablature of the track being edited, re-rendered on every committed edit. No save, no GP5 round-trip, no other plugin — this is the current in-memory chart, unlike the existing Tab preview modal (which stays: it proofreads the last-saved pack).
v1 contract:
How
src/alphatex.js— a pure generator (_alphaTexFromNotesPure): notes + beat grid + injectedbeatOf→ alphaTex text, plus abeatMap[barIdx][beatIdx] → source-note refsaligned 1:1 with emitted beats (rests arenull) — exactly the coordinates alphaTab's click events report, so a click maps straight back to editor notes. Handles meter changes (\tsre-emission), chord grouping, the string-number flip, alphaTab's octave convention in\tuning(high E MIDI 64 =e5, deliberately NOT the editor'smidiToNoteflavour — inlined + commented so nobody "fixes" it), capo, and honest pickup/tail skip counts.src/tab-view-live.js— the lens.S.tabViewModeis an orthogonal flag likepartsViewMode; the other mode toggles clear it and the draw pass is the single visibility truth (pings while on, hides the moment it's off — no toggle needs teardown knowledge). Renders via the same pinned CDN alphaTab as the preview (shared memoized loader + font dir;enablePlayeris on ONLY for the interaction layer — no soundfont download, no cursor, the editor owns all audio). Click-to-select ridesbeatMouseDownplus a capture-phase DOM fallback (alphaTab stops propagation on events it consumes) throughrenderer.boundsLookup.getBeatAtPos._editorCycleViewMode), shortcut-registry row (toggleTabView, unbound by default), View menu entry, session-teardown api destroy, mount div inscreen.html.One ordering trap found live and pinned in a comment:
_rendermust call_ensureApibefore assigning_beatMap— the api build path runs_destroyApi(), which nulls the map (this silently disarmed click-to-select until a probe caught it).Testing
tests/alphatex.test.mjs(new, 7 cases, fails on main): octave convention, string flip, chord + rest fill exactness, meter-change\ts, beatMap↔token alignment, pickup/tail counts, header material.🤖 Generated with Claude Code
https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Summary by CodeRabbit