Fix Bluetooth headset mode churn while idle - #749
Conversation
|
@grohith327 This fork PR’s required Build and Test and PR Archive workflows are currently blocked with |
Greptile SummaryFixes idle Bluetooth headset mode churn by:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 785b2bca0f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } else { | ||
| self.scheduleAudioEngineStandbyRetirement() | ||
| await self.retireAudioEngineAndWait(reason: "recording_stop_release") | ||
| } |
There was a problem hiding this comment.
Avoid blocking stop on engine retirement
When Faster Recording Start is off, this awaits retireAudioEngineAndWait on every normal stop after the tap/engine have already been stopped but before onCaptureStopped and before final transcription begins. The retirement drain performs AVAudioEngine deallocation, which this codebase treats as potentially waiting on AVAudioIOUnit internals, so legacy-mode users can see stop cues/UI release and text output stall on Core Audio teardown. Consider scheduling the retirement off-main (while still waiting before the next engine creation) or moving the callback/final transcription path ahead of the deallocation barrier.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65cbced092
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| await AudioStartupGate.shared.scheduleOpenAfterInitialUISettled() | ||
| await AudioStartupGate.shared.waitUntilOpen() |
There was a problem hiding this comment.
Move the startup gate before CoreAudio calls
This wait happens too late to protect launch from CoreAudio initialization: initialize() has already registered CoreAudio listeners and called AudioDevice.listInputDevices() on lines 1258-1264 before the gated task starts. Since ContentView opens the gate 2 seconds after appear while initialize() runs after 1.5 seconds, those earlier calls can still hit the SwiftUI/AVFoundation launch race the gate is meant to avoid; the wait needs to happen before any listener registration or device query.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid—fixed in 4912bdc. ContentView now waits for AudioStartupGate before starting AudioHardwareObserver or invoking ASR initialization, and ASRService.initialize() itself awaits the gate before permission, listener, or device-list calls.
|
|
||
| let microphonePreferenceCoordinator = | ||
| AppServices.shared.microphonePreferenceCoordinator | ||
| _ = microphonePreferenceCoordinator.enforcePreferredInput(reason: "startup") |
There was a problem hiding this comment.
Avoid changing the system mic on direct startup
When manual microphone mode is saved and Faster Recording Start remains enabled, this startup path still calls enforcePreferredInput, which writes macOS's global default input via setDefaultInputDevice. The direct capture path does not need that global switch—start() only enforces the preferred input for the legacy AVAudioEngine path when experimentalDirectAudioCaptureEnabled == false—so simply launching FluidVoice can override the user's system default and, for Bluetooth microphones, move the headset back toward the mic/HFP route while idle.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid—fixed in 4912bdc. The direct backend no longer writes the global system input at startup or during default/device-list changes; it resolves the saved preferred device through direct route recovery instead. Legacy AVAudioEngine retains global-input enforcement, with focused policy coverage for both modes.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4912bdc0e4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| self.scheduleAudioRouteRecovery( | ||
| reason: "direct input device list changed", | ||
| requiresIdlePrewarm: true | ||
| ) |
There was a problem hiding this comment.
Gate direct recovery to selected-device changes
In manual microphone mode with direct capture enabled, this branch now schedules route recovery for any input device-list delta, not just changes involving the selected microphone. During an active dictation, scheduleAudioRouteRecovery immediately disables the capture pipeline before rebuilding, while the direct backend already owns liveness/format listeners for the active device, so plugging in or removing an unrelated input device can create an avoidable audio gap. Consider filtering the UID delta to the preferred/current device, or relying on the direct invalidation path for active-device failures.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid—fixed in bcc85ca. Direct route recovery now runs only when the saved preferred input UID changes availability; unrelated device-list deltas do nothing, while the active device remains covered by its existing liveness and format listeners. Added regression coverage for unrelated additions plus preferred-device removal and return.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bcc85ca4ea
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| Task { | ||
| await AudioStartupGate.shared.scheduleOpenAfterInitialUISettled() | ||
| } |
There was a problem hiding this comment.
Move default-device lookup behind the startup gate
This still schedules the startup gate from onAppear, after SwiftUI has already initialized the view's stored @State values; selectedInputUID is still initialized with AudioDevice.getDefaultInputDevice() in ContentView.swift:254, and the repo comment in SettingsView.swift:54-55 identifies that HAL query as the launch-time AttributeGraph/CoreAudio race. Fresh evidence: that initializer remains outside the gate, so the first ContentView construction can initialize CoreAudio before this task runs; move the default-device lookup into the gated initialization instead.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid—fixed in 65ab727. selectedInputUID now initializes without touching Core Audio and is populated by the existing gated audio-initialization block after AudioStartupGate opens. The later hardware-change and backup-restore lookups occur only after startup.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65ab7270ac
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| do { | ||
| try self.configureSession() | ||
| self.benchmarkLog("audio_engine_prewarm reason=\(reason) elapsedMs=\(self.elapsedMilliseconds(since: startedAt))") | ||
| _ = try await self.prepareDirectAudioInput(reason: reason) |
There was a problem hiding this comment.
Add drain barrier before direct prewarm
When Faster Recording Start is enabled, or an idle route recovery requests prewarm immediately after a legacy recording stop, the stop path only schedules AVAudioEngine deallocation on the retirement drain, but this path prepares Direct Core Audio without the waitForScheduledReleases() barrier that startConfiguredAudioCapture() uses. In that handoff window, direct capture can create/register a new Core Audio backend while the old AVAudioEngine/AUHAL is still being released, reintroducing the teardown/creation overlap this drain is meant to serialize; please await the drain before direct prewarm as well.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid—fixed in ada31c3. Direct idle prewarm now waits for every scheduled AVAudioEngine release before registering the direct backend, matching the recording-start barrier. It rechecks termination, capture transition, and prepared state after the await to avoid continuing stale prewarm work.
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Description
Prevents FluidVoice from repeatedly forcing Bluetooth headphones into low-quality HFP/call mode while dictation is idle.
On v1.6.6, a legacy
AVAudioEngineidle prewarm can publishAVAudioEngineConfigurationChangeimmediately after it is created. FluidVoice treats that notification as another idle route change, retires the engine, prewarms a replacement, and repeats indefinitely. In a reproduced session this reached more than 3,000 recovery generations, with each generation creating input/output audio units again. Bluetooth playback oscillated or remained at 16 kHz mono until FluidVoice quit.This PR:
AVAudioEngineis created on demand insteadAVAudioEngineConfigurationChangewhile capture is fully idle, preventing the self-generated recovery loopType of Change
Related Issue or Discussion
Closes #694.
Also relates to #678 and the user reports collected in #727, #663, and #695.
Testing
swiftlint --strict --config .swiftlint.yml Sources— blocked because this machine has Command Line Tools but not Xcode'ssourcekitdInProc.frameworkwrapIfStatementBodiesbaseline findings inASRService.swiftunder SwiftFormat 0.62.1)Hardware regression sequence:
engine configuration changedidle recoveries and dropped output to the Bluetooth headset route.2 ch 32-bit Float 44.1 kHzoutput.Screenshots / Video
Notes
The normal Bluetooth quality drop while actively recording from a Bluetooth microphone is inherent to HFP. This fix targets the avoidable case: FluidVoice touching or retaining the input audio unit while idle, or allowing macOS to replace an explicitly preferred built-in microphone after a Bluetooth reconnect.
The direct Core Audio prewarm remains safe because it prepares registration and fixed memory without starting hardware IO.