@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
@@ -46,9 +46,11 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
46
46
  currentDurationMs: number
47
47
  currentSize: number
48
48
  currentInterval: number
49
+ currentIntervalAnalysis: number
49
50
  lastEmittedSize: number
50
51
  lastEmittedTime: number
51
52
  lastEmittedCompressionSize: number
53
+ lastEmittedAnalysisTime: number
52
54
  streamUuid: string | null
53
55
  extension: 'webm' | 'wav' = 'wav' // Default extension is 'wav'
54
56
  recordingConfig?: RecordingConfig
@@ -87,10 +89,12 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
87
89
  this.currentSize = 0
88
90
  this.bitDepth = 32 // Default
89
91
  this.currentInterval = 1000 // Default interval in ms
92
+ this.currentIntervalAnalysis = 500 // Default analysis interval in ms
90
93
  this.lastEmittedSize = 0
91
94
  this.lastEmittedTime = 0
92
95
  this.latestPosition = 0
93
96
  this.lastEmittedCompressionSize = 0
97
+ this.lastEmittedAnalysisTime = 0
94
98
  this.streamUuid = null // Initialize UUID on first recording start
95
99
  this.audioWorkletUrl = audioWorkletUrl
96
100
  this.featuresExtratorUrl = featuresExtratorUrl
@@ -132,7 +136,6 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
132
136
  audioContext,
133
137
  source,
134
138
  recordingConfig,
135
- audioWorkletUrl: this.audioWorkletUrl,
136
139
  emitAudioEventCallback: ({
137
140
  data,
138
141
  position,
@@ -172,6 +175,9 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
172
175
  this.lastEmittedSize = 0
173
176
  this.lastEmittedTime = 0
174
177
  this.lastEmittedCompressionSize = 0
178
+ this.currentInterval = recordingConfig.interval ?? 1000
179
+ this.currentIntervalAnalysis = recordingConfig.intervalAnalysis ?? 500
180
+ this.lastEmittedAnalysisTime = Date.now()
175
181
 
176
182
  // Use custom filename if provided, otherwise fallback to timestamp
177
183
  if (recordingConfig.filename) {
@@ -335,6 +341,7 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
335
341
  durationMs: this.currentDurationMs,
336
342
  size: this.currentSize,
337
343
  interval: this.currentInterval,
344
+ intervalAnalysis: this.currentIntervalAnalysis,
338
345
  mimeType: `audio/${this.extension}`,
339
346
  compression: this.recordingConfig?.compression?.enabled
340
347
  ? {
@@ -1,10 +1,18 @@
1
+ import crc32 from 'crc-32'
1
2
  import { requireNativeModule } from 'expo-modules-core'
2
3
  import { Platform } from 'react-native'
3
4
 
5
+ import {
6
+ ExtractAudioDataOptions,
7
+ ExtractedAudioData,
8
+ BitDepth,
9
+ } from './ExpoAudioStream.types'
4
10
  import {
5
11
  ExpoAudioStreamWeb,
6
12
  ExpoAudioStreamWebProps,
7
13
  } from './ExpoAudioStream.web'
14
+ import { processAudioBuffer } from './utils/audioProcessing'
15
+ import { writeWavHeader } from './utils/writeWavHeader'
8
16
 
9
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
18
  let ExpoAudioStreamModule: any
@@ -19,19 +27,256 @@ if (Platform.OS === 'web') {
19
27
  return instance
20
28
  }
21
29
  ExpoAudioStreamModule.requestPermissionsAsync = async () => {
22
- return {
23
- status: 'granted',
24
- granted: true,
25
- expires: 'never',
26
- canAskAgain: true,
30
+ try {
31
+ const stream = await navigator.mediaDevices.getUserMedia({
32
+ audio: true,
33
+ })
34
+ stream.getTracks().forEach((track) => track.stop())
35
+ return {
36
+ status: 'granted',
37
+ expires: 'never',
38
+ canAskAgain: true,
39
+ granted: true,
40
+ }
41
+ } catch {
42
+ return {
43
+ status: 'denied',
44
+ expires: 'never',
45
+ canAskAgain: true,
46
+ granted: false,
47
+ }
27
48
  }
28
49
  }
29
50
  ExpoAudioStreamModule.getPermissionsAsync = async () => {
30
- return {
31
- status: 'granted',
32
- granted: true,
33
- expires: 'never',
34
- canAskAgain: true,
51
+ let maybeStatus: string | null = null
52
+
53
+ if (navigator?.permissions?.query) {
54
+ try {
55
+ const { state } = await navigator.permissions.query({
56
+ name: 'microphone' as PermissionName,
57
+ })
58
+ maybeStatus = state
59
+ } catch {
60
+ maybeStatus = null
61
+ }
62
+ }
63
+
64
+ switch (maybeStatus) {
65
+ case 'granted':
66
+ return {
67
+ status: 'granted',
68
+ expires: 'never',
69
+ canAskAgain: true,
70
+ granted: true,
71
+ }
72
+ case 'denied':
73
+ return {
74
+ status: 'denied',
75
+ expires: 'never',
76
+ canAskAgain: true,
77
+ granted: false,
78
+ }
79
+ default:
80
+ return await ExpoAudioStreamModule.requestPermissionsAsync()
81
+ }
82
+ }
83
+ ExpoAudioStreamModule.extractAudioData = async (
84
+ options: ExtractAudioDataOptions
85
+ ): Promise<ExtractedAudioData> => {
86
+ try {
87
+ const {
88
+ fileUri,
89
+ position,
90
+ length,
91
+ startTimeMs,
92
+ endTimeMs,
93
+ decodingOptions,
94
+ includeNormalizedData,
95
+ includeBase64Data,
96
+ includeWavHeader = false,
97
+ logger,
98
+ } = options
99
+
100
+ logger?.debug('EXTRACT AUDIO - Step 1: Initial request', {
101
+ fileUri,
102
+ extractionParams: {
103
+ position,
104
+ length,
105
+ startTimeMs,
106
+ endTimeMs,
107
+ },
108
+ decodingOptions: {
109
+ targetSampleRate:
110
+ decodingOptions?.targetSampleRate ?? 16000,
111
+ targetChannels: decodingOptions?.targetChannels ?? 1,
112
+ targetBitDepth: decodingOptions?.targetBitDepth ?? 16,
113
+ normalizeAudio: decodingOptions?.normalizeAudio ?? false,
114
+ },
115
+ outputOptions: {
116
+ includeNormalizedData,
117
+ includeBase64Data,
118
+ includeWavHeader,
119
+ },
120
+ })
121
+
122
+ // Process the audio using shared helper function
123
+ const processedBuffer = await processAudioBuffer({
124
+ fileUri,
125
+ targetSampleRate: decodingOptions?.targetSampleRate ?? 16000,
126
+ targetChannels: decodingOptions?.targetChannels ?? 1,
127
+ normalizeAudio: decodingOptions?.normalizeAudio ?? false,
128
+ position,
129
+ length,
130
+ startTimeMs,
131
+ endTimeMs,
132
+ logger,
133
+ })
134
+
135
+ logger?.debug('EXTRACT AUDIO - Step 2: Audio processing complete', {
136
+ processedData: {
137
+ samples: processedBuffer.samples,
138
+ sampleRate: processedBuffer.sampleRate,
139
+ channels: processedBuffer.channels,
140
+ durationMs: processedBuffer.durationMs,
141
+ },
142
+ })
143
+
144
+ const channelData = processedBuffer.channelData
145
+ const bitDepth = (decodingOptions?.targetBitDepth ?? 16) as BitDepth
146
+ const bytesPerSample = bitDepth / 8
147
+ const numSamples = processedBuffer.samples
148
+
149
+ logger?.debug('EXTRACT AUDIO - Step 3: PCM conversion setup', {
150
+ channelData: {
151
+ length: channelData.length,
152
+ first: channelData[0],
153
+ last: channelData[channelData.length - 1],
154
+ },
155
+ calculation: {
156
+ bitDepth,
157
+ bytesPerSample,
158
+ numSamples,
159
+ expectedBytes: numSamples * bytesPerSample,
160
+ },
161
+ })
162
+
163
+ // Create PCM data with correct length based on original byte length
164
+ const pcmData = new Uint8Array(numSamples * bytesPerSample)
165
+ let offset = 0
166
+
167
+ // Convert Float32 samples to PCM format
168
+ for (let i = 0; i < numSamples; i++) {
169
+ const sample = channelData[i]
170
+ const value = Math.max(-1, Math.min(1, sample))
171
+ // Convert to 16-bit signed integer
172
+ let intValue = Math.round(value * 32767)
173
+
174
+ // Handle negative values correctly
175
+ if (intValue < 0) {
176
+ intValue = 65536 + intValue
177
+ }
178
+
179
+ // Write as little-endian
180
+ pcmData[offset++] = intValue & 255 // Low byte
181
+ pcmData[offset++] = (intValue >> 8) & 255 // High byte
182
+ }
183
+
184
+ const durationMs = Math.round(
185
+ (numSamples / processedBuffer.sampleRate) * 1000
186
+ )
187
+
188
+ logger?.debug('EXTRACT AUDIO - Step 4: Final output', {
189
+ pcmData: {
190
+ length: pcmData.length,
191
+ first: pcmData[0],
192
+ last: pcmData[pcmData.length - 1],
193
+ },
194
+ timing: {
195
+ numSamples,
196
+ sampleRate: processedBuffer.sampleRate,
197
+ durationMs,
198
+ shouldBe3000ms: endTimeMs
199
+ ? endTimeMs - (startTimeMs ?? 0) === 3000
200
+ : undefined,
201
+ },
202
+ })
203
+
204
+ const result: ExtractedAudioData = {
205
+ pcmData: new Uint8Array(pcmData.buffer),
206
+ sampleRate: processedBuffer.sampleRate,
207
+ channels: processedBuffer.channels,
208
+ bitDepth,
209
+ durationMs,
210
+ format: `pcm_${bitDepth}bit` as const,
211
+ samples: numSamples,
212
+ }
213
+
214
+ // Add WAV header if requested
215
+ if (includeWavHeader) {
216
+ logger?.debug('EXTRACT AUDIO - Step 4: Adding WAV header', {
217
+ originalLength: pcmData.length,
218
+ newLength: result.pcmData.length,
219
+ firstBytes: Array.from(result.pcmData.slice(0, 44)), // WAV header is 44 bytes
220
+ })
221
+ const wavBuffer = writeWavHeader({
222
+ buffer: pcmData.buffer.slice(0, pcmData.length),
223
+ sampleRate: processedBuffer.sampleRate,
224
+ numChannels: processedBuffer.channels,
225
+ bitDepth,
226
+ })
227
+ result.pcmData = new Uint8Array(wavBuffer)
228
+ result.hasWavHeader = true
229
+ }
230
+
231
+ if (includeNormalizedData) {
232
+ // // Simple approach: Create normalized data directly from the PCM data
233
+ // // Just convert to -1 to 1 range without any amplification
234
+ // const normalizedData = new Float32Array(numSamples)
235
+
236
+ // // Convert the PCM data to float values
237
+ // for (let i = 0; i < numSamples; i++) {
238
+ // // Get the 16-bit PCM value (little endian)
239
+ // const lowByte = pcmData[i * 2]
240
+ // const highByte = pcmData[i * 2 + 1]
241
+ // const pcmValue = (highByte << 8) | lowByte
242
+
243
+ // // Convert to signed 16-bit value
244
+ // const signedValue =
245
+ // pcmValue > 32767 ? pcmValue - 65536 : pcmValue
246
+
247
+ // // Normalize to float between -1 and 1
248
+ // normalizedData[i] = signedValue / 32768.0
249
+ // }
250
+ // Store the normalized data in the result
251
+ result.normalizedData = channelData
252
+ }
253
+
254
+ if (includeBase64Data) {
255
+ // Convert the PCM data to a base64 string
256
+ const binary = Array.from(new Uint8Array(pcmData.buffer))
257
+ .map((b) => String.fromCharCode(b))
258
+ .join('')
259
+ result.base64Data = btoa(binary)
260
+ }
261
+
262
+ if (options.computeChecksum) {
263
+ result.checksum = crc32.buf(pcmData)
264
+ }
265
+
266
+ logger?.debug('EXTRACT AUDIO - Step 3: PCM conversion complete', {
267
+ pcmStats: {
268
+ length: pcmData.length,
269
+ bytesPerSample,
270
+ totalSamples: numSamples,
271
+ firstBytes: Array.from(pcmData.slice(0, 16)),
272
+ lastBytes: Array.from(pcmData.slice(-16)),
273
+ },
274
+ })
275
+
276
+ return result
277
+ } catch (error) {
278
+ options.logger?.error('EXTRACT AUDIO - Error:', error)
279
+ throw error
35
280
  }
36
281
  }
37
282
  } else {