package com.xenoage.zong.core.music.chord;
import com.xenoage.utils.annotations.MaybeNull;
import com.xenoage.utils.annotations.NonEmpty;
import com.xenoage.utils.annotations.NonNull;
import com.xenoage.utils.annotations.Optimized;
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.Pitch;
import com.xenoage.zong.core.music.Voice;
import com.xenoage.zong.core.music.VoiceElement;
import com.xenoage.zong.core.music.annotation.Annotation;
import com.xenoage.zong.core.music.annotation.Articulation;
import com.xenoage.zong.core.music.beam.Beam;
import com.xenoage.zong.core.music.direction.Direction;
import com.xenoage.zong.core.music.direction.DirectionContainer;
import com.xenoage.zong.core.music.lyric.Lyric;
import com.xenoage.zong.core.music.slur.Slur;
import com.xenoage.zong.core.music.tuplet.Tuplet;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.core.position.MPElement;
import com.xenoage.zong.core.util.InconsistentScoreException;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import static com.xenoage.utils.CheckUtils.checkArgsNotNull;
import static com.xenoage.utils.annotations.Optimized.Reason.MemorySaving;
import static com.xenoage.utils.collections.CollectionUtils.addOrNew;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.utils.math.Fraction._0;
import static java.util.Collections.emptyList;
/**
* Class for a chord.
*
* To make things easy, every note is in a chord.
* Thus also single notes are chord elements by definition.
*
* A chord can have a stem and articulations.
* It may be a normal chord (default case), a cue chord (printed small,
* but has a duration like normal chords) or a grace chord, which duration
* is 0 (the grace duration is saved in the {@link Grace} class).
*
* A chord can be part of a tuplet, a beam, one or more slurs and
* and one or more lyrics and directions can be attached to it.
*
* @author Andreas Wenger
*/
@Getter @Setter
public class Chord
implements VoiceElement, DirectionContainer {
/** The notes within this chord, sorted ascending (begin with lowest notated pitch). */
@NonNull @NonEmpty private List<Note> notes;
/** The duration of this chord. For a grace chord, this is 0. */
@NonNull private Fraction duration;
/** The stem of this chord. */
@NonNull private Stem stem = Stem.defaultStem;
/** True, if this chord has cue size, otherwise false. */
private boolean cue = false;
/** The grace value of this chord, or null if it is a normal chord. */
@MaybeNull private Grace grace = null;
/** The articulation, ornament and other annotations on this chord,
* sorted by ascending distance to the chord. The empty list may be immutable. */
@NonNull @Optimized(MemorySaving) private List<Annotation> annotations = emptyList();
/** The beam this chord is part of, or null. */
@MaybeNull private Beam beam = null;
/** The slurs which start or end at this chord. The empty list may be immutable. */
@NonNull @Optimized(MemorySaving) private List<Slur> slurs = emptyList();
/** The tuplet this chord is part of, or null. */
@MaybeNull private Tuplet tuplet = null;
/** The lyrics attached to this chord. The empty list may be immutable. */
@NonNull @Optimized(MemorySaving) private List<Lyric> lyrics = emptyList();
/** The directions attached to this chord. The empty list may be immutable. */
@NonNull @Optimized(MemorySaving) private List<Direction> directions = emptyList();
/** Back reference: the parent voice, or null if not part of a score. */
@Getter @Setter private Voice parent = null;
/**
* Creates a chord with the given note and duration.
*/
public Chord(Note note, Fraction duration) {
checkArgsNotNull(note, duration);
this.notes = new ArrayList<>(1);
this.notes.add(note);
this.duration = duration;
}
/**
* Creates a chord with the given notes and duration.
* The pitches must be sorted ascending (begin with the lowest notated pitch,
* end with the highest notated pitch), otherwise an {@link IllegalArgumentException} is thrown.
*/
public Chord(ArrayList<Note> notes, Fraction duration) {
checkArgsNotNull(notes, duration);
checkNotesOrder(notes);
if (false == duration.isGreater0())
throw new InconsistentScoreException("Only grace chords may not have 0 duration");
this.notes = notes;
this.duration = duration;
}
/**
* Creates a grace chord with the given notes.
* The pitches must be sorted ascending (begin with the lowest notated pitch,
* end with the highest notated pitch), otherwise an {@link IllegalArgumentException} is thrown.
*/
public Chord(ArrayList<Note> notes, Grace grace) {
checkArgsNotNull(notes, grace);
checkNotesOrder(notes);
if (false == grace.getGraceDuration().isGreater0())
throw new InconsistentScoreException("Grace duration must be greater than 0");
this.notes = notes;
this.duration = _0;
this.grace = grace;
}
/**
* Collects and returns all pitches of this chord.
* Pitches that are used more times are also used more
* times in the list.
*/
public List<Pitch> getPitches() {
List<Pitch> ret = alist(notes.size());
for (int i : range(notes)) {
ret.add(notes.get(i).getPitch());
}
return ret;
}
/**
* Adds a pitch this chord.
*/
public void addPitch(Pitch pitch) {
addNote(new Note(pitch));
}
/**
* Adds a note this chord.
*/
public void addNote(Note note) {
//insert at right position
int i = 0;
for (; i < notes.size(); i++) {
if (notes.get(i).getPitch().compareToNotation(note.getPitch()) > 0)
break;
}
notes.add(i, note);
}
private void checkNotesOrder(ArrayList<Note> notes) {
Pitch currentPitch = null;
Pitch lastPitch = notes.get(0).getPitch();
for (int i : range(1, notes.size() - 1)) {
currentPitch = notes.get(i).getPitch();
//pitches must be sorted ascending
if (currentPitch.compareToNotation(lastPitch) < 0)
throw new IllegalArgumentException("Pitches must be sorted ascending (notation order)!");
lastPitch = currentPitch;
}
}
/**
* Gets the displayed duration of this chord. For full chords, this method returns the
* same value as {@link #getDuration()}. For grace notes, the value of
* {@link Grace#getGraceDuration()} is returned.
*/
public Fraction getDisplayedDuration() {
return (grace == null ? duration : grace.getGraceDuration());
}
/**
* Returns true, if this chord is a grace chord.
*/
public boolean isGrace() {
return grace != null;
}
public void addDirection(Direction direction) {
directions = addOrNew(directions, direction);
}
@Override public String toString() {
return "chord(" + notes.get(0).toString() + (notes.size() > 1 ? ",..." : "") +
(duration.isGreater0() ? ";dur:" + duration : ";grace") + ")";
}
@Override public MP getChildMP(MPElement child) {
//all children have the same musical position as this chord
return MP.getMP(this);
}
/**
* Gets the articulations on this chord, in ascending distance to the chord.
*/
public List<Articulation> getArticulations() { //TIDY: this method should not be needed
ArrayList<Articulation> ret = alist();
for (Annotation a : annotations) {
if (a instanceof Articulation)
ret.add((Articulation) a);
}
return ret;
}
/**
* Convenience method. Gets the parent score of this chord,
* or null, if this chord is not part of a score.
*/
public Score getScore() {
return (parent != null ? parent.getScore() : null);
}
@Override public MusicElementType getMusicElementType() {
return MusicElementType.Chord;
}
@Override public MP getMP() {
return MP.getMPFromParent(this);
}
}