#import <Foundation/Foundation.h>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
@import ScreenCaptureKit;
@import CoreMedia;
#pragma clang diagnostic pop
@interface AXTSCKAudioDelegate : NSObject <SCStreamOutput>
@property (nonatomic, strong) NSMutableData *pcmData;
@property (nonatomic) double sampleRate;
@property (nonatomic) int channels;
@property (nonatomic) BOOL capturing;
@end
@implementation AXTSCKAudioDelegate
- (instancetype)init {
self = [super init];
if (self) {
_pcmData = [NSMutableData data];
_sampleRate = 48000.0;
_channels = 1;
_capturing = YES;
}
return self;
}
- (void)stream:(SCStream *)stream
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
ofType:(SCStreamOutputType)type
{
if (!self.capturing) return;
if (type != SCStreamOutputTypeAudio) return;
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
if (formatDesc) {
const AudioStreamBasicDescription *asbd =
CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc);
if (asbd) {
self.sampleRate = asbd->mSampleRate;
self.channels = (int)asbd->mChannelsPerFrame;
}
}
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
if (!blockBuffer) return;
size_t totalLength = 0;
char *dataPointer = NULL;
OSStatus status = CMBlockBufferGetDataPointer(
blockBuffer, 0, NULL, &totalLength, &dataPointer
);
if (status != noErr || !dataPointer || totalLength == 0) return;
@synchronized (self.pcmData) {
[self.pcmData appendBytes:dataPointer length:totalLength];
}
}
@end
typedef struct {
float *samples; int sample_count;
float sample_rate;
int channels;
int error_code; char error_msg[256];
} AXTSCKCaptureResult;
bool axt_sck_is_available(void) {
if (@available(macOS 14.0, *)) {
Class cls = NSClassFromString(@"SCStream");
return cls != nil;
}
return false;
}
AXTSCKCaptureResult axt_sck_capture_system_audio(float duration_secs) {
AXTSCKCaptureResult result = {0};
if (!axt_sck_is_available()) {
result.error_code = 1;
snprintf(result.error_msg, sizeof(result.error_msg),
"ScreenCaptureKit audio-only requires macOS 14.0+");
return result;
}
if (@available(macOS 14.0, *)) {
dispatch_semaphore_t doneSem = dispatch_semaphore_create(0);
__block AXTSCKCaptureResult blockResult = {0};
float captureDuration = duration_secs;
dispatch_async(dispatch_get_main_queue(), ^{
__block SCShareableContent *sharedContent = nil;
__block NSError *contentError = nil;
dispatch_semaphore_t contentSem = dispatch_semaphore_create(0);
[SCShareableContent getShareableContentExcludingDesktopWindows:NO
onScreenWindowsOnly:YES
completionHandler:^(SCShareableContent *content,
NSError *error) {
sharedContent = content;
contentError = error;
dispatch_semaphore_signal(contentSem);
}];
dispatch_semaphore_wait(contentSem,
dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
if (contentError || !sharedContent || sharedContent.displays.count == 0) {
blockResult.error_code = 2;
snprintf(blockResult.error_msg, sizeof(blockResult.error_msg),
"No display available for SCK content filter: %s",
contentError
? contentError.localizedDescription.UTF8String
: "no displays");
dispatch_semaphore_signal(doneSem);
return;
}
SCDisplay *display = sharedContent.displays.firstObject;
SCContentFilter *filter =
[[SCContentFilter alloc] initWithDisplay:display excludingWindows:@[]];
SCStreamConfiguration *config = [[SCStreamConfiguration alloc] init];
config.width = 0;
config.height = 0;
config.capturesAudio = YES;
config.excludesCurrentProcessAudio = YES;
config.channelCount = 1; config.sampleRate = 48000;
SCStream *stream = [[SCStream alloc] initWithFilter:filter
configuration:config
delegate:nil];
AXTSCKAudioDelegate *delegate = [[AXTSCKAudioDelegate alloc] init];
NSError *streamError = nil;
BOOL added = [stream addStreamOutput:delegate
type:SCStreamOutputTypeAudio
sampleHandlerQueue:dispatch_get_global_queue(
QOS_CLASS_USER_INTERACTIVE, 0)
error:&streamError];
if (!added || streamError) {
blockResult.error_code = 3;
snprintf(blockResult.error_msg, sizeof(blockResult.error_msg),
"Failed to add stream output: %s",
streamError
? streamError.localizedDescription.UTF8String
: "unknown");
dispatch_semaphore_signal(doneSem);
return;
}
__block NSError *startError = nil;
dispatch_semaphore_t startSem = dispatch_semaphore_create(0);
[stream startCaptureWithCompletionHandler:^(NSError *error) {
startError = error;
dispatch_semaphore_signal(startSem);
}];
dispatch_semaphore_wait(startSem,
dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
if (startError) {
blockResult.error_code = 3;
snprintf(blockResult.error_msg, sizeof(blockResult.error_msg),
"SCStream start failed: %s",
startError.localizedDescription.UTF8String);
dispatch_semaphore_signal(doneSem);
return;
}
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
[NSThread sleepForTimeInterval:(NSTimeInterval)captureDuration];
delegate.capturing = NO;
dispatch_semaphore_t stopSem = dispatch_semaphore_create(0);
[stream stopCaptureWithCompletionHandler:^(NSError * _Nullable __unused e) {
dispatch_semaphore_signal(stopSem);
}];
dispatch_semaphore_wait(stopSem,
dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
NSData *pcmBytes;
@synchronized (delegate.pcmData) {
pcmBytes = [delegate.pcmData copy];
}
if (pcmBytes.length == 0) {
blockResult.sample_count = 0;
blockResult.samples = NULL;
blockResult.sample_rate = (float)delegate.sampleRate;
blockResult.channels = delegate.channels;
blockResult.error_code = 0;
dispatch_semaphore_signal(doneSem);
return;
}
int float_count = (int)(pcmBytes.length / sizeof(float));
float *out = (float *)malloc((size_t)float_count * sizeof(float));
if (!out) {
blockResult.error_code = 3;
snprintf(blockResult.error_msg, sizeof(blockResult.error_msg),
"malloc failed for audio buffer");
dispatch_semaphore_signal(doneSem);
return;
}
memcpy(out, pcmBytes.bytes, (size_t)float_count * sizeof(float));
blockResult.samples = out;
blockResult.sample_count = float_count;
blockResult.sample_rate = (float)delegate.sampleRate;
blockResult.channels = delegate.channels;
blockResult.error_code = 0;
dispatch_semaphore_signal(doneSem);
}); });
dispatch_time_t timeout = dispatch_time(
DISPATCH_TIME_NOW,
(int64_t)((captureDuration + 15.0f) * (float)NSEC_PER_SEC));
long waited = dispatch_semaphore_wait(doneSem, timeout);
if (waited != 0) {
blockResult.error_code = 1;
snprintf(blockResult.error_msg, sizeof(blockResult.error_msg),
"System audio capture timed out");
}
result = blockResult;
return result;
}
result.error_code = 1;
snprintf(result.error_msg, sizeof(result.error_msg), "Unreachable: macOS version check failed");
return result;
}
void axt_sck_free_result(AXTSCKCaptureResult *result) {
if (result && result->samples) {
free(result->samples);
result->samples = NULL;
result->sample_count = 0;
}
}