@siteed/expo-audio-stream 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +151 -109
- package/build/ExpoAudioStream.types.d.ts +7 -0
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStreamModule.web.d.ts +2 -2
- package/build/ExpoAudioStreamModule.web.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.web.js +5 -1
- package/build/ExpoAudioStreamModule.web.js.map +1 -1
- package/build/useAudioRecording.d.ts +5 -4
- package/build/useAudioRecording.d.ts.map +1 -1
- package/build/useAudioRecording.js +38 -27
- package/build/useAudioRecording.js.map +1 -1
- package/ios/AudioStreamManager.swift +35 -5
- package/ios/ExpoAudioStreamModule.swift +2 -2
- package/package.json +1 -1
- package/src/ExpoAudioStream.types.ts +8 -0
- package/src/ExpoAudioStreamModule.web.ts +6 -1
- package/src/useAudioRecording.ts +46 -32
|
@@ -6,23 +6,24 @@ import android.content.pm.PackageManager
|
|
|
6
6
|
import android.media.AudioFormat
|
|
7
7
|
import android.media.AudioRecord
|
|
8
8
|
import android.media.MediaRecorder
|
|
9
|
+
import android.os.Handler
|
|
10
|
+
import android.os.Looper
|
|
11
|
+
import android.os.SystemClock
|
|
12
|
+
import android.util.Base64
|
|
9
13
|
import android.util.Log
|
|
10
14
|
import androidx.core.content.ContextCompat
|
|
11
15
|
import androidx.core.os.bundleOf
|
|
16
|
+
import expo.modules.kotlin.Promise
|
|
12
17
|
import expo.modules.kotlin.modules.Module
|
|
13
18
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
14
|
-
import expo.modules.kotlin.Promise
|
|
15
|
-
import android.util.Base64
|
|
16
|
-
import android.os.Handler
|
|
17
|
-
import android.os.SystemClock
|
|
18
19
|
import java.io.ByteArrayOutputStream
|
|
19
|
-
import android.os.Looper
|
|
20
|
-
import expo.modules.core.interfaces.Function
|
|
21
|
-
import java.util.concurrent.atomic.AtomicBoolean
|
|
22
20
|
import java.io.File
|
|
21
|
+
import java.io.FileInputStream
|
|
23
22
|
import java.io.FileOutputStream
|
|
24
23
|
import java.io.IOException
|
|
24
|
+
import java.io.OutputStream
|
|
25
25
|
import java.io.RandomAccessFile
|
|
26
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
26
27
|
|
|
27
28
|
const val AUDIO_EVENT_NAME = "AudioData"
|
|
28
29
|
const val DEFAULT_SAMPLE_RATE = 16000 // Default sample rate for audio recording
|
|
@@ -43,7 +44,6 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
43
44
|
private var recordingStartTime: Long = 0
|
|
44
45
|
private var totalRecordedTime: Long = 0
|
|
45
46
|
private var totalDataSize = 0
|
|
46
|
-
private val audioDataBuffer = ByteArrayOutputStream()
|
|
47
47
|
private var interval = 1000L // Emit data every 1000 milliseconds (1 second)
|
|
48
48
|
private var lastEmitTime = SystemClock.elapsedRealtime()
|
|
49
49
|
private var lastPauseTime = 0L
|
|
@@ -53,6 +53,7 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
53
53
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
54
54
|
private var bitDepth = 16
|
|
55
55
|
private var channels = 1
|
|
56
|
+
private val audioRecordLock = Any()
|
|
56
57
|
|
|
57
58
|
@SuppressLint("MissingPermission")
|
|
58
59
|
override fun definition() = ModuleDefinition {
|
|
@@ -73,21 +74,35 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
Function("status") {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
synchronized(audioRecordLock) {
|
|
78
|
+
|
|
79
|
+
if(!isRecording.get()) {
|
|
80
|
+
return@Function bundleOf(
|
|
81
|
+
"isRecording" to false,
|
|
82
|
+
"isPaused" to false,
|
|
83
|
+
"mime" to mimeType,
|
|
84
|
+
"size" to 0,
|
|
85
|
+
"interval" to interval,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Ensure you update this to check if audioFile is null or not
|
|
90
|
+
val fileSize = audioFile?.length() ?: 0
|
|
91
|
+
val dataFileSize = fileSize - 44 // Assuming header is always 44 bytes
|
|
92
|
+
|
|
93
|
+
val byteRate = sampleRateInHz * channels * (bitDepth / 8)
|
|
94
|
+
val duration =
|
|
95
|
+
if (byteRate > 0) (dataFileSize * 1000 / byteRate) else 0 // Duration in milliseconds
|
|
96
|
+
|
|
97
|
+
bundleOf(
|
|
98
|
+
"duration" to duration,
|
|
99
|
+
"isRecording" to isRecording.get(),
|
|
100
|
+
"isPaused" to isPaused.get(),
|
|
101
|
+
"mime" to mimeType,
|
|
102
|
+
"size" to totalDataSize,
|
|
103
|
+
"interval" to interval,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
AsyncFunction("listAudioFiles") { promise: Promise ->
|
|
@@ -111,7 +126,7 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
// Method to write WAV file header
|
|
114
|
-
private fun writeWavHeader(out:
|
|
129
|
+
private fun writeWavHeader(out: OutputStream) {
|
|
115
130
|
val header = ByteArray(44)
|
|
116
131
|
val byteRate = sampleRateInHz * channels * bitDepth / 8
|
|
117
132
|
val blockAlign = channels * bitDepth / 8
|
|
@@ -223,47 +238,75 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
223
238
|
}
|
|
224
239
|
|
|
225
240
|
recordingThread = Thread { recordingProcess() }.apply { start() }
|
|
226
|
-
|
|
241
|
+
|
|
242
|
+
val result = bundleOf(
|
|
243
|
+
"fileUri" to audioFile?.toURI().toString(),
|
|
244
|
+
"channels" to channels,
|
|
245
|
+
"bitDepth" to bitDepth,
|
|
246
|
+
"sampleRate" to sampleRateInHz,
|
|
247
|
+
"mimeType" to mimeType
|
|
248
|
+
)
|
|
249
|
+
promise.resolve(result)
|
|
227
250
|
}
|
|
228
251
|
|
|
229
252
|
private fun stopRecording(promise: Promise) {
|
|
230
|
-
|
|
231
|
-
promise.reject("NOT_RECORDING", "Recording is not active", null)
|
|
232
|
-
return
|
|
233
|
-
}
|
|
253
|
+
synchronized(audioRecordLock) {
|
|
234
254
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
255
|
+
if (!isRecording.get()) {
|
|
256
|
+
promise.reject("NOT_RECORDING", "Recording is not active", null)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
val audioData = ByteArray(bufferSizeInBytes)
|
|
262
|
+
val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
|
|
263
|
+
Log.d("AudioRecorderModule", "Last Read $bytesRead bytes")
|
|
264
|
+
if (bytesRead > 0) {
|
|
265
|
+
emitAudioData(audioData, bytesRead)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
Log.d("AudioRecorderModule", "Stopping recording state = ${audioRecord?.state}")
|
|
269
|
+
if (audioRecord != null && audioRecord!!.state == AudioRecord.STATE_INITIALIZED) {
|
|
270
|
+
Log.d("AudioRecorderModule", "Stopping AudioRecord");
|
|
271
|
+
audioRecord!!.stop()
|
|
272
|
+
}
|
|
273
|
+
} catch (e: IllegalStateException) {
|
|
274
|
+
Log.e("AudioRecording", "Error reading from AudioRecord", e);
|
|
275
|
+
} finally {
|
|
276
|
+
audioRecord?.release()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
val fileSize = audioFile?.length() ?: 0
|
|
281
|
+
val dataFileSize = fileSize - 44 // Subtract header size
|
|
282
|
+
val byteRate = sampleRateInHz * channels * (bitDepth / 8)
|
|
283
|
+
|
|
284
|
+
// Calculate duration based on the data size and byte rate
|
|
285
|
+
val duration = if (byteRate > 0) (dataFileSize * 1000 / byteRate) else 0
|
|
286
|
+
|
|
287
|
+
// Create result bundle
|
|
288
|
+
val result = bundleOf(
|
|
289
|
+
"fileUri" to audioFile?.toURI().toString(),
|
|
290
|
+
"duration" to duration,
|
|
291
|
+
"channels" to channels,
|
|
292
|
+
"bitDepth" to bitDepth,
|
|
293
|
+
"sampleRate" to sampleRateInHz,
|
|
294
|
+
"size" to fileSize,
|
|
295
|
+
"mimeType" to mimeType
|
|
296
|
+
)
|
|
297
|
+
promise.resolve(result)
|
|
298
|
+
|
|
299
|
+
// Reset the timing variables
|
|
300
|
+
isRecording.set(false)
|
|
301
|
+
isPaused.set(false)
|
|
302
|
+
totalRecordedTime = 0
|
|
303
|
+
pausedDuration = 0
|
|
304
|
+
} catch (e: Exception) {
|
|
305
|
+
Log.d("AudioRecorderModule", "Failed to stop recording", e)
|
|
306
|
+
promise.reject("STOP_FAILED", "Failed to stop recording", e)
|
|
307
|
+
} finally {
|
|
308
|
+
audioRecord = null
|
|
309
|
+
}
|
|
267
310
|
}
|
|
268
311
|
}
|
|
269
312
|
|
|
@@ -271,28 +314,36 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
271
314
|
FileOutputStream(audioFile, true).use { fos ->
|
|
272
315
|
// Buffer to accumulate data
|
|
273
316
|
val accumulatedAudioData = ByteArrayOutputStream()
|
|
317
|
+
writeWavHeader(accumulatedAudioData)
|
|
274
318
|
|
|
275
319
|
// Write audio data directly to the file
|
|
276
320
|
val audioData = ByteArray(bufferSizeInBytes)
|
|
277
|
-
while (isRecording.get()) {
|
|
321
|
+
while (isRecording.get() && !Thread.currentThread().isInterrupted) {
|
|
278
322
|
if (!isPaused.get()) {
|
|
279
|
-
val bytesRead =
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
323
|
+
val bytesRead = synchronized(audioRecordLock) {
|
|
324
|
+
// Only synchronize the read operation and the check
|
|
325
|
+
audioRecord?.let {
|
|
326
|
+
if (it.state != AudioRecord.STATE_INITIALIZED) {
|
|
327
|
+
Log.e("AudioRecorderModule", "AudioRecord not initialized")
|
|
328
|
+
return@let -1
|
|
329
|
+
}
|
|
330
|
+
it.read(audioData, 0, bufferSizeInBytes)
|
|
331
|
+
} ?: -1 // Handle null case
|
|
283
332
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
333
|
+
if (bytesRead > 0) {
|
|
334
|
+
fos.write(audioData, 0, bytesRead)
|
|
335
|
+
totalDataSize += bytesRead
|
|
336
|
+
accumulatedAudioData.write(audioData, 0, bytesRead)
|
|
337
|
+
|
|
338
|
+
// Emit audio data at defined intervals
|
|
339
|
+
if (SystemClock.elapsedRealtime() - lastEmitTime >= interval) {
|
|
340
|
+
emitAudioData(accumulatedAudioData.toByteArray(), accumulatedAudioData.size())
|
|
341
|
+
lastEmitTime = SystemClock.elapsedRealtime() // Reset the timer
|
|
342
|
+
accumulatedAudioData.reset() // Clear the accumulator
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
Log.d("AudioRecorderModule", "Bytes written to file: $bytesRead")
|
|
294
346
|
}
|
|
295
|
-
}
|
|
296
347
|
}
|
|
297
348
|
}
|
|
298
349
|
}
|
|
@@ -327,7 +378,6 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
327
378
|
}
|
|
328
379
|
|
|
329
380
|
private fun emitAudioData(audioData: ByteArray, length: Int) {
|
|
330
|
-
// Since audioData now contains the actual bytes read, you do not need to read from audioDataBuffer.
|
|
331
381
|
// Encode the part of the buffer that contains new audio data.
|
|
332
382
|
val encodedBuffer = encodeAudioData(audioData)
|
|
333
383
|
val fileSize = audioFile?.length() ?: 0 // Update file size information
|
|
@@ -354,28 +404,12 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
354
404
|
Log.e("AudioRecorderModule", "Failed to send event", e)
|
|
355
405
|
}
|
|
356
406
|
}
|
|
357
|
-
// audioDataBuffer.reset() is no longer needed here as we do not use the buffer to store ongoing data
|
|
358
407
|
}
|
|
359
408
|
|
|
360
409
|
private fun encodeAudioData(rawData: ByteArray): String {
|
|
361
410
|
return Base64.encodeToString(rawData, Base64.NO_WRAP)
|
|
362
411
|
}
|
|
363
412
|
|
|
364
|
-
private fun saveAudioToFile(rawData: ByteArray): Boolean {
|
|
365
|
-
return try {
|
|
366
|
-
// Open a FileOutputStream in append mode.
|
|
367
|
-
FileOutputStream(audioFile, true).use { output ->
|
|
368
|
-
// Write rawData to the file.
|
|
369
|
-
output.write(rawData)
|
|
370
|
-
}
|
|
371
|
-
true
|
|
372
|
-
} catch (e: IOException) {
|
|
373
|
-
// Handle exceptions here
|
|
374
|
-
Log.e("AudioRecorderModule", "Could not write to file: ${audioFile?.absolutePath}", e)
|
|
375
|
-
false
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
413
|
private fun checkPermission(): Boolean {
|
|
380
414
|
val reactContext = appContext.reactContext ?: return false // If reactContext is null, permissions cannot be checked
|
|
381
415
|
return ContextCompat.checkSelfPermission(reactContext, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|
|
@@ -394,24 +428,32 @@ class ExpoAudioStreamModule() : Module() {
|
|
|
394
428
|
|
|
395
429
|
@SuppressLint("MissingPermission")
|
|
396
430
|
private fun initializeRecorder(): Boolean {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
431
|
+
synchronized(audioRecordLock) {
|
|
432
|
+
Log.d("AudioRecorderModule", "Initializing recorder")
|
|
433
|
+
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
|
434
|
+
Log.d("AudioRecorderModule", "Buffer size: $bufferSizeInBytes")
|
|
435
|
+
|
|
436
|
+
if (bufferSizeInBytes == AudioRecord.ERROR_BAD_VALUE) {
|
|
437
|
+
Log.e("AudioRecorderModule", "Invalid buffer size")
|
|
438
|
+
return false
|
|
439
|
+
}
|
|
400
440
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
441
|
+
val recorder = AudioRecord(
|
|
442
|
+
MediaRecorder.AudioSource.MIC,
|
|
443
|
+
sampleRateInHz,
|
|
444
|
+
channelConfig,
|
|
445
|
+
audioFormat,
|
|
446
|
+
bufferSizeInBytes
|
|
447
|
+
)
|
|
448
|
+
if (recorder.state != AudioRecord.STATE_INITIALIZED) {
|
|
449
|
+
Log.e("AudioRecorderModule", "AudioRecord initialization failed")
|
|
450
|
+
recorder.release() // Clean up resources if initialization fails
|
|
451
|
+
return false
|
|
452
|
+
}
|
|
405
453
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
Log.e("AudioRecorderModule", "AudioRecord initialization failed")
|
|
409
|
-
recorder.release() // Clean up resources if initialization fails
|
|
410
|
-
return false
|
|
454
|
+
this.audioRecord = recorder // Properly assign the recorder to the class member
|
|
455
|
+
return true
|
|
411
456
|
}
|
|
412
|
-
|
|
413
|
-
this.audioRecord = recorder // Properly assign the recorder to the class member
|
|
414
|
-
return true
|
|
415
457
|
}
|
|
416
458
|
|
|
417
459
|
}
|
|
@@ -18,6 +18,13 @@ export interface AudioStreamResult {
|
|
|
18
18
|
bitDepth?: number;
|
|
19
19
|
sampleRate?: number;
|
|
20
20
|
}
|
|
21
|
+
export interface StartAudioStreamResult {
|
|
22
|
+
fileUri: string;
|
|
23
|
+
mimeType: string;
|
|
24
|
+
channels?: number;
|
|
25
|
+
bitDepth?: number;
|
|
26
|
+
sampleRate?: number;
|
|
27
|
+
}
|
|
21
28
|
export interface AudioStreamStatus {
|
|
22
29
|
isRecording: boolean;
|
|
23
30
|
isPaused: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStream.types.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAK/B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
|
|
1
|
+
{"version":3,"file":"ExpoAudioStream.types.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAK/B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStream.types.js","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"","sourcesContent":["export interface AudioEventPayload {\n encoded?: string;\n buffer?: Blob;\n fileUri: string;\n lastEmittedSize: number;\n position: number;\n deltaSize: number;\n totalSize: number;\n mimeType: string;\n streamUuid: string;\n}\n\nexport interface AudioStreamResult {\n fileUri: string;\n duration: number;\n size: number;\n mimeType: string;\n channels?: number;\n bitDepth?: number;\n sampleRate?: number;\n}\n\nexport interface AudioStreamStatus {\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n size: number;\n interval: number;\n mimeType: string;\n}\n\nexport interface RecordingOptions {\n // TODO align Android and IOS options\n // sampleRate?: number;\n // channelConfig?: number; // numberOfChannel\n // audioFormat?: number; // bitDepth (ENCODING_PCM_16BIT --> 2)\n interval?: number;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ExpoAudioStream.types.js","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"","sourcesContent":["export interface AudioEventPayload {\n encoded?: string;\n buffer?: Blob;\n fileUri: string;\n lastEmittedSize: number;\n position: number;\n deltaSize: number;\n totalSize: number;\n mimeType: string;\n streamUuid: string;\n}\n\nexport interface AudioStreamResult {\n fileUri: string;\n duration: number;\n size: number;\n mimeType: string;\n channels?: number;\n bitDepth?: number;\n sampleRate?: number;\n}\n\nexport interface StartAudioStreamResult {\n fileUri: string;\n mimeType: string;\n channels?: number;\n bitDepth?: number;\n sampleRate?: number;\n}\n\nexport interface AudioStreamStatus {\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n size: number;\n interval: number;\n mimeType: string;\n}\n\nexport interface RecordingOptions {\n // TODO align Android and IOS options\n // sampleRate?: number;\n // channelConfig?: number; // numberOfChannel\n // audioFormat?: number; // bitDepth (ENCODING_PCM_16BIT --> 2)\n interval?: number;\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "expo-modules-core";
|
|
2
|
-
import { AudioStreamResult, RecordingOptions } from "./ExpoAudioStream.types";
|
|
2
|
+
import { AudioStreamResult, RecordingOptions, StartAudioStreamResult } from "./ExpoAudioStream.types";
|
|
3
3
|
declare class ExpoAudioStreamWeb extends EventEmitter {
|
|
4
4
|
mediaRecorder: MediaRecorder | null;
|
|
5
5
|
audioChunks: Blob[];
|
|
@@ -15,7 +15,7 @@ declare class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
15
15
|
streamUuid: string | null;
|
|
16
16
|
constructor();
|
|
17
17
|
getMediaStream(): Promise<MediaStream>;
|
|
18
|
-
startRecording(options?: RecordingOptions): Promise<
|
|
18
|
+
startRecording(options?: RecordingOptions): Promise<StartAudioStreamResult>;
|
|
19
19
|
setupRecordingListeners(): void;
|
|
20
20
|
emitAudioEvent({ data, position }: {
|
|
21
21
|
data: Blob;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStreamModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAEL,iBAAiB,EACjB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"ExpoAudioStreamModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAEL,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAGjC,cAAM,kBAAmB,SAAQ,YAAY;IAC3C,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;;IA4BpB,cAAc;IAUd,cAAc,CAAC,OAAO,GAAE,gBAAqB;IAwBnD,uBAAuB;IA4BvB,cAAc,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE;IAiBnE,YAAY;IAUN,aAAa,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAelD,cAAc;IAcpB,MAAM;;;;;;;IAUN,cAAc;IAId,eAAe;CAGhB;;AAED,wBAAwC"}
|
|
@@ -63,7 +63,11 @@ class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
63
63
|
this.lastEmittedTime = 0;
|
|
64
64
|
this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session
|
|
65
65
|
const fileUri = `${this.streamUuid}.webm`;
|
|
66
|
-
|
|
66
|
+
const streamConfig = {
|
|
67
|
+
fileUri,
|
|
68
|
+
mimeType: "audio/webm",
|
|
69
|
+
};
|
|
70
|
+
return streamConfig;
|
|
67
71
|
}
|
|
68
72
|
// Setup listeners for the MediaRecorder
|
|
69
73
|
setupRecordingListeners() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStreamModule.web.js","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"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","sourcesContent":["import debug from \"debug\";\nimport { EventEmitter } from \"expo-modules-core\";\n\nimport {\n AudioEventPayload,\n AudioStreamResult,\n RecordingOptions,\n} from \"./ExpoAudioStream.types\";\n\nconst log = debug(\"expo-audio-stream:useAudioRecording\");\nclass ExpoAudioStreamWeb extends EventEmitter {\n mediaRecorder: MediaRecorder | null;\n audioChunks: Blob[];\n isRecording: boolean;\n isPaused: boolean;\n recordingStartTime: number;\n pausedTime: number;\n currentDurationMs: number;\n currentSize: number;\n currentInterval: number;\n lastEmittedSize: number;\n lastEmittedTime: number;\n streamUuid: string | null;\n\n constructor() {\n const mockNativeModule = {\n addListener: (eventName: string) => {\n // Not used on web\n },\n removeListeners: (count: number) => {\n // Not used on web\n },\n };\n super(mockNativeModule); // Pass the mock native module to the parent class\n\n this.mediaRecorder = null;\n this.audioChunks = [];\n this.isRecording = false;\n this.isPaused = false;\n this.recordingStartTime = 0;\n this.pausedTime = 0;\n this.currentDurationMs = 0;\n this.currentSize = 0;\n this.currentInterval = 1000; // Default interval in ms\n this.lastEmittedSize = 0;\n this.lastEmittedTime = 0;\n this.streamUuid = null; // Initialize UUID on first recording start\n }\n\n // Utility to handle user media stream\n async getMediaStream() {\n try {\n return await navigator.mediaDevices.getUserMedia({ audio: true });\n } catch (error) {\n console.error(\"Failed to get media stream:\", error);\n throw error;\n }\n }\n\n // Start recording with options\n async startRecording(options: RecordingOptions = {}) {\n if (this.isRecording) {\n throw new Error(\"Recording is already in progress\");\n }\n\n const stream = await this.getMediaStream();\n this.mediaRecorder = new MediaRecorder(stream);\n this.setupRecordingListeners();\n this.mediaRecorder.start(options.interval || this.currentInterval);\n this.isRecording = true;\n this.recordingStartTime = Date.now();\n this.pausedTime = 0;\n this.lastEmittedSize = 0;\n this.lastEmittedTime = 0;\n this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session\n const fileUri = `${this.streamUuid}.webm`;\n return fileUri;\n }\n\n // Setup listeners for the MediaRecorder\n setupRecordingListeners() {\n if (!this.mediaRecorder) {\n throw new Error(\"No active media recorder\");\n }\n this.mediaRecorder.ondataavailable = (event) => {\n this.audioChunks.push(event.data);\n this.currentSize += event.data.size; // Update the size of the recording\n\n this.emitAudioEvent({ data: event.data, position: this.lastEmittedTime });\n this.lastEmittedTime = event.timeStamp;\n this.lastEmittedSize = this.currentSize;\n };\n\n this.mediaRecorder.onstop = () => {\n this.isRecording = false;\n log(\"Recording stopped\", this.audioChunks);\n };\n\n this.mediaRecorder.onpause = () => {\n this.isPaused = true;\n };\n\n this.mediaRecorder.onresume = () => {\n this.isPaused = false;\n this.recordingStartTime += Date.now() - this.pausedTime; // Adjust start time after resuming\n };\n }\n\n emitAudioEvent({ data, position }: { data: Blob; position: number }) {\n const fileUri = `${this.streamUuid}.webm`;\n const audioEventPayload: AudioEventPayload = {\n fileUri,\n mimeType: \"audio/webm\",\n lastEmittedSize: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly\n deltaSize: data.size,\n position,\n totalSize: this.currentSize,\n buffer: data,\n streamUuid: this.streamUuid ?? \"\", // Generate or manage UUID for stream identification\n };\n\n this.emit(\"AudioData\", audioEventPayload);\n }\n\n // Helper method to generate a UUID\n generateUUID() {\n // Implementation of UUID generation (use a library or custom method)\n return \"xxxx-xxxx-xxxx-xxxx\".replace(/[x]/g, (c) => {\n const r = (Math.random() * 16) | 0,\n v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n }\n\n // Stop recording\n async stopRecording(): Promise<AudioStreamResult | null> {\n this.mediaRecorder?.stop();\n this.isRecording = false;\n this.currentDurationMs = Date.now() - this.recordingStartTime;\n const result: AudioStreamResult = {\n fileUri: `${this.streamUuid}.webm`,\n duration: this.currentDurationMs,\n size: this.currentSize,\n mimeType: \"audio/webm\",\n };\n\n return result;\n }\n\n // Pause recording\n async pauseRecording() {\n if (!this.mediaRecorder) {\n throw new Error(\"No active media recorder\");\n }\n\n if (this.isRecording && !this.isPaused) {\n this.mediaRecorder.pause();\n this.pausedTime = Date.now();\n } else {\n throw new Error(\"Recording is not active or already paused\");\n }\n }\n\n // Get current status\n status() {\n return {\n isRecording: this.isRecording,\n isPaused: this.isPaused,\n duration: Date.now() - this.recordingStartTime,\n size: this.currentSize,\n interval: this.currentInterval,\n };\n }\n\n listAudioFiles() {\n // Not applicable on web\n }\n\n clearAudioFiles() {\n // Not applicable on web\n }\n}\n\nexport default new ExpoAudioStreamWeb();\n"]}
|
|
1
|
+
{"version":3,"file":"ExpoAudioStreamModule.web.js","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"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","sourcesContent":["import debug from \"debug\";\nimport { EventEmitter } from \"expo-modules-core\";\n\nimport {\n AudioEventPayload,\n AudioStreamResult,\n RecordingOptions,\n StartAudioStreamResult,\n} from \"./ExpoAudioStream.types\";\n\nconst log = debug(\"expo-audio-stream:useAudioRecording\");\nclass ExpoAudioStreamWeb extends EventEmitter {\n mediaRecorder: MediaRecorder | null;\n audioChunks: Blob[];\n isRecording: boolean;\n isPaused: boolean;\n recordingStartTime: number;\n pausedTime: number;\n currentDurationMs: number;\n currentSize: number;\n currentInterval: number;\n lastEmittedSize: number;\n lastEmittedTime: number;\n streamUuid: string | null;\n\n constructor() {\n const mockNativeModule = {\n addListener: (eventName: string) => {\n // Not used on web\n },\n removeListeners: (count: number) => {\n // Not used on web\n },\n };\n super(mockNativeModule); // Pass the mock native module to the parent class\n\n this.mediaRecorder = null;\n this.audioChunks = [];\n this.isRecording = false;\n this.isPaused = false;\n this.recordingStartTime = 0;\n this.pausedTime = 0;\n this.currentDurationMs = 0;\n this.currentSize = 0;\n this.currentInterval = 1000; // Default interval in ms\n this.lastEmittedSize = 0;\n this.lastEmittedTime = 0;\n this.streamUuid = null; // Initialize UUID on first recording start\n }\n\n // Utility to handle user media stream\n async getMediaStream() {\n try {\n return await navigator.mediaDevices.getUserMedia({ audio: true });\n } catch (error) {\n console.error(\"Failed to get media stream:\", error);\n throw error;\n }\n }\n\n // Start recording with options\n async startRecording(options: RecordingOptions = {}) {\n if (this.isRecording) {\n throw new Error(\"Recording is already in progress\");\n }\n\n const stream = await this.getMediaStream();\n this.mediaRecorder = new MediaRecorder(stream);\n this.setupRecordingListeners();\n this.mediaRecorder.start(options.interval || this.currentInterval);\n this.isRecording = true;\n this.recordingStartTime = Date.now();\n this.pausedTime = 0;\n this.lastEmittedSize = 0;\n this.lastEmittedTime = 0;\n this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session\n const fileUri = `${this.streamUuid}.webm`;\n const streamConfig: StartAudioStreamResult = {\n fileUri,\n mimeType: \"audio/webm\",\n };\n return streamConfig;\n }\n\n // Setup listeners for the MediaRecorder\n setupRecordingListeners() {\n if (!this.mediaRecorder) {\n throw new Error(\"No active media recorder\");\n }\n this.mediaRecorder.ondataavailable = (event) => {\n this.audioChunks.push(event.data);\n this.currentSize += event.data.size; // Update the size of the recording\n\n this.emitAudioEvent({ data: event.data, position: this.lastEmittedTime });\n this.lastEmittedTime = event.timeStamp;\n this.lastEmittedSize = this.currentSize;\n };\n\n this.mediaRecorder.onstop = () => {\n this.isRecording = false;\n log(\"Recording stopped\", this.audioChunks);\n };\n\n this.mediaRecorder.onpause = () => {\n this.isPaused = true;\n };\n\n this.mediaRecorder.onresume = () => {\n this.isPaused = false;\n this.recordingStartTime += Date.now() - this.pausedTime; // Adjust start time after resuming\n };\n }\n\n emitAudioEvent({ data, position }: { data: Blob; position: number }) {\n const fileUri = `${this.streamUuid}.webm`;\n const audioEventPayload: AudioEventPayload = {\n fileUri,\n mimeType: \"audio/webm\",\n lastEmittedSize: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly\n deltaSize: data.size,\n position,\n totalSize: this.currentSize,\n buffer: data,\n streamUuid: this.streamUuid ?? \"\", // Generate or manage UUID for stream identification\n };\n\n this.emit(\"AudioData\", audioEventPayload);\n }\n\n // Helper method to generate a UUID\n generateUUID() {\n // Implementation of UUID generation (use a library or custom method)\n return \"xxxx-xxxx-xxxx-xxxx\".replace(/[x]/g, (c) => {\n const r = (Math.random() * 16) | 0,\n v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n }\n\n // Stop recording\n async stopRecording(): Promise<AudioStreamResult | null> {\n this.mediaRecorder?.stop();\n this.isRecording = false;\n this.currentDurationMs = Date.now() - this.recordingStartTime;\n const result: AudioStreamResult = {\n fileUri: `${this.streamUuid}.webm`,\n duration: this.currentDurationMs,\n size: this.currentSize,\n mimeType: \"audio/webm\",\n };\n\n return result;\n }\n\n // Pause recording\n async pauseRecording() {\n if (!this.mediaRecorder) {\n throw new Error(\"No active media recorder\");\n }\n\n if (this.isRecording && !this.isPaused) {\n this.mediaRecorder.pause();\n this.pausedTime = Date.now();\n } else {\n throw new Error(\"Recording is not active or already paused\");\n }\n }\n\n // Get current status\n status() {\n return {\n isRecording: this.isRecording,\n isPaused: this.isPaused,\n duration: Date.now() - this.recordingStartTime,\n size: this.currentSize,\n interval: this.currentInterval,\n };\n }\n\n listAudioFiles() {\n // Not applicable on web\n }\n\n clearAudioFiles() {\n // Not applicable on web\n }\n}\n\nexport default new ExpoAudioStreamWeb();\n"]}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { AudioStreamResult, RecordingOptions } from "./ExpoAudioStream.types";
|
|
1
|
+
import { AudioStreamResult, RecordingOptions, StartAudioStreamResult } from "./ExpoAudioStream.types";
|
|
2
2
|
export interface AudioDataEvent {
|
|
3
|
-
data: string |
|
|
3
|
+
data: string | Blob;
|
|
4
4
|
position: number;
|
|
5
|
-
|
|
5
|
+
eventDataSize: number;
|
|
6
|
+
totalSize: number;
|
|
6
7
|
}
|
|
7
8
|
export interface UseAudioRecorderState {
|
|
8
|
-
startRecording: (_: RecordingOptions) => Promise<
|
|
9
|
+
startRecording: (_: RecordingOptions) => Promise<StartAudioStreamResult>;
|
|
9
10
|
stopRecording: () => Promise<AudioStreamResult | null>;
|
|
10
11
|
pauseRecording: () => void;
|
|
11
12
|
isRecording: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAudioRecording.d.ts","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,iBAAiB,EAEjB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"useAudioRecording.d.ts","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,iBAAiB,EAEjB,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAGjC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACzE,aAAa,EAAE,MAAM,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACvD,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,aAAa,EACb,KAAa,GACd,EAAE;IACD,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,qBAAqB,CA0MxB"}
|
|
@@ -7,31 +7,31 @@ export function useAudioRecorder({ onAudioStream, debug = false, }) {
|
|
|
7
7
|
const [isPaused, setIsPaused] = useState(false);
|
|
8
8
|
const [duration, setDuration] = useState(0);
|
|
9
9
|
const [size, setSize] = useState(0);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const checkStatus = useCallback(async () => {
|
|
11
|
+
try {
|
|
12
|
+
if (!isRecording) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const status = ExpoAudioStreamModule.status();
|
|
16
|
+
if (debug) {
|
|
17
|
+
console.log(`[useAudioRecorder] Status:`, status);
|
|
18
|
+
}
|
|
19
|
+
if (!status.isRecording) {
|
|
20
|
+
// Don't update if recording stopped.
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Extract matching file from filesystem
|
|
24
|
+
setDuration(status.duration);
|
|
25
|
+
setSize(status.size);
|
|
13
26
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
if (debug)
|
|
18
|
-
console.log(`[useAudioRecorder] Getting status`);
|
|
19
|
-
const status = ExpoAudioStreamModule.status();
|
|
20
|
-
if (debug) {
|
|
21
|
-
console.log(`[useAudioRecorder] Status:`, status);
|
|
22
|
-
}
|
|
23
|
-
// Extract matching file from filesystem
|
|
24
|
-
setDuration(status.duration);
|
|
25
|
-
setSize(status.size);
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
console.error(`[useAudioRecorder] Error getting status:`, error);
|
|
29
|
-
}
|
|
30
|
-
}, 1000);
|
|
31
|
-
return () => clearInterval(interval);
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(`[useAudioRecorder] Error getting status:`, error);
|
|
32
29
|
}
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
}, [isRecording]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const interval = setInterval(checkStatus, 1000);
|
|
33
|
+
return () => clearInterval(interval);
|
|
34
|
+
}, [checkStatus]);
|
|
35
35
|
useEffect(() => {
|
|
36
36
|
if (debug) {
|
|
37
37
|
console.log(`[useAudioRecorder] Registering audio event listener`, onAudioStream);
|
|
@@ -70,7 +70,12 @@ export function useAudioRecorder({ onAudioStream, debug = false, }) {
|
|
|
70
70
|
// `[useAudioRecorder] Read audio file position=${position} deltaSize: ${deltaSize} vs encoded.length: ${encoded.length}`,
|
|
71
71
|
// );
|
|
72
72
|
// }
|
|
73
|
-
onAudioStream?.({
|
|
73
|
+
onAudioStream?.({
|
|
74
|
+
data: encoded,
|
|
75
|
+
position,
|
|
76
|
+
eventDataSize: deltaSize,
|
|
77
|
+
totalSize,
|
|
78
|
+
});
|
|
74
79
|
// Below code is optional, used to compare encoded data to audio on file system
|
|
75
80
|
// Fetch the audio data from the fileUri
|
|
76
81
|
// const options = {
|
|
@@ -92,9 +97,13 @@ export function useAudioRecorder({ onAudioStream, debug = false, }) {
|
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
else if (buffer) {
|
|
95
|
-
const data = await buffer.arrayBuffer();
|
|
96
100
|
// Coming from web
|
|
97
|
-
onAudioStream?.({
|
|
101
|
+
onAudioStream?.({
|
|
102
|
+
data: buffer,
|
|
103
|
+
position,
|
|
104
|
+
eventDataSize: deltaSize,
|
|
105
|
+
totalSize,
|
|
106
|
+
});
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
}
|
|
@@ -130,9 +139,11 @@ export function useAudioRecorder({ onAudioStream, debug = false, }) {
|
|
|
130
139
|
}
|
|
131
140
|
}, [debug]);
|
|
132
141
|
const stopRecording = useCallback(async () => {
|
|
142
|
+
console.log(`STOOOOOP NOW`);
|
|
143
|
+
const result = await ExpoAudioStreamModule.stopRecording();
|
|
144
|
+
console.log(`STOOOOOP NOW 2`);
|
|
133
145
|
setIsRecording(false);
|
|
134
146
|
setIsPaused(false);
|
|
135
|
-
const result = await ExpoAudioStreamModule.stopRecording();
|
|
136
147
|
return result;
|
|
137
148
|
}, []);
|
|
138
149
|
const pauseRecording = useCallback(async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAudioRecording.js","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"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","sourcesContent":["import { Platform } from \"expo-modules-core\";\nimport { useCallback, useEffect, useState } from \"react\";\n\nimport { addAudioEventListener } from \".\";\nimport {\n AudioStreamResult,\n AudioStreamStatus,\n RecordingOptions,\n} from \"./ExpoAudioStream.types\";\nimport ExpoAudioStreamModule from \"./ExpoAudioStreamModule\";\n\nexport interface AudioDataEvent {\n data: string | ArrayBuffer;\n position: number;\n size: number;\n}\nexport interface UseAudioRecorderState {\n startRecording: (_: RecordingOptions) => Promise<string | null>;\n stopRecording: () => Promise<AudioStreamResult | null>;\n pauseRecording: () => void;\n isRecording: boolean;\n isPaused: boolean;\n duration: number; // Duration of the recording\n size: number; // Size in bytes of the recorded audio\n}\n\nexport function useAudioRecorder({\n onAudioStream,\n debug = false,\n}: {\n onAudioStream?: (_: AudioDataEvent) => Promise<void>;\n debug?: boolean;\n}): UseAudioRecorderState {\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [size, setSize] = useState(0);\n\n useEffect(() => {\n if (debug) {\n console.log(\n `[useAudioRecorder] isRecording: ${isRecording}, isPaused: ${isPaused}`,\n );\n }\n if (isRecording || isPaused) {\n const interval = setInterval(() => {\n try {\n if (debug) console.log(`[useAudioRecorder] Getting status`);\n const status: AudioStreamStatus = ExpoAudioStreamModule.status();\n if (debug) {\n console.log(`[useAudioRecorder] Status:`, status);\n }\n // Extract matching file from filesystem\n setDuration(status.duration);\n setSize(status.size);\n } catch (error) {\n console.error(`[useAudioRecorder] Error getting status:`, error);\n }\n }, 1000);\n return () => clearInterval(interval);\n }\n\n return () => null;\n }, [isRecording, isPaused, debug]);\n\n useEffect(() => {\n if (debug) {\n console.log(\n `[useAudioRecorder] Registering audio event listener`,\n onAudioStream,\n );\n }\n const subscribe = addAudioEventListener(\n async ({\n fileUri,\n deltaSize,\n totalSize,\n lastEmittedSize,\n position,\n streamUuid,\n encoded,\n mimeType,\n buffer,\n }) => {\n try {\n if (debug) {\n console.log(`[useAudioRecorder] Received audio event:`, {\n fileUri,\n deltaSize,\n totalSize,\n position,\n mimeType,\n lastEmittedSize,\n streamUuid,\n encodedLength: encoded?.length,\n });\n }\n if (deltaSize > 0) {\n // Coming from native ( ios / android ) otherwise buffer is set\n if (Platform.OS !== \"web\") {\n // Read the audio file as a base64 string for comparison\n try {\n if (!encoded) {\n console.error(\n \"[useAudioRecorder] Encoded audio data is missing\",\n );\n throw new Error(\"Encoded audio data is missing\");\n }\n // const binaryData = atob(encoded);\n // const bytes = new Uint8Array(binaryData.length);\n // for (let i = 0; i < binaryData.length; i++) {\n // bytes[i] = binaryData.charCodeAt(i) & 0xff; // Mask to 8 bits\n // }\n // const arrayBuffer = bytes.buffer;\n\n // if (debug) {\n // console.log(\n // `[useAudioRecorder] Read audio file position=${position} deltaSize: ${deltaSize} vs encoded.length: ${encoded.length}`,\n // );\n // }\n\n onAudioStream?.({ data: encoded, position, size: deltaSize });\n\n // Below code is optional, used to compare encoded data to audio on file system\n // Fetch the audio data from the fileUri\n // const options = {\n // encoding: FileSystem.EncodingType.Base64,\n // position: lastEmittedSize,\n // length: deltaSize,\n // };\n // const base64Content = await FileSystem.readAsStringAsync(fileUri, options);\n // const binaryData = atob(base64Content);\n // const content = new Uint8Array(binaryData.length);\n // for (let i = 0; i < binaryData.length; i++) {\n // content[i] = binaryData.charCodeAt(i);\n // }\n // const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array\n // console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`)\n } catch (error) {\n console.error(\n \"[useAudioRecorder] Error reading audio file:\",\n error,\n );\n }\n } else if (buffer) {\n const data = await buffer.arrayBuffer();\n // Coming from web\n onAudioStream?.({ data, position, size: deltaSize });\n }\n }\n } catch (error) {\n console.error(\n \"[useAudioRecorder] Error processing audio event:\",\n error,\n );\n }\n },\n );\n if (debug) {\n console.log(\n `[useAudioRecorder] Subscribed to audio event listener`,\n subscribe,\n );\n }\n return () => {\n if (debug) {\n console.log(`[useAudioRecorder] Removing audio event listener`);\n }\n subscribe.remove();\n };\n }, []);\n\n const startRecording = useCallback(\n async (recordingOptions: RecordingOptions) => {\n setIsRecording(true);\n setIsPaused(false);\n setSize(0);\n setDuration(0);\n try {\n if (debug) {\n console.log(`[useAudioRecorder] start recoding`, recordingOptions);\n }\n\n const fileUrl =\n await ExpoAudioStreamModule.startRecording(recordingOptions);\n\n return fileUrl;\n } catch (error) {\n console.error(\"[useAudioRecorder] Error starting recording:\", error);\n setIsRecording(false);\n }\n },\n [debug],\n );\n\n const stopRecording = useCallback(async () => {\n setIsRecording(false);\n setIsPaused(false);\n const result: AudioStreamResult =\n await ExpoAudioStreamModule.stopRecording();\n return result;\n }, []);\n\n const pauseRecording = useCallback(async () => {\n try {\n await ExpoAudioStreamModule.stopRecording();\n setIsPaused(true);\n setIsRecording(false);\n } catch (error) {\n console.error(\"[useAudioRecorder] Error pausing recording:\", error);\n }\n }, [debug]);\n\n return {\n startRecording,\n stopRecording,\n pauseRecording,\n isPaused,\n isRecording,\n duration,\n size,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"useAudioRecording.js","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"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","sourcesContent":["import { Platform } from \"expo-modules-core\";\nimport { useCallback, useEffect, useState } from \"react\";\n\nimport { addAudioEventListener } from \".\";\nimport {\n AudioStreamResult,\n AudioStreamStatus,\n RecordingOptions,\n StartAudioStreamResult,\n} from \"./ExpoAudioStream.types\";\nimport ExpoAudioStreamModule from \"./ExpoAudioStreamModule\";\n\nexport interface AudioDataEvent {\n data: string | Blob;\n position: number;\n eventDataSize: number;\n totalSize: number;\n}\nexport interface UseAudioRecorderState {\n startRecording: (_: RecordingOptions) => Promise<StartAudioStreamResult>;\n stopRecording: () => Promise<AudioStreamResult | null>;\n pauseRecording: () => void;\n isRecording: boolean;\n isPaused: boolean;\n duration: number; // Duration of the recording\n size: number; // Size in bytes of the recorded audio\n}\n\nexport function useAudioRecorder({\n onAudioStream,\n debug = false,\n}: {\n onAudioStream?: (_: AudioDataEvent) => Promise<void>;\n debug?: boolean;\n}): UseAudioRecorderState {\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [size, setSize] = useState(0);\n\n const checkStatus = useCallback(async () => {\n try {\n if (!isRecording) {\n return;\n }\n\n const status: AudioStreamStatus = ExpoAudioStreamModule.status();\n if (debug) {\n console.log(`[useAudioRecorder] Status:`, status);\n }\n\n if (!status.isRecording) {\n // Don't update if recording stopped.\n return;\n }\n // Extract matching file from filesystem\n setDuration(status.duration);\n setSize(status.size);\n } catch (error) {\n console.error(`[useAudioRecorder] Error getting status:`, error);\n }\n }, [isRecording]);\n\n useEffect(() => {\n const interval = setInterval(checkStatus, 1000);\n return () => clearInterval(interval);\n }, [checkStatus]);\n\n useEffect(() => {\n if (debug) {\n console.log(\n `[useAudioRecorder] Registering audio event listener`,\n onAudioStream,\n );\n }\n const subscribe = addAudioEventListener(\n async ({\n fileUri,\n deltaSize,\n totalSize,\n lastEmittedSize,\n position,\n streamUuid,\n encoded,\n mimeType,\n buffer,\n }) => {\n try {\n if (debug) {\n console.log(`[useAudioRecorder] Received audio event:`, {\n fileUri,\n deltaSize,\n totalSize,\n position,\n mimeType,\n lastEmittedSize,\n streamUuid,\n encodedLength: encoded?.length,\n });\n }\n if (deltaSize > 0) {\n // Coming from native ( ios / android ) otherwise buffer is set\n if (Platform.OS !== \"web\") {\n // Read the audio file as a base64 string for comparison\n try {\n if (!encoded) {\n console.error(\n \"[useAudioRecorder] Encoded audio data is missing\",\n );\n throw new Error(\"Encoded audio data is missing\");\n }\n // const binaryData = atob(encoded);\n // const bytes = new Uint8Array(binaryData.length);\n // for (let i = 0; i < binaryData.length; i++) {\n // bytes[i] = binaryData.charCodeAt(i) & 0xff; // Mask to 8 bits\n // }\n // const arrayBuffer = bytes.buffer;\n\n // if (debug) {\n // console.log(\n // `[useAudioRecorder] Read audio file position=${position} deltaSize: ${deltaSize} vs encoded.length: ${encoded.length}`,\n // );\n // }\n\n onAudioStream?.({\n data: encoded,\n position,\n eventDataSize: deltaSize,\n totalSize,\n });\n\n // Below code is optional, used to compare encoded data to audio on file system\n // Fetch the audio data from the fileUri\n // const options = {\n // encoding: FileSystem.EncodingType.Base64,\n // position: lastEmittedSize,\n // length: deltaSize,\n // };\n // const base64Content = await FileSystem.readAsStringAsync(fileUri, options);\n // const binaryData = atob(base64Content);\n // const content = new Uint8Array(binaryData.length);\n // for (let i = 0; i < binaryData.length; i++) {\n // content[i] = binaryData.charCodeAt(i);\n // }\n // const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array\n // console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`)\n } catch (error) {\n console.error(\n \"[useAudioRecorder] Error reading audio file:\",\n error,\n );\n }\n } else if (buffer) {\n // Coming from web\n onAudioStream?.({\n data: buffer,\n position,\n eventDataSize: deltaSize,\n totalSize,\n });\n }\n }\n } catch (error) {\n console.error(\n \"[useAudioRecorder] Error processing audio event:\",\n error,\n );\n }\n },\n );\n if (debug) {\n console.log(\n `[useAudioRecorder] Subscribed to audio event listener`,\n subscribe,\n );\n }\n return () => {\n if (debug) {\n console.log(`[useAudioRecorder] Removing audio event listener`);\n }\n subscribe.remove();\n };\n }, []);\n\n const startRecording = useCallback(\n async (recordingOptions: RecordingOptions) => {\n setIsRecording(true);\n setIsPaused(false);\n setSize(0);\n setDuration(0);\n try {\n if (debug) {\n console.log(`[useAudioRecorder] start recoding`, recordingOptions);\n }\n\n const fileUrl =\n await ExpoAudioStreamModule.startRecording(recordingOptions);\n\n return fileUrl;\n } catch (error) {\n console.error(\"[useAudioRecorder] Error starting recording:\", error);\n setIsRecording(false);\n }\n },\n [debug],\n );\n\n const stopRecording = useCallback(async () => {\n console.log(`STOOOOOP NOW`);\n const result: AudioStreamResult =\n await ExpoAudioStreamModule.stopRecording();\n console.log(`STOOOOOP NOW 2`);\n setIsRecording(false);\n setIsPaused(false);\n return result;\n }, []);\n\n const pauseRecording = useCallback(async () => {\n try {\n await ExpoAudioStreamModule.stopRecording();\n setIsPaused(true);\n setIsRecording(false);\n } catch (error) {\n console.error(\"[useAudioRecorder] Error pausing recording:\", error);\n }\n }, [debug]);\n\n return {\n startRecording,\n stopRecording,\n pauseRecording,\n isPaused,\n isRecording,\n duration,\n size,\n };\n}\n"]}
|
|
@@ -40,6 +40,14 @@ struct RecordingResult {
|
|
|
40
40
|
var sampleRate: Double
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
struct StartRecordingResult {
|
|
44
|
+
var fileUri: String
|
|
45
|
+
var mimeType: String
|
|
46
|
+
var channels: Int
|
|
47
|
+
var bitDepth: Int
|
|
48
|
+
var sampleRate: Double
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
protocol AudioStreamManagerDelegate: AnyObject {
|
|
44
52
|
func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64)
|
|
45
53
|
}
|
|
@@ -68,6 +76,9 @@ class AudioStreamManager: NSObject {
|
|
|
68
76
|
internal var recordingSettings: RecordingSettings?
|
|
69
77
|
internal var recordingUUID: UUID?
|
|
70
78
|
internal var mimeType: String = "audio/wav"
|
|
79
|
+
private var lastBuffer: AVAudioPCMBuffer?
|
|
80
|
+
private var lastBufferTime: AVAudioTime?
|
|
81
|
+
|
|
71
82
|
weak var delegate: AudioStreamManagerDelegate? // Define the delegate here
|
|
72
83
|
|
|
73
84
|
override init() {
|
|
@@ -123,8 +134,8 @@ class AudioStreamManager: NSObject {
|
|
|
123
134
|
let channels = UInt32(recordingSettings!.numberOfChannels)
|
|
124
135
|
let bitDepth = UInt32(recordingSettings!.bitDepth)
|
|
125
136
|
|
|
126
|
-
let byteRate = sampleRate * channels * (bitDepth / 8)
|
|
127
137
|
let blockAlign = channels * (bitDepth / 8)
|
|
138
|
+
let byteRate = sampleRate * blockAlign
|
|
128
139
|
|
|
129
140
|
// "RIFF" chunk descriptor
|
|
130
141
|
header.append(contentsOf: "RIFF".utf8)
|
|
@@ -138,7 +149,7 @@ class AudioStreamManager: NSObject {
|
|
|
138
149
|
header.append(contentsOf: UInt16(channels).littleEndianBytes)
|
|
139
150
|
header.append(contentsOf: sampleRate.littleEndianBytes)
|
|
140
151
|
header.append(contentsOf: byteRate.littleEndianBytes) // byteRate
|
|
141
|
-
header.append(contentsOf: UInt16(
|
|
152
|
+
header.append(contentsOf: UInt16(blockAlign).littleEndianBytes) // blockAlign
|
|
142
153
|
header.append(contentsOf: UInt16(bitDepth).littleEndianBytes) // bits per sample
|
|
143
154
|
|
|
144
155
|
// "data" sub-chunk
|
|
@@ -176,7 +187,7 @@ class AudioStreamManager: NSObject {
|
|
|
176
187
|
|
|
177
188
|
}
|
|
178
189
|
|
|
179
|
-
func startRecording(settings: RecordingSettings, intervalMilliseconds: Int)
|
|
190
|
+
func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> StartRecordingResult? {
|
|
180
191
|
guard !isRecording else {
|
|
181
192
|
Logger.debug("Debug: Recording is already in progress.")
|
|
182
193
|
return nil
|
|
@@ -234,7 +245,12 @@ class AudioStreamManager: NSObject {
|
|
|
234
245
|
let formatDescription = describeAudioFormat(buffer.format)
|
|
235
246
|
Logger.debug("Debug: Buffer format - \(formatDescription)")
|
|
236
247
|
|
|
237
|
-
|
|
248
|
+
// Processing the current buffer
|
|
249
|
+
self.processAudioBuffer(buffer, fileURL: self.recordingFileURL!)
|
|
250
|
+
|
|
251
|
+
// Store the buffer and time as the last buffer and time
|
|
252
|
+
self.lastBuffer = buffer
|
|
253
|
+
self.lastBufferTime = time
|
|
238
254
|
}
|
|
239
255
|
|
|
240
256
|
recordingFileURL = createRecordingFile()
|
|
@@ -248,7 +264,13 @@ class AudioStreamManager: NSObject {
|
|
|
248
264
|
try audioEngine.start()
|
|
249
265
|
isRecording = true
|
|
250
266
|
Logger.debug("Debug: Recording started successfully.")
|
|
251
|
-
return
|
|
267
|
+
return StartRecordingResult(
|
|
268
|
+
fileUri: recordingFileURL!.path,
|
|
269
|
+
mimeType: mimeType,
|
|
270
|
+
channels: settings.numberOfChannels,
|
|
271
|
+
bitDepth: settings.bitDepth,
|
|
272
|
+
sampleRate: settings.sampleRate
|
|
273
|
+
)
|
|
252
274
|
} catch {
|
|
253
275
|
Logger.debug("Error: Could not start the audio engine: \(error.localizedDescription)")
|
|
254
276
|
isRecording = false
|
|
@@ -287,6 +309,11 @@ class AudioStreamManager: NSObject {
|
|
|
287
309
|
return nil
|
|
288
310
|
}
|
|
289
311
|
|
|
312
|
+
// Ensure all remaining audio data is sent
|
|
313
|
+
if let lastBuffer = lastBuffer, let lastBufferTime = lastBufferTime {
|
|
314
|
+
self.processAudioBuffer(lastBuffer, fileURL: fileURL)
|
|
315
|
+
}
|
|
316
|
+
|
|
290
317
|
let endTime = Date()
|
|
291
318
|
let duration = Int64(endTime.timeIntervalSince(startTime) * 1000) - Int64(pausedDuration * 1000)
|
|
292
319
|
|
|
@@ -309,6 +336,9 @@ class AudioStreamManager: NSObject {
|
|
|
309
336
|
sampleRate: settings.sampleRate
|
|
310
337
|
)
|
|
311
338
|
recordingFileURL = nil // Reset for next recording
|
|
339
|
+
lastBuffer = nil // Reset last buffer
|
|
340
|
+
lastBufferTime = nil // Reset last buffer time
|
|
341
|
+
|
|
312
342
|
return result
|
|
313
343
|
} catch {
|
|
314
344
|
print("Failed to fetch file attributes: \(error)")
|
|
@@ -31,9 +31,9 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
31
31
|
let interval = options["interval"] as? Int ?? 1000
|
|
32
32
|
|
|
33
33
|
let settings = RecordingSettings(sampleRate: sampleRate, numberOfChannels: numberOfChannels, bitDepth: bitDepth)
|
|
34
|
-
let
|
|
34
|
+
let result = self.streamManager.startRecording(settings: settings, intervalMilliseconds: interval)
|
|
35
35
|
|
|
36
|
-
promise.resolve(
|
|
36
|
+
promise.resolve(result)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
package/package.json
CHANGED
|
@@ -20,6 +20,14 @@ export interface AudioStreamResult {
|
|
|
20
20
|
sampleRate?: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface StartAudioStreamResult {
|
|
24
|
+
fileUri: string;
|
|
25
|
+
mimeType: string;
|
|
26
|
+
channels?: number;
|
|
27
|
+
bitDepth?: number;
|
|
28
|
+
sampleRate?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export interface AudioStreamStatus {
|
|
24
32
|
isRecording: boolean;
|
|
25
33
|
isPaused: boolean;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
AudioEventPayload,
|
|
6
6
|
AudioStreamResult,
|
|
7
7
|
RecordingOptions,
|
|
8
|
+
StartAudioStreamResult,
|
|
8
9
|
} from "./ExpoAudioStream.types";
|
|
9
10
|
|
|
10
11
|
const log = debug("expo-audio-stream:useAudioRecording");
|
|
@@ -74,7 +75,11 @@ class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
74
75
|
this.lastEmittedTime = 0;
|
|
75
76
|
this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session
|
|
76
77
|
const fileUri = `${this.streamUuid}.webm`;
|
|
77
|
-
|
|
78
|
+
const streamConfig: StartAudioStreamResult = {
|
|
79
|
+
fileUri,
|
|
80
|
+
mimeType: "audio/webm",
|
|
81
|
+
};
|
|
82
|
+
return streamConfig;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
// Setup listeners for the MediaRecorder
|
package/src/useAudioRecording.ts
CHANGED
|
@@ -6,16 +6,18 @@ import {
|
|
|
6
6
|
AudioStreamResult,
|
|
7
7
|
AudioStreamStatus,
|
|
8
8
|
RecordingOptions,
|
|
9
|
+
StartAudioStreamResult,
|
|
9
10
|
} from "./ExpoAudioStream.types";
|
|
10
11
|
import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
|
|
11
12
|
|
|
12
13
|
export interface AudioDataEvent {
|
|
13
|
-
data: string |
|
|
14
|
+
data: string | Blob;
|
|
14
15
|
position: number;
|
|
15
|
-
|
|
16
|
+
eventDataSize: number;
|
|
17
|
+
totalSize: number;
|
|
16
18
|
}
|
|
17
19
|
export interface UseAudioRecorderState {
|
|
18
|
-
startRecording: (_: RecordingOptions) => Promise<
|
|
20
|
+
startRecording: (_: RecordingOptions) => Promise<StartAudioStreamResult>;
|
|
19
21
|
stopRecording: () => Promise<AudioStreamResult | null>;
|
|
20
22
|
pauseRecording: () => void;
|
|
21
23
|
isRecording: boolean;
|
|
@@ -36,32 +38,33 @@ export function useAudioRecorder({
|
|
|
36
38
|
const [duration, setDuration] = useState(0);
|
|
37
39
|
const [size, setSize] = useState(0);
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}, 1000);
|
|
60
|
-
return () => clearInterval(interval);
|
|
41
|
+
const checkStatus = useCallback(async () => {
|
|
42
|
+
try {
|
|
43
|
+
if (!isRecording) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const status: AudioStreamStatus = ExpoAudioStreamModule.status();
|
|
48
|
+
if (debug) {
|
|
49
|
+
console.log(`[useAudioRecorder] Status:`, status);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!status.isRecording) {
|
|
53
|
+
// Don't update if recording stopped.
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Extract matching file from filesystem
|
|
57
|
+
setDuration(status.duration);
|
|
58
|
+
setSize(status.size);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(`[useAudioRecorder] Error getting status:`, error);
|
|
61
61
|
}
|
|
62
|
+
}, [isRecording]);
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const interval = setInterval(checkStatus, 1000);
|
|
66
|
+
return () => clearInterval(interval);
|
|
67
|
+
}, [checkStatus]);
|
|
65
68
|
|
|
66
69
|
useEffect(() => {
|
|
67
70
|
if (debug) {
|
|
@@ -119,7 +122,12 @@ export function useAudioRecorder({
|
|
|
119
122
|
// );
|
|
120
123
|
// }
|
|
121
124
|
|
|
122
|
-
onAudioStream?.({
|
|
125
|
+
onAudioStream?.({
|
|
126
|
+
data: encoded,
|
|
127
|
+
position,
|
|
128
|
+
eventDataSize: deltaSize,
|
|
129
|
+
totalSize,
|
|
130
|
+
});
|
|
123
131
|
|
|
124
132
|
// Below code is optional, used to compare encoded data to audio on file system
|
|
125
133
|
// Fetch the audio data from the fileUri
|
|
@@ -143,9 +151,13 @@ export function useAudioRecorder({
|
|
|
143
151
|
);
|
|
144
152
|
}
|
|
145
153
|
} else if (buffer) {
|
|
146
|
-
const data = await buffer.arrayBuffer();
|
|
147
154
|
// Coming from web
|
|
148
|
-
onAudioStream?.({
|
|
155
|
+
onAudioStream?.({
|
|
156
|
+
data: buffer,
|
|
157
|
+
position,
|
|
158
|
+
eventDataSize: deltaSize,
|
|
159
|
+
totalSize,
|
|
160
|
+
});
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
} catch (error) {
|
|
@@ -194,10 +206,12 @@ export function useAudioRecorder({
|
|
|
194
206
|
);
|
|
195
207
|
|
|
196
208
|
const stopRecording = useCallback(async () => {
|
|
197
|
-
|
|
198
|
-
setIsPaused(false);
|
|
209
|
+
console.log(`STOOOOOP NOW`);
|
|
199
210
|
const result: AudioStreamResult =
|
|
200
211
|
await ExpoAudioStreamModule.stopRecording();
|
|
212
|
+
console.log(`STOOOOOP NOW 2`);
|
|
213
|
+
setIsRecording(false);
|
|
214
|
+
setIsPaused(false);
|
|
201
215
|
return result;
|
|
202
216
|
}, []);
|
|
203
217
|
|