UNPKG

wavefile

Version:

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

438 lines (408 loc) 14.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 WaveFileCueEditor class. * @see https://github.com/rochars/wavefile */ import { WaveFileTagEditor } from './wavefile-tag-editor'; /** * A class to edit meta information in wav files. * @extends WaveFileTagEditor * @ignore */ export class WaveFileCueEditor extends WaveFileTagEditor { /** * Return an array with all cue points in the file, in the order they appear * in the file. * Objects representing cue points/regions look like this: * { * position: 500, // the position in milliseconds * label: 'cue marker 1', * end: 1500, // the end position in milliseconds * dwName: 1, * dwPosition: 0, * fccChunk: 'data', * dwChunkStart: 0, * dwBlockStart: 0, * dwSampleOffset: 22050, // the position as a sample offset * dwSampleLength: 3646827, // length as a sample count, 0 if not a region * dwPurposeID: 544106354, * dwCountry: 0, * dwLanguage: 0, * dwDialect: 0, * dwCodePage: 0, * } * @return {!Array<Object>} */ listCuePoints() { /** @type {!Array<!Object>} */ let points = this.getCuePoints_(); for (let i = 0, len = points.length; i < len; i++) { // Add attrs that should exist in the object points[i].position = (points[i].dwSampleOffset / this.fmt.sampleRate) * 1000; // If it is a region, calc the end // position in milliseconds if (points[i].dwSampleLength) { points[i].end = (points[i].dwSampleLength / this.fmt.sampleRate) * 1000; points[i].end += points[i].position; // If its not a region, end should be null } else { points[i].end = null; } // Remove attrs that should not go in the results delete points[i].value; } return points; } /** * Create a cue point in the wave file. * @param {!{ * position: number, * label: ?string, * end: ?number, * dwPurposeID: ?number, * dwCountry: ?number, * dwLanguage: ?number, * dwDialect: ?number, * dwCodePage: ?number * }} pointData A object with the data of the cue point. * * # Only required attribute to create a cue point: * pointData.position: The position of the point in milliseconds * * # Optional attribute for cue points: * pointData.label: A string label for the cue point * * # Extra data used for regions * pointData.end: A number representing the end of the region, * in milliseconds, counting from the start of the file. If * no end attr is specified then no region is created. * * # You may also specify the following attrs for regions, all optional: * pointData.dwPurposeID * pointData.dwCountry * pointData.dwLanguage * pointData.dwDialect * pointData.dwCodePage */ setCuePoint(pointData) { this.cue.chunkId = 'cue '; // label attr should always exist if (!pointData.label) { pointData.label = ''; } /** * Load the existing points before erasing * the LIST 'adtl' chunk and the cue attr * @type {!Array<!Object>} */ let existingPoints = this.getCuePoints_(); // Clear any LIST labeled 'adtl' // The LIST chunk should be re-written // after the new cue point is created this.clearLISTadtl_(); // Erase this.cue so it can be re-written // after the point is added this.cue.points = []; /** * Cue position param is informed in milliseconds, * here its value is converted to the sample offset * @type {number} */ pointData.dwSampleOffset = (pointData.position * this.fmt.sampleRate) / 1000; /** * end param is informed in milliseconds, counting * from the start of the file. * here its value is converted to the sample length * of the region. * @type {number} */ pointData.dwSampleLength = 0; if (pointData.end) { pointData.dwSampleLength = ((pointData.end * this.fmt.sampleRate) / 1000) - pointData.dwSampleOffset; } // If there were no cue points in the file, // insert the new cue point as the first if (existingPoints.length === 0) { this.setCuePoint_(pointData, 1); // If the file already had cue points, This new one // must be added in the list according to its position. } else { this.setCuePointInOrder_(existingPoints, pointData); } this.cue.dwCuePoints = this.cue.points.length; } /** * Remove a cue point from a wave file. * @param {number} index the index of the point. First is 1, * second is 2, and so on. */ deleteCuePoint(index) { this.cue.chunkId = 'cue '; /** @type {!Array<!Object>} */ let existingPoints = this.getCuePoints_(); this.clearLISTadtl_(); /** @type {number} */ let len = this.cue.points.length; this.cue.points = []; for (let i = 0; i < len; i++) { if (i + 1 !== index) { this.setCuePoint_(existingPoints[i], i + 1); } } this.cue.dwCuePoints = this.cue.points.length; if (this.cue.dwCuePoints) { this.cue.chunkId = 'cue '; } else { this.cue.chunkId = ''; this.clearLISTadtl_(); } } /** * Update the label of a cue point. * @param {number} pointIndex The ID of the cue point. * @param {string} label The new text for the label. */ updateLabel(pointIndex, label) { /** @type {?number} */ let cIndex = this.getLISTIndex('adtl'); if (cIndex !== null) { for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) { if (this.LIST[cIndex].subChunks[i].dwName == pointIndex) { this.LIST[cIndex].subChunks[i].value = label; } } } } /** * Return an array with all cue points in the file, in the order they appear * in the file. * @return {!Array<!Object>} * @private */ getCuePoints_() { /** @type {!Array<!Object>} */ let points = []; for (let i = 0; i < this.cue.points.length; i++) { /** @type {!Object} */ let chunk = this.cue.points[i]; /** @type {!Object} */ let pointData = this.getDataForCuePoint_(chunk.dwName); pointData.label = pointData.value ? pointData.value : ''; pointData.dwPosition = chunk.dwPosition; pointData.fccChunk = chunk.fccChunk; pointData.dwChunkStart = chunk.dwChunkStart; pointData.dwBlockStart = chunk.dwBlockStart; pointData.dwSampleOffset = chunk.dwSampleOffset; points.push(pointData); } return points; } /** * Return the associated data of a cue point. * @param {number} pointDwName The ID of the cue point. * @return {!Object} * @private */ getDataForCuePoint_(pointDwName) { /** @type {?number} */ let LISTindex = this.getLISTIndex('adtl'); /** @type {!Object} */ let pointData = {}; // If there is a adtl LIST in the file, look for // LIST subchunks with data referencing this point if (LISTindex !== null) { this.getCueDataFromLIST_(pointData, LISTindex, pointDwName); } return pointData; } /** * Get all data associated to a cue point in a LIST chunk. * @param {!Object} pointData A object to hold the point data. * @param {number} index The index of the adtl LIST chunk. * @param {number} pointDwName The ID of the cue point. * @private */ getCueDataFromLIST_(pointData, index, pointDwName) { // got through all chunks in the adtl LIST checking // for references to this cue point for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) { if (this.LIST[index].subChunks[i].dwName == pointDwName) { /** @type {!Object} */ let chunk = this.LIST[index].subChunks[i]; // Some chunks may reference the point but // have a empty text; this is to ensure that if // one chunk that reference the point has a text, // this value will be kept as the associated data label // for the cue point. // If different values are present, the last value found // will be considered the label for the cue point. pointData.value = chunk.value || pointData.value; pointData.dwName = chunk.dwName || 0; pointData.dwSampleLength = chunk.dwSampleLength || 0; pointData.dwPurposeID = chunk.dwPurposeID || 0; pointData.dwCountry = chunk.dwCountry || 0; pointData.dwLanguage = chunk.dwLanguage || 0; pointData.dwDialect = chunk.dwDialect || 0; pointData.dwCodePage = chunk.dwCodePage || 0; } } } /** * Push a new cue point in this.cue.points. * @param {!Object} pointData A object with data of the cue point. * @param {number} dwName the dwName of the cue point * @private */ setCuePoint_(pointData, dwName) { this.cue.points.push({ dwName: dwName, dwPosition: pointData.dwPosition ? pointData.dwPosition : 0, fccChunk: pointData.fccChunk ? pointData.fccChunk : 'data', dwChunkStart: pointData.dwChunkStart ? pointData.dwChunkStart : 0, dwBlockStart: pointData.dwBlockStart ? pointData.dwBlockStart : 0, dwSampleOffset: pointData.dwSampleOffset }); this.setLabl_(pointData, dwName); } /** * Push a new cue point in this.cue.points according to existing cue points. * @param {!Array} existingPoints Array with the existing points. * @param {!Object} pointData A object with data of the cue point. * @private */ setCuePointInOrder_(existingPoints, pointData) { /** @type {boolean} */ let hasSet = false; // Iterate over the cue points that existed // before this one was added for (let i = 0; i < existingPoints.length; i++) { // If the new point is located before this original point // and the new point have not been created, create the // new point and then the original point if (existingPoints[i].dwSampleOffset > pointData.dwSampleOffset && !hasSet) { // create the new point this.setCuePoint_(pointData, i + 1); // create the original point this.setCuePoint_(existingPoints[i], i + 2); hasSet = true; // Otherwise, re-create the original point } else { this.setCuePoint_(existingPoints[i], hasSet ? i + 2 : i + 1); } } // If no point was created in the above loop, // create the new point as the last one if (!hasSet) { this.setCuePoint_(pointData, this.cue.points.length + 1); } } /** * Clear any LIST chunk labeled as 'adtl'. * @private */ clearLISTadtl_() { for (let i = 0, len = this.LIST.length; i < len; i++) { if (this.LIST[i].format == 'adtl') { this.LIST.splice(i); } } } /** * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'. * This method creates a LIST adtl chunk in the file if one * is not present. * @param {!Object} pointData A object with data of the cue point. * @param {number} dwName The ID of the cue point. * @private */ setLabl_(pointData, dwName) { /** * Get the index of the LIST chunk labeled as adtl. * A file can have many LIST chunks with unique labels. * @type {?number} */ let adtlIndex = this.getLISTIndex('adtl'); // If there is no adtl LIST, create one if (adtlIndex === null) { // Include a new item LIST chunk this.LIST.push({ chunkId: 'LIST', chunkSize: 4, format: 'adtl', subChunks: []}); // Get the index of the new LIST chunk adtlIndex = this.LIST.length - 1; } this.setLabelText_(adtlIndex, pointData, dwName); if (pointData.dwSampleLength) { this.setLtxtChunk_(adtlIndex, pointData, dwName); } } /** * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'. * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST. * @param {!Object} pointData A object with data of the cue point. * @param {number} dwName The ID of the cue point. * @private */ setLabelText_(adtlIndex, pointData, dwName) { this.LIST[adtlIndex].subChunks.push({ chunkId: 'labl', chunkSize: 4, // should be 4 + label length in bytes dwName: dwName, value: pointData.label }); this.LIST[adtlIndex].chunkSize += 12; // should be 4 + label byte length } /** * Create a new 'ltxt' subchunk in a 'LIST' chunk of type 'adtl'. * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST. * @param {!Object} pointData A object with data of the cue point. * @param {number} dwName The ID of the cue point. * @private */ setLtxtChunk_(adtlIndex, pointData, dwName) { this.LIST[adtlIndex].subChunks.push({ chunkId: 'ltxt', chunkSize: 20, // should be 12 + label byte length dwName: dwName, dwSampleLength: pointData.dwSampleLength, dwPurposeID: pointData.dwPurposeID || 0, dwCountry: pointData.dwCountry || 0, dwLanguage: pointData.dwLanguage || 0, dwDialect: pointData.dwDialect || 0, dwCodePage: pointData.dwCodePage || 0, value: pointData.label // kept for compatibility }); this.LIST[adtlIndex].chunkSize += 28; } }