@siteed/expo-audio-stream 1.17.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.
- package/CHANGELOG.md +21 -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/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,220 +1,239 @@
|
|
|
1
1
|
// packages/expo-audio-stream/src/AudioAnalysis/extractAudioAnalysis.ts
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* This module provides functions for extracting and analyzing audio data.
|
|
4
|
+
* - `extractAudioAnalysis`: For detailed analysis with customizable ranges and decoding options.
|
|
5
|
+
* - `extractWavAudioAnalysis`: For analyzing WAV files without decoding, preserving original PCM values.
|
|
6
|
+
* - `extractPreview`: For generating quick previews of audio waveforms, optimized for UI rendering.
|
|
7
|
+
*/
|
|
8
|
+
import crc32 from 'crc-32'
|
|
9
|
+
|
|
10
|
+
import { ConsoleLike, ExtractAudioDataOptions } from '../ExpoAudioStream.types'
|
|
3
11
|
import ExpoAudioStreamModule from '../ExpoAudioStreamModule'
|
|
4
12
|
import { isWeb } from '../constants'
|
|
5
13
|
import {
|
|
6
|
-
AmplitudeAlgorithm,
|
|
7
14
|
AudioAnalysis,
|
|
8
15
|
AudioFeaturesOptions,
|
|
9
|
-
|
|
16
|
+
DataPoint,
|
|
10
17
|
DecodingConfig,
|
|
11
18
|
PreviewOptions,
|
|
12
19
|
} from './AudioAnalysis.types'
|
|
20
|
+
import { processAudioBuffer } from '../utils/audioProcessing'
|
|
13
21
|
import { convertPCMToFloat32 } from '../utils/convertPCMToFloat32'
|
|
14
22
|
import { getWavFileInfo, WavFileInfo } from '../utils/getWavFileInfo'
|
|
15
23
|
import { InlineFeaturesExtractor } from '../workers/InlineFeaturesExtractor.web'
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
function calculateCRC32ForDataPoint(data: Float32Array): number {
|
|
26
|
+
// Convert float array to byte array for CRC32
|
|
27
|
+
const byteArray = new Uint8Array(data.length * 4)
|
|
28
|
+
const dataView = new DataView(byteArray.buffer)
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < data.length; i++) {
|
|
31
|
+
dataView.setFloat32(i * 4, data[i], true)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return crc32.buf(byteArray)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ExtractWavAudioAnalysisProps {
|
|
18
38
|
fileUri?: string // should provide either fileUri or arrayBuffer
|
|
19
39
|
wavMetadata?: WavFileInfo
|
|
20
40
|
arrayBuffer?: ArrayBuffer
|
|
21
41
|
bitDepth?: number
|
|
22
|
-
skipWavHeader?: boolean
|
|
23
42
|
durationMs?: number
|
|
24
43
|
sampleRate?: number
|
|
25
44
|
numberOfChannels?: number
|
|
26
|
-
algorithm?: AmplitudeAlgorithm
|
|
27
45
|
position?: number // Optional number of bytes to skip. Default is 0
|
|
28
46
|
length?: number // Optional number of bytes to read.
|
|
29
|
-
|
|
47
|
+
segmentDurationMs?: number // Optional number of points per second. Use to reduce the number of points and compute the number of datapoints to return.
|
|
30
48
|
features?: AudioFeaturesOptions
|
|
31
49
|
featuresExtratorUrl?: string
|
|
32
50
|
logger?: ConsoleLike
|
|
33
51
|
decodingOptions?: DecodingConfig
|
|
34
52
|
}
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
// Define base options interface with common properties
|
|
55
|
+
interface BaseExtractOptions {
|
|
56
|
+
fileUri?: string
|
|
57
|
+
arrayBuffer?: ArrayBuffer
|
|
58
|
+
/**
|
|
59
|
+
* Duration of each analysis segment in milliseconds. Defaults to 100ms if not specified.
|
|
60
|
+
*/
|
|
61
|
+
segmentDurationMs?: number
|
|
62
|
+
features?: AudioFeaturesOptions
|
|
39
63
|
decodingOptions?: DecodingConfig
|
|
40
|
-
|
|
41
|
-
endTime?: number // Add end time in milliseconds
|
|
64
|
+
logger?: ConsoleLike
|
|
42
65
|
}
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
// Time-based range options
|
|
68
|
+
interface TimeRangeOptions extends BaseExtractOptions {
|
|
69
|
+
startTimeMs?: number
|
|
70
|
+
endTimeMs?: number
|
|
71
|
+
position?: never
|
|
72
|
+
length?: never
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Byte-based range options
|
|
76
|
+
interface ByteRangeOptions extends BaseExtractOptions {
|
|
77
|
+
position?: number
|
|
78
|
+
length?: number
|
|
79
|
+
startTimeMs?: never
|
|
80
|
+
endTimeMs?: never
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Options for extracting audio analysis.
|
|
85
|
+
* - For time-based analysis, provide `startTimeMs` and `endTimeMs`.
|
|
86
|
+
* - For byte-based analysis, provide `position` and `length`.
|
|
87
|
+
* - Do not mix time and byte ranges.
|
|
88
|
+
*/
|
|
89
|
+
export type ExtractAudioAnalysisProps = TimeRangeOptions | ByteRangeOptions
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extracts detailed audio analysis from the specified audio file or buffer.
|
|
93
|
+
* Supports either time-based or byte-based ranges for flexibility in analysis.
|
|
94
|
+
*
|
|
95
|
+
* @param props - The options for extraction, including file URI, ranges, and decoding settings.
|
|
96
|
+
* @returns A promise that resolves to the audio analysis data.
|
|
97
|
+
* @throws {Error} If both time and byte ranges are provided or if required parameters are missing.
|
|
98
|
+
*/
|
|
99
|
+
export async function extractAudioAnalysis(
|
|
100
|
+
props: ExtractAudioAnalysisProps
|
|
101
|
+
): Promise<AudioAnalysis> {
|
|
102
|
+
const {
|
|
103
|
+
fileUri,
|
|
104
|
+
arrayBuffer,
|
|
105
|
+
decodingOptions,
|
|
106
|
+
logger,
|
|
107
|
+
segmentDurationMs = 100,
|
|
108
|
+
features,
|
|
109
|
+
} = props
|
|
110
|
+
|
|
53
111
|
if (isWeb) {
|
|
54
112
|
try {
|
|
55
|
-
//
|
|
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
|
|
113
|
+
// Create AudioContext here
|
|
74
114
|
const audioContext = new (window.AudioContext ||
|
|
75
115
|
(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,
|
|
116
|
+
sampleRate: decodingOptions?.targetSampleRate ?? 16000,
|
|
95
117
|
})
|
|
96
118
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
}
|
|
119
|
+
try {
|
|
120
|
+
const processedBuffer = await processAudioBuffer({
|
|
121
|
+
arrayBuffer,
|
|
122
|
+
fileUri,
|
|
123
|
+
targetSampleRate:
|
|
124
|
+
decodingOptions?.targetSampleRate ?? 16000,
|
|
125
|
+
targetChannels: decodingOptions?.targetChannels ?? 1,
|
|
126
|
+
normalizeAudio: decodingOptions?.normalizeAudio ?? false,
|
|
127
|
+
startTimeMs:
|
|
128
|
+
'startTimeMs' in props ? props.startTimeMs : undefined,
|
|
129
|
+
endTimeMs:
|
|
130
|
+
'endTimeMs' in props ? props.endTimeMs : undefined,
|
|
131
|
+
position: 'position' in props ? props.position : undefined,
|
|
132
|
+
length: 'length' in props ? props.length : undefined,
|
|
133
|
+
audioContext, // Pass the context we created
|
|
134
|
+
logger,
|
|
135
|
+
})
|
|
135
136
|
|
|
136
|
-
|
|
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
|
-
}
|
|
137
|
+
const channelData = processedBuffer.buffer.getChannelData(0)
|
|
153
138
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
139
|
+
// Create and initialize the worker
|
|
140
|
+
const blob = new Blob([InlineFeaturesExtractor], {
|
|
141
|
+
type: 'application/javascript',
|
|
142
|
+
})
|
|
143
|
+
const workerUrl = URL.createObjectURL(blob)
|
|
144
|
+
const worker = new Worker(workerUrl)
|
|
145
|
+
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
worker.onmessage = (event) => {
|
|
148
|
+
if (event.data.error) {
|
|
149
|
+
reject(new Error(event.data.error))
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result: AudioAnalysis = event.data.result
|
|
154
|
+
// Calculate CRC32 after worker completes if requested
|
|
155
|
+
if (features?.crc32) {
|
|
156
|
+
const samplesPerSegment = Math.floor(
|
|
157
|
+
(processedBuffer.sampleRate *
|
|
158
|
+
segmentDurationMs) /
|
|
159
|
+
1000
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
result.dataPoints = result.dataPoints.map(
|
|
163
|
+
(point: DataPoint, index: number) => {
|
|
164
|
+
const startSample =
|
|
165
|
+
index * samplesPerSegment
|
|
166
|
+
const segmentData = channelData.slice(
|
|
167
|
+
startSample,
|
|
168
|
+
startSample + samplesPerSegment
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...point,
|
|
173
|
+
features: {
|
|
174
|
+
...point.features,
|
|
175
|
+
crc32: calculateCRC32ForDataPoint(
|
|
176
|
+
segmentData
|
|
177
|
+
),
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
URL.revokeObjectURL(workerUrl)
|
|
185
|
+
worker.terminate()
|
|
186
|
+
resolve(result)
|
|
167
187
|
}
|
|
168
188
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
sample = sample * 127 // Convert to 8-bit range
|
|
189
|
+
worker.onerror = (error) => {
|
|
190
|
+
URL.revokeObjectURL(workerUrl)
|
|
191
|
+
worker.terminate()
|
|
192
|
+
reject(error)
|
|
174
193
|
}
|
|
175
194
|
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
worker.postMessage({
|
|
196
|
+
channelData,
|
|
197
|
+
sampleRate: processedBuffer.sampleRate,
|
|
198
|
+
segmentDurationMs,
|
|
199
|
+
bitDepth: decodingOptions?.targetBitDepth ?? 32,
|
|
200
|
+
numberOfChannels: processedBuffer.channels,
|
|
201
|
+
// enableLogging: !!logger,
|
|
202
|
+
features,
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
} finally {
|
|
206
|
+
await audioContext.close()
|
|
178
207
|
}
|
|
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
208
|
} catch (error) {
|
|
191
|
-
|
|
209
|
+
logger?.error('Failed to process audio:', error)
|
|
192
210
|
throw error
|
|
193
211
|
}
|
|
194
212
|
} else {
|
|
195
|
-
|
|
196
|
-
return await extractAudioAnalysis({
|
|
197
|
-
fileUri,
|
|
198
|
-
decodingOptions,
|
|
199
|
-
...restProps,
|
|
200
|
-
})
|
|
213
|
+
return await ExpoAudioStreamModule.extractAudioAnalysis(props)
|
|
201
214
|
}
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Analyzes WAV files without decoding, preserving original PCM values.
|
|
219
|
+
* Use this function when you need to ensure the analysis matches other software by avoiding any transformations.
|
|
220
|
+
*
|
|
221
|
+
* @param props - The options for WAV analysis, including file URI and range.
|
|
222
|
+
* @returns A promise that resolves to the audio analysis data.
|
|
223
|
+
*/
|
|
224
|
+
export const extractRawWavAnalysis = async ({
|
|
205
225
|
fileUri,
|
|
206
|
-
|
|
226
|
+
segmentDurationMs = 100, // Default to 100ms
|
|
207
227
|
arrayBuffer,
|
|
208
228
|
bitDepth,
|
|
209
|
-
skipWavHeader = true,
|
|
210
229
|
durationMs,
|
|
211
230
|
sampleRate,
|
|
212
231
|
numberOfChannels,
|
|
213
|
-
algorithm = 'rms',
|
|
214
232
|
features,
|
|
215
|
-
featuresExtratorUrl,
|
|
216
233
|
logger,
|
|
217
|
-
|
|
234
|
+
position = 0,
|
|
235
|
+
length,
|
|
236
|
+
}: ExtractWavAudioAnalysisProps): Promise<AudioAnalysis> => {
|
|
218
237
|
if (isWeb) {
|
|
219
238
|
if (!arrayBuffer && !fileUri) {
|
|
220
239
|
throw new Error('Either arrayBuffer or fileUri must be provided')
|
|
@@ -237,7 +256,7 @@ export const extractAudioAnalysis = async ({
|
|
|
237
256
|
// Create a new copy of the ArrayBuffer to avoid detachment issues
|
|
238
257
|
const bufferCopy = arrayBuffer.slice(0)
|
|
239
258
|
logger?.log(
|
|
240
|
-
`extractAudioAnalysis
|
|
259
|
+
`extractAudioAnalysis bitDepth=${bitDepth} len=${bufferCopy.byteLength}`,
|
|
241
260
|
bufferCopy.slice(0, 100)
|
|
242
261
|
)
|
|
243
262
|
|
|
@@ -258,25 +277,22 @@ export const extractAudioAnalysis = async ({
|
|
|
258
277
|
} = await convertPCMToFloat32({
|
|
259
278
|
buffer: arrayBuffer,
|
|
260
279
|
bitDepth: actualBitDepth,
|
|
261
|
-
skipWavHeader,
|
|
262
280
|
})
|
|
263
281
|
logger?.log(
|
|
264
|
-
`extractAudioAnalysis
|
|
282
|
+
`extractAudioAnalysis convertPCMToFloat32 length=${channelData.length} range: [ ${min} :: ${max} ]`
|
|
265
283
|
)
|
|
266
284
|
|
|
285
|
+
// Apply position and length constraints to channelData if specified
|
|
286
|
+
const startIndex = position
|
|
287
|
+
const endIndex = length ? startIndex + length : channelData.length
|
|
288
|
+
const constrainedChannelData = channelData.slice(startIndex, endIndex)
|
|
289
|
+
|
|
267
290
|
return new Promise((resolve, reject) => {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
} else {
|
|
274
|
-
const blob = new Blob([InlineFeaturesExtractor], {
|
|
275
|
-
type: 'application/javascript',
|
|
276
|
-
})
|
|
277
|
-
const url = URL.createObjectURL(blob)
|
|
278
|
-
worker = new Worker(url)
|
|
279
|
-
}
|
|
291
|
+
const blob = new Blob([InlineFeaturesExtractor], {
|
|
292
|
+
type: 'application/javascript',
|
|
293
|
+
})
|
|
294
|
+
const url = URL.createObjectURL(blob)
|
|
295
|
+
const worker = new Worker(url)
|
|
280
296
|
|
|
281
297
|
worker.onmessage = (event) => {
|
|
282
298
|
resolve(event.data.result)
|
|
@@ -288,10 +304,10 @@ export const extractAudioAnalysis = async ({
|
|
|
288
304
|
|
|
289
305
|
worker.postMessage({
|
|
290
306
|
command: 'process',
|
|
291
|
-
channelData,
|
|
307
|
+
channelData: constrainedChannelData,
|
|
292
308
|
sampleRate,
|
|
293
|
-
|
|
294
|
-
|
|
309
|
+
segmentDurationMs,
|
|
310
|
+
logger,
|
|
295
311
|
bitDepth,
|
|
296
312
|
fullAudioDurationMs: durationMs,
|
|
297
313
|
numberOfChannels,
|
|
@@ -303,64 +319,52 @@ export const extractAudioAnalysis = async ({
|
|
|
303
319
|
}
|
|
304
320
|
logger?.log(`extractAudioAnalysis`, {
|
|
305
321
|
fileUri,
|
|
306
|
-
|
|
307
|
-
algorithm,
|
|
322
|
+
segmentDurationMs,
|
|
308
323
|
})
|
|
309
324
|
const res = await ExpoAudioStreamModule.extractAudioAnalysis({
|
|
310
325
|
fileUri,
|
|
311
|
-
|
|
312
|
-
skipWavHeader,
|
|
313
|
-
algorithm,
|
|
326
|
+
segmentDurationMs,
|
|
314
327
|
features,
|
|
328
|
+
position,
|
|
329
|
+
length,
|
|
315
330
|
})
|
|
316
331
|
logger?.log(`extractAudioAnalysis`, res)
|
|
317
332
|
return res
|
|
318
333
|
}
|
|
319
334
|
}
|
|
320
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Generates a simplified preview of the audio waveform for quick visualization.
|
|
338
|
+
* Ideal for UI rendering with a specified number of points.
|
|
339
|
+
*
|
|
340
|
+
* @param options - The options for the preview, including file URI and time range.
|
|
341
|
+
* @returns A promise that resolves to the audio preview data.
|
|
342
|
+
*/
|
|
321
343
|
export async function extractPreview({
|
|
322
344
|
fileUri,
|
|
323
|
-
numberOfPoints,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
endTime,
|
|
345
|
+
numberOfPoints = 100,
|
|
346
|
+
startTimeMs = 0,
|
|
347
|
+
endTimeMs = 30000, // First 30 seconds
|
|
327
348
|
decodingOptions,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
}
|
|
349
|
+
logger,
|
|
350
|
+
}: PreviewOptions): Promise<AudioAnalysis> {
|
|
351
|
+
const durationMs = endTimeMs - startTimeMs
|
|
352
|
+
const segmentDurationMs = Math.floor(durationMs / numberOfPoints)
|
|
357
353
|
|
|
358
|
-
|
|
354
|
+
// Call extractAudioAnalysis with calculated parameters
|
|
355
|
+
const analysis = await extractAudioAnalysis({
|
|
359
356
|
fileUri,
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
357
|
+
startTimeMs,
|
|
358
|
+
endTimeMs,
|
|
359
|
+
logger,
|
|
360
|
+
segmentDurationMs,
|
|
364
361
|
decodingOptions,
|
|
365
362
|
})
|
|
363
|
+
|
|
364
|
+
// Transform the result into AudioPreview format
|
|
365
|
+
return analysis
|
|
366
366
|
}
|
|
367
|
+
|
|
368
|
+
export const extractAudioData = async (props: ExtractAudioDataOptions) => {
|
|
369
|
+
return await ExpoAudioStreamModule.extractAudioData(props)
|
|
370
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// packages/expo-audio-stream/src/ExpoAudioStream.types.ts
|
|
2
2
|
import {
|
|
3
|
-
AmplitudeAlgorithm,
|
|
4
3
|
AudioAnalysis,
|
|
5
4
|
AudioFeaturesOptions,
|
|
5
|
+
DecodingConfig,
|
|
6
6
|
} from './AudioAnalysis/AudioAnalysis.types'
|
|
7
7
|
import { AudioAnalysisEvent } from './events'
|
|
8
8
|
|
|
@@ -37,12 +37,14 @@ export interface AudioDataEvent {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export type EncodingType = 'pcm_32bit' | 'pcm_16bit' | 'pcm_8bit'
|
|
40
|
-
export type SampleRate = 16000 | 44100 | 48000
|
|
40
|
+
export type SampleRate = 16000 | 44100 | 48000
|
|
41
41
|
export type BitDepth = 8 | 16 | 32
|
|
42
|
+
export type PCMFormat = `pcm_${BitDepth}bit`
|
|
42
43
|
|
|
43
44
|
export type ConsoleLike = {
|
|
44
45
|
log: (message: string, ...args: unknown[]) => void
|
|
45
46
|
debug: (message: string, ...args: unknown[]) => void
|
|
47
|
+
info: (message: string, ...args: unknown[]) => void
|
|
46
48
|
warn: (message: string, ...args: unknown[]) => void
|
|
47
49
|
error: (message: string, ...args: unknown[]) => void
|
|
48
50
|
}
|
|
@@ -169,11 +171,8 @@ export interface RecordingConfig {
|
|
|
169
171
|
// iOS-specific configuration
|
|
170
172
|
ios?: IOSConfig
|
|
171
173
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Algorithm to use for amplitude computation (default is "rms")
|
|
176
|
-
algorithm?: AmplitudeAlgorithm
|
|
174
|
+
// Duration of each segment in milliseconds (default: 100)
|
|
175
|
+
segmentDurationMs?: number
|
|
177
176
|
|
|
178
177
|
// Feature options to extract (default is empty)
|
|
179
178
|
features?: AudioFeaturesOptions
|
|
@@ -268,6 +267,53 @@ export interface WaveformConfig {
|
|
|
268
267
|
height?: number // Height of the waveform view in dp (default: 64)
|
|
269
268
|
}
|
|
270
269
|
|
|
270
|
+
export interface ExtractAudioDataOptions {
|
|
271
|
+
fileUri: string
|
|
272
|
+
// Time-based range (mutually exclusive with byte-based range)
|
|
273
|
+
startTimeMs?: number
|
|
274
|
+
endTimeMs?: number
|
|
275
|
+
// Byte-based range (mutually exclusive with time-based range)
|
|
276
|
+
position?: number
|
|
277
|
+
length?: number
|
|
278
|
+
/** Include normalized audio data in [-1, 1] range */
|
|
279
|
+
includeNormalizedData?: boolean
|
|
280
|
+
/** Include base64 encoded string representation of the audio data */
|
|
281
|
+
includeBase64Data?: boolean
|
|
282
|
+
/** Include WAV header in the PCM data (makes it a valid WAV file) */
|
|
283
|
+
includeWavHeader?: boolean
|
|
284
|
+
/** Logger for debugging - can pass console directly. */
|
|
285
|
+
logger?: ConsoleLike
|
|
286
|
+
/** Compute the checksum of the pcm data */
|
|
287
|
+
computeChecksum?: boolean
|
|
288
|
+
/** Target config for the normalized audio (Android and Web) */
|
|
289
|
+
decodingOptions?: DecodingConfig
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface ExtractedAudioData {
|
|
293
|
+
/** Raw PCM audio data */
|
|
294
|
+
pcmData: Uint8Array
|
|
295
|
+
/** Normalized audio data in [-1, 1] range (when includeNormalizedData is true) */
|
|
296
|
+
normalizedData?: Float32Array
|
|
297
|
+
/** Base64 encoded string representation of the audio data (when includeBase64Data is true) */
|
|
298
|
+
base64Data?: string
|
|
299
|
+
/** Sample rate in Hz (e.g., 44100, 48000) */
|
|
300
|
+
sampleRate: number
|
|
301
|
+
/** Number of audio channels (1 for mono, 2 for stereo) */
|
|
302
|
+
channels: number
|
|
303
|
+
/** Bits per sample (8, 16, or 32) */
|
|
304
|
+
bitDepth: BitDepth
|
|
305
|
+
/** Duration of the audio in milliseconds */
|
|
306
|
+
durationMs: number
|
|
307
|
+
/** PCM format identifier (e.g., "pcm_16bit") */
|
|
308
|
+
format: PCMFormat
|
|
309
|
+
/** Total number of audio samples per channel */
|
|
310
|
+
samples: number
|
|
311
|
+
/** Whether the pcmData includes a WAV header */
|
|
312
|
+
hasWavHeader?: boolean
|
|
313
|
+
/** CRC32 Checksum of pcm data */
|
|
314
|
+
checksum?: number
|
|
315
|
+
}
|
|
316
|
+
|
|
271
317
|
export interface UseAudioRecorderState {
|
|
272
318
|
startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>
|
|
273
319
|
stopRecording: () => Promise<AudioRecording | null>
|