@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +14 -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 +581 -255
  9. package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
  10. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +435 -158
  11. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
  12. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +14 -5
  13. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +9 -1
  14. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  15. package/build/AudioDeviceManager.d.ts +107 -0
  16. package/build/AudioDeviceManager.d.ts.map +1 -0
  17. package/build/AudioDeviceManager.js +493 -0
  18. package/build/AudioDeviceManager.js.map +1 -0
  19. package/build/AudioRecorder.provider.d.ts.map +1 -1
  20. package/build/AudioRecorder.provider.js +3 -0
  21. package/build/AudioRecorder.provider.js.map +1 -1
  22. package/build/ExpoAudioStream.types.d.ts +90 -1
  23. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  24. package/build/ExpoAudioStream.types.js +7 -1
  25. package/build/ExpoAudioStream.types.js.map +1 -1
  26. package/build/ExpoAudioStream.web.d.ts +37 -0
  27. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  28. package/build/ExpoAudioStream.web.js +399 -54
  29. package/build/ExpoAudioStream.web.js.map +1 -1
  30. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  31. package/build/ExpoAudioStreamModule.js +20 -0
  32. package/build/ExpoAudioStreamModule.js.map +1 -1
  33. package/build/WebRecorder.web.d.ts +63 -10
  34. package/build/WebRecorder.web.d.ts.map +1 -1
  35. package/build/WebRecorder.web.js +277 -68
  36. package/build/WebRecorder.web.js.map +1 -1
  37. package/build/hooks/useAudioDevices.d.ts +14 -0
  38. package/build/hooks/useAudioDevices.d.ts.map +1 -0
  39. package/build/hooks/useAudioDevices.js +151 -0
  40. package/build/hooks/useAudioDevices.js.map +1 -0
  41. package/build/index.d.ts +2 -0
  42. package/build/index.d.ts.map +1 -1
  43. package/build/index.js +4 -0
  44. package/build/index.js.map +1 -1
  45. package/build/useAudioRecorder.d.ts +1 -0
  46. package/build/useAudioRecorder.d.ts.map +1 -1
  47. package/build/useAudioRecorder.js +20 -1
  48. package/build/useAudioRecorder.js.map +1 -1
  49. package/build/utils/BlobFix.d.ts.map +1 -1
  50. package/build/utils/BlobFix.js +2 -2
  51. package/build/utils/BlobFix.js.map +1 -1
  52. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  53. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  54. package/build/workers/InlineFeaturesExtractor.web.js +27 -26
  55. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  56. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  57. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  58. package/build/workers/inlineAudioWebWorker.web.js +25 -1
  59. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  60. package/ios/AudioDeviceManager.swift +654 -0
  61. package/ios/AudioStreamManager.swift +964 -760
  62. package/ios/ExpoAudioStreamModule.swift +174 -19
  63. package/ios/Features.swift +1 -1
  64. package/ios/ISSUE_IOS.md +45 -0
  65. package/ios/Logger.swift +13 -1
  66. package/ios/RecordingSettings.swift +12 -0
  67. package/package.json +2 -2
  68. package/src/AudioAnalysis/AudioAnalysis.types.ts +2 -2
  69. package/src/AudioDeviceManager.ts +571 -0
  70. package/src/AudioRecorder.provider.tsx +3 -0
  71. package/src/ExpoAudioStream.types.ts +97 -1
  72. package/src/ExpoAudioStream.web.ts +513 -63
  73. package/src/ExpoAudioStreamModule.ts +23 -0
  74. package/src/WebRecorder.web.ts +346 -81
  75. package/src/hooks/useAudioDevices.ts +180 -0
  76. package/src/index.ts +6 -0
  77. package/src/types/crc-32.d.ts +6 -6
  78. package/src/useAudioRecorder.tsx +27 -1
  79. package/src/utils/BlobFix.ts +6 -4
  80. package/src/workers/InlineFeaturesExtractor.web.tsx +27 -26
  81. 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
