From dbb39a2b7e9e6dcfa20f93947479a43d643b8f0c Mon Sep 17 00:00:00 2001 From: poneciak Date: Sat, 24 Jan 2026 15:27:05 +0100 Subject: [PATCH 1/3] feat: made AudioFileWriter more abstract --- .../android/core/AndroidAudioRecorder.cpp | 72 ++++++++++++++----- .../core/utils/AndroidFileWriterBackend.h | 12 ++-- .../utils/ffmpegBackend/FFmpegFileWriter.cpp | 29 ++++---- .../utils/ffmpegBackend/FFmpegFileWriter.h | 5 +- .../miniaudioBackend/MiniAudioFileWriter.cpp | 20 +++--- .../miniaudioBackend/MiniAudioFileWriter.h | 5 +- .../audioapi/core/inputs/AudioRecorder.cpp | 4 +- .../cpp/audioapi/core/inputs/AudioRecorder.h | 1 + .../cpp/audioapi/core/utils/AudioFileWriter.h | 12 +++- .../ios/audioapi/ios/core/IOSAudioRecorder.mm | 25 ++++--- .../audioapi/ios/core/utils/IOSFileWriter.h | 8 +-- .../audioapi/ios/core/utils/IOSFileWriter.mm | 24 ++++--- 12 files changed, 142 insertions(+), 75 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp index dc0a9ad1f..3a11fa4de 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -120,9 +121,31 @@ Result AndroidAudioRecorder::start() { } if (usesFileOutput()) { - auto fileResult = - std::static_pointer_cast(fileWriter_) - ->openFile(streamSampleRate_, streamChannelCount_, streamMaxBufferSizeInFrames_); + if (fileProperties_->format == AudioFileProperties::Format::WAV) { + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, + fileProperties_, + streamSampleRate_, + streamChannelCount_, + streamMaxBufferSizeInFrames_); + } else { +#if !RN_AUDIO_API_FFMPEG_DISABLED + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, + fileProperties_, + streamSampleRate_, + streamChannelCount_, + streamMaxBufferSizeInFrames_); +#else + return Result::Err( + "FFmpeg backend is disabled. Cannot create file writer for the requested format. Use WAV " + "format instead."); +#endif + } + + fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire)); + + auto fileResult = fileWriter_->openFile(); if (!fileResult.is_ok()) { return Result::Err( @@ -130,6 +153,11 @@ Result AndroidAudioRecorder::start() { } filePath_ = fileResult.unwrap(); + __android_log_print( + ANDROID_LOG_INFO, + "AndroidAudioRecorder", + "File created successfully at path: %s", + filePath_.c_str()); } if (usesCallback()) { @@ -212,23 +240,34 @@ Result, std::string> AndroidAudioRecorde Result AndroidAudioRecorder::enableFileOutput( std::shared_ptr properties) { std::scoped_lock fileWriterLock(fileWriterMutex_); + fileProperties_ = properties; - if (properties->format == AudioFileProperties::Format::WAV) { - fileWriter_ = std::make_shared(audioEventHandlerRegistry_, properties); - } else { + if (!isIdle()) { + if (properties->format == AudioFileProperties::Format::WAV) { + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, + properties, + streamSampleRate_, + streamChannelCount_, + streamMaxBufferSizeInFrames_); + } else { #if !RN_AUDIO_API_FFMPEG_DISABLED - fileWriter_ = std::make_shared( - audioEventHandlerRegistry_, properties); + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, + properties, + streamSampleRate_, + streamChannelCount_, + streamMaxBufferSizeInFrames_); #else - return Result::Err( - "FFmpeg backend is disabled. Cannot create file writer for the requested format. Use WAV format instead."); + return Result::Err( + "FFmpeg backend is disabled. Cannot create file writer for the requested format. Use WAV " + "format instead."); #endif - } + } - if (!isIdle()) { - auto fileResult = - std::static_pointer_cast(fileWriter_) - ->openFile(streamSampleRate_, streamChannelCount_, streamMaxBufferSizeInFrames_); + fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire)); + + auto fileResult = fileWriter_->openFile(); if (!fileResult.is_ok()) { return Result::Err( @@ -355,8 +394,7 @@ oboe::DataCallbackResult AndroidAudioRecorder::onAudioReady( if (usesFileOutput()) { if (auto fileWriterLock = Locker::tryLock(fileWriterMutex_)) { - std::static_pointer_cast(fileWriter_) - ->writeAudioData(audioData, numFrames); + fileWriter_->writeAudioData(audioData, numFrames); } } diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.h index 802a225b3..987b33822 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.h @@ -14,10 +14,14 @@ class AndroidFileWriterBackend : public AudioFileWriter { public: explicit AndroidFileWriterBackend( const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &fileProperties) - : AudioFileWriter(audioEventHandlerRegistry, fileProperties) {} - - virtual OpenFileResult openFile(float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize) = 0; + const std::shared_ptr &fileProperties, + float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize) + : AudioFileWriter(audioEventHandlerRegistry, fileProperties), + streamSampleRate_(streamSampleRate), + streamChannelCount_(streamChannelCount), + streamMaxBufferSize_(streamMaxBufferSize) {} + + virtual OpenFileResult openFile() = 0; virtual bool writeAudioData(void *data, int numFrames) = 0; std::string getFilePath() const override { return filePath_; } diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp index 0b272e752..24a38d2d7 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp @@ -30,8 +30,16 @@ namespace audioapi::android::ffmpeg { FFmpegAudioFileWriter::FFmpegAudioFileWriter( const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &fileProperties) - : AndroidFileWriterBackend(audioEventHandlerRegistry, fileProperties) { + const std::shared_ptr &fileProperties, + float streamSampleRate, + int32_t streamChannelCount, + int32_t streamMaxBufferSize) + : AndroidFileWriterBackend( + audioEventHandlerRegistry, + fileProperties, + streamSampleRate, + streamChannelCount, + streamMaxBufferSize) { // Set flush interval from properties, limit minimum to 100ms // to avoid people hurting themselves too much flushIntervalMs_ = std::max(fileProperties_->androidFlushIntervalMs, defaultFlushInterval); @@ -50,13 +58,7 @@ FFmpegAudioFileWriter::~FFmpegAudioFileWriter() { /// @param streamChannelCount The number of channels in the incoming audio stream. /// @param streamMaxBufferSize The estimated maximum buffer size for the incoming audio stream. /// @returns Success status with file path or Error status with message. -OpenFileResult FFmpegAudioFileWriter::openFile( - float streamSampleRate, - int32_t streamChannelCount, - int32_t streamMaxBufferSize) { - streamSampleRate_ = streamSampleRate; - streamChannelCount_ = streamChannelCount; - streamMaxBufferSize_ = streamMaxBufferSize; +OpenFileResult FFmpegAudioFileWriter::openFile() { framesWritten_.store(0, std::memory_order_release); nextPts_ = 0; Result result = Result::Ok(None); @@ -78,11 +80,10 @@ OpenFileResult FFmpegAudioFileWriter::openFile( .and_then([this, codec](auto) { return configureAndOpenCodec(codec); }) .and_then([this](auto) { return initializeStream(); }) .and_then([this](auto) { return openIOAndWriteHeader(); }) - .and_then([this, streamSampleRate, streamChannelCount](auto) { - return initializeResampler(streamSampleRate, streamChannelCount); - }) - .and_then([this, streamMaxBufferSize, filePath = std::move(filePath_)](auto) { - initializeBuffers(streamMaxBufferSize); + .and_then( + [this](auto) { return initializeResampler(streamSampleRate_, streamChannelCount_); }) + .and_then([this, filePath = std::move(filePath_)](auto) { + initializeBuffers(streamMaxBufferSize_); isFileOpen_.store(true, std::memory_order_release); return OpenFileResult::Ok(filePath); }); diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h index 6c92f6585..9f1160296 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h @@ -26,10 +26,11 @@ class FFmpegAudioFileWriter : public AndroidFileWriterBackend { public: explicit FFmpegAudioFileWriter( const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &fileProperties); + const std::shared_ptr &fileProperties, + float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize); ~FFmpegAudioFileWriter(); - OpenFileResult openFile(float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize) override; + OpenFileResult openFile() override; CloseFileResult closeFile() override; bool writeAudioData(void *data, int numFrames) override; diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.cpp index 507ddf7c3..0d7a19e8b 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.cpp @@ -40,8 +40,16 @@ inline ma_format getDataFormat(const std::shared_ptr &prope MiniAudioFileWriter::MiniAudioFileWriter( const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &fileProperties) - : AndroidFileWriterBackend(audioEventHandlerRegistry, fileProperties) {} + const std::shared_ptr &fileProperties, + float streamSampleRate, + int32_t streamChannelCount, + int32_t streamMaxBufferSize) + : AndroidFileWriterBackend( + audioEventHandlerRegistry, + fileProperties, + streamSampleRate, + streamChannelCount, + streamMaxBufferSize) {} MiniAudioFileWriter::~MiniAudioFileWriter() { isFileOpen_.store(false, std::memory_order_release); @@ -72,13 +80,7 @@ MiniAudioFileWriter::~MiniAudioFileWriter() { /// @param streamChannelCount The channel count of the incoming audio stream. /// @param streamMaxBufferSize The maximum buffer size of the incoming audio stream. /// @return The status of the file opening operation. -OpenFileResult MiniAudioFileWriter::openFile( - float streamSampleRate, - int32_t streamChannelCount, - int32_t streamMaxBufferSize) { - streamSampleRate_ = streamSampleRate; - streamChannelCount_ = streamChannelCount; - streamMaxBufferSize_ = streamMaxBufferSize; +OpenFileResult MiniAudioFileWriter::openFile() { ma_result result; framesWritten_.store(0, std::memory_order_release); diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h index f2fb2f3f6..1c668fcbb 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h @@ -14,10 +14,11 @@ class MiniAudioFileWriter : public AndroidFileWriterBackend { public: explicit MiniAudioFileWriter( const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &fileProperties); + const std::shared_ptr &fileProperties, + float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize); ~MiniAudioFileWriter(); - OpenFileResult openFile(float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize) override; + OpenFileResult openFile() override; CloseFileResult closeFile() override; bool writeAudioData(void *data, int numFrames) override; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.cpp index b080d0596..af7b5c527 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.cpp @@ -11,11 +11,11 @@ namespace audioapi { void AudioRecorder::setOnErrorCallback(uint64_t callbackId) { std::scoped_lock lock(callbackMutex_, fileWriterMutex_, errorCallbackMutex_); - if (usesFileOutput()) { + if (usesFileOutput() && fileWriter_ != nullptr) { fileWriter_->setOnErrorCallback(callbackId); } - if (usesCallback()) { + if (usesCallback() && dataCallback_ != nullptr) { dataCallback_->setOnErrorCallback(callbackId); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.h b/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.h index 76e060b6e..81be543fe 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.h @@ -77,6 +77,7 @@ class AudioRecorder { std::shared_ptr adapterNode_ = nullptr; std::shared_ptr dataCallback_ = nullptr; std::shared_ptr audioEventHandlerRegistry_; + std::shared_ptr fileProperties_ = nullptr; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h index b7ad85c7f..2534ce676 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h @@ -14,6 +14,13 @@ class AudioEventHandlerRegistry; typedef Result OpenFileResult; typedef Result, std::string> CloseFileResult; +#ifdef ANDROID +typedef void *AudioDataType; +#else +#include +typedef const AudioBufferList *AudioDataType; +#endif + class AudioFileWriter { public: AudioFileWriter( @@ -22,8 +29,11 @@ class AudioFileWriter { virtual ~AudioFileWriter() = default; virtual CloseFileResult closeFile() = 0; - + virtual OpenFileResult openFile() = 0; virtual std::string getFilePath() const = 0; + + virtual bool writeAudioData(AudioDataType data, int numFrames) = 0; + virtual double getCurrentDuration() const = 0; void setOnErrorCallback(uint64_t callbackId); 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 57885fdf3..c3227a4af 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 @@ -36,8 +36,7 @@ AudioReceiverBlock receiverBlock = ^(const AudioBufferList *inputBuffer, int numFrames) { if (usesFileOutput()) { if (auto lock = Locker::tryLock(fileWriterMutex_)) { - std::static_pointer_cast(fileWriter_) - ->writeAudioData(inputBuffer, numFrames); + fileWriter_->writeAudioData(inputBuffer, numFrames); } } @@ -103,8 +102,11 @@ auto inputFormat = [nativeRecorder_ getInputFormat]; if (usesFileOutput()) { - auto fileResult = std::static_pointer_cast(fileWriter_) - ->openFile(inputFormat, maxInputBufferLength); + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, fileProperties_, inputFormat, maxInputBufferLength); + fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire)); + + auto fileResult = fileWriter_->openFile(); if (fileResult.is_err()) { return Result::Err( @@ -112,6 +114,7 @@ } filePath_ = fileResult.unwrap(); + NSLog(@"[IOSAudioRecorder] File created successfully at path: %s", filePath_.c_str()); } if (usesCallback()) { @@ -188,11 +191,16 @@ std::shared_ptr properties) { std::scoped_lock lock(fileWriterMutex_, errorCallbackMutex_); - fileWriter_ = std::make_shared(audioEventHandlerRegistry_, properties); + fileProperties_ = properties; if (!isIdle()) { - auto result = std::static_pointer_cast(fileWriter_) - ->openFile([nativeRecorder_ getInputFormat], [nativeRecorder_ getBufferSize]); + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, + properties, + [nativeRecorder_ getInputFormat], + [nativeRecorder_ getBufferSize]); + + auto result = fileWriter_->openFile(); if (result.is_err()) { return Result::Err( @@ -200,10 +208,9 @@ } filePath_ = result.unwrap(); + fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire)); } - fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire)); - fileOutputEnabled_.store(true, std::memory_order_release); return Result::Ok(filePath_); } 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 3bcc0f4af..32e1e7a52 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 @@ -24,12 +24,12 @@ class IOSFileWriter : public AudioFileWriter { public: IOSFileWriter( const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &fileProperties); - ~IOSFileWriter(); - - Result openFile( + const std::shared_ptr &fileProperties, AVAudioFormat *bufferFormat, size_t maxInputBufferLength); + ~IOSFileWriter(); + + Result openFile(); Result, std::string> closeFile() override; bool writeAudioData(const AudioBufferList *audioBufferList, int numFrames); 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 73e307c21..ac7737e17 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 @@ -11,9 +11,13 @@ namespace audioapi { IOSFileWriter::IOSFileWriter( const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &fileProperties) + const std::shared_ptr &fileProperties, + AVAudioFormat *bufferFormat, + size_t maxInputBufferLength) : AudioFileWriter(audioEventHandlerRegistry, fileProperties) { + bufferFormat_ = bufferFormat; + converterInputBufferSize_ = maxInputBufferLength; } IOSFileWriter::~IOSFileWriter() @@ -32,7 +36,7 @@ /// @param bufferFormat The audio format of the input buffer. /// @param maxInputBufferLength The maximum length of the input buffer in frames. /// @returns An OpenFileResult indicating success with the file path or an error message. -OpenFileResult IOSFileWriter::openFile(AVAudioFormat *bufferFormat, size_t maxInputBufferLength) +Result IOSFileWriter::openFile() { @autoreleasepool { if (audioFile_ != nil) { @@ -40,7 +44,6 @@ } framesWritten_.store(0, std::memory_order_release); - bufferFormat_ = bufferFormat; NSError *error = nil; NSDictionary *settings = ios::fileoptions::getFileSettings(fileProperties_); @@ -51,7 +54,7 @@ "Invalid file properties: sampleRate and channelCount must be greater than 0"); } - if (bufferFormat.sampleRate == 0 || bufferFormat.channelCount == 0) { + if (bufferFormat_.sampleRate == 0 || bufferFormat_.channelCount == 0) { return OpenFileResult::Err( "Invalid input format: sampleRate and channelCount must be greater than 0"); } @@ -59,7 +62,7 @@ audioFile_ = [[AVAudioFile alloc] initForWriting:fileURL_ settings:settings commonFormat:AVAudioPCMFormatFloat32 - interleaved:bufferFormat.interleaved + interleaved:bufferFormat_.interleaved error:&error]; if (error != nil) { @@ -68,20 +71,19 @@ [[error debugDescription] UTF8String]); } - converter_ = [[AVAudioConverter alloc] initFromFormat:bufferFormat + converter_ = [[AVAudioConverter alloc] initFromFormat:bufferFormat_ toFormat:[audioFile_ processingFormat]]; converter_.sampleRateConverterAlgorithm = AVSampleRateConverterAlgorithm_Normal; converter_.sampleRateConverterQuality = AVAudioQualityMax; converter_.primeMethod = AVAudioConverterPrimeMethod_None; - converterInputBufferSize_ = maxInputBufferLength; converterOutputBufferSize_ = std::max( - (double)maxInputBufferLength, - fileProperties_->sampleRate / bufferFormat.sampleRate * maxInputBufferLength); + (double)converterInputBufferSize_, + fileProperties_->sampleRate / bufferFormat_.sampleRate * converterInputBufferSize_); converterInputBuffer_ = - [[AVAudioPCMBuffer alloc] initWithPCMFormat:bufferFormat - frameCapacity:(AVAudioFrameCount)maxInputBufferLength]; + [[AVAudioPCMBuffer alloc] initWithPCMFormat:bufferFormat_ + frameCapacity:(AVAudioFrameCount)converterInputBufferSize_]; converterOutputBuffer_ = [[AVAudioPCMBuffer alloc] initWithPCMFormat:[audioFile_ processingFormat] frameCapacity:(AVAudioFrameCount)converterOutputBufferSize_]; From 98570b15ab1c1a8872e2c304ea6b5becd80ed348 Mon Sep 17 00:00:00 2001 From: poneciak Date: Thu, 29 Jan 2026 13:17:57 +0100 Subject: [PATCH 2/3] feat: implemented rotating file writer --- apps/common-app/src/demos/Record/Record.tsx | 2 +- .../audiodocs/docs/inputs/audio-recorder.mdx | 2 + .../android/core/AndroidAudioRecorder.cpp | 116 +++++++++++++----- .../core/utils/AndroidFileWriterBackend.h | 5 +- .../utils/ffmpegBackend/FFmpegFileWriter.cpp | 21 +++- .../utils/ffmpegBackend/FFmpegFileWriter.h | 1 + .../miniaudioBackend/MiniAudioFileWriter.cpp | 11 ++ .../miniaudioBackend/MiniAudioFileWriter.h | 1 + .../cpp/audioapi/core/utils/AudioFileWriter.h | 4 + .../core/utils/RotatingFileWriter.cpp | 101 +++++++++++++++ .../audioapi/core/utils/RotatingFileWriter.h | 48 ++++++++ .../audioapi/utils/AudioFileProperties.cpp | 6 + .../cpp/audioapi/utils/AudioFileProperties.h | 2 + .../ios/audioapi/ios/core/IOSAudioRecorder.mm | 42 +++++-- .../audioapi/ios/core/utils/IOSFileWriter.h | 1 + .../audioapi/ios/core/utils/IOSFileWriter.mm | 19 +++ .../src/core/AudioRecorder.ts | 1 + packages/react-native-audio-api/src/types.ts | 1 + 18 files changed, 338 insertions(+), 46 deletions(-) create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/RotatingFileWriter.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/RotatingFileWriter.h diff --git a/apps/common-app/src/demos/Record/Record.tsx b/apps/common-app/src/demos/Record/Record.tsx index 5c93ff2d1..f33d05288 100644 --- a/apps/common-app/src/demos/Record/Record.tsx +++ b/apps/common-app/src/demos/Record/Record.tsx @@ -228,7 +228,7 @@ const Record: FC = () => { }, [onPauseRecording, onResumeRecording]); useEffect(() => { - Recorder.enableFileOutput(); + Recorder.enableFileOutput({ rotateIntervalBytes: 1024 * 1024 }); return () => { Recorder.disableFileOutput(); diff --git a/packages/audiodocs/docs/inputs/audio-recorder.mdx b/packages/audiodocs/docs/inputs/audio-recorder.mdx index 87ee85956..c936785c1 100644 --- a/packages/audiodocs/docs/inputs/audio-recorder.mdx +++ b/packages/audiodocs/docs/inputs/audio-recorder.mdx @@ -717,6 +717,7 @@ interface OnAudioReadyEventType { ```tsx interface AudioRecorderFileOptions { channelCount?: number; + rotateIntervalBytes?: number; format?: FileFormat; preset?: FilePresetType; @@ -729,6 +730,7 @@ interface AudioRecorderFileOptions { ``` - `channelCount` - The desired channel count in the resulting file. not all file formats supports all possible channel counts. +- `rotateIntervalBytes` - The threshold size (in bytes) at which the recorder will start writing to a new file. If set to 0 (default), file output rotation is disabled. When active, new files are named with the original prefix appended with a timestamp. - `format` - The desired extension and file format of the recorder file. Check: [FileFormat](#fileformat) below. - `preset` - The desired recorder file properties, you can use either one of built-in properties or tweak low-level parameters yourself. Check [FilePresetType](#filepresettype) for more details. - `directory` - Either `FileDirectory.Cache` or `FileDirectory.Document` (default: `FileDirectory.Cache`). Determines the system directory that the file will be saved to. diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp index 3a11fa4de..daf26e958 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -121,26 +122,51 @@ Result AndroidAudioRecorder::start() { } if (usesFileOutput()) { - if (fileProperties_->format == AudioFileProperties::Format::WAV) { - fileWriter_ = std::make_shared( - audioEventHandlerRegistry_, - fileProperties_, - streamSampleRate_, - streamChannelCount_, - streamMaxBufferSizeInFrames_); - } else { + auto createWriter = + [this]( + const std::shared_ptr &props) -> std::shared_ptr { + if (props->format == AudioFileProperties::Format::WAV) { + return std::make_shared( + audioEventHandlerRegistry_, + props, + streamSampleRate_, + streamChannelCount_, + streamMaxBufferSizeInFrames_); + } else { #if !RN_AUDIO_API_FFMPEG_DISABLED - fileWriter_ = std::make_shared( - audioEventHandlerRegistry_, - fileProperties_, - streamSampleRate_, - streamChannelCount_, - streamMaxBufferSizeInFrames_); + return std::make_shared( + audioEventHandlerRegistry_, + props, + streamSampleRate_, + streamChannelCount_, + streamMaxBufferSizeInFrames_); #else - return Result::Err( - "FFmpeg backend is disabled. Cannot create file writer for the requested format. Use WAV " - "format instead."); + return nullptr; #endif + } + }; + + if (fileProperties_->rotateIntervalBytes > 0) { + if (createWriter(fileProperties_) == nullptr) { + return Result::Err( + "FFmpeg backend is disabled. Cannot create file writer for the requested format. Use " + "WAV " + "format instead."); + } + + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, + fileProperties_, + fileProperties_->rotateIntervalBytes, + createWriter); + } else { + fileWriter_ = createWriter(fileProperties_); + if (fileWriter_ == nullptr) { + return Result::Err( + "FFmpeg backend is disabled. Cannot create file writer for the requested format. Use " + "WAV " + "format instead."); + } } fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire)); @@ -243,26 +269,48 @@ Result AndroidAudioRecorder::enableFileOutput( fileProperties_ = properties; if (!isIdle()) { - if (properties->format == AudioFileProperties::Format::WAV) { - fileWriter_ = std::make_shared( - audioEventHandlerRegistry_, - properties, - streamSampleRate_, - streamChannelCount_, - streamMaxBufferSizeInFrames_); - } else { + auto createWriter = + [this]( + const std::shared_ptr &props) -> std::shared_ptr { + if (props->format == AudioFileProperties::Format::WAV) { + return std::make_shared( + audioEventHandlerRegistry_, + props, + streamSampleRate_, + streamChannelCount_, + streamMaxBufferSizeInFrames_); + } else { #if !RN_AUDIO_API_FFMPEG_DISABLED - fileWriter_ = std::make_shared( - audioEventHandlerRegistry_, - properties, - streamSampleRate_, - streamChannelCount_, - streamMaxBufferSizeInFrames_); + return std::make_shared( + audioEventHandlerRegistry_, + props, + streamSampleRate_, + streamChannelCount_, + streamMaxBufferSizeInFrames_); #else - return Result::Err( - "FFmpeg backend is disabled. Cannot create file writer for the requested format. Use WAV " - "format instead."); + return nullptr; #endif + } + }; + + if (properties->rotateIntervalBytes > 0) { + if (createWriter(properties) == nullptr) { + return Result::Err( + "FFmpeg backend is disabled. Cannot create file writer for the requested format. Use " + "WAV " + "format instead."); + } + + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, properties, properties->rotateIntervalBytes, createWriter); + } else { + fileWriter_ = createWriter(properties); + if (fileWriter_ == nullptr) { + return Result::Err( + "FFmpeg backend is disabled. Cannot create file writer for the requested format. Use " + "WAV " + "format instead."); + } } fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire)); diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.h index 987b33822..44798f8d1 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.h @@ -21,11 +21,12 @@ class AndroidFileWriterBackend : public AudioFileWriter { streamChannelCount_(streamChannelCount), streamMaxBufferSize_(streamMaxBufferSize) {} - virtual OpenFileResult openFile() = 0; - virtual bool writeAudioData(void *data, int numFrames) = 0; + OpenFileResult openFile() override = 0; + bool writeAudioData(void *data, int numFrames) override = 0; std::string getFilePath() const override { return filePath_; } double getCurrentDuration() const override { return static_cast(framesWritten_.load(std::memory_order_acquire)) / streamSampleRate_; } + size_t getFileSizeBytes() const override { return 0; } protected: float streamSampleRate_{0}; diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp index 24a38d2d7..5224e5cc3 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp @@ -15,6 +15,7 @@ extern "C" { #include #include #include +#include #include #include @@ -61,8 +62,7 @@ FFmpegAudioFileWriter::~FFmpegAudioFileWriter() { OpenFileResult FFmpegAudioFileWriter::openFile() { framesWritten_.store(0, std::memory_order_release); nextPts_ = 0; - Result result = Result::Ok(None); - Result filePathResult = fileoptions::getFilePath(fileProperties_); + auto filePathResult = fileoptions::getFilePath(fileProperties_); if (!filePathResult.is_ok()) { return OpenFileResult::Err(filePathResult.unwrap_err()); @@ -241,6 +241,23 @@ Result FFmpegAudioFileWriter::openIOAndWriteHeader() { return Result::Ok(None); } +size_t FFmpegAudioFileWriter::getFileSizeBytes() const { + if (formatCtx_ == nullptr) { + return 0; + } + + if (formatCtx_ && formatCtx_->pb) { + return static_cast(avio_tell(formatCtx_->pb)); + } + + // Fallback + struct stat st; + if (stat(filePath_.c_str(), &st) == 0) { + return st.st_size; + } + return 0; +} + /// @brief Initializes the resampler context for audio conversion. /// @param inputRate The sample rate of the input audio. /// @param inputChannels The number of channels in the input audio. diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h index 9f1160296..a99ed5b65 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h @@ -34,6 +34,7 @@ class FFmpegAudioFileWriter : public AndroidFileWriterBackend { CloseFileResult closeFile() override; bool writeAudioData(void *data, int numFrames) override; + size_t getFileSizeBytes() const override; private: av_unique_ptr encoderCtx_{nullptr}; diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.cpp index 0d7a19e8b..6cbb04dba 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -165,6 +166,16 @@ CloseFileResult MiniAudioFileWriter::closeFile() { return CloseFileResult ::Ok({fileSizeInMB, durationInSeconds}); } +/// @brief Get the current file size in bytes. +/// @return The size of the file in bytes. +size_t MiniAudioFileWriter::getFileSizeBytes() const { + struct stat st; + if (stat(filePath_.c_str(), &st) == 0) { + return st.st_size; + } + return 0; +} + /// @brief Writes audio data to the file. /// If possible (sample format, channel count, and interleaving matches), /// the data is written directly, otherwise in-memory conversion is performed first diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h index 1c668fcbb..649e31eb1 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h @@ -22,6 +22,7 @@ class MiniAudioFileWriter : public AndroidFileWriterBackend { CloseFileResult closeFile() override; bool writeAudioData(void *data, int numFrames) override; + size_t getFileSizeBytes() const override; private: std::atomic isConverterRequired_{false}; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h index 2534ce676..25e93300b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h @@ -9,6 +9,7 @@ namespace audioapi { class AudioFileProperties; +class RotatingFileWriter; class AudioEventHandlerRegistry; typedef Result OpenFileResult; @@ -35,6 +36,7 @@ class AudioFileWriter { virtual bool writeAudioData(AudioDataType data, int numFrames) = 0; virtual double getCurrentDuration() const = 0; + virtual size_t getFileSizeBytes() const = 0; void setOnErrorCallback(uint64_t callbackId); void clearOnErrorCallback(); @@ -49,6 +51,8 @@ class AudioFileWriter { std::shared_ptr fileProperties_; std::shared_ptr audioEventHandlerRegistry_; + + friend class RotatingFileWriter; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/RotatingFileWriter.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/RotatingFileWriter.cpp new file mode 100644 index 000000000..9708a5ade --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/RotatingFileWriter.cpp @@ -0,0 +1,101 @@ +#include + +#include +#include +#include + +namespace audioapi { + +RotatingFileWriter::RotatingFileWriter( + const std::shared_ptr& audioEventHandlerRegistry, + const std::shared_ptr& fileProperties, + size_t rotateIntervalBytes, + WriterFactory writerFactory) + : AudioFileWriter(audioEventHandlerRegistry, fileProperties), + writerFactory_(writerFactory), + rotateIntervalBytes_(rotateIntervalBytes), + baseFileName_(fileProperties->fileNamePrefix) {} + +OpenFileResult RotatingFileWriter::openFile() { + if (currentWriter_) { + return currentWriter_->openFile(); + } + openNewFile(); + return currentWriter_->openFile(); +} + +CloseFileResult RotatingFileWriter::closeFile() { + if (currentWriter_) { + return currentWriter_->closeFile(); + } + return CloseFileResult::Err("No file open"); +} + +std::string RotatingFileWriter::getFilePath() const { + if (currentWriter_) { + return currentWriter_->getFilePath(); + } + return ""; +} + +bool RotatingFileWriter::writeAudioData(AudioDataType data, int numFrames) { + if (!currentWriter_) { + return false; + } + + bool success = currentWriter_->writeAudioData(data, numFrames); + + if (success) { + writesSinceLastCheck_++; + // Check file size every ~10 writes to avoid syscall overhead + if (writesSinceLastCheck_ >= 10) { + writesSinceLastCheck_ = 0; + size_t size = currentWriter_->getFileSizeBytes(); + currentFileBytes_ = size; + if (size > rotateIntervalBytes_) { + rotateFiles(); + } + } + framesWritten_.fetch_add(numFrames, std::memory_order_relaxed); + } + return success; +} + +double RotatingFileWriter::getCurrentDuration() const { + return static_cast(framesWritten_.load()) / fileProperties_->sampleRate; +} + +size_t RotatingFileWriter::getFileSizeBytes() const { + if (currentWriter_) { + return currentWriter_->getFileSizeBytes(); + } + return 0; +} + +void RotatingFileWriter::rotateFiles() { + if (currentWriter_) { + currentWriter_->closeFile(); + // Start new file + openNewFile(); + currentWriter_->openFile(); + } +} + +void RotatingFileWriter::openNewFile() { + auto newProperties = createRotatedProperties(); + currentWriter_ = writerFactory_(newProperties); + currentFileBytes_ = 0; +} + +std::shared_ptr RotatingFileWriter::createRotatedProperties() { + auto ts = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + + std::string newName = baseFileName_ + "." + std::to_string(ts); + + auto newProps = std::make_shared(*fileProperties_); + newProps->fileNamePrefix = newName; + return newProps; +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/RotatingFileWriter.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/RotatingFileWriter.h new file mode 100644 index 000000000..3dc575123 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/RotatingFileWriter.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace audioapi { + +class RotatingFileWriter : public AudioFileWriter { + public: + using WriterFactory = + std::function(const std::shared_ptr &)>; + + RotatingFileWriter( + const std::shared_ptr &audioEventHandlerRegistry, + const std::shared_ptr &fileProperties, + size_t rotateIntervalBytes, + WriterFactory writerFactory); + + ~RotatingFileWriter() override = default; + + // AudioFileWriter overrides + OpenFileResult openFile() override; + CloseFileResult closeFile() override; + std::string getFilePath() const override; + bool writeAudioData(AudioDataType data, int numFrames) override; + double getCurrentDuration() const override; + size_t getFileSizeBytes() const override; + + // Rotating logic + void rotateFiles(); + + private: + std::shared_ptr createRotatedProperties(); + void openNewFile(); + + WriterFactory writerFactory_; + size_t rotateIntervalBytes_; + size_t currentFileBytes_ = 0; + size_t writesSinceLastCheck_ = 0; + std::shared_ptr currentWriter_; + std::string baseFileName_; +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioFileProperties.cpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioFileProperties.cpp index 430a78252..6770ae9ba 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioFileProperties.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioFileProperties.cpp @@ -14,6 +14,7 @@ AudioFileProperties::AudioFileProperties( const std::string &fileNamePrefix, int channelCount, size_t batchDurationSeconds, + size_t rotateIntervalBytes, Format format, float sampleRate, size_t bitRate, @@ -26,6 +27,7 @@ AudioFileProperties::AudioFileProperties( fileNamePrefix(fileNamePrefix), channelCount(channelCount), batchDurationSeconds(batchDurationSeconds), + rotateIntervalBytes(rotateIntervalBytes), format(format), sampleRate(sampleRate), bitRate(bitRate), @@ -53,6 +55,9 @@ std::shared_ptr AudioFileProperties::CreateFromJSIValue( size_t batchDurationSeconds = static_cast(options.getProperty(runtime, "batchDurationSeconds").getNumber()); + size_t rotateIntervalBytes = + static_cast(options.getProperty(runtime, "rotateIntervalBytes").getNumber()); + Format format = static_cast(options.getProperty(runtime, "format").getNumber()); int androidFlushIntervalMs = @@ -80,6 +85,7 @@ std::shared_ptr AudioFileProperties::CreateFromJSIValue( fileNamePrefix, channelCount, batchDurationSeconds, + rotateIntervalBytes, format, sampleRate, bitRate, diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioFileProperties.h b/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioFileProperties.h index 7f64e08e0..12ac2a2e8 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioFileProperties.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioFileProperties.h @@ -47,6 +47,7 @@ class AudioFileProperties { const std::string &fileNamePrefix, int channelCount, size_t batchDurationSeconds, + size_t rotateIntervalBytes, Format format, float sampleRate, size_t bitRate, @@ -64,6 +65,7 @@ class AudioFileProperties { std::string fileNamePrefix; int channelCount; size_t batchDurationSeconds; + size_t rotateIntervalBytes; Format format; float sampleRate; size_t bitRate; 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 c3227a4af..975398fee 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 @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -102,8 +103,26 @@ auto inputFormat = [nativeRecorder_ getInputFormat]; if (usesFileOutput()) { - fileWriter_ = std::make_shared( - audioEventHandlerRegistry_, fileProperties_, inputFormat, maxInputBufferLength); + auto createWriter = + [this, maxInputBufferLength]( + const std::shared_ptr &props) -> std::shared_ptr { + return std::make_shared( + audioEventHandlerRegistry_, + props, + [nativeRecorder_ getInputFormat], + maxInputBufferLength); + }; + + if (fileProperties_->rotateIntervalBytes > 0) { + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, + fileProperties_, + fileProperties_->rotateIntervalBytes, + createWriter); + } else { + fileWriter_ = createWriter(fileProperties_); + } + fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire)); auto fileResult = fileWriter_->openFile(); @@ -194,11 +213,20 @@ fileProperties_ = properties; if (!isIdle()) { - fileWriter_ = std::make_shared( - audioEventHandlerRegistry_, - properties, - [nativeRecorder_ getInputFormat], - [nativeRecorder_ getBufferSize]); + size_t bufferSize = [nativeRecorder_ getBufferSize]; + auto createWriter = + [this, bufferSize]( + const std::shared_ptr &props) -> std::shared_ptr { + return std::make_shared( + audioEventHandlerRegistry_, props, [nativeRecorder_ getInputFormat], bufferSize); + }; + + if (properties->rotateIntervalBytes > 0) { + fileWriter_ = std::make_shared( + audioEventHandlerRegistry_, properties, properties->rotateIntervalBytes, createWriter); + } else { + fileWriter_ = createWriter(properties); + } auto result = fileWriter_->openFile(); 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 32e1e7a52..191ec9b6d 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 @@ -34,6 +34,7 @@ class IOSFileWriter : public AudioFileWriter { bool writeAudioData(const AudioBufferList *audioBufferList, int numFrames); double getCurrentDuration() const override; + size_t getFileSizeBytes() const override; std::string getFilePath() const 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 ac7737e17..2313aa3bc 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 @@ -1,6 +1,8 @@ #import #import +#include + #include #include #include @@ -138,6 +140,23 @@ } } +/// @brief Retrieves the current file size in bytes from the actual file on disk. +/// This method uses POSIX stat to minimize overhead compared to NSFileManager attributes. +/// @returns The file size in bytes, or 0 if the file is not open or an error occurs. +size_t IOSFileWriter::getFileSizeBytes() const +{ + if (fileURL_ == nil) { + return 0; + } + + // Use stat for faster file size retrieval than NSFileManager + struct stat st; + if (stat([[fileURL_ path] fileSystemRepresentation], &st) == 0) { + return (size_t)st.st_size; + } + return 0; +} + /// @brief Writes audio data to the open audio file, performing format conversion if necessary. /// This method should be called from the audio thread. /// @param audioBufferList Pointer to the AudioBufferList containing the audio data to write. diff --git a/packages/react-native-audio-api/src/core/AudioRecorder.ts b/packages/react-native-audio-api/src/core/AudioRecorder.ts index 2c21ac625..01e90a887 100644 --- a/packages/react-native-audio-api/src/core/AudioRecorder.ts +++ b/packages/react-native-audio-api/src/core/AudioRecorder.ts @@ -31,6 +31,7 @@ function withDefaultOptions( batchDurationSeconds: 0, preset: FilePreset.High, androidFlushIntervalMs: 500, + rotateIntervalBytes: 0, ...inOptions, }; } diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts index 1a6aecec9..e2499f820 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -92,6 +92,7 @@ export interface FilePresetType { export interface AudioRecorderFileOptions { channelCount?: number; batchDurationSeconds?: number; + rotateIntervalBytes?: number; format?: FileFormat; preset?: FilePresetType; From d5dfee8fe1a9da4b63de9700bc90231c666029aa Mon Sep 17 00:00:00 2001 From: poneciak Date: Fri, 30 Jan 2026 15:16:15 +0100 Subject: [PATCH 3/3] fix: fixed tests --- .../common/cpp/audioapi/core/utils/AudioFileWriter.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h index 25e93300b..6e09ea2fd 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h @@ -15,11 +15,11 @@ class AudioEventHandlerRegistry; typedef Result OpenFileResult; typedef Result, std::string> CloseFileResult; -#ifdef ANDROID -typedef void *AudioDataType; -#else +#if defined(__APPLE__) #include typedef const AudioBufferList *AudioDataType; +#else +typedef void *AudioDataType; #endif class AudioFileWriter {