@siteed/expo-audio-stream 1.17.0 → 2.0.0

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 (72) hide show
  1. package/CHANGELOG.md +21 -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/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
  63. package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
  64. package/src/ExpoAudioStream.types.ts +53 -7
  65. package/src/ExpoAudioStream.web.ts +0 -1
  66. package/src/ExpoAudioStreamModule.ts +255 -10
  67. package/src/WebRecorder.web.ts +231 -244
  68. package/src/index.ts +5 -3
  69. package/src/useAudioRecorder.tsx +14 -10
  70. package/src/utils/audioProcessing.ts +205 -0
  71. package/src/workers/InlineFeaturesExtractor.web.tsx +694 -194
  72. package/src/workers/inlineAudioWebWorker.web.tsx +3 -2
@@ -1,4 +1,4 @@
1
- // src/WebRecorder.ts
1
+ // packages/expo-audio-stream/src/WebRecorder.web.ts
2
2
 
3
3
  import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
4
4
  import { ConsoleLike, RecordingConfig } from './ExpoAudioStream.types'
@@ -6,9 +6,7 @@ import {
6
6
  EmitAudioAnalysisFunction,
7
7
  EmitAudioEventFunction,
8
8
  } from './ExpoAudioStream.web'
9
- import { convertPCMToFloat32 } from './utils/convertPCMToFloat32'
10
9
  import { encodingToBitDepth } from './utils/encodingToBitDepth'
11
- import { writeWavHeader } from './utils/writeWavHeader'
12
10
  import { InlineFeaturesExtractor } from './workers/InlineFeaturesExtractor.web'
13
11
  import { InlineAudioWebWorker } from './workers/inlineAudioWebWorker.web'
14
12
 
@@ -28,27 +26,17 @@ interface AudioFeaturesEvent {
28
26
  }
29
27
 
30
28
  const DEFAULT_WEB_BITDEPTH = 32
31
- const DEFAULT_WEB_POINTS_PER_SECOND = 10
29
+ const DEFAULT_SEGMENT_DURATION_MS = 100
32
30
  const DEFAULT_WEB_INTERVAL = 500
33
31
  const DEFAULT_WEB_NUMBER_OF_CHANNELS = 1
34
- const DEFAULT_ALGORITHM = 'rms'
35
32
 
36
33
  const TAG = 'WebRecorder'
37
34
 
