feat(editor): song key/scale + in-key highlight for the piano roll - #108
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds piano-roll key/scale controls, persisted key preferences, rendering changes that dim out-of-key notes, shortcut wiring, scale-membership tests, and an unreleased changelog entry. ChangesPiano-roll in-key highlighting feature
Estimated code review effort: 3 (Moderate) | ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.jsast-grep timed out on this file Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
screen.js (1)
1863-1869: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winHoist
_activeKeyHighlight()out of the per-note render path.
drawPianoLanescomputes_activeKeyHighlight()once per call (Line 1584), but_drawPianoNoterecomputes it for every note (Line 1865), which re-runslocalStorage.getItem(viaeditorKeyHighlightEnabled()) and re-validatesS.editorKeyon every note draw. If_drawPianoNoteis invoked in a per-note loop during frequent redraws, this adds avoidable synchronous localStorage reads to a hot rendering path.Consider computing the active highlight once per frame (e.g., in the caller that iterates notes, or cache it alongside the lane pass) and passing it into
_drawPianoNote, mirroring the pattern already used indrawPianoLanes.♻️ Suggested direction
-function _drawPianoNote(n, selected) { +function _drawPianoNote(n, selected, hl) { const midi = noteToMidi(n.string, n.fret); if (midi < pianoRange.lo || midi > pianoRange.hi) return; ... - const hl = _activeKeyHighlight(); const outOfKey = !!hl && !_pcInScalePure(midi % 12, hl.tonic, hl.scale);Then compute
hlonce in the note-iteration loop and pass it through.🤖 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 1863 - 1869, Hoist the active key highlight lookup out of the per-note render path so _drawPianoNote no longer calls _activeKeyHighlight() for every note. Compute the highlight once per frame or per note-iteration in the caller that draws notes, then pass it into _drawPianoNote, following the same pattern already used by drawPianoLanes. Keep the outOfKey alpha logic in _drawPianoNote, but use the precomputed hl value instead of recalculating it.
🤖 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 1863-1869: Hoist the active key highlight lookup out of the
per-note render path so _drawPianoNote no longer calls _activeKeyHighlight() for
every note. Compute the highlight once per frame or per note-iteration in the
caller that draws notes, then pass it into _drawPianoNote, following the same
pattern already used by drawPianoLanes. Keep the outOfKey alpha logic in
_drawPianoNote, but use the precomputed hl value instead of recalculating it.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e427a733-3c51-44d7-a26b-5c92c8fc05a7
📒 Files selected for processing (4)
CHANGELOG.mdscreen.htmlscreen.jstests/scale_membership.test.js
Directly answers the "note roll toggle for in-key notes, account for the key of the song" ask. A Key control (tonic + scale/mode) and an In-key toggle appear in the toolbar when a keys arrangement is active. With the highlight on, drawPianoLanes gives out-of-key rows a neutral desaturating wash and _drawPianoNote dims out-of-key notes — chromatic notes read as chromatic without being hidden or flagged wrong (no red). - SCALE_INTERVALS (@pure:scale): 13 tonic-relative pitch-class sets — major, the seven diatonic modes, harmonic/melodic minor, major/minor pentatonic, blues, chromatic (which shades nothing → atonal regions are unshaded). Covers the genre span pop→classical→prog→blues. - _pcInScalePure(pc, tonic, scale): octave-invariant membership; unknown scale / non-finite inputs default to IN-KEY so a bad state never paints the whole roll out-of-key. - Key is a per-song editor pref (localStorage keyed by S.filename, never the feedpak — a view aid, not chart data); highlight on/off is a global pref. Lazily loaded per song in the control refresh (no load-path edit). The control group is gated visible to keys mode. Registry command + toolbar button + palette; no key binding (avoids collision). This is deliberately the broad-stroke infrastructure; a later pass reads authored key regions (keys.json) and adds enharmonic row spelling + the chord-tone tier + guitar-lane scale-degree tinting. Tests: tests/scale_membership.test.js (9 cases) drive the real _pcInScalePure: C-major naturals/accidentals, membership proven to use BOTH the tonic and the scale argument (F# out of C major, in of G major; Bb out of C major, in of C mixolydian/minor), octave-invariance, wrap-around tonic (A minor), pentatonic/blues subsets, chromatic = all-in, adversarial unknown-scale/NaN → in-key, and every declared scale is a valid deduped 0..11 tonic-rooted set. 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
c416942 to
3d6736f
Compare
_drawPianoNote ran once per visible note and called _activeKeyHighlight()
each time, which chains to localStorage.getItem('editorKeyHighlight').
drawNotes loops every visible note on every draw() (playback + scroll), so
the highlight state was re-read from synchronous storage O(visibleNotes)
times per frame. Resolve it once per draw in drawNotes (matching the sibling
drawPianoLanes) and pass it into _drawPianoNote as a parameter. Behavior is
unchanged: all notes in a single draw already saw a consistent snapshot.
Also guard _persistEditorKey/_loadEditorKeyIfNeeded to no-op when S.filename
is empty, so unsaved songs no longer read/write the shared bare `editorKey:`
slot and collide with each other.
Add a by-construction regression test pinning that _drawPianoNote takes the
highlight as a param and never resolves it (or touches storage) itself, and
that drawNotes hoists exactly one lookup and threads it through.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/key_highlight_hoist.test.js (1)
24-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winComment-stripping in
body()misses block comments.Only
//line comments are stripped (line 38) before thelocalStorage/_activeKeyHighlight(checks run. A future block comment (/* ... */) inside either function mentioning those tokens would cause a false test failure even though the code is compliant.♻️ Strip block comments too
if (depth === 0) { - return src.slice(start, i + 1).replace(/\/\/[^\n]*/g, ''); + return src.slice(start, i + 1) + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/\/\/[^\n]*/g, ''); }🤖 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/key_highlight_hoist.test.js` around lines 24 - 43, The body() helper in the key highlight hoist test only removes line comments, so block comments can still trigger false positives in the code-shape assertions. Update the comment-stripping logic in body() to remove both // and /* ... */ comments before checking for localStorage and _activeKeyHighlight so the function scan stays robust.
🤖 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/key_highlight_hoist.test.js`:
- Around line 24-43: The body() helper in the key highlight hoist test only
removes line comments, so block comments can still trigger false positives in
the code-shape assertions. Update the comment-stripping logic in body() to
remove both // and /* ... */ comments before checking for localStorage and
_activeKeyHighlight so the function scan stays robust.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 09160ce5-1b1c-40e4-9abf-a931b46b8286
📒 Files selected for processing (2)
screen.jstests/key_highlight_hoist.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
- screen.js
Resolve CHANGELOG.md [Unreleased] conflict (take-both: keep the key/scale entry alongside the section-undo/duplicate/inspector-time/coverage entries). Fix cross-PR integration break: #104-#107 landing together left EditHistory ._afterEdit() (in the @pure:edit-history block) bumping _coverageEditGen, which is declared outside that block — so the undo-test sandboxes that extract edit-history in isolation threw "_coverageEditGen is not defined". Guard the bump with typeof, matching the isKeysMode guard two lines below. Full JS suite 46/46 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ding pitch (#115) * feat(editor): in-key highlight on the fretted lanes — capo-aware sounding pitch Roadmap 4.16a remainder (guitar-lane scale-degree tint). Extends the merged song key/scale highlight (#108) from the piano roll to guitar/bass lanes. - New _soundingPitchPure: openMidi + tuning offset + CAPO + fret, capo added exactly ONCE. Chart frets are capo-relative — verified against core lib/song.py pitch_from_base, the single source of the formula the tuner and highway scale-degree derivation share. - The flagged double-count trap is now pinned in code and tests: _absolutePitch (string-moves) still deliberately omits capo (it cancels when comparing two pitches on one arrangement) and both helpers document the division of labor. - Out-of-key fretted notes dim (body alpha cc->55, softened fret number — the piano-roll treatment; never red), unresolvable pitches stay fully lit. Highlight context is hoisted once per draw, zero per-note arrangement work. Key controls now show for any pitched arrangement. Tests: tests/fret_key_highlight.test.js (8 cases) — the formula against known pitches, Drop-D + capo composition, the capo-flips-membership case an uncapoed resolver gets wrong, and the omits-capo pin on _absolutePitch. Full JS suite green except tests/section_coverage.test.js, which fails on current MAIN itself (pre-existing _afterEdit/#107 merge interaction). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu * test(editor): document extractFn brace-count assumption for #115 (CodeRabbit nitpick) 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> Co-authored-by: byrongamatos <xasiklas@gmail.com>
…l (read-first) (#119) * feat(editor): in-key highlight on the fretted lanes — capo-aware sounding pitch Roadmap 4.16a remainder (guitar-lane scale-degree tint). Extends the merged song key/scale highlight (#108) from the piano roll to guitar/bass lanes. - New _soundingPitchPure: openMidi + tuning offset + CAPO + fret, capo added exactly ONCE. Chart frets are capo-relative — verified against core lib/song.py pitch_from_base, the single source of the formula the tuner and highway scale-degree derivation share. - The flagged double-count trap is now pinned in code and tests: _absolutePitch (string-moves) still deliberately omits capo (it cancels when comparing two pitches on one arrangement) and both helpers document the division of labor. - Out-of-key fretted notes dim (body alpha cc->55, softened fret number — the piano-roll treatment; never red), unresolvable pitches stay fully lit. Highlight context is hoisted once per draw, zero per-note arrangement work. Key controls now show for any pitched arrangement. Tests: tests/fret_key_highlight.test.js (8 cases) — the formula against known pitches, Drop-D + capo composition, the capo-flips-membership case an uncapoed resolver gets wrong, and the omits-capo pin on _absolutePitch. Full JS suite green except tests/section_coverage.test.js, which fails on current MAIN itself (pre-existing _afterEdit/#107 merge interaction). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu * feat(editor): per-part view switcher — any fretted part opens in the piano roll (read-first) EDITOR-VIEW-MODALITY-DESIGN P1 (VA.1+VA.2, decisions V1-V4/V9). The editing view was derived from the arrangement NAME; it is now a per-part choice. - viewFor(part): per-part pref in editor localStorage keyed song + stable part id (never index/display-name; keys parts piano-locked). Kind inference stays the default. - isKeysMode() split: piano-SURFACE predicate (draw geometry, hit-testing, viewport) vs new isKeysArr() keys-DATA predicate (string moves, chord-sibling grouping, anchors, resize chord-expansion) — a fretted part in the roll still groups chords and keeps string-move machinery (P5 position cycling depends on exactly this). - Read-first roll for fretted parts: one sounding-pitch mapping (_rollMidiForNote via _soundingPitchPure — capo once) hoisted per pass and shared by draw, hitNote, marquee, and updatePianoRange; null pitches skip, never render wrong. - Edit-lock (V4): central gate in EditHistory.exec (typeof-guarded for extracted-test envs) + the live-mutating drag starts (move/resize) + dblclick add + EOF right-click edit; selection still works; a visible pill + status explain why. Lock lifts live on switching back. - Toolbar String/Piano-roll segmented switcher + registry cycleViewMode; selection/drag/note-UI cleared on switch (V3). STACKED ON #115 (feat/editor-key-highlight-guitar) — needs its _soundingPitchPure; merge #115 first. Tests: tests/view_switcher.test.js (11) — pure view resolution, pref persistence/rename stability over stub localStorage, sounding-pitch roll mapping + viewport fit (asserts NOT the wire packing), and the exec gate (inert+notice / regression / live-unlock). Full suite green except the pre-existing CRLF section_coverage failure (#116). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu * feat(editor): review fixes for #119 (view switcher) Read-only roll (fretted part in the piano roll) was enforced only at the EditHistory exec chokepoint and the mouse/right-click-add handlers. Two gaps: - exec blocked ALL commands, including songScope (drum tab, tempo grid) edits, so switching an unrelated part into the roll froze tempo/drum editing. songScope commands now pass through the lock (exec + undo/redo). - Several note-edit paths bypass EditHistory entirely and so escaped the lock: note-scope undo/redo, promptSlide/promptSlideUnpitch, the inspector setters (editorInspectorSetTech/SetFlag), and the context-menu editorToggleTech. The context menu opens in the roll under the default right-click behavior and the inspector renders for any selection, so all were reachable. Each is now guarded with _rollReadOnly()/_rollLockNotice. Regression tests (tests/view_switcher.test.js) fail on pre-fix code. 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> Co-authored-by: byrongamatos <xasiklas@gmail.com>
Summary
A broad-stroke pass on the key-awareness thread — directly answering Christian's ask for "a toggle for notes that are in key only, account for the key of the song." When a keys arrangement is active, the toolbar shows a Key control (tonic + scale/mode) and an In-key toggle. With the highlight on:
drawPianoLanes);_drawPianoNote).Nothing is hidden or flagged as wrong — no red, and chromatic notes stay fully visible and editable (barbershop, prog modal interchange, and atonal passages are correctly chromatic, per the harmony charrette seat).
SCALE_INTERVALSships 13 tonic-relative pitch-class sets: major, the seven diatonic modes, harmonic/melodic minor, major/minor pentatonic, blues, and chromatic (shades nothing) for atonal regions._pcInScalePure(pc, tonic, scale)is octave-invariant; unknown scale / non-finite inputs default to in-key so a bad state never paints the whole roll out-of-key.S.filename, never the feedpak — a view aid, not chart data); the highlight is a global preference. Lazily loaded per song in the control refresh (no song-load-path edit). Registry command + toolbar button + command palette; no key binding (avoids collision).Deliberately the broad-stroke infrastructure: a later pass reads authored key regions (
keys.json), adds enharmonic key-signature row spelling, the chord-tone emphasis tier, and guitar-lane scale-degree tinting.Verification — held to the testing habits
Display feature, so the habits that bite are adversarial inputs and proving the helper uses all its arguments.
tests/scale_membership.test.js(9 cases) drive the real_pcInScalePure: C-major naturals vs accidentals; membership proven to use both the tonic (F# out of C major, in of G major) and the scale (Bb out of C major, in of C mixolydian/minor); octave-invariance; wrap-around tonic (A minor); pentatonic/blues subsets; chromatic = all-in; adversarial unknown-scale/NaN→ in-key; and every declared scale is a valid, deduped,0..11, tonic-rooted set.node --checkclean; all 26 JS test files pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu
Summary by CodeRabbit