diff --git a/.gitignore b/.gitignore index 5572e2b4e..3ce52d245 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ nbproject *.mf *.orig build/* +app/bin/* dist/* config/run.properties data/args diff --git a/app/build.gradle b/app/build.gradle index 8807f0b6f..f937c8daa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -173,7 +173,7 @@ dependencies { [group: 'javax.media', name: 'jai-core', version: '1.1.3'], [group: 'net.imagej', name: 'ij', version: '1.53j'], [group: 'org.apache.pdfbox', name: 'pdfbox', version: '2.0.27'], - [group: 'org.audiveris', name: 'proxymusic', version: '4.0.2'], + [group: 'org.audiveris', name: 'proxymusic', version: '4.0.3'], [group: 'org.jgrapht', name: 'jgrapht-core', version: '1.5.1'], [group: 'org.jfree', name: 'jfreechart', version: '1.5.3'], [group: 'com.itextpdf', name: 'itextpdf', version: '5.5.13.2'], diff --git a/app/config/user-actions.xml b/app/config/user-actions.xml index aeeb14103..4b28a3265 100644 --- a/app/config/user-actions.xml +++ b/app/config/user-actions.xml @@ -1,5 +1,6 @@ + diff --git a/nb-configuration.xml b/app/nb-configuration.xml similarity index 95% rename from nb-configuration.xml rename to app/nb-configuration.xml index 438df1430..031ad74f0 100644 --- a/nb-configuration.xml +++ b/app/nb-configuration.xml @@ -1,13 +1,12 @@ - - arg arpeggiato Audiveris barline diff --git a/app/res/Leland.otf b/app/res/Leland.otf new file mode 100644 index 000000000..2ea8b433c Binary files /dev/null and b/app/res/Leland.otf differ diff --git a/app/res/basic-classifier.zip b/app/res/basic-classifier.zip index 521b6bc7b..a6f35780e 100644 Binary files a/app/res/basic-classifier.zip and b/app/res/basic-classifier.zip differ diff --git a/app/src/main/java/org/audiveris/omr/classifier/Sample.java b/app/src/main/java/org/audiveris/omr/classifier/Sample.java index 5c1ef4ae3..c320d5e16 100644 --- a/app/src/main/java/org/audiveris/omr/classifier/Sample.java +++ b/app/src/main/java/org/audiveris/omr/classifier/Sample.java @@ -50,29 +50,32 @@ public class Sample private static final Logger logger = LoggerFactory.getLogger(Sample.class); - /** For comparing Sample instances by shape. */ - public static final Comparator byShape = (Sample s1, - Sample s2) -> Integer.compare( - s1.getShape().ordinal(), - s2.getShape().ordinal()); + /** For comparing Sample instances by (logical) shape. */ + public static final Comparator byShape = (s1, + s2) -> // + Integer.compare(s1.getShape().ordinal(), s2.getShape().ordinal()); + + /** For comparing Sample instances by physical shape. */ + public static final Comparator byPhysicalShape = (s1, + s2) -> // + Integer.compare( + s1.getShape().getPhysicalShape().ordinal(), + s2.getShape().getPhysicalShape().ordinal()); /** For comparing Sample instances by normalized width. */ - public static final Comparator byNormalizedWidth = (Sample s1, - Sample s2) -> Double.compare( - s1.getNormalizedWidth(), - s2.getNormalizedWidth()); + public static final Comparator byNormalizedWidth = (s1, + s2) -> // + Double.compare(s1.getNormalizedWidth(), s2.getNormalizedWidth()); /** For comparing Sample instances by normalized height. */ - public static final Comparator byNormalizedHeight = (Sample s1, - Sample s2) -> Double.compare( - s1.getNormalizedHeight(), - s2.getNormalizedHeight()); + public static final Comparator byNormalizedHeight = (s1, + s2) -> // + Double.compare(s1.getNormalizedHeight(), s2.getNormalizedHeight()); /** For comparing Sample instances by normalized weight. */ - public static final Comparator byNormalizedWeight = (Sample s1, - Sample s2) -> Double.compare( - s1.getNormalizedWeight(), - s2.getNormalizedWeight()); + public static final Comparator byNormalizedWeight = (s1, + s2) -> // + Double.compare(s1.getNormalizedWeight(), s2.getNormalizedWeight()); //~ Instance fields ---------------------------------------------------------------------------- @@ -340,7 +343,7 @@ public static Shape getRecordableShape (Shape shape) return null; } - Shape physicalShape = shape.getPhysicalShape(); + final Shape physicalShape = shape.getPhysicalShape(); if (physicalShape.isTrainable() && (physicalShape != Shape.NOISE)) { return physicalShape; diff --git a/app/src/main/java/org/audiveris/omr/classifier/SampleRepository.java b/app/src/main/java/org/audiveris/omr/classifier/SampleRepository.java index 36359a504..e1e5289f4 100644 --- a/app/src/main/java/org/audiveris/omr/classifier/SampleRepository.java +++ b/app/src/main/java/org/audiveris/omr/classifier/SampleRepository.java @@ -265,9 +265,9 @@ public void addSample (Shape shape, SampleSheet sampleSheet, Double pitch) { - shape = Sample.getRecordableShape(shape); + final Shape physicalShape = Sample.getRecordableShape(shape); - if (shape != null) { + if (physicalShape != null) { final Sample sample = new Sample(glyph, interline, shape, pitch); addSample(sample, sampleSheet); } @@ -480,8 +480,7 @@ public void checkAllSamples (Collection conflictings, Collections.sort( allSamples, (Sample s1, - Sample s2) -> - { + Sample s2) -> { int comp = Integer.compare(s1.getWeight(), s2.getWeight()); if (comp != 0) { @@ -518,7 +517,7 @@ public void checkAllSamples (Collection conflictings, } if ((s.getInterline() == interline) && s.getRunTable().equals(runTable)) { - if (s.getShape() != sample.getShape()) { + if (s.getShape().getPhysicalShape() != sample.getShape().getPhysicalShape()) { logger.warn( "Conflicting shapes between {}/{} and {}/{}", getSheetName(sample), @@ -566,8 +565,7 @@ public void checkFontSamples () Collections.sort( fontSamples, (Sample s1, - Sample s2) -> - { + Sample s2) -> { int comp = Integer.compare(s1.getWeight(), s2.getWeight()); if (comp != 0) { @@ -605,7 +603,7 @@ public void checkFontSamples () } if ((s.getInterline() == interline) && s.getRunTable().equals(runTable)) { - if (s.getShape() != sample.getShape()) { + if (s.getShape().getPhysicalShape() != sample.getShape().getPhysicalShape()) { logger.warn( "Conflicting shapes between {}/{} and {}/{}", getSheetName(sample), @@ -786,8 +784,7 @@ public SampleSheet findSampleSheet (String name, } root.getFileSystem().close(); - } catch (IOException ignored) { - } + } catch (IOException ignored) {} } if (sampleSheet == null) { @@ -1644,7 +1641,7 @@ public boolean removeListener (ChangeListener listener) */ public void removeSample (Sample sample) { - SampleSheet sampleSheet = getSampleSheet(sample); + final SampleSheet sampleSheet = getSampleSheet(sample); if (isSymbols(sampleSheet.getDescriptor().getName())) { logger.info("A font-based symbol cannot be removed"); @@ -1753,7 +1750,7 @@ public void splitTrainAndTest (List train, // Flag redundant font-based samples as such checkFontSamples(); - // Gather samples by shape + // Gather samples by physical shape EnumMap> shapeSamples = new EnumMap<>(Shape.class); for (Sample sample : getAllSamples()) { @@ -1762,11 +1759,11 @@ public void splitTrainAndTest (List train, continue; } - Shape shape = sample.getShape(); - List list = shapeSamples.get(shape); + Shape physicalShape = sample.getShape().getPhysicalShape(); + List list = shapeSamples.get(physicalShape); if (list == null) { - shapeSamples.put(shape, list = new ArrayList<>()); + shapeSamples.put(physicalShape, list = new ArrayList<>()); } list.add(sample); diff --git a/app/src/main/java/org/audiveris/omr/classifier/SampleSheet.java b/app/src/main/java/org/audiveris/omr/classifier/SampleSheet.java index d31fb3068..0d92f4b96 100644 --- a/app/src/main/java/org/audiveris/omr/classifier/SampleSheet.java +++ b/app/src/main/java/org/audiveris/omr/classifier/SampleSheet.java @@ -101,7 +101,7 @@ public class SampleSheet /** True if image is already on disk. */ private boolean imageSaved = true; - /** Samples gathered by shape. */ + /** Samples gathered by logical shape. */ private final EnumMap> shapeMap = new EnumMap<>(Shape.class); /** Has this sheet been modified?. */ @@ -137,7 +137,8 @@ private SampleSheet (SampleList value, this.descriptor = descriptor; for (Sample sample : value.samples) { - Shape shape = sample.getShape(); + final Shape shape = sample.getShape(); + if (shape == null) { logger.warn("Null shape sample:{} in sheet:{}", sample, descriptor.getName()); } else { @@ -423,8 +424,8 @@ void privateAddSample (Sample sample) */ void privateRemoveSample (Sample sample) { - Shape shape = sample.getShape(); - ArrayList list = shapeMap.get(shape); + final Shape shape = sample.getShape(); + final ArrayList list = shapeMap.get(shape); if ((list == null) || !list.contains(sample)) { logger.warn("{} not found in {}", sample, this); @@ -650,13 +651,17 @@ private void afterUnmarshal (Unmarshaller um, for (Sample sample : samples) { final Shape shape = sample.getShape(); - switch (shape) { - case FLAG_1_UP -> modified |= sample.renameShapeAs(Shape.FLAG_1_DOWN); - case FLAG_2_UP -> modified |= sample.renameShapeAs(Shape.FLAG_2_DOWN); - case FLAG_3_UP -> modified |= sample.renameShapeAs(Shape.FLAG_3_DOWN); - case FLAG_4_UP -> modified |= sample.renameShapeAs(Shape.FLAG_4_DOWN); - case FLAG_5_UP -> modified |= sample.renameShapeAs(Shape.FLAG_5_DOWN); - default -> {} + if (shape == null) { + logger.warn("Null shape for sample: {}", sample); + } else { + switch (shape) { + case FLAG_1_UP -> modified |= sample.renameShapeAs(Shape.FLAG_1_DOWN); + case FLAG_2_UP -> modified |= sample.renameShapeAs(Shape.FLAG_2_DOWN); + case FLAG_3_UP -> modified |= sample.renameShapeAs(Shape.FLAG_3_DOWN); + case FLAG_4_UP -> modified |= sample.renameShapeAs(Shape.FLAG_4_DOWN); + case FLAG_5_UP -> modified |= sample.renameShapeAs(Shape.FLAG_5_DOWN); + default -> {} + } } } } diff --git a/app/src/main/java/org/audiveris/omr/classifier/ui/SampleBrowser.java b/app/src/main/java/org/audiveris/omr/classifier/ui/SampleBrowser.java index 504248155..3148013cc 100644 --- a/app/src/main/java/org/audiveris/omr/classifier/ui/SampleBrowser.java +++ b/app/src/main/java/org/audiveris/omr/classifier/ui/SampleBrowser.java @@ -166,7 +166,7 @@ public class SampleBrowser /** Panel for sheets selection. */ private SheetSelector sheetSelector; - /** Panel for shapes selection. */ + /** Panel for (physical) shapes selection. */ private ShapeSelector shapeSelector; //~ Constructors ------------------------------------------------------------------------------- @@ -459,19 +459,15 @@ private JFrame defineLayout (JFrame frame) // boardsPane.addBoard( // new SampleEvaluationBoard(sampleController, DeepClassifier.getInstance())); // - JSplitPane centerPane = new JSplitPane( - VERTICAL_SPLIT, - sheetSelector, - boardsPane.getComponent()); + JSplitPane centerPane = + new JSplitPane(VERTICAL_SPLIT, sheetSelector, boardsPane.getComponent()); centerPane.setBorder(null); centerPane.setOneTouchExpandable(true); centerPane.setName("centerPane"); // Right - JSplitPane rightPane = new JSplitPane( - VERTICAL_SPLIT, - sampleListing, - sampleContext.getComponent()); + JSplitPane rightPane = + new JSplitPane(VERTICAL_SPLIT, sampleListing, sampleContext.getComponent()); rightPane.setBorder(null); rightPane.setOneTouchExpandable(true); rightPane.setName("rightPane"); @@ -537,7 +533,7 @@ public void displayAll (Collection samples) EnumSet shapeSet = EnumSet.noneOf(Shape.class); for (Sample sample : samples) { - shapeSet.add(sample.getShape()); + shapeSet.add(sample.getShape().getPhysicalShape()); } shapeSelector.populateWith(shapeSet); @@ -545,7 +541,7 @@ public void displayAll (Collection samples) // Populate samples List sorted = new ArrayList<>(samples); - Collections.sort(sorted, Sample.byShape); // Must be ordered by shape for listing + Collections.sort(sorted, Sample.byPhysicalShape); // Ordered by physical shape for listing sampleListing.populateWith(sorted); connectSelectors(true); // Re-enable standard triggers: sheets -> shapes -> samples @@ -1028,8 +1024,7 @@ private static class SampleEvaluationBoard @Override protected void evaluate (Glyph glyph) { - if (glyph instanceof Sample) { - final Sample sample = (Sample) glyph; + if (glyph instanceof Sample sample) { selector.setEvals( classifier.evaluate( glyph, @@ -1045,11 +1040,13 @@ protected void evaluate (Glyph glyph) } } + //----------// + // Selector // + //----------// private abstract static class Selector extends TitledPanel implements ChangeListener { - // Buttons protected final JButton selectAll = new JButton("Select All"); @@ -1243,7 +1240,7 @@ public Component getListCellRendererComponent (JList list, // ShapeSelector // //---------------// /** - * Display a list of available shapes (within selected sheets) and let user make a + * Display a list of available physical shapes (within selected sheets) and let user make a * selection. */ private class ShapeSelector @@ -1282,8 +1279,8 @@ private class ShapePopup { super("ShapePopup"); - ApplicationActionMap actionMap = OmrGui.getApplication().getContext().getActionMap( - SampleBrowser.this); + ApplicationActionMap actionMap = + OmrGui.getApplication().getContext().getActionMap(SampleBrowser.this); add(new JMenuItem(actionMap.get("removeShapes"))); } } @@ -1293,7 +1290,7 @@ private class ShapePopup // SheetSelector // //---------------// /** - * Display a list of available sheets and let user make a selection. + * Display a list of available sheets and let the user make a selection. */ private class SheetSelector extends Selector @@ -1337,8 +1334,8 @@ private class SheetPopup { super("SheetPopup"); - ApplicationActionMap actionMap = OmrGui.getApplication().getContext().getActionMap( - SampleBrowser.this); + ApplicationActionMap actionMap = + OmrGui.getApplication().getContext().getActionMap(SampleBrowser.this); add(new JMenuItem(actionMap.get("removeSheets"))); add(new JMenuItem(actionMap.get("validateSheets"))); } diff --git a/app/src/main/java/org/audiveris/omr/classifier/ui/SampleListing.java b/app/src/main/java/org/audiveris/omr/classifier/ui/SampleListing.java index 6b18284e7..53e13200f 100644 --- a/app/src/main/java/org/audiveris/omr/classifier/ui/SampleListing.java +++ b/app/src/main/java/org/audiveris/omr/classifier/ui/SampleListing.java @@ -248,8 +248,8 @@ void removeSample (Sample sample) */ private void sortBy (Comparator comparator) { - final Sample currentSample = (Sample) browser.getSampleController().getGlyphService() - .getSelectedEntity(); + final Sample currentSample = + (Sample) browser.getSampleController().getGlyphService().getSelectedEntity(); final ShapePane shapePane = getShapePane(currentSample.getShape()); final List samples = Collections.list(shapePane.model.elements()); Collections.sort(samples, comparator); @@ -279,6 +279,7 @@ public void stateChanged (ChangeEvent e) } } + Collections.sort(allSamples, Sample.byShape); populateWith(allSamples); } @@ -600,7 +601,7 @@ private class ShapePane ShapePane (Shape shape, List samples) { - super(shape + " (" + samples.size() + ")"); + super(fullName(shape) + " (" + samples.size() + ")"); this.shape = shape; setLayout(new BorderLayout()); @@ -643,9 +644,8 @@ public void keyPressed (KeyEvent ke) add(list, BorderLayout.CENTER); // Support for delete key - getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( - KeyStroke.getKeyStroke("DELETE"), - "RemoveAction"); + getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke("DELETE"), "RemoveAction"); getActionMap().put("RemoveAction", new RemoveAction()); // ShapePane popup inherited from scrollablePanel parent @@ -657,6 +657,17 @@ public void keyPressed (KeyEvent ke) list.setComponentPopupMenu(null); } + private static String fullName (Shape shape) + { + final Shape physical = shape.getPhysicalShape(); + + if (physical == shape) { + return shape.name(); + } + + return physical + " / " + shape; + } + public Shape getShape () { return shape; @@ -747,8 +758,8 @@ private class SortByGradeAction public void actionPerformed (ActionEvent e) { // To avoid repetitive grade computing, we save grade into GradedSample entities - final Sample currentSample = (Sample) browser.getSampleController().getGlyphService() - .getSelectedEntity(); + final Sample currentSample = + (Sample) browser.getSampleController().getGlyphService().getSelectedEntity(); final ShapePane shapePane = getShapePane(currentSample.getShape()); final List list = new ArrayList<>(); @@ -756,9 +767,8 @@ public void actionPerformed (ActionEvent e) for (Enumeration en = shapePane.model.elements(); en.hasMoreElements();) { Sample sample = en.nextElement(); - Evaluation[] evals = classifier.getNaturalEvaluations( - sample, - sample.getInterline()); + Evaluation[] evals = + classifier.getNaturalEvaluations(sample, sample.getInterline()); double grade = evals[sample.getShape().ordinal()].grade; list.add(new GradedSample(sample, grade)); } diff --git a/app/src/main/java/org/audiveris/omr/classifier/ui/TrainingPanel.java b/app/src/main/java/org/audiveris/omr/classifier/ui/TrainingPanel.java index 605610cec..261e5e5dc 100644 --- a/app/src/main/java/org/audiveris/omr/classifier/ui/TrainingPanel.java +++ b/app/src/main/java/org/audiveris/omr/classifier/ui/TrainingPanel.java @@ -167,20 +167,19 @@ class TrainingPanel */ private List checkPopulation (List samples) { - EnumMap> shapeSamples = new EnumMap<>(Shape.class); + final EnumMap> shapeSamples = new EnumMap<>(Shape.class); for (Iterator it = samples.iterator(); it.hasNext();) { - Sample sample = it.next(); - Shape shape = sample.getShape(); + final Sample sample = it.next(); try { - Shape physicalShape = shape.getPhysicalShape(); + final Shape physicalShape = sample.getShape().getPhysicalShape(); if (physicalShape.isTrainable()) { - List list = shapeSamples.get(shape); + List list = shapeSamples.get(physicalShape); if (list == null) { - shapeSamples.put(shape, list = new ArrayList<>()); + shapeSamples.put(physicalShape, list = new ArrayList<>()); } list.add(sample); @@ -189,7 +188,7 @@ private List checkPopulation (List samples) it.remove(); } } catch (Exception ex) { - logger.warn("Removing weird shape: " + shape, ex); + logger.warn("Removing weird sample: " + sample, ex); it.remove(); } } @@ -200,19 +199,19 @@ private List checkPopulation (List samples) final List newSamples = new ArrayList<>(); for (int is = 0; is <= iMax; is++) { - Shape shape = shapes[is]; - List list = shapeSamples.get(shape); + final Shape physicalShape = shapes[is]; + final List list = shapeSamples.get(physicalShape); if (list == null) { - logger.warn("Missing shape: {}", shape); + logger.warn("Missing shape: {}", physicalShape); } else if (!list.isEmpty()) { - logger.info(String.format("%4d %s", list.size(), shape)); + logger.info(String.format("%4d %s", list.size(), physicalShape)); final int size = list.size(); int togo = minCount - size; newSamples.addAll(list); // Ensure minimum sample count is reached for this shape - if ((togo > 0) && (shape != Shape.CLUTTER)) { + if ((togo > 0) && (physicalShape != Shape.CLUTTER)) { Collections.shuffle(list); do { @@ -387,7 +386,6 @@ public void propertyChange (PropertyChangeEvent evt) private static class Constants extends ConstantSet { - private final Constant.Integer listenerPeriod = new Constant.Integer( "period", 50, diff --git a/app/src/main/java/org/audiveris/omr/glyph/Shape.java b/app/src/main/java/org/audiveris/omr/glyph/Shape.java index bb3e85985..66f74d7a0 100644 --- a/app/src/main/java/org/audiveris/omr/glyph/Shape.java +++ b/app/src/main/java/org/audiveris/omr/glyph/Shape.java @@ -48,7 +48,7 @@ * NOTA: All the physical shapes MUST have different characteristics for the glyph * classifier training to work correctly. * The same physical shape can lead to different logical shapes according to the context. - * Two physical shapes are in this case (their name ends with "_set" to make this clear): + * Three physical shapes are in this case (their name ends with "_set" to make this clear): *
    *
  • Physical DOT_set: only the context can disambiguate between: *
      @@ -59,12 +59,16 @@ *
    • a simple text dot. *
    *
  • - *
  • Physical HW_REST_set: depending on the precise pitch position within the staff, it can mean - * different logicals: + *
  • Physical HW_REST_set: depending on the precise pitch position within the staff, it can mean: *
      *
    • HALF_REST
    • *
    • WHOLE_REST
    • *
    + *
  • Physical EIGHTH_set: depending on the context, it can mean: + *
      + *
    • GRACE_NOTE
    • + *
    • METRO_EIGHTH
    • + *
    *
* As far as possible, a display symbol should be generated for every shape. *

@@ -91,6 +95,7 @@ public enum Shape // DOT_set("Dot set"), HW_REST_set("Half & Whole Rest set"), + EIGHTH_set("Grace & beat unit set"), // // Bars --- @@ -206,10 +211,23 @@ public enum Shape // // Grace notes --- // - GRACE_NOTE("Grace Note with no slash"), + //GRACE_NOTE("Grace Note with no slash"), // Handled by EIGHTH_set GRACE_NOTE_DOWN("Grace Note down with no slash"), - GRACE_NOTE_SLASH("Grace Note with a Slash"), - GRACE_NOTE_SLASH_DOWN("Grace Note down with a Slash"), + GRACE_NOTE_SLASH("Grace Note with a slash"), + GRACE_NOTE_SLASH_DOWN("Grace Note down with a slash"), + + // + // Notes for metronome indication --- + // + METRO_WHOLE("Metronome whole note", Colors.SCORE_PHYSICALS), + METRO_HALF("Metronome half note", Colors.SCORE_PHYSICALS), + METRO_QUARTER("Metronome quarter note", Colors.SCORE_PHYSICALS), + //METRO_EIGHTH("Metronome 8th note"), // Handled by EIGHTH_set + METRO_SIXTEENTH("Metronome 16th note", Colors.SCORE_PHYSICALS), + METRO_DOTTED_HALF("Metronome dotted half note", Colors.SCORE_PHYSICALS), + METRO_DOTTED_QUARTER("Metronome dotted quarter note", Colors.SCORE_PHYSICALS), + METRO_DOTTED_EIGHTH("Metronome dotted 8th note", Colors.SCORE_PHYSICALS), + METRO_DOTTED_SIXTEENTH("Metronome dotted 16th note", Colors.SCORE_PHYSICALS), // // Articulations --- @@ -320,6 +338,7 @@ public enum Shape // Miscellaneous --- // CLUTTER("Pure clutter", Colors.SHAPE_UNKNOWN), + /** * ============================================================================================= * End of physical shapes @@ -327,6 +346,7 @@ public enum Shape * All head shapes are among them, they are recognized by template matching * ============================================================================================= */ + TEXT("Sequence of letters & spaces"), CHARACTER("Any letter"), @@ -343,6 +363,12 @@ public enum Shape WHOLE_REST("Rest for a 1", HW_REST_set), HALF_REST("Rest for a 1/2", HW_REST_set), + // + // Shapes based on physical EIGHTH_set --- + // + GRACE_NOTE("Grace Note with no slash", EIGHTH_set), + METRO_EIGHTH("Metronome 8th note", EIGHTH_set), + // // StemLessHeads duration 2 --- // @@ -386,12 +412,18 @@ public enum Shape NOTEHEAD_CIRCLE_X("Circle-x shape note head for unpitched percussion"), // - // Compound notes (head + stem) --- + // Compound notes --- // + SIXTEENTH_NOTE_UP("Filled head plus its up stem and two flags"), + DOTTED_SIXTEENTH_NOTE_UP("Filled head plus its up stem, two flag and dot"), + EIGHTH_NOTE_UP("Filled head plus its up stem and flag"), + DOTTED_EIGHTH_NOTE_UP("Filled head plus its up stem, flag and dot"), QUARTER_NOTE_UP("Filled head plus its up stem"), QUARTER_NOTE_DOWN("Filled head plus its down stem"), + DOTTED_QUARTER_NOTE_UP("Filled head plus its up stem and dot"), HALF_NOTE_UP("Hollow head plus its up stem"), HALF_NOTE_DOWN("Hollow head plus its down stem"), + DOTTED_HALF_NOTE_UP("Hollow head plus its up stem and dot"), // // Beams and slurs --- @@ -460,6 +492,7 @@ public enum Shape LEDGER("Ledger"), SEGMENT("Wedge or ending segment"), LYRICS("Lyrics", Colors.SCORE_LYRICS), + METRONOME("Text-based notes", Colors.SCORE_PHYSICALS), // // Stems --- @@ -495,16 +528,15 @@ public enum Shape // ============================================================================================= // This is the end of shape enumeration // ============================================================================================= - // + private static final Logger logger = LoggerFactory.getLogger(Shape.class); /** Last physical shape. */ public static final Shape LAST_PHYSICAL_SHAPE = CLUTTER; /** A comparator based on shape name. */ - public static final Comparator alphaComparator = (Shape o1, - Shape o2) -> o1.name() - .compareTo(o2.name()); + public static final Comparator alphaComparator = (o1, + o2) -> o1.name().compareTo(o2.name()); //~ Instance fields ---------------------------------------------------------------------------- @@ -743,60 +775,27 @@ public HeadMotif getHeadMotif () */ public Rational getNoteDuration () { - switch (this) { - case LONG_REST: - return new Rational(4, 1); - - case BREVE_REST: - case BREVE: - case BREVE_SMALL: - case BREVE_CROSS: - case BREVE_DIAMOND: - case BREVE_TRIANGLE_DOWN, BREVE_CIRCLE_X: - return Rational.TWO; - - case WHOLE_REST: - case WHOLE_NOTE: - case WHOLE_NOTE_SMALL: - case WHOLE_NOTE_CROSS: - case WHOLE_NOTE_DIAMOND, WHOLE_NOTE_TRIANGLE_DOWN: - case WHOLE_NOTE_CIRCLE_X: - return Rational.ONE; - - case HALF_REST: - case NOTEHEAD_VOID: - case NOTEHEAD_VOID_SMALL: - case NOTEHEAD_CROSS_VOID, NOTEHEAD_DIAMOND_VOID: - case NOTEHEAD_TRIANGLE_DOWN_VOID: - case NOTEHEAD_CIRCLE_X_VOID: - return Rational.HALF; - - case QUARTER_REST: - case NOTEHEAD_BLACK: - case NOTEHEAD_BLACK_SMALL: - case NOTEHEAD_CROSS, NOTEHEAD_DIAMOND_FILLED: - case NOTEHEAD_TRIANGLE_DOWN_FILLED: - case NOTEHEAD_CIRCLE_X: - return Rational.QUARTER; - - 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: - return null; - } + return switch (this) { + case LONG_REST -> new Rational(4, 1); + case BREVE_REST, BREVE, BREVE_SMALL, BREVE_CROSS, BREVE_DIAMOND, BREVE_TRIANGLE_DOWN, // + BREVE_CIRCLE_X // + -> Rational.TWO; + case WHOLE_REST, WHOLE_NOTE, WHOLE_NOTE_SMALL, WHOLE_NOTE_CROSS, WHOLE_NOTE_DIAMOND, // + WHOLE_NOTE_TRIANGLE_DOWN, WHOLE_NOTE_CIRCLE_X // + -> Rational.ONE; + case HALF_REST, NOTEHEAD_VOID, NOTEHEAD_VOID_SMALL, NOTEHEAD_CROSS_VOID, // + NOTEHEAD_DIAMOND_VOID, NOTEHEAD_TRIANGLE_DOWN_VOID, NOTEHEAD_CIRCLE_X_VOID // + -> Rational.HALF; + case QUARTER_REST, NOTEHEAD_BLACK, NOTEHEAD_BLACK_SMALL, NOTEHEAD_CROSS, // + NOTEHEAD_DIAMOND_FILLED, NOTEHEAD_TRIANGLE_DOWN_FILLED, NOTEHEAD_CIRCLE_X // + -> Rational.QUARTER; + case EIGHTH_REST -> new Rational(1, 8); + case ONE_16TH_REST -> new Rational(1, 16); + case ONE_32ND_REST -> new Rational(1, 32); + case ONE_64TH_REST -> new Rational(1, 64); + case ONE_128TH_REST -> new Rational(1, 128); + default -> null; + }; } //------------------// diff --git a/app/src/main/java/org/audiveris/omr/glyph/ShapeSet.java b/app/src/main/java/org/audiveris/omr/glyph/ShapeSet.java index 90f1436d7..95dfd96ab 100644 --- a/app/src/main/java/org/audiveris/omr/glyph/ShapeSet.java +++ b/app/src/main/java/org/audiveris/omr/glyph/ShapeSet.java @@ -119,22 +119,16 @@ public class ShapeSet TIME_TWELVE_EIGHT); /** All sorts of F clefs. */ - public static final EnumSet BassClefs = EnumSet.of( - F_CLEF, - F_CLEF_SMALL, - F_CLEF_8VA, - F_CLEF_8VB); + public static final EnumSet BassClefs = + EnumSet.of(F_CLEF, F_CLEF_SMALL, F_CLEF_8VA, F_CLEF_8VB); /** All sorts of G clefs. */ - public static final EnumSet TrebleClefs = EnumSet.of( - G_CLEF, - G_CLEF_SMALL, - G_CLEF_8VA, - G_CLEF_8VB); + public static final EnumSet TrebleClefs = + EnumSet.of(G_CLEF, G_CLEF_SMALL, G_CLEF_8VA, G_CLEF_8VB); /** All clefs. */ - public static final EnumSet Clefs = EnumSet.copyOf( - shapesOf(TrebleClefs, BassClefs, shapesOf(C_CLEF, PERCUSSION_CLEF))); + public static final EnumSet Clefs = + EnumSet.copyOf(shapesOf(TrebleClefs, BassClefs, shapesOf(C_CLEF, PERCUSSION_CLEF))); /** Flags up. */ public static final EnumSet FlagsUp = EnumSet.of(FLAG_1, FLAG_2, FLAG_3, FLAG_4, FLAG_5); @@ -143,17 +137,12 @@ public class ShapeSet public static final List SmallFlagsUp = Arrays.asList(SMALL_FLAG, SMALL_FLAG_SLASH); /** Flags down. */ - public static final EnumSet FlagsDown = EnumSet.of( - FLAG_1_DOWN, - FLAG_2_DOWN, - FLAG_3_DOWN, - FLAG_4_DOWN, - FLAG_5_DOWN); + public static final EnumSet FlagsDown = + EnumSet.of(FLAG_1_DOWN, FLAG_2_DOWN, FLAG_3_DOWN, FLAG_4_DOWN, FLAG_5_DOWN); /** Small flags down. */ - public static final List SmallFlagsDown = Arrays.asList( - SMALL_FLAG_DOWN, - SMALL_FLAG_SLASH_DOWN); + public static final List SmallFlagsDown = + Arrays.asList(SMALL_FLAG_DOWN, SMALL_FLAG_SLASH_DOWN); /** All SHARP-based keys. */ public static final EnumSet SharpKeys = EnumSet.of( @@ -166,52 +155,35 @@ public class ShapeSet KEY_SHARP_7); /** All FLAT-based keys. */ - public static final EnumSet FlatKeys = EnumSet.of( - KEY_FLAT_1, - KEY_FLAT_2, - KEY_FLAT_3, - KEY_FLAT_4, - KEY_FLAT_5, - KEY_FLAT_6, - KEY_FLAT_7); + public static final EnumSet FlatKeys = EnumSet + .of(KEY_FLAT_1, KEY_FLAT_2, KEY_FLAT_3, KEY_FLAT_4, KEY_FLAT_5, KEY_FLAT_6, KEY_FLAT_7); /** FermataArcs. */ public static final EnumSet FermataArcs = EnumSet.of(FERMATA_ARC, FERMATA_ARC_BELOW); /** Core shapes for barlines. */ - public static final EnumSet CoreBarlines = EnumSet.copyOf( - Arrays.asList(THICK_BARLINE, THICK_CONNECTOR, THIN_BARLINE, THIN_CONNECTOR)); + public static final EnumSet CoreBarlines = EnumSet + .copyOf(Arrays.asList(THICK_BARLINE, THICK_CONNECTOR, THIN_BARLINE, THIN_CONNECTOR)); /** Repeat bars. */ - public static final List RepeatBars = Arrays.asList( - REPEAT_ONE_BAR, - REPEAT_TWO_BARS, - REPEAT_FOUR_BARS); + public static final List RepeatBars = + Arrays.asList(REPEAT_ONE_BAR, REPEAT_TWO_BARS, REPEAT_FOUR_BARS); /** Beams. */ - public static final EnumSet Beams = EnumSet.copyOf( - Arrays.asList(BEAM, BEAM_SMALL, BEAM_HOOK, BEAM_HOOK_SMALL)); + public static final EnumSet Beams = + EnumSet.copyOf(Arrays.asList(BEAM, BEAM_SMALL, BEAM_HOOK, BEAM_HOOK_SMALL)); /** Heads with an oval shape. */ - public static final List HeadsOval = Arrays.asList( - NOTEHEAD_BLACK, - NOTEHEAD_VOID, - WHOLE_NOTE, - BREVE); + public static final List HeadsOval = + Arrays.asList(NOTEHEAD_BLACK, NOTEHEAD_VOID, WHOLE_NOTE, BREVE); /** Heads with a small oval shape. */ - public static final List HeadsOvalSmall = Arrays.asList( - NOTEHEAD_BLACK_SMALL, - NOTEHEAD_VOID_SMALL, - WHOLE_NOTE_SMALL, - BREVE_SMALL); + public static final List HeadsOvalSmall = + Arrays.asList(NOTEHEAD_BLACK_SMALL, NOTEHEAD_VOID_SMALL, WHOLE_NOTE_SMALL, BREVE_SMALL); /** Heads with a cross shape. */ - public static final List HeadsCross = Arrays.asList( - NOTEHEAD_CROSS, - NOTEHEAD_CROSS_VOID, - WHOLE_NOTE_CROSS, - BREVE_CROSS); + public static final List HeadsCross = + Arrays.asList(NOTEHEAD_CROSS, NOTEHEAD_CROSS_VOID, WHOLE_NOTE_CROSS, BREVE_CROSS); /** Heads with a filled cross shape. */ public static final List HeadsCrossBlack = Arrays.asList( // @@ -238,18 +210,12 @@ public class ShapeSet BREVE_TRIANGLE_DOWN); /** Heads with a circle X shape. */ - public static final List HeadsCircle = Arrays.asList( - NOTEHEAD_CIRCLE_X, - NOTEHEAD_CIRCLE_X_VOID, - WHOLE_NOTE_CIRCLE_X, - BREVE_CIRCLE_X); + public static final List HeadsCircle = Arrays + .asList(NOTEHEAD_CIRCLE_X, NOTEHEAD_CIRCLE_X_VOID, WHOLE_NOTE_CIRCLE_X, BREVE_CIRCLE_X); /** All compound notes. */ - public static final List CompoundNotes = Arrays.asList( - QUARTER_NOTE_UP, - QUARTER_NOTE_DOWN, - HALF_NOTE_UP, - HALF_NOTE_DOWN); + public static final List CompoundNotes = + Arrays.asList(QUARTER_NOTE_UP, QUARTER_NOTE_DOWN, HALF_NOTE_UP, HALF_NOTE_DOWN); /** All quarter heads (duration: 1/4). */ public static final EnumSet QuarterHeads = EnumSet.of( @@ -344,23 +310,16 @@ public class ShapeSet WHOLE_NOTE_CIRCLE_X); /** Grace notes. */ - public static final List Graces = Arrays.asList( - GRACE_NOTE, - GRACE_NOTE_DOWN, - GRACE_NOTE_SLASH, - GRACE_NOTE_SLASH_DOWN); + public static final List Graces = + Arrays.asList(GRACE_NOTE, GRACE_NOTE_DOWN, GRACE_NOTE_SLASH, GRACE_NOTE_SLASH_DOWN); /** Octave shifts. */ - public static final List OctaveShifts = Arrays.asList( - OTTAVA, - QUINDICESIMA, - VENTIDUESIMA); + public static final List OctaveShifts = + Arrays.asList(OTTAVA, QUINDICESIMA, VENTIDUESIMA); /** Percussion playing techniques. */ - public static final List Playings = Arrays.asList( - PLAYING_OPEN, - PLAYING_HALF_OPEN, - PLAYING_CLOSED); + public static final List Playings = + Arrays.asList(PLAYING_OPEN, PLAYING_HALF_OPEN, PLAYING_CLOSED); /** Tremolos. */ public static final List Tremolos = Arrays.asList(TREMOLO_1, TREMOLO_2, TREMOLO_3); @@ -418,10 +377,8 @@ public class ShapeSet Tremolos, Arrays.asList(TUPLET_THREE, TUPLET_SIX))); - public static final ShapeSet ClefsAndShifts = new ShapeSet( - G_CLEF, - Colors.SCORE_FRAME, - shapesOf(Clefs, OctaveShifts)); + public static final ShapeSet ClefsAndShifts = + new ShapeSet(G_CLEF, Colors.SCORE_FRAME, shapesOf(Clefs, OctaveShifts)); public static final ShapeSet Dynamics = new ShapeSet( DYNAMICS_F, @@ -459,10 +416,8 @@ public class ShapeSet Colors.SCORE_NOTES, shapesOf(Heads, shapesOf(AUGMENTATION_DOT), CompoundNotes, Playings)); - public static final ShapeSet Markers = new ShapeSet( - CODA, - Colors.SCORE_FRAME, - shapesOf(DAL_SEGNO, DA_CAPO, SEGNO, CODA)); + public static final ShapeSet Markers = + new ShapeSet(CODA, Colors.SCORE_FRAME, shapesOf(DAL_SEGNO, DA_CAPO, SEGNO, CODA)); public static final ShapeSet GraceAndOrnaments = new ShapeSet( MORDENT, @@ -472,11 +427,11 @@ public class ShapeSet GRACE_NOTE_SLASH, GRACE_NOTE_DOWN, GRACE_NOTE_SLASH_DOWN, - TR, TURN, TURN_INVERTED, TURN_UP, TURN_SLASH, + TR, MORDENT, MORDENT_INVERTED)); @@ -533,19 +488,28 @@ public class ShapeSet ROMAN_XI, ROMAN_XII)); + public static final ShapeSet BeatUnits = new ShapeSet( + METRO_EIGHTH, + Colors.SCORE_PHYSICALS, + shapesOf( + METRO_WHOLE, + METRO_HALF, + METRO_QUARTER, + METRO_EIGHTH, + METRO_SIXTEENTH, + METRO_DOTTED_HALF, + METRO_DOTTED_QUARTER, + METRO_DOTTED_EIGHTH, + METRO_DOTTED_SIXTEENTH)); + + public static final ShapeSet Texts = + new ShapeSet(TEXT, Colors.SCORE_PHYSICALS, shapesOf(LYRICS, TEXT, METRONOME)); + public static final ShapeSet Physicals = new ShapeSet( - LEDGER, + SLUR_ABOVE, Colors.SCORE_PHYSICALS, shapesOf( - shapesOf( - LYRICS, - TEXT, /// CHARACTER, - SLUR_ABOVE, - SLUR_BELOW, - LEDGER, - STEM, - ENDING, - ENDING_WRL), + shapesOf(SLUR_ABOVE, SLUR_BELOW, ENDING, ENDING_WRL, STEM, LEDGER), constants.addClutterInPhysicals.isSet() ? shapesOf(CLUTTER) : Collections.emptyList())); @@ -555,9 +519,8 @@ public class ShapeSet // ========================================================================= // /** All physical shapes. Here the use of EnumSet.range is OK */ - public static final EnumSet allPhysicalShapes = EnumSet.range( - Shape.values()[0], - LAST_PHYSICAL_SHAPE); + public static final EnumSet allPhysicalShapes = + EnumSet.range(Shape.values()[0], LAST_PHYSICAL_SHAPE); /** Pedals */ public static final EnumSet Pedals = EnumSet.of(PEDAL_MARK, PEDAL_UP_MARK); @@ -801,8 +764,7 @@ public static void addAllShapes (MusicFamily family, * Populate the given menu with all ShapeSet instances defined * in this class. * - * @param top the JComponent to populate (typically a JMenu or a - * JPopupMenu) + * @param top the JComponent to populate (typically a JMenu or a JPopupMenu) * @param listener the listener for notification of user selection */ public static void addAllShapeSets (JComponent top, @@ -1083,8 +1045,8 @@ public static EnumSet getTemplateNotesStem (Sheet sheet) */ public static Collection shapesOf (Collection col) { - Collection shapes = (col instanceof List) ? new ArrayList<>() - : EnumSet.noneOf(Shape.class); + Collection shapes = + (col instanceof List) ? new ArrayList<>() : EnumSet.noneOf(Shape.class); shapes.addAll(col); @@ -1104,8 +1066,8 @@ public static Collection shapesOf (Collection col) public static Collection shapesOf (Collection col1, Collection col2) { - Collection shapes = (col1 instanceof List) ? new ArrayList<>() - : EnumSet.noneOf(Shape.class); + Collection shapes = + (col1 instanceof List) ? new ArrayList<>() : EnumSet.noneOf(Shape.class); shapes.addAll(col1); shapes.addAll(col2); @@ -1128,8 +1090,8 @@ public static Collection shapesOf (Collection col1, Collection col2, Collection col3) { - Collection shapes = (col1 instanceof List) ? new ArrayList<>() - : EnumSet.noneOf(Shape.class); + Collection shapes = + (col1 instanceof List) ? new ArrayList<>() : EnumSet.noneOf(Shape.class); shapes.addAll(col1); shapes.addAll(col2); @@ -1155,8 +1117,8 @@ public static Collection shapesOf (Collection col1, Collection col3, Collection col4) { - Collection shapes = (col1 instanceof List) ? new ArrayList<>() - : EnumSet.noneOf(Shape.class); + Collection shapes = + (col1 instanceof List) ? new ArrayList<>() : EnumSet.noneOf(Shape.class); shapes.addAll(col1); shapes.addAll(col2); @@ -1203,9 +1165,8 @@ private static class Constants extends ConstantSet { - private final Constant.Boolean addClutterInPhysicals = new Constant.Boolean( - false, - "(Hidden feature)"); + private final Constant.Boolean addClutterInPhysicals = + new Constant.Boolean(false, "(Hidden feature)"); } //-----------// diff --git a/app/src/main/java/org/audiveris/omr/glyph/ui/EvaluationBoard.java b/app/src/main/java/org/audiveris/omr/glyph/ui/EvaluationBoard.java index bc9e0273b..de4d736a5 100644 --- a/app/src/main/java/org/audiveris/omr/glyph/ui/EvaluationBoard.java +++ b/app/src/main/java/org/audiveris/omr/glyph/ui/EvaluationBoard.java @@ -49,7 +49,6 @@ import org.slf4j.LoggerFactory; import com.jgoodies.forms.builder.FormBuilder; -import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import com.jgoodies.forms.layout.FormSpecs; @@ -84,8 +83,7 @@ public class EvaluationBoard private static final Logger logger = LoggerFactory.getLogger(EvaluationBoard.class); /** Events this board is interested in */ - private static final Class[] eventsRead = new Class[] - { EntityListEvent.class }; + private static final Class[] eventsRead = new Class[] { EntityListEvent.class }; /** Color for well recognized glyphs */ private static final Color EVAL_GOOD_COLOR = new Color(100, 200, 100); @@ -98,17 +96,17 @@ public class EvaluationBoard /** Underlying glyph classifier. */ protected final Classifier classifier; - /** Related inters controller */ + /** Related inters controller. */ protected final InterController interController; - /** Related sheet */ + /** Related sheet. */ @Navigable(false) private final Sheet sheet; - /** Pane for detailed info display about the glyph evaluation */ + /** Pane for detailed info display about the glyph evaluation. */ protected final Selector selector; - /** Do we use GlyphChecker annotations? */ + /** Do we use GlyphChecker annotations?. */ private boolean useAnnotations; /** True for active buttons, false for passive fields. */ @@ -163,7 +161,7 @@ public EvaluationBoard (boolean isActive, //--------------// private void defineLayout () { - String colSpec = Panel.makeColumns(3); + String colSpec = Panel.makeColumns(2, "right:", Panel.getLabelWidth(), "50dlu"); FormLayout layout = new FormLayout(colSpec, ""); int visibleButtons = Math.min(constants.visibleButtons.getValue(), selector.buttons.size()); @@ -177,13 +175,12 @@ private void defineLayout () } FormBuilder builder = FormBuilder.create().layout(layout).panel(getBody()); - CellConstraints cst = new CellConstraints(); for (int i = 0; i < visibleButtons; i++) { int r = (2 * i) + 1; // -------------------------------- EvalButton evb = selector.buttons.get(i); - builder.addRaw(evb.grade).xy(5, r); - builder.addRaw(isActive ? evb.button : evb.field).xyw(7, r, 5); + builder.addRaw(evb.grade).xy(1, r); + builder.addRaw(isActive ? evb.button : evb.field).xyw(3, r, 5); } } @@ -219,11 +216,11 @@ protected void evaluate (Glyph glyph) return; } - } else if (glyph instanceof Sample) { + } else if (glyph instanceof Sample sample) { selector.setEvals( classifier.evaluate( glyph, - ((Sample) glyph).getInterline(), + sample.getInterline(), evalCount(), constants.minGrade.getValue(), useAnnotations ? EnumSet.of(Classifier.Condition.CHECKED) @@ -286,6 +283,34 @@ public void selectButton (int buttonID) } } + //--------// + // update // + //--------// + @Override + public void update () + { + final MusicFamily musicFamily = (sheet != null) ? sheet.getStub().getMusicFamily() + : MusicFont.getDefaultMusicFamily(); + + if (musicFamily != cachedMusicFamily) { + selector.buttons.forEach(b -> { + if ((b.button != null) && b.button.isVisible()) { + final Shape shape = Shape.valueOf(b.button.getText()); + final ShapeSymbol symbol = shape.getDecoratedSymbol(musicFamily); + b.button.setIcon((symbol != null) ? new FixedWidthIcon(symbol) : null); + } + + if ((b.field != null) && b.field.isVisible()) { + final Shape shape = Shape.valueOf(b.field.getText()); + final ShapeSymbol symbol = shape.getDecoratedSymbol(musicFamily); + b.field.setIcon((symbol != null) ? new FixedWidthIcon(symbol) : null); + } + }); + + cachedMusicFamily = musicFamily; + } + } + //~ Static Methods ----------------------------------------------------------------------------- //-----------// @@ -399,19 +424,18 @@ public void setEval (Evaluation eval, final String tip = (failure != null) ? failure.toString() : null; final MusicFamily family = sheet != null ? sheet.getStub().getMusicFamily() : MusicFont.getDefaultMusicFamily(); + final ShapeSymbol symbol = eval.shape.getDecoratedSymbol(family); if (isActive) { button.setEnabled(enabled); button.setText(text); button.setToolTipText(tip); - ShapeSymbol symbol = eval.shape.getDecoratedSymbol(family); button.setIcon((symbol != null) ? new FixedWidthIcon(symbol) : null); } else { field.setText(text); field.setToolTipText(tip); - final ShapeSymbol symbol = eval.shape.getDecoratedSymbol(family); field.setIcon((symbol != null) ? new FixedWidthIcon(symbol) : null); } @@ -437,7 +461,6 @@ public void setEval (Evaluation eval, //----------// protected class Selector { - // A collection of EvalButton's final List buttons = new ArrayList<>(); diff --git a/app/src/main/java/org/audiveris/omr/math/GeoUtil.java b/app/src/main/java/org/audiveris/omr/math/GeoUtil.java index 5349abd52..7e22b4bdb 100644 --- a/app/src/main/java/org/audiveris/omr/math/GeoUtil.java +++ b/app/src/main/java/org/audiveris/omr/math/GeoUtil.java @@ -164,6 +164,28 @@ public static double ptDistanceSq (Rectangle r, return d; } + //---------// + // rounded // + //---------// + /** + * Report a rectangle with integer coordinates + * + * @param r provided Rectangle2D instance + * @return Rectangle instance + */ + public static Rectangle rounded (Rectangle2D r) + { + if (r == null) { + return null; + } + + return new Rectangle( + (int) Math.rint(r.getX()), + (int) Math.rint(r.getY()), + (int) Math.rint(r.getWidth()), + (int) Math.rint(r.getHeight())); + } + //-------// // touch // //-------// diff --git a/app/src/main/java/org/audiveris/omr/moments/GeometricMoments.java b/app/src/main/java/org/audiveris/omr/moments/GeometricMoments.java index dc99c1ba8..dfd607194 100644 --- a/app/src/main/java/org/audiveris/omr/moments/GeometricMoments.java +++ b/app/src/main/java/org/audiveris/omr/moments/GeometricMoments.java @@ -233,24 +233,26 @@ public GeometricMoments (int[] xx, // (Invariant to translation / scaling / rotation) int i = 12; k[i++] = n20 + n02; - // + k[i++] = ((n20 - n02) * (n20 - n02)) + (4 * n11 * n11); - // - k[i++] = ((n30 - (3 * n12)) * (n30 - (3 * n12))) + ((n03 - (3 * n21)) * (n03 - (3 - * n21))); - // + + k[i++] = ((n30 - (3 * n12)) * (n30 - (3 * n12))) + + ((n03 - (3 * n21)) * (n03 - (3 * n21))); + k[i++] = ((n30 + n12) * (n30 + n12)) + ((n03 + n21) * (n03 + n21)); - // - k[i++] = ((n30 - (3 * n12)) * (n30 + n12) * (((n30 + n12) * (n30 + n12)) - (3 * (n21 - + n03) * (n21 + n03)))) + ((n03 - (3 * n21)) * (n03 + n21) * (((n03 + n21) - * (n03 + n21)) - (3 * (n12 + n30) * (n12 + n30)))); - // + + k[i++] = ((n30 - (3 * n12)) * (n30 + n12) + * (((n30 + n12) * (n30 + n12)) - (3 * (n21 + n03) * (n21 + n03)))) + + ((n03 - (3 * n21)) * (n03 + n21) + * (((n03 + n21) * (n03 + n21)) - (3 * (n12 + n30) * (n12 + n30)))); + k[i++] = ((n20 - n02) * (((n30 + n12) * (n30 + n12)) - ((n03 + n21) * (n03 + n21)))) + (4 * n11 * (n30 + n12) * (n03 + n21)); - // - k[i++] = (((3 * n21) - n03) * (n30 + n12) * (((n30 + n12) * (n30 + n12)) - (3 * (n21 - + n03) * (n21 + n03)))) - (((3 * n12) - n30) * (n03 + n21) * (((n03 + n21) - * (n03 + n21)) - (3 * (n12 + n30) * (n12 + n30)))); + + k[i++] = (((3 * n21) - n03) * (n30 + n12) + * (((n30 + n12) * (n30 + n12)) - (3 * (n21 + n03) * (n21 + n03)))) + - (((3 * n12) - n30) * (n03 + n21) + * (((n03 + n21) * (n03 + n21)) - (3 * (n12 + n30) * (n12 + n30)))); } } @@ -283,7 +285,7 @@ public double getHeight () } //--------// - // getN12 // + // getN11 // //--------// /** * Report the n11 moment (which relates to xy covariance). diff --git a/app/src/main/java/org/audiveris/omr/score/MusicXML.java b/app/src/main/java/org/audiveris/omr/score/MusicXML.java index fede99b75..8a0cdded3 100644 --- a/app/src/main/java/org/audiveris/omr/score/MusicXML.java +++ b/app/src/main/java/org/audiveris/omr/score/MusicXML.java @@ -53,9 +53,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.math.BigDecimal; +import jakarta.xml.bind.JAXBElement; -import javax.xml.bind.JAXBElement; +import java.math.BigDecimal; /** * Class MusicXML gathers convenient methods dealing with MusicXML data diff --git a/app/src/main/java/org/audiveris/omr/score/PartwiseBuilder.java b/app/src/main/java/org/audiveris/omr/score/PartwiseBuilder.java index b194dd6b2..efae6ed2b 100644 --- a/app/src/main/java/org/audiveris/omr/score/PartwiseBuilder.java +++ b/app/src/main/java/org/audiveris/omr/score/PartwiseBuilder.java @@ -56,6 +56,7 @@ import org.audiveris.omr.sig.inter.ArpeggiatoInter; import org.audiveris.omr.sig.inter.ArticulationInter; import org.audiveris.omr.sig.inter.BeamGroupInter; +import org.audiveris.omr.sig.inter.BeatUnitInter; import org.audiveris.omr.sig.inter.ChordNameInter; import org.audiveris.omr.sig.inter.ClefInter; import org.audiveris.omr.sig.inter.DynamicsInter; @@ -69,6 +70,7 @@ import org.audiveris.omr.sig.inter.LyricItemInter; import org.audiveris.omr.sig.inter.MarkerInter; import org.audiveris.omr.sig.inter.MeasureRepeatInter; +import org.audiveris.omr.sig.inter.MetronomeInter; import org.audiveris.omr.sig.inter.MultipleRestInter; import org.audiveris.omr.sig.inter.OctaveShiftInter; import org.audiveris.omr.sig.inter.OrnamentInter; @@ -80,7 +82,6 @@ import org.audiveris.omr.sig.inter.SlurInter; import org.audiveris.omr.sig.inter.SmallChordInter; import org.audiveris.omr.sig.inter.StaffBarlineInter; -import org.audiveris.omr.sig.inter.TempoInter; import org.audiveris.omr.sig.inter.TremoloInter; import org.audiveris.omr.sig.inter.TupletInter; import org.audiveris.omr.sig.inter.WedgeInter; @@ -157,6 +158,7 @@ import org.audiveris.proxymusic.MeasureNumberingValue; import org.audiveris.proxymusic.MeasureRepeat; import org.audiveris.proxymusic.MeasureStyle; +import org.audiveris.proxymusic.Metronome; import org.audiveris.proxymusic.MidiInstrument; import org.audiveris.proxymusic.MultipleRest; import org.audiveris.proxymusic.Notations; @@ -174,6 +176,7 @@ import org.audiveris.proxymusic.PartName; import org.audiveris.proxymusic.Pedal; import org.audiveris.proxymusic.PedalType; +import org.audiveris.proxymusic.PerMinute; import org.audiveris.proxymusic.Pitch; import org.audiveris.proxymusic.PlacementText; import org.audiveris.proxymusic.Print; @@ -222,6 +225,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jakarta.xml.bind.JAXBElement; +import jakarta.xml.bind.JAXBException; + import java.awt.Font; import java.awt.Point; import java.awt.Rectangle; @@ -247,9 +253,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import javax.xml.bind.JAXBElement; -import javax.xml.bind.JAXBException; - /** * Class PartwiseBuilder builds a ProxyMusic MusicXML {@link ScorePartwise} * from an Audiveris {@link Score} instance. @@ -277,12 +280,12 @@ public class PartwiseBuilder }); /** Default page horizontal margin. */ - private static final BigDecimal pageHorizontalMargin = - new BigDecimal(constants.pageHorizontalMargin.getValue()); + private static final BigDecimal pageHorizontalMargin = new BigDecimal( + constants.pageHorizontalMargin.getValue()); /** Default page vertical margin. */ - private static final BigDecimal pageVerticalMargin = - new BigDecimal(constants.pageVerticalMargin.getValue()); + private static final BigDecimal pageVerticalMargin = new BigDecimal( + constants.pageVerticalMargin.getValue()); /** Maximum level number. */ private static final int MAX_LEVEL_NUMBER = 16; @@ -711,8 +714,8 @@ private Key getCurrentKey () // Browse the current list of measures backwards within current part List measures = current.pmPart.getMeasure(); - for (ListIterator it = - measures.listIterator(measures.size()); it.hasPrevious();) { + for (ListIterator it = measures.listIterator( + measures.size()); it.hasPrevious();) { ScorePartwise.Part.Measure pmMeasure = it.previous(); for (Object obj : pmMeasure.getNoteOrBackupOrForward()) { @@ -1004,8 +1007,8 @@ private void insertMultipleRest (MeasureStack stack) // Insert dummy measure current.pmMeasure = factory.createScorePartwisePartMeasure(); current.pmPart.getMeasure().add(current.pmMeasure); - current.pmMeasure - .setNumber(stack.getScoreId(current.pageMeasureIdOffset + num + 1)); + current.pmMeasure.setNumber( + stack.getScoreId(current.pageMeasureIdOffset + num + 1)); } } } @@ -1054,8 +1057,8 @@ private boolean isNewClef (Clef newClef) // Browse the current list of measures backwards List measures = current.pmPart.getMeasure(); - for (ListIterator mit = - measures.listIterator(measures.size()); mit.hasPrevious();) { + for (ListIterator mit = measures.listIterator( + measures.size()); mit.hasPrevious();) { ScorePartwise.Part.Measure pmMeasure = mit.previous(); // Look backwards in measure items, checking staff @@ -1171,8 +1174,8 @@ private void processBarline (PartBarline partBarline, final MeasureStack stack = current.measure.getStack(); final PartBarline.Style style = partBarline.getStyle(); final List fermatas = partBarline.getFermatas(); // Top down list - final EndingInter ending = - partBarline.getEnding((location == RightLeftMiddle.RIGHT) ? RIGHT : LEFT); + final EndingInter ending = partBarline.getEnding( + (location == RightLeftMiddle.RIGHT) ? RIGHT : LEFT); final String endingValue = (ending != null) ? ending.getValue() : null; String endingNumber = (ending != null) ? ending.getExportedNumber() : null; @@ -1197,8 +1200,8 @@ private void processBarline (PartBarline partBarline, // Specific barline on left side: needed |= (partBarline == current.measure.getLeftPartBarline()); // On left side, with stuff (left repeat, left ending): - needed |= ((location == RightLeftMiddle.LEFT) - && (stack.isRepeat(LEFT) || (ending != null))); + needed |= ((location == RightLeftMiddle.LEFT) && (stack.isRepeat(LEFT) + || (ending != null))); // Specific barline on middle location: needed |= (location == RightLeftMiddle.MIDDLE); // On right side, but with stuff (right repeat, right ending, fermata) or non regular: @@ -1279,8 +1282,8 @@ private void processBarline (PartBarline partBarline, } // Pick up last inverted fermata if any. - for (ListIterator it = - fermatas.listIterator(fermatas.size()); it.hasPrevious();) { + for (ListIterator it = fermatas.listIterator( + fermatas.size()); it.hasPrevious();) { FermataInter f = it.previous(); if (f.getShape() == Shape.FERMATA_BELOW) { @@ -1331,8 +1334,9 @@ private void processBarline (PartBarline partBarline, PartBarline topPartBarline = getBarlineOnLeft(topMeasure); if (topPartBarline != null) { - StaffBarlineInter topBarline = - topPartBarline.getStaffBarline(part, part.getFirstStaff()); + StaffBarlineInter topBarline = topPartBarline.getStaffBarline( + part, + part.getFirstStaff()); for (Inter marker : topBarline.getRelatedInters(MarkerBarRelation.class)) { processMarker((MarkerInter) marker); @@ -1491,46 +1495,89 @@ private void processClef (ClefInter clef) //------------------// // processDirection // //------------------// + // For sentences linked to a note private void processDirection (SentenceInter sentence) { try { logger.debug("Visiting {}", sentence); - String content = sentence.getValue(); + final String content = sentence.getValue(); + final Direction direction = factory.createDirection(); + final Point2D location = sentence.getLocation(); - Direction direction = factory.createDirection(); DirectionType directionType = factory.createDirectionType(); - FormattedTextId pmWords = factory.createFormattedTextId(); - Point2D location = sentence.getLocation(); - - pmWords.setValue(content); + direction.getDirectionType().add(directionType); // Staff - Staff staff = current.note.getStaff(); + final Staff staff = current.note.getStaff(); insertStaffId(direction, staff); // Placement direction.setPlacement( (location.getY() < current.note.getCenter().y) ? AboveBelow.ABOVE : AboveBelow.BELOW); + // Metronome? + if (sentence instanceof MetronomeInter metro) { + final Metronome metronome = factory.createMetronome(); + + // Tempo text indication? + final String tempoText = metro.getTempoText(); + if (!tempoText.isBlank()) { + // NOTA: Tempo text is put in a separate directionType element + final FormattedTextId pmWords = factory.createFormattedTextId(); + pmWords.setValue(tempoText); + pmWords.setDefaultY(yOf(location, staff)); + pmWords.setRelativeX( + toTenths(location.getX() - current.note.getCenterLeft().x)); + setFontInfo(pmWords, sentence); + directionType.getWordsOrSymbol().add(pmWords); + + directionType = factory.createDirectionType(); + direction.getDirectionType().add(directionType); + } else { + metronome.setDefaultY(yOf(location, staff)); + metronome.setRelativeX( + toTenths(location.getX() - current.note.getCenterLeft().x)); + } - // default-y - pmWords.setDefaultY(yOf(location, staff)); + // Note symbol + final BeatUnitInter.Note note = metro.getNote(); + metronome.setBeatUnit(note.toMusicXml()); - // Font information - setFontInfo(pmWords, sentence); + // Dotted symbol? + if (note.hasDot()) { + metronome.getBeatUnitDot().add(factory.createEmpty()); + } - // relative-x - pmWords.setRelativeX(toTenths(location.getX() - current.note.getCenterLeft().x)); + // BPM text + final PerMinute perMinute = factory.createPerMinute(); + perMinute.setValue(metro.getBpmText()); + metronome.setPerMinute(perMinute); - directionType.getWordsOrSymbol().add(pmWords); - direction.getDirectionType().add(directionType); + if (metro.hasParentheses()) { + metronome.setParentheses(YesNo.YES); + } - // Tempo? - if (sentence instanceof TempoInter tempo) { + directionType.setMetronome(metronome); + + // Sound tempo based on metronome value final Sound sound = factory.createSound(); - sound.setTempo(new BigDecimal(tempo.getBpm())); + sound.setTempo(new BigDecimal(metro.getQuartersPerMinute())); direction.setSound(sound); + } else { + final FormattedTextId pmWords = factory.createFormattedTextId(); + pmWords.setValue(content); + + // default-y + pmWords.setDefaultY(yOf(location, staff)); + + // relative-x + pmWords.setRelativeX(toTenths(location.getX() - current.note.getCenterLeft().x)); + + // Font information + setFontInfo(pmWords, sentence); + + directionType.getWordsOrSymbol().add(pmWords); } // Everything is now OK @@ -1957,29 +2004,10 @@ private void processMeasure (Measure measure) // Number of staves, if > 1 if (isScoreFirstMeasure && current.logicalPart.isMultiStaff()) { - getAttributes() - .setStaves(new BigInteger("" + current.logicalPart.getStaffCount())); + getAttributes().setStaves( + new BigInteger("" + current.logicalPart.getStaffCount())); } - // // Tempo? - // if (isScoreFirstMeasure && isFirst.part && !measure.isDummy()) { - // Direction direction = factory.createDirection(); - // current.pmMeasure.getNoteOrBackupOrForward().add(direction); - // direction.setPlacement(AboveBelow.ABOVE); - // - // DirectionType directionType = factory.createDirectionType(); - // direction.getDirectionType().add(directionType); - // - // // Use a dummy words element - // FormattedTextId pmWords = factory.createFormattedTextId(); - // directionType.getWordsOrSymbol().add(pmWords); - // pmWords.setValue(""); - // - // Sound sound = factory.createSound(); - // sound.setTempo(new BigDecimal(score.getTempoParam().getValue())); - // direction.setSound(sound); - // } - // Insert KeySignature(s), if any (they may vary between staves) processKeys(); @@ -2302,17 +2330,17 @@ private void processNote (AbstractNoteInter note) // Default-x (use left side of the note wrt measure) if (!current.measure.isDummy() && !current.repeatCopying) { int noteLeft = note.getCenterLeft().x; - current.pmNote - .setDefaultX(toTenths(noteLeft - current.measure.getAbscissa(LEFT, staff))); + current.pmNote.setDefaultX( + toTenths(noteLeft - current.measure.getAbscissa(LEFT, staff))); } // Tuplet factor? if (chord.getTupletFactor() != null) { TimeModification timeModification = factory.createTimeModification(); - timeModification - .setActualNotes(new BigInteger("" + chord.getTupletFactor().actualDen)); - timeModification - .setNormalNotes(new BigInteger("" + chord.getTupletFactor().actualNum)); + timeModification.setActualNotes( + new BigInteger("" + chord.getTupletFactor().actualDen)); + timeModification.setNormalNotes( + new BigInteger("" + chord.getTupletFactor().actualNum)); TupletInter tuplet = chord.getTuplet(); Rational chordDur = chord.getDurationSansDotOrTuplet(); @@ -2327,8 +2355,8 @@ private void processNote (AbstractNoteInter note) if (isFirstInChord) { List embraced = tuplet.getChords(); - if ((embraced.get(0) == chord) - || (embraced.get(embraced.size() - 1) == chord)) { + if ((embraced.get(0) == chord) || (embraced.get( + embraced.size() - 1) == chord)) { processTuplet(tuplet); } } @@ -2381,8 +2409,8 @@ private void processNote (AbstractNoteInter note) final MotifSign ms = new MotifSign(motif, sign); final DrumSet drumSet = DrumSet.getInstance(); final int lineCount = staff.getLineCount(); - final Map> staffSet = - drumSet.getStaffSet(lineCount); + final Map> staffSet = drumSet + .getStaffSet(lineCount); if (staffSet == null) { logger.warn("No drum set defined for staff size {}", lineCount); } else { @@ -2513,8 +2541,8 @@ private void processNote (AbstractNoteInter note) : AboveBelow.BELOW); pmFingering.setDefaultY(yOf(fingering.getCenter(), staff)); - getTechnical().getUpBowOrDownBowOrHarmonic() - .add(factory.createTechnicalFingering(pmFingering)); + getTechnical().getUpBowOrDownBowOrHarmonic().add( + factory.createTechnicalFingering(pmFingering)); } // Plucking? @@ -2527,8 +2555,8 @@ private void processNote (AbstractNoteInter note) : AboveBelow.BELOW); placement.setDefaultY(yOf(plucking.getCenter(), staff)); - getTechnical().getUpBowOrDownBowOrHarmonic() - .add(factory.createTechnicalPluck(placement)); + getTechnical().getUpBowOrDownBowOrHarmonic().add( + factory.createTechnicalPluck(placement)); } } @@ -2783,24 +2811,22 @@ private void processScore () // [Encoding]/Supports // 1/ Attributes of 'print' element - for (String attribute : new String[] - { "new-system", "new-page" }) { + for (String attribute : new String[] { "new-system", "new-page" }) { final Supports supports = factory.createSupports(); supports.setElement("print"); supports.setType(YesNo.YES); supports.setAttribute(attribute); supports.setValue("yes"); - encoding.getEncodingDateOrEncoderOrSoftware() - .add(factory.createEncodingSupports(supports)); + encoding.getEncodingDateOrEncoderOrSoftware().add( + factory.createEncodingSupports(supports)); } // 2/ Other elements - for (String element : new String[] - { "accidental", "beam", "stem" }) { + for (String element : new String[] { "accidental", "beam", "stem" }) { final Supports supports = factory.createSupports(); supports.setElement(element); supports.setType(YesNo.YES); - encoding.getEncodingDateOrEncoderOrSoftware() - .add(factory.createEncodingSupports(supports)); + encoding.getEncodingDateOrEncoderOrSoftware().add( + factory.createEncodingSupports(supports)); } identification.setEncoding(encoding); @@ -2876,6 +2902,7 @@ private void processScore () //-----------------// // processSentence // //-----------------// + // For stand-alone sentences (not linked to a note) private void processSentence (SentenceInter sentence) { try { @@ -2915,8 +2942,7 @@ private void processSentence (SentenceInter sentence) case UnknownRole -> {} default -> { - // LyricsItem, Direction, ChordName - // Handle them through related Note + // LyricItem, Direction, Metronome, ChordName are handled through related Note return; } } @@ -3107,8 +3133,8 @@ private void processSystem (SystemInfo system) } else { // Need to build a dummy system Part on-the-fly // Based on the first usable (i.e. not tablature) part - final Part dummyPart = - system.getFirstStandardPart().createDummyPart(current.logicalPart.getId()); + final Part dummyPart = system.getFirstStandardPart().createDummyPart( + current.logicalPart.getId()); current.isDrumPart = dummyPart.isDrumPart(); processPart(dummyPart); } @@ -3389,10 +3415,10 @@ private BigDecimal yOf (Point2D point, private static boolean areEqual (Clef left, Clef right) { - return Objects.equals(left.getNumber(), right.getNumber()) - && Objects.equals(left.getSign(), right.getSign()) - && Objects.equals(left.getLine(), right.getLine()) - && Objects.equals(left.getClefOctaveChange(), right.getClefOctaveChange()); + return Objects.equals(left.getNumber(), right.getNumber()) && Objects.equals( + left.getSign(), + right.getSign()) && Objects.equals(left.getLine(), right.getLine()) && Objects + .equals(left.getClefOctaveChange(), right.getClefOctaveChange()); } //----------// @@ -3541,14 +3567,19 @@ private static class Constants extends ConstantSet { - private final Constant.Integer pageHorizontalMargin = - new Constant.Integer("tenths", 80, "Page horizontal margin"); + private final Constant.Integer pageHorizontalMargin = new Constant.Integer( + "tenths", + 80, + "Page horizontal margin"); - private final Constant.Integer pageVerticalMargin = - new Constant.Integer("tenths", 80, "Page vertical margin"); + private final Constant.Integer pageVerticalMargin = new Constant.Integer( + "tenths", + 80, + "Page vertical margin"); - private final Constant.Boolean avoidTupletBrackets = - new Constant.Boolean(false, "Should we avoid brackets for all tuplets"); + private final Constant.Boolean avoidTupletBrackets = new Constant.Boolean( + false, + "Should we avoid brackets for all tuplets"); } //---------// @@ -3759,8 +3790,8 @@ private void populatePrint () if (!isFirst.part || (staff.getIndexInPart() > 0)) { try { StaffLayout staffLayout = factory.createStaffLayout(); - staffLayout - .setNumber(new BigInteger("" + (1 + staff.getIndexInPart()))); + staffLayout.setNumber( + new BigInteger("" + (1 + staff.getIndexInPart()))); int staffIndexInSystem = system.getStaves().indexOf(staff); diff --git a/app/src/main/java/org/audiveris/omr/score/Score.java b/app/src/main/java/org/audiveris/omr/score/Score.java index 5e90c132e..d0b49495d 100644 --- a/app/src/main/java/org/audiveris/omr/score/Score.java +++ b/app/src/main/java/org/audiveris/omr/score/Score.java @@ -29,7 +29,6 @@ import org.audiveris.omr.sheet.SheetStub; import org.audiveris.omr.util.Jaxb; import org.audiveris.omr.util.Navigable; -import org.audiveris.omr.util.param.Param; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -129,9 +128,6 @@ public class Score @Navigable(false) private Book book; - /** Handling of tempo parameter. */ - private final Param tempoParam; - /** The specified sound volume, if any. */ private Integer volume; @@ -145,8 +141,6 @@ public class Score */ public Score () { - tempoParam = new Param<>(this); - tempoParam.setParent(Tempo.defaultTempo); } //~ Methods ------------------------------------------------------------------------------------ @@ -725,19 +719,6 @@ public List getStubs () return pageStubs; } - //----------------// - // getTempoParam // - //---------------// - /** - * Report the tempo parameter. - * - * @return tempo information - */ - public Param getTempoParam () - { - return tempoParam; - } - //-----------// // getVolume // //-----------// @@ -998,19 +979,6 @@ public static int getDefaultVolume () return constants.defaultVolume.getValue(); } - //-----------------// - // setDefaultTempo // - //-----------------// - /** - * Assign default value for Midi tempo. - * - * @param tempo the default tempo value - */ - public static void setDefaultTempo (int tempo) - { - constants.defaultTempo.setValue(tempo); - } - //------------------// // setDefaultVolume // //------------------// @@ -1032,15 +1000,7 @@ public static void setDefaultVolume (int volume) private static class Constants extends ConstantSet { - - private final Constant.Integer defaultTempo = new Constant.Integer( - "QuartersPerMn", - 120, - "Default tempo, stated in number of quarters per minute"); - - private final Constant.Integer defaultVolume = new Constant.Integer( - "Volume", - 78, - "Default Volume in 0..127 range"); + private final Constant.Integer defaultVolume = + new Constant.Integer("Volume", 78, "Default Volume in 0..127 range"); } } diff --git a/app/src/main/java/org/audiveris/omr/score/StaffConfig.java b/app/src/main/java/org/audiveris/omr/score/StaffConfig.java index 3f5da32f0..0f379ff9b 100644 --- a/app/src/main/java/org/audiveris/omr/score/StaffConfig.java +++ b/app/src/main/java/org/audiveris/omr/score/StaffConfig.java @@ -167,9 +167,8 @@ public static List decodeCsv (String csv) */ public static String toCsvString (Collection collection) { - return new StringBuilder().append( - collection.stream() // - .map(sc -> (sc == null) ? "null" : sc.toString()) // - .collect(Collectors.joining(","))).toString(); + return collection.stream() // + .map(sc -> (sc == null) ? "null" : sc.toString()) // + .collect(Collectors.joining(",")); } } diff --git a/app/src/main/java/org/audiveris/omr/score/Tempo.java b/app/src/main/java/org/audiveris/omr/score/Tempo.java deleted file mode 100644 index 4d83bf18e..000000000 --- a/app/src/main/java/org/audiveris/omr/score/Tempo.java +++ /dev/null @@ -1,116 +0,0 @@ -//------------------------------------------------------------------------------------------------// -// // -// T e m p o // -// // -//------------------------------------------------------------------------------------------------// -// -// -// Copyright © Audiveris 2024. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify it under the terms of the -// GNU Affero General Public License as published by the Free Software Foundation, either version -// 3 of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with this -// program. If not, see . -//------------------------------------------------------------------------------------------------// -// -package org.audiveris.omr.score; - -import org.audiveris.omr.constant.Constant; -import org.audiveris.omr.constant.ConstantSet; -import org.audiveris.omr.util.param.Param; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Class Tempo handles the default tempo value. - * - * @author Hervé Bitteur - */ -public abstract class Tempo -{ - //~ Static fields/initializers ----------------------------------------------------------------- - - private static final Constants constants = new Constants(); - - private static final Logger logger = LoggerFactory.getLogger(Tempo.class); - - /** Default parameter. */ - public static final Param defaultTempo = new Default(); - - //~ Constructors ------------------------------------------------------------------------------- - - /** Not meant to be instantiated. */ - private Tempo () - { - } - - //~ Inner Classes ------------------------------------------------------------------------------ - - //-----------// - // Constants // - //-----------// - private static class Constants - extends ConstantSet - { - - private final Constant.Integer defaultTempo = new Constant.Integer( - "QuartersPerMn", - 120, - "Default tempo, stated in number of quarters per minute"); - } - - //---------// - // Default // - //---------// - private static class Default - extends Param - { - - public Default () - { - super(GLOBAL_SCOPE); - } - - @Override - public Integer getSpecific () - { - if (constants.defaultTempo.isSourceValue()) { - return null; - } else { - return constants.defaultTempo.getValue(); - } - } - - @Override - public Integer getValue () - { - return constants.defaultTempo.getValue(); - } - - @Override - public boolean isSpecific () - { - return !constants.defaultTempo.isSourceValue(); - } - - @Override - public boolean setSpecific (Integer specific) - { - if (!getValue().equals(specific)) { - constants.defaultTempo.setValue(specific); - logger.info("Default tempo is now {}", specific); - - return true; - } else { - return false; - } - } - } -} diff --git a/app/src/main/java/org/audiveris/omr/sheet/Book.java b/app/src/main/java/org/audiveris/omr/sheet/Book.java index 924ccc7ae..7e67344b9 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/Book.java +++ b/app/src/main/java/org/audiveris/omr/sheet/Book.java @@ -392,8 +392,7 @@ public void annotate (List theStubs) if (root != null) { try { root.getFileSystem().close(); - } catch (IOException ignored) { - } + } catch (IOException ignored) {} } } } @@ -1794,7 +1793,7 @@ public synchronized BufferedImage loadSheetImage (int id) { try { if (!Files.exists(path)) { - logger.warn("Book input {} not found", path); + logger.info("Book input {} not found", path); return null; } @@ -2031,8 +2030,7 @@ public boolean reachBookStep (final OmrStep target, LogUtil.start(stub); try { - if (stub.reachStep(target, force)) { - } else { + if (stub.reachStep(target, force)) {} else { someFailure = true; } } catch (StepPause ex) { @@ -2533,8 +2531,7 @@ public void store (Path bookPath, if (root != null) { try { root.getFileSystem().close(); - } catch (IOException ignored) { - } + } catch (IOException ignored) {} } getLock().unlock(); diff --git a/app/src/main/java/org/audiveris/omr/sheet/PageCleaner.java b/app/src/main/java/org/audiveris/omr/sheet/PageCleaner.java index db332b75e..5a6a2b780 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/PageCleaner.java +++ b/app/src/main/java/org/audiveris/omr/sheet/PageCleaner.java @@ -33,6 +33,7 @@ import org.audiveris.omr.sig.inter.ArpeggiatoInter; import org.audiveris.omr.sig.inter.BarConnectorInter; import org.audiveris.omr.sig.inter.BarlineInter; +import org.audiveris.omr.sig.inter.BeatUnitInter; import org.audiveris.omr.sig.inter.BraceInter; import org.audiveris.omr.sig.inter.BracketConnectorInter; import org.audiveris.omr.sig.inter.BracketInter; @@ -385,6 +386,12 @@ public void visit (BarlineInter inter) // No BeamGroup + @Override + public void visit (BeatUnitInter inter) + { + processGlyph(inter.getGlyph()); + } + @Override public void visit (BraceInter inter) { diff --git a/app/src/main/java/org/audiveris/omr/sheet/clef/ClefBuilder.java b/app/src/main/java/org/audiveris/omr/sheet/clef/ClefBuilder.java index fa12bfb29..39b75f9fc 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/clef/ClefBuilder.java +++ b/app/src/main/java/org/audiveris/omr/sheet/clef/ClefBuilder.java @@ -581,7 +581,7 @@ public void evaluateGlyph (Glyph glyph, ClefInter bestInter = bestMap.get(kind); if ((bestInter == null) || (bestInter.getGrade() < grade)) { - bestMap.put(kind, ClefInter.create(glyph, shape, grade, staff)); + bestMap.put(kind, ClefInter.createValid(glyph, shape, grade, staff)); } } } diff --git a/app/src/main/java/org/audiveris/omr/sheet/curve/CircleModel.java b/app/src/main/java/org/audiveris/omr/sheet/curve/CircleModel.java index 998a15989..7d05b7f25 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/curve/CircleModel.java +++ b/app/src/main/java/org/audiveris/omr/sheet/curve/CircleModel.java @@ -171,9 +171,9 @@ public void setDistance (double dist) * @param last last point * @return the CircleModel instance if OK, null otherwise */ - public static CircleModel create (Point2D first, - Point2D middle, - Point2D last) + public static CircleModel createValid (Point2D first, + Point2D middle, + Point2D last) { CircleModel model = new CircleModel(first, middle, last); diff --git a/app/src/main/java/org/audiveris/omr/sheet/curve/SlurInfo.java b/app/src/main/java/org/audiveris/omr/sheet/curve/SlurInfo.java index 818fdaf85..bc170d8e9 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/curve/SlurInfo.java +++ b/app/src/main/java/org/audiveris/omr/sheet/curve/SlurInfo.java @@ -144,7 +144,7 @@ public Model computeSideModel (List points, Point p2 = points.get(np - 1); // Choose a circle-model, otherwise a line-model - CircleModel rough = CircleModel.create(p0, p1, p2); + CircleModel rough = CircleModel.createValid(p0, p1, p2); if (rough != null) { return rough; diff --git a/app/src/main/java/org/audiveris/omr/sheet/rhythm/MeasureStack.java b/app/src/main/java/org/audiveris/omr/sheet/rhythm/MeasureStack.java index 7d62ad997..ca2d8451f 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/rhythm/MeasureStack.java +++ b/app/src/main/java/org/audiveris/omr/sheet/rhythm/MeasureStack.java @@ -752,22 +752,26 @@ public AbstractTimeInter getCurrentTimeSignature () /** * Retrieve the most suitable chord to connect the event point to. * - * @param point the system-based location - * @param xRange required abscissa range, or null + * @param point the system-based location + * @param area required abscissa range (excluding ordinate range), or null + * @param tryAbove should we first lookup at the staff above the provided point * @return the most suitable chord, or null */ public AbstractChordInter getEventChord (Point2D point, - Rectangle xRange) + Rectangle area, + boolean tryAbove) { - // First, try staff just above - AbstractChordInter above = getStandardChordAbove(point, xRange); + if (tryAbove) { + // First, try staff just above + AbstractChordInter above = getStandardChordAbove(point, area); - if (above != null) { - return above; + if (above != null) { + return above; + } } - // Second, try staff just below - return getStandardChordBelow(point, xRange); + // Try below + return getStandardChordBelow(point, area); } //-----------// @@ -1186,14 +1190,14 @@ public Rational getSlotsDuration () /** * Retrieve the closest chord (head or rest) within staff above. * - * @param point the system-based location - * @param xRange required abscissa range, or null + * @param point the system-based location + * @param area required abscissa range (excluding ordinate range), or null * @return the most suitable chord, or null */ public AbstractChordInter getStandardChordAbove (Point2D point, - Rectangle xRange) + Rectangle area) { - Collection aboves = getStandardChordsAbove(point, xRange); + Collection aboves = getStandardChordsAbove(point, area); if (!aboves.isEmpty()) { return getClosestChord(aboves, point); @@ -1208,14 +1212,14 @@ public AbstractChordInter getStandardChordAbove (Point2D point, /** * Retrieve the closest chord (head or rest) within staff below. * - * @param point the system-based location - * @param xRange required abscissa range, or null + * @param point the system-based location + * @param area required abscissa range (excluding ordinate range), or null * @return the most suitable chord, or null */ public AbstractChordInter getStandardChordBelow (Point2D point, - Rectangle xRange) + Rectangle area) { - Collection belows = getStandardChordsBelow(point, xRange); + Collection belows = getStandardChordsBelow(point, area); if (!belows.isEmpty()) { return getClosestChord(belows, point); @@ -1250,12 +1254,12 @@ public Set getStandardChords () * Report the set of standard chords whose 'head' is located in the staff above the * provided point. * - * @param point the provided point - * @param xRange required abscissa range, or null + * @param point the provided point + * @param area required abscissa range (excluding ordinate range), or null * @return the (perhaps empty) set of chords */ public Set getStandardChordsAbove (Point2D point, - Rectangle xRange) + Rectangle area) { Staff desiredStaff = getSystem().getStaffAtOrAbove(point); Set found = new LinkedHashSet<>(); @@ -1264,8 +1268,11 @@ public Set getStandardChordsAbove (Point2D point, if (measure != null) { for (AbstractChordInter chord : measure.getStandardChords()) { if (chord.getBottomStaff() == desiredStaff) { - if ((xRange == null) || (GeoUtil.xOverlap(chord.getBounds(), xRange) > 0)) { - Point head = chord.getHeadLocation(); + final Rectangle chordBounds = chord.getBounds(); + if ((area == null) || // + ((GeoUtil.xOverlap(chordBounds, area) > 0) && // + (GeoUtil.yOverlap(chordBounds, area) < 0))) { + final Point head = chord.getHeadLocation(); if ((head != null) && (head.y < point.getY())) { found.add(chord); @@ -1285,22 +1292,27 @@ public Set getStandardChordsAbove (Point2D point, * Report the set of standard chords whose 'head' is located in the staff below the * provided point. * - * @param point the provided point - * @param xRange required abscissa range, or null + * @param point the provided point + * @param area required abscissa range (excluding ordinate range), or null * @return the (perhaps empty) collection of chords */ public Set getStandardChordsBelow (Point2D point, - Rectangle xRange) + Rectangle area) { - Staff desiredStaff = getSystem().getStaffAtOrBelow(point); - Set found = new LinkedHashSet<>(); - Measure measure = getMeasureAt(desiredStaff); + final Staff desiredStaff = getSystem().getStaffAtOrBelow(point); + final Set found = new LinkedHashSet<>(); + final Measure measure = getMeasureAt(desiredStaff); if (measure != null) { for (AbstractChordInter chord : measure.getStandardChords()) { if (chord.getTopStaff() == desiredStaff) { - if ((xRange == null) || (GeoUtil.xOverlap(chord.getBounds(), xRange) > 0)) { - Point head = chord.getHeadLocation(); + final Rectangle chordBounds = chord.getBounds(); + + if ((area == null) || // + ((GeoUtil.xOverlap(chordBounds, area) > 0) && (GeoUtil.yOverlap( + chordBounds, + area) < 0))) { + final Point head = chord.getHeadLocation(); if ((head != null) && (head.y > point.getY())) { found.add(chord); diff --git a/app/src/main/java/org/audiveris/omr/sheet/symbol/InterFactory.java b/app/src/main/java/org/audiveris/omr/sheet/symbol/InterFactory.java index 6946aa183..b6be0ae19 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/symbol/InterFactory.java +++ b/app/src/main/java/org/audiveris/omr/sheet/symbol/InterFactory.java @@ -45,6 +45,7 @@ import org.audiveris.omr.sig.inter.BarlineInter; import org.audiveris.omr.sig.inter.BeamHookInter; import org.audiveris.omr.sig.inter.BeamInter; +import org.audiveris.omr.sig.inter.BeatUnitInter; import org.audiveris.omr.sig.inter.BraceInter; import org.audiveris.omr.sig.inter.BracketInter; import org.audiveris.omr.sig.inter.BreathMarkInter; @@ -69,6 +70,7 @@ import org.audiveris.omr.sig.inter.LyricItemInter; import org.audiveris.omr.sig.inter.MarkerInter; import org.audiveris.omr.sig.inter.MeasureRepeatInter; +import org.audiveris.omr.sig.inter.MetronomeInter; import org.audiveris.omr.sig.inter.MultipleRestInter; import org.audiveris.omr.sig.inter.NumberInter; import org.audiveris.omr.sig.inter.OctaveShiftInter; @@ -288,7 +290,7 @@ private Inter doCreate (Evaluation eval, case F_CLEF_8VB: case C_CLEF: case PERCUSSION_CLEF: - return ClefInter.create(glyph, shape, grade, closestStaff); // Staff is OK + return ClefInter.createValid(glyph, shape, grade, closestStaff); // Staff is OK // Key signatures case KEY_FLAT_7: @@ -670,11 +672,10 @@ private void handleComplexDynamics () private void handleTimes () { // Retrieve all time inters (outside staff headers) - final List systemTimes = sig.inters(new Class[] - { - TimeWholeInter.class, // Whole symbol like C or predefined 6/8 - TimeCustomInter.class, // User modifiable combo 6/8 - TimeNumberInter.class }); // Partial symbol like 6 or 8 + final List systemTimes = sig.inters( + new Class[] { TimeWholeInter.class, // Whole symbol like C or predefined 6/8 + TimeCustomInter.class, // User modifiable combo 6/8 + TimeNumberInter.class }); // Partial symbol like 6 or 8 final List headerTimes = new ArrayList<>(); @@ -825,11 +826,7 @@ private static Inter doCreateManual (Shape shape, // Barlines case THIN_BARLINE: case THICK_BARLINE: - // if (sheet.getStub().getLatestStep().compareTo(OmrStep.MEASURES) < 0) { - // return new BarlineInter(null, shape, GRADE, null, null); - // } else { return new StaffBarlineInter(shape, GRADE); - // } case DOUBLE_BARLINE: case FINAL_BARLINE: @@ -887,6 +884,9 @@ private static Inter doCreateManual (Shape shape, case TEXT: return new WordInter(shape, GRADE); + case METRONOME: + return new MetronomeInter(GRADE); + // Clefs case G_CLEF: case G_CLEF_SMALL: @@ -1091,6 +1091,18 @@ private static Inter doCreateManual (Shape shape, case GRACE_NOTE_SLASH_DOWN: return new GraceChordInter(null, shape, GRADE); + // Metronome units + case METRO_WHOLE: + case METRO_HALF: + case METRO_QUARTER: + case METRO_EIGHTH: + case METRO_SIXTEENTH: + case METRO_DOTTED_HALF: + case METRO_DOTTED_QUARTER: + case METRO_DOTTED_EIGHTH: + case METRO_DOTTED_SIXTEENTH: + return new BeatUnitInter(shape, GRADE); + // Ornaments case TR: case TURN: diff --git a/app/src/main/java/org/audiveris/omr/sheet/symbol/LinksStep.java b/app/src/main/java/org/audiveris/omr/sheet/symbol/LinksStep.java index fe70591b9..af9b6b107 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/symbol/LinksStep.java +++ b/app/src/main/java/org/audiveris/omr/sheet/symbol/LinksStep.java @@ -164,29 +164,30 @@ public void impact (UITaskList seq, for (UITask task : seq.getTasks()) { if (task instanceof InterTask interTask) { - Inter inter = interTask.getInter(); - SystemInfo system = inter.getSig().getSystem(); - Class interClass = inter.getClass(); + final Inter inter = interTask.getInter(); - if (isImpactedBy(interClass, forTexts)) { - if (inter instanceof LyricItemInter item) { + if (isImpactedBy(inter.getClass(), forTexts)) { + final SystemInfo system = inter.getSig().getSystem(); + + switch (inter) { + case LyricItemInter item -> { if ((opKind != OpKind.UNDO) && task instanceof AdditionTask) { final int profile = Math.max(item.getProfile(), system.getProfile()); item.mapToChord(profile); } - } else if (inter instanceof SentenceInter sentence) { - SymbolsLinker linker = new SymbolsLinker(system); - + } + case SentenceInter sentence -> { if ((opKind != OpKind.UNDO) && task instanceof AdditionTask) { - linker.linkOneSentence(sentence); + sentence.link(system); } else if (task instanceof SentenceRoleTask roleTask) { - linker.unlinkOneSentence( - sentence, - (opKind == OpKind.UNDO) ? roleTask.getNewRole() - : roleTask.getOldRole()); - linker.linkOneSentence(sentence); + sentence.unlink((opKind == OpKind.UNDO) // + ? roleTask.getNewRole() + : roleTask.getOldRole()); + sentence.link(system); } } + default -> {} + } } } } diff --git a/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsFilter.java b/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsFilter.java index 4f12c922d..aaebad43b 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsFilter.java +++ b/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsFilter.java @@ -215,30 +215,25 @@ private static class Constants extends ConstantSet { - private final Constant.Boolean displaySymbols = new Constant.Boolean( - false, - "Should we display the symbols image?"); + private final Constant.Boolean displaySymbols = + new Constant.Boolean(false, "Should we display the symbols image?"); - private final Constant.Boolean saveSymbolsBuffer = new Constant.Boolean( - false, - "Should we save symbols image on disk?"); + private final Constant.Boolean saveSymbolsBuffer = + new Constant.Boolean(false, "Should we save symbols image on disk?"); - private final Scale.Fraction staffVerticalMargin = new Scale.Fraction( - 0.5, - "Margin erased above & below staff header area"); + private final Scale.Fraction staffVerticalMargin = + new Scale.Fraction(0.5, "Margin erased above & below staff header area"); private final Constant.Integer maxSymbolLength = new Constant.Integer( "letter count", 3, "Maximum number of chars for a word to be checked as a symbol"); - private final Constant.Ratio minHeadContextualGrade = new Constant.Ratio( - 0.6, - "Minimum contextual grade to hide a head"); + private final Constant.Ratio minHeadContextualGrade = + new Constant.Ratio(0.6, "Minimum contextual grade to hide a head"); - private final Constant.Ratio minStemContextualGrade = new Constant.Ratio( - 0.7, - "Minimum contextual grade to hide a stem"); + private final Constant.Ratio minStemContextualGrade = + new Constant.Ratio(0.7, "Minimum contextual grade to hide a stem"); } //----------------// @@ -342,6 +337,11 @@ public void eraseInters (Map> weaksMap) continue; } + if (inter.isFrozen()) { + strongs.add(inter); + continue; + } + // Check short words if (inter instanceof WordInter word) { if (word.getValue().length() <= maxSymbolLength) { @@ -414,10 +414,8 @@ protected void processGlyph (Glyph glyph) // Save the glyph? if (systemWeaks != null) { // The glyph may be made of several parts, so it's safer to restart from pixels - List glyphs = GlyphFactory.buildGlyphs( - glyph.getRunTable(), - glyph.getTopLeft(), - GlyphGroup.SYMBOL); + List glyphs = GlyphFactory + .buildGlyphs(glyph.getRunTable(), glyph.getTopLeft(), GlyphGroup.SYMBOL); systemWeaks.addAll(glyphs); } } @@ -444,10 +442,8 @@ private void savePixels (Rectangle box, final RunTable runTable = factory.createTable(buf); // Glyphs - final List glyphs = GlyphFactory.buildGlyphs( - runTable, - new Point(0, 0), - GlyphGroup.SYMBOL); + final List glyphs = + GlyphFactory.buildGlyphs(runTable, new Point(0, 0), GlyphGroup.SYMBOL); systemWeaks.addAll(glyphs); } diff --git a/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsLinker.java b/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsLinker.java index 7c3d38543..5197b7f10 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsLinker.java +++ b/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsLinker.java @@ -21,22 +21,17 @@ // package org.audiveris.omr.sheet.symbol; -import org.audiveris.omr.sheet.Part; -import org.audiveris.omr.sheet.Scale; -import org.audiveris.omr.sheet.Staff; import org.audiveris.omr.sheet.SystemInfo; import org.audiveris.omr.sheet.rhythm.MeasureStack; import org.audiveris.omr.sheet.rhythm.TupletsBuilder; import org.audiveris.omr.sig.SIGraph; import org.audiveris.omr.sig.inter.AbstractChordInter; import org.audiveris.omr.sig.inter.BeamGroupInter; -import org.audiveris.omr.sig.inter.ChordNameInter; import org.audiveris.omr.sig.inter.DynamicsInter; import org.audiveris.omr.sig.inter.FermataInter; import org.audiveris.omr.sig.inter.HeadChordInter; import org.audiveris.omr.sig.inter.HeadInter; import org.audiveris.omr.sig.inter.Inter; -import org.audiveris.omr.sig.inter.LyricItemInter; import org.audiveris.omr.sig.inter.NumberInter; import org.audiveris.omr.sig.inter.OctaveShiftInter; import org.audiveris.omr.sig.inter.PedalInter; @@ -45,21 +40,14 @@ import org.audiveris.omr.sig.inter.SmallChordInter; import org.audiveris.omr.sig.inter.WedgeInter; import org.audiveris.omr.sig.relation.ChordGraceRelation; -import org.audiveris.omr.sig.relation.ChordNameRelation; -import org.audiveris.omr.sig.relation.ChordSentenceRelation; -import org.audiveris.omr.sig.relation.ChordSyllableRelation; -import org.audiveris.omr.sig.relation.EndingSentenceRelation; import org.audiveris.omr.sig.relation.Link; import org.audiveris.omr.sig.relation.Relation; import org.audiveris.omr.sig.relation.SlurHeadRelation; -import org.audiveris.omr.text.TextRole; import org.audiveris.omr.util.HorizontalSide; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.awt.Rectangle; -import java.awt.geom.Point2D; import java.util.Collection; import java.util.List; @@ -274,107 +262,6 @@ private void linkOctaveShifts () } } - //-----------------// - // linkOneSentence // - //-----------------// - /** - * Link a text sentence, according to its role, with its related entity if any. - * - * @param sentence the sentence to link - */ - public void linkOneSentence (SentenceInter sentence) - { - try { - if (sentence.isVip()) { - logger.info("VIP linkOneSentence for {}", sentence); - } - - final TextRole role = sentence.getRole(); - - if (role == null) { - logger.info("No role for {}", sentence); - - return; - } - - final Point2D location = sentence.getLocation(); - final Rectangle bounds = sentence.getBounds(); - final Scale scale = system.getSheet().getScale(); - - switch (role) { - case Lyrics -> { - // Map each syllable with proper chord, in assigned staff - for (Inter wInter : sentence.getMembers()) { - final LyricItemInter item = (LyricItemInter) wInter; - final int profile = Math.max(item.getProfile(), system.getProfile()); - item.mapToChord(profile); - } - } - - case Direction, Tempo -> { - // Map direction with proper chord - MeasureStack stack = system.getStackAt(location); - - if (stack == null) { - logger.info( - "No measure stack for direction {} {}", - sentence, - sentence.getValue()); - } else { - int xGapMax = scale.toPixels(ChordSentenceRelation.getXGapMax()); - Rectangle fatBounds = new Rectangle(bounds); - fatBounds.grow(xGapMax, 0); - - AbstractChordInter chord = stack.getEventChord(location, fatBounds); - - if (chord != null) { - sig.addEdge(chord, sentence, new ChordSentenceRelation()); - } else { - logger.info( - "No chord near direction {} {}", - sentence, - sentence.getValue()); - } - } - } - - case PartName -> { - // Assign part name to proper part - Staff staff = system.getClosestStaff(sentence.getCenter()); - Part part = staff.getPart(); - part.setName(sentence); - } - - case ChordName -> { - // Map each word with proper chord, in assigned staff - for (Inter wInter : sentence.getMembers()) { - final ChordNameInter word = (ChordNameInter) wInter; - final Link link = word.lookupLink(system); - - if (link == null) { - logger.info("No chord below {}", word); - } else { - link.applyTo(wInter); - } - } - } - - case EndingNumber, EndingText -> { - // Look for related ending - final Link link = sentence.lookupEndingLink(system); - - if ((link != null) && (null == sig - .getRelation(link.partner, sentence, EndingSentenceRelation.class))) { - sig.addEdge(link.partner, sentence, link.relation); - } - } - } - // Roles UnknownRole, Title, Number, Creator*, Rights stand by themselves - } catch (Exception ex) { - logger.warn("Error in linkOneSentence for {} {}", sentence, ex.toString(), ex); - } - } - //------------// // linkPedals // //------------// @@ -409,14 +296,13 @@ private void linkPedals () // linkTexts // //-----------// /** - * Link text interpretations, according to their role, with their related entity if - * any. + * Link text interpretations, according to their role, with their related entity if any. */ private void linkTexts () { for (Inter sInter : sig.inters(SentenceInter.class)) { final SentenceInter sentence = (SentenceInter) sInter; - linkOneSentence(sentence); + sentence.link(system); } } @@ -478,53 +364,4 @@ public void process () linkOctaveShifts(); linkNumbers(); } - - //-------------------// - // unlinkOneSentence // - //-------------------// - /** - * Unlink a text sentence, according to its role, with its related entity if any. - * - * @param sentence the sentence to unlink - * @param oldRole the role this sentence had - */ - public void unlinkOneSentence (SentenceInter sentence, - TextRole oldRole) - { - try { - if (sentence.isVip()) { - logger.info("VIP unlinkOneSentence for {}", sentence); - } - - switch (oldRole) { - case null -> logger.info("Null old role for {}", sentence); - default -> {} - - case Lyrics -> sentence.getMembers().forEach( - wInter -> sig.getRelations(wInter, ChordSyllableRelation.class) - .forEach(rel -> sig.removeEdge(rel))); - - case Direction -> sig.getRelations(sentence, ChordSentenceRelation.class) - .forEach(rel -> sig.removeEdge(rel)); - - case PartName -> { - // Look for proper part - final Staff staff = system.getClosestStaff(sentence.getCenter()); - final Part part = staff.getPart(); - part.setName((SentenceInter) null); - } - - case ChordName -> sentence.getMembers().forEach( - wInter -> sig.getRelations(wInter, ChordNameRelation.class) - .forEach(rel -> sig.removeEdge(rel))); - - case EndingNumber, EndingText -> // - sig.getRelations(sentence, EndingSentenceRelation.class) - .forEach(rel -> sig.removeEdge(rel)); - - } - } catch (Exception ex) { - logger.warn("Error in unlinkOneSentence for {} {}", sentence, ex.toString(), ex); - } - } } diff --git a/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsStep.java b/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsStep.java index 45383e04c..d06b66bfd 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsStep.java +++ b/app/src/main/java/org/audiveris/omr/sheet/symbol/SymbolsStep.java @@ -77,7 +77,7 @@ public void displayUI (OmrStep step, { sheet.getSheetEditor().refresh(); - // Update glyph board if needed (to see OCR'ed data) + // Update glyph board if needed (to see OCR'd data) final SelectionService service = sheet.getGlyphIndex().getEntityService(); @SuppressWarnings("unchecked") diff --git a/app/src/main/java/org/audiveris/omr/sheet/ui/BookActions.java b/app/src/main/java/org/audiveris/omr/sheet/ui/BookActions.java index 635182cfe..557e71a52 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/ui/BookActions.java +++ b/app/src/main/java/org/audiveris/omr/sheet/ui/BookActions.java @@ -409,8 +409,7 @@ public void defineSheetScaling (ActionEvent e) try { // TODO: Is there a more civilized way? optionPane.setValue(JOptionPane.UNINITIALIZED_VALUE); - } catch (Exception ignored) { - } + } catch (Exception ignored) {} } } }); @@ -590,7 +589,7 @@ public void dumpBook (ActionEvent e) // dumpEventServices // //-------------------// /** - * Action to erase the dump the content of all event services + * Action to dump the content of all event services * * @param e the event which triggered this action */ @@ -1702,8 +1701,7 @@ public void windowClosing (WindowEvent we) JOptionPane.PLAIN_MESSAGE, JOptionPane.DEFAULT_OPTION, null, - new Object[] - { UserOpt.OK, UserOpt.Apply, UserOpt.Cancel }); + new Object[] { UserOpt.OK, UserOpt.Apply, UserOpt.Cancel }); optionPane.addPropertyChangeListener(e -> { if (dialog.isVisible() && (e.getSource() == optionPane) && (e.getPropertyName() .equals(JOptionPane.VALUE_PROPERTY))) { @@ -1847,8 +1845,7 @@ public static boolean checkStored (Book book) //--------// public static OmrFileFilter filter (String ext) { - return new OmrFileFilter(ext, new String[] - { ext }); + return new OmrFileFilter(ext, new String[] { ext }); } //-------------// diff --git a/app/src/main/java/org/audiveris/omr/sheet/ui/SheetPainter.java b/app/src/main/java/org/audiveris/omr/sheet/ui/SheetPainter.java index bf1e32daf..871ed629b 100644 --- a/app/src/main/java/org/audiveris/omr/sheet/ui/SheetPainter.java +++ b/app/src/main/java/org/audiveris/omr/sheet/ui/SheetPainter.java @@ -50,6 +50,7 @@ import org.audiveris.omr.sig.inter.AugmentationDotInter; import org.audiveris.omr.sig.inter.BarConnectorInter; import org.audiveris.omr.sig.inter.BarlineInter; +import org.audiveris.omr.sig.inter.BeatUnitInter; import org.audiveris.omr.sig.inter.BraceInter; import org.audiveris.omr.sig.inter.BracketConnectorInter; import org.audiveris.omr.sig.inter.BracketInter; @@ -62,7 +63,9 @@ import org.audiveris.omr.sig.inter.KeyInter; import org.audiveris.omr.sig.inter.LedgerInter; import org.audiveris.omr.sig.inter.LyricLineInter; +import org.audiveris.omr.sig.inter.MetronomeInter; import org.audiveris.omr.sig.inter.MultipleRestInter; +import org.audiveris.omr.sig.inter.MusicWordInter; import org.audiveris.omr.sig.inter.OctaveShiftInter; import org.audiveris.omr.sig.inter.RestInter; import org.audiveris.omr.sig.inter.SentenceInter; @@ -223,6 +226,10 @@ public SheetPainter (Sheet sheet, this.withJumbos = withJumbos; clip = g.getClipBounds(); + + // To avoid the display being slightly clipped near a window border + final int margin = scale.toPixels(constants.clipMargin); + clip.grow(margin, margin); } //~ Methods ------------------------------------------------------------------------------------ @@ -476,7 +483,7 @@ protected void processSystem (SystemInfo system) // All interpretations for this system sigPainter.process(system.getSig()); - } catch (ConcurrentModificationException ignored) { + } catch (ConcurrentModificationException ignored) { // } catch (Exception ex) { logger.warn("Cannot paint system#{}", system.getId(), ex); } @@ -645,6 +652,10 @@ private static class Constants private final Constant.Boolean jumboColored = new Constant.Boolean( true, "Should the jumbo items be colored specifically?"); + + private final Scale.Fraction clipMargin = new Scale.Fraction( + 4.0, + "Margin added to clip bounds to avoid truncation"); } //-----------// @@ -1022,12 +1033,17 @@ protected void paintWord (WordInter word, return; } - FontRenderContext frc = g.getFontRenderContext(); - Font font = TextFont.create(textFont, fontInfo); - TextLayout layout = new TextLayout(word.getValue(), font, frc); setColor(word); - paint(layout, word.getLocation(), BASELINE_LEFT); + if (word instanceof MusicWordInter) { + final MusicFont mf = musicFont.deriveFont((float) fontInfo.pointsize); + final TextLayout layout = mf.layout(word.getValue()); + paint(layout, word.getCenter(), AREA_CENTER); + } else { + final TextFont tf = TextFont.create(textFont, fontInfo); + final TextLayout layout = tf.layout(word.getValue()); + paint(layout, word.getLocation(), BASELINE_LEFT); + } } //---------// @@ -1258,7 +1274,17 @@ public void visit (BarlineInter barline) g.fill(barline.getArea()); } - // No beam group + // No visit for beam group + + //-------// + // visit // + //-------// + @Override + public void visit (BeatUnitInter word) + { + final FontInfo fontInfo = word.getFontInfo(); + paintWord(word, fontInfo); + } //-------// // visit // @@ -1474,6 +1500,19 @@ public void visit (LedgerInter ledger) g.draw(ledger.getMedian()); } + //-------// + // visit // + //-------// + @Override + public void visit (MetronomeInter inter) + { + // Painted directky only when its member words are not yet created (case of a ghost) + // Otherwise, the member words are painted individually + if (inter.getId() == 0) { + visit((Inter) inter); + } + } + //-------// // visit // //-------// @@ -1561,12 +1600,15 @@ public void visit (RestInter rest) @Override public void visit (SentenceInter sentence) { - FontInfo lineMeanFont = sentence.getMeanFont(); - for (Inter member : sentence.getMembers()) { - WordInter word = (WordInter) member; - paintWord(word, lineMeanFont); - ///paintWord(word, word.getFontInfo()); - } + // final FontInfo lineMeanFont = sentence.getMeanFont(); + // for (Inter member : sentence.getMembers()) { + // WordInter word = (WordInter) member; + // + // if (!(word instanceof MusicWordInter)) { + // paintWord(word, lineMeanFont); + // } + // ///paintWord(word, word.getFontInfo()); + // } } //-------// @@ -1682,10 +1724,10 @@ public void visit (WordInter word) { // Usually, words are displayed via their containing sentence, using sentence mean font. // But in the specific case of a (temporarily) orphan word, we display the word as it is. - if ((word.getSig() == null) || (word.getEnsemble() == null)) { - FontInfo fontInfo = word.getFontInfo(); - paintWord(word, fontInfo); - } + // if ((word.getSig() == null) || (word.getEnsemble() == null)) { + FontInfo fontInfo = word.getFontInfo(); + paintWord(word, fontInfo); + // } } } } diff --git a/app/src/main/java/org/audiveris/omr/sig/SigValue.java b/app/src/main/java/org/audiveris/omr/sig/SigValue.java index 3f6d7760a..7482d478b 100644 --- a/app/src/main/java/org/audiveris/omr/sig/SigValue.java +++ b/app/src/main/java/org/audiveris/omr/sig/SigValue.java @@ -35,6 +35,7 @@ import org.audiveris.omr.sig.inter.BeamGroupInter; import org.audiveris.omr.sig.inter.BeamHookInter; import org.audiveris.omr.sig.inter.BeamInter; +import org.audiveris.omr.sig.inter.BeatUnitInter; import org.audiveris.omr.sig.inter.BraceInter; import org.audiveris.omr.sig.inter.BracketConnectorInter; import org.audiveris.omr.sig.inter.BracketInter; @@ -65,6 +66,7 @@ import org.audiveris.omr.sig.inter.MarkerInter; import org.audiveris.omr.sig.inter.MeasureCountInter; import org.audiveris.omr.sig.inter.MeasureRepeatInter; +import org.audiveris.omr.sig.inter.MetronomeInter; import org.audiveris.omr.sig.inter.MultipleRestInter; import org.audiveris.omr.sig.inter.NumberInter; import org.audiveris.omr.sig.inter.OctaveShiftInter; @@ -251,6 +253,8 @@ public class SigValue @XmlElementRef(type = SmallFlagInter.class), @XmlElementRef(type = StaffBarlineInter.class), @XmlElementRef(type = StemInter.class), + @XmlElementRef(type = MetronomeInter.class), + @XmlElementRef(type = BeatUnitInter.class), @XmlElementRef(type = TimeCustomInter.class), @XmlElementRef(type = TimeNumberInter.class), @XmlElementRef(type = TimePairInter.class), @@ -318,8 +322,8 @@ public void populateSig (SIGraph sig) @Override public String toString () { - return new StringBuilder("SigValue{").append("inters:").append(inters.size()).append( - " relations:").append(relations.size()).append('}').toString(); + return new StringBuilder("SigValue{").append("inters:").append(inters.size()) + .append(" relations:").append(relations.size()).append('}').toString(); } //~ Inner Classes ------------------------------------------------------------------------------ diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/AbstractInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/AbstractInter.java index 5693d5cc0..67be0c3fa 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/AbstractInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/AbstractInter.java @@ -435,10 +435,8 @@ public void freeze () frozen = true; // Freeze members if any - if (this instanceof InterEnsemble) { - InterEnsemble ens = (InterEnsemble) this; - - for (Inter member : ens.getMembers()) { + if (this instanceof InterEnsemble ensemble) { + for (Inter member : ensemble.getMembers()) { member.freeze(); } } @@ -1002,7 +1000,7 @@ protected String internals () sb.append(")"); if (staff != null) { - sb.append(" stf:").append(staff.getId()); + sb.append(" staff:").append(staff.getId()); } if (shape != null) { @@ -1150,9 +1148,8 @@ public boolean overlaps (Inter that) } for (Inter thisMember : members) { - if (thisMember.overlaps(that) && that.overlaps(thisMember) && sig.noSupport( - thisMember, - that)) { + if (thisMember.overlaps(that) && that.overlaps(thisMember) + && sig.noSupport(thisMember, that)) { return true; } } @@ -1213,8 +1210,8 @@ public List preAdd (WrappedBoolean cancel, { final SystemInfo system = staff.getSystem(); - return Arrays.asList( - new AdditionTask(system.getSig(), this, getBounds(), searchLinks(system))); + return Arrays + .asList(new AdditionTask(system.getSig(), this, getBounds(), searchLinks(system))); } //---------// diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/AbstractInterVisitor.java b/app/src/main/java/org/audiveris/omr/sig/inter/AbstractInterVisitor.java index 4ec8b1456..bc6362c6c 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/AbstractInterVisitor.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/AbstractInterVisitor.java @@ -88,6 +88,12 @@ public void visit (BeamGroupInter inter) { } + @Override + public void visit (BeatUnitInter inter) + { + visit((Inter) inter); + } + @Override public void visit (BraceInter inter) { @@ -149,6 +155,11 @@ public void visit (MultipleRestInter inter) { } + @Override + public void visit (MetronomeInter inter) + { + } + @Override public void visit (OctaveShiftInter inter) { diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/BeatUnitInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/BeatUnitInter.java new file mode 100644 index 000000000..7f2a9a30f --- /dev/null +++ b/app/src/main/java/org/audiveris/omr/sig/inter/BeatUnitInter.java @@ -0,0 +1,325 @@ +//------------------------------------------------------------------------------------------------// +// // +// B e a t U n i t I n t e r // +// // +//------------------------------------------------------------------------------------------------// +// +// +// Copyright © Audiveris 2023. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, either version +// 3 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with this +// program. If not, see . +//------------------------------------------------------------------------------------------------// +// +package org.audiveris.omr.sig.inter; + +import org.audiveris.omr.glyph.Glyph; +import org.audiveris.omr.glyph.Shape; +import org.audiveris.omr.math.Rational; +import org.audiveris.omr.sheet.Sheet; +import org.audiveris.omr.sig.ui.DefaultEditor; +import org.audiveris.omr.sig.ui.InterEditor; +import org.audiveris.omr.ui.symbol.MusicFamily; +import org.audiveris.omr.ui.symbol.MusicFont; +import org.audiveris.omr.ui.symbol.ShapeSymbol; +import org.audiveris.omr.ui.symbol.Symbols; +import org.audiveris.omr.util.StringUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; +import java.awt.Rectangle; +import java.util.Objects; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class BeatUnitInter is a word that represents the beat specification part in a + * metronome mark. + * + * @author Hervé Bitteur + */ +@XmlRootElement(name = "beat-unit") +@XmlAccessorType(XmlAccessType.NONE) +public class BeatUnitInter + extends MusicWordInter +{ + //~ Static fields/initializers ----------------------------------------------------------------- + + private static final Logger logger = LoggerFactory.getLogger(BeatUnitInter.class); + + //~ Instance fields ---------------------------------------------------------------------------- + + // Persistent data + //---------------- + + /** Unit symbol, perhaps dotted. */ + @XmlElement + private Note note; + + //~ Constructors ------------------------------------------------------------------------------- + + /** + * No-argument constructor meant for JAXB. + */ + @SuppressWarnings("unused") + private BeatUnitInter () + { + } + + /** + * Creates a new BeatUnitInter object meant for manual assignment. + * + * @param shape one of the METRO shapes + * @param grade the interpretation quality + */ + public BeatUnitInter (Shape shape, + Double grade) + { + super(grade, shape, Note.noteOf(shape).getString()); + note = Note.noteOf(shape); + } + + /** + * Creates a new BeatUnitInter object. + * + * @param glyph the underlying glyph + * @param bounds the precise object bounds + * @param grade the interpretation quality + * @param value the word content (music characters) + * @param musicFont the current music font + * @param note the metronome note type + * @param location the baseline location + */ + public BeatUnitInter (Glyph glyph, + Rectangle bounds, + Double grade, + String value, + MusicFont musicFont, + Note note, + Point location) + { + super(glyph, bounds, grade, note.toShape(), value, musicFont, location); + this.note = note; + } + + //~ Methods ------------------------------------------------------------------------------------ + + //--------// + // accept // + //--------// + @Override + public void accept (InterVisitor visitor) + { + visitor.visit(this); + } + + //------------// + // deriveFrom // + //------------// + @Override + public boolean deriveFrom (ShapeSymbol symbol, + Sheet sheet, + MusicFont font, + Point dropLocation) + { + //logger.info("BeatUnitInter.deriveFrom {}", symbol); + // MetronomeSymbol metroSymbol = (MetronomeSymbol) symbol; + // MetronomeInter.Model model = metroSymbol.getModel(font, dropLocation); + // setValue(model.value); + // fontInfo = model.fontInfo; + // location = new Point2D.Double(model.baseLoc.getX(), model.baseLoc.getY()); + // setBounds(null); + // + // return true; + + return false; + } + + //-----------// + // getEditor // + //-----------// + @Override + public InterEditor getEditor () + { + return new DefaultEditor(this); + } + + //---------// + // getNote // + //---------// + /** + * Report the note used as the beat unit. + * + * @return the unit note + */ + public Note getNote () + { + return note; + } + + //---------// + // setNote // + //---------// + public void setNote (Note note) + { + this.note = note; + } + + //----------------// + // getShapeString // + //----------------// + @Override + public String getShapeString () + { + return shape.toString(); + } + + //----------// + // setValue // + //----------// + /** + * Assign a new value and update note accordingly. + * + * @param value the new value + */ + @Override + public void setValue (String value) + { + super.setValue(value); + + note = Note.decode(value); + shape = note.toShape(); + } + + //~ Inner Classes ------------------------------------------------------------------------------ + + //------// + // Note // + //------// + /** Notes that can appear as beat units in a metronome mark. */ + public static enum Note + { + WHOLE(32), + HALF(16), + QUARTER(8), + EIGHTH(4), + SIXTEENTH(2), + DOTTED_HALF(24), + DOTTED_QUARTER(12), + DOTTED_EIGHTH(6), + DOTTED_SIXTEENTH(3); + + /** Duration, specified in 1/32. */ + private final int duration; + + Note (int duration) + { + this.duration = duration; + } + + public String getString () + { + final Shape shape = toShape(); + final Symbols symbols = MusicFamily.Bravura.getSymbols(); + + return MusicFont.getString(symbols.getCode(shape)); + } + + public boolean hasDot () + { + return switch (this) { + case WHOLE, HALF, QUARTER, EIGHTH, SIXTEENTH -> false; + case DOTTED_HALF, DOTTED_QUARTER, DOTTED_EIGHTH, DOTTED_SIXTEENTH -> true; + }; + } + + /** + * Report the note duration, expressed in quarters. + * + * @return A rational number representing the quarter-based duration + */ + public Rational quarterValue () + { + return new Rational(duration, QUARTER.duration); + } + + public Shape toShape () + { + return switch (this) { + case WHOLE -> Shape.METRO_WHOLE; + case HALF -> Shape.METRO_HALF; + case QUARTER -> Shape.METRO_QUARTER; + case EIGHTH -> Shape.METRO_EIGHTH; + case SIXTEENTH -> Shape.METRO_SIXTEENTH; + case DOTTED_HALF -> Shape.METRO_DOTTED_HALF; + case DOTTED_QUARTER -> Shape.METRO_DOTTED_QUARTER; + case DOTTED_EIGHTH -> Shape.METRO_DOTTED_EIGHTH; + case DOTTED_SIXTEENTH -> Shape.METRO_DOTTED_SIXTEENTH; + }; + } + + public String toMusicXml () + { + // Reminder: the potential augmentation dot is handled separately in MusicXML + return switch (this) { + case WHOLE -> "whole"; + case HALF, DOTTED_HALF -> "half"; + case QUARTER, DOTTED_QUARTER -> "quarter"; + case EIGHTH, DOTTED_EIGHTH -> "eighth"; + case SIXTEENTH, DOTTED_SIXTEENTH -> "16th"; + }; + } + + /** + * Infer the note from the provided string codes. + * + * @param str the provided string + * @return the decoded note, or null + */ + public static Note decode (String str) + { + final String shrunk = StringUtil.shrink(str); + + for (Note note : Note.values()) { + final String noteStr = StringUtil.shrink(note.getString()); + + if (shrunk.equals(noteStr)) { + return note; + } + } + + return null; + } + + public static Note noteOf (Shape shape) + { + Objects.requireNonNull(shape, "Null shape value"); + + return switch (shape) { + case Shape.METRO_WHOLE -> Note.WHOLE; + case Shape.METRO_HALF -> Note.HALF; + case Shape.METRO_QUARTER -> Note.QUARTER; + case Shape.METRO_EIGHTH -> Note.EIGHTH; + case Shape.METRO_SIXTEENTH -> Note.SIXTEENTH; + case Shape.METRO_DOTTED_HALF -> Note.DOTTED_HALF; + case Shape.METRO_DOTTED_QUARTER -> Note.DOTTED_QUARTER; + case Shape.METRO_DOTTED_EIGHTH -> Note.DOTTED_EIGHTH; + case Shape.METRO_DOTTED_SIXTEENTH -> Note.DOTTED_SIXTEENTH; + default -> null; + }; + } + } +} diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/ChordNameInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/ChordNameInter.java index 5b37ca4f4..b28d87c73 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/ChordNameInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/ChordNameInter.java @@ -158,11 +158,11 @@ public class ChordNameInter private static final String STEP_CLASS = "[A-G]"; /** Pattern for root value. A, A# or Ab */ - private static final String rootPat = + private static final String rootPat = // group(ROOT_STEP, STEP_CLASS) + group(ROOT_ALTER, Alter.CLASS) + "?"; /** Pattern for bass value, if any. /A, /A# or /Ab */ - private static final String bassPat = + private static final String bassPat = // "(/" + group(BASS_STEP, STEP_CLASS) + group(BASS_ALTER, Alter.CLASS) + "?" + ")"; /** Pattern for major indication. M, maj or DELTA */ @@ -181,49 +181,49 @@ public class ChordNameInter private static final String hdimPat = group(HDIM, "\u00F8"); /** Pattern for any of the indication alternatives. (except sus) */ - private static final String modePat = + private static final String modePat = // "(" + majPat + "|" + minPat + "|" + augPat + "|" + dimPat + "|" + hdimPat + ")"; /** Pattern for (maj7) in min(maj7) = MAJOR_MINOR. */ - private static final String parMajPat = + private static final String parMajPat = // "(\\(" + group(PMAJ7, "(M|[Mm][Aa][Jj]|" + DELTA + ")7") + "\\))"; /** Pattern for any degree value. 5, 6, 7, 9, 11 or 13 */ private static final String DEG_CLASS = "(5|6|7|9|11|13)"; /** Pattern for a sequence of degrees. */ - private static final String degsPat = + private static final String degsPat = // group(DEGS, DEG_CLASS + "(" + Alter.CLASS + DEG_CLASS + ")?"); /** Pattern for a suspended indication. sus2 or sus4 */ private static final String susPat = group(SUS, "([Ss][Uu][Ss][24])"); /** Pattern for the whole kind value. */ - private static final String kindPat = + private static final String kindPat = // group(KIND, modePat + "?" + parMajPat + "?" + degsPat + "?" + susPat + "?"); /** Pattern for parenthesized degrees if any. (6), (#9), (#11b13) */ - private static final String parPat = "(\\(" + private static final String parPat = "(\\(" // + group(PARS, Alter.CLASS + "?" + DEG_CLASS + "(" + Alter.CLASS + DEG_CLASS + ")*") + "\\))"; /** Pattern for non-parenthesized degrees if any. b5 */ - private static final String noParPat = "(" + private static final String noParPat = "(" // + group(NO_PARS, Alter.CLASS + "?" + DEG_CLASS + "(" + Alter.CLASS + DEG_CLASS + ")*") + ")"; /** - * Un-compiled patterns for whole chord symbol. + * Non-compiled patterns for whole chord symbol. * TODO: add a pattern for functions */ - private static final String[] raws = new String[] + private static final String[] raws = new String[] // { rootPat + kindPat + "?" + "(" + parPat + "|" + noParPat + ")" + "?" + bassPat + "?" }; /** Compiled patterns for whole chord symbol. */ private static List patterns; /** Pattern for one degree. (in a sequence of degrees) */ - private static final String degPat = + private static final String degPat = // group(DEG_ALTER, Alter.CLASS) + "?" + group(DEG_VALUE, DEG_CLASS); /** Compiled pattern for one degree. */ @@ -514,7 +514,7 @@ public static SentenceInter create (TextLine line) // createValid // //-------------// /** - * Try to build a ChordNameInter instance from a provided TextWord. + * Try to create a ChordNameInter instance from a provided TextWord. * * @param textWord the provided TextWord * @return a populated ChordNameInter instance if successful, null otherwise @@ -593,25 +593,27 @@ private static ChordStructure parseChord (String value) if (matcher.matches()) { // Root - ChordNamePitch root = ChordNamePitch - .create(getGroup(matcher, ROOT_STEP), getGroup(matcher, ROOT_ALTER)); + ChordNamePitch root = ChordNamePitch.createValid( + getGroup(matcher, ROOT_STEP), + getGroup(matcher, ROOT_ALTER)); // Degrees String degStr = getGroup(matcher, DEGS); List degrees = ChordDegree.createList(degStr, null); ChordDegree firstDeg = (!degrees.isEmpty()) ? degrees.get(0) : null; - String firstDegStr = - (firstDeg != null) ? Integer.toString(degrees.get(0).value) : ""; + String firstDegStr = (firstDeg != null) ? Integer.toString(degrees.get(0).value) + : ""; // (maj7) special stuff String pmaj7 = standard(matcher, PMAJ7); // ChordKind - ChordKind kind = ChordKind.create(matcher, firstDegStr + pmaj7); + ChordKind kind = ChordKind.createValid(matcher, firstDegStr + pmaj7); // Bass - ChordNamePitch bass = ChordNamePitch - .create(getGroup(matcher, BASS_STEP), getGroup(matcher, BASS_ALTER)); + ChordNamePitch bass = ChordNamePitch.createValid( + getGroup(matcher, BASS_STEP), + getGroup(matcher, BASS_ALTER)); if ((firstDeg != null) && (kind.type != SUSPENDED_FOURTH) && (kind.type != SUSPENDED_SECOND)) { @@ -859,7 +861,6 @@ public static enum DegreeType //-----------// public static class ChordKind { - /** Precise type of kind. (subset of the 33 Music XML values) */ @XmlAttribute public final ChordType type; @@ -879,6 +880,7 @@ public static class ChordKind public final String text; // For JAXB + @SuppressWarnings("unused") private ChordKind () { this.type = null; @@ -942,15 +944,15 @@ public String toString () } /** - * Create proper ChordKind object from a provided matcher, augmented + * Try to create proper ChordKind object from a provided matcher, augmented * by dominant string if any. * * @param matcher matcher on input string * @param dominant dominant information if any, empty string otherwise * @return ChordKind instance, or null if failed */ - private static ChordKind create (Matcher matcher, - String dominant) + private static ChordKind createValid (Matcher matcher, + String dominant) { final String kindStr = getGroup(matcher, KIND); final String parStr = getGroup(matcher, PARS); @@ -970,7 +972,7 @@ private static ChordKind create (Matcher matcher, } // Then check for other combinations - final String str = + final String str = // standard(matcher, MIN) + standard(matcher, MAJ) + standard(matcher, AUG) + standard(matcher, DIM) + standard(matcher, HDIM) + dominant; ChordType type = typeOf(str); @@ -1072,7 +1074,6 @@ public static enum ChordType @XmlAccessorType(XmlAccessType.NONE) public static class ChordNamePitch { - /** Related step. */ @XmlAttribute public final AbstractNoteInter.NoteStep step; @@ -1113,14 +1114,14 @@ public String toString () } /** - * Create a ChordNamePitch object from provided step and alter strings + * Try to create a ChordNamePitch object from provided step and alter strings * * @param stepStr provided step string * @param alterStr provided alteration string * @return ChordNamePitch instance, or null if failed */ - public static ChordNamePitch create (String stepStr, - String alterStr) + public static ChordNamePitch createValid (String stepStr, + String alterStr) { stepStr = stepStr.trim(); alterStr = alterStr.trim(); @@ -1140,7 +1141,6 @@ public static ChordNamePitch create (String stepStr, //----------------// private static class ChordStructure { - public final ChordNamePitch root; public final ChordKind kind; diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/ClefInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/ClefInter.java index 34b5207cf..90290f217 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/ClefInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/ClefInter.java @@ -342,11 +342,11 @@ public static int absolutePitchOf (ClefInter clef, } } - //--------// - // create // - //--------// + //-------------// + // createValid // + //-------------// /** - * Create a Clef inter. + * Try to create a Clef inter. * * @param glyph underlying glyph * @param shape precise shape @@ -354,10 +354,10 @@ public static int absolutePitchOf (ClefInter clef, * @param staff related staff * @return the created instance or null if failed */ - public static ClefInter create (Glyph glyph, - Shape shape, - Double grade, - Staff staff) + public static ClefInter createValid (Glyph glyph, + Shape shape, + Double grade, + Staff staff) { if (staff.isTablature()) { return null; diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/CompoundNoteInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/CompoundNoteInter.java index 69e4555d1..eee733c2f 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/CompoundNoteInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/CompoundNoteInter.java @@ -120,9 +120,9 @@ public CompoundNoteInter (Glyph glyph, //~ Methods ------------------------------------------------------------------------------------ - //----------// - // getModel // - //----------// + //------------// + // buildModel // + //------------// /** * Build a poor-man model, just from staff and bounds (from glyph?). * @@ -317,7 +317,6 @@ public Collection searchLinks (SystemInfo system) public static class Model implements ObjectUIModel { - public Rectangle2D box; // CompoundNote bounds public Rectangle2D headBox; // Head bounds diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/FermataInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/FermataInter.java index 1c92adaa6..f8d5e37b6 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/FermataInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/FermataInter.java @@ -280,9 +280,9 @@ private Link lookupChordLink (SystemInfo system, return null; } - final Collection chords = - (shape == Shape.FERMATA_BELOW) ? stack.getStandardChordsAbove(center, bounds) - : stack.getStandardChordsBelow(center, bounds); + final Collection chords = (shape == Shape.FERMATA_BELOW) // + ? stack.getStandardChordsAbove(center, bounds) + : stack.getStandardChordsBelow(center, bounds); // Look for a suitable chord related to this fermata AbstractChordInter chord = AbstractChordInter.getClosestChord(chords, center); diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/Inter.java b/app/src/main/java/org/audiveris/omr/sig/inter/Inter.java index 2c640e670..3b8237cf5 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/Inter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/Inter.java @@ -108,7 +108,7 @@ public interface Inter /** * Derive (ghost) inter geometry from the provided symbol, font and current mouse location - * (when ghost is dragged, dropped or when created with repetitive input). + * (when ghost is dragged, dropped or when created via repetitive input). * * @param symbol the dropped symbol * @param sheet containing sheet @@ -470,8 +470,8 @@ boolean deriveFrom (ShapeSymbol symbol, boolean overlaps (Inter that); /** - * Prepare the manual addition of this inter, for which only staff and bounds have - * been set (notably, sig is not yet set). + * Prepare the manual addition of this inter, for which only 'staff' and 'bounds' have + * been set (notably, 'sig' is not yet set). *

* Build all the UI tasks to insert this inter: the addition task itself, together with * related tasks if any (other additions, links, ...). diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/InterVisitor.java b/app/src/main/java/org/audiveris/omr/sig/inter/InterVisitor.java index 704ee2832..7181005b3 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/InterVisitor.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/InterVisitor.java @@ -54,6 +54,8 @@ public interface InterVisitor void visit (BeamGroupInter inter); + void visit (BeatUnitInter inter); + void visit (BraceInter inter); void visit (BracketConnectorInter inter); @@ -76,6 +78,8 @@ public interface InterVisitor void visit (LedgerInter inter); + void visit (MetronomeInter inter); + void visit (MultipleRestInter inter); void visit (OctaveShiftInter inter); diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/LyricItemInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/LyricItemInter.java index 151529afb..ddccf2602 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/LyricItemInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/LyricItemInter.java @@ -111,7 +111,7 @@ public LyricItemInter (Double grade) /** * Creates a new LyricItemInter object. * - * @param textWord the OCR'ed text word + * @param textWord the OCR'd text word */ public LyricItemInter (TextWord textWord) { @@ -248,7 +248,7 @@ public LyricLineInter getLyricLine () */ private double getReferenceAbscissa () { - final Scale scale = sig.getSystem().getSheet().getScale(); + final Scale scale = staff.getSystem().getSheet().getScale(); final int xShift = scale.toPixels(constants.leftShift); return getLocation().getX() + xShift; @@ -515,7 +515,7 @@ public List preAdd (WrappedBoolean cancel, // Look for a containing lyric line final Point2D loc = getLocation(); final SystemInfo system = staff.getSystem(); - LyricLineInter line = new TextBuilder(system, true).lookupLyricLine(loc); + LyricLineInter line = new TextBuilder(system, Shape.LYRICS).lookupLyricLine(loc); if (line == null) { // Create a new lyric line diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/LyricLineInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/LyricLineInter.java index dee250543..ef4dbb092 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/LyricLineInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/LyricLineInter.java @@ -306,7 +306,7 @@ public void setNumber (int number) /** * Create a LyricLineInter from a TextLine. * - * @param line the OCR'ed text line + * @param line the OCR'd text line * @return the LyricLine inter */ public static LyricLineInter create (TextLine line) diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/MetronomeInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/MetronomeInter.java new file mode 100644 index 000000000..8a20e4d0e --- /dev/null +++ b/app/src/main/java/org/audiveris/omr/sig/inter/MetronomeInter.java @@ -0,0 +1,1491 @@ +//------------------------------------------------------------------------------------------------// +// // +// M e t r o n o m e I n t e r // +// // +//------------------------------------------------------------------------------------------------// +// +// +// Copyright © Audiveris 2023. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, either version +// 3 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with this +// program. If not, see . +//------------------------------------------------------------------------------------------------// +// +package org.audiveris.omr.sig.inter; + +import org.audiveris.omr.classifier.Evaluation; +import org.audiveris.omr.classifier.ShapeClassifier; +import org.audiveris.omr.constant.Constant; +import org.audiveris.omr.constant.ConstantSet; +import org.audiveris.omr.glyph.Glyph; +import org.audiveris.omr.glyph.GlyphFactory; +import org.audiveris.omr.glyph.GlyphIndex; +import org.audiveris.omr.glyph.Grades; +import org.audiveris.omr.glyph.Shape; +import static org.audiveris.omr.glyph.Shape.TEXT; +import org.audiveris.omr.math.GeoUtil; +import org.audiveris.omr.math.LineUtil; +import org.audiveris.omr.math.PointUtil; +import org.audiveris.omr.math.Rational; +import org.audiveris.omr.sheet.Scale; +import org.audiveris.omr.sheet.Sheet; +import org.audiveris.omr.sheet.SheetStub; +import org.audiveris.omr.sheet.SystemInfo; +import org.audiveris.omr.sheet.rhythm.MeasureStack; +import org.audiveris.omr.sheet.ui.ObjectUIModel; +import org.audiveris.omr.sig.SIGraph; +import org.audiveris.omr.sig.inter.BeatUnitInter.Note; +import static org.audiveris.omr.sig.inter.BeatUnitInter.Note.noteOf; +import static org.audiveris.omr.sig.inter.Inters.byAbscissa; +import org.audiveris.omr.sig.relation.ChordSentenceRelation; +import org.audiveris.omr.sig.relation.Containment; +import org.audiveris.omr.sig.relation.Link; +import org.audiveris.omr.sig.ui.AdditionTask; +import org.audiveris.omr.sig.ui.UITask; +import org.audiveris.omr.text.FontInfo; +import org.audiveris.omr.text.TextChar; +import org.audiveris.omr.text.TextLine; +import org.audiveris.omr.text.TextRole; +import org.audiveris.omr.text.TextWord; +import org.audiveris.omr.ui.symbol.MetronomeSymbol; +import org.audiveris.omr.ui.symbol.MusicFamily; +import org.audiveris.omr.ui.symbol.MusicFont; +import org.audiveris.omr.ui.symbol.ShapeSymbol; +import org.audiveris.omr.ui.symbol.TextFamily; +import org.audiveris.omr.ui.symbol.TextFont; +import org.audiveris.omr.util.Entities; +import org.audiveris.omr.util.HorizontalSide; +import static org.audiveris.omr.util.RegexUtil.getGroup; +import static org.audiveris.omr.util.RegexUtil.group; +import static org.audiveris.omr.util.StringUtil.codesOf; +import org.audiveris.omr.util.WrappedBoolean; +import org.audiveris.omr.util.Wrapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.font.TextLayout; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class MetronomeInter is a sentence that represents a metronome mark. + *

+ * In the current implementation, a metronome mark can contain: + *

    + *
  1. (Optional) a tempo textual indication, like "Allegretto" + *
  2. (Optional) an opening parenthesis, '(' + *
  3. A note symbol, like a quarter note or a dotted eighth note to specify the beat unit + *
  4. The equal sign '=' (we also accept the ':' character) + *
  5. (Optional) some text like "ca." + *
  6. A positive number, like "100", to specify the number of beat units per minute (bpm) + *
  7. (Optional) a second number, introduced by '-', like "-120", to specify a maximum bpm value + *
  8. (Optional) some text like "env." + *
  9. (Optional) a closing parenthesis, ')' + *
  10. (Optional) some final text, ignored + *
+ *

+ * Examples of various beat-unit values:
+ * Examples of various beat-unit values + *

+ * Instead of a precise single number, we can have two numbers to indicate an interval:
+ * Example with an interval value + *

+ * Tesseract OCR is not (yet?) able to recognize music notes within a sentence, these notes are thus + * mistaken with letters. For example, a quarter note is typically mistaken with a capital "J". + *

+ * However, the equal sign ("=") followed by the bpm value or interval are standard text characters + * and can thus be correctly OCR'd. + *

+ * The trick is organized around this "= bpm" sentence part: the preceding OCR'd character(s) are + * wrong but their underlying glyph can be extracted and then submitted to the glyph classifier in + * order to recognize a typical metronome beat unit shape. + *

+ * TODO: There are more complex marks, which could be covered in a future version, + * but first I need to figure out what they mean precisely. + * + * @author Hervé Bitteur + */ +@XmlRootElement(name = "metronome") +public class MetronomeInter + extends SentenceInter +{ + //~ Static fields/initializers ----------------------------------------------------------------- + + private static final Constants constants = new Constants(); + + private static final Logger logger = LoggerFactory.getLogger(MetronomeInter.class); + + private static final String TEMPO = "tempo"; + + private static final String PAR_START = "parStart"; // Opening parenthesis + + private static final String NOTE = "note"; + + private static final String EQUAL = "equal"; + + private static final String BPM1 = "bpm1"; + + private static final String BPM_EXT = "bpmext"; + + private static final String BPM2 = "bpm2"; + + private static final String BPM_TEXT = "bpmtext"; + + private static final String PAR_STOP = "parStop"; // Closing parenthesis + + private static final String GARBAGE = "garbage"; + + private static final String spacePat = "\\s*"; + + /** Pattern for tempo textual indication. It includes a final space. */ + private static final String tempoPat = group(TEMPO, "[^\\(\\x{E000}-\\x{FFFF}]*\\s"); + + /** Pattern for opening parenthesis. */ + private static final String parPatStart = group(PAR_START, "\\("); + + /** Pattern for note. */ + ///private static final String notePat = group(NOTE, "[^=\\s]+"); + private static final String notePat = group(NOTE, "[^=]+"); + + /** Pattern for equal. */ + private static final String equalPat = group(EQUAL, "[=:]"); + + /** Pattern for bpm numerical specification, a single value or an interval: 123[-456]. */ + private static final String bpmPat = group(BPM1, "[0-9]+") + spacePat + group( + BPM_EXT, + "-" + spacePat + group(BPM2, "[0-9]+")) + "?"; + + /** + * Pattern for bpm full text specification. + * The spec starts at '=' excluded and stops at either ')' excluded or the end of the sentence. + * It is meant to grab text portions like: "ca. 100", "110", "120-140", "130 env." + */ + private static final String bpmTextPat = group( + BPM_TEXT, + "[^0-9]*" + spacePat + bpmPat + spacePat + "[^\\)]*"); + + /** Pattern for closing parenthesis. */ + private static final String parPatStop = group(PAR_STOP, "\\)"); + + /** Pattern for potential ending garbage. */ + private static final String garbagePat = group(GARBAGE, ".*"); + + /** Pattern for the whole metronome mark. */ + private static final String metroPat = tempoPat + "?" // + + parPatStart + "?" + spacePat // + + notePat + spacePat // + + equalPat + spacePat // + + bpmTextPat + parPatStop + "?" // + + spacePat + garbagePat; + + private static final Pattern metroPattern = Pattern.compile(metroPat); + + //~ Instance fields ---------------------------------------------------------------------------- + + /** Required single (or minimum) value. */ + private Integer bpm1; + + /** Optional maximum value. */ + private Integer bpm2; + + /** Optional parentheses indicator. */ + private boolean parentheses = false; + + /** Related model, if any. */ + private Model model; + + //~ Constructors ------------------------------------------------------------------------------- + + /** + * No-argument constructor meant for JAXB. + */ + @SuppressWarnings("unused") + private MetronomeInter () + { + } + + /** + * Creates a new MetronomeInter object meant for manual assignment. + * + * @param grade inter grade + */ + public MetronomeInter (Double grade) + { + super(TextRole.Metronome, grade); + shape = Shape.METRONOME; + } + + /** + * Create a new MetronomeInter object from a former SentenceInter. + * + * @param s the sentence to be "replaced" + */ + public MetronomeInter (SentenceInter s) + { + super(s.getBounds(), s.getGrade(), s.getMeanFont(), TextRole.Metronome); + shape = Shape.METRONOME; + } + + /** + * Create a new MetronomeInter object from an OCR'd line + * + * @param line the OCR'd text line + */ + private MetronomeInter (TextLine line) + { + super(line.getBounds(), line.getGrade(), line.getMeanFont(), TextRole.Metronome); + shape = Shape.METRONOME; + } + + //~ Methods ------------------------------------------------------------------------------------ + + //--------// + // accept // + //--------// + @Override + public void accept (InterVisitor visitor) + { + visitor.visit(this); + } + + //------------// + // buildModel // + //------------// + /** + * Build the model from metronome concrete members. + * + * @return the model built + */ + private Model buildModel () + { + // Populate the model + final Model m = new Model(); + final List members = getMembers(); + Collections.sort(members, byAbscissa); // Safer + boolean afterBeat = false; // Have we processed the beat unit yet? + + for (Inter member : members) { + final WordInter word = (WordInter) member; + final String val = word.getValue(); + + if (val.contains("(") || val.contains(")")) { + m.parentheses = true; + } + + if (word instanceof BeatUnitInter beatUnit) { + m.unit = beatUnit.getShape(); + m.unitFontSize = beatUnit.getFontInfo().pointsize; + + final SheetStub stub = staff.getSystem().getSheet().getStub(); + final MusicFamily musicFamily = stub.getMusicFamily(); + final FontInfo fi = new FontInfo(m.unitFontSize, musicFamily.getFontName()); + final MusicFont f = new MusicFont(fi); + final Note note = Note.noteOf(m.unit); + final String str = note.getString(); + final TextLayout layout = f.layout(str); + final Rectangle2D rect = layout.getBounds(); + final Rectangle buRect = beatUnit.getBounds(); + m.baseCenter = new Point2D.Double( + buRect.x + buRect.width / 2, + buRect.y - rect.getY()); + + afterBeat = true; + } else { + if (!afterBeat) { + if (m.tempo == null) { + m.tempo = val; + m.tempoFontSize = word.getFontInfo().pointsize; + } else { + m.tempo += " "; + m.tempo += val; + } + } else { + if (m.bpmText == null) { + m.bpmText = val; + m.bpmFontSize = word.getFontInfo().pointsize; + } else { + m.bpmText += " "; + m.bpmText += val; + } + } + } + } + + logger.debug("buildModel. {}", m); + return m; + } + + //------------// + // deriveFrom // + //------------// + @Override + public boolean deriveFrom (ShapeSymbol symbol, + Sheet sheet, + MusicFont font, + Point dropLocation) + { + logger.debug("deriveFrom dropLocation:{}", dropLocation); + final MetronomeSymbol metroSymbol = (MetronomeSymbol) symbol; + + model = metroSymbol.getModel(font, dropLocation); + logger.debug("deriveFrom {}", model); + + setBounds(model.box.getBounds()); + + return true; + } + + //-------------// + // getBeatUnit // + //-------------// + /** + * Report the beat unit specified in this metronome mark. + * + * @return the 'note' used as beat unit + */ + private BeatUnitInter getBeatUnit () + { + for (Inter member : getMembers()) { + if (member instanceof BeatUnitInter beatUnit) { + return beatUnit; + } + } + + return null; + } + + //-----------// + // getBounds // + //-----------// + @Override + public Rectangle getBounds () + { + if (bounds == null) { + final List members = getMembers(); + + if (!members.isEmpty()) { + bounds = Entities.getBounds(members); + } else if (glyph != null) { + bounds = glyph.getBounds(); + } + } + + return (bounds != null) ? new Rectangle(bounds) : null; + } + + //--------// + // getBpm // + //--------// + /** + * Report the bpm value (if single) or the mean bpm value (if interval) + * + * @return the integer to be used as bpm + */ + private Integer getBpm () + { + if (bpm1 == null) { + parseValue(getValue(), false); + } + + if (bpm2 != null) { // Interval + return (bpm1 + bpm2) / 2; + } + + return bpm1; // Single value + } + + //---------// + // getBpm1 // + //---------// + /** + * Report the (minimum) bpm value. + * + * @return the bpm (minimum) value + */ + private Integer getBpm1 () + { + if (bpm1 == null) { + parseValue(getValue(), false); + } + + return bpm1; // Single value + } + + //------------// + // getBpmText // + //------------// + /** + * Report the full bpm specification, that is the text that follows the equal sign. + *

+ * This is either a number or an interval (min-max), perhaps introduced and/or followed by some + * text, but excluding the closing parenthesis if any. + *

+ * Examples: + *

    + *
  • "60" + *
  • "90-100" + *
  • "Ca. 120-144" + *
  • "69 env." + *
+ * + * @return the full bpm specification string or an empty string if none + */ + public String getBpmText () + { + final Matcher matcher = metroPattern.matcher(getValue()); + + if (!matcher.matches()) { + return ""; + } + + return getBpmText(matcher); + } + + private String getBpmText (Matcher matcher) + { + return getGroup(matcher, BPM_TEXT); + } + + //-----------------// + // getDisplayValue // + //-----------------// + /** + * Report the metronome content, meant for display in InterBoard. + * + * @return text content + */ + public String getDisplayValue () + { + if (model == null) { + model = buildModel(); + } + + String val = getValue(); + + // Discard the spaces introduced by getValue() after and before parentheses if any + val = val.replaceAll("\\( ", "("); + val = val.replaceAll(" \\)", ")"); + + return val; + } + + //---------// + // getNote // + //---------/ + /** + * Report the note symbol used as beat unit. + * + * @return the note symbol + */ + public Note getNote () + { + final BeatUnitInter beatUnit = getBeatUnit(); + + if (beatUnit == null) { + return null; + } + + return beatUnit.getNote(); + } + + //----------------------// + // getQuartersPerMinute // + //----------------------// + /** + * Report the number of quarters per minute, + * based on the note symbol and the number of beats per minute (bpm). + * + * @return the tempo, expressed in quarters per minute + */ + public int getQuartersPerMinute () + { + final Rational r = getBeatUnit().getNote().quarterValue().times(getBpm()); + return rounded(r.doubleValue()); + } + + //----------------// + // getShapeString // + //----------------// + @Override + public String getShapeString () + { + final String str = getValue(); + final Matcher matcher = metroPattern.matcher(str); + final StringBuilder sb = new StringBuilder(); + + if (!matcher.matches()) { + sb.append("INVALID"); + if (model.unit == null) + sb.append(", no unit"); + if (getGroup(matcher, EQUAL).isBlank()) + sb.append(", no '='"); + if (bpm1 == null) + sb.append(", no bpm"); + } else { + final Note note = getNote(); + sb.append((note != null) ? note.toShape() : "no unit") // + .append(' ').append(getBpmText(matcher)); + } + + return sb.toString(); + } + + //--------------// + // getTempoText // + //--------------// + /** + * Report the tempo indication (such as: Andante) that may precede the metronome mark. + * + * @return the tempo text, perhaps empty + */ + public String getTempoText () + { + final String str = getValue(); + final Matcher matcher = metroPattern.matcher(str); + + if (!matcher.matches()) { + return ""; + } + + return getGroup(matcher, TEMPO); + } + + //----------// + // getValue // + //----------// + /** + * Report the metronome content, built out of the contained words and make sure the model + * has been built. + * + * @return text content + */ + @Override + public String getValue () + { + if (model == null) { + model = buildModel(); + } + + return super.getValue(); + } + + //----------------// + // hasParentheses // + //----------------// + /** + * Report whether the metronome is wrapped with parentheses (at least one). + * + * @return true if so + */ + public boolean hasParentheses () + { + if (bpm1 == null) { + parseValue(getValue(), false); + } + + return parentheses; + } + + //-----------// + // internals // + //-----------// + @Override + protected String internals () + { + final StringBuilder sb = new StringBuilder(super.internals()); + if (getNote() != null) { + sb.append(" beat:").append(getNote()); + } + if (getBpm1() != null) { + sb.append(" bpm1:").append(getBpm1()); + } + if (bpm2 != null) { + sb.append(" bpm2:").append(bpm2); + } + if (parentheses) { + sb.append(" parentheses"); + } + + return sb.toString(); + } + + //-----------------// + // invalidateCache // + //-----------------// + /** + * Invalidate cached information. (typically following a word modification) + */ + @Override + public void invalidateCache () + { + super.invalidateCache(); + + bpm1 = null; + bpm2 = null; + parentheses = false; + } + + //------// + // link // + //------// + /** + * Try to link this metronome sentence. + * + * @param system the related system + */ + @Override + public void link (SystemInfo system) + { + try { + if (isVip()) { + logger.info("VIP link {}", this); + } + + if (!sig.hasRelation(this, ChordSentenceRelation.class)) { + // Map metronome with proper chord below + final Collection links = searchLinks(system); + + if (!links.isEmpty()) { + links.iterator().next().applyTo(this); + } else { + logger.info("No chord available for {} {}", this, getValue()); + } + } + + } catch (Exception ex) { + logger.warn("Error in link {} {}", this, ex.toString(), ex); + } + } + + //------------// + // parseValue // + //------------// + /** + * Parse the provided metronome sentence value, to populate a model. + * + * @param value the whole sentence value (perhaps containing music words!) + * @param plain true for pure text, false for text and music + * @return a populated model, null if failed + */ + private Model parseValue (String value, + boolean plain) + { + logger.debug("parseValue: \"{}\" [{}]", value, codesOf(value)); + + final Matcher matcher = metroPattern.matcher(value); + if (!matcher.matches()) { + logger.debug("Not a metronome matching string: \"{}\"", value); + return null; + } + + final Model m = new Model(); + m.tempo = getGroup(matcher, TEMPO).trim(); + + final String noteStr = getGroup(matcher, NOTE).trim(); + if (!plain) { + // Convert string codes into note + final Note note = Note.decode(noteStr); + logger.debug("noteStr: \"{}\" [{}] note: {}", noteStr, codesOf(noteStr, false), note); + + if (note != null) { + m.unit = note.toShape(); + } else { + logger.info("No beat unit in metronome line \"{}\" str: \"{}\"", value, noteStr); + } + } + + m.bpmText = getGroup(matcher, BPM_TEXT).trim(); + + // BPM1 + final String bpm1Str = getGroup(matcher, BPM1); + try { + bpm1 = Integer.decode(bpm1Str); + logger.debug("bpm1Str: \"{}\" bpm1: {}", bpm1Str, bpm1); + } catch (NumberFormatException ex) { + logger.info("Invalid bpm in metronome line {} str: \"{}\"", value, bpm1Str); + } + + // BPM2 + final String bpm2Str = getGroup(matcher, BPM2); + if (!bpm2Str.isEmpty()) { + try { + bpm2 = Integer.decode(bpm2Str); + logger.debug("bpm2Str: \"{}\" bpm2: {}", bpm2Str, bpm2); + } catch (NumberFormatException ex) { + logger.info("Invalid bpm2 in metronome line {} str: \"{}\"", value, bpm2Str); + } + } + + // Parentheses? + final String parStart = getGroup(matcher, PAR_START); + final String parStop = getGroup(matcher, PAR_STOP); + logger.debug("parStart: \"{}\" parStop: \"{}\"", parStart, parStop); + m.parentheses = !parStart.isEmpty() || !parStop.isEmpty(); + + return m; + } + + //--------// + // preAdd // + //--------// + @Override + public List preAdd (WrappedBoolean cancel, + Wrapper toPublish) + { + logger.debug("preAdd {}", model); + final List tasks = new ArrayList<>(super.preAdd(cancel, toPublish)); + + // Build members from model + final SIGraph theSig = staff.getSystem().getSig(); + final SheetStub stub = staff.getSystem().getSheet().getStub(); + + final MusicFamily musicFamily = stub.getMusicFamily(); + final FontInfo musicInfo = new FontInfo(model.unitFontSize, musicFamily.getFontName()); + + final TextFamily textFamily = stub.getTextFamily(); + final FontInfo textInfo = new FontInfo(model.bpmFontSize, textFamily.getFontName()); + + final BeatUnitInter beatUnit = new BeatUnitInter(model.unit, 1.0); + beatUnit.setFontInfo(musicInfo); + final int beatAdvance = beatUnit.getAdvance(); + + final TextFont textFont = new TextFont(textInfo); + TextLayout layout = textFont.layout(" "); + final int space = rounded(layout.getAdvance()); + + final WordInter bpmWord = new WordInter(Shape.TEXT, 1.0); + bpmWord.setValue("= " + model.bpmText); + bpmWord.setFontInfo(textInfo); + + final int y = rounded(model.baseCenter.getY()); + beatUnit.setLocation(new Point(rounded(model.box.getX()), y)); + bpmWord.setLocation(new Point(rounded(model.box.getX() + beatAdvance + space), y)); + + tasks.add( + new AdditionTask( + theSig, + beatUnit, + null, + Arrays.asList(new Link(this, new Containment(), false)))); + tasks.add( + new AdditionTask( + theSig, + bpmWord, + null, + Arrays.asList(new Link(this, new Containment(), false)))); + + return tasks; + } + + //-------------// + // searchLinks // + //-------------// + @Override + public Collection searchLinks (SystemInfo system) + { + final Point center = getCenter(); + + if (staff == null) { + staff = system.getStaffAtOrBelow(center); + } + + final Point ref = new Point(staff.getAbscissa(HorizontalSide.LEFT), center.y); + + // We target the first chord in the first stack(s) of the containing system, + // regardless of the metronome precise abscissa + for (MeasureStack stack : system.getStacks()) { + final AbstractChordInter chord = stack.getStandardChordBelow(ref, null); + + if (chord != null) { + return Collections.singleton(new Link(chord, new ChordSentenceRelation(), false)); + } + } + + return Collections.emptySet(); + } + + //----------// + // setValue // + //----------// + /** + * Assign a new value and change all members accordingly. + * + * @param newValue the new value + * @return the new member words + */ + public List setValue (String newValue) + { + final String oldValue = getValue(); + + if (newValue.equals(oldValue)) { + logger.debug("No modification made"); + return null; + } + + final Model newModel = parseValue(newValue, false); + + newModel.baseCenter = model.baseCenter; + if (newModel.baseCenter == null) { + // No too stupid: center abscissa and baseline of middle word + final List words = getMembers(); + if (!words.isEmpty()) { + final int idx = words.size() / 2; + final WordInter word = (WordInter) words.get(idx); + final Point2D loc = word.getLocation(); + newModel.baseCenter = new Point2D.Double(word.getCenter().x, loc.getY()); + } else { + newModel.baseCenter = getCenter(); // Better than nothing... + } + } + + final int mfs = meanFont.pointsize; + newModel.tempoFontSize = (model.tempoFontSize != null) ? model.tempoFontSize : mfs; + newModel.unitFontSize = (model.unitFontSize != null) ? model.unitFontSize : mfs; + newModel.bpmFontSize = (model.bpmFontSize != null) ? model.bpmFontSize : mfs; + logger.debug("newModel: {}", newModel); + + return buildNewWords(newModel, staff.getSystem()); + } + + //~ Static Methods ----------------------------------------------------------------------------- + + //--------// + // create // + //--------// + /** + * Create a MetronomeInter instance from the provided text line. + *

+ * Some of its items may need to be further adjusted by the end user. + *

+ * The caller is responsible for SIG insertion of the created metronome inter and of its + * member words. + * + * @param line the provided text line + * @param system the related system + * @param quiet quiet mode + * @param words (output) filled with the created member words + * @return a metronome inter + */ + public static MetronomeInter create (TextLine line, + SystemInfo system, + boolean quiet, + List words) + { + final Context ctx = new Context(); + ctx.sheet = system.getSheet(); + ctx.line = line; + + final Reporter reporter = new Reporter(quiet); + final MetronomeInter metro = new MetronomeInter(line); + + try { + final GlyphIndex glyphIndex = ctx.sheet.getGlyphIndex(); + final String str = line.getValue(); + + final Matcher matcher = metroPattern.matcher(str); + if (!matcher.matches()) { + reporter.info("Invalid line: " + str); // We can continue + } + + // Index to the word that contains the '=' equal sign + final int equalIndex = equalIndex(line); + if (equalIndex == -1) { + reporter.alert("No '=' character found"); + } + + // Note shape + // We retrieve the 'characters' glyph located just before the equal sign. + final String noteStr = getGroup(matcher, NOTE).trim(); + logger.debug("create. noteStr:\"{}\" codes[{}]", noteStr, codesOf(noteStr)); + + // Perhaps the note 'characters' are in the same word as the '=' sign + ctx.noteWord = line.getWords().get(equalIndex); + ctx.charIndex = ctx.noteWord.getValue().indexOf(noteStr); + + if (ctx.charIndex == -1) { + // Note not found, let's look in the word before + ctx.noteWord = line.getWords().get(equalIndex - 1); + ctx.charIndex = ctx.noteWord.getValue().indexOf(noteStr); + } + + if (ctx.charIndex == -1) { + reporter.alert("Note characters not found in line: " + line); + } + + ctx.charCount = noteStr.length(); + ctx.noteGlyph = getNoteGlyph(glyphIndex, ctx.noteWord, ctx.charIndex, ctx.charCount); + if (ctx.noteGlyph == null) { + reporter.alert("No underlying glyph for note in line: " + line); + } else { + ctx.noteGlyph = glyphIndex.registerOriginal(ctx.noteGlyph); + + ctx.note = recognizeNote(ctx.noteGlyph, system); + logger.debug("note: {}", ctx.note); + if (ctx.note == null) { + reporter.alert("Non recognized note for glyph#" + ctx.noteGlyph.getId()); + } + } + + // BPM1 + final String bpm1Str = getGroup(matcher, BPM1); + try { + Integer bpm1 = Integer.decode(bpm1Str); + logger.debug("bpm1: {}", bpm1); + } catch (NumberFormatException ex) { + reporter.alert("Non recognized bpm in \"" + bpm1Str + "\""); + } + + // BPM2 + final String bpm2Str = getGroup(matcher, BPM2); + if (!bpm2Str.isEmpty()) { + try { + Integer bpm2 = Integer.decode(bpm2Str); + logger.debug("bpm2: {}", bpm2); + } catch (NumberFormatException ex) { + reporter.alert("Non recognized bpm2 in \"" + bpm2Str + "\""); + } + } + } catch (ParsingException ignored) {} + + // Build the member words even if the metronome is still invalid + words.addAll(buildWords(ctx)); + + if (ctx.noteWord != null) { + // To be protected against symbol competitors + metro.freeze(); + words.forEach(w -> w.freeze()); + } + + return metro; + } + + //---------------// + // buildNewWords // + //---------------// + /** + * Generate metronome member words from the provided new model. + *

+ * Items are placed around the baseCenter reference point, separated by standard spaces. + * Target structure is: + * + *

+     * tempo text|(B|=|bpm text)
+     * ..........S..s.s.........
+     * 'S' means space (w/ tempo font)
+     * 's' means space (w/ bpm font)
+     * 'tempo text' is only one word, using tempo font
+     * The potential "(" is separate (w/ same font as bpm)
+     * 'bpm text' is only one word, perhaps ended by ")", w/ bpm font
+     * 
+ * + * @param m the new model + * @return the generated words, ready for insertion + */ + private static List buildNewWords (Model m, + SystemInfo system) + { + final SheetStub stub = system.getSheet().getStub(); + + final MusicFamily musicFamily = stub.getMusicFamily(); + final TextFamily textFamily = stub.getTextFamily(); + + final List newWords = new ArrayList<>(); + final int y = rounded(m.baseCenter.getY()); + double xMin = m.baseCenter.getX(); + double xMax = m.baseCenter.getX(); + + // We start on beat unit + if (m.unit != null) { + final FontInfo fi = new FontInfo(m.unitFontSize, musicFamily.getFontName()); + final MusicFont f = new MusicFont(fi); + final Note note = Note.noteOf(m.unit); + final String str = note.getString(); + final TextLayout layout = f.layout(str); + final Rectangle2D rect = layout.getBounds(); + final Point loc = rounded(m.baseCenter.getX() - rect.getWidth() / 2, y); + final Rectangle box = rounded( + loc.x, + m.baseCenter.getY() + rect.getY(), + rect.getWidth(), + rect.getHeight()); + newWords.add(new BeatUnitInter(null, box, 1.0, str, f, note, loc)); + xMin = loc.x; + xMax = loc.x + rect.getWidth(); + } else { + // TODO: use a beat unit place-holder??? + } + + // Moving forwards from unit + final FontInfo fiBpm = new FontInfo(m.bpmFontSize, textFamily.getFontName()); + final TextFont fBpm = new TextFont(fiBpm); + + if (m.bpmText != null) { + xMax += fBpm.layout(" ").getAdvance(); + + final String val = "= " + m.bpmText + (m.parentheses ? ")" : ""); + final TextLayout layout = fBpm.layout(val); + final Rectangle2D rect = layout.getBounds(); + xMax += rect.getX(); + + final Rectangle box = rounded( + xMax, + m.baseCenter.getY() + rect.getY(), + rect.getWidth(), + rect.getHeight()); + newWords.add(new WordInter(null, box, TEXT, 1.0, val, fiBpm, rounded(xMax, y))); + } + + // Moving backwards from unit + if (m.parentheses) { + final String val = "("; + final TextLayout layout = fBpm.layout(val); + final Rectangle2D rect = layout.getBounds(); + xMin -= layout.getAdvance(); + + final Rectangle box = rounded( + xMin, + m.baseCenter.getY() + rect.getY(), + rect.getWidth(), + rect.getHeight()); + newWords.add(new WordInter(null, box, TEXT, 1.0, val, fiBpm, rounded(xMin, y))); + } + + if (m.tempo != null && !m.tempo.isBlank()) { + final FontInfo fi = new FontInfo(m.tempoFontSize, textFamily.getFontName()); + final TextFont f = new TextFont(fi); + final TextLayout layout = f.layout(m.tempo); + final Rectangle2D rect = layout.getBounds(); + xMin -= layout.getAdvance(); + + final Rectangle box = rounded( + xMin, + m.baseCenter.getY() + rect.getY(), + rect.getWidth(), + rect.getHeight()); + newWords.add(new WordInter(null, box, TEXT, 1.0, m.tempo, fi, rounded(xMin, y))); + } + + Collections.sort(newWords, Inters.byCenterAbscissa); + return newWords; + } + + //------------// + // buildWords // + //------------// + /** + * Build the member words of the metronome sentence. + * + * @param ctx context built while running the metronome create() method + * @return the list of created WordInter instances (perhaps including a BeatUnitInter instance) + */ + private static List buildWords (Context ctx) + { + final List created = new ArrayList<>(); + final GlyphIndex glyphIndex = ctx.sheet.getGlyphIndex(); + + for (TextWord word : ctx.line.getWords()) { + if (word == ctx.noteWord) { // This is the word that contains the note + // Stuff before note? + if (ctx.charIndex > 0) { + created.add(extractText(glyphIndex, word, 0, ctx.charIndex)); + } + + // Note itself + if (ctx.note != null) { + final Scale scale = ctx.sheet.getScale(); + final MusicFamily family = ctx.sheet.getStub().getMusicFamily(); + final MusicFont musicFont = MusicFont.getBaseFont(family, scale.getInterline()); + final Rectangle bounds = ctx.noteGlyph.getBounds(); + final Point2D location = LineUtil.intersectionAtX(word.getBaseline(), bounds.x); + created.add( + new BeatUnitInter( + ctx.noteGlyph, + bounds, + 1.0, + ctx.note.getString(), + musicFont, + ctx.note, + PointUtil.rounded(location))); + } + + // Stuff after note? + final String content = word.getValue(); + final int nextIndex = ctx.charIndex + ctx.charCount; + if (content.length() > nextIndex) { + created.add(extractText(glyphIndex, word, nextIndex, content.length())); + } + } else { // This is just a plain word + final WordInter wi = new WordInter(word); + wi.setValue(wi.getValue().replace(':', '=')); + created.add(wi); + } + } + + return created; + } + + //------------// + // equalIndex // + //------------// + /** + * In the input line, report the index of the text word that contains the equal sign. + * + * @param line the input line + * @return the index of the "=" (or ":") word in line, or -1 if not found + */ + private static int equalIndex (TextLine line) + { + final List words = line.getWords(); + + for (int i = 0; i < words.size(); i++) { + final String value = words.get(i).getValue(); + if (value.contains("=") || value.contains(":")) { + return i; + } + } + + return -1; + } + + //-------------// + // extractText // + //-------------// + /** + * Build a WordInter from a portion of the provided TextWord. + * + * @param glyphIndex the sheet index for glyphs + * @param word the source text word + * @param beginIndex the beginning character index, inclusive. + * @param endIndex the ending character index, exclusive. + * @return the created WordInter instance + */ + private static WordInter extractText (GlyphIndex glyphIndex, + TextWord word, + int beginIndex, + int endIndex) + { + final List chars = word.getChars().subList(beginIndex, endIndex); + final Set parts = new LinkedHashSet<>(); + chars.forEach(c -> parts.addAll(glyphIndex.getContainedEntities(c.getBounds()))); + + final Glyph glyph = glyphIndex.registerOriginal(GlyphFactory.buildGlyph(parts)); + final Rectangle bounds = glyph.getBounds(); + final Point2D location = LineUtil.intersectionAtX(word.getBaseline(), bounds.x); + + return new WordInter( + glyph, + glyph.getBounds(), + Shape.TEXT, + word.getConfidence() * Grades.intrinsicRatio, + word.getValue().substring(beginIndex, endIndex).replace(':', '='), + word.getFontInfo(), + PointUtil.rounded(location)); + } + + //-------------------// + // fullValidityCheck // + //-------------------// + /** + * A debug/test tool to check a given input string. + * + * @param input the string to check + * @return the match result + */ + public static boolean fullValidityCheck (String input) + { + final Matcher matcher = metroPattern.matcher(input); + System.out.println(String.format("\n\"%s\"", input)); + System.out.println(String.format(" codes[%s]", codesOf(input, true))); + + final boolean result = matcher.matches(); + + if (result) { + // Dump all groups + for (String group : new String[] { TEMPO, PAR_START, NOTE, EQUAL, BPM_TEXT, BPM1, + BPM_EXT, BPM2, PAR_STOP, GARBAGE }) { + final String str = getGroup(matcher, group); + + final String n; + if (group.equals(NOTE)) { + final Note note = Note.decode(str); + n = (note != null) ? note.name() : "null"; + } else { + n = ""; + } + + System.out.println( + String.format(" %10s %d \"%s\" %s", group, str.length(), str, n)); + } + } else { + System.out.println("Not a metronome matching string."); + } + + return result; + } + + //--------------// + // getNoteGlyph // + //--------------// + /** + * Extract the underlying glyph of the note 'characters'. + * + * @param glyphIndex all sheet glyphs + * @param noteWord the text word that contains the note 'characters' + * @param charIndex the 'characters' index in the text word + * @param length the count of characters to extract + * @return the note glyph + */ + private static Glyph getNoteGlyph (GlyphIndex glyphIndex, + TextWord noteWord, + int charIndex, + int length) + { + Rectangle noteBox = null; + + for (int i = 0; i < length; i++) { + final TextChar noteChar = noteWord.getChars().get(charIndex + i); + logger.debug("noteChar: {}", noteChar); + final Rectangle charBox = noteChar.getBounds(); + + if (noteBox == null) { + noteBox = charBox; + } else { + noteBox = noteBox.union(charBox); + } + } + + final List glyphs = glyphIndex.getContainedEntities(noteBox); + + if (glyphs.isEmpty()) { + return null; + } + + return GlyphFactory.buildGlyph(glyphs); + } + + //----------// + // isLikely // + //----------// + /** + * Check whether the provided text line is likely to be a metronome mark. + * + * @param line the text line to check + * @return true if so + */ + public static boolean isLikely (TextLine line) + { + final String str = line.getValue(); + + if (logger.isDebugEnabled()) { + fullValidityCheck(str); + } + + final Matcher matcher = metroPattern.matcher(str); + + return matcher.matches(); + } + + //---------------// + // recognizeNote // + //---------------// + /** + * Try to recognize the provided glyph as a beat unit note symbol. + * + * @param noteGlyph the glyph to process + * @param system the related system + * @return the note recognized, or null if failed + */ + private static Note recognizeNote (Glyph noteGlyph, + SystemInfo system) + { + logger.debug("Note glyph: {}", noteGlyph); + + final int evalNb = constants.maxEvaluationRank.getValue(); + final Evaluation[] evals = ShapeClassifier.getInstance().evaluate( + noteGlyph, + system, + evalNb, + 0.0, + null); + + for (int i = 0; i < evalNb; i++) { + final Evaluation eval = evals[i]; + final Note note = noteOf(eval.shape); + + if (note != null) { + return note; + } + } + + return null; + } + + // Rounding utilities + private static int rounded (double v) + { + return (int) Math.rint(v); + } + + private static Point rounded (double x, + double y) + { + return new Point(rounded(x), rounded(y)); + } + + private static Rectangle rounded (double x, + double y, + double w, + double h) + { + return new Rectangle(rounded(x), rounded(y), rounded(w), rounded(h)); + } + + //~ Inner Classes ------------------------------------------------------------------------------ + + //----------// + // Reporter // + //----------// + private static class Reporter + { + final boolean quiet; + + public Reporter (boolean quiet) + { + this.quiet = quiet; + } + + public void alert (String message) + throws ParsingException + { + if (quiet) { + logger.debug("Metronome. {}", message); // Meant for debugging only + } else { + info(message); + } + + throw new ParsingException(message); // Stop processing + } + + public void info (String message) + { + logger.info("Metronome. {}", message); // Feedback to the end user + } + } + + //-----------// + // Constants // + //-----------// + private static class Constants + extends ConstantSet + { + private final Constant.Integer maxEvaluationRank = new Constant.Integer( + "none", + 5, + "Maximum acceptable rank for note recognition"); + } + + //-------// + // Model // + //-------// + public static class Model + implements ObjectUIModel + { + public String tempo; // Such as "Adagio" + + public Shape unit; // Such as METRO_QUARTER + + public String bpmText; // Such as "ca. 140" + + public boolean parentheses; // Parentheses? + + public Integer tempoFontSize; // Font size for tempo + + public Integer unitFontSize; // Font size for beat unit + + public Integer bpmFontSize; // Font size for "=" and for bpm text + + public Rectangle2D box; // Metronome global bounds + + public Point2D baseCenter; // Unit baseline center + + @Override + public void translate (double dx, + double dy) + { + PointUtil.add(baseCenter, dx, dy); + GeoUtil.translate2D(box, dx, dy); + } + + @Override + public String toString () + { + return new StringBuilder("Model{") // + .append("tempo:\"").append(tempo).append('\"') // + .append(" unit:").append(unit) // + .append(" bpmText:\"").append(bpmText).append('\"') // + .append(" par:").append(parentheses) // + .append(" tempoFS:").append(tempoFontSize) // + .append(" unitFS:").append(unitFontSize) // + .append(" bpmFS:").append(bpmFontSize) // + .append(" box:").append(box) // + .append(" baseCenter:").append(baseCenter) // + .append('}').toString(); + } + } + + //---------// + // Context // + //---------// + private static class Context + { + Sheet sheet; + + TextLine line; + + TextWord noteWord; + + Integer charIndex; + + Integer charCount; + + Glyph noteGlyph; + + Note note; + } + + //------------------// + // ParsingException // + //------------------// + private static class ParsingException + extends Exception + { + public ParsingException (String message) + { + super(message); + } + } +} diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/MusicWordInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/MusicWordInter.java new file mode 100644 index 000000000..1c6f7b3ec --- /dev/null +++ b/app/src/main/java/org/audiveris/omr/sig/inter/MusicWordInter.java @@ -0,0 +1,376 @@ +//------------------------------------------------------------------------------------------------// +// // +// M u s i c W o r d I n t e r // +// // +//------------------------------------------------------------------------------------------------// +// +// +// Copyright © Audiveris 2023. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, either version +// 3 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with this +// program. If not, see . +//------------------------------------------------------------------------------------------------// +// +package org.audiveris.omr.sig.inter; + +import org.audiveris.omr.glyph.Glyph; +import org.audiveris.omr.glyph.Shape; +import org.audiveris.omr.sheet.Scale; +import org.audiveris.omr.sheet.Sheet; +import org.audiveris.omr.sheet.Staff; +import org.audiveris.omr.text.FontInfo; +import org.audiveris.omr.ui.symbol.MusicFamily; +import org.audiveris.omr.ui.symbol.MusicFont; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.font.TextLayout; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class MusicWordInter represents a word made of music characters. + *

+ * This kind of word is used to handle the music notes in a metronome indication. + *

+ * Examples taken from MusicXML reference: + *

+ *
metronome
+ *
+ *
+ *
metronome-note
+ *
+ *
per-minute
+ *
+ *
beat-unit-dot
+ *
+ *
beat-unit-tied
+ *
+ *
beat-unit
+ *
+ *
metronome-arrows
+ *
+ *
metronome-tied
+ *
+ *
beat-unit-dot
+ *
+ *
+ * + * @author Hervé Bitteur + */ +@XmlRootElement(name = "music-word") +@XmlAccessorType(XmlAccessType.NONE) +public class MusicWordInter + extends WordInter +{ + //~ Static fields/initializers ----------------------------------------------------------------- + + private static final Logger logger = LoggerFactory.getLogger(MusicWordInter.class); + + //~ Instance fields ---------------------------------------------------------------------------- + + /** Temporary use during marshalling/unmarshalling. */ + @XmlElement(name = "code") + private volatile List codes; + + //~ Constructors ------------------------------------------------------------------------------- + + /** + * No-argument constructor meant for JAXB. + */ + @SuppressWarnings("unused") + protected MusicWordInter () + { + } + + /** + * Creates a new MusicWordInter object, meant for manual assignment. + * + * @param grade the interpretation quality + * @param shape the precise shape if any + * @param value the word content (music characters) + */ + public MusicWordInter (Double grade, + Shape shape, + String value) + { + super(null, null, shape, grade, value, null, null); + } + + /** + * Creates a new MusicWordInter object. + * + * @param glyph the underlying glyph + * @param bounds the precise object bounds + * @param grade the interpretation quality + * @param shape the precise shape if any + * @param value the word content (music characters) + * @param musicFont the music font to use + * @param location the baseline location + */ + public MusicWordInter (Glyph glyph, + Rectangle bounds, + Double grade, + Shape shape, + String value, + MusicFont musicFont, + Point location) + { + super( + glyph, + bounds, + shape, + grade, + value, + new FontInfo( + musicFont.computeSize(value, bounds.getSize()), + musicFont.getFontName()), + location); + } + + //~ Methods ------------------------------------------------------------------------------------ + + //---------------// + // beforeMarshal // + //---------------// + /** + * Called immediately before the marshalling of this object begins. + *

+ * Transcribe value to a list of hexadecimal codes. + */ + @Override + @SuppressWarnings("unused") + protected void beforeMarshal (Marshaller m) + { + super.beforeMarshal(m); + + codes = new ArrayList<>(); + value.codePoints().forEach(c -> codes.add("0x" + Integer.toHexString(c))); + logger.debug("beforeMarshal. codes: {}", codes); + value = null; + } + + //--------------// + // afterMarshal // + //--------------// + /** + * Called immediately after marshalling of this object. + * We reset any empty RunSequence to null. + */ + @SuppressWarnings("unused") + private void afterMarshal (Marshaller m) + { + logger.debug("afterMarshal. codes: {}", codes); + value = computeValue(codes); + codes = null; + } + + //-----------------// + // beforeUnmarshal // + //-----------------// + @SuppressWarnings("unused") + private void beforeUnmarshal (Object target, + Object parent) + { + logger.debug("beforeUnmarshal"); + } + + //----------------// + // afterUnmarshal // + //----------------// + @SuppressWarnings("unused") + private void afterUnmarshal (Unmarshaller um, + Object parent) + { + logger.debug("afterUnmarshal. codes: {}", codes); + value = computeValue(codes); + logger.debug("afterUnmarshal. value: {}", value); + } + + //--------------// + // computeValue // + //--------------// + /** + * Compute the content value by decoding the sequence of characters codes. + * + * @param codes the list of character codes (hexadecimal strings) + * @return the corresponding content value + */ + private String computeValue (List codes) + { + final int nb = codes.size(); + int[] ints = new int[nb]; + + for (int i = 0; i < nb; i++) { + ints[i] = Integer.decode(codes.get(i)); + } + + return new String(ints, 0, nb); + } + + //------------// + // getAdvance // + //------------// + @Override + public int getAdvance () + { + if (value.isEmpty()) { + return 0; + } + + // As opposed to a plain WordInter, we use MusicFont rather than TextFont + final MusicFont font = new MusicFont(fontInfo); + final TextLayout layout = font.layout(value); + + return (int) Math.rint(layout.getAdvance()); + } + + //-----------// + // getBounds // + //-----------// + @Override + public Rectangle getBounds () + { + if (bounds != null) { + return new Rectangle(bounds); + } + + if (value.isEmpty()) { + return new Rectangle( + bounds = new Rectangle( + (int) Math.rint(location.getX()), + (int) Math.rint(location.getY()), + 0, + 0)); + } + + final MusicFont textFont = new MusicFont(fontInfo); + final TextLayout layout = textFont.layout(value); + final Rectangle2D rect = layout.getBounds(); + + return new Rectangle( + bounds = new Rectangle( + (int) Math.rint(location.getX()), + (int) Math.rint(location.getY() + rect.getY()), + (int) Math.rint(rect.getWidth()), + (int) Math.rint(rect.getHeight()))); + } + + //--------------// + // getDimension // + //--------------// + @Override + public Dimension getDimension () + { + if (bounds != null) { + return bounds.getSize(); + } + + if (value.isEmpty()) { + return new Dimension(0, 0); + } + + final MusicFont musicFont = new MusicFont(fontInfo); + final TextLayout layout = musicFont.layout(value); + final Rectangle2D rect = layout.getBounds(); + + return new Dimension((int) Math.rint(rect.getWidth()), (int) Math.rint(rect.getHeight())); + } + + //----------// + // getValue // + //----------// + /** + * Report the word (music) value, even if called in the middle of any [un]marshalling. + * + * @return the value + */ + @Override + public String getValue () + { + final String theValue = value; + + if (theValue != null) { + return theValue; + } + + final List theCodes = codes; + + if (theCodes != null) { + return computeValue(theCodes); + } else { + return value; + } + } + + //-----------// + // setBounds // + //-----------// + @Override + public void setBounds (Rectangle bounds) + { + super.setBounds(bounds); + + if (fontInfo == null) { + tryToSetFontInfo(); + } + } + + //----------// + // setStaff // + //----------// + @Override + public void setStaff (Staff staff) + { + super.setStaff(staff); + + if (fontInfo == null) { + tryToSetFontInfo(); + } + } + + //------------------// + // tryToSetFontInfo // + //------------------// + private void tryToSetFontInfo () + { + if ((bounds != null) && (staff != null)) { + final Sheet sheet = staff.getSystem().getSheet(); + final Scale scale = sheet.getScale(); + final MusicFamily family = sheet.getStub().getMusicFamily(); + final MusicFont musicFont = MusicFont.getBaseFont(family, scale.getInterline()); + fontInfo = new FontInfo( + musicFont.computeSize(value, bounds.getSize()), + musicFont.getFontName()); + location = musicFont.computeLocation(value, bounds); + } + } +} diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/RestInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/RestInter.java index 60582817e..d28673c2f 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/RestInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/RestInter.java @@ -148,7 +148,7 @@ public List preAdd (WrappedBoolean cancel, // createValid // //-------------// /** - * (Try to) create a Rest inter. + * Try to create a Rest inter. *

* A rest cannot be too close abscissa-wise to a head-chord. * @@ -190,8 +190,7 @@ public static RestInter createValid (Glyph glyph, // All head-chords in staff measure final int left = measure.getAbscissa(HorizontalSide.LEFT, restStaff); final int right = measure.getAbscissa(HorizontalSide.RIGHT, restStaff); - final List measureChords = Inters.inters(systemHeadChords, (Inter inter) -> - { + final List measureChords = Inters.inters(systemHeadChords, (Inter inter) -> { if (inter.getStaff() != restStaff) { return false; } diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/SentenceInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/SentenceInter.java index 74143b275..42dd0bc27 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/SentenceInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/SentenceInter.java @@ -21,11 +21,15 @@ // package org.audiveris.omr.sig.inter; -import org.audiveris.omr.glyph.Glyph; +import org.audiveris.omr.sheet.Scale; import org.audiveris.omr.sheet.Skew; import org.audiveris.omr.sheet.Staff; import org.audiveris.omr.sheet.SystemInfo; +import org.audiveris.omr.sheet.rhythm.MeasureStack; import org.audiveris.omr.sig.SIGraph; +import org.audiveris.omr.sig.relation.ChordNameRelation; +import org.audiveris.omr.sig.relation.ChordSentenceRelation; +import org.audiveris.omr.sig.relation.ChordSyllableRelation; import org.audiveris.omr.sig.relation.Containment; import org.audiveris.omr.sig.relation.EndingSentenceRelation; import org.audiveris.omr.sig.relation.Link; @@ -33,6 +37,13 @@ import org.audiveris.omr.text.FontInfo; import org.audiveris.omr.text.TextLine; import org.audiveris.omr.text.TextRole; +import static org.audiveris.omr.text.TextRole.ChordName; +import static org.audiveris.omr.text.TextRole.Direction; +import static org.audiveris.omr.text.TextRole.EndingNumber; +import static org.audiveris.omr.text.TextRole.EndingText; +import static org.audiveris.omr.text.TextRole.Lyrics; +import static org.audiveris.omr.text.TextRole.Metronome; +import static org.audiveris.omr.text.TextRole.PartName; import org.audiveris.omr.ui.symbol.TextFont; import org.audiveris.omr.util.Entities; import org.audiveris.omr.util.WrappedBoolean; @@ -48,6 +59,7 @@ import java.util.Comparator; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlRootElement; @@ -85,8 +97,7 @@ public class SentenceInter /** For ordering sentences by their de-skewed ordinate. */ public static final Comparator byOrdinate = (s1, - s2) -> - { + s2) -> { final Skew skew = s1.getSig().getSystem().getSkew(); return Double.compare( @@ -131,7 +142,7 @@ public SentenceInter (Rectangle bounds, FontInfo meanFont, TextRole role) { - super((Glyph) null, bounds, null, grade); + super(null, bounds, null, grade); this.meanFont = meanFont; this.role = role; @@ -140,7 +151,7 @@ public SentenceInter (Rectangle bounds, /** * Creates a new SentenceInter object, meant for user handling of glyph. * - * @param role the sentence role, if known + * @param role the sentence role, if known, null otherwise * @param grade the interpretation quality */ public SentenceInter (TextRole role, @@ -221,7 +232,7 @@ public Rectangle getBounds () */ public int getExportedFontSize () { - return (int) Math.rint(meanFont.pointsize * TextFont.TO_POINT); + return (int) Math.rint(getMeanFont().pointsize * TextFont.TO_POINT); } //--------------// @@ -348,30 +359,14 @@ public String getShapeString () // getValue // //----------// /** - * Report sentence text content, built out of contained words. + * Report the sentence text content, built out of the contained words. * * @return text content */ public String getValue () { - StringBuilder sb = null; - - // Use each word value - for (Inter word : getMembers()) { - String str = ((WordInter) word).getValue(); - - if (sb == null) { - sb = new StringBuilder(str); - } else { - sb.append(" ").append(str); - } - } - - if (sb == null) { - return ""; - } else { - return sb.toString(); - } + return getMembers().stream().map(w -> ((WordInter) w).getValue()) // + .collect(Collectors.joining(" ")); } //-----------// @@ -380,13 +375,10 @@ public String getValue () @Override protected String internals () { - StringBuilder sb = new StringBuilder(super.internals()); - - sb.append(' ').append((meanFont != null) ? meanFont.getMnemo() : "NO_FONT"); - - sb.append(' ').append((role != null) ? role : "NO_ROLE"); - - return sb.toString(); + return new StringBuilder(super.internals()) // + .append(' ').append((meanFont != null) ? "mFont:" + meanFont.getMnemo() : "NO_FONT") // + .append(' ').append((role != null) ? role : "NO_ROLE") // + .toString(); } //-----------------// @@ -412,6 +404,108 @@ public void invalidateCache () // TODO: should we update sentence grade? } + //------// + // link // + //------// + /** + * Try to link this sentence, based on its role. + * + * @param system the related system + */ + public void link (SystemInfo system) + { + try { + if (isVip()) { + logger.info("VIP link {}", this); + } + + if (role == null) { + logger.info("No role for {}", this); + return; + } + + final Point2D location = getLocation(); + getBounds(); + final Scale scale = system.getSheet().getScale(); + + switch (role) { + case Lyrics -> { + // Map each syllable with proper chord, in assigned staff + for (Inter wInter : getMembers()) { + final LyricItemInter item = (LyricItemInter) wInter; + final int profile = Math.max(item.getProfile(), system.getProfile()); + item.mapToChord(profile); + } + } + + case Direction -> { + if (!sig.hasRelation(this, ChordSentenceRelation.class)) { + // Map sentence with proper chord, preferably above for a direction + final MeasureStack stack = system.getStackAt(location); + + if (stack == null) { + logger.info("No measure stack for {} {}", this, getValue()); + } else { + final int xGapMax = scale.toPixels(ChordSentenceRelation.getXGapMax()); + final Rectangle box = new Rectangle(bounds); + box.grow(xGapMax, 0); + + final AbstractChordInter chord = stack.getEventChord( + location, + box, + true); + + if (chord != null) { + sig.addEdge(chord, this, new ChordSentenceRelation()); + } else { + logger.info("No chord near {} {}", this, getValue()); + } + } + } + } + + case PartName -> { + // Assign part name to proper part + staff = system.getClosestStaff(getCenter()); + part = staff.getPart(); + part.setName(this); + } + + case ChordName -> { + // Map each word with proper chord, in assigned staff + for (Inter wInter : getMembers()) { + final ChordNameInter word = (ChordNameInter) wInter; + final Link link = word.lookupLink(system); + + if (link == null) { + logger.info("No chord below {}", word); + } else { + link.applyTo(wInter); + } + } + } + + case EndingNumber, EndingText -> { + // Look for related ending + final Link link = lookupEndingLink(system); + + if ((link != null) && (null == sig.getRelation( + link.partner, + this, + EndingSentenceRelation.class))) { + sig.addEdge(link.partner, this, link.relation); + } + } + } + + // Roles UnknownRole, Title, Number, Creator*, Rights stand by themselves + // and thus need no link. + + } catch (Exception ex) { + logger.warn("Error in link {} {}", this, ex.toString(), ex); + } + } + //------------------// // lookupEndingLink // //------------------// @@ -521,6 +615,52 @@ public void setRole (TextRole role) this.role = role; } + //--------// + // unlink // + //--------// + /** + * Unlink the sentence, according to its role, with its related entity if any. + * + * @param oldRole the role this sentence had + */ + public void unlink (TextRole oldRole) + { + try { + if (isVip()) { + logger.info("VIP unlink for {}", this); + } + + switch (oldRole) { + case null -> logger.info("Null old role for {}", this); + default -> {} + + case Lyrics -> getMembers().forEach( + wInter -> sig.getRelations(wInter, ChordSyllableRelation.class).forEach( + rel -> sig.removeEdge(rel))); + + case Direction, Metronome -> sig.getRelations(this, ChordSentenceRelation.class) + .forEach(rel -> sig.removeEdge(rel)); + + case PartName -> { + // Look for proper part + staff = sig.getSystem().getClosestStaff(getCenter()); + part = staff.getPart(); + part.setName((SentenceInter) null); + } + + case ChordName -> getMembers().forEach( + wInter -> sig.getRelations(wInter, ChordNameRelation.class).forEach( + rel -> sig.removeEdge(rel))); + + case EndingNumber, EndingText -> // + sig.getRelations(this, EndingSentenceRelation.class).forEach( + rel -> sig.removeEdge(rel)); + } + } catch (Exception ex) { + logger.warn("Error in unlink for {} {}", this, ex.toString(), ex); + } + } + //~ Static Methods ----------------------------------------------------------------------------- //--------// @@ -529,7 +669,7 @@ public void setRole (TextRole role) /** * Create a SentenceInter from a TextLine. * - * @param line the OCR'ed text line + * @param line the OCR'd text line * @return the sentence inter */ public static SentenceInter create (TextLine line) diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/TempoInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/TempoInter.java deleted file mode 100644 index 47bdb4b65..000000000 --- a/app/src/main/java/org/audiveris/omr/sig/inter/TempoInter.java +++ /dev/null @@ -1,192 +0,0 @@ -//------------------------------------------------------------------------------------------------// -// // -// T e m p o I n t e r // -// // -//------------------------------------------------------------------------------------------------// -// -// -// Copyright © Audiveris 2023. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify it under the terms of the -// GNU Affero General Public License as published by the Free Software Foundation, either version -// 3 of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with this -// program. If not, see . -//------------------------------------------------------------------------------------------------// -// -package org.audiveris.omr.sig.inter; - -import org.audiveris.omr.glyph.Shape; -import org.audiveris.omr.text.TextLine; -import org.audiveris.omr.text.TextRole; -import static org.audiveris.omr.util.RegexUtil.getGroup; -import static org.audiveris.omr.util.RegexUtil.group; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Class TempoInter is a sentence that specifies a tempo value. - *

- * Its contained words are expected as: - *

    - *
  1. A note symbol (quarter, half, dotted quarter, 8th note). - *
  2. The "=" sign - *
  3. A positive number giving the beats-per-minute value - *
- * Comments: - *
    - *
  1. This is a preliminary version meant for scores where the note symbol is a quarter, - * often OCR'ed as a capital 'J'. - *
  2. Perhaps a better version could recognize a note symbol on the left side of the "=" sign. - * We would need to add the supported note symbols as physical shapes in the {@link Shape} class, - * provide samples and train the glyph classifier on these new shapes. - *
- * - * @author Hervé Bitteur - */ -public class TempoInter - extends SentenceInter -{ - //~ Static fields/initializers ----------------------------------------------------------------- - - private static final Logger logger = LoggerFactory.getLogger(TempoInter.class); - - private static final String NOTE = "note"; - - private static final String EQUAL = "equal"; - - private static final String BPM = "bpm"; - - private static final String spacePat = "\\s*"; - - /** Pattern for note. */ - private static final String notePat = group(NOTE, "[J]"); // To be improved! - - /** Pattern for equal. */ - private static final String equalPat = group(EQUAL, "="); - - /** Pattern for bpm. */ - private static final String bpmPat = group(BPM, "[0-9]+"); - - /** Pattern for the whole tempo instruction. */ - private static final String tempoPat = notePat + spacePat + equalPat + spacePat + bpmPat; - - private static final Pattern tempoPattern = Pattern.compile(tempoPat); - - //~ Instance fields ---------------------------------------------------------------------------- - - final TempoNote note; - - final int bpm; - - //~ Constructors ------------------------------------------------------------------------------- - - /** - * No-arg constructor meant for JAXB. - */ - private TempoInter () - { - this.note = null; - this.bpm = 0; - } - - /** - * Create a new TempoInter object. - * - * @param note the base note (QUARTER only for this version) - * @param bpm the number of beats per minute - */ - private TempoInter (TextLine line, - TempoNote note, - Integer bpm) - { - super(line.getBounds(), line.getGrade(), line.getMeanFont(), TextRole.Tempo); - this.note = note; - this.bpm = bpm; - } - - //~ Methods ------------------------------------------------------------------------------------ - - //--------// - // getBpm // - //--------// - public int getBpm () - { - return bpm; - } - - //---------// - // getNote // - //---------// - public TempoNote getNote () - { - return note; - } - - //-----------// - // internals // - //-----------// - @Override - protected String internals () - { - return new StringBuilder(super.internals())// - .append(" note:").append(note)// - .append(" bpm:").append(bpm)// - .toString(); - } - - //~ Static Methods ----------------------------------------------------------------------------- - - //-------------// - // createValid // - //-------------// - /** - * Try to create a TempoInter instance from the provided text line. - * - * @param line the provided text line - * @return the tempo specification or null if failed - */ - public static TempoInter createValid (TextLine line) - { - - final String str = line.getValue(); - final Matcher matcher = tempoPattern.matcher(str); - - if (matcher.matches()) { - - final TempoNote note = TempoNote.QUARTER; // Imposed for the first version - final String noteStr = getGroup(matcher, NOTE); - final String equalStr = getGroup(matcher, EQUAL); - final String bpmStr = getGroup(matcher, BPM); - - try { - final Integer bpm = Integer.decode(bpmStr); - - return new TempoInter(line, note, bpm); - } catch (NumberFormatException ex) { - return null; - } - } - - return null; - } - - //~ Inner Classes ------------------------------------------------------------------------------ - - public static enum TempoNote - { - QUARTER, - HALF, - DOTTED_QUARTER, - EIGHTH; - } -} diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/TupletInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/TupletInter.java index e9ac39d05..ed507f51a 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/TupletInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/TupletInter.java @@ -296,7 +296,7 @@ public Collection searchUnlinks (SystemInfo system, // createValid // //-------------// /** - * (Try to) create a tuplet inter, checking that there is at least one (head) chord + * Try to create a tuplet inter, checking that there is at least one (head) chord * nearby. * * @param glyph the candidate tuplet glyph diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/WordInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/WordInter.java index 581421d33..f6269ed79 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/WordInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/WordInter.java @@ -38,6 +38,7 @@ import org.audiveris.omr.text.TextWord; import org.audiveris.omr.ui.symbol.MusicFont; import org.audiveris.omr.ui.symbol.ShapeSymbol; +import org.audiveris.omr.ui.symbol.TextFamily; import org.audiveris.omr.ui.symbol.TextFont; import org.audiveris.omr.ui.symbol.TextSymbol; import org.audiveris.omr.util.Jaxb; @@ -48,6 +49,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.font.TextLayout; @@ -63,7 +65,10 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; /** - * Class WordInter represents a text word. + * Class WordInter represents a word made of text characters. + *

+ * By default, a WordInter uses a {@link TextFont}. + * But the {@link BeatUnitInter} subclass uses a {@link MusicFont} instead of a TextFont. *

* The containing {@link SentenceInter} is linked by a {@link Containment} relation. * @@ -82,11 +87,11 @@ public class WordInter // Persistent data //---------------- - /** Word text content. */ + /** Word content. */ @XmlAttribute - protected String value; + protected volatile String value; - /** Detected font attributes. */ + /** Font attributes. */ @XmlAttribute(name = "font") @XmlJavaTypeAdapter(FontInfo.JaxbAdapter.class) protected FontInfo fontInfo; @@ -99,7 +104,7 @@ public class WordInter //~ Constructors ------------------------------------------------------------------------------- /** - * No-arg constructor meant for JAXB. + * No-argument constructor meant for JAXB. */ protected WordInter () { @@ -113,9 +118,9 @@ protected WordInter () * * @param glyph underlying glyph * @param bounds bounding box - * @param shape specific shape (TEXT or LYRICS) + * @param shape specific shape (TEXT or LYRICS or METRONOME) * @param grade quality - * @param value text content + * @param value the word content * @param fontInfo font information * @param location location */ @@ -151,7 +156,7 @@ public WordInter (Shape shape, /** * Creates a new WordInter object, with TEXT shape. * - * @param textWord the OCR'ed text word + * @param textWord the OCR'd text word */ public WordInter (TextWord textWord) { @@ -161,7 +166,7 @@ public WordInter (TextWord textWord) /** * Creates a new WordInter object, with provided shape. * - * @param textWord the OCR'ed text word + * @param textWord the OCR'd text word * @param shape specific shape (TEXT or LYRICS) */ public WordInter (TextWord textWord, @@ -238,6 +243,21 @@ public boolean deriveFrom (ShapeSymbol symbol, return true; } + //------------// + // getAdvance // + //------------// + public int getAdvance () + { + if (value.isEmpty()) { + return 0; + } + + final TextFont font = new TextFont(fontInfo); + final TextLayout layout = font.layout(value); + + return (int) Math.rint(layout.getAdvance()); + } + //-----------// // getBounds // //-----------// @@ -248,6 +268,15 @@ public Rectangle getBounds () return new Rectangle(bounds); } + if (value.isEmpty()) { + return new Rectangle( + bounds = new Rectangle( + (int) Math.rint(location.getX()), + (int) Math.rint(location.getY()), + 0, + 0)); + } + TextFont textFont = new TextFont(fontInfo); TextLayout layout = textFont.layout(value); Rectangle2D rect = layout.getBounds(); @@ -273,12 +302,27 @@ public String getDetails () sb.append("codes[").append(StringUtil.codesOf(value, false)).append(']'); } - if (fontInfo != null) { - sb.append((sb.length() != 0) ? " " : ""); - sb.append(fontInfo.getMnemo()); + return sb.toString(); + } + + //--------------// + // getDimension // + //--------------// + public Dimension getDimension () + { + if (bounds != null) { + return bounds.getSize(); } - return sb.toString(); + if (value.isEmpty()) { + return new Dimension(0, 0); + } + + final TextFont textFont = new TextFont(fontInfo); + final TextLayout layout = textFont.layout(value); + final Rectangle2D rect = layout.getBounds(); + + return new Dimension((int) Math.rint(rect.getWidth()), (int) Math.rint(rect.getHeight())); } //-----------// @@ -340,11 +384,10 @@ public String getValue () @Override protected String internals () { - StringBuilder sb = new StringBuilder(super.internals()); - - sb.append(" \"").append(value).append("\""); - - return sb.toString(); + return new StringBuilder(super.internals()) // + .append(" \"").append(value).append("\"") // + .append((fontInfo != null) ? " font:" + fontInfo.getMnemo() : "") // + .toString(); } //--------// @@ -392,6 +435,14 @@ public void setGlyph (Glyph glyph) // FontInfo? } + //-------------// + // setLocation // + //-------------// + public void setLocation (Point location) + { + this.location = location; + } + //----------// // setValue // //----------// @@ -415,8 +466,8 @@ public void setValue (String value) if (sentence.getRole() == TextRole.PartName) { // Update partRef name as well - final Part part = sentence.getStaff().getPart(); - part.setName(sentence); + final Part thePart = sentence.getStaff().getPart(); + thePart.setName(sentence); } } } @@ -493,17 +544,21 @@ public boolean move (int dx, box.width += dx; if (box.width > 0) { - WordInter word = (WordInter) getInter(); - String value = word.getValue(); - int fontSize = (int) Math.rint( - TextFont.computeFontSize(value, FontInfo.DEFAULT, box.width)); - model.fontInfo = FontInfo.createDefault(fontSize); + final WordInter word = (WordInter) getInter(); + final String value = word.getValue(); + + // Select proper text font (family and size) + final TextFamily textFamily = TextFont.getCurrentFamily(); + TextFont textFont = new TextFont(textFamily.getFontName(), null, 0, 50); + final int fontSize = textFont.computeSize(value, box.getSize()); + model.fontInfo = new FontInfo(fontSize, textFamily.getFontName()); + textFont = textFont.deriveFont((float) fontSize); // Handles - TextFont textFont = new TextFont(model.fontInfo); - TextLayout layout = textFont.layout(value); - Rectangle2D rect = layout.getBounds(); - double y = model.baseLoc.getY() + rect.getY() + (rect.getHeight() / 2); + final TextLayout layout = textFont.layout(value); + final Rectangle2D rect = layout.getBounds(); + final double y = model.baseLoc.getY() + rect.getY() // + + (rect.getHeight() / 2); middle.setLocation(box.x + (rect.getWidth() / 2), y); right.setLocation(box.x + rect.getWidth(), y); } diff --git a/app/src/main/java/org/audiveris/omr/sig/relation/ChordSentenceRelation.java b/app/src/main/java/org/audiveris/omr/sig/relation/ChordSentenceRelation.java index f12dfffb2..843f3303b 100644 --- a/app/src/main/java/org/audiveris/omr/sig/relation/ChordSentenceRelation.java +++ b/app/src/main/java/org/audiveris/omr/sig/relation/ChordSentenceRelation.java @@ -23,6 +23,12 @@ import org.audiveris.omr.constant.ConstantSet; import org.audiveris.omr.sheet.Scale; +import org.audiveris.omr.sheet.SystemInfo; +import org.audiveris.omr.sig.inter.AbstractChordInter; +import org.audiveris.omr.sig.inter.Inter; +import org.audiveris.omr.sig.inter.SentenceInter; + +import org.jgrapht.event.GraphEdgeChangeEvent; import javax.xml.bind.annotation.XmlRootElement; @@ -68,6 +74,29 @@ public static Scale.Fraction getXGapMax () return constants.xGapMax; } + //---------// + // removed // + //---------// + /** + * {@inheritDoc}. + *

+ * If the chord is being removed (and not the sentence), we try to find out a new chord + * to be linked with the orphan sentence. + * + * @param e the relation event. + */ + @Override + public void removed (GraphEdgeChangeEvent e) + { + final AbstractChordInter chord = (AbstractChordInter) e.getEdgeSource(); + final SentenceInter sentence = (SentenceInter) e.getEdgeTarget(); + + if (chord.isRemoved() && !sentence.isRemoved()) { + final SystemInfo system = sentence.getSig().getSystem(); + sentence.link(system); + } + } + //~ Inner Classes ------------------------------------------------------------------------------ //-----------// @@ -77,8 +106,7 @@ private static class Constants extends ConstantSet { - private final Scale.Fraction xGapMax = new Scale.Fraction( - 1.0, - "Maximum horizontal gap between chord & sentence"); + private final Scale.Fraction xGapMax = + new Scale.Fraction(1.0, "Maximum horizontal gap between chord & sentence"); } } diff --git a/app/src/main/java/org/audiveris/omr/sig/relation/Relations.java b/app/src/main/java/org/audiveris/omr/sig/relation/Relations.java index 9372af10f..d4f8206fa 100644 --- a/app/src/main/java/org/audiveris/omr/sig/relation/Relations.java +++ b/app/src/main/java/org/audiveris/omr/sig/relation/Relations.java @@ -46,6 +46,7 @@ import org.audiveris.omr.sig.inter.MarkerInter; import org.audiveris.omr.sig.inter.MeasureCountInter; import org.audiveris.omr.sig.inter.MeasureRepeatInter; +import org.audiveris.omr.sig.inter.MetronomeInter; import org.audiveris.omr.sig.inter.MultipleRestInter; import org.audiveris.omr.sig.inter.OrnamentInter; import org.audiveris.omr.sig.inter.PedalInter; @@ -134,6 +135,7 @@ private static void buildMaps () map(AbstractChordInter.class, ChordPedalRelation.class, PedalInter.class); map(AbstractChordInter.class, ChordTupletRelation.class, TupletInter.class); map(AbstractChordInter.class, ChordWedgeRelation.class, WedgeInter.class); + map(AbstractChordInter.class, ChordSentenceRelation.class, MetronomeInter.class); map(AlterInter.class, AlterHeadRelation.class, HeadInter.class); @@ -221,9 +223,7 @@ private static Set> definedRelationsBetween ( * @return the list of defined relation classes, perhaps empty */ private static Set> definedRelationsFrom ( - // @formatter:off - Class sourceClass) - // @formatter:on + Class sourceClass) { Objects.requireNonNull(sourceClass, "Source class is null"); diff --git a/app/src/main/java/org/audiveris/omr/sig/ui/EditingTask.java b/app/src/main/java/org/audiveris/omr/sig/ui/EditingTask.java index 597bf2cda..e5a763edc 100644 --- a/app/src/main/java/org/audiveris/omr/sig/ui/EditingTask.java +++ b/app/src/main/java/org/audiveris/omr/sig/ui/EditingTask.java @@ -60,11 +60,7 @@ public EditingTask (InterEditor editor, super(editor.getInter().getSig(), editor.getInter(), null, links, "edit"); this.editor = editor; - if (unlinks != null) { - this.unlinks = new ArrayList<>(unlinks); - } else { - this.unlinks = Collections.emptySet(); - } + this.unlinks = (unlinks != null) ? new ArrayList<>(unlinks) : Collections.emptySet(); } //~ Methods ------------------------------------------------------------------------------------ diff --git a/app/src/main/java/org/audiveris/omr/sig/ui/InterBoard.java b/app/src/main/java/org/audiveris/omr/sig/ui/InterBoard.java index 3c1664c45..ceb95e8b3 100644 --- a/app/src/main/java/org/audiveris/omr/sig/ui/InterBoard.java +++ b/app/src/main/java/org/audiveris/omr/sig/ui/InterBoard.java @@ -26,13 +26,17 @@ import org.audiveris.omr.glyph.Shape; import org.audiveris.omr.score.TimeRational; import org.audiveris.omr.sheet.Sheet; +import org.audiveris.omr.sheet.SheetStub; import org.audiveris.omr.sheet.rhythm.Voice; import org.audiveris.omr.sig.inter.AbstractNumberInter; +import org.audiveris.omr.sig.inter.BeatUnitInter; +import org.audiveris.omr.sig.inter.BeatUnitInter.Note; import org.audiveris.omr.sig.inter.ChordNameInter; import org.audiveris.omr.sig.inter.HeadChordInter; import org.audiveris.omr.sig.inter.Inter; import org.audiveris.omr.sig.inter.LyricItemInter; import org.audiveris.omr.sig.inter.LyricLineInter; +import org.audiveris.omr.sig.inter.MetronomeInter; import org.audiveris.omr.sig.inter.SentenceInter; import org.audiveris.omr.sig.inter.SlurInter; import org.audiveris.omr.sig.inter.TimeCustomInter; @@ -46,11 +50,15 @@ import org.audiveris.omr.ui.field.LIntegerField; import org.audiveris.omr.ui.field.LLabel; import org.audiveris.omr.ui.field.LTextField; +import org.audiveris.omr.ui.field.MusicPane; import org.audiveris.omr.ui.selection.EntityListEvent; import org.audiveris.omr.ui.selection.SelectionHint; import org.audiveris.omr.ui.symbol.MusicFamily; import org.audiveris.omr.ui.symbol.ShapeSymbol; +import org.audiveris.omr.ui.symbol.TextFamily; import org.audiveris.omr.ui.util.Panel; +import org.audiveris.omr.ui.util.SeparablePopupMenu; +import static org.audiveris.omr.ui.util.UIPredicates.isContextWanted; import org.jdesktop.application.Application; import org.jdesktop.application.ResourceMap; @@ -64,14 +72,20 @@ import java.awt.BorderLayout; import java.awt.Font; import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.util.Arrays; +import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; +import javax.swing.JMenuItem; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingConstants; @@ -81,6 +95,7 @@ * * @author Hervé Bitteur */ + public class InterBoard extends EntityBoard { @@ -98,34 +113,34 @@ public class InterBoard /** Related sheet. */ private final Sheet sheet; - /** Output : shape icon. */ + /** Output: shape icon. */ private final JLabel shapeIcon = new JLabel(); - /** Output : grade (intrinsic/contextual). */ + /** Output: grade (intrinsic/contextual). */ private final LTextField grade = new LTextField( false, resources.getString("grade.text"), resources.getString("grade.toolTipText")); - /** Output : implicit / manual. */ + /** Output: implicit / manual. */ private final JLabel specific = new JLabel(""); - /** Output : shape. */ + /** Output: shape. */ private final LLabel shapeName = new LLabel("", resources.getString("shapeName.toolTipText")); - /** Output : lyric verse. */ + /** Output: lyric verse. */ private final LIntegerField verse = new LIntegerField( false, resources.getString("verse.text"), resources.getString("verse.toolTipText")); - /** Output : voice. */ + /** Output: voice. */ private final LIntegerField voice = new LIntegerField( false, resources.getString("voice.text"), resources.getString("voice.toolTipText")); - /** Output : lyrics above or below related note line. */ + /** Output: lyrics above or below related note line. */ private final JLabel aboveBelow = new JLabel(); /** To delete/de-assign. */ @@ -156,12 +171,15 @@ public class InterBoard resources.getString("roleCombo.toolTipText"), TextRole.values()); - /** Input/Output : textual content. */ + /** Input/Output: textual content. */ private final LTextField textField = new LTextField( true, resources.getString("textField.text"), resources.getString("textField.toolTipText")); + /** Input/Output: mixed music & text content. */ + private final MusicPane musicPane; + /** Handling of entered / selected values. */ private final Action paramAction; @@ -189,16 +207,11 @@ public InterBoard (Sheet sheet) public InterBoard (Sheet sheet, boolean selected) { - super(Board.INTER, sheet.getInterIndex().getEntityService(), true); + super(Board.INTER, sheet.getInterIndex().getEntityService(), selected); this.sheet = sheet; - edit.addActionListener(this); - tie.addActionListener(this); - paramAction = new ParamAction(); - defineLayout(); - aboveBelow.setToolTipText(resources.getString("aboveBelow.toolTipText")); // Initial status @@ -214,6 +227,20 @@ public InterBoard (Sheet sheet, // Trick for alteration signs adjustFontForAlterations(); + + // MusicPane with sheet music family + final SheetStub stub = sheet.getStub(); + musicPane = new MusicPane( + true, + resources.getString("musicPane.toolTipText"), + stub.getMusicFamily(), + stub.getTextFamily()); + musicPane.addMouseListener(new MusicMouseAdapter()); + + defineLayout(); + + edit.addActionListener(this); + tie.addActionListener(this); } //~ Methods ------------------------------------------------------------------------------------ @@ -248,9 +275,9 @@ public void actionPerformed (ActionEvent e) // adjustFontForAlterations // //--------------------------// /** - * Text items 'shapeName' and 'textField' need to display alteration signs: - * {@link ChordNameInter#FLAT} , {@link ChordNameInter#SHARP} and perhaps - * {@link ChordNameInter#NATURAL}, so they need a font different from default Arial. + * Text items 'shapeName' and 'textField' need to display alteration signs: {@link + * ChordNameInter#FLAT} , {@link ChordNameInter#SHARP} and perhaps {@link + * ChordNameInter#NATURAL}, so they need a font different from default Arial. */ private void adjustFontForAlterations () { @@ -265,12 +292,10 @@ private void adjustFontForAlterations () //--------------// // defineLayout // //--------------// - /** - * Define the layout for InterBoard specific fields. - */ + /** Define the layout for InterBoard specific fields. */ private void defineLayout () { - int r = 1; // ----------------------------- + int r = 1; //----------------------------- // Shape Icon (start, spans several rows) + grade + Deassign button builder.addRaw(shapeIcon).xywh(1, r, 1, 7, CellConstraints.CENTER, CellConstraints.CENTER); @@ -322,15 +347,19 @@ private void defineLayout () builder.addRaw(tiePane).xyw(3, r, 1); // Shape name - builder.addRaw(shapeName.getField()).xyw(7, r, 5); + builder.addRaw(shapeName.getField()).xyw(5, r, 7); r += 2; // -------------------------------- - // Text field + // Text field (exclusive of musicPane & insertNoteButton) textField.getField().setHorizontalAlignment(JTextField.LEFT); textField.setVisible(false); builder.addRaw(textField.getField()).xyw(3, r, 9); + // Music pane (exclusive of textField) + musicPane.setVisible(false); + builder.addRaw(musicPane).xyw(3, r, 9); + // Custom time custom.setVisible(false); builder.addRaw(custom.getLabel()).xyw(1, r, 1); @@ -352,6 +381,12 @@ private void defineLayout () KeyStroke.getKeyStroke("ENTER"), "TextAction"); getComponent().getActionMap().put("TextAction", paramAction); + + // Needed to exit from musicPane when RETURN/ENTER is pressed + musicPane.getInputMap(JComponent.WHEN_FOCUSED).put( + KeyStroke.getKeyStroke("ENTER"), + "MusicAction"); + musicPane.getActionMap().put("MusicAction", paramAction); } //---------------------// @@ -403,8 +438,7 @@ protected ShapeSymbol getTinySymbol (Inter inter) * @param interListEvent the inter list event */ @Override - protected void handleEntityListEvent (EntityListEvent interListEvent) - { + protected void handleEntityListEvent(EntityListEvent interListEvent) { super.handleEntityListEvent(interListEvent); final Inter inter = interListEvent.getEntity(); @@ -417,6 +451,7 @@ protected void handleEntityListEvent (EntityListEvent interListEvent) textField.setVisible(false); textField.setEnabled(false); roleCombo.setVisible(false); + musicPane.setVisible(false); verse.setVisible(false); aboveBelow.setVisible(false); voice.setVisible(false); @@ -436,72 +471,105 @@ protected void handleEntityListEvent (EntityListEvent interListEvent) deassignAction.putValue( Action.NAME, - inter.isRemoved() ? resources.getString("deassign.Action.deleted") - : resources.getString("deassign.Action.text")); + inter.isRemoved() + ? resources.getString("deassign.Action.deleted") + : resources.getString("deassign.Action.text")); + + switch (inter) { + case BeatUnitInter beatUnit -> { + selfUpdatingText = true; - if (inter instanceof WordInter wordInter) { - selfUpdatingText = true; + // The text field is replaced by a JTextPane + musicPane.setText(beatUnit.getValue()); + musicPane.setVisible(true); + musicPane.setEnabled(true); - WordInter word = wordInter; - textField.setText(word.getValue()); - textField.setEnabled(true); - textField.setVisible(true); - selfUpdatingText = false; - } else if (inter instanceof SentenceInter sentenceInter) { - selfUpdatingText = true; + selfUpdatingText = false; + } - SentenceInter sentence = sentenceInter; - textField.setText(sentence.getValue()); - textField.setVisible(true); + case WordInter word -> { + selfUpdatingText = true; - roleCombo.setSelectedItem(sentence.getRole()); - roleCombo.setVisible(true); - roleCombo.setEnabled(true); + textField.setText(word.getValue()); + textField.setEnabled(true); + textField.setVisible(true); + selfUpdatingText = false; + } - if (inter instanceof LyricLineInter lyric) { - verse.setVisible(true); - verse.setValue(lyric.getNumber()); + case MetronomeInter metronome -> { + selfUpdatingText = true; - boolean isAbove = lyric.getStaff().isPointAbove(inter.getCenter()); - aboveBelow.setText(resources.getString(isAbove ? "above" : "below")); - aboveBelow.setVisible(true); + // The text field is replaced by a JTextPane + musicPane.setText(metronome.getDisplayValue()); + musicPane.setVisible(true); + musicPane.setEnabled(true); + + selfUpdatingText = false; + } + + case SentenceInter sentence -> { + selfUpdatingText = true; - LyricItemInter firstNormalItem = lyric.getFirstNormalItem(); + textField.setText(sentence.getValue()); + textField.setVisible(true); - if (firstNormalItem != null) { - HeadChordInter firstChord = firstNormalItem.getHeadChord(); - Voice theVoice = firstChord.getVoice(); + roleCombo.setSelectedItem(sentence.getRole()); + roleCombo.setVisible(true); + roleCombo.setEnabled(true); - if (theVoice != null) { - voice.setVisible(true); - voice.setValue(theVoice.getId()); + if (inter instanceof LyricLineInter lyric) { + verse.setVisible(true); + verse.setValue(lyric.getNumber()); + + boolean isAbove = lyric.getStaff().isPointAbove(inter.getCenter()); + aboveBelow.setText(resources.getString(isAbove ? "above" : "below")); + aboveBelow.setVisible(true); + + LyricItemInter firstNormalItem = lyric.getFirstNormalItem(); + + if (firstNormalItem != null) { + HeadChordInter firstChord = firstNormalItem.getHeadChord(); + Voice theVoice = firstChord.getVoice(); + + if (theVoice != null) { + voice.setVisible(true); + voice.setValue(theVoice.getId()); + } } } + + selfUpdatingText = false; } - selfUpdatingText = false; - } else if (inter instanceof AbstractNumberInter number) { - if (number.getShape() == Shape.NUMBER_CUSTOM) { + case AbstractNumberInter number -> { + if (number.getShape() == Shape.NUMBER_CUSTOM) { + selfUpdatingText = true; + + custom.setText(number.getValue().toString()); + custom.setEnabled(true); + custom.setVisible(true); + + selfUpdatingText = false; + } + } + + case TimeCustomInter timeCustomInter -> { selfUpdatingText = true; - custom.setText(number.getValue().toString()); + custom.setText(timeCustomInter.getTimeRational().toString()); custom.setEnabled(true); custom.setVisible(true); selfUpdatingText = false; } - } else if (inter instanceof TimeCustomInter timeCustomInter) { - selfUpdatingText = true; - - custom.setText(timeCustomInter.getTimeRational().toString()); - custom.setEnabled(true); - custom.setVisible(true); - - selfUpdatingText = false; - } else if (inter instanceof SlurInter slur) { - tie.getField().setSelected(slur.isTie()); - tie.setEnabled(true); - tie.setVisible(true); + + case SlurInter slur -> { + tie.getField().setSelected(slur.isTie()); + tie.setEnabled(true); + tie.setVisible(true); + } + + default -> {} } edit.getField().setSelected(sheet.getSheetEditor().isEditing(inter)); @@ -517,8 +585,10 @@ protected void handleEntityListEvent (EntityListEvent interListEvent) shapeName.setEnabled(inter != null); edit.setEnabled((inter != null) && !inter.isRemoved() && inter.isEditable()); toEnsAction.setEnabled( - (inter != null) && !inter.isRemoved() && (inter.getSig() != null) && (inter - .getEnsemble() != null)); + (inter != null) + && !inter.isRemoved() + && (inter.getSig() != null) + && (inter.getEnsemble() != null)); } //--------------------// @@ -540,7 +610,16 @@ protected void tieActionPerformed (ActionEvent e) @Override public void update () { - shapeIcon.setIcon(getTinySymbol(getSelectedEntity())); + final MusicFamily musicFamily = sheet.getStub().getMusicFamily(); + final TextFamily textFamily = sheet.getStub().getTextFamily(); + + if (musicFamily != cachedMusicFamily || textFamily != cachedTextFamily) { + shapeIcon.setIcon(getTinySymbol(getSelectedEntity())); + musicPane.setFamilies(musicFamily, textFamily); + + cachedMusicFamily = musicFamily; + cachedTextFamily = textFamily; + } } //-----------------------// @@ -619,14 +698,13 @@ public void actionPerformed (ActionEvent e) private class ParamAction extends AbstractAction { - /** - * Method run whenever user presses Return/Enter in one of the parameter fields + * Method run whenever the user presses RETURN/ENTER in one of the parameter fields. * - * @param e unused? + * @param e semantic event */ @Override - public void actionPerformed (ActionEvent e) + public void actionPerformed(ActionEvent e) { // Discard irrelevant action events if (selfUpdatingText) { @@ -636,10 +714,20 @@ public void actionPerformed (ActionEvent e) // Current inter final Inter inter = getSelectedEntity(); - if (inter != null) { - if (inter instanceof WordInter) { - WordInter word = (WordInter) inter; + switch (inter) { + case null -> {} + case BeatUnitInter beatUnit -> { + // Any change, including note symbol insertion + final String newValue = musicPane.getText().trim(); + + if (!beatUnit.getValue().equals(newValue)) { + logger.debug("beatUnit newValue=\"{}\"", newValue); + sheet.getInterController().changeWord(beatUnit, newValue); + } + } + + case WordInter word -> { // Change text value? final String newValue = textField.getText().trim(); @@ -647,20 +735,33 @@ public void actionPerformed (ActionEvent e) logger.debug("Word=\"{}\"", newValue); sheet.getInterController().changeWord(word, newValue); } - } else if (inter instanceof SentenceInter) { - SentenceInter sentence = (SentenceInter) inter; + } + + case MetronomeInter metro -> { + // Any change, including note symbol insertion + final String newValue = musicPane.getText().trim(); + + if (!metro.getValue().equals(newValue)) { + logger.debug("metro newValue=\"{}\"", newValue); + final List newWords = metro.setValue(newValue); + if (newWords != null) { + sheet.getInterController().changeMetronome(metro, newWords); + } + } + } + case SentenceInter sentence -> { // Change sentence role? final TextRole newRole = roleCombo.getSelectedItem(); if (newRole != sentence.getRole()) { logger.debug( - "Sentence=\"{}\" Role={}", - textField.getText().trim(), - newRole); + "Sentence=\"{}\" Role={}", textField.getText().trim(), newRole); sheet.getInterController().changeSentence(sentence, newRole); } - } else if (inter instanceof AbstractNumberInter number) { + } + + case AbstractNumberInter number -> { if (number.getShape() == Shape.NUMBER_CUSTOM) { try { // Change custom value? @@ -668,16 +769,17 @@ public void actionPerformed (ActionEvent e) if (!newValue.equals(number.getValue())) { logger.debug("Custom={}", newValue); + sheet.getInterController().changeNumber(number, newValue); } - } catch (Exception ex) { + } catch (NumberFormatException ex) { logger.warn("Illegal integer value {}", ex.toString()); custom.getField().requestFocusInWindow(); } } - } else if (inter instanceof TimeCustomInter) { - TimeCustomInter timeCustom = (TimeCustomInter) inter; + } + case TimeCustomInter timeCustom -> { try { // Change custom time? TimeRational newTime = TimeRational.decode(custom.getText()); @@ -691,6 +793,63 @@ public void actionPerformed (ActionEvent e) custom.getField().requestFocusInWindow(); } } + + default -> {} + } + } + } + + //-------------------// + // MusicMouseAdapter // + //-------------------// + /** Sub-classed to offer mouse interaction to insert music text. */ + private class MusicMouseAdapter + extends MouseAdapter + { + /** Insert in musicPane the music text that corresponds to the selected beat unit. */ + private final ActionListener shapeListener = (ActionEvent e) -> { + final JMenuItem source = (JMenuItem) e.getSource(); + Note note = Note.valueOf(source.getText()); + logger.debug("shapeListener. note:{}", note); + + final MusicFamily musicFamily = sheet.getStub().getMusicFamily(); + final int[] codes = musicFamily.getSymbols().getCode(note.toShape()); + final String str = new String(codes, 0, codes.length); + + musicPane.insertMusic(str); + }; + + /** + * Triggered when mouse is pressed. + * On a right-click, we display a popup menu with all beat unit symbols to pick from. + * + * @param e mouse event + */ + @Override + public void mousePressed (MouseEvent e) + { + if (isContextWanted(e)) { + final JPopupMenu popup = new SeparablePopupMenu(); + + // A title for this menu + final JMenuItem title = new JMenuItem(resources.getString("insertNote.text")); + title.setToolTipText(resources.getString("insertNote.shortDescription")); + title.setHorizontalAlignment(SwingConstants.CENTER); + title.setEnabled(false); + popup.add(title); + popup.addSeparator(); + + // Populate menu with all possible notes + final MusicFamily musicFamily = sheet.getStub().getMusicFamily(); + for (Note note : Note.values()) { + final JMenuItem item = new JMenuItem( + note.name(), + note.toShape().getDecoratedSymbol(musicFamily)); + item.addActionListener(shapeListener); + popup.add(item); + } + + popup.show(getBody(), e.getX(), e.getY() + musicPane.getHeight()); } } } diff --git a/app/src/main/java/org/audiveris/omr/sig/ui/InterController.java b/app/src/main/java/org/audiveris/omr/sig/ui/InterController.java index 66c5d8daa..0ba1b8748 100644 --- a/app/src/main/java/org/audiveris/omr/sig/ui/InterController.java +++ b/app/src/main/java/org/audiveris/omr/sig/ui/InterController.java @@ -28,6 +28,7 @@ import org.audiveris.omr.glyph.GlyphFactory; import org.audiveris.omr.glyph.Glyphs; import org.audiveris.omr.glyph.Shape; +import static org.audiveris.omr.glyph.Shape.LYRICS; import org.audiveris.omr.glyph.ui.NestView; import static org.audiveris.omr.image.PixelSource.BACKGROUND; import static org.audiveris.omr.image.PixelSource.FOREGROUND; @@ -67,6 +68,7 @@ import org.audiveris.omr.sig.inter.Inters; import org.audiveris.omr.sig.inter.LyricItemInter; import org.audiveris.omr.sig.inter.LyricLineInter; +import org.audiveris.omr.sig.inter.MetronomeInter; import org.audiveris.omr.sig.inter.OctaveShiftInter; import org.audiveris.omr.sig.inter.SentenceInter; import org.audiveris.omr.sig.inter.SlurInter; @@ -252,8 +254,8 @@ protected void publish () /** * Special addition of glyph text. * - * @param glyph to be OCR'ed to text lines and words - * @param shape either TEXT or LYRICS + * @param glyph to be OCR'd into text lines and words + * @param shape not null, either TEXT, LYRICS or METRONOME */ @UIThread private void addText (final Glyph glyph, @@ -290,8 +292,7 @@ protected void build () glyph.getId()); // Convert to absolute lines (and the underlying word glyphs) - final boolean lyrics = (shape == Shape.LYRICS); - final TextBuilder textBuilder = new TextBuilder(system, lyrics); + final TextBuilder textBuilder = new TextBuilder(system, shape); final List glyphLines = textBuilder.processGlyph( buffer, relativeLines, @@ -300,59 +301,65 @@ protected void build () // Generate the sequence of word/line Inter additions for (TextLine line : glyphLines) { logger.debug("line {}", line); + final TextRole role = line.getRole(); + final List wordInters = new ArrayList<>(); + + // Allocate the sentence + final SentenceInter sentence = + switch (shape) { + case LYRICS -> { + // In lyrics role, check if we should join an existing lyric line + SentenceInter s = textBuilder.lookupLyricLine(line.getLocation()); + yield (s != null) ? s: LyricLineInter.create(line); + } + case METRONOME -> MetronomeInter.create(line, system, false, wordInters); + case TEXT -> (role == TextRole.ChordName) // + ? ChordNameInter.create(line) + : SentenceInter.create(line); + default -> throw new IllegalArgumentException(); + }; + + sentence.setManual(true); + sentence.assignStaff(system, line.getLocation()); + seq.add( + new AdditionTask( + sig, + sentence, + line.getBounds(), + Collections.emptySet())); - TextRole role = line.getRole(); - Staff staff; - - SentenceInter sentence = null; - - if (lyrics) { - // In lyrics role, check if we should join an existing lyric line - sentence = textBuilder.lookupLyricLine(line.getLocation()); - } - + // Retrieve the member words (already done for METRONOME) for (TextWord textWord : line.getWords()) { logger.debug("word {}", textWord); - final WordInter word = lyrics ? new LyricItemInter(textWord) - : ((role == TextRole.ChordName) ? ChordNameInter.createValid( - textWord) : new WordInter(textWord)); - - if (sentence != null) { - staff = sentence.getStaff(); - seq.add( - new AdditionTask( - sig, - word, - textWord.getBounds(), - Arrays.asList( - new Link(sentence, new Containment(), false)))); - } else { - sentence = lyrics ? LyricLineInter.create(line) - : ((role == TextRole.ChordName) ? ChordNameInter.create(line) - : SentenceInter.create(line)); - staff = sentence.assignStaff(system, line.getLocation()); - seq.add( - new AdditionTask( - sig, - word, - textWord.getBounds(), - Collections.emptySet())); - seq.add( - new AdditionTask( - sig, - sentence, - line.getBounds(), - Arrays.asList( - new Link(word, new Containment(), true)))); + final WordInter word = switch (shape) { + case LYRICS -> new LyricItemInter(textWord); + case METRONOME -> null; + case TEXT -> (role == TextRole.ChordName) + ? ChordNameInter.createValid(textWord) // May be null + : new WordInter(textWord); + default -> throw new IllegalArgumentException(); + }; + + if (word != null) { + wordInters.add(word); } - - word.setStaff(staff); } - if (sentence != null) { - sentences.add(sentence); - } + // Add and link the member words + wordInters.forEach(w -> { + w.setStaff(sentence.getStaff()); + w.setManual(true); + seq.add( + new AdditionTask( + sig, + w, + w.getBounds(), + Arrays.asList( + new Link(sentence, new Containment(), false)))); + }); + + sentences.add(sentence); } } @@ -382,7 +389,7 @@ protected void publish () public void assignGlyph (Glyph aGlyph, final Shape shape) { - if ((shape == Shape.TEXT) || (shape == Shape.LYRICS)) { + if ((shape == Shape.TEXT) || (shape == Shape.LYRICS) || (shape == Shape.METRONOME)) { addText(aGlyph, shape); return; @@ -483,6 +490,58 @@ public boolean canUndo () return history.canUndo(); } + //-----------------// + // changeMetronome // + //-----------------// + /** + * Change the value of a metronome line. + *

+ * Brute force approach: we keep the metronome inter but change all its members. + * + * @param metro the metronome to modify + * @param newWords the new metronome members + */ + @UIThread + public void changeMetronome (MetronomeInter metro, + List newWords) + { + new CtrlTask(DO, "changeMetronome") + { + @Override + protected void build () + { + final Staff staff = metro.getStaff(); + final SystemInfo system = staff.getSystem(); + final SIGraph sig = system.getSig(); + metro.setManual(true); + + final List oldMembers = metro.getMembers(); + + // First, insert the new members + newWords.forEach(w -> { + w.setManual(true); + seq.add( + new AdditionTask( + sig, + w, + null, + Arrays.asList(new Link(metro, new Containment(), false)))); + }); + + // Second, remove all the old members + oldMembers.forEach(m -> seq.add(new RemovalTask(m))); + + metro.invalidateCache(); + } + + @Override + protected void publish () + { + sheet.getInterIndex().publish(metro); + } + }.execute(); + } + //--------------// // changeNumber // //--------------// @@ -518,8 +577,9 @@ protected void publish () /** * Change the role of a sentence. *

- * When a sentence changes its role between "plain", chordName and lyrics, each of its word - * may have to be converted to a WordInter, ChordNameInter of LyricItemInter. + * When a sentence changes its role between "plain", chordName, lyrics and metronome, + * each of its words may have to be converted to a WordInter, ChordNameInter, LyricItemInter or + * BeaUnitInter. *

* Plus some conversion for the sentence as well. * @@ -626,6 +686,31 @@ protected void build () } } + case Metronome -> { + // Convert to MetronomeInter + final MetronomeInter metro = new MetronomeInter(sentence); + final Staff stf = system.getStaffAtOrBelow(sentence.getCenter()); + metro.setStaff((stf != null) ? stf : sentence.getStaff()); + metro.setManual(true); + seq.add( + new AdditionTask( + sig, + metro, + metro.getBounds(), + metro.searchLinks(system))); + + // Migrate the members from sentence to metro + final List members = sentence.getMembers(); + + // Remove former sentence (and its links to members) + seq.add(new RemovalTask(sentence)); + + for (Inter member : members) { + member.setManual(true); + seq.add(new LinkTask(sig, metro, member, new Containment())); + } + } + default -> { // Convert to SentenceInter if so needed final SentenceInter finalSentence; diff --git a/app/src/main/java/org/audiveris/omr/sig/ui/InterDnd.java b/app/src/main/java/org/audiveris/omr/sig/ui/InterDnd.java index 4df538ac1..ad52392e3 100644 --- a/app/src/main/java/org/audiveris/omr/sig/ui/InterDnd.java +++ b/app/src/main/java/org/audiveris/omr/sig/ui/InterDnd.java @@ -378,10 +378,9 @@ private boolean updateGhost (Point location) { final int staffInterline = staff.getSpecificInterline(); final MusicFamily family = sheet.getStub().getMusicFamily(); - final MusicFont font = (ShapeSet.Heads.contains(ghost.getShape())) ? MusicFont.getHeadFont( - family, - sheet.getScale(), - staffInterline) : MusicFont.getBaseFont(family, staffInterline); + final MusicFont font = (ShapeSet.Heads.contains(ghost.getShape())) // + ? MusicFont.getHeadFont(family, sheet.getScale(), staffInterline) + : MusicFont.getBaseFont(family, staffInterline); return ghost.deriveFrom(symbol, sheet, font, location); } diff --git a/app/src/main/java/org/audiveris/omr/sig/ui/RemovalTask.java b/app/src/main/java/org/audiveris/omr/sig/ui/RemovalTask.java index f4e742b3a..19afebf90 100644 --- a/app/src/main/java/org/audiveris/omr/sig/ui/RemovalTask.java +++ b/app/src/main/java/org/audiveris/omr/sig/ui/RemovalTask.java @@ -37,7 +37,8 @@ public class RemovalTask //~ Constructors ------------------------------------------------------------------------------- /** - * Creates a new RemovalTask object. + * Creates a new RemovalTask object, which saves the current inter links + * for potential undo. * * @param inter the inter to remove */ @@ -52,7 +53,8 @@ public RemovalTask (Inter inter) * Useful when inter is no longer in sig when this task is performed. * * @param inter the inter to remove - * @param links the inter current links + * @param links the inter links to save, to be used if/when the removal is undone. + * If null, use all the current relations when the inter is about to be removed. */ public RemovalTask (Inter inter, Collection links) diff --git a/app/src/main/java/org/audiveris/omr/sig/ui/ShapeBoard.java b/app/src/main/java/org/audiveris/omr/sig/ui/ShapeBoard.java index ed2e45a84..bf5f5b065 100644 --- a/app/src/main/java/org/audiveris/omr/sig/ui/ShapeBoard.java +++ b/app/src/main/java/org/audiveris/omr/sig/ui/ShapeBoard.java @@ -233,12 +233,6 @@ public void mouseClicked (MouseEvent e) /** Cached list of HeadsAndDot shapes, if any. To trigger board update only when needed. */ private List cachedHeads; - /** Cached music font family, if any. To trigger board symbols update only when needed. */ - private MusicFamily cachedMusicFamily; - - /** Cached text font family, if any. To trigger board symbols update only when needed. */ - private TextFamily cachedTextFamily; - //~ Constructors ------------------------------------------------------------------------------- /** @@ -418,12 +412,16 @@ private Panel buildSetPanel (ShapeSet set) new HeadButtons().build(panel, filtered); } else if (set == ShapeSet.Barlines) { new ButtonsTable(8).build(panel, filtered); + } else if (set == ShapeSet.BeamsEtc) { + new ButtonsTable(6).build(panel, filtered); } else if (set == ShapeSet.ClefsAndShifts) { new ButtonsTable(5).build(panel, filtered); } else if (set == ShapeSet.Dynamics) { new ButtonsTable(6).build(panel, filtered); } else if (set == ShapeSet.Flags) { new ButtonsTable(7).build(panel, filtered); + } else if (set == ShapeSet.GraceAndOrnaments) { + new ButtonsTable(4).build(panel, filtered); } else if (set == ShapeSet.Rests) { new ButtonsTable(6).build(panel, filtered); } else if (set == ShapeSet.Times) { @@ -901,9 +899,12 @@ private static void populateCharMaps () shapeMap.put("" + c + '4', Shape.QUARTER_REST); shapeMap.put("" + c + '8', Shape.EIGHTH_REST); - setMap.put(c = 'p', ShapeSet.Physicals); + setMap.put(c = 't', ShapeSet.Texts); shapeMap.put("" + c + 'l', Shape.LYRICS); shapeMap.put("" + c + 't', Shape.TEXT); + shapeMap.put("" + c + 'm', Shape.METRONOME); + + setMap.put(c = 'p', ShapeSet.Physicals); shapeMap.put("" + c + 'a', Shape.SLUR_ABOVE); shapeMap.put("" + c + 'b', Shape.SLUR_BELOW); shapeMap.put("" + c + 's', Shape.STEM); diff --git a/app/src/main/java/org/audiveris/omr/sig/ui/WordValueTask.java b/app/src/main/java/org/audiveris/omr/sig/ui/WordValueTask.java index fe4f1c76a..c79e70859 100644 --- a/app/src/main/java/org/audiveris/omr/sig/ui/WordValueTask.java +++ b/app/src/main/java/org/audiveris/omr/sig/ui/WordValueTask.java @@ -24,7 +24,7 @@ import org.audiveris.omr.sig.inter.WordInter; /** - * Class WordValueTask handle the text value update of a word. + * Class WordValueTask handles the text value update of a word. * * @author Hervé Bitteur */ diff --git a/app/src/main/java/org/audiveris/omr/sig/ui/resources/InterBoard.properties b/app/src/main/java/org/audiveris/omr/sig/ui/resources/InterBoard.properties index b9f434752..d8bc6fd66 100644 --- a/app/src/main/java/org/audiveris/omr/sig/ui/resources/InterBoard.properties +++ b/app/src/main/java/org/audiveris/omr/sig/ui/resources/InterBoard.properties @@ -12,6 +12,9 @@ deassign.Action.text = Deassign deassign.Action.shortDescription = Deassign inter deassign.Action.deleted = deleted +insertNote.text = Insert Note +insertNote.shortDescription = Insert a note symbol as beat unit + aboveBelow.toolTipText = Lyric above or below note line above = above below = below @@ -22,13 +25,18 @@ edit.toolTipText = Set inter into edit mode grade.text = Grade grade.toolTipText = Intrinsic / Contextual +musicPane.toolTipText = This is a pane for mixed music & text.\ +
Use a right-click for note insertion.\ +
Press ENTER to commit your modifications. + roleCombo.text = Role roleCombo.toolTipText = Role of the Sentence shapeName.toolTipText = Shape for this interpretation textField.text = Text -textField.toolTipText = Content of textual item +textField.toolTipText = Content of textual item.\ +
Press ENTER to commit your modifications. tie.text = Tie tie.toolTipText = Set/Unset slur as tie @@ -37,7 +45,7 @@ time.text = Custom time.toolTipText = Custom num/den time ToEnsembleAction.text = To Ensemble -ToEnsembleAction.shortDescription = Move to containing ensemble +ToEnsembleAction.shortDescription = Move to the containing ensemble verse.text = Verse verse.toolTipText = Lyric verse number diff --git a/app/src/main/java/org/audiveris/omr/sig/ui/resources/InterBoard_fr.properties b/app/src/main/java/org/audiveris/omr/sig/ui/resources/InterBoard_fr.properties index ab04fd325..1bd804fbd 100644 --- a/app/src/main/java/org/audiveris/omr/sig/ui/resources/InterBoard_fr.properties +++ b/app/src/main/java/org/audiveris/omr/sig/ui/resources/InterBoard_fr.properties @@ -12,6 +12,9 @@ deassign.Action.text = Supprimer deassign.Action.shortDescription = Supprimer cette interpr\u00e9tation deassign.Action.deleted = supprim\u00e9 +insertNote.text = Ins\u00e9rer une Note +insertNote.shortDescription = Insertion d'une note comme unit\u00e9 de temps + aboveBelow.toolTipText = Paroles au-dessus ou au-dessous des notes above = dessus below = dessous @@ -22,13 +25,18 @@ edit.toolTipText = Ajuster cette interpr\u00e9tation grade.text = Qualit\u00e9 grade.toolTipText = Intrins\u00e8que / Contextuelle +musicPane.toolTipText = Ceci est un panneau de musique et texte m\u00e9l\u00e9s.\ +
Utiliser un clic droit pour ins\u00e9rer une note.\ +
Appuyer sur ENTER pour valider vos modifications. + roleCombo.text = R\u00f4le roleCombo.toolTipText = R\u00f4le de cette phrase shapeName.toolTipText = Classe pour cette interpr\u00e9tation textField.text = Texte -textField.toolTipText = Contenu de cet \u00e9l\u00e9ment de texte +textField.toolTipText = Contenu de cet \u00e9l\u00e9ment de texte.\ +
Appuyer sur ENTER pour valider vos modifications. tie.text = Tenue tie.toolTipText = Liaison de prolongation diff --git a/app/src/main/java/org/audiveris/omr/text/FontInfo.java b/app/src/main/java/org/audiveris/omr/text/FontInfo.java index 217c431aa..f77146196 100644 --- a/app/src/main/java/org/audiveris/omr/text/FontInfo.java +++ b/app/src/main/java/org/audiveris/omr/text/FontInfo.java @@ -72,7 +72,7 @@ public class FontInfo //~ Constructors ------------------------------------------------------------------------------- /** - * Creates a new FontInfo object. + * Creates a new FontInfo object. * * @param isBold True if the font is bold * @param isItalic True if the font is italic @@ -102,6 +102,22 @@ public FontInfo (boolean isBold, this.fontName = fontName; } + /** + * Creates a new FontInfo object, with only bold and italic possible attributes. + * + * @param isBold True if the font is bold + * @param isItalic True if the font is italic + * @param pointsize font size in points + * @param fontName font name + */ + public FontInfo (boolean isBold, + boolean isItalic, + int pointsize, + String fontName) + { + this(isBold, isItalic, false, false, false, false, pointsize, fontName); + } + /** * Create a new FontInfo from another one and a specific point size. * @@ -123,7 +139,8 @@ public FontInfo (FontInfo org, } /** - * Creates a new FontInfo object, with no attribute set, just point size and font name. + * Creates a new FontInfo object, with no attribute set, + * just point size and font name. * * @param pointsize font size in points * @param fontName font name diff --git a/app/src/main/java/org/audiveris/omr/text/OCR.java b/app/src/main/java/org/audiveris/omr/text/OCR.java index 8e93d2ab2..af42c32d4 100644 --- a/app/src/main/java/org/audiveris/omr/text/OCR.java +++ b/app/src/main/java/org/audiveris/omr/text/OCR.java @@ -50,7 +50,7 @@ public interface OCR Set getLanguages (); /** - * Report the minimum confidence for an OCR'ed item. + * Report the minimum confidence for an OCR'd item. * * @return minimum confidence value */ diff --git a/app/src/main/java/org/audiveris/omr/text/OcrUtil.java b/app/src/main/java/org/audiveris/omr/text/OcrUtil.java index 0503fe77f..a2237c988 100644 --- a/app/src/main/java/org/audiveris/omr/text/OcrUtil.java +++ b/app/src/main/java/org/audiveris/omr/text/OcrUtil.java @@ -26,6 +26,7 @@ import org.audiveris.omr.sheet.Sheet; import org.audiveris.omr.text.OCR.LayoutMode; import org.audiveris.omr.text.tesseract.TesseractOCR; +import org.audiveris.omr.ui.symbol.TextFamily; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -99,7 +100,7 @@ public static List scan (BufferedImage image, final int height = image.getHeight(); final Point origin = new Point(0, 0); - // Add some white some white margin around the image? + // Add some white margin around the image? final int margin = constants.whiteMarginAdded.getValue(); final BufferedImage bi; @@ -124,20 +125,24 @@ public static List scan (BufferedImage image, final List lines = ocr.recognize(sheet, bi, origin, language, layoutMode, label); if (lines == null) { - logger.info("No OCR'ed lines"); + logger.info("No OCR'd lines"); return Collections.emptyList(); } - + Collections.sort(lines, TextLine.byOrdinate(sheet.getSkew())); + final TextFamily family = sheet.getStub().getTextFamily(); + lines.forEach(line -> line.getWords().forEach(word -> word.adjustFontSize(family))); + if (logger.isDebugEnabled()) { - TextLine.dump("Raw OCR'ed lines:", lines, constants.dumpWords.isSet()); + TextLine.dump("Raw OCR'd lines:", lines, constants.dumpWords.isSet()); } return lines; } //~ Inner Classes ------------------------------------------------------------------------------ + //-----------// // Constants // //-----------// diff --git a/app/src/main/java/org/audiveris/omr/text/SheetScanner.java b/app/src/main/java/org/audiveris/omr/text/SheetScanner.java index e208758c6..2e0900172 100644 --- a/app/src/main/java/org/audiveris/omr/text/SheetScanner.java +++ b/app/src/main/java/org/audiveris/omr/text/SheetScanner.java @@ -158,7 +158,7 @@ protected void renderItems (Graphics2D g) /** * Get a clean image of whole sheet and run OCR on it. * - * @return the list of OCR'ed lines found and filtered + * @return the list of OCR'd lines found and filtered */ public List scanSheet () { diff --git a/app/src/main/java/org/audiveris/omr/text/TextBuilder.java b/app/src/main/java/org/audiveris/omr/text/TextBuilder.java index 3679da90a..ee458c679 100644 --- a/app/src/main/java/org/audiveris/omr/text/TextBuilder.java +++ b/app/src/main/java/org/audiveris/omr/text/TextBuilder.java @@ -25,6 +25,7 @@ import org.audiveris.omr.constant.ConstantSet; import org.audiveris.omr.glyph.Glyph; import org.audiveris.omr.glyph.GlyphIndex; +import org.audiveris.omr.glyph.Shape; import org.audiveris.omr.glyph.dynamic.CompoundFactory; import org.audiveris.omr.glyph.dynamic.CompoundFactory.CompoundConstructor; import org.audiveris.omr.glyph.dynamic.SectionCompound; @@ -47,9 +48,12 @@ import org.audiveris.omr.sig.inter.ChordNameInter; import org.audiveris.omr.sig.inter.LyricItemInter; import org.audiveris.omr.sig.inter.LyricLineInter; +import org.audiveris.omr.sig.inter.MetronomeInter; import org.audiveris.omr.sig.inter.SentenceInter; -import org.audiveris.omr.sig.inter.TempoInter; import org.audiveris.omr.sig.inter.WordInter; +import static org.audiveris.omr.text.TextRole.ChordName; +import static org.audiveris.omr.text.TextRole.Lyrics; +import static org.audiveris.omr.text.TextRole.Metronome; import org.audiveris.omr.text.tesseract.TesseractOCR; import org.audiveris.omr.util.Navigable; import org.audiveris.omr.util.Pair; @@ -83,17 +87,21 @@ * Class TextBuilder works at system level, providing features to check, build * and reorganize text items, including interacting with the OCR engine. *

- * This builder can operate in 3 different modes: + * This builder can operate in 4 different modes: *

    *
  1. Free mode: Engine mode, text role can be any role, determined by heuristics. *
    - * manualLyrics == null; + * shape == null; *
  2. Manual as lyrics: Manual mode, for which text role is imposed as lyrics. *
    - * manualLyrics == true; - *
  3. Manual as non-lyrics: Manual mode, for which text role is imposed as non lyrics. + * shape == LYRICS + *
  4. Manual as metronome: Manual mode, for which text role is imposed as metronome. *
    - * manualLyrics == false; + * shape == METRONOME + *
  5. Manual as plain text: Manual mode, for which text role can be anything, except lyrics + * and metronome. + *
    + * shape == TEXT *
* * @author Hervé Bitteur @@ -125,8 +133,8 @@ public class TextBuilder /** Set of text lines. */ private final Set textLines = new LinkedHashSet<>(); - /** Manual mode. */ - private final Boolean manualLyrics; + /** Shape specification. */ + private final Shape shape; /** Maximum acceptable vertical shift between line chunks. */ private final int maxLineDy; @@ -136,32 +144,17 @@ public class TextBuilder //~ Constructors ------------------------------------------------------------------------------- - /** - * Creates a new TextBuilder object in engine mode (TEXTS step). - * - * @param system the related system - */ - public TextBuilder (SystemInfo system) - { - this(system, null); - } - /** * Creates a new TextBuilder object, in either engine or manual mode. - *

- * In engine mode, manualLyrics is null, leaving lines roles fully open. - *

- * In manual mode, the user has selected either "lyrics" (which forces lyrics mode) or "text" - * (which leaves the role open to anything but lyrics). * - * @param system the related system - * @param manualLyrics null for any role, true for lyrics, false for any role but lyrics + * @param system the related system + * @param shape null for any role (engine) or specifically TEXT, LYRICS, METRONOME (manual) */ public TextBuilder (SystemInfo system, - Boolean manualLyrics) + Shape shape) { this.system = system; - this.manualLyrics = manualLyrics; + this.shape = shape; sheet = system.getSheet(); scale = sheet.getScale(); @@ -209,26 +202,25 @@ private void adjustLines (List lines) } } - //--------------// - // createInters // - //--------------// + //--------------------// + // createSystemInters // + //--------------------// /** - * Allocate corresponding inters based on text role. - *

    - *
  • For any role other than Lyrics, a plain Sentence is created for each text line.
  • - *
  • For Lyrics role, a specific LyricLine (sub-class of Sentence) is created.
  • - *
+ * Allocate corresponding inters based on the role for each text line. */ - private void createInters () + private void createSystemInters () { final SIGraph sig = system.getSig(); for (TextLine line : textLines) { + final List createdWords = new ArrayList<>(); final TextRole role = line.getRole(); - final SentenceInter sentence = (role == TextRole.Lyrics) ? LyricLineInter.create(line) - : (role == TextRole.ChordName) ? ChordNameInter.create(line) - : (role == TextRole.Tempo) ? TempoInter.createValid(line) - : SentenceInter.create(line); + final SentenceInter sentence = switch (role) { + case Lyrics -> LyricLineInter.create(line); + case ChordName -> ChordNameInter.create(line); + case Metronome -> MetronomeInter.create(line, system, false, createdWords); + default -> SentenceInter.create(line); + }; // Related staff (can still be modified later) Staff staff = line.getStaff(); @@ -240,21 +232,27 @@ private void createInters () } if (staff != null) { - // Populate sig - sig.addVertex(sentence); - - // Link sentence and words + // Create words for (TextWord word : line.getWords()) { - final WordInter wordInter = (role == TextRole.Lyrics) ? new LyricItemInter(word) - : ((role == TextRole.ChordName) ? ChordNameInter.createValid(word) - : new WordInter(word)); - - if (wordInter != null) { - wordInter.setStaff(staff); - sig.addVertex(wordInter); - sentence.addMember(wordInter); + final WordInter w = switch (role) { + case Lyrics -> new LyricItemInter(word); + case ChordName -> ChordNameInter.createValid(word); + case Metronome -> null; // Already performed at MetronomeInter creation + default -> new WordInter(word); + }; + + if (w != null) { + createdWords.add(w); } } + + // Populate sig + sig.addVertex(sentence); + createdWords.forEach(w -> { + w.setStaff(sentence.getStaff()); + sig.addVertex(w); + sentence.addMember(w); + }); } } } @@ -366,7 +364,7 @@ private List getGutterLines (List lines, // getSections // //-------------// /** - * Build all the system sections that could be part of OCR'ed items. + * Build all the system sections that could be part of OCR'd items. * * @param buffer the pixel buffer used by OCR * @param lines the text lines kept @@ -393,12 +391,12 @@ private List
getSections (ByteProcessor buffer, // getSystemValidLines // //---------------------// /** - * Among the lines OCR'ed from whole sheet, select the ones that could belong to our + * Among the lines OCR'd from whole sheet, select the ones that could belong to our * system and considered as valid. *

* Sheet lines are deep-copied to system lines. * - * @param sheetLines sheet collection of raw OCR'ed lines + * @param sheetLines sheet collection of raw OCR'd lines * @return the relevant system lines, duly validated */ private List getSystemValidLines (List sheetLines) @@ -462,9 +460,13 @@ private void guessLongRoles (List longLines) private void guessRole (TextLine line) { try { - TextRole role = (isManual() && manualLyrics) ? TextRole.Lyrics - : TextRole.guessRole(line, system, manualLyrics == null); - line.setRole(role); + line.setRole(switch (shape) { + case null -> TextRole.guess(line, system, true, true); // Engine mode + case LYRICS -> TextRole.Lyrics; + case METRONOME -> TextRole.Metronome; + case TEXT -> TextRole.guess(line, system, false, false); + default -> null; // To keep the compiler happy. + }); } catch (Exception ex) { logger.warn("Error in guessRole for {} {}", line, ex.toString(), ex); } @@ -480,7 +482,7 @@ private void guessRole (TextLine line) */ private boolean isManual () { - return manualLyrics != null; + return shape != null; } //-----------------// @@ -556,12 +558,15 @@ private void mapGlyphs (List lines, for (TextWord word : sortedWords) { // Isolate proper word glyph from its enclosed sections - SortedSet

wordSections = - retrieveSections(word.getChars(), allSections, offset); + SortedSet
wordSections = retrieveSections( + word.getChars(), + allSections, + offset); if (!wordSections.isEmpty()) { - SectionCompound compound = - CompoundFactory.buildCompound(wordSections, constructor); + SectionCompound compound = CompoundFactory.buildCompound( + wordSections, + constructor); Glyph rel = compound.toGlyph(null); Glyph wordGlyph = glyphIndex.registerOriginal( new Glyph(rel.getLeft() + dx, rel.getTop() + dy, rel.getRunTable())); @@ -707,7 +712,7 @@ private TextLine mergeLines (List lines) /** * Gather the provided raw lines into long lines, based on their ordinate. * - * @param rawLines collection of raw OCR'ed lines + * @param rawLines collection of raw OCR'd lines * @return resulting long lines */ private List mergeRawLines (List rawLines) @@ -902,11 +907,10 @@ private void partitionPartLines (List longLines) // processGlyph // //--------------// /** - * Retrieve the glyph lines, among the lines OCR'ed from the glyph buffer. + * Retrieve the glyph lines, among the lines OCR'd from the glyph buffer. *

* This method is called in manual mode only. - * Boolean 'manualLyrics' is not null and is either true for lyrics imposed or false for lyrics - * forbidden. + * The 'shape' element is not null and is LYRICS, METRONOME or TEXT * * @param buffer the (glyph) pixel buffer * @param glyphLines the glyph raw OCR lines, relative to buffer origin @@ -918,7 +922,7 @@ public List processGlyph (ByteProcessor buffer, Point offset) { // Pre-assign text role as lyrics? - if (isManual() && manualLyrics) { + if (shape == Shape.LYRICS) { for (TextLine line : glyphLines) { line.setRole(TextRole.Lyrics); // Here, lyrics role is certain! } @@ -941,13 +945,13 @@ public List processGlyph (ByteProcessor buffer, // processSystem // //---------------// /** - * Retrieve the system-relevant lines, among all the lines OCR'ed at sheet level. + * Retrieve the system-relevant lines, among all the lines OCR'd at sheet level. *

* We may have lines that belong to the system above and lines that belong to the system below. * We try to filter them out immediately. * * @param buffer the (sheet) pixel buffer - * @param sheetLines the sheet raw OCR'ed lines + * @param sheetLines the sheet raw OCR'd lines */ public void processSystem (ByteProcessor buffer, List sheetLines) @@ -998,7 +1002,7 @@ public void processSystem (ByteProcessor buffer, // - Sentences of Words (or of one ChordName) // - LyricLines of LyricItems watch.start("createInters"); - createInters(); + createSystemInters(); watch.start("numberLyricLines()"); system.numberLyricLines(); @@ -1120,6 +1124,7 @@ private void purgeInvalidLines (List lines) *

  • A too small inter-word gap triggers a word merge
  • *
  • For lyrics, a separation character triggers a word split into syllables
  • * + * If this TextBuilder operates with a null or TEXT shape, the role of each line is guessed. * * @param longLines the lines to process * @return the sequence of re-composed lines @@ -1195,8 +1200,8 @@ private SortedSet
    retrieveSections (List chars, Point offset) { final SortedSet
    wordSections = new TreeSet<>(Section.byFullAbscissa); - final CompoundConstructor constructor = - new SectionCompound.Constructor(sheet.getInterline()); + final CompoundConstructor constructor = new SectionCompound.Constructor( + sheet.getInterline()); final int dx = (offset != null) ? offset.x : 0; final int dy = (offset != null) ? offset.y : 0; @@ -1393,11 +1398,12 @@ public static boolean isMainlyItalic (TextLine line) private static class Constants extends ConstantSet { + private final Constant.Boolean printWatch = new Constant.Boolean( + false, + "Should we print out the stop watch?"); - private final Constant.Boolean printWatch = - new Constant.Boolean(false, "Should we print out the stop watch?"); - - private final Scale.Fraction maxLineDy = - new Scale.Fraction(1.0, "Max vertical gap between two line chunks"); + private final Scale.Fraction maxLineDy = new Scale.Fraction( + 1.0, + "Max vertical gap between two line chunks"); } } diff --git a/app/src/main/java/org/audiveris/omr/text/TextLine.java b/app/src/main/java/org/audiveris/omr/text/TextLine.java index 311c50770..0406d5f25 100644 --- a/app/src/main/java/org/audiveris/omr/text/TextLine.java +++ b/app/src/main/java/org/audiveris/omr/text/TextLine.java @@ -31,6 +31,8 @@ import org.audiveris.omr.sig.inter.ChordNameInter; import org.audiveris.omr.text.WordScanner.OcrScanner; import org.audiveris.omr.text.tesseract.TesseractOCR; +import org.audiveris.omr.ui.symbol.OmrFont; +import org.audiveris.omr.ui.symbol.TextFont; import org.audiveris.omr.util.VerticalSide; import org.slf4j.Logger; @@ -45,6 +47,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; /** * Class TextLine defines a non-mutable structure to report all information on @@ -457,10 +460,13 @@ public FontInfo getMeanFont () // Discard one-char words, they are not reliable if (length > 1) { + final FontInfo info = word.getFontInfo(); + final OmrFont font = new TextFont(info); + final int fontSize = font.computeSize( + word.getValue(), + word.getBounds().getSize()); + sizeTotal += (fontSize * length); charCount += length; - sizeTotal += (word.getPreciseFontSize() * length); - - FontInfo info = word.getFontInfo(); if (info.isBold) { boldCount += length; @@ -536,24 +542,7 @@ public TextRole getRole () @Override public String getValue () { - StringBuilder sb = null; - - // Use each word value - for (TextWord word : words) { - String str = word.getValue(); - - if (sb == null) { - sb = new StringBuilder(str); - } else { - sb.append(" ").append(str); - } - } - - if (sb == null) { - return ""; - } else { - return sb.toString(); - } + return words.stream().map(w -> w.getValue()).collect(Collectors.joining(" ")); } //---------------// @@ -938,7 +927,7 @@ public static Comparator byAbscissa (final Skew skew) { return (TextLine line1, TextLine line2) -> Double.compare( - line1.getDskOrigin(skew).getX(), + line1.getDskOrigin(skew).getX(), // line2.getDskOrigin(skew).getX()); } @@ -952,7 +941,7 @@ public static Comparator byOrdinate (final Skew skew) { return (TextLine line1, TextLine line2) -> Double.compare( - line1.getDskOrigin(skew).getY(), + line1.getDskOrigin(skew).getY(), // line2.getDskOrigin(skew).getY()); } @@ -986,11 +975,11 @@ private static class Constants { private final Scale.Fraction maxFontSize = new Scale.Fraction( - 5.0, + 4.0, "Maximum font size with respect to interline"); private final Scale.Fraction minFontSize = new Scale.Fraction( - 1.25, + 1.0, "Minimum font size with respect to interline"); private final Scale.Fraction maxTitleFontSize = new Scale.Fraction( diff --git a/app/src/main/java/org/audiveris/omr/text/TextRole.java b/app/src/main/java/org/audiveris/omr/text/TextRole.java index 3054b6b39..ba9f96475 100644 --- a/app/src/main/java/org/audiveris/omr/text/TextRole.java +++ b/app/src/main/java/org/audiveris/omr/text/TextRole.java @@ -31,7 +31,7 @@ import org.audiveris.omr.sheet.Sheet; import org.audiveris.omr.sheet.Staff; import org.audiveris.omr.sheet.SystemInfo; -import org.audiveris.omr.sig.inter.TempoInter; +import org.audiveris.omr.sig.inter.MetronomeInter; import static org.audiveris.omr.util.HorizontalSide.LEFT; import static org.audiveris.omr.util.HorizontalSide.RIGHT; @@ -78,8 +78,8 @@ public enum TextRole EndingNumber, /** Ending text, when different from number. */ EndingText, - /** Tempo indication, such as "quarter = value". */ - Tempo; + /** Metronome mark, such as "quarter = value". */ + Metronome; private static final Constants constants = new Constants(); @@ -103,30 +103,32 @@ public boolean isCreator () //~ Static Methods ----------------------------------------------------------------------------- - //-----------// - // guessRole // - //-----------// + //-------// + // guess // + //-------// /** * Try to infer the role of this textual item. *

    * For the time being, this is a simple algorithm based on sentence location within the sheet, * augmented by valid chord name, etc. * - * @param line the sentence - * @param system the containing system - * @param lyricsAllowed false for manual mode, forbidding lyrics + * @param line the sentence + * @param system the containing system + * @param lyricsAllowed false for manual mode, forbidding lyrics + * @param metronomeAllowed false for manual mode, forbidding metronome * @return the role information inferred for the provided sentence glyph */ - public static TextRole guessRole (TextLine line, - SystemInfo system, - boolean lyricsAllowed) + public static TextRole guess (TextLine line, + SystemInfo system, + boolean lyricsAllowed, + boolean metronomeAllowed) { if (line == null) { return null; } if (line.isVip()) { - logger.info("TextRoleInfo. guessRole for {}", line.getValue()); + logger.info("TextRole.guess for {}", line.getValue()); } final Rectangle box = line.getBounds(); @@ -179,8 +181,8 @@ public static TextRole guessRole (TextLine line, // Right aligned with staves (and starts after staff center abscissa) ? int maxRightDx = scale.toPixels(constants.maxRightDx); - boolean rightAligned = - (Math.abs(right.x - system.getRight()) <= maxRightDx) && (staffMidX <= left.x); + boolean rightAligned = (Math.abs(right.x - system.getRight()) <= maxRightDx) + && (staffMidX <= left.x); // Short Sentence? int maxShortLength = scale.toPixels(constants.maxShortLength); @@ -199,7 +201,7 @@ public static TextRole guessRole (TextLine line, // Decisions ... switch (systemPosition) { - case ABOVE_STAVES: // Title, Number, Creator, Direction, ChordName, Lyrics above staff + case ABOVE_STAVES: // Title, Number, Creator, Direction, ChordName, Lyrics, Metronome if (tinySentence) { if (isAllChords) { return ChordName; @@ -210,19 +212,20 @@ public static TextRole guessRole (TextLine line, if (firstSystem) { if (leftOfStaves) { - return CreatorLyricist; + if (metronomeAllowed && MetronomeInter.isLikely(line)) { + return Metronome; + } else { + return CreatorLyricist; + } } else if (rightAligned) { return CreatorComposer; + } else if (metronomeAllowed && MetronomeInter.isLikely(line)) { + return Metronome; } else if (closeToStaff) { if (isAllChords) { return ChordName; } else { - final TempoInter tempo = TempoInter.createValid(line); - if (tempo != null) { - return Tempo; - } else { - return Direction; - } + return Direction; } } else if (pageCentered) { // Title, Number if (highText) { @@ -257,6 +260,8 @@ public static TextRole guessRole (TextLine line, logger.debug("Abnormal part name: {}", line); } } + } else if (metronomeAllowed && MetronomeInter.isLikely(line)) { + return Metronome; } else if (lyricsAllowed // && hasVowel // ///&& !isMainlyItalic // @@ -308,26 +313,32 @@ private static class Constants extends ConstantSet { - private final Scale.Fraction maxRightDx = - new Scale.Fraction(2, "Maximum horizontal distance on the right end of the staff"); + private final Scale.Fraction maxRightDx = new Scale.Fraction( + 2, + "Maximum horizontal distance on the right end of the staff"); - private final Scale.Fraction maxCenterDx = - new Scale.Fraction(30, "Maximum horizontal distance around center of page"); + private final Scale.Fraction maxCenterDx = new Scale.Fraction( + 30, + "Maximum horizontal distance around center of page"); - private final Scale.Fraction maxShortLength = - new Scale.Fraction(35, "Maximum length for a short sentence (no lyrics)"); + private final Scale.Fraction maxShortLength = new Scale.Fraction( + 35, + "Maximum length for a short sentence (no lyrics)"); - private final Scale.Fraction maxTinyLength = - new Scale.Fraction(2.5, "Maximum length for a tiny sentence (no lyrics)"); + private final Scale.Fraction maxTinyLength = new Scale.Fraction( + 2.5, + "Maximum length for a tiny sentence (no lyrics)"); private final Scale.Fraction maxStaffDy = new Scale.Fraction( 7, "Maximum distance above staff for a direction (or lyrics above staves)"); - private final Scale.Fraction minStaffDy = - new Scale.Fraction(6, "Minimum distance below staff for a copyright"); + private final Scale.Fraction minStaffDy = new Scale.Fraction( + 6, + "Minimum distance below staff for a copyright"); - private final Scale.Fraction minTitleHeight = - new Scale.Fraction(2, "Minimum height for a title text"); + private final Scale.Fraction minTitleHeight = new Scale.Fraction( + 2, + "Minimum height for a title text"); } } diff --git a/app/src/main/java/org/audiveris/omr/text/TextWord.java b/app/src/main/java/org/audiveris/omr/text/TextWord.java index da63b1551..e58ad6f3c 100644 --- a/app/src/main/java/org/audiveris/omr/text/TextWord.java +++ b/app/src/main/java/org/audiveris/omr/text/TextWord.java @@ -28,6 +28,7 @@ import org.audiveris.omr.sheet.Scale; import org.audiveris.omr.sheet.Sheet; import org.audiveris.omr.ui.symbol.OmrFont; +import org.audiveris.omr.ui.symbol.TextFamily; import org.audiveris.omr.ui.symbol.TextFont; import org.audiveris.omr.util.Navigable; import org.audiveris.omr.util.StringUtil; @@ -67,8 +68,7 @@ public class TextWord private static final Logger logger = LoggerFactory.getLogger(TextWord.class); /** Abnormal characters. */ - private static final char[] ABNORMAL_CHARS = new char[] - { '\\' }; + private static final char[] ABNORMAL_CHARS = new char[] { '\\' }; /** Regexp for one-letter words. */ private static final Pattern ONE_LETTER_WORDS = compileRegexp(constants.oneLetterWordRegexp); @@ -115,9 +115,6 @@ public class TextWord /** Underlying glyph, if known. */ private Glyph glyph; - /** Precise font size, lazily computed. */ - private Float preciseFontSize; - /** Has this word been adjusted?. */ private boolean adjusted; @@ -244,38 +241,46 @@ public void adjust (Scale scale) } } - // //----------------// - // // adjustFontSize // - // //----------------// - // /** - // * Adjust font size precisely according to underlying bounds. - // * WARNING: OCR bounds can be crazy, hence bounds-based font adjustment is not reliable enough - // * - // * @return true if OK, false if abnormal font modification - // */ - // public boolean adjustFontSize () - // { - // double size = TextFont.computeFontSize(value, fontInfo, bounds.getSize()); - // double ratio = size / fontInfo.pointsize; - // // - // // if (ratio < constants.minFontRatio.getSourceValue() // - // // || ratio > constants.maxFontRatio.getSourceValue()) { - // // logger.debug(" abnormal font ratio {} {}", String.format("%.2f", ratio), this); - // // - // // return false; - // // } - // // - // // fontInfo = new FontInfo(fontInfo, (int) Math.rint(size)); - // // textLine.invalidateCache(); - // // - // return true; - // } + //----------------// + // adjustFontSize // + //----------------// + /** + * Adjust font size precisely according to underlying bounds. + * + * @param family the text family selected for the related sheet + * @return true if OK, false if no font modification was performed + */ + public boolean adjustFontSize (TextFamily family) + { + // Discard one-char words, they are not reliable + if (getLength() <= 1) { + return false; + } + + final int style = (fontInfo.isBold ? Font.BOLD : 0) | (fontInfo.isItalic ? Font.ITALIC : 0); + final TextFont font = new TextFont(family.getFontName(), null, style, fontInfo.pointsize); + + final int fontSize = font.computeSize(getValue(), getBounds().getSize()); + final double ratio = (double) fontSize / fontInfo.pointsize; + + if (ratio < constants.minFontRatio.getSourceValue() // + || ratio > constants.maxFontRatio.getSourceValue()) { + logger.info(" Abnormal font ratio {} {}", String.format("%.2f", ratio), this); + + return false; + } + + fontInfo = new FontInfo(fontInfo, fontSize); + textLine.invalidateCache(); + + return true; + } //---------------// // checkValidity // //---------------// /** - * Check the provided OCR'ed word (the word is not modified). + * Check the provided OCR'd word (the word is not modified). * * @return reason for invalidity if any, otherwise null */ @@ -470,28 +475,6 @@ public int getLength () return getValue().length(); } - //--------------------// - // getPreciseFontSize // - //--------------------// - /** - * Report the best computed font size for this word, likely to precisely match the - * word bounds. - *

    - * The size appears to be a bit larger than OCR detected side, by a factor in the range 1.1 - - * 1.2. TODO: To be improved, using font attributes for better font selection - *

    - * - * @return the computed font size - */ - public float getPreciseFontSize () - { - if (preciseFontSize == null) { - preciseFontSize = TextFont.computeFontSize(getValue(), fontInfo, getBounds().getSize()); - } - - return preciseFontSize; - } - //-------------// // getSubWords // //-------------// @@ -647,20 +630,6 @@ public void setGlyph (Glyph glyph) } } - //--------------------// - // setPreciseFontSize // - //--------------------// - /** - * Assign a font size. - * (to enforce consistent font size across all words of the same sentence) - * - * @param preciseFontSize the enforced font size, or null - */ - public void setPreciseFontSize (Float preciseFontSize) - { - this.preciseFontSize = preciseFontSize; - } - //-------------// // setTextLine // //-------------// @@ -722,40 +691,6 @@ private static Pattern compileRegexp (Constant.String str) } } - //------------------// - // createManualWord // - //------------------// - /** - * Create a TextWord instance manually, out of a given glyph and value. - *

    - * TODO: Perhaps we could improve the baseline, according to the precise string value provided. - * - * @param sheet the related sheet - * @param glyph the underlying glyph - * @param value the provided string value - * @return the TextWord created - */ - public static TextWord createManualWord (Sheet sheet, - Glyph glyph, - String value) - { - Rectangle box = glyph.getBounds(); - int fontSize = (int) Math.rint( - TextFont.computeFontSize(value, FontInfo.DEFAULT, box.getSize())); - TextWord word = new TextWord( - sheet, - box, - value, - new Line2D.Double(box.x, box.y + box.height, box.x + box.width, box.y + box.height), - 1.0, // Confidence - FontInfo.createDefault(fontSize), - null); - - word.setGlyph(glyph); - - return word; - } - //---------// // mergeOf // //---------// @@ -809,7 +744,7 @@ private static class Constants "Regular expression to detect one-letter words"); private final Constant.String abnormalWordRegexp = new Constant.String( - "^[^a-zA-Z_0-9-.,&=©\\?]+$", + "^[^a-zA-Z_0-9-.,&=©\\?}]+$", "Regular expression to detect abnormal words"); private final Constant.String tupletWordRegexp = new Constant.String( @@ -833,7 +768,7 @@ private static class Constants "Maximum ratio between ocr and glyph font sizes"); private final Constant.Ratio minFontRatio = new Constant.Ratio( - 0.5, + 0.3, "Minimum ratio between ocr and glyph font sizes"); private final Scale.Fraction standardFontSize = new Scale.Fraction( diff --git a/app/src/main/java/org/audiveris/omr/text/TextsStep.java b/app/src/main/java/org/audiveris/omr/text/TextsStep.java index 36a1f1adc..048fc8593 100644 --- a/app/src/main/java/org/audiveris/omr/text/TextsStep.java +++ b/app/src/main/java/org/audiveris/omr/text/TextsStep.java @@ -42,7 +42,7 @@ import java.util.Set; /** - * Class TextsStep discovers text items in a system area. + * Class TextsStep discovers text items in every system area. * * @author Hervé Bitteur */ @@ -110,7 +110,7 @@ public void doSystem (SystemInfo system, throws StepException { // Process texts at system level - new TextBuilder(system).processSystem(context.buffer, context.textLines); + new TextBuilder(system, null).processSystem(context.buffer, context.textLines); } //--------// @@ -161,7 +161,7 @@ protected static class Context /** The sheet buffer handed to OCR. */ public final ByteProcessor buffer; - /** The raw text lines OCR'ed. */ + /** The raw text lines OCR'd. */ public final List textLines; /** diff --git a/app/src/main/java/org/audiveris/omr/text/doc-files/font.png b/app/src/main/java/org/audiveris/omr/text/doc-files/font.png new file mode 100644 index 000000000..d482915b2 Binary files /dev/null and b/app/src/main/java/org/audiveris/omr/text/doc-files/font.png differ diff --git a/app/src/main/java/org/audiveris/omr/text/doc-files/font.uxf b/app/src/main/java/org/audiveris/omr/text/doc-files/font.uxf new file mode 100644 index 000000000..c6e21f05c --- /dev/null +++ b/app/src/main/java/org/audiveris/omr/text/doc-files/font.uxf @@ -0,0 +1,187 @@ + + + 10 + + UMLClass + + 280 + 40 + 80 + 100 + + *Font* +-- +name +style +size +pointSize +-- + + + + UMLClass + + 220 + 180 + 200 + 160 + + /*OmrFont*/ +bg=#ffffaa +-- +/fontCache/ +-- +computeSize(value,dim) +getLineMetrics(str) +layout(str[,fat]) +-- +/createFont(name,file,size)/ +/getFont(name,file,style,size)/ +/paint(g,layout,loc,align)/ + + + + Relation + + 310 + 130 + 30 + 70 + + lt=<<- + 10.0;10.0;10.0;50.0 + + + UMLClass + + 340 + 410 + 220 + 190 + + *TextFont* +bg=#ffffaa +-- +/defaultTextFamily/ +-- +TextFont(font) +TextFont(info) +TextInfo(size) +TextFont(name,file,style,size) +-- +deriveFont(size) +-- +/create(textFont,info)/ +/getBaseFontBySize(family,size)/ +/getTextFont(family,size)/ + + + + + Relation + + 370 + 330 + 30 + 100 + + lt=<<- + 10.0;10.0;10.0;80.0 + + + UMLClass + + 80 + 410 + 220 + 450 + + *MusicFont* +bg=#ffffaa +-- +/defaultMusicFamily/ +-- +musicFamily +-- +MusicFont(font) +MusicFont(family,size) +MusicFont(info) +-- +buildImage(shape,dec) +buildMusicFontScale(width) +computePointSize(width) ??? +deriveFont(size) +deriveFont(style) +getStaffInterline() +getSymbol(shape) +layoutNumberByCode(num) +layoutShape(shape,dim) +layoutShapeByCode(shape[,fat]) +layoutSymbol(sym,dim) +-- +/getBaseFont(family,il)/ +/getBaseFontBySize(family,size)/ +/getDefaultMusicFamily()/ +/getHeadFont(family,scale,itl)/ +/getHeadPointSize(scale,itl)/ +/getMusicFont(family, +size)/ +/getPointSize(itl)/ +/getString(codes)/ + + + + + Relation + + 250 + 330 + 30 + 100 + + lt=<<- + 10.0;10.0;10.0;80.0 + + + Relation + + 10 + 410 + 90 + 80 + + lt=<- +m2=backup + 70.0;10.0;30.0;10.0;30.0;50.0;70.0;50.0 + + + UMLClass + + 630 + 40 + 130 + 260 + + *FontInfo* +bg=#ffffaa +-- +/DEFAULT/ +-- +isBold +isItalic +isUnderlined +isMonospace +isSerif +isSmallCaps +pointSize +fontName +-- +getMnemo() +-- +/createDefault(size)/ +/decode(str)/ +-- +/JaxbAdapter/ + + + diff --git a/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOCR.java b/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOCR.java index 426268681..a5c72ba72 100644 --- a/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOCR.java +++ b/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOCR.java @@ -75,9 +75,9 @@ public class TesseractOCR private static final String TESSDATA_PREFIX = "TESSDATA_PREFIX"; /** Warning message when OCR folder cannot be found. */ - private static final String ocrNotFoundMsg = "Tesseract data could not be found. " - + "Try setting " + TESSDATA_PREFIX + " environment variable to point to " + TESSDATA - + " folder."; + private static final String ocrNotFoundMsg = + "Tesseract data could not be found. " + "Try setting " + TESSDATA_PREFIX + + " environment variable to point to " + TESSDATA + " folder."; //~ Instance fields ---------------------------------------------------------------------------- @@ -369,27 +369,23 @@ public static TesseractOCR getInstance () private static class Constants extends ConstantSet { - private final Constant.Boolean useOCR = new Constant.Boolean( - true, - "Should we use the OCR feature?"); + private final Constant.Boolean useOCR = + new Constant.Boolean(true, "Should we use the OCR feature?"); private final Constant.Boolean forceSingleBlock = new Constant.Boolean( false, "Should we force OCR to use PSM_SINGLE_BLOCK rather than PSM_AUTO?"); - private final Constant.Boolean saveImages = new Constant.Boolean( - false, - "Should we save on disk the images sent to Tesseract?"); + private final Constant.Boolean saveImages = + new Constant.Boolean(false, "Should we save on disk the images sent to Tesseract?"); // // private final Scale.Fraction maxDashWidth = new Scale.Fraction( // 1.0, // "Maximum width for a dash character"); // - private final Constant.Double minConfidence = new Constant.Double( - "0..1", - 0.65, - "Minimum confidence for OCR validity"); + private final Constant.Double minConfidence = + new Constant.Double("0..1", 0.65, "Minimum confidence for OCR validity"); } //---------------// diff --git a/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOrder.java b/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOrder.java index 7fd390b06..f79b5d3f0 100644 --- a/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOrder.java +++ b/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOrder.java @@ -354,16 +354,6 @@ private List getLines () line); logger.debug(" {}", word); line.appendWord(word); - - // // Heuristic... (just to test) - // boolean isDict = it.WordIsFromDictionary(); - // boolean isNumeric = it.WordIsNumeric(); - // boolean isLatin = encoder.canEncode(wordContent); - // int conf = (int) Math.rint(it.Confidence(WORD)); - // int len = wordContent.length(); - // boolean isValid = isLatin - // && (conf >= 80 - // || (conf >= 50 && ((isDict && len > 1) || isNumeric))); } // Char/symbol to be processed diff --git a/app/src/main/java/org/audiveris/omr/ui/Board.java b/app/src/main/java/org/audiveris/omr/ui/Board.java index 494226c0a..4bab019eb 100644 --- a/app/src/main/java/org/audiveris/omr/ui/Board.java +++ b/app/src/main/java/org/audiveris/omr/ui/Board.java @@ -24,6 +24,8 @@ import org.audiveris.omr.ui.field.LCheckBox; import org.audiveris.omr.ui.selection.SelectionService; import org.audiveris.omr.ui.selection.UserEvent; +import org.audiveris.omr.ui.symbol.MusicFamily; +import org.audiveris.omr.ui.symbol.TextFamily; import org.audiveris.omr.ui.util.Panel; import org.audiveris.omr.ui.util.UIUtil; import org.audiveris.omr.util.ClassUtil; @@ -57,28 +59,23 @@ *

    * Each board has a standard header composed of a title, a horizontal separator and optionally a * dump button. The board body is handled by the subclass. - *

    *

    * Any board can be (de)selected in its containing {@link BoardsPane}. This can be done * programmatically using {@link #setSelected(boolean)} and manually (via a right-click in the * BoardsPane). - *

    *

    - * Only selected boards can be seen in the BoardsPane display. A selected board can be made + * Only selected boards are displayed in the BoardsPane display. A selected board can be made * currently (in)visible programmatically using {@link #setVisible(boolean)}. * Typically, {@link org.audiveris.omr.check.CheckBoard}'s are visible only when they carry * glyph information. - *

    *

    * By default, any board can have a related SelectionService, used for subscribe (input) and publish * (output). When {@link #connect} is called, the board instance is subscribed to its * SelectionService for a specific collection of event classes. Similarly, {@link #disconnect} * unsubscribes the Board instance from the same event classes. - *

    *

    * This Board class is still an abstract class, since the onEvent() method must be * provided by every subclass. - *

    * * @author Hervé Bitteur */ @@ -165,6 +162,12 @@ public abstract class Board /** Board is selected? (it appears in boards pane). */ private boolean selected; + /** Cached music font family, if any. To trigger board symbols update only when needed. */ + protected MusicFamily cachedMusicFamily; + + /** Cached text font family, if any. To trigger board symbols update only when needed. */ + protected TextFamily cachedTextFamily; + //~ Constructors ------------------------------------------------------------------------------- /** diff --git a/app/src/main/java/org/audiveris/omr/ui/Colors.java b/app/src/main/java/org/audiveris/omr/ui/Colors.java index c7076f065..6fe2a2f42 100644 --- a/app/src/main/java/org/audiveris/omr/ui/Colors.java +++ b/app/src/main/java/org/audiveris/omr/ui/Colors.java @@ -132,7 +132,7 @@ public abstract class Colors public static final Color RUBBER_RULE = new Color(255, 0, 0, 100); // Colors for physical score view - /** frame: barlines, brackets, clefs, markers, time signatures. */ + /** frame: bar-lines, brackets, clefs, markers, time signatures. */ public static final Color SCORE_FRAME = Color.BLUE; /** frame: heads, beams, flags, rests, augmentation dots. */ @@ -189,6 +189,9 @@ public abstract class Colors /** Warping points. */ public static final Color WARP_POINT = Color.RED; + /** Background of a MusicPane. */ + public static final Color MUSIC_PANE_BACKGROUND = new Color(255, 255, 240); + //~ Constructors ------------------------------------------------------------------------------- /** diff --git a/app/src/main/java/org/audiveris/omr/ui/EntityBoard.java b/app/src/main/java/org/audiveris/omr/ui/EntityBoard.java index 7df5ecb68..feb73c6e7 100644 --- a/app/src/main/java/org/audiveris/omr/ui/EntityBoard.java +++ b/app/src/main/java/org/audiveris/omr/ui/EntityBoard.java @@ -69,8 +69,8 @@ public class EntityBoard private static final Logger logger = LoggerFactory.getLogger(EntityBoard.class); - private static final ResourceMap resources = Application.getInstance().getContext() - .getResourceMap(EntityBoard.class); + private static final ResourceMap resources = + Application.getInstance().getContext().getResourceMap(EntityBoard.class); /** Events this board is interested in. */ private static final Class[] eventsRead = new Class[] @@ -241,7 +241,7 @@ protected void dumpActionPerformed (ActionEvent e) // getFormLayout // //---------------// /** - * Overridable method to provide layout of the body part of the board. + * Override-able method to provide layout of the body part of the board. * (not including the top board line: title + dump button) * * @return the proper FormLayout diff --git a/app/src/main/java/org/audiveris/omr/ui/field/LTextPane.java b/app/src/main/java/org/audiveris/omr/ui/field/LTextPane.java new file mode 100644 index 000000000..6f50b3c54 --- /dev/null +++ b/app/src/main/java/org/audiveris/omr/ui/field/LTextPane.java @@ -0,0 +1,97 @@ +//------------------------------------------------------------------------------------------------// +// // +// L T e x t P a n e // +// // +//------------------------------------------------------------------------------------------------// +// +// +// Copyright © Audiveris 2023. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, either version +// 3 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with this +// program. If not, see . +//------------------------------------------------------------------------------------------------// +// +package org.audiveris.omr.ui.field; + +import javax.swing.JTextPane; + +/** + * Class LTextPane + * + * @author Hervé Bitteur + */ +public class LTextPane + extends LField +{ + //~ Constructors ------------------------------------------------------------------------------- + + /** + * Creates a new LTextPane object. + * + * @param editable Specifies whether this field will be editable + * @param label the string to be used as label text + * @param tip the related tool tip text + */ + public LTextPane (boolean editable, + String label, + String tip) + { + super(label, tip, new JTextPane()); + + JTextPane pane = getField(); + pane.setEditable(editable); + + if (!editable) { + pane.setFocusable(false); + pane.setBorder(null); + } + } + + /** + * Creates a new non-editable LTextPane object. + * + * @param label the string to be used as label text + * @param tip the related tool tip text + */ + public LTextPane (String label, + String tip) + { + this(false, label, tip); + } + + //~ Methods ------------------------------------------------------------------------------------ + + //---------// + // getText // + //---------// + /** + * Report the current content of the field + * + * @return the field content + */ + public String getText () + { + return getField().getText(); + } + + //---------// + // setText // + //---------// + /** + * Modify the content of the field + * + * @param text new text to set + */ + public void setText (String text) + { + getField().setText(text); + } +} diff --git a/app/src/main/java/org/audiveris/omr/ui/field/MusicPane.java b/app/src/main/java/org/audiveris/omr/ui/field/MusicPane.java new file mode 100644 index 000000000..221a5718a --- /dev/null +++ b/app/src/main/java/org/audiveris/omr/ui/field/MusicPane.java @@ -0,0 +1,224 @@ +//------------------------------------------------------------------------------------------------// +// // +// M u s i c P a n e // +// // +//------------------------------------------------------------------------------------------------// +// +// +// Copyright © Audiveris 2023. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, either version +// 3 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with this +// program. If not, see . +//------------------------------------------------------------------------------------------------// +// +package org.audiveris.omr.ui.field; + +import org.audiveris.omr.constant.Constant; +import org.audiveris.omr.constant.ConstantSet; +import org.audiveris.omr.ui.Colors; +import org.audiveris.omr.ui.symbol.MusicFamily; +import org.audiveris.omr.ui.symbol.TextFamily; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.Font; + +import javax.swing.BorderFactory; +import javax.swing.JTextPane; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultStyledDocument; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; + +/** + * Class MusicPane is a JTextPane which can mix text and music code points. + * + * @author Hervé Bitteur + */ +public class MusicPane + extends JTextPane +{ + //~ Static fields/initializers ----------------------------------------------------------------- + + private static final Constants constants = new Constants(); + + private static final Logger logger = LoggerFactory.getLogger(MusicPane.class); + + /** Minimum code point value to detect a music character. */ + private static final int HIGH_CODE = 0xE000; + + //~ Instance fields ---------------------------------------------------------------------------- + + /** Attributes referring the music font family. */ + private final SimpleAttributeSet musicSet = new SimpleAttributeSet(); + + /** Default text attributes. */ + private final SimpleAttributeSet textSet = new SimpleAttributeSet(); + + /** The specific pane document. */ + private final StyledDocument doc = new MyStyledDocument(); + + //~ Constructors ------------------------------------------------------------------------------- + + /** + * Creates a new MusicPane object. + * + * @param editable Specifies whether this pane will be editable + * @param tip the related tool tip text + * @param musicFamily the music family to be used initially + * @param textFamily the text family to be used + * @see #setFamilies(MusicFamily) + */ + + public MusicPane (boolean editable, + String tip, + MusicFamily musicFamily, + TextFamily textFamily) + { + setEditable(editable); + + if (tip != null) { + setToolTipText(tip); + } + + if (!editable) { + setFocusable(false); + setBorder(null); + } else { + setBorder(BorderFactory.createEtchedBorder()); + } + + // Specific attributes for music portions + StyleConstants.setFontFamily(musicSet, musicFamily.name()); + StyleConstants.setForeground(musicSet, Color.RED); + StyleConstants.setFontSize(musicSet, constants.fontSize.getValue()); + + // Use a large enough font size to make content more readable + setFont(new Font(textFamily.name(), Font.PLAIN, constants.fontSize.getValue())); + + // Use a specific background color, to avoid confusion with plain text fields + setBackground(Colors.MUSIC_PANE_BACKGROUND); + + setDocument(doc); + } + + //~ Methods ------------------------------------------------------------------------------------ + + //-----------------------// + // adjustMusicCharacters // + //-----------------------// + /** + * Process every character to set music attributes where needed. + */ + public void adjustMusicCharacters () + { + final String str = getText(); + final int length = str.length(); + + // Set default attributes to all characters + doc.setCharacterAttributes(0, length, textSet, true); + + for (int i = 0; i < length; i++) { + final int code = str.codePointAt(i); + + if (code >= HIGH_CODE) { + // Set music attributes to the current character + doc.setCharacterAttributes(i, 1, musicSet, true); + } + } + } + + //-------------// + // insertMusic // + //-------------// + /** + * Use the provided music string to replace the selection if any, + * otherwise insert it at the caret location. + * + * @param str the music string to insert + */ + public void insertMusic (String str) + { + try { + if (str == null || str.isEmpty()) { + return; + } + + // Selection? + final int start = getSelectionStart(); + final int end = getSelectionEnd(); + if (end > start) { + doc.remove(start, end - start); + } + + // Insert raw text + doc.insertString(start, str, null); + } catch (BadLocationException ex) { + logger.warn("MusicPane.insertMusic error {}", ex.getMessage(), ex); + } + } + + //-------------// + // setFamilies // + //-------------// + /** + * Switch to the provided music and text families + * + * @param musicFamily the new music family + * @param textFamily the new text family + */ + public void setFamilies (MusicFamily musicFamily, + TextFamily textFamily) + { + StyleConstants.setFontFamily(musicSet, musicFamily.name()); + StyleConstants.setFontFamily(textSet, textFamily.name()); + + adjustMusicCharacters(); + } + + //~ Inner Classes ------------------------------------------------------------------------------ + + //-----------// + // Constants // + //-----------// + private static class Constants + extends ConstantSet + { + private final Constant.Integer fontSize = new Constant.Integer( + "PointSize", + 20, + "Font size for text and music"); + } + + //------------------// + // MyStyledDocument // + //------------------// + /** + * A StyledDocument which adjusts its content (music vs text) after each insertion. + */ + public class MyStyledDocument + extends DefaultStyledDocument + { + @Override + public void insertString (int offs, + String str, + AttributeSet a) + throws BadLocationException + { + super.insertString(offs, str, a); + adjustMusicCharacters(); + } + } +} diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/BravuraSymbols.java b/app/src/main/java/org/audiveris/omr/ui/symbol/BravuraSymbols.java index b23f4ab8d..3cc814fe2 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/BravuraSymbols.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/BravuraSymbols.java @@ -72,6 +72,10 @@ public int[] getCode (Shape shape) case DA_CAPO -> ints(0xE046); case DIMINUENDO -> ints(0xE53F); case DOT_set -> ints(0xE044); + case DOTTED_HALF_NOTE_UP, METRO_DOTTED_HALF -> ints(0xE1D3, 0x0020, 0xE1E7); + case DOTTED_QUARTER_NOTE_UP, METRO_DOTTED_QUARTER -> ints(0xE1D5, 0x0020, 0xE1E7); + case DOTTED_EIGHTH_NOTE_UP, METRO_DOTTED_EIGHTH -> ints(0xE1D7, 0xE1E7); + case DOTTED_SIXTEENTH_NOTE_UP, METRO_DOTTED_SIXTEENTH -> ints(0xE1D9, 0xE1E7); case DOUBLE_BARLINE -> ints(0xE031); case DOUBLE_FLAT -> ints(0xE264); case DOUBLE_SHARP -> ints(0xE263); @@ -99,7 +103,9 @@ public int[] getCode (Shape shape) case DYNAMICS_SFZ -> ints(0xE539); // case DYNAMICS_SFPP -> ints(0xE538); + case EIGHTH_NOTE_UP, METRO_EIGHTH -> ints(0xE1D7); case EIGHTH_REST -> ints(0xE4E6); + case EIGHTH_set, GRACE_NOTE -> ints(0xE562); case FERMATA -> ints(0xE4C0); case FERMATA_BELOW -> ints(0xE4C1); @@ -120,7 +126,6 @@ public int[] getCode (Shape shape) case F_CLEF_8VA -> ints(0xE065); case F_CLEF_8VB -> ints(0xE064); - case GRACE_NOTE -> ints(0xE562); case GRACE_NOTE_DOWN -> ints(0xE563); case GRACE_NOTE_SLASH -> ints(0xE560); case GRACE_NOTE_SLASH_DOWN -> ints(0xE561); @@ -130,9 +135,8 @@ public int[] getCode (Shape shape) case G_CLEF_8VB -> ints(0xE052); case HALF_NOTE_DOWN -> ints(0xE1D4); - case HALF_NOTE_UP -> ints(0xE1D3); - case HALF_REST -> ints(0xE4E4); - case HW_REST_set -> ints(0xE4E4); + case HALF_NOTE_UP, METRO_HALF -> ints(0xE1D3); + case HALF_REST, HW_REST_set -> ints(0xE4E4); case KEY_CANCEL -> ints(0xE261); @@ -163,8 +167,8 @@ public int[] getCode (Shape shape) case NOTEHEAD_CIRCLE_X_VOID -> ints(0xE0B2); case ONE_16TH_REST -> ints(0xE4E7); - case ONE_64TH_REST -> ints(0xE4E9); case ONE_32ND_REST -> ints(0xE4E8); + case ONE_64TH_REST -> ints(0xE4E9); case ONE_128TH_REST -> ints(0xE4EA); case OTTAVA -> ints(0xE510); @@ -177,7 +181,7 @@ public int[] getCode (Shape shape) case PLAYING_CLOSED -> ints(0xE7F5); case QUARTER_NOTE_DOWN -> ints(0xE1D6); - case QUARTER_NOTE_UP -> ints(0xE1D5); + case QUARTER_NOTE_UP, METRO_QUARTER -> ints(0xE1D5); case QUARTER_REST -> ints(0xE4E5); case QUINDICESIMA -> ints(0xE514); @@ -191,6 +195,7 @@ public int[] getCode (Shape shape) case SEGNO -> ints(0xE047); case SHARP -> ints(0xE262); + case SIXTEENTH_NOTE_UP, METRO_SIXTEENTH -> ints(0xE1D9); case STACCATISSIMO -> ints(0xE4A6); case STACCATO -> ints(0xE4A2); case STAFF_LINES -> ints(0xE01A); @@ -225,7 +230,7 @@ public int[] getCode (Shape shape) case VENTIDUESIMA -> ints(0xE517); - case WHOLE_NOTE -> ints(0xE0A2); + case WHOLE_NOTE, METRO_WHOLE -> ints(0xE0A2); case WHOLE_NOTE_CROSS -> ints(0xE0A7); case WHOLE_NOTE_DIAMOND -> ints(0xE0D8); case WHOLE_NOTE_TRIANGLE_DOWN -> ints(0xE0C4); diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/FinaleJazzSymbols.java b/app/src/main/java/org/audiveris/omr/ui/symbol/FinaleJazzSymbols.java index 68f537d0e..1817b78d5 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/FinaleJazzSymbols.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/FinaleJazzSymbols.java @@ -26,6 +26,7 @@ import org.audiveris.omr.math.PointUtil; import static org.audiveris.omr.ui.symbol.Alignment.BOTTOM_LEFT; import static org.audiveris.omr.ui.symbol.Alignment.TOP_LEFT; +import static org.audiveris.omr.ui.symbol.Symbols.ints; import java.awt.Graphics2D; import java.awt.font.TextLayout; @@ -80,6 +81,10 @@ public int[] getCode (Shape shape) ///case DA_CAPO -> ints(0xE046); case DIMINUENDO -> ints(0xE53F); case DOT_set -> ints(0xE044); + case DOTTED_HALF_NOTE_UP, METRO_DOTTED_HALF -> ints(0xE1D3, 0x0020, 0xE1E7); + case DOTTED_QUARTER_NOTE_UP, METRO_DOTTED_QUARTER -> ints(0xE1D5, 0x0020, 0xE1E7); + case DOTTED_EIGHTH_NOTE_UP, METRO_DOTTED_EIGHTH -> ints(0xE1D7, 0xE1E7); + case DOTTED_SIXTEENTH_NOTE_UP, METRO_DOTTED_SIXTEENTH -> ints(0xE1D9, 0xE1E7); //case DOUBLE_BARLINE -> ints(0xE031); case DOUBLE_FLAT -> ints(0xE264); case DOUBLE_SHARP -> ints(0xE263); @@ -107,7 +112,9 @@ public int[] getCode (Shape shape) case DYNAMICS_SFZ -> ints(0xE539); // case DYNAMICS_SFPP -> ints(0xE538); + case EIGHTH_NOTE_UP, METRO_EIGHTH -> ints(0xE1D7); case EIGHTH_REST -> ints(0xE4E6); + case EIGHTH_set, GRACE_NOTE -> ints(0xE562); case FERMATA -> ints(0xE4C0); case FERMATA_BELOW -> ints(0xE4C1); @@ -128,7 +135,6 @@ public int[] getCode (Shape shape) case F_CLEF_8VA -> ints(0xE065); case F_CLEF_8VB -> ints(0xE064); - case GRACE_NOTE -> ints(0xE562); case GRACE_NOTE_DOWN -> ints(0xE563); case GRACE_NOTE_SLASH -> ints(0xE560); case GRACE_NOTE_SLASH_DOWN -> ints(0xE561); @@ -138,7 +144,7 @@ public int[] getCode (Shape shape) case G_CLEF_8VB -> ints(0xE052); case HALF_NOTE_DOWN -> ints(0xE1D4); - case HALF_NOTE_UP -> ints(0xE1D3); + case HALF_NOTE_UP, METRO_HALF -> ints(0xE1D3); case HALF_REST -> ints(0xE4E4); case HW_REST_set -> ints(0xE4E4); @@ -185,7 +191,7 @@ public int[] getCode (Shape shape) case PLAYING_CLOSED -> ints(0xE5E5); // 0xE872 ? case QUARTER_NOTE_DOWN -> ints(0xE1D6); - case QUARTER_NOTE_UP -> ints(0xE1D5); + case QUARTER_NOTE_UP, METRO_QUARTER -> ints(0xE1D5); case QUARTER_REST -> ints(0xE4E5); case QUINDICESIMA -> ints(0xE514); @@ -199,6 +205,7 @@ public int[] getCode (Shape shape) case SEGNO -> ints(0xE047); case SHARP -> ints(0xE262); + case SIXTEENTH_NOTE_UP, METRO_SIXTEENTH -> ints(0xE1D9); case STACCATISSIMO -> ints(0xE4A6); case STACCATO -> ints(0xE4A2); case STAFF_LINES -> ints(0xE014); // 0xE01A in Bravura @@ -232,7 +239,8 @@ public int[] getCode (Shape shape) ///case TURN_UP -> ints(0xE56A); ///case VENTIDUESIMA -> ints(0xE517); - case WHOLE_NOTE -> ints(0xE0A2); + + case WHOLE_NOTE, METRO_WHOLE -> ints(0xE0A2); ///case WHOLE_NOTE_CROSS -> ints(0xE0A7); case WHOLE_NOTE_DIAMOND -> ints(0xE0DA); case WHOLE_NOTE_TRIANGLE_DOWN -> ints(0xE0C6); @@ -262,8 +270,8 @@ protected void populateSymbols () symbolMap.put(FLAG_3_DOWN, new FlagsDownSymbol(FLAG_3_DOWN, family(), 3)); symbolMap.put(FLAG_4_DOWN, new FlagsDownSymbol(FLAG_4_DOWN, family(), 4)); symbolMap.put(FLAG_5_DOWN, new FlagsDownSymbol(FLAG_5_DOWN, family(), 5)); - } + //~ Inner Classes ------------------------------------------------------------------------------ //-----------------// diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/KeySymbol.java b/app/src/main/java/org/audiveris/omr/ui/symbol/KeySymbol.java index bd976b9e7..85b304771 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/KeySymbol.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/KeySymbol.java @@ -158,10 +158,8 @@ protected void paint (Graphics2D g, Point2D loc = alignment.translatedPoint(AREA_CENTER, p.rect, location); // Set loc to (x=left side, y=staff mid line) - PointUtil.add( - loc, - -p.rect.getWidth() / 2, - -KeyInter.getStandardPosition(fifths) * p.stepDy); + PointUtil + .add(loc, -p.rect.getWidth() / 2, -KeyInter.getStandardPosition(fifths) * p.stepDy); if (fifths == 0) { int pitch = KeyInter.getItemPitch(1, null); diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/LelandSymbols.java b/app/src/main/java/org/audiveris/omr/ui/symbol/LelandSymbols.java new file mode 100644 index 000000000..55e9dce3a --- /dev/null +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/LelandSymbols.java @@ -0,0 +1,243 @@ +//------------------------------------------------------------------------------------------------// +// // +// L e l a n d S y m b o l s // +// // +//------------------------------------------------------------------------------------------------// +// +// +// Copyright © Audiveris 2023. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, either version +// 3 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with this +// program. If not, see . +//------------------------------------------------------------------------------------------------// +// +package org.audiveris.omr.ui.symbol; + +import org.audiveris.omr.glyph.Shape; +import static org.audiveris.omr.glyph.Shape.*; + +/** + * Class LelandSymbols + * + * @author Hervé Bitteur + */ +public class LelandSymbols + extends Symbols +{ + //~ Methods ------------------------------------------------------------------------------------ + + @Override + protected MusicFamily family () + { + return MusicFamily.Leland; + } + + @Override + public int[] getCode (Shape shape) + { + return switch (shape) { + case ACCENT -> ints(0xE4A0); + // case ARPEGGIATO -> ints(0xE63C); + case AUGMENTATION_DOT -> ints(0xE044); + + // case BACK_TO_BACK_REPEAT_SIGN -> ints(0xE042); + case BRACE -> ints(0xE000); + case BRACKET -> ints(0xE002); + case BRACKET_LOWER_SERIF -> ints(0xE004); + case BRACKET_UPPER_SERIF -> ints(0xE003); + case BREATH_MARK -> ints(0xE4CE); + case BREVE -> ints(0xE0A0); + // case BREVE_CROSS -> ints(0xE0A6); + // case BREVE_DIAMOND -> ints(0xE0D7); + // case BREVE_TRIANGLE_DOWN -> ints(0xE0C3); + // case BREVE_CIRCLE_X -> ints(0xE0B0); + case BREVE_REST -> ints(0xE4E2); + + case CAESURA -> ints(0xE4D1); + case COMMON_TIME -> ints(0xE08A); + case CUT_TIME -> ints(0xE08B); + case C_CLEF -> ints(0xE05C); + case CODA -> ints(0xE048); + case CRESCENDO -> ints(0xE53E); + + // case DAL_SEGNO -> ints(0xE045); + // case DA_CAPO -> ints(0xE046); + case DIMINUENDO -> ints(0xE53F); + case DOT_set -> ints(0xE044); + case DOTTED_HALF_NOTE_UP, METRO_DOTTED_HALF -> ints(0xE1D3, 0x0020, 0xE1E7); + case DOTTED_QUARTER_NOTE_UP, METRO_DOTTED_QUARTER -> ints(0xE1D5, 0x0020, 0xE1E7); + case DOTTED_EIGHTH_NOTE_UP, METRO_DOTTED_EIGHTH -> ints(0xE1D7, 0xE1E7); + case DOTTED_SIXTEENTH_NOTE_UP, METRO_DOTTED_SIXTEENTH -> ints(0xE1D9, 0xE1E7); + // case DOUBLE_BARLINE -> ints(0xE031); + case DOUBLE_FLAT -> ints(0xE264); + case DOUBLE_SHARP -> ints(0xE263); + case DYNAMICS_F -> ints(0xE522); + case DYNAMICS_FF -> ints(0xE52F); + // case DYNAMICS_FFF -> ints(0xE530); + // case DYNAMICS_FFFF -> ints(0xE531); + // case DYNAMICS_FFFFF -> ints(0xE532); + // case DYNAMICS_FFFFFF -> ints(0xE533); + // case DYNAMICS_FZ -> ints(0xE535); + case DYNAMICS_FP -> ints(0xE534); + case DYNAMICS_MF -> ints(0xE52D); + case DYNAMICS_MP -> ints(0xE52C); + case DYNAMICS_P -> ints(0xE520); + case DYNAMICS_PP -> ints(0xE52B); + // case DYNAMICS_PPP -> ints(0xE52A); + // case DYNAMICS_PPPP -> ints(0xE529); + // case DYNAMICS_PPPPP -> ints(0xE528); + // case DYNAMICS_PPPPPP -> ints(0xE527); + // case DYNAMICS_RF -> ints(0xE53C); + // case DYNAMICS_RFZ -> ints(0xE53D); + case DYNAMICS_SF -> ints(0xE536); + // case DYNAMICS_SFFZ -> ints(0xE53B); + // case DYNAMICS_SFP -> ints(0xE537); + case DYNAMICS_SFZ -> ints(0xE539); + // case DYNAMICS_SFPP -> ints(0xE538); + + case EIGHTH_NOTE_UP, METRO_EIGHTH -> ints(0xE1D7); + case EIGHTH_REST -> ints(0xE4E6); + case EIGHTH_set, GRACE_NOTE -> ints(0xE562); + + case FERMATA -> ints(0xE4C0); + case FERMATA_BELOW -> ints(0xE4C1); + // case FINAL_BARLINE -> ints(0xE032); + case FLAG_1 -> ints(0xE240); + case FLAG_1_DOWN -> ints(0xE241); + case FLAG_2 -> ints(0xE242); + case FLAG_2_DOWN -> ints(0xE243); + case FLAG_3 -> ints(0xE244); + case FLAG_3_DOWN -> ints(0xE245); + case FLAG_4 -> ints(0xE246); + case FLAG_4_DOWN -> ints(0xE247); + case FLAG_5 -> ints(0xE248); + case FLAG_5_DOWN -> ints(0xE249); + case FLAT -> ints(0xE260); + case F_CLEF -> ints(0xE062); + case F_CLEF_SMALL -> ints(0xE07C); + case F_CLEF_8VA -> ints(0xE065); + case F_CLEF_8VB -> ints(0xE064); + + case GRACE_NOTE_DOWN -> ints(0xE563); + case GRACE_NOTE_SLASH -> ints(0xE560); + case GRACE_NOTE_SLASH_DOWN -> ints(0xE561); + case G_CLEF -> ints(0xE050); + case G_CLEF_SMALL -> ints(0xE07A); + case G_CLEF_8VA -> ints(0xE053); + case G_CLEF_8VB -> ints(0xE052); + + case HALF_NOTE_DOWN -> ints(0xE1D4); + case HALF_NOTE_UP, METRO_HALF -> ints(0xE1D3); + case HALF_REST, HW_REST_set -> ints(0xE4E4); + + case KEY_CANCEL -> ints(0xE261); + + // case LEDGER -> ints(0xE022); + // case LEFT_REPEAT_SIGN -> ints(0xE040); + case LONG_REST -> ints(0xE4E1); + + case MORDENT -> ints(0xE56C); + case MORDENT_INVERTED -> ints(0xE56D); // With bar + // case MULTIPLE_REST -> ints(0xE4EE); + // case MULTIPLE_REST_LEFT -> ints(0xE4EF); + // case MULTIPLE_REST_MIDDLE -> ints(0xE4F0); + // case MULTIPLE_REST_RIGHT -> ints(0xE4F1); + + case NATURAL -> ints(0xE261); + // case NON_DRAGGABLE -> ints(0xEA94, 0xEA93); + + case NOTEHEAD_BLACK -> ints(0xE0A4); + case NOTEHEAD_CROSS -> ints(0xE0A9); + case NOTEHEAD_DIAMOND_FILLED -> ints(0xE0DB); + // case NOTEHEAD_TRIANGLE_DOWN_FILLED -> ints(0xE0C7); + case NOTEHEAD_CIRCLE_X -> ints(0xE0B3); + + case NOTEHEAD_VOID -> ints(0xE0A3); + // case NOTEHEAD_CROSS_VOID -> ints(0xE0A8); + case NOTEHEAD_DIAMOND_VOID -> ints(0xE0D9); + // case NOTEHEAD_TRIANGLE_DOWN_VOID -> ints(0xE0C5); + // case NOTEHEAD_CIRCLE_X_VOID -> ints(0xE0B2); + + case ONE_16TH_REST -> ints(0xE4E7); + case ONE_32ND_REST -> ints(0xE4E8); + case ONE_64TH_REST -> ints(0xE4E9); + case ONE_128TH_REST -> ints(0xE4EA); + case OTTAVA -> ints(0xE510); + + case PEDAL_MARK -> ints(0xE650); + case PEDAL_UP_MARK -> ints(0xE655); + case PERCUSSION_CLEF -> ints(0xE069); + + // case PLAYING_OPEN -> ints(0xE7F8); + // case PLAYING_HALF_OPEN -> ints(0xE7F7); + // case PLAYING_CLOSED -> ints(0xE7F5); + + case QUARTER_NOTE_DOWN -> ints(0xE1D6); + case QUARTER_NOTE_UP, METRO_QUARTER -> ints(0xE1D5); + case QUARTER_REST -> ints(0xE4E5); + case QUINDICESIMA -> ints(0xE514); + + case REPEAT_DOT -> ints(0xE044); + case REPEAT_DOT_PAIR -> ints(0xE043); + case REPEAT_ONE_BAR -> ints(0xE500); + case REPEAT_TWO_BARS -> ints(0xE501); + case REPEAT_FOUR_BARS -> ints(0xE502); + // case REVERSE_FINAL_BARLINE -> ints(0xE033); + // case RIGHT_REPEAT_SIGN -> ints(0xE041); + + case SEGNO -> ints(0xE047); + case SHARP -> ints(0xE262); + case SIXTEENTH_NOTE_UP, METRO_SIXTEENTH -> ints(0xE1D9); + case STACCATISSIMO -> ints(0xE4A6); + case STACCATO -> ints(0xE4A2); + // case STAFF_LINES -> ints(0xE01A); + // case STEM -> ints(0xE210); + case STRONG_ACCENT -> ints(0xE4AC); + + case TENUTO -> ints(0xE4A4); + // case THICK_BARLINE -> ints(0xE034); + // case THIN_BARLINE -> ints(0xE030); + case TIME_ZERO -> ints(0xE080); + case TIME_ONE -> ints(0xE081); + case TIME_TWO -> ints(0xE082); + case TIME_THREE -> ints(0xE083); + case TIME_FOUR -> ints(0xE084); + case TIME_FIVE -> ints(0xE085); + case TIME_SIX -> ints(0xE086); + case TIME_SEVEN -> ints(0xE087); + case TIME_EIGHT -> ints(0xE088); + case TIME_NINE -> ints(0xE089); + case TIME_TWELVE -> ints(0xE081, 0xE082); + case TIME_SIXTEEN -> ints(0xE081, 0xE086); + case TR -> ints(0xE566); + case TREMOLO_1 -> ints(0xE220); + case TREMOLO_2 -> ints(0xE221); + case TREMOLO_3 -> ints(0xE222); + case TUPLET_SIX -> ints(0xE886); + case TUPLET_THREE -> ints(0xE883); + case TURN -> ints(0xE567); + case TURN_INVERTED -> ints(0xE568); + case TURN_SLASH -> ints(0xE569); + case TURN_UP -> ints(0xE56A); + + case VENTIDUESIMA -> ints(0xE517); + + case WHOLE_NOTE, METRO_WHOLE -> ints(0xE0A2); + // case WHOLE_NOTE_CROSS -> ints(0xE0A7); + case WHOLE_NOTE_DIAMOND -> ints(0xE0D8); + // case WHOLE_NOTE_TRIANGLE_DOWN -> ints(0xE0C4); + // case WHOLE_NOTE_CIRCLE_X -> ints(0xE0B1); + case WHOLE_REST -> ints(0xE4E3); + + default -> null; + }; + } +} diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/MetronomeSymbol.java b/app/src/main/java/org/audiveris/omr/ui/symbol/MetronomeSymbol.java new file mode 100644 index 000000000..f965731f6 --- /dev/null +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/MetronomeSymbol.java @@ -0,0 +1,242 @@ +//------------------------------------------------------------------------------------------------// +// // +// M e t r o n o m e S y m b o l // +// // +//------------------------------------------------------------------------------------------------// +// +// +// Copyright © Audiveris 2023. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, either version +// 3 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with this +// program. If not, see . +//------------------------------------------------------------------------------------------------// +// +package org.audiveris.omr.ui.symbol; + +import org.audiveris.omr.glyph.Shape; +import org.audiveris.omr.glyph.ShapeSet; +import org.audiveris.omr.sheet.SheetStub; +import org.audiveris.omr.sheet.ui.StubsController; +import org.audiveris.omr.sig.inter.BeatUnitInter.Note; +import org.audiveris.omr.sig.inter.MetronomeInter; +import static org.audiveris.omr.ui.symbol.Alignment.MIDDLE_LEFT; +import static org.audiveris.omr.ui.symbol.Alignment.MIDDLE_RIGHT; +import static org.audiveris.omr.ui.symbol.Alignment.TOP_LEFT; +import static org.audiveris.omr.ui.symbol.OmrFont.RATIO_METRO; +import static org.audiveris.omr.ui.symbol.OmrFont.RATIO_TINY; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.font.TextLayout; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +/** + * Class MetronomeSymbol implements a metronome, composed of a beat unit, + * an equal sign, and a bpm text. + * + * @author Hervé Bitteur + */ +public class MetronomeSymbol + extends DecorableSymbol +{ + //~ Static fields/initializers ----------------------------------------------------------------- + + private static final Logger logger = LoggerFactory.getLogger(MetronomeSymbol.class); + + /** The label used for metronome button. */ + private static final String LABEL = "metronome"; + + /** The dummy bpm value. */ + public static final String DUMMY_BPM = "00"; + + //~ Instance fields ---------------------------------------------------------------------------- + + /** The note used as beat unit. */ + protected final Note note; + + /** The bpm textual specification. */ + protected final String bpmString; + + //~ Constructors ------------------------------------------------------------------------------- + + /** + * Create a MetronomeSymbol with the dummy bpm value. + * + * @param noteShape one of {@link ShapeSet#BeatUnits} + * @param musicFamily the music font family + */ + public MetronomeSymbol (Shape noteShape, + MusicFamily musicFamily) + { + this(noteShape, musicFamily, DUMMY_BPM); + } + + /** + * Create a MetronomeSymbol. + * + * @param noteShape one of {@link ShapeSet#BeatUnits} + * @param musicFamily the music font family + * @param bpmString the bpm textual specification + */ + public MetronomeSymbol (Shape noteShape, + MusicFamily musicFamily, + String bpmString) + { + super(Shape.METRONOME, musicFamily); + note = Note.noteOf(noteShape); + this.bpmString = bpmString; + } + + //~ Methods ------------------------------------------------------------------------------------ + + //----------// + // getModel // + //----------// + @Override + public MetronomeInter.Model getModel (MusicFont font, + Point location) + { + final MyParams p = getParams(font); + p.model.translate(p.vectorTo(location)); + + return p.model; + } + + //-----------// + // getParams // + //-----------// + @Override + protected MyParams getParams (MusicFont sheetMusicFont) + { + final MyParams p = new MyParams(); + + final StubsController controller = StubsController.getInstance(); + final SheetStub stub = controller.getSelectedStub(); + final TextFamily textFamily = (stub != null) ? stub.getTextFamily() : TextFamily.SansSerif; + + if (isDecorated && isTiny) { + // Use the metronome label + final int fontSize = (int) Math.rint(sheetMusicFont.getSize2D() * RATIO_TINY); + final TextFont textFont = new TextFont( + textFamily.getFontName(), + null, + Font.PLAIN, + fontSize); + p.layout = textFont.layout(LABEL); + p.rect = p.layout.getBounds(); + } else { + // Properly sized music and text fonts + final int fontSize = (int) Math.rint(sheetMusicFont.getSize2D() * RATIO_METRO); + final TextFont textFont = new TextFont( + textFamily.getFontName(), + null, + Font.PLAIN, + fontSize); + final MusicFont musicFont = sheetMusicFont.deriveFont((float) fontSize); + + final Symbols symbols = MusicFamily.Bravura.getSymbols(); + final int[] codes = symbols.getCode(note.toShape()); + final String str = new String(codes, 0, codes.length); + p.layout = musicFont.layout(str); + final Rectangle2D noteRect = p.layout.getBounds(); + final float noteAdvance = p.layout.getAdvance(); + double minY = noteRect.getMinY(); + double maxY = noteRect.getMaxY(); + + p.bpmLayout = textFont.layout(" = " + bpmString); + final Rectangle2D bpmRect = p.bpmLayout.getBounds(); + final float bpmAdvance = p.bpmLayout.getAdvance(); + minY = Math.min(minY, bpmRect.getMinY()); + maxY = Math.max(maxY, bpmRect.getMaxY()); + + p.rect = new Rectangle2D.Double(0, 0, noteAdvance + bpmAdvance, maxY - minY); + + // Offset from box center to note baseline center + p.offset = new Point2D.Double( + -p.rect.getWidth() / 2 + noteAdvance / 2, + -p.rect.getHeight() / 2 - noteRect.getY()); + + // Model + p.model = new MetronomeInter.Model(); + p.model.box = p.rect.getBounds2D(); + p.model.tempo = ""; + p.model.unit = note.toShape(); + p.model.bpmText = bpmString; + p.model.baseCenter = new Point2D.Double(noteRect.getWidth() / 2, -noteRect.getY()); + p.model.unitFontSize = musicFont.getSize(); + p.model.tempoFontSize = textFont.getSize(); + p.model.bpmFontSize = textFont.getSize(); + } + + return p; + } + + //-----------// + // internals // + //-----------// + @Override + protected String internals () + { + return new StringBuilder(super.internals()) // + .append(' ').append(note) // + .append(' ').append(bpmString) // + .toString(); + } + + //-------// + // paint // + //-------// + @Override + protected void paint (Graphics2D g, + Params params, + Point2D location, + Alignment alignment) + { + final MyParams p = (MyParams) params; + + if (isDecorated && isTiny) { + final Point2D loc = alignment.translatedPoint(TOP_LEFT, p.rect, location); + OmrFont.paint(g, p.layout, loc, TOP_LEFT); + } else { + // note on left side + final Point2D loc1 = alignment.translatedPoint(MIDDLE_LEFT, p.rect, location); + OmrFont.paint(g, p.layout, loc1, MIDDLE_LEFT); + + // = bpm on right side + final Point2D loc2 = alignment.translatedPoint(MIDDLE_RIGHT, p.rect, location); + OmrFont.paint(g, p.bpmLayout, loc2, MIDDLE_RIGHT); + } + } + + //~ Inner Classes ------------------------------------------------------------------------------ + + //----------// + // MyParams // + //----------// + protected static class MyParams + extends ShapeSymbol.Params + { + // offset: from area center to note baseline + // layout: note layout + // rect: global image rectangle + + // bpm layout + TextLayout bpmLayout; + + // model + MetronomeInter.Model model; + } +} diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/MusicFamily.java b/app/src/main/java/org/audiveris/omr/ui/symbol/MusicFamily.java index 91c344694..a8ff868c7 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/MusicFamily.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/MusicFamily.java @@ -42,6 +42,9 @@ public enum MusicFamily null, // No backup needed for this comprehensive font new BravuraSymbols()), + /** Alternate family, some symbols missing. */ + Leland("Leland", "Leland.otf", Bravura, new LelandSymbols()), + /** Alternate family, some symbols missing. */ FinaleJazz("Finale Jazz", "FinaleJazz.otf", Bravura, new FinaleJazzSymbols()), @@ -121,6 +124,10 @@ public static MusicFamily valueOfName (String value) } } + if (value.equalsIgnoreCase("generic")) { + return MusicFamily.Bravura; + } + logger.warn("No music family for value: \"{}\"", value); return null; } diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/MusicFont.java b/app/src/main/java/org/audiveris/omr/ui/symbol/MusicFont.java index e0b0bd3a3..6517ad7de 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/MusicFont.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/MusicFont.java @@ -26,6 +26,9 @@ import org.audiveris.omr.glyph.Shape; import static org.audiveris.omr.glyph.Shape.TIME_ZERO; import org.audiveris.omr.sheet.Scale; +import org.audiveris.omr.sheet.SheetStub; +import org.audiveris.omr.sheet.ui.StubsController; +import org.audiveris.omr.text.FontInfo; import org.audiveris.omr.ui.util.UIUtil; import org.audiveris.omr.util.param.ConstantBasedParam; import org.audiveris.omr.util.param.Param; @@ -72,7 +75,7 @@ * scaling. * * To get properly scaled instances, use the convenient methods {@link #getBaseFont(Family,int)} - * and {@link #getHeadFont(Family,Scale, int)}. + * and {@link #getHeadFont(Family,Scale,int)}. * * * @see SMuFL web site @@ -94,8 +97,9 @@ public class MusicFont public static final int TINY_INTERLINE = (int) Math.rint(DEFAULT_INTERLINE * RATIO_TINY); /** Default music font family. */ - public static final Param defaultMusicParam = - new ConstantBasedParam<>(constants.defaultMusicFamily, Param.GLOBAL_SCOPE); + public static final Param defaultMusicParam = new ConstantBasedParam<>( + constants.defaultMusicFamily, + Param.GLOBAL_SCOPE); //~ Instance fields ---------------------------------------------------------------------------- @@ -107,9 +111,6 @@ public class MusicFont */ protected final MusicFamily musicFamily; - /** The related font for text. */ - protected TextFont textFont; - /** Backup font, if any. */ protected MusicFont backupFont; @@ -131,13 +132,13 @@ private MusicFont (Font font) } /** - * Creates a new MusicFont object, based on chosen family and size. + * Creates a new MusicFont object, based on the provided family and size. * * @param family chosen music font family * @param size the point size of the Font */ - protected MusicFont (MusicFamily family, - int size) + public MusicFont (MusicFamily family, + int size) { super(family.getFontName(), family.getFileName(), Font.PLAIN, size); musicFamily = family; @@ -147,6 +148,16 @@ protected MusicFont (MusicFamily family, } } + /** + * Creates a new MusicFont object, based on the provided FontInfo. + * + * @param info the font info + */ + public MusicFont (FontInfo info) + { + this(MusicFamily.valueOfName(info.fontName), info.pointsize); + } + //~ Methods ------------------------------------------------------------------------------------ //------------// @@ -186,7 +197,7 @@ public Scale.MusicFontScale buildMusicFontScale (double width) } //------------------// - // computePointSize // + // computePointSize // TODO: Could we get rid of this method? //------------------// /** * Using this font family, compute the point size that best matches the provided size @@ -238,6 +249,15 @@ public MusicFont deriveFont (float size) return new MusicFont(super.deriveFont(size)); } + //------------// + // deriveFont // + //------------// + @Override + public MusicFont deriveFont (int style) + { + return new MusicFont(super.deriveFont(style)); + } + //--------// // equals // //--------// @@ -359,8 +379,8 @@ public TextLayout layoutNumberByCode (int number) } final int baseCode = zeroCode[0]; - final int[] numberCodes = - (number >= 100) ? new int[3] : (number >= 10) ? new int[2] : new int[1]; + final int[] numberCodes = (number >= 100) ? new int[3] + : (number >= 10) ? new int[2] : new int[1]; int index = 0; if (number >= 100) { @@ -541,6 +561,20 @@ public static MusicFont getBaseFontBySize (MusicFamily family, return getMusicFont(family, pointSize); } + //------------------// + // getCurrentFamily // + //------------------// + /** + * Report the music family used in the sheet currently displayed + * + * @return the current sheet music family, null if no sheet is displayed + */ + public static MusicFamily getCurrentFamily () + { + final SheetStub stub = StubsController.getInstance().getSelectedStub(); + return (stub != null) ? stub.getMusicFamily() : null; + } + //-----------------------// // getDefaultMusicFamily // //-----------------------// diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/MusicalSymbols.java b/app/src/main/java/org/audiveris/omr/ui/symbol/MusicalSymbols.java index ae1bf73ff..4e89fdb8f 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/MusicalSymbols.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/MusicalSymbols.java @@ -118,7 +118,7 @@ public int[] getCode (Shape shape) ///case F_CLEF_8VA -> ints(0xE065); ///case F_CLEF_8VB -> ints(0xE064); - case GRACE_NOTE -> ints(0xF03B); + case EIGHTH_set, GRACE_NOTE, METRO_EIGHTH -> ints(0xF03B); case GRACE_NOTE_DOWN -> ints(0xF03A); case GRACE_NOTE_SLASH -> ints(0xF0C9); ///case GRACE_NOTE_SLASH_DOWN -> ints(0xF0C9); // Use vertical mirror of GRACE_NOTE_SLASH? diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/OmrFont.java b/app/src/main/java/org/audiveris/omr/ui/symbol/OmrFont.java index de3e6b67f..7c612effa 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/OmrFont.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/OmrFont.java @@ -30,11 +30,15 @@ import org.slf4j.LoggerFactory; import java.awt.Color; +import java.awt.Dimension; import java.awt.Font; import java.awt.FontFormatException; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; +import java.awt.Point; +import java.awt.Rectangle; import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; import java.awt.font.LineMetrics; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; @@ -47,6 +51,7 @@ import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Class OmrFont is meant to simplify the use of rendering symbols when using a @@ -63,22 +68,29 @@ public abstract class OmrFont private static final Logger logger = LoggerFactory.getLogger(OmrFont.class); - /** Ratio to be applied for tiny symbols. */ + /** Ratio to be applied for tiny symbols (as used in shape buttons). */ public static final double RATIO_TINY = constants.tinyRatio.getValue(); /** Ratio to be applied for small shapes. */ public static final double RATIO_SMALL = constants.smallRatio.getValue(); - /** AffineTransform for tiny displays. */ - public static final AffineTransform TRANSFORM_TINY = AffineTransform.getScaleInstance( - RATIO_TINY, - RATIO_TINY); + /** Ratio to be applied for metronome shapes. */ + public static final double RATIO_METRO = constants.metroRatio.getValue(); + + // /** AffineTransform for tiny displays. */ + // public static final AffineTransform TRANSFORM_TINY = + // AffineTransform.getScaleInstance(RATIO_TINY, RATIO_TINY); /** AffineTransform for small shapes. */ public static final AffineTransform TRANSFORM_SMALL = AffineTransform.getScaleInstance( RATIO_SMALL, RATIO_SMALL); + /** AffineTransform for metronome shapes. */ + public static final AffineTransform TRANSFORM_METRO = AffineTransform.getScaleInstance( + RATIO_METRO, + RATIO_METRO); + /** Default color for images. */ public static final Color defaultImageColor = Color.BLACK; @@ -86,9 +98,9 @@ public abstract class OmrFont public static final FontRenderContext frc = new FontRenderContext(null, true, true); /** - * Cache for all created fonts (music and text), based on name and size. + * Cache for all created fonts (music and text), based on name and point size. *

    - * Only PLAIN style is cached. + * Only the PLAIN style is cached. * If a different style is desired, the caller must derive it from the cached plain one. */ private static final Map> fontCache = new HashMap<>(); @@ -118,6 +130,67 @@ protected OmrFont (String fontName, //~ Methods ------------------------------------------------------------------------------------ + //-------------// + // computeSize // + //-------------// + /** + * Compute the point size so that the content would fit the target dimension. + * + * @param content the provided content string + * @param dim the target dimension + * @return the best font size + */ + public int computeSize (String content, + Dimension dim) + { + Objects.requireNonNull(content, "OmrFont.computeSize. Content is null"); + Objects.requireNonNull(content, "OmrFont.computeSize. Dimension is null"); + + final GlyphVector glyphVector = createGlyphVector(frc, content); + final Rectangle2D rect = glyphVector.getVisualBounds(); + final float ratio = (dim.width >= dim.height) // + ? dim.width / (float) rect.getWidth() + : dim.height / (float) rect.getHeight(); + final float s2d = getSize2D(); + final int sz = (int) Math.rint(ratio * s2d); + logger.debug( + "OmrFont.computeSize {} f: {} dim: {} ratio: {} size: {} content: {}", + getFontName(), + s2d, + dim, + ratio, + sz, + content); + return sz; + } + + //-----------------// + // computeLocation // + //-----------------// + /** + * Compute the baseline location, based on content and bounds. + * + * @param content the provided content string + * @param bounds the underlying glyph bounding box + * @return the computed location + */ + public Point computeLocation (String content, + Rectangle bounds) + { + Objects.requireNonNull(content, "OmrFont.computeLocation. Content is null"); + Objects.requireNonNull(content, "OmrFont.computeLocation. Bounds are null"); + + final GlyphVector glyphVector = createGlyphVector(frc, content); + final Rectangle2D rect = glyphVector.getVisualBounds(); + final double rectDy = rect.getY() / rect.getHeight(); + final double ratio = (bounds.width >= bounds.height) // + ? bounds.width / rect.getWidth() + : bounds.height / rect.getHeight(); + final int y = bounds.y + (int) Math.rint(rectDy * ratio); + + return new Point(bounds.x, y); + } + //----------------// // getLineMetrics // //----------------// @@ -154,7 +227,7 @@ public TextLayout layout (String str) public TextLayout layout (String str, AffineTransform fat) { - Font font = (fat == null) ? this : this.deriveFont(fat); + final Font font = (fat == null) ? this : this.deriveFont(fat); return new TextLayout(str, font, frc); } @@ -190,7 +263,7 @@ protected static void cacheFont (Font font) * * @param fontName font name (e.g. "Finale Jazz") * @param fileName file name (e.g. "FinaleJazz.otf") - * @param size the desired size for this font + * @param size the desired point size for this font * @return the cached or created font */ private static Font createFont (String fontName, @@ -246,7 +319,7 @@ private static Font createFont (String fontName, * Try to retrieve the font defined by its name and size from the font cache. * * @param fontName font name (family name actually) - * @param size desired font size + * @param size desired font point size * @return the cached font or null */ private static Font getCachedFont (String fontName, @@ -312,12 +385,14 @@ protected static Font getFont (String fontName, // Try to derive from another size final Font any = getCachedFontAnySize(fontName); - if (any != null) + if (any != null) { font = any.deriveFont((float) size); + } - // We need to create it - if (font == null) + if (font == null) { + // We need to create it font = createFont(fontName, fileName, size); + } } return (style == Font.PLAIN) ? font : font.deriveFont(style); @@ -350,7 +425,7 @@ public static void paint (Graphics2D g, g, (float) (location.getX() + toTextOrigin.getX()), (float) (location.getY() + toTextOrigin.getY())); - } catch (ConcurrentModificationException ignored) { + } catch (ConcurrentModificationException ignored) { // } catch (Exception ex) { logger.warn("Cannot paint at " + location, ex); } @@ -364,6 +439,9 @@ public static void paint (Graphics2D g, private static class Constants extends ConstantSet { + private final Constant.Ratio metroRatio = new Constant.Ratio( + 0.5, + "Ratio applied to metronome note shapes"); private final Constant.Ratio smallRatio = new Constant.Ratio( 0.67, diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/ShapeSymbol.java b/app/src/main/java/org/audiveris/omr/ui/symbol/ShapeSymbol.java index 42b69fe82..7e765d362 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/ShapeSymbol.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/ShapeSymbol.java @@ -106,19 +106,20 @@ public class ShapeSymbol 0); // m12 /** A transformation to turn 1 quadrant clockwise. */ - protected static final AffineTransform quadrantRotateOne = - AffineTransform.getQuadrantRotateInstance(1); + protected static final AffineTransform quadrantRotateOne = AffineTransform + .getQuadrantRotateInstance(1); /** A transformation to turn 2 quadrants clockwise. */ - protected static final AffineTransform quadrantRotateTwo = - AffineTransform.getQuadrantRotateInstance(2); + protected static final AffineTransform quadrantRotateTwo = AffineTransform + .getQuadrantRotateInstance(2); /** The symbol meta data. */ public static final DataFlavor DATA_FLAVOR = new DataFlavor(ShapeSymbol.class, "shape-symbol"); /** Composite used for decoration. */ - protected static final AlphaComposite decoComposite = - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.15f); + protected static final AlphaComposite decoComposite = AlphaComposite.getInstance( + AlphaComposite.SRC_OVER, + 0.15f); //~ Instance fields ---------------------------------------------------------------------------- @@ -199,8 +200,10 @@ public SymbolImage buildImage (MusicFont font, // Allocate image of proper size Rectangle intRect = p.rect.getBounds(); - SymbolImage img = - new SymbolImage(intRect.width, intRect.height, PointUtil.rounded(p.offset)); + SymbolImage img = new SymbolImage( + intRect.width, + intRect.height, + PointUtil.rounded(p.offset)); // Paint the image Graphics2D g = (Graphics2D) img.getGraphics(); @@ -572,8 +575,7 @@ public Object getTransferData (DataFlavor flavor) @Override public DataFlavor[] getTransferDataFlavors () { - return new DataFlavor[] - { DATA_FLAVOR }; + return new DataFlavor[] { DATA_FLAVOR }; } //----------// @@ -647,19 +649,21 @@ public boolean isDecorated () //-------// /** * Actual painting, to be redefined by subclasses if needed. + *

    + * This default implementation paints only the 'params.layout' item. * * @param g graphics context - * @param p the parameters fed by getParams() + * @param params the parameters fed by getParams() * @param location where to paint * @param alignment relative position of provided location WRT symbol */ protected void paint (Graphics2D g, - Params p, + Params params, Point2D location, Alignment alignment) { logger.trace("ShapeSymbol.paint {}", this); - OmrFont.paint(g, p.layout, location, alignment); + OmrFont.paint(g, params.layout, location, alignment); } //-----------// @@ -729,8 +733,8 @@ protected boolean supportsDecoration () @Override public String toString () { - return new StringBuilder(getClass().getSimpleName()).append("{").append(internals()) - .append("}").toString(); + return new StringBuilder(getClass().getSimpleName()).append("{").append(internals()).append( + "}").toString(); } //-------------// @@ -756,7 +760,7 @@ public void updateModel (Sheet sheet) * Tell the symbol that it can update its model with staff informations. *

    * This is useful when the dragged item enters a staff, since it can adapt itself to - * staff informations (such as the typical beam thickness for small staff). + * staff information (such as the typical beam thickness for small staff). * * @param staff underlying staff */ @@ -775,7 +779,6 @@ public void updateModel (Staff staff) */ protected static class Params { - /** * Specific offset, if any, for focus center off of area center. * Since user pointing location is by default taken as center of 'rect' bounds. diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/SymbolRipper.java b/app/src/main/java/org/audiveris/omr/ui/symbol/SymbolRipper.java index b9012c767..f4e8d83a5 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/SymbolRipper.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/SymbolRipper.java @@ -181,6 +181,9 @@ public void stateChanged (ChangeEvent e) // y symbol private final LDoubleField hSym = new LDoubleField(false, "hSym", "h symbol", f); + // Advance symbol + private final LDoubleField aSym = new LDoubleField(false, "advance", "symbol advance", f); + //~ Constructors ------------------------------------------------------------------------------- /** @@ -189,30 +192,55 @@ public void stateChanged (ChangeEvent e) public SymbolRipper () { // Actors - drawing=new Drawing(); + drawing = new Drawing(); - fontBase.setModel(new SpinnerListModel(new Integer[]{0,0xf000,0x1_d100}));SpinnerUtil.setRightAlignment(fontBase);SpinnerUtil.fixIntegerList(fontBase); + fontBase.setModel(new SpinnerListModel(new Integer[] { 0, 0xf000, 0x1d100 })); + SpinnerUtil.setRightAlignment(fontBase); + SpinnerUtil.fixIntegerList(fontBase); - fontName.setModel(new SpinnerListModel(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames())); + fontName.setModel( + new SpinnerListModel( + GraphicsEnvironment.getLocalGraphicsEnvironment() + .getAvailableFontFamilyNames())); - pointCode.setModel(new SpinnerNumberModel(0x1_d100,0,0x1_d1ff,1)); + pointCode.setModel(new SpinnerNumberModel(0x1d100, 0, 0x1d1ff, 1)); // Initial values - if(true){fontName.getSpinner().setValue("Bravura");fontBase.setValue(0); // (for Bravura) - pointCode.setValue(0xE0A4); // Quarter note (in Bravura) - }else{fontName.getSpinner().setValue("MusicalSymbols");fontBase.setValue(fontBase.getModel().getNextValue()); // (for MusicalSymbols) - pointCode.setValue(113); // Quarter note (in MusicalSymbols) + if (true) { + fontName.getSpinner().setValue("Bravura"); + fontBase.setValue(0); // (for Bravura) + pointCode.setValue(0xE0A4); // Quarter note (in Bravura) + } else { + fontName.getSpinner().setValue("MusicalSymbols"); + fontBase.setValue(fontBase.getModel().getNextValue()); // (for MusicalSymbols) + pointCode.setValue(113); // Quarter note (in MusicalSymbols) } - fontSize.setValue(200);width.setValue(400);height.setValue(500);xOffset.setValue(200);yOffset.setValue(300); + fontSize.setValue(200); + width.setValue(400); + height.setValue(500); + xOffset.setValue(200); + yOffset.setValue(300); - changeCode();defineFont(); + changeCode(); + defineFont(); // Listeners - fontName.addChangeListener(paramListener);fontBase.addChangeListener(paramListener);fontSize.addChangeListener(paramListener);pointCode.addChangeListener(paramListener);hexaCode.addChangeListener(paramListener);xOffset.addChangeListener(paramListener);yOffset.addChangeListener(paramListener);width.addChangeListener(paramListener);height.addChangeListener(paramListener); + fontName.addChangeListener(paramListener); + fontBase.addChangeListener(paramListener); + fontSize.addChangeListener(paramListener); + pointCode.addChangeListener(paramListener); + hexaCode.addChangeListener(paramListener); + xOffset.addChangeListener(paramListener); + yOffset.addChangeListener(paramListener); + width.addChangeListener(paramListener); + height.addChangeListener(paramListener); // Global layout - if(!standAlone){frame=defineLayout(new JFrame("Symbol Ripper"));OmrGui.getApplication().show(frame);} + if (!standAlone) { + frame = defineLayout(new JFrame("Symbol Ripper")); + OmrGui.getApplication().show(frame); + } } //~ Methods ------------------------------------------------------------------------------------ @@ -243,6 +271,7 @@ private BufferedImage buildImage () ySym.setValue(rect.getY()); wSym.setValue(rect.getWidth()); hSym.setValue(rect.getHeight()); + aSym.setValue(layout.getAdvance()); return img; } @@ -321,7 +350,7 @@ public JFrame getFrame () //---------------// private JPanel getParamPanel () { - final FormLayout layout = Panel.makeFormLayout(13, 2, "right:", "35dlu", "45dlu"); + final FormLayout layout = Panel.makeFormLayout(14, 2, "right:", "35dlu", "45dlu"); FormBuilder builder = FormBuilder.create().layout(layout).panel(new Panel()); int r = 1; // -------------------------------- builder.addSeparator("Font").xyw(1, r, 7); @@ -364,6 +393,10 @@ private JPanel getParamPanel () r += 2; // -------------------------------- builder.addSeparator("Symbol").xyw(1, r, 7); + r += 2; // -------------------------------- + builder.addRaw(aSym.getLabel()).xy(5, r); + builder.addRaw(aSym.getField()).xy(7, r); + r += 2; // -------------------------------- builder.addRaw(xSym.getLabel()).xy(1, r); builder.addRaw(xSym.getField()).xy(3, r); @@ -466,13 +499,6 @@ public void paintComponent (Graphics g) yOffset.getValue()); g.setColor(Color.RED); g2.draw(rect); - - // Debug - TextLayout layout = new TextLayout(string, musicFont, frc); - logger.debug( - "getAdvance(): {} getVisibleAdvance(): {}", - layout.getAdvance(), - layout.getVisibleAdvance()); } } } diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/Symbols.java b/app/src/main/java/org/audiveris/omr/ui/symbol/Symbols.java index c3520c338..48b1154a1 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/Symbols.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/Symbols.java @@ -23,11 +23,13 @@ import org.audiveris.omr.glyph.Shape; import static org.audiveris.omr.glyph.Shape.*; +import static org.audiveris.omr.ui.symbol.OmrFont.TRANSFORM_METRO; import static org.audiveris.omr.ui.symbol.OmrFont.TRANSFORM_SMALL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; import java.util.List; @@ -59,6 +61,9 @@ public abstract class Symbols public static final CodeRange PRIVATE_USE_AREA = new CodeRange(0xE000, 0xF8FF); + /** Code for a space character. */ + public static final int SPACE = 0x20; + //~ Instance fields ---------------------------------------------------------------------------- /** @@ -91,7 +96,7 @@ public abstract class Symbols * @param shape the provided music shape * @return code point or null if not supported */ - protected abstract int[] getCode (Shape shape); + public abstract int[] getCode (Shape shape); //---------------// // getCodeRanges // @@ -169,6 +174,15 @@ protected void mapFlatKey (Shape shape, symbolMap.put(shape, new KeyFlatSymbol(key, shape, family())); } + //----------// + // mapMetro // + //----------// + protected void mapMetro (Shape shape, + Shape root) + { + symbolMap.put(shape, new TransformedSymbol(shape, root, TRANSFORM_METRO, family())); + } + //-----------// // mapNumDen // //-----------// @@ -258,15 +272,25 @@ protected void populateSymbols () mapSmall(SMALL_FLAG, FLAG_1); mapSmall(SMALL_FLAG_DOWN, FLAG_1_DOWN); + // Metronome symbols + mapMetro(METRO_WHOLE, WHOLE_NOTE); + mapMetro(METRO_HALF, HALF_NOTE_UP); + mapMetro(METRO_DOTTED_HALF, DOTTED_HALF_NOTE_UP); + mapMetro(METRO_QUARTER, QUARTER_NOTE_UP); + mapMetro(METRO_DOTTED_QUARTER, DOTTED_QUARTER_NOTE_UP); + mapMetro(METRO_EIGHTH, EIGHTH_NOTE_UP); + mapMetro(METRO_DOTTED_EIGHTH, DOTTED_EIGHTH_NOTE_UP); + mapMetro(METRO_SIXTEENTH, SIXTEENTH_NOTE_UP); + mapMetro(METRO_DOTTED_SIXTEENTH, DOTTED_SIXTEENTH_NOTE_UP); + // Specific symbols symbolMap.put(BRACE, new BraceSymbol(family())); symbolMap.put(ENDING, new EndingSymbol(false, family())); symbolMap.put(ENDING_WRL, new EndingSymbol(true, family())); symbolMap.put(SMALL_FLAG_SLASH, new SlashedFlagSymbol(SMALL_FLAG_SLASH, family())); - symbolMap.put( - SMALL_FLAG_SLASH_DOWN, - new SlashedFlagSymbol(SMALL_FLAG_SLASH_DOWN, family())); + symbolMap + .put(SMALL_FLAG_SLASH_DOWN, new SlashedFlagSymbol(SMALL_FLAG_SLASH_DOWN, family())); symbolMap.put(FLAT, new FlatSymbol(FLAT, family())); symbolMap.put(DOUBLE_FLAT, new FlatSymbol(DOUBLE_FLAT, family())); @@ -276,6 +300,8 @@ protected void populateSymbols () symbolMap.put(QUARTER_NOTE_UP, new CompoundNoteSymbol(QUARTER_NOTE_UP, family())); symbolMap.put(QUARTER_NOTE_DOWN, new CompoundNoteSymbol(QUARTER_NOTE_DOWN, family())); + symbolMap.put(METRONOME, new MetronomeSymbol(METRO_QUARTER, family())); + symbolMap.put(NON_DRAGGABLE, new NonDraggableSymbol(family())); symbolMap.put(REPEAT_TWO_BARS, new RepeatBarSymbol(REPEAT_TWO_BARS, family())); @@ -364,11 +390,39 @@ protected void populateSymbols () * @param codes sequence of one int or more parameters * @return the int array */ - protected static int[] ints (int... codes) + public static int[] ints (int... codes) { return codes; } + //--------// + // shrink // + //--------// + /** + * Report an array where the space characters have been removed. + * + * @param codes the input array + * @return the purged array + */ + public static int[] shrink (int... codes) + { + final int[] output = new int[codes.length]; + + int j = 0; + for (int i = 0; i < codes.length; i++) { + final int code = codes[i]; + if (code != SPACE) { + output[j++] = code; + } + } + + if (j == codes.length) { + return codes; + } + + return Arrays.copyOf(output, j); + } + //~ Inner Classes ------------------------------------------------------------------------------ //-----------// @@ -394,8 +448,8 @@ public CodeRange (int start, @Override public String toString () { - return new StringBuilder().append('[').append(start).append("..").append(stop).append( - ']').toString(); + return new StringBuilder().append('[').append(start).append("..").append(stop) + .append(']').toString(); } } } diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/TextFamily.java b/app/src/main/java/org/audiveris/omr/ui/symbol/TextFamily.java index 71e126a4f..e48da7103 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/TextFamily.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/TextFamily.java @@ -35,10 +35,10 @@ */ public enum TextFamily { - /** Standard text family. */ + /** Standard text family, without serif. */ SansSerif("Sans Serif", null), - /** Standard text family. */ + /** Standard text family, with serif. */ Serif("Serif", null), /** Jazz text family. */ @@ -92,7 +92,7 @@ public static TextFamily valueOfName (String value) } } - logger.warn("No music family for value: \"{}\"", value); + logger.warn("No text family for value: \"{}\"", value); return null; } diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/TextFont.java b/app/src/main/java/org/audiveris/omr/ui/symbol/TextFont.java index f7c2b5a03..744d8b71c 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/TextFont.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/TextFont.java @@ -23,6 +23,8 @@ import org.audiveris.omr.constant.Constant; import org.audiveris.omr.constant.ConstantSet; +import org.audiveris.omr.sheet.SheetStub; +import org.audiveris.omr.sheet.ui.StubsController; import org.audiveris.omr.text.FontInfo; import org.audiveris.omr.util.param.ConstantBasedParam; import org.audiveris.omr.util.param.Param; @@ -30,10 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.awt.Dimension; import java.awt.Font; -import java.awt.font.GlyphVector; -import java.awt.geom.Rectangle2D; /** * Class TextFont is meant to simplify the use of text font for pieces of text @@ -73,7 +72,7 @@ public TextFont (Font font) } /** - * Creates a font based on OCR font information. + * Creates a new TextFont based on OCR-based font information. * * @param info OCR-based font information */ @@ -87,7 +86,7 @@ public TextFont (FontInfo info) } /** - * Creates a new TextFont object of provided point size, with default font name + * Creates a new TextFont object of provided point size, with default font name * and plain style. * * @param size the point size of the Font @@ -98,7 +97,7 @@ public TextFont (int size) } /** - * Creates a new TextFont object. + * Creates a new TextFont object. * * @param fontName the font name. This can be a font face name or a font * family name, and may represent either a logical font or @@ -123,78 +122,11 @@ public TextFont (String fontName, @Override public TextFont deriveFont (float pointSize) { - final Font font = super.deriveFont(pointSize); - return new TextFont(font); + return new TextFont(super.deriveFont(pointSize)); } //~ Static Methods ----------------------------------------------------------------------------- - //-----------------// - // computeFontSize // - //-----------------// - /** - * Convenient method to compute a font size using a string content and dimension. - * - * @param content the string value - * @param fontInfo OCR-based font information - * @param dim string dimension in pixels - * @return the computed font size - */ - public static Float computeFontSize (String content, - FontInfo fontInfo, - Dimension dim) - { - if (content == null) { - return null; - } - - Font font = new TextFont(fontInfo); - float fontSize = font.getSize2D(); - GlyphVector glyphVector = font.createGlyphVector(frc, content); - Rectangle2D basicRect = glyphVector.getVisualBounds(); - - float ratio; - if (dim.width >= dim.height) { - ratio = dim.width / (float) basicRect.getWidth(); - } else { - ratio = dim.height / (float) basicRect.getHeight(); - } - - // To protect against crazy OCR bounds - final float maxRatio = constants.maxFontResizeRatio.getValue().floatValue(); - logger.debug("font resize ratio: {} {}", ratio, content); - ratio = Math.min(maxRatio, ratio); - - return fontSize * ratio; - } - - //-----------------// - // computeFontSize // - //-----------------// - /** - * Convenient method to compute a font size using a string content and width. - * - * @param content the string value - * @param fontInfo OCR-based font information - * @param width string width in pixels - * @return the computed font size - */ - public static Float computeFontSize (String content, - FontInfo fontInfo, - int width) - { - if (content == null) { - return null; - } - - Font font = new TextFont(fontInfo); - float fontSize = font.getSize2D(); - GlyphVector glyphVector = font.createGlyphVector(frc, content); - Rectangle2D basicRect = glyphVector.getVisualBounds(); - - return fontSize * (width / (float) basicRect.getWidth()); - } - //--------// // create // //--------// @@ -230,6 +162,20 @@ public static TextFont getBaseFontBySize (TextFamily family, return getTextFont(family, pointSize); } + //------------------// + // getCurrentFamily // + //------------------// + /** + * Report the text family used in the sheet currently displayed. + * + * @return the current sheet text family, null if no sheet is displayed + */ + public static TextFamily getCurrentFamily () + { + final SheetStub stub = StubsController.getInstance().getSelectedStub(); + return (stub != null) ? stub.getTextFamily() : null; + } + //-------------// // getTextFont // //-------------// @@ -254,7 +200,6 @@ public static TextFont getTextFont (TextFamily family, private static class Constants extends ConstantSet { - private final Constant.Enum defaultTextFamily = new Constant.Enum<>( TextFamily.class, TextFamily.SansSerif, @@ -268,9 +213,5 @@ private static class Constants "points", 10, "Default font point size for texts"); - - private final Constant.Ratio maxFontResizeRatio = new Constant.Ratio( - 1.3, - "Maximum font increase ratio when recomputed"); } } diff --git a/app/src/main/java/org/audiveris/omr/ui/symbol/TextSymbol.java b/app/src/main/java/org/audiveris/omr/ui/symbol/TextSymbol.java index 955699343..afd0561cc 100644 --- a/app/src/main/java/org/audiveris/omr/ui/symbol/TextSymbol.java +++ b/app/src/main/java/org/audiveris/omr/ui/symbol/TextSymbol.java @@ -23,8 +23,6 @@ import org.audiveris.omr.glyph.Shape; import org.audiveris.omr.math.PointUtil; -import org.audiveris.omr.sheet.SheetStub; -import org.audiveris.omr.sheet.ui.StubsController; import org.audiveris.omr.sig.inter.WordInter; import org.audiveris.omr.text.FontInfo; import static org.audiveris.omr.ui.symbol.Alignment.TOP_LEFT; @@ -49,6 +47,8 @@ public class TextSymbol extends ShapeSymbol { + //~ Static fields/initializers ----------------------------------------------------------------- + private static final Logger logger = LoggerFactory.getLogger(TextSymbol.class); //~ Instance fields ---------------------------------------------------------------------------- @@ -148,10 +148,9 @@ protected MyParams getParams (MusicFont font) TextFamily theTextFamily = textFamily; if (theTextFamily == null) { - // Workaround to retrieve sheet text family - final StubsController controller = StubsController.getInstance(); - final SheetStub stub = controller.getSelectedStub(); - theTextFamily = (stub != null) ? stub.getTextFamily() : TextFamily.SansSerif; + theTextFamily = TextFont.getCurrentFamily(); + if (theTextFamily == null) + theTextFamily = TextFamily.SansSerif; } final int fontSize = (int) Math.rint(font.getSize2D() * RATIO_TINY); @@ -199,16 +198,17 @@ public TextFamily getTextFamily () //~ Inner Classes ------------------------------------------------------------------------------ - //--------// - // Params // - //--------// + //----------// + // MyParams // + //----------// protected static class MyParams extends ShapeSymbol.Params { - // offset: not used + // layout: text layout // rect: global image + // model WordInter.Model model; } diff --git a/app/src/main/java/org/audiveris/omr/util/Jaxb.java b/app/src/main/java/org/audiveris/omr/util/Jaxb.java index 185ad1154..2bb1ae0d5 100644 --- a/app/src/main/java/org/audiveris/omr/util/Jaxb.java +++ b/app/src/main/java/org/audiveris/omr/util/Jaxb.java @@ -684,8 +684,8 @@ public Line2D getLine () @Override public String toString () { - return new StringBuilder("Line2DF{").append("p1:").append(p1).append(",p2:").append( - p2).append('}').toString(); + return new StringBuilder("Line2DF{").append("p1:").append(p1).append(",p2:") + .append(p2).append('}').toString(); } } } @@ -1144,9 +1144,9 @@ public Rectangle getRectangle () @Override public String toString () { - return new StringBuilder("RectangleF{").append("x:").append(x).append(",y:").append( - y).append(",w:").append(width).append(",h:").append(height).append('}') - .toString(); + return new StringBuilder("RectangleF{").append("x:").append(x).append(",y:") + .append(y).append(",w:").append(width).append(",h:").append(height) + .append('}').toString(); } } } diff --git a/app/src/main/java/org/audiveris/omr/util/RegexUtil.java b/app/src/main/java/org/audiveris/omr/util/RegexUtil.java index 1dcc532d4..282387320 100644 --- a/app/src/main/java/org/audiveris/omr/util/RegexUtil.java +++ b/app/src/main/java/org/audiveris/omr/util/RegexUtil.java @@ -71,8 +71,7 @@ public static String getGroup (Matcher matcher, try { result = matcher.group(name); - } catch (Exception ignored) { - } + } catch (Exception ignored) {} if (result != null) { return result; @@ -94,16 +93,6 @@ public static String getGroup (Matcher matcher, public static String group (String name, String content) { - StringBuilder sb = new StringBuilder(); - - sb.append("(?<"); - sb.append(name); - sb.append(">"); - - sb.append(content); - - sb.append(")"); - - return sb.toString(); + return "(?<" + name + ">" + content + ")"; } } diff --git a/app/src/main/java/org/audiveris/omr/util/StringUtil.java b/app/src/main/java/org/audiveris/omr/util/StringUtil.java index 0b0f3dd53..866483897 100644 --- a/app/src/main/java/org/audiveris/omr/util/StringUtil.java +++ b/app/src/main/java/org/audiveris/omr/util/StringUtil.java @@ -99,6 +99,11 @@ public static String codesOf (String s, return sb.toString(); } + public static String codesOf (String s) + { + return codesOf(s, false); + } + //---------// // compare // //---------// @@ -151,4 +156,32 @@ public static List parseStrings (String str) return strList; } + + //--------// + // shrink // + //--------// + /** + * Report a string where the space characters have been removed. + * + * @param input the input string + * @return the purged string + */ + public static String shrink (String input) + { + final char[] output = new char[input.length()]; + + int j = 0; + for (int i = 0; i < input.length(); i++) { + final char c = input.charAt(i); + if (c != ' ') { + output[j++] = c; + } + } + + if (j == input.length()) { + return input; + } + + return new String(output, 0, j); + } } diff --git a/app/src/test/java/org/audiveris/omr/sig/inter/MetronomeInterTest.java b/app/src/test/java/org/audiveris/omr/sig/inter/MetronomeInterTest.java new file mode 100644 index 000000000..5746b789e --- /dev/null +++ b/app/src/test/java/org/audiveris/omr/sig/inter/MetronomeInterTest.java @@ -0,0 +1,184 @@ +//------------------------------------------------------------------------------------------------// +// // +// M e t r o n o m e I n t e r T e s t // +// // +//------------------------------------------------------------------------------------------------// +// +// +// Copyright © Audiveris 2023. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, either version +// 3 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with this +// program. If not, see . +//------------------------------------------------------------------------------------------------// +// +package org.audiveris.omr.sig.inter; + +import org.audiveris.omr.util.ClassUtil; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +/** + * Unitary tests for class MetronomeInterTest . + * + * @author Hervé Bitteur + */ +public class MetronomeInterTest +{ + /** Store output in dedicated file. */ + private static final PrintWriter out = getPrintWriter(new File("../data/metronome-tests.log")); + + @Test + public void test_01 () + { + t("J =116", true); + } + + @Test + public void test_02 () + { + t("J=116", true); + } + + @Test + public void test_03 () + { + t("Slowly J = 116", true); + } + + @Test + public void test_04 () + { + t("Slowly J=116 ", true); + } + + @Test + public void test_05 () + { + t("Slowly (J = 116)", true); + } + + @Test + public void test_06 () + { + t("Slowly (J = ca. 116)", true); + } + + @Test + public void test_07 () + { + t("Slowly ( J = 116 env.)", true); + } + + @Test + public void test_08 () + { + t("Slowly ( J = 116-140)", true); + } + + @Test + public void test_09 () + { + t("Slowly ( J = ca. 116 - 140)", true); + } + + @Test + public void test_10 () + { + t("Allegretto quasi andantino (J = 69 env.)", true); + } + + @Test + public void test_11 () + { + t("Allegretto quasi andantino (J = ca. 100-120 env.)", true); + } + + @Test + public void test_12 () + { + t("Allegretto quasi andantino J = 100-120 grosso modo", true); + } + + @Test + public void test_13 () + { + t("Allegretto quasi andantino (J = 100 env.) garbage", true); + } + + @Test + public void test_14 () + { + t("Adagio .h = 126", true); + } + + @Test + public void test_15 () + { + t("Presto (J: 160)", false); // '=' OCR'd as ':' + } + + //----------------// + // getPrintWriter // + //----------------// + private static PrintWriter getPrintWriter (File file) + { + try { + final BufferedWriter bw = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(file), "UTF8")); + + return new PrintWriter(bw); + } catch (Exception ex) { + System.err.println("Error creating " + file + ex); + + return null; + } + } + + //---// + // t // + //---// + private void t (String text, + boolean exp) + { + // Print method name + StackTraceElement elem = ClassUtil.getCallingFrame(); + + if (elem != null) { + System.out.println(); + System.out.println("method : " + elem.getMethodName()); + out.println(); + out.println("method : " + elem.getMethodName()); + out.flush(); + } + + System.out.println("input : \"" + text + "\""); + out.println("input : \"" + text + "\""); + out.flush(); + + System.out.println("expected : " + exp); + out.println("expected : " + exp); + out.flush(); + + boolean result = MetronomeInter.fullValidityCheck(text); + System.out.println("output : " + result); + out.println("output : " + result); + out.flush(); + + assertEquals(exp, result); + } +} diff --git a/app/src/test/java/org/audiveris/omr/ui/symbol/MusicFontTest.java b/app/src/test/java/org/audiveris/omr/ui/symbol/MusicFontTest.java index 6aef37146..8cf5b62f0 100644 --- a/app/src/test/java/org/audiveris/omr/ui/symbol/MusicFontTest.java +++ b/app/src/test/java/org/audiveris/omr/ui/symbol/MusicFontTest.java @@ -147,8 +147,12 @@ public void textPrintout () g.drawString(Integer.toHexString(i), x + 10, y + 30); // Draw info - String info = - String.format(frm, r.getX(), r.getY(), r.getWidth(), r.getHeight()); + String info = String.format( + frm, + r.getX(), + r.getY(), + r.getWidth(), + r.getHeight()); g.setFont(infoFont); g.setColor(Color.GRAY); g.drawString(info, x + 5, (y + cellHeight) - 5); diff --git a/gradle.properties b/gradle.properties index b981391eb..800b1671c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,3 @@ netbeans.de-funfried-netbeans-plugins-externalcodeformatter.showNotifications=fa netbeans.de-funfried-netbeans-plugins-externalcodeformatter.enabledFormatter_2e_HTML=netbeans-formatter netbeans.de-funfried-netbeans-plugins-externalcodeformatter.useProjectSettings=false netbeans.de-funfried-netbeans-plugins-externalcodeformatter.enabledFormatter_2e_CSS=netbeans-formatter - -# Uncomment the two following lines to read run/debug arguments from data/args/default.txt file -#action.run.args=run --args=@data/args/default.txt -#action.debug.args=run --debug-jvm --args=@data/args/default.txt