Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5bd8e54
wip(macos): add system-wide audio tap support
ThomVanL Aug 28, 2025
74c2cf1
feat(macos): some unit tests
ThomVanL Aug 28, 2025
0c2e096
wip(macos): converter creation now queries device to get accurate info
ThomVanL Aug 28, 2025
48ef28d
wip(macos): refactored setupSystemTap and split into methods
ThomVanL Aug 28, 2025
05c76c1
wip(macos): more unit tests to cover refactored system tap methods
ThomVanL Aug 28, 2025
d594c06
wip(macos): nullability and cleanup return types
ThomVanL Aug 29, 2025
2b0ba99
wip(macos): NS_ASSUME_NONNULL_BEGIN should be included
ThomVanL Aug 29, 2025
1dc8217
wip(macos): added some info log statements
ThomVanL Aug 29, 2025
1a3bc52
wip(macos): doxygen documentation
ThomVanL Aug 29, 2025
cab7da6
wip(macos): added cleanupSystemTapContext instance method to header a…
ThomVanL Aug 29, 2025
a729f78
wip(macos): renamed instance method
ThomVanL Aug 29, 2025
b29445e
fix(macos): add macOS-specific test files only when building tests fo…
ThomVanL Aug 29, 2025
9762651
fix(cmake): add missing newline
ThomVanL Aug 29, 2025
13bc467
style: format C++ code with clang-format
ThomVanL Aug 29, 2025
5aa03e2
fix(macos): improve nil-safety in av_audio microphone code-path
ThomVanL Aug 30, 2025
a847d1f
style(windows): code incorrectly formatted
ThomVanL Aug 30, 2025
6404705
revert: style(windows): code incorrectly formatted
ThomVanL Aug 30, 2025
faa1170
wip(macos): refactor ioprocs to c/c++.
ThomVanL Sep 2, 2025
fc3609b
style(macos): formatting
ThomVanL Sep 2, 2025
b056036
refactor(macos): simplify audio tap to always use stereo configuration
ThomVanL Sep 2, 2025
2c40470
feat(audio): Core Audio tap mute behavior for macOS host audio control.
ThomVanL Sep 3, 2025
1ba9208
fix(audio): mark unused host_audio_enabled parameter as [[maybe_unused]]
ThomVanL Sep 3, 2025
44407a4
Merge branch 'master' into users/thomasvanlaere/feat-macos-ca-taps
ThomVanL Nov 3, 2025
3ce6572
refactor(config): macos_system_wide_audio_tap removed
ThomVanL Nov 3, 2025
fe2ef3f
fix(macos): correct minimum macOS version for Core Audio taps from 14…
ThomVanL Nov 3, 2025
186c21c
feat(macos): use system audio tap on macOS 14+ and update related docs
ThomVanL Nov 3, 2025
db3d2df
fix(audio): add missing host_audio_enabled parameter to Linux audio.
ThomVanL Nov 3, 2025
f667865
fix: gitignore did not end with a newline character.
ThomVanL Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ package-lock.json
# Python
*.pyc
venv/


.cache/
5 changes: 4 additions & 1 deletion cmake/compile_definitions/macos.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
${CORE_MEDIA_LIBRARY}
${CORE_VIDEO_LIBRARY}
${FOUNDATION_LIBRARY}
${AUDIO_TOOLBOX_LIBRARY}
${AUDIO_UNIT_LIBRARY}
${CORE_AUDIO_LIBRARY}
${VIDEO_TOOLBOX_LIBRARY})

set(APPLE_PLIST_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/Info.plist")
Expand All @@ -33,7 +36,7 @@ set(SUNSHINE_TRAY 0)

set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.h"
"${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.m"
"${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.mm"
"${CMAKE_SOURCE_DIR}/src/platform/macos/av_img_t.h"
"${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.h"
"${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.m"
Expand Down
3 changes: 2 additions & 1 deletion cmake/compile_definitions/unix.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
${CURL_LIBRARIES})

