@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.
- package/.size-limit.json +6 -0
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +76 -0
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
- package/build/AudioAnalysis/AudioAnalysis.types.js +3 -0
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +4 -0
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
- package/build/AudioAnalysis/extractAudioAnalysis.js +101 -0
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
- package/build/AudioAnalysis/extractWaveform.d.ts +8 -0
- package/build/AudioAnalysis/extractWaveform.d.ts.map +1 -0
- package/build/AudioAnalysis/extractWaveform.js +14 -0
- package/build/AudioAnalysis/extractWaveform.js.map +1 -0
- package/build/AudioRecorder.provider.d.ts +14 -1
- package/build/AudioRecorder.provider.d.ts.map +1 -1
- package/build/AudioRecorder.provider.js +17 -4
- package/build/AudioRecorder.provider.js.map +1 -1
- package/build/ExpoAudioStream.types.d.ts +26 -84
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts +6 -5
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +9 -8
- package/build/ExpoAudioStream.web.js.map +1 -1
- package/build/ExpoAudioStreamModule.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.js +5 -1
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/{WebRecorder.d.ts → WebRecorder.web.d.ts} +7 -3
- package/build/WebRecorder.web.d.ts.map +1 -0
- package/build/{WebRecorder.js → WebRecorder.web.js} +74 -29
- package/build/WebRecorder.web.js.map +1 -0
- package/build/constants.d.ts +11 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +14 -0
- package/build/constants.js.map +1 -0
- package/build/events.d.ts +6 -0
- package/build/events.d.ts.map +1 -0
- package/build/events.js +15 -0
- package/build/events.js.map +1 -0
- package/build/index.d.ts +8 -16
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -112
- package/build/index.js.map +1 -1
- package/build/logger.d.ts +9 -0
- package/build/logger.d.ts.map +1 -0
- package/build/logger.js +17 -0
- package/build/logger.js.map +1 -0
- package/build/{useAudioRecording.d.ts → useAudioRecorder.d.ts} +6 -7
- package/build/useAudioRecorder.d.ts.map +1 -0
- package/build/{useAudioRecording.js → useAudioRecorder.js} +69 -65
- package/build/useAudioRecorder.js.map +1 -0
- package/build/utils/convertPCMToFloat32.d.ts +11 -0
- package/build/utils/convertPCMToFloat32.d.ts.map +1 -0
- package/build/utils/convertPCMToFloat32.js +41 -0
- package/build/utils/convertPCMToFloat32.js.map +1 -0
- package/build/utils/encodingToBitDepth.d.ts +5 -0
- package/build/utils/encodingToBitDepth.d.ts.map +1 -0
- package/build/utils/encodingToBitDepth.js +13 -0
- package/build/utils/encodingToBitDepth.js.map +1 -0
- package/build/utils/getWavFileInfo.d.ts +25 -0
- package/build/utils/getWavFileInfo.d.ts.map +1 -0
- package/build/utils/getWavFileInfo.js +89 -0
- package/build/utils/getWavFileInfo.js.map +1 -0
- package/build/utils/writeWavHeader.d.ts +9 -0
- package/build/utils/writeWavHeader.d.ts.map +1 -0
- package/build/utils/writeWavHeader.js +41 -0
- package/build/utils/writeWavHeader.js.map +1 -0
- package/build/workers/InlineFeaturesExtractor.web.d.ts +2 -0
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
- package/build/workers/InlineFeaturesExtractor.web.js +303 -0
- package/build/workers/InlineFeaturesExtractor.web.js.map +1 -0
- package/build/workers/inlineAudioWebWorker.web.d.ts +2 -0
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
- package/build/workers/inlineAudioWebWorker.web.js +243 -0
- package/build/workers/inlineAudioWebWorker.web.js.map +1 -0
- package/ios/AudioStreamManager.swift +39 -2
- package/ios/ExpoAudioStreamModule.swift +10 -0
- package/package.json +7 -6
- package/plugin/tsconfig.json +1 -1
- package/publish.sh +0 -0
- package/src/AudioAnalysis/AudioAnalysis.types.ts +85 -0
- package/src/AudioAnalysis/extractAudioAnalysis.ts +136 -0
- package/src/AudioAnalysis/extractWaveform.ts +25 -0
- package/src/AudioRecorder.provider.tsx +35 -7
- package/src/ExpoAudioStream.types.ts +33 -94
- package/src/ExpoAudioStream.web.ts +17 -16
- package/src/ExpoAudioStreamModule.ts +6 -1
- package/src/{WebRecorder.ts → WebRecorder.web.ts} +85 -33
- package/src/constants.ts +18 -0
- package/src/events.ts +25 -0
- package/src/index.ts +8 -169
- package/src/logger.ts +26 -0
- package/src/{useAudioRecording.tsx → useAudioRecorder.tsx} +141 -136
- package/src/utils/convertPCMToFloat32.ts +48 -0
- package/src/utils/encodingToBitDepth.ts +18 -0
- package/src/utils/getWavFileInfo.ts +125 -0
- package/src/utils/writeWavHeader.ts +56 -0
- package/src/workers/InlineFeaturesExtractor.web.tsx +302 -0
- package/src/workers/inlineAudioWebWorker.web.tsx +242 -0
- package/build/WebRecorder.d.ts.map +0 -1
- package/build/WebRecorder.js.map +0 -1
- package/build/inlineAudioWebWorker.d.ts +0 -3
- package/build/inlineAudioWebWorker.d.ts.map +0 -1
- package/build/inlineAudioWebWorker.js +0 -340
- package/build/inlineAudioWebWorker.js.map +0 -1
- package/build/useAudioRecording.d.ts.map +0 -1
- package/build/useAudioRecording.js.map +0 -1
- package/build/utils.d.ts +0 -31
- package/build/utils.d.ts.map +0 -1
- package/build/utils.js +0 -143
- package/build/utils.js.map +0 -1
- package/src/inlineAudioWebWorker.tsx +0 -340
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
export
|
|
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
|
-
|
|
177
|
-
getWavFileInfo,
|
|
16
|
+
extractAudioAnalysis,
|
|
178
17
|
useAudioRecorder,
|
|
179
18
|
useSharedAudioRecorder,
|
|
180
|
-
writeWavHeader as writeWaveHeader,
|
|
181
19
|
};
|
|
182
20
|
|
|
183
|
-
export * from "./
|
|
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/
|
|
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
|
-
|
|
11
|
-
AudioStreamResult,
|
|
13
|
+
AudioRecordingResult,
|
|
12
14
|
AudioStreamStatus,
|
|
13
15
|
RecordingConfig,
|
|
14
|
-
|
|
16
|
+
StartRecordingResult,
|
|
15
17
|
} from "./ExpoAudioStream.types";
|
|
16
18
|
import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
|
|
17
|
-
import {
|
|
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<
|
|
44
|
-
stopRecording: () => Promise<
|
|
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
|
|
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:
|
|
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
|
|
84
|
-
state:
|
|
88
|
+
function audioRecorderReducer(
|
|
89
|
+
state: RecorderReducerState,
|
|
85
90
|
action: RecorderAction,
|
|
86
|
-
):
|
|
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(
|
|
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
|
|
151
|
+
async ({ analysis, visualizationDuration }: AudioAnalysisEventPayload) => {
|
|
161
152
|
const savedAnalysisData = analysisRef.current || { ...defaultAnalysis };
|
|
162
153
|
|
|
163
154
|
const maxDuration = visualizationDuration;
|
|
164
155
|
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
214
|
+
[dispatch],
|
|
224
215
|
);
|
|
225
216
|
|
|
226
|
-
const handleAudioEvent = useCallback(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
315
|
+
const startResult: StartRecordingResult =
|
|
349
316
|
await ExpoAudioStream.startRecording(options);
|
|
350
317
|
dispatch({ type: "START" });
|
|
351
318
|
|
|
352
319
|
if (enableProcessing) {
|
|
353
|
-
|
|
320
|
+
logger.debug(`Enabling audio analysis listener`);
|
|
354
321
|
const listener = addAudioAnalysisListener(async (analysisData) => {
|
|
355
322
|
try {
|
|
356
|
-
await handleAudioAnalysis(
|
|
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
|
-
[
|
|
337
|
+
[handleAudioAnalysis, dispatch],
|
|
368
338
|
);
|
|
369
339
|
|
|
370
340
|
const stopRecording = useCallback(async () => {
|
|
371
|
-
|
|
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:
|
|
348
|
+
const stopResult: AudioRecordingResult =
|
|
349
|
+
await ExpoAudioStream.stopRecording();
|
|
379
350
|
onAudioStreamRef.current = null;
|
|
380
|
-
|
|
351
|
+
logger.debug(`recording stopped`, stopResult);
|
|
381
352
|
dispatch({ type: "STOP" });
|
|
382
353
|
return stopResult;
|
|
383
|
-
}, [
|
|
354
|
+
}, [dispatch]);
|
|
384
355
|
|
|
385
356
|
const pauseRecording = useCallback(async () => {
|
|
386
|
-
|
|
357
|
+
logger.debug(`pause recording`);
|
|
387
358
|
const pauseResult = await ExpoAudioStream.pauseRecording();
|
|
388
359
|
dispatch({ type: "PAUSE" });
|
|
389
360
|
return pauseResult;
|
|
390
|
-
}, [
|
|
361
|
+
}, [dispatch]);
|
|
391
362
|
|
|
392
363
|
const resumeRecording = useCallback(async () => {
|
|
393
|
-
|
|
364
|
+
logger.debug(`resume recording`);
|
|
394
365
|
const resumeResult = await ExpoAudioStream.resumeRecording();
|
|
395
366
|
dispatch({ type: "RESUME" });
|
|
396
367
|
return resumeResult;
|
|
397
|
-
}, [
|
|
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
|
+
};
|