react-native-audio-api 0.11.0-nightly-95f9c99-20251215 → 0.11.0-nightly-dd83923-20251216

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 (138) hide show
  1. package/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +382 -39
  2. package/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h +45 -18
  3. package/android/src/main/cpp/audioapi/android/core/NativeAudioRecorder.hpp +9 -9
  4. package/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.h +33 -0
  5. package/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.cpp +170 -0
  6. package/android/src/main/cpp/audioapi/android/core/utils/AndroidRecorderCallback.h +46 -0
  7. package/android/src/main/cpp/audioapi/android/core/utils/AudioDecoder.cpp +0 -1
  8. package/android/src/main/cpp/audioapi/android/core/utils/FileOptions.cpp +83 -0
  9. package/android/src/main/cpp/audioapi/android/core/utils/FileOptions.h +22 -0
  10. package/android/src/main/cpp/audioapi/android/core/utils/MiniaudioImplementation.cpp +8 -0
  11. package/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.cpp +493 -0
  12. package/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h +70 -0
  13. package/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/ptrs.hpp +56 -0
  14. package/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/utils.cpp +114 -0
  15. package/android/src/main/cpp/audioapi/android/core/utils/ffmpegBackend/utils.h +34 -0
  16. package/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.cpp +296 -0
  17. package/android/src/main/cpp/audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h +40 -0
  18. package/android/src/main/cpp/audioapi/android/system/NativeFileInfo.hpp +32 -0
  19. package/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +2 -0
  20. package/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt +7 -3
  21. package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +1 -0
  22. package/android/src/main/java/com/swmansion/audioapi/system/NativeFileInfo.kt +18 -0
  23. package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt +1 -0
  24. package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotificationReceiver.kt +2 -0
  25. package/android/src/oldarch/NativeAudioAPIModuleSpec.java +100 -80
  26. package/common/cpp/audioapi/AudioAPIModuleInstaller.h +3 -11
  27. package/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp +145 -16
  28. package/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.h +21 -6
  29. package/common/cpp/audioapi/core/inputs/AudioRecorder.cpp +43 -60
  30. package/common/cpp/audioapi/core/inputs/AudioRecorder.h +53 -33
  31. package/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp +42 -14
  32. package/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +12 -9
  33. package/common/cpp/audioapi/core/utils/AudioFileWriter.cpp +41 -0
  34. package/common/cpp/audioapi/core/utils/AudioFileWriter.h +44 -0
  35. package/common/cpp/audioapi/core/utils/AudioRecorderCallback.cpp +101 -0
  36. package/common/cpp/audioapi/core/utils/AudioRecorderCallback.h +52 -0
  37. package/common/cpp/audioapi/utils/AudioFileProperties.cpp +92 -0
  38. package/common/cpp/audioapi/utils/AudioFileProperties.h +76 -0
  39. package/common/cpp/audioapi/utils/Result.hpp +323 -0
  40. package/common/cpp/audioapi/utils/UnitConversion.h +9 -0
  41. package/ios/audioapi/ios/AudioAPIModule.mm +9 -14
  42. package/ios/audioapi/ios/core/IOSAudioPlayer.h +1 -1
  43. package/ios/audioapi/ios/core/IOSAudioPlayer.mm +7 -6
  44. package/ios/audioapi/ios/core/IOSAudioRecorder.h +39 -13
  45. package/ios/audioapi/ios/core/IOSAudioRecorder.mm +302 -26
  46. package/ios/audioapi/ios/core/NativeAudioPlayer.m +7 -11
  47. package/ios/audioapi/ios/core/NativeAudioRecorder.h +8 -9
  48. package/ios/audioapi/ios/core/NativeAudioRecorder.m +70 -76
  49. package/ios/audioapi/ios/core/utils/AudioDecoder.mm +1 -0
  50. package/ios/audioapi/ios/core/utils/FileOptions.h +33 -0
  51. package/ios/audioapi/ios/core/utils/FileOptions.mm +195 -0
  52. package/ios/audioapi/ios/core/utils/IOSFileWriter.h +53 -0
  53. package/ios/audioapi/ios/core/utils/IOSFileWriter.mm +239 -0
  54. package/ios/audioapi/ios/core/utils/IOSRecorderCallback.h +47 -0
  55. package/ios/audioapi/ios/core/utils/IOSRecorderCallback.mm +185 -0
  56. package/ios/audioapi/ios/system/AudioEngine.h +21 -16
  57. package/ios/audioapi/ios/system/AudioEngine.mm +138 -130
  58. package/ios/audioapi/ios/system/AudioSessionManager.h +19 -9
  59. package/ios/audioapi/ios/system/AudioSessionManager.mm +250 -215
  60. package/ios/audioapi/ios/system/NotificationManager.mm +24 -42
  61. package/lib/commonjs/api.js +82 -109
  62. package/lib/commonjs/api.js.map +1 -1
  63. package/lib/commonjs/core/AudioRecorder.js +159 -13
  64. package/lib/commonjs/core/AudioRecorder.js.map +1 -1
  65. package/lib/commonjs/specs/NativeAudioAPIModule.js.map +1 -1
  66. package/lib/commonjs/system/notification/PlaybackNotificationManager.js +17 -14
  67. package/lib/commonjs/system/notification/PlaybackNotificationManager.js.map +1 -1
  68. package/lib/commonjs/system/notification/RecordingNotificationManager.js +22 -19
  69. package/lib/commonjs/system/notification/RecordingNotificationManager.js.map +1 -1
  70. package/lib/commonjs/system/notification/SimpleNotificationManager.js +16 -13
  71. package/lib/commonjs/system/notification/SimpleNotificationManager.js.map +1 -1
  72. package/lib/commonjs/types.js +39 -0
  73. package/lib/commonjs/types.js.map +1 -1
  74. package/lib/commonjs/utils/filePresets.js +43 -0
  75. package/lib/commonjs/utils/filePresets.js.map +1 -0
  76. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js +6 -3
  77. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js.map +1 -1
  78. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js +6 -3
  79. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js.map +1 -1
  80. package/lib/module/api.js +5 -4
  81. package/lib/module/api.js.map +1 -1
  82. package/lib/module/core/AudioRecorder.js +159 -13
  83. package/lib/module/core/AudioRecorder.js.map +1 -1
  84. package/lib/module/specs/NativeAudioAPIModule.js.map +1 -1
  85. package/lib/module/system/notification/PlaybackNotificationManager.js +17 -14
  86. package/lib/module/system/notification/PlaybackNotificationManager.js.map +1 -1
  87. package/lib/module/system/notification/RecordingNotificationManager.js +22 -19
  88. package/lib/module/system/notification/RecordingNotificationManager.js.map +1 -1
  89. package/lib/module/system/notification/SimpleNotificationManager.js +16 -13
  90. package/lib/module/system/notification/SimpleNotificationManager.js.map +1 -1
  91. package/lib/module/types.js +38 -1
  92. package/lib/module/types.js.map +1 -1
  93. package/lib/module/utils/filePresets.js +39 -0
  94. package/lib/module/utils/filePresets.js.map +1 -0
  95. package/lib/module/web-system/notification/PlaybackNotificationManager.js +6 -3
  96. package/lib/module/web-system/notification/PlaybackNotificationManager.js.map +1 -1
  97. package/lib/module/web-system/notification/RecordingNotificationManager.js +6 -3
  98. package/lib/module/web-system/notification/RecordingNotificationManager.js.map +1 -1
  99. package/lib/typescript/api.d.ts +5 -4
  100. package/lib/typescript/api.d.ts.map +1 -1
  101. package/lib/typescript/core/AudioRecorder.d.ts +69 -7
  102. package/lib/typescript/core/AudioRecorder.d.ts.map +1 -1
  103. package/lib/typescript/events/types.d.ts +36 -2
  104. package/lib/typescript/events/types.d.ts.map +1 -1
  105. package/lib/typescript/interfaces.d.ts +24 -4
  106. package/lib/typescript/interfaces.d.ts.map +1 -1
  107. package/lib/typescript/specs/NativeAudioAPIModule.d.ts +1 -1
  108. package/lib/typescript/specs/NativeAudioAPIModule.d.ts.map +1 -1
  109. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts +4 -3
  110. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts.map +1 -1
  111. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts +4 -3
  112. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts.map +1 -1
  113. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts +3 -2
  114. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts.map +1 -1
  115. package/lib/typescript/system/notification/types.d.ts.map +1 -1
  116. package/lib/typescript/types.d.ts +79 -3
  117. package/lib/typescript/types.d.ts.map +1 -1
  118. package/lib/typescript/utils/filePresets.d.ts +9 -0
  119. package/lib/typescript/utils/filePresets.d.ts.map +1 -0
  120. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts +4 -3
  121. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts.map +1 -1
  122. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts +4 -3
  123. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts.map +1 -1
  124. package/package.json +4 -4
  125. package/src/AudioAPIModule/globals.d.ts +1 -2
  126. package/src/api.ts +8 -29
  127. package/src/core/AudioRecorder.ts +195 -23
  128. package/src/events/types.ts +40 -2
  129. package/src/interfaces.ts +34 -5
  130. package/src/specs/NativeAudioAPIModule.ts +2 -2
  131. package/src/system/notification/PlaybackNotificationManager.ts +20 -16
  132. package/src/system/notification/RecordingNotificationManager.ts +26 -21
  133. package/src/system/notification/SimpleNotificationManager.ts +18 -13
  134. package/src/system/notification/types.ts +1 -0
  135. package/src/types.ts +89 -3
  136. package/src/utils/filePresets.ts +47 -0
  137. package/src/web-system/notification/PlaybackNotificationManager.ts +9 -5
  138. package/src/web-system/notification/RecordingNotificationManager.ts +9 -5
