opensheetmusicdisplay/opensheetmusicdisplay

View on GitHub
src/MusicalScore/MusicParts/MusicPartManagerIterator.ts

Summary

Maintainability
F
1 wk
Test Coverage
import {Fraction} from "../../Common/DataObjects/Fraction";
import {Repetition} from "../MusicSource/Repetition";
import {DynamicsContainer} from "../VoiceData/HelperObjects/DynamicsContainer";
import {MappingSourceMusicPart} from "../MusicSource/MappingSourceMusicPart";
import {SourceMeasure} from "../VoiceData/SourceMeasure";
import {VoiceEntry} from "../VoiceData/VoiceEntry";
import {Instrument} from "../Instrument";
import {VerticalSourceStaffEntryContainer} from "../VoiceData/VerticalSourceStaffEntryContainer";
import {RhythmInstruction} from "../VoiceData/Instructions/RhythmInstruction";
import {AbstractNotationInstruction} from "../VoiceData/Instructions/AbstractNotationInstruction";
import {RepetitionInstruction} from "../VoiceData/Instructions/RepetitionInstruction";
import {ContinuousDynamicExpression} from "../VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression";
import {InstantaneousDynamicExpression} from "../VoiceData/Expressions/InstantaneousDynamicExpression";
import {MultiTempoExpression} from "../VoiceData/Expressions/MultiTempoExpression";
import {AbstractExpression} from "../VoiceData/Expressions/AbstractExpression";
import log from "loglevel";
import { MusicSheet } from "../MusicSheet";

export class MusicPartManagerIterator {
    constructor(musicSheet: MusicSheet, startTimestamp?: Fraction, endTimestamp?: Fraction) {
        try {
            this.frontReached = true;
            this.musicSheet = musicSheet;
            this.currentVoiceEntries = undefined;
            this.frontReached = false;
            for (const rep of this.musicSheet.Repetitions) {
                this.setRepetitionIterationCount(rep, 1);
            }
            this.activeDynamicExpressions = new Array(this.musicSheet.getCompleteNumberOfStaves());
            this.currentMeasure = this.musicSheet.SourceMeasures[0];
            if (!startTimestamp) { return; }
            do {
                this.moveToNext();
            } while ((!this.currentVoiceEntries || this.currentTimeStamp.lt(startTimestamp)) && !this.endReached);
            for (let staffIndex: number = 0; staffIndex < this.activeDynamicExpressions.length; staffIndex++) {
                if (this.activeDynamicExpressions[staffIndex]) {
                    if (this.activeDynamicExpressions[staffIndex] instanceof ContinuousDynamicExpression) {
                        const continuousDynamic: ContinuousDynamicExpression =
                            <ContinuousDynamicExpression>this.activeDynamicExpressions[staffIndex];
                        this.currentDynamicChangingExpressions.push(new DynamicsContainer(continuousDynamic, staffIndex));
                    } else {
                        const instantaneousDynamic: InstantaneousDynamicExpression =
                            <InstantaneousDynamicExpression>this.activeDynamicExpressions[staffIndex];
                        this.currentDynamicChangingExpressions.push(new DynamicsContainer(instantaneousDynamic, staffIndex));
                    }
                }
            }
            this.currentTempoChangingExpression = this.activeTempoExpression;
        } catch (err) {
            log.info("MusicPartManagerIterator: " + err);
        }

    }
    public backJumpOccurred: boolean;
    public forwardJumpOccurred: boolean;
    private musicSheet: MusicSheet;
    private currentMappingPart: MappingSourceMusicPart;
    private currentMeasure: SourceMeasure;
    private currentMeasureIndex: number = 0;
    private currentPartIndex: number = 0;
    private currentVoiceEntryIndex: number = -1;
    private currentDynamicEntryIndex: number = 0;
    private currentTempoEntryIndex: number = 0;
    private currentVoiceEntries: VoiceEntry[];
    private currentDynamicChangingExpressions: DynamicsContainer[] = [];
    private currentTempoChangingExpression: MultiTempoExpression;
    // FIXME: replace these two with a real Dictionary!
    private repetitionIterationCountDictKeys: Repetition[];
    private repetitionIterationCountDictValues: number[];
    private currentRepetition: Repetition = undefined;
    private endReached: boolean = false;
    private frontReached: boolean = false;
    public currentTimeStamp: Fraction = new Fraction(0, 1);
    private currentEnrolledMeasureTimestamp: Fraction = new Fraction(0, 1);
    private currentRelativeInMeasureTimestamp: Fraction = new Fraction(0, 1);
    private currentVerticalContainerInMeasureTimestamp: Fraction = new Fraction(0, 1);
    private jumpResponsibleRepetition: Repetition = undefined;
    private currentBpm: number;
    private activeDynamicExpressions: AbstractExpression[] = [];
    private activeTempoExpression: MultiTempoExpression;
    public SkipInvisibleNotes: boolean = true;

