Skip to content

feat(editor): multi-track MIDI playback — the chart plays as a band, mixed by the Tracks strips - #280

Merged
byrongamatos merged 4 commits into
mainfrom
feat/editor-midi-playback
Jul 15, 2026
Merged

feat(editor): multi-track MIDI playback — the chart plays as a band, mixed by the Tracks strips#280
byrongamatos merged 4 commits into
mainfrom
feat/editor-midi-playback

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What

Multi-track MIDI playback — the chart plays as a band, and the Mixer mixes it. A new All tracks toggle (Mixer panel header, Transport menu, command palette; persisted editor pref) makes every track voice its GM instrument simultaneously — lead and rhythm as guitars, bass as bass, the drum grid clapping along — instead of today's single current-track guide voice. The Tracks strips stop being clap-gates and become a real mixer over the band: per-track volume / mute / solo ramp live per-part gain nodes (~20 ms, never a pop, never a restart), with the standard DAW rule (mute wins; any solo isolates).

OFF = today's behavior, bit-identical — the single-guide path is the untouched else-branch.

Design

  • The strips and the engine can never disagree: the band roster (_bandPartsPure) uses exactly the mixer panel's strip keys (arr:<idx> / drums), and per-part audibility comes from the panel's own whole-map solo pure through a host hook.
  • Per-part gain nodes into the guide bus — the same architecture as feat(editor): stem mixer — isolate any instrument while editing #275's per-stem gains, on the parts axis. Voices schedule at unit scale; the gain node owns the strip level, so a mixer gesture is a ramp, not a reschedule.
  • The one shared pitch converter: _rollPitchCtxFor(arr) is the per-arrangement form of _rollPitchCtx, so the band's pitches can never disagree with the roll/strip (capo-aware fretted, keys packing).
  • Never silent while enabled: each part's GM preset loads lazily (ensureGmPreset per program) and falls back to one clap per bucket until ready — the existing guide contract, per part.
  • Part-scoped dedupe: two parts hitting the same millisecond both sound (the old scalar cross-tick key would swallow the second); seek/wrap clears the set (typeof-guarded for the sliced compose_transport suite).
  • The transport reaches everyone's last note: in band mode _guideSourceTimes unions all parts, so a bass outro past the lead's final chord no longer cuts the song short.
  • The recording is untouched — its fader is a separate bus, as always; guide voices still ride the guide bus and its limiter.

Relationship to #275 (stem mixer)

Complementary, not conflicting: #275 mixes the recording (audio stems), this mixes the chart (MIDI voices). Both hang per-source gain nodes off the mixer panel with live ramps — same idiom, different axis. Expect an adjacent keep-both in host.js/mixer-panel.js/audio.js whichever merges second.

Testing

