@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.
- package/CHANGELOG.md +11 -1
- package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +41 -35
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +500 -485
- package/build/cjs/AudioAnalysis/wasmConfig.js.map +1 -1
- package/build/cjs/AudioAnalysis/wasmLoader.web.js +2 -1
- package/build/cjs/AudioAnalysis/wasmLoader.web.js.map +1 -1
- package/build/cjs/trimAudio.js.map +1 -1
- package/build/esm/AudioAnalysis/wasmConfig.js.map +1 -1
- package/build/esm/AudioAnalysis/wasmLoader.web.js +2 -1
- package/build/esm/AudioAnalysis/wasmLoader.web.js.map +1 -1
- package/build/esm/trimAudio.js.map +1 -1
- package/build/types/AudioAnalysis/wasmLoader.web.d.ts.map +1 -1
- package/build/types/trimAudio.d.ts.map +1 -1
- package/ios/AudioStreamManager.swift +135 -89
- package/ios/AudioStudioModule.swift +239 -216
- package/package.json +1 -1
- package/src/AudioAnalysis/wasmConfig.ts +1 -1
- package/src/AudioAnalysis/wasmLoader.web.ts +8 -3
- package/src/trimAudio.ts +19 -5
|
@@ -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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
+
options
|
|
197
198
|
}
|
|
198
|
-
|
|
199
|
-
if (audioRecorderManager.prepareRecording(
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
378
|
-
|
|
381
|
+
LogUtils.d(CLASS_NAME, "trimAudio called with fileUri: $fileUri")
|
|
382
|
+
LogUtils.d(CLASS_NAME, "Full options: $options")
|
|
379
383
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
"
|
|
418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
551
|
+
val normalizeAudio = it["normalizeAudio"] as? Boolean ?: false
|
|
552
552
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
673
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
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, "
|
|
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 =
|
|
845
|
+
config = decodingConfig
|
|
726
846
|
)
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
LogUtils.d(CLASS_NAME, "
|
|
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 =
|
|
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
|
-
|
|
820
|
-
-
|
|
821
|
-
-
|
|
822
|
-
-
|
|
823
|
-
-
|
|
824
|
-
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
865
|
-
|
|
878
|
+
// Create the result map
|
|
879
|
+
val resultMap = mutableMapOf<String, Any>()
|
|
866
880
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
887
|
+
// Write the WAV header
|
|
888
|
+
audioFileHandler.writeWavHeader(
|
|
889
|
+
outputStream,
|
|
890
|
+
audioData.sampleRate,
|
|
891
|
+
audioData.channels,
|
|
892
|
+
audioData.bitDepth
|
|
893
|
+
)
|
|
880
894
|
|
|
881
|
-
|
|
882
|
-
|
|
895
|
+
// Write the PCM data
|
|
896
|
+
outputStream.write(audioData.data)
|
|
883
897
|
|
|
884
|
-
|
|
885
|
-
|
|
898
|
+
// Get the complete WAV data
|
|
899
|
+
val wavData = outputStream.toByteArray()
|
|
886
900
|
|
|
887
|
-
|
|
888
|
-
|
|
901
|
+
resultMap["pcmData"] = wavData
|
|
902
|
+
resultMap["hasWavHeader"] = true
|
|
889
903
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
913
|
-
|
|
926
|
+
LogUtils.d(CLASS_NAME, "Computed CRC32 checksum: ${crc32.value}")
|
|
927
|
+
}
|
|
914
928
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
}
|