How Vorssaint Tracks Audio Output Routing and Per-App Volume Settings

Vorssaint utilizes a MixerRoutingSupport helper class that interfaces directly with macOS Core Audio to monitor default output devices, persist per-application volume fractions in UserDefaults, and apply real-time volume adjustments using AudioObjectSetPropertyData.

Vorssaint's approach to granular audio management centers on the open-source vorssaint/vorssaint-utils repository and its sophisticated handling of system-level audio APIs. By tracking audio output routing and per-app volume settings through a dedicated helper class, the application enables users to maintain independent volume levels for each application while seamlessly switching between output devices.

Core Architecture: The MixerRoutingSupport Helper

The entire audio routing system revolves around MixerRoutingSupport, a Swift helper defined in Sources/Vorssaint/UI/MenuPanel/MixerSection.swift. This class serves as the exclusive bridge between the application's UI layer and macOS Core Audio, handling three primary responsibilities:

  • Device Monitoring: Registers property listeners on kAudioHardwarePropertyDefaultOutputDevice to detect when users plug in headphones or switch to external displays.
  • Volume Persistence: Maintains a [String: Double] dictionary in UserDefaults that maps application identifiers to linear volume fractions (0.0 to 1.0).
  • System Integration: Translates UI percentages into Core Audio scalar values using AudioObjectGetPropertyData and AudioObjectSetPropertyData on kAudioDevicePropertyVolumeScalar.

The UI components, including MixerSection and MixerPercentNativeTextField, bind to this helper rather than accessing Core Audio directly, ensuring consistent behavior across the application.

Tracking Default Output Devices with Core Audio

Vorssaint implements live device tracking through a multi-step initialization and monitoring process:

  1. Initial Detection: On launch, MixerRoutingSupport.systemDefaultSelectionID queries the current default output device using AudioObjectGetPropertyData on the kAudioHardwarePropertyDefaultOutputDevice selector.
  2. Continuous Monitoring: The helper registers an AudioObjectPropertyListener callback that triggers whenever the system reports a hardware change, such as headphones being connected or disconnected.
  3. Programmatic Switching: When users activate the Output Switcher feature, MixerRoutingSupport.nextSelectedOutputDeviceUID(from:in:) rotates through a user-defined list of device UIDs and commits the selection via AudioObjectSetPropertyData.

This architecture ensures that mixer.currentOutputDeviceUID always reflects the actual system state, allowing the UI to update instantly when hardware changes occur.

Persisting Per-App Volume Levels

Per-application volume storage relies on stable identity keys generated by MixerRoutingSupport.rowIdentity(bundleIdentifier:):

  • Key Generation: For applications with valid bundle identifiers (e.g., com.apple.Music), the helper uses the bundle ID as the dictionary key. For processes lacking bundle information, it falls back to the display name.
  • Storage Format: Volume levels are stored as raw Double fractions in UserDefaults, accessible via MixerRoutingSupport.savedVolumeFraction(for:).
  • Retrieval: When constructing the mixer interface, MixerRoutingSupport.rowMayBeTapped(savedVolume:) retrieves the persisted fraction or returns a system default if no prior setting exists.

The persistence layer guarantees that volume preferences survive application restarts and system reboots without requiring background processes.

Applying Volume Changes to the System

When users adjust a volume slider, the UI invokes MixerRoutingSupport.setVolume(forApp:toFraction:), which executes a three-phase update:

  1. Translation: Converts the linear fraction (0.0–1.0) to the device-specific scalar volume scale required by Core Audio.
  2. System Commit: Calls AudioObjectSetPropertyData targeting the specific output device's kAudioDevicePropertyVolumeScalar address.
  3. Persistence: Writes the new fraction back to UserDefaults under the application's identity key.

This synchronous write-through strategy ensures that volume changes take effect immediately at the hardware level while maintaining durable state for future sessions.

Handling Edge Cases and Error States

