@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
|
@@ -1,18 +1,46 @@
|
|
|
1
|
+
// packages/expo-audio-stream/src/AudioRecorder.provider.tsx
|
|
1
2
|
import React, { createContext, useContext } from "react";
|
|
2
3
|
|
|
4
|
+
import { AudioAnalysisData } from "./AudioAnalysis/AudioAnalysis.types";
|
|
3
5
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "./
|
|
6
|
+
AudioRecordingResult,
|
|
7
|
+
RecordingConfig,
|
|
8
|
+
StartRecordingResult,
|
|
9
|
+
} from "./ExpoAudioStream.types";
|
|
10
|
+
import { UseAudioRecorderProps, useAudioRecorder } from "./useAudioRecorder";
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
export interface UseAudioRecorderState {
|
|
13
|
+
startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>;
|
|
14
|
+
stopRecording: () => Promise<AudioRecordingResult | null>;
|
|
15
|
+
pauseRecording: () => void;
|
|
16
|
+
resumeRecording: () => void;
|
|
17
|
+
isRecording: boolean;
|
|
18
|
+
isPaused: boolean;
|
|
19
|
+
durationMs: number; // Duration of the recording
|
|
20
|
+
size: number; // Size in bytes of the recorded audio
|
|
21
|
+
analysisData?: AudioAnalysisData;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const initContext: UseAudioRecorderState = {
|
|
10
25
|
isRecording: false,
|
|
11
26
|
isPaused: false,
|
|
12
27
|
durationMs: 0,
|
|
13
28
|
size: 0,
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
startRecording: async () => {
|
|
30
|
+
throw new Error("AudioRecorderProvider not found");
|
|
31
|
+
},
|
|
32
|
+
stopRecording: async () => {
|
|
33
|
+
throw new Error("AudioRecorderProvider not found");
|
|
34
|
+
},
|
|
35
|
+
pauseRecording: () => {
|
|
36
|
+
throw new Error("AudioRecorderProvider not found");
|
|
37
|
+
},
|
|
38
|
+
resumeRecording: () => {
|
|
39
|
+
throw new Error("AudioRecorderProvider not found");
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const AudioRecorderContext = createContext<UseAudioRecorderState>(initContext);
|
|
16
44
|
|
|
17
45
|
interface AudioRecorderProviderProps {
|
|
18
46
|
children: React.ReactNode;
|
|
@@ -1,33 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
position: number;
|
|
7
|
-
deltaSize: number;
|
|
8
|
-
totalSize: number;
|
|
9
|
-
mimeType: string;
|
|
10
|
-
streamUuid: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface AudioStreamResult {
|
|
14
|
-
fileUri: string;
|
|
15
|
-
webAudioUri?: string;
|
|
16
|
-
durationMs: number;
|
|
17
|
-
size: number;
|
|
18
|
-
mimeType: string;
|
|
19
|
-
channels?: number;
|
|
20
|
-
bitDepth?: number;
|
|
21
|
-
sampleRate?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface StartAudioStreamResult {
|
|
25
|
-
fileUri: string;
|
|
26
|
-
mimeType: string;
|
|
27
|
-
channels?: number;
|
|
28
|
-
bitDepth?: number;
|
|
29
|
-
sampleRate?: number;
|
|
30
|
-
}
|
|
1
|
+
// packages/expo-audio-stream/src/ExpoAudioStream.types.ts
|
|
2
|
+
import {
|
|
3
|
+
AudioAnalysisData,
|
|
4
|
+
AudioFeaturesOptions,
|
|
5
|
+
} from "./AudioAnalysis/AudioAnalysis.types";
|
|
31
6
|
|
|
32
7
|
export interface AudioStreamStatus {
|
|
33
8
|
isRecording: boolean;
|
|
@@ -46,74 +21,41 @@ export interface AudioDataEvent {
|
|
|
46
21
|
totalSize: number;
|
|
47
22
|
}
|
|
48
23
|
|
|
49
|
-
export interface
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
spectralBandwidth: number;
|
|
60
|
-
chromagram: number[];
|
|
61
|
-
tempo: number;
|
|
62
|
-
hnr: number;
|
|
24
|
+
export interface AudioEventPayload {
|
|
25
|
+
encoded?: string;
|
|
26
|
+
buffer?: ArrayBuffer;
|
|
27
|
+
fileUri: string;
|
|
28
|
+
lastEmittedSize: number;
|
|
29
|
+
position: number;
|
|
30
|
+
deltaSize: number;
|
|
31
|
+
totalSize: number;
|
|
32
|
+
mimeType: string;
|
|
33
|
+
streamUuid: string;
|
|
63
34
|
}
|
|
64
35
|
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
rms?: boolean;
|
|
69
|
-
zcr?: boolean;
|
|
70
|
-
spectralCentroid?: boolean;
|
|
71
|
-
spectralFlatness?: boolean;
|
|
72
|
-
spectralRolloff?: boolean;
|
|
73
|
-
spectralBandwidth?: boolean;
|
|
74
|
-
chromagram?: boolean;
|
|
75
|
-
tempo?: boolean;
|
|
76
|
-
hnr?: boolean;
|
|
77
|
-
}
|
|
36
|
+
export type EncodingType = "pcm_32bit" | "pcm_16bit" | "pcm_8bit";
|
|
37
|
+
export type SampleRate = 16000 | 44100 | 48000;
|
|
38
|
+
export type BitDepth = 8 | 16 | 32;
|
|
78
39
|
|
|
79
|
-
export interface
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// start / end position in bytes
|
|
89
|
-
startPosition?: number;
|
|
90
|
-
endPosition?: number;
|
|
91
|
-
// number of audio samples for this point (samples size depends on bit depth)
|
|
92
|
-
samples?: number;
|
|
93
|
-
// Id of the speaker for this point
|
|
94
|
-
speaker?: number;
|
|
40
|
+
export interface AudioRecordingResult {
|
|
41
|
+
fileUri: string;
|
|
42
|
+
webAudioUri?: string;
|
|
43
|
+
durationMs: number;
|
|
44
|
+
size: number;
|
|
45
|
+
mimeType: string;
|
|
46
|
+
channels: number;
|
|
47
|
+
bitDepth: BitDepth;
|
|
48
|
+
sampleRate: SampleRate;
|
|
95
49
|
}
|
|
96
50
|
|
|
97
|
-
export interface
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
sampleRate: number; // Sample rate of the audio
|
|
104
|
-
dataPoints: DataPoint[];
|
|
105
|
-
amplitudeRange: {
|
|
106
|
-
min: number;
|
|
107
|
-
max: number;
|
|
108
|
-
};
|
|
109
|
-
speakerChanges?: {
|
|
110
|
-
timestamp: number;
|
|
111
|
-
speaker: number;
|
|
112
|
-
}[];
|
|
51
|
+
export interface StartRecordingResult {
|
|
52
|
+
fileUri: string;
|
|
53
|
+
mimeType: string;
|
|
54
|
+
channels?: number;
|
|
55
|
+
bitDepth?: BitDepth;
|
|
56
|
+
sampleRate?: SampleRate;
|
|
113
57
|
}
|
|
114
58
|
|
|
115
|
-
export type EncodingType = "pcm_32bit" | "pcm_16bit" | "pcm_8bit";
|
|
116
|
-
export type SampleRate = 16000 | 44100 | 48000;
|
|
117
59
|
export interface RecordingConfig {
|
|
118
60
|
sampleRate?: SampleRate; // Sample rate for recording
|
|
119
61
|
channels?: 1 | 2; // 1 or 2 (MONO or STEREO)
|
|
@@ -121,15 +63,12 @@ export interface RecordingConfig {
|
|
|
121
63
|
interval?: number; // Interval in milliseconds at which to emit recording data
|
|
122
64
|
|
|
123
65
|
// Optional parameters for audio processing
|
|
124
|
-
//TODO remove maxRecentDataDuration - should be replaced by maxDataPoints to 100.
|
|
125
|
-
maxRecentDataDuration?: number; // Maximum duration of recent data to keep for processing (default is 10.0 seconds)
|
|
126
66
|
enableProcessing?: boolean; // Boolean to enable/disable audio processing (default is false)
|
|
127
67
|
pointsPerSecond?: number; // Number of data points to extract per second of audio (default is 1000)
|
|
128
68
|
algorithm?: string; // Algorithm to use for extraction (default is "rms")
|
|
129
69
|
features?: AudioFeaturesOptions; // Feature options to extract (default is empty)
|
|
130
70
|
|
|
131
71
|
// Optional paramters from web
|
|
132
|
-
|
|
133
72
|
onAudioStream?: (_: AudioDataEvent) => Promise<void>; // Callback function to handle audio stream
|
|
134
73
|
onProcessingResult?: (_: AudioAnalysisData) => Promise<void>; // Callback function to handle processing results
|
|
135
74
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// src/ExpoAudioStreamModule.web.ts
|
|
2
|
-
import debug from "debug";
|
|
3
2
|
import { EventEmitter } from "expo-modules-core";
|
|
4
3
|
|
|
4
|
+
import { AudioAnalysisData } from "./AudioAnalysis/AudioAnalysis.types";
|
|
5
5
|
import {
|
|
6
|
-
AudioAnalysisData,
|
|
7
6
|
AudioEventPayload,
|
|
8
|
-
|
|
7
|
+
AudioRecordingResult,
|
|
9
8
|
AudioStreamStatus,
|
|
9
|
+
BitDepth,
|
|
10
10
|
RecordingConfig,
|
|
11
|
-
|
|
11
|
+
StartRecordingResult,
|
|
12
12
|
} from "./ExpoAudioStream.types";
|
|
13
|
-
import { WebRecorder } from "./WebRecorder";
|
|
14
|
-
import {
|
|
13
|
+
import { WebRecorder } from "./WebRecorder.web";
|
|
14
|
+
import { getLogger } from "./logger";
|
|
15
|
+
import { encodingToBitDepth } from "./utils/encodingToBitDepth";
|
|
15
16
|
|
|
16
17
|
export interface EmitAudioEventProps {
|
|
17
18
|
data: ArrayBuffer;
|
|
@@ -25,8 +26,8 @@ export interface ExpoAudioStreamWebProps {
|
|
|
25
26
|
featuresExtratorUrl: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const logger = getLogger("ExpoAudioStreamWeb");
|
|
30
|
+
|
|
30
31
|
export class ExpoAudioStreamWeb extends EventEmitter {
|
|
31
32
|
customRecorder: WebRecorder | null;
|
|
32
33
|
audioChunks: ArrayBuffer[];
|
|
@@ -42,7 +43,7 @@ export class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
42
43
|
streamUuid: string | null;
|
|
43
44
|
extension: "webm" | "wav" = "wav"; // Default extension is 'webm'
|
|
44
45
|
recordingConfig?: RecordingConfig;
|
|
45
|
-
bitDepth:
|
|
46
|
+
bitDepth: BitDepth; // Bit depth of the audio
|
|
46
47
|
audioWorkletUrl: string;
|
|
47
48
|
featuresExtratorUrl: string;
|
|
48
49
|
|
|
@@ -118,7 +119,7 @@ export class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
118
119
|
this.lastEmittedSize = this.currentSize;
|
|
119
120
|
},
|
|
120
121
|
emitAudioAnalysisCallback: (audioAnalysisData: AudioAnalysisData) => {
|
|
121
|
-
|
|
122
|
+
logger.log(`Emitted AudioAnalysis:`, audioAnalysisData);
|
|
122
123
|
this.emit("AudioAnalysis", audioAnalysisData);
|
|
123
124
|
},
|
|
124
125
|
});
|
|
@@ -127,7 +128,7 @@ export class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
127
128
|
|
|
128
129
|
// // Set a timer to stop recording after 5 seconds
|
|
129
130
|
// setTimeout(() => {
|
|
130
|
-
//
|
|
131
|
+
// logger.log("AUTO Stopping recording");
|
|
131
132
|
// this.customRecorder?.stopAndPlay();
|
|
132
133
|
// this.isRecording = false;
|
|
133
134
|
// }, 3000);
|
|
@@ -140,7 +141,7 @@ export class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
140
141
|
this.lastEmittedTime = 0;
|
|
141
142
|
this.streamUuid = Date.now().toString();
|
|
142
143
|
const fileUri = `${this.streamUuid}.${this.extension}`;
|
|
143
|
-
const streamConfig:
|
|
144
|
+
const streamConfig: StartRecordingResult = {
|
|
144
145
|
fileUri,
|
|
145
146
|
mimeType: `audio/${this.extension}`,
|
|
146
147
|
bitDepth: this.bitDepth,
|
|
@@ -167,14 +168,14 @@ export class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
// Stop recording
|
|
170
|
-
async stopRecording(): Promise<
|
|
171
|
+
async stopRecording(): Promise<AudioRecordingResult | null> {
|
|
171
172
|
if (this.customRecorder) {
|
|
172
173
|
const fullPcmBuffer = await this.customRecorder.stop();
|
|
173
|
-
|
|
174
|
+
logger.debug(`Stopped recording`, fullPcmBuffer);
|
|
174
175
|
}
|
|
175
176
|
this.isRecording = false;
|
|
176
177
|
this.currentDurationMs = Date.now() - this.recordingStartTime;
|
|
177
|
-
const result:
|
|
178
|
+
const result: AudioRecordingResult = {
|
|
178
179
|
fileUri: `${this.streamUuid}.${this.extension}`,
|
|
179
180
|
bitDepth: this.bitDepth,
|
|
180
181
|
channels: this.recordingConfig?.channels ?? 1,
|
|
@@ -194,7 +195,7 @@ export class ExpoAudioStreamWeb extends EventEmitter {
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
if (this.customRecorder) {
|
|
197
|
-
this.customRecorder.
|
|
198
|
+
this.customRecorder.pause();
|
|
198
199
|
}
|
|
199
200
|
this.isPaused = true;
|
|
200
201
|
this.pausedTime = Date.now();
|
|
@@ -9,8 +9,13 @@ import {
|
|
|
9
9
|
let ExpoAudioStreamModule: any;
|
|
10
10
|
|
|
11
11
|
if (Platform.OS === "web") {
|
|
12
|
+
let instance: ExpoAudioStreamWeb | null = null;
|
|
13
|
+
|
|
12
14
|
ExpoAudioStreamModule = (webProps: ExpoAudioStreamWebProps) => {
|
|
13
|
-
|
|
15
|
+
if (!instance) {
|
|
16
|
+
instance = new ExpoAudioStreamWeb(webProps);
|
|
17
|
+
}
|
|
18
|
+
return instance;
|
|
14
19
|
};
|
|
15
20
|
} else {
|
|
16
21
|
ExpoAudioStreamModule = requireNativeModule("ExpoAudioStream");
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// src/WebRecorder.ts
|
|
2
|
-
import { AudioAnalysisData
|
|
2
|
+
import { AudioAnalysisData } from "./AudioAnalysis/AudioAnalysis.types";
|
|
3
|
+
import { RecordingConfig } from "./ExpoAudioStream.types";
|
|
3
4
|
import {
|
|
4
5
|
EmitAudioAnalysisFunction,
|
|
5
6
|
EmitAudioEventFunction,
|
|
6
7
|
} from "./ExpoAudioStream.web";
|
|
7
|
-
import {
|
|
8
|
+
import { getLogger } from "./logger";
|
|
9
|
+
import { encodingToBitDepth } from "./utils/encodingToBitDepth";
|
|
10
|
+
import { InlineFeaturesExtractor } from "./workers/InlineFeaturesExtractor.web";
|
|
11
|
+
import { InlineAudioWebWorker } from "./workers/inlineAudioWebWorker.web";
|
|
12
|
+
|
|
8
13
|
interface AudioWorkletEvent {
|
|
9
14
|
data: {
|
|
10
15
|
command: string;
|
|
@@ -25,12 +30,13 @@ const DEFAULT_WEB_POINTS_PER_SECOND = 10;
|
|
|
25
30
|
const DEFAULT_WEB_INTERVAL = 500;
|
|
26
31
|
const DEFAULT_WEB_NUMBER_OF_CHANNELS = 1;
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
const
|
|
33
|
+
const TAG = "WebRecorder";
|
|
34
|
+
const logger = getLogger(TAG);
|
|
35
|
+
|
|
30
36
|
export class WebRecorder {
|
|
31
37
|
private audioContext: AudioContext;
|
|
32
38
|
private audioWorkletNode!: AudioWorkletNode;
|
|
33
|
-
private featureExtractorWorker
|
|
39
|
+
private featureExtractorWorker?: Worker;
|
|
34
40
|
private source: MediaStreamAudioSourceNode;
|
|
35
41
|
private audioWorkletUrl: string;
|
|
36
42
|
private emitAudioEventCallback: EmitAudioEventFunction;
|
|
@@ -72,7 +78,7 @@ export class WebRecorder {
|
|
|
72
78
|
const audioContextFormat = this.checkAudioContextFormat({
|
|
73
79
|
sampleRate: this.audioContext.sampleRate,
|
|
74
80
|
});
|
|
75
|
-
|
|
81
|
+
logger.debug("Initialized WebRecorder with config:", {
|
|
76
82
|
sampleRate: audioContextFormat.sampleRate,
|
|
77
83
|
bitDepth: audioContextFormat.bitDepth,
|
|
78
84
|
numberOfChannels: audioContextFormat.numberOfChannels,
|
|
@@ -101,26 +107,22 @@ export class WebRecorder {
|
|
|
101
107
|
speakerChanges: [],
|
|
102
108
|
};
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this.featureExtractorWorker = new Worker(
|
|
108
|
-
new URL(featuresExtratorUrl, window.location.href),
|
|
109
|
-
);
|
|
110
|
-
this.featureExtractorWorker.onmessage =
|
|
111
|
-
this.handleFeatureExtractorMessage.bind(this);
|
|
110
|
+
if (recordingConfig.enableProcessing) {
|
|
111
|
+
this.initFeatureExtractorWorker();
|
|
112
|
+
}
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
async init() {
|
|
115
116
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
if (!this.audioWorkletUrl) {
|
|
118
|
+
const blob = new Blob([InlineAudioWebWorker], {
|
|
119
|
+
type: "application/javascript",
|
|
120
|
+
});
|
|
121
|
+
const url = URL.createObjectURL(blob);
|
|
122
|
+
await this.audioContext.audioWorklet.addModule(url);
|
|
123
|
+
} else {
|
|
124
|
+
await this.audioContext.audioWorklet.addModule(this.audioWorkletUrl);
|
|
125
|
+
}
|
|
124
126
|
this.audioWorkletNode = new AudioWorkletNode(
|
|
125
127
|
this.audioContext,
|
|
126
128
|
"recorder-processor",
|
|
@@ -134,7 +136,7 @@ export class WebRecorder {
|
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
136
138
|
// Handle the audio blob (e.g., send it to the server or process it further)
|
|
137
|
-
|
|
139
|
+
logger.debug("Received audio blob from processor", event);
|
|
138
140
|
const pcmBuffer = event.data.recordedData;
|
|
139
141
|
|
|
140
142
|
if (!pcmBuffer) {
|
|
@@ -154,7 +156,7 @@ export class WebRecorder {
|
|
|
154
156
|
const otherDuration =
|
|
155
157
|
pcmBuffer.byteLength /
|
|
156
158
|
(otherSampleRate * (this.exportBitDepth / this.numberOfChannels)); // Calculate duration of the current buffer
|
|
157
|
-
|
|
159
|
+
logger.debug(
|
|
158
160
|
`sampleRate=${sampleRate} Duration: ${duration} -- otherSampleRate=${otherSampleRate} Other duration: ${otherDuration}`,
|
|
159
161
|
);
|
|
160
162
|
|
|
@@ -164,7 +166,7 @@ export class WebRecorder {
|
|
|
164
166
|
});
|
|
165
167
|
this.position += duration; // Update position
|
|
166
168
|
|
|
167
|
-
this.featureExtractorWorker
|
|
169
|
+
this.featureExtractorWorker?.postMessage(
|
|
168
170
|
{
|
|
169
171
|
command: "process",
|
|
170
172
|
channelData,
|
|
@@ -181,7 +183,7 @@ export class WebRecorder {
|
|
|
181
183
|
);
|
|
182
184
|
};
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
logger.debug(
|
|
185
187
|
`WebRecorder initialized -- recordSampleRate=${this.audioContext.sampleRate}`,
|
|
186
188
|
this.config,
|
|
187
189
|
);
|
|
@@ -200,10 +202,60 @@ export class WebRecorder {
|
|
|
200
202
|
this.source.connect(this.audioWorkletNode);
|
|
201
203
|
this.audioWorkletNode.connect(this.audioContext.destination);
|
|
202
204
|
} catch (error) {
|
|
203
|
-
console.error(
|
|
205
|
+
console.error(`[${TAG}] Failed to initialize WebRecorder`, error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
initFeatureExtractorWorker(featuresExtratorUrl?: string) {
|
|
210
|
+
try {
|
|
211
|
+
if (featuresExtratorUrl) {
|
|
212
|
+
// Initialize the feature extractor worker
|
|
213
|
+
//TODO: create audio feature extractor from a Blob instead of url since we cannot include the url directly in the library
|
|
214
|
+
// We keep the url during dev and use the blob in production.
|
|
215
|
+
this.featureExtractorWorker = new Worker(
|
|
216
|
+
new URL(featuresExtratorUrl, window.location.href),
|
|
217
|
+
);
|
|
218
|
+
this.featureExtractorWorker.onmessage =
|
|
219
|
+
this.handleFeatureExtractorMessage.bind(this);
|
|
220
|
+
this.featureExtractorWorker.onerror = this.handleWorkerError.bind(this);
|
|
221
|
+
} else {
|
|
222
|
+
// Fallback to the inline worker if the URL is not provided
|
|
223
|
+
this.initFallbackWorker();
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(
|
|
227
|
+
`[${TAG}] Failed to initialize feature extractor worker`,
|
|
228
|
+
error,
|
|
229
|
+
);
|
|
230
|
+
this.initFallbackWorker();
|
|
204
231
|
}
|
|
205
232
|
}
|
|
206
233
|
|
|
234
|
+
initFallbackWorker() {
|
|
235
|
+
try {
|
|
236
|
+
const blob = new Blob([InlineFeaturesExtractor], {
|
|
237
|
+
type: "application/javascript",
|
|
238
|
+
});
|
|
239
|
+
const url = URL.createObjectURL(blob);
|
|
240
|
+
this.featureExtractorWorker = new Worker(url);
|
|
241
|
+
this.featureExtractorWorker.onmessage =
|
|
242
|
+
this.handleFeatureExtractorMessage.bind(this);
|
|
243
|
+
this.featureExtractorWorker.onerror = (error) => {
|
|
244
|
+
console.error(`[${TAG}] Default Inline worker failed`, error);
|
|
245
|
+
};
|
|
246
|
+
logger.log("Inline worker initialized successfully");
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error(
|
|
249
|
+
`[${TAG}] Failed to initialize Inline Feature Extractor worker`,
|
|
250
|
+
error,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
handleWorkerError(error: ErrorEvent) {
|
|
256
|
+
console.error(`[${TAG}] Feature extractor worker error:`, error);
|
|
257
|
+
}
|
|
258
|
+
|
|
207
259
|
handleFeatureExtractorMessage(event: AudioFeaturesEvent) {
|
|
208
260
|
if (event.data.command === "features") {
|
|
209
261
|
const segmentResult = event.data.result;
|
|
@@ -227,8 +279,8 @@ export class WebRecorder {
|
|
|
227
279
|
};
|
|
228
280
|
}
|
|
229
281
|
// Handle the extracted features (e.g., emit an event or log them)
|
|
230
|
-
|
|
231
|
-
|
|
282
|
+
logger.debug("features event segmentResult", segmentResult);
|
|
283
|
+
logger.debug("features event audioAnalysisData", this.audioAnalysisData);
|
|
232
284
|
this.emitAudioAnalysisCallback(segmentResult);
|
|
233
285
|
}
|
|
234
286
|
}
|
|
@@ -272,10 +324,10 @@ export class WebRecorder {
|
|
|
272
324
|
rawPCMDataFull.byteLength /
|
|
273
325
|
(this.audioContext.sampleRate *
|
|
274
326
|
(this.exportBitDepth / this.numberOfChannels));
|
|
275
|
-
|
|
327
|
+
logger.debug(
|
|
276
328
|
`Received recorded data -- Duration: ${duration} vs ${rawPCMDataFull.byteLength / this.audioContext.sampleRate} seconds`,
|
|
277
329
|
);
|
|
278
|
-
|
|
330
|
+
logger.debug(
|
|
279
331
|
`recordedData.length=${rawPCMDataFull.byteLength} vs transmittedData.length=${this.buffers[0].byteLength}`,
|
|
280
332
|
);
|
|
281
333
|
|
|
@@ -330,9 +382,9 @@ export class WebRecorder {
|
|
|
330
382
|
bufferSource.buffer = audioBuffer;
|
|
331
383
|
bufferSource.connect(this.audioContext.destination);
|
|
332
384
|
bufferSource.start();
|
|
333
|
-
|
|
385
|
+
logger.debug("Playing recorded data", recordedData);
|
|
334
386
|
} catch (error) {
|
|
335
|
-
console.error(`Failed to play recorded data:`, error);
|
|
387
|
+
console.error(`[${TAG}] Failed to play recorded data:`, error);
|
|
336
388
|
}
|
|
337
389
|
}
|
|
338
390
|
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// packages/expo-audio-stream/src/constants.ts
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { BitDepth, SampleRate } from "./ExpoAudioStream.types";
|
|
5
|
+
|
|
6
|
+
export const isWeb = Platform.OS === "web";
|
|
7
|
+
export const DEBUG_NAMESPACE = "expo-audio-stream";
|
|
8
|
+
|
|
9
|
+
// Constants for identifying chunks in a WAV file
|
|
10
|
+
export const RIFF_HEADER = 0x52494646; // "RIFF"
|
|
11
|
+
export const WAVE_HEADER = 0x57415645; // "WAVE"
|
|
12
|
+
export const FMT_CHUNK_ID = 0x666d7420; // "fmt "
|
|
13
|
+
export const DATA_CHUNK_ID = 0x64617461; // "data"
|
|
14
|
+
export const INFO_CHUNK_ID = 0x494e464f; // "INFO"
|
|
15
|
+
|
|
16
|
+
// Default values
|
|
17
|
+
export const DEFAULT_SAMPLE_RATE: SampleRate = 16000;
|
|
18
|
+
export const DEFAULT_BIT_DEPTH: BitDepth = 32;
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// packages/expo-audio-stream/src/events.ts
|
|
2
|
+
|
|
3
|
+
import { EventEmitter, type Subscription } from "expo-modules-core";
|
|
4
|
+
|
|
5
|
+
import { AudioAnalysisData } from "./AudioAnalysis/AudioAnalysis.types";
|
|
6
|
+
import { AudioEventPayload } from "./ExpoAudioStream.types";
|
|
7
|
+
import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
|
|
8
|
+
import { getLogger } from "./logger";
|
|
9
|
+
|
|
10
|
+
const emitter = new EventEmitter(ExpoAudioStreamModule);
|
|
11
|
+
const logger = getLogger("events");
|
|
12
|
+
|
|
13
|
+
export function addAudioEventListener(
|
|
14
|
+
listener: (event: AudioEventPayload) => Promise<void>,
|
|
15
|
+
): Subscription {
|
|
16
|
+
logger.log("Adding listener for AudioData event");
|
|
17
|
+
return emitter.addListener<AudioEventPayload>("AudioData", listener);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function addAudioAnalysisListener(
|
|
21
|
+
listener: (event: AudioAnalysisData) => Promise<void>,
|
|
22
|
+
): Subscription {
|
|
23
|
+
logger.log("Adding listener for AudioAnalysis event");
|
|
24
|
+
return emitter.addListener<AudioAnalysisData>("AudioAnalysis", listener);
|
|
25
|
+
}
|