@siteed/expo-audio-studio 2.18.4 → 2.18.6

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 (46) hide show
  1. package/CHANGELOG.md +19 -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 +117 -73
  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 +3 -2
  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 +3 -2
  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/AudioDeviceManager.swift +9 -5
  38. package/ios/AudioStreamManager.swift +66 -39
  39. package/ios/ExpoAudioStreamModule.swift +9 -3
  40. package/package.json +1 -2
  41. package/src/AudioAnalysis/extractAudioAnalysis.ts +12 -1
  42. package/src/AudioAnalysis/extractAudioData.ts +12 -1
  43. package/src/AudioAnalysis/extractMelSpectrogram.ts +11 -1
  44. package/src/trimAudio.ts +5 -1
  45. package/src/useAudioRecorder.tsx +3 -2
  46. package/src/utils/cleanNativeOptions.ts +18 -0
package/CHANGELOG.md CHANGED
@@ -7,7 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.18.6] - 2026-03-06
11
+ ### Fixed
12
+ - fix(ios): `resetToDefaultDevice` now correctly resets the engine tap when switching back to the system default input (was a no-op due to `deviceId=nil` guard)
13
+ - fix(ios): recovery path after a failed device switch no longer produces silent audio (missing tap reinstall before engine restart)
14
+ - fix(ios): `setupNowPlayingInfo` no longer overrides user-configured audio session options (e.g. whisper-mode / no-Bluetooth configs)
15
+ - fix(ios): `selectInputDevice` now syncs `deviceId` into `recordingSettings` before updating the engine, ensuring the port lookup succeeds
16
+ - fix(ios): phone-call auto-resume handler respects user-configured `categoryOptions` instead of hardcoded `[.allowBluetooth, .mixWithOthers]`
17
+ - fix(ios): `AudioDeviceManager.prepareAudioSession` preserves existing session options when already `.playAndRecord`
10
18
 
