@siteed/expo-audio-stream 1.0.2 → 1.0.3

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 (113) hide show
  1. package/.size-limit.json +6 -0
  2. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +76 -0
  3. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
  4. package/build/AudioAnalysis/AudioAnalysis.types.js +3 -0
  5. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
  6. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +4 -0
  7. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
  8. package/build/AudioAnalysis/extractAudioAnalysis.js +101 -0
  9. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
  10. package/build/AudioAnalysis/extractWaveform.d.ts +8 -0
  11. package/build/AudioAnalysis/extractWaveform.d.ts.map +1 -0
  12. package/build/AudioAnalysis/extractWaveform.js +14 -0
  13. package/build/AudioAnalysis/extractWaveform.js.map +1 -0
  14. package/build/AudioRecorder.provider.d.ts +14 -1
  15. package/build/AudioRecorder.provider.d.ts.map +1 -1
  16. package/build/AudioRecorder.provider.js +17 -4
  17. package/build/AudioRecorder.provider.js.map +1 -1
  18. package/build/ExpoAudioStream.types.d.ts +26 -84
  19. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  20. package/build/ExpoAudioStream.types.js.map +1 -1
  21. package/build/ExpoAudioStream.web.d.ts +6 -5
  22. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  23. package/build/ExpoAudioStream.web.js +9 -8
  24. package/build/ExpoAudioStream.web.js.map +1 -1
  25. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  26. package/build/ExpoAudioStreamModule.js +5 -1
  27. package/build/ExpoAudioStreamModule.js.map +1 -1
  28. package/build/{WebRecorder.d.ts → WebRecorder.web.d.ts} +7 -3
  29. package/build/WebRecorder.web.d.ts.map +1 -0
  30. package/build/{WebRecorder.js → WebRecorder.web.js} +74 -29
  31. package/build/WebRecorder.web.js.map +1 -0
  32. package/build/constants.d.ts +11 -0
  33. package/build/constants.d.ts.map +1 -0
  34. package/build/constants.js +14 -0
  35. package/build/constants.js.map +1 -0
  36. package/build/events.d.ts +6 -0
  37. package/build/events.d.ts.map +1 -0
  38. package/build/events.js +15 -0
  39. package/build/events.js.map +1 -0
  40. package/build/index.d.ts +8 -16
  41. package/build/index.d.ts.map +1 -1
  42. package/build/index.js +6 -112
  43. package/build/index.js.map +1 -1
  44. package/build/logger.d.ts +9 -0
  45. package/build/logger.d.ts.map +1 -0
  46. package/build/logger.js +17 -0
  47. package/build/logger.js.map +1 -0
  48. package/build/{useAudioRecording.d.ts → useAudioRecorder.d.ts} +6 -7
  49. package/build/useAudioRecorder.d.ts.map +1 -0
  50. package/build/{useAudioRecording.js → useAudioRecorder.js} +69 -65
  51. package/build/useAudioRecorder.js.map +1 -0
  52. package/build/utils/convertPCMToFloat32.d.ts +11 -0
  53. package/build/utils/convertPCMToFloat32.d.ts.map +1 -0
  54. package/build/utils/convertPCMToFloat32.js +41 -0
  55. package/build/utils/convertPCMToFloat32.js.map +1 -0
  56. package/build/utils/encodingToBitDepth.d.ts +5 -0
  57. package/build/utils/encodingToBitDepth.d.ts.map +1 -0
  58. package/build/utils/encodingToBitDepth.js +13 -0
  59. package/build/utils/encodingToBitDepth.js.map +1 -0
  60. package/build/utils/getWavFileInfo.d.ts +25 -0
  61. package/build/utils/getWavFileInfo.d.ts.map +1 -0
  62. package/build/utils/getWavFileInfo.js +89 -0
  63. package/build/utils/getWavFileInfo.js.map +1 -0
  64. package/build/utils/writeWavHeader.d.ts +9 -0
  65. package/build/utils/writeWavHeader.d.ts.map +1 -0
  66. package/build/utils/writeWavHeader.js +41 -0
  67. package/build/utils/writeWavHeader.js.map +1 -0
  68. package/build/workers/InlineFeaturesExtractor.web.d.ts +2 -0
  69. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
  70. package/build/workers/InlineFeaturesExtractor.web.js +303 -0
  71. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -0
  72. package/build/workers/inlineAudioWebWorker.web.d.ts +2 -0
  73. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
  74. package/build/workers/inlineAudioWebWorker.web.js +243 -0
  75. package/build/workers/inlineAudioWebWorker.web.js.map +1 -0
  76. package/ios/AudioStreamManager.swift +39 -2
  77. package/ios/ExpoAudioStreamModule.swift +10 -0
  78. package/package.json +7 -6
  79. package/plugin/tsconfig.json +1 -1
  80. package/publish.sh +0 -0
  81. package/src/AudioAnalysis/AudioAnalysis.types.ts +85 -0
  82. package/src/AudioAnalysis/extractAudioAnalysis.ts +136 -0
  83. package/src/AudioAnalysis/extractWaveform.ts +25 -0
  84. package/src/AudioRecorder.provider.tsx +35 -7
  85. package/src/ExpoAudioStream.types.ts +33 -94
  86. package/src/ExpoAudioStream.web.ts +17 -16
  87. package/src/ExpoAudioStreamModule.ts +6 -1
  88. package/src/{WebRecorder.ts → WebRecorder.web.ts} +85 -33
  89. package/src/constants.ts +18 -0
  90. package/src/events.ts +25 -0
  91. package/src/index.ts +8 -169
  92. package/src/logger.ts +26 -0
  93. package/src/{useAudioRecording.tsx → useAudioRecorder.tsx} +141 -136
  94. package/src/utils/convertPCMToFloat32.ts +48 -0
  95. package/src/utils/encodingToBitDepth.ts +18 -0
  96. package/src/utils/getWavFileInfo.ts +125 -0
  97. package/src/utils/writeWavHeader.ts +56 -0
  98. package/src/workers/InlineFeaturesExtractor.web.tsx +302 -0
  99. package/src/workers/inlineAudioWebWorker.web.tsx +242 -0
  100. package/build/WebRecorder.d.ts.map +0 -1
  101. package/build/WebRecorder.js.map +0 -1
  102. package/build/inlineAudioWebWorker.d.ts +0 -3
  103. package/build/inlineAudioWebWorker.d.ts.map +0 -1
  104. package/build/inlineAudioWebWorker.js +0 -340
  105. package/build/inlineAudioWebWorker.js.map +0 -1
  106. package/build/useAudioRecording.d.ts.map +0 -1
  107. package/build/useAudioRecording.js.map +0 -1
  108. package/build/utils.d.ts +0 -31
  109. package/build/utils.d.ts.map +0 -1
  110. package/build/utils.js +0 -143
  111. package/build/utils.js.map +0 -1
  112. package/src/inlineAudioWebWorker.tsx +0 -340
  113. package/src/utils.ts +0 -189
