react-native-suuqencode 0.1.5 → 0.1.7
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/Suuqencode.podspec +1 -1
- package/ios/Suuqencode.h +1 -0
- package/ios/Suuqencode.mm +333 -7
- package/lib/module/NativeSuuqencode.js.map +1 -1
- package/lib/module/index.js +40 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeSuuqencode.d.ts +2 -0
- package/lib/typescript/src/NativeSuuqencode.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +25 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeSuuqencode.ts +2 -0
- package/src/index.tsx +58 -0
package/Suuqencode.podspec
CHANGED
|
@@ -15,7 +15,7 @@ Pod::Spec.new do |s|
|
|
|
15
15
|
|
|
16
16
|
s.source_files = "ios/**/*.{h,m,mm,cpp}"
|
|
17
17
|
s.private_header_files = "ios/**/*.h"
|
|
18
|
-
s.frameworks = "VideoToolbox", "CoreMedia", "CoreFoundation"
|
|
18
|
+
s.frameworks = "VideoToolbox", "CoreMedia", "CoreFoundation", "AudioToolbox", "AVFoundation"
|
|
19
19
|
s.dependency "SuuqeDMABuf"
|
|
20
20
|
|
|
21
21
|
install_modules_dependencies(s)
|
package/ios/Suuqencode.h
CHANGED
package/ios/Suuqencode.mm
CHANGED
|
@@ -3,10 +3,21 @@
|
|
|
3
3
|
|
|
4
4
|
@interface Suuqencode ()
|
|
5
5
|
|
|
6
|
+
// Video encoding properties
|
|
6
7
|
@property(nonatomic) VTCompressionSessionRef compressionSession;
|
|
7
8
|
@property(nonatomic) dispatch_queue_t encodeQueue;
|
|
8
9
|
@property(nonatomic) int frameCount;
|
|
9
10
|
|
|
11
|
+
// Audio recording + FLAC encoding properties
|
|
12
|
+
@property(nonatomic, strong) AVAudioEngine *audioEngine;
|
|
13
|
+
@property(nonatomic, strong) AVAudioConverter *pcmConverter;
|
|
14
|
+
@property(nonatomic, strong) AVAudioConverter *flacConverter;
|
|
15
|
+
@property(nonatomic, strong) AVAudioFormat *targetPCMFormat;
|
|
16
|
+
@property(nonatomic, strong) AVAudioFormat *flacFormat;
|
|
17
|
+
@property(nonatomic) dispatch_queue_t audioEncodeQueue;
|
|
18
|
+
@property(nonatomic) BOOL isRecordingAudio;
|
|
19
|
+
@property(nonatomic) double audioSampleRate;
|
|
20
|
+
|
|
10
21
|
- (void)sendEncodedData:(NSData *)data;
|
|
11
22
|
|
|
12
23
|
@end
|
|
@@ -20,6 +31,8 @@ RCT_EXPORT_MODULE()
|
|
|
20
31
|
if (self) {
|
|
21
32
|
_encodeQueue = dispatch_queue_create("com.suuqencode.encodequeue",
|
|
22
33
|
DISPATCH_QUEUE_SERIAL);
|
|
34
|
+
_audioEncodeQueue = dispatch_queue_create("com.suuqencode.audioencodequeue",
|
|
35
|
+
DISPATCH_QUEUE_SERIAL);
|
|
23
36
|
}
|
|
24
37
|
return self;
|
|
25
38
|
}
|
|
@@ -108,6 +121,8 @@ void compressionOutputCallback(void *outputCallbackRefCon,
|
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
Suuqencode *encoder = (__bridge Suuqencode *)outputCallbackRefCon;
|
|
124
|
+
const uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
|
|
125
|
+
size_t startCodeSize = sizeof(startCode);
|
|
111
126
|
|
|
112
127
|
bool isKeyFrame = !CFDictionaryContainsKey(
|
|
113
128
|
(CFDictionaryRef)CFArrayGetValueAtIndex(
|
|
@@ -127,11 +142,15 @@ void compressionOutputCallback(void *outputCallbackRefCon,
|
|
|
127
142
|
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
|
128
143
|
format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
145
|
+
NSMutableData *spsData = [NSMutableData dataWithBytes:startCode
|
|
146
|
+
length:startCodeSize];
|
|
147
|
+
[spsData appendBytes:sparameterSet length:sparameterSetSize];
|
|
148
|
+
[encoder sendEncodedData:spsData];
|
|
132
149
|
|
|
133
|
-
[
|
|
134
|
-
|
|
150
|
+
NSMutableData *ppsData = [NSMutableData dataWithBytes:startCode
|
|
151
|
+
length:startCodeSize];
|
|
152
|
+
[ppsData appendBytes:pparameterSet length:pparameterSetSize];
|
|
153
|
+
[encoder sendEncodedData:ppsData];
|
|
135
154
|
}
|
|
136
155
|
|
|
137
156
|
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
|
|
@@ -140,8 +159,26 @@ void compressionOutputCallback(void *outputCallbackRefCon,
|
|
|
140
159
|
CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength,
|
|
141
160
|
&dataPointer);
|
|
142
161
|
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
// Parse AVCC NAL units and convert to Annex B
|
|
163
|
+
size_t bufferOffset = 0;
|
|
164
|
+
static const int AVCCHeaderLength = 4;
|
|
165
|
+
|
|
166
|
+
while (bufferOffset < totalLength - AVCCHeaderLength) {
|
|
167
|
+
uint32_t NALUnitLength = 0;
|
|
168
|
+
memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
|
|
169
|
+
|
|
170
|
+
// Convert big-endian length to host endianness
|
|
171
|
+
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
|
|
172
|
+
|
|
173
|
+
NSMutableData *naluData = [NSMutableData dataWithBytes:startCode
|
|
174
|
+
length:startCodeSize];
|
|
175
|
+
[naluData appendBytes:(dataPointer + bufferOffset + AVCCHeaderLength)
|
|
176
|
+
length:NALUnitLength];
|
|
177
|
+
|
|
178
|
+
[encoder sendEncodedData:naluData];
|
|
179
|
+
|
|
180
|
+
bufferOffset += AVCCHeaderLength + NALUnitLength;
|
|
181
|
+
}
|
|
145
182
|
}
|
|
146
183
|
|
|
147
184
|
- (void)sendEncodedData:(NSData *)data {
|
|
@@ -149,8 +186,297 @@ void compressionOutputCallback(void *outputCallbackRefCon,
|
|
|
149
186
|
[self sendEventWithName:@"onEncodedData" body:base64Encoded];
|
|
150
187
|
}
|
|
151
188
|
|
|
189
|
+
#pragma mark - Audio Recording & FLAC Encoding
|
|
190
|
+
|
|
191
|
+
RCT_EXPORT_METHOD(startAudioEncode:(double)sampleRate
|
|
192
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
193
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
194
|
+
if (self.isRecordingAudio) {
|
|
195
|
+
reject(@"ALREADY_RECORDING", @"Audio recording is already in progress", nil);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
self.audioSampleRate = sampleRate > 0 ? sampleRate : 16000.0;
|
|
200
|
+
|
|
201
|
+
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
202
|
+
[session requestRecordPermission:^(BOOL granted) {
|
|
203
|
+
if (!granted) {
|
|
204
|
+
reject(@"PERMISSION_DENIED", @"Microphone permission was denied", nil);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
209
|
+
NSError *setupError = nil;
|
|
210
|
+
BOOL success = [self setupAudioCaptureWithError:&setupError];
|
|
211
|
+
if (!success) {
|
|
212
|
+
reject(@"SETUP_FAILED",
|
|
213
|
+
setupError ? setupError.localizedDescription : @"Failed to set up audio capture",
|
|
214
|
+
setupError);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Emit format info so the receiver knows the audio parameters
|
|
219
|
+
NSDictionary *formatInfo = @{
|
|
220
|
+
@"sampleRate": @(self.audioSampleRate),
|
|
221
|
+
@"channels": @(1),
|
|
222
|
+
@"bitsPerSample": @(16),
|
|
223
|
+
@"codec": @"flac"
|
|
224
|
+
};
|
|
225
|
+
[self sendEventWithName:@"onAudioFormatInfo" body:formatInfo];
|
|
226
|
+
resolve(@(YES));
|
|
227
|
+
});
|
|
228
|
+
}];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
RCT_EXPORT_METHOD(stopAudioEncode) {
|
|
232
|
+
if (!self.isRecordingAudio) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
self.isRecordingAudio = NO;
|
|
237
|
+
|
|
238
|
+
if (self.audioEngine) {
|
|
239
|
+
[self.audioEngine.inputNode removeTapOnBus:0];
|
|
240
|
+
[self.audioEngine stop];
|
|
241
|
+
self.audioEngine = nil;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
self.pcmConverter = nil;
|
|
245
|
+
self.flacConverter = nil;
|
|
246
|
+
self.targetPCMFormat = nil;
|
|
247
|
+
self.flacFormat = nil;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
- (BOOL)setupAudioCaptureWithError:(NSError **)outError {
|
|
251
|
+
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
252
|
+
NSError *error = nil;
|
|
253
|
+
|
|
254
|
+
[session setCategory:AVAudioSessionCategoryPlayAndRecord
|
|
255
|
+
withOptions:(AVAudioSessionCategoryOptionDefaultToSpeaker |
|
|
256
|
+
AVAudioSessionCategoryOptionAllowBluetooth)
|
|
257
|
+
error:&error];
|
|
258
|
+
if (error) {
|
|
259
|
+
if (outError) *outError = error;
|
|
260
|
+
return NO;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
[session setActive:YES error:&error];
|
|
264
|
+
if (error) {
|
|
265
|
+
if (outError) *outError = error;
|
|
266
|
+
return NO;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Initialize audio engine
|
|
270
|
+
self.audioEngine = [[AVAudioEngine alloc] init];
|
|
271
|
+
AVAudioInputNode *inputNode = self.audioEngine.inputNode;
|
|
272
|
+
AVAudioFormat *hardwareFormat = [inputNode outputFormatForBus:0];
|
|
273
|
+
|
|
274
|
+
if (!hardwareFormat || hardwareFormat.sampleRate == 0) {
|
|
275
|
+
if (outError) {
|
|
276
|
+
*outError = [NSError errorWithDomain:@"SuuqencodeAudio"
|
|
277
|
+
code:-1
|
|
278
|
+
userInfo:@{NSLocalizedDescriptionKey: @"Could not get hardware audio format"}];
|
|
279
|
+
}
|
|
280
|
+
return NO;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Target PCM format: 16-bit signed integer, mono, at requested sample rate
|
|
284
|
+
AudioStreamBasicDescription pcmASBD = {};
|
|
285
|
+
pcmASBD.mFormatID = kAudioFormatLinearPCM;
|
|
286
|
+
pcmASBD.mSampleRate = self.audioSampleRate;
|
|
287
|
+
pcmASBD.mChannelsPerFrame = 1;
|
|
288
|
+
pcmASBD.mBitsPerChannel = 16;
|
|
289
|
+
pcmASBD.mBytesPerFrame = 2;
|
|
290
|
+
pcmASBD.mBytesPerPacket = 2;
|
|
291
|
+
pcmASBD.mFramesPerPacket = 1;
|
|
292
|
+
pcmASBD.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
|
293
|
+
|
|
294
|
+
self.targetPCMFormat = [[AVAudioFormat alloc] initWithStreamDescription:&pcmASBD];
|
|
295
|
+
if (!self.targetPCMFormat) {
|
|
296
|
+
if (outError) {
|
|
297
|
+
*outError = [NSError errorWithDomain:@"SuuqencodeAudio"
|
|
298
|
+
code:-2
|
|
299
|
+
userInfo:@{NSLocalizedDescriptionKey: @"Could not create target PCM format"}];
|
|
300
|
+
}
|
|
301
|
+
return NO;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// FLAC output format
|
|
305
|
+
AudioStreamBasicDescription flacASBD = {};
|
|
306
|
+
flacASBD.mFormatID = kAudioFormatFLAC;
|
|
307
|
+
flacASBD.mSampleRate = self.audioSampleRate;
|
|
308
|
+
flacASBD.mChannelsPerFrame = 1;
|
|
309
|
+
flacASBD.mBitsPerChannel = 16;
|
|
310
|
+
flacASBD.mFramesPerPacket = 0;
|
|
311
|
+
flacASBD.mBytesPerPacket = 0;
|
|
312
|
+
flacASBD.mBytesPerFrame = 0;
|
|
313
|
+
flacASBD.mFormatFlags = 0;
|
|
314
|
+
|
|
315
|
+
self.flacFormat = [[AVAudioFormat alloc] initWithStreamDescription:&flacASBD];
|
|
316
|
+
if (!self.flacFormat) {
|
|
317
|
+
if (outError) {
|
|
318
|
+
*outError = [NSError errorWithDomain:@"SuuqencodeAudio"
|
|
319
|
+
code:-3
|
|
320
|
+
userInfo:@{NSLocalizedDescriptionKey: @"FLAC audio format not available on this device"}];
|
|
321
|
+
}
|
|
322
|
+
return NO;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// PCM resampler: hardware format → target PCM
|
|
326
|
+
self.pcmConverter = [[AVAudioConverter alloc] initFromFormat:hardwareFormat
|
|
327
|
+
toFormat:self.targetPCMFormat];
|
|
328
|
+
if (!self.pcmConverter) {
|
|
329
|
+
if (outError) {
|
|
330
|
+
*outError = [NSError errorWithDomain:@"SuuqencodeAudio"
|
|
331
|
+
code:-4
|
|
332
|
+
userInfo:@{NSLocalizedDescriptionKey: @"Could not create PCM resampler"}];
|
|
333
|
+
}
|
|
334
|
+
return NO;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// FLAC encoder: target PCM → FLAC
|
|
338
|
+
self.flacConverter = [[AVAudioConverter alloc] initFromFormat:self.targetPCMFormat
|
|
339
|
+
toFormat:self.flacFormat];
|
|
340
|
+
if (!self.flacConverter) {
|
|
341
|
+
if (outError) {
|
|
342
|
+
*outError = [NSError errorWithDomain:@"SuuqencodeAudio"
|
|
343
|
+
code:-5
|
|
344
|
+
userInfo:@{NSLocalizedDescriptionKey: @"Could not create FLAC encoder — FLAC encoding may not be supported on this OS version"}];
|
|
345
|
+
}
|
|
346
|
+
return NO;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Install tap on the input node using the hardware's native format
|
|
350
|
+
__weak Suuqencode *weakSelf = self;
|
|
351
|
+
[inputNode installTapOnBus:0
|
|
352
|
+
bufferSize:4096
|
|
353
|
+
format:hardwareFormat
|
|
354
|
+
block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
|
|
355
|
+
__strong Suuqencode *strongSelf = weakSelf;
|
|
356
|
+
if (!strongSelf || !strongSelf.isRecordingAudio) return;
|
|
357
|
+
|
|
358
|
+
// Copy the buffer so we can safely dispatch off the realtime audio thread
|
|
359
|
+
AVAudioPCMBuffer *bufferCopy = [[AVAudioPCMBuffer alloc] initWithPCMFormat:buffer.format
|
|
360
|
+
frameCapacity:buffer.frameLength];
|
|
361
|
+
bufferCopy.frameLength = buffer.frameLength;
|
|
362
|
+
|
|
363
|
+
if (hardwareFormat.commonFormat == AVAudioPCMFormatFloat32) {
|
|
364
|
+
for (AVAudioChannelCount ch = 0; ch < hardwareFormat.channelCount; ch++) {
|
|
365
|
+
memcpy(bufferCopy.floatChannelData[ch],
|
|
366
|
+
buffer.floatChannelData[ch],
|
|
367
|
+
buffer.frameLength * sizeof(float));
|
|
368
|
+
}
|
|
369
|
+
} else if (hardwareFormat.commonFormat == AVAudioPCMFormatInt16) {
|
|
370
|
+
for (AVAudioChannelCount ch = 0; ch < hardwareFormat.channelCount; ch++) {
|
|
371
|
+
memcpy(bufferCopy.int16ChannelData[ch],
|
|
372
|
+
buffer.int16ChannelData[ch],
|
|
373
|
+
buffer.frameLength * sizeof(int16_t));
|
|
374
|
+
}
|
|
375
|
+
} else if (hardwareFormat.commonFormat == AVAudioPCMFormatInt32) {
|
|
376
|
+
for (AVAudioChannelCount ch = 0; ch < hardwareFormat.channelCount; ch++) {
|
|
377
|
+
memcpy(bufferCopy.int32ChannelData[ch],
|
|
378
|
+
buffer.int32ChannelData[ch],
|
|
379
|
+
buffer.frameLength * sizeof(int32_t));
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
// Float64 or other — fallback to raw bytes
|
|
383
|
+
size_t byteCount = buffer.frameLength * hardwareFormat.streamDescription->mBytesPerFrame;
|
|
384
|
+
for (AVAudioChannelCount ch = 0; ch < hardwareFormat.channelCount; ch++) {
|
|
385
|
+
memcpy(bufferCopy.floatChannelData[ch],
|
|
386
|
+
buffer.floatChannelData[ch],
|
|
387
|
+
byteCount);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
dispatch_async(strongSelf.audioEncodeQueue, ^{
|
|
392
|
+
[strongSelf processAudioBuffer:bufferCopy];
|
|
393
|
+
});
|
|
394
|
+
}];
|
|
395
|
+
|
|
396
|
+
// Start the engine
|
|
397
|
+
[self.audioEngine startAndReturnError:&error];
|
|
398
|
+
if (error) {
|
|
399
|
+
if (outError) *outError = error;
|
|
400
|
+
[inputNode removeTapOnBus:0];
|
|
401
|
+
self.audioEngine = nil;
|
|
402
|
+
return NO;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
self.isRecordingAudio = YES;
|
|
406
|
+
return YES;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
- (void)processAudioBuffer:(AVAudioPCMBuffer *)inputBuffer {
|
|
410
|
+
if (!self.isRecordingAudio) return;
|
|
411
|
+
|
|
412
|
+
// Step 1: Resample hardware PCM → target PCM (16kHz mono int16)
|
|
413
|
+
double sampleRateRatio = self.targetPCMFormat.sampleRate / inputBuffer.format.sampleRate;
|
|
414
|
+
AVAudioFrameCount outputFrameCapacity = (AVAudioFrameCount)ceil(inputBuffer.frameLength * sampleRateRatio) + 16;
|
|
415
|
+
|
|
416
|
+
AVAudioPCMBuffer *pcmBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:self.targetPCMFormat
|
|
417
|
+
frameCapacity:outputFrameCapacity];
|
|
418
|
+
NSError *error = nil;
|
|
419
|
+
__block BOOL pcmInputProvided = NO;
|
|
420
|
+
|
|
421
|
+
AVAudioConverterOutputStatus pcmStatus = [self.pcmConverter
|
|
422
|
+
convertToBuffer:pcmBuffer
|
|
423
|
+
error:&error
|
|
424
|
+
withInputFromBlock:^AVAudioBuffer * _Nullable(AVAudioPacketCount inNumberOfPackets,
|
|
425
|
+
AVAudioConverterInputStatus *outStatus) {
|
|
426
|
+
if (pcmInputProvided) {
|
|
427
|
+
*outStatus = AVAudioConverterInputStatus_NoDataNow;
|
|
428
|
+
return nil;
|
|
429
|
+
}
|
|
430
|
+
pcmInputProvided = YES;
|
|
431
|
+
*outStatus = AVAudioConverterInputStatus_HaveData;
|
|
432
|
+
return inputBuffer;
|
|
433
|
+
}];
|
|
434
|
+
|
|
435
|
+
if (error) {
|
|
436
|
+
NSLog(@"[Suuqencode] PCM resampling error: %@", error);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (pcmBuffer.frameLength == 0) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Step 2: Encode PCM → FLAC
|
|
444
|
+
AVAudioCompressedBuffer *flacBuffer =
|
|
445
|
+
[[AVAudioCompressedBuffer alloc] initWithFormat:self.flacFormat
|
|
446
|
+
packetCapacity:8
|
|
447
|
+
maximumPacketSize:65536];
|
|
448
|
+
|
|
449
|
+
__block BOOL flacInputProvided = NO;
|
|
450
|
+
AVAudioConverterOutputStatus flacStatus = [self.flacConverter
|
|
451
|
+
convertToBuffer:flacBuffer
|
|
452
|
+
error:&error
|
|
453
|
+
withInputFromBlock:^AVAudioBuffer * _Nullable(AVAudioPacketCount inNumberOfPackets,
|
|
454
|
+
AVAudioConverterInputStatus *outStatus) {
|
|
455
|
+
if (flacInputProvided) {
|
|
456
|
+
*outStatus = AVAudioConverterInputStatus_NoDataNow;
|
|
457
|
+
return nil;
|
|
458
|
+
}
|
|
459
|
+
flacInputProvided = YES;
|
|
460
|
+
*outStatus = AVAudioConverterInputStatus_HaveData;
|
|
461
|
+
return pcmBuffer;
|
|
462
|
+
}];
|
|
463
|
+
|
|
464
|
+
if (error) {
|
|
465
|
+
NSLog(@"[Suuqencode] FLAC encoding error: %@", error);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if ((flacStatus == AVAudioConverterOutputStatus_HaveData ||
|
|
470
|
+
flacStatus == AVAudioConverterOutputStatus_InputRanDry) &&
|
|
471
|
+
flacBuffer.byteLength > 0) {
|
|
472
|
+
NSData *flacData = [NSData dataWithBytes:flacBuffer.data length:flacBuffer.byteLength];
|
|
473
|
+
NSString *base64 = [flacData base64EncodedStringWithOptions:0];
|
|
474
|
+
[self sendEventWithName:@"onAudioEncodedData" body:base64];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
152
478
|
- (NSArray<NSString *> *)supportedEvents {
|
|
153
|
-
return @[ @"onEncodedData" ];
|
|
479
|
+
return @[ @"onEncodedData", @"onAudioEncodedData", @"onAudioFormatInfo" ];
|
|
154
480
|
}
|
|
155
481
|
|
|
156
482
|
+ (BOOL)requiresMainQueueSetup {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSuuqencode.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSuuqencode.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;AAUpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,YAAY,CAAC","ignoreList":[]}
|
package/lib/module/index.js
CHANGED
|
@@ -16,4 +16,44 @@ export function addEncodedDataListener(callback) {
|
|
|
16
16
|
subscription.remove();
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Start recording from the microphone and encoding audio to FLAC in real time.
|
|
21
|
+
* Each encoded FLAC packet is base64-encoded and emitted via the onAudioEncodedData event.
|
|
22
|
+
* @param sampleRate Target sample rate in Hz (default: 16000)
|
|
23
|
+
* @returns Promise that resolves when recording has started successfully
|
|
24
|
+
*/
|
|
25
|
+
export function startAudioEncode(sampleRate = 16000) {
|
|
26
|
+
return Suuqencode.startAudioEncode(sampleRate);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stop audio recording and FLAC encoding.
|
|
31
|
+
*/
|
|
32
|
+
export function stopAudioEncode() {
|
|
33
|
+
Suuqencode.stopAudioEncode();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Listen for base64-encoded FLAC audio packets.
|
|
38
|
+
*/
|
|
39
|
+
export function addAudioDataListener(callback) {
|
|
40
|
+
const subscription = eventEmitter.addListener('onAudioEncodedData', data => {
|
|
41
|
+
callback(data);
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
subscription.remove();
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Listen for audio format info emitted once at the start of recording.
|
|
50
|
+
*/
|
|
51
|
+
export function addAudioFormatInfoListener(callback) {
|
|
52
|
+
const subscription = eventEmitter.addListener('onAudioFormatInfo', info => {
|
|
53
|
+
callback(info);
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
subscription.remove();
|
|
57
|
+
};
|
|
58
|
+
}
|
|
19
59
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NativeModules","NativeEventEmitter","Suuqencode","eventEmitter","startEncode","addEncodedDataListener","callback","subscription","addListener","data","remove"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,aAAa,EAAEC,kBAAkB,QAAQ,cAAc;AAEhE,MAAM;EAAEC;AAAW,CAAC,GAAGF,aAAa;AAEpC,MAAMG,YAAY,GAAG,IAAIF,kBAAkB,CAACC,UAAU,CAAC;AAEvD,OAAO,SAASE,WAAWA,CAAA,EAAS;EAClCF,UAAU,CAACE,WAAW,CAAC,CAAC;AAC1B;AAEA,OAAO,SAASC,sBAAsBA,CACpCC,QAAgC,EACpB;EACZ,MAAMC,YAAY,GAAGJ,YAAY,CAACK,WAAW,CAC3C,eAAe,EACdC,IAAS,IAAK;IACbH,QAAQ,CAACG,IAAc,CAAC;EAC1B,CACF,CAAC;EACD,OAAO,MAAM;IACXF,YAAY,CAACG,MAAM,CAAC,CAAC;EACvB,CAAC;AACH","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["NativeModules","NativeEventEmitter","Suuqencode","eventEmitter","startEncode","addEncodedDataListener","callback","subscription","addListener","data","remove","startAudioEncode","sampleRate","stopAudioEncode","addAudioDataListener","addAudioFormatInfoListener","info"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,aAAa,EAAEC,kBAAkB,QAAQ,cAAc;AAEhE,MAAM;EAAEC;AAAW,CAAC,GAAGF,aAAa;AAEpC,MAAMG,YAAY,GAAG,IAAIF,kBAAkB,CAACC,UAAU,CAAC;AAEvD,OAAO,SAASE,WAAWA,CAAA,EAAS;EAClCF,UAAU,CAACE,WAAW,CAAC,CAAC;AAC1B;AAEA,OAAO,SAASC,sBAAsBA,CACpCC,QAAgC,EACpB;EACZ,MAAMC,YAAY,GAAGJ,YAAY,CAACK,WAAW,CAC3C,eAAe,EACdC,IAAS,IAAK;IACbH,QAAQ,CAACG,IAAc,CAAC;EAC1B,CACF,CAAC;EACD,OAAO,MAAM;IACXF,YAAY,CAACG,MAAM,CAAC,CAAC;EACvB,CAAC;AACH;AASA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAACC,UAAkB,GAAG,KAAK,EAAoB;EAC7E,OAAOV,UAAU,CAACS,gBAAgB,CAACC,UAAU,CAAC;AAChD;;AAEA;AACA;AACA;AACA,OAAO,SAASC,eAAeA,CAAA,EAAS;EACtCX,UAAU,CAACW,eAAe,CAAC,CAAC;AAC9B;;AAEA;AACA;AACA;AACA,OAAO,SAASC,oBAAoBA,CAClCR,QAAgC,EACpB;EACZ,MAAMC,YAAY,GAAGJ,YAAY,CAACK,WAAW,CAC3C,oBAAoB,EACnBC,IAAS,IAAK;IACbH,QAAQ,CAACG,IAAc,CAAC;EAC1B,CACF,CAAC;EACD,OAAO,MAAM;IACXF,YAAY,CAACG,MAAM,CAAC,CAAC;EACvB,CAAC;AACH;;AAEA;AACA;AACA;AACA,OAAO,SAASK,0BAA0BA,CACxCT,QAAyC,EAC7B;EACZ,MAAMC,YAAY,GAAGJ,YAAY,CAACK,WAAW,CAC3C,mBAAmB,EAClBQ,IAAS,IAAK;IACbV,QAAQ,CAACU,IAAuB,CAAC;EACnC,CACF,CAAC;EACD,OAAO,MAAM;IACXT,YAAY,CAACG,MAAM,CAAC,CAAC;EACvB,CAAC;AACH","ignoreList":[]}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type TurboModule } from 'react-native';
|
|
2
2
|
export interface Spec extends TurboModule {
|
|
3
3
|
startEncode(): void;
|
|
4
|
+
startAudioEncode(sampleRate: number): Promise<boolean>;
|
|
5
|
+
stopAudioEncode(): void;
|
|
4
6
|
addListener(eventName: string): void;
|
|
5
7
|
removeListeners(count: number): void;
|
|
6
8
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NativeSuuqencode.d.ts","sourceRoot":"","sources":["../../../src/NativeSuuqencode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,WAAW,IAAI,IAAI,CAAC;IACpB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAAoE"}
|
|
1
|
+
{"version":3,"file":"NativeSuuqencode.d.ts","sourceRoot":"","sources":["../../../src/NativeSuuqencode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,WAAW,IAAI,IAAI,CAAC;IACpB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD,eAAe,IAAI,IAAI,CAAC;IACxB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAAoE"}
|
|
@@ -1,3 +1,28 @@
|
|
|
1
1
|
export declare function startEncode(): void;
|
|
2
2
|
export declare function addEncodedDataListener(callback: (data: string) => void): () => void;
|
|
3
|
+
export interface AudioFormatInfo {
|
|
4
|
+
sampleRate: number;
|
|
5
|
+
channels: number;
|
|
6
|
+
bitsPerSample: number;
|
|
7
|
+
codec: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Start recording from the microphone and encoding audio to FLAC in real time.
|
|
11
|
+
* Each encoded FLAC packet is base64-encoded and emitted via the onAudioEncodedData event.
|
|
12
|
+
* @param sampleRate Target sample rate in Hz (default: 16000)
|
|
13
|
+
* @returns Promise that resolves when recording has started successfully
|
|
14
|
+
*/
|
|
15
|
+
export declare function startAudioEncode(sampleRate?: number): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Stop audio recording and FLAC encoding.
|
|
18
|
+
*/
|
|
19
|
+
export declare function stopAudioEncode(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Listen for base64-encoded FLAC audio packets.
|
|
22
|
+
*/
|
|
23
|
+
export declare function addAudioDataListener(callback: (data: string) => void): () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Listen for audio format info emitted once at the start of recording.
|
|
26
|
+
*/
|
|
27
|
+
export declare function addAudioFormatInfoListener(callback: (info: AudioFormatInfo) => void): () => void;
|
|
3
28
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAMA,wBAAgB,WAAW,IAAI,IAAI,CAElC;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC/B,MAAM,IAAI,CAUZ"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAMA,wBAAgB,WAAW,IAAI,IAAI,CAElC;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC/B,MAAM,IAAI,CAUZ;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,GAAE,MAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAE7E;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC/B,MAAM,IAAI,CAUZ;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,GACxC,MAAM,IAAI,CAUZ"}
|
package/package.json
CHANGED
package/src/NativeSuuqencode.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { TurboModuleRegistry, type TurboModule } from 'react-native';
|
|
|
2
2
|
|
|
3
3
|
export interface Spec extends TurboModule {
|
|
4
4
|
startEncode(): void;
|
|
5
|
+
startAudioEncode(sampleRate: number): Promise<boolean>;
|
|
6
|
+
stopAudioEncode(): void;
|
|
5
7
|
addListener(eventName: string): void;
|
|
6
8
|
removeListeners(count: number): void;
|
|
7
9
|
}
|
package/src/index.tsx
CHANGED
|
@@ -21,3 +21,61 @@ export function addEncodedDataListener(
|
|
|
21
21
|
subscription.remove();
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
export interface AudioFormatInfo {
|
|
26
|
+
sampleRate: number;
|
|
27
|
+
channels: number;
|
|
28
|
+
bitsPerSample: number;
|
|
29
|
+
codec: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start recording from the microphone and encoding audio to FLAC in real time.
|
|
34
|
+
* Each encoded FLAC packet is base64-encoded and emitted via the onAudioEncodedData event.
|
|
35
|
+
* @param sampleRate Target sample rate in Hz (default: 16000)
|
|
36
|
+
* @returns Promise that resolves when recording has started successfully
|
|
37
|
+
*/
|
|
38
|
+
export function startAudioEncode(sampleRate: number = 16000): Promise<boolean> {
|
|
39
|
+
return Suuqencode.startAudioEncode(sampleRate);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stop audio recording and FLAC encoding.
|
|
44
|
+
*/
|
|
45
|
+
export function stopAudioEncode(): void {
|
|
46
|
+
Suuqencode.stopAudioEncode();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Listen for base64-encoded FLAC audio packets.
|
|
51
|
+
*/
|
|
52
|
+
export function addAudioDataListener(
|
|
53
|
+
callback: (data: string) => void
|
|
54
|
+
): () => void {
|
|
55
|
+
const subscription = eventEmitter.addListener(
|
|
56
|
+
'onAudioEncodedData',
|
|
57
|
+
(data: any) => {
|
|
58
|
+
callback(data as string);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
return () => {
|
|
62
|
+
subscription.remove();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Listen for audio format info emitted once at the start of recording.
|
|
68
|
+
*/
|
|
69
|
+
export function addAudioFormatInfoListener(
|
|
70
|
+
callback: (info: AudioFormatInfo) => void
|
|
71
|
+
): () => void {
|
|
72
|
+
const subscription = eventEmitter.addListener(
|
|
73
|
+
'onAudioFormatInfo',
|
|
74
|
+
(info: any) => {
|
|
75
|
+
callback(info as AudioFormatInfo);
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
return () => {
|
|
79
|
+
subscription.remove();
|
|
80
|
+
};
|
|
81
|
+
}
|