/**
* @class
* @classdesc Encapsulation of several functions for manipulating the parsed AlphaTexStructure from AlphaTex
*/
class ParsedOutputManipulator {
/**
* Creates a new ParsedOutputManipulator
* @param {ParsedOutputPackage} mainObj The main ParsedOutputPackage object
*/
constructor(mainObj) {
this.mainObj = mainObj;
}
/**
* Finds the provided index of the provided attribute in the provided object if present
* @param {object} obj The object to be searched. It's expected that obj.attribute is a string[] where attribute is the provided parameter
* @param {string} attribute The attribute to be searched in the provided object
* @param {number} index The index of the attribute to be retrieved
* @returns {object} Either the found value of obj[attribute][index] or null
*/
indexObj(obj, attribute, index) {
if (Array.isArray(obj[attribute])) {
if (index < obj[attribute].length) {
return obj[attribute][index];
} else {
return null;
}
} else if (index === 0) {
return obj[attribute];
} else {
return null;
}
}
/**
* Gets the Track object at the provided index from the main AlphaTexStructure object
* @param {number} trackIndex The index to be retrieved
* @returns {Track} The found Track object or null if not found
*/
getTrack(trackIndex) {
return this.indexObj(this.mainObj, "tracks", trackIndex);
}
/**
* Gets the Track object with the given name
* @param {string} trackName Track name to be found
* @returns {Track} The found Track object or null if not found
*/
getTrackByName(trackName) {
let track = null;
if (Array.isArray(this.mainObj["tracks"])) {
for (let i = 0; i < this.mainObj["tracks"].length; i++) {
let nextTrack = this.mainObj["tracks"][i];
if (nextTrack["name"] === trackName) {
track = nextTrack;
break;
}
}
} else {
let nextTrack = this.mainObj["tracks"];
if (nextTrack["name"] === trackName) {
track = nextTrack;
}
}
return track;
}
/**
* Gets the index of the Track with the provided track name
* @param {string} trackName The name of the track to be found
* @returns {number} The index of the track or null if not found
*/
getTrackIndexByName(trackName) {
let trackIndex = null;
if (Array.isArray(this.mainObj["tracks"])) {
for (let i = 0; i < this.mainObj["tracks"].length; i++) {
let nextTrack = this.mainObj["tracks"][i];
if (nextTrack["name"] === trackName) {
trackIndex = i;
break;
}
}
} else {
let nextTrack = this.mainObj["tracks"];
if (nextTrack["name"] === trackName) {
trackIndex = i;
}
}
return trackIndex;
}
/**
* Gets the Staff object of the provided Track object at the given index
* @param {Track} track The track to be searched
* @param {number} staffIndex The index of the staff to be accessed
* @returns {Staff} The found Staff object or null if not found
*/
getStaff(track, staffIndex) {
return this.indexObj(track, "staffs", staffIndex);
}
/**
* Gets the Measure object of the provided Staff object at the given index
* @param {Staff} staff The Staff to be serached
* @param {number} measureIndex The index of the measure to be accessed
* @returns {Measure} The found Measure or null if not found
*/
getMeasure(staff, measureIndex) {
return this.indexObj(staff, "measures", measureIndex);
}
/**
* Gets the end measure number of the given staff number of the Track with the given name
* @param {string} trackName The name of the Track to be accessed
* @param {number} staffNumber The number of the Staff to be accessed. NOTE: This is a number (i.e. index + 1)
* @returns {number} The ending measure number
*/
getMeasureEnd(trackName, staffNumber) {
let measureEnd = null;
let track = this.getTrackByName(trackName);
if (track !== null) {
const staffIndex = staffNumber - 1;
let staff;
if (Array.isArray(track["staffs"]) && staffIndex < track["staffs"].length) {
staff = track["staffs"][staffIndex];
} else if (staffIndex === 0) {
staff = track["staffs"];
}
if (staff !== null) {
if (Array.isArray(staff["measures"])) {
measureEnd = staff["measures"].length;
} else {
measureEnd = 1;
}
}
}
return measureEnd;
}
/**
* Adds either the given value or the retrieved value of the attribute from attributesInt off of measure if present to the given array
* @param {number[]} targetArray Collection to which to add the value
* @param {Measure} measure Measure to access
* @param {number} currentValue The current value to be added unless the given attribute is an attributeInt of the given Measure
* @param {string} attribute The attribute to be accessed from the given Measure if present
* @returns {number} The updated current value if the measure has the attribute as an attributeInt otherwise the given currentValue
* @private
*/
addToParsedArray(targetArray, measure, currentValue, attribute) {
if (measure["attributesInt"][attribute]) {
currentValue = measure["attributesInt"][attribute];
}
targetArray.push(currentValue);
return currentValue;
}
/**
* @typedef {object} MeasureTempoAndTSPackage
* @property {number[]} measureToTempo Array where the element at the ith index is the tempo for the i+1 measure number
* @property {number[]} ts A 1D 2 element array representing the last time signature read
*/
/**
* Iterates over a Staff collecting an array showing what the tempo of each measure is and the final time signature recorded
* @param {Staff} staff The Staff object to be visited
* @returns {MeasureTempoAndTSPackage} Tempo data per measure at the latest read time signature.
*/
visitMeasure(staff) {
let measureToTempo = [];
let currentTempo = -1;
let ts = [4, 4];
if (Array.isArray(staff["measures"])) {
staff["measures"].forEach((measure) => {
currentTempo = this.addToParsedArray(measureToTempo, measure, currentTempo, "tempo");
if (measure["attributesInt"]["tsTop"]) {
ts[0] = measure["attributesInt"]["tsTop"];
ts[1] = measure["attributesInt"]["tsBottom"];
}
});
} else {
let measure = staff["measures"];
currentTempo = this.addToParsedArray(measureToTempo, measure, currentTempo, "tempo");
if (measure["attributesInt"]["tsTop"]) {
ts[0] = measure["attributesInt"]["tsTop"];
ts[1] = measure["attributesInt"]["tsBottom"];
}
}
return {measureToTempo, ts};
}
/**
* Utility function for getLyricArray visiting the Chord object
* Adds lyrics to the lyricArray from the lyrics collection for each note that isn't silence
* @param {Chord} chord The Chord object that is being accessed
* @param {string[]} lyrics Collection of lyrics for the current staff
* @param {string[]} lyricArray The lyric collection that is being built
* @param {number} lyricIndex The current index into the lyrics array
* @returns {number} The latest lyricIndex
* @private
*/
getLyricArrayChord(chord, lyrics, lyricArray, lyricIndex) {
if (Array.isArray(chord["pitches"])) {
chord["pitches"].forEach((pitch) => {
if (pitch["midiValue"] !== -1) {
lyricArray.push(lyrics[lyricIndex]);
lyricIndex++;
}
});
} else {
if (chord["pitches"]["midiValue"] !== -1) {
lyricArray.push(lyrics[lyricIndex]);
lyricIndex++;
}
}
return lyricIndex;
}
/**
* Utility function for getLyricArray visiting the Measure object
* @param {Measure} measure The Measure object being accessed
* @param {string[]} lyrics Collection of lyrics for the current staff
* @param {string[]} lyricArray The lyric collection that is being built
* @param {number} lyricIndex The current index into the lyrics array
* @returns {number} The latest lyricIndex
* @private
*/
getLyricArrayMeasure(measure, lyrics, lyricArray, lyricIndex) {
if (Array.isArray(measure["chords"])) {
measure["chords"].forEach((chord) => {
lyricIndex = this.getLyricArrayChord(chord, lyrics, lyricArray, lyricIndex);
});
} else {
lyricIndex = this.getLyricArrayChord(measure["chords"], lyrics, lyricArray, lyricIndex);
}
return lyricIndex;
}
/**
* @typedef {object} LyricArrayPackage
* @property {string[]} lyricArray Collection of lyrics for a Staff
* @property {number} lyricArrayIndex The latest index into lyricArray to add lyrics to lyricArray
*/
/**
* Creates a new lyrics array for the given Staff starting the first lyric at the provided measure number
* @param {Staff} staff The Staff object to be accessed
* @param {number} firstMeasureNumber The first measure number to access. Note: The index into staff.measures = firstMeasureNumber - 1
* @returns {string[]}
*/
getLyricArray(staff, firstMeasureNumber) {
if (!staff["lyrics"]) {
return null;
}
let staffLyrics = staff["lyrics"];
let lyricArray = [];
let lyricIndex = 0;
let measureIndex = 0;
let lyricArrayIndex = -1;
if (Array.isArray(staff["measures"])) {
staff["measures"].forEach((measure) => {
if (measureIndex === firstMeasureNumber - 1) {
lyricArrayIndex = lyricIndex;
}
lyricIndex = this.getLyricArrayMeasure(measure, staffLyrics, lyricArray, lyricIndex);
measureIndex++;
});
} else {
if (measureIndex === firstMeasureNumber - 1) {
lyricArrayIndex = lyricIndex;
}
lyricIndex = this.getLyricArrayMeasure(staff["measures"], staffLyrics, lyricArray, lyricIndex);
measureIndex++;
}
return {lyricArray, lyricArrayIndex};
}
/**
* @typedef {object} TempoTimePackage
* @property {number} tempTempo The last noted tempo
* @property {number} tempTimeFactor The last noted Measure length
*/
/**
* Utility function for getMeasureTimeLength that gets the time length of the given measure
* @param {number[]} measureTimeLength The ith index contains the length in seconds of the i+1 Measure
* @param {Measure} measure The Measure to be evaluated
* @param {number} tempo The last noted tempo
* @param {number} timeFactor The last noted Measure length
* @param {number[]} ts A 1D 3 element array. The first and second elements represent the time signature. The third element is the number of quarter notes per Measure
* @returns {TempoTimePackage} The latest noted tempo and Measure length
* @private
*/
getNextMeasureTimeLength(measureTimeLength, measure, tempo, timeFactor, ts) {
let tempTempo = tempo;
let tempTimeFactor = timeFactor;
if (measure["attributesInt"]["tempo"]) {
tempTempo = measure["attributesInt"]["tempo"];
tempTimeFactor = (60/tempTempo)*ts[2];
}
if (measure["attributesInt"]["tsTop"]) {
ts[0] = measure["attributesInt"]["tsTop"];
ts[1] = measure["attributesInt"]["tsBottom"];
ts[2] = ts[0] / (ts[1]/4);
tempTimeFactor = (60/tempTempo)*ts[2];
}
measureTimeLength.push(tempTimeFactor);
return {tempTempo, tempTimeFactor};
}
/**
* Gets an array where the ith index contains the length in seconds of the i+1 Measure of the first Staff of the given Track
* @param {string} trackName The name of the Track to be accessed
* @returns {number[]} A collection of the lengths of each Measure. The ith index contains the length in seconds of the i+1 Measure
*/
getMeasureTimeLength(trackName) {
let track = this.getTrackByName(trackName);
// assumes you want the first staff
let staff = this.getStaff(track, 0);
let measureTimeLength = [];
let tempo = this.mainObj["tempo"];
let ts = [4,4,4];
let timeFactor = (60/tempo)*ts[2];
if (Array.isArray(staff["measures"])) {
staff["measures"].forEach((measure) => {
let {tempTempo, tempTimeFactor} = this.getNextMeasureTimeLength(measureTimeLength, measure, tempo, timeFactor, ts);
tempo = tempTempo;
timeFactor = tempTimeFactor;
});
} else {
let measure = staff["measures"];
let {tempTempo, tempTimeFactor} = this.getNextMeasureTimeLength(measureTimeLength, measure, tempo, timeFactor, ts);
tempo = tempTempo;
timeFactor = tempTimeFactor;
}
return measureTimeLength;
}
}
module.exports = ParsedOutputManipulator;