feat(audio): add system default audio device selection to AudioDeviceSelector#54
feat(audio): add system default audio device selection to AudioDeviceSelector#54BegoniaHe wants to merge 1 commit intorvarunrathod:mainfrom
Conversation
…Selector - add system default audio device selection to AudioDeviceSelector according to ISSUE rvarunrathod#48
There was a problem hiding this comment.
Pull request overview
Adds a “System Default” audio output option so HiFidelity can follow the OS-selected output device (Issue #48), improving behavior when devices (e.g., USB DACs) disconnect/reconnect or when macOS switches outputs automatically.
Changes:
- Add a “System Default” row to the audio output device popover and route selection through a new
switchToSystemDefault()action. - Extend
DACManagerto track the system default device and optionally follow it, including a CoreAudio listener for default-output changes. - Refactor device switching logic to separate “follow system default” mode from direct device selection.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| HiFidelity/Views/Playback/AudioDeviceSelector.swift | Adds a selectable “System Default” row and updates selection logic/UI to reflect follow-default mode. |
| HiFidelity/Core/Audio/DACManager.swift | Introduces follow-default state + default-device tracking and listens for macOS default output changes to auto-switch when enabled. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| nonisolated deinit { | ||
| Task { @MainActor [weak self] in | ||
| self?.removeDeviceChangeListener() | ||
| self?.removeDefaultDeviceChangeListener() | ||
| } |
There was a problem hiding this comment.
The new nonisolated deinit schedules listener removal asynchronously and captures self weakly. By the time the Task runs, self will often already be deallocated, so the listeners never get removed and CoreAudio callbacks can later dereference a freed clientData pointer (use-after-free / crash). Also, nonisolated is only valid for actor-isolated/global-actor-isolated types; if DACManager isn’t @MainActor, this likely won’t compile. Remove both listeners synchronously in deinit (no Task, no weak self), and if you truly need main-actor isolation, mark the type @MainActor and keep teardown synchronous within deinit.
| nonisolated deinit { | |
| Task { @MainActor [weak self] in | |
| self?.removeDeviceChangeListener() | |
| self?.removeDefaultDeviceChangeListener() | |
| } | |
| deinit { | |
| removeDeviceChangeListener() | |
| removeDefaultDeviceChangeListener() |
|
|
||
| /* | ||
| Text("•") | ||
| .font(AppFonts.captionMedium) | ||
| .foregroundColor(.secondary.opacity(0.5)) | ||
|
|
||
| Text("\(Int(defaultDevice.sampleRate)) Hz") | ||
| .font(AppFonts.captionMedium) | ||
| .foregroundColor(.secondary) | ||
|
|
||
| Text("•") | ||
| .font(AppFonts.captionMedium) | ||
| .foregroundColor(.secondary.opacity(0.5)) | ||
|
|
||
| Text(channelDescription(defaultDevice.channels)) | ||
| .font(AppFonts.captionMedium) | ||
| .foregroundColor(.secondary) | ||
| */ |
There was a problem hiding this comment.
There’s a large commented-out UI block inside the System Default row (sample rate / channels). Leaving dead UI code in-line makes this view harder to maintain; either remove it or reintroduce it behind a clear condition/flag so the intent is preserved without commented code.
| /* | |
| Text("•") | |
| .font(AppFonts.captionMedium) | |
| .foregroundColor(.secondary.opacity(0.5)) | |
| Text("\(Int(defaultDevice.sampleRate)) Hz") | |
| .font(AppFonts.captionMedium) | |
| .foregroundColor(.secondary) | |
| Text("•") | |
| .font(AppFonts.captionMedium) | |
| .foregroundColor(.secondary.opacity(0.5)) | |
| Text(channelDescription(defaultDevice.channels)) | |
| .font(AppFonts.captionMedium) | |
| .foregroundColor(.secondary) | |
| */ |
There was a problem hiding this comment.
I'm not sure if the "System Default" row should display technical details (sample rate + channel configuration)
Summary
This PR implements a "System Default" audio output option that automatically follows macOS system audio device changes, addressing issue #48.
Problem Statement
Previously, when users had external DAC/amp devices that aren't USB-powered, HiFidelity required manual reselection of the audio device after sleep/wake cycles, even though macOS automatically switched back to the device. This created friction in the user experience, especially for users who frequently pause playback by closing their laptop lid.
Solution
Core Changes
1. Audio Device Management (
DACManager.swift)systemDefaultDeviceIDandsystemDefaultDeviceproperties to track the current system default output deviceAudioObjectPropertyListenerforkAudioHardwarePropertyDefaultOutputDeviceto detect system audio routing changes2. User Interface (
AudioDeviceSelector.swift)Screenshot
Related Issue
Closes #48
Notes
Questions for Reviewers
Should the "System Default" row display technical details (sample rate, stereo/mono) like other device entries? Currently opted for a cleaner look since:
Feedback welcome!