UNPKG

@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
/** * 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