diff --git a/AudioPriorityBar/AudioPriorityBarApp.swift b/AudioPriorityBar/AudioPriorityBarApp.swift index 3f455e9..6498750 100644 --- a/AudioPriorityBar/AudioPriorityBarApp.swift +++ b/AudioPriorityBar/AudioPriorityBarApp.swift @@ -158,6 +158,9 @@ class AudioManager: ObservableObject { } } + /// Tracks the last output device ID that was set by the app (not externally) + private var lastAppSetOutputId: AudioObjectID? + init() { currentMode = priorityManager.currentMode isCustomMode = priorityManager.isCustomMode @@ -167,10 +170,13 @@ class AudioManager: ObservableObject { refreshMuteStatus() setupDeviceChangeListener() setupMuteVolumeListener() + setupDefaultOutputChangeListener() if !isCustomMode { applyHighestPriorityInput() applyHighestPriorityOutput() } + // Track initial output device + lastAppSetOutputId = currentOutputId } private func setupMuteVolumeListener() { @@ -181,11 +187,58 @@ class AudioManager: ObservableObject { } } + private func setupDefaultOutputChangeListener() { + deviceService.onDefaultOutputDeviceChanged = { [weak self] in + Task { @MainActor in + self?.handleDefaultOutputDeviceChange() + } + } + } + private func handleMuteOrVolumeChange() { refreshMuteStatus() refreshVolume() } + /// Handles when the default output device changes (e.g., via System Preferences or menu bar). + /// If the user manually switched to a device in a different category, switch modes to follow. + private func handleDefaultOutputDeviceChange() { + guard !isCustomMode else { return } + + let newDefaultId = deviceService.getCurrentDefaultDevice(type: .output) + + // If this change was triggered by our own app, ignore it + if newDefaultId == lastAppSetOutputId { + return + } + + // Update our tracked output ID + currentOutputId = newDefaultId + + // Find which device was selected + guard let newDeviceId = newDefaultId else { return } + + // Check if the new device is in speakers or headphones + if speakerDevices.contains(where: { $0.id == newDeviceId }) { + // User switched to a speaker - change to speaker mode + if currentMode != .speaker { + currentMode = .speaker + priorityManager.currentMode = .speaker + lastAppSetOutputId = newDeviceId + } + } else if headphoneDevices.contains(where: { $0.id == newDeviceId }) { + // User switched to headphones - change to headphone mode + if currentMode != .headphone { + currentMode = .headphone + priorityManager.currentMode = .headphone + lastAppSetOutputId = newDeviceId + } + } + + refreshMuteStatus() + refreshVolume() + } + func refreshDevices() { let allConnectedDevices = deviceService.getDevices() connectedDeviceUIDs = Set(allConnectedDevices.map { $0.uid }) @@ -393,6 +446,7 @@ class AudioManager: ObservableObject { private func applyOutputDevice(_ device: AudioDevice) { deviceService.setDefaultDevice(device.id, type: .output) currentOutputId = device.id + lastAppSetOutputId = device.id } private func applyHighestPriorityInput() { diff --git a/AudioPriorityBar/Services/AudioDeviceService.swift b/AudioPriorityBar/Services/AudioDeviceService.swift index 43431fe..fb519cc 100644 --- a/AudioPriorityBar/Services/AudioDeviceService.swift +++ b/AudioPriorityBar/Services/AudioDeviceService.swift @@ -5,8 +5,10 @@ import AudioToolbox class AudioDeviceService { var onDevicesChanged: (() -> Void)? var onMuteOrVolumeChanged: (() -> Void)? + var onDefaultOutputDeviceChanged: (() -> Void)? private var listenerBlock: AudioObjectPropertyListenerBlock? + private var defaultOutputListenerBlock: AudioObjectPropertyListenerBlock? private var muteVolumeListenerBlock: AudioObjectPropertyListenerBlock? private var monitoredDeviceIds: Set = [] @@ -261,6 +263,12 @@ class AudioDeviceService { listenerBlock! ) + // Use a separate listener for default output device changes + // This allows us to detect manual switches via System Preferences / menu bar + defaultOutputListenerBlock = { [weak self] _, _ in + self?.onDefaultOutputDeviceChanged?() + } + var outputDefaultAddress = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, @@ -270,7 +278,7 @@ class AudioDeviceService { AudioObjectID(kAudioObjectSystemObject), &outputDefaultAddress, DispatchQueue.main, - listenerBlock! + defaultOutputListenerBlock! ) // Initial setup of mute/volume listeners @@ -417,17 +425,20 @@ class AudioDeviceService { block ) - var outputDefaultAddress = AudioObjectPropertyAddress( - mSelector: kAudioHardwarePropertyDefaultOutputDevice, - mScope: kAudioObjectPropertyScopeGlobal, - mElement: kAudioObjectPropertyElementMain - ) - AudioObjectRemovePropertyListenerBlock( - AudioObjectID(kAudioObjectSystemObject), - &outputDefaultAddress, - DispatchQueue.main, - block - ) + if let outputBlock = defaultOutputListenerBlock { + var outputDefaultAddress = AudioObjectPropertyAddress( + mSelector: kAudioHardwarePropertyDefaultOutputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain + ) + AudioObjectRemovePropertyListenerBlock( + AudioObjectID(kAudioObjectSystemObject), + &outputDefaultAddress, + DispatchQueue.main, + outputBlock + ) + defaultOutputListenerBlock = nil + } listenerBlock = nil }