opensheetmusicdisplay/opensheetmusicdisplay

View on GitHub
src/MusicalScore/ScoreIO/MusicSheetReader.ts

Summary

Maintainability
F
2 wks
Test Coverage
import {MusicSheet} from "../MusicSheet";
import {SourceMeasure} from "../VoiceData/SourceMeasure";
import {Fraction} from "../../Common/DataObjects/Fraction";
import {InstrumentReader} from "./InstrumentReader";
import {IXmlElement} from "../../Common/FileIO/Xml";
import {Instrument} from "../Instrument";
import {ITextTranslation} from "../Interfaces/ITextTranslation";
import {MusicSheetReadingException} from "../Exceptions";
import log from "loglevel";
import {IXmlAttribute} from "../../Common/FileIO/Xml";
import {RhythmInstruction} from "../VoiceData/Instructions/RhythmInstruction";
import {RhythmSymbolEnum} from "../VoiceData/Instructions/RhythmInstruction";
import {SourceStaffEntry} from "../VoiceData/SourceStaffEntry";
import {VoiceEntry} from "../VoiceData/VoiceEntry";
import {InstrumentalGroup} from "../InstrumentalGroup";
import {SubInstrument} from "../SubInstrument";
import {MidiInstrument} from "../VoiceData/Instructions/ClefInstruction";
import {AbstractNotationInstruction} from "../VoiceData/Instructions/AbstractNotationInstruction";
import {Label} from "../Label";
import {MusicSymbolModuleFactory} from "./MusicSymbolModuleFactory";
import {IAfterSheetReadingModule} from "../Interfaces/IAfterSheetReadingModule";
import {RepetitionInstructionReader} from "./MusicSymbolModules/RepetitionInstructionReader";
import {RepetitionCalculator} from "./MusicSymbolModules/RepetitionCalculator";
import {EngravingRules} from "../Graphical/EngravingRules";
import { ReaderPluginManager } from "./ReaderPluginManager";
import { TextAlignmentEnum } from "../../Common/Enums/TextAlignment";

export class MusicSheetReader /*implements IMusicSheetReader*/ {

    constructor(afterSheetReadingModules: IAfterSheetReadingModule[] = undefined, rules: EngravingRules = new EngravingRules()) {
     if (!afterSheetReadingModules) {
       this.afterSheetReadingModules = [];
     } else {
       this.afterSheetReadingModules = afterSheetReadingModules;
     }
     this.repetitionInstructionReader = MusicSymbolModuleFactory.createRepetitionInstructionReader();
     this.repetitionCalculator = MusicSymbolModuleFactory.createRepetitionCalculator();
     this.rules = rules;
    }

    private repetitionInstructionReader: RepetitionInstructionReader;
    private repetitionCalculator: RepetitionCalculator;
    private afterSheetReadingModules: IAfterSheetReadingModule[];
    private musicSheet: MusicSheet;
    private completeNumberOfStaves: number = 0;
    private currentMeasure: SourceMeasure;
    private previousMeasure: SourceMeasure;
    private currentFraction: Fraction;
    private pluginManager: ReaderPluginManager = new ReaderPluginManager();
    public rules: EngravingRules;

    public get PluginManager(): ReaderPluginManager {
        return this.pluginManager;
    }

    public get CompleteNumberOfStaves(): number {
        return this.completeNumberOfStaves;
    }

    private static doCalculationsAfterDurationHasBeenSet(instrumentReaders: InstrumentReader[]): void {
        for (const instrumentReader of instrumentReaders) {
            instrumentReader.doCalculationsAfterDurationHasBeenSet();
        }
    }

    /**
     * Read a music XML file and saves the values in the MusicSheet class.
     * @param root
     * @param path
     * @returns {MusicSheet}
     */
    public createMusicSheet(root: IXmlElement, path: string): MusicSheet {
        try {
            return this._createMusicSheet(root, path);
        } catch (e) {
            log.error("MusicSheetReader.CreateMusicSheet", e);
            return undefined;
        }
    }

    private _removeFromArray(list: any[], elem: any): void {
        const i: number = list.indexOf(elem);
        if (i !== -1) {
            list.splice(i, 1);
        }
    }

    // Trim from a string also newlines
    private trimString(str: string): string {
        return str.replace(/^\s+|\s+$/g, "");
    }

    private _lastElement<T>(list: T[]): T {
        return list[list.length - 1];
    }

    //public SetPhonicScoreInterface(phonicScoreInterface: IPhonicScoreInterface): void {
    //  this.phonicScoreInterface = phonicScoreInterface;
    //}
    //public ReadMusicSheetParameters(sheetObject: MusicSheetParameterObject, root: IXmlElement, path: string): MusicSheetParameterObject {
    //  this.musicSheet = new MusicSheet();
    //  if (root) {
    //    this.pushSheetLabels(root, path);
    //    if (this.musicSheet.Title) {
    //      sheetObject.Title = this.musicSheet.Title.text;
    //    }
    //    if (this.musicSheet.Composer) {
    //      sheetObject.Composer = this.musicSheet.Composer.text;
    //    }
    //    if (this.musicSheet.Lyricist) {
    //      sheetObject.Lyricist = this.musicSheet.Lyricist.text;
    //    }
    //    let partlistNode: IXmlElement = root.element("part-list");
    //    let partList: IXmlElement[] = partlistNode.elements();
    //    this.createInstrumentGroups(partList);
    //    for (let idx: number = 0, len: number = this.musicSheet.Instruments.length; idx < len; ++idx) {
    //      let instr: Instrument = this.musicSheet.Instruments[idx];
    //      sheetObject.InstrumentList.push(__init(new MusicSheetParameterObject.LibrarySheetInstrument(), { name: instr.name }));
    //    }
    //  }
    //  return sheetObject;
    //}

