package com.xenoage.zong.musiclayout.spacer.beam.slant;
import com.xenoage.zong.core.music.chord.StemDirection;
import com.xenoage.zong.musiclayout.spacer.beam.Direction;
import com.xenoage.zong.musiclayout.spacer.beam.Slant;
import com.xenoage.zong.musiclayout.spacer.beam.stem.BeamedStems;
import lombok.val;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.zong.core.music.chord.StemDirection.Down;
import static com.xenoage.zong.core.music.chord.StemDirection.Up;
import static com.xenoage.zong.musiclayout.spacer.beam.Direction.Ascending;
import static com.xenoage.zong.musiclayout.spacer.beam.Direction.Descending;
import static com.xenoage.zong.musiclayout.spacer.beam.Slant.*;
import static java.lang.Math.abs;
import static java.lang.Math.round;
/**
* Computes the {@link Slant} of a beam within a single staff.
*
* The rules are adopted from Ross, p. 97-118, and Chlapik, p. 41-43.
*
* On page 115 ff., Ross names a lot of rules when beams have to be horizontal.
* It seems as if they can be unified to two rules:
* <ol>
* <li>A beam is horizontal, if the first and last note are on the same LP.</li>
* <li>An upstem/downstem beam with 3 or more chords is horizontal, if at least one of
* the middle notes is higher/lower than or equal to the left and right note.
* There are some exceptions, listed in Ross, p. 97.</li>
* </ol>
* All other beams receive a non-horizontal slant.
*
* The amount of non-horizontal slants is dependent on
* <ul>
* <li>the interval between the first and last note (see Ross, p. 111) and</li>
* <li>the horizontal spacing (close spacing for crowded notes, normal
* spacing otherwise, see Ross p. 112 ff.)</li>
* <li>the LP of the notes (Ross, p. 111, rows 1 and 2)</li>
* </ul>
*
* @author Andreas Wenger
*/
public class SingleStaffBeamSlanter {
public static SingleStaffBeamSlanter singleStaffBeamSlanter = new SingleStaffBeamSlanter();
public Slant compute(BeamedStems stems, int staffLines) {
//Ross, p. 115, row 1: use horizontal beam, if first and last note is on the same LP
if (stems.leftNoteLp == stems.rightNoteLp)
return horizontalSlant;
//unification of the rules in Ross, p. 115-117:
//a horizontal beam may be correct if all middle notes are lower/higher or equal than
//outer notes for a downstem/upstem beam
val stemDir = stems.getFirst().dir;
if (containsMiddleExtremeNote(stems, stemDir)) {
//there are some exceptions, listed in Ross, p. 97
//Ross, p. 97, row 3: 3 notes with middle note equal to outer note: normal slant
if (is3NotesMiddleEqualsOuter(stems))
return computeNormal(stems.leftNoteLp, stems.rightNoteLp);
//Ross, p. 97, row 4: 4 notes in special constellation: half space slant
int rossSpecialDir = get4NotesRossSpecialDir(stems, stemDir);
if (rossSpecialDir != 0)
return slant(rossSpecialDir * 0.5f);
//Ross, p. 97, rows 5 and 6: inner run with half space slant
int innerRunDir = getInnerRunDir(stems);
if (innerRunDir != 0)
return slant(innerRunDir * 0.5f);
//no exception. horizontal is correct.
return horizontalSlant;
}
//otherwise, compute slant dependent on the horizontal spacing
Slant slant;
if (isCloseSpacing(stems))
slant = computeClose(stems, stemDir);
else
slant = computeNormal(stems.leftNoteLp, stems.rightNoteLp);
//limit slant
slant = limitSlantForExtremeNotes(slant, stems, stemDir, staffLines);
return slant;
}
/**
* A special rule from Ross, p. 97, row 4.
* Returns the direction (1: up, -1: down) of this pattern
* if it can be found, otherwise 0.
*/
int get4NotesRossSpecialDir(BeamedStems stems, StemDirection stemDir) {
if (stems.getCount() != 4)
return 0;
//first note must be like second one, or third like last
boolean firstEqual;
if (stems.leftNoteLp == stems.get(1).noteSlp.lp)
firstEqual = true;
else if (stems.get(2).noteSlp.lp == stems.get(3).noteSlp.lp)
firstEqual = false;
else
return 0;
//remaining note must be like its outer neighbor, but 1 LP further out
float outerLp = stems.get(firstEqual ? 3 : 0).noteSlp.lp;
float innerLp = stems.get(firstEqual ? 2 : 1).noteSlp.lp;
if (outerLp == innerLp + 1 * stemDir.getSign())
return (stems.leftNoteLp > stems.rightNoteLp ? -1 : 1); //overall direction
return 0;
}
/**
* If the beam has at least 6 notes and the notes ascend or descend
* from the first to the second last note or from the second to the last note,
* we have an "inner run" (see Ross p. 97).
* The direction of the run is returned (1: up, -1: down, 0: no run found).
* It seems that 4 notes are not enough for this rule, otherwise for
* example p. 117 row 2 and row 7 col 1 would qualify, too.
*/
int getInnerRunDir(BeamedStems stems) {
if (stems.getCount() < 6)
return 0;
//try both directions
for (int dir : new int[]{-1, 1}) {
boolean foundRun = true;
boolean exceptFirst = false;
for (int i : range(stems.getCount() - 1)) {
if (dir * stems.get(i).noteSlp.lp >= dir * stems.get(i + 1).noteSlp.lp) {
//break in run. allowed between first and second note,
//and between second last and last note (but not both!)
if (i == 0) {
//break between first and second note
exceptFirst = true;
}
else if (i == stems.getCount() - 2 && !exceptFirst) {
//break between second last and last note (and none before)
}
else {
//break, no matching run found
foundRun = false;
break;
}
}
}
if (foundRun)
return dir;
}
return 0;
}
/**
* Returns true, iff the given beam is very crowded on the x-axis
* and requires close spacing.
*/
boolean isCloseSpacing(BeamedStems stems) {
//Ross, p. 100 and p. 112:
//we use close spacing, if the distance is less than 3 or 4 spaces
//we use the average value, 3.5, and have a look at the average stem distance
float avgDistanceIs = (stems.rightXIs - stems.leftXIs) / (stems.getCount() - 1);
return avgDistanceIs < 3.5;
}
/**
* Computes the slant for closely spaced beams.
*/
Slant computeClose(BeamedStems stems, StemDirection stemDir) {
//Ross, p. 112: beams in close spacing slant only 1/4 to 1/2 space
int dictatorLp = Math.round(stemDir == Up ? stems.getMaxNoteLp() : stems.getMinNoteLp());
Direction dir = (stems.rightNoteLp > stems.leftNoteLp ? Ascending : Descending);
//if dictator is on a staff line, use slant of 1/4 space
if (dictatorLp % 2 == 0 || abs(stems.rightNoteLp - stems.leftNoteLp) <= 1)
//on staff (Ross p. 112) or 2nd interval (Ross p. 111)
return slantIs(0.25f, dir);
else
return slantIs(0.5f, dir);
}
/**
* Computes the slant for beams with normal horizontal spacing.
*/
public Slant computeNormal(float firstNoteLp, float lastNoteLp) {
//Ross, p. 111 (and p. 101)
int interval = round(abs(firstNoteLp - lastNoteLp));
Direction dir = (lastNoteLp > firstNoteLp ? Ascending : Descending);
//Ross' rules for 4th are inconsistent p. 101d min=0.5 and p. 111:
//101d min=0.5 seems to be a special case, when an extreme note requires its stem to
//reach the middle line. then, we could use the smaller slant of 0.5 IS instead of the
//minimal slant of 1 IS which is described on page 111 4th.
//we already tried to apply this rule, but the tests with lots of examples
//showed no real improvements, so we ignore it and assume a minimum of 0.5 IS for a 4th slant
switch (interval) {
case 0: return horizontalSlant; //unison
case 1: return slantIs(0.25f, dir); //2nd
case 2: return slantIs(0.5f, 1, dir); //3rd
case 3: return slantIs(0.5f, 1.25f, dir); //4th
case 4: return slantIs(1.25f, dir); //5th
case 5: return slantIs(1.25f, 1.5f, dir); //6th
case 6: return slantIs(1.25f, 1.75f, dir); //7th
default: return slantIs(1.25f, 2, dir); //8th or higher
}
}
/**
* Ross, p. 111: Limit the slant to 0.5 IS for very high and very low notes.
*/
Slant limitSlantForExtremeNotes(Slant slant, BeamedStems stems,
StemDirection stemDir, int staffLines) {
int maxSlantAbsQs = 2; //2 QS = 0.5 IS
if (slant.maxAbsQs > maxSlantAbsQs) {
//Ross, p. 111, row 1 and 2: upstem and only notes below bottom leger lines
//or downstem and only notes above top leger lines
int bottomLegerLp = -2;
int topLegerLp = staffLines * 2;
if ((stemDir == Up && stems.getMaxNoteLp() < bottomLegerLp) ||
(stemDir == Down && stems.getMinNoteLp() > topLegerLp)) {
slant = slant.limitQs(maxSlantAbsQs);
}
}
return slant;
}
/**
* Returns true, if at least one of the middle notes is higher/lower than or equal
* to the left and right note, when the given stem direction is up/down.
*/
boolean containsMiddleExtremeNote(BeamedStems stems, StemDirection stemDir) {
if (stemDir == Up) {
float outerMax = Math.max(stems.leftNoteLp, stems.rightNoteLp);
for (int i : range(1, stems.getCount() - 2))
if (stems.get(i).noteSlp.lp >= outerMax)
return true;
}
else if (stemDir == Down) {
float outerMin = Math.min(stems.leftNoteLp, stems.rightNoteLp);
for (int i : range(1, stems.getCount() - 2))
if (stems.get(i).noteSlp.lp <= outerMin)
return true;
}
return false;
}
/**
* Returns true, iff the beam has 3 stems and the LP of the middle note is
* the same as the LP of the left or right note.
*/
boolean is3NotesMiddleEqualsOuter(BeamedStems stems) {
return (stems.getCount() == 3 &&
(stems.leftNoteLp == stems.get(1).noteSlp.lp || stems.get(1).noteSlp.lp == stems.rightNoteLp));
}
}