UNPKG

prx-wavefile

Version:

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

357 lines (337 loc) 11.2 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 WaveFileConverter class. * @see https://github.com/rochars/wavefile */ import { changeBitDepth } from './codecs/bitdepth'; import * as imaadpcm from './codecs/imaadpcm'; import * as alaw from './codecs/alaw'; import * as mulaw from './codecs/mulaw'; import { unpackArrayTo } from './parsers/binary'; import { WaveFileCueEditor } from './wavefile-cue-editor'; import { validateSampleRate } from './validators/validate-sample-rate'; import { resample } from './resampler'; /** * A class to convert wav files to other types of wav files. * @extends WaveFileCueEditor * @ignore */ export class WaveFileConverter extends WaveFileCueEditor { /** * Force a file as RIFF. */ toRIFF() { /** @type {!Float64Array} */ let output = new Float64Array( outputSize_(this.data.samples.length, this.dataType.bits / 8)); unpackArrayTo(this.data.samples, this.dataType, output, 0, this.data.samples.length); this.fromExisting_( this.fmt.numChannels, this.fmt.sampleRate, this.bitDepth, output, {container: 'RIFF'}); } /** * Force a file as RIFX. */ toRIFX() { /** @type {!Float64Array} */ let output = new Float64Array( outputSize_(this.data.samples.length, this.dataType.bits / 8)); unpackArrayTo(this.data.samples, this.dataType, output, 0, this.data.samples.length); this.fromExisting_( this.fmt.numChannels, this.fmt.sampleRate, this.bitDepth, output, {container: 'RIFX'}); } /** * Encode a 16-bit wave file as 4-bit IMA ADPCM. * @throws {Error} If sample rate is not 8000. * @throws {Error} If number of channels is not 1. */ toIMAADPCM() { if (this.fmt.sampleRate !== 8000) { throw new Error( 'Only 8000 Hz files can be compressed as IMA-ADPCM.'); } else if (this.fmt.numChannels !== 1) { throw new Error( 'Only mono files can be compressed as IMA-ADPCM.'); } else { this.assure16Bit_(); /** @type {!Int16Array} */ let output = new Int16Array( outputSize_(this.data.samples.length, 2)); unpackArrayTo(this.data.samples, this.dataType, output, 0, this.data.samples.length); this.fromExisting_( this.fmt.numChannels, this.fmt.sampleRate, '4', imaadpcm.encode(output), {container: this.correctContainer_()}); } } /** * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file. * @param {string=} [bitDepthCode='16'] The new bit depth of the samples. * One of '8' ... '32' (integers), '32f' or '64' (floats). */ fromIMAADPCM(bitDepthCode='16') { this.fromExisting_( this.fmt.numChannels, this.fmt.sampleRate, '16', imaadpcm.decode(this.data.samples, this.fmt.blockAlign), {container: this.correctContainer_()}); if (bitDepthCode != '16') { this.toBitDepth(bitDepthCode); } } /** * Encode a 16-bit wave file as 8-bit A-Law. */ toALaw() { this.assure16Bit_(); /** @type {!Int16Array} */ let output = new Int16Array( outputSize_(this.data.samples.length, 2)); unpackArrayTo(this.data.samples, this.dataType, output, 0, this.data.samples.length); this.fromExisting_( this.fmt.numChannels, this.fmt.sampleRate, '8a', alaw.encode(output), {container: this.correctContainer_()}); } /** * Decode a 8-bit A-Law wave file into a 16-bit wave file. * @param {string=} [bitDepthCode='16'] The new bit depth of the samples. * One of '8' ... '32' (integers), '32f' or '64' (floats). */ fromALaw(bitDepthCode='16') { this.fromExisting_( this.fmt.numChannels, this.fmt.sampleRate, '16', alaw.decode(this.data.samples), {container: this.correctContainer_()}); if (bitDepthCode != '16') { this.toBitDepth(bitDepthCode); } } /** * Encode 16-bit wave file as 8-bit mu-Law. */ toMuLaw() { this.assure16Bit_(); /** @type {!Int16Array} */ let output = new Int16Array( outputSize_(this.data.samples.length, 2)); unpackArrayTo(this.data.samples, this.dataType, output, 0, this.data.samples.length); this.fromExisting_( this.fmt.numChannels, this.fmt.sampleRate, '8m', mulaw.encode(output), {container: this.correctContainer_()}); } /** * Decode a 8-bit mu-Law wave file into a 16-bit wave file. * @param {string=} [bitDepthCode='16'] The new bit depth of the samples. * One of '8' ... '32' (integers), '32f' or '64' (floats). */ fromMuLaw(bitDepthCode='16') { this.fromExisting_( this.fmt.numChannels, this.fmt.sampleRate, '16', mulaw.decode(this.data.samples), {container: this.correctContainer_()}); if (bitDepthCode != '16') { this.toBitDepth(bitDepthCode); } } /** * Change the bit depth of the samples. * @param {string} newBitDepth The new bit depth of the samples. * One of '8' ... '32' (integers), '32f' or '64' (floats) * @param {boolean=} [changeResolution=true] A boolean indicating if the * resolution of samples should be actually changed or not. * @throws {Error} If the bit depth is not valid. */ toBitDepth(newBitDepth, changeResolution=true) { /** @type {string} */ let toBitDepth = newBitDepth; /** @type {string} */ let thisBitDepth = this.bitDepth; if (!changeResolution) { if (newBitDepth != '32f') { toBitDepth = this.dataType.bits.toString(); } thisBitDepth = '' + this.dataType.bits; } // If the file is compressed, make it // PCM before changing the bit depth this.assureUncompressed_(); /** * The original samples, interleaved. * @type {!(Array|TypedArray)} */ let samples = this.getSamples(true); /** * The container for the new samples. * @type {!Float64Array} */ let newSamples = new Float64Array(samples.length); // Change the bit depth changeBitDepth(samples, thisBitDepth, newSamples, toBitDepth); // Re-create the file this.fromExisting_( this.fmt.numChannels, this.fmt.sampleRate, newBitDepth, newSamples, {container: this.correctContainer_()}); } /** * Convert the sample rate of the file. * @param {number} sampleRate The target sample rate. * @param {Object=} options The extra configuration, if needed. */ toSampleRate(sampleRate, options) { this.validateResample_(sampleRate); /** @type {!(Array|TypedArray)} */ let samples = this.getSamples(); /** @type {!(Array|Float64Array)} */ let newSamples = []; // Mono files if (samples.constructor === Float64Array) { newSamples = resample(samples, this.fmt.sampleRate, sampleRate, options); // Multi-channel files } else { for (let i = 0; i < samples.length; i++) { newSamples.push(resample( samples[i], this.fmt.sampleRate, sampleRate, options)); } } // Recreate the file this.fromExisting_( this.fmt.numChannels, sampleRate, this.bitDepth, newSamples, {'container': this.correctContainer_()}); } /** * Validate the conditions for resampling. * @param {number} sampleRate The target sample rate. * @throws {Error} If the file cant be resampled. * @private */ validateResample_(sampleRate) { if (!validateSampleRate( this.fmt.numChannels, this.fmt.bitsPerSample, sampleRate)) { throw new Error('Invalid sample rate.'); } else if (['4','8a','8m'].indexOf(this.bitDepth) > -1) { throw new Error( 'wavefile can\'t change the sample rate of compressed files.'); } } /** * Make the file 16-bit if it is not. * @private */ assure16Bit_() { this.assureUncompressed_(); if (this.bitDepth != '16') { this.toBitDepth('16'); } } /** * Uncompress the samples in case of a compressed file. * @private */ assureUncompressed_() { if (this.bitDepth == '8a') { this.fromALaw(); } else if (this.bitDepth == '8m') { this.fromMuLaw(); } else if (this.bitDepth == '4') { this.fromIMAADPCM(); } } /** * Return 'RIFF' if the container is 'RF64', the current container name * otherwise. Used to enforce 'RIFF' when RF64 is not allowed. * @return {string} * @private */ correctContainer_() { return this.container == 'RF64' ? 'RIFF' : this.container; } /** * Set up the WaveFileCreator object based on the arguments passed. * This method only reset the fmt , fact, ds64 and data chunks. * @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|TypedArray)} samples * The samples. Must be in the correct range according to the bit depth. * @param {Object} options Used to define the container. Uses RIFF by default. * @throws {Error} If any argument does not meet the criteria. * @private */ fromExisting_(numChannels, sampleRate, bitDepthCode, samples, options) { /** @type {!Object} */ let tmpWav = new WaveFileCueEditor(); Object.assign(this.fmt, tmpWav.fmt); Object.assign(this.fact, tmpWav.fact); Object.assign(this.ds64, tmpWav.ds64); Object.assign(this.data, tmpWav.data); this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options); } } /** * Return the size in bytes of the output sample array when applying * compression to 16-bit samples. * @return {number} * @private */ function outputSize_(byteLen, byteOffset) { /** @type {number} */ let outputSize = byteLen / byteOffset; if (outputSize % 2) { outputSize++; } return outputSize; }