package com.xenoage.zong.musiclayout.spacer.beam.placement; import com.xenoage.utils.annotations.Const; import com.xenoage.zong.core.music.StaffLines; import com.xenoage.zong.core.music.chord.StemDirection; import com.xenoage.zong.musiclayout.notation.BeamNotation; import com.xenoage.zong.musiclayout.notator.beam.lines.BeamRules; import com.xenoage.zong.musiclayout.spacer.beam.Anchor; 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.Data; import lombok.val; import static com.xenoage.utils.kernel.Range.range; import static com.xenoage.utils.kernel.Range.rangeReverse; 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.Anchor.*; import static com.xenoage.zong.musiclayout.spacer.beam.Direction.*; import static java.lang.Math.abs; import static java.lang.Math.round; /** * Computes the {@link Placement} of a beam within a single staff, given its {@link Slant}. * * The rules are adopted from Ross, p. 98-101 and 120-126, and Chlapik, p. 41. * * When the beam falls within or touches the staff lines, the following rules apply: * <ul> * <li>A horizontal beam may straddle, sit on or hang below a staff line (Chlapik p. 41, 3)</li> * <li>For ascending or descending beams (observed, but no explicit rule found) * (H=Hang, S=Sit, St=Straddle): * <table border="1"> * <tr> * <td></td> * <th>First stem</th> * <th>Last stem</th> * </tr> * <tr> * <th>Ascending</th> * <td>H, St</td> * <td>S, St</td> * </tr> * <tr> * <th>Descending</th> * <td>S, St</td> * <td>H, St</td> * </tr> * </table> * </li> * </ul> * * These rules ensure, that white wedges within the staff are avoided. * * If in doubt, a smaller slant may be the better choice, and a slant of one space * should not be exceeded (Ross, p. 98). Also, shortening the stems is usually better * than lengthening them (Ross, p. 103). * * TODO (ZONG-92): Improve layout of multiline beams * * @author Andreas Wenger */ public class SingleStaffBeamPlacer { public static final SingleStaffBeamPlacer singleStaffBeamPlacer = new SingleStaffBeamPlacer(); /** * Vertical placement of a single-staff beam, defined by the outer LPs of * the left and the right stem. * * @author Andreas Wenger */ @Const @Data public static final class Placement { public final float leftEndLp, rightEndLp; public Direction getDirection() { float d = rightEndLp - leftEndLp; if (d > 0.1) return Ascending; else if (d < -0.1) return Descending; else return Horizontal; } } //stems at maximum 8 quarter spaces (2 spaces) shorter or longer private static final int[] stemLengthModLp = { //Ross recommends shortening first on p. 103, last paragraph. 0, //first, try the perfect solutin -1, -2, -3, -4, //then, try to shorten (up to 1 IS) +1, +2, +3, +4, //then, try to lengthen (up to 1 IS) -5, +5, -6, +6, -7, +7, -8, +8 //if still not found, try to shorten or lengthen up to 2 IS }; /** * Computes the {@link Placement} of a beam within a single staff. * @param slant the preferred slant for this beam * @param stems the positions of the stems and their preferred lengths * @param staffLines the number of staff lines, e.g. 5 */ public Placement compute(Slant slant, BeamedStems stems, int beamLinesCount, StaffLines staffLines) { val stemDir = stems.primaryStemDir; //TODO: different stem directions are possible? float slantIs; int dictatorIndex; Placement candidate; //try to find the optimum placement //start with default stem length of the dictator stem, and try the allowed //slants, beginning with the steepest one. if no solution can be found, //try with a steeper slant, then with shorter and longer stems for (int stemLengthAddQs : stemLengthModLp) { for (int slantAbsQs : rangeReverse(slant.maxAbsQs, slant.minAbsQs)) { //slant in allowed range slantIs = slant.direction.getSign() * slantAbsQs / 4f; dictatorIndex = getDictatorStemIndex(stemDir, stems, slantIs); float dictatorStemEndLp = stems.get(dictatorIndex).endSlp.lp + stemDir.getSign() * stemLengthAddQs; candidate = getPlacement(stems.leftXIs, stems.rightXIs, stems.get(dictatorIndex).xIs, dictatorStemEndLp, slantIs); if (isPlacementCorrect(candidate, stemDir, beamLinesCount, staffLines)) { //try to shorten candidate = shorten(candidate, stemDir, stems, beamLinesCount, staffLines); return candidate; } } } //no optimal placement could be found. just use the minimum slant //and the stem lengths enforced by the dictator stem slantIs = slant.getFlattestIs(); dictatorIndex = getDictatorStemIndex(stemDir, stems, slantIs); return getPlacement(stems.leftXIs, stems.rightXIs, stems.get(dictatorIndex).xIs, stems.get(dictatorIndex).endSlp.lp, slantIs); } /** * Gets the {@link Placement}, rounded to quarter spaces, of the beam which is * placed by the given dictator stem. */ Placement getPlacement(float leftXIs, float rightXIs, float dictatorXIs, float dictatorStemEndLp, float slantIs) { float widthIs = rightXIs - leftXIs; //compute exact end LPs float leftEndLpExact = getBeamLpAtXIs(leftXIs, dictatorXIs, dictatorStemEndLp, slantIs, widthIs); float rightEndLpExact = getBeamLpAtXIs(rightXIs, dictatorXIs, dictatorStemEndLp, slantIs, widthIs); //round to quarter spaces (both in the same direction!) float leftEndLp = round(leftEndLpExact * 2) / 2f; float rightEndLp = (int)(rightEndLpExact * 2 + (leftEndLp > leftEndLpExact ? 1 : 0)) / 2f; return new Placement(leftEndLp, rightEndLp); } /** * Given a beam, defined by a point on the beam (stem end), and its slant and width, * this method computes the LP of the beam (end of the stem) at the given hroizontal position. */ float getBeamLpAtXIs(float atXIs, float beamPointXIs, float beamPointLp, float slantIs, float beamWidthIs) { float slantIsPerIs = slantIs / beamWidthIs; return beamPointLp + slantIsPerIs * 2 * (atXIs - beamPointXIs); } /** * Gets the index of the dictator stem for the given stem direction. * This is the index of the stem, which ends at the lowest/highest LP for downstem/upstem beams, * with respect to the given beam slant. */ int getDictatorStemIndex(StemDirection forStemDir, BeamedStems stems, float slantIs) { int sign = forStemDir.getSign(); float extremeDistance = (forStemDir == Up ? Float.MIN_VALUE : Float.MAX_VALUE); int extremeIndex = 0; for (int i : range(stems)) { if (stems.get(i).dir == forStemDir) { float distance = getDistanceToLineLp(stems.get(i).endSlp.lp, stems.get(i).xIs, slantIs, stems.leftXIs, stems.rightXIs); if (distance * sign > extremeDistance * sign) { extremeDistance = distance; extremeIndex = i; } } } return extremeIndex; } /** * Gets the vertical distance between the given LP at the given horizontal * position in IS to an imaginary line starting at (lineLeftXIs,0) and * ending at (lineRightXIs,lineSlantIs). * A positive value means, that the layoutPos is above the line. */ float getDistanceToLineLp(float lp, float xIs, float lineSlantIs, float lineLeftXIs, float lineRightXIs) { //horizontal position of stem between 0 (left) and 1 (right) float t = (xIs - lineLeftXIs) / (lineRightXIs - lineLeftXIs); //LP on the line at this position float lineLp = t * lineSlantIs * 2; //return distance return lp - lineLp; } /** * Returns true, iff the given placement does not violate the rules * listed in the documentation of this class. */ public boolean isPlacementCorrect(Placement candidate, StemDirection stemDir, int beamLinesCount, StaffLines staffLines) { //when the beam does not touch the staff at all, its exact placement //does not matter (p. 98; and p. 103, last sentence before the box). if (false == isTouchingStaff(candidate, stemDir, BeamNotation.lineHeightIs, staffLines)) return true; //check anchor Anchor leftAnchor = Anchor.fromLp(candidate.leftEndLp, stemDir); Anchor rightAnchor = Anchor.fromLp(candidate.rightEndLp, stemDir); if (beamLinesCount == 1) return isAnchor8thCorrect(leftAnchor, rightAnchor, candidate.getDirection()); else if (beamLinesCount == 2) return isAnchor16thCorrect(leftAnchor, rightAnchor, stemDir); else return isAnchor32ndOrHigherCorrect(leftAnchor, rightAnchor, stemDir); } /** * Returns true, iff both the left LP and the right LP are completely * outside the staff and do not touch it. * @param beamHeightIs the total height of the beam lines (including gaps) in IS */ boolean isTouchingStaff(Placement candidate, StemDirection stemDir, float beamHeightIs, StaffLines staffLines) { float minDistanceIs = 0.45f; //at least about an half space //beam lines above the staff? float minLp = staffLines.topLp + minDistanceIs * 2 + (stemDir == Up ? beamHeightIs * 2 : 0); if (candidate.leftEndLp >= minLp && candidate.rightEndLp >= minLp) return false; //beam lines below the staff? float maxLp = -minDistanceIs * 2 - (stemDir == Down ? beamHeightIs * 2 : 0); if (candidate.leftEndLp <= maxLp && candidate.rightEndLp <= maxLp) return false; return true; } boolean isAnchor8thCorrect(Anchor leftAnchor, Anchor rightAnchor, Direction beamDir) { if (beamDir == Ascending) { //ascending beam: left may hang or straddle, right may sit or straddle if ((leftAnchor == Hang || leftAnchor == Straddle) && (rightAnchor == Sit || rightAnchor == Straddle)) return true; } else if (beamDir == Descending) { //descending beam: left may sit or straddle, right may hang or straddle if ((leftAnchor == Sit || leftAnchor == Straddle) && (rightAnchor == Hang || rightAnchor == Straddle)) return true; } else { //horizontal beam: both sides may sit, hang or straddle if (leftAnchor != WhiteSpace && rightAnchor != WhiteSpace) return true; } //violates the rules return false; } boolean isAnchor16thCorrect(Anchor leftAnchor, Anchor rightAnchor, StemDirection stemDir) { //see Ross, p. 120-121 if (stemDir == Up) { //upstem beam: both sides may straddle or hang (Ross, p. 120, section 8, 1) if ((leftAnchor == Straddle || leftAnchor == Hang) && (rightAnchor == Straddle || rightAnchor == Hang)) return true; } else { //downstem beam: both sides may sit or straddle (Ross, p. 121, section 8, 2) if ((leftAnchor == Sit || leftAnchor == Straddle) && (rightAnchor == Sit || rightAnchor == Straddle)) return true; } //violates the rules return false; } boolean isAnchor32ndOrHigherCorrect(Anchor leftAnchor, Anchor rightAnchor, StemDirection stemDir) { //see Ross, p. 125, section 10 //Beam always hangs (upstem) or sits (downstem), so it fills exactly 2 spaces //same for quadruple beams, see Ross p. 125, section 11. if (stemDir == Up) { //upstem beam: both sides must hang if (leftAnchor == Hang & rightAnchor == Hang) return true; } else { //downstem beam: both sides must sit if (leftAnchor == Sit & rightAnchor == Sit) return true; } //violates the rules return false; } /** * Shortens the stem lengths of the given placement candidate by one quarter space, * if possible and when no stem gets shorter than the {@link BeamRules} allow it. * This rule not found explicitly mentioned by Ross, but applies to many examples and * conforms to the general rule that beamed stems tend to be shortened (p. 103, last * paragraph). See for example: * <ul> * <li>p104 r1 c1: could be 3.5/sit, but is 3.25/straddle</li> * <li>p104 r6 c1: could be 3.75/straddle and 3.5/hang, but is 3.5/sit and 3.25/straddle</li> * <li>p105 r1 c2: could be 3.5/hang, but is 3.25/staddle</li> * </ul> */ Placement shorten(Placement candidate, StemDirection stemDir, BeamedStems stems, int beamLinesCount, StaffLines staffLines) { //shorten Placement shorterCandidate = new Placement( candidate.leftEndLp - stemDir.getSign() * 0.5f, candidate.rightEndLp - stemDir.getSign() * 0.5f); //stems still long enough? float slantIs = (shorterCandidate.rightEndLp - shorterCandidate.leftEndLp) / 2; BeamRules beamRules = BeamRules.getRules(beamLinesCount); for (val stem : stems) { float distanceToBeam = abs(getDistanceToLineLp(stem.noteSlp.lp, stem.xIs, slantIs, stems.leftXIs, stems.rightXIs) - shorterCandidate.leftEndLp) / 2; if (distanceToBeam < beamRules.getMinimumStemLengthIs()) return candidate; //shortening not possible } //edges correct? if (isPlacementCorrect(shorterCandidate, stemDir, beamLinesCount, staffLines)) return shorterCandidate; //success else return candidate; //shortening not possible } /** * Returns true, when the lines of the given beam are completely outside the staff * (not even touching a staff line). * @param stemDirection the direction of the stems * @param firstStemEndLp the LP of the endpoint of the first stem * @param lastStemEndLp the LP of the endpoint of the last stem * @param staffLinesCount the number of staff lines * @param totalBeamHeightIs the total height of the beam lines (including gaps) in IS * TODO (ZONG-92): use this method for multiline beams to find the smallest * possible line distance */ public boolean isBeamOutsideStaff(StemDirection stemDirection, float firstStemEndLp, float lastStemEndLp, int staffLinesCount, float totalBeamHeightIs) { float maxStaffLp = (staffLinesCount - 1) * 2; if (stemDirection == Up) { //beam lines above the staff? if (firstStemEndLp > maxStaffLp + totalBeamHeightIs * 2 && lastStemEndLp > maxStaffLp + totalBeamHeightIs * 2) { return true; } //beam lines below the staff? if (firstStemEndLp < 0 && lastStemEndLp < 0) { return true; } } else if (stemDirection == Down) { //beam lines above the staff? if (firstStemEndLp > maxStaffLp && lastStemEndLp > maxStaffLp) { return true; } //beam lines below the staff? if (firstStemEndLp < -1 * totalBeamHeightIs * 2 && lastStemEndLp < -1 * totalBeamHeightIs * 2) { return true; } } return false; } }