How Vorssaint Utils Implements Per-App Volume Routing on macOS

The Audio Mixer service routes per-application audio by creating HAL process taps and aggregate devices for each app, using AppVolumeMixer to manage stream interception and MixerRoutingSupport to determine routing logic, volume scaling, and device selection.

The Vorssaint Utils Audio Mixer provides sophisticated per-app volume routing capabilities on macOS, allowing users to control individual application volumes and output destinations independently. This functionality relies on CoreAudio HAL (Hardware Abstraction Layer) process taps and aggregate devices to intercept, modify, and redirect audio streams at the system level.

Core Architecture of the Audio Mixer Service

AppVolumeMixer – The Central Engine

Located in Sources/Vorssaint/Services/Audio/AppVolumeMixer.swift, the AppVolumeMixer class serves as the primary orchestrator. It discovers running audio-producing applications, maintains the registry of active process taps, and constructs aggregate devices that mix tapped streams with user-defined gain levels. The engine stores per-app volume levels, routing destinations, and bypass flags in memory, synchronizing them with user defaults for persistence.

MixerRoutingSupport – The Routing Decision Layer

The MixerRoutingSupport.swift file encapsulates the policy logic that determines when audio intervention is necessary. This utility class decides whether an app requires a process tap through requiresEngine(volume:hasAudioObjects:), computes effective output devices via effectiveDeviceUID(selectedUID:currentUID:unavailable:), and handles special cases like headphone detection and bypassed applications. It also provides conversion utilities between UI percentage strings (0%–200%) and the internal floating-point range of 0.0–2.0.

How Per-App Volume Routing Works Under the Hood

Discovering Audio Apps and Establishing HAL Taps

When AppVolumeMixer.start() initializes the service, it registers HAL listeners for device list changes, default output changes, and process object lists:

installListener(selector: kAudioHardwarePropertyProcessObjectList)   // lines 182-183

For each discovered application, the mixer evaluates whether it requires a tap. If MixerRoutingSupport.requiresEngine() returns true, the mixer creates a process tap that intercepts the app's audio stream and an aggregate device that receives the tapped output. The tap applies real-time gain adjustment before feeding the aggregate device, which becomes the active audio output for that specific application.

Determining When a Tap Is Required

The MixerRoutingSupport.requiresEngine(volume:hasAudioObjects:) method implements the routing policy:

// Validation from MetricsTests.swift
expect(!MixerRoutingSupport.requiresEngine(volume: 1,   hasAudioObjects: false)) // line 7328
expect( MixerRoutingSupport.requiresEngine(volume: 0.5, hasAudioObjects: true))  // line 7323

A tap is created only when an app's volume differs from unity (1.0) or when the application manages its own audio (such as Zoom or DAWs). Bypassed applications (isBypassed) remain pinned at unity volume and never receive a process tap, allowing them to pass through the system untouched.

Resolving Output Devices and Fallbacks

Each MixerApp instance tracks two device identifiers: selectedOutputDeviceUID (user's choice) and effectiveOutputDeviceUID (actual routing target). When the selected device becomes unavailable, MixerRoutingSupport.effectiveDeviceUID() automatically falls back to the current system default:

let uid = MixerRoutingSupport.effectiveDeviceUID(selectedUID: app.selectedOutputDeviceUID,
                                                currentUID: mixer.currentOutputDeviceUID,
                                                unavailable: app.outputDeviceUnavailable)

The system includes special handling for headphone devices through outputLooksLikeHeadphones(name:), ensuring that disconnect-protection volume levels are restored correctly when switching between headphone and speaker outputs.

Converting UI Percentages to Internal Gain Values

The user interface in MixerSection.swift presents volume controls as percentages (0%–200%), while the CoreAudio backend requires normalized floating-point values (0.0–2.0). MixerRoutingSupport provides conversion utilities:

let fraction = MixerRoutingSupport.volumeFraction(fromPercentageText: "75%")   // test line 7137

These helpers parse user input, clamp values to the valid range, and return Double values ready for the AppVolumeMixer engine.

Updating and Persisting Routing Configuration

Real-Time Updates to the Audio Graph

When users interact with the SwiftUI panel in Sources/Vorssaint/UI/MenuPanel/MixerSection.swift, changes propagate to AppVolumeMixer through dedicated setters:

mixer.setVolume(for: app.id, to: newFraction)
mixer.setOutputDevice(for: app.id, to: newDeviceUID)

Each modification triggers a teardown of the existing tap and aggregate device on a dedicated queue (teardownQueue), followed by creation of fresh audio objects with updated parameters. The mixer then refreshes HAL listeners to publish the new configuration to the system.

Persisting State Across Sessions

AppVolumeMixer maintains two dictionaries for state preservation: sessionVolumes (for apps without bundle identifiers) and sessionRoutes (selected output devices). During the syncWithPreferences() call, these values serialize to user defaults and restore automatically on launch, ensuring user routing preferences survive application restarts.

Practical Code Examples

To interact with the per-app volume routing system programmatically:

// Retrieve current volume fraction for a specific app
let fraction = AppVolumeMixer.shared.apps
    .first { $0.id == "com.apple.Music" }?.volume ?? 1.0

// Set volume to 75%
AppVolumeMixer.shared.setVolume(for: "com.apple.Music", to: 0.75)

// Route to specific headphones by UID
AppVolumeMixer.shared.setOutputDevice(for: "com.apple.Music",
                                      to: "AppleUSBAudioEngine:2")

// Reset to system default output
AppVolumeMixer.shared.setOutputDevice(for: "com.apple.Music",
                                      to: MixerRoutingSupport.systemDefaultSelectionID)

Summary

  • AppVolumeMixer (AppVolumeMixer.swift) serves as the central engine that creates HAL process taps and aggregate devices for audio interception.
  • MixerRoutingSupport (MixerRoutingSupport.swift) determines when taps are needed, resolves effective output devices, and converts between UI percentages and internal gain values.
  • The system creates process taps only when volume differs from unity or for self-managing audio applications, optimizing performance for bypassed apps.
  • Per-app routing state persists through sessionVolumes and sessionRoutes dictionaries synchronized with user defaults.
  • Real-time updates teardown and rebuild audio graphs atomically to prevent audio glitches during configuration changes.

Frequently Asked Questions

What happens when a selected output device becomes unavailable?

When the user-selected device disconnects, MixerRoutingSupport.effectiveDeviceUID() automatically falls back to the current system default output device. The effectiveOutputDeviceUID property updates to reflect this change while preserving the user's original selection in selectedOutputDeviceUID for when the device reconnects.

Why does the mixer only create taps for certain applications rather than all apps?

The requiresEngine(volume:hasAudioObjects:) method optimizes system resources by creating taps only when necessary. Apps running at unity volume (1.0) without special audio management requirements pass through the system directly without interception, reducing CPU overhead and audio latency for the majority of use cases.

How does the mixer handle volume percentages above 100%?

The internal representation uses a floating-point range of 0.0 to 2.0, where 2.0 represents 200% volume. MixerRoutingSupport.volumeFraction(fromPercentageText:) parses percentage strings and clamps values to this range, allowing users to boost quiet applications beyond their original levels while preventing distortion from extreme values.

Where is the per-app routing configuration stored between launches?

AppVolumeMixer persists configuration through the syncWithPreferences() method, which serializes the sessionVolumes and sessionRoutes dictionaries to user defaults. This ensures volume levels and output device selections survive application restarts, particularly for applications identified by bundle ID.

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 →