@siteed/expo-audio-stream 1.12.2 → 1.13.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.
- package/CHANGELOG.md +6 -2
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +866 -70
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +4 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +30 -9
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +163 -24
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +62 -0
- 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 +10 -1
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
- package/build/AudioAnalysis/extractAudioAnalysis.js +158 -0
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/index.d.ts +3 -2
- 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 +35 -16
- package/build/useAudioRecorder.js.map +1 -1
- package/ios/AudioProcessor.swift +391 -1
- package/ios/ExpoAudioStreamModule.swift +100 -0
- package/ios/Features.swift +30 -0
- package/package.json +1 -1
- package/plugin/build/index.d.ts +0 -1
- package/plugin/build/index.js +0 -5
- package/plugin/src/index.ts +0 -6
- package/src/AudioAnalysis/AudioAnalysis.types.ts +66 -0
- package/src/AudioAnalysis/extractAudioAnalysis.ts +219 -0
- package/src/index.ts +12 -1
- package/src/useAudioRecorder.tsx +37 -16
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
AmplitudeAlgorithm,
|
|
7
7
|
AudioAnalysis,
|
|
8
8
|
AudioFeaturesOptions,
|
|
9
|
+
AudioPreview,
|
|
10
|
+
DecodingConfig,
|
|
11
|
+
PreviewOptions,
|
|
9
12
|
} from './AudioAnalysis.types'
|
|
10
13
|
import { convertPCMToFloat32 } from '../utils/convertPCMToFloat32'
|
|
11
14
|
import { getWavFileInfo, WavFileInfo } from '../utils/getWavFileInfo'
|
|
@@ -27,6 +30,175 @@ export interface ExtractAudioAnalysisProps {
|
|
|
27
30
|
features?: AudioFeaturesOptions
|
|
28
31
|
featuresExtratorUrl?: string
|
|
29
32
|
logger?: ConsoleLike
|
|
33
|
+
decodingOptions?: DecodingConfig
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ExtractAudioFromAnyFormatProps
|
|
37
|
+
extends ExtractAudioAnalysisProps {
|
|
38
|
+
mimeType?: string
|
|
39
|
+
decodingOptions?: DecodingConfig
|
|
40
|
+
startTime?: number // Add start time in milliseconds
|
|
41
|
+
endTime?: number // Add end time in milliseconds
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function extractAudioFromAnyFormat({
|
|
45
|
+
fileUri,
|
|
46
|
+
arrayBuffer,
|
|
47
|
+
mimeType,
|
|
48
|
+
decodingOptions,
|
|
49
|
+
startTime,
|
|
50
|
+
endTime,
|
|
51
|
+
...restProps
|
|
52
|
+
}: ExtractAudioFromAnyFormatProps): Promise<AudioAnalysis> {
|
|
53
|
+
if (isWeb) {
|
|
54
|
+
try {
|
|
55
|
+
// Get the audio data
|
|
56
|
+
let audioBuffer: ArrayBuffer
|
|
57
|
+
if (arrayBuffer) {
|
|
58
|
+
audioBuffer = arrayBuffer
|
|
59
|
+
} else if (fileUri) {
|
|
60
|
+
const response = await fetch(fileUri)
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Failed to fetch fileUri: ${response.statusText}`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
audioBuffer = await response.arrayBuffer()
|
|
67
|
+
} else {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'Either arrayBuffer or fileUri must be provided'
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create audio context with target sample rate if specified
|
|
74
|
+
const audioContext = new (window.AudioContext ||
|
|
75
|
+
(window as any).webkitAudioContext)({
|
|
76
|
+
sampleRate: decodingOptions?.targetSampleRate,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Decode the audio data
|
|
80
|
+
const decodedAudioBuffer =
|
|
81
|
+
await audioContext.decodeAudioData(audioBuffer)
|
|
82
|
+
|
|
83
|
+
// Calculate the actual duration in milliseconds
|
|
84
|
+
const fullDurationMs = decodedAudioBuffer.duration * 1000
|
|
85
|
+
const effectiveDurationMs = endTime
|
|
86
|
+
? endTime - (startTime || 0)
|
|
87
|
+
: fullDurationMs - (startTime || 0)
|
|
88
|
+
|
|
89
|
+
// Create a new buffer for the selected range
|
|
90
|
+
const rangeLength = decodedAudioBuffer.length
|
|
91
|
+
const rangeBuffer = new AudioBuffer({
|
|
92
|
+
length: rangeLength,
|
|
93
|
+
numberOfChannels: decodedAudioBuffer.numberOfChannels,
|
|
94
|
+
sampleRate: decodedAudioBuffer.sampleRate,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Copy the selected range
|
|
98
|
+
for (
|
|
99
|
+
let channel = 0;
|
|
100
|
+
channel < decodedAudioBuffer.numberOfChannels;
|
|
101
|
+
channel++
|
|
102
|
+
) {
|
|
103
|
+
const channelData = decodedAudioBuffer.getChannelData(channel)
|
|
104
|
+
const rangeData = channelData.slice(0, rangeLength)
|
|
105
|
+
rangeBuffer.copyToChannel(rangeData, channel)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Use the range buffer instead of the full buffer
|
|
109
|
+
let processedBuffer = rangeBuffer
|
|
110
|
+
|
|
111
|
+
// Get original properties
|
|
112
|
+
const originalSampleRate = processedBuffer.sampleRate
|
|
113
|
+
const length = processedBuffer.length
|
|
114
|
+
|
|
115
|
+
// Determine target format
|
|
116
|
+
const targetChannels =
|
|
117
|
+
decodingOptions?.targetChannels ??
|
|
118
|
+
processedBuffer.numberOfChannels
|
|
119
|
+
const targetSampleRate =
|
|
120
|
+
decodingOptions?.targetSampleRate ?? processedBuffer.sampleRate
|
|
121
|
+
|
|
122
|
+
// Create offline context for resampling if needed
|
|
123
|
+
if (targetSampleRate !== originalSampleRate) {
|
|
124
|
+
const offlineCtx = new OfflineAudioContext(
|
|
125
|
+
targetChannels,
|
|
126
|
+
(length * targetSampleRate) / originalSampleRate,
|
|
127
|
+
targetSampleRate
|
|
128
|
+
)
|
|
129
|
+
const source = offlineCtx.createBufferSource()
|
|
130
|
+
source.buffer = processedBuffer
|
|
131
|
+
source.connect(offlineCtx.destination)
|
|
132
|
+
source.start()
|
|
133
|
+
processedBuffer = await offlineCtx.startRendering()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Convert to the desired format
|
|
137
|
+
const newLength = processedBuffer.length
|
|
138
|
+
let wavBuffer: Float32Array | Int16Array | Int8Array
|
|
139
|
+
|
|
140
|
+
// Create appropriate buffer based on target bit depth
|
|
141
|
+
switch (decodingOptions?.targetBitDepth) {
|
|
142
|
+
case 16:
|
|
143
|
+
wavBuffer = new Int16Array(newLength * targetChannels)
|
|
144
|
+
break
|
|
145
|
+
case 8:
|
|
146
|
+
wavBuffer = new Int8Array(newLength * targetChannels)
|
|
147
|
+
break
|
|
148
|
+
case 32:
|
|
149
|
+
default:
|
|
150
|
+
wavBuffer = new Float32Array(newLength * targetChannels)
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Interleave channels and handle bit depth conversion
|
|
155
|
+
const numChannels = Math.min(
|
|
156
|
+
processedBuffer.numberOfChannels,
|
|
157
|
+
targetChannels
|
|
158
|
+
)
|
|
159
|
+
for (let channel = 0; channel < numChannels; channel++) {
|
|
160
|
+
const channelData = processedBuffer.getChannelData(channel)
|
|
161
|
+
for (let i = 0; i < newLength; i++) {
|
|
162
|
+
let sample = channelData[i]
|
|
163
|
+
|
|
164
|
+
// Normalize if requested
|
|
165
|
+
if (decodingOptions?.normalizeAudio) {
|
|
166
|
+
sample = Math.max(-1, Math.min(1, sample))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Convert sample based on target bit depth
|
|
170
|
+
if (decodingOptions?.targetBitDepth === 16) {
|
|
171
|
+
sample = sample * 32767 // Convert to 16-bit range
|
|
172
|
+
} else if (decodingOptions?.targetBitDepth === 8) {
|
|
173
|
+
sample = sample * 127 // Convert to 8-bit range
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
wavBuffer[i * targetChannels + channel] = sample
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Pass the duration to extractAudioAnalysis
|
|
181
|
+
return await extractAudioAnalysis({
|
|
182
|
+
arrayBuffer: wavBuffer.buffer as ArrayBuffer,
|
|
183
|
+
bitDepth: decodingOptions?.targetBitDepth ?? 32,
|
|
184
|
+
skipWavHeader: true,
|
|
185
|
+
sampleRate: targetSampleRate,
|
|
186
|
+
numberOfChannels: targetChannels,
|
|
187
|
+
durationMs: effectiveDurationMs,
|
|
188
|
+
...restProps,
|
|
189
|
+
})
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Failed to process audio:', error)
|
|
192
|
+
throw error
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
// For native platforms, pass through all options
|
|
196
|
+
return await extractAudioAnalysis({
|
|
197
|
+
fileUri,
|
|
198
|
+
decodingOptions,
|
|
199
|
+
...restProps,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
30
202
|
}
|
|
31
203
|
|
|
32
204
|
export const extractAudioAnalysis = async ({
|
|
@@ -145,3 +317,50 @@ export const extractAudioAnalysis = async ({
|
|
|
145
317
|
return res
|
|
146
318
|
}
|
|
147
319
|
}
|
|
320
|
+
|
|
321
|
+
export async function extractPreview({
|
|
322
|
+
fileUri,
|
|
323
|
+
numberOfPoints,
|
|
324
|
+
algorithm = 'rms',
|
|
325
|
+
startTime,
|
|
326
|
+
endTime,
|
|
327
|
+
decodingOptions,
|
|
328
|
+
}: PreviewOptions): Promise<AudioPreview> {
|
|
329
|
+
if (isWeb) {
|
|
330
|
+
// For web, we can reuse the existing extractAudioFromAnyFormat with modified parameters
|
|
331
|
+
const analysis = await extractAudioFromAnyFormat({
|
|
332
|
+
fileUri,
|
|
333
|
+
algorithm,
|
|
334
|
+
decodingOptions,
|
|
335
|
+
startTime, // Pass startTime
|
|
336
|
+
endTime, // Pass endTime
|
|
337
|
+
pointsPerSecond:
|
|
338
|
+
(numberOfPoints ?? 100) /
|
|
339
|
+
((endTime ? endTime - (startTime || 0) : 1000) / 1000), // Adjust points per second calculation
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// Convert AudioAnalysis to AudioPreview format and adjust duration
|
|
343
|
+
return {
|
|
344
|
+
pointsPerSecond: analysis.pointsPerSecond,
|
|
345
|
+
durationMs: endTime
|
|
346
|
+
? endTime - (startTime || 0)
|
|
347
|
+
: analysis.durationMs, // Use range duration if specified
|
|
348
|
+
amplitudeRange: analysis.amplitudeRange,
|
|
349
|
+
dataPoints: analysis.dataPoints.map((point) => ({
|
|
350
|
+
id: point.id,
|
|
351
|
+
amplitude: point.amplitude,
|
|
352
|
+
startTime: point.startTime,
|
|
353
|
+
endTime: point.endTime,
|
|
354
|
+
})),
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return await ExpoAudioStreamModule.extractPreview({
|
|
359
|
+
fileUri,
|
|
360
|
+
numberOfPoints,
|
|
361
|
+
algorithm,
|
|
362
|
+
startTime,
|
|
363
|
+
endTime,
|
|
364
|
+
decodingOptions,
|
|
365
|
+
})
|
|
366
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
extractAudioAnalysis,
|
|
5
|
+
extractAudioFromAnyFormat,
|
|
6
|
+
extractPreview,
|
|
7
|
+
} from './AudioAnalysis/extractAudioAnalysis'
|
|
4
8
|
import {
|
|
5
9
|
AudioRecorderProvider,
|
|
6
10
|
useSharedAudioRecorder,
|
|
@@ -16,9 +20,16 @@ export {
|
|
|
16
20
|
AudioRecorderProvider,
|
|
17
21
|
ExpoAudioStreamModule,
|
|
18
22
|
extractAudioAnalysis,
|
|
23
|
+
extractAudioFromAnyFormat,
|
|
24
|
+
extractPreview,
|
|
19
25
|
useAudioRecorder,
|
|
20
26
|
useSharedAudioRecorder,
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export type * from './AudioAnalysis/AudioAnalysis.types'
|
|
30
|
+
|
|
24
31
|
export type * from './ExpoAudioStream.types'
|
|
32
|
+
export type {
|
|
33
|
+
ExtractAudioAnalysisProps,
|
|
34
|
+
ExtractAudioFromAnyFormatProps,
|
|
35
|
+
} from './AudioAnalysis/extractAudioAnalysis'
|
package/src/useAudioRecorder.tsx
CHANGED
|
@@ -178,6 +178,14 @@ export function useAudioRecorder({
|
|
|
178
178
|
((_: AudioDataEvent) => Promise<void>) | null
|
|
179
179
|
>(null)
|
|
180
180
|
|
|
181
|
+
const stateRef = useRef({
|
|
182
|
+
isRecording: false,
|
|
183
|
+
isPaused: false,
|
|
184
|
+
durationMs: 0,
|
|
185
|
+
size: 0,
|
|
186
|
+
compression: undefined as CompressionInfo | undefined,
|
|
187
|
+
})
|
|
188
|
+
|
|
181
189
|
const handleAudioAnalysis = useCallback(
|
|
182
190
|
async ({
|
|
183
191
|
analysis,
|
|
@@ -373,15 +381,17 @@ export function useAudioRecorder({
|
|
|
373
381
|
try {
|
|
374
382
|
const status: AudioStreamStatus = ExpoAudioStream.status()
|
|
375
383
|
logger?.debug(
|
|
376
|
-
`Status: paused: ${status.isPaused} durationMs: ${status.durationMs} size: ${status.size}`,
|
|
384
|
+
`Status: paused: ${status.isPaused} isRecording: ${status.isRecording} durationMs: ${status.durationMs} size: ${status.size}`,
|
|
377
385
|
status.compression
|
|
378
386
|
)
|
|
379
387
|
|
|
380
388
|
// Only dispatch if values actually changed
|
|
381
389
|
if (
|
|
382
|
-
status.isRecording !==
|
|
383
|
-
status.isPaused !==
|
|
390
|
+
status.isRecording !== stateRef.current.isRecording ||
|
|
391
|
+
status.isPaused !== stateRef.current.isPaused
|
|
384
392
|
) {
|
|
393
|
+
stateRef.current.isRecording = status.isRecording
|
|
394
|
+
stateRef.current.isPaused = status.isPaused
|
|
385
395
|
dispatch({
|
|
386
396
|
type: 'UPDATE_RECORDING_STATE',
|
|
387
397
|
payload: {
|
|
@@ -391,13 +401,13 @@ export function useAudioRecorder({
|
|
|
391
401
|
})
|
|
392
402
|
}
|
|
393
403
|
|
|
394
|
-
// Only dispatch if progress values changed
|
|
395
404
|
if (
|
|
396
|
-
status.durationMs !==
|
|
397
|
-
status.size !==
|
|
398
|
-
JSON.stringify(status.compression) !==
|
|
399
|
-
JSON.stringify(state.compression)
|
|
405
|
+
status.durationMs !== stateRef.current.durationMs ||
|
|
406
|
+
status.size !== stateRef.current.size
|
|
400
407
|
) {
|
|
408
|
+
stateRef.current.durationMs = status.durationMs
|
|
409
|
+
stateRef.current.size = status.size
|
|
410
|
+
stateRef.current.compression = status.compression
|
|
401
411
|
dispatch({
|
|
402
412
|
type: 'UPDATE_STATUS',
|
|
403
413
|
payload: {
|
|
@@ -410,6 +420,17 @@ export function useAudioRecorder({
|
|
|
410
420
|
} catch (error) {
|
|
411
421
|
logger?.error(`Error getting status:`, error)
|
|
412
422
|
}
|
|
423
|
+
}, [ExpoAudioStream, logger]) // Only depend on ExpoAudioStream and logger
|
|
424
|
+
|
|
425
|
+
// Update ref when state changes
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
stateRef.current = {
|
|
428
|
+
isRecording: state.isRecording,
|
|
429
|
+
isPaused: state.isPaused,
|
|
430
|
+
durationMs: state.durationMs,
|
|
431
|
+
size: state.size,
|
|
432
|
+
compression: state.compression,
|
|
433
|
+
}
|
|
413
434
|
}, [
|
|
414
435
|
state.isRecording,
|
|
415
436
|
state.isPaused,
|
|
@@ -499,18 +520,18 @@ export function useAudioRecorder({
|
|
|
499
520
|
useEffect(() => {
|
|
500
521
|
let intervalId: NodeJS.Timeout | undefined
|
|
501
522
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
intervalId = setTimeout(runInterval, 1000)
|
|
506
|
-
}
|
|
507
|
-
}
|
|
523
|
+
if (state.isRecording || state.isPaused) {
|
|
524
|
+
// Immediately check status when starting
|
|
525
|
+
checkStatus()
|
|
508
526
|
|
|
509
|
-
|
|
527
|
+
// Start interval
|
|
528
|
+
intervalId = setInterval(checkStatus, 1000)
|
|
529
|
+
}
|
|
510
530
|
|
|
511
531
|
return () => {
|
|
512
532
|
if (intervalId) {
|
|
513
|
-
|
|
533
|
+
clearInterval(intervalId)
|
|
534
|
+
intervalId = undefined
|
|
514
535
|
}
|
|
515
536
|
}
|
|
516
537
|
}, [checkStatus, state.isRecording, state.isPaused])
|