VoiceGenerator.ts 50 KB

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