How to Select an Audio Input Device in Fluid Voice: A Complete Technical Guide
Fluid Voice selects an audio input device by enumerating Core Audio devices at startup, binding the user's choice to a persisted UID in the UI, and applying that selection through AudioCaptureCoordinator when capture begins.
To select an audio input device in Fluid Voice, you interact with the AudioDeviceManager which wraps macOS Core Audio functions, choose from the picker in the audio preferences or command mode UI, and have the AudioCaptureCoordinator reconcile your selection against available hardware before starting capture. This article walks through the complete implementation in the altic-dev/FluidVoice repository.
How Device Enumeration Works in Fluid Voice
Fluid Voice discovers microphones through a thin C wrapper around macOS Core Audio. The AudioDeviceManager class exposes this functionality to Swift via the AudioDeviceManaging protocol.
In Sources/Fluid/AudioDeviceManaging.swift, the manager calls into Sources/CoreAudioCaptureSupport/CoreAudioCaptureSupport.c to enumerate hardware:
// CoreAudioCaptureSupport.c – low-level device enumeration
FVStatus fv_core_audio_capture_list_input_devices(
AudioDeviceList **outDevices,
size_t *outCount
);
This C function queries kAudioHardwarePropertyDevices, filters for kAudioDevicePropertyStreamConfiguration inputs, and populates a list of AudioDevice structs. The Swift wrapper transforms this into a typed array:
// Sources/Fluid/AudioDeviceManaging.swift
let devices = AudioDeviceManager.shared.listInputDevices()
devices.forEach { device in
print("\(device.uid) – \(device.name) – transport: \(device.transportType)")
}
Each AudioDevice carries a UID (persistent identifier), name (human-readable), and transport type (USB, Bluetooth, built-in, etc.).
Binding the UI to Device Selection
The selection interface appears in Sources/Fluid/Views/CommandModeView.swift and related settings views. The picker binds directly to SettingsStore.shared.selectedInputDeviceUID:
// CommandModeView.swift – microphone selector UI
Menu {
ForEach(devices) { device in
Button(device.name) {
SettingsStore.shared.selectedInputDeviceUID = device.uid
}
}
} label: {
Text("Microphone")
}
When the user taps a device name, the UID is immediately written to UserDefaults via Sources/Fluid/SettingsStore.swift. This persistence ensures the selection survives app restarts.
Applying the Selected Device at Capture Time
The AudioCaptureCoordinator does not use the stored UID directly. Instead, it calls reconcileMicrophoneSelection(preferredUID:) to resolve the UID to a valid AudioObjectID. This pattern handles cases where the preferred device was unplugged:
// AudioCaptureCoordinator.swift – device resolution before capture
func reconcileMicrophoneSelection(preferredUID: String?) -> AudioDevice? {
guard let uid = preferredUID,
let device = AudioDeviceManager.shared.deviceMatching(uid: uid) else {
return AudioDeviceManager.shared.defaultInputDevice() // Fallback
}
return device
}
Once reconciled, the coordinator creates a capture session by calling the C layer:
// CoreAudioCaptureSupport.h – capture creation with explicit device
FVStatus fv_core_audio_capture_create(
AudioObjectID deviceID,
FVCoreAudioCapture **outCapture
);
The capture starts with fv_core_audio_capture_start(_:) and runs until explicitly stopped or the device disappears.
Handling Disconnected Devices with AudioHardwareObserver
Fluid Voice monitors hardware changes through AudioHardwareObserver. If the selected device is unplugged during a session, the observer notifies the coordinator, which automatically falls back to the system default input device.
This resilience is tested in Tests/FluidDictationIntegrationTests/HotkeyShortcutTests.swift around line 1224, where reconcileMicrophoneSelection is exercised with missing and present UIDs.
Complete Flow Summary
- Enumeration –
AudioDeviceManager.listInputDevices()queries Core Audio viafv_core_audio_capture_list_input_devices(). - Selection – User picks from the
MenuinCommandModeView, settingSettingsStore.shared.selectedInputDeviceUID. - Persistence – The UID is saved to
UserDefaultsimmediately. - Reconciliation –
AudioCaptureCoordinatorcallsreconcileMicrophoneSelection(preferredUID:)to validate the UID. - Capture – Valid device ID passed to
fv_core_audio_capture_create(_:outCapture:)to start microphone input.
Summary
- Fluid Voice wraps macOS Core Audio in a C layer (
CoreAudioCaptureSupport.c) with Swift bindings (AudioDeviceManaging.swift). - Device UIDs are the stable identifiers used for selection and persistence.
- The
AudioCaptureCoordinatorreconciles stored preferences against current hardware before each capture session. - Automatic fallback to the default device occurs if the preferred microphone becomes unavailable.
Frequently Asked Questions
Where does Fluid Voice store the selected microphone setting?
Fluid Voice stores the selected device UID in UserDefaults through SettingsStore.shared.selectedInputDeviceUID defined in Sources/Fluid/SettingsStore.swift. This persists the choice across app launches without requiring manual configuration each time.
What happens if I unplug my selected microphone while Fluid Voice is running?
The AudioHardwareObserver detects the device removal and notifies the AudioCaptureCoordinator, which falls back to the system default input device. The UI does not automatically update to show this change, but capture continues uninterrupted.
Can I programmatically list available input devices in Fluid Voice?
Yes. Import the Fluid module and use AudioDeviceManager.shared.listInputDevices() to retrieve an array of AudioDevice structs containing UID, name, and transport type. For matching a specific UID, use deviceMatching(uid:).
How does Fluid Voice handle multiple microphones with identical names?
Fluid Voice distinguishes devices by UID, not display name. The UID is a unique hardware identifier provided by Core Audio, ensuring that two microphones of the same model do not collide in the selection logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →