@siteed/audio-studio 3.0.2 → 3.0.3
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.
- package/CHANGELOG.md +9 -1
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +7 -1
- package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/cjs/AudioAnalysis/audioFeaturesWasm.js +10 -7
- package/build/cjs/AudioAnalysis/audioFeaturesWasm.js.map +1 -1
- package/build/cjs/AudioAnalysis/audioFeaturesWasm.web.js +78 -97
- package/build/cjs/AudioAnalysis/audioFeaturesWasm.web.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +15 -12
- package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractAudioData.js +144 -2
- package/build/cjs/AudioAnalysis/extractAudioData.js.map +1 -1
- package/build/cjs/AudioAnalysis/melSpectrogramWasm.web.js +9 -56
- package/build/cjs/AudioAnalysis/melSpectrogramWasm.web.js.map +1 -1
- package/build/cjs/AudioAnalysis/wasmConfig.js +4 -4
- package/build/cjs/AudioAnalysis/wasmConfig.js.map +1 -1
- package/build/cjs/AudioAnalysis/wasmLoader.web.js +78 -0
- package/build/cjs/AudioAnalysis/wasmLoader.web.js.map +1 -0
- package/build/cjs/AudioStudioModule.js +4 -599
- package/build/cjs/AudioStudioModule.js.map +1 -1
- package/build/cjs/trimAudio.js +227 -0
- package/build/cjs/trimAudio.js.map +1 -1
- package/build/cjs/utils/encodeCompressedAudio.web.js +65 -0
- package/build/cjs/utils/encodeCompressedAudio.web.js.map +1 -0
- package/build/cjs/utils/resampleAudioBuffer.web.js +25 -0
- package/build/cjs/utils/resampleAudioBuffer.web.js.map +1 -0
- package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/esm/AudioAnalysis/audioFeaturesWasm.js +8 -5
- package/build/esm/AudioAnalysis/audioFeaturesWasm.js.map +1 -1
- package/build/esm/AudioAnalysis/audioFeaturesWasm.web.js +76 -62
- package/build/esm/AudioAnalysis/audioFeaturesWasm.web.js.map +1 -1
- package/build/esm/AudioAnalysis/extractAudioAnalysis.js +15 -12
- package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/esm/AudioAnalysis/extractAudioData.js +144 -2
- package/build/esm/AudioAnalysis/extractAudioData.js.map +1 -1
- package/build/esm/AudioAnalysis/melSpectrogramWasm.web.js +9 -23
- package/build/esm/AudioAnalysis/melSpectrogramWasm.web.js.map +1 -1
- package/build/esm/AudioAnalysis/wasmConfig.js +4 -4
- package/build/esm/AudioAnalysis/wasmConfig.js.map +1 -1
- package/build/esm/AudioAnalysis/wasmLoader.web.js +42 -0
- package/build/esm/AudioAnalysis/wasmLoader.web.js.map +1 -0
- package/build/esm/AudioStudioModule.js +4 -596
- package/build/esm/AudioStudioModule.js.map +1 -1
- package/build/esm/trimAudio.js +227 -0
- package/build/esm/trimAudio.js.map +1 -1
- package/build/esm/utils/encodeCompressedAudio.web.js +62 -0
- package/build/esm/utils/encodeCompressedAudio.web.js.map +1 -0
- package/build/esm/utils/resampleAudioBuffer.web.js +22 -0
- package/build/esm/utils/resampleAudioBuffer.web.js.map +1 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +11 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
- package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts +5 -9
- package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts.map +1 -1
- package/build/types/AudioAnalysis/audioFeaturesWasm.web.d.ts +35 -16
- package/build/types/AudioAnalysis/audioFeaturesWasm.web.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractAudioData.d.ts +2 -2
- package/build/types/AudioAnalysis/extractAudioData.d.ts.map +1 -1
- package/build/types/AudioAnalysis/melSpectrogramWasm.web.d.ts.map +1 -1
- package/build/types/AudioAnalysis/wasmLoader.web.d.ts +3 -0
- package/build/types/AudioAnalysis/wasmLoader.web.d.ts.map +1 -0
- package/build/types/AudioStudioModule.d.ts.map +1 -1
- package/build/types/trimAudio.d.ts.map +1 -1
- package/build/types/utils/encodeCompressedAudio.web.d.ts +10 -0
- package/build/types/utils/encodeCompressedAudio.web.d.ts.map +1 -0
- package/build/types/utils/resampleAudioBuffer.web.d.ts +2 -0
- package/build/types/utils/resampleAudioBuffer.web.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/AudioAnalysis/AudioAnalysis.types.ts +12 -0
- package/src/AudioAnalysis/audioFeaturesWasm.ts +17 -22
- package/src/AudioAnalysis/audioFeaturesWasm.web.ts +102 -94
- package/src/AudioAnalysis/extractAudioAnalysis.ts +23 -20
- package/src/AudioAnalysis/extractAudioData.ts +186 -4
- package/src/AudioAnalysis/melSpectrogramWasm.web.ts +10 -27
- package/src/AudioAnalysis/wasmConfig.ts +4 -4
- package/src/AudioAnalysis/wasmLoader.web.ts +48 -0
- package/src/AudioStudioModule.ts +6 -854
- package/src/trimAudio.ts +337 -0
- package/src/utils/encodeCompressedAudio.web.ts +78 -0
- package/src/utils/resampleAudioBuffer.web.ts +39 -0
- package/build/cjs/AudioAnalysis/extractWaveform.js +0 -18
- package/build/cjs/AudioAnalysis/extractWaveform.js.map +0 -1
- package/build/esm/AudioAnalysis/extractWaveform.js +0 -11
- package/build/esm/AudioAnalysis/extractWaveform.js.map +0 -1
- package/build/types/AudioAnalysis/extractWaveform.d.ts +0 -8
- package/build/types/AudioAnalysis/extractWaveform.d.ts.map +0 -1
- package/src/AudioAnalysis/extractWaveform.ts +0 -22
package/src/AudioStudioModule.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
}
|