package/src/index.ts CHANGED
@@ -1,183 +1,22 @@
1
1
  // src/index.ts
2
- import { EventEmitter, type Subscription } from "expo-modules-core";
3
- import { Platform } from "react-native";
4
2
 
5
- // Import the native module. On web, it will be resolved to ExpoAudioStream.web.ts
6
- // and on native platforms to ExpoAudioStream.ts
3
+ import { extractAudioAnalysis } from "./AudioAnalysis/extractAudioAnalysis";
7
4
  import {
8
5
  AudioRecorderProvider,
9
6
  useSharedAudioRecorder,
10
7
  } from "./AudioRecorder.provider";
11
- import { AudioAnalysisData, AudioEventPayload } from "./ExpoAudioStream.types";
12
- import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
13
- import { ExtractMetadataProps, useAudioRecorder } from "./useAudioRecording";
14
- import { convertPCMToFloat32, getWavFileInfo, writeWavHeader } from "./utils";
8
+ import { useAudioRecorder } from "./useAudioRecorder";
15
9
 
16
- const emitter = new EventEmitter(ExpoAudioStreamModule);
17
-
18
- export function addAudioEventListener(
19
- listener: (event: AudioEventPayload) => Promise<void>,
20
- ): Subscription {
21
- console.log(`addAudioEventListener`, listener);
22
- return emitter.addListener<AudioEventPayload>("AudioData", listener);
23
- }
24
-
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
- };
10
+ export * from "./utils/getWavFileInfo";
11
+ export * from "./utils/convertPCMToFloat32";
12
+ export * from "./utils/writeWavHeader";
173
13
 
174
14
  export {
175
15
  AudioRecorderProvider,
176
- convertPCMToFloat32,
177
- getWavFileInfo,
16
+ extractAudioAnalysis,
178
17
  useAudioRecorder,
179
18
  useSharedAudioRecorder,
180
- writeWavHeader as writeWaveHeader,
181
19
  };
