UNPKG

wavefile

Version:

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

491 lines (462 loc) 16.9 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 WaveFileCreator class. * @see https://github.com/rochars/wavefile */ import { WaveFileParser } from './wavefile-parser'; import { interleave, deInterleave } from './parsers/interleave'; import { validateNumChannels } from './validators/validate-num-channels'; import { validateSampleRate } from './validators/validate-sample-rate'; import { packArrayTo, unpackArrayTo, packTo, unpack } from './parsers/binary'; /** * A class to read, write and create wav files. * @extends WaveFileParser * @ignore */ export class WaveFileCreator extends WaveFileParser { constructor() { super(); /** * The bit depth code according to the samples. * @type {string} */ this.bitDepth = '0'; /** * @type {!{bits: number, be: boolean}} * @protected */ this.dataType = {bits: 0, be: false}; /** * Audio formats. * Formats not listed here should be set to 65534, * the code for WAVE_FORMAT_EXTENSIBLE * @enum {number} * @protected */ this.WAV_AUDIO_FORMATS = { '4': 17, '8': 1, '8a': 6, '8m': 7, '16': 1, '24': 1, '32': 1, '32f': 3, '64': 3 }; } /** * Set up the WaveFileCreator object based on the arguments passed. * Existing chunks are reset. * @param {number} numChannels The number of channels. * @param {number} sampleRate The sample rate. * Integers like 8000, 44100, 48000, 96000, 192000. * @param {string} bitDepthCode The audio bit depth code. * One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64' * or any value between '8' and '32' (like '12'). * @param {!(Array|TypedArray)} samples The samples. * @param {Object=} options Optional. Used to force the container * as RIFX with {'container': 'RIFX'} * @throws {Error} If any argument does not meet the criteria. */ fromScratch(numChannels, sampleRate, bitDepthCode, samples, options) { options = options || {}; // reset all chunks this.clearHeaders(); this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options); } /** * Set up the WaveFileParser 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) { super.fromBuffer(wavBuffer, samples); this.bitDepthFromFmt_(); this.updateDataType_(); } /** * 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. * @throws {Error} If bit depth is invalid. * @throws {Error} If the number of channels is invalid. * @throws {Error} If the sample rate is invalid. */ toBuffer() { this.validateWavHeader_(); return super.toBuffer(); } /** * Return the samples packed in a Float64Array. * @param {boolean=} [interleaved=false] True to return interleaved samples, * false to return the samples de-interleaved. * @param {Function=} [OutputObject=Float64Array] The sample container. * @return {!(Array|TypedArray)} the samples. */ getSamples(interleaved=false, OutputObject=Float64Array) { /** * A Float64Array created with a size to match the * the length of the samples. * @type {!(Array|TypedArray)} */ let samples = new OutputObject( this.data.samples.length / (this.dataType.bits / 8)); // Unpack all the samples unpackArrayTo(this.data.samples, this.dataType, samples, 0, this.data.samples.length); if (!interleaved && this.fmt.numChannels > 1) { return deInterleave(samples, this.fmt.numChannels, OutputObject); } return samples; } /** * Return the sample at a given index. * @param {number} index The sample index. * @return {number} The sample. * @throws {Error} If the sample index is off range. */ getSample(index) { index = index * (this.dataType.bits / 8); if (index + this.dataType.bits / 8 > this.data.samples.length) { throw new Error('Range error'); } return unpack( this.data.samples.slice(index, index + this.dataType.bits / 8), this.dataType); } /** * Set the sample at a given index. * @param {number} index The sample index. * @param {number} sample The sample. * @throws {Error} If the sample index is off range. */ setSample(index, sample) { index = index * (this.dataType.bits / 8); if (index + this.dataType.bits / 8 > this.data.samples.length) { throw new Error('Range error'); } packTo(sample, this.dataType, this.data.samples, index, true); } /** * Return the value of the iXML chunk. * @return {string} The contents of the iXML chunk. */ getiXML() { return this.iXML.value; } /** * Set the value of the iXML chunk. * @param {string} iXMLValue The value for the iXML chunk. * @throws {TypeError} If the value is not a string. */ setiXML(iXMLValue) { if (typeof iXMLValue !== 'string') { throw new TypeError('iXML value must be a string.'); } this.iXML.value = iXMLValue; this.iXML.chunkId = 'iXML'; } /** * Get the value of the _PMX chunk. * @return {string} The contents of the _PMX chunk. */ get_PMX() { return this._PMX.value; } /** * Set the value of the _PMX chunk. * @param {string} _PMXValue The value for the _PMX chunk. * @throws {TypeError} If the value is not a string. */ set_PMX(_PMXValue) { if (typeof _PMXValue !== 'string') { throw new TypeError('_PMX value must be a string.'); } this._PMX.value = _PMXValue; this._PMX.chunkId = '_PMX'; } /** * Set up the WaveFileCreator object based on the arguments passed. * @param {number} numChannels The number of channels. * @param {number} sampleRate The sample rate. * Integers like 8000, 44100, 48000, 96000, 192000. * @param {string} bitDepthCode The audio bit depth code. * One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64' * or any value between '8' and '32' (like '12'). * @param {!(Array|TypedArray)} samples The samples. * @param {Object} options Used to define the container. * @throws {Error} If any argument does not meet the criteria. * @private */ newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options) { if (!options.container) { options.container = 'RIFF'; } this.container = options.container; this.bitDepth = bitDepthCode; samples = interleave(samples); this.updateDataType_(); /** @type {number} */ let numBytes = this.dataType.bits / 8; this.data.samples = new Uint8Array(samples.length * numBytes); packArrayTo(samples, this.dataType, this.data.samples, 0, true); this.makeWavHeader_( bitDepthCode, numChannels, sampleRate, numBytes, this.data.samples.length, options); this.data.chunkId = 'data'; this.data.chunkSize = this.data.samples.length; this.validateWavHeader_(); } /** * Define the header of a wav file. * @param {string} bitDepthCode The audio bit depth * @param {number} numChannels The number of channels * @param {number} sampleRate The sample rate. * @param {number} numBytes The number of bytes each sample use. * @param {number} samplesLength The length of the samples in bytes. * @param {!Object} options The extra options, like container defintion. * @private */ makeWavHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) { if (bitDepthCode == '4') { this.createADPCMHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options); } else if (bitDepthCode == '8a' || bitDepthCode == '8m') { this.createALawMulawHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options); } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 || numChannels > 2) { this.createExtensibleHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options); } else { this.createPCMHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options); } } /** * Create the header of a linear PCM wave file. * @param {string} bitDepthCode The audio bit depth * @param {number} numChannels The number of channels * @param {number} sampleRate The sample rate. * @param {number} numBytes The number of bytes each sample use. * @param {number} samplesLength The length of the samples in bytes. * @param {!Object} options The extra options, like container defintion. * @private */ createPCMHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) { this.container = options.container; this.chunkSize = 36 + samplesLength; this.format = 'WAVE'; this.bitDepth = bitDepthCode; this.fmt = { chunkId: 'fmt ', chunkSize: 16, audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534, numChannels: numChannels, sampleRate: sampleRate, byteRate: (numChannels * numBytes) * sampleRate, blockAlign: numChannels * numBytes, bitsPerSample: parseInt(bitDepthCode, 10), cbSize: 0, validBitsPerSample: 0, dwChannelMask: 0, subformat: [] }; } /** * Create the header of a ADPCM wave file. * @param {string} bitDepthCode The audio bit depth * @param {number} numChannels The number of channels * @param {number} sampleRate The sample rate. * @param {number} numBytes The number of bytes each sample use. * @param {number} samplesLength The length of the samples in bytes. * @param {!Object} options The extra options, like container defintion. * @private */ createADPCMHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) { this.createPCMHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options); this.chunkSize = 40 + samplesLength; this.fmt.chunkSize = 20; this.fmt.byteRate = 4055; this.fmt.blockAlign = 256; this.fmt.bitsPerSample = 4; this.fmt.cbSize = 2; this.fmt.validBitsPerSample = 505; this.fact = { chunkId: 'fact', chunkSize: 4, dwSampleLength: samplesLength * 2 }; } /** * Create the header of WAVE_FORMAT_EXTENSIBLE file. * @param {string} bitDepthCode The audio bit depth * @param {number} numChannels The number of channels * @param {number} sampleRate The sample rate. * @param {number} numBytes The number of bytes each sample use. * @param {number} samplesLength The length of the samples in bytes. * @param {!Object} options The extra options, like container defintion. * @private */ createExtensibleHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) { this.createPCMHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options); this.chunkSize = 36 + 24 + samplesLength; this.fmt.chunkSize = 40; this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1; this.fmt.cbSize = 22; this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10); this.fmt.dwChannelMask = dwChannelMask_(numChannels); // subformat 128-bit GUID as 4 32-bit values // only supports uncompressed integer PCM samples this.fmt.subformat = [1, 1048576, 2852126848, 1905997824]; } /** * Create the header of mu-Law and A-Law wave files. * @param {string} bitDepthCode The audio bit depth * @param {number} numChannels The number of channels * @param {number} sampleRate The sample rate. * @param {number} numBytes The number of bytes each sample use. * @param {number} samplesLength The length of the samples in bytes. * @param {!Object} options The extra options, like container defintion. * @private */ createALawMulawHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) { this.createPCMHeader_( bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options); this.chunkSize = 40 + samplesLength; this.fmt.chunkSize = 20; this.fmt.cbSize = 2; this.fmt.validBitsPerSample = 8; this.fact = { chunkId: 'fact', chunkSize: 4, dwSampleLength: samplesLength }; } /** * Set the string code of the bit depth based on the 'fmt ' chunk. * @private */ bitDepthFromFmt_() { if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) { this.bitDepth = '32f'; } else if (this.fmt.audioFormat === 6) { this.bitDepth = '8a'; } else if (this.fmt.audioFormat === 7) { this.bitDepth = '8m'; } else { this.bitDepth = this.fmt.bitsPerSample.toString(); } } /** * Validate the bit depth. * @return {boolean} True is the bit depth is valid. * @throws {Error} If bit depth is invalid. * @private */ validateBitDepth_() { if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) { if (parseInt(this.bitDepth, 10) > 8 && parseInt(this.bitDepth, 10) < 54) { return true; } throw new Error('Invalid bit depth.'); } return true; } /** * Update the type definition used to read and write the samples. * @private */ updateDataType_() { this.dataType = { bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1, fp: this.bitDepth == '32f' || this.bitDepth == '64', signed: this.bitDepth != '8', be: this.container == 'RIFX' }; if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) { this.dataType.bits = 8; this.dataType.signed = false; } } /** * Validate the header of the file. * @throws {Error} If bit depth is invalid. * @throws {Error} If the number of channels is invalid. * @throws {Error} If the sample rate is invalid. * @ignore * @private */ validateWavHeader_() { this.validateBitDepth_(); if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) { throw new Error('Invalid number of channels.'); } if (!validateSampleRate( this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) { throw new Error('Invalid sample rate.'); } } } /** * Return the value for dwChannelMask according to the number of channels. * @param {number} numChannels the number of channels. * @return {number} the dwChannelMask value. * @private */ function dwChannelMask_(numChannels) { /** @type {number} */ let mask = 0; // mono = FC if (numChannels === 1) { mask = 0x4; // stereo = FL, FR } else if (numChannels === 2) { mask = 0x3; // quad = FL, FR, BL, BR } else if (numChannels === 4) { mask = 0x33; // 5.1 = FL, FR, FC, LF, BL, BR } else if (numChannels === 6) { mask = 0x3F; // 7.1 = FL, FR, FC, LF, BL, BR, SL, SR } else if (numChannels === 8) { mask = 0x63F; } return mask; }