Build SoundOrder, a native macOS menu-bar utility that automatically selects input and output devices from independent priority lists. The app must react to device availability, ignore explicitly disabled devices, retain disconnected devices in settings, and support temporary manual overrides.
Per-application volume is intentionally deferred. It requires a separate real-time audio engine based on Core Audio process taps and aggregate devices. The MVP architecture should leave room for that feature without introducing its permissions or failure modes now.
The app targets the current macOS version and uses Swift 6, SwiftUI, AppKit, Core Audio, Service Management, and OSLog.
CoreAudioDeviceServicewraps Core Audio HAL operations: device enumeration, stable UID and name lookup, input/output capability detection, property listeners, current-default lookup, and default-device changes.DevicePolicyEngineis a pure component that chooses input and output devices from snapshots, preferences, disabled UIDs, and temporary overrides.PreferencesStorepersists versioned Codable preferences inUserDefaults.AppModelcoordinates services and publishes menu and settings state on the main actor.- SwiftUI provides a
MenuBarExtracontrol surface and a settings window.
The app runs as an accessory application without a Dock icon. It does not need a privileged helper, virtual audio device, system extension, or audio-capture permission for the MVP.
Input and output use separate ordered priority lists. A policy evaluation picks the first connected, enabled device that supports the required direction. Built-in devices are kept as the final fallback. If no valid candidate exists, the current default remains unchanged and the app reports an error.
On first launch, current defaults rank first, other discovered external devices follow, and built-in devices rank last. A newly discovered external device is inserted immediately above the built-in fallback. Devices are identified by their stable Core Audio UID rather than display name.
Output changes update both the normal default output and the system-sounds output. Input changes update the default input. Writes are idempotent.
Selecting a device from the menu creates a temporary override for that direction. The override ends when the selected device disconnects or the user resumes automatic selection. If System Settings changes a default while automatic mode is active, policy is re-applied. While an override is active, a known enabled external selection becomes the new override.
Disabled devices are excluded before ranking and hidden everywhere except a dedicated Disabled section in settings.
At launch, the app:
- Loads preferences.
- Enumerates Core Audio devices.
- Merges the snapshot with known-device records.
- Installs hardware-list and default-device listeners.
- Evaluates input and output policy.
Hardware-list changes are debounced before a fresh snapshot and reevaluation. Core Audio callbacks run on a private serial queue. UI state is updated on the main actor.
Core Audio failures are surfaced in the menu with a retry action and logged
with Logger. The app does not silently choose an arbitrary device or report a
successful switch after an API failure.
The menu-bar panel contains:
- An Output section with the active device, automatic/override state, and enabled connected output devices.
- An Input section with the equivalent controls for input devices.
- A Resume Automatic Selection action whenever an override exists.
- Settings and Quit actions.
- A concise inline error state when switching or enumeration fails.
Settings contains independent drag-to-reorder input and output lists. Each row shows its connection state and offers a Disable action. Disabled devices appear in a separate restorable section.
Settings also includes Launch at Login using SMAppService.
DevicePolicyEngine unit tests cover:
- Priority ordering.
- Disconnected and disabled devices.
- Direction capabilities.
- Independent input and output selection.
- Temporary overrides.
- Override disconnection.
- Built-in fallback.
- Empty snapshots.
AppModel tests use a fake audio service to cover event-driven reevaluation,
preference merging, override behavior, and prevention of redundant writes.
The HAL wrapper remains intentionally thin. A diagnostic executable path and manual hardware smoke tests cover real device enumeration, connection events, and input/output switching.
Current macOS versions expose Core Audio process taps, so a later release can add per-application gain without a legacy virtual audio driver. That phase will need system-audio-capture consent, per-process taps, a private aggregate device, a real-time mixer, output-device hot-swap handling, and strict fail-open behavior. It remains outside the MVP.