    public get EndReached(): boolean {
        return this.endReached;
    }
    public get FrontReached(): boolean {
        return this.frontReached;
    }
    public get CurrentMeasure(): SourceMeasure {
        return this.currentMeasure;
    }
    public get CurrentRepetition(): Repetition {
        return this.currentRepetition;
    }
    public get CurrentRepetitionIteration(): number {
        if (this.CurrentRepetition) {
            return this.getRepetitionIterationCount(this.CurrentRepetition);
        }
        return 0;
    }
    public get CurrentJumpResponsibleRepetitionIterationBeforeJump(): number {
        if (this.jumpResponsibleRepetition) {
            return this.getRepetitionIterationCount(this.jumpResponsibleRepetition) - 1;
        }
        return 0;
    }
    public get CurrentBpm(): number {
        return this.currentBpm;
    }
    public get CurrentVoiceEntries(): VoiceEntry[] {
        return this.currentVoiceEntries;
    }
    public get CurrentMeasureIndex(): number {
        return this.currentMeasureIndex;
    }
    public get CurrentEnrolledTimestamp(): Fraction {
        return Fraction.plus(this.currentEnrolledMeasureTimestamp, this.currentVerticalContainerInMeasureTimestamp);
    }
    public get CurrentSourceTimestamp(): Fraction {
        return this.currentTimeStamp;
    }
    public get CurrentRelativeInMeasureTimestamp(): Fraction {
        return this.currentRelativeInMeasureTimestamp;
    }
    public get JumpOccurred(): boolean {
        return this.backJumpOccurred || this.forwardJumpOccurred;
    }
    public get ActiveTempoExpression(): MultiTempoExpression {
        return this.activeTempoExpression;
    }
    public get ActiveDynamicExpressions(): AbstractExpression[] {
        return this.activeDynamicExpressions;
    }
    public get CurrentTempoChangingExpression(): MultiTempoExpression {
        return this.currentTempoChangingExpression;
    }
    public get JumpResponsibleRepetition(): Repetition {
        return this.jumpResponsibleRepetition;
    }

    /**
     * Creates a clone of this iterator which has the same actual position.
     */
    public clone(startTimeStamp: Fraction = undefined, endTimeStamp: Fraction = undefined): MusicPartManagerIterator {
        const ret: MusicPartManagerIterator = new MusicPartManagerIterator(this.musicSheet, startTimeStamp ?? this.currentTimeStamp, endTimeStamp);
        ret.currentVoiceEntryIndex = this.currentVoiceEntryIndex;
        ret.currentMappingPart = this.currentMappingPart;
        ret.currentPartIndex = this.currentPartIndex;
        ret.currentVoiceEntries = this.currentVoiceEntries;
        ret.endReached = this.endReached;
        ret.frontReached = this.frontReached;
        // alternative method to set currentTimeStamp? may not fully affect current iterator position
        // ret.currentTimeStamp = this.currentTimeStamp;
        return ret;
    }

