@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +1 -1
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +68 -22
  4. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +24 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +836 -386
  6. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -2
  7. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +35 -29
  8. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +236 -96
  9. package/android/src/main/java/net/siteed/audiostream/FFT.kt +55 -0
  10. package/android/src/main/java/net/siteed/audiostream/Features.kt +49 -7
  11. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +2 -4
  12. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +55 -47
  13. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  14. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  15. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +60 -13
  16. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
  17. package/build/AudioAnalysis/extractAudioAnalysis.js +147 -162
  18. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  19. package/build/ExpoAudioStream.types.d.ts +47 -3
  20. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  21. package/build/ExpoAudioStream.types.js.map +1 -1
  22. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  23. package/build/ExpoAudioStream.web.js +0 -1
  24. package/build/ExpoAudioStream.web.js.map +1 -1
  25. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  26. package/build/ExpoAudioStreamModule.js +216 -12
  27. package/build/ExpoAudioStreamModule.js.map +1 -1
  28. package/build/WebRecorder.web.d.ts +67 -13
  29. package/build/WebRecorder.web.d.ts.map +1 -1
  30. package/build/WebRecorder.web.js +177 -173
  31. package/build/WebRecorder.web.js.map +1 -1
  32. package/build/index.d.ts +3 -3
  33. package/build/index.d.ts.map +1 -1
  34. package/build/index.js +2 -2
  35. package/build/index.js.map +1 -1
  36. package/build/useAudioRecorder.d.ts.map +1 -1
  37. package/build/useAudioRecorder.js +12 -8
  38. package/build/useAudioRecorder.js.map +1 -1
  39. package/build/utils/audioProcessing.d.ts +24 -0
  40. package/build/utils/audioProcessing.d.ts.map +1 -0
  41. package/build/utils/audioProcessing.js +133 -0
  42. package/build/utils/audioProcessing.js.map +1 -0
  43. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  44. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  45. package/build/workers/InlineFeaturesExtractor.web.js +694 -194
  46. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  47. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  48. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  49. package/build/workers/inlineAudioWebWorker.web.js +3 -2
  50. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  51. package/ios/AudioAnalysisData.swift +51 -16
  52. package/ios/AudioProcessingHelpers.swift +710 -26
  53. package/ios/AudioProcessor.swift +334 -185
  54. package/ios/AudioStreamManager.swift +2 -3
  55. package/ios/DataPoint.swift +25 -12
  56. package/ios/DecodingConfig.swift +47 -0
  57. package/ios/ExpoAudioStreamModule.swift +187 -103
  58. package/ios/FFT.swift +62 -0
  59. package/ios/Features.swift +24 -3
  60. package/ios/RecordingSettings.swift +7 -7
  61. package/package.json +2 -1
  62. package/plugin/build/index.js +6 -1
  63. package/plugin/src/index.ts +9 -1
  64. package/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
  65. package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
  66. package/src/ExpoAudioStream.types.ts +53 -7
  67. package/src/ExpoAudioStream.web.ts +0 -1
  68. package/src/ExpoAudioStreamModule.ts +255 -10
  69. package/src/WebRecorder.web.ts +231 -244
  70. package/src/index.ts +5 -3
  71. package/src/useAudioRecorder.tsx +14 -10
  72. package/src/utils/audioProcessing.ts +205 -0
  73. package/src/workers/InlineFeaturesExtractor.web.tsx +694 -194
  74. package/src/workers/inlineAudioWebWorker.web.tsx +3 -2
@@ -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 DEFAULT_WEB_POINTS_PER_SECOND = 10;
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
- constructor({ audioContext, source, recordingConfig, audioWorkletUrl, emitAudioEventCallback, emitAudioAnalysisCallback, logger, }) {
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
- pointsPerSecond: this.config.pointsPerSecond || DEFAULT_WEB_POINTS_PER_SECOND,
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
- if (!this.audioWorkletUrl) {
90
- const blob = new Blob([InlineAudioWebWorker], {
91
- type: 'application/javascript',
92
- });
93
- const url = URL.createObjectURL(blob);
94
- await this.audioContext.audioWorklet.addModule(url);
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
- pointsPerSecond: this.config.pointsPerSecond ||
125
- DEFAULT_WEB_POINTS_PER_SECOND,
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: this.position * 1000,
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
- initFeatureExtractorWorker(featuresExtratorUrl) {
173
- try {
174
- if (featuresExtratorUrl) {
175
- // Initialize the feature extractor worker
176
- //TODO: create audio feature extractor from a Blob instead of url since we cannot include the url directly in the library
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}] Default Inline worker failed`, error);
192
+ console.error(`[${TAG}] Feature extractor worker error:`, error);
205
193
  };
206
- this.logger?.log('Inline worker initialized successfully');
194
+ this.logger?.log('Feature extractor worker initialized successfully');
207
195
  }
208
196
  catch (error) {
209
- console.error(`[${TAG}] Failed to initialize Inline Feature Extractor worker`, error);
197
+ console.error(`[${TAG}] Failed to initialize feature extractor worker`, error);
210
198
  }
211
199
  }
212
- handleWorkerError(error) {
213
- console.error(`[${TAG}] Feature extractor worker error:`, error);
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
- // Merge the segment result with the full audio analysis data
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.speakerChanges?.push(...(segmentResult.speakerChanges ?? []));
221
- this.audioAnalysisData.durationMs = segmentResult.durationMs;
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
- min: Math.min(this.audioAnalysisData.amplitudeRange.min, segmentResult.amplitudeRange.min),
225
- max: Math.max(this.audioAnalysisData.amplitudeRange.max, segmentResult.amplitudeRange.max),
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
- // Helper method to process recording stop
276
- async processRecordingStop() {
277
- const processStartTime = performance.now();
278
- this.logger?.debug('[Performance] Starting recording stop process');
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
- async playRecordedData({ recordedData, }) {
362
- try {
363
- // Create a WAV blob with proper headers
364
- const wavHeaderBuffer = writeWavHeader({
365
- buffer: recordedData,
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