@siteed/expo-audio-stream 1.17.0 → 2.0.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +26 -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 +0 -2
  7. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +35 -29
  8. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +236 -96
  9. package/android/src/main/java/net/siteed/audiostream/FFT.kt +55 -0
  10. package/android/src/main/java/net/siteed/audiostream/Features.kt +49 -7
  11. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +2 -4
  12. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +55 -47
  13. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  14. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  15. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +60 -13
  16. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
  17. package/build/AudioAnalysis/extractAudioAnalysis.js +147 -162
  18. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  19. package/build/ExpoAudioStream.types.d.ts +47 -3
  20. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  21. package/build/ExpoAudioStream.types.js.map +1 -1
  22. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  23. package/build/ExpoAudioStream.web.js +0 -1
  24. package/build/ExpoAudioStream.web.js.map +1 -1
  25. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  26. package/build/ExpoAudioStreamModule.js +216 -12
  27. package/build/ExpoAudioStreamModule.js.map +1 -1
  28. package/build/WebRecorder.web.d.ts +67 -13
  29. package/build/WebRecorder.web.d.ts.map +1 -1
  30. package/build/WebRecorder.web.js +177 -173
  31. package/build/WebRecorder.web.js.map +1 -1
  32. package/build/index.d.ts +3 -3
  33. package/build/index.d.ts.map +1 -1
  34. package/build/index.js +2 -2
  35. package/build/index.js.map +1 -1
  36. package/build/useAudioRecorder.d.ts.map +1 -1
  37. package/build/useAudioRecorder.js +12 -8
  38. package/build/useAudioRecorder.js.map +1 -1
  39. package/build/utils/audioProcessing.d.ts +24 -0
  40. package/build/utils/audioProcessing.d.ts.map +1 -0
  41. package/build/utils/audioProcessing.js +133 -0
  42. package/build/utils/audioProcessing.js.map +1 -0
  43. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  44. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  45. package/build/workers/InlineFeaturesExtractor.web.js +694 -194
  46. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  47. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  48. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  49. package/build/workers/inlineAudioWebWorker.web.js +3 -2
  50. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  51. package/ios/AudioAnalysisData.swift +51 -16
  52. package/ios/AudioProcessingHelpers.swift +710 -26
  53. package/ios/AudioProcessor.swift +334 -185
  54. package/ios/AudioStreamManager.swift +2 -3
  55. package/ios/DataPoint.swift +25 -12
  56. package/ios/DecodingConfig.swift +47 -0
  57. package/ios/ExpoAudioStreamModule.swift +187 -103
  58. package/ios/FFT.swift +62 -0
  59. package/ios/Features.swift +24 -3
  60. package/ios/RecordingSettings.swift +7 -7
  61. package/package.json +2 -1
  62. package/plugin/build/index.js +6 -1
  63. package/plugin/src/index.ts +9 -1
  64. package/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
  65. package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
  66. package/src/ExpoAudioStream.types.ts +53 -7
  67. package/src/ExpoAudioStream.web.ts +0 -1
  68. package/src/ExpoAudioStreamModule.ts +255 -10
  69. package/src/WebRecorder.web.ts +231 -244
  70. package/src/index.ts +5 -3
  71. package/src/useAudioRecorder.tsx +14 -10
  72. package/src/utils/audioProcessing.ts +205 -0
  73. package/src/workers/InlineFeaturesExtractor.web.tsx +694 -194
  74. package/src/workers/inlineAudioWebWorker.web.tsx +3 -2
@@ -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 {