UNPKG

wavefile-creator

Version:
479 lines (452 loc) 16.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 WaveFileCreator class. * @see https://github.com/rochars/wavefile */ import { WaveFileParser } from 'wavefile-parser'; import { packArrayTo } from 'byte-data'; /** @module wavefile-creator */ /** * A class to read, write and create wav files. * @extends WaveFileParser */ export class WaveFileCreator extends WaveFileParser { /** * @param {?Uint8Array=} wavBuffer A wave file 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. */ constructor(wavBuffer=null, samples=true) { // Dont load the file yet super(); /** * The bit depth code according to the samples. * @type {string} */ this.bitDepth = '0'; /** * @type {{ be: boolean, bits: number, fp: boolean, signed: boolean }} * @protected */ this.dataType = { be: false, bits: 0, fp: false, signed: 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 }; // Now load the file if (wavBuffer) { this.fromBuffer(wavBuffer, samples); } } /** * Set up the WaveFileCreator 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) { super.fromBuffer(wavBuffer, samples); this.bitDepthFromFmt_(); this.updateDataType_(); } /** * Return a byte buffer representig the WaveFileCreator 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(); } /** * Set up the WaveFileCreator object based on the arguments passed. * Existing chunks are reset. * @param {number} numChannels The number of channels * (Integer numbers: 1 for mono, 2 stereo and so on). * @param {number} sampleRate The sample rate. * Integer numbers 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<number>|!Array<!Array<number>>|!TypedArray} samples * The samples. Must be in the correct range according to the bit depth. * @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={}) { // reset all chunks this.clearHeaders(); this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options); } /** * Set up the WaveFileCreator object based on the arguments passed. * @param {number} numChannels The number of channels * (Integer numbers: 1 for mono, 2 stereo and so on). * @param {number} sampleRate The sample rate. * Integer numbers 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<number>|!Array<!Array<number>>|!TypedArray} samples * The samples. Must be in the correct range according to the bit depth. * @param {?Object} options Optional. Used to force the container * as RIFX with {'container': 'RIFX'} * @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); 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; } } /** * 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.'); } } } /** * Interleave de-interleaved samples. * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples * The samples. * @return {!Array<number>|!Array<!Array<number>>|!TypedArray} * @private */ function interleave(samples) { if (samples.length > 0) { if (samples[0].constructor === Array) { /** @type {!Array<number>} */ let finalSamples = []; for (let i = 0, len = samples[0].length; i < len; i++) { for (let j = 0, subLen = samples.length; j < subLen; j++) { finalSamples.push(samples[j][i]); } } samples = finalSamples; } } return samples; } /** * 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; } /** * Validate the number of channels in a wav file according to the * bit depth of the audio. * @param {number} channels The number of channels in the file. * @param {number} bits The number of bits per sample. * @return {boolean} True is the number of channels is valid. * @private */ function validateNumChannels(channels, bits) { /** @type {number} */ let blockAlign = channels * bits / 8; if (channels < 1 || blockAlign > 65535) { return false; } return true; } /** * Validate the sample rate value of a wav file according to the number of * channels and the bit depth of the audio. * @param {number} channels The number of channels in the file. * @param {number} bits The number of bits per sample. * @param {number} sampleRate The sample rate to be validated. * @return {boolean} True is the sample rate is valid, false otherwise. * @private */ function validateSampleRate(channels, bits, sampleRate) { /** @type {number} */ let byteRate = channels * (bits / 8) * sampleRate; if (sampleRate < 1 || byteRate > 4294967295) { return false; } return true; }