@siteed/expo-audio-studio 2.12.3 → 2.13.1

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 (51) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/android/build.gradle +11 -0
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +266 -42
  5. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +55 -1
  6. package/app.plugin.js +3 -1
  7. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  8. package/build/cjs/AudioDeviceManager.js +229 -40
  9. package/build/cjs/AudioDeviceManager.js.map +1 -1
  10. package/build/cjs/WebRecorder.web.js +1 -0
  11. package/build/cjs/WebRecorder.web.js.map +1 -1
  12. package/build/cjs/hooks/useAudioDevices.js +30 -5
  13. package/build/cjs/hooks/useAudioDevices.js.map +1 -1
  14. package/build/cjs/useAudioRecorder.js +53 -8
  15. package/build/cjs/useAudioRecorder.js.map +1 -1
  16. package/build/cjs/workers/InlineFeaturesExtractor.web.js +8 -2
  17. package/build/cjs/workers/InlineFeaturesExtractor.web.js.map +1 -1
  18. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  19. package/build/esm/AudioDeviceManager.js +229 -40
  20. package/build/esm/AudioDeviceManager.js.map +1 -1
  21. package/build/esm/WebRecorder.web.js +1 -0
  22. package/build/esm/WebRecorder.web.js.map +1 -1
  23. package/build/esm/hooks/useAudioDevices.js +31 -6
  24. package/build/esm/hooks/useAudioDevices.js.map +1 -1
  25. package/build/esm/useAudioRecorder.js +54 -9
  26. package/build/esm/useAudioRecorder.js.map +1 -1
  27. package/build/esm/workers/InlineFeaturesExtractor.web.js +8 -2
  28. package/build/esm/workers/InlineFeaturesExtractor.web.js.map +1 -1
  29. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +1 -0
  30. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  31. package/build/types/AudioDeviceManager.d.ts +82 -2
  32. package/build/types/AudioDeviceManager.d.ts.map +1 -1
  33. package/build/types/WebRecorder.web.d.ts.map +1 -1
  34. package/build/types/hooks/useAudioDevices.d.ts +1 -0
  35. package/build/types/hooks/useAudioDevices.d.ts.map +1 -1
  36. package/build/types/useAudioRecorder.d.ts.map +1 -1
  37. package/build/types/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  38. package/build/types/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  39. package/ios/AudioDeviceManager.swift +21 -9
  40. package/ios/ExpoAudioStreamModule.swift +33 -1
  41. package/package.json +7 -6
  42. package/plugin/build/index.cjs +194 -0
  43. package/plugin/build/index.d.cts +1 -0
  44. package/plugin/build/index.js +7 -6
  45. package/plugin/src/index.ts +8 -8
  46. package/src/AudioAnalysis/AudioAnalysis.types.ts +1 -0
  47. package/src/AudioDeviceManager.ts +290 -59
  48. package/src/WebRecorder.web.ts +1 -0
  49. package/src/hooks/useAudioDevices.ts +39 -6
  50. package/src/useAudioRecorder.tsx +103 -9
  51. package/src/workers/InlineFeaturesExtractor.web.tsx +8 -2
