A menu bar volume control for macOS aggregate and multi-output audio devices. It can also create them, and balance the volume between the devices inside them.
macOS can't change the volume of an aggregate or multi-output device. The aggregate has no volume control of its own, so the Sound menu slider is greyed out and the volume keys do nothing.
MultiVolume finds the aggregate's active sub-devices and sets the volume on each one. It can also create and edit multi-output devices itself, so you don't need Audio MIDI Setup.
It is a from-scratch replacement for MultiSoundChanger, which has not been updated since 2021. No code was copied from it. The rewrite exists mostly for security reasons:
| MultiSoundChanger 1.0.1 | MultiVolume | |
|---|---|---|
| Accessibility | Requested at launch | Only for the media keys |
| Hardened runtime | Off | On |
get-task-allow entitlement |
Present | Absent |
| Architectures | x86_64 only | arm64 + x86_64 |
| Dependencies | CocoaPods | None |
| Private frameworks | OSD.framework |
None |
The MultiSoundChanger column was checked with codesign, lipo and otool on its 1.0.1 release, and against its source.
The real problem with the original is the combination: an app with the Accessibility permission, plus get-task-allow, plus no hardened runtime. Any process running as your user could attach to it or inject a library, then use that permission without macOS asking you again. MultiVolume ships with hardened runtime on and get-task-allow off, so that route is closed.
Homebrew:
brew tap EcossAIah/multivolume https://github.com/EcossAIah/MultiVolume
brew trust ecossaiah/multivolume # Homebrew asks before using a third-party tap
brew install --cask multivolumeUpdate later with brew upgrade --cask multivolume.
Or by hand:
- Download
MultiVolume-<version>.zipfrom Releases. - Unzip it and drag
MultiVolume.appto/Applications. - Open it. A speaker icon appears in the menu bar. The app has no Dock icon.
The release build is signed with a self-signed certificate, not notarized, so Gatekeeper blocks the first launch. On macOS 15 and later, try to open the app once, then go to System Settings > Privacy & Security and click Open Anyway. The button stays there for about an hour after the blocked launch and asks for an administrator password. If you'd rather not do that, build from source. A local build has no quarantine flag, so Gatekeeper doesn't block it.
Because every release is signed with the same certificate, macOS keeps the Accessibility and Input Monitoring grants when you update. (Updating from 1.0.0, which was ad-hoc signed, needs them once more.)
Homebrew:
brew uninstall --zap --cask multivolume--zap also deletes the settings file. Without it, only the app is removed.
By hand: quit MultiVolume, delete /Applications/MultiVolume.app, then run defaults delete io.github.ecossaiah.multivolume.
Before uninstalling either way, untick Open at Login if you turned it on. Multi-output devices made with MultiVolume are ordinary system devices and stay after uninstalling. Delete them first with Edit Devices… > Delete, or later in Audio MIDI Setup.
Either way, macOS keeps the app's entries under System Settings > Privacy & Security > Accessibility and Input Monitoring. Select MultiVolume in each list and click − to remove them. To do this from Terminal instead, run this before uninstalling, while the app is still installed:
tccutil reset All io.github.ecossaiah.multivolumeClick the menu bar icon to open the panel:
- Volume slider and mute. These control the current default output device. On an aggregate device they act on every active sub-device.
- −10% / −5% / +5% / +10% buttons for set steps.
- Device Balance. Shown when the current device is a multi-output device with two or more sub-devices that have a volume control. Each one gets a share from 0 to 100%. Its volume is the main volume times its share, so the slider and volume keys move everything together and keep the balance. Shares are saved per multi-output device.
- Output Device list. Choosing a device makes it the system default, same as the Sound menu. Aggregate devices are tagged
AGG. Devices without a software volume control, such as most HDMI and DisplayPort monitors, show an ⓘ. Hover over it to see why. - New Multi-Output… creates a multi-output device. Name it, tick two or more devices and click Create, and it becomes the output. The first device ticked, tagged
CLOCK, sets the timing; the others are drift-corrected to it, as in Audio MIDI Setup. - Edit Devices… appears when the current device is an aggregate. It changes its name and devices, or deletes it.
- Open at Login. Starts MultiVolume when you log in. It matches the entry under System Settings > General > Login Items.
- Refresh and Quit (⌘Q).
Media keys (F10 / F11 / F12) work once the permission is granted (see below):
- Volume up and down change the volume in 1/16 steps.
- Mute toggles mute.
- MultiVolume only takes over these keys when the default output is an aggregate device. On any other device it passes them through, so macOS handles them as usual and shows its own volume overlay.
- For aggregate devices MultiVolume shows its own overlay at the top right of the screen. Hover over it to get a slider. Click the left speaker to mute, the right speaker for full volume, or × to close it.
- When Play feedback when volume is changed is on in System Settings > Sound, the volume keys and the overlay's right speaker play the system feedback sound, as macOS does.
Everything except the media keys works with no permissions.
macOS sends the volume keys as system-defined HID events, not normal key presses. The only way to catch them is a CGEventTap, and macOS only allows that with the Accessibility permission. On some macOS versions, Input Monitoring is needed too. While either is missing, the menu shows the state of both, and Open Settings… asks for whichever one is missing. Media keys turn on by themselves once access is granted.
How the event tap is limited (Sources/MultiVolume/MediaKeyMonitor.swift):
- It listens only for
NX_SYSDEFINEDevents. Normal key presses never reach the app, so it can't see what you type. - It acts only on volume up, volume down and mute. Every other event passes through unchanged.
- Nothing is logged, saved or sent anywhere.
The app has no networking code. It doesn't ask for Screen Recording, Microphone, Camera, Full Disk Access, Contacts or Automation. If it shows up under any of those, something is wrong.
APP=/Applications/MultiVolume.app
codesign -dvvv --entitlements - "$APP" 2>&1 | grep -E "flags=|Signature=|get-task-allow"
lipo -archs "$APP/Contents/MacOS/MultiVolume"
otool -L "$APP/Contents/MacOS/MultiVolume" | grep -i private || echo "no private frameworks"
lsof -i -a -p "$(pgrep -x MultiVolume)" # prints nothing: no socketsYou should see runtime in the flags, no get-task-allow line, and both architectures.
Requirements:
- macOS 14 or later
- A Swift 6 toolchain: Xcode 16 or later, or the matching Command Line Tools
git clone https://github.com/EcossAIah/MultiVolume.git
cd MultiVolume
swift test # unit tests, no audio hardware needed
./build.shbuild.sh does the following:
- Builds a universal release binary. If the toolchain can't, it falls back to the native architecture.
- Puts together
build/MultiVolume.appand signs it with hardened runtime. - Maps the checkout path to
.and strips debug symbols, so the binary carries no absolute build paths. - Checks its own output. It fails if hardened runtime is missing, if
get-task-allowis present, if the app links a private framework, or if the binary contains the checkout path or a home folder path. - Writes
build/MultiVolume-<version>.zip.
Options are set with environment variables:
SIGN_ID="Apple Development: Name (TEAMID)" ./build.sh # sign with your certificate
BUNDLE_ID=com.example.multivolume ./build.sh # default: io.github.ecossaiah.multivolume
UNIVERSAL=0 ./build.sh # native architecture onlyThen install:
cp -R build/MultiVolume.app /Applications/To keep the Accessibility grant across rebuilds, run ./make-signing-cert.sh once. It creates a self-signed certificate in your login keychain, and build.sh uses it from then on. Any other identity works too via SIGN_ID (security find-identity -v -p codesigning lists yours).
To publish a release, bump the version in Resources/Info.plist, add a CHANGELOG.md section for it, commit, and run ./release.sh. It tests, builds, updates the cask, tags, pushes, and creates the GitHub release.
To regenerate the app icon (needs Python 3 and Pillow):
python3 icon/generate_icon.py && python3 icon/make_iconset.pyAudioCataloglists output devices through the CoreAudio HAL. A device with transport typekAudioDeviceTransportTypeAggregateis an aggregate. Both Aggregate and Multi-Output devices from Audio MIDI Setup count. For these, the app readskAudioAggregateDevicePropertyActiveSubDeviceList.- A device can be a volume target if it has a settable
kAudioDevicePropertyVolumeScalar. The app tries the main element first. If that isn't settable, it uses the preferred stereo channel pair. - Setting the volume writes the same value (0 to 1) to every target. The value shown is the average of all targets.
- For a multi-output device with two or more targets, each target is set to the main volume times its share. The main volume shown is the highest target volume divided by its share. Shares are stored in the app's settings, keyed by the device's UID.
- Multi-output devices are created with
AudioHardwareCreateAggregateDevice, the public API Audio MIDI Setup uses, as public stacked aggregates. Editing one destroys it and creates it again with the same UID, because CoreAudio can't redo drift correction in place. The same UID keeps its saved shares. - Open at Login uses
SMAppService.mainApp. - Mute uses the hardware mute property where one exists. Many aggregates have none. For those, mute sets the volume to 0 and unmute restores the earlier level.
- CoreAudio property listeners watch the device list, the default output device, and each target's volume and mute. Changes made by other apps show up right away. Each listener is removed when its owner is freed.
- The UI is a SwiftUI
MenuBarExtra. The app runs as an agent (LSUIElement), so it has no Dock icon.
Sources/MultiVolume/
CoreAudioSupport.swift typed wrapper over the CoreAudio HAL, listener lifetime
AudioDevice.swift device discovery, sub-device resolution, volume and mute
AudioManager.swift observable state, listeners, media-key policy
MediaKeyMonitor.swift CGEventTap for the volume keys (the only permissioned code)
MultiOutputDevices.swift create, rebuild and delete multi-output devices
MultiOutputEditor.swift state for the in-menu multi-output editor
MenuContentView.swift menu UI
InfoTip.swift the ⓘ and its instant tooltip
VolumeHUD.swift on-screen volume overlay for the volume keys
VolumeFeedback.swift the volume-key feedback sound
LoginItem.swift Open at Login
MultiVolumeApp.swift entry point
Tests/MultiVolumeTests/ media-key event decoding tests
icon/ icon master, iconset, and the scripts that produce them
Casks/multivolume.rb Homebrew cask, updated by release.sh
- The overlay for aggregate devices is MultiVolume's own, not the system one, which comes from a private framework. It looks close but not identical.
- Outside Device Balance, every sub-device gets the same volume value, and devices with different volume curves may not sound equally loud. Use the balance shares to even them out.
- Editing a multi-output device recreates it. If macOS refuses to delete one made elsewhere, the editor shows the error and nothing changes.
- Sub-devices without a settable volume control, such as many HDMI and DisplayPort outputs, are skipped. If none of them have one, the slider is disabled.
- Only the current default output device can be controlled.
- Ad-hoc signed builds (anything built without the certificate from
make-signing-cert.sh) get a new code hash on every rebuild, so macOS drops the Accessibility grant. To grant it again, remove the old entry under Privacy & Security, then add the app back.