@siteed/expo-audio-studio
Version:
Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web
206 lines (183 loc) • 6.53 kB
text/typescript
// packages/expo-audio-stream/src/utils/audioProcessing.ts
import { Platform } from 'react-native'
import { ConsoleLike } from '../ExpoAudioStream.types'
export interface ProcessAudioBufferOptions {
arrayBuffer?: ArrayBuffer
fileUri?: string
targetSampleRate: number
targetChannels: number
normalizeAudio: boolean
startTimeMs?: number
endTimeMs?: number
position?: number
length?: number
audioContext?: AudioContext
logger?: ConsoleLike
}
export interface ProcessedAudioData {
channelData: Float32Array
samples: number
durationMs: number
sampleRate: number
channels: number
buffer: AudioBuffer
}
export async function processAudioBuffer({
arrayBuffer,
fileUri,
targetSampleRate,
targetChannels,
normalizeAudio,
startTimeMs,
endTimeMs,
position,
length,
audioContext,
logger,
}: ProcessAudioBufferOptions): Promise<ProcessedAudioData> {
if (Platform.OS !== 'web') {
throw new Error('processAudioBuffer is only supported on web')
}
let ctx: AudioContext | undefined
let buffer: AudioBuffer | undefined
try {
// Log initial parameters
logger?.debug('Process audio buffer - Initial params:', {
hasArrayBuffer: !!arrayBuffer,
fileUri,
targetSampleRate,
targetChannels,
normalizeAudio,
startTimeMs,
endTimeMs,
position,
length,
})
// Get the audio data
let audioData: ArrayBuffer
if (arrayBuffer) {
audioData = arrayBuffer
} else if (fileUri) {
const response = await fetch(fileUri)
if (!response.ok) {
throw new Error(
`Failed to fetch fileUri: ${response.statusText}`
)
}
audioData = await response.arrayBuffer()
} else {
throw new Error('Either arrayBuffer or fileUri must be provided')
}
logger?.debug('Audio data loaded:', {
byteLength: audioData.byteLength,
firstBytes: Array.from(new Uint8Array(audioData.slice(0, 16))),
})
// Create context at original sample rate first
ctx =
audioContext ||
new (window.AudioContext || (window as any).webkitAudioContext)()
buffer = await ctx.decodeAudioData(audioData)
logger?.debug('Decoded audio buffer:', {
originalChannels: buffer.numberOfChannels,
originalSampleRate: buffer.sampleRate,
originalDuration: buffer.duration,
originalLength: buffer.length,
})
// Calculate time range
const startSample =
startTimeMs !== undefined
? Math.floor((startTimeMs / 1000) * buffer.sampleRate)
: position !== undefined
? Math.floor(position / 2)
: 0
// Fix: Adjust position calculation based on original sample rate
// When position is provided in bytes, we need to account for the original sample rate
const bytesPerSample = 2 // 16-bit audio = 2 bytes per sample
const adjustedStartSample =
position !== undefined
? Math.floor(
(position / bytesPerSample) *
(buffer.sampleRate / targetSampleRate)
)
: startSample
const samplesNeeded =
length !== undefined
? Math.floor(
(length / bytesPerSample) *
(buffer.sampleRate / targetSampleRate)
)
: endTimeMs !== undefined && startTimeMs !== undefined
? Math.floor(
((endTimeMs - startTimeMs) / 1000) * buffer.sampleRate
)
: buffer.length - adjustedStartSample
logger?.debug('Sample calculations (adjusted):', {
originalStartSample: startSample,
adjustedStartSample,
samplesNeeded,
originalSampleRate: buffer.sampleRate,
targetSampleRate,
conversionRatio: buffer.sampleRate / targetSampleRate,
expectedDurationMs: (samplesNeeded / buffer.sampleRate) * 1000,
})
// Create temporary buffer for the segment
const segmentBuffer = ctx.createBuffer(
buffer.numberOfChannels,
samplesNeeded,
buffer.sampleRate
)
// Copy the segment
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
const channelData = buffer.getChannelData(channel)
const segmentData = segmentBuffer.getChannelData(channel)
for (let i = 0; i < samplesNeeded; i++) {
segmentData[i] = channelData[adjustedStartSample + i]
}
}
// Create offline context for resampling
const offlineCtx = new OfflineAudioContext(
targetChannels,
Math.ceil((samplesNeeded * targetSampleRate) / buffer.sampleRate),
targetSampleRate
)
// Create source and connect
const source = offlineCtx.createBufferSource()
source.buffer = segmentBuffer
source.connect(offlineCtx.destination)
// Render at new sample rate
source.start()
const processedBuffer = await offlineCtx.startRendering()
// Get the final audio data
const channelData = processedBuffer.getChannelData(0)
const durationMs = Math.round(
(samplesNeeded / buffer.sampleRate) * 1000
)
logger?.debug('Final processed audio:', {
outputSamples: channelData.length,
outputSampleRate: targetSampleRate,
durationMs,
})
return {
buffer: processedBuffer,
channelData,
samples: channelData.length,
durationMs,
sampleRate: targetSampleRate,
channels: processedBuffer.numberOfChannels,
}
} catch (error) {
logger?.error('Failed to process audio buffer:', {
error,
position,
length,
startTimeMs,
endTimeMs,
bufferLength: buffer?.length,
})
throw error
} finally {
if (!audioContext && ctx) {
await ctx.close()
}
}
}