Skip to content

Stop the audio hardware before freeing the render buffers in deinit - #148

Open
chrizzchrizz wants to merge 1 commit into
dimitris-c:mainfrom
chrizzchrizz:fix/deinit-stop-hardware
Open

chrizzchrizz wants to merge 1 commit into
dimitris-c:mainfrom
chrizzchrizz:fix/deinit-stop-hardware

Conversation

@chrizzchrizz

Copy link
Copy Markdown

Problem

AudioPlayer.deinit frees the render buffers (rendererContext.clean()) without stopping the engine or the audio hardware first. The IO thread keeps pulling from the render callback until the hardware is stopped, so releasing a player while it is still rendering is a use-after-free.

This happens for example after an error: raiseUnexpected sets the state to .error, the engine keeps running, and pause() is a no-op in that state because the state no longer contains .running. An app that reacts to the error by releasing the player and creating a new one then crashes on the IO thread:

AURemoteIO::IOThread: EXC_BAD_ACCESS
ioData.mNumberBuffers=2554235498, ASBD::NumberChannelStreams(output.GetStreamFormat())=1; kAudio_ParamError
#0 AVAudioEngineGraph::RenderToABL ()
...
#3 AudioPlayerRenderProcessor.render(inNumberFrames:ioData:flags:)
#4 AudioPlayerRenderProcessor.renderProvider(...)
#7 AUHALOutputUnit_ProvideOutputCallback ()

The garbage mNumberBuffers is the freed inOutAudioBufferList.

Change

deinit stops the hardware first and then the engine, before freeing the buffers. Stopping the hardware first also avoids the IO thread calling the manual rendering block of an already stopped engine.

Testing

Reproduced in an app (Amperfy) that recreates the player after a stream error. The crash is gone with this change. The full test suite passes on macOS.

The IO thread keeps pulling from the render callback until the hardware is
stopped. Releasing an AudioPlayer while it was still rendering (e.g. after
an error, where pause() is a no-op) freed inOutAudioBufferList while the
render block still used it and crashed in AVAudioEngineGraph::RenderToABL.

Stop the hardware first, then the engine, so the IO thread does not call
the manual rendering block of a stopped engine.
@chrizzchrizz

chrizzchrizz commented Oct 19, 2026 •

Copy link
Copy Markdown
Author

Audio streaming isn't my area of expertise. So I had to rely on Claude Code to fix the issue. In my opinion, it worked well and the bug is gone, but I need your expertise for the final assessment.

Thx
Chrizz

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