@siteed/audio-studio 3.0.2 → 3.0.4

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 (89) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +41 -35
  3. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +500 -479
  4. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  5. package/build/cjs/AudioAnalysis/audioFeaturesWasm.js +10 -7
  6. package/build/cjs/AudioAnalysis/audioFeaturesWasm.js.map +1 -1
  7. package/build/cjs/AudioAnalysis/audioFeaturesWasm.web.js +78 -97
  8. package/build/cjs/AudioAnalysis/audioFeaturesWasm.web.js.map +1 -1
  9. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +15 -12
  10. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  11. package/build/cjs/AudioAnalysis/extractAudioData.js +144 -2
  12. package/build/cjs/AudioAnalysis/extractAudioData.js.map +1 -1
  13. package/build/cjs/AudioAnalysis/melSpectrogramWasm.web.js +9 -56
  14. package/build/cjs/AudioAnalysis/melSpectrogramWasm.web.js.map +1 -1
  15. package/build/cjs/AudioAnalysis/wasmConfig.js +4 -4
  16. package/build/cjs/AudioAnalysis/wasmConfig.js.map +1 -1
  17. package/build/cjs/AudioAnalysis/wasmLoader.web.js +79 -0
  18. package/build/cjs/AudioAnalysis/wasmLoader.web.js.map +1 -0
  19. package/build/cjs/AudioStudioModule.js +4 -599
  20. package/build/cjs/AudioStudioModule.js.map +1 -1
  21. package/build/cjs/trimAudio.js +227 -0
  22. package/build/cjs/trimAudio.js.map +1 -1
  23. package/build/cjs/utils/encodeCompressedAudio.web.js +65 -0
  24. package/build/cjs/utils/encodeCompressedAudio.web.js.map +1 -0
  25. package/build/cjs/utils/resampleAudioBuffer.web.js +25 -0
  26. package/build/cjs/utils/resampleAudioBuffer.web.js.map +1 -0
  27. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  28. package/build/esm/AudioAnalysis/audioFeaturesWasm.js +8 -5
  29. package/build/esm/AudioAnalysis/audioFeaturesWasm.js.map +1 -1
  30. package/build/esm/AudioAnalysis/audioFeaturesWasm.web.js +76 -62
  31. package/build/esm/AudioAnalysis/audioFeaturesWasm.web.js.map +1 -1
  32. package/build/esm/AudioAnalysis/extractAudioAnalysis.js +15 -12
  33. package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  34. package/build/esm/AudioAnalysis/extractAudioData.js +144 -2
  35. package/build/esm/AudioAnalysis/extractAudioData.js.map +1 -1
  36. package/build/esm/AudioAnalysis/melSpectrogramWasm.web.js +9 -23
  37. package/build/esm/AudioAnalysis/melSpectrogramWasm.web.js.map +1 -1
  38. package/build/esm/AudioAnalysis/wasmConfig.js +4 -4
  39. package/build/esm/AudioAnalysis/wasmConfig.js.map +1 -1
  40. package/build/esm/AudioAnalysis/wasmLoader.web.js +43 -0
  41. package/build/esm/AudioAnalysis/wasmLoader.web.js.map +1 -0
  42. package/build/esm/AudioStudioModule.js +4 -596
  43. package/build/esm/AudioStudioModule.js.map +1 -1
  44. package/build/esm/trimAudio.js +227 -0
  45. package/build/esm/trimAudio.js.map +1 -1
  46. package/build/esm/utils/encodeCompressedAudio.web.js +62 -0
  47. package/build/esm/utils/encodeCompressedAudio.web.js.map +1 -0
  48. package/build/esm/utils/resampleAudioBuffer.web.js +22 -0
  49. package/build/esm/utils/resampleAudioBuffer.web.js.map +1 -0
  50. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +11 -0
  51. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  52. package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts +5 -9
  53. package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts.map +1 -1
  54. package/build/types/AudioAnalysis/audioFeaturesWasm.web.d.ts +35 -16
  55. package/build/types/AudioAnalysis/audioFeaturesWasm.web.d.ts.map +1 -1
  56. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
  57. package/build/types/AudioAnalysis/extractAudioData.d.ts +2 -2
  58. package/build/types/AudioAnalysis/extractAudioData.d.ts.map +1 -1
  59. package/build/types/AudioAnalysis/melSpectrogramWasm.web.d.ts.map +1 -1
  60. package/build/types/AudioAnalysis/wasmLoader.web.d.ts +3 -0
  61. package/build/types/AudioAnalysis/wasmLoader.web.d.ts.map +1 -0
  62. package/build/types/AudioStudioModule.d.ts.map +1 -1
  63. package/build/types/trimAudio.d.ts.map +1 -1
  64. package/build/types/utils/encodeCompressedAudio.web.d.ts +10 -0
  65. package/build/types/utils/encodeCompressedAudio.web.d.ts.map +1 -0
  66. package/build/types/utils/resampleAudioBuffer.web.d.ts +2 -0
  67. package/build/types/utils/resampleAudioBuffer.web.d.ts.map +1 -0
  68. package/ios/AudioStreamManager.swift +135 -89
  69. package/ios/AudioStudioModule.swift +239 -216
  70. package/package.json +1 -1
  71. package/src/AudioAnalysis/AudioAnalysis.types.ts +12 -0
  72. package/src/AudioAnalysis/audioFeaturesWasm.ts +17 -22
  73. package/src/AudioAnalysis/audioFeaturesWasm.web.ts +102 -94
  74. package/src/AudioAnalysis/extractAudioAnalysis.ts +23 -20
  75. package/src/AudioAnalysis/extractAudioData.ts +186 -4
  76. package/src/AudioAnalysis/melSpectrogramWasm.web.ts +10 -27
  77. package/src/AudioAnalysis/wasmConfig.ts +4 -4
  78. package/src/AudioAnalysis/wasmLoader.web.ts +53 -0
  79. package/src/AudioStudioModule.ts +6 -854
  80. package/src/trimAudio.ts +351 -0
  81. package/src/utils/encodeCompressedAudio.web.ts +78 -0
  82. package/src/utils/resampleAudioBuffer.web.ts +39 -0
  83. package/build/cjs/AudioAnalysis/extractWaveform.js +0 -18
  84. package/build/cjs/AudioAnalysis/extractWaveform.js.map +0 -1
  85. package/build/esm/AudioAnalysis/extractWaveform.js +0 -11
  86. package/build/esm/AudioAnalysis/extractWaveform.js.map +0 -1
  87. package/build/types/AudioAnalysis/extractWaveform.d.ts +0 -8
  88. package/build/types/AudioAnalysis/extractWaveform.d.ts.map +0 -1
  89. package/src/AudioAnalysis/extractWaveform.ts +0 -22