182
20
 
183
- export * from "./ExpoAudioStream.types";
21
+ export type * from "./AudioAnalysis/AudioAnalysis.types";
22
+ export type * from "./ExpoAudioStream.types";
package/src/logger.ts ADDED
@@ -0,0 +1,26 @@
1
+ // packages/expo-audio-stream/src/logger.ts
2
+ import createDebug from "debug";
3
+
4
+ import { DEBUG_NAMESPACE } from "./constants";
5
+
6
+ type ConsoleLike = {
7
+ log: (message: string, ...args: unknown[]) => void;
8
+ debug: (message: string, ...args: unknown[]) => void;
9
+ };
10
+
11
+ export const getLogger = (tag: string): ConsoleLike => {
12
+ const baseLogger = createDebug(`${DEBUG_NAMESPACE}:${tag}`);
13
+
14
+ return {
15
+ log: (...args: unknown[]) => baseLogger(...(args as [unknown])),
16
+ debug: (...args: unknown[]) => baseLogger(...(args as [unknown])),
17
+ };
18
+ };
19
+
20
+ export const enableAllLoggers = () => {
21
+ createDebug.enable(`${DEBUG_NAMESPACE}:*`);
22
+ };
23
+
24
+ export const disableAllLoggers = () => {
25
+ createDebug.disable();
26
+ };
@@ -1,20 +1,27 @@
1
- // src/useAudioRecording.ts
1
+ // src/useAudioRecorder.ts
2
2
  import { Platform, Subscription } from "expo-modules-core";
3
3
  import { useCallback, useEffect, useReducer, useRef } from "react";
4
4
 
5
- import { addAudioAnalysisListener, addAudioEventListener } from ".";
6
5
  import {
7
6
  AudioAnalysisData,
7
+ AudioAnalysisEventPayload,
8
+ AudioFeaturesOptions,
9
+ } from "./AudioAnalysis/AudioAnalysis.types";
10
+ import {
8
11
  AudioDataEvent,
9
12
  AudioEventPayload,
10
- AudioFeaturesOptions,
11
- AudioStreamResult,
13
+ AudioRecordingResult,
12
14
  AudioStreamStatus,
13
15
  RecordingConfig,
14
- StartAudioStreamResult,
16
+ StartRecordingResult,
15
17
  } from "./ExpoAudioStream.types";
16
18
  import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
17
- import { WavFileInfo } from "./utils";
19
+ import { addAudioAnalysisListener, addAudioEventListener } from "./events";
20
+ import { disableAllLoggers, enableAllLoggers, getLogger } from "./logger";
21
+ import { WavFileInfo } from "./utils/getWavFileInfo";
22
+
23
+ const TAG = "useAudioRecorder";
24
+ const logger = getLogger(TAG);
18
25
 
