/*
* Copyright (c) 2019 Rafael da Silva Rocha.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
/**
* @fileoverview The WaveFileReader class.
* @see https://github.com/rochars/wavefile-reader
*/
/** @module wavefile-reader */
import { RIFFFile } from 'riff-file';
import { unpackString, unpack } from 'byte-data';
/**
* A class to read wav files.
* @extends RIFFFile
*/
export class WaveFileReader extends RIFFFile {
/**
* @param {?Uint8Array=} wavBuffer A wave file buffer.
* @param {boolean=} loadSamples True if the samples should be loaded.
* @throws {Error} If container is not RIFF, RIFX or RF64.
* @throws {Error} If format is not WAVE.
* @throws {Error} If no 'fmt ' chunk is found.
* @throws {Error} If no 'data' chunk is found.
*/
constructor(wavBuffer=null, loadSamples=true) {
super();
// Include 'RF64' as a supported container format
this.supported_containers.push('RF64');
/**
* The data of the 'fmt' chunk.
* @type {!Object<string, *>}
*/
this.fmt = {
/** @type {string} */
chunkId: '',
/** @type {number} */
chunkSize: 0,
/** @type {number} */
audioFormat: 0,
/** @type {number} */
numChannels: 0,
/** @type {number} */
sampleRate: 0,
/** @type {number} */
byteRate: 0,
/** @type {number} */
blockAlign: 0,
/** @type {number} */
bitsPerSample: 0,
/** @type {number} */
cbSize: 0,
/** @type {number} */
validBitsPerSample: 0,
/** @type {number} */
dwChannelMask: 0,
/**
* 4 32-bit values representing a 128-bit ID
* @type {!Array<number>}
*/
subformat: []
};
/**
* The data of the 'fact' chunk.
* @type {!Object<string, *>}
*/
this.fact = {
/** @type {string} */
chunkId: '',
/** @type {number} */
chunkSize: 0,
/** @type {number} */
dwSampleLength: 0
};
/**
* The data of the 'cue ' chunk.
* @type {!Object<string, *>}
*/
this.cue = {
/** @type {string} */
chunkId: '',
/** @type {number} */
chunkSize: 0,
/** @type {number} */
dwCuePoints: 0,
/** @type {!Array<!Object>} */
points: [],
};
/**
* The data of the 'smpl' chunk.
* @type {!Object<string, *>}
*/
this.smpl = {
/** @type {string} */
chunkId: '',
/** @type {number} */
chunkSize: 0,
/** @type {number} */
dwManufacturer: 0,
/** @type {number} */
dwProduct: 0,
/** @type {number} */
dwSamplePeriod: 0,
/** @type {number} */
dwMIDIUnityNote: 0,
/** @type {number} */
dwMIDIPitchFraction: 0,
/** @type {number} */
dwSMPTEFormat: 0,
/** @type {number} */
dwSMPTEOffset: 0,
/** @type {number} */
dwNumSampleLoops: 0,
/** @type {number} */
dwSamplerData: 0,
/** @type {!Array<!Object>} */
loops: []
};
/**
* The data of the 'bext' chunk.
* @type {!Object<string, *>}
*/
this.bext = {
/** @type {string} */
chunkId: '',
/** @type {number} */
chunkSize: 0,
/** @type {string} */
description: '', //256
/** @type {string} */
originator: '', //32
/** @type {string} */
originatorReference: '', //32
/** @type {string} */
originationDate: '', //10
/** @type {string} */
originationTime: '', //8
/**
* 2 32-bit values, timeReference high and low
* @type {!Array<number>}
*/
timeReference: [0, 0],
/** @type {number} */
version: 0, //WORD
/** @type {string} */
UMID: '', // 64 chars
/** @type {number} */
loudnessValue: 0, //WORD
/** @type {number} */
loudnessRange: 0, //WORD
/** @type {number} */
maxTruePeakLevel: 0, //WORD
/** @type {number} */
maxMomentaryLoudness: 0, //WORD
/** @type {number} */
maxShortTermLoudness: 0, //WORD
/** @type {string} */
reserved: '', //180
/** @type {string} */
codingHistory: '' // string, unlimited
};
/**
* The data of the 'ds64' chunk.
* Used only with RF64 files.
* @type {!Object<string, *>}
*/
this.ds64 = {
/** @type {string} */
chunkId: '',
/** @type {number} */
chunkSize: 0,
/** @type {number} */
riffSizeHigh: 0, // DWORD
/** @type {number} */
riffSizeLow: 0, // DWORD
/** @type {number} */
dataSizeHigh: 0, // DWORD
/** @type {number} */
dataSizeLow: 0, // DWORD
/** @type {number} */
originationTime: 0, // DWORD
/** @type {number} */
sampleCountHigh: 0, // DWORD
/** @type {number} */
sampleCountLow: 0 // DWORD
/** @type {number} */
//'tableLength': 0, // DWORD
/** @type {!Array<number>} */
//'table': []
};
/**
* The data of the 'data' chunk.
* @type {!Object<string, *>}
*/
this.data = {
/** @type {string} */
chunkId: '',
/** @type {number} */
chunkSize: 0,
/** @type {!Uint8Array} */
samples: new Uint8Array(0)
};
/**
* The data of the 'LIST' chunks.
* Each item in this list look like this:
* {
* chunkId: '',
* chunkSize: 0,
* format: '',
* subChunks: []
* }
* @type {!Array<!Object>}
*/
this.LIST = [];
/**
* The data of the 'junk' chunk.
* @type {!Object<string, *>}
*/
this.junk = {
/** @type {string} */
chunkId: '',
/** @type {number} */
chunkSize: 0,
/** @type {!Array<number>} */
chunkData: []
};
/**
* @type {{
be: boolean,
bits: number,
fp: boolean,
signed: boolean
}}
* @protected
*/
this.uInt16 = {
bits: 16, be: false, signed: false, fp: false
};
// If a file was informed, read it
if (wavBuffer) {
this.fromBuffer(wavBuffer, loadSamples);
}
}
/**
* Set up the WaveFileReader object from a byte buffer.
* @param {!Uint8Array} wavBuffer The buffer.
* @param {boolean=} samples True if the samples should be loaded.
* @throws {Error} If container is not RIFF, RIFX or RF64.
* @throws {Error} If format is not WAVE.
* @throws {Error} If no 'fmt ' chunk is found.
* @throws {Error} If no 'data' chunk is found.
*/
fromBuffer(wavBuffer, samples=true) {
// Always should reset the chunks when reading from a buffer
this.clearHeaders();
this.setSignature(wavBuffer);
this.uInt16.be = this.uInt32.be;
if (this.format != 'WAVE') {
throw Error('Could not find the "WAVE" format identifier');
}
this.readDs64Chunk_(wavBuffer);
this.readFmtChunk_(wavBuffer);
this.readFactChunk_(wavBuffer);
this.readBextChunk_(wavBuffer);
this.readCueChunk_(wavBuffer);
this.readSmplChunk_(wavBuffer);
this.readDataChunk_(wavBuffer, samples);
this.readJunkChunk_(wavBuffer);
this.readLISTChunk_(wavBuffer);
}
/**
* Reset the chunks of the WaveFileReader instance.
* @protected
* @ignore
*/
clearHeaders() {
let tmpWav = new WaveFileReader();
Object.assign(this.fmt, tmpWav.fmt);
Object.assign(this.fact, tmpWav.fact);
Object.assign(this.cue, tmpWav.cue);
Object.assign(this.smpl, tmpWav.smpl);
Object.assign(this.bext, tmpWav.bext);
Object.assign(this.ds64, tmpWav.ds64);
Object.assign(this.data, tmpWav.data);
this.LIST = [];
Object.assign(this.junk, tmpWav.junk);
}
/**
* Read the 'fmt ' chunk of a wave file.
* @param {!Uint8Array} buffer The wav file buffer.
* @throws {Error} If no 'fmt ' chunk is found.
* @private
*/
readFmtChunk_(buffer) {
/** @type {?Object} */
let chunk = this.findChunk('fmt ');
if (chunk) {
this.head = chunk.chunkData.start;
this.fmt.chunkId = chunk.chunkId;
this.fmt.chunkSize = chunk.chunkSize;
this.fmt.audioFormat = this.readUInt16_(buffer);
this.fmt.numChannels = this.readUInt16_(buffer);
this.fmt.sampleRate = this.readUInt32(buffer);
this.fmt.byteRate = this.readUInt32(buffer);
this.fmt.blockAlign = this.readUInt16_(buffer);
this.fmt.bitsPerSample = this.readUInt16_(buffer);
this.readFmtExtension_(buffer);
} else {
throw Error('Could not find the "fmt " chunk');
}
}
/**
* Read the 'fmt ' chunk extension.
* @param {!Uint8Array} buffer The wav file buffer.
* @private
*/
readFmtExtension_(buffer) {
if (this.fmt.chunkSize > 16) {
this.fmt.cbSize = this.readUInt16_(buffer);
if (this.fmt.chunkSize > 18) {
this.fmt.validBitsPerSample = this.readUInt16_(buffer);
if (this.fmt.chunkSize > 20) {
this.fmt.dwChannelMask = this.readUInt32(buffer);
this.fmt.subformat = [
this.readUInt32(buffer),
this.readUInt32(buffer),
this.readUInt32(buffer),
this.readUInt32(buffer)];
}
}
}
}
/**
* Read the 'fact' chunk of a wav file.
* @param {!Uint8Array} buffer The wav file buffer.
* @private
*/
readFactChunk_(buffer) {
/** @type {?Object} */
let chunk = this.findChunk('fact');
if (chunk) {
this.head = chunk.chunkData.start;
this.fact.chunkId = chunk.chunkId;
this.fact.chunkSize = chunk.chunkSize;
this.fact.dwSampleLength = this.readUInt32(buffer);
}
}
/**
* Read the 'cue ' chunk of a wave file.
* @param {!Uint8Array} buffer The wav file buffer.
* @private
*/
readCueChunk_(buffer) {
/** @type {?Object} */
let chunk = this.findChunk('cue ');
if (chunk) {
this.head = chunk.chunkData.start;
this.cue.chunkId = chunk.chunkId;
this.cue.chunkSize = chunk.chunkSize;
this.cue.dwCuePoints = this.readUInt32(buffer);
for (let i = 0; i < this.cue.dwCuePoints; i++) {
this.cue.points.push({
dwName: this.readUInt32(buffer),
dwPosition: this.readUInt32(buffer),
fccChunk: this.readString(buffer, 4),
dwChunkStart: this.readUInt32(buffer),
dwBlockStart: this.readUInt32(buffer),
dwSampleOffset: this.readUInt32(buffer),
});
}
}
}
/**
* Read the 'smpl' chunk of a wave file.
* @param {!Uint8Array} buffer The wav file buffer.
* @private
*/
readSmplChunk_(buffer) {
/** @type {?Object} */
let chunk = this.findChunk('smpl');
if (chunk) {
this.head = chunk.chunkData.start;
this.smpl.chunkId = chunk.chunkId;
this.smpl.chunkSize = chunk.chunkSize;
this.smpl.dwManufacturer = this.readUInt32(buffer);
this.smpl.dwProduct = this.readUInt32(buffer);
this.smpl.dwSamplePeriod = this.readUInt32(buffer);
this.smpl.dwMIDIUnityNote = this.readUInt32(buffer);
this.smpl.dwMIDIPitchFraction = this.readUInt32(buffer);
this.smpl.dwSMPTEFormat = this.readUInt32(buffer);
this.smpl.dwSMPTEOffset = this.readUInt32(buffer);
this.smpl.dwNumSampleLoops = this.readUInt32(buffer);
this.smpl.dwSamplerData = this.readUInt32(buffer);
for (let i = 0; i < this.smpl.dwNumSampleLoops; i++) {
this.smpl.loops.push({
dwName: this.readUInt32(buffer),
dwType: this.readUInt32(buffer),
dwStart: this.readUInt32(buffer),
dwEnd: this.readUInt32(buffer),
dwFraction: this.readUInt32(buffer),
dwPlayCount: this.readUInt32(buffer),
});
}
}
}
/**
* Read the 'data' chunk of a wave file.
* @param {!Uint8Array} buffer The wav file buffer.
* @param {boolean} samples True if the samples should be loaded.
* @throws {Error} If no 'data' chunk is found.
* @private
*/
readDataChunk_(buffer, samples) {
/** @type {?Object} */
let chunk = this.findChunk('data');
if (chunk) {
this.data.chunkId = 'data';
this.data.chunkSize = chunk.chunkSize;
if (samples) {
this.data.samples = buffer.slice(
chunk.chunkData.start,
chunk.chunkData.end);
}
} else {
throw Error('Could not find the "data" chunk');
}
}
/**
* Read the 'bext' chunk of a wav file.
* @param {!Uint8Array} buffer The wav file buffer.
* @private
*/
readBextChunk_(buffer) {
/** @type {?Object} */
let chunk = this.findChunk('bext');
if (chunk) {
this.head = chunk.chunkData.start;
this.bext.chunkId = chunk.chunkId;
this.bext.chunkSize = chunk.chunkSize;
this.bext.description = this.readString(buffer, 256);
this.bext.originator = this.readString(buffer, 32);
this.bext.originatorReference = this.readString(buffer, 32);
this.bext.originationDate = this.readString(buffer, 10);
this.bext.originationTime = this.readString(buffer, 8);
this.bext.timeReference = [
this.readUInt32(buffer),
this.readUInt32(buffer)];
this.bext.version = this.readUInt16_(buffer);
this.bext.UMID = this.readString(buffer, 64);
this.bext.loudnessValue = this.readUInt16_(buffer);
this.bext.loudnessRange = this.readUInt16_(buffer);
this.bext.maxTruePeakLevel = this.readUInt16_(buffer);
this.bext.maxMomentaryLoudness = this.readUInt16_(buffer);
this.bext.maxShortTermLoudness = this.readUInt16_(buffer);
this.bext.reserved = this.readString(buffer, 180);
this.bext.codingHistory = this.readString(
buffer, this.bext.chunkSize - 602);
}
}
/**
* Read the 'ds64' chunk of a wave file.
* @param {!Uint8Array} buffer The wav file buffer.
* @throws {Error} If no 'ds64' chunk is found and the file is RF64.
* @private
*/
readDs64Chunk_(buffer) {
/** @type {?Object} */
let chunk = this.findChunk('ds64');
if (chunk) {
this.head = chunk.chunkData.start;
this.ds64.chunkId = chunk.chunkId;
this.ds64.chunkSize = chunk.chunkSize;
this.ds64.riffSizeHigh = this.readUInt32(buffer);
this.ds64.riffSizeLow = this.readUInt32(buffer);
this.ds64.dataSizeHigh = this.readUInt32(buffer);
this.ds64.dataSizeLow = this.readUInt32(buffer);
this.ds64.originationTime = this.readUInt32(buffer);
this.ds64.sampleCountHigh = this.readUInt32(buffer);
this.ds64.sampleCountLow = this.readUInt32(buffer);
//if (wav.ds64.chunkSize > 28) {
// wav.ds64.tableLength = unpack(
// chunkData.slice(28, 32), uInt32_);
// wav.ds64.table = chunkData.slice(
// 32, 32 + wav.ds64.tableLength);
//}
} else {
if (this.container == 'RF64') {
throw Error('Could not find the "ds64" chunk');
}
}
}
/**
* Read the 'LIST' chunks of a wave file.
* @param {!Uint8Array} buffer The wav file buffer.
* @private
*/
readLISTChunk_(buffer) {
/** @type {?Object} */
let listChunks = this.findChunk('LIST', true);
if (listChunks !== null) {
for (let j=0; j < listChunks.length; j++) {
/** @type {!Object} */
let subChunk = listChunks[j];
this.LIST.push({
chunkId: subChunk.chunkId,
chunkSize: subChunk.chunkSize,
format: subChunk.format,
subChunks: []});
for (let x=0; x<subChunk.subChunks.length; x++) {
this.readLISTSubChunks_(subChunk.subChunks[x],
subChunk.format, buffer);
}
}
}
}
/**
* Read the sub chunks of a 'LIST' chunk.
* @param {!Object} subChunk The 'LIST' subchunks.
* @param {string} format The 'LIST' format, 'adtl' or 'INFO'.
* @param {!Uint8Array} buffer The wav file buffer.
* @private
*/
readLISTSubChunks_(subChunk, format, buffer) {
if (format == 'adtl') {
if (['labl', 'note','ltxt'].indexOf(subChunk.chunkId) > -1) {
this.head = subChunk.chunkData.start;
/** @type {!Object<string, string|number>} */
let item = {
chunkId: subChunk.chunkId,
chunkSize: subChunk.chunkSize,
dwName: this.readUInt32(buffer)
};
if (subChunk.chunkId == 'ltxt') {
item.dwSampleLength = this.readUInt32(buffer);
item.dwPurposeID = this.readUInt32(buffer);
item.dwCountry = this.readUInt16_(buffer);
item.dwLanguage = this.readUInt16_(buffer);
item.dwDialect = this.readUInt16_(buffer);
item.dwCodePage = this.readUInt16_(buffer);
}
item.value = this.readZSTR_(buffer, this.head);
this.LIST[this.LIST.length - 1].subChunks.push(item);
}
// RIFF INFO tags like ICRD, ISFT, ICMT
} else if(format == 'INFO') {
this.head = subChunk.chunkData.start;
this.LIST[this.LIST.length - 1].subChunks.push({
chunkId: subChunk.chunkId,
chunkSize: subChunk.chunkSize,
value: this.readZSTR_(buffer, this.head)
});
}
}
/**
* Read the 'junk' chunk of a wave file.
* @param {!Uint8Array} buffer The wav file buffer.
* @private
*/
readJunkChunk_(buffer) {
/** @type {?Object} */
let chunk = this.findChunk('junk');
if (chunk) {
this.junk = {
chunkId: chunk.chunkId,
chunkSize: chunk.chunkSize,
chunkData: [].slice.call(buffer.slice(
chunk.chunkData.start,
chunk.chunkData.end))
};
}
}
/**
* Read bytes as a ZSTR string.
* @param {!Uint8Array} bytes The bytes.
* @param {number} index the index to start reading.
* @return {string} The string.
* @private
*/
readZSTR_(bytes, index=0) {
for (let i = index; i < bytes.length; i++) {
this.head++;
if (bytes[i] === 0) {
break;
}
}
return unpackString(bytes, index, this.head - 1);
}
/**
* Read a number from a chunk.
* @param {!Uint8Array} bytes The chunk bytes.
* @return {number} The number.
* @private
*/
readUInt16_(bytes) {
/** @type {number} */
let value = unpack(bytes, this.uInt16, this.head);
this.head += 2;
return value;
}
}