VoiceGenerator.ts 42 KB

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