VoiceGenerator.ts 48 KB

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