    private _createMusicSheet(root: IXmlElement, path: string): MusicSheet {
        const instrumentReaders: InstrumentReader[] = [];
        let sourceMeasureCounter: number = 0;
        this.musicSheet = new MusicSheet();
        this.musicSheet.Path = path;
        this.musicSheet.Rules = this.rules;
        const globalWidthAttr: IXmlAttribute = root.attribute("osmdMeasureWidthFactor");
        // custom xml attribute, similar to osmdWidthFactor for individual measures
        if (globalWidthAttr) {
            const globalWidthValue: number = Number.parseFloat(globalWidthAttr.value);
            if (typeof globalWidthValue === "number" && !isNaN(globalWidthValue)) {
                this.musicSheet.MeasureWidthFactor = globalWidthValue;
            } else {
                log.info("xml parse: osmdMeasureWidthFactor invalid");
            }
        }
        if (!root) {
            throw new MusicSheetReadingException("Undefined root element");
        }
        this.pushSheetLabels(root, path);
        const partlistNode: IXmlElement = root.element("part-list");
        if (!partlistNode) {
            throw new MusicSheetReadingException("Undefined partListNode");
        }

        const partInst: IXmlElement[] = root.elements("part");
        const partList: IXmlElement[] = partlistNode.elements();
        this.initializeReading(partList, partInst, instrumentReaders);
        let couldReadMeasure: boolean = true;
        this.currentFraction = new Fraction(0, 1);
        let octavePlusOneEncoding: boolean = false; // GuitarPro and Sibelius give octaves -1 apparently
        let encoding: IXmlElement = root.element("identification");
        if (encoding) {
            encoding = encoding.element("encoding");
        }
        if (encoding) {
            encoding = encoding.element("software");
        }
        if (encoding !== undefined && (encoding.value === "Guitar Pro 5")) { //|| encoding.value.startsWith("Sibelius")
            octavePlusOneEncoding = true;
        }

        while (couldReadMeasure) {
            // TODO changing this.rules.PartAndSystemAfterFinalBarline requires a reload of the piece for measure numbers to be updated
            if (this.currentMeasure !== undefined && this.currentMeasure.HasEndLine && this.rules.NewPartAndSystemAfterFinalBarline) {
                sourceMeasureCounter = 0;
            }
            this.currentMeasure = new SourceMeasure(this.completeNumberOfStaves, this.musicSheet.Rules);
            for (const instrumentReader of instrumentReaders) {
                try {
                    couldReadMeasure = couldReadMeasure && instrumentReader.readNextXmlMeasure(
                        this.currentMeasure, this.currentFraction, octavePlusOneEncoding);
                } catch (e) {
                    const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/InstrumentError", "Error while reading instruments.");
                    throw new MusicSheetReadingException(errorMsg, e);
                }

            }
            if (couldReadMeasure) {
                this.musicSheet.addMeasure(this.currentMeasure);
                this.checkIfRhythmInstructionsAreSetAndEqual(instrumentReaders);
                this.checkSourceMeasureForNullEntries();
                sourceMeasureCounter = this.setSourceMeasureDuration(instrumentReaders, sourceMeasureCounter);
                MusicSheetReader.doCalculationsAfterDurationHasBeenSet(instrumentReaders);
                this.currentMeasure.AbsoluteTimestamp = this.currentFraction.clone();
                this.musicSheet.SheetErrors.finalizeMeasure(this.currentMeasure.MeasureNumber);
                this.currentFraction.Add(this.currentMeasure.Duration);
                this.previousMeasure = this.currentMeasure;
            }
        }

        if (this.repetitionInstructionReader) {
            this.repetitionInstructionReader.removeRedundantInstructions();
            if (this.repetitionCalculator) {
                this.repetitionCalculator.calculateRepetitions(this.musicSheet, this.repetitionInstructionReader.repetitionInstructions);
            }
        }
        this.musicSheet.checkForInstrumentWithNoVoice();
        this.musicSheet.fillStaffList();
        //this.musicSheet.DefaultStartTempoInBpm = this.musicSheet.SheetPlaybackSetting.BeatsPerMinute;
        for (let idx: number = 0, len: number = this.afterSheetReadingModules.length; idx < len; ++idx) {
         const afterSheetReadingModule: IAfterSheetReadingModule = this.afterSheetReadingModules[idx];
         afterSheetReadingModule.calculate(this.musicSheet);
        }

        //this.musicSheet.DefaultStartTempoInBpm = this.musicSheet.SourceMeasures[0].TempoInBPM;
        this.musicSheet.userStartTempoInBPM = this.musicSheet.userStartTempoInBPM || this.musicSheet.DefaultStartTempoInBpm;

        return this.musicSheet;
    }

    private initializeReading(partList: IXmlElement[], partInst: IXmlElement[], instrumentReaders: InstrumentReader[]): void {
        const instrumentDict: { [_: string]: Instrument } = this.createInstrumentGroups(partList);
        this.completeNumberOfStaves = this.getCompleteNumberOfStavesFromXml(partInst);
        if (partInst.length !== 0) {
            this.repetitionInstructionReader.MusicSheet = this.musicSheet;
            this.currentFraction = new Fraction(0, 1);
            this.currentMeasure = undefined;
            this.previousMeasure = undefined;
        }
        let counter: number = 0;
        for (const node of partInst) {
            const idNode: IXmlAttribute = node.attribute("id");
            if (idNode) {
                const currentInstrument: Instrument = instrumentDict[idNode.value];
                const xmlMeasureList: IXmlElement[] = node.elements("measure");
                let instrumentNumberOfStaves: number = 1;
                try {
                    instrumentNumberOfStaves = this.getInstrumentNumberOfStavesFromXml(node);
                } catch (err) {
                    const errorMsg: string = ITextTranslation.translateText(
                        "ReaderErrorMessages/InstrumentStavesNumberError",
                        "Invalid number of staves at instrument: "
                    );
                    this.musicSheet.SheetErrors.push(errorMsg + currentInstrument.Name);
                    continue;
                }

                currentInstrument.createStaves(instrumentNumberOfStaves);
                instrumentReaders.push(new InstrumentReader(this.pluginManager, this.repetitionInstructionReader, xmlMeasureList, currentInstrument));
                if (this.repetitionInstructionReader) {
                    this.repetitionInstructionReader.xmlMeasureList[counter] = xmlMeasureList;
                }
                counter++;
            }
        }
    }

