@siteed/expo-audio-stream 1.0.1 → 1.0.2

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 (85) hide show
  1. package/README.md +6 -6
  2. package/android/build.gradle +5 -0
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +120 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +34 -4
  5. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +635 -0
  6. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +194 -79
  7. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  8. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +48 -2
  9. package/android/src/main/java/net/siteed/audiostream/FFT.kt +44 -0
  10. package/android/src/main/java/net/siteed/audiostream/Features.kt +56 -0
  11. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +12 -0
  12. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +56 -0
  13. package/app.plugin.js +1 -1
  14. package/build/AudioRecorder.provider.js +1 -1
  15. package/build/AudioRecorder.provider.js.map +1 -1
  16. package/build/ExpoAudioStream.native.d.ts +3 -0
  17. package/build/ExpoAudioStream.native.d.ts.map +1 -0
  18. package/build/ExpoAudioStream.native.js +6 -0
  19. package/build/ExpoAudioStream.native.js.map +1 -0
  20. package/build/ExpoAudioStream.types.d.ts +79 -6
  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 +41 -0
  24. package/build/ExpoAudioStream.web.d.ts.map +1 -0
  25. package/build/ExpoAudioStream.web.js +184 -0
  26. package/build/ExpoAudioStream.web.js.map +1 -0
  27. package/build/ExpoAudioStreamModule.d.ts +2 -2
  28. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  29. package/build/ExpoAudioStreamModule.js +12 -3
  30. package/build/ExpoAudioStreamModule.js.map +1 -1
  31. package/build/WebRecorder.d.ts +47 -0
  32. package/build/WebRecorder.d.ts.map +1 -0
  33. package/build/WebRecorder.js +243 -0
  34. package/build/WebRecorder.js.map +1 -0
  35. package/build/index.d.ts +14 -5
  36. package/build/index.d.ts.map +1 -1
  37. package/build/index.js +106 -7
  38. package/build/index.js.map +1 -1
  39. package/build/inlineAudioWebWorker.d.ts +3 -0
  40. package/build/inlineAudioWebWorker.d.ts.map +1 -0
  41. package/build/inlineAudioWebWorker.js +340 -0
  42. package/build/inlineAudioWebWorker.js.map +1 -0
  43. package/build/useAudioRecording.d.ts +24 -9
  44. package/build/useAudioRecording.d.ts.map +1 -1
  45. package/build/useAudioRecording.js +107 -29
  46. package/build/useAudioRecording.js.map +1 -1
  47. package/build/utils.d.ts +31 -0
  48. package/build/utils.d.ts.map +1 -0
  49. package/build/utils.js +143 -0
  50. package/build/utils.js.map +1 -0
  51. package/expo-module.config.json +13 -4
  52. package/ios/AudioAnalysisData.swift +39 -0
  53. package/ios/AudioProcessingHelpers.swift +59 -0
  54. package/ios/AudioProcessor.swift +317 -0
  55. package/ios/AudioStreamError.swift +7 -0
  56. package/ios/AudioStreamManager.swift +204 -52
  57. package/ios/AudioStreamManagerDelegate.swift +4 -0
  58. package/ios/DataPoint.swift +41 -0
  59. package/ios/ExpoAudioStreamModule.swift +188 -6
  60. package/ios/Features.swift +44 -0
  61. package/ios/RecordingResult.swift +19 -0
  62. package/ios/RecordingSettings.swift +13 -0
  63. package/ios/WaveformExtractor.swift +105 -0
  64. package/package.json +9 -9
  65. package/plugin/tsconfig.json +13 -8
  66. package/publish.sh +8 -0
  67. package/src/AudioRecorder.provider.tsx +1 -1
  68. package/src/ExpoAudioStream.native.ts +6 -0
  69. package/src/ExpoAudioStream.types.ts +97 -11
  70. package/src/ExpoAudioStream.web.ts +228 -0
  71. package/src/ExpoAudioStreamModule.ts +17 -3
  72. package/src/WebRecorder.ts +364 -0
  73. package/src/index.ts +166 -20
  74. package/src/inlineAudioWebWorker.tsx +340 -0
  75. package/src/useAudioRecording.tsx +410 -0
  76. package/src/utils.ts +189 -0
  77. package/build/ExpoAudioStreamModule.web.d.ts +0 -37
  78. package/build/ExpoAudioStreamModule.web.d.ts.map +0 -1
  79. package/build/ExpoAudioStreamModule.web.js +0 -156
  80. package/build/ExpoAudioStreamModule.web.js.map +0 -1
  81. package/docs/demo.gif +0 -0
  82. package/release-it.js +0 -18
  83. package/src/ExpoAudioStreamModule.web.ts +0 -181
  84. package/src/useAudioRecording.ts +0 -268
  85. package/yarn-error.log +0 -7793
