Skip to content

feat(macOS): Capture audio on macOS using Tap API#4209

Open
ThomVanL wants to merge 32 commits intoLizardByte:masterfrom
ThomVanL:users/thomasvanlaere/feat-macos-ca-taps
Open

feat(macOS): Capture audio on macOS using Tap API#4209
ThomVanL wants to merge 32 commits intoLizardByte:masterfrom
ThomVanL:users/thomasvanlaere/feat-macos-ca-taps

Conversation

@ThomVanL
Copy link

@ThomVanL ThomVanL commented Aug 29, 2025

Description

This PR adds system-wide audio tap support for macOS. The implementation introduces:

  • System-wide audio tap functionality to capture audio from all system sources
    • Uses an audio converter to handle varying client audio requirements
  • Unit tests covering the new system tap methods, along with additional coverage for the existing microphone path
  • Updated UI with a macOS-specific toggle (see screenshots)
  • Updated configuration options to support the new audio tap feature
  • Added Doxygen documentation for new APIs

Additional changes

  • Adjusted cmake files for compatibility with Homebrew-based setup.
    • Tweaked dependency detection so that openssl and opus are found automatically. This replaces the need to run manual ln commands, but I’m not sure if this is the best long-term approach. Feedback welcome.
    • Updated cmake/compile_definitions/unix.cmake to ensure SUNSHINE_ASSETS_DIR resolves correctly.
  • Updated src_assets/macos/assets/Info.plist to prepare for required macOS permission prompts.
  • Added a macos_system_wide_audio_tap config option to the audio_t struct.

Testing

  • Verified functionality with multiple (Moonlight) clients requiring mono, stereo, 5.1, and 7.1 audio configurations.
  • Confirmed audio conversion works as expected across varying client setups.
  • Stress-tested with multiple concurrent clients for several hours without memory leaks or race conditions observed.
  • Host ran an arm64 build during testing

Notes

  • My background is primarily in .NET, with some experience in C, C++, and Rust. Objective-C is new to me, but I carefully reviewed and tested the bits on memory management and synchronization.
  • I leaned on GitHub Copilot and AI assistance heavily for the Objective-C parts, especially syntax and boilerplate; but I reviewed, debugged and tested everything myself a bunch of times.
  • Some of the unit tests were bordering on integration tests, so I tried to keep them focused and not blur the lines too much.
  • I developed this feature to make Sunshine more accessible on macOS, especially for less technical users. I have to admit that I fumbled quite a bit getting BlackHole to work. 🙂 But I also wanted to make sure to not break the existing setupMicrophone functionality as it is also a viable option!
  • I am not entirely convinced on the naming or location of the macos_system_wide_audio_tap setting.
    • Open to suggestions here!
  • Built this on arm64 macOS 15.6.1 (24G90)

If the AI involvement is a blocker for merging, no worries. I can totally understand! This was also a learning project for me and I had a lot of fun building it.

Screenshot

Web UI – Audio/Video Configuration
New option for enabling system-wide audio recording on macOS. Disables the audio sink option when checked.
a

macOS Permission Prompt
System permission request when Sunshine first tries to access system audio:
b

System Settings – Screen & System Audio Recording
macOS privacy settings showing Sunshine/Terminal access to "System Audio Recording Only":
c

Issues Fixed or Closed

Roadmap Issues

Type of Change

  • feat: New feature (non-breaking change which adds functionality)
  • fix: Bug fix (non-breaking change which fixes an issue)
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)
  • refactor: Code change that neither fixes a bug nor adds a feature
  • perf: Code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Other changes that don't modify src or test files
  • revert: Reverts a previous commit
  • BREAKING CHANGE: Introduces a breaking change (can be combined with any type above)

Checklist

  • Code follows the style guidelines of this project
  • Code has been self-reviewed
  • Code has been commented, particularly in hard-to-understand areas
  • Code docstring/documentation-blocks for new or existing methods/components have been added or updated
  • Unit tests have been added or updated for any new or modified functionality

AI Usage

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes

@ThomVanL ThomVanL changed the title Capture audio on macOS using Tap API feat(macOS): Capture audio on macOS using Tap API Aug 29, 2025
@ReenigneArcher

This comment was marked as resolved.

@ThomVanL

This comment was marked as resolved.

@ReenigneArcher

This comment was marked as resolved.

