Add opt-in 'Lock input device' setting#489
Conversation
FluidVoice always follows the macOS default input because `syncAudioDevicesWithSystem` is hard-coded to `true`, leaving the existing preferred-device binding path as dead code. As a result, capture switches to whatever input macOS selects, and AirPods/Bluetooth connections silently overwrite the preferred mic. This adds an opt-in `lockInputDevice` setting (default off, so existing behaviour is unchanged) that pins capture to a chosen microphone: - ASRService binds the engine input to the preferred device when locked, and ignores macOS default-input changes and Bluetooth auto-switching. Falls back to the system default only when the locked device is unavailable. - SettingsView adds an "Always use this input device when available" toggle and keeps the picker pinned to the locked device without rewriting the macOS system default. - ContentView and MenuBarManager keep their UI in sync with the locked device. Scope is intentionally input-only to avoid the aggregate/Bluetooth output issues (OSStatus -10851) that motivated removing the original independent mode. Also refresh the cached default-device names on audio hardware ticks so the "(System Default)" markers stay accurate when only the system default changes (e.g. while the input is locked) — this also fixes a latent staleness bug for the output marker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 55d6911eed
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if self.lockInputDevice, | ||
| let prefUID = SettingsStore.shared.preferredInputDeviceUID, | ||
| newDevices.contains(where: { $0.uid == prefUID }) |
There was a problem hiding this comment.
Preserve the pinned UID when falling back
This locked-mode guard only returns while the preferred device is present; when the locked mic disappears with Settings open, the handler falls through to the existing fallback that assigns selectedInputUID to the system default. That programmatic selection change still runs the picker onChange above, which writes the fallback UID to SettingsStore.shared.preferredInputDeviceUID, so the original pinned microphone is forgotten and reconnecting it will not restore the lock target.
Useful? React with 👍 / 👎.
| .onReceive(self.appServices.audioObserver.$changeTick) { _ in | ||
| self.cachedDefaultInputName = AudioDevice.getDefaultInputDevice()?.name ?? "" | ||
| self.cachedDefaultOutputName = AudioDevice.getDefaultOutputDevice()?.name ?? "" |
There was a problem hiding this comment.
Gate the default-device refresh on startup
$changeTick is a @Published publisher, so this subscription receives the current value as soon as SettingsView appears, not only after later hardware changes. For onboarded users Settings is the initial detail view, so this can call AudioDevice.getDefaultInputDevice() before the .onAppear task has awaited AudioStartupGate, bypassing the startup ordering that the adjacent comments say prevents the CoreAudio/AttributeGraph launch crash.
Useful? React with 👍 / 👎.
Summary
FluidVoice always followed the macOS system input device — Settings even states "Audio devices are synced with macOS System Settings." There was no way to pin dictation to a specific microphone, so any system input change (e.g. a Bluetooth headset connecting) would silently switch the mic used for capture.
The root cause is that
SettingsStore.syncAudioDevicesWithSystemis hard-coded toreturn true, permanently disabling the existing preferred-input binding path inASRService.This PR adds an opt-in "Always use this input device when available" toggle (default off, so existing behavior is unchanged). When enabled, FluidVoice binds the chosen input device on the audio engine and ignores macOS input changes. If the locked device is unavailable, it transparently falls back to the system default and re-binds to the locked device when it reconnects.
Changes
SettingsStore— new opt-inlockInputDevicesetting (LockInputDevicekey).ASRService—bindPreferredInputDeviceIfNeeded()now also binds whenlockInputDeviceis on;handleDefaultInputChanged()andhandleDeviceListChanged()skip the auto-switch/route-recovery logic while locked.SettingsView— new toggle (far-right, switch style) with concise help text; input picker no longer overrides the system default while locked; restores the locked device on appear. Refreshes the (System Default) marker via the audio observer'schangeTickso it stays accurate while locked.ContentView/MenuBarManager— keep the preferred input selected (instead of the system input) while locked.Design notes
OSStatus -10851(kAudioUnitErr_InvalidPropertyValue) for aggregate/Bluetooth devices.syncAudioDevicesWithSystemcontract; nothing new is added to backup/restore for the same reason.Testing
Built locally (ad-hoc signed) and verified: locking pins capture to the selected mic, system input changes are ignored while locked, and disconnect/reconnect of the locked device falls back and recovers correctly.
🤖 Generated with Claude Code