fix(sound): play concurrently, add back play_in_tab - #874
Conversation
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
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
@CodeRabbit review |
✅ Action performedReview finished.
|
WalkthroughThe sound module now supports concurrent ChangesSound playback and tab players
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
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winPrevent
__ensureAudioContext()from reusing a closedAudioContext.
destroy()closesthis.__audioContextwhen__destroyedis true and__pendingPlaybackCountis zero, but it does not resetthis.__audioContext.__ensureAudioContext()only creates a new context when!this.__audioContext, so any laterplaySamples()/stopRecording()call uses the closed context and fails or hangs. Treat a closedAudioContextas 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 winAssociate 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 noaria-label/aria-labelledby/<label>association. Addaria-label(orid/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 winBound the
__playerslist to avoid unbounded growth per Run.
__playersonly ever grows viaaddPlayerToTab()(Line 254) and is never trimmed until the wholeSoundTabPlugininstance is replaced by the next Run. Each entry retains a full base64 WAV data URI plus a rendered<audio>element. A loop that callsplay_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
📒 Files selected for processing (7)
src/bundles/sound/src/__tests__/sound.test.tssrc/bundles/sound/src/__tests__/utils.tssrc/bundles/sound/src/functions.tssrc/bundles/sound/src/index.tssrc/bundles/sound/src/protocol.tssrc/tabs/Sound/src/__tests__/index.test.tssrc/tabs/Sound/src/index.tsx
Summary
Fixes #841. The Conductor migration of the
soundmodule 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:
SoundTabPlugin) playback queue, so repeated/loopedplay()/play_wave()calls now start as soon as their own sampling finishes and are mixed together by the sharedAudioContext, instead of queueing behind whatever's already playing.play_in_tab(): brought back as a new exported function. It samples the givenSoundand encodes it as adata:audio/wav;base64,...URI (pure computation — noAudioContextneeded, 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 testpasses for bothsrc/bundles/sound(69 tests, including newplay_in_tabcoverage) andsrc/tabs/Sound(22 tests, including new concurrency +addPlayerToTabcoverage)yarn tscandyarn lintpass for both packages (no new lint errors; one neweslint-disable-next-linewarning matches the existing pattern already present for every other validated function in the file)yarn buildsucceeds for both packages🤖 Generated with Claude Code
https://claude.ai/code/session_01B3LRoXRMQ5ADeYMdVTChCZ