    /**
     * Returns the visible voice entries for the provided instrument of the current iterator position.
     * @param instrument
     * Returns: A List of voiceEntries. If there are no entries the List has a Count of 0 (it does not return null).
     */
    public CurrentVisibleVoiceEntries(instrument?: Instrument): VoiceEntry[] {
        const voiceEntries: VoiceEntry[] = [];
        if (!this.currentVoiceEntries) {
            return voiceEntries;
        }
        if (instrument) {
            for (const entry of this.currentVoiceEntries) {
                if (entry.ParentVoice.Parent.IdString === instrument.IdString) {
                    this.getVisibleEntries(entry, voiceEntries);
                    return voiceEntries;
                }
            }
        } else {
            for (const entry of this.currentVoiceEntries) {
                this.getVisibleEntries(entry, voiceEntries);
            }
        }
        return voiceEntries;
    }

    /**
     * Returns the visible voice entries for the provided instrument of the current iterator position.
     * @param instrument
     * Returns: A List of voiceEntries. If there are no entries the List has a Count of 0 (it does not return null).
     */
    public CurrentAudibleVoiceEntries(instrument?: Instrument): VoiceEntry[] {
        const voiceEntries: VoiceEntry[] = [];
        if (!this.currentVoiceEntries) {
            return voiceEntries;
        }
        if (instrument) {
            for (const entry of this.currentVoiceEntries) {
                if (entry.ParentVoice.Parent.IdString === instrument.IdString) {
                    this.getAudibleEntries(entry, voiceEntries);
                    return voiceEntries;
                }
            }
        } else {
            for (const entry of this.currentVoiceEntries) {
                this.getAudibleEntries(entry, voiceEntries);
            }
        }
        return voiceEntries;
    }

    /**
     * Returns the audible dynamics of the current iterator position.
     * Returns: A List of Dynamics. If there are no entries the List has a Count of 0 (it does not return null).
     */
    public getCurrentDynamicChangingExpressions(): DynamicsContainer[] {
        return this.currentDynamicChangingExpressions;
    }

    /**
     * Returns the score following voice entries for the provided instrument of the current iterator position.
     * @param instrument
     * Returns: A List of voiceEntries. If there are no entries the List has a Count of 0
     * (it does not return null).
     */
    public CurrentScoreFollowingVoiceEntries(instrument?: Instrument): VoiceEntry[] {
        const voiceEntries: VoiceEntry[] = [];
        if (!this.currentVoiceEntries) {
            return voiceEntries;
        }
        if (instrument) {
            for (const entry of this.currentVoiceEntries) {
                if (entry.ParentVoice.Parent.IdString === instrument.IdString) {
                    this.getScoreFollowingEntries(entry, voiceEntries);
                    return voiceEntries;
                }
            }
        } else {
            for (const entry of this.currentVoiceEntries) {
                this.getScoreFollowingEntries(entry, voiceEntries);
            }
        }
        return voiceEntries;
    }

    //public currentPlaybackSettings(): PlaybackSettings {
    //    return this.manager.MusicSheet.SheetPlaybackSetting;
    //}

    // move to previous
    public moveToPrevious(): void {
        // this.forwardJumpOccurred = this.backJumpOccurred = false;
        if (this.frontReached) {
            return;
        }
        if (this.currentVoiceEntries) {
            this.currentVoiceEntries = [];
        }
        this.recursiveMoveBack();
    }

