Native macOS system audio capture via Core Audio tap - #127
Open
loganprosser wants to merge 1 commit into
Open
Conversation
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. The process-tap API only exists on macOS 14.4+, so a small dispatcher (AudioCaptureImpl_macOS) picks the backend at runtime: the Core Audio tap on 14.4+, falling back to the existing SDL microphone capture on older systems. The shared SDL backend is compiled a second time under a distinct class name so both can live in one binary; its sources are left untouched so the Linux build is unaffected. The tap hangs a global CATapDescription off a private aggregate device and pushes the PCM straight into projectM from the IO proc. Also links CoreAudio/AudioToolbox/Foundation on Darwin and adds the NSAudioCaptureUsageDescription key so the permission prompt has a message. Tested on macOS 26.5: on 14.4+ the 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.
loganprosser
force-pushed
the
macos-system-audio-pr
branch
from
July 28, 2026 13:51
964a03b to
13585b7
Compare
loganprosser
marked this pull request as ready for review
July 28, 2026 13:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.Backend selection / older systems. The process-tap API landed in macOS 14.4, so rather than dropping the existing mic path below that, macOS now selects a backend at runtime (
AudioCaptureImpl_macOS): the Core Audio tap on 14.4+, falling back to the current SDL microphone capture on older systems. The shared SDL backend is compiled a second time under a distinct class name (via aCOMPILE_DEFINITIONSrename insrc/CMakeLists.txt) so both can live in one binary — the SDL sources and the cross-platformAudioCaptureproxy are left completely untouched, so Windows and Linux are unaffected.Other changes are small: link CoreAudio/AudioToolbox/Foundation on Darwin, and add
NSAudioCaptureUsageDescriptionso the permission prompt has a description string.I tested it on macOS 26.5 (Apple silicon): the dispatcher selects the tap, which 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). Both backends compile and link into the one binary. 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.
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. Happy to adjust the structure if you'd prefer a different shape.
Authored with AI assistance.