Source: Parser/ParserMeasures.js

  1. const ParsedOutputManipulator = require("./ParsedOutputManipulator.js");
  2. const NOTE_STRING = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"];
  3. /**
  4. * @class
  5. * @classdesc Encapsulates functions used to generate an exercise from a collection of measures
  6. */
  7. class ParserMeasures {
  8. /**
  9. * Creates a new ParserMeasures
  10. * @param {ParsedOutputPackage} parsedOutputObj The main ParsedOutputPackage
  11. */
  12. constructor(parsedOutputObj) {
  13. this.manipulator = new ParsedOutputManipulator(parsedOutputObj);
  14. }
  15. /**
  16. * Converts the duration in seconds of a Note back to the duration in AlphaTex
  17. * @param {number} durationInSeconds The duration in seconds of the given Note
  18. * @param {number} currentTempoFactor The current factor based on the tempo
  19. * @param {Set} beatEffects A set of effects that are modifying the Note
  20. * @returns {number} The duration as would be noted in AlphaTex
  21. */
  22. getDurationFromDurationInSeconds(durationInSeconds, currentTempoFactor, beatEffects) {
  23. let duration = durationInSeconds;
  24. if (beatEffects.includes("dotted")) {
  25. duration /= 1.5;
  26. }
  27. duration /= currentTempoFactor;
  28. duration = 4 / duration;
  29. return duration;
  30. }
  31. /**
  32. * Creates AlphaTex from a given midi value
  33. * @param {number} midi The midi value of the Note with -1 as a special value for silence
  34. * @returns {string} The AlphaTex for that note either "r" for a rest (midi === -1) or the appended string representation of the note and its octave
  35. * @private
  36. */
  37. getTextFromMidi(midi) {
  38. let text = "";
  39. if (midi === -1) {
  40. text = "r";
  41. } else {
  42. let octave = Math.floor((midi / 12) - 1);
  43. let noteIndex = midi % 12;
  44. let note = NOTE_STRING[noteIndex];
  45. text = note + octave;
  46. }
  47. return text;
  48. }
  49. /**
  50. * @typedef {object} LyricPackage
  51. * @property {string} tempLyricText The lyric text for this note or empty string
  52. * @property {number} lyricArrayIndex The latest lyric index into lyricArray
  53. */
  54. /**
  55. * Gets the lyric text for the given Pitch object
  56. * @param {ParsedOutputPitchPackage} pitch The Pitch object to be analyzed
  57. * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
  58. * @param {string[]} lyricArray The collection of lyrics to pull from
  59. * @param {number} lyricArrayIndex The latest index into lyricArray
  60. * @returns {LyricPackage} The lyric text for the pitch if it's not silence and the latest lyric index
  61. * @private
  62. */
  63. getLyricText(pitch, isDurationExercise, lyricArray, lyricArrayIndex) {
  64. let tempLyricText;
  65. let tempLyricIndex = lyricArrayIndex;
  66. if (isDurationExercise) {
  67. if (pitch["midiValue"] !== -1) {
  68. tempLyricText = "oo";
  69. } else {
  70. tempLyricText = "";
  71. }
  72. } else if (pitch["midiValue"] !== -1 && lyricArray !== null) {
  73. tempLyricText = lyricArray[tempLyricIndex];
  74. tempLyricIndex++;
  75. } else {
  76. tempLyricText = "";
  77. }
  78. return {tempLyricText, "lyricArrayIndex":tempLyricIndex};
  79. }
  80. /**
  81. * Creates AlphaTex for the given Pitch
  82. * @param {ParsedOutputPitchPackage} pitch The Pitch object to be analyzed
  83. * @param {number} tempoFactor The latest factor based on the tempo
  84. * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
  85. * @returns {string} The generated AlphaTex for the given Pitch
  86. * @private
  87. */
  88. getPitchText(pitch, tempoFactor, isDurationExercise) {
  89. let pitchText;
  90. let effectText = "";
  91. let beatEffectText = "";
  92. if (pitch["beatEffects"].length > 0) {
  93. pitch["beatEffects"].forEach((effect) => {
  94. if (effect === "tied") {
  95. if (effectText.length > 0) {
  96. effectText += " ";
  97. }
  98. effectText += "-";
  99. } else if (effect === "dotted") {
  100. if (effectText.length > 0) {
  101. effectText += " ";
  102. }
  103. effectText += "d";
  104. } else if (effect === "crescendo") {
  105. if (beatEffectText.length > 0) {
  106. beatEffectText += " ";
  107. }
  108. beatEffectText += "cre";
  109. } else if (effect === "decrescendo") {
  110. if (beatEffectText.length > 0) {
  111. beatEffectText += " ";
  112. }
  113. beatEffectText += "dec";
  114. }
  115. });
  116. }
  117. let duration = this.getDurationFromDurationInSeconds(pitch["duration"], tempoFactor, pitch["beatEffects"]);
  118. if (isDurationExercise) {
  119. if (pitch["midiValue"] === -1) {
  120. pitchText = "r";
  121. } else {
  122. pitchText = "c4";
  123. }
  124. } else {
  125. pitchText = this.getTextFromMidi(pitch["midiValue"]);
  126. }
  127. if (pitchText === "r") {
  128. pitchText += "." + duration;
  129. if (effectText.length > 0 || beatEffectText.length > 0) {
  130. pitchText += "{";
  131. pitchText += effectText + beatEffectText;
  132. pitchText += "}";
  133. }
  134. } else {
  135. if (effectText.length > 0) {
  136. pitchText += "{" + effectText + "}";
  137. }
  138. pitchText += "." + duration;
  139. if (beatEffectText.length > 0) {
  140. pitchText += "{" + beatEffectText + "}";
  141. }
  142. }
  143. return pitchText;
  144. }
  145. /**
  146. * @typedef {object} ChordTextPackage
  147. * @property {string} chordText The AlphaTex of the Chord
  148. * @property {string} lyricText The lyrics that go along with the Chord
  149. * @property {number} lyricArrayIndex The latest index in the lyricArray
  150. */
  151. /**
  152. * Creates AlphaTex for the given Chord
  153. * @param {ParsedOutputChordPackage} chord The Chord object to be analyzed
  154. * @param {number} tempoFactor The latest factor based on the tempo
  155. * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
  156. * @param {string[]} lyricArray The collection of lyrics to pull from
  157. * @param {number} lyricArrayIndex The latest index into lyricArray
  158. * @returns {ChordTextPackage} The generated AlphaTex for the Chord and some auxilary information
  159. * @private
  160. */
  161. getChordText(chord, tempoFactor, isDurationExercise, lyricArray, lyricArrayIndex) {
  162. let chordText = "";
  163. let lyricText = "";
  164. let tempLyricIndex = lyricArrayIndex;
  165. if (Array.isArray(chord["pitches"])) {
  166. chordText += "( ";
  167. let duration = null;
  168. chord["pitches"].forEach((pitch) => {
  169. let pitchText = this.getPitchText(pitch, tempoFactor, isDurationExercise);
  170. let dotIndex = pitchText.indexOf(".");
  171. chordText += pitchText.substring(0, dotIndex) + " ";
  172. if (duration === null) {
  173. duration = parseInt(pitchText.substring(dotIndex+1, pitchText.length));
  174. }
  175. let {tempLyricText, lyricArrayIndex} = this.getLyricText(pitch, isDurationExercise, lyricArray, tempLyricIndex);
  176. tempLyricIndex = lyricArrayIndex;
  177. if (tempLyricText.length > 0) {
  178. if (lyricText.length > 0) {
  179. lyricText += " ";
  180. }
  181. lyricText += tempLyricText;
  182. }
  183. });
  184. chordText += ")";
  185. chordText += "." + duration;
  186. } else {
  187. chordText += this.getPitchText(chord["pitches"], tempoFactor, isDurationExercise) + " ";
  188. let {tempLyricText, lyricArrayIndex} = this.getLyricText(chord["pitches"], isDurationExercise, lyricArray, tempLyricIndex);
  189. tempLyricIndex = lyricArrayIndex;
  190. if (tempLyricText.length > 0) {
  191. if (lyricText.length > 0) {
  192. lyricText += " ";
  193. }
  194. lyricText += tempLyricText;
  195. }
  196. }
  197. return {chordText, lyricText, "lyricArrayIndex":tempLyricIndex};
  198. }
  199. /**
  200. * @typedef {object} MeasureTextPackage
  201. * @property {string} measureText The AlphaTex for the Measure
  202. * @property {string} lyricsText The lyrics that go along with the Measure
  203. * @property {number} tempLyricIndex The latest index in the lyricArray
  204. */
  205. /**
  206. * Creates AlphaTex for the given Measure
  207. * @param {number} measureIndex The index of the Measure to be accessed
  208. * @param {ParsedOutputStaffPackage} staff The Staff object to be accessed
  209. * @param {number} currentTempoFactor The latest factor based on the tempo
  210. * @param {number} measureTempo The tempo of the Measure to be accessed
  211. * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
  212. * @param {string[]} lyricArray The collection of lyrics to pull from
  213. * @param {number} lyricArrayIndex The latest index into lyricArray
  214. * @param {number[]} startingTs A 1D two element array representing the current tempo
  215. * @param {boolean} isLastMeasure If true then this is the last measure otherwise false
  216. * @returns {MeasureTextPackage} The generated AlphaTex for the Measure and some auxillary data
  217. * @private
  218. */
  219. getMeasureText(measureIndex, staff, currentTempoFactor, measureTempo, isDurationExercise, lyricArray, lyricArrayIndex, startingTs, isLastMeasure) {
  220. let measureObj = this.manipulator.getMeasure(staff, measureIndex);
  221. let tempLyricIndex = lyricArrayIndex;
  222. let measureText = "";
  223. let lyricsText = "";
  224. let attributesInt = measureObj["attributesInt"];
  225. if (attributesInt["tempo"]) {
  226. measureText += "\\tempo " + attributesInt["tempo"] + "\n";
  227. }
  228. if (startingTs !== null) {
  229. measureText += "\\ts " + startingTs[0] + " " + startingTs[1] + "\n";
  230. } else if (attributesInt["tsTop"]) {
  231. measureText += "\\ts " + attributesInt["tsTop"] + " " + attributesInt["tsBottom"] + "\n";
  232. }
  233. let tempoFactor = measureTempo === -1 ? currentTempoFactor : 60 / measureTempo;
  234. if (Array.isArray(measureObj["chords"])) {
  235. measureObj["chords"].forEach((chord) => {
  236. let {chordText, lyricText, lyricArrayIndex} = this.getChordText(chord, tempoFactor, isDurationExercise, lyricArray, tempLyricIndex);
  237. tempLyricIndex = lyricArrayIndex;
  238. measureText += chordText + " ";
  239. if (lyricText.length > 0) {
  240. if (lyricsText.length > 0) {
  241. lyricsText += " ";
  242. }
  243. }
  244. lyricsText += lyricText;
  245. });
  246. } else {
  247. let {chordText, lyricText, lyricArrayIndex} = this.getChordText(measureObj["chords"], tempoFactor, isDurationExercise, lyricArray, tempLyricIndex);
  248. tempLyricIndex = lyricArrayIndex;
  249. measureText += chordText;
  250. if (lyricText.length > 0) {
  251. if (lyricsText.length > 0) {
  252. lyricsText += " ";
  253. }
  254. }
  255. lyricsText += lyricText;
  256. }
  257. if (!isLastMeasure) {
  258. measureText += "|";
  259. }
  260. measureText += "\n";
  261. return {measureText, lyricsText, tempLyricIndex};
  262. }
  263. /**
  264. * @typedef {object} MeasureAlphaTexPackage
  265. * @property {string} lyricText The lyrics of the provided AlphaTex
  266. * @property {string} alphaTex The generated AlphaTex
  267. */
  268. /**
  269. * Creates an exercise within the provided measure bounds of the track and staff number specified either a duration exercise or a normal one
  270. * @param {number[]} measures The start and end of the measure bounds. If null, then get all measures
  271. * @param {number} trackNumber The number of the Track to be retrieved.
  272. * @param {number} staffNumber The number of the Staff to be retrieved
  273. * @param {boolean} isDurationExercise If true then this will generate a duration exercise otherwise if false then it will just copy the Measures
  274. * @returns {MeasureAlphaTexPackage} If successful, provides the lyrics and generated alphaTex. Otherwise, returns null
  275. */
  276. measuresToAlphaTex(measures, trackNumber, staffNumber, isDurationExercise) {
  277. let trackIndex = trackNumber - 1;
  278. let staffIndex = staffNumber - 1;
  279. let track = this.manipulator.getTrack(trackIndex);
  280. let staff;
  281. if (track !== null) {
  282. staff = this.manipulator.getStaff(track, staffIndex);
  283. }
  284. if (staff && Array.isArray(measures) && measures.length > 0) {
  285. // assumes provided array measures is sorted
  286. let {lyricArray, lyricArrayIndex} = this.manipulator.getLyricArray(staff, measures[0]);
  287. let lyricText = "";
  288. let measuresText = "";
  289. let {measureToTempo, ts} = this.manipulator.visitMeasure(staff);
  290. // assumes provided array measures is sorted
  291. let firstMeasureTempo = measureToTempo[measures[0]];
  292. let startingTempo = firstMeasureTempo === -1 ? this.manipulator.mainObj["tempo"] : firstMeasureTempo;
  293. let currentTempoFactor = 60 / startingTempo;
  294. let alphaTex = "\\title \"Exercise t: " + trackNumber + " s: " + staffNumber + " startM: " + measures[0] + " endM: " + measures[1] + "\"\n" +
  295. "\\tempo " + startingTempo + "\n" +
  296. ".\n" +
  297. "\n";
  298. if (measures.length > 2 && measures[2] === "all") {
  299. alphaTex += "\\track \"" + this.manipulator.getTrack(trackIndex).name + "\"\n";
  300. } else {
  301. alphaTex += "\\track \"Exercise\"\n";
  302. }
  303. alphaTex += "\\staff {score} \\tuning piano \\instrument acousticgrandpiano \\ks " + staff.keySignature + (staff.clef ? " \\clef " + staff.clef : "") + "\n";
  304. for (let i = measures[0]; i <= measures[1]; i++) {
  305. let measureIndex = i - 1;
  306. let startingTs = i !== measures[0] ? null : ts;
  307. let isLastMeasure = i === measures[1];
  308. let {measureText, lyricsText, tempLyricIndex } = this.getMeasureText(measureIndex, staff, currentTempoFactor, measureToTempo[measureIndex], isDurationExercise, lyricArray, lyricArrayIndex, startingTs, isLastMeasure);
  309. lyricArrayIndex = tempLyricIndex;
  310. measuresText += measureText;
  311. if (lyricsText.length > 0) {
  312. if (lyricText.length > 0) {
  313. lyricText += " ";
  314. }
  315. lyricText += lyricsText;
  316. }
  317. }
  318. if (lyricText.length > 0) {
  319. alphaTex += "\\lyrics \"" + lyricText + "\"\n";
  320. }
  321. alphaTex += measuresText;
  322. return {lyricText, alphaTex};
  323. } else {
  324. return null;
  325. }
  326. }
  327. }
  328. module.exports = ParserMeasures;