@siteed/expo-audio-stream 1.16.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 (77) hide show
  1. package/CHANGELOG.md +28 -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 +134 -23
  7. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +35 -29
  8. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  9. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +236 -96
  10. package/android/src/main/java/net/siteed/audiostream/FFT.kt +55 -0
  11. package/android/src/main/java/net/siteed/audiostream/Features.kt +49 -7
  12. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +4 -4
  13. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +55 -47
  14. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  15. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  16. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +60 -13
  17. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
  18. package/build/AudioAnalysis/extractAudioAnalysis.js +147 -162
  19. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  20. package/build/ExpoAudioStream.types.d.ts +49 -3
  21. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  22. package/build/ExpoAudioStream.types.js.map +1 -1
  23. package/build/ExpoAudioStream.web.d.ts +2 -0
  24. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  25. package/build/ExpoAudioStream.web.js +8 -1
  26. package/build/ExpoAudioStream.web.js.map +1 -1
  27. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  28. package/build/ExpoAudioStreamModule.js +216 -12
  29. package/build/ExpoAudioStreamModule.js.map +1 -1
  30. package/build/WebRecorder.web.d.ts +67 -13
  31. package/build/WebRecorder.web.d.ts.map +1 -1
  32. package/build/WebRecorder.web.js +178 -173
  33. package/build/WebRecorder.web.js.map +1 -1
  34. package/build/index.d.ts +3 -3
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +2 -2
  37. package/build/index.js.map +1 -1
  38. package/build/useAudioRecorder.d.ts.map +1 -1
  39. package/build/useAudioRecorder.js +12 -8
  40. package/build/useAudioRecorder.js.map +1 -1
  41. package/build/utils/audioProcessing.d.ts +24 -0
  42. package/build/utils/audioProcessing.d.ts.map +1 -0
  43. package/build/utils/audioProcessing.js +133 -0
  44. package/build/utils/audioProcessing.js.map +1 -0
  45. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  46. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  47. package/build/workers/InlineFeaturesExtractor.web.js +692 -175
  48. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  49. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  50. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  51. package/build/workers/inlineAudioWebWorker.web.js +3 -2
  52. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  53. package/ios/AudioAnalysisData.swift +51 -16
  54. package/ios/AudioProcessingHelpers.swift +710 -26
  55. package/ios/AudioProcessor.swift +334 -185
  56. package/ios/AudioStreamManager.swift +66 -22
  57. package/ios/DataPoint.swift +25 -12
  58. package/ios/DecodingConfig.swift +47 -0
  59. package/ios/ExpoAudioStreamModule.swift +189 -104
  60. package/ios/FFT.swift +62 -0
  61. package/ios/Features.swift +24 -3
  62. package/ios/RecordingSettings.swift +9 -7
  63. package/package.json +2 -1
  64. package/plugin/build/index.d.ts +2 -0
  65. package/plugin/build/index.js +10 -3
  66. package/plugin/src/index.ts +10 -1
  67. package/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
  68. package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
  69. package/src/ExpoAudioStream.types.ts +57 -7
  70. package/src/ExpoAudioStream.web.ts +8 -1
  71. package/src/ExpoAudioStreamModule.ts +255 -10
  72. package/src/WebRecorder.web.ts +231 -243
  73. package/src/index.ts +5 -3
  74. package/src/useAudioRecorder.tsx +14 -10
  75. package/src/utils/audioProcessing.ts +205 -0
  76. package/src/workers/InlineFeaturesExtractor.web.tsx +692 -175
  77. 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,32 +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
- },
197
- []
198
- )
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
+ })
199
203
  }
200
204
 
201
205
  // Emit chunk immediately
@@ -234,6 +238,7 @@ export class WebRecorder {
234
238
  exportBitDepth: this.exportBitDepth,
235
239
  channels: this.numberOfChannels,
236
240
  interval: this.config.interval ?? DEFAULT_WEB_INTERVAL,
241
+ // enableLogging: !!this.logger,
237
242
  })
238
243
 
239
244
  // Connect the source to the AudioWorkletNode and start recording
@@ -244,33 +249,11 @@ export class WebRecorder {
244
249
  }
245
250
  }
246
251
 
