package com.xenoage.zong.core.music;
import com.xenoage.utils.annotations.NonEmpty;
import com.xenoage.utils.annotations.Untested;
import com.xenoage.utils.collections.SortedList;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.music.chord.Chord;
import com.xenoage.zong.core.music.chord.Note;
import com.xenoage.zong.core.music.clef.Clef;
import com.xenoage.zong.core.music.direction.Direction;
import com.xenoage.zong.core.music.direction.DirectionContainer;
import com.xenoage.zong.core.music.key.Key;
import com.xenoage.zong.core.music.util.BeatE;
import com.xenoage.zong.core.music.util.BeatEList;
import com.xenoage.zong.core.music.util.Interval;
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 lombok.val;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.xenoage.utils.CheckUtils.checkArgsNotNull;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.zong.core.music.Voice.voice;
import static com.xenoage.zong.core.music.util.BeatEList.beatEList;
import static com.xenoage.zong.core.music.util.BeatEList.emptyBeatEList;
import static com.xenoage.zong.core.music.util.Interval.*;
import static com.xenoage.zong.core.position.MP.atVoice;
/**
* Measure within a single staff.
*
* A measure consists of one or more voices and a
* list of clefs, private keys (keys that only apply to
* this staff), directions and instrument changes.
*
* @author Andreas Wenger
*/
public class Measure
implements MPContainer, DirectionContainer {
/** The list of voices (at least one). */
@Getter @NonEmpty private List<Voice> voices;
/** The list of clefs, or null. */
@Getter private BeatEList<Clef> clefs;
/** The list of staff-intern key signature changes, or null. */
@Getter private BeatEList<Key> privateKeys;
/** The list of directions, or null. Use {@link #getDirections()} to get a non-null value. */
private BeatEList<Direction> directions;
/** The list of instrument changes, or null. */
@Getter private BeatEList<InstrumentChange> instrumentChanges;
/** Back reference: the parent staff, or null if not part of a staff. */
@Getter @Setter private Staff parent = null;
public Measure(List<Voice> voices, BeatEList<Clef> clefs, BeatEList<Key> privateKeys, BeatEList<Direction> directions,
BeatEList<InstrumentChange> instrumentChanges) {
checkArgsNotNull(voices);
if (voices.size() == 0)
throw new IllegalArgumentException("A measure must have at least one voice");
for (Voice voice : voices)
voice.setParent(this);
this.voices = voices;
this.clefs = clefs;
this.privateKeys = privateKeys;
this.directions = directions;
this.instrumentChanges = instrumentChanges;
}
/**
* Creates an empty measure.
*/
public static Measure measure() {
return new Measure(alist(Voice.voice()), null, null, null, null);
}
/**
* Adds a clef at the given beat or removes it, when null is given.
* If there is already one, it is replaced and returned (otherwise null).
*/
public Clef setClef(Clef clef, Fraction beat) {
if (clef != null) {
//add clef to list. create list if needed
clef.setParent(this);
if (clefs == null)
clefs = beatEList();
return clefs.set(clef, beat);
}
else if (clefs != null) {
//remove clef from list. delete list if not needed any more.
Clef ret = clefs.remove(beat);
if (clefs.size() == 0)
clefs = null;
return ret;
}
return null;
}
/**
* Adds a key at the given beat. If there is already one, it is replaced
* and returned (otherwise null).
*/
@Untested public Key setKey(Key key, Fraction beat) {
if (key != null) {
//add key to list. create list if needed
key.setParent(this);
if (privateKeys == null)
privateKeys = beatEList();
return privateKeys.set(key, beat);
}
else if (privateKeys != null) {
//remove key from list. delete list if not needed any more.
Key ret = privateKeys.remove(beat);
if (privateKeys.size() == 0)
privateKeys = null;
return ret;
}
return null;
}
/**
* Adds a direction at the given beat. If there is already one, it is not
* replaced, since there may be many directions belonging to a single beat.
*/
@Untested public void addDirection(Direction direction, Fraction beat) {
direction.setParent(this);
if (directions == null)
directions = beatEList();
directions.add(direction, beat);
}
/**
* Adds an instrument change at the given beat.
* If there is already one, it is replaced and returned (otherwise null).
*/
@Untested public InstrumentChange setInstrumentChange(InstrumentChange instrumentChange, Fraction beat) {
if (instrumentChange != null) {
//add instrumentChange to list. create list if needed
instrumentChange.setParent(this);
if (instrumentChanges == null)
instrumentChanges = beatEList();
return instrumentChanges.set(instrumentChange, beat);
}
else if (instrumentChanges != null) {
//remove instrumentChange from list. delete list if not needed any more.
InstrumentChange ret = instrumentChanges.remove(beat);
if (instrumentChanges.size() == 0)
instrumentChanges = null;
return ret;
}
return null;
}
/**
* Adds the given {@link MeasureElement} at the given beat. Dependent on its type,
* it may replace elements of the same type, which is then returned (otherwise null).
* See the documentation for the methods working with specific {@link MeasureElement}s.
*/
@Untested public MeasureElement addMeasureElement(MeasureElement element, Fraction beat) {
if (element instanceof Clef)
return setClef((Clef) element, beat);
else if (element instanceof Key)
return setKey((Key) element, beat);
else if (element instanceof Direction) {
addDirection((Direction) element, beat);
return null;
}
else if (element instanceof InstrumentChange)
return setInstrumentChange((InstrumentChange) element, beat);
else
throw new IllegalArgumentException("Unknown MeasureElement subclass: " + element.getClass().getName());
}
/**
* Removes the given {@link MeasureElement}.
*/
@Untested public void removeMeasureElement(MeasureElement element) {
if (element instanceof Clef)
clefs.remove((Clef) element);
else if (element instanceof Key)
privateKeys.remove((Key) element);
else if (element instanceof Direction)
directions.remove((Direction) element);
else if (element instanceof InstrumentChange)
instrumentChanges.remove((InstrumentChange) element);
else
throw new IllegalArgumentException("Unknown MeasureElement subclass: " + element.getClass().getName());
}
/**
* Replaces the given {@link MeasureElement} at the given beat with the other given one.
*/
@Untested public <T extends MeasureElement> void replaceMeasureElement(T oldElement, T newElement, Fraction beat) {
if (oldElement instanceof Direction) {
directions.remove((Direction) oldElement);
directions.add((Direction) newElement, beat);
}
else {
//all other cases are like addMeasureElement
addMeasureElement(newElement, beat);
}
}
/**
* Collect the accidentals within this measure (backwards),
* beginning at the given start beat where the given key is valid, ending before or at
* the given beat (depending on the given interval), looking at all voices.
* The private keys of this measure are ignored. They must be queried before and
* used for the last two parameters.
*
* @param beat the maximum beat (inclusive if exclusive, depending on the interval)
* @param interval where to stop looking ({@link Interval#Before} or
* {@link Interval#BeforeOrAt}). {@link Interval#At} is
* handled like {@link Interval#BeforeOrAt}.
* @param startBeat the beat where to start collecting accidentals (if there are
* no private keys in this measure before the given beat, this
* is always 0).
* @param startBeatKey the key that is valid at the given start beat
* @return a map with the pitches that have accidentals (without alter)
* as keys and their corresponding alter values as values.
*/
@Untested public Map<Pitch, Integer> getAccidentals(Fraction beat, Interval interval, Fraction startBeat, Key startBeatKey) {
if (!(interval == Before || interval == BeforeOrAt || interval == At)) {
throw new IllegalArgumentException("Illegal interval for this method: " + interval);
}
if (interval == At) {
interval = BeforeOrAt;
}
Map<Pitch, Integer> ret = new HashMap<>();
Map<Pitch, Fraction> retBeats = new HashMap<>();
for (Voice voice : voices) {
Fraction pos = startBeat;
for (VoiceElement e : voice.getElements()) {
if (pos.compareTo(startBeat) < 0) {
pos = pos.add(e.getDuration());
continue;
}
if (interval.isInInterval(pos, beat) != Interval.Result.True) {
break;
}
if (e instanceof Chord) {
Chord chord = (Chord) e;
for (Note note : chord.getNotes()) {
Pitch pitch = note.getPitch();
Pitch pitchUnaltered = pitch.withoutAlter();
//accidental already set?
Integer oldAccAlter = ret.get(pitchUnaltered);
if (oldAccAlter != null) {
//there is already an accidental. only replace it if alter changed
//and if it is at a later position than the already found one
Fraction existingBeat = retBeats.get(pitch);
if (pitch.getAlter() != oldAccAlter && pos.compareTo(existingBeat) > 0) {
ret.put(pitchUnaltered, (int) pitch.getAlter());
retBeats.put(pitchUnaltered, pos);
}
}
else {
//accidental not neccessary because of key?
if (startBeatKey.getAlterations()[pitch.getStep()] == pitch.getAlter()) {
//ok, we need no accidental here.
}
else {
//add accidental
ret.put(pitchUnaltered, (int) pitch.getAlter());
retBeats.put(pitchUnaltered, pos);
}
}
}
}
pos = pos.add(e.getDuration());
}
}
return ret;
}
/**
* Gets a list of all beats used in this measure, that means
* all beats where at least one element with a duration greater than 0 begins.
* Beat 0 is always used.
* @param withMeasureElements true, iff also the beats of the measure elements should be used
*/
public SortedList<Fraction> getUsedBeats(boolean withMeasureElements) {
SortedList<Fraction> ret = new SortedList<>(false);
//voice element beats
for (Voice voice : voices) {
SortedList<Fraction> voiceBeats = voice.getUsedBeats();
ret = ret.merge(voiceBeats, false);
}
//beats of directions
if (withMeasureElements) {
for (val dir : getMeasureElements())
ret.add(dir.beat);
}
return ret;
}
/**
* Gets the filled beats in this measure, that
* means, the first beat in this measure where there is no music
* element following any more.
*/
public Fraction getFilledBeats() {
Fraction maxBeat = Fraction._0;
for (Voice voice : voices) {
Fraction beat = voice.getFilledBeats();
if (beat.compareTo(maxBeat) > 0)
maxBeat = beat;
}
return maxBeat;
}
/**
* Gets the voice with the given index, or throws an
* {@link IllegalMPException} if there is none.
*/
public Voice getVoice(int index) {
if (index >= 0 && index <= voices.size())
return voices.get(index);
else
throw new IllegalMPException(atVoice(index));
}
/**
* Gets the voice with the given index, or throws an
* {@link IllegalMPException} if there is none.
* Only the voice index of the given position is relevant.
*/
public Voice getVoice(MP mp) {
int index = mp.voice;
if (index >= 0 && index < voices.size())
return voices.get(index);
else
throw new IllegalMPException(mp);
}
/**
* Sets the voice with the given index.
* If the voice does not exist yet, it is created.
*/
public void setVoice(int index, Voice voice) {
while (index >= voices.size()) {
Voice voiceFill = voice();
voiceFill.setParent(this);
voices.add(voiceFill);
}
voice.setParent(this);
voices.set(index, voice);
}
/**
* Gets a list of all {@link MeasureElement},s sorted by beat,
* and within beat sorted by clef, key, directions, instrument change.
*/
public BeatEList<MeasureElement> getMeasureElements() {
BeatEList<MeasureElement> ret = beatEList();
ret.addAll(clefs);
ret.addAll(privateKeys);
ret.addAll(directions);
ret.addAll(instrumentChanges);
return ret;
}
/**
* Gets the MP of the given {@link Voice}, or null if it is not part
* of this measure or this measure is not part of a score.
*/
public MP getMP(Voice voice) {
int voiceIndex = voices.indexOf(voice);
if (parent == null || voiceIndex == -1)
return null;
MP mp = parent.getMP(this);
mp = mp.withVoice(voiceIndex);
return mp;
}
/**
* Gets the MP of the given {@link ColumnElement}, or null if it is not part
* of this measure or this measure is not part of a score.
*/
@Override public MP getChildMP(MPElement element) {
if (parent == null)
return null;
MP mp = parent.getMP(this);
if (element instanceof Clef)
return getMPIn(element, clefs, mp);
else if (element instanceof Key)
return getMPIn(element, privateKeys, mp);
else if (element instanceof Direction)
return getMPIn(element, directions, mp);
else if (element instanceof InstrumentChange)
return getMPIn(element, instrumentChanges, mp);
return null;
}
/**
* Gets the {@link MP} of the given element within the given list of elements,
* based on the given {@link MP} (staff, measure), or null if the list of elements
* is null or the element could not be found.
*/
private MP getMPIn(MPElement element, BeatEList<?> elements, MP baseMP) {
if (elements == null)
return null;
for (BeatE<?> e : elements)
if (e.getElement() == element)
return MP.atBeat(baseMP.staff, baseMP.measure, baseMP.voice, e.getBeat());
return null;
}
/**
* Convenience method. Gets the parent score of this voice,
* or null, if this element is not part of a score.
*/
public Score getScore() {
return (parent != null ? parent.getScore() : null);
}
/**
* The list of directions, maybe empty and immutable.
*/
public BeatEList<Direction> getDirections() {
if (directions == null)
return emptyBeatEList();
return directions;
}
}