Skip to content

fix(sound): play concurrently, add back play_in_tab - #874

Open
martin-henz wants to merge 2 commits into
masterfrom
sound-concurrent-play-and-play-in-tab
Open

fix(sound): play concurrently, add back play_in_tab#874
martin-henz wants to merge 2 commits into
masterfrom
sound-concurrent-play-and-play-in-tab

Conversation

@martin-henz

Copy link
Copy Markdown
Member

Summary

Fixes #841. The Conductor migration of the sound module regressed two behaviours from before the migration:

  • play()/play_wave() calls used to overlap (genuine concurrent playback); after the migration they silently serialized instead (each call waited for the previous one to finish before starting).
  • play_in_tab() (and its tab UI, a play bar per call) was dropped entirely.

This PR restores both:

  • Concurrent playback: removed the host-side (SoundTabPlugin) playback queue, so repeated/looped play()/play_wave() calls now start as soon as their own sampling finishes and are mixed together by the shared AudioContext, instead of queueing behind whatever's already playing.
  • play_in_tab(): brought back as a new exported function. It samples the given Sound and encodes it as a data:audio/wav;base64,... URI (pure computation — no AudioContext needed, so this happens module-side rather than via a host round trip), which the sound tab renders as a native <audio controls> play bar. Per the clarification on the issue, each call adds a new bar to the existing sound tab, stacked vertically below any earlier ones (using the browser's own play/pause/scrub controls, rather than a custom widget).

Test plan

  • yarn test passes for both src/bundles/sound (69 tests, including new play_in_tab coverage) and src/tabs/Sound (22 tests, including new concurrency + addPlayerToTab coverage)
  • yarn tsc and yarn lint pass for both packages (no new lint errors; one new eslint-disable-next-line warning matches the existing pattern already present for every other validated function in the file)
  • yarn build succeeds for both packages

🤖 Generated with Claude Code

https://claude.ai/code/session_01B3LRoXRMQ5ADeYMdVTChCZ

The Conductor migration accidentally serialized play()/play_wave()
(each call now waited for the previous one to finish) and dropped
play_in_tab() (and its tab UI) entirely, regressing two behaviours
the sound module used to have: genuinely overlapping playback, and a
tab showing per-sound play bars.

- Removed the host-side playback queue in SoundTabPlugin so repeated/
  looped play()/play_wave() calls start immediately and overlap,
  mixed by the shared AudioContext, instead of playing one after
  another.
- Brought back play_in_tab(): it samples the Sound and encodes it as
  a WAV data URI (pure computation, done module-side rather than via
  a host round trip), which the sound tab renders as a native
  <audio controls> play bar. Each call adds a new bar, stacked
  vertically below any earlier ones, per the clarification on #841.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3LRoXRMQ5ADeYMdVTChCZ
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@martin-henz

Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Aug 2, 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.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The sound module now supports concurrent play() calls and adds play_in_tab(). The Sound tab tracks overlapping sources, delays cleanup until playback ends, and renders independent native audio controls for WAV data URI players.

Changes

Sound playback and tab players

Layer / File(s) Summary
Concurrent playback lifecycle
src/tabs/Sound/src/index.tsx, src/tabs/Sound/src/__tests__/index.test.ts
Repeated play() calls start concurrently. Pending sources control status and AudioContext cleanup. Stop actions affect all active sources.
Tab player storage and rendering
src/bundles/sound/src/protocol.ts, src/tabs/Sound/src/index.tsx, src/tabs/Sound/src/__tests__/index.test.ts
addPlayerToTab() stores WAV data URI entries, notifies subscribers, and renders one native audio control per entry.
play_in_tab API and WAV encoding
src/bundles/sound/src/functions.ts, src/bundles/sound/src/index.ts, src/bundles/sound/src/__tests__/*
play_in_tab() validates and samples sounds, creates stereo PCM WAV data, and adds separate tab players without immediate playback.

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

Sequence Diagram(s)

sequenceDiagram
  participant SoundModulePlugin
  participant play_in_tab
  participant SoundTabRpc
  participant SoundTabPlugin
  SoundModulePlugin->>play_in_tab: validate and convert Sound
  play_in_tab->>play_in_tab: sample channels and encode WAV data URI
  play_in_tab->>SoundTabRpc: addPlayerToTab(wavDataUri)
  SoundTabRpc->>SoundTabPlugin: insert player entry
  SoundTabPlugin-->>SoundTabRpc: resolve after insertion
  SoundTabRpc-->>play_in_tab: resolve
  play_in_tab-->>SoundModulePlugin: return Sound
Loading

Possibly related PRs

Suggested reviewers: leeyi45

Poem

A rabbit hears the sources play,
Together now, without delay.
WAV bars bloom within the tab,
Each one gets its own control slab.
play_in_tab hops into view,
With players made for me and you.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Title check ✅ Passed The title clearly summarizes the two main changes: concurrent sound playback and restoring play_in_tab.
Description check ✅ Passed The description explains the issue, motivation, implementation, and test results, but omits some template sections.
Linked Issues check ✅ Passed The changes satisfy issue #841 by restoring concurrent playback and adding per-call in-tab audio players.
Out of Scope Changes check ✅ Passed The code and tests remain focused on the sound playback and play_in_tab requirements from issue #841.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sound-concurrent-play-and-play-in-tab

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.

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/tabs/Sound/src/index.tsx (1)

201-213: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent __ensureAudioContext() from reusing a closed AudioContext.

destroy() closes this.__audioContext when __destroyed is true and __pendingPlaybackCount is zero, but it does not reset this.__audioContext. __ensureAudioContext() only creates a new context when !this.__audioContext, so any later playSamples()/stopRecording() call uses the closed context and fails or hangs. Treat a closed AudioContext as no longer usable and create a new one.

🔧 Proposed fix
   private __ensureAudioContext(): AudioContext {
-    if (!this.__audioContext) {
+    if (!this.__audioContext || this.__audioContext.state === 'closed') {
       this.__audioContext = new AudioContext();
     }
     return this.__audioContext;
   }
🤖 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/tabs/Sound/src/index.tsx` around lines 201 - 213, Update
__ensureAudioContext() to detect when the existing AudioContext has been closed,
treating it as unusable and creating a new context just as it does when
__audioContext is absent. Preserve reuse of open contexts while ensuring later
playSamples() or stopRecording() calls never operate on the closed context
finalized by __maybeFinalizeDestroy().
🧹 Nitpick comments (2)
src/tabs/Sound/src/index.tsx (2)

61-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Associate the "Sound N" label with its <audio> control for assistive tech.

Currently the label is only a <p> placed visually above the control; screen readers announce the <audio> element without the "Sound N" context since there is no aria-label/aria-labelledby/<label> association. Add aria-label (or id/aria-labelledby) so the control's accessible name matches its visible label.

♿ Proposed fix
-          <audio src={player.dataUri} controls style={{ width: '100%' }} />
+          <audio src={player.dataUri} controls style={{ width: '100%' }} aria-label={`Sound ${index + 1}`} />
🤖 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/tabs/Sound/src/index.tsx` around lines 61 - 88, Update PlayerBarsView so
each audio control is programmatically associated with its visible “Sound N”
label. Add a unique label identifier per player and reference it from the
corresponding audio element via aria-labelledby, or provide the same visible
text through aria-label while preserving the existing presentation.

116-134: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound the __players list to avoid unbounded growth per Run.

__players only ever grows via addPlayerToTab() (Line 254) and is never trimmed until the whole SoundTabPlugin instance is replaced by the next Run. Each entry retains a full base64 WAV data URI plus a rendered <audio> element. A loop that calls play_in_tab() many times within one Run (a plausible pattern, e.g. adding a bar per note in a sequence) accumulates memory and DOM nodes with no upper bound for the life of that Run, which can noticeably degrade or freeze the tab for longer/larger sounds. Consider capping the list (e.g. keep the most recent N entries) or otherwise bounding growth.

♻️ Proposed fix
+  private static readonly MAX_PLAYERS = 50;
+
   async addPlayerToTab(wavDataUri: string): Promise<void> {
-    this.__players = [...this.__players, { id: this.__nextPlayerId, dataUri: wavDataUri }];
+    const nextPlayers = [...this.__players, { id: this.__nextPlayerId, dataUri: wavDataUri }];
+    this.__players =
+      nextPlayers.length > SoundTabPlugin.MAX_PLAYERS
+        ? nextPlayers.slice(nextPlayers.length - SoundTabPlugin.MAX_PLAYERS)
+        : nextPlayers;
     this.__nextPlayerId += 1;
     this.__emit();
   }

Also applies to: 253-257

🤖 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/tabs/Sound/src/index.tsx` around lines 116 - 134, Bound the per-run
__players collection in addPlayerToTab so repeated play_in_tab() calls cannot
grow it indefinitely. Keep only the most recent entries up to a defined maximum,
removing older entries and their associated rendered audio resources as needed
while preserving newest-entry playback behavior.
🤖 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.

Outside diff comments:
In `@src/tabs/Sound/src/index.tsx`:
- Around line 201-213: Update __ensureAudioContext() to detect when the existing
AudioContext has been closed, treating it as unusable and creating a new context
just as it does when __audioContext is absent. Preserve reuse of open contexts
while ensuring later playSamples() or stopRecording() calls never operate on the
closed context finalized by __maybeFinalizeDestroy().

---

Nitpick comments:
In `@src/tabs/Sound/src/index.tsx`:
- Around line 61-88: Update PlayerBarsView so each audio control is
programmatically associated with its visible “Sound N” label. Add a unique label
identifier per player and reference it from the corresponding audio element via
aria-labelledby, or provide the same visible text through aria-label while
preserving the existing presentation.
- Around line 116-134: Bound the per-run __players collection in addPlayerToTab
so repeated play_in_tab() calls cannot grow it indefinitely. Keep only the most
recent entries up to a defined maximum, removing older entries and their
associated rendered audio resources as needed while preserving newest-entry
playback behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f210742f-f22a-40e4-986f-da9b8fab0c5b

📥 Commits

Reviewing files that changed from the base of the PR and between 21123a0 and b5692f1.

📒 Files selected for processing (7)
  • src/bundles/sound/src/__tests__/sound.test.ts
  • src/bundles/sound/src/__tests__/utils.ts
  • src/bundles/sound/src/functions.ts
  • src/bundles/sound/src/index.ts
  • src/bundles/sound/src/protocol.ts
  • src/tabs/Sound/src/__tests__/index.test.ts
  • src/tabs/Sound/src/index.tsx

@Akshay-2007-1
Akshay-2007-1 self-requested a review August 2, 2026 11:06
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.

sound: play concurrently, sequentially, in-tab?

2 participants