Skip to content

feat(editor): guide claps — audible ticks for charted notes during playback - #90

Merged
byrongamatos merged 2 commits into
mainfrom
feat/editor-guide-clap
Jul 6, 2026
Merged

feat(editor): guide claps — audible ticks for charted notes during playback#90
byrongamatos merged 2 commits into
mainfrom
feat/editor-guide-clap

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

The editor has zero note sonification — you cannot verify note placement by ear, which hurts most exactly where charting-by-ear matters (drum placement, dense passages). This PR adds guide claps: a Claps transport toggle (shortcut C in both the FeedBack and EOF profiles) that ticks each charted event during playback — drum hits in drum-edit mode, the current arrangement's notes everywhere else.

First slice of the DAW-workspace guide-playback design (percussive clap now; pitched per-instrument guide voices ride the same scheduler/bus in a later PR).

Engine

  • Lookahead scheduler: setInterval (25 ms cadence, 120 ms window) anchored on the existing transport anchor (playStartWall/playStartTime) — deliberately not the rAF draw loop, so clap timing stays accurate while the canvas is busy.
  • Seek/loop safe: every playback (re)start funnels through _startAudioSourceAtCursor, which now cancels queued voices and resets the schedule watermark — no ghost claps after a loop wrap or scrub. Timer-stall recovery skips missed past events instead of machine-gunning them late.
  • Level safety: chord stacks dedupe to a single tick (1 ms buckets) so N simultaneous notes can't sum into a louder transient (drum flams >1 ms apart keep both hits); the voice is a soft 3 ms-attack / 45 ms-decay tick, and playback now routes through a new shared master bus — reference audio and claps each get a gain node, summed through a DynamicsCompressor limiter (threshold −1, ratio 20) instead of connecting raw to destination.
  • Toggle persists as an editor preference (localStorage, never in the pack); joins the command registry, both shortcut profiles, and the transport bar.

Verification

  • node --check screen.js clean
  • New tests/guide_clap.test.js — 8 cases via the @pure:guide-clap block: window semantics, half-open boundaries (no double-fire across adjacent windows), chord-stack dedupe vs flam preservation, degenerate inputs, dense-chart binary search, transport-clock mapping
  • All 26 JS test files pass (shortcut-profile tests green with the new command)
  • Audible smoke on the testbed pending (WebAudio needs a live browser); scheduler math, seek-reset wiring, and dispatch are covered headlessly

🤖 Generated with Claude Code

https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu

Summary by CodeRabbit

  • New Features
    • Added a “Claps” (Guide claps) playback toggle with toolbar control and keyboard shortcut (C), persisted as a saved editor preference.
    • When enabled, charted events are sonified with tightly scheduled clap ticks using a transport lookahead approach.
  • Bug Fixes
    • Playback start/seek/loop behavior now resets the clap scheduler and cancels any queued claps to prevent “ghost” sounds.
    • Near-simultaneous events are deduplicated to avoid double-firing.
  • Tests
    • Added automated tests covering clap scheduling/windowing and timing conversions.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a Guide claps transport toggle that sonifies charted events during playback. It includes keyboard and toolbar controls, AudioContext-based scheduling with lookahead timing, shared audio routing with limiter, seek/loop cancellation, persistence, and tests.

Changes

Guide Claps Feature

Layer / File(s) Summary
Shortcut and UI toggle wiring
screen.js, screen.html
Registers toggleGuideClap for key C in both shortcut profiles, dispatches it to _editorToggleGuideClap(), and adds a toolbar Claps button wired to editorToggleGuideClap().
Playback integration with master bus and clap scheduler
screen.js
Resets and syncs the guide clap scheduler on playback start/stop and cancels queued clap voices on stop.
Guide clap scheduling engine implementation
screen.js
Adds pure helpers, scheduler state, a limiter-backed guide-clap bus, timer-driven clap scheduling, and the editorGuideClap toggle with localStorage persistence and UI refresh.
Tests and changelog
tests/guide_clap.test.js, CHANGELOG.md
Adds tests for window selection, deduplication, sanitization, loop-end clamping, and time mapping, plus a changelog entry for the feature.

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

🚥 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 describes the main change: adding guide claps as audible ticks during playback in the 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-guide-clap

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
screen.js (1)

5130-5142: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Avoid rebuilding the full event-time array (and reading localStorage) every tick.

_guideTick runs every 25 ms and calls _guideSourceTimes(), which .map()s the entire note/hit list into a fresh array each time (~40 allocations/sec over the whole chart; noticeable on dense charts). editorGuideClapEnabled() also hits localStorage on every tick. Consider caching the sorted times array (invalidated on edit/arrangement/mode change) and caching the enabled flag in memory, updating it only in _editorToggleGuideClap.

