# How Vorssaint Utils Implements Per-App Volume Routing on macOS

> Discover how Vorssaint Utils routes per-app audio on macOS. Learn about HAL process taps, aggregate devices, and stream interception for granular audio control.

- Repository: [vorssaint/vorssaint-utils](https://github.com/vorssaint/vorssaint-utils)
- Tags: internals
- Published: 2026-09-08

---

**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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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:

```swift
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:

```swift
// 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:

```swift
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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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:

```swift
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`](https://github.com/vorssaint/vorssaint-utils/blob/main/Sources/Vorssaint/UI/MenuPanel/MixerSection.swift), changes propagate to `AppVolumeMixer` through dedicated setters:

```swift
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:

```swift
// 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`](https://github.com/vorssaint/vorssaint-utils/blob/main/AppVolumeMixer.swift)) serves as the central engine that creates HAL process taps and aggregate devices for audio interception.
- **MixerRoutingSupport** ([`MixerRoutingSupport.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/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.