From 4498d742634eb7b41f754522999a822c32351a88 Mon Sep 17 00:00:00 2001 From: maciejmakowski2003 Date: Mon, 20 Oct 2025 14:31:53 +0200 Subject: [PATCH 1/4] fix: attach jni env to thread calling nativeAudioPlayer methods --- .../src/main/cpp/audioapi/android/core/AudioPlayer.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index 4ba4d79c6..600fb0160 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace audioapi { @@ -49,7 +50,9 @@ bool AudioPlayer::openAudioStream() { bool AudioPlayer::start() { if (mStream_) { - nativeAudioPlayer_->start(); + jni::ThreadScope::WithClassLoader([this]() { + nativeAudioPlayer_->start(); + }); auto result = mStream_->requestStart(); return result == oboe::Result::OK; } @@ -59,7 +62,9 @@ bool AudioPlayer::start() { void AudioPlayer::stop() { if (mStream_) { - nativeAudioPlayer_->stop(); + jni::ThreadScope::WithClassLoader([this]() { + nativeAudioPlayer_->stop(); + }); mStream_->requestStop(); } } From ce7247ac491b0771d4696d00894aa71615d8dd13 Mon Sep 17 00:00:00 2001 From: maciejmakowski2003 Date: Mon, 20 Oct 2025 14:33:05 +0200 Subject: [PATCH 2/4] fix: hot reload issues in Worklets example --- .../src/examples/Worklets/Worklets.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/common-app/src/examples/Worklets/Worklets.tsx b/apps/common-app/src/examples/Worklets/Worklets.tsx index bd3cab830..9e142df36 100644 --- a/apps/common-app/src/examples/Worklets/Worklets.tsx +++ b/apps/common-app/src/examples/Worklets/Worklets.tsx @@ -32,11 +32,19 @@ function Worklets() { const bar4 = useSharedValue(0); useEffect(() => { + if (!aCtxRef.current) { + aCtxRef.current = new AudioContext({ sampleRate: SAMPLE_RATE }); + } + AudioManager.setAudioSessionOptions({ iosCategory: 'playAndRecord', iosMode: 'spokenAudio', iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'], }); + + return () => { + aCtxRef.current?.close(); + }; }, []); const start = () => { @@ -45,7 +53,7 @@ function Worklets() { inputAudioData: Array, outputAudioData: Array, framesToProcess: number, - currentTime: number + _currentTime: number ) => { 'worklet'; const gain = 0.5; @@ -62,7 +70,7 @@ function Worklets() { audioData: Array, framesToProcess: number, currentTime: number, - startOffset: number + _startOffset: number ) => { 'worklet'; const frequency = 440; // A4 note @@ -70,20 +78,24 @@ function Worklets() { const modulationFreq = 2; // 2 Hz modulation const modulationDepth = 0.8; - const amplitudeModulation = Math.sin(2 * Math.PI * modulationFreq * currentTime); - const dynamicAmplitude = baseAmplitude * (1 + modulationDepth * amplitudeModulation); + const amplitudeModulation = Math.sin( + 2 * Math.PI * modulationFreq * currentTime + ); + const dynamicAmplitude = + baseAmplitude * (1 + modulationDepth * amplitudeModulation); for (let channel = 0; channel < audioData.length; channel++) { for (let sample = 0; sample < framesToProcess; sample++) { // Calculate phase based on sample position and time - const phase = 2 * Math.PI * frequency * (currentTime + sample / SAMPLE_RATE); + const phase = + 2 * Math.PI * frequency * (currentTime + sample / SAMPLE_RATE); audioData[channel][sample] = dynamicAmplitude * Math.sin(phase); } } }; const worklet = ( audioData: Array, - inputChannelCount: number + _inputChannelCount: number ) => { 'worklet'; @@ -106,7 +118,10 @@ function Worklets() { bar2.value = withSpring(scaledAmplitude, { damping: 15, stiffness: 200 }); }; - aCtxRef.current = new AudioContext({ sampleRate: SAMPLE_RATE }); + if (!aCtxRef.current) { + aCtxRef.current = new AudioContext({ sampleRate: SAMPLE_RATE }); + } + workletSourceNodeRef.current = aCtxRef.current.createWorkletSourceNode( sourceWorklet, 'AudioRuntime' From 508259a4c32a6a5da6f00e1108b68b7ee83a7cba Mon Sep 17 00:00:00 2001 From: maciejmakowski2003 Date: Mon, 20 Oct 2025 14:33:20 +0200 Subject: [PATCH 3/4] ci: yarn fix --- .../src/main/cpp/audioapi/android/core/AudioPlayer.cpp | 9 +++------ .../ios/audioapi/ios/core/IOSAudioRecorder.h | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index 600fb0160..3f1e7513a 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -50,9 +50,8 @@ bool AudioPlayer::openAudioStream() { bool AudioPlayer::start() { if (mStream_) { - jni::ThreadScope::WithClassLoader([this]() { - nativeAudioPlayer_->start(); - }); + jni::ThreadScope::WithClassLoader( + [this]() { nativeAudioPlayer_->start(); }); auto result = mStream_->requestStart(); return result == oboe::Result::OK; } @@ -62,9 +61,7 @@ bool AudioPlayer::start() { void AudioPlayer::stop() { if (mStream_) { - jni::ThreadScope::WithClassLoader([this]() { - nativeAudioPlayer_->stop(); - }); + jni::ThreadScope::WithClassLoader([this]() { nativeAudioPlayer_->stop(); }); mStream_->requestStop(); } } diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.h b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.h index 5f5233774..39a2e0b0c 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.h @@ -18,8 +18,7 @@ class IOSAudioRecorder : public AudioRecorder { IOSAudioRecorder( float sampleRate, int bufferLength, - const std::shared_ptr - &audioEventHandlerRegistry); + const std::shared_ptr &audioEventHandlerRegistry); ~IOSAudioRecorder() override; From b84170ba781dc0434061e43d531338234b3bbc92 Mon Sep 17 00:00:00 2001 From: maciejmakowski2003 Date: Mon, 20 Oct 2025 22:08:20 +0200 Subject: [PATCH 4/4] fix: fixed issues with AudioContext lifecycle management --- .../cpp/audioapi/android/AudioAPIModule.cpp | 5 ++++ .../cpp/audioapi/android/AudioAPIModule.h | 1 + .../com/swmansion/audioapi/AudioAPIModule.kt | 30 ++++++++++++++++++- .../cpp/audioapi/AudioAPIModuleInstaller.h | 20 +++++++++++++ .../ios/audioapi/ios/AudioAPIModule.h | 3 +- .../ios/audioapi/ios/AudioAPIModule.mm | 2 ++ 6 files changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.cpp index 092c1b3a4..4781792f4 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.cpp @@ -51,6 +51,7 @@ void AudioAPIModule::registerNatives() { makeNativeMethod( "invokeHandlerWithEventNameAndEventBody", AudioAPIModule::invokeHandlerWithEventNameAndEventBody), + makeNativeMethod("closeAllContexts", AudioAPIModule::closeAllContexts), }); } @@ -101,4 +102,8 @@ void AudioAPIModule::invokeHandlerWithEventNameAndEventBody( eventName->toStdString(), body); } } + +void AudioAPIModule::closeAllContexts() { + AudioAPIModuleInstaller::closeAllContexts(); +} } // namespace audioapi diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.h index 2504b37a8..886acd211 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.h @@ -34,6 +34,7 @@ class AudioAPIModule : public jni::HybridClass { void injectJSIBindings(); void invokeHandlerWithEventNameAndEventBody(jni::alias_ref eventName, jni::alias_ref> eventBody); + void closeAllContexts(); private: friend HybridBase; diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt index 265a68784..9c9ee2c64 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt @@ -1,6 +1,8 @@ package com.swmansion.audioapi import com.facebook.jni.HybridData +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableArray @@ -16,7 +18,8 @@ import java.lang.ref.WeakReference @ReactModule(name = AudioAPIModule.NAME) class AudioAPIModule( reactContext: ReactApplicationContext, -) : NativeAudioAPIModuleSpec(reactContext) { +) : NativeAudioAPIModuleSpec(reactContext), + LifecycleEventListener { companion object { const val NAME = NativeAudioAPIModuleSpec.NAME } @@ -24,6 +27,7 @@ class AudioAPIModule( val reactContext: WeakReference = WeakReference(reactContext) private val mHybridData: HybridData + private var reanimatedModule: NativeModule? = null external fun initHybrid( workletsModule: Any?, @@ -38,6 +42,8 @@ class AudioAPIModule( eventBody: Map, ) + private external fun closeAllContexts() + init { try { System.loadLibrary("react-native-audio-api") @@ -47,6 +53,8 @@ class AudioAPIModule( if (BuildConfig.RN_AUDIO_API_ENABLE_WORKLETS) { try { workletsModule = reactContext.getNativeModule("WorkletsModule") + reanimatedModule = reactContext.getNativeModule("ReanimatedModule") + reanimatedModule } catch (ex: Exception) { throw RuntimeException("WorkletsModule not found - make sure react-native-worklets is properly installed") } @@ -64,6 +72,26 @@ class AudioAPIModule( return true } + override fun onHostResume() { + // do nothing + } + + override fun onHostPause() { + closeAllContexts() + } + + override fun onHostDestroy() { + // do nothing + } + + override fun initialize() { + reactContext.get()?.addLifecycleEventListener(this) + } + + override fun invalidate() { + // think about cleaning up resources, singletons etc. + } + override fun getDevicePreferredSampleRate(): Double = MediaSessionManager.getDevicePreferredSampleRate() override fun setAudioSessionActivity( 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 e3f3856a0..73778ce14 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h @@ -16,12 +16,16 @@ #include #include +#include namespace audioapi { using namespace facebook; class AudioAPIModuleInstaller { + private: + inline static std::vector> contexts_ = {}; + public: static void injectJSIBindings( jsi::Runtime *jsiRuntime, @@ -61,6 +65,19 @@ class AudioAPIModuleInstaller { *jsiRuntime, audioEventHandlerRegistryHostObject)); } + static void closeAllContexts() { + for (auto it = contexts_.begin(); it != contexts_.end(); ++it) { + auto weakContext = *it; + + if (auto context = weakContext.lock()) { + context->close(); + } + + it = contexts_.erase(it); + --it; + } + } + private: static jsi::Function getCreateAudioContextFunction( jsi::Runtime *jsiRuntime, @@ -95,6 +112,8 @@ class AudioAPIModuleInstaller { initSuspended, audioEventHandlerRegistry, runtimeRegistry); + AudioAPIModuleInstaller::contexts_.push_back(audioContext); + auto audioContextHostObject = std::make_shared( audioContext, &runtime, jsCallInvoker); @@ -138,6 +157,7 @@ class AudioAPIModuleInstaller { sampleRate, audioEventHandlerRegistry, runtimeRegistry); + auto audioContextHostObject = std::make_shared( offlineAudioContext, &runtime, jsCallInvoker); diff --git a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.h b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.h index 7f618fcf3..3e2a4d9dc 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.h @@ -1,5 +1,6 @@ #ifdef RCT_NEW_ARCH_ENABLED #import +#import #import #else // RCT_NEW_ARCH_ENABLED #import @@ -14,7 +15,7 @@ @interface AudioAPIModule : RCTEventEmitter #ifdef RCT_NEW_ARCH_ENABLED - + #else #endif // RCT_NEW_ARCH_ENABLED diff --git a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm index cc2caccdd..1974d8171 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -54,6 +54,8 @@ - (void)invalidate _eventHandler = nullptr; + audioapi::AudioAPIModuleInstaller::closeAllContexts(); + [super invalidate]; }