tests/midi_playback.test.mjs (5 cases, fails on main): roster/strip-key mirroring (empty drum tab is not a part), part-scoped dedupe, the cross-part DAW rule (someone else's solo silences you; mute beats own solo), pref round-trip + panel notification, and the all-parts duration bound vs active-only. Full suites green: 159 files / 0 fail, lint at the 3-warning baseline. Live-verified on the testbed (AC/DC, Lead/Rhythm/Bass): toggle + honest status, voices schedule during playback (instrumented node counts), soloing Rhythm silences Lead through the real strips, zero page errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Summary by CodeRabbit

  • New Features

    • Added an All tracks (band mode) playback option to play every track simultaneously with DAW-style mute/solo/volume behavior.
    • Added an All tracks toggle in the Mixer header, Transport menu, and command palette; playback now reaches each track’s last note (including band-mode drum claps).
    • The setting persists and can be toggled on/off during playback.
  • Tests

    • Added Node-based test coverage for band-mode scheduling semantics, mixer strip interactions, preference persistence, and live toggle behavior (including band review fixes).

…mixed by the Tracks strips

DAW move: 'All tracks' (Mixer header toggle + registry command +
Transport menu row, persisted pref) voices EVERY part's charted notes
through its own GM instrument simultaneously; drum-grid hits clap along.
The Mixer's Tracks strips become a real mixer over the band.

- audio.js: @pure:midi-playback block (_bandPartsPure mirrors the mixer
  strip keys exactly — 'arr:<idx>'/'drums' — so strips and engine can
  never disagree; part-scoped dedupe keys so two parts on the same
  millisecond both sound); per-part GainNodes into the guide bus, ramped
  ~20 ms via host.partMixChanged (the stem-mixer architecture); the band
  scheduler branch (per-part GM program by kind, ensure/ready per
  preset, clap fallback per bucket, unit-scale voices — the gain node
  owns the strip level); _guideSourceTimes unions all parts in band
  mode (a bass outro past the lead's last note bounds the song again);
  seek/wrap clears the band dedupe (typeof-guarded for the sliced
  compose_transport suite); teardown drops the gains.
- keys.js: _rollPitchCtxFor(arr) — the per-arrangement form of the one
  shared pitch converter, so the band can never disagree with the roll.
- mixer-panel.js: _mixerPartStripState(key) (whole-map solo rule per
  key), every strip write pokes host.partMixChanged, the header toggle
  renders its pressed state from the pref.
- host.js: partStripState / partMixChanged / stripUiChanged /
  playAllTracksEnabled hooks, inert defaults.

OFF = today's behavior, bit-identical (the single-guide path is the
untouched else-branch).

tests/midi_playback.test.mjs (5 cases, fails on main): roster/strip-key
mirror, part-scoped dedupe, cross-part DAW rule, pref round-trip +
panel notify, all-parts duration bound. Live-verified on :8001 (AC/DC,
3 tracks): toggle + status, playback schedules voices (instrumented
node counts), soloing Rhythm silences Lead across parts, zero errors.

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

coderabbitai Bot commented Jul 15, 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: 049aa8c3-474a-4a96-9084-a929d1aea887

📥 Commits

Reviewing files that changed from the base of the PR and between 6c9b851 and e055298.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • screen.html
  • src/audio.js
  • src/input.js
  • src/main.js
  • src/menu-bar.js
  • src/shortcuts.js

📝 Walkthrough

Walkthrough

Adds a persisted “Play all tracks” mode with mixer-aware per-track MIDI playback, controls in the Mixer and Transport menu, command wiring, expanded scheduling bounds, and tests for playback routing, persistence, live toggling, and timing.

Changes

Play all tracks band mode

Layer / File(s) Summary
Band-mode scheduling and routing
src/audio.js, src/keys.js
Playback schedules arrangement tracks and drums through per-part gains, applies mixer state, deduplicates events, extends source bounds, and resets band playback state.
Mixer state and host integration
src/host.js, src/mixer-panel.js, src/main.js
Mixer strip audibility and volume are exposed to audio scheduling, while preference and mix changes refresh UI and audio state.
Mixer, transport, and command access
screen.html, src/input.js, src/menu-bar.js, src/shortcuts.js, src/main.js
The All tracks button, Transport command, command dispatch, shortcut registry entry, and window handler expose the toggle.
Playback validation and release metadata
tests/midi_playback.test.mjs, tests/band_review_fixes.test.mjs, CHANGELOG.md
Tests cover roster, deduplication, mute/solo precedence, persistence, scheduling bounds, live toggles, gain routing, and duration; the changelog documents the mode.

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

Sequence Diagram(s)

sequenceDiagram
  participant MixerPanel
  participant editorTogglePlayAllTracks
  participant GuideScheduler
  participant MixerStripState
  MixerPanel->>editorTogglePlayAllTracks: Toggle band mode
  editorTogglePlayAllTracks->>GuideScheduler: Refresh playback preference
  GuideScheduler->>MixerStripState: Read per-part mute, solo, and volume
  GuideScheduler->>GuideScheduler: Schedule pitched and drum events
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.15% 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 accurately summarizes the main change: editor multi-track MIDI playback controlled by mixer strips.
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-midi-playback

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

🧹 Nitpick comments (1)
tests/midi_playback.test.mjs (1)

44-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated, indentation-fragile source-slicing logic.

The indexOf('function ' + name)indexOf('\n}', at) + 2 extraction algorithm in slice() (lines 46-50) is re-implemented verbatim inline at lines 116-117 to build the injectable _guideSourceTimes function. Both rely on the target function's closing brace sitting at column 0 while every nested block's closing brace is indented — true today, but a reformat of audio.js (e.g. a differently-indented block, or a trailing comment after }) breaks this silently for whichever copy wasn't checked, and since both call sites run outside t()'s try/catch, a break surfaces as a raw crash rather than a clean "FAIL" line.

Consider extracting a single sliceBody(name) that returns the raw string, then have both the plain slice() helper and the parameterized Function construction build on top of it.

♻️ Suggested consolidation
 function slice(name) {
+    return sliceBody(name);
+}
+function sliceBody(name) {
     const at = src.indexOf(`function ${name}`);
-    const body = src.slice(at, src.indexOf('\n}', at) + 2);
+    return src.slice(at, src.indexOf('\n}', at) + 2);
+}
+function slice(name) {
+    const body = sliceBody(name);
     return new Function(`return (${body.replace(`function ${name}`, 'function')})`)();
 }
     const fn = new Function('S', 'notes', '_guideSanitizeTimesPure', 'editorPlayAllTracksEnabled',
-        `return (${src.slice(src.indexOf('function _guideSourceTimes'), src.indexOf('\n}', src.indexOf('function _guideSourceTimes')) + 2).replace('function _guideSourceTimes', 'function')})()`);
+        `return (${sliceBody('_guideSourceTimes').replace('function _guideSourceTimes', 'function')})()`);

Also applies to: 116-117