package/CHANGELOG.md CHANGED
@@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [2.13.1] - 2025-06-09
12
+ ### Changed
13
+ - feat(investigation): resolve Issue #251 - comprehensive sub-100ms audio events analysis (#270) ([4813f1e](https://github.com/deeeed/expo-audio-stream/commit/4813f1ef05f3856b58ec8fde95b7b8909feb513d))
14
+ - fix(deps): update expo-modules-core peer dependency for Expo SDK 53 compatibility ([40b946f](https://github.com/deeeed/expo-audio-stream/commit/40b946f83eecd3fdcedfe7a2cbac62f1207a4ff0))
15
+ - docs: updated docs site ([8a01a97](https://github.com/deeeed/expo-audio-stream/commit/8a01a97ebee927a2dfa0a7cb40b11329410509d2))
16
+ ## [2.13.0] - 2025-06-09
17
+ ### Changed
18
+ - feat(expo-audio-studio): enhance device detection and management system ([97ceef0](https://github.com/deeeed/expo-audio-stream/commit/97ceef003ddb8eb5246cda8a5a00ddc75bf665a0))
11
19
  ## [2.12.3] - 2025-06-07
12
20
  ### Changed
13
21
  - refactor(expo-audio-studio): adjust audio focus request timing in AudioRecorderManager ([317367c](https://github.com/deeeed/expo-audio-stream/commit/317367cb29fa09016aa73884f2f51e9cfdee1086))
@@ -304,7 +312,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
304
312
  - Feature: Audio features extraction during recording.
305
313
  - Feature: Consistent WAV PCM recording format across all platforms.
306
314
 
307
- [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.3...HEAD
315
+ [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.13.1...HEAD
316
+ [2.13.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.13.0...@siteed/expo-audio-studio@2.13.1
317
+ [2.13.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.3...@siteed/expo-audio-studio@2.13.0
308
318
  [2.12.3]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.2...@siteed/expo-audio-studio@2.12.3
309
319
  [2.12.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.1...@siteed/expo-audio-studio@2.12.2
310
320
  [2.12.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.0...@siteed/expo-audio-studio@2.12.1
@@ -32,6 +32,7 @@ buildscript {
32
32
 
33
33
  repositories {
34
34
  mavenCentral()
35
+ google()
35
36
  }
36
37
 
37
38
  dependencies {
@@ -91,12 +92,22 @@ android {
91
92
 
92
93
  repositories {
93
94
  mavenCentral()
95
+ google()
94
96
  }
95
97
 
96
98
  dependencies {
97
99
  implementation project(':expo-modules-core')
98
100
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
99
101
 
102
+ // Add AndroidX dependencies
103
+ implementation 'androidx.core:core-ktx:1.10.1'
104
+ implementation 'androidx.annotation:annotation:1.6.0'
105
+ implementation 'androidx.appcompat:appcompat:1.6.1'
106
+
107
+ // Add Kotlinx Coroutines for main code
108
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
109
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
110
+
100
111
  // Add testing dependencies
101
112
  testImplementation 'junit:junit:4.13.2'
102
113
  testImplementation 'org.jetbrains.kotlin:kotlin-test:1.8.10'
@@ -6,6 +6,14 @@
6
6
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
7
7
  <uses-permission android:name="android.permission.WAKE_LOCK" />
8
8
  <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
9
+
10
+ <!-- Bluetooth permissions for device detection -->
11
+ <uses-permission android:name="android.permission.BLUETOOTH" />
12
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
13
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
14
+
15
+ <!-- USB permission for USB device detection -->
16
+ <uses-permission android:name="android.permission.USB_PERMISSION" />
9
17
 
10
18
  <application>
11
19
  <receiver
@@ -21,6 +21,7 @@ import net.siteed.audiostream.LogUtils
21
21
  import kotlinx.coroutines.CoroutineScope
22
22
  import kotlinx.coroutines.Dispatchers
23
23
  import kotlinx.coroutines.launch
24
+ import kotlinx.coroutines.delay
24
25
 
25
26
  /**
26
27
  * Constants not available in all Android versions
@@ -62,6 +63,12 @@ class AudioDeviceManager(private val context: Context) {
62
63
  // Delegate for handling device disconnection
63
64
  var delegate: AudioDeviceManagerDelegate? = null
64
65
 
66
+ // Simple callback for device connections
67
+ var onDeviceConnected: ((String) -> Unit)? = null
68
+
69
+ // Simple callback for device disconnections
70
+ var onDeviceDisconnected: ((String) -> Unit)? = null
71
+
65
72
  // Audio manager for accessing device information
66
73
  private val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
67
74
 
@@ -1113,6 +1120,9 @@ class AudioDeviceManager(private val context: Context) {
1113
1120
  addAction(UsbManager.ACTION_USB_ACCESSORY_ATTACHED)
1114
1121
  addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED)
1115
1122
  }
1123
+
1124
+ // Bluetooth SCO state changes - to detect when microphone becomes available
1125
+ addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
1116
1126
  }
1117
1127
 
1118
1128
  deviceReceiver = object : BroadcastReceiver() {
@@ -1153,6 +1163,28 @@ class AudioDeviceManager(private val context: Context) {
1153
1163
  }
1154
1164
  }
1155
1165
  }
1166
+ } else if (state == 1) { // Plugged in
1167
+ LogUtils.d(CLASS_NAME, "Wired headset connected: $name")
1168
+
1169
+ // For M+ find the actual new device
1170
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1171
+ // Find the newly connected wired device
1172
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1173
+ val wiredDevice = audioDevices.firstOrNull {
1174
+ (it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
1175
+ it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) &&
1176
+ getDeviceName(it).contains(name, ignoreCase = true)
1177
+ }
1178
+
1179
+ if (wiredDevice != null) {
1180
+ val connectedDeviceId = wiredDevice.id.toString()
1181
+ LogUtils.d(CLASS_NAME, "Found connected wired device: $name (ID: $connectedDeviceId)")
1182
+ handleDeviceConnection(connectedDeviceId)
1183
+ }
1184
+ } else {
1185
+ // Legacy handling for older Android
1186
+ handleDeviceConnection("1")
1187
+ }
1156
1188
  }
1157
1189
  }
1158
1190
 
@@ -1184,6 +1216,59 @@ class AudioDeviceManager(private val context: Context) {
1184
1216
  }
1185
1217
  }
1186
1218
 
1219
+ BluetoothDevice.ACTION_ACL_CONNECTED -> {
1220
+ val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1221
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
1222
+ } else {
1223
+ @Suppress("DEPRECATION")
1224
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
1225
+ }
1226
+
1227
+ if (device != null) {
1228
+ LogUtils.d(CLASS_NAME, "Bluetooth device connected: ${device.name}")
1229
+
1230
+ // For M+ find the actual new device
1231
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1232
+ val actualDevice = findBluetoothDevice(device)
1233
+ if (actualDevice != null) {
1234
+ val connectedDeviceId = actualDevice.id.toString()
1235
+ LogUtils.d(CLASS_NAME, "Found connected Bluetooth device: ${device.name} (ID: $connectedDeviceId)")
1236
+ handleDeviceConnection(connectedDeviceId)
1237
+ } else {
1238
+ LogUtils.d(CLASS_NAME, "Bluetooth device ${device.name} connected but not found in audio device list - attempting to activate SCO")
1239
+
1240
+ // Try to activate Bluetooth SCO to make microphone available
1241
+ if (!audioManager.isBluetoothScoOn) {
1242
+ LogUtils.d(CLASS_NAME, "Starting Bluetooth SCO to activate microphone for ${device.name}")
1243
+ audioManager.startBluetoothSco()
1244
+
1245
+ // Give SCO time to activate, then check again
1246
+ coroutineScope.launch {
1247
+ delay(2000) // Wait 2 seconds
1248
+
1249
+ val scoDevice = findBluetoothDevice(device)
1250
+ if (scoDevice != null) {
1251
+ val activatedDeviceId = scoDevice.id.toString()
1252
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO activated for device: ${device.name} (ID: $activatedDeviceId)")
1253
+ handleDeviceConnection(activatedDeviceId)
1254
+ } else {
1255
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO didn't activate microphone for ${device.name}")
1256
+ // Send generic connection event anyway
1257
+ handleDeviceConnection("bluetooth:${device.address}")
1258
+ }
1259
+ }
1260
+ } else {
1261
+ // SCO already on, send generic event
1262
+ handleDeviceConnection("bluetooth:${device.address}")
1263
+ }
1264
+ }
1265
+ } else {
1266
+ // Legacy handling for older Android
1267
+ handleDeviceConnection("2")
1268
+ }
1269
+ }
1270
+ }
1271
+
1187
1272
  BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
1188
1273
  val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1189
1274
  intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
@@ -1213,42 +1298,117 @@ class AudioDeviceManager(private val context: Context) {
1213
1298
  val state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1)
1214
1299
  logAudioState()
1215
1300
 
1216
- // Only handle disconnect events and only for HEADSET profile (relevant for audio)
1217
- if (state == BluetoothAdapter.STATE_DISCONNECTED) {
1218
- val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1219
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
1220
- } else {
1221
- @Suppress("DEPRECATION")
1222
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
1223
- }
1224
-
1225
- if (device != null) {
1226
- deviceId = "2" // Legacy ID for bluetooth
1227
- deviceName = device.name
1228
- deviceType = DEVICE_TYPE_BLUETOOTH
1301
+ when (state) {
1302
+ BluetoothAdapter.STATE_CONNECTED -> {
1303
+ val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1304
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
1305
+ } else {
1306
+ @Suppress("DEPRECATION")
1307
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
1308
+ }
1229
1309
 
1230
- // For M+ get the actual ID
1231
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1232
- val actualDevice = findBluetoothDevice(device)
1233
- if (actualDevice != null) {
1234
- deviceId = actualDevice.id.toString()
1310
+ if (device != null) {
1311
+ LogUtils.d(CLASS_NAME, "Bluetooth profile connected: ${device.name}")
1312
+
1313
+ // For M+ find the actual new device
1314
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1315
+ val actualDevice = findBluetoothDevice(device)
1316
+ if (actualDevice != null) {
1317
+ val connectedDeviceId = actualDevice.id.toString()
1318
+ LogUtils.d(CLASS_NAME, "Found connected Bluetooth profile device: ${device.name} (ID: $connectedDeviceId)")
1319
+ handleDeviceConnection(connectedDeviceId)
1320
+ } else {
1321
+ LogUtils.d(CLASS_NAME, "Bluetooth profile ${device.name} connected but not found in audio device list - attempting to activate SCO")
1322
+
1323
+ // Try to activate Bluetooth SCO to make microphone available
1324
+ if (!audioManager.isBluetoothScoOn) {
1325
+ LogUtils.d(CLASS_NAME, "Starting Bluetooth SCO to activate microphone for ${device.name}")
1326
+ audioManager.startBluetoothSco()
1327
+
1328
+ // Give SCO time to activate, then check again
1329
+ coroutineScope.launch {
1330
+ delay(2000) // Wait 2 seconds
1331
+
1332
+ val scoDevice = findBluetoothDevice(device)
1333
+ if (scoDevice != null) {
1334
+ val activatedDeviceId = scoDevice.id.toString()
1335
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO activated for device: ${device.name} (ID: $activatedDeviceId)")
1336
+ handleDeviceConnection(activatedDeviceId)
1337
+ } else {
1338
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO didn't activate microphone for ${device.name}")
1339
+ // Send generic connection event anyway
1340
+ handleDeviceConnection("bluetooth:${device.address}")
1341
+ }
1342
+ }
1343
+ } else {
1344
+ // SCO already on, send generic event
1345
+ handleDeviceConnection("bluetooth:${device.address}")
1346
+ }
1347
+ }
1348
+ } else {
1349
+ // Legacy handling for older Android
1350
+ handleDeviceConnection("2")
1235
1351
  }
1236
1352
  }
1353
+ }
1354
+
1355
+ BluetoothAdapter.STATE_DISCONNECTED -> {
1356
+ val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1357
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
1358
+ } else {
1359
+ @Suppress("DEPRECATION")
1360
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
1361
+ }
1237
1362
 
1238
- LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected: ${device.name}, using ID: $deviceId")
1239
- }
1240
- // No device info, check if our last device was bluetooth
1241
- else if (lastSelectedDeviceId != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1242
- val lastDeviceInfo = findDeviceById(lastSelectedDeviceId!!)
1243
-
1244
- if (lastDeviceInfo?.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
1245
- deviceId = lastSelectedDeviceId
1246
- LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected, using last selected device ID: $deviceId")
1363
+ if (device != null) {
1364
+ deviceId = "2" // Legacy ID for bluetooth
1365
+ deviceName = device.name
1366
+ deviceType = DEVICE_TYPE_BLUETOOTH
1367
+
1368
+ // For M+ get the actual ID
1369
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1370
+ val actualDevice = findBluetoothDevice(device)
1371
+ if (actualDevice != null) {
1372
+ deviceId = actualDevice.id.toString()
1373
+ }
1374
+ }
1375
+
1376
+ LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected: ${device.name}, using ID: $deviceId")
1377
+ }
1378
+ // No device info, check if our last device was bluetooth
1379
+ else if (lastSelectedDeviceId != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1380
+ val lastDeviceInfo = findDeviceById(lastSelectedDeviceId!!)
1381
+
1382
+ if (lastDeviceInfo?.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
1383
+ deviceId = lastSelectedDeviceId
1384
+ LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected, using last selected device ID: $deviceId")
1385
+ }
1247
1386
  }
1248
1387
  }
1249
1388
  }
1250
1389
  }
1251
1390
 
1391
+ UsbManager.ACTION_USB_DEVICE_ATTACHED, UsbManager.ACTION_USB_ACCESSORY_ATTACHED -> {
1392
+ LogUtils.d(CLASS_NAME, "USB device attached")
1393
+
1394
+ // For M+ find newly connected USB audio devices
1395
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1396
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1397
+ val usbDevice = audioDevices.firstOrNull {
1398
+ it.type == AudioDeviceInfo.TYPE_USB_DEVICE ||
1399
+ it.type == AudioDeviceInfo.TYPE_USB_HEADSET ||
1400
+ it.type == AudioDeviceInfo.TYPE_USB_ACCESSORY
1401
+ }
1402
+
1403
+ if (usbDevice != null) {
1404
+ val connectedDeviceId = usbDevice.id.toString()
1405
+ val deviceName = getDeviceName(usbDevice)
1406
+ LogUtils.d(CLASS_NAME, "Found connected USB audio device: $deviceName (ID: $connectedDeviceId)")
1407
+ handleDeviceConnection(connectedDeviceId)
1408
+ }
1409
+ }
1410
+ }
1411
+
1252
1412
  UsbManager.ACTION_USB_DEVICE_DETACHED, UsbManager.ACTION_USB_ACCESSORY_DETACHED -> {
1253
1413
  LogUtils.d(CLASS_NAME, "USB device detached")
1254
1414
 
@@ -1266,28 +1426,74 @@ class AudioDeviceManager(private val context: Context) {
1266
1426
  }
1267
1427
  }
1268
1428
  }
1429
+
1430
+ AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
1431
+ val scoState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)
1432
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO state changed: $scoState")
1433
+
1434
+ when (scoState) {
1435
+ AudioManager.SCO_AUDIO_STATE_CONNECTED -> {
1436
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO connected - microphone available")
1437
+
1438
+ // Check if any new Bluetooth SCO devices appeared
1439
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1440
+ val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1441
+ val bluetoothScoDevices = audioDevices.filter {
1442
+ it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
1443
+ }
1444
+
1445
+ if (bluetoothScoDevices.isNotEmpty()) {
1446
+ // Found Bluetooth SCO device(s), notify about the first one
1447
+ val scoDevice = bluetoothScoDevices.first()
1448
+ val scoDeviceId = scoDevice.id.toString()
1449
+ val scoDeviceName = getDeviceName(scoDevice)
1450
+ LogUtils.d(CLASS_NAME, "Found Bluetooth SCO device: $scoDeviceName (ID: $scoDeviceId)")
1451
+ handleDeviceConnection(scoDeviceId)
1452
+ }
1453
+ }
1454
+ }
1455
+
1456
+ AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> {
1457
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO disconnected - microphone no longer available")
1458
+ // Note: Device disconnection will be handled by other broadcasts
1459
+ }
1460
+
1461
+ AudioManager.SCO_AUDIO_STATE_CONNECTING -> {
1462
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO connecting...")
1463
+ }
1464
+
1465
+ else -> {
1466
+ LogUtils.d(CLASS_NAME, "Bluetooth SCO state: $scoState")
1467
+ }
1468
+ }
1469
+ }
1269
1470
  }
1270
1471
 
1271
- // If this is the currently selected device, notify delegate
1272
- if (deviceId != null && deviceId == lastSelectedDeviceId) {
1273
- LogUtils.d(CLASS_NAME, "Currently selected device disconnected: $deviceId")
1472
+ // Handle any device disconnection - send events to React Native for device list updates
1473
+ if (deviceId != null) {
1474
+ LogUtils.d(CLASS_NAME, "Device disconnected: $deviceId (selected: ${deviceId == lastSelectedDeviceId})")
1475
+ if (deviceName != null) {
1476
+ LogUtils.d(CLASS_NAME, "Device name: $deviceName, type: $deviceType")
1477
+ }
1478
+
1274
1479
  // Log the disconnection for debugging
1275
1480
  logDeviceDisconnection(deviceId, action ?: "unknown")
1276
1481
 
1277
- // Launch a coroutine to call the suspend function
1278
- coroutineScope.launch {
1279
- try {
1280
- handleDeviceDisconnection(deviceId)
1281
- } catch (e: Exception) {
1282
- LogUtils.e(CLASS_NAME, "Error handling device disconnection: ${e.message}", e)
1482
+ // Send device disconnection event to React Native (for UI updates)
1483
+ handleDeviceDisconnectionEvent(deviceId)
1484
+
1485
+ // If this was the currently selected device, also notify delegate for recording interruption
1486
+ if (deviceId == lastSelectedDeviceId) {
1487
+ LogUtils.d(CLASS_NAME, "Currently selected device disconnected - notifying delegate: $deviceId")
1488
+ // Launch a coroutine to call the suspend function
1489
+ coroutineScope.launch {
1490
+ try {
1491
+ handleDeviceDisconnection(deviceId)
1492
+ } catch (e: Exception) {
1493
+ LogUtils.e(CLASS_NAME, "Error handling device disconnection: ${e.message}", e)
1494
+ }
1283
1495
  }
1284
1496
  }
1285
- } else if (deviceId != null) {
1286
- // Even if not our current device, log for debugging
1287
- LogUtils.d(CLASS_NAME, "Device disconnected but not currently selected: $deviceId")
1288
- if (deviceName != null) {
1289
- LogUtils.d(CLASS_NAME, "Device name: $deviceName, type: $deviceType")
1290
- }
1291
1497
  }
1292
1498
 
1293
1499
  // Force refresh the device list
@@ -1498,4 +1704,22 @@ class AudioDeviceManager(private val context: Context) {
1498
1704
  LogUtils.d(CLASS_NAME, "Device disconnected: $deviceId. Pausing recording.")
1499
1705
  delegate?.onDeviceDisconnected(deviceId)
1500
1706
  }
1707
+
1708
+ /**
1709
+ * Handles audio device connection
1710
+ */
1711
+ private fun handleDeviceConnection(deviceId: String) {
1712
+ LogUtils.d(CLASS_NAME, "Device connected: $deviceId")
1713
+ onDeviceConnected?.invoke(deviceId)
1714
+ }
1715
+
1716
+ /**
1717
+ * Handles audio device disconnection (for React Native events)
1718
+ */
1719
+ private fun handleDeviceDisconnectionEvent(deviceId: String) {
1720
+ LogUtils.d(CLASS_NAME, "Device disconnected: $deviceId")
1721
+ onDeviceDisconnected?.invoke(deviceId)
1722
+ }
1723
+
1724
+
1501
1725
  }
@@ -7,6 +7,7 @@ import android.os.Bundle
7
7
  import android.util.Log
8
8
  import android.content.pm.PackageManager
9
9
  import androidx.annotation.RequiresApi
10
+ import androidx.core.content.ContextCompat
10
11
  import androidx.core.os.bundleOf
11
12
  import expo.modules.kotlin.Promise
12
13
  import expo.modules.kotlin.modules.Module
@@ -30,6 +31,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
30
31
  private var enablePhoneStateHandling: Boolean = false // Default to false until we check manifest
31
32
  private var enableNotificationHandling: Boolean = false // Default to false until we check manifest
32
33
  private var enableBackgroundAudio: Boolean = false // Default to false until we check manifest
34
+ private var enableDeviceDetection: Boolean = false // Default to false until we check manifest
33
35
  private val coroutineScope = CoroutineScope(Dispatchers.Main)
34
36
 
35
37
  private val audioFileHandler by lazy {
@@ -65,14 +67,19 @@ class ExpoAudioStreamModule : Module(), EventSender {
65
67
  // Check if background audio is enabled by looking for FOREGROUND_SERVICE_MICROPHONE permission
66
68
  enableBackgroundAudio = packageInfo.requestedPermissions?.contains(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE) ?: false
67
69
 
70
+ // Check if device detection is enabled by looking for BLUETOOTH_CONNECT permission
71
+ enableDeviceDetection = packageInfo.requestedPermissions?.contains(Manifest.permission.BLUETOOTH_CONNECT) ?: false
72
+
68
73
  LogUtils.d(CLASS_NAME, "Phone state handling ${if (enablePhoneStateHandling) "enabled" else "disabled"} based on manifest permissions")
69
74
  LogUtils.d(CLASS_NAME, "Notification handling ${if (enableNotificationHandling) "enabled" else "disabled"} based on manifest permissions")
70
75
  LogUtils.d(CLASS_NAME, "Background audio handling ${if (enableBackgroundAudio) "enabled" else "disabled"} based on manifest permissions")
76
+ LogUtils.d(CLASS_NAME, "Device detection ${if (enableDeviceDetection) "enabled" else "disabled"} based on manifest permissions")
71
77
  } catch (e: Exception) {
72
78
  LogUtils.e(CLASS_NAME, "Failed to check manifest permissions: ${e.message}", e)
73
79
  enablePhoneStateHandling = false
74
80
  enableNotificationHandling = false
75
81
  enableBackgroundAudio = false
82
+ enableDeviceDetection = false
76
83
  }
77
84
 
78
85
  Events(
@@ -91,6 +98,11 @@ class ExpoAudioStreamModule : Module(), EventSender {
91
98
  return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && enableBackgroundAudio
92
99
  }
93
100
 
101
+ // Helper function to check if device detection is enabled
102
+ fun isDeviceDetectionEnabled(): Boolean {
103
+ return enableDeviceDetection
104
+ }
105
+
94
106
  // Add device-related functions to the module
95
107
 
96
108
  // Gets available audio input devices with an optional refresh parameter
@@ -169,6 +181,8 @@ class ExpoAudioStreamModule : Module(), EventSender {
169
181
  return@Function mapOf("success" to success)
170
182
  }
171
183
 
184
+
185
+
172
186
  AsyncFunction("prepareRecording") { options: Map<String, Any?>, promise: Promise ->
173
187
  try {
174
188
  // If notifications are requested but permission not in manifest, modify options
@@ -263,6 +277,15 @@ class ExpoAudioStreamModule : Module(), EventSender {
263
277
  permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
264
278
  }
265
279
 
280
+ // Add device detection permissions if device detection is enabled
281
+ if (isDeviceDetectionEnabled()) {
282
+ // BLUETOOTH_CONNECT is needed on Android 12+ to access device names/addresses
283
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
284
+ permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
285
+ LogUtils.d(CLASS_NAME, "Adding BLUETOOTH_CONNECT permission request for device detection")
286
+ }
287
+ }
288
+
266
289
  LogUtils.d(CLASS_NAME, "Requesting permissions: $permissions")
267
290
  Permissions.askForPermissionsWithPermissionsManager(
268
291
  appContext.permissions,
@@ -290,6 +313,14 @@ class ExpoAudioStreamModule : Module(), EventSender {
290
313
  permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
291
314
  }
292
315
 
316
+ // Add device detection permissions if enabled
317
+ if (isDeviceDetectionEnabled()) {
318
+ // BLUETOOTH_CONNECT is needed on Android 12+ to access device names/addresses
319
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
320
+ permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
321
+ }
322
+ }
323
+
293
324
  Permissions.getPermissionsWithPermissionsManager(
294
325
  appContext.permissions,
295
326
  promise,
@@ -909,7 +940,10 @@ class ExpoAudioStreamModule : Module(), EventSender {
909
940
  val audioDataEncoder = AudioDataEncoder()
910
941
 
911
942
  // Initialize AudioDeviceManager
943
+ LogUtils.d(CLASS_NAME, "🔧 Initializing AudioDeviceManager...")
944
+ LogUtils.d(CLASS_NAME, "🔧 Device detection enabled: $enableDeviceDetection")
912
945
  audioDeviceManager = AudioDeviceManager(context)
946
+ LogUtils.d(CLASS_NAME, "🔧 AudioDeviceManager initialized")
913
947
 
914
948
  // Initialize AudioRecorderManager with AudioDeviceManager integration
915
949
  audioRecorderManager = AudioRecorderManager.initialize(
@@ -936,7 +970,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
936
970
 
937
971
  // Notify JS about the disconnection
938
972
  sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
939
- "reason" to "deviceDisconnected",
973
+ "type" to "deviceDisconnected",
940
974
  "deviceId" to deviceId
941
975
  ))
942
976
  } catch (e: Exception) {
@@ -946,6 +980,26 @@ class ExpoAudioStreamModule : Module(), EventSender {
946
980
  }
947
981
  }
948
982
 
983
+ // Set up connection callback
984
+ audioDeviceManager.onDeviceConnected = { deviceId ->
985
+ LogUtils.d(CLASS_NAME, "📱 Device connected: $deviceId")
986
+ // Notify JS about the connection
987
+ sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
988
+ "type" to "deviceConnected",
989
+ "deviceId" to deviceId
990
+ ))
991
+ }
992
+
993
+ // Set up disconnection callback
994
+ audioDeviceManager.onDeviceDisconnected = { deviceId ->
995
+ LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
996
+ // Notify JS about the disconnection
997
+ sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
998
+ "type" to "deviceDisconnected",
999
+ "deviceId" to deviceId
1000
+ ))
1001
+ }
1002
+
949
1003
  audioProcessor = AudioProcessor(filesDir)
950
1004
  }
951
1005
 
package/app.plugin.js CHANGED
@@ -1 +1,3 @@
1
- module.exports = require('./plugin/build')
1
+ // Plugin for app.json usage (regular Node.js context)
2
+ const plugin = require('./plugin/build/index.js')
3
+ module.exports = plugin.default || plugin
@@ -1 +1 @@
1
- {"version":3,"file":"AudioAnalysis.types.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/AudioAnalysis.types.ts"],"names":[],"mappings":";AAAA,sEAAsE","sourcesContent":["// packages/expo-audio-stream/src/AudioAnalysis/AudioAnalysis.types.ts\n\nimport { BitDepth, ConsoleLike } from '../ExpoAudioStream.types'\n\n/**\n * Represents the configuration for decoding audio data.\n */\nexport interface DecodingConfig {\n /** Target sample rate for decoded audio (Android and Web) */\n targetSampleRate?: number\n /** Target number of channels (Android and Web) */\n targetChannels?: number\n /** Target bit depth (Android and Web) */\n targetBitDepth?: BitDepth\n /** Whether to normalize audio levels (Android and Web) */\n normalizeAudio?: boolean\n}\n\n/**\n * Represents speech-related features extracted from audio.\n */\nexport interface SpeechFeatures {\n isActive: boolean // Whether speech is detected in this segment\n speakerId?: number // Optional speaker identification\n // Could add more speech-related features here like:\n // confidence: number\n // language?: string\n // sentiment?: number\n // etc.\n}\n\n/**\n * Represents various audio features extracted from an audio signal.\n */\nexport interface AudioFeatures {\n energy?: number // The infinite integral of the squared signal, representing the overall energy of the audio.\n mfcc?: number[] // Mel-frequency cepstral coefficients, describing the short-term power spectrum of a sound.\n rms?: number // Root mean square value, indicating the amplitude of the audio signal.\n minAmplitude?: number // Minimum amplitude value in the audio signal.\n maxAmplitude?: number // Maximum amplitude value in the audio signal.\n zcr?: number // Zero-crossing rate, indicating the rate at which the signal changes sign.\n spectralCentroid?: number // The center of mass of the spectrum, indicating the brightness of the sound.\n spectralFlatness?: number // Measure of the flatness of the spectrum, indicating how noise-like the signal is.\n spectralRolloff?: number // The frequency below which a specified percentage (usually 85%) of the total spectral energy lies.\n spectralBandwidth?: number // The width of the spectrum, indicating the range of frequencies present.\n chromagram?: number[] // Chromagram, representing the 12 different pitch classes of the audio.\n tempo?: number // Estimated tempo of the audio signal, measured in beats per minute (BPM).\n hnr?: number // Harmonics-to-noise ratio, indicating the proportion of harmonics to noise in the audio signal.\n melSpectrogram?: number[] // Mel-scaled spectrogram representation of the audio.\n spectralContrast?: number[] // Spectral contrast features representing the difference between peaks and valleys.\n tonnetz?: number[] // Tonal network features representing harmonic relationships.\n pitch?: number // Pitch of the audio signal, measured in Hertz (Hz).\n crc32?: number // crc32 checksum of the audio signal, used to verify the integrity of the audio.\n}\n\n/**\n * Options to specify which audio features to extract.\n * Note: Advanced features (spectral features, chromagram, pitch, etc.) are experimental,\n * especially during live recording, due to high processing requirements.\n */\nexport interface AudioFeaturesOptions {\n // Basic features - well optimized\n energy?: boolean\n rms?: boolean\n zcr?: boolean\n\n // Advanced features - experimental, may impact performance in live recording\n mfcc?: boolean\n spectralCentroid?: boolean\n spectralFlatness?: boolean\n spectralRolloff?: boolean\n spectralBandwidth?: boolean\n chromagram?: boolean\n tempo?: boolean\n hnr?: boolean\n melSpectrogram?: boolean\n spectralContrast?: boolean\n tonnetz?: boolean\n pitch?: boolean\n\n // Utility\n crc32?: boolean\n}\n\n/**\n * Represents a single data point in the audio analysis.\n */\nexport interface DataPoint {\n id: number\n amplitude: number // Peak amplitude for the segment\n rms: number // Root mean square value\n dB: number // dBFS (decibels relative to full scale) computed from RMS value\n silent: boolean // Always computed\n features?: AudioFeatures\n speech?: SpeechFeatures\n startTime?: number\n endTime?: number\n // start / end position in bytes\n startPosition?: number\n endPosition?: number\n // number of audio samples for this point (samples size depends on bit depth)\n samples?: number\n}\n\n/**\n * Represents the complete data from the audio analysis.\n */\nexport interface AudioAnalysis {\n segmentDurationMs: number // Duration of each segment in milliseconds\n durationMs: number // Duration of the audio in milliseconds\n bitDepth: number // Bit depth of the audio\n samples: number // Size of the audio in bytes\n numberOfChannels: number // Number of audio channels\n sampleRate: number // Sample rate of the audio\n dataPoints: DataPoint[] // Array of data points from the analysis.\n amplitudeRange: {\n min: number\n max: number\n }\n rmsRange: {\n min: number\n max: number\n }\n // TODO: speaker changes into a broader speech analysis section\n speechAnalysis?: {\n speakerChanges: {\n timestamp: number\n speakerId: number\n }[]\n // Could add more speech analysis data here like:\n // dominantSpeaker?: number\n // totalSpeechDuration?: number\n // speakerStats?: { [speakerId: number]: { duration: number, segments: number } }\n }\n}\n\n/**\n * Options for specifying a time range within an audio file.\n */\nexport interface AudioRangeOptions {\n /** Start time in milliseconds */\n startTimeMs?: number\n /** End time in milliseconds */\n endTimeMs?: number\n}\n\n/**\n * Options for generating a quick preview of audio waveform.\n * This is optimized for UI rendering with a specified number of points.\n */\nexport interface PreviewOptions extends AudioRangeOptions {\n /** URI of the audio file to analyze */\n fileUri: string\n /**\n * Total number of points to generate for the preview.\n * @default 100\n */\n numberOfPoints?: number\n /**\n * Optional logger for debugging.\n */\n logger?: ConsoleLike\n /**\n * Optional configuration for decoding the audio file.\n * Defaults to:\n * - targetSampleRate: undefined (keep original)\n * - targetChannels: undefined (keep original)\n * - targetBitDepth: 16\n * - normalizeAudio: false\n */\n decodingOptions?: DecodingConfig\n}\n\n/**\n * Options for mel-spectrogram extraction\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions.\n */\nexport interface ExtractMelSpectrogramOptions {\n fileUri?: string // Path to audio file\n arrayBuffer?: ArrayBuffer // Raw audio buffer\n windowSizeMs: number // Window size in ms (e.g., 25)\n hopLengthMs: number // Hop length in ms (e.g., 10)\n nMels: number // Number of mel filters (e.g., 60)\n fMin?: number // Min frequency (default: 0)\n fMax?: number // Max frequency (default: sampleRate / 2)\n windowType?: 'hann' | 'hamming' // Window function (default: 'hann')\n normalize?: boolean // Mean normalization (default: false)\n logScale?: boolean // Log scaling of mel energies (default: true)\n decodingOptions?: DecodingConfig // Audio decoding settings\n startTimeMs?: number // Optional start time\n endTimeMs?: number // Optional end time\n logger?: ConsoleLike\n}\n\n/**\n * Return type for mel spectrogram extraction\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions.\n */\nexport interface MelSpectrogram {\n spectrogram: number[][] // 2D array [time][mel]\n sampleRate: number // Audio sample rate\n nMels: number // Number of mel filters\n timeSteps: number // Number of time frames\n durationMs: number // Audio duration in ms\n}\n"]}
1
+ {"version":3,"file":"AudioAnalysis.types.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/AudioAnalysis.types.ts"],"names":[],"mappings":";AAAA,sEAAsE","sourcesContent":["// packages/expo-audio-stream/src/AudioAnalysis/AudioAnalysis.types.ts\n\nimport { BitDepth, ConsoleLike } from '../ExpoAudioStream.types'\n\n/**\n * Represents the configuration for decoding audio data.\n */\nexport interface DecodingConfig {\n /** Target sample rate for decoded audio (Android and Web) */\n targetSampleRate?: number\n /** Target number of channels (Android and Web) */\n targetChannels?: number\n /** Target bit depth (Android and Web) */\n targetBitDepth?: BitDepth\n /** Whether to normalize audio levels (Android and Web) */\n normalizeAudio?: boolean\n}\n\n/**\n * Represents speech-related features extracted from audio.\n */\nexport interface SpeechFeatures {\n isActive: boolean // Whether speech is detected in this segment\n speakerId?: number // Optional speaker identification\n // Could add more speech-related features here like:\n // confidence: number\n // language?: string\n // sentiment?: number\n // etc.\n}\n\n/**\n * Represents various audio features extracted from an audio signal.\n */\nexport interface AudioFeatures {\n energy?: number // The infinite integral of the squared signal, representing the overall energy of the audio.\n mfcc?: number[] // Mel-frequency cepstral coefficients, describing the short-term power spectrum of a sound.\n rms?: number // Root mean square value, indicating the amplitude of the audio signal.\n minAmplitude?: number // Minimum amplitude value in the audio signal.\n maxAmplitude?: number // Maximum amplitude value in the audio signal.\n zcr?: number // Zero-crossing rate, indicating the rate at which the signal changes sign.\n spectralCentroid?: number // The center of mass of the spectrum, indicating the brightness of the sound.\n spectralFlatness?: number // Measure of the flatness of the spectrum, indicating how noise-like the signal is.\n spectralRolloff?: number // The frequency below which a specified percentage (usually 85%) of the total spectral energy lies.\n spectralBandwidth?: number // The width of the spectrum, indicating the range of frequencies present.\n chromagram?: number[] // Chromagram, representing the 12 different pitch classes of the audio.\n tempo?: number // Estimated tempo of the audio signal, measured in beats per minute (BPM).\n hnr?: number // Harmonics-to-noise ratio, indicating the proportion of harmonics to noise in the audio signal.\n melSpectrogram?: number[] // Mel-scaled spectrogram representation of the audio.\n spectralContrast?: number[] // Spectral contrast features representing the difference between peaks and valleys.\n tonnetz?: number[] // Tonal network features representing harmonic relationships.\n pitch?: number // Pitch of the audio signal, measured in Hertz (Hz).\n crc32?: number // crc32 checksum of the audio signal, used to verify the integrity of the audio.\n}\n\n/**\n * Options to specify which audio features to extract.\n * Note: Advanced features (spectral features, chromagram, pitch, etc.) are experimental,\n * especially during live recording, due to high processing requirements.\n */\nexport interface AudioFeaturesOptions {\n // Basic features - well optimized\n energy?: boolean\n rms?: boolean\n zcr?: boolean\n\n // Advanced features - experimental, may impact performance in live recording\n mfcc?: boolean\n spectralCentroid?: boolean\n spectralFlatness?: boolean\n spectralRolloff?: boolean\n spectralBandwidth?: boolean\n chromagram?: boolean\n tempo?: boolean\n hnr?: boolean\n melSpectrogram?: boolean\n spectralContrast?: boolean\n tonnetz?: boolean\n pitch?: boolean\n\n // Utility\n crc32?: boolean\n}\n\n/**\n * Represents a single data point in the audio analysis.\n */\nexport interface DataPoint {\n id: number\n amplitude: number // Peak amplitude for the segment\n rms: number // Root mean square value\n dB: number // dBFS (decibels relative to full scale) computed from RMS value\n silent: boolean // Always computed\n features?: AudioFeatures\n speech?: SpeechFeatures\n startTime?: number\n endTime?: number\n // start / end position in bytes\n startPosition?: number\n endPosition?: number\n // number of audio samples for this point (samples size depends on bit depth)\n samples?: number\n}\n\n/**\n * Represents the complete data from the audio analysis.\n */\nexport interface AudioAnalysis {\n segmentDurationMs: number // Duration of each segment in milliseconds\n durationMs: number // Duration of the audio in milliseconds\n bitDepth: number // Bit depth of the audio\n samples: number // Size of the audio in bytes\n numberOfChannels: number // Number of audio channels\n sampleRate: number // Sample rate of the audio\n dataPoints: DataPoint[] // Array of data points from the analysis.\n amplitudeRange: {\n min: number\n max: number\n }\n rmsRange: {\n min: number\n max: number\n }\n extractionTimeMs: number // Time taken to extract/process the analysis in milliseconds\n // TODO: speaker changes into a broader speech analysis section\n speechAnalysis?: {\n speakerChanges: {\n timestamp: number\n speakerId: number\n }[]\n // Could add more speech analysis data here like:\n // dominantSpeaker?: number\n // totalSpeechDuration?: number\n // speakerStats?: { [speakerId: number]: { duration: number, segments: number } }\n }\n}\n\n/**\n * Options for specifying a time range within an audio file.\n */\nexport interface AudioRangeOptions {\n /** Start time in milliseconds */\n startTimeMs?: number\n /** End time in milliseconds */\n endTimeMs?: number\n}\n\n/**\n * Options for generating a quick preview of audio waveform.\n * This is optimized for UI rendering with a specified number of points.\n */\nexport interface PreviewOptions extends AudioRangeOptions {\n /** URI of the audio file to analyze */\n fileUri: string\n /**\n * Total number of points to generate for the preview.\n * @default 100\n */\n numberOfPoints?: number\n /**\n * Optional logger for debugging.\n */\n logger?: ConsoleLike\n /**\n * Optional configuration for decoding the audio file.\n * Defaults to:\n * - targetSampleRate: undefined (keep original)\n * - targetChannels: undefined (keep original)\n * - targetBitDepth: 16\n * - normalizeAudio: false\n */\n decodingOptions?: DecodingConfig\n}\n\n/**\n * Options for mel-spectrogram extraction\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions.\n */\nexport interface ExtractMelSpectrogramOptions {\n fileUri?: string // Path to audio file\n arrayBuffer?: ArrayBuffer // Raw audio buffer\n windowSizeMs: number // Window size in ms (e.g., 25)\n hopLengthMs: number // Hop length in ms (e.g., 10)\n nMels: number // Number of mel filters (e.g., 60)\n fMin?: number // Min frequency (default: 0)\n fMax?: number // Max frequency (default: sampleRate / 2)\n windowType?: 'hann' | 'hamming' // Window function (default: 'hann')\n normalize?: boolean // Mean normalization (default: false)\n logScale?: boolean // Log scaling of mel energies (default: true)\n decodingOptions?: DecodingConfig // Audio decoding settings\n startTimeMs?: number // Optional start time\n endTimeMs?: number // Optional end time\n logger?: ConsoleLike\n}\n\n/**\n * Return type for mel spectrogram extraction\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions.\n */\nexport interface MelSpectrogram {\n spectrogram: number[][] // 2D array [time][mel]\n sampleRate: number // Audio sample rate\n nMels: number // Number of mel filters\n timeSteps: number // Number of time frames\n durationMs: number // Audio duration in ms\n}\n"]}