38
- const STOP_PERFORMANCE_MARKS = {
39
- STOP_INITIATED: 'stopInitiated',
40
- COMPRESSED_RECORDING_STOP: 'compressedRecordingStop',
41
- AUDIO_WORKLET_STOP: 'audioWorkletStop',
42
- CLEANUP: 'cleanup',
43
- TOTAL_STOP_TIME: 'totalStopTime',
44
- } as const
45
-
46
35
  export class WebRecorder {
47
36
  private audioContext: AudioContext
48
37
  private audioWorkletNode!: AudioWorkletNode
49
38
  private featureExtractorWorker?: Worker
50
39
  private source: MediaStreamAudioSourceNode
51
- private audioWorkletUrl: string
52
40
  private emitAudioEventCallback: EmitAudioEventFunction
53
41
  private emitAudioAnalysisCallback: EmitAudioAnalysisFunction
54
42
  private config: RecordingConfig
@@ -64,12 +52,21 @@ export class WebRecorder {
64
52
  private compressedSize: number = 0
65
53
  private pendingCompressedChunk: Blob | null = null
66
54
  private readonly wavMimeType = 'audio/wav'
67
-
55
+ private dataPointIdCounter: number = 0 // Add this property to track the counter
56
+
57
+ /**
58
+ * Initializes a new WebRecorder instance for audio recording and processing
59
+ * @param audioContext - The AudioContext to use for recording
60
+ * @param source - The MediaStreamAudioSourceNode providing the audio input
61
+ * @param recordingConfig - Configuration options for the recording
62
+ * @param emitAudioEventCallback - Callback function for audio data events
63
+ * @param emitAudioAnalysisCallback - Callback function for audio analysis events
64
+ * @param logger - Optional logger for debugging information
65
+ */
68
66
  constructor({
69
67
  audioContext,
70
68
  source,
71
69
  recordingConfig,
72
- audioWorkletUrl,
73
70
  emitAudioEventCallback,
74
71
  emitAudioAnalysisCallback,
75
72
  logger,
@@ -77,14 +74,12 @@ export class WebRecorder {
77
74
  audioContext: AudioContext
78
75
  source: MediaStreamAudioSourceNode
79
76
  recordingConfig: RecordingConfig
80
- audioWorkletUrl: string
81
77
  emitAudioEventCallback: EmitAudioEventFunction
82
78
  emitAudioAnalysisCallback: EmitAudioAnalysisFunction
83
79
  logger?: ConsoleLike
84
80
  }) {
85
81
  this.audioContext = audioContext
86
82
  this.source = source
87
- this.audioWorkletUrl = audioWorkletUrl
88
83
  this.emitAudioEventCallback = emitAudioEventCallback
89
84
  this.emitAudioAnalysisCallback = emitAudioAnalysisCallback
90
85
  this.config = recordingConfig
@@ -112,16 +107,15 @@ export class WebRecorder {
112
107
 
113
108
  this.audioAnalysisData = {
114
109
  amplitudeRange: { min: 0, max: 0 },
110
+ rmsRange: { min: 0, max: 0 },
115
111
  dataPoints: [],
116
112
  durationMs: 0,
117
113
  samples: 0,
118
- amplitudeAlgorithm: recordingConfig.algorithm || DEFAULT_ALGORITHM,
119
114
  bitDepth: this.bitDepth,
120
115
  numberOfChannels: this.numberOfChannels,
121
116
  sampleRate: this.config.sampleRate || this.audioContext.sampleRate,
122
- pointsPerSecond:
123
- this.config.pointsPerSecond || DEFAULT_WEB_POINTS_PER_SECOND,
124
- speakerChanges: [],
117
+ segmentDurationMs:
118
+ this.config.segmentDurationMs ?? DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms segments
125
119
  }
126
120
 
127
121
  if (recordingConfig.enableProcessing) {
@@ -134,19 +128,19 @@ export class WebRecorder {
134
128
  }
135
129
  }
136
130
 
131
+ /**
132
+ * Initializes the audio worklet using an inline script
133
+ * Creates and connects the audio processing pipeline
134
+ */
137
135
  async init() {
138
136
  try {
139
- if (!this.audioWorkletUrl) {
140
- const blob = new Blob([InlineAudioWebWorker], {
141
- type: 'application/javascript',
142
- })
143
- const url = URL.createObjectURL(blob)
144
- await this.audioContext.audioWorklet.addModule(url)
145
- } else {
146
- await this.audioContext.audioWorklet.addModule(
147
- this.audioWorkletUrl
148
- )
149
- }
137
+ // Create and use inline audio worklet
138
+ const blob = new Blob([InlineAudioWebWorker], {
139
+ type: 'application/javascript',
140
+ })
141
+ const url = URL.createObjectURL(blob)
142
+ await this.audioContext.audioWorklet.addModule(url)
143
+
150
144
  this.audioWorkletNode = new AudioWorkletNode(
151
145
  this.audioContext,
152
146
  'recorder-processor'
@@ -170,33 +164,42 @@ export class WebRecorder {
170
164
  event.data.sampleRate ?? this.audioContext.sampleRate
171
165
  const duration = pcmBufferFloat.length / sampleRate
172
166
 
167
+ // Calculate bytes per sample based on bit depth
168
+ const bytesPerSample = this.bitDepth / 8
169
+
173
170
  // Emit chunks without storing them
174
171
  for (let i = 0; i < pcmBufferFloat.length; i += chunkSize) {
175
172
  const chunk = pcmBufferFloat.slice(i, i + chunkSize)
176
173
  const chunkPosition = this.position + i / sampleRate
177
174
 
175
+ // Calculate byte positions and samples
176
+ const startPosition = Math.floor(i * bytesPerSample)
177
+ const endPosition = Math.floor(
178
+ (i + chunk.length) * bytesPerSample
179
+ )
180
+ const samples = chunk.length // Number of samples in this chunk
181
+
178
182
  // Process features if enabled
179
183
  if (
180
184
  this.config.enableProcessing &&
181
185
  this.featureExtractorWorker
182
186
  ) {
183
- this.featureExtractorWorker.postMessage(
184
- {
185
- command: 'process',
186
- channelData: chunk,
187
- sampleRate,
188
- pointsPerSecond:
189
- this.config.pointsPerSecond ||
190
- DEFAULT_WEB_POINTS_PER_SECOND,
191
- algorithm: this.config.algorithm || 'rms',
192
- bitDepth: this.bitDepth,
193
- fullAudioDurationMs: this.position * 1000,
194
- numberOfChannels: this.numberOfChannels,
195
- features: this.config.features,
196
- intervalAnalysis: this.config.intervalAnalysis,
197
- },
198
- []
199
- )
187
+ this.featureExtractorWorker.postMessage({
188
+ command: 'process',
189
+ channelData: chunk,
190
+ sampleRate,
191
+ segmentDurationMs:
192
+ this.config.segmentDurationMs ??
193
+ DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms
194
+ bitDepth: this.bitDepth,
195
+ fullAudioDurationMs: chunkPosition * 1000,
196
+ numberOfChannels: this.numberOfChannels,
197
+ features: this.config.features,
198
+ intervalAnalysis: this.config.intervalAnalysis,
199
+ startPosition,
200
+ endPosition,
201
+ samples,
202
+ })
200
203
  }
201
204
 
202
205
  // Emit chunk immediately
@@ -235,6 +238,7 @@ export class WebRecorder {
235
238
  exportBitDepth: this.exportBitDepth,
236
239
  channels: this.numberOfChannels,
237
240
  interval: this.config.interval ?? DEFAULT_WEB_INTERVAL,
241
+ // enableLogging: !!this.logger,
238
242
  })
239
243
 
240
244
  // Connect the source to the AudioWorkletNode and start recording
@@ -245,33 +249,11 @@ export class WebRecorder {
245
249
  }
246
250
  }
247
251
 
248
- initFeatureExtractorWorker(featuresExtratorUrl?: string) {
249
- try {
250
- if (featuresExtratorUrl) {
251
- // Initialize the feature extractor worker
252
- //TODO: create audio feature extractor from a Blob instead of url since we cannot include the url directly in the library
253
- // We keep the url during dev and use the blob in production.
254
- this.featureExtractorWorker = new Worker(
255
- new URL(featuresExtratorUrl, window.location.href)
256
- )
257
- this.featureExtractorWorker.onmessage =
258
- this.handleFeatureExtractorMessage.bind(this)
259
- this.featureExtractorWorker.onerror =
260
- this.handleWorkerError.bind(this)
261
- } else {
262
- // Fallback to the inline worker if the URL is not provided
263
- this.initFallbackWorker()
264
- }
265
- } catch (error) {
266
- console.error(
267
- `[${TAG}] Failed to initialize feature extractor worker`,
268
- error
269
- )
270
- this.initFallbackWorker()
271
- }
272
- }
273
-
274
- initFallbackWorker() {
252
+ /**
253
+ * Initializes the feature extractor worker for audio analysis
254
+ * Creates an inline worker from a blob for audio feature extraction
255
+ */
256
+ initFeatureExtractorWorker() {
275
257
  try {
276
258
  const blob = new Blob([InlineFeaturesExtractor], {
277
259
  type: 'application/javascript',
@@ -281,63 +263,156 @@ export class WebRecorder {
281
263
  this.featureExtractorWorker.onmessage =
282
264
  this.handleFeatureExtractorMessage.bind(this)
283
265
  this.featureExtractorWorker.onerror = (error) => {
284
- console.error(`[${TAG}] Default Inline worker failed`, error)
266
+ console.error(`[${TAG}] Feature extractor worker error:`, error)
285
267
  }
286
- this.logger?.log('Inline worker initialized successfully')
268
+ this.logger?.log(
269
+ 'Feature extractor worker initialized successfully'
270
+ )
287
271
  } catch (error) {
288
272
  console.error(
289
- `[${TAG}] Failed to initialize Inline Feature Extractor worker`,
273
+ `[${TAG}] Failed to initialize feature extractor worker`,
290
274
  error
291
275
  )
292
276
  }
293
277
  }
294
278
 
295
- handleWorkerError(error: ErrorEvent) {
296
- console.error(`[${TAG}] Feature extractor worker error:`, error)
297
- }
298
-
279
+ /**
280
+ * Processes audio analysis results from the feature extractor worker
281
+ * Updates the audio analysis data and emits events
282
+ * @param event - The event containing audio analysis results
283
+ */
299
284
  handleFeatureExtractorMessage(event: AudioFeaturesEvent) {
300
285
  if (event.data.command === 'features') {
301
286
  const segmentResult = event.data.result
302
287
 
303
- // Merge the segment result with the full audio analysis data
288
+ // Update the dataPointIdCounter based on the last ID received
289
+ if (
290
+ segmentResult.dataPoints &&
291
+ segmentResult.dataPoints.length > 0
292
+ ) {
293
+ const lastDataPoint =
294
+ segmentResult.dataPoints[
295
+ segmentResult.dataPoints.length - 1
296
+ ]
297
+ if (lastDataPoint && typeof lastDataPoint.id === 'number') {
298
+ this.dataPointIdCounter = Math.max(
299
+ this.dataPointIdCounter,
300
+ lastDataPoint.id + 1
301
+ )
302
+ }
303
+ }
304
+
305
+ console.debug('[WebRecorder] Raw segment result:', {
306
+ dataPointsLength: segmentResult.dataPoints.length,
307
+ durationMs: segmentResult.durationMs,
308
+ sampleRate: segmentResult.sampleRate,
309
+ amplitudeRange: segmentResult.amplitudeRange,
310
+ })
311
+
312
+ // Ensure consistent sample rate in the result
313
+ segmentResult.sampleRate =
314
+ this.config.sampleRate || this.audioContext.sampleRate
315
+
316
+ // Update the full audio analysis data with proper range merging
304
317
  this.audioAnalysisData.dataPoints.push(...segmentResult.dataPoints)
305
- this.audioAnalysisData.speakerChanges?.push(
306
- ...(segmentResult.speakerChanges ?? [])
307
- )
308
- this.audioAnalysisData.durationMs = segmentResult.durationMs
318
+ this.audioAnalysisData.durationMs += segmentResult.durationMs
319
+
320
+ // Make sure the sample rate is consistent
321
+ this.audioAnalysisData.sampleRate = segmentResult.sampleRate
322
+
323
+ // Properly merge amplitude ranges
309
324
  if (segmentResult.amplitudeRange) {
310
- this.audioAnalysisData.amplitudeRange = {
311
- min: Math.min(
312
- this.audioAnalysisData.amplitudeRange.min,
313
- segmentResult.amplitudeRange.min
314
- ),
315
- max: Math.max(
316
- this.audioAnalysisData.amplitudeRange.max,
317
- segmentResult.amplitudeRange.max
318
- ),
325
+ if (!this.audioAnalysisData.amplitudeRange) {
326
+ this.audioAnalysisData.amplitudeRange = {
327
+ ...segmentResult.amplitudeRange,
328
+ }
329
+ } else {
330
+ this.audioAnalysisData.amplitudeRange = {
331
+ min: Math.min(
332
+ this.audioAnalysisData.amplitudeRange.min,
333
+ segmentResult.amplitudeRange.min
334
+ ),
335
+ max: Math.max(
336
+ this.audioAnalysisData.amplitudeRange.max,
337
+ segmentResult.amplitudeRange.max
338
+ ),
339
+ }
340
+ }
341
+ }
342
+
343
+ // Properly merge RMS ranges
344
+ if (segmentResult.rmsRange) {
345
+ if (!this.audioAnalysisData.rmsRange) {
346
+ this.audioAnalysisData.rmsRange = {
347
+ ...segmentResult.rmsRange,
348
+ }
349
+ } else {
350
+ this.audioAnalysisData.rmsRange = {
351
+ min: Math.min(
352
+ this.audioAnalysisData.rmsRange.min,
353
+ segmentResult.rmsRange.min
354
+ ),
355
+ max: Math.max(
356
+ this.audioAnalysisData.rmsRange.max,
357
+ segmentResult.rmsRange.max
358
+ ),
359
+ }
319
360
  }
320
361
  }
321
- // Handle the extracted features (e.g., emit an event or log them)
362
+
322
363
  this.logger?.debug('features event segmentResult', segmentResult)
323
364
  this.logger?.debug(
324
365
  `features event audioAnalysisData duration=${this.audioAnalysisData.durationMs}`,
325
366
  this.audioAnalysisData
326
367
  )
327
368
  this.emitAudioAnalysisCallback(segmentResult)
369
+
370
+ console.debug('[WebRecorder] Updated audioAnalysisData:', {
371
+ dataPointsLength: this.audioAnalysisData.dataPoints.length,
372
+ durationMs: this.audioAnalysisData.durationMs,
373
+ sampleRate: this.audioAnalysisData.sampleRate,
374
+ amplitudeRange: this.audioAnalysisData.amplitudeRange,
375
+ })
328
376
  }
329
377
  }
330
378
 
379
+ /**
380
+ * Resets the data point ID counter
381
+ * Used when starting a new recording
382
+ */
383
+ resetDataPointCounter() {
384
+ this.dataPointIdCounter = 0
385
+
386
+ // Reset the counter in the worker
387
+ if (this.featureExtractorWorker) {
388
+ this.featureExtractorWorker.postMessage({
389
+ command: 'resetCounter',
390
+ startCounterFrom: 0,
391
+ })
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Starts the audio recording process
397
+ * Connects the audio nodes and begins capturing audio data
398
+ */
331
399
  start() {
332
400
  this.source.connect(this.audioWorkletNode)
333
401
  this.audioWorkletNode.connect(this.audioContext.destination)
334
402
  this.packetCount = 0
335
403
 
404
+ // Reset the counter when starting a new recording
405
+ this.resetDataPointCounter()
406
+
336
407
  if (this.compressedMediaRecorder) {
337
408
  this.compressedMediaRecorder.start(this.config.interval ?? 1000)
338
409
  }
339
410
  }
340
411
 
412
+ /**
413
+ * Stops the audio recording process and returns the recorded data
414
+ * @returns Promise resolving to an object containing PCM data and optional compressed blob
415
+ */
341
416
  async stop(): Promise<{ pcmData: Float32Array; compressedBlob?: Blob }> {
342
417
  try {
343
418
  if (this.compressedMediaRecorder) {
@@ -359,6 +434,10 @@ export class WebRecorder {
359
434
  }
360
435
  }
361
436
 
437
+ /**
438
+ * Cleans up resources when recording is stopped
439
+ * Closes audio context and disconnects nodes
440
+ */
362
441
  private cleanup() {
363
442
  if (this.audioContext) {
364
443
  this.audioContext.close()
@@ -372,113 +451,10 @@ export class WebRecorder {
372
451
  this.stopMediaStreamTracks()
373
452
  }
374
453
 
375
- // Helper method to process recording stop
376
- private async processRecordingStop(): Promise<{
377
- pcmData: Float32Array
378
- compressedBlob?: Blob
379
- }> {
380
- const processStartTime = performance.now()
381
- this.logger?.debug('[Performance] Starting recording stop process')
382
-
383
- const [compressedData, workletData] = await Promise.all([
384
- this.stopCompressedRecording(),
385
- this.stopAudioWorklet(),
386
- ])
387
-
388
- this.logger?.debug(
389
- `[Performance] Recording stop process completed in ${performance.now() - processStartTime}ms`
390
- )
391
- return {
392
- pcmData:
393
- workletData ??
394
- new Float32Array(this.audioAnalysisData.dataPoints.length),
395
- compressedBlob: compressedData,
396
- }
397
- }
398
-
399
- // Helper method to stop compressed recording
400
- private stopCompressedRecording(): Promise<Blob | undefined> {
401
- const startTime = performance.now()
402
- this.logger?.debug(
403
- `[Performance][${STOP_PERFORMANCE_MARKS.COMPRESSED_RECORDING_STOP}] Starting compressed recording stop`
404
- )
405
-
406
- if (!this.compressedMediaRecorder) {
407
- this.logger?.debug('[Performance] No compressed recorder to stop')
408
- return Promise.resolve(undefined)
409
- }
410
-
411
- return new Promise((resolve) => {
412
- this.compressedMediaRecorder!.onstop = () => {
413
- const blob = new Blob(this.compressedChunks, {
414
- type: 'audio/webm;codecs=opus',
415
- })
416
- this.logger?.debug(
417
- `[Performance][${STOP_PERFORMANCE_MARKS.COMPRESSED_RECORDING_STOP}] Compressed recording stopped in ${performance.now() - startTime}ms, size: ${blob.size}`
418
- )
419
- resolve(blob)
420
- }
421
- this.compressedMediaRecorder!.stop()
422
- })
423
- }
424
-
425
- // Helper method to stop audio worklet
426
- private stopAudioWorklet(): Promise<Float32Array | undefined> {
427
- const startTime = performance.now()
428
- this.logger?.debug(
429
- `[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Starting audio worklet stop`
430
- )
431
-
432
- if (!this.audioWorkletNode) {
433
- this.logger?.debug('[Performance] No audio worklet to stop')
434
- return Promise.resolve(undefined)
435
- }
436
-
437
- return new Promise((resolve) => {
438
- const onMessage = (event: AudioWorkletEvent) => {
439
- if (event.data.command === 'recordedData') {
440
- this.audioWorkletNode?.port.removeEventListener(
441
- 'message',
442
- onMessage
443
- )
444
- const rawPCMDataFull = event.data.recordedData?.slice(0)
445
-
446
- if (!rawPCMDataFull) {
447
- this.logger?.debug('[Performance] No PCM data received')
448
- resolve(undefined)
449
- return
450
- }
451
-
452
- if (this.exportBitDepth !== this.bitDepth) {
453
- const conversionStart = performance.now()
454
- convertPCMToFloat32({
455
- buffer: rawPCMDataFull.buffer,
456
- bitDepth: this.exportBitDepth,
457
- skipWavHeader: true,
458
- logger: this.logger,
459
- }).then(({ pcmValues }) => {
460
- this.logger?.debug(
461
- `[Performance] PCM conversion completed in ${performance.now() - conversionStart}ms`
462
- )
463
- this.logger?.debug(
464
- `[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Audio worklet stopped in ${performance.now() - startTime}ms`
465
- )
466
- resolve(pcmValues)
467
- })
468
- } else {
469
- this.logger?.debug(
470
- `[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Audio worklet stopped in ${performance.now() - startTime}ms`
471
- )
472
- resolve(rawPCMDataFull)
473
- }
474
- }
475
- }
476
-
477
- this.audioWorkletNode.port.addEventListener('message', onMessage)
478
- this.audioWorkletNode.port.postMessage({ command: 'stop' })
479
- })
480
- }
481
-
454
+ /**
455
+ * Pauses the audio recording process
456
+ * Disconnects audio nodes and pauses the media recorder
457
+ */
482
458
  pause() {
483
459
  this.source.disconnect(this.audioWorkletNode) // Disconnect the source from the AudioWorkletNode
484
460
  this.audioWorkletNode.disconnect(this.audioContext.destination) // Disconnect the AudioWorkletNode from the destination
@@ -486,50 +462,21 @@ export class WebRecorder {
486
462
  this.compressedMediaRecorder?.pause()
487
463
  }
488
464
 
465
+ /**
466
+ * Stops all media stream tracks to release hardware resources
467
+ * Ensures recording indicators (like microphone icon) are turned off
468
+ */
489
469
  stopMediaStreamTracks() {
490
470
  // Stop all audio tracks to stop the recording icon
491
471
  const tracks = this.source.mediaStream.getTracks()
492
472
  tracks.forEach((track) => track.stop())
493
473
  }
494
474
 
495
- async playRecordedData({
496
- recordedData,
497
- }: {
498
- recordedData: ArrayBuffer
499
- mimeType?: string
500
- }) {
501
- try {
502
- // Create a WAV blob with proper headers
503
- const wavHeaderBuffer = writeWavHeader({
504
- buffer: recordedData,
505
- sampleRate: this.audioContext.sampleRate,
506
- numChannels: this.numberOfChannels,
507
- bitDepth: this.exportBitDepth,
508
- })
509
-
510
- const blob = new Blob([wavHeaderBuffer], { type: 'audio/wav' })
511
- const url = URL.createObjectURL(blob)
512
- const response = await fetch(url)
513
- const arrayBuffer = await response.arrayBuffer()
514
-
515
- // Decode the audio data
516
- const audioBuffer =
517
- await this.audioContext.decodeAudioData(arrayBuffer)
518
-
519
- // Create a buffer source node and play the audio
520
- const bufferSource = this.audioContext.createBufferSource()
521
- bufferSource.buffer = audioBuffer
522
- bufferSource.connect(this.audioContext.destination)
523
- bufferSource.start()
524
- this.logger?.debug('Playing recorded data', recordedData)
525
-
526
- // Clean up
527
- URL.revokeObjectURL(url)
528
- } catch (error) {
529
- console.error(`[${TAG}] Failed to play recorded data:`, error)
530
- }
531
- }
532
-
475
+ /**
476
+ * Determines the audio format capabilities of the current audio context
477
+ * @param sampleRate - The sample rate to check
478
+ * @returns Object containing format information (sample rate, bit depth, channels)
479
+ */
533
480
  private checkAudioContextFormat({ sampleRate }: { sampleRate: number }) {
534
481
  // Create a silent AudioBuffer
535
482
  const frameCount = sampleRate * 1.0 // 1 second buffer
@@ -550,6 +497,10 @@ export class WebRecorder {
550
497
  }
551
498
  }
552
499
 
500
+ /**
501
+ * Resumes a paused recording
502
+ * Reconnects audio nodes and resumes the media recorder
503
+ */
553
504
  resume() {
554
505
  this.source.connect(this.audioWorkletNode)
555
506
  this.audioWorkletNode.connect(this.audioContext.destination)
@@ -557,6 +508,10 @@ export class WebRecorder {
557
508
  this.compressedMediaRecorder?.resume()
558
509
  }
559
510
 
511
+ /**
512
+ * Initializes the compressed media recorder if compression is enabled
513
+ * Sets up event handlers for compressed audio data
514
+ */
560
515
  private initializeCompressedRecorder() {
561
516
  try {
562
517
  const mimeType = 'audio/webm;codecs=opus'
@@ -590,4 +545,36 @@ export class WebRecorder {
590
545
  )
591
546
  }
592
547
  }
548
+
549
+ /**
550
+ * Processes features if enabled
551
+ */
552
+ processFeatures(
553
+ chunk: Float32Array,
554
+ sampleRate: number,
555
+ chunkPosition: number,
556
+ startPosition: number,
557
+ endPosition: number,
558
+ samples: number
559
+ ) {
560
+ if (this.config.enableProcessing && this.featureExtractorWorker) {
561
+ this.featureExtractorWorker.postMessage({
562
+ command: 'process',
563
+ channelData: chunk,
564
+ sampleRate,
565
+ segmentDurationMs:
566
+ this.config.segmentDurationMs ??
567
+ DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms
568
+ bitDepth: this.bitDepth,
569
+ fullAudioDurationMs: chunkPosition * 1000,
570
+ numberOfChannels: this.numberOfChannels,
571
+ features: this.config.features,
572
+ intervalAnalysis: this.config.intervalAnalysis,
573
+ startPosition,
574
+ endPosition,
575
+ samples,
576
+ startCounterFrom: this.dataPointIdCounter, // Pass the current counter value
577
+ })
578
+ }
579
+ }
593
580
  }