@siteed/audio-studio 3.1.0 → 3.2.0-beta.1

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +97 -50
  3. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +190 -0
  4. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +29 -83
  5. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +17 -1
  6. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +186 -0
  7. package/android/src/main/java/net/siteed/audiostudio/AudioProcessor.kt +473 -380
  8. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +640 -0
  9. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +187 -13
  10. package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +174 -212
  11. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +4 -0
  12. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  13. package/build/cjs/AudioAnalysis/extractPreview.js +92 -15
  14. package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -1
  15. package/build/cjs/AudioAnalysis/extractPreviewBars.js +134 -0
  16. package/build/cjs/AudioAnalysis/extractPreviewBars.js.map +1 -0
  17. package/build/cjs/errors/AudioExtractionError.js +127 -0
  18. package/build/cjs/errors/AudioExtractionError.js.map +1 -0
  19. package/build/cjs/errors/AudioStreamError.js +152 -0
  20. package/build/cjs/errors/AudioStreamError.js.map +1 -0
  21. package/build/cjs/errors/AudioStreamError.test.js +61 -0
  22. package/build/cjs/errors/AudioStreamError.test.js.map +1 -0
  23. package/build/cjs/index.js +12 -1
  24. package/build/cjs/index.js.map +1 -1
  25. package/build/cjs/streamAudioData.js +467 -0
  26. package/build/cjs/streamAudioData.js.map +1 -0
  27. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  28. package/build/esm/AudioAnalysis/extractPreview.js +92 -15
  29. package/build/esm/AudioAnalysis/extractPreview.js.map +1 -1
  30. package/build/esm/AudioAnalysis/extractPreviewBars.js +128 -0
  31. package/build/esm/AudioAnalysis/extractPreviewBars.js.map +1 -0
  32. package/build/esm/errors/AudioExtractionError.js +122 -0
  33. package/build/esm/errors/AudioExtractionError.js.map +1 -0
  34. package/build/esm/errors/AudioStreamError.js +147 -0
  35. package/build/esm/errors/AudioStreamError.js.map +1 -0
  36. package/build/esm/errors/AudioStreamError.test.js +59 -0
  37. package/build/esm/errors/AudioStreamError.test.js.map +1 -0
  38. package/build/esm/index.js +5 -1
  39. package/build/esm/index.js.map +1 -1
  40. package/build/esm/streamAudioData.js +460 -0
  41. package/build/esm/streamAudioData.js.map +1 -0
  42. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +79 -0
  43. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  44. package/build/types/AudioAnalysis/extractPreview.d.ts +2 -2
  45. package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -1
  46. package/build/types/AudioAnalysis/extractPreviewBars.d.ts +12 -0
  47. package/build/types/AudioAnalysis/extractPreviewBars.d.ts.map +1 -0
  48. package/build/types/errors/AudioExtractionError.d.ts +24 -0
  49. package/build/types/errors/AudioExtractionError.d.ts.map +1 -0
  50. package/build/types/errors/AudioStreamError.d.ts +25 -0
  51. package/build/types/errors/AudioStreamError.d.ts.map +1 -0
  52. package/build/types/errors/AudioStreamError.test.d.ts +2 -0
  53. package/build/types/errors/AudioStreamError.test.d.ts.map +1 -0
  54. package/build/types/index.d.ts +8 -1
  55. package/build/types/index.d.ts.map +1 -1
  56. package/build/types/streamAudioData.d.ts +114 -0
  57. package/build/types/streamAudioData.d.ts.map +1 -0
  58. package/ios/AudioProcessingHelpers.swift +10 -5
  59. package/ios/AudioProcessor.swift +99 -0
  60. package/ios/AudioStreamDecoder.swift +523 -0
  61. package/ios/AudioStudioModule.swift +210 -3
  62. package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +128 -0
  63. package/package.json +7 -7
  64. package/src/AudioAnalysis/AudioAnalysis.types.ts +82 -0
  65. package/src/AudioAnalysis/extractPreview.ts +118 -17
  66. package/src/AudioAnalysis/extractPreviewBars.ts +193 -0
  67. package/src/errors/AudioExtractionError.ts +167 -0
  68. package/src/errors/AudioStreamError.test.ts +65 -0
  69. package/src/errors/AudioStreamError.ts +185 -0
  70. package/src/index.ts +34 -0
  71. package/src/streamAudioData.ts +654 -0
