@siteed/expo-audio-studio 2.4.1 → 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.
Files changed (80) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +25 -0
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +22 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +1501 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +10 -5
  6. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +27 -25
  7. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +73 -71
  8. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +576 -252
  9. package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
  10. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +419 -155
  11. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
  12. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +9 -1
  13. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  14. package/build/AudioDeviceManager.d.ts +107 -0
  15. package/build/AudioDeviceManager.d.ts.map +1 -0
  16. package/build/AudioDeviceManager.js +493 -0
  17. package/build/AudioDeviceManager.js.map +1 -0
  18. package/build/AudioRecorder.provider.d.ts.map +1 -1
  19. package/build/AudioRecorder.provider.js +3 -0
  20. package/build/AudioRecorder.provider.js.map +1 -1
  21. package/build/ExpoAudioStream.types.d.ts +90 -1
  22. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  23. package/build/ExpoAudioStream.types.js +7 -1
  24. package/build/ExpoAudioStream.types.js.map +1 -1
  25. package/build/ExpoAudioStream.web.d.ts +37 -0
  26. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  27. package/build/ExpoAudioStream.web.js +399 -54
  28. package/build/ExpoAudioStream.web.js.map +1 -1
  29. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  30. package/build/ExpoAudioStreamModule.js +20 -0
  31. package/build/ExpoAudioStreamModule.js.map +1 -1
  32. package/build/WebRecorder.web.d.ts +63 -10
  33. package/build/WebRecorder.web.d.ts.map +1 -1
  34. package/build/WebRecorder.web.js +277 -68
  35. package/build/WebRecorder.web.js.map +1 -1
  36. package/build/hooks/useAudioDevices.d.ts +14 -0
  37. package/build/hooks/useAudioDevices.d.ts.map +1 -0
  38. package/build/hooks/useAudioDevices.js +151 -0
  39. package/build/hooks/useAudioDevices.js.map +1 -0
  40. package/build/index.d.ts +2 -0
  41. package/build/index.d.ts.map +1 -1
  42. package/build/index.js +4 -0
  43. package/build/index.js.map +1 -1
  44. package/build/useAudioRecorder.d.ts +1 -0
  45. package/build/useAudioRecorder.d.ts.map +1 -1
  46. package/build/useAudioRecorder.js +20 -1
  47. package/build/useAudioRecorder.js.map +1 -1
  48. package/build/utils/BlobFix.d.ts.map +1 -1
  49. package/build/utils/BlobFix.js +2 -2
  50. package/build/utils/BlobFix.js.map +1 -1
  51. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  52. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  53. package/build/workers/InlineFeaturesExtractor.web.js +27 -26
  54. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  55. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  56. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  57. package/build/workers/inlineAudioWebWorker.web.js +25 -1
  58. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  59. package/ios/AudioDeviceManager.swift +654 -0
  60. package/ios/AudioStreamManager.swift +964 -760
  61. package/ios/ExpoAudioStreamModule.swift +174 -19
  62. package/ios/Features.swift +1 -1
  63. package/ios/ISSUE_IOS.md +45 -0
  64. package/ios/Logger.swift +13 -1
  65. package/ios/RecordingSettings.swift +12 -0
  66. package/package.json +2 -2
  67. package/src/AudioAnalysis/AudioAnalysis.types.ts +2 -2
  68. package/src/AudioDeviceManager.ts +571 -0
  69. package/src/AudioRecorder.provider.tsx +3 -0
  70. package/src/ExpoAudioStream.types.ts +97 -1
  71. package/src/ExpoAudioStream.web.ts +513 -63
  72. package/src/ExpoAudioStreamModule.ts +23 -0
  73. package/src/WebRecorder.web.ts +346 -81
  74. package/src/hooks/useAudioDevices.ts +180 -0
  75. package/src/index.ts +6 -0
  76. package/src/types/crc-32.d.ts +6 -6
  77. package/src/useAudioRecorder.tsx +27 -1
  78. package/src/utils/BlobFix.ts +6 -4
  79. package/src/workers/InlineFeaturesExtractor.web.tsx +27 -26
  80. 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
