From f03c68e4551636d365e5da610c7bfa5656f450c7 Mon Sep 17 00:00:00 2001 From: jerryseigle Date: Mon, 9 Feb 2026 21:35:23 -0500 Subject: [PATCH 1/4] Add Catalyst stubs and mac build flags --- .../react-native-audio-api/RNAudioAPI.podspec | 2 + .../audioapi/ios/core/NativeAudioRecorder.m | 8 + .../ios/audioapi/ios/system/AudioEngine.mm | 6 + .../audioapi/ios/system/AudioSessionManager.h | 14 +- .../ios/system/AudioSessionManager.mm | 145 ++++++++++++++++++ .../ios/system/SystemNotificationManager.h | 5 +- .../ios/system/SystemNotificationManager.mm | 50 ++++++ 7 files changed, 228 insertions(+), 2 deletions(-) diff --git a/packages/react-native-audio-api/RNAudioAPI.podspec b/packages/react-native-audio-api/RNAudioAPI.podspec index 38f2f3a87..a2736f881 100644 --- a/packages/react-native-audio-api/RNAudioAPI.podspec +++ b/packages/react-native-audio-api/RNAudioAPI.podspec @@ -16,6 +16,7 @@ worklets_preprocessor_flag = worklets_enabled ? '-DRN_AUDIO_API_ENABLE_WORKLETS= ffmpeg_flag = $RN_AUDIO_API_FFMPEG_DISABLED ? '-DRN_AUDIO_API_FFMPEG_DISABLED=1' : '' skip_ffmpeg_argument = $RN_AUDIO_API_FFMPEG_DISABLED ? 'skipffmpeg' : '' +mac_catalyst_cflags = '-DMA_NO_LIBOPUS -DMA_NO_LIBVORBIS' Pod::Spec.new do |s| s.name = "RNAudioAPI" @@ -103,6 +104,7 @@ Pod::Spec.new do |s| "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", "GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) HAVE_ACCELERATE=1', 'OTHER_CFLAGS' => "$(inherited) #{fabric_flags} #{version_flag} #{worklets_preprocessor_flag} #{ffmpeg_flag}", + 'OTHER_CFLAGS[sdk=macosx*]' => "$(inherited) #{fabric_flags} #{version_flag} #{worklets_preprocessor_flag} #{ffmpeg_flag} #{mac_catalyst_cflags}", } s.xcconfig = { diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioRecorder.m b/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioRecorder.m index 52b0e2092..28d2be960 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioRecorder.m +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioRecorder.m @@ -1,3 +1,4 @@ +#import #import #import #import @@ -61,6 +62,12 @@ - (AVAudioFormat *)getInputFormat - (int)getBufferSize { +#if TARGET_OS_MACCATALYST + AVAudioFormat *format = [self getInputFormat]; + double sampleRate = format.sampleRate > 0 ? format.sampleRate : 48000.0; + float bufferDuration = 0.2; + return nextPowerOfTwo(ceil(bufferDuration * sampleRate)); +#else // NOTE: this method should be called only after the session is activated AVAudioSession *audioSession = [AVAudioSession sharedInstance]; @@ -72,6 +79,7 @@ - (int)getBufferSize // IOS returns buffer duration rounded, but expects the buffer size to be power of two in runtime return nextPowerOfTwo(ceil(bufferDuration * audioSession.sampleRate)); +#endif } - (void)start diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm index 41cd498f8..78cb94b3c 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm @@ -1,3 +1,4 @@ +#import #import #import @@ -278,6 +279,10 @@ - (void)restartAudioEngine - (void)logAudioEngineState { +#if TARGET_OS_MACCATALYST + NSLog(@"[AudioEngine] logAudioEngineState is not available on Mac Catalyst."); + return; +#else AVAudioSession *session = [AVAudioSession sharedInstance]; NSLog(@"================ 🎧 AVAudioEngine STATE ================"); @@ -312,6 +317,7 @@ - (void)logAudioEngineState NSLog(@"📐 Engine output format: %.0f Hz, %u channels", format.sampleRate, format.channelCount); NSLog(@"======================================================="); +#endif } @end diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h index 39fcee303..4d734f79f 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h @@ -1,7 +1,13 @@ #pragma once -#import +#import #import +#if !TARGET_OS_MACCATALYST +#import +#else +@class AVAudioSession; +@class AVAudioSessionPortDescription; +#endif #import @interface AudioSessionManager : NSObject @@ -13,9 +19,15 @@ @property (nonatomic, assign) bool shouldManageSession; // Session configuration options (desired by user) +#if TARGET_OS_MACCATALYST +@property (nonatomic, copy) NSString *desiredMode; +@property (nonatomic, copy) NSString *desiredCategory; +@property (nonatomic, assign) NSUInteger desiredOptions; +#else @property (nonatomic, assign) AVAudioSessionMode desiredMode; @property (nonatomic, assign) AVAudioSessionCategory desiredCategory; @property (nonatomic, assign) AVAudioSessionCategoryOptions desiredOptions; +#endif @property (nonatomic, assign) bool allowHapticsAndSounds; @property (nonatomic, assign) bool notifyOthersOnDeactivation; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm index cb9cca613..10d882a7f 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm @@ -1,6 +1,150 @@ +#import +#if !TARGET_OS_MACCATALYST #import +#endif #import +#if TARGET_OS_MACCATALYST +@implementation AudioSessionManager + +static AudioSessionManager *_sharedInstance = nil; + +- (instancetype)init +{ + if (self = [super init]) { + self.audioSession = nil; + + self.isActive = false; + self.shouldManageSession = true; + + self.desiredCategory = @"playback"; + self.desiredMode = @"default"; + self.desiredOptions = 0; + self.allowHapticsAndSounds = false; + self.notifyOthersOnDeactivation = true; + } + + _sharedInstance = self; + return self; +} + ++ (instancetype)sharedInstance +{ + return _sharedInstance; +} + +- (void)cleanup +{ + self.audioSession = nil; +} + +- (bool)configureAudioSession +{ + return true; +} + +- (void)setAudioSessionOptions:(NSString *)categoryStr + mode:(NSString *)modeStr + options:(NSArray *)optionsArray + allowHaptics:(BOOL)allowHaptics + notifyOthersOnDeactivation:(BOOL)notifyOthersOnDeactivation +{ + (void)optionsArray; + self.desiredCategory = categoryStr; + self.desiredMode = modeStr; + self.desiredOptions = 0; + self.allowHapticsAndSounds = allowHaptics; + self.notifyOthersOnDeactivation = notifyOthersOnDeactivation; +} + +- (bool)setActive:(bool)active error:(NSError **)error +{ + (void)error; + if (!self.shouldManageSession) { + return true; + } + + self.isActive = active; + return true; +} + +- (void)markInactive +{ + self.isActive = false; +} + +- (void)disableSessionManagement +{ + self.shouldManageSession = false; +} + +- (NSNumber *)getDevicePreferredSampleRate +{ + return @(48000); +} + +- (NSNumber *)getDevicePreferredInputChannelCount +{ + return @(2); +} + +- (void)requestRecordingPermissions:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + (void)reject; + resolve(@"Granted"); +} + +- (NSString *)requestRecordingPermissions +{ + return @"Granted"; +} + +- (void)checkRecordingPermissions:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + (void)reject; + resolve(@"Granted"); +} + +- (NSString *)checkRecordingPermissions +{ + return @"Granted"; +} + +- (void)getDevicesInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + (void)reject; + resolve(@{ + @"availableInputs" : @[], + @"currentInputs" : @[], + @"availableOutputs" : @[], + @"currentOutputs" : @[], + }); +} + +- (NSArray *)parseDeviceList:(NSArray *)devices +{ + (void)devices; + return @[]; +} + +- (void)setInputDevice:(NSString *)deviceId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + (void)resolve; + (void)deviceId; + reject(nil, @"Input device selection is not supported on Mac Catalyst", nil); +} + +- (bool)isSessionActive +{ + return self.isActive; +} + +@end +#else @implementation AudioSessionManager static AudioSessionManager *_sharedInstance = nil; @@ -435,3 +579,4 @@ - (bool)isSessionActive } @end +#endif diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/SystemNotificationManager.h b/packages/react-native-audio-api/ios/audioapi/ios/system/SystemNotificationManager.h index 5ddf0d4e2..736262719 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/SystemNotificationManager.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/SystemNotificationManager.h @@ -1,7 +1,10 @@ #pragma once -#import +#import #import +#if !TARGET_OS_MACCATALYST +#import +#endif @class AudioAPIModule; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/SystemNotificationManager.mm b/packages/react-native-audio-api/ios/audioapi/ios/system/SystemNotificationManager.mm index c629af13a..33b9855f5 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/SystemNotificationManager.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/SystemNotificationManager.mm @@ -1,9 +1,58 @@ +#import #import #import #import #import #import +#if TARGET_OS_MACCATALYST +@implementation SystemNotificationManager + +- (instancetype)initWithAudioAPIModule:(AudioAPIModule *)audioAPIModule +{ + if (self = [super init]) { + self.audioAPIModule = audioAPIModule; + self.notificationCenter = [NSNotificationCenter defaultCenter]; + self.audioInterruptionsObserved = false; + self.volumeChangesObserved = false; + } + + return self; +} + +- (void)cleanup +{ + self.notificationCenter = nil; +} + +- (void)observeAudioInterruptions:(BOOL)enabled +{ + self.audioInterruptionsObserved = enabled; +} + +- (void)activelyReclaimSession:(BOOL)enabled +{ + (void)enabled; +} + +- (void)observeVolumeChanges:(BOOL)enabled +{ + self.volumeChangesObserved = enabled; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + (void)keyPath; + (void)object; + (void)change; + (void)context; +} + +@end +#else @implementation SystemNotificationManager static NSString *NotificationManagerContext = @"SystemNotificationManagerContext"; @@ -305,3 +354,4 @@ - (void)checkSecondaryAudioHint } @end +#endif From 24bba9b6b8d4b49ff680e4d59b4071b5b24bc07b Mon Sep 17 00:00:00 2001 From: jerryseigle Date: Mon, 9 Feb 2026 21:36:01 -0500 Subject: [PATCH 2/4] Fix worklets runtime API usage and namespace collision --- .../core/utils/worklets/WorkletsRunner.cpp | 20 ++++++++++--------- .../common/cpp/audioapi/utils/Result.hpp | 4 ++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp index 3e4300c50..bfdc8027a 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp @@ -14,15 +14,14 @@ WorkletsRunner::WorkletsRunner( return; } #if RN_AUDIO_API_ENABLE_WORKLETS + // NOTE: We construct the worklet function directly from the runtime. + // This avoids relying on deprecated Worklets APIs. unsafeRuntimePtr = &strongRuntime->getJSIRuntime(); - strongRuntime->executeSync([this, shareableWorklet](jsi::Runtime &rt) -> jsi::Value { - /// Placement new to avoid dynamic memory allocation - new (reinterpret_cast(&unsafeWorklet)) - jsi::Function(shareableWorklet->toJSValue(*unsafeRuntimePtr) - .asObject(*unsafeRuntimePtr) - .asFunction(*unsafeRuntimePtr)); - return jsi::Value::undefined(); - }); + /// Placement new to avoid dynamic memory allocation + new (reinterpret_cast(&unsafeWorklet)) + jsi::Function(shareableWorklet->toJSValue(*unsafeRuntimePtr) + .asObject(*unsafeRuntimePtr) + .asFunction(*unsafeRuntimePtr)); workletInitialized = true; #else unsafeRuntimePtr = nullptr; @@ -62,7 +61,10 @@ std::optional WorkletsRunner::executeOnRuntimeGuarded( return std::nullopt; } #if RN_AUDIO_API_ENABLE_WORKLETS - return strongRuntime->executeSync(std::move(job)); + // Worklets no longer expose an executeSync(job) API. + // Execute the job directly on the runtime instance. + auto &rt = strongRuntime->getJSIRuntime(); + return job(rt); #else return std::nullopt; #endif diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/Result.hpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/Result.hpp index 4a90d7938..6ee4cde68 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/Result.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/Result.hpp @@ -7,6 +7,8 @@ #include #include +namespace audioapi { + struct NoneType {}; inline constexpr NoneType None{}; @@ -321,3 +323,5 @@ class Result { }; bool is_ok_; }; + +} // namespace audioapi From ced783bffc6b9f9753fd086250de328df981e0a3 Mon Sep 17 00:00:00 2001 From: jerryseigle Date: Mon, 9 Feb 2026 22:30:58 -0500 Subject: [PATCH 3/4] Fix worklet runner init for missing runtime --- .../core/utils/worklets/WorkletsRunner.cpp | 65 ++++++++++++------- .../core/utils/worklets/WorkletsRunner.h | 5 +- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp index bfdc8027a..0c8d5a2b7 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp @@ -8,29 +8,14 @@ WorkletsRunner::WorkletsRunner( std::weak_ptr weakRuntime, const std::shared_ptr &shareableWorklet, bool shouldLockRuntime) - : weakRuntime_(std::move(weakRuntime)), shouldLockRuntime(shouldLockRuntime) { - auto strongRuntime = weakRuntime_.lock(); - if (strongRuntime == nullptr) { - return; - } -#if RN_AUDIO_API_ENABLE_WORKLETS - // NOTE: We construct the worklet function directly from the runtime. - // This avoids relying on deprecated Worklets APIs. - unsafeRuntimePtr = &strongRuntime->getJSIRuntime(); - /// Placement new to avoid dynamic memory allocation - new (reinterpret_cast(&unsafeWorklet)) - jsi::Function(shareableWorklet->toJSValue(*unsafeRuntimePtr) - .asObject(*unsafeRuntimePtr) - .asFunction(*unsafeRuntimePtr)); - workletInitialized = true; -#else - unsafeRuntimePtr = nullptr; - workletInitialized = false; -#endif + : weakRuntime_(std::move(weakRuntime)), + shareableWorklet_(shareableWorklet), + shouldLockRuntime(shouldLockRuntime) { } WorkletsRunner::WorkletsRunner(WorkletsRunner &&other) : weakRuntime_(std::move(other.weakRuntime_)), + shareableWorklet_(std::move(other.shareableWorklet_)), unsafeRuntimePtr(other.unsafeRuntimePtr), workletInitialized(other.workletInitialized), shouldLockRuntime(other.shouldLockRuntime) { @@ -54,6 +39,30 @@ WorkletsRunner::~WorkletsRunner() { workletInitialized = false; } +bool WorkletsRunner::ensureWorkletInitialized(jsi::Runtime &rt) { +#if RN_AUDIO_API_ENABLE_WORKLETS + if (workletInitialized) { + return true; + } + if (!shareableWorklet_) { + return false; + } + auto valueUnpacker = rt.global().getProperty(rt, "__valueUnpacker"); + if (!valueUnpacker.isObject()) { + return false; + } + unsafeRuntimePtr = &rt; + /// Placement new to avoid dynamic memory allocation + new (reinterpret_cast(&unsafeWorklet)) + jsi::Function(shareableWorklet_->toJSValue(rt).asObject(rt).asFunction(rt)); + workletInitialized = true; + return true; +#else + (void)rt; + return false; +#endif +} + std::optional WorkletsRunner::executeOnRuntimeGuarded( const std::function &&job) const noexcept(noexcept(job)) { auto strongRuntime = weakRuntime_.lock(); @@ -61,9 +70,10 @@ std::optional WorkletsRunner::executeOnRuntimeGuarded( return std::nullopt; } #if RN_AUDIO_API_ENABLE_WORKLETS - // Worklets no longer expose an executeSync(job) API. - // Execute the job directly on the runtime instance. auto &rt = strongRuntime->getJSIRuntime(); + if (!const_cast(this)->ensureWorkletInitialized(rt)) { + return std::nullopt; + } return job(rt); #else return std::nullopt; @@ -73,7 +83,18 @@ std::optional WorkletsRunner::executeOnRuntimeGuarded( std::optional WorkletsRunner::executeOnRuntimeUnsafe( const std::function &&job) const noexcept(noexcept(job)) { #if RN_AUDIO_API_ENABLE_WORKLETS - return job(*unsafeRuntimePtr); + jsi::Runtime *rt = unsafeRuntimePtr; + if (rt == nullptr) { + auto strongRuntime = weakRuntime_.lock(); + if (strongRuntime == nullptr) { + return std::nullopt; + } + rt = &strongRuntime->getJSIRuntime(); + } + if (!const_cast(this)->ensureWorkletInitialized(*rt)) { + return std::nullopt; + } + return job(*rt); #else return std::nullopt; #endif diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.h index f414a945c..982bc38e2 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.h @@ -68,7 +68,8 @@ class WorkletsRunner { } private: - std::weak_ptr weakRuntime_; + std::weak_ptr weakRuntime_; + std::shared_ptr shareableWorklet_; jsi::Runtime *unsafeRuntimePtr = nullptr; /// @note We want to avoid automatic destruction as @@ -81,6 +82,8 @@ class WorkletsRunner { return *reinterpret_cast(&unsafeWorklet); } + bool ensureWorkletInitialized(jsi::Runtime &rt); + std::optional executeOnRuntimeGuarded( const std::function &&job) const noexcept(noexcept(job)); From 66f0347c2583d120d7fa28b8ab1be10700f3f402 Mon Sep 17 00:00:00 2001 From: jerryseigle Date: Mon, 9 Feb 2026 22:31:11 -0500 Subject: [PATCH 4/4] Guard worklet runtime extraction --- .../cpp/audioapi/AudioAPIModuleInstaller.h | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h index c58ee2519..518aa2c20 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h @@ -76,9 +76,11 @@ class AudioAPIModuleInstaller { auto sampleRate = static_cast(args[0].getNumber()); #if RN_AUDIO_API_ENABLE_WORKLETS - auto runtimeRegistry = RuntimeRegistry{ - .uiRuntime = uiRuntime, - .audioRuntime = worklets::extractWorkletRuntime(runtime, args[1])}; + auto runtimeRegistry = RuntimeRegistry{.uiRuntime = uiRuntime}; + if (count > 1 && args[1].isObject()) { + runtimeRegistry.audioRuntime = + worklets::extractWorkletRuntime(runtime, args[1]); + } #else auto runtimeRegistry = RuntimeRegistry{}; #endif @@ -109,9 +111,11 @@ class AudioAPIModuleInstaller { auto sampleRate = static_cast(args[2].getNumber()); #if RN_AUDIO_API_ENABLE_WORKLETS - auto runtimeRegistry = RuntimeRegistry{ - .uiRuntime = uiRuntime, - .audioRuntime = worklets::extractWorkletRuntime(runtime, args[3])}; + auto runtimeRegistry = RuntimeRegistry{.uiRuntime = uiRuntime}; + if (count > 3 && args[3].isObject()) { + runtimeRegistry.audioRuntime = + worklets::extractWorkletRuntime(runtime, args[3]); + } #else auto runtimeRegistry = RuntimeRegistry{}; #endif