247
- initFeatureExtractorWorker(featuresExtratorUrl?: string) {
248
- try {
249
- if (featuresExtratorUrl) {
250
- // Initialize the feature extractor worker
251
- //TODO: create audio feature extractor from a Blob instead of url since we cannot include the url directly in the library
252
- // We keep the url during dev and use the blob in production.
253
- this.featureExtractorWorker = new Worker(
254
- new URL(featuresExtratorUrl, window.location.href)
255
- )
256
- this.featureExtractorWorker.onmessage =
257
- this.handleFeatureExtractorMessage.bind(this)
258
- this.featureExtractorWorker.onerror =
259
- this.handleWorkerError.bind(this)
260
- } else {
261
- // Fallback to the inline worker if the URL is not provided
262
- this.initFallbackWorker()
263
- }
264
- } catch (error) {
265
- console.error(
266
- `[${TAG}] Failed to initialize feature extractor worker`,
267
- error
268
- )
269
- this.initFallbackWorker()
270
- }
271
- }
272
-
273
- 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() {
274
257
  try {
275
258
  const blob = new Blob([InlineFeaturesExtractor], {
276
259
  type: 'application/javascript',
@@ -280,63 +263,156 @@ export class WebRecorder {
280
263
  this.featureExtractorWorker.onmessage =
281
264
  this.handleFeatureExtractorMessage.bind(this)
282
265
  this.featureExtractorWorker.onerror = (error) => {
283
- console.error(`[${TAG}] Default Inline worker failed`, error)
266
+ console.error(`[${TAG}] Feature extractor worker error:`, error)
284
267
  }
285
- this.logger?.log('Inline worker initialized successfully')
268
+ this.logger?.log(
269
+ 'Feature extractor worker initialized successfully'
270
+ )
286
271
  } catch (error) {
287
272
  console.error(
288
- `[${TAG}] Failed to initialize Inline Feature Extractor worker`,
273
+ `[${TAG}] Failed to initialize feature extractor worker`,
289
274
  error
290
275
  )
291
276
  }
292
277
  }
293
278
 
294
- handleWorkerError(error: ErrorEvent) {
295
- console.error(`[${TAG}] Feature extractor worker error:`, error)
296
- }
297
-
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
+ */
298
284
  handleFeatureExtractorMessage(event: AudioFeaturesEvent) {
299
285
  if (event.data.command === 'features') {
300
286
  const segmentResult = event.data.result
301
287
 
302
- // 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
303
317
  this.audioAnalysisData.dataPoints.push(...segmentResult.dataPoints)
304
- this.audioAnalysisData.speakerChanges?.push(
305
- ...(segmentResult.speakerChanges ?? [])
306
- )
307
- 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
308
324
  if (segmentResult.amplitudeRange) {
309
- this.audioAnalysisData.amplitudeRange = {
310
- min: Math.min(
311
- this.audioAnalysisData.amplitudeRange.min,
312
- segmentResult.amplitudeRange.min
313
- ),
314
- max: Math.max(
315
- this.audioAnalysisData.amplitudeRange.max,
316
- segmentResult.amplitudeRange.max
317
- ),
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
+ }
318
360
  }
319
361
  }
320
- // Handle the extracted features (e.g., emit an event or log them)
362
+
321
363
  this.logger?.debug('features event segmentResult', segmentResult)
322
364
  this.logger?.debug(
323
365
  `features event audioAnalysisData duration=${this.audioAnalysisData.durationMs}`,
324
366
  this.audioAnalysisData
325
367
  )
326
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
+ })
327
376
  }
328
377
  }
329
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
+ */
330
399
  start() {
331
400
  this.source.connect(this.audioWorkletNode)
332
401
  this.audioWorkletNode.connect(this.audioContext.destination)
333
402
  this.packetCount = 0
334
403
 
404
+ // Reset the counter when starting a new recording
405
+ this.resetDataPointCounter()
406
+
335
407
  if (this.compressedMediaRecorder) {
336
408
  this.compressedMediaRecorder.start(this.config.interval ?? 1000)
337
409
  }
338
410
  }
