fix(audio): move entire engine setup off main thread to prevent HAL hang#93
Open
skhe wants to merge 1 commit intomissuo:mainfrom
Open
fix(audio): move entire engine setup off main thread to prevent HAL hang#93skhe wants to merge 1 commit intomissuo:mainfrom
skhe wants to merge 1 commit intomissuo:mainfrom
Conversation
missuo#77's fix only wrapped startAndReturnError: in a background queue with a 3 s timeout, but the earlier CoreAudio-touching calls in -startCaptureWithAudioCallback: were still running on the main thread: -[AVAudioEngine inputNode] (triggers UpdateInputNode -> GetHWFormat) AudioUnitSetProperty(..., CurrentDevice, ...) -[AVAudioNode outputFormatForBus:] -[AVAudioInputNode installTapOnBus:...] -[AVAudioEngine prepare] All of them synchronously dispatch onto the AVAudioIOUnit serial queue, which in turn talks to coreaudiod via mach_msg. After a Bluetooth route change or other HAL glitch the reply can stall indefinitely and freeze the whole app (process alive, ~30% CPU, no recovery short of killing the process). Move the full setup+start sequence into the existing background dispatch and gate it with the same kEngineStartTimeoutSec semaphore. On timeout the main thread returns NO (SPAppDelegate.startAudioCaptureWithRetry retries once after 500 ms). A late-completing background setup whose main thread already gave up is detected via a mainAborted flag and the late-started engine is stopped to avoid leaking a live capture session.
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.
Fixes #92 (follow-up to #77).
Background
#77 fixed one variant of "Koe freezes at hotkey-hold, process alive, ~30% CPU, no recovery short of killing it" by moving
-[AVAudioEngine startAndReturnError:]to a background queue guarded by a 3 sdispatch_semaphore_wait. Since then I hit the exact same symptom again on macOS 26.3 / 1.0.14 (15), but the sample shows the main thread stuck earlier, before the existing timeout gate:All of the CoreAudio-touching calls in
startCaptureWithAudioCallback:—-[AVAudioEngine inputNode],AudioUnitSetProperty(..., CurrentDevice, ...),-[AVAudioNode outputFormatForBus:],-[AVAudioInputNode installTapOnBus:...],-[AVAudioEngine prepare]— internallydispatch_synconto theAVAudioIOUnitserial queue, which in turn talks tocoreaudiodovermach_msg. After a Bluetooth route change, aggregate-device reconfiguration, or other HAL glitches, that reply can stall indefinitely, freezing the entire app.#77only movedstartAndReturnError:off-main, so whenever the HAL stalls before that (as it does for me now), the code never even reaches the existing timeout gate.Change
Move the full engine setup + start sequence into the background
dispatch_asyncthat was already used forstartAndReturnError:, and gate the whole thing behind the samekEngineStartTimeoutSecsemaphore. On timeout, the main thread returnsNO;SPAppDelegate.startAudioCaptureWithRetrystill retries once after 500 ms, matching the existing recovery path.To avoid leaking a live capture session when the main thread has already given up but the background setup eventually succeeds, a
mainAbortedflag guarded by a shared lock tells the background block to[engine stop]the late-started engine instead of publishing it.Diff summary:
KoeApp/Koe/Audio/SPAudioCaptureManager.minputNode,AudioUnitSetProperty,outputFormatForBus:, converter init,installTapOnBus:,prepare, andstartAndReturnError:in one backgrounddispatch_async(QOS_CLASS_USER_INITIATED, 0)block.dispatch_semaphore_waittimeout and the same error-return shape (return NO), sostartAudioCaptureWithRetryworks unchanged.mainAborted+setupLockto cleanly stop the engine if the background block finishes after the main thread has already bailed out.AudioUnitSetPropertyfailure still rebuilds a fresh engine; the invalid-format guard (channelCount == 0 || sampleRate <= 0) still aborts; the tap block is byte-for-byte the same.No public API changes. No config or UX changes.
Testing
xcodebuild -scheme Koe-liteis broken on my box (unrelated Xcode plug-in issue), butxcrun clang -fsyntax-onlypasses with no new warnings on the modified file.startCaptureWithAudioCallback:and thestartAudioCaptureWithRetrywrapper inSPAppDelegate, so the normal path is unchanged; only the timing/threading of the pre-existing steps moves off the main thread.AVAudioEngine's internaldispatch_syncintoAVAudioIOUnitstalls on HAL IPC, the main thread unblocks afterkEngineStartTimeoutSec(3 s) instead of forever — putting it on the same footing as thestart()hang that#77already handles.Happy to iterate on the locking style or the timeout constant if you'd prefer.