Skip to content

Pin the microphone by name with a mic_device setting - #6

Open
MacphersonDesigns wants to merge 1 commit into
jaredrhod:mainfrom
MacphersonDesigns:mic-device-pinning
Open

Pin the microphone by name with a mic_device setting#6
MacphersonDesigns wants to merge 1 commit into
jaredrhod:mainfrom
MacphersonDesigns:mic-device-pinning

Conversation

@MacphersonDesigns

Copy link
Copy Markdown

The problem

Both sd.InputStream calls in ears.py open with no device= argument, so capture always follows the OS default input. The OS re-points that the moment a headset with a mic connects, which has two consequences:

  1. The agent starts listening through the headset instead of the mic you chose.
  2. A Bluetooth headset gets pulled out of high-quality A2DP into the narrowband HFP call profile, so playback audibly degrades mid-utterance — you hear it as the voice going thin and mono right after you speak.

Measured on macOS with an ATH-M50xBT2 connected: the headset held the default input at index 0 already running at 16000 Hz (HFP), while the onboard mic sat at index 3.

The fix

A new mic_device config key holding a device name, resolved to a sounddevice index at every stream open.

"mic_device": "MacBook Pro Microphone"
  • Names, not indices. Indices shift whenever a device connects or disconnects — the exact event this setting exists to survive.
  • Re-resolved per stream open, not cached at startup, so devices appearing or vanishing mid-session are handled.
  • Exact match first, then case-insensitive substring, so "MacBook Pro" resolves too.
  • Empty string is the default and preserves current behavior byte for byte.

Degradation

Follows the codebase's existing rule that the voice line degrades but never goes mute:

  • A name matching nothing → falls back to the system default and logs the available inputs, once, not per frame.
  • A device that resolves but refuses to open (unplugged between lookup and open, busy, wrong sample rate) → falls back rather than raising.
  • Device enumeration itself failing → falls back with a log line.

Naming

Deliberately not merged into or named after stt_device, which already exists and means the Whisper compute device (cpu/cuda). Different axis entirely.

Verification

With the headset connected and still holding the system default, capture opened the pinned onboard mic (index 3) instead of the headset (index 0). Each degradation path was exercised without raising: missing device, empty value, partial name, and the warn-once guard across repeated lookups.

Docs added to README.md (fine print) and TROUBLESHOOTING.md (next to the existing input-device entry).

Both sd.InputStream calls in ears.py opened with no device= argument, so
capture always followed the OS default input. The OS re-points that the
moment a headset with a mic connects, which has two consequences: the
agent starts listening through the headset instead of the mic you chose,
and a Bluetooth headset gets pulled out of high-quality A2DP into the
narrowband HFP call profile, so playback audibly degrades mid-utterance.

Adds a "mic_device" config key holding a device NAME, resolved to a
sounddevice index at every stream open. Names rather than indices
because indices are not stable — they shift whenever a device connects
or disconnects, which is the exact event this setting exists to survive.
Resolution is exact-match first, then case-insensitive substring, so
"MacBook Pro" finds "MacBook Pro Microphone".

Degradation follows the codebase's existing rule that the voice line
degrades but never goes mute: a name matching nothing falls back to the
system default and logs the available inputs once, and a device that
resolves but refuses to open falls back too rather than raising. An
empty value is the default and preserves current behavior exactly.

Deliberately NOT named after or merged into "stt_device", which is the
Whisper compute device (cpu/cuda) and is a different axis entirely.

Verified on macOS with a Bluetooth headset connected and holding the
system default: capture opened the pinned onboard mic (index 3) instead
of the headset (index 0), and the missing-device, empty-value, partial-
name and warn-once paths were each exercised without raising.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoPSh1EzL35HJAcBj6JFjF
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