    public moveToPreviousVisibleVoiceEntry(notesOnly: boolean): void {
        while (!this.frontReached) {
            this.moveToPrevious();
            if (this.checkEntries(notesOnly)) {
                return;
            }
        }
    }
    public moveToNext(): void {
        this.forwardJumpOccurred = this.backJumpOccurred = false;
        if (this.endReached) { return; }
        if (this.frontReached) {
            this.frontReached = false;
            this.currentVoiceEntryIndex = -1;
        }
        if (this.currentVoiceEntries) {
            this.currentVoiceEntries = [];
        }
        this.recursiveMove();
        if (!this.currentMeasure) {
            this.currentTimeStamp = new Fraction(99999, 1);
            this.currentMeasure = this.musicSheet.SourceMeasures.last();
        }
    }
    public moveToNextVisibleVoiceEntry(notesOnly: boolean): void {
        while (!this.endReached) {
            this.moveToNext();
            if (this.checkEntries(notesOnly)) { return; }
        }
    }
    private resetRepetitionIterationCount(repetition: Repetition): number {
        this.setRepetitionIterationCount(repetition, 1);
        return 1;
    }
    private incrementRepetitionIterationCount(repetition: Repetition): number {
        if (this.repetitionIterationCountDictKeys.indexOf(repetition) === -1) {
            return this.setRepetitionIterationCount(repetition, 1);
        } else {
            return this.setRepetitionIterationCount(repetition, this.getRepetitionIterationCount(repetition) + 1);
        }
    }
    private setRepetitionIterationCount(repetition: Repetition, iterationCount: number): number {
        const i: number = this.repetitionIterationCountDictKeys.indexOf(repetition);
        if (i === -1) {
            this.repetitionIterationCountDictKeys.push(repetition);
            this.repetitionIterationCountDictValues.push(iterationCount);
        } else {
            this.repetitionIterationCountDictValues[i] = iterationCount;
        }
        return iterationCount;
    }
    private getRepetitionIterationCount(rep: Repetition): number {
        const i: number = this.repetitionIterationCountDictKeys.indexOf(rep);
        if (i !== -1) {
            return this.repetitionIterationCountDictValues[i];
        }
    }
/*    private moveTempoIndexToTimestamp(measureNumber: number): void {
        for (let index: number = 0; index < this.manager.MusicSheet.TimestampSortedTempoExpressionsList.length; index++) {
            if (this.manager.MusicSheet.TimestampSortedTempoExpressionsList[index].SourceMeasureParent.MeasureNumber >= measureNumber) {
                this.currentTempoEntryIndex = Math.Max(-1, index - 1);
                return
            }
        }
    }
    private getNextTempoEntryTimestamp(): Fraction {
        if (this.currentTempoEntryIndex >= this.manager.MusicSheet.TimestampSortedTempoExpressionsList.length - 1) {
            return new Fraction(99999, 1);
        }
        return this.manager.MusicSheet.TimestampSortedTempoExpressionsList[this.currentTempoEntryIndex + 1].SourceMeasureParent.AbsoluteTimestamp +
        this.manager.MusicSheet.TimestampSortedTempoExpressionsList[this.currentTempoEntryIndex + 1].Timestamp;
    }
    private moveToNextDynamic(): void {
        this.currentDynamicEntryIndex++;
        this.currentDynamicChangingExpressions.Clear();
        let curDynamicEntry: DynamicsContainer = this.manager.MusicSheet.TimestampSortedDynamicExpressionsList[this.currentDynamicEntryIndex];
        this.currentDynamicChangingExpressions.push(curDynamicEntry);
        let tsNow: Fraction = curDynamicEntry.parMultiExpression().AbsoluteTimestamp;
        for (let i: number = this.currentDynamicEntryIndex + 1; i < this.manager.MusicSheet.TimestampSortedDynamicExpressionsList.length; i++) {
            curDynamicEntry = this.manager.MusicSheet.TimestampSortedDynamicExpressionsList[i];
            if ((curDynamicEntry.parMultiExpression().AbsoluteTimestamp !== tsNow)) { break; }
            this.currentDynamicEntryIndex = i;
            this.currentDynamicChangingExpressions.push(curDynamicEntry);
        }
    }
    private moveDynamicIndexToTimestamp(absoluteTimestamp: Fraction): void {
        let dynamics: DynamicsContainer[] = this.manager.MusicSheet.TimestampSortedDynamicExpressionsList;
        for (let index: number = 0; index < dynamics.length; index++) {
            if (dynamics[index].parMultiExpression().AbsoluteTimestamp.gte(absoluteTimestamp)) {
                this.currentDynamicEntryIndex = Math.Max(0, index - 1);
                return
            }
        }
    }
    private getNextDynamicsEntryTimestamp(): Fraction {
        if (this.currentDynamicEntryIndex >= this.manager.MusicSheet.TimestampSortedDynamicExpressionsList.length - 1) {
            return new Fraction(99999, 1);
        }
        return this.manager.MusicSheet.TimestampSortedDynamicExpressionsList[this.currentDynamicEntryIndex + 1].parMultiExpression().AbsoluteTimestamp;
    }
    */
    private handleRepetitionsAtMeasureBegin(): void {
        for (let idx: number = 0, len: number = this.currentMeasure.FirstRepetitionInstructions.length; idx < len; ++idx) {
            const repetitionInstruction: RepetitionInstruction = this.currentMeasure.FirstRepetitionInstructions[idx];
            if (!repetitionInstruction.parentRepetition) { continue; }
            const currentRepetition: Repetition = repetitionInstruction.parentRepetition;
            this.currentRepetition = currentRepetition;
            if (currentRepetition.StartIndex === this.currentMeasureIndex) {
                if (
                  this.JumpResponsibleRepetition !== undefined &&
                  currentRepetition !== this.JumpResponsibleRepetition &&
                  currentRepetition.StartIndex >= this.JumpResponsibleRepetition.StartIndex &&
                  currentRepetition.EndIndex <= this.JumpResponsibleRepetition.EndIndex
                ) {
                    this.resetRepetitionIterationCount(currentRepetition);
                }
            }
        }
    }

    private handleRepetitionsAtMeasureEnd(): void {
        for (let idx: number = 0, len: number = this.currentMeasure.LastRepetitionInstructions.length; idx < len; ++idx) {
            const repetitionInstruction: RepetitionInstruction = this.currentMeasure.LastRepetitionInstructions[idx];
            const currentRepetition: Repetition = repetitionInstruction.parentRepetition;
            if (!currentRepetition) { continue; }
            if (currentRepetition.BackwardJumpInstructions.indexOf(repetitionInstruction) > -1) {
                if (this.getRepetitionIterationCount(currentRepetition) < currentRepetition.UserNumberOfRepetitions) {
                    this.doBackJump(currentRepetition);
                    this.backJumpOccurred = true;
                    return;
                }
            }
            if (repetitionInstruction === currentRepetition.forwardJumpInstruction) {
                if (
                  this.JumpResponsibleRepetition !== undefined
                  && currentRepetition !== this.JumpResponsibleRepetition
                  && currentRepetition.StartIndex >= this.JumpResponsibleRepetition.StartIndex
                  && currentRepetition.EndIndex <= this.JumpResponsibleRepetition.EndIndex
                ) {
                    this.resetRepetitionIterationCount(currentRepetition);
                }

                const forwardJumpTargetMeasureIndex: number = currentRepetition.getForwardJumpTargetForIteration(
                  this.getRepetitionIterationCount(currentRepetition)
                );
                if (forwardJumpTargetMeasureIndex >= 0) {
                    this.currentMeasureIndex = forwardJumpTargetMeasureIndex;
                    this.currentMeasure = this.musicSheet.SourceMeasures[this.currentMeasureIndex];
                    this.currentVoiceEntryIndex = -1;
                    this.jumpResponsibleRepetition = currentRepetition;
                    this.forwardJumpOccurred = true;
                    return;
                }
                if (forwardJumpTargetMeasureIndex === -2) {
                    this.endReached = true;
                }
            }
        }
        this.currentMeasureIndex++;
        if (this.JumpResponsibleRepetition !== undefined && this.currentMeasureIndex > this.JumpResponsibleRepetition.EndIndex) {
            this.jumpResponsibleRepetition = undefined;
        }
    }
    private doBackJump(currentRepetition: Repetition): void {
        this.currentMeasureIndex = currentRepetition.getBackwardJumpTarget();
        this.currentMeasure = this.musicSheet.SourceMeasures[this.currentMeasureIndex];
        this.currentVoiceEntryIndex = -1;
        this.incrementRepetitionIterationCount(currentRepetition);
        this.jumpResponsibleRepetition = currentRepetition;
    }
    private activateCurrentRhythmInstructions(): void {
        if (
          this.currentMeasure !== undefined &&
          this.currentMeasure.FirstInstructionsStaffEntries.length > 0 &&
          this.currentMeasure.FirstInstructionsStaffEntries[0] !== undefined
        ) {
            const instructions: AbstractNotationInstruction[] = this.currentMeasure.FirstInstructionsStaffEntries[0].Instructions;
            for (let idx: number = 0, len: number = instructions.length; idx < len; ++idx) {
                const abstractNotationInstruction: AbstractNotationInstruction = instructions[idx];
                if (abstractNotationInstruction instanceof RhythmInstruction) {
                    this.musicSheet.SheetPlaybackSetting.rhythm = (<RhythmInstruction>abstractNotationInstruction).Rhythm;
                }
            }
        }
    }
    private activateCurrentDynamicOrTempoInstructions(): void {
        const timeSortedDynamics: DynamicsContainer[] = this.musicSheet.TimestampSortedDynamicExpressionsList;
        while (
          this.currentDynamicEntryIndex > 0 && (
            this.currentDynamicEntryIndex >= timeSortedDynamics.length ||
            this.CurrentSourceTimestamp.lte(timeSortedDynamics[this.currentDynamicEntryIndex].parMultiExpression().AbsoluteTimestamp)
          )
        ) {
            this.currentDynamicEntryIndex--;
        }
        while (
          this.currentDynamicEntryIndex < timeSortedDynamics.length &&
          timeSortedDynamics[this.currentDynamicEntryIndex].parMultiExpression().AbsoluteTimestamp.lt(this.CurrentSourceTimestamp)
        ) {
            this.currentDynamicEntryIndex++;
        }
        while (
          this.currentDynamicEntryIndex < timeSortedDynamics.length
          && timeSortedDynamics[this.currentDynamicEntryIndex].parMultiExpression().AbsoluteTimestamp.Equals(this.CurrentSourceTimestamp)
        ) {
            const dynamicsContainer: DynamicsContainer = timeSortedDynamics[this.currentDynamicEntryIndex];
            const staffIndex: number = dynamicsContainer.staffNumber;
            if (this.CurrentSourceTimestamp.Equals(dynamicsContainer.parMultiExpression().AbsoluteTimestamp)) {
                if (dynamicsContainer.continuousDynamicExpression) {
                    this.activeDynamicExpressions[staffIndex] = dynamicsContainer.continuousDynamicExpression;
                } else if (dynamicsContainer.instantaneousDynamicExpression) {
                    this.activeDynamicExpressions[staffIndex] = dynamicsContainer.instantaneousDynamicExpression;
                }
            }
            this.currentDynamicEntryIndex++;
        }
        this.currentDynamicChangingExpressions = [];
        for (let staffIndex: number = 0; staffIndex < this.activeDynamicExpressions.length; staffIndex++) {
            if (this.activeDynamicExpressions[staffIndex]) {
                let startTime: Fraction;
                let endTime: Fraction;
                if (this.activeDynamicExpressions[staffIndex] instanceof ContinuousDynamicExpression) {
                    const continuousDynamic: ContinuousDynamicExpression = <ContinuousDynamicExpression>this.activeDynamicExpressions[staffIndex];
                    startTime = continuousDynamic.StartMultiExpression.AbsoluteTimestamp;
                    endTime = continuousDynamic.EndMultiExpression.AbsoluteTimestamp;
                    if (startTime.lte(this.CurrentSourceTimestamp) && this.CurrentSourceTimestamp.lte(endTime)) {
                        this.currentDynamicChangingExpressions.push(new DynamicsContainer(continuousDynamic, staffIndex));
                    }
                } else {
                    const instantaneousDynamic: InstantaneousDynamicExpression = <InstantaneousDynamicExpression>this.activeDynamicExpressions[staffIndex];
                    if (this.CurrentSourceTimestamp.Equals(instantaneousDynamic.ParentMultiExpression.AbsoluteTimestamp)) {
                        this.currentDynamicChangingExpressions.push(new DynamicsContainer(instantaneousDynamic, staffIndex));
                    }
                }
            }
        }
        const timeSortedTempoExpressions: MultiTempoExpression[] = this.musicSheet.TimestampSortedTempoExpressionsList;

        while (this.currentTempoEntryIndex > 0 && (
          this.currentTempoEntryIndex >= timeSortedTempoExpressions.length
          || this.CurrentSourceTimestamp.lte(timeSortedTempoExpressions[this.currentTempoEntryIndex].AbsoluteTimestamp)
        )) {
            this.currentTempoEntryIndex--;
        }

        while (
          this.currentTempoEntryIndex < timeSortedTempoExpressions.length &&
          timeSortedTempoExpressions[this.currentTempoEntryIndex].AbsoluteTimestamp.lt(this.CurrentSourceTimestamp)
        ) {
            this.currentTempoEntryIndex++;
        }

        while (
          this.currentTempoEntryIndex < timeSortedTempoExpressions.length
          && timeSortedTempoExpressions[this.currentTempoEntryIndex].AbsoluteTimestamp.Equals(this.CurrentSourceTimestamp)
        ) {
            this.activeTempoExpression = timeSortedTempoExpressions[this.currentTempoEntryIndex];
            this.currentTempoEntryIndex++;
        }
        this.currentTempoChangingExpression = undefined;
        if (this.activeTempoExpression) {
            let endTime: Fraction = this.activeTempoExpression.AbsoluteTimestamp;
            if (this.activeTempoExpression.ContinuousTempo) {
                endTime = this.activeTempoExpression.ContinuousTempo.AbsoluteEndTimestamp;
            }
            if (   this.activeTempoExpression.AbsoluteTimestamp.lte(this.CurrentSourceTimestamp)
                || this.CurrentSourceTimestamp.lte(endTime)
            ) {
                this.currentTempoChangingExpression = this.activeTempoExpression;
            }
        }
    }

