diff --git a/apps/common-app/src/demos/Record/Record.tsx b/apps/common-app/src/demos/Record/Record.tsx index 7d9def11a..e73aa327c 100644 --- a/apps/common-app/src/demos/Record/Record.tsx +++ b/apps/common-app/src/demos/Record/Record.tsx @@ -240,7 +240,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 c73a17fa9..9930d2154 100644 --- a/packages/audiodocs/docs/inputs/audio-recorder.mdx +++ b/packages/audiodocs/docs/inputs/audio-recorder.mdx @@ -720,6 +720,7 @@ interface OnAudioReadyEventType { ```tsx interface AudioRecorderFileOptions { channelCount?: number; + rotateIntervalBytes?: number; format?: FileFormat; preset?: FilePresetType; @@ -732,6 +733,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 60475b89b..0db214e11 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 @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -122,12 +124,56 @@ Result AndroidAudioRecorder::start(const std::string & } if (usesFileOutput()) { - auto fileResult = std::static_pointer_cast(fileWriter_) - ->openFile( - streamSampleRate_, - streamChannelCount_, - streamMaxBufferSizeInFrames_, - fileNameOverride); + 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 + return std::make_shared( + audioEventHandlerRegistry_, + props, + streamSampleRate_, + streamChannelCount_, + streamMaxBufferSizeInFrames_); +#else + 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)); + + auto fileResult = fileWriter_->openFile(); if (!fileResult.is_ok()) { return Result::Err( @@ -135,6 +181,11 @@ Result AndroidAudioRecorder::start(const std::string & } filePath_ = fileResult.unwrap(); + __android_log_print( + ANDROID_LOG_INFO, + "AndroidAudioRecorder", + "File created successfully at path: %s", + filePath_.c_str()); } if (usesCallback()) { @@ -217,23 +268,56 @@ 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()) { + 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); + 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."); + } - if (!isIdle()) { - auto fileResult = - std::static_pointer_cast(fileWriter_) - ->openFile(streamSampleRate_, streamChannelCount_, streamMaxBufferSizeInFrames_, ""); + 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)); + + auto fileResult = fileWriter_->openFile(); if (!fileResult.is_ok()) { return Result::Err( @@ -360,8 +444,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 62bde4b5f..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 @@ -14,14 +14,19 @@ class AndroidFileWriterBackend : public AudioFileWriter { public: explicit AndroidFileWriterBackend( const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &fileProperties) - : AudioFileWriter(audioEventHandlerRegistry, fileProperties) {} + 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(float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize, const std::string &fileNameOverride) = 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 c713b9717..c1b5b1f65 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 @@ -16,6 +16,7 @@ extern "C" { #include #include +#include #include #include #include @@ -30,8 +31,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,19 +59,10 @@ 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, - const std::string &fileNameOverride) { - streamSampleRate_ = streamSampleRate; - streamChannelCount_ = streamChannelCount; - streamMaxBufferSize_ = streamMaxBufferSize; +OpenFileResult FFmpegAudioFileWriter::openFile() { framesWritten_.store(0, std::memory_order_release); nextPts_ = 0; - Result result = Result::Ok(None); - Result filePathResult = - fileoptions::getFilePath(fileProperties_, fileNameOverride); + auto filePathResult = fileoptions::getFilePath(fileProperties_, ""); if (!filePathResult.is_ok()) { return OpenFileResult::Err(filePathResult.unwrap_err()); @@ -80,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); }); @@ -242,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 be44a2b8c..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 @@ -26,13 +26,15 @@ 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, const std::string &fileNameOverride) override; + OpenFileResult openFile() override; 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 6d205992f..8da96ed0b 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 @@ -40,8 +41,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,14 +81,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, - const std::string &fileNameOverride) { - streamSampleRate_ = streamSampleRate; - streamChannelCount_ = streamChannelCount; - streamMaxBufferSize_ = streamMaxBufferSize; +OpenFileResult MiniAudioFileWriter::openFile() { ma_result result; framesWritten_.store(0, std::memory_order_release); @@ -96,7 +98,7 @@ OpenFileResult MiniAudioFileWriter::openFile( "Failed to initialize converter" + std::string(ma_result_description(result))); } - result = initializeEncoder(fileNameOverride); + result = initializeEncoder(); if (result != MA_SUCCESS) { return OpenFileResult ::Err( @@ -164,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 @@ -267,10 +279,10 @@ ma_result MiniAudioFileWriter::initializeConverterIfNeeded() { /// This method sets up the audio encoder for writing to the file, /// it should be called only on the JS thread. (during file opening) /// @return MA_SUCCESS if initialization was successful, otherwise an error code. -ma_result MiniAudioFileWriter::initializeEncoder(const std::string &fileNameOverride) { +ma_result MiniAudioFileWriter::initializeEncoder() { ma_result result; Result filePathResult = - android::fileoptions::getFilePath(fileProperties_, fileNameOverride); + android::fileoptions::getFilePath(fileProperties_, ""); if (!filePathResult.is_ok()) { return MA_ERROR; 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 d49063b45..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 @@ -14,13 +14,15 @@ 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, const std::string &fileNameOverride) override; + OpenFileResult openFile() override; CloseFileResult closeFile() override; bool writeAudioData(void *data, int numFrames) override; + size_t getFileSizeBytes() const override; private: std::atomic isConverterRequired_{false}; @@ -31,7 +33,7 @@ class MiniAudioFileWriter : public AndroidFileWriterBackend { ma_uint64 processingBufferLength_{0}; ma_result initializeConverterIfNeeded(); - ma_result initializeEncoder(const std::string &fileNameOverride); + ma_result initializeEncoder(); ma_uint64 convertBuffer(void *data, int numFrames); bool isConverterRequired(); 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 b0f1fb89c..7423d7863 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..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 @@ -9,11 +9,19 @@ namespace audioapi { class AudioFileProperties; +class RotatingFileWriter; class AudioEventHandlerRegistry; typedef Result OpenFileResult; typedef Result, std::string> CloseFileResult; +#if defined(__APPLE__) +#include +typedef const AudioBufferList *AudioDataType; +#else +typedef void *AudioDataType; +#endif + class AudioFileWriter { public: AudioFileWriter( @@ -22,9 +30,13 @@ 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; + virtual size_t getFileSizeBytes() const = 0; void setOnErrorCallback(uint64_t callbackId); void clearOnErrorCallback(); @@ -39,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 ac6229b1b..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 @@ -36,8 +37,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); } } @@ -71,7 +71,7 @@ /// @brief Starts the audio recording process and prepares necessary resources. /// This method should be called from the JS thread only. /// @returns Result containing the file path if recording started successfully, or an error message. -Result IOSAudioRecorder::start(const std::string &fileNameOverride) +Result IOSAudioRecorder::start() { if (!isIdle()) { return Result::Err("Recorder is already recording"); @@ -103,8 +103,29 @@ auto inputFormat = [nativeRecorder_ getInputFormat]; if (usesFileOutput()) { - auto fileResult = std::static_pointer_cast(fileWriter_) - ->openFile(inputFormat, maxInputBufferLength, fileNameOverride); + 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(); if (fileResult.is_err()) { return Result::Err( @@ -112,6 +133,7 @@ } filePath_ = fileResult.unwrap(); + NSLog(@"[IOSAudioRecorder] File created successfully at path: %s", filePath_.c_str()); } if (usesCallback()) { @@ -188,12 +210,25 @@ 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], ""); + 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(); if (result.is_err()) { return Result::Err( @@ -201,10 +236,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 af34f6baa..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 @@ -24,17 +24,17 @@ class IOSFileWriter : public AudioFileWriter { public: IOSFileWriter( const std::shared_ptr &audioEventHandlerRegistry, - const std::shared_ptr &fileProperties); + const std::shared_ptr &fileProperties, + AVAudioFormat *bufferFormat, + size_t maxInputBufferLength); ~IOSFileWriter(); - Result openFile( - AVAudioFormat *bufferFormat, - size_t maxInputBufferLength, - const std::string &fileNameOverride); + Result openFile(); Result, std::string> closeFile() override; 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 a453e9233..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 @@ -11,9 +13,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,10 +38,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, - const std::string &fileNameOverride) +Result IOSFileWriter::openFile() { @autoreleasepool { if (audioFile_ != nil) { @@ -43,18 +46,17 @@ } framesWritten_.store(0, std::memory_order_release); - bufferFormat_ = bufferFormat; NSError *error = nil; NSDictionary *settings = ios::fileoptions::getFileSettings(fileProperties_); - fileURL_ = ios::fileoptions::getFileURL(fileProperties_, fileNameOverride); + fileURL_ = ios::fileoptions::getFileURL(fileProperties_); if (fileProperties_->sampleRate == 0 || fileProperties_->channelCount == 0) { return OpenFileResult::Err( "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"); } @@ -62,7 +64,7 @@ audioFile_ = [[AVAudioFile alloc] initForWriting:fileURL_ settings:settings commonFormat:AVAudioPCMFormatFloat32 - interleaved:bufferFormat.interleaved + interleaved:bufferFormat_.interleaved error:&error]; if (error != nil) { @@ -71,20 +73,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_]; @@ -139,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 8b88b3f6b..805cfde37 100644 --- a/packages/react-native-audio-api/src/core/AudioRecorder.ts +++ b/packages/react-native-audio-api/src/core/AudioRecorder.ts @@ -32,6 +32,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 01a6a8de8..cf6e86220 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -89,6 +89,7 @@ export interface FilePresetType { export interface AudioRecorderFileOptions { channelCount?: number; batchDurationSeconds?: number; + rotateIntervalBytes?: number; format?: FileFormat; preset?: FilePresetType;