@@ -1,3 +1,4 @@
1
+ // net/siteed/audiostream/AudioRecorderManager.kt
1
2
  package net.siteed.audiostream
2
3
 
3
4
  import android.media.AudioFormat
@@ -16,6 +17,7 @@ import java.io.ByteArrayOutputStream
16
17
  import java.io.File
17
18
  import java.io.FileOutputStream
18
19
  import java.io.IOException
20
+ import java.io.Serializable
19
21
  import java.util.concurrent.atomic.AtomicBoolean
20
22
 
21
23
 
@@ -26,11 +28,7 @@ class AudioRecorderManager(
26
28
  private val eventSender: EventSender
27
29
  ) {
28
30
  private var audioRecord: AudioRecord? = null
29
- private var sampleRateInHz = Constants.DEFAULT_SAMPLE_RATE
30
- private var channelConfig = Constants.DEFAULT_CHANNEL_CONFIG
31
- private var audioFormat = Constants.DEFAULT_AUDIO_FORMAT
32
- private var bufferSizeInBytes =
33
- AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
31
+ private var bufferSizeInBytes = 0
34
32
  private var isRecording = AtomicBoolean(false)
35
33
  private val isPaused = AtomicBoolean(false)
36
34
  private var streamUuid: String? = null
@@ -44,13 +42,15 @@ class AudioRecorderManager(
44
42
  private var lastPauseTime = 0L
45
43
  private var pausedDuration = 0L
46
44
  private var lastEmittedSize = 0L
47
- private var mimeType = "audio/wav"
48
45
  private val mainHandler = Handler(Looper.getMainLooper())
49
- private var bitDepth = 16
50
- private var channels = 1
51
46
  private val audioRecordLock = Any()
52
47
  private var audioFileHandler: AudioFileHandler = AudioFileHandler(filesDir)
53
48
 
49
+ private lateinit var recordingConfig: RecordingConfig
50
+ private var mimeType = "audio/wav"
51
+ private var audioFormat: Int = AudioFormat.ENCODING_PCM_16BIT
52
+ private var audioProcessor: AudioProcessor = AudioProcessor(filesDir)
53
+
54
54
  @RequiresApi(Build.VERSION_CODES.R)
55
55
  fun startRecording(options: Map<String, Any?>, promise: Promise) {
56
56
  if (!permissionUtils.checkRecordingPermission()) {
@@ -63,9 +63,29 @@ class AudioRecorderManager(
63
63
  return
64
64
  }
65
65
 
66
- // Extract and validate recording options
67
- sampleRateInHz = options["sampleRate"] as? Int ?: Constants.DEFAULT_SAMPLE_RATE
68
- if (sampleRateInHz !in listOf(16000, 44100, 48000)) {
66
+ // Extract and filter features
67
+ val featuresMap = options["features"] as? Map<*, *>
68
+ val features = featuresMap?.filterKeys { it is String }
69
+ ?.filterValues { it is Boolean }
70
+ ?.mapKeys { it.key as String }
71
+ ?.mapValues { it.value as Boolean }
72
+ ?: emptyMap()
73
+
74
+ // Initialize the recording configuration
75
+ var tempRecordingConfig = RecordingConfig(
76
+ sampleRate = (options["sampleRate"] as? Number)?.toInt() ?: Constants.DEFAULT_SAMPLE_RATE,
77
+ channels = (options["channels"] as? Number)?.toInt() ?: 1,
78
+ encoding = options["encoding"] as? String ?: "pcm_16bit",
79
+ interval = (options["interval"] as? Number)?.toLong() ?: Constants.DEFAULT_INTERVAL,
80
+ enableProcessing = options["enableProcessing"] as? Boolean ?: false,
81
+ pointsPerSecond = (options["pointsPerSecond"] as? Number)?.toDouble() ?: 20.0,
82
+ algorithm = options["algorithm"] as? String ?: "rms",
83
+ features = features
84
+ )
85
+ Log.d(Constants.TAG, "Initial recording configuration: $tempRecordingConfig")
86
+
87
+ // Validate sample rate and channels
88
+ if (tempRecordingConfig.sampleRate !in listOf(16000, 44100, 48000)) {
69
89
  promise.reject(
70
90
  "INVALID_SAMPLE_RATE",
71
91
  "Sample rate must be one of 16000, 44100, or 48000 Hz",
@@ -73,9 +93,7 @@ class AudioRecorderManager(
73
93
  )
74
94
  return
75
95
  }
76
-
77
- channels = options["channels"] as? Int ?: 1
78
- if (channels !in 1..2) {
96
+ if (tempRecordingConfig.channels !in 1..2) {
79
97
  promise.reject(
80
98
  "INVALID_CHANNELS",
81
99
  "Channels must be either 1 (Mono) or 2 (Stereo)",
@@ -84,29 +102,26 @@ class AudioRecorderManager(
84
102
  return
85
103
  }
86
104
 
87
- val encodingType = options["encoding"] as? String ?: "pcm_16bit"
88
- if (encodingType !in listOf("pcm_16bit", "pcm_8bit", "aac", "opus")) {
89
- promise.reject(
90
- "INVALID_ENCODING",
91
- "Encoding must be one of the following: pcm_16bit, pcm_8bit, aac, opus",
92
- null
93
- )
94
- return
95
- }
96
-
97
-
98
- var fileExtension = ".wav" // Default
99
-
100
- audioFormat = when (encodingType) {
101
- "pcm_8bit", "pcm_16bit" -> {
105
+ // Set encoding and file extension
106
+ var fileExtension = ".wav"
107
+ audioFormat = when (tempRecordingConfig.encoding) {
108
+ "pcm_8bit" -> {
102
109
  fileExtension = "wav"
103
- mimeType = "audio/wav" // WAV is typically used for PCM data.
110
+ mimeType = "audio/wav"
111
+ AudioFormat.ENCODING_PCM_8BIT
112
+ }
113
+ "pcm_16bit" -> {
114
+ fileExtension = "wav"
115
+ mimeType = "audio/wav"
104
116
  AudioFormat.ENCODING_PCM_16BIT
105
117
  }
106
-
118
+ "pcm_32bit" -> {
119
+ fileExtension = "wav"
120
+ mimeType = "audio/wav"
121
+ AudioFormat.ENCODING_PCM_FLOAT
122
+ }
107
123
  "opus" -> {
108
124
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
109
- // Handle the case where Opus is not supported by the device
110
125
  promise.reject(
111
126
  "UNSUPPORTED_FORMAT",
112
127
  "Opus encoding not supported on this Android version.",
@@ -118,43 +133,69 @@ class AudioRecorderManager(
118
133
  mimeType = "audio/opus"
119
134
  AudioFormat.ENCODING_OPUS
120
135
  }
121
-
122
136
  "aac_lc" -> {
123
137
  fileExtension = "aac"
124
138
  mimeType = "audio/aac"
125
139
  AudioFormat.ENCODING_AAC_LC
126
140
  }
127
-
128
141
  else -> {
129
142
  fileExtension = "wav"
130
- mimeType = "audio/wav" // Default case or throw an error if unsupported
143
+ mimeType = "audio/wav"
131
144
  AudioFormat.ENCODING_DEFAULT
132
145
  }
133
146
  }
134
147
 
135
- interval = options["interval"] as? Long ?: Constants.DEFAULT_INTERVAL
136
- if (interval < Constants.MIN_INTERVAL) {
137
- promise.reject(
138
- "INVALID_INTERVAL",
139
- "Interval must be at least ${Constants.MIN_INTERVAL} ms",
140
- null
141
- )
142
- return
148
+ // Check if selected audio format is supported
149
+ if (!isAudioFormatSupported(tempRecordingConfig.sampleRate, tempRecordingConfig.channels, audioFormat)) {
150
+ Log.e(Constants.TAG, "Selected audio format not supported, falling back to 16-bit PCM")
151
+ audioFormat = AudioFormat.ENCODING_PCM_16BIT
152
+ if (!isAudioFormatSupported(tempRecordingConfig.sampleRate, tempRecordingConfig.channels, audioFormat)) {
153
+ promise.reject("INITIALIZATION_FAILED", "Failed to initialize audio recorder with any supported format", null)
154
+ return
155
+ }
156
+ tempRecordingConfig = tempRecordingConfig.copy(encoding = "pcm_16bit")
143
157
  }
144
158
 
145
- bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
159
+ // Update recordingConfig with potentially new encoding
160
+ recordingConfig = tempRecordingConfig
146
161
 
147
- Log.d(
148
- Constants.TAG,
149
- "Starting recording with the following parameters: Sample Rate: $sampleRateInHz Hz, Channels: $channels, Encoding: $encodingType, File Extension: $fileExtension, MIME Type: $mimeType, Interval: $interval ms"
162
+
163
+ // Check if selected audio format is supported
164
+ if (!isAudioFormatSupported(tempRecordingConfig.sampleRate, tempRecordingConfig.channels, audioFormat)) {
165
+ Log.e(Constants.TAG, "Selected audio format not supported, falling back to 16-bit PCM")
166
+ audioFormat = AudioFormat.ENCODING_PCM_16BIT
167
+ if (!isAudioFormatSupported(tempRecordingConfig.sampleRate, tempRecordingConfig.channels, audioFormat)) {
168
+ promise.reject("INITIALIZATION_FAILED", "Failed to initialize audio recorder with any supported format", null)
169
+ return
170
+ }
171
+ tempRecordingConfig = tempRecordingConfig.copy(encoding = "pcm_16bit")
172
+ }
173
+
174
+ // Update recordingConfig with potentially new encoding
175
+ recordingConfig = tempRecordingConfig
176
+
177
+ // Recalculate bufferSizeInBytes if the format has changed
178
+ bufferSizeInBytes = AudioRecord.getMinBufferSize(
179
+ recordingConfig.sampleRate,
180
+ if (recordingConfig.channels == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO,
181
+ audioFormat
150
182
  )
151
183
 
184
+ if (bufferSizeInBytes == AudioRecord.ERROR || bufferSizeInBytes == AudioRecord.ERROR_BAD_VALUE || bufferSizeInBytes < 0) {
185
+ Log.e(Constants.TAG, "Failed to get minimum buffer size, falling back to default buffer size.")
186
+ bufferSizeInBytes = 4096 // Default buffer size in bytes
187
+ }
188
+
189
+ Log.d(Constants.TAG, "AudioFormat: $audioFormat, BufferSize: $bufferSizeInBytes")
190
+
152
191
  // Initialize the AudioRecord if it's a new recording or if it's not currently paused
153
192
  if (audioRecord == null || !isPaused.get()) {
193
+ Log.d(Constants.TAG, "AudioFormat: $audioFormat, BufferSize: $bufferSizeInBytes")
194
+
154
195
  audioRecord = AudioRecord(
155
196
  MediaRecorder.AudioSource.MIC,
156
- sampleRateInHz,
157
- channelConfig,
197
+ recordingConfig.sampleRate,
198
+ if (recordingConfig.channels == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO,
158
199
  audioFormat,
159
200
  bufferSizeInBytes
160
201
  )
@@ -173,7 +214,12 @@ class AudioRecorderManager(
173
214
 
174
215
  try {
175
216
  FileOutputStream(audioFile, true).use { fos ->
176
- audioFileHandler.writeWavHeader(fos, sampleRateInHz, channels, bitDepth)
217
+ audioFileHandler.writeWavHeader(fos, recordingConfig.sampleRate, recordingConfig.channels, when (recordingConfig.encoding) {
218
+ "pcm_8bit" -> 8
219
+ "pcm_16bit" -> 16
220
+ "pcm_32bit" -> 32
221
+ else -> 16 // Default to 16 if the encoding is not recognized
222
+ })
177
223
  }
178
224
  } catch (e: IOException) {
179
225
  promise.reject("FILE_CREATION_FAILED", "Failed to create the audio file", e)
@@ -193,14 +239,54 @@ class AudioRecorderManager(
193
239
 
194
240
  val result = bundleOf(
195
241
  "fileUri" to audioFile?.toURI().toString(),
196
- "channels" to channels,
197
- "bitDepth" to bitDepth,
198
- "sampleRate" to sampleRateInHz,
242
+ "channels" to recordingConfig.channels,
243
+ "bitDepth" to when (recordingConfig.encoding) {
244
+ "pcm_8bit" -> 8
245
+ "pcm_16bit" -> 16
246
+ "pcm_32bit" -> 32
247
+ else -> 16 // Default to 16 if the encoding is not recognized
248
+ },
249
+ "sampleRate" to recordingConfig.sampleRate,
199
250
  "mimeType" to mimeType
200
251
  )
201
252
  promise.resolve(result)
202
253
  }
203
254
 
255
+ private fun isAudioFormatSupported(sampleRate: Int, channels: Int, format: Int): Boolean {
256
+ if (!permissionUtils.checkRecordingPermission()) {
257
+ throw SecurityException("Recording permission has not been granted")
258
+ }
259
+
260
+ val channelConfig = if (channels == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO
261
+ val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, format)
262
+
263
+ if (bufferSize <= 0) {
264
+ return false
265
+ }
266
+
267
+ val audioRecord = AudioRecord(
268
+ MediaRecorder.AudioSource.MIC,
269
+ sampleRate,
270
+ channelConfig,
271
+ format,
272
+ bufferSize
273
+ )
274
+
275
+ val isSupported = audioRecord.state == AudioRecord.STATE_INITIALIZED
276
+ if (isSupported) {
277
+ val testBuffer = ByteArray(bufferSize)
278
+ audioRecord.startRecording()
279
+ val testRead = audioRecord.read(testBuffer, 0, bufferSize)
280
+ audioRecord.stop()
281
+ if (testRead < 0) {
282
+ return false
283
+ }
284
+ }
285
+
286
+ audioRecord.release()
287
+ return isSupported
288
+ }
289
+
204
290
  fun stopRecording(promise: Promise) {
205
291
  synchronized(audioRecordLock) {
206
292
 
@@ -232,18 +318,27 @@ class AudioRecorderManager(
232
318
  try {
233
319
  val fileSize = audioFile?.length() ?: 0
234
320
  val dataFileSize = fileSize - 44 // Subtract header size
235
- val byteRate = sampleRateInHz * channels * (bitDepth / 8)
236
-
321
+ val byteRate = recordingConfig.sampleRate * recordingConfig.channels * when (recordingConfig.encoding) {
322
+ "pcm_8bit" -> 1
323
+ "pcm_16bit" -> 2
324
+ "pcm_32bit" -> 4
325
+ else -> 2 // Default to 2 bytes per sample if the encoding is not recognized
326
+ }
237
327
  // Calculate duration based on the data size and byte rate
238
328
  val duration = if (byteRate > 0) (dataFileSize * 1000 / byteRate) else 0
239
329
 
240
330
  // Create result bundle
241
331
  val result = bundleOf(
242
332
  "fileUri" to audioFile?.toURI().toString(),
243
- "duration" to duration,
244
- "channels" to channels,
245
- "bitDepth" to bitDepth,
246
- "sampleRate" to sampleRateInHz,
333
+ "durationMs" to duration,
334
+ "channels" to recordingConfig.channels,
335
+ "bitDepth" to when (recordingConfig.encoding) {
336
+ "pcm_8bit" -> 8
337
+ "pcm_16bit" -> 16
338
+ "pcm_32bit" -> 32
339
+ else -> 16 // Default to 16 if the encoding is not recognized
340
+ },
341
+ "sampleRate" to recordingConfig.sampleRate,
247
342
  "size" to fileSize,
248
343
  "mimeType" to mimeType
249
344
  )
@@ -313,28 +408,20 @@ class AudioRecorderManager(
313
408
 
314
409
  val duration = when (mimeType) {
315
410
  "audio/wav" -> {
316
- // WAV files store raw audio data, so we can calculate duration like this
317
- val dataFileSize =
318
- fileSize - Constants.WAV_HEADER_SIZE // Assuming header is always 44 bytes
319
- val byteRate = sampleRateInHz * channels * (bitDepth / 8)
411
+ val dataFileSize = fileSize - Constants.WAV_HEADER_SIZE // Assuming header is always 44 bytes
412
+ val byteRate = recordingConfig.sampleRate * recordingConfig.channels * (if (recordingConfig.encoding == "pcm_8bit") 8 else 16) / 8
320
413
  if (byteRate > 0) dataFileSize * 1000 / byteRate else 0
321
414
  }
322
-
323
- "audio/opus", "audio/aac" -> {
324
- // For compressed formats, the duration might need to be retrieved differently,
325
- // perhaps from metadata or requiring a library to parse the file if not stored elsewhere.
326
- getCompressedAudioDuration(audioFile)
327
- }
328
-
415
+ "audio/opus", "audio/aac" -> getCompressedAudioDuration(audioFile)
329
416
  else -> 0
330
417
  }
331
418
  return bundleOf(
332
- "duration" to duration,
419
+ "durationMs" to duration,
333
420
  "isRecording" to isRecording.get(),
334
421
  "isPaused" to isPaused.get(),
335
- "mime" to mimeType,
422
+ "mimeType" to mimeType,
336
423
  "size" to totalDataSize,
337
- "interval" to interval,
424
+ "interval" to recordingConfig.interval
338
425
  )
339
426
  }
340
427
  }
@@ -357,11 +444,15 @@ class AudioRecorderManager(
357
444
  val accumulatedAudioData = ByteArrayOutputStream()
358
445
  audioFileHandler.writeWavHeader(
359
446
  accumulatedAudioData,
360
- sampleRateInHz,
361
- channels,
362
- bitDepth
447
+ recordingConfig.sampleRate,
448
+ recordingConfig.channels,
449
+ when (recordingConfig.encoding) {
450
+ "pcm_8bit" -> 8
451
+ "pcm_16bit" -> 16
452
+ "pcm_32bit" -> 32
453
+ else -> 16 // Default to 16 if the encoding is not recognized
454
+ }
363
455
  )
364
-
365
456
  // Write audio data directly to the file
366
457
  val audioData = ByteArray(bufferSizeInBytes)
367
458
  Log.d(Constants.TAG, "Entering recording loop")
@@ -378,7 +469,11 @@ class AudioRecorderManager(
378
469
  Log.e(Constants.TAG, "AudioRecord not initialized")
379
470
  return@let -1
380
471
  }
381
- it.read(audioData, 0, bufferSizeInBytes)
472
+ it.read(audioData, 0, bufferSizeInBytes).also { bytes ->
473
+ if (bytes < 0) {
474
+ Log.e(Constants.TAG, "AudioRecord read error: $bytes")
475
+ }
476
+ }
382
477
  } ?: -1 // Handle null case
383
478
  }
384
479
  if (bytesRead > 0) {
@@ -415,7 +510,7 @@ class AudioRecorderManager(
415
510
  lastEmittedSize = fileSize
416
511
 
417
512
  // Calculate position in milliseconds
418
- val positionInMs = (from * 1000) / (sampleRateInHz * channels * (bitDepth / 8))
513
+ val positionInMs = (from * 1000) / (recordingConfig.sampleRate * recordingConfig.channels * (if (recordingConfig.encoding == "pcm_8bit") 8 else 16) / 8)
419
514
 
420
515
  mainHandler.post {
421
516
  try {
@@ -435,8 +530,28 @@ class AudioRecorderManager(
435
530
  Log.e(Constants.TAG, "Failed to send event", e)
436
531
  }
437
532
  }
533
+
534
+ if (recordingConfig.enableProcessing) {
535
+ processAudioData(audioData)
536
+ }
438
537
  }
439
538
 
539
+ private fun processAudioData(audioData: ByteArray) {
540
+ val audioAnalysisData = audioProcessor.processAudioData(audioData, recordingConfig)
541
+ val analysisBundle = audioAnalysisData.toBundle()
542
+
543
+ mainHandler.post {
544
+ try {
545
+ eventSender.sendExpoEvent(
546
+ Constants.AUDIO_ANALYSIS_EVENT_NAME, analysisBundle
547
+ )
548
+ } catch (e: Exception) {
549
+ Log.e(Constants.TAG, "Failed to send audio analysis event", e)
550
+ }
551
+ }
552
+ }
553
+
554
+
440
555
  private fun getCompressedAudioDuration(file: File?): Long {
441
556
  // Placeholder function for fetching duration from a compressed audio file
442
557
  // This would depend on how you store or can retrieve duration info for compressed formats
@@ -2,6 +2,7 @@ package net.siteed.audiostream
2
2
 
3
3
  object Constants {
4
4
  const val AUDIO_EVENT_NAME = "AudioData"
5
+ const val AUDIO_ANALYSIS_EVENT_NAME = "AudioAnalysis"
5
6
  const val DEFAULT_SAMPLE_RATE = 16000 // Default sample rate for audio recording
6
7
  const val DEFAULT_CHANNEL_CONFIG = 1 // Mono
7
8
  const val DEFAULT_AUDIO_FORMAT = 16 // 16-bit PCM
@@ -10,6 +10,7 @@ import expo.modules.kotlin.modules.ModuleDefinition
10
10
 
11
11
  class ExpoAudioStreamModule() : Module(), EventSender {
12
12
  private lateinit var audioRecorderManager: AudioRecorderManager
13
+ private lateinit var audioProcessor: AudioProcessor
13
14
 
14
15
  @RequiresApi(Build.VERSION_CODES.R)
15
16
  override fun definition() = ModuleDefinition {
@@ -18,7 +19,7 @@ class ExpoAudioStreamModule() : Module(), EventSender {
18
19
  // The module will be accessible from `requireNativeModule('ExpoAudioStream')` in JavaScript.
19
20
  Name("ExpoAudioStream")
20
21
 
21
- Events(Constants.AUDIO_EVENT_NAME)
22
+ Events(Constants.AUDIO_EVENT_NAME, Constants.AUDIO_ANALYSIS_EVENT_NAME)
22
23
 
23
24
  // Initialize AudioRecorderManager
24
25
  initializeManager()
@@ -43,6 +44,49 @@ class ExpoAudioStreamModule() : Module(), EventSender {
43
44
  audioRecorderManager.pauseRecording(promise)
44
45
  }
45
46
 
47
+
48
+ AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
49
+ val fileUri = options["fileUri"] as? String
50
+ val pointsPerSecond = (options["pointsPerSecond"] as? Double) ?: 20.0
51
+ val algorithm = options["algorithm"] as? String ?: "rms"
52
+ val featuresMap = options["features"] as? Map<*, *>
53
+ val features = featuresMap?.filterKeys { it is String }
54
+ ?.filterValues { it is Boolean }
55
+ ?.mapKeys { it.key as String }
56
+ ?.mapValues { it.value as Boolean }
57
+ ?: emptyMap()
58
+ val skipWavHeader = (options["skipWavHeader"] as? Boolean) ?: false
59
+
60
+ if (fileUri == null) {
61
+ promise.reject("INVALID_ARGUMENTS", "fileUri is required", null)
62
+ return@AsyncFunction
63
+ }
64
+
65
+ try {
66
+ val audioData = audioProcessor.loadAudioFile(fileUri, skipWavHeader)
67
+ if (audioData == null) {
68
+ promise.reject("PROCESSING_ERROR", "Failed to load audio file", null)
69
+ return@AsyncFunction
70
+ }
71
+
72
+ val recordingConfig = RecordingConfig(
73
+ sampleRate = audioData.sampleRate,
74
+ channels = audioData.channels,
75
+ encoding = "pcm_${audioData.bitDepth}bit",
76
+ pointsPerSecond = pointsPerSecond,
77
+ algorithm = algorithm,
78
+ features = features
79
+ )
80
+
81
+ Log.d("ExpoAudioStreamModule", "extractAudioAnalysis: $recordingConfig")
82
+
83
+ val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
84
+ promise.resolve(analysisData.toDictionary())
85
+ } catch (e: Exception) {
86
+ promise.reject("PROCESSING_ERROR", "Failed to process audio file: ${e.message}", e)
87
+ }
88
+ }
89
+
46
90
  AsyncFunction("resumeRecording") { promise: Promise ->
47
91
  audioRecorderManager.resumeRecording(promise)
48
92
  }
@@ -59,10 +103,12 @@ class ExpoAudioStreamModule() : Module(), EventSender {
59
103
  val audioEncoder = AudioDataEncoder()
60
104
  audioRecorderManager =
61
105
  AudioRecorderManager(androidContext.filesDir, permissionUtils, audioEncoder, this)
106
+ audioProcessor = AudioProcessor(androidContext.filesDir) // Instantiate here with filesDir
62
107
  }
63
108
 
64
109
  override fun sendExpoEvent(eventName: String, params: Bundle) {
65
- this@ExpoAudioStreamModule.sendEvent(Constants.AUDIO_EVENT_NAME, params)
110
+ Log.d(Constants.TAG, "Sending event: $eventName")
111
+ this@ExpoAudioStreamModule.sendEvent(eventName, params)
66
112
  }
67
113
 
68
114
  }
@@ -0,0 +1,44 @@
1
+ package net.siteed.audiostream
2
+
3
+ import kotlin.math.PI
4
+ import kotlin.math.cos
5
+ import kotlin.math.sin
6
+
7
+ class FFT(private val n: Int) {
8
+ private val cosTable = FloatArray(n / 2)
9
+ private val sinTable = FloatArray(n / 2)
10
+
11
+ init {
12
+ for (i in 0 until n / 2) {
13
+ cosTable[i] = cos(2.0 * PI * i / n).toFloat()
14
+ sinTable[i] = sin(2.0 * PI * i / n).toFloat()
15
+ }
16
+ }
17
+
18
+ fun realForward(data: FloatArray) {
19
+ realForwardRecursive(data)
20
+ }
21
+
22
+ private fun realForwardRecursive(data: FloatArray) {
23
+ val n = data.size
24
+ if (n <= 1) return
25
+
26
+ val even = FloatArray(n / 2)
27
+ val odd = FloatArray(n / 2)
28
+
29
+ for (i in 0 until n / 2) {
30
+ even[i] = data[2 * i]
31
+ odd[i] = data[2 * i + 1]
32
+ }
33
+
34
+ realForwardRecursive(even)
35
+ realForwardRecursive(odd)
36
+
37
+ for (i in 0 until n / 2) {
38
+ val t = cosTable[i] * odd[i] - sinTable[i] * even[i]
39
+ val u = sinTable[i] * odd[i] + cosTable[i] * even[i]
40
+ data[i] = even[i] + t
41
+ data[i + n / 2] = even[i] - t
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,56 @@
1
+ package net.siteed.audiostream
2
+
3
+ import android.os.Bundle
4
+ import androidx.core.os.bundleOf
5
+
6
+ data class Features(
7
+ val energy: Float = 0f,
8
+ val mfcc: List<Float> = emptyList(),
9
+ val rms: Float = 0f,
10
+ val minAmplitude: Float = 0f,
11
+ val maxAmplitude: Float = 0f,
12
+ val zcr: Float = 0f,
13
+ val spectralCentroid: Float = 0f,
14
+ val spectralFlatness: Float = 0f,
15
+ val spectralRollOff: Float = 0f,
16
+ val spectralBandwidth: Float = 0f,
17
+ val chromagram: List<Float> = emptyList(),
18
+ val tempo: Float = 0f,
19
+ val hnr: Float = 0f
20
+ ) {
21
+ fun toDictionary(): Map<String, Any> {
22
+ return mapOf(
23
+ "energy" to energy,
24
+ "mfcc" to mfcc,
25
+ "rms" to rms,
26
+ "minAmplitude" to minAmplitude,
27
+ "maxAmplitude" to maxAmplitude,
28
+ "zcr" to zcr,
29
+ "spectralCentroid" to spectralCentroid,
30
+ "spectralFlatness" to spectralFlatness,
31
+ "spectralRollOff" to spectralRollOff,
32
+ "spectralBandwidth" to spectralBandwidth,
33
+ "chromagram" to chromagram,
34
+ "tempo" to tempo,
35
+ "hnr" to hnr
36
+ )
37
+ }
38
+
39
+ fun toBundle(): Bundle {
40
+ return bundleOf(
41
+ "energy" to energy,
42
+ "mfcc" to mfcc,
43
+ "rms" to rms,
44
+ "minAmplitude" to minAmplitude,
45
+ "maxAmplitude" to maxAmplitude,
46
+ "zcr" to zcr,
47
+ "spectralCentroid" to spectralCentroid,
48
+ "spectralFlatness" to spectralFlatness,
49
+ "spectralRollOff" to spectralRollOff,
50
+ "spectralBandwidth" to spectralBandwidth,
51
+ "chromagram" to chromagram,
52
+ "tempo" to tempo,
53
+ "hnr" to hnr
54
+ )
55
+ }
56
+ }
@@ -0,0 +1,12 @@
1
+ package net.siteed.audiostream
2
+
3
+ data class RecordingConfig(
4
+ val sampleRate: Int = Constants.DEFAULT_SAMPLE_RATE,
5
+ val channels: Int = 1,
6
+ val encoding: String = "pcm_16bit",
7
+ val interval: Long = Constants.DEFAULT_INTERVAL,
8
+ val enableProcessing: Boolean = false,
9
+ val pointsPerSecond: Double = 20.0,
10
+ val algorithm: String = "rms",
11
+ val features: Map<String, Boolean> = emptyMap()
12
+ )