Skip to content

Native macOS system audio capture via Core Audio tap#127

Draft
loganprosser wants to merge 1 commit into
projectM-visualizer:masterfrom
loganprosser:macos-system-audio-pr
Draft

Native macOS system audio capture via Core Audio tap#127
loganprosser wants to merge 1 commit into
projectM-visualizer:masterfrom
loganprosser:macos-system-audio-pr

Conversation

@loganprosser

Copy link
Copy Markdown

On macOS the SDL capture backend can only read a microphone, so if you want projectM to react to whatever's actually playing (Spotify, a YouTube tab, etc.) you have to install BlackHole or another virtual device and reroute your output through it. Windows already avoids that with the WASAPI loopback backend. This does the macOS equivalent using a Core Audio process tap, so system audio just works with no extra driver.

How it works: it creates a global CATapDescription, attaches it to a private aggregate device, and forwards the PCM into projectM from the IO proc. Nothing exotic beyond the tap plumbing itself.

The whole tap setup is behind if (@available(macOS 14.4, *)) (the process-tap API landed in 14.4), so on older systems the symbols weak-link and it just logs a message instead of crashing.

Other changes are small: link CoreAudio/AudioToolbox/Foundation on Darwin, and add NSAudioCaptureUsageDescription so the permission prompt has a description string.

I tested it on macOS 26.5 (Apple silicon). The tap comes up at 48kHz stereo and only produces samples while something is actually playing (checked with a temporary peak meter — flat at idle, jumps as soon as audio plays). One gotcha worth noting: capture only works if the app bundle is signed, because the system ties the audio-capture permission to the signing identity. Ad-hoc signing is enough for local builds.

One thing I want your call on before polishing: right now this replaces the SDL backend on macOS entirely, so on < 14.4 there's no mic fallback, it just idles. Since there's no declared minimum macOS in the project, I can instead build both backends on macOS and pick at runtime (tap on 14.4+, SDL mic below that) if you'd prefer not to drop the old path. Happy to go either way.

Marking this a draft since that decision affects the shape of the code. Follow-ups I left out on purpose: per-app capture, a richer device list that includes inputs, handling default-output-device changes, and recovering from the known "all zeros after a long session" tap bug.

On macOS the SDL backend can only grab a microphone, so to visualize whatever's
actually playing you currently have to install BlackHole (or similar) and reroute
your output through it. This adds a Core Audio process-tap backend that captures
system output directly, the same idea as the existing WASAPI loopback path on
Windows.

It hangs a global CATapDescription off a private aggregate device and pushes the
PCM straight into projectM from the IO proc. The tap setup is wrapped in
@available(macOS 14.4, *) so it weak-links and just logs instead of crashing on
older systems.

Also links CoreAudio/AudioToolbox/Foundation on Darwin and adds the
NSAudioCaptureUsageDescription key so the permission prompt has a message.

Tested on macOS 26.5: tap comes up at 48kHz stereo and only produces audio while
something is actually playing. Capture needs the bundle to be code-signed since
the permission is tied to the signing identity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant