Skip to content

feat(editor): buffer-less compose-mode transport (Phase A3) - #136

Merged
byrongamatos merged 3 commits into
mainfrom
feat/editor-compose-transport
Jul 9, 2026
Merged

feat(editor): buffer-less compose-mode transport (Phase A3)#136
byrongamatos merged 3 commits into
mainfrom
feat/editor-compose-transport

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Phase A3 — buffer-less compose-mode transport

Third step of the workspace-shell time-model foundation (§1.7). Independent of A1/A2 — builds directly on main.

The problem

startPlayback() early-returned without S.audioBuffer:

if (!S.audioBuffer || !S.audioCtx) return;

So a from-scratch song (compose mode — no recording) had no clock, no metronome, no guide claps. You could place notes but not hear time move.

What A3 adds

A buffer-less transport. When there's no recording:

  • The clock runs off the AudioContext, not a BufferSource. _anchorTransportAtCursor() pins playStartWall/playStartTime; playbackTick() advances cursorTime from the ctx clock exactly as before — just with no audio node.
  • The grid bounds the song. S.duration = the time of the last grid beat via the A1 converter (timeOf(lastBeat)), extended if authored content runs past the last bar, or an explicit S.composeLength. No audio buffer needed to know how long the song is.
  • The metronome/guide scheduler is the only sound. _guideTick was already ctx-time-based and gates on S.audioCtx (never S.audioBuffer), so it fires off the beat map with no changes.
  • _ensureAudioCtx() creates the shared context on the play gesture, since compose mode never hits the decode path that used to be the only creation site.

_restartPlaybackAt() (loop wrap / seek) takes the same buffer-less branch. Audio-present playback is unchanged — buffered and buffer-less share one clock; a recording rides the clock, it isn't the source of time.

Incidental cleanup

The clock-advance formula playStartTime + (ctxNow − playStartWall) was copy-pasted at four sites (playbackTick, guide tick, guide-timer sync, chartTimeNow). A3 extracts it to one pure _transportChartTimePure and rewires all four — no behaviour change, less drift for the Logic-style ruler/LCD (B2/B3) to trip over later.

Tests

tests/compose_transport.test.js15 cases, all would-fail-on-main (every A3 symbol is absent on main):

  • _transportChartTimePure — advance / stationary-at-anchor / monotonic
  • _composeSongDurationPure — user length wins · max(grid, content) · junk → 0
  • _composeSongDuration (live, real timeOf, no buffer) — grid-derived length · extends for late content · honours composeLength · degenerate grid → 0
  • _anchorTransportAtCursor pins the anchor and creates no BufferSource
  • playback advances cursorTime with audioBuffer = null (the core A3 claim)
  • guide scheduler is buffer-independent — reset seeds the watermark from cursorTime; _guideTick gates on audioCtx, never audioBuffer

Verification

  • node --check screen.js clean
  • Full JS suite: 72 pass / 0 fail
  • No routes.py change (no pytest); no wire/spec change

Ordering