    /**
     * Check if all (should there be any apart from the first Measure) [[RhythmInstruction]]s in the [[SourceMeasure]] are the same.
     *
     * If not, then the max [[RhythmInstruction]] (Fraction) is set to all staves.
     * Also, if it happens to have the same [[RhythmInstruction]]s in RealValue but given in Symbol AND Fraction, then the Fraction prevails.
     * @param instrumentReaders
     */
    private checkIfRhythmInstructionsAreSetAndEqual(instrumentReaders: InstrumentReader[]): void {
        const rhythmInstructions: RhythmInstruction[] = [];
        for (let i: number = 0; i < this.completeNumberOfStaves; i++) {
            if (this.currentMeasure.FirstInstructionsStaffEntries[i]) {
                const last: AbstractNotationInstruction = this.currentMeasure.FirstInstructionsStaffEntries[i].Instructions[
                this.currentMeasure.FirstInstructionsStaffEntries[i].Instructions.length - 1
                    ];
                if (last instanceof RhythmInstruction) {
                    rhythmInstructions.push(<RhythmInstruction>last);
                }
            }
        }
        let maxRhythmValue: number = 0.0;
        let index: number = -1;
        for (let idx: number = 0, len: number = rhythmInstructions.length; idx < len; ++idx) {
            const rhythmInstruction: RhythmInstruction = rhythmInstructions[idx];
            if (rhythmInstruction.Rhythm.RealValue > maxRhythmValue) {
                if (this.areRhythmInstructionsMixed(rhythmInstructions) && rhythmInstruction.SymbolEnum !== RhythmSymbolEnum.NONE) {
                    continue;
                }
                maxRhythmValue = rhythmInstruction.Rhythm.RealValue;
                index = rhythmInstructions.indexOf(rhythmInstruction);
            }
        }
        if (rhythmInstructions.length > 0 && rhythmInstructions.length < this.completeNumberOfStaves) {
            const rhythmInstruction: RhythmInstruction = rhythmInstructions[index].clone();
            for (let i: number = 0; i < this.completeNumberOfStaves; i++) {
                if (
                    this.currentMeasure.FirstInstructionsStaffEntries[i] !== undefined &&
                    !(this._lastElement(this.currentMeasure.FirstInstructionsStaffEntries[i].Instructions) instanceof RhythmInstruction)
                ) {
                    this.currentMeasure.FirstInstructionsStaffEntries[i].removeAllInstructionsOfTypeRhythmInstruction();
                    this.currentMeasure.FirstInstructionsStaffEntries[i].Instructions.push(rhythmInstruction.clone());
                }
                if (!this.currentMeasure.FirstInstructionsStaffEntries[i]) {
                    this.currentMeasure.FirstInstructionsStaffEntries[i] = new SourceStaffEntry(undefined, undefined);
                    this.currentMeasure.FirstInstructionsStaffEntries[i].Instructions.push(rhythmInstruction.clone());
                }
            }
            for (let idx: number = 0, len: number = instrumentReaders.length; idx < len; ++idx) {
                const instrumentReader: InstrumentReader = instrumentReaders[idx];
                instrumentReader.ActiveRhythm = rhythmInstruction;
            }
        }
        if (rhythmInstructions.length === 0 && this.currentMeasure === this.musicSheet.SourceMeasures[0]) {
            const rhythmInstruction: RhythmInstruction = new RhythmInstruction(new Fraction(4, 4, 0, false), RhythmSymbolEnum.NONE);
            for (let i: number = 0; i < this.completeNumberOfStaves; i++) {
                if (!this.currentMeasure.FirstInstructionsStaffEntries[i]) {
                    this.currentMeasure.FirstInstructionsStaffEntries[i] = new SourceStaffEntry(undefined, undefined);
                } else {
                    this.currentMeasure.FirstInstructionsStaffEntries[i].removeAllInstructionsOfTypeRhythmInstruction();
                }
                this.currentMeasure.FirstInstructionsStaffEntries[i].Instructions.push(rhythmInstruction);
            }
            for (let idx: number = 0, len: number = instrumentReaders.length; idx < len; ++idx) {
                const instrumentReader: InstrumentReader = instrumentReaders[idx];
                instrumentReader.ActiveRhythm = rhythmInstruction;
            }
        }
        for (let idx: number = 0, len: number = rhythmInstructions.length; idx < len; ++idx) {
            const rhythmInstruction: RhythmInstruction = rhythmInstructions[idx];
            if (rhythmInstruction.Rhythm.RealValue < maxRhythmValue) {
                if (this._lastElement(
                        this.currentMeasure.FirstInstructionsStaffEntries[rhythmInstructions.indexOf(rhythmInstruction)].Instructions
                    ) instanceof RhythmInstruction) {
                    // TODO Test correctness
                    const instrs: AbstractNotationInstruction[] =
                        this.currentMeasure.FirstInstructionsStaffEntries[rhythmInstructions.indexOf(rhythmInstruction)].Instructions;
                    instrs[instrs.length - 1] = rhythmInstructions[index].clone();
                }
            }
            if (
                Math.abs(rhythmInstruction.Rhythm.RealValue - maxRhythmValue) < 0.000001 &&
                rhythmInstruction.SymbolEnum !== RhythmSymbolEnum.NONE &&
                this.areRhythmInstructionsMixed(rhythmInstructions)
            ) {
                rhythmInstruction.SymbolEnum = RhythmSymbolEnum.NONE;
            }
        }
    }