    private recursiveMoveBack(): void {
       if (this.currentVoiceEntryIndex > 0 ) {
            this.currentVoiceEntryIndex--;
            const currentContainer: VerticalSourceStaffEntryContainer = this.currentMeasure.VerticalSourceStaffEntryContainers[this.currentVoiceEntryIndex];
            this.currentVoiceEntries = this.getVoiceEntries(currentContainer);
            this.currentVerticalContainerInMeasureTimestamp = currentContainer.Timestamp;
            this.currentRelativeInMeasureTimestamp = this.currentVerticalContainerInMeasureTimestamp;
            this.currentTimeStamp = Fraction.plus(this.currentMeasure.AbsoluteTimestamp, this.currentVerticalContainerInMeasureTimestamp);
            this.activateCurrentDynamicOrTempoInstructions();
            // re-check endReached
            const selectionEnd: Fraction = this.musicSheet.SelectionEnd;
            if (selectionEnd && this.currentTimeStamp.lt(selectionEnd)) {
                this.endReached = false;
            }
            this.currentMeasureIndex = this.musicSheet.SourceMeasures.indexOf(this.CurrentMeasure);
            return;
        }
        else if (this.currentVoiceEntryIndex === 0  && this.currentMeasureIndex !== 0) {
            const m: SourceMeasure = this.musicSheet.SourceMeasures[this.currentMeasureIndex-1];
            this.currentMeasureIndex--;
            this.currentMeasure = this.musicSheet.SourceMeasures[this.currentMeasureIndex];
            const currentContainer: VerticalSourceStaffEntryContainer = m.VerticalSourceStaffEntryContainers[m.VerticalSourceStaffEntryContainers.length-1];
            this.currentVoiceEntries = this.getVoiceEntries(currentContainer);
            this.currentVerticalContainerInMeasureTimestamp = currentContainer.Timestamp;
            this.currentVoiceEntryIndex = m.VerticalSourceStaffEntryContainers.length-1;
            this.currentTimeStamp = Fraction.plus(this.currentMeasure.AbsoluteTimestamp, currentContainer.Timestamp);
            this.activateCurrentDynamicOrTempoInstructions();
            // re-check endReached
            const selectionEnd: Fraction = this.musicSheet.SelectionEnd;
            if (selectionEnd && this.currentTimeStamp.lt(selectionEnd)) {
                this.endReached = false;
            }
            return;
        }
        // we reached the beginning
        this.frontReached = true;
        this.currentTimeStamp = new Fraction(-1, 1);
    }

    private recursiveMove(): void {
        this.currentVoiceEntryIndex++; // TODO handle hidden part: skip hidden voice if requested by parameter
        if (this.currentVoiceEntryIndex === 0) {
            this.handleRepetitionsAtMeasureBegin();
            this.activateCurrentRhythmInstructions();
        }
        // everything fine, no complications
        if (this.currentVoiceEntryIndex >= 0 && this.currentVoiceEntryIndex < this.currentMeasure.VerticalSourceStaffEntryContainers.length) {
            const currentContainer: VerticalSourceStaffEntryContainer = this.currentMeasure.VerticalSourceStaffEntryContainers[this.currentVoiceEntryIndex];
            this.currentVoiceEntries = this.getVoiceEntries(currentContainer);
            this.currentVerticalContainerInMeasureTimestamp = currentContainer.Timestamp;
            this.currentRelativeInMeasureTimestamp = this.currentVerticalContainerInMeasureTimestamp;
            this.currentTimeStamp = Fraction.plus(this.currentMeasure.AbsoluteTimestamp, this.currentVerticalContainerInMeasureTimestamp);
            const selectionEnd: Fraction = this.musicSheet.SelectionEnd;
            // TODO handle selectionEnd undefined, can happen in Beethoven Ferne Geliebte
            if (selectionEnd && this.currentTimeStamp.gte(selectionEnd)) {
                this.endReached = true;
            }
            this.activateCurrentDynamicOrTempoInstructions();
            return;
        }
        this.currentEnrolledMeasureTimestamp.Add(this.currentMeasure.Duration);
        this.handleRepetitionsAtMeasureEnd();
        if (this.currentMeasureIndex >= 0 && this.currentMeasureIndex < this.musicSheet.SourceMeasures.length) {
            this.currentMeasure = this.musicSheet.SourceMeasures[this.currentMeasureIndex];
            this.currentTimeStamp = Fraction.plus(this.currentMeasure.AbsoluteTimestamp, this.currentVerticalContainerInMeasureTimestamp);
            this.currentVoiceEntryIndex = -1;
            this.recursiveMove();
            return;
        }
        // we reached the end
        this.currentVerticalContainerInMeasureTimestamp = new Fraction();
        this.currentMeasure = undefined;
        this.currentVoiceEntries = undefined;
        this.endReached = true;
    }

