UNPKG

wav-file-decoder

Version:
215 lines (214 loc) 9.07 kB
export function isWavFile(fileData) { try { const chunks = unpackWavFileChunks(fileData); const fmt = decodeFormatChunk(chunks.get("fmt")); const data = chunks.get("data"); void getWavFileType(fmt); verifyDataChunkLength(data, fmt); return true; } catch (e) { return false; } } const audioEncodingNames = ["int", "float"]; const wavFileTypeAudioEncodings = [0, 0, 0, 1]; export function decodeWavFile(fileData) { const chunks = unpackWavFileChunks(fileData); const fmt = decodeFormatChunk(chunks.get("fmt")); const data = chunks.get("data"); const wavFileType = getWavFileType(fmt); const audioEncoding = wavFileTypeAudioEncodings[wavFileType]; const audioEncodingName = audioEncodingNames[audioEncoding]; const wavFileTypeName = audioEncodingName + fmt.bitsPerSample; verifyDataChunkLength(data, fmt); const channelData = decodeDataChunk(data, fmt, wavFileType); return { channelData, sampleRate: fmt.sampleRate, numberOfChannels: fmt.numberOfChannels, audioEncoding, bitsPerSample: fmt.bitsPerSample, wavFileTypeName }; } function unpackWavFileChunks(fileData) { let dataView; if (fileData instanceof ArrayBuffer) { dataView = new DataView(fileData); } else { dataView = new DataView(fileData.buffer, fileData.byteOffset, fileData.byteLength); } const fileLength = dataView.byteLength; if (fileLength < 20) { throw new Error("WAV file is too short."); } if (getString(dataView, 0, 4) != "RIFF") { throw new Error("Not a valid WAV file (no RIFF header)."); } const mainChunkLength = dataView.getUint32(4, true); if (8 + mainChunkLength != fileLength) { throw new Error(`Main chunk length of WAV file (${8 + mainChunkLength}) does not match file size (${fileLength}).`); } if (getString(dataView, 8, 4) != "WAVE") { throw new Error("RIFF file is not a WAV file."); } const chunks = new Map(); let fileOffset = 12; while (fileOffset < fileLength) { if (fileOffset + 8 > fileLength) { throw new Error(`Incomplete chunk prefix in WAV file at offset ${fileOffset}.`); } const chunkId = getString(dataView, fileOffset, 4).trim(); const chunkLength = dataView.getUint32(fileOffset + 4, true); if (fileOffset + 8 + chunkLength > fileLength) { throw new Error(`Incomplete chunk data in WAV file at offset ${fileOffset}.`); } const chunkData = new DataView(dataView.buffer, dataView.byteOffset + fileOffset + 8, chunkLength); chunks.set(chunkId, chunkData); const padLength = (chunkLength % 2); fileOffset += 8 + chunkLength + padLength; } return chunks; } function getString(dataView, offset, length) { const a = new Uint8Array(dataView.buffer, dataView.byteOffset + offset, length); return String.fromCharCode.apply(null, a); } function getInt24(dataView, offset) { const b0 = dataView.getInt8(offset + 2) * 0x10000; const b12 = dataView.getUint16(offset, true); return b0 + b12; } function decodeFormatChunk(dataView) { if (!dataView) { throw new Error("No format chunk found in WAV file."); } if (dataView.byteLength < 16) { throw new Error("Format chunk of WAV file is too short."); } const fmt = {}; fmt.formatCode = dataView.getUint16(0, true); fmt.numberOfChannels = dataView.getUint16(2, true); fmt.sampleRate = dataView.getUint32(4, true); fmt.bytesPerSec = dataView.getUint32(8, true); fmt.bytesPerFrame = dataView.getUint16(12, true); fmt.bitsPerSample = dataView.getUint16(14, true); return fmt; } function getWavFileType(fmt) { if (fmt.numberOfChannels < 1 || fmt.numberOfChannels > 999) { throw new Error("Invalid number of channels in WAV file."); } const bytesPerSample = Math.ceil(fmt.bitsPerSample / 8); const expectedBytesPerFrame = fmt.numberOfChannels * bytesPerSample; if (fmt.formatCode == 1 && fmt.bitsPerSample >= 1 && fmt.bitsPerSample <= 8 && fmt.bytesPerFrame == expectedBytesPerFrame) { return 0; } if (fmt.formatCode == 1 && fmt.bitsPerSample >= 9 && fmt.bitsPerSample <= 16 && fmt.bytesPerFrame == expectedBytesPerFrame) { return 1; } if (fmt.formatCode == 1 && fmt.bitsPerSample >= 17 && fmt.bitsPerSample <= 24 && fmt.bytesPerFrame == expectedBytesPerFrame) { return 2; } if (fmt.formatCode == 3 && fmt.bitsPerSample == 32 && fmt.bytesPerFrame == expectedBytesPerFrame) { return 3; } throw new Error(`Unsupported WAV file type, formatCode=${fmt.formatCode}, bitsPerSample=${fmt.bitsPerSample}, bytesPerFrame=${fmt.bytesPerFrame}, numberOfChannels=${fmt.numberOfChannels}.`); } function decodeDataChunk(data, fmt, wavFileType) { switch (wavFileType) { case 0: return decodeDataChunk_uint8(data, fmt); case 1: return decodeDataChunk_int16(data, fmt); case 2: return decodeDataChunk_int24(data, fmt); case 3: return decodeDataChunk_float32(data, fmt); default: throw new Error("No decoder."); } } function decodeDataChunk_int16(data, fmt) { const channelData = allocateChannelDataArrays(data.byteLength, fmt); const numberOfChannels = fmt.numberOfChannels; const numberOfFrames = channelData[0].length; let offs = 0; for (let frameNo = 0; frameNo < numberOfFrames; frameNo++) { for (let channelNo = 0; channelNo < numberOfChannels; channelNo++) { const sampleValueInt = data.getInt16(offs, true); const sampleValueFloat = sampleValueInt / 0x8000; channelData[channelNo][frameNo] = sampleValueFloat; offs += 2; } } return channelData; } function decodeDataChunk_uint8(data, fmt) { const channelData = allocateChannelDataArrays(data.byteLength, fmt); const numberOfChannels = fmt.numberOfChannels; const numberOfFrames = channelData[0].length; let offs = 0; for (let frameNo = 0; frameNo < numberOfFrames; frameNo++) { for (let channelNo = 0; channelNo < numberOfChannels; channelNo++) { const sampleValueInt = data.getUint8(offs); const sampleValueFloat = (sampleValueInt - 0x80) / 0x80; channelData[channelNo][frameNo] = sampleValueFloat; offs += 1; } } return channelData; } function decodeDataChunk_int24(data, fmt) { const channelData = allocateChannelDataArrays(data.byteLength, fmt); const numberOfChannels = fmt.numberOfChannels; const numberOfFrames = channelData[0].length; let offs = 0; for (let frameNo = 0; frameNo < numberOfFrames; frameNo++) { for (let channelNo = 0; channelNo < numberOfChannels; channelNo++) { const sampleValueInt = getInt24(data, offs); const sampleValueFloat = sampleValueInt / 0x800000; channelData[channelNo][frameNo] = sampleValueFloat; offs += 3; } } return channelData; } function decodeDataChunk_float32(data, fmt) { const channelData = allocateChannelDataArrays(data.byteLength, fmt); const numberOfChannels = fmt.numberOfChannels; const numberOfFrames = channelData[0].length; let offs = 0; for (let frameNo = 0; frameNo < numberOfFrames; frameNo++) { for (let channelNo = 0; channelNo < numberOfChannels; channelNo++) { const sampleValueFloat = data.getFloat32(offs, true); channelData[channelNo][frameNo] = sampleValueFloat; offs += 4; } } return channelData; } function allocateChannelDataArrays(dataLength, fmt) { const numberOfFrames = Math.floor(dataLength / fmt.bytesPerFrame); const channelData = new Array(fmt.numberOfChannels); for (let channelNo = 0; channelNo < fmt.numberOfChannels; channelNo++) { channelData[channelNo] = new Float32Array(numberOfFrames); } return channelData; } function verifyDataChunkLength(data, fmt) { if (!data) { throw new Error("No data chunk found in WAV file."); } if (data.byteLength % fmt.bytesPerFrame != 0) { throw new Error("WAV file data chunk length is not a multiple of frame size."); } } export function getWavFileInfo(fileData) { const chunks = unpackWavFileChunks(fileData); const chunkInfo = getChunkInfo(chunks); const fmt = decodeFormatChunk(chunks.get("fmt")); return { chunkInfo, fmt }; } function getChunkInfo(chunks) { const chunkInfo = []; for (const e of chunks) { const ci = {}; ci.chunkId = e[0]; ci.dataOffset = e[1].byteOffset; ci.dataLength = e[1].byteLength; chunkInfo.push(ci); } chunkInfo.sort((e1, e2) => e1.dataOffset - e2.dataOffset); return chunkInfo; }