@ReenigneArcher

This comment was marked as resolved.

@codecov
Copy link

codecov bot commented Aug 30, 2025

Bundle Report

Changes will increase total bundle size by 121 bytes (0.01%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
sunshine-esm 965.15kB 121 bytes (0.01%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: sunshine-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/_plugin-*.js 121 bytes 343.43kB 0.04%

Files in assets/_plugin-*.js:

  • ./src_assets/common/assets/web/public/assets/locale/en.json → Total Size: 34.91kB

@codecov
Copy link

codecov bot commented Aug 30, 2025

Codecov Report

❌ Patch coverage is 0% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 12.09%. Comparing base (eb72930) to head (f667865).
⚠️ Report is 8 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/audio.cpp 0.00% 3 Missing ⚠️
src/platform/linux/audio.cpp 0.00% 1 Missing ⚠️
src/platform/windows/audio.cpp 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4209      +/-   ##
==========================================
- Coverage   12.09%   12.09%   -0.01%     
==========================================
  Files          87       87              
  Lines       17612    17613       +1     
  Branches     8097     8097              
==========================================
  Hits         2131     2131              
- Misses      14579    14580       +1     
  Partials      902      902              
Flag Coverage Δ
Linux-AppImage 11.62% <0.00%> (-0.01%) ⬇️
Windows-AMD64 13.41% <0.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/config.h 0.00% <ø> (ø)
src/platform/common.h 24.13% <ø> (ø)
src/platform/linux/audio.cpp 10.63% <0.00%> (ø)
src/platform/windows/audio.cpp 25.13% <0.00%> (ø)
src/audio.cpp 21.89% <0.00%> (-0.17%) ⬇️

@andygrundman
Copy link
Contributor

Thanks for the PR. I have built this on my M1 Pro MBP and have been trying to get it to work, but haven't had much success so far. I am not able to get any sound to be captured and/or sent. So far I am only testing with the simplest use case of audio playing out of my MBP speakers. I edited the code to make the tap and aggregate non-private, so I could try to view the tap/aggregate with Apple's sample app I can see the tap, but not the aggregate. I'm not sure what's wrong. Can you detail your testing process?

I do seem to be getting OK log entries and since I'm running from iTerm, all my permissions seem to be in order (iTerm has access to many things).

[2025-08-29 23:56:39.783]: Info: Detected display: Built-in Retina Display (id: 1) connected: true
[2025-08-29 23:56:39.783]: Info: Configuring selected display (1) to stream
[2025-08-29 23:56:39.841]: Info: Creating encoder [hevc_videotoolbox]
[2025-08-29 23:56:39.841]: Info: Color coding: SDR (Rec. 601)
[2025-08-29 23:56:39.841]: Info: Color depth: 10-bit
[2025-08-29 23:56:39.841]: Info: Color range: MPEG
[2025-08-29 23:56:39.841]: Info: Streaming bitrate is 55987000
[2025-08-29 23:56:39.850]: Info: [hevc_videotoolbox @ 0x143e60e70] This device does not support the max_ref_frames option. Value ignored.
[2025-08-29 23:56:40.712]: Info: Using macOS system audio tap for capture.
[2025-08-29 23:56:40.712]: Info: Sample rate: 48000, Frame size: 240, Channels: 2
[2025-08-29 23:56:40.738]: Info: Aggregate device created with ID: 203
[2025-08-29 23:56:40.738]: Info: Aggregate device created and configured successfully
[2025-08-29 23:56:40.739]: Info: No conversion needed - formats match (device: 48000Hz/2ch)
[2025-08-29 23:56:40.739]: Info: Device properties and converter configuration completed
[2025-08-29 23:56:40.793]: Info: System tap IO proc created and started successfully
[2025-08-29 23:56:40.793]: Info: Audio buffer initialized successfully with size: 8192 bytes
[2025-08-29 23:56:40.793]: Info: System tap setup completed successfully!
[2025-08-29 23:56:40.793]: Info: macOS system audio tap capturing.
[2025-08-29 23:56:40.794]: Info: Opus initialized: 48 kHz, 2 channels, 512 kbps (total), LOWDELAY

I had to make the following changes to get it to build:

-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wunguarded-availability-new"
-    AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone, AVCaptureDeviceTypeExternalUnknown]
+    AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeMicrophone, AVCaptureDeviceTypeExternal]
                                                                                                                mediaType:AVMediaTypeAudio
                                                                                                                 position:AVCaptureDevicePositionUnspecified];
     NSArray *devices = discoverySession.devices;
     BOOST_LOG(debug) << "Found "sv << [devices count] << " devices using discovery session"sv;
     return devices;