    /**
     * helper function for moveToNextVisibleVoiceEntry and moveToPreviousVisibleVoiceEntry
     * Get all entries and check if there is at least one valid entry in the list
     * @param notesOnly
     */
    private checkEntries(notesOnly: boolean): boolean {
        const tlist: VoiceEntry[] = this.CurrentVisibleVoiceEntries();
        if (tlist.length > 0) {
            if (!notesOnly) { return true; }
            for (let idx: number = 0, len: number = tlist.length; idx < len; ++idx) {
                const entry: VoiceEntry = tlist[idx];
                if (entry.Notes[0].Pitch) { return true; }
            }
        }
        return false;
    }
    private getVisibleEntries(entry: VoiceEntry, visibleEntries: VoiceEntry[]): void {
        if (entry.ParentVoice.Visible) {
            let anyNoteVisible: boolean = false;
            for (const note of entry.Notes) {
                if (note.PrintObject) {
                    anyNoteVisible = true;
                    break;
                }
            }
            if (!anyNoteVisible && this.SkipInvisibleNotes) {
                return;
            }
            visibleEntries.push(entry);
        }
    }
    private getAudibleEntries(entry: VoiceEntry, audibleEntries: VoiceEntry[]): void {
        if (entry.ParentVoice.Audible) {
            audibleEntries.push(entry);
        }
    }
    private getScoreFollowingEntries(entry: VoiceEntry, followingEntries: VoiceEntry[]): void {
        if (entry.ParentVoice.Following && entry.ParentVoice.Parent.Following) {
            followingEntries.push(entry);
        }
    }
    private getVoiceEntries(container: VerticalSourceStaffEntryContainer): VoiceEntry[] {
        const entries: VoiceEntry[] = [];
        for (const sourceStaffEntry of container.StaffEntries) {
            if (!sourceStaffEntry) { continue; }
            for (const voiceEntry of sourceStaffEntry.VoiceEntries) {
                entries.push(voiceEntry);
            }
        }
        return entries;
    }


}