🤖 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 5130 - 5142, _guideTick is doing too much work on
every timer pulse by rebuilding the full source-time list via
_guideSourceTimes() and re-reading editorGuideClapEnabled() from localStorage.
Update _guideTick to use an in-memory cached sorted times array instead of
recomputing the map each tick, and invalidate that cache only when the chart
data/arrangement/mode changes. Also cache the guide-clap enabled state in memory
and refresh it from _editorToggleGuideClap rather than polling localStorage in
the hot path.
🤖 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 `@screen.js`:
- Around line 5086-5093: The arrangement branch in _guideSourceTimes returns
note times directly from notes(), but those n.time values can become unsorted
after edits, which breaks the binary search in _guideClapTimesInWindowPure.
Update _guideSourceTimes to return a sorted list of times for the arrangement
path (while leaving the S.drumEditMode/S.drumTab.hits path unchanged), or ensure
the same sorting happens whenever note times are mutated so the guide times are
always in ascending order.

---

Nitpick comments:
In `@screen.js`:
- Around line 5130-5142: _guideTick is doing too much work on every timer pulse
by rebuilding the full source-time list via _guideSourceTimes() and re-reading
editorGuideClapEnabled() from localStorage. Update _guideTick to use an
in-memory cached sorted times array instead of recomputing the map each tick,
and invalidate that cache only when the chart data/arrangement/mode changes.
Also cache the guide-clap enabled state in memory and refresh it from
_editorToggleGuideClap rather than polling localStorage in the hot path.
🪄 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: 3d3ce69b-a3c8-4e36-bb19-875d39614b30

📥 Commits

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

📒 Files selected for processing (4)
  • CHANGELOG.md
  • screen.html
  • screen.js
  • tests/guide_clap.test.js

Comment thread screen.js

@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.

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

5209-5213: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cancel queued clap voices when disabling the toggle.

Turning claps off only syncs the timer, so voices already scheduled in the 120 ms lookahead window can still fire after the UI says “off”. Stop handling cancels voices separately; the off-toggle path should do the same.

Proposed fix
     try { localStorage.setItem('editorGuideClap', next ? '1' : '0'); } catch (_) {}
     _refreshGuideBtn();
     _guideTimerSync();
