From 1439839709809f04336cb42fd71025a32f8e04f3 Mon Sep 17 00:00:00 2001 From: drtootsie Date: Sun, 1 Mar 2026 10:15:41 -0600 Subject: [PATCH] Fix #151: Improve wedge export ordering in MusicXML --- .../audiveris/omr/score/PartwiseBuilder.java | 131 +++++++++++++++++- .../audiveris/omr/sig/inter/WedgeInter.java | 24 ++++ 2 files changed, 148 insertions(+), 7 deletions(-) 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 d9115c754..aee2f0861 100644 --- a/app/src/main/java/org/audiveris/omr/score/PartwiseBuilder.java +++ b/app/src/main/java/org/audiveris/omr/score/PartwiseBuilder.java @@ -2049,13 +2049,18 @@ private void processMeasure (Measure measure) // Clefs may be inserted further down the measure final ClefIterators clefIters = new ClefIterators(measure); + // Wedges are collected and sorted for the whole measure + final WedgeIterators wedgeIters = new WedgeIterators(measure); + // Insert clefs that occur before the first time slot final List slots = stack.getSlots(); if (slots.isEmpty()) { clefIters.push(null, null); + wedgeIters.push(Rational.ZERO); } else { clefIters.push(slots.get(0).getXOffset(), null); + wedgeIters.push(Rational.ZERO); } // Now voice per voice @@ -2074,6 +2079,7 @@ private void processMeasure (Measure measure) // Delegate to the chord children directly AbstractChordInter chord = voice.getWholeChord(); clefIters.push(measure.getWidth(), chord.getTopStaff()); + wedgeIters.push(chord.getTimeOffset()); processChord(chord); if (stack.getActualDuration() != null) { @@ -2096,6 +2102,9 @@ private void processMeasure (Measure measure) timeCounter = timeOffset; } + // Wedge events occurring at this time offset? + wedgeIters.push(timeOffset); + // Grace chord(s) before this chord? if (chord instanceof HeadChordInter headChord) { SmallChordInter small = headChord.getGraceChord(); @@ -2137,6 +2146,9 @@ private void processMeasure (Measure measure) // Clefs that occur after time slots, if any clefIters.push(null, null); + // Wedges that occur after time slots, if any + wedgeIters.push(null); + // Right Barline if (!measure.isDummy()) { processBarline(measure.getRightPartBarline(), RightLeftMiddle.RIGHT); @@ -2248,9 +2260,6 @@ private void processNote (AbstractNoteInter note) processBow((BowInter) other); } else if (rel instanceof ChordPedalRelation) { processPedal((PedalInter) other); - } else if (rel instanceof ChordWedgeRelation chordWedgeRelation) { - HorizontalSide side = chordWedgeRelation.getSide(); - processWedge((WedgeInter) other, side); } else if (rel instanceof ChordDynamicsRelation) { processDynamics((DynamicsInter) other); } else if (rel instanceof ChordArticulationRelation) { @@ -3275,7 +3284,8 @@ private void processTuplet (TupletInter tuplet) // processWedge // //--------------// private void processWedge (WedgeInter wedge, - HorizontalSide side) + HorizontalSide side, + AbstractNoteInter referenceNote) { try { logger.debug("Visiting {}", wedge); @@ -3288,7 +3298,7 @@ private void processWedge (WedgeInter wedge, pmWedge.setSpread(toTenths(wedge.getSpread(side))); // Staff? - Staff staff = current.note.getStaff(); + Staff staff = referenceNote.getStaff(); insertStaffId(direction, staff); // Start or stop? @@ -3308,7 +3318,7 @@ private void processWedge (WedgeInter wedge, // Placement direction.setPlacement( - (refPoint.getY() < current.note.getCenter().y) ? AboveBelow.ABOVE + (refPoint.getY() < referenceNote.getCenter().y) ? AboveBelow.ABOVE : AboveBelow.BELOW); // default-y @@ -3324,7 +3334,7 @@ private void processWedge (WedgeInter wedge, } // default-x using note left side (No offset for the time being) - pmWedge.setDefaultX(toTenths(refPoint.getX() - current.note.getCenterLeft().x)); + pmWedge.setDefaultX(toTenths(refPoint.getX() - referenceNote.getCenterLeft().x)); // Everything is OK directionType.setWedge(pmWedge); @@ -3586,6 +3596,113 @@ public void push (Integer xOffset, } } + //------------// + // TimedWedge // + //------------// + /** + * Helper class to keep track of a wedge event (start or stop) and its timing. + */ + private static class TimedWedge + { + final WedgeInter wedge; + + final HorizontalSide side; + + final AbstractNoteInter referenceNote; + + final Rational timeOffset; + + TimedWedge (WedgeInter wedge, + HorizontalSide side, + AbstractNoteInter referenceNote, + Rational timeOffset) + { + this.wedge = wedge; + this.side = side; + this.referenceNote = referenceNote; + this.timeOffset = timeOffset; + } + } + + //----------------// + // WedgeIterators // + //----------------// + /** + * Class to handle the insertion of wedges in a measure. + * Wedges are collected for the whole measure and sorted by time offset, + * to ensure that a wedge 'stop' never occurs before a 'start' in the MusicXML stream, + * even if they are linked to different voices. + */ + private class WedgeIterators + { + /** Sorted list of wedge events in this measure. */ + private final List events = new ArrayList<>(); + + /** Iterator on events. */ + private ListIterator it; + + WedgeIterators (Measure measure) + { + final SIGraph sig = measure.getStack().getSystem().getSig(); + + for (Inter inter : sig.inters(WedgeInter.class)) { + WedgeInter wedge = (WedgeInter) inter; + + for (HorizontalSide side : HorizontalSide.values()) { + AbstractChordInter chord = wedge.getChord(side); + + if ((chord != null) && (chord.getMeasure() == measure)) { + // We take the first note of the chord as reference + AbstractNoteInter refNote = (AbstractNoteInter) chord.getNotes().get(0); + events.add(new TimedWedge(wedge, side, refNote, chord.getTimeOffset())); + } + } + } + + // Sort by time offset, and for same offset, ensure START (LEFT) comes before STOP (RIGHT) + Collections.sort(events, (w1, w2) -> { + int cmp = w1.timeOffset.compareTo(w2.timeOffset); + if (cmp != 0) { + return cmp; + } + if (w1.side == w2.side) { + return 0; + } + return (w1.side == LEFT) ? -1 : 1; + }); + + it = events.listIterator(); + } + + /** + * Push as far as possible the relevant wedge events, according to the + * current time offset. + * + * @param timeOffset the time offset of the current position in measure + */ + public void push (Rational timeOffset) + { + if (timeOffset != null) { + while (it.hasNext()) { + TimedWedge event = it.next(); + + if (event.timeOffset.compareTo(timeOffset) <= 0) { + processWedge(event.wedge, event.side, event.referenceNote); + } else { + it.previous(); + break; + } + } + } else { + // Flush all remaining events + while (it.hasNext()) { + TimedWedge event = it.next(); + processWedge(event.wedge, event.side, event.referenceNote); + } + } + } + } + //-----------// // Constants // //-----------// diff --git a/app/src/main/java/org/audiveris/omr/sig/inter/WedgeInter.java b/app/src/main/java/org/audiveris/omr/sig/inter/WedgeInter.java index 1aeed0365..344bae2cc 100644 --- a/app/src/main/java/org/audiveris/omr/sig/inter/WedgeInter.java +++ b/app/src/main/java/org/audiveris/omr/sig/inter/WedgeInter.java @@ -281,6 +281,30 @@ public double getSpread (HorizontalSide side) } } + //----------// + // getChord // + //----------// + /** + * Report the chord linked to this wedge on the provided side. + * + * @param side the provided side + * @return the linked chord, if any, otherwise null + */ + public AbstractChordInter getChord (HorizontalSide side) + { + if (sig != null) { + for (org.audiveris.omr.sig.relation.Relation rel : sig.edgesOf(this)) { + if (rel instanceof ChordWedgeRelation chordWedgeRelation) { + if (chordWedgeRelation.getSide() == side) { + return (AbstractChordInter) sig.getOppositeInter(this, rel); + } + } + } + } + + return null; + } + //-------------// // searchLinks // //-------------//