react-native-audio-api 0.13.3 → 0.13.5

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 (96) hide show
  1. package/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt +1 -0
  2. package/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp +8 -2
  3. package/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.h +1 -0
  4. package/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp +2 -0
  5. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp +15 -1
  6. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.h +19 -0
  7. package/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp +12 -1
  8. package/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +6 -0
  9. package/common/cpp/audioapi/core/utils/param/ParamQueueBase.hpp +1 -1
  10. package/common/cpp/audioapi/core/utils/param/ParamRenderQueue.cpp +35 -12
  11. package/common/cpp/audioapi/core/utils/param/ParamRenderQueue.h +9 -0
  12. package/common/cpp/audioapi/core/utils/worklets/SafeIncludes.h +3 -3
  13. package/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp +2 -2
  14. package/common/cpp/audioapi/events/AudioEvent.h +1 -0
  15. package/common/cpp/audioapi/events/AudioEventPayload.h +11 -0
  16. package/common/cpp/audioapi/events/AudioEventPayloadMapping.h +1 -0
  17. package/common/cpp/audioapi/utils/events/BufferingStateDispatcher.cpp +49 -0
  18. package/common/cpp/audioapi/utils/events/BufferingStateDispatcher.h +42 -0
  19. package/ios/audioapi/ios/core/IOSAudioRecorder.h +12 -1
  20. package/ios/audioapi/ios/core/IOSAudioRecorder.mm +195 -72
  21. package/ios/audioapi/ios/core/NativeAudioRecorder.h +3 -1
  22. package/ios/audioapi/ios/core/NativeAudioRecorder.m +41 -14
  23. package/ios/audioapi/ios/core/utils/FileOptions.mm +18 -5
  24. package/ios/audioapi/ios/core/utils/IOSFileWriter.h +8 -0
  25. package/ios/audioapi/ios/core/utils/IOSFileWriter.mm +55 -8
  26. package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.h +3 -0
  27. package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm +16 -0
  28. package/ios/audioapi/ios/system/AudioEngine.h +2 -1
  29. package/ios/audioapi/ios/system/AudioEngine.mm +49 -23
  30. package/ios/audioapi/ios/system/SystemNotificationManager.mm +10 -0
  31. package/lib/commonjs/Audio/Audio.js +30 -3
  32. package/lib/commonjs/Audio/Audio.js.map +1 -1
  33. package/lib/commonjs/Audio/Audio.web.js +21 -3
  34. package/lib/commonjs/Audio/Audio.web.js.map +1 -1
  35. package/lib/commonjs/Audio/AudioFileSourceNode.js +20 -0
  36. package/lib/commonjs/Audio/AudioFileSourceNode.js.map +1 -1
  37. package/lib/commonjs/Audio/controls/AudioControls.js +90 -94
  38. package/lib/commonjs/Audio/controls/AudioControls.js.map +1 -1
  39. package/lib/commonjs/Audio/controls/audioControlUtils.js +48 -18
  40. package/lib/commonjs/Audio/controls/audioControlUtils.js.map +1 -1
  41. package/lib/commonjs/Audio/index.js +7 -0
  42. package/lib/commonjs/Audio/index.js.map +1 -1
  43. package/lib/commonjs/Audio/utils.js +16 -5
  44. package/lib/commonjs/Audio/utils.js.map +1 -1
  45. package/lib/commonjs/AudioAPIModule/AudioAPIModule.js +1 -1
  46. package/lib/commonjs/core/AudioDecoder.js +4 -1
  47. package/lib/commonjs/core/AudioDecoder.js.map +1 -1
  48. package/lib/module/Audio/Audio.js +30 -3
  49. package/lib/module/Audio/Audio.js.map +1 -1
  50. package/lib/module/Audio/Audio.web.js +21 -3
  51. package/lib/module/Audio/Audio.web.js.map +1 -1
  52. package/lib/module/Audio/AudioFileSourceNode.js +20 -0
  53. package/lib/module/Audio/AudioFileSourceNode.js.map +1 -1
  54. package/lib/module/Audio/controls/AudioControls.js +92 -96
  55. package/lib/module/Audio/controls/AudioControls.js.map +1 -1
  56. package/lib/module/Audio/controls/audioControlUtils.js +48 -19
  57. package/lib/module/Audio/controls/audioControlUtils.js.map +1 -1
  58. package/lib/module/Audio/index.js +1 -0
  59. package/lib/module/Audio/index.js.map +1 -1
  60. package/lib/module/Audio/utils.js +15 -5
  61. package/lib/module/Audio/utils.js.map +1 -1
  62. package/lib/module/AudioAPIModule/AudioAPIModule.js +1 -1
  63. package/lib/module/core/AudioDecoder.js +4 -1
  64. package/lib/module/core/AudioDecoder.js.map +1 -1
  65. package/lib/typescript/Audio/Audio.d.ts.map +1 -1
  66. package/lib/typescript/Audio/Audio.web.d.ts.map +1 -1
  67. package/lib/typescript/Audio/AudioFileSourceNode.d.ts +2 -0
  68. package/lib/typescript/Audio/AudioFileSourceNode.d.ts.map +1 -1
  69. package/lib/typescript/Audio/controls/AudioControls.d.ts.map +1 -1
  70. package/lib/typescript/Audio/controls/audioControlUtils.d.ts +11 -4
  71. package/lib/typescript/Audio/controls/audioControlUtils.d.ts.map +1 -1
  72. package/lib/typescript/Audio/index.d.ts +1 -0
  73. package/lib/typescript/Audio/index.d.ts.map +1 -1
  74. package/lib/typescript/Audio/types.d.ts +15 -1
  75. package/lib/typescript/Audio/types.d.ts.map +1 -1
  76. package/lib/typescript/Audio/utils.d.ts +2 -1
  77. package/lib/typescript/Audio/utils.d.ts.map +1 -1
  78. package/lib/typescript/core/AudioDecoder.d.ts.map +1 -1
  79. package/lib/typescript/events/types.d.ts +5 -0
  80. package/lib/typescript/events/types.d.ts.map +1 -1
  81. package/lib/typescript/jsi-interfaces.d.ts +1 -0
  82. package/lib/typescript/jsi-interfaces.d.ts.map +1 -1
  83. package/package.json +8 -8
  84. package/scripts/validate-worklets-version.js +1 -1
  85. package/src/Audio/Audio.tsx +38 -2
  86. package/src/Audio/Audio.web.tsx +26 -2
  87. package/src/Audio/AudioFileSourceNode.ts +28 -0
  88. package/src/Audio/controls/AudioControls.tsx +117 -116
  89. package/src/Audio/controls/audioControlUtils.ts +63 -31
  90. package/src/Audio/index.ts +1 -0
  91. package/src/Audio/types.ts +15 -1
  92. package/src/Audio/utils.ts +20 -2
  93. package/src/AudioAPIModule/AudioAPIModule.ts +1 -1
  94. package/src/core/AudioDecoder.ts +5 -3
  95. package/src/events/types.ts +6 -0
  96. package/src/jsi-interfaces.ts +1 -0