@@ -1,70 +1,346 @@
1
1
  #import <AVFoundation/AVFoundation.h>
2
+ #import <AudioEngine.h>
3
+ #import <AudioSessionManager.h>
4
+ #import <Foundation/Foundation.h>
2
5
 
6
+ #include <unordered_map>
7
+
8
+ #include <audioapi/core/sources/RecorderAdapterNode.h>
9
+ #include <audioapi/core/utils/AudioFileWriter.h>
3
10
  #include <audioapi/core/utils/Constants.h>
11
+ #include <audioapi/core/utils/Locker.h>
4
12
  #include <audioapi/dsp/VectorMath.h>
5
13
  #include <audioapi/events/AudioEventHandlerRegistry.h>
6
14
  #include <audioapi/ios/core/IOSAudioRecorder.h>
15
+ #include <audioapi/ios/core/utils/IOSFileWriter.h>
16
+ #include <audioapi/ios/core/utils/IOSRecorderCallback.h>
17
+ #include <audioapi/ios/system/AudioEngine.h>
7
18
  #include <audioapi/utils/AudioArray.h>
8
19
  #include <audioapi/utils/AudioBus.h>
20
+ #include <audioapi/utils/AudioFileProperties.h>
9
21
  #include <audioapi/utils/CircularAudioArray.h>
