package com.xenoage.zong.core.music;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.utils.kernel.Range.rangeReverse;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import com.xenoage.utils.annotations.Unneeded;
import com.xenoage.utils.math.Fraction;
import com.xenoage.utils.math.MathUtils;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.music.group.BarlineGroup;
import com.xenoage.zong.core.music.group.BracketGroup;
import com.xenoage.zong.core.music.group.StavesRange;
import com.xenoage.zong.core.music.util.Column;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.utils.exceptions.IllegalMPException;
/**
* A {@link StavesList} manages the staves of a score
* and all of its parts, and bracket and barline groups.
*
* @author Andreas Wenger
*/
@Data
public final class StavesList {
/** The parent score. */
private Score score = null;
/** The list of all staves. */
private List<Staff> staves = new ArrayList<>();
/** The list of all parts. */
private List<Part> parts = new ArrayList<>();
/** The groups of the staves with barlines. */
private List<BarlineGroup> barlineGroups = new ArrayList<>();
/** The groups of the staves with brackets. */
private List<BracketGroup> bracketGroups = new ArrayList<>();
/**
* Gets the measure column at the given measure index.
*/
public Column getColumn(int measure) {
Column ret = new Column(staves.size());
for (int iStaff : range(staves.size())) {
ret.add(staves.get(iStaff).getMeasures().get(measure));
}
return ret;
}
/**
* Gets the part the given staff belongs to, or null if not found.
*/
public Part getPartByStaffIndex(int staffIndex) {
int iStaff = 0;
for (Part part : parts) {
iStaff += part.getStavesCount();
if (staffIndex < iStaff)
return part;
}
return null;
}
/**
* Gets the staves indices of the given part, or null if not found.
*/
public StavesRange getPartStaffIndices(Part part) {
int iStaff = 0;
for (Part p : parts) {
if (p == part)
return new StavesRange(iStaff, iStaff + part.getStavesCount() - 1);
iStaff += p.getStavesCount();
}
return null;
}
/**
* Gets the barline group the given staff belongs to.
* If no group is found, null is returned.
*/
public BarlineGroup getBarlineGroupByStaff(int staffIndex) {
for (BarlineGroup barlineGroup : barlineGroups) {
if (barlineGroup.getStaves().contains(staffIndex))
return barlineGroup;
}
return null;
}
/**
* Returns the number of divisions of a quarter note. This is needed for
* Midi and MusicXML Export.
*/
public int computeDivisions() {
int actualdivision = 4;
for (Staff staff : staves) {
for (Measure measure : staff.getMeasures()) {
for (Voice voice : measure.getVoices()) {
for (VoiceElement me : voice.getElements()) {
if (me.getDuration() != null) {
actualdivision = MathUtils.lcm(actualdivision, me.getDuration().getDenominator());
}
}
}
}
}
return actualdivision / 4;
}
/**
* Gets the {@link Staff} at the given {@link MP} (only the staff index is read).
*/
public Staff getStaff(MP mp) {
return getStaff(mp.staff);
}
/**
* Gets the {@link Staff} at the given global index.
*/
public Staff getStaff(int staffIndex) {
if (staffIndex >= 0 && staffIndex < staves.size()) {
return staves.get(staffIndex);
}
else {
throw new IllegalMPException(MP.atStaff(staffIndex));
}
}
/**
* Gets the global index of the given {@link Staff}, or -1 if the staff
* is not part of this staves list.
*/
public int getStaffIndex(Staff staff) {
return staves.indexOf(staff);
}
/**
* Gets the {@link Measure} with the given index at the staff with the given
* global index.
*/
public Measure getMeasure(MP mp) {
return getStaff(mp).getMeasure(mp);
}
/**
* Gets the {@link Voice} at the given {@link BMP} (beat is ignored).
*/
public Voice getVoice(MP mp) {
return getMeasure(mp).getVoice(mp);
}
/**
* Gets the filled beats for each measure column, that
* means, the first beat in each column where there is no music
* element following any more.
*/
@Unneeded public Fraction[] getFilledBeats() {
int measuresCount = staves.get(0).getMeasures().size();
Fraction[] ret = new Fraction[measuresCount];
for (int iMeasure = 0; iMeasure < measuresCount; iMeasure++) {
Fraction maxBeat = Fraction._0;
for (Staff staff : staves) {
Fraction beat = staff.getMeasures().get(iMeasure).getFilledBeats();
if (beat.compareTo(maxBeat) > 0)
maxBeat = beat;
}
ret[iMeasure] = maxBeat;
}
return ret;
}
/**
* Adds a staff group for the given staves with the given style.
* Since a staff may only have one barline group, existing barline groups
* at the given positions are merged with the given group.
*/
public void addBarlineGroup(StavesRange stavesRange, BarlineGroup.Style style) {
if (stavesRange.getStop() >= staves.size())
throw new IllegalArgumentException("staves out of range");
//if the given group is within an existing one, ignore the new group
//(we do not support nested barline groups)
for (int i : range(barlineGroups)) {
BarlineGroup group = barlineGroups.get(i);
if (group.getStaves().contains(stavesRange))
return;
}
//delete existing groups intersecting the given range, but merge
//the affected staves into the given group
for (int i : rangeReverse(barlineGroups)) {
BarlineGroup group = barlineGroups.get(i);
if (group.getStaves().intersects(stavesRange)) {
barlineGroups.remove(i);
stavesRange = stavesRange.merge(group.getStaves());
}
}
//add new group at the right position
//(the barline groups are sorted by start index)
int i = 0;
while (i < barlineGroups.size() && stavesRange.getStart() > barlineGroups.get(i).getStaves().getStart()) {
i++;
}
barlineGroups.add(i, new BarlineGroup(stavesRange, style));
}
/**
* Adds a bracket group for the given staves with the given style.
*/
public void addBracketGroup(StavesRange stavesRange, BracketGroup.Style style) {
if (stavesRange.getStop() >= staves.size())
throw new IllegalArgumentException("staves out of range");
//add new group at the right position
//(the bracket groups are sorted by start index)
int i = 0;
while (i < bracketGroups.size() && bracketGroups.get(i).getStaves().getStart() > stavesRange.getStart()) {
i++;
}
bracketGroups.add(i, new BracketGroup(stavesRange, style));
}
}