@@ -1,17 +1,7 @@
1
1
  import { requireNativeModule } from 'expo-modules-core'
2
2
  import { Platform } from 'react-native'
3
3
 
4
- import {
5
- ExtractAudioDataOptions,
6
- ExtractedAudioData,
7
- BitDepth,
8
- TrimAudioOptions,
9
- TrimAudioResult,
10
- } from './AudioStudio.types'
11
4
  import { AudioStudioWeb, AudioStudioWebProps } from './AudioStudio.web'
12
- import { processAudioBuffer } from './utils/audioProcessing'
13
- import crc32 from './utils/crc32'
14
- import { writeWavHeader } from './utils/writeWavHeader'
15
5
 
16
6
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
7
  let AudioStudioModule: any
@@ -20,9 +10,7 @@ if (Platform.OS === 'web') {
20
10
  let instance: AudioStudioWeb | null = null
21
11
 
22
12
  AudioStudioModule = (webProps: AudioStudioWebProps) => {
23
- if (!instance) {
24
- instance = new AudioStudioWeb(webProps)
25
- }
13
+ instance ??= new AudioStudioWeb(webProps)
26
14
  return instance
27
15
  }
28
16
  AudioStudioModule.requestPermissionsAsync = async () => {
@@ -79,692 +67,14 @@ if (Platform.OS === 'web') {
79
67
  return await AudioStudioModule.requestPermissionsAsync()
80
68
  }
81
69
  }
82
- AudioStudioModule.extractAudioData = async (
83
- options: ExtractAudioDataOptions
84
- ): Promise<ExtractedAudioData> => {
85
- try {
86
- const {
87
- fileUri,
88
- position,
89
- length,
90
- startTimeMs,
91
- endTimeMs,
92
- decodingOptions,
93
- includeNormalizedData,
94
- includeBase64Data,
95
- includeWavHeader = false,
96
- logger,
97
- } = options
98
-
99
- logger?.debug('EXTRACT AUDIO - Step 1: Initial request', {
100
- fileUri,
101
- extractionParams: {
102
- position,
103
- length,
104
- startTimeMs,
105
- endTimeMs,
106
- },
107
- decodingOptions: {
108
- targetSampleRate:
109
- decodingOptions?.targetSampleRate ?? 16000,
110
- targetChannels: decodingOptions?.targetChannels ?? 1,
111
- targetBitDepth: decodingOptions?.targetBitDepth ?? 16,
112
- normalizeAudio: decodingOptions?.normalizeAudio ?? false,
113
- },
114
- outputOptions: {
115
- includeNormalizedData,
116
- includeBase64Data,
117
- includeWavHeader,
118
- },
119
- })
120
-
121
- // Process the audio using shared helper function
122
- const processedBuffer = await processAudioBuffer({
123
- fileUri,
124
- targetSampleRate: decodingOptions?.targetSampleRate ?? 16000,
125
- targetChannels: decodingOptions?.targetChannels ?? 1,
126
- normalizeAudio: decodingOptions?.normalizeAudio ?? false,
127
- position,
128
- length,
129
- startTimeMs,
130
- endTimeMs,
131
- logger,
132
- })
133
-
134
- logger?.debug('EXTRACT AUDIO - Step 2: Audio processing complete', {
135
- processedData: {
136
- samples: processedBuffer.samples,
137
- sampleRate: processedBuffer.sampleRate,
138
- channels: processedBuffer.channels,
139
- durationMs: processedBuffer.durationMs,
140
- },
141
- })
142
-
143
- const channelData = processedBuffer.channelData
144
- const bitDepth = (decodingOptions?.targetBitDepth ?? 16) as BitDepth
145
- const bytesPerSample = bitDepth / 8
146
- const numSamples = processedBuffer.samples
147
-
148
- logger?.debug('EXTRACT AUDIO - Step 3: PCM conversion setup', {
149
- channelData: {
150
- length: channelData.length,
151
- first: channelData[0],
152
- last: channelData[channelData.length - 1],
153
- },
154
- calculation: {
155
- bitDepth,
156
- bytesPerSample,
157
- numSamples,
158
- expectedBytes: numSamples * bytesPerSample,
159
- },
160
- })
161
-
162
- // Create PCM data with correct length based on original byte length
163
- const pcmData = new Uint8Array(numSamples * bytesPerSample)
164
- let offset = 0
165
-
166
- // Convert Float32 samples to PCM format
167
- for (let i = 0; i < numSamples; i++) {
168
- const sample = channelData[i]
169
- const value = Math.max(-1, Math.min(1, sample))
170
- // Convert to 16-bit signed integer
171
- let intValue = Math.round(value * 32767)
172
-
173
- // Handle negative values correctly
174
- if (intValue < 0) {
175
- intValue = 65536 + intValue
176
- }
177
-
178
- // Write as little-endian
179
- pcmData[offset++] = intValue & 255 // Low byte
180
- pcmData[offset++] = (intValue >> 8) & 255 // High byte
181
- }
182
-
183
- const durationMs = Math.round(
184
- (numSamples / processedBuffer.sampleRate) * 1000
185
- )
186
-
187
- logger?.debug('EXTRACT AUDIO - Step 4: Final output', {
188
- pcmData: {
189
- length: pcmData.length,
190
- first: pcmData[0],
191
- last: pcmData[pcmData.length - 1],
192
- },
193
- timing: {
194
- numSamples,
195
- sampleRate: processedBuffer.sampleRate,
196
- durationMs,
197
- shouldBe3000ms: endTimeMs
198
- ? endTimeMs - (startTimeMs ?? 0) === 3000
199
- : undefined,
200
- },
201
- })
202
-
203
- const result: ExtractedAudioData = {
204
- pcmData: new Uint8Array(pcmData.buffer),
205
- sampleRate: processedBuffer.sampleRate,
206
- channels: processedBuffer.channels,
207
- bitDepth,
208
- durationMs,
209
- format: `pcm_${bitDepth}bit` as const,
210
- samples: numSamples,
211
- }
212
-
213
- // Add WAV header if requested
214
- if (includeWavHeader) {
215
- logger?.debug('EXTRACT AUDIO - Step 4: Adding WAV header', {
216
- originalLength: pcmData.length,
217
- newLength: result.pcmData.length,
218
- firstBytes: Array.from(result.pcmData.slice(0, 44)), // WAV header is 44 bytes
219
- })
220
- const wavBuffer = writeWavHeader({
221
- buffer: pcmData.buffer.slice(0, pcmData.length),
222
- sampleRate: processedBuffer.sampleRate,
223
- numChannels: processedBuffer.channels,
224
- bitDepth,
225
- })
226
- result.pcmData = new Uint8Array(wavBuffer)
227
- result.hasWavHeader = true
228
- }
229
-
230
- if (includeNormalizedData) {
231
- // // Simple approach: Create normalized data directly from the PCM data
232
- // // Just convert to -1 to 1 range without any amplification
233
- // const normalizedData = new Float32Array(numSamples)
234
-
235
- // // Convert the PCM data to float values
236
- // for (let i = 0; i < numSamples; i++) {
237
- // // Get the 16-bit PCM value (little endian)
238
- // const lowByte = pcmData[i * 2]
239
- // const highByte = pcmData[i * 2 + 1]
240
- // const pcmValue = (highByte << 8) | lowByte
241
-
242
- // // Convert to signed 16-bit value
243
- // const signedValue =
244
- // pcmValue > 32767 ? pcmValue - 65536 : pcmValue
245
-
246
- // // Normalize to float between -1 and 1
247
- // normalizedData[i] = signedValue / 32768.0
248
- // }
249
- // Store the normalized data in the result
250
- result.normalizedData = channelData
251
- }
252
-
253
- if (includeBase64Data) {
254
- // Convert the PCM data to a base64 string
255
- const binary = Array.from(new Uint8Array(pcmData.buffer))
256
- .map((b) => String.fromCharCode(b))
257
- .join('')
258
- result.base64Data = btoa(binary)
259
- }
260
-
261
- if (options.computeChecksum) {
262
- result.checksum = crc32.buf(pcmData)
263
- }
264
-
265
- logger?.debug('EXTRACT AUDIO - Step 3: PCM conversion complete', {
266
- pcmStats: {
267
- length: pcmData.length,
268
- bytesPerSample,
269
- totalSamples: numSamples,
270
- firstBytes: Array.from(pcmData.slice(0, 16)),
271
- lastBytes: Array.from(pcmData.slice(-16)),
272
- },
273
- })
274
-
275
- return result
276
- } catch (error) {
277
- options.logger?.error('EXTRACT AUDIO - Error:', error)
278
- throw error
279
- }
280
- }
281
-
282
- AudioStudioModule.trimAudio = async (
283
- options: TrimAudioOptions
284
- ): Promise<TrimAudioResult> => {
285
- try {
286
- const startTime = performance.now()
287
- const {
288
- fileUri,
289
- mode = 'single',
290
- startTimeMs,
291
- endTimeMs,
292
- ranges,
293
- outputFileName,
294
- outputFormat,
295
- } = options
296
-
297
- // Validate inputs
298
- if (!fileUri) {
299
- throw new Error('fileUri is required')
300
- }
301
-
302
- if (
303
- mode === 'single' &&
304
- startTimeMs === undefined &&
305
- endTimeMs === undefined
306
- ) {
307
- throw new Error(
308
- 'At least one of startTimeMs or endTimeMs must be provided in single mode'
309
- )
310
- }
311
-
312
- if (
313
- (mode === 'keep' || mode === 'remove') &&
314
- (!ranges || ranges.length === 0)
315
- ) {
316
- throw new Error(
317
- 'ranges must be provided and non-empty for keep or remove modes'
318
- )
319
- }
320
-
321
- // Create AudioContext
322
- const audioContext = new (window.AudioContext ||
323
- (window as any).webkitAudioContext)()
324
-
325
- // First, load the entire audio file to get its properties
326
- const response = await fetch(fileUri)
327
- const arrayBuffer = await response.arrayBuffer()
328
- const originalAudioBuffer =
329
- await audioContext.decodeAudioData(arrayBuffer)
330
-
331
- // Get original audio properties
332
- const originalSampleRate = originalAudioBuffer.sampleRate
333
- const originalChannels = originalAudioBuffer.numberOfChannels
334
-
335
- // Add more detailed logging
336
- console.log(`Original audio details:`, {
337
- sampleRate: originalSampleRate,
338
- channels: originalChannels,
339
- duration: originalAudioBuffer.duration,
340
- length: originalAudioBuffer.length,
341
- // Log a few samples to verify content
342
- firstSamples: Array.from(
343
- originalAudioBuffer.getChannelData(0).slice(0, 5)
344
- ),
345
- })
346
-
347
- // Determine output format - use original values as defaults if not specified
348
- let format = outputFormat?.format || 'wav'
349
- const targetSampleRate =
350
- outputFormat?.sampleRate || originalSampleRate
351
- const targetChannels = outputFormat?.channels || originalChannels
352
- const targetBitDepth = outputFormat?.bitDepth || 16
353
-
354
- // Get file info from the URL
355
- const filename =
356
- outputFileName ||
357
- fileUri.split('/').pop() ||
358
- 'trimmed-audio.wav'
359
-
360
- // Process based on mode
361
- let resultBuffer: AudioBuffer
362
-
363
- // Report initial progress
364
- AudioStudioModule.sendEvent('TrimProgress', {
365
- progress: 10,
366
- })
367
-
368
- if (mode === 'single') {
369
- // Single mode: extract a single range
370
- // Use original sample rate and channels for extraction to preserve quality
371
- const { buffer } = await processAudioBuffer({
372
- fileUri,
373
- targetSampleRate, // Use the requested sample rate
374
- targetChannels,
375
- normalizeAudio: false,
376
- startTimeMs,
377
- endTimeMs,
378
- audioContext,
379
- })
380
-
381
- console.log(`Processed buffer details:`, {
382
- sampleRate: buffer.sampleRate,
383
- channels: buffer.numberOfChannels,
384
- duration: buffer.duration,
385
- length: buffer.length,
386
- // Log a few samples to verify content
387
- firstSamples: Array.from(
388
- buffer.getChannelData(0).slice(0, 5)
389
- ),
390
- })
391
-
392
- resultBuffer = buffer
393
-
394
- // If we need to change sample rate or channels, do it after extraction
395
- if (
396
- targetSampleRate !== originalSampleRate ||
397
- targetChannels !== originalChannels
398
- ) {
399
- console.log(
400
- `Resampling from ${originalSampleRate}Hz to ${targetSampleRate}Hz`
401
- )
402
- resultBuffer = await resampleAudioBuffer(
403
- audioContext,
404
- buffer,
405
- targetSampleRate,
406
- targetChannels
407
- )
408
- }
409
- } else {
410
- // For keep or remove modes
411
- const fullDuration = originalAudioBuffer.duration * 1000 // in ms
412
-
413
- type ProcessSegment = {
414
- startTimeMs: number
415
- endTimeMs: number
416
- }
417
-
418
- let segmentsToProcess: ProcessSegment[] = []
419
-
420
- if (mode === 'keep') {
421
- // For keep mode, use the ranges directly
422
- segmentsToProcess = ranges!
423
- } else {
424
- // mode === 'remove'
425
- // For remove mode, invert the ranges
426
- const sortedRanges = [...ranges!].sort(
427
- (a, b) => a.startTimeMs - b.startTimeMs
428
- )
429
-
430
- // Add segment from start to first range if needed
431
- if (
432
- sortedRanges.length > 0 &&
433
- sortedRanges[0].startTimeMs > 0
434
- ) {
435
- segmentsToProcess.push({
436
- startTimeMs: 0,
437
- endTimeMs: sortedRanges[0].startTimeMs,
438
- })
439
- }
440
-
441
- // Add segments between ranges
442
- for (let i = 0; i < sortedRanges.length - 1; i++) {
443
- segmentsToProcess.push({
444
- startTimeMs: sortedRanges[i].endTimeMs,
445
- endTimeMs: sortedRanges[i + 1].startTimeMs,
446
- })
447
- }
448
-
449
- // Add segment from last range to end if needed
450
- if (
451
- sortedRanges.length > 0 &&
452
- sortedRanges[sortedRanges.length - 1].endTimeMs <
453
- fullDuration
454
- ) {
455
- segmentsToProcess.push({
456
- startTimeMs:
457
- sortedRanges[sortedRanges.length - 1].endTimeMs,
458
- endTimeMs: fullDuration,
459
- })
460
- }
461
- }
462
-
463
- // Filter out empty or invalid segments
464
- segmentsToProcess = segmentsToProcess.filter(
465
- (segment) =>
466
- segment.startTimeMs < segment.endTimeMs &&
467
- segment.endTimeMs - segment.startTimeMs > 1
468
- ) // 1ms minimum
469
-
470
- if (segmentsToProcess.length === 0) {
471
- throw new Error(
472
- 'No valid segments to process after filtering ranges'
473
- )
474
- }
475
-
476
- // Process each segment using original sample rate and channels
477
- const segmentBuffers: AudioBuffer[] = []
478
-
479
- for (let i = 0; i < segmentsToProcess.length; i++) {
480
- const segment = segmentsToProcess[i]
481
-
482
- // Report progress for each segment
483
- AudioStudioModule.sendEvent('TrimProgress', {
484
- progress:
485
- 10 +
486
- Math.round((i / segmentsToProcess.length) * 40),
487
- })
488
-
489
- // Use processAudioBuffer to extract this segment
490
- const { buffer: segmentBuffer } = await processAudioBuffer({
491
- fileUri,
492
- targetSampleRate: originalSampleRate, // Use original sample rate
493
- targetChannels: originalChannels, // Use original channels
494
- normalizeAudio: false,
495
- startTimeMs: segment.startTimeMs,
496
- endTimeMs: segment.endTimeMs,
497
- audioContext,
498
- })
499
-
500
- segmentBuffers.push(segmentBuffer)
501
- }
502
-
503
- // Concatenate all segments
504
- const totalSamples = segmentBuffers.reduce(
505
- (sum, buffer) => sum + buffer.length,
506
- 0
507
- )
508
-
509
- // Create buffer with original properties first
510
- const concatenatedBuffer = audioContext.createBuffer(
511
- originalChannels,
512
- totalSamples,
513
- originalSampleRate
514
- )
515
-
516
- let offset = 0
517
- for (const segmentBuffer of segmentBuffers) {
518
- for (
519
- let channel = 0;
520
- channel < originalChannels;
521
- channel++
522
- ) {
523
- const outputData =
524
- concatenatedBuffer.getChannelData(channel)
525
- const segmentData =
526
- segmentBuffer.getChannelData(channel)
527
-
528
- for (let i = 0; i < segmentBuffer.length; i++) {
529
- outputData[offset + i] = segmentData[i]
530
- }
531
- }
532
- offset += segmentBuffer.length
533
- }
534
-
535
- resultBuffer = concatenatedBuffer
536
-
537
- // If we need to change sample rate or channels, do it after concatenation
538
- if (
539
- targetSampleRate !== originalSampleRate ||
540
- targetChannels !== originalChannels
541
- ) {
542
- console.log(
543
- `Resampling concatenated buffer from ${originalSampleRate}Hz to ${targetSampleRate}Hz`
544
- )
545
- resultBuffer = await resampleAudioBuffer(
546
- audioContext,
547
- concatenatedBuffer,
548
- targetSampleRate,
549
- targetChannels
550
- )
551
- }
552
- }
553
-
554
- // Report progress (50% - processing complete)
555
- AudioStudioModule.sendEvent('TrimProgress', {
556
- progress: 50,
557
- })
558
-
559
- // Encode the result based on the requested format
560
- let outputData: ArrayBuffer
561
- let outputMimeType: string
562
- let compressionInfo: any = null
563
-
564
- // Check if AAC was requested on web and show a warning
565
- if (format === 'aac' && Platform.OS === 'web') {
566
- console.warn(
567
- 'AAC format is not supported on web platforms. Falling back to OPUS format.'
568
- )
569
- format = 'opus'
570
- }
571
-
572
- if (format === 'wav') {
573
- // Create a properly interleaved buffer for WAV format
574
- // For WAV, we need to convert Float32Array to Int16Array (for 16-bit audio)
575
- const numSamples =
576
- resultBuffer.length * resultBuffer.numberOfChannels
577
- const interleavedData = new Int16Array(numSamples)
578
-
579
- // Log detailed information about the buffer before encoding
580
- console.log(`Creating WAV file:`, {
581
- bufferSampleRate: resultBuffer.sampleRate,
582
- bufferChannels: resultBuffer.numberOfChannels,
583
- bufferLength: resultBuffer.length,
584
- targetSampleRate,
585
- targetChannels,
586
- targetBitDepth,
587
- // Log a few samples to verify content
588
- firstSamples: Array.from(
589
- resultBuffer.getChannelData(0).slice(0, 5)
590
- ),
591
- })
592
-
593
- // Interleave channels properly
594
- for (let i = 0; i < resultBuffer.length; i++) {
595
- for (
596
- let channel = 0;
597
- channel < resultBuffer.numberOfChannels;
598
- channel++
599
- ) {
600
- // Convert float (-1.0 to 1.0) to int16 (-32768 to 32767)
601
- const floatSample =
602
- resultBuffer.getChannelData(channel)[i]
603
- // Clamp the value to -1.0 to 1.0
604
- const clampedSample = Math.max(
605
- -1.0,
606
- Math.min(1.0, floatSample)
607
- )
608
- // Convert to int16
609
- const intSample = Math.round(clampedSample * 32767)
610
- // Store in interleaved buffer
611
- interleavedData[
612
- i * resultBuffer.numberOfChannels + channel
613
- ] = intSample
614
- }
615
- }
616
-
617
- // Convert Int16Array to ArrayBuffer for WAV header
618
- const rawBuffer = interleavedData.buffer
619
-
620
- // IMPORTANT: Make sure we're using the ACTUAL sample rate of the buffer
621
- // not just what was requested in the options
622
- console.log(
623
- `Creating WAV with ${resultBuffer.numberOfChannels} channels at ${resultBuffer.sampleRate}Hz`
624
- )
625
-
626
- outputData = writeWavHeader({
627
- buffer: rawBuffer as ArrayBuffer,
628
- sampleRate: resultBuffer.sampleRate, // Use the actual buffer's sample rate
629
- numChannels: resultBuffer.numberOfChannels,
630
- bitDepth: targetBitDepth as BitDepth,
631
- })
632
- outputMimeType = 'audio/wav'
633
- } else if (format === 'opus' || format === 'aac') {
634
- try {
635
- // Try to use MediaRecorder for compressed formats
636
- const { data, bitrate } = await encodeCompressedAudio(
637
- resultBuffer,
638
- format,
639
- outputFormat?.bitrate
640
- )
641
-
642
- outputData = data
643
- outputMimeType =
644
- format === 'opus' ? 'audio/webm' : 'audio/aac'
645
- compressionInfo = {
646
- format,
647
- bitrate,
648
- size: data.byteLength,
649
- }
650
- } catch (error) {
651
- console.warn(
652
- `Failed to encode to ${format}, falling back to WAV: ${error}`
653
- )
654
-
655
- // Same WAV encoding as above
656
- const wavData = new Float32Array(
657
- resultBuffer.length * resultBuffer.numberOfChannels
658
- )
659
-
660
- for (let i = 0; i < resultBuffer.length; i++) {
661
- for (
662
- let channel = 0;
663
- channel < resultBuffer.numberOfChannels;
664
- channel++
665
- ) {
666
- wavData[
667
- i * resultBuffer.numberOfChannels + channel
668
- ] = resultBuffer.getChannelData(channel)[i]
669
- }
670
- }
671
-
672
- outputData = writeWavHeader({
673
- buffer: wavData.buffer as ArrayBuffer,
674
- sampleRate: resultBuffer.sampleRate,
675
- numChannels: resultBuffer.numberOfChannels,
676
- bitDepth: targetBitDepth as BitDepth,
677
- })
678
- outputMimeType = 'audio/wav'
679
- }
680
- } else {
681
- // Default to WAV for unsupported formats
682
- console.warn(
683
- `Format ${format} not supported on web, using WAV instead`
684
- )
685
-
686
- // Same WAV encoding as above
687
- const wavData = new Float32Array(
688
- resultBuffer.length * resultBuffer.numberOfChannels
689
- )
690
-
691
- for (let i = 0; i < resultBuffer.length; i++) {
692
- for (
693
- let channel = 0;
694
- channel < resultBuffer.numberOfChannels;
695
- channel++
696
- ) {
697
- wavData[i * resultBuffer.numberOfChannels + channel] =
698
- resultBuffer.getChannelData(channel)[i]
699
- }
700
- }
701
-
702
- outputData = writeWavHeader({
703
- buffer: wavData.buffer as ArrayBuffer,
704
- sampleRate: resultBuffer.sampleRate,
705
- numChannels: resultBuffer.numberOfChannels,
706
- bitDepth: targetBitDepth as BitDepth,
707
- })
708
- outputMimeType = 'audio/wav'
709
- }
710
-
711
- // Report progress (90% - encoding complete)
712
- AudioStudioModule.sendEvent('TrimProgress', {
713
- progress: 90,
714
- })
715
-
716
- // Create a blob and URL for the result
717
- const blob = new Blob([outputData], { type: outputMimeType })
718
- const outputUri = URL.createObjectURL(blob)
719
-
720
- // Calculate processing time
721
- const processingTimeMs = performance.now() - startTime
722
-
723
- // Report progress (100% - complete)
724
- AudioStudioModule.sendEvent('TrimProgress', {
725
- progress: 100,
726
- })
727
-
728
- // Create result object
729
- const result: TrimAudioResult = {
730
- uri: outputUri,
731
- filename,
732
- durationMs: Math.round(resultBuffer.duration * 1000),
733
- size: outputData.byteLength,
734
- sampleRate: resultBuffer.sampleRate,
735
- channels: resultBuffer.numberOfChannels,
736
- bitDepth: targetBitDepth,
737
- mimeType: outputMimeType,
738
- processingInfo: {
739
- durationMs: processingTimeMs,
740
- },
741
- }
742
-
743
- // Add compression info if available
744
- if (compressionInfo) {
745
- result.compression = compressionInfo
746
- }
747
-
748
- return result
749
- } catch (error) {
750
- console.error('Error in trimAudio:', error)
751
- throw error
752
- }
753
- }
754
-
755
70
  // Add a sendEvent method for web
756
71
  AudioStudioModule.sendEvent = (eventName: string, params: any) => {
757
72
  // This will be picked up by the LegacyEventEmitter in trimAudio.ts
758
- if (
759
- AudioStudioModule.listeners &&
760
- AudioStudioModule.listeners[eventName]
761
- ) {
762
- AudioStudioModule.listeners[eventName].forEach(
763
- (listener: Function) => {
764
- listener(params)
765
- }
766
- )
767
- }
73
+ AudioStudioModule.listeners?.[eventName]?.forEach(
74
+ (listener: Function) => {
75
+ listener(params)
76
+ }
77
+ )
768
78
  }
769
79
 
770
80
  // Initialize listeners object
@@ -819,164 +129,6 @@ if (Platform.OS === 'web') {
819
129
  }
820
130
  }
