How the Per-App Volume Mixer Uses CoreAudio Aggregate Devices in vorssaint-utils
The per-app volume mixer in vorssaint-utils creates individual CoreAudio process taps for each application and routes them through private aggregate devices to apply gain adjustments in real-time.
The vorssaint-utils repository implements system-wide per-application volume control on macOS 14.4+ by leveraging low-level CoreAudio APIs that are not exposed through standard system preferences. This architecture intercepts audio at the process level, mixes it through temporary aggregate devices with custom gain staging, and outputs to any selected device without requiring application-specific support.
Architecture Overview
The mixer operates as a middleman between audio-producing processes and physical output devices. When a user adjusts the volume for a specific app in the UI, the system evaluates whether a tap is necessary, constructs a private audio pipeline, and manages the lifecycle of CoreAudio objects to prevent resource exhaustion or HAL deadlocks.
The core components reside in Sources/Vorssaint/Services/Audio/AppVolumeMixer.swift, with routing decisions abstracted into MixerRoutingSupport.swift. The implementation relies on two primary CoreAudio primitives: process taps (AudioHardwareCreateProcessTap) to capture and mute original app audio, and aggregate devices (AudioHardwareCreateAggregateDevice) to remix the signal with user-defined gain.
Device Discovery and State Monitoring
Before any audio processing occurs, the AppVolumeMixer class maintains a continuous watch on the CoreAudio Hardware Abstraction Layer (HAL). It monitors three specific properties on a dedicated halQueue:
kAudioHardwarePropertyDevices– Available output deviceskAudioHardwarePropertyDefaultOutputDevice– Current system defaultkAudioHardwarePropertyProcessObjectList– Running audio-producing processes
When any of these change, AppVolumeMixer.readSnapshot() assembles a RefreshSnapshot containing the current state. This snapshot drives the decision engine that determines which applications require active tapping and which can pass through to the default output unmodified.
Tap Requirement Logic
Not every application requires a dedicated audio engine. The MixerRoutingSupport.swift file contains the requiresEngine static function that implements a short-circuit logic to avoid unnecessary CoreAudio object creation:
// MixerRoutingSupport.swift
static func requiresEngine(
hasAudioObjects: Bool = true,
volume: Double,
selectedOutputDeviceUID: String?,
targetOutputDeviceUID: String?,
defaultOutputDeviceUID: String?) -> Bool {
guard hasAudioObjects else { return false }
guard let targetOutputDeviceUID else { return false }
if !isUnity(volume) { return true } // user lowered/boosted volume
guard let selectedOutputDeviceUID else { return false }
guard let defaultOutputDeviceUID else { return true }
return selectedOutputDeviceUID != defaultOutputDeviceUID &&
targetOutputDeviceUID != defaultOutputDeviceUID
}
If an application is at 100% volume (isUnity) and routed to the system default output device, the mixer returns false and avoids creating tap infrastructure entirely. This optimization reduces CPU usage and HAL object count for the common case where most apps use default system routing.
Engine Construction with Aggregate Devices
When requiresEngine returns true, AppVolumeMixer.applyRouting(for:) spawns a TapGainEngine on a dedicated buildQueue. This engine constructs the actual audio pipeline through a specific sequence of CoreAudio calls.
First, it creates a process tap that mutes the original application output:
// AppVolumeMixer.swift – TapGainEngine init
let description = CATapDescription(stereoMixdownOfProcesses: objects)
description.muteBehavior = .mutedWhenTapped
guard AudioHardwareCreateProcessTap(description, &tapID) == noErr else { return nil }
Next, it builds a private aggregate device containing the tap as a sub-tap and the target output device as the main sub-device:
let aggregate: [String: Any] = [
kAudioAggregateDeviceNameKey: "Vorssaint Mixer",
kAudioAggregateDeviceUIDKey: UUID().uuidString,
kAudioAggregateDeviceIsPrivateKey: true,
kAudioAggregateDeviceMainSubDeviceKey: outputDeviceUID,
kAudioAggregateDeviceSubDeviceListKey: [[kAudioSubDeviceUIDKey: outputDeviceUID]],
kAudioAggregateDeviceTapListKey: [[
kAudioSubTapUIDKey: description.uuid.uuidString,
kAudioSubTapDriftCompensationKey: true,
]],
kAudioAggregateDeviceTapAutoStartKey: true,
]
guard AudioHardwareCreateAggregateDevice(aggregate as CFDictionary, &aggregateID) == noErr else {
AudioHardwareDestroyProcessTap(tapID); return nil
}
The kAudioAggregateDeviceIsPrivateKey: true flag ensures these temporary devices do not appear in system audio preference panes or other applications' device lists, keeping the UI clean while the mixer manages dozens of potential concurrent pipelines.
Real-Time Audio Processing
Once constructed, the aggregate device's IOProc receives audio frames from the tapped process. Inside AppVolumeMixer.swift, the engine applies the user-specified gain and implements protective limiting:
// Inside the IOProc block (AppVolumeMixer.swift)
let gain = box.value
let frames = MixerRender.render(source: inputBuffers[tapIndex],
into: outputBuffers,
gain: gain)
if frames > 0 { cycles.increment() }
// Apply limiter, fallback if needed
A BoostLookaheadBufferListLimiter prevents digital clipping when users set gain above 100%. This look-ahead limiter analyzes upcoming audio frames to apply transparent gain reduction only when necessary, preserving audio quality during sudden transient spikes.
Lifecycle Management and Cleanup
The mixer maintains an engines: [String: any GainEngine] dictionary keyed by process identifier to track active pipelines. When users adjust volume or change output devices, applyRouting either updates the existing engine's gain property or destroys and rebuilds the engine if the routing target changes.
Engine teardown is intentionally throttled using maximumConcurrentTeardowns = 4 to prevent overwhelming the CoreAudio HAL with simultaneous destruction requests, which can cause deadlock or audio dropouts across the system. This throttling mechanism ensures that rapid UI changes—such as dragging a volume slider—do not destabilize the audio subsystem.
Permission Handling and Edge Cases
Creating process taps requires the Screen Recording / Audio Recording permission on macOS 14.4 and later. If AudioHardwareCreateProcessTap returns a permission error, the mixer sets a needsPermission flag that propagates to the UI layer (MixerSection.swift), directing users to System Settings to grant access.
The implementation also includes headphone-disconnect protection. If the system detects headphones unplugging, the mixer optionally lowers the system output volume and stores the previous level in loweredOutput, restoring it when the original device returns to prevent sudden loud playback through speakers.
Summary
- Process taps mute original app audio and redirect it into the mixer's private pipeline via
AudioHardwareCreateProcessTap. - Private aggregate devices function as invisible mixers, combining the tapped stream with the target output device while applying user-defined gain.
- Smart routing logic in
MixerRoutingSupport.swiftavoids creating engines for apps at 100% volume using the default output, optimizing performance. - Look-ahead limiting prevents clipping when boosting audio above unity gain.
- Throttled teardown and
maximumConcurrentTeardownsprotect the HAL from deadlocks during rapid configuration changes.
Frequently Asked Questions
What is a CoreAudio aggregate device in the context of vorssaint-utils?
In vorssaint-utils, a CoreAudio aggregate device is a temporary virtual audio device created for each tapped application. It combines the process tap (input) and the desired output device (main sub-device) into a single logical unit that the system treats as one audio device, allowing the mixer to intercept and modify the audio stream before it reaches physical hardware.
Why does the mixer require Screen Recording permission on macOS 14.4+?
The permission requirement stems from AudioHardwareCreateProcessTap, which captures audio from other processes' output streams. macOS classifies this capability as audio recording (process tapping) and requires explicit user consent through the Screen Recording & Audio Recording privacy pane, even though the mixer does not record to disk or capture screen content.
How does vorssaint-utils handle volume changes without creating audio glitches?
The mixer implements two protective mechanisms: throttled engine teardown (limiting concurrent destructions to 4) prevents HAL overload, and a look-ahead limiter (BoostLookaheadBufferListLimiter) prevents digital clipping when boosting volume above 100%. These ensure smooth transitions when users drag volume sliders or switch output devices.
Can the mixer route different apps to different physical devices simultaneously?
Yes. Each TapGainEngine creates a private aggregate device targeting a specific output device UID. If User A routes Spotify to Headphones and Chrome to Built-in Output, the mixer maintains two separate aggregate devices with independent taps and gain settings, routing each app's audio to its designated hardware endpoint.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →