-
-
Notifications
You must be signed in to change notification settings - Fork 469
Add support for choosing custom audio input #531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,6 +143,24 @@ final class SettingsStore: ObservableObject { | |
| } | ||
| } | ||
|
|
||
| enum MicrophoneSelectionMode: String, Codable, CaseIterable, Identifiable { | ||
| case system | ||
| case manual | ||
|
|
||
| var id: String { | ||
| self.rawValue | ||
| } | ||
|
|
||
| var displayName: String { | ||
| switch self { | ||
| case .system: | ||
| return "Use macOS Default" | ||
| case .manual: | ||
| return "Use Preferred Microphone" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| enum DictationPromptSelection: Equatable { | ||
| case off, `default`, privateAI | ||
| case profile(String) | ||
|
|
@@ -1664,20 +1682,63 @@ final class SettingsStore: ObservableObject { | |
| set { self.defaults.set(newValue, forKey: Keys.preferredOutputDeviceUID) } | ||
| } | ||
|
|
||
| /// When enabled, changing audio devices in FluidVoice will also update macOS system audio settings. | ||
| /// ALWAYS TRUE: Independent mode removed due to CoreAudio aggregate device limitations (OSStatus -10851) | ||
| var syncAudioDevicesWithSystem: Bool { | ||
| var microphoneSelectionMode: MicrophoneSelectionMode { | ||
| get { | ||
| // Always return true - independent mode doesn't work for Bluetooth/aggregate devices | ||
| return true | ||
| if let raw = self.defaults.string(forKey: Keys.microphoneSelectionMode), | ||
| let mode = MicrophoneSelectionMode(rawValue: raw) | ||
| { | ||
| return mode | ||
| } | ||
|
|
||
| return .system | ||
| } | ||
| set { | ||
| // No-op: sync mode is always enabled | ||
| // Kept for backward compatibility but value is ignored | ||
| _ = newValue | ||
| objectWillChange.send() | ||
| self.defaults.set(newValue.rawValue, forKey: Keys.microphoneSelectionMode) | ||
| } | ||
| } | ||
|
|
||
| func recordInputDeviceSelection(_ uid: String) { | ||
| guard uid.isEmpty == false else { return } | ||
| guard self.microphoneSelectionMode == .manual else { return } | ||
|
|
||
| self.preferredInputDeviceUID = uid | ||
| } | ||
|
|
||
| func shouldSyncInputSelectionToSystemDefault() -> Bool { | ||
| self.microphoneSelectionMode == .system | ||
| } | ||
|
|
||
| @discardableResult | ||
| func setMicrophoneSelectionMode( | ||
| _ mode: MicrophoneSelectionMode, | ||
| currentSystemInputUID: String?, | ||
| availableInputUIDs: Set<String> | ||
| ) -> String? { | ||
| let previousMode = self.microphoneSelectionMode | ||
|
|
||
| if previousMode == .system, | ||
| mode == .manual, | ||
| let currentSystemInputUID, | ||
| currentSystemInputUID.isEmpty == false | ||
| { | ||
| self.defaults.set(currentSystemInputUID, forKey: Keys.systemInputDeviceUIDBeforeManual) | ||
| } | ||
|
|
||
| self.microphoneSelectionMode = mode | ||
|
|
||
| guard mode == .system else { return nil } | ||
|
|
||
| if let previousSystemInputUID = self.defaults.string(forKey: Keys.systemInputDeviceUIDBeforeManual), | ||
| previousSystemInputUID.isEmpty == false, | ||
| availableInputUIDs.contains(previousSystemInputUID) | ||
| { | ||
| return previousSystemInputUID | ||
| } | ||
|
|
||
| return currentSystemInputUID | ||
| } | ||
|
|
||
| var visualizerNoiseThreshold: Double { | ||
| get { | ||
| let value = self.defaults.double(forKey: Keys.visualizerNoiseThreshold) | ||
|
|
@@ -2854,6 +2915,7 @@ final class SettingsStore: ObservableObject { | |
| textInsertionMode: self.textInsertionMode, | ||
| preferredInputDeviceUID: self.preferredInputDeviceUID, | ||
| preferredOutputDeviceUID: self.preferredOutputDeviceUID, | ||
| microphoneSelectionMode: self.microphoneSelectionMode, | ||
| visualizerNoiseThreshold: self.visualizerNoiseThreshold, | ||
| overlayPosition: self.overlayPosition, | ||
| overlayBottomOffset: self.overlayBottomOffset, | ||
|
|
@@ -2959,6 +3021,9 @@ final class SettingsStore: ObservableObject { | |
| self.textInsertionMode = payload.textInsertionMode | ||
| self.preferredInputDeviceUID = payload.preferredInputDeviceUID | ||
| self.preferredOutputDeviceUID = payload.preferredOutputDeviceUID | ||
| if let microphoneSelectionMode = payload.microphoneSelectionMode { | ||
| self.microphoneSelectionMode = microphoneSelectionMode | ||
| } | ||
|
Comment on lines
+3024
to
+3026
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When restoring a backup from before this field existed while the current install is in manual mode, the Useful? React with 👍 / 👎. |
||
| self.visualizerNoiseThreshold = payload.visualizerNoiseThreshold | ||
| self.overlayPosition = payload.overlayPosition | ||
| self.overlayBottomOffset = payload.overlayBottomOffset | ||
|
|
@@ -4460,7 +4525,8 @@ private extension SettingsStore { | |
| static let primaryDictationShortcutsKey = "PrimaryDictationShortcuts" | ||
| static let preferredInputDeviceUID = "PreferredInputDeviceUID" | ||
| static let preferredOutputDeviceUID = "PreferredOutputDeviceUID" | ||
| static let syncAudioDevicesWithSystem = "SyncAudioDevicesWithSystem" | ||
| static let microphoneSelectionMode = "MicrophoneSelectionMode" | ||
| static let systemInputDeviceUIDBeforeManual = "SystemInputDeviceUIDBeforeManual" | ||
| static let visualizerNoiseThreshold = "VisualizerNoiseThreshold" | ||
| static let launchAtStartup = "LaunchAtStartup" | ||
| static let showInDock = "ShowInDock" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
.manualmode the hardware-change handler now setsselectedInputUIDto the storedpreferredInputDeviceUIDunconditionally, even when the device was just disconnected. BecauserefreshDevices()runs first,inputDevicesis 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.Prompt To Fix With AI