19
26
  export interface ExtractMetadataProps {
20
27
  fileUri?: string; // should provide either fileUri or arrayBuffer
@@ -40,8 +47,8 @@ export interface UseAudioRecorderProps {
40
47
  }
41
48
 
42
49
  export interface UseAudioRecorderState {
43
- startRecording: (_: RecordingConfig) => Promise<StartAudioStreamResult>;
44
- stopRecording: () => Promise<AudioStreamResult | null>;
50
+ startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>;
51
+ stopRecording: () => Promise<AudioRecordingResult | null>;
45
52
  pauseRecording: () => void;
46
53
  resumeRecording: () => void;
47
54
  isRecording: boolean;
@@ -49,11 +56,9 @@ export interface UseAudioRecorderState {
49
56
  durationMs: number; // Duration of the recording
50
57
  size: number; // Size in bytes of the recorded audio
51
58
  analysisData?: AudioAnalysisData;
52
- audioWorkletUrl?: string;
53
- featuresExtratorUrl?: string;
54
59
  }
55
60
 
56
- interface RecorderState {
61
+ interface RecorderReducerState {
57
62
  isRecording: boolean;
58
63
  isPaused: boolean;
59
64
  durationMs: number;
@@ -67,7 +72,7 @@ type RecorderAction =
67
72
  | { type: "UPDATE_ANALYSIS"; payload: AudioAnalysisData };
68
73
 
69
74
  const defaultAnalysis: AudioAnalysisData = {
70
- pointsPerSecond: 20,
75
+ pointsPerSecond: 10,
71
76
  bitDepth: 32,
72
77
  numberOfChannels: 1,
73
78
  durationMs: 0,
@@ -80,10 +85,10 @@ const defaultAnalysis: AudioAnalysisData = {
80
85
  },
81
86
  };
82
87
 
83
- function recorderReducer(
84
- state: RecorderState,
88
+ function audioRecorderReducer(
89
+ state: RecorderReducerState,
85
90
  action: RecorderAction,
86
- ): RecorderState {
91
+ ): RecorderReducerState {
87
92
  switch (action.type) {
88
93
  case "START":
89
94
  return {
@@ -115,14 +120,13 @@ function recorderReducer(
115
120
  return state;
116
121
  }
117
122
  }
118
- const TAG = "[ useAudioRecorder ] ";
119
123
 
120
124
  export function useAudioRecorder({
121
125
  debug = false,
122
126
  audioWorkletUrl,
123
127
  featuresExtratorUrl,
124
128
  }: UseAudioRecorderProps = {}): UseAudioRecorderState {
125
- const [state, dispatch] = useReducer(recorderReducer, {
129
+ const [state, dispatch] = useReducer(audioRecorderReducer, {
126
130
  isRecording: false,
127
131
  isPaused: false,
128
132
  durationMs: 0,
@@ -143,26 +147,13 @@ export function useAudioRecorder({
143
147
  ((_: AudioDataEvent) => Promise<void>) | null
144
148
  >(null);
145
149
 
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
150
  const handleAudioAnalysis = useCallback(
160
- async (analysis: AudioAnalysisData, visualizationDuration: number) => {
151
+ async ({ analysis, visualizationDuration }: AudioAnalysisEventPayload) => {
161
152
  const savedAnalysisData = analysisRef.current || { ...defaultAnalysis };
162
153
 
163
154
  const maxDuration = visualizationDuration;
164
155
 
165
- logDebug(
156
+ logger.debug(
166
157
  `[handleAudioAnalysis] Received audio analysis: maxDuration=${maxDuration} analysis.dataPoints=${analysis.dataPoints.length} analysisData.dataPoints=${savedAnalysisData.dataPoints.length}`,
167
158
  analysis,
168
159
  );
@@ -178,7 +169,7 @@ export function useAudioRecorder({
178
169
  analysis.pointsPerSecond || savedAnalysisData.pointsPerSecond;
179
170
  const maxDataPoints = (pointsPerSecond * visualizationDuration) / 1000;
180
171
 
181
- logDebug(
172
+ logger.debug(
182
173
  `[handleAudioAnalysis] Combined data points before trimming: pointsPerSecond=${pointsPerSecond} visualizationDuration=${visualizationDuration} combinedDataPointsLength=${combinedDataPoints.length} vs maxDataPoints=${maxDataPoints}`,
183
174
  );
184
175
 
@@ -208,7 +199,7 @@ export function useAudioRecorder({
208
199
  max: newMax,
209
200
  };
210
201
 
211
- logDebug(
202
+ logger.debug(
212
203
  `[handleAudioAnalysis] Updated analysis data: durationMs=${savedAnalysisData.durationMs}`,
213
204
  savedAnalysisData,
214
205
  );
@@ -220,78 +211,80 @@ export function useAudioRecorder({
220
211
  // need to use spread operator otherwise it doesnt trigger update.
221
212
  dispatch({ type: "UPDATE_ANALYSIS", payload: { ...savedAnalysisData } });
222
213
  },
223
- [logDebug],
214
+ [dispatch],
224
215
  );
225
216
 
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
- });
217
+ const handleAudioEvent = useCallback(async (eventData: AudioEventPayload) => {
218
+ const {
219
+ fileUri,
220
+ deltaSize,
221
+ totalSize,
222
+ lastEmittedSize,
223
+ position,
224
+ streamUuid,
225
+ encoded,
226
+ mimeType,
227
+ buffer,
228
+ } = eventData;
229
+ logger.debug(`[handleAudioEvent] Received audio event:`, {
230
+ fileUri,
231
+ deltaSize,
232
+ totalSize,
233
+ position,
234
+ mimeType,
235
+ lastEmittedSize,
236
+ streamUuid,
237
+ encodedLength: encoded?.length,
238
+ });
239
+ if (deltaSize === 0) {
240
+ // Ignore packet with no data
241
+ return;
242
+ }
243
+ try {
244
+ // Coming from native ( ios / android ) otherwise buffer is set
245
+ if (Platform.OS !== "web") {
246
+ // Read the audio file as a base64 string for comparison
247
+ if (!encoded) {
248
+ console.error(`${TAG} Encoded audio data is missing`);
249
+ throw new Error("Encoded audio data is missing");
277
250
  }
278
- } catch (error) {
279
- console.error(`${TAG} Error processing audio event:`, error);
251
+ onAudioStreamRef.current?.({
252
+ data: encoded,
253
+ position,
254
+ fileUri,
255
+ eventDataSize: deltaSize,
256
+ totalSize,
257
+ });
258
+ } else if (buffer) {
259
+ // Coming from web
260
+ const webEvent: AudioDataEvent = {
261
+ data: buffer,
262
+ position,
263
+ fileUri,
264
+ eventDataSize: deltaSize,
265
+ totalSize,
266
+ };
267
+ onAudioStreamRef.current?.(webEvent);
268
+ logger.debug(
269
+ `[handleAudioEvent] Audio data sent to onAudioStream`,
270
+ webEvent,
271
+ );
280
272
  }
281
- },
282
- [logDebug],
283
- );
273
+ } catch (error) {
274
+ console.error(`${TAG} Error processing audio event:`, error);
275
+ }
276
+ }, []);
284
277
 
285
278
  const checkStatus = useCallback(async () => {
286
279
  try {
287
280
  if (!state.isRecording) {
288
- logDebug(`${TAG} Not recording, exiting status check.`);
281
+ logger.debug(`Not recording, exiting status check.`);
289
282
  return;
290
283
  }
291
284
 
292
285
  const status: AudioStreamStatus = ExpoAudioStream.status();
293
286
  if (debug) {
294
- logDebug(`${TAG} Status:`, status);
287
+ logger.debug(`Status:`, status);
295
288
  }
296
289
 
297
290
  dispatch({
@@ -301,59 +294,36 @@ export function useAudioRecorder({
301
294
  } catch (error) {
302
295
  console.error(`${TAG} Error getting status:`, error);
303
296
  }
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]);
297
+ }, [state.isRecording]);
331
298
 
332
299
  const startRecording = useCallback(
333
300
  async (recordingOptions: RecordingConfig) => {
334
- if (debug) {
335
- logDebug(`start recoding`, recordingOptions);
336
- }
301
+ logger.debug(`start recoding`, recordingOptions);
337
302
 
338
303
  analysisRef.current = { ...defaultAnalysis }; // Reset analysis data
339
304
 
340
305
  const { onAudioStream, ...options } = recordingOptions;
341
- const { maxRecentDataDuration = 10000, enableProcessing } = options;
306
+ const { enableProcessing } = options;
307
+
308
+ const maxRecentDataDuration = 10000; // TODO compute maxRecentDataDuration based on screen dimensions
342
309
  if (typeof onAudioStream === "function") {
343
310
  onAudioStreamRef.current = onAudioStream;
344
311
  } else {
345
312
  console.warn(`${TAG} onAudioStream is not a function`, onAudioStream);
346
313
  onAudioStreamRef.current = null;
347
314
  }
348
- const startResult: StartAudioStreamResult =
315
+ const startResult: StartRecordingResult =
349
316
  await ExpoAudioStream.startRecording(options);
350
317
  dispatch({ type: "START" });
351
318
 
352
319
  if (enableProcessing) {
353
- logDebug(`Enabling audio analysis listener`);
320
+ logger.debug(`Enabling audio analysis listener`);
354
321
  const listener = addAudioAnalysisListener(async (analysisData) => {
355
322
  try {
356
- await handleAudioAnalysis(analysisData, maxRecentDataDuration);
323
+ await handleAudioAnalysis({
324
+ analysis: analysisData,
325
+ visualizationDuration: maxRecentDataDuration,
326
+ });
357
327
  } catch (error) {
358
328
  console.warn(`${TAG} Error processing audio analysis:`, error);
359
329
  }
@@ -364,37 +334,72 @@ export function useAudioRecorder({
364
334
 
365
335
  return startResult;
366
336
  },
367
- [logDebug],
337
+ [handleAudioAnalysis, dispatch],
368
338
  );
369
339
 
370
340
  const stopRecording = useCallback(async () => {
371
- logDebug(`${TAG} stoping recording`);
341
+ logger.debug(`stoping recording`);
372
342
 
373
343
  if (analysisListenerRef.current) {
374
344
  analysisListenerRef.current.remove();
375
345
  analysisListenerRef.current = null;
376
346
  }
377
347
 
378
- const stopResult: AudioStreamResult = await ExpoAudioStream.stopRecording();
348
+ const stopResult: AudioRecordingResult =
349
+ await ExpoAudioStream.stopRecording();
379
350
  onAudioStreamRef.current = null;
380
- logDebug(`${TAG} recording stopped`, stopResult);
351
+ logger.debug(`recording stopped`, stopResult);
381
352
  dispatch({ type: "STOP" });
382
353
  return stopResult;
383
- }, [logDebug]);
354
+ }, [dispatch]);
384
355
 
385
356
  const pauseRecording = useCallback(async () => {
386
- logDebug(`${TAG} pause recording`);
357
+ logger.debug(`pause recording`);
387
358
  const pauseResult = await ExpoAudioStream.pauseRecording();
388
359
  dispatch({ type: "PAUSE" });
389
360
  return pauseResult;
390
- }, [logDebug]);
361
+ }, [dispatch]);
391
362
 
392
363
  const resumeRecording = useCallback(async () => {
393
- logDebug(`${TAG} resume recording`);
364
+ logger.debug(`resume recording`);
394
365
  const resumeResult = await ExpoAudioStream.resumeRecording();
395
366
  dispatch({ type: "RESUME" });
396
367
  return resumeResult;
397
- }, [logDebug]);
368
+ }, [dispatch]);
369
+
370
+ useEffect(() => {
371
+ let interval: ReturnType<typeof setTimeout>;
372
+ if (state.isRecording) {
373
+ interval = setInterval(checkStatus, 1000);
374
+ }
375
+ return () => {
376
+ if (interval) {
377
+ clearInterval(interval);
378
+ }
379
+ };
380
+ }, [checkStatus, state.isRecording]);
381
+
382
+ useEffect(() => {
383
+ logger.debug(`Registering audio event listener`);
384
+ const subscribeAudio = addAudioEventListener(handleAudioEvent);
385
+
386
+ logger.debug(`Subscribed to audio event listener and analysis listener`, {
387
+ subscribeAudio,
388
+ });
389
+
390
+ return () => {
391
+ logger.debug(`Removing audio event listener`);
392
+ subscribeAudio.remove();
393
+ };
394
+ }, [handleAudioEvent, handleAudioAnalysis]);
395
+
396
+ useEffect(() => {
397
+ if (debug) {
398
+ enableAllLoggers();
399
+ } else {
400
+ disableAllLoggers();
401
+ }
402
+ }, [debug]);
398
403
 
399
404
  return {
400
405
  startRecording,
@@ -0,0 +1,48 @@
1
+ export const WAV_HEADER_SIZE = 44;
2
+ export const convertPCMToFloat32 = ({
3
+ bitDepth,
4
+ buffer,
5
+ skipWavHeader = false,
6
+ }: {
7
+ buffer: ArrayBuffer;
8
+ bitDepth: number;
9
+ skipWavHeader?: boolean;
10
+ }): { pcmValues: Float32Array; min: number; max: number } => {
11
+ const dataView = new DataView(buffer);
12
+ const headerOffset = skipWavHeader ? WAV_HEADER_SIZE : 0;
13
+ const dataLength = buffer.byteLength - headerOffset;
14
+ const sampleLength = dataLength / (bitDepth / 8);
15
+ const float32Array = new Float32Array(sampleLength);
16
+ let min = Infinity;
17
+ let max = -Infinity;
18
+
19
+ for (let i = 0; i < sampleLength; i++) {
20
+ let value = 0;
21
+ const offset = headerOffset + i * (bitDepth / 8);
22
+ switch (bitDepth) {
23
+ case 8:
24
+ value = dataView.getUint8(offset) / 128;
25
+ break;
26
+ case 16:
27
+ value = dataView.getInt16(offset, true) / 32768;
28
+ break;
29
+ case 24:
30
+ value =
31
+ (dataView.getUint8(offset) +
32
+ (dataView.getUint8(offset + 1) << 8) +
33
+ (dataView.getUint8(offset + 2) << 16)) /
34
+ 8388608;
35
+ break;
36
+ case 32:
37
+ value = dataView.getFloat32(offset, true);
38
+ break;
39
+ default:
40
+ throw new Error(`Unsupported bit depth: ${bitDepth}`);
41
+ }
42
+ if (value < min) min = value;
43
+ if (value > max) max = value;
44
+ float32Array[i] = value;
45
+ }
46
+
47
+ return { pcmValues: float32Array, min, max };
48
+ };