- Log.d(Constants.TAG, "Phone state handling ${if (enablePhoneStateHandling) "enabled" else "disabled"} based on manifest permissions")
54
- Log.d(Constants.TAG, "Notification handling ${if (enableNotificationHandling) "enabled" else "disabled"} based on manifest permissions")
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
- 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)
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 AudioRecorderManager
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
- Log.d(Constants.TAG, "Notification permission not in manifest, disabling showNotification")
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("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
226
+ AsyncFunction("resumeRecording") { promise: Promise ->
227
+ LogUtils.d(CLASS_NAME, "⏺️ resumeRecording() called from JS layer")
100
228
  try {
101
- val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
102
-
103
- // Get time or byte range options
104
- val startTimeMs = options["startTimeMs"] as? Number
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
- else -> {
167
- Log.d(Constants.TAG, "Loading entire audio file")
168
- 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)
169
237
  }
170
- } ?: throw IllegalStateException("Failed to load audio data")
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
- Log.e(Constants.TAG, "Failed to extract audio analysis: ${e.message}", e)
195
- 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)
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
220
- Log.d(Constants.TAG, "Adding FOREGROUND_SERVICE_MICROPHONE permission request")
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
- Log.d(Constants.TAG, "Requesting permissions: $permissions")
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
- Log.e(Constants.TAG, "Error requesting permissions", e)
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
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
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
- Log.d(Constants.TAG, "trimAudio called with fileUri: $fileUri")
305
- 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")
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
- 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.")
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
- Log.d(Constants.TAG, "Output format options: $outputFormatMap")
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
- 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")
369
412
  promise.resolve(resultWithProcessingTime)
370
413
  } catch (e: Exception) {
371
- Log.e(Constants.TAG, "Error trimming audio: ${e.message}", e)
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
- Log.d(Constants.TAG, "extractMelSpectrogram called with options: $options")
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
- Log.d(Constants.TAG, "fileUri: $fileUri")
426
+ LogUtils.d(CLASS_NAME, "fileUri: $fileUri")
384
427
  if (fileUri == null) {
385
- Log.e(Constants.TAG, "Missing required parameter: fileUri")
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
- Log.d(Constants.TAG, "windowSizeMs: $windowSizeMs")
433
+ LogUtils.d(CLASS_NAME, "windowSizeMs: $windowSizeMs")
391
434
  if (windowSizeMs == null) {
392
- Log.e(Constants.TAG, "Missing required parameter: windowSizeMs")
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
- Log.d(Constants.TAG, "hopLengthMs: $hopLengthMs")
440
+ LogUtils.d(CLASS_NAME, "hopLengthMs: $hopLengthMs")
398
441
  if (hopLengthMs == null) {
399
- Log.e(Constants.TAG, "Missing required parameter: hopLengthMs")
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
- 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})")
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
- Log.e(Constants.TAG, "Missing or invalid required parameter: nMels")
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
- Log.d(Constants.TAG, "Converted nMels: $nMels (from ${nMelsValue?.javaClass?.name})")
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, "Decoding options: $decodingOptions")
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, "Using default decoding config")
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
- Log.d(Constants.TAG, "Audio duration for spectrogram: $durationMs ms")
538
+ LogUtils.d(CLASS_NAME, "Audio duration for spectrogram: $durationMs ms")
496
539
  if (durationMs < 25) { // 25ms is minimum for a single window
497
- 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")
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
- Log.d(Constants.TAG, "Loading audio data...")
546
+ LogUtils.d(CLASS_NAME, "Loading audio data...")
504
547
  val audioData = when {
505
548
  startTimeMs != null && endTimeMs != null -> {
506
- Log.d(Constants.TAG, "Loading audio range: $startTimeMs to $endTimeMs ms")
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
- Log.d(Constants.TAG, "Loading entire audio file")
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
- Log.e(Constants.TAG, "Failed to load audio data")
559
+ LogUtils.e(CLASS_NAME, "Failed to load audio data")
517
560
  throw IllegalStateException("Failed to load audio data")
518
561
  }
519
562
 
520
- Log.d(Constants.TAG, """
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
- 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")
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
- Log.d(Constants.TAG, "Computing mel-spectrogram...")
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
- 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")
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
- 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")
564
607
  promise.resolve(result)
565
608
  } catch (e: Exception) {
566
- Log.e(Constants.TAG, "Failed to extract mel-spectrogram: ${e.message}")
567
- 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()}")
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, """
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
- Log.d(Constants.TAG, """
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
- 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")
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
- Log.d(Constants.TAG, "Computed CRC32 checksum: ${crc32.value}")
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
- Log.e(Constants.TAG, "Failed to extract audio data: ${e.message}")
755
- 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()}")
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 filesDir = appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available")
763
- 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)
764
909
  val audioDataEncoder = AudioDataEncoder()
765
- audioRecorderManager = AudioRecorderManager(
766
- appContext.reactContext!!,
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
- Log.d(Constants.TAG, "Sending event: $eventName")
1056
+ LogUtils.d(CLASS_NAME, "Sending event: $eventName")
779
1057
  this@ExpoAudioStreamModule.sendEvent(eventName, params)
780
1058
  }
781
-
782
1059
  }