From 9a47e43ce3dcf6c953255e5104e6de53697da4d8 Mon Sep 17 00:00:00 2001 From: Damien Metzler Date: Fri, 17 Apr 2026 13:52:45 +0200 Subject: [PATCH] fix: resolve system default input device explicitly for Bluetooth HFP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When 'System Default' is selected, AudioEngine now explicitly resolves the default device ID via kAudioHardwarePropertyDefaultInputDevice and sets it on the AudioUnit with AudioUnitSetProperty. This ensures Bluetooth devices (e.g. AirPods) get the A2DP→HFP/SCO profile settling wait and the tap format is built from actual hardware properties rather than AVAudioEngine's potentially stale cache. --- wispr/Services/AudioEngine.swift | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/wispr/Services/AudioEngine.swift b/wispr/Services/AudioEngine.swift index 32767b3..059bf11 100644 --- a/wispr/Services/AudioEngine.swift +++ b/wispr/Services/AudioEngine.swift @@ -97,13 +97,14 @@ actor AudioEngine { self.engine = audioEngine let inputNode = audioEngine.inputNode - // Assign the selected device to this engine's input AudioUnit - // BEFORE reading any format or installing taps (Req 2.1, 2.3). - // For selected devices, build an explicit tap format from CoreAudio - // since inputNode's cached format may be stale after device switches. - // For system-default devices, nil lets AVAudioEngine use its cache. + // Assign the input device to this engine's AudioUnit BEFORE reading + // any format or installing taps (Req 2.1, 2.3). + // Always resolve the actual device — even for "System Default" — so + // Bluetooth devices get the HFP/SCO settling wait and the tap format + // is built from real hardware properties rather than a stale cache. var tapFormat: AVAudioFormat? = nil - if let deviceID = selectedDeviceID { + let resolvedDeviceID = selectedDeviceID ?? getDefaultInputDeviceID() + if let deviceID = resolvedDeviceID { var devID = deviceID let status = AudioUnitSetProperty( inputNode.audioUnit!, @@ -114,13 +115,13 @@ actor AudioEngine { UInt32(MemoryLayout.size) ) if status == noErr { - Log.audioEngine.debug("Per-engine input device set to \(deviceID)") + let source = selectedDeviceID != nil ? "explicit" : "system default" + Log.audioEngine.debug("Per-engine input device set to \(deviceID) (\(source))") } else { - Log.audioEngine.warning("AudioUnitSetProperty failed (OSStatus: \(status)) for device \(deviceID), falling back to system default") - selectedDeviceID = nil + Log.audioEngine.warning("AudioUnitSetProperty failed (OSStatus: \(status)) for device \(deviceID), falling back to AVAudioEngine default") } - if selectedDeviceID != nil { + if status == noErr { let device = CoreAudioDevice(id: deviceID) // Bluetooth devices switch from A2DP (48kHz) to HFP/SCO @@ -151,8 +152,8 @@ actor AudioEngine { } } - let deviceDescription = selectedDeviceID.map { String($0) } ?? "system default" - Log.audioEngine.debug("startCapture — device: \(deviceDescription), tapFormat: \(tapFormat?.description ?? "nil (system default)")") + let deviceDescription = resolvedDeviceID.map { "\($0)\(selectedDeviceID == nil ? " (system default)" : "")" } ?? "none" + Log.audioEngine.debug("startCapture — device: \(deviceDescription), tapFormat: \(tapFormat?.description ?? "nil")") // The converter is created lazily from the first buffer's actual format. nonisolated(unsafe) var converter: AVAudioConverter?