//----------------------------------------------------------------------------// // // // N o t 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.math.Rational; import omr.score.visitor.ScoreVisitor; import omr.sheet.NotePosition; import omr.sheet.Scale; import omr.util.TreeNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Line2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; /** * Class {@code Note} represents the characteristics of a note. * Besides a regular note (standard note, or rest), it can also be a cue note * or a grace note (these last two variants are not handled yet, TODO). * * @author Hervé Bitteur */ public class Note 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(Note.class); /** The quarter duration value */ public static final Rational QUARTER_DURATION = new Rational(1, 4); //~ Enumerations ----------------------------------------------------------- /** Names of the various note steps */ public static enum Step { //~ Enumeration constant initializers ---------------------------------- /** La */ A, /** Si */ B, /** Do */ C, /** Re */ D, /** Mi */ E, /** Fa */ F, /** Sol */ G; } //~ Instance fields -------------------------------------------------------- /** The note shape */ private final Shape shape; /** Pitch position */ private final double pitchPosition; /** * Cardinality of the note pack (stuck glyphs) this note is part of. * Card = 1 for an isolated note */ private final int packCard; /* Index within the note pack. Index = 0 for an isolated note */ private final int packIndex; /** Indicate a rest */ private final boolean isRest; /** First augmentation dot, if any */ private Glyph firstDot; /** Second augmentation dot, if any */ private Glyph secondDot; /** Accidental glyph, if any */ private Glyph accidental; /** Pitch alteration (not for rests) */ private Integer alter; /** Note step */ private Step step; /** Octave */ private Integer octave; /** Tie / slurs */ private Set<Slur> slurs = new HashSet<>(); /** Lyrics syllables (in different lines) */ private SortedSet<LyricsItem> syllables; //~ Constructors ----------------------------------------------------------- //------// // Note // //------// /** Create a new instance of an isolated Note * * @param chord the containing chord * @param glyph the underlying glyph */ public Note (Chord chord, Glyph glyph) { this( chord, glyph, getItemCenter(glyph, 0, chord.getScale().getInterline()), 1, 0); glyph.setTranslation(this); } //------// // Note // //------// /** * Create a note as a clone of another Note, into another chord * * @param chord the chord to host the newly created note * @param other the note to clone */ public Note (Chord chord, Note other) { super(chord); for (Glyph glyph : other.getGlyphs()) { addGlyph(glyph); } packCard = other.packCard; packIndex = other.packIndex; isRest = other.isRest; setCenter(other.getCenter()); setStaff(other.getStaff()); pitchPosition = other.pitchPosition; shape = other.getShape(); setBox(other.getBox()); for (Glyph glyph : getGlyphs()) { glyph.addTranslation(this); } // We specifically don't carry over: // slurs } //------// // Note // //------// /** Create a new instance of Note with no underlying glyph * * @param staff the containing staff * @param chord the containing chord * @param shape the provided shape * @param pitchPosition the pitchPosition * @param center the center (Point) of the note */ private Note (Staff staff, Chord chord, Shape shape, double pitchPosition, Point center) { super(chord); this.packCard = 1; this.packIndex = 0; // Rest? isRest = ShapeSet.Rests.contains(shape); // Staff setStaff(staff); // Pitch Position this.pitchPosition = pitchPosition; // Location center? if (center != null) { setCenter( new Point( center.x, (int) Math.rint( (staff.getTopLeft().y - staff.getSystem().getTopLeft().y) + ((chord.getScale().getInterline() * (4d + pitchPosition)) / 2)))); } // Note box setBox(null); // Shape of this note this.shape = baseShapeOf(shape); } //------// // Note // //------// /** Create a new instance of Note, as a chunk of a larger note pack. * * @param chord the containing chord * @param glyph the underlying glyph * @param center the center of the note instance * @param packCard the number of notes in the pack * @param packIndex the zero-based index of this note in the pack */ private Note (Chord chord, Glyph glyph, Point center, int packCard, int packIndex) { super(chord); addGlyph(glyph); this.packCard = packCard; this.packIndex = packIndex; ScoreSystem system = getSystem(); int interline = system.getScale().getInterline(); // Rest? isRest = ShapeSet.Rests.contains(glyph.getShape()); // Location center setCenter(center); // Note box setBox(getItemBox(glyph, packIndex, interline)); // Shape of this note shape = baseShapeOf(glyph.getShape()); // Determine proper staff and pitch position, using ledgers if any. NotePosition pos = getSystem().getInfo().getNoteStaffAt(getCenter()); setStaff(pos.getStaff().getScoreStaff()); pitchPosition = pos.getPitchPosition(); } //~ Methods ---------------------------------------------------------------- //------------// // createPack // //------------// /** * Create a bunch of Note instances for one note pack glyph. * A note pack is a glyph whose shape "contains" several notes, just like * a NOTEHEAD_BLACK_3 shape represents a vertical sequence of 3 heads. * Within a pack, notes are numbered by their vertical top down index, * counted from 0. * * <p>If we have 0 or 1 stem attached to the provided glyph, no specific * test is needed, all the note instances are created on the provided chord. * </p> * * <p>When the underlying glyph is stuck to 2 stems, we have to decide * which notes go to one stem (the left chord), which notes go to the other * (the right chord) and which notes go to both. * Every stem gets at least the vertically closest note. * At the end, to make sure that all notes from the initial pack are * actually assigned, we force the unassigned notes to the stem at hand. * </p> * * <p>(On the diagram below with a note pack of 3, the two upper notes * will go to the right stem, and the third note will go to the left stem). * </p> * <img src="doc-files/Note-Pack.png" alt="Pack of 3"> * * <p>(On the diagram below with a note pack of 1, the "mirrored" note is * duplicated, one instance goes to the left stem and the other to the * right stem). * </p> * <img src="doc-files/Note-Pack-both.png" alt="Shared note"> * * @param chord the containing chord * @param glyph the underlying glyph of the note pack * @param assigned (input/output) the set of note indices assigned so far * @param completing true if all unassigned notes must be assigned now */ public static void createPack (Chord chord, Glyph glyph, Set<Integer> assigned, boolean completing) { // Number of "notes" to create (not counting the duplicates) final int card = packCardOf(glyph.getShape()); final Glyph stem = chord.getStem(); Rectangle stemBox = null; Scale scale = chord.getScale(); // Variable stemSir exists only when we have a dual stem situation. // It gives the direction of the stem at hand (-1:down, +1:up) Integer stemDir = null; if (glyph.getStemNumber() >= 2) { stemBox = stem.getBounds(); stemBox.grow( scale.toPixels(constants.maxStemDx), scale.toPixels(constants.maxMultiStemDy)); stemDir = Integer.signum(glyph.getAreaCenter().y - stem.getAreaCenter().y); } // Index goes from top to bottom for (int i = 0; i < card; i++) { Rectangle itemBox = getItemBox(glyph, i, scale.getInterline()); if (stemDir != null) { // Apply the stem test, except on the item closest to the stem if ((stemDir == 1 && i > 0) || (stemDir == -1 && i < card - 1)) { if (!itemBox.intersects(stemBox)) { // Stem check failed. // Force assignment if unassigned and this is the end. if (assigned.contains(i) || !completing) { continue; } } } } Point center = new Point( itemBox.x + (itemBox.width / 2), itemBox.y + (itemBox.height / 2)); glyph.addTranslation(new Note(chord, glyph, center, card, i)); assigned.add(i); } } //--------// // accept // //--------// @Override public boolean accept (ScoreVisitor visitor) { return visitor.visit(this); } //---------------------// // getTranslationLinks // //---------------------// @Override public List<Line2D> getTranslationLinks (Glyph glyph) { if (getGlyphs().contains(glyph)) { Chord chord = getChord(); if (chord != null) { Point from = glyph.getLocation(); Glyph stem = chord.getStem(); Point to = stem != null ? stem.getAreaCenter() : chord.getReferencePoint(); Line2D line = new Line2D.Double(from, to); return Arrays.asList(line); } else { return Collections.emptyList(); } } else { return super.getTranslationLinks(glyph); } } //---------// // addSlur // //---------// /** * Add a slur in the collection of slurs connected to this note * * @param slur the slur to connect */ public void addSlur (Slur slur) { slurs.add(slur); } //------------// // removeSlur // //------------// /** * Remove a slur from the collection of slurs connected to this note * * @param slur the slur to remove */ public void removeSlur (Slur slur) { slurs.remove(slur); } //-------------// // addSyllable // //-------------// public void addSyllable (LyricsItem item) { if (syllables == null) { syllables = new TreeSet<>(LyricsItem.numberComparator); } syllables.add(item); } //-----------------// // createWholeRest // //-----------------// public static Note createWholeRest (Staff staff, Chord chord, Point center) { return new Note(staff, chord, Shape.WHOLE_REST, -1.5, center); } //--------------------// // populateAccidental // //--------------------// /** * Process the potential impact of an accidental glyph within the * containing measure * * @param glyph the underlying glyph of the accidental * @param measure the containing measure * @param accidCenter the center of the glyph */ public static void populateAccidental (Glyph glyph, Measure measure, final Point accidCenter) { if (glyph.isVip()) { logger.info("Note. populateAccidental {}", glyph.idString()); } final Scale scale = measure.getScale(); final int minDx = scale.toPixels(constants.minAccidDx); final int maxDx = scale.toPixels(constants.maxAccidDx); final int maxDy = scale.toPixels(constants.maxAccidDy); final List<Note> candidates = new ArrayList<>(); // An accidental impacts the note right after (even if mirrored) // Use an intersection rectangle defined from accidCenter Rectangle rect = new Rectangle( accidCenter.x + minDx, accidCenter.y - maxDy, maxDx - minDx, 2 * maxDy); glyph.addAttachment("#", rect); for (TreeNode node : measure.getChords()) { final Chord chord = (Chord) node; for (TreeNode n : chord.getNotes()) { final Note note = (Note) n; if (!note.isRest() && rect.contains(note.getCenterLeft())) { candidates.add(note); } } } // Select the closest candidate note using euclidian distance // from accidental center to note left center if (!candidates.isEmpty()) { Collections.sort(candidates, new Comparator<Note>() { @Override public int compare (Note n1, Note n2) { double dx1 = n1.getCenterLeft().x - accidCenter.x; double dy1 = n1.getCenterLeft().y - accidCenter.y; double ds1 = dx1 * dx1 + dy1 * dy1; double dx2 = n2.getCenterLeft().x - accidCenter.x; double dy2 = n2.getCenterLeft().y - accidCenter.y; double ds2 = dx2 * dx2 + dy2 * dy2; return Double.compare(ds1, ds2); } }); logger.debug("{} Candidates={}", candidates.size(), candidates); glyph.clearTranslations(); Note bestNote = candidates.get(0); bestNote.accidental = glyph; glyph.addTranslation(bestNote); logger.debug("{} accidental {} at {}", bestNote.getContextString(), glyph.getShape(), bestNote.getCenter()); // Apply also to mirrored note if any Note mirrored = bestNote.getMirroredNote(); if (mirrored != null) { mirrored.accidental = glyph; glyph.addTranslation(mirrored); logger.debug("{} accidental {} at {} (mirrored)", mirrored.getContextString(), glyph.getShape(), mirrored.getCenter()); } } else { // Deassign the glyph if (!glyph.isManualShape()) { glyph.setShape(null, Evaluation.ALGORITHM); } } } //---------------// // getAccidental // //---------------// /** * Report the accidental, if any, related to this note * * @return the accidental, or null */ public Glyph getAccidental () { return accidental; } //----------------// // getActualShape // //----------------// public static Shape getActualShape (Shape base, int card) { switch (card) { case 3: switch (base) { case NOTEHEAD_VOID: return NOTEHEAD_VOID_3; case NOTEHEAD_BLACK: return NOTEHEAD_BLACK_3; case WHOLE_NOTE: return WHOLE_NOTE_3; default: return null; } case 2: switch (base) { case NOTEHEAD_VOID: return NOTEHEAD_VOID_2; case NOTEHEAD_BLACK: return NOTEHEAD_BLACK_2; case WHOLE_NOTE: return WHOLE_NOTE_2; default: return null; } case 1: return base; default: return null; } } //----------// // getAlter // //----------// /** * Report the actual alteration of this note, taking into account * the accidental of this note if any, the accidental of previous * note with same step within the same measure, and finally the * current key signature. * * @return the actual alteration */ public int getAlter () { if (alter == null) { // Look for local accidental if (accidental != null) { return alter = alterationOf(accidental); } // Look for previous accidental with same note step in the measure List<Slot> slots = getMeasure().getSlots(); boolean started = false; for (ListIterator<Slot> it = slots.listIterator(slots.size()); it.hasPrevious();) { Slot slot = it.previous(); // Inspect all notes of all chords for (Chord chord : slot.getChords()) { for (TreeNode node : chord.getNotes()) { Note note = (Note) node; if (note == this) { started = true; } else if (started && (note.getStep() == getStep()) && (note.getAccidental() != null)) { return alter = alterationOf(note.getAccidental()); } } } } // Finally, use the current key signature KeySignature ks = getMeasure().getKeyBefore(getCenter(), getStaff()); if (ks != null) { return alter = ks.getAlterFor(getStep()); } // Nothing found, so... alter = 0; } return alter; } //--------------// // alterationOf // //--------------// /** * Report the pitch alteration that corresponds to the provided * accidental. * * @param accidental the provided accidental * @return the pitch impact */ private int alterationOf (Glyph accidental) { switch (accidental.getShape()) { case SHARP: return 1; case DOUBLE_SHARP: return 2; case FLAT: return -1; case DOUBLE_FLAT: return -2; case NATURAL: return 0; default: logger.warn("Weird shape {} for accidental {}", accidental.getShape(), accidental.idString()); return 0; // Should not happen } } //-----------------// // getCenterBottom // //-----------------// /** * Report the system point at the center bottom of the note * * @return center point at bottom of note */ public Point getCenterBottom () { return new Point( getCenter().x, getCenter().y + (getBox().height / 2)); } //---------------// // getCenterLeft // //---------------// /** * Report the system point at the center left of the note * * @return left point at mid height */ public Point getCenterLeft () { return new Point( getCenter().x - (getBox().width / 2), getCenter().y); } //----------------// // getCenterRight // //----------------// /** * Report the system point at the center right of the note * * @return right point at mid height */ public Point getCenterRight () { return new Point( getCenter().x + (getBox().width / 2), getCenter().y); } //--------------// // getCenterTop // //--------------// /** * Report the system point at the center top of the note * * @return center point at top of note */ public Point getCenterTop () { return new Point( getCenter().x, getCenter().y - (getBox().height / 2)); } //----------// // getChord // //----------// /** * Report the chord this note is part of * * @return the containing chord (cannot be null) */ public Chord getChord () { return (Chord) getParent(); } //-----------------// // getTypeDuration // //-----------------// /** * Report the duration indicated by the shape of the note head * (regardless of any beam, flag, dot or tuplet). * * @param shape the shape of the note head * @return the corresponding duration */ public static Rational getTypeDuration (Shape shape) { switch (baseShapeOf(shape)) { case LONG_REST: // 4 measures return new Rational(4, 1); case BREVE_REST: // 2 measures case BREVE: return new Rational(2, 1); case WHOLE_REST: // 1 measure case WHOLE_NOTE: return Rational.ONE; case HALF_REST: case NOTEHEAD_VOID: return new Rational(1, 2); case QUARTER_REST: case NOTEHEAD_BLACK: return QUARTER_DURATION; case EIGHTH_REST: return new Rational(1, 8); case ONE_16TH_REST: return new Rational(1, 16); case ONE_32ND_REST: return new Rational(1, 32); case ONE_64TH_REST: return new Rational(1, 64); case ONE_128TH_REST: return new Rational(1, 128); default: // Error logger.error("Illegal note type {}", shape); return Rational.ZERO; } } //-------------// // getFirstDot // //-------------// /** * Report the first augmentation dot, if any * * @return first dot or null */ public Glyph getFirstDot () { return firstDot; } //-----------------// // getMirroredNote // //-----------------// /** * If any, report the note that is the mirror of this one. * This happens when the same note head is "shared" by 2 chords (because the * note head is shared by 2 stems or by 1 stem leading to 2 beam groups). * * @return the mirrored note, or null if none. */ public Note getMirroredNote () { // We use the underlying glyph which keeps the link to translated notes Collection<Glyph> glyphs = getGlyphs(); if (glyphs.isEmpty()) { return null; } Glyph glyph = glyphs.iterator().next(); for (Object obj : glyph.getTranslations()) { if ((obj != this) && obj instanceof Note) { Note that = (Note) obj; if (that.getPitchPosition() == this.getPitchPosition()) { return that; } } } return null; } //-----------------// // getNoteDuration // //-----------------// /** * Report the duration of this note, based purely on its shape and * the number of beams or flags. * This does not take into account the potential augmentation dots, nor * tuplets. * The purpose of this method is to find out the name of the note * ("eighth" versus "quarter" for example) * * @return the intrinsic note duration */ public Rational getNoteDuration () { Rational dur = getTypeDuration(shape); // Apply fraction if any (not for rests) due to beams or flags if (!isRest()) { int fbn = getChord().getFlagsNumber() + getChord().getBeams().size(); if (fbn > 0) { /** * Beware, some mirrored notes exhibit a void note head * because the same head is shared by a half-note and at * the same time by a beam group. * In the case of the beam/flag side of the mirror, strictly * speaking, the note head should be considered as black. */ if ((shape == NOTEHEAD_VOID) && (getMirroredNote() != null)) { dur = getTypeDuration(NOTEHEAD_BLACK); } // Apply the divisions for (int i = 0; i < fbn; i++) { dur = dur.divides(2); } } } return dur; } //-----------// // getOctave // //-----------// /** * Report the octave for this note, using the current clef, and the * pitch position of the note. * * @return the related octave */ public int getOctave () { if (octave == null) { octave = Clef.octaveOf( getMeasure().getClefBefore(getCenter(), getStaff()), (int) Math.rint(getPitchPosition())); } return octave; } //--------------------// // getPackCardinality // //--------------------// public int getPackCardinality () { return packCard; } //------------------// // getPitchPosition // //------------------// /** * Report the pith position of the note within the containing staff * * @return staff-based pitch position */ public double getPitchPosition () { return pitchPosition; } //--------------// // getSecondDot // //--------------// /** * Report the second augmentation dot, if any * * @return second dot or null */ public Glyph getSecondDot () { return secondDot; } //----------// // getShape // //----------// /** * Report the shape of the note * * @return the note shape */ public Shape getShape () { return shape; } //----------// // getSlurs // //----------// /** * Report the collection of slurs that start or stop at this note * * @return a perhaps empty collection of slurs */ public Set<Slur> getSlurs () { return Collections.unmodifiableSet(slurs); } //---------// // getStep // //---------// /** * Report the note step (within the octave) * * @return the note step */ public Note.Step getStep () { if (step == null) { step = Clef.noteStepOf( getMeasure().getClefBefore(getCenter(), getStaff()), (int) Math.rint(getPitchPosition())); } return step; } //--------------// // getSyllables // //--------------// public SortedSet<LyricsItem> getSyllables () { return syllables; } //--------// // isRest // //--------// /** * Check whether this note is a rest (vs a 'real' note) * * @return true if a rest, false otherwise */ public boolean isRest () { return isRest; } //--------// // moveTo // //--------// /** * Move this note from its current chord to the provided chord * * @param chord the new hosting chord */ public void moveTo (Chord chord) { getParent().getChildren().remove(this); chord.addChild(this); for (Glyph glyph : getGlyphs()) { if (packCard > 1) { glyph.addTranslation(this); } else { glyph.setTranslation(this); } } } //---------// // setDots // //---------// /** * Define the number of augmentation dots that impact this chord * * @param first the glyph of first dot * @param second the glyph of second dot (if any) */ public void setDots (Glyph first, Glyph second) { firstDot = first; secondDot = second; } //----------// // toString // //----------// @Override public String toString () { StringBuilder sb = new StringBuilder(); sb.append("{Note"); sb.append(" ").append(shape); sb.append(" Ch#").append(getChord().getId()); if (packCard != 1) { sb.append(" [").append(packIndex + 1) // For easier reading by end user .append("/").append(packCard).append("]"); } if (accidental != null) { sb.append(" accid=").append(accidental.getShape()); } if (isRest) { sb.append(" ").append("rest"); } if (alter != null) { sb.append(" alter=").append(alter); } sb.append(" pp=").append((float) pitchPosition); sb.append(" ").append(Glyphs.toString(getGlyphs())); sb.append("}"); return sb.toString(); } //-------------// // baseShapeOf // //-------------// private static Shape baseShapeOf (Shape shape) { switch (shape) { case NOTEHEAD_BLACK: case NOTEHEAD_BLACK_2: case NOTEHEAD_BLACK_3: return Shape.NOTEHEAD_BLACK; case NOTEHEAD_VOID: case NOTEHEAD_VOID_2: case NOTEHEAD_VOID_3: return Shape.NOTEHEAD_VOID; case WHOLE_NOTE: case WHOLE_NOTE_2: case WHOLE_NOTE_3: return Shape.WHOLE_NOTE; default: return shape; } } //------------// // getItemBox // //------------// /** * Compute the bounding box of item with rank 'index' in the * provided note pack glyph. */ private static Rectangle getItemBox (Glyph glyph, int index, int interline) { final Shape shape = glyph.getShape(); final int card = packCardOf(shape); final Rectangle box = glyph.getBounds(); // For true notes use centroid y, for rests use area center y if (ShapeSet.Rests.contains(shape)) { return glyph.getBounds(); } else { final Point centroid = glyph.getCentroid(); final int top = centroid.y - ((card * interline) / 2); return new Rectangle( box.x, top + (index * interline), box.width, interline); } } //---------------// // getItemCenter // //---------------// /** * Compute the center of item with rank 'index' in the provided note * pack glyph. */ private static Point getItemCenter (Glyph glyph, int index, int interline) { Rectangle box = getItemBox(glyph, index, interline); return new Point( box.x + (box.width / 2), box.y + (box.height / 2)); } //------------// // packCardOf // //------------// public static int packCardOf (Shape shape) { switch (shape) { case NOTEHEAD_VOID_3: case NOTEHEAD_BLACK_3: case WHOLE_NOTE_3: return 3; case NOTEHEAD_VOID_2: case NOTEHEAD_BLACK_2: case WHOLE_NOTE_2: return 2; default: return 1; } } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Scale.Fraction minAccidDx = new Scale.Fraction( 0.3d, "Minimum dx between accidental and note"); Scale.Fraction maxAccidDx = new Scale.Fraction( 4d, "Maximum dx between accidental and note"); Scale.Fraction maxAccidDy = new Scale.Fraction( 0.5d, "Maximum absolute dy between note and accidental"); Scale.Fraction maxStemDx = new Scale.Fraction( 0.25d, "Maximum absolute dx between note and stem"); Scale.Fraction maxSingleStemDy = new Scale.Fraction( 3d, "Maximum absolute dy between note and single stem end"); Scale.Fraction maxMultiStemDy = new Scale.Fraction( 0.25d, "Maximum absolute dy between note and multi stem end"); Scale.Fraction maxCenterDy = new Scale.Fraction( 0.1d, "Maximum absolute dy between note center and centroid"); } }