From 328cec37e0623b50bb5d12c2771ef0785b64a349 Mon Sep 17 00:00:00 2001 From: michal Date: Thu, 22 Jan 2026 15:11:43 +0100 Subject: [PATCH 1/3] feat: moved writing to different thread --- .../core/utils/AndroidFileWriterBackend.cpp | 31 ++++ .../core/utils/AndroidFileWriterBackend.h | 16 +- .../utils/ffmpegBackend/FFmpegFileWriter.cpp | 34 +++-- .../utils/ffmpegBackend/FFmpegFileWriter.h | 2 +- .../miniaudioBackend/MiniAudioFileWriter.cpp | 74 ++++----- .../miniaudioBackend/MiniAudioFileWriter.h | 2 +- .../cpp/audioapi/core/utils/AudioFileWriter.h | 10 ++ .../audioapi/ios/core/utils/IOSFileWriter.h | 21 ++- .../audioapi/ios/core/utils/IOSFileWriter.mm | 142 ++++++++++-------- .../src/core/AudioRecorder.ts | 2 +- 10 files changed, 214 insertions(+), 120 deletions(-) create mode 100644 packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.cpp diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.cpp new file mode 100644 index 000000000..bbda0d06d --- /dev/null +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.cpp @@ -0,0 +1,31 @@ +#include +#include +#include + +namespace audioapi { +AndroidFileWriterBackend::AndroidFileWriterBackend( + const std::shared_ptr &audioEventHandlerRegistry, + const std::shared_ptr &fileProperties) + : AudioFileWriter(audioEventHandlerRegistry, fileProperties) { + auto [sender, receiver] = channels::spsc::channel< + WriterData, + AudioFileWriter::FILE_WRITER_SPSC_OVERFLOW_STRATEGY, + AudioFileWriter::FILE_WRITER_SPSC_WAIT_STRATEGY>( + AudioFileWriter::FILE_WRITER_CHANNEL_CAPACITY); + sender_ = std::move(sender); + receiver_ = std::move(receiver); + stopFileWriterThread_.store(false, std::memory_order_release); +} + +void AndroidFileWriterBackend::writeAudioData(void *data, int numFrames) { + sender_.send({data, numFrames}); +} + +void AndroidFileWriterBackend::stopFileWriterThread() { + stopFileWriterThread_.store(true, std::memory_order_release); + sender_.send({nullptr, 0}); + if (fileWriterThread_.joinable()) { + fileWriterThread_.join(); + } +} +} // namespace audioapi 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..04b8bac73 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 @@ -1,11 +1,17 @@ #pragma once #include +#include #include #include #include #include +struct WriterData { + void *data; + int numFrames; +}; + namespace audioapi { class AudioFileProperties; @@ -14,20 +20,22 @@ 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); virtual OpenFileResult openFile(float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize, const std::string &fileNameOverride) = 0; - virtual bool writeAudioData(void *data, int numFrames) = 0; + void writeAudioData(void *data, int numFrames); std::string getFilePath() const override { return filePath_; } double getCurrentDuration() const override { return static_cast(framesWritten_.load(std::memory_order_acquire)) / streamSampleRate_; } protected: + void stopFileWriterThread(); float streamSampleRate_{0}; int32_t streamChannelCount_{0}; int32_t streamMaxBufferSize_{0}; - std::string filePath_{""}; + std::string filePath_; + channels::spsc::Sender sender_; + channels::spsc::Receiver receiver_; }; } // namespace audioapi 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..c5984b203 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 @@ -87,6 +87,10 @@ OpenFileResult FFmpegAudioFileWriter::openFile( initializeBuffers(streamMaxBufferSize); isFileOpen_.store(true, std::memory_order_release); return OpenFileResult::Ok(filePath); + }) + .and_then([this](const auto &res) { + fileWriterThread_ = std::thread(&FFmpegAudioFileWriter::fileWriterThreadHandler, this); + return OpenFileResult::Ok(res); }); } @@ -120,30 +124,30 @@ CloseFileResult FFmpegAudioFileWriter::closeFile() { return CloseFileResult::Err("Failed to drain encoder packets"); } + stopFileWriterThread(); + return finalizeOutput(); } /// @brief Writes audio data to the currently opened file. /// This method should be called only from the audio thread (or audio side-effect thread in the future). -/// @param data Pointer to the audio data buffer (interleaved float samples) as returned by Oboe stream. -/// @param numFrames Number of audio frames in the data buffer. -/// @returns True if the data was written successfully, false otherwise. -bool FFmpegAudioFileWriter::writeAudioData(void *data, int numFrames) { - if (!isFileOpen()) { - return false; - } +void FFmpegAudioFileWriter::fileWriterThreadHandler() { + while (!stopFileWriterThread_.load(std::memory_order_acquire)) { + auto [data, numFrames] = receiver_.receive(); + if (!isFileOpen()) { + return; + } - if (!resampleAndPushToFifo(data, numFrames)) { - return false; - } + if (!resampleAndPushToFifo(data, numFrames)) { + return; + } - framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); + framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); - if (processFifo(false) < 0) { - return false; + if (processFifo(false) < 0) { + return; + } } - - return true; } /// @brief Initializes the FFmpeg format context for the output file. 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..9ef11c2a4 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 @@ -32,7 +32,6 @@ class FFmpegAudioFileWriter : public AndroidFileWriterBackend { OpenFileResult openFile(float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize, const std::string &fileNameOverride) override; CloseFileResult closeFile() override; - bool writeAudioData(void *data, int numFrames) override; private: av_unique_ptr encoderCtx_{nullptr}; @@ -64,6 +63,7 @@ class FFmpegAudioFileWriter : public AndroidFileWriterBackend { // Finalization helper methods CloseFileResult finalizeOutput(); + void fileWriterThreadHandler() override; }; } // namespace android::ffmpeg 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..a22fa0aae 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 @@ -53,12 +53,12 @@ MiniAudioFileWriter::~MiniAudioFileWriter() { } if (converter_ != nullptr) { - ma_data_converter_uninit(converter_.get(), NULL); + ma_data_converter_uninit(converter_.get(), nullptr); converter_.reset(); } if (processingBuffer_ != nullptr) { - ma_free(processingBuffer_, NULL); + ma_free(processingBuffer_, nullptr); processingBuffer_ = nullptr; processingBufferLength_ = 0; } @@ -104,6 +104,7 @@ OpenFileResult MiniAudioFileWriter::openFile( } isFileOpen_.store(true, std::memory_order_release); + fileWriterThread_ = std::thread(&MiniAudioFileWriter::fileWriterThreadHandler, this); return OpenFileResult ::Ok(filePath_); } @@ -125,12 +126,12 @@ CloseFileResult MiniAudioFileWriter::closeFile() { } if (converter_ != nullptr) { - ma_data_converter_uninit(converter_.get(), NULL); + ma_data_converter_uninit(converter_.get(), nullptr); converter_.reset(); } if (processingBuffer_ != nullptr) { - ma_free(processingBuffer_, NULL); + ma_free(processingBuffer_, nullptr); processingBuffer_ = nullptr; processingBufferLength_ = 0; } @@ -141,7 +142,7 @@ CloseFileResult MiniAudioFileWriter::closeFile() { ma_decoder decoder; - if (ma_decoder_init_file(filePath_.c_str(), NULL, &decoder) == MA_SUCCESS) { + if (ma_decoder_init_file(filePath_.c_str(), nullptr, &decoder) == MA_SUCCESS) { ma_uint64 frameCount = 0; if (ma_decoder_get_length_in_pcm_frames(&decoder, &frameCount) == MA_SUCCESS) { @@ -159,6 +160,7 @@ CloseFileResult MiniAudioFileWriter::closeFile() { fclose(file); fileSizeInMB = static_cast(fileSizeInBytes) / MB_IN_BYTES; } + stopFileWriterThread(); filePath_ = ""; return CloseFileResult ::Ok({fileSizeInMB, durationInSeconds}); @@ -167,46 +169,44 @@ CloseFileResult MiniAudioFileWriter::closeFile() { /// @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 -/// It should be called only on the audio thread. -/// @param data Pointer to the audio data buffer. (Interleaved float32 format - as oboe likes it) -/// @param numFrames Number of audio frames to write. -/// @return True if the write operation was successful, false otherwise. -bool MiniAudioFileWriter::writeAudioData(void *data, int numFrames) { - ma_uint64 framesWritten = 0; - ma_result result; +void MiniAudioFileWriter::fileWriterThreadHandler() { + while (!stopFileWriterThread_.load(std::memory_order_acquire)) { + auto [data, numFrames] = receiver_.receive(); + ma_uint64 framesWritten = 0; + ma_result result; + + if (!isFileOpen()) { + return; + } - if (!isFileOpen()) { - return false; - } + if (!isConverterRequired()) { + result = ma_encoder_write_pcm_frames(encoder_.get(), data, numFrames, &framesWritten); + + if (result != MA_SUCCESS) { + invokeOnErrorCallback( + "Failed to write audio data to file: " + filePath_ + + std::string(ma_result_description(result))); + return; + } + + framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); + continue; + } - if (!isConverterRequired()) { - result = ma_encoder_write_pcm_frames(encoder_.get(), data, numFrames, &framesWritten); + ma_uint64 convertedFrameCount = convertBuffer(data, numFrames); + + result = ma_encoder_write_pcm_frames( + encoder_.get(), processingBuffer_, convertedFrameCount, &framesWritten); if (result != MA_SUCCESS) { invokeOnErrorCallback( - "Failed to write audio data to file: " + filePath_ + + "Failed to write converted audio data to file: " + filePath_ + std::string(ma_result_description(result))); - return false; + return; } framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); - return result == MA_SUCCESS; } - - ma_uint64 convertedFrameCount = convertBuffer(data, numFrames); - - result = ma_encoder_write_pcm_frames( - encoder_.get(), processingBuffer_, convertedFrameCount, &framesWritten); - - if (result != MA_SUCCESS) { - invokeOnErrorCallback( - "Failed to write converted audio data to file: " + filePath_ + - std::string(ma_result_description(result))); - return false; - } - - framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); - return result == MA_SUCCESS; } /// @brief Converts the audio data buffer if necessary. @@ -247,7 +247,7 @@ ma_result MiniAudioFileWriter::initializeConverterIfNeeded() { fileProperties_->sampleRate); converter_ = std::make_unique(); - result = ma_data_converter_init(&converterConfig, NULL, converter_.get()); + result = ma_data_converter_init(&converterConfig, nullptr, converter_.get()); if (result != MA_SUCCESS) { return result; @@ -258,7 +258,7 @@ ma_result MiniAudioFileWriter::initializeConverterIfNeeded() { processingBuffer_ = ma_malloc( processingBufferLength_ * fileProperties_->channelCount * ma_get_bytes_per_sample(dataFormat), - NULL); + nullptr); return MA_SUCCESS; } 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..fe86ead01 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 @@ -20,7 +20,6 @@ class MiniAudioFileWriter : public AndroidFileWriterBackend { OpenFileResult openFile(float streamSampleRate, int32_t streamChannelCount, int32_t streamMaxBufferSize, const std::string &fileNameOverride) override; CloseFileResult closeFile() override; - bool writeAudioData(void *data, int numFrames) override; private: std::atomic isConverterRequired_{false}; @@ -35,6 +34,7 @@ class MiniAudioFileWriter : public AndroidFileWriterBackend { ma_uint64 convertBuffer(void *data, int numFrames); bool isConverterRequired(); + void fileWriterThreadHandler() override; }; } // 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..c13da7c6d 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 @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -39,6 +40,15 @@ class AudioFileWriter { std::shared_ptr fileProperties_; std::shared_ptr audioEventHandlerRegistry_; + + static constexpr auto FILE_WRITER_SPSC_OVERFLOW_STRATEGY = + channels::spsc::OverflowStrategy::OVERWRITE_ON_FULL; + static constexpr auto FILE_WRITER_SPSC_WAIT_STRATEGY = channels::spsc::WaitStrategy::ATOMIC_WAIT; + static constexpr auto FILE_WRITER_CHANNEL_CAPACITY = 64; + + std::thread fileWriterThread_; + std::atomic stopFileWriterThread_; + virtual void fileWriterThreadHandler() = 0; }; } // namespace audioapi 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..4f5b4a0bc 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 @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -15,6 +16,11 @@ typedef struct objc_object AudioBufferList; typedef struct objc_object AVAudioConverter; #endif // __OBJC__ +struct WriterData { + const AudioBufferList *audioBufferList; + int numFrames; +}; + namespace audioapi { class AudioFileProperties; @@ -33,7 +39,7 @@ class IOSFileWriter : public AudioFileWriter { const std::string &fileNameOverride); Result, std::string> closeFile() override; - bool writeAudioData(const AudioBufferList *audioBufferList, int numFrames); + void writeAudioData(const AudioBufferList *audioBufferList, int numFrames); double getCurrentDuration() const override; std::string getFilePath() const override; @@ -49,6 +55,19 @@ class IOSFileWriter : public AudioFileWriter { AVAudioPCMBuffer *converterInputBuffer_; AVAudioPCMBuffer *converterOutputBuffer_; + + private: + channels::spsc::Sender< + WriterData, + AudioFileWriter::FILE_WRITER_SPSC_OVERFLOW_STRATEGY, + AudioFileWriter::FILE_WRITER_SPSC_WAIT_STRATEGY> + sender_; + channels::spsc::Receiver< + WriterData, + AudioFileWriter::FILE_WRITER_SPSC_OVERFLOW_STRATEGY, + AudioFileWriter::FILE_WRITER_SPSC_WAIT_STRATEGY> + receiver_; + void fileWriterThreadHandler() override; }; } // namespace audioapi 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..446ac9a53 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 @@ -14,6 +14,14 @@ const std::shared_ptr &fileProperties) : AudioFileWriter(audioEventHandlerRegistry, fileProperties) { + auto [sender, receiver] = channels::spsc::channel< + WriterData, + AudioFileWriter::FILE_WRITER_SPSC_OVERFLOW_STRATEGY, + AudioFileWriter::FILE_WRITER_SPSC_WAIT_STRATEGY>( + AudioFileWriter::FILE_WRITER_CHANNEL_CAPACITY); + sender_ = std::move(sender); + receiver_ = std::move(receiver); + stopFileWriterThread_.store(false, std::memory_order_release); } IOSFileWriter::~IOSFileWriter() @@ -99,6 +107,7 @@ return OpenFileResult::Err("Error creating converter buffers"); } + fileWriterThread_ = std::thread(&IOSFileWriter::fileWriterThreadHandler, this); return OpenFileResult::Ok([[fileURL_ path] UTF8String]); } } @@ -135,96 +144,109 @@ fileURL_ = nil; framesWritten_.store(0, std::memory_order_release); + stopFileWriterThread_.store(true, std::memory_order_release); + sender_.send({nullptr, 0}); + if (fileWriterThread_.joinable()) { + fileWriterThread_.join(); + } + return CloseFileResult::Ok(std::make_tuple(fileDuration, fileSizeBytesMb)); } } /// @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. -/// @param numFrames Number of audio frames in the audioBufferList. -/// @returns True if the write operation was successful, false otherwise. -bool IOSFileWriter::writeAudioData(const AudioBufferList *audioBufferList, int numFrames) +void IOSFileWriter::writeAudioData(const AudioBufferList *audioBufferList, int numFrames) { if (audioFile_ == nil) { invokeOnErrorCallback("Attempted to write audio data when file is not open"); - return false; + } else { + sender_.send({audioBufferList, numFrames}); } +} - @autoreleasepool { - NSError *error = nil; - AVAudioFormat *fileFormat = [audioFile_ processingFormat]; +void IOSFileWriter::fileWriterThreadHandler() +{ + while (!stopFileWriterThread_.load(std::memory_order_acquire)) { + auto [audioBufferList, numFrames] = receiver_.receive(); + if (audioBufferList == nullptr) + continue; + @autoreleasepool { + NSError *error = nil; + AVAudioFormat *fileFormat = [audioFile_ processingFormat]; + + if (bufferFormat_.sampleRate == fileFormat.sampleRate && + bufferFormat_.channelCount == fileFormat.channelCount && + bufferFormat_.isInterleaved == fileFormat.isInterleaved) { + // We can use the converter input buffer as a "transport" layer to the file + for (size_t i = 0; i < bufferFormat_.channelCount; ++i) { + memcpy( + converterInputBuffer_.mutableAudioBufferList->mBuffers[i].mData, + audioBufferList->mBuffers[i].mData, + audioBufferList->mBuffers[i].mDataByteSize); + } + converterInputBuffer_.frameLength = numFrames; + + [audioFile_ writeFromBuffer:converterInputBuffer_ error:&error]; + + if (error != nil) { + invokeOnErrorCallback( + std::string("Error writing audio data to file, native error: ") + + [[error debugDescription] UTF8String]); + return; + } + + framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); + return; + } - if (bufferFormat_.sampleRate == fileFormat.sampleRate && - bufferFormat_.channelCount == fileFormat.channelCount && - bufferFormat_.isInterleaved == fileFormat.isInterleaved) { - // We can use the converter input buffer as a "transport" layer to the file for (size_t i = 0; i < bufferFormat_.channelCount; ++i) { memcpy( converterInputBuffer_.mutableAudioBufferList->mBuffers[i].mData, audioBufferList->mBuffers[i].mData, audioBufferList->mBuffers[i].mDataByteSize); } + converterInputBuffer_.frameLength = numFrames; - [audioFile_ writeFromBuffer:converterInputBuffer_ error:&error]; + __block BOOL handedOff = false; + AVAudioConverterInputBlock inputBlock = ^AVAudioBuffer *_Nullable( + AVAudioPacketCount inNumberOfPackets, AVAudioConverterInputStatus *outStatus) + { + if (handedOff) { + *outStatus = AVAudioConverterInputStatus_NoDataNow; + return nil; + } + + handedOff = true; + *outStatus = AVAudioConverterInputStatus_HaveData; + return converterInputBuffer_; + }; + + [converter_ convertToBuffer:converterOutputBuffer_ + error:&error + withInputFromBlock:inputBlock]; + converterOutputBuffer_.frameLength = + fileProperties_->sampleRate / bufferFormat_.sampleRate * numFrames; if (error != nil) { invokeOnErrorCallback( - std::string("Error writing audio data to file, native error: ") + + std::string("Error during audio conversion, native error: ") + [[error debugDescription] UTF8String]); - return false; + return; } - framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); - return true; - } + [audioFile_ writeFromBuffer:converterOutputBuffer_ error:&error]; - for (size_t i = 0; i < bufferFormat_.channelCount; ++i) { - memcpy( - converterInputBuffer_.mutableAudioBufferList->mBuffers[i].mData, - audioBufferList->mBuffers[i].mData, - audioBufferList->mBuffers[i].mDataByteSize); - } - - converterInputBuffer_.frameLength = numFrames; - - __block BOOL handedOff = false; - AVAudioConverterInputBlock inputBlock = ^AVAudioBuffer *_Nullable( - AVAudioPacketCount inNumberOfPackets, AVAudioConverterInputStatus *outStatus) - { - if (handedOff) { - *outStatus = AVAudioConverterInputStatus_NoDataNow; - return nil; + if (error != nil) { + invokeOnErrorCallback( + std::string("Error writing audio data to file, native error: ") + + [[error debugDescription] UTF8String]); + return; } - handedOff = true; - *outStatus = AVAudioConverterInputStatus_HaveData; - return converterInputBuffer_; - }; - - [converter_ convertToBuffer:converterOutputBuffer_ error:&error withInputFromBlock:inputBlock]; - converterOutputBuffer_.frameLength = - fileProperties_->sampleRate / bufferFormat_.sampleRate * numFrames; - - if (error != nil) { - invokeOnErrorCallback( - std::string("Error during audio conversion, native error: ") + - [[error debugDescription] UTF8String]); - return false; - } - - [audioFile_ writeFromBuffer:converterOutputBuffer_ error:&error]; - - if (error != nil) { - invokeOnErrorCallback( - std::string("Error writing audio data to file, native error: ") + - [[error debugDescription] UTF8String]); - return false; + framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); } - - framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); - return true; } } diff --git a/packages/react-native-audio-api/src/core/AudioRecorder.ts b/packages/react-native-audio-api/src/core/AudioRecorder.ts index 8faf15af7..34407bcdb 100644 --- a/packages/react-native-audio-api/src/core/AudioRecorder.ts +++ b/packages/react-native-audio-api/src/core/AudioRecorder.ts @@ -28,7 +28,7 @@ function withDefaultOptions( subDirectory: 'AudioAPI', fileNamePrefix: 'recording_', channelCount: 2, - format: FileFormat.M4A, + format: FileFormat.Wav, batchDurationSeconds: 0, preset: FilePreset.High, androidFlushIntervalMs: 500, From edb1aac1ba3bc5a61d6433261b78bdb807beea25 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 3 Feb 2026 16:13:24 +0100 Subject: [PATCH 2/3] feat: offloader to writing files --- .../core/utils/AndroidFileWriterBackend.cpp | 24 +-- .../core/utils/AndroidFileWriterBackend.h | 8 +- .../core/utils/AndroidRecorderCallback.cpp | 5 +- .../core/utils/AndroidRecorderCallback.h | 2 +- .../utils/ffmpegBackend/FFmpegFileWriter.cpp | 40 ++--- .../utils/ffmpegBackend/FFmpegFileWriter.h | 2 +- .../miniaudioBackend/MiniAudioFileWriter.cpp | 64 ++++---- .../miniaudioBackend/MiniAudioFileWriter.h | 2 +- .../cpp/audioapi/core/utils/AudioFileWriter.h | 4 - .../audioapi/ios/core/utils/IOSFileWriter.h | 17 +- .../audioapi/ios/core/utils/IOSFileWriter.mm | 145 ++++++++---------- .../ios/core/utils/IOSRecorderCallback.h | 4 +- .../ios/core/utils/IOSRecorderCallback.mm | 5 +- .../src/core/AudioRecorder.ts | 2 +- 14 files changed, 154 insertions(+), 170 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.cpp index bbda0d06d..2819ec06c 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.cpp @@ -7,25 +7,17 @@ AndroidFileWriterBackend::AndroidFileWriterBackend( const std::shared_ptr &audioEventHandlerRegistry, const std::shared_ptr &fileProperties) : AudioFileWriter(audioEventHandlerRegistry, fileProperties) { - auto [sender, receiver] = channels::spsc::channel< + + auto offloaderLambda = [this](WriterData data) { + taskOffloaderFunction(data); + }; + offloader_ = std::make_unique( - AudioFileWriter::FILE_WRITER_CHANNEL_CAPACITY); - sender_ = std::move(sender); - receiver_ = std::move(receiver); - stopFileWriterThread_.store(false, std::memory_order_release); + FILE_WRITER_SPSC_OVERFLOW_STRATEGY, + FILE_WRITER_SPSC_WAIT_STRATEGY>>(FILE_WRITER_CHANNEL_CAPACITY, offloaderLambda); } void AndroidFileWriterBackend::writeAudioData(void *data, int numFrames) { - sender_.send({data, numFrames}); -} - -void AndroidFileWriterBackend::stopFileWriterThread() { - stopFileWriterThread_.store(true, std::memory_order_release); - sender_.send({nullptr, 0}); - if (fileWriterThread_.joinable()) { - fileWriterThread_.join(); - } + offloader_->getSender()->send({data, numFrames}); } } // namespace audioapi 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 04b8bac73..5c7056a51 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 @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -29,13 +30,14 @@ class AndroidFileWriterBackend : public AudioFileWriter { double getCurrentDuration() const override { return static_cast(framesWritten_.load(std::memory_order_acquire)) / streamSampleRate_; } protected: - void stopFileWriterThread(); float streamSampleRate_{0}; int32_t streamChannelCount_{0}; int32_t streamMaxBufferSize_{0}; std::string filePath_; - channels::spsc::Sender sender_; - channels::spsc::Receiver receiver_; + + // delay initialization of offloader until prepare is called + std::unique_ptr> offloader_; + virtual void taskOffloaderFunction(WriterData data) = 0; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.cpp index 83f62c4a5..1a2e3ba56 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.cpp @@ -105,9 +105,8 @@ Result AndroidRecorderCallback::prepare( }; offloader_ = std::make_unique>( - AudioRecorderCallback::RECORDER_CALLBACK_CHANNEL_CAPACITY, offloaderLambda); + RECORDER_CALLBACK_SPSC_OVERFLOW_STRATEGY, + RECORDER_CALLBACK_SPSC_WAIT_STRATEGY>>(RECORDER_CALLBACK_CHANNEL_CAPACITY, offloaderLambda); return Result::Ok(None); } diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.h index 781b2b7ba..8c4c6e81a 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.h @@ -50,7 +50,7 @@ class AndroidRecorderCallback : public AudioRecorderCallback { private: // delay initialization of offloader until prepare is called - std::unique_ptr> offloader_; + std::unique_ptr> offloader_; void taskOffloaderFunction(CallbackData data); }; 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 c5984b203..a0107388f 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 @@ -76,6 +76,15 @@ OpenFileResult FFmpegAudioFileWriter::openFile( return OpenFileResult::Err("Unsupported codec for the given file format"); } + auto offloaderLambda = [this](WriterData data) { + taskOffloaderFunction(data); + }; + + offloader_ = std::make_unique>(FILE_WRITER_CHANNEL_CAPACITY, offloaderLambda); + return initializeFormatContext(codec) .and_then([this, codec](auto) { return configureAndOpenCodec(codec); }) .and_then([this](auto) { return initializeStream(); }) @@ -87,10 +96,6 @@ OpenFileResult FFmpegAudioFileWriter::openFile( initializeBuffers(streamMaxBufferSize); isFileOpen_.store(true, std::memory_order_release); return OpenFileResult::Ok(filePath); - }) - .and_then([this](const auto &res) { - fileWriterThread_ = std::thread(&FFmpegAudioFileWriter::fileWriterThreadHandler, this); - return OpenFileResult::Ok(res); }); } @@ -123,30 +128,27 @@ CloseFileResult FFmpegAudioFileWriter::closeFile() { if (writeEncodedPackets() < 0) { return CloseFileResult::Err("Failed to drain encoder packets"); } - - stopFileWriterThread(); + offloader_.reset(); return finalizeOutput(); } /// @brief Writes audio data to the currently opened file. /// This method should be called only from the audio thread (or audio side-effect thread in the future). -void FFmpegAudioFileWriter::fileWriterThreadHandler() { - while (!stopFileWriterThread_.load(std::memory_order_acquire)) { - auto [data, numFrames] = receiver_.receive(); - if (!isFileOpen()) { - return; - } +void FFmpegAudioFileWriter::taskOffloaderFunction(WriterData data) { + auto [audioData, numFrames] = data; + if (!isFileOpen()) { + return; + } - if (!resampleAndPushToFifo(data, numFrames)) { - return; - } + if (!resampleAndPushToFifo(audioData, numFrames)) { + return; + } - framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); + framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); - if (processFifo(false) < 0) { - return; - } + if (processFifo(false) < 0) { + return; } } 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 9ef11c2a4..ff95874e7 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 @@ -63,7 +63,7 @@ class FFmpegAudioFileWriter : public AndroidFileWriterBackend { // Finalization helper methods CloseFileResult finalizeOutput(); - void fileWriterThreadHandler() override; + void taskOffloaderFunction(WriterData data) override; }; } // namespace android::ffmpeg 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 a22fa0aae..d8e1fe8e3 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 @@ -103,8 +103,16 @@ OpenFileResult MiniAudioFileWriter::openFile( "Failed to initialize encoder" + std::string(ma_result_description(result))); } + auto offloaderLambda = [this](WriterData data) { + taskOffloaderFunction(data); + }; + + offloader_ = std::make_unique>(FILE_WRITER_CHANNEL_CAPACITY, offloaderLambda); + isFileOpen_.store(true, std::memory_order_release); - fileWriterThread_ = std::thread(&MiniAudioFileWriter::fileWriterThreadHandler, this); return OpenFileResult ::Ok(filePath_); } @@ -160,7 +168,7 @@ CloseFileResult MiniAudioFileWriter::closeFile() { fclose(file); fileSizeInMB = static_cast(fileSizeInBytes) / MB_IN_BYTES; } - stopFileWriterThread(); + offloader_.reset(); filePath_ = ""; return CloseFileResult ::Ok({fileSizeInMB, durationInSeconds}); @@ -169,44 +177,42 @@ CloseFileResult MiniAudioFileWriter::closeFile() { /// @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 -void MiniAudioFileWriter::fileWriterThreadHandler() { - while (!stopFileWriterThread_.load(std::memory_order_acquire)) { - auto [data, numFrames] = receiver_.receive(); - ma_uint64 framesWritten = 0; - ma_result result; - - if (!isFileOpen()) { - return; - } - - if (!isConverterRequired()) { - result = ma_encoder_write_pcm_frames(encoder_.get(), data, numFrames, &framesWritten); - - if (result != MA_SUCCESS) { - invokeOnErrorCallback( - "Failed to write audio data to file: " + filePath_ + - std::string(ma_result_description(result))); - return; - } - - framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); - continue; - } +void MiniAudioFileWriter::taskOffloaderFunction(WriterData data) { + auto [audioData, numFrames] = data; + ma_uint64 framesWritten = 0; + ma_result result; - ma_uint64 convertedFrameCount = convertBuffer(data, numFrames); + if (!isFileOpen()) { + return; + } - result = ma_encoder_write_pcm_frames( - encoder_.get(), processingBuffer_, convertedFrameCount, &framesWritten); + if (!isConverterRequired()) { + result = ma_encoder_write_pcm_frames(encoder_.get(), audioData, numFrames, &framesWritten); if (result != MA_SUCCESS) { invokeOnErrorCallback( - "Failed to write converted audio data to file: " + filePath_ + + "Failed to write audio data to file: " + filePath_ + std::string(ma_result_description(result))); return; } framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); + return; } + + ma_uint64 convertedFrameCount = convertBuffer(audioData, numFrames); + + result = ma_encoder_write_pcm_frames( + encoder_.get(), processingBuffer_, convertedFrameCount, &framesWritten); + + if (result != MA_SUCCESS) { + invokeOnErrorCallback( + "Failed to write converted audio data to file: " + filePath_ + + std::string(ma_result_description(result))); + return; + } + + framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); } /// @brief Converts the audio data buffer if necessary. 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 fe86ead01..624c9ed87 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 @@ -34,7 +34,7 @@ class MiniAudioFileWriter : public AndroidFileWriterBackend { ma_uint64 convertBuffer(void *data, int numFrames); bool isConverterRequired(); - void fileWriterThreadHandler() override; + void taskOffloaderFunction(WriterData data) override; }; } // 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 c13da7c6d..464ac82e8 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 @@ -45,10 +45,6 @@ class AudioFileWriter { channels::spsc::OverflowStrategy::OVERWRITE_ON_FULL; static constexpr auto FILE_WRITER_SPSC_WAIT_STRATEGY = channels::spsc::WaitStrategy::ATOMIC_WAIT; static constexpr auto FILE_WRITER_CHANNEL_CAPACITY = 64; - - std::thread fileWriterThread_; - std::atomic stopFileWriterThread_; - virtual void fileWriterThreadHandler() = 0; }; } // namespace audioapi 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 4f5b4a0bc..399406511 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 @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -57,17 +58,13 @@ class IOSFileWriter : public AudioFileWriter { AVAudioPCMBuffer *converterOutputBuffer_; private: - channels::spsc::Sender< + // delay initialization of offloader until prepare is called + std::unique_ptr - sender_; - channels::spsc::Receiver< - WriterData, - AudioFileWriter::FILE_WRITER_SPSC_OVERFLOW_STRATEGY, - AudioFileWriter::FILE_WRITER_SPSC_WAIT_STRATEGY> - receiver_; - void fileWriterThreadHandler() override; + FILE_WRITER_SPSC_OVERFLOW_STRATEGY, + FILE_WRITER_SPSC_WAIT_STRATEGY>> + offloader_; + void taskOffloaderFunction(WriterData data); }; } // namespace audioapi 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 446ac9a53..e486edf2b 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 @@ -14,14 +14,6 @@ const std::shared_ptr &fileProperties) : AudioFileWriter(audioEventHandlerRegistry, fileProperties) { - auto [sender, receiver] = channels::spsc::channel< - WriterData, - AudioFileWriter::FILE_WRITER_SPSC_OVERFLOW_STRATEGY, - AudioFileWriter::FILE_WRITER_SPSC_WAIT_STRATEGY>( - AudioFileWriter::FILE_WRITER_CHANNEL_CAPACITY); - sender_ = std::move(sender); - receiver_ = std::move(receiver); - stopFileWriterThread_.store(false, std::memory_order_release); } IOSFileWriter::~IOSFileWriter() @@ -107,7 +99,15 @@ return OpenFileResult::Err("Error creating converter buffers"); } - fileWriterThread_ = std::thread(&IOSFileWriter::fileWriterThreadHandler, this); + auto offloaderLambda = [this](WriterData data) { + taskOffloaderFunction(data); + }; + + offloader_ = std::make_unique>(FILE_WRITER_CHANNEL_CAPACITY, offloaderLambda); + return OpenFileResult::Ok([[fileURL_ path] UTF8String]); } } @@ -143,12 +143,7 @@ fileURL_ = nil; framesWritten_.store(0, std::memory_order_release); - - stopFileWriterThread_.store(true, std::memory_order_release); - sender_.send({nullptr, 0}); - if (fileWriterThread_.joinable()) { - fileWriterThread_.join(); - } + offloader_.reset(); return CloseFileResult::Ok(std::make_tuple(fileDuration, fileSizeBytesMb)); } @@ -161,92 +156,88 @@ if (audioFile_ == nil) { invokeOnErrorCallback("Attempted to write audio data when file is not open"); } else { - sender_.send({audioBufferList, numFrames}); + offloader_->getSender()->send({audioBufferList, numFrames}); } } -void IOSFileWriter::fileWriterThreadHandler() +void IOSFileWriter::taskOffloaderFunction(WriterData data) { - while (!stopFileWriterThread_.load(std::memory_order_acquire)) { - auto [audioBufferList, numFrames] = receiver_.receive(); - if (audioBufferList == nullptr) - continue; - @autoreleasepool { - NSError *error = nil; - AVAudioFormat *fileFormat = [audioFile_ processingFormat]; - - if (bufferFormat_.sampleRate == fileFormat.sampleRate && - bufferFormat_.channelCount == fileFormat.channelCount && - bufferFormat_.isInterleaved == fileFormat.isInterleaved) { - // We can use the converter input buffer as a "transport" layer to the file - for (size_t i = 0; i < bufferFormat_.channelCount; ++i) { - memcpy( - converterInputBuffer_.mutableAudioBufferList->mBuffers[i].mData, - audioBufferList->mBuffers[i].mData, - audioBufferList->mBuffers[i].mDataByteSize); - } - converterInputBuffer_.frameLength = numFrames; - - [audioFile_ writeFromBuffer:converterInputBuffer_ error:&error]; - - if (error != nil) { - invokeOnErrorCallback( - std::string("Error writing audio data to file, native error: ") + - [[error debugDescription] UTF8String]); - return; - } - - framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); - return; - } + auto [audioBufferList, numFrames] = data; + if (audioBufferList == nullptr) + return; + @autoreleasepool { + NSError *error = nil; + AVAudioFormat *fileFormat = [audioFile_ processingFormat]; + if (bufferFormat_.sampleRate == fileFormat.sampleRate && + bufferFormat_.channelCount == fileFormat.channelCount && + bufferFormat_.isInterleaved == fileFormat.isInterleaved) { + // We can use the converter input buffer as a "transport" layer to the file for (size_t i = 0; i < bufferFormat_.channelCount; ++i) { memcpy( converterInputBuffer_.mutableAudioBufferList->mBuffers[i].mData, audioBufferList->mBuffers[i].mData, audioBufferList->mBuffers[i].mDataByteSize); } - converterInputBuffer_.frameLength = numFrames; - __block BOOL handedOff = false; - AVAudioConverterInputBlock inputBlock = ^AVAudioBuffer *_Nullable( - AVAudioPacketCount inNumberOfPackets, AVAudioConverterInputStatus *outStatus) - { - if (handedOff) { - *outStatus = AVAudioConverterInputStatus_NoDataNow; - return nil; - } - - handedOff = true; - *outStatus = AVAudioConverterInputStatus_HaveData; - return converterInputBuffer_; - }; - - [converter_ convertToBuffer:converterOutputBuffer_ - error:&error - withInputFromBlock:inputBlock]; - converterOutputBuffer_.frameLength = - fileProperties_->sampleRate / bufferFormat_.sampleRate * numFrames; + [audioFile_ writeFromBuffer:converterInputBuffer_ error:&error]; if (error != nil) { invokeOnErrorCallback( - std::string("Error during audio conversion, native error: ") + + std::string("Error writing audio data to file, native error: ") + [[error debugDescription] UTF8String]); return; } - [audioFile_ writeFromBuffer:converterOutputBuffer_ error:&error]; + framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); + return; + } - if (error != nil) { - invokeOnErrorCallback( - std::string("Error writing audio data to file, native error: ") + - [[error debugDescription] UTF8String]); - return; + for (size_t i = 0; i < bufferFormat_.channelCount; ++i) { + memcpy( + converterInputBuffer_.mutableAudioBufferList->mBuffers[i].mData, + audioBufferList->mBuffers[i].mData, + audioBufferList->mBuffers[i].mDataByteSize); + } + + converterInputBuffer_.frameLength = numFrames; + + __block BOOL handedOff = false; + AVAudioConverterInputBlock inputBlock = ^AVAudioBuffer *_Nullable( + AVAudioPacketCount inNumberOfPackets, AVAudioConverterInputStatus *outStatus) + { + if (handedOff) { + *outStatus = AVAudioConverterInputStatus_NoDataNow; + return nil; } - framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); + handedOff = true; + *outStatus = AVAudioConverterInputStatus_HaveData; + return converterInputBuffer_; + }; + + [converter_ convertToBuffer:converterOutputBuffer_ error:&error withInputFromBlock:inputBlock]; + converterOutputBuffer_.frameLength = + fileProperties_->sampleRate / bufferFormat_.sampleRate * numFrames; + + if (error != nil) { + invokeOnErrorCallback( + std::string("Error during audio conversion, native error: ") + + [[error debugDescription] UTF8String]); + return; } + + [audioFile_ writeFromBuffer:converterOutputBuffer_ error:&error]; + + if (error != nil) { + invokeOnErrorCallback( + std::string("Error writing audio data to file, native error: ") + + [[error debugDescription] UTF8String]); + return; + } + + framesWritten_.fetch_add(numFrames, std::memory_order_acq_rel); } } diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRecorderCallback.h b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRecorderCallback.h index c02f57345..ac87f79a9 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRecorderCallback.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRecorderCallback.h @@ -51,8 +51,8 @@ class IOSRecorderCallback : public AudioRecorderCallback { private: std::unique_ptr> + RECORDER_CALLBACK_SPSC_OVERFLOW_STRATEGY, + RECORDER_CALLBACK_SPSC_WAIT_STRATEGY>> offloader_; // delay initialization of offloader until prepare is called void taskOffloaderFunction(CallbackData data); diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRecorderCallback.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRecorderCallback.mm index c906d9e1f..fdcf7c58d 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRecorderCallback.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/utils/IOSRecorderCallback.mm @@ -93,9 +93,8 @@ }; offloader_ = std::make_unique>( - AudioRecorderCallback::RECORDER_CALLBACK_CHANNEL_CAPACITY, offloaderLambda); + RECORDER_CALLBACK_SPSC_OVERFLOW_STRATEGY, + RECORDER_CALLBACK_SPSC_WAIT_STRATEGY>>(RECORDER_CALLBACK_CHANNEL_CAPACITY, offloaderLambda); } return Result::Ok(None); diff --git a/packages/react-native-audio-api/src/core/AudioRecorder.ts b/packages/react-native-audio-api/src/core/AudioRecorder.ts index 84fd3db46..8b88b3f6b 100644 --- a/packages/react-native-audio-api/src/core/AudioRecorder.ts +++ b/packages/react-native-audio-api/src/core/AudioRecorder.ts @@ -28,7 +28,7 @@ function withDefaultOptions( subDirectory: 'AudioAPI', fileNamePrefix: 'recording', channelCount: 2, - format: FileFormat.Wav, + format: FileFormat.M4A, batchDurationSeconds: 0, preset: FilePreset.High, androidFlushIntervalMs: 500, From 53d9a778bee0148b7309835b8ccd0657ca3498e3 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 3 Feb 2026 16:15:12 +0100 Subject: [PATCH 3/3] feat: comment --- .../android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp | 1 - 1 file changed, 1 deletion(-) 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 a0107388f..7d97acf1b 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 @@ -134,7 +134,6 @@ CloseFileResult FFmpegAudioFileWriter::closeFile() { } /// @brief Writes audio data to the currently opened file. -/// This method should be called only from the audio thread (or audio side-effect thread in the future). void FFmpegAudioFileWriter::taskOffloaderFunction(WriterData data) { auto [audioData, numFrames] = data; if (!isFileOpen()) {