@@ -21,14 +21,17 @@ import kotlinx.coroutines.cancelChildren
21
21
  import kotlinx.coroutines.launch
22
22
  import kotlinx.coroutines.withContext
23
23
 
24
- class AudioStudioModule : Module(), EventSender {
24
+ class AudioStudioModule : Module(), EventSender, AudioStreamDecoderDelegate {
25
25
  companion object {
26
26
  private const val CLASS_NAME = "AudioStudioModule"
27
27
  }
28
-
28
+
29
29
  private lateinit var audioRecorderManager: AudioRecorderManager
30
30
  private lateinit var audioProcessor: AudioProcessor
31
31
  private lateinit var audioDeviceManager: AudioDeviceManager
32
+
33
+ private val streamDecoders = mutableMapOf<String, AudioStreamDecoder>()
34
+ private val streamDecodersLock = Object()
32
35
  private var enablePhoneStateHandling: Boolean = false // Default to false until we check manifest
33
36
  private var enableNotificationHandling: Boolean = false // Default to false until we check manifest
34
37
  private var enableBackgroundAudio: Boolean = false // Default to false until we check manifest
@@ -88,7 +91,11 @@ class AudioStudioModule : Module(), EventSender {
88
91
  Constants.AUDIO_ANALYSIS_EVENT_NAME,
89
92
  Constants.RECORDING_INTERRUPTED_EVENT_NAME,
90
93
  Constants.TRIM_PROGRESS_EVENT,
91
- Constants.DEVICE_CHANGED_EVENT // Add device changed event name
94
+ Constants.DEVICE_CHANGED_EVENT, // Add device changed event name
95
+ Constants.AUDIO_STREAM_CHUNK_EVENT,
96
+ Constants.AUDIO_STREAM_PROGRESS_EVENT,
97
+ Constants.AUDIO_STREAM_COMPLETE_EVENT,
98
+ Constants.AUDIO_STREAM_ERROR_EVENT
92
99
  )
93
100
 
94
101
  // Initialize Managers
@@ -520,7 +527,7 @@ class AudioStudioModule : Module(), EventSender {
520
527
  """.trimIndent())
521
528
 
522
529
  // Handle decoding options
523
- val decodingOptions = options["decodingOptions"] as? Map<String, Any>
530
+ val decodingOptions = options["decodingOptions"] as? Map<*, *>
524
531
  LogUtils.d(CLASS_NAME, "Decoding options: $decodingOptions")
525
532
 
526
533
  val config = decodingOptions?.let {
@@ -676,6 +683,48 @@ class AudioStudioModule : Module(), EventSender {
676
683
  }
677
684
 
678
685
 
686
+ AsyncFunction("extractPreviewBars") { options: Map<String, Any>, promise: Promise ->
687
+ coroutineScope.launch(Dispatchers.IO) {
688
+ try {
689
+ val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
690
+ val numberOfBars = (options["numberOfBars"] as? Number)?.toInt() ?: 100
691
+ val startTimeMs = options["startTimeMs"] as? Number
692
+ val endTimeMs = options["endTimeMs"] as? Number
693
+
694
+ val defaultConfig = DecodingConfig(
695
+ targetSampleRate = null,
696
+ targetChannels = 1,
697
+ targetBitDepth = 16,
698
+ normalizeAudio = false
699
+ )
700
+ val decodingOptionsMap = options["decodingOptions"] as? Map<*, *>
701
+ val config = decodingOptionsMap?.let {
702
+ DecodingConfig(
703
+ targetSampleRate = (it["targetSampleRate"] as? Number)?.toInt(),
704
+ targetChannels = (it["targetChannels"] as? Number)?.toInt(),
705
+ targetBitDepth = (it["targetBitDepth"] as? Number)?.toInt() ?: 16,
706
+ normalizeAudio = (it["normalizeAudio"] as? Boolean) ?: false
707
+ )
708
+ } ?: defaultConfig
709
+ val silenceRmsThreshold = ((decodingOptionsMap?.get("silenceRmsThreshold") as? Number)?.toFloat()) ?: 0.01f
710
+
711
+ val audioData = audioProcessor.loadAudioFromAnyFormat(fileUri, config)
712
+ ?: throw IllegalStateException("Failed to load audio data")
713
+ val result = audioProcessor.generatePreviewBars(
714
+ audioData = audioData,
715
+ numberOfBars = numberOfBars,
716
+ startTimeMs = startTimeMs?.toLong(),
717
+ endTimeMs = endTimeMs?.toLong(),
718
+ silenceRmsThreshold = silenceRmsThreshold
719
+ )
720
+ promise.resolve(result)
721
+ } catch (e: Exception) {
722
+ LogUtils.e(CLASS_NAME, "Failed to extract preview bars: ${e.message}", e)
723
+ promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
724
+ }
725
+ }
726
+ }
727
+
679
728
  AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
680
729
  // Off the shared executor so other JS calls don't block during
681
730
  // multi-second analysis on large files.
@@ -707,12 +756,13 @@ class AudioStudioModule : Module(), EventSender {
707
756
  normalizeAudio = false
708
757
  )
709
758
 
710
- val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
759
+ val decodingOptionsMap = options["decodingOptions"] as? Map<*, *>
760
+ val config = decodingOptionsMap?.let {
711
761
  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
762
+ targetSampleRate = (it["targetSampleRate"] as? Number)?.toInt(),
763
+ targetChannels = (it["targetChannels"] as? Number)?.toInt(),
764
+ targetBitDepth = (it["targetBitDepth"] as? Number)?.toInt() ?: 16,
765
+ normalizeAudio = (it["normalizeAudio"] as? Boolean) ?: false
716
766
  )
717
767
  } ?: defaultConfig
718
768
 
@@ -802,12 +852,12 @@ class AudioStudioModule : Module(), EventSender {
802
852
  }
803
853
 
804
854
  // Get decoding options
805
- val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
855
+ val decodingOptionsMap = options["decodingOptions"] as? Map<*, *>
806
856
  val decodingConfig = if (decodingOptionsMap != null) {
807
857
  DecodingConfig(
808
- targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
809
- targetChannels = decodingOptionsMap["targetChannels"] as? Int,
810
- targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
858
+ targetSampleRate = (decodingOptionsMap["targetSampleRate"] as? Number)?.toInt(),
859
+ targetChannels = (decodingOptionsMap["targetChannels"] as? Number)?.toInt(),
860
+ targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Number)?.toInt() ?: 16,
811
861
  normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
812
862
  ).also {
813
863
  LogUtils.d(CLASS_NAME, """
@@ -951,6 +1001,130 @@ class AudioStudioModule : Module(), EventSender {
951
1001
  }
952
1002
  }
953
1003
  }
1004
+
1005
+ AsyncFunction("streamAudioData") { options: Map<String, Any?>, promise: Promise ->
1006
+ try {
1007
+ val requestId = options["requestId"] as? String
1008
+ ?: throw IllegalArgumentException("requestId is required")
1009
+ val fileUri = options["fileUri"] as? String
1010
+ ?: throw IllegalArgumentException("fileUri is required")
1011
+ val streamFormat = (options["streamFormat"] as? String) ?: "float32"
1012
+ if (streamFormat != "float32") {
1013
+ promise.reject(
1014
+ "ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT",
1015
+ "Only streamFormat='float32' is supported",
1016
+ null
1017
+ )
1018
+ return@AsyncFunction
1019
+ }
1020
+
1021
+ val context = appContext.reactContext
1022
+ ?: throw IllegalStateException("React context not available")
1023
+
1024
+ val decoderOptions = AudioStreamDecoder.Options(
1025
+ requestId = requestId,
1026
+ fileUri = fileUri,
1027
+ startTimeMs = (options["startTimeMs"] as? Number)?.toLong(),
1028
+ endTimeMs = (options["endTimeMs"] as? Number)?.toLong(),
1029
+ targetSampleRate = (options["targetSampleRate"] as? Number)?.toInt()
1030
+ ?: (options["sampleRate"] as? Number)?.toInt(),
1031
+ channels = (options["channels"] as? Number)?.toInt(),
1032
+ normalizeAudio = (options["normalizeAudio"] as? Boolean) ?: true,
1033
+ chunkDurationMs = ((options["chunkDurationMs"] as? Number)?.toInt() ?: 1000)
1034
+ .coerceIn(10, 60000),
1035
+ maxChunkBytes = (options["maxChunkBytes"] as? Number)?.toInt(),
1036
+ maxBufferedChunks = ((options["maxBufferedChunks"] as? Number)?.toInt() ?: 4)
1037
+ .coerceAtLeast(1),
1038
+ )
1039
+
1040
+ val decoder = AudioStreamDecoder(context, decoderOptions, this@AudioStudioModule)
1041
+ synchronized(streamDecodersLock) {
1042
+ if (streamDecoders.containsKey(requestId)) {
1043
+ promise.reject(
1044
+ "ERR_AUDIO_STREAM_BUSY",
1045
+ "requestId already in use",
1046
+ null
1047
+ )
1048
+ return@AsyncFunction
1049
+ }
1050
+ streamDecoders[requestId] = decoder
1051
+ }
1052
+ decoder.start()
1053
+ promise.resolve(bundleOf("requestId" to requestId))
1054
+ } catch (e: Exception) {
1055
+ LogUtils.e(CLASS_NAME, "streamAudioData failed: ${e.message}", e)
1056
+ promise.reject(
1057
+ "ERR_AUDIO_STREAM_DECODE_FAILED",
1058
+ e.message ?: "streamAudioData failed",
1059
+ e
1060
+ )
1061
+ }
1062
+ }
1063
+
1064
+ AsyncFunction("cancelStreamAudioData") { requestId: String, promise: Promise ->
1065
+ val decoder = synchronized(streamDecodersLock) {
1066
+ streamDecoders[requestId]
1067
+ }
1068
+ decoder?.cancel()
1069
+ promise.resolve(bundleOf(
1070
+ "requestId" to requestId,
1071
+ "cancelled" to (decoder != null)
1072
+ ))
1073
+ }
1074
+
1075
+ Function("acknowledgeStreamAudioChunk") { requestId: String, chunkIndex: Int ->
1076
+ val decoder = synchronized(streamDecodersLock) {
1077
+ streamDecoders[requestId]
1078
+ }
1079
+ decoder?.acknowledgeChunk(chunkIndex)
1080
+ }
1081
+
1082
+ AsyncFunction("getAudioDecodeCapabilities") { promise: Promise ->
1083
+ promise.resolve(bundleOf(
1084
+ "platform" to "android",
1085
+ "supportedInputFormats" to listOf(
1086
+ "audio/wav",
1087
+ "audio/mpeg",
1088
+ "audio/mp4",
1089
+ "audio/aac",
1090
+ "audio/ogg",
1091
+ "audio/opus",
1092
+ "audio/webm",
1093
+ "audio/flac",
1094
+ "audio/amr-wb",
1095
+ ),
1096
+ "supportedOutputFormats" to listOf("float32"),
1097
+ "supportsCancellation" to true,
1098
+ "supportsBackpressure" to true,
1099
+ "supportsTimeRange" to true,
1100
+ "supportsTargetSampleRate" to true,
1101
+ "supportsChannelMixing" to true,
1102
+ "knownLimitations" to listOf(
1103
+ "MediaCodec output rate may differ from extractor metadata for some encoders; output is resampled via linear interpolation."
1104
+ )
1105
+ ))
1106
+ }
1107
+ }
1108
+
1109
+ private fun releaseStreamDecoder(requestId: String) {
1110
+ synchronized(streamDecodersLock) {
1111
+ streamDecoders.remove(requestId)
1112
+ }
1113
+ }
1114
+
1115
+ override fun streamDecoderEmit(eventName: String, payload: Bundle) {
1116
+ when (eventName) {
1117
+ Constants.AUDIO_STREAM_COMPLETE_EVENT -> {
1118
+ (payload.getString("requestId"))?.let { releaseStreamDecoder(it) }
1119
+ }
1120
+ Constants.AUDIO_STREAM_ERROR_EVENT -> {
1121
+ val code = payload.getString("code") ?: ""
1122
+ if (code != "ERR_AUDIO_STREAM_CANCELLED") {
1123
+ payload.getString("requestId")?.let { releaseStreamDecoder(it) }
1124
+ }
1125
+ }
1126
+ }
1127
+ safeSendEvent(eventName, payload)
954
1128
  }
955
1129
 
956
1130
  private fun initializeManager() {