UNPKG

prx-wavefile

Version:

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

629 lines (603 loc) 19.5 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.getMextBytes_(), this.getCartBytes_(), 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; } /** * Return the bytes of the 'mext' chunk. * @private */ getMextBytes_() { /** @type {!Array<number>} */ let bytes = []; if (this.mext.chunkId) { this.mext.chunkSize = 12; bytes = bytes.concat( packString(this.mext.chunkId), pack(this.mext.chunkSize, this.uInt32), pack(this.mext.soundInformation, this.uInt16), pack(this.mext.frameSize, this.uInt16), pack(this.mext.ancillaryDataLength, this.uInt16), pack(this.mext.ancillaryDataDef, this.uInt16), writeString(this.mext.reserved, 4) ); } 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 'cart' chunk. * @private */ getCartBytes_() { /** @type {!Array<number>} */ let bytes = []; let postTimerBytes = this.getPostTimerBytes_(); if (this.cart.chunkId) { this.cart.chunkSize = 2048 + this.cart.tagText.length; bytes = bytes.concat( packString(this.cart.chunkId), pack(this.cart.chunkSize, this.uInt32), writeString(this.cart.version, 4), writeString(this.cart.title, 64), writeString(this.cart.artist, 64), writeString(this.cart.cutId, 64), writeString(this.cart.clientId, 64), writeString(this.cart.category, 64), writeString(this.cart.classification, 64), writeString(this.cart.outCue, 64), writeString(this.cart.startDate, 10), writeString(this.cart.startTime, 8), writeString(this.cart.endDate, 10), writeString(this.cart.endTime, 8), writeString(this.cart.producerAppId, 64), writeString(this.cart.producerAppVersion, 64), writeString(this.cart.userDef, 64), pack(this.cart.levelReference, this.uInt32), postTimerBytes, writeString(this.cart.reserved, 276), writeString(this.cart.url, 1024), writeString(this.cart.tagText, this.cart.tagText.length) ); } this.enforceByteLen_(bytes); return bytes; } /** * Return the bytes of the 'cart' postTimers * @return {!Array<number>} The 'cart' postTimers as an array of bytes. * @private */ getPostTimerBytes_() { /** @type {!Array<number>} */ let postTimers = []; for (let i = 0; i < 8; i++) { let usage = ""; let value = 4294967295; if (i < this.cart.postTimer.length) { usage = this.cart.postTimer[i].usage; value = this.cart.postTimer[i].value; } postTimers = postTimers.concat( writeString(usage, 4), pack(value, this.uInt32) ); } return postTimers; } /** * 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.audioFormat == 80 && this.fmt.chunkSize == 40) { extension = extension.concat( pack(this.fmt.headLayer, this.uInt16), pack(this.fmt.headBitRate, this.uInt32), pack(this.fmt.headMode, this.uInt16), pack(this.fmt.headModeExt, this.uInt16), pack(this.fmt.headEmphasis, this.uInt16), pack(this.fmt.headFlags, this.uInt16), pack(this.fmt.ptsLow, this.uInt32), pack(this.fmt.ptsHigh, this.uInt32) ); } else { 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 (this.padBytes) { if (bytes.length % 2) { bytes.push(0); } } } }