# add install prefix to assets path if not already there
if(NOT SUNSHINE_ASSETS_DIR MATCHES "^${CMAKE_INSTALL_PREFIX}")
# Skip prefix addition for absolute paths or development builds
if(NOT SUNSHINE_ASSETS_DIR MATCHES "^/" AND NOT SUNSHINE_ASSETS_DIR MATCHES "^${CMAKE_INSTALL_PREFIX}")
set(SUNSHINE_ASSETS_DIR "${CMAKE_INSTALL_PREFIX}/${SUNSHINE_ASSETS_DIR}")
endif()
3 changes: 3 additions & 0 deletions cmake/dependencies/common.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libdisplaydevice")
# common dependencies
include("${CMAKE_MODULE_PATH}/dependencies/nlohmann_json.cmake")
find_package(OpenSSL REQUIRED)
if(OPENSSL_FOUND)
include_directories(SYSTEM ${OPENSSL_INCLUDE_DIR})
endif()
find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED)
pkg_check_modules(CURL REQUIRED libcurl)
Expand Down
8 changes: 8 additions & 0 deletions cmake/dependencies/macos.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox)
if(SUNSHINE_ENABLE_TRAY)
FIND_LIBRARY(COCOA Cocoa REQUIRED)
endif()

# Audio frameworks required for audio capture/processing
FIND_LIBRARY(AUDIO_TOOLBOX_LIBRARY AudioToolbox)
FIND_LIBRARY(AUDIO_UNIT_LIBRARY AudioUnit)
FIND_LIBRARY(CORE_AUDIO_LIBRARY CoreAudio)

include_directories(/opt/homebrew/opt/opus/include)
link_directories(/opt/homebrew/opt/opus/lib)
25 changes: 25 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,31 @@ editing the `conf` file in a text editor. Use the examples as reference.
</tr>
</table>

### macos_system_wide_audio_tap
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason this needs to be a setting? The main point of confusion with users will be the fact that you don't link your capture to an audio device, the way you do on Windows. If we just defaulted to this, is there a downside?

Just FYI, when I was first playing around with the Tap API I went about it using the

CATapDescription *tapDesc = [[CATapDescription alloc] initExcludingProcesses:toExclude
                                                                andDeviceUID:audioSinkUID
                                                                  withStream:0];

version which used the default audio device. I never tried the system audio tap which I suppose works better. For example system tap captures audio even when the default audio device is muted, I think device capture would not capture audio in this case, but I haven't tested it.

Copy link
Author

Choose a reason for hiding this comment

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

I kept it as a setting to avoid breaking existing setups (users relying on BlackHole or similar). This way an update wouldn’t disrupt their workflow. That said, I'm happy to defer to you and the other maintainers on whether it makes sense to just default to the system tap.

Also, I’m not entirely sure initStereoGlobalTapButExcludeProcesses is the best long-term choice, since it enforces stereo and might complicate 5.1/7.1 setups? I don’t have a great way to test multichannel properly, so I’d appreciate your input here.

Copy link
Member

Choose a reason for hiding this comment

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

Without looking at the code super closely, I believe it should just use the Tap API if audio/virtual sink are unset. If those are set then use whatever they are set to. With this approach their setup will not change as they had to manually set blackhole or whatever.


<table>
<tr>
<td>Description</td>
<td colspan="2">
@tip{Overrides Audio Sink settings.}
Toggles the creation of a system-wide audio tap that captures outgoing audio from all processes.
This tap can act as an input in a HAL aggregate device, like a virtual microphone.
@note{Requirement: macOS 14.2 or later.}
@attention{macOS Privacy Settings: The user must add Terminal or Sunshine to <strong>Privacy & Security &gt; Screen & System Audio Recording &gt; System Audio Recording Only</strong> in System Settings.}
</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">disabled</td>
</tr>
<tr>
<td>Example</td>
<td colspan="2">@code{}
macos_system_wide_audio_tap = disabled
@endcode</td>
</tr>
</table>

