package com.xenoage.zong.musiclayout.notator.chord;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.kernel.Range.rangeReverse;
import static java.lang.Math.abs;
import java.util.ArrayList;
import com.xenoage.utils.collections.ArrayUtils;
import com.xenoage.utils.kernel.Range;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.music.MusicContext;
import com.xenoage.zong.core.music.chord.Chord;
import com.xenoage.zong.core.music.chord.StemDirection;
import com.xenoage.zong.musiclayout.notation.chord.ChordLps;
import com.xenoage.zong.musiclayout.notation.chord.NoteDisplacement;
import com.xenoage.zong.musiclayout.notation.chord.NoteSuspension;
import com.xenoage.zong.musiclayout.notation.chord.NotesNotation;
import com.xenoage.zong.musiclayout.settings.ChordWidths;
/**
* Computes the notation of the notes and the dots of a given chord.
*
* The rules are adapted from "Chlapik: Die Praxis des Notengraphikers", page 40.
* The dot placing rules are based on Sibelius 1.4.
*
* @author Andreas Wenger
*/
public class NotesNotator {
public static final NotesNotator notesNotator = new NotesNotator();
private final int[] emptyIntArray = {};
private final float[] emptyFloatArray = {};
/**
* Classes of chords, dependent on stem direction and unison/second interval.
*/
private enum ChordClass {
/** Stem down without unison/second interval. */
StemDown,
/** Stem down with unison/second interval. */
StemDownSuspended,
/** Stem up without unison/second interval. */
StemUp,
/** Stem up with unison/second interval. */
StemUpSuspended
}
/**
* Computes the displacement of the notes of the given chord, which has a stem into
* the given direction, using the given musical context.
*/
public NotesNotation compute(Chord chord, StemDirection stemDirection,
ChordWidths chordWidths, MusicContext musicContext) {
ChordLps lp = new ChordLps(chord, musicContext);
ChordClass chordClass = computeChordClass(lp, stemDirection);
float noteheadWidth = chordWidths.get(chord.getDisplayedDuration());
float stemOffset = computeStemOffset(chordClass, noteheadWidth);
float notesWidth = computeNotesWidth(chordClass, noteheadWidth);
NoteDisplacement[] notes = computeNotes(lp, stemDirection, stemOffset);
int dotsCount = computeDotsCount(chord.getDuration());
float[] dotsOffsets = computeDotsOffsets(notesWidth, dotsCount, chordWidths);
int[] dotsLp = (dotsCount > 0 ? computeDotsLp(lp) : emptyIntArray);
boolean leftSuspended = isLeftSuspended(notes);
float totalWidth;
if (dotsCount > 0)
totalWidth = dotsOffsets[dotsCount - 1];
else
totalWidth = notesWidth;
return new NotesNotation(totalWidth, noteheadWidth, notes, dotsOffsets,
dotsLp, stemOffset, leftSuspended);
}
private ChordClass computeChordClass(ChordLps lps, StemDirection stemDirection) {
ChordClass chordClass = (stemDirection == StemDirection.Up) ? ChordClass.StemUp : ChordClass.StemDown;
for (int i = 1; i < lps.getNotesCount(); i++)
if (lps.get(i) - lps.get(i - 1) <= 1)
return ChordClass.values()[chordClass.ordinal() + 1];
return chordClass;
}
private float computeStemOffset(ChordClass chordClass, float noteheadWidth) {
if (chordClass == ChordClass.StemDown)
return 0;
else
return noteheadWidth;
}
private float computeNotesWidth(ChordClass chordClass, float noteheadWidth) {
if (chordClass == ChordClass.StemDown || chordClass == ChordClass.StemUp)
return noteheadWidth;
else
return 2 * noteheadWidth;
}
private NoteDisplacement[] computeNotes(ChordLps lps, StemDirection sd, float stemOffset) {
NoteDisplacement[] notes = new NoteDisplacement[lps.getNotesCount()];
//if stem direction is down or none, begin with the highest note,
//otherwise with the lowest
int dir = (sd == StemDirection.Up) ? 1 : -1;
int startIndex = (dir == 1) ? 0 : lps.getNotesCount() - 1;
int endIndex = lps.getNotesCount() - 1 - startIndex;
//default side of the stem. 1 = right, 0 = left
int stemSide = (sd == StemDirection.Up) ? 1 : 0;
int lastSide = stemSide;
for (int i = startIndex; dir * i <= dir * endIndex; i += dir) {
int side = 1 - stemSide;
NoteSuspension suspension = NoteSuspension.None;
if (i == startIndex) {
//first note: use default side
}
else {
//following note: change side, if last note is 1 or 0 LPs away
if (abs(lps.get(i) - lps.get(i - dir)) <= 1) {
//change side
side = 1 - lastSide;
if (side != 1 - stemSide) {
if (side == 0)
suspension = NoteSuspension.Left;
else
suspension = NoteSuspension.Right;
}
}
}
notes[i] = new NoteDisplacement(lps.get(i), side * stemOffset, suspension);
lastSide = side;
}
return notes;
}
/**
* Computes the number of prolongation dots, e.g. 2 if it is
* a half+quarter+eighth note (a half with two dots).
*/
private int computeDotsCount(Fraction duration) {
int num = duration.getNumerator();
if (num == 3)
return 1;
else if (num == 7)
return 2;
else
return 0;
}
private float[] computeDotsOffsets(float notesWidth, int dotsCount, ChordWidths chordWidths) {
if (dotsCount == 0)
return emptyFloatArray;
float[] ret = new float[dotsCount];
for (int i : Range.range(dotsCount))
ret[i] = notesWidth + chordWidths.dotGap + i * chordWidths.dot;
return ret;
}
private int[] computeDotsLp(ChordLps lps) {
ArrayList<Integer> dotsLp = alist(lps.getNotesCount());
int lastDotLp = Integer.MAX_VALUE;
for (int i : rangeReverse(lps.getNotesCount())) {
int lp = lps.get(i);
//place dot between (leger) lines, not on them. use the space above.
int dotLp = (lp % 2 == 0 ? lp + 1 : lp);
if (dotLp < lastDotLp) {
//space is free, use it
dotsLp.add(0, dotLp);
lastDotLp = dotLp;
}
else {
//there is already a dot. if we are on a line, use the space below, if it is free
if (lp % 2 == 0 && lp - 1 < lastDotLp) {
dotLp = lp - 1;
dotsLp.add(0, dotLp);
lastDotLp = dotLp;
}
}
}
return ArrayUtils.toIntArray(dotsLp);
}
private boolean isLeftSuspended(NoteDisplacement... notes) {
for (NoteDisplacement note : notes)
if (note.suspension == NoteSuspension.Left)
return true;
return false;
}
}