@siteed/expo-audio-studio 2.5.0 → 2.6.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 (35) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +3 -3
  3. package/build/AudioDeviceManager.d.ts +1 -1
  4. package/build/AudioDeviceManager.js +1 -1
  5. package/build/AudioDeviceManager.js.map +1 -1
  6. package/build/ExpoAudioStream.types.d.ts +19 -1
  7. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  8. package/build/ExpoAudioStream.types.js.map +1 -1
  9. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  10. package/build/ExpoAudioStream.web.js +80 -9
  11. package/build/ExpoAudioStream.web.js.map +1 -1
  12. package/build/WebRecorder.web.d.ts +14 -4
  13. package/build/WebRecorder.web.d.ts.map +1 -1
  14. package/build/WebRecorder.web.js +121 -14
  15. package/build/WebRecorder.web.js.map +1 -1
  16. package/build/useAudioRecorder.d.ts.map +1 -1
  17. package/build/useAudioRecorder.js +1 -1
  18. package/build/useAudioRecorder.js.map +1 -1
  19. package/build/utils/writeWavHeader.d.ts +3 -18
  20. package/build/utils/writeWavHeader.d.ts.map +1 -1
  21. package/build/utils/writeWavHeader.js +19 -26
  22. package/build/utils/writeWavHeader.js.map +1 -1
  23. package/ios/AudioDeviceManager.swift +65 -65
  24. package/ios/AudioProcessor.swift +32 -32
  25. package/ios/AudioStreamManager.swift +323 -158
  26. package/ios/ExpoAudioStreamModule.swift +92 -75
  27. package/ios/ISSUE_IOS.md +26 -3
  28. package/ios/Logger.swift +27 -7
  29. package/package.json +1 -1
  30. package/src/AudioDeviceManager.ts +1 -1
  31. package/src/ExpoAudioStream.types.ts +21 -1
  32. package/src/ExpoAudioStream.web.ts +99 -9
  33. package/src/WebRecorder.web.ts +146 -21
  34. package/src/useAudioRecorder.tsx +1 -2
  35. package/src/utils/writeWavHeader.ts +26 -25
@@ -207,6 +207,19 @@ export interface IOSConfig {
207
207
  audioSession?: AudioSessionConfig
208
208
  }
209
209
 
210
+ /** Web platform specific configuration options */
211
+ export interface WebConfig {
212
+ /**
213
+ * Whether to store uncompressed audio data for WAV generation
214
+ *
215
+ * When true, all PCM chunks are stored in memory to create a WAV file when compression is disabled
216
+ * When false, uncompressed audio won't be available, but memory usage will be lower
217
+ *
218
+ * Default: true (for backward compatibility)
219
+ */
220
+ storeUncompressedAudio?: boolean
221
+ }
222
+
210
223
  // Add new type for interruption reasons
211
224
  export type RecordingInterruptionReason =
212
225
  /** Audio focus was lost to another app */
