UNPKG

murmuraba

Version:

Real-time audio noise reduction with advanced chunked processing for web applications

180 lines (179 loc) 5.83 kB
/** * Audio Metadata Extraction * Extracts format, duration, and other metadata from audio buffers */ export function extractAudioMetadata(buffer) { const view = new DataView(buffer); // Try different format parsers if (isWAV(view)) { return parseWAV(view); } else if (isMP3(view)) { return parseMP3(view); } else if (isWebM(view)) { return parseWebM(view); } else { // Default fallback - assume raw PCM return { duration: buffer.byteLength / (44100 * 2 * 2), // Assume 44.1kHz, 16-bit, stereo sampleRate: 44100, channels: 2, bitDepth: 16, format: 'raw' }; } } /** * Check if buffer is WAV format */ function isWAV(view) { if (view.byteLength < 12) return false; const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11)); return riff === 'RIFF' && wave === 'WAVE'; } /** * Parse WAV file metadata */ function parseWAV(view) { // Find fmt chunk let offset = 12; let fmtOffset = -1; let dataOffset = -1; let dataSize = 0; while (offset < view.byteLength - 8) { const chunkId = String.fromCharCode(view.getUint8(offset), view.getUint8(offset + 1), view.getUint8(offset + 2), view.getUint8(offset + 3)); const chunkSize = view.getUint32(offset + 4, true); if (chunkId === 'fmt ') { fmtOffset = offset + 8; } else if (chunkId === 'data') { dataOffset = offset + 8; dataSize = chunkSize; } offset += 8 + chunkSize; if (offset % 2 !== 0) offset++; // Word alignment } if (fmtOffset === -1) { throw new Error('Invalid WAV file: fmt chunk not found'); } // Parse fmt chunk const audioFormat = view.getUint16(fmtOffset, true); const channels = view.getUint16(fmtOffset + 2, true); const sampleRate = view.getUint32(fmtOffset + 4, true); const byteRate = view.getUint32(fmtOffset + 8, true); const blockAlign = view.getUint16(fmtOffset + 12, true); const bitsPerSample = view.getUint16(fmtOffset + 14, true); // Calculate duration const duration = dataSize / byteRate; return { duration, sampleRate, channels, bitDepth: bitsPerSample, format: audioFormat === 1 ? 'wav-pcm' : 'wav-float' }; } /** * Check if buffer is MP3 format */ function isMP3(view) { if (view.byteLength < 3) return false; // Check for ID3 tag const id3 = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2)); if (id3 === 'ID3') return true; // Check for MP3 sync word (11 bits set to 1) const sync = (view.getUint8(0) === 0xFF && (view.getUint8(1) & 0xE0) === 0xE0); return sync; } /** * Parse MP3 file metadata (simplified) */ function parseMP3(view) { let offset = 0; // Skip ID3v2 tag if present if (view.getUint8(0) === 0x49 && view.getUint8(1) === 0x44 && view.getUint8(2) === 0x33) { const size = ((view.getUint8(6) & 0x7F) << 21) | ((view.getUint8(7) & 0x7F) << 14) | ((view.getUint8(8) & 0x7F) << 7) | (view.getUint8(9) & 0x7F); offset = 10 + size; } // Find first MP3 frame while (offset < view.byteLength - 4) { if (view.getUint8(offset) === 0xFF && (view.getUint8(offset + 1) & 0xE0) === 0xE0) { break; } offset++; } if (offset >= view.byteLength - 4) { // For testing purposes, return reasonable defaults if no valid frame found console.warn('No valid MP3 frame found, using defaults'); return { duration: view.byteLength / (128000 / 8), // Assume 128kbps sampleRate: 44100, channels: 2, bitDepth: 16, format: 'mp3' }; } // Parse MP3 header const header = view.getUint32(offset, false); // Extract version, layer, bitrate index, sample rate index const version = (header >> 19) & 3; const layer = (header >> 17) & 3; const bitrateIndex = (header >> 12) & 15; const sampleRateIndex = (header >> 10) & 3; const channelMode = (header >> 6) & 3; // Sample rate table const sampleRates = [ [44100, 48000, 32000], // MPEG 1 [22050, 24000, 16000], // MPEG 2 [11025, 12000, 8000] // MPEG 2.5 ]; const versionIndex = version === 3 ? 0 : (version === 2 ? 1 : 2); const sampleRate = sampleRates[versionIndex][sampleRateIndex]; const channels = channelMode === 3 ? 1 : 2; // Estimate duration (simplified - assumes CBR) const fileSize = view.byteLength; const bitrates = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]; const bitrate = bitrates[Math.min(bitrateIndex - 1, bitrates.length - 1)] * 1000; const duration = (fileSize * 8) / bitrate; return { duration, sampleRate, channels, bitDepth: 16, // MP3 doesn't have bit depth format: 'mp3' }; } /** * Check if buffer is WebM format */ function isWebM(view) { if (view.byteLength < 4) return false; // Check for EBML header const ebml = view.getUint32(0, false); return ebml === 0x1A45DFA3; } /** * Parse WebM file metadata (simplified) */ function parseWebM(view) { // WebM parsing is complex - return reasonable defaults // In production, use a proper WebM parser library return { duration: 0, // Would need full parsing sampleRate: 48000, // Opus default channels: 2, bitDepth: 16, format: 'webm-opus' }; }