@@ -2,7 +2,9 @@
2
2
  #import <AudioEngine.h>
3
3
  #import <AudioSessionManager.h>
4
4
  #import <Foundation/Foundation.h>
5
-
5
+ #include <algorithm>
6
+ #include <mutex>
7
+ #include <unordered_map>
6
8
  #include <vector>
7
9
 
8
10
  #include <audioapi/core/sources/RecorderAdapterNode.h>
@@ -19,62 +21,26 @@
19
21
 
20
22
  namespace audioapi {
21
23
 
22
- static inline NSNumber *recorderFormatNumber(id format, NSString *key)
24
+ static double recorderFormatSampleRate(AVAudioFormat *format)
23
25
  {
24
- if (format == nil) {
25
- return nil;
26
- }
27
-
28
- if ([format isKindOfClass:[AVAudioFormat class]]) {
29
- AVAudioFormat *audioFormat = (AVAudioFormat *)format;
30
-
31
- if ([key isEqualToString:@"sampleRate"]) {
32
- return @(audioFormat.sampleRate);
33
- }
34
-
35
- if ([key isEqualToString:@"channelCount"]) {
36
- return @(audioFormat.channelCount);
37
- }
38
-
39
- if ([key isEqualToString:@"interleaved"]) {
40
- return @(audioFormat.interleaved);
41
- }
42
- }
43
-
44
- @try {
45
- id value = [format valueForKey:key];
46
- return [value isKindOfClass:[NSNumber class]] ? value : nil;
47
- } @catch (__unused NSException *exception) {
48
- return nil;
49
- }
26
+ return format.sampleRate;
50
27
  }
51
28
 
52
- static inline double recorderFormatSampleRate(id format)
29
+ static AVAudioChannelCount recorderFormatChannelCount(AVAudioFormat *format)
53
30
  {
54
- return [recorderFormatNumber(format, @"sampleRate") doubleValue];
31
+ return format.channelCount;
55
32
  }
56
33
 
57
- static inline AVAudioChannelCount recorderFormatChannelCount(id format)
34
+ static bool hasUsableRecorderFormat(AVAudioFormat *format)
58
35
  {
59
- return (AVAudioChannelCount)[recorderFormatNumber(format, @"channelCount") unsignedIntegerValue];
36
+ return format != nil && format.sampleRate > 0 && format.channelCount > 0;
60
37
  }
61
38
 
62
- static inline bool recorderFormatInterleaved(id format)
39
+ static std::string describeRecorderFormat(AVAudioFormat *format)
63
40
  {
64
- return [recorderFormatNumber(format, @"interleaved") boolValue];
65
- }
66
-
67
- static inline bool hasUsableRecorderFormat(id format)
68
- {
69
- return format != nil && recorderFormatSampleRate(format) > 0 &&
70
- recorderFormatChannelCount(format) > 0;
71
- }
72
-
73
- static std::string describeRecorderFormat(id format)
74
- {
75
- return "engineFormat={sampleRate=" + std::to_string(recorderFormatSampleRate(format)) +
76
- ", channelCount=" + std::to_string(recorderFormatChannelCount(format)) +
77
- ", interleaved=" + std::string(recorderFormatInterleaved(format) ? "true" : "false") + "}";
41
+ return "engineFormat={sampleRate=" + std::to_string(format.sampleRate) +
42
+ ", channelCount=" + std::to_string(format.channelCount) +
43
+ ", interleaved=" + (format.interleaved ? "true" : "false") + "}";
78
44
  }
79
45
 
80
46
  /// @brief Appends the shared input diagnostics (and, on the simulator, the host-mic hint)
@@ -103,31 +69,183 @@ IOSAudioRecorder::IOSAudioRecorder(
103
69
  : AudioRecorder(audioEventHandlerRegistry)
104
70
  {
105
71
  AudioReceiverBlock receiverBlock = ^(const AudioBufferList *inputBuffer, int numFrames) {
106
- if (usesFileOutput()) {
107
- if (auto lock = Locker::tryLock(fileWriterMutex_)) {
108
- fileWriter_->writeAudioData(inputBuffer, numFrames);
109
- }
72
+ runSideEffects(inputBuffer, numFrames);
73
+ };
74
+
75
+ nativeRecorder_ = [[NativeAudioRecorder alloc] initWithReceiverBlock:receiverBlock];
76
+
77
+ nativeRecorder_.onInputConfigurationChange = ^{ this->handleInputConfigurationChange(); };
78
+ }
79
+
80
+ void IOSAudioRecorder::runSideEffects(const AudioBufferList *inputBuffer, int numFrames)
81
+ {
82
+ if (usesFileOutput()) {
83
+ if (auto lock = Locker::tryLock(fileWriterMutex_)) {
84
+ fileWriter_->writeAudioData(inputBuffer, numFrames);
85
+ }
86
+ }
87
+
88
+ if (usesCallback()) {
89
+ if (auto lock = Locker::tryLock(callbackMutex_)) {
90
+ std::static_pointer_cast<IOSRecorderCallback>(dataCallback_)
91
+ ->receiveAudioData(inputBuffer, numFrames);
110
92
  }
93
+ }
111
94
 
112
- if (usesCallback()) {
113
- if (auto lock = Locker::tryLock(callbackMutex_)) {
114
- std::static_pointer_cast<IOSRecorderCallback>(dataCallback_)
115
- ->receiveAudioData(inputBuffer, numFrames);
95
+ if (isConnected()) {
96
+ if (auto lock = Locker::tryLock(adapterNodeMutex_)) {
97
+ const size_t channelCount = std::min(
98
+ adapterNode_->getChannelCount(), static_cast<size_t>(inputBuffer->mNumberBuffers));
99
+ for (size_t channel = 0; channel < channelCount; ++channel) {
100
+ auto *data = static_cast<float *>(inputBuffer->mBuffers[channel].mData);
101
+ adapterNode_->buff_[channel]->write(data, numFrames);
116
102
  }
117
103
  }
104
+ }
105
+ }
118
106
 
119
- if (isConnected()) {
120
- if (auto lock = Locker::tryLock(adapterNodeMutex_)) {
121
- for (size_t channel = 0; channel < adapterNode_->getChannelCount(); ++channel) {
122
- auto data = (float *)inputBuffer->mBuffers[channel].mData;
107
+ void IOSAudioRecorder::handleInputConfigurationChange()
108
+ {
109
+ if (isIdle()) {
110
+ return;
111
+ }
123
112
 
124
- adapterNode_->buff_[channel]->write(data, numFrames);
125
- }
113
+ BOOL formatChanged = NO;
114
+ if (![nativeRecorder_ refreshResolvedInputFormatReturningChanged:&formatChanged]) {
115
+ return;
116
+ }
117
+
118
+ if (!formatChanged) {
119
+ if (state_.load(std::memory_order_acquire) == RecorderState::Recording) {
120
+ [nativeRecorder_ setInputArmed:true];
121
+ }
122
+ return;
123
+ }
124
+
125
+ reprepareForLiveInput();
126
+ }
127
+
128
+ Result<NoneType, std::string> IOSAudioRecorder::reprepareForLiveInput()
129
+ {
130
+ if (isIdle()) {
131
+ return Result<NoneType, std::string>::Ok(None);
132
+ }
133
+
134
+ // those values are resolved earlier with actual correct values
135
+ AVAudioFormat *inputFormat = [nativeRecorder_ getResolvedInputFormat];
136
+ int maxInputBufferLength = [nativeRecorder_ getResolvedBufferSize];
137
+
138
+ if (!hasUsableRecorderFormat(inputFormat) || maxInputBufferLength <= 0) {
139
+ return Result<NoneType, std::string>::Err("Recorder input format is unavailable");
140
+ }
141
+
142
+ const bool shouldArmInput = state_.load(std::memory_order_acquire) == RecorderState::Recording;
143
+ [nativeRecorder_ setInputArmed:false];
144
+
145
+ if (usesFileOutput()) {
146
+ auto fileResult = reprepareFileWriter(inputFormat, maxInputBufferLength);
147
+ if (fileResult.is_err()) {
148
+ if (shouldArmInput) {
149
+ [nativeRecorder_ setInputArmed:true];
126
150
  }
151
+ return fileResult;
127
152
  }
128
- };
153
+ }
129
154
 
130
- nativeRecorder_ = [[NativeAudioRecorder alloc] initWithReceiverBlock:receiverBlock];
155
+ if (usesCallback()) {
156
+ auto callbackResult = reprepareCallback(inputFormat, maxInputBufferLength);
157
+ if (callbackResult.is_err()) {
158
+ if (shouldArmInput) {
159
+ [nativeRecorder_ setInputArmed:true];
160
+ }
161
+ return callbackResult;
162
+ }
163
+ }
164
+
165
+ if (isConnected() && adapterNode_ != nullptr) {
166
+ reprepareAdapter(inputFormat, maxInputBufferLength);
167
+ }
168
+
169
+ if (shouldArmInput) {
170
+ [nativeRecorder_ setInputArmed:true];
171
+ }
172
+
173
+ return Result<NoneType, std::string>::Ok(None);
174
+ }
175
+
176
+ Result<NoneType, std::string> IOSAudioRecorder::reprepareFileWriter(
177
+ AVAudioFormat *inputFormat,
178
+ int maxInputBufferLength)
179
+ {
180
+ std::scoped_lock lock(fileWriterMutex_);
181
+
182
+ if (fileWriter_ == nullptr) {
183
+ return Result<NoneType, std::string>::Err("File writer is unavailable");
184
+ }
185
+
186
+ if (auto rotatingWriter = std::dynamic_pointer_cast<IOSRotatingFileWriter>(fileWriter_)) {
187
+ auto result = rotatingWriter->reprepareStreamFormat(
188
+ inputFormat, static_cast<size_t>(maxInputBufferLength));
189
+ if (result.is_err()) {
190
+ fileOutputConfigured_.store(false, std::memory_order_release);
191
+ return Result<NoneType, std::string>::Err(
192
+ "Failed to reopen file for writing: " + result.unwrap_err());
193
+ }
194
+
195
+ filePath_ = result.unwrap();
196
+ fileOutputConfigured_.store(true, std::memory_order_release);
197
+ return Result<NoneType, std::string>::Ok(None);
198
+ }
199
+
200
+ auto iosWriter = std::static_pointer_cast<IOSFileWriter>(fileWriter_);
201
+ auto result =
202
+ iosWriter->reopenForInputFormatChange(inputFormat, static_cast<size_t>(maxInputBufferLength));
203
+ if (result.is_err()) {
204
+ fileOutputConfigured_.store(false, std::memory_order_release);
205
+ return Result<NoneType, std::string>::Err(
206
+ "Failed to reopen file for writing: " + result.unwrap_err());
207
+ }
208
+
209
+ filePath_ = result.unwrap();
210
+ fileOutputConfigured_.store(true, std::memory_order_release);
211
+ return Result<NoneType, std::string>::Ok(None);
212
+ }
213
+
214
+ Result<NoneType, std::string> IOSAudioRecorder::reprepareCallback(
215
+ AVAudioFormat *inputFormat,
216
+ int maxInputBufferLength)
217
+ {
218
+ std::scoped_lock lock(callbackMutex_);
219
+
220
+ if (dataCallback_ == nullptr) {
221
+ return Result<NoneType, std::string>::Err("Callback is unavailable");
222
+ }
223
+
224
+ auto result = std::static_pointer_cast<IOSRecorderCallback>(dataCallback_)
225
+ ->prepare(inputFormat, static_cast<size_t>(maxInputBufferLength));
226
+ if (result.is_err()) {
227
+ callbackOutputConfigured_.store(false, std::memory_order_release);
228
+ return Result<NoneType, std::string>::Err("Failed to prepare callback: " + result.unwrap_err());
229
+ }
230
+
231
+ callbackOutputConfigured_.store(true, std::memory_order_release);
232
+ return Result<NoneType, std::string>::Ok(None);
233
+ }
234
+
235
+ void IOSAudioRecorder::reprepareAdapter(AVAudioFormat *inputFormat, int maxInputBufferLength)
236
+ {
237
+ std::scoped_lock lock(adapterNodeMutex_);
238
+ if (adapterNode_ == nullptr) {
239
+ return;
240
+ }
241
+
242
+ // init() is a no-op on an initialized adapter, so the old format has to be torn down first.
243
+ adapterNode_->adapterCleanup();
244
+ adapterNode_->init(
245
+ static_cast<size_t>(maxInputBufferLength),
246
+ recorderFormatChannelCount(inputFormat),
247
+ recorderFormatSampleRate(inputFormat));
248
+ connectedConfigured_.store(true, std::memory_order_release);
131
249
  }
132
250
 
133
251
  IOSAudioRecorder::~IOSAudioRecorder()
@@ -136,6 +254,8 @@ IOSAudioRecorder::~IOSAudioRecorder()
136
254
  stop();
137
255
  }
138
256
 
257
+ nativeRecorder_.onInputConfigurationChange = nil;
258
+
139
259
  {
140
260
  std::scoped_lock lock(callbackMutex_, fileWriterMutex_, adapterNodeMutex_);
141
261
  callbackOutputConfigured_.store(false, std::memory_order_release);
@@ -291,16 +411,16 @@ AudioRecorder::StopResult IOSAudioRecorder::stop()
291
411
  std::shared_ptr<RecorderAdapterNode> adapterNode;
292
412
  std::vector<std::string> outputPaths;
293
413
 
294
- {
295
- std::scoped_lock stopLock(callbackMutex_, fileWriterMutex_, adapterNodeMutex_);
414
+ if (isIdle()) {
415
+ return StopResult::Err("Recorder is not in recording state.");
416
+ }
296
417
 
297
- if (isIdle()) {
298
- return StopResult::Err("Recorder is not in recording state.");
299
- }
418
+ state_.store(RecorderState::Idle, std::memory_order_release);
419
+ [nativeRecorder_ setInputArmed:false];
420
+ [nativeRecorder_ stop];
300
421
 
301
- [nativeRecorder_ setInputArmed:false];
302
- state_.store(RecorderState::Idle, std::memory_order_release);
303
- [nativeRecorder_ stop];
422
+ {
423
+ std::scoped_lock stopLock(callbackMutex_, fileWriterMutex_, adapterNodeMutex_);
304
424
 
305
425
  if (usesFileOutput()) {
306
426
  fileOutputConfigured_.store(false, std::memory_order_release);
@@ -449,8 +569,11 @@ void IOSAudioRecorder::resume()
449
569
  return;
450
570
  }
451
571
 
452
- [nativeRecorder_ resume];
453
572
  state_.store(RecorderState::Recording, std::memory_order_release);
573
+
574
+ if (![nativeRecorder_ resume]) {
575
+ state_.store(RecorderState::Paused, std::memory_order_release);
576
+ }
454
577
  }
455
578
 
456
579
  /// @brief Checks if the recorder is currently recording.
@@ -12,19 +12,21 @@ typedef void (^AudioReceiverBlock)(const AudioBufferList *inputBuffer, int numFr
12
12
  @property (nonatomic, strong) AVAudioFormat *resolvedInputFormat;
13
13
  @property (nonatomic, assign) int resolvedBufferSize;
14
14
  @property (atomic, assign) BOOL inputArmed;
15
+ @property (nonatomic, copy) void (^onInputConfigurationChange)(void);
15
16
 
16
17
  - (instancetype)initWithReceiverBlock:(AudioReceiverBlock)receiverBlock;
17
18
 
18
19
  - (int)getBufferSize;
19
20
  - (AVAudioFormat *)getResolvedInputFormat;
20
21
  - (int)getResolvedBufferSize;
22
+ - (BOOL)refreshResolvedInputFormatReturningChanged:(BOOL *)formatChanged;
21
23
  - (BOOL)start:(NSError **)error;
22
24
 
23
25
  - (void)stop;
24
26
 
25
27
  - (void)pause;
26
28
 
27
- - (void)resume;
29
+ - (BOOL)resume;
28
30
 
29
31
  - (void)cleanup;
30
32
 
@@ -57,17 +57,15 @@ static inline uint32_t nextPowerOfTwo(uint32_t x)
57
57
 
58
58
  - (int)getBufferSize
59
59
  {
60
- // NOTE: this method should be called only after the session is activated
61
60
  AVAudioSession *audioSession = [AVAudioSession sharedInstance];
61
+ double sampleRate = audioSession.sampleRate;
62
62
 
63
- // TMPfix: it seems that buffer duration in some cases (background/device change) can switch
64
- // to longer values, exceeding buffer size predicted after session start
65
- // since it is just a couple of buffers we can set min value of 200ms
66
- // to enforce we always have enough frames allocated to pass further down the pipeline
67
- float bufferDuration = MAX(audioSession.IOBufferDuration, 0.2);
63
+ if (self.resolvedInputFormat != nil && self.resolvedInputFormat.sampleRate > 0) {
64
+ sampleRate = self.resolvedInputFormat.sampleRate;
65
+ }
68
66
 
69
- // IOS returns buffer duration rounded, but expects the buffer size to be power of two in runtime
70
- return nextPowerOfTwo(ceil(bufferDuration * audioSession.sampleRate));
67
+ float bufferDuration = MAX(audioSession.IOBufferDuration, 0.2);
68
+ return nextPowerOfTwo(ceil(bufferDuration * sampleRate));
71
69
  }
72
70
 
73
71
  - (int)getResolvedBufferSize
@@ -75,6 +73,29 @@ static inline uint32_t nextPowerOfTwo(uint32_t x)
75
73
  return self.resolvedBufferSize;
76
74
  }
77
75
 
76
+ - (BOOL)refreshResolvedInputFormatReturningChanged:(BOOL *)formatChanged
77
+ {
78
+ AVAudioFormat *liveFormat = [self readLiveInputFormat];
79
+
80
+ if (liveFormat == nil || liveFormat.sampleRate <= 0 || liveFormat.channelCount == 0) {
81
+ return NO;
82
+ }
83
+
84
+ AVAudioFormat *previousFormat = self.resolvedInputFormat;
85
+ BOOL changed = previousFormat == nil || previousFormat.sampleRate != liveFormat.sampleRate ||
86
+ previousFormat.channelCount != liveFormat.channelCount ||
87
+ previousFormat.isInterleaved != liveFormat.isInterleaved;
88
+
89
+ self.resolvedInputFormat = liveFormat;
90
+ self.resolvedBufferSize = [self getBufferSize];
91
+
92
+ if (formatChanged != nil) {
93
+ *formatChanged = changed;
94
+ }
95
+
96
+ return YES;
97
+ }
98
+
78
99
  - (BOOL)start:(NSError **)error
79
100
  {
80
101
  AudioEngine *audioEngine = [AudioEngine sharedInstance];
@@ -93,7 +114,8 @@ static inline uint32_t nextPowerOfTwo(uint32_t x)
93
114
  self.resolvedBufferSize = 0;
94
115
 
95
116
  [audioEngine stopIfNecessary];
96
- [audioEngine attachInputNodeWithReceiverBlock:self.receiverSinkBlock];
117
+ [audioEngine attachInputNodeWithReceiverBlock:self.receiverSinkBlock
118
+ onInputConfigurationChange:self.onInputConfigurationChange];
97
119
 
98
120
  if (![audioEngine startIfNecessary]) {
99
121
  [audioEngine detachInputNode];
@@ -128,8 +150,6 @@ static inline uint32_t nextPowerOfTwo(uint32_t x)
128
150
  self.inputArmed = NO;
129
151
  [audioEngine detachInputNode];
130
152
  [audioEngine stopIfPossible];
131
- // This makes sure that the engine releases the input properly when we no longer need it
132
- // (i.e. no more misleading dot)
133
153
  [audioEngine restartAudioEngine];
134
154
  self.resolvedInputFormat = nil;
135
155
  self.resolvedBufferSize = 0;
@@ -144,14 +164,20 @@ static inline uint32_t nextPowerOfTwo(uint32_t x)
144
164
  [audioEngine pauseIfNecessary];
145
165
  }
146
166
 
147
- - (void)resume
167
+ - (BOOL)resume
148
168
  {
149
169
  AudioEngine *audioEngine = [AudioEngine sharedInstance];
150
170
  assert(audioEngine != nil);
151
171
 
152
- if ([audioEngine startIfNecessary]) {
153
- self.inputArmed = YES;
172
+ if (![audioEngine startIfNecessary]) {
173
+ return NO;
174
+ }
175
+
176
+ if (self.onInputConfigurationChange != nil) {
177
+ self.onInputConfigurationChange();
154
178
  }
179
+
180
+ return YES;
155
181
  }
156
182
 
157
183
  - (void)cleanup
@@ -161,6 +187,7 @@ static inline uint32_t nextPowerOfTwo(uint32_t x)
161
187
  self.resolvedBufferSize = 0;
162
188
  self.receiverBlock = nil;
163
189
  self.receiverSinkBlock = nil;
190
+ self.onInputConfigurationChange = nil;
164
191
  }
165
192
 
166
193
  @end
@@ -166,13 +166,26 @@ NSURL *getFileURL(
166
166
  NSString *timestamp = getTimestampString();
167
167
  NSString *fileExtension = getFileExtension(properties);
168
168
 
169
- NSString *fileName = fileNameOverride.length() > 0 && properties->rotateIntervalBytes == 0
170
- ? [NSString stringWithFormat:@"%@.%@",
169
+ if (fileNameOverride.length() > 0 && properties->rotateIntervalBytes == 0) {
170
+ NSString *fileName =
171
+ [NSString stringWithFormat:@"%@.%@",
171
172
  [NSString stringWithUTF8String:fileNameOverride.c_str()],
172
- fileExtension]
173
- : [NSString stringWithFormat:@"%@_%@.%@", fileNamePrefix, timestamp, fileExtension];
173
+ fileExtension];
174
+ return [directoryURL URLByAppendingPathComponent:fileName];
175
+ }
176
+
177
+ NSString *fileName =
178
+ [NSString stringWithFormat:@"%@_%@.%@", fileNamePrefix, timestamp, fileExtension];
179
+ NSURL *fileURL = [directoryURL URLByAppendingPathComponent:fileName];
180
+
181
+ for (NSUInteger suffix = 1; [[NSFileManager defaultManager] fileExistsAtPath:fileURL.path];
182
+ ++suffix) {
183
+ fileName = [NSString
184
+ stringWithFormat:@"%@_%@_%lu.%@", fileNamePrefix, timestamp, (unsigned long)suffix, fileExtension];
185
+ fileURL = [directoryURL URLByAppendingPathComponent:fileName];
186
+ }
174
187
 
175
- return [directoryURL URLByAppendingPathComponent:fileName];
188
+ return fileURL;
176
189
  }
177
190
 
178
191
  NSSearchPathDirectory getDirectory(const std::shared_ptr<AudioFileProperties> &properties)
@@ -41,6 +41,9 @@ class IOSFileWriter : public AudioFileWriter {
41
41
  AVAudioFormat *bufferFormat,
42
42
  size_t maxInputBufferLength,
43
43
  const std::string &fileNameOverride);
44
+ OpenFileResult reopenForInputFormatChange(
45
+ AVAudioFormat *bufferFormat,
46
+ size_t maxInputBufferLength);
44
47
  CloseFileResult closeFile() override;
45
48
 
46
49
  void writeAudioData(const AudioBufferList *audioBufferList, int numFrames) override;
@@ -64,6 +67,11 @@ class IOSFileWriter : public AudioFileWriter {
64
67
  private:
65
68
  using FreeList = slots::SlotFreeList<FILE_WRITER_POOL_SIZE>;
66
69
 
70
+ Result<NoneType, std::string> prepareConversionPipeline(
71
+ AVAudioFormat *bufferFormat,
72
+ size_t maxInputBufferLength);
73
+ void releaseConversionPipeline();
74
+
67
75
  std::vector<ios::OwnedAudioBufferListPtr> inputBufferPool_;
68
76
  size_t inputBufferBytesPerBuffer_{0};
69
77
  std::unique_ptr<FreeList> freeSlots_;
@@ -83,6 +83,24 @@ OpenFileResult IOSFileWriter::openFile(
83
83
  [[error debugDescription] UTF8String]);
84
84
  }
85
85
 
86
+ auto pipelineResult = prepareConversionPipeline(bufferFormat, maxInputBufferLength);
87
+
88
+ if (pipelineResult.is_err()) {
89
+ rollbackFailedOpen();
90
+ return OpenFileResult::Err(pipelineResult.unwrap_err());
91
+ }
92
+
93
+ isFileOpen_.store(true, std::memory_order_release);
94
+ return OpenFileResult::Ok([[fileURL_ path] UTF8String]);
95
+ }
96
+ }
97
+
98
+ /// @brief Builds the converter, conversion buffers and offloader for the given input format.
99
+ Result<NoneType, std::string> IOSFileWriter::prepareConversionPipeline(
100
+ AVAudioFormat *bufferFormat,
101
+ size_t maxInputBufferLength)
102
+ {
103
+ @autoreleasepool {
86
104
  converter_ = [[AVAudioConverter alloc] initFromFormat:bufferFormat
87
105
  toFormat:[audioFile_ processingFormat]];
88
106
  converter_.sampleRateConverterAlgorithm = AVSampleRateConverterAlgorithm_Normal;
@@ -103,8 +121,7 @@ OpenFileResult IOSFileWriter::openFile(
103
121
 
104
122
  if (converterInputBuffer_ == nil || converterOutputBuffer_ == nil || audioFile_ == nil ||
105
123
  converter_ == nil) {
106
- rollbackFailedOpen();
107
- return OpenFileResult::Err("Error creating converter buffers");
124
+ return Result<NoneType, std::string>::Err("Error creating converter buffers");
108
125
  }
109
126
 
110
127
  auto offloaderLambda = [this](WriterData data) {
@@ -126,8 +143,7 @@ OpenFileResult IOSFileWriter::openFile(
126
143
  for (size_t i = 0; i < FILE_WRITER_POOL_SIZE; ++i) {
127
144
  OwnedAudioBufferListPtr buffer(allocateOwnedAudioBufferList(bufferCount, bytesPerBuffer));
128
145
  if (buffer == nullptr) {
129
- rollbackFailedOpen();
130
- return OpenFileResult::Err("Failed to preallocate iOS file writer buffers");
146
+ return Result<NoneType, std::string>::Err("Failed to preallocate iOS file writer buffers");
131
147
  }
132
148
  inputBufferPool_.push_back(std::move(buffer));
133
149
  }
@@ -135,22 +151,53 @@ OpenFileResult IOSFileWriter::openFile(
135
151
  freeSlots_ = std::make_unique<FreeList>();
136
152
  freeSlots_->seed();
137
153
 
138
- isFileOpen_.store(true, std::memory_order_release);
139
- return OpenFileResult::Ok([[fileURL_ path] UTF8String]);
154
+ return Result<NoneType, std::string>::Ok(None);
140
155
  }
141
156
  }
142
157
 
143
- void IOSFileWriter::rollbackFailedOpen()
158
+ /// @brief Tears down the converter, conversion buffers and offloader, draining pending writes.
159
+ void IOSFileWriter::releaseConversionPipeline()
144
160
  {
145
161
  offloader_.reset();
146
162
  freeSlots_.reset();
147
163
  inputBufferPool_.clear();
148
164
  inputBufferBytesPerBuffer_ = 0;
149
165
 
150
- audioFile_ = nil;
151
166
  converter_ = nil;
152
167
  converterInputBuffer_ = nil;
153
168
  converterOutputBuffer_ = nil;
169
+ }
170
+
171
+ OpenFileResult IOSFileWriter::reopenForInputFormatChange(
172
+ AVAudioFormat *bufferFormat,
173
+ size_t maxInputBufferLength)
174
+ {
175
+ @autoreleasepool {
176
+ if (!isFileOpen() || audioFile_ == nil) {
177
+ return openFile(bufferFormat, maxInputBufferLength, "");
178
+ }
179
+
180
+ // The file settings come from the recorder's file properties, not from the input, so the file
181
+ // stays open across a configuration change and the recording is not split. Only the conversion
182
+ // pipeline, which is built for the input format, is rebuilt.
183
+ releaseConversionPipeline();
184
+ bufferFormat_ = bufferFormat;
185
+
186
+ auto pipelineResult = prepareConversionPipeline(bufferFormat, maxInputBufferLength);
187
+
188
+ if (pipelineResult.is_err()) {
189
+ return OpenFileResult::Err(pipelineResult.unwrap_err());
190
+ }
191
+
192
+ return OpenFileResult::Ok([[fileURL_ path] UTF8String]);
193
+ }
194
+ }
195
+
196
+ void IOSFileWriter::rollbackFailedOpen()
197
+ {
198
+ releaseConversionPipeline();
199
+
200
+ audioFile_ = nil;
154
201
  bufferFormat_ = nil;
155
202
 
156
203
  if (fileURL_ != nil) {
@@ -30,6 +30,9 @@ class IOSRotatingFileWriter : public IOSFileWriter, public RotatingFileWriter {
30
30
  AVAudioFormat *streamFormat,
31
31
  size_t streamMaxBufferSizeInFrames,
32
32
  const std::string &fileNameOverride) override;
33
+ OpenFileResult reprepareStreamFormat(
34
+ AVAudioFormat *streamFormat,
35
+ size_t streamMaxBufferSizeInFrames);
33
36
  CloseFileResult closeFile() override;
34
37
  [[nodiscard]] double getCurrentDuration() const override;
35
38
 
@@ -46,6 +46,22 @@ OpenFileResult IOSRotatingFileWriter::openFile(
46
46
  return openInnerWriter();
47
47
  }
48
48
 
49
+ OpenFileResult IOSRotatingFileWriter::reprepareStreamFormat(
50
+ AVAudioFormat *streamFormat,
51
+ size_t streamMaxBufferSizeInFrames)
52
+ {
53
+ streamFormat_ = streamFormat;
54
+ streamMaxBufferSizeInFrames_ = streamMaxBufferSizeInFrames;
55
+
56
+ if (currentWriter_ == nullptr) {
57
+ currentWriter_ = writerFactory_(fileProperties_);
58
+ return openInnerWriter();
59
+ }
60
+
61
+ auto inner = std::static_pointer_cast<IOSFileWriter>(currentWriter_);
62
+ return inner->reopenForInputFormatChange(streamFormat_, streamMaxBufferSizeInFrames_);
63
+ }
64
+
49
65
  void IOSRotatingFileWriter::writeAudioData(AudioDataType data, int numFrames)
50
66
  {
51
67
  if (currentWriter_ == nullptr) {
@@ -33,7 +33,8 @@ typedef NS_ENUM(NSInteger, AudioEngineState) {
33
33
  channelCount:(AVAudioChannelCount)channelCount;
34
34
  - (void)detachSourceNodeWithId:(NSString *)sourceNodeId;
35
35
 
36
- - (void)attachInputNodeWithReceiverBlock:(AVAudioSinkNodeReceiverBlock)receiverBlock;
36
+ - (void)attachInputNodeWithReceiverBlock:(AVAudioSinkNodeReceiverBlock)receiverBlock
37
+ onInputConfigurationChange:(void (^)(void))onInputConfigurationChange;
37
38
  - (void)detachInputNode;
38
39
  - (AVAudioFormat *)getLiveInputFormat;
39
40