UNPKG

smoosic

Version:

<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i

771 lines (754 loc) 28.7 kB
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. import {XmlDurationAlteration, XmlHelpers, XmlLyricData, XmlSlurType, XmlTieType, XmlTupletType} from './xmlHelpers'; import {SmoScore} from '../data/score'; import {SmoFormattingManager, SmoSystemGroup} from '../data/scoreModifiers'; import {SmoSystemStaff} from '../data/systemStaff'; import { SmoInstrument, SmoInstrumentParams, SmoSlur, SmoSlurParams, SmoStaffHairpin, SmoTie, TieLine } from '../data/staffModifiers'; import {SmoBarline, SmoMeasureModifierBase, SmoRehearsalMark, SmoTempoText} from '../data/measureModifiers'; import {SmoPartInfo} from '../data/partInfo'; import {SmoMeasure} from '../data/measure'; import {SmoNote} from '../data/note'; import {SmoDynamicText, SmoGraceNote, SmoLyric} from '../data/noteModifiers'; import {SmoTuplet, SmoTupletTree} from '../data/tuplet'; import {Clef} from '../data/common'; import {SmoMusic} from '../data/music'; import {SmoSelection, SmoSelector} from '../xform/selections'; /** * @category serialization */ export interface XmlClefInfo { clef: string, staffId: number } /** * @category serialization */ export interface XmlVoiceInfo { notes: SmoNote[], ticksUsed: number } /** * @category serialization */ export interface XmlStaffInfo { clefInfo: XmlClefInfo, measure: SmoMeasure | null, voices: Record<string | number, XmlVoiceInfo> } /** * @category serialization */ export interface XmlBeamGroupInfo { ticks: number, notes: number } /** * @category serialization */ export interface XmlSystemInfo { startSelector: SmoSelector, endSelector: SmoSelector, leftConnector: number } /** * @category serialization */ export interface XmlStaffGroupInfo { start: number, length: number } /** * Wedge is a hairpin/cresc. */ export interface XmlWedgeInfo { type: string } /** * @category serialization */ export interface XmlWedgeState { type: string, start: number } /** * @category serialization */ export interface XmlHairpinInfo { type: string, start: number, end: number } /** * @category serialization */ export interface XmlDynamicInfo { dynamic: string, offset: number } /** * @category serialization */ export interface XmlCompletedTies { startSelector: SmoSelector, endSelector: SmoSelector, fromPitch: number, toPitch: number } /** * @category serialization */ export interface XmlCompletedTuplet { tuplet: SmoTuplet, staffId: number, voiceId: number } /** * @category serialization */ export class XmlTupletStateTreeNode { tupletState: XmlTupletState; children: XmlTupletStateTreeNode[]; constructor(tupletState: XmlTupletState) { this.tupletState = tupletState; this.children = []; } } /** * @category serialization */ export interface XmlCompletedTupletState { tupletState: XmlTupletState, staffId: number, voiceId: number } /** * @category serialization */ export interface XmlTupletState { start: SmoSelector | null, end: SmoSelector | null, data: XmlTupletData | null, } /** * @category serialization */ export interface XmlTupletData { numNotes: number, notesOccupied: number, stemTicks: number, } /** * @category serialization */ export interface XmlEnding { start: number, end: number, number: number } /** * @category serialization */ export interface XmlPartGroup { partNum: number, group: SmoSystemGroup, parts: number[] } /** * Keep state of musical objects while parsing music xml * @category serialization * */ export class XmlState { static get defaults() { return { divisions: 4096, tempo: new SmoTempoText(SmoTempoText.defaults), timeSignature: '4/4', keySignature: 'c', clefInfo: [], staffGroups: [], smoStaves: [] }; } clefInfo: XmlClefInfo[] = []; systems: XmlSystemInfo[] = []; staffGroups: XmlStaffGroupInfo[] = []; smoStaves: SmoSystemStaff[] = []; slurs: Record<number, XmlSlurType | null> = {}; wedges: XmlWedgeState = {} as XmlWedgeState; hairpins: XmlHairpinInfo[] = []; instrument: SmoInstrumentParams = SmoInstrument.defaults; instrumentMap: Record<number, SmoInstrument> = {}; globalCursor = 0; staffVoiceHash: Record<string | number, number[]> = {}; endingMap: Record<number, XmlEnding[]> = {}; startRepeatMap: Record<number, number> = {}; endRepeatMap: Record<number, number> = {}; startBarline: number = SmoBarline.barlines.singleBar; endBarline: number = SmoBarline.barlines.singleBar; measureIndex = -1; completedSlurs: SmoSlurParams[] = []; completedTies: XmlTieType[] = []; verseMap: Record<number | string, number> = {}; measureNumber: number = 0; formattingManager = new SmoFormattingManager(SmoFormattingManager.defaults); completedTupletStates: XmlCompletedTupletState[] = []; tupletStatesInProgress: Record<number, XmlTupletState> = {}; tickCursor: number = 0; tempo: SmoTempoText = new SmoTempoText(SmoTempoText.defaults); staffArray: XmlStaffInfo[] = []; staffIndex: number = 0; graceNotes: SmoGraceNote[] = []; currentDuration: number = 0; beamGroups: Record<number, XmlBeamGroupInfo | null> = {}; dynamics: XmlDynamicInfo[] = []; previousNote: SmoNote = new SmoNote(SmoNote.defaults); completedTuplets: XmlCompletedTuplet[] = []; newTitle: boolean = false; divisions: number = 4096; keySignature: string = 'c'; timeSignature: string = '4/4'; voiceIndex: number = 0; pixelsPerTenth: number = 0.4; musicFontSize: number = 16; partId: string = ''; rehearsalMark = ''; rehearsalMarks: Record<number, string> = {}; parts: Record<string, SmoPartInfo> = {}; openPartGroup: XmlPartGroup | null = null; // Initialize things that persist throughout a staff // likc hairpins and slurs initializeForPart() { this.slurs = {}; this.wedges = {} as XmlWedgeState; this.hairpins = []; this.globalCursor = 0; this.staffVoiceHash = {}; this.measureIndex = -1; this.completedSlurs = []; this.verseMap = {}; this.instrument.keyOffset = 0; this.instrumentMap = {}; this.partId = ''; this.clefInfo = []; this.formattingManager = new SmoFormattingManager(SmoFormattingManager.defaults); } // ### initializeForMeasure // reset state for a new measure: beam groups, tuplets // etc. that don't cross measure boundaries initializeForMeasure(measureElement: Element) { const oldMeasure = this.measureNumber; this.measureNumber = parseInt(measureElement.getAttribute('number') as string, 10) - 1; if (isNaN(this.measureNumber)) { this.measureNumber = oldMeasure + 1; } this.tupletStatesInProgress = {}; this.tickCursor = 0; this.tempo = SmoMeasureModifierBase.deserialize(this.tempo.serialize()); this.tempo.display = false; this.staffArray = []; this.graceNotes = []; this.currentDuration = 0; this.beamGroups = {}; this.completedTuplets = []; this.dynamics = []; this.startBarline = SmoBarline.barlines.singleBar; this.endBarline = SmoBarline.barlines.singleBar; this.previousNote = new SmoNote(SmoNote.defaults); this.measureIndex += 1; this.rehearsalMark = ''; } // ### initializeStaff // voices are not sequential, seem to have artitrary numbers and // persist per part, so we treat them as a hash. // staff IDs persist per part but are sequential. initializeStaff(staffIndex: number, voiceIndex: number) { // If no clef is specified, default to treble if (typeof (this.staffArray[staffIndex]) === 'undefined') { this.staffArray.push({ clefInfo: { clef: 'treble', staffId: this.staffIndex }, measure: null, voices: {} }); } if (typeof (this.staffArray[staffIndex].voices[voiceIndex]) === 'undefined') { this.staffArray[staffIndex].voices[voiceIndex] = { notes: [], ticksUsed: 0 }; // keep track of 0-indexed voice for slurs and other modifiers if (!this.staffVoiceHash[staffIndex]) { this.staffVoiceHash[staffIndex] = []; } if (this.staffVoiceHash[staffIndex].indexOf(voiceIndex) < 0) { this.staffVoiceHash[staffIndex].push(voiceIndex); } // The smo 0-indexed voice index, used in selectors this.beamGroups[voiceIndex] = null; } } // ### updateStaffGroups // once everything is parsed, figure out how to group the staves updateStaffGroups() { this.systems = []; this.staffGroups.forEach((staffGroup) => { const len = this.smoStaves[staffGroup.start].measures.length; const startSelector = SmoSelector.default; startSelector.staff = staffGroup.start; startSelector.measure = 0; const endSelector = SmoSelector.default; endSelector.staff = staffGroup.start + (staffGroup.length - 1); endSelector.measure = len; const grpParams = SmoSystemGroup.defaults; grpParams.startSelector = startSelector; grpParams.endSelector = endSelector; grpParams.leftConnector = SmoSystemGroup.connectorTypes.brace; this.systems.push( new SmoSystemGroup(grpParams) ); }); } addLyric(note: SmoNote, lyricData: XmlLyricData) { if (typeof (this.verseMap[lyricData.verse]) === 'undefined') { const keys = Object.keys(this.verseMap); this.verseMap[lyricData.verse] = keys.length; } lyricData.verse = this.verseMap[lyricData.verse]; const params = SmoLyric.defaults; params.text = lyricData._text; params.verse = lyricData.verse; if (lyricData.syllabic === 'begin' || lyricData.syllabic === 'middle') { params.text += '-'; } const lyric = new SmoLyric(params); note.addLyric(lyric); } /** * process a wedge aka hairpin dynamic * @param wedgeInfo */ processWedge(wedgeInfo: XmlWedgeInfo) { if (wedgeInfo.type) { // If we already know about this wedge, it must have been // started, so complete it if (this.wedges.type) { this.hairpins.push({ type: this.wedges.type, start: this.wedges.start, end: this.tickCursor + this.globalCursor }); this.wedges = {} as XmlWedgeState; } else { this.wedges.type = wedgeInfo.type; this.wedges.start = this.tickCursor + this.globalCursor; } } } // ### backtrackHairpins // For the measure just parsed, find the correct tick for the // beginning and end of hairpins, if a hairpin stop directive // was received. These are not associated with a staff or voice, so // we use the first one in the measure element for both backtrackHairpins(smoStaff: SmoSystemStaff, staffId: number) { this.hairpins.forEach((hairpin) => { let hpMeasureIndex = this.measureIndex; let hpMeasure = smoStaff.measures[hpMeasureIndex]; let startTick = hpMeasure.voices[0].notes.length - 1; let hpTickCount = this.globalCursor; // All ticks read so far const endSelector = { staff: staffId - 1, measure: hpMeasureIndex, voice: 0, tick: -1, pitches: [] }; while (hpMeasureIndex >= 0 && hpTickCount > hairpin.start) { if (endSelector.tick < 0 && hpTickCount <= hairpin.end) { endSelector.tick = startTick; } hpTickCount -= hpMeasure.voices[0].notes[startTick].ticks.numerator; if (hpTickCount > hairpin.start) { startTick -= 1; if (startTick < 0) { hpMeasureIndex -= 1; hpMeasure = smoStaff.measures[hpMeasureIndex]; startTick = hpMeasure.voices[0].notes.length - 1; } } } const params = SmoStaffHairpin.defaults; params.startSelector = { staff: staffId - 1, measure: hpMeasureIndex, voice: 0, tick: startTick, pitches: [] }; params.endSelector = endSelector; params.hairpinType = hairpin.type === 'crescendo' ? SmoStaffHairpin.types.CRESCENDO : SmoStaffHairpin.types.DECRESCENDO; const smoHp = new SmoStaffHairpin(params); smoStaff.modifiers.push(smoHp); }); this.hairpins = []; } // ### updateDynamics // Based on note just parsed, put the dynamics on the closest // note, based on the offset of dynamic updateDynamics() { const smoNote = this.previousNote; const tickCursor = this.tickCursor; const newArray: XmlDynamicInfo[] = []; this.dynamics.forEach((dynamic) => { if (tickCursor >= dynamic.offset) { const modParams = SmoDynamicText.defaults; modParams.text = dynamic.dynamic; // TODO: change the smonote name of this interface smoNote.addDynamic(new SmoDynamicText(modParams)); } else { newArray.push(dynamic); } }); this.dynamics = newArray; } // For the given voice, beam the notes according to the // note beam length backtrackBeamGroup(voice: XmlVoiceInfo, beamGroup: XmlBeamGroupInfo) { let i = 0; for (i = 0; i < beamGroup.notes; ++i) { const note = voice.notes[voice.notes.length - (i + 1)]; if (!note) { console.warn('no note for beam group'); return; } if (i === 0) { note.beamState = SmoNote.beamStates.end } else { note.beamState = SmoNote.beamStates.auto; } note.beamBeats = beamGroup.ticks; } } // ### updateBeamState // Keep track of beam instructions found while parsing note element // includes time alteration from tuplets updateBeamState(beamState: number, alteration: XmlDurationAlteration, voice: XmlVoiceInfo, voiceIndex: number) { const note = voice.notes[voice.notes.length - 1]; if (beamState === XmlHelpers.beamStates.BEGIN) { this.beamGroups[voiceIndex] = { ticks: (note.tickCount * alteration.noteCount) / alteration.noteDuration, notes: 1 }; } else if (this.beamGroups[voiceIndex]) { (this.beamGroups[voiceIndex] as XmlBeamGroupInfo).ticks += note.tickCount; (this.beamGroups[voiceIndex] as XmlBeamGroupInfo).notes += 1; if (beamState === XmlHelpers.beamStates.END) { this.backtrackBeamGroup(voice, this.beamGroups[voiceIndex] as XmlBeamGroupInfo); this.beamGroups[voiceIndex] = null; } } } updateTieStates(tieInfos: XmlTieType[]) { tieInfos.forEach((tieInfo) => { // tieInfo = { number, type, orientation, selector, pitchIndex } if (tieInfo.type === 'start') { this.completedTies.push(tieInfo); } }); } updateEndings(barlineNode: Element) { const findStartEnding = (endingNumber: number, ix: number): XmlEnding | null | undefined => { const endingIx = Object.keys(this.endingMap).map((xx) => parseInt(xx, 10)); let gt = -1; let rv: XmlEnding | null = null; endingIx.forEach((ee) => { if (ee > gt && ee <= ix) { const endings = this.endingMap[ee]; const txt = endings.find((xx: XmlEnding) => xx.number === endingNumber); if (txt) { gt = ee; rv = txt; } if (endings.findIndex((xx: XmlEnding) => xx.number === endingNumber) >= 0) { gt = ee; } } }); if (gt >= 0) { return rv; } else { return null; } }; const ending = XmlHelpers.getEnding(barlineNode); if (ending) { if (ending.type === 'start') { const numbers = ending.numbers; numbers.forEach((nn) => { const endings: XmlEnding[] | undefined = this.endingMap[this.measureIndex]; if (!endings) { this.endingMap[this.measureIndex] = []; } const inst = this.endingMap[this.measureIndex].find((ee) => ee.number === nn); if (!inst) { this.endingMap[this.measureIndex].push({ start: this.measureIndex, end: -1, number: nn }); } }); } else { ending.numbers.forEach((nn) => { const inst = findStartEnding(nn, this.measureIndex); if (!inst) { console.warn('bad ending ' + nn + ' at ' + this.measureIndex); } else { inst.end = this.measureIndex; } }); } } const barline = XmlHelpers.getBarline(barlineNode); if (barline === SmoBarline.barlines.startRepeat) { this.startBarline = barline; } else { this.endBarline = barline; } } /** * While parsing a measure, * on a slur element, either complete a started * slur or start a new one. * @param slurInfos */ updateSlurStates(slurInfos: XmlSlurType[]) { const clef: Clef = this.staffArray[this.staffIndex].clefInfo.clef as Clef; const note = this.previousNote; const getForcedSlurDirection = (smoParams: SmoSlurParams, xmlStart: XmlSlurType, xmlEnd: XmlSlurType | null) => { // If the slur direction is specified, otherwise use autor. if (xmlStart.placement === 'above' || xmlEnd?.placement === 'above') { smoParams.position_end = SmoSlur.positions.ABOVE; smoParams.position = SmoSlur.positions.ABOVE; if (xmlStart.orientation === 'over') { smoParams.orientation = SmoSlur.orientations.DOWN; } else if (xmlStart.orientation === 'under') { smoParams.orientation = SmoSlur.orientations.UP; } } else if (xmlStart.placement === 'below' || xmlEnd?.placement === 'below') { smoParams.position_end = SmoSlur.positions.BELOW; smoParams.position = SmoSlur.positions.BELOW; if (xmlStart.orientation === 'over') { smoParams.orientation = SmoSlur.orientations.DOWN; } else if (xmlStart.orientation === 'under') { smoParams.orientation = SmoSlur.orientations.UP; } } }; slurInfos.forEach((slurInfo) => { // slurInfo = { number, type, selector } if (slurInfo.type === 'start') { const slurParams = SmoSlur.defaults; // if start and stop come out of order if (this.slurs[slurInfo.number] && (this.slurs[slurInfo.number] as XmlSlurType).type === 'stop') { slurParams.endSelector = JSON.parse(JSON.stringify((this.slurs[slurInfo.number] as XmlSlurType).selector)); slurParams.startSelector = slurInfo.selector; slurParams.cp1x = slurInfo.controlX; slurParams.cp1y = slurInfo.controlY; const slurType = this.slurs[slurInfo.number]; getForcedSlurDirection(slurParams, slurInfo, slurType); this.completedSlurs.push(slurParams); this.slurs[slurInfo.number] = null; } else { // We no longer try to pick the slur direction until the score is complete. this.slurs[slurInfo.number] = JSON.parse(JSON.stringify(slurInfo)); } } else if (slurInfo.type === 'stop') { if (this.slurs[slurInfo.number] && (this.slurs[slurInfo.number] as XmlSlurType).type === 'start') { const slurData = this.slurs[slurInfo.number] as XmlSlurType; const slurParams = SmoSlur.defaults; slurParams.startSelector = JSON.parse(JSON.stringify((this.slurs[slurInfo.number] as XmlSlurType).selector)); slurParams.endSelector = slurInfo.selector; slurParams.cp2x = slurInfo.controlX; slurParams.cp2y = slurInfo.controlY; slurParams.yOffset = slurData.yOffset; const slurType = this.slurs[slurInfo.number]; getForcedSlurDirection(slurParams, slurInfo, slurType); // console.log('complete slur ' + slurInfo.number + JSON.stringify(slurParams, null, ' ')); this.completedSlurs.push(slurParams); this.slurs[slurInfo.number] = null; } else { this.slurs[slurInfo.number] = JSON.parse(JSON.stringify(slurInfo)); } } }); } assignRehearsalMarks() { Object.keys(this.rehearsalMarks).forEach((rm) => { const measureIx = parseInt(rm, 10); this.smoStaves.forEach((staff) => { const mark = new SmoRehearsalMark(SmoRehearsalMark.defaults); staff.addRehearsalMark(measureIx, mark); }); }); } /** * After reading in a measure, update any completed slurs and make them * into SmoSlur and add them to the SmoSystemGroup objects. * staffIndexOffset is the offset from the xml staffId and the score staff Id * (i.e. the staves that have already been parsed in other parts) */ completeSlurs() { this.completedSlurs.forEach((slur) => { const smoSlur = new SmoSlur(slur); this.smoStaves[slur.startSelector.staff].addStaffModifier(smoSlur); }); } /** * Go through saved start ties, try to find the endpoint of the tie. Ties in music xml * are a little ambiguous, we assume we are tying to the same pitch * @param score */ completeTies(score: SmoScore) { this.completedTies.forEach((tieInfo) => { const startSelection: SmoSelection | null = SmoSelection.noteFromSelector(score, tieInfo.selector); if (startSelection && startSelection.note) { const startNote = startSelection.note; const endSelection = SmoSelection.nextNoteSelectionFromSelector(score, startSelection.selector); const endNote = endSelection?.note; const pitches: TieLine[] = []; if (endSelection && endNote) { startNote.pitches.forEach((spitch, ix) => { endNote.pitches.forEach((epitch, jx) => { if (SmoMusic.smoPitchToInt(spitch) === SmoMusic.smoPitchToInt(epitch)) { pitches.push({ from: ix, to: jx }); } }); }); } if (pitches.length && endSelection) { const params = SmoTie.defaults; params.startSelector = startSelection.selector; params.endSelector = endSelection.selector; params.lines = pitches; const smoTie = new SmoTie(params); score.staves[smoTie.startSelector.staff].addStaffModifier(smoTie); } } }); } // ### updateTupletStates // react to a tuplet start or stop directive // we need to handle start and stop directives that appear out of order updateTupletStates(tupletInfos: XmlTupletType[], voice: XmlVoiceInfo, staffIndex: number, voiceIndex: number) { // this.tickCursor; const tick = voice.notes.length - 1; tupletInfos.forEach((tupletInfo) => { let tupletState: XmlTupletState | undefined = this.tupletStatesInProgress[tupletInfo.number]; if (tupletState == undefined) { tupletState = { start: null, end: null, data: null, }; this.tupletStatesInProgress[tupletInfo.number] = tupletState; } if (tupletInfo.type === 'start') { tupletState.start = { staff: staffIndex, measure: this.measureNumber, voice: voiceIndex, tick, pitches: [] }; tupletState.data = tupletInfo.data; } else if (tupletInfo.type === 'stop') { tupletState.end = { staff: staffIndex, measure: this.measureNumber, voice: voiceIndex, tick, pitches: [] }; } if (tupletState.start != null && tupletState.end != null) { this.completedTupletStates.push({ tupletState: tupletState, staffId: staffIndex, voiceId: voiceIndex }); delete this.tupletStatesInProgress[tupletInfo.number]; } }); } addTupletsToMeasure(smoMeasure: SmoMeasure, staffId: number, voiceId: number) { const tupletStates = this.findAndRemoveCompletedTupletStatesByStaffAndVoice(staffId, voiceId); const xmlTupletStateTrees = this.buildXmlTupletStateTrees(tupletStates); const notes: SmoNote[] = smoMeasure.voices[voiceId].notes; smoMeasure.tupletTrees = this.buildSmoTupletTreesFromXmlTupletStateTrees(xmlTupletStateTrees, notes); } private findAndRemoveCompletedTupletStatesByStaffAndVoice(staffId: number, voiceId: number): XmlTupletState[] { const remainingXmlTupletStates: XmlCompletedTupletState[] = []; const tupletStatesForReturn: XmlTupletState[] = []; this.completedTupletStates.forEach((completedTupletState) => { if (completedTupletState.staffId === staffId && completedTupletState.voiceId === voiceId) { tupletStatesForReturn.push(completedTupletState.tupletState); } else { remainingXmlTupletStates.push(completedTupletState) } }); this.completedTupletStates = remainingXmlTupletStates; return tupletStatesForReturn; } private buildXmlTupletStateTrees(tupletStates: XmlTupletState[]): XmlTupletStateTreeNode[] { let sortedTupletStates = this.sortTupletStates(tupletStates); let roots: XmlTupletStateTreeNode[] = []; let activeNodes: XmlTupletStateTreeNode[] = []; for (let tupletState of sortedTupletStates) { let node = new XmlTupletStateTreeNode(tupletState); let placed = false; while (activeNodes.length > 0) { let lastNode = activeNodes[activeNodes.length - 1]; if (tupletState.start!.tick <= lastNode.tupletState.end!.tick) { lastNode.children.push(node); placed = true; break; } else { activeNodes.pop(); } } if (!placed) { roots.push(node); } activeNodes.push(node); } return roots; } private sortTupletStates(tupletStates: XmlTupletState[]): XmlTupletState[] { return tupletStates.sort((a, b) => { if (a.start === b.start) { return a.end!.tick - b.end!.tick; } return a.start!.tick - b.start!.tick; }); } /** * Create SmoTuplets out of completedTupletStates */ buildSmoTupletTreesFromXmlTupletStateTrees(xmlTupletStateTrees: XmlTupletStateTreeNode[], notes: SmoNote[]): SmoTupletTree[] { const smoTupletTrees: SmoTupletTree[] = []; const traverseXmlTupletStateTree = (xmlTupletStateTreeNode: XmlTupletStateTreeNode): SmoTuplet => { const smoTupletParams = SmoTuplet.defaults; const xmlTupletState = xmlTupletStateTreeNode.tupletState; if (xmlTupletState.data) { smoTupletParams.numNotes = xmlTupletState.data.numNotes; smoTupletParams.notesOccupied = xmlTupletState.data.notesOccupied; smoTupletParams.stemTicks = xmlTupletState.data.stemTicks; } smoTupletParams.startIndex = xmlTupletState.start!.tick; smoTupletParams.endIndex = xmlTupletState.end!.tick; for (let i = smoTupletParams.startIndex; i <= smoTupletParams.endIndex; i++) { smoTupletParams.totalTicks += Math.floor(notes[i].tickCount); } // Normalize to an allowed note length, because MusicXML durations do not add up smoTupletParams.totalTicks = SmoMusic.closestSimpleDurationFromTicks(smoTupletParams.totalTicks); smoTupletParams.voice = xmlTupletState.start!.voice; const smoTuplet = new SmoTuplet(smoTupletParams); for (let i = 0; i < xmlTupletStateTreeNode.children.length; i++) { const childSmoTuplet = traverseXmlTupletStateTree(xmlTupletStateTreeNode.children[i]); childSmoTuplet.parentTuplet = {id: smoTuplet.attrs.id}; smoTuplet.childrenTuplets.push(childSmoTuplet); } return smoTuplet; }; for (let i = 0; i < xmlTupletStateTrees.length; i++) { const xmlTupletStateTreeNode = xmlTupletStateTrees[i]; const tuplet: SmoTuplet = traverseXmlTupletStateTree(xmlTupletStateTreeNode); smoTupletTrees.push(new SmoTupletTree({tuplet: tuplet})); } return smoTupletTrees; } getSystems(): SmoSystemGroup[] { const rv: SmoSystemGroup[] = []; this.systems.forEach((system: { startSelector: SmoSelector; endSelector: SmoSelector; leftConnector: number; }) => { const params = SmoSystemGroup.defaults; params.startSelector = system.startSelector; params.endSelector = system.endSelector; params.leftConnector = system.leftConnector; rv.push(new SmoSystemGroup(params)); }); return rv; } }