@siteed/expo-audio-stream 1.16.0 → 2.0.0
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 +28 -1
- package/README.md +1 -1
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +68 -22
- package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +24 -0
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +836 -386
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +134 -23
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +35 -29
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +236 -96
- package/android/src/main/java/net/siteed/audiostream/FFT.kt +55 -0
- package/android/src/main/java/net/siteed/audiostream/Features.kt +49 -7
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +4 -4
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +55 -47
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +60 -13
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
- package/build/AudioAnalysis/extractAudioAnalysis.js +147 -162
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/ExpoAudioStream.types.d.ts +49 -3
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts +2 -0
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +8 -1
- package/build/ExpoAudioStream.web.js.map +1 -1
- package/build/ExpoAudioStreamModule.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.js +216 -12
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/WebRecorder.web.d.ts +67 -13
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +178 -173
- package/build/WebRecorder.web.js.map +1 -1
- package/build/index.d.ts +3 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/build/useAudioRecorder.d.ts.map +1 -1
- package/build/useAudioRecorder.js +12 -8
- package/build/useAudioRecorder.js.map +1 -1
- package/build/utils/audioProcessing.d.ts +24 -0
- package/build/utils/audioProcessing.d.ts.map +1 -0
- package/build/utils/audioProcessing.js +133 -0
- package/build/utils/audioProcessing.js.map +1 -0
- package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
- package/build/workers/InlineFeaturesExtractor.web.js +692 -175
- package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.js +3 -2
- package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
- package/ios/AudioAnalysisData.swift +51 -16
- package/ios/AudioProcessingHelpers.swift +710 -26
- package/ios/AudioProcessor.swift +334 -185
- package/ios/AudioStreamManager.swift +66 -22
- package/ios/DataPoint.swift +25 -12
- package/ios/DecodingConfig.swift +47 -0
- package/ios/ExpoAudioStreamModule.swift +189 -104
- package/ios/FFT.swift +62 -0
- package/ios/Features.swift +24 -3
- package/ios/RecordingSettings.swift +9 -7
- package/package.json +2 -1
- package/plugin/build/index.d.ts +2 -0
- package/plugin/build/index.js +10 -3
- package/plugin/src/index.ts +10 -1
- package/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
- package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
- package/src/ExpoAudioStream.types.ts +57 -7
- package/src/ExpoAudioStream.web.ts +8 -1
- package/src/ExpoAudioStreamModule.ts +255 -10
- package/src/WebRecorder.web.ts +231 -243
- package/src/index.ts +5 -3
- package/src/useAudioRecorder.tsx +14 -10
- package/src/utils/audioProcessing.ts +205 -0
- package/src/workers/InlineFeaturesExtractor.web.tsx +692 -175
- package/src/workers/inlineAudioWebWorker.web.tsx +3 -2
|
@@ -84,6 +84,10 @@ class AudioRecorderManager(
|
|
|
84
84
|
return field
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
private var lastEmissionTimeAnalysis = 0L
|
|
88
|
+
private val analysisBuffer = ByteArrayOutputStream()
|
|
89
|
+
private var isFirstAnalysis = true
|
|
90
|
+
|
|
87
91
|
private fun initializePhoneStateListener() {
|
|
88
92
|
try {
|
|
89
93
|
Log.d(Constants.TAG, "Initializing phone state listener...")
|
|
@@ -671,7 +675,29 @@ class AudioRecorderManager(
|
|
|
671
675
|
|
|
672
676
|
private fun startRecordingProcess(promise: Promise): Boolean {
|
|
673
677
|
try {
|
|
674
|
-
|
|
678
|
+
// Add detailed logging of recording configuration
|
|
679
|
+
Log.d(Constants.TAG, """
|
|
680
|
+
Starting audio recording with configuration:
|
|
681
|
+
- Sample Rate: ${recordingConfig.sampleRate} Hz
|
|
682
|
+
- Channels: ${recordingConfig.channels}
|
|
683
|
+
- Encoding: ${recordingConfig.encoding}
|
|
684
|
+
- Data Emission Interval: ${recordingConfig.interval}ms
|
|
685
|
+
- Analysis Interval: ${recordingConfig.intervalAnalysis}ms
|
|
686
|
+
- Processing Enabled: ${recordingConfig.enableProcessing}
|
|
687
|
+
- Keep Awake: ${recordingConfig.keepAwake}
|
|
688
|
+
- Show Notification: ${recordingConfig.showNotification}
|
|
689
|
+
- Show Waveform: ${recordingConfig.showWaveformInNotification}
|
|
690
|
+
- Compressed Output: ${recordingConfig.enableCompressedOutput}
|
|
691
|
+
${if (recordingConfig.enableCompressedOutput) """
|
|
692
|
+
- Compressed Format: ${recordingConfig.compressedFormat}
|
|
693
|
+
- Compressed Bitrate: ${recordingConfig.compressedBitRate}
|
|
694
|
+
""".trimIndent() else ""}
|
|
695
|
+
- Auto Resume: ${recordingConfig.autoResumeAfterInterruption}
|
|
696
|
+
- Output Directory: ${recordingConfig.outputDirectory ?: "default"}
|
|
697
|
+
- Filename: ${recordingConfig.filename ?: "auto-generated"}
|
|
698
|
+
- Features: ${recordingConfig.features.entries.joinToString { "${it.key}=${it.value}" }}
|
|
699
|
+
""".trimIndent())
|
|
700
|
+
|
|
675
701
|
audioRecord?.startRecording()
|
|
676
702
|
isPaused.set(false)
|
|
677
703
|
isRecording.set(true)
|
|
@@ -969,8 +995,13 @@ class AudioRecorderManager(
|
|
|
969
995
|
try {
|
|
970
996
|
Log.i(Constants.TAG, "Starting recording process...")
|
|
971
997
|
FileOutputStream(audioFile, true).use { fos ->
|
|
998
|
+
// Write audio data directly to the file
|
|
999
|
+
val audioData = ByteArray(bufferSizeInBytes)
|
|
1000
|
+
Log.d(Constants.TAG, "Entering recording loop")
|
|
1001
|
+
|
|
972
1002
|
// Buffer to accumulate data
|
|
973
1003
|
val accumulatedAudioData = ByteArrayOutputStream()
|
|
1004
|
+
val accumulatedAnalysisData = ByteArrayOutputStream() // Separate buffer for analysis
|
|
974
1005
|
audioFileHandler.writeWavHeader(
|
|
975
1006
|
accumulatedAudioData,
|
|
976
1007
|
recordingConfig.sampleRate,
|
|
@@ -982,17 +1013,34 @@ class AudioRecorderManager(
|
|
|
982
1013
|
else -> 16 // Default to 16 if the encoding is not recognized
|
|
983
1014
|
}
|
|
984
1015
|
)
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1016
|
+
|
|
1017
|
+
// Initialize timing variables
|
|
1018
|
+
var lastEmitTime = System.currentTimeMillis()
|
|
1019
|
+
var lastEmissionTimeAnalysis = System.currentTimeMillis()
|
|
1020
|
+
var isFirstAnalysis = true
|
|
1021
|
+
var shouldProcessAnalysis = false
|
|
1022
|
+
|
|
1023
|
+
// Debug log for intervals
|
|
1024
|
+
Log.d(Constants.TAG, """
|
|
1025
|
+
Recording process started with intervals:
|
|
1026
|
+
- Data emission interval: ${recordingConfig.interval}ms
|
|
1027
|
+
- Analysis interval: ${recordingConfig.intervalAnalysis}ms
|
|
1028
|
+
- Buffer size: $bufferSizeInBytes bytes
|
|
1029
|
+
""".trimIndent())
|
|
1030
|
+
|
|
1031
|
+
// Recording loop
|
|
988
1032
|
while (isRecording.get() && !Thread.currentThread().isInterrupted) {
|
|
989
1033
|
if (isPaused.get()) {
|
|
990
|
-
//
|
|
1034
|
+
Thread.sleep(100) // Add small delay when paused
|
|
991
1035
|
continue
|
|
992
1036
|
}
|
|
993
1037
|
|
|
1038
|
+
val currentTime = System.currentTimeMillis()
|
|
1039
|
+
val timeSinceLastAnalysis = currentTime - lastEmissionTimeAnalysis
|
|
1040
|
+
shouldProcessAnalysis = recordingConfig.enableProcessing &&
|
|
1041
|
+
(isFirstAnalysis || timeSinceLastAnalysis >= recordingConfig.intervalAnalysis)
|
|
1042
|
+
|
|
994
1043
|
val bytesRead = synchronized(audioRecordLock) {
|
|
995
|
-
// Only synchronize the read operation and the check
|
|
996
1044
|
audioRecord?.let {
|
|
997
1045
|
if (it.state != AudioRecord.STATE_INITIALIZED) {
|
|
998
1046
|
Log.e(Constants.TAG, "AudioRecord not initialized")
|
|
@@ -1005,22 +1053,65 @@ class AudioRecorderManager(
|
|
|
1005
1053
|
}
|
|
1006
1054
|
} ?: -1 // Handle null case
|
|
1007
1055
|
}
|
|
1056
|
+
|
|
1008
1057
|
if (bytesRead > 0) {
|
|
1009
1058
|
fos.write(audioData, 0, bytesRead)
|
|
1010
1059
|
totalDataSize += bytesRead
|
|
1060
|
+
|
|
1011
1061
|
accumulatedAudioData.write(audioData, 0, bytesRead)
|
|
1012
1062
|
|
|
1013
|
-
//
|
|
1014
|
-
if (
|
|
1063
|
+
// Handle regular audio data emission
|
|
1064
|
+
if (currentTime - lastEmitTime >= recordingConfig.interval) {
|
|
1015
1065
|
emitAudioData(
|
|
1016
1066
|
accumulatedAudioData.toByteArray(),
|
|
1017
1067
|
accumulatedAudioData.size()
|
|
1018
1068
|
)
|
|
1019
|
-
lastEmitTime =
|
|
1069
|
+
lastEmitTime = currentTime
|
|
1020
1070
|
accumulatedAudioData.reset() // Clear the accumulator
|
|
1021
1071
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1072
|
+
|
|
1073
|
+
// Handle analysis emission separately
|
|
1074
|
+
if (shouldProcessAnalysis) {
|
|
1075
|
+
val analysisDataSize = accumulatedAnalysisData.size()
|
|
1076
|
+
Log.d(Constants.TAG, """
|
|
1077
|
+
Processing analysis data:
|
|
1078
|
+
- Time since last: ${currentTime - lastEmissionTimeAnalysis}ms
|
|
1079
|
+
- Configured interval: ${recordingConfig.intervalAnalysis}ms
|
|
1080
|
+
- Accumulated size: $analysisDataSize bytes
|
|
1081
|
+
- Is first analysis: $isFirstAnalysis
|
|
1082
|
+
""".trimIndent())
|
|
1083
|
+
|
|
1084
|
+
if (analysisDataSize > 0) {
|
|
1085
|
+
// Add this check to enforce minimum interval
|
|
1086
|
+
if (isFirstAnalysis || (currentTime - lastEmissionTimeAnalysis) >= recordingConfig.intervalAnalysis) {
|
|
1087
|
+
// Process and emit analysis data
|
|
1088
|
+
val analysisData = audioProcessor.processAudioData(
|
|
1089
|
+
accumulatedAnalysisData.toByteArray(),
|
|
1090
|
+
recordingConfig
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
Log.d(Constants.TAG, """
|
|
1094
|
+
Analysis data details:
|
|
1095
|
+
- Raw data size: ${accumulatedAnalysisData.size()} bytes
|
|
1096
|
+
""".trimIndent())
|
|
1097
|
+
|
|
1098
|
+
mainHandler.post {
|
|
1099
|
+
try {
|
|
1100
|
+
eventSender.sendExpoEvent(
|
|
1101
|
+
Constants.AUDIO_ANALYSIS_EVENT_NAME,
|
|
1102
|
+
analysisData.toBundle()
|
|
1103
|
+
)
|
|
1104
|
+
} catch (e: Exception) {
|
|
1105
|
+
Log.e(Constants.TAG, "Failed to send audio analysis event", e)
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
lastEmissionTimeAnalysis = currentTime
|
|
1110
|
+
accumulatedAnalysisData.reset() // Clear the analysis accumulator
|
|
1111
|
+
isFirstAnalysis = false
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1024
1115
|
}
|
|
1025
1116
|
}
|
|
1026
1117
|
}
|
|
@@ -1034,6 +1125,7 @@ class AudioRecorderManager(
|
|
|
1034
1125
|
if (!isPaused.get()) {
|
|
1035
1126
|
releaseWakeLock()
|
|
1036
1127
|
}
|
|
1128
|
+
Log.e(Constants.TAG, "Error in recording process", e)
|
|
1037
1129
|
}
|
|
1038
1130
|
}
|
|
1039
1131
|
|
|
@@ -1120,24 +1212,43 @@ class AudioRecorderManager(
|
|
|
1120
1212
|
audioData
|
|
1121
1213
|
}
|
|
1122
1214
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1215
|
+
// Accumulate data for analysis
|
|
1216
|
+
if (recordingConfig.enableProcessing) {
|
|
1217
|
+
synchronized(analysisBuffer) {
|
|
1218
|
+
analysisBuffer.write(dataToProcess)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
val currentTime = SystemClock.elapsedRealtime()
|
|
1222
|
+
if (isFirstAnalysis || (currentTime - lastEmissionTimeAnalysis) >= recordingConfig.intervalAnalysis) {
|
|
1223
|
+
synchronized(analysisBuffer) {
|
|
1224
|
+
if (analysisBuffer.size() > 0) {
|
|
1225
|
+
val analysisData = audioProcessor.processAudioData(
|
|
1226
|
+
analysisBuffer.toByteArray(),
|
|
1227
|
+
recordingConfig
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
mainHandler.post {
|
|
1231
|
+
eventSender.sendExpoEvent(
|
|
1232
|
+
Constants.AUDIO_ANALYSIS_EVENT_NAME,
|
|
1233
|
+
analysisData.toBundle()
|
|
1234
|
+
)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Reset buffer after processing
|
|
1238
|
+
analysisBuffer.reset()
|
|
1239
|
+
lastEmissionTimeAnalysis = currentTime
|
|
1240
|
+
isFirstAnalysis = false
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1125
1245
|
|
|
1246
|
+
// Only update notification if needed
|
|
1126
1247
|
if (recordingConfig.showNotification && recordingConfig.showWaveformInNotification) {
|
|
1127
1248
|
val floatArray = convertByteArrayToFloatArray(audioData)
|
|
1128
1249
|
notificationManager.updateNotification(floatArray)
|
|
1129
1250
|
}
|
|
1130
1251
|
|
|
1131
|
-
mainHandler.post {
|
|
1132
|
-
try {
|
|
1133
|
-
eventSender.sendExpoEvent(
|
|
1134
|
-
Constants.AUDIO_ANALYSIS_EVENT_NAME, analysisBundle
|
|
1135
|
-
)
|
|
1136
|
-
} catch (e: Exception) {
|
|
1137
|
-
Log.e(Constants.TAG, "Failed to send audio analysis event", e)
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
1252
|
// Reset isFirstChunk after processing
|
|
1142
1253
|
isFirstChunk = false
|
|
1143
1254
|
}
|
|
@@ -32,39 +32,45 @@ class AudioRecordingService : Service() {
|
|
|
32
32
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
33
33
|
Log.d(Constants.TAG, "AudioRecordingService onStartCommand")
|
|
34
34
|
|
|
35
|
+
// Check if service is being started from BOOT_COMPLETED
|
|
36
|
+
val isFromBoot = intent?.action == Intent.ACTION_BOOT_COMPLETED
|
|
37
|
+
|
|
35
38
|
if (!isRunning) {
|
|
36
39
|
isRunning = true
|
|
37
40
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
// Don't start foreground service if coming from BOOT_COMPLETED on Android 15+
|
|
42
|
+
if (!isFromBoot || Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
43
|
+
// Start as foreground service if keepAwake is true, regardless of notification settings
|
|
44
|
+
val keepAwake = AudioRecorderManager.getInstance()?.getKeepAwakeStatus() ?: true
|
|
45
|
+
if (keepAwake) {
|
|
46
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
47
|
+
// Create a minimal notification channel if needed
|
|
48
|
+
val channel = NotificationChannel(
|
|
49
|
+
"recording_service",
|
|
50
|
+
"Recording Service",
|
|
51
|
+
NotificationManager.IMPORTANCE_LOW
|
|
52
|
+
).apply {
|
|
53
|
+
setSound(null, null)
|
|
54
|
+
enableLights(false)
|
|
55
|
+
enableVibration(false)
|
|
56
|
+
}
|
|
57
|
+
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
58
|
+
notificationManager.createNotificationChannel(channel)
|
|
59
|
+
|
|
60
|
+
// Create minimal silent notification
|
|
61
|
+
val notification = NotificationCompat.Builder(this, "recording_service")
|
|
62
|
+
.setContentTitle("")
|
|
63
|
+
.setContentText("")
|
|
64
|
+
.setSmallIcon(R.drawable.ic_microphone)
|
|
65
|
+
.setOngoing(true)
|
|
66
|
+
.setSound(null)
|
|
67
|
+
.setVibrate(null)
|
|
68
|
+
.setDefaults(0)
|
|
69
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
70
|
+
.build()
|
|
71
|
+
|
|
72
|
+
startForeground(1, notification)
|
|
51
73
|
}
|
|
52
|
-
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
53
|
-
notificationManager.createNotificationChannel(channel)
|
|
54
|
-
|
|
55
|
-
// Create minimal silent notification
|
|
56
|
-
val notification = NotificationCompat.Builder(this, "recording_service")
|
|
57
|
-
.setContentTitle("")
|
|
58
|
-
.setContentText("")
|
|
59
|
-
.setSmallIcon(R.drawable.ic_microphone)
|
|
60
|
-
.setOngoing(true)
|
|
61
|
-
.setSound(null)
|
|
62
|
-
.setVibrate(null)
|
|
63
|
-
.setDefaults(0)
|
|
64
|
-
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
65
|
-
.build()
|
|
66
|
-
|
|
67
|
-
startForeground(1, notification)
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
76
|
}
|
|
@@ -8,6 +8,7 @@ object Constants {
|
|
|
8
8
|
const val DEFAULT_CHANNEL_CONFIG = 1 // Mono
|
|
9
9
|
const val DEFAULT_AUDIO_FORMAT = 16 // 16-bit PCM
|
|
10
10
|
const val DEFAULT_INTERVAL = 1000L
|
|
11
|
+
const val DEFAULT_INTERVAL_ANALYSIS = 500L
|
|
11
12
|
const val MIN_INTERVAL = 10L // Minimum interval in ms for emitting audio data
|
|
12
13
|
const val WAV_HEADER_SIZE = 44
|
|
13
14
|
const val RIFF_HEADER = 0x52494646 // "RIFF"
|