Copy link
Contributor

Choose a reason for hiding this comment

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

At the top of this page there is the text "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." We should remove that text.

Copy link
Member

Choose a reason for hiding this comment

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

Also here:

  • Sunshine can only access microphones on macOS due to system limitations.
    To stream system audio use "Soundflower" or "BlackHole".
  • Sunshine can only access microphones on macOS due to system limitations. To stream system audio use
    [Soundflower](https://github.com/mattingalls/Soundflower) or
    [BlackHole](https://github.com/ExistentialAudio/BlackHole).
  • <template #macos>
    <a href="https://github.com/mattingalls/Soundflower" target="_blank">Soundflower</a><br>
    <a href="https://github.com/ExistentialAudio/BlackHole" target="_blank">BlackHole</a>.
    </template>

Maybe we should keep a little blurb somewhere about how to use these two if they don't want to use the Tap API?

Copy link
Author

Choose a reason for hiding this comment

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

I'll remove that.

### install_steam_audio_drivers

<table>
Expand Down
2 changes: 2 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ namespace config {
{}, // virtual_sink
true, // stream audio
true, // install_steam_drivers
true, // macos_system_wide_audio_tap
};

stream_t stream {
Expand Down Expand Up @@ -1166,6 +1167,7 @@ namespace config {
string_f(vars, "virtual_sink", audio.virtual_sink);
bool_f(vars, "stream_audio", audio.stream);
bool_f(vars, "install_steam_audio_drivers", audio.install_steam_drivers);
bool_f(vars, "macos_system_wide_audio_tap", audio.macos_system_wide_audio_tap);

string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, {"pc"sv, "lan"sv, "wan"sv});

Expand Down
9 changes: 5 additions & 4 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,11 @@ namespace config {
};

struct audio_t {
std::string sink;
std::string virtual_sink;
bool stream;
bool install_steam_drivers;
std::string sink; ///< Audio output device/sink to use for audio capture
std::string virtual_sink; ///< Virtual audio sink for audio routing
bool stream; ///< Enable audio streaming to clients
bool install_steam_drivers; ///< Install Steam audio drivers for enhanced compatibility
bool macos_system_wide_audio_tap; ///< Enable system-wide audio capture on macOS using Core Audio taps (requires macOS 14.2+)
};

constexpr int ENCRYPTION_MODE_NEVER = 0; // Never use video encryption, even if the client supports it
Expand Down
192 changes: 184 additions & 8 deletions src/platform/macos/av_audio.h
Original file line number Diff line number Diff line change
@@ -1,29 +1,205 @@
/**
* @file src/platform/macos/av_audio.h
* @brief Declarations for audio capture on macOS.
* @brief Declarations for macOS audio capture with dual input paths.
*
* This header defines the AVAudio class which provides two distinct audio capture methods:
* 1. **Microphone capture** - Uses AVFoundation framework to capture from specific microphone devices
* 2. **System-wide audio tap** - Uses Core Audio taps to capture all system audio output (macOS 14.2+)
*
* The system-wide audio tap allows capturing audio from all applications and system sounds,
* while microphone capture focuses on input from physical or virtual microphone devices.
*/
#pragma once

// platform includes
#import <AudioToolbox/AudioToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreAudio/AudioHardwareTapping.h>
#import <CoreAudio/CoreAudio.h>

// lib includes
#include "third-party/TPCircularBuffer/TPCircularBuffer.h"

// Buffer length for audio processing
#define kBufferLength 4096

NS_ASSUME_NONNULL_BEGIN

// Forward declarations
@class AVAudio;
@class CATapDescription;

/**
* @brief Data structure for AudioConverter input callback.
* Contains audio data and metadata needed for format conversion during audio processing.
*/
struct AudioConverterInputData {
float *inputData; ///< Pointer to input audio data
UInt32 inputFrames; ///< Total number of input frames available
UInt32 framesProvided; ///< Number of frames already provided to converter
UInt32 deviceChannels; ///< Number of channels in the device audio
AVAudio *avAudio; ///< Reference to the AVAudio instance
};

/**
* @brief IOProc client data structure for Core Audio system taps.
* Contains configuration and conversion data for real-time audio processing.
*/
typedef struct {
AVAudio *avAudio; ///< Reference to AVAudio instance
UInt32 clientRequestedChannels; ///< Number of channels requested by client
UInt32 clientRequestedSampleRate; ///< Sample rate requested by client
UInt32 clientRequestedFrameSize; ///< Frame size requested by client
UInt32 aggregateDeviceSampleRate; ///< Sample rate of the aggregate device
UInt32 aggregateDeviceChannels; ///< Number of channels in aggregate device
AudioConverterRef _Nullable audioConverter; ///< Audio converter for format conversion
} AVAudioIOProcData;

/**
* @brief Core Audio capture class for macOS audio input and system-wide audio tapping.
* Provides functionality for both microphone capture via AVFoundation and system-wide
* audio capture via Core Audio taps (requires macOS 14.2+).
*/
@interface AVAudio: NSObject <AVCaptureAudioDataOutputSampleBufferDelegate> {
@public
TPCircularBuffer audioSampleBuffer;
TPCircularBuffer audioSampleBuffer; ///< Shared circular buffer for both audio capture paths
@private
// System-wide audio tap components (Core Audio)
AudioObjectID tapObjectID; ///< Core Audio tap object identifier for system audio capture
AudioObjectID aggregateDeviceID; ///< Aggregate device ID for system tap audio routing
AudioDeviceIOProcID ioProcID; ///< IOProc identifier for real-time audio processing
AVAudioIOProcData *_Nullable ioProcData; ///< Context data for IOProc callbacks and format conversion
}

@property (nonatomic, assign) AVCaptureSession *audioCaptureSession;
@property (nonatomic, assign) AVCaptureConnection *audioConnection;
@property (nonatomic, assign) NSCondition *samplesArrivedSignal;
// AVFoundation microphone capture properties
@property (nonatomic, assign, nullable) AVCaptureSession *audioCaptureSession; ///< AVFoundation capture session for microphone input
@property (nonatomic, assign, nullable) AVCaptureConnection *audioConnection; ///< Audio connection within the capture session

+ (NSArray *)microphoneNames;
+ (AVCaptureDevice *)findMicrophone:(NSString *)name;
// Shared synchronization property (used by both audio paths)
@property (nonatomic, assign, nullable) NSCondition *samplesArrivedSignal; ///< Condition variable to signal when audio samples are available

- (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels;
/**
* @brief Get all available microphone devices on the system.
* @return Array of AVCaptureDevice objects representing available microphones
*/
+ (NSArray<AVCaptureDevice *> *)microphones;

/**
* @brief Get names of all available microphone devices.
* @return Array of NSString objects with microphone device names
*/
+ (NSArray<NSString *> *)microphoneNames;

/**
* @brief Find a specific microphone device by name.
* @param name The name of the microphone to find (nullable - will return nil if name is nil)
* @return AVCaptureDevice object if found, nil otherwise
*/
+ (nullable AVCaptureDevice *)findMicrophone:(nullable NSString *)name;

/**
* @brief Sets up microphone capture using AVFoundation framework.
* @param device The AVCaptureDevice to use for audio input (nullable - will return error if nil)
* @param sampleRate Target sample rate in Hz
* @param frameSize Number of frames per buffer
* @param channels Number of audio channels (1=mono, 2=stereo)
* @return 0 on success, -1 on failure
*/
- (int)setupMicrophone:(nullable AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels;

/**
* @brief Sets up system-wide audio tap for capturing all system audio.
* Requires macOS 14.2+ and appropriate permissions.
* @param sampleRate Target sample rate in Hz
* @param frameSize Number of frames per buffer
* @param channels Number of audio channels
* @return 0 on success, -1 on failure
*/
- (int)setupSystemTap:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels;

// Buffer management methods for testing and internal use
/**
* @brief Initializes the circular audio buffer for the specified number of channels.
* @param channels Number of audio channels to configure the buffer for
*/
- (void)initializeAudioBuffer:(UInt8)channels;

/**
* @brief Cleans up and deallocates the audio buffer resources.
*/
- (void)cleanupAudioBuffer;

/**
* @brief Cleans up system tap resources in a safe, ordered manner.
* @param tapDescription Optional tap description object to release (can be nil)
*/
- (void)cleanupSystemTapContext:(nullable id)tapDescription;

/**
* @brief Initializes the system tap context with specified audio parameters.
* @param sampleRate Target sample rate in Hz
* @param frameSize Number of frames per buffer
* @param channels Number of audio channels
* @return 0 on success, -1 on failure
*/
- (int)initializeSystemTapContext:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels;

/**
* @brief Creates a Core Audio tap description for system audio capture.
* @param channels Number of audio channels to configure the tap for
* @return CATapDescription object on success, nil on failure
*/
- (nullable CATapDescription *)createSystemTapDescriptionForChannels:(UInt8)channels;

/**
* @brief Creates an aggregate device with the specified tap description and audio parameters.
* @param tapDescription Core Audio tap description for system audio capture
* @param sampleRate Target sample rate in Hz
* @param frameSize Number of frames per buffer
* @return OSStatus indicating success (noErr) or error code
*/
- (OSStatus)createAggregateDeviceWithTapDescription:(CATapDescription *)tapDescription sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize;

/**
* @brief Audio converter complex input callback for format conversion.
* Handles audio data conversion between different formats during system audio capture.
* @param inAudioConverter The audio converter reference
* @param ioNumberDataPackets Number of data packets to convert
* @param ioData Audio buffer list for converted data
* @param outDataPacketDescription Packet description for output data
* @param inputInfo Input data structure containing source audio
* @return OSStatus indicating success (noErr) or error code
*/
- (OSStatus)audioConverterComplexInputProc:(AudioConverterRef)inAudioConverter
ioNumberDataPackets:(UInt32 *)ioNumberDataPackets
ioData:(AudioBufferList *)ioData
outDataPacketDescription:(AudioStreamPacketDescription *_Nullable *_Nullable)outDataPacketDescription
inputInfo:(struct AudioConverterInputData *)inputInfo;

/**
* @brief Core Audio IOProc callback for processing system audio data.
* Handles real-time audio processing, format conversion, and writes to circular buffer.
* @param inDevice The audio device identifier
* @param inNow Current audio time stamp
* @param inInputData Input audio buffer list from the device
* @param inInputTime Time stamp for input data
* @param outOutputData Output audio buffer list (nullable for input-only devices)
* @param inOutputTime Time stamp for output data
* @param clientChannels Number of channels requested by client
* @param clientFrameSize Frame size requested by client
* @param clientSampleRate Sample rate requested by client
* @return OSStatus indicating success (noErr) or error code
*/
- (OSStatus)systemAudioIOProc:(AudioObjectID)inDevice
inNow:(const AudioTimeStamp *)inNow
inInputData:(const AudioBufferList *)inInputData
inInputTime:(const AudioTimeStamp *)inInputTime
outOutputData:(nullable AudioBufferList *)outOutputData
inOutputTime:(const AudioTimeStamp *)inOutputTime
clientChannels:(UInt32)clientChannels
clientFrameSize:(UInt32)clientFrameSize
clientSampleRate:(UInt32)clientSampleRate;

@end

NS_ASSUME_NONNULL_END
Loading
Loading