From e51e57f3b16f5fd0773231a4edf81728cd322f3d Mon Sep 17 00:00:00 2001 From: Beingpax Date: Wed, 4 Jun 2025 17:22:53 +0545 Subject: [PATCH] Refactor(AudioDeviceManager): Use UID for custom device persistence, cleanup UserDefaults logic for prioritized mode, and remove all comments and legacy keys --- VoiceInk/Services/AudioDeviceManager.swift | 86 +++++++++------------- 1 file changed, 34 insertions(+), 52 deletions(-) diff --git a/VoiceInk/Services/AudioDeviceManager.swift b/VoiceInk/Services/AudioDeviceManager.swift index 4e28d75..67f50c8 100644 --- a/VoiceInk/Services/AudioDeviceManager.swift +++ b/VoiceInk/Services/AudioDeviceManager.swift @@ -4,7 +4,7 @@ import AVFoundation import os struct PrioritizedDevice: Codable, Identifiable { - let id: String // Device UID + let id: String let name: String let priority: Int } @@ -21,11 +21,15 @@ class AudioDeviceManager: ObservableObject { @Published var selectedDeviceID: AudioDeviceID? @Published var inputMode: AudioInputMode = .systemDefault @Published var prioritizedDevices: [PrioritizedDevice] = [] - var fallbackDeviceID: AudioDeviceID? + var fallbackDeviceID: AudioDeviceID? var isRecordingActive: Bool = false static let shared = AudioDeviceManager() + + private static let audioInputModeKey = "audioInputMode" + private static let selectedAudioDeviceUIDKey = "selectedAudioDeviceUID" + private static let prioritizedDevicesKey = "prioritizedDevices" init() { setupFallbackDevice() @@ -34,17 +38,15 @@ class AudioDeviceManager: ObservableObject { self?.initializeSelectedDevice() } - // Load saved input mode - if let savedMode = UserDefaults.standard.string(forKey: "audioInputMode"), + if let savedMode = UserDefaults.standard.string(forKey: AudioDeviceManager.audioInputModeKey), let mode = AudioInputMode(rawValue: savedMode) { inputMode = mode } - // Setup device change notifications setupDeviceChangeNotifications() } - func setupFallbackDevice() { + func setupFallbackDevice() { let deviceID: AudioDeviceID? = getDeviceProperty( deviceID: AudioObjectID(kAudioObjectSystemObject), selector: kAudioHardwarePropertyDefaultInputDevice @@ -66,17 +68,16 @@ class AudioDeviceManager: ObservableObject { return } - // Try to load saved device - if let savedID = UserDefaults.standard.object(forKey: "selectedAudioDeviceID") as? AudioDeviceID { - // Verify the saved device still exists and is valid - if isDeviceAvailable(savedID) { - selectedDeviceID = savedID - logger.info("Loaded saved device ID: \(savedID)") - if let name = getDeviceName(deviceID: savedID) { + if let savedUID = UserDefaults.standard.string(forKey: AudioDeviceManager.selectedAudioDeviceUIDKey) { + if let device = availableDevices.first(where: { $0.uid == savedUID }) { + selectedDeviceID = device.id + logger.info("Loaded saved device UID: \(savedUID), mapped to ID: \(device.id)") + if let name = getDeviceName(deviceID: device.id) { logger.info("Using saved device: \(name)") } } else { - logger.warning("Saved device ID \(savedID) is no longer available") + logger.warning("Saved device UID \(savedUID) is no longer available") + UserDefaults.standard.removeObject(forKey: AudioDeviceManager.selectedAudioDeviceUIDKey) fallbackToDefaultDevice() } } else { @@ -89,9 +90,6 @@ class AudioDeviceManager: ObservableObject { } private func fallbackToDefaultDevice() { - // Instead of just setting a fallback device ID, explicitly switch to system default mode. - // selectInputMode(.systemDefault) will handle setting inputMode, - // clearing selectedDeviceID, and updating UserDefaults. selectInputMode(.systemDefault) logger.info("Switched to system default audio input mode due to fallback.") } @@ -149,7 +147,6 @@ class AudioDeviceManager: ObservableObject { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.availableDevices = devices.map { ($0.id, $0.uid, $0.name) } - // Verify current selection is still valid if let currentID = self.selectedDeviceID, !devices.contains(where: { $0.id == currentID }) { self.logger.warning("Currently selected device is no longer available") self.fallbackToDefaultDevice() @@ -212,29 +209,33 @@ class AudioDeviceManager: ObservableObject { logger.info("Selected device name: \(name)") } - if isDeviceAvailable(id) { + if let deviceToSelect = availableDevices.first(where: { $0.id == id }), let uid = deviceToSelect.uid { DispatchQueue.main.async { self.selectedDeviceID = id - UserDefaults.standard.set(id, forKey: "selectedAudioDeviceID") - self.logger.info("Device selection saved") + UserDefaults.standard.set(uid, forKey: AudioDeviceManager.selectedAudioDeviceUIDKey) + self.logger.info("Device selection saved with UID: \(uid)") self.notifyDeviceChange() } } else { - logger.error("Attempted to select unavailable device: \(id)") + logger.error("Attempted to select unavailable device or device with no UID: \(id)") fallbackToDefaultDevice() } } func selectInputMode(_ mode: AudioInputMode) { inputMode = mode - UserDefaults.standard.set(mode.rawValue, forKey: "audioInputMode") + UserDefaults.standard.set(mode.rawValue, forKey: AudioDeviceManager.audioInputModeKey) if mode == .systemDefault { selectedDeviceID = nil - UserDefaults.standard.removeObject(forKey: "selectedAudioDeviceID") + UserDefaults.standard.removeObject(forKey: AudioDeviceManager.selectedAudioDeviceUIDKey) } else if selectedDeviceID == nil { - if let firstDevice = availableDevices.first { - selectDevice(id: firstDevice.id) + if inputMode == .custom { + if let firstDevice = availableDevices.first { + selectDevice(id: firstDevice.id) + } + } else if inputMode == .prioritized { + selectHighestPriorityAvailableDevice() } } @@ -259,7 +260,7 @@ class AudioDeviceManager: ObservableObject { } private func loadPrioritizedDevices() { - if let data = UserDefaults.standard.data(forKey: "prioritizedDevices"), + if let data = UserDefaults.standard.data(forKey: AudioDeviceManager.prioritizedDevicesKey), let devices = try? JSONDecoder().decode([PrioritizedDevice].self, from: data) { prioritizedDevices = devices logger.info("Loaded \(devices.count) prioritized devices") @@ -268,7 +269,7 @@ class AudioDeviceManager: ObservableObject { func savePrioritizedDevices() { if let data = try? JSONEncoder().encode(prioritizedDevices) { - UserDefaults.standard.set(data, forKey: "prioritizedDevices") + UserDefaults.standard.set(data, forKey: AudioDeviceManager.prioritizedDevicesKey) logger.info("Saved \(self.prioritizedDevices.count) prioritized devices") } } @@ -285,7 +286,6 @@ class AudioDeviceManager: ObservableObject { let wasSelected = selectedDeviceID == availableDevices.first(where: { $0.uid == id })?.id prioritizedDevices.removeAll { $0.id == id } - // Reindex remaining devices to ensure continuous priority numbers let updatedDevices = prioritizedDevices.enumerated().map { index, device in PrioritizedDevice(id: device.id, name: device.name, priority: index) } @@ -293,7 +293,6 @@ class AudioDeviceManager: ObservableObject { prioritizedDevices = updatedDevices savePrioritizedDevices() - // If we removed the currently selected device, select the next best option if wasSelected && inputMode == .prioritized { selectHighestPriorityAvailableDevice() } @@ -311,28 +310,24 @@ class AudioDeviceManager: ObservableObject { } private func selectHighestPriorityAvailableDevice() { - // Sort by priority (lowest number = highest priority) let sortedDevices = prioritizedDevices.sorted { $0.priority < $1.priority } - // Try each device in priority order for device in sortedDevices { if let availableDevice = availableDevices.first(where: { $0.uid == device.id }) { selectedDeviceID = availableDevice.id logger.info("Selected prioritized device: \(device.name) (Priority: \(device.priority))") - // Actually set the device as the current input device do { try AudioDeviceConfiguration.setDefaultInputDevice(availableDevice.id) - UserDefaults.standard.set(availableDevice.id, forKey: "selectedAudioDeviceID") + self.notifyDeviceChange() } catch { logger.error("Failed to set prioritized device: \(error.localizedDescription)") - continue // Try next device if this one fails + continue } return } } - // If no prioritized device is available, fall back to default fallbackToDefaultDevice() } @@ -345,7 +340,6 @@ class AudioDeviceManager: ObservableObject { let systemObjectID = AudioObjectID(kAudioObjectSystemObject) - // Add listener for device changes let status = AudioObjectAddPropertyListener( systemObjectID, &address, @@ -371,21 +365,13 @@ class AudioDeviceManager: ObservableObject { loadAvailableDevices { [weak self] in guard let self = self else { return } - // If in prioritized mode, recheck the device selection if self.inputMode == .prioritized { self.selectHighestPriorityAvailableDevice() - } - // If in custom mode and selected device is no longer available, fallback - else if self.inputMode == .custom, - let currentID = self.selectedDeviceID, - !self.isDeviceAvailable(currentID) { + } else if self.inputMode == .custom, + let currentID = self.selectedDeviceID, + !self.isDeviceAvailable(currentID) { self.fallbackToDefaultDevice() } - - // Notify UI of changes - if !self.isRecordingActive { - NotificationCenter.default.post(name: NSNotification.Name("AudioDeviceChanged"), object: nil) - } } } @@ -396,7 +382,6 @@ class AudioDeviceManager: ObservableObject { } deinit { - // Remove the listener when the manager is deallocated var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, @@ -413,7 +398,6 @@ class AudioDeviceManager: ObservableObject { ) } - // MARK: - Helper Methods private func createPropertyAddress(selector: AudioObjectPropertySelector, scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal, element: AudioObjectPropertyElement = kAudioObjectPropertyElementMain) -> AudioObjectPropertyAddress { @@ -427,7 +411,6 @@ class AudioDeviceManager: ObservableObject { private func getDeviceProperty(deviceID: AudioDeviceID, selector: AudioObjectPropertySelector, scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal) -> T? { - // Skip invalid device IDs guard deviceID != 0 else { return nil } var address = createPropertyAddress(selector: selector, scope: scope) @@ -452,7 +435,6 @@ class AudioDeviceManager: ObservableObject { } private func notifyDeviceChange() { - // Only notify if recording is not active if !isRecordingActive { NotificationCenter.default.post(name: NSNotification.Name("AudioDeviceChanged"), object: nil) }