Skip to content

feat(editor): swing quantization on the snap grid (workspace-shell D2) - #197

Merged
byrongamatos merged 2 commits into
mainfrom
feat/editor-swing-snap
Jul 11, 2026
Merged

feat(editor): swing quantization on the snap grid (workspace-shell D2)#197
byrongamatos merged 2 commits into
mainfrom
feat/editor-swing-snap

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

Swing on the snap grid (charrette §1.6): a Swing select next to Snap — Straight · 54% · 58% · 62% — that displaces the off subdivision of each pair toward the next beat.

The design rule

Swing is a beat-domain phase offset fed through the tempo-map converter:

snap(t) = timeOf(_swingQuantizeBeatPure(beatOf(t), subs, pct))

— never a seconds nudge. Expressed as a beat coordinate, a swung note keeps its groove ratio through a tempo flex exactly like a straight one (pinned by test: 62% stays 62% through the pair after the grid times change under it).

Guard rails

  • 50% is bit-identical to the previous rounding path (pinned across grids).
  • Triplet grids are a no-op — triplet-ness is divisibility by 3, not parity (1/6T and 1/12T are even; the first cut got this wrong and the suite caught it). Odd grids can't pair; also a no-op.
  • Out-of-band values fall back to straight (≤50, >75, NaN, a corrupt pref) rather than flinging notes.
  • Snap placement only: playback, existing notes, and the drawn grid are unchanged. S.swingPct is an editor pref (localStorage), never written to the pack.

The legacy sliced-source onset suite injects a straight-path stand-in for the new symbol (its fixture carries no swingPct, so the real quantizer reduces to plain rounding there); swing behavior has its own real-import suite.

Tests

tests/swing_snap.test.mjs (8): pct%-through-the-pair placement (would-fail-on-main), pair starts/downbeats fixed, nearest-candidate rounding, triplet/odd no-ops, corruption fallback, the flex-survival property, and a no-drift check that the toolbar options equal SWING_PRESETS. 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 Swing dropdown to the snap toolbar (Straight, 54%, 58%, 62%) to control swing-only snap placement.
    • Added swing snapping for applicable even-numbered grids and restored the last used swing setting between sessions.
  • Behavior
    • Playback, existing notes, and the displayed/drawn grid remain unchanged; only snap placement is affected.
    • Triplet/odd grids ignore swing automatically.
    • Invalid/unsupported swing values safely fall back to Straight.

Add swing to grid snap as a BEAT-DOMAIN phase offset fed through the
tempo-map converter — snap(t) = timeOf(_swingQuantizeBeatPure(beatOf(t),
subs, pct)) — never a seconds nudge (charrette $1.6). Expressed as a beat
coordinate, a swung note keeps its groove ratio through a tempo flex
exactly like a straight one.

Model: swing displaces the OFF subdivision of each consecutive pair; a pair
spans 2/subs beats, so pairs align to the beat for every straight even grid
(1/8 swing at subs=2, 1/16 swing at subs=4, ...). Presets: Straight / 54% /
58% / 62% (a select next to Snap). Guard rails:

- 50% is BIT-IDENTICAL to the old rounding path (pinned across grids).
- Triplet grids are a no-op — and triplet-ness is divisibility by 3, not
  parity (1/6T and 1/12T are even; the first cut got this wrong and the
  suite caught it). Odd grids can't pair; also a no-op.
- Out-of-band values (<=50, >75, NaN, corrupt pref) fall back to straight
  rather than flinging notes.
- Snap placement only: playback, existing notes and the drawn grid are
  unchanged. S.swingPct is an editor pref (localStorage), never the pack.

The legacy sliced-source onset suite injects a straight-path stand-in for
the new symbol (its fixture carries no swingPct, so the real quantizer
reduces to plain rounding there); swing behavior has its own real-import
suite: tests/swing_snap.test.mjs (8) — the pct%-through-the-pair placement
(would-fail-on-main), pair starts and downbeats fixed, nearest-candidate
rounding, triplet/odd no-ops, corruption fallback, and the groove-ratio-
survives-a-flex property. 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>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8233db0f-2569-402e-910e-ffc2e218054a

