fix(audio): prevent main-thread hang after Bluetooth device route change#82
Merged
Merged
Conversation
After a Bluetooth headphone route change, AudioUnitSetProperty fails with 'nope' (1852797029) and the IO unit enters an inconsistent state. The previous code logged the error but continued with the broken engine, causing HALC_ProxyIOContext::StartAndWaitForState to block indefinitely on the main thread — the app appeared frozen with 30%+ CPU. Three defenses: 1. When AudioUnitSetProperty fails, discard the corrupted engine and create a fresh one that uses the system default input device instead of proceeding with a broken IO unit. 2. Validate inputNode format (channelCount > 0, sampleRate > 0) before installing the tap. After device route changes the node can report '0 ch, 0 Hz' — bail early instead of entering a doomed start. 3. Move audioEngine.start() to a background thread with a 3-second timeout via dispatch_semaphore. If CoreAudio's HAL proxy blocks in StartAndWaitForState the main thread is released after the timeout, the error path triggers, and the user can retry immediately. Also: - Extract duplicated audio-start code into startAudioCaptureWithRetry with a single 500ms retry for transient post-route-change failures - Release audioEngine on all failure paths and in stopCapture to prevent stale engine references from accumulating Closes missuo#77 Co-authored-by: Davy <thedavidweng@users.noreply.github.com>
Owner
|
@thedavidweng Please resolve the conflicts. |
Contributor
Author
|
@cursoragent Resolve conflicts |
Owner
It seems that cursor ignores you. |
Contributor
Author
你大爷的 |
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.
Address #77
Summary
Fix the app-freeze bug where Koe hangs with 30%+ CPU after a Bluetooth headphone / audio device route change. The hotkey triggers but recording never starts; the process stays alive but unresponsive until force-quit.
Root Cause
Three cascading failures after a BT device route change:
AudioUnitSetPropertyfails with'nope'(1852797029) when setting the input device on the newAVAudioEngine. The previous code logged this but continued with the corrupted engine whose IO unit was in an inconsistent state.inputNode.outputFormatForBus:0returns0 ch, 0 Hzon the broken engine. No validation existed, so an invalid format was passed toinstallTapOnBus:andstartAndReturnError:.AVAudioEngine.start()blocks indefinitely — CoreAudio'sHALC_ProxyIOContext::StartAndWaitForStatereturns error 35 (EAGAIN) and never completes. Since this runs synchronously on the main thread, the entire app freezes.Fix
Three defensive layers, any one of which prevents the hang:
1. Discard corrupted engine on
AudioUnitSetPropertyfailureWhen setting the input device fails, the IO unit is in an unusable state. Instead of proceeding, create a fresh engine that falls back to the system default input device.
2. Validate
inputNodeformat before proceedingReturn
NOearly ifchannelCount == 0orsampleRate <= 0:3. Move
audioEngine.start()off the main thread with a 3s timeoutUse
dispatch_semaphoreto bound the blockingstart()call. If CoreAudio's HAL proxy hangs, the main thread is released after 3 seconds, the error path triggers, and the user can retry immediately.Also
startAudioCaptureWithRetrywith a single 500ms retry for transient post-route-change failuresaudioEngine = nilon all failure paths and instopCaptureto prevent stale engine refsBehavior
start()returns in <100ms)AudioUnitSetPropertyreturns'nope'inputNodereports0 ch, 0 HzinstallTap→ crash/hangNOimmediatelyTesting
cargo test --no-default-features— 56 tests).mfiles verified for balanced braces/brackets/parenscargo fmt --check— pass