Skip to content

feat(audio): optional Bluetooth input guard that keeps output on A2DP - #473

Open
wxtc0219 wants to merge 1 commit into
ronitsingh10:mainfrom
wxtc0219:feat/bluetooth-a2dp-input-guard
Open

wxtc0219 wants to merge 1 commit into
ronitsingh10:mainfrom
wxtc0219:feat/bluetooth-a2dp-input-guard

Conversation

@wxtc0219

Copy link
Copy Markdown

What this does

Adds an opt-in "Bluetooth Audio Quality" guard that keeps the default input on the built-in microphone, so AirPods stay on the A2DP link instead of falling back to HFP/SCO (which roughly halves output quality and burns headset battery).

When enabled:

  • Bluetooth inputs are disabled in FineTune's input pickers (menu bar popup + settings), so the headset mic cannot be selected as the input by accident.
  • The built-in mic is re-pinned on device connect, system wake, default-input change, and Bluetooth output sample-rate changes.
  • Corrections are rate limited (one write per second, max four in a rolling ten-second window) and fall back to a single delayed retry, so a stubborn app can't put the guard into a write loop.
  • A hint appears when a Bluetooth output reports below 44.1 kHz, which is how SCO/call mode shows up.
  • The status line reports what the guard is doing (pinned / contention / unavailable).

Off by default. When disabled, none of the existing input-lock paths change behaviour.

Why the existing input lock doesn't cover this

Lock Input Device (from #29) protects a user-chosen input and correctly steps aside for priority rules and deliberate changes (#428). That means:

  • A user who locked, say, a USB mic still has Bluetooth fallbacks applied on top of it.
  • Auto-switching on connect can still move the default input before the lock reacts ([BUG]Lock Input Device #459 reports this on some machines).
  • Nothing stops the user from selecting the headset mic manually.

This PR adds a separate, stricter mode for the one case where the headset must never become the default input. It intentionally does not claim to disable HFP/SCO: macOS has no public API to make one device's input unavailable to other apps, so if a call app opens the headset mic directly, the guard can only report it.

Testing

  • New BluetoothInputGuardPolicyTests covers the selection rules: built-in preference, Bluetooth never chosen (even when it claims builtIn), no built-in mic → no target, disabled → unrestricted, A2DP rates not flagged, SCO rates flagged, unknown rate not flagged.
  • Manual on an M-series Mac with AirPods Pro: switching the default input to another device with SwitchAudioSource/System Settings is reverted within ~1 s; after disabling the guard, the same switch sticks. Wake and reconnect paths behave the same.

Notes

  • Settings key bluetoothInputProtection, default false, so users opt in.
  • No audio is captured; the guard only reads device metadata and sets the default input.
  • Happy to rename the setting or restructure the UI if you'd prefer it inside the existing input-lock section instead of its own section.

…A2DP

AirPods drop from the A2DP link to HFP/SCO as soon as anything opens the
headset microphone, which halves the output quality. The default input
flipping to the headset is the usual trigger, and the existing input lock
does not cover it: priority rules and macOS auto-switching on connect can
still move the default input.

Add an opt-in guard that keeps the default input on the built-in mic:
- refuses Bluetooth inputs in FineTune's device pickers
- re-pins the built-in mic on connect, wake, default-input change and
  Bluetooth sample-rate changes, rate limited so it cannot write in a loop
- surfaces a call-mode hint when a Bluetooth output runs below 44.1 kHz
- states in the UI that an app opening the headset mic directly cannot be
  preempted, so HFP/SCO is not disabled outright

The guard is off by default and leaves every existing input-lock path
untouched while disabled. Selection rules are covered by unit tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant