@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +1 -1
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +68 -22
  4. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +24 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +836 -386
  6. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +134 -23
  7. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +35 -29
  8. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  9. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +236 -96
  10. package/android/src/main/java/net/siteed/audiostream/FFT.kt +55 -0
  11. package/android/src/main/java/net/siteed/audiostream/Features.kt +49 -7
  12. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +4 -4
  13. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +55 -47
  14. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  15. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  16. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +60 -13
  17. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
  18. package/build/AudioAnalysis/extractAudioAnalysis.js +147 -162
  19. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  20. package/build/ExpoAudioStream.types.d.ts +49 -3
  21. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  22. package/build/ExpoAudioStream.types.js.map +1 -1
  23. package/build/ExpoAudioStream.web.d.ts +2 -0
  24. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  25. package/build/ExpoAudioStream.web.js +8 -1
  26. package/build/ExpoAudioStream.web.js.map +1 -1
  27. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  28. package/build/ExpoAudioStreamModule.js +216 -12
  29. package/build/ExpoAudioStreamModule.js.map +1 -1
  30. package/build/WebRecorder.web.d.ts +67 -13
  31. package/build/WebRecorder.web.d.ts.map +1 -1
  32. package/build/WebRecorder.web.js +178 -173
  33. package/build/WebRecorder.web.js.map +1 -1
  34. package/build/index.d.ts +3 -3
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +2 -2
  37. package/build/index.js.map +1 -1
  38. package/build/useAudioRecorder.d.ts.map +1 -1
  39. package/build/useAudioRecorder.js +12 -8
  40. package/build/useAudioRecorder.js.map +1 -1
  41. package/build/utils/audioProcessing.d.ts +24 -0
  42. package/build/utils/audioProcessing.d.ts.map +1 -0
  43. package/build/utils/audioProcessing.js +133 -0
  44. package/build/utils/audioProcessing.js.map +1 -0
  45. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  46. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  47. package/build/workers/InlineFeaturesExtractor.web.js +692 -175
  48. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  49. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  50. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  51. package/build/workers/inlineAudioWebWorker.web.js +3 -2
  52. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  53. package/ios/AudioAnalysisData.swift +51 -16
  54. package/ios/AudioProcessingHelpers.swift +710 -26
  55. package/ios/AudioProcessor.swift +334 -185
  56. package/ios/AudioStreamManager.swift +66 -22
  57. package/ios/DataPoint.swift +25 -12
  58. package/ios/DecodingConfig.swift +47 -0
  59. package/ios/ExpoAudioStreamModule.swift +189 -104
  60. package/ios/FFT.swift +62 -0
  61. package/ios/Features.swift +24 -3
  62. package/ios/RecordingSettings.swift +9 -7
  63. package/package.json +2 -1
  64. package/plugin/build/index.d.ts +2 -0
  65. package/plugin/build/index.js +10 -3
  66. package/plugin/src/index.ts +10 -1
  67. package/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
  68. package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
  69. package/src/ExpoAudioStream.types.ts +57 -7
  70. package/src/ExpoAudioStream.web.ts +8 -1
  71. package/src/ExpoAudioStreamModule.ts +255 -10
  72. package/src/WebRecorder.web.ts +231 -243
  73. package/src/index.ts +5 -3
  74. package/src/useAudioRecorder.tsx +14 -10
  75. package/src/utils/audioProcessing.ts +205 -0
  76. package/src/workers/InlineFeaturesExtractor.web.tsx +692 -175
  77. 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
- Log.d(Constants.TAG, "Starting audio recording")
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
- // Write audio data directly to the file
986
- val audioData = ByteArray(bufferSizeInBytes)
987
- Log.d(Constants.TAG, "Entering recording loop")
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
- // If recording is paused, skip reading from the microphone
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
- // Emit audio data at defined intervals
1014
- if (SystemClock.elapsedRealtime() - lastEmitTime >= recordingConfig.interval) {
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 = SystemClock.elapsedRealtime() // Reset the timer
1069
+ lastEmitTime = currentTime
1020
1070
  accumulatedAudioData.reset() // Clear the accumulator
1021
1071
  }
1022
-
1023
- Log.d(Constants.TAG, "Bytes written to file: $bytesRead")
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
- val audioAnalysisData = audioProcessor.processAudioData(dataToProcess, recordingConfig)
1124
- val analysisBundle = audioAnalysisData.toBundle()
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
- // Start as foreground service if keepAwake is true, regardless of notification settings
39
- val keepAwake = AudioRecorderManager.getInstance()?.getKeepAwakeStatus() ?: true
40
- if (keepAwake) {
41
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
42
- // Create a minimal notification channel if needed
43
- val channel = NotificationChannel(
44
- "recording_service",
45
- "Recording Service",
46
- NotificationManager.IMPORTANCE_LOW
47
- ).apply {
48
- setSound(null, null)
49
- enableLights(false)
50
- enableVibration(false)
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"