From 70c6c0a63220caa4f9f1056285c86904d070be95 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 28 Jul 2026 16:33:13 +0200 Subject: [PATCH 1/3] fix: first implementation --- .../FabricExampleTests/AudioEngineTests.mm | 30 +- .../FabricExampleTests/AudioPlayerTests.mm | 3 + .../IOSAudioRecorderTests.mm | 39 +-- .../NativeAudioRecorderTests.mm | 2 + .../inputs/AudioRecorderHostObject.cpp | 3 +- .../ios/audioapi/ios/core/IOSAudioRecorder.h | 31 +- .../ios/audioapi/ios/core/IOSAudioRecorder.mm | 271 +++++++++++++----- .../audioapi/ios/core/NativeAudioRecorder.h | 2 + .../audioapi/ios/core/NativeAudioRecorder.m | 49 +++- .../audioapi/ios/core/utils/IOSFileWriter.h | 3 + .../audioapi/ios/core/utils/IOSFileWriter.mm | 17 ++ .../ios/core/utils/IOSRotatingFileWriter.h | 3 + .../ios/core/utils/IOSRotatingFileWriter.mm | 21 ++ .../ios/audioapi/ios/system/AudioEngine.h | 3 +- .../ios/audioapi/ios/system/AudioEngine.mm | 68 +++-- .../ios/system/SystemNotificationManager.mm | 17 ++ 16 files changed, 413 insertions(+), 149 deletions(-) diff --git a/apps/fabric-example/ios/FabricExampleTests/AudioEngineTests.mm b/apps/fabric-example/ios/FabricExampleTests/AudioEngineTests.mm index e85646121..7a3e47895 100644 --- a/apps/fabric-example/ios/FabricExampleTests/AudioEngineTests.mm +++ b/apps/fabric-example/ios/FabricExampleTests/AudioEngineTests.mm @@ -422,7 +422,8 @@ - (void)testDetachSourceNodeRemovesTrackedNodeAndClearsGraphWhenEmpty { - (void)testDetachSourceNodeKeepsGraphNeedsRebuildWhenInputRemains { NSString *sourceNodeId = [self attachSourceNodeToAudioEngine]; [self.audioEngine - attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; self.audioEngine.graphNeedsRebuild = YES; [self.audioEngine detachSourceNodeWithId:sourceNodeId]; @@ -435,7 +436,8 @@ - (void)testDetachSourceNodeKeepsGraphNeedsRebuildWhenInputRemains { - (void)testAttachInputNodeStoresAndConnectsInput { FakeAudioEngine *fakeEngine = self.audioEngine.currentFakeAudioEngine; [self.audioEngine - attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; AVAudioSinkNode *inputNode = self.audioEngine.inputNode; XCTAssertNotNil(inputNode); @@ -455,7 +457,8 @@ - (void)testAttachInputNodeDefersConnectionUntilLiveInputFormatIsAvailable { fakeEngine.fakeInputNode.outputFormat = nil; [self.audioEngine - attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; XCTAssertNil(self.audioEngine.inputNode); XCTAssertEqual(fakeEngine.attachNodeCallCount, 0); @@ -483,7 +486,8 @@ - (void)testDetachInputNodeWithoutInputDoesNothing { - (void)testDetachInputNodeClearsGraphOnlyWhenNoSourcesRemain { [self.audioEngine - attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; self.audioEngine.graphNeedsRebuild = YES; [self.audioEngine detachInputNode]; @@ -493,7 +497,8 @@ - (void)testDetachInputNodeClearsGraphOnlyWhenNoSourcesRemain { [self attachSourceNodeToAudioEngine]; [self.audioEngine - attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; self.audioEngine.graphNeedsRebuild = YES; [self.audioEngine detachInputNode]; @@ -507,7 +512,8 @@ - (void)testDetachInputNodePreservesSessionDeactivationInvalidation { fakeEngine.fakeRunning = YES; self.audioEngine.state = AudioEngineStateRunning; [self.audioEngine - attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; [self.audioEngine onSessionDeactivated]; [self.audioEngine detachInputNode]; @@ -730,7 +736,8 @@ - (void)testStartIfNecessaryRebuildsWhenGraphNeedsRebuild { - (void) testStartIfNecessaryRebuildsAfterSessionDeactivationEvenWhenTeardownClearsGraph { [self.audioEngine - attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; FakeAudioEngine *oldEngine = self.audioEngine.currentFakeAudioEngine; oldEngine.fakeRunning = YES; @@ -748,7 +755,8 @@ - (void)testStartIfNecessaryRebuildsWhenGraphNeedsRebuild { [self testInputFormatWithSampleRate:48000 channelCount:1]; self.audioEngine.nextCreatedEngineInputFormat = recoveredInputFormat; [self.audioEngine - attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; AVAudioSinkNode *recoveredInputNode = self.audioEngine.inputNode; XCTAssertTrue([self.audioEngine startIfNecessary]); @@ -770,7 +778,8 @@ - (void)testStartIfNecessaryRebuildsWhenGraphNeedsRebuild { - (void)testStartIfNecessaryRebuildsInputNodeWithFreshInstance { [self.audioEngine - attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; FakeAudioEngine *oldEngine = self.audioEngine.currentFakeAudioEngine; AVAudioSinkNode *oldInputNode = self.audioEngine.inputNode; AVAudioFormat *replacementInputFormat = @@ -991,7 +1000,8 @@ - (void)testConcurrentRecordAndPlayPathsDoNotCrash { for (NSInteger index = 0; index < 10; index += 1) { dispatch_group_enter(group); dispatch_async(queue, ^{ - [self.audioEngine attachInputNodeWithReceiverBlock:[self testInputReceiverBlock]]; + [self.audioEngine attachInputNodeWithReceiverBlock:[self testInputReceiverBlock] + onInputConfigurationChange:nil]; [self.audioEngine startIfNecessary]; dispatch_group_leave(group); }); diff --git a/apps/fabric-example/ios/FabricExampleTests/AudioPlayerTests.mm b/apps/fabric-example/ios/FabricExampleTests/AudioPlayerTests.mm index 9a9b57789..9cc6086ac 100644 --- a/apps/fabric-example/ios/FabricExampleTests/AudioPlayerTests.mm +++ b/apps/fabric-example/ios/FabricExampleTests/AudioPlayerTests.mm @@ -36,6 +36,9 @@ [[nodiscard]] bool isRunning() const override; + [[nodiscard]] double getBaseLatency() const override; + [[nodiscard]] double getOutputLatency() const override; + protected: std::shared_ptr audioBuffer_; NativeAudioPlayer *audioPlayer_; diff --git a/apps/fabric-example/ios/FabricExampleTests/IOSAudioRecorderTests.mm b/apps/fabric-example/ios/FabricExampleTests/IOSAudioRecorderTests.mm index 4953e52c9..7ea7f1409 100644 --- a/apps/fabric-example/ios/FabricExampleTests/IOSAudioRecorderTests.mm +++ b/apps/fabric-example/ios/FabricExampleTests/IOSAudioRecorderTests.mm @@ -1,6 +1,7 @@ #import #import +#import #import #import #import @@ -22,41 +23,6 @@ namespace audioapi { -class AudioEventHandlerRegistry; - -class IOSAudioRecorder : public AudioRecorder { - public: - IOSAudioRecorder(const std::shared_ptr &audioEventHandlerRegistry); - ~IOSAudioRecorder() override; - - Result start(const std::string &fileNameOverride = "") override; - Result, double, double>, std::string> stop() override; - - Result enableFileOutput( - std::shared_ptr properties) override; - void disableFileOutput() override; - - void connect(const std::shared_ptr &node) override; - void disconnect() override; - - void pause() override; - void resume() override; - - bool isRecording() const override; - bool isPaused() const override; - bool isIdle() const override; - - Result setOnAudioReadyCallback( - float sampleRate, - size_t bufferLength, - int channelCount, - uint64_t callbackId) override; - void clearOnAudioReadyCallback() override; - - protected: - NativeAudioRecorder *nativeRecorder_; -}; - struct RecorderAdapterTestFixture { std::shared_ptr context; std::shared_ptr handle; @@ -243,7 +209,7 @@ - (void)cleanup public: explicit TestableIOSAudioRecorder( const std::shared_ptr &audioEventHandlerRegistry) - : IOSAudioRecorder(audioEventHandlerRegistry) {} + : IOSAudioRecorder(audioEventHandlerRegistry, nullptr) {} NativeAudioRecorder *replaceNativeRecorder(NativeAudioRecorder *nativeRecorder) { @@ -317,6 +283,7 @@ - (void)setUp _recorder = std::make_unique(std::shared_ptr()); self.originalNativeRecorder = _recorder->replaceNativeRecorder(self.nativeRecorder); + self.nativeRecorder.onInputConfigurationChange = self.originalNativeRecorder.onInputConfigurationChange; } - (void)tearDown diff --git a/apps/fabric-example/ios/FabricExampleTests/NativeAudioRecorderTests.mm b/apps/fabric-example/ios/FabricExampleTests/NativeAudioRecorderTests.mm index 86e8736da..98de6424f 100644 --- a/apps/fabric-example/ios/FabricExampleTests/NativeAudioRecorderTests.mm +++ b/apps/fabric-example/ios/FabricExampleTests/NativeAudioRecorderTests.mm @@ -113,11 +113,13 @@ - (void)stopIfNecessary } - (void)attachInputNodeWithReceiverBlock:(AVAudioSinkNodeReceiverBlock)receiverBlock + onInputConfigurationChange:(void (^)(void))onInputConfigurationChange { self.attachInputNodeCallCount += 1; self.inputNode = [[AVAudioSinkNode alloc] initWithReceiverBlock:receiverBlock]; self.lastAttachedInputNode = self.inputNode; self.lastAttachedReceiverBlock = receiverBlock; + (void)onInputConfigurationChange; } - (bool)startIfNecessary diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp index 5fe0476c4..374029b3d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp @@ -26,7 +26,8 @@ AudioRecorderHostObject::AudioRecorderHostObject( #ifdef ANDROID audioRecorder_ = std::make_shared(audioEventHandlerRegistry); #else - audioRecorder_ = std::make_shared(audioEventHandlerRegistry); + audioRecorder_ = std::make_shared(audioEventHandlerRegistry, callInvoker); + std::static_pointer_cast(audioRecorder_)->bindLifetime(); #endif promiseVendor_ = std::make_shared(runtime, callInvoker); 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 3ecfa37f2..1ccd293fc 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 @@ -1,12 +1,13 @@ #pragma once #ifdef __OBJC__ // when compiled as Objective-C -#import +#import #else typedef struct objc_object NSURL; typedef struct objc_object AVAudioFile; typedef struct objc_object AudioBufferList; typedef struct objc_object NativeAudioRecorder; +typedef struct objc_object AVAudioFormat; #endif // __OBJC__ #include @@ -15,10 +16,15 @@ typedef struct objc_object NativeAudioRecorder; #include #include +#include #include #include #include +namespace facebook::react { +class CallInvoker; +} + namespace audioapi { class RecorderCallback; @@ -27,11 +33,16 @@ class AudioFileProperties; class AudioEventHandlerRegistry; class AudioFileWriter; -class IOSAudioRecorder : public AudioRecorder { +class IOSAudioRecorder : public AudioRecorder, + public std::enable_shared_from_this { public: - IOSAudioRecorder(const std::shared_ptr &audioEventHandlerRegistry); + IOSAudioRecorder( + const std::shared_ptr &audioEventHandlerRegistry, + const std::shared_ptr &jsCallInvoker); ~IOSAudioRecorder() override; + void bindLifetime(); + Result start(const std::string &fileNameOverride = "") override; Result, double, double>, std::string> stop() override; @@ -67,6 +78,20 @@ class IOSAudioRecorder : public AudioRecorder { Result setupFileWriter( const std::shared_ptr &properties, const std::string &fileNameOverride = ""); + Result reprepareForLiveInput(); + void scheduleReprepareForLiveInput(); + void handleInputConfigurationChange(); + Result reprepareFileWriter( + AVAudioFormat *inputFormat, + int maxInputBufferLength); + Result reprepareCallback( + AVAudioFormat *inputFormat, + int maxInputBufferLength); + void reprepareAdapter(AVAudioFormat *inputFormat, int maxInputBufferLength); + void runSideEffects(const AudioBufferList *inputBuffer, int numFrames); + + std::weak_ptr lifetime_; + std::shared_ptr jsCallInvoker_; std::vector recordingSegmentPaths_; std::atomic streamSampleRate_{0.0f}; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm index 2f1bf413a..e3c125ee6 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm @@ -2,6 +2,7 @@ #import #import #import +#import #include #include @@ -27,62 +28,26 @@ namespace audioapi { -static inline NSNumber *recorderFormatNumber(id format, NSString *key) +static double recorderFormatSampleRate(AVAudioFormat *format) { - if (format == nil) { - return nil; - } - - if ([format isKindOfClass:[AVAudioFormat class]]) { - AVAudioFormat *audioFormat = (AVAudioFormat *)format; - - if ([key isEqualToString:@"sampleRate"]) { - return @(audioFormat.sampleRate); - } - - if ([key isEqualToString:@"channelCount"]) { - return @(audioFormat.channelCount); - } - - if ([key isEqualToString:@"interleaved"]) { - return @(audioFormat.interleaved); - } - } - - @try { - id value = [format valueForKey:key]; - return [value isKindOfClass:[NSNumber class]] ? value : nil; - } @catch (__unused NSException *exception) { - return nil; - } -} - -static inline double recorderFormatSampleRate(id format) -{ - return [recorderFormatNumber(format, @"sampleRate") doubleValue]; + return format.sampleRate; } -static inline AVAudioChannelCount recorderFormatChannelCount(id format) +static AVAudioChannelCount recorderFormatChannelCount(AVAudioFormat *format) { - return (AVAudioChannelCount)[recorderFormatNumber(format, @"channelCount") unsignedIntegerValue]; + return format.channelCount; } -static inline bool recorderFormatInterleaved(id format) +static bool hasUsableRecorderFormat(AVAudioFormat *format) { - return [recorderFormatNumber(format, @"interleaved") boolValue]; + return format != nil && format.sampleRate > 0 && format.channelCount > 0; } -static inline bool hasUsableRecorderFormat(id format) +static std::string describeRecorderFormat(AVAudioFormat *format) { - return format != nil && recorderFormatSampleRate(format) > 0 && - recorderFormatChannelCount(format) > 0; -} - -static std::string describeRecorderFormat(id format) -{ - return "engineFormat={sampleRate=" + std::to_string(recorderFormatSampleRate(format)) + - ", channelCount=" + std::to_string(recorderFormatChannelCount(format)) + - ", interleaved=" + std::string(recorderFormatInterleaved(format) ? "true" : "false") + "}"; + return "engineFormat={sampleRate=" + std::to_string(format.sampleRate) + + ", channelCount=" + std::to_string(format.channelCount) + + ", interleaved=" + (format.interleaved ? "true" : "false") + "}"; } static void cleanupStartedRecorder( @@ -104,45 +69,223 @@ static void cleanupStartedRecorder( /// This "method" should be called from the JS thread only. /// @param audioEventHandlerRegistry Shared pointer to the AudioEventHandlerRegistry for event handling. IOSAudioRecorder::IOSAudioRecorder( - const std::shared_ptr &audioEventHandlerRegistry) - : AudioRecorder(audioEventHandlerRegistry) + const std::shared_ptr &audioEventHandlerRegistry, + const std::shared_ptr &jsCallInvoker) + : AudioRecorder(audioEventHandlerRegistry), jsCallInvoker_(jsCallInvoker) { AudioReceiverBlock receiverBlock = ^(const AudioBufferList *inputBuffer, int numFrames) { if (numFrames > 0) { lastCallbackFrameCount_.store(numFrames, std::memory_order_release); } - if (usesFileOutput()) { - if (auto lock = Locker::tryLock(fileWriterMutex_)) { - fileWriter_->writeAudioData(inputBuffer, numFrames); + runSideEffects(inputBuffer, numFrames); + }; + + nativeRecorder_ = [[NativeAudioRecorder alloc] initWithReceiverBlock:receiverBlock]; + + nativeRecorder_.onInputConfigurationChange = ^{ this->handleInputConfigurationChange(); }; +} + +void IOSAudioRecorder::bindLifetime() +{ + lifetime_ = shared_from_this(); +} + +void IOSAudioRecorder::runSideEffects(const AudioBufferList *inputBuffer, int numFrames) +{ + if (usesFileOutput()) { + if (auto lock = Locker::tryLock(fileWriterMutex_)) { + fileWriter_->writeAudioData(inputBuffer, numFrames); + } + } + + if (usesCallback()) { + if (auto lock = Locker::tryLock(callbackMutex_)) { + std::static_pointer_cast(dataCallback_) + ->receiveAudioData(inputBuffer, numFrames); + } + } + + if (isConnected()) { + if (auto lock = Locker::tryLock(adapterNodeMutex_)) { + auto *adapterNode = static_cast(adapterNodeHandle_->audioNode.get()); + for (size_t channel = 0; channel < adapterNode->getChannelCount(); ++channel) { + auto *data = static_cast(inputBuffer->mBuffers[channel].mData); + adapterNode->buff_[channel]->write(data, numFrames); } } + } +} + +void IOSAudioRecorder::handleInputConfigurationChange() +{ + if (isIdle()) { + return; + } + + BOOL formatChanged = NO; + if (![nativeRecorder_ refreshResolvedInputFormatReturningChanged:&formatChanged]) { + return; + } + + if (!formatChanged) { + if (state_.load(std::memory_order_acquire) == RecorderState::Recording) { + [nativeRecorder_ setInputArmed:true]; + } + return; + } + + scheduleReprepareForLiveInput(); +} - if (usesCallback()) { - if (auto lock = Locker::tryLock(callbackMutex_)) { - std::static_pointer_cast(dataCallback_) - ->receiveAudioData(inputBuffer, numFrames); +void IOSAudioRecorder::scheduleReprepareForLiveInput() +{ + if (jsCallInvoker_ == nullptr) { + reprepareForLiveInput(); + return; + } + + auto recorder = lifetime_.lock(); + if (recorder == nullptr) { + return; + } + + jsCallInvoker_->invokeAsync([recorder](facebook::jsi::Runtime &) { + static_cast(recorder.get())->reprepareForLiveInput(); + }); +} + +Result IOSAudioRecorder::reprepareForLiveInput() +{ + if (isIdle()) { + return Result::Ok(None); + } + + // those values are resolved earlier with actual correct values + AVAudioFormat *inputFormat = [nativeRecorder_ getResolvedInputFormat]; + int maxInputBufferLength = [nativeRecorder_ getResolvedBufferSize]; + + if (!hasUsableRecorderFormat(inputFormat) || maxInputBufferLength <= 0) { + return Result::Err("Recorder input format is unavailable"); + } + + const bool shouldArmInput = state_.load(std::memory_order_acquire) == RecorderState::Recording; + [nativeRecorder_ setInputArmed:false]; + + if (usesFileOutput()) { + auto fileResult = reprepareFileWriter(inputFormat, maxInputBufferLength); + if (fileResult.is_err()) { + if (shouldArmInput) { + [nativeRecorder_ setInputArmed:true]; } + return fileResult; } + } - if (isConnected()) { - if (auto lock = Locker::tryLock(adapterNodeMutex_)) { - auto *adapterNode = static_cast(adapterNodeHandle_->audioNode.get()); - for (size_t channel = 0; channel < adapterNode->getChannelCount(); ++channel) { - auto *data = static_cast(inputBuffer->mBuffers[channel].mData); - adapterNode->buff_[channel]->write(data, numFrames); - } + if (usesCallback()) { + auto callbackResult = reprepareCallback(inputFormat, maxInputBufferLength); + if (callbackResult.is_err()) { + if (shouldArmInput) { + [nativeRecorder_ setInputArmed:true]; } + return callbackResult; } - }; + } - nativeRecorder_ = [[NativeAudioRecorder alloc] initWithReceiverBlock:receiverBlock]; + if (isConnected() && adapterNodeHandle_ != nullptr) { + reprepareAdapter(inputFormat, maxInputBufferLength); + } + + streamSampleRate_ = static_cast(recorderFormatSampleRate(inputFormat)); + + if (shouldArmInput) { + [nativeRecorder_ setInputArmed:true]; + } + + return Result::Ok(None); +} + +Result IOSAudioRecorder::reprepareFileWriter( + AVAudioFormat *inputFormat, + int maxInputBufferLength) +{ + std::scoped_lock lock(fileWriterMutex_); + + if (fileWriter_ == nullptr) { + return Result::Err("File writer is unavailable"); + } + + if (auto rotatingWriter = std::dynamic_pointer_cast(fileWriter_)) { + auto result = rotatingWriter->reprepareStreamFormat( + inputFormat, static_cast(maxInputBufferLength)); + if (result.is_err()) { + fileOutputConfigured_.store(false, std::memory_order_release); + return Result::Err( + "Failed to reopen file for writing: " + result.unwrap_err()); + } + + filePath_ = result.unwrap(); + fileOutputConfigured_.store(true, std::memory_order_release); + return Result::Ok(None); + } + + auto iosWriter = std::static_pointer_cast(fileWriter_); + auto result = + iosWriter->reopenForInputFormatChange(inputFormat, static_cast(maxInputBufferLength)); + if (result.is_err()) { + fileOutputConfigured_.store(false, std::memory_order_release); + return Result::Err( + "Failed to reopen file for writing: " + result.unwrap_err()); + } + + filePath_ = result.unwrap(); + recordingSegmentPaths_.push_back(filePath_); + fileOutputConfigured_.store(true, std::memory_order_release); + return Result::Ok(None); +} + +Result IOSAudioRecorder::reprepareCallback( + AVAudioFormat *inputFormat, + int maxInputBufferLength) +{ + std::scoped_lock lock(callbackMutex_); + + if (dataCallback_ == nullptr) { + return Result::Err("Callback is unavailable"); + } + + auto result = std::static_pointer_cast(dataCallback_) + ->prepare(inputFormat, static_cast(maxInputBufferLength)); + if (result.is_err()) { + callbackOutputConfigured_.store(false, std::memory_order_release); + return Result::Err("Failed to prepare callback: " + result.unwrap_err()); + } + + callbackOutputConfigured_.store(true, std::memory_order_release); + return Result::Ok(None); +} + +void IOSAudioRecorder::reprepareAdapter(AVAudioFormat *inputFormat, int maxInputBufferLength) +{ + std::scoped_lock lock(adapterNodeMutex_); + if (adapterNodeHandle_ == nullptr) { + return; + } + + static_cast(adapterNodeHandle_->audioNode.get()) + ->init( + static_cast(maxInputBufferLength), + recorderFormatChannelCount(inputFormat), + recorderFormatSampleRate(inputFormat)); + connectedConfigured_.store(true, std::memory_order_release); } IOSAudioRecorder::~IOSAudioRecorder() { stop(); + nativeRecorder_.onInputConfigurationChange = nil; + { std::scoped_lock lock(callbackMutex_, fileWriterMutex_, adapterNodeMutex_); callbackOutputConfigured_.store(false, std::memory_order_release); diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioRecorder.h b/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioRecorder.h index 576f3ac8a..3c7339188 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioRecorder.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/NativeAudioRecorder.h @@ -12,12 +12,14 @@ typedef void (^AudioReceiverBlock)(const AudioBufferList *inputBuffer, int numFr @property (nonatomic, strong) AVAudioFormat *resolvedInputFormat; @property (nonatomic, assign) int resolvedBufferSize; @property (atomic, assign) BOOL inputArmed; +@property (nonatomic, copy) void (^onInputConfigurationChange)(void); - (instancetype)initWithReceiverBlock:(AudioReceiverBlock)receiverBlock; - (int)getBufferSize; - (AVAudioFormat *)getResolvedInputFormat; - (int)getResolvedBufferSize; +- (BOOL)refreshResolvedInputFormatReturningChanged:(BOOL *)formatChanged; - (BOOL)start:(NSError **)error; - (void)stop; 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 d5724728d..d2c3f2536 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 @@ -57,17 +57,15 @@ - (AVAudioFormat *)getResolvedInputFormat - (int)getBufferSize { - // NOTE: this method should be called only after the session is activated AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + double sampleRate = audioSession.sampleRate; - // TMPfix: it seems that buffer duration in some cases (background/device change) can switch - // to longer values, exceeding buffer size predicted after session start - // since it is just a couple of buffers we can set min value of 200ms - // to enforce we always have enough frames allocated to pass further down the pipeline - float bufferDuration = MAX(audioSession.IOBufferDuration, 0.2); + if (self.resolvedInputFormat != nil && self.resolvedInputFormat.sampleRate > 0) { + sampleRate = self.resolvedInputFormat.sampleRate; + } - // IOS returns buffer duration rounded, but expects the buffer size to be power of two in runtime - return nextPowerOfTwo(ceil(bufferDuration * audioSession.sampleRate)); + float bufferDuration = MAX(audioSession.IOBufferDuration, 0.2); + return nextPowerOfTwo(ceil(bufferDuration * sampleRate)); } - (int)getResolvedBufferSize @@ -75,6 +73,29 @@ - (int)getResolvedBufferSize return self.resolvedBufferSize; } +- (BOOL)refreshResolvedInputFormatReturningChanged:(BOOL *)formatChanged +{ + AVAudioFormat *liveFormat = [self readLiveInputFormat]; + + if (liveFormat == nil || liveFormat.sampleRate <= 0 || liveFormat.channelCount == 0) { + return NO; + } + + AVAudioFormat *previousFormat = self.resolvedInputFormat; + BOOL changed = previousFormat == nil || previousFormat.sampleRate != liveFormat.sampleRate || + previousFormat.channelCount != liveFormat.channelCount || + previousFormat.isInterleaved != liveFormat.isInterleaved; + + self.resolvedInputFormat = liveFormat; + self.resolvedBufferSize = [self getBufferSize]; + + if (formatChanged != nil) { + *formatChanged = changed; + } + + return YES; +} + - (BOOL)start:(NSError **)error { AudioEngine *audioEngine = [AudioEngine sharedInstance]; @@ -93,7 +114,8 @@ - (BOOL)start:(NSError **)error self.resolvedBufferSize = 0; [audioEngine stopIfNecessary]; - [audioEngine attachInputNodeWithReceiverBlock:self.receiverSinkBlock]; + [audioEngine attachInputNodeWithReceiverBlock:self.receiverSinkBlock + onInputConfigurationChange:self.onInputConfigurationChange]; if (![audioEngine startIfNecessary]) { [audioEngine detachInputNode]; @@ -128,8 +150,6 @@ - (void)stop self.inputArmed = NO; [audioEngine detachInputNode]; [audioEngine stopIfPossible]; - // This makes sure that the engine releases the input properly when we no longer need it - // (i.e. no more misleading dot) [audioEngine restartAudioEngine]; self.resolvedInputFormat = nil; self.resolvedBufferSize = 0; @@ -150,7 +170,11 @@ - (void)resume assert(audioEngine != nil); if ([audioEngine startIfNecessary]) { - self.inputArmed = YES; + if (self.onInputConfigurationChange != nil) { + self.onInputConfigurationChange(); + } else { + self.inputArmed = YES; + } } } @@ -161,6 +185,7 @@ - (void)cleanup self.resolvedBufferSize = 0; self.receiverBlock = nil; self.receiverSinkBlock = nil; + self.onInputConfigurationChange = nil; } @end diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSFileWriter.h b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSFileWriter.h index 572fd8460..d15583af5 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSFileWriter.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSFileWriter.h @@ -41,6 +41,9 @@ class IOSFileWriter : public AudioFileWriter { AVAudioFormat *bufferFormat, size_t maxInputBufferLength, const std::string &fileNameOverride); + OpenFileResult reopenForInputFormatChange( + AVAudioFormat *bufferFormat, + size_t maxInputBufferLength); CloseFileResult closeFile() override; void writeAudioData(const AudioBufferList *audioBufferList, int numFrames) override; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSFileWriter.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSFileWriter.mm index 1b6662279..681d52372 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSFileWriter.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSFileWriter.mm @@ -140,6 +140,23 @@ } } +OpenFileResult IOSFileWriter::reopenForInputFormatChange( + AVAudioFormat *bufferFormat, + size_t maxInputBufferLength) +{ + @autoreleasepool { + if (isFileOpen()) { + auto closeResult = closeFile(); + if (closeResult.is_err()) { + return OpenFileResult::Err( + "Failed to finalize recording segment: " + closeResult.unwrap_err()); + } + } + + return openFile(bufferFormat, maxInputBufferLength, ""); + } +} + void IOSFileWriter::rollbackFailedOpen() { offloader_.reset(); diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.h b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.h index 456aaaac0..b59b4e806 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.h @@ -30,6 +30,9 @@ class IOSRotatingFileWriter : public IOSFileWriter, public RotatingFileWriter { AVAudioFormat *streamFormat, size_t streamMaxBufferSizeInFrames, const std::string &fileNameOverride) override; + OpenFileResult reprepareStreamFormat( + AVAudioFormat *streamFormat, + size_t streamMaxBufferSizeInFrames); CloseFileResult closeFile() override; [[nodiscard]] double getCurrentDuration() const override; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm index bd9bc6bb1..c2c232785 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm @@ -46,6 +46,27 @@ return openInnerWriter(); } +OpenFileResult IOSRotatingFileWriter::reprepareStreamFormat( + AVAudioFormat *streamFormat, + size_t streamMaxBufferSizeInFrames) +{ + streamFormat_ = streamFormat; + streamMaxBufferSizeInFrames_ = streamMaxBufferSizeInFrames; + + if (currentWriter_ == nullptr) { + currentWriter_ = writerFactory_(fileProperties_); + return openInnerWriter(); + } + + rotateFiles(); + + if (currentWriter_ == nullptr) { + return OpenFileResult::Err("Failed to reopen file for writing after input format change"); + } + + return OpenFileResult::Ok(currentWriter_->getFilePath()); +} + void IOSRotatingFileWriter::writeAudioData(AudioDataType data, int numFrames) { if (currentWriter_ == nullptr) { diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.h b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.h index e4f9fdd39..71279285c 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.h @@ -33,7 +33,8 @@ typedef NS_ENUM(NSInteger, AudioEngineState) { channelCount:(AVAudioChannelCount)channelCount; - (void)detachSourceNodeWithId:(NSString *)sourceNodeId; -- (void)attachInputNodeWithReceiverBlock:(AVAudioSinkNodeReceiverBlock)receiverBlock; +- (void)attachInputNodeWithReceiverBlock:(AVAudioSinkNodeReceiverBlock)receiverBlock + onInputConfigurationChange:(void (^)(void))onInputConfigurationChange; - (void)detachInputNode; - (AVAudioFormat *)getLiveInputFormat; 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 d91ed60f9..6aa349dba 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 @@ -17,6 +17,7 @@ @implementation AudioEngineSourceRegistration @interface AudioEngineInputRegistration : NSObject @property (nonatomic, copy) AVAudioSinkNodeReceiverBlock receiverBlock; +@property (nonatomic, copy) void (^onInputConfigurationChange)(void); @end @@ -43,6 +44,7 @@ - (void)materializeTrackedNodesIfNeeded; - (AVAudioFormat *)liveInputFormat; - (void)resetInputNode; - (void)rebuildAudioEngineAndResumeIfNeeded; +- (void)notifyConfigurationChanges; @end @@ -161,6 +163,27 @@ - (void)materializeSourceNodeWithId:(NSString *)sourceNodeId [self.audioEngine connect:sourceNode to:self.audioEngine.mainMixerNode format:format]; } +- (AVAudioFormat *)liveInputFormat +{ + if (self.audioEngine == nil) { + return nil; + } + + AVAudioInputNode *engineInputNode = self.audioEngine.inputNode; + + if (engineInputNode == nil) { + return nil; + } + + AVAudioFormat *inputFormat = [engineInputNode outputFormatForBus:0]; + + if (inputFormat == nil || inputFormat.sampleRate <= 0 || inputFormat.channelCount == 0) { + return nil; + } + + return inputFormat; +} + - (AVAudioFormat *)currentInputConnectionFormat { AVAudioFormat *inputFormat = [self liveInputFormat]; @@ -186,6 +209,14 @@ - (BOOL)materializeInputNodeIfNeeded return YES; } + NSError *sessionError = nil; + if (![self.sessionManager ensureActive:true error:&sessionError]) { + NSLog( + @"Error while activating audio session before input materialization: %@", + [sessionError debugDescription]); + return NO; + } + AVAudioFormat *inputFormat = [self currentInputConnectionFormat]; if (inputFormat == nil) { @@ -253,6 +284,7 @@ - (void)detachSourceNodeWithId:(NSString *)sourceNodeId } - (void)attachInputNodeWithReceiverBlock:(AVAudioSinkNodeReceiverBlock)receiverBlock + onInputConfigurationChange:(void (^)(void))onInputConfigurationChange { std::scoped_lock lock(_engineLock); [self createAudioEngineIfNeeded]; @@ -263,6 +295,7 @@ - (void)attachInputNodeWithReceiverBlock:(AVAudioSinkNodeReceiverBlock)receiverB AudioEngineInputRegistration *registration = [[AudioEngineInputRegistration alloc] init]; registration.receiverBlock = receiverBlock; + registration.onInputConfigurationChange = onInputConfigurationChange; self.inputRegistration = registration; [self materializeInputNodeIfNeeded]; @@ -292,30 +325,8 @@ - (void)detachInputNode [self resetInputNode]; } -- (AVAudioFormat *)liveInputFormat -{ - if (self.audioEngine == nil) { - return nil; - } - - AVAudioInputNode *engineInputNode = self.audioEngine.inputNode; - - if (engineInputNode == nil) { - return nil; - } - - AVAudioFormat *inputFormat = [engineInputNode outputFormatForBus:0]; - - if (inputFormat == nil || inputFormat.sampleRate <= 0 || inputFormat.channelCount == 0) { - return nil; - } - - return inputFormat; -} - - (AVAudioFormat *)getLiveInputFormat { - std::scoped_lock lock(_engineLock); return [self liveInputFormat]; } @@ -377,6 +388,7 @@ - (void)onInterruptionEnd:(bool)shouldResume if (!shouldResume) { self.state = AudioEngineState::AudioEngineStatePaused; + [self notifyConfigurationChanges]; return; } @@ -388,11 +400,20 @@ - (void)onInterruptionEnd:(bool)shouldResume @"Error while restarting the audio engine after interruption: %@", [error debugDescription]); self.state = AudioEngineState::AudioEngineStateIdle; + [self notifyConfigurationChanges]; return; } self.state = AudioEngineState::AudioEngineStateRunning; self.sessionDeactivationInvalidatedGraph = false; + [self notifyConfigurationChanges]; +} + +- (void)notifyConfigurationChanges +{ + if (self.inputRegistration != nil && self.inputRegistration.onInputConfigurationChange != nil) { + self.inputRegistration.onInputConfigurationChange(); + } } - (AudioEngineState)getState @@ -426,6 +447,8 @@ - (void)rebuildAudioEngineAndResumeIfNeeded [self startEngine]; } + [self notifyConfigurationChanges]; + _isRebuildingAudioEngine = NO; } @@ -497,6 +520,7 @@ - (bool)startIfNecessary std::scoped_lock lock(_engineLock); if (self.state == AudioEngineState::AudioEngineStateRunning && self.audioEngine != nil && [self.audioEngine isRunning]) { + [self materializeTrackedNodesIfNeeded]; return true; } 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 6761a4369..830fc845b 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 @@ -218,6 +218,23 @@ - (void)handleRouteChange:(NSNotification *)notification invokeHandlerWithEventName:audioapi::AudioEvent::ROUTE_CHANGE payload:audioapi::StringPayload{ .name = "reason", .reason = [reasonStr UTF8String]}]; + + AudioEngine *audioEngine = self.audioAPIModule.audioEngine; + AudioSessionManager *sessionManager = self.audioAPIModule.audioSessionManager; + + switch (routeChangeReason) { + case AVAudioSessionRouteChangeReasonNewDeviceAvailable: + case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: + case AVAudioSessionRouteChangeReasonRouteConfigurationChange: { + dispatch_async(dispatch_get_main_queue(), ^{ + [sessionManager markInactive]; + [audioEngine restartAudioEngine]; + }); + break; + } + default: + break; + } } - (void)handleMediaServicesReset:(NSNotification *)notification From 766f469ea386527260c51e7e1fcb1402acd08dcc Mon Sep 17 00:00:00 2001 From: michal Date: Thu, 30 Jul 2026 11:54:43 +0200 Subject: [PATCH 2/3] fix: remove running on js thread and code duplication --- .../IOSAudioRecorderTests.mm | 2 +- .../inputs/AudioRecorderHostObject.cpp | 3 +- .../ios/audioapi/ios/core/IOSAudioRecorder.h | 18 ++--------- .../ios/audioapi/ios/core/IOSAudioRecorder.mm | 31 ++----------------- .../ios/system/SystemNotificationManager.mm | 9 ++---- 5 files changed, 9 insertions(+), 54 deletions(-) diff --git a/apps/fabric-example/ios/FabricExampleTests/IOSAudioRecorderTests.mm b/apps/fabric-example/ios/FabricExampleTests/IOSAudioRecorderTests.mm index 7ea7f1409..56db5fcb9 100644 --- a/apps/fabric-example/ios/FabricExampleTests/IOSAudioRecorderTests.mm +++ b/apps/fabric-example/ios/FabricExampleTests/IOSAudioRecorderTests.mm @@ -209,7 +209,7 @@ - (void)cleanup public: explicit TestableIOSAudioRecorder( const std::shared_ptr &audioEventHandlerRegistry) - : IOSAudioRecorder(audioEventHandlerRegistry, nullptr) {} + : IOSAudioRecorder(audioEventHandlerRegistry) {} NativeAudioRecorder *replaceNativeRecorder(NativeAudioRecorder *nativeRecorder) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp index 374029b3d..5fe0476c4 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp @@ -26,8 +26,7 @@ AudioRecorderHostObject::AudioRecorderHostObject( #ifdef ANDROID audioRecorder_ = std::make_shared(audioEventHandlerRegistry); #else - audioRecorder_ = std::make_shared(audioEventHandlerRegistry, callInvoker); - std::static_pointer_cast(audioRecorder_)->bindLifetime(); + audioRecorder_ = std::make_shared(audioEventHandlerRegistry); #endif promiseVendor_ = std::make_shared(runtime, callInvoker); 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 1ccd293fc..7e26361be 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 @@ -16,15 +16,10 @@ typedef struct objc_object AVAudioFormat; #include #include -#include #include #include #include -namespace facebook::react { -class CallInvoker; -} - namespace audioapi { class RecorderCallback; @@ -33,16 +28,11 @@ class AudioFileProperties; class AudioEventHandlerRegistry; class AudioFileWriter; -class IOSAudioRecorder : public AudioRecorder, - public std::enable_shared_from_this { +class IOSAudioRecorder : public AudioRecorder { public: - IOSAudioRecorder( - const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &jsCallInvoker); + IOSAudioRecorder(const std::shared_ptr &audioEventHandlerRegistry); ~IOSAudioRecorder() override; - void bindLifetime(); - Result start(const std::string &fileNameOverride = "") override; Result, double, double>, std::string> stop() override; @@ -79,7 +69,6 @@ class IOSAudioRecorder : public AudioRecorder, const std::shared_ptr &properties, const std::string &fileNameOverride = ""); Result reprepareForLiveInput(); - void scheduleReprepareForLiveInput(); void handleInputConfigurationChange(); Result reprepareFileWriter( AVAudioFormat *inputFormat, @@ -90,9 +79,6 @@ class IOSAudioRecorder : public AudioRecorder, void reprepareAdapter(AVAudioFormat *inputFormat, int maxInputBufferLength); void runSideEffects(const AudioBufferList *inputBuffer, int numFrames); - std::weak_ptr lifetime_; - std::shared_ptr jsCallInvoker_; - std::vector recordingSegmentPaths_; std::atomic streamSampleRate_{0.0f}; /// Updated on the audio thread from each input callback `numFrames`. diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm index e3c125ee6..709a2607b 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm @@ -2,8 +2,6 @@ #import #import #import -#import - #include #include #include @@ -69,9 +67,8 @@ static void cleanupStartedRecorder( /// This "method" should be called from the JS thread only. /// @param audioEventHandlerRegistry Shared pointer to the AudioEventHandlerRegistry for event handling. IOSAudioRecorder::IOSAudioRecorder( - const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &jsCallInvoker) - : AudioRecorder(audioEventHandlerRegistry), jsCallInvoker_(jsCallInvoker) + const std::shared_ptr &audioEventHandlerRegistry) + : AudioRecorder(audioEventHandlerRegistry) { AudioReceiverBlock receiverBlock = ^(const AudioBufferList *inputBuffer, int numFrames) { if (numFrames > 0) { @@ -86,11 +83,6 @@ static void cleanupStartedRecorder( nativeRecorder_.onInputConfigurationChange = ^{ this->handleInputConfigurationChange(); }; } -void IOSAudioRecorder::bindLifetime() -{ - lifetime_ = shared_from_this(); -} - void IOSAudioRecorder::runSideEffects(const AudioBufferList *inputBuffer, int numFrames) { if (usesFileOutput()) { @@ -135,24 +127,7 @@ static void cleanupStartedRecorder( return; } - scheduleReprepareForLiveInput(); -} - -void IOSAudioRecorder::scheduleReprepareForLiveInput() -{ - if (jsCallInvoker_ == nullptr) { - reprepareForLiveInput(); - return; - } - - auto recorder = lifetime_.lock(); - if (recorder == nullptr) { - return; - } - - jsCallInvoker_->invokeAsync([recorder](facebook::jsi::Runtime &) { - static_cast(recorder.get())->reprepareForLiveInput(); - }); + reprepareForLiveInput(); } Result IOSAudioRecorder::reprepareForLiveInput() 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 830fc845b..383ee14f4 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 @@ -219,17 +219,12 @@ - (void)handleRouteChange:(NSNotification *)notification payload:audioapi::StringPayload{ .name = "reason", .reason = [reasonStr UTF8String]}]; - AudioEngine *audioEngine = self.audioAPIModule.audioEngine; - AudioSessionManager *sessionManager = self.audioAPIModule.audioSessionManager; - switch (routeChangeReason) { case AVAudioSessionRouteChangeReasonNewDeviceAvailable: case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: case AVAudioSessionRouteChangeReasonRouteConfigurationChange: { - dispatch_async(dispatch_get_main_queue(), ^{ - [sessionManager markInactive]; - [audioEngine restartAudioEngine]; - }); + handleEngineConfigurationChange: + nil; break; } default: From 9feafb521d819cf1cbcebc350dbd25e7283b13a7 Mon Sep 17 00:00:00 2001 From: michal Date: Thu, 30 Jul 2026 12:38:20 +0200 Subject: [PATCH 3/3] fix: race condition between processing and cleaning recorder adapter --- .../audioapi/core/sources/RecorderAdapterNode.cpp | 13 ++++++++++++- .../cpp/audioapi/core/sources/RecorderAdapterNode.h | 5 +++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp index 202bb6045..9b3ac4099 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp @@ -2,12 +2,14 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace audioapi { @@ -59,15 +61,24 @@ void RecorderAdapterNode::init(size_t bufferSize, int channelCount, float sample } void RecorderAdapterNode::adapterCleanup() { + isInitialized_.store(false, std::memory_order_release); + waitForProcessQuiescence(); + needsResampling_ = false; buff_.clear(); resampler_.reset(); overflowSize_ = 0; +} - isInitialized_.store(false, std::memory_order_release); +void RecorderAdapterNode::waitForProcessQuiescence() const { + while (currentProcesses_.load(std::memory_order_acquire) != 0) { + std::this_thread::yield(); + } } void RecorderAdapterNode::processNode(int framesToProcess) { + const CurrentRenderScope processScope(currentProcesses_); + if (!isInitialized_.load(std::memory_order_acquire)) { audioBuffer_->zero(); return; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.h index 15fef1c4d..bb72c90d8 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include @@ -39,6 +41,7 @@ class RecorderAdapterNode : public AudioNode { private: void readFrames(AudioBuffer &target, size_t framesToRead); void processResampled(int framesToProcess); + void waitForProcessQuiescence() const; std::unique_ptr resampler_; bool needsResampling_ = false; @@ -53,6 +56,8 @@ class RecorderAdapterNode : public AudioNode { size_t overflowSize_ = 0; std::atomic isInitialized_{false}; + /// Incremented around each processNode() call; adapterCleanup waits for quiescence. + std::atomic currentProcesses_{0}; }; } // namespace audioapi