@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
968 lines (850 loc) • 36.3 kB
text/typescript
import { requireNativeModule } from 'expo-modules-core'
import { Platform } from 'react-native'
import {
ExtractAudioDataOptions,
ExtractedAudioData,
BitDepth,
TrimAudioOptions,
TrimAudioResult,
} from './ExpoAudioStream.types'
import {
ExpoAudioStreamWeb,
ExpoAudioStreamWebProps,
} from './ExpoAudioStream.web'
import { processAudioBuffer } from './utils/audioProcessing'
import crc32 from './utils/crc32'
import { writeWavHeader } from './utils/writeWavHeader'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let ExpoAudioStreamModule: any
if (Platform.OS === 'web') {
let instance: ExpoAudioStreamWeb | null = null
ExpoAudioStreamModule = (webProps: ExpoAudioStreamWebProps) => {
if (!instance) {
instance = new ExpoAudioStreamWeb(webProps)
}
return instance
}
ExpoAudioStreamModule.requestPermissionsAsync = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
})
stream.getTracks().forEach((track) => track.stop())
return {
status: 'granted',
expires: 'never',
canAskAgain: true,
granted: true,
}
} catch {
return {
status: 'denied',
expires: 'never',
canAskAgain: true,
granted: false,
}
}
}
ExpoAudioStreamModule.getPermissionsAsync = async () => {
let maybeStatus: string | null = null
if (navigator?.permissions?.query) {
try {
const { state } = await navigator.permissions.query({
name: 'microphone' as PermissionName,
})
maybeStatus = state
} catch {
maybeStatus = null
}
}
switch (maybeStatus) {
case 'granted':
return {
status: 'granted',
expires: 'never',
canAskAgain: true,
granted: true,
}
case 'denied':
return {
status: 'denied',
expires: 'never',
canAskAgain: true,
granted: false,
}
default:
return await ExpoAudioStreamModule.requestPermissionsAsync()
}
}
ExpoAudioStreamModule.extractAudioData = async (
options: ExtractAudioDataOptions
): Promise<ExtractedAudioData> => {
try {
const {
fileUri,
position,
length,
startTimeMs,
endTimeMs,
decodingOptions,
includeNormalizedData,
includeBase64Data,
includeWavHeader = false,
logger,
} = options
logger?.debug('EXTRACT AUDIO - Step 1: Initial request', {
fileUri,
extractionParams: {
position,
length,
startTimeMs,
endTimeMs,
},
decodingOptions: {
targetSampleRate:
decodingOptions?.targetSampleRate ?? 16000,
targetChannels: decodingOptions?.targetChannels ?? 1,
targetBitDepth: decodingOptions?.targetBitDepth ?? 16,
normalizeAudio: decodingOptions?.normalizeAudio ?? false,
},
outputOptions: {
includeNormalizedData,
includeBase64Data,
includeWavHeader,
},
})
// Process the audio using shared helper function
const processedBuffer = await processAudioBuffer({
fileUri,
targetSampleRate: decodingOptions?.targetSampleRate ?? 16000,
targetChannels: decodingOptions?.targetChannels ?? 1,
normalizeAudio: decodingOptions?.normalizeAudio ?? false,
position,
length,
startTimeMs,
endTimeMs,
logger,
})
logger?.debug('EXTRACT AUDIO - Step 2: Audio processing complete', {
processedData: {
samples: processedBuffer.samples,
sampleRate: processedBuffer.sampleRate,
channels: processedBuffer.channels,
durationMs: processedBuffer.durationMs,
},
})
const channelData = processedBuffer.channelData
const bitDepth = (decodingOptions?.targetBitDepth ?? 16) as BitDepth
const bytesPerSample = bitDepth / 8
const numSamples = processedBuffer.samples
logger?.debug('EXTRACT AUDIO - Step 3: PCM conversion setup', {
channelData: {
length: channelData.length,
first: channelData[0],
last: channelData[channelData.length - 1],
},
calculation: {
bitDepth,
bytesPerSample,
numSamples,
expectedBytes: numSamples * bytesPerSample,
},
})
// Create PCM data with correct length based on original byte length
const pcmData = new Uint8Array(numSamples * bytesPerSample)
let offset = 0
// Convert Float32 samples to PCM format
for (let i = 0; i < numSamples; i++) {
const sample = channelData[i]
const value = Math.max(-1, Math.min(1, sample))
// Convert to 16-bit signed integer
let intValue = Math.round(value * 32767)
// Handle negative values correctly
if (intValue < 0) {
intValue = 65536 + intValue
}
// Write as little-endian
pcmData[offset++] = intValue & 255 // Low byte
pcmData[offset++] = (intValue >> 8) & 255 // High byte
}
const durationMs = Math.round(
(numSamples / processedBuffer.sampleRate) * 1000
)
logger?.debug('EXTRACT AUDIO - Step 4: Final output', {
pcmData: {
length: pcmData.length,
first: pcmData[0],
last: pcmData[pcmData.length - 1],
},
timing: {
numSamples,
sampleRate: processedBuffer.sampleRate,
durationMs,
shouldBe3000ms: endTimeMs
? endTimeMs - (startTimeMs ?? 0) === 3000
: undefined,
},
})
const result: ExtractedAudioData = {
pcmData: new Uint8Array(pcmData.buffer),
sampleRate: processedBuffer.sampleRate,
channels: processedBuffer.channels,
bitDepth,
durationMs,
format: `pcm_${bitDepth}bit` as const,
samples: numSamples,
}
// Add WAV header if requested
if (includeWavHeader) {
logger?.debug('EXTRACT AUDIO - Step 4: Adding WAV header', {
originalLength: pcmData.length,
newLength: result.pcmData.length,
firstBytes: Array.from(result.pcmData.slice(0, 44)), // WAV header is 44 bytes
})
const wavBuffer = writeWavHeader({
buffer: pcmData.buffer.slice(0, pcmData.length),
sampleRate: processedBuffer.sampleRate,
numChannels: processedBuffer.channels,
bitDepth,
})
result.pcmData = new Uint8Array(wavBuffer)
result.hasWavHeader = true
}
if (includeNormalizedData) {
// // Simple approach: Create normalized data directly from the PCM data
// // Just convert to -1 to 1 range without any amplification
// const normalizedData = new Float32Array(numSamples)
// // Convert the PCM data to float values
// for (let i = 0; i < numSamples; i++) {
// // Get the 16-bit PCM value (little endian)
// const lowByte = pcmData[i * 2]
// const highByte = pcmData[i * 2 + 1]
// const pcmValue = (highByte << 8) | lowByte
// // Convert to signed 16-bit value
// const signedValue =
// pcmValue > 32767 ? pcmValue - 65536 : pcmValue
// // Normalize to float between -1 and 1
// normalizedData[i] = signedValue / 32768.0
// }
// Store the normalized data in the result
result.normalizedData = channelData
}
if (includeBase64Data) {
// Convert the PCM data to a base64 string
const binary = Array.from(new Uint8Array(pcmData.buffer))
.map((b) => String.fromCharCode(b))
.join('')
result.base64Data = btoa(binary)
}
if (options.computeChecksum) {
result.checksum = crc32.buf(pcmData)
}
logger?.debug('EXTRACT AUDIO - Step 3: PCM conversion complete', {
pcmStats: {
length: pcmData.length,
bytesPerSample,
totalSamples: numSamples,
firstBytes: Array.from(pcmData.slice(0, 16)),
lastBytes: Array.from(pcmData.slice(-16)),
},
})
return result
} catch (error) {
options.logger?.error('EXTRACT AUDIO - Error:', error)
throw error
}
}
ExpoAudioStreamModule.trimAudio = async (
options: TrimAudioOptions
): Promise<TrimAudioResult> => {
try {
const startTime = performance.now()
const {
fileUri,
mode = 'single',
startTimeMs,
endTimeMs,
ranges,
outputFileName,
outputFormat,
} = options
// Validate inputs
if (!fileUri) {
throw new Error('fileUri is required')
}
if (
mode === 'single' &&
startTimeMs === undefined &&
endTimeMs === undefined
) {
throw new Error(
'At least one of startTimeMs or endTimeMs must be provided in single mode'
)
}
if (
(mode === 'keep' || mode === 'remove') &&
(!ranges || ranges.length === 0)
) {
throw new Error(
'ranges must be provided and non-empty for keep or remove modes'
)
}
// Create AudioContext
const audioContext = new (window.AudioContext ||
(window as any).webkitAudioContext)()
// First, load the entire audio file to get its properties
const response = await fetch(fileUri)
const arrayBuffer = await response.arrayBuffer()
const originalAudioBuffer =
await audioContext.decodeAudioData(arrayBuffer)
// Get original audio properties
const originalSampleRate = originalAudioBuffer.sampleRate
const originalChannels = originalAudioBuffer.numberOfChannels
// Add more detailed logging
console.log(`Original audio details:`, {
sampleRate: originalSampleRate,
channels: originalChannels,
duration: originalAudioBuffer.duration,
length: originalAudioBuffer.length,
// Log a few samples to verify content
firstSamples: Array.from(
originalAudioBuffer.getChannelData(0).slice(0, 5)
),
})
// Determine output format - use original values as defaults if not specified
let format = outputFormat?.format || 'wav'
const targetSampleRate =
outputFormat?.sampleRate || originalSampleRate
const targetChannels = outputFormat?.channels || originalChannels
const targetBitDepth = outputFormat?.bitDepth || 16
// Get file info from the URL
const filename =
outputFileName ||
fileUri.split('/').pop() ||
'trimmed-audio.wav'
// Process based on mode
let resultBuffer: AudioBuffer
// Report initial progress
ExpoAudioStreamModule.sendEvent('TrimProgress', {
progress: 10,
})
if (mode === 'single') {
// Single mode: extract a single range
// Use original sample rate and channels for extraction to preserve quality
const { buffer } = await processAudioBuffer({
fileUri,
targetSampleRate, // Use the requested sample rate
targetChannels,
normalizeAudio: false,
startTimeMs,
endTimeMs,
audioContext,
})
console.log(`Processed buffer details:`, {
sampleRate: buffer.sampleRate,
channels: buffer.numberOfChannels,
duration: buffer.duration,
length: buffer.length,
// Log a few samples to verify content
firstSamples: Array.from(
buffer.getChannelData(0).slice(0, 5)
),
})
resultBuffer = buffer
// If we need to change sample rate or channels, do it after extraction
if (
targetSampleRate !== originalSampleRate ||
targetChannels !== originalChannels
) {
console.log(
`Resampling from ${originalSampleRate}Hz to ${targetSampleRate}Hz`
)
resultBuffer = await resampleAudioBuffer(
audioContext,
buffer,
targetSampleRate,
targetChannels
)
}
} else {
// For keep or remove modes
const fullDuration = originalAudioBuffer.duration * 1000 // in ms
type ProcessSegment = {
startTimeMs: number
endTimeMs: number
}
let segmentsToProcess: ProcessSegment[] = []
if (mode === 'keep') {
// For keep mode, use the ranges directly
segmentsToProcess = ranges!
} else {
// mode === 'remove'
// For remove mode, invert the ranges
const sortedRanges = [...ranges!].sort(
(a, b) => a.startTimeMs - b.startTimeMs
)
// Add segment from start to first range if needed
if (
sortedRanges.length > 0 &&
sortedRanges[0].startTimeMs > 0
) {
segmentsToProcess.push({
startTimeMs: 0,
endTimeMs: sortedRanges[0].startTimeMs,
})
}
// Add segments between ranges
for (let i = 0; i < sortedRanges.length - 1; i++) {
segmentsToProcess.push({
startTimeMs: sortedRanges[i].endTimeMs,
endTimeMs: sortedRanges[i + 1].startTimeMs,
})
}
// Add segment from last range to end if needed
if (
sortedRanges.length > 0 &&
sortedRanges[sortedRanges.length - 1].endTimeMs <
fullDuration
) {
segmentsToProcess.push({
startTimeMs:
sortedRanges[sortedRanges.length - 1].endTimeMs,
endTimeMs: fullDuration,
})
}
}
// Filter out empty or invalid segments
segmentsToProcess = segmentsToProcess.filter(
(segment) =>
segment.startTimeMs < segment.endTimeMs &&
segment.endTimeMs - segment.startTimeMs > 1
) // 1ms minimum
if (segmentsToProcess.length === 0) {
throw new Error(
'No valid segments to process after filtering ranges'
)
}
// Process each segment using original sample rate and channels
const segmentBuffers: AudioBuffer[] = []
for (let i = 0; i < segmentsToProcess.length; i++) {
const segment = segmentsToProcess[i]
// Report progress for each segment
ExpoAudioStreamModule.sendEvent('TrimProgress', {
progress:
10 +
Math.round((i / segmentsToProcess.length) * 40),
})
// Use processAudioBuffer to extract this segment
const { buffer: segmentBuffer } = await processAudioBuffer({
fileUri,
targetSampleRate: originalSampleRate, // Use original sample rate
targetChannels: originalChannels, // Use original channels
normalizeAudio: false,
startTimeMs: segment.startTimeMs,
endTimeMs: segment.endTimeMs,
audioContext,
})
segmentBuffers.push(segmentBuffer)
}
// Concatenate all segments
const totalSamples = segmentBuffers.reduce(
(sum, buffer) => sum + buffer.length,
0
)
// Create buffer with original properties first
const concatenatedBuffer = audioContext.createBuffer(
originalChannels,
totalSamples,
originalSampleRate
)
let offset = 0
for (const segmentBuffer of segmentBuffers) {
for (
let channel = 0;
channel < originalChannels;
channel++
) {
const outputData =
concatenatedBuffer.getChannelData(channel)
const segmentData =
segmentBuffer.getChannelData(channel)
for (let i = 0; i < segmentBuffer.length; i++) {
outputData[offset + i] = segmentData[i]
}
}
offset += segmentBuffer.length
}
resultBuffer = concatenatedBuffer
// If we need to change sample rate or channels, do it after concatenation
if (
targetSampleRate !== originalSampleRate ||
targetChannels !== originalChannels
) {
console.log(
`Resampling concatenated buffer from ${originalSampleRate}Hz to ${targetSampleRate}Hz`
)
resultBuffer = await resampleAudioBuffer(
audioContext,
concatenatedBuffer,
targetSampleRate,
targetChannels
)
}
}
// Report progress (50% - processing complete)
ExpoAudioStreamModule.sendEvent('TrimProgress', {
progress: 50,
})
// Encode the result based on the requested format
let outputData: ArrayBuffer
let outputMimeType: string
let compressionInfo: any = null
// Check if AAC was requested on web and show a warning
if (format === 'aac' && Platform.OS === 'web') {
console.warn(
'AAC format is not supported on web platforms. Falling back to OPUS format.'
)
format = 'opus'
}
if (format === 'wav') {
// Create a properly interleaved buffer for WAV format
// For WAV, we need to convert Float32Array to Int16Array (for 16-bit audio)
const numSamples =
resultBuffer.length * resultBuffer.numberOfChannels
const interleavedData = new Int16Array(numSamples)
// Log detailed information about the buffer before encoding
console.log(`Creating WAV file:`, {
bufferSampleRate: resultBuffer.sampleRate,
bufferChannels: resultBuffer.numberOfChannels,
bufferLength: resultBuffer.length,
targetSampleRate,
targetChannels,
targetBitDepth,
// Log a few samples to verify content
firstSamples: Array.from(
resultBuffer.getChannelData(0).slice(0, 5)
),
})
// Interleave channels properly
for (let i = 0; i < resultBuffer.length; i++) {
for (
let channel = 0;
channel < resultBuffer.numberOfChannels;
channel++
) {
// Convert float (-1.0 to 1.0) to int16 (-32768 to 32767)
const floatSample =
resultBuffer.getChannelData(channel)[i]
// Clamp the value to -1.0 to 1.0
const clampedSample = Math.max(
-1.0,
Math.min(1.0, floatSample)
)
// Convert to int16
const intSample = Math.round(clampedSample * 32767)
// Store in interleaved buffer
interleavedData[
i * resultBuffer.numberOfChannels + channel
] = intSample
}
}
// Convert Int16Array to ArrayBuffer for WAV header
const rawBuffer = interleavedData.buffer
// IMPORTANT: Make sure we're using the ACTUAL sample rate of the buffer
// not just what was requested in the options
console.log(
`Creating WAV with ${resultBuffer.numberOfChannels} channels at ${resultBuffer.sampleRate}Hz`
)
outputData = writeWavHeader({
buffer: rawBuffer as ArrayBuffer,
sampleRate: resultBuffer.sampleRate, // Use the actual buffer's sample rate
numChannels: resultBuffer.numberOfChannels,
bitDepth: targetBitDepth as BitDepth,
})
outputMimeType = 'audio/wav'
} else if (format === 'opus' || format === 'aac') {
try {
// Try to use MediaRecorder for compressed formats
const { data, bitrate } = await encodeCompressedAudio(
resultBuffer,
format,
outputFormat?.bitrate
)
outputData = data
outputMimeType =
format === 'opus' ? 'audio/webm' : 'audio/aac'
compressionInfo = {
format,
bitrate,
size: data.byteLength,
}
} catch (error) {
console.warn(
`Failed to encode to ${format}, falling back to WAV: ${error}`
)
// Same WAV encoding as above
const wavData = new Float32Array(
resultBuffer.length * resultBuffer.numberOfChannels
)
for (let i = 0; i < resultBuffer.length; i++) {
for (
let channel = 0;
channel < resultBuffer.numberOfChannels;
channel++
) {
wavData[
i * resultBuffer.numberOfChannels + channel
] = resultBuffer.getChannelData(channel)[i]
}
}
outputData = writeWavHeader({
buffer: wavData.buffer as ArrayBuffer,
sampleRate: resultBuffer.sampleRate,
numChannels: resultBuffer.numberOfChannels,
bitDepth: targetBitDepth as BitDepth,
})
outputMimeType = 'audio/wav'
}
} else {
// Default to WAV for unsupported formats
console.warn(
`Format ${format} not supported on web, using WAV instead`
)
// Same WAV encoding as above
const wavData = new Float32Array(
resultBuffer.length * resultBuffer.numberOfChannels
)
for (let i = 0; i < resultBuffer.length; i++) {
for (
let channel = 0;
channel < resultBuffer.numberOfChannels;
channel++
) {
wavData[i * resultBuffer.numberOfChannels + channel] =
resultBuffer.getChannelData(channel)[i]
}
}
outputData = writeWavHeader({
buffer: wavData.buffer as ArrayBuffer,
sampleRate: resultBuffer.sampleRate,
numChannels: resultBuffer.numberOfChannels,
bitDepth: targetBitDepth as BitDepth,
})
outputMimeType = 'audio/wav'
}
// Report progress (90% - encoding complete)
ExpoAudioStreamModule.sendEvent('TrimProgress', {
progress: 90,
})
// Create a blob and URL for the result
const blob = new Blob([outputData], { type: outputMimeType })
const outputUri = URL.createObjectURL(blob)
// Calculate processing time
const processingTimeMs = performance.now() - startTime
// Report progress (100% - complete)
ExpoAudioStreamModule.sendEvent('TrimProgress', {
progress: 100,
})
// Create result object
const result: TrimAudioResult = {
uri: outputUri,
filename,
durationMs: Math.round(resultBuffer.duration * 1000),
size: outputData.byteLength,
sampleRate: resultBuffer.sampleRate,
channels: resultBuffer.numberOfChannels,
bitDepth: targetBitDepth,
mimeType: outputMimeType,
processingInfo: {
durationMs: processingTimeMs,
},
}
// Add compression info if available
if (compressionInfo) {
result.compression = compressionInfo
}
return result
} catch (error) {
console.error('Error in trimAudio:', error)
throw error
}
}
// Add a sendEvent method for web
ExpoAudioStreamModule.sendEvent = (eventName: string, params: any) => {
// This will be picked up by the LegacyEventEmitter in trimAudio.ts
if (
ExpoAudioStreamModule.listeners &&
ExpoAudioStreamModule.listeners[eventName]
) {
ExpoAudioStreamModule.listeners[eventName].forEach(
(listener: Function) => {
listener(params)
}
)
}
}
// Initialize listeners object
ExpoAudioStreamModule.listeners = {}
// Add methods for event listeners that LegacyEventEmitter will use
ExpoAudioStreamModule.addListener = (
eventName: string,
listener: Function
) => {
if (!ExpoAudioStreamModule.listeners[eventName]) {
ExpoAudioStreamModule.listeners[eventName] = []
}
ExpoAudioStreamModule.listeners[eventName].push(listener)
// Return an object with a remove method
return {
remove: () => {
const index =
ExpoAudioStreamModule.listeners[eventName].indexOf(listener)
if (index !== -1) {
ExpoAudioStreamModule.listeners[eventName].splice(index, 1)
}
},
}
}
ExpoAudioStreamModule.removeAllListeners = (eventName: string) => {
if (ExpoAudioStreamModule.listeners[eventName]) {
delete ExpoAudioStreamModule.listeners[eventName]
}
}
}
// Move the encodeCompressedAudio function outside the if block to fix the ESLint error
async function encodeCompressedAudio(
buffer: AudioBuffer,
format: 'opus' | 'aac',
bitrate?: number
): Promise<{ data: ArrayBuffer; bitrate: number }> {
return new Promise((resolve, reject) => {
try {
// On web, always use opus if aac is requested
const actualFormat =
Platform.OS === 'web' && format === 'aac' ? 'opus' : format
// Check if MediaRecorder supports the requested format
const mimeType =
actualFormat === 'opus' ? 'audio/webm;codecs=opus' : 'audio/aac'
if (!MediaRecorder.isTypeSupported(mimeType)) {
throw new Error(`MediaRecorder does not support ${mimeType}`)
}
// Create a new AudioContext and source
const ctx = new (window.AudioContext ||
(window as any).webkitAudioContext)()
const source = ctx.createBufferSource()
source.buffer = buffer
// Create a MediaStreamDestination to capture the audio
const destination = ctx.createMediaStreamDestination()
source.connect(destination)
// Create a MediaRecorder with the requested format
const recorder = new MediaRecorder(destination.stream, {
mimeType,
audioBitsPerSecond:
bitrate || (actualFormat === 'opus' ? 32000 : 64000),
})
const chunks: Blob[] = []
recorder.ondataavailable = (e) => {
if (e.data.size > 0) {
chunks.push(e.data)
}
}
recorder.onstop = async () => {
try {
const blob = new Blob(chunks, { type: mimeType })
const arrayBuffer = await blob.arrayBuffer()
// Get the actual bitrate used
const actualBitrate = Math.round(
(arrayBuffer.byteLength * 8) / buffer.duration
)
resolve({
data: arrayBuffer,
bitrate: actualBitrate / 1000, // Convert to kbps
})
// Clean up
ctx.close()
} catch (error) {
reject(error)
}
}
// Start recording and playback
recorder.start()
source.start(0)
// Stop recording when the buffer finishes playing
setTimeout(() => {
recorder.stop()
source.stop()
}, buffer.duration * 1000)
} catch (error) {
reject(error)
}
})
}
// Improved resampleAudioBuffer function
async function resampleAudioBuffer(
context: AudioContext,
buffer: AudioBuffer,
targetSampleRate: number,
targetChannels: number
): Promise<AudioBuffer> {
// If no change needed, return the original buffer
if (
buffer.sampleRate === targetSampleRate &&
buffer.numberOfChannels === targetChannels
) {
return buffer
}
console.log(
`Resampling: ${buffer.sampleRate}Hz → ${targetSampleRate}Hz, ${buffer.numberOfChannels} → ${targetChannels} channels`
)
// Calculate the new length based on the sample rate change
const newLength = Math.round(
(buffer.length * targetSampleRate) / buffer.sampleRate
)
// Create an offline context for resampling
const offlineContext = new OfflineAudioContext(
targetChannels,
newLength,
targetSampleRate
)
// Create a source node
const source = offlineContext.createBufferSource()
source.buffer = buffer
// If we need to change channel count
if (buffer.numberOfChannels !== targetChannels) {
if (targetChannels === 1 && buffer.numberOfChannels > 1) {
// Downmix to mono
const merger = offlineContext.createChannelMerger(1)
// Create a gain node to reduce volume when downmixing to prevent clipping
const gainNode = offlineContext.createGain()
gainNode.gain.value = 1.0 / buffer.numberOfChannels
source.connect(gainNode)
gainNode.connect(merger)
merger.connect(offlineContext.destination)
} else if (targetChannels === 2 && buffer.numberOfChannels === 1) {
// Upmix mono to stereo (duplicate the channel)
const splitter = offlineContext.createChannelSplitter(1)
const merger = offlineContext.createChannelMerger(2)
source.connect(splitter)
splitter.connect(merger, 0, 0)
splitter.connect(merger, 0, 1)
merger.connect(offlineContext.destination)
} else {
// For other cases, just connect and let the system handle it
source.connect(offlineContext.destination)
}
} else {
// No channel conversion needed
source.connect(offlineContext.destination)
}
// Start rendering
source.start(0)
const resampledBuffer = await offlineContext.startRendering()
console.log(
`Resampling complete: ${resampledBuffer.length} samples at ${resampledBuffer.sampleRate}Hz`
)
return resampledBuffer
}
if (Platform.OS !== 'web') {
ExpoAudioStreamModule = requireNativeModule('ExpoAudioStream')
}
export default ExpoAudioStreamModule