@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +1 -1
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +68 -22
  4. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +24 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +836 -386
  6. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -2
  7. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +35 -29
  8. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +236 -96
  9. package/android/src/main/java/net/siteed/audiostream/FFT.kt +55 -0
  10. package/android/src/main/java/net/siteed/audiostream/Features.kt +49 -7
  11. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +2 -4
  12. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +55 -47
  13. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  14. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  15. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +60 -13
  16. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
  17. package/build/AudioAnalysis/extractAudioAnalysis.js +147 -162
  18. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  19. package/build/ExpoAudioStream.types.d.ts +47 -3
  20. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  21. package/build/ExpoAudioStream.types.js.map +1 -1
  22. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  23. package/build/ExpoAudioStream.web.js +0 -1
  24. package/build/ExpoAudioStream.web.js.map +1 -1
  25. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  26. package/build/ExpoAudioStreamModule.js +216 -12
  27. package/build/ExpoAudioStreamModule.js.map +1 -1
  28. package/build/WebRecorder.web.d.ts +67 -13
  29. package/build/WebRecorder.web.d.ts.map +1 -1
  30. package/build/WebRecorder.web.js +177 -173
  31. package/build/WebRecorder.web.js.map +1 -1
  32. package/build/index.d.ts +3 -3
  33. package/build/index.d.ts.map +1 -1
  34. package/build/index.js +2 -2
  35. package/build/index.js.map +1 -1
  36. package/build/useAudioRecorder.d.ts.map +1 -1
  37. package/build/useAudioRecorder.js +12 -8
  38. package/build/useAudioRecorder.js.map +1 -1
  39. package/build/utils/audioProcessing.d.ts +24 -0
  40. package/build/utils/audioProcessing.d.ts.map +1 -0
  41. package/build/utils/audioProcessing.js +133 -0
  42. package/build/utils/audioProcessing.js.map +1 -0
  43. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  44. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  45. package/build/workers/InlineFeaturesExtractor.web.js +694 -194
  46. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  47. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  48. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  49. package/build/workers/inlineAudioWebWorker.web.js +3 -2
  50. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  51. package/ios/AudioAnalysisData.swift +51 -16
  52. package/ios/AudioProcessingHelpers.swift +710 -26
  53. package/ios/AudioProcessor.swift +334 -185
  54. package/ios/AudioStreamManager.swift +2 -3
  55. package/ios/DataPoint.swift +25 -12
  56. package/ios/DecodingConfig.swift +47 -0
  57. package/ios/ExpoAudioStreamModule.swift +187 -103
  58. package/ios/FFT.swift +62 -0
  59. package/ios/Features.swift +24 -3
  60. package/ios/RecordingSettings.swift +7 -7
  61. package/package.json +2 -1
  62. package/plugin/build/index.js +6 -1
  63. package/plugin/src/index.ts +9 -1
  64. package/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
  65. package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
  66. package/src/ExpoAudioStream.types.ts +53 -7
  67. package/src/ExpoAudioStream.web.ts +0 -1
  68. package/src/ExpoAudioStreamModule.ts +255 -10
  69. package/src/WebRecorder.web.ts +231 -244
  70. package/src/index.ts +5 -3
  71. package/src/useAudioRecorder.tsx +14 -10
  72. package/src/utils/audioProcessing.ts +205 -0
  73. package/src/workers/InlineFeaturesExtractor.web.tsx +694 -194
  74. package/src/workers/inlineAudioWebWorker.web.tsx +3 -2
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  // src/index.ts
2
2
 
3
3
  import {
4
+ extractRawWavAnalysis,
4
5
  extractAudioAnalysis,
5
- extractAudioFromAnyFormat,
6
6
  extractPreview,
7
+ extractAudioData,
7
8
  } from './AudioAnalysis/extractAudioAnalysis'
