package com.xenoage.zong.core;
import com.xenoage.utils.annotations.NonNull;
import com.xenoage.utils.collections.SortedList;
import com.xenoage.utils.document.Document;
import com.xenoage.utils.document.command.CommandPerformer;
import com.xenoage.utils.document.io.SupportedFormats;
import com.xenoage.utils.math.Fraction;
import com.xenoage.utils.math.MathUtils;
import com.xenoage.zong.core.format.ScoreFormat;
import com.xenoage.zong.core.header.ColumnHeader;
import com.xenoage.zong.core.header.ScoreHeader;
import com.xenoage.zong.core.info.ScoreInfo;
import com.xenoage.zong.core.music.*;
import com.xenoage.zong.core.music.clef.Clef;
import com.xenoage.zong.core.music.clef.ClefType;
import com.xenoage.zong.core.music.key.Key;
import com.xenoage.zong.core.music.key.TraditionalKey;
import com.xenoage.zong.core.music.key.TraditionalKey.Mode;
import com.xenoage.zong.core.music.util.*;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.utils.exceptions.IllegalMPException;
import lombok.Getter;
import lombok.Setter;
import lombok.val;
import java.util.HashMap;
import java.util.Map;
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.utils.math.Fraction.fr;
import static com.xenoage.zong.core.header.ScoreHeader.scoreHeader;
import static com.xenoage.zong.core.music.MusicContext.noAccidentals;
import static com.xenoage.zong.core.music.util.BeatE.selectLatest;
import static com.xenoage.zong.core.music.util.Interval.At;
import static com.xenoage.zong.core.music.util.Interval.BeforeOrAt;
import static com.xenoage.zong.core.music.util.MPE.mpE;
import static com.xenoage.zong.core.position.MP.atMeasure;
import static com.xenoage.zong.core.position.MP.mpb0;
/**
* This class contains a single piece of written music.
*
* @author Andreas Wenger
*/
public final class Score
implements Document {
/** General information about the score. */
@Getter @Setter @NonNull private ScoreInfo info = new ScoreInfo();
/** The default formats of the score. */
@Getter @Setter @NonNull private ScoreFormat format = new ScoreFormat();
/** The list of elements that are normally used in all staves, like time signatures and key signatures. */
@Getter @Setter @NonNull private ScoreHeader header = scoreHeader(this);
/** The list of staves, parts and groups. */
@Getter @Setter @NonNull private StavesList stavesList = new StavesList();
/** Additional meta information. This other application-dependend meta-information
* can be used for example to store page layout information, which is not part
* of the musical score in this project. */
@Getter private Map<String, Object> metaData = new HashMap<>();
/** Performs commands on this score and supports undo. */
@Getter private CommandPerformer commandPerformer = new CommandPerformer(this);
/** Supported formats for reading scores from files and writing them to files. */
@Getter private SupportedFormats<Score> supportedFormats = null;
public Score() {
stavesList.setScore(this);
}
/**
* Gets the number of staves.
*/
public int getStavesCount() {
return stavesList.getStaves().size();
}
/**
* Sets the given meta-data information.
*/
public void setMetaData(String key, Object value) {
metaData.put(key, value);
}
/**
* Gets the measure column header at the given measure.
*/
public ColumnHeader getColumnHeader(int measureIndex) {
return header.getColumnHeader(measureIndex);
}
/**
* Gets the measure at the given {@link BMP}.
*/
public Measure getMeasure(MP mp) {
return stavesList.getMeasure(mp);
}
/**
* Gets the number of measures.
*/
public int getMeasuresCount() {
return header.getColumnHeaders().size();
}
/**
* Gets the staff with the given index.
*/
public Staff getStaff(int staffIndex) {
return stavesList.getStaff(staffIndex);
}
/**
* Gets the staff at the given {@link MP}.
*/
public Staff getStaff(MP mp) {
return stavesList.getStaff(mp);
}
/**
* Gets the voice at the given {@link MP}.
*/
public Voice getVoice(MP mp) {
return stavesList.getVoice(mp);
}
/**
* Gets the interline space for the staff with the given index.
* If unknown, the default value is returned.
*/
public float getInterlineSpace(int staffIndex) {
Float is = getStaff(staffIndex).getInterlineSpace();
if (is != null)
return is;
else
return format.getInterlineSpace();
}
/**
* Gets the biggest interline space of the score.
*/
public float getMaxInterlineSpace() {
float ret = 0;
for (int iStaff : range(getStavesCount()))
ret = Math.max(ret, getInterlineSpace(iStaff));
return ret;
}
/**
* Returns true, if the given {@link MP} is in a valid range, otherwise false.
*/
public boolean isMPExisting(MP mp) {
try {
if (mp.staff < 0 || mp.staff >= getStavesCount() || mp.measure < 0 || mp.measure >= getMeasuresCount() ||
mp.voice < 0 || mp.voice >= getMeasure(mp).getVoices().size())
return false;
Voice voice = getVoice(mp);
if (mp.element != MP.unknown && voice.getElements().size() <= mp.element)
return false;
if (mp.beat != MP.unknownBeat && (mp.beat.compareTo(_0) < 0 ||
mp.beat.compareTo(voice.getParent().getFilledBeats()) > 0))
return false;
return true;
} catch (IllegalMPException ex) {
return false;
}
}
/**
* Gets the number of beats in the given measure column.
* If a time signature is defined, its beats are returned.
* If the time signature is unknown (senza-misura), the beats of the
* voice with the most beats are returned.
*/
public Fraction getMeasureBeats(int measureIndex)
{
Fraction ret = header.getTimeAtOrBefore(measureIndex).getType().getMeasureBeats();
if (ret != null)
return ret;
else
return getMeasureFilledBeats(measureIndex);
}
/**
* Gets the filled beats for the given measure column, that
* means, the first beat in each column where there is no music
* element following any more.
* The given measure may be one measure after the last measure. There, only beat 0
* exists to mark the ending of the score.
*/
public Fraction getMeasureFilledBeats(int measureIndex) {
if (measureIndex == getMeasuresCount())
return _0;
Fraction maxBeat = Fraction._0;
for (Staff staff : stavesList.getStaves()) {
Fraction beat = staff.getMeasures().get(measureIndex).getFilledBeats();
if (beat.compareTo(maxBeat) > 0)
maxBeat = beat;
}
return maxBeat;
}
/**
* Gets the used beats within the given measure column.
* The given measure may be one measure after the last measure. There, only beat 0
* exists to mark the ending of the score.
* @param measureIndex the index of the measure column
* @param withMeasureAndColumnElements true, iff also the beats of column elements and
* measure elements should be used
*/
public SortedList<Fraction> getMeasureUsedBeats(int measureIndex, boolean withMeasureAndColumnElements) {
//last measure?
if (measureIndex == getMeasuresCount())
return new SortedList<>(new Fraction[]{_0}, false);
//add measure beats
SortedList<Fraction> columnBeats = new SortedList<>(false);
for (int iStaff : range(getStavesCount())) {
val measure = getMeasure(atMeasure(iStaff, measureIndex));
val beats = measure.getUsedBeats(withMeasureAndColumnElements);
columnBeats = columnBeats.merge(beats, false);
}
//add column beats
if (withMeasureAndColumnElements) {
for (val beatE : getColumnHeader(measureIndex).getColumnElementsWithBeats())
columnBeats.add(beatE.beat);
}
return columnBeats;
}
/**
* Gets the last {@link Key} that is defined before (or at,
* dependent on the given {@link Interval}) the given
* {@link MP}, also over measure boundaries. If there is
* none, a default C major time signature is returned.
* Private keys (in measure) override public keys (in measure column header).
*/
@SuppressWarnings("unchecked")
public MPE<? extends Key> getKey(MP mp, Interval interval) {
if (!interval.isPrecedingOrAt()) {
throw new IllegalArgumentException("Illegal interval for this method");
}
//begin with the given measure. if there is one, return it.
BeatE<Key> columnKey = header.getColumnHeader(mp.measure).getKeys().getLastBefore(interval, mp.beat);
BeatE<Key> measureKey = null;
if (getMeasure(mp).getPrivateKeys() != null)
measureKey = getMeasure(mp).getPrivateKeys().getLastBefore(interval, mp.beat);
BeatE<Key> ret = selectLatest(columnKey, measureKey);
if (ret != null)
return mpE(ret.element, mp.withBeat(ret.beat));
if (interval != At) {
//search in the preceding measures
for (int iMeasure = mp.measure - 1; iMeasure >= 0; iMeasure--) {
columnKey = header.getColumnHeader(iMeasure).getKeys().getLast();
BeatEList<Key> privateKeys = getMeasure(atMeasure(mp.staff, iMeasure)).getPrivateKeys();
measureKey = (privateKeys != null ? privateKeys.getLast() : null);
ret = selectLatest(columnKey, measureKey);
if (ret != null)
return mpE(ret.element, mp.withMeasure(iMeasure).withBeat(ret.beat));
}
}
//no key found. return default key.
return mpE(new TraditionalKey(0, Mode.Major), mpb0);
}
/**
* Gets the accidentals at the given {@link MP} that are
* valid before or at the given beat (depending on the given interval),
* looking at all voices. The beat in the {@link MP} is required.
*/
public Map<Pitch, Integer> getAccidentals(MP mp, Interval interval) {
if (mp.beat == MP.unknownBeat)
throw new IllegalArgumentException("beat is required");
//start key of the measure always counts
MPE<? extends Key> key = getKey(mp, BeforeOrAt);
//if key change is in same measure, start at that beat. otherwise start at beat 0.
Fraction keyBeat = (key.mp.measure == mp.measure ? key.mp.beat : _0);
Measure measure = getMeasure(mp);
return measure.getAccidentals(mp.beat, interval, keyBeat, key.element);
}
/**
* Gets the last {@link Clef} that is defined before (or at,
* dependent on the given {@link Interval}) the given
* {@link MP}, also over measure boundaries. If there is
* none, a default g clef is returned. The beat in the {@link MP} is required.
*/
public ClefType getClef(MP mp, Interval interval) {
if (!interval.isPrecedingOrAt())
throw new IllegalArgumentException("Illegal interval for this method");
if (mp.beat == MP.unknownBeat)
throw new IllegalArgumentException("beat is required");
//begin with the given measure. if there is one, return it.
Measure measure = getMeasure(mp);
BeatE<Clef> ret = null;
if (measure.getClefs() != null) {
ret = measure.getClefs().getLastBefore(interval, mp.beat);
if (ret != null)
return ret.element.getType();
}
if (interval != At) {
//search in the preceding measures
for (int iMeasure : rangeReverse(mp.measure - 1, 0)) {
measure = getMeasure(atMeasure(mp.staff, iMeasure));
if (measure.getClefs() != null) {
ret = measure.getClefs().getLast();
if (ret != null)
return ret.element.getType();
}
}
}
//no clef found. return default clef.
return ClefType.clefTreble;
}
/**
* Gets the {@link MusicContext} that is defined before (or at,
* dependent on the given {@link Interval}s) the given
* {@link BMP}, also over measure boundaries.
*
* Calling this method can be quite expensive, so call only when neccessary.
*
* @param mp the musical position
* @param clefAndKeyInterval where to look for the last clef and key:
* {@link Interval#Before} ignores a clef and a key
* at the given position, {@link Interval#BeforeOrAt}
* and {@link Interval#At} (meaning the same here)
* include it
* @param accidentalsInterval the same as for clefAndKeyInterval, but for the accidentals.
* If null, no accidentals are collected.
*/
public MusicContext getMusicContext(MP mp,
Interval clefAndKeyInterval, Interval accidentalsInterval)
{
if (clefAndKeyInterval == At)
clefAndKeyInterval = BeforeOrAt; //At and BeforeOrAt mean the same in this context
ClefType clef = getClef(mp, clefAndKeyInterval);
Key key = getKey(mp, clefAndKeyInterval).element;
Map<Pitch, Integer> accidentals = noAccidentals;
if (accidentalsInterval != null)
accidentals = getAccidentals(mp, accidentalsInterval);
return new MusicContext(clef, key, accidentals, getStaff(mp).getLinesCount());
}
/**
* Returns the number of divisions of a quarter note within the whole score.
* This is e.g. needed for Midi and MusicXML Export.
*/
public int getDivisions() {
int actualdivision = 4;
for (Staff staff : stavesList.getStaves()) {
for (Measure measure : staff.getMeasures()) {
for (Voice voice : measure.getVoices()) {
for (VoiceElement e : voice.getElements()) {
actualdivision = MathUtils.lcm(actualdivision, e.getDuration().getDenominator());
}
}
}
}
return actualdivision / 4;
}
/**
* Gets the measures of the column with the given index.
*/
public Column getColumn(int measureIndex) {
Column ret = new Column(getStavesCount());
for (Staff staff : stavesList.getStaves()) {
ret.add(staff.getMeasure(measureIndex));
}
return ret;
}
/**
* Gets the interline space of the staff with the given index.
* If unspecified, the default value of the score is returned.
*/
public float getInterlineSpace(MP mp) {
int staffIndex = mp.staff;
if (staffIndex >= 0 && staffIndex < getStavesCount()) {
Float custom = getStaff(staffIndex).getInterlineSpace();
if (custom != null) {
return custom;
}
}
return format.getInterlineSpace();
}
/**
* Gets the gap in beats between the end of the left element and
* the start of the right element. If it can not be determined, because
* the musical position of one of the elements is unknown, null is returned.
*/
public Fraction getGapBetween(VoiceElement left, VoiceElement right) {
MP mpLeft = MP.getMP(left);
if (mpLeft == null)
return null;
mpLeft = mpLeft.withBeat(mpLeft.beat.add(left.getDuration()));
MP mpRight = MP.getMP(right);
if (mpRight == null)
return null;
if (mpRight.measure == mpLeft.measure) {
//simple case: same measure. just subtract beats
return mpRight.beat.sub(mpLeft.beat);
}
else if (mpRight.measure > mpLeft.measure) {
//right element is in a following measure. add the following:
//- beats between end of left element and its measure end
//- beats in the following measures (until the measure which contains the right element)
//- start beat of the right element in its measure
Fraction gap = getMeasureFilledBeats(mpLeft.measure).sub(mpLeft.beat);
for (int iMeasure : range(mpLeft.measure + 1, mpRight.measure - 1))
gap = gap.add(getMeasureFilledBeats(iMeasure));
gap = gap.add(mpRight.beat);
return gap;
}
else {
//right element is not really at the right, but actually before the left element
//add the following and sign with minus:
//- betweens between the start of the right element and the end of its measure
//- beats in the following measures (until the measure which contains the left element)
//- end beat of the left element in its measure
Fraction gap = getMeasureFilledBeats(mpRight.measure).sub(mpRight.beat);
for (int iMeasure : range(mpRight.measure + 1, mpLeft.measure - 1))
gap = gap.add(getMeasureFilledBeats(iMeasure));
gap = gap.add(mpLeft.beat);
gap = gap.mult(fr(-1));
return gap;
}
}
}