Skip to content

Fix mic going silent after display or input device changes#102

Open
modojodo wants to merge 2 commits into
karansinghgit:mainfrom
modojodo:fix/mic-recovery-on-device-change
Open

Fix mic going silent after display or input device changes#102
modojodo wants to merge 2 commits into
karansinghgit:mainfrom
modojodo:fix/mic-recovery-on-device-change

Conversation

@modojodo

@modojodo modojodo commented Jul 5, 2026

Copy link
Copy Markdown

Summary

Fixes a bug where the microphone stops picking up audio after switching monitors/displays, plugging/unplugging input devices, or changing the default input in macOS System Settings — even though the app still appears to be recording.

The root cause is in AudioRecordingService: the AVCaptureSession was being torn down without being restarted, and hardware route changes were not triggering a session recovery when the selected device ID stayed the same.

Root cause

Three separate gaps combined to produce the reported behavior:

1. setupSession() stopped the capture session but never restarted it

selectedDeviceId's didSet calls setupSession(), which runs captureSession?.stopRunning() and rebuilds inputs/outputs — but intentionally does not call startRunning() (to avoid keeping the mic hot while idle).

That is correct for idle state, but broken when a session rebuild happens during an active recording or while a pre-warmed session is running. The session is left configured but stopped, so no audio buffers arrive and the waveform flatlines.

2. Plug/unplug notifications did not recover stale sessions

handleDeviceChange only called fetchAvailableDevices(). If the selected device's uniqueID did not change (common when reconnecting a display with HDMI/DisplayPort audio, or when macOS re-enumerates devices without changing the UID), setupSession() was never invoked and the existing AVCaptureSession could remain alive-but-silent.

3. System Settings input changes were ignored

SpeakType persists a specific selectedDeviceId in UserDefaults. Changing the default input in System Settings → Sound → Input does not fire AVCaptureDevice connect/disconnect notifications, so the app kept capturing from the old pinned device. Users reasonably expect System Settings changes to take effect.

Fix

All changes are in speaktype/Services/AudioRecordingService.swift:

  1. Restart after reconfigurationsetupSession() now records whether the session was running or a recording was active before teardown, and calls restartCaptureSession() on audioQueue when either is true.

  2. Recover on hardware route changeshandleDeviceChange now calls recoverCaptureSessionIfNeeded(forceReconfigure: true) after refreshing the device list, forcing a rebuild even when selectedDeviceId is unchanged.

  3. Follow macOS default input changes — Added a Core Audio property listener on kAudioHardwarePropertyDefaultInputDevice. When the system default changes:

    • If the app's current selection matched the previous system default, it follows the new default (so System Settings changes work as expected).
    • Otherwise it rebuilds/restarts the session in place (preserving an explicit in-app device choice).

Test plan

  • Start recording with the built-in mic; confirm waveform and transcription work.
  • While recording, switch to an external monitor (or unplug/replug one) and confirm audio continues without restarting the app.
  • While recording, plug/unplug a USB microphone or headset and confirm capture recovers (or falls back to the next available device).
  • Change the default input in System Settings while the app is using that device; confirm the app follows the new default on the next recording (or mid-recording if applicable).
  • Explicitly select a non-default device in SpeakType Settings; change System Settings default to a different device; confirm SpeakType keeps the explicitly chosen device.
  • Confirm the mic indicator is not lit continuously while idle (no regression from pre-warm behavior).

Restart the AVCaptureSession after reconfiguration and recover the capture
path when hardware routes change, including system default input updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
@modojodo

modojodo commented Jul 5, 2026

Copy link
Copy Markdown
Author

@karansinghgit Any way to get this merged or have a fix available for this issue.

- Re-arm idle auto-stop after restarting a pre-warmed session on a
  device/route change, so the mic no longer stays hot forever while idle.
- Remove the Core Audio default-input listener (and NotificationCenter
  observers) on deinit. Non-singleton instances (Dashboard/Transcribe
  views, tests) previously leaked a listener registration per instance.
@karansinghgit

Copy link
Copy Markdown
Owner

Reviewed and pushed two fixes on top of this branch.

Verdict: correct diagnosis and sound approach (restart after reconfigure, recover on route changes when the device ID is unchanged, follow the macOS default input via a Core Audio listener). Builds clean, unit tests pass. Found two real defects and fixed them.

1. Idle mic stays hot after a device change (regresses this PR's own goal).
restartCaptureSession() cancelled the idle auto-stop and started the session but never re-armed the auto-stop. A pre-warmed but idle session rebuilt on a display reconnect or default-input change would then run forever with the indicator lit, which is exactly the "no continuous mic while idle" case in the last test-plan item. Fix: after restarting, if not actively recording, call scheduleIdleSessionStop() again.

2. Core Audio listener leaked per instance.
AudioObjectAddPropertyListenerBlock had no matching removal and there was no deinit. The service is not only a singleton: DashboardView, TranscribeAudioView, and the tests each construct their own AudioRecordingService(), so every one registered a system-wide default-input listener that was never removed. Fix: retain the block, add stopObservingDefaultInputDevice(), and add a deinit removing the Core Audio listener plus the NotificationCenter observers.

Left alone: setupSession() mutates captureSession/audioOutput on the main thread while the capture delegate runs on audioQueue. This is a pre-existing threading seam that the PR fires more often but does not newly introduce; worth a follow-up if you want it hardened.

@karansinghgit

Copy link
Copy Markdown
Owner

@modojodo can you help test this?

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