Skip to content

feat: improve loop practice with continuous repeats and clearer controls#1016

Open
vo90 wants to merge 3 commits into
got-feedBack:mainfrom
vo90:agent/unified-loop-experience
Open

feat: improve loop practice with continuous repeats and clearer controls#1016
vo90 wants to merge 3 commits into
got-feedBack:mainfrom
vo90:agent/unified-loop-experience

Conversation

@vo90

@vo90 vo90 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

What

This started with one practice improvement: let an A/B loop repeat continuously without forcing another count-in every time.

While testing that flow in feedback-desktop, it became clear that loops could also be easier to find, understand, reuse, and control. This PR expands the original change into a complete loop-practice experience:

  • Continuous loop practice: choose whether the first pass and later repeats use a four-beat count-in or start immediately.
  • Loop controls where players practice: manual A/B loops now live in Practice & Loops alongside section practice.
  • Clear timeline markers: setting A shows its marker immediately; setting B adds the highlighted A–B range and B marker.
  • Saved loops per song: keep and reload useful practice passages for the current song.
  • Visible loop status during normal play: the HUD shows LOOP READY, LOOP STARTING, or LOOP ON, so repeated playback does not look like the game is stuck.
  • Predictable seeking: seek freely within A–B; seeking outside the loop restarts from A when playback begins or continues.
  • Smoother count-ins and restarts: the highway and continuous path stay still until the count-in actually begins.
  • Clearer controls: plain-language option names, aligned controls and markers, accessible labels, and a direct way to open or clear the loop from the HUD.

Playback choices

  • After setting a loop
    • Wait for me to start
    • Start automatically
  • When the loop starts
    • Count in first (4 beats)
    • Start right away
  • When the loop repeats
    • Count in again (4 beats)
    • Repeat right away

These choices are saved and shared by manual A/B loops, saved loops, Practice Sections, and the editor handoff.

Technical implementation

  • centralizes loop activation, restart, seeking, and count-in policy behind one loop-session controller
  • keeps configured, starting, and active loop states distinct
  • routes outside-loop seeks through the same restart-at-A behavior as an initial start
  • coordinates browser-media and desktop/JUCE playback paths
  • freezes highway timing and continuous-path visuals during count-in preparation
  • adds a pointer-transparent full-song timeline overlay for partial and complete bounds
  • adds persistent loop preferences and per-song saved-loop handling
  • expands playback-domain, controller, restart, seek, UI, and accessibility coverage

Companion PR

Review feedback addressed

  • pause-only desktop/JUCE seek batches no longer trigger an outside-loop restart or count-in
  • loop.lastRestartAt and media.loop.lastRestartAt now stay synchronized
  • selected Practice Sections retain a visible pill state without replacing armed/active styling
  • loop-indicator CSS uses the repository's expected keyword casing

feedpak surface

  • This PR does not change how the app reads/writes feedpaks (manifest keys, pack files, folder layout)
  • Not applicable: this PR does not require a feedpak spec change or FEP.

Checklist

  • CHANGELOG.md [Unreleased] updated (user-visible changes)
  • Tests added/updated for new behaviour
  • Commits are DCO signed off (git commit -s)

Developer test checklist

  • In feedback-desktop, create a manual loop with Set A and confirm A appears immediately below the matching timeline line
  • Set B and confirm the highlighted range plus B marker align with their timeline boundaries
  • Confirm Wait for me to start leaves valid bounds ready without moving playback
  • Confirm Start automatically starts according to the first-pass choice
  • Test both first-pass choices: four-beat count-in and immediate start
  • Test both repeat choices: four-beat count-in and immediate repeat
  • While paused, seek inside A–B and confirm Play starts from that position
  • While paused, seek outside A–B and confirm it stays paused; Play then restarts from A
  • While playing, seek inside A–B and confirm playback continues from that position
  • While playing, seek outside A–B and confirm the loop restarts from A with the selected first-pass behavior
  • Restart a loop with count-in and confirm neither the highway nor continuous path moves before the count begins
  • Confirm saved loops, selected Practice Sections, full sections, and manual A/B use the same policy
  • Confirm the HUD state remains visible in normal gameplay, pulses when returning to A, opens Practice & Loops when clicked, and clears via ×
  • Repeat representative checks with both desktop-native/JUCE and browser-media playback routes when available

Validation

  • npm run test:js — 1,168 passed after rebasing onto current main
  • focused loop UI and playback-domain tests — 28 passed
  • ESLint on all changed JavaScript — passed with zero errors
  • git diff --check — clean

Summary by CodeRabbit

  • New Features

    • Added a unified Practice & Loops experience with clearer A/B controls, saved loops, activation preferences, and loop status indicators.
    • Added timeline loop bounds and HUD visibility, including “returning to A” feedback.
    • Improved loop playback, seeking, restarting, count-ins, and pause/resume behavior.
    • Added persistent loop-entry preferences and stronger handling for rapid or canceled loop actions.
  • Bug Fixes

    • Fixed loop state synchronization after playback restarts and arrangement changes.
    • Prevented stale loop operations and unwanted re-arming during section practice.
  • Documentation

    • Updated the unreleased changelog with the new practice-loop behavior.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces direct A/B loop handling with a preference-aware controller, integrates loop restart policy into playback seeking and starts, adds frozen highway timing for count-ins, updates playback contracts, and introduces Practice & Loops UI with v3 timeline and indicator support.

Changes

Unified A/B Loop Controller

Layer / File(s) Summary
Loop preferences and count-in timing
static/js/loop-preferences.js, static/js/count-in.js, static/highway.js, tests/js/*
Loop preferences are normalized and persisted, count-in accepts immutable bounds and pause state, and freezeTime() prevents timing drift during rewinds and count-ins.
Preference-aware loop lifecycle
static/js/loops.js, tests/js/loop_controller.test.js, tests/js/loop_api.test.js
Loop phases, cancellation, activation preferences, first-pass behavior, repeat wrapping, saved-loop loading, and transport event snapshots are centralized in the loop controller.
Loop-aware transport and application wiring
static/js/transport.js, static/app.js, static/capabilities/playback.js, static/js/juce-audio.js, tests/js/*
Play starts and seeks consult loop resolvers and restart handlers; application APIs expose controller state and startLoop, while playback diagnostics promote restarted loops to active state.
Practice and loop interface
static/js/section-practice.js, static/style.css, static/v3/*, tests/js/loop_ui.test.js
Practice controls become a Practice & Loops surface with preference controls, loop indicators, A/B timeline markers, accessibility states, and phase-specific styling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: byrongamatos

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.70% 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
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.
Title check ✅ Passed The title clearly summarizes the main change: improved loop practice with continuous repeats and clearer controls.
Description check ✅ Passed The description follows the required template with What, feedpak surface, and Checklist sections filled in.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vo90 vo90 changed the title feat: unify loop practice experience feat: improve loop practice with continuous repeats and clearer controls Jul 19, 2026
@vo90

vo90 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
static/js/juce-audio.js (1)

900-919: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Only set restartActiveLoopWhilePlaying on the play-following path static/js/juce-audio.js:900-919
In the pause-only branch, a seek while S.isPlaying is still true can be rerouted into _restartLoopFromOutside() before jucePlayer.pause() runs, which briefly restarts playback/count-in and then immediately stops it.

🤖 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 `@static/js/juce-audio.js` around lines 900 - 919, The seek options in the
wantsPause branch currently always enable restartActiveLoopWhilePlaying;
restrict that option to the play-following path by setting it false or omitting
it when forUpcomingPlay is false. Preserve the existing pause flow and enable
loop restarting only when the seek will be followed by playback.
static/capabilities/playback.js (1)

1034-1054: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

media.loop.lastRestartAt goes stale relative to loop.lastRestartAt on every valid loop-restarted event.

_updateLoopFromSnapshot() clones currentSession.loop into currentSession.media.loop internally, but the lastRestartAt timestamp is only applied afterward (line 1051) directly on currentSession.loop. The clone captured in media.loop therefore keeps the previous lastRestartAt (inherited via the ...currentSession.loop spread) instead of the new timestamp, so the two loop snapshots diverge on every restart with valid A/B bounds.

🐛 Proposed fix: apply the timestamp before the clone
         else if (name === 'loop-restarted') {
             const startTime = _number(source.loopA, null);
             const endTime = _number(source.loopB, null);
             if (startTime != null && endTime != null) {
                 // The core loop bridge emits one legacy loop:restart event for
                 // both the initial pass and later wraps. Promote a previously
                 // armed snapshot to active here without requiring a duplicate
                 // loop-set event.
                 _updateLoopFromSnapshot({
                     ...currentSession.loop,
                     startTime,
                     endTime,
                     enabled: true,
                     state: 'active',
                     requesterId: source.requesterId,
+                    lastRestartAt: _now(),
                 });
+            } else if (currentSession.loop) {
+                currentSession.loop.lastRestartAt = _now();
+                currentSession.media.loop = _clone(currentSession.loop);
             }
-            if (currentSession.loop) currentSession.loop.lastRestartAt = _now();
         } else if (name === 'loop-stale') {
🤖 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 `@static/capabilities/playback.js` around lines 1034 - 1054, Update the
loop-restarted handling around _updateLoopFromSnapshot so a valid restart
assigns the new lastRestartAt timestamp to the loop snapshot before cloning it
into currentSession.media.loop. Preserve the existing timestamp update for
currentSession.loop and ensure both loop snapshots receive the same timestamp on
valid events.
🤖 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 `@static/js/section-practice.js`:
- Around line 81-84: Add CSS selectors for
.section-practice-pill--section-selected in the existing section practice pill
styles within static/style.css and static/v3/v3.css, matching the intended
visual treatment of the toggled selected state. Preserve the separate --active
and --armed states managed by the loop controller.

In `@static/v3/v3.css`:
- Around line 823-843: Update the currentColor values in .v3-loop-indicator-icon
and .v3-loop-indicator-label to lowercase currentcolor, preserving the existing
styling and declarations.

---

Outside diff comments:
In `@static/capabilities/playback.js`:
- Around line 1034-1054: Update the loop-restarted handling around
_updateLoopFromSnapshot so a valid restart assigns the new lastRestartAt
timestamp to the loop snapshot before cloning it into currentSession.media.loop.
Preserve the existing timestamp update for currentSession.loop and ensure both
loop snapshots receive the same timestamp on valid events.

In `@static/js/juce-audio.js`:
- Around line 900-919: The seek options in the wantsPause branch currently
always enable restartActiveLoopWhilePlaying; restrict that option to the
play-following path by setting it false or omitting it when forUpcomingPlay is
false. Preserve the existing pause flow and enable loop restarting only when the
seek will be followed by playback.
🪄 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: 734b81ee-c174-417a-a3de-b5f4888d26c2

📥 Commits

Reviewing files that changed from the base of the PR and between 35c0d0e and 01ab216.

⛔ Files ignored due to path filters (1)
  • static/tailwind.min.css is excluded by !**/*.min.css
