react-native-audio-api 0.12.1 → 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 (107) 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 +4 -3
  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/sources/AudioBufferBaseSourceNode.cpp +13 -11
  24. package/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.h +5 -9
  25. package/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp +47 -139
  26. package/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h +11 -8
  27. package/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp +29 -114
  28. package/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h +6 -8
  29. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp +9 -11
  30. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.h +1 -1
  31. package/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp +2 -2
  32. package/common/cpp/audioapi/core/types/AudioFormat.h +3 -1
  33. package/common/cpp/audioapi/core/utils/AudioDecoder.cpp +120 -91
  34. package/common/cpp/audioapi/core/utils/AudioDecoder.h +24 -101
  35. package/common/cpp/audioapi/core/utils/AudioFileConcatenator.cpp +862 -0
  36. package/common/cpp/audioapi/core/utils/AudioFileConcatenator.h +164 -0
  37. package/common/cpp/audioapi/core/utils/AudioFileWriter.cpp +2 -4
  38. package/common/cpp/audioapi/core/utils/AudioRecorderCallback.cpp +7 -12
  39. package/common/cpp/audioapi/core/utils/AudioRecorderCallback.h +2 -0
  40. package/common/cpp/audioapi/core/utils/buffer/BufferProcessingDirection.h +6 -0
  41. package/common/cpp/audioapi/core/utils/buffer/BufferProcessorBase.cpp +110 -0
  42. package/common/cpp/audioapi/core/utils/buffer/BufferProcessorBase.h +75 -0
  43. package/common/cpp/audioapi/core/utils/buffer/QueueBufferProcessor.cpp +129 -0
  44. package/common/cpp/audioapi/core/utils/buffer/QueueBufferProcessor.h +55 -0
  45. package/common/cpp/audioapi/core/utils/buffer/SingleBufferProcessor.cpp +95 -0
  46. package/common/cpp/audioapi/core/utils/buffer/SingleBufferProcessor.h +52 -0
  47. package/common/cpp/audioapi/events/AudioEventHandlerRegistry.cpp +65 -157
  48. package/common/cpp/audioapi/events/AudioEventHandlerRegistry.h +52 -33
  49. package/common/cpp/audioapi/events/AudioEventPayload.h +87 -0
  50. package/common/cpp/audioapi/events/IAudioEventHandlerRegistry.h +12 -12
  51. package/common/cpp/audioapi/libs/decoding/IncrementalAudioDecoder.h +12 -10
  52. package/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.cpp +152 -78
  53. package/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.h +6 -6
  54. package/common/cpp/audioapi/libs/miniaudio/MiniAudioDecoding.cpp +34 -20
  55. package/common/cpp/audioapi/libs/miniaudio/MiniAudioDecoding.h +4 -4
  56. package/common/cpp/audioapi/utils/AudioArray.hpp +6 -1
  57. package/common/cpp/audioapi/utils/CrossThreadEventScheduler.hpp +1 -1
  58. package/common/cpp/audioapi/utils/TaskOffloader.hpp +3 -5
  59. package/ios/audioapi/ios/AudioAPIModule.h +2 -1
  60. package/ios/audioapi/ios/AudioAPIModule.mm +4 -25
  61. package/ios/audioapi/ios/core/IOSAudioPlayer.h +16 -2
  62. package/ios/audioapi/ios/core/IOSAudioPlayer.mm +90 -24
  63. package/ios/audioapi/ios/core/IOSAudioRecorder.mm +1 -1
  64. package/ios/audioapi/ios/core/utils/IOSRecorderCallback.mm +64 -20
  65. package/ios/audioapi/ios/system/AudioSessionManager.mm +18 -7
  66. package/ios/audioapi/ios/system/SystemNotificationManager.mm +22 -22
  67. package/ios/audioapi/ios/system/notification/PlaybackNotification.mm +10 -13
  68. package/lib/commonjs/AudioAPIModule/AudioAPIModule.js +1 -1
  69. package/lib/commonjs/AudioAPIModule/AudioAPIModule.js.map +1 -1
  70. package/lib/commonjs/api.js +8 -0
  71. package/lib/commonjs/api.js.map +1 -1
  72. package/lib/commonjs/api.web.js +5 -0
  73. package/lib/commonjs/api.web.js.map +1 -1
  74. package/lib/commonjs/core/AudioFileUtils.js +35 -0
  75. package/lib/commonjs/core/AudioFileUtils.js.map +1 -0
  76. package/lib/commonjs/mock/index.js +15 -1
  77. package/lib/commonjs/mock/index.js.map +1 -1
  78. package/lib/module/AudioAPIModule/AudioAPIModule.js +1 -1
  79. package/lib/module/AudioAPIModule/AudioAPIModule.js.map +1 -1
  80. package/lib/module/api.js +1 -0
  81. package/lib/module/api.js.map +1 -1
  82. package/lib/module/api.web.js +3 -0
  83. package/lib/module/api.web.js.map +1 -1
  84. package/lib/module/core/AudioFileUtils.js +31 -0
  85. package/lib/module/core/AudioFileUtils.js.map +1 -0
  86. package/lib/module/mock/index.js +14 -1
  87. package/lib/module/mock/index.js.map +1 -1
  88. package/lib/typescript/AudioAPIModule/AudioAPIModule.d.ts.map +1 -1
  89. package/lib/typescript/api.d.ts +1 -0
  90. package/lib/typescript/api.d.ts.map +1 -1
  91. package/lib/typescript/api.web.d.ts +1 -0
  92. package/lib/typescript/api.web.d.ts.map +1 -1
  93. package/lib/typescript/core/AudioFileUtils.d.ts +2 -0
  94. package/lib/typescript/core/AudioFileUtils.d.ts.map +1 -0
  95. package/lib/typescript/interfaces.d.ts +3 -0
  96. package/lib/typescript/interfaces.d.ts.map +1 -1
  97. package/lib/typescript/mock/index.d.ts +3 -1
  98. package/lib/typescript/mock/index.d.ts.map +1 -1
  99. package/package.json +3 -3
  100. package/scripts/download-prebuilt-binaries.sh +34 -1
  101. package/src/AudioAPIModule/AudioAPIModule.ts +1 -0
  102. package/src/AudioAPIModule/globals.d.ts +3 -0
  103. package/src/api.ts +1 -0
  104. package/src/api.web.ts +7 -0
  105. package/src/core/AudioFileUtils.ts +49 -0
  106. package/src/interfaces.ts +7 -0
  107. package/src/mock/index.ts +29 -0