8
9
  import {
9
10
  AudioRecorderProvider,
@@ -19,9 +20,10 @@ export * from './utils/writeWavHeader'
19
20
  export {
20
21
  AudioRecorderProvider,
21
22
  ExpoAudioStreamModule,
23
+ extractRawWavAnalysis as extractWavAudioAnalysis,
22
24
  extractAudioAnalysis,
23
- extractAudioFromAnyFormat,
24
25
  extractPreview,
26
+ extractAudioData,
25
27
  useAudioRecorder,
26
28
  useSharedAudioRecorder,
27
29
  }
@@ -30,6 +32,6 @@ export type * from './AudioAnalysis/AudioAnalysis.types'
30
32
 
31
33
  export type * from './ExpoAudioStream.types'
32
34
  export type {
35
+ ExtractWavAudioAnalysisProps,
33
36
  ExtractAudioAnalysisProps,
34
- ExtractAudioFromAnyFormatProps,
35
37
  } from './AudioAnalysis/extractAudioAnalysis'
@@ -68,15 +68,17 @@ type RecorderAction =
68
68
  | { type: 'UPDATE_ANALYSIS'; payload: AudioAnalysis }
69
69
 
70
70
  const defaultAnalysis: AudioAnalysis = {
71
- pointsPerSecond: 10,
71
+ segmentDurationMs: 100,
72
72
  bitDepth: 32,
73
73
  numberOfChannels: 1,
74
74
  durationMs: 0,
75
75
  sampleRate: 44100,
76
76
  samples: 0,
77
77
  dataPoints: [],
78
- amplitudeAlgorithm: 'rms',
79
- speakerChanges: [],
78
+ rmsRange: {
79
+ min: Number.POSITIVE_INFINITY,
80
+ max: Number.NEGATIVE_INFINITY,
81
+ },
80
82
  amplitudeRange: {
81
83
  min: Number.POSITIVE_INFINITY,
82
84
  max: Number.NEGATIVE_INFINITY,
@@ -225,13 +227,15 @@ export function useAudioRecorder({
225
227
  ]
226
228
 
227
229
  // Calculate the new duration
228
- const pointsPerSecond =
229
- analysis.pointsPerSecond || savedAnalysisData.pointsPerSecond
230
- const maxDataPoints =
231
- (pointsPerSecond * visualizationDuration) / 1000
230
+ // The number of segments is based on how many segments of segmentDurationMs can fit in visualizationDuration
231
+ const numberOfSegments = Math.ceil(
232
+ visualizationDuration / analysis.segmentDurationMs
233
+ )
234
+ // maxDataPoints should be the number of data points, not milliseconds
235
+ const maxDataPoints = numberOfSegments
232
236
 
233
237
  logger?.debug(
234
- `[handleAudioAnalysis] Combined data points before trimming: pointsPerSecond=${pointsPerSecond} visualizationDuration=${visualizationDuration} combinedDataPointsLength=${combinedDataPoints.length} vs maxDataPoints=${maxDataPoints}`
238
+ `[handleAudioAnalysis] Combined data points before trimming: numberOfSegments=${numberOfSegments} visualizationDuration=${visualizationDuration} combinedDataPointsLength=${combinedDataPoints.length} vs maxDataPoints=${maxDataPoints}`
235
239
  )
236
240
 
237
241
  // Trim data points to keep within the maximum number of data points
@@ -248,12 +252,12 @@ export function useAudioRecorder({
248
252
  dataPoints: fullCombinedDataPoints,
249
253
  }
250
254
  fullAnalysisRef.current.durationMs =
251
- fullCombinedDataPoints.length * (1000 / pointsPerSecond)
255
+ fullCombinedDataPoints.length * analysis.segmentDurationMs
252
256
  savedAnalysisData.dataPoints = combinedDataPoints
253
257
  savedAnalysisData.bitDepth =
254
258
  analysis.bitDepth || savedAnalysisData.bitDepth
255
259
  savedAnalysisData.durationMs =
256
- combinedDataPoints.length * (1000 / pointsPerSecond)
260
+ combinedDataPoints.length * analysis.segmentDurationMs
257
261
 
258
262
  // Update amplitude range
259
263
  const newMin = Math.min(
@@ -0,0 +1,205 @@
1
+ // packages/expo-audio-stream/src/utils/audioProcessing.ts
2
+ import { Platform } from 'react-native'
3
+
4
+ import { ConsoleLike } from '../ExpoAudioStream.types'
5
+
6
+ export interface ProcessAudioBufferOptions {
7
+ arrayBuffer?: ArrayBuffer
8
+ fileUri?: string
9
+ targetSampleRate: number
10
+ targetChannels: number
11
+ normalizeAudio: boolean
12
+ startTimeMs?: number
13
+ endTimeMs?: number
14
+ position?: number
15
+ length?: number
16
+ audioContext?: AudioContext
17
+ logger?: ConsoleLike
18
+ }
19
+
20
+ export interface ProcessedAudioData {
21
+ channelData: Float32Array
22
+ samples: number
23
+ durationMs: number
24
+ sampleRate: number
25
+ channels: number
26
+ buffer: AudioBuffer
27
+ }
28
+
29
+ export async function processAudioBuffer({
30
+ arrayBuffer,
31
+ fileUri,
32
+ targetSampleRate,
33
+ targetChannels,
34
+ normalizeAudio,
35
+ startTimeMs,
36
+ endTimeMs,
37
+ position,
38
+ length,
39
+ audioContext,
40
+ logger,
41
+ }: ProcessAudioBufferOptions): Promise<ProcessedAudioData> {
42
+ if (Platform.OS !== 'web') {
43
+ throw new Error('processAudioBuffer is only supported on web')
44
+ }
45
+
46
+ let ctx: AudioContext | undefined
47
+ let buffer: AudioBuffer | undefined
48
+
49
+ try {
50
+ // Log initial parameters
51
+ logger?.debug('Process audio buffer - Initial params:', {
52
+ hasArrayBuffer: !!arrayBuffer,
53
+ fileUri,
54
+ targetSampleRate,
55
+ targetChannels,
56
+ normalizeAudio,
57
+ startTimeMs,
58
+ endTimeMs,
59
+ position,
60
+ length,
61
+ })
62
+
63
+ // Get the audio data
64
+ let audioData: ArrayBuffer
65
+ if (arrayBuffer) {
66
+ audioData = arrayBuffer
67
+ } else if (fileUri) {
68
+ const response = await fetch(fileUri)
69
+ if (!response.ok) {
70
+ throw new Error(
71
+ `Failed to fetch fileUri: ${response.statusText}`
72
+ )
73
+ }
74
+ audioData = await response.arrayBuffer()
75
+ } else {
76
+ throw new Error('Either arrayBuffer or fileUri must be provided')
77
+ }
78
+
79
+ logger?.debug('Audio data loaded:', {
80
+ byteLength: audioData.byteLength,
81
+ firstBytes: Array.from(new Uint8Array(audioData.slice(0, 16))),
82
+ })
83
+
84
+ // Create context at original sample rate first
85
+ ctx =
86
+ audioContext ||
87
+ new (window.AudioContext || (window as any).webkitAudioContext)()
88
+ buffer = await ctx.decodeAudioData(audioData)
89
+
90
+ logger?.debug('Decoded audio buffer:', {
91
+ originalChannels: buffer.numberOfChannels,
92
+ originalSampleRate: buffer.sampleRate,
93
+ originalDuration: buffer.duration,
94
+ originalLength: buffer.length,
95
+ })
96
+
97
+ // Calculate time range
98
+ const startSample =
99
+ startTimeMs !== undefined
100
+ ? Math.floor((startTimeMs / 1000) * buffer.sampleRate)
101
+ : position !== undefined
102
+ ? Math.floor(position / 2)
103
+ : 0
104
+
105
+ // Fix: Adjust position calculation based on original sample rate
106
+ // When position is provided in bytes, we need to account for the original sample rate
107
+ const bytesPerSample = 2 // 16-bit audio = 2 bytes per sample
108
+ const adjustedStartSample =
109
+ position !== undefined
110
+ ? Math.floor(
111
+ (position / bytesPerSample) *
112
+ (buffer.sampleRate / targetSampleRate)
113
+ )
114
+ : startSample
115
+
116
+ const samplesNeeded =
117
+ length !== undefined
118
+ ? Math.floor(
119
+ (length / bytesPerSample) *
120
+ (buffer.sampleRate / targetSampleRate)
121
+ )
122
+ : endTimeMs !== undefined && startTimeMs !== undefined
123
+ ? Math.floor(
124
+ ((endTimeMs - startTimeMs) / 1000) * buffer.sampleRate
125
+ )
126
+ : buffer.length - adjustedStartSample
127
+
128
+ logger?.debug('Sample calculations (adjusted):', {
129
+ originalStartSample: startSample,
130
+ adjustedStartSample,
131
+ samplesNeeded,
132
+ originalSampleRate: buffer.sampleRate,
133
+ targetSampleRate,
134
+ conversionRatio: buffer.sampleRate / targetSampleRate,
135
+ expectedDurationMs: (samplesNeeded / buffer.sampleRate) * 1000,
136
+ })
137
+
138
+ // Create temporary buffer for the segment
139
+ const segmentBuffer = ctx.createBuffer(
140
+ buffer.numberOfChannels,
141
+ samplesNeeded,
142
+ buffer.sampleRate
143
+ )
144
+
145
+ // Copy the segment
146
+ for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
147
+ const channelData = buffer.getChannelData(channel)
148
+ const segmentData = segmentBuffer.getChannelData(channel)
149
+ for (let i = 0; i < samplesNeeded; i++) {
150
+ segmentData[i] = channelData[adjustedStartSample + i]
151
+ }
152
+ }
153
+
154
+ // Create offline context for resampling
155
+ const offlineCtx = new OfflineAudioContext(
156
+ targetChannels,
157
+ Math.ceil((samplesNeeded * targetSampleRate) / buffer.sampleRate),
158
+ targetSampleRate
159
+ )
160
+
161
+ // Create source and connect
162
+ const source = offlineCtx.createBufferSource()
163
+ source.buffer = segmentBuffer
164
+ source.connect(offlineCtx.destination)
165
+
166
+ // Render at new sample rate
167
+ source.start()
168
+ const processedBuffer = await offlineCtx.startRendering()
169
+
170
+ // Get the final audio data
171
+ const channelData = processedBuffer.getChannelData(0)
172
+ const durationMs = Math.round(
173
+ (samplesNeeded / buffer.sampleRate) * 1000
174
+ )
175
+
176
+ logger?.debug('Final processed audio:', {
177
+ outputSamples: channelData.length,
178
+ outputSampleRate: targetSampleRate,
179
+ durationMs,
180
+ })
181
+
182
+ return {
183
+ buffer: processedBuffer,
184
+ channelData,
185
+ samples: channelData.length,
186
+ durationMs,
187
+ sampleRate: targetSampleRate,
188
+ channels: processedBuffer.numberOfChannels,
189
+ }
190
+ } catch (error) {
191
+ logger?.error('Failed to process audio buffer:', {
192
+ error,
193
+ position,
194
+ length,
195
+ startTimeMs,
196
+ endTimeMs,
197
+ bufferLength: buffer?.length,
198
+ })
199
+ throw error
200
+ } finally {
201
+ if (!audioContext && ctx) {
202
+ await ctx.close()
203
+ }
204
+ }
205
+ }