    /**
     * True in case of 4/4 and COMMON TIME (or 2/2 and CUT TIME)
     * @param rhythmInstructions
     * @returns {boolean}
     */
    private areRhythmInstructionsMixed(rhythmInstructions: RhythmInstruction[]): boolean {
        for (let i: number = 1; i < rhythmInstructions.length; i++) {
            if (
                Math.abs(rhythmInstructions[i].Rhythm.RealValue - rhythmInstructions[0].Rhythm.RealValue) < 0.000001 &&
                rhythmInstructions[i].SymbolEnum !== rhythmInstructions[0].SymbolEnum
            ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Set the [[Measure]]'s duration taking into account the longest [[Instrument]] duration and the active Rhythm read from XML.
     * @param instrumentReaders
     * @param sourceMeasureCounter
     * @returns {number}
     */
    private setSourceMeasureDuration(instrumentReaders: InstrumentReader[], sourceMeasureCounter: number): number {
        let activeRhythm: Fraction = new Fraction(0, 1);
        const instrumentsMaxTieNoteFractions: Fraction[] = [];
        for (const instrumentReader of instrumentReaders) {
            instrumentsMaxTieNoteFractions.push(instrumentReader.MaxTieNoteFraction);
            const activeRythmMeasure: Fraction = instrumentReader.ActiveRhythm.Rhythm;
            if (activeRhythm.lt(activeRythmMeasure)) {
                activeRhythm = new Fraction(activeRythmMeasure.Numerator, activeRythmMeasure.Denominator, 0, false);
            }
        }
        const instrumentsDurations: Fraction[] = this.currentMeasure.calculateInstrumentsDuration(this.musicSheet, instrumentsMaxTieNoteFractions);
        let maxInstrumentDuration: Fraction = new Fraction(0, 1);
        for (const instrumentsDuration of instrumentsDurations) {
            if (maxInstrumentDuration.lt(instrumentsDuration)) {
                maxInstrumentDuration = instrumentsDuration;
            }
        }
        if (Fraction.Equal(maxInstrumentDuration, activeRhythm)) {
            this.checkFractionsForEquivalence(maxInstrumentDuration, activeRhythm);
        } else {
            if (maxInstrumentDuration.lt(activeRhythm)) {
                maxInstrumentDuration = this.currentMeasure.reverseCheck(this.musicSheet, maxInstrumentDuration);
                this.checkFractionsForEquivalence(maxInstrumentDuration, activeRhythm);
            }
        }
        this.currentMeasure.ImplicitMeasure = this.checkIfMeasureIsImplicit(maxInstrumentDuration, activeRhythm);
        if (!this.currentMeasure.ImplicitMeasure) {
            sourceMeasureCounter++;
        }
        this.currentMeasure.Duration = maxInstrumentDuration; // can be 1/1 in a 4/4 time signature
        // if (this.currentMeasure.Duration.Numerator === 0) {
        //     this.currentMeasure.Duration = activeRhythm; // might be related to #1073
        // }
        this.currentMeasure.ActiveTimeSignature = activeRhythm;
        this.currentMeasure.MeasureNumber = sourceMeasureCounter;
        for (let i: number = 0; i < instrumentsDurations.length; i++) {
            const instrumentsDuration: Fraction = instrumentsDurations[i];
            if (
                (this.currentMeasure.ImplicitMeasure && instrumentsDuration !== maxInstrumentDuration) ||
                !Fraction.Equal(instrumentsDuration, activeRhythm) &&
                !this.allInstrumentsHaveSameDuration(instrumentsDurations, maxInstrumentDuration)
            ) {
                const firstStaffIndexOfInstrument: number = this.musicSheet.getGlobalStaffIndexOfFirstStaff(this.musicSheet.Instruments[i]);
                for (let staffIndex: number = 0; staffIndex < this.musicSheet.Instruments[i].Staves.length; staffIndex++) {
                    if (!this.graphicalMeasureIsEmpty(firstStaffIndexOfInstrument + staffIndex)) {
                        this.currentMeasure.setErrorInGraphicalMeasure(firstStaffIndexOfInstrument + staffIndex, true);
                        const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/MissingNotesError",
                                                                                "Given Notes don't correspond to measure duration.");
                        this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
                    }
                }
            }
        }
        return sourceMeasureCounter;
    }

    /**
     * Check the Fractions for Equivalence and if so, sets maxInstrumentDuration's members accordingly.
     * *
     * Example: if maxInstrumentDuration = 1/1 and sourceMeasureDuration = 4/4, maxInstrumentDuration becomes 4/4.
     * @param maxInstrumentDuration
     * @param activeRhythm
     */
    private checkFractionsForEquivalence(maxInstrumentDuration: Fraction, activeRhythm: Fraction): void {
        if (activeRhythm.Denominator > maxInstrumentDuration.Denominator) {
            const factor: number = activeRhythm.Denominator / maxInstrumentDuration.Denominator;
            maxInstrumentDuration.expand(factor);
        }
    }

    /**
     * Handle the case of an implicit [[SourceMeasure]].
     * @param maxInstrumentDuration
     * @param activeRhythm
     * @returns {boolean}
     */
    private checkIfMeasureIsImplicit(maxInstrumentDuration: Fraction, activeRhythm: Fraction): boolean {
        if (!this.previousMeasure && maxInstrumentDuration.lt(activeRhythm)) {
            return true;
        }
        if (this.previousMeasure) {
            return Fraction.plus(this.previousMeasure.Duration, maxInstrumentDuration).Equals(activeRhythm);
        }
        return false;
    }

    /**
     * Check the Duration of all the given Instruments.
     * @param instrumentsDurations
     * @param maxInstrumentDuration
     * @returns {boolean}
     */
    private allInstrumentsHaveSameDuration(instrumentsDurations: Fraction[], maxInstrumentDuration: Fraction): boolean {
        let counter: number = 0;
        for (let idx: number = 0, len: number = instrumentsDurations.length; idx < len; ++idx) {
            const instrumentsDuration: Fraction = instrumentsDurations[idx];
            if (instrumentsDuration.Equals(maxInstrumentDuration)) {
                counter++;
            }
        }
        return (counter === instrumentsDurations.length && maxInstrumentDuration !== new Fraction(0, 1));
    }

    private graphicalMeasureIsEmpty(index: number): boolean {
        let counter: number = 0;
        for (let i: number = 0; i < this.currentMeasure.VerticalSourceStaffEntryContainers.length; i++) {
            if (!this.currentMeasure.VerticalSourceStaffEntryContainers[i].StaffEntries[index]) {
                counter++;
            }
        }
        return (counter === this.currentMeasure.VerticalSourceStaffEntryContainers.length);
    }

    /**
     * Check a [[SourceMeasure]] for possible empty / undefined entries ([[VoiceEntry]], [[SourceStaffEntry]], VerticalContainer)
     * (caused from TieAlgorithm removing EndTieNote) and removes them if completely empty / null
     */
    private checkSourceMeasureForNullEntries(): void {
        for (let i: number = this.currentMeasure.VerticalSourceStaffEntryContainers.length - 1; i >= 0; i--) {
            for (let j: number = this.currentMeasure.VerticalSourceStaffEntryContainers[i].StaffEntries.length - 1; j >= 0; j--) {
                const sourceStaffEntry: SourceStaffEntry = this.currentMeasure.VerticalSourceStaffEntryContainers[i].StaffEntries[j];
                if (sourceStaffEntry) {
                    for (let k: number = sourceStaffEntry.VoiceEntries.length - 1; k >= 0; k--) {
                        const voiceEntry: VoiceEntry = sourceStaffEntry.VoiceEntries[k];
                        if (voiceEntry.Notes.length === 0) {
                            this._removeFromArray(voiceEntry.ParentVoice.VoiceEntries, voiceEntry);
                            this._removeFromArray(sourceStaffEntry.VoiceEntries, voiceEntry);
                        }
                    }
                }
                if (sourceStaffEntry !== undefined && sourceStaffEntry.VoiceEntries.length === 0 && sourceStaffEntry.ChordContainers.length === 0) {
                    this.currentMeasure.VerticalSourceStaffEntryContainers[i].StaffEntries[j] = undefined;
                }
            }
        }
        for (let i: number = this.currentMeasure.VerticalSourceStaffEntryContainers.length - 1; i >= 0; i--) {
            let counter: number = 0;
            for (let idx: number = 0, len: number = this.currentMeasure.VerticalSourceStaffEntryContainers[i].StaffEntries.length; idx < len; ++idx) {
                const sourceStaffEntry: SourceStaffEntry = this.currentMeasure.VerticalSourceStaffEntryContainers[i].StaffEntries[idx];
                if (!sourceStaffEntry) {
                    counter++;
                }
            }
            if (counter === this.currentMeasure.VerticalSourceStaffEntryContainers[i].StaffEntries.length) {
                this._removeFromArray(this.currentMeasure.VerticalSourceStaffEntryContainers, this.currentMeasure.VerticalSourceStaffEntryContainers[i]);
            }
        }
    }

    /**
     * Read the XML file and creates the main sheet Labels.
     * @param root
     * @param filePath
     */
    private pushSheetLabels(root: IXmlElement, filePath: string): void {
        this.readComposer(root);
        this.readTitle(root);
        this.readCopyright(root);
        try {
            if (!this.musicSheet.Title || !this.musicSheet.Composer || !this.musicSheet.Subtitle) {
                this.readTitleAndComposerFromCredits(root); // this can also throw an error
            }
        } catch (ex) {
            log.info("MusicSheetReader.pushSheetLabels", "readTitleAndComposerFromCredits", ex);
        }
        try {
            if (!this.musicSheet.Title) {
                const barI: number = Math.max(
                    0, filePath.lastIndexOf("/"), filePath.lastIndexOf("\\")
                );
                const filename: string = filePath.substr(barI);
                const filenameSplits: string[] = filename.split(".", 1);
                this.musicSheet.Title = new Label(filenameSplits[0]);
            }
        } catch (ex) {
            log.info("MusicSheetReader.pushSheetLabels", "read title from file name", ex);
        }
    }

    // Checks whether _elem_ has an attribute with value _val_.
    private presentAttrsWithValue(elem: IXmlElement, val: string): boolean {
        for (const attr of elem.attributes()) {
            if (attr.value === val) {
                return true;
            }
        }
        return false;
    }

    private readComposer(root: IXmlElement): void {
        const identificationNode: IXmlElement = root.element("identification");
        if (identificationNode) {
            const creators: IXmlElement[] = identificationNode.elements("creator");
            for (let idx: number = 0, len: number = creators.length; idx < len; ++idx) {
                const creator: IXmlElement = creators[idx];
                if (creator.hasAttributes) {
                    if (this.presentAttrsWithValue(creator, "composer")) {
                        this.musicSheet.Composer = new Label(this.trimString(creator.value));
                        continue;
                    }
                    if (this.presentAttrsWithValue(creator, "lyricist") || this.presentAttrsWithValue(creator, "poet")) {
                        this.musicSheet.Lyricist = new Label(this.trimString(creator.value));
                    }
                }
            }
        }
    }

    private readCopyright(root: IXmlElement): void {
        const idElements: IXmlElement[] = root.elements("identification");
        if(idElements.length > 0){
            const idElement: IXmlElement = idElements[0];
            const rightElements: IXmlElement[] = idElement.elements("rights");
            if(rightElements.length > 0){
                for(let idx: number = 0, len: number = rightElements.length; idx < len; ++idx){
                    const rightElement: IXmlElement = rightElements[idx];
                    if(rightElement.value){
                        this.musicSheet.Copyright = new Label(rightElement.value, TextAlignmentEnum.CenterBottom, undefined, true);
                        break;
                    }
                }
            }
        }
    }

    private readTitleAndComposerFromCredits(root: IXmlElement): void {
        if (this.rules.SheetComposerSubtitleUseLegacyParsing) {
            this.readTitleAndComposerFromCreditsLegacy(root);
            return;
        }
        const systemYCoordinates: number = this.computeSystemYCoordinates(root);
        if (systemYCoordinates === 0) {
            return;
        }
        // let largestTitleCreditSize: number = 1;
        let finalTitle: string = undefined;
        // let largestCreditYInfo: number = 0;
        let finalSubtitle: string = undefined;
        // let possibleTitle: string = undefined;
        let finalComposer: string = undefined;
        const creditElements: IXmlElement[] = root.elements("credit");
        for (let idx: number = 0, len: number = creditElements.length; idx < len; ++idx) {
            const credit: IXmlElement = creditElements[idx];
            if (!credit.attribute("page")) {
                return;
            }
            if (credit.attribute("page").value === "1") {
                let creditChildren: IXmlElement[] = undefined;
                if (credit) {
                    let isSubtitle: boolean = false;
                    let isComposer: boolean = false;
                    const typeChild: IXmlElement = credit.element("credit-type");
                    if (typeChild?.value === "subtitle") {
                        isSubtitle = true;
                    } else if (typeChild?.value === "composer") {
                        isComposer = true;
                    }
                    let isSubtitleOrComposer: boolean = isSubtitle || isComposer;

                    creditChildren = credit.elements("credit-words");
                    for (const creditChild of creditChildren) {
                        const creditChildValue: string = creditChild.value?.trim();
                        if (creditChildValue === "Copyright ©") {
                            continue; // this seems to be a MuseScore default, useless
                        }
                        const creditJustify: string = creditChild.attribute("justify")?.value;
                        if (creditJustify === "right") {
                            isComposer = true;
                            isSubtitleOrComposer = true;
                        } else if (creditJustify === "center" && finalTitle) {
                            isSubtitle = true;
                            isSubtitleOrComposer = true;
                        }
                        const creditY: string = creditChild.attribute("default-y")?.value;
                        // eslint-disable-next-line no-null/no-null
                        const creditYGiven: boolean = creditY !== undefined && creditY !== null;
                        const creditYInfo: number = creditYGiven ? parseFloat(creditY) : Number.MIN_VALUE;
                        if ((creditYGiven && creditYInfo > systemYCoordinates) || isSubtitleOrComposer) {
                            if (!finalTitle && !isSubtitleOrComposer) {
                                // only take largest font size label
                                // const creditSize: string = creditChild.attribute("font-size")?.value;
                                // if (creditSize) {
                                //     const titleCreditSizeInt: number = parseFloat(creditSize);
                                //     if (largestTitleCreditSize < titleCreditSizeInt) {
                                //         largestTitleCreditSize = titleCreditSizeInt;
                                //         finalTitle = creditChild.value;
                                //     }
                                // }
                                finalTitle = creditChildValue;
                                // if (!finalTitle) {
                                //     finalTitle = creditChild.value;
                                // } else {
                                //     finalTitle += "\n" + creditChild.value;
                                // }
                            } else if (isComposer || creditJustify === "right") {
                                if (!finalComposer) {
                                    finalComposer = creditChildValue;
                                } else {
                                    finalComposer += "\n" + creditChildValue;
                                }
                            } else if (isSubtitle || creditJustify !== "right" && creditJustify !== "left") {
                                // if (largestCreditYInfo < creditYInfo) {
                                //     largestCreditYInfo = creditYInfo;
                                //     if (possibleTitle) {
                                //         finalSubtitle = possibleTitle;
                                //         possibleTitle = creditChild.value;
                                //     } else {
                                //         possibleTitle = creditChild.value;
                                //     }
                                // } else {
                                if (finalSubtitle) {
                                    finalSubtitle += "\n" + creditChildValue;
                                } else {
                                    finalSubtitle = creditChildValue;
                                }
                                // }
                            } else if (creditJustify === "left") {
                                if (!this.musicSheet.Lyricist) {
                                    this.musicSheet.Lyricist = new Label(creditChildValue);
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }
        if (!this.musicSheet.Title && finalTitle) {
            this.musicSheet.Title = new Label(this.trimString(finalTitle));
        }
        if (!this.musicSheet.Subtitle && finalSubtitle) {
            this.musicSheet.Subtitle = new Label(this.trimString(finalSubtitle));
        }
        if (finalComposer) {
            let overrideSheetComposer: boolean = false;
            if (!this.musicSheet.Composer) {
                overrideSheetComposer = true;
            } else {
                // check if we have more lines in existing composer label
                //   we should only take the existing label if it has less lines,
                //   since the credit labels are more likely to be the rendering intention than the metadata
                const creditComposerLines: number = (finalComposer.match("\n") ?? []).length + 1;
                const sheetComposerLines: number = (this.musicSheet.Composer.text.match("\n") ?? []).length + 1;
                if (creditComposerLines >= sheetComposerLines) {
                    overrideSheetComposer = true;
                }
            }
            if (overrideSheetComposer) {
                this.musicSheet.Composer = new Label(this.trimString(finalComposer));
            }
        }
    }

    /** @deprecated Old OSMD < 1.8.6 way of parsing composer + subtitles,
     * ignores multiline composer + subtitles, uses XML identification tags instead.
     * Will probably be removed soon.
     */
    private readTitleAndComposerFromCreditsLegacy(root: IXmlElement): void {
        const systemYCoordinates: number = this.computeSystemYCoordinates(root);
        if (systemYCoordinates === 0) {
            return;
        }
        let largestTitleCreditSize: number = 1;
        let finalTitle: string = undefined;
        let largestCreditYInfo: number = 0;
        let finalSubtitle: string = undefined;
        let possibleTitle: string = undefined;
        const creditElements: IXmlElement[] = root.elements("credit");
        for (let idx: number = 0, len: number = creditElements.length; idx < len; ++idx) {
            const credit: IXmlElement = creditElements[idx];
            if (!credit.attribute("page")) {
                return;
            }
            if (credit.attribute("page").value === "1") {
                let creditChild: IXmlElement = undefined;
                if (credit) {
                    creditChild = credit.element("credit-words");
                    if (!creditChild.attribute("justify")) {
                        break;
                    }
                    const creditJustify: string = creditChild.attribute("justify")?.value;
                    const creditY: string = creditChild.attribute("default-y")?.value;
                    // eslint-disable-next-line no-null/no-null
                    const creditYGiven: boolean = creditY !== undefined && creditY !== null;
                    const creditYInfo: number = creditYGiven ? parseFloat(creditY) : Number.MIN_VALUE;
                    let isSubtitle: boolean = false;
                    const typeChild: IXmlElement = credit.element("credit-type");
                    if (typeChild?.value === "subtitle") {
                        isSubtitle = true;
                    }
                    if ((creditYGiven && creditYInfo > systemYCoordinates) || isSubtitle) {
                        if (!this.musicSheet.Title && !isSubtitle) {
                            const creditSize: string = creditChild.attribute("font-size")?.value;
                            if (creditSize) {
                                const titleCreditSizeInt: number = parseFloat(creditSize);
                                if (largestTitleCreditSize < titleCreditSizeInt) {
                                    largestTitleCreditSize = titleCreditSizeInt;
                                    finalTitle = creditChild.value;
                                }
                            }
                        }
                        if (!this.musicSheet.Subtitle) {
                            if (creditJustify !== "right" && creditJustify !== "left" || isSubtitle) {
                                if (largestCreditYInfo < creditYInfo) {
                                    largestCreditYInfo = creditYInfo;
                                    if (possibleTitle) {
                                        finalSubtitle = possibleTitle;
                                        possibleTitle = creditChild.value;
                                    } else {
                                        possibleTitle = creditChild.value;
                                    }
                                } else {
                                    if (finalSubtitle) {
                                        finalSubtitle += "\n" + creditChild.value;
                                    } else {
                                        finalSubtitle = creditChild.value;
                                    }
                                }
                            }
                        }
                        switch (creditJustify) {
                            case "right":
                                if (!this.musicSheet.Composer) {
                                    this.musicSheet.Composer = new Label(this.trimString(creditChild.value));
                                }
                                break;
                            case "left":
                                if (!this.musicSheet.Lyricist) {
                                    this.musicSheet.Lyricist = new Label(this.trimString(creditChild.value));
                                }
                                break;
                            default:
                                break;
                        }
                    }
                }
            }
        }
        if (!this.musicSheet.Title && finalTitle) {
            this.musicSheet.Title = new Label(this.trimString(finalTitle));
        }
        if (!this.musicSheet.Subtitle && finalSubtitle) {
            this.musicSheet.Subtitle = new Label(this.trimString(finalSubtitle));
        }
    }

    private computeSystemYCoordinates(root: IXmlElement): number {
        if (!root.element("defaults")) {
            return 0;
        }
        let paperHeight: number = 0;
        let topSystemDistance: number = 0;
        try {
            const defi: string = root.element("defaults").element("page-layout").element("page-height").value;
            paperHeight = parseFloat(defi);
        } catch (e) {
            log.info("MusicSheetReader.computeSystemYCoordinates(): couldn't find page height, not reading title/composer.");
            return 0;
        }
        let found: boolean = false;
        const parts: IXmlElement[] = root.elements("part");
        for (let idx: number = 0, len: number = parts.length; idx < len; ++idx) {
            const measures: IXmlElement[] = parts[idx].elements("measure");
            for (let idx2: number = 0, len2: number = measures.length; idx2 < len2; ++idx2) {
                const measure: IXmlElement = measures[idx2];
                if (measure.element("print")) {
                    const systemLayouts: IXmlElement[] = measure.element("print").elements("system-layout");
                    for (let idx3: number = 0, len3: number = systemLayouts.length; idx3 < len3; ++idx3) {
                        const syslab: IXmlElement = systemLayouts[idx3];
                        if (syslab.element("top-system-distance")) {
                            const topSystemDistanceString: string = syslab.element("top-system-distance").value;
                            topSystemDistance = parseFloat(topSystemDistanceString);
                            found = true;
                            break;
                        }
                    }
                    break;
                }
            }
            if (found) {
                break;
            }
        }
        if (root.element("defaults").element("system-layout")) {
            const syslay: IXmlElement = root.element("defaults").element("system-layout");
            if (syslay.element("top-system-distance")) {
                const topSystemDistanceString: string = root.element("defaults").element("system-layout").element("top-system-distance").value;
                topSystemDistance = parseFloat(topSystemDistanceString);
            }
        }
        if (topSystemDistance === 0) {
            return 0;
        }
        return paperHeight - topSystemDistance;
    }

    private readTitle(root: IXmlElement): void {
        const titleNode: IXmlElement = root.element("work");
        let titleNodeChild: IXmlElement = undefined;
        if (titleNode) {
            titleNodeChild = titleNode.element("work-title");
            if (titleNodeChild && titleNodeChild.value) {
                this.musicSheet.Title = new Label(this.trimString(titleNodeChild.value));
            }
        }
        const movementNode: IXmlElement = root.element("movement-title");
        let finalSubTitle: string = "";
        if (movementNode) {
            if (!this.musicSheet.Title) {
                this.musicSheet.Title = new Label(this.trimString(movementNode.value));
            } else {
                finalSubTitle = this.trimString(movementNode.value);
            }
        }
        if (titleNode) {
            const subtitleNodeChild: IXmlElement = titleNode.element("work-number");
            if (subtitleNodeChild) {
                const workNumber: string = subtitleNodeChild.value;
                if (workNumber) {
                    if (finalSubTitle === "") {
                        finalSubTitle = workNumber;
                    } else {
                        finalSubTitle = finalSubTitle + ", " + workNumber;
                    }
                }
            }
        }
        if (finalSubTitle
        ) {
            this.musicSheet.Subtitle = new Label(finalSubTitle);
        }
    }

    /**
     * Build the [[InstrumentalGroup]]s and [[Instrument]]s.
     * @param entryList
     * @returns {{}}
     */
    private createInstrumentGroups(entryList: IXmlElement[]): { [_: string]: Instrument } {
        let instrumentId: number = 0;
        const instrumentDict: { [_: string]: Instrument } = {};
        let currentGroup: InstrumentalGroup;
        try {
            const entryArray: IXmlElement[] = entryList;
            for (let idx: number = 0, len: number = entryArray.length; idx < len; ++idx) {
                const node: IXmlElement = entryArray[idx];
                if (node.name === "score-part") {
                    const instrIdString: string = node.attribute("id").value;
                    const instrument: Instrument = new Instrument(instrumentId, instrIdString, this.musicSheet, currentGroup);
                    instrumentId++;
                    const partElements: IXmlElement[] = node.elements();
                    for (let idx2: number = 0, len2: number = partElements.length; idx2 < len2; ++idx2) {
                        const partElement: IXmlElement = partElements[idx2];
                        try {
                            if (partElement.name === "part-name") {
                                instrument.Name = partElement.value;
                                if (partElement.attribute("print-object") &&
                                   partElement.attribute("print-object").value === "no") {
                                    instrument.NameLabel.print = false;
                                }
                            } else if (partElement.name === "part-abbreviation") {
                                instrument.PartAbbreviation = partElement.value;
                            } else if (partElement.name === "score-instrument") {
                                const subInstrument: SubInstrument = new SubInstrument(instrument);
                                subInstrument.idString = partElement.firstAttribute.value;
                                instrument.SubInstruments.push(subInstrument);
                                const subElement: IXmlElement = partElement.element("instrument-name");
                                if (subElement) {
                                    subInstrument.name = subElement.value;
                                    subInstrument.setMidiInstrument(subElement.value);
                                }
                            } else if (partElement.name === "midi-instrument") {
                                let subInstrument: SubInstrument = instrument.getSubInstrument(partElement.firstAttribute.value);
                                for (let idx3: number = 0, len3: number = instrument.SubInstruments.length; idx3 < len3; ++idx3) {
                                    const subInstr: SubInstrument = instrument.SubInstruments[idx3];
                                    if (subInstr.idString === partElement.value) {
                                        subInstrument = subInstr;
                                        break;
                                    }
                                }
                                const instrumentElements: IXmlElement[] = partElement.elements();
                                for (let idx3: number = 0, len3: number = instrumentElements.length; idx3 < len3; ++idx3) {
                                    const instrumentElement: IXmlElement = instrumentElements[idx3];
                                    try {
                                        if (instrumentElement.name === "midi-channel") {
                                            if (parseInt(instrumentElement.value, 10) === 10) {
                                                instrument.MidiInstrumentId = MidiInstrument.Percussion;
                                            }
                                        } else if (instrumentElement.name === "midi-program") {
                                            if (instrument.SubInstruments.length > 0 && instrument.MidiInstrumentId !== MidiInstrument.Percussion) {
                                                subInstrument.midiInstrumentID = <MidiInstrument>Math.max(0, parseInt(instrumentElement.value, 10) - 1);
                                            }
                                        } else if (instrumentElement.name === "midi-unpitched") {
                                            subInstrument.fixedKey = Math.max(0, parseInt(instrumentElement.value, 10));
                                        } else if (instrumentElement.name === "volume") {
                                            try {
                                                const result: number = parseFloat(instrumentElement.value);
                                                subInstrument.volume = result / 127.0;
                                            } catch (ex) {
                                                log.debug("ExpressionReader.readExpressionParameters", "read volume", ex);
                                            }

                                        } else if (instrumentElement.name === "pan") {
                                            try {
                                                const result: number = parseFloat(instrumentElement.value);
                                                subInstrument.pan = result / 64.0;
                                            } catch (ex) {
                                                log.debug("ExpressionReader.readExpressionParameters", "read pan", ex);
                                            }

                                        }
                                    } catch (ex) {
                                        log.info("MusicSheetReader.createInstrumentGroups midi settings: ", ex);
                                    }

                                }
                            }
                        } catch (ex) {
                            log.info("MusicSheetReader.createInstrumentGroups: ", ex);
                        }

                    }
                    if (instrument.SubInstruments.length === 0) {
                        const subInstrument: SubInstrument = new SubInstrument(instrument);
                        instrument.SubInstruments.push(subInstrument);
                    }
                    instrumentDict[instrIdString] = instrument;
                    if (currentGroup) {
                        currentGroup.InstrumentalGroups.push(instrument);
                        this.musicSheet.Instruments.push(instrument);
                    } else {
                        this.musicSheet.InstrumentalGroups.push(instrument);
                        this.musicSheet.Instruments.push(instrument);
                    }
                } else {
                    if ((node.name === "part-group") && (node.attribute("type").value === "start")) {
                        const iG: InstrumentalGroup = new InstrumentalGroup("group", this.musicSheet, currentGroup);
                        if (currentGroup) {
                            currentGroup.InstrumentalGroups.push(iG);
                        } else {
                            this.musicSheet.InstrumentalGroups.push(iG);
                        }
                        currentGroup = iG;
                    } else {
                        if ((node.name === "part-group") && (node.attribute("type").value === "stop")) {
                            if (currentGroup) {
                                if (currentGroup.InstrumentalGroups.length === 1) {
                                    const instr: InstrumentalGroup = currentGroup.InstrumentalGroups[0];
                                    if (currentGroup.Parent) {
                                        currentGroup.Parent.InstrumentalGroups.push(instr);
                                        this._removeFromArray(currentGroup.Parent.InstrumentalGroups, currentGroup);
                                    } else {
                                        this.musicSheet.InstrumentalGroups.push(instr);
                                        this._removeFromArray(this.musicSheet.InstrumentalGroups, currentGroup);
                                    }
                                }
                                currentGroup = currentGroup.Parent;
                            }
                        }
                    }
                }
            }
        } catch (e) {
            const errorMsg: string = ITextTranslation.translateText(
                "ReaderErrorMessages/InstrumentError", "Error while reading Instruments"
            );
            throw new MusicSheetReadingException(errorMsg, e);
        }

        for (let idx: number = 0, len: number = this.musicSheet.Instruments.length; idx < len; ++idx) {
            const instrument: Instrument = this.musicSheet.Instruments[idx];
            if (!instrument.Name) {
                instrument.Name = "Instr. " + instrument.IdString;
            }
        }
        return instrumentDict;
    }

    /**
     * Read from each xmlInstrumentPart the first xmlMeasure in order to find out the [[Instrument]]'s number of Staves
     * @param partInst
     * @returns {number} - Complete number of Staves for all Instruments.
     */
    private getCompleteNumberOfStavesFromXml(partInst: IXmlElement[]): number {
        let num: number = 0;
        for (const partNode of partInst) {
            const xmlMeasureList: IXmlElement[] = partNode.elements("measure");
            if (xmlMeasureList.length > 0) {
                const xmlMeasure: IXmlElement = xmlMeasureList[0];
                if (xmlMeasure) {
                    let stavesNode: IXmlElement = xmlMeasure.element("attributes");
                    if (stavesNode) {
                        stavesNode = stavesNode.element("staves");
                    }
                    if (!stavesNode) {
                        num++;
                    } else {
                        num += parseInt(stavesNode.value, 10);
                    }
                }
            }
        }
        if (isNaN(num) || num <= 0) {
            const errorMsg: string = ITextTranslation.translateText(
                "ReaderErrorMessages/StaffError", "Invalid number of staves."
            );
            throw new MusicSheetReadingException(errorMsg);
        }
        return num;
    }

    /**
     * Read from XML for a single [[Instrument]] the first xmlMeasure in order to find out the Instrument's number of Staves.
     * @param partNode
     * @returns {number}
     */
    private getInstrumentNumberOfStavesFromXml(partNode: IXmlElement): number {
        let num: number = 0;
        const xmlMeasure: IXmlElement = partNode.element("measure");
        if (xmlMeasure) {
            const attributes: IXmlElement = xmlMeasure.element("attributes");
            let staves: IXmlElement = undefined;
            if (attributes) {
                staves = attributes.element("staves");
            }
            if (!attributes || !staves) {
                num = 1;
            } else {
                num = parseInt(staves.value, 10);
            }
        }
        if (isNaN(num) || num <= 0) {
            const errorMsg: string = ITextTranslation.translateText(
                "ReaderErrorMessages/StaffError", "Invalid number of Staves."
            );
            throw new MusicSheetReadingException(errorMsg);
        }
        return num;
    }

}