📒 Files selected for processing (23)
  • static/app.js
  • static/capabilities/playback.js
  • static/highway.js
  • static/js/count-in.js
  • static/js/juce-audio.js
  • static/js/loop-preferences.js
  • static/js/loops.js
  • static/js/section-practice.js
  • static/js/transport.js
  • static/style.css
  • static/v3/index.html
  • static/v3/v3.css
  • tests/js/highway_monotonic_clock.test.js
  • tests/js/loop_api.test.js
  • tests/js/loop_controller.test.js
  • tests/js/loop_preferences.test.js
  • tests/js/loop_restart.test.js
  • tests/js/loop_ui.test.js
  • tests/js/play_button_reroute_guard.test.js
  • tests/js/playback_app_adapter.test.js
  • tests/js/playback_domain.test.js
  • tests/js/song_restart.test.js
  • tests/js/song_seek.test.js

Comment thread static/js/section-practice.js
Comment thread static/v3/v3.css
vo90 added 3 commits July 20, 2026 10:04
Signed-off-by: Viktor Olausson <viktor.olausson@gmail.com>
Signed-off-by: Viktor Olausson <viktor.olausson@gmail.com>
Signed-off-by: Viktor Olausson <viktor.olausson@gmail.com>
@vo90
vo90 force-pushed the agent/unified-loop-experience branch from 01ab216 to a8db82b Compare July 20, 2026 08:05
@vo90
vo90 marked this pull request as ready for review July 20, 2026 08:11
@vo90

vo90 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

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.

1 participant