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