📥 Commits

Reviewing files that changed from the base of the PR and between fae9e5f and eacfbec.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • screen.html
  • src/main.js

📝 Walkthrough

Walkthrough

Adds swing presets to snap-grid placement, with beat-domain quantization, editor state persistence in localStorage, a toolbar selector, initialization restoration, and tests covering grid behavior, validation, tempo mapping, and UI preset consistency.

Changes

Swing quantization

Layer / File(s) Summary
Swing quantizer and validation
src/snap.js, tests/swing_snap.test.mjs
Adds swing presets and pair-based beat quantization, including straight fallback, odd/triplet handling, nearest-candidate behavior, tempo consistency, and UI preset checks.
Swing-aware snap placement
src/loop.js, tests/onset_snap.test.js
Routes snapTime() through swing quantization while preserving onset-snap behavior and updates its injected test dependency.
Editor preference and toolbar control
src/state.js, screen.html, src/main.js, CHANGELOG.md
Adds the swing state field, toolbar selector, bounded localStorage-backed preference handler, initialization restoration, status text, and changelog entry.

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

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant snapTime
  participant SwingQuantizer
  participant TempoMapping
  Editor->>snapTime: Request snap placement
  snapTime->>SwingQuantizer: Quantize beat with swing percentage
  SwingQuantizer-->>snapTime: Return quantized beat
  snapTime->>TempoMapping: Convert beat to time
  TempoMapping-->>Editor: Return snapped time
Loading

Possibly related PRs

🚥 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 summarizes the main change: adding swing quantization to the editor snap grid.
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-swing-snap
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/editor-swing-snap

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 (1)
src/main.js (1)

1242-1252: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard-band duplicated across files.

The (50,75] swing bound is hand-mirrored here and in _swingQuantizeBeatPure (src/snap.js). Exporting it as a constant (e.g. SWING_MIN/SWING_MAX) from snap.js and importing here would remove the drift risk the current comment is compensating for.

♻️ Proposed refactor
+// src/snap.js
+export const SWING_MIN = 50;
+export const SWING_MAX = 75;
-window.editorSetSwing = (pct) => {
-    const n = Number(pct);
-    // Same guard band as the quantizer: outside (50,75] means straight.
-    S.swingPct = Number.isFinite(n) && n > 50 && n <= 75 ? n : 50;
+window.editorSetSwing = (pct) => {
+    const n = Number(pct);
+    S.swingPct = Number.isFinite(n) && n > SWING_MIN && n <= SWING_MAX ? n : SWING_MIN;
🤖 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/main.js` around lines 1242 - 1252, Centralize the swing bounds instead of
duplicating the (50,75] guard in window.editorSetSwing. Export shared constants
such as SWING_MIN and SWING_MAX from _swingQuantizeBeatPure’s module in
src/snap.js, import them in src/main.js, and use them in the Number.isFinite
range check while preserving the existing fallback to 50.
🤖 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/main.js`:
- Around line 1242-1252: Centralize the swing bounds instead of duplicating the
(50,75] guard in window.editorSetSwing. Export shared constants such as
SWING_MIN and SWING_MAX from _swingQuantizeBeatPure’s module in src/snap.js,
import them in src/main.js, and use them in the Number.isFinite range check
while preserving the existing fallback to 50.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d3e5c1ea-1cb4-48ea-81bc-90f916295121

📥 Commits

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

📒 Files selected for processing (8)
  • CHANGELOG.md
  • screen.html
  • src/loop.js
  • src/main.js
  • src/snap.js
  • src/state.js
  • tests/onset_snap.test.js
  • tests/swing_snap.test.mjs

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