Skip to content

feat(editor): count-in — N bars of clicks before playback starts - #203

Merged
byrongamatos merged 3 commits into
mainfrom
feat/editor-count-in
Jul 11, 2026
Merged

feat(editor): count-in — N bars of clicks before playback starts#203
byrongamatos merged 3 commits into
mainfrom
feat/editor-count-in

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

The charrette's Count, built as its own feature — this is what the B2 transport bar's Count-in LCD cell (#195) and the Count mode toggle were explicitly deferred behind; both can now follow as small PRs.

A Count: off / 1 / 2 / 4 bars select next to the metronome. On play, the pre-roll derives from the grid at the cursor — beats-per-bar from the enclosing measure's downbeat span, beat length from the local gap — so a 3/4 section counts three beats at its own tempo. No tempo map falls back to a 4/4 bar at 120 (a play gesture never fails).

One anchor shift

_anchorTransportAtCursor / _startAudioSourceAtCursor take an optional preRoll and pin the wall anchor (and the BufferSource start) into the future by it. The whole chart→ctx mapping derives from that anchor pair, so the guide/click scheduler and the MIDI record clock follow with zero extra plumbing — recording gets a count-in for free. playbackTick clamps the cursor at the start during the pre-roll. Clicks schedule through the existing _metroClickVoiceAt (tracked voices — a stop mid-count cancels them like any queued clap). Loop wraps and mid-play seeks route through _restartPlaybackAt and stay immediate by design.

Pref: editorCountIn (editor pref, never the pack), seeded at initAudio like the snap-mode pref.

Tests

tests/count_in.test.mjs (5, real-import): the 4/4 plan (duration, click offsets, downbeat-only accents), multi-bar accents, meter/tempo at the cursor on a drifting mixed-meter grid, the no-grid fallback, junk refusals. Full suite 91/91 · ESLint 0 errors · CHANGELOG updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Summary by CodeRabbit

  • New Features

    • Added a Count-in control with options for 1, 2, or 4 bars before playback or recording.
    • Count-in clicks follow the cursor’s meter and tempo, with sensible defaults when timing information is unavailable.
    • Playback and recording remain synchronized during the count-in period.
  • Bug Fixes

    • Prevented count-in clicks from being canceled before they play.
    • Prevented pre-roll timing from creating negative chart positions.
  • Tests

    • Added coverage for count-in timing, accents, tempo changes, fallback behavior, and playback integration.

ChrisBeWithYou and others added 2 commits July 10, 2026 15:01
The charrette's Count, built as its own feature (the B2 transport bar's
Count-in LCD cell and the Count mode toggle were both deferred because
the editor had nothing to write through to — this unblocks them).

A 'Count: off / 1 / 2 / 4 bars' select next to the metronome. On play,
_countInPlanPure (src/transport.js, dep-free) derives the pre-roll from
the grid AT THE CURSOR — beats-per-bar from the enclosing measure's
downbeat span, beat length from the local gap — so a 3/4 section counts
three beats at its own tempo. No grid falls back to a 4/4 bar at 120 so
a play gesture never fails; junk bar counts refuse.

The engine change is ONE anchor shift: _anchorTransportAtCursor and
_startAudioSourceAtCursor take an optional preRoll and pin the wall
anchor (and the BufferSource start) into the future by it. Because the
whole chart-to-ctx mapping derives from that anchor pair, the guide/
click scheduler and the MIDI record clock follow with zero extra
plumbing — recording gets a count-in for free. playbackTick clamps the
cursor at the start position while the pre-roll runs (the raw chart
time reads negative there). Count-in clicks schedule through the
existing _metroClickVoiceAt (tracked voices — a stop mid-count cancels
them like any queued clap). Loop wraps and mid-play seeks route through
_restartPlaybackAt and stay immediate by design.

Pref: localStorage 'editorCountIn' (0/1/2/4), seeded into the select at
initAudio like the snap-mode pref; never written to the pack.

tests/count_in.test.mjs (5): the 4/4 plan (duration, click offsets,
downbeat-only accents), multi-bar accents, meter/tempo AT the cursor on
a drifting mixed-meter grid, the no-grid fallback, junk refusals.
Suite 91/91, ESLint 0 errors.

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

The count-in clicks were scheduled onto _guideVoices BEFORE
_anchorTransportAtCursor, whose _guideResetSchedule() -> _guideCancelVoices()
osc.stop()s every queued voice — cancelling the clicks before they could sound.
The count-in was audibly silent (N bars of dead pre-roll, then playback).
Schedule the clicks AFTER the anchor instead; nothing between there and
playback start cancels voices.

Also clamp the MIDI record clock (chartTimeNow) at playStartTime, mirroring
playbackTick: during a pre-roll the anchor sits in the future, so a note played
over the count would otherwise record at a negative/pre-region chart time.
Post-start notes are already >= playStartTime, so the clamp is a no-op for them.

Adds tests/count_in_wiring.test.mjs: drives startPlayback with a fake
AudioContext and asserts no click voice is cancelled (distinguishing the
natural scheduled osc.stop(t) from the no-arg cancel stop), plus the recorder
clamp rule. Verified the test fails when the scheduling is moved back before
the anchor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 45 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: 5103c417-3416-40d9-b33a-9d8ffbebd442

📥 Commits

Reviewing files that changed from the base of the PR and between 0e5ac8a and 6dea76b.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • screen.html
  • src/main.js
  • src/midi-record.js
📝 Walkthrough

Walkthrough

Adds configurable 1-, 2-, and 4-bar count-in playback with tempo- and meter-aware click scheduling, pre-roll transport alignment, recording-time clamping, editor persistence, UI wiring, and automated planner and playback tests.

Changes

Count-in playback

Layer / File(s) Summary
Count-in planning and controls
src/transport.js, src/audio.js, screen.html, src/main.js
The editor stores the selected count-in length, exposes it through the toolbar, and _countInPlanPure calculates click timing, accents, duration, and fallback values.
Pre-roll playback and clock alignment
src/audio.js, src/midi-record.js
Playback anchors transport and buffered audio at the pre-roll offset, schedules count-in voices after the reset, and clamps chart time during pre-roll.
Count-in validation and documentation
tests/count_in.test.mjs, tests/count_in_wiring.test.mjs, CHANGELOG.md
Tests cover planner calculations, invalid inputs, click scheduling, cancellation behavior, and recording clamping; the changelog documents the feature.

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

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant startPlayback
  participant _countInPlanPure
  participant Transport
  participant Audio
  participant Metronome
  Editor->>startPlayback: start playback or recording
  startPlayback->>_countInPlanPure: calculate count-in plan
  _countInPlanPure-->>startPlayback: pre-roll duration and clicks
  startPlayback->>Transport: anchor cursor with preRoll
  startPlayback->>Audio: start source with preRoll
  startPlayback->>Metronome: schedule count-in click voices
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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 clearly summarizes the main change: an editor count-in feature with N bars of clicks before playback starts.
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/editor-count-in

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 (2)
tests/count_in_wiring.test.mjs (1)

119-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test doesn't exercise the real chartTimeNow.

This test reimplements the clamp formula inline rather than calling midi-record.js's chartTimeNow(), which isn't exported. It validates that _transportChartTimePure + Math.max behave correctly in isolation, but a regression introduced directly inside chartTimeNow (e.g., dropped clamp, swapped args) would pass this test undetected — despite the docstring's claim that it "pins the recorder clamp (chartTimeNow) rule."

Consider exporting chartTimeNow for direct testing, or driving the test through the actual recording start/tick path.