- Log.d(Constants.TAG, "Phone state handling ${if (enablePhoneStateHandling) "enabled" else "disabled"} based on manifest permissions")
58
- Log.d(Constants.TAG, "Notification handling ${if (enableNotificationHandling) "enabled" else "disabled"} based on manifest permissions")
59
- Log.d(Constants.TAG, "Background audio handling ${if (enableBackgroundAudio) "enabled" else "disabled"} based on manifest permissions")
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
- Log.e(Constants.TAG, "Failed to check manifest permissions: ${e.message}", e)
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 AudioRecorderManager
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
- Log.d(Constants.TAG, "Notification permission not in manifest, disabling showNotification")
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("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
226
+ AsyncFunction("resumeRecording") { promise: Promise ->
227
+ LogUtils.d(CLASS_NAME, "⏺️ resumeRecording() called from JS layer")
111
228
  try {
112
- val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
113
-
114
- // Get time or byte range options
115
- val startTimeMs = options["startTimeMs"] as? Number
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
- else -> {
178
- Log.d(Constants.TAG, "Loading entire audio file")
179
- audioProcessor.loadAudioFromAnyFormat(fileUri, config)
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
- } ?: throw IllegalStateException("Failed to load audio data")
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
- Log.e(Constants.TAG, "Failed to extract audio analysis: ${e.message}", e)
206
- promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
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
- Log.d(Constants.TAG, "Adding FOREGROUND_SERVICE_MICROPHONE permission request")
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
- Log.d(Constants.TAG, "Requesting permissions: $permissions")
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
- Log.e(Constants.TAG, "Error requesting permissions", e)
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
- Log.d(Constants.TAG, "trimAudio called with fileUri: $fileUri")
317
- Log.d(Constants.TAG, "Full options: $options")
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
- Log.w(Constants.TAG, "Requested format '$format' is not fully supported. Using 'aac' instead.")
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
- Log.d(Constants.TAG, "Output format options: $outputFormatMap")
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
- Log.d(Constants.TAG, "Trim operation completed successfully in ${processingTimeMs}ms: $result")
411
+ LogUtils.d(CLASS_NAME, "Trim operation completed successfully in ${processingTimeMs}ms: $result")
381
412
  promise.resolve(resultWithProcessingTime)
382
413
  } catch (e: Exception) {
383
- Log.e(Constants.TAG, "Error trimming audio: ${e.message}", e)
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
- Log.d(Constants.TAG, "extractMelSpectrogram called with options: $options")
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
- Log.d(Constants.TAG, "fileUri: $fileUri")
426
+ LogUtils.d(CLASS_NAME, "fileUri: $fileUri")
396
427
  if (fileUri == null) {
397
- Log.e(Constants.TAG, "Missing required parameter: fileUri")
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
- Log.d(Constants.TAG, "windowSizeMs: $windowSizeMs")
433
+ LogUtils.d(CLASS_NAME, "windowSizeMs: $windowSizeMs")
403
434
  if (windowSizeMs == null) {
404
- Log.e(Constants.TAG, "Missing required parameter: windowSizeMs")
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
- Log.d(Constants.TAG, "hopLengthMs: $hopLengthMs")
440
+ LogUtils.d(CLASS_NAME, "hopLengthMs: $hopLengthMs")
410
441
  if (hopLengthMs == null) {
411
- Log.e(Constants.TAG, "Missing required parameter: hopLengthMs")
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
- Log.d(Constants.TAG, "Raw nMels value: $nMelsValue (type: ${nMelsValue?.javaClass?.name})")
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
- Log.e(Constants.TAG, "Missing or invalid required parameter: nMels")
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
- Log.d(Constants.TAG, "Converted nMels: $nMels (from ${nMelsValue?.javaClass?.name})")
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, "Decoding options: $decodingOptions")
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, "Using default decoding config")
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
- Log.d(Constants.TAG, "Audio duration for spectrogram: $durationMs ms")
538
+ LogUtils.d(CLASS_NAME, "Audio duration for spectrogram: $durationMs ms")
508
539
  if (durationMs < 25) { // 25ms is minimum for a single window
509
- Log.w(Constants.TAG, "Audio duration is too short for spectrogram analysis: $durationMs ms")
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
- Log.d(Constants.TAG, "Loading audio data...")
546
+ LogUtils.d(CLASS_NAME, "Loading audio data...")
516
547
  val audioData = when {
517
548
  startTimeMs != null && endTimeMs != null -> {
518
- Log.d(Constants.TAG, "Loading audio range: $startTimeMs to $endTimeMs ms")
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
- Log.d(Constants.TAG, "Loading entire audio file")
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
- Log.e(Constants.TAG, "Failed to load audio data")
559
+ LogUtils.e(CLASS_NAME, "Failed to load audio data")
529
560
  throw IllegalStateException("Failed to load audio data")
530
561
  }
531
562
 
532
- Log.d(Constants.TAG, """
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
- Log.e(Constants.TAG, "Audio data is too short for spectrogram analysis: ${audioData.durationMs}ms, data size: ${audioData.data.size} bytes")
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
- Log.d(Constants.TAG, "Computing mel-spectrogram...")
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
- Log.d(Constants.TAG, "Mel-spectrogram computed successfully with ${spectrogramData.spectrogram.size} time steps")
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
- Log.d(Constants.TAG, "Returning result with ${result["timeSteps"]} time steps and $nMels mel bands")
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
- Log.e(Constants.TAG, "Failed to extract mel-spectrogram: ${e.message}")
579
- Log.e(Constants.TAG, "Stack trace: ${e.stackTraceToString()}")
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, "Added WAV header to PCM data, total size: ${wavData.size} bytes")
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
- Log.d(Constants.TAG, "Computed CRC32 checksum: ${crc32.value}")
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
- Log.e(Constants.TAG, "Failed to extract audio data: ${e.message}")
767
- Log.e(Constants.TAG, "Stack trace: ${e.stackTraceToString()}")
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 filesDir = appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available")
775
- val permissionUtils = PermissionUtils(appContext.reactContext!!)
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
- audioRecorderManager = AudioRecorderManager(
778
- appContext.reactContext!!,
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
- Log.d(Constants.TAG, "Sending event: $eventName")
1056
+ LogUtils.d(CLASS_NAME, "Sending event: $eventName")
792
1057
  this@ExpoAudioStreamModule.sendEvent(eventName, params)
793
1058
  }
794
-
795
1059
  }