@siteed/expo-audio-stream 2.1.0 → 2.2.1-beta.0

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 (189) hide show
  1. package/README.md +23 -260
  2. package/build/index.d.ts +11 -15
  3. package/build/index.js +54 -14
  4. package/build/src/index.d.ts +11 -0
  5. package/build/src/index.js +54 -0
  6. package/package.json +49 -110
  7. package/src/index.ts +18 -32
  8. package/CHANGELOG.md +0 -206
  9. package/android/build.gradle +0 -105
  10. package/android/src/main/AndroidManifest.xml +0 -27
  11. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
  12. package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
  13. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
  14. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
  15. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
  16. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -2235
  17. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
  18. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -152
  19. package/android/src/main/java/net/siteed/audiostream/AudioTrimmer.kt +0 -1099
  20. package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -21
  21. package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
  22. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -739
  23. package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
  24. package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
  25. package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
  26. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
  27. package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
  28. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
  29. package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
  30. package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
  31. package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
  32. package/android/src/main/res/drawable/ic_microphone.xml +0 -13
  33. package/android/src/main/res/drawable/ic_pause.xml +0 -10
  34. package/android/src/main/res/drawable/ic_play.xml +0 -10
  35. package/android/src/main/res/drawable/ic_stop.xml +0 -10
  36. package/android/src/main/res/layout/notification_recording.xml +0 -37
  37. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
  38. package/app.plugin.js +0 -1
  39. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -179
  40. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
  41. package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
  42. package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  43. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -68
  44. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
  45. package/build/AudioAnalysis/extractAudioAnalysis.js +0 -203
  46. package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  47. package/build/AudioAnalysis/extractAudioData.d.ts +0 -3
  48. package/build/AudioAnalysis/extractAudioData.d.ts.map +0 -1
  49. package/build/AudioAnalysis/extractAudioData.js +0 -5
  50. package/build/AudioAnalysis/extractAudioData.js.map +0 -1
  51. package/build/AudioAnalysis/extractMelSpectrogram.d.ts +0 -14
  52. package/build/AudioAnalysis/extractMelSpectrogram.d.ts.map +0 -1
  53. package/build/AudioAnalysis/extractMelSpectrogram.js +0 -85
  54. package/build/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
  55. package/build/AudioAnalysis/extractPreview.d.ts +0 -11
  56. package/build/AudioAnalysis/extractPreview.d.ts.map +0 -1
  57. package/build/AudioAnalysis/extractPreview.js +0 -25
  58. package/build/AudioAnalysis/extractPreview.js.map +0 -1
  59. package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
  60. package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
  61. package/build/AudioAnalysis/extractWaveform.js +0 -11
  62. package/build/AudioAnalysis/extractWaveform.js.map +0 -1
  63. package/build/AudioRecorder.provider.d.ts +0 -11
  64. package/build/AudioRecorder.provider.d.ts.map +0 -1
  65. package/build/AudioRecorder.provider.js +0 -37
  66. package/build/AudioRecorder.provider.js.map +0 -1
  67. package/build/ExpoAudioStream.native.d.ts +0 -3
  68. package/build/ExpoAudioStream.native.d.ts.map +0 -1
  69. package/build/ExpoAudioStream.native.js +0 -6
  70. package/build/ExpoAudioStream.native.js.map +0 -1
  71. package/build/ExpoAudioStream.types.d.ts +0 -532
  72. package/build/ExpoAudioStream.types.d.ts.map +0 -1
  73. package/build/ExpoAudioStream.types.js +0 -2
  74. package/build/ExpoAudioStream.types.js.map +0 -1
  75. package/build/ExpoAudioStream.web.d.ts +0 -59
  76. package/build/ExpoAudioStream.web.d.ts.map +0 -1
  77. package/build/ExpoAudioStream.web.js +0 -285
  78. package/build/ExpoAudioStream.web.js.map +0 -1
  79. package/build/ExpoAudioStreamModule.d.ts +0 -3
  80. package/build/ExpoAudioStreamModule.d.ts.map +0 -1
  81. package/build/ExpoAudioStreamModule.js +0 -693
  82. package/build/ExpoAudioStreamModule.js.map +0 -1
  83. package/build/WebRecorder.web.d.ts +0 -119
  84. package/build/WebRecorder.web.d.ts.map +0 -1
  85. package/build/WebRecorder.web.js +0 -436
  86. package/build/WebRecorder.web.js.map +0 -1
  87. package/build/constants.d.ts +0 -11
  88. package/build/constants.d.ts.map +0 -1
  89. package/build/constants.js +0 -14
  90. package/build/constants.js.map +0 -1
  91. package/build/events.d.ts +0 -26
  92. package/build/events.d.ts.map +0 -1
  93. package/build/events.js +0 -21
  94. package/build/events.js.map +0 -1
  95. package/build/index.d.ts.map +0 -1
  96. package/build/index.js.map +0 -1
  97. package/build/trimAudio.d.ts +0 -25
  98. package/build/trimAudio.d.ts.map +0 -1
  99. package/build/trimAudio.js +0 -67
  100. package/build/trimAudio.js.map +0 -1
  101. package/build/useAudioRecorder.d.ts +0 -21
  102. package/build/useAudioRecorder.d.ts.map +0 -1
  103. package/build/useAudioRecorder.js +0 -427
  104. package/build/useAudioRecorder.js.map +0 -1
  105. package/build/utils/BlobFix.d.ts +0 -9
  106. package/build/utils/BlobFix.d.ts.map +0 -1
  107. package/build/utils/BlobFix.js +0 -498
  108. package/build/utils/BlobFix.js.map +0 -1
  109. package/build/utils/audioProcessing.d.ts +0 -24
  110. package/build/utils/audioProcessing.d.ts.map +0 -1
  111. package/build/utils/audioProcessing.js +0 -133
  112. package/build/utils/audioProcessing.js.map +0 -1
  113. package/build/utils/concatenateBuffers.d.ts +0 -8
  114. package/build/utils/concatenateBuffers.d.ts.map +0 -1
  115. package/build/utils/concatenateBuffers.js +0 -21
  116. package/build/utils/concatenateBuffers.js.map +0 -1
  117. package/build/utils/convertPCMToFloat32.d.ts +0 -13
  118. package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
  119. package/build/utils/convertPCMToFloat32.js +0 -120
  120. package/build/utils/convertPCMToFloat32.js.map +0 -1
  121. package/build/utils/encodingToBitDepth.d.ts +0 -5
  122. package/build/utils/encodingToBitDepth.d.ts.map +0 -1
  123. package/build/utils/encodingToBitDepth.js +0 -13
  124. package/build/utils/encodingToBitDepth.js.map +0 -1
  125. package/build/utils/getWavFileInfo.d.ts +0 -26
  126. package/build/utils/getWavFileInfo.d.ts.map +0 -1
  127. package/build/utils/getWavFileInfo.js +0 -92
  128. package/build/utils/getWavFileInfo.js.map +0 -1
  129. package/build/utils/writeWavHeader.d.ts +0 -49
  130. package/build/utils/writeWavHeader.d.ts.map +0 -1
  131. package/build/utils/writeWavHeader.js +0 -91
  132. package/build/utils/writeWavHeader.js.map +0 -1
  133. package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
  134. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
  135. package/build/workers/InlineFeaturesExtractor.web.js +0 -828
  136. package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
  137. package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
  138. package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
  139. package/build/workers/inlineAudioWebWorker.web.js +0 -157
  140. package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
  141. package/expo-module.config.json +0 -9
  142. package/ios/AudioAnalysisData.swift +0 -74
  143. package/ios/AudioNotificationManager.swift +0 -135
  144. package/ios/AudioProcessingHelpers.swift +0 -743
  145. package/ios/AudioProcessor.swift +0 -1313
  146. package/ios/AudioStreamError.swift +0 -7
  147. package/ios/AudioStreamManager.swift +0 -1708
  148. package/ios/AudioStreamManagerDelegate.swift +0 -16
  149. package/ios/DataPoint.swift +0 -54
  150. package/ios/DecodingConfig.swift +0 -47
  151. package/ios/ExpoAudioStream.podspec +0 -27
  152. package/ios/ExpoAudioStreamModule.swift +0 -805
  153. package/ios/FFT.swift +0 -62
  154. package/ios/Features.swift +0 -95
  155. package/ios/Logger.swift +0 -7
  156. package/ios/NotificationExtension.swift +0 -15
  157. package/ios/RecordingResult.swift +0 -22
  158. package/ios/RecordingSettings.swift +0 -265
  159. package/ios/WaveformExtractor.swift +0 -105
  160. package/plugin/build/index.d.ts +0 -21
  161. package/plugin/build/index.js +0 -191
  162. package/plugin/src/index.ts +0 -278
  163. package/plugin/tsconfig.json +0 -10
  164. package/plugin/tsconfig.tsbuildinfo +0 -1
  165. package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -202
  166. package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -333
  167. package/src/AudioAnalysis/extractAudioData.ts +0 -6
  168. package/src/AudioAnalysis/extractMelSpectrogram.ts +0 -144
  169. package/src/AudioAnalysis/extractPreview.ts +0 -34
  170. package/src/AudioAnalysis/extractWaveform.ts +0 -22
  171. package/src/AudioRecorder.provider.tsx +0 -54
  172. package/src/ExpoAudioStream.native.ts +0 -6
  173. package/src/ExpoAudioStream.types.ts +0 -641
  174. package/src/ExpoAudioStream.web.ts +0 -359
  175. package/src/ExpoAudioStreamModule.ts +0 -967
  176. package/src/WebRecorder.web.ts +0 -580
  177. package/src/constants.ts +0 -18
  178. package/src/events.ts +0 -60
  179. package/src/trimAudio.ts +0 -90
  180. package/src/useAudioRecorder.tsx +0 -620
  181. package/src/utils/BlobFix.ts +0 -559
  182. package/src/utils/audioProcessing.ts +0 -205
  183. package/src/utils/concatenateBuffers.ts +0 -24
  184. package/src/utils/convertPCMToFloat32.ts +0 -170
  185. package/src/utils/encodingToBitDepth.ts +0 -18
  186. package/src/utils/getWavFileInfo.ts +0 -132
  187. package/src/utils/writeWavHeader.ts +0 -114
  188. package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
  189. package/src/workers/inlineAudioWebWorker.web.tsx +0 -156