821
131
 
822
- // Move the encodeCompressedAudio function outside the if block to fix the ESLint error
823
- async function encodeCompressedAudio(
824
- buffer: AudioBuffer,
825
- format: 'opus' | 'aac',
826
- bitrate?: number
827
- ): Promise<{ data: ArrayBuffer; bitrate: number }> {
828
- return new Promise((resolve, reject) => {
829
- try {
830
- // On web, always use opus if aac is requested
831
- const actualFormat =
832
- Platform.OS === 'web' && format === 'aac' ? 'opus' : format
833
-
834
- // Check if MediaRecorder supports the requested format
835
- const mimeType =
836
- actualFormat === 'opus' ? 'audio/webm;codecs=opus' : 'audio/aac'
837
- if (!MediaRecorder.isTypeSupported(mimeType)) {
838
- throw new Error(`MediaRecorder does not support ${mimeType}`)
839
- }
840
-
841
- // Create a new AudioContext and source
842
- const ctx = new (window.AudioContext ||
843
- (window as any).webkitAudioContext)()
844
- const source = ctx.createBufferSource()
845
- source.buffer = buffer
846
-
847
- // Create a MediaStreamDestination to capture the audio
848
- const destination = ctx.createMediaStreamDestination()
849
- source.connect(destination)
850
-
851
- // Create a MediaRecorder with the requested format
852
- const recorder = new MediaRecorder(destination.stream, {
853
- mimeType,
854
- audioBitsPerSecond:
855
- bitrate || (actualFormat === 'opus' ? 32000 : 64000),
856
- })
857
-
858
- const chunks: Blob[] = []
859
-
860
- recorder.ondataavailable = (e) => {
861
- if (e.data.size > 0) {
862
- chunks.push(e.data)
863
- }
864
- }
865
-
866
- recorder.onstop = async () => {
867
- try {
868
- const blob = new Blob(chunks, { type: mimeType })
869
- const arrayBuffer = await blob.arrayBuffer()
870
-
871
- // Get the actual bitrate used
872
- const actualBitrate = Math.round(
873
- (arrayBuffer.byteLength * 8) / buffer.duration
874
- )
875
-
876
- resolve({
877
- data: arrayBuffer,
878
- bitrate: actualBitrate / 1000, // Convert to kbps
879
- })
880
-
881
- // Clean up
882
- ctx.close()
883
- } catch (error) {
884
- reject(error)
885
- }
886
- }
887
-
888
- // Start recording and playback
889
- recorder.start()
890
- source.start(0)
891
-
892
- // Stop recording when the buffer finishes playing
893
- setTimeout(() => {
894
- recorder.stop()
895
- source.stop()
896
- }, buffer.duration * 1000)
897
- } catch (error) {
898
- reject(error)
899
- }
900
- })
901
- }
902
-
903
- // Improved resampleAudioBuffer function
904
- async function resampleAudioBuffer(
905
- context: AudioContext,
906
- buffer: AudioBuffer,
907
- targetSampleRate: number,
908
- targetChannels: number
909
- ): Promise<AudioBuffer> {
910
- // If no change needed, return the original buffer
911
- if (
912
- buffer.sampleRate === targetSampleRate &&
913
- buffer.numberOfChannels === targetChannels
914
- ) {
915
- return buffer
916
- }
917
-
918
- console.log(
919
- `Resampling: ${buffer.sampleRate}Hz → ${targetSampleRate}Hz, ${buffer.numberOfChannels} → ${targetChannels} channels`
920
- )
921
-
922
- // Calculate the new length based on the sample rate change
923
- const newLength = Math.round(
924
- (buffer.length * targetSampleRate) / buffer.sampleRate
925
- )
926
-
927
- // Create an offline context for resampling
928
- const offlineContext = new OfflineAudioContext(
929
- targetChannels,
930
- newLength,
931
- targetSampleRate
932
- )
933
-
934
- // Create a source node
935
- const source = offlineContext.createBufferSource()
936
- source.buffer = buffer
937
-
938
- // If we need to change channel count
939
- if (buffer.numberOfChannels !== targetChannels) {
940
- if (targetChannels === 1 && buffer.numberOfChannels > 1) {
941
- // Downmix to mono
942
- const merger = offlineContext.createChannelMerger(1)
943
-
944
- // Create a gain node to reduce volume when downmixing to prevent clipping
945
- const gainNode = offlineContext.createGain()
946
- gainNode.gain.value = 1.0 / buffer.numberOfChannels
947
-
948
- source.connect(gainNode)
949
- gainNode.connect(merger)
950
- merger.connect(offlineContext.destination)
951
- } else if (targetChannels === 2 && buffer.numberOfChannels === 1) {
952
- // Upmix mono to stereo (duplicate the channel)
953
- const splitter = offlineContext.createChannelSplitter(1)
954
- const merger = offlineContext.createChannelMerger(2)
955
-
956
- source.connect(splitter)
957
- splitter.connect(merger, 0, 0)
958
- splitter.connect(merger, 0, 1)
959
- merger.connect(offlineContext.destination)
960
- } else {
961
- // For other cases, just connect and let the system handle it
962
- source.connect(offlineContext.destination)
963
- }
964
- } else {
965
- // No channel conversion needed
966
- source.connect(offlineContext.destination)
967
- }
968
-
969
- // Start rendering
970
- source.start(0)
971
- const resampledBuffer = await offlineContext.startRendering()
972
-
973
- console.log(
974
- `Resampling complete: ${resampledBuffer.length} samples at ${resampledBuffer.sampleRate}Hz`
975
- )
976
-
977
- return resampledBuffer
978
- }
979
-
980
132
  if (Platform.OS !== 'web') {
981
133
  AudioStudioModule = requireNativeModule('AudioStudio')
982
134
  }