VoiceGenerator.ts 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. import { Instrument } from "../Instrument";
  2. import { LinkedVoice } from "../VoiceData/LinkedVoice";
  3. import { Voice } from "../VoiceData/Voice";
  4. import { MusicSheet } from "../MusicSheet";
  5. import { VoiceEntry, StemDirectionType } from "../VoiceData/VoiceEntry";
  6. import { Note } from "../VoiceData/Note";
  7. import { SourceMeasure } from "../VoiceData/SourceMeasure";
  8. import { SourceStaffEntry } from "../VoiceData/SourceStaffEntry";
  9. import { Beam } from "../VoiceData/Beam";
  10. import { Tie } from "../VoiceData/Tie";
  11. import { TieTypes } from "../../Common/Enums/";
  12. import { Tuplet } from "../VoiceData/Tuplet";
  13. import { Fraction } from "../../Common/DataObjects/Fraction";
  14. import { IXmlElement } from "../../Common/FileIO/Xml";
  15. import { ITextTranslation } from "../Interfaces/ITextTranslation";
  16. import { LyricsReader } from "../ScoreIO/MusicSymbolModules/LyricsReader";
  17. import { MusicSheetReadingException } from "../Exceptions";
  18. import { AccidentalEnum } from "../../Common/DataObjects/Pitch";
  19. import { NoteEnum } from "../../Common/DataObjects/Pitch";
  20. import { Staff } from "../VoiceData/Staff";
  21. import { StaffEntryLink } from "../VoiceData/StaffEntryLink";
  22. import { VerticalSourceStaffEntryContainer } from "../VoiceData/VerticalSourceStaffEntryContainer";
  23. import log from "loglevel";
  24. import { Pitch } from "../../Common/DataObjects/Pitch";
  25. import { IXmlAttribute } from "../../Common/FileIO/Xml";
  26. import { CollectionUtil } from "../../Util/CollectionUtil";
  27. import { ArticulationReader } from "./MusicSymbolModules/ArticulationReader";
  28. import { SlurReader } from "./MusicSymbolModules/SlurReader";
  29. import { Notehead } from "../VoiceData/Notehead";
  30. import { Arpeggio, ArpeggioType } from "../VoiceData/Arpeggio";
  31. import { NoteType, NoteTypeHandler } from "../VoiceData/NoteType";
  32. import { TabNote } from "../VoiceData/TabNote";
  33. import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
  34. export class VoiceGenerator {
  35. constructor(instrument: Instrument, voiceId: number, slurReader: SlurReader, mainVoice: Voice = undefined) {
  36. this.musicSheet = instrument.GetMusicSheet;
  37. this.slurReader = slurReader;
  38. if (mainVoice) {
  39. this.voice = new LinkedVoice(instrument, voiceId, mainVoice);
  40. } else {
  41. this.voice = new Voice(instrument, voiceId);
  42. }
  43. instrument.Voices.push(this.voice);
  44. this.lyricsReader = new LyricsReader(this.musicSheet);
  45. this.articulationReader = new ArticulationReader();
  46. }
  47. private slurReader: SlurReader;
  48. private lyricsReader: LyricsReader;
  49. private articulationReader: ArticulationReader;
  50. private musicSheet: MusicSheet;
  51. private voice: Voice;
  52. private currentVoiceEntry: VoiceEntry;
  53. private currentNote: Note;
  54. private currentMeasure: SourceMeasure;
  55. private currentStaffEntry: SourceStaffEntry;
  56. // private lastBeamTag: string = "";
  57. private openBeams: Beam[] = []; // works like a stack, with push and pop
  58. private beamNumberOffset: number = 0;
  59. private openTieDict: { [_: number]: Tie; } = {};
  60. private currentOctaveShift: number = 0;
  61. private tupletDict: { [_: number]: Tuplet; } = {};
  62. private openTupletNumber: number = 0;
  63. public get GetVoice(): Voice {
  64. return this.voice;
  65. }
  66. public get OctaveShift(): number {
  67. return this.currentOctaveShift;
  68. }
  69. public set OctaveShift(value: number) {
  70. this.currentOctaveShift = value;
  71. }
  72. /**
  73. * Create new [[VoiceEntry]], add it to given [[SourceStaffEntry]] and if given so, to [[Voice]].
  74. * @param musicTimestamp
  75. * @param parentStaffEntry
  76. * @param addToVoice
  77. * @param isGrace States whether the new VoiceEntry (only) has grace notes
  78. */
  79. public createVoiceEntry(musicTimestamp: Fraction, parentStaffEntry: SourceStaffEntry, addToVoice: boolean,
  80. isGrace: boolean = false, graceNoteSlash: boolean = false, graceSlur: boolean = false): void {
  81. this.currentVoiceEntry = new VoiceEntry(musicTimestamp.clone(), this.voice, parentStaffEntry, isGrace, graceNoteSlash, graceSlur);
  82. if (addToVoice) {
  83. this.voice.VoiceEntries.push(this.currentVoiceEntry);
  84. }
  85. }
  86. /**
  87. * Create [[Note]]s and handle Lyrics, Articulations, Beams, Ties, Slurs, Tuplets.
  88. * @param noteNode
  89. * @param noteDuration
  90. * @param divisions
  91. * @param restNote
  92. * @param parentStaffEntry
  93. * @param parentMeasure
  94. * @param measureStartAbsoluteTimestamp
  95. * @param maxTieNoteFraction
  96. * @param chord
  97. * @param guitarPro
  98. * @param printObject whether the note should be rendered (true) or invisible (false)
  99. * @returns {Note}
  100. */
  101. public read(noteNode: IXmlElement, noteDuration: Fraction, typeDuration: Fraction, noteTypeXml: NoteType, normalNotes: number, restNote: boolean,
  102. parentStaffEntry: SourceStaffEntry, parentMeasure: SourceMeasure,
  103. measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, chord: boolean, guitarPro: boolean,
  104. printObject: boolean, isCueNote: boolean, isGraceNote: boolean, stemDirectionXml: StemDirectionType, tremoloStrokes: number,
  105. stemColorXml: string, noteheadColorXml: string, vibratoStrokes: boolean): Note {
  106. this.currentStaffEntry = parentStaffEntry;
  107. this.currentMeasure = parentMeasure;
  108. //log.debug("read called:", restNote);
  109. try {
  110. this.currentNote = restNote
  111. ? this.addRestNote(noteNode.element("rest"), noteDuration, noteTypeXml, printObject, isCueNote, noteheadColorXml)
  112. : this.addSingleNote(noteNode, noteDuration, noteTypeXml, typeDuration, normalNotes, chord, guitarPro,
  113. printObject, isCueNote, isGraceNote, stemDirectionXml, tremoloStrokes, stemColorXml, noteheadColorXml, vibratoStrokes);
  114. // read lyrics
  115. const lyricElements: IXmlElement[] = noteNode.elements("lyric");
  116. if (this.lyricsReader !== undefined && lyricElements) {
  117. this.lyricsReader.addLyricEntry(lyricElements, this.currentVoiceEntry);
  118. this.voice.Parent.HasLyrics = true;
  119. }
  120. let hasTupletCommand: boolean = false;
  121. const notationNode: IXmlElement = noteNode.element("notations");
  122. if (notationNode) {
  123. // read articulations
  124. if (this.articulationReader) {
  125. this.readArticulations(notationNode, this.currentVoiceEntry, this.currentNote);
  126. }
  127. // read slurs
  128. const slurElements: IXmlElement[] = notationNode.elements("slur");
  129. if (this.slurReader !== undefined &&
  130. slurElements.length > 0 &&
  131. !this.currentNote.ParentVoiceEntry.IsGrace) {
  132. this.slurReader.addSlur(slurElements, this.currentNote);
  133. }
  134. // read Tuplets
  135. const tupletElements: IXmlElement[] = notationNode.elements("tuplet");
  136. if (tupletElements.length > 0) {
  137. this.openTupletNumber = this.addTuplet(noteNode, tupletElements);
  138. hasTupletCommand = true;
  139. }
  140. // check for Arpeggios
  141. const arpeggioNode: IXmlElement = notationNode.element("arpeggiate");
  142. if (arpeggioNode !== undefined && !this.currentVoiceEntry.IsGrace) {
  143. let currentArpeggio: Arpeggio;
  144. if (this.currentVoiceEntry.Arpeggio) { // add note to existing Arpeggio
  145. currentArpeggio = this.currentVoiceEntry.Arpeggio;
  146. } else { // create new Arpeggio
  147. let arpeggioAlreadyExists: boolean = false;
  148. for (const voiceEntry of this.currentStaffEntry.VoiceEntries) {
  149. if (voiceEntry.Arpeggio) {
  150. arpeggioAlreadyExists = true;
  151. currentArpeggio = voiceEntry.Arpeggio;
  152. // TODO handle multiple arpeggios across multiple voices at same timestamp
  153. // this.currentVoiceEntry.Arpeggio = currentArpeggio; // register the arpeggio in the current voice entry as well?
  154. // but then we duplicate information, and may have to take care not to render it multiple times
  155. // we already have an arpeggio in another voice, at the current timestamp. add the notes there.
  156. break;
  157. }
  158. }
  159. if (!arpeggioAlreadyExists) {
  160. let arpeggioType: ArpeggioType = ArpeggioType.ARPEGGIO_DIRECTIONLESS;
  161. const directionAttr: Attr = arpeggioNode.attribute("direction");
  162. if (directionAttr !== null) {
  163. switch (directionAttr.value) {
  164. case "up":
  165. arpeggioType = ArpeggioType.ROLL_UP;
  166. break;
  167. case "down":
  168. arpeggioType = ArpeggioType.ROLL_DOWN;
  169. break;
  170. default:
  171. arpeggioType = ArpeggioType.ARPEGGIO_DIRECTIONLESS;
  172. }
  173. }
  174. currentArpeggio = new Arpeggio(this.currentVoiceEntry, arpeggioType);
  175. this.currentVoiceEntry.Arpeggio = currentArpeggio;
  176. }
  177. }
  178. currentArpeggio.addNote(this.currentNote);
  179. }
  180. // check for Ties - must be the last check
  181. const tiedNodeList: IXmlElement[] = notationNode.elements("tied");
  182. if (tiedNodeList.length > 0) {
  183. this.addTie(tiedNodeList, measureStartAbsoluteTimestamp, maxTieNoteFraction, TieTypes.SIMPLE);
  184. }
  185. //check for slides, they are the same as Ties but with a different connection
  186. const slideNodeList: IXmlElement[] = notationNode.elements("slide");
  187. if (slideNodeList.length > 0) {
  188. this.addTie(slideNodeList, measureStartAbsoluteTimestamp, maxTieNoteFraction, TieTypes.SLIDE);
  189. }
  190. //check for slides, they are the same as Ties but with a different connection
  191. const technicalNode: IXmlElement = notationNode.element("technical");
  192. const hammerNodeList: IXmlElement[] = technicalNode.elements("hammer-on");
  193. if (hammerNodeList.length > 0) {
  194. this.addTie(hammerNodeList, measureStartAbsoluteTimestamp, maxTieNoteFraction, TieTypes.HAMMERON);
  195. }
  196. const pulloffNodeList: IXmlElement[] = technicalNode.elements("pull-off");
  197. if (pulloffNodeList.length > 0) {
  198. this.addTie(pulloffNodeList, measureStartAbsoluteTimestamp, maxTieNoteFraction, TieTypes.PULLOFF);
  199. }
  200. // remove open ties, if there is already a gap between the last tie note and now.
  201. const openTieDict: { [_: number]: Tie; } = this.openTieDict;
  202. for (const key in openTieDict) {
  203. if (openTieDict.hasOwnProperty(key)) {
  204. const tie: Tie = openTieDict[key];
  205. if (Fraction.plus(tie.StartNote.ParentStaffEntry.Timestamp, tie.Duration).lt(this.currentStaffEntry.Timestamp)) {
  206. delete openTieDict[key];
  207. }
  208. }
  209. }
  210. }
  211. // time-modification yields tuplet in currentNote
  212. // mustn't execute method, if this is the Note where the Tuplet has been created
  213. if (noteNode.element("time-modification") !== undefined && !hasTupletCommand) {
  214. this.handleTimeModificationNode(noteNode);
  215. }
  216. } catch (err) {
  217. const errorMsg: string = ITextTranslation.translateText(
  218. "ReaderErrorMessages/NoteError", "Ignored erroneous Note."
  219. );
  220. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  221. }
  222. return this.currentNote;
  223. }
  224. /**
  225. * Create a new [[StaffEntryLink]] and sets the currenstStaffEntry accordingly.
  226. * @param index
  227. * @param currentStaff
  228. * @param currentStaffEntry
  229. * @param currentMeasure
  230. * @returns {SourceStaffEntry}
  231. */
  232. public checkForStaffEntryLink(index: number, currentStaff: Staff, currentStaffEntry: SourceStaffEntry, currentMeasure: SourceMeasure): SourceStaffEntry {
  233. const staffEntryLink: StaffEntryLink = new StaffEntryLink(this.currentVoiceEntry);
  234. staffEntryLink.LinkStaffEntries.push(currentStaffEntry);
  235. currentStaffEntry.Link = staffEntryLink;
  236. const linkMusicTimestamp: Fraction = this.currentVoiceEntry.Timestamp.clone();
  237. const verticalSourceStaffEntryContainer: VerticalSourceStaffEntryContainer = currentMeasure.getVerticalContainerByTimestamp(linkMusicTimestamp);
  238. currentStaffEntry = verticalSourceStaffEntryContainer.StaffEntries[index];
  239. if (!currentStaffEntry) {
  240. currentStaffEntry = new SourceStaffEntry(verticalSourceStaffEntryContainer, currentStaff);
  241. verticalSourceStaffEntryContainer.StaffEntries[index] = currentStaffEntry;
  242. }
  243. currentStaffEntry.VoiceEntries.push(this.currentVoiceEntry);
  244. staffEntryLink.LinkStaffEntries.push(currentStaffEntry);
  245. currentStaffEntry.Link = staffEntryLink;
  246. return currentStaffEntry;
  247. }
  248. public checkForOpenBeam(): void {
  249. if (this.openBeams.length > 0 && this.currentNote) {
  250. this.handleOpenBeam();
  251. }
  252. }
  253. public checkOpenTies(): void {
  254. const openTieDict: { [key: number]: Tie } = this.openTieDict;
  255. for (const key in openTieDict) {
  256. if (openTieDict.hasOwnProperty(key)) {
  257. const tie: Tie = openTieDict[key];
  258. if (Fraction.plus(tie.StartNote.ParentStaffEntry.Timestamp, tie.Duration)
  259. .lt(tie.StartNote.SourceMeasure.Duration)) {
  260. delete openTieDict[key];
  261. }
  262. }
  263. }
  264. }
  265. public hasVoiceEntry(): boolean {
  266. return this.currentVoiceEntry !== undefined;
  267. }
  268. private readArticulations(notationNode: IXmlElement, currentVoiceEntry: VoiceEntry, currentNote: Note): void {
  269. const articNode: IXmlElement = notationNode.element("articulations");
  270. if (articNode) {
  271. this.articulationReader.addArticulationExpression(articNode, currentVoiceEntry);
  272. }
  273. const fermaNode: IXmlElement = notationNode.element("fermata");
  274. if (fermaNode) {
  275. this.articulationReader.addFermata(fermaNode, currentVoiceEntry);
  276. }
  277. const tecNode: IXmlElement = notationNode.element("technical");
  278. if (tecNode) {
  279. this.articulationReader.addTechnicalArticulations(tecNode, currentVoiceEntry, currentNote);
  280. }
  281. const ornaNode: IXmlElement = notationNode.element("ornaments");
  282. if (ornaNode) {
  283. this.articulationReader.addOrnament(ornaNode, currentVoiceEntry);
  284. // const tremoloNode: IXmlElement = ornaNode.element("tremolo");
  285. // tremolo should be and is added per note, not per VoiceEntry. see addSingleNote()
  286. }
  287. }
  288. /**
  289. * Create a new [[Note]] and adds it to the currentVoiceEntry
  290. * @param node
  291. * @param noteDuration
  292. * @param divisions
  293. * @param chord
  294. * @param guitarPro
  295. * @returns {Note}
  296. */
  297. private addSingleNote(node: IXmlElement, noteDuration: Fraction, noteTypeXml: NoteType, typeDuration: Fraction,
  298. normalNotes: number, chord: boolean, guitarPro: boolean,
  299. printObject: boolean, isCueNote: boolean, isGraceNote: boolean, stemDirectionXml: StemDirectionType, tremoloStrokes: number,
  300. stemColorXml: string, noteheadColorXml: string, vibratoStrokes: boolean): Note {
  301. //log.debug("addSingleNote called");
  302. let noteAlter: number = 0;
  303. let noteAccidental: AccidentalEnum = AccidentalEnum.NONE;
  304. let noteStep: NoteEnum = NoteEnum.C;
  305. let noteOctave: number = 0;
  306. let playbackInstrumentId: string = undefined;
  307. let noteheadShapeXml: string = undefined;
  308. let noteheadFilledXml: boolean = undefined; // if undefined, the final filled parameter will be calculated from duration
  309. const xmlnodeElementsArr: IXmlElement[] = node.elements();
  310. for (let idx: number = 0, len: number = xmlnodeElementsArr.length; idx < len; ++idx) {
  311. const noteElement: IXmlElement = xmlnodeElementsArr[idx];
  312. try {
  313. if (noteElement.name === "pitch") {
  314. const noteElementsArr: IXmlElement[] = noteElement.elements();
  315. for (let idx2: number = 0, len2: number = noteElementsArr.length; idx2 < len2; ++idx2) {
  316. const pitchElement: IXmlElement = noteElementsArr[idx2];
  317. noteheadShapeXml = undefined; // reinitialize for each pitch
  318. noteheadFilledXml = undefined;
  319. try {
  320. if (pitchElement.name === "step") {
  321. noteStep = NoteEnum[pitchElement.value];
  322. if (noteStep === undefined) { // don't replace undefined check
  323. const errorMsg: string = ITextTranslation.translateText(
  324. "ReaderErrorMessages/NotePitchError",
  325. "Invalid pitch while reading note."
  326. );
  327. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  328. throw new MusicSheetReadingException(errorMsg, undefined);
  329. }
  330. } else if (pitchElement.name === "alter") {
  331. noteAlter = parseFloat(pitchElement.value);
  332. if (isNaN(noteAlter)) {
  333. const errorMsg: string = ITextTranslation.translateText(
  334. "ReaderErrorMessages/NoteAlterationError", "Invalid alteration while reading note."
  335. );
  336. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  337. throw new MusicSheetReadingException(errorMsg, undefined);
  338. }
  339. noteAccidental = Pitch.AccidentalFromHalfTones(noteAlter); // potentially overwritten by "accidental" noteElement
  340. } else if (pitchElement.name === "octave") {
  341. noteOctave = parseInt(pitchElement.value, 10);
  342. if (isNaN(noteOctave)) {
  343. const errorMsg: string = ITextTranslation.translateText(
  344. "ReaderErrorMessages/NoteOctaveError", "Invalid octave value while reading note."
  345. );
  346. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  347. throw new MusicSheetReadingException(errorMsg, undefined);
  348. }
  349. }
  350. } catch (ex) {
  351. log.info("VoiceGenerator.addSingleNote read Step: ", ex.message);
  352. }
  353. }
  354. } else if (noteElement.name === "accidental") {
  355. const accidentalValue: string = noteElement.value;
  356. if (accidentalValue === "natural") {
  357. noteAccidental = AccidentalEnum.NATURAL;
  358. }
  359. } else if (noteElement.name === "unpitched") {
  360. const displayStep: IXmlElement = noteElement.element("display-step");
  361. if (displayStep) {
  362. noteStep = NoteEnum[displayStep.value.toUpperCase()];
  363. }
  364. const octave: IXmlElement = noteElement.element("display-octave");
  365. if (octave) {
  366. noteOctave = parseInt(octave.value, 10);
  367. if (guitarPro) {
  368. noteOctave += 1;
  369. }
  370. }
  371. } else if (noteElement.name === "instrument") {
  372. if (noteElement.firstAttribute) {
  373. playbackInstrumentId = noteElement.firstAttribute.value;
  374. }
  375. } else if (noteElement.name === "notehead") {
  376. noteheadShapeXml = noteElement.value;
  377. if (noteElement.attribute("filled") !== null) {
  378. noteheadFilledXml = noteElement.attribute("filled").value === "yes";
  379. }
  380. }
  381. } catch (ex) {
  382. log.info("VoiceGenerator.addSingleNote: ", ex);
  383. }
  384. }
  385. noteOctave -= Pitch.OctaveXmlDifference;
  386. const pitch: Pitch = new Pitch(noteStep, noteOctave, noteAccidental);
  387. const noteLength: Fraction = Fraction.createFromFraction(noteDuration);
  388. let note: Note = undefined;
  389. let stringNumber: number = -1;
  390. let fretNumber: number = -1;
  391. const bends: {bendalter: number, direction: string}[] = [];
  392. // check for guitar tabs:
  393. const notationNode: IXmlElement = node.element("notations");
  394. if (notationNode) {
  395. const technicalNode: IXmlElement = notationNode.element("technical");
  396. if (technicalNode) {
  397. const stringNode: IXmlElement = technicalNode.element("string");
  398. if (stringNode) {
  399. stringNumber = parseInt(stringNode.value, 10);
  400. }
  401. const fretNode: IXmlElement = technicalNode.element("fret");
  402. if (fretNode) {
  403. fretNumber = parseInt(fretNode.value, 10);
  404. }
  405. const bendElementsArr: IXmlElement[] = technicalNode.elements("bend");
  406. bendElementsArr.forEach(function (bend: IXmlElement): void {
  407. const bendalterNote: IXmlElement = bend.element("bend-alter");
  408. const releaseNode: IXmlElement = bend.element("release");
  409. if (releaseNode !== undefined) {
  410. bends.push({bendalter: parseInt (bendalterNote.value, 10), direction: "down"});
  411. } else {
  412. bends.push({bendalter: parseInt (bendalterNote.value, 10), direction: "up"});
  413. }
  414. });
  415. }
  416. }
  417. if (stringNumber < 0 || fretNumber < 0) {
  418. // create normal Note
  419. note = new Note(this.currentVoiceEntry, this.currentStaffEntry, noteLength, pitch, this.currentMeasure);
  420. } else {
  421. // create TabNote
  422. note = new TabNote(this.currentVoiceEntry, this.currentStaffEntry, noteLength, pitch, this.currentMeasure,
  423. stringNumber, fretNumber, bends, vibratoStrokes);
  424. }
  425. note.TypeLength = typeDuration;
  426. note.NoteTypeXml = noteTypeXml;
  427. note.NormalNotes = normalNotes;
  428. note.PrintObject = printObject;
  429. note.IsCueNote = isCueNote;
  430. note.IsGraceNote = isGraceNote;
  431. note.StemDirectionXml = stemDirectionXml; // maybe unnecessary, also in VoiceEntry
  432. note.TremoloStrokes = tremoloStrokes; // could be a Tremolo object in future if we have more data to manage like two-note tremolo
  433. if ((noteheadShapeXml !== undefined && noteheadShapeXml !== "normal") || noteheadFilledXml !== undefined) {
  434. note.Notehead = new Notehead(note, noteheadShapeXml, noteheadFilledXml);
  435. } // if normal, leave note head undefined to save processing/runtime
  436. note.NoteheadColorXml = noteheadColorXml; // color set in Xml, shouldn't be changed.
  437. note.NoteheadColor = noteheadColorXml; // color currently used
  438. note.PlaybackInstrumentId = playbackInstrumentId;
  439. this.currentVoiceEntry.Notes.push(note);
  440. this.currentVoiceEntry.StemDirectionXml = stemDirectionXml;
  441. if (stemColorXml) {
  442. this.currentVoiceEntry.StemColorXml = stemColorXml;
  443. this.currentVoiceEntry.StemColor = stemColorXml;
  444. note.StemColorXml = stemColorXml;
  445. }
  446. if (node.elements("beam") && !chord) {
  447. this.createBeam(node, note);
  448. }
  449. return note;
  450. }
  451. /**
  452. * Create a new rest note and add it to the currentVoiceEntry.
  453. * @param noteDuration
  454. * @param divisions
  455. * @returns {Note}
  456. */
  457. private addRestNote(node: IXmlElement, noteDuration: Fraction, noteTypeXml: NoteType,
  458. printObject: boolean, isCueNote: boolean, noteheadColorXml: string): Note {
  459. const restFraction: Fraction = Fraction.createFromFraction(noteDuration);
  460. const displayStep: IXmlElement = node.element("display-step");
  461. const octave: IXmlElement = node.element("display-octave");
  462. let pitch: Pitch = undefined;
  463. if (displayStep && octave) {
  464. const noteStep: NoteEnum = NoteEnum[displayStep.value.toUpperCase()];
  465. pitch = new Pitch(noteStep, parseInt(octave.value, 10), AccidentalEnum.NONE);
  466. }
  467. const restNote: Note = new Note(this.currentVoiceEntry, this.currentStaffEntry, restFraction, pitch, this.currentMeasure, true);
  468. restNote.NoteTypeXml = noteTypeXml;
  469. restNote.PrintObject = printObject;
  470. restNote.IsCueNote = isCueNote;
  471. restNote.NoteheadColorXml = noteheadColorXml;
  472. restNote.NoteheadColor = noteheadColorXml;
  473. this.currentVoiceEntry.Notes.push(restNote);
  474. if (this.openBeams.length > 0) {
  475. this.openBeams.last().ExtendedNoteList.push(restNote);
  476. }
  477. return restNote;
  478. }
  479. /**
  480. * Handle the currentVoiceBeam.
  481. * @param node
  482. * @param note
  483. */
  484. private createBeam(node: IXmlElement, note: Note): void {
  485. try {
  486. const beamNode: IXmlElement = node.element("beam");
  487. let beamAttr: IXmlAttribute = undefined;
  488. if (beamNode !== undefined && beamNode.hasAttributes) {
  489. beamAttr = beamNode.attribute("number");
  490. }
  491. if (beamAttr) {
  492. let beamNumber: number = parseInt(beamAttr.value, 10);
  493. const mainBeamNode: IXmlElement[] = node.elements("beam");
  494. const currentBeamTag: string = mainBeamNode[0].value;
  495. if (mainBeamNode) {
  496. if (currentBeamTag === "begin") {
  497. if (beamNumber === this.openBeams.last()?.BeamNumber) {
  498. // beam with same number already existed (error in XML), bump beam number
  499. this.beamNumberOffset++;
  500. beamNumber += this.beamNumberOffset;
  501. } else if (this.openBeams.last()) {
  502. this.handleOpenBeam();
  503. }
  504. this.openBeams.push(new Beam(beamNumber, this.beamNumberOffset));
  505. } else {
  506. beamNumber += this.beamNumberOffset;
  507. }
  508. }
  509. let sameVoiceEntry: boolean = false;
  510. if (!(beamNumber > 0 && beamNumber <= this.openBeams.length) || !this.openBeams[beamNumber - 1]) {
  511. console.log("invalid beamnumber"); // this shouldn't happen, probably error in this method
  512. return;
  513. }
  514. for (let idx: number = 0, len: number = this.openBeams[beamNumber - 1].Notes.length; idx < len; ++idx) {
  515. const beamNote: Note = this.openBeams[beamNumber - 1].Notes[idx];
  516. if (this.currentVoiceEntry === beamNote.ParentVoiceEntry) {
  517. sameVoiceEntry = true;
  518. }
  519. }
  520. if (!sameVoiceEntry) {
  521. const openBeam: Beam = this.openBeams[beamNumber - 1];
  522. openBeam.addNoteToBeam(note);
  523. // const lastBeamNote: Note = openBeam.Notes.last();
  524. // const graceStatusChanged: boolean = (lastBeamNote?.IsCueNote || lastBeamNote?.IsGraceNote) !== (note.IsCueNote) || (note.IsGraceNote);
  525. if (currentBeamTag === "end") {
  526. this.endBeam();
  527. }
  528. }
  529. }
  530. } catch (e) {
  531. const errorMsg: string = ITextTranslation.translateText(
  532. "ReaderErrorMessages/BeamError", "Error while reading beam."
  533. );
  534. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  535. throw new MusicSheetReadingException("", e);
  536. }
  537. }
  538. private endBeam(): void {
  539. this.openBeams.pop(); // pop the last open beam from the stack. the latest openBeam will be the one before that now
  540. this.beamNumberOffset = Math.max(0, this.beamNumberOffset - 1);
  541. }
  542. /**
  543. * Check for open [[Beam]]s at end of [[SourceMeasure]] and closes them explicity.
  544. */
  545. private handleOpenBeam(): void {
  546. const openBeam: Beam = this.openBeams.last();
  547. if (openBeam.Notes.length === 1) {
  548. const beamNote: Note = openBeam.Notes[0];
  549. beamNote.NoteBeam = undefined;
  550. this.endBeam();
  551. return;
  552. }
  553. if (this.currentNote === CollectionUtil.last(openBeam.Notes)) {
  554. this.endBeam();
  555. } else {
  556. const beamLastNote: Note = CollectionUtil.last(openBeam.Notes);
  557. const beamLastNoteStaffEntry: SourceStaffEntry = beamLastNote.ParentStaffEntry;
  558. const horizontalIndex: number = this.currentMeasure.getVerticalContainerIndexByTimestamp(beamLastNoteStaffEntry.Timestamp);
  559. const verticalIndex: number = beamLastNoteStaffEntry.VerticalContainerParent.StaffEntries.indexOf(beamLastNoteStaffEntry);
  560. if (horizontalIndex < this.currentMeasure.VerticalSourceStaffEntryContainers.length - 1) {
  561. const nextStaffEntry: SourceStaffEntry = this.currentMeasure
  562. .VerticalSourceStaffEntryContainers[horizontalIndex + 1]
  563. .StaffEntries[verticalIndex];
  564. if (nextStaffEntry) {
  565. for (let idx: number = 0, len: number = nextStaffEntry.VoiceEntries.length; idx < len; ++idx) {
  566. const voiceEntry: VoiceEntry = nextStaffEntry.VoiceEntries[idx];
  567. if (voiceEntry.ParentVoice === this.voice) {
  568. const candidateNote: Note = voiceEntry.Notes[0];
  569. if (candidateNote.Length.lte(new Fraction(1, 8))) {
  570. this.openBeams.last().addNoteToBeam(candidateNote);
  571. this.endBeam();
  572. } else {
  573. this.endBeam();
  574. }
  575. }
  576. }
  577. }
  578. } else {
  579. this.endBeam();
  580. }
  581. }
  582. }
  583. /**
  584. * Create a [[Tuplet]].
  585. * @param node
  586. * @param tupletNodeList
  587. * @returns {number}
  588. */
  589. private addTuplet(node: IXmlElement, tupletNodeList: IXmlElement[]): number {
  590. let bracketed: boolean = false; // xml bracket attribute value
  591. // TODO refactor this to not duplicate lots of code for the cases tupletNodeList.length == 1 and > 1
  592. if (tupletNodeList !== undefined && tupletNodeList.length > 1) {
  593. let timeModNode: IXmlElement = node.element("time-modification");
  594. if (timeModNode) {
  595. timeModNode = timeModNode.element("actual-notes");
  596. }
  597. const tupletNodeListArr: IXmlElement[] = tupletNodeList;
  598. for (let idx: number = 0, len: number = tupletNodeListArr.length; idx < len; ++idx) {
  599. const tupletNode: IXmlElement = tupletNodeListArr[idx];
  600. if (tupletNode !== undefined && tupletNode.attributes()) {
  601. const bracketAttr: Attr = tupletNode.attribute("bracket");
  602. if (bracketAttr && bracketAttr.value === "yes") {
  603. bracketed = true;
  604. }
  605. const placementAttr: Attr = tupletNode.attribute("placement");
  606. const placementBelow: boolean = placementAttr && placementAttr.value === "below";
  607. const type: Attr = tupletNode.attribute("type");
  608. if (type && type.value === "start") {
  609. let tupletNumber: number = 1;
  610. if (tupletNode.attribute("number")) {
  611. tupletNumber = parseInt(tupletNode.attribute("number").value, 10);
  612. }
  613. let tupletLabelNumber: number = 0;
  614. if (timeModNode) {
  615. tupletLabelNumber = parseInt(timeModNode.value, 10);
  616. if (isNaN(tupletLabelNumber)) {
  617. const errorMsg: string = ITextTranslation.translateText(
  618. "ReaderErrorMessages/TupletNoteDurationError", "Invalid tuplet note duration."
  619. );
  620. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  621. throw new MusicSheetReadingException(errorMsg, undefined);
  622. }
  623. }
  624. const tuplet: Tuplet = new Tuplet(tupletLabelNumber, bracketed);
  625. tuplet.tupletLabelNumberPlacement = placementBelow ? PlacementEnum.Below : PlacementEnum.Above;
  626. if (this.tupletDict[tupletNumber]) {
  627. delete this.tupletDict[tupletNumber];
  628. if (Object.keys(this.tupletDict).length === 0) {
  629. this.openTupletNumber = 0;
  630. } else if (Object.keys(this.tupletDict).length > 1) {
  631. this.openTupletNumber--;
  632. }
  633. }
  634. this.tupletDict[tupletNumber] = tuplet;
  635. const subnotelist: Note[] = [];
  636. subnotelist.push(this.currentNote);
  637. tuplet.Notes.push(subnotelist);
  638. tuplet.Fractions.push(this.getTupletNoteDurationFromType(node));
  639. this.currentNote.NoteTuplet = tuplet;
  640. this.openTupletNumber = tupletNumber;
  641. } else if (type.value === "stop") {
  642. let tupletNumber: number = 1;
  643. if (tupletNode.attribute("number")) {
  644. tupletNumber = parseInt(tupletNode.attribute("number").value, 10);
  645. }
  646. const tuplet: Tuplet = this.tupletDict[tupletNumber];
  647. if (tuplet) {
  648. const subnotelist: Note[] = [];
  649. subnotelist.push(this.currentNote);
  650. tuplet.Notes.push(subnotelist);
  651. tuplet.Fractions.push(this.getTupletNoteDurationFromType(node));
  652. this.currentNote.NoteTuplet = tuplet;
  653. delete this.tupletDict[tupletNumber];
  654. if (Object.keys(this.tupletDict).length === 0) {
  655. this.openTupletNumber = 0;
  656. } else if (Object.keys(this.tupletDict).length > 1) {
  657. this.openTupletNumber--;
  658. }
  659. }
  660. }
  661. }
  662. }
  663. } else if (tupletNodeList[0]) {
  664. const n: IXmlElement = tupletNodeList[0];
  665. if (n.hasAttributes) {
  666. const type: string = n.attribute("type").value;
  667. let tupletnumber: number = 1;
  668. if (n.attribute("number")) {
  669. tupletnumber = parseInt(n.attribute("number").value, 10);
  670. }
  671. const noTupletNumbering: boolean = isNaN(tupletnumber);
  672. const bracketAttr: Attr = n.attribute("bracket");
  673. if (bracketAttr && bracketAttr.value === "yes") {
  674. bracketed = true;
  675. }
  676. const placementAttr: Attr = n.attribute("placement");
  677. const placementBelow: boolean = placementAttr && placementAttr.value === "below";
  678. if (type === "start") {
  679. let tupletLabelNumber: number = 0;
  680. let timeModNode: IXmlElement = node.element("time-modification");
  681. if (timeModNode) {
  682. timeModNode = timeModNode.element("actual-notes");
  683. }
  684. if (timeModNode) {
  685. tupletLabelNumber = parseInt(timeModNode.value, 10);
  686. if (isNaN(tupletLabelNumber)) {
  687. const errorMsg: string = ITextTranslation.translateText(
  688. "ReaderErrorMessages/TupletNoteDurationError", "Invalid tuplet note duration."
  689. );
  690. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  691. throw new MusicSheetReadingException(errorMsg);
  692. }
  693. }
  694. if (noTupletNumbering) {
  695. this.openTupletNumber++;
  696. tupletnumber = this.openTupletNumber;
  697. }
  698. let tuplet: Tuplet = this.tupletDict[tupletnumber];
  699. if (!tuplet) {
  700. tuplet = this.tupletDict[tupletnumber] = new Tuplet(tupletLabelNumber, bracketed);
  701. tuplet.tupletLabelNumberPlacement = placementBelow ? PlacementEnum.Below : PlacementEnum.Above;
  702. }
  703. const subnotelist: Note[] = [];
  704. subnotelist.push(this.currentNote);
  705. tuplet.Notes.push(subnotelist);
  706. tuplet.Fractions.push(this.getTupletNoteDurationFromType(node));
  707. this.currentNote.NoteTuplet = tuplet;
  708. this.openTupletNumber = tupletnumber;
  709. } else if (type === "stop") {
  710. if (noTupletNumbering) {
  711. tupletnumber = this.openTupletNumber;
  712. }
  713. const tuplet: Tuplet = this.tupletDict[this.openTupletNumber];
  714. if (tuplet) {
  715. const subnotelist: Note[] = [];
  716. subnotelist.push(this.currentNote);
  717. tuplet.Notes.push(subnotelist);
  718. tuplet.Fractions.push(this.getTupletNoteDurationFromType(node));
  719. this.currentNote.NoteTuplet = tuplet;
  720. if (Object.keys(this.tupletDict).length === 0) {
  721. this.openTupletNumber = 0;
  722. } else if (Object.keys(this.tupletDict).length > 1) {
  723. this.openTupletNumber--;
  724. }
  725. delete this.tupletDict[tupletnumber];
  726. }
  727. }
  728. }
  729. }
  730. return this.openTupletNumber;
  731. }
  732. /**
  733. * This method handles the time-modification IXmlElement for the Tuplet case (tupletNotes not at begin/end of Tuplet).
  734. * @param noteNode
  735. */
  736. private handleTimeModificationNode(noteNode: IXmlElement): void {
  737. if (this.tupletDict[this.openTupletNumber]) {
  738. try {
  739. // Tuplet should already be created
  740. const tuplet: Tuplet = this.tupletDict[this.openTupletNumber];
  741. const notes: Note[] = CollectionUtil.last(tuplet.Notes);
  742. const lastTupletVoiceEntry: VoiceEntry = notes[0].ParentVoiceEntry;
  743. let noteList: Note[];
  744. if (lastTupletVoiceEntry.Timestamp.Equals(this.currentVoiceEntry.Timestamp)) {
  745. noteList = notes;
  746. } else {
  747. noteList = [];
  748. tuplet.Notes.push(noteList);
  749. tuplet.Fractions.push(this.getTupletNoteDurationFromType(noteNode));
  750. }
  751. noteList.push(this.currentNote);
  752. this.currentNote.NoteTuplet = tuplet;
  753. } catch (ex) {
  754. const errorMsg: string = ITextTranslation.translateText(
  755. "ReaderErrorMessages/TupletNumberError", "Invalid tuplet number."
  756. );
  757. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  758. throw ex;
  759. }
  760. } else if (this.currentVoiceEntry.Notes.length > 0) {
  761. const firstNote: Note = this.currentVoiceEntry.Notes[0];
  762. if (firstNote.NoteTuplet) {
  763. const tuplet: Tuplet = firstNote.NoteTuplet;
  764. const notes: Note[] = CollectionUtil.last(tuplet.Notes);
  765. notes.push(this.currentNote);
  766. this.currentNote.NoteTuplet = tuplet;
  767. }
  768. }
  769. }
  770. private addTie(tieNodeList: IXmlElement[], measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, tieType: TieTypes): void {
  771. if (tieNodeList) {
  772. if (tieNodeList.length === 1) {
  773. const tieNode: IXmlElement = tieNodeList[0];
  774. if (tieNode !== undefined && tieNode.attributes()) {
  775. const type: string = tieNode.attribute("type").value;
  776. try {
  777. if (type === "start") {
  778. const num: number = this.findCurrentNoteInTieDict(this.currentNote);
  779. if (num < 0) {
  780. delete this.openTieDict[num];
  781. }
  782. const newTieNumber: number = this.getNextAvailableNumberForTie();
  783. const tie: Tie = new Tie(this.currentNote, tieType);
  784. this.openTieDict[newTieNumber] = tie;
  785. } else if (type === "stop") {
  786. const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
  787. const tie: Tie = this.openTieDict[tieNumber];
  788. if (tie) {
  789. tie.AddNote(this.currentNote);
  790. delete this.openTieDict[tieNumber];
  791. }
  792. }
  793. } catch (err) {
  794. const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/TieError", "Error while reading tie.");
  795. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  796. }
  797. }
  798. } else if (tieNodeList.length === 2) {
  799. const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
  800. if (tieNumber >= 0) {
  801. const tie: Tie = this.openTieDict[tieNumber];
  802. tie.AddNote(this.currentNote);
  803. }
  804. }
  805. }
  806. }
  807. /**
  808. * Find the next free int (starting from 0) to use as key in TieDict.
  809. * @returns {number}
  810. */
  811. private getNextAvailableNumberForTie(): number {
  812. const keys: string[] = Object.keys(this.openTieDict);
  813. if (keys.length === 0) {
  814. return 1;
  815. }
  816. keys.sort((a, b) => (+a - +b)); // FIXME Andrea: test
  817. for (let i: number = 0; i < keys.length; i++) {
  818. if ("" + (i + 1) !== keys[i]) {
  819. return i + 1;
  820. }
  821. }
  822. return +(keys[keys.length - 1]) + 1;
  823. }
  824. /**
  825. * Search the tieDictionary for the corresponding candidateNote to the currentNote (same FundamentalNote && Octave).
  826. * @param candidateNote
  827. * @returns {number}
  828. */
  829. private findCurrentNoteInTieDict(candidateNote: Note): number {
  830. const openTieDict: { [_: number]: Tie; } = this.openTieDict;
  831. for (const key in openTieDict) {
  832. if (openTieDict.hasOwnProperty(key)) {
  833. const tie: Tie = openTieDict[key];
  834. const tieTabNote: TabNote = tie.Notes[0] as TabNote;
  835. const tieCandidateNote: TabNote = candidateNote as TabNote;
  836. if (tie.Pitch.FundamentalNote === candidateNote.Pitch.FundamentalNote && tie.Pitch.Octave === candidateNote.Pitch.Octave) {
  837. return parseInt(key, 10);
  838. } else if (tieTabNote.StringNumber !== undefined) {
  839. if (tieTabNote.StringNumber === tieCandidateNote.StringNumber) {
  840. return parseInt(key, 10);
  841. }
  842. }
  843. }
  844. }
  845. return -1;
  846. }
  847. /**
  848. * Calculate the normal duration of a [[Tuplet]] note.
  849. * @param xmlNode
  850. * @returns {any}
  851. */
  852. private getTupletNoteDurationFromType(xmlNode: IXmlElement): Fraction {
  853. if (xmlNode.element("type")) {
  854. const typeNode: IXmlElement = xmlNode.element("type");
  855. if (typeNode) {
  856. const type: string = typeNode.value;
  857. try {
  858. return NoteTypeHandler.getNoteDurationFromType(type);
  859. } catch (e) {
  860. const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/NoteDurationError", "Invalid note duration.");
  861. this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
  862. throw new MusicSheetReadingException("", e);
  863. }
  864. }
  865. }
  866. return undefined;
  867. }
  868. }