339
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
+ */
340
416
  async stop(): Promise<{ pcmData: Float32Array; compressedBlob?: Blob }> {
341
417
  try {
342
418
  if (this.compressedMediaRecorder) {
@@ -358,6 +434,10 @@ export class WebRecorder {
358
434
  }
359
435
  }
360
436
 
437
+ /**
438
+ * Cleans up resources when recording is stopped
439
+ * Closes audio context and disconnects nodes
440
+ */
361
441
  private cleanup() {
362
442
  if (this.audioContext) {
363
443
  this.audioContext.close()
@@ -371,113 +451,10 @@ export class WebRecorder {
371
451
  this.stopMediaStreamTracks()
372
452
  }
373
453
 
374
- // Helper method to process recording stop
375
- private async processRecordingStop(): Promise<{
376
- pcmData: Float32Array
377
- compressedBlob?: Blob
378
- }> {
379
- const processStartTime = performance.now()
380
- this.logger?.debug('[Performance] Starting recording stop process')
381
-
382
- const [compressedData, workletData] = await Promise.all([
383
- this.stopCompressedRecording(),
384
- this.stopAudioWorklet(),
385
- ])
386
-
387
- this.logger?.debug(
388
- `[Performance] Recording stop process completed in ${performance.now() - processStartTime}ms`
389
- )
390
- return {
391
- pcmData:
392
- workletData ??
393
- new Float32Array(this.audioAnalysisData.dataPoints.length),
394
- compressedBlob: compressedData,
395
- }
396
- }
397
-
398
- // Helper method to stop compressed recording
399
- private stopCompressedRecording(): Promise<Blob | undefined> {
400
- const startTime = performance.now()
401
- this.logger?.debug(
402
- `[Performance][${STOP_PERFORMANCE_MARKS.COMPRESSED_RECORDING_STOP}] Starting compressed recording stop`
403
- )
404
-
405
- if (!this.compressedMediaRecorder) {
406
- this.logger?.debug('[Performance] No compressed recorder to stop')
407
- return Promise.resolve(undefined)
408
- }
409
-
410
- return new Promise((resolve) => {
411
- this.compressedMediaRecorder!.onstop = () => {
412
- const blob = new Blob(this.compressedChunks, {
413
- type: 'audio/webm;codecs=opus',
414
- })
415
- this.logger?.debug(
416
- `[Performance][${STOP_PERFORMANCE_MARKS.COMPRESSED_RECORDING_STOP}] Compressed recording stopped in ${performance.now() - startTime}ms, size: ${blob.size}`
417
- )
418
- resolve(blob)
419
- }
420
- this.compressedMediaRecorder!.stop()
421
- })
422
- }
423
-
424
- // Helper method to stop audio worklet
425
- private stopAudioWorklet(): Promise<Float32Array | undefined> {
426
- const startTime = performance.now()
427
- this.logger?.debug(
428
- `[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Starting audio worklet stop`
429
- )
430
-
431
- if (!this.audioWorkletNode) {
432
- this.logger?.debug('[Performance] No audio worklet to stop')
433
- return Promise.resolve(undefined)
434
- }
435
-
436
- return new Promise((resolve) => {
437
- const onMessage = (event: AudioWorkletEvent) => {
438
- if (event.data.command === 'recordedData') {
439
- this.audioWorkletNode?.port.removeEventListener(
440
- 'message',
441
- onMessage
442
- )
443
- const rawPCMDataFull = event.data.recordedData?.slice(0)
444
-
445
- if (!rawPCMDataFull) {
446
- this.logger?.debug('[Performance] No PCM data received')
447
- resolve(undefined)
448
- return
449
- }
450
-
451
- if (this.exportBitDepth !== this.bitDepth) {
452
- const conversionStart = performance.now()
453
- convertPCMToFloat32({
454
- buffer: rawPCMDataFull.buffer,
455
- bitDepth: this.exportBitDepth,
456
- skipWavHeader: true,
457
- logger: this.logger,
458
- }).then(({ pcmValues }) => {
459
- this.logger?.debug(
460
- `[Performance] PCM conversion completed in ${performance.now() - conversionStart}ms`
461
- )
462
- this.logger?.debug(
463
- `[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Audio worklet stopped in ${performance.now() - startTime}ms`
464
- )
465
- resolve(pcmValues)
466
- })
467
- } else {
468
- this.logger?.debug(
469
- `[Performance][${STOP_PERFORMANCE_MARKS.AUDIO_WORKLET_STOP}] Audio worklet stopped in ${performance.now() - startTime}ms`
470
- )
471
- resolve(rawPCMDataFull)
472
- }
473
- }
474
- }
475
-
476
- this.audioWorkletNode.port.addEventListener('message', onMessage)
477
- this.audioWorkletNode.port.postMessage({ command: 'stop' })
478
- })
479
- }
480
-
454
+ /**
455
+ * Pauses the audio recording process
456
+ * Disconnects audio nodes and pauses the media recorder
457
+ */
481
458
  pause() {
482
459
  this.source.disconnect(this.audioWorkletNode) // Disconnect the source from the AudioWorkletNode
483
460
  this.audioWorkletNode.disconnect(this.audioContext.destination) // Disconnect the AudioWorkletNode from the destination
@@ -485,50 +462,21 @@ export class WebRecorder {
485
462
  this.compressedMediaRecorder?.pause()
486
463
  }
487
464
 
465
+ /**
466
+ * Stops all media stream tracks to release hardware resources
467
+ * Ensures recording indicators (like microphone icon) are turned off
468
+ */
488
469
  stopMediaStreamTracks() {
489
470
  // Stop all audio tracks to stop the recording icon
490
471
  const tracks = this.source.mediaStream.getTracks()
491
472
  tracks.forEach((track) => track.stop())
492
473
  }
493
474
 
494
- async playRecordedData({
495
- recordedData,
496
- }: {
497
- recordedData: ArrayBuffer
498
- mimeType?: string
499
- }) {
500
- try {
501
- // Create a WAV blob with proper headers
502
- const wavHeaderBuffer = writeWavHeader({
503
- buffer: recordedData,
504
- sampleRate: this.audioContext.sampleRate,
505
- numChannels: this.numberOfChannels,
506
- bitDepth: this.exportBitDepth,
507
- })
508
-
509
- const blob = new Blob([wavHeaderBuffer], { type: 'audio/wav' })
510
- const url = URL.createObjectURL(blob)
511
- const response = await fetch(url)
512
- const arrayBuffer = await response.arrayBuffer()
513
-
514
- // Decode the audio data
515
- const audioBuffer =
516
- await this.audioContext.decodeAudioData(arrayBuffer)
517
-
518
- // Create a buffer source node and play the audio
519
- const bufferSource = this.audioContext.createBufferSource()
520
- bufferSource.buffer = audioBuffer
521
- bufferSource.connect(this.audioContext.destination)
522
- bufferSource.start()
523
- this.logger?.debug('Playing recorded data', recordedData)
524
-
525
- // Clean up
526
- URL.revokeObjectURL(url)
527
- } catch (error) {
528
- console.error(`[${TAG}] Failed to play recorded data:`, error)
529
- }
530
- }
531
-
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
+ */
532
480
  private checkAudioContextFormat({ sampleRate }: { sampleRate: number }) {
533
481
  // Create a silent AudioBuffer
534
482
  const frameCount = sampleRate * 1.0 // 1 second buffer
@@ -549,6 +497,10 @@ export class WebRecorder {
549
497
  }
550
498
  }
551
499
 
500
+ /**
501
+ * Resumes a paused recording
502
+ * Reconnects audio nodes and resumes the media recorder
503
+ */
552
504
  resume() {
553
505
  this.source.connect(this.audioWorkletNode)
554
506
  this.audioWorkletNode.connect(this.audioContext.destination)
@@ -556,6 +508,10 @@ export class WebRecorder {
556
508
  this.compressedMediaRecorder?.resume()
557
509
  }
558
510
 
511
+ /**
512
+ * Initializes the compressed media recorder if compression is enabled
513
+ * Sets up event handlers for compressed audio data
514
+ */
559
515
  private initializeCompressedRecorder() {
560
516
  try {
561
517
  const mimeType = 'audio/webm;codecs=opus'
@@ -589,4 +545,36 @@ export class WebRecorder {
589
545
  )
590
546
  }
591
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
+ }
592
580
  }