@siteed/expo-audio-studio 2.4.1 → 2.6.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 +576 -252
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +419 -155
- package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
- 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 +104 -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 +478 -62
- 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 +74 -11
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +390 -74
- 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/utils/writeWavHeader.d.ts +3 -18
- package/build/utils/writeWavHeader.d.ts.map +1 -1
- package/build/utils/writeWavHeader.js +19 -26
- package/build/utils/writeWavHeader.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 +113 -1
- package/src/ExpoAudioStream.web.ts +609 -69
- package/src/ExpoAudioStreamModule.ts +23 -0
- package/src/WebRecorder.web.ts +482 -92
- 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/utils/writeWavHeader.ts +26 -25
- package/src/workers/InlineFeaturesExtractor.web.tsx +27 -26
- package/src/workers/inlineAudioWebWorker.web.tsx +25 -1
|
@@ -13,13 +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
|
|
22
32
|
private var enableBackgroundAudio: Boolean = false // Default to false until we check manifest
|
|
33
|
+
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
23
34
|
|
|
24
35
|
private val audioFileHandler by lazy {
|
|
25
36
|
AudioFileHandler(appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available"))
|
|
@@ -54,11 +65,11 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
54
65
|
// Check if background audio is enabled by looking for FOREGROUND_SERVICE permission
|
|
55
66
|
enableBackgroundAudio = packageInfo.requestedPermissions?.contains(Manifest.permission.FOREGROUND_SERVICE) ?: false
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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")
|
|
60
71
|
} catch (e: Exception) {
|
|
61
|
-
|
|
72
|
+
LogUtils.e(CLASS_NAME, "Failed to check manifest permissions: ${e.message}", e)
|
|
62
73
|
enablePhoneStateHandling = false
|
|
63
74
|
enableNotificationHandling = false
|
|
64
75
|
enableBackgroundAudio = false
|
|
@@ -68,10 +79,11 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
68
79
|
Constants.AUDIO_EVENT_NAME,
|
|
69
80
|
Constants.AUDIO_ANALYSIS_EVENT_NAME,
|
|
70
81
|
Constants.RECORDING_INTERRUPTED_EVENT_NAME,
|
|
71
|
-
Constants.TRIM_PROGRESS_EVENT
|
|
82
|
+
Constants.TRIM_PROGRESS_EVENT,
|
|
83
|
+
Constants.DEVICE_CHANGED_EVENT // Add device changed event name
|
|
72
84
|
)
|
|
73
85
|
|
|
74
|
-
// Initialize
|
|
86
|
+
// Initialize Managers
|
|
75
87
|
initializeManager()
|
|
76
88
|
|
|
77
89
|
// Add a convenience function to check for foreground service permission separately
|
|
@@ -79,12 +91,116 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
79
91
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && enableBackgroundAudio
|
|
80
92
|
}
|
|
81
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
|
+
|
|
82
198
|
AsyncFunction("startRecording") { options: Map<String, Any?>, promise: Promise ->
|
|
83
199
|
// If notifications are requested but permission not in manifest, modify options
|
|
84
200
|
if (options["showNotification"] as? Boolean == true && !enableNotificationHandling) {
|
|
85
201
|
val modifiedOptions = options.toMutableMap()
|
|
86
202
|
modifiedOptions["showNotification"] = false
|
|
87
|
-
|
|
203
|
+
LogUtils.d(CLASS_NAME, "Notification permission not in manifest, disabling showNotification")
|
|
88
204
|
audioRecorderManager.startRecording(modifiedOptions, promise)
|
|
89
205
|
} else {
|
|
90
206
|
audioRecorderManager.startRecording(options, promise)
|
|
@@ -107,110 +223,25 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
107
223
|
audioRecorderManager.pauseRecording(promise)
|
|
108
224
|
}
|
|
109
225
|
|
|
110
|
-
AsyncFunction("
|
|
226
|
+
AsyncFunction("resumeRecording") { promise: Promise ->
|
|
227
|
+
LogUtils.d(CLASS_NAME, "⏺️ resumeRecording() called from JS layer")
|
|
111
228
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
val endTimeMs = options["endTimeMs"] as? Number
|
|
117
|
-
val position = options["position"] as? Number
|
|
118
|
-
val length = options["length"] as? Number
|
|
119
|
-
val segmentDurationMs = (options["segmentDurationMs"] as? Number)?.toInt() ?: 100
|
|
120
|
-
|
|
121
|
-
// Validate ranges - can have time range OR byte range OR no range
|
|
122
|
-
val hasTimeRange = startTimeMs != null && endTimeMs != null
|
|
123
|
-
val hasByteRange = position != null && length != null
|
|
124
|
-
|
|
125
|
-
// Only throw if both ranges are provided
|
|
126
|
-
if (hasTimeRange && hasByteRange) {
|
|
127
|
-
throw IllegalArgumentException("Cannot specify both time range and byte range")
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Get decoding options with default configuration
|
|
131
|
-
val defaultConfig = DecodingConfig(
|
|
132
|
-
targetSampleRate = null,
|
|
133
|
-
targetChannels = 1, // Default to mono
|
|
134
|
-
targetBitDepth = 16,
|
|
135
|
-
normalizeAudio = false
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
|
|
139
|
-
DecodingConfig(
|
|
140
|
-
targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
|
|
141
|
-
targetChannels = decodingOptionsMap["targetChannels"] as? Int,
|
|
142
|
-
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
143
|
-
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
144
|
-
)
|
|
145
|
-
} ?: defaultConfig
|
|
146
|
-
|
|
147
|
-
// Load audio data based on range type (or full file if no range specified)
|
|
148
|
-
val audioData = when {
|
|
149
|
-
hasByteRange -> {
|
|
150
|
-
val format = audioProcessor.getAudioFormat(fileUri)
|
|
151
|
-
?: throw IllegalArgumentException("Could not determine audio format")
|
|
152
|
-
|
|
153
|
-
// Calculate time range from byte position
|
|
154
|
-
val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
|
|
155
|
-
val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
|
|
156
|
-
val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
|
|
157
|
-
|
|
158
|
-
Log.d(Constants.TAG, "Loading audio with byte range: position=$position, length=$length")
|
|
159
|
-
|
|
160
|
-
audioProcessor.loadAudioRange(
|
|
161
|
-
fileUri = fileUri,
|
|
162
|
-
startTimeMs = effectiveStartTimeMs,
|
|
163
|
-
endTimeMs = effectiveEndTimeMs,
|
|
164
|
-
config = config
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
hasTimeRange -> {
|
|
168
|
-
Log.d(Constants.TAG, "Loading audio with time range: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs")
|
|
169
|
-
|
|
170
|
-
audioProcessor.loadAudioRange(
|
|
171
|
-
fileUri = fileUri,
|
|
172
|
-
startTimeMs = startTimeMs!!.toLong(),
|
|
173
|
-
endTimeMs = endTimeMs!!.toLong(),
|
|
174
|
-
config = config
|
|
175
|
-
)
|
|
229
|
+
audioRecorderManager.resumeRecording(object : Promise {
|
|
230
|
+
override fun resolve(value: Any?) {
|
|
231
|
+
LogUtils.d(CLASS_NAME, "⏺️ resumeRecording completed successfully")
|
|
232
|
+
promise.resolve(value)
|
|
176
233
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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)
|
|
180
237
|
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
val featuresMap = options["features"] as? Map<*, *>
|
|
184
|
-
val features = Features.parseFeatureOptions(featuresMap)
|
|
185
|
-
|
|
186
|
-
val recordingConfig = RecordingConfig(
|
|
187
|
-
sampleRate = audioData.sampleRate,
|
|
188
|
-
channels = audioData.channels,
|
|
189
|
-
encoding = when (audioData.bitDepth) {
|
|
190
|
-
8 -> "pcm_8bit"
|
|
191
|
-
16 -> "pcm_16bit"
|
|
192
|
-
32 -> "pcm_32bit"
|
|
193
|
-
else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
|
|
194
|
-
},
|
|
195
|
-
segmentDurationMs = segmentDurationMs,
|
|
196
|
-
features = features
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
Log.d(Constants.TAG, "extractAudioAnalysis: $recordingConfig")
|
|
200
|
-
audioProcessor.resetCumulativeAmplitudeRange()
|
|
201
|
-
|
|
202
|
-
val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
|
|
203
|
-
promise.resolve(analysisData.toDictionary())
|
|
238
|
+
})
|
|
204
239
|
} catch (e: Exception) {
|
|
205
|
-
|
|
206
|
-
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)
|
|
207
242
|
}
|
|
208
243
|
}
|
|
209
244
|
|
|
210
|
-
AsyncFunction("resumeRecording") { promise: Promise ->
|
|
211
|
-
audioRecorderManager.resumeRecording(promise)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
245
|
AsyncFunction("stopRecording") { promise: Promise ->
|
|
215
246
|
audioRecorderManager.stopRecording(promise)
|
|
216
247
|
}
|
|
@@ -228,18 +259,18 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
228
259
|
|
|
229
260
|
// Add foreground service permission for Android 14+ only if background audio is enabled
|
|
230
261
|
if (isForegroundServiceMicRequired()) {
|
|
231
|
-
|
|
262
|
+
LogUtils.d(CLASS_NAME, "Adding FOREGROUND_SERVICE_MICROPHONE permission request")
|
|
232
263
|
permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
|
|
233
264
|
}
|
|
234
265
|
|
|
235
|
-
|
|
266
|
+
LogUtils.d(CLASS_NAME, "Requesting permissions: $permissions")
|
|
236
267
|
Permissions.askForPermissionsWithPermissionsManager(
|
|
237
268
|
appContext.permissions,
|
|
238
269
|
promise,
|
|
239
270
|
*permissions.toTypedArray()
|
|
240
271
|
)
|
|
241
272
|
} catch (e: Exception) {
|
|
242
|
-
|
|
273
|
+
LogUtils.e(CLASS_NAME, "Error requesting permissions", e)
|
|
243
274
|
promise.reject("PERMISSION_ERROR", "Failed to request permissions: ${e.message}", e)
|
|
244
275
|
}
|
|
245
276
|
}
|
|
@@ -313,8 +344,8 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
313
344
|
return@AsyncFunction
|
|
314
345
|
}
|
|
315
346
|
|
|
316
|
-
|
|
317
|
-
|
|
347
|
+
LogUtils.d(CLASS_NAME, "trimAudio called with fileUri: $fileUri")
|
|
348
|
+
LogUtils.d(CLASS_NAME, "Full options: $options")
|
|
318
349
|
|
|
319
350
|
val mode = options["mode"] as? String ?: "single"
|
|
320
351
|
val startTimeMs = (options["startTimeMs"] as? Number)?.toLong()
|
|
@@ -332,7 +363,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
332
363
|
if (outputFormatMap != null) {
|
|
333
364
|
val format = outputFormatMap["format"] as? String
|
|
334
365
|
if (format != null && format != "wav" && format != "aac" && format != "opus") {
|
|
335
|
-
|
|
366
|
+
LogUtils.w(CLASS_NAME, "Requested format '$format' is not fully supported. Using 'aac' instead.")
|
|
336
367
|
// Create a new map with the corrected format
|
|
337
368
|
val newOutputFormat = HashMap<String, Any>(outputFormatMap)
|
|
338
369
|
newOutputFormat["format"] = "aac"
|
|
@@ -340,7 +371,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
340
371
|
}
|
|
341
372
|
}
|
|
342
373
|
|
|
343
|
-
|
|
374
|
+
LogUtils.d(CLASS_NAME, "Output format options: $outputFormatMap")
|
|
344
375
|
|
|
345
376
|
// Create progress listener
|
|
346
377
|
val progressListener = object : AudioTrimmer.ProgressListener {
|
|
@@ -377,10 +408,10 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
377
408
|
"durationMs" to processingTimeMs
|
|
378
409
|
)
|
|
379
410
|
|
|
380
|
-
|
|
411
|
+
LogUtils.d(CLASS_NAME, "Trim operation completed successfully in ${processingTimeMs}ms: $result")
|
|
381
412
|
promise.resolve(resultWithProcessingTime)
|
|
382
413
|
} catch (e: Exception) {
|
|
383
|
-
|
|
414
|
+
LogUtils.e(CLASS_NAME, "Error trimming audio: ${e.message}", e)
|
|
384
415
|
promise.reject("TRIM_ERROR", "Error trimming audio: ${e.message}", e)
|
|
385
416
|
}
|
|
386
417
|
}
|
|
@@ -388,45 +419,45 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
388
419
|
AsyncFunction("extractMelSpectrogram") { options: Map<String, Any>, promise: Promise ->
|
|
389
420
|
try {
|
|
390
421
|
// Log all incoming options for debugging
|
|
391
|
-
|
|
422
|
+
LogUtils.d(CLASS_NAME, "extractMelSpectrogram called with options: $options")
|
|
392
423
|
|
|
393
424
|
// Extract required parameters with detailed logging
|
|
394
425
|
val fileUri = options["fileUri"] as? String
|
|
395
|
-
|
|
426
|
+
LogUtils.d(CLASS_NAME, "fileUri: $fileUri")
|
|
396
427
|
if (fileUri == null) {
|
|
397
|
-
|
|
428
|
+
LogUtils.e(CLASS_NAME, "Missing required parameter: fileUri")
|
|
398
429
|
throw IllegalArgumentException("fileUri is required")
|
|
399
430
|
}
|
|
400
431
|
|
|
401
432
|
val windowSizeMs = options["windowSizeMs"] as? Double
|
|
402
|
-
|
|
433
|
+
LogUtils.d(CLASS_NAME, "windowSizeMs: $windowSizeMs")
|
|
403
434
|
if (windowSizeMs == null) {
|
|
404
|
-
|
|
435
|
+
LogUtils.e(CLASS_NAME, "Missing required parameter: windowSizeMs")
|
|
405
436
|
throw IllegalArgumentException("windowSizeMs is required")
|
|
406
437
|
}
|
|
407
438
|
|
|
408
439
|
val hopLengthMs = options["hopLengthMs"] as? Double
|
|
409
|
-
|
|
440
|
+
LogUtils.d(CLASS_NAME, "hopLengthMs: $hopLengthMs")
|
|
410
441
|
if (hopLengthMs == null) {
|
|
411
|
-
|
|
442
|
+
LogUtils.e(CLASS_NAME, "Missing required parameter: hopLengthMs")
|
|
412
443
|
throw IllegalArgumentException("hopLengthMs is required")
|
|
413
444
|
}
|
|
414
445
|
|
|
415
446
|
// Handle nMels which might come as Double from JavaScript
|
|
416
447
|
val nMelsValue = options["nMels"]
|
|
417
|
-
|
|
448
|
+
LogUtils.d(CLASS_NAME, "Raw nMels value: $nMelsValue (type: ${nMelsValue?.javaClass?.name})")
|
|
418
449
|
|
|
419
450
|
val nMels = when (nMelsValue) {
|
|
420
451
|
is Int -> nMelsValue
|
|
421
452
|
is Double -> nMelsValue.toInt()
|
|
422
453
|
is Number -> nMelsValue.toInt()
|
|
423
454
|
else -> {
|
|
424
|
-
|
|
455
|
+
LogUtils.e(CLASS_NAME, "Missing or invalid required parameter: nMels")
|
|
425
456
|
throw IllegalArgumentException("nMels is required and must be a number")
|
|
426
457
|
}
|
|
427
458
|
}
|
|
428
459
|
|
|
429
|
-
|
|
460
|
+
LogUtils.d(CLASS_NAME, "Converted nMels: $nMels (from ${nMelsValue?.javaClass?.name})")
|
|
430
461
|
|
|
431
462
|
// Extract optional parameters with defaults
|
|
432
463
|
val fMin = options["fMin"] as? Double ?: 0.0
|
|
@@ -441,7 +472,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
441
472
|
val startTimeMs = startTimeMsNumber?.toLong() ?: startTimeMsNumber?.toDouble()?.toLong()
|
|
442
473
|
val endTimeMs = endTimeMsNumber?.toLong() ?: endTimeMsNumber?.toDouble()?.toLong()
|
|
443
474
|
|
|
444
|
-
|
|
475
|
+
LogUtils.d(CLASS_NAME, """
|
|
445
476
|
Optional parameters:
|
|
446
477
|
- fMin: $fMin
|
|
447
478
|
- fMax: $fMax
|
|
@@ -454,7 +485,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
454
485
|
|
|
455
486
|
// Handle decoding options
|
|
456
487
|
val decodingOptions = options["decodingOptions"] as? Map<String, Any>
|
|
457
|
-
|
|
488
|
+
LogUtils.d(CLASS_NAME, "Decoding options: $decodingOptions")
|
|
458
489
|
|
|
459
490
|
val config = decodingOptions?.let {
|
|
460
491
|
val targetSampleRateValue = it["targetSampleRate"]
|
|
@@ -489,7 +520,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
489
520
|
targetBitDepth = targetBitDepth,
|
|
490
521
|
normalizeAudio = normalizeAudio
|
|
491
522
|
).also { config ->
|
|
492
|
-
|
|
523
|
+
LogUtils.d(CLASS_NAME, """
|
|
493
524
|
Using decoding config:
|
|
494
525
|
- targetSampleRate: ${config.targetSampleRate ?: "original"}
|
|
495
526
|
- targetChannels: ${config.targetChannels ?: "original"}
|
|
@@ -498,38 +529,38 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
498
529
|
""".trimIndent())
|
|
499
530
|
}
|
|
500
531
|
} ?: DecodingConfig(targetSampleRate = null, targetChannels = 1, targetBitDepth = 16).also {
|
|
501
|
-
|
|
532
|
+
LogUtils.d(CLASS_NAME, "Using default decoding config")
|
|
502
533
|
}
|
|
503
534
|
|
|
504
535
|
// Check if the audio data is too short
|
|
505
536
|
if (startTimeMs != null && endTimeMs != null) {
|
|
506
537
|
val durationMs = endTimeMs - startTimeMs
|
|
507
|
-
|
|
538
|
+
LogUtils.d(CLASS_NAME, "Audio duration for spectrogram: $durationMs ms")
|
|
508
539
|
if (durationMs < 25) { // 25ms is minimum for a single window
|
|
509
|
-
|
|
540
|
+
LogUtils.w(CLASS_NAME, "Audio duration is too short for spectrogram analysis: $durationMs ms")
|
|
510
541
|
throw IllegalArgumentException("Audio duration must be at least 25ms for spectrogram analysis")
|
|
511
542
|
}
|
|
512
543
|
}
|
|
513
544
|
|
|
514
545
|
// Load audio data with optional time range
|
|
515
|
-
|
|
546
|
+
LogUtils.d(CLASS_NAME, "Loading audio data...")
|
|
516
547
|
val audioData = when {
|
|
517
548
|
startTimeMs != null && endTimeMs != null -> {
|
|
518
|
-
|
|
549
|
+
LogUtils.d(CLASS_NAME, "Loading audio range: $startTimeMs to $endTimeMs ms")
|
|
519
550
|
audioProcessor.loadAudioRange(fileUri, startTimeMs, endTimeMs, config)
|
|
520
551
|
}
|
|
521
552
|
else -> {
|
|
522
|
-
|
|
553
|
+
LogUtils.d(CLASS_NAME, "Loading entire audio file")
|
|
523
554
|
audioProcessor.loadAudioFromAnyFormat(fileUri, config)
|
|
524
555
|
}
|
|
525
556
|
}
|
|
526
557
|
|
|
527
558
|
if (audioData == null) {
|
|
528
|
-
|
|
559
|
+
LogUtils.e(CLASS_NAME, "Failed to load audio data")
|
|
529
560
|
throw IllegalStateException("Failed to load audio data")
|
|
530
561
|
}
|
|
531
562
|
|
|
532
|
-
|
|
563
|
+
LogUtils.d(CLASS_NAME, """
|
|
533
564
|
Audio data loaded successfully:
|
|
534
565
|
- data size: ${audioData.data.size} bytes
|
|
535
566
|
- sampleRate: ${audioData.sampleRate}
|
|
@@ -540,7 +571,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
540
571
|
|
|
541
572
|
// Validate that we have enough audio data for processing
|
|
542
573
|
if (audioData.data.size == 0 || audioData.durationMs < windowSizeMs) {
|
|
543
|
-
|
|
574
|
+
LogUtils.e(CLASS_NAME, "Audio data is too short for spectrogram analysis: ${audioData.durationMs}ms, data size: ${audioData.data.size} bytes")
|
|
544
575
|
throw IllegalArgumentException(
|
|
545
576
|
"Audio data is too short for spectrogram analysis. " +
|
|
546
577
|
"Duration: ${audioData.durationMs}ms, minimum required: ${windowSizeMs}ms"
|
|
@@ -548,7 +579,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
548
579
|
}
|
|
549
580
|
|
|
550
581
|
// Compute mel-spectrogram
|
|
551
|
-
|
|
582
|
+
LogUtils.d(CLASS_NAME, "Computing mel-spectrogram...")
|
|
552
583
|
val spectrogramData = audioProcessor.extractMelSpectrogram(
|
|
553
584
|
audioData = audioData,
|
|
554
585
|
windowSizeMs = windowSizeMs.toFloat(),
|
|
@@ -561,7 +592,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
561
592
|
windowType = windowType
|
|
562
593
|
)
|
|
563
594
|
|
|
564
|
-
|
|
595
|
+
LogUtils.d(CLASS_NAME, "Mel-spectrogram computed successfully with ${spectrogramData.spectrogram.size} time steps")
|
|
565
596
|
|
|
566
597
|
// Convert to map for React Native
|
|
567
598
|
val result = mapOf(
|
|
@@ -572,11 +603,11 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
572
603
|
"durationMs" to audioData.durationMs
|
|
573
604
|
)
|
|
574
605
|
|
|
575
|
-
|
|
606
|
+
LogUtils.d(CLASS_NAME, "Returning result with ${result["timeSteps"]} time steps and $nMels mel bands")
|
|
576
607
|
promise.resolve(result)
|
|
577
608
|
} catch (e: Exception) {
|
|
578
|
-
|
|
579
|
-
|
|
609
|
+
LogUtils.e(CLASS_NAME, "Failed to extract mel-spectrogram: ${e.message}")
|
|
610
|
+
LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
|
|
580
611
|
promise.reject("SPECTROGRAM_ERROR", e.message ?: "Unknown error", e)
|
|
581
612
|
}
|
|
582
613
|
}
|
|
@@ -600,6 +631,107 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
600
631
|
promise.resolve(status)
|
|
601
632
|
}
|
|
602
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
|
+
|
|
603
735
|
AsyncFunction("extractAudioData") { options: Map<String, Any>, promise: Promise ->
|
|
604
736
|
try {
|
|
605
737
|
val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
|
|
@@ -628,7 +760,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
628
760
|
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
629
761
|
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
630
762
|
).also {
|
|
631
|
-
|
|
763
|
+
LogUtils.d(CLASS_NAME, """
|
|
632
764
|
Using decoding config:
|
|
633
765
|
- targetSampleRate: ${it.targetSampleRate ?: "original"}
|
|
634
766
|
- targetChannels: ${it.targetChannels ?: "original"}
|
|
@@ -647,7 +779,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
647
779
|
val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
|
|
648
780
|
val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
|
|
649
781
|
|
|
650
|
-
|
|
782
|
+
LogUtils.d(CLASS_NAME, """
|
|
651
783
|
Converting byte range to time range:
|
|
652
784
|
- position: $position bytes
|
|
653
785
|
- length: $length bytes
|
|
@@ -664,7 +796,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
664
796
|
)
|
|
665
797
|
} else {
|
|
666
798
|
// Must be time range due to earlier validation
|
|
667
|
-
|
|
799
|
+
LogUtils.d(CLASS_NAME, """
|
|
668
800
|
Using time range:
|
|
669
801
|
- startTimeMs: $startTimeMs
|
|
670
802
|
- endTimeMs: $endTimeMs
|
|
@@ -678,7 +810,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
678
810
|
)
|
|
679
811
|
} ?: throw IllegalStateException("Failed to load audio data")
|
|
680
812
|
|
|
681
|
-
|
|
813
|
+
LogUtils.d(CLASS_NAME, """
|
|
682
814
|
Audio data loaded successfully:
|
|
683
815
|
- data size: ${audioData.data.size} bytes
|
|
684
816
|
- sampleRate: ${audioData.sampleRate}
|
|
@@ -719,7 +851,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
719
851
|
resultMap["pcmData"] = wavData
|
|
720
852
|
resultMap["hasWavHeader"] = true
|
|
721
853
|
|
|
722
|
-
|
|
854
|
+
LogUtils.d(CLASS_NAME, "Added WAV header to PCM data, total size: ${wavData.size} bytes")
|
|
723
855
|
} else {
|
|
724
856
|
resultMap["pcmData"] = audioData.data
|
|
725
857
|
resultMap["hasWavHeader"] = false
|
|
@@ -741,7 +873,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
741
873
|
crc32.update(audioData.data)
|
|
742
874
|
resultMap["checksum"] = crc32.value.toInt()
|
|
743
875
|
|
|
744
|
-
|
|
876
|
+
LogUtils.d(CLASS_NAME, "Computed CRC32 checksum: ${crc32.value}")
|
|
745
877
|
}
|
|
746
878
|
|
|
747
879
|
if (includeNormalizedData) {
|
|
@@ -763,19 +895,25 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
763
895
|
|
|
764
896
|
promise.resolve(resultMap)
|
|
765
897
|
} catch (e: Exception) {
|
|
766
|
-
|
|
767
|
-
|
|
898
|
+
LogUtils.e(CLASS_NAME, "Failed to extract audio data: ${e.message}")
|
|
899
|
+
LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
|
|
768
900
|
promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
|
|
769
901
|
}
|
|
770
902
|
}
|
|
771
903
|
}
|
|
772
904
|
|
|
773
905
|
private fun initializeManager() {
|
|
774
|
-
val
|
|
775
|
-
val
|
|
906
|
+
val context = appContext.reactContext ?: throw IllegalStateException("React context not available")
|
|
907
|
+
val filesDir = context.filesDir
|
|
908
|
+
val permissionUtils = PermissionUtils(context)
|
|
776
909
|
val audioDataEncoder = AudioDataEncoder()
|
|
777
|
-
|
|
778
|
-
|
|
910
|
+
|
|
911
|
+
// Initialize AudioDeviceManager
|
|
912
|
+
audioDeviceManager = AudioDeviceManager(context)
|
|
913
|
+
|
|
914
|
+
// Initialize AudioRecorderManager with AudioDeviceManager integration
|
|
915
|
+
audioRecorderManager = AudioRecorderManager.initialize(
|
|
916
|
+
context,
|
|
779
917
|
filesDir,
|
|
780
918
|
permissionUtils,
|
|
781
919
|
audioDataEncoder,
|
|
@@ -783,13 +921,139 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
783
921
|
enablePhoneStateHandling,
|
|
784
922
|
enableBackgroundAudio
|
|
785
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
|
+
|
|
786
949
|
audioProcessor = AudioProcessor(filesDir)
|
|
787
950
|
}
|
|
788
|
-
|
|
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
|
+
}
|
|
789
1054
|
|
|
790
1055
|
override fun sendExpoEvent(eventName: String, params: Bundle) {
|
|
791
|
-
|
|
1056
|
+
LogUtils.d(CLASS_NAME, "Sending event: $eventName")
|
|
792
1057
|
this@ExpoAudioStreamModule.sendEvent(eventName, params)
|
|
793
1058
|
}
|
|
794
|
-
|
|
795
1059
|
}
|