@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.
- package/README.md +23 -260
- package/build/index.d.ts +11 -15
- package/build/index.js +54 -14
- package/build/src/index.d.ts +11 -0
- package/build/src/index.js +54 -0
- package/package.json +49 -110
- package/src/index.ts +18 -32
- package/CHANGELOG.md +0 -206
- package/android/build.gradle +0 -105
- package/android/src/main/AndroidManifest.xml +0 -27
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
- package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
- package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
- package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -2235
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -152
- package/android/src/main/java/net/siteed/audiostream/AudioTrimmer.kt +0 -1099
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -21
- package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -739
- package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
- package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
- package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
- package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
- package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
- package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
- package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
- package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
- package/android/src/main/res/drawable/ic_microphone.xml +0 -13
- package/android/src/main/res/drawable/ic_pause.xml +0 -10
- package/android/src/main/res/drawable/ic_play.xml +0 -10
- package/android/src/main/res/drawable/ic_stop.xml +0 -10
- package/android/src/main/res/layout/notification_recording.xml +0 -37
- package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
- package/app.plugin.js +0 -1
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -179
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -68
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
- package/build/AudioAnalysis/extractAudioAnalysis.js +0 -203
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
- package/build/AudioAnalysis/extractAudioData.d.ts +0 -3
- package/build/AudioAnalysis/extractAudioData.d.ts.map +0 -1
- package/build/AudioAnalysis/extractAudioData.js +0 -5
- package/build/AudioAnalysis/extractAudioData.js.map +0 -1
- package/build/AudioAnalysis/extractMelSpectrogram.d.ts +0 -14
- package/build/AudioAnalysis/extractMelSpectrogram.d.ts.map +0 -1
- package/build/AudioAnalysis/extractMelSpectrogram.js +0 -85
- package/build/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
- package/build/AudioAnalysis/extractPreview.d.ts +0 -11
- package/build/AudioAnalysis/extractPreview.d.ts.map +0 -1
- package/build/AudioAnalysis/extractPreview.js +0 -25
- package/build/AudioAnalysis/extractPreview.js.map +0 -1
- package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
- package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
- package/build/AudioAnalysis/extractWaveform.js +0 -11
- package/build/AudioAnalysis/extractWaveform.js.map +0 -1
- package/build/AudioRecorder.provider.d.ts +0 -11
- package/build/AudioRecorder.provider.d.ts.map +0 -1
- package/build/AudioRecorder.provider.js +0 -37
- package/build/AudioRecorder.provider.js.map +0 -1
- package/build/ExpoAudioStream.native.d.ts +0 -3
- package/build/ExpoAudioStream.native.d.ts.map +0 -1
- package/build/ExpoAudioStream.native.js +0 -6
- package/build/ExpoAudioStream.native.js.map +0 -1
- package/build/ExpoAudioStream.types.d.ts +0 -532
- package/build/ExpoAudioStream.types.d.ts.map +0 -1
- package/build/ExpoAudioStream.types.js +0 -2
- package/build/ExpoAudioStream.types.js.map +0 -1
- package/build/ExpoAudioStream.web.d.ts +0 -59
- package/build/ExpoAudioStream.web.d.ts.map +0 -1
- package/build/ExpoAudioStream.web.js +0 -285
- package/build/ExpoAudioStream.web.js.map +0 -1
- package/build/ExpoAudioStreamModule.d.ts +0 -3
- package/build/ExpoAudioStreamModule.d.ts.map +0 -1
- package/build/ExpoAudioStreamModule.js +0 -693
- package/build/ExpoAudioStreamModule.js.map +0 -1
- package/build/WebRecorder.web.d.ts +0 -119
- package/build/WebRecorder.web.d.ts.map +0 -1
- package/build/WebRecorder.web.js +0 -436
- package/build/WebRecorder.web.js.map +0 -1
- package/build/constants.d.ts +0 -11
- package/build/constants.d.ts.map +0 -1
- package/build/constants.js +0 -14
- package/build/constants.js.map +0 -1
- package/build/events.d.ts +0 -26
- package/build/events.d.ts.map +0 -1
- package/build/events.js +0 -21
- package/build/events.js.map +0 -1
- package/build/index.d.ts.map +0 -1
- package/build/index.js.map +0 -1
- package/build/trimAudio.d.ts +0 -25
- package/build/trimAudio.d.ts.map +0 -1
- package/build/trimAudio.js +0 -67
- package/build/trimAudio.js.map +0 -1
- package/build/useAudioRecorder.d.ts +0 -21
- package/build/useAudioRecorder.d.ts.map +0 -1
- package/build/useAudioRecorder.js +0 -427
- package/build/useAudioRecorder.js.map +0 -1
- package/build/utils/BlobFix.d.ts +0 -9
- package/build/utils/BlobFix.d.ts.map +0 -1
- package/build/utils/BlobFix.js +0 -498
- package/build/utils/BlobFix.js.map +0 -1
- package/build/utils/audioProcessing.d.ts +0 -24
- package/build/utils/audioProcessing.d.ts.map +0 -1
- package/build/utils/audioProcessing.js +0 -133
- package/build/utils/audioProcessing.js.map +0 -1
- package/build/utils/concatenateBuffers.d.ts +0 -8
- package/build/utils/concatenateBuffers.d.ts.map +0 -1
- package/build/utils/concatenateBuffers.js +0 -21
- package/build/utils/concatenateBuffers.js.map +0 -1
- package/build/utils/convertPCMToFloat32.d.ts +0 -13
- package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
- package/build/utils/convertPCMToFloat32.js +0 -120
- package/build/utils/convertPCMToFloat32.js.map +0 -1
- package/build/utils/encodingToBitDepth.d.ts +0 -5
- package/build/utils/encodingToBitDepth.d.ts.map +0 -1
- package/build/utils/encodingToBitDepth.js +0 -13
- package/build/utils/encodingToBitDepth.js.map +0 -1
- package/build/utils/getWavFileInfo.d.ts +0 -26
- package/build/utils/getWavFileInfo.d.ts.map +0 -1
- package/build/utils/getWavFileInfo.js +0 -92
- package/build/utils/getWavFileInfo.js.map +0 -1
- package/build/utils/writeWavHeader.d.ts +0 -49
- package/build/utils/writeWavHeader.d.ts.map +0 -1
- package/build/utils/writeWavHeader.js +0 -91
- package/build/utils/writeWavHeader.js.map +0 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
- package/build/workers/InlineFeaturesExtractor.web.js +0 -828
- package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
- package/build/workers/inlineAudioWebWorker.web.js +0 -157
- package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
- package/expo-module.config.json +0 -9
- package/ios/AudioAnalysisData.swift +0 -74
- package/ios/AudioNotificationManager.swift +0 -135
- package/ios/AudioProcessingHelpers.swift +0 -743
- package/ios/AudioProcessor.swift +0 -1313
- package/ios/AudioStreamError.swift +0 -7
- package/ios/AudioStreamManager.swift +0 -1708
- package/ios/AudioStreamManagerDelegate.swift +0 -16
- package/ios/DataPoint.swift +0 -54
- package/ios/DecodingConfig.swift +0 -47
- package/ios/ExpoAudioStream.podspec +0 -27
- package/ios/ExpoAudioStreamModule.swift +0 -805
- package/ios/FFT.swift +0 -62
- package/ios/Features.swift +0 -95
- package/ios/Logger.swift +0 -7
- package/ios/NotificationExtension.swift +0 -15
- package/ios/RecordingResult.swift +0 -22
- package/ios/RecordingSettings.swift +0 -265
- package/ios/WaveformExtractor.swift +0 -105
- package/plugin/build/index.d.ts +0 -21
- package/plugin/build/index.js +0 -191
- package/plugin/src/index.ts +0 -278
- package/plugin/tsconfig.json +0 -10
- package/plugin/tsconfig.tsbuildinfo +0 -1
- package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -202
- package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -333
- package/src/AudioAnalysis/extractAudioData.ts +0 -6
- package/src/AudioAnalysis/extractMelSpectrogram.ts +0 -144
- package/src/AudioAnalysis/extractPreview.ts +0 -34
- package/src/AudioAnalysis/extractWaveform.ts +0 -22
- package/src/AudioRecorder.provider.tsx +0 -54
- package/src/ExpoAudioStream.native.ts +0 -6
- package/src/ExpoAudioStream.types.ts +0 -641
- package/src/ExpoAudioStream.web.ts +0 -359
- package/src/ExpoAudioStreamModule.ts +0 -967
- package/src/WebRecorder.web.ts +0 -580
- package/src/constants.ts +0 -18
- package/src/events.ts +0 -60
- package/src/trimAudio.ts +0 -90
- package/src/useAudioRecorder.tsx +0 -620
- package/src/utils/BlobFix.ts +0 -559
- package/src/utils/audioProcessing.ts +0 -205
- package/src/utils/concatenateBuffers.ts +0 -24
- package/src/utils/convertPCMToFloat32.ts +0 -170
- package/src/utils/encodingToBitDepth.ts +0 -18
- package/src/utils/getWavFileInfo.ts +0 -132
- package/src/utils/writeWavHeader.ts +0 -114
- package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
- 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
|
-
}
|