//----------------------------------------------------------------------------// // // // S c o r e C h e c k e r // // // //----------------------------------------------------------------------------// // <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; import omr.constant.Constant; import omr.constant.ConstantSet; import omr.glyph.Evaluation; import omr.glyph.GlyphNetwork; import omr.glyph.Grades; import omr.glyph.Shape; import omr.glyph.ShapeEvaluator; import omr.glyph.ShapeSet; import omr.glyph.facets.Glyph; import omr.math.GeoUtil; import omr.score.entity.Beam; import omr.score.entity.BeamGroup; import omr.score.entity.Chord; import omr.score.entity.Dynamics; import omr.score.entity.Measure; import omr.score.entity.Note; import omr.score.entity.ScoreSystem; import omr.score.entity.Staff; import omr.score.entity.SystemPart; import omr.score.entity.TimeSignature; import omr.score.entity.TimeSignature.InvalidTimeSignature; import omr.score.visitor.AbstractScoreVisitor; import omr.sheet.Scale; import omr.sheet.SystemInfo; import omr.util.Predicate; import omr.util.TreeNode; import omr.util.WrappedBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Point; import java.awt.Rectangle; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Class {@code ScoreChecker} can visit the score hierarchy and perform * global checking on score nodes. * <p>We use it for: <ul> * <li>Improving the recognition of beam hooks</li> * <li>Fixing false beam hooks</li> * <li>Forcing consistency among time signatures</li> * <li>Making sure all dynamics can be assigned a shape</li> * <li>Merge note heads with pitches too close to each other</li> * <li>Enforce consistency of note heads within the same chord</li> * </ul> * * TODO: Split this class into smaller modular classes, one per feature * since the browsing of the score by itself is very cheap (.15 ms for a page) * * @author Hervé Bitteur */ public class ScoreChecker extends AbstractScoreVisitor { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(ScoreChecker.class); /** Specific predicate for beam hooks */ private static final Predicate<Shape> hookPredicate = new Predicate<Shape>() { @Override public boolean check (Shape shape) { return ShapeSet.Beams.contains(shape); } }; //~ Instance fields -------------------------------------------------------- /** Glyph evaluator */ private final ShapeEvaluator evaluator = GlyphNetwork.getInstance(); /** Output of the checks */ private final WrappedBoolean modified; //~ Constructors ----------------------------------------------------------- //--------------// // ScoreChecker // //--------------// /** * Creates a new ScoreChecker object. * * @param modified This is actually an out parameter, to tell if one or * several entities have been modified by the score visit */ public ScoreChecker (WrappedBoolean modified) { this.modified = modified; } //~ Methods ---------------------------------------------------------------- //------------// // visit Beam // //------------// /** * Check that all beam hooks are legal * * @param beam the beam to check * @return true */ @Override public boolean visit (Beam beam) { try { Glyph glyph = beam.getFirstItem().getGlyph(); if (!beam.isHook() || glyph.isManualShape()) { return true; } List<Chord> chords = beam.getChords(); if (chords.size() > 1) { beam.addError(glyph, "Beam hook connected to several chords"); return true; } if (chords.isEmpty()) { beam.addError(glyph, "Beam hook connected to no chords"); return true; } // Check that there is at least one full beam on the same chord // And vertically closer than the chord head Chord chord = chords.get(0); int stemX = chord.getStem().getLocation().x; double hookY = glyph.getCentroid().y; int headY = chord.getHeadLocation().y; double toHead = Math.abs(headY - hookY); for (Beam b : chord.getBeams()) { if (!b.isHook()) { // Check hook is closer to beam than to head double beamY = b.getLine().yAtX(stemX); double toBeam = Math.abs(beamY - hookY); if (toBeam <= toHead) { return true; } } } // No real beam found on the same chord, so let's discard the hook if (glyph.isVip() || logger.isDebugEnabled()) { logger.info("{} Removing false beam hook {}", beam.getMeasure().getContextString(), glyph.idString()); } glyph.setShape(null); modified.set(true); return true; } catch (Exception ex) { logger.warn( getClass().getSimpleName() + " Error visiting " + beam, ex); } return true; } //-------------// // visit Chord // //-------------// @Override public boolean visit (Chord chord) { try { // Check note heads pitches checkNotePitches(chord); // Check note heads consistency checkNoteConsistency(chord); // Check void note heads WRT flags or beams checkVoidHeads(chord); // Check note heads do not appear on both stem head and tail checkHeadLocations(chord); } catch (Exception ex) { logger.warn( getClass().getSimpleName() + " Error visiting " + chord, ex); } return true; } //----------------// // visit Dynamics // //----------------// /** * Check that each dynamics shape can be computed * * @param dynamics the dynamics item * @return true */ @Override public boolean visit (Dynamics dynamics) { try { dynamics.getShape(); } catch (Exception ex) { logger.warn( getClass().getSimpleName() + " Error visiting " + dynamics, ex); } return true; } //---------------// // visit Measure // //---------------// /** * This method is used to detect & fix unrecognized beam hooks * * @param measure the measure to browse * @return true. The real output is stored in the modified global which is * set to true if at least a beam_hook has been fixed. */ @Override public boolean visit (Measure measure) { try { final Scale scale = measure.getScale(); final ScoreSystem system = measure.getSystem(); final SystemInfo systemInfo = system.getInfo(); // Check the beam groups for non-recognized hooks for (BeamGroup group : measure.getBeamGroups()) { for (Chord chord : group.getChords()) { Glyph stem = chord.getStem(); // We could have rests (w/o stem!) if (stem != null) { searchHooks( chord, systemInfo.lookupIntersectedGlyphs( systemInfo.stemBoxOf(stem), stem)); } } } } catch (Exception ex) { logger.warn( getClass().getSimpleName() + " Error visiting " + measure, ex); } return true; } //-------------// // visit Score // //-------------// /** * Not used * * @param score * @return true */ @Override public boolean visit (Score score) { try { logger.debug("Checking score ..."); score.acceptChildren(this); } catch (Exception ex) { logger.warn( getClass().getSimpleName() + " Error visiting " + score, ex); } return false; } //------------------// // visit SystemPart // //------------------// /** * Check that all slurs have embraced notes on each end, except * perhaps on left and right sides of the part * * @param systemPart the part to process * @return true, since measures below must be visited too */ @Override public boolean visit (SystemPart systemPart) { systemPart.checkSlurConnections(); return true; } //---------------------// // visit TimeSignature // //---------------------// /** * Method used to check and correct the consistency between all time * signatures that occur in parallel measures. * * @param timeSignature the score entity that triggers the check * @return true */ @Override public boolean visit (TimeSignature timeSignature) { try { logger.debug("{} Checking {}", timeSignature.getContextString(), timeSignature); // Trigger computation of Num & Den if not already done Shape shape = timeSignature.getShape(); if (shape == null) { // This is assumed to be a complex time sig // (with no equivalent predefined shape) // Just check we are able to get num and den if ((timeSignature.getNumerator() == null) || (timeSignature.getDenominator() == null)) { timeSignature.addError( "Time signature with no rational value"); } else { logger.debug("Complex {}", timeSignature); // Normal complex shape if (!timeSignature.isDummy()) { checkTimeSig(timeSignature); } } } else if (shape == Shape.NO_LEGAL_TIME) { timeSignature.addError("Illegal " + timeSignature); } else if (ShapeSet.PartialTimes.contains(shape)) { // This time sig has the shape of a single digit // So some other part is still missing timeSignature.addError( "Orphan time signature shape : " + shape); } else { // Normal predefined shape if (!timeSignature.isDummy()) { checkTimeSig(timeSignature); } } } catch (Exception ex) { logger.warn( getClass().getSimpleName() + " Error visiting " + timeSignature, ex); } return true; } //- Utilities -------------------------------------------------------------- //------------------// // arePitchDeltasOk // //------------------// private boolean arePitchDeltasOk (List<Note> list) { double minDeltaPitch = constants.minDeltaNotePitch.getValue(); Note lastNote = null; for (Note note : list) { if (lastNote != null) { double deltaPitch = note.getPitchPosition() - lastNote.getPitchPosition(); if (Math.abs(deltaPitch) < minDeltaPitch) { logger.debug("Too small delta pitch between {} & {}", note, lastNote); mergeNotes(lastNote, note); return false; } } lastNote = note; } return true; } //----------------------// // checkNoteConsistency // //----------------------// /** * Check that all note heads of a chord are of the same shape * (either all black or all void). * * @param chord */ private void checkNoteConsistency (Chord chord) { EnumMap<Shape, List<Note>> shapes = new EnumMap<>(Shape.class); for (TreeNode node : chord.getNotes()) { Note note = (Note) node; if (!note.isRest()) { Shape shape = note.getShape(); List<Note> notes = shapes.get(shape); if (notes == null) { notes = new ArrayList<>(); shapes.put(shape, notes); } notes.add(note); } } if (shapes.keySet().size() > 1) { chord.addError(chord.getStem(), "Note inconsistency in " + chord + shapes); // Check evaluations double bestEval = Double.MIN_VALUE; Shape bestShape = null; for (Shape shape : shapes.keySet()) { List<Note> notes = shapes.get(shape); for (Note note : notes) { for (Glyph glyph : note.getGlyphs()) { if (glyph.getGrade() > bestEval) { bestEval = glyph.getGrade(); bestShape = shape; } } } } logger.debug("{} aligned on shape {}", chord, bestShape); final Shape baseShape = bestShape; // Must be final Predicate<Shape> predicate = new Predicate<Shape>() { final Collection<Shape> desiredShapes = Arrays.asList( Note.getActualShape(baseShape, 1), Note.getActualShape(baseShape, 2), Note.getActualShape(baseShape, 3)); @Override public boolean check (Shape shape) { return desiredShapes.contains(shape); } }; ScoreSystem system = chord.getSystem(); for (Shape shape : shapes.keySet()) { if (shape == bestShape) { continue; } List<Note> notes = shapes.get(shape); for (Note note : notes) { for (Glyph glyph : note.getGlyphs()) { Evaluation vote = evaluator.vote( glyph, system.getInfo(), Grades.consistentNoteMinGrade, predicate); if (vote != null) { glyph.setEvaluation(vote); } } } } } } //------------------// // checkNotePitches // //------------------// /** * Check that on each side of the chord stem, the notes pitches are * not too close to each other. * * @param chord the chord at hand */ private void checkNotePitches (Chord chord) { Glyph stem = chord.getStem(); if (stem == null) { return; } Point pixPoint = stem.getAreaCenter(); Point stemCenter = pixPoint; // Look on left and right sides List<TreeNode> allNotes = new ArrayList<>(chord.getNotes()); Collections.sort(allNotes, Chord.noteHeadComparator); List<Note> lefts = new ArrayList<>(); List<Note> rights = new ArrayList<>(); for (TreeNode nNode : allNotes) { Note note = (Note) nNode; Point center = note.getCenter(); if (center.x < stemCenter.x) { lefts.add(note); } else { rights.add(note); } } // Check on left & right if (!arePitchDeltasOk(lefts)) { modified.set(true); } if (!arePitchDeltasOk(rights)) { modified.set(true); } } //--------------// // checkTimeSig // //--------------// /** * Here we check time signature across all staves of the system * * @param timeSignature the sig to check */ private void checkTimeSig (TimeSignature timeSignature) { // Check others, similar abscissa, in all other staves of the system // Use score hierarchy, same system, all parts, same measure id // If there is no time sig, create a dummy one // If there is one, make sure the sig is identical // Priority to manually assigned shapes of course TimeSignature bestSig = findBestTimeSig(timeSignature.getMeasure()); if (bestSig != null) { for (Staff.SystemIterator sit = new Staff.SystemIterator( timeSignature.getMeasure()); sit.hasNext();) { Staff staff = sit.next(); Measure measure = sit.getMeasure(); TimeSignature sig = measure.getTimeSignature(staff); if (sig == null) { sig = new TimeSignature(measure, staff, bestSig); try { logger.debug("{} Created time sig {}/{}", sig.getContextString(), sig.getNumerator(), sig.getDenominator()); } catch (InvalidTimeSignature ignored) { logger.warn("InvalidTimeSignature", ignored); } } else { logger.debug("{} Existing sig {}", sig.getContextString(), sig); } } } } //----------------// // checkVoidHeads // //----------------// /** * Check that void note heads do not coexist with flags or beams. * * @param chord */ private void checkVoidHeads (Chord chord) { // Void heads? double noteGrade = Double.MIN_VALUE; Set<Glyph> voidGlyphs = new HashSet<>(); for (TreeNode node : chord.getNotes()) { Note note = (Note) node; Shape noteShape = note.getShape(); if (ShapeSet.VoidNoteHeads.contains(noteShape)) { for (Glyph glyph : note.getGlyphs()) { noteGrade = Math.max(noteGrade, glyph.getGrade()); voidGlyphs.add(glyph); } } } if (voidGlyphs.isEmpty()) { return; } Predicate<Shape> blackHeadPredicate = new Predicate<Shape>() { @Override public boolean check (Shape shape) { return ShapeSet.BlackNoteHeads.contains(shape); } }; // Flags or beams boolean fix = false; if (!chord.getBeams().isEmpty()) { // We trust beams logger.debug("{} Head/beam conflict in {}", chord.getContextString(), chord); fix = true; } else if (chord.getFlagsNumber() > 0) { // Check grade of flag(s) double flagGrade = Double.MIN_VALUE; for (Glyph flag : chord.getFlagGlyphs()) { flagGrade = Math.max(flagGrade, flag.getGrade()); } logger.debug("{} Head/flag conflict in {}", chord.getContextString(), chord); if (noteGrade <= flagGrade) { fix = true; } } if (fix) { // Change note shape (void -> black) for (Glyph glyph : voidGlyphs) { Evaluation vote = evaluator.rawVote( glyph, Grades.consistentNoteMinGrade, blackHeadPredicate); if (vote != null) { glyph.setEvaluation(vote); } } } } //--------------------// // checkHeadLocations // //--------------------// /** * Check that note heads do not appear on both stem head and tail. * On tail we can have nothing or beams or flags, but no heads * * @param chord the chord to check */ private void checkHeadLocations (Chord chord) { // This test applies only to chords with stem Glyph stem = chord.getStem(); if (stem == null) { return; } Rectangle tailBox = new Rectangle(chord.getTailLocation()); int halfTailBoxSide = chord.getScale().toPixels(constants.halfTailBoxSide); tailBox.grow(halfTailBoxSide, halfTailBoxSide); for (TreeNode node : chord.getNotes()) { Note note = (Note) node; // If note is close to tail, it can't be a note if (note.getBox().intersects(tailBox)) { for (Glyph glyph : note.getGlyphs()) { if (logger.isDebugEnabled() || glyph.isVip()) { logger.info("Note {} too close to tail of stem {}", note, stem); } glyph.setShape(null); } modified.set(true); } } } //-----------------// // findBestTimeSig // //-----------------// /** * Report the best time signature for all parallel measures * (among all the parallel candidate time signatures) * * @param measure the reference measure * @return the best signature, or null if no suitable signature found */ private TimeSignature findBestTimeSig (Measure measure) { TimeSignature manualSig; try { manualSig = findManualTimeSig(measure); // Perhaps null } catch (Exception ex) { measure.addError("Inconsistent Measures or TimeSignatures"); return null; } TimeSignature bestSig = manualSig; for (Staff.SystemIterator sit = new Staff.SystemIterator(measure); sit.hasNext();) { Staff staff = sit.next(); measure = sit.getMeasure(); TimeSignature sig = measure.getTimeSignature(staff); if ((sig == null) || sig.isDummy()) { continue; } try { // Make sure the signature is valid int num = sig.getNumerator(); int den = sig.getDenominator(); // First instance? if (bestSig == null) { bestSig = sig; continue; } // Still consistent? if ((num == bestSig.getNumerator()) && (den == bestSig.getDenominator()) && sig.getShape() == bestSig.getShape()) { continue; } // Inconsistency detected if (manualSig != null) { // Assign this manual sig to this (different) sig sig.copy(manualSig); } else { // Inconsistent sigs logger.debug("Inconsistency between time sigs"); sig.addError("Inconsistent time signature "); bestSig.addError("Inconsistent time signature"); return null; } } catch (Exception ex) { // Skip invalid signatures } } return bestSig; } //-------------------// // findManualTimeSig // //-------------------// /** * Report a suitable manually assigned time signature, if any. * For this, we need to find a manual time sig, after having checked that * all manual time sigs in the measure are consistent. * * @param measure the reference measure * @return the suitable manual sig, if any * @throws InconsistentTimeSignatures if at least two manual sigs differ */ private TimeSignature findManualTimeSig (Measure measure) throws InconsistentTimeSignatures { TimeSignature manualSig = null; for (Staff.SystemIterator sit = new Staff.SystemIterator(measure); sit.hasNext();) { Staff staff = sit.next(); TimeSignature sig = sit.getMeasure().getTimeSignature(staff); if ((sig != null) && !sig.isDummy() && sig.isManual()) { try { // Make sure the signature is valid int num = sig.getNumerator(); int den = sig.getDenominator(); // First instance? if (manualSig == null) { manualSig = sig; continue; } // Still consistent? if ((num != manualSig.getNumerator()) || (den != manualSig.getDenominator())) { sig.addError("Inconsistent time signature"); manualSig.addError("Inconsistent time signature"); throw new InconsistentTimeSignatures(); } } catch (InvalidTimeSignature | InconsistentTimeSignatures ex) { // Unusable signature, forget about this one } } } return manualSig; } //------------// // mergeNotes // //------------// private void mergeNotes (Note first, Note second) { if ((first.getShape() == Shape.NOTEHEAD_VOID) && (second.getShape() == Shape.NOTEHEAD_VOID)) { List<Glyph> glyphs = new ArrayList<>(); glyphs.addAll(first.getGlyphs()); glyphs.addAll(second.getGlyphs()); SystemInfo system = first.getSystem().getInfo(); Glyph compound = system.buildTransientCompound(glyphs); Evaluation vote = GlyphNetwork.getInstance().vote( compound, first.getSystem().getInfo(), Grades.mergedNoteMinGrade); if (vote != null) { compound = system.addGlyph(compound); compound.setEvaluation(vote); logger.debug("{} merged two note heads", compound.idString()); } } } //-------------// // searchHooks // //-------------// /** * Search unrecognized beam hooks among the glyphs around the * provided chord. * * @param chord the provided chord * @param glyphs the surrounding glyphs */ private void searchHooks (Chord chord, Collection<Glyph> glyphs) { // Up(+1) or down(-1) stem? final int stemDir = chord.getStemDir(); final Point chordCenter = chord.getCenter(); final ScoreSystem system = chord.getSystem(); final GlyphNetwork network = GlyphNetwork.getInstance(); for (Glyph glyph : glyphs) { if (glyph.getShape() != null) { continue; } logger.debug("Spurious {}", glyph.idString()); // Check we are on the tail (beam) end of the stem // Beware, stemDir is >0 upwards, while y is >0 downwards Point glyphCenter = glyph.getAreaCenter(); if ((GeoUtil.vectorOf(chordCenter, glyphCenter).y * stemDir) > 0) { logger.debug("{} not on beam side", glyph.idString()); continue; } // Check if a beam appears in the top evaluations Evaluation vote = network.vote( glyph, system.getInfo(), Grades.hookMinGrade, hookPredicate); if (vote != null) { glyph.setShape(vote.shape, Evaluation.ALGORITHM); logger.debug("{} recognized as {}", glyph.idString(), vote.shape); modified.set(true); } } } //~ Inner Classes ---------------------------------------------------------- //----------------------------// // InconsistentTimeSignatures // //----------------------------// /** * Used to signal that parallel time signatures are not consistent */ public static class InconsistentTimeSignatures extends Exception { //~ Constructors ------------------------------------------------------- public InconsistentTimeSignatures () { super("Time signatures are inconsistent"); } } //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Constant.Double minDeltaNotePitch = new Constant.Double( "PitchPosition", 1.5, "Minimum pitch difference between note heads on same stem side"); Scale.Fraction halfTailBoxSide = new Scale.Fraction( 1, "Half side of box on stem tail to exclude notes"); } }