Skip to content

feat: fail over to an external mic when the MacBook lid is closed - #117

Merged
IchenDEV merged 1 commit into
mainfrom
t3code/clamshell-mic-failover
Sep 30, 2026
Merged

IchenDEV merged 1 commit into
mainfrom
t3code/clamshell-mic-failover

Conversation

@IchenDEV

Copy link
Copy Markdown
Owner

Problem

On a MacBook used in clamshell mode (external display, lid closed), the built-in microphone is physically disconnected in hardware, but macOS still lists it in CoreAudio as an available input device. Utter selected it, recorded silence, and never surfaced an error — the user spoke, saw the overlay, and got no text. This is the docked-at-a-desk case, a primary usage context for a menu-bar dictation app.

Fix

  • Lid detection — read AppleClamshellState from the IOPMrootDomain IORegistry entry (ClamshellState). Desktops without a lid report open and are unaffected.
  • Device classification — enumerate input devices and flag the built-in mic via kAudioDevicePropertyTransportType (AudioInputDevices). Continuity/iPhone, USB, Bluetooth, display, and aggregate inputs are all treated as external.
  • Resolution — a pure AudioInputResolver keeps the user's selected/system-default input when usable, and otherwise falls back to the first available external input; .unavailable when none exists.
  • Mid-session failover — while recording, a 0.5s watchdog re-checks the active input. If it becomes unusable (lid closes over the built-in mic, or the device is removed), capture restarts on a fallback input and the session continues; buffers are converted with AVAudioConverter when the new device's format differs.
  • Fail fast — when the lid is closed and no external input exists, the session ends with a new localized message and no insertion/clipboard/history side effect.
  • The wireless-remote (Xiaomi) capture path is unchanged.

Files

  • Added: Sources/Audio/ClamshellState.swift, Sources/Audio/AudioInputDevice.swift, Sources/Audio/AudioInputResolver.swift, Tests/OpenTypeTests/AudioInputResolverTests.swift
  • Changed: Sources/Audio/AudioCaptureManager.swift, Sources/App/VoicePipeline+Recording.swift, Sources/Integration/InputSessionCoordinator.swift, both Localizable.strings
  • SDLC bundle: docs/sdlc/changes/2026-10-01-clamshell-mic-failover/

Verification

  • bash scripts/sdlc-checks.sh — pass
  • bash scripts/ci-basic-checks.sh — pass (strings lint + key parity)
  • swift test — 808 tests, 18 skipped, 0 failures (run via the Xcode toolchain; the CommandLineTools SDK has no XCTest)
  • New resolver/classification/failover tests: 14 cases

Residual risk

  • Real hardware failover (engine restart + format conversion) and the no-input message are not exercised on a physical docked MacBook in this environment — a maintainer should run the docked check before merge.
  • Bluetooth/Continuity fallback may add latency or lower fidelity.
  • A failover seam may drop up to the watchdog interval (≤0.5s) of audio.

The SDLC verification artifact is at Status: pending approval pending human review.

Apple hardware disconnects the built-in microphone while the lid is
closed, but macOS still lists it as an available input, so Utter
recorded silence with no error.

- detect lid state via IOPMrootDomain AppleClamshellState
- classify inputs as built-in/external and resolve a usable device
- switch to an external input mid-session when the active one is lost
- fail fast with a localized message when no usable input exists
- keep the wireless-remote capture path unchanged
@IchenDEV
IchenDEV merged commit 1425696 into main Sep 30, 2026
3 checks passed
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