@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,410 @@
1
+ // src/useAudioRecording.ts
2
+ import { Platform, Subscription } from "expo-modules-core";
3
+ import { useCallback, useEffect, useReducer, useRef } from "react";
4
+
5
+ import { addAudioAnalysisListener, addAudioEventListener } from ".";
6
+ import {
7
+ AudioAnalysisData,
8
+ AudioDataEvent,
9
+ AudioEventPayload,
10
+ AudioFeaturesOptions,
11
+ AudioStreamResult,
12
+ AudioStreamStatus,
13
+ RecordingConfig,
14
+ StartAudioStreamResult,
15
+ } from "./ExpoAudioStream.types";
16
+ import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
17
+ import { WavFileInfo } from "./utils";
18
+
19
+ export interface ExtractMetadataProps {
20
+ fileUri?: string; // should provide either fileUri or arrayBuffer
21
+ wavMetadata?: WavFileInfo;
22
+ arrayBuffer?: ArrayBuffer;
23
+ bitDepth?: number;
24
+ skipWavHeader?: boolean;
25
+ durationMs?: number;
26
+ sampleRate?: number;
27
+ numberOfChannels?: number;
28
+ algorithm?: "peak" | "rms";
29
+ position?: number; // Optional number of bytes to skip. Default is 0
30
+ length?: number; // Optional number of bytes to read.
31
+ pointsPerSecond?: number; // Optional number of points per second. Use to reduce the number of points and compute the number of datapoints to return.
32
+ features?: AudioFeaturesOptions;
33
+ featuresExtratorUrl?: string;
34
+ }
35
+
36
+ export interface UseAudioRecorderProps {
37
+ debug?: boolean;
38
+ audioWorkletUrl?: string;
39
+ featuresExtratorUrl?: string;
40
+ }
41
+
42
+ export interface UseAudioRecorderState {
43
+ startRecording: (_: RecordingConfig) => Promise<StartAudioStreamResult>;
44
+ stopRecording: () => Promise<AudioStreamResult | null>;
45
+ pauseRecording: () => void;
46
+ resumeRecording: () => void;
47
+ isRecording: boolean;
48
+ isPaused: boolean;
49
+ durationMs: number; // Duration of the recording
50
+ size: number; // Size in bytes of the recorded audio
51
+ analysisData?: AudioAnalysisData;
52
+ audioWorkletUrl?: string;
53
+ featuresExtratorUrl?: string;
54
+ }
55
+
56
+ interface RecorderState {
57
+ isRecording: boolean;
58
+ isPaused: boolean;
59
+ durationMs: number;
60
+ size: number;
61
+ analysisData?: AudioAnalysisData;
62
+ }
63
+
64
+ type RecorderAction =
65
+ | { type: "START" | "STOP" | "PAUSE" | "RESUME" }
66
+ | { type: "UPDATE_STATUS"; payload: { durationMs: number; size: number } }
67
+ | { type: "UPDATE_ANALYSIS"; payload: AudioAnalysisData };
68
+
69
+ const defaultAnalysis: AudioAnalysisData = {
70
+ pointsPerSecond: 20,
71
+ bitDepth: 32,
72
+ numberOfChannels: 1,
73
+ durationMs: 0,
74
+ sampleRate: 44100,
75
+ samples: 0,
76
+ dataPoints: [],
77
+ amplitudeRange: {
78
+ min: Number.POSITIVE_INFINITY,
79
+ max: Number.NEGATIVE_INFINITY,
80
+ },
81
+ };
82
+
83
+ function recorderReducer(
84
+ state: RecorderState,
85
+ action: RecorderAction,
86
+ ): RecorderState {
87
+ switch (action.type) {
88
+ case "START":
89
+ return {
90
+ ...state,
91
+ isRecording: true,
92
+ isPaused: false,
93
+ durationMs: 0,
94
+ size: 0,
95
+ analysisData: defaultAnalysis, // Reset analysis data
96
+ };
97
+ case "STOP":
98
+ return { ...state, isRecording: false, isPaused: false };
99
+ case "PAUSE":
100
+ return { ...state, isPaused: true, isRecording: false };
101
+ case "RESUME":
102
+ return { ...state, isPaused: false, isRecording: true };
103
+ case "UPDATE_STATUS":
104
+ return {
105
+ ...state,
106
+ durationMs: action.payload.durationMs,
107
+ size: action.payload.size,
108
+ };
109
+ case "UPDATE_ANALYSIS":
110
+ return {
111
+ ...state,
112
+ analysisData: action.payload,
113
+ };
114
+ default:
115
+ return state;
116
+ }
117
+ }
118
+ const TAG = "[ useAudioRecorder ] ";
119
+
120
+ export function useAudioRecorder({
121
+ debug = false,
122
+ audioWorkletUrl,
123
+ featuresExtratorUrl,
124
+ }: UseAudioRecorderProps = {}): UseAudioRecorderState {
125
+ const [state, dispatch] = useReducer(recorderReducer, {
126
+ isRecording: false,
127
+ isPaused: false,
128
+ durationMs: 0,
129
+ size: 0,
130
+ analysisData: undefined,
131
+ });
132
+
133
+ const analysisListenerRef = useRef<Subscription | null>(null);
134
+ const analysisRef = useRef<AudioAnalysisData>({ ...defaultAnalysis });
135
+
136
+ // Instantiate the module for web with URLs
137
+ const ExpoAudioStream =
138
+ Platform.OS === "web"
139
+ ? ExpoAudioStreamModule({ audioWorkletUrl, featuresExtratorUrl })
140
+ : ExpoAudioStreamModule;
141
+
142
+ const onAudioStreamRef = useRef<
143
+ ((_: AudioDataEvent) => Promise<void>) | null
144
+ >(null);
145
+
146
+ const logDebug = useCallback(
147
+ (message: string, data?: any) => {
148
+ if (debug) {
149
+ if (data) {
150
+ console.log(`${TAG} ${message}`, data);
151
+ } else {
152
+ console.log(`${TAG} ${message}`);
153
+ }
154
+ }
155
+ },
156
+ [debug],
157
+ );
158
+
159
+ const handleAudioAnalysis = useCallback(
160
+ async (analysis: AudioAnalysisData, visualizationDuration: number) => {
161
+ const savedAnalysisData = analysisRef.current || { ...defaultAnalysis };
162
+
163
+ const maxDuration = visualizationDuration;
164
+
165
+ logDebug(
166
+ `[handleAudioAnalysis] Received audio analysis: maxDuration=${maxDuration} analysis.dataPoints=${analysis.dataPoints.length} analysisData.dataPoints=${savedAnalysisData.dataPoints.length}`,
167
+ analysis,
168
+ );
169
+
170
+ // Combine data points
171
+ const combinedDataPoints = [
172
+ ...savedAnalysisData.dataPoints,
173
+ ...analysis.dataPoints,
174
+ ];
175
+
176
+ // Calculate the new duration
177
+ const pointsPerSecond =
178
+ analysis.pointsPerSecond || savedAnalysisData.pointsPerSecond;
179
+ const maxDataPoints = (pointsPerSecond * visualizationDuration) / 1000;
180
+
181
+ logDebug(
182
+ `[handleAudioAnalysis] Combined data points before trimming: pointsPerSecond=${pointsPerSecond} visualizationDuration=${visualizationDuration} combinedDataPointsLength=${combinedDataPoints.length} vs maxDataPoints=${maxDataPoints}`,
183
+ );
184
+
185
+ // Trim data points to keep within the maximum number of data points
186
+ if (combinedDataPoints.length > maxDataPoints) {
187
+ combinedDataPoints.splice(0, combinedDataPoints.length - maxDataPoints);
188
+ }
189
+
190
+ savedAnalysisData.dataPoints = combinedDataPoints;
191
+ savedAnalysisData.bitDepth =
192
+ analysis.bitDepth || savedAnalysisData.bitDepth;
193
+ savedAnalysisData.durationMs =
194
+ combinedDataPoints.length * (1000 / pointsPerSecond);
195
+
196
+ // Update amplitude range
197
+ const newMin = Math.min(
198
+ savedAnalysisData.amplitudeRange.min,
199
+ analysis.amplitudeRange.min,
200
+ );
201
+ const newMax = Math.max(
202
+ savedAnalysisData.amplitudeRange.max,
203
+ analysis.amplitudeRange.max,
204
+ );
205
+
206
+ savedAnalysisData.amplitudeRange = {
207
+ min: newMin,
208
+ max: newMax,
209
+ };
210
+
211
+ logDebug(
212
+ `[handleAudioAnalysis] Updated analysis data: durationMs=${savedAnalysisData.durationMs}`,
213
+ savedAnalysisData,
214
+ );
215
+
216
+ // Update the ref
217
+ analysisRef.current = savedAnalysisData;
218
+
219
+ // Dispatch the updated analysis data to state to trigger re-render
220
+ // need to use spread operator otherwise it doesnt trigger update.
221
+ dispatch({ type: "UPDATE_ANALYSIS", payload: { ...savedAnalysisData } });
222
+ },
223
+ [logDebug],
224
+ );
225
+
226
+ const handleAudioEvent = useCallback(
227
+ async (eventData: AudioEventPayload) => {
228
+ const {
229
+ fileUri,
230
+ deltaSize,
231
+ totalSize,
232
+ lastEmittedSize,
233
+ position,
234
+ streamUuid,
235
+ encoded,
236
+ mimeType,
237
+ buffer,
238
+ } = eventData;
239
+ logDebug(`[handleAudioEvent] Received audio event:`, {
240
+ fileUri,
241
+ deltaSize,
242
+ totalSize,
243
+ position,
244
+ mimeType,
245
+ lastEmittedSize,
246
+ streamUuid,
247
+ encodedLength: encoded?.length,
248
+ });
249
+ if (deltaSize === 0) {
250
+ // Ignore packet with no data
251
+ return;
252
+ }
253
+ try {
254
+ // Coming from native ( ios / android ) otherwise buffer is set
255
+ if (Platform.OS !== "web") {
256
+ // Read the audio file as a base64 string for comparison
257
+ if (!encoded) {
258
+ console.error(`${TAG} Encoded audio data is missing`);
259
+ throw new Error("Encoded audio data is missing");
260
+ }
261
+ onAudioStreamRef.current?.({
262
+ data: encoded,
263
+ position,
264
+ fileUri,
265
+ eventDataSize: deltaSize,
266
+ totalSize,
267
+ });
268
+ } else if (buffer) {
269
+ // Coming from web
270
+ onAudioStreamRef.current?.({
271
+ data: buffer,
272
+ position,
273
+ fileUri,
274
+ eventDataSize: deltaSize,
275
+ totalSize,
276
+ });
277
+ }
278
+ } catch (error) {
279
+ console.error(`${TAG} Error processing audio event:`, error);
280
+ }
281
+ },
282
+ [logDebug],
283
+ );
284
+
285
+ const checkStatus = useCallback(async () => {
286
+ try {
287
+ if (!state.isRecording) {
288
+ logDebug(`${TAG} Not recording, exiting status check.`);
289
+ return;
290
+ }
291
+
292
+ const status: AudioStreamStatus = ExpoAudioStream.status();
293
+ if (debug) {
294
+ logDebug(`${TAG} Status:`, status);
295
+ }
296
+
297
+ dispatch({
298
+ type: "UPDATE_STATUS",
299
+ payload: { durationMs: status.durationMs, size: status.size },
300
+ });
301
+ } catch (error) {
302
+ console.error(`${TAG} Error getting status:`, error);
303
+ }
304
+ }, [state.isRecording, logDebug]);
305
+
306
+ useEffect(() => {
307
+ let interval: ReturnType<typeof setTimeout>;
308
+ if (state.isRecording) {
309
+ interval = setInterval(checkStatus, 1000);
310
+ }
311
+ return () => {
312
+ if (interval) {
313
+ clearInterval(interval);
314
+ }
315
+ };
316
+ }, [checkStatus, state.isRecording]);
317
+
318
+ useEffect(() => {
319
+ logDebug(`Registering audio event listener`);
320
+ const subscribeAudio = addAudioEventListener(handleAudioEvent);
321
+
322
+ logDebug(`Subscribed to audio event listener and analysis listener`, {
323
+ subscribeAudio,
324
+ });
325
+
326
+ return () => {
327
+ logDebug(`Removing audio event listener`);
328
+ subscribeAudio.remove();
329
+ };
330
+ }, [handleAudioEvent, handleAudioAnalysis, logDebug]);
331
+
332
+ const startRecording = useCallback(
333
+ async (recordingOptions: RecordingConfig) => {
334
+ if (debug) {
335
+ logDebug(`start recoding`, recordingOptions);
336
+ }
337
+
338
+ analysisRef.current = { ...defaultAnalysis }; // Reset analysis data
339
+
340
+ const { onAudioStream, ...options } = recordingOptions;
341
+ const { maxRecentDataDuration = 10000, enableProcessing } = options;
342
+ if (typeof onAudioStream === "function") {
343
+ onAudioStreamRef.current = onAudioStream;
344
+ } else {
345
+ console.warn(`${TAG} onAudioStream is not a function`, onAudioStream);
346
+ onAudioStreamRef.current = null;
347
+ }
348
+ const startResult: StartAudioStreamResult =
349
+ await ExpoAudioStream.startRecording(options);
350
+ dispatch({ type: "START" });
351
+
352
+ if (enableProcessing) {
353
+ logDebug(`Enabling audio analysis listener`);
354
+ const listener = addAudioAnalysisListener(async (analysisData) => {
355
+ try {
356
+ await handleAudioAnalysis(analysisData, maxRecentDataDuration);
357
+ } catch (error) {
358
+ console.warn(`${TAG} Error processing audio analysis:`, error);
359
+ }
360
+ });
361
+
362
+ analysisListenerRef.current = listener;
363
+ }
364
+
365
+ return startResult;
366
+ },
367
+ [logDebug],
368
+ );
369
+
370
+ const stopRecording = useCallback(async () => {
371
+ logDebug(`${TAG} stoping recording`);
372
+
373
+ if (analysisListenerRef.current) {
374
+ analysisListenerRef.current.remove();
375
+ analysisListenerRef.current = null;
376
+ }
377
+
378
+ const stopResult: AudioStreamResult = await ExpoAudioStream.stopRecording();
379
+ onAudioStreamRef.current = null;
380
+ logDebug(`${TAG} recording stopped`, stopResult);
381
+ dispatch({ type: "STOP" });
382
+ return stopResult;
383
+ }, [logDebug]);
384
+
385
+ const pauseRecording = useCallback(async () => {
386
+ logDebug(`${TAG} pause recording`);
387
+ const pauseResult = await ExpoAudioStream.pauseRecording();
388
+ dispatch({ type: "PAUSE" });
389
+ return pauseResult;
390
+ }, [logDebug]);
391
+
392
+ const resumeRecording = useCallback(async () => {
393
+ logDebug(`${TAG} resume recording`);
394
+ const resumeResult = await ExpoAudioStream.resumeRecording();
395
+ dispatch({ type: "RESUME" });
396
+ return resumeResult;
397
+ }, [logDebug]);
398
+
399
+ return {
400
+ startRecording,
401
+ stopRecording,
402
+ pauseRecording,
403
+ resumeRecording,
404
+ isPaused: state.isPaused,
405
+ isRecording: state.isRecording,
406
+ durationMs: state.durationMs,
407
+ size: state.size,
408
+ analysisData: state.analysisData,
409
+ };
410
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,189 @@
1
+ import { EncodingType } from "./ExpoAudioStream.types";
2
+
3
+ export const WAV_HEADER_SIZE = 44;
4
+ export const convertPCMToFloat32 = ({
5
+ bitDepth,
6
+ buffer,
7
+ skipWavHeader = false,
8
+ }: {
9
+ buffer: ArrayBuffer;
10
+ bitDepth: number;
11
+ skipWavHeader?: boolean;
12
+ }): { pcmValues: Float32Array; min: number; max: number } => {
13
+ const dataView = new DataView(buffer);
14
+ const headerOffset = skipWavHeader ? WAV_HEADER_SIZE : 0;
15
+ const dataLength = buffer.byteLength - headerOffset;
16
+ const sampleLength = dataLength / (bitDepth / 8);
17
+ const float32Array = new Float32Array(sampleLength);
18
+ let min = Infinity;
19
+ let max = -Infinity;
20
+
21
+ for (let i = 0; i < sampleLength; i++) {
22
+ let value = 0;
23
+ const offset = headerOffset + i * (bitDepth / 8);
24
+ switch (bitDepth) {
25
+ case 8:
26
+ value = dataView.getUint8(offset) / 128;
27
+ break;
28
+ case 16:
29
+ value = dataView.getInt16(offset, true) / 32768;
30
+ break;
31
+ case 24:
32
+ value =
33
+ (dataView.getUint8(offset) +
34
+ (dataView.getUint8(offset + 1) << 8) +
35
+ (dataView.getUint8(offset + 2) << 16)) /
36
+ 8388608;
37
+ break;
38
+ case 32:
39
+ value = dataView.getFloat32(offset, true);
40
+ break;
41
+ default:
42
+ throw new Error(`Unsupported bit depth: ${bitDepth}`);
43
+ }
44
+ if (value < min) min = value;
45
+ if (value > max) max = value;
46
+ float32Array[i] = value;
47
+ }
48
+
49
+ return { pcmValues: float32Array, min, max };
50
+ };
51
+
52
+ interface WavHeaderOptions {
53
+ buffer: ArrayBuffer;
54
+ sampleRate: number;
55
+ numChannels: number;
56
+ bitDepth: number;
57
+ }
58
+
59
+ export const writeWavHeader = ({
60
+ buffer,
61
+ sampleRate,
62
+ numChannels,
63
+ bitDepth,
64
+ }: WavHeaderOptions): ArrayBuffer => {
65
+ const bytesPerSample = bitDepth / 8;
66
+ const numSamples = buffer.byteLength / (numChannels * bytesPerSample);
67
+ const view = new DataView(buffer);
68
+ const blockAlign = numChannels * bytesPerSample;
69
+ const byteRate = sampleRate * blockAlign;
70
+
71
+ // Function to write a string to the DataView
72
+ const writeString = (view: DataView, offset: number, string: string) => {
73
+ for (let i = 0; i < string.length; i++) {
74
+ view.setUint8(offset + i, string.charCodeAt(i));
75
+ }
76
+ };
77
+
78
+ // Check if the buffer already has a WAV header by looking for "RIFF" at the start
79
+ const existingHeader = view.getUint32(0, false) === 0x52494646; // "RIFF" in ASCII
80
+
81
+ if (!existingHeader) {
82
+ // Write the WAV header
83
+ writeString(view, 0, "RIFF"); // ChunkID
84
+ view.setUint32(4, 36 + numSamples * blockAlign, true); // ChunkSize
85
+ writeString(view, 8, "WAVE"); // Format
86
+ writeString(view, 12, "fmt "); // Subchunk1ID
87
+ view.setUint32(16, 16, true); // Subchunk1Size (16 for PCM)
88
+ view.setUint16(20, bitDepth === 32 ? 3 : 1, true); // AudioFormat (3 for float, 1 for PCM)
89
+ view.setUint16(22, numChannels, true); // NumChannels
90
+ view.setUint32(24, sampleRate, true); // SampleRate
91
+ view.setUint32(28, byteRate, true); // ByteRate
92
+ view.setUint16(32, blockAlign, true); // BlockAlign
93
+ view.setUint16(34, bitDepth, true); // BitsPerSample
94
+ writeString(view, 36, "data"); // Subchunk2ID
95
+ view.setUint32(40, numSamples * blockAlign, true); // Subchunk2Size
96
+ } else {
97
+ // Update the existing WAV header if necessary
98
+ view.setUint32(4, 36 + numSamples * blockAlign, true); // Update ChunkSize
99
+ view.setUint32(24, sampleRate, true); // Update SampleRate
100
+ view.setUint32(28, byteRate, true); // Update ByteRate
101
+ view.setUint16(32, blockAlign, true); // Update BlockAlign
102
+ view.setUint32(40, numSamples * blockAlign, true); // Update Subchunk2Size
103
+ }
104
+
105
+ return buffer;
106
+ };
107
+
108
+ export interface WavFileInfo {
109
+ sampleRate: number;
110
+ numChannels: number;
111
+ bitDepth: number;
112
+ size: number; // in bytes
113
+ durationMs: number; // in seconds
114
+ }
115
+
116
+ export const getWavFileInfo = async (
117
+ arrayBuffer: ArrayBuffer,
118
+ ): Promise<WavFileInfo> => {
119
+ const view = new DataView(arrayBuffer);
120
+
121
+ // Check if the file is a valid RIFF/WAVE file
122
+ const riffHeader = view.getUint32(0, false); // "RIFF"
123
+ const waveHeader = view.getUint32(8, false); // "WAVE"
124
+ if (riffHeader !== 0x52494646 || waveHeader !== 0x57415645) {
125
+ throw new Error("Invalid WAV file");
126
+ }
127
+
128
+ // Locate the "fmt " chunk
129
+ let fmtChunkOffset = 12;
130
+ let sampleRate = 0;
131
+ let numChannels = 0;
132
+ let bitDepth = 0;
133
+ let dataChunkSize = 0;
134
+ let audioFormat = 0;
135
+
136
+ while (fmtChunkOffset < view.byteLength) {
137
+ const chunkId = view.getUint32(fmtChunkOffset, false);
138
+ const chunkSize = view.getUint32(fmtChunkOffset + 4, true);
139
+ if (chunkId === 0x666d7420) {
140
+ // "fmt "
141
+ audioFormat = view.getUint16(fmtChunkOffset + 8, true);
142
+ if (audioFormat !== 1 && audioFormat !== 3) {
143
+ throw new Error("Unsupported WAV file format");
144
+ }
145
+ numChannels = view.getUint16(fmtChunkOffset + 10, true);
146
+ sampleRate = view.getUint32(fmtChunkOffset + 12, true);
147
+ bitDepth = view.getUint16(fmtChunkOffset + 22, true);
148
+ } else if (chunkId === 0x64617461) {
149
+ // "data"
150
+ dataChunkSize = chunkSize;
151
+ break;
152
+ }
153
+ fmtChunkOffset += 8 + chunkSize;
154
+ }
155
+
156
+ if (!sampleRate || !numChannels || !bitDepth || !dataChunkSize) {
157
+ throw new Error("Incomplete WAV file information");
158
+ }
159
+
160
+ // Calculate duration
161
+ const bytesPerSample = bitDepth / 8;
162
+ const numSamples = dataChunkSize / (numChannels * bytesPerSample);
163
+ const durationMs = (numSamples / sampleRate) * 1000;
164
+
165
+ return {
166
+ sampleRate,
167
+ numChannels,
168
+ bitDepth,
169
+ size: arrayBuffer.byteLength,
170
+ durationMs,
171
+ };
172
+ };
173
+
174
+ export const encodingToBitDepth = ({
175
+ encoding,
176
+ }: {
177
+ encoding: EncodingType;
178
+ }): number => {
179
+ switch (encoding) {
180
+ case "pcm_32bit":
181
+ return 32;
182
+ case "pcm_16bit":
183
+ return 16;
184
+ case "pcm_8bit":
185
+ return 8;
186
+ default:
187
+ throw new Error(`Unsupported encoding type: ${encoding}`);
188
+ }
189
+ };
@@ -1,37 +0,0 @@
1
- import { EventEmitter } from "expo-modules-core";
2
- import { AudioStreamResult, RecordingConfig, StartAudioStreamResult } from "./ExpoAudioStream.types";
3
- declare class ExpoAudioStreamWeb extends EventEmitter {
4
- mediaRecorder: MediaRecorder | null;
5
- audioChunks: Blob[];
6
- isRecording: boolean;
7
- isPaused: boolean;
8
- recordingStartTime: number;
9
- pausedTime: number;
10
- currentDurationMs: number;
11
- currentSize: number;
12
- currentInterval: number;
13
- lastEmittedSize: number;
14
- lastEmittedTime: number;
15
- streamUuid: string | null;
16
- constructor();
17
- getMediaStream(): Promise<MediaStream>;
18
- startRecording(options?: RecordingConfig): Promise<StartAudioStreamResult>;
19
- setupRecordingListeners(): void;
20
- emitAudioEvent({ data, position }: {
21
- data: Blob;
22
- position: number;
23
- }): void;
24
- generateUUID(): string;
25
- stopRecording(): Promise<AudioStreamResult | null>;
26
- pauseRecording(): Promise<void>;
27
- status(): {
28
- isRecording: boolean;
29
- isPaused: boolean;
30
- duration: number;
31
- size: number;
32
- interval: number;
33
- };
34
- }
35
- declare const _default: ExpoAudioStreamWeb;
36
- export default _default;
37
- //# sourceMappingURL=ExpoAudioStreamModule.web.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ExpoAudioStreamModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAEL,iBAAiB,EACjB,eAAe,EACf,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAGjC,cAAM,kBAAmB,SAAQ,YAAY;IAC3C,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;;IA4BpB,cAAc;IAUd,cAAc,CAAC,OAAO,GAAE,eAAoB;IAwBlD,uBAAuB;IA4BvB,cAAc,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE;IAiBnE,YAAY;IAUN,aAAa,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAelD,cAAc;IAcpB,MAAM;;;;;;;CASP;;AAED,wBAAwC"}