🤖 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/midi_playback.test.mjs` around lines 44 - 52, Consolidate the
duplicated source-extraction logic by adding a shared sliceBody(name) helper
that returns the raw function body from src. Update slice(name) and the
parameterized _guideSourceTimes Function construction to use this helper,
preserving their existing behavior while ensuring both call sites use the same
extraction implementation.
🤖 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 `@src/audio.js`:
- Around line 1294-1304: Update the band-mode timing and eligibility logic
around the arrangement-length checks and the all-track time collection to
include drum data, allowing drum-only charts and drum hits beyond arrangement
notes to determine the transport duration. Preserve existing arrangement
behavior while removing the S.arrangements.length-only gates in the affected
paths.
- Around line 1386-1389: Update the missing-node creation path in the part-gain
initialization logic around _partGains and the later mixer-state application so
each newly created GainNode receives the current mixer gain/mute/solo state
before any notes are scheduled. Ensure the initial value is applied immediately
after creating the node, while preserving the existing bus.guideGain connection
and subsequent scheduling behavior.
- Around line 1368-1375: Update editorTogglePlayAllTracks to reset queued guide
scheduling when the playback mode changes: clear or invalidate pending scheduled
guide voices and reset _guideScheduledUntil before applying the new mode.
Preserve the existing preference persistence, status update, UI notification,
and return behavior.

---

Nitpick comments:
In `@tests/midi_playback.test.mjs`:
- Around line 44-52: Consolidate the duplicated source-extraction logic by
adding a shared sliceBody(name) helper that returns the raw function body from
src. Update slice(name) and the parameterized _guideSourceTimes Function
construction to use this helper, preserving their existing behavior while
ensuring both call sites use the same extraction implementation.
🪄 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: eb191917-e18e-483f-abcd-4e27b6d448a9

📥 Commits

Reviewing files that changed from the base of the PR and between d7c5e3c and 30b65cd.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • screen.html
  • src/audio.js
  • src/host.js
  • src/input.js
  • src/keys.js
  • src/main.js
  • src/menu-bar.js
  • src/mixer-panel.js
  • src/shortcuts.js
  • tests/midi_playback.test.mjs

Comment thread src/audio.js Outdated
Comment thread src/audio.js
Comment thread src/audio.js
…nit (review #280 items 8-10)

Item 8: _guideSourceTimes unions drum-grid hits in band mode (before the
no-arrangements early-out) and _guideTick gates on the real band roster —
a drum-only chart plays and a late drum hit bounds the song.
Item 9: a live Play All Tracks toggle cancels the old mode's queued
voices and rewinds the schedule watermark to the current transport time
so the next tick refills in the new mode, both directions.
Item 10: _ensurePartGain seats a fresh gain node at the strip's current
mute/solo/volume before the first voice connects — no unity leak.

Regression suite drives the real _guideTimerSync/_guideTick over a
recording fake AudioContext; all six cases fail on the pre-fix code.

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

Band mode (multi-track MIDI playback) clapped only the 'drums' sidecar key.
A drum-ENCODED arrangement — the created/imported/legacy "Drums" part that
lives in S.arrangements (key 'arr:i') — has no pitch, so
_bandPartPitchedEvents returns [] by design. With no clap fallback on the
arrangement path, that whole part voiced neither GM nor clap and went
SILENT in band mode, even though single-part mode sounds it. This
contradicts the feature's promise that EVERY track plays.

Root cause: the clap path was gated on part.key === 'drums' only. Unify it
so percussion — the drum grid OR any /^drums/ arrangement — claps its
rhythm through its own part gain (mute/solo/volume still apply).

Regression test in tests/band_review_fixes.test.mjs (fails pre-fix:
drum-arrangement claps absent; also asserts the mute path seats the clap's
gain at 0).

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

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

301-306: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the clap fallback, not only event timing.

This assertion passes for any voice scheduled at 0.01 and 0.05; it would not catch routing the drum arrangement through a pitched GM voice instead of the required clap fallback. Capture and assert the voice/instrument type as well.

🤖 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/band_review_fixes.test.mjs` around lines 301 - 306, Update the
assertion around tickOnce and the collected ctx.oscs entries to verify both
charted start times and the expected clap fallback voice/instrument type. Match
each relevant event by its startAt value and assert its voice/instrument
identity, so pitched GM routing at those times cannot satisfy the test.
🤖 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 `@tests/band_review_fixes.test.mjs`:
- Around line 301-306: Update the assertion around tickOnce and the collected
ctx.oscs entries to verify both charted start times and the expected clap
fallback voice/instrument type. Match each relevant event by its startAt value
and assert its voice/instrument identity, so pitched GM routing at those times
cannot satisfy the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d42ebdb9-51af-4f4c-9797-8b003be47f69

📥 Commits

Reviewing files that changed from the base of the PR and between 1f74537 and 6c9b851.

📒 Files selected for processing (2)
  • src/audio.js
  • tests/band_review_fixes.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/audio.js

# Conflicts:
#	CHANGELOG.md
@byrongamatos
byrongamatos merged commit d684849 into main Jul 15, 2026
3 of 4 checks passed
@byrongamatos
byrongamatos deleted the feat/editor-midi-playback branch July 15, 2026 20:35
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