10
22
  #include <audioapi/utils/CircularOverflowableAudioArray.h>
11
- #include <unordered_map>
23
+ #include <audioapi/utils/Result.hpp>
12
24
 
13
25
  namespace audioapi {
14
26
 
27
+ /// @brief Constructs an IOSAudioRecorder instance.
28
+ /// This constructor initializes the receiver block and native side recorder wrapper (AVAudioSinkNode).
29
+ /// All other necessary fields (like buffers) are initialized in start() method.
30
+ /// This "method" should be called from the JS thread only.
31
+ /// @param audioEventHandlerRegistry Shared pointer to the AudioEventHandlerRegistry for event handling.
15
32
  IOSAudioRecorder::IOSAudioRecorder(
16
- float sampleRate,
17
- int bufferLength,
18
33
  const std::shared_ptr<AudioEventHandlerRegistry> &audioEventHandlerRegistry)
19
- : AudioRecorder(sampleRate, bufferLength, audioEventHandlerRegistry)
34
+ : AudioRecorder(audioEventHandlerRegistry)
20
35
  {
21
- AudioReceiverBlock audioReceiverBlock = ^(const AudioBufferList *inputBuffer, int numFrames) {
22
- if (isRunning_.load()) {
23
- auto *inputChannel = static_cast<float *>(inputBuffer->mBuffers[0].mData);
24
- writeToBuffers(inputChannel, numFrames);
36
+ AudioReceiverBlock receiverBlock = ^(const AudioBufferList *inputBuffer, int numFrames) {
37
+ if (usesFileOutput()) {
38
+ if (auto lock = Locker::tryLock(fileWriterMutex_)) {
39
+ std::static_pointer_cast<IOSFileWriter>(fileWriter_)
40
+ ->writeAudioData(inputBuffer, numFrames);
41
+ }
25
42
  }
26
43
 
27
- while (circularBuffer_->getNumberOfAvailableFrames() >= bufferLength_) {
28
- auto bus = std::make_shared<AudioBus>(bufferLength_, 1, sampleRate_);
29
- auto *outputChannel = bus->getChannel(0)->getData();
44
+ if (usesCallback()) {
45
+ if (auto lock = Locker::tryLock(callbackMutex_)) {
46
+ std::static_pointer_cast<IOSRecorderCallback>(dataCallback_)
47
+ ->receiveAudioData(inputBuffer, numFrames);
48
+ }
49
+ }
30
50
 
31
- circularBuffer_->pop_front(outputChannel, bufferLength_);
51
+ if (isConnected()) {
52
+ if (auto lock = Locker::tryLock(adapterNodeMutex_)) {
53
+ for (size_t channel = 0; channel < adapterNode_->channelCount_; ++channel) {
54
+ float *channelData = (float *)inputBuffer->mBuffers[channel].mData;
32
55
 
33
- invokeOnAudioReadyCallback(bus, bufferLength_);
56
+ adapterNode_->buff_[channel]->write(channelData, numFrames);
57
+ }
58
+ }
34
59
  }
35
60
  };
36
61
 
37
- audioRecorder_ = [[NativeAudioRecorder alloc] initWithReceiverBlock:audioReceiverBlock
38
- bufferLength:bufferLength
39
- sampleRate:sampleRate];
62
+ nativeRecorder_ = [[NativeAudioRecorder alloc] initWithReceiverBlock:receiverBlock];
40
63
  }
41
64
 
42
65
  IOSAudioRecorder::~IOSAudioRecorder()
43
66
  {
44
67
  stop();
45
- [audioRecorder_ cleanup];
68
+ [nativeRecorder_ cleanup];
69
+ }
70
+
71
+ /// @brief Starts the audio recording process and prepares necessary resources.
72
+ /// This method should be called from the JS thread only.
73
+ /// @returns Result containing the file path if recording started successfully, or an error message.
74
+ Result<std::string, std::string> IOSAudioRecorder::start()
75
+ {
76
+ if (isRecording()) {
77
+ return Result<std::string, std::string>::Err("Already recording");
78
+ }
79
+
80
+ std::scoped_lock startLock(callbackMutex_, fileWriterMutex_, adapterNodeMutex_);
81
+ AudioSessionManager *audioSessionManager = [AudioSessionManager sharedInstance];
82
+
83
+ if ([[audioSessionManager checkRecordingPermissions] isEqual:@"Denied"]) {
84
+ return Result<std::string, std::string>::Err("Microphone permissions are not granted");
85
+ }
86
+
87
+ // TODO: recorder should probably request the options if not set by user
88
+ // but lets handle that in another PR
89
+ if (![audioSessionManager isSessionActive]) {
90
+ return Result<std::string, std::string>::Err("Audio session is not active");
91
+ }
92
+
93
+ // TODO: this is a bit ugly, and could be written slightly better
94
+ // we need to stop the audio engine if it's running, to be able to get
95
+ // proper input format values, otherwise the system my zero out the sample rate or channel count
96
+ // if input wasn't used yet
97
+ // (especially on simulators)
98
+ // Engine will be started again once the native recorder starts
99
+ [AudioEngine.sharedInstance stopIfNecessary];
100
+
101
+ // Estimate the maximum input buffer lengths that can be expected from the sink node
102
+ size_t maxInputBufferLength = [nativeRecorder_ getBufferSize];
103
+ auto inputFormat = [nativeRecorder_ getInputFormat];
104
+
105
+ if (usesFileOutput()) {
106
+ auto fileResult = std::static_pointer_cast<IOSFileWriter>(fileWriter_)
107
+ ->openFile(inputFormat, maxInputBufferLength);
108
+
109
+ if (fileResult.is_err()) {
110
+ return Result<std::string, std::string>::Err(
111
+ "Failed to open file for writing: " + fileResult.unwrap_err());
112
+ }
113
+
114
+ filePath_ = fileResult.unwrap();
115
+ }
116
+
117
+ if (usesCallback()) {
118
+ auto callbackResult = std::static_pointer_cast<IOSRecorderCallback>(dataCallback_)
119
+ ->prepare(inputFormat, maxInputBufferLength);
120
+
121
+ if (callbackResult.is_err()) {
122
+ return Result<std::string, std::string>::Err(
123
+ "Failed to prepare callback: " + callbackResult.unwrap_err());
124
+ }
125
+ }
126
+
127
+ if (isConnected()) {
128
+ // TODO: pass sample rate, in case conversion is necessary
129
+ adapterNode_->init(maxInputBufferLength, inputFormat.channelCount);
130
+ }
131
+
132
+ [nativeRecorder_ start];
133
+ state_.store(RecorderState::Recording, std::memory_order_release);
134
+ return Result<std::string, std::string>::Ok(filePath_);
135
+ }
136
+
137
+ /// @brief Stops the audio recording process and releases resources.
138
+ /// It finalizes any data receiver and closes the stream.
139
+ /// This method should be called from the JS thread only.
140
+ /// @returns Result containing a tuple of the output file path, size, and duration if stopped successfully, or an error message.
141
+ Result<std::tuple<std::string, double, double>, std::string> IOSAudioRecorder::stop()
142
+ {
143
+ std::scoped_lock stopLock(callbackMutex_, fileWriterMutex_, adapterNodeMutex_);
144
+
145
+ std::string filePath = filePath_;
146
+ double outputFileSize = 0;
147
+ double outputDuration = 0;
148
+
149
+ if (isIdle()) {
150
+ return Result<std::tuple<std::string, double, double>, std::string>::Err("Not recording");
151
+ }
152
+
153
+ state_.store(RecorderState::Idle, std::memory_order_release);
154
+ [nativeRecorder_ stop];
155
+
156
+ if (usesFileOutput()) {
157
+ auto fileResult = fileWriter_->closeFile();
158
+
159
+ if (fileResult.is_err()) {
160
+ return Result<std::tuple<std::string, double, double>, std::string>::Err(
161
+ "Failed to close file: " + fileResult.unwrap_err());
162
+ }
163
+
164
+ outputFileSize = std::get<0>(fileResult.unwrap());
165
+ outputDuration = std::get<1>(fileResult.unwrap());
166
+ }
167
+
168
+ if (usesCallback()) {
169
+ dataCallback_->cleanup();
170
+ }
171
+
172
+ if (isConnected()) {
173
+ adapterNode_->cleanup();
174
+ }
175
+
176
+ filePath_ = "";
177
+ return Result<std::tuple<std::string, double, double>, std::string>::Ok(
178
+ std::make_tuple(filePath, outputFileSize, outputDuration));
179
+ }
180
+
181
+ /// @brief Enables file output for the recorder with specified properties.
182
+ /// If the recorder is already active, it will open the file for writing immediately.
183
+ /// This method should be called from the JS thread only.
184
+ /// @param properties Shared pointer to AudioFileProperties defining the output file format.
185
+ /// @returns Result containing the output file path if enabled successfully, or an error message.
186
+ Result<std::string, std::string> IOSAudioRecorder::enableFileOutput(
187
+ std::shared_ptr<AudioFileProperties> properties)
188
+ {
189
+ std::scoped_lock lock(fileWriterMutex_, errorCallbackMutex_);
190
+ fileWriter_ = std::make_shared<IOSFileWriter>(audioEventHandlerRegistry_, properties);
191
+
192
+ if (!isIdle()) {
193
+ auto result = std::static_pointer_cast<IOSFileWriter>(fileWriter_)
194
+ ->openFile([nativeRecorder_ getInputFormat], [nativeRecorder_ getBufferSize]);
195
+
196
+ if (result.is_err()) {
197
+ return Result<std::string, std::string>::Err(
198
+ "Failed to open file for writing: " + result.unwrap_err());
199
+ }
200
+
201
+ filePath_ = result.unwrap();
202
+ }
203
+
204
+ fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire));
205
+
206
+ fileOutputEnabled_.store(true, std::memory_order_release);
207
+ return Result<std::string, std::string>::Ok(filePath_);
208
+ }
209
+
210
+ void IOSAudioRecorder::disableFileOutput()
211
+ {
212
+ std::scoped_lock lock(fileWriterMutex_);
213
+ fileOutputEnabled_.store(false, std::memory_order_release);
214
+ fileWriter_ = nullptr;
46
215
  }
47
216
 
48
- void IOSAudioRecorder::start()
217
+ /// @brief Connects a RecorderAdapterNode to the recorder for audio data routing.
218
+ /// If the recorder is already active, it will initialize the adapter node immediately.
219
+ /// This method should be called from the JS thread only.
220
+ /// @param node Shared pointer to the RecorderAdapterNode to connect.
221
+ void IOSAudioRecorder::connect(const std::shared_ptr<RecorderAdapterNode> &node)
49
222
  {
50
- if (isRunning_.load()) {
223
+ std::scoped_lock lock(adapterNodeMutex_);
224
+ adapterNode_ = node;
225
+
226
+ if (!isIdle()) {
227
+ adapterNode_->init(
228
+ [nativeRecorder_ getBufferSize], [nativeRecorder_ getInputFormat].channelCount);
229
+ }
230
+
231
+ isConnected_.store(true, std::memory_order_release);
232
+ }
233
+
234
+ /// @brief Disconnects the currently connected RecorderAdapterNode from the recorder.
235
+ /// If the recorder is currently active, it will stop routing audio data immediately.
236
+ /// This method should be called from the JS thread only.
237
+ void IOSAudioRecorder::disconnect()
238
+ {
239
+ std::scoped_lock lock(adapterNodeMutex_);
240
+ adapterNode_ = nullptr;
241
+ isConnected_.store(false, std::memory_order_release);
242
+ }
243
+
244
+ void IOSAudioRecorder::pause()
245
+ {
246
+ if (!isRecording()) {
51
247
  return;
52
248
  }
53
249
 
54
- [audioRecorder_ start];
55
- isRunning_.store(true);
250
+ [nativeRecorder_ pause];
251
+ state_.store(RecorderState::Paused, std::memory_order_release);
56
252
  }
57
253
 
58
- void IOSAudioRecorder::stop()
254
+ void IOSAudioRecorder::resume()
59
255
  {
60
- if (!isRunning_.load()) {
256
+ if (!isPaused()) {
61
257
  return;
62
258
  }
63
259
 
64
- isRunning_.store(false);
65
- [audioRecorder_ stop];
260
+ [nativeRecorder_ resume];
261
+ state_.store(RecorderState::Recording, std::memory_order_release);
262
+ }
263
+
264
+ /// @brief Checks if the recorder is currently recording.
265
+ /// Besides recorder internal state, it also check if the audio engine is running.
266
+ /// this helps with restarts after interruptions or other audio session changes.
267
+ /// This method can be called from any thread.
268
+ /// @returns True if recording, false otherwise.
269
+ bool IOSAudioRecorder::isRecording() const
270
+ {
271
+ AudioEngine *audioEngine = [AudioEngine sharedInstance];
272
+ return state_.load(std::memory_order_acquire) == RecorderState::Recording &&
273
+ [audioEngine getState] == AudioEngineState::AudioEngineStateRunning;
274
+ }
275
+
276
+ /// @brief Checks if the recorder is currently paused.
277
+ /// Besides recorder internal state, it also check if the audio engine is running.
278
+ /// this helps with restarts after interruptions or other audio session changes.
279
+ /// This method can be called from any thread.
280
+ /// @returns True if paused, false otherwise.
281
+ bool IOSAudioRecorder::isPaused() const
282
+ {
283
+ AudioEngine *audioEngine = [AudioEngine sharedInstance];
284
+ auto currentState = state_.load(std::memory_order_acquire);
285
+
286
+ if (currentState == RecorderState::Idle) {
287
+ return false;
288
+ }
289
+
290
+ return currentState == RecorderState::Paused ||
291
+ [audioEngine getState] != AudioEngineState::AudioEngineStateRunning;
292
+ }
293
+
294
+ /// @brief Checks if the recorder is currently idle (not recording or paused).
295
+ /// This method can be called from any thread.
296
+ /// @returns True if idle, false otherwise.
297
+ bool IOSAudioRecorder::isIdle() const
298
+ {
299
+ return state_.load(std::memory_order_acquire) == RecorderState::Idle;
300
+ }
66
301
 
67
- sendRemainingData();
302
+ /// @brief Sets the callback to be invoked when audio data is ready.
303
+ /// If the recorder is already active, it will prepare the callback for receiving audio data immediately.
304
+ /// This method should be called from the JS thread only.
305
+ /// @param sampleRate Desired sample rate for the callback audio data.
306
+ /// @param bufferLength Desired buffer length in frames for the callback audio data.
307
+ /// @param channelCount Number of channels for the callback audio data.
308
+ /// @param callbackId Identifier for the JS callback to be invoked.
309
+ /// @returns Success status or Error status with message.
310
+ Result<NoneType, std::string> IOSAudioRecorder::setOnAudioReadyCallback(
311
+ float sampleRate,
312
+ size_t bufferLength,
313
+ int channelCount,
314
+ uint64_t callbackId)
315
+ {
316
+ std::scoped_lock lock(callbackMutex_, errorCallbackMutex_);
317
+
318
+ dataCallback_ = std::make_shared<IOSRecorderCallback>(
319
+ audioEventHandlerRegistry_, sampleRate, bufferLength, channelCount, callbackId);
320
+
321
+ if (!isIdle()) {
322
+ auto result = std::static_pointer_cast<IOSRecorderCallback>(dataCallback_)
323
+ ->prepare([nativeRecorder_ getInputFormat], [nativeRecorder_ getBufferSize]);
324
+
325
+ if (result.is_err()) {
326
+ return Result<NoneType, std::string>::Err(result.unwrap_err());
327
+ }
328
+ }
329
+
330
+ dataCallback_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire));
331
+
332
+ callbackOutputEnabled_.store(true, std::memory_order_release);
333
+ return Result<NoneType, std::string>::Ok(None);
334
+ }
335
+
336
+ /// @brief Clears the audio data callback.
337
+ /// If the recorder is currently active, it will stop invoking the callback immediately.
338
+ /// This method should be called from the JS thread only.
339
+ void IOSAudioRecorder::clearOnAudioReadyCallback()
340
+ {
341
+ std::scoped_lock lock(callbackMutex_);
342
+ callbackOutputEnabled_.store(false, std::memory_order_release);
343
+ dataCallback_ = nullptr;
68
344
  }
69
345
 
70
346
  } // namespace audioapi
@@ -39,8 +39,6 @@
39
39
 
40
40
  - (bool)start
41
41
  {
42
- NSLog(@"[AudioPlayer] start");
43
-
44
42
  AudioEngine *audioEngine = [AudioEngine sharedInstance];
45
43
  assert(audioEngine != nil);
46
44
 
@@ -50,28 +48,25 @@
50
48
  // break rules of at runtime modifications from docs
51
49
  // https://developer.apple.com/documentation/avfaudio/avaudioengine?language=objc
52
50
  //
53
- // Currently we are restarting because we do not see any significant
54
- // performance issue and case when you will need to start and stop player very
55
- // frequently
56
- [audioEngine stopEngine];
51
+ // Currently we are restarting because we do not see any significant performance issue and case when
52
+ // you will need to start and stop player very frequently
53
+ [audioEngine stopIfNecessary];
57
54
  self.sourceNodeId = [audioEngine attachSourceNode:self.sourceNode format:self.format];
58
55
  return [audioEngine startIfNecessary];
59
56
  }
60
57
 
61
58
  - (void)stop
62
59
  {
63
- NSLog(@"[AudioPlayer] stop");
64
-
65
60
  AudioEngine *audioEngine = [AudioEngine sharedInstance];
66
61
  assert(audioEngine != nil);
62
+
67
63
  [audioEngine detachSourceNodeWithId:self.sourceNodeId];
68
- [audioEngine stopIfNecessary];
64
+ [audioEngine stopIfPossible];
69
65
  self.sourceNodeId = nil;
70
66
  }
71
67
 
72
68
  - (bool)resume
73
69
  {
74
- NSLog(@"[AudioPlayer] resume");
75
70
  AudioEngine *audioEngine = [AudioEngine sharedInstance];
76
71
  assert(audioEngine != nil);
77
72
 
@@ -82,7 +77,8 @@
82
77
  {
83
78
  AudioEngine *audioEngine = [AudioEngine sharedInstance];
84
79
  assert(audioEngine != nil);
85
- [audioEngine pauseEngine:self.sourceNodeId];
80
+
81
+ [audioEngine pauseIfNecessary];
86
82
  }
87
83
 
88
84
  - (void)cleanup
@@ -7,25 +7,24 @@ typedef void (^AudioReceiverBlock)(const AudioBufferList *inputBuffer, int numFr
7
7
 
8
8
  @interface NativeAudioRecorder : NSObject
9
9
 
10
- @property (nonatomic, assign) int bufferLength;
11
- @property (nonatomic, assign) float sampleRate;
12
-
13
10
  @property (nonatomic, strong) AVAudioSinkNode *sinkNode;
14
11
  @property (nonatomic, copy) AVAudioSinkNodeReceiverBlock receiverSinkBlock;
15
12
  @property (nonatomic, copy) AudioReceiverBlock receiverBlock;
16
13
 
17
- @property (nonatomic, strong) AVAudioConverter *audioConverter;
18
- @property (nonatomic, strong) AVAudioFormat *inputFormat;
19
- @property (nonatomic, strong) AVAudioFormat *outputFormat;
14
+ - (instancetype)initWithReceiverBlock:(AudioReceiverBlock)receiverBlock;
15
+
16
+ - (AVAudioFormat *)getInputFormat;
20
17
 
21
- - (instancetype)initWithReceiverBlock:(AudioReceiverBlock)receiverBlock
22
- bufferLength:(int)bufferLength
23
- sampleRate:(float)sampleRate;
18
+ - (int)getBufferSize;
24
19
 
25
20
  - (void)start;
26
21
 
27
22
  - (void)stop;
28
23
 
24
+ - (void)pause;
25
+
26
+ - (void)resume;
27
+
29
28
  - (void)cleanup;
30
29
 
31
30
  @end
@@ -4,42 +4,36 @@
4
4
 
5
5
  @implementation NativeAudioRecorder
6
6
 
7
- - (instancetype)initWithReceiverBlock:(AudioReceiverBlock)receiverBlock
8
- bufferLength:(int)bufferLength
9
- sampleRate:(float)sampleRate
7
+ static inline uint32_t nextPowerOfTwo(uint32_t x)
10
8
  {
11
- if (self = [super init]) {
12
- self.bufferLength = bufferLength;
13
- self.sampleRate = sampleRate;
14
-
15
- self.receiverBlock = [receiverBlock copy];
16
-
17
- float devicePrefferedSampleRate = [[AVAudioSession sharedInstance] sampleRate];
9
+ if (x == 0) {
10
+ return 1;
11
+ }
18
12
 
19
- if (!devicePrefferedSampleRate) {
20
- NSError *error;
21
- devicePrefferedSampleRate = sampleRate;
13
+ x--;
14
+ x |= x >> 1;
15
+ x |= x >> 2;
16
+ x |= x >> 4;
17
+ x |= x >> 8;
18
+ x |= x >> 16;
19
+ x++;
22
20
 
23
- [[AVAudioSession sharedInstance] setPreferredSampleRate:sampleRate error:&error];
24
- }
21
+ return x;
22
+ }
25
23
 
26
- self.inputFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
27
- sampleRate:devicePrefferedSampleRate
28
- channels:1
29
- interleaved:NO];
30
- self.outputFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
31
- sampleRate:sampleRate
32
- channels:1
33
- interleaved:NO];
34
- self.audioConverter = [[AVAudioConverter alloc] initFromFormat:self.inputFormat
35
- toFormat:self.outputFormat];
24
+ - (instancetype)initWithReceiverBlock:(AudioReceiverBlock)receiverBlock
25
+ {
26
+ if (self = [super init]) {
27
+ self.receiverBlock = [receiverBlock copy];
36
28
 
37
29
  __weak typeof(self) weakSelf = self;
38
30
  self.receiverSinkBlock = ^OSStatus(
39
31
  const AudioTimeStamp *_Nonnull timestamp,
40
32
  AVAudioFrameCount frameCount,
41
33
  const AudioBufferList *_Nonnull inputData) {
42
- return [weakSelf processAudioInput:inputData withFrameCount:frameCount atTimestamp:timestamp];
34
+ weakSelf.receiverBlock(inputData, frameCount);
35
+
36
+ return kAudioServicesNoError;
43
37
  };
44
38
 
45
39
  self.sinkNode = [[AVAudioSinkNode alloc] initWithReceiverBlock:self.receiverSinkBlock];
@@ -48,58 +42,36 @@
48
42
  return self;
49
43
  }
50
44
 
51
- - (OSStatus)processAudioInput:(const AudioBufferList *)inputData
52
- withFrameCount:(AVAudioFrameCount)frameCount
53
- atTimestamp:(const AudioTimeStamp *)timestamp
45
+ // Note: this method should be called only after the session is activated
46
+ - (AVAudioFormat *)getInputFormat
54
47
  {
55
- float inputSampleRate = self.inputFormat.sampleRate;
56
- float outputSampleRate = self.outputFormat.sampleRate;
57
-
58
- if (inputSampleRate != outputSampleRate) {
59
- AVAudioPCMBuffer *inputBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:self.inputFormat
60
- frameCapacity:frameCount];
61
- memcpy(
62
- inputBuffer.mutableAudioBufferList->mBuffers[0].mData,
63
- inputData->mBuffers[0].mData,
64
- inputData->mBuffers[0].mDataByteSize);
65
- inputBuffer.frameLength = frameCount;
66
-
67
- int outputFrameCount = frameCount * outputSampleRate / inputSampleRate;
68
-
69
- AVAudioPCMBuffer *outputBuffer =
70
- [[AVAudioPCMBuffer alloc] initWithPCMFormat:self.audioConverter.outputFormat
71
- frameCapacity:outputFrameCount];
72
-
73
- NSError *error = nil;
74
- AVAudioConverterInputBlock inputBlock = ^AVAudioBuffer *_Nullable(
75
- AVAudioPacketCount inNumberOfPackets, AVAudioConverterInputStatus *outStatus)
76
- {
77
- *outStatus = AVAudioConverterInputStatus_HaveData;
78
- return inputBuffer;
79
- };
48
+ AVAudioFormat *format = [AudioEngine.sharedInstance.audioEngine.inputNode inputFormatForBus:0];
80
49
 
81
- /// IMPORTANT: AVAudioConverter leaks memory without autorelease pool
82
- /// more details here:
83
- /// https://github.com/poneciak57/AVAudioConverter-memory-leak-repro-electric-boogaloo
84
- /// we can try to remove it in the future or refactor to reuse buffers to
85
- /// minimize allocations
86
- @autoreleasepool {
87
- [self.audioConverter convertToBuffer:outputBuffer error:&error withInputFromBlock:inputBlock];
88
- }
50
+ if (format.sampleRate == 0 || format.channelCount == 0) {
51
+ AudioSessionManager *sessionManager = [AudioSessionManager sharedInstance];
89
52
 
90
- if (error) {
91
- NSLog(@"Error during audio conversion: %@", error.localizedDescription);
92
- return kAudioServicesBadSpecifierSizeError;
93
- }
53
+ format = [[AVAudioFormat alloc]
54
+ initStandardFormatWithSampleRate:[[sessionManager getDevicePreferredSampleRate] doubleValue]
55
+ channels:[[sessionManager getDevicePreferredInputChannelCount]
56
+ intValue]];
57
+ }
94
58
 
95
- self.receiverBlock(outputBuffer.audioBufferList, outputBuffer.frameLength);
59
+ return format;
60
+ }
96
61
 
97
- return kAudioServicesNoError;
98
- }
62
+ - (int)getBufferSize
63
+ {
64
+ // NOTE: this method should be called only after the session is activated
65
+ AVAudioSession *audioSession = [AVAudioSession sharedInstance];
99
66
 
100
- self.receiverBlock(inputData, frameCount);
67
+ // TMPfix: it seems that buffer duration in some cases (background/device change) can switch
68
+ // to longer values, exceeding buffer size predicted after session start
69
+ // since it is just a couple of buffers we can set min value of 200ms
70
+ // to enforce we always have enough frames allocated to pass further down the pipeline
71
+ float bufferDuration = MAX(audioSession.IOBufferDuration, 0.2);
101
72
 
102
- return kAudioServicesNoError;
73
+ // IOS returns buffer duration rounded, but expects the buffer size to be power of two in runtime
74
+ return nextPowerOfTwo(ceil(bufferDuration * audioSession.sampleRate));
103
75
  }
104
76
 
105
77
  - (void)start
@@ -113,10 +85,9 @@
113
85
  // we haven't break rules of at runtime modifications from docs
114
86
  // https://developer.apple.com/documentation/avfaudio/avaudioengine?language=objc
115
87
  //
116
- // Currently we are restarting because we do not see any significant
117
- // performance issue and case when you will need to start and stop recorder
118
- // very frequently
119
- [audioEngine stopEngine];
88
+ // Currently we are restarting because we do not see any significant performance issue and case when
89
+ // you will need to start and stop recorder very frequently
90
+ [audioEngine stopIfNecessary];
120
91
  [audioEngine attachInputNode:self.sinkNode];
121
92
  [audioEngine startIfNecessary];
122
93
  }
@@ -125,8 +96,31 @@
125
96
  {
126
97
  AudioEngine *audioEngine = [AudioEngine sharedInstance];
127
98
  assert(audioEngine != nil);
99
+ [audioEngine stopIfPossible];
128
100
  [audioEngine detachInputNode];
129
- [audioEngine stopIfNecessary];
101
+
102
+ // This makes sure that the engine releases the input properly when we no longer need it
103
+ // (i.e. no more misleading dot)
104
+ // Restart only if is not running to avoid interruptions of playback
105
+ if ([audioEngine getState] != AudioEngineStateRunning) {
106
+ [audioEngine restartAudioEngine];
107
+ }
108
+ }
109
+
110
+ - (void)pause
111
+ {
112
+ AudioEngine *audioEngine = [AudioEngine sharedInstance];
113
+ assert(audioEngine != nil);
114
+
115
+ [audioEngine pauseIfNecessary];
116
+ }
117
+
118
+ - (void)resume
119
+ {
120
+ AudioEngine *audioEngine = [AudioEngine sharedInstance];
121
+ assert(audioEngine != nil);
122
+
123
+ [audioEngine startIfNecessary];
130
124
  }
131
125
 
132
126
  - (void)cleanup
@@ -61,6 +61,7 @@ std::shared_ptr<AudioBuffer> AudioDecoder::makeAudioBufferFromFloatBuffer(
61
61
  channelData[i] = buffer[i * outputChannels + ch];
62
62
  }
63
63
  }
64
+
64
65
  return std::make_shared<AudioBuffer>(audioBus);
65
66
  }
66
67