//----------------------------------------------------------------------------// // // // C h o r d // // // //----------------------------------------------------------------------------// // <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.Shape; import static omr.glyph.Shape.*; import omr.glyph.ShapeSet; import omr.glyph.facets.Glyph; import omr.math.Rational; import omr.score.entity.Voice.VoiceChord; import omr.score.visitor.ScoreVisitor; import omr.sheet.Scale; import omr.util.HorizontalSide; import omr.util.TreeNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Point; import java.awt.Polygon; 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.Set; import java.util.SortedSet; import java.util.TreeSet; /** * Class {@code Chord} represents an ensemble of entities (rests, notes) * attached to the same stem if any, and that occur on the same time in * a staff. * <p><b>NB</b>We assume that all notes of a chord have the same duration. * * @author Hervé Bitteur */ public class Chord 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(Chord.class); /** Compare two chords by abscissa within a measure. */ public static final Comparator<TreeNode> byNodeAbscissa = new Comparator<TreeNode>() { @Override public int compare (TreeNode tn1, TreeNode tn2) { Chord c1 = (Chord) tn1; Chord c2 = (Chord) tn2; return byAbscissa.compare(c1, c2); } }; /** Compare two chords by abscissa within a measure. */ public static final Comparator<Chord> byAbscissa = new Comparator<Chord>() { @Override public int compare (Chord c1, Chord c2) { return Integer.signum(c1.getHeadLocation().x - c2.getHeadLocation().x); } }; /** Comparator to sort chords vertically within the same slot. */ public static final Comparator<Chord> byOrdinate = new Comparator<Chord>() { @Override public int compare (Chord c1, Chord c2) { Note n1 = (Note) c1.getNotes().get(0); Note n2 = (Note) c2.getNotes().get(0); // First : staff int dStaff = n1.getStaff().getId() - n2.getStaff().getId(); if (dStaff != 0) { return Integer.signum(dStaff); } // Second : head ordinate int dHead = c1.getHeadLocation().y - c2.getHeadLocation().y; if (dHead != 0) { return Integer.signum(dHead); } // Third : chord id return Integer.signum(c1.getId() - c2.getId()); } }; /** * Compare two notes of the same chord, ordered by increasing * distance from chord head ordinate. */ public static Comparator<TreeNode> noteHeadComparator = new Comparator<TreeNode>() { @Override public int compare (TreeNode tn1, TreeNode tn2) { if (tn1 == tn2) { return 0; } Note n1 = (Note) tn1; Note n2 = (Note) tn2; if (n1.getChord() != n2.getChord()) { logger.error("Ordering notes from different chords"); } return n1.getChord().getStemDir() * (n2.getCenter().y - n1. getCenter().y); } }; //~ Instance fields -------------------------------------------------------- /** Id for debug (unique within measure) */ private final int id; /** A chord stem is virtual when there is no real stem (breve, rest...) */ private Glyph stem; /** Containing slot, if any (no slot for whole/multi rests) */ private Slot slot; /** Location for chord head (head farthest from chord tail) */ private Point headLocation; /** Location for chord tail */ private Point tailLocation; /** Collection of beams this chord is connected to */ private List<Beam> beams = new ArrayList<>(); /** Number of augmentation dots */ private int dotsNumber; /** Flag glyphs */ private Set<Glyph> flagGlyphs = new HashSet<>(); /** Number of flags (a beam is not a flag) */ private int flagsNumber; /** Ratio to get actual rawDuration wrt graphical notation */ private DurationFactor tupletFactor; /** Start time since beginning of the containing measure */ private Rational startTime; /** Voice this chord belongs to */ private Voice voice; /** Collection of marks for user */ private List<Mark> marks = new ArrayList<>(); /** Notations related to this chord */ private List<Notation> notations = new ArrayList<>(); /** Directions (loosely) related to this chord */ private List<Direction> directions = new ArrayList<>(); /** Symbol (chord name), if any, related to this chord */ private ChordSymbol chordSymbol; /** Compare two beams linked to this chord, ordered from tail. */ private Comparator<Beam> byLevel = new Comparator<Beam>() { @Override public int compare (Beam b1, Beam b2) { int x = stem.getLocation().x; int y1 = b1.getLine().yAtX(x); int y2 = b2.getLine().yAtX(x); int yHead = getHeadLocation().y; int result = Integer.signum( Math.abs(yHead - y2) - Math.abs(yHead - y1)); return result; } }; //~ Constructors ----------------------------------------------------------- // //-------// // Chord // //-------// /** * Creates a new instance of Chord * * @param measure the containing measure * @param slot the containing slot (null for whole/multi rest chords) */ public Chord (Measure measure, Slot slot) { super(measure); reset(); this.slot = slot; id = measure.getChords().size(); } //~ Methods ---------------------------------------------------------------- //--------------// // getFlagShape // //--------------// public static Shape getFlagShape (int fn, boolean up) { switch (fn) { case 1: return up ? FLAG_1_UP : FLAG_1; case 2: return up ? FLAG_2_UP : FLAG_2; case 3: return up ? FLAG_3_UP : FLAG_3; case 4: return up ? FLAG_4_UP : FLAG_4; case 5: return up ? FLAG_5_UP : FLAG_5; } logger.error("Illegal flag number: {}", fn); return null; } //---------------// // getStemChords // //---------------// /** * Find the chord(s) that are carried by a given stem * * @param measure the containing measure * @param stem the given stem * @return the collection of related chords, which may be empty if no chord * is yet attached to this stem */ public static List<Chord> getStemChords (Measure measure, Glyph stem) { List<Chord> chords = new ArrayList<>(); for (TreeNode node : measure.getChords()) { Chord chord = (Chord) node; if (chord.getStem() == stem) { chords.add(chord); } } return chords; } //-------------------------// // lookupInterleavedChords // //-------------------------// /** * Look up for all chords interleaved between the given stemed chords * * @param left the chord on the left of the area * @param right the chord on the right of the area * @return the collection of interleaved chords, which may be empty */ public static SortedSet<Chord> lookupInterleavedChords (Chord left, Chord right) { SortedSet<Chord> found = new TreeSet<>(Chord.byAbscissa); if ((left == null) || (right == null)) { return found; // Safer } // Define the area limited by the left and right chords with their stems // and check for intersection with a rest note // More precisely, we use the area half on the tail side. // And we check that the interleaved chords have the same stem dir Polygon polygon = new Polygon(); polygon.addPoint(left.getHeadLocation().x, left.getCenter().y); polygon.addPoint(left.getTailLocation().x, left.getTailLocation().y); polygon.addPoint(right.getTailLocation().x, right.getTailLocation().y); polygon.addPoint(right.getHeadLocation().x, right.getCenter().y); for (TreeNode node : left.getMeasure().getChords()) { Chord chord = (Chord) node; // Not interested in the bounding chords (TBC) if ((chord == left) || (chord == right)) { continue; } // Additional check on stem dir, if left & right agree if (left.getStemDir() == right.getStemDir()) { if (chord.getReferencePoint() == null || (chord.getStemDir() != 0 && chord.getStemDir() != left.getStemDir())) { continue; } } Rectangle box = chord.getBox(); if (polygon.intersects(box.x, box.y, box.width, box.height)) { found.add(chord); } } return found; } //------------// // lookupRest // //------------// /** * Look up for a potential rest interleaved between the given stemed * chords * * @param left the chord on the left of the area * @param right the chord on the right of the area * @return the rest found, or null otherwise */ public static Note lookupRest (Chord left, Chord right) { // Define the area limited by the left and right chords with their stems // and check for intersection with a rest note Polygon polygon = new Polygon(); polygon.addPoint(left.headLocation.x, left.headLocation.y); polygon.addPoint(left.tailLocation.x, left.tailLocation.y); polygon.addPoint(right.tailLocation.x, right.tailLocation.y); polygon.addPoint(right.headLocation.x, right.headLocation.y); for (TreeNode node : left.getMeasure().getChords()) { Chord chord = (Chord) node; // Not interested in the bounding chords if ((chord == left) || (chord == right)) { continue; } for (TreeNode n : chord.getNotes()) { Note note = (Note) n; // Interested in rest notes only if (note.isRest()) { Rectangle box = note.getBox(); if (polygon.intersects(box.x, box.y, box.width, box.height)) { return note; } } } } return null; } //--------------// // populateFlag // //--------------// /** * Try to assign a flag to a relevant chord. * * @param glyph the underlying glyph of this flag * @param measure the containing measure */ public static void populateFlag (Glyph glyph, Measure measure) { logger.debug("Chord Populating flag {}", glyph); // Retrieve the related chord Glyph stem = null; for (HorizontalSide side : HorizontalSide.values()) { stem = glyph.getStem(side); if (stem != null) { List<Chord> sideChords = Chord.getStemChords(measure, stem); if (!sideChords.isEmpty()) { for (Chord chord : sideChords) { chord.flagsNumber += getFlagValue(glyph.getShape()); chord.addFlagGlyph(glyph); glyph.addTranslation(chord); } } else { measure.addError(stem, "No chord for stem " + stem.getId()); } return; } } measure.addError(glyph, "Flag glyph with no stem"); } //----------// // populate // //----------// /** * Create a new chord (or join an existing one) to host the note(s) * that correspond to the provided glyph. * * @param glyph the provided underlying glyph (notehead, note or rest) * @param measure the containing measure */ public static void populate (Glyph glyph, Measure measure) { if (glyph.isVip()) { logger.info("Chord. populate {}", glyph.idString()); } glyph.clearTranslations(); Set<Integer> assigned = new HashSet<>(); // Allocate 1 chord per stem, per rest, per whole note if (glyph.getStemNumber() > 0) { // Beware of noteheads with 2 stems, we need to duplicate them // in order to actually have two logical chords. // Sides are browsed LEFT, then RIGHT for (HorizontalSide side : HorizontalSide.values()) { boolean completing = side == HorizontalSide.RIGHT; Glyph stem = glyph.getStem(side); if (stem != null) { List<Chord> chords = Chord.getStemChords(measure, stem); if (!chords.isEmpty()) { // At this point, we can have at most one chord per stem // Join the chord(s) for (Chord chord : chords) { Note.createPack(chord, glyph, assigned, completing); } } else { // Create a new stem-based chord Chord chord = new Chord(measure, null); chord.setStem(stem); stem.setTranslation(chord); Note.createPack(chord, glyph, assigned, completing); } } } } else { // Create a brand-new stem-less chord (rest or whole note) Chord chord = new Chord(measure, null); Note.createPack(chord, glyph, assigned, true); // Keep track of measure rests (linked to no time slot) if (glyph.getShape().isMeasureRest()) { measure.addWholeChord(chord); } } } //--------// // accept // //--------// @Override public boolean accept (ScoreVisitor visitor) { return visitor.visit(this); } //---------------------// // getTranslationLinks // //---------------------// @Override public List<Line2D> getTranslationLinks (Glyph glyph) { Point from = glyph.getLocation(); Point to = stem != null ? stem.getAreaCenter() : getReferencePoint(); Line2D line = new Line2D.Double(from, to); return Arrays.asList(line); } //---------// // addBeam // //---------// /** * Insert a beam as attached to this chord * * @param beam the attached beam */ public void addBeam (Beam beam) { if (!beams.contains(beam)) { beams.add(beam); // Keep the sequence sorted Collections.sort(beams, byLevel); } } //----------// // addChild // //----------// /** * Override normal behavior, so that adding a note resets chord * internal parameters * * @param node the child to insert in the chord */ @Override public void addChild (TreeNode node) { super.addChild(node); // Side effect for note, since the geometric parameters of the chord // are modified if (node instanceof Note) { reset(); } } //--------------// // addDirection // //--------------// /** * Add a direction element that should appear right before the chord * first note * * @param direction the direction element to add */ public void addDirection (Direction direction) { directions.add(direction); } //----------------// // setChordSymbol // //----------------// /** * Set the chord symbol for this chord * * @param chordSymbol the symbol to assign */ public void setChordSymbol (ChordSymbol chordSymbol) { this.chordSymbol = chordSymbol; } //--------------// // addFlagGlyph // //--------------// /** * Remember a flag glyph for this chord * * @param flag the glyph (which may represent several flags) */ public void addFlagGlyph (Glyph flag) { flagGlyphs.add(flag); } //---------// // addMark // //---------// /** * Add a UI mark to this chord * * @param mark the mark to add */ public void addMark (Mark mark) { marks.add(mark); } //-------------// // addNotation // //-------------// /** * Add a notation element related to this chord note(s) * * @param notation the notation element to add */ public void addNotation (Notation notation) { notations.add(notation); } //-----------// // checkTies // //-----------// /** * Check that all incoming ties come from the same chord, * and similarly that all outgoing ties go to the same chord. */ public void checkTies () { // Incoming ties SplitOrder order = checkTies( new TieRelation() { @Override public Note getDistantNote (Slur slur) { return slur.getLeftNote(); } @Override public Note getLocalNote (Slur slur) { return slur.getRightNote(); } }); if (order != null) { split(order); return; } // Outgoing ties order = checkTies( new TieRelation() { @Override public Note getDistantNote (Slur slur) { return slur.getRightNote(); } @Override public Note getLocalNote (Slur slur) { return slur.getLeftNote(); } }); if (order != null) { split(order); } } //-----------// // duplicate // //-----------// /** * Make a clone of a chord (except for its beams). * This duplication is needed in cases such as: a note head with stems on * both sides, or a chord shared by two BeamGroups. * * @return a clone of this chord (including notes, but beams are not copied) */ public Chord duplicate () { // Beams are not copied Chord clone = new Chord(getMeasure(), slot); clone.stem = stem; stem.addTranslation(clone); // Notes (we make a deep copy of each note) List<TreeNode> notesCopy = new ArrayList<>(); notesCopy.addAll(getNotes()); for (TreeNode node : notesCopy) { Note note = (Note) node; new Note(clone, note); } clone.tupletFactor = tupletFactor; clone.dotsNumber = dotsNumber; clone.flagsNumber = flagsNumber; // Not sure TODO // Insure correct ordering of chords within their container ///Collections.sort(getParent().getChildren(), chordComparator); return clone; } //--------------// // getBeamGroup // //--------------// /** * Report the group of beams this chord belongs to * * @return the related group of beams */ public BeamGroup getBeamGroup () { if (!getBeams().isEmpty()) { return getBeams().get(0).getGroup(); } else { return null; } } //----------// // getBeams // //----------// /** * Report the sequence of beams that are attached to this chord, * ordered from the tail of the chord. * * @return the attached beams */ public List<Beam> getBeams () { return beams; } //---------------// // getDirections // //---------------// /** * Report the direction entities loosely related to this chord * * @return the collection of (loosely) related directions, perhaps empty */ public Collection<? extends Direction> getDirections () { return directions; } //----------------// // getChordSymbol // //----------------// /** * Report the symbol (chord name) related to this chord * * @return the chord symbol, perhaps null */ public ChordSymbol getChordSymbol () { return chordSymbol; } //---------------// // getDotsNumber // //---------------// /** * Report the number of augmentation dots that impact this chord * * @return the number of dots (should be the same for all notes within this * chord) */ public int getDotsNumber () { return dotsNumber; } //-------------// // getDuration // //-------------// /** * Report the real duration computed for this chord, including the * tuplet impact if any, with null value for whole/multi rest. * * @return The real chord/note duration, or null for a whole rest chord * @see #getRawDuration */ public Rational getDuration () { if (this.isWholeDuration()) { return null; } else { Rational raw = getRawDuration(); if (tupletFactor == null) { return raw; } else { return raw.times(tupletFactor); } } } //------------// // getEndTime // //------------// /** * Report the time when this chord ends * * @return chord ending time, since beginning of the measure */ public Rational getEndTime () { if (isWholeDuration()) { return null; } Rational chordDur = getDuration(); if (chordDur == null) { return null; } else { return startTime.plus(chordDur); } } //---------------// // getFlagGlyphs // //---------------// /** * @return the flagGlyphs */ public Set<Glyph> getFlagGlyphs () { return flagGlyphs; } //----------------// // getFlagsNumber // //----------------// /** * Report the number of flags attached to the chord stem * * @return the number of flags */ public int getFlagsNumber () { return flagsNumber; } //------------------------// // getFollowingTiedChords // //------------------------// /** * Report the x-ordered collection of chords which are directly * tied to the right of this chord. * * @return the (perhaps empty) collection of tied chords */ public List<Chord> getFollowingTiedChords () { List<Chord> tied = new ArrayList<>(); for (TreeNode node : children) { Note note = (Note) node; for (Slur slur : note.getSlurs()) { if (slur.isTie() && (slur.getLeftNote() == note) && (slur.getRightNote() != null)) { tied.add(slur.getRightNote().getChord()); } } } Collections.sort(tied, Chord.byAbscissa); return tied; } //-----------------// // getHeadLocation // //-----------------// /** * Report the system-based location of the chord head (the head * which is farthest from the tail). * * @return the head location */ public Point getHeadLocation () { if (headLocation == null) { computeLocations(); } return headLocation; } //-------// // getId // //-------// /** * Report the id of the chord within the measure (meant for debug) * * @return the unique id within the measure */ public int getId () { return id; } //----------// // getNotes // //----------// /** * Report the collection of UI marks related to this chord * * @return the collection of marks, perhaps empty */ public List<Mark> getMarks () { return marks; } //--------------// // getNotations // //--------------// /** * Report the (perhaps empty) collection of related notations * * @return the collection of notations */ public Collection<? extends Notation> getNotations () { return notations; } //----------// // getNotes // //----------// /** * Report all the notes that compose this chord * * @return the chord notes */ public List<TreeNode> getNotes () { return children; } //-------------------------// // getPreviousChordInVoice // //-------------------------// /** * Report the chord that occurs right before this one, within the * same voice. * * @return the previous chord within the same voice */ public Chord getPreviousChordInVoice () { return voice.getPreviousChord(this); } //----------------// // getRawDuration // //---------------// /** * Report the intrinsic duration of this chord, taking flag/beams * and dots into account, but not the tuplet impact if any. * The duration is assumed to be the same for all notes of this chord, * otherwise the chord must be split. * This includes the local information (flags, dots) but not the tuplet * impact if any. * A specific value (WHOLE_DURATION) indicates the whole/multi rest chord. * * Nota: this value is not cached, but computed at every call * * @return the intrinsic chord duration * @see #getDuration */ public Rational getRawDuration () { Rational rawDuration = null; if (!getNotes().isEmpty()) { // All note heads are assumed to be the same within one chord Note note = (Note) getNotes().get(0); if (!note.getShape().isMeasureRest()) { // Duration (with flags/beams applied for non-rests) rawDuration = note.getNoteDuration(); // Apply augmentation (applies to rests as well) if (dotsNumber == 1) { rawDuration = rawDuration.times(new Rational(3, 2)); } else if (dotsNumber == 2) { rawDuration = rawDuration.times(new Rational(7, 4)); } } } return rawDuration; } //---------// // setSlot // //---------// public void setSlot (Slot slot) { this.slot = slot; } //---------// // getSlot // //---------// /** * Report the slot this chord belongs to * * @return the containing slot (or null if not found) */ public Slot getSlot () { return slot; } //----------// // getStaff // //----------// /** * Report the staff that contains this chord (we use the staff of the notes) * * @return the chord staff */ @Override public Staff getStaff () { if (super.getStaff() == null) { if (!getNotes().isEmpty()) { Note note = (Note) getNotes().get(0); setStaff(note.getStaff()); } } return super.getStaff(); } //--------------// // getStartTime // //--------------// /** * Report the starting time for this chord * * @return startTime chord starting time (counted within the measure) */ public Rational getStartTime () { return startTime; } //---------// // getStem // //---------// /** * Report the stem of this chord (or null in the case of stem-less * chord). * * @return the chord stem, or null */ public Glyph getStem () { return stem; } //------------// // getStemDir // //------------// /** * Report the stem direction of this chord * * @return -1 if stem is down, 0 if no stem, +1 if stem is up */ public int getStemDir () { if (stem == null) { return 0; } else { return Integer.signum(getHeadLocation().y - getTailLocation().y); } } //-----------------// // getTailLocation // //-----------------// /** * Report the system-based location of the tail of the chord * * @return the tail location */ public Point getTailLocation () { if (tailLocation == null) { computeLocations(); } return tailLocation; } //-----------------// // getTupletFactor // //-----------------// /** * Report the chord tuplet factor, if any * * @return the factor to apply, or nulkl */ public DurationFactor getTupletFactor () { return tupletFactor; } //----------// // getVoice // //----------// /** * Report the (single) voice used by the notes of this chord * * @return the chord voice */ public Voice getVoice () { return voice; } //------------// // isAllRests // //------------// /** * Checks whether this chord contains only rests (and no standard note) * * @return true is made of rests only */ public boolean isAllRests () { for (TreeNode node : getNotes()) { Note note = (Note) node; if (!note.isRest()) { return false; } } return true; } //--------------// // isEmbracedBy // //--------------// /** * Check whether the notes of this chord stand within the given * vertical range * * @param top top of vertical range * @param bottom bottom of vertical range * @return true if all notes are within the given range */ public boolean isEmbracedBy (Point top, Point bottom) { for (TreeNode node : getNotes()) { Note note = (Note) node; Point center = note.getCenter(); if ((center.y >= top.y) && (center.y <= bottom.y)) { return true; } } return false; } //-----------------// // isWholeDuration // //-----------------// /** * Check whether the chord/note is a whole rest * * @return true if whole */ public boolean isWholeDuration () { if (!getNotes().isEmpty()) { Note note = (Note) getNotes().get(0); return note.getShape().isMeasureRest(); } return false; } //---------------// // setDotsNumber // //---------------// /** * Define the number of augmentation dots that impact this chord * * @param dotsNumber the number of dots (should be the same for all notes * within this chord) */ public void setDotsNumber (int dotsNumber) { this.dotsNumber = dotsNumber; } //--------------// // setStartTime // //--------------// /** * Remember the starting time for this chord * * @param startTime chord starting time (counted within the measure) */ public void setStartTime (Rational startTime) { // Already done? if (this.startTime == null) { logger.debug("setStartTime {} for chord #{}", startTime, getId()); this.startTime = startTime; // // // Set the same info in containing slot if any // if (slot != null) { // slot.setStartTime(startTime); // } } else { if (!this.startTime.equals(startTime)) { addError( "Reassigning startTime from " + this.startTime + " to " + startTime + " in " + this); } } } //---------// // setStem // //---------// /** * Assign the proper stem to this chord * * @param stem the chord stem */ public void setStem (Glyph stem) { this.stem = stem; } //-----------------// // setTupletFactor // //-----------------// /** * Assign a tuplet factor to this chord * * @param tupletFactor the factor to apply */ public void setTupletFactor (DurationFactor tupletFactor) { this.tupletFactor = tupletFactor; } //----------// // setVoice // //----------// /** * Assign a voice to this chord * * @param voice the voice to assign */ public void setVoice (Voice voice) { // Already done? if (this.voice == null) { final String contextString = getContextString(); logger.debug("{} Ch#{} setVoice {}", contextString, id, voice.getId()); this.voice = voice; // Update the voice entity if (!isWholeDuration()) { if (slot != null) { voice.setSlotInfo( slot, new VoiceChord(this, Voice.Status.BEGIN)); } // Extend this info to other grouped chords if any BeamGroup group = getBeamGroup(); if (group != null) { logger.debug( "{} Ch#{} extending voice#{} to group#{}", contextString, id, voice.getId(), group.getId()); group.setVoice(voice); } // Extend to the following tied chords as well List<Chord> tied = getFollowingTiedChords(); for (Chord chord : tied) { logger.debug("{} tied to {}", this, chord); // Check the tied chords belong to the same measure if (this.getMeasure() == chord.getMeasure()) { logger.debug( "{} Ch#{} extending voice#{} to tied chord#{}", contextString, id, voice.getId(), chord.getId()); chord.setVoice(voice); } else { // Chords tied across measure boundary logger.debug("{} Cross tie {} -> {}", contextString, toShortString(), chord.toShortString()); } } } } else if (this.voice != voice) { addError( "Chord. Attempt to reassign voice from " + this.voice. getId() + " to " + voice.getId() + " in " + this); } else { if (!isWholeDuration()) { if (slot != null) { voice.setSlotInfo( slot, new VoiceChord(this, Voice.Status.BEGIN)); } } } } //--------------// // toLongString // //--------------// /** * Report a more detailed description than plain toStrïng * * @return a detailed description */ public String toLongString () { StringBuilder sb = new StringBuilder(); sb.append(this); sb.deleteCharAt(sb.length() - 1); // Remove trailing "}" try { if (headLocation != null) { sb.append(" head[x=").append(headLocation.x).append(",y="). append(headLocation.y).append("]"); } if (tailLocation != null) { sb.append(" tail[x=").append(tailLocation.x).append(",y="). append(tailLocation.y).append("]"); } if (!beams.isEmpty()) { try { sb.append(" beams G#"). append(beams.get(0).getGroup().getId()).append( "["); for (Beam beam : beams) { sb.append(beam).append(" "); } sb.append("]"); } catch (Exception ex) { logger.warn("Exception in chord toLongString()"); } } } catch (NullPointerException e) { sb.append(" INVALID"); } sb.append("}"); return sb.toString(); } //---------------// // toShortString // //---------------// /** * A description meant for constrained labels * * @return a short chord description */ public String toShortString () { StringBuilder sb = new StringBuilder(); sb.append("["); if (getVoice() != null) { sb.append("Voice#").append(getVoice().getId()); } sb.append(" Chord#").append(getId()); sb.append(" dur:"); if (isWholeDuration()) { sb.append("W"); } else { Rational chordDur = getDuration(); if (chordDur != null) { sb.append(chordDur); } else { sb.append("none"); } } sb.append("]"); return sb.toString(); } //----------// // toString // //----------// @Override public String toString () { StringBuilder sb = new StringBuilder(); sb.append("{Chord"); try { sb.append("#").append(id); if (voice != null) { sb.append(" voice#").append(voice.getId()); } // Staff ? if (!getNotes().isEmpty()) { Note note = (Note) getNotes().get(0); if (note != null) { sb.append(" staff#").append(note.getStaff().getId()); } } if (startTime != null) { sb.append(" start=").append(startTime); } sb.append(" dur="); if (isWholeDuration()) { sb.append("W"); } else { Rational chordDur = getDuration(); if (chordDur != null) { sb.append(chordDur); } else { sb.append("none"); } } if (isAllRests()) { sb.append(" rest"); } if (stem != null) { sb.append(" stem#").append(stem.getId()); } if (tupletFactor != null) { sb.append(" tupletFactor=").append(tupletFactor); } if (dotsNumber != 0) { sb.append(" dots=").append(dotsNumber); } if (flagsNumber != 0) { sb.append(" flags=").append(flagsNumber); } } catch (NullPointerException e) { sb.append(" INVALID"); } sb.append("}"); return sb.toString(); } //-------// // reset // //-------// /** * Reset all internal data that depends on the chord composition in * terms of notes. */ @Override protected final void reset () { super.reset(); headLocation = null; tailLocation = null; startTime = null; } //------------// // computeBox // //------------// /** * Compute the chord bounding box, including its stem (if any) as * well as all the notes of the chord. */ @Override protected void computeBox () { // Stem or similar info Rectangle newBox = new Rectangle(getTailLocation()); newBox.add(getHeadLocation()); // Each and every note for (TreeNode n : getNotes()) { Note note = (Note) n; newBox.add(note.getBox()); } setBox(newBox); } //-----------------------// // computeReferencePoint // //-----------------------// /** * Define the reference point as the head location. */ @Override protected void computeReferencePoint () { setReferencePoint(getHeadLocation()); } //-----------// // checkTies // //-----------// /** * For this chord, check either the incoming or the outgoing ties * according to the TieRelation information. * For true ties (slurs linking notes with same pitch) we make sure there * is no more than one distant chord. If not, we split the chord in two, * so that each (sub)chord has only consistent ties. * * @param tie info about the relation between the slur and this chord * @return how to split the chord, or null if no split is needed */ private SplitOrder checkTies (TieRelation tie) { List<Note> distantNotes = new ArrayList<>(); List<Chord> distantChords = new ArrayList<>(); for (TreeNode nn : getNotes()) { Note note = (Note) nn; // Use a COPY of slurs to allow concurrent deletion for (Slur slur : new ArrayList<>(note.getSlurs())) { if (tie.isRelevant(slur, note)) { Note distantNote = tie.getDistantNote(slur); if (distantNote != null) { // Safety check if (distantNote == note || distantNote.getChord() == this) { // This slur is a loop on the same note or chord! logger.info("Looping slur detected {}", slur); slur.destroy(); continue; } if (distantNote.getMeasure() == getMeasure()) { distantNotes.add(distantNote); distantChords.add(distantNote.getChord()); } } } } } if (distantChords.size() > 1) { logger.debug("{} Ch#{} with multiple tied chords: {}", getContextString(), getId(), distantChords); // Prepare the split of this chord, using the most distant note // from chord head SortedSet<Note> tiedNotes = new TreeSet<>(noteHeadComparator); for (Note distantNote : distantNotes) { for (Slur slur : distantNote.getSlurs()) { Note note = tie.getLocalNote(slur); if ((note != null) && (note.getChord() == this)) { tiedNotes.add(note); } } } logger.debug("Splitting from {}", tiedNotes.last()); return new SplitOrder(this, tiedNotes.last()); } else { return null; } } //------------------// // computeLocations // //------------------// /** * Compute the head and tail locations for this chord. */ private void computeLocations () { // Find the note farthest from stem middle point if (!getNotes().isEmpty()) { if (stem != null) { Point middle = stem.getLocation(); Note bestNote = null; int bestDy = Integer.MIN_VALUE; for (TreeNode node : getNotes()) { Note note = (Note) node; int noteY = note.getCenter().y; int dy = Math.abs(noteY - middle.y); if (dy > bestDy) { bestNote = note; bestDy = dy; } } Rectangle stemBox = stem.getBounds(); if (middle.y < bestNote.getCenter().y) { // Stem is up tailLocation = new Point( stemBox.x + (stemBox.width / 2), stemBox.y); } else { // Stem is down tailLocation = new Point( stemBox.x + (stemBox.width / 2), (stemBox.y + stemBox.height)); } headLocation = getHeadLocation(bestNote); } else { Note note = (Note) getNotes().get(0); headLocation = note.getCenter(); tailLocation = headLocation; } } else { addError("No notes in chord " + this); } } //--------------// // getFlagValue // //--------------// /** * Report the number of flags that corresponds to the flag glyph * * @param shape the given flag glyph shape * @return the number of flags */ private static int getFlagValue (Shape shape) { switch (shape) { case FLAG_1: case FLAG_1_UP: return 1; case FLAG_2: case FLAG_2_UP: return 2; case FLAG_3: case FLAG_3_UP: return 3; case FLAG_4: case FLAG_4_UP: return 4; case FLAG_5: case FLAG_5_UP: return 5; } logger.error("Illegal flag shape: {}", shape); return 0; } //-----------------// // getHeadLocation // //-----------------// /** * Compute the head location of a chord, given the chord head note. * (TODO: Perhaps we could adjust note ordinate, to align it to pitch while * using the real staff lines ordinates) * * @param note the head note * @return the head location */ private Point getHeadLocation (Note note) { return new Point(tailLocation.x, note.getReferencePoint().y); } //---------------// // retrieveFlags // //---------------// /** * Report the set of flags for this chord. * Nota: This is not an efficient implementation. A better one would require * to record a link from chord to flag glyphs. * * @return the set of flags, perhaps empty but not null */ private Set<Glyph> retrieveFlags () { Set<Glyph> foundFlags = new HashSet<>(); for (Glyph glyph : getSystem().getInfo().getGlyphs()) { Shape shape = glyph.getShape(); if (ShapeSet.Flags.contains(shape)) { if (glyph.getTranslations().contains(this)) { foundFlags.add(glyph); } } } return foundFlags; } //-------// // split // //-------// /** * Apply the split order on this chord, which is impacted by the split * * @param order the details of the split order * @return the created chord */ private Chord split (SplitOrder order) { logger.debug("{}", order); logger.debug("Initial notes={}", getNotes()); // Same measure & slot Chord alien = new Chord(getMeasure(), slot); // Same stem if (stem != null) { alien.stem = stem; stem.addTranslation(alien); } // Tuplet factor, if any, is copied alien.tupletFactor = tupletFactor; // Augmentation dots as well alien.dotsNumber = dotsNumber; // Beams are not copied, but flags are if (flagsNumber > 0) { alien.flagsNumber = flagsNumber; for (Glyph flag : retrieveFlags()) { flag.addTranslation(alien); } } // Notes, sorted from head Collections.sort(getNotes(), noteHeadComparator); boolean started = false; for (TreeNode tn : getChildrenCopy()) { Note note = (Note) tn; if (note == order.alienNote) { started = true; } if (started) { note.moveTo(alien); } } // Locations of the old and the new chord alien.tailLocation = this.tailLocation; alien.headLocation = getHeadLocation(order.alienNote); // Should we split the stem as well? // Use a test on length of resulting stem fragment int stemFragmentLength = Math.abs( alien.headLocation.y - this.headLocation.y); if (stemFragmentLength >= getScale().toPixels( constants.minStemFragmentLength)) { this.tailLocation = alien.headLocation; } if (logger.isDebugEnabled()) { logger.debug("Remaining notes={}", getNotes()); logger.debug("Remaining {}", this.toLongString()); logger.debug("Alien notes={}", alien.getNotes()); logger.debug("Alien {}", alien.toLongString()); } return alien; } //~ Inner Classes ---------------------------------------------------------- //------------// // SplitOrder // //------------// /** * Class {@code SplitOrder} records a chord split order. * Splitting must be separate from browsing to avoid concurrent modification * of collections */ public static class SplitOrder { //~ Instance fields ---------------------------------------------------- /** The chord to be split */ final Chord chord; /** The first note of this chord to feed an alien chord */ final Note alienNote; //~ Constructors ------------------------------------------------------- /** * Create a chord SplitOrder * * @param chord the chord to be split * @param alienNote the first chord note to feed an alien chord */ public SplitOrder (Chord chord, Note alienNote) { this.chord = chord; this.alienNote = alienNote; } //~ Methods ------------------------------------------------------------ @Override public String toString () { StringBuilder sb = new StringBuilder(); sb.append("{Split"); sb.append(" chord=").append(chord); sb.append(" alienNote=").append(alienNote); sb.append("}"); return sb.toString(); } } //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Scale.Fraction minStemFragmentLength = new Scale.Fraction( 2d, "Minimum length for stem fragment in split"); } //-------------// // TieRelation // //-------------// private abstract static class TieRelation { //~ Methods ------------------------------------------------------------ public abstract Note getDistantNote (Slur slur); public abstract Note getLocalNote (Slur slur); public boolean isRelevant (Slur slur, Note note) { return slur.isTie() && (getLocalNote(slur) == note); } } }