-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(macOS): Capture audio on macOS using Tap API #4209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
5bd8e54
74c2cf1
0c2e096
48ef28d
05c76c1
d594c06
2b0ba99
1dc8217
1a3bc52
cab7da6
a729f78
b29445e
9762651
13bc467
5aa03e2
a847d1f
6404705
faa1170
fc3609b
b056036
2c40470
1ba9208
44407a4
3ce6572
fe2ef3f
186c21c
db3d2df
f667865
ee2bc0d
1cf8e9e
db69ca2
f5a6672
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,3 +56,6 @@ package-lock.json | |
| # Python | ||
| *.pyc | ||
| venv/ | ||
|
|
||
| # Caches | ||
| .cache/ | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -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() | ||||
|
Comment on lines
+21
to
+23
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be necessary?
|
||||
| find_package(PkgConfig REQUIRED) | ||||
| find_package(Threads REQUIRED) | ||||
| pkg_check_modules(CURL REQUIRED libcurl) | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Comment on lines
+20
to
+21
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -455,9 +455,10 @@ systemctl --user enable sunshine | |||||
| ### macOS | ||||||
| The first time you start Sunshine, you will be asked to grant access to screen recording and your microphone. | ||||||
|
|
||||||
| 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). | ||||||
| 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. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
macos-13 isn't supported anymore |
||||||
|
|
||||||
| > [!NOTE] | ||||||
| > Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -194,8 +194,11 @@ def post_install | |||||||||||||||||
|
|
||||||||||||||||||
| if OS.mac? | ||||||||||||||||||
| opoo <<~EOS | ||||||||||||||||||
| Sunshine can only access microphones on macOS due to system limitations. | ||||||||||||||||||
| To stream system audio use "Soundflower" or "BlackHole". | ||||||||||||||||||
| 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. | ||||||||||||||||||
|
Comment on lines
+197
to
203
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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. |
||||||||||||||||||
| EOS | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,171 @@ | ||
| /** | ||
| * @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 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.0+) | ||
| * | ||
| * 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; | ||
|
|
||
| namespace platf { | ||
| OSStatus audioConverterComplexInputProc(AudioConverterRef _Nullable inAudioConverter, UInt32 *_Nonnull ioNumberDataPackets, AudioBufferList *_Nonnull ioData, AudioStreamPacketDescription *_Nullable *_Nullable outDataPacketDescription, void *_Nonnull inUserData); | ||
| OSStatus systemAudioIOProc(AudioObjectID inDevice, const AudioTimeStamp *_Nullable inNow, const AudioBufferList *_Nullable inInputData, const AudioTimeStamp *_Nullable inInputTime, AudioBufferList *_Nullable outOutputData, const AudioTimeStamp *_Nullable inOutputTime, void *_Nullable inClientData); | ||
| } // namespace platf | ||
|
|
||
| /** | ||
| * @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 | ||
| float *_Nullable conversionBuffer; ///< Pre-allocated buffer for audio conversion | ||
| UInt32 conversionBufferSize; ///< Size of the conversion buffer in bytes | ||
| } 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.0+). | ||
| */ | ||
| @interface AVAudio: NSObject <AVCaptureAudioDataOutputSampleBufferDelegate> { | ||
| @public | ||
| TPCircularBuffer audioSampleBuffer; | ||
| TPCircularBuffer audioSampleBuffer; ///< Shared circular buffer for both audio capture paths | ||
| dispatch_semaphore_t audioSemaphore; ///< Real-time safe semaphore for signaling audio sample availability | ||
| @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 | ||
| @property (nonatomic, assign) BOOL hostAudioEnabled; ///< Whether host audio playback should be enabled (affects tap mute behavior) | ||
|
|
||
| + (NSArray *)microphoneNames; | ||
| + (AVCaptureDevice *)findMicrophone:(NSString *)name; | ||
| /** | ||
| * @brief Get all available microphone devices on the system. | ||
| * @return Array of AVCaptureDevice objects representing available microphones | ||
| */ | ||
| + (NSArray<AVCaptureDevice *> *)microphones; | ||
|
|
||
| - (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels; | ||
| /** | ||
| * @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.0+ 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; | ||
|
|
||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END |
There was a problem hiding this comment.
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?