react-native-audio-api 0.13.4 → 0.13.6
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.
- package/android/src/main/cpp/audioapi/android/OnLoad.cpp +5 -1
- package/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +26 -17
- package/android/src/main/cpp/audioapi/android/system/NativeRecorderControl.cpp +41 -0
- package/android/src/main/cpp/audioapi/android/system/NativeRecorderControl.h +23 -0
- package/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt +1 -0
- package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +136 -30
- package/android/src/main/java/com/swmansion/audioapi/system/ForegroundServiceManager.kt +47 -12
- package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +23 -0
- package/android/src/main/java/com/swmansion/audioapi/system/NativeRecorderControl.kt +39 -0
- package/android/src/main/java/com/swmansion/audioapi/system/RecorderState.kt +17 -0
- package/android/src/main/java/com/swmansion/audioapi/system/notification/NotificationRegistry.kt +48 -20
- package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt +221 -227
- package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotificationReceiver.kt +122 -9
- package/android/src/main/java/com/swmansion/audioapi/system/notification/state/RecordingNotificationState.kt +17 -11
- package/common/cpp/audioapi/AudioAPIModuleInstaller.h +44 -0
- package/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp +18 -11
- package/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.h +1 -0
- package/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp +2 -0
- package/common/cpp/audioapi/core/inputs/ActiveRecorderHandle.cpp +117 -0
- package/common/cpp/audioapi/core/inputs/ActiveRecorderHandle.h +85 -0
- package/common/cpp/audioapi/core/inputs/AudioRecorder.cpp +8 -0
- package/common/cpp/audioapi/core/inputs/AudioRecorder.h +3 -1
- package/common/cpp/audioapi/core/inputs/RecorderState.h +13 -0
- package/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +0 -1
- package/common/cpp/audioapi/core/utils/AudioRecorderCallback.cpp +4 -4
- package/common/cpp/audioapi/core/utils/AudioRecorderCallback.h +2 -4
- package/common/cpp/audioapi/events/AudioEvent.h +1 -0
- package/ios/audioapi/ios/AudioAPIModule.mm +30 -23
- package/ios/audioapi/ios/core/IOSAudioRecorder.mm +27 -29
- package/ios/audioapi/ios/core/NativeAudioRecorder.h +1 -1
- package/ios/audioapi/ios/core/NativeAudioRecorder.m +9 -7
- package/ios/audioapi/ios/core/utils/FileOptions.mm +18 -5
- package/ios/audioapi/ios/core/utils/IOSFileWriter.h +5 -0
- package/ios/audioapi/ios/core/utils/IOSFileWriter.mm +50 -20
- package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm +2 -7
- package/ios/audioapi/ios/system/SystemNotificationManager.mm +4 -1
- package/lib/commonjs/core/AudioRecorder.js +12 -4
- package/lib/commonjs/core/AudioRecorder.js.map +1 -1
- package/lib/commonjs/mock/index.js +11 -1
- package/lib/commonjs/mock/index.js.map +1 -1
- package/lib/commonjs/plugin/withAudioAPI.js +4 -2
- package/lib/commonjs/plugin/withAudioAPI.js.map +1 -1
- package/lib/module/core/AudioRecorder.js +12 -4
- package/lib/module/core/AudioRecorder.js.map +1 -1
- package/lib/module/mock/index.js +11 -1
- package/lib/module/mock/index.js.map +1 -1
- package/lib/module/plugin/withAudioAPI.js +4 -2
- package/lib/module/plugin/withAudioAPI.js.map +1 -1
- package/lib/typescript/core/AudioRecorder.d.ts +2 -0
- package/lib/typescript/core/AudioRecorder.d.ts.map +1 -1
- package/lib/typescript/mock/index.d.ts +3 -0
- package/lib/typescript/mock/index.d.ts.map +1 -1
- package/lib/typescript/plugin/withAudioAPI.d.ts +1 -0
- package/lib/typescript/plugin/withAudioAPI.d.ts.map +1 -1
- package/lib/typescript/system/notification/types.d.ts +32 -2
- package/lib/typescript/system/notification/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AudioAPIModule/globals.d.ts +5 -0
- package/src/core/AudioRecorder.ts +17 -4
- package/src/mock/index.ts +14 -1
- package/src/plugin/withAudioAPI.ts +4 -2
- package/src/system/notification/types.ts +32 -2
- package/android/src/main/res/layout/btn_round_ripple.xml +0 -9
- package/android/src/main/res/layout/notification_collapsed.xml +0 -45
- package/android/src/main/res/layout/notification_expanded.xml +0 -44
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
#import <audioapi/ios/system/SystemNotificationManager.h>
|
|
21
21
|
#import <audioapi/ios/system/notification/NotificationRegistry.h>
|
|
22
22
|
|
|
23
|
+
#import <audioapi/core/inputs/ActiveRecorderHandle.h>
|
|
23
24
|
#import <audioapi/events/AudioEventHandlerRegistry.h>
|
|
24
25
|
|
|
25
26
|
using namespace audioapi;
|
|
@@ -145,17 +146,23 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(resolveAndroidReleaseAsset : (NSString *)
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
RCT_EXPORT_METHOD(
|
|
148
|
-
readAndroidReleaseAssetBytesAsBase64 : (NSString *)assetPath resolve : (
|
|
149
|
-
resolve reject : (RCTPromiseRejectBlock)reject)
|
|
149
|
+
readAndroidReleaseAssetBytesAsBase64 : (NSString *)assetPath resolve : (
|
|
150
|
+
RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
|
|
150
151
|
{
|
|
151
152
|
reject(@"E_PLATFORM", @"readAndroidReleaseAssetBytesAsBase64 is only available on Android", nil);
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
RCT_EXPORT_METHOD(
|
|
155
|
-
setAudioSessionActivity : (BOOL)enabled resolve : (RCTPromiseResolveBlock)
|
|
156
|
-
|
|
156
|
+
setAudioSessionActivity : (BOOL)enabled resolve : (RCTPromiseResolveBlock)resolve reject : (
|
|
157
|
+
RCTPromiseRejectBlock)reject)
|
|
157
158
|
{
|
|
158
159
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
160
|
+
// Deactivating the session underneath a live recording would corrupt its output, so
|
|
161
|
+
// the recording is finalized first
|
|
162
|
+
if (!enabled) {
|
|
163
|
+
ActiveRecorderHandle::global().stopAndReturnInfo();
|
|
164
|
+
}
|
|
165
|
+
|
|
159
166
|
NSError *error = nil;
|
|
160
167
|
auto success = [self.audioSessionManager setActive:enabled error:&error];
|
|
161
168
|
|
|
@@ -191,9 +198,9 @@ RCT_EXPORT_METHOD(
|
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
RCT_EXPORT_METHOD(
|
|
194
|
-
setAudioSessionOptions : (NSString *)category mode : (NSString *)mode options : (
|
|
195
|
-
options allowHaptics : (BOOL)allowHaptics notifyOthersOnDeactivation : (BOOL)
|
|
196
|
-
|
|
201
|
+
setAudioSessionOptions : (NSString *)category mode : (NSString *)mode options : (
|
|
202
|
+
NSArray *)options allowHaptics : (BOOL)allowHaptics notifyOthersOnDeactivation : (BOOL)
|
|
203
|
+
notifyOthersOnDeactivation)
|
|
197
204
|
{
|
|
198
205
|
if (!self.audioSessionManager.shouldManageSession) {
|
|
199
206
|
[self.audioSessionManager setShouldManageSession:true];
|
|
@@ -222,8 +229,8 @@ RCT_EXPORT_METHOD(observeVolumeChanges : (BOOL)enabled)
|
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
RCT_EXPORT_METHOD(
|
|
225
|
-
requestRecordingPermissions : (nonnull RCTPromiseResolveBlock)
|
|
226
|
-
|
|
232
|
+
requestRecordingPermissions : (nonnull RCTPromiseResolveBlock)resolve reject : (
|
|
233
|
+
nonnull RCTPromiseRejectBlock)reject)
|
|
227
234
|
{
|
|
228
235
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
229
236
|
[self.audioSessionManager requestRecordingPermissions:resolve reject:reject];
|
|
@@ -231,8 +238,8 @@ RCT_EXPORT_METHOD(
|
|
|
231
238
|
}
|
|
232
239
|
|
|
233
240
|
RCT_EXPORT_METHOD(
|
|
234
|
-
checkRecordingPermissions : (nonnull RCTPromiseResolveBlock)
|
|
235
|
-
|
|
241
|
+
checkRecordingPermissions : (nonnull RCTPromiseResolveBlock)resolve reject : (
|
|
242
|
+
nonnull RCTPromiseRejectBlock)reject)
|
|
236
243
|
{
|
|
237
244
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
238
245
|
[self.audioSessionManager checkRecordingPermissions:resolve reject:reject];
|
|
@@ -240,8 +247,8 @@ RCT_EXPORT_METHOD(
|
|
|
240
247
|
}
|
|
241
248
|
|
|
242
249
|
RCT_EXPORT_METHOD(
|
|
243
|
-
requestNotificationPermissions : (nonnull RCTPromiseResolveBlock)
|
|
244
|
-
|
|
250
|
+
requestNotificationPermissions : (nonnull RCTPromiseResolveBlock)resolve reject : (
|
|
251
|
+
nonnull RCTPromiseRejectBlock)reject)
|
|
245
252
|
{
|
|
246
253
|
// iOS doesn't require explicit notification permissions for media controls
|
|
247
254
|
// MPNowPlayingInfoCenter and MPRemoteCommandCenter work without permissions
|
|
@@ -250,8 +257,8 @@ RCT_EXPORT_METHOD(
|
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
RCT_EXPORT_METHOD(
|
|
253
|
-
checkNotificationPermissions : (nonnull RCTPromiseResolveBlock)
|
|
254
|
-
|
|
260
|
+
checkNotificationPermissions : (nonnull RCTPromiseResolveBlock)resolve reject : (
|
|
261
|
+
nonnull RCTPromiseRejectBlock)reject)
|
|
255
262
|
{
|
|
256
263
|
// iOS doesn't require explicit notification permissions for media controls
|
|
257
264
|
// Return 'Granted' to match the spec interface
|
|
@@ -259,8 +266,8 @@ RCT_EXPORT_METHOD(
|
|
|
259
266
|
}
|
|
260
267
|
|
|
261
268
|
RCT_EXPORT_METHOD(
|
|
262
|
-
getDevicesInfo : (nonnull RCTPromiseResolveBlock)
|
|
263
|
-
|
|
269
|
+
getDevicesInfo : (nonnull RCTPromiseResolveBlock)resolve reject : (
|
|
270
|
+
nonnull RCTPromiseRejectBlock)reject)
|
|
264
271
|
{
|
|
265
272
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
266
273
|
[self.audioSessionManager getDevicesInfo:resolve reject:reject];
|
|
@@ -268,8 +275,8 @@ RCT_EXPORT_METHOD(
|
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
RCT_EXPORT_METHOD(
|
|
271
|
-
setInputDevice : (NSString *)deviceId resolve : (RCTPromiseResolveBlock)
|
|
272
|
-
|
|
278
|
+
setInputDevice : (NSString *)deviceId resolve : (RCTPromiseResolveBlock)resolve reject : (
|
|
279
|
+
RCTPromiseRejectBlock)reject)
|
|
273
280
|
{
|
|
274
281
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
275
282
|
[self.audioSessionManager setInputDevice:deviceId resolve:resolve reject:reject];
|
|
@@ -300,8 +307,8 @@ RCT_EXPORT_METHOD(
|
|
|
300
307
|
}
|
|
301
308
|
|
|
302
309
|
RCT_EXPORT_METHOD(
|
|
303
|
-
hideNotification : (NSString *)key resolve : (RCTPromiseResolveBlock)
|
|
304
|
-
|
|
310
|
+
hideNotification : (NSString *)key resolve : (RCTPromiseResolveBlock)resolve reject : (
|
|
311
|
+
RCTPromiseRejectBlock)reject)
|
|
305
312
|
{
|
|
306
313
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
307
314
|
BOOL success = [self.notificationRegistry hideNotificationWithKey:key];
|
|
@@ -315,8 +322,8 @@ RCT_EXPORT_METHOD(
|
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
RCT_EXPORT_METHOD(
|
|
318
|
-
isNotificationActive : (NSString *)key resolve : (RCTPromiseResolveBlock)
|
|
319
|
-
|
|
325
|
+
isNotificationActive : (NSString *)key resolve : (RCTPromiseResolveBlock)resolve reject : (
|
|
326
|
+
RCTPromiseRejectBlock)reject)
|
|
320
327
|
{
|
|
321
328
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
322
329
|
BOOL isActive = [self.notificationRegistry isNotificationActiveWithKey:key];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#import <AudioEngine.h>
|
|
3
3
|
#import <AudioSessionManager.h>
|
|
4
4
|
#import <Foundation/Foundation.h>
|
|
5
|
+
#include <algorithm>
|
|
5
6
|
#include <mutex>
|
|
6
7
|
#include <unordered_map>
|
|
7
8
|
#include <vector>
|
|
@@ -93,7 +94,9 @@ void IOSAudioRecorder::runSideEffects(const AudioBufferList *inputBuffer, int nu
|
|
|
93
94
|
|
|
94
95
|
if (isConnected()) {
|
|
95
96
|
if (auto lock = Locker::tryLock(adapterNodeMutex_)) {
|
|
96
|
-
|
|
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) {
|
|
97
100
|
auto *data = static_cast<float *>(inputBuffer->mBuffers[channel].mData);
|
|
98
101
|
adapterNode_->buff_[channel]->write(data, numFrames);
|
|
99
102
|
}
|
|
@@ -204,7 +207,6 @@ Result<NoneType, std::string> IOSAudioRecorder::reprepareFileWriter(
|
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
filePath_ = result.unwrap();
|
|
207
|
-
recordingSegmentPaths_.push_back(filePath_);
|
|
208
210
|
fileOutputConfigured_.store(true, std::memory_order_release);
|
|
209
211
|
return Result<NoneType, std::string>::Ok(None);
|
|
210
212
|
}
|
|
@@ -237,6 +239,8 @@ void IOSAudioRecorder::reprepareAdapter(AVAudioFormat *inputFormat, int maxInput
|
|
|
237
239
|
return;
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
// init() is a no-op on an initialized adapter, so the old format has to be torn down first.
|
|
243
|
+
adapterNode_->adapterCleanup();
|
|
240
244
|
adapterNode_->init(
|
|
241
245
|
static_cast<size_t>(maxInputBufferLength),
|
|
242
246
|
recorderFormatChannelCount(inputFormat),
|
|
@@ -407,16 +411,16 @@ AudioRecorder::StopResult IOSAudioRecorder::stop()
|
|
|
407
411
|
std::shared_ptr<RecorderAdapterNode> adapterNode;
|
|
408
412
|
std::vector<std::string> outputPaths;
|
|
409
413
|
|
|
410
|
-
{
|
|
411
|
-
|
|
414
|
+
if (isIdle()) {
|
|
415
|
+
return StopResult::Err("Recorder is not in recording state.");
|
|
416
|
+
}
|
|
412
417
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
418
|
+
state_.store(RecorderState::Idle, std::memory_order_release);
|
|
419
|
+
[nativeRecorder_ setInputArmed:false];
|
|
420
|
+
[nativeRecorder_ stop];
|
|
416
421
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
[nativeRecorder_ stop];
|
|
422
|
+
{
|
|
423
|
+
std::scoped_lock stopLock(callbackMutex_, fileWriterMutex_, adapterNodeMutex_);
|
|
420
424
|
|
|
421
425
|
if (usesFileOutput()) {
|
|
422
426
|
fileOutputConfigured_.store(false, std::memory_order_release);
|
|
@@ -443,34 +447,25 @@ AudioRecorder::StopResult IOSAudioRecorder::stop()
|
|
|
443
447
|
}
|
|
444
448
|
|
|
445
449
|
/// @brief Enables file output for the recorder with specified properties.
|
|
446
|
-
///
|
|
450
|
+
/// The file itself is created by the next start(). An active (recording or paused) session keeps
|
|
451
|
+
/// the output it started with, so calling this during a session fails and changes nothing.
|
|
447
452
|
/// This method should be called from the JS thread only.
|
|
448
453
|
/// @param properties Shared pointer to AudioFileProperties defining the output file format.
|
|
449
|
-
/// @returns
|
|
454
|
+
/// @returns Ok when the properties were stored, otherwise an error message.
|
|
450
455
|
Result<NoneType, std::string> IOSAudioRecorder::enableFileOutput(
|
|
451
456
|
std::shared_ptr<AudioFileProperties> properties)
|
|
452
457
|
{
|
|
453
458
|
std::scoped_lock lock(fileWriterMutex_, errorCallbackMutex_);
|
|
454
|
-
fileProperties_ = properties;
|
|
455
|
-
fileOutputEnabled_.store(true, std::memory_order_release);
|
|
456
|
-
fileOutputConfigured_.store(false, std::memory_order_release);
|
|
457
459
|
|
|
458
460
|
if (!isIdle()) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (!hasUsableRecorderFormat(resolvedInputFormat) || resolvedBufferSize <= 0) {
|
|
463
|
-
return Result<NoneType, std::string>::Err(
|
|
464
|
-
"Failed to open file for writing: recorder input format is unavailable");
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
auto writerResult = setupFileWriter(properties);
|
|
468
|
-
if (writerResult.is_err()) {
|
|
469
|
-
fileOutputEnabled_.store(false, std::memory_order_release);
|
|
470
|
-
return Result<NoneType, std::string>::Err(writerResult.unwrap_err());
|
|
471
|
-
}
|
|
461
|
+
return Result<NoneType, std::string>::Err(
|
|
462
|
+
"File output cannot be changed while a recording session is active");
|
|
472
463
|
}
|
|
473
464
|
|
|
465
|
+
fileProperties_ = properties;
|
|
466
|
+
fileOutputEnabled_.store(true, std::memory_order_release);
|
|
467
|
+
fileOutputConfigured_.store(false, std::memory_order_release);
|
|
468
|
+
|
|
474
469
|
return Result<NoneType, std::string>::Ok(None);
|
|
475
470
|
}
|
|
476
471
|
|
|
@@ -565,8 +560,11 @@ void IOSAudioRecorder::resume()
|
|
|
565
560
|
return;
|
|
566
561
|
}
|
|
567
562
|
|
|
568
|
-
[nativeRecorder_ resume];
|
|
569
563
|
state_.store(RecorderState::Recording, std::memory_order_release);
|
|
564
|
+
|
|
565
|
+
if (![nativeRecorder_ resume]) {
|
|
566
|
+
state_.store(RecorderState::Paused, std::memory_order_release);
|
|
567
|
+
}
|
|
570
568
|
}
|
|
571
569
|
|
|
572
570
|
/// @brief Checks if the recorder is currently recording.
|
|
@@ -164,18 +164,20 @@ static inline uint32_t nextPowerOfTwo(uint32_t x)
|
|
|
164
164
|
[audioEngine pauseIfNecessary];
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
- (
|
|
167
|
+
- (BOOL)resume
|
|
168
168
|
{
|
|
169
169
|
AudioEngine *audioEngine = [AudioEngine sharedInstance];
|
|
170
170
|
assert(audioEngine != nil);
|
|
171
171
|
|
|
172
|
-
if ([audioEngine startIfNecessary]) {
|
|
173
|
-
|
|
174
|
-
self.onInputConfigurationChange();
|
|
175
|
-
} else {
|
|
176
|
-
self.inputArmed = YES;
|
|
177
|
-
}
|
|
172
|
+
if (![audioEngine startIfNecessary]) {
|
|
173
|
+
return NO;
|
|
178
174
|
}
|
|
175
|
+
|
|
176
|
+
if (self.onInputConfigurationChange != nil) {
|
|
177
|
+
self.onInputConfigurationChange();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return YES;
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
- (void)cleanup
|
|
@@ -166,13 +166,26 @@ NSURL *getFileURL(
|
|
|
166
166
|
NSString *timestamp = getTimestampString();
|
|
167
167
|
NSString *fileExtension = getFileExtension(properties);
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
if (fileNameOverride.length() > 0 && properties->rotateIntervalBytes == 0) {
|
|
170
|
+
NSString *fileName =
|
|
171
|
+
[NSString stringWithFormat:@"%@.%@",
|
|
171
172
|
[NSString stringWithUTF8String:fileNameOverride.c_str()],
|
|
172
|
-
fileExtension]
|
|
173
|
-
|
|
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
|
|
188
|
+
return fileURL;
|
|
176
189
|
}
|
|
177
190
|
|
|
178
191
|
NSSearchPathDirectory getDirectory(const std::shared_ptr<AudioFileProperties> &properties)
|
|
@@ -67,6 +67,11 @@ class IOSFileWriter : public AudioFileWriter {
|
|
|
67
67
|
private:
|
|
68
68
|
using FreeList = slots::SlotFreeList<FILE_WRITER_POOL_SIZE>;
|
|
69
69
|
|
|
70
|
+
Result<NoneType, std::string> prepareConversionPipeline(
|
|
71
|
+
AVAudioFormat *bufferFormat,
|
|
72
|
+
size_t maxInputBufferLength);
|
|
73
|
+
void releaseConversionPipeline();
|
|
74
|
+
|
|
70
75
|
std::vector<ios::OwnedAudioBufferListPtr> inputBufferPool_;
|
|
71
76
|
size_t inputBufferBytesPerBuffer_{0};
|
|
72
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
|
-
|
|
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
|
-
|
|
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,39 +151,53 @@ OpenFileResult IOSFileWriter::openFile(
|
|
|
135
151
|
freeSlots_ = std::make_unique<FreeList>();
|
|
136
152
|
freeSlots_->seed();
|
|
137
153
|
|
|
138
|
-
|
|
139
|
-
return OpenFileResult::Ok([[fileURL_ path] UTF8String]);
|
|
154
|
+
return Result<NoneType, std::string>::Ok(None);
|
|
140
155
|
}
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
/// @brief Tears down the converter, conversion buffers and offloader, draining pending writes.
|
|
159
|
+
void IOSFileWriter::releaseConversionPipeline()
|
|
160
|
+
{
|
|
161
|
+
offloader_.reset();
|
|
162
|
+
freeSlots_.reset();
|
|
163
|
+
inputBufferPool_.clear();
|
|
164
|
+
inputBufferBytesPerBuffer_ = 0;
|
|
165
|
+
|
|
166
|
+
converter_ = nil;
|
|
167
|
+
converterInputBuffer_ = nil;
|
|
168
|
+
converterOutputBuffer_ = nil;
|
|
169
|
+
}
|
|
170
|
+
|
|
143
171
|
OpenFileResult IOSFileWriter::reopenForInputFormatChange(
|
|
144
172
|
AVAudioFormat *bufferFormat,
|
|
145
173
|
size_t maxInputBufferLength)
|
|
146
174
|
{
|
|
147
175
|
@autoreleasepool {
|
|
148
|
-
if (isFileOpen()) {
|
|
149
|
-
|
|
150
|
-
if (closeResult.is_err()) {
|
|
151
|
-
return OpenFileResult::Err(
|
|
152
|
-
"Failed to finalize recording segment: " + closeResult.unwrap_err());
|
|
153
|
-
}
|
|
176
|
+
if (!isFileOpen() || audioFile_ == nil) {
|
|
177
|
+
return openFile(bufferFormat, maxInputBufferLength, "");
|
|
154
178
|
}
|
|
155
179
|
|
|
156
|
-
|
|
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]);
|
|
157
193
|
}
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
void IOSFileWriter::rollbackFailedOpen()
|
|
161
197
|
{
|
|
162
|
-
|
|
163
|
-
freeSlots_.reset();
|
|
164
|
-
inputBufferPool_.clear();
|
|
165
|
-
inputBufferBytesPerBuffer_ = 0;
|
|
198
|
+
releaseConversionPipeline();
|
|
166
199
|
|
|
167
200
|
audioFile_ = nil;
|
|
168
|
-
converter_ = nil;
|
|
169
|
-
converterInputBuffer_ = nil;
|
|
170
|
-
converterOutputBuffer_ = nil;
|
|
171
201
|
bufferFormat_ = nil;
|
|
172
202
|
|
|
173
203
|
if (fileURL_ != nil) {
|
|
@@ -58,13 +58,8 @@ OpenFileResult IOSRotatingFileWriter::reprepareStreamFormat(
|
|
|
58
58
|
return openInnerWriter();
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (currentWriter_ == nullptr) {
|
|
64
|
-
return OpenFileResult::Err("Failed to reopen file for writing after input format change");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return OpenFileResult::Ok(currentWriter_->getFilePath());
|
|
61
|
+
auto inner = std::static_pointer_cast<IOSFileWriter>(currentWriter_);
|
|
62
|
+
return inner->reopenForInputFormatChange(streamFormat_, streamMaxBufferSizeInFrames_);
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
void IOSRotatingFileWriter::writeAudioData(AudioDataType data, int numFrames)
|
|
@@ -23,6 +23,7 @@ static NSString *NotificationManagerContext = @"SystemNotificationManagerContext
|
|
|
23
23
|
|
|
24
24
|
- (void)cleanup
|
|
25
25
|
{
|
|
26
|
+
[self observeVolumeChanges:NO];
|
|
26
27
|
self.notificationCenter = nil;
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -64,7 +65,9 @@ static NSString *NotificationManagerContext = @"SystemNotificationManagerContext
|
|
|
64
65
|
options:NSKeyValueObservingOptionNew
|
|
65
66
|
context:(void *)&NotificationManagerContext];
|
|
66
67
|
} else {
|
|
67
|
-
[[AVAudioSession sharedInstance] removeObserver:self
|
|
68
|
+
[[AVAudioSession sharedInstance] removeObserver:self
|
|
69
|
+
forKeyPath:@"outputVolume"
|
|
70
|
+
context:(void *)&NotificationManagerContext];
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
self.volumeChangesObserved = enabled;
|
|
@@ -34,11 +34,19 @@ class AudioRecorder {
|
|
|
34
34
|
constructor() {
|
|
35
35
|
this.recorder = globalThis.createAudioRecorder();
|
|
36
36
|
}
|
|
37
|
+
static isRecordingOngoing() {
|
|
38
|
+
return globalThis.isRecordingOngoing?.() ?? false;
|
|
39
|
+
}
|
|
40
|
+
static consumeLastRecordingResult() {
|
|
41
|
+
return globalThis.consumeLastRecordingResult?.() ?? null;
|
|
42
|
+
}
|
|
37
43
|
enableFileOutput(options) {
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const requestedOptions = options || {};
|
|
45
|
+
const result = this.recorder.enableFileOutput(withDefaultOptions(requestedOptions));
|
|
46
|
+
if (result.status === 'success') {
|
|
47
|
+
this.options_ = requestedOptions;
|
|
48
|
+
this.isFileOutputEnabled = true;
|
|
49
|
+
}
|
|
42
50
|
return result;
|
|
43
51
|
}
|
|
44
52
|
get options() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_errors","require","_events","_types","_filePresets","_interopRequireDefault","_AudioBuffer","e","__esModule","default","withDefaultOptions","inOptions","directory","FileDirectory","Cache","subDirectory","fileNamePrefix","channelCount","format","FileFormat","M4A","preset","FilePreset","High","androidFlushIntervalMs","rotateIntervalBytes","AudioRecorder","onAudioReadySubscription","onErrorSubscription","options_","isFileOutputEnabled","audioEventEmitter","AudioEventEmitter","globalThis","constructor","recorder","createAudioRecorder","enableFileOutput","options","
|
|
1
|
+
{"version":3,"names":["_errors","require","_events","_types","_filePresets","_interopRequireDefault","_AudioBuffer","e","__esModule","default","withDefaultOptions","inOptions","directory","FileDirectory","Cache","subDirectory","fileNamePrefix","channelCount","format","FileFormat","M4A","preset","FilePreset","High","androidFlushIntervalMs","rotateIntervalBytes","AudioRecorder","onAudioReadySubscription","onErrorSubscription","options_","isFileOutputEnabled","audioEventEmitter","AudioEventEmitter","globalThis","constructor","recorder","createAudioRecorder","isRecordingOngoing","consumeLastRecordingResult","enableFileOutput","options","requestedOptions","result","status","disableFileOutput","start","fileNameOverride","stop","pause","resume","connect","node","wasConnected","AudioApiError","getNode","disconnect","onAudioReady","callback","clearOnAudioReady","remove","addAudioEventListener","event","audioBuffer","AudioBuffer","buffer","setOnAudioReady","sampleRate","bufferLength","callbackId","subscriptionId","isRecording","isPaused","getCurrentDuration","onError","clearOnError","setOnError","exports"],"sourceRoot":"../../../src","sources":["core/AudioRecorder.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -652,6 +652,7 @@ class OfflineAudioContextMock extends BaseAudioContextMock {
|
|
|
652
652
|
}
|
|
653
653
|
}
|
|
654
654
|
class AudioRecorderMock {
|
|
655
|
+
static lastCreated = null;
|
|
655
656
|
_isRecording = false;
|
|
656
657
|
_isPaused = false;
|
|
657
658
|
_currentDuration = 0;
|
|
@@ -660,7 +661,16 @@ class AudioRecorderMock {
|
|
|
660
661
|
eventEmitter = new MockAudioEventEmitter();
|
|
661
662
|
onAudioReadySubscription = null;
|
|
662
663
|
onErrorSubscription = null;
|
|
663
|
-
constructor() {
|
|
664
|
+
constructor() {
|
|
665
|
+
AudioRecorderMock.lastCreated = this;
|
|
666
|
+
}
|
|
667
|
+
static isRecordingOngoing() {
|
|
668
|
+
const recorder = AudioRecorderMock.lastCreated;
|
|
669
|
+
return recorder != null && (recorder._isRecording || recorder._isPaused);
|
|
670
|
+
}
|
|
671
|
+
static consumeLastRecordingResult() {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
664
674
|
enableFileOutput(options) {
|
|
665
675
|
this._options = options || {};
|
|
666
676
|
this.isFileOutputEnabled = true;
|