package com.xenoage.zong.io.midi.out.repetitions;
import com.xenoage.utils.annotations.Const;
import com.xenoage.utils.annotations.MaybeNull;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.music.MusicElementType;
import com.xenoage.zong.core.music.barline.Barline;
import com.xenoage.zong.core.music.direction.Coda;
import com.xenoage.zong.core.music.direction.DaCapo;
import com.xenoage.zong.core.music.direction.NavigationSign;
import com.xenoage.zong.core.music.direction.Segno;
import com.xenoage.zong.core.music.util.BeatEList;
import com.xenoage.zong.core.music.util.Interval;
import com.xenoage.zong.core.position.Time;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.val;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.xenoage.utils.NullUtils.notNull;
import static com.xenoage.utils.collections.CollectionUtils.*;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.utils.kernel.Range.rangeReverse;
import static com.xenoage.utils.math.Fraction._0;
import static com.xenoage.zong.core.position.Time.time;
import static com.xenoage.zong.core.position.Time.time0;
import static java.lang.Math.min;
/**
* Finds the {@link Repetitions} in a score.
*
* @author Andreas Wenger
*/
public class RepetitionsFinder {
//avoid endless loops: when this number of repetitions is reached, the algorithm stops
private static final int maxJumps = 100;
/**
* A jump from a given {@link Time} to a given {@link Time}.
*/
@Const @Data @AllArgsConstructor
public static final class Jump {
public final Time from, to;
}
//state
private Score score;
private List<Jump> jumps = alist();
//voltas
private VoltaGroups voltaGroups;
//repeat counter for all volta groups
private Map<VoltaGroup, Integer> voltaGroupsCounter = map();;
//maps backward repeats (their TimeSignature) to the number of already played repeats
private Map<Time, Integer> barlineRepeatCounter = map();
//list of played navigation signs
private Set<NavigationSign> playedNavigationSigns = set();
//true after a dacapo or segno jump, before we see the next origin navigation sign
private boolean isWithinJumpRepeat = false;
//loop index: index of the current measure
private int currentMeasureIndex;
//where to start in the current measure. null = complete measure
@MaybeNull private Fraction currentMeasureStartBeat;
//con repetizione (true, play repeats) or senza repetizione (false, ignore repeats)
private boolean isWithRepeats = true;
/**
* Finds the {@link Repetitions} in the given score.
*/
public static Repetitions findRepetitions(Score score) {
return new RepetitionsFinder(score).find();
}
private RepetitionsFinder(Score score) {
this.score = score;
}
private Repetitions find() {
ArrayList<Repetition> ranges = alist();
this.voltaGroups = new VoltaGroupFinder(score).findAllVoltaGroups();
Time start = time(0, _0);
Time end = time(score.getMeasuresCount(), _0);
collectJumps();
if (jumps.size() == 0) {
//simple case: no jumps
ranges.add(new Repetition(start, end));
}
else {
//one or more jumps
ranges.add(new Repetition(start, jumps.get(0).from));
for (int i : range(1, jumps.size() - 1)) {
Time lastEnd = jumps.get(i - 1).to;
Time currentStart = jumps.get(i).from;
ranges.add(new Repetition(lastEnd, currentStart));
}
ranges.add(new Repetition(jumps.get(jumps.size() - 1).to, end));
}
return new Repetitions(ranges);
}
/**
* Creates the list of jumps for this score.
*/
private void collectJumps() {
Segno lastSegno = null; //if not null, the next segno will jump back to this one
int lastVoltaCounter = 1; //in the next volta group, jump to this repeat number
Fraction measureStartBeat = null;
nextMeasure: for (currentMeasureIndex = 0; currentMeasureIndex < score.getMeasuresCount();) {
val measure = score.getColumnHeader(currentMeasureIndex);
//are we stuck within an endless loop? then do not jump at all.
if (jumps.size() > maxJumps) {
jumps.clear();
return;
}
//enter a volta
if (voltaGroups.getVoltaGroupStartingAt(currentMeasureIndex) != null)
if (processVolta())
continue nextMeasure;
//inner backward repeat barlines
if (isWithRepeats) {
for (val e : getInnerBarlines()) {
val innerBarline = e.element;
val eTime = time(currentMeasureIndex, e.beat);
if (innerBarline.getRepeat().isBackward())
if (processBackwardRepeat(innerBarline, eTime))
continue nextMeasure;
}
}
//backward repeat at measure end
val endBarline = measure.getEndBarline();
val endTime = time(currentMeasureIndex + 1, _0);
if (isWithRepeats && endBarline != null) {
if (endBarline.getRepeat().isBackward()) {
//ignore it at the end of the final volta of a volta group, otherwise we are stuck in an endless loop
if (false == isFinalVoltaAt(currentMeasureIndex))
if (processBackwardRepeat(endBarline, endTime))
continue nextMeasure;
}
}
//origin navigation sign
//we read them after the backward repeat barlines. e.g. when there is both
//a repeat and a "to coda", we first play the repeat and then the "to coda".
val sign = measure.getNavigationOrigin();
if (sign != null) {
//da capo ,
if (MusicElementType.DaCapo.is(sign))
if (processDaCapo((DaCapo) sign))
continue nextMeasure;
//target segno
if (MusicElementType.Segno.is(sign))
if (processSegno((Segno) sign))
continue nextMeasure;
//to coda
if (MusicElementType.Coda.is(sign))
if (processCoda((Coda) sign))
continue nextMeasure;
}
//no jump found in this measure, continue
currentMeasureIndex++;
currentMeasureStartBeat = null;
}
}
/**
* Processes the given backward repeat {@link Barline} at the given {@link Time}.
* When another repeat is to be played, a {@link Jump} is added to
* the jump list, the current measure and start beat are modified
* and true is returned.
* Otherwise false is returned.
*/
private boolean processBackwardRepeat(Barline barline, Time barlineTime) {
int counter = notNull(barlineRepeatCounter.get(barlineTime), 0);
if (counter < barline.getRepeatTimes()) {
//repeat. jump back to last forward repeat
if (false == isVoltaEndAt(barlineTime)) //do not count repeats at volta end
barlineRepeatCounter.put(barlineTime, counter + 1);
Time to = findLastForwardRepeatTime(barlineTime);
addJump(barlineTime, to);
return true;
}
else {
//finished, delete counter
barlineRepeatCounter.remove(barlineTime);
return false;
}
}
/**
* Processes the given {@link DaCapo} in the current measure.
* When it was not played yet, a {@link Jump} to the beginning is added to
* the jump list,the con repetizione and jump repeat flags are updated,
* the current measure and start beat are modified and true is returned.
* Otherwise false is returned.
*/
private boolean processDaCapo(DaCapo daCapo) {
//each da capo is played only one time to avoid endless repeats
if (false == playedNavigationSigns.contains(daCapo)) {
//jump back to the beginning
playedNavigationSigns.add(daCapo);
isWithRepeats = daCapo.isWithRepeats();
isWithinJumpRepeat = true;
addJump(time(currentMeasureIndex + 1, _0), time0);
return true;
}
else {
return false;
}
}
/**
* Processes the given origin {@link Segno} in the current measure.
* When it was not played yet, and there is an earlier target segno,
* a {@link Jump} to that last target segno is added to
* the jump list, the con repetizione and jump repeat flags are updated,
* the current measure and start beat are modified and true is returned.
* Otherwise false is returned.
*/
private boolean processSegno(Segno segno) {
//each origin segno is played only one time to avoid endless repeats
if (false == playedNavigationSigns.contains(segno)) {
//jump back to last target segno
playedNavigationSigns.add(segno);
isWithRepeats = segno.isWithRepeats();
isWithinJumpRepeat = true;
addJump(time(currentMeasureIndex + 1, _0), time(findSegnoTargetMeasure(), _0));
return true;
}
else {
return false;
}
}
/**
* Processes the given origin "to {@link Coda}" at the current measure.
* When we are within a jump repeat and the coda was not played yet,
* and there is a later target coda,
* a {@link Jump} to that target coda is added to
* the jump list, the con repetizione flag set back to true,
* the jump repeat flag set back to false,
* the current measure and start beat are modified and true is returned.
* Otherwise false is returned.
*/
private boolean processCoda(Coda coda) {
//codas are only played when we are within a repeat caused by a dacapo or segno
if (isWithinJumpRepeat) {
//each origin coda is played only one time to avoid endless repeats
if (false == playedNavigationSigns.contains(coda)) {
//find next target coda
val nextCodaMeasure = findCodaTargetMeasure();
if (nextCodaMeasure > 0) {
//jump forward to the next coda
playedNavigationSigns.add(coda);
isWithRepeats = true; //after coda jump, always con repetizione
isWithinJumpRepeat = false;
addJump(time(currentMeasureIndex + 1, _0), time(nextCodaMeasure, _0));
return true;
}
}
}
return false;
}
/**
* Processes the volta group at the current measure.
* When playing the first volta is not fine, a {@link Jump} into the right volta
* is created, or if nothing suitable is found, a {@link Jump} into to the first measure
* after the volta group is created.
* True is returned, iff a jump was created.
*/
private boolean processVolta() {
val voltaGroup = voltaGroups.getVoltaGroupAt(currentMeasureIndex);
//find next number to play
int nextNumber;
//within a senza repetizione passage, jump into the last number
if (false == isWithRepeats)
nextNumber = voltaGroup.getRepeatCount();
else
nextNumber = notNull(voltaGroupsCounter.get(voltaGroup), 1);
val targetVolta = voltaGroup.findVolta(nextNumber);
if (targetVolta != null) {
//suitable volta found
//update repeat counter
if (voltaGroup.getRepeatCount() == nextNumber)
voltaGroupsCounter.remove(voltaGroup); //last repeat; volta group is then finished
else
voltaGroupsCounter.put(voltaGroup, nextNumber + 1); //next time one number higher
//prepare jump
if (currentMeasureIndex == targetVolta.startMeasure) {
//we are already in the right volta; no jump needed
return false;
}
else {
//create jump into right volta
addJump(time(currentMeasureIndex, _0), time(targetVolta.startMeasure, _0));
return true;
}
}
else {
//no suitable volta found. jump to the end of the volta group
voltaGroupsCounter.remove(voltaGroup);
addJump(time(currentMeasureIndex, _0), time(voltaGroup.endMeasure + 1, _0));
return true;
}
}
/**
* Adds a {@link Jump} to the list of jumps and updates the current
* measure index and start beat accordingly.
*/
private void addJump(Time from, Time to) {
jumps.add(new Jump(from, to));
currentMeasureIndex = to.measure;
currentMeasureStartBeat = to.beat;
}
/**
* Gets the inner {@link Barline}s within of the current measure.
* When the current measure start beat is not null, only barlines after (not at) that beat
* are returned.
*/
private BeatEList<Barline> getInnerBarlines() {
val ret = score.getColumnHeader(currentMeasureIndex).getMiddleBarlines();
if (currentMeasureStartBeat != null)
return ret.filter(Interval.After, currentMeasureStartBeat);
return ret;
}
/**
* Finds the {@link Time} of the last forward repeat (also within measures),
* starting from the given time. If there is none, the beginning of the score is returned.
* This value can not be cached during playback, but must be searched each time when
* needed. For example, imagine a score where a segno jumps into the middle of a
* repeating sequence. When reaching the right backward repeat, the left forward
* repeat should be used, and not the last forward repeat that was visited before
* the segno jump.
* When the backward repeat is at the end of a volta, the first forward repeat before
* the volta group is used.
*/
private Time findLastForwardRepeatTime(Time from) {
//if we are at the end of a volta, find last forward repeat before the volta group
if (isVoltaEndAt(from)) {
val voltaState = voltaGroups.getStateAt(from.measure - 1);
from = time(voltaState.group.startMeasure, _0);
}
//iterate through measures in reverse order
int fromMeasure = min(from.measure, score.getMeasuresCount() - 1); //from.measure could be 1 measure after score
for (int iMeasure : rangeReverse(fromMeasure, 0)) {
val measure = score.getColumnHeader(iMeasure);
//different volta group in this measure? then start repeat after this volta
/* VoltaGroupState voltaState = voltaGroups.getStateAt(iMeasure);
if (voltaState != null && (thisVoltaState == null || thisVoltaState.group != voltaState.group)) {
return time(iMeasure + 1, _0);
} */
//barline within the measure?
val innerBarlines = measure.getMiddleBarlines();
//if starting in this measure, only before the given beat
val innerStartBeat = (iMeasure == from.measure ? from.beat : null);
for (val innerBarline : innerBarlines.reverseIt()) {
if ((innerStartBeat == null || innerBarline.beat.compareTo(innerStartBeat) < 0) &&
innerBarline.getElement().getRepeat().isForward())
return time(iMeasure, innerBarline.beat);
}
//forward repeat at the beginning of the measure? (but not when we started at beat 0 of this measure)
if (false == time(iMeasure, _0).equals(from))
if (measure.getStartBarline() != null && measure.getStartBarline().getRepeat().isForward())
return time(iMeasure, _0);
}
//nothing was found, so return the beginning of the score
return time0;
}
/**
* Finds the measure of the last target {@link Segno} starting from the current measure.
* If there is none, the first measure is returned.
*/
private int findSegnoTargetMeasure() {
//iterate through measures in reverse order
for (int iMeasure : rangeReverse(currentMeasureIndex, 0)) {
val sign = score.getColumnHeader(iMeasure).getNavigationTarget();
if (MusicElementType.Segno.is(sign))
return iMeasure;
}
//nothing was found, jump to the beginning of the score
return 0;
}
/**
* Finds the measure of the next target {@link Coda} starting from the current measure.
* If there is none, -1 is returned.
*/
private int findCodaTargetMeasure() {
//iterate through measures
for (int iMeasure : range(currentMeasureIndex, score.getMeasuresCount() - 1)) {
val sign = score.getColumnHeader(iMeasure).getNavigationTarget();
if (MusicElementType.Coda.is(sign))
return iMeasure;
}
//nothing was found
return -1;
}
/**
* Returns true, if the given time is the end of a volta.
*/
private boolean isVoltaEndAt(Time time) {
val volta = voltaGroups.getStateAt(time.measure - 1);
return volta != null && volta.voltaEndMeasure == time.measure - 1 && time.beat.equals(_0);
}
/**
* Returns true, if the given measure is the last repetition of a volta group.
*/
private boolean isFinalVoltaAt(int measure) {
val voltaState = voltaGroups.getStateAt(measure);
if (voltaState != null) {
val finalVolta = voltaState.group.findVolta(voltaState.group.getRepeatCount());
if (finalVolta.volta == voltaState.volta && voltaState.voltaEndMeasure == measure)
return true;
}
return false;
}
}