@siteed/expo-audio-stream 1.17.0 → 2.0.1
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/CHANGELOG.md +26 -1
- package/README.md +1 -1
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +68 -22
- package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +24 -0
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +836 -386
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -2
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +35 -29
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +236 -96
- package/android/src/main/java/net/siteed/audiostream/FFT.kt +55 -0
- package/android/src/main/java/net/siteed/audiostream/Features.kt +49 -7
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +2 -4
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +55 -47
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +60 -13
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
- package/build/AudioAnalysis/extractAudioAnalysis.js +147 -162
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/ExpoAudioStream.types.d.ts +47 -3
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +0 -1
- package/build/ExpoAudioStream.web.js.map +1 -1
- package/build/ExpoAudioStreamModule.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.js +216 -12
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/WebRecorder.web.d.ts +67 -13
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +177 -173
- package/build/WebRecorder.web.js.map +1 -1
- package/build/index.d.ts +3 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/build/useAudioRecorder.d.ts.map +1 -1
- package/build/useAudioRecorder.js +12 -8
- package/build/useAudioRecorder.js.map +1 -1
- package/build/utils/audioProcessing.d.ts +24 -0
- package/build/utils/audioProcessing.d.ts.map +1 -0
- package/build/utils/audioProcessing.js +133 -0
- package/build/utils/audioProcessing.js.map +1 -0
- package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
- package/build/workers/InlineFeaturesExtractor.web.js +694 -194
- package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.js +3 -2
- package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
- package/ios/AudioAnalysisData.swift +51 -16
- package/ios/AudioProcessingHelpers.swift +710 -26
- package/ios/AudioProcessor.swift +334 -185
- package/ios/AudioStreamManager.swift +2 -3
- package/ios/DataPoint.swift +25 -12
- package/ios/DecodingConfig.swift +47 -0
- package/ios/ExpoAudioStreamModule.swift +187 -103
- package/ios/FFT.swift +62 -0
- package/ios/Features.swift +24 -3
- package/ios/RecordingSettings.swift +7 -7
- package/package.json +2 -1
- package/plugin/build/index.js +6 -1
- package/plugin/src/index.ts +9 -1
- package/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
- package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
- package/src/ExpoAudioStream.types.ts +53 -7
- package/src/ExpoAudioStream.web.ts +0 -1
- package/src/ExpoAudioStreamModule.ts +255 -10
- package/src/WebRecorder.web.ts +231 -244
- package/src/index.ts +5 -3
- package/src/useAudioRecorder.tsx +14 -10
- package/src/utils/audioProcessing.ts +205 -0
- package/src/workers/InlineFeaturesExtractor.web.tsx +694 -194
- package/src/workers/inlineAudioWebWorker.web.tsx +3 -2
package/build/WebRecorder.web.js
CHANGED
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
// src/WebRecorder.ts
|
|
2
|
-
import { convertPCMToFloat32 } from './utils/convertPCMToFloat32';
|
|
1
|
+
// packages/expo-audio-stream/src/WebRecorder.web.ts
|
|
3
2
|
import { encodingToBitDepth } from './utils/encodingToBitDepth';
|
|
4
|
-
import { writeWavHeader } from './utils/writeWavHeader';
|
|
5
3
|
import { InlineFeaturesExtractor } from './workers/InlineFeaturesExtractor.web';
|
|
6
4
|
import { InlineAudioWebWorker } from './workers/inlineAudioWebWorker.web';
|
|
7
5
|
const DEFAULT_WEB_BITDEPTH = 32;
|
|
8
|
-
const
|
|
6
|
+
const DEFAULT_SEGMENT_DURATION_MS = 100;
|
|
9
7
|
const DEFAULT_WEB_INTERVAL = 500;
|
|
10
8
|
const DEFAULT_WEB_NUMBER_OF_CHANNELS = 1;
|
|
11
|
-
const DEFAULT_ALGORITHM = 'rms';
|
|
12
9
|
const TAG = 'WebRecorder';
|
|
13
|
-
const STOP_PERFORMANCE_MARKS = {
|
|
14
|
-
STOP_INITIATED: 'stopInitiated',
|
|
15
|
-
COMPRESSED_RECORDING_STOP: 'compressedRecordingStop',
|
|
16
|
-
AUDIO_WORKLET_STOP: 'audioWorkletStop',
|
|
17
|
-
CLEANUP: 'cleanup',
|
|
18
|
-
TOTAL_STOP_TIME: 'totalStopTime',
|
|
19
|
-
};
|
|
20
10
|
export class WebRecorder {
|
|
21
11
|
audioContext;
|
|
22
12
|
audioWorkletNode;
|
|
23
13
|
featureExtractorWorker;
|
|
24
14
|
source;
|
|
25
|
-
audioWorkletUrl;
|
|
26
15
|
emitAudioEventCallback;
|
|
27
16
|
emitAudioAnalysisCallback;
|
|
28
17
|
config;
|
|
@@ -38,10 +27,19 @@ export class WebRecorder {
|
|
|
38
27
|
compressedSize = 0;
|
|
39
28
|
pendingCompressedChunk = null;
|
|
40
29
|
wavMimeType = 'audio/wav';
|
|
41
|
-
|
|
30
|
+
dataPointIdCounter = 0; // Add this property to track the counter
|
|
31
|
+
/**
|
|
32
|
+
* Initializes a new WebRecorder instance for audio recording and processing
|
|
33
|
+
* @param audioContext - The AudioContext to use for recording
|
|
34
|
+
* @param source - The MediaStreamAudioSourceNode providing the audio input
|
|
35
|
+
* @param recordingConfig - Configuration options for the recording
|
|
36
|
+
* @param emitAudioEventCallback - Callback function for audio data events
|
|
37
|
+
* @param emitAudioAnalysisCallback - Callback function for audio analysis events
|
|
38
|
+
* @param logger - Optional logger for debugging information
|
|
39
|
+
*/
|
|
40
|
+
constructor({ audioContext, source, recordingConfig, emitAudioEventCallback, emitAudioAnalysisCallback, logger, }) {
|
|
42
41
|
this.audioContext = audioContext;
|
|
43
42
|
this.source = source;
|
|
44
|
-
this.audioWorkletUrl = audioWorkletUrl;
|
|
45
43
|
this.emitAudioEventCallback = emitAudioEventCallback;
|
|
46
44
|
this.emitAudioAnalysisCallback = emitAudioAnalysisCallback;
|
|
47
45
|
this.config = recordingConfig;
|
|
@@ -66,15 +64,14 @@ export class WebRecorder {
|
|
|
66
64
|
DEFAULT_WEB_BITDEPTH;
|
|
67
65
|
this.audioAnalysisData = {
|
|
68
66
|
amplitudeRange: { min: 0, max: 0 },
|
|
67
|
+
rmsRange: { min: 0, max: 0 },
|
|
69
68
|
dataPoints: [],
|
|
70
69
|
durationMs: 0,
|
|
71
70
|
samples: 0,
|
|
72
|
-
amplitudeAlgorithm: recordingConfig.algorithm || DEFAULT_ALGORITHM,
|
|
73
71
|
bitDepth: this.bitDepth,
|
|
74
72
|
numberOfChannels: this.numberOfChannels,
|
|
75
73
|
sampleRate: this.config.sampleRate || this.audioContext.sampleRate,
|
|
76
|
-
|
|
77
|
-
speakerChanges: [],
|
|
74
|
+
segmentDurationMs: this.config.segmentDurationMs ?? DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms segments
|
|
78
75
|
};
|
|
79
76
|
if (recordingConfig.enableProcessing) {
|
|
80
77
|
this.initFeatureExtractorWorker();
|
|
@@ -84,18 +81,18 @@ export class WebRecorder {
|
|
|
84
81
|
this.initializeCompressedRecorder();
|
|
85
82
|
}
|
|
86
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Initializes the audio worklet using an inline script
|
|
86
|
+
* Creates and connects the audio processing pipeline
|
|
87
|
+
*/
|
|
87
88
|
async init() {
|
|
88
89
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
await this.audioContext.audioWorklet.addModule(this.audioWorkletUrl);
|
|
98
|
-
}
|
|
90
|
+
// Create and use inline audio worklet
|
|
91
|
+
const blob = new Blob([InlineAudioWebWorker], {
|
|
92
|
+
type: 'application/javascript',
|
|
93
|
+
});
|
|
94
|
+
const url = URL.createObjectURL(blob);
|
|
95
|
+
await this.audioContext.audioWorklet.addModule(url);
|
|
99
96
|
this.audioWorkletNode = new AudioWorkletNode(this.audioContext, 'recorder-processor');
|
|
100
97
|
this.audioWorkletNode.port.onmessage = async (event) => {
|
|
101
98
|
const command = event.data.command;
|
|
@@ -110,10 +107,16 @@ export class WebRecorder {
|
|
|
110
107
|
const chunkSize = this.audioContext.sampleRate * 2; // Reduce to 2 seconds chunks
|
|
111
108
|
const sampleRate = event.data.sampleRate ?? this.audioContext.sampleRate;
|
|
112
109
|
const duration = pcmBufferFloat.length / sampleRate;
|
|
110
|
+
// Calculate bytes per sample based on bit depth
|
|
111
|
+
const bytesPerSample = this.bitDepth / 8;
|
|
113
112
|
// Emit chunks without storing them
|
|
114
113
|
for (let i = 0; i < pcmBufferFloat.length; i += chunkSize) {
|
|
115
114
|
const chunk = pcmBufferFloat.slice(i, i + chunkSize);
|
|
116
115
|
const chunkPosition = this.position + i / sampleRate;
|
|
116
|
+
// Calculate byte positions and samples
|
|
117
|
+
const startPosition = Math.floor(i * bytesPerSample);
|
|
118
|
+
const endPosition = Math.floor((i + chunk.length) * bytesPerSample);
|
|
119
|
+
const samples = chunk.length; // Number of samples in this chunk
|
|
117
120
|
// Process features if enabled
|
|
118
121
|
if (this.config.enableProcessing &&
|
|
119
122
|
this.featureExtractorWorker) {
|
|
@@ -121,15 +124,17 @@ export class WebRecorder {
|
|
|
121
124
|
command: 'process',
|
|
122
125
|
channelData: chunk,
|
|
123
126
|
sampleRate,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
algorithm: this.config.algorithm || 'rms',
|
|
127
|
+
segmentDurationMs: this.config.segmentDurationMs ??
|
|
128
|
+
DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms
|
|
127
129
|
bitDepth: this.bitDepth,
|
|
128
|
-
fullAudioDurationMs:
|
|
130
|
+
fullAudioDurationMs: chunkPosition * 1000,
|
|
129
131
|
numberOfChannels: this.numberOfChannels,
|
|
130
132
|
features: this.config.features,
|
|
131
133
|
intervalAnalysis: this.config.intervalAnalysis,
|
|
132
|
-
|
|
134
|
+
startPosition,
|
|
135
|
+
endPosition,
|
|
136
|
+
samples,
|
|
137
|
+
});
|
|
133
138
|
}
|
|
134
139
|
// Emit chunk immediately
|
|
135
140
|
this.emitAudioEventCallback({
|
|
@@ -160,6 +165,7 @@ export class WebRecorder {
|
|
|
160
165
|
exportBitDepth: this.exportBitDepth,
|
|
161
166
|
channels: this.numberOfChannels,
|
|
162
167
|
interval: this.config.interval ?? DEFAULT_WEB_INTERVAL,
|
|
168
|
+
// enableLogging: !!this.logger,
|
|
163
169
|
});
|
|
164
170
|
// Connect the source to the AudioWorkletNode and start recording
|
|
165
171
|
this.source.connect(this.audioWorkletNode);
|
|
@@ -169,29 +175,11 @@ export class WebRecorder {
|
|
|
169
175
|
console.error(`[${TAG}] Failed to initialize WebRecorder`, error);
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// We keep the url during dev and use the blob in production.
|
|
178
|
-
this.featureExtractorWorker = new Worker(new URL(featuresExtratorUrl, window.location.href));
|
|
179
|
-
this.featureExtractorWorker.onmessage =
|
|
180
|
-
this.handleFeatureExtractorMessage.bind(this);
|
|
181
|
-
this.featureExtractorWorker.onerror =
|
|
182
|
-
this.handleWorkerError.bind(this);
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
// Fallback to the inline worker if the URL is not provided
|
|
186
|
-
this.initFallbackWorker();
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
console.error(`[${TAG}] Failed to initialize feature extractor worker`, error);
|
|
191
|
-
this.initFallbackWorker();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
initFallbackWorker() {
|
|
178
|
+
/**
|
|
179
|
+
* Initializes the feature extractor worker for audio analysis
|
|
180
|
+
* Creates an inline worker from a blob for audio feature extraction
|
|
181
|
+
*/
|
|
182
|
+
initFeatureExtractorWorker() {
|
|
195
183
|
try {
|
|
196
184
|
const blob = new Blob([InlineFeaturesExtractor], {
|
|
197
185
|
type: 'application/javascript',
|
|
@@ -201,44 +189,115 @@ export class WebRecorder {
|
|
|
201
189
|
this.featureExtractorWorker.onmessage =
|
|
202
190
|
this.handleFeatureExtractorMessage.bind(this);
|
|
203
191
|
this.featureExtractorWorker.onerror = (error) => {
|
|
204
|
-
console.error(`[${TAG}]
|
|
192
|
+
console.error(`[${TAG}] Feature extractor worker error:`, error);
|
|
205
193
|
};
|
|
206
|
-
this.logger?.log('
|
|
194
|
+
this.logger?.log('Feature extractor worker initialized successfully');
|
|
207
195
|
}
|
|
208
196
|
catch (error) {
|
|
209
|
-
console.error(`[${TAG}] Failed to initialize
|
|
197
|
+
console.error(`[${TAG}] Failed to initialize feature extractor worker`, error);
|
|
210
198
|
}
|
|
211
199
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Processes audio analysis results from the feature extractor worker
|
|
202
|
+
* Updates the audio analysis data and emits events
|
|
203
|
+
* @param event - The event containing audio analysis results
|
|
204
|
+
*/
|
|
215
205
|
handleFeatureExtractorMessage(event) {
|
|
216
206
|
if (event.data.command === 'features') {
|
|
217
207
|
const segmentResult = event.data.result;
|
|
218
|
-
//
|
|
208
|
+
// Update the dataPointIdCounter based on the last ID received
|
|
209
|
+
if (segmentResult.dataPoints &&
|
|
210
|
+
segmentResult.dataPoints.length > 0) {
|
|
211
|
+
const lastDataPoint = segmentResult.dataPoints[segmentResult.dataPoints.length - 1];
|
|
212
|
+
if (lastDataPoint && typeof lastDataPoint.id === 'number') {
|
|
213
|
+
this.dataPointIdCounter = Math.max(this.dataPointIdCounter, lastDataPoint.id + 1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
console.debug('[WebRecorder] Raw segment result:', {
|
|
217
|
+
dataPointsLength: segmentResult.dataPoints.length,
|
|
218
|
+
durationMs: segmentResult.durationMs,
|
|
219
|
+
sampleRate: segmentResult.sampleRate,
|
|
220
|
+
amplitudeRange: segmentResult.amplitudeRange,
|
|
221
|
+
});
|
|
222
|
+
// Ensure consistent sample rate in the result
|
|
223
|
+
segmentResult.sampleRate =
|
|
224
|
+
this.config.sampleRate || this.audioContext.sampleRate;
|
|
225
|
+
// Update the full audio analysis data with proper range merging
|
|
219
226
|
this.audioAnalysisData.dataPoints.push(...segmentResult.dataPoints);
|
|
220
|
-
this.audioAnalysisData.
|
|
221
|
-
|
|
227
|
+
this.audioAnalysisData.durationMs += segmentResult.durationMs;
|
|
228
|
+
// Make sure the sample rate is consistent
|
|
229
|
+
this.audioAnalysisData.sampleRate = segmentResult.sampleRate;
|
|
230
|
+
// Properly merge amplitude ranges
|
|
222
231
|
if (segmentResult.amplitudeRange) {
|
|
223
|
-
this.audioAnalysisData.amplitudeRange
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
if (!this.audioAnalysisData.amplitudeRange) {
|
|
233
|
+
this.audioAnalysisData.amplitudeRange = {
|
|
234
|
+
...segmentResult.amplitudeRange,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
this.audioAnalysisData.amplitudeRange = {
|
|
239
|
+
min: Math.min(this.audioAnalysisData.amplitudeRange.min, segmentResult.amplitudeRange.min),
|
|
240
|
+
max: Math.max(this.audioAnalysisData.amplitudeRange.max, segmentResult.amplitudeRange.max),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Properly merge RMS ranges
|
|
245
|
+
if (segmentResult.rmsRange) {
|
|
246
|
+
if (!this.audioAnalysisData.rmsRange) {
|
|
247
|
+
this.audioAnalysisData.rmsRange = {
|
|
248
|
+
...segmentResult.rmsRange,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
this.audioAnalysisData.rmsRange = {
|
|
253
|
+
min: Math.min(this.audioAnalysisData.rmsRange.min, segmentResult.rmsRange.min),
|
|
254
|
+
max: Math.max(this.audioAnalysisData.rmsRange.max, segmentResult.rmsRange.max),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
227
257
|
}
|
|
228
|
-
// Handle the extracted features (e.g., emit an event or log them)
|
|
229
258
|
this.logger?.debug('features event segmentResult', segmentResult);
|
|
230
259
|
this.logger?.debug(`features event audioAnalysisData duration=${this.audioAnalysisData.durationMs}`, this.audioAnalysisData);
|
|
231
260
|
this.emitAudioAnalysisCallback(segmentResult);
|
|
261
|
+
console.debug('[WebRecorder] Updated audioAnalysisData:', {
|
|
262
|
+
dataPointsLength: this.audioAnalysisData.dataPoints.length,
|
|
263
|
+
durationMs: this.audioAnalysisData.durationMs,
|
|
264
|
+
sampleRate: this.audioAnalysisData.sampleRate,
|
|
265
|
+
amplitudeRange: this.audioAnalysisData.amplitudeRange,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Resets the data point ID counter
|
|
271
|
+
* Used when starting a new recording
|
|
272
|
+
*/
|
|
273
|
+
resetDataPointCounter() {
|
|
274
|
+
this.dataPointIdCounter = 0;
|
|
275
|
+
// Reset the counter in the worker
|
|
276
|
+
if (this.featureExtractorWorker) {
|
|
277
|
+
this.featureExtractorWorker.postMessage({
|
|
278
|
+
command: 'resetCounter',
|
|
279
|
+
startCounterFrom: 0,
|
|
280
|
+
});
|
|
232
281
|
}
|
|
233
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Starts the audio recording process
|
|
285
|
+
* Connects the audio nodes and begins capturing audio data
|
|
286
|
+
*/
|
|
234
287
|
start() {
|
|
235
288
|
this.source.connect(this.audioWorkletNode);
|
|
236
289
|
this.audioWorkletNode.connect(this.audioContext.destination);
|
|
237
290
|
this.packetCount = 0;
|
|
291
|
+
// Reset the counter when starting a new recording
|
|
292
|
+
this.resetDataPointCounter();
|
|
238
293
|
if (this.compressedMediaRecorder) {
|
|
239
294
|
this.compressedMediaRecorder.start(this.config.interval ?? 1000);
|
|
240
295
|
}
|
|
241
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Stops the audio recording process and returns the recorded data
|
|
299
|
+
* @returns Promise resolving to an object containing PCM data and optional compressed blob
|
|
300
|
+
*/
|
|
242
301
|
async stop() {
|
|
243
302
|
try {
|
|
244
303
|
if (this.compressedMediaRecorder) {
|
|
@@ -260,6 +319,10 @@ export class WebRecorder {
|
|
|
260
319
|
this.pendingCompressedChunk = null;
|
|
261
320
|
}
|
|
262
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Cleans up resources when recording is stopped
|
|
324
|
+
* Closes audio context and disconnects nodes
|
|
325
|
+
*/
|
|
263
326
|
cleanup() {
|
|
264
327
|
if (this.audioContext) {
|
|
265
328
|
this.audioContext.close();
|
|
@@ -272,120 +335,30 @@ export class WebRecorder {
|
|
|
272
335
|
}
|
|
273
336
|
this.stopMediaStreamTracks();
|
|
274
337
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const [compressedData, workletData] = await Promise.all([
|
|
280
|
-
this.stopCompressedRecording(),
|
|
281
|
-
this.stopAudioWorklet(),
|
|
282
|
-
]);
|
|
283
|
-
this.logger?.debug(`[Performance] Recording stop process completed in ${performance.now() - processStartTime}ms`);
|
|
284
|
-
return {
|
|
285
|
-
pcmData: workletData ??
|
|
286
|
-
new Float32Array(this.audioAnalysisData.dataPoints.length),
|
|
287
|
-
compressedBlob: compressedData,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
// Helper method to stop compressed recording
|
|
291
|
-
stopCompressedRecording() {
|
|
292
|
-
const startTime = performance.now();
|
|
293
|
-
this.logger?.debug(`[Performance][${STOP_PERFORMANCE_MARKS.COMPRESSED_RECORDING_STOP}] Starting compressed recording stop`);
|
|
294
|
-
if (!this.compressedMediaRecorder) {
|
|
295
|
-
this.logger?.debug('[Performance] No compressed recorder to stop');
|
|
296
|
-
return Promise.resolve(undefined);
|
|
297
|
-
}
|
|
298
|
-
return new Promise((resolve) => {
|
|
299
|
-
this.compressedMediaRecorder.onstop = () => {
|
|
300
|
-
const blob = new Blob(this.compressedChunks, {
|
|
301
|
-
type: 'audio/webm;codecs=opus',
|
|
302
|
-
});
|
|
303
|
-
this.logger?.debug(`[Performance][${STOP_PERFORMANCE_MARKS.COMPRESSED_RECORDING_STOP}] Compressed recording stopped in ${performance.now() - startTime}ms, size: ${blob.size}`);
|
|
304
|
-
resolve(blob);
|
|
305
|
-
};
|
|
306
|
-
this.compressedMediaRecorder.stop();
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
// Helper method to stop audio worklet
|
|
310
|
-
stopAudioWorklet() {
|
|
311
|
-
const startTime = performance.now();
|
|
312
|
-
this.logger?.debug(`[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Starting audio worklet stop`);
|
|
313
|
-
if (!this.audioWorkletNode) {
|
|
314
|
-
this.logger?.debug('[Performance] No audio worklet to stop');
|
|
315
|
-
return Promise.resolve(undefined);
|
|
316
|
-
}
|
|
317
|
-
return new Promise((resolve) => {
|
|
318
|
-
const onMessage = (event) => {
|
|
319
|
-
if (event.data.command === 'recordedData') {
|
|
320
|
-
this.audioWorkletNode?.port.removeEventListener('message', onMessage);
|
|
321
|
-
const rawPCMDataFull = event.data.recordedData?.slice(0);
|
|
322
|
-
if (!rawPCMDataFull) {
|
|
323
|
-
this.logger?.debug('[Performance] No PCM data received');
|
|
324
|
-
resolve(undefined);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (this.exportBitDepth !== this.bitDepth) {
|
|
328
|
-
const conversionStart = performance.now();
|
|
329
|
-
convertPCMToFloat32({
|
|
330
|
-
buffer: rawPCMDataFull.buffer,
|
|
331
|
-
bitDepth: this.exportBitDepth,
|
|
332
|
-
skipWavHeader: true,
|
|
333
|
-
logger: this.logger,
|
|
334
|
-
}).then(({ pcmValues }) => {
|
|
335
|
-
this.logger?.debug(`[Performance] PCM conversion completed in ${performance.now() - conversionStart}ms`);
|
|
336
|
-
this.logger?.debug(`[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Audio worklet stopped in ${performance.now() - startTime}ms`);
|
|
337
|
-
resolve(pcmValues);
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
this.logger?.debug(`[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Audio worklet stopped in ${performance.now() - startTime}ms`);
|
|
342
|
-
resolve(rawPCMDataFull);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
this.audioWorkletNode.port.addEventListener('message', onMessage);
|
|
347
|
-
this.audioWorkletNode.port.postMessage({ command: 'stop' });
|
|
348
|
-
});
|
|
349
|
-
}
|
|
338
|
+
/**
|
|
339
|
+
* Pauses the audio recording process
|
|
340
|
+
* Disconnects audio nodes and pauses the media recorder
|
|
341
|
+
*/
|
|
350
342
|
pause() {
|
|
351
343
|
this.source.disconnect(this.audioWorkletNode); // Disconnect the source from the AudioWorkletNode
|
|
352
344
|
this.audioWorkletNode.disconnect(this.audioContext.destination); // Disconnect the AudioWorkletNode from the destination
|
|
353
345
|
this.audioWorkletNode.port.postMessage({ command: 'pause' });
|
|
354
346
|
this.compressedMediaRecorder?.pause();
|
|
355
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Stops all media stream tracks to release hardware resources
|
|
350
|
+
* Ensures recording indicators (like microphone icon) are turned off
|
|
351
|
+
*/
|
|
356
352
|
stopMediaStreamTracks() {
|
|
357
353
|
// Stop all audio tracks to stop the recording icon
|
|
358
354
|
const tracks = this.source.mediaStream.getTracks();
|
|
359
355
|
tracks.forEach((track) => track.stop());
|
|
360
356
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
sampleRate: this.audioContext.sampleRate,
|
|
367
|
-
numChannels: this.numberOfChannels,
|
|
368
|
-
bitDepth: this.exportBitDepth,
|
|
369
|
-
});
|
|
370
|
-
const blob = new Blob([wavHeaderBuffer], { type: 'audio/wav' });
|
|
371
|
-
const url = URL.createObjectURL(blob);
|
|
372
|
-
const response = await fetch(url);
|
|
373
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
374
|
-
// Decode the audio data
|
|
375
|
-
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
376
|
-
// Create a buffer source node and play the audio
|
|
377
|
-
const bufferSource = this.audioContext.createBufferSource();
|
|
378
|
-
bufferSource.buffer = audioBuffer;
|
|
379
|
-
bufferSource.connect(this.audioContext.destination);
|
|
380
|
-
bufferSource.start();
|
|
381
|
-
this.logger?.debug('Playing recorded data', recordedData);
|
|
382
|
-
// Clean up
|
|
383
|
-
URL.revokeObjectURL(url);
|
|
384
|
-
}
|
|
385
|
-
catch (error) {
|
|
386
|
-
console.error(`[${TAG}] Failed to play recorded data:`, error);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
357
|
+
/**
|
|
358
|
+
* Determines the audio format capabilities of the current audio context
|
|
359
|
+
* @param sampleRate - The sample rate to check
|
|
360
|
+
* @returns Object containing format information (sample rate, bit depth, channels)
|
|
361
|
+
*/
|
|
389
362
|
checkAudioContextFormat({ sampleRate }) {
|
|
390
363
|
// Create a silent AudioBuffer
|
|
391
364
|
const frameCount = sampleRate * 1.0; // 1 second buffer
|
|
@@ -399,12 +372,20 @@ export class WebRecorder {
|
|
|
399
372
|
numberOfChannels: audioBuffer.numberOfChannels,
|
|
400
373
|
};
|
|
401
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Resumes a paused recording
|
|
377
|
+
* Reconnects audio nodes and resumes the media recorder
|
|
378
|
+
*/
|
|
402
379
|
resume() {
|
|
403
380
|
this.source.connect(this.audioWorkletNode);
|
|
404
381
|
this.audioWorkletNode.connect(this.audioContext.destination);
|
|
405
382
|
this.audioWorkletNode.port.postMessage({ command: 'resume' });
|
|
406
383
|
this.compressedMediaRecorder?.resume();
|
|
407
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Initializes the compressed media recorder if compression is enabled
|
|
387
|
+
* Sets up event handlers for compressed audio data
|
|
388
|
+
*/
|
|
408
389
|
initializeCompressedRecorder() {
|
|
409
390
|
try {
|
|
410
391
|
const mimeType = 'audio/webm;codecs=opus';
|
|
@@ -428,5 +409,28 @@ export class WebRecorder {
|
|
|
428
409
|
this.logger?.error('Failed to initialize compressed recorder:', error);
|
|
429
410
|
}
|
|
430
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Processes features if enabled
|
|
414
|
+
*/
|
|
415
|
+
processFeatures(chunk, sampleRate, chunkPosition, startPosition, endPosition, samples) {
|
|
416
|
+
if (this.config.enableProcessing && this.featureExtractorWorker) {
|
|
417
|
+
this.featureExtractorWorker.postMessage({
|
|
418
|
+
command: 'process',
|
|
419
|
+
channelData: chunk,
|
|
420
|
+
sampleRate,
|
|
421
|
+
segmentDurationMs: this.config.segmentDurationMs ??
|
|
422
|
+
DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms
|
|
423
|
+
bitDepth: this.bitDepth,
|
|
424
|
+
fullAudioDurationMs: chunkPosition * 1000,
|
|
425
|
+
numberOfChannels: this.numberOfChannels,
|
|
426
|
+
features: this.config.features,
|
|
427
|
+
intervalAnalysis: this.config.intervalAnalysis,
|
|
428
|
+
startPosition,
|
|
429
|
+
endPosition,
|
|
430
|
+
samples,
|
|
431
|
+
startCounterFrom: this.dataPointIdCounter, // Pass the current counter value
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
431
435
|
}
|
|
432
436
|
//# sourceMappingURL=WebRecorder.web.js.map
|