Skip to content

Add a loopback input on macOS through a process tap - #86

Merged
chrisuthe merged 6 commits into
mainfrom
chrisuthe/task/add-a-loopback-input-on-macos
Oct 15, 2026
Merged

chrisuthe merged 6 commits into
mainfrom
chrisuthe/task/add-a-loopback-input-on-macos

Conversation

@chrisuthe

Copy link
Copy Markdown
Member

Stacked on #85. Merge that one first, then retarget this PR to main.

On macOS 14.2 or later, --input loopback captures everything the Mac plays, and loopback:<index|name> captures one output device, named the way -o coreaudio: names it. Both use a CoreAudio process tap that leaves out sendspin-cli's own playback, so a player on the same Mac is never sent back to the server. That is also why macOS gets no feedback warning.

Rather than adding a separate capture source, CoreAudioSource gets a loopback mode. It points its existing AUHAL input at a private aggregate device that holds the tap. The tap and aggregate are created and destroyed in a small coreaudio_tap.mm. Older macOS gets a clear error, and SDKs older than 14.2 still build.

macOS has no public API to ask for or check System Audio Recording permission, and a refusal just gives silence. So the start-up log says where to grant it (and tccutil reset AudioCapture to be asked again), and a tap that delivers only silence for 10 s logs one warning. A tap with nothing playing delivers no audio at all, so a quiet Mac does not set off the warning. NSAudioCaptureUsageDescription is added to the embedded Info.plist.

Tested on macOS 26 from Terminal:

  • A system sound is captured.
  • With only sendspin-cli's own tone playing, nothing is captured.
  • loopback:<speakers> captures, and an idle second output stays silent.
  • With permission refused, the silence warning fires once.
  • Changing the output rate mid-capture causes no reopen and no pitch shift.

ctest passes on Linux and macOS.

Part of Sendspin/backlog#72

--input loopback captures everything the Mac plays, and loopback:<device>
one output device, through a CoreAudio process tap read by the existing
AUHAL capture path via a private aggregate device. Both taps leave out
sendspin-cli's own playback. macOS 14.2 or later is required at runtime,
and older SDKs still build without it. A refused System Audio Recording
permission yields silence, so the start-up log says where to grant it and
a tap that delivers only silence logs one warning.
A failed open now releases its tap at once, the tap's format is watched
for a rate change on the tapped output, and errors and logs name the
loopback as typed rather than its private aggregate device.
@chrisuthe
chrisuthe added this pull request to stack #87 October 15, 2026 18:47
Capturing a loopback driver that the player also plays into sends the
server its own audio back, so the player logs one warning at startup.
@chrisuthe
chrisuthe marked this pull request as ready for review October 15, 2026 18:49
…dio-and' into chrisuthe/task/add-a-loopback-input-on-macos

# Conflicts:
#	README.md
#	docs/wiki/Configuration.md
#	src/audio_source.cpp
Base automatically changed from chrisuthe/task/add-a-loopback-input-on-pipewire-pulseaudio-and to main October 15, 2026 19:08
@chrisuthe
chrisuthe requested a balanced review from Copilot October 15, 2026 19:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Failed self-exclusion permits feedback, and tap-format notifications force reopening even when the capture format remains compatible.

2 open findings
What changed in this PR

Extends loopback capture to macOS using CoreAudio process taps within the existing audio-source backend.

Changes:

  • Adds system-wide and per-output loopback capture with self-playback exclusion.
  • Adds permission guidance, silence detection, and virtual-device feedback warnings.
  • Updates build integration, tests, and documentation.
File Description
tests/​audio_source_test.cpp Updates loopback availability and warning tests.
src/​main.cpp Warns about virtual-device feedback.
src/​coreaudio_tap.mm Implements process taps and aggregate-device lifecycle.
src/​coreaudio_tap.h Declares the process-tap interface.
src/​coreaudio_source.h Adds loopback state and helpers.
src/​coreaudio_source.cpp Integrates tap capture and silence detection.
src/​coreaudio_device.h Adds custom device-error labels.
src/​coreaudio_device.cpp Uses supplied device-error labels.
src/​cli.cpp Enables loopback help on macOS.
src/​audio_source.h Expands loopback availability and warning interfaces.
src/​audio_source.cpp Selects CoreAudio loopback and lists outputs.
README.md Documents platform-specific playback exclusion.
packaging/​sendspin-cli.Info.plist.in Adds system-audio capture permission text.
docs/​wiki/​Configuration.md Documents macOS loopback selectors.
docs/​wiki/​Advanced-Usage.md Explains macOS capture and permissions.
CMakeLists.txt Builds the Objective-C++ tap implementation.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/coreaudio_source.cpp Outdated
Comment thread src/coreaudio_tap.mm Outdated
…-format listener

The aggregate's own rate listener already catches a tapped output's rate
change, so watching the tap's format only added a second path to the
same reopen.
@chrisuthe
chrisuthe merged commit c7412fb into main Oct 15, 2026
19 checks passed
@chrisuthe chrisuthe mentioned this pull request Oct 15, 2026
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.

2 participants