@siteed/expo-audio-studio 2.18.2 → 2.18.5

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 (45) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +19 -3
  3. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +142 -91
  4. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +1 -0
  5. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +1 -1
  6. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +6 -1
  7. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  8. package/build/cjs/AudioAnalysis/extractAudioData.js +10 -1
  9. package/build/cjs/AudioAnalysis/extractAudioData.js.map +1 -1
  10. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js +5 -1
  11. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js.map +1 -1
  12. package/build/cjs/trimAudio.js +3 -1
  13. package/build/cjs/trimAudio.js.map +1 -1
  14. package/build/cjs/useAudioRecorder.js +9 -38
  15. package/build/cjs/useAudioRecorder.js.map +1 -1
  16. package/build/cjs/utils/cleanNativeOptions.js +22 -0
  17. package/build/cjs/utils/cleanNativeOptions.js.map +1 -0
  18. package/build/esm/AudioAnalysis/extractAudioAnalysis.js +6 -1
  19. package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  20. package/build/esm/AudioAnalysis/extractAudioData.js +10 -1
  21. package/build/esm/AudioAnalysis/extractAudioData.js.map +1 -1
  22. package/build/esm/AudioAnalysis/extractMelSpectrogram.js +5 -1
  23. package/build/esm/AudioAnalysis/extractMelSpectrogram.js.map +1 -1
  24. package/build/esm/trimAudio.js +3 -1
  25. package/build/esm/trimAudio.js.map +1 -1
  26. package/build/esm/useAudioRecorder.js +8 -4
  27. package/build/esm/useAudioRecorder.js.map +1 -1
  28. package/build/esm/utils/cleanNativeOptions.js +19 -0
  29. package/build/esm/utils/cleanNativeOptions.js.map +1 -0
  30. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
  31. package/build/types/AudioAnalysis/extractAudioData.d.ts.map +1 -1
  32. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts.map +1 -1
  33. package/build/types/trimAudio.d.ts.map +1 -1
  34. package/build/types/useAudioRecorder.d.ts.map +1 -1
  35. package/build/types/utils/cleanNativeOptions.d.ts +15 -0
  36. package/build/types/utils/cleanNativeOptions.d.ts.map +1 -0
  37. package/ios/AudioStreamManager.swift +76 -18
  38. package/ios/ExpoAudioStreamModule.swift +17 -19
  39. package/package.json +6 -7
  40. package/src/AudioAnalysis/extractAudioAnalysis.ts +12 -1
  41. package/src/AudioAnalysis/extractAudioData.ts +12 -1
  42. package/src/AudioAnalysis/extractMelSpectrogram.ts +11 -1
  43. package/src/trimAudio.ts +5 -1
  44. package/src/useAudioRecorder.tsx +8 -7
  45. package/src/utils/cleanNativeOptions.ts +18 -0
