@siteed/expo-audio-studio 2.4.0 → 2.5.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/CHANGELOG.md +14 -1
- package/README.md +25 -0
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +22 -0
- package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +1501 -0
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +10 -5
- package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +27 -25
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +73 -71
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +581 -255
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +435 -158
- package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
- package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +14 -5
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +9 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/AudioDeviceManager.d.ts +107 -0
- package/build/AudioDeviceManager.d.ts.map +1 -0
- package/build/AudioDeviceManager.js +493 -0
- package/build/AudioDeviceManager.js.map +1 -0
- package/build/AudioRecorder.provider.d.ts.map +1 -1
- package/build/AudioRecorder.provider.js +3 -0
- package/build/AudioRecorder.provider.js.map +1 -1
- package/build/ExpoAudioStream.types.d.ts +90 -1
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js +7 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts +37 -0
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +399 -54
- package/build/ExpoAudioStream.web.js.map +1 -1
- package/build/ExpoAudioStreamModule.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.js +20 -0
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/WebRecorder.web.d.ts +63 -10
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +277 -68
- package/build/WebRecorder.web.js.map +1 -1
- package/build/hooks/useAudioDevices.d.ts +14 -0
- package/build/hooks/useAudioDevices.d.ts.map +1 -0
- package/build/hooks/useAudioDevices.js +151 -0
- package/build/hooks/useAudioDevices.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/build/useAudioRecorder.d.ts +1 -0
- package/build/useAudioRecorder.d.ts.map +1 -1
- package/build/useAudioRecorder.js +20 -1
- package/build/useAudioRecorder.js.map +1 -1
- package/build/utils/BlobFix.d.ts.map +1 -1
- package/build/utils/BlobFix.js +2 -2
- package/build/utils/BlobFix.js.map +1 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
- package/build/workers/InlineFeaturesExtractor.web.js +27 -26
- package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.js +25 -1
- package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
- package/ios/AudioDeviceManager.swift +654 -0
- package/ios/AudioStreamManager.swift +964 -760
- package/ios/ExpoAudioStreamModule.swift +174 -19
- package/ios/Features.swift +1 -1
- package/ios/ISSUE_IOS.md +45 -0
- package/ios/Logger.swift +13 -1
- package/ios/RecordingSettings.swift +12 -0
- package/package.json +2 -2
- package/src/AudioAnalysis/AudioAnalysis.types.ts +2 -2
- package/src/AudioDeviceManager.ts +571 -0
- package/src/AudioRecorder.provider.tsx +3 -0
- package/src/ExpoAudioStream.types.ts +97 -1
- package/src/ExpoAudioStream.web.ts +513 -63
- package/src/ExpoAudioStreamModule.ts +23 -0
- package/src/WebRecorder.web.ts +346 -81
- package/src/hooks/useAudioDevices.ts +180 -0
- package/src/index.ts +6 -0
- package/src/types/crc-32.d.ts +6 -6
- package/src/useAudioRecorder.tsx +27 -1
- package/src/utils/BlobFix.ts +6 -4
- package/src/workers/InlineFeaturesExtractor.web.tsx +27 -26
- package/src/workers/inlineAudioWebWorker.web.tsx +25 -1
|
@@ -13,12 +13,24 @@ import expo.modules.kotlin.modules.Module
|
|
|
13
13
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
14
14
|
import expo.modules.interfaces.permissions.Permissions
|
|
15
15
|
import java.util.zip.CRC32
|
|
16
|
+
import kotlinx.coroutines.CoroutineScope
|
|
17
|
+
import kotlinx.coroutines.Dispatchers
|
|
18
|
+
import kotlinx.coroutines.launch
|
|
19
|
+
import kotlinx.coroutines.withContext
|
|
20
|
+
import net.siteed.audiostream.LogUtils
|
|
16
21
|
|
|
17
22
|
class ExpoAudioStreamModule : Module(), EventSender {
|
|
23
|
+
companion object {
|
|
24
|
+
private const val CLASS_NAME = "ExpoAudioStreamModule"
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
private lateinit var audioRecorderManager: AudioRecorderManager
|
|
19
28
|
private lateinit var audioProcessor: AudioProcessor
|
|
29
|
+
private lateinit var audioDeviceManager: AudioDeviceManager
|
|
20
30
|
private var enablePhoneStateHandling: Boolean = false // Default to false until we check manifest
|
|
21
31
|
private var enableNotificationHandling: Boolean = false // Default to false until we check manifest
|
|
32
|
+
private var enableBackgroundAudio: Boolean = false // Default to false until we check manifest
|
|
33
|
+
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
22
34
|
|
|
23
35
|
private val audioFileHandler by lazy {
|
|
24
36
|
AudioFileHandler(appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available"))
|
|
@@ -50,30 +62,145 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
50
62
|
// Check if POST_NOTIFICATIONS is in the requested permissions
|
|
51
63
|
enableNotificationHandling = packageInfo.requestedPermissions?.contains(Manifest.permission.POST_NOTIFICATIONS) ?: false
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
// Check if background audio is enabled by looking for FOREGROUND_SERVICE permission
|
|
66
|
+
enableBackgroundAudio = packageInfo.requestedPermissions?.contains(Manifest.permission.FOREGROUND_SERVICE) ?: false
|
|
67
|
+
|
|
68
|
+
LogUtils.d(CLASS_NAME, "Phone state handling ${if (enablePhoneStateHandling) "enabled" else "disabled"} based on manifest permissions")
|
|
69
|
+
LogUtils.d(CLASS_NAME, "Notification handling ${if (enableNotificationHandling) "enabled" else "disabled"} based on manifest permissions")
|
|
70
|
+
LogUtils.d(CLASS_NAME, "Background audio handling ${if (enableBackgroundAudio) "enabled" else "disabled"} based on manifest permissions")
|
|
55
71
|
} catch (e: Exception) {
|
|
56
|
-
|
|
72
|
+
LogUtils.e(CLASS_NAME, "Failed to check manifest permissions: ${e.message}", e)
|
|
57
73
|
enablePhoneStateHandling = false
|
|
58
74
|
enableNotificationHandling = false
|
|
75
|
+
enableBackgroundAudio = false
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
Events(
|
|
62
79
|
Constants.AUDIO_EVENT_NAME,
|
|
63
80
|
Constants.AUDIO_ANALYSIS_EVENT_NAME,
|
|
64
81
|
Constants.RECORDING_INTERRUPTED_EVENT_NAME,
|
|
65
|
-
Constants.TRIM_PROGRESS_EVENT
|
|
82
|
+
Constants.TRIM_PROGRESS_EVENT,
|
|
83
|
+
Constants.DEVICE_CHANGED_EVENT // Add device changed event name
|
|
66
84
|
)
|
|
67
85
|
|
|
68
|
-
// Initialize
|
|
86
|
+
// Initialize Managers
|
|
69
87
|
initializeManager()
|
|
70
88
|
|
|
89
|
+
// Add a convenience function to check for foreground service permission separately
|
|
90
|
+
fun isForegroundServiceMicRequired(): Boolean {
|
|
91
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && enableBackgroundAudio
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add device-related functions to the module
|
|
95
|
+
|
|
96
|
+
// Gets available audio input devices with an optional refresh parameter
|
|
97
|
+
AsyncFunction("getAvailableInputDevices") { options: Map<String, Any>?, promise: Promise ->
|
|
98
|
+
try {
|
|
99
|
+
LogUtils.d(CLASS_NAME, "getAvailableInputDevices called. Refresh: ${options?.get("refresh") ?: false}")
|
|
100
|
+
|
|
101
|
+
// Check if refresh is requested
|
|
102
|
+
if (options?.get("refresh") as? Boolean == true) {
|
|
103
|
+
audioDeviceManager.forceRefreshAudioDevices()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get the list of devices
|
|
107
|
+
audioDeviceManager.getAvailableInputDevices(promise)
|
|
108
|
+
} catch (e: Exception) {
|
|
109
|
+
LogUtils.e(CLASS_NAME, "Error getting available input devices: ${e.message}", e)
|
|
110
|
+
promise.reject("DEVICE_ERROR", "Failed to get available audio devices: ${e.message}", e)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Gets the currently selected audio input device
|
|
115
|
+
AsyncFunction("getCurrentInputDevice") { promise: Promise ->
|
|
116
|
+
try {
|
|
117
|
+
LogUtils.d(CLASS_NAME, "getCurrentInputDevice called")
|
|
118
|
+
audioDeviceManager.getCurrentInputDevice(promise)
|
|
119
|
+
} catch (e: Exception) {
|
|
120
|
+
LogUtils.e(CLASS_NAME, "Error getting current input device: ${e.message}", e)
|
|
121
|
+
promise.reject("DEVICE_ERROR", "Failed to get current audio device: ${e.message}", e)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Selects a specific audio input device for recording
|
|
126
|
+
AsyncFunction("selectInputDevice") { deviceId: String, promise: Promise ->
|
|
127
|
+
try {
|
|
128
|
+
LogUtils.d(CLASS_NAME, "selectInputDevice called with ID: $deviceId")
|
|
129
|
+
audioDeviceManager.selectInputDevice(deviceId, promise)
|
|
130
|
+
|
|
131
|
+
// Update recording if in progress
|
|
132
|
+
if (audioRecorderManager.isRecording || audioRecorderManager.isPrepared) {
|
|
133
|
+
LogUtils.d(CLASS_NAME, "selectInputDevice: Notifying recorder of device change")
|
|
134
|
+
audioRecorderManager.handleDeviceChange()
|
|
135
|
+
}
|
|
136
|
+
} catch (e: Exception) {
|
|
137
|
+
LogUtils.e(CLASS_NAME, "Error selecting input device: ${e.message}", e)
|
|
138
|
+
promise.reject("DEVICE_ERROR", "Failed to select audio device: ${e.message}", e)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Resets to the default audio input device
|
|
143
|
+
AsyncFunction("resetToDefaultDevice") { promise: Promise ->
|
|
144
|
+
try {
|
|
145
|
+
LogUtils.d(CLASS_NAME, "resetToDefaultDevice called")
|
|
146
|
+
audioDeviceManager.resetToDefaultDevice { success, error ->
|
|
147
|
+
if (success) {
|
|
148
|
+
// Update recording if in progress
|
|
149
|
+
if (audioRecorderManager.isRecording || audioRecorderManager.isPrepared) {
|
|
150
|
+
LogUtils.d(CLASS_NAME, "resetToDefaultDevice: Notifying recorder of device change")
|
|
151
|
+
audioRecorderManager.handleDeviceChange()
|
|
152
|
+
}
|
|
153
|
+
promise.resolve(true)
|
|
154
|
+
} else {
|
|
155
|
+
LogUtils.e(CLASS_NAME, "Failed to reset to default device: ${error?.message}")
|
|
156
|
+
promise.reject("DEVICE_ERROR", "Failed to reset to default device: ${error?.message}", error)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (e: Exception) {
|
|
160
|
+
LogUtils.e(CLASS_NAME, "Error resetting to default device: ${e.message}", e)
|
|
161
|
+
promise.reject("DEVICE_ERROR", "Failed to reset to default device: ${e.message}", e)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Refreshes the audio devices list
|
|
166
|
+
Function("refreshAudioDevices") {
|
|
167
|
+
LogUtils.d(CLASS_NAME, "refreshAudioDevices called")
|
|
168
|
+
val success = audioDeviceManager.forceRefreshAudioDevices()
|
|
169
|
+
return@Function mapOf("success" to success)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
AsyncFunction("prepareRecording") { options: Map<String, Any?>, promise: Promise ->
|
|
173
|
+
try {
|
|
174
|
+
// If notifications are requested but permission not in manifest, modify options
|
|
175
|
+
if (options["showNotification"] as? Boolean == true && !enableNotificationHandling) {
|
|
176
|
+
val modifiedOptions = options.toMutableMap()
|
|
177
|
+
modifiedOptions["showNotification"] = false
|
|
178
|
+
LogUtils.d(CLASS_NAME, "Notification permission not in manifest, disabling showNotification")
|
|
179
|
+
|
|
180
|
+
if (audioRecorderManager.prepareRecording(modifiedOptions)) {
|
|
181
|
+
promise.resolve(true)
|
|
182
|
+
} else {
|
|
183
|
+
promise.reject("PREPARE_ERROR", "Failed to prepare recording", null)
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
if (audioRecorderManager.prepareRecording(options)) {
|
|
187
|
+
promise.resolve(true)
|
|
188
|
+
} else {
|
|
189
|
+
promise.reject("PREPARE_ERROR", "Failed to prepare recording", null)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (e: Exception) {
|
|
193
|
+
LogUtils.e(CLASS_NAME, "Error preparing recording", e)
|
|
194
|
+
promise.reject("PREPARE_ERROR", "Failed to prepare recording: ${e.message}", e)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
71
198
|
AsyncFunction("startRecording") { options: Map<String, Any?>, promise: Promise ->
|
|
72
199
|
// If notifications are requested but permission not in manifest, modify options
|
|
73
200
|
if (options["showNotification"] as? Boolean == true && !enableNotificationHandling) {
|
|
74
201
|
val modifiedOptions = options.toMutableMap()
|
|
75
202
|
modifiedOptions["showNotification"] = false
|
|
76
|
-
|
|
203
|
+
LogUtils.d(CLASS_NAME, "Notification permission not in manifest, disabling showNotification")
|
|
77
204
|
audioRecorderManager.startRecording(modifiedOptions, promise)
|
|
78
205
|
} else {
|
|
79
206
|
audioRecorderManager.startRecording(options, promise)
|
|
@@ -96,110 +223,25 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
96
223
|
audioRecorderManager.pauseRecording(promise)
|
|
97
224
|
}
|
|
98
225
|
|
|
99
|
-
AsyncFunction("
|
|
226
|
+
AsyncFunction("resumeRecording") { promise: Promise ->
|
|
227
|
+
LogUtils.d(CLASS_NAME, "⏺️ resumeRecording() called from JS layer")
|
|
100
228
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
val endTimeMs = options["endTimeMs"] as? Number
|
|
106
|
-
val position = options["position"] as? Number
|
|
107
|
-
val length = options["length"] as? Number
|
|
108
|
-
val segmentDurationMs = (options["segmentDurationMs"] as? Number)?.toInt() ?: 100
|
|
109
|
-
|
|
110
|
-
// Validate ranges - can have time range OR byte range OR no range
|
|
111
|
-
val hasTimeRange = startTimeMs != null && endTimeMs != null
|
|
112
|
-
val hasByteRange = position != null && length != null
|
|
113
|
-
|
|
114
|
-
// Only throw if both ranges are provided
|
|
115
|
-
if (hasTimeRange && hasByteRange) {
|
|
116
|
-
throw IllegalArgumentException("Cannot specify both time range and byte range")
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Get decoding options with default configuration
|
|
120
|
-
val defaultConfig = DecodingConfig(
|
|
121
|
-
targetSampleRate = null,
|
|
122
|
-
targetChannels = 1, // Default to mono
|
|
123
|
-
targetBitDepth = 16,
|
|
124
|
-
normalizeAudio = false
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
|
|
128
|
-
DecodingConfig(
|
|
129
|
-
targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
|
|
130
|
-
targetChannels = decodingOptionsMap["targetChannels"] as? Int,
|
|
131
|
-
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
132
|
-
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
133
|
-
)
|
|
134
|
-
} ?: defaultConfig
|
|
135
|
-
|
|
136
|
-
// Load audio data based on range type (or full file if no range specified)
|
|
137
|
-
val audioData = when {
|
|
138
|
-
hasByteRange -> {
|
|
139
|
-
val format = audioProcessor.getAudioFormat(fileUri)
|
|
140
|
-
?: throw IllegalArgumentException("Could not determine audio format")
|
|
141
|
-
|
|
142
|
-
// Calculate time range from byte position
|
|
143
|
-
val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
|
|
144
|
-
val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
|
|
145
|
-
val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
|
|
146
|
-
|
|
147
|
-
Log.d(Constants.TAG, "Loading audio with byte range: position=$position, length=$length")
|
|
148
|
-
|
|
149
|
-
audioProcessor.loadAudioRange(
|
|
150
|
-
fileUri = fileUri,
|
|
151
|
-
startTimeMs = effectiveStartTimeMs,
|
|
152
|
-
endTimeMs = effectiveEndTimeMs,
|
|
153
|
-
config = config
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
hasTimeRange -> {
|
|
157
|
-
Log.d(Constants.TAG, "Loading audio with time range: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs")
|
|
158
|
-
|
|
159
|
-
audioProcessor.loadAudioRange(
|
|
160
|
-
fileUri = fileUri,
|
|
161
|
-
startTimeMs = startTimeMs!!.toLong(),
|
|
162
|
-
endTimeMs = endTimeMs!!.toLong(),
|
|
163
|
-
config = config
|
|
164
|
-
)
|
|
229
|
+
audioRecorderManager.resumeRecording(object : Promise {
|
|
230
|
+
override fun resolve(value: Any?) {
|
|
231
|
+
LogUtils.d(CLASS_NAME, "⏺️ resumeRecording completed successfully")
|
|
232
|
+
promise.resolve(value)
|
|
165
233
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
234
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
235
|
+
LogUtils.e(CLASS_NAME, "⏺️ resumeRecording failed: $code - $message", cause)
|
|
236
|
+
promise.reject(code, message, cause)
|
|
169
237
|
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
val featuresMap = options["features"] as? Map<*, *>
|
|
173
|
-
val features = Features.parseFeatureOptions(featuresMap)
|
|
174
|
-
|
|
175
|
-
val recordingConfig = RecordingConfig(
|
|
176
|
-
sampleRate = audioData.sampleRate,
|
|
177
|
-
channels = audioData.channels,
|
|
178
|
-
encoding = when (audioData.bitDepth) {
|
|
179
|
-
8 -> "pcm_8bit"
|
|
180
|
-
16 -> "pcm_16bit"
|
|
181
|
-
32 -> "pcm_32bit"
|
|
182
|
-
else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
|
|
183
|
-
},
|
|
184
|
-
segmentDurationMs = segmentDurationMs,
|
|
185
|
-
features = features
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
Log.d(Constants.TAG, "extractAudioAnalysis: $recordingConfig")
|
|
189
|
-
audioProcessor.resetCumulativeAmplitudeRange()
|
|
190
|
-
|
|
191
|
-
val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
|
|
192
|
-
promise.resolve(analysisData.toDictionary())
|
|
238
|
+
})
|
|
193
239
|
} catch (e: Exception) {
|
|
194
|
-
|
|
195
|
-
promise.reject("
|
|
240
|
+
LogUtils.e(CLASS_NAME, "⏺️ Exception when calling resumeRecording: ${e.message}", e)
|
|
241
|
+
promise.reject("RESUME_ERROR", "Failed to resume recording: ${e.message}", e)
|
|
196
242
|
}
|
|
197
243
|
}
|
|
198
244
|
|
|
199
|
-
AsyncFunction("resumeRecording") { promise: Promise ->
|
|
200
|
-
audioRecorderManager.resumeRecording(promise)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
245
|
AsyncFunction("stopRecording") { promise: Promise ->
|
|
204
246
|
audioRecorderManager.stopRecording(promise)
|
|
205
247
|
}
|
|
@@ -215,20 +257,20 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
215
257
|
permissions.add(Manifest.permission.READ_PHONE_STATE)
|
|
216
258
|
}
|
|
217
259
|
|
|
218
|
-
// Add foreground service permission for Android 14+
|
|
219
|
-
if (
|
|
220
|
-
|
|
260
|
+
// Add foreground service permission for Android 14+ only if background audio is enabled
|
|
261
|
+
if (isForegroundServiceMicRequired()) {
|
|
262
|
+
LogUtils.d(CLASS_NAME, "Adding FOREGROUND_SERVICE_MICROPHONE permission request")
|
|
221
263
|
permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
|
|
222
264
|
}
|
|
223
265
|
|
|
224
|
-
|
|
266
|
+
LogUtils.d(CLASS_NAME, "Requesting permissions: $permissions")
|
|
225
267
|
Permissions.askForPermissionsWithPermissionsManager(
|
|
226
268
|
appContext.permissions,
|
|
227
269
|
promise,
|
|
228
270
|
*permissions.toTypedArray()
|
|
229
271
|
)
|
|
230
272
|
} catch (e: Exception) {
|
|
231
|
-
|
|
273
|
+
LogUtils.e(CLASS_NAME, "Error requesting permissions", e)
|
|
232
274
|
promise.reject("PERMISSION_ERROR", "Failed to request permissions: ${e.message}", e)
|
|
233
275
|
}
|
|
234
276
|
}
|
|
@@ -243,7 +285,8 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
243
285
|
permissions.add(Manifest.permission.READ_PHONE_STATE)
|
|
244
286
|
}
|
|
245
287
|
|
|
246
|
-
|
|
288
|
+
// Only check foreground service permission when background audio is enabled
|
|
289
|
+
if (isForegroundServiceMicRequired()) {
|
|
247
290
|
permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
|
|
248
291
|
}
|
|
249
292
|
|
|
@@ -301,8 +344,8 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
301
344
|
return@AsyncFunction
|
|
302
345
|
}
|
|
303
346
|
|
|
304
|
-
|
|
305
|
-
|
|
347
|
+
LogUtils.d(CLASS_NAME, "trimAudio called with fileUri: $fileUri")
|
|
348
|
+
LogUtils.d(CLASS_NAME, "Full options: $options")
|
|
306
349
|
|
|
307
350
|
val mode = options["mode"] as? String ?: "single"
|
|
308
351
|
val startTimeMs = (options["startTimeMs"] as? Number)?.toLong()
|
|
@@ -320,7 +363,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
320
363
|
if (outputFormatMap != null) {
|
|
321
364
|
val format = outputFormatMap["format"] as? String
|
|
322
365
|
if (format != null && format != "wav" && format != "aac" && format != "opus") {
|
|
323
|
-
|
|
366
|
+
LogUtils.w(CLASS_NAME, "Requested format '$format' is not fully supported. Using 'aac' instead.")
|
|
324
367
|
// Create a new map with the corrected format
|
|
325
368
|
val newOutputFormat = HashMap<String, Any>(outputFormatMap)
|
|
326
369
|
newOutputFormat["format"] = "aac"
|
|
@@ -328,7 +371,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
328
371
|
}
|
|
329
372
|
}
|
|
330
373
|
|
|
331
|
-
|
|
374
|
+
LogUtils.d(CLASS_NAME, "Output format options: $outputFormatMap")
|
|
332
375
|
|
|
333
376
|
// Create progress listener
|
|
334
377
|
val progressListener = object : AudioTrimmer.ProgressListener {
|
|
@@ -365,10 +408,10 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
365
408
|
"durationMs" to processingTimeMs
|
|
366
409
|
)
|
|
367
410
|
|
|
368
|
-
|
|
411
|
+
LogUtils.d(CLASS_NAME, "Trim operation completed successfully in ${processingTimeMs}ms: $result")
|
|
369
412
|
promise.resolve(resultWithProcessingTime)
|
|
370
413
|
} catch (e: Exception) {
|
|
371
|
-
|
|
414
|
+
LogUtils.e(CLASS_NAME, "Error trimming audio: ${e.message}", e)
|
|
372
415
|
promise.reject("TRIM_ERROR", "Error trimming audio: ${e.message}", e)
|
|
373
416
|
}
|
|
374
417
|
}
|
|
@@ -376,45 +419,45 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
376
419
|
AsyncFunction("extractMelSpectrogram") { options: Map<String, Any>, promise: Promise ->
|
|
377
420
|
try {
|
|
378
421
|
// Log all incoming options for debugging
|
|
379
|
-
|
|
422
|
+
LogUtils.d(CLASS_NAME, "extractMelSpectrogram called with options: $options")
|
|
380
423
|
|
|
381
424
|
// Extract required parameters with detailed logging
|
|
382
425
|
val fileUri = options["fileUri"] as? String
|
|
383
|
-
|
|
426
|
+
LogUtils.d(CLASS_NAME, "fileUri: $fileUri")
|
|
384
427
|
if (fileUri == null) {
|
|
385
|
-
|
|
428
|
+
LogUtils.e(CLASS_NAME, "Missing required parameter: fileUri")
|
|
386
429
|
throw IllegalArgumentException("fileUri is required")
|
|
387
430
|
}
|
|
388
431
|
|
|
389
432
|
val windowSizeMs = options["windowSizeMs"] as? Double
|
|
390
|
-
|
|
433
|
+
LogUtils.d(CLASS_NAME, "windowSizeMs: $windowSizeMs")
|
|
391
434
|
if (windowSizeMs == null) {
|
|
392
|
-
|
|
435
|
+
LogUtils.e(CLASS_NAME, "Missing required parameter: windowSizeMs")
|
|
393
436
|
throw IllegalArgumentException("windowSizeMs is required")
|
|
394
437
|
}
|
|
395
438
|
|
|
396
439
|
val hopLengthMs = options["hopLengthMs"] as? Double
|
|
397
|
-
|
|
440
|
+
LogUtils.d(CLASS_NAME, "hopLengthMs: $hopLengthMs")
|
|
398
441
|
if (hopLengthMs == null) {
|
|
399
|
-
|
|
442
|
+
LogUtils.e(CLASS_NAME, "Missing required parameter: hopLengthMs")
|
|
400
443
|
throw IllegalArgumentException("hopLengthMs is required")
|
|
401
444
|
}
|
|
402
445
|
|
|
403
446
|
// Handle nMels which might come as Double from JavaScript
|
|
404
447
|
val nMelsValue = options["nMels"]
|
|
405
|
-
|
|
448
|
+
LogUtils.d(CLASS_NAME, "Raw nMels value: $nMelsValue (type: ${nMelsValue?.javaClass?.name})")
|
|
406
449
|
|
|
407
450
|
val nMels = when (nMelsValue) {
|
|
408
451
|
is Int -> nMelsValue
|
|
409
452
|
is Double -> nMelsValue.toInt()
|
|
410
453
|
is Number -> nMelsValue.toInt()
|
|
411
454
|
else -> {
|
|
412
|
-
|
|
455
|
+
LogUtils.e(CLASS_NAME, "Missing or invalid required parameter: nMels")
|
|
413
456
|
throw IllegalArgumentException("nMels is required and must be a number")
|
|
414
457
|
}
|
|
415
458
|
}
|
|
416
459
|
|
|
417
|
-
|
|
460
|
+
LogUtils.d(CLASS_NAME, "Converted nMels: $nMels (from ${nMelsValue?.javaClass?.name})")
|
|
418
461
|
|
|
419
462
|
// Extract optional parameters with defaults
|
|
420
463
|
val fMin = options["fMin"] as? Double ?: 0.0
|
|
@@ -429,7 +472,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
429
472
|
val startTimeMs = startTimeMsNumber?.toLong() ?: startTimeMsNumber?.toDouble()?.toLong()
|
|
430
473
|
val endTimeMs = endTimeMsNumber?.toLong() ?: endTimeMsNumber?.toDouble()?.toLong()
|
|
431
474
|
|
|
432
|
-
|
|
475
|
+
LogUtils.d(CLASS_NAME, """
|
|
433
476
|
Optional parameters:
|
|
434
477
|
- fMin: $fMin
|
|
435
478
|
- fMax: $fMax
|
|
@@ -442,7 +485,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
442
485
|
|
|
443
486
|
// Handle decoding options
|
|
444
487
|
val decodingOptions = options["decodingOptions"] as? Map<String, Any>
|
|
445
|
-
|
|
488
|
+
LogUtils.d(CLASS_NAME, "Decoding options: $decodingOptions")
|
|
446
489
|
|
|
447
490
|
val config = decodingOptions?.let {
|
|
448
491
|
val targetSampleRateValue = it["targetSampleRate"]
|
|
@@ -477,7 +520,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
477
520
|
targetBitDepth = targetBitDepth,
|
|
478
521
|
normalizeAudio = normalizeAudio
|
|
479
522
|
).also { config ->
|
|
480
|
-
|
|
523
|
+
LogUtils.d(CLASS_NAME, """
|
|
481
524
|
Using decoding config:
|
|
482
525
|
- targetSampleRate: ${config.targetSampleRate ?: "original"}
|
|
483
526
|
- targetChannels: ${config.targetChannels ?: "original"}
|
|
@@ -486,38 +529,38 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
486
529
|
""".trimIndent())
|
|
487
530
|
}
|
|
488
531
|
} ?: DecodingConfig(targetSampleRate = null, targetChannels = 1, targetBitDepth = 16).also {
|
|
489
|
-
|
|
532
|
+
LogUtils.d(CLASS_NAME, "Using default decoding config")
|
|
490
533
|
}
|
|
491
534
|
|
|
492
535
|
// Check if the audio data is too short
|
|
493
536
|
if (startTimeMs != null && endTimeMs != null) {
|
|
494
537
|
val durationMs = endTimeMs - startTimeMs
|
|
495
|
-
|
|
538
|
+
LogUtils.d(CLASS_NAME, "Audio duration for spectrogram: $durationMs ms")
|
|
496
539
|
if (durationMs < 25) { // 25ms is minimum for a single window
|
|
497
|
-
|
|
540
|
+
LogUtils.w(CLASS_NAME, "Audio duration is too short for spectrogram analysis: $durationMs ms")
|
|
498
541
|
throw IllegalArgumentException("Audio duration must be at least 25ms for spectrogram analysis")
|
|
499
542
|
}
|
|
500
543
|
}
|
|
501
544
|
|
|
502
545
|
// Load audio data with optional time range
|
|
503
|
-
|
|
546
|
+
LogUtils.d(CLASS_NAME, "Loading audio data...")
|
|
504
547
|
val audioData = when {
|
|
505
548
|
startTimeMs != null && endTimeMs != null -> {
|
|
506
|
-
|
|
549
|
+
LogUtils.d(CLASS_NAME, "Loading audio range: $startTimeMs to $endTimeMs ms")
|
|
507
550
|
audioProcessor.loadAudioRange(fileUri, startTimeMs, endTimeMs, config)
|
|
508
551
|
}
|
|
509
552
|
else -> {
|
|
510
|
-
|
|
553
|
+
LogUtils.d(CLASS_NAME, "Loading entire audio file")
|
|
511
554
|
audioProcessor.loadAudioFromAnyFormat(fileUri, config)
|
|
512
555
|
}
|
|
513
556
|
}
|
|
514
557
|
|
|
515
558
|
if (audioData == null) {
|
|
516
|
-
|
|
559
|
+
LogUtils.e(CLASS_NAME, "Failed to load audio data")
|
|
517
560
|
throw IllegalStateException("Failed to load audio data")
|
|
518
561
|
}
|
|
519
562
|
|
|
520
|
-
|
|
563
|
+
LogUtils.d(CLASS_NAME, """
|
|
521
564
|
Audio data loaded successfully:
|
|
522
565
|
- data size: ${audioData.data.size} bytes
|
|
523
566
|
- sampleRate: ${audioData.sampleRate}
|
|
@@ -528,7 +571,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
528
571
|
|
|
529
572
|
// Validate that we have enough audio data for processing
|
|
530
573
|
if (audioData.data.size == 0 || audioData.durationMs < windowSizeMs) {
|
|
531
|
-
|
|
574
|
+
LogUtils.e(CLASS_NAME, "Audio data is too short for spectrogram analysis: ${audioData.durationMs}ms, data size: ${audioData.data.size} bytes")
|
|
532
575
|
throw IllegalArgumentException(
|
|
533
576
|
"Audio data is too short for spectrogram analysis. " +
|
|
534
577
|
"Duration: ${audioData.durationMs}ms, minimum required: ${windowSizeMs}ms"
|
|
@@ -536,7 +579,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
536
579
|
}
|
|
537
580
|
|
|
538
581
|
// Compute mel-spectrogram
|
|
539
|
-
|
|
582
|
+
LogUtils.d(CLASS_NAME, "Computing mel-spectrogram...")
|
|
540
583
|
val spectrogramData = audioProcessor.extractMelSpectrogram(
|
|
541
584
|
audioData = audioData,
|
|
542
585
|
windowSizeMs = windowSizeMs.toFloat(),
|
|
@@ -549,7 +592,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
549
592
|
windowType = windowType
|
|
550
593
|
)
|
|
551
594
|
|
|
552
|
-
|
|
595
|
+
LogUtils.d(CLASS_NAME, "Mel-spectrogram computed successfully with ${spectrogramData.spectrogram.size} time steps")
|
|
553
596
|
|
|
554
597
|
// Convert to map for React Native
|
|
555
598
|
val result = mapOf(
|
|
@@ -560,11 +603,11 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
560
603
|
"durationMs" to audioData.durationMs
|
|
561
604
|
)
|
|
562
605
|
|
|
563
|
-
|
|
606
|
+
LogUtils.d(CLASS_NAME, "Returning result with ${result["timeSteps"]} time steps and $nMels mel bands")
|
|
564
607
|
promise.resolve(result)
|
|
565
608
|
} catch (e: Exception) {
|
|
566
|
-
|
|
567
|
-
|
|
609
|
+
LogUtils.e(CLASS_NAME, "Failed to extract mel-spectrogram: ${e.message}")
|
|
610
|
+
LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
|
|
568
611
|
promise.reject("SPECTROGRAM_ERROR", e.message ?: "Unknown error", e)
|
|
569
612
|
}
|
|
570
613
|
}
|
|
@@ -588,6 +631,107 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
588
631
|
promise.resolve(status)
|
|
589
632
|
}
|
|
590
633
|
|
|
634
|
+
|
|
635
|
+
AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
|
|
636
|
+
try {
|
|
637
|
+
val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
|
|
638
|
+
|
|
639
|
+
// Get time or byte range options
|
|
640
|
+
val startTimeMs = options["startTimeMs"] as? Number
|
|
641
|
+
val endTimeMs = options["endTimeMs"] as? Number
|
|
642
|
+
val position = options["position"] as? Number
|
|
643
|
+
val length = options["length"] as? Number
|
|
644
|
+
val segmentDurationMs = (options["segmentDurationMs"] as? Number)?.toInt() ?: 100
|
|
645
|
+
|
|
646
|
+
// Validate ranges - can have time range OR byte range OR no range
|
|
647
|
+
val hasTimeRange = startTimeMs != null && endTimeMs != null
|
|
648
|
+
val hasByteRange = position != null && length != null
|
|
649
|
+
|
|
650
|
+
// Only throw if both ranges are provided
|
|
651
|
+
if (hasTimeRange && hasByteRange) {
|
|
652
|
+
throw IllegalArgumentException("Cannot specify both time range and byte range")
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Get decoding options with default configuration
|
|
656
|
+
val defaultConfig = DecodingConfig(
|
|
657
|
+
targetSampleRate = null,
|
|
658
|
+
targetChannels = 1, // Default to mono
|
|
659
|
+
targetBitDepth = 16,
|
|
660
|
+
normalizeAudio = false
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
|
|
664
|
+
DecodingConfig(
|
|
665
|
+
targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
|
|
666
|
+
targetChannels = decodingOptionsMap["targetChannels"] as? Int,
|
|
667
|
+
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
668
|
+
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
669
|
+
)
|
|
670
|
+
} ?: defaultConfig
|
|
671
|
+
|
|
672
|
+
// Load audio data based on range type (or full file if no range specified)
|
|
673
|
+
val audioData = when {
|
|
674
|
+
hasByteRange -> {
|
|
675
|
+
val format = audioProcessor.getAudioFormat(fileUri)
|
|
676
|
+
?: throw IllegalArgumentException("Could not determine audio format")
|
|
677
|
+
|
|
678
|
+
// Calculate time range from byte position
|
|
679
|
+
val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
|
|
680
|
+
val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
|
|
681
|
+
val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
|
|
682
|
+
|
|
683
|
+
LogUtils.d(CLASS_NAME, "Loading audio with byte range: position=$position, length=$length")
|
|
684
|
+
|
|
685
|
+
audioProcessor.loadAudioRange(
|
|
686
|
+
fileUri = fileUri,
|
|
687
|
+
startTimeMs = effectiveStartTimeMs,
|
|
688
|
+
endTimeMs = effectiveEndTimeMs,
|
|
689
|
+
config = config
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
hasTimeRange -> {
|
|
693
|
+
LogUtils.d(CLASS_NAME, "Loading audio with time range: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs")
|
|
694
|
+
|
|
695
|
+
audioProcessor.loadAudioRange(
|
|
696
|
+
fileUri = fileUri,
|
|
697
|
+
startTimeMs = startTimeMs!!.toLong(),
|
|
698
|
+
endTimeMs = endTimeMs!!.toLong(),
|
|
699
|
+
config = config
|
|
700
|
+
)
|
|
701
|
+
}
|
|
702
|
+
else -> {
|
|
703
|
+
LogUtils.d(CLASS_NAME, "Loading entire audio file")
|
|
704
|
+
audioProcessor.loadAudioFromAnyFormat(fileUri, config)
|
|
705
|
+
}
|
|
706
|
+
} ?: throw IllegalStateException("Failed to load audio data")
|
|
707
|
+
|
|
708
|
+
val featuresMap = options["features"] as? Map<*, *>
|
|
709
|
+
val features = Features.parseFeatureOptions(featuresMap)
|
|
710
|
+
|
|
711
|
+
val recordingConfig = RecordingConfig(
|
|
712
|
+
sampleRate = audioData.sampleRate,
|
|
713
|
+
channels = audioData.channels,
|
|
714
|
+
encoding = when (audioData.bitDepth) {
|
|
715
|
+
8 -> "pcm_8bit"
|
|
716
|
+
16 -> "pcm_16bit"
|
|
717
|
+
32 -> "pcm_32bit"
|
|
718
|
+
else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
|
|
719
|
+
},
|
|
720
|
+
segmentDurationMs = segmentDurationMs,
|
|
721
|
+
features = features
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
LogUtils.d(CLASS_NAME, "extractAudioAnalysis: $recordingConfig")
|
|
725
|
+
audioProcessor.resetCumulativeAmplitudeRange()
|
|
726
|
+
|
|
727
|
+
val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
|
|
728
|
+
promise.resolve(analysisData.toDictionary())
|
|
729
|
+
} catch (e: Exception) {
|
|
730
|
+
LogUtils.e(CLASS_NAME, "Failed to extract audio analysis: ${e.message}", e)
|
|
731
|
+
promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
591
735
|
AsyncFunction("extractAudioData") { options: Map<String, Any>, promise: Promise ->
|
|
592
736
|
try {
|
|
593
737
|
val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
|
|
@@ -616,7 +760,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
616
760
|
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
617
761
|
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
618
762
|
).also {
|
|
619
|
-
|
|
763
|
+
LogUtils.d(CLASS_NAME, """
|
|
620
764
|
Using decoding config:
|
|
621
765
|
- targetSampleRate: ${it.targetSampleRate ?: "original"}
|
|
622
766
|
- targetChannels: ${it.targetChannels ?: "original"}
|
|
@@ -635,7 +779,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
635
779
|
val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
|
|
636
780
|
val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
|
|
637
781
|
|
|
638
|
-
|
|
782
|
+
LogUtils.d(CLASS_NAME, """
|
|
639
783
|
Converting byte range to time range:
|
|
640
784
|
- position: $position bytes
|
|
641
785
|
- length: $length bytes
|
|
@@ -652,7 +796,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
652
796
|
)
|
|
653
797
|
} else {
|
|
654
798
|
// Must be time range due to earlier validation
|
|
655
|
-
|
|
799
|
+
LogUtils.d(CLASS_NAME, """
|
|
656
800
|
Using time range:
|
|
657
801
|
- startTimeMs: $startTimeMs
|
|
658
802
|
- endTimeMs: $endTimeMs
|
|
@@ -666,7 +810,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
666
810
|
)
|
|
667
811
|
} ?: throw IllegalStateException("Failed to load audio data")
|
|
668
812
|
|
|
669
|
-
|
|
813
|
+
LogUtils.d(CLASS_NAME, """
|
|
670
814
|
Audio data loaded successfully:
|
|
671
815
|
- data size: ${audioData.data.size} bytes
|
|
672
816
|
- sampleRate: ${audioData.sampleRate}
|
|
@@ -707,7 +851,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
707
851
|
resultMap["pcmData"] = wavData
|
|
708
852
|
resultMap["hasWavHeader"] = true
|
|
709
853
|
|
|
710
|
-
|
|
854
|
+
LogUtils.d(CLASS_NAME, "Added WAV header to PCM data, total size: ${wavData.size} bytes")
|
|
711
855
|
} else {
|
|
712
856
|
resultMap["pcmData"] = audioData.data
|
|
713
857
|
resultMap["hasWavHeader"] = false
|
|
@@ -729,7 +873,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
729
873
|
crc32.update(audioData.data)
|
|
730
874
|
resultMap["checksum"] = crc32.value.toInt()
|
|
731
875
|
|
|
732
|
-
|
|
876
|
+
LogUtils.d(CLASS_NAME, "Computed CRC32 checksum: ${crc32.value}")
|
|
733
877
|
}
|
|
734
878
|
|
|
735
879
|
if (includeNormalizedData) {
|
|
@@ -751,32 +895,165 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
751
895
|
|
|
752
896
|
promise.resolve(resultMap)
|
|
753
897
|
} catch (e: Exception) {
|
|
754
|
-
|
|
755
|
-
|
|
898
|
+
LogUtils.e(CLASS_NAME, "Failed to extract audio data: ${e.message}")
|
|
899
|
+
LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
|
|
756
900
|
promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
|
|
757
901
|
}
|
|
758
902
|
}
|
|
759
903
|
}
|
|
760
904
|
|
|
761
905
|
private fun initializeManager() {
|
|
762
|
-
val
|
|
763
|
-
val
|
|
906
|
+
val context = appContext.reactContext ?: throw IllegalStateException("React context not available")
|
|
907
|
+
val filesDir = context.filesDir
|
|
908
|
+
val permissionUtils = PermissionUtils(context)
|
|
764
909
|
val audioDataEncoder = AudioDataEncoder()
|
|
765
|
-
|
|
766
|
-
|
|
910
|
+
|
|
911
|
+
// Initialize AudioDeviceManager
|
|
912
|
+
audioDeviceManager = AudioDeviceManager(context)
|
|
913
|
+
|
|
914
|
+
// Initialize AudioRecorderManager with AudioDeviceManager integration
|
|
915
|
+
audioRecorderManager = AudioRecorderManager.initialize(
|
|
916
|
+
context,
|
|
767
917
|
filesDir,
|
|
768
918
|
permissionUtils,
|
|
769
919
|
audioDataEncoder,
|
|
770
920
|
this,
|
|
771
|
-
enablePhoneStateHandling
|
|
921
|
+
enablePhoneStateHandling,
|
|
922
|
+
enableBackgroundAudio
|
|
772
923
|
)
|
|
924
|
+
|
|
925
|
+
// Set up the delegate for the AudioDeviceManager
|
|
926
|
+
audioDeviceManager.delegate = object : AudioDeviceManagerDelegate {
|
|
927
|
+
override fun onDeviceDisconnected(deviceId: String) {
|
|
928
|
+
LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
|
|
929
|
+
// Handle device disconnection
|
|
930
|
+
coroutineScope.launch {
|
|
931
|
+
try {
|
|
932
|
+
// If recording is active, handle the disconnection based on the recording config
|
|
933
|
+
if (audioRecorderManager.isRecording) {
|
|
934
|
+
handleDeviceDisconnection(deviceId)
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Notify JS about the disconnection
|
|
938
|
+
sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
939
|
+
"reason" to "deviceDisconnected",
|
|
940
|
+
"deviceId" to deviceId
|
|
941
|
+
))
|
|
942
|
+
} catch (e: Exception) {
|
|
943
|
+
LogUtils.e(CLASS_NAME, "📱 Error handling device disconnection: ${e.message}", e)
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
773
949
|
audioProcessor = AudioProcessor(filesDir)
|
|
774
950
|
}
|
|
775
|
-
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Handles audio device disconnection based on the recording configuration
|
|
954
|
+
*/
|
|
955
|
+
private suspend fun handleDeviceDisconnection(deviceId: String) {
|
|
956
|
+
LogUtils.d(CLASS_NAME, "📱 handleDeviceDisconnection called for device: $deviceId")
|
|
957
|
+
// Get disconnection behavior from recorder config
|
|
958
|
+
val behavior = audioRecorderManager.getDeviceDisconnectionBehavior()
|
|
959
|
+
LogUtils.d(CLASS_NAME, "📱 Device disconnection behavior configured as: $behavior")
|
|
960
|
+
|
|
961
|
+
when (behavior) {
|
|
962
|
+
"fallback" -> {
|
|
963
|
+
LogUtils.d(CLASS_NAME, "📱 Using fallback behavior, getting default device")
|
|
964
|
+
// Get default device
|
|
965
|
+
val defaultDevice = withContext(Dispatchers.IO) {
|
|
966
|
+
audioDeviceManager.getDefaultInputDevice()
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (defaultDevice != null) {
|
|
970
|
+
LogUtils.d(CLASS_NAME, "📱 Falling back to default device: ${defaultDevice["name"]}")
|
|
971
|
+
|
|
972
|
+
// Select default device
|
|
973
|
+
val deviceId = defaultDevice["id"] as String
|
|
974
|
+
LogUtils.d(CLASS_NAME, "📱 Attempting to select default device: $deviceId")
|
|
975
|
+
val success = audioDeviceManager.selectDevice(deviceId)
|
|
976
|
+
|
|
977
|
+
if (success) {
|
|
978
|
+
LogUtils.d(CLASS_NAME, "📱 Successfully selected default device, notifying AudioRecorderManager")
|
|
979
|
+
// Notify AudioRecorderManager to update its recording source
|
|
980
|
+
audioRecorderManager.handleDeviceChange()
|
|
981
|
+
|
|
982
|
+
// Notify JS about fallback
|
|
983
|
+
LogUtils.d(CLASS_NAME, "📱 Sending deviceFallback event to JS")
|
|
984
|
+
sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
985
|
+
"reason" to "deviceFallback",
|
|
986
|
+
"isPaused" to false,
|
|
987
|
+
"deviceId" to deviceId
|
|
988
|
+
))
|
|
989
|
+
} else {
|
|
990
|
+
LogUtils.e(CLASS_NAME, "📱 Failed to select default device, pausing recording")
|
|
991
|
+
|
|
992
|
+
// Fall back to pause if we can't select the default device
|
|
993
|
+
audioRecorderManager.pauseRecording(object : Promise {
|
|
994
|
+
override fun resolve(value: Any?) {
|
|
995
|
+
LogUtils.d(CLASS_NAME, "📱 Recording successfully paused, notifying AudioRecorderManager")
|
|
996
|
+
// Notify AudioRecorderManager to handle device change while paused
|
|
997
|
+
audioRecorderManager.handleDeviceChange()
|
|
998
|
+
|
|
999
|
+
sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1000
|
+
"reason" to "deviceSwitchFailed",
|
|
1001
|
+
"isPaused" to true
|
|
1002
|
+
))
|
|
1003
|
+
}
|
|
1004
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1005
|
+
LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
|
|
1006
|
+
}
|
|
1007
|
+
})
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
LogUtils.e(CLASS_NAME, "📱 No default device found, pausing recording")
|
|
1011
|
+
|
|
1012
|
+
// Fall back to pause if we can't find a default device
|
|
1013
|
+
audioRecorderManager.pauseRecording(object : Promise {
|
|
1014
|
+
override fun resolve(value: Any?) {
|
|
1015
|
+
LogUtils.d(CLASS_NAME, "📱 Recording successfully paused when no default device found")
|
|
1016
|
+
// Notify AudioRecorderManager to handle device change while paused
|
|
1017
|
+
audioRecorderManager.handleDeviceChange()
|
|
1018
|
+
|
|
1019
|
+
sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1020
|
+
"reason" to "deviceDisconnected",
|
|
1021
|
+
"isPaused" to true
|
|
1022
|
+
))
|
|
1023
|
+
}
|
|
1024
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1025
|
+
LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
|
|
1026
|
+
}
|
|
1027
|
+
})
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
else -> { // Default to pause behavior
|
|
1032
|
+
LogUtils.d(CLASS_NAME, "📱 Using pause behavior for device disconnection")
|
|
1033
|
+
|
|
1034
|
+
// Pause recording
|
|
1035
|
+
audioRecorderManager.pauseRecording(object : Promise {
|
|
1036
|
+
override fun resolve(value: Any?) {
|
|
1037
|
+
LogUtils.d(CLASS_NAME, "📱 Recording successfully paused after device disconnection")
|
|
1038
|
+
// Notify AudioRecorderManager to handle device change while paused
|
|
1039
|
+
audioRecorderManager.handleDeviceChange()
|
|
1040
|
+
|
|
1041
|
+
sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1042
|
+
"reason" to "deviceDisconnected",
|
|
1043
|
+
"isPaused" to true
|
|
1044
|
+
))
|
|
1045
|
+
}
|
|
1046
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1047
|
+
LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
|
|
1048
|
+
}
|
|
1049
|
+
})
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
LogUtils.d(CLASS_NAME, "📱 handleDeviceDisconnection completed")
|
|
1053
|
+
}
|
|
776
1054
|
|
|
777
1055
|
override fun sendExpoEvent(eventName: String, params: Bundle) {
|
|
778
|
-
|
|
1056
|
+
LogUtils.d(CLASS_NAME, "Sending event: $eventName")
|
|
779
1057
|
this@ExpoAudioStreamModule.sendEvent(eventName, params)
|
|
780
1058
|
}
|
|
781
|
-
|
|
782
1059
|
}
|