@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/trimAudio.ts
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
1
1
|
import { LegacyEventEmitter, type EventSubscription } from 'expo-modules-core'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
BitDepth,
|
|
4
5
|
TrimAudioOptions,
|
|
5
6
|
TrimAudioResult,
|
|
6
7
|
TrimProgressEvent,
|
|
7
8
|
} from './AudioStudio.types'
|
|
8
9
|
import AudioStudioModule from './AudioStudioModule'
|
|
10
|
+
import { isWeb } from './constants'
|
|
11
|
+
import { processAudioBuffer } from './utils/audioProcessing'
|
|
9
12
|
import { cleanNativeOptions } from './utils/cleanNativeOptions'
|
|
13
|
+
import { encodeCompressedAudio } from './utils/encodeCompressedAudio.web'
|
|
14
|
+
import { resampleAudioBuffer } from './utils/resampleAudioBuffer.web'
|
|
15
|
+
import { writeWavHeader } from './utils/writeWavHeader'
|
|
10
16
|
|
|
11
17
|
// Create a single emitter instance
|
|
12
18
|
const emitter = new LegacyEventEmitter(AudioStudioModule)
|
|
13
19
|
|
|
20
|
+
function sliceAudioBuffer(
|
|
21
|
+
src: AudioBuffer,
|
|
22
|
+
ctx: AudioContext,
|
|
23
|
+
startMs: number,
|
|
24
|
+
endMs: number
|
|
25
|
+
): AudioBuffer {
|
|
26
|
+
const sr = src.sampleRate
|
|
27
|
+
const start = Math.floor((startMs / 1000) * sr)
|
|
28
|
+
const end = Math.min(Math.ceil((endMs / 1000) * sr), src.length)
|
|
29
|
+
const length = Math.max(0, end - start)
|
|
30
|
+
const out = ctx.createBuffer(src.numberOfChannels, length, sr)
|
|
31
|
+
for (let c = 0; c < src.numberOfChannels; c++) {
|
|
32
|
+
out.getChannelData(c).set(src.getChannelData(c).subarray(start, end))
|
|
33
|
+
}
|
|
34
|
+
return out
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function encodeBufferToWav(buffer: AudioBuffer, bitDepth: BitDepth): ArrayBuffer {
|
|
38
|
+
const { length, numberOfChannels, sampleRate } = buffer
|
|
39
|
+
const channels: Float32Array[] = []
|
|
40
|
+
for (let c = 0; c < numberOfChannels; c++) {
|
|
41
|
+
channels.push(buffer.getChannelData(c))
|
|
42
|
+
}
|
|
43
|
+
const interleavedData = new Int16Array(length * numberOfChannels)
|
|
44
|
+
for (let i = 0; i < length; i++) {
|
|
45
|
+
for (let c = 0; c < numberOfChannels; c++) {
|
|
46
|
+
const clamped = Math.max(-1, Math.min(1, channels[c][i]))
|
|
47
|
+
interleavedData[i * numberOfChannels + c] = Math.round(clamped * 32767)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return writeWavHeader({
|
|
51
|
+
buffer: interleavedData.buffer as ArrayBuffer,
|
|
52
|
+
sampleRate,
|
|
53
|
+
numChannels: numberOfChannels,
|
|
54
|
+
bitDepth,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
14
58
|
/**
|
|
15
59
|
* Trims an audio file based on the provided options.
|
|
16
60
|
*
|
|
@@ -52,6 +96,299 @@ export async function trimAudio(
|
|
|
52
96
|
)
|
|
53
97
|
}
|
|
54
98
|
|
|
99
|
+
if (isWeb) {
|
|
100
|
+
try {
|
|
101
|
+
const startTime = performance.now()
|
|
102
|
+
const {
|
|
103
|
+
fileUri,
|
|
104
|
+
startTimeMs,
|
|
105
|
+
endTimeMs,
|
|
106
|
+
ranges,
|
|
107
|
+
outputFileName,
|
|
108
|
+
outputFormat,
|
|
109
|
+
} = options
|
|
110
|
+
|
|
111
|
+
// Create AudioContext
|
|
112
|
+
const audioContext = new (window.AudioContext ||
|
|
113
|
+
(window as any).webkitAudioContext)()
|
|
114
|
+
|
|
115
|
+
// First, load the entire audio file to get its properties
|
|
116
|
+
const response = await fetch(fileUri)
|
|
117
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
118
|
+
const originalAudioBuffer =
|
|
119
|
+
await audioContext.decodeAudioData(arrayBuffer)
|
|
120
|
+
|
|
121
|
+
// Get original audio properties
|
|
122
|
+
const originalSampleRate = originalAudioBuffer.sampleRate
|
|
123
|
+
const originalChannels = originalAudioBuffer.numberOfChannels
|
|
124
|
+
|
|
125
|
+
// Determine output format - use original values as defaults if not specified
|
|
126
|
+
let format = outputFormat?.format || 'wav'
|
|
127
|
+
const targetSampleRate =
|
|
128
|
+
outputFormat?.sampleRate || originalSampleRate
|
|
129
|
+
const targetChannels = outputFormat?.channels || originalChannels
|
|
130
|
+
const targetBitDepth = outputFormat?.bitDepth || 16
|
|
131
|
+
|
|
132
|
+
// Get file info from the URL
|
|
133
|
+
const filename =
|
|
134
|
+
outputFileName ||
|
|
135
|
+
fileUri.split('/').pop() ||
|
|
136
|
+
'trimmed-audio.wav'
|
|
137
|
+
|
|
138
|
+
// Process based on mode
|
|
139
|
+
let resultBuffer: AudioBuffer
|
|
140
|
+
|
|
141
|
+
// Report initial progress
|
|
142
|
+
progressCallback?.({ progress: 10 })
|
|
143
|
+
|
|
144
|
+
if (mode === 'single') {
|
|
145
|
+
// Single mode: extract a single range
|
|
146
|
+
// Use original sample rate and channels for extraction to preserve quality
|
|
147
|
+
const { buffer } = await processAudioBuffer({
|
|
148
|
+
fileUri,
|
|
149
|
+
targetSampleRate, // Use the requested sample rate
|
|
150
|
+
targetChannels,
|
|
151
|
+
normalizeAudio: false,
|
|
152
|
+
startTimeMs,
|
|
153
|
+
endTimeMs,
|
|
154
|
+
audioContext,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
resultBuffer = buffer
|
|
158
|
+
|
|
159
|
+
// If we need to change sample rate or channels, do it after extraction
|
|
160
|
+
if (
|
|
161
|
+
targetSampleRate !== originalSampleRate ||
|
|
162
|
+
targetChannels !== originalChannels
|
|
163
|
+
) {
|
|
164
|
+
resultBuffer = await resampleAudioBuffer(
|
|
165
|
+
buffer,
|
|
166
|
+
targetSampleRate,
|
|
167
|
+
targetChannels
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
// For keep or remove modes
|
|
172
|
+
const fullDuration = originalAudioBuffer.duration * 1000 // in ms
|
|
173
|
+
|
|
174
|
+
type ProcessSegment = {
|
|
175
|
+
startTimeMs: number
|
|
176
|
+
endTimeMs: number
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let segmentsToProcess: ProcessSegment[] = []
|
|
180
|
+
|
|
181
|
+
if (mode === 'keep') {
|
|
182
|
+
// For keep mode, use the ranges directly
|
|
183
|
+
segmentsToProcess = ranges!
|
|
184
|
+
} else {
|
|
185
|
+
// mode === 'remove'
|
|
186
|
+
// For remove mode, invert the ranges
|
|
187
|
+
const sortedRanges = [...ranges!].sort(
|
|
188
|
+
(a, b) => a.startTimeMs - b.startTimeMs
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
// Add segment from start to first range if needed
|
|
192
|
+
if (
|
|
193
|
+
sortedRanges.length > 0 &&
|
|
194
|
+
sortedRanges[0].startTimeMs > 0
|
|
195
|
+
) {
|
|
196
|
+
segmentsToProcess.push({
|
|
197
|
+
startTimeMs: 0,
|
|
198
|
+
endTimeMs: sortedRanges[0].startTimeMs,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Add segments between ranges
|
|
203
|
+
for (let i = 0; i < sortedRanges.length - 1; i++) {
|
|
204
|
+
segmentsToProcess.push({
|
|
205
|
+
startTimeMs: sortedRanges[i].endTimeMs,
|
|
206
|
+
endTimeMs: sortedRanges[i + 1].startTimeMs,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Add segment from last range to end if needed
|
|
211
|
+
if (
|
|
212
|
+
sortedRanges.length > 0 &&
|
|
213
|
+
sortedRanges[sortedRanges.length - 1].endTimeMs <
|
|
214
|
+
fullDuration
|
|
215
|
+
) {
|
|
216
|
+
segmentsToProcess.push({
|
|
217
|
+
startTimeMs:
|
|
218
|
+
sortedRanges[sortedRanges.length - 1].endTimeMs,
|
|
219
|
+
endTimeMs: fullDuration,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Filter out empty or invalid segments
|
|
225
|
+
segmentsToProcess = segmentsToProcess.filter(
|
|
226
|
+
(segment) =>
|
|
227
|
+
segment.startTimeMs < segment.endTimeMs &&
|
|
228
|
+
segment.endTimeMs - segment.startTimeMs > 1
|
|
229
|
+
) // 1ms minimum
|
|
230
|
+
|
|
231
|
+
if (segmentsToProcess.length === 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
'No valid segments to process after filtering ranges'
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Process each segment using original sample rate and channels
|
|
238
|
+
const segmentBuffers: AudioBuffer[] = []
|
|
239
|
+
|
|
240
|
+
for (let i = 0; i < segmentsToProcess.length; i++) {
|
|
241
|
+
const segment = segmentsToProcess[i]
|
|
242
|
+
|
|
243
|
+
// Report progress for each segment
|
|
244
|
+
progressCallback?.({
|
|
245
|
+
progress:
|
|
246
|
+
10 +
|
|
247
|
+
Math.round((i / segmentsToProcess.length) * 40),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Slice from the already-decoded buffer (avoids N re-fetches)
|
|
251
|
+
const segmentBuffer = sliceAudioBuffer(
|
|
252
|
+
originalAudioBuffer,
|
|
253
|
+
audioContext,
|
|
254
|
+
segment.startTimeMs,
|
|
255
|
+
segment.endTimeMs
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
segmentBuffers.push(segmentBuffer)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Concatenate all segments
|
|
262
|
+
const totalSamples = segmentBuffers.reduce(
|
|
263
|
+
(sum, buffer) => sum + buffer.length,
|
|
264
|
+
0
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
// Create buffer with original properties first
|
|
268
|
+
const concatenatedBuffer = audioContext.createBuffer(
|
|
269
|
+
originalChannels,
|
|
270
|
+
totalSamples,
|
|
271
|
+
originalSampleRate
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
let offset = 0
|
|
275
|
+
for (const segmentBuffer of segmentBuffers) {
|
|
276
|
+
for (
|
|
277
|
+
let channel = 0;
|
|
278
|
+
channel < originalChannels;
|
|
279
|
+
channel++
|
|
280
|
+
) {
|
|
281
|
+
const outputData =
|
|
282
|
+
concatenatedBuffer.getChannelData(channel)
|
|
283
|
+
const segmentData =
|
|
284
|
+
segmentBuffer.getChannelData(channel)
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i < segmentBuffer.length; i++) {
|
|
287
|
+
outputData[offset + i] = segmentData[i]
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
offset += segmentBuffer.length
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
resultBuffer = concatenatedBuffer
|
|
294
|
+
|
|
295
|
+
// If we need to change sample rate or channels, do it after concatenation
|
|
296
|
+
if (
|
|
297
|
+
targetSampleRate !== originalSampleRate ||
|
|
298
|
+
targetChannels !== originalChannels
|
|
299
|
+
) {
|
|
300
|
+
resultBuffer = await resampleAudioBuffer(
|
|
301
|
+
concatenatedBuffer,
|
|
302
|
+
targetSampleRate,
|
|
303
|
+
targetChannels
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Report progress (50% - processing complete)
|
|
309
|
+
progressCallback?.({ progress: 50 })
|
|
310
|
+
|
|
311
|
+
// Encode the result based on the requested format
|
|
312
|
+
let outputData: ArrayBuffer
|
|
313
|
+
let outputMimeType: string
|
|
314
|
+
let compressionInfo: TrimAudioResult['compression'] = undefined
|
|
315
|
+
|
|
316
|
+
// AAC is not reliably supported in browsers; fall back to opus
|
|
317
|
+
if (format === 'aac') {
|
|
318
|
+
console.warn(
|
|
319
|
+
'AAC format is not supported on web platforms. Falling back to OPUS format.'
|
|
320
|
+
)
|
|
321
|
+
format = 'opus'
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (format === 'wav') {
|
|
325
|
+
outputData = encodeBufferToWav(resultBuffer, targetBitDepth as BitDepth)
|
|
326
|
+
outputMimeType = 'audio/wav'
|
|
327
|
+
} else if (format === 'opus') {
|
|
328
|
+
try {
|
|
329
|
+
const { data, bitrate } = await encodeCompressedAudio(
|
|
330
|
+
resultBuffer,
|
|
331
|
+
format,
|
|
332
|
+
outputFormat?.bitrate
|
|
333
|
+
)
|
|
334
|
+
outputData = data
|
|
335
|
+
outputMimeType = 'audio/webm'
|
|
336
|
+
compressionInfo = { format, bitrate, size: data.byteLength }
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.warn(
|
|
339
|
+
`Failed to encode to ${format}, falling back to WAV: ${error}`
|
|
340
|
+
)
|
|
341
|
+
outputData = encodeBufferToWav(resultBuffer, targetBitDepth as BitDepth)
|
|
342
|
+
outputMimeType = 'audio/wav'
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Default to WAV for unsupported formats
|
|
346
|
+
console.warn(
|
|
347
|
+
`Format ${format} not supported on web, using WAV instead`
|
|
348
|
+
)
|
|
349
|
+
outputData = encodeBufferToWav(resultBuffer, targetBitDepth as BitDepth)
|
|
350
|
+
outputMimeType = 'audio/wav'
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Report progress (90% - encoding complete)
|
|
354
|
+
progressCallback?.({ progress: 90 })
|
|
355
|
+
|
|
356
|
+
// Create a blob and URL for the result
|
|
357
|
+
const blob = new Blob([outputData], { type: outputMimeType })
|
|
358
|
+
const outputUri = URL.createObjectURL(blob)
|
|
359
|
+
|
|
360
|
+
// Calculate processing time
|
|
361
|
+
const processingTimeMs = performance.now() - startTime
|
|
362
|
+
|
|
363
|
+
// Report progress (100% - complete)
|
|
364
|
+
progressCallback?.({ progress: 100 })
|
|
365
|
+
|
|
366
|
+
// Create result object
|
|
367
|
+
const result: TrimAudioResult = {
|
|
368
|
+
uri: outputUri,
|
|
369
|
+
filename,
|
|
370
|
+
durationMs: Math.round(resultBuffer.duration * 1000),
|
|
371
|
+
size: outputData.byteLength,
|
|
372
|
+
sampleRate: resultBuffer.sampleRate,
|
|
373
|
+
channels: resultBuffer.numberOfChannels,
|
|
374
|
+
bitDepth: targetBitDepth,
|
|
375
|
+
mimeType: outputMimeType,
|
|
376
|
+
processingInfo: {
|
|
377
|
+
durationMs: processingTimeMs,
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (compressionInfo) {
|
|
382
|
+
result.compression = compressionInfo
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return result
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error('Error in trimAudio:', error)
|
|
388
|
+
throw error
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
55
392
|
// Set up progress event listener if callback is provided
|
|
56
393
|
let subscription: EventSubscription | undefined
|
|
57
394
|
if (progressCallback) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOTE: Encodes audio in real-time via AudioContext + MediaRecorder playback.
|
|
3
|
+
* A 60-second clip takes ~60 seconds to encode — this is a known Web Audio API
|
|
4
|
+
* limitation; there is no offline API for Opus/AAC encoding in browsers.
|
|
5
|
+
*/
|
|
6
|
+
export function encodeCompressedAudio(
|
|
7
|
+
buffer: AudioBuffer,
|
|
8
|
+
format: 'opus' | 'aac',
|
|
9
|
+
bitrate?: number
|
|
10
|
+
): Promise<{ data: ArrayBuffer; bitrate: number }> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
try {
|
|
13
|
+
// On web, always use opus if aac is requested (browser aac support is rare)
|
|
14
|
+
const actualFormat = format === 'aac' ? 'opus' : format
|
|
15
|
+
|
|
16
|
+
// Check if MediaRecorder supports the requested format
|
|
17
|
+
const mimeType =
|
|
18
|
+
actualFormat === 'opus' ? 'audio/webm;codecs=opus' : 'audio/aac'
|
|
19
|
+
if (!MediaRecorder.isTypeSupported(mimeType)) {
|
|
20
|
+
throw new Error(`MediaRecorder does not support ${mimeType}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Create a new AudioContext and source
|
|
24
|
+
const ctx = new (window.AudioContext ||
|
|
25
|
+
(window as any).webkitAudioContext)()
|
|
26
|
+
const source = ctx.createBufferSource()
|
|
27
|
+
source.buffer = buffer
|
|
28
|
+
|
|
29
|
+
// Create a MediaStreamDestination to capture the audio
|
|
30
|
+
const destination = ctx.createMediaStreamDestination()
|
|
31
|
+
source.connect(destination)
|
|
32
|
+
|
|
33
|
+
// Create a MediaRecorder with the requested format
|
|
34
|
+
const recorder = new MediaRecorder(destination.stream, {
|
|
35
|
+
mimeType,
|
|
36
|
+
audioBitsPerSecond:
|
|
37
|
+
bitrate || (actualFormat === 'opus' ? 32000 : 64000),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const chunks: Blob[] = []
|
|
41
|
+
|
|
42
|
+
recorder.ondataavailable = (e) => {
|
|
43
|
+
if (e.data.size > 0) {
|
|
44
|
+
chunks.push(e.data)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
recorder.onstop = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const blob = new Blob(chunks, { type: mimeType })
|
|
51
|
+
const arrayBuffer = await blob.arrayBuffer()
|
|
52
|
+
|
|
53
|
+
// Get the actual bitrate used
|
|
54
|
+
const actualBitrate = Math.round(
|
|
55
|
+
(arrayBuffer.byteLength * 8) / buffer.duration
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
resolve({
|
|
59
|
+
data: arrayBuffer,
|
|
60
|
+
bitrate: actualBitrate / 1000, // Convert to kbps
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Clean up
|
|
64
|
+
ctx.close()
|
|
65
|
+
} catch (error) {
|
|
66
|
+
reject(error)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Start recording and playback; stop recorder when playback ends
|
|
71
|
+
recorder.start()
|
|
72
|
+
source.onended = () => recorder.stop()
|
|
73
|
+
source.start(0)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
reject(error)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export async function resampleAudioBuffer(
|
|
2
|
+
buffer: AudioBuffer,
|
|
3
|
+
targetSampleRate: number,
|
|
4
|
+
targetChannels: number
|
|
5
|
+
): Promise<AudioBuffer> {
|
|
6
|
+
// If no change needed, return the original buffer
|
|
7
|
+
if (
|
|
8
|
+
buffer.sampleRate === targetSampleRate &&
|
|
9
|
+
buffer.numberOfChannels === targetChannels
|
|
10
|
+
) {
|
|
11
|
+
return buffer
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Calculate the new length based on the sample rate change
|
|
15
|
+
const newLength = Math.round(
|
|
16
|
+
(buffer.length * targetSampleRate) / buffer.sampleRate
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// Create an offline context for resampling
|
|
20
|
+
const offlineContext = new OfflineAudioContext(
|
|
21
|
+
targetChannels,
|
|
22
|
+
newLength,
|
|
23
|
+
targetSampleRate
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// Create a source node
|
|
27
|
+
const source = offlineContext.createBufferSource()
|
|
28
|
+
source.buffer = buffer
|
|
29
|
+
|
|
30
|
+
// The OfflineAudioContext was created with targetChannels; the Web Audio API
|
|
31
|
+
// applies its built-in speaker downmix/upmix rules automatically.
|
|
32
|
+
source.connect(offlineContext.destination)
|
|
33
|
+
|
|
34
|
+
// Start rendering
|
|
35
|
+
source.start(0)
|
|
36
|
+
const resampledBuffer = await offlineContext.startRendering()
|
|
37
|
+
|
|
38
|
+
return resampledBuffer
|
|
39
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.extractWaveform = void 0;
|
|
7
|
-
const AudioStudioModule_1 = __importDefault(require("../AudioStudioModule"));
|
|
8
|
-
const extractWaveform = async ({ fileUri, numberOfSamples, offset = 0, length, }) => {
|
|
9
|
-
const res = await AudioStudioModule_1.default.extractAudioAnalysis({
|
|
10
|
-
fileUri,
|
|
11
|
-
numberOfSamples,
|
|
12
|
-
offset,
|
|
13
|
-
length,
|
|
14
|
-
});
|
|
15
|
-
return res;
|
|
16
|
-
};
|
|
17
|
-
exports.extractWaveform = extractWaveform;
|
|
18
|
-
//# sourceMappingURL=extractWaveform.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"extractWaveform.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractWaveform.ts"],"names":[],"mappings":";;;;;;AAAA,6EAAoD;AAQ7C,MAAM,eAAe,GAAG,KAAK,EAAE,EAClC,OAAO,EACP,eAAe,EACf,MAAM,GAAG,CAAC,EACV,MAAM,GACa,EAAoB,EAAE;IACzC,MAAM,GAAG,GAAG,MAAM,2BAAiB,CAAC,oBAAoB,CAAC;QACrD,OAAO;QACP,eAAe;QACf,MAAM;QACN,MAAM;KACT,CAAC,CAAA;IACF,OAAO,GAAG,CAAA;AACd,CAAC,CAAA;AAbY,QAAA,eAAe,mBAa3B","sourcesContent":["import AudioStudioModule from '../AudioStudioModule'\n\nexport interface ExtractWaveformProps {\n fileUri: string\n numberOfSamples: number\n offset?: number\n length?: number\n}\nexport const extractWaveform = async ({\n fileUri,\n numberOfSamples,\n offset = 0,\n length,\n}: ExtractWaveformProps): Promise<unknown> => {\n const res = await AudioStudioModule.extractAudioAnalysis({\n fileUri,\n numberOfSamples,\n offset,\n length,\n })\n return res\n}\n"]}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import AudioStudioModule from '../AudioStudioModule';
|
|
2
|
-
export const extractWaveform = async ({ fileUri, numberOfSamples, offset = 0, length, }) => {
|
|
3
|
-
const res = await AudioStudioModule.extractAudioAnalysis({
|
|
4
|
-
fileUri,
|
|
5
|
-
numberOfSamples,
|
|
6
|
-
offset,
|
|
7
|
-
length,
|
|
8
|
-
});
|
|
9
|
-
return res;
|
|
10
|
-
};
|
|
11
|
-
//# sourceMappingURL=extractWaveform.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"extractWaveform.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractWaveform.ts"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,MAAM,sBAAsB,CAAA;AAQpD,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,EAClC,OAAO,EACP,eAAe,EACf,MAAM,GAAG,CAAC,EACV,MAAM,GACa,EAAoB,EAAE;IACzC,MAAM,GAAG,GAAG,MAAM,iBAAiB,CAAC,oBAAoB,CAAC;QACrD,OAAO;QACP,eAAe;QACf,MAAM;QACN,MAAM;KACT,CAAC,CAAA;IACF,OAAO,GAAG,CAAA;AACd,CAAC,CAAA","sourcesContent":["import AudioStudioModule from '../AudioStudioModule'\n\nexport interface ExtractWaveformProps {\n fileUri: string\n numberOfSamples: number\n offset?: number\n length?: number\n}\nexport const extractWaveform = async ({\n fileUri,\n numberOfSamples,\n offset = 0,\n length,\n}: ExtractWaveformProps): Promise<unknown> => {\n const res = await AudioStudioModule.extractAudioAnalysis({\n fileUri,\n numberOfSamples,\n offset,\n length,\n })\n return res\n}\n"]}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export interface ExtractWaveformProps {
|
|
2
|
-
fileUri: string;
|
|
3
|
-
numberOfSamples: number;
|
|
4
|
-
offset?: number;
|
|
5
|
-
length?: number;
|
|
6
|
-
}
|
|
7
|
-
export declare const extractWaveform: ({ fileUri, numberOfSamples, offset, length, }: ExtractWaveformProps) => Promise<unknown>;
|
|
8
|
-
//# sourceMappingURL=extractWaveform.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"extractWaveform.d.ts","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractWaveform.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AACD,eAAO,MAAM,eAAe,GAAU,+CAKnC,oBAAoB,KAAG,OAAO,CAAC,OAAO,CAQxC,CAAA"}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import AudioStudioModule from '../AudioStudioModule'
|
|
2
|
-
|
|
3
|
-
export interface ExtractWaveformProps {
|
|
4
|
-
fileUri: string
|
|
5
|
-
numberOfSamples: number
|
|
6
|
-
offset?: number
|
|
7
|
-
length?: number
|
|
8
|
-
}
|
|
9
|
-
export const extractWaveform = async ({
|
|
10
|
-
fileUri,
|
|
11
|
-
numberOfSamples,
|
|
12
|
-
offset = 0,
|
|
13
|
-
length,
|
|
14
|
-
}: ExtractWaveformProps): Promise<unknown> => {
|
|
15
|
-
const res = await AudioStudioModule.extractAudioAnalysis({
|
|
16
|
-
fileUri,
|
|
17
|
-
numberOfSamples,
|
|
18
|
-
offset,
|
|
19
|
-
length,
|
|
20
|
-
})
|
|
21
|
-
return res
|
|
22
|
-
}
|