wav-file-decoder
Version:
A simple decoder for WAV audio files
215 lines (214 loc) • 9.07 kB
JavaScript
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;
}