@siteed/expo-audio-stream 1.0.0 → 1.0.2

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 (85) hide show
  1. package/README.md +7 -18
  2. package/android/build.gradle +5 -0
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +120 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +34 -4
  5. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +635 -0
  6. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +194 -79
  7. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  8. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +48 -2
  9. package/android/src/main/java/net/siteed/audiostream/FFT.kt +44 -0
  10. package/android/src/main/java/net/siteed/audiostream/Features.kt +56 -0
  11. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +12 -0
  12. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +56 -0
  13. package/app.plugin.js +1 -1
  14. package/build/AudioRecorder.provider.js +1 -1
  15. package/build/AudioRecorder.provider.js.map +1 -1
  16. package/build/ExpoAudioStream.native.d.ts +3 -0
  17. package/build/ExpoAudioStream.native.d.ts.map +1 -0
  18. package/build/ExpoAudioStream.native.js +6 -0
  19. package/build/ExpoAudioStream.native.js.map +1 -0
  20. package/build/ExpoAudioStream.types.d.ts +79 -6
  21. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  22. package/build/ExpoAudioStream.types.js.map +1 -1
  23. package/build/ExpoAudioStream.web.d.ts +41 -0
  24. package/build/ExpoAudioStream.web.d.ts.map +1 -0
  25. package/build/ExpoAudioStream.web.js +184 -0
  26. package/build/ExpoAudioStream.web.js.map +1 -0
  27. package/build/ExpoAudioStreamModule.d.ts +2 -2
  28. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  29. package/build/ExpoAudioStreamModule.js +12 -3
  30. package/build/ExpoAudioStreamModule.js.map +1 -1
  31. package/build/WebRecorder.d.ts +47 -0
  32. package/build/WebRecorder.d.ts.map +1 -0
  33. package/build/WebRecorder.js +243 -0
  34. package/build/WebRecorder.js.map +1 -0
  35. package/build/index.d.ts +14 -5
  36. package/build/index.d.ts.map +1 -1
  37. package/build/index.js +106 -7
  38. package/build/index.js.map +1 -1
  39. package/build/inlineAudioWebWorker.d.ts +3 -0
  40. package/build/inlineAudioWebWorker.d.ts.map +1 -0
  41. package/build/inlineAudioWebWorker.js +340 -0
  42. package/build/inlineAudioWebWorker.js.map +1 -0
  43. package/build/useAudioRecording.d.ts +24 -9
  44. package/build/useAudioRecording.d.ts.map +1 -1
  45. package/build/useAudioRecording.js +107 -29
  46. package/build/useAudioRecording.js.map +1 -1
  47. package/build/utils.d.ts +31 -0
  48. package/build/utils.d.ts.map +1 -0
  49. package/build/utils.js +143 -0
  50. package/build/utils.js.map +1 -0
  51. package/expo-module.config.json +13 -4
  52. package/ios/AudioAnalysisData.swift +39 -0
  53. package/ios/AudioProcessingHelpers.swift +59 -0
  54. package/ios/AudioProcessor.swift +317 -0
  55. package/ios/AudioStreamError.swift +7 -0
  56. package/ios/AudioStreamManager.swift +204 -52
  57. package/ios/AudioStreamManagerDelegate.swift +4 -0
  58. package/ios/DataPoint.swift +41 -0
  59. package/ios/ExpoAudioStreamModule.swift +188 -6
  60. package/ios/Features.swift +44 -0
  61. package/ios/RecordingResult.swift +19 -0
  62. package/ios/RecordingSettings.swift +13 -0
  63. package/ios/WaveformExtractor.swift +105 -0
  64. package/package.json +9 -9
  65. package/plugin/tsconfig.json +13 -8
  66. package/publish.sh +8 -0
  67. package/src/AudioRecorder.provider.tsx +1 -1
  68. package/src/ExpoAudioStream.native.ts +6 -0
  69. package/src/ExpoAudioStream.types.ts +97 -11
  70. package/src/ExpoAudioStream.web.ts +228 -0
  71. package/src/ExpoAudioStreamModule.ts +17 -3
  72. package/src/WebRecorder.ts +364 -0
  73. package/src/index.ts +166 -20
  74. package/src/inlineAudioWebWorker.tsx +340 -0
  75. package/src/useAudioRecording.tsx +410 -0
  76. package/src/utils.ts +189 -0
  77. package/build/ExpoAudioStreamModule.web.d.ts +0 -37
  78. package/build/ExpoAudioStreamModule.web.d.ts.map +0 -1
  79. package/build/ExpoAudioStreamModule.web.js +0 -156
  80. package/build/ExpoAudioStreamModule.web.js.map +0 -1
  81. package/docs/demo.gif +0 -0
  82. package/release-it.js +0 -18
  83. package/src/ExpoAudioStreamModule.web.ts +0 -181
  84. package/src/useAudioRecording.ts +0 -268
  85. package/yarn-error.log +0 -7793
