@siteed/audio-studio 3.0.3 → 3.0.4

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.
@@ -16,6 +16,8 @@ import expo.modules.interfaces.permissions.Permissions
16
16
  import java.util.zip.CRC32
17
17
  import kotlinx.coroutines.CoroutineScope
18
18
  import kotlinx.coroutines.Dispatchers
19
+ import kotlinx.coroutines.SupervisorJob
20
+ import kotlinx.coroutines.cancelChildren
19
21
  import kotlinx.coroutines.launch
20
22
  import kotlinx.coroutines.withContext
21
23
 
@@ -31,7 +33,7 @@ class AudioStudioModule : Module(), EventSender {
31
33
  private var enableNotificationHandling: Boolean = false // Default to false until we check manifest
32
34
  private var enableBackgroundAudio: Boolean = false // Default to false until we check manifest
33
35
  private var enableDeviceDetection: Boolean = false // Default to false until we check manifest
34
- private val coroutineScope = CoroutineScope(Dispatchers.Main)
36
+ private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
35
37
 
36
38
  private val audioFileHandler by lazy {
37
39
  AudioFileHandler(appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available"))
@@ -183,28 +185,27 @@ class AudioStudioModule : Module(), EventSender {
183
185
 
184
186
 
185
187
  AsyncFunction("prepareRecording") { options: Map<String, Any?>, promise: Promise ->
186
- try {
187
- // If notifications are requested but permission not in manifest, modify options
188
- if (options["showNotification"] as? Boolean == true && !enableNotificationHandling) {
189
- val modifiedOptions = options.toMutableMap()
190
- modifiedOptions["showNotification"] = false
191
- LogUtils.d(CLASS_NAME, "Notification permission not in manifest, disabling showNotification")
192
-
193
- if (audioRecorderManager.prepareRecording(modifiedOptions)) {
194
- promise.resolve(true)
188
+ // Heavy native init (AudioRecord probe, MediaRecorder.prepare, file I/O)
189
+ // must run off the main thread to keep the JS/UI thread responsive.
190
+ // Module-scoped Job ensures cancellation on module destroy.
191
+ coroutineScope.launch(Dispatchers.IO) {
192
+ try {
193
+ val opts = if (options["showNotification"] as? Boolean == true && !enableNotificationHandling) {
194
+ LogUtils.d(CLASS_NAME, "Notification permission not in manifest, disabling showNotification")
195
+ options.toMutableMap().apply { this["showNotification"] = false }
195
196
  } else {
196
- promise.reject("PREPARE_ERROR", "Failed to prepare recording", null)
197
+ options
197
198
  }
198
- } else {
199
- if (audioRecorderManager.prepareRecording(options)) {
199
+
200
+ if (audioRecorderManager.prepareRecording(opts)) {
200
201
  promise.resolve(true)
201
202
  } else {
202
203
  promise.reject("PREPARE_ERROR", "Failed to prepare recording", null)
203
204
  }
205
+ } catch (e: Exception) {
206
+ LogUtils.e(CLASS_NAME, "Error preparing recording", e)
207
+ promise.reject("PREPARE_ERROR", "Failed to prepare recording: ${e.message}", e)
204
208
  }
205
- } catch (e: Exception) {
206
- LogUtils.e(CLASS_NAME, "Error preparing recording", e)
207
- promise.reject("PREPARE_ERROR", "Failed to prepare recording: ${e.message}", e)
208
209
  }
209
210
  }
210
211
 
@@ -368,287 +369,294 @@ class AudioStudioModule : Module(), EventSender {
368
369
  }
369
370
 
370
371
  AsyncFunction("trimAudio") { options: Map<String, Any>, promise: Promise ->
371
- try {
372
- val fileUri = options["fileUri"] as? String ?: run {
373
- promise.reject("INVALID_URI", "fileUri is required", null)
374
- return@AsyncFunction
375
- }
372
+ // Trim does heavy decode/encode + file I/O — must run off the
373
+ // shared module executor so other JS calls don't queue behind it.
374
+ coroutineScope.launch(Dispatchers.IO) {
375
+ try {
376
+ val fileUri = options["fileUri"] as? String ?: run {
377
+ promise.reject("INVALID_URI", "fileUri is required", null)
378
+ return@launch
379
+ }
376
380
 
377
- LogUtils.d(CLASS_NAME, "trimAudio called with fileUri: $fileUri")
378
- LogUtils.d(CLASS_NAME, "Full options: $options")
381
+ LogUtils.d(CLASS_NAME, "trimAudio called with fileUri: $fileUri")
382
+ LogUtils.d(CLASS_NAME, "Full options: $options")
379
383
 
380
- val mode = options["mode"] as? String ?: "single"
381
- val startTimeMs = (options["startTimeMs"] as? Number)?.toLong()
382
- val endTimeMs = (options["endTimeMs"] as? Number)?.toLong()
383
-
384
- @Suppress("UNCHECKED_CAST")
385
- val rawRanges = options["ranges"] as? List<Map<String, Any>>
386
- val ranges = rawRanges?.map { range ->
387
- mapOf(
388
- "startTimeMs" to ((range["startTimeMs"] as? Number)?.toLong() ?: 0L),
389
- "endTimeMs" to ((range["endTimeMs"] as? Number)?.toLong() ?: 0L)
390
- )
391
- }
392
-
393
- val outputFileName = options["outputFileName"] as? String
394
-
395
- @Suppress("UNCHECKED_CAST")
396
- var outputFormatMap = options["outputFormat"] as? Map<String, Any>
397
-
398
- // Validate output format if provided
399
- if (outputFormatMap != null) {
400
- val format = outputFormatMap["format"] as? String
401
- if (format != null && format != "wav" && format != "aac" && format != "opus") {
402
- LogUtils.w(CLASS_NAME, "Requested format '$format' is not fully supported. Using 'aac' instead.")
403
- // Create a new map with the corrected format
404
- val newOutputFormat = HashMap<String, Any>(outputFormatMap)
405
- newOutputFormat["format"] = "aac"
406
- outputFormatMap = newOutputFormat
384
+ val mode = options["mode"] as? String ?: "single"
385
+ val startTimeMs = (options["startTimeMs"] as? Number)?.toLong()
386
+ val endTimeMs = (options["endTimeMs"] as? Number)?.toLong()
387
+
388
+ @Suppress("UNCHECKED_CAST")
389
+ val rawRanges = options["ranges"] as? List<Map<String, Any>>
390
+ val ranges = rawRanges?.map { range ->
391
+ mapOf(
392
+ "startTimeMs" to ((range["startTimeMs"] as? Number)?.toLong() ?: 0L),
393
+ "endTimeMs" to ((range["endTimeMs"] as? Number)?.toLong() ?: 0L)
394
+ )
407
395
  }
408
- }
409
-
410
- LogUtils.d(CLASS_NAME, "Output format options: $outputFormatMap")
411
-
412
- // Create progress listener
413
- val progressListener = object : AudioTrimmer.ProgressListener {
414
- override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
415
- sendEvent(Constants.TRIM_PROGRESS_EVENT, mapOf(
416
- "progress" to progress,
417
- "bytesProcessed" to bytesProcessed,
418
- "totalBytes" to totalBytes
419
- ))
396
+
397
+ val outputFileName = options["outputFileName"] as? String
398
+
399
+ @Suppress("UNCHECKED_CAST")
400
+ var outputFormatMap = options["outputFormat"] as? Map<String, Any>
401
+
402
+ if (outputFormatMap != null) {
403
+ val format = outputFormatMap["format"] as? String
404
+ if (format != null && format != "wav" && format != "aac" && format != "opus") {
405
+ LogUtils.w(CLASS_NAME, "Requested format '$format' is not fully supported. Using 'aac' instead.")
406
+ val newOutputFormat = HashMap<String, Any>(outputFormatMap)
407
+ newOutputFormat["format"] = "aac"
408
+ outputFormatMap = newOutputFormat
409
+ }
420
410
  }
421
- }
422
411
 
423
- // Record start time
424
- val startTime = System.currentTimeMillis()
425
-
426
- // Perform the trim operation
427
- val result = audioTrimmer.trimAudio(
428
- fileUri = fileUri,
429
- mode = mode,
430
- startTimeMs = startTimeMs,
431
- endTimeMs = endTimeMs,
432
- ranges = ranges,
433
- outputFileName = outputFileName,
434
- outputFormat = outputFormatMap,
435
- progressListener = progressListener
436
- )
412
+ LogUtils.d(CLASS_NAME, "Output format options: $outputFormatMap")
437
413
 
438
- // Calculate processing time
439
- val processingTimeMs = System.currentTimeMillis() - startTime
440
-
441
- // Add processing time to result
442
- val resultWithProcessingTime = result.toMutableMap()
443
- resultWithProcessingTime["processingInfo"] = mapOf(
444
- "durationMs" to processingTimeMs
445
- )
414
+ val progressListener = object : AudioTrimmer.ProgressListener {
415
+ override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
416
+ sendEvent(Constants.TRIM_PROGRESS_EVENT, mapOf(
417
+ "progress" to progress,
418
+ "bytesProcessed" to bytesProcessed,
419
+ "totalBytes" to totalBytes
420
+ ))
421
+ }
422
+ }
446
423
 
447
- LogUtils.d(CLASS_NAME, "Trim operation completed successfully in ${processingTimeMs}ms: $result")
448
- promise.resolve(resultWithProcessingTime)
449
- } catch (e: Exception) {
450
- LogUtils.e(CLASS_NAME, "Error trimming audio: ${e.message}", e)
451
- promise.reject("TRIM_ERROR", "Error trimming audio: ${e.message}", e)
424
+ val startTime = System.currentTimeMillis()
425
+
426
+ val result = audioTrimmer.trimAudio(
427
+ fileUri = fileUri,
428
+ mode = mode,
429
+ startTimeMs = startTimeMs,
430
+ endTimeMs = endTimeMs,
431
+ ranges = ranges,
432
+ outputFileName = outputFileName,
433
+ outputFormat = outputFormatMap,
434
+ progressListener = progressListener
435
+ )
436
+
437
+ val processingTimeMs = System.currentTimeMillis() - startTime
438
+
439
+ val resultWithProcessingTime = result.toMutableMap()
440
+ resultWithProcessingTime["processingInfo"] = mapOf(
441
+ "durationMs" to processingTimeMs
442
+ )
443
+
444
+ LogUtils.d(CLASS_NAME, "Trim operation completed successfully in ${processingTimeMs}ms: $result")
445
+ promise.resolve(resultWithProcessingTime)
446
+ } catch (e: Exception) {
447
+ LogUtils.e(CLASS_NAME, "Error trimming audio: ${e.message}", e)
448
+ promise.reject("TRIM_ERROR", "Error trimming audio: ${e.message}", e)
449
+ }
452
450
  }
453
451
  }
454
452
 
455
453
  AsyncFunction("extractMelSpectrogram") { options: Map<String, Any>, promise: Promise ->
456
- try {
457
- // Log all incoming options for debugging
458
- LogUtils.d(CLASS_NAME, "extractMelSpectrogram called with options: $options")
454
+ // Heavy DSP: file decode + STFT + mel projection. Off the shared
455
+ // module executor so other JS calls don't block.
456
+ coroutineScope.launch(Dispatchers.IO) {
457
+ try {
458
+ LogUtils.d(CLASS_NAME, "extractMelSpectrogram called with options: $options")
459
459
 
460
- // Extract required parameters with detailed logging
461
- val fileUri = options["fileUri"] as? String
462
- LogUtils.d(CLASS_NAME, "fileUri: $fileUri")
463
- if (fileUri == null) {
464
- LogUtils.e(CLASS_NAME, "Missing required parameter: fileUri")
465
- throw IllegalArgumentException("fileUri is required")
466
- }
460
+ // Extract required parameters with detailed logging
461
+ val fileUri = options["fileUri"] as? String
462
+ LogUtils.d(CLASS_NAME, "fileUri: $fileUri")
463
+ if (fileUri == null) {
464
+ LogUtils.e(CLASS_NAME, "Missing required parameter: fileUri")
465
+ throw IllegalArgumentException("fileUri is required")
466
+ }
467
467
 
468
- val windowSizeMs = options["windowSizeMs"] as? Double
469
- LogUtils.d(CLASS_NAME, "windowSizeMs: $windowSizeMs")
470
- if (windowSizeMs == null) {
471
- LogUtils.e(CLASS_NAME, "Missing required parameter: windowSizeMs")
472
- throw IllegalArgumentException("windowSizeMs is required")
473
- }
468
+ val windowSizeMs = options["windowSizeMs"] as? Double
469
+ LogUtils.d(CLASS_NAME, "windowSizeMs: $windowSizeMs")
470
+ if (windowSizeMs == null) {
471
+ LogUtils.e(CLASS_NAME, "Missing required parameter: windowSizeMs")
472
+ throw IllegalArgumentException("windowSizeMs is required")
473
+ }
474
474
 
475
- val hopLengthMs = options["hopLengthMs"] as? Double
476
- LogUtils.d(CLASS_NAME, "hopLengthMs: $hopLengthMs")
477
- if (hopLengthMs == null) {
478
- LogUtils.e(CLASS_NAME, "Missing required parameter: hopLengthMs")
479
- throw IllegalArgumentException("hopLengthMs is required")
480
- }
475
+ val hopLengthMs = options["hopLengthMs"] as? Double
476
+ LogUtils.d(CLASS_NAME, "hopLengthMs: $hopLengthMs")
477
+ if (hopLengthMs == null) {
478
+ LogUtils.e(CLASS_NAME, "Missing required parameter: hopLengthMs")
479
+ throw IllegalArgumentException("hopLengthMs is required")
480
+ }
481
481
 
482
- // Handle nMels which might come as Double from JavaScript
483
- val nMelsValue = options["nMels"]
484
- LogUtils.d(CLASS_NAME, "Raw nMels value: $nMelsValue (type: ${nMelsValue?.javaClass?.name})")
482
+ // Handle nMels which might come as Double from JavaScript
483
+ val nMelsValue = options["nMels"]
484
+ LogUtils.d(CLASS_NAME, "Raw nMels value: $nMelsValue (type: ${nMelsValue?.javaClass?.name})")
485
485
 
486
- val nMels = when (nMelsValue) {
487
- is Int -> nMelsValue
488
- is Double -> nMelsValue.toInt()
489
- is Number -> nMelsValue.toInt()
490
- else -> {
491
- LogUtils.e(CLASS_NAME, "Missing or invalid required parameter: nMels")
492
- throw IllegalArgumentException("nMels is required and must be a number")
486
+ val nMels = when (nMelsValue) {
487
+ is Int -> nMelsValue
488
+ is Double -> nMelsValue.toInt()
489
+ is Number -> nMelsValue.toInt()
490
+ else -> {
491
+ LogUtils.e(CLASS_NAME, "Missing or invalid required parameter: nMels")
492
+ throw IllegalArgumentException("nMels is required and must be a number")
493
+ }
493
494
  }
494
- }
495
495
 
496
- LogUtils.d(CLASS_NAME, "Converted nMels: $nMels (from ${nMelsValue?.javaClass?.name})")
497
-
498
- // Extract optional parameters with defaults
499
- val fMin = options["fMin"] as? Double ?: 0.0
500
- val fMax = options["fMax"] as? Double
501
- val windowType = options["windowType"] as? String ?: "hann"
502
- val normalize = options["normalize"] as? Boolean ?: false
503
- val logScale = options["logScale"] as? Boolean ?: true
496
+ LogUtils.d(CLASS_NAME, "Converted nMels: $nMels (from ${nMelsValue?.javaClass?.name})")
497
+
498
+ // Extract optional parameters with defaults
499
+ val fMin = options["fMin"] as? Double ?: 0.0
500
+ val fMax = options["fMax"] as? Double
501
+ val windowType = options["windowType"] as? String ?: "hann"
502
+ val normalize = options["normalize"] as? Boolean ?: false
503
+ val logScale = options["logScale"] as? Boolean ?: true
504
504
 
505
- // Fix the conversion from Number to Long to preserve decimal values
506
- val startTimeMsNumber = options["startTimeMs"] as? Number
507
- val endTimeMsNumber = options["endTimeMs"] as? Number
508
- val startTimeMs = startTimeMsNumber?.toLong() ?: startTimeMsNumber?.toDouble()?.toLong()
509
- val endTimeMs = endTimeMsNumber?.toLong() ?: endTimeMsNumber?.toDouble()?.toLong()
510
-
511
- LogUtils.d(CLASS_NAME, """
512
- Optional parameters:
513
- - fMin: $fMin
514
- - fMax: $fMax
515
- - windowType: $windowType
516
- - normalize: $normalize
517
- - logScale: $logScale
518
- - startTimeMs: $startTimeMs (original: $startTimeMsNumber)
519
- - endTimeMs: $endTimeMs (original: $endTimeMsNumber)
520
- """.trimIndent())
521
-
522
- // Handle decoding options
523
- val decodingOptions = options["decodingOptions"] as? Map<String, Any>
524
- LogUtils.d(CLASS_NAME, "Decoding options: $decodingOptions")
505
+ // Fix the conversion from Number to Long to preserve decimal values
506
+ val startTimeMsNumber = options["startTimeMs"] as? Number
507
+ val endTimeMsNumber = options["endTimeMs"] as? Number
508
+ val startTimeMs = startTimeMsNumber?.toLong() ?: startTimeMsNumber?.toDouble()?.toLong()
509
+ val endTimeMs = endTimeMsNumber?.toLong() ?: endTimeMsNumber?.toDouble()?.toLong()
510
+
511
+ LogUtils.d(CLASS_NAME, """
512
+ Optional parameters:
513
+ - fMin: $fMin
514
+ - fMax: $fMax
515
+ - windowType: $windowType
516
+ - normalize: $normalize
517
+ - logScale: $logScale
518
+ - startTimeMs: $startTimeMs (original: $startTimeMsNumber)
519
+ - endTimeMs: $endTimeMs (original: $endTimeMsNumber)
520
+ """.trimIndent())
521
+
522
+ // Handle decoding options
523
+ val decodingOptions = options["decodingOptions"] as? Map<String, Any>
524
+ LogUtils.d(CLASS_NAME, "Decoding options: $decodingOptions")
525
525
 
526
- val config = decodingOptions?.let {
527
- val targetSampleRateValue = it["targetSampleRate"]
528
- val targetSampleRate = when (targetSampleRateValue) {
529
- is Int -> targetSampleRateValue
530
- is Double -> targetSampleRateValue.toInt()
531
- is Number -> targetSampleRateValue.toInt()
532
- else -> null
533
- }
526
+ val config = decodingOptions?.let {
527
+ val targetSampleRateValue = it["targetSampleRate"]
528
+ val targetSampleRate = when (targetSampleRateValue) {
529
+ is Int -> targetSampleRateValue
530
+ is Double -> targetSampleRateValue.toInt()
531
+ is Number -> targetSampleRateValue.toInt()
532
+ else -> null
533
+ }
534
534
 
535
- val targetChannelsValue = it["targetChannels"]
536
- val targetChannels = when (targetChannelsValue) {
537
- is Int -> targetChannelsValue
538
- is Double -> targetChannelsValue.toInt()
539
- is Number -> targetChannelsValue.toInt()
540
- else -> 1
541
- }
535
+ val targetChannelsValue = it["targetChannels"]
536
+ val targetChannels = when (targetChannelsValue) {
537
+ is Int -> targetChannelsValue
538
+ is Double -> targetChannelsValue.toInt()
539
+ is Number -> targetChannelsValue.toInt()
540
+ else -> 1
541
+ }
542
542
 
543
- val targetBitDepthValue = it["targetBitDepth"]
544
- val targetBitDepth = when (targetBitDepthValue) {
545
- is Int -> targetBitDepthValue
546
- is Double -> targetBitDepthValue.toInt()
547
- is Number -> targetBitDepthValue.toInt()
548
- else -> 16
549
- }
543
+ val targetBitDepthValue = it["targetBitDepth"]
544
+ val targetBitDepth = when (targetBitDepthValue) {
545
+ is Int -> targetBitDepthValue
546
+ is Double -> targetBitDepthValue.toInt()
547
+ is Number -> targetBitDepthValue.toInt()
548
+ else -> 16
549
+ }
550
550
 
551
- val normalizeAudio = it["normalizeAudio"] as? Boolean ?: false
551
+ val normalizeAudio = it["normalizeAudio"] as? Boolean ?: false
552
552
 
553
- DecodingConfig(
554
- targetSampleRate = targetSampleRate,
555
- targetChannels = targetChannels,
556
- targetBitDepth = targetBitDepth,
557
- normalizeAudio = normalizeAudio
558
- ).also { config ->
559
- LogUtils.d(CLASS_NAME, """
560
- Using decoding config:
561
- - targetSampleRate: ${config.targetSampleRate ?: "original"}
562
- - targetChannels: ${config.targetChannels ?: "original"}
563
- - targetBitDepth: ${config.targetBitDepth}
564
- - normalizeAudio: ${config.normalizeAudio}
565
- """.trimIndent())
553
+ DecodingConfig(
554
+ targetSampleRate = targetSampleRate,
555
+ targetChannels = targetChannels,
556
+ targetBitDepth = targetBitDepth,
557
+ normalizeAudio = normalizeAudio
558
+ ).also { config ->
559
+ LogUtils.d(CLASS_NAME, """
560
+ Using decoding config:
561
+ - targetSampleRate: ${config.targetSampleRate ?: "original"}
562
+ - targetChannels: ${config.targetChannels ?: "original"}
563
+ - targetBitDepth: ${config.targetBitDepth}
564
+ - normalizeAudio: ${config.normalizeAudio}
565
+ """.trimIndent())
566
+ }
567
+ } ?: DecodingConfig(targetSampleRate = null, targetChannels = 1, targetBitDepth = 16).also {
568
+ LogUtils.d(CLASS_NAME, "Using default decoding config")
566
569
  }
567
- } ?: DecodingConfig(targetSampleRate = null, targetChannels = 1, targetBitDepth = 16).also {
568
- LogUtils.d(CLASS_NAME, "Using default decoding config")
569
- }
570
570
 
571
- // Check if the audio data is too short
572
- if (startTimeMs != null && endTimeMs != null) {
573
- val durationMs = endTimeMs - startTimeMs
574
- LogUtils.d(CLASS_NAME, "Audio duration for spectrogram: $durationMs ms")
575
- if (durationMs < 25) { // 25ms is minimum for a single window
576
- LogUtils.w(CLASS_NAME, "Audio duration is too short for spectrogram analysis: $durationMs ms")
577
- throw IllegalArgumentException("Audio duration must be at least 25ms for spectrogram analysis")
571
+ // Check if the audio data is too short
572
+ if (startTimeMs != null && endTimeMs != null) {
573
+ val durationMs = endTimeMs - startTimeMs
574
+ LogUtils.d(CLASS_NAME, "Audio duration for spectrogram: $durationMs ms")
575
+ if (durationMs < 25) { // 25ms is minimum for a single window
576
+ LogUtils.w(CLASS_NAME, "Audio duration is too short for spectrogram analysis: $durationMs ms")
577
+ throw IllegalArgumentException("Audio duration must be at least 25ms for spectrogram analysis")
578
+ }
578
579
  }
579
- }
580
580
 
581
- // Load audio data with optional time range
582
- LogUtils.d(CLASS_NAME, "Loading audio data...")
583
- val audioData = when {
584
- startTimeMs != null && endTimeMs != null -> {
585
- LogUtils.d(CLASS_NAME, "Loading audio range: $startTimeMs to $endTimeMs ms")
586
- audioProcessor.loadAudioRange(fileUri, startTimeMs, endTimeMs, config)
587
- }
588
- else -> {
589
- LogUtils.d(CLASS_NAME, "Loading entire audio file")
590
- audioProcessor.loadAudioFromAnyFormat(fileUri, config)
581
+ // Load audio data with optional time range
582
+ LogUtils.d(CLASS_NAME, "Loading audio data...")
583
+ val audioData = when {
584
+ startTimeMs != null && endTimeMs != null -> {
585
+ LogUtils.d(CLASS_NAME, "Loading audio range: $startTimeMs to $endTimeMs ms")
586
+ audioProcessor.loadAudioRange(fileUri, startTimeMs, endTimeMs, config)
587
+ }
588
+ else -> {
589
+ LogUtils.d(CLASS_NAME, "Loading entire audio file")
590
+ audioProcessor.loadAudioFromAnyFormat(fileUri, config)
591
+ }
591
592
  }
592
- }
593
593
 
594
- if (audioData == null) {
595
- LogUtils.e(CLASS_NAME, "Failed to load audio data")
596
- throw IllegalStateException("Failed to load audio data")
597
- }
594
+ if (audioData == null) {
595
+ LogUtils.e(CLASS_NAME, "Failed to load audio data")
596
+ throw IllegalStateException("Failed to load audio data")
597
+ }
598
598
 
599
- LogUtils.d(CLASS_NAME, """
600
- Audio data loaded successfully:
601
- - data size: ${audioData.data.size} bytes
602
- - sampleRate: ${audioData.sampleRate}
603
- - channels: ${audioData.channels}
604
- - bitDepth: ${audioData.bitDepth}
605
- - durationMs: ${audioData.durationMs}
606
- """.trimIndent())
607
-
608
- // Validate that we have enough audio data for processing
609
- if (audioData.data.size == 0 || audioData.durationMs < windowSizeMs) {
610
- LogUtils.e(CLASS_NAME, "Audio data is too short for spectrogram analysis: ${audioData.durationMs}ms, data size: ${audioData.data.size} bytes")
611
- throw IllegalArgumentException(
612
- "Audio data is too short for spectrogram analysis. " +
613
- "Duration: ${audioData.durationMs}ms, minimum required: ${windowSizeMs}ms"
614
- )
615
- }
599
+ LogUtils.d(CLASS_NAME, """
600
+ Audio data loaded successfully:
601
+ - data size: ${audioData.data.size} bytes
602
+ - sampleRate: ${audioData.sampleRate}
603
+ - channels: ${audioData.channels}
604
+ - bitDepth: ${audioData.bitDepth}
605
+ - durationMs: ${audioData.durationMs}
606
+ """.trimIndent())
616
607
 
617
- // Compute mel-spectrogram
618
- LogUtils.d(CLASS_NAME, "Computing mel-spectrogram...")
619
- val spectrogramData = audioProcessor.extractMelSpectrogram(
620
- audioData = audioData,
621
- windowSizeMs = windowSizeMs.toFloat(),
622
- hopLengthMs = hopLengthMs.toFloat(),
623
- nMels = nMels,
624
- fMin = fMin.toFloat(),
625
- fMax = fMax?.toFloat() ?: (audioData.sampleRate.toFloat() / 2),
626
- normalize = normalize,
627
- logScaling = logScale,
628
- windowType = windowType
629
- )
608
+ // Validate that we have enough audio data for processing
609
+ if (audioData.data.size == 0 || audioData.durationMs < windowSizeMs) {
610
+ LogUtils.e(CLASS_NAME, "Audio data is too short for spectrogram analysis: ${audioData.durationMs}ms, data size: ${audioData.data.size} bytes")
611
+ throw IllegalArgumentException(
612
+ "Audio data is too short for spectrogram analysis. " +
613
+ "Duration: ${audioData.durationMs}ms, minimum required: ${windowSizeMs}ms"
614
+ )
615
+ }
616
+
617
+ // Compute mel-spectrogram
618
+ LogUtils.d(CLASS_NAME, "Computing mel-spectrogram...")
619
+ val spectrogramData = audioProcessor.extractMelSpectrogram(
620
+ audioData = audioData,
621
+ windowSizeMs = windowSizeMs.toFloat(),
622
+ hopLengthMs = hopLengthMs.toFloat(),
623
+ nMels = nMels,
624
+ fMin = fMin.toFloat(),
625
+ fMax = fMax?.toFloat() ?: (audioData.sampleRate.toFloat() / 2),
626
+ normalize = normalize,
627
+ logScaling = logScale,
628
+ windowType = windowType
629
+ )
630
630
 
631
- LogUtils.d(CLASS_NAME, "Mel-spectrogram computed successfully with ${spectrogramData.spectrogram.size} time steps")
632
-
633
- // Convert to map for React Native
634
- val result = mapOf(
635
- "spectrogram" to spectrogramData.spectrogram.map { it.toList() },
636
- "sampleRate" to audioData.sampleRate,
637
- "nMels" to nMels,
638
- "timeSteps" to spectrogramData.spectrogram.size,
639
- "durationMs" to audioData.durationMs
640
- )
631
+ LogUtils.d(CLASS_NAME, "Mel-spectrogram computed successfully with ${spectrogramData.spectrogram.size} time steps")
632
+
633
+ // Convert to map for React Native
634
+ val result = mapOf(
635
+ "spectrogram" to spectrogramData.spectrogram.map { it.toList() },
636
+ "sampleRate" to audioData.sampleRate,
637
+ "nMels" to nMels,
638
+ "timeSteps" to spectrogramData.spectrogram.size,
639
+ "durationMs" to audioData.durationMs
640
+ )
641
641
 
642
- LogUtils.d(CLASS_NAME, "Returning result with ${result["timeSteps"]} time steps and $nMels mel bands")
643
- promise.resolve(result)
644
- } catch (e: Exception) {
645
- LogUtils.e(CLASS_NAME, "Failed to extract mel-spectrogram: ${e.message}")
646
- LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
647
- promise.reject("SPECTROGRAM_ERROR", e.message ?: "Unknown error", e)
642
+ LogUtils.d(CLASS_NAME, "Returning result with ${result["timeSteps"]} time steps and $nMels mel bands")
643
+ promise.resolve(result)
644
+ } catch (e: Exception) {
645
+ LogUtils.e(CLASS_NAME, "Failed to extract mel-spectrogram: ${e.message}")
646
+ LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
647
+ promise.reject("SPECTROGRAM_ERROR", e.message ?: "Unknown error", e)
648
+ }
648
649
  }
649
650
  }
650
651
 
651
652
  OnDestroy {
653
+ // Cancel in-flight prepare/trim/extract coroutines so promises
654
+ // and event sends do not outlive the React context. Use
655
+ // cancelChildren rather than cancel() so the scope itself stays
656
+ // usable: Expo can re-invoke definition() on dev-client reloads
657
+ // while keeping the same module instance, and a fully cancelled
658
+ // scope would silently no-op every subsequent launch.
659
+ coroutineScope.coroutineContext.cancelChildren()
652
660
  AudioRecorderManager.destroy()
653
661
  }
654
662
 
@@ -669,271 +677,278 @@ class AudioStudioModule : Module(), EventSender {
669
677
 
670
678
 
671
679
  AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
672
- try {
673
- val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
680
+ // Off the shared executor so other JS calls don't block during
681
+ // multi-second analysis on large files.
682
+ coroutineScope.launch(Dispatchers.IO) {
683
+ try {
684
+ val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
685
+
686
+ // Get time or byte range options
687
+ val startTimeMs = options["startTimeMs"] as? Number
688
+ val endTimeMs = options["endTimeMs"] as? Number
689
+ val position = options["position"] as? Number
690
+ val length = options["length"] as? Number
691
+ val segmentDurationMs = (options["segmentDurationMs"] as? Number)?.toInt() ?: 100
674
692
 
675
- // Get time or byte range options
676
- val startTimeMs = options["startTimeMs"] as? Number
677
- val endTimeMs = options["endTimeMs"] as? Number
678
- val position = options["position"] as? Number
679
- val length = options["length"] as? Number
680
- val segmentDurationMs = (options["segmentDurationMs"] as? Number)?.toInt() ?: 100
693
+ // Validate ranges - can have time range OR byte range OR no range
694
+ val hasTimeRange = startTimeMs != null && endTimeMs != null
695
+ val hasByteRange = position != null && length != null
681
696
 
682
- // Validate ranges - can have time range OR byte range OR no range
683
- val hasTimeRange = startTimeMs != null && endTimeMs != null
684
- val hasByteRange = position != null && length != null
697
+ // Only throw if both ranges are provided
698
+ if (hasTimeRange && hasByteRange) {
699
+ throw IllegalArgumentException("Cannot specify both time range and byte range")
700
+ }
701
+
702
+ // Get decoding options with default configuration
703
+ val defaultConfig = DecodingConfig(
704
+ targetSampleRate = null,
705
+ targetChannels = 1, // Default to mono
706
+ targetBitDepth = 16,
707
+ normalizeAudio = false
708
+ )
685
709
 
686
- // Only throw if both ranges are provided
687
- if (hasTimeRange && hasByteRange) {
688
- throw IllegalArgumentException("Cannot specify both time range and byte range")
710
+ val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
711
+ DecodingConfig(
712
+ targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
713
+ targetChannels = decodingOptionsMap["targetChannels"] as? Int,
714
+ targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
715
+ normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
716
+ )
717
+ } ?: defaultConfig
718
+
719
+ // Load audio data based on range type (or full file if no range specified)
720
+ val audioData = when {
721
+ hasByteRange -> {
722
+ val format = audioProcessor.getAudioFormat(fileUri)
723
+ ?: throw IllegalArgumentException("Could not determine audio format")
724
+
725
+ // Calculate time range from byte position
726
+ val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
727
+ val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
728
+ val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
729
+
730
+ LogUtils.d(CLASS_NAME, "Loading audio with byte range: position=$position, length=$length")
731
+
732
+ audioProcessor.loadAudioRange(
733
+ fileUri = fileUri,
734
+ startTimeMs = effectiveStartTimeMs,
735
+ endTimeMs = effectiveEndTimeMs,
736
+ config = config
737
+ )
738
+ }
739
+ hasTimeRange -> {
740
+ LogUtils.d(CLASS_NAME, "Loading audio with time range: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs")
741
+
742
+ audioProcessor.loadAudioRange(
743
+ fileUri = fileUri,
744
+ startTimeMs = startTimeMs!!.toLong(),
745
+ endTimeMs = endTimeMs!!.toLong(),
746
+ config = config
747
+ )
748
+ }
749
+ else -> {
750
+ LogUtils.d(CLASS_NAME, "Loading entire audio file")
751
+ audioProcessor.loadAudioFromAnyFormat(fileUri, config)
752
+ }
753
+ } ?: throw IllegalStateException("Failed to load audio data")
754
+
755
+ val featuresMap = options["features"] as? Map<*, *>
756
+ val features = Features.parseFeatureOptions(featuresMap)
757
+
758
+ val recordingConfig = RecordingConfig(
759
+ sampleRate = audioData.sampleRate,
760
+ channels = audioData.channels,
761
+ encoding = when (audioData.bitDepth) {
762
+ 8 -> "pcm_8bit"
763
+ 16 -> "pcm_16bit"
764
+ 32 -> "pcm_32bit"
765
+ else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
766
+ },
767
+ segmentDurationMs = segmentDurationMs,
768
+ features = features
769
+ )
770
+
771
+ LogUtils.d(CLASS_NAME, "extractAudioAnalysis: $recordingConfig")
772
+ audioProcessor.resetCumulativeAmplitudeRange()
773
+
774
+ val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
775
+ promise.resolve(analysisData.toDictionary())
776
+ } catch (e: Exception) {
777
+ LogUtils.e(CLASS_NAME, "Failed to extract audio analysis: ${e.message}", e)
778
+ promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
689
779
  }
780
+ }
781
+ }
690
782
 
691
- // Get decoding options with default configuration
692
- val defaultConfig = DecodingConfig(
693
- targetSampleRate = null,
694
- targetChannels = 1, // Default to mono
695
- targetBitDepth = 16,
696
- normalizeAudio = false
697
- )
783
+ AsyncFunction("extractAudioData") { options: Map<String, Any>, promise: Promise ->
784
+ // Off the shared executor so concurrent JS calls don't block.
785
+ coroutineScope.launch(Dispatchers.IO) {
786
+ try {
787
+ val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
788
+ val startTimeMs = options["startTimeMs"] as? Number
789
+ val endTimeMs = options["endTimeMs"] as? Number
790
+ val position = options["position"] as? Number
791
+ val length = options["length"] as? Number
698
792
 
699
- val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
700
- DecodingConfig(
701
- targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
702
- targetChannels = decodingOptionsMap["targetChannels"] as? Int,
703
- targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
704
- normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
705
- )
706
- } ?: defaultConfig
793
+ // Validate that we have either time range or byte range, but not both and not neither
794
+ val hasTimeRange = startTimeMs != null && endTimeMs != null
795
+ val hasByteRange = position != null && length != null
796
+
797
+ if (!hasTimeRange && !hasByteRange) {
798
+ throw IllegalArgumentException("Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
799
+ }
800
+ if (hasTimeRange && hasByteRange) {
801
+ throw IllegalArgumentException("Cannot specify both time range and byte range")
802
+ }
803
+
804
+ // Get decoding options
805
+ val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
806
+ val decodingConfig = if (decodingOptionsMap != null) {
807
+ DecodingConfig(
808
+ targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
809
+ targetChannels = decodingOptionsMap["targetChannels"] as? Int,
810
+ targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
811
+ normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
812
+ ).also {
813
+ LogUtils.d(CLASS_NAME, """
814
+ Using decoding config:
815
+ - targetSampleRate: ${it.targetSampleRate ?: "original"}
816
+ - targetChannels: ${it.targetChannels ?: "original"}
817
+ - targetBitDepth: ${it.targetBitDepth}
818
+ - normalizeAudio: ${it.normalizeAudio}
819
+ """.trimIndent())
820
+ }
821
+ } else null
707
822
 
708
- // Load audio data based on range type (or full file if no range specified)
709
- val audioData = when {
710
- hasByteRange -> {
823
+ val audioData = if (hasByteRange) {
711
824
  val format = audioProcessor.getAudioFormat(fileUri)
712
825
  ?: throw IllegalArgumentException("Could not determine audio format")
713
-
826
+
714
827
  // Calculate time range from byte position
715
828
  val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
716
829
  val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
717
830
  val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
718
-
719
- LogUtils.d(CLASS_NAME, "Loading audio with byte range: position=$position, length=$length")
720
-
831
+
832
+ LogUtils.d(CLASS_NAME, """
833
+ Converting byte range to time range:
834
+ - position: $position bytes
835
+ - length: $length bytes
836
+ - bytesPerSecond: $bytesPerSecond
837
+ - effectiveStartTimeMs: $effectiveStartTimeMs
838
+ - effectiveEndTimeMs: $effectiveEndTimeMs
839
+ """.trimIndent())
840
+
721
841
  audioProcessor.loadAudioRange(
722
842
  fileUri = fileUri,
723
843
  startTimeMs = effectiveStartTimeMs,
724
844
  endTimeMs = effectiveEndTimeMs,
725
- config = config
845
+ config = decodingConfig
726
846
  )
727
- }
728
- hasTimeRange -> {
729
- LogUtils.d(CLASS_NAME, "Loading audio with time range: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs")
730
-
847
+ } else {
848
+ // Must be time range due to earlier validation
849
+ LogUtils.d(CLASS_NAME, """
850
+ Using time range:
851
+ - startTimeMs: $startTimeMs
852
+ - endTimeMs: $endTimeMs
853
+ """.trimIndent())
854
+
731
855
  audioProcessor.loadAudioRange(
732
856
  fileUri = fileUri,
733
857
  startTimeMs = startTimeMs!!.toLong(),
734
858
  endTimeMs = endTimeMs!!.toLong(),
735
- config = config
859
+ config = decodingConfig
736
860
  )
737
- }
738
- else -> {
739
- LogUtils.d(CLASS_NAME, "Loading entire audio file")
740
- audioProcessor.loadAudioFromAnyFormat(fileUri, config)
741
- }
742
- } ?: throw IllegalStateException("Failed to load audio data")
743
-
744
- val featuresMap = options["features"] as? Map<*, *>
745
- val features = Features.parseFeatureOptions(featuresMap)
746
-
747
- val recordingConfig = RecordingConfig(
748
- sampleRate = audioData.sampleRate,
749
- channels = audioData.channels,
750
- encoding = when (audioData.bitDepth) {
751
- 8 -> "pcm_8bit"
752
- 16 -> "pcm_16bit"
753
- 32 -> "pcm_32bit"
754
- else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
755
- },
756
- segmentDurationMs = segmentDurationMs,
757
- features = features
758
- )
759
-
760
- LogUtils.d(CLASS_NAME, "extractAudioAnalysis: $recordingConfig")
761
- audioProcessor.resetCumulativeAmplitudeRange()
861
+ } ?: throw IllegalStateException("Failed to load audio data")
762
862
 
763
- val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
764
- promise.resolve(analysisData.toDictionary())
765
- } catch (e: Exception) {
766
- LogUtils.e(CLASS_NAME, "Failed to extract audio analysis: ${e.message}", e)
767
- promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
768
- }
769
- }
770
-
771
- AsyncFunction("extractAudioData") { options: Map<String, Any>, promise: Promise ->
772
- try {
773
- val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
774
- val startTimeMs = options["startTimeMs"] as? Number
775
- val endTimeMs = options["endTimeMs"] as? Number
776
- val position = options["position"] as? Number
777
- val length = options["length"] as? Number
778
-
779
- // Validate that we have either time range or byte range, but not both and not neither
780
- val hasTimeRange = startTimeMs != null && endTimeMs != null
781
- val hasByteRange = position != null && length != null
782
-
783
- if (!hasTimeRange && !hasByteRange) {
784
- throw IllegalArgumentException("Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
785
- }
786
- if (hasTimeRange && hasByteRange) {
787
- throw IllegalArgumentException("Cannot specify both time range and byte range")
788
- }
789
-
790
- // Get decoding options
791
- val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
792
- val decodingConfig = if (decodingOptionsMap != null) {
793
- DecodingConfig(
794
- targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
795
- targetChannels = decodingOptionsMap["targetChannels"] as? Int,
796
- targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
797
- normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
798
- ).also {
799
- LogUtils.d(CLASS_NAME, """
800
- Using decoding config:
801
- - targetSampleRate: ${it.targetSampleRate ?: "original"}
802
- - targetChannels: ${it.targetChannels ?: "original"}
803
- - targetBitDepth: ${it.targetBitDepth}
804
- - normalizeAudio: ${it.normalizeAudio}
805
- """.trimIndent())
806
- }
807
- } else null
808
-
809
- val audioData = if (hasByteRange) {
810
- val format = audioProcessor.getAudioFormat(fileUri)
811
- ?: throw IllegalArgumentException("Could not determine audio format")
812
-
813
- // Calculate time range from byte position
814
- val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
815
- val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
816
- val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
817
-
818
863
  LogUtils.d(CLASS_NAME, """
819
- Converting byte range to time range:
820
- - position: $position bytes
821
- - length: $length bytes
822
- - bytesPerSecond: $bytesPerSecond
823
- - effectiveStartTimeMs: $effectiveStartTimeMs
824
- - effectiveEndTimeMs: $effectiveEndTimeMs
864
+ Audio data loaded successfully:
865
+ - data size: ${audioData.data.size} bytes
866
+ - sampleRate: ${audioData.sampleRate}
867
+ - channels: ${audioData.channels}
868
+ - bitDepth: ${audioData.bitDepth}
869
+ - durationMs: ${audioData.durationMs}
825
870
  """.trimIndent())
826
-
827
- audioProcessor.loadAudioRange(
828
- fileUri = fileUri,
829
- startTimeMs = effectiveStartTimeMs,
830
- endTimeMs = effectiveEndTimeMs,
831
- config = decodingConfig
832
- )
833
- } else {
834
- // Must be time range due to earlier validation
835
- LogUtils.d(CLASS_NAME, """
836
- Using time range:
837
- - startTimeMs: $startTimeMs
838
- - endTimeMs: $endTimeMs
839
- """.trimIndent())
840
-
841
- audioProcessor.loadAudioRange(
842
- fileUri = fileUri,
843
- startTimeMs = startTimeMs!!.toLong(),
844
- endTimeMs = endTimeMs!!.toLong(),
845
- config = decodingConfig
846
- )
847
- } ?: throw IllegalStateException("Failed to load audio data")
848
-
849
- LogUtils.d(CLASS_NAME, """
850
- Audio data loaded successfully:
851
- - data size: ${audioData.data.size} bytes
852
- - sampleRate: ${audioData.sampleRate}
853
- - channels: ${audioData.channels}
854
- - bitDepth: ${audioData.bitDepth}
855
- - durationMs: ${audioData.durationMs}
856
- """.trimIndent())
857
-
858
- val includeNormalizedData = options["includeNormalizedData"] as? Boolean ?: false
859
- val includeBase64Data = options["includeBase64Data"] as? Boolean ?: false
860
- val includeWavHeader = options["includeWavHeader"] as? Boolean ?: false
861
- val bytesPerSample = audioData.bitDepth / 8
862
- val samples = audioData.data.size / (bytesPerSample * audioData.channels)
871
+
872
+ val includeNormalizedData = options["includeNormalizedData"] as? Boolean ?: false
873
+ val includeBase64Data = options["includeBase64Data"] as? Boolean ?: false
874
+ val includeWavHeader = options["includeWavHeader"] as? Boolean ?: false
875
+ val bytesPerSample = audioData.bitDepth / 8
876
+ val samples = audioData.data.size / (bytesPerSample * audioData.channels)
863
877
 
864
- // Create the result map
865
- val resultMap = mutableMapOf<String, Any>()
878
+ // Create the result map
879
+ val resultMap = mutableMapOf<String, Any>()
866
880
 
867
- // Add WAV header if requested
868
- if (includeWavHeader) {
869
- // Use ByteArrayOutputStream to write the WAV header and data
870
- val outputStream = java.io.ByteArrayOutputStream()
871
- val audioFileHandler = AudioFileHandler(appContext.reactContext!!.filesDir)
881
+ // Add WAV header if requested
882
+ if (includeWavHeader) {
883
+ // Use ByteArrayOutputStream to write the WAV header and data
884
+ val outputStream = java.io.ByteArrayOutputStream()
885
+ val audioFileHandler = AudioFileHandler(appContext.reactContext!!.filesDir)
872
886
 
873
- // Write the WAV header
874
- audioFileHandler.writeWavHeader(
875
- outputStream,
876
- audioData.sampleRate,
877
- audioData.channels,
878
- audioData.bitDepth
879
- )
887
+ // Write the WAV header
888
+ audioFileHandler.writeWavHeader(
889
+ outputStream,
890
+ audioData.sampleRate,
891
+ audioData.channels,
892
+ audioData.bitDepth
893
+ )
880
894
 
881
- // Write the PCM data
882
- outputStream.write(audioData.data)
895
+ // Write the PCM data
896
+ outputStream.write(audioData.data)
883
897
 
884
- // Get the complete WAV data
885
- val wavData = outputStream.toByteArray()
898
+ // Get the complete WAV data
899
+ val wavData = outputStream.toByteArray()
886
900
 
887
- resultMap["pcmData"] = wavData
888
- resultMap["hasWavHeader"] = true
901
+ resultMap["pcmData"] = wavData
902
+ resultMap["hasWavHeader"] = true
889
903
 
890
- LogUtils.d(CLASS_NAME, "Added WAV header to PCM data, total size: ${wavData.size} bytes")
891
- } else {
892
- resultMap["pcmData"] = audioData.data
893
- resultMap["hasWavHeader"] = false
894
- }
904
+ LogUtils.d(CLASS_NAME, "Added WAV header to PCM data, total size: ${wavData.size} bytes")
905
+ } else {
906
+ resultMap["pcmData"] = audioData.data
907
+ resultMap["hasWavHeader"] = false
908
+ }
895
909
 
896
- // Add the rest of the data
897
- resultMap.putAll(mapOf(
898
- "sampleRate" to audioData.sampleRate,
899
- "channels" to audioData.channels,
900
- "bitDepth" to audioData.bitDepth,
901
- "durationMs" to audioData.durationMs,
902
- "format" to "pcm_${audioData.bitDepth}bit",
903
- "samples" to samples
904
- ))
910
+ // Add the rest of the data
911
+ resultMap.putAll(mapOf(
912
+ "sampleRate" to audioData.sampleRate,
913
+ "channels" to audioData.channels,
914
+ "bitDepth" to audioData.bitDepth,
915
+ "durationMs" to audioData.durationMs,
916
+ "format" to "pcm_${audioData.bitDepth}bit",
917
+ "samples" to samples
918
+ ))
905
919
 
906
- // Add checksum if requested
907
- if (options["computeChecksum"] == true) {
908
- val crc32 = CRC32()
909
- crc32.update(audioData.data)
910
- resultMap["checksum"] = crc32.value.toInt()
920
+ // Add checksum if requested
921
+ if (options["computeChecksum"] == true) {
922
+ val crc32 = CRC32()
923
+ crc32.update(audioData.data)
924
+ resultMap["checksum"] = crc32.value.toInt()
911
925
 
912
- LogUtils.d(CLASS_NAME, "Computed CRC32 checksum: ${crc32.value}")
913
- }
926
+ LogUtils.d(CLASS_NAME, "Computed CRC32 checksum: ${crc32.value}")
927
+ }
914
928
 
915
- if (includeNormalizedData) {
916
- val float32Data = AudioFormatUtils.convertByteArrayToFloatArray(
917
- audioData.data,
918
- "pcm_${audioData.bitDepth}bit"
919
- )
920
- resultMap["normalizedData"] = float32Data
921
- }
929
+ if (includeNormalizedData) {
930
+ val float32Data = AudioFormatUtils.convertByteArrayToFloatArray(
931
+ audioData.data,
932
+ "pcm_${audioData.bitDepth}bit"
933
+ )
934
+ resultMap["normalizedData"] = float32Data
935
+ }
922
936
 
923
- if (includeBase64Data) {
924
- // Convert the PCM data to a base64 string
925
- val base64Data = android.util.Base64.encodeToString(
926
- audioData.data,
927
- android.util.Base64.NO_WRAP
928
- )
929
- resultMap["base64Data"] = base64Data
930
- }
937
+ if (includeBase64Data) {
938
+ // Convert the PCM data to a base64 string
939
+ val base64Data = android.util.Base64.encodeToString(
940
+ audioData.data,
941
+ android.util.Base64.NO_WRAP
942
+ )
943
+ resultMap["base64Data"] = base64Data
944
+ }
931
945
 
932
- promise.resolve(resultMap)
933
- } catch (e: Exception) {
934
- LogUtils.e(CLASS_NAME, "Failed to extract audio data: ${e.message}")
935
- LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
936
- promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
946
+ promise.resolve(resultMap)
947
+ } catch (e: Exception) {
948
+ LogUtils.e(CLASS_NAME, "Failed to extract audio data: ${e.message}")
949
+ LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
950
+ promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
951
+ }
937
952
  }
938
953
  }
939
954
  }