19
+ ## [2.18.5] - 2026-02-23
20
+ ### Changed
21
+ - 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))
22
+ - fix(expo-audio-studio): migrate phone state listener to TelephonyCallback on API 31+ (#275) ([cace0e7](https://github.com/deeeed/expo-audio-stream/commit/cace0e77854c9f5d98abcd320c3759cd765c22da))
23
+ - fix(expo-audio-studio): reset startTime in startRecording and validate hardware format (#298, #223) ([9eee59f](https://github.com/deeeed/expo-audio-stream/commit/9eee59fdb0bb0c3435c728a37880541914a181d0))
24
+ - fix(expo-audio-studio): gate foreground service on enableBackgroundAudio (#288, #294) ([ea6ff85](https://github.com/deeeed/expo-audio-stream/commit/ea6ff855cf6e333cdddd8be0d9581cb7481d7d6f))
25
+ - fix(expo-audio-studio): sanitize options before native bridge calls to prevent Android crash ([5af91d6](https://github.com/deeeed/expo-audio-stream/commit/5af91d6bae0bee92013dc21023e38765cbbd94f3))
26
+ - feat(playground): CDP agentic bridge for multi-platform automation ([48d7dda](https://github.com/deeeed/expo-audio-stream/commit/48d7dda2b017c9b9029c716ea1c0b4dc18a135c9))
27
+ - chore(expo-audio-studio): release @siteed/expo-audio-studio@2.18.4 ([d93ceae](https://github.com/deeeed/expo-audio-stream/commit/d93ceae943c98322d34d38f5e76f2da91bd739a2))
11
28
  ## [2.18.4] - 2026-02-16
12
29
  ### Changed
13
30
  - chore(expo-audio-studio): release @siteed/expo-audio-studio@2.18.3 ([61d58e5](https://github.com/deeeed/expo-audio-stream/commit/61d58e57fd624182aef9983745bf82f584ffc2c7))
@@ -390,7 +407,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
390
407
  - Feature: Audio features extraction during recording.
391
408
  - Feature: Consistent WAV PCM recording format across all platforms.
392
409
 
393
- [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.4...HEAD
410
+ [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.5...HEAD
411
+ [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
394
412
  [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
395
413
  [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
396
414
  [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
@@ -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
@@ -116,6 +117,7 @@ class AudioRecorderManager(
116
117
  private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
117
118
  private var audioFocusRequest: Any? = null // Type Any to handle both old and new APIs
118
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
119
121
  private var telephonyManager: TelephonyManager? = null
120
122
  get() {
121
123
  if (field == null) {
@@ -387,81 +389,102 @@ class AudioRecorderManager(
387
389
  val isRecording: Boolean
388
390
  get() = _isRecording.get()
389
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
+
390
457
  private fun initializePhoneStateListener() {
391
458
  try {
392
459
  LogUtils.d(CLASS_NAME, "Initializing phone state listener...")
393
-
460
+
394
461
  if (permissionUtils.checkPhoneStatePermission()) {
395
462
  LogUtils.d(CLASS_NAME, "Phone state permission granted")
396
-
397
- phoneStateListener = object : PhoneStateListener() {
398
- override fun onCallStateChanged(state: Int, phoneNumber: String?) {
399
- val stateStr = when (state) {
400
- TelephonyManager.CALL_STATE_RINGING -> "RINGING"
401
- TelephonyManager.CALL_STATE_OFFHOOK -> "OFFHOOK"
402
- TelephonyManager.CALL_STATE_IDLE -> "IDLE"
403
- else -> "UNKNOWN"
404
- }
405
- LogUtils.d(CLASS_NAME, "Phone state changed to: $stateStr")
406
-
407
- when (state) {
408
- TelephonyManager.CALL_STATE_RINGING,
409
- TelephonyManager.CALL_STATE_OFFHOOK -> {
410
- if (_isRecording.get() && !isPaused.get()) {
411
- LogUtils.d(CLASS_NAME, "Pausing recording due to incoming/ongoing call")
412
- mainHandler.post {
413
- pauseRecording(object : Promise {
414
- override fun resolve(value: Any?) {
415
- LogUtils.d(CLASS_NAME, "Successfully paused recording due to call")
416
- eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
417
- "reason" to "phoneCall",
418
- "isPaused" to true
419
- ))
420
- }
421
- override fun reject(code: String, message: String?, cause: Throwable?) {
422
- LogUtils.e(CLASS_NAME, "Failed to pause recording on phone call", cause)
423
- }
424
- })
425
- }
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)
426
472
  }
427
473
  }
428
- TelephonyManager.CALL_STATE_IDLE -> {
429
- if (_isRecording.get() && isPaused.get()) {
430
- val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
431
- LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: $autoResume)")
432
- if (autoResume) {
433
- mainHandler.post {
434
- resumeRecording(object : Promise {
435
- override fun resolve(value: Any?) {
436
- LogUtils.d(CLASS_NAME, "Successfully resumed recording after call")
437
- eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
438
- "reason" to "phoneCallEnded",
439
- "isPaused" to false
440
- ))
441
- }
442
- override fun reject(code: String, message: String?, cause: Throwable?) {
443
- LogUtils.e(CLASS_NAME, "Failed to resume recording after phone call", cause)
444
- }
445
- })
446
- }
447
- } else {
448
- LogUtils.d(CLASS_NAME, "Auto-resume disabled, staying paused")
449
- eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
450
- "reason" to "phoneCallEnded",
451
- "isPaused" to true
452
- ))
453
- }
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)
454
483
  }
455
484
  }
485
+ localTelephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
486
+ LogUtils.d(CLASS_NAME, "Successfully registered PhoneStateListener (legacy)")
456
487
  }
457
- }
458
- }
459
-
460
- val localTelephonyManager = telephonyManager
461
- if (localTelephonyManager != null) {
462
- try {
463
- localTelephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
464
- LogUtils.d(CLASS_NAME, "Successfully registered phone state listener")
465
488
  } catch (e: SecurityException) {
466
489
  LogUtils.w(CLASS_NAME, "Missing permission for phone state listener: ${e.message}")
467
490
  } catch (e: Exception) {
@@ -478,6 +501,30 @@ class AudioRecorderManager(
478
501
  }
479
502
  }
480
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
+
481
528
 
482
529
  @RequiresApi(Build.VERSION_CODES.R)
483
530
  fun startRecording(options: Map<String, Any?>, promise: Promise) {
@@ -594,11 +641,7 @@ class AudioRecorderManager(
594
641
 
595
642
  } catch (e: Exception) {
596
643
  releaseAudioFocus()
597
- try {
598
- telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
599
- } catch (e: Exception) {
600
- LogUtils.w(CLASS_NAME, "Failed to unregister phone state listener: ${e.message}")
601
- }
644
+ unregisterPhoneStateListener()
602
645
  promise.reject("UNEXPECTED_ERROR", "Unexpected error: ${e.message}", e)
603
646
  }
604
647
  }
@@ -860,7 +903,7 @@ class AudioRecorderManager(
860
903
  LogUtils.d(CLASS_NAME, "Skipping primary file creation - primary output is disabled")
861
904
  }
862
905
 
863
- if (recordingConfig.showNotification) {
906
+ if (recordingConfig.showNotification && enableBackgroundAudio) {
864
907
  notificationManager.initialize(recordingConfig)
865
908
  notificationManager.startUpdates(System.currentTimeMillis())
866
909
  AudioRecordingService.startService(context)
@@ -920,8 +963,8 @@ class AudioRecorderManager(
920
963
 
921
964
  recordingThread = Thread { recordingProcess() }.apply { start() }
922
965
 
923
- // Start service if keepAwake is true, regardless of notification settings
924
- if (recordingConfig.keepAwake) {
966
+ // Start service if keepAwake is true, but only if background audio is enabled (#288)
967
+ if (recordingConfig.keepAwake && enableBackgroundAudio) {
925
968
  AudioRecordingService.startService(context)
926
969
  }
927
970
 
@@ -1693,6 +1736,7 @@ class AudioRecorderManager(
1693
1736
 
1694
1737
  releaseWakeLock()
1695
1738
  releaseAudioFocus()
1739
+ unregisterPhoneStateListener()
1696
1740
  audioRecord?.release()
1697
1741
  audioRecord = null
1698
1742
 
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"extractMelSpectrogram.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractMelSpectrogram.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions. The web implementation is a placeholder.\n */\n\nimport { ExpoAudioStreamModule } from '..'\nimport { isWeb } from '../constants'\nimport {\n ExtractMelSpectrogramOptions,\n MelSpectrogram,\n} from './AudioAnalysis.types'\nimport {\n processAudioBuffer,\n ProcessedAudioData,\n} from '../utils/audioProcessing'\n\n/**\n * Extracts a mel spectrogram from audio data\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The iOS implementation will throw an \"UNSUPPORTED_PLATFORM\" error.\n * The web implementation is a placeholder that returns dummy data.\n */\nexport async function extractMelSpectrogram(\n options: ExtractMelSpectrogramOptions\n): Promise<MelSpectrogram> {\n const {\n fileUri,\n arrayBuffer,\n windowSizeMs,\n hopLengthMs,\n nMels,\n fMin = 0,\n fMax,\n windowType = 'hann',\n normalize = false,\n logScale = true,\n decodingOptions,\n startTimeMs,\n endTimeMs,\n logger,\n } = options\n\n if (isWeb) {\n // Create audio context\n const audioContext = new (window.AudioContext ||\n (window as any).webkitAudioContext)()\n\n try {\n // Process audio data using the existing utility\n const processedAudio: ProcessedAudioData = await processAudioBuffer(\n {\n arrayBuffer,\n fileUri,\n targetSampleRate:\n decodingOptions?.targetSampleRate || 16000,\n targetChannels: decodingOptions?.targetChannels || 1,\n normalizeAudio: decodingOptions?.normalizeAudio ?? false,\n startTimeMs,\n endTimeMs,\n audioContext,\n logger: options.logger,\n }\n )\n\n // Calculate window and hop size in samples\n const sampleRate = processedAudio.sampleRate\n const windowSize = Math.floor((windowSizeMs * sampleRate) / 1000)\n const hopLength = Math.floor((hopLengthMs * sampleRate) / 1000)\n const maxFreq = fMax || sampleRate / 2\n\n // Extract the mel spectrogram from the processed audio\n const spectrogram = computeMelSpectrogram(\n processedAudio.channelData,\n sampleRate,\n nMels,\n windowSize,\n hopLength,\n fMin,\n maxFreq,\n windowType,\n normalize,\n logScale\n )\n\n const timeSteps = spectrogram.length\n\n return {\n spectrogram,\n sampleRate,\n nMels,\n timeSteps,\n durationMs: processedAudio.durationMs,\n }\n } catch (error) {\n logger?.error('Error extracting mel spectrogram:', error)\n throw error\n } finally {\n // Close the audio context\n await audioContext.close()\n }\n }\n return ExpoAudioStreamModule.extractMelSpectrogram(options)\n}\n\n/**\n * Computes a mel spectrogram from audio data\n *\n * @experimental This is a placeholder implementation that returns dummy data.\n * The actual implementation will be added in a future version.\n */\nfunction computeMelSpectrogram(\n audioData: Float32Array,\n sampleRate: number,\n nMels: number,\n windowSize: number,\n hopLength: number,\n fMin: number,\n fMax: number,\n windowType: 'hann' | 'hamming',\n normalize: boolean,\n logScale: boolean\n): number[][] {\n // Placeholder for the actual implementation\n // This would include:\n // 1. Windowing the audio data using the specified window type\n // 2. Computing the STFT (Short-Time Fourier Transform)\n // 3. Converting to power spectrogram\n // 4. Applying mel filterbanks\n // 5. Taking the logarithm if logScale is true\n // 6. Normalizing if normalize is true\n\n // For now, return a dummy implementation\n const numFrames =\n Math.floor((audioData.length - windowSize) / hopLength) + 1\n const spectrogram: number[][] = []\n\n // Create dummy mel spectrogram data\n for (let i = 0; i < numFrames; i++) {\n spectrogram.push(Array(nMels).fill(0))\n }\n\n return spectrogram\n}\n"]}
1
+ {"version":3,"file":"extractMelSpectrogram.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractMelSpectrogram.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions. The web implementation is a placeholder.\n */\n\nimport { ExpoAudioStreamModule } from '..'\nimport { isWeb } from '../constants'\nimport {\n ExtractMelSpectrogramOptions,\n MelSpectrogram,\n} from './AudioAnalysis.types'\nimport {\n processAudioBuffer,\n ProcessedAudioData,\n} from '../utils/audioProcessing'\nimport { cleanNativeOptions } from '../utils/cleanNativeOptions'\n\n/**\n * Extracts a mel spectrogram from audio data\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The iOS implementation will throw an \"UNSUPPORTED_PLATFORM\" error.\n * The web implementation is a placeholder that returns dummy data.\n */\nexport async function extractMelSpectrogram(\n options: ExtractMelSpectrogramOptions\n): Promise<MelSpectrogram> {\n const {\n fileUri,\n arrayBuffer,\n windowSizeMs,\n hopLengthMs,\n nMels,\n fMin = 0,\n fMax,\n windowType = 'hann',\n normalize = false,\n logScale = true,\n decodingOptions,\n startTimeMs,\n endTimeMs,\n logger,\n } = options\n\n if (isWeb) {\n // Create audio context\n const audioContext = new (window.AudioContext ||\n (window as any).webkitAudioContext)()\n\n try {\n // Process audio data using the existing utility\n const processedAudio: ProcessedAudioData = await processAudioBuffer(\n {\n arrayBuffer,\n fileUri,\n targetSampleRate:\n decodingOptions?.targetSampleRate || 16000,\n targetChannels: decodingOptions?.targetChannels || 1,\n normalizeAudio: decodingOptions?.normalizeAudio ?? false,\n startTimeMs,\n endTimeMs,\n audioContext,\n logger: options.logger,\n }\n )\n\n // Calculate window and hop size in samples\n const sampleRate = processedAudio.sampleRate\n const windowSize = Math.floor((windowSizeMs * sampleRate) / 1000)\n const hopLength = Math.floor((hopLengthMs * sampleRate) / 1000)\n const maxFreq = fMax || sampleRate / 2\n\n // Extract the mel spectrogram from the processed audio\n const spectrogram = computeMelSpectrogram(\n processedAudio.channelData,\n sampleRate,\n nMels,\n windowSize,\n hopLength,\n fMin,\n maxFreq,\n windowType,\n normalize,\n logScale\n )\n\n const timeSteps = spectrogram.length\n\n return {\n spectrogram,\n sampleRate,\n nMels,\n timeSteps,\n durationMs: processedAudio.durationMs,\n }\n } catch (error) {\n logger?.error('Error extracting mel spectrogram:', error)\n throw error\n } finally {\n // Close the audio context\n await audioContext.close()\n }\n }\n // Strip logger/arrayBuffer (non-serializable) then clean undefined values\n // to avoid Android \"Cannot convert '[object Object]' to Kotlin type\" crash\n const {\n logger: _logger,\n arrayBuffer: _arrayBuffer,\n ...nativeOptions\n } = options\n return ExpoAudioStreamModule.extractMelSpectrogram(\n cleanNativeOptions(nativeOptions)\n )\n}\n\n/**\n * Computes a mel spectrogram from audio data\n *\n * @experimental This is a placeholder implementation that returns dummy data.\n * The actual implementation will be added in a future version.\n */\nfunction computeMelSpectrogram(\n audioData: Float32Array,\n sampleRate: number,\n nMels: number,\n windowSize: number,\n hopLength: number,\n fMin: number,\n fMax: number,\n windowType: 'hann' | 'hamming',\n normalize: boolean,\n logScale: boolean\n): number[][] {\n // Placeholder for the actual implementation\n // This would include:\n // 1. Windowing the audio data using the specified window type\n // 2. Computing the STFT (Short-Time Fourier Transform)\n // 3. Converting to power spectrogram\n // 4. Applying mel filterbanks\n // 5. Taking the logarithm if logScale is true\n // 6. Normalizing if normalize is true\n\n // For now, return a dummy implementation\n const numFrames =\n Math.floor((audioData.length - windowSize) / hopLength) + 1\n const spectrogram: number[][] = []\n\n // Create dummy mel spectrogram data\n for (let i = 0; i < numFrames; i++) {\n spectrogram.push(Array(nMels).fill(0))\n }\n\n return spectrogram\n}\n"]}
@@ -7,6 +7,7 @@ exports.trimAudio = trimAudio;
7
7
  exports.trimAudioSimple = trimAudioSimple;
8
8
  const expo_modules_core_1 = require("expo-modules-core");
9
9
  const ExpoAudioStreamModule_1 = __importDefault(require("./ExpoAudioStreamModule"));
10
+ const cleanNativeOptions_1 = require("./utils/cleanNativeOptions");
10
11
  // Create a single emitter instance
11
12
  const emitter = new expo_modules_core_1.LegacyEventEmitter(ExpoAudioStreamModule_1.default);
12
13
  /**
@@ -48,7 +49,8 @@ async function trimAudio(options, progressCallback) {
48
49
  });
49
50
  }
50
51
  try {
51
- const result = await ExpoAudioStreamModule_1.default.trimAudio(options);
52
+ // Clean non-serializable/undefined values to avoid Android Kotlin bridge crash
53
+ const result = await ExpoAudioStreamModule_1.default.trimAudio((0, cleanNativeOptions_1.cleanNativeOptions)(options));
52
54
  return result;
53
55
  }
54
56
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"trimAudio.js","sourceRoot":"","sources":["../../src/trimAudio.ts"],"names":[],"mappings":";;;;;AAuBA,8BAiDC;AAYD,0CAKC;AAzFD,yDAA8E;AAO9E,oFAA2D;AAE3D,mCAAmC;AACnC,MAAM,OAAO,GAAG,IAAI,sCAAkB,CAAC,+BAAqB,CAAC,CAAA;AAE7D;;;;;;;;;;GAUG;AACI,KAAK,UAAU,SAAS,CAC3B,OAAyB,EACzB,gBAAqD;IAErD,aAAa;IACb,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;IAC1C,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,QAAQ,CAAA;IACrC,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACpB,IACI,OAAO,CAAC,WAAW,KAAK,SAAS;YACjC,OAAO,CAAC,SAAS,KAAK,SAAS,EACjC,CAAC;YACC,MAAM,IAAI,KAAK,CACX,0EAA0E,CAC7E,CAAA;QACL,CAAC;IACL,CAAC;SAAM,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9C,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CACX,gEAAgE,CACnE,CAAA;QACL,CAAC;IACL,CAAC;SAAM,CAAC;QACJ,MAAM,IAAI,KAAK,CACX,iBAAiB,IAAI,yCAAyC,CACjE,CAAA;IACL,CAAC;IAED,yDAAyD;IACzD,IAAI,YAA2C,CAAA;IAC/C,IAAI,gBAAgB,EAAE,CAAC;QACnB,YAAY,GAAG,OAAO,CAAC,WAAW,CAC9B,cAAc,EACd,CAAC,KAAwB,EAAE,EAAE;YACzB,gBAAgB,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC,CACJ,CAAA;IACL,CAAC;IAED,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,+BAAqB,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;QAC7D,OAAO,MAAM,CAAA;IACjB,CAAC;YAAS,CAAC;QACP,IAAI,YAAY,EAAE,CAAC;YACf,YAAY,CAAC,MAAM,EAAE,CAAA;QACzB,CAAC;IACL,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,eAAe,CACjC,OAAyB;IAEzB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;IACvC,OAAO,MAAM,CAAC,GAAG,CAAA;AACrB,CAAC","sourcesContent":["import { LegacyEventEmitter, type EventSubscription } from 'expo-modules-core'\n\nimport {\n TrimAudioOptions,\n TrimAudioResult,\n TrimProgressEvent,\n} from './ExpoAudioStream.types'\nimport ExpoAudioStreamModule from './ExpoAudioStreamModule'\n\n// Create a single emitter instance\nconst emitter = new LegacyEventEmitter(ExpoAudioStreamModule)\n\n/**\n * Trims an audio file based on the provided options.\n *\n * @experimental This API is experimental and not fully optimized for production use.\n * Performance may vary based on file size and device capabilities.\n * Future versions may include breaking changes.\n *\n * @param options Configuration options for the trimming operation\n * @param progressCallback Optional callback to receive progress updates\n * @returns Promise resolving to the trimmed audio file information, including processing time\n */\nexport async function trimAudio(\n options: TrimAudioOptions,\n progressCallback?: (event: TrimProgressEvent) => void\n): Promise<TrimAudioResult> {\n // Validation\n if (!options.fileUri) {\n throw new Error('fileUri is required')\n }\n const mode = options.mode ?? 'single'\n if (mode === 'single') {\n if (\n options.startTimeMs === undefined &&\n options.endTimeMs === undefined\n ) {\n throw new Error(\n 'At least one of startTimeMs or endTimeMs must be provided in single mode'\n )\n }\n } else if (mode === 'keep' || mode === 'remove') {\n if (!options.ranges || options.ranges.length === 0) {\n throw new Error(\n 'ranges must be provided and non-empty for keep or remove modes'\n )\n }\n } else {\n throw new Error(\n `Invalid mode: ${mode}. Must be 'single', 'keep', or 'remove'`\n )\n }\n\n // Set up progress event listener if callback is provided\n let subscription: EventSubscription | undefined\n if (progressCallback) {\n subscription = emitter.addListener(\n 'TrimProgress',\n (event: TrimProgressEvent) => {\n progressCallback(event)\n }\n )\n }\n\n try {\n const result = await ExpoAudioStreamModule.trimAudio(options)\n return result\n } finally {\n if (subscription) {\n subscription.remove()\n }\n }\n}\n\n/**\n * Simplified version of trimAudio that returns only the URI of the trimmed file.\n *\n * @experimental This API is experimental and not fully optimized for production use.\n * Performance may vary based on file size and device capabilities.\n * Future versions may include breaking changes.\n *\n * @param options Configuration options for the trimming operation\n * @returns Promise resolving to the URI of the trimmed audio file\n */\nexport async function trimAudioSimple(\n options: TrimAudioOptions\n): Promise<string> {\n const result = await trimAudio(options)\n return result.uri\n}\n"]}
1
+ {"version":3,"file":"trimAudio.js","sourceRoot":"","sources":["../../src/trimAudio.ts"],"names":[],"mappings":";;;;;AAwBA,8BAoDC;AAYD,0CAKC;AA7FD,yDAA8E;AAO9E,oFAA2D;AAC3D,mEAA+D;AAE/D,mCAAmC;AACnC,MAAM,OAAO,GAAG,IAAI,sCAAkB,CAAC,+BAAqB,CAAC,CAAA;AAE7D;;;;;;;;;;GAUG;AACI,KAAK,UAAU,SAAS,CAC3B,OAAyB,EACzB,gBAAqD;IAErD,aAAa;IACb,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;IAC1C,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,QAAQ,CAAA;IACrC,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACpB,IACI,OAAO,CAAC,WAAW,KAAK,SAAS;YACjC,OAAO,CAAC,SAAS,KAAK,SAAS,EACjC,CAAC;YACC,MAAM,IAAI,KAAK,CACX,0EAA0E,CAC7E,CAAA;QACL,CAAC;IACL,CAAC;SAAM,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9C,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CACX,gEAAgE,CACnE,CAAA;QACL,CAAC;IACL,CAAC;SAAM,CAAC;QACJ,MAAM,IAAI,KAAK,CACX,iBAAiB,IAAI,yCAAyC,CACjE,CAAA;IACL,CAAC;IAED,yDAAyD;IACzD,IAAI,YAA2C,CAAA;IAC/C,IAAI,gBAAgB,EAAE,CAAC;QACnB,YAAY,GAAG,OAAO,CAAC,WAAW,CAC9B,cAAc,EACd,CAAC,KAAwB,EAAE,EAAE;YACzB,gBAAgB,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC,CACJ,CAAA;IACL,CAAC;IAED,IAAI,CAAC;QACD,+EAA+E;QAC/E,MAAM,MAAM,GAAG,MAAM,+BAAqB,CAAC,SAAS,CAChD,IAAA,uCAAkB,EAAC,OAAO,CAAC,CAC9B,CAAA;QACD,OAAO,MAAM,CAAA;IACjB,CAAC;YAAS,CAAC;QACP,IAAI,YAAY,EAAE,CAAC;YACf,YAAY,CAAC,MAAM,EAAE,CAAA;QACzB,CAAC;IACL,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,eAAe,CACjC,OAAyB;IAEzB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;IACvC,OAAO,MAAM,CAAC,GAAG,CAAA;AACrB,CAAC","sourcesContent":["import { LegacyEventEmitter, type EventSubscription } from 'expo-modules-core'\n\nimport {\n TrimAudioOptions,\n TrimAudioResult,\n TrimProgressEvent,\n} from './ExpoAudioStream.types'\nimport ExpoAudioStreamModule from './ExpoAudioStreamModule'\nimport { cleanNativeOptions } from './utils/cleanNativeOptions'\n\n// Create a single emitter instance\nconst emitter = new LegacyEventEmitter(ExpoAudioStreamModule)\n\n/**\n * Trims an audio file based on the provided options.\n *\n * @experimental This API is experimental and not fully optimized for production use.\n * Performance may vary based on file size and device capabilities.\n * Future versions may include breaking changes.\n *\n * @param options Configuration options for the trimming operation\n * @param progressCallback Optional callback to receive progress updates\n * @returns Promise resolving to the trimmed audio file information, including processing time\n */\nexport async function trimAudio(\n options: TrimAudioOptions,\n progressCallback?: (event: TrimProgressEvent) => void\n): Promise<TrimAudioResult> {\n // Validation\n if (!options.fileUri) {\n throw new Error('fileUri is required')\n }\n const mode = options.mode ?? 'single'\n if (mode === 'single') {\n if (\n options.startTimeMs === undefined &&\n options.endTimeMs === undefined\n ) {\n throw new Error(\n 'At least one of startTimeMs or endTimeMs must be provided in single mode'\n )\n }\n } else if (mode === 'keep' || mode === 'remove') {\n if (!options.ranges || options.ranges.length === 0) {\n throw new Error(\n 'ranges must be provided and non-empty for keep or remove modes'\n )\n }\n } else {\n throw new Error(\n `Invalid mode: ${mode}. Must be 'single', 'keep', or 'remove'`\n )\n }\n\n // Set up progress event listener if callback is provided\n let subscription: EventSubscription | undefined\n if (progressCallback) {\n subscription = emitter.addListener(\n 'TrimProgress',\n (event: TrimProgressEvent) => {\n progressCallback(event)\n }\n )\n }\n\n try {\n // Clean non-serializable/undefined values to avoid Android Kotlin bridge crash\n const result = await ExpoAudioStreamModule.trimAudio(\n cleanNativeOptions(options)\n )\n return result\n } finally {\n if (subscription) {\n subscription.remove()\n }\n }\n}\n\n/**\n * Simplified version of trimAudio that returns only the URI of the trimmed file.\n *\n * @experimental This API is experimental and not fully optimized for production use.\n * Performance may vary based on file size and device capabilities.\n * Future versions may include breaking changes.\n *\n * @param options Configuration options for the trimming operation\n * @returns Promise resolving to the URI of the trimmed audio file\n */\nexport async function trimAudioSimple(\n options: TrimAudioOptions\n): Promise<string> {\n const result = await trimAudio(options)\n return result.uri\n}\n"]}