@@ -0,0 +1,364 @@
1
+ // src/WebRecorder.ts
2
+ import { AudioAnalysisData, RecordingConfig } from "./ExpoAudioStream.types";
3
+ import {
4
+ EmitAudioAnalysisFunction,
5
+ EmitAudioEventFunction,
6
+ } from "./ExpoAudioStream.web";
7
+ import { encodingToBitDepth } from "./utils";
8
+ interface AudioWorkletEvent {
9
+ data: {
10
+ command: string;
11
+ recordedData?: ArrayBuffer;
12
+ sampleRate?: number;
13
+ };
14
+ }
15
+
16
+ interface AudioFeaturesEvent {
17
+ data: {
18
+ command: string;
19
+ result: AudioAnalysisData;
20
+ };
21
+ }
22
+
23
+ const DEFAULT_WEB_BITDEPTH = 32;
24
+ const DEFAULT_WEB_POINTS_PER_SECOND = 10;
25
+ const DEFAULT_WEB_INTERVAL = 500;
26
+ const DEFAULT_WEB_NUMBER_OF_CHANNELS = 1;
27
+
28
+ // const log = debug("expo-audio-stream:WebRecorder");
29
+ const log = console.log;
30
+ export class WebRecorder {
31
+ private audioContext: AudioContext;
32
+ private audioWorkletNode!: AudioWorkletNode;
33
+ private featureExtractorWorker: Worker;
34
+ private source: MediaStreamAudioSourceNode;
35
+ private audioWorkletUrl: string;
36
+ private emitAudioEventCallback: EmitAudioEventFunction;
37
+ private emitAudioAnalysisCallback: EmitAudioAnalysisFunction;
38
+ private config: RecordingConfig;
39
+ private position: number; // Track the cumulative position
40
+ private numberOfChannels: number; // Number of audio channels
41
+ private bitDepth: number; // Bit depth of the audio
42
+ private exportBitDepth: number; // Bit depth of the audio
43
+ private buffers: ArrayBuffer[]; // Array to store the buffers
44
+ private audioAnalysisData: AudioAnalysisData; // Keep updating the full audio analysis data with latest events
45
+
46
+ constructor({
47
+ audioContext,
48
+ source,
49
+ recordingConfig,
50
+ featuresExtratorUrl,
51
+ audioWorkletUrl,
52
+ emitAudioEventCallback,
53
+ emitAudioAnalysisCallback,
54
+ }: {
55
+ audioContext: AudioContext;
56
+ source: MediaStreamAudioSourceNode;
57
+ recordingConfig: RecordingConfig;
58
+ featuresExtratorUrl: string;
59
+ audioWorkletUrl: string;
60
+ emitAudioEventCallback: EmitAudioEventFunction;
61
+ emitAudioAnalysisCallback: EmitAudioAnalysisFunction;
62
+ }) {
63
+ this.audioContext = audioContext;
64
+ this.source = source;
65
+ this.audioWorkletUrl = audioWorkletUrl;
66
+ this.emitAudioEventCallback = emitAudioEventCallback;
67
+ this.emitAudioAnalysisCallback = emitAudioAnalysisCallback;
68
+ this.config = recordingConfig;
69
+ this.position = 0;
70
+ this.buffers = []; // Initialize the buffers array
71
+
72
+ const audioContextFormat = this.checkAudioContextFormat({
73
+ sampleRate: this.audioContext.sampleRate,
74
+ });
75
+ log("Initialized WebRecorder with config:", {
76
+ sampleRate: audioContextFormat.sampleRate,
77
+ bitDepth: audioContextFormat.bitDepth,
78
+ numberOfChannels: audioContextFormat.numberOfChannels,
79
+ });
80
+
81
+ this.bitDepth = audioContextFormat.bitDepth;
82
+ this.numberOfChannels =
83
+ audioContextFormat.numberOfChannels || DEFAULT_WEB_NUMBER_OF_CHANNELS; // Default to 1 if not available
84
+ this.exportBitDepth =
85
+ encodingToBitDepth({
86
+ encoding: recordingConfig.encoding ?? "pcm_32bit",
87
+ }) ||
88
+ audioContextFormat.bitDepth ||
89
+ DEFAULT_WEB_BITDEPTH;
90
+
91
+ this.audioAnalysisData = {
92
+ amplitudeRange: { min: 0, max: 0 },
93
+ dataPoints: [],
94
+ durationMs: 0,
95
+ samples: 0,
96
+ bitDepth: this.bitDepth,
97
+ numberOfChannels: this.numberOfChannels,
98
+ sampleRate: this.config.sampleRate || this.audioContext.sampleRate,
99
+ pointsPerSecond:
100
+ this.config.pointsPerSecond || DEFAULT_WEB_POINTS_PER_SECOND,
101
+ speakerChanges: [],
102
+ };
103
+
104
+ // Initialize the feature extractor worker
105
+ //TODO: create audio feature extractor from a Blob instead of url since we cannot include the url directly in the library
106
+ // We keep the url during dev and use the blob in production.
107
+ this.featureExtractorWorker = new Worker(
108
+ new URL(featuresExtratorUrl, window.location.href),
109
+ );
110
+ this.featureExtractorWorker.onmessage =
111
+ this.handleFeatureExtractorMessage.bind(this);
112
+ }
113
+
114
+ async init() {
115
+ try {
116
+ // TODO: Use the inline processor script for the audio worklet if the script is not available
117
+ // const blob = new Blob([InlineProcessorScrippt], {
118
+ // type: "application/javascript",
119
+ // });
120
+ // const url = URL.createObjectURL(blob);
121
+ // await this.audioContext.audioWorklet.addModule(url);
122
+ await this.audioContext.audioWorklet.addModule(this.audioWorkletUrl);
123
+
124
+ this.audioWorkletNode = new AudioWorkletNode(
125
+ this.audioContext,
126
+ "recorder-processor",
127
+ );
128
+
129
+ this.audioWorkletNode.port.onmessage = async (
130
+ event: AudioWorkletEvent,
131
+ ) => {
132
+ const command = event.data.command;
133
+ if (command !== "newData") {
134
+ return;
135
+ }
136
+ // Handle the audio blob (e.g., send it to the server or process it further)
137
+ log("Received audio blob from processor", event);
138
+ const pcmBuffer = event.data.recordedData;
139
+
140
+ if (!pcmBuffer) {
141
+ return;
142
+ }
143
+
144
+ this.buffers.push(pcmBuffer); // Store the buffer
145
+ const sampleRate =
146
+ event.data.sampleRate ?? this.audioContext.sampleRate;
147
+ const otherSampleRate = this.audioContext.sampleRate;
148
+
149
+ // Pass the intermediary buffer to the feature extractor worker
150
+ const pcmBufferCopy = pcmBuffer.slice(0);
151
+ const channelData = new Float32Array(pcmBufferCopy);
152
+
153
+ const duration = channelData.length / sampleRate; // Calculate duration of the current buffer
154
+ const otherDuration =
155
+ pcmBuffer.byteLength /
156
+ (otherSampleRate * (this.exportBitDepth / this.numberOfChannels)); // Calculate duration of the current buffer
157
+ log(
158
+ `sampleRate=${sampleRate} Duration: ${duration} -- otherSampleRate=${otherSampleRate} Other duration: ${otherDuration}`,
159
+ );
160
+
161
+ this.emitAudioEventCallback({
162
+ data: pcmBuffer,
163
+ position: this.position,
164
+ });
165
+ this.position += duration; // Update position
166
+
167
+ this.featureExtractorWorker.postMessage(
168
+ {
169
+ command: "process",
170
+ channelData,
171
+ sampleRate: this.audioContext.sampleRate,
172
+ pointsPerSecond:
173
+ this.config.pointsPerSecond || DEFAULT_WEB_POINTS_PER_SECOND,
174
+ algorithm: this.config.algorithm || "rms",
175
+ bitDepth: this.bitDepth,
176
+ fullAudioDurationMs: this.position * 1000,
177
+ numberOfChannels: this.numberOfChannels,
178
+ features: this.config.features,
179
+ },
180
+ [],
181
+ );
182
+ };
183
+
184
+ log(
185
+ `WebRecorder initialized -- recordSampleRate=${this.audioContext.sampleRate}`,
186
+ this.config,
187
+ );
188
+ this.audioWorkletNode.port.postMessage({
189
+ command: "init",
190
+ recordSampleRate: this.audioContext.sampleRate, // Pass the original sample rate
191
+ exportSampleRate:
192
+ this.config.sampleRate ?? this.audioContext.sampleRate,
193
+ bitDepth: this.bitDepth,
194
+ exportBitDepth: this.exportBitDepth,
195
+ channels: this.numberOfChannels,
196
+ interval: this.config.interval ?? DEFAULT_WEB_INTERVAL,
197
+ });
198
+
199
+ // Connect the source to the AudioWorkletNode and start recording
200
+ this.source.connect(this.audioWorkletNode);
201
+ this.audioWorkletNode.connect(this.audioContext.destination);
202
+ } catch (error) {
203
+ console.error("Failed to initialize WebRecorder", error);
204
+ }
205
+ }
206
+
207
+ handleFeatureExtractorMessage(event: AudioFeaturesEvent) {
208
+ if (event.data.command === "features") {
209
+ const segmentResult = event.data.result;
210
+
211
+ // Merge the segment result with the full audio analysis data
212
+ this.audioAnalysisData.dataPoints.push(...segmentResult.dataPoints);
213
+ this.audioAnalysisData.speakerChanges?.push(
214
+ ...(segmentResult.speakerChanges ?? []),
215
+ );
216
+ this.audioAnalysisData.durationMs = segmentResult.durationMs;
217
+ if (segmentResult.amplitudeRange) {
218
+ this.audioAnalysisData.amplitudeRange = {
219
+ min: Math.min(
220
+ this.audioAnalysisData.amplitudeRange.min,
221
+ segmentResult.amplitudeRange.min,
222
+ ),
223
+ max: Math.max(
224
+ this.audioAnalysisData.amplitudeRange.max,
225
+ segmentResult.amplitudeRange.max,
226
+ ),
227
+ };
228
+ }
229
+ // Handle the extracted features (e.g., emit an event or log them)
230
+ log("features event segmentResult", segmentResult);
231
+ log("features event audioAnalysisData", this.audioAnalysisData);
232
+ this.emitAudioAnalysisCallback(segmentResult);
233
+ }
234
+ }
235
+
236
+ start() {
237
+ this.source.connect(this.audioWorkletNode);
238
+ this.audioWorkletNode.connect(this.audioContext.destination);
239
+ }
240
+
241
+ stop() {
242
+ return new Promise((resolve, reject) => {
243
+ try {
244
+ if (this.audioWorkletNode) {
245
+ // this.source.disconnect(this.audioWorkletNode);
246
+ // this.audioWorkletNode.disconnect(this.audioContext.destination);
247
+ this.audioWorkletNode.port.postMessage({ command: "stop" });
248
+
249
+ // Set a timeout to reject the promise if no message is received within 5 seconds
250
+ const timeout = setTimeout(() => {
251
+ this.audioWorkletNode.port.removeEventListener(
252
+ "message",
253
+ onMessage,
254
+ );
255
+ reject(
256
+ new Error("Timeout error, audioWorkletNode didn't complete."),
257
+ );
258
+ }, 5000);
259
+
260
+ // Listen for the recordedData message to confirm stopping
261
+ const onMessage = async (event: AudioWorkletEvent) => {
262
+ const command = event.data.command;
263
+ if (command === "recordedData") {
264
+ clearTimeout(timeout); // Clear the timeout
265
+
266
+ const rawPCMDataFull = event.data.recordedData?.slice(
267
+ 0,
268
+ ) as ArrayBuffer;
269
+
270
+ // Compute duration of the recorded data
271
+ const duration =
272
+ rawPCMDataFull.byteLength /
273
+ (this.audioContext.sampleRate *
274
+ (this.exportBitDepth / this.numberOfChannels));
275
+ log(
276
+ `Received recorded data -- Duration: ${duration} vs ${rawPCMDataFull.byteLength / this.audioContext.sampleRate} seconds`,
277
+ );
278
+ log(
279
+ `recordedData.length=${rawPCMDataFull.byteLength} vs transmittedData.length=${this.buffers[0].byteLength}`,
280
+ );
281
+
282
+ // Remove the event listener after receiving the final data
283
+ this.audioWorkletNode.port.removeEventListener(
284
+ "message",
285
+ onMessage,
286
+ );
287
+ resolve(this.buffers); // Resolve the promise with the collected buffers
288
+ }
289
+ };
290
+ this.audioWorkletNode.port.addEventListener("message", onMessage);
291
+ }
292
+
293
+ // Stop all media stream tracks to stop the browser recording
294
+ this.stopMediaStreamTracks();
295
+ } catch (error) {
296
+ reject(error);
297
+ }
298
+ });
299
+ }
300
+
301
+ pause() {
302
+ this.source.disconnect(this.audioWorkletNode); // Disconnect the source from the AudioWorkletNode
303
+ this.audioWorkletNode.disconnect(this.audioContext.destination); // Disconnect the AudioWorkletNode from the destination
304
+ this.audioWorkletNode.port.postMessage({ command: "pause" });
305
+ }
306
+
307
+ stopMediaStreamTracks() {
308
+ // Stop all audio tracks to stop the recording icon
309
+ const tracks = this.source.mediaStream.getTracks();
310
+ tracks.forEach((track) => track.stop());
311
+ }
312
+
313
+ async playRecordedData({
314
+ recordedData,
315
+ }: {
316
+ recordedData: ArrayBuffer;
317
+ mimeType?: string;
318
+ }) {
319
+ try {
320
+ const blob = new Blob([recordedData]);
321
+ const url = URL.createObjectURL(blob);
322
+ const response = await fetch(url);
323
+ const arrayBuffer = await response.arrayBuffer();
324
+
325
+ // Decode the audio data
326
+ const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
327
+
328
+ // Create a buffer source node and play the audio
329
+ const bufferSource = this.audioContext.createBufferSource();
330
+ bufferSource.buffer = audioBuffer;
331
+ bufferSource.connect(this.audioContext.destination);
332
+ bufferSource.start();
333
+ log("Playing recorded data", recordedData);
334
+ } catch (error) {
335
+ console.error(`Failed to play recorded data:`, error);
336
+ }
337
+ }
338
+
339
+ private checkAudioContextFormat({ sampleRate }: { sampleRate: number }) {
340
+ // Create a silent AudioBuffer
341
+ const frameCount = sampleRate * 1.0; // 1 second buffer
342
+ const audioBuffer = this.audioContext.createBuffer(
343
+ 1,
344
+ frameCount,
345
+ sampleRate,
346
+ );
347
+
348
+ // Check the format
349
+ const channelData = audioBuffer.getChannelData(0);
350
+ const bitDepth = channelData.BYTES_PER_ELEMENT * 8; // 4 bytes per element means 32-bit
351
+
352
+ return {
353
+ sampleRate: audioBuffer.sampleRate,
354
+ bitDepth,
355
+ numberOfChannels: audioBuffer.numberOfChannels,
356
+ };
357
+ }
358
+
359
+ resume() {
360
+ this.source.connect(this.audioWorkletNode);
361
+ this.audioWorkletNode.connect(this.audioContext.destination);
362
+ this.audioWorkletNode.port.postMessage({ command: "resume" });
363
+ }
364
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,6 @@
1
- import {
2
- EventEmitter,
3
- NativeModulesProxy,
4
- type Subscription,
5
- } from "expo-modules-core";
1
+ // src/index.ts
2
+ import { EventEmitter, type Subscription } from "expo-modules-core";
3
+ import { Platform } from "react-native";
6
4
 
7
5
  // Import the native module. On web, it will be resolved to ExpoAudioStream.web.ts
8
6
  // and on native platforms to ExpoAudioStream.ts
@@ -10,21 +8,12 @@ import {
10
8
  AudioRecorderProvider,
11
9
  useSharedAudioRecorder,
12
10
  } from "./AudioRecorder.provider";
13
- import { AudioEventPayload } from "./ExpoAudioStream.types";
11
+ import { AudioAnalysisData, AudioEventPayload } from "./ExpoAudioStream.types";
14
12
  import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
15
- import {
16
- AudioDataEvent,
17
- UseAudioRecorderState,
18
- useAudioRecorder,
19
- } from "./useAudioRecording";
13
+ import { ExtractMetadataProps, useAudioRecorder } from "./useAudioRecording";
14
+ import { convertPCMToFloat32, getWavFileInfo, writeWavHeader } from "./utils";
20
15
 
21
- const emitter = new EventEmitter(
22
- ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream,
23
- );
24
-
25
- export function test(): void {
26
- return ExpoAudioStreamModule.test();
27
- }
16
+ const emitter = new EventEmitter(ExpoAudioStreamModule);
28
17
 
29
18
  export function addAudioEventListener(
30
19
  listener: (event: AudioEventPayload) => Promise<void>,
@@ -33,5 +22,162 @@ export function addAudioEventListener(
33
22
  return emitter.addListener<AudioEventPayload>("AudioData", listener);
34
23
  }
35
24
 
36
- export { AudioRecorderProvider, useAudioRecorder, useSharedAudioRecorder };
37
- export type { AudioDataEvent, AudioEventPayload, UseAudioRecorderState };
25
+ export function addAudioAnalysisListener(
26
+ listener: (event: AudioAnalysisData) => Promise<void>,
27
+ ): Subscription {
28
+ console.log(`addAudioAnalysisListener`, listener);
29
+ return emitter.addListener<AudioAnalysisData>("AudioAnalysis", listener);
30
+ }
31
+
32
+ const isWeb = Platform.OS === "web";
33
+
34
+ export const extractAudioAnalysis = async ({
35
+ fileUri,
36
+ pointsPerSecond = 20,
37
+ arrayBuffer,
38
+ bitDepth,
39
+ skipWavHeader,
40
+ durationMs,
41
+ sampleRate,
42
+ numberOfChannels,
43
+ algorithm = "rms",
44
+ features,
45
+ featuresExtratorUrl = "/audio-features-extractor.js",
46
+ }: ExtractMetadataProps): Promise<AudioAnalysisData> => {
47
+ if (isWeb) {
48
+ if (!arrayBuffer && !fileUri) {
49
+ throw new Error("Either arrayBuffer or fileUri must be provided");
50
+ }
51
+
52
+ if (!arrayBuffer) {
53
+ console.log(`fetching fileUri`, fileUri);
54
+ const response = await fetch(fileUri!);
55
+
56
+ if (!response.ok) {
57
+ throw new Error(`Failed to fetch fileUri: ${response.statusText}`);
58
+ }
59
+
60
+ arrayBuffer = (await response.arrayBuffer()).slice(0);
61
+ console.log(`fetched fileUri`, arrayBuffer.byteLength, arrayBuffer);
62
+ }
63
+
64
+ // Create a new copy of the ArrayBuffer to avoid detachment issues
65
+ const bufferCopy = arrayBuffer.slice(0);
66
+ console.log(
67
+ `extractAudioAnalysis skipWavHeader=${skipWavHeader} bitDepth=${bitDepth} len=${bufferCopy.byteLength}`,
68
+ bufferCopy.slice(0, 100),
69
+ );
70
+
71
+ let actualBitDepth = bitDepth;
72
+ if (!actualBitDepth) {
73
+ console.log(
74
+ `extractAudioAnalysis bitDepth not provided -- getting wav file info`,
75
+ );
76
+ const fileInfo = await getWavFileInfo(bufferCopy);
77
+ actualBitDepth = fileInfo.bitDepth;
78
+ }
79
+ console.log(`extractAudioAnalysis actualBitDepth=${actualBitDepth}`);
80
+ // let copyChannelData: Float32Array;
81
+ // try {
82
+ // const audioContext = new (window.AudioContext ||
83
+ // // @ts-ignore
84
+ // window.webkitAudioContext)();
85
+ // const audioBuffer = await audioContext.decodeAudioData(bufferCopy);
86
+ // const channelData = audioBuffer.getChannelData(0); // Use only the first channel
87
+ // copyChannelData = new Float32Array(channelData); // Create a new Float32Array
88
+ // } catch (error) {
89
+ // console.warn("Failed to decode audio data:", error);
90
+ // // Fall back to creating a new Float32Array from the ArrayBuffer if decoding fails
91
+ // copyChannelData = new Float32Array(arrayBuffer);
92
+ // }
93
+
94
+ const {
95
+ pcmValues: channelData,
96
+ min,
97
+ max,
98
+ } = convertPCMToFloat32({
99
+ buffer: arrayBuffer,
100
+ bitDepth: actualBitDepth,
101
+ skipWavHeader,
102
+ });
103
+ console.log(
104
+ `extractAudioAnalysis skipWaveHeader=${skipWavHeader} convertPCMToFloat32 length=${channelData.length} range: [ ${min} :: ${max} ]`,
105
+ );
106
+
107
+ return new Promise((resolve, reject) => {
108
+ const worker = new Worker(
109
+ new URL(featuresExtratorUrl, window.location.href),
110
+ );
111
+
112
+ worker.onmessage = (event) => {
113
+ resolve(event.data.result);
114
+ };
115
+
116
+ worker.onerror = (error) => {
117
+ reject(error);
118
+ };
119
+
120
+ worker.postMessage({
121
+ command: "process",
122
+ channelData,
123
+ sampleRate,
124
+ pointsPerSecond,
125
+ algorithm,
126
+ bitDepth,
127
+ fullAudioDurationMs: durationMs,
128
+ numberOfChannels,
129
+ });
130
+ });
131
+ } else {
132
+ if (!fileUri) {
133
+ throw new Error("fileUri is required");
134
+ }
135
+ console.log(`extractAudioAnalysis`, {
136
+ fileUri,
137
+ pointsPerSecond,
138
+ algorithm,
139
+ });
140
+ const res = await ExpoAudioStreamModule.extractAudioAnalysis({
141
+ fileUri,
142
+ pointsPerSecond,
143
+ skipWavHeader,
144
+ algorithm,
145
+ features,
146
+ });
147
+ console.log(`extractAudioAnalysis`, res);
148
+ return res;
149
+ }
150
+ };
151
+
152
+ export interface ExtractWaveformProps {
153
+ fileUri: string;
154
+ numberOfSamples: number;
155
+ offset?: number;
156
+ length?: number;
157
+ }
158
+ export const extractWaveform = async ({
159
+ fileUri,
160
+ numberOfSamples,
161
+ offset = 0,
162
+ length,
163
+ }: ExtractWaveformProps): Promise<unknown> => {
164
+ const res = await ExpoAudioStreamModule.extractAudioAnalysis({
165
+ fileUri,
166
+ numberOfSamples,
167
+ offset,
168
+ length,
169
+ });
170
+ console.log(`extractWaveform`, res);
171
+ return res;
172
+ };
173
+
174
+ export {
175
+ AudioRecorderProvider,
176
+ convertPCMToFloat32,
177
+ getWavFileInfo,
178
+ useAudioRecorder,
179
+ useSharedAudioRecorder,
180
+ writeWavHeader as writeWaveHeader,
181
+ };
182
+
183
+ export * from "./ExpoAudioStream.types";