@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.
- package/CHANGELOG.md +23 -1
- package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +19 -3
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +142 -91
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +1 -0
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +1 -1
- package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +6 -1
- package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractAudioData.js +10 -1
- package/build/cjs/AudioAnalysis/extractAudioData.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractMelSpectrogram.js +5 -1
- package/build/cjs/AudioAnalysis/extractMelSpectrogram.js.map +1 -1
- package/build/cjs/trimAudio.js +3 -1
- package/build/cjs/trimAudio.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +9 -38
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/cjs/utils/cleanNativeOptions.js +22 -0
- package/build/cjs/utils/cleanNativeOptions.js.map +1 -0
- package/build/esm/AudioAnalysis/extractAudioAnalysis.js +6 -1
- package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/esm/AudioAnalysis/extractAudioData.js +10 -1
- package/build/esm/AudioAnalysis/extractAudioData.js.map +1 -1
- package/build/esm/AudioAnalysis/extractMelSpectrogram.js +5 -1
- package/build/esm/AudioAnalysis/extractMelSpectrogram.js.map +1 -1
- package/build/esm/trimAudio.js +3 -1
- package/build/esm/trimAudio.js.map +1 -1
- package/build/esm/useAudioRecorder.js +8 -4
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/esm/utils/cleanNativeOptions.js +19 -0
- package/build/esm/utils/cleanNativeOptions.js.map +1 -0
- package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractAudioData.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts.map +1 -1
- package/build/types/trimAudio.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/build/types/utils/cleanNativeOptions.d.ts +15 -0
- package/build/types/utils/cleanNativeOptions.d.ts.map +1 -0
- package/ios/AudioStreamManager.swift +76 -18
- package/ios/ExpoAudioStreamModule.swift +17 -19
- package/package.json +6 -7
- package/src/AudioAnalysis/extractAudioAnalysis.ts +12 -1
- package/src/AudioAnalysis/extractAudioData.ts +12 -1
- package/src/AudioAnalysis/extractMelSpectrogram.ts +11 -1
- package/src/trimAudio.ts +5 -1
- package/src/useAudioRecorder.tsx +8 -7
- 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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|