Skip to content

Recover recorder after audio device changes - #1

Open
luckybucky9 wants to merge 1 commit into
mainfrom
fix-audio-config-change
Open

Recover recorder after audio device changes#1
luckybucky9 wants to merge 1 commit into
mainfrom
fix-audio-config-change

Conversation

@luckybucky9

Copy link
Copy Markdown
Owner

What

Rebuilds the audio engine when the audio device landscape changes, so dictation keeps working after a Bluetooth headset connects, an iPhone Continuity mic appears, Teams/Zoom register their virtual devices, or a USB mic is replugged.

Why

The engine is created once (prewarm() at startup) and reused for every recording. When a device change invalidates it, the failure is completely silent: the hotkey fires, the status bar animates, a recording file is created — but the tap delivers zero buffers. Whisper transcribes an empty WAV and nothing is typed.

On my machine (two USB mics + Bluetooth headset + iPhone + Teams/Zoom, so the device list churns all day) I found 19 recording files all frozen at exactly 4096 bytes — a WAV header and no audio — clustered around device-change times, across several days. A daemon restart cures it every time, which is the tell that the engine binding, not the mic, is stale. I believe this is also what the AirPods comment in human37#46 is describing ("open-wispr regularly just can't hear me despite airpods being connected"). human37#83 fixed exactly this failure mode for sleep/wake; this covers the device-change trigger, reusing the same machinery.

How

Two complementary recovery signals, both funneled into the existing reload() path:

  1. AVAudioEngineConfigurationChange (the engine actually died, possibly mid-recording): cancel the in-flight recording, discard the partial file, reset the status bar, rebuild. Mirrors the systemWillSleep path. Observer is registered per-engine in prewarm() and removed in teardown(), so reloads never leak or double-register.

  2. CoreAudio listeners on kAudioHardwarePropertyDevices + kAudioHardwarePropertyDefaultInputDevice (topology changed while idle): re-resolve the configured device UID against the devices that exist now and rebuild proactively. Debounced 0.5s because a single Bluetooth connect fires several events. In testing the engine usually survives a topology change while running, so an in-flight recording is not cancelled for this — the rebuild is deferred until the recording stops (reloadRecorderAfterRecording), and the notification in (1) catches the case where the engine genuinely died.

RecordingLifecycle gains audioConfigurationChanged(isReady:) so the decision logic stays pure and testable, matching the sleep/wake actions.

Testing

  • swift build clean, swift test — all 126 tests pass, including two new RecordingLifecycle cases (cancel-while-recording, prepare-only-when-ready).
  • Verified the CoreAudio listeners fire on this machine (macOS 26 / M5) when the default input switches, with a standalone harness using the same API calls.
  • Reproduced the original failure fingerprint (header-only 4096-byte WAVs) from the field before the fix; after a daemon restart with a fresh engine, recordings capture normally.

🤖 Generated with Claude Code

The audio engine was created once at startup and never rebuilt when the
audio device landscape changed (Bluetooth headset connecting, iPhone
Continuity mic appearing, Teams/Zoom virtual devices, USB replug). Once
the engine's binding went stale, every recording produced a header-only
WAV: the hotkey fired and a file was created, but the tap delivered no
buffers — dictation silently typed nothing until the daemon was
restarted. Same failure mode human37#83 fixed for sleep/wake, but triggered by
device changes.

Recovery is wired through the existing reload() machinery on two
complementary signals:

- AVAudioEngineConfigurationChange (the engine actually died): cancel
  any in-flight recording, discard the partial file, and rebuild —
  mirrors the sleep path.
- CoreAudio device-list / default-input listeners (topology changed
  while idle): re-resolve the configured device UID and rebuild
  proactively, debounced to coalesce event bursts. If a recording is in
  flight the engine usually survives a topology change, so the rebuild
  is deferred until the recording stops rather than killing it.

RecordingLifecycle gains audioConfigurationChanged(isReady:) with unit
tests, keeping the decision logic pure like the sleep/wake actions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants