/*
* 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;
}