Skip to content

fix: resume a paused AudioBufferQueueSourceNode - #1434

Closed
HilloriDesai wants to merge 2 commits into
software-mansion:mainfrom
HilloriDesai:fix/queue-source-pause-resume
Closed

HilloriDesai wants to merge 2 commits into
software-mansion:mainfrom
HilloriDesai:fix/queue-source-pause-resume

Conversation

@HilloriDesai

Copy link
Copy Markdown

No existing issue. The bug is described below and covered by new tests.

⚠️ Breaking changes ⚠️

  • None.

Introduced changes

Bug: on 1.0.0, calling AudioBufferQueueSourceNode.pause() and then resume() plays silence. The node never reaches the end of its queue, so onPositionChanged and onBufferEnded stop firing. We hit this in a streaming TTS player on a Pixel 4a (Android 13).

Cause: pause() called AudioScheduledSourceNode::stop(0.0).

Fix (all in AudioBufferQueueSourceNode; AudioScheduledSourceNode is unchanged):

  • pause():
    • A node that hasn't rendered yet is unscheduled, keeping its buffers and read position.
    • Otherwise it sets stopTime_ = 0, so the render path stops the node on its next quantum. The node's own disable() keeps the buffers while isPaused_ is set.
    • isPaused_ is set in both cases.
  • disable(): no longer clears isPaused_. start() (that is, resume()) or stop() clears it.
  • stop() on a paused node:
    • Finishes it immediately, whatever when is: one ended, then the buffers are cleared.
    • A paused node is already silent, and waiting for the render path would never end for an empty queue, because processNode returns before updatePlaybackInfo.
    • The base stop() would also undo a pending pause, or compare against startTime_ == -1, which UBSan flags as an out-of-range cast to size_t.
  • stop() on a finished node: a no-op. JS doesn't guard a second stop().

Tests (AudioBufferQueueSourceTest, rendered through processGraph):

  • PauseThenResumeContinuesFromPausedFrame: the paused node is silent and fires no ended, and resume() continues from the exact frame where it paused.
  • PauseBeforeFirstQuantumThenResumePlaysFromStart: pause() right after start(), then resume().
  • StopWhilePausedStaysSilentAndEndsOnce: each case must stay silent, fire ended exactly once, and treat a second stop() as a no-op. The cases are:
    • stop(0) after the pause has rendered;
    • pause() and stop(later) in the same batch;
    • stop() inside the next quantum;
    • stop() after a pause before the first quantum;
    • a paused queue that played all its buffers;
    • a paused queue that never had a buffer.

The first two tests fail on main; the third fails on each intermediate version of the fix.

Checklist

  • Linked relevant issue
  • Updated relevant documentation
  • Added/Conducted relevant tests
  • Performed self-review of the code
  • Updated Web Audio API coverage
  • Added support for web
  • Updated old arch android spec file

Validation

Run Result
yarn test:cpp (smoke) 588 passed
Smoke with --ubasan 586 passed, no sanitizer reports
AudioBufferQueueSourceTest and AudioScheduledSourceTest with --tsan 14 passed
clang-format 23 and cpplint Clean
Devices The same change applied as a patch to 1.0.0, in a React Native app on a Pixel 4a (Android 13) and an iPhone 13 mini. Pause, resume, and play to the end now work on both

🤖 Generated with Claude Code

HilloriDesai and others added 2 commits November 12, 2026 17:27
pause() stopped the node through AudioScheduledSourceNode::stop(0). For a
started node, stop(0) takes the stop-before-start branch, which marks the
node finished and disables it for good (excludeFromProcessablePull_), so
resume() played silence and the queue never ended.

pause() now unschedules a node that has not rendered yet, and otherwise lets
the render path stop it, where disable() keeps the buffers while paused.
stop() on a paused node finishes it right away (a paused node is already
silent, and an empty queue never reaches the render path), and a second
stop() is a no-op.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@closetcaiman closetcaiman added the fix Code changes specifically addressing and resolving a bug label Nov 12, 2026
@github-actions

Copy link
Copy Markdown

WPT non-regression comparison

PASS — no regressions · 0 improved section(s) · overall 3430 → 3430 (0)

Unchanged sections (28)
Spec section Base pass Head pass Delta
Processing model 1 1 0
Other 56 56 0
AnalyserNode 138 138 0
AudioBuffer 142 142 0
AudioBufferSourceNode 220 220 0
AudioContext 58 58 0
AudioNode 261 261 0
AudioParam 680 680 0
BiquadFilterNode 275 275 0
ChannelMergerNode 30 30 0
ChannelSplitterNode 7 7 0
ConstantSourceNode 64 64 0
ConvolverNode 179 179 0
DelayNode 127 127 0
DestinationNode 0 0 0
DynamicsCompressorNode 4 4 0
GainNode 15 15 0
IIRFilterNode 87 87 0
MediaElementAudioSourceNode 0 0 0
MediaStreamAudioDestinationNode 1 1 0
MediaStreamAudioSourceNode 0 0 0
OfflineAudioContext 35 35 0
OscillatorNode 104 104 0
PannerNode 727 727 0
PeriodicWave 33 33 0
ScriptProcessorNode 0 0 0
StereoPannerNode 104 104 0
WaveShaperNode 82 82 0

Baseline: b3d198bb18013ff3188ba90fdb6385339d2e3d95 · Candidate: 3e378ecc9f6cdfb1cd14d0e33b775e74026f7528

Workflow run · this comment is updated on every push.

@mdydek

mdydek commented Nov 12, 2026

Copy link
Copy Markdown
Member

on it

@mdydek

mdydek commented Nov 13, 2026

Copy link
Copy Markdown
Member

issue fixed in #1436, it has a little different approach, but the issue is fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Code changes specifically addressing and resolving a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants