//----------------------------------------------------------------------------// // // // T i m e S i g n a t u r e // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. // // This software is released under the GNU General Public License. // // Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // //----------------------------------------------------------------------------// // </editor-fold> package omr.score.entity; import omr.constant.ConstantSet; import omr.glyph.Evaluation; import omr.glyph.Glyphs; import omr.glyph.Shape; import static omr.glyph.Shape.*; import omr.glyph.ShapeSet; import omr.glyph.facets.Glyph; import omr.score.visitor.ScoreVisitor; import omr.sheet.Scale; import omr.sheet.SystemInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Point; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Class {@code TimeSignature} encapsulates a time signature, * which may be composed of one or several glyphs. * * @author Hervé Bitteur */ public class TimeSignature extends MeasureNode { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(TimeSignature.class); /** Rational value of each (full) time sig shape */ private static final Map<Shape, TimeRational> rationals = new EnumMap<>( Shape.class); static { for (Shape s : ShapeSet.FullTimes) { if (s != CUSTOM_TIME) { TimeRational nd = rationalOf(s); if (nd == null) { logger.error("Rational for ''{}'' is not defined", s); } else { rationals.put(s, nd); } } } } /** Set of acceptable N/D values for programmatic recognition */ private static final Set<TimeRational> acceptables = new HashSet<>(); static { // Predefined for (Shape s : ShapeSet.FullTimes) { TimeRational nd = rationals.get(s); if (nd != null) { acceptables.add(nd); } } // A few others acceptables.add(new TimeRational(5, 4)); } //~ Instance fields -------------------------------------------------------- // /** Flag a time sig not created out of its glyphs. */ private final boolean isDummy; /** * Precise time signature shape (if any, since we may have no * predefined shape for complex time signatures). * * Since a time signature may be composed incrementally through several * glyphs, its shape is determined only when needed. * * In case of failure, a NO_LEGAL_TIME shape is assigned, * preventing any further computation until a reset is performed on this * item (such a reset is performed for example when an additional glyph is * added to the item). */ private Shape shape; /** Actual TimeRational components. */ private TimeRational timeRational; //~ Constructors ----------------------------------------------------------- // //---------------// // TimeSignature // //--------------// /** * Create a time signature, with containing measure and related staff * * @param measure the containing measure * @param staff the related staff */ public TimeSignature (Measure measure, Staff staff) { super(measure); isDummy = false; setStaff(staff); logger.debug("Created TS"); } //---------------// // TimeSignature // //--------------// /** * Create a time signature, with containing measure and related * staff, and information copied from the provided time signature. * * @param measure the containing measure * @param staff the related staff * @param other the other time signature to clone */ public TimeSignature (Measure measure, Staff staff, TimeSignature other) { super(measure); isDummy = true; setStaff(staff); try { timeRational = other.timeRational; shape = other.getShape(); if (!staff.isDummy()) { setCenter( new Point( other.getCenter().x, other.getCenter().y - other.getStaff().getTopLeft().y + staff.getTopLeft().y)); } } catch (InvalidTimeSignature ex) { logger.error("Cannot duplicate TimeSignature", ex); } logger.debug("Created TS copy"); } //~ Methods ---------------------------------------------------------------- // //--------------// // isAcceptable // //--------------// /** * Check whether the provided value is a rather common time signature * * @param timeRational * @return true if the provided sig is acceptable */ public static boolean isAcceptable (TimeRational timeRational) { if (predefinedShape(timeRational) != null) { return true; } // Check other acceptable rational values return acceptables.contains(timeRational); } //--------// // accept // //--------// @Override public boolean accept (ScoreVisitor visitor) { return visitor.visit(this); } //------// // copy // //------// /** * Replaces in situ this time signature by the logical information of * 'newSig'. * * @param newSig the correct sig to assign in lieu of this one */ public void copy (TimeSignature newSig) { try { modify(newSig.getShape(), newSig.timeRational); } catch (InvalidTimeSignature ex) { logger.warn("Invalid time signature", ex); } } //-----------------// // createDummyCopy // //-----------------// public TimeSignature createDummyCopy (Measure measure, Point center) { TimeSignature dummy = new TimeSignature(measure, null); dummy.setCenter(center); dummy.timeRational = timeRational; dummy.shape = shape; return dummy; } //----------------// // getDenominator // //----------------// /** * Report the bottom part of the time signature. * * @return the bottom part * @throws InvalidTimeSignature */ public Integer getDenominator () throws InvalidTimeSignature { if (timeRational == null) { if (shape == NO_LEGAL_TIME) { throw new InvalidTimeSignature(); } else { computeTimeRational(); } } if (timeRational != null) { return timeRational.den; } else { return null; } } //----------------------// // getDenominatorShapes // //----------------------// /** * Report the sequence of shapes that depict the denominator. * * @return for example: [1, 6] if denominator is 16 */ public List<Shape> getDenominatorShapes () { try { return getShapes(getDenominator()); } catch (InvalidTimeSignature ex) { return Collections.emptyList(); } } //--------------// // getNumerator // //--------------// /** * Report the top part of the time signature. * * @return the top part * @throws InvalidTimeSignature */ public Integer getNumerator () throws InvalidTimeSignature { if (timeRational == null) { if (shape == NO_LEGAL_TIME) { throw new InvalidTimeSignature(); } else { computeTimeRational(); } } if (timeRational != null) { return timeRational.num; } else { return null; } } //--------------------// // getNumeratorShapes // //--------------------// /** * Report the sequence of shapes that depict the numerator. * * @return for example: [1, 2] if numerator is 12 */ public List<Shape> getNumeratorShapes () { try { return getShapes(getNumerator()); } catch (InvalidTimeSignature ex) { return Collections.emptyList(); } } //----------// // getShape // //----------// /** * Report the shape of this time signature. * * @return the (lazily determined) shape, which may be null * @throws omr.score.entity.TimeSignature.InvalidTimeSignature */ public Shape getShape () throws InvalidTimeSignature { if (shape == null) { computeTimeRational(); } return shape; } //-----------------// // getTimeRational // //-----------------// /** * Report the time signature as a rational * * @return the num/den time rational, or null */ public TimeRational getTimeRational () throws InvalidTimeSignature { if (timeRational == null) { computeTimeRational(); } return timeRational; } //---------// // isDummy // //---------// /** * Report whether this time signature has been built artificially * * @return true if artificial */ public boolean isDummy () { return isDummy; } //----------// // isManual // //----------// /** * Report whether this time signature is based on manual assignment * * @return true if manually assigned */ public boolean isManual () { return Glyphs.containsManual(getGlyphs()); } //--------// // modify // //--------// /** * Modify in situ this time signature using provided shape and * rational value. * We use the intersected glyphs of the old sig as the glyphs for the newly * built signature. * * @param shape the shape (perhaps null) of correct signature * @param timeRational the new sig rational value */ public void modify (Shape shape, TimeRational timeRational) { if (!isDummy()) { if (shape == null) { shape = predefinedShape(timeRational); if (shape == null) { shape = Shape.CUSTOM_TIME; } } SystemInfo systemInfo = getSystem().getInfo(); Glyph compound = systemInfo.buildTransientCompound( getGlyphs()); compound = systemInfo.addGlyph(compound); compound.setShape(shape, Evaluation.ALGORITHM); if (shape == Shape.CUSTOM_TIME) { compound.setTimeRational(timeRational); } logger.debug("{} assigned to {}", shape, compound.idString()); } setRational(timeRational, shape); } //----------// // populate // //----------// /** * Populate the score with a time signature built from the provided glyph * * @param glyph the source glyph * @param measure containing measure * @param staff the related staff * @param center the glyph center wrt system * @return true if population is successful, false otherwise */ public static boolean populate (Glyph glyph, Measure measure, Staff staff, Point center) { // First, some basic tests // Horizontal distance since beginning of measure int unitDx = center.x - measure.getLeftX(); if (unitDx < measure.getScale().toPixels(constants.minTimeOffset)) { logger.debug("Too small offset for time signature" + " (glyph #{})", glyph.getId()); return false; } // Then, processing depends on partial / full time-signature Shape shape = glyph.getShape(); if (ShapeSet.PartialTimes.contains(shape)) { return populatePartialTime(glyph, measure, staff); } else { return populateFullTime(glyph, measure, staff); } } //------------------// // populateFullTime // //------------------// /** * We create a full time signature with just the provided glyph. * (assumed to be the whole signature, perhaps composed of several digits, * for example one digit for the numerator and one digit for the * denominator) * * @param glyph the provided (multi-digit) glyph * @param measure the containing measure * @param staff the related satff * @return true if successful */ public static boolean populateFullTime (Glyph glyph, Measure measure, Staff staff) { logger.debug("populateFullTime with {}", glyph); TimeSignature ts = measure.getTimeSignature(staff); if (ts == null) { ts = new TimeSignature(measure, staff); ts.addGlyph(glyph); glyph.setTranslation(ts); return true; } else { logger.debug("Second whole time signature ({}" + ")" + " in the same measure", glyph.idString()); return false; } } //------------// // rationalOf // //------------// /** * Report the num/den pair of predefined timesig shapes. * * @param shape the queried shape * @return the related num/den or null */ public static TimeRational rationalOf (Shape shape) { switch (shape) { case COMMON_TIME: case TIME_FOUR_FOUR: return new TimeRational(4, 4); case CUT_TIME: case TIME_TWO_TWO: return new TimeRational(2, 2); case TIME_TWO_FOUR: return new TimeRational(2, 4); case TIME_THREE_FOUR: return new TimeRational(3, 4); case TIME_SIX_EIGHT: return new TimeRational(6, 8); default: return null; } } //-------// // reset // //-------// /** * Invalidate cached data, so that it gets lazily recomputed when * needed. */ @Override public void reset () { super.reset(); shape = null; timeRational = null; } //-------------// // setRational // //-------------// /** * Force this time signature to align to the provided rational value. * * @param timeRational the forced value * @param shape the forced shape, if any */ public void setRational (TimeRational timeRational, Shape shape) { this.timeRational = timeRational; if (shape != null) { this.shape = shape; } else { this.shape = predefinedShape(); } } //----------// // toString // //----------// /** * Report a readable description. * * @return description */ @Override public String toString () { StringBuilder sb = new StringBuilder(); sb.append("{TimeSignature"); try { if (getNumerator() != null) { sb.append(" ").append(getNumerator()).append("/").append( getDenominator()); } if (getShape() != null) { sb.append(" ").append(getShape()); } if (getCenter() != null) { sb.append(" center=").append(getCenter()); } if (!getGlyphs().isEmpty()) { sb.append(" ").append(Glyphs.toString(getGlyphs())); } } catch (InvalidTimeSignature e) { sb.append(" INVALID"); } sb.append("}"); return sb.toString(); } //---------------// // computeCenter // //---------------// @Override protected void computeCenter () { setCenter(computeGlyphsCenter(glyphs)); } //-----------------// // getNumericShape // //-----------------// private static Shape getNumericShape (int val) { switch (val) { case 0: return TIME_ZERO; case 1: return TIME_ONE; case 2: return TIME_TWO; case 3: return TIME_THREE; case 4: return TIME_FOUR; case 5: return TIME_FIVE; case 6: return TIME_SIX; case 7: return TIME_SEVEN; case 8: return TIME_EIGHT; case 9: return TIME_NINE; case 12: return TIME_TWELVE; case 16: return TIME_SIXTEEN; default: return null; } } //-----------------// // getNumericValue // //-----------------// private static Integer getNumericValue (Glyph glyph) { Shape shape = glyph.getShape(); if (shape != null) { switch (shape) { case TIME_ZERO: return 0; case TIME_ONE: return 1; case TIME_TWO: return 2; case TIME_THREE: return 3; case TIME_FOUR: return 4; case TIME_FIVE: return 5; case TIME_SIX: return 6; case TIME_SEVEN: return 7; case TIME_EIGHT: return 8; case TIME_NINE: return 9; case TIME_TWELVE: return 12; case TIME_SIXTEEN: return 16; } } return null; } //-----------// // getShapes // //-----------// private List<Shape> getShapes (int val) { List<Shape> shapes = new ArrayList<>(); for (; val > 0; val /= 10) { int digit = val % 10; shapes.add(getNumericShape(digit)); } Collections.reverse(shapes); return shapes; } //---------------------// // populatePartialTime // //---------------------// /** * We add the provided glyph to a time signature composed of single * digits. * * @param glyph the provided (single-digit) glyph * @param measure the containing measure * @param staff the related staff * @return true if successful */ private static boolean populatePartialTime (Glyph glyph, Measure measure, Staff staff) { logger.debug("populatePartialTime with {}", glyph); TimeSignature ts = measure.getTimeSignature(staff); if (ts != null) { // Check we are not too far from this first time signature part Point center = measure.computeGlyphCenter(glyph); double dist = center.distance(ts.getCenter()); double max = measure.getScale().toPixelsDouble( constants.maxTimeDistance); if (dist > max) { logger.debug("Time signature part ({}" + ")" + " too far from previous one", glyph.idString()); return false; } } else { // Start a brand new time sig ts = new TimeSignature(measure, staff); } ts.addGlyph(glyph); glyph.setTranslation(ts); return true; } //---------------------// // computeTimeRational // //---------------------// /** * Compute the actual members of time rational. * * @throws InvalidTimeSignature */ private void computeTimeRational () throws InvalidTimeSignature { if (glyphs == null) { throw new InvalidTimeSignature(); } if (!glyphs.isEmpty()) { if (glyphs.size() == 1) { // Just one symbol Glyph theGlyph = glyphs.first(); Shape theShape = theGlyph.getShape(); if (theShape != null) { if (ShapeSet.FullTimes.contains(theShape)) { TimeRational theRational = (theShape == CUSTOM_TIME) ? theGlyph.getTimeRational() : rationalOf(theShape); if (theRational != null) { shape = theShape; this.timeRational = theRational; } return; } addError(glyphs.first(), "Weird single time component"); return; } } else { // Several symbols // Dispatch symbols on top and bottom parts timeRational = null; int numerator = 0; int denominator = 0; for (Glyph glyph : glyphs) { int pitch = (int) Math.rint( getStaff().pitchPositionOf(computeGlyphCenter(glyph))); Integer value = getNumericValue(glyph); logger.debug("pitch={} value={} glyph={}", pitch, value, glyph); if (value != null) { if (pitch < 0) { numerator = (10 * numerator) + value; } else if (pitch > 0) { denominator = (10 * denominator) + value; } else { addError( glyph, "Multi-symbol time signature" + " with a component of pitch position 0"); } } else { addError( glyph, "Time signature component with no numeric value"); } } if ((numerator != 0) && (denominator != 0)) { timeRational = new TimeRational(numerator, denominator); } // Try to assign a predefined shape shape = predefinedShape(); } logger.debug("time rational: {}", timeRational); } } //-----------------// // predefinedShape // //-----------------// /** * Look for a predefined shape, if any, that would correspond to the * current * <code>num</code> and * <code>den</code> values of this time * sig. * * @return the shape found or null */ private static Shape predefinedShape (TimeRational timeRational) { if (timeRational == null) { return null; // Safer } for (Shape s : ShapeSet.FullTimes) { TimeRational nd = rationals.get(s); if (timeRational.equals(nd)) { return s; } } return null; } //-----------------// // predefinedShape // //-----------------// /** * Look for a predefined shape, if any, that would correspond to the * current * <code>num</code> and * <code>den</code> values of this time * sig. * * @return the shape found or null */ private Shape predefinedShape () { if (timeRational == null) { return null; // Safer } else { return predefinedShape(timeRational); } } //~ Inner Classes ---------------------------------------------------------- //----------------------// // InvalidTimeSignature // //----------------------// /** * Used to signal that a time signature is invalid. * (for example because some of its parts are missing or incorrectly * recognized) */ public static class InvalidTimeSignature extends Exception { //~ Constructors ------------------------------------------------------- public InvalidTimeSignature () { super("Time signature is invalid"); } } //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Scale.Fraction maxTimeDistance = new Scale.Fraction( 4d, "Maximum euclidian distance between two" + " parts of a time signature"); Scale.Fraction minTimeOffset = new Scale.Fraction( 1d, "Minimum horizontal offset for a time" + " signature since start of measure"); } }