UNPKG

@pompeii-labs/audio

Version:
566 lines (555 loc) 10.6 kB
'use strict'; // src/decoders/wav.ts function decodeWAV(bytes) { const view = new DataView(bytes.buffer); if (String.fromCharCode(...bytes.slice(0, 4)) !== "RIFF" || String.fromCharCode(...bytes.slice(8, 12)) !== "WAVE") { console.log("Invalid WAV header detected"); return null; } let offset = 12; let audioFormat = 0; let channels = 0; let sampleRate = 0; let bitsPerSample = 0; let dataOffset = 0; let dataSize = 0; while (offset < bytes.length - 8) { const chunkId = String.fromCharCode(...bytes.slice(offset, offset + 4)); const chunkSize = view.getUint32(offset + 4, true); if (chunkId === "fmt ") { audioFormat = view.getUint16(offset + 8, true); channels = view.getUint16(offset + 10, true); sampleRate = view.getUint32(offset + 12, true); bitsPerSample = view.getUint16(offset + 22, true); } else if (chunkId === "data") { dataOffset = offset + 8; const availableSize = bytes.length - dataOffset; dataSize = Math.min(chunkSize, availableSize); break; } offset += 8 + chunkSize; } if (audioFormat !== 1) { throw new Error(`Unsupported WAV format: ${audioFormat}`); } const bytesPerSample = bitsPerSample / 8; const numSamples = Math.floor(dataSize / bytesPerSample); const samples = new Float32Array(numSamples); for (let i = 0; i < numSamples; i++) { const byteOffset = dataOffset + i * bytesPerSample; if (byteOffset + bytesPerSample > bytes.length) { console.error("Buffer overflow detected:", { byteOffset, bytesPerSample, bufferLength: bytes.length, sampleIndex: i }); throw new Error("Buffer overflow while reading samples"); } let sample = 0; if (bitsPerSample === 8) { sample = (bytes[byteOffset] - 128) / 128; } else if (bitsPerSample === 16) { const rawValue = view.getInt16(byteOffset, true); sample = rawValue / 32768; } else if (bitsPerSample === 24) { const byte1 = bytes[byteOffset]; const byte2 = bytes[byteOffset + 1]; const byte3 = bytes[byteOffset + 2]; const value = byte3 << 16 | byte2 << 8 | byte1; sample = (value > 8388607 ? value - 16777216 : value) / 8388608; } else if (bitsPerSample === 32) { sample = view.getFloat32(byteOffset, true); } samples[i] = Math.max(-1, Math.min(1, sample)); } const result = { sampleRate, channels, bitsPerSample, samples, duration: numSamples / (sampleRate * channels) // Calculate duration in seconds }; return result; } // src/decoders/mulaw.ts function mulawToPcm16(mulawData) { const pcmData = new Int16Array(mulawData.length); for (let i = 0; i < mulawData.length; i++) { pcmData[i] = mulawToLinear(mulawData[i]); } return pcmData; } function mulawToLinear(mulawByte) { const inverted = mulawByte ^ 255; const sign = inverted & 128; const segment = (inverted & 112) >> 4; const step = inverted & 15; let linear; if (segment === 0) { linear = (step << 1) + 1; } else { linear = (step << 1) + 1 + 32 << segment + 2; } linear -= 33; return sign ? -linear : linear; } // src/encoders/mulaw.ts var BIAS = 132; var CLIP = 32635; var encodeTable = [ 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 ]; function encodeSample(sample) { const sign = sample >> 8 & 128; if (sign !== 0) sample = -sample; sample = sample + BIAS; if (sample > CLIP) sample = CLIP; const exponent = encodeTable[sample >> 7 & 255]; const mantissa = sample >> exponent + 3 & 15; return ~(sign | exponent << 4 | mantissa); } function pcm16ToMulaw(pcmData) { const mulawData = new Uint8Array(pcmData.length); for (let i = 0; i < pcmData.length; i++) { mulawData[i] = encodeSample(pcmData[i]); } return mulawData; } // src/helpers/bufferToInt16Array.ts function bufferToInt16Array(buffer) { return new Int16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 2); } // src/helpers/int16ArrayToBuffer.ts function int16ArrayToBuffer(int16Array) { return Buffer.from(int16Array.buffer, int16Array.byteOffset, int16Array.byteLength); } // src/helpers/convertAudioFormat.ts function encodePcm(audio, encoding) { switch (encoding) { case "mulaw": return Buffer.from(pcm16ToMulaw(audio)); case "pcm": return int16ArrayToBuffer(audio); default: throw new Error(`Could not encode audio: Unsupported encoding: ${encoding}`); } } function decodeToPcm(audio, encoding) { switch (encoding) { case "mulaw": return mulawToPcm16(audio); case "pcm": return bufferToInt16Array(audio); default: throw new Error(`Could not decode audio: Unsupported encoding: ${encoding}`); } } // src/helpers/detectFormat.ts function identifyAudioFormat(bytes) { const checkBytes = (offset, expected) => { if (offset + expected.length > bytes.length) return false; return expected.every((byte, i) => bytes[offset + i] === byte); }; if (checkBytes(0, [82, 73, 70, 70]) && // "RIFF" checkBytes(8, [87, 65, 86, 69])) { return { format: "WAV", mimeType: "audio/wav", description: "Waveform Audio File Format" }; } if (checkBytes(0, [73, 68, 51]) || // ID3 tag checkBytes(0, [255, 251]) || // MP3 frame sync checkBytes(0, [255, 243]) || // MP3 frame sync checkBytes(0, [255, 242])) { return { format: "MP3", mimeType: "audio/mpeg", description: "MPEG Audio Layer III" }; } if (checkBytes(0, [102, 76, 97, 67])) { return { format: "FLAC", mimeType: "audio/flac", description: "Free Lossless Audio Codec" }; } if (checkBytes(0, [79, 103, 103, 83])) { return { format: "OGG", mimeType: "audio/ogg", description: "Ogg Vorbis" }; } if (checkBytes(4, [102, 116, 121, 112]) && // "ftyp" (checkBytes(8, [77, 52, 65, 32]) || // "M4A " checkBytes(8, [105, 115, 111, 109]))) { return { format: "M4A", mimeType: "audio/mp4", description: "MPEG-4 Audio" }; } if (checkBytes(0, [70, 79, 82, 77]) && // "FORM" checkBytes(8, [65, 73, 70, 70])) { return { format: "AIFF", mimeType: "audio/aiff", description: "Audio Interchange File Format" }; } if (checkBytes( 0, [ 48, 38, 178, 117, 142, 102, 207, 17, 166, 217, 0, 170, 0, 98, 206, 108 ] )) { return { format: "WMA", mimeType: "audio/x-ms-wma", description: "Windows Media Audio" }; } return null; } // src/helpers/generateFadeOutSamples.ts function generateFadeOutSamples(lastSampleValue, fadeDurationMs, sampleRate) { const fadeNumSamples = Math.ceil(fadeDurationMs / 1e3 * sampleRate); const fadeSamples = new Int16Array(fadeNumSamples); for (let i = 0; i < fadeNumSamples; i++) { const progress = 1 - i / (fadeNumSamples - 1); fadeSamples[i] = Math.round(lastSampleValue * progress); } return new Uint8Array(fadeSamples.buffer); } // src/helpers/resamplePcm.ts function resamplePcm(pcm, originalSampleRate, targetSampleRate) { if (originalSampleRate === targetSampleRate) { return pcm; } const ratio = originalSampleRate / targetSampleRate; const newLength = Math.floor(pcm.length / ratio); const newSamples = new Int16Array(newLength); if (ratio < 1) { for (let i = 0; i < newSamples.length; i++) { const exactPos = i * ratio; const lowerIndex = Math.floor(exactPos); const upperIndex = Math.min(lowerIndex + 1, pcm.length - 1); const fraction = exactPos - lowerIndex; const lowerSample = pcm[lowerIndex]; const upperSample = pcm[upperIndex]; newSamples[i] = Math.round(lowerSample + (upperSample - lowerSample) * fraction); } return newSamples; } const nyquistFreq = targetSampleRate / 2; const cutoffFreq = nyquistFreq * 0.9; const filteredPcm = applyLowPassFilter(pcm, originalSampleRate, cutoffFreq); for (let i = 0; i < newSamples.length; i++) { const exactPos = i * ratio; const lowerIndex = Math.floor(exactPos); const upperIndex = Math.min(lowerIndex + 1, filteredPcm.length - 1); const fraction = exactPos - lowerIndex; const lowerSample = filteredPcm[lowerIndex]; const upperSample = filteredPcm[upperIndex]; newSamples[i] = Math.round(lowerSample + (upperSample - lowerSample) * fraction); } return newSamples; } function applyLowPassFilter(pcm, sampleRate, cutoffFreq) { const filterOrder = Math.max(3, Math.floor(sampleRate / (cutoffFreq * 4))); const filtered = new Int16Array(pcm.length); for (let i = 0; i < pcm.length; i++) { let sum = 0; let count = 0; for (let j = Math.max(0, i - filterOrder); j <= Math.min(pcm.length - 1, i + filterOrder); j++) { sum += pcm[j]; count++; } filtered[i] = Math.round(sum / count); } return filtered; } exports.bufferToInt16Array = bufferToInt16Array; exports.decodeToPcm = decodeToPcm; exports.decodeWAV = decodeWAV; exports.encodePcm = encodePcm; exports.generateFadeOutSamples = generateFadeOutSamples; exports.identifyAudioFormat = identifyAudioFormat; exports.int16ArrayToBuffer = int16ArrayToBuffer; exports.mulawToPcm16 = mulawToPcm16; exports.pcm16ToMulaw = pcm16ToMulaw; exports.resamplePcm = resamplePcm;