🤖 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/count_in_wiring.test.mjs` around lines 119 - 128, The recorder clamp
test currently validates an inline formula instead of the production
chartTimeNow implementation. Export chartTimeNow from midi-record.js and update
the “recorder clamp” test to call that exported function directly, preserving
the existing pre-roll clamping and post-start passthrough assertions.
src/audio.js (1)

260-273: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Fragile: two independent currentTime reads for one scheduling decision.

_startAudioSourceAtCursor reads S.audioCtx.currentTime for .start(), then _anchorTransportAtCursor re-reads it independently for S.playStartWall. Today these happen back-to-back synchronously so they resolve to the same value, but nothing enforces that — a future refactor adding any async step between the two calls would silently desync the audio start time from the transport anchor, breaking count-in alignment without an obvious symptom.

Consider computing the wall time once and threading it through both calls.

💡 Illustrative fix
 export function _startAudioSourceAtCursor(preRoll = 0) {
     S.audioSource = S.audioCtx.createBufferSource();
     S.audioSource.buffer = S.audioBuffer;
     ...
-    S.audioSource.start(preRoll > 0 ? S.audioCtx.currentTime + preRoll : 0, S.cursorTime);
-    _anchorTransportAtCursor(preRoll);
+    const startWall = preRoll > 0 ? S.audioCtx.currentTime + preRoll : 0;
+    S.audioSource.start(startWall, S.cursorTime);
+    _anchorTransportAtCursor(preRoll, startWall > 0 ? startWall : S.audioCtx.currentTime);
 }

Also applies to: 283-291

🤖 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/audio.js` around lines 260 - 273, Update _startAudioSourceAtCursor to
compute the scheduling wall time from S.audioCtx.currentTime once, use that
value for audioSource.start, and pass the same value into
_anchorTransportAtCursor. Adjust _anchorTransportAtCursor to accept and use the
supplied wall time for S.playStartWall, preserving the existing preRoll and
cursor behavior.
🤖 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/audio.js`:
- Around line 260-273: Update _startAudioSourceAtCursor to compute the
scheduling wall time from S.audioCtx.currentTime once, use that value for
audioSource.start, and pass the same value into _anchorTransportAtCursor. Adjust
_anchorTransportAtCursor to accept and use the supplied wall time for
S.playStartWall, preserving the existing preRoll and cursor behavior.

In `@tests/count_in_wiring.test.mjs`:
- Around line 119-128: The recorder clamp test currently validates an inline
formula instead of the production chartTimeNow implementation. Export
chartTimeNow from midi-record.js and update the “recorder clamp” test to call
that exported function directly, preserving the existing pre-roll clamping and
post-start passthrough assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8179c745-0bca-4eff-8055-9162bf1354c3

📥 Commits

Reviewing files that changed from the base of the PR and between 2ec753b and 0e5ac8a.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • screen.html
  • src/audio.js
  • src/main.js
  • src/midi-record.js
  • src/transport.js
  • tests/count_in.test.mjs
  • tests/count_in_wiring.test.mjs

@byrongamatos
byrongamatos merged commit 0731dab into main Jul 11, 2026
byrongamatos added a commit that referenced this pull request Jul 11, 2026
… paths

The Count toggle restores the "last non-zero count" from
localStorage['editorCountInLast'], but that memory was only written by
the toggle and the LCD-cell handler — not by editorSetCountIn itself.
Setting the count via #203's toolbar Count: select and then clicking the
toggle restored a stale value (or fell back to 1 bar).

Record editorCountInLast inside editorSetCountIn whenever it's set to a
non-zero value, so the toolbar select, the LCD cell, and the toggle all
share one memory. Whitelist ({0,1,2,4}) unchanged; pure editor pref, no
chart/history involvement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byrongamatos added a commit that referenced this pull request Jul 11, 2026
… paths

The Count toggle restores the "last non-zero count" from
localStorage['editorCountInLast'], but that memory was only written by
the toggle and the LCD-cell handler — not by editorSetCountIn itself.
Setting the count via #203's toolbar Count: select and then clicking the
toggle restored a stale value (or fell back to 1 bar).

Record editorCountInLast inside editorSetCountIn whenever it's set to a
non-zero value, so the toolbar select, the LCD cell, and the toggle all
share one memory. Whitelist ({0,1,2,4}) unchanged; pure editor pref, no
chart/history involvement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byrongamatos added a commit that referenced this pull request Jul 11, 2026
* feat(editor): count-in LCD cell + Count transport toggle

The pair the B2 transport slice deferred until count-in existed: a
Count LCD cell (Off/1/2/4 bars, between Key and Sel) writing through
to editorSetCountIn and mirrored on the tick, and a Count mode toggle
that arms/disarms the pre-roll, remembering the bar count across the
off state (editor pref). Old Customize pref blobs default the new
cell to visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

* fix(editor): remember LCD count-in selection

* fix(editor): record count-in memory in editorSetCountIn for all write paths

The Count toggle restores the "last non-zero count" from
localStorage['editorCountInLast'], but that memory was only written by
the toggle and the LCD-cell handler — not by editorSetCountIn itself.
Setting the count via #203's toolbar Count: select and then clicking the
toggle restored a stale value (or fell back to 1 bar).

Record editorCountInLast inside editorSetCountIn whenever it's set to a
non-zero value, so the toolbar select, the LCD cell, and the toggle all
share one memory. Whitelist ({0,1,2,4}) unchanged; pure editor pref, no
chart/history involvement.

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