Skip to content

fix: resolve system default input device explicitly for Bluetooth HFP#57

Open
dmetzler wants to merge 1 commit intosebsto:mainfrom
dmetzler:fix/airpods-system-default-bluetooth-hfp
Open

fix: resolve system default input device explicitly for Bluetooth HFP#57
dmetzler wants to merge 1 commit intosebsto:mainfrom
dmetzler:fix/airpods-system-default-bluetooth-hfp

Conversation

@dmetzler
Copy link
Copy Markdown

Summary

  • Bug: AirPods (and other Bluetooth devices) produce no audio when input is set to "System Default"
  • Root cause: When selectedDeviceID is nil, startCapture() skipped both the explicit AudioUnitSetProperty call and the waitForBluetoothHFP() settling delay — AirPods never switched from A2DP to HFP/SCO mic profile
  • Fix: Resolve the system default device ID via kAudioHardwarePropertyDefaultInputDevice and route it through the same per-engine device setup path as explicitly-selected devices

What changed

AudioEngine.startCapture() now uses selectedDeviceID ?? getDefaultInputDeviceID() so that "System Default" gets:

  1. Explicit AudioUnitSetProperty on the AudioUnit (no stale cache)
  2. Bluetooth transport type detection → waitForBluetoothHFP() settling
  3. Tap format built from actual hardware sample rate and channel count

Net diff: +13 / -12 lines in AudioEngine.swift. No new files, no API changes.

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.
Copilot AI review requested due to automatic review settings April 17, 2026 11:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Resolves a macOS audio capture issue where Bluetooth HFP devices (e.g., AirPods) produce no audio when the input device is set to “System Default” by explicitly resolving and configuring the default input device ID.

Changes:

  • Resolve “System Default” to a concrete AudioDeviceID via kAudioHardwarePropertyDefaultInputDevice.
  • Route default-device capture through the same per-engine AudioUnitSetProperty / Bluetooth HFP settling / tap-format path as explicitly selected devices.
  • Improve logging to indicate whether the device selection was explicit vs system default.
Comments suppressed due to low confidence (2)

wispr/Services/AudioEngine.swift:112

  • inputNode.audioUnit! is now executed even when selectedDeviceID is nil ("System Default"), because resolvedDeviceID resolves to a real device ID. Since AVAudioInputNode.audioUnit is optional, this introduces a potential crash if the underlying AudioUnit hasn’t been created yet. Consider safely unwrapping inputNode.audioUnit (and logging / falling back) before calling AudioUnitSetProperty.
            var devID = deviceID
            let status = AudioUnitSetProperty(
                inputNode.audioUnit!,
                kAudioOutputUnitProperty_CurrentDevice,
                kAudioUnitScope_Global,

wispr/Services/AudioEngine.swift:134

  • The Bluetooth HFP settling wait and explicit tap-format construction are currently gated on status == noErr. If AudioUnitSetProperty fails while using the system default device, waitForBluetoothHFP() is skipped again and tapFormat remains nil, which can reintroduce the original “System Default” AirPods failure mode. Consider running the Bluetooth detection / settling wait and building tapFormat based on resolvedDeviceID even when the per-engine device assignment fails (or at least for the system-default path).
            if status == noErr {
                let device = CoreAudioDevice(id: deviceID)

                // Bluetooth devices switch from A2DP (48kHz) to HFP/SCO
                // (16/24kHz) when the mic is activated. Wait for the rate
                // to settle before querying the hardware format.
                let isBluetooth = device.transportType == kAudioDeviceTransportTypeBluetooth
                    || device.transportType == kAudioDeviceTransportTypeBluetoothLE
                if isBluetooth {
                    try await waitForBluetoothHFP(deviceID: deviceID)
                }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sebsto
Copy link
Copy Markdown
Owner

sebsto commented May 2, 2026

Hey @dmetzler What is the bug this PR fixes ? How can I reproduce the bug to verify this change ?
Here is my test procedure

  • start wispr
  • verify sound input is "System"
  • press the hotkey and try a dictation
  • connect the airpods
  • verify in wispr settings that the input is still system default
  • start the dictation again

It works.

The Bluetooth HFP bug has been fixed in
#57

Is this different ?

@sebsto sebsto self-assigned this May 2, 2026
@sebsto sebsto added question Further information is requested bug Something isn't working labels May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working question Further information is requested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants