package com.xenoage.zong.io.musicxml.in.readers;
import com.xenoage.utils.kernel.Range;
import com.xenoage.utils.math.Fraction;
import com.xenoage.utils.math.VSide;
import com.xenoage.zong.commands.core.music.ColumnElementWrite;
import com.xenoage.zong.commands.core.music.MeasureElementWrite;
import com.xenoage.zong.commands.core.music.VoiceAdd;
import com.xenoage.zong.commands.core.music.VoiceElementWrite;
import com.xenoage.zong.commands.core.music.beam.BeamAdd;
import com.xenoage.zong.commands.core.music.slur.SlurAdd;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.instrument.Instrument;
import com.xenoage.zong.core.music.*;
import com.xenoage.zong.core.music.beam.Beam;
import com.xenoage.zong.core.music.chord.Chord;
import com.xenoage.zong.core.music.direction.Wedge;
import com.xenoage.zong.core.music.direction.WedgeEnd;
import com.xenoage.zong.core.music.group.StavesRange;
import com.xenoage.zong.core.music.slur.Slur;
import com.xenoage.zong.core.music.slur.SlurType;
import com.xenoage.zong.core.music.slur.SlurWaypoint;
import com.xenoage.zong.core.music.util.Interval;
import com.xenoage.zong.core.music.volta.Volta;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.io.musicxml.in.util.*;
import com.xenoage.zong.utils.exceptions.MeasureFullException;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.iterators.It.it;
import static com.xenoage.utils.math.Fraction._0;
import static com.xenoage.zong.core.position.MP.atBeat;
import static com.xenoage.zong.io.musicxml.in.util.CommandPerformer.execute;
/**
* This class stores the current context when
* reading a MusicXML 2.0 document.
*
* Example for context variables are the
* current divisions value or open ties.
*
* @author Andreas Wenger
*/
@Getter @Setter
public final class Context {
private Score score;
private MP mp = MP.mp0;
private int divisions = 1;
//current system and page index. only valid if system breaks and page breaks
//are used continuously
private int systemIndex = 0;
private int pageIndex = 0;
//maps MusicXML staff-/voice-element to our voice:
//musicXMLVoice == voiceMappings.get(musicXMLStaff).get(scoreVoice)
private List<List<String>> voiceMappings = alist();
private OpenElements openElements = new OpenElements();
private String instrumentID = null;
private ReaderSettings settings;
private VoiceElementWrite.Options writeVoicElementOptions;
public Context(Score score, ReaderSettings settings) {
this.score = score;
this.settings = settings;
//when writing voice elements, alwayse obey time signature and fill gaps
//with hidden rests
writeVoicElementOptions = new VoiceElementWrite.Options();
writeVoicElementOptions.checkTimeSignature = true;
writeVoicElementOptions.fillWithHiddenRests = true;
}
/**
* Moves the current beat within the current part and measure.
*/
public void moveCurrentBeat(Fraction beat) {
Fraction newBeat = this.mp.beat.add(beat);
//never step back behind 0
if (newBeat.getNumerator() < 0) {
reportError("Step back behind beat 0");
newBeat = _0;
}
this.mp = this.mp.withBeat(newBeat);
}
/**
* Gets the current system index.
* The value is only valid if system breaks and page breaks
* are used continuously.
*/
public int getSystemIndex() {
return systemIndex;
}
/**
* Increments the current system index.
*/
public void incSystemIndex() {
this.systemIndex++;
}
/**
* Gets the current page index.
* The value is only valid if system breaks and page breaks
* are used continuously.
*/
public int getPageIndex() {
return pageIndex;
}
/**
* Increments the current page index.
*/
public void incPageIndex() {
this.pageIndex++;
}
/**
* Gets the global value of a tenth in mm.
*/
public float getTenthMm() {
return getScore().getFormat().getInterlineSpace() / 10;
}
/**
* Gets the indices of the staves of the current part.
*/
public StavesRange getPartStaffIndices() {
StavesList stavesList = getScore().getStavesList();
Part part = stavesList.getPartByStaffIndex(mp.staff);
return stavesList.getPartStaffIndices(part);
}
/**
* Gets the number of lines of the staff with the given index, relative
* to the current part.
*/
public int getStaffLinesCount(int staffIndexInPart) {
MP mp = this.mp.withStaff(getPartStaffIndices().getStart() + staffIndexInPart);
return getScore().getStaff(mp).getLinesCount();
}
/**
* Call this method when a new part begins.
*/
public Part beginNewPart(int partIndex) {
Part part = score.getStavesList().getParts().get(partIndex);
this.mp = atBeat(score.getStavesList().getPartStaffIndices(part).getStart(), 0, 0, _0);
List<List<String>> voiceMappings = alist();
for (int i = 0; i < part.getStavesCount(); i++)
voiceMappings.add(new ArrayList<>());
this.voiceMappings = voiceMappings;
//reset instrument to null
this.instrumentID = null;
return part;
}
/**
* Call this method when a new measure begins.
*/
public void beginNewMeasure(int measureIndex) {
this.mp = atBeat(this.mp.staff, measureIndex, 0, _0);
}
/**
* Sets a waypoint for a slur or tie with the given number.
* When all required waypoints of the slur are set, the slur is created.
*
* For tied elements without a number, use openUnnumberedTied instead.
*/
public void registerSlur(SlurType type, WaypointPosition wpPos,
int number, SlurWaypoint wp, VSide side) {
if (false == checkNumber1to6(number))
return;
//ignore continue waypoints at the moment
if (wpPos == WaypointPosition.Continue)
return;
boolean start = (wpPos == WaypointPosition.Start);
boolean stop = !start;
List<OpenSlur> openSlurs = (type == SlurType.Slur ? openElements.getOpenSlurs()
: openElements.getOpenTies());
//get open slur, or create it
OpenSlur openSlur = openSlurs.get(number - 1);
if (openSlur == null) {
openSlur = new OpenSlur();
openSlur.type = type;
}
//this point must not already be set
if ((start && openSlur.start != null) || (stop && openSlur.stop != null)) {
reportError(wpPos + " waypoint already set for " + type + " " + number);
}
OpenSlur.Waypoint openSlurWP = new OpenSlur.Waypoint(wp, side);
if (start)
openSlur.start = openSlurWP;
else
openSlur.stop = openSlurWP;
//if complete, write it now, otherwise remember it
if (openSlur.start != null && openSlur.stop != null) {
createSlur(openSlur);
openSlurs.set(number - 1, null);
}
else {
openSlurs.set(number - 1, openSlur);
}
}
/**
* Creates and writes a slur or tie from the given {@link OpenSlur} object.
*/
public void createSlur(OpenSlur openSlur) {
if (checkSlur(openSlur)) {
Slur slur = new Slur(openSlur.type, alist(openSlur.start.wp,
openSlur.stop.wp), openSlur.start.side);
writeSlur(slur);
}
}
private boolean checkSlur(OpenSlur slur) {
if (slur.start == null || slur.stop == null)
return false;
Fraction gap = score.getGapBetween(slur.start.wp.getChord(), slur.stop.wp.getChord());
if (gap == null) {
reportError("Can not determine gap between slurred/tied elements; slur/tie is ignored");
return false;
}
if (slur.type == SlurType.Slur) {
//slur must end after the start chord
return gap.compareTo(_0) >= 0;
}
else if (slur.type == SlurType.Tie) {
//tie must end directly after the start chord (no gap), since
//it is played as a single note without a break inbetween
return gap.compareTo(_0) == 0;
}
return false;
}
/**
* Returns true, if the given beam/slur/tie number is valid,
* i.e. between 1 and 6.
*/
private boolean checkNumber1to6(int number) {
if (number < 1 || number > 6) {
reportError("number is not valid: " + number);
return false;
}
return true;
}
/**
* Sets the beginning of a wedge with the given number.
* When there is still an open wedge with this number, it
* is removed from the score.
*/
public void openWedge(int number, Wedge wedge) {
if (false == checkNumber1to6(number))
return;
//remove existing open wedge
Wedge openWedge = openElements.getOpenWedges().get(number - 1);
if (openWedge != null)
removeUnclosedWedge(openWedge);
//add new wedge
List<Wedge> openWedges = openElements.getOpenWedges();
openWedges.set(number - 1, wedge);
}
/**
* Closes the wedge with the given number and returns it,
* or null when the number is invalid.
*/
public Wedge closeWedge(int number) {
if (false == checkNumber1to6(number))
return null;
Wedge ret = openElements.getOpenWedges().get(number - 1);
List<Wedge> openWedges = openElements.getOpenWedges();
openWedges.set(number - 1, null);
return ret;
}
/**
* Removes all open wedges from the score.
* If left in a score, these would make the score inconsistent because the
* {@link WedgeEnd} element would be missing.
*/
public void removeUnclosedWedges() {
for (Wedge wedge : openElements.getOpenWedges()) {
if (wedge != null)
removeUnclosedWedge(wedge);
}
}
/**
* See {@link #removeUnclosedWedges()}.
*/
public void removeUnclosedWedge(Wedge wedge) {
Measure measure = score.getMeasure(wedge.getMP());
measure.removeMeasureElement(wedge);
}
/**
* Gets the {@link MusicContext} at the current position at the
* staff with the given part-intern index.
*/
public MusicContext getMusicContext(int staffIndexInPart) {
MP mp = this.mp.withStaff(getPartStaffIndices().getStart() + staffIndexInPart);
return score.getMusicContext(mp, Interval.BeforeOrAt, Interval.Before);
}
/**
* Gets the voice index for the given MusicXML voice and MusicXML staff.
* This is needed, because voices are defined for staves in this program, while
* voices are defined for parts in MusicXML. If the voice does not exist yet, it
* is created. If the staff does not exist, a {@link MusicReaderException} is thrown.
* @param mxlStaff 0-based staff index, found in staff-element minus 1
* @param mxlVoice voice id, found in voice-element
* @return the updated context and the voice index
*/
public int getVoice(int mxlStaff, String mxlVoice)
throws MusicReaderException {
try {
//gets the voices list for the given staff
List<String> voices = voiceMappings.get(mxlStaff);
//look for the given MusicXML voice
for (int scoreVoice = 0; scoreVoice < voices.size(); scoreVoice++) {
if (voices.get(scoreVoice).equals(mxlVoice))
return scoreVoice;
}
//if not existent yet, we have to create it
voices.add(mxlVoice);
return voices.size() - 1;
} catch (IndexOutOfBoundsException ex) {
throw new MusicReaderException("MusicXML staff " + mxlStaff + " and voice \"" + mxlVoice +
"\" are invalid for the current position. Enough staves defined in attributes?", this);
}
}
/**
* Writes the given {@link ColumnElement} at the
* current measure and current beat.
*/
public void writeColumnElement(ColumnElement element) {
new ColumnElementWrite(element, score.getColumnHeader(mp.getMeasure()), mp.getBeat(), null).execute();
}
/**
* Writes the given {@link ColumnElement} at the given measure.
*/
public void writeColumnElement(ColumnElement element, int measure) {
new ColumnElementWrite(element, score.getColumnHeader(measure), MP.unknownBeat, null).execute();
}
/**
* Writes the given {@link ColumnElement} at the current measure.
* @param side the side of the measure. If null, the current beat is used.
*/
public void writeColumnElement(ColumnElement element, MeasureSide side) {
Fraction beat = (side == null ? mp.beat : MP.unknownBeat);
new ColumnElementWrite(element, score.getColumnHeader(mp.measure), beat, side).execute();
}
/**
* Writes the given {@link MeasureElement} at the given staff (index relative to first
* staff in current part), current measure and current beat.
*/
public void writeMeasureElement(MeasureElement element, int staffIndexInPart) {
int staffIndex = getPartStaffIndices().getStart() + staffIndexInPart;
MP mp = this.mp.withStaff(staffIndex);
new MeasureElementWrite(element, score.getMeasure(mp), mp.getBeat()).execute();
}
/**
* Writes the given {@link VoiceElement} to the current position
* without moving the cursor forward. When the element could be written, true is returned,
* otherwise (e.g. when the measure was full), false is returned.
*/
public boolean writeVoiceElement(VoiceElement element, int staffIndexInPart, int voice) {
MP mp = this.mp.withStaff(getPartStaffIndices().getStart() + staffIndexInPart).withVoice(voice);
try {
//create voice if needed
Measure measure = score.getMeasure(mp);
if (measure.getVoices().size() < voice + 1)
execute(new VoiceAdd(measure, voice));
execute(new VoiceElementWrite(score.getVoice(mp), mp, element, writeVoicElementOptions));
return true;
} catch (MeasureFullException ex) {
reportError(ex.getMessage());
return false;
}
}
/**
* Moves the cursor forward by one index.
*/
public void moveCursorForward(Fraction duration) {
this.mp = this.mp.withBeat(this.mp.beat.add(duration));
}
/**
* Creates a beam for the given chords.
*/
public void writeBeam(List<Chord> chords) {
try {
new BeamAdd(Beam.beamFromChords(chords)).execute();
} catch (IllegalArgumentException ex) {
reportError(ex.getMessage());
}
}
/**
* Creates the given slur or tie.
*/
public void writeSlur(Slur slur) {
new SlurAdd(slur).execute();
}
/**
* Returns the ID of the current instrument. May also return null
* for "default instrument" or "no change".
*/
public String getInstrumentID() {
return instrumentID;
}
/**
* Changes the current instrument.
*/
public void writeInstrumentChange(String instrumentID) {
//find instrument
Part part = getScore().getStavesList().getPartByStaffIndex(mp.staff);
Instrument newInstrument = null;
for (Instrument instr : it(part.getInstruments())) {
if (instr.getId().equals(instrumentID)) {
newInstrument = instr;
break;
}
}
if (newInstrument == null) {
//error: instrument is unknown to this part
reportError("Unknown instrument: \"" + instrumentID + "\"");
}
//apply instrument change
new MeasureElementWrite(new InstrumentChange(newInstrument), score.getMeasure(this.mp), mp.beat).execute();
}
public void openVolta(Range numbers, String caption) {
if (openElements.getOpenVolta() == null)
openElements.setOpenVolta(new OpenVolta(mp.measure, numbers, caption));
}
public ClosedVolta closeVolta(boolean rightHook) {
OpenVolta openVolta = openElements.getOpenVolta();
if (openVolta == null) {
reportError("No volta open");
return null;
}
int length = mp.measure - openVolta.startMeasure + 1;
if (length < 1) {
reportError("Invalid volta");
return null;
}
Volta volta = new Volta(length, openVolta.numbers, openVolta.caption, rightHook);
openElements.setOpenVolta(null);
return new ClosedVolta(volta, openVolta.startMeasure);
}
public ReaderSettings getSettings() {
return settings;
}
public void reportError(String message) {
try {
settings.getErrorHandling().reportError(message + "; at " + mp);
}
catch (Exception ex) {
//when an exception was thrown, forward it with this context attached
throw new MusicReaderException(ex.getMessage(), this);
}
}
@Override public String toString() {
return "cursor at " + mp + ", system " + systemIndex + ", page " + pageIndex;
}
}