Skip to content

Fix crash when playing assistant audio on a non-rendering engine - #197

Merged
jamesrochabrun merged 1 commit into
mainfrom
jroch-fix-playback-io-cycle-crash
Aug 23, 2026
Merged

Fix crash when playing assistant audio on a non-rendering engine#197
jamesrochabrun merged 1 commit into
mainfrom
jroch-fix-playback-io-cycle-crash

Conversation

@jamesrochabrun

Copy link
Copy Markdown
Owner

Problem

A host app using the realtime voice stack crashed with:

*** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio',
reason: 'player did not see an IO cycle.'

AudioPCMPlayer.playPCM16Audio guards on audioEngine.isRunning before calling AVAudioPlayerNode.play(), but that flag can report true while the engine's IO thread never actually cycles — the AUVPAggregate stream-timeout failure (-10877) already documented for the capture side, where the engine claims it started but the HAL stream never runs. Calling play() on such an engine raises an uncatchable NSException and terminates the process, and it fires as soon as the first assistant audio delta arrives — before any capture-side watchdog gets a chance to rebuild the dead graph.

Fix

Require evidence that the render loop is actually producing cycles — a valid sample time on the engine's output node — in addition to isRunning before scheduling and playing:

guard
  audioEngine.isRunning,
  audioEngine.outputNode.lastRenderTime?.isSampleTimeValid == true
else {
  logger.warning("Dropping assistant audio: engine is not rendering IO cycles")
  return
}

On a dead engine the buffer is now dropped with a log warning instead of crashing. Since capture shares the same engine, the mic stays digitally silent in that state, so callers that watchdog for dead capture (e.g. a self-healing controller that rebuilds the graph) can recover the session.

The guard moved to the top of the method, which also skips the base64 decode and format conversion work when the engine can't play, and the old if audioEngine.isRunning wrapper around scheduling became redundant and was unwrapped (indentation-only change for that block).

Testing

  • swift build and package tests pass.
  • Verified in a host app: the previously crashing path now logs the warning and the session recovers via graph rebuild.

🤖 Generated with Claude Code

AVAudioEngine.isRunning can report true while the engine's IO thread
never cycles (AUVPAggregate stream timeout, -10877). Calling
AVAudioPlayerNode.play() in that state raises an uncatchable NSException
("player did not see an IO cycle") and terminates the host app.

Require a valid sample time on the output node before scheduling and
playing, and drop the buffer with a log warning otherwise so callers
that rebuild dead capture graphs get the chance to recover.
@jamesrochabrun
jamesrochabrun merged commit 6602038 into main Aug 23, 2026
3 checks passed
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.

1 participant