Recover recorder after audio device changes - #1
Open
luckybucky9 wants to merge 1 commit into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:AVAudioEngineConfigurationChange(the engine actually died, possibly mid-recording): cancel the in-flight recording, discard the partial file, reset the status bar, rebuild. Mirrors thesystemWillSleeppath. Observer is registered per-engine inprewarm()and removed inteardown(), so reloads never leak or double-register.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.RecordingLifecyclegainsaudioConfigurationChanged(isReady:)so the decision logic stays pure and testable, matching the sleep/wake actions.Testing
swift buildclean,swift test— all 126 tests pass, including two newRecordingLifecyclecases (cancel-while-recording, prepare-only-when-ready).🤖 Generated with Claude Code