Fix macOS freeze when PortAudio playback ends as the stream is stopped - #718
Open
Muskworker wants to merge 1 commit into
Open
Muskworker wants to merge 1 commit into
Muskworker wants to merge 1 commit into
Conversation
When the end of the playback range was reached, paCallback returned paAbort, which makes PortAudio stop the stream from within the audio thread. AudioController's playback timer notices the end of the range at about the same moment and calls Pa_StopStream from the main thread. On macOS both paths end up in AudioOutputUnitStop: the main thread holds the audio unit's mutex while waiting for the HAL IO mutex, and the IO thread holds the HAL IO mutex while waiting for the audio unit's mutex. When the two coincide Aegisub freezes permanently. Keep the callback running and pad with silence past the end of the range instead, and have IsPlaying() report false once the end has been reached so that AudioController stops the stream via Stop() as it already does for other players. Make the positions shared with the callback atomic while at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(tl;dr I was encountering a freeze, Claude found it was apparently due to a deadlock when using PortAudio and wrote up a suggested fix. appears to work for me; hopefully it can be useful to y'all.)
Problem
On macOS, Aegisub occasionally freezes permanently (0% CPU, must be force quit) at the end of an audio "play line". Seen on macOS 26.6.1, Apple Silicon, PortAudio 19.7.0.
sampleof the frozen process shows a lock-order inversion between two threads that are both insideAudioOutputUnitStop:Main thread (holds the audio unit's mutex, wants the HAL IO mutex):
CoreAudio IO thread (holds the HAL IO mutex, wants the audio unit's mutex):
PortAudioPlayer::paCallbackreturnspaAbortonce the playback range is exhausted, which makes PortAudio's CoreAudio backend callAudioOutputUnitStopfrom the IO thread (stop_stream:inpa_mac_core.c).AudioController::OnPlaybackTimernotices the end of the range at about the same moment and callsPa_StopStreamfrom the main thread. When the two coincide, they deadlock.This is related to, but not fixed by, PortAudio/portaudio#1174 / PortAudio/portaudio#1175: that PR removes the same inversion for
startStopCallback, but leaves thestop_stream:path alone. The reproducer below still deadlocks against both PortAudio master (873e3c8) and the PR branch (1240f06).Fix
Never stop the stream from the callback. Past the end of the range the callback pads with silence and returns
paContinue;IsPlaying()reports false once the end has been reached, soAudioController's existing timer stops the stream from the main thread viaStop().currentandend, which are shared with the callback, become atomics.A side effect is that
Play()on a stream that has run out but has not been stopped yet now simply resumes, instead of callingPa_StartStreamon a stream that is not stopped.Testing
paAbort(iterations 64 and 459 in two runs), and completes 3000 iterations when the callback pads with silence instead.meson testpasses; play line / play to end behave as before in manual use.pa_stop_race.c