@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.
- package/CHANGELOG.md +30 -1
- package/README.md +97 -50
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +190 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +29 -83
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +17 -1
- package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +186 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioProcessor.kt +473 -380
- package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +640 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +187 -13
- package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +174 -212
- package/android/src/main/java/net/siteed/audiostudio/Constants.kt +4 -0
- package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractPreview.js +92 -15
- package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractPreviewBars.js +134 -0
- package/build/cjs/AudioAnalysis/extractPreviewBars.js.map +1 -0
- package/build/cjs/errors/AudioExtractionError.js +127 -0
- package/build/cjs/errors/AudioExtractionError.js.map +1 -0
- package/build/cjs/errors/AudioStreamError.js +152 -0
- package/build/cjs/errors/AudioStreamError.js.map +1 -0
- package/build/cjs/errors/AudioStreamError.test.js +61 -0
- package/build/cjs/errors/AudioStreamError.test.js.map +1 -0
- package/build/cjs/index.js +12 -1
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/streamAudioData.js +467 -0
- package/build/cjs/streamAudioData.js.map +1 -0
- package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/esm/AudioAnalysis/extractPreview.js +92 -15
- package/build/esm/AudioAnalysis/extractPreview.js.map +1 -1
- package/build/esm/AudioAnalysis/extractPreviewBars.js +128 -0
- package/build/esm/AudioAnalysis/extractPreviewBars.js.map +1 -0
- package/build/esm/errors/AudioExtractionError.js +122 -0
- package/build/esm/errors/AudioExtractionError.js.map +1 -0
- package/build/esm/errors/AudioStreamError.js +147 -0
- package/build/esm/errors/AudioStreamError.js.map +1 -0
- package/build/esm/errors/AudioStreamError.test.js +59 -0
- package/build/esm/errors/AudioStreamError.test.js.map +1 -0
- package/build/esm/index.js +5 -1
- package/build/esm/index.js.map +1 -1
- package/build/esm/streamAudioData.js +460 -0
- package/build/esm/streamAudioData.js.map +1 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +79 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractPreview.d.ts +2 -2
- package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractPreviewBars.d.ts +12 -0
- package/build/types/AudioAnalysis/extractPreviewBars.d.ts.map +1 -0
- package/build/types/errors/AudioExtractionError.d.ts +24 -0
- package/build/types/errors/AudioExtractionError.d.ts.map +1 -0
- package/build/types/errors/AudioStreamError.d.ts +25 -0
- package/build/types/errors/AudioStreamError.d.ts.map +1 -0
- package/build/types/errors/AudioStreamError.test.d.ts +2 -0
- package/build/types/errors/AudioStreamError.test.d.ts.map +1 -0
- package/build/types/index.d.ts +8 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/streamAudioData.d.ts +114 -0
- package/build/types/streamAudioData.d.ts.map +1 -0
- package/ios/AudioProcessingHelpers.swift +10 -5
- package/ios/AudioProcessor.swift +99 -0
- package/ios/AudioStreamDecoder.swift +523 -0
- package/ios/AudioStudioModule.swift +210 -3
- package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +128 -0
- package/package.json +7 -7
- package/src/AudioAnalysis/AudioAnalysis.types.ts +82 -0
- package/src/AudioAnalysis/extractPreview.ts +118 -17
- package/src/AudioAnalysis/extractPreviewBars.ts +193 -0
- package/src/errors/AudioExtractionError.ts +167 -0
- package/src/errors/AudioStreamError.test.ts +65 -0
- package/src/errors/AudioStreamError.ts +185 -0
- package/src/index.ts +34 -0
- 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
|
|
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
|
|
759
|
+
val decodingOptionsMap = options["decodingOptions"] as? Map<*, *>
|
|
760
|
+
val config = decodingOptionsMap?.let {
|
|
711
761
|
DecodingConfig(
|
|
712
|
-
targetSampleRate =
|
|
713
|
-
targetChannels =
|
|
714
|
-
targetBitDepth = (
|
|
715
|
-
normalizeAudio = (
|
|
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
|
|
855
|
+
val decodingOptionsMap = options["decodingOptions"] as? Map<*, *>
|
|
806
856
|
val decodingConfig = if (decodingOptionsMap != null) {
|
|
807
857
|
DecodingConfig(
|
|
808
|
-
targetSampleRate = decodingOptionsMap["targetSampleRate"] as?
|
|
809
|
-
targetChannels = decodingOptionsMap["targetChannels"] as?
|
|
810
|
-
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as?
|
|
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() {
|