package com.xenoage.zong.core.music; import com.xenoage.zong.core.Score; import com.xenoage.zong.core.music.chord.Chord; import com.xenoage.zong.core.music.util.MPE; import com.xenoage.zong.core.position.MP; import com.xenoage.zong.utils.exceptions.IllegalMPException; import lombok.Getter; import lombok.Setter; import java.util.ArrayList; import java.util.List; import static com.xenoage.utils.kernel.Range.rangeReverse; import static com.xenoage.zong.core.music.Measure.measure; import static com.xenoage.zong.core.music.util.MPE.mpE; import static com.xenoage.zong.core.position.MP.atMeasure; import static java.lang.Math.max; /** * Staff of any size. * * A vocal staff that is visible throughout the whole * score is an instance of this class as well * as a small ossia staff that is only displayed * over a single measure. * * Staves are divided into measures. * * @author Andreas Wenger */ public final class Staff { /** The measures from the beginning to the ending of the score (even the invisible ones). */ @Getter private List<Measure> measures; /** The number of lines in this staff. */ @Getter @Setter private int linesCount; /** Distance between the lines in this staff in mm, or null for default. */ @Getter @Setter private Float interlineSpace; /** Back reference: The parent staves list, or null if not part of a score. */ @Getter @Setter private StavesList parent; /** * Creates a new {@link Staff}. */ public Staff(List<Measure> measures, int linesCount, Float interlineSpace) { for (Measure measure : measures) measure.setParent(this); this.measures = measures; this.linesCount = linesCount; this.interlineSpace = interlineSpace; } /** * Creates a new {@link Staff}. */ public static Staff staff(int linesCount, Float interlineSpace) { @SuppressWarnings("Convert2Diamond") ArrayList<Measure> measures = new ArrayList<>(); return new Staff(measures, linesCount, interlineSpace); } /** * Creates a minimal staff with no content. */ public static Staff staffMinimal() { return staff(5, null); } /** * Adds the given number of empty measures at the end * of the staff. */ public void addEmptyMeasures(int measuresCount) { for (int i = 0; i < measuresCount; i++) { Measure m = measure(); m.setParent(this); measures.add(m); } } /** * Gets the measure with the given index, or throws an * {@link IllegalMPException} if there is none. */ public Measure getMeasure(int index) { if (index >= 0 && index < measures.size()) return measures.get(index); else throw new IllegalMPException(atMeasure(index)); } /** * Gets the measure with the given index, or throws an * {@link IllegalMPException} if there is none. * Only the measure index of the given position is relevant. */ public Measure getMeasure(MP mp) { int index = mp.measure; if (index >= 0 && index < measures.size()) return measures.get(index); else throw new IllegalMPException(mp); } /** * Gets the voice within the measure at the given position, or throws an * {@link IllegalMPException} if there is none. * Only the measure index and voice index of the given position are relevant. */ public Voice getVoice(MP mp) { return getMeasure(mp).getVoice(mp); } /** * Gets the {@link VoiceElement} before the given position (also over measure * boundaries) together with its index in the voice, * or null, if there is none (begin of score or everything is empty) * @param mp the position after the voice element, with element index * @param onlyChord if true, rests are ignored, and only a chord (or null) is returned */ public MPE<VoiceElement> getVoiceElementBefore(MP mp, boolean onlyChord) { mp.requireStaffAndMeasureAndVoiceAndElement(); //find the last voice element ending at or before the current beat //in the given measure Voice voice = getMeasure(mp.measure).getVoice(mp.voice); for (int i : rangeReverse(mp.element - 1, 0)) { VoiceElement e = voice.getElement(i); if (!onlyChord || (e instanceof Chord)) return mpE(e, mp.withElement(i)); } //no result in this measure. loop through the preceding measures. for (int iMeasure : rangeReverse(mp.measure - 1, 0)) { voice = getMeasure(iMeasure).getVoice(mp.voice); for (int i : rangeReverse(voice.getElements())) { VoiceElement e = voice.getElement(i); if (!onlyChord || (e instanceof Chord)) return mpE(e, MP.atElement(mp.staff, iMeasure, mp.voice, i)); } } //nothing found return null; } /** * Sets the measure with the given index. * If out of current range, empty measures up to the given one are created. */ public void setMeasure(int index, Measure measure) { while (index >= measures.size()) { Measure m = measure(); m.setParent(this); measures.add(m); } measure.setParent(this); measures.set(index, measure); } /** * Gets the number of voices in this staff. * This is the number of voices in the measure with the most number of voices. */ public int getVoicesCount() { int voiceCount = 0; for (Measure measure : measures) { int size = measure.getVoices().size(); voiceCount = max(voiceCount, size); } return voiceCount; } /** * Gets the {@link MP} of the given measure, or null if this staff is not * part of a score or if the measure is not part of this staff. */ public MP getMP(Measure measure) { int measureIndex = measures.indexOf(measure); if (parent == null || measureIndex == -1) return null; int staffIndex = parent.getStaves().indexOf(this); if (staffIndex == -1) return null; return MP.atMeasure(staffIndex, measureIndex); } /** * Convenience method. Gets the parent score of this staff, * or null, if this element is not part of a score. */ public Score getScore() { return (parent != null ? parent.getScore() : null); } }