@siteed/audio-studio 3.0.5 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/README.md +108 -41
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +190 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +29 -83
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +17 -1
- package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +186 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioProcessor.kt +473 -380
- package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +74 -22
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +86 -19
- package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +174 -212
- package/android/src/main/java/net/siteed/audiostudio/EventSender.kt +6 -0
- package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +37 -0
- package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +28 -0
- package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +49 -0
- package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractPreview.js +92 -15
- package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractPreviewBars.js +134 -0
- package/build/cjs/AudioAnalysis/extractPreviewBars.js.map +1 -0
- package/build/cjs/AudioStudio.types.js.map +1 -1
- package/build/cjs/errors/AudioExtractionError.js +127 -0
- package/build/cjs/errors/AudioExtractionError.js.map +1 -0
- package/build/cjs/index.js +6 -1
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +36 -18
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/esm/AudioAnalysis/extractPreview.js +92 -15
- package/build/esm/AudioAnalysis/extractPreview.js.map +1 -1
- package/build/esm/AudioAnalysis/extractPreviewBars.js +128 -0
- package/build/esm/AudioAnalysis/extractPreviewBars.js.map +1 -0
- package/build/esm/AudioStudio.types.js.map +1 -1
- package/build/esm/errors/AudioExtractionError.js +122 -0
- package/build/esm/errors/AudioExtractionError.js.map +1 -0
- package/build/esm/index.js +2 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/useAudioRecorder.js +36 -18
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +79 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractPreview.d.ts +2 -2
- package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractPreviewBars.d.ts +12 -0
- package/build/types/AudioAnalysis/extractPreviewBars.d.ts.map +1 -0
- package/build/types/AudioStudio.types.d.ts +14 -1
- package/build/types/AudioStudio.types.d.ts.map +1 -1
- package/build/types/errors/AudioExtractionError.d.ts +24 -0
- package/build/types/errors/AudioExtractionError.d.ts.map +1 -0
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/ios/AudioProcessor.swift +99 -0
- package/ios/AudioStreamManager.swift +79 -15
- package/ios/AudioStudioModule.swift +63 -0
- package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +41 -1
- package/package.json +7 -7
- package/src/AudioAnalysis/AudioAnalysis.types.ts +82 -0
- package/src/AudioAnalysis/extractPreview.ts +118 -17
- package/src/AudioAnalysis/extractPreviewBars.ts +193 -0
- package/src/AudioStudio.types.ts +15 -1
- package/src/errors/AudioExtractionError.ts +167 -0
- package/src/index.ts +10 -0
- package/src/useAudioRecorder.tsx +36 -14
|
@@ -118,6 +118,7 @@ class AudioRecorderManager(
|
|
|
118
118
|
private var audioFocusRequest: Any? = null // Type Any to handle both old and new APIs
|
|
119
119
|
private var phoneStateListener: PhoneStateListener? = null
|
|
120
120
|
private var telephonyCallback: Any? = null // TelephonyCallback for API 31+, typed as Any to avoid class verification issues on older APIs
|
|
121
|
+
private val pausedBySystemInterruption = AtomicBoolean(false)
|
|
121
122
|
private var telephonyManager: TelephonyManager? = null
|
|
122
123
|
get() {
|
|
123
124
|
if (field == null) {
|
|
@@ -408,7 +409,7 @@ class AudioRecorderManager(
|
|
|
408
409
|
if (_isRecording.get() && !isPaused.get()) {
|
|
409
410
|
LogUtils.d(CLASS_NAME, "Pausing recording due to incoming/ongoing call")
|
|
410
411
|
mainHandler.post {
|
|
411
|
-
|
|
412
|
+
pauseRecordingForSystemInterruption(object : Promise {
|
|
412
413
|
override fun resolve(value: Any?) {
|
|
413
414
|
LogUtils.d(CLASS_NAME, "Successfully paused recording due to call")
|
|
414
415
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
@@ -426,8 +427,14 @@ class AudioRecorderManager(
|
|
|
426
427
|
TelephonyManager.CALL_STATE_IDLE -> {
|
|
427
428
|
if (_isRecording.get() && isPaused.get()) {
|
|
428
429
|
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
429
|
-
|
|
430
|
-
|
|
430
|
+
val shouldAutoResume = InterruptionAutoResumePolicy.shouldAutoResume(
|
|
431
|
+
autoResumeAfterInterruption = autoResume,
|
|
432
|
+
isRecording = _isRecording.get(),
|
|
433
|
+
isPaused = isPaused.get(),
|
|
434
|
+
pausedBySystemInterruption = pausedBySystemInterruption.get()
|
|
435
|
+
)
|
|
436
|
+
LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: $autoResume, pausedBySystemInterruption: ${pausedBySystemInterruption.get()})")
|
|
437
|
+
if (shouldAutoResume) {
|
|
431
438
|
mainHandler.post {
|
|
432
439
|
resumeRecording(object : Promise {
|
|
433
440
|
override fun resolve(value: Any?) {
|
|
@@ -443,7 +450,7 @@ class AudioRecorderManager(
|
|
|
443
450
|
})
|
|
444
451
|
}
|
|
445
452
|
} else {
|
|
446
|
-
LogUtils.d(CLASS_NAME, "Auto-resume
|
|
453
|
+
LogUtils.d(CLASS_NAME, "Auto-resume not permitted, staying paused")
|
|
447
454
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
448
455
|
"reason" to "phoneCallEnded",
|
|
449
456
|
"isPaused" to true
|
|
@@ -954,6 +961,7 @@ class AudioRecorderManager(
|
|
|
954
961
|
|
|
955
962
|
audioRecord?.startRecording()
|
|
956
963
|
isPaused.set(false)
|
|
964
|
+
pausedBySystemInterruption.set(false)
|
|
957
965
|
isFirstChunk = true
|
|
958
966
|
recordingStartTime = System.currentTimeMillis()
|
|
959
967
|
|
|
@@ -1174,6 +1182,7 @@ class AudioRecorderManager(
|
|
|
1174
1182
|
// Reset the timing variables
|
|
1175
1183
|
_isRecording.set(false)
|
|
1176
1184
|
isPaused.set(false)
|
|
1185
|
+
pausedBySystemInterruption.set(false)
|
|
1177
1186
|
totalRecordedTime = 0
|
|
1178
1187
|
pausedDuration = 0
|
|
1179
1188
|
} catch (e: Exception) {
|
|
@@ -1258,6 +1267,7 @@ class AudioRecorderManager(
|
|
|
1258
1267
|
}
|
|
1259
1268
|
|
|
1260
1269
|
LogUtils.d(CLASS_NAME, "⏺️ Recording resumed successfully")
|
|
1270
|
+
pausedBySystemInterruption.set(false)
|
|
1261
1271
|
promise.resolve("Recording resumed")
|
|
1262
1272
|
} catch (e: Exception) {
|
|
1263
1273
|
LogUtils.e(CLASS_NAME, "⏺️ Failed to resume recording: ${e.message}", e)
|
|
@@ -1267,12 +1277,21 @@ class AudioRecorderManager(
|
|
|
1267
1277
|
}
|
|
1268
1278
|
|
|
1269
1279
|
fun pauseRecording(promise: Promise) {
|
|
1280
|
+
pauseRecording(promise, isSystemInterruption = false)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
private fun pauseRecordingForSystemInterruption(promise: Promise) {
|
|
1284
|
+
pauseRecording(promise, isSystemInterruption = true)
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
private fun pauseRecording(promise: Promise, isSystemInterruption: Boolean) {
|
|
1270
1288
|
if (_isRecording.get() && !isPaused.get()) {
|
|
1271
1289
|
audioRecord?.stop()
|
|
1272
1290
|
compressedRecorder?.pause()
|
|
1273
1291
|
|
|
1274
1292
|
lastPauseTime = System.currentTimeMillis()
|
|
1275
1293
|
isPaused.set(true)
|
|
1294
|
+
pausedBySystemInterruption.set(isSystemInterruption)
|
|
1276
1295
|
|
|
1277
1296
|
if (recordingConfig.showNotification) {
|
|
1278
1297
|
notificationManager.pauseUpdates()
|
|
@@ -1417,17 +1436,7 @@ class AudioRecorderManager(
|
|
|
1417
1436
|
"audioManager.mode=${audioManager.mode}, " +
|
|
1418
1437
|
"audioManager.isBluetoothScoOn=${audioManager.isBluetoothScoOn}")
|
|
1419
1438
|
|
|
1420
|
-
|
|
1421
|
-
if (callState == TelephonyManager.CALL_STATE_RINGING ||
|
|
1422
|
-
callState == TelephonyManager.CALL_STATE_OFFHOOK) {
|
|
1423
|
-
return true
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// Only check audio manager mode as secondary indicator
|
|
1427
|
-
return audioManager.mode == AudioManager.MODE_IN_CALL ||
|
|
1428
|
-
audioManager.mode == AudioManager.MODE_IN_COMMUNICATION
|
|
1429
|
-
|
|
1430
|
-
// Remove audioManager.isBluetoothScoOn check as it can be erroneously true after disconnection
|
|
1439
|
+
return AndroidCallState.isOngoingCall(callState, audioManager.mode)
|
|
1431
1440
|
} catch (e: Exception) {
|
|
1432
1441
|
LogUtils.e(CLASS_NAME, "Error checking call state: ${e.message}")
|
|
1433
1442
|
return false
|
|
@@ -1757,6 +1766,7 @@ class AudioRecorderManager(
|
|
|
1757
1766
|
|
|
1758
1767
|
_isRecording.set(false)
|
|
1759
1768
|
isPaused.set(false)
|
|
1769
|
+
pausedBySystemInterruption.set(false)
|
|
1760
1770
|
isPrepared = false // Reset prepared state
|
|
1761
1771
|
|
|
1762
1772
|
if (::recordingConfig.isInitialized && recordingConfig.showNotification) {
|
|
@@ -1919,9 +1929,8 @@ class AudioRecorderManager(
|
|
|
1919
1929
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
1920
1930
|
if (_isRecording.get() && !isPaused.get()) {
|
|
1921
1931
|
mainHandler.post {
|
|
1922
|
-
|
|
1932
|
+
pauseRecordingForSystemInterruption(object : Promise {
|
|
1923
1933
|
override fun resolve(value: Any?) {
|
|
1924
|
-
isPaused.set(true)
|
|
1925
1934
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1926
1935
|
"reason" to "audioFocusLoss",
|
|
1927
1936
|
"isPaused" to true
|
|
@@ -1936,7 +1945,12 @@ class AudioRecorderManager(
|
|
|
1936
1945
|
}
|
|
1937
1946
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
1938
1947
|
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
1939
|
-
if (
|
|
1948
|
+
if (InterruptionAutoResumePolicy.shouldAutoResume(
|
|
1949
|
+
autoResumeAfterInterruption = autoResume,
|
|
1950
|
+
isRecording = _isRecording.get(),
|
|
1951
|
+
isPaused = isPaused.get(),
|
|
1952
|
+
pausedBySystemInterruption = pausedBySystemInterruption.get()
|
|
1953
|
+
)) {
|
|
1940
1954
|
mainHandler.post {
|
|
1941
1955
|
resumeRecording(object : Promise {
|
|
1942
1956
|
override fun resolve(value: Any?) {
|
|
@@ -1985,9 +1999,8 @@ class AudioRecorderManager(
|
|
|
1985
1999
|
// Only pause for permanent focus loss (like phone calls)
|
|
1986
2000
|
if (_isRecording.get() && !isPaused.get()) {
|
|
1987
2001
|
mainHandler.post {
|
|
1988
|
-
|
|
2002
|
+
pauseRecordingForSystemInterruption(object : Promise {
|
|
1989
2003
|
override fun resolve(value: Any?) {
|
|
1990
|
-
isPaused.set(true)
|
|
1991
2004
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1992
2005
|
"reason" to "audioFocusLoss",
|
|
1993
2006
|
"isPaused" to true
|
|
@@ -2006,7 +2019,12 @@ class AudioRecorderManager(
|
|
|
2006
2019
|
}
|
|
2007
2020
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
2008
2021
|
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
2009
|
-
if (
|
|
2022
|
+
if (InterruptionAutoResumePolicy.shouldAutoResume(
|
|
2023
|
+
autoResumeAfterInterruption = autoResume,
|
|
2024
|
+
isRecording = _isRecording.get(),
|
|
2025
|
+
isPaused = isPaused.get(),
|
|
2026
|
+
pausedBySystemInterruption = pausedBySystemInterruption.get()
|
|
2027
|
+
)) {
|
|
2010
2028
|
mainHandler.post {
|
|
2011
2029
|
resumeRecording(object : Promise {
|
|
2012
2030
|
override fun resolve(value: Any?) {
|
|
@@ -2168,4 +2186,38 @@ class AudioRecorderManager(
|
|
|
2168
2186
|
return false
|
|
2169
2187
|
}
|
|
2170
2188
|
}
|
|
2171
|
-
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
internal object AndroidCallState {
|
|
2192
|
+
/**
|
|
2193
|
+
* Telephony call state wins when known. AudioManager mode is only a fallback
|
|
2194
|
+
* for unknown state because some Android devices leave it stale after calls.
|
|
2195
|
+
*/
|
|
2196
|
+
fun isOngoingCall(callState: Int?, audioMode: Int): Boolean {
|
|
2197
|
+
return when (callState) {
|
|
2198
|
+
TelephonyManager.CALL_STATE_RINGING,
|
|
2199
|
+
TelephonyManager.CALL_STATE_OFFHOOK -> true
|
|
2200
|
+
TelephonyManager.CALL_STATE_IDLE -> false
|
|
2201
|
+
else -> audioMode == AudioManager.MODE_IN_CALL ||
|
|
2202
|
+
audioMode == AudioManager.MODE_IN_COMMUNICATION
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
internal object InterruptionAutoResumePolicy {
|
|
2208
|
+
/**
|
|
2209
|
+
* Auto-resume is only allowed when the pause was caused by a system interruption.
|
|
2210
|
+
* User-initiated pauses must stay paused even after phone/audio focus interruptions end.
|
|
2211
|
+
*/
|
|
2212
|
+
fun shouldAutoResume(
|
|
2213
|
+
autoResumeAfterInterruption: Boolean,
|
|
2214
|
+
isRecording: Boolean,
|
|
2215
|
+
isPaused: Boolean,
|
|
2216
|
+
pausedBySystemInterruption: Boolean
|
|
2217
|
+
): Boolean {
|
|
2218
|
+
return autoResumeAfterInterruption &&
|
|
2219
|
+
isRecording &&
|
|
2220
|
+
isPaused &&
|
|
2221
|
+
pausedBySystemInterruption
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
@@ -413,7 +413,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
413
413
|
|
|
414
414
|
val progressListener = object : AudioTrimmer.ProgressListener {
|
|
415
415
|
override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
|
|
416
|
-
|
|
416
|
+
safeSendEvent(Constants.TRIM_PROGRESS_EVENT, mapOf(
|
|
417
417
|
"progress" to progress,
|
|
418
418
|
"bytesProcessed" to bytesProcessed,
|
|
419
419
|
"totalBytes" to totalBytes
|
|
@@ -520,7 +520,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
520
520
|
""".trimIndent())
|
|
521
521
|
|
|
522
522
|
// Handle decoding options
|
|
523
|
-
val decodingOptions = options["decodingOptions"] as? Map
|
|
523
|
+
val decodingOptions = options["decodingOptions"] as? Map<*, *>
|
|
524
524
|
LogUtils.d(CLASS_NAME, "Decoding options: $decodingOptions")
|
|
525
525
|
|
|
526
526
|
val config = decodingOptions?.let {
|
|
@@ -676,6 +676,48 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
676
676
|
}
|
|
677
677
|
|
|
678
678
|
|
|
679
|
+
AsyncFunction("extractPreviewBars") { options: Map<String, Any>, promise: Promise ->
|
|
680
|
+
coroutineScope.launch(Dispatchers.IO) {
|
|
681
|
+
try {
|
|
682
|
+
val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
|
|
683
|
+
val numberOfBars = (options["numberOfBars"] as? Number)?.toInt() ?: 100
|
|
684
|
+
val startTimeMs = options["startTimeMs"] as? Number
|
|
685
|
+
val endTimeMs = options["endTimeMs"] as? Number
|
|
686
|
+
|
|
687
|
+
val defaultConfig = DecodingConfig(
|
|
688
|
+
targetSampleRate = null,
|
|
689
|
+
targetChannels = 1,
|
|
690
|
+
targetBitDepth = 16,
|
|
691
|
+
normalizeAudio = false
|
|
692
|
+
)
|
|
693
|
+
val decodingOptionsMap = options["decodingOptions"] as? Map<*, *>
|
|
694
|
+
val config = decodingOptionsMap?.let {
|
|
695
|
+
DecodingConfig(
|
|
696
|
+
targetSampleRate = (it["targetSampleRate"] as? Number)?.toInt(),
|
|
697
|
+
targetChannels = (it["targetChannels"] as? Number)?.toInt(),
|
|
698
|
+
targetBitDepth = (it["targetBitDepth"] as? Number)?.toInt() ?: 16,
|
|
699
|
+
normalizeAudio = (it["normalizeAudio"] as? Boolean) ?: false
|
|
700
|
+
)
|
|
701
|
+
} ?: defaultConfig
|
|
702
|
+
val silenceRmsThreshold = ((decodingOptionsMap?.get("silenceRmsThreshold") as? Number)?.toFloat()) ?: 0.01f
|
|
703
|
+
|
|
704
|
+
val audioData = audioProcessor.loadAudioFromAnyFormat(fileUri, config)
|
|
705
|
+
?: throw IllegalStateException("Failed to load audio data")
|
|
706
|
+
val result = audioProcessor.generatePreviewBars(
|
|
707
|
+
audioData = audioData,
|
|
708
|
+
numberOfBars = numberOfBars,
|
|
709
|
+
startTimeMs = startTimeMs?.toLong(),
|
|
710
|
+
endTimeMs = endTimeMs?.toLong(),
|
|
711
|
+
silenceRmsThreshold = silenceRmsThreshold
|
|
712
|
+
)
|
|
713
|
+
promise.resolve(result)
|
|
714
|
+
} catch (e: Exception) {
|
|
715
|
+
LogUtils.e(CLASS_NAME, "Failed to extract preview bars: ${e.message}", e)
|
|
716
|
+
promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
679
721
|
AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
|
|
680
722
|
// Off the shared executor so other JS calls don't block during
|
|
681
723
|
// multi-second analysis on large files.
|
|
@@ -707,12 +749,13 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
707
749
|
normalizeAudio = false
|
|
708
750
|
)
|
|
709
751
|
|
|
710
|
-
val
|
|
752
|
+
val decodingOptionsMap = options["decodingOptions"] as? Map<*, *>
|
|
753
|
+
val config = decodingOptionsMap?.let {
|
|
711
754
|
DecodingConfig(
|
|
712
|
-
targetSampleRate =
|
|
713
|
-
targetChannels =
|
|
714
|
-
targetBitDepth = (
|
|
715
|
-
normalizeAudio = (
|
|
755
|
+
targetSampleRate = (it["targetSampleRate"] as? Number)?.toInt(),
|
|
756
|
+
targetChannels = (it["targetChannels"] as? Number)?.toInt(),
|
|
757
|
+
targetBitDepth = (it["targetBitDepth"] as? Number)?.toInt() ?: 16,
|
|
758
|
+
normalizeAudio = (it["normalizeAudio"] as? Boolean) ?: false
|
|
716
759
|
)
|
|
717
760
|
} ?: defaultConfig
|
|
718
761
|
|
|
@@ -802,12 +845,12 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
802
845
|
}
|
|
803
846
|
|
|
804
847
|
// Get decoding options
|
|
805
|
-
val decodingOptionsMap = options["decodingOptions"] as? Map
|
|
848
|
+
val decodingOptionsMap = options["decodingOptions"] as? Map<*, *>
|
|
806
849
|
val decodingConfig = if (decodingOptionsMap != null) {
|
|
807
850
|
DecodingConfig(
|
|
808
|
-
targetSampleRate = decodingOptionsMap["targetSampleRate"] as?
|
|
809
|
-
targetChannels = decodingOptionsMap["targetChannels"] as?
|
|
810
|
-
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as?
|
|
851
|
+
targetSampleRate = (decodingOptionsMap["targetSampleRate"] as? Number)?.toInt(),
|
|
852
|
+
targetChannels = (decodingOptionsMap["targetChannels"] as? Number)?.toInt(),
|
|
853
|
+
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Number)?.toInt() ?: 16,
|
|
811
854
|
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
812
855
|
).also {
|
|
813
856
|
LogUtils.d(CLASS_NAME, """
|
|
@@ -989,7 +1032,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
989
1032
|
}
|
|
990
1033
|
|
|
991
1034
|
// Notify JS about the disconnection
|
|
992
|
-
|
|
1035
|
+
safeSendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
993
1036
|
"type" to "deviceDisconnected",
|
|
994
1037
|
"deviceId" to deviceId
|
|
995
1038
|
))
|
|
@@ -1004,7 +1047,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1004
1047
|
audioDeviceManager.onDeviceConnected = { deviceId ->
|
|
1005
1048
|
LogUtils.d(CLASS_NAME, "📱 Device connected: $deviceId")
|
|
1006
1049
|
// Notify JS about the connection
|
|
1007
|
-
|
|
1050
|
+
safeSendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
1008
1051
|
"type" to "deviceConnected",
|
|
1009
1052
|
"deviceId" to deviceId
|
|
1010
1053
|
))
|
|
@@ -1014,7 +1057,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1014
1057
|
audioDeviceManager.onDeviceDisconnected = { deviceId ->
|
|
1015
1058
|
LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
|
|
1016
1059
|
// Notify JS about the disconnection
|
|
1017
|
-
|
|
1060
|
+
safeSendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
1018
1061
|
"type" to "deviceDisconnected",
|
|
1019
1062
|
"deviceId" to deviceId
|
|
1020
1063
|
))
|
|
@@ -1023,6 +1066,18 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1023
1066
|
audioProcessor = AudioProcessor(filesDir)
|
|
1024
1067
|
}
|
|
1025
1068
|
|
|
1069
|
+
private fun safeSendEvent(eventName: String, params: Bundle) {
|
|
1070
|
+
AndroidEventEmitter.safeSend(CLASS_NAME, eventName) {
|
|
1071
|
+
sendEvent(eventName, params)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
private fun safeSendEvent(eventName: String, params: Map<String, Any?>) {
|
|
1076
|
+
AndroidEventEmitter.safeSend(CLASS_NAME, eventName) {
|
|
1077
|
+
sendEvent(eventName, params)
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1026
1081
|
/**
|
|
1027
1082
|
* Handles audio device disconnection based on the recording configuration
|
|
1028
1083
|
*/
|
|
@@ -1055,7 +1110,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1055
1110
|
|
|
1056
1111
|
// Notify JS about fallback
|
|
1057
1112
|
LogUtils.d(CLASS_NAME, "📱 Sending deviceFallback event to JS")
|
|
1058
|
-
|
|
1113
|
+
safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1059
1114
|
"reason" to "deviceFallback",
|
|
1060
1115
|
"isPaused" to false,
|
|
1061
1116
|
"deviceId" to deviceId
|
|
@@ -1070,7 +1125,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1070
1125
|
// Notify AudioRecorderManager to handle device change while paused
|
|
1071
1126
|
audioRecorderManager.handleDeviceChange()
|
|
1072
1127
|
|
|
1073
|
-
|
|
1128
|
+
safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1074
1129
|
"reason" to "deviceSwitchFailed",
|
|
1075
1130
|
"isPaused" to true
|
|
1076
1131
|
))
|
|
@@ -1090,7 +1145,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1090
1145
|
// Notify AudioRecorderManager to handle device change while paused
|
|
1091
1146
|
audioRecorderManager.handleDeviceChange()
|
|
1092
1147
|
|
|
1093
|
-
|
|
1148
|
+
safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1094
1149
|
"reason" to "deviceDisconnected",
|
|
1095
1150
|
"isPaused" to true
|
|
1096
1151
|
))
|
|
@@ -1112,7 +1167,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1112
1167
|
// Notify AudioRecorderManager to handle device change while paused
|
|
1113
1168
|
audioRecorderManager.handleDeviceChange()
|
|
1114
1169
|
|
|
1115
|
-
|
|
1170
|
+
safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1116
1171
|
"reason" to "deviceDisconnected",
|
|
1117
1172
|
"isPaused" to true
|
|
1118
1173
|
))
|
|
@@ -1128,6 +1183,18 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1128
1183
|
|
|
1129
1184
|
override fun sendExpoEvent(eventName: String, params: Bundle) {
|
|
1130
1185
|
LogUtils.d(CLASS_NAME, "Sending event: $eventName")
|
|
1131
|
-
|
|
1186
|
+
safeSendEvent(eventName, params)
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
internal object AndroidEventEmitter {
|
|
1191
|
+
fun safeSend(className: String, eventName: String, send: () -> Unit): Boolean {
|
|
1192
|
+
return try {
|
|
1193
|
+
send()
|
|
1194
|
+
true
|
|
1195
|
+
} catch (e: Exception) {
|
|
1196
|
+
LogUtils.e(className, "Failed to send event $eventName: ${e.message}", e)
|
|
1197
|
+
false
|
|
1198
|
+
}
|
|
1132
1199
|
}
|
|
1133
1200
|
}
|