The helper includes defensive logic for common hardware scenarios found in Tests/MetricsTests.swift:

  • Headphone Disconnect Protection: If the current output device disappears (e.g., Bluetooth headphones powering down), Vorssaint automatically restores a previously saved "audible floor" volume or the last known safe level, preventing silent output. This behavior is verified by the headphone disconnect protection starts at an audible volume test case.
  • Read-Only Volumes: When encountering read-only audio destinations (such as Disk Images mounted as audio devices), the helper sets a volumeIsReadOnly flag that disables sliders and prevents write attempts to kAudioDevicePropertyVolumeScalar.
  • Input Validation: malformed percentage strings (e.g., "loud" or "nan") are rejected by volumeFraction(fromPercentageText:), preserving the existing volume rather than applying undefined behavior.

Code Example: Reading and Writing Audio Routes

The following Swift patterns demonstrate direct interaction with the routing system:

import Vorssaint

// Retrieve the current system output device UID
let currentUID = MixerRoutingSupport.systemDefaultSelectionID

// Rotate to the next device in a predefined list
if let nextUID = MixerRoutingSupport.nextSelectedOutputDeviceUID(
        from: currentUID,
        in: ["Built-in Output", "AirPods Pro", "Studio Display"])
{
    try MixerRoutingSupport.setCurrentOutputDeviceUID(nextUID)
}

// Read the saved volume for a specific application
let bundleID = "com.spotify.client"
let savedLevel = MixerRoutingSupport.savedVolumeFraction(for: bundleID)

// Apply a new 75% volume level and persist it
try MixerRoutingSupport.setVolume(
        forApp: bundleID,
        toFraction: 0.75)

All operations route through the same Core Audio pathways used by the system Sound preferences panel, ensuring compatibility with hardware volume controls and system notifications.

Summary

  • Centralized Control: MixerRoutingSupport in Sources/Vorssaint/UI/MenuPanel/MixerSection.swift manages all interactions with macOS Core Audio for device routing and volume control.
  • Live Monitoring: Property listeners on kAudioHardwarePropertyDefaultOutputDevice enable real-time updates when hardware changes occur.
  • Durable Storage: Per-app volumes persist as [String: Double] maps in UserDefaults, keyed by bundle identifier or display name.
  • Immediate Application: Volume changes apply instantly via AudioObjectSetPropertyData on kAudioDevicePropertyVolumeScalar, with fallbacks for disconnected hardware and read-only devices.

Frequently Asked Questions

How does Vorssaint identify individual applications for volume tracking?

Vorssaint generates stable identity keys using MixerRoutingSupport.rowIdentity(bundleIdentifier:). For standard applications with bundle identifiers, it uses the reverse-DNS string (e.g., com.apple.Music). For background processes or helpers without bundle IDs, it falls back to the process display name, ensuring every audio-producing entity receives a unique persistence key.

What happens to per-app volume settings when I switch audio devices?

Per-app volume fractions remain stored in UserDefaults keyed by application identity, not by output device. When switching devices via nextSelectedOutputDeviceUID(from:in:), Vorssaint applies the stored volume levels to the new hardware immediately. The system does not maintain separate volume profiles per device; instead, it applies the last known user preference regardless of which speakers or headphones are active.

Does Vorssaint handle unexpected hardware disconnections?

Yes. The helper includes headphone disconnect protection logic verified in Tests/MetricsTests.swift. If the current output device disappears (such as Bluetooth headphones losing connection), Vorssaint detects the property change and restores volume to an audible floor level or the previously saved safe value, preventing scenarios where the system becomes unexpectedly silent.

Where are the per-app volume levels physically stored?

Volume data persists in the application's UserDefaults domain as a [String: Double] dictionary. The Preferences struct wraps this storage, and MixerRoutingSupport synchronizes the in-memory Mixer object with this backing store on every volume change, ensuring data survives application termination and system restarts without requiring elevated privileges or background daemons.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →