/*
* 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
*/
/** @module wavefile-parser */
import { WaveFileReader } from 'wavefile-reader';
import { packTo, packStringTo, packString, pack } from 'byte-data';
/**
* 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.getFmtBytes_(),
this.getFactBytes_(),
packString(this.data.chunkId),
pack(this.data.samples.length, this.uInt32),
this.data.samples,
this.getCueBytes_(),
this.getSmplBytes_(),
this.getLISTBytes_()
];
/** @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));
}
if (bytes.length % 2) {
bytes.push(0);
}
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 '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);
//}
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),
pack(this.cue.dwCuePoints, this.uInt32),
cuePointsBytes);
}
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),
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);
}
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));
}
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) {
return 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_());
}
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.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),
packString(this.LIST[i].format),
subChunksBytes);
}
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; i<subChunks.length; i++) {
if (format == 'INFO') {
bytes = bytes.concat(
packString(subChunks[i].chunkId),
pack(subChunks[i].value.length + 1, this.uInt32),
writeString(
subChunks[i].value, subChunks[i].value.length));
bytes.push(0);
} else if (format == 'adtl') {
if (['labl', 'note'].indexOf(subChunks[i].chunkId) > -1) {
bytes = bytes.concat(
packString(subChunks[i].chunkId),
pack(
subChunks[i].value.length + 4 + 1, this.uInt32),
pack(subChunks[i].dwName, this.uInt32),
writeString(
subChunks[i].value,
subChunks[i].value.length));
bytes.push(0);
} else if (subChunks[i].chunkId == 'ltxt') {
bytes = bytes.concat(
this.getLtxtChunkBytes_(subChunks[i]));
}
}
if (bytes.length % 2) {
bytes.push(0);
}
}
return bytes;
}
/**
* Return the bytes of a 'ltxt' chunk.
* @param {!Object} ltxt the 'ltxt' chunk.
* @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),
writeString(ltxt.value, ltxt.value.length));
}
/**
* 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),
this.junk.chunkData);
}
return bytes;
}
}
/**
* Pack a string an array of bytes. If the string is smaller than the max size
* and the string is of fixed size, the output array is filled with 0s.
* @param {string} str The string to be written as bytes.
* @param {number} maxSize the max size of the string.
* @param {boolean} fixedSize If the string is of fixed size or not.
* @return {!Array<number>} The packed string.
* @private
*/
function writeString(str, maxSize, fixedSize=true) {
/** @type {!Array<number>} */
let packedString = packString(str);
if (fixedSize) {
for (let i=packedString.length; i<maxSize; i++) {
packedString.push(0);
}
}
return packedString;
}