fix(sound): share pan sampling during playback - #869
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Reviewed the diff and checked out the branch to run the suite directly. This is a solid, well-motivated fix with good test coverage — one design question worth a second pair of eyes, no blockers. The problem, as I traced it: Why the boundary-crossing half is necessary, not just the pure fix: I confirmed this isn't hypothetical — under Conductor,
One design question, not a blocker: the cache keys on closure IDs + duration, scoped to one evaluator/run, with no explicit invalidation — entries just accumulate for the run's lifetime and are matched by Verified locally (checked out
Minor nit, not worth blocking on: in both No changes requested from me — the core fix is correct, well-tested, and the harder boundary-crossing half is handled thoughtfully. |
|
Good work @11suixing11 . Thanks for contributing. |
- __ensureAudioContext() now treats a closed AudioContext as absent rather than reusing it: destroy() closes the context but never resets the field, so a playSamples() call still in flight when a Run ends (e.g. sampling was still running) could otherwise be handed back an already-closed, unusable context. - Cap __players to the most recent MAX_PLAYER_BARS entries so a loop calling play_in_tab() many times in one Run can't grow the list (each holding a full WAV data URI plus a rendered <audio> element) without bound for the tab's lifetime. - Associate each play bar's "Sound N" label with its <audio> control via aria-labelledby, so the control's accessible name matches what is shown on screen. Also updates play_in_tab() to use the shared sampleSound() helper (added by the just-merged #869) instead of directly sampling leftWave/rightWave, so it gets the same "sample the source once" fix play() already has for panned sounds - this one only surfaced once resolving this PR's conflict with master pulled sampleSound() in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Dv6WCgTvMVbCJmyRUY9pL
* fix(sound): play concurrently, add back play_in_tab (#841) 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 * fix(sound): address CodeRabbit findings on play_in_tab tab UI - __ensureAudioContext() now treats a closed AudioContext as absent rather than reusing it: destroy() closes the context but never resets the field, so a playSamples() call still in flight when a Run ends (e.g. sampling was still running) could otherwise be handed back an already-closed, unusable context. - Cap __players to the most recent MAX_PLAYER_BARS entries so a loop calling play_in_tab() many times in one Run can't grow the list (each holding a full WAV data URI plus a rendered <audio> element) without bound for the tab's lifetime. - Associate each play bar's "Sound N" label with its <audio> control via aria-labelledby, so the control's accessible name matches what is shown on screen. Also updates play_in_tab() to use the shared sampleSound() helper (added by the just-merged #869) instead of directly sampling leftWave/rightWave, so it gets the same "sample the source once" fix play() already has for panned sounds - this one only surfaced once resolving this PR's conflict with master pulled sampleSound() in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Dv6WCgTvMVbCJmyRUY9pL * fix(sound): address Akshay's review on play_in_tab - play_in_tab() now calls notifyConstructing() before sampling, like play() already does, so a long Sound shows "Constructing…" instead of looking stalled until the bar appears. addPlayerToTab() is the corresponding "done" signal (decrementing __constructingCount and recomputing status), matching how playSamples() does it for play(). - Deduplicated play()/play_in_tab()'s near-identical Sound argument validation into a shared assertPlayableSound() helper. - Added browser-mode rendering tests for PlayerBarsView (the actual "multiple bars stacked vertically" UI the issue asked for), which previously only had coverage at the addPlayerToTab() data layer. Exported PlayerBarsView so it's directly renderable in tests, and brought in the same vitest-browser-react/@vitest/browser-playwright setup already used by the Rune/Matrix/Curve tabs. The player-list cap Akshay's review also flagged (point 1) was already fixed in the previous commit before the review landed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Dv6WCgTvMVbCJmyRUY9pL * fix(sound): complete the play_in_tab review fixes The previous commit's git add silently dropped these files (one bad pathspec in a multi-path invocation aborts the whole add) - this is the rest of that change: - assertPlayableSound() dedup in functions.ts, and play_in_tab()'s new notifyConstructing() call - addPlayerToTab()'s matching __constructingCount decrement - the PlayerBarsView rendering tests and their vitest-browser-react/ browser-playwright devDependencies - 2 new bundle-level tests (notifyConstructing ordering) and 1 new tab-level test (constructing status closes out via addPlayerToTab) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Dv6WCgTvMVbCJmyRUY9pL * fix(sound): show a placeholder instead of silently dropping zero-duration play_in_tab() calls A zero-duration Sound is a valid neutral element (for consecutively()/ simultaneously(), e.g. as a reduce() starting value), not an error - but play_in_tab() previously just silently added nothing to the tab for one, which could read as the call having been dropped. Adds a new addZeroDurationPlayerToTab() RPC method (deliberately separate from addPlayerToTab(), not a variant of it): no sampling happens for a zero-duration Sound, so play_in_tab() never calls notifyConstructing() for one either, and folding this into addPlayerToTab() would incorrectly decrement __constructingCount on behalf of a call that never incremented it - which could cancel out an unrelated, genuinely concurrent play_in_tab() call's still-in- flight notifyConstructing(). PlayerBarEntry becomes a discriminated union so PlayerBarsView can render a plain "zero duration sound" placeholder line in the entry's call-order position, instead of a native <audio> control. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Dv6WCgTvMVbCJmyRUY9pL --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Akshay <131676168+Akshay-2007-1@users.noreply.github.com>
Re-opens #838, which was automatically closed (not merged) when its base branch
conductor-migrationwas deleted after being merged intomastervia #680. This branch cherry-picks the original commits from @11suixing11 unchanged, onto currentmaster.Summary
panoutputs once per timestamp and derive both PCM channels from that shared samplepan_modWhy the boundary handling matters
The pure
functions.tsoptimisation alone is lost in normal Source usage: a transformed Sound is encoded as[[left_wave, right_wave], duration]and decoded again byplay. TypedValue and pair wrappers are recreated during the Python round-trip, so the sampler metadata is retained using evaluator-scoped closure IDs instead of object identity. A modified Sound does not match the cache and falls back to the existing per-wave sampling path.Testing (from original PR, re-verify on CI)
play(pan(...))play(pan_mod(...))yarn workspace @sourceacademy/bundle-sound test(68 passed)yarn workspace @sourceacademy/bundle-sound tscyarn workspace @sourceacademy/bundle-sound lintFixes #800
Original author: @11suixing11