@lvyanxiang/react-native-audio-waveform
Version:
A React Native library for extracting audio waveform data from audio and video files. Supports iOS and Android platforms with streaming processing for large files.
230 lines (214 loc) • 7.08 kB
JavaScript
/**
* Convert PCM audio data to Float32Array
*/
export async function convertPCMToFloat32(buffer, bitDepth, skipWavHeader = false) {
let headerOffset = 0;
let sampleRate = 44100; // Default
let channels = 1; // Default
if (!skipWavHeader) {
// Parse WAV header
const view = new DataView(buffer);
// Check for WAV format
const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
if (riff === 'RIFF') {
sampleRate = view.getUint32(24, true);
channels = view.getUint16(22, true);
headerOffset = 44; // Standard WAV header size
}
}
const uint8Array = new Uint8Array(buffer);
const dataLength = buffer.byteLength - headerOffset;
const bytesPerSample = bitDepth / 8;
const sampleLength = Math.floor(dataLength / bytesPerSample);
const float32Array = new Float32Array(sampleLength);
for (let i = 0; i < sampleLength; i++) {
const startIndex = headerOffset + i * bytesPerSample;
let value = 0;
switch (bitDepth) {
case 8:
value = (uint8Array[startIndex] - 128) / 128;
break;
case 16:
{
const low = uint8Array[startIndex];
const high = uint8Array[startIndex + 1];
const intValue = high << 8 | low;
value = (intValue > 32767 ? intValue - 65536 : intValue) / 32768;
break;
}
case 24:
{
const byte1 = uint8Array[startIndex];
const byte2 = uint8Array[startIndex + 1];
const byte3 = uint8Array[startIndex + 2];
const intValue = byte3 << 16 | byte2 << 8 | byte1;
value = (intValue > 8388607 ? intValue - 16777216 : intValue) / 8388608 * 2 - 1;
break;
}
case 32:
{
const view = new DataView(buffer, startIndex, 4);
value = view.getFloat32(0, true);
break;
}
default:
throw new Error(`Unsupported bit depth: ${bitDepth}`);
}
float32Array[i] = value;
}
return {
audioData: float32Array,
sampleRate,
channels
};
}
/**
* Resample audio data to target sample rate
*/
export function resampleAudio(audioData, originalSampleRate, targetSampleRate) {
if (originalSampleRate === targetSampleRate) {
return audioData;
}
const ratio = originalSampleRate / targetSampleRate;
const newLength = Math.floor(audioData.length / ratio);
const resampled = new Float32Array(newLength);
for (let i = 0; i < newLength; i++) {
const originalIndex = i * ratio;
const index = Math.floor(originalIndex);
const fraction = originalIndex - index;
if (index + 1 < audioData.length) {
// Linear interpolation
resampled[i] = audioData[index] * (1 - fraction) + audioData[index + 1] * fraction;
} else {
resampled[i] = audioData[index];
}
}
return resampled;
}
/**
* Normalize audio data
*/
export function normalizeAudio(audioData) {
let maxValue = 0;
for (let i = 0; i < audioData.length; i++) {
maxValue = Math.max(maxValue, Math.abs(audioData[i]));
}
if (maxValue === 0) {
return audioData;
}
const normalized = new Float32Array(audioData.length);
const factor = 1 / maxValue;
for (let i = 0; i < audioData.length; i++) {
normalized[i] = audioData[i] * factor;
}
return normalized;
}
/**
* Convert mono to stereo or stereo to mono
*/
export function convertChannels(audioData, originalChannels, targetChannels) {
if (originalChannels === targetChannels) {
return audioData;
}
if (originalChannels === 2 && targetChannels === 1) {
// Stereo to mono: average the channels
const monoData = new Float32Array(audioData.length / 2);
for (let i = 0; i < monoData.length; i++) {
monoData[i] = (audioData[i * 2] + audioData[i * 2 + 1]) / 2;
}
return monoData;
}
if (originalChannels === 1 && targetChannels === 2) {
// Mono to stereo: duplicate the channel
const stereoData = new Float32Array(audioData.length * 2);
for (let i = 0; i < audioData.length; i++) {
stereoData[i * 2] = audioData[i];
stereoData[i * 2 + 1] = audioData[i];
}
return stereoData;
}
throw new Error(`Unsupported channel conversion: ${originalChannels} to ${targetChannels}`);
}
/**
* Apply decoding options to audio data
*/
export async function applyDecodingOptions(audioData, originalSampleRate, originalChannels, options) {
let processedData = audioData;
let sampleRate = originalSampleRate;
let channels = originalChannels;
if (!options) {
return {
processedData,
sampleRate,
channels
};
}
// Convert channels if needed
if (options.targetChannels && options.targetChannels !== channels) {
processedData = convertChannels(processedData, channels, options.targetChannels);
channels = options.targetChannels;
}
// Resample if needed
if (options.targetSampleRate && options.targetSampleRate !== sampleRate) {
processedData = resampleAudio(processedData, sampleRate, options.targetSampleRate);
sampleRate = options.targetSampleRate;
}
// Normalize if requested
if (options.normalizeAudio) {
processedData = normalizeAudio(processedData);
}
return {
processedData,
sampleRate,
channels
};
}
/**
* Extract time segment from audio data
*/
export function extractTimeSegment(audioData, sampleRate, startTimeMs, endTimeMs) {
if (!startTimeMs && !endTimeMs) {
return audioData;
}
const startSample = startTimeMs ? Math.floor(startTimeMs / 1000 * sampleRate) : 0;
const endSample = endTimeMs ? Math.floor(endTimeMs / 1000 * sampleRate) : audioData.length;
return audioData.slice(Math.max(0, startSample), Math.min(audioData.length, endSample));
}
/**
* Extract byte segment from audio data
*/
export function extractByteSegment(audioData, position, length) {
if (!position && !length) {
return audioData;
}
// Convert byte position to sample position (assuming 16-bit samples)
const startSample = position ? Math.floor(position / 2) : 0;
const endSample = length ? startSample + Math.floor(length / 2) : audioData.length;
return audioData.slice(Math.max(0, startSample), Math.min(audioData.length, endSample));
}
/**
* Validate file URI
*/
export function validateFileUri(fileUri) {
if (!fileUri || typeof fileUri !== 'string') {
return false;
}
// Check if it's a valid file URI
return fileUri.startsWith('file://') || fileUri.startsWith('content://') || fileUri.startsWith('asset://') || fileUri.startsWith('http://') || fileUri.startsWith('https://');
}
/**
* Get file extension from URI
*/
export function getFileExtension(fileUri) {
const parts = fileUri.split('.');
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
}
/**
* Check if file format is supported
*/
export function isSupportedFormat(fileUri) {
const extension = getFileExtension(fileUri);
const supportedFormats = ['wav', 'mp3', 'aac', 'm4a', 'flac', 'ogg'];
return supportedFormats.includes(extension);
}
//# sourceMappingURL=utils.js.map