package com.xenoage.zong.core.music;
import com.xenoage.utils.annotations.MaybeEmpty;
import com.xenoage.utils.collections.SortedList;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.music.util.FirstOrLast;
import com.xenoage.zong.core.music.util.IndexE;
import com.xenoage.zong.core.music.util.Interval;
import com.xenoage.zong.core.music.util.StartOrStop;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.core.position.MPContainer;
import com.xenoage.zong.core.position.MPElement;
import com.xenoage.zong.utils.exceptions.IllegalMPException;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.utils.math.Fraction.fr;
import static com.xenoage.zong.core.music.util.FirstOrLast.First;
import static com.xenoage.zong.core.music.util.FirstOrLast.Last;
import static com.xenoage.zong.core.music.util.IndexE.indexE;
import static com.xenoage.zong.core.music.util.Interval.Result.FalseHigh;
import static com.xenoage.zong.core.music.util.Interval.Result.True;
import static com.xenoage.zong.core.music.util.StartOrStop.Start;
import static com.xenoage.zong.core.music.util.StartOrStop.Stop;
import static com.xenoage.zong.core.position.MP.atVoice;
/**
* Voice in a single measure within a single staff.
*
* A voice contains musical elements like chords and rests.
*
* All these elements have durations greater than 0.
* Grace chords are attached to their main chords.
*
* @author Andreas Wenger
*/
public final class Voice
implements MPContainer {
/** The list of rests and chords, sorted by time */
@MaybeEmpty @Getter private List<VoiceElement> elements;
/** Back reference: the parent measure of this voice, or null if not part of a measure. */
@Getter @Setter private Measure parent = null;
/**
* Creates a new {@link Voice}.
* @param elements the elements, sorted by time
*/
public Voice(List<VoiceElement> elements) {
this.elements = elements;
}
/**
* Creates an empty voice.
*/
public static Voice voice() {
return new Voice(new ArrayList<>(0));
}
/**
* Creates an empty voice with the given parent measure.
*/
public static Voice voice(Measure parent) {
Voice voice = voice();
voice.setParent(parent);
return voice;
}
/**
* Gets the element with the given index, or throws an
* {@link IllegalMPException} if there is none.
*/
public VoiceElement getElement(int index) {
if (index >= 0 && index <= elements.size())
return elements.get(index);
else
throw new IllegalMPException(atVoice(index));
}
/**
* Gets the {@link FirstOrLast} element, which {@link StartOrStop}s within
* the given {@link Interval} relative to the given element index, or null if there is none.
*/
public IndexE<VoiceElement> getElement(FirstOrLast side, StartOrStop border, Interval interval, int elementIndex) {
if (isEmpty())
return null;
int pos = 0;
if (border == Start) {
if (side == First) {
for (VoiceElement e : elements) {
Interval.Result r = interval.isInInterval(pos, elementIndex);
if (r == True)
return indexE(e, pos);
else if (r == FalseHigh)
return null; //gone to far
pos++;
}
return null;
}
else if (side == Last) {
IndexE<VoiceElement> ret = null;
for (VoiceElement e : elements) {
Interval.Result r = interval.isInInterval(pos, elementIndex);
if (r == True)
ret = indexE(e, pos);
pos++;
}
return ret;
}
}
else if (border == Stop) {
if (side == First) {
for (VoiceElement e : elements) {
Interval.Result r = interval.isInInterval(pos + 1, elementIndex);
if (r == True)
return indexE(e, pos);
else if (r == FalseHigh)
return null; //gone to far
pos++;
}
return null;
}
else if (side == Last) {
IndexE<VoiceElement> ret = null;
for (VoiceElement e : elements) {
Interval.Result r = interval.isInInterval(pos + 1, elementIndex);
if (r == True)
ret = indexE(e, pos);
pos++;
}
return ret;
}
}
throw new IllegalArgumentException("Unknown parameters");
}
/**
* Adds the given element at the end of this voice.
*/
public void addElement(VoiceElement element) {
element.setParent(this);
elements.add(element);
}
/**
* Adds the given element at the given position within this voice.
*/
public void addElement(int index, VoiceElement element) {
element.setParent(this);
elements.add(index, element);
}
/**
* Replaces the element with the given index by the given one.
*/
public void replaceElement(int index, VoiceElement element) {
element.setParent(this);
elements.set(index, element);
}
/**
* Replaces the given element by the given ones.
*/
public void replaceElement(VoiceElement oldElement, VoiceElement... newElements) {
int index = elements.indexOf(oldElement);
if (index == -1)
throw new IllegalArgumentException("Given element is not part of this voice.");
for (VoiceElement newElement : newElements) {
elements.add(index, newElement);
index++;
}
}
/**
* Removes the element with the given index.
*/
public void removeElement(int index) {
elements.remove(index);
}
/**
* Removes the given element.
* If found, its index is returned, otherwise -1.
*/
public int removeElement(VoiceElement element) {
for (int i : range(elements)) {
if (elements.get(i) == element) {
elements.remove(i);
return i;
}
}
return -1;
}
/**
* Gets the last used beat less than or equal the given one.
* If there are no elements, 0 is returned.
*/
public Fraction getLastUsedBeat(Fraction maxBeat) {
Fraction beat = fr(0);
for (VoiceElement e : elements) {
Fraction pos = beat.add(e.getDuration());
if (pos.compareTo(maxBeat) > 0)
break;
else
beat = pos;
}
return beat;
}
/**
* Returns true, if the given beat is the starting
* beat of an element within this voice, beat 0,
* or the empty beat behind the last element.
*/
public boolean isBeatUsed(Fraction beat) {
//all measures start with beat 0
if (beat.getNumerator() == 0)
return true;
//is there an element at this beat?
Fraction curBeat = fr(0);
for (VoiceElement e : elements) {
if (beat.equals(curBeat))
return true;
curBeat = curBeat.add(e.getDuration());
}
//first unused (empty) beat
if (beat.equals(curBeat))
return true;
return false;
}
/**
* Returns true, if this voice contains no elements.
*/
public boolean isEmpty() {
return elements.size() == 0;
}
/**
* Gets the filled beats in this voice, that means, the first beat in this
* voice where the is no music element following any more.
*/
public Fraction getFilledBeats() {
Fraction ret = Fraction._0;
for (VoiceElement e : elements)
ret = ret.add(e.getDuration());
return ret;
}
/**
* Gets the last element at the given beat.
* That means, that if the beat starts with grace elements followed
* by a full element, the full element is returned.
* If no element starts at exactly the given beat, null is returned.
*/
public VoiceElement getElementAt(Fraction beat) {
Fraction currentBeat = Fraction._0;
VoiceElement foundElement = null;
for (VoiceElement e : elements) {
int compare = beat.compareTo(currentBeat);
if (compare == 0)
foundElement = e;
else if (compare < 0)
break;
currentBeat = currentBeat.add(e.getDuration());
}
return foundElement;
}
/**
* Gets a list of all beats used in this voice, that means
* all beats where at least one element with a duration greater than 0 begins.
* Beat 0 is always used.
*/
public SortedList<Fraction> getUsedBeats() {
SortedList<Fraction> ret = new SortedList<>(false);
Fraction currentBeat = Fraction._0;
ret.add(currentBeat);
for (VoiceElement e : elements) {
Fraction duration = e.getDuration();
if (duration != null && duration.getNumerator() > 0) {
if (!currentBeat.equals(ret.getLast()))
ret.add(currentBeat);
currentBeat = currentBeat.add(duration);
}
}
return ret;
}
/**
* Gets a list of the elements in this voice, beginning at or after
* the given start beat and before the given end beat.
*/
public LinkedList<VoiceElement> getElementsInRange(Fraction startBeat, Fraction endBeat) {
LinkedList<VoiceElement> ret = new LinkedList<>();
//collect elements
Fraction beat = Fraction._0;
for (VoiceElement e : elements) {
if (beat.compareTo(endBeat) >= 0)
break;
else if (beat.compareTo(startBeat) > 0 || (beat.compareTo(startBeat) == 0 && e.getDuration().isGreater0()))
ret.add(e);
beat = beat.add(e.getDuration());
}
return ret;
}
@Override public String toString() {
return elements.toString();
}
/**
* Gets the start beat of the given element.
* If the element is not in this voice, null is returned.
*/
public Fraction getBeat(MusicElement element) {
Fraction beat = Fraction._0;
for (VoiceElement e : elements) {
if (e == element)
return beat;
else
beat = beat.add(e.getDuration());
}
return null;
}
/**
* Gets the start beat of the element with the given index.
* If the index is greater than the number of elements, the beat after
* the last element is returned (or 0 if the voice is empty).
*/
public Fraction getBeat(int elementIndex) {
Fraction beat = Fraction._0;
for (int i : range(elements)) {
if (i >= elementIndex)
break;
beat = beat.add(elements.get(i).getDuration());
}
return beat;
}
/**
* Gets the index of the last element that starts before or at
* the given beat. If the given beat is after the last element,
* the index of the last element + 1 is returned (or 0 if the voice is empty).
*/
public int getElementIndex(Fraction beat) {
int posI = 0;
Fraction posB = Fraction._0;
for (int i : range(elements)) {
Fraction newPosB = posB.add(elements.get(i).getDuration());
if (newPosB.compareTo(beat) > 0)
break;
posB = newPosB;
posI++;
}
return posI;
}
@Override public MP getChildMP(MPElement element) {
if (parent == null)
return null;
Fraction beat = getBeat(element);
if (beat == null)
return null;
MP mp = parent.getMP(this);
mp = mp.withBeat(beat);
return mp;
}
/**
* Convenience method. Gets the parent score of this voice,
* or null, if this voice is not part of a score.
*/
public Score getScore() {
return (parent != null ? parent.getScore() : null);
}
}