@@ -7,20 +7,31 @@
7
7
  #include <algorithm>
8
8
  #include <cmath>
9
9
  #include <cstddef>
10
+ #include <string>
10
11
  #include <vector>
11
12
 
12
13
  namespace audioapi::miniaudio_decoder {
13
14
 
14
15
  namespace {
15
16
 
17
+ std::string parseMiniAudioError(ma_result result) {
18
+ const char *description = ma_result_description(result);
19
+ if (description == nullptr) {
20
+ return "Unknown MiniAudio error (" + std::to_string(result) + ")";
21
+ }
22
+ return std::string(description) + " (" + std::to_string(result) + ")";
23
+ }
24
+
16
25
  ma_decoder_config makeDecoderConfig(const int outputSampleRate) {
17
26
  const ma_uint32 outRate =
18
27
  outputSampleRate > 0 ? static_cast<ma_uint32>(outputSampleRate) : 0;
19
28
  ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 0, outRate);
29
+ // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays, modernize-avoid-c-arrays)
20
30
  static ma_decoding_backend_vtable *customBackends[] = {
21
31
  ma_decoding_backend_libvorbis,
22
32
  ma_decoding_backend_libopus,
23
33
  };
34
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
24
35
  config.ppCustomBackendVTables = customBackends;
25
36
  config.customBackendCount =
26
37
  sizeof(customBackends) / sizeof(customBackends[0]);
@@ -79,12 +90,12 @@ float MiniAudioDecoder::getCurrentPositionInSeconds() const {
79
90
  static_cast<double>(outputSampleRate_));
80
91
  }
81
92
 
82
- bool MiniAudioDecoder::openFile(
93
+ decoding::DecoderResult MiniAudioDecoder::openFile(
83
94
  int outputSampleRate,
84
95
  const std::string &path) {
85
96
  close();
86
97
  if (path.empty()) {
87
- return false;
98
+ return Err("MiniAudioDecoder::openFile failed: path is empty");
88
99
  }
89
100
 
90
101
  ma_decoder_config config = makeDecoderConfig(outputSampleRate);
@@ -93,7 +104,7 @@ bool MiniAudioDecoder::openFile(
93
104
  ma_decoder_init_file(path.c_str(), &config, decoder_.get());
94
105
  if (result != MA_SUCCESS) {
95
106
  teardownDecoder();
96
- return false;
107
+ return Err("MiniAudioDecoder::openFile failed: " + parseMiniAudioError(result));
97
108
  }
98
109
 
99
110
  outputChannels_ = static_cast<int>(decoder_->outputChannels);
@@ -106,16 +117,16 @@ bool MiniAudioDecoder::openFile(
106
117
  totalLengthFrames_ = 0;
107
118
  }
108
119
  totalOutputFrames_ = 0;
109
- return true;
120
+ return Ok(None);
110
121
  }
111
122
 
112
- bool MiniAudioDecoder::openMemory(
123
+ decoding::DecoderResult MiniAudioDecoder::openMemory(
113
124
  int outputSampleRate,
114
125
  const void *data,
115
126
  size_t size) {
116
127
  close();
117
128
  if (data == nullptr || size == 0) {
118
- return false;
129
+ return Err("MiniAudioDecoder::openMemory failed: input data is empty");
119
130
  }
120
131
  memoryCopy_.assign(
121
132
  static_cast<const uint8_t *>(data),
@@ -130,7 +141,7 @@ bool MiniAudioDecoder::openMemory(
130
141
  decoder_.get());
131
142
  if (result != MA_SUCCESS) {
132
143
  teardownDecoder();
133
- return false;
144
+ return Err("MiniAudioDecoder::openMemory failed: " + parseMiniAudioError(result));
134
145
  }
135
146
 
136
147
  outputChannels_ = static_cast<int>(decoder_->outputChannels);
@@ -143,7 +154,7 @@ bool MiniAudioDecoder::openMemory(
143
154
  totalLengthFrames_ = 0;
144
155
  }
145
156
  totalOutputFrames_ = 0;
146
- return true;
157
+ return Ok(None);
147
158
  }
148
159
 
149
160
  size_t MiniAudioDecoder::readPcmFrames(float *outInterleaved, size_t frameCount) {
@@ -160,9 +171,9 @@ size_t MiniAudioDecoder::readPcmFrames(float *outInterleaved, size_t frameCount)
160
171
  return static_cast<size_t>(framesRead);
161
172
  }
162
173
 
163
- bool MiniAudioDecoder::seekToTime(double seconds) {
174
+ decoding::DecoderResult MiniAudioDecoder::seekToTime(double seconds) {
164
175
  if (!isOpen() || outputSampleRate_ <= 0) {
165
- return false;
176
+ return Err("MiniAudioDecoder::seekToTime failed: decoder is not open");
166
177
  }
167
178
  const float dur = getDurationInSeconds();
168
179
  if (dur > 0 && std::isfinite(dur)) {
@@ -170,17 +181,18 @@ bool MiniAudioDecoder::seekToTime(double seconds) {
170
181
  } else {
171
182
  seconds = std::max(0.0, seconds);
172
183
  if (!std::isfinite(seconds)) {
173
- return false;
184
+ return Err("MiniAudioDecoder::seekToTime failed: seconds is not finite");
174
185
  }
175
186
  }
176
187
 
177
188
  const ma_uint64 frame =
178
189
  static_cast<ma_uint64>(std::llround(seconds * static_cast<double>(outputSampleRate_)));
179
- if (ma_decoder_seek_to_pcm_frame(decoder_.get(), frame) != MA_SUCCESS) {
180
- return false;
190
+ const ma_result result = ma_decoder_seek_to_pcm_frame(decoder_.get(), frame);
191
+ if (result != MA_SUCCESS) {
192
+ return Err("MiniAudioDecoder::seekToTime failed: " + parseMiniAudioError(result));
181
193
  }
182
194
  totalOutputFrames_ = static_cast<size_t>(frame);
183
- return true;
195
+ return Ok(None);
184
196
  }
185
197
 
186
198
  namespace {
@@ -203,14 +215,15 @@ std::shared_ptr<AudioBuffer> buildAudioBufferFromInterleaved(
203
215
 
204
216
  std::shared_ptr<AudioBuffer> decodeWithFilePath(const std::string &path, int sample_rate) {
205
217
  MiniAudioDecoder dec;
206
- if (!dec.openFile(sample_rate, path)) {
218
+ const auto openResult = dec.openFile(sample_rate, path);
219
+ if (openResult.is_err()) {
207
220
  return nullptr;
208
221
  }
209
222
  const int ch = std::max(1, dec.outputChannels());
210
223
  std::vector<float> acc;
211
- std::vector<float> tmp(decoding::IIncrementalAudioDecoder::CHUNK_SIZE * static_cast<size_t>(ch));
224
+ std::vector<float> tmp(decoding::IncrementalAudioDecoder::CHUNK_SIZE * static_cast<size_t>(ch));
212
225
  while (true) {
213
- const size_t n = dec.readPcmFrames(tmp.data(), decoding::IIncrementalAudioDecoder::CHUNK_SIZE);
226
+ const size_t n = dec.readPcmFrames(tmp.data(), decoding::IncrementalAudioDecoder::CHUNK_SIZE);
214
227
  if (n == 0) {
215
228
  break;
216
229
  }
@@ -224,14 +237,15 @@ std::shared_ptr<AudioBuffer> decodeWithFilePath(const std::string &path, int sam
224
237
 
225
238
  std::shared_ptr<AudioBuffer> decodeWithMemoryBlock(const void *data, size_t size, int sample_rate) {
226
239
  MiniAudioDecoder dec;
227
- if (!dec.openMemory(sample_rate, data, size)) {
240
+ const auto openResult = dec.openMemory(sample_rate, data, size);
241
+ if (openResult.is_err()) {
228
242
  return nullptr;
229
243
  }
230
244
  const int ch = std::max(1, dec.outputChannels());
231
245
  std::vector<float> acc;
232
- std::vector<float> tmp(decoding::IIncrementalAudioDecoder::CHUNK_SIZE * static_cast<size_t>(ch));
246
+ std::vector<float> tmp(decoding::IncrementalAudioDecoder::CHUNK_SIZE * static_cast<size_t>(ch));
233
247
  while (true) {
234
- const size_t n = dec.readPcmFrames(tmp.data(), decoding::IIncrementalAudioDecoder::CHUNK_SIZE);
248
+ const size_t n = dec.readPcmFrames(tmp.data(), decoding::IncrementalAudioDecoder::CHUNK_SIZE);
235
249
  if (n == 0) {
236
250
  break;
237
251
  }
@@ -17,16 +17,16 @@ namespace audioapi::miniaudio_decoder {
17
17
  * MiniAudio-backed incremental decoder (Vorbis/Opus/WAV, etc. via ma_decoder + custom backends).
18
18
  * Same usage contract as ffmpegdecoder::FFmpegDecoder.
19
19
  */
20
- class MiniAudioDecoder : public decoding::IIncrementalAudioDecoder {
20
+ class MiniAudioDecoder : public decoding::IncrementalAudioDecoder {
21
21
  public:
22
22
  MiniAudioDecoder();
23
23
  ~MiniAudioDecoder() override;
24
24
  DELETE_COPY_AND_MOVE(MiniAudioDecoder);
25
25
 
26
- [[nodiscard]] bool openFile(
26
+ [[nodiscard]] decoding::DecoderResult openFile(
27
27
  int outputSampleRate,
28
28
  const std::string &path) override;
29
- [[nodiscard]] bool openMemory(
29
+ [[nodiscard]] decoding::DecoderResult openMemory(
30
30
  int outputSampleRate,
31
31
  const void *data,
32
32
  size_t size) override;
@@ -37,7 +37,7 @@ class MiniAudioDecoder : public decoding::IIncrementalAudioDecoder {
37
37
  [[nodiscard]] int outputSampleRate() const override;
38
38
  [[nodiscard]] float getDurationInSeconds() const override;
39
39
  [[nodiscard]] float getCurrentPositionInSeconds() const override;
40
- [[nodiscard]] bool seekToTime(double seconds) override;
40
+ [[nodiscard]] decoding::DecoderResult seekToTime(double seconds) override;
41
41
 
42
42
  private:
43
43
  void teardownDecoder();
@@ -195,13 +195,18 @@ class AlignedAudioArray {
195
195
 
196
196
  /// @brief Copies source array in reverse order into this array.
197
197
  /// @note Assumes source and this are in distinct, non-overlapping memory locations.
198
+ /// @note Precondition: length > 0. Reads source[sourceStart .. sourceStart-length+1].
198
199
  template <size_t OtherAlignment>
199
200
  void copyReverse(
200
201
  const AlignedAudioArray<OtherAlignment> &source,
201
202
  size_t sourceStart,
202
203
  size_t destinationStart,
203
204
  size_t length) {
204
- if (size_ - destinationStart < length || source.size_ - sourceStart < length) [[unlikely]] {
205
+ // Reverse read walks from sourceStart down to sourceStart-length+1, so the source
206
+ // precondition is sourceStart < source.size_ on the high end and sourceStart+1 >= length
207
+ // on the low end. Caller must also pass length > 0.
208
+ if (size_ - destinationStart < length || sourceStart + 1 < length ||
209
+ sourceStart >= source.size_) [[unlikely]] {
205
210
  throw std::out_of_range("Not enough space to copy to destination or from source.");
206
211
  }
207
212
 
@@ -23,7 +23,7 @@ using namespace channels::spsc;
23
23
  /// In this setup no locking happens and modifications can be seen by Audio thread.
24
24
  /// @note it is intended to be used for two threads one which schedules events and one which processes them
25
25
  /// @note it is not safe to be copied across two threads use std::shared_ptr if you need to share data
26
- template <typename T, int FunctionSize = 64>
26
+ template <typename T, int FunctionSize = 72>
27
27
  class CrossThreadEventScheduler {
28
28
  using EventType = FatFunction<FunctionSize, void(T &)>;
29
29
 
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <audioapi/utils/Macros.h>
3
4
  #include <audioapi/utils/SpscChannel.hpp>
4
5
  #include <cassert>
5
6
  #include <concepts>
@@ -16,7 +17,7 @@ namespace audioapi::task_offloader {
16
17
  template <std::default_initializable T, OverflowStrategy Strategy, WaitStrategy Wait>
17
18
  class TaskOffloader {
18
19
  public:
19
- template <typename Func>
20
+ template <std::invocable<T> Func>
20
21
  explicit TaskOffloader(size_t capacity, Func &&task) : shouldRun_(true) {
21
22
  auto [sender, receiver] = channels::spsc::channel<T, Strategy, Wait>(capacity);
22
23
  sender_ = std::move(sender);
@@ -33,10 +34,7 @@ class TaskOffloader {
33
34
  }
34
35
 
35
36
  // delete other functions
36
- TaskOffloader(const TaskOffloader &) = delete;
37
- TaskOffloader &operator=(const TaskOffloader &) = delete;
38
- TaskOffloader(TaskOffloader &&other) = delete;
39
- TaskOffloader &operator=(TaskOffloader &&other) = delete;
37
+ DELETE_COPY_AND_MOVE(TaskOffloader);
40
38
 
41
39
  ~TaskOffloader() {
42
40
  shouldRun_.store(false, std::memory_order_release);
@@ -8,6 +8,7 @@
8
8
 
9
9
  #import <React/RCTEventEmitter.h>
10
10
  #import <audioapi/events/AudioEvent.h>
11
+ #include <audioapi/events/AudioEventPayload.h>
11
12
 
12
13
  @class AudioEngine;
13
14
  @class SystemNotificationManager;
@@ -27,6 +28,6 @@
27
28
  @property (nonatomic, strong) NotificationRegistry *notificationRegistry;
28
29
 
29
30
  - (void)invokeHandlerWithEventName:(audioapi::AudioEvent)eventName
30
- eventBody:(NSDictionary *)eventBody;
31
+ payload:(audioapi::AudioEventPayload)payload;
31
32
 
32
33
  @end
@@ -318,34 +318,13 @@ RCT_EXPORT_METHOD(
318
318
  #endif // RCT_NEW_ARCH_ENABLED
319
319
 
320
320
  - (void)invokeHandlerWithEventName:(audioapi::AudioEvent)eventName
321
- eventBody:(NSDictionary *)eventBody
321
+ payload:(audioapi::AudioEventPayload)payload
322
322
  {
323
- std::unordered_map<std::string, EventValue> body = {};
324
-
325
- for (NSString *key in eventBody) {
326
- id value = eventBody[key];
327
- std::string stdKey = [key UTF8String];
328
-
329
- if ([value isKindOfClass:[NSString class]]) {
330
- std::string stdValue = [value UTF8String];
331
- body[stdKey] = EventValue(stdValue);
332
- } else if ([value isKindOfClass:[NSNumber class]]) {
333
- const char *type = [value objCType];
334
- if (strcmp(type, @encode(int)) == 0) {
335
- body[stdKey] = EventValue([value intValue]);
336
- } else if (strcmp(type, @encode(double)) == 0) {
337
- body[stdKey] = EventValue([value doubleValue]);
338
- } else if (strcmp(type, @encode(float)) == 0) {
339
- body[stdKey] = EventValue([value floatValue]);
340
- } else {
341
- body[stdKey] = EventValue([value boolValue]);
342
- }
343
- }
323
+ if (_eventHandler == nullptr) {
324
+ return;
344
325
  }
345
326
 
346
- if (_eventHandler != nullptr) {
347
- _eventHandler->invokeHandlerWithEventBody(eventName, body);
348
- }
327
+ _eventHandler->dispatchEvent(eventName, audioapi::kBroadcastListenerId, std::move(payload));
349
328
  }
350
329
 
351
330
  @end
@@ -4,11 +4,14 @@
4
4
  #import <NativeAudioPlayer.h>
5
5
  #else // when compiled as C++
6
6
  typedef struct objc_object NativeAudioPlayer;
7
+ typedef struct objc_object AudioBufferList;
7
8
  #endif // __OBJC__
8
9
 
9
10
  #include <audioapi/utils/AudioBuffer.hpp>
10
- #include <functional>
11
11
 
12
+ #include <atomic>
13
+ #include <cstddef>
14
+ #include <functional>
12
15
  namespace audioapi {
13
16
 
14
17
  class AudioContext;
@@ -29,12 +32,23 @@ class IOSAudioPlayer {
29
32
 
30
33
  bool isRunning() const;
31
34
 
32
- protected:
35
+ private:
36
+ void clearPendingSaved();
37
+ /// Audio-thread only. Always pulls the graph in steps of RENDER_QUANTUM_SIZE; if the system
38
+ /// buffer size is not a multiple of 128, the unused tail of the last quantum is kept (max 128
39
+ /// frames) and played at the start of the next callback.
40
+ void deliverOutputBuffers(AudioBufferList *outputData, int numFrames);
41
+
33
42
  std::shared_ptr<DSPAudioBuffer> audioBuffer_;
34
43
  NativeAudioPlayer *audioPlayer_;
35
44
  std::function<void(std::shared_ptr<DSPAudioBuffer>, int)> renderAudio_;
36
45
  int channelCount_;
37
46
  std::atomic<bool> isRunning_;
47
+ /// Set from main thread on start/resume; consumed on audio thread to drop stale pending audio.
48
+ std::atomic<bool> flushOverflowNextPull_{false};
49
+ /// Frames valid at the front of each `pendingSaved_[ch]` (0 … RENDER_QUANTUM_SIZE).
50
+ int pendingSavedCount_{0};
51
+ DSPAudioBuffer pendingSaved_;
38
52
  };
39
53
 
40
54
  } // namespace audioapi
@@ -1,10 +1,11 @@
1
1
  #import <AVFoundation/AVFoundation.h>
2
2
 
3
+ #include <algorithm>
4
+ #include <cstring>
5
+
3
6
  #include <audioapi/core/utils/Constants.h>
4
- #include <audioapi/dsp/VectorMath.h>
5
7
  #include <audioapi/ios/core/IOSAudioPlayer.h>
6
8
  #include <audioapi/ios/system/AudioEngine.h>
7
- #include <audioapi/utils/AudioArray.hpp>
8
9
  #include <audioapi/utils/AudioBuffer.hpp>
9
10
 
10
11
  namespace audioapi {
@@ -13,35 +14,20 @@ IOSAudioPlayer::IOSAudioPlayer(
13
14
  const std::function<void(std::shared_ptr<DSPAudioBuffer>, int)> &renderAudio,
14
15
  float sampleRate,
15
16
  int channelCount)
16
- : renderAudio_(renderAudio), channelCount_(channelCount), audioBuffer_(0), isRunning_(false)
17
+ : audioBuffer_(nullptr),
18
+ audioPlayer_(nullptr),
19
+ renderAudio_(renderAudio),
20
+ channelCount_(channelCount),
21
+ isRunning_(false),
22
+ pendingSaved_(RENDER_QUANTUM_SIZE, channelCount_, sampleRate)
17
23
  {
18
24
  RenderAudioBlock renderAudioBlock = ^(AudioBufferList *outputData, int numFrames) {
19
- int processedFrames = 0;
20
-
21
- while (processedFrames < numFrames) {
22
- int framesToProcess = std::min(numFrames - processedFrames, RENDER_QUANTUM_SIZE);
23
-
24
- if (isRunning_.load(std::memory_order_acquire)) {
25
- renderAudio_(audioBuffer_, framesToProcess);
26
- } else {
27
- audioBuffer_->zero();
28
- }
29
-
30
- for (size_t channel = 0; channel < channelCount_; channel += 1) {
31
- float *outputChannel = (float *)outputData->mBuffers[channel].mData;
32
-
33
- audioBuffer_->getChannel(channel)->copyTo(
34
- outputChannel, 0, processedFrames, framesToProcess);
35
- }
36
-
37
- processedFrames += framesToProcess;
38
- }
25
+ deliverOutputBuffers(outputData, numFrames);
39
26
  };
40
27
 
41
28
  audioPlayer_ = [[NativeAudioPlayer alloc] initWithRenderAudio:renderAudioBlock
42
29
  sampleRate:sampleRate
43
30
  channelCount:channelCount_];
44
-
45
31
  audioBuffer_ = std::make_shared<DSPAudioBuffer>(RENDER_QUANTUM_SIZE, channelCount_, sampleRate);
46
32
  }
47
33
 
@@ -50,6 +36,80 @@ IOSAudioPlayer::~IOSAudioPlayer()
50
36
  cleanup();
51
37
  }
52
38
 
39
+ void IOSAudioPlayer::clearPendingSaved()
40
+ {
41
+ pendingSavedCount_ = 0;
42
+ pendingSaved_.zero();
43
+ }
44
+
45
+ void IOSAudioPlayer::deliverOutputBuffers(AudioBufferList *outputData, int numFrames)
46
+ {
47
+ // If requested, clear any saved overflow before continuing normal rendering.
48
+ if (flushOverflowNextPull_.exchange(false, std::memory_order_acq_rel)) {
49
+ clearPendingSaved();
50
+ }
51
+
52
+ // if not running, set output to 0
53
+ if (!isRunning_.load(std::memory_order_acquire)) {
54
+ for (int channel = 0; channel < channelCount_; ++channel) {
55
+ auto *outputChannel = static_cast<float *>(outputData->mBuffers[channel].mData);
56
+ std::memset(outputChannel, 0, static_cast<size_t>(numFrames) * sizeof(float));
57
+ }
58
+ return;
59
+ }
60
+
61
+ int outPos = 0;
62
+ while (outPos < numFrames) {
63
+ const int need = numFrames - outPos;
64
+
65
+ if (pendingSavedCount_ > 0) {
66
+ const int fromPending = std::min(need, pendingSavedCount_);
67
+
68
+ // populate output with pendingSaved
69
+ for (int ch = 0; ch < channelCount_; ++ch) {
70
+ float *dst = static_cast<float *>(outputData->mBuffers[ch].mData) + outPos;
71
+ const float *src = pendingSaved_[ch].begin();
72
+ std::memcpy(dst, src, fromPending * sizeof(float));
73
+
74
+ // move the remaining samples to the beginning of the pendingSaved buffer
75
+ const int remain = pendingSavedCount_ - fromPending;
76
+ if (remain > 0) {
77
+ float *buf = pendingSaved_[ch].begin();
78
+ std::memmove(buf, buf + fromPending, remain * sizeof(float));
79
+ }
80
+ }
81
+
82
+ pendingSavedCount_ -= fromPending;
83
+ outPos += fromPending;
84
+ continue;
85
+ }
86
+
87
+ renderAudio_(audioBuffer_, RENDER_QUANTUM_SIZE);
88
+
89
+ // normal rendering - take RENDER_QUANTUM_SIZE frames from the graph and copy to output
90
+ const int stillNeed = numFrames - outPos;
91
+ if (stillNeed >= RENDER_QUANTUM_SIZE) {
92
+ for (int ch = 0; ch < channelCount_; ++ch) {
93
+ auto *src = (*audioBuffer_)[ch].begin();
94
+ float *dst = static_cast<float *>(outputData->mBuffers[ch].mData) + outPos;
95
+ std::memcpy(dst, src, RENDER_QUANTUM_SIZE * sizeof(float));
96
+ }
97
+ outPos += RENDER_QUANTUM_SIZE;
98
+ } else {
99
+ // when output will be sliced, copy the remaining frames to pendingSaved
100
+ const int tail = RENDER_QUANTUM_SIZE - stillNeed;
101
+ for (int ch = 0; ch < channelCount_; ++ch) {
102
+ auto *src = (*audioBuffer_)[ch].begin();
103
+ float *dst = static_cast<float *>(outputData->mBuffers[ch].mData) + outPos;
104
+ std::memcpy(dst, src, stillNeed * sizeof(float));
105
+ }
106
+ pendingSaved_.copy(*audioBuffer_, stillNeed, 0, tail);
107
+ pendingSavedCount_ = tail;
108
+ outPos += stillNeed;
109
+ }
110
+ }
111
+ }
112
+
53
113
  bool IOSAudioPlayer::start()
54
114
  {
55
115
  if (isRunning()) {
@@ -57,6 +117,9 @@ bool IOSAudioPlayer::start()
57
117
  }
58
118
 
59
119
  bool success = [audioPlayer_ start];
120
+ if (success) {
121
+ flushOverflowNextPull_.store(true, std::memory_order_release);
122
+ }
60
123
  isRunning_.store(success, std::memory_order_release);
61
124
  return success;
62
125
  }
@@ -74,6 +137,9 @@ bool IOSAudioPlayer::resume()
74
137
  }
75
138
 
76
139
  bool success = [audioPlayer_ resume];
140
+ if (success) {
141
+ flushOverflowNextPull_.store(true, std::memory_order_release);
142
+ }
77
143
  isRunning_.store(success, std::memory_order_release);
78
144
  return success;
79
145
  }
@@ -322,7 +322,7 @@ Result<std::tuple<std::vector<std::string>, double, double>, std::string> IOSAud
322
322
 
323
323
  if (hadCallback) {
324
324
  callbackOutputConfigured_.store(false, std::memory_order_release);
325
- dataCallback = std::move(dataCallback_);
325
+ dataCallback = dataCallback_;
326
326
  }
327
327
 
328
328
  if (hadConnection) {
@@ -11,6 +11,9 @@
11
11
  #include <audioapi/utils/CircularArray.hpp>
12
12
  #include <audioapi/utils/Result.hpp>
13
13
  #include <algorithm>
14
+ #include <cstdlib>
15
+ #include <cstring>
16
+ #include <memory>
14
17
  #include <utility>
15
18
 
16
19
  namespace audioapi {
@@ -32,17 +35,7 @@ IOSRecorderCallback::IOSRecorderCallback(
32
35
 
33
36
  IOSRecorderCallback::~IOSRecorderCallback()
34
37
  {
35
- @autoreleasepool {
36
- converter_ = nil;
37
- bufferFormat_ = nil;
38
- callbackFormat_ = nil;
39
- converterInputBuffer_ = nil;
40
- converterOutputBuffer_ = nil;
41
-
42
- for (size_t i = 0; i < channelCount_; ++i) {
43
- circularBuffer_[i]->zero();
44
- }
45
- }
38
+ cleanup();
46
39
  }
47
40
 
48
41
  /// @brief Prepares the IOSRecorderCallback for receiving audio data.
@@ -101,10 +94,14 @@ Result<NoneType, std::string> IOSRecorderCallback::prepare(
101
94
  }
102
95
 
103
96
  /// @brief Cleans up resources used by the IOSRecorderCallback.
104
- /// This method should be called from the JS thread only.
97
+ /// This method should be called from the JS GC thread only.
105
98
  void IOSRecorderCallback::cleanup()
106
99
  {
100
+ std::scoped_lock audioLock(destructionAudioGuard_);
107
101
  @autoreleasepool {
102
+ // join the worker
103
+ offloader_.reset();
104
+
108
105
  if (circularBuffer_[0]->getNumberOfAvailableFrames() > 0) {
109
106
  emitAudioData(true);
110
107
  }
@@ -115,43 +112,89 @@ void IOSRecorderCallback::cleanup()
115
112
  converterInputBuffer_ = nil;
116
113
  converterOutputBuffer_ = nil;
117
114
 
118
- for (size_t i = 0; i < channelCount_; ++i) {
115
+ for (int i = 0; i < channelCount_; ++i) {
119
116
  circularBuffer_[i]->zero();
120
117
  }
121
- offloader_.reset();
122
118
  }
123
119
  }
124
120
 
121
+ static inline void freeOwnedAudioBufferList(const AudioBufferList *bufferList)
122
+ {
123
+ if (bufferList == nullptr) {
124
+ return;
125
+ }
126
+ for (UInt32 i = 0; i < bufferList->mNumberBuffers; ++i) {
127
+ std::free(bufferList->mBuffers[i].mData);
128
+ }
129
+ std::free(const_cast<AudioBufferList *>(bufferList));
130
+ }
131
+
125
132
  /// @brief Receives audio data from the recorder, processes it, and stores it in the circular buffer.
126
133
  /// The data is converted using AVAudioConverter if the input format differs from the user desired callback format.
127
134
  /// This method runs on the audio thread.
128
135
  /// @param inputBuffer Pointer to the AudioBufferList containing the incoming audio data.
129
136
  /// @param numFrames Number of frames in the input buffer.
130
- void IOSRecorderCallback::receiveAudioData(const AudioBufferList *inputBuffer, int numFrames)
137
+ void IOSRecorderCallback::receiveAudioData(const AudioBufferList *audioBufferList, int numFrames)
131
138
  {
139
+ // if we wait here, we are in the middle of the destruction
140
+ std::scoped_lock lock(destructionAudioGuard_);
141
+ if (offloader_ == nullptr) {
142
+ return;
143
+ }
132
144
  if (!isInitialized_.load(std::memory_order_acquire)) {
133
145
  return;
134
146
  }
135
- offloader_->getSender()->send({inputBuffer, numFrames});
147
+
148
+ // CoreAudio owns `audioBufferList` only for the duration of this synchronous
149
+ // callback. Copy into an owned AudioBufferList before handing off to the
150
+ // worker thread; the consumer in taskOffloaderFunction frees it.
151
+ UInt32 bufferCount = audioBufferList->mNumberBuffers;
152
+ size_t headerSize = offsetof(AudioBufferList, mBuffers) + sizeof(AudioBuffer) * bufferCount;
153
+ AudioBufferList *owned = static_cast<AudioBufferList *>(std::malloc(headerSize));
154
+ if (owned == nullptr) {
155
+ return;
156
+ }
157
+ owned->mNumberBuffers = bufferCount;
158
+ for (UInt32 i = 0; i < bufferCount; ++i) {
159
+ UInt32 byteSize = audioBufferList->mBuffers[i].mDataByteSize;
160
+ owned->mBuffers[i].mNumberChannels = audioBufferList->mBuffers[i].mNumberChannels;
161
+ owned->mBuffers[i].mDataByteSize = byteSize;
162
+ void *channelData = std::malloc(byteSize);
163
+ if (channelData == nullptr) {
164
+ for (UInt32 j = 0; j < i; ++j) {
165
+ std::free(owned->mBuffers[j].mData);
166
+ }
167
+ std::free(owned);
168
+ return;
169
+ }
170
+ std::memcpy(channelData, audioBufferList->mBuffers[i].mData, byteSize);
171
+ owned->mBuffers[i].mData = channelData;
172
+ }
173
+ offloader_->getSender()->send({.audioBufferList = owned, .numFrames = numFrames});
136
174
  }
137
175
 
138
176
  void IOSRecorderCallback::taskOffloaderFunction(CallbackData data)
139
177
  {
140
178
  auto [inputBuffer, numFrames] = data;
141
- // dummy data to wake up thread after cleanup, skip processing it
142
- if (inputBuffer == nullptr)
179
+
180
+ // The TaskOffloader destructor sends a default-constructed CallbackData
181
+ // (audioBufferList == nullptr) to unblock the receiver; ignore it here.
182
+ if (inputBuffer == nullptr) {
143
183
  return;
184
+ }
185
+
144
186
  @autoreleasepool {
145
187
  NSError *error = nil;
146
188
 
147
189
  if (bufferFormat_.sampleRate == sampleRate_ && bufferFormat_.channelCount == channelCount_ &&
148
190
  !bufferFormat_.isInterleaved) {
149
191
  // Directly write to circular buffer
150
- for (size_t i = 0; i < channelCount_; ++i) {
192
+ for (int i = 0; i < channelCount_; ++i) {
151
193
  auto *data = static_cast<float *>(inputBuffer->mBuffers[i].mData);
152
194
  circularBuffer_[i]->push_back(data, numFrames);
153
195
  }
154
196
 
197
+ freeOwnedAudioBufferList(inputBuffer);
155
198
  inputBuffer = nullptr;
156
199
  if (circularBuffer_[0]->getNumberOfAvailableFrames() >= bufferLength_) {
157
200
  emitAudioData();
@@ -168,6 +211,7 @@ void IOSRecorderCallback::taskOffloaderFunction(CallbackData data)
168
211
  inputBuffer->mBuffers[i].mDataByteSize);
169
212
  }
170
213
 
214
+ freeOwnedAudioBufferList(inputBuffer);
171
215
  inputBuffer = nullptr;
172
216
  converterInputBuffer_.frameLength = numFrames;
173
217
 
@@ -195,7 +239,7 @@ void IOSRecorderCallback::taskOffloaderFunction(CallbackData data)
195
239
  return;
196
240
  }
197
241
 
198
- for (size_t i = 0; i < channelCount_; ++i) {
242
+ for (int i = 0; i < channelCount_; ++i) {
199
243
  auto *data = static_cast<float *>(converterOutputBuffer_.audioBufferList->mBuffers[i].mData);
200
244
  circularBuffer_[i]->push_back(data, outputFrameCount);
201
245
  }