@siteed/expo-audio-stream 1.12.3 → 1.13.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 +9 -4
- package/README.md +20 -18
- 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.js +9 -9
- 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 +9 -9
|
@@ -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
|
@@ -381,7 +381,7 @@ export function useAudioRecorder({
|
|
|
381
381
|
try {
|
|
382
382
|
const status: AudioStreamStatus = ExpoAudioStream.status()
|
|
383
383
|
logger?.debug(
|
|
384
|
-
`Status: paused: ${status.isPaused} durationMs: ${status.durationMs} size: ${status.size}`,
|
|
384
|
+
`Status: paused: ${status.isPaused} isRecording: ${status.isRecording} durationMs: ${status.durationMs} size: ${status.size}`,
|
|
385
385
|
status.compression
|
|
386
386
|
)
|
|
387
387
|
|
|
@@ -520,18 +520,18 @@ export function useAudioRecorder({
|
|
|
520
520
|
useEffect(() => {
|
|
521
521
|
let intervalId: NodeJS.Timeout | undefined
|
|
522
522
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
intervalId = setTimeout(runInterval, 1000)
|
|
527
|
-
}
|
|
528
|
-
}
|
|
523
|
+
if (state.isRecording || state.isPaused) {
|
|
524
|
+
// Immediately check status when starting
|
|
525
|
+
checkStatus()
|
|
529
526
|
|
|
530
|
-
|
|
527
|
+
// Start interval
|
|
528
|
+
intervalId = setInterval(checkStatus, 1000)
|
|
529
|
+
}
|
|
531
530
|
|
|
532
531
|
return () => {
|
|
533
532
|
if (intervalId) {
|
|
534
|
-
|
|
533
|
+
clearInterval(intervalId)
|
|
534
|
+
intervalId = undefined
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
}, [checkStatus, state.isRecording, state.isPaused])
|