UNPKG

wavefile

Version:

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

514 lines (490 loc) 16.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 WaveFileParser class. * @see https://github.com/rochars/wavefile */ import { WaveFileReader } from './wavefile-reader'; import { writeString } from './parsers/write-string'; import { packTo, packStringTo, packString, pack } from './parsers/binary'; /** * A class to read and write wav files. * @extends WaveFileReader */ export class WaveFileParser extends WaveFileReader { /** * Return a byte buffer representig the WaveFileParser object as a .wav file. * The return value of this method can be written straight to disk. * @return {!Uint8Array} A wav file. */ toBuffer() { this.uInt16.be = this.container === 'RIFX'; this.uInt32.be = this.uInt16.be; /** @type {!Array<!Array<number>>} */ let fileBody = [ this.getJunkBytes_(), this.getDs64Bytes_(), this.getBextBytes_(), this.getiXMLBytes_(), this.getFmtBytes_(), this.getFactBytes_(), packString(this.data.chunkId), pack(this.data.samples.length, this.uInt32), this.data.samples, this.getCueBytes_(), this.getSmplBytes_(), this.getLISTBytes_(), this.get_PMXBytes_() ]; /** @type {number} */ let fileBodyLength = 0; for (let i=0; i<fileBody.length; i++) { fileBodyLength += fileBody[i].length; } /** @type {!Uint8Array} */ let file = new Uint8Array(fileBodyLength + 12); /** @type {number} */ let index = 0; index = packStringTo(this.container, file, index); index = packTo(fileBodyLength + 4, this.uInt32, file, index); index = packStringTo(this.format, file, index); for (let i=0; i<fileBody.length; i++) { file.set(fileBody[i], index); index += fileBody[i].length; } return file; } /** * Return the bytes of the 'bext' chunk. * @private */ getBextBytes_() { /** @type {!Array<number>} */ let bytes = []; this.enforceBext_(); if (this.bext.chunkId) { this.bext.chunkSize = 602 + this.bext.codingHistory.length; bytes = bytes.concat( packString(this.bext.chunkId), pack(602 + this.bext.codingHistory.length, this.uInt32), writeString(this.bext.description, 256), writeString(this.bext.originator, 32), writeString(this.bext.originatorReference, 32), writeString(this.bext.originationDate, 10), writeString(this.bext.originationTime, 8), pack(this.bext.timeReference[0], this.uInt32), pack(this.bext.timeReference[1], this.uInt32), pack(this.bext.version, this.uInt16), writeString(this.bext.UMID, 64), pack(this.bext.loudnessValue, this.uInt16), pack(this.bext.loudnessRange, this.uInt16), pack(this.bext.maxTruePeakLevel, this.uInt16), pack(this.bext.maxMomentaryLoudness, this.uInt16), pack(this.bext.maxShortTermLoudness, this.uInt16), writeString(this.bext.reserved, 180), writeString( this.bext.codingHistory, this.bext.codingHistory.length)); } this.enforceByteLen_(bytes); return bytes; } /** * Make sure a 'bext' chunk is created if BWF data was created in a file. * @private */ enforceBext_() { for (let prop in this.bext) { if (this.bext.hasOwnProperty(prop)) { if (this.bext[prop] && prop != 'timeReference') { this.bext.chunkId = 'bext'; break; } } } if (this.bext.timeReference[0] || this.bext.timeReference[1]) { this.bext.chunkId = 'bext'; } } /** * Return the bytes of the 'iXML' chunk. * @return {!Array<number>} The 'iXML' chunk bytes. * @private */ getiXMLBytes_() { /** @type {!Array<number>} */ let bytes = []; if (this.iXML.chunkId) { /** @type {!Array<number>} */ let iXMLPackedValue = packString(this.iXML.value); this.iXML.chunkSize = iXMLPackedValue.length; bytes = bytes.concat( packString(this.iXML.chunkId), pack(this.iXML.chunkSize, this.uInt32), iXMLPackedValue); } this.enforceByteLen_(bytes); return bytes; } /** * Return the bytes of the 'ds64' chunk. * @return {!Array<number>} The 'ds64' chunk bytes. * @private */ getDs64Bytes_() { /** @type {!Array<number>} */ let bytes = []; if (this.ds64.chunkId) { bytes = bytes.concat( packString(this.ds64.chunkId), pack(this.ds64.chunkSize, this.uInt32), pack(this.ds64.riffSizeHigh, this.uInt32), pack(this.ds64.riffSizeLow, this.uInt32), pack(this.ds64.dataSizeHigh, this.uInt32), pack(this.ds64.dataSizeLow, this.uInt32), pack(this.ds64.originationTime, this.uInt32), pack(this.ds64.sampleCountHigh, this.uInt32), pack(this.ds64.sampleCountLow, this.uInt32)); } //if (this.ds64.tableLength) { // ds64Bytes = ds64Bytes.concat( // pack(this.ds64.tableLength, this.uInt32), // this.ds64.table); //} this.enforceByteLen_(bytes); return bytes; } /** * Return the bytes of the 'cue ' chunk. * @return {!Array<number>} The 'cue ' chunk bytes. * @private */ getCueBytes_() { /** @type {!Array<number>} */ let bytes = []; if (this.cue.chunkId) { /** @type {!Array<number>} */ let cuePointsBytes = this.getCuePointsBytes_(); bytes = bytes.concat( packString(this.cue.chunkId), pack(cuePointsBytes.length + 4, this.uInt32), // chunkSize pack(this.cue.dwCuePoints, this.uInt32), cuePointsBytes); } this.enforceByteLen_(bytes); return bytes; } /** * Return the bytes of the 'cue ' points. * @return {!Array<number>} The 'cue ' points as an array of bytes. * @private */ getCuePointsBytes_() { /** @type {!Array<number>} */ let points = []; for (let i=0; i<this.cue.dwCuePoints; i++) { points = points.concat( pack(this.cue.points[i].dwName, this.uInt32), pack(this.cue.points[i].dwPosition, this.uInt32), packString(this.cue.points[i].fccChunk), pack(this.cue.points[i].dwChunkStart, this.uInt32), pack(this.cue.points[i].dwBlockStart, this.uInt32), pack(this.cue.points[i].dwSampleOffset, this.uInt32)); } return points; } /** * Return the bytes of the 'smpl' chunk. * @return {!Array<number>} The 'smpl' chunk bytes. * @private */ getSmplBytes_() { /** @type {!Array<number>} */ let bytes = []; if (this.smpl.chunkId) { /** @type {!Array<number>} */ let smplLoopsBytes = this.getSmplLoopsBytes_(); bytes = bytes.concat( packString(this.smpl.chunkId), pack(smplLoopsBytes.length + 36, this.uInt32), //chunkSize pack(this.smpl.dwManufacturer, this.uInt32), pack(this.smpl.dwProduct, this.uInt32), pack(this.smpl.dwSamplePeriod, this.uInt32), pack(this.smpl.dwMIDIUnityNote, this.uInt32), pack(this.smpl.dwMIDIPitchFraction, this.uInt32), pack(this.smpl.dwSMPTEFormat, this.uInt32), pack(this.smpl.dwSMPTEOffset, this.uInt32), pack(this.smpl.dwNumSampleLoops, this.uInt32), pack(this.smpl.dwSamplerData, this.uInt32), smplLoopsBytes); } this.enforceByteLen_(bytes); return bytes; } /** * Return the bytes of the 'smpl' loops. * @return {!Array<number>} The 'smpl' loops as an array of bytes. * @private */ getSmplLoopsBytes_() { /** @type {!Array<number>} */ let loops = []; for (let i=0; i<this.smpl.dwNumSampleLoops; i++) { loops = loops.concat( pack(this.smpl.loops[i].dwName, this.uInt32), pack(this.smpl.loops[i].dwType, this.uInt32), pack(this.smpl.loops[i].dwStart, this.uInt32), pack(this.smpl.loops[i].dwEnd, this.uInt32), pack(this.smpl.loops[i].dwFraction, this.uInt32), pack(this.smpl.loops[i].dwPlayCount, this.uInt32)); } return loops; } /** * Return the bytes of the 'fact' chunk. * @return {!Array<number>} The 'fact' chunk bytes. * @private */ getFactBytes_() { /** @type {!Array<number>} */ let bytes = []; if (this.fact.chunkId) { bytes = bytes.concat( packString(this.fact.chunkId), pack(this.fact.chunkSize, this.uInt32), pack(this.fact.dwSampleLength, this.uInt32)); } this.enforceByteLen_(bytes); return bytes; } /** * Return the bytes of the 'fmt ' chunk. * @return {!Array<number>} The 'fmt' chunk bytes. * @throws {Error} if no 'fmt ' chunk is present. * @private */ getFmtBytes_() { /** @type {!Array<number>} */ let fmtBytes = []; if (this.fmt.chunkId) { /** @type {!Array<number>} */ let bytes = fmtBytes.concat( packString(this.fmt.chunkId), pack(this.fmt.chunkSize, this.uInt32), pack(this.fmt.audioFormat, this.uInt16), pack(this.fmt.numChannels, this.uInt16), pack(this.fmt.sampleRate, this.uInt32), pack(this.fmt.byteRate, this.uInt32), pack(this.fmt.blockAlign, this.uInt16), pack(this.fmt.bitsPerSample, this.uInt16), this.getFmtExtensionBytes_()); this.enforceByteLen_(bytes); return bytes; } throw Error('Could not find the "fmt " chunk'); } /** * Return the bytes of the fmt extension fields. * @return {!Array<number>} The fmt extension bytes. * @private */ getFmtExtensionBytes_() { /** @type {!Array<number>} */ let extension = []; if (this.fmt.chunkSize > 16) { extension = extension.concat( pack(this.fmt.cbSize, this.uInt16)); } if (this.fmt.chunkSize > 18) { extension = extension.concat( pack(this.fmt.validBitsPerSample, this.uInt16)); } if (this.fmt.chunkSize > 20) { extension = extension.concat( pack(this.fmt.dwChannelMask, this.uInt32)); } if (this.fmt.chunkSize > 24) { extension = extension.concat( pack(this.fmt.subformat[0], this.uInt32), pack(this.fmt.subformat[1], this.uInt32), pack(this.fmt.subformat[2], this.uInt32), pack(this.fmt.subformat[3], this.uInt32)); } return extension; } /** * Return the bytes of the 'LIST' chunk. * @return {!Array<number>} The 'LIST' chunk bytes. * @private */ getLISTBytes_() { /** @type {!Array<number>} */ let bytes = []; for (let i=0; i<this.LIST.length; i++) { /** @type {!Array<number>} */ let subChunksBytes = this.getLISTSubChunksBytes_( this.LIST[i].subChunks, this.LIST[i].format); bytes = bytes.concat( packString(this.LIST[i].chunkId), pack(subChunksBytes.length + 4, this.uInt32), //chunkSize packString(this.LIST[i].format), subChunksBytes); } this.enforceByteLen_(bytes); return bytes; } /** * Return the bytes of the sub chunks of a 'LIST' chunk. * @param {!Array<!Object>} subChunks The 'LIST' sub chunks. * @param {string} format The format of the 'LIST' chunk. * Currently supported values are 'adtl' or 'INFO'. * @return {!Array<number>} The sub chunk bytes. * @private */ getLISTSubChunksBytes_(subChunks, format) { /** @type {!Array<number>} */ let bytes = []; for (let i = 0, len = subChunks.length; i < len; i++) { if (format == 'INFO') { bytes = bytes.concat(this.getLISTINFOSubChunksBytes_(subChunks[i])); } else if (format == 'adtl') { bytes = bytes.concat(this.getLISTadtlSubChunksBytes_(subChunks[i])); } this.enforceByteLen_(bytes); } return bytes; } /** * Return the bytes of the sub chunks of a 'LIST' chunk of type 'INFO'. * @param {!Object} subChunk The 'LIST' sub chunk. * @return {!Array<number>} * @private */ getLISTINFOSubChunksBytes_(subChunk) { /** @type {!Array<number>} */ let bytes = []; /** @type {!Array<number>} */ let LISTsubChunkValue = writeString( subChunk.value, subChunk.value.length); bytes = bytes.concat( packString(subChunk.chunkId), pack(LISTsubChunkValue.length + 1, this.uInt32), //chunkSize LISTsubChunkValue); bytes.push(0); return bytes; } /** * Return the bytes of the sub chunks of a 'LIST' chunk of type 'INFO'. * @param {!Object} subChunk The 'LIST' sub chunk. * @return {!Array<number>} * @private */ getLISTadtlSubChunksBytes_(subChunk) { /** @type {!Array<number>} */ let bytes = []; if (['labl', 'note'].indexOf(subChunk.chunkId) > -1) { /** @type {!Array<number>} */ let LISTsubChunkValue = writeString( subChunk.value, subChunk.value.length); bytes = bytes.concat( packString(subChunk.chunkId), pack(LISTsubChunkValue.length + 4 + 1, this.uInt32), //chunkSize pack(subChunk.dwName, this.uInt32), LISTsubChunkValue); bytes.push(0); } else if (subChunk.chunkId == 'ltxt') { bytes = bytes.concat( this.getLtxtChunkBytes_(subChunk)); } return bytes; } /** * Return the bytes of a 'ltxt' chunk. * @param {!Object} ltxt the 'ltxt' chunk. * @return {!Array<number>} * @private */ getLtxtChunkBytes_(ltxt) { return [].concat( packString(ltxt.chunkId), pack(ltxt.value.length + 20, this.uInt32), pack(ltxt.dwName, this.uInt32), pack(ltxt.dwSampleLength, this.uInt32), pack(ltxt.dwPurposeID, this.uInt32), pack(ltxt.dwCountry, this.uInt16), pack(ltxt.dwLanguage, this.uInt16), pack(ltxt.dwDialect, this.uInt16), pack(ltxt.dwCodePage, this.uInt16), // should always be a empty string; // kept for compatibility writeString(ltxt.value, ltxt.value.length)); } /** * Return the bytes of the '_PMX' chunk. * @return {!Array<number>} The '_PMX' chunk bytes. * @private */ get_PMXBytes_() { /** @type {!Array<number>} */ let bytes = []; if (this._PMX.chunkId) { /** @type {!Array<number>} */ let _PMXPackedValue = packString(this._PMX.value); this._PMX.chunkSize = _PMXPackedValue.length; bytes = bytes.concat( packString(this._PMX.chunkId), pack(this._PMX.chunkSize, this.uInt32), _PMXPackedValue); } this.enforceByteLen_(bytes); return bytes; } /** * Return the bytes of the 'junk' chunk. * @private */ getJunkBytes_() { /** @type {!Array<number>} */ let bytes = []; if (this.junk.chunkId) { return bytes.concat( packString(this.junk.chunkId), pack(this.junk.chunkData.length, this.uInt32), //chunkSize this.junk.chunkData); } this.enforceByteLen_(bytes); return bytes; } /** * Push a null byte into a byte array if * the byte count is odd. * @param {!Array<number>} bytes The byte array. * @private */ enforceByteLen_(bytes) { if (bytes.length % 2) { bytes.push(0); } } }