Skip to content

fix(rtmg/web): reset loop band on swap to a different track#289

Open
seanhanca wants to merge 1 commit into
mainfrom
fix/reset-loop-band-on-track-swap
Open

fix(rtmg/web): reset loop band on swap to a different track#289
seanhanca wants to merge 1 commit into
mainfrom
fix/reset-loop-band-on-track-swap

Conversation

@seanhanca

Copy link
Copy Markdown

Bug

Issue tracker: "Edit loop length on upload" (Value Category = Bug).

A loop region (loopBand) drawn over the current track carries over with stale frame coordinates when the user swaps/uploads a different track. On a shorter new track the worklet indexes the playback buffer out of bounds (pops / NaN); on a longer one the loop span is musically misaligned.

Root cause (verified)

The loop region is never reset/re-clamped on a track swap:

  • usePerformanceStore.setLoopBand is the source of truth for the band, but nothing in the swap path clears it. setFixture only sets the name (store/usePerformanceStore.ts:719).
  • useFixtureSwap.run() swaps the player buffer in place (session.player?.swap(...)) and never clears the band.
  • WaveformScrubBox's effect that mirrors the band to the worklet + server only re-runs on [bandState, bandLoopEnabled, hasPlayer, player] (components/Performance/WaveformScrubBox.tsx:384) — none change on a swap, so it never re-fires.
  • The worklet swap handler updates frameCount but leaves loopBandStart/End at the OLD frame values (public/audio-worklet.js:119-128); process() reads regionEnd from loopBandEnd with no re-clamp (public/audio-worklet.js:272-276) → out-of-bounds indexing on a shorter track.

Fix (minimal)

In useFixtureSwap.run, inside the existing non-forced new-track block (if (!force)), clear the band:

usePerformanceStore.getState().setLoopBand(null);

This reuses existing plumbing: clearing the store band makes WaveformScrubBox's sync effect fire, which sends clearLoopBand to the worklet and sendLoopBand(null, null) to the server. No new subsystem.

Source-mode hotswaps (vocals ↔ instruments) use force = true — the same song — and intentionally keep the loop.

No-regression test

web/tests/unit/fixtureSwapLoopBand.test.ts drives the real hook (stubbing React's useEffect/useRef, exercising the real zustand stores + a fake RemoteBackend that echoes swap_ready) and asserts:

  • loopBand is null after a non-forced swap to a different track.
  • loopBand is unchanged after a forced source-mode (same-song) hotswap.

Verified the test fails on the non-force case when the fix is reverted, and passes with it.

Verification

In demos/realtime_motion_graph_web/web/:

  • npx vitest run tests/unit/fixtureSwapLoopBand.test.ts2 passed
  • npm run typecheck → clean
  • npm run build → compiled successfully

(Unrelated: float16/sliceEpoch suites fail locally with Math.f16round is not a function — a Node-version artifact of the dev box, pre-existing and untouched by this change.)

Made with Cursor

A drawn loop region (loopBand) is stored in coordinates measured
against the current source. useFixtureSwap.run() swapped the player
buffer in place but never cleared the band, so the worklet kept stale
loopBandStart/End: on a shorter new track process() indexed the buffer
out of bounds (pops / NaN); on a longer one the loop was musically
misaligned.

Clear the store band in the non-forced new-track path. That makes
WaveformScrubBox's existing sync effect fire, which pushes clearLoopBand
to the worklet and sendLoopBand(null, null) to the server — no new
plumbing. Source-mode hotswaps (force=true) are the same song and keep
the loop.

Adds a focused unit test asserting loopBand is null after a non-forced
swap and unchanged after a forced source-mode swap.

Co-authored-by: Cursor <cursoragent@cursor.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.

1 participant