Independent of A2 (#135) — both branch off main and can merge in any order. A4 (loop-as-beats) and A5 (beat-lock) require A2 merged; Phase B waits on the feature-PR backlog draining.

Design source: SONG-EDITOR-WORKSPACE-SHELL-CHARRETTE.md §1.7.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added buffer-less compose-mode transport playback (no recording loaded), including AudioContext-clock-based playhead movement.
    • Compose-mode playback now bounds song length to the grid while still including authored content/tail, with consistent guide/metronome output.
  • Bug Fixes

    • Fixed compose-mode end-of-guide clap being cut short.
    • A/B compare now remains inactive when no reference audio is available.
    • Transport and guide timing are more resilient when seeking/restarting.
  • Tests

    • Expanded automated coverage for compose-mode transport timing, duration selection, scheduling independence, and safe initialization when Web Audio is unavailable.
    • Updated A/B loop test setup to default to buffered behavior.

@coderabbitai

coderabbitai Bot commented Jul 8, 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: 93de2747-ac34-49b3-872a-3b7d717361f1

📥 Commits

Reviewing files that changed from the base of the PR and between 0951584 and ee29476.

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

📝 Walkthrough

Walkthrough

This PR adds buffer-less compose-mode playback in screen.js, shared transport-time calculation, updated A/B gating for no-buffer playback, and matching tests plus changelog notes.

Changes

Compose-mode transport clock

Layer / File(s) Summary
Playback start and anchoring
screen.js
Adds lazy _ensureAudioCtx() for loadAudio, and implements compose-mode playback start/restart anchoring with duration derived from grid/content or explicit compose length.
Shared transport clock call sites
screen.js, CHANGELOG.md, tests/loop_ab.test.js
Updates playbackTick, guide scheduling, the scheduled-until horizon, and chartTimeNow to use _transportChartTimePure, tightens _abActive for compose mode, and updates release notes plus loop A/B test setup.
Compose transport tests
tests/compose_transport.test.js
Adds tests for transport-time math, compose duration rules, compose anchoring, scheduler independence from audioBuffer, A/B compose-mode behavior, and AudioContext creation fallback.

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

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 matches the main change: buffer-less compose-mode transport for Phase A3.
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-compose-transport

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

🤖 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 6864-6867: _ensureAudioCtx() currently instantiates the audio
context unconditionally, so it throws a TypeError when neither AudioContext nor
webkitAudioContext exists and breaks the no-op fallback in startPlayback. Update
_ensureAudioCtx() to detect unsupported Web Audio before constructing the
context, leave S.audioCtx unset when unavailable, and return null so callers
like startPlayback and loadAudio can safely bail out via their existing guard.
🪄 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: d8a45bdc-0873-4670-8fae-53a690057259

📥 Commits

Reviewing files that changed from the base of the PR and between b367c23 and 96a0b84.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • screen.js
  • tests/compose_transport.test.js

Comment thread screen.js
@byrongamatos

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

ChrisBeWithYou and others added 3 commits July 9, 2026 09:49
…ith no recording (Phase A3)

startPlayback() used to early-return without S.audioBuffer, so a from-scratch
song had no clock, no metronome, no guide. Add a buffer-less transport: the
playhead advances off the AudioContext clock with no BufferSource, the grid
(not a buffer) bounds the song via the A1 converter (S.duration =
timeOf(lastBeat), extended for content past the last bar, or S.composeLength),
and the already-ctx-time-based metronome/guide scheduler is the only sound.

- _ensureAudioCtx(): compose mode never decodes audio, so create the shared
  context on the play gesture (the decode path is the only other creation site).
- _anchorTransportAtCursor(): the buffer-less clock anchor, extracted from
  _startAudioSourceAtCursor; startPlayback / _restartPlaybackAt take the
  buffer-less branch when there is no S.audioBuffer.
- _composeSongDuration() + pure _composeSongDurationPure(): grid-defined length.
- _transportChartTimePure(): the one clock-advance formula, previously
  copy-pasted at four sites (playbackTick, guide tick, guide sync, chartTimeNow)
  — now shared. Audio-present playback is unchanged.

Tests: tests/compose_transport.test.js (15, all would-fail-on-main). Full JS
suite green (72/0). No routes.py / no wire change. Independent of A2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013JgxKh99UAeQqmhzSc73tv
…view)

FIX 1: _composeSongDurationPure pads a 0.25s tail past the last authored
onset when the duration is content-bound, so the final guide clap rings out
instead of being cut by stopPlayback(). Explicit composeLength and grid-bound
lengths stay exact.

FIX 2: _abActive() now requires S.audioBuffer, so A/B compare is inert in
compose mode (no reference recording) and every loop clap fires instead of
half each pass being gated to silence.

Regression tests extended (compose_transport, loop_ab) — both new paths fail
on pre-fix code.

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

Detect a missing AudioContext/webkitAudioContext before constructing, so
_ensureAudioCtx returns null and leaves S.audioCtx unset instead of throwing
a `new undefined()` TypeError. startPlayback/loadAudio already guard on
!S.audioCtx, which was dead code while the constructor threw first.

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