Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions AudioPriorityBar/AudioPriorityBarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -167,10 +170,13 @@ class AudioManager: ObservableObject {
refreshMuteStatus()
setupDeviceChangeListener()
setupMuteVolumeListener()
setupDefaultOutputChangeListener()
if !isCustomMode {
applyHighestPriorityInput()
applyHighestPriorityOutput()
}
// Track initial output device
lastAppSetOutputId = currentOutputId
}

private func setupMuteVolumeListener() {
Expand All @@ -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 })
Expand Down Expand Up @@ -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() {
Expand Down
35 changes: 23 additions & 12 deletions AudioPriorityBar/Services/AudioDeviceService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AudioObjectID> = []

Expand Down Expand Up @@ -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,
Expand All @@ -270,7 +278,7 @@ class AudioDeviceService {
AudioObjectID(kAudioObjectSystemObject),
&outputDefaultAddress,
DispatchQueue.main,
listenerBlock!
defaultOutputListenerBlock!
)

// Initial setup of mute/volume listeners
Expand Down Expand Up @@ -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
}
Expand Down