UNPKG

wavefile

Version:

Create, read and write wav files according to the specs.

703 lines (679 loc) 21.6 kB
/* * Copyright (c) 2017-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 */ import { RIFFFile } from './riff-file'; import { unpackString, unpack } from './parsers/binary'; /** * A class to read wav files. * @extends RIFFFile */ export class WaveFileReader extends RIFFFile { constructor() { 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 'iXML' chunk. * @type {!Object<string, *>} */ this.iXML = { /** @type {string} */ chunkId: '', /** @type {number} */ chunkSize: 0, /** @type {string} */ value: '' }; /** * 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: [] }; /** * The data of the '_PMX' chunk. * @type {!Object<string, *>} */ this._PMX = { /** @type {string} */ chunkId: '', /** @type {number} */ chunkSize: 0, /** @type {string} */ value: '' }; /** * @type {{be: boolean, bits: number, fp: boolean, signed: boolean}} * @protected */ this.uInt16 = {bits: 16, be: false, signed: false, fp: false}; } /** * Set up the WaveFileReader object from a byte buffer. * @param {!Uint8Array} wavBuffer The buffer. * @param {boolean=} [samples=true] 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.readiXMLChunk_(wavBuffer); this.readCueChunk_(wavBuffer); this.readSmplChunk_(wavBuffer); this.readDataChunk_(wavBuffer, samples); this.readJunkChunk_(wavBuffer); this.readLISTChunk_(wavBuffer); this.read_PMXChunk_(wavBuffer); } /** * Reset the chunks of the WaveFileReader instance. * @protected * @ignore */ clearHeaders() { /** @type {!Object} */ 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.iXML, tmpWav.iXML); Object.assign(this.ds64, tmpWav.ds64); Object.assign(this.data, tmpWav.data); this.LIST = []; Object.assign(this.junk, tmpWav.junk); Object.assign(this._PMX, tmpWav._PMX); } /** * 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 'iXML' chunk of a wav file. * @param {!Uint8Array} buffer The wav file buffer. * @private */ readiXMLChunk_(buffer) { /** @type {?Object} */ let chunk = this.findChunk('iXML'); if (chunk) { this.head = chunk.chunkData.start; this.iXML.chunkId = chunk.chunkId; this.iXML.chunkSize = chunk.chunkSize; this.iXML.value = unpackString( buffer, this.head, this.head + this.iXML.chunkSize); } } /** * 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.readLISTadtlSubChunks_(buffer, subChunk); } // RIFF INFO tags like ICRD, ISFT, ICMT } else if(format == 'INFO') { this.readLISTINFOSubChunks_(buffer, subChunk); } } /** * Read the sub chunks of a 'LIST' chunk of type 'adtl'. * @param {!Uint8Array} buffer The wav file buffer. * @param {!Object} subChunk The 'LIST' subchunks. * @private */ readLISTadtlSubChunks_(buffer, subChunk) { 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 = ''; // kept for compatibility } else { item.value = this.readZSTR_(buffer, this.head); } this.LIST[this.LIST.length - 1].subChunks.push(item); } /** * Read the sub chunks of a 'LIST' chunk of type 'INFO'. * @param {!Uint8Array} buffer The wav file buffer. * @param {!Object} subChunk The 'LIST' subchunks. * @private */ readLISTINFOSubChunks_(buffer, subChunk) { 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 the '_PMX' chunk of a wav file. * @param {!Uint8Array} buffer The wav file buffer. * @private */ read_PMXChunk_(buffer) { /** @type {?Object} */ let chunk = this.findChunk('_PMX'); if (chunk) { this.head = chunk.chunkData.start; this._PMX.chunkId = chunk.chunkId; this._PMX.chunkSize = chunk.chunkSize; this._PMX.value = unpackString( buffer, this.head, this.head + this._PMX.chunkSize); } } /** * Read bytes as a ZSTR string. * @param {!Uint8Array} bytes The bytes. * @param {number=} [index=0] 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; } }