@@ -1,739 +0,0 @@
1
- // packages/expo-audio-stream/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt
2
- package net.siteed.audiostream
3
-
4
- import android.Manifest
5
- import android.os.Build
6
- import android.os.Bundle
7
- import android.util.Log
8
- import androidx.annotation.RequiresApi
9
- import androidx.core.os.bundleOf
10
- import expo.modules.kotlin.Promise
11
- import expo.modules.kotlin.modules.Module
12
- import expo.modules.kotlin.modules.ModuleDefinition
13
- import expo.modules.interfaces.permissions.Permissions
14
- import java.util.zip.CRC32
15
-
16
- class ExpoAudioStreamModule : Module(), EventSender {
17
- private lateinit var audioRecorderManager: AudioRecorderManager
18
- private lateinit var audioProcessor: AudioProcessor
19
-
20
- private val audioFileHandler by lazy {
21
- AudioFileHandler(appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available"))
22
- }
23
-
24
- private val audioTrimmer by lazy {
25
- AudioTrimmer(
26
- appContext.reactContext ?: throw IllegalStateException("React context not available"),
27
- audioFileHandler
28
- )
29
- }
30
-
31
- @RequiresApi(Build.VERSION_CODES.R)
32
- override fun definition() = ModuleDefinition {
33
- // The module will be accessible from `requireNativeModule('ExpoAudioStream')` in JavaScript.
34
- Name("ExpoAudioStream")
35
-
36
- Events(
37
- Constants.AUDIO_EVENT_NAME,
38
- Constants.AUDIO_ANALYSIS_EVENT_NAME,
39
- Constants.RECORDING_INTERRUPTED_EVENT_NAME,
40
- Constants.TRIM_PROGRESS_EVENT
41
- )
42
-
43
- // Initialize AudioRecorderManager
44
- initializeManager()
45
-
46
- AsyncFunction("startRecording") { options: Map<String, Any?>, promise: Promise ->
47
- audioRecorderManager.startRecording(options, promise)
48
- }
49
-
50
- Function("clearAudioFiles") {
51
- audioRecorderManager.clearAudioStorage()
52
- }
53
-
54
- Function("status") {
55
- return@Function audioRecorderManager.getStatus()
56
- }
57
-
58
- AsyncFunction("listAudioFiles") { promise: Promise ->
59
- audioRecorderManager.listAudioFiles(promise)
60
- }
61
-
62
- AsyncFunction("pauseRecording") { promise: Promise ->
63
- audioRecorderManager.pauseRecording(promise)
64
- }
65
-
66
- AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
67
- try {
68
- val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
69
-
70
- // Get time or byte range options
71
- val startTimeMs = options["startTimeMs"] as? Number
72
- val endTimeMs = options["endTimeMs"] as? Number
73
- val position = options["position"] as? Number
74
- val length = options["length"] as? Number
75
- val segmentDurationMs = (options["segmentDurationMs"] as? Number)?.toInt() ?: 100
76
-
77
- // Validate ranges - can have time range OR byte range OR no range
78
- val hasTimeRange = startTimeMs != null && endTimeMs != null
79
- val hasByteRange = position != null && length != null
80
-
81
- // Only throw if both ranges are provided
82
- if (hasTimeRange && hasByteRange) {
83
- throw IllegalArgumentException("Cannot specify both time range and byte range")
84
- }
85
-
86
- // Get decoding options with default configuration
87
- val defaultConfig = DecodingConfig(
88
- targetSampleRate = null,
89
- targetChannels = 1, // Default to mono
90
- targetBitDepth = 16,
91
- normalizeAudio = false
92
- )
93
-
94
- val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
95
- DecodingConfig(
96
- targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
97
- targetChannels = decodingOptionsMap["targetChannels"] as? Int,
98
- targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
99
- normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
100
- )
101
- } ?: defaultConfig
102
-
103
- // Load audio data based on range type (or full file if no range specified)
104
- val audioData = when {
105
- hasByteRange -> {
106
- val format = audioProcessor.getAudioFormat(fileUri)
107
- ?: throw IllegalArgumentException("Could not determine audio format")
108
-
109
- // Calculate time range from byte position
110
- val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
111
- val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
112
- val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
113
-
114
- Log.d(Constants.TAG, "Loading audio with byte range: position=$position, length=$length")
115
-
116
- audioProcessor.loadAudioRange(
117
- fileUri = fileUri,
118
- startTimeMs = effectiveStartTimeMs,
119
- endTimeMs = effectiveEndTimeMs,
120
- config = config
121
- )
122
- }
123
- hasTimeRange -> {
124
- Log.d(Constants.TAG, "Loading audio with time range: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs")
125
-
126
- audioProcessor.loadAudioRange(
127
- fileUri = fileUri,
128
- startTimeMs = startTimeMs!!.toLong(),
129
- endTimeMs = endTimeMs!!.toLong(),
130
- config = config
131
- )
132
- }
133
- else -> {
134
- Log.d(Constants.TAG, "Loading entire audio file")
135
- audioProcessor.loadAudioFromAnyFormat(fileUri, config)
136
- }
137
- } ?: throw IllegalStateException("Failed to load audio data")
138
-
139
- val featuresMap = options["features"] as? Map<*, *>
140
- val features = Features.parseFeatureOptions(featuresMap)
141
-
142
- val recordingConfig = RecordingConfig(
143
- sampleRate = audioData.sampleRate,
144
- channels = audioData.channels,
145
- encoding = when (audioData.bitDepth) {
146
- 8 -> "pcm_8bit"
147
- 16 -> "pcm_16bit"
148
- 32 -> "pcm_32bit"
149
- else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
150
- },
151
- segmentDurationMs = segmentDurationMs,
152
- features = features
153
- )
154
-
155
- Log.d(Constants.TAG, "extractAudioAnalysis: $recordingConfig")
156
- audioProcessor.resetCumulativeAmplitudeRange()
157
-
158
- val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
159
- promise.resolve(analysisData.toDictionary())
160
- } catch (e: Exception) {
161
- Log.e(Constants.TAG, "Failed to extract audio analysis: ${e.message}", e)
162
- promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
163
- }
164
- }
165
-
166
- AsyncFunction("resumeRecording") { promise: Promise ->
167
- audioRecorderManager.resumeRecording(promise)
168
- }
169
-
170
- AsyncFunction("stopRecording") { promise: Promise ->
171
- audioRecorderManager.stopRecording(promise)
172
- }
173
-
174
- AsyncFunction("requestPermissionsAsync") { promise: Promise ->
175
- try {
176
- val permissions = mutableListOf(
177
- Manifest.permission.RECORD_AUDIO,
178
- Manifest.permission.READ_PHONE_STATE
179
- )
180
-
181
- // Add foreground service permission for Android 14+
182
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
183
- Log.d(Constants.TAG, "Adding FOREGROUND_SERVICE_MICROPHONE permission request")
184
- permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
185
- }
186
-
187
- Log.d(Constants.TAG, "Requesting permissions: $permissions")
188
- Permissions.askForPermissionsWithPermissionsManager(
189
- appContext.permissions,
190
- promise,
191
- *permissions.toTypedArray()
192
- )
193
- } catch (e: Exception) {
194
- Log.e(Constants.TAG, "Error requesting permissions", e)
195
- promise.reject("PERMISSION_ERROR", "Failed to request permissions: ${e.message}", e)
196
- }
197
- }
198
-
199
- AsyncFunction("getPermissionsAsync") { promise: Promise ->
200
- val permissions = mutableListOf(
201
- Manifest.permission.RECORD_AUDIO,
202
- Manifest.permission.READ_PHONE_STATE
203
- )
204
-
205
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
206
- permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
207
- }
208
-
209
- Permissions.getPermissionsWithPermissionsManager(
210
- appContext.permissions,
211
- promise,
212
- *permissions.toTypedArray()
213
- )
214
- }
215
-
216
- AsyncFunction("requestNotificationPermissionsAsync") { promise: Promise ->
217
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
218
- Permissions.askForPermissionsWithPermissionsManager(
219
- appContext.permissions,
220
- promise,
221
- Manifest.permission.POST_NOTIFICATIONS
222
- )
223
- } else {
224
- promise.resolve(
225
- bundleOf(
226
- "status" to "granted",
227
- "expires" to "never",
228
- "granted" to true
229
- )
230
- )
231
- }
232
- }
233
-
234
- AsyncFunction("getNotificationPermissionsAsync") { promise: Promise ->
235
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
236
- Permissions.getPermissionsWithPermissionsManager(
237
- appContext.permissions,
238
- promise,
239
- Manifest.permission.POST_NOTIFICATIONS
240
- )
241
- } else {
242
- promise.resolve(
243
- bundleOf(
244
- "status" to "granted",
245
- "expires" to "never",
246
- "granted" to true
247
- )
248
- )
249
- }
250
- }
251
-
252
- AsyncFunction("trimAudio") { options: Map<String, Any>, promise: Promise ->
253
- try {
254
- val fileUri = options["fileUri"] as? String ?: run {
255
- promise.reject("INVALID_URI", "fileUri is required", null)
256
- return@AsyncFunction
257
- }
258
-
259
- Log.d(Constants.TAG, "trimAudio called with fileUri: $fileUri")
260
- Log.d(Constants.TAG, "Full options: $options")
261
-
262
- val mode = options["mode"] as? String ?: "single"
263
- val startTimeMs = (options["startTimeMs"] as? Number)?.toLong()
264
- val endTimeMs = (options["endTimeMs"] as? Number)?.toLong()
265
-
266
- @Suppress("UNCHECKED_CAST")
267
- val ranges = options["ranges"] as? List<Map<String, Long>>
268
-
269
- val outputFileName = options["outputFileName"] as? String
270
-
271
- @Suppress("UNCHECKED_CAST")
272
- var outputFormatMap = options["outputFormat"] as? Map<String, Any>
273
-
274
- // Validate output format if provided
275
- if (outputFormatMap != null) {
276
- val format = outputFormatMap["format"] as? String
277
- if (format != null && format != "wav" && format != "aac" && format != "opus") {
278
- Log.w(Constants.TAG, "Requested format '$format' is not fully supported. Using 'aac' instead.")
279
- // Create a new map with the corrected format
280
- val newOutputFormat = HashMap<String, Any>(outputFormatMap)
281
- newOutputFormat["format"] = "aac"
282
- outputFormatMap = newOutputFormat
283
- }
284
- }
285
-
286
- Log.d(Constants.TAG, "Output format options: $outputFormatMap")
287
-
288
- // Create progress listener
289
- val progressListener = object : AudioTrimmer.ProgressListener {
290
- override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
291
- sendEvent(Constants.TRIM_PROGRESS_EVENT, mapOf(
292
- "progress" to progress,
293
- "bytesProcessed" to bytesProcessed,
294
- "totalBytes" to totalBytes
295
- ))
296
- }
297
- }
298
-
299
- // Record start time
300
- val startTime = System.currentTimeMillis()
301
-
302
- // Perform the trim operation
303
- val result = audioTrimmer.trimAudio(
304
- fileUri = fileUri,
305
- mode = mode,
306
- startTimeMs = startTimeMs,
307
- endTimeMs = endTimeMs,
308
- ranges = ranges,
309
- outputFileName = outputFileName,
310
- outputFormat = outputFormatMap,
311
- progressListener = progressListener
312
- )
313
-
314
- // Calculate processing time
315
- val processingTimeMs = System.currentTimeMillis() - startTime
316
-
317
- // Add processing time to result
318
- val resultWithProcessingTime = result.toMutableMap()
319
- resultWithProcessingTime["processingInfo"] = mapOf(
320
- "durationMs" to processingTimeMs
321
- )
322
-
323
- Log.d(Constants.TAG, "Trim operation completed successfully in ${processingTimeMs}ms: $result")
324
- promise.resolve(resultWithProcessingTime)
325
- } catch (e: Exception) {
326
- Log.e(Constants.TAG, "Error trimming audio: ${e.message}", e)
327
- promise.reject("TRIM_ERROR", "Error trimming audio: ${e.message}", e)
328
- }
329
- }
330
-
331
- AsyncFunction("extractMelSpectrogram") { options: Map<String, Any>, promise: Promise ->
332
- try {
333
- // Log all incoming options for debugging
334
- Log.d(Constants.TAG, "extractMelSpectrogram called with options: $options")
335
-
336
- // Extract required parameters with detailed logging
337
- val fileUri = options["fileUri"] as? String
338
- Log.d(Constants.TAG, "fileUri: $fileUri")
339
- if (fileUri == null) {
340
- Log.e(Constants.TAG, "Missing required parameter: fileUri")
341
- throw IllegalArgumentException("fileUri is required")
342
- }
343
-
344
- val windowSizeMs = options["windowSizeMs"] as? Double
345
- Log.d(Constants.TAG, "windowSizeMs: $windowSizeMs")
346
- if (windowSizeMs == null) {
347
- Log.e(Constants.TAG, "Missing required parameter: windowSizeMs")
348
- throw IllegalArgumentException("windowSizeMs is required")
349
- }
350
-
351
- val hopLengthMs = options["hopLengthMs"] as? Double
352
- Log.d(Constants.TAG, "hopLengthMs: $hopLengthMs")
353
- if (hopLengthMs == null) {
354
- Log.e(Constants.TAG, "Missing required parameter: hopLengthMs")
355
- throw IllegalArgumentException("hopLengthMs is required")
356
- }
357
-
358
- // Handle nMels which might come as Double from JavaScript
359
- val nMelsValue = options["nMels"]
360
- Log.d(Constants.TAG, "Raw nMels value: $nMelsValue (type: ${nMelsValue?.javaClass?.name})")
361
-
362
- val nMels = when (nMelsValue) {
363
- is Int -> nMelsValue
364
- is Double -> nMelsValue.toInt()
365
- is Number -> nMelsValue.toInt()
366
- else -> {
367
- Log.e(Constants.TAG, "Missing or invalid required parameter: nMels")
368
- throw IllegalArgumentException("nMels is required and must be a number")
369
- }
370
- }
371
-
372
- Log.d(Constants.TAG, "Converted nMels: $nMels (from ${nMelsValue?.javaClass?.name})")
373
-
374
- // Extract optional parameters with defaults
375
- val fMin = options["fMin"] as? Double ?: 0.0
376
- val fMax = options["fMax"] as? Double
377
- val windowType = options["windowType"] as? String ?: "hann"
378
- val normalize = options["normalize"] as? Boolean ?: false
379
- val logScale = options["logScale"] as? Boolean ?: true
380
-
381
- // Fix the conversion from Number to Long to preserve decimal values
382
- val startTimeMsNumber = options["startTimeMs"] as? Number
383
- val endTimeMsNumber = options["endTimeMs"] as? Number
384
- val startTimeMs = startTimeMsNumber?.toLong() ?: startTimeMsNumber?.toDouble()?.toLong()
385
- val endTimeMs = endTimeMsNumber?.toLong() ?: endTimeMsNumber?.toDouble()?.toLong()
386
-
387
- Log.d(Constants.TAG, """
388
- Optional parameters:
389
- - fMin: $fMin
390
- - fMax: $fMax
391
- - windowType: $windowType
392
- - normalize: $normalize
393
- - logScale: $logScale
394
- - startTimeMs: $startTimeMs (original: $startTimeMsNumber)
395
- - endTimeMs: $endTimeMs (original: $endTimeMsNumber)
396
- """.trimIndent())
397
-
398
- // Handle decoding options
399
- val decodingOptions = options["decodingOptions"] as? Map<String, Any>
400
- Log.d(Constants.TAG, "Decoding options: $decodingOptions")
401
-
402
- val config = decodingOptions?.let {
403
- val targetSampleRateValue = it["targetSampleRate"]
404
- val targetSampleRate = when (targetSampleRateValue) {
405
- is Int -> targetSampleRateValue
406
- is Double -> targetSampleRateValue.toInt()
407
- is Number -> targetSampleRateValue.toInt()
408
- else -> null
409
- }
410
-
411
- val targetChannelsValue = it["targetChannels"]
412
- val targetChannels = when (targetChannelsValue) {
413
- is Int -> targetChannelsValue
414
- is Double -> targetChannelsValue.toInt()
415
- is Number -> targetChannelsValue.toInt()
416
- else -> 1
417
- }
418
-
419
- val targetBitDepthValue = it["targetBitDepth"]
420
- val targetBitDepth = when (targetBitDepthValue) {
421
- is Int -> targetBitDepthValue
422
- is Double -> targetBitDepthValue.toInt()
423
- is Number -> targetBitDepthValue.toInt()
424
- else -> 16
425
- }
426
-
427
- val normalizeAudio = it["normalizeAudio"] as? Boolean ?: false
428
-
429
- DecodingConfig(
430
- targetSampleRate = targetSampleRate,
431
- targetChannels = targetChannels,
432
- targetBitDepth = targetBitDepth,
433
- normalizeAudio = normalizeAudio
434
- ).also { config ->
435
- Log.d(Constants.TAG, """
436
- Using decoding config:
437
- - targetSampleRate: ${config.targetSampleRate ?: "original"}
438
- - targetChannels: ${config.targetChannels ?: "original"}
439
- - targetBitDepth: ${config.targetBitDepth}
440
- - normalizeAudio: ${config.normalizeAudio}
441
- """.trimIndent())
442
- }
443
- } ?: DecodingConfig(targetSampleRate = null, targetChannels = 1, targetBitDepth = 16).also {
444
- Log.d(Constants.TAG, "Using default decoding config")
445
- }
446
-
447
- // Check if the audio data is too short
448
- if (startTimeMs != null && endTimeMs != null) {
449
- val durationMs = endTimeMs - startTimeMs
450
- Log.d(Constants.TAG, "Audio duration for spectrogram: $durationMs ms")
451
- if (durationMs < 25) { // 25ms is minimum for a single window
452
- Log.w(Constants.TAG, "Audio duration is too short for spectrogram analysis: $durationMs ms")
453
- throw IllegalArgumentException("Audio duration must be at least 25ms for spectrogram analysis")
454
- }
455
- }
456
-
457
- // Load audio data with optional time range
458
- Log.d(Constants.TAG, "Loading audio data...")
459
- val audioData = when {
460
- startTimeMs != null && endTimeMs != null -> {
461
- Log.d(Constants.TAG, "Loading audio range: $startTimeMs to $endTimeMs ms")
462
- audioProcessor.loadAudioRange(fileUri, startTimeMs, endTimeMs, config)
463
- }
464
- else -> {
465
- Log.d(Constants.TAG, "Loading entire audio file")
466
- audioProcessor.loadAudioFromAnyFormat(fileUri, config)
467
- }
468
- }
469
-
470
- if (audioData == null) {
471
- Log.e(Constants.TAG, "Failed to load audio data")
472
- throw IllegalStateException("Failed to load audio data")
473
- }
474
-
475
- Log.d(Constants.TAG, """
476
- Audio data loaded successfully:
477
- - data size: ${audioData.data.size} bytes
478
- - sampleRate: ${audioData.sampleRate}
479
- - channels: ${audioData.channels}
480
- - bitDepth: ${audioData.bitDepth}
481
- - durationMs: ${audioData.durationMs}
482
- """.trimIndent())
483
-
484
- // Validate that we have enough audio data for processing
485
- if (audioData.data.size == 0 || audioData.durationMs < windowSizeMs) {
486
- Log.e(Constants.TAG, "Audio data is too short for spectrogram analysis: ${audioData.durationMs}ms, data size: ${audioData.data.size} bytes")
487
- throw IllegalArgumentException(
488
- "Audio data is too short for spectrogram analysis. " +
489
- "Duration: ${audioData.durationMs}ms, minimum required: ${windowSizeMs}ms"
490
- )
491
- }
492
-
493
- // Compute mel-spectrogram
494
- Log.d(Constants.TAG, "Computing mel-spectrogram...")
495
- val spectrogramData = audioProcessor.extractMelSpectrogram(
496
- audioData = audioData,
497
- windowSizeMs = windowSizeMs.toFloat(),
498
- hopLengthMs = hopLengthMs.toFloat(),
499
- nMels = nMels,
500
- fMin = fMin.toFloat(),
501
- fMax = fMax?.toFloat() ?: (audioData.sampleRate.toFloat() / 2),
502
- normalize = normalize,
503
- logScaling = logScale,
504
- windowType = windowType
505
- )
506
-
507
- Log.d(Constants.TAG, "Mel-spectrogram computed successfully with ${spectrogramData.spectrogram.size} time steps")
508
-
509
- // Convert to map for React Native
510
- val result = mapOf(
511
- "spectrogram" to spectrogramData.spectrogram.map { it.toList() },
512
- "sampleRate" to audioData.sampleRate,
513
- "nMels" to nMels,
514
- "timeSteps" to spectrogramData.spectrogram.size,
515
- "durationMs" to audioData.durationMs
516
- )
517
-
518
- Log.d(Constants.TAG, "Returning result with ${result["timeSteps"]} time steps and $nMels mel bands")
519
- promise.resolve(result)
520
- } catch (e: Exception) {
521
- Log.e(Constants.TAG, "Failed to extract mel-spectrogram: ${e.message}")
522
- Log.e(Constants.TAG, "Stack trace: ${e.stackTraceToString()}")
523
- promise.reject("SPECTROGRAM_ERROR", e.message ?: "Unknown error", e)
524
- }
525
- }
526
-
527
- OnDestroy {
528
- AudioRecorderManager.destroy()
529
- }
530
-
531
- // Add a new function to check if recording is actually running
532
- AsyncFunction("checkRecordingStatus") { promise: Promise ->
533
- val isServiceRunning = AudioRecordingService.isServiceRunning()
534
-
535
- val status = audioRecorderManager.getStatus()
536
-
537
- // If service is running but isRecording is false, we need to cleanup
538
- if (isServiceRunning && !status.getBoolean("isRecording")) {
539
- audioRecorderManager.cleanup()
540
- AudioRecordingService.stopService(appContext.reactContext!!)
541
- }
542
-
543
- promise.resolve(status)
544
- }
545
-
546
- AsyncFunction("extractAudioData") { options: Map<String, Any>, promise: Promise ->
547
- try {
548
- val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
549
- val startTimeMs = options["startTimeMs"] as? Number
550
- val endTimeMs = options["endTimeMs"] as? Number
551
- val position = options["position"] as? Number
552
- val length = options["length"] as? Number
553
-
554
- // Validate that we have either time range or byte range, but not both and not neither
555
- val hasTimeRange = startTimeMs != null && endTimeMs != null
556
- val hasByteRange = position != null && length != null
557
-
558
- if (!hasTimeRange && !hasByteRange) {
559
- throw IllegalArgumentException("Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
560
- }
561
- if (hasTimeRange && hasByteRange) {
562
- throw IllegalArgumentException("Cannot specify both time range and byte range")
563
- }
564
-
565
- // Get decoding options
566
- val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
567
- val decodingConfig = if (decodingOptionsMap != null) {
568
- DecodingConfig(
569
- targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
570
- targetChannels = decodingOptionsMap["targetChannels"] as? Int,
571
- targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
572
- normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
573
- ).also {
574
- Log.d(Constants.TAG, """
575
- Using decoding config:
576
- - targetSampleRate: ${it.targetSampleRate ?: "original"}
577
- - targetChannels: ${it.targetChannels ?: "original"}
578
- - targetBitDepth: ${it.targetBitDepth}
579
- - normalizeAudio: ${it.normalizeAudio}
580
- """.trimIndent())
581
- }
582
- } else null
583
-
584
- val audioData = if (hasByteRange) {
585
- val format = audioProcessor.getAudioFormat(fileUri)
586
- ?: throw IllegalArgumentException("Could not determine audio format")
587
-
588
- // Calculate time range from byte position
589
- val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
590
- val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
591
- val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
592
-
593
- Log.d(Constants.TAG, """
594
- Converting byte range to time range:
595
- - position: $position bytes
596
- - length: $length bytes
597
- - bytesPerSecond: $bytesPerSecond
598
- - effectiveStartTimeMs: $effectiveStartTimeMs
599
- - effectiveEndTimeMs: $effectiveEndTimeMs
600
- """.trimIndent())
601
-
602
- audioProcessor.loadAudioRange(
603
- fileUri = fileUri,
604
- startTimeMs = effectiveStartTimeMs,
605
- endTimeMs = effectiveEndTimeMs,
606
- config = decodingConfig
607
- )
608
- } else {
609
- // Must be time range due to earlier validation
610
- Log.d(Constants.TAG, """
611
- Using time range:
612
- - startTimeMs: $startTimeMs
613
- - endTimeMs: $endTimeMs
614
- """.trimIndent())
615
-
616
- audioProcessor.loadAudioRange(
617
- fileUri = fileUri,
618
- startTimeMs = startTimeMs!!.toLong(),
619
- endTimeMs = endTimeMs!!.toLong(),
620
- config = decodingConfig
621
- )
622
- } ?: throw IllegalStateException("Failed to load audio data")
623
-
624
- Log.d(Constants.TAG, """
625
- Audio data loaded successfully:
626
- - data size: ${audioData.data.size} bytes
627
- - sampleRate: ${audioData.sampleRate}
628
- - channels: ${audioData.channels}
629
- - bitDepth: ${audioData.bitDepth}
630
- - durationMs: ${audioData.durationMs}
631
- """.trimIndent())
632
-
633
- val includeNormalizedData = options["includeNormalizedData"] as? Boolean ?: false
634
- val includeBase64Data = options["includeBase64Data"] as? Boolean ?: false
635
- val includeWavHeader = options["includeWavHeader"] as? Boolean ?: false
636
- val bytesPerSample = audioData.bitDepth / 8
637
- val samples = audioData.data.size / (bytesPerSample * audioData.channels)
638
-
639
- // Create the result map
640
- val resultMap = mutableMapOf<String, Any>()
641
-
642
- // Add WAV header if requested
643
- if (includeWavHeader) {
644
- // Use ByteArrayOutputStream to write the WAV header and data
645
- val outputStream = java.io.ByteArrayOutputStream()
646
- val audioFileHandler = AudioFileHandler(appContext.reactContext!!.filesDir)
647
-
648
- // Write the WAV header
649
- audioFileHandler.writeWavHeader(
650
- outputStream,
651
- audioData.sampleRate,
652
- audioData.channels,
653
- audioData.bitDepth
654
- )
655
-
656
- // Write the PCM data
657
- outputStream.write(audioData.data)
658
-
659
- // Get the complete WAV data
660
- val wavData = outputStream.toByteArray()
661
-
662
- resultMap["pcmData"] = wavData
663
- resultMap["hasWavHeader"] = true
664
-
665
- Log.d(Constants.TAG, "Added WAV header to PCM data, total size: ${wavData.size} bytes")
666
- } else {
667
- resultMap["pcmData"] = audioData.data
668
- resultMap["hasWavHeader"] = false
669
- }
670
-
671
- // Add the rest of the data
672
- resultMap.putAll(mapOf(
673
- "sampleRate" to audioData.sampleRate,
674
- "channels" to audioData.channels,
675
- "bitDepth" to audioData.bitDepth,
676
- "durationMs" to audioData.durationMs,
677
- "format" to "pcm_${audioData.bitDepth}bit",
678
- "samples" to samples
679
- ))
680
-
681
- // Add checksum if requested
682
- if (options["computeChecksum"] == true) {
683
- val crc32 = CRC32()
684
- crc32.update(audioData.data)
685
- resultMap["checksum"] = crc32.value.toInt()
686
-
687
- Log.d(Constants.TAG, "Computed CRC32 checksum: ${crc32.value}")
688
- }
689
-
690
- if (includeNormalizedData) {
691
- val float32Data = AudioFormatUtils.convertByteArrayToFloatArray(
692
- audioData.data,
693
- "pcm_${audioData.bitDepth}bit"
694
- )
695
- resultMap["normalizedData"] = float32Data
696
- }
697
-
698
- if (includeBase64Data) {
699
- // Convert the PCM data to a base64 string
700
- val base64Data = android.util.Base64.encodeToString(
701
- audioData.data,
702
- android.util.Base64.NO_WRAP
703
- )
704
- resultMap["base64Data"] = base64Data
705
- }
706
-
707
- promise.resolve(resultMap)
708
- } catch (e: Exception) {
709
- Log.e(Constants.TAG, "Failed to extract audio data: ${e.message}")
710
- Log.e(Constants.TAG, "Stack trace: ${e.stackTraceToString()}")
711
- promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
712
- }
713
- }
714
- }
715
-
716
- private fun initializeManager() {
717
- val androidContext =
718
- appContext.reactContext ?: throw IllegalStateException("Android context not available")
719
- val permissionUtils = PermissionUtils(androidContext)
720
- val audioEncoder = AudioDataEncoder()
721
- audioRecorderManager =
722
- AudioRecorderManager(androidContext, androidContext.filesDir, permissionUtils, audioEncoder, this)
723
- audioRecorderManager = AudioRecorderManager.initialize(
724
- androidContext,
725
- androidContext.filesDir,
726
- permissionUtils,
727
- audioEncoder,
728
- this
729
- )
730
- audioProcessor = AudioProcessor(androidContext.filesDir)
731
- }
732
-
733
-
734
- override fun sendExpoEvent(eventName: String, params: Bundle) {
735
- Log.d(Constants.TAG, "Sending event: $eventName")
736
- this@ExpoAudioStreamModule.sendEvent(eventName, params)
737
- }
738
-
739
- }