Skip to content

Add support for choosing custom audio input#531

Merged
grohith327 merged 4 commits into
mainfrom
allow-default-audio-input
Jul 18, 2026
Merged

Add support for choosing custom audio input#531
grohith327 merged 4 commits into
mainfrom
allow-default-audio-input

Conversation

@grohith327

@grohith327 grohith327 commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Description

Add support for choosing custom audio input separate from MacOS default. The following is the behavior

  • Toggle on: input dropdown stays enabled.
  • Toggle on + user changes input: FluidVoice immediately syncs that selection to the Mac default input.
  • Toggle off + user changes input: FluidVoice only saves the manual preference and does not immediately change the Mac input.
  • Toggle on transition still restores/syncs back to the captured system input when available.

Also, add a menu bar setting to toggle micrphone

Type of Change

  • 🐞 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 🧹 Chore
  • 📝 Documentation update

Related Issue or Discussion

Closes #136

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS version: 27
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml Sources
  • Ran tests locally

Screenshots / Video

fv_audio_input_compressed.mp4

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 838862350d

ℹ️ 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".

Comment on lines +2359 to +2367
if nextMode == .manual {
if self.selectedInputUID.isEmpty,
let defaultUID = currentSystemInputUID
{
self.selectedInputUID = defaultUID
SettingsStore.shared.recordInputDeviceSelection(defaultUID)
} else {
SettingsStore.shared.recordInputDeviceSelection(self.selectedInputUID)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the saved manual microphone when toggling back

When the Settings view is in system mode, selectedInputUID tracks the current macOS default, so turning off “Use macOS default microphone” here overwrites any previously saved manual microphone with that system default. This differs from the menu path, which only initializes preferredInputDeviceUID when it is empty, and it means a user who temporarily switches back to macOS default loses their custom mic preference as soon as they re-enable manual mode unless they reselect it.

Useful? React with 👍 / 👎.

@grohith327
grohith327 force-pushed the allow-default-audio-input branch from 8388623 to 457af51 Compare July 5, 2026 18:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 457af51ff5

ℹ️ 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".

Comment thread Sources/Fluid/UI/SettingsView.swift Outdated
SettingsStore.shared.preferredInputDeviceUID = newUID
// Only change system default if sync is enabled
if SettingsStore.shared.syncAudioDevicesWithSystem {
SettingsStore.shared.recordInputDeviceSelection(newUID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid recording fallback selections as manual preferences

In manual mode this onChange fires for programmatic selection changes too, not just user picker choices. For example, SettingsView.onAppear still replaces an invalid/unavailable selectedInputUID with the current default; this line then persists that fallback as PreferredInputDeviceUID, so simply opening Settings while the saved mic is disconnected loses the custom mic that manual mode is meant to preserve. Gate this write to user-initiated picker changes or make the fallback path mode-aware.

Useful? React with 👍 / 👎.

Comment on lines +3016 to +3018
if let microphoneSelectionMode = payload.microphoneSelectionMode {
self.microphoneSelectionMode = microphoneSelectionMode
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset missing microphone mode to system on restore

When restoring a backup from before this field existed while the current install is in manual mode, the if let leaves microphoneSelectionMode unchanged. Those older backups came from builds where audio selection always followed macOS defaults, so after restore future recordings can keep forcing the restored PreferredInputDeviceUID as a manual mic instead of following the legacy system-mode behavior. Treat nil as .system (or remove the key) during restore.

Useful? React with 👍 / 👎.

@grohith327

Copy link
Copy Markdown
Collaborator Author

@altic-dev Good to review, tested with the new audio optimized pipeline

@altic-dev

Copy link
Copy Markdown
Owner

@greptileai review

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a MicrophoneSelectionMode enum (.system / .manual) to replace the previously inert syncAudioDevicesWithSystem stub, adds a new MicrophonePreferenceCoordinator that centralises preferred-device enforcement with retry logic, and surfaces both a Settings-panel toggle and a menu-bar item for switching between modes.

  • MicrophonePreferenceCoordinator — new testable coordinator with AudioDeviceManaging protocol injection; handles enforcement, fallback, and hardware-change stabilisation.
  • SettingsStore.setMicrophoneSelectionMode — atomically transitions modes, saves the pre-manual system UID for restoration, and returns the UID to restore on .system re-entry.
  • One confirmed desyncContentView.microphoneSelectionMode is a @State initialised once from SettingsStore; changes made via the menu bar are not reflected in the binding passed to SettingsView, causing onChange(of: inputDevices) to branch on the wrong mode after a menu-bar toggle.

Confidence Score: 4/5

Safe to merge after addressing the mode-binding desync between the menu bar and SettingsView.

The menu bar can change SettingsStore.microphoneSelectionMode independently of the @State copy held by ContentView. After such a toggle, SettingsView.onChange(of: inputDevices) branches on the stale local value, which can silently reset selectedInputUID to the system default even when the user just enabled manual mode. The coordinator design, SettingsStore transitions, recording-path device resolution, and test coverage are all solid.

Sources/Fluid/ContentView.swift — the microphoneSelectionMode @State needs to be kept in sync with SettingsStore when the mode is changed via the menu bar.

Reviews (6): Last reviewed commit: "fix for preferred device being unavailab..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a MicrophoneSelectionMode enum (.system / .manual) to replace the always-true syncAudioDevicesWithSystem flag, giving users explicit control over whether FluidVoice follows the macOS default input or locks to a preferred microphone. A new MicrophonePreferenceCoordinator service centralizes enforcement of the preferred device — re-asserting it after hardware topology changes — and is injected via a lazy property on AppServices.

  • New coordinator: MicrophonePreferenceCoordinator provides enforcePreferredInput, inputDeviceForCapture, and a debounced stabilizePreferredInputAfterHardwareChange with a progressive retry schedule; a AudioDeviceManaging protocol makes it fully unit-testable via FakeAudioDeviceManager.
  • Settings & menu bar: a toggle is added to both the Settings panel and the menu bar microphone submenu; SettingsStore.setMicrophoneSelectionMode snapshots the system UID before entering manual mode so it can be restored on toggle-off.
  • One defect identified: the hardware-change handler in ContentView sets selectedInputUID to the stored preferred UID without verifying the device is still in the refreshed device list, leaving the picker blank when the preferred device disconnects.

Confidence Score: 3/5

The core recording path correctly delegates device resolution to MicrophonePreferenceCoordinator, which handles device unavailability safely. However, the ContentView hardware-change handler leaves the picker UI in a broken blank state when a preferred device disconnects in manual mode.

The device-availability check dropped from the ContentView hardware-change .manual branch is a real UI regression: after the preferred device is unplugged, the Picker renders no matching item while the stored UID persists, and neither the ContentView handler nor the SettingsView .onChange(of: inputDevices) recovers it. The actual recording path falls back correctly, but the UI state is wrong and will mislead users.

Sources/Fluid/ContentView.swift — hardware-change handler for .manual mode needs an availability check with a system-default fallback.

Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
Sources/Fluid/ContentView.swift:365-368
**Preferred-device unavailability not handled after hardware change**

In `.manual` mode the hardware-change handler now sets `selectedInputUID` to the stored `preferredInputDeviceUID` unconditionally, even when the device was just disconnected. Because `refreshDevices()` runs first, `inputDevices` is already up-to-date, so the picker immediately shows a UID that is not in the list — the selection renders blank/missing. The old implementation guarded against this with an availability check and fell back to the system default, which needs to be restored here. Note: `MicrophonePreferenceCoordinator.inputDeviceForCapture()` does fall back correctly for the actual recording path, but the UI state is left stale.

```suggestion
                case .manual:
                    if let prefIn = SettingsStore.shared.preferredInputDeviceUID,
                       self.inputDevices.contains(where: { $0.uid == prefIn })
                    {
                        self.selectedInputUID = prefIn
                    } else if let sysIn = AudioDevice.getDefaultInputDevice()?.uid {
                        self.selectedInputUID = sysIn
                    }
```

### Issue 2 of 3
Sources/Fluid/Services/AppServices.swift:71-79
**Manual lazy-init pattern is not thread-safe; prefer `lazy var`**

The same manual double-check pattern is used for `_asr`, so it's consistent with the existing style, but `MicrophonePreferenceCoordinator` is `@MainActor`-isolated and is accessed from multiple call sites. Swift's `lazy var` is automatically initialized on first access and is thread-safe for value-type initialization, making intent clearer and removing the possibility of a race if the surrounding class ever stops being exclusively main-actor-accessed.

```suggestion
    lazy var microphonePreferenceCoordinator: MicrophonePreferenceCoordinator = MicrophonePreferenceCoordinator()
```

### Issue 3 of 3
Sources/Fluid/Services/MenuBarManager.swift:621-647
**Duplicated mode-toggle logic diverges from SettingsView**

`MenuBarManager.toggleMicrophoneSelectionMode` and `SettingsView.updateMicrophoneSelectionMode` implement the same "switch mode and seed the manual preference" flow independently. The key difference: the menu-bar path checks `SettingsStore.shared.preferredInputDeviceUID` to decide whether to seed a default, while the settings-view path checks the local UI binding `selectedInputUID`. This means flipping the toggle from the menu bar when `preferredInputDeviceUID` already holds a value will skip seeding (correct), but if the state somehow diverges the two call-sites will behave differently. Extracting the shared transition logic into `SettingsStore.setMicrophoneSelectionMode` (or a dedicated coordinator method) would remove this maintenance risk.

Reviews (2): Last reviewed commit: "centralize microhpone input irrespective..." | Re-trigger Greptile

Comment thread Sources/Fluid/ContentView.swift Outdated
Comment thread Sources/Fluid/Services/AppServices.swift
Comment thread Sources/Fluid/Services/MenuBarManager.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97a8530813

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


do {
if SettingsStore.shared.microphoneSelectionMode == .manual {
AppServices.shared.microphonePreferenceCoordinator.enforcePreferredInput(reason: "recording start")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not reassert manual mics as the system default

When manual mode is enabled and the saved preferred mic differs from the current macOS default, this call routes through MicrophonePreferenceCoordinator.enforcePreferredInput(), which calls setDefaultInputDevice before capture starts. That makes a FluidVoice-only/manual selection overwrite the user's global macOS input device, and it can leave other apps on that mic even if the recording start later fails; the manual path should capture from the preferred device without changing the system default, or limit/restore any workaround to the backend path that truly needs it.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c718d8de92

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +682 to +686
let restoredSystemInputUID = SettingsStore.shared.setMicrophoneSelectionMode(
nextMode,
currentSystemInputUID: currentSystemInputUID,
availableInputUIDs: availableInputUIDs
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Settings state in sync with menu microphone mode

When the user toggles microphone mode from the menu bar, this updates SettingsStore and refreshes only the menu, but ContentView keeps a separate @State microphoneSelectionMode that is passed into SettingsView and is only refreshed on backup restore. In the common flow of changing the mode from the menu and then opening Preferences, the Settings toggle/info can still reflect the old mode until relaunch, so the UI can present and act on stale state. Drive Settings directly from SettingsStore or notify/synchronize the ContentView state here.

Useful? React with 👍 / 👎.

@grohith327
grohith327 force-pushed the allow-default-audio-input branch from c718d8d to 07e7b6c Compare July 16, 2026 02:57
@grohith327

Copy link
Copy Markdown
Collaborator Author

@greptileai review latest changes

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 07e7b6c986

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +391 to +393
case .manual:
// The refreshed device list drives the displayed selection.
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the available manual mic after hardware changes

When manual mode is active and Settings is not the current detail view, this branch leaves selectedInputUID unchanged after refreshDevices(). If the saved manual microphone was previously disconnected while Settings fell back to the default, then the preferred mic reconnects while Preferences is closed, reopening Settings keeps showing the still-valid fallback instead of the saved preferred UID even though capture resolves preferredInputDeviceUID; update the manual path when the refreshed device list contains the saved preference.

Useful? React with 👍 / 👎.

@grohith327
grohith327 merged commit 2c8a954 into main Jul 18, 2026
5 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.

[✨ FEATURE] Set default microphone not matter what

2 participants