package/CHANGELOG.md CHANGED
@@ -8,6 +8,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [2.18.5] - 2026-02-23
12
+ ### Changed
13
+ - fix(expo-audio-studio): guard Bluetooth API calls behind permission check on API 31+ (#294) ([05d6e5a](https://github.com/deeeed/expo-audio-stream/commit/05d6e5adb0b8aff35d88aea264d8b75ebb1ae1e4))
14
+ - fix(expo-audio-studio): migrate phone state listener to TelephonyCallback on API 31+ (#275) ([cace0e7](https://github.com/deeeed/expo-audio-stream/commit/cace0e77854c9f5d98abcd320c3759cd765c22da))
15
+ - fix(expo-audio-studio): reset startTime in startRecording and validate hardware format (#298, #223) ([9eee59f](https://github.com/deeeed/expo-audio-stream/commit/9eee59fdb0bb0c3435c728a37880541914a181d0))
16
+ - fix(expo-audio-studio): gate foreground service on enableBackgroundAudio (#288, #294) ([ea6ff85](https://github.com/deeeed/expo-audio-stream/commit/ea6ff855cf6e333cdddd8be0d9581cb7481d7d6f))
17
+ - fix(expo-audio-studio): sanitize options before native bridge calls to prevent Android crash ([5af91d6](https://github.com/deeeed/expo-audio-stream/commit/5af91d6bae0bee92013dc21023e38765cbbd94f3))
18
+ - feat(playground): CDP agentic bridge for multi-platform automation ([48d7dda](https://github.com/deeeed/expo-audio-stream/commit/48d7dda2b017c9b9029c716ea1c0b4dc18a135c9))
19
+ - chore(expo-audio-studio): release @siteed/expo-audio-studio@2.18.4 ([d93ceae](https://github.com/deeeed/expo-audio-stream/commit/d93ceae943c98322d34d38f5e76f2da91bd739a2))
20
+ ## [2.18.4] - 2026-02-16
21
+ ### Changed
22
+ - chore(expo-audio-studio): release @siteed/expo-audio-studio@2.18.3 ([61d58e5](https://github.com/deeeed/expo-audio-stream/commit/61d58e57fd624182aef9983745bf82f584ffc2c7))
23
+ - fix(expo-audio-studio): include compression data in iOS onAudioStream events ([e0444f3](https://github.com/deeeed/expo-audio-stream/commit/e0444f321803deee1aa5fc4259a3e160a668869f))
24
+ - chore: upgrade to Expo SDK 54 (React Native 0.81, React 19.1) (#305) ([f8ff916](https://github.com/deeeed/expo-audio-stream/commit/f8ff916865ae9139282cad088c6b920adb59f6c2))
25
+ - Revert "chore: upgrade to Expo SDK 54 (React Native 0.81, React 19.1) (#303)" (#304) ([6ef8a2f](https://github.com/deeeed/expo-audio-stream/commit/6ef8a2f91973055fc026c3190355bb375052b699))
26
+ - chore: upgrade to Expo SDK 54 (React Native 0.81, React 19.1) (#303) ([822d82c](https://github.com/deeeed/expo-audio-stream/commit/822d82c007da13fdb8dc85698a7b87f8613e5383))
27
+ - feat: properly emit final chunk of audio data in android (#293) ([b468495](https://github.com/deeeed/expo-audio-stream/commit/b46849595562cbc2a0914240e60ebcc225bbb889))
28
+ ## [2.18.3] - 2026-02-16
29
+
11
30
  ## [2.18.2] - 2026-02-16
12
31
  ### Changed
13
32
  - chore(expo-audio-studio): release @siteed/expo-audio-studio@2.18.1 ([067ebfe](https://github.com/deeeed/expo-audio-stream/commit/067ebfe3b6ad3c506e64c3988b67ea90dc894c18))
@@ -380,7 +399,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
380
399
  - Feature: Audio features extraction during recording.
381
400
  - Feature: Consistent WAV PCM recording format across all platforms.
382
401
 
383
- [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.2...HEAD
402
+ [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.5...HEAD
403
+ [2.18.5]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.4...@siteed/expo-audio-studio@2.18.5
404
+ [2.18.4]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.3...@siteed/expo-audio-studio@2.18.4
405
+ [2.18.3]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.2...@siteed/expo-audio-studio@2.18.3
384
406
  [2.18.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.1...@siteed/expo-audio-studio@2.18.2
385
407
  [2.18.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.0...@siteed/expo-audio-studio@2.18.1
386
408
  [2.18.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.17.0...@siteed/expo-audio-studio@2.18.0
@@ -1,5 +1,6 @@
1
1
  package net.siteed.audiostream
2
2
 
3
+ import android.Manifest
3
4
  import android.bluetooth.BluetoothAdapter
4
5
  import android.bluetooth.BluetoothDevice
5
6
  import android.bluetooth.BluetoothProfile
@@ -7,6 +8,7 @@ import android.content.BroadcastReceiver
7
8
  import android.content.Context
8
9
  import android.content.Intent
9
10
  import android.content.IntentFilter
11
+ import android.content.pm.PackageManager
10
12
  import android.media.AudioDeviceInfo
11
13
  import android.media.AudioFormat
12
14
  import android.media.AudioManager
@@ -38,7 +40,10 @@ interface AudioDeviceManagerDelegate {
38
40
  /**
39
41
  * Manages audio device detection, selection and capabilities for Android
40
42
  */
41
- class AudioDeviceManager(private val context: Context) {
43
+ class AudioDeviceManager(
44
+ private val context: Context,
45
+ private val enableDeviceDetection: Boolean = true
46
+ ) {
42
47
 
43
48
  companion object {
44
49
  private const val TAG = "AudioDeviceManager"
@@ -82,8 +87,10 @@ class AudioDeviceManager(private val context: Context) {
82
87
  private val coroutineScope = CoroutineScope(Dispatchers.Main)
83
88
 
84
89
  init {
85
- // Start monitoring device changes
86
- startMonitoringDeviceChanges()
90
+ // Start monitoring device changes only if BT_CONNECT permission is available (#294)
91
+ if (enableDeviceDetection) {
92
+ startMonitoringDeviceChanges()
93
+ }
87
94
  }
88
95
 
89
96
  /**
@@ -1037,6 +1044,15 @@ class AudioDeviceManager(private val context: Context) {
1037
1044
  */
1038
1045
  private fun isBluetoothHeadsetConnected(): Boolean {
1039
1046
  try {
1047
+ // BLUETOOTH_CONNECT permission is required on API 31+ for Bluetooth API access.
1048
+ // Without it, BluetoothAdapter calls throw SecurityException.
1049
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1050
+ if (context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT)
1051
+ != PackageManager.PERMISSION_GRANTED) {
1052
+ return false
1053
+ }
1054
+ }
1055
+
1040
1056
  val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: return false
1041
1057
  if (!bluetoothAdapter.isEnabled) {
1042
1058
  return false
@@ -29,6 +29,7 @@ import android.media.AudioManager
29
29
  import android.media.AudioAttributes
30
30
  import android.media.AudioFocusRequest
31
31
  import android.telephony.PhoneStateListener
32
+ import android.telephony.TelephonyCallback
32
33
  import android.telephony.TelephonyManager
33
34
  import android.app.ActivityManager
34
35
  import java.util.UUID
@@ -94,6 +95,7 @@ class AudioRecorderManager(
94
95
  private var lastEmittedSize = 0L
95
96
  private var lastEmittedCompressedSize = 0L
96
97
  private var streamPosition = 0L // Track total bytes processed in the stream
98
+ private var accumulatedAudioData: ByteArrayOutputStream? = null
97
99
  private val mainHandler = Handler(Looper.getMainLooper())
98
100
  private val audioRecordLock = Any()
99
101
  private var audioFileHandler: AudioFileHandler = AudioFileHandler(filesDir)
@@ -115,6 +117,7 @@ class AudioRecorderManager(
115
117
  private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
116
118
  private var audioFocusRequest: Any? = null // Type Any to handle both old and new APIs
117
119
  private var phoneStateListener: PhoneStateListener? = null
120
+ private var telephonyCallback: Any? = null // TelephonyCallback for API 31+, typed as Any to avoid class verification issues on older APIs
118
121
  private var telephonyManager: TelephonyManager? = null
119
122
  get() {
120
123
  if (field == null) {
@@ -386,81 +389,102 @@ class AudioRecorderManager(
386
389
  val isRecording: Boolean
387
390
  get() = _isRecording.get()
388
391
 
392
+ /**
393
+ * Shared handler for call state changes, used by both the modern TelephonyCallback (API 31+)
394
+ * and the legacy PhoneStateListener (API < 31).
395
+ */
396
+ private fun handleCallStateChanged(state: Int) {
397
+ val stateStr = when (state) {
398
+ TelephonyManager.CALL_STATE_RINGING -> "RINGING"
399
+ TelephonyManager.CALL_STATE_OFFHOOK -> "OFFHOOK"
400
+ TelephonyManager.CALL_STATE_IDLE -> "IDLE"
401
+ else -> "UNKNOWN"
402
+ }
403
+ LogUtils.d(CLASS_NAME, "Phone state changed to: $stateStr")
404
+
405
+ when (state) {
406
+ TelephonyManager.CALL_STATE_RINGING,
407
+ TelephonyManager.CALL_STATE_OFFHOOK -> {
408
+ if (_isRecording.get() && !isPaused.get()) {
409
+ LogUtils.d(CLASS_NAME, "Pausing recording due to incoming/ongoing call")
410
+ mainHandler.post {
411
+ pauseRecording(object : Promise {
412
+ override fun resolve(value: Any?) {
413
+ LogUtils.d(CLASS_NAME, "Successfully paused recording due to call")
414
+ eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
415
+ "reason" to "phoneCall",
416
+ "isPaused" to true
417
+ ))
418
+ }
419
+ override fun reject(code: String, message: String?, cause: Throwable?) {
420
+ LogUtils.e(CLASS_NAME, "Failed to pause recording on phone call", cause)
421
+ }
422
+ })
423
+ }
424
+ }
425
+ }
426
+ TelephonyManager.CALL_STATE_IDLE -> {
427
+ if (_isRecording.get() && isPaused.get()) {
428
+ val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
429
+ LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: $autoResume)")
430
+ if (autoResume) {
431
+ mainHandler.post {
432
+ resumeRecording(object : Promise {
433
+ override fun resolve(value: Any?) {
434
+ LogUtils.d(CLASS_NAME, "Successfully resumed recording after call")
435
+ eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
436
+ "reason" to "phoneCallEnded",
437
+ "isPaused" to false
438
+ ))
439
+ }
440
+ override fun reject(code: String, message: String?, cause: Throwable?) {
441
+ LogUtils.e(CLASS_NAME, "Failed to resume recording after phone call", cause)
442
+ }
443
+ })
444
+ }
445
+ } else {
446
+ LogUtils.d(CLASS_NAME, "Auto-resume disabled, staying paused")
447
+ eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
448
+ "reason" to "phoneCallEnded",
449
+ "isPaused" to true
450
+ ))
451
+ }
452
+ }
453
+ }
454
+ }
455
+ }
456
+
389
457
  private fun initializePhoneStateListener() {
390
458
  try {
391
459
  LogUtils.d(CLASS_NAME, "Initializing phone state listener...")
392
-
460
+
393
461
  if (permissionUtils.checkPhoneStatePermission()) {
394
462
  LogUtils.d(CLASS_NAME, "Phone state permission granted")
395
-
396
- phoneStateListener = object : PhoneStateListener() {
397
- override fun onCallStateChanged(state: Int, phoneNumber: String?) {
398
- val stateStr = when (state) {
399
- TelephonyManager.CALL_STATE_RINGING -> "RINGING"
400
- TelephonyManager.CALL_STATE_OFFHOOK -> "OFFHOOK"
401
- TelephonyManager.CALL_STATE_IDLE -> "IDLE"
402
- else -> "UNKNOWN"
403
- }
404
- LogUtils.d(CLASS_NAME, "Phone state changed to: $stateStr")
405
-
406
- when (state) {
407
- TelephonyManager.CALL_STATE_RINGING,
408
- TelephonyManager.CALL_STATE_OFFHOOK -> {
409
- if (_isRecording.get() && !isPaused.get()) {
410
- LogUtils.d(CLASS_NAME, "Pausing recording due to incoming/ongoing call")
411
- mainHandler.post {
412
- pauseRecording(object : Promise {
413
- override fun resolve(value: Any?) {
414
- LogUtils.d(CLASS_NAME, "Successfully paused recording due to call")
415
- eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
416
- "reason" to "phoneCall",
417
- "isPaused" to true
418
- ))
419
- }
420
- override fun reject(code: String, message: String?, cause: Throwable?) {
421
- LogUtils.e(CLASS_NAME, "Failed to pause recording on phone call", cause)
422
- }
423
- })
424
- }
463
+
464
+ val localTelephonyManager = telephonyManager
465
+ if (localTelephonyManager != null) {
466
+ try {
467
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
468
+ // API 31+: Use modern TelephonyCallback which reliably fires on Android 12+
469
+ val callback = object : TelephonyCallback(), TelephonyCallback.CallStateListener {
470
+ override fun onCallStateChanged(state: Int) {
471
+ handleCallStateChanged(state)
425
472
  }
426
473
  }
427
- TelephonyManager.CALL_STATE_IDLE -> {
428
- if (_isRecording.get() && isPaused.get()) {
429
- val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
430
- LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: $autoResume)")
431
- if (autoResume) {
432
- mainHandler.post {
433
- resumeRecording(object : Promise {
434
- override fun resolve(value: Any?) {
435
- LogUtils.d(CLASS_NAME, "Successfully resumed recording after call")
436
- eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
437
- "reason" to "phoneCallEnded",
438
- "isPaused" to false
439
- ))
440
- }
441
- override fun reject(code: String, message: String?, cause: Throwable?) {
442
- LogUtils.e(CLASS_NAME, "Failed to resume recording after phone call", cause)
443
- }
444
- })
445
- }
446
- } else {
447
- LogUtils.d(CLASS_NAME, "Auto-resume disabled, staying paused")
448
- eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
449
- "reason" to "phoneCallEnded",
450
- "isPaused" to true
451
- ))
452
- }
474
+ telephonyCallback = callback
475
+ localTelephonyManager.registerTelephonyCallback(context.mainExecutor, callback)
476
+ LogUtils.d(CLASS_NAME, "Successfully registered TelephonyCallback (API 31+)")
477
+ } else {
478
+ // Legacy: PhoneStateListener for API < 31
479
+ phoneStateListener = object : PhoneStateListener() {
480
+ @Deprecated("Deprecated in API 31")
481
+ override fun onCallStateChanged(state: Int, phoneNumber: String?) {
482
+ handleCallStateChanged(state)
453
483
  }
454
484
  }
485
+ localTelephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
486
+ LogUtils.d(CLASS_NAME, "Successfully registered PhoneStateListener (legacy)")
455
487
  }
456
- }
457
- }
458
-
459
- val localTelephonyManager = telephonyManager
460
- if (localTelephonyManager != null) {
461
- try {
462
- localTelephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
463
- LogUtils.d(CLASS_NAME, "Successfully registered phone state listener")
464
488
  } catch (e: SecurityException) {
465
489
  LogUtils.w(CLASS_NAME, "Missing permission for phone state listener: ${e.message}")
466
490
  } catch (e: Exception) {
@@ -477,6 +501,30 @@ class AudioRecorderManager(
477
501
  }
478
502
  }
479
503
 
504
+ /**
505
+ * Unregisters the phone state listener/callback, using the appropriate API for the device.
506
+ */
507
+ private fun unregisterPhoneStateListener() {
508
+ try {
509
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
510
+ val callback = telephonyCallback
511
+ if (callback != null) {
512
+ telephonyManager?.unregisterTelephonyCallback(callback as TelephonyCallback)
513
+ telephonyCallback = null
514
+ LogUtils.d(CLASS_NAME, "Unregistered TelephonyCallback (API 31+)")
515
+ }
516
+ } else {
517
+ if (phoneStateListener != null) {
518
+ telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
519
+ phoneStateListener = null
520
+ LogUtils.d(CLASS_NAME, "Unregistered PhoneStateListener (legacy)")
521
+ }
522
+ }
523
+ } catch (e: Exception) {
524
+ LogUtils.w(CLASS_NAME, "Failed to unregister phone state listener: ${e.message}")
525
+ }
526
+ }
527
+
480
528
 
481
529
  @RequiresApi(Build.VERSION_CODES.R)
482
530
  fun startRecording(options: Map<String, Any?>, promise: Promise) {
@@ -593,11 +641,7 @@ class AudioRecorderManager(
593
641
 
594
642
  } catch (e: Exception) {
595
643
  releaseAudioFocus()
596
- try {
597
- telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
598
- } catch (e: Exception) {
599
- LogUtils.w(CLASS_NAME, "Failed to unregister phone state listener: ${e.message}")
600
- }
644
+ unregisterPhoneStateListener()
601
645
  promise.reject("UNEXPECTED_ERROR", "Unexpected error: ${e.message}", e)
602
646
  }
603
647
  }
@@ -859,7 +903,7 @@ class AudioRecorderManager(
859
903
  LogUtils.d(CLASS_NAME, "Skipping primary file creation - primary output is disabled")
860
904
  }
861
905
 
862
- if (recordingConfig.showNotification) {
906
+ if (recordingConfig.showNotification && enableBackgroundAudio) {
863
907
  notificationManager.initialize(recordingConfig)
864
908
  notificationManager.startUpdates(System.currentTimeMillis())
865
909
  AudioRecordingService.startService(context)
@@ -919,8 +963,8 @@ class AudioRecorderManager(
919
963
 
920
964
  recordingThread = Thread { recordingProcess() }.apply { start() }
921
965
 
922
- // Start service if keepAwake is true, regardless of notification settings
923
- if (recordingConfig.keepAwake) {
966
+ // Start service if keepAwake is true, but only if background audio is enabled (#288)
967
+ if (recordingConfig.keepAwake && enableBackgroundAudio) {
924
968
  AudioRecordingService.startService(context)
925
969
  }
926
970
 
@@ -975,13 +1019,13 @@ class AudioRecorderManager(
975
1019
  val threadJoinStartTime = System.currentTimeMillis()
976
1020
  recordingThread?.join(timeoutMs)
977
1021
 
978
- val finalReadStartTime = System.currentTimeMillis()
979
- val audioData = ByteArray(bufferSizeInBytes)
980
- val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
981
- if (bytesRead > 0) {
982
- val emitStartTime = System.currentTimeMillis()
983
- emitAudioData(audioData.copyOfRange(0, bytesRead), bytesRead)
984
- streamPosition += bytesRead // Update stream position for final data
1022
+ // This ensures complete audio data is captured even when stopped before interval threshold
1023
+ accumulatedAudioData?.let { audioData ->
1024
+ if (audioData.size() > 0) {
1025
+ LogUtils.d(CLASS_NAME, "Emitting final accumulated audio chunk of ${audioData.size()} bytes before stopping")
1026
+ emitAudioData(audioData.toByteArray(), audioData.size())
1027
+ streamPosition += audioData.size() // Update stream position for final data
1028
+ }
985
1029
  }
986
1030
 
987
1031
  LogUtils.d(CLASS_NAME, "Stopping recording state = ${audioRecord?.state}")
@@ -1410,10 +1454,10 @@ class AudioRecorderManager(
1410
1454
  LogUtils.d(CLASS_NAME, "Entering recording loop")
1411
1455
 
1412
1456
  // Buffer to accumulate data
1413
- val accumulatedAudioData = ByteArrayOutputStream()
1457
+ accumulatedAudioData = ByteArrayOutputStream()
1414
1458
  val accumulatedAnalysisData = ByteArrayOutputStream() // Separate buffer for analysis
1415
1459
  audioFileHandler.writeWavHeader(
1416
- accumulatedAudioData,
1460
+ accumulatedAudioData!!,
1417
1461
  recordingConfig.sampleRate,
1418
1462
  recordingConfig.channels,
1419
1463
  when (recordingConfig.encoding) {
@@ -1443,7 +1487,7 @@ class AudioRecorderManager(
1443
1487
  while (_isRecording.get() && !Thread.currentThread().isInterrupted) {
1444
1488
  loopCount++
1445
1489
  if (loopCount % 100 == 0) {
1446
- LogUtils.d(CLASS_NAME, "Recording loop iteration $loopCount, isRecording: ${_isRecording.get()}, accumulatedAudioSize: ${accumulatedAudioData.size()}, accumulatedAnalysisSize: ${accumulatedAnalysisData.size()}")
1490
+ LogUtils.d(CLASS_NAME, "Recording loop iteration $loopCount, isRecording: ${_isRecording.get()}, accumulatedAudioSize: ${accumulatedAudioData?.size() ?: 0}, accumulatedAnalysisSize: ${accumulatedAnalysisData.size()}")
1447
1491
  }
1448
1492
  if (isPaused.get()) {
1449
1493
  Thread.sleep(100) // Add small delay when paused
@@ -1478,7 +1522,7 @@ class AudioRecorderManager(
1478
1522
  }
1479
1523
  totalDataSize += bytesRead
1480
1524
 
1481
- accumulatedAudioData.write(audioData, 0, bytesRead)
1525
+ accumulatedAudioData?.write(audioData, 0, bytesRead)
1482
1526
 
1483
1527
  // Always accumulate data for analysis if enabled (moved outside shouldProcessAnalysis check)
1484
1528
  if (recordingConfig.enableProcessing) {
@@ -1492,13 +1536,15 @@ class AudioRecorderManager(
1492
1536
 
1493
1537
  // Handle regular audio data emission
1494
1538
  if (currentTime - lastEmitTime >= recordingConfig.interval) {
1495
- emitAudioData(
1496
- accumulatedAudioData.toByteArray(),
1497
- accumulatedAudioData.size()
1498
- )
1499
- streamPosition += accumulatedAudioData.size() // Update stream position
1500
- lastEmitTime = currentTime
1501
- accumulatedAudioData.reset() // Clear the accumulator
1539
+ accumulatedAudioData?.let { audioData ->
1540
+ emitAudioData(
1541
+ audioData.toByteArray(),
1542
+ audioData.size()
1543
+ )
1544
+ streamPosition += audioData.size() // Update stream position
1545
+ lastEmitTime = currentTime
1546
+ audioData.reset() // Clear the accumulator
1547
+ }
1502
1548
  }
1503
1549
 
1504
1550
  // Handle analysis emission separately
@@ -1690,6 +1736,7 @@ class AudioRecorderManager(
1690
1736
 
1691
1737
  releaseWakeLock()
1692
1738
  releaseAudioFocus()
1739
+ unregisterPhoneStateListener()
1693
1740
  audioRecord?.release()
1694
1741
  audioRecord = null
1695
1742
 
@@ -1700,6 +1747,10 @@ class AudioRecorderManager(
1700
1747
  streamPosition = 0
1701
1748
  recordingStartTime = 0
1702
1749
 
1750
+ // Clean up accumulated audio data
1751
+ accumulatedAudioData?.close()
1752
+ accumulatedAudioData = null
1753
+
1703
1754
  // Update the WAV header if needed
1704
1755
  audioFile?.let { file ->
1705
1756
  // Skip WAV header update if we're only doing compressed output
@@ -70,6 +70,7 @@ class AudioRecordingService : Service() {
70
70
  }
71
71
  } catch (e: Exception) {
72
72
  Log.e(Constants.TAG, "Failed to start foreground service: ${e.message}", e)
73
+ stopSelf() // Don't leave service in broken state (#288)
73
74
  }
74
75
  }
75
76
 
@@ -942,7 +942,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
942
942
  // Initialize AudioDeviceManager
943
943
  LogUtils.d(CLASS_NAME, "🔧 Initializing AudioDeviceManager...")
944
944
  LogUtils.d(CLASS_NAME, "🔧 Device detection enabled: $enableDeviceDetection")
945
- audioDeviceManager = AudioDeviceManager(context)
945
+ audioDeviceManager = AudioDeviceManager(context, enableDeviceDetection)
946
946
  LogUtils.d(CLASS_NAME, "🔧 AudioDeviceManager initialized")
947
947
 
948
948
  // Initialize AudioRecorderManager with AudioDeviceManager integration
@@ -8,6 +8,7 @@ exports.extractAudioAnalysis = extractAudioAnalysis;
8
8
  const ExpoAudioStreamModule_1 = __importDefault(require("../ExpoAudioStreamModule"));
9
9
  const constants_1 = require("../constants");
10
10
  const audioProcessing_1 = require("../utils/audioProcessing");
11
+ const cleanNativeOptions_1 = require("../utils/cleanNativeOptions");
11
12
  const convertPCMToFloat32_1 = require("../utils/convertPCMToFloat32");
12
13
  const crc32_1 = __importDefault(require("../utils/crc32"));
13
14
  const getWavFileInfo_1 = require("../utils/getWavFileInfo");
@@ -114,7 +115,11 @@ async function extractAudioAnalysis(props) {
114
115
  }
115
116
  }
116
117
  else {
117
- return await ExpoAudioStreamModule_1.default.extractAudioAnalysis(props);
118
+ // Strip non-serializable fields — logger and arrayBuffer cause
119
+ // "Cannot convert '[object Object]' to a Kotlin type" on Android.
120
+ const { logger: _logger, arrayBuffer: _arrayBuffer, ...nativeOptions } = props;
121
+ // Clean undefined values to avoid Android Kotlin bridge crash
122
+ return await ExpoAudioStreamModule_1.default.extractAudioAnalysis((0, cleanNativeOptions_1.cleanNativeOptions)(nativeOptions));
118
123
  }
119
124
  }
120
125
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"extractAudioAnalysis.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractAudioAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["// packages/expo-audio-stream/src/AudioAnalysis/extractAudioAnalysis.ts\n/**\n * This module provides functions for extracting and analyzing audio data.\n * - `extractAudioAnalysis`: For detailed analysis with customizable ranges and decoding options.\n * - `extractWavAudioAnalysis`: For analyzing WAV files without decoding, preserving original PCM values.\n * - `extractPreview`: For generating quick previews of audio waveforms, optimized for UI rendering.\n */\nimport { ConsoleLike } from '../ExpoAudioStream.types'\nimport ExpoAudioStreamModule from '../ExpoAudioStreamModule'\nimport { isWeb } from '../constants'\nimport {\n AudioAnalysis,\n AudioFeaturesOptions,\n DataPoint,\n DecodingConfig,\n} from './AudioAnalysis.types'\nimport { processAudioBuffer } from '../utils/audioProcessing'\nimport { convertPCMToFloat32 } from '../utils/convertPCMToFloat32'\nimport crc32 from '../utils/crc32'\nimport { getWavFileInfo, WavFileInfo } from '../utils/getWavFileInfo'\nimport { InlineFeaturesExtractor } from '../workers/InlineFeaturesExtractor.web'\n\nfunction calculateCRC32ForDataPoint(data: Float32Array): number {\n // Convert float array to byte array for CRC32\n const byteArray = new Uint8Array(data.length * 4)\n const dataView = new DataView(byteArray.buffer)\n\n for (let i = 0; i < data.length; i++) {\n dataView.setFloat32(i * 4, data[i], true)\n }\n\n return crc32.buf(byteArray)\n}\n\nexport interface ExtractWavAudioAnalysisProps {\n fileUri?: string // should provide either fileUri or arrayBuffer\n wavMetadata?: WavFileInfo\n arrayBuffer?: ArrayBuffer\n bitDepth?: number\n durationMs?: number\n sampleRate?: number\n numberOfChannels?: number\n position?: number // Optional number of bytes to skip. Default is 0\n length?: number // Optional number of bytes to read.\n segmentDurationMs?: number // Optional number of points per second. Use to reduce the number of points and compute the number of datapoints to return.\n features?: AudioFeaturesOptions\n featuresExtratorUrl?: string\n logger?: ConsoleLike\n decodingOptions?: DecodingConfig\n}\n\n// Define base options interface with common properties\ninterface BaseExtractOptions {\n fileUri?: string\n arrayBuffer?: ArrayBuffer\n /**\n * Duration of each analysis segment in milliseconds. Defaults to 100ms if not specified.\n */\n segmentDurationMs?: number\n features?: AudioFeaturesOptions\n decodingOptions?: DecodingConfig\n logger?: ConsoleLike\n}\n\n// Time-based range options\ninterface TimeRangeOptions extends BaseExtractOptions {\n startTimeMs?: number\n endTimeMs?: number\n position?: never\n length?: never\n}\n\n// Byte-based range options\ninterface ByteRangeOptions extends BaseExtractOptions {\n position?: number\n length?: number\n startTimeMs?: never\n endTimeMs?: never\n}\n\n/**\n * Options for extracting audio analysis.\n * - For time-based analysis, provide `startTimeMs` and `endTimeMs`.\n * - For byte-based analysis, provide `position` and `length`.\n * - Do not mix time and byte ranges.\n */\nexport type ExtractAudioAnalysisProps = TimeRangeOptions | ByteRangeOptions\n\n/**\n * Extracts detailed audio analysis from the specified audio file or buffer.\n * Supports either time-based or byte-based ranges for flexibility in analysis.\n *\n * @param props - The options for extraction, including file URI, ranges, and decoding settings.\n * @returns A promise that resolves to the audio analysis data.\n * @throws {Error} If both time and byte ranges are provided or if required parameters are missing.\n */\nexport async function extractAudioAnalysis(\n props: ExtractAudioAnalysisProps\n): Promise<AudioAnalysis> {\n const {\n fileUri,\n arrayBuffer,\n decodingOptions,\n logger,\n segmentDurationMs = 100,\n features,\n } = props\n\n if (isWeb) {\n try {\n // Create AudioContext here\n const audioContext = new (window.AudioContext ||\n (window as any).webkitAudioContext)({\n sampleRate: decodingOptions?.targetSampleRate ?? 16000,\n })\n\n try {\n const processedBuffer = await processAudioBuffer({\n arrayBuffer,\n fileUri,\n targetSampleRate:\n decodingOptions?.targetSampleRate ?? 16000,\n targetChannels: decodingOptions?.targetChannels ?? 1,\n normalizeAudio: decodingOptions?.normalizeAudio ?? false,\n startTimeMs:\n 'startTimeMs' in props ? props.startTimeMs : undefined,\n endTimeMs:\n 'endTimeMs' in props ? props.endTimeMs : undefined,\n position: 'position' in props ? props.position : undefined,\n length: 'length' in props ? props.length : undefined,\n audioContext, // Pass the context we created\n logger,\n })\n\n const channelData = processedBuffer.buffer.getChannelData(0)\n\n // Create and initialize the worker\n const blob = new Blob([InlineFeaturesExtractor], {\n type: 'application/javascript',\n })\n const workerUrl = URL.createObjectURL(blob)\n const worker = new Worker(workerUrl)\n\n return new Promise((resolve, reject) => {\n worker.onmessage = (event) => {\n if (event.data.error) {\n reject(new Error(event.data.error))\n return\n }\n\n const result: AudioAnalysis = event.data.result\n // Calculate CRC32 after worker completes if requested\n if (features?.crc32) {\n const samplesPerSegment = Math.floor(\n (processedBuffer.sampleRate *\n segmentDurationMs) /\n 1000\n )\n\n result.dataPoints = result.dataPoints.map(\n (point: DataPoint, index: number) => {\n const startSample =\n index * samplesPerSegment\n const segmentData = channelData.slice(\n startSample,\n startSample + samplesPerSegment\n )\n\n return {\n ...point,\n features: {\n ...point.features,\n crc32: calculateCRC32ForDataPoint(\n segmentData\n ),\n },\n }\n }\n )\n }\n\n URL.revokeObjectURL(workerUrl)\n worker.terminate()\n resolve(result)\n }\n\n worker.onerror = (error) => {\n URL.revokeObjectURL(workerUrl)\n worker.terminate()\n reject(error)\n }\n\n worker.postMessage({\n channelData,\n sampleRate: processedBuffer.sampleRate,\n segmentDurationMs,\n bitDepth: decodingOptions?.targetBitDepth ?? 32,\n numberOfChannels: processedBuffer.channels,\n fullAudioDurationMs: processedBuffer.durationMs,\n // enableLogging: !!logger,\n features,\n })\n })\n } finally {\n await audioContext.close()\n }\n } catch (error) {\n logger?.error('Failed to process audio:', error)\n throw error\n }\n } else {\n return await ExpoAudioStreamModule.extractAudioAnalysis(props)\n }\n}\n\n/**\n * Analyzes WAV files without decoding, preserving original PCM values.\n * Use this function when you need to ensure the analysis matches other software by avoiding any transformations.\n *\n * @param props - The options for WAV analysis, including file URI and range.\n * @returns A promise that resolves to the audio analysis data.\n */\nexport const extractRawWavAnalysis = async ({\n fileUri,\n segmentDurationMs = 100, // Default to 100ms\n arrayBuffer,\n bitDepth,\n durationMs,\n sampleRate,\n numberOfChannels,\n features,\n logger,\n position = 0,\n length,\n}: ExtractWavAudioAnalysisProps): Promise<AudioAnalysis> => {\n if (isWeb) {\n if (!arrayBuffer && !fileUri) {\n throw new Error('Either arrayBuffer or fileUri must be provided')\n }\n\n if (!arrayBuffer) {\n logger?.log(`fetching fileUri`, fileUri)\n const response = await fetch(fileUri!)\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch fileUri: ${response.statusText}`\n )\n }\n\n arrayBuffer = await response.arrayBuffer()\n logger?.log(`fetched fileUri`, arrayBuffer.byteLength, arrayBuffer)\n }\n\n // Create a new copy of the ArrayBuffer to avoid detachment issues\n const bufferCopy = arrayBuffer.slice(0)\n logger?.log(\n `extractAudioAnalysis bitDepth=${bitDepth} len=${bufferCopy.byteLength}`,\n bufferCopy.slice(0, 100)\n )\n\n let actualBitDepth = bitDepth\n if (!actualBitDepth) {\n logger?.log(\n `extractAudioAnalysis bitDepth not provided -- getting wav file info`\n )\n const fileInfo = await getWavFileInfo(bufferCopy)\n actualBitDepth = fileInfo.bitDepth\n }\n logger?.log(`extractAudioAnalysis actualBitDepth=${actualBitDepth}`)\n\n const {\n pcmValues: channelData,\n min,\n max,\n } = await convertPCMToFloat32({\n buffer: arrayBuffer,\n bitDepth: actualBitDepth,\n })\n logger?.log(\n `extractAudioAnalysis convertPCMToFloat32 length=${channelData.length} range: [ ${min} :: ${max} ]`\n )\n\n // Apply position and length constraints to channelData if specified\n const startIndex = position\n const endIndex = length ? startIndex + length : channelData.length\n const constrainedChannelData = channelData.slice(startIndex, endIndex)\n\n return new Promise((resolve, reject) => {\n const blob = new Blob([InlineFeaturesExtractor], {\n type: 'application/javascript',\n })\n const url = URL.createObjectURL(blob)\n const worker = new Worker(url)\n\n worker.onmessage = (event) => {\n resolve(event.data.result)\n }\n\n worker.onerror = (error) => {\n reject(error)\n }\n\n worker.postMessage({\n command: 'process',\n channelData: constrainedChannelData,\n sampleRate,\n segmentDurationMs,\n logger,\n bitDepth,\n fullAudioDurationMs: durationMs,\n numberOfChannels,\n })\n })\n } else {\n if (!fileUri) {\n throw new Error('fileUri is required')\n }\n logger?.log(`extractAudioAnalysis`, {\n fileUri,\n segmentDurationMs,\n })\n const res = await ExpoAudioStreamModule.extractAudioAnalysis({\n fileUri,\n segmentDurationMs,\n features,\n position,\n length,\n })\n logger?.log(`extractAudioAnalysis`, res)\n return res\n }\n}\n"]}
1
+ {"version":3,"file":"extractAudioAnalysis.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractAudioAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["// packages/expo-audio-stream/src/AudioAnalysis/extractAudioAnalysis.ts\n/**\n * This module provides functions for extracting and analyzing audio data.\n * - `extractAudioAnalysis`: For detailed analysis with customizable ranges and decoding options.\n * - `extractWavAudioAnalysis`: For analyzing WAV files without decoding, preserving original PCM values.\n * - `extractPreview`: For generating quick previews of audio waveforms, optimized for UI rendering.\n */\nimport { ConsoleLike } from '../ExpoAudioStream.types'\nimport ExpoAudioStreamModule from '../ExpoAudioStreamModule'\nimport { isWeb } from '../constants'\nimport {\n AudioAnalysis,\n AudioFeaturesOptions,\n DataPoint,\n DecodingConfig,\n} from './AudioAnalysis.types'\nimport { processAudioBuffer } from '../utils/audioProcessing'\nimport { cleanNativeOptions } from '../utils/cleanNativeOptions'\nimport { convertPCMToFloat32 } from '../utils/convertPCMToFloat32'\nimport crc32 from '../utils/crc32'\nimport { getWavFileInfo, WavFileInfo } from '../utils/getWavFileInfo'\nimport { InlineFeaturesExtractor } from '../workers/InlineFeaturesExtractor.web'\n\nfunction calculateCRC32ForDataPoint(data: Float32Array): number {\n // Convert float array to byte array for CRC32\n const byteArray = new Uint8Array(data.length * 4)\n const dataView = new DataView(byteArray.buffer)\n\n for (let i = 0; i < data.length; i++) {\n dataView.setFloat32(i * 4, data[i], true)\n }\n\n return crc32.buf(byteArray)\n}\n\nexport interface ExtractWavAudioAnalysisProps {\n fileUri?: string // should provide either fileUri or arrayBuffer\n wavMetadata?: WavFileInfo\n arrayBuffer?: ArrayBuffer\n bitDepth?: number\n durationMs?: number\n sampleRate?: number\n numberOfChannels?: number\n position?: number // Optional number of bytes to skip. Default is 0\n length?: number // Optional number of bytes to read.\n segmentDurationMs?: number // Optional number of points per second. Use to reduce the number of points and compute the number of datapoints to return.\n features?: AudioFeaturesOptions\n featuresExtratorUrl?: string\n logger?: ConsoleLike\n decodingOptions?: DecodingConfig\n}\n\n// Define base options interface with common properties\ninterface BaseExtractOptions {\n fileUri?: string\n arrayBuffer?: ArrayBuffer\n /**\n * Duration of each analysis segment in milliseconds. Defaults to 100ms if not specified.\n */\n segmentDurationMs?: number\n features?: AudioFeaturesOptions\n decodingOptions?: DecodingConfig\n logger?: ConsoleLike\n}\n\n// Time-based range options\ninterface TimeRangeOptions extends BaseExtractOptions {\n startTimeMs?: number\n endTimeMs?: number\n position?: never\n length?: never\n}\n\n// Byte-based range options\ninterface ByteRangeOptions extends BaseExtractOptions {\n position?: number\n length?: number\n startTimeMs?: never\n endTimeMs?: never\n}\n\n/**\n * Options for extracting audio analysis.\n * - For time-based analysis, provide `startTimeMs` and `endTimeMs`.\n * - For byte-based analysis, provide `position` and `length`.\n * - Do not mix time and byte ranges.\n */\nexport type ExtractAudioAnalysisProps = TimeRangeOptions | ByteRangeOptions\n\n/**\n * Extracts detailed audio analysis from the specified audio file or buffer.\n * Supports either time-based or byte-based ranges for flexibility in analysis.\n *\n * @param props - The options for extraction, including file URI, ranges, and decoding settings.\n * @returns A promise that resolves to the audio analysis data.\n * @throws {Error} If both time and byte ranges are provided or if required parameters are missing.\n */\nexport async function extractAudioAnalysis(\n props: ExtractAudioAnalysisProps\n): Promise<AudioAnalysis> {\n const {\n fileUri,\n arrayBuffer,\n decodingOptions,\n logger,\n segmentDurationMs = 100,\n features,\n } = props\n\n if (isWeb) {\n try {\n // Create AudioContext here\n const audioContext = new (window.AudioContext ||\n (window as any).webkitAudioContext)({\n sampleRate: decodingOptions?.targetSampleRate ?? 16000,\n })\n\n try {\n const processedBuffer = await processAudioBuffer({\n arrayBuffer,\n fileUri,\n targetSampleRate:\n decodingOptions?.targetSampleRate ?? 16000,\n targetChannels: decodingOptions?.targetChannels ?? 1,\n normalizeAudio: decodingOptions?.normalizeAudio ?? false,\n startTimeMs:\n 'startTimeMs' in props ? props.startTimeMs : undefined,\n endTimeMs:\n 'endTimeMs' in props ? props.endTimeMs : undefined,\n position: 'position' in props ? props.position : undefined,\n length: 'length' in props ? props.length : undefined,\n audioContext, // Pass the context we created\n logger,\n })\n\n const channelData = processedBuffer.buffer.getChannelData(0)\n\n // Create and initialize the worker\n const blob = new Blob([InlineFeaturesExtractor], {\n type: 'application/javascript',\n })\n const workerUrl = URL.createObjectURL(blob)\n const worker = new Worker(workerUrl)\n\n return new Promise((resolve, reject) => {\n worker.onmessage = (event) => {\n if (event.data.error) {\n reject(new Error(event.data.error))\n return\n }\n\n const result: AudioAnalysis = event.data.result\n // Calculate CRC32 after worker completes if requested\n if (features?.crc32) {\n const samplesPerSegment = Math.floor(\n (processedBuffer.sampleRate *\n segmentDurationMs) /\n 1000\n )\n\n result.dataPoints = result.dataPoints.map(\n (point: DataPoint, index: number) => {\n const startSample =\n index * samplesPerSegment\n const segmentData = channelData.slice(\n startSample,\n startSample + samplesPerSegment\n )\n\n return {\n ...point,\n features: {\n ...point.features,\n crc32: calculateCRC32ForDataPoint(\n segmentData\n ),\n },\n }\n }\n )\n }\n\n URL.revokeObjectURL(workerUrl)\n worker.terminate()\n resolve(result)\n }\n\n worker.onerror = (error) => {\n URL.revokeObjectURL(workerUrl)\n worker.terminate()\n reject(error)\n }\n\n worker.postMessage({\n channelData,\n sampleRate: processedBuffer.sampleRate,\n segmentDurationMs,\n bitDepth: decodingOptions?.targetBitDepth ?? 32,\n numberOfChannels: processedBuffer.channels,\n fullAudioDurationMs: processedBuffer.durationMs,\n // enableLogging: !!logger,\n features,\n })\n })\n } finally {\n await audioContext.close()\n }\n } catch (error) {\n logger?.error('Failed to process audio:', error)\n throw error\n }\n } else {\n // Strip non-serializable fields — logger and arrayBuffer cause\n // \"Cannot convert '[object Object]' to a Kotlin type\" on Android.\n const {\n logger: _logger,\n arrayBuffer: _arrayBuffer,\n ...nativeOptions\n } = props\n // Clean undefined values to avoid Android Kotlin bridge crash\n return await ExpoAudioStreamModule.extractAudioAnalysis(\n cleanNativeOptions(nativeOptions)\n )\n }\n}\n\n/**\n * Analyzes WAV files without decoding, preserving original PCM values.\n * Use this function when you need to ensure the analysis matches other software by avoiding any transformations.\n *\n * @param props - The options for WAV analysis, including file URI and range.\n * @returns A promise that resolves to the audio analysis data.\n */\nexport const extractRawWavAnalysis = async ({\n fileUri,\n segmentDurationMs = 100, // Default to 100ms\n arrayBuffer,\n bitDepth,\n durationMs,\n sampleRate,\n numberOfChannels,\n features,\n logger,\n position = 0,\n length,\n}: ExtractWavAudioAnalysisProps): Promise<AudioAnalysis> => {\n if (isWeb) {\n if (!arrayBuffer && !fileUri) {\n throw new Error('Either arrayBuffer or fileUri must be provided')\n }\n\n if (!arrayBuffer) {\n logger?.log(`fetching fileUri`, fileUri)\n const response = await fetch(fileUri!)\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch fileUri: ${response.statusText}`\n )\n }\n\n arrayBuffer = await response.arrayBuffer()\n logger?.log(`fetched fileUri`, arrayBuffer.byteLength, arrayBuffer)\n }\n\n // Create a new copy of the ArrayBuffer to avoid detachment issues\n const bufferCopy = arrayBuffer.slice(0)\n logger?.log(\n `extractAudioAnalysis bitDepth=${bitDepth} len=${bufferCopy.byteLength}`,\n bufferCopy.slice(0, 100)\n )\n\n let actualBitDepth = bitDepth\n if (!actualBitDepth) {\n logger?.log(\n `extractAudioAnalysis bitDepth not provided -- getting wav file info`\n )\n const fileInfo = await getWavFileInfo(bufferCopy)\n actualBitDepth = fileInfo.bitDepth\n }\n logger?.log(`extractAudioAnalysis actualBitDepth=${actualBitDepth}`)\n\n const {\n pcmValues: channelData,\n min,\n max,\n } = await convertPCMToFloat32({\n buffer: arrayBuffer,\n bitDepth: actualBitDepth,\n })\n logger?.log(\n `extractAudioAnalysis convertPCMToFloat32 length=${channelData.length} range: [ ${min} :: ${max} ]`\n )\n\n // Apply position and length constraints to channelData if specified\n const startIndex = position\n const endIndex = length ? startIndex + length : channelData.length\n const constrainedChannelData = channelData.slice(startIndex, endIndex)\n\n return new Promise((resolve, reject) => {\n const blob = new Blob([InlineFeaturesExtractor], {\n type: 'application/javascript',\n })\n const url = URL.createObjectURL(blob)\n const worker = new Worker(url)\n\n worker.onmessage = (event) => {\n resolve(event.data.result)\n }\n\n worker.onerror = (error) => {\n reject(error)\n }\n\n worker.postMessage({\n command: 'process',\n channelData: constrainedChannelData,\n sampleRate,\n segmentDurationMs,\n logger,\n bitDepth,\n fullAudioDurationMs: durationMs,\n numberOfChannels,\n })\n })\n } else {\n if (!fileUri) {\n throw new Error('fileUri is required')\n }\n logger?.log(`extractAudioAnalysis`, {\n fileUri,\n segmentDurationMs,\n })\n const res = await ExpoAudioStreamModule.extractAudioAnalysis({\n fileUri,\n segmentDurationMs,\n features,\n position,\n length,\n })\n logger?.log(`extractAudioAnalysis`, res)\n return res\n }\n}\n"]}
@@ -5,8 +5,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.extractAudioData = void 0;
7
7
  const ExpoAudioStreamModule_1 = __importDefault(require("../ExpoAudioStreamModule"));
8
+ const constants_1 = require("../constants");
9
+ const cleanNativeOptions_1 = require("../utils/cleanNativeOptions");
8
10
  const extractAudioData = async (props) => {
9
- return await ExpoAudioStreamModule_1.default.extractAudioData(props);
11
+ if (constants_1.isWeb) {
12
+ // Web implementation handles logger natively in ExpoAudioStreamModule.ts
13
+ return await ExpoAudioStreamModule_1.default.extractAudioData(props);
14
+ }
15
+ // Native: only pass serializable fields — logger causes crash on Android
16
+ const { logger: _logger, ...nativeOptions } = props;
17
+ // Clean undefined values to avoid Android Kotlin bridge crash
18
+ return await ExpoAudioStreamModule_1.default.extractAudioData((0, cleanNativeOptions_1.cleanNativeOptions)(nativeOptions));
10
19
  };
11
20
  exports.extractAudioData = extractAudioData;
12
21
  //# sourceMappingURL=extractAudioData.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"extractAudioData.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractAudioData.ts"],"names":[],"mappings":";;;;;;AACA,qFAA4D;AAErD,MAAM,gBAAgB,GAAG,KAAK,EAAE,KAA8B,EAAE,EAAE;IACrE,OAAO,MAAM,+BAAqB,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAA;AAC9D,CAAC,CAAA;AAFY,QAAA,gBAAgB,oBAE5B","sourcesContent":["import { ExtractAudioDataOptions } from '../ExpoAudioStream.types'\nimport ExpoAudioStreamModule from '../ExpoAudioStreamModule'\n\nexport const extractAudioData = async (props: ExtractAudioDataOptions) => {\n return await ExpoAudioStreamModule.extractAudioData(props)\n}\n"]}
1
+ {"version":3,"file":"extractAudioData.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractAudioData.ts"],"names":[],"mappings":";;;;;;AACA,qFAA4D;AAC5D,4CAAoC;AACpC,oEAAgE;AAEzD,MAAM,gBAAgB,GAAG,KAAK,EAAE,KAA8B,EAAE,EAAE;IACrE,IAAI,iBAAK,EAAE,CAAC;QACR,yEAAyE;QACzE,OAAO,MAAM,+BAAqB,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAA;IAC9D,CAAC;IACD,yEAAyE;IACzE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,EAAE,GAAG,KAAK,CAAA;IACnD,8DAA8D;IAC9D,OAAO,MAAM,+BAAqB,CAAC,gBAAgB,CAC/C,IAAA,uCAAkB,EAAC,aAAa,CAAC,CACpC,CAAA;AACL,CAAC,CAAA;AAXY,QAAA,gBAAgB,oBAW5B","sourcesContent":["import { ExtractAudioDataOptions } from '../ExpoAudioStream.types'\nimport ExpoAudioStreamModule from '../ExpoAudioStreamModule'\nimport { isWeb } from '../constants'\nimport { cleanNativeOptions } from '../utils/cleanNativeOptions'\n\nexport const extractAudioData = async (props: ExtractAudioDataOptions) => {\n if (isWeb) {\n // Web implementation handles logger natively in ExpoAudioStreamModule.ts\n return await ExpoAudioStreamModule.extractAudioData(props)\n }\n // Native: only pass serializable fields — logger causes crash on Android\n const { logger: _logger, ...nativeOptions } = props\n // Clean undefined values to avoid Android Kotlin bridge crash\n return await ExpoAudioStreamModule.extractAudioData(\n cleanNativeOptions(nativeOptions)\n )\n}\n"]}
@@ -8,6 +8,7 @@ exports.extractMelSpectrogram = extractMelSpectrogram;
8
8
  const __1 = require("..");
9
9
  const constants_1 = require("../constants");
10
10
  const audioProcessing_1 = require("../utils/audioProcessing");
11
+ const cleanNativeOptions_1 = require("../utils/cleanNativeOptions");
11
12
  /**
12
13
  * Extracts a mel spectrogram from audio data
13
14
  *
@@ -59,7 +60,10 @@ async function extractMelSpectrogram(options) {
59
60
  await audioContext.close();
60
61
  }
61
62
  }
62
- return __1.ExpoAudioStreamModule.extractMelSpectrogram(options);
63
+ // Strip logger/arrayBuffer (non-serializable) then clean undefined values
64
+ // to avoid Android "Cannot convert '[object Object]' to Kotlin type" crash
65
+ const { logger: _logger, arrayBuffer: _arrayBuffer, ...nativeOptions } = options;
66
+ return __1.ExpoAudioStreamModule.extractMelSpectrogram((0, cleanNativeOptions_1.cleanNativeOptions)(nativeOptions));
63
67
  }
64
68
  /**
65
69
  * Computes a mel spectrogram from audio data