A free, open-source macOS 14.4+ utility that adds a real, keyboard-drivable software master volume to any audio output — including pro interfaces like the Focusrite Scarlett 2i2 and other fixed-level interfaces/DACs where the macOS volume keys just show the crossed-out "no volume" HUD.
No driver, no kext, no admin rights. Loudini uses a driverless Core Audio process tap (macOS 14.4+): a small daemon taps every app's output, mutes the direct path, and re-renders the mix to your output device through a software gain. It fails open — if the daemon ever dies, macOS instantly restores the normal direct audio path. A crash can never leave you muted.
Download Loudini.dmg — Developer ID signed & notarized by Apple, Apple Silicon, macOS 14.4+. More at loudini.app · all releases
Open the .dmg, drag Loudini to Applications, then follow First run below.
Requires macOS 14.4+ and the Xcode Command Line Tools (xcode-select --install) — build-app.sh
uses swiftc and codesign; the release scripts also need python3.
menubar/build-app.sh && open menubar/Loudini.appOne command: it compiles the engine (helper/loudini-helper, the daemon + CLI binary), builds the
app, and bundles the daemon inside.
- Built it yourself? Ad-hoc-signed builds are unsigned as far as macOS is concerned: right-click →
Open once, or
xattr -dr com.apple.quarantine Loudini.app(recursive — quarantine sits on every file inside). The DMG from Releases needs none of this: it is signed and notarized, and opens on a normal double-click. - Grant Accessibility when prompted — this is what lets Loudini own your volume keys.
- Grant System Audio Recording when the daemon starts — this is the volume engine itself. (Loudini captures app audio only to re-render it at your chosen volume; nothing is recorded.)
- Verify:
helper/loudini-helper getshould printrunning=true pipeline=trueand your device name — or just look at the menu bar: the Loudini item shows the live level, and a ⚠︎ badge if anything still needs attention (the dropdown then contains the fix).
⚠️ Ad-hoc builds: re-grant after every rebuild. With ad-hoc signing each rebuild changes the app's code identity, so macOS silently invalidates both grants while the Settings toggles still show enabled. Runscripts/make-dev-cert.shonce and this stops happening — grants then survive rebuilds. To recover a build you already have: the menu's Repair Volume-Key Permission… item (Accessibility) and the Audio capture not working — click to fix row (System Audio Recording).
| Frontend | Needs | What you get |
|---|---|---|
Menu-bar app (menubar/) |
Accessibility + System Audio Recording | Volume keys just work (hold ⇧ Shift for 1% fine steps); live level in the menu bar (+ ⚠︎ when broken); volume + brightness sliders, mute, HUD; Start at Login; self-repair actions |
CLI (scripts/install-cli.sh) |
a running daemon for the volume commands; brightness and doctor need nothing |
loudini up/down/mute/set/get/apps/app/brightness/doctor from any terminal or hotkey tool |
Stream Deck (plugin/) |
Stream Deck app | Up/Down/Mute keys, faces show the live level (42% / 🔇 / ⚠︎) |
| Karabiner / BTT / skhd | that tool + the CLI | Bind any key to the CLI — recipe in the appendix |
Every frontend needs the engine built once — menubar/build-app.sh does that (or run the swiftc
line inside it if you only want the binary).
Run exactly one daemon — it enforces this itself with a lock file, so worst case a second daemon exits immediately. The menu-bar app prefers your LaunchAgent when one is installed (and revives it if it died); otherwise it runs its own bundled daemon.
With an external DDC-capable monitor attached, the menu-bar app also controls its brightness (DDC/CI, the same mechanism MonitorControl uses — no extra permission needed):
- The hardware brightness keys drive it — but only when no built-in display is active, so a MacBook with the lid open keeps native control of its own panel. Toggle with Grab Brightness Keys in the menu.
- A second slider (sun icons) appears in the dropdown, and brightness changes get their own HUD.
- All external displays move together for now. Intel Macs: the feature simply stays hidden.
The universal fallback — a hotkey bound to the CLI. Some keyboards can't be reached by key capture at all: boards with no brightness keys, a Bluetooth Magic Keyboard with Touch ID (it hides its special keys over BT), or keys rerouted by vendor software (Logitech G HUB / Options+). For every one of these, bind the permission-free CLI to any shortcut instead:
loudini brightness up # +6% (loudini brightness down / set 50 / get)DDC writes need no permission at all, so this works from macOS Shortcuts, Karabiner, BetterTouchTool, a Stream Deck key, or Raycast — see the appendix for a Karabiner recipe. This is the recommended path whenever the built-in key grab doesn't cover your keyboard.
scripts/install-daemon.sh # installs + starts; survives logout/reboot
scripts/uninstall-daemon.sh # clean removalLogs go to ~/.config/loudini/daemon.log.
scripts/install-cli.sh # symlinks the binary to ~/.local/bin/loudiniloudini up [step] volume += step (default 6), un-mutes
loudini down [step] volume -= step (default 6), un-mutes
loudini mute toggle mute
loudini set <0-100> set the volume
loudini get print: gain=42 muted=false running=true pipeline=true device="Scarlett 2i2 USB"
loudini apps list apps currently producing audio (from status.json)
loudini apps reset reset every per-app volume to 100% (clears overrides)
loudini app <id|name> set <0-100> | mute | get
set/toggle/read one app's volume (bundle id exact, name fuzzy)
loudini doctor diagnose the whole setup (daemon, permissions, conflicts) with fixes
loudini brightness up|down [step] | set <0-100> | get
external-monitor brightness over DDC — needs NO permission, bind it to any key
The volume subcommands (up/down/mute/set/apps/app) only write control.json and exit
instantly — the running daemon applies the change within 100 ms. brightness needs no daemon at all:
it talks to the display over DDC directly. doctor only reads, and exits non-zero when it finds
problems, so it is safe to use in a script's health check. Debug metering: LOUDINI_METER=1 loudini runs the daemon with per-second in/out RMS
logging (stop the normal daemon first — the single-instance lock makes a second one exit).
Build and install with the Stream Deck app running:
cd plugin
pnpm install && pnpm build
pnpm --package=@elgato/cli dlx streamdeck link gg.pim.loudini.sdPlugin # register with Stream DeckTwo JSON files in ~/.config/loudini/ are the whole API:
control.json—{"gain": 0-100, "muted": bool, "apps"?: {"<bundleID>": {"gain": 0-100, "muted": bool}}}. Write it (atomically: temp file in the same directory, then rename) and the daemon applies it within 100 ms. Malformed content is ignored — the daemon keeps its last good values. If you only owngain/muted, read-modify-write — never replace the document. Writing just{"gain":…,"muted":…}erases the wholeappsmap and every per-app volume with it.status.json— written by the daemon on every change:gain,muted— the applied level.running— daemon alive (falseafter a clean shutdown). Readers should also probepid: Loudini's own frontends treat the file asrunning:falsewhen that process is gone, so a hard-killed daemon can't leave a lying status behind.pipeline—trueonly when audio is actually being captured and re-rendered.running:true, pipeline:falsealmost always means the System Audio Recording permission is missing (orreason:"no-device": no output device;reason:"render stalled": the daemon tore the tap down after the render callbacks went quiet while apps were playing, and is rebuilding it).device— current output device name;pid— the daemon's process id;reason— why the pipeline is down (omitted when it's up).apps— the live roster of apps currently producing audio (read-only):[{bundleID,name,pid,gain,muted,active}]. Populated fromkAudioProcessPropertyIsRunningOutput;loudini appsprints it. Set one app withloudini app <id|name> set <0-100>|mute, clear all withloudini apps reset, or use the per-app rows under the master slider in the menu-bar app. SeeSPEC-per-app-volume.md.
Start with loudini doctor — it checks the config dir (with a real atomic write), the daemon
(via its lock file, immune to stale files), the audio pipeline, launchd, and known conflicting apps,
and prints the exact fix for anything broken.
- Volume keys do nothing, but Accessibility shows Loudini enabled. Stale grant after a rebuild — see the warning in Quick start. Menu → Repair Volume-Key Permission… fixes it in one click.
- Menu says "Audio capture not working — click to fix" / key faces show ⚠︎. The daemon can't create its tap — grant System Audio Recording to whichever app runs the daemon. Audio keeps playing normally until then (fail-open), and the volume keys deliberately fall through to macOS so you see the native crossed-out HUD instead of fake feedback.
- Keys work only sometimes. Another media-key app (MonitorControl, BeardedSpice) may grab them first — the menu warns when one is running. Disable its volume-key handling or launch Loudini after it.
- Menu-bar icon is dimmed. Loudini isn't controlling audio right now — open the menu; the broken row names the problem and is usually clickable as the fix.
- Brightness keys don't respond. They're only grabbed when an external DDC display is present AND no built-in display is active (lid closed / desktop Mac); check the Grab Brightness Keys toggle. The slider in the menu always works when the row is visible.
- WebKit apps share one per-app row. Safari and anything embedding WebKit (Citrix Workspace,
Zscaler, many Electron-ish wrappers) all play audio from a helper process reporting the same
bundle id,
com.apple.WebKit.GPU. Per-app volumes are keyed on that id, so if two of them play at once they collapse into a single row and share one level — and the row is named after whichever was seen last. Chrome is unaffected (it uses its own id). Splitting them needs a different key than the bundle id, whichstatus.json, the CLI and the menu-bar app all match on, so it is a deliberate breaking change rather than a quick fix. The master volume is unaffected either way — it applies to everything. - Virtual audio devices as your default output (Background Music, BlackHole routed as default, eqMac): quit/uninstall them. Loudini taps the default output and re-renders back to it; a virtual device that re-plays that audio creates a feedback loop. Loudini covers the same "software volume" job without a driver, so it replaces Background Music rather than running alongside it.
- System Audio Recording permission is granted per responsible process: under the LaunchAgent that's the daemon itself; when the Stream Deck app or the menu-bar app spawns the daemon, the prompt names that app instead. Grant it once per app identity (with ad-hoc signing that means again after each rebuild — see the Quick start warning; the stable "Loudini Dev" cert removes that).
- The macOS volume HUD stays the crossed-out one unless you use the menu-bar app (which replaces it with its own HUD) — Karabiner/BTT bindings change the volume without any HUD.
- AirPlay speakers work — as a system output. Pick an AirPlay device in Control Center → Sound and Loudini controls its volume like any other output (it follows the default output automatically). What it can't touch is in-app AirPlay — the AirPlay button inside a Safari video or the Music/TV app, which streams straight to the speaker and never passes through the Mac's audio output. No tap-based tool can reach that path.
Karabiner-Elements complex modification (volume keys → Loudini)
Paste into ~/.config/karabiner/assets/complex_modifications/loudini.json, then enable it in
Karabiner → Complex Modifications → Add rule:
{
"title": "Loudini volume keys",
"rules": [
{
"description": "Hardware volume keys → Loudini (F10/F11/F12 positions: mute/down/up)",
"manipulators": [
{
"type": "basic",
"from": { "consumer_key_code": "volume_increment", "modifiers": { "optional": ["any"] } },
"to": [{ "shell_command": "$HOME/.local/bin/loudini up" }]
},
{
"type": "basic",
"from": { "consumer_key_code": "volume_decrement", "modifiers": { "optional": ["any"] } },
"to": [{ "shell_command": "$HOME/.local/bin/loudini down" }]
},
{
"type": "basic",
"from": { "consumer_key_code": "mute", "modifiers": { "optional": ["any"] } },
"to": [{ "shell_command": "$HOME/.local/bin/loudini mute" }]
}
]
}
]
}If your keyboard sends plain function keys instead (you enabled "Use F1, F2, etc. keys as standard
function keys"), swap each from for { "key_code": "f12" } / "f11" / "f10".
Add external-monitor brightness the same way (Karabiner captures these keys at the HID level, below where the menu-bar app's tap loses them):
{
"type": "basic",
"from": { "consumer_key_code": "display_brightness_increment", "modifiers": { "optional": ["any"] } },
"to": [{ "shell_command": "$HOME/.local/bin/loudini brightness up" }]
},
{
"type": "basic",
"from": { "consumer_key_code": "display_brightness_decrement", "modifiers": { "optional": ["any"] } },
"to": [{ "shell_command": "$HOME/.local/bin/loudini brightness down" }]
}The same one-liners work anywhere that can run a shell command: BetterTouchTool (Execute Shell
Script → ~/.local/bin/loudini up), skhd (f12 : ~/.local/bin/loudini up), or a Raycast
script command.