@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.
@@ -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 { extractAudioAnalysis } from './AudioAnalysis/extractAudioAnalysis'
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'
@@ -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
- const runInterval = () => {
524
- if (state.isRecording || state.isPaused) {
525
- checkStatus()
526
- intervalId = setTimeout(runInterval, 1000)
527
- }
528
- }
523
+ if (state.isRecording || state.isPaused) {
524
+ // Immediately check status when starting
525
+ checkStatus()
529
526
 
530
- runInterval()
527
+ // Start interval
528
+ intervalId = setInterval(checkStatus, 1000)
529
+ }
531
530
 
532
531
  return () => {
533
532
  if (intervalId) {
534
- clearTimeout(intervalId)
533
+ clearInterval(intervalId)
534
+ intervalId = undefined
535
535
  }
536
536
  }
537
537
  }, [checkStatus, state.isRecording, state.isPaused])