A per-application volume mixer for macOS, in the menu bar.
The Windows volume mixer, on a Mac: one slider per app and an output switcher. No audio driver, no kernel extension.
macOS has no per-application volume. The system mixer is one slider for everything, and every app that has offered per-app control has done it by installing a HAL plug-in or a virtual audio driver that permanently sits in your audio path. Trumpet does not install anything. Since macOS 14.2 CoreAudio can hand a client a process tap: a private stream carrying one process's output, with a mute behaviour that keeps that audio off the hardware while the tap is being read. Trumpet taps the apps you turn down, silences their own path to the speakers, and re-renders them itself with a gain applied.
- One slider per app that is making sound, with the level remembered per app. A browser that respawns its audio helper on every tab keeps the level you set.
- Output device switcher at the top of the panel. Switches both the default output and the system output, so notification sounds follow.
- Main volume for the current device, with mute.
- Mute per app, independent of its level.
- Apps are grouped by the app that owns them, not by the process that happens to emit audio: Chromium-family browsers show as one row, not one row per helper.
- Nothing is installed into the audio path. No driver, no kext, no login-time daemon unless you ask for one.
- Scriptable, over a local socket and a URL scheme. Read the mixer and drive it from a script, a hotkey tool, or another app. See Scripting.
- macOS 14.2 or later. That is where
AudioHardwareCreateProcessTaplands; there is no per-process audio below it without a driver. Developed and tested on macOS 26.5 on Apple Silicon. - The Xcode command line tools:
xcode-select --install. A full Xcode install is not needed.
brew tap orellius/tap brew install trumpet
A formula, not a cask, and it builds from source on your machine. That is the
point: a locally built bundle is never Gatekeeper-quarantined, so there is no
"damaged, can't be opened" wall. Because Homebrew formulae install into the
Cellar rather than /Applications, link the bundle once:
ln -sfn "$(brew --prefix)/opt/trumpet/Trumpet.app" /Applications/Trumpet.app
open /Applications/Trumpet.appgit clone https://github.com/Orellius/trumpet
cd trumpet
scripts/install.shThat builds a release binary, wraps it into Trumpet.app, ad-hoc signs it, replaces any copy already in /Applications, and launches it. A locally built bundle is not Gatekeeper-quarantined, so there is no "unidentified developer" prompt and nothing to right-click-open.
To build without installing:
scripts/bundle.sh release # produces ./Trumpet.app in the repo swift run # run straight from source, no bundle
Trumpet has no Dock icon and no window. Look for the fader icon in the menu bar.
- Left click drops the mixer panel. Right click opens a short menu (Open at Login, Reset All Levels, Quit).
- The Apps list is empty until something is actually playing. That is expected: it lists apps that currently hold audio, the same as the Windows mixer.
- The first time you pull an app below 100%, macOS shows its audio-recording indicator in the menu bar, because a process tap counts as an audio capture. Trumpet never records, buffers to disk, or transmits anything: the tapped samples are multiplied by a gain and written straight back out to your speakers inside the same callback. The indicator disappears when every app is back at 100%, because at that point Trumpet holds no taps at all.
Open the panel, use the ⋯ menu at the bottom right, and pick Open at Login. This registers through SMAppService, which refuses apps whose signature does not verify, which is why bundle.sh signs the assembled bundle rather than relying on the linker's ad-hoc signature.
brew upgrade trumpet # Homebrew install cd trumpet && git pull && scripts/install.sh # source install
The script quits the running copy first. Your levels survive: they live in ~/Library/Preferences/com.orellius.trumpet.plist, not in the bundle.
osascript -e 'quit app "Trumpet"' rm -f /Applications/Trumpet.app # the symlink, if you made one brew uninstall trumpet # Homebrew install rm -rf /Applications/Trumpet.app # source install defaults delete com.orellius.trumpet # optional: forget saved levels
Nothing else is left behind. There is no driver, no kernel extension, no launch daemon and no audio device to remove: every tap Trumpet holds dies with the process, so quitting it already restores every app to full volume.
| Symptom | Cause |
|---|---|
| The Apps list is empty | Nothing is playing. Start audio somewhere and reopen the panel. |
| An app's slider does nothing | It is playing to a device other than the current default output. That case is untested; see Measured limits. |
| The main volume slider is greyed out | That output exposes no software volume at all. Some monitors over DisplayPort and HDMI answer no volume property; use the monitor's own controls. |
| The recording indicator stays on | Something is still below 100%, which holds a tap. Use Reset. |
| Open at Login does nothing | The bundle signature did not verify. Rebuild with scripts/install.sh rather than copying the binary by hand. |
One private aggregate device holds the real output device as its main sub-device, plus one sub-tap per controlled app. The IOProc receives one input buffer per sub-tap, in tap-list order, multiplies each by its app's gain, sums them, and writes the result to the device.
Brave ──tap──┐
Spotify ─tap──┼──► private aggregate ──► IOProc: ×ばつgain, sum ──► real output device
Consequences worth knowing, both deliberate:
- An app left at 100% is never tapped. With every app at 100%, Trumpet holds no taps at all and adds nothing to anyone's audio path.
- If Trumpet dies, the taps die with the process and audio returns to normal immediately. The failure mode is "the controls stop working", never "audio stops working".
Trumpet can be read and driven from outside. Both surfaces are local to your Mac and to your user account; nothing listens on the network.
trumpet://settings opens and focuses the panel. Any other trumpet:// URL does the same, because the panel is the whole interface.
open trumpet://settings
A UNIX domain socket at ~/Library/Application Support/com.orellius.trumpet/control.sock, created when Trumpet starts and removed when it quits. The directory is 0700 and the socket is 0600, so only your user account can connect. Anything running as you can, which is the same trust level as a shell script you run yourself.
The protocol is line-delimited JSON: one request object per line, one reply object per line. A reply is {"ok": true, "state": {...}} or {"ok": false, "error": "..."}, and every successful reply carries the full state.
| Request | Effect |
|---|---|
{"cmd":"state"} |
Return the state and change nothing. |
{"cmd":"setLevel","bundleId":"com.brave.Browser","level":40} |
Set one app's level, 0-100. |
{"cmd":"mute","bundleId":"com.brave.Browser","muted":true} |
Mute or unmute one app. |
{"cmd":"setDevice","uid":"..."} |
Switch the output device by UID. |
{"cmd":"setMainVolume","level":60} |
Set the current device's volume, 0-100. |
{"cmd":"subscribe"} |
Keep the connection open and receive {"event":"state","state":{...}} whenever the state changes. |
The state object:
{
"apps": [{"bundleId": "com.brave.Browser", "name": "Brave Browser", "level": 40, "muted": false}],
"devices": [{"uid": "BuiltInSpeakerDevice", "name": "Mac Studio Speakers", "current": true, "hasVolume": true}],
"mainVolume": 100,
"mainMuted": false
}Reading it from a shell:
SOCK=~/"Library/Application Support/com.orellius.trumpet/control.sock" echo '{"cmd":"state"}' | nc -U "$SOCK" -w 1
Three behaviours worth knowing before you write a client:
appsholds only what the panel holds: apps currently making sound, apps that made sound in the last 20 seconds, and apps you have turned down. AsetLevelfor anything else is refused withno audible app with bundleId ..., because Trumpet has nothing to tap.- Levels out of
0-100are refused, not clamped. A client sending150has a bug and is told so. subscribecosts something. Trumpet stops polling when its panel is closed and it controls nothing, and a subscriber turns polling back on so its events stay current. Disconnect when you are not displaying the state.
Stated rather than implied. All figures measured on a Mac Studio, macOS 26.5.1, 2026年08月11日.
- Tested on macOS 26.5.1 on Apple Silicon only. The deployment target is 14.2 because that is what the tap API requires; 14.x and Intel are untested.
- Latency. A tapped app is delayed by one buffer, 512 frames at 48 kHz, about 11 ms. Untapped apps are unaffected.
- CPU. About 2% of one core with a global tap running (measured before the bass boost was removed; per-app taps cost less).
- Some devices have no software volume at all. An LG ULTRAGEAR over DisplayPort answers neither
VirtualMainVolumenorVolumeScalar; the panel says so and disables the main slider rather than showing one that does nothing. Bluetooth speakers typically answer onlyVirtualMainVolume, built-in outputs answer both. - Apps that play to a device other than the default output are still tapped as a global mixdown and re-rendered to the default output. This is untested and probably wrong for that case.
- Changing the tapped set rebuilds the aggregate, which is a brief glitch for the other tapped apps. Moving a slider does not: levels are applied to a live aggregate with no rebuild.
- The app-level mute is a gain of zero, not a CoreAudio mute, so a muted app is still tapped and re-rendered silently.
Made for the community by Orellius (Orel Ohayon), github.com/Orellius. The same credit and a link to the repository are in the panel itself, at the bottom of the dropdown.
If Trumpet is useful to you, starring the repo is the whole ask.
AGPL-3.0-only. See LICENSE and NOTICE. Forks must stay open and must preserve the attribution; the app checks its own copyright notice at launch and refuses to run without it.