@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.
- package/CHANGELOG.md +26 -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 +0 -2
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +35 -29
- 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 +2 -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 +47 -3
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +0 -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 +177 -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 +694 -194
- 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 +2 -3
- package/ios/DataPoint.swift +25 -12
- package/ios/DecodingConfig.swift +47 -0
- package/ios/ExpoAudioStreamModule.swift +187 -103
- package/ios/FFT.swift +62 -0
- package/ios/Features.swift +24 -3
- package/ios/RecordingSettings.swift +7 -7
- package/package.json +2 -1
- package/plugin/build/index.js +6 -1
- package/plugin/src/index.ts +9 -1
- package/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
- package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
- package/src/ExpoAudioStream.types.ts +53 -7
- package/src/ExpoAudioStream.web.ts +0 -1
- package/src/ExpoAudioStreamModule.ts +255 -10
- package/src/WebRecorder.web.ts +231 -244
- 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 +694 -194
- 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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 {
|