-#pragma clang diagnostic pop

plus a fix in our input.cpp that breaks the latest Xcode 16.4, I'm surprised if you didn't run into this one. I am running Xcode 16.4 clang-1700.0.13.5).

--- a/src/platform/macos/input.cpp
+++ b/src/platform/macos/input.cpp
@@ -534,7 +534,7 @@ const KeyCodeMap kKeyCodesMap[] = {
     if (!output_name.empty()) {
       uint32_t max_display = 32;
       uint32_t display_count;
-      CGDirectDisplayID displays[max_display];
+      CGDirectDisplayID displays[32];

@ReenigneArcher
Copy link
Member

Posting this here for reference in case it's needed for permissions of unit tests. We used to need something like this for macports, but it was never necessary for homebrew.

- name: Fix permissions
run: |
# https://apple.stackexchange.com/questions/362865/macos-list-apps-authorized-for-full-disk-access
# https://github.com/actions/runner-images/issues/9529
# https://github.com/actions/runner-images/pull/9530
# function to execute sql query for each value
function execute_sql_query {
local value=$1
local dbPath=$2
echo "Executing SQL query for value: $value"
sudo sqlite3 "$dbPath" "INSERT OR IGNORE INTO access VALUES($value);"
}
# Find all provisioner paths and store them in an array
readarray -t provisioner_paths < <(sudo find /opt /usr -name provisioner)
echo "Provisioner paths: ${provisioner_paths[@]}"
# Create an empty array
declare -a values=()
# Loop through the provisioner paths and add them to the values array
for p_path in "${provisioner_paths[@]}"; do
# Adjust the service name and other parameters as needed
values+=("'kTCCServiceAccessibility','${p_path}',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,NULL,1592919552")
values+=("'kTCCServiceScreenCapture','${p_path}',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159")
done
echo "Values: ${values[@]}"
if [[ "${{ matrix.os_version }}" == "14" ]]; then
# TCC access table in Sonoma has extra 4 columns: pid, pid_version, boot_uuid, last_reminded
for i in "${!values[@]}"; do
values[$i]="${values[$i]},NULL,NULL,'UNUSED',${values[$i]##*,}"
done
fi
# system and user databases
dbPaths=(
"/Library/Application Support/com.apple.TCC/TCC.db"
"$HOME/Library/Application Support/com.apple.TCC/TCC.db"
)
for value in "${values[@]}"; do
for dbPath in "${dbPaths[@]}"; do
echo "Column names for $dbPath"
echo "-------------------"
sudo sqlite3 "$dbPath" "PRAGMA table_info(access);"
echo "Current permissions for $dbPath"
echo "-------------------"
sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';"
execute_sql_query "$value" "$dbPath"
echo "Updated permissions for $dbPath"
echo "-------------------"
sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';"
done
done

@ThomVanL
Copy link
Author

Hey @andygrundman, thanks for taking the time to test the PR. Sorry to hear it’s not working correctly.

Here’s my setup on an M4 MBP and what I did.

clang --version
Apple clang version 17.0.0 (clang-1700.0.13.5)
Target: arm64-apple-darwin24.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

I made the changes in VS Code and followed the steps in building.md:

mkdir build
cmake -B build -G Ninja -S . 
ninja -C build

For testing I did the following steps:

  • In the VS Code terminal: ./build/sunshine
  • Accept the allow connections prompt
  • Connect with multiple Moonlight clients with the following sample rate/frame size/channel combos:
    • iPad @ 1080p → 48000Hz/240/2ch audio → works
    • Android TV @ 1080p → 48000Hz/240/8ch audio → works
    • iOS @ 360p → 48000Hz/480/2ch audio → works

So at one point I'm running three streams simultaneously and the audio plays through the devices.

My sunshine.conf looks like:

audio_sink = Steam Streaming Speakers
macos_system_wide_audio_tap = true
origin_web_ui_allowed = pc
stream_audio = true
wan_encryption_mode = 2

With min_log_level = debug, here’s the output when connecting from the 360p device (trimmed for brevity).

[2025-08-30 10:42:27.456]: Info: CLIENT CONNECTED
[2025-08-30 10:42:27.463]: Debug: Start capturing Video
[2025-08-30 10:42:27.463]: Info: Detecting displays
[2025-08-30 10:42:27.463]: Info: Detected display: Built-in Retina Display (id: 1) connected: true
[2025-08-30 10:42:27.463]: Info: Configuring selected display (1) to stream
[2025-08-30 10:42:27.541]: Info: Creating encoder [hevc_videotoolbox]
[2025-08-30 10:42:27.541]: Info: Color coding: SDR (Rec. 601)
[2025-08-30 10:42:27.541]: Info: Color depth: 8-bit
[2025-08-30 10:42:27.541]: Info: Color range: MPEG
[2025-08-30 10:42:27.541]: Info: Streaming bitrate is 1268000
[2025-08-30 10:42:27.550]: Info: [hevc_videotoolbox @ 0x13d01de00] This device does not support the max_ref_frames option. Value ignored.
[2025-08-30 10:42:27.945]: Debug: Start capturing Audio
[2025-08-30 10:42:27.946]: Warning: audio_control_t::set_sink() unimplemented: Steam Streaming Speakers
[2025-08-30 10:42:27.946]: Info: Using macOS system audio tap for capture.
[2025-08-30 10:42:27.946]: Info: Sample rate: 48000, Frame size: 480, Channels: 2
[2025-08-30 10:42:27.946]: Debug: setupSystemTap called with sampleRate:48000 frameSize:480 channels:2
[2025-08-30 10:42:27.946]: Debug: macOS version check passed (running 15.6.1)
[2025-08-30 10:42:27.946]: Debug: System tap initialization completed
[2025-08-30 10:42:27.946]: Debug: Creating tap description for 2 channels
[2025-08-30 10:42:27.946]: Debug: Creating process tap with name: SunshineAVAudio-Tap-0x6000007d00c0
[2025-08-30 10:42:27.949]: Debug: AudioHardwareCreateProcessTap returned status: 0
[2025-08-30 10:42:27.949]: Debug: Process tap created successfully with ID: 140
[2025-08-30 10:42:27.949]: Debug: Creating aggregate device with tap UID: 675EC59C-D7CC-4A25-A093-F2B4B4227895
[2025-08-30 10:42:27.957]: Debug: AudioHardwareCreateAggregateDevice returned status: 0
[2025-08-30 10:42:27.957]: Info: Aggregate device created with ID: 141
[2025-08-30 10:42:27.959]: Debug: Set aggregate device sample rate to 48000Hz
[2025-08-30 10:42:27.959]: Debug: Set aggregate device buffer size to 480 frames
[2025-08-30 10:42:27.959]: Info: Aggregate device created and configured successfully
[2025-08-30 10:42:27.960]: Debug: Device reports 2 input channels
[2025-08-30 10:42:27.960]: Debug: Device properties - Sample Rate: 48000Hz, Channels: 2
[2025-08-30 10:42:27.960]: Debug: needsConversion: NO (device: 48000Hz/2ch -> client: 48000Hz/2ch)
[2025-08-30 10:42:27.960]: Info: No conversion needed - formats match (device: 48000Hz/2ch)
[2025-08-30 10:42:27.960]: Info: Device properties and converter configuration completed
[2025-08-30 10:42:27.960]: Debug: Creating IOProc for aggregate device ID: 141
[2025-08-30 10:42:27.977]: Debug: AudioDeviceCreateIOProcID returned status: 0
[2025-08-30 10:42:27.978]: Debug: Starting IOProc for aggregate device
[2025-08-30 10:42:27.995]: Debug: AudioDeviceStart returned status: 0
[2025-08-30 10:42:27.995]: Info: System tap IO proc created and started successfully
[2025-08-30 10:42:27.995]: Debug: Initializing audio buffer for 2 channels
[2025-08-30 10:42:27.995]: Info: Audio buffer initialized successfully with size: 8192 bytes
[2025-08-30 10:42:27.995]: Info: System tap setup completed successfully!
[2025-08-30 10:42:27.995]: Info: macOS system audio tap capturing.
[2025-08-30 10:42:27.995]: Info: Opus initialized: 48 kHz, 2 channels, 96 kbps (total), LOWDELAY

I’m familiar with the sample app! It might be the case that the aggregate device settings are probably still marked as private. You’ll need to flip those to NO in two places, once for the tap description and once for the aggregate device.

[tapDescription setPrivate:YES];

@kAudioAggregateDeviceIsPrivateKey: @YES,

Then the tap will show up in Apple's sample app's UI.

Screenshot 2025-08-30 at 10 56 15

And the aggregate device shows up as well.

Screenshot 2025-08-30 at 10 56 22

But even with those values flipped to YES, I can still hear audio on the 360p device!

@andygrundman
Copy link
Contributor

Thanks for the detailed info. I forgot my log only had Info level, when using Debug my log does look exactly like yours. I feel like I must just have a permission issue, maybe using iTerm as the permission "owner" isn't correct.

Do you see both of these items? What process names are they using? I only get a System Audio item when recording a test file in AudioTapSample.
permissions

Here's basically what my Screen & System Audio Recording settings look like:
privacy

If this is the issue, I wonder how Sunshine can detect that it doesn't actually have permission, hmm.

@ThomVanL
Copy link
Author

ThomVanL commented Aug 30, 2025

You're right, because I ran into a similar issue with VS Code. Granting it "Screen & System Audio Recording" wasn’t enough; I had to explicitly allow "System Audio Recording Only." From what I found online, the VS Code app bundle itself might be causing the problem. I also tried running tccutil reset All to clear permissions, but the behavior stayed the same. I had to explicitly add permissions, but only for VS Code.

Screenshot 2025-08-30 at 13 26 59

I did not even notice the little privacy notice at the top until just now, thanks for that. Here's what it's like on my end.

Screenshot 2025-08-30 at 13 28 52

When I launched ./build/sunshine directly from the macOS Terminal (not iTerm), I did get the prompt mentioned in my initial message on this PR.

Edit: just to be clear, my dev loop consists of launching sunshine builds through the VS Code integrated terminal.

@ThomVanL
Copy link
Author

Overall I think this is a well done patch, and is a good example of how to do an AI-assisted PR.

I originally was worried about the frequent use of manual release, e.g.

[audioInput release];
[audioOutput release];

but then I learned that ObjC built by cmake doesn't use the newer ARC memory management model that I was more familiar with from developing in Xcode. ARC dates from 2012 in OSX Lion if you can believe it, but since devs had to opt into using ARC, nobody ever did for this code. So, I guess that's a point for AI here. I would have probably not thought about this and been quite confused at all the memory leaks I was creating. Or more likely it would just result in a lot of compiler errors. (In -fobjc-arc mode, these kinds of release calls are compiler errors.)

Yeah, I wondered about ARC too and did a quick search for -fobjc-arc and @autoreleasepool usage. Everything pointed to ARC not being enabled, so I followed the existing pattern of manual release calls.

Good to hear that lines up with your findings as well.

@ThomVanL ThomVanL force-pushed the users/thomasvanlaere/feat-macos-ca-taps branch from d75fde0 to db3d2df Compare November 3, 2025 21:00
@sonarqubecloud
Copy link

@ThomVanL
Copy link
Author

Hi @ReenigneArcher and @andygrundman, happy new year! 🎉

Also, my apologies for the delay in regards to this PR. I've removed some of the unit tests that turned out to be integration tests in disguise (particularly the TCC ones). After poking around for best practices on running integration tests in a CI/CD pipeline with TCC, I mostly found workarounds that felt a little hacky, so I decided to strip those integration tests out and stick with pure unit tests instead. Not sure if that sits right with you both, but feel free to let me know if it needs to be changed.

@andygrundman

This comment was marked as outdated.

@andygrundman
Copy link
Contributor

andygrundman commented Jan 20, 2026

This is looking pretty good, thanks! What I did was create a branch from yours, cherry-pick in some helper things (my build script, input fixes, my previous compile fix (how does this build for you in Tahoe?), and moved a bunch of noisy logging from debug to verbose). With all that out of the way, the main commit in my branch has a bunch of improvements for you to consider. Here's the items in that commit:

https://github.com/andygrundman/Sunshine/commits/andyg.feat-macos-ca-taps-review/

* Refactor microphone.mm sample() to memcpy the buffer directly instead of copying twice by using std::vector, and optimize the case where avail < neededBytes.
* Move initializeAudioBuffer before anything can use the buffer.
* Change size of ring buffer to 30ms (6 * 240 * channels * sizeof(float)).
* Avoid confusion in converter setup between float and int samplerate.
* Add helper for logging CoreAudio error codes in FourCC text mode. Remove some redundant logging, and always log the exact API call that is failing.
* Add optional way to create the aggregate in public mode so it's visible in Audio MIDI Setup/HALLab. Set SUNSHINE_PUBLIC_AUDIO_TAP=1.

I'm not married to these changes, so feel free to counter any of them. I'm not sure what the right buffer size should be, but I felt like 4096 * channels seemed small and just as arbitrary. The public tap thing is neat to look at, especially check out the Apple utility HALLab which exposes all the internals of audio devices. I worry that public taps may not get cleaned up in some situations such as a crash, although maybe private taps get automatically cleaned up. Using an env var for devs is a bit ugly but there's no other way to view the aggregate device.

Re: resampling: do we need to have so much or any resampling code? Doesn't CoreAudio handle a lot of this behind the scenes? I couldn't get the system tap to ever need resampling (I think because it's not using a specific device), and when using BlackHole 16ch sink directly, the resampling code doesn't seem to be called anyway. Even if I run BlackHole at 192k or something, it seems to work fine. I did a trace in Activity Monitor and you can find the bit where the OS is resampling from 192k -> 48k if you search for acv2::Resampler2Wrapper::ProduceOutput. I just need some help on how to test your resampling code, since I couldn't get it to use the code path with procData->audioConverter.

This all works quite well for stereo. For surround, I guess we need to think about it some more. I just stumbled across a project called SoundPusher that creates a virtual 5.1 device and does on-demand AC3 encoding. I want to study it some more. https://codeberg.org/q-p/SoundPusher I found it via https://www.maven.de/2025/04/coreaudio-taps-for-dummies/ by the way.

Edit: I think I just realized the conversion code is mostly there for multichannel right? I honestly have not even tested surround because I think that still requires a virtual audio device like what SoundPusher includes, or BlackHole. I'm not sure it works with Tap. This PR should probably just focus on making stereo taps work in the most user-friendly way possible.

@andygrundman
Copy link
Contributor

Another edit: I had a few moments of panic while testing this, you know the feeling: the audio is skipping and glitching out real bad, and your first thought is there's some bug in the realtime buffer code. Nope, it's all just AWDL. This code basically becomes unusable on wifi devices that don't have N1 chips. The iPad I have been using as a client still had 6ghz 6E mode enabled which is a death sentence for Moonlight. My N1-equipped iPhone Air streams it perfectly. Thanks Apple.

@LizardByte LizardByte deleted a comment from sonarqubecloud bot Feb 3, 2026
@LizardByte LizardByte deleted a comment from sonarqubecloud bot Feb 3, 2026
Comment on lines 20 to 30
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
${APP_KIT_LIBRARY}
${APP_SERVICES_LIBRARY}
${AV_FOUNDATION_LIBRARY}
${CORE_MEDIA_LIBRARY}
${CORE_VIDEO_LIBRARY}
${FOUNDATION_LIBRARY}
${AUDIO_TOOLBOX_LIBRARY}
${AUDIO_UNIT_LIBRARY}
${CORE_AUDIO_LIBRARY}
${VIDEO_TOOLBOX_LIBRARY})
Copy link
Member

Choose a reason for hiding this comment

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

Could this list be alphabetized?

Comment on lines +21 to +23
if(OPENSSL_FOUND)
include_directories(SYSTEM ${OPENSSL_INCLUDE_DIR})
endif()
Copy link
Member

@ReenigneArcher ReenigneArcher Feb 3, 2026

Choose a reason for hiding this comment

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

This shouldn't be necessary?

${OPENSSL_LIBRARIES}

Comment on lines +20 to +21
include_directories(/opt/homebrew/opt/opus/include)
link_directories(/opt/homebrew/opt/opus/lib)
Copy link
Member

Choose a reason for hiding this comment

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

These paths depend on the architecture of homebrew, and can't be hardcoded.

Sunshine supports native system audio capture on macOS 14.0 (Sonoma) and newer via Apple’s Audio Tap API.
To use it, simply leave the **Audio Sink** setting blank.

If you are running macOS 13 (Ventura) or earlier—or if you prefer to manage your own loopback device—you can still use [Soundflower](https://github.com/mattingalls/Soundflower) or [BlackHole](https://github.com/ExistentialAudio/BlackHole) and enter its device name in the **Audio Sink** field.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
If you are running macOS 13 (Ventura) or earlier—or if you prefer to manage your own loopback device—you can still use [Soundflower](https://github.com/mattingalls/Soundflower) or [BlackHole](https://github.com/ExistentialAudio/BlackHole) and enter its device name in the **Audio Sink** field.
If you prefer to manage your own loopback device—you can still use [Soundflower](https://github.com/mattingalls/Soundflower) or [BlackHole](https://github.com/ExistentialAudio/BlackHole) and enter its device name in the **Audio Sink** field.

macos-13 isn't supported anymore

Comment on lines +197 to 203
Sunshine now supports system audio capture natively on macOS 14.0 (Sonoma) and later,
using the built-in Core Audio Tap API.

On macOS 13 or earlier, or if you prefer a virtual loopback device,
you can still use "Soundflower" or "BlackHole" for system audio capture.

Gamepads are not currently supported on macOS.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Sunshine now supports system audio capture natively on macOS 14.0 (Sonoma) and later,
using the built-in Core Audio Tap API.
On macOS 13 or earlier, or if you prefer a virtual loopback device,
you can still use "Soundflower" or "BlackHole" for system audio capture.
Gamepads are not currently supported on macOS.
Gamepads are not currently supported on macOS.

Since we don't need to worry about macos-13 now, and audio will work out of the box... we can remove the comment about audio altogether.

@@ -8,5 +8,8 @@
<string>Sunshine</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your microphone to stream audio.</string>
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this comment should be updated?

venv/

# Caches
.cache/
Copy link
Member

Choose a reason for hiding this comment

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

What, specifically, is generating this directory?

@@ -0,0 +1,388 @@
/**
* @file tests/unit/platform/test_macos_av_audio.mm
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* @file tests/unit/platform/test_macos_av_audio.mm
* @file tests/unit/platform/macos/test_av_audio.mm

Can you rename the file to be in a macos specific folder. We also have a folder for windows there now as well.

TEST_F(AVAudioTest, ObjectLifecycle) {
AVAudio *avAudio = [[AVAudio alloc] init];
EXPECT_NE(avAudio, nil); // Should create successfully
[avAudio release]; // Should not crash
Copy link
Member

Choose a reason for hiding this comment

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

If this can crash, I think it should be wrapped in a try/catch. If we hit the catch, then it should trigger GTEST to fail.

Same for anywhere else it could crash. This is because when a test crashes it crashes the whole test run.

"audio_sink": "Audio Sink",
"audio_sink_desc_linux": "The name of the audio sink used for Audio Loopback. If you do not specify this variable, pulseaudio will select the default monitor device. You can find the name of the audio sink using either command:",
"audio_sink_desc_macos": "The name of the audio sink used for Audio Loopback. Sunshine can only access microphones on macOS due to system limitations. To stream system audio using Soundflower or BlackHole.",
"audio_sink_desc_macos": "The name of the audio sink used for Audio Loopback. Leave this blank to use the built-in system audio capture (requires macOS 14.0 or later, using the Core Audio Tap API). Alternatively, specify a virtual device such as Soundflower or BlackHole if you prefer or are running an older version of macOS.",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"audio_sink_desc_macos": "The name of the audio sink used for Audio Loopback. Leave this blank to use the built-in system audio capture (requires macOS 14.0 or later, using the Core Audio Tap API). Alternatively, specify a virtual device such as Soundflower or BlackHole if you prefer or are running an older version of macOS.",
"audio_sink_desc_macos": "The name of the audio sink used for Audio Loopback. Leave this blank to use the built-in system audio capture. Alternatively, specify a virtual device such as Soundflower or BlackHole if you prefer or are running an older version of macOS.",

@ReenigneArcher ReenigneArcher added roadmap This PR closes a roadmap entry ai PR has signs of heavy ai usage (either indicated by user or assumed) labels Feb 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai PR has signs of heavy ai usage (either indicated by user or assumed) roadmap This PR closes a roadmap entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sunshine: Capture audio on macOS using Tap API

3 participants