react-native-audio-api 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +8 -3
  2. package/RNAudioAPI.podspec +0 -2
  3. package/android/src/main/cpp/audioapi/CMakeLists.txt +8 -2
  4. package/android/src/main/cpp/audioapi/android/AudioAPIModule.cpp +7 -29
  5. package/android/src/main/cpp/audioapi/android/JniEventPayloadParser.cpp +83 -0
  6. package/android/src/main/cpp/audioapi/android/JniEventPayloadParser.h +14 -0
  7. package/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +5 -4
  8. package/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +1 -0
  9. package/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.cpp +37 -21
  10. package/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.h +1 -1
  11. package/common/cpp/audioapi/AudioAPIModuleInstaller.h +21 -0
  12. package/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.cpp +26 -4
  13. package/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.h +1 -0
  14. package/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp +2 -2
  15. package/common/cpp/audioapi/HostObjects/utils/AudioDecoderHostObject.cpp +3 -3
  16. package/common/cpp/audioapi/HostObjects/utils/AudioFileUtilsHostObject.cpp +60 -0
  17. package/common/cpp/audioapi/HostObjects/utils/AudioFileUtilsHostObject.h +24 -0
  18. package/common/cpp/audioapi/HostObjects/utils/NodeOptionsParser.h +2 -2
  19. package/common/cpp/audioapi/core/OfflineAudioContext.cpp +0 -1
  20. package/common/cpp/audioapi/core/OfflineAudioContext.h +0 -1
  21. package/common/cpp/audioapi/core/effects/ConvolverNode.cpp +4 -10
  22. package/common/cpp/audioapi/core/effects/ConvolverNode.h +0 -4
  23. package/common/cpp/audioapi/core/effects/WorkletProcessingNode.cpp +1 -1
  24. package/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.cpp +13 -11
  25. package/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.h +5 -9
  26. package/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp +47 -139
  27. package/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h +11 -8
  28. package/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp +29 -114
  29. package/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h +6 -8
  30. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp +9 -11
  31. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.h +1 -1
  32. package/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp +2 -2
  33. package/common/cpp/audioapi/core/types/AudioFormat.h +3 -1
  34. package/common/cpp/audioapi/core/utils/AudioDecoder.cpp +120 -91
  35. package/common/cpp/audioapi/core/utils/AudioDecoder.h +24 -101
  36. package/common/cpp/audioapi/core/utils/AudioFileConcatenator.cpp +862 -0
  37. package/common/cpp/audioapi/core/utils/AudioFileConcatenator.h +164 -0
  38. package/common/cpp/audioapi/core/utils/AudioFileWriter.cpp +2 -4
  39. package/common/cpp/audioapi/core/utils/AudioRecorderCallback.cpp +7 -12
  40. package/common/cpp/audioapi/core/utils/AudioRecorderCallback.h +2 -0
  41. package/common/cpp/audioapi/core/utils/buffer/BufferProcessingDirection.h +6 -0
  42. package/common/cpp/audioapi/core/utils/buffer/BufferProcessorBase.cpp +110 -0
  43. package/common/cpp/audioapi/core/utils/buffer/BufferProcessorBase.h +75 -0
  44. package/common/cpp/audioapi/core/utils/buffer/QueueBufferProcessor.cpp +129 -0
  45. package/common/cpp/audioapi/core/utils/buffer/QueueBufferProcessor.h +55 -0
  46. package/common/cpp/audioapi/core/utils/buffer/SingleBufferProcessor.cpp +95 -0
  47. package/common/cpp/audioapi/core/utils/buffer/SingleBufferProcessor.h +52 -0
  48. package/common/cpp/audioapi/core/utils/param/ParamControlQueue.cpp +12 -8
  49. package/common/cpp/audioapi/events/AudioEventHandlerRegistry.cpp +65 -157
  50. package/common/cpp/audioapi/events/AudioEventHandlerRegistry.h +52 -33
  51. package/common/cpp/audioapi/events/AudioEventPayload.h +87 -0
  52. package/common/cpp/audioapi/events/IAudioEventHandlerRegistry.h +12 -12
  53. package/common/cpp/audioapi/libs/decoding/IncrementalAudioDecoder.h +12 -10
  54. package/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.cpp +152 -78
  55. package/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.h +6 -6
  56. package/common/cpp/audioapi/libs/miniaudio/MiniAudioDecoding.cpp +34 -20
  57. package/common/cpp/audioapi/libs/miniaudio/MiniAudioDecoding.h +4 -4
  58. package/common/cpp/audioapi/utils/AudioArray.hpp +6 -1
  59. package/common/cpp/audioapi/utils/CrossThreadEventScheduler.hpp +1 -1
  60. package/common/cpp/audioapi/utils/TaskOffloader.hpp +3 -5
  61. package/ios/audioapi/ios/AudioAPIModule.h +2 -1
  62. package/ios/audioapi/ios/AudioAPIModule.mm +4 -25
  63. package/ios/audioapi/ios/core/IOSAudioPlayer.h +16 -2
  64. package/ios/audioapi/ios/core/IOSAudioPlayer.mm +90 -24
  65. package/ios/audioapi/ios/core/IOSAudioRecorder.mm +1 -1
  66. package/ios/audioapi/ios/core/utils/IOSRecorderCallback.mm +64 -20
  67. package/ios/audioapi/ios/system/AudioSessionManager.mm +18 -7
  68. package/ios/audioapi/ios/system/SystemNotificationManager.mm +22 -22
  69. package/ios/audioapi/ios/system/notification/PlaybackNotification.mm +10 -13
  70. package/lib/commonjs/AudioAPIModule/AudioAPIModule.js +1 -1
  71. package/lib/commonjs/AudioAPIModule/AudioAPIModule.js.map +1 -1
  72. package/lib/commonjs/api.js +8 -0
  73. package/lib/commonjs/api.js.map +1 -1
  74. package/lib/commonjs/api.web.js +6 -1
  75. package/lib/commonjs/api.web.js.map +1 -1
  76. package/lib/commonjs/core/AudioFileUtils.js +35 -0
  77. package/lib/commonjs/core/AudioFileUtils.js.map +1 -0
  78. package/lib/commonjs/mock/index.js +15 -1
  79. package/lib/commonjs/mock/index.js.map +1 -1
  80. package/lib/module/AudioAPIModule/AudioAPIModule.js +1 -1
  81. package/lib/module/AudioAPIModule/AudioAPIModule.js.map +1 -1
  82. package/lib/module/api.js +1 -0
  83. package/lib/module/api.js.map +1 -1
  84. package/lib/module/api.web.js +4 -1
  85. package/lib/module/api.web.js.map +1 -1
  86. package/lib/module/core/AudioFileUtils.js +31 -0
  87. package/lib/module/core/AudioFileUtils.js.map +1 -0
  88. package/lib/module/mock/index.js +14 -1
  89. package/lib/module/mock/index.js.map +1 -1
  90. package/lib/typescript/AudioAPIModule/AudioAPIModule.d.ts.map +1 -1
  91. package/lib/typescript/api.d.ts +2 -0
  92. package/lib/typescript/api.d.ts.map +1 -1
  93. package/lib/typescript/api.web.d.ts +3 -1
  94. package/lib/typescript/api.web.d.ts.map +1 -1
  95. package/lib/typescript/core/AudioFileUtils.d.ts +2 -0
  96. package/lib/typescript/core/AudioFileUtils.d.ts.map +1 -0
  97. package/lib/typescript/interfaces.d.ts +3 -0
  98. package/lib/typescript/interfaces.d.ts.map +1 -1
  99. package/lib/typescript/mock/index.d.ts +3 -1
  100. package/lib/typescript/mock/index.d.ts.map +1 -1
  101. package/package.json +10 -4
  102. package/scripts/download-prebuilt-binaries.sh +34 -1
  103. package/scripts/validate-worklets-version.js +1 -1
  104. package/src/AudioAPIModule/AudioAPIModule.ts +1 -0
  105. package/src/AudioAPIModule/globals.d.ts +3 -0
  106. package/src/api.ts +2 -0
  107. package/src/api.web.ts +10 -2
  108. package/src/core/AudioFileUtils.ts +49 -0
  109. package/src/interfaces.ts +7 -0
  110. package/src/mock/index.ts +29 -0