@@ -312,6 +325,9 @@ export interface RecordingConfig {
312
325
  /** iOS-specific configuration */
313
326
  ios?: IOSConfig
314
327
 
328
+ /** Web-specific configuration options */
329
+ web?: WebConfig
330
+
315
331
  /** Duration of each segment in milliseconds for analysis (default: 100) */
316
332
  segmentDurationMs?: number
317
333
 
@@ -328,7 +344,11 @@ export interface RecordingConfig {
328
344
  compression?: {
329
345
  /** Enable audio compression */
330
346
  enabled: boolean
331
- /** Format for compression (aac or opus) */
347
+ /**
348
+ * Format for compression
349
+ * - 'aac': Advanced Audio Coding - supported on all platforms
350
+ * - 'opus': Opus encoding - supported on Android and Web; on iOS will automatically fall back to AAC
351
+ */
332
352
  format: 'aac' | 'opus'
333
353
  /** Bitrate for compression in bits per second */
334
354
  bitrate?: number
@@ -113,7 +113,56 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
113
113
  // Utility to handle user media stream
114
114
  async getMediaStream() {
115
115
  try {
116
- return await navigator.mediaDevices.getUserMedia({ audio: true })
116
+ this.logger?.debug('Requesting user media (microphone)...')
117
+
118
+ // First check if the browser supports the necessary audio APIs
119
+ if (!navigator?.mediaDevices?.getUserMedia) {
120
+ this.logger?.error(
121
+ 'Browser does not support mediaDevices.getUserMedia'
122
+ )
123
+ throw new Error('Browser does not support audio recording')
124
+ }
125
+
126
+ // Get media with detailed audio constraints for better diagnostics
127
+ const constraints = {
128
+ audio: {
129
+ echoCancellation: true,
130
+ noiseSuppression: true,
131
+ autoGainControl: true,
132
+ // Add deviceId constraint if specified
133
+ ...(this.recordingConfig?.deviceId
134
+ ? {
135
+ deviceId: {
136
+ exact: this.recordingConfig.deviceId,
137
+ },
138
+ }
139
+ : {}),
140
+ },
141
+ }
142
+
143
+ this.logger?.debug('Media constraints:', constraints)
144
+
145
+ const stream =
146
+ await navigator.mediaDevices.getUserMedia(constraints)
147
+
148
+ // Get detailed info about the audio track for debugging
149
+ const audioTracks = stream.getAudioTracks()
150
+ if (audioTracks.length > 0) {
151
+ const track = audioTracks[0]
152
+ const settings = track.getSettings()
153
+ this.logger?.debug('Audio track obtained:', {
154
+ label: track.label,
155
+ id: track.id,
156
+ enabled: track.enabled,
157
+ muted: track.muted,
158
+ readyState: track.readyState,
159
+ settings,
160
+ })
161
+ } else {
162
+ this.logger?.warn('Stream has no audio tracks!')
163
+ }
164
+
165
+ return stream
117
166
  } catch (error) {
118
167
  this.logger?.error('Failed to get media stream:', error)
119
168
  throw error
@@ -400,7 +449,8 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
400
449
  this.logger?.debug('Starting stop process')
401
450
 
402
451
  try {
403
- const { compressedBlob } = await this.customRecorder.stop()
452
+ const { compressedBlob, uncompressedBlob } =
453
+ await this.customRecorder.stop()
404
454
 
405
455
  this.isRecording = false
406
456
  this.isPaused = false
@@ -409,20 +459,52 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
409
459
  let fileUri = `${this.streamUuid}.${this.extension}`
410
460
  let mimeType = `audio/${this.extension}`
411
461
 
412
- // Process compressed audio if available
413
- if (compressedBlob && this.recordingConfig?.compression?.enabled) {
462
+ // Handle both compressed and uncompressed blobs according to configuration
463
+ const compressionEnabled =
464
+ this.recordingConfig?.compression?.enabled ?? false
465
+
466
+ // Process compressed blob if available
467
+ if (compressedBlob) {
414
468
  const compressedUri = URL.createObjectURL(compressedBlob)
415
- compression = {
469
+ const compressedInfo = {
416
470
  compressedFileUri: compressedUri,
417
471
  size: compressedBlob.size,
418
472
  mimeType: 'audio/webm',
419
473
  format: 'opus',
420
- bitrate: this.recordingConfig.compression.bitrate ?? 128000,
474
+ bitrate:
475
+ this.recordingConfig?.compression?.bitrate ?? 128000,
421
476
  }
422
477
 
423
- // Use compressed values when compression is enabled
424
- fileUri = compressedUri
425
- mimeType = 'audio/webm'
478
+ // If compression is enabled, use compressed blob as primary format
479
+ if (compressionEnabled) {
480
+ this.logger?.debug(
481
+ 'Using compressed audio as primary output'
482
+ )
483
+ fileUri = compressedUri
484
+ mimeType = 'audio/webm'
485
+
486
+ // Store compression info
487
+ compression = compressedInfo
488
+ } else {
489
+ // Compression was enabled during recording but not set as primary
490
+ // Store as alternate format
491
+ compression = compressedInfo
492
+ }
493
+ }
494
+
495
+ // Process uncompressed WAV if available
496
+ if (uncompressedBlob) {
497
+ const wavUri = URL.createObjectURL(uncompressedBlob)
498
+
499
+ // If compression is disabled or no compressed blob is available,
500
+ // use WAV as primary format
501
+ if (!compressionEnabled || !compressedBlob) {
502
+ this.logger?.debug(
503
+ 'Using uncompressed WAV as primary output'
504
+ )
505
+ fileUri = wavUri
506
+ mimeType = 'audio/wav'
507
+ }
426
508
  }
427
509
 
428
510
  // Use the stored streamUuid for the final filename
@@ -443,6 +525,14 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
443
525
  // Reset after creating the result
444
526
  this.streamUuid = null
445
527
 
528
+ // Reset recording state variables to prepare for next recording
529
+ this.currentDurationMs = 0
530
+ this.currentSize = 0
531
+ this.lastEmittedSize = 0
532
+ this.totalCompressedSize = 0
533
+ this.lastEmittedCompressionSize = 0
534
+ this.audioChunks = []
535
+
446
536
  return result
447
537
  } catch (error) {
448
538
  this.logger?.error('Error stopping recording:', error)
@@ -7,6 +7,7 @@ import {
7
7
  EmitAudioEventFunction,
8
8
  } from './ExpoAudioStream.web'
9
9
  import { encodingToBitDepth } from './utils/encodingToBitDepth'
10
+ import { writeWavHeader } from './utils/writeWavHeader'
10
11
  import { InlineFeaturesExtractor } from './workers/InlineFeaturesExtractor.web'
11
12
  import { InlineAudioWebWorker } from './workers/inlineAudioWebWorker.web'
12
13
 
@@ -16,6 +17,7 @@ interface AudioWorkletEvent {
16
17
  recordedData?: Float32Array
17
18
  sampleRate?: number
18
19
  position?: number
20
+ message?: string // For debug messages
19
21
  }
20
22
  }
21
23
 
@@ -60,6 +62,8 @@ export class WebRecorder {
60
62
  timestamp: number
61
63
  }) => void
62
64
  private _isDeviceDisconnected: boolean = false
65
+ private pcmData: Float32Array | null = null // Store original PCM data
66
+ private totalSampleCount: number = 0
63
67
 
64
68
  /**
65
69
  * Flag to indicate whether this is the first audio chunk after a device switch
@@ -183,6 +187,11 @@ export class WebRecorder {
183
187
  event: AudioWorkletEvent
184
188
  ) => {
185
189
  const command = event.data.command
190
+ if (command === 'debug') {
191
+ this.logger?.debug(`[AudioWorklet] ${event.data.message}`)
192
+ return
193
+ }
194
+
186
195
  if (command !== 'newData') return
187
196
 
188
197
  const pcmBufferFloat = event.data.recordedData
@@ -192,9 +201,11 @@ export class WebRecorder {
192
201
  }
193
202
 
194
203
  // Process data in smaller chunks and emit immediately
195
- const chunkSize = this.audioContext.sampleRate * 2 // Reduce to 2 seconds chunks
196
204
  const sampleRate =
197
205
  event.data.sampleRate ?? this.audioContext.sampleRate
206
+ // Use chunk size from config interval or default to 2 seconds
207
+ const intervalMs = this.config.interval ?? DEFAULT_WEB_INTERVAL
208
+ const chunkSize = Math.floor(sampleRate * (intervalMs / 1000))
198
209
  const duration = pcmBufferFloat.length / sampleRate
199
210
 
200
211
  // Use incoming position if provided by worklet, otherwise use our tracked position
@@ -241,6 +252,17 @@ export class WebRecorder {
241
252
  })
242
253
  }
243
254
 
255
+ // Only store PCM data if web.storeUncompressedAudio is not explicitly false
256
+ const shouldStoreUncompressed =
257
+ this.config.web?.storeUncompressedAudio !== false
258
+
259
+ // Store PCM chunks when needed
260
+ if (shouldStoreUncompressed) {
261
+ // Store the original Float32Array data for later WAV creation
262
+ this.appendPcmData(chunk)
263
+ this.totalSampleCount += chunk.length
264
+ }
265
+
244
266
  // Emit chunk immediately
245
267
  this.emitAudioEventCallback({
246
268
  data: chunk,
@@ -265,21 +287,42 @@ export class WebRecorder {
265
287
  this.pendingCompressedChunk = null
266
288
  }
267
289
 
268
- this.logger?.debug(
269
- `WebRecorder initialized -- recordSampleRate=${this.audioContext.sampleRate}, startPosition=${this.position}`,
270
- this.config
271
- )
290
+ // Ensure we use all relevant settings from config
291
+ const recordSampleRate = this.audioContext.sampleRate
292
+ const exportSampleRate =
293
+ this.config.sampleRate ?? this.audioContext.sampleRate
294
+ const channels = this.config.channels ?? this.numberOfChannels
295
+ const interval = this.config.interval ?? DEFAULT_WEB_INTERVAL
296
+
297
+ this.logger?.debug(`WebRecorder initialized with config:`, {
298
+ recordSampleRate,
299
+ exportSampleRate,
300
+ bitDepth: this.bitDepth,
301
+ exportBitDepth: this.exportBitDepth,
302
+ channels,
303
+ interval,
304
+ position: this.position,
305
+ deviceId: this.config.deviceId || 'default',
306
+ compression: this.config.compression
307
+ ? {
308
+ enabled: this.config.compression.enabled,
309
+ format: this.config.compression.format,
310
+ bitrate: this.config.compression.bitrate,
311
+ }
312
+ : 'disabled',
313
+ })
314
+
315
+ // Initialize the worklet with all settings from config
272
316
  this.audioWorkletNode.port.postMessage({
273
317
  command: 'init',
274
- recordSampleRate: this.audioContext.sampleRate,
275
- exportSampleRate:
276
- this.config.sampleRate ?? this.audioContext.sampleRate,
318
+ recordSampleRate,
319
+ exportSampleRate,
277
320
  bitDepth: this.bitDepth,
278
321
  exportBitDepth: this.exportBitDepth,
279
- channels: this.numberOfChannels,
280
- interval: this.config.interval ?? DEFAULT_WEB_INTERVAL,
322
+ channels,
323
+ interval,
281
324
  position: this.position, // Pass the current position to the processor
282
- // enableLogging: !!this.logger,
325
+ enableLogging: true,
283
326
  })
284
327
 
285
328
  // Connect the source to the AudioWorkletNode and start recording
@@ -290,6 +333,35 @@ export class WebRecorder {
290
333
  }
291
334
  }
292
335
 
336
+ /**
337
+ * Append new PCM data to the existing buffer
338
+ * @param newData New Float32Array data to append
339
+ */
340
+ private appendPcmData(newData: Float32Array): void {
341
+ // Clone the incoming data to ensure it's not modified
342
+ const dataToAdd = new Float32Array(newData)
343
+
344
+ if (!this.pcmData) {
345
+ // First chunk - create a copy to avoid references to original data
346
+ this.pcmData = new Float32Array(dataToAdd)
347
+ return
348
+ }
349
+
350
+ // Create a new buffer with increased size
351
+ const newBuffer = new Float32Array(
352
+ this.pcmData.length + dataToAdd.length
353
+ )
354
+
355
+ // Copy existing data
356
+ newBuffer.set(this.pcmData)
357
+
358
+ // Append new data
359
+ newBuffer.set(dataToAdd, this.pcmData.length)
360
+
361
+ // Replace existing buffer
362
+ this.pcmData = newBuffer
363
+ }
364
+
293
365
  /**
294
366
  * Initializes the feature extractor worker for audio analysis
295
367
  * Creates an inline worker from a blob for audio feature extraction
@@ -492,6 +564,10 @@ export class WebRecorder {
492
564
  )
493
565
  this.resetDataPointCounter(0) // Explicitly reset to 0 for new recordings
494
566
  this.isFirstChunkAfterSwitch = false
567
+
568
+ // Clear PCM data for new recording
569
+ this.pcmData = null
570
+ this.totalSampleCount = 0
495
571
  } else {
496
572
  this.logger?.debug(
497
573
  `Preserving counter at ${this.dataPointIdCounter} during device switch`
@@ -504,17 +580,55 @@ export class WebRecorder {
504
580
  }
505
581
 
506
582
  /**
507
- * Stops the audio recording process and returns the recorded data
508
- * @param externalAudioChunks Optional array of Float32Array chunks from previous devices
509
- * @returns Promise resolving to an object containing PCM data and optional compressed blob
583
+ * Creates a WAV file from the stored PCM data
510
584
  */
511
- async stop(
512
- externalAudioChunks?: Float32Array[]
513
- ): Promise<{ pcmData: Float32Array; compressedBlob?: Blob }> {
585
+ private createWavFromPcmData(): Blob | null {
514
586
  try {
515
- // Log what's happening for debugging
516
- this.logger?.debug('Stopping recording and collecting final data')
587
+ // Check if we have PCM data
588
+ if (!this.pcmData || this.pcmData.length === 0) {
589
+ this.logger?.warn('No PCM data available to create WAV file')
590
+ return null
591
+ }
592
+
593
+ const sampleRate =
594
+ this.config.sampleRate || this.audioContext.sampleRate
595
+ const channels = this.numberOfChannels || 1
596
+
597
+ // Convert float32 PCM data to 16-bit PCM for WAV
598
+ const bytesPerSample = 2 // 16-bit = 2 bytes
599
+ const dataLength = this.pcmData.length * bytesPerSample
600
+ const buffer = new ArrayBuffer(dataLength)
601
+ const view = new DataView(buffer)
602
+
603
+ // Convert Float32Array (-1 to 1) to Int16Array (-32768 to 32767)
604
+ for (let i = 0; i < this.pcmData.length; i++) {
605
+ const sample = Math.max(-1, Math.min(1, this.pcmData[i]))
606
+ const int16Value = Math.round(sample * 32767)
607
+ view.setInt16(i * 2, int16Value, true)
608
+ }
609
+
610
+ // Use the existing writeWavHeader utility to add a WAV header
611
+ const wavBuffer = writeWavHeader({
612
+ buffer,
613
+ sampleRate,
614
+ numChannels: channels,
615
+ bitDepth: 16,
616
+ isFloat: false,
617
+ })
517
618
 
619
+ return new Blob([wavBuffer], { type: 'audio/wav' })
620
+ } catch (error) {
621
+ this.logger?.error('Error creating WAV file from PCM data:', error)
622
+ return null
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Stops the audio recording process and returns the recorded data
628
+ * @returns Promise resolving to an object containing compressed and/or uncompressed blobs
629
+ */
630
+ async stop(): Promise<{ compressedBlob?: Blob; uncompressedBlob?: Blob }> {
631
+ try {
518
632
  // Stop any compressed recording first
519
633
  if (
520
634
  this.compressedMediaRecorder &&
@@ -529,15 +643,24 @@ export class WebRecorder {
529
643
  await new Promise((resolve) => setTimeout(resolve, 100))
530
644
  }
531
645
 
532
- // Return the compressed blob if available
646
+ // Create uncompressed WAV file from the PCM data
647
+ let uncompressedBlob: Blob | undefined
648
+
649
+ // Only create WAV if we have PCM data
650
+ if (this.pcmData && this.pcmData.length > 0) {
651
+ uncompressedBlob =
652
+ (await this.createWavFromPcmData()) || undefined
653
+ }
654
+
655
+ // Return the compressed and/or uncompressed blobs if available
533
656
  return {
534
- pcmData: new Float32Array(), // Return empty array since we're streaming
535
657
  compressedBlob:
536
658
  this.compressedChunks.length > 0
537
659
  ? new Blob(this.compressedChunks, {
538
660
  type: 'audio/webm;codecs=opus',
539
661
  })
540
662
  : undefined,
663
+ uncompressedBlob,
541
664
  }
542
665
  } finally {
543
666
  this.cleanup()
@@ -545,6 +668,8 @@ export class WebRecorder {
545
668
  this.compressedChunks = []
546
669
  this.compressedSize = 0
547
670
  this.pendingCompressedChunk = null
671
+ this.pcmData = null
672
+ this.totalSampleCount = 0
548
673
  }
549
674
  }
550
675
 
@@ -212,8 +212,7 @@ export function useAudioRecorder({
212
212
  const maxDuration = visualizationDuration
213
213
 
214
214
  logger?.debug(
215
- `[handleAudioAnalysis] Received audio analysis: maxDuration=${maxDuration} analysis.dataPoints=${analysis.dataPoints.length} analysisData.dataPoints=${savedAnalysisData.dataPoints.length}`,
216
- analysis
215
+ `[handleAudioAnalysis] Received audio analysis: maxDuration=${maxDuration} analysis.dataPoints=${analysis.dataPoints.length} analysisData.dataPoints=${savedAnalysisData.dataPoints.length}`
217
216
  )
218
217
 
219
218
  // Combine data points
@@ -12,6 +12,8 @@ export interface WavHeaderOptions {
12
12
  numChannels: number
13
13
  /** The bit depth of the audio (e.g., 16, 24, or 32). */
14
14
  bitDepth: number
15
+ /** Whether the audio data is in float format (only applies to 32-bit) */
16
+ isFloat?: boolean
15
17
  }
16
18
 
17
19
  /**
@@ -30,30 +32,17 @@ export interface WavHeaderOptions {
30
32
  * @returns An ArrayBuffer containing the WAV header, or the header combined with the provided audio data.
31
33
  *
32
34
  * @throws {Error} Throws an error if the provided options are invalid or if the buffer is too small.
33
- *
34
- * @example
35
- * // Create a standalone WAV header
36
- * const header = writeWavHeader({
37
- * sampleRate: 44100,
38
- * numChannels: 2,
39
- * bitDepth: 16
40
- * });
41
- *
42
- * @example
43
- * // Create a WAV header and combine it with audio data
44
- * const completeWav = writeWavHeader({
45
- * buffer: audioData,
46
- * sampleRate: 44100,
47
- * numChannels: 2,
48
- * bitDepth: 16
49
- * });
50
35
  */
51
36
  export const writeWavHeader = ({
52
37
  buffer,
53
38
  sampleRate,
54
39
  numChannels,
55
40
  bitDepth,
41
+ isFloat = bitDepth === 32, // Default to float for 32-bit
56
42
  }: WavHeaderOptions): ArrayBuffer => {
43
+ // For 32-bit float, we use format 3, otherwise format 1 for PCM
44
+ const audioFormat = isFloat ? 3 : 1 // 3 = IEEE float, 1 = PCM
45
+
57
46
  const bytesPerSample = bitDepth / 8
58
47
  const blockAlign = numChannels * bytesPerSample
59
48
  const byteRate = sampleRate * blockAlign
@@ -67,22 +56,30 @@ export const writeWavHeader = ({
67
56
 
68
57
  // Function to write or update the header
69
58
  const writeHeader = (view: DataView, dataSize: number = 0xffffffff) => {
59
+ // RIFF chunk descriptor
70
60
  writeString(view, 0, 'RIFF') // ChunkID
71
- view.setUint32(4, 36 + dataSize, true) // ChunkSize
61
+ view.setUint32(4, 36 + dataSize, true) // ChunkSize: 4 + (8 + 16) + (8 + dataSize)
72
62
  writeString(view, 8, 'WAVE') // Format
63
+
64
+ // "fmt " sub-chunk
73
65
  writeString(view, 12, 'fmt ') // Subchunk1ID
74
- view.setUint32(16, 16, true) // Subchunk1Size (16 for PCM)
75
- view.setUint16(20, bitDepth === 32 ? 3 : 1, true) // AudioFormat (3 for float, 1 for PCM)
66
+ view.setUint32(16, 16, true) // Subchunk1Size (16 for PCM/Float)
67
+ view.setUint16(20, audioFormat, true) // AudioFormat (3 for float, 1 for PCM)
76
68
  view.setUint16(22, numChannels, true) // NumChannels
77
69
  view.setUint32(24, sampleRate, true) // SampleRate
78
- view.setUint32(28, byteRate, true) // ByteRate
79
- view.setUint16(32, blockAlign, true) // BlockAlign
70
+ view.setUint32(28, byteRate, true) // ByteRate = SampleRate * NumChannels * BitsPerSample/8
71
+ view.setUint16(32, blockAlign, true) // BlockAlign = NumChannels * BitsPerSample/8
80
72
  view.setUint16(34, bitDepth, true) // BitsPerSample
73
+
74
+ // "data" sub-chunk
81
75
  writeString(view, 36, 'data') // Subchunk2ID
82
- view.setUint32(40, dataSize, true) // Subchunk2Size
76
+ view.setUint32(40, dataSize, true) // Subchunk2Size = NumSamples * NumChannels * BitsPerSample/8
83
77
  }
84
78
 
85
79
  if (buffer) {
80
+ // Handle existing buffer
81
+
82
+ // Check for minimum size
86
83
  if (buffer.byteLength < 44) {
87
84
  throw new Error('Buffer is too small to contain a valid WAV header')
88
85
  }
@@ -97,15 +94,19 @@ export const writeWavHeader = ({
97
94
  writeHeader(view, buffer.byteLength - 44)
98
95
  return buffer
99
96
  } else {
100
- // Combine the new header with the existing buffer
97
+ // Create a new buffer with header + data
101
98
  const newBuffer = new ArrayBuffer(44 + buffer.byteLength)
102
99
  const newView = new DataView(newBuffer)
100
+
101
+ // Write header to new buffer
103
102
  writeHeader(newView, buffer.byteLength)
103
+
104
+ // Copy audio data after header
104
105
  new Uint8Array(newBuffer).set(new Uint8Array(buffer), 44)
105
106
  return newBuffer
106
107
  }
107
108
  } else {
108
- // Create a standalone header
109
+ // Create standalone header
109
110
  const headerBuffer = new ArrayBuffer(44)
110
111
  const view = new DataView(headerBuffer)
111
112
  writeHeader(view)