@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
197 lines • 9.01 kB
JavaScript
import ExpoAudioStreamModule from '../ExpoAudioStreamModule';
import { isWeb } from '../constants';
import { processAudioBuffer } from '../utils/audioProcessing';
import { convertPCMToFloat32 } from '../utils/convertPCMToFloat32';
import crc32 from '../utils/crc32';
import { getWavFileInfo } from '../utils/getWavFileInfo';
import { InlineFeaturesExtractor } from '../workers/InlineFeaturesExtractor.web';
function calculateCRC32ForDataPoint(data) {
// Convert float array to byte array for CRC32
const byteArray = new Uint8Array(data.length * 4);
const dataView = new DataView(byteArray.buffer);
for (let i = 0; i < data.length; i++) {
dataView.setFloat32(i * 4, data[i], true);
}
return crc32.buf(byteArray);
}
/**
* Extracts detailed audio analysis from the specified audio file or buffer.
* Supports either time-based or byte-based ranges for flexibility in analysis.
*
* @param props - The options for extraction, including file URI, ranges, and decoding settings.
* @returns A promise that resolves to the audio analysis data.
* @throws {Error} If both time and byte ranges are provided or if required parameters are missing.
*/
export async function extractAudioAnalysis(props) {
const { fileUri, arrayBuffer, decodingOptions, logger, segmentDurationMs = 100, features, } = props;
if (isWeb) {
try {
// Create AudioContext here
const audioContext = new (window.AudioContext ||
window.webkitAudioContext)({
sampleRate: decodingOptions?.targetSampleRate ?? 16000,
});
try {
const processedBuffer = await processAudioBuffer({
arrayBuffer,
fileUri,
targetSampleRate: decodingOptions?.targetSampleRate ?? 16000,
targetChannels: decodingOptions?.targetChannels ?? 1,
normalizeAudio: decodingOptions?.normalizeAudio ?? false,
startTimeMs: 'startTimeMs' in props ? props.startTimeMs : undefined,
endTimeMs: 'endTimeMs' in props ? props.endTimeMs : undefined,
position: 'position' in props ? props.position : undefined,
length: 'length' in props ? props.length : undefined,
audioContext, // Pass the context we created
logger,
});
const channelData = processedBuffer.buffer.getChannelData(0);
// Create and initialize the worker
const blob = new Blob([InlineFeaturesExtractor], {
type: 'application/javascript',
});
const workerUrl = URL.createObjectURL(blob);
const worker = new Worker(workerUrl);
return new Promise((resolve, reject) => {
worker.onmessage = (event) => {
if (event.data.error) {
reject(new Error(event.data.error));
return;
}
const result = event.data.result;
// Calculate CRC32 after worker completes if requested
if (features?.crc32) {
const samplesPerSegment = Math.floor((processedBuffer.sampleRate *
segmentDurationMs) /
1000);
result.dataPoints = result.dataPoints.map((point, index) => {
const startSample = index * samplesPerSegment;
const segmentData = channelData.slice(startSample, startSample + samplesPerSegment);
return {
...point,
features: {
...point.features,
crc32: calculateCRC32ForDataPoint(segmentData),
},
};
});
}
URL.revokeObjectURL(workerUrl);
worker.terminate();
resolve(result);
};
worker.onerror = (error) => {
URL.revokeObjectURL(workerUrl);
worker.terminate();
reject(error);
};
worker.postMessage({
channelData,
sampleRate: processedBuffer.sampleRate,
segmentDurationMs,
bitDepth: decodingOptions?.targetBitDepth ?? 32,
numberOfChannels: processedBuffer.channels,
fullAudioDurationMs: processedBuffer.durationMs,
// enableLogging: !!logger,
features,
});
});
}
finally {
await audioContext.close();
}
}
catch (error) {
logger?.error('Failed to process audio:', error);
throw error;
}
}
else {
return await ExpoAudioStreamModule.extractAudioAnalysis(props);
}
}
/**
* Analyzes WAV files without decoding, preserving original PCM values.
* Use this function when you need to ensure the analysis matches other software by avoiding any transformations.
*
* @param props - The options for WAV analysis, including file URI and range.
* @returns A promise that resolves to the audio analysis data.
*/
export const extractRawWavAnalysis = async ({ fileUri, segmentDurationMs = 100, // Default to 100ms
arrayBuffer, bitDepth, durationMs, sampleRate, numberOfChannels, features, logger, position = 0, length, }) => {
if (isWeb) {
if (!arrayBuffer && !fileUri) {
throw new Error('Either arrayBuffer or fileUri must be provided');
}
if (!arrayBuffer) {
logger?.log(`fetching fileUri`, fileUri);
const response = await fetch(fileUri);
if (!response.ok) {
throw new Error(`Failed to fetch fileUri: ${response.statusText}`);
}
arrayBuffer = await response.arrayBuffer();
logger?.log(`fetched fileUri`, arrayBuffer.byteLength, arrayBuffer);
}
// Create a new copy of the ArrayBuffer to avoid detachment issues
const bufferCopy = arrayBuffer.slice(0);
logger?.log(`extractAudioAnalysis bitDepth=${bitDepth} len=${bufferCopy.byteLength}`, bufferCopy.slice(0, 100));
let actualBitDepth = bitDepth;
if (!actualBitDepth) {
logger?.log(`extractAudioAnalysis bitDepth not provided -- getting wav file info`);
const fileInfo = await getWavFileInfo(bufferCopy);
actualBitDepth = fileInfo.bitDepth;
}
logger?.log(`extractAudioAnalysis actualBitDepth=${actualBitDepth}`);
const { pcmValues: channelData, min, max, } = await convertPCMToFloat32({
buffer: arrayBuffer,
bitDepth: actualBitDepth,
});
logger?.log(`extractAudioAnalysis convertPCMToFloat32 length=${channelData.length} range: [ ${min} :: ${max} ]`);
// Apply position and length constraints to channelData if specified
const startIndex = position;
const endIndex = length ? startIndex + length : channelData.length;
const constrainedChannelData = channelData.slice(startIndex, endIndex);
return new Promise((resolve, reject) => {
const blob = new Blob([InlineFeaturesExtractor], {
type: 'application/javascript',
});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = (event) => {
resolve(event.data.result);
};
worker.onerror = (error) => {
reject(error);
};
worker.postMessage({
command: 'process',
channelData: constrainedChannelData,
sampleRate,
segmentDurationMs,
logger,
bitDepth,
fullAudioDurationMs: durationMs,
numberOfChannels,
});
});
}
else {
if (!fileUri) {
throw new Error('fileUri is required');
}
logger?.log(`extractAudioAnalysis`, {
fileUri,
segmentDurationMs,
});
const res = await ExpoAudioStreamModule.extractAudioAnalysis({
fileUri,
segmentDurationMs,
features,
position,
length,
});
logger?.log(`extractAudioAnalysis`, res);
return res;
}
};
//# sourceMappingURL=extractAudioAnalysis.js.map