+    if (!next) _guideCancelVoices();
     setStatus(next
🤖 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 5209 - 5213, The off-toggle path in
_editorToggleGuideClap only updates storage and calls _guideTimerSync, so queued
clap voices can still fire after the toggle is disabled. Update
_editorToggleGuideClap to explicitly cancel any scheduled clap voices when next
is false, using the same cancel logic already used elsewhere for stopping
guide/clap playback, then keep the existing refresh and sync calls so the UI and
timer state stay aligned.
🧹 Nitpick comments (1)
screen.js (1)

5068-5068: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Cache sanitized guide times outside the 25 ms scheduler tick.

_guideTick() now maps, filters, and sorts the full guide source every tick. That makes dense charts pay O(n log n) about 40 times/sec, undermining the lookahead scheduler’s canvas-load goal. Prefer caching the sanitized/sorted times and invalidating on note/hit edits, arrangement changes, drum mode changes, or playback reset.

Also applies to: 5108-5114, 5166-5166

🤖 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` at line 5068, _cache sanitized guide times instead of recomputing
them inside _guideTick(); the current tick path is mapping, filtering, and
sorting the full guide source every 25 ms, so move that sanitized/sorted result
into a reusable cache and have _guideTick() consume it. Invalidate or rebuild
the cache from the places that change guide data or timing, including note/hit
edits, arrangement changes, drum mode changes, and playback reset, so the
scheduler stays cheap while keeping GUIDE_TICK_MS behavior correct.
🤖 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 `@screen.js`:
- Around line 5077-5079: The editor guide clap toggle currently reads only from
localStorage in editorGuideClapEnabled(), so when storage access throws the UI
can switch to “on” but the feature still evaluates as off. Update the toggle
flow around editorGuideClapEnabled() and the related setter logic to maintain an
in-memory fallback state whenever localStorage.getItem() or setItem() fails, and
have the enabled check consult that fallback before returning false.

---

Outside diff comments:
In `@screen.js`:
- Around line 5209-5213: The off-toggle path in _editorToggleGuideClap only
updates storage and calls _guideTimerSync, so queued clap voices can still fire
after the toggle is disabled. Update _editorToggleGuideClap to explicitly cancel
any scheduled clap voices when next is false, using the same cancel logic
already used elsewhere for stopping guide/clap playback, then keep the existing
refresh and sync calls so the UI and timer state stay aligned.

---

Nitpick comments:
In `@screen.js`:
- Line 5068: _cache sanitized guide times instead of recomputing them inside
_guideTick(); the current tick path is mapping, filtering, and sorting the full
guide source every 25 ms, so move that sanitized/sorted result into a reusable
cache and have _guideTick() consume it. Invalidate or rebuild the cache from the
places that change guide data or timing, including note/hit edits, arrangement
changes, drum mode changes, and playback reset, so the scheduler stays cheap
while keeping GUIDE_TICK_MS behavior correct.
🪄 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: 5cf9ddc6-9916-4c68-a1a2-8486200368bc

📥 Commits

Reviewing files that changed from the base of the PR and between 8684e73 and 31866df.

📒 Files selected for processing (2)
  • screen.js
  • tests/guide_clap.test.js

Comment thread screen.js
Comment on lines +5077 to +5079
function editorGuideClapEnabled() {
try { return localStorage.getItem('editorGuideClap') === '1'; }
catch (_) { return false; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep an in-memory toggle state when localStorage is unavailable.

If localStorage.getItem() or setItem() throws, next becomes true and the status says claps are on, but editorGuideClapEnabled() still returns false, so the feature never enables.

Proposed fix
+let _editorGuideClapOn = (() => {
+    try { return localStorage.getItem('editorGuideClap') === '1'; }
+    catch (_) { return false; }
+})();
+
 function editorGuideClapEnabled() {
-    try { return localStorage.getItem('editorGuideClap') === '1'; }
-    catch (_) { return false; }
+    return _editorGuideClapOn;
 }
 
 function _editorToggleGuideClap() {
     const next = !editorGuideClapEnabled();
+    _editorGuideClapOn = next;
     try { localStorage.setItem('editorGuideClap', next ? '1' : '0'); } catch (_) {}

Also applies to: 5209-5216

🤖 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 5077 - 5079, The editor guide clap toggle currently
reads only from localStorage in editorGuideClapEnabled(), so when storage access
throws the UI can switch to “on” but the feature still evaluates as off. Update
the toggle flow around editorGuideClapEnabled() and the related setter logic to
maintain an in-memory fallback state whenever localStorage.getItem() or
setItem() fails, and have the enabled check consult that fallback before
returning false.

ChrisBeWithYou and others added 2 commits July 6, 2026 21:36
…ayback

The editor had zero note sonification: charting by ear was impossible,
especially for drum placement. Adds a "Claps" transport toggle (key C in
both shortcut profiles) that ticks each charted event during playback —
drum hits in drum-edit mode, the current arrangement's notes elsewhere.

Engine (first slice of the DAW-workspace guide-playback design):
- setInterval lookahead scheduler (25 ms cadence, 120 ms window) anchored
  on playStartWall/playStartTime — never the rAF draw loop, so clap timing
  survives heavy canvas load.
- Every (re)start via _startAudioSourceAtCursor cancels queued voices and
  resets the window, so seeks and loop wraps can't fire ghost claps.
- Chord stacks dedupe to one tick (1 ms buckets) so simultaneous notes
  can't sum into a louder transient; soft 3 ms attack / 45 ms decay voice.
- New shared master bus: reference audio and claps each get a gain node,
  summed through a DynamicsCompressor limiter (threshold -1, ratio 20) —
  playback no longer connects raw to destination.
- Toggle persists as an editor preference (localStorage), joins the
  command registry + both key profiles + the transport bar.

Tests: tests/guide_clap.test.js (8 cases: window semantics, half-open
boundaries, chord dedupe vs drum flams, dense-chart binary search,
transport mapping). 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
…oop-end clamp

Reviewer-confirmed fixes to the guide-clap scheduler:

- Sanitize _guideSourceTimes: filter non-finite + sort ascending (new
  _guideSanitizeTimesPure) before the window query, matching every other
  time-array consumer. A NaN time reached osc.start(NaN) and threw inside
  the tick; an unsorted array made the early-terminating scan drop claps.
- Keep the reference recording on a transparent path straight to
  destination; route only the guide voices through the limiter
  (guideGain -> limiter -> destination). The limiter no longer colors
  loud/brickwalled reference audio when claps are off.
- Clamp the lookahead window end to the loop end while looping
  (new _guideWindowEndPure) so no clap is scheduled past the boundary
  before the rAF wrap cancels it (ghost claps).
- Persist the last-fired 1 ms bucket key across ticks so a chord
  straddling a window boundary can't double-fire; reset on seek/wrap.
- Extend guide_clap tests: unsorted+NaN sanitizing and loop-end clamp.

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