@@ -0,0 +1,862 @@
1
+ #include <audioapi/core/utils/AudioFileConcatenator.h>
2
+ #include <audioapi/libs/miniaudio/miniaudio.h>
3
+
4
+ #include <algorithm>
5
+ #include <array>
6
+ #include <cctype>
7
+ #include <charconv>
8
+ #include <cstdint>
9
+ #include <cstring>
10
+ #include <limits>
11
+ #include <memory>
12
+ #include <string>
13
+ #include <system_error>
14
+ #include <utility>
15
+ #include <vector>
16
+
17
+ #if !RN_AUDIO_API_FFMPEG_DISABLED
18
+ extern "C" {
19
+ #include <libavcodec/avcodec.h>
20
+ #include <libavformat/avformat.h>
21
+ #include <libavutil/avutil.h>
22
+ #include <libavutil/channel_layout.h>
23
+ #include <libavutil/dict.h>
24
+ #include <libavutil/mathematics.h>
25
+ }
26
+ #endif // RN_AUDIO_API_FFMPEG_DISABLED
27
+
28
+ namespace audioapi {
29
+
30
+ #if !RN_AUDIO_API_FFMPEG_DISABLED
31
+ AVDictionaryGuard::~AVDictionaryGuard() {
32
+ av_dict_free(&dictionary_);
33
+ }
34
+
35
+ AVDictionary **AVDictionaryGuard::ptr() {
36
+ return &dictionary_;
37
+ }
38
+
39
+ InputFormatContext::InputFormatContext(
40
+ std::string filePath,
41
+ AVFormatContext *context,
42
+ int audioStreamIndex)
43
+ : filePath_(std::move(filePath)), context_(context), audioStreamIndex_(audioStreamIndex) {}
44
+
45
+ InputFormatContext::InputFormatContext(InputFormatContext &&other) noexcept
46
+ : filePath_(std::move(other.filePath_)),
47
+ context_(std::exchange(other.context_, nullptr)),
48
+ audioStreamIndex_(std::exchange(other.audioStreamIndex_, -1)) {}
49
+
50
+ InputFormatContext &InputFormatContext::operator=(InputFormatContext &&other) noexcept {
51
+ if (this != &other) {
52
+ close();
53
+ filePath_ = std::move(other.filePath_);
54
+ context_ = std::exchange(other.context_, nullptr);
55
+ audioStreamIndex_ = std::exchange(other.audioStreamIndex_, -1);
56
+ }
57
+ return *this;
58
+ }
59
+
60
+ InputFormatContext::~InputFormatContext() {
61
+ close();
62
+ }
63
+
64
+ AVFormatContext *InputFormatContext::context() const {
65
+ return context_;
66
+ }
67
+
68
+ AVStream *InputFormatContext::audioStream() const {
69
+ return context_->streams[audioStreamIndex_];
70
+ }
71
+
72
+ int InputFormatContext::audioStreamIndex() const {
73
+ return audioStreamIndex_;
74
+ }
75
+
76
+ const std::string &InputFormatContext::filePath() const {
77
+ return filePath_;
78
+ }
79
+
80
+ void InputFormatContext::close() {
81
+ if (context_ != nullptr) {
82
+ avformat_close_input(&context_);
83
+ }
84
+ }
85
+
86
+ OutputFormatContext::OutputFormatContext(AVFormatContext *context) : context_(context) {}
87
+
88
+ OutputFormatContext::~OutputFormatContext() {
89
+ if (context_ == nullptr) {
90
+ return;
91
+ }
92
+
93
+ if (!(context_->oformat->flags & AVFMT_NOFILE) && context_->pb != nullptr) {
94
+ avio_closep(&context_->pb);
95
+ }
96
+
97
+ avformat_free_context(context_);
98
+ }
99
+
100
+ AVFormatContext *OutputFormatContext::get() const {
101
+ return context_;
102
+ }
103
+ #endif // RN_AUDIO_API_FFMPEG_DISABLED
104
+
105
+ namespace {
106
+
107
+ constexpr const char *fileUrlPrefix = "file://";
108
+ constexpr ma_uint64 miniaudioChunkFrames = 4096;
109
+ constexpr ma_uint64 riffWaveHeaderBytes = 36;
110
+ constexpr ma_uint64 maxRiffChunkSize = std::numeric_limits<uint32_t>::max();
111
+
112
+ bool hasNonFileProtocol(const std::string &path) {
113
+ const auto colon = path.find(':');
114
+ if (colon == std::string::npos) {
115
+ return false;
116
+ }
117
+
118
+ const auto firstSlash = path.find('/');
119
+ return firstSlash == std::string::npos || colon < firstSlash;
120
+ }
121
+
122
+ std::string percentDecode(const std::string &path) {
123
+ std::string decoded;
124
+ decoded.reserve(path.size());
125
+
126
+ for (size_t i = 0; i < path.size(); ++i) {
127
+ if (path[i] != '%' || i + 2 >= path.size()) {
128
+ decoded.push_back(path[i]);
129
+ continue;
130
+ }
131
+
132
+ unsigned int value = 0;
133
+ auto first = path.data() + i + 1;
134
+ auto last = first + 2;
135
+ auto result = std::from_chars(first, last, value, 16);
136
+ if (result.ec != std::errc() || result.ptr != last) {
137
+ decoded.push_back(path[i]);
138
+ continue;
139
+ }
140
+
141
+ decoded.push_back(static_cast<char>(value));
142
+ i += 2;
143
+ }
144
+
145
+ return decoded;
146
+ }
147
+
148
+ AudioFileConcatResult validatePaths(
149
+ const std::vector<std::string> &inputPaths,
150
+ const std::string &outputPath) {
151
+ if (inputPaths.empty()) {
152
+ return Err("concatAudioFiles requires at least one input path.");
153
+ }
154
+
155
+ for (size_t i = 0; i < inputPaths.size(); ++i) {
156
+ if (inputPaths[i].empty()) {
157
+ return Err("concatAudioFiles input path at index " + std::to_string(i) + " is empty.");
158
+ }
159
+
160
+ if (hasNonFileProtocol(inputPaths[i])) {
161
+ return Err(
162
+ "concatAudioFiles input path at index " + std::to_string(i) +
163
+ " must be a local file path or file:// URL.");
164
+ }
165
+ }
166
+
167
+ if (outputPath.empty()) {
168
+ return Err("concatAudioFiles requires an output path.");
169
+ }
170
+
171
+ if (hasNonFileProtocol(outputPath)) {
172
+ return Err("concatAudioFiles output path must be a local file path or file:// URL.");
173
+ }
174
+
175
+ return Ok(outputPath);
176
+ }
177
+
178
+ std::string lowercaseExtension(const std::string &path) {
179
+ const auto dotIndex = path.find_last_of('.');
180
+ if (dotIndex == std::string::npos) {
181
+ return "";
182
+ }
183
+
184
+ std::string extension = path.substr(dotIndex + 1);
185
+ std::ranges::transform(extension, extension.begin(), [](unsigned char c) {
186
+ return static_cast<char>(std::tolower(c));
187
+ });
188
+
189
+ return extension;
190
+ }
191
+
192
+ bool hasExtension(const std::string &path, const std::vector<std::string> &extensions) {
193
+ const std::string extension = lowercaseExtension(path);
194
+ return std::ranges::find(extensions, extension) != extensions.end();
195
+ }
196
+
197
+ bool isMiniaudioOutputPath(const std::string &path) {
198
+ return hasExtension(path, {"wav"});
199
+ }
200
+
201
+ bool isFFmpegRemuxOutputPath(const std::string &path) {
202
+ return hasExtension(path, {"m4a", "mp4"});
203
+ }
204
+
205
+ bool isFFmpegOutputPath(const std::string &path) {
206
+ return isFFmpegRemuxOutputPath(path);
207
+ }
208
+
209
+ std::string parseMiniAudioError(ma_result errorCode) {
210
+ return {ma_result_description(errorCode)};
211
+ }
212
+
213
+ } // namespace
214
+
215
+ MiniAudioDecoderGuard::MiniAudioDecoderGuard(MiniAudioDecoderGuard &&other) noexcept
216
+ : filePath_(std::move(other.filePath_)),
217
+ decoder_(std::move(other.decoder_)),
218
+ initialized_(std::exchange(other.initialized_, false)) {}
219
+
220
+ MiniAudioDecoderGuard &MiniAudioDecoderGuard::operator=(MiniAudioDecoderGuard &&other) noexcept {
221
+ if (this != &other) {
222
+ close();
223
+ filePath_ = std::move(other.filePath_);
224
+ decoder_ = std::move(other.decoder_);
225
+ initialized_ = std::exchange(other.initialized_, false);
226
+ }
227
+ return *this;
228
+ }
229
+
230
+ MiniAudioDecoderGuard::~MiniAudioDecoderGuard() {
231
+ close();
232
+ }
233
+
234
+ Result<MiniAudioDecoderGuard, std::string> MiniAudioDecoderGuard::open(
235
+ const std::string &filePath,
236
+ ma_format outputFormat) {
237
+ MiniAudioDecoderGuard input;
238
+ input.filePath_ = filePath;
239
+ input.decoder_ = std::make_unique<ma_decoder>();
240
+
241
+ ma_decoder_config config = ma_decoder_config_init(outputFormat, 0, 0);
242
+ const ma_result result = ma_decoder_init_file(filePath.c_str(), &config, input.decoder_.get());
243
+ if (result != MA_SUCCESS) {
244
+ return Err(
245
+ "Failed to open input file '" + filePath +
246
+ "' with miniaudio: " + parseMiniAudioError(result));
247
+ }
248
+
249
+ input.initialized_ = true;
250
+ if (input.decoder_->outputSampleRate == 0 || input.decoder_->outputChannels == 0) {
251
+ return Err("Input file '" + filePath + "' is missing required audio parameters.");
252
+ }
253
+
254
+ return Ok(std::move(input));
255
+ }
256
+
257
+ ma_decoder *MiniAudioDecoderGuard::get() {
258
+ return decoder_.get();
259
+ }
260
+
261
+ const std::string &MiniAudioDecoderGuard::filePath() const {
262
+ return filePath_;
263
+ }
264
+
265
+ ma_uint32 MiniAudioDecoderGuard::sampleRate() const {
266
+ return decoder_->outputSampleRate;
267
+ }
268
+
269
+ ma_uint32 MiniAudioDecoderGuard::channels() const {
270
+ return decoder_->outputChannels;
271
+ }
272
+
273
+ ma_format MiniAudioDecoderGuard::format() const {
274
+ return decoder_->outputFormat;
275
+ }
276
+
277
+ void MiniAudioDecoderGuard::close() {
278
+ if (initialized_ && decoder_ != nullptr) {
279
+ ma_decoder_uninit(decoder_.get());
280
+ initialized_ = false;
281
+ }
282
+ decoder_.reset();
283
+ }
284
+
285
+ MiniAudioEncoderGuard::~MiniAudioEncoderGuard() {
286
+ close();
287
+ }
288
+
289
+ AudioFileConcatResult MiniAudioEncoderGuard::open(
290
+ const std::string &outputPath,
291
+ ma_format format,
292
+ ma_uint32 sampleRate,
293
+ ma_uint32 channels) {
294
+ ma_encoder_config config =
295
+ ma_encoder_config_init(ma_encoding_format_wav, format, channels, sampleRate);
296
+
297
+ const ma_result result = ma_encoder_init_file(outputPath.c_str(), &config, &encoder_);
298
+ if (result != MA_SUCCESS) {
299
+ return Err(
300
+ "Failed to open output file '" + outputPath +
301
+ "' with miniaudio: " + parseMiniAudioError(result));
302
+ }
303
+
304
+ initialized_ = true;
305
+ return Ok(outputPath);
306
+ }
307
+
308
+ AudioFileConcatResult MiniAudioEncoderGuard::write(
309
+ const std::string &inputPath,
310
+ const void *frames,
311
+ ma_uint64 frameCount) {
312
+ ma_uint64 framesWritten = 0;
313
+ const ma_result result =
314
+ ma_encoder_write_pcm_frames(&encoder_, frames, frameCount, &framesWritten);
315
+ if (result != MA_SUCCESS) {
316
+ return Err(
317
+ "Failed to write decoded frames from '" + inputPath +
318
+ "' with miniaudio: " + parseMiniAudioError(result));
319
+ }
320
+
321
+ if (framesWritten != frameCount) {
322
+ return Err("Failed to write all decoded frames from '" + inputPath + "' with miniaudio.");
323
+ }
324
+
325
+ return Ok(inputPath);
326
+ }
327
+
328
+ void MiniAudioEncoderGuard::close() {
329
+ if (initialized_) {
330
+ ma_encoder_uninit(&encoder_);
331
+ initialized_ = false;
332
+ }
333
+ }
334
+
335
+ namespace {
336
+
337
+ AudioFileConcatResult validateCompatibleMiniAudioInput(
338
+ const MiniAudioDecoderGuard &input,
339
+ const MiniAudioDecoderGuard &reference) {
340
+ if (input.sampleRate() != reference.sampleRate()) {
341
+ return Err("Input file '" + input.filePath() + "' uses a different sample rate.");
342
+ }
343
+
344
+ if (input.channels() != reference.channels()) {
345
+ return Err("Input file '" + input.filePath() + "' uses a different channel count.");
346
+ }
347
+
348
+ if (input.format() != reference.format()) {
349
+ return Err("Input file '" + input.filePath() + "' uses a different sample format.");
350
+ }
351
+
352
+ return Ok(input.filePath());
353
+ }
354
+
355
+ AudioFileConcatResult openAndValidateMiniAudioInputs(
356
+ const std::vector<std::string> &inputPaths,
357
+ std::vector<MiniAudioDecoderGuard> &inputs,
358
+ ma_format outputFormat = ma_format_unknown) {
359
+ inputs.reserve(inputPaths.size());
360
+
361
+ for (const auto &inputPath : inputPaths) {
362
+ auto inputResult = MiniAudioDecoderGuard::open(inputPath, outputFormat);
363
+ if (inputResult.is_err()) {
364
+ return Err(inputResult.unwrap_err());
365
+ }
366
+
367
+ inputs.emplace_back(std::move(inputResult).unwrap());
368
+
369
+ if (inputs.size() > 1) {
370
+ auto validationResult = validateCompatibleMiniAudioInput(inputs.back(), inputs.front());
371
+ if (validationResult.is_err()) {
372
+ return validationResult;
373
+ }
374
+ }
375
+ }
376
+
377
+ return Ok(std::string());
378
+ }
379
+
380
+ AudioFileConcatResult validateRiffWaveOutputSize(
381
+ std::vector<MiniAudioDecoderGuard> &inputs,
382
+ ma_uint64 bytesPerFrame) {
383
+ if (bytesPerFrame == 0) {
384
+ return Err("Input files use an unsupported WAV sample format.");
385
+ }
386
+
387
+ ma_uint64 totalDataBytes = 0;
388
+ for (auto &input : inputs) {
389
+ ma_uint64 frameCount = 0;
390
+ const ma_result result = ma_decoder_get_length_in_pcm_frames(input.get(), &frameCount);
391
+ if (result != MA_SUCCESS) {
392
+ return Err(
393
+ "Failed to determine decoded frame count for '" + input.filePath() +
394
+ "' with miniaudio: " + parseMiniAudioError(result));
395
+ }
396
+
397
+ if (frameCount > (std::numeric_limits<ma_uint64>::max() - totalDataBytes) / bytesPerFrame) {
398
+ return Err(
399
+ "concatAudioFiles WAV output exceeds the RIFF WAV size limit. Split the output or use a format with RF64/W64 support.");
400
+ }
401
+
402
+ totalDataBytes += frameCount * bytesPerFrame;
403
+ if (riffWaveHeaderBytes + totalDataBytes + (totalDataBytes & 1) > maxRiffChunkSize) {
404
+ return Err(
405
+ "concatAudioFiles WAV output exceeds the RIFF WAV size limit. Split the output or use a format with RF64/W64 support.");
406
+ }
407
+ }
408
+
409
+ return Ok(std::string());
410
+ }
411
+
412
+ AudioFileConcatResult concatAudioFilesWithMiniAudio(
413
+ const std::vector<std::string> &inputPaths,
414
+ const std::string &outputPath) {
415
+ if (!isMiniaudioOutputPath(outputPath)) {
416
+ return Err("concatAudioFiles supports miniaudio output only for WAV files.");
417
+ }
418
+
419
+ for (const auto &inputPath : inputPaths) {
420
+ if (!hasExtension(inputPath, {"wav"})) {
421
+ return Err("concatAudioFiles WAV output requires all input files to use the WAV extension.");
422
+ }
423
+ }
424
+
425
+ std::vector<MiniAudioDecoderGuard> inputs;
426
+ auto inputValidationResult = openAndValidateMiniAudioInputs(inputPaths, inputs);
427
+ if (inputValidationResult.is_err()) {
428
+ return inputValidationResult;
429
+ }
430
+
431
+ const ma_uint64 bytesPerFrame =
432
+ inputs.front().channels() * ma_get_bytes_per_sample(inputs.front().format());
433
+ auto outputSizeResult = validateRiffWaveOutputSize(inputs, bytesPerFrame);
434
+ if (outputSizeResult.is_err()) {
435
+ return outputSizeResult;
436
+ }
437
+
438
+ MiniAudioEncoderGuard output;
439
+ auto outputResult = output.open(
440
+ outputPath, inputs.front().format(), inputs.front().sampleRate(), inputs.front().channels());
441
+ if (outputResult.is_err()) {
442
+ return outputResult;
443
+ }
444
+
445
+ std::vector<uint8_t> buffer(miniaudioChunkFrames * bytesPerFrame);
446
+ for (auto &input : inputs) {
447
+ while (true) {
448
+ ma_uint64 framesRead = 0;
449
+ const ma_result result =
450
+ ma_decoder_read_pcm_frames(input.get(), buffer.data(), miniaudioChunkFrames, &framesRead);
451
+ if (result != MA_SUCCESS && result != MA_AT_END) {
452
+ return Err(
453
+ "Failed to decode frames from '" + input.filePath() +
454
+ "' with miniaudio: " + parseMiniAudioError(result));
455
+ }
456
+
457
+ if (framesRead == 0) {
458
+ break;
459
+ }
460
+
461
+ auto writeResult = output.write(input.filePath(), buffer.data(), framesRead);
462
+ if (writeResult.is_err()) {
463
+ return writeResult;
464
+ }
465
+
466
+ if (result == MA_AT_END) {
467
+ break;
468
+ }
469
+ }
470
+ }
471
+
472
+ return Ok(outputPath);
473
+ }
474
+
475
+ #if !RN_AUDIO_API_FFMPEG_DISABLED
476
+ const char *getMuxerNameForOutputPath(const std::string &outputPath) {
477
+ const std::string extension = lowercaseExtension(outputPath);
478
+
479
+ if (extension == "m4a" || extension == "mp4") {
480
+ return "mp4";
481
+ }
482
+
483
+ return nullptr;
484
+ }
485
+
486
+ std::string parseFFmpegError(int errorCode) {
487
+ std::array<char, AV_ERROR_MAX_STRING_SIZE> errorBuffer{};
488
+
489
+ if (av_strerror(errorCode, errorBuffer.data(), sizeof(errorBuffer)) < 0) {
490
+ return "Unknown FFmpeg error: " + std::to_string(errorCode);
491
+ }
492
+
493
+ return {errorBuffer.data()};
494
+ }
495
+
496
+ int findAudioStreamIndex(AVFormatContext *formatContext) {
497
+ for (unsigned int i = 0; i < formatContext->nb_streams; ++i) {
498
+ if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
499
+ return static_cast<int>(i);
500
+ }
501
+ }
502
+ return -1;
503
+ }
504
+
505
+ bool hasUsableAudioParameters(const AVStream *stream) {
506
+ const auto *codecParameters = stream->codecpar;
507
+ return codecParameters->codec_id != AV_CODEC_ID_NONE && codecParameters->sample_rate > 0 &&
508
+ codecParameters->ch_layout.nb_channels > 0;
509
+ }
510
+
511
+ using InputOpenResult = Result<InputFormatContext, std::string>;
512
+
513
+ InputOpenResult openInput(const std::string &filePath) {
514
+ AVFormatContext *rawContext = nullptr;
515
+ AVDictionaryGuard openOptions;
516
+ av_dict_set(openOptions.ptr(), "protocol_whitelist", "file", 0);
517
+
518
+ int result = avformat_open_input(&rawContext, filePath.c_str(), nullptr, openOptions.ptr());
519
+
520
+ if (result < 0) {
521
+ return Err("Failed to open input file '" + filePath + "': " + parseFFmpegError(result));
522
+ }
523
+
524
+ std::unique_ptr<AVFormatContext, void (*)(AVFormatContext *)> guard(
525
+ rawContext, [](AVFormatContext *context) {
526
+ if (context != nullptr) {
527
+ avformat_close_input(&context);
528
+ }
529
+ });
530
+
531
+ int audioStreamIndex = findAudioStreamIndex(rawContext);
532
+ if (audioStreamIndex < 0) {
533
+ return Err("Input file '" + filePath + "' does not contain an audio stream.");
534
+ }
535
+
536
+ if (!hasUsableAudioParameters(rawContext->streams[audioStreamIndex])) {
537
+ AVDictionaryGuard streamInfoOptions;
538
+ av_dict_set(streamInfoOptions.ptr(), "protocol_whitelist", "file", 0);
539
+
540
+ result = avformat_find_stream_info(rawContext, streamInfoOptions.ptr());
541
+ if (result < 0) {
542
+ return Err("Failed to read stream info from '" + filePath + "': " + parseFFmpegError(result));
543
+ }
544
+
545
+ audioStreamIndex = findAudioStreamIndex(rawContext);
546
+ if (audioStreamIndex < 0) {
547
+ return Err("Input file '" + filePath + "' does not contain an audio stream.");
548
+ }
549
+ }
550
+
551
+ if (!hasUsableAudioParameters(rawContext->streams[audioStreamIndex])) {
552
+ return Err("Input file '" + filePath + "' is missing required audio parameters.");
553
+ }
554
+
555
+ return Ok(InputFormatContext(filePath, guard.release(), audioStreamIndex));
556
+ }
557
+
558
+ AudioFileConcatResult validateCompatibleInput(
559
+ const InputFormatContext &input,
560
+ const InputFormatContext &reference) {
561
+ const auto *candidate = input.audioStream()->codecpar;
562
+ const auto *base = reference.audioStream()->codecpar;
563
+
564
+ if (std::strcmp(input.context()->iformat->name, reference.context()->iformat->name) != 0) {
565
+ return Err(
566
+ "Input file '" + input.filePath() +
567
+ "' uses a different container format than the first input.");
568
+ }
569
+
570
+ if (candidate->codec_type != base->codec_type || candidate->codec_id != base->codec_id) {
571
+ return Err("Input file '" + input.filePath() + "' uses a different audio codec.");
572
+ }
573
+
574
+ if (candidate->sample_rate != base->sample_rate) {
575
+ return Err("Input file '" + input.filePath() + "' uses a different sample rate.");
576
+ }
577
+
578
+ if (av_channel_layout_compare(&candidate->ch_layout, &base->ch_layout) != 0) {
579
+ return Err("Input file '" + input.filePath() + "' uses a different channel layout.");
580
+ }
581
+
582
+ if (candidate->format != base->format ||
583
+ candidate->bits_per_coded_sample != base->bits_per_coded_sample ||
584
+ candidate->bits_per_raw_sample != base->bits_per_raw_sample ||
585
+ candidate->profile != base->profile || candidate->level != base->level ||
586
+ candidate->block_align != base->block_align || candidate->frame_size != base->frame_size) {
587
+ return Err("Input file '" + input.filePath() + "' has incompatible audio parameters.");
588
+ }
589
+
590
+ if (candidate->extradata_size != base->extradata_size) {
591
+ return Err("Input file '" + input.filePath() + "' has incompatible codec extradata.");
592
+ }
593
+
594
+ if (candidate->extradata_size > 0 &&
595
+ std::memcmp(candidate->extradata, base->extradata, candidate->extradata_size) != 0) {
596
+ return Err("Input file '" + input.filePath() + "' has incompatible codec extradata.");
597
+ }
598
+
599
+ return Ok(input.filePath());
600
+ }
601
+
602
+ AudioFileConcatResult openAndValidateInputs(
603
+ const std::vector<std::string> &inputPaths,
604
+ std::vector<InputFormatContext> &inputs) {
605
+ inputs.reserve(inputPaths.size());
606
+
607
+ for (const auto &inputPath : inputPaths) {
608
+ auto inputResult = openInput(inputPath);
609
+ if (inputResult.is_err()) {
610
+ return Err(inputResult.unwrap_err());
611
+ }
612
+
613
+ inputs.emplace_back(std::move(inputResult).unwrap());
614
+
615
+ if (inputs.size() > 1) {
616
+ auto validationResult = validateCompatibleInput(inputs.back(), inputs.front());
617
+ if (validationResult.is_err()) {
618
+ return validationResult;
619
+ }
620
+ }
621
+ }
622
+
623
+ return Ok(std::string());
624
+ }
625
+
626
+ AudioFileConcatResult createOutput(
627
+ const std::string &outputPath,
628
+ const InputFormatContext &firstInput,
629
+ std::unique_ptr<OutputFormatContext> &output,
630
+ AVStream **outputStream) {
631
+ if (firstInput.context()->iformat->extensions != nullptr &&
632
+ av_match_ext(outputPath.c_str(), firstInput.context()->iformat->extensions) == 0) {
633
+ return Err("concatAudioFiles output path must use an extension compatible with the inputs.");
634
+ }
635
+
636
+ const char *muxerName = getMuxerNameForOutputPath(outputPath);
637
+ const AVOutputFormat *outputFormat = av_guess_format(muxerName, outputPath.c_str(), nullptr);
638
+ if (outputFormat == nullptr) {
639
+ return Err("Failed to determine output format for '" + outputPath + "'.");
640
+ }
641
+
642
+ AVFormatContext *rawOutputContext = nullptr;
643
+ int result = avformat_alloc_output_context2(
644
+ &rawOutputContext, outputFormat, muxerName, outputPath.c_str());
645
+
646
+ if (result < 0 || rawOutputContext == nullptr) {
647
+ return Err("Failed to allocate output context: " + parseFFmpegError(result));
648
+ }
649
+
650
+ output = std::make_unique<OutputFormatContext>(rawOutputContext);
651
+ *outputStream = avformat_new_stream(rawOutputContext, nullptr);
652
+
653
+ if (*outputStream == nullptr) {
654
+ return Err("Failed to create output audio stream.");
655
+ }
656
+
657
+ auto *inputStream = firstInput.audioStream();
658
+ result = avcodec_parameters_copy((*outputStream)->codecpar, inputStream->codecpar);
659
+ if (result < 0) {
660
+ return Err("Failed to copy audio stream parameters: " + parseFFmpegError(result));
661
+ }
662
+
663
+ (*outputStream)->codecpar->codec_tag = 0;
664
+ (*outputStream)->time_base = AVRational{.num = 1, .den = inputStream->codecpar->sample_rate};
665
+
666
+ if (!(rawOutputContext->oformat->flags & AVFMT_NOFILE)) {
667
+ result = avio_open(&rawOutputContext->pb, outputPath.c_str(), AVIO_FLAG_WRITE);
668
+ if (result < 0) {
669
+ return Err("Failed to open output file '" + outputPath + "': " + parseFFmpegError(result));
670
+ }
671
+ }
672
+
673
+ result = avformat_write_header(rawOutputContext, nullptr);
674
+ if (result < 0) {
675
+ return Err("Failed to write output header: " + parseFFmpegError(result));
676
+ }
677
+
678
+ return Ok(outputPath);
679
+ }
680
+
681
+ int64_t firstValidTimestamp(const AVPacket *packet) {
682
+ if (packet->dts != AV_NOPTS_VALUE) {
683
+ return packet->dts;
684
+ }
685
+ if (packet->pts != AV_NOPTS_VALUE) {
686
+ return packet->pts;
687
+ }
688
+ return 0;
689
+ }
690
+
691
+ int64_t packetEndTimestamp(const AVPacket *packet, AVRational outputTimeBase) {
692
+ int64_t endTimestamp = AV_NOPTS_VALUE;
693
+ if (packet->pts != AV_NOPTS_VALUE) {
694
+ endTimestamp = packet->pts;
695
+ }
696
+ if (packet->dts != AV_NOPTS_VALUE &&
697
+ (endTimestamp == AV_NOPTS_VALUE || packet->dts > endTimestamp)) {
698
+ endTimestamp = packet->dts;
699
+ }
700
+ if (endTimestamp == AV_NOPTS_VALUE) {
701
+ return AV_NOPTS_VALUE;
702
+ }
703
+ if (packet->duration > 0) {
704
+ endTimestamp += packet->duration;
705
+ }
706
+ return av_rescale_q(endTimestamp, outputTimeBase, outputTimeBase);
707
+ }
708
+
709
+ AudioFileConcatResult appendInputPackets(
710
+ InputFormatContext &input,
711
+ AVFormatContext *outputContext,
712
+ AVStream *outputStream,
713
+ int64_t &timestampOffset) {
714
+ auto *inputStream = input.audioStream();
715
+ const AVRational inputTimeBase = inputStream->time_base;
716
+ const AVRational outputTimeBase = outputStream->time_base;
717
+ AVPacket *packet = av_packet_alloc();
718
+
719
+ if (packet == nullptr) {
720
+ return Err("Failed to allocate FFmpeg packet.");
721
+ }
722
+
723
+ std::unique_ptr<AVPacket, void (*)(AVPacket *)> packetGuard(
724
+ packet, [](AVPacket *value) { av_packet_free(&value); });
725
+
726
+ int64_t baseTimestamp = AV_NOPTS_VALUE;
727
+ int64_t segmentEndTimestamp = timestampOffset;
728
+
729
+ while (true) {
730
+ int result = av_read_frame(input.context(), packet);
731
+ if (result == AVERROR_EOF) {
732
+ break;
733
+ }
734
+ if (result < 0) {
735
+ return Err(
736
+ "Failed to read packet from '" + input.filePath() + "': " + parseFFmpegError(result));
737
+ }
738
+
739
+ if (packet->stream_index != input.audioStreamIndex()) {
740
+ av_packet_unref(packet);
741
+ continue;
742
+ }
743
+
744
+ if (baseTimestamp == AV_NOPTS_VALUE) {
745
+ baseTimestamp = firstValidTimestamp(packet);
746
+ }
747
+
748
+ if (packet->pts != AV_NOPTS_VALUE) {
749
+ packet->pts = av_rescale_q(packet->pts - baseTimestamp, inputTimeBase, outputTimeBase) +
750
+ timestampOffset;
751
+ }
752
+ if (packet->dts != AV_NOPTS_VALUE) {
753
+ packet->dts = av_rescale_q(packet->dts - baseTimestamp, inputTimeBase, outputTimeBase) +
754
+ timestampOffset;
755
+ }
756
+ if (packet->duration > 0) {
757
+ packet->duration = av_rescale_q(packet->duration, inputTimeBase, outputTimeBase);
758
+ }
759
+ packet->pos = -1;
760
+ packet->stream_index = outputStream->index;
761
+
762
+ const int64_t packetEnd = packetEndTimestamp(packet, outputTimeBase);
763
+ if (packetEnd != AV_NOPTS_VALUE && packetEnd > segmentEndTimestamp) {
764
+ segmentEndTimestamp = packetEnd;
765
+ }
766
+
767
+ result = av_interleaved_write_frame(outputContext, packet);
768
+ if (result < 0) {
769
+ av_packet_unref(packet);
770
+ return Err(
771
+ "Failed to write packet from '" + input.filePath() + "': " + parseFFmpegError(result));
772
+ }
773
+ }
774
+
775
+ if (inputStream->duration != AV_NOPTS_VALUE && inputStream->duration > 0) {
776
+ const int64_t streamEnd =
777
+ timestampOffset + av_rescale_q(inputStream->duration, inputTimeBase, outputTimeBase);
778
+ segmentEndTimestamp = std::max(segmentEndTimestamp, streamEnd);
779
+ }
780
+
781
+ timestampOffset = segmentEndTimestamp;
782
+ return Ok(input.filePath());
783
+ }
784
+
785
+ AudioFileConcatResult concatAudioFilesWithFFmpeg(
786
+ const std::vector<std::string> &inputPaths,
787
+ const std::string &outputPath) {
788
+ std::vector<InputFormatContext> inputs;
789
+ auto inputValidationResult = openAndValidateInputs(inputPaths, inputs);
790
+ if (inputValidationResult.is_err()) {
791
+ return inputValidationResult;
792
+ }
793
+
794
+ std::unique_ptr<OutputFormatContext> output;
795
+ AVStream *outputStream = nullptr;
796
+ auto outputResult = createOutput(outputPath, inputs.front(), output, &outputStream);
797
+ if (outputResult.is_err()) {
798
+ return outputResult;
799
+ }
800
+
801
+ int64_t timestampOffset = 0;
802
+ for (auto &input : inputs) {
803
+ auto appendResult = appendInputPackets(input, output->get(), outputStream, timestampOffset);
804
+ if (appendResult.is_err()) {
805
+ return appendResult;
806
+ }
807
+ }
808
+
809
+ int result = av_write_trailer(output->get());
810
+ if (result < 0) {
811
+ return Err("Failed to write output trailer: " + parseFFmpegError(result));
812
+ }
813
+
814
+ return Ok(outputPath);
815
+ }
816
+ #endif // RN_AUDIO_API_FFMPEG_DISABLED
817
+
818
+ } // namespace
819
+
820
+ std::string normalizeFilePath(const std::string &path) {
821
+ if (path.starts_with(fileUrlPrefix)) {
822
+ return percentDecode(path.substr(std::strlen(fileUrlPrefix)));
823
+ }
824
+
825
+ return percentDecode(path);
826
+ }
827
+
828
+ AudioFileConcatResult concatAudioFiles(
829
+ const std::vector<std::string> &inputPaths,
830
+ const std::string &outputPath) {
831
+ std::vector<std::string> normalizedInputPaths;
832
+ normalizedInputPaths.reserve(inputPaths.size());
833
+ for (const auto &inputPath : inputPaths) {
834
+ normalizedInputPaths.push_back(normalizeFilePath(inputPath));
835
+ }
836
+
837
+ const std::string normalizedOutputPath = normalizeFilePath(outputPath);
838
+
839
+ auto pathValidationResult = validatePaths(normalizedInputPaths, normalizedOutputPath);
840
+ if (pathValidationResult.is_err()) {
841
+ return pathValidationResult;
842
+ }
843
+
844
+ if (isMiniaudioOutputPath(normalizedOutputPath)) {
845
+ return concatAudioFilesWithMiniAudio(normalizedInputPaths, normalizedOutputPath)
846
+ .map([&outputPath](const std::string &) { return outputPath; });
847
+ }
848
+
849
+ if (!isFFmpegOutputPath(normalizedOutputPath)) {
850
+ return Err(
851
+ "concatAudioFiles supports WAV output with miniaudio and M4A/MP4 output with FFmpeg.");
852
+ }
853
+
854
+ #if RN_AUDIO_API_FFMPEG_DISABLED
855
+ return Err("FFmpeg is disabled, cannot concatenate M4A/MP4 audio files.");
856
+ #else
857
+ return concatAudioFilesWithFFmpeg(normalizedInputPaths, normalizedOutputPath)
858
+ .map([&outputPath](const std::string &) { return outputPath; });
859
+ #endif // RN_AUDIO_API_FFMPEG_DISABLED
860
+ }
861
+
862
+ } // namespace audioapi