瀏覽代碼

merge osmd-public/develop: fix tuplet brackets + lyrics spacing, add EngravingRule IgnoreRepeatedDynamics (default false)

fixes tuplets not having a bracket or number despite needing them (if it wasn't given via XML)
Reduces measure width for measures with long lyrics by adding spacing to short notes with long lyrics, reducing measure elongation

adds new EngravingRule IgnoreRepeatedDynamics (default false),
which works for most cases, except e.g. in the second volta of a repetition, where it removes a desired (repeated) dynamic.
100% correctness also for these cases would need a more complex implementation as an after-reading-module.
(this is still useful for programs like Guitar Pro 7 which export complete nonsense to MusicXML like 12 mf directions on a single chord)
sschmidTU 1 年之前
父節點
當前提交
6ff5a6f203
共有 30 個文件被更改,包括 1463 次插入56 次删除
  1. 4 0
      .gitignore
  2. 28 0
      src/MusicalScore/Graphical/EngravingRules.ts
  3. 8 0
      src/MusicalScore/Graphical/GraphicalLyricEntry.ts
  4. 32 7
      src/MusicalScore/Graphical/MusicSheetCalculator.ts
  5. 51 0
      src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts
  6. 7 2
      src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalSymbolFactory.ts
  7. 52 13
      src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts
  8. 11 6
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts
  9. 2 1
      src/MusicalScore/Graphical/VexFlow/VexFlowVoiceEntry.ts
  10. 19 15
      src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts
  11. 10 1
      src/MusicalScore/ScoreIO/VoiceGenerator.ts
  12. 1 0
      src/MusicalScore/VoiceData/Beam.ts
  13. 2 2
      src/MusicalScore/VoiceData/SourceStaffEntry.ts
  14. 25 0
      src/MusicalScore/VoiceData/Tuplet.ts
  15. 1 0
      src/VexFlowPatch/readme.txt
  16. 7 2
      src/VexFlowPatch/src/stavenote.js
  17. 6 0
      test/Util/generateImages_browserless.mjs
  18. 14 1
      test/Util/visual_regression.sh
  19. 1 1
      test/data/note_height_bottomline_test_sample.musicxml
  20. 1 1
      test/data/note_height_skyline_test_sample_with_chord_symbol_etc.musicxml
  21. 302 0
      test/data/test_chord_symbol_centering_short_symbols.musicxml
  22. 1 1
      test/data/test_chord_symbols_collision_high_notes_alignment.musicxml
  23. 二進制
      test/data/test_clef_measure_end_backup_nodes_Sibelius.musicxml
  24. 193 0
      test/data/test_decrescendo_crescendo_stop_start.musicxml
  25. 165 0
      test/data/test_lyrics_spacing_short_notes_four_characters.musicxml
  26. 1 1
      test/data/test_slide_glissando.musicxml
  27. 1 1
      test/data/test_soft-accent_cresc_decresc_single_note.musicxml
  28. 225 0
      test/data/test_tuplet_bracket_necessary.musicxml
  29. 292 0
      test/data/test_tuplet_bracket_tuplet_number.musicxml
  30. 1 1
      test/data/test_wedge_diminuendo_duplicated.musicxml

+ 4 - 0
.gitignore

@@ -13,6 +13,10 @@ npm-debug.log*
 pids
 *.pid
 *.seed
+.DS_Store
+
+# OS files
+.DS_Store
 
 # optional npm script-generated data
 visual_regression/

+ 28 - 0
src/MusicalScore/Graphical/EngravingRules.ts

@@ -123,6 +123,10 @@ export class EngravingRules {
     public ChordSymbolTextHeight: number;
     public ChordSymbolTextAlignment: TextAlignmentEnum;
     public ChordSymbolRelativeXOffset: number;
+    /** Additional x-shift for short chord symbols (e.g. C, but not Eb/7), to appear more centered. */
+    public ChordSymbolExtraXShiftForShortChordSymbols: number;
+    /** Threshold width below which to apply ChordSymbolExtraXShiftForShortChordSymbols. */
+    public ChordSymbolExtraXShiftWidthThreshold: number;
     public ChordSymbolXSpacing: number;
     public ChordOverlapAllowedIntoNextMeasure: number;
     public ChordSymbolYOffset: number;
@@ -158,6 +162,10 @@ export class EngravingRules {
      * (Bracketing all triplets can be cluttering)
      */
     public TripletsBracketed: boolean;
+    /** Whether to bracket like the XML says when 'bracket="no"' or "yes" is given.
+     * Otherwise, OSMD decides bracket usage.
+     * Note that sometimes the XML doesn't have any 'bracket' value. */
+    public TupletsBracketedUseXMLValue: boolean;
     public TupletNumberLabelHeight: number;
     public TupletNumberYOffset: number;
     public TupletNumberLimitConsecutiveRepetitions: boolean;
@@ -181,6 +189,18 @@ export class EngravingRules {
     public LyricsHeight: number;
     public LyricsYOffsetToStaffHeight: number;
     public LyricsYMarginToBottomLine: number;
+    /** Whether to enable x padding (to the right) for short notes, see LyricsXPaddingFactorForLongLyrics for the degree. */
+    public LyricsUseXPaddingForShortNotes: boolean;
+    /** How much spacing/padding should be added after notes with long lyrics on short notes
+     * (>4 characters on <8th note),
+     * so that the measure doesn't need to be elongated too much to avoid lyrics collisions.
+     * Default 0.8 = 8 pixels */
+    public LyricsXPaddingFactorForLongLyrics: number;
+    /** How wide a text needs to be to trigger lyrics padding for short notes.
+     * This is visual width, not number of characters, as e.g. 'zzz' is about as wide as 'iiii'.
+     * Default 3.3.
+     */
+    public LyricsXPaddingWidthThreshold: number;
     public VerticalBetweenLyricsDistance: number;
     public HorizontalBetweenLyricsDistance: number;
     public BetweenSyllableMaximumDistance: number;
@@ -353,6 +373,7 @@ export class EngravingRules {
     public RenderPedals: boolean;
     public DynamicExpressionMaxDistance: number;
     public DynamicExpressionSpacer: number;
+    public IgnoreRepeatedDynamics: boolean;
     public MpatMode: boolean;
 
     public ArticulationPlacementFromXML: boolean;
@@ -563,6 +584,8 @@ export class EngravingRules {
         this.ChordSymbolTextHeight = 2.0;
         this.ChordSymbolTextAlignment = TextAlignmentEnum.LeftBottom;
         this.ChordSymbolRelativeXOffset = -1.0;
+        this.ChordSymbolExtraXShiftForShortChordSymbols = 0.3;
+        this.ChordSymbolExtraXShiftWidthThreshold = 2.0;
         this.ChordSymbolXSpacing = 1.0;
         this.ChordOverlapAllowedIntoNextMeasure = 0;
         this.ChordSymbolYOffset = 0.1;
@@ -591,6 +614,7 @@ export class EngravingRules {
         this.TupletsRatioed = false;
         this.TupletsBracketed = false;
         this.TripletsBracketed = false; // special setting for triplets, overrides tuplet setting (for triplets only)
+        this.TupletsBracketedUseXMLValue = true;
         this.TupletNumberLabelHeight = 1.5 * EngravingRules.unit;
         this.TupletNumberYOffset = 0.5;
         this.TupletNumberLimitConsecutiveRepetitions = true;
@@ -647,6 +671,9 @@ export class EngravingRules {
         this.LyricsHeight = 2.0; // actually size of lyrics
         this.LyricsYOffsetToStaffHeight = 0.0; // distance between lyrics and staff. could partly be even lower/dynamic
         this.LyricsYMarginToBottomLine = 0.2;
+        this.LyricsUseXPaddingForShortNotes = true;
+        this.LyricsXPaddingFactorForLongLyrics = 0.8;
+        this.LyricsXPaddingWidthThreshold = 3.3;
         this.VerticalBetweenLyricsDistance = 0.5;
         this.HorizontalBetweenLyricsDistance = 0.2;
         this.BetweenSyllableMaximumDistance = 10.0;
@@ -664,6 +691,7 @@ export class EngravingRules {
         this.ContinuousTempoTextHeight = 2.3;
         this.DynamicExpressionMaxDistance = 2;
         this.DynamicExpressionSpacer = 0.5;
+        this.IgnoreRepeatedDynamics = false;
 
         // Line Widths
         this.VexFlowDefaultNotationFontScale = 39; // scales notes, including rests. default value 39 in Vexflow.

+ 8 - 0
src/MusicalScore/Graphical/GraphicalLyricEntry.ts

@@ -43,6 +43,14 @@ export class GraphicalLyricEntry {
         }
     }
 
+    public hasDashFromLyricWord(): boolean {
+        if (!this.ParentLyricWord) {
+            return false;
+        }
+        const lyricWordIndex: number = this.ParentLyricWord.GraphicalLyricsEntries.indexOf(this);
+        return this.ParentLyricWord.GraphicalLyricsEntries.length > 1 && lyricWordIndex < this.ParentLyricWord.GraphicalLyricsEntries.length - 1;
+    }
+
     public get LyricsEntry(): LyricsEntry {
         return this.lyricsEntry;
     }

+ 32 - 7
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -1157,7 +1157,14 @@ export abstract class MusicSheetCalculator {
                 for (const ve of voice.VoiceEntries) {
                     if (ve.Notes.length > 0) {
                         const firstNote: Note = ve.Notes[0];
-                        if (!firstNote.NoteTuplet) {
+                        if (!firstNote.NoteTuplet ||
+                            firstNote.NoteTuplet.shouldBeBracketed(
+                                this.rules.TupletsBracketedUseXMLValue,
+                                this.rules.TupletsBracketed,
+                                this.rules.TripletsBracketed
+                            )
+                        ) {
+                            // don't disable tuplet numbers under these conditions, reset consecutive tuplet count
                             currentTupletNumber = -1;
                             consecutiveTupletCount = 0;
                             currentTuplet = undefined;
@@ -1184,7 +1191,8 @@ export abstract class MusicSheetCalculator {
                             }
                         }
                         if (firstNote.NoteTuplet.TupletLabelNumber !== currentTupletNumber ||
-                            !typeLength.Equals(currentTypeLength)) {
+                            !typeLength.Equals(currentTypeLength) ||
+                            firstNote.NoteTuplet.Bracket) {
                             currentTupletNumber = firstNote.NoteTuplet.TupletLabelNumber;
                             currentTypeLength = typeLength;
                             consecutiveTupletCount = 0;
@@ -1290,17 +1298,23 @@ export abstract class MusicSheetCalculator {
         const endOfMeasure: number = parentMeasure.PositionAndShape.AbsolutePosition.x + parentMeasure.PositionAndShape.BorderRight;
         let maxNoteLength: Fraction = new Fraction(0, 0, 0);
         for (const staffEntry of container.StaffEntries) {
-            const currentMaxLength: Fraction = staffEntry?.sourceStaffEntry?.calculateMaxNoteLength();
+            const currentMaxLength: Fraction = staffEntry?.sourceStaffEntry?.calculateMaxNoteLength(false);
             if ( currentMaxLength?.gt(maxNoteLength) ) {
                 maxNoteLength = currentMaxLength;
             }
         }
+        const useStaffEntryBorderLeft: boolean = !isSoftAccent &&
+            graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo;
         const endPosInStaffLine: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
-            endAbsoluteTimestamp, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0);
+            endAbsoluteTimestamp, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0,
+            useStaffEntryBorderLeft);
 
         const beginOfNextNote: Fraction = Fraction.plus(endAbsoluteTimestamp, maxNoteLength);
+        // TODO for the last note of the piece (wedge ending after last note), this timestamp is incorrect, being after the last note
+        //   but there's a workaround in getRelativePositionInStaffLineFromTimestamp() via the variable endAfterRightStaffEntry
         const nextNotePosInStaffLine: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
-            beginOfNextNote, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0);
+            beginOfNextNote, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0,
+            graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo);
         const wedgePadding: number = this.rules.SoftAccentWedgePadding;
         const staffEntryWidth: number = container.getFirstNonNullStaffEntry().PositionAndShape.Size.width; // staff entry widths for whole notes is too long
         const sizeFactor: number = this.rules.SoftAccentSizeFactor;
@@ -2146,8 +2160,11 @@ export abstract class MusicSheetCalculator {
         }
     }
 
-    protected getRelativePositionInStaffLineFromTimestamp(timestamp: Fraction, verticalIndex: number, staffLine: StaffLine,
-                                                          multiStaffInstrument: boolean, firstVisibleMeasureRelativeX: number = 0.0): PointF2D {
+    protected getRelativePositionInStaffLineFromTimestamp(
+        timestamp: Fraction, verticalIndex: number, staffLine: StaffLine,
+        multiStaffInstrument: boolean, firstVisibleMeasureRelativeX: number = 0.0,
+        useLeftStaffEntryBorder: boolean = false
+    ): PointF2D {
         let relative: PointF2D = new PointF2D();
         let leftStaffEntry: GraphicalStaffEntry = undefined;
         let rightStaffEntry: GraphicalStaffEntry = undefined;
@@ -2167,8 +2184,16 @@ export abstract class MusicSheetCalculator {
             }
             let leftX: number = leftStaffEntry.PositionAndShape.RelativePosition.x + measureRelativeX;
             let rightX: number = rightStaffEntry.PositionAndShape.RelativePosition.x + rightStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
+            const endAfterRightStaffEntry: boolean = timestamp.RealValue > rightStaffEntry.getAbsoluteTimestamp().RealValue;
+            // endAfterRightStaffEntry is an unfortunate case where the timestamp isn't correct for the last note in the piece,
+            //   see test_wedge_diminuendo_duplicated.musicxml
             if (firstVisibleMeasureRelativeX > 0) {
                 rightX = rightStaffEntry.PositionAndShape.RelativePosition.x + measureRelativeX;
+            } else if (useLeftStaffEntryBorder &&
+                (leftStaffEntry.getAbsoluteTimestamp().RealValue === timestamp.RealValue || endAfterRightStaffEntry)
+            ) {
+                leftX = leftStaffEntry.PositionAndShape.RelativePosition.x + leftStaffEntry.PositionAndShape.BorderLeft + measureRelativeX;
+                rightX = leftX;
             }
             let timestampQuotient: number = 0.0;
             if (leftStaffEntry !== rightStaffEntry) {

+ 51 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts

@@ -31,6 +31,7 @@ import { TabNote } from "../../VoiceData/TabNote";
 import { PlacementEnum } from "../../VoiceData/Expressions/AbstractExpression";
 import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
 import { Slur } from "../../VoiceData/Expressions/ContinuousExpressions/Slur";
+import { GraphicalLyricEntry } from "../GraphicalLyricEntry";
 
 /**
  * Helper class, which contains static methods which actually convert
@@ -460,6 +461,56 @@ export class VexFlowConverter {
             vfnote = new VF.StaveNote(vfnoteStruct);
             (vfnote as any).stagger_same_whole_notes = rules.StaggerSameWholeNotes;
             //   it would be nice to only save this once, not for every note, but has to be accessible in stavenote.js
+            const lyricsEntries: GraphicalLyricEntry[] = gve.parentStaffEntry.LyricsEntries;
+            if (rules.RenderLyrics && rules.LyricsUseXPaddingForShortNotes && lyricsEntries.length > 0) {
+                // VexFlowPatch: add padding to the right for large lyrics,
+                //   so that measure doesn't need to be enlarged too much for spacing
+
+                let hasShortNotes: boolean = false;
+                let paddingMultiplier: number = 1;
+                for (const note of notes) {
+                    if (note.sourceNote.Length.RealValue <= 0.125) { // 8th or shorter
+                        hasShortNotes = true;
+                        if (note.sourceNote.Length.RealValue <= 0.0625) { // 16th or shorter
+                            paddingMultiplier = 1.7;
+                        }
+                        break;
+                    }
+                }
+
+                if (hasShortNotes) {
+                    let addPadding: boolean = false;
+                    for (const lyricsEntry of lyricsEntries) {
+                        const widthThreshold: number = rules.LyricsXPaddingWidthThreshold;
+                        // letters like i and l take less space, so we should use the visual width and not number of characters
+                        let currentLyricsWidth: number = lyricsEntry.GraphicalLabel.PositionAndShape.Size.width;
+                        if (lyricsEntry.hasDashFromLyricWord()) {
+                            currentLyricsWidth += 1.5;
+                        }
+                        if (currentLyricsWidth > widthThreshold) {
+                            paddingMultiplier *= currentLyricsWidth / widthThreshold;
+                            // check if we need padding because next staff entry also has long lyrics or it's the last note in the measure
+                            const currentStaffEntry: GraphicalStaffEntry = gve.parentStaffEntry;
+                            const measureStaffEntries: GraphicalStaffEntry[] = currentStaffEntry.parentMeasure.staffEntries;
+                            const currentStaffEntryIndex: number = measureStaffEntries.indexOf(currentStaffEntry);
+                            if (currentStaffEntryIndex === measureStaffEntries.length - 1) {
+                                // addPadding = true; // last note in the measure
+                                // probably unnecessary
+                            } else {
+                                addPadding = true;
+                            }
+                            break;
+                        }
+                        // for situations unlikely to cause overlap we shouldn't add padding,
+                        //   e.g. Brooke West sample (OSMD Function Test Chord Symbols) - width ~3.1 in measure 11 on 'ling', no padding needed.
+                        //   though Beethoven - Geliebte has only 8ths in measure 2 and is still problematic,
+                        //   so unfortunately we can't just check if the next note is 16th or less.
+                    }
+                    if (addPadding) {
+                        (vfnote as any).paddingRight = 10 * rules.LyricsXPaddingFactorForLongLyrics * paddingMultiplier;
+                    }
+                }
+            }
         }
         const lineShift: number = gve.notes[0].lineShift;
         if (lineShift !== 0) {

+ 7 - 2
src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalSymbolFactory.ts

@@ -225,9 +225,14 @@ export class VexFlowGraphicalSymbolFactory implements IGraphicalSymbolFactory {
                                                 );
             const graphicalLabel: GraphicalLabel = graphicalChordSymbolContainer.GraphicalLabel;
             graphicalLabel.PositionAndShape.RelativePosition.y -= rules.ChordSymbolYOffset;
-            graphicalLabel.PositionAndShape.RelativePosition.x += xShift;
-            // TODO check for available space until next staffEntry or chord symbol (x direction)
+            graphicalLabel.setLabelPositionAndShapeBorders(); // to get Size.width
+            let extraXShiftForShortChordSymbols: number = 0;
+            if (graphicalLabel.PositionAndShape.Size.width < rules.ChordSymbolExtraXShiftWidthThreshold) {
+                extraXShiftForShortChordSymbols = rules.ChordSymbolExtraXShiftForShortChordSymbols;
+            }
+            graphicalLabel.PositionAndShape.RelativePosition.x += xShift + extraXShiftForShortChordSymbols;
             graphicalLabel.setLabelPositionAndShapeBorders();
+            // TODO check for available space until next staffEntry or chord symbol? (x direction)
             graphicalChordSymbolContainer.PositionAndShape.calculateBoundingBox();
             graphicalStaffEntry.graphicalChordContainers.push(graphicalChordSymbolContainer);
 

+ 52 - 13
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -36,6 +36,7 @@ import {SkyBottomLineCalculator} from "../SkyBottomLineCalculator";
 import { NoteType } from "../../VoiceData/NoteType";
 import { Arpeggio } from "../../VoiceData/Arpeggio";
 import { GraphicalTie } from "../GraphicalTie";
+import { Note } from "../../VoiceData/Note";
 
 // type StemmableNote = VF.StemmableNote;
 
@@ -981,12 +982,21 @@ export class VexFlowMeasure extends GraphicalMeasure {
         if (!this.rules.AutoBeamTabs && this.isTabMeasure) { // could also use an option tabBeams to disable beams there completely
             return;
         }
-        let notesToAutoBeam: StemmableNote[] = [];
-        let consecutiveBeamableNotes: StemmableNote[] = [];
+        let autoBeamId: number = 60; // start with 60 to not collide (ids) with xml beams
+        /** Link between OSMD note (Note) and Vexflow note (StaveNote).
+         * For adding OSMD beams (note.NoteBeam), we also need the note (+ corresponding vfnote)
+         * This avoids needing to check (stavenote as any).beam, and registers the beam in the OSMD Note(.NoteBeam).
+         */
+        interface LinkedNote {
+            vfStaveNote: StaveNote;
+            sourceNote: Note;
+        }
+        let notesToAutoBeam: LinkedNote[] = [];
+        let consecutiveBeamableNotes: LinkedNote[] = [];
         let currentTuplet: Tuplet;
-        let tupletNotesToAutoBeam: StaveNote[] = [];
+        let tupletNotesToAutoBeam: LinkedNote[] = [];
         this.autoTupletVfBeams = [];
-        const separateAutoBeams: StemmableNote[][] = []; // a set of separate beams, each having a set of notes (StemmableNote[]).
+        const separateAutoBeams: LinkedNote[][] = []; // a set of separate beams, each having a set of notes (StemmableNote[]).
         this.autoVfBeams = []; // final VF.Beams will be pushed/collected into this
         let timeSignature: Fraction = this.parentSourceMeasure.ActiveTimeSignature;
         if (!timeSignature) { // this doesn't happen in OSMD, but maybe in a SourceGenerator
@@ -999,11 +1009,14 @@ export class VexFlowMeasure extends GraphicalMeasure {
                 }
             }
         }*/
-
         for (const staffEntry of this.staffEntries) {
             for (const gve of staffEntry.graphicalVoiceEntries) {
                 const vfStaveNote: StaveNote = <StaveNote> (gve as VexFlowVoiceEntry).vfStaveNote;
                 const gNote: GraphicalNote = gve.notes[0]; // TODO check for all notes within the graphical voice entry
+                const linkedNote: LinkedNote = {
+                    vfStaveNote: vfStaveNote,
+                    sourceNote: gNote.sourceNote
+                };
                 const isOnBeat: boolean = staffEntry.relInMeasureTimestamp.isOnBeat(timeSignature);
                 const haveTwoOrMoreNotesToBeamAlready: boolean = consecutiveBeamableNotes.length >= 2;
                 //const noteIsQuarterOrLonger: boolean = gNote.sourceNote.Length.CompareTo(new Fraction(1, 4)) >= 0; // trusting Fraction class, no float check
@@ -1061,37 +1074,57 @@ export class VexFlowMeasure extends GraphicalMeasure {
                     } else {
                         if (currentTuplet !== noteTuplet) { // new tuplet, finish old one
                             if (tupletNotesToAutoBeam.length > 1) {
-                                const vfBeam: VF.Beam = new VF.Beam(tupletNotesToAutoBeam, true);
+                                const beamVFNotes: StaveNote[] = [];
+                                for (const tupletNote of tupletNotesToAutoBeam) {
+                                    beamVFNotes.push(tupletNote.vfStaveNote);
+                                }
+                                const vfBeam: VF.Beam = new VF.Beam(beamVFNotes, true);
                                 if (this.rules.FlatBeams) {
                                     (<any>vfBeam).render_options.flat_beams = true;
                                     (<any>vfBeam).render_options.flat_beam_offset = this.rules.FlatBeamOffset;
                                     (<any>vfBeam).render_options.flat_beam_offset_per_beam = this.rules.FlatBeamOffsetPerBeam;
                                 }
                                 this.autoTupletVfBeams.push(vfBeam);
+
+                                const osmdBeam: Beam = new Beam(autoBeamId++);
+                                osmdBeam.AutoGenerated = true;
+                                for (const tupletNote of tupletNotesToAutoBeam) {
+                                    osmdBeam.addNoteToBeam(tupletNote.sourceNote);
+                                }
                             }
                             tupletNotesToAutoBeam = [];
                             currentTuplet = noteTuplet;
                         }
                     }
                     if (!tupletContainsUnbeamableNote) {
-                        tupletNotesToAutoBeam.push(vfStaveNote);
+                        tupletNotesToAutoBeam.push(linkedNote);
                     }
                     continue;
                 } else {
                     currentTuplet = undefined;
                 }
 
-                consecutiveBeamableNotes.push(vfStaveNote); // also happens on new beat
+                consecutiveBeamableNotes.push(linkedNote); // also happens on new beat
             }
         }
         if (tupletNotesToAutoBeam.length >= 2) {
-            const vfBeam: VF.Beam = new VF.Beam(tupletNotesToAutoBeam, true);
+            const beamVFNotes: StaveNote[] = [];
+            for (const tupletNote of tupletNotesToAutoBeam) {
+                beamVFNotes.push(tupletNote.vfStaveNote);
+            }
+            const vfBeam: VF.Beam = new VF.Beam(beamVFNotes, true);
             if (this.rules.FlatBeams) {
                 (<any>vfBeam).render_options.flat_beams = true;
                 (<any>vfBeam).render_options.flat_beam_offset = this.rules.FlatBeamOffset;
                 (<any>vfBeam).render_options.flat_beam_offset_per_beam = this.rules.FlatBeamOffsetPerBeam;
             }
             this.autoTupletVfBeams.push(vfBeam);
+
+            const osmdBeam: Beam = new Beam(autoBeamId++);
+            osmdBeam.AutoGenerated = true;
+            for (const tupletNote of tupletNotesToAutoBeam) {
+                osmdBeam.addNoteToBeam(tupletNote.sourceNote);
+            }
         }
         if (consecutiveBeamableNotes.length >= 2) {
             for (const note of consecutiveBeamableNotes) {
@@ -1116,7 +1149,11 @@ export class VexFlowMeasure extends GraphicalMeasure {
         }
 
         for (const notesForSeparateAutoBeam of separateAutoBeams) {
-            const newBeams: VF.Beam[] = VF.Beam.generateBeams(notesForSeparateAutoBeam, generateBeamOptions);
+            const beamVFNotes: StaveNote[] = [];
+            for (const linkedNote of notesForSeparateAutoBeam) {
+                beamVFNotes.push(linkedNote.vfStaveNote);
+            }
+            const newBeams: VF.Beam[] = VF.Beam.generateBeams(beamVFNotes, generateBeamOptions);
             for (const vfBeam of newBeams) {
                 if (this.rules.FlatBeams) {
                     (<any>vfBeam).render_options.flat_beams = true;
@@ -1151,9 +1188,11 @@ export class VexFlowMeasure extends GraphicalMeasure {
                     if (tupletStaveNotes.length > 1) {
                       const tuplet: Tuplet = tupletBuilder[0];
                       const notesOccupied: number = tuplet.Notes[0][0].NormalNotes;
-                      const bracketed: boolean = tuplet.Bracket ||
-                        (tuplet.TupletLabelNumber === 3 && this.rules.TripletsBracketed) ||
-                        (tuplet.TupletLabelNumber !== 3 && this.rules.TupletsBracketed);
+                      const bracketed: boolean = tuplet.shouldBeBracketed(
+                        this.rules.TupletsBracketedUseXMLValue,
+                        this.rules.TupletsBracketed,
+                        this.rules.TripletsBracketed
+                      );
                       let location: number = VF.Tuplet.LOCATION_TOP;
                       if (tuplet.tupletLabelNumberPlacement === PlacementEnum.Below) {
                           location = VF.Tuplet.LOCATION_BOTTOM;

+ 11 - 6
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -509,7 +509,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
 
     // for all staffEntries i, each containing the lyric entry for all verses at that timestamp in the measure
     for (const staffEntry of staffEntries) {
-      if (staffEntry.LyricsEntries.length > 0) {
+      if (staffEntry.LyricsEntries.length > 0 && this.rules.RenderLyrics) {
         newElongationFactorForMeasureWidth =
           this.calculateElongationFactor(
             staffEntry.LyricsEntries,
@@ -522,7 +522,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
             this.rules.LyricOverlapAllowedIntoNextMeasure,
           );
       }
-      if (staffEntry.graphicalChordContainers.length > 0) {
+      if (staffEntry.graphicalChordContainers.length > 0 && this.rules.RenderChordSymbols) {
         newElongationFactorForMeasureWidth =
           this.calculateElongationFactor(
             staffEntry.graphicalChordContainers,
@@ -740,11 +740,17 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
 
     // start position in staffline:
+    // const useStaffEntryBorderLeft: boolean = multiExpression.StartingContinuousDynamic?.DynamicType === ContDynamicEnum.diminuendo;
+    const continuousDynamic: ContinuousDynamicExpression = multiExpression.StartingContinuousDynamic;
+    const useStaffEntryBorderLeft: boolean = continuousDynamic !== undefined && !continuousDynamic.IsStartOfSoftAccent;
     const dynamicStartPosition: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
       absoluteTimestamp,
       staffIndex,
       staffLine,
-      staffLine?.isPartOfMultiStaffInstrument());
+      staffLine?.isPartOfMultiStaffInstrument(),
+      undefined,
+      useStaffEntryBorderLeft
+      );
     if (dynamicStartPosition.x <= 0) {
       dynamicStartPosition.x = startMeasure.beginInstructionsWidth + this.rules.RhythmRightMargin;
     }
@@ -758,10 +764,9 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
       this.calculateGraphicalInstantaneousDynamicExpression(graphicalInstantaneousDynamic, dynamicStartPosition, absoluteTimestamp);
       this.dynamicExpressionMap.set(absoluteTimestamp.RealValue, graphicalInstantaneousDynamic.PositionAndShape);
     }
-    if (multiExpression.StartingContinuousDynamic) {
-      const continuousDynamic: ContinuousDynamicExpression = multiExpression.StartingContinuousDynamic;
+    if (continuousDynamic) {
       const graphicalContinuousDynamic: VexFlowContinuousDynamicExpression = new VexFlowContinuousDynamicExpression(
-        multiExpression.StartingContinuousDynamic,
+        continuousDynamic,
         staffLine,
         startMeasure.parentSourceMeasure);
       graphicalContinuousDynamic.StartMeasure = startMeasure;

+ 2 - 1
src/MusicalScore/Graphical/VexFlow/VexFlowVoiceEntry.ts

@@ -28,7 +28,8 @@ export class VexFlowVoiceEntry extends GraphicalVoiceEntry {
         this.PositionAndShape.RelativePosition.y = boundingBox.y / unitInPixels;
         this.PositionAndShape.BorderTop = 0;
         this.PositionAndShape.BorderBottom = boundingBox.h / unitInPixels;
-        this.PositionAndShape.BorderLeft = -(modifierWidth + staveNote.width / 2) / unitInPixels; // Left of our X origin is the modifier
+        const halfStavenoteWidth: number = (staveNote.width - ((staveNote as any).paddingRight ?? 0)) / 2;
+        this.PositionAndShape.BorderLeft = -(modifierWidth + halfStavenoteWidth) / unitInPixels; // Left of our X origin is the modifier
         this.PositionAndShape.BorderRight = (boundingBox.w - modifierWidth) / unitInPixels; // Right of x origin is the note
     }
 

+ 19 - 15
src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts

@@ -3,7 +3,7 @@ import {Fraction} from "../../../Common/DataObjects/Fraction";
 import {MultiTempoExpression} from "../../VoiceData/Expressions/MultiTempoExpression";
 import {ContDynamicEnum, ContinuousDynamicExpression} from "../../VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression";
 import {ContinuousTempoExpression} from "../../VoiceData/Expressions/ContinuousExpressions/ContinuousTempoExpression";
-import {InstantaneousDynamicExpression} from "../../VoiceData/Expressions/InstantaneousDynamicExpression";
+import {DynamicEnum, InstantaneousDynamicExpression} from "../../VoiceData/Expressions/InstantaneousDynamicExpression";
 import {OctaveShift} from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
 import {Instrument} from "../../Instrument";
 import {MultiExpression} from "../../VoiceData/Expressions/MultiExpression";
@@ -515,20 +515,24 @@ export class ExpressionReader {
                 expressionText = dynamicsNode.elements()[0].value;
             }
             if (expressionText) {
-                // // ToDo: add doublettes recognition again as a afterReadingModule, as we can't check here if there is a repetition:
-                // // Make here a comparison with the active dynamic expression and only add it, if there is a change in dynamic
-                // // Exception is when there starts a repetition, where this might be different when repeating.
-                // // see PR #767 where this was removed
-                // let dynamicEnum: DynamicEnum;
-                // try {
-                //     dynamicEnum = DynamicEnum[expressionText];
-                // } catch (err) {
-                //     const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/DynamicError", "Error while reading dynamic.");
-                //     this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
-                //     return;
-                // }
-                // if (!this.activeInstantaneousDynamic ||
-                //     (this.activeInstantaneousDynamic && this.activeInstantaneousDynamic.DynEnum !== dynamicEnum)) {
+                // ToDo: make duplicate recognition an afterReadingModule, as we can't definitively check here if there is a repetition:
+                // Compare with the active dynamic expression and only add it if there is a change in dynamic
+                // Exception is when a repetition starts here, where the "repeated" dynamic might be desired.
+                // see PR #767 where this was removed
+                if (currentMeasure.Rules?.IgnoreRepeatedDynamics) {
+                    let dynamicEnum: DynamicEnum;
+                    try {
+                        dynamicEnum = DynamicEnum[expressionText];
+                    } catch (err) {
+                        const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/DynamicError", "Error while reading dynamic.");
+                        this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                        return;
+                    }
+                    if (this.activeInstantaneousDynamic?.DynEnum === dynamicEnum) {
+                        // repeated dynamic
+                        return;
+                    }
+                }
                 if (!fromNotation) {
                     this.createNewMultiExpressionIfNeeded(currentMeasure, numberXml);
                 } else {

+ 10 - 1
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -732,7 +732,8 @@ export class VoiceGenerator {
    * @returns {number}
    */
   private addTuplet(node: IXmlElement, tupletNodeList: IXmlElement[]): number {
-    let bracketed: boolean = false; // xml bracket attribute value
+    let bracketed: boolean = false; // true if bracket=yes given, otherwise false
+    let bracketedXmlValue: boolean = undefined; // Exact xml bracket value given: true for bracket=yes, false for bracket=no, undefined if not given.
     // TODO refactor this to not duplicate lots of code for the cases tupletNodeList.length == 1 and > 1
     if (tupletNodeList !== undefined && tupletNodeList.length > 1) {
       let timeModNode: IXmlElement = node.element("time-modification");
@@ -746,6 +747,9 @@ export class VoiceGenerator {
           const bracketAttr: Attr = tupletNode.attribute("bracket");
           if (bracketAttr && bracketAttr.value === "yes") {
             bracketed = true;
+            bracketedXmlValue = true;
+          } else if (bracketAttr && bracketAttr.value === "no") {
+            bracketedXmlValue = false;
           }
 
           const type: Attr = tupletNode.attribute("type");
@@ -767,6 +771,7 @@ export class VoiceGenerator {
 
             }
             const tuplet: Tuplet = new Tuplet(tupletLabelNumber, bracketed);
+            tuplet.BracketedXmlValue = bracketedXmlValue;
             //Default to above
             tuplet.tupletLabelNumberPlacement = PlacementEnum.Above;
             //If we ever encounter a placement attribute for this tuplet, should override.
@@ -858,6 +863,9 @@ export class VoiceGenerator {
         const bracketAttr: Attr = n.attribute("bracket");
         if (bracketAttr && bracketAttr.value === "yes") {
           bracketed = true;
+          bracketedXmlValue = true;
+        } else if (bracketAttr && bracketAttr.value === "no") {
+          bracketedXmlValue = false;
         }
         if (type === "start") {
           let tupletLabelNumber: number = 0;
@@ -883,6 +891,7 @@ export class VoiceGenerator {
           let tuplet: Tuplet = this.tupletDict[tupletnumber];
           if (!tuplet) {
             tuplet = this.tupletDict[tupletnumber] = new Tuplet(tupletLabelNumber, bracketed);
+            tuplet.BracketedXmlValue = bracketedXmlValue;
             //Default to above
             tuplet.tupletLabelNumberPlacement = PlacementEnum.Above;
           }

+ 1 - 0
src/MusicalScore/VoiceData/Beam.ts

@@ -9,6 +9,7 @@ export class Beam {
     private extendedNoteList: Note[] = [];
     public BeamNumber: number;
     public BeamNumberOffsetToXML: number = 0;
+    public AutoGenerated: boolean = false;
 
     constructor(beamNumber: number = 1, beamNumberOffsetToXML: number = 0) {
         this.BeamNumber = beamNumber;

+ 2 - 2
src/MusicalScore/VoiceData/SourceStaffEntry.ts

@@ -205,13 +205,13 @@ export class SourceStaffEntry {
         return duration;
     }
 
-    public calculateMaxNoteLength(): Fraction {
+    public calculateMaxNoteLength(untilEndOfTie: boolean = true): Fraction {
         let duration: Fraction = new Fraction(0, 1);
         for (let idx: number = 0, len: number = this.VoiceEntries.length; idx < len; ++idx) {
             const voiceEntry: VoiceEntry = this.VoiceEntries[idx];
             for (let idx2: number = 0, len2: number = voiceEntry.Notes.length; idx2 < len2; ++idx2) {
                 const note: Note = voiceEntry.Notes[idx2];
-                if (note.NoteTie) {
+                if (untilEndOfTie && note.NoteTie) {
                     // only add notes from this and after this sse!!
                     const tieRestDuration: Fraction = Fraction.createFromFraction(note.Length);
                     let addFollowingNotes: boolean = false;

+ 25 - 0
src/MusicalScore/VoiceData/Tuplet.ts

@@ -1,6 +1,7 @@
 import { Note } from "./Note";
 import { Fraction } from "../../Common/DataObjects/Fraction";
 import { PlacementEnum } from "./Expressions/AbstractExpression";
+import { Beam } from "./Beam";
 
 /**
  * Tuplets create irregular rhythms; e.g. triplets, quadruplets, quintuplets, etc.
@@ -21,6 +22,30 @@ export class Tuplet {
     private fractions: Fraction[] = [];
     /** Whether this tuplet has a bracket. (e.g. showing |--3--| or just 3 for a triplet) */
     private bracket: boolean;
+    /** Boolean if 'bracket="no"' or "yes" was explicitly requested in the XML, otherwise undefined. */
+    public BracketedXmlValue: boolean;
+
+    /** Determines whether the tuplet should be bracketed (arguments are EngravingRules). */
+    public shouldBeBracketed(useXmlValue: boolean, tupletsBracketed: boolean, tripletsBracketed: boolean): boolean {
+        if (useXmlValue && this.BracketedXmlValue !== undefined) {
+            return this.BracketedXmlValue;
+        }
+        // Gould: tuplets need bracket if they're not on one single beam (see #1400)
+        const startingBeam: Beam = this.Notes[0][0].NoteBeam;
+        // const startingVFBeam: VF.Beam = (tupletStaveNotes[0] as any).beam; // alternative way to check. see for loop
+        if (!startingBeam) {
+            return true;
+        } else {
+            for (const tupletNotes of this.Notes) {
+                if (tupletNotes[0].NoteBeam !== startingBeam) {
+                    return true;
+                }
+            }
+        }
+        return this.Bracket ||
+            (this.TupletLabelNumber === 3 && tripletsBracketed) ||
+            (this.TupletLabelNumber !== 3 && tupletsBracketed);
+    }
 
     public get TupletLabelNumber(): number {
         return this.tupletLabelNumber;

+ 1 - 0
src/VexFlowPatch/readme.txt

@@ -50,6 +50,7 @@ Fix stem/flag formatting. Instead of shifting notes by default, update the stem/
   (not yet in vexflow 4, PR 1263 open)
 able to add svg node id+class to stem (merged vexflow 4.x)
 Save and restore noteheads (e.g. slash noteheads) in reset()
+preFormat() and getBoundingBox(): add paddingRight variable to allow for custom right padding (e.g. for long lyrics below note)
 
 staverepetition.js (fixed vexflow 4):
 add TO_CODA enum to type() and draw()

+ 7 - 2
src/VexFlowPatch/src/stavenote.js

@@ -399,6 +399,9 @@ export class StaveNote extends StemmableNote {
     // for displaced ledger lines
     this.use_default_head_x = false;
 
+    // VexFlowPatch: add optional padding to the right (e.g. for large lyrics)
+    this.paddingRight = 0;
+
     // Drawing
     this.note_heads = [];
     this.modifiers = [];
@@ -602,7 +605,8 @@ export class StaveNote extends StemmableNote {
     }
 
     const { width: w, modLeftPx, extraLeftPx } = this.getMetrics();
-    const x = this.getAbsoluteX() - modLeftPx - extraLeftPx;
+    // VexFlowPatch: also subtract paddingRight (newly added in VexFlowPatch) to not shift note bbox
+    const x = this.getAbsoluteX() - modLeftPx - extraLeftPx - this.paddingRight;
 
     let minY = 0;
     let maxY = 0;
@@ -951,7 +955,8 @@ export class StaveNote extends StemmableNote {
     if (this.preFormatted) return;
     if (this.modifierContext) this.modifierContext.preFormat();
 
-    let width = this.getGlyphWidth() + this.extraLeftPx + this.extraRightPx;
+    // VexFlowPatch: add optional padding to the right (e.g. for large lyrics), default 0.
+    let width = this.getGlyphWidth() + this.extraLeftPx + this.extraRightPx + this.paddingRight;
 
     // For upward flagged notes, the width of the flag needs to be added
     if (this.renderFlag && this.glyph.flag && this.beam === null && this.stem_direction === Stem.UP) {

+ 6 - 0
test/Util/generateImages_browserless.mjs

@@ -320,6 +320,7 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
         const isTestEndClefStaffEntryBboxes = sampleFilename.startsWith("test_end_measure_clefs_staffentry_bbox");
         const isTestPageBreakImpliesSystemBreak = sampleFilename.startsWith("test_pagebreak_implies_systembreak");
         const isTestPageBottomMargin0 = sampleFilename.includes("PageBottomMargin0");
+        const isTestTupletBracketTupletNumber = sampleFilename.includes("test_tuplet_bracket_tuplet_number");
         const enableNewSystemAtSystemBreak = sampleFilename.includes("test_octaveshift_extragraphicalmeasure");
         osmdInstance.EngravingRules.loadDefaultValues(); // note this may also be executed in setOptions below via drawingParameters default
         if (isTestEndClefStaffEntryBboxes) {
@@ -361,6 +362,11 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
         if (isTestPageBottomMargin0) {
             osmdInstance.EngravingRules.PageBottomMargin = 0;
         }
+        if (isTestTupletBracketTupletNumber) {
+            osmdInstance.EngravingRules.TupletNumberLimitConsecutiveRepetitions = true;
+            osmdInstance.EngravingRules.TupletNumberMaxConsecutiveRepetitions = 2;
+            osmdInstance.EngravingRules.TupletNumberAlwaysDisableAfterFirstMax = true; // necessary to trigger bug
+        }
         if (enableNewSystemAtSystemBreak) {
             osmdInstance.EngravingRules.NewSystemAtXMLNewSystemAttribute = true;
         }

+ 14 - 1
test/Util/visual_regression.sh

@@ -43,7 +43,8 @@
 # Show images over this PHASH threshold.
 # 0.01 is probably too low, but a good first pass.
 # 0.0001 catches for example a repetition ending not having a down line at the end (see Saltarello bar 10) (0.001 doesn't catch this)
-THRESHOLD=0.0001
+# 0.0000001 (6 0s after the dot) catches e.g. a chord symbol moving about 3 pixels to the right (on a canvas of ~1450px width)
+THRESHOLD=0.00000001
 
 # Set up Directories
 #   It does not matter where this script is executed, as long as these folders are given correctly (and blessed/current have png images set up correctly)
@@ -154,6 +155,18 @@ function diff_image() {
 
   # Calculate the difference metric and store the composite diff image.
   local hash=`compare -metric PHASH -highlight-color '#ff000050' $diff-b.png $diff-a.png $diff-diff.png 2>&1`
+  # convert hash to decimal if it was in scientific notation (e.g. 1.5e-2 -> 0.015)
+  #   otherwise, syntax error will be returned for $hash > $THRESHOLD" | bc -l
+  if [ ! $hash == 0 ] # don't change a "0" string
+  then
+    export LC_NUMERIC="en_US.UTF-8" # use dot instead of comma for decimals (1.5 instead of 1,5)
+    hash=$(printf "%.14f" $hash) # precision seems limited to 15 digits in shell/awk(?)
+    hash=$(echo $hash | bc -l | grep -o '.*[1-9]') # remove trailing 0s
+    if (( $(echo "$hash < 1" |bc -l) ))
+    then
+      hash="0$hash" # add leading 0 (e.g. .01 -> 0.01), just for readability/display
+    fi
+  fi
 
   local isGT=`echo "$hash > $THRESHOLD" | bc -l`
   if [ "$isGT" == "1" ]

+ 1 - 1
test/data/note_height_bottomline_test_sample.musicxml

@@ -2,7 +2,7 @@
 <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
 <score-partwise version="3.1">
   <work>
-    <work-title>Title</work-title>
+    <work-title>note_height_bottomline_test_sample</work-title>
     </work>
   <identification>
     <creator type="composer">Composer</creator>

+ 1 - 1
test/data/note_height_skyline_test_sample_with_chord_symbol_etc.musicxml

@@ -2,7 +2,7 @@
 <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
 <score-partwise version="3.1">
   <work>
-    <work-title>Title</work-title>
+    <work-title>note_height_skyline_test_sample_with_chord_symbol_etc</work-title>
     </work>
   <identification>
     <creator type="composer">Composer</creator>

+ 302 - 0
test/data/test_chord_symbol_centering_short_symbols.musicxml

@@ -0,0 +1,302 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<!DOCTYPE score-partwise PUBLIC '-//Recordare//DTD MusicXML 4.0 Partwise//EN' 'http://www.musicxml.org/dtds/partwise.dtd'>
+<score-partwise version='4.0'>
+	<movement-title>test_chord_symbol_centering_short_symbols</movement-title>
+	<identification>
+		<encoding>
+			<software>Sibelius 2022.12</software>
+			<software>Dolet 8.2 for Sibelius</software>
+			<encoding-date>2023-02-27</encoding-date>
+			<supports element='accidental' type='yes'/>
+			<supports element='transpose' type='yes'/>
+			<supports attribute='new-page' element='print' type='yes' value='yes'/>
+			<supports attribute='new-system' element='print' type='yes' value='yes'/>
+		</encoding>
+	</identification>
+	<defaults>
+		<scaling>
+			<millimeters>6.5</millimeters>
+			<tenths>40</tenths>
+		</scaling>
+		<page-layout>
+			<page-height>1828</page-height>
+			<page-width>1292</page-width>
+			<page-margins type='both'>
+				<left-margin>78.1538</left-margin>
+				<right-margin>78.1538</right-margin>
+				<top-margin>78.1538</top-margin>
+				<bottom-margin>78.1538</bottom-margin>
+			</page-margins>
+		</page-layout>
+		<system-layout>
+			<system-margins>
+				<left-margin>0</left-margin>
+				<right-margin>0</right-margin>
+			</system-margins>
+			<system-distance>100</system-distance>
+			<top-system-distance>153.75</top-system-distance>
+		</system-layout>
+		<staff-layout>
+			<staff-distance>55</staff-distance>
+		</staff-layout>
+		<?DoletSibelius StaffJustificationPercentage=40?>
+		<appearance>
+			<line-width type='beam'>5</line-width>
+			<line-width type='heavy barline'>5</line-width>
+			<line-width type='leger'>1.5625</line-width>
+			<line-width type='light barline'>1.5625</line-width>
+			<line-width type='slur middle'>2.1875</line-width>
+			<line-width type='slur tip'>0.625</line-width>
+			<line-width type='staff'>1.25</line-width>
+			<line-width type='stem'>1.25</line-width>
+			<line-width type='tie middle'>2.1875</line-width>
+			<line-width type='tie tip'>0.625</line-width>
+			<note-size type='grace'>60</note-size>
+			<note-size type='cue'>75</note-size>
+		</appearance>
+		<music-font font-family='Helsinki Std,engraved'/>
+		<word-font font-family='Palatino,serif'/>
+	</defaults>
+	<part-list>
+		<score-part id='P1'>
+			<part-name print-object='no'>Piano</part-name>
+			<part-abbreviation print-object='no'>Pno.</part-abbreviation>
+			<group>score</group>
+			<score-instrument id='P1-I1'>
+				<instrument-name>Piano</instrument-name>
+				<instrument-abbreviation>Pno.</instrument-abbreviation>
+				<instrument-sound>keyboard.piano.grand</instrument-sound>
+			</score-instrument>
+			<midi-instrument id='P1-I1'>
+				<volume>79</volume>
+				<pan>-18</pan>
+			</midi-instrument>
+		</score-part>
+	</part-list>
+<!--=========================================================-->
+	<part id='P1'>
+		<measure number='1'>
+			<print>
+				<system-layout>
+					<top-system-distance>246.25</top-system-distance>
+				</system-layout>
+				<staff-layout>
+					<?DoletSibelius JustifyAllStaves=false?>
+				</staff-layout>
+			</print>
+			<attributes>
+				<divisions>768</divisions>
+				<key>
+					<fifths>0</fifths>
+					<mode>minor</mode>
+				</key>
+				<time>
+					<beats>4</beats>
+					<beat-type>4</beat-type>
+				</time>
+				<staves>2</staves>
+				<clef number='1'>
+					<sign>G</sign>
+					<line>2</line>
+				</clef>
+				<clef number='2'>
+					<sign>F</sign>
+					<line>4</line>
+				</clef>
+			</attributes>
+			<harmony print-frame='no' default-y='25'>
+				<root>
+					<root-step>C</root-step>
+				</root>
+				<kind text=''>major</kind>
+				<staff>1</staff>
+			</harmony>
+			<note>
+				<pitch>
+					<step>C</step>
+					<octave>4</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<stem>up</stem>
+				<staff>1</staff>
+			</note>
+			<harmony print-frame='no' default-y='25'>
+				<root>
+					<root-step>F</root-step>
+				</root>
+				<kind text=''>major</kind>
+				<staff>1</staff>
+			</harmony>
+			<note>
+				<pitch>
+					<step>F</step>
+					<octave>4</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<stem>up</stem>
+				<staff>1</staff>
+			</note>
+			<backup>
+				<duration>3072</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>C</step>
+					<octave>3</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<stem>up</stem>
+				<staff>2</staff>
+			</note>
+			<note>
+				<pitch>
+					<step>F</step>
+					<octave>3</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<stem>down</stem>
+				<staff>2</staff>
+			</note>
+		</measure>
+<!--=========================================================-->
+		<measure number='2'>
+			<harmony print-frame='no' default-y='25'>
+				<root>
+					<root-step>G</root-step>
+				</root>
+				<kind text=''>major</kind>
+				<staff>1</staff>
+			</harmony>
+			<note>
+				<pitch>
+					<step>G</step>
+					<octave>4</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<stem>up</stem>
+				<staff>1</staff>
+			</note>
+			<harmony print-frame='no' default-y='25'>
+				<root>
+					<root-step>F</root-step>
+				</root>
+				<kind text=''>major</kind>
+				<staff>1</staff>
+			</harmony>
+			<note>
+				<pitch>
+					<step>F</step>
+					<octave>4</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<stem>up</stem>
+				<staff>1</staff>
+			</note>
+			<backup>
+				<duration>3072</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>G</step>
+					<octave>3</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<stem>down</stem>
+				<staff>2</staff>
+			</note>
+			<note>
+				<pitch>
+					<step>F</step>
+					<octave>3</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<stem>down</stem>
+				<staff>2</staff>
+			</note>
+		</measure>
+<!--=========================================================-->
+		<measure number='3'>
+			<harmony print-frame='no' default-y='25'>
+				<root>
+					<root-step>C</root-step>
+				</root>
+				<kind text=''>major</kind>
+				<staff>1</staff>
+			</harmony>
+			<note>
+				<pitch>
+					<step>C</step>
+					<octave>4</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<stem>up</stem>
+				<staff>1</staff>
+			</note>
+			<harmony print-frame='no' default-y='25'>
+				<root>
+					<root-step>F</root-step>
+				</root>
+				<kind text=''>major</kind>
+				<staff>1</staff>
+			</harmony>
+			<note>
+				<pitch>
+					<step>F</step>
+					<octave>4</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<stem>up</stem>
+				<staff>1</staff>
+			</note>
+			<backup>
+				<duration>3072</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>C</step>
+					<octave>3</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<stem>up</stem>
+				<staff>2</staff>
+			</note>
+			<note>
+				<pitch>
+					<step>F</step>
+					<octave>3</octave>
+				</pitch>
+				<duration>1536</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<stem>down</stem>
+				<staff>2</staff>
+			</note>
+			<barline location='right'>
+				<bar-style>light-heavy</bar-style>
+			</barline>
+		</measure>
+	</part>
+<!--=========================================================-->
+</score-partwise>

+ 1 - 1
test/data/test_chord_symbols_collision_high_notes_alignment.musicxml

@@ -2,7 +2,7 @@
 <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
 <score-partwise version="3.1">
   <work>
-    <work-title>Title</work-title>
+    <work-title>test_chord_symbols_collision_high_notes_alignment</work-title>
     </work>
   <identification>
     <creator type="composer">Composer</creator>

二進制
test/data/test_clef_measure_end_backup_nodes_Sibelius.musicxml


+ 193 - 0
test/data/test_decrescendo_crescendo_stop_start.musicxml

@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test_decrescendo_crescendo_stop_start</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2023-06-17</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>6.5</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1828</page-height>
+      <page-width>1292</page-width>
+      <page-margins type="both">
+        <left-margin>78.1538</left-margin>
+        <right-margin>78.1538</right-margin>
+        <top-margin>78.1538</top-margin>
+        <bottom-margin>78.1538</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Palatino,serif" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="646" default-y="1749.85" justify="center" valign="top" font-family="Edwin" font-size="22">test_decrescendo_crescendo_stop_start</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Voice</part-name>
+      <part-abbreviation>Vo.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Voice</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>53</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="295.48">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>558.86</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="diminuendo" number="1" default-y="20.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="89.48" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="start"/>
+        <voice>1</voice>
+        <type>half</type>
+        <accidental>sharp</accidental>
+        <stem>up</stem>
+        <notations>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="20.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="191.58" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="stop"/>
+        <tie type="start"/>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>up</stem>
+        <notations>
+          <tied type="stop"/>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="2" width="231.35">
+      <direction placement="above">
+        <direction-type>
+          <wedge type="diminuendo" number="1" default-y="20.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="16.50" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="stop"/>
+        <tie type="start"/>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>up</stem>
+        <notations>
+          <tied type="stop"/>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="20.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="118.60" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="stop"/>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>up</stem>
+        <notations>
+          <tied type="stop"/>
+          </notations>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 165 - 0
test/data/test_lyrics_spacing_short_notes_four_characters.musicxml

@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test_lyrics_spacing_short_notes_four_characters</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2023-06-12</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>6.99911</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1596.77</page-height>
+      <page-width>1233.87</page-width>
+      <page-margins type="even">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="616.935" default-y="1511.05" justify="center" valign="top" font-size="22">test_lyrics_spacing_short_notes_four_characters</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="978.70">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>4</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <note default-x="80.72" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <lyric number="1" default-x="6.50" default-y="-42.97" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>longNote</text>
+          </lyric>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        </note>
+      <note default-x="654.88" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        <lyric number="1" default-x="6.50" default-y="-42.97" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>four</text>
+          </lyric>
+        </note>
+      <note default-x="794.07" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">continue</beam>
+        <beam number="2">begin</beam>
+        <lyric number="1" default-x="6.50" default-y="-42.97" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>chars</text>
+          </lyric>
+        </note>
+      <note default-x="881.06" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        <lyric number="1" default-x="6.50" default-y="-42.97" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>spcng</text>
+          </lyric>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 1 - 1
test/data/test_slide_glissando.musicxml

@@ -1,4 +1,4 @@
-<?xml version='1.0' encoding='UTF-16' standalone='no'?>
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
 <!DOCTYPE score-partwise PUBLIC '-//Recordare//DTD MusicXML 4.0 Partwise//EN' 'http://www.musicxml.org/dtds/partwise.dtd'>
 <score-partwise version='4.0'>
 	<movement-title>test_slide_glissando</movement-title>

+ 1 - 1
test/data/test_soft-accent_cresc_decresc_single_note.musicxml

@@ -2,7 +2,7 @@
 <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
 <score-partwise version="3.1">
   <work>
-    <work-title>Title</work-title>
+    <work-title>test_soft-accent_cresc_decresc_single_note</work-title>
     </work>
   <identification>
     <creator type="composer">Composer</creator>

+ 225 - 0
test/data/test_tuplet_bracket_necessary.musicxml

@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test_tuplet_bracket_necessary</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2023-06-14</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>6.99911</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1596.77</page-height>
+      <page-width>1233.87</page-width>
+      <page-margins type="even">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="616.935" default-y="1511.05" justify="center" valign="top" font-size="22">test_tuplet_bracket_necessary</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="546.64">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>432.07</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>3</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>3</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <notations>
+          <tuplet type="start"/>
+          </notations>
+        </note>
+      <note default-x="133.66" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="186.60" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="239.54" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start"/>
+          </notations>
+        </note>
+      <note default-x="292.47" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="345.41" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="398.35" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          <normal-type>eighth</normal-type>
+          </time-modification>
+        <stem>up</stem>
+        <notations>
+          <tuplet type="start"/>
+          </notations>
+        </note>
+      <note default-x="483.05" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          <normal-type>eighth</normal-type>
+          </time-modification>
+        <stem>down</stem>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 292 - 0
test/data/test_tuplet_bracket_tuplet_number.musicxml

@@ -0,0 +1,292 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test_tuplet_bracket_tuplet_number</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2023-06-15</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1697.14</page-height>
+      <page-width>1200</page-width>
+      <page-margins type="even">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="600" default-y="1611.43" justify="center" valign="top" font-size="22">test_tuplet_bracket_tuplet_number</credit-words>
+    </credit>
+  <part-list>
+    <part-group type="start" number="1">
+      <group-symbol>brace</group-symbol>
+      </part-group>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="978.57">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>3</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <note default-x="84.22" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="157.86" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="231.51" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="305.15" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="378.79" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="452.43" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="526.07" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="599.71" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="673.35" default-y="-5.00">
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="747.00" default-y="0.00">
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <notations>
+          <tuplet type="start" bracket="yes"/>
+          </notations>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        </note>
+      <note default-x="894.28" default-y="5.00">
+        <pitch>
+          <step>G</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 1 - 1
test/data/test_wedge_diminuendo_duplicated.musicxml

@@ -2,7 +2,7 @@
 <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
 <score-partwise version="3.1">
   <work>
-    <work-title>Title</work-title>
+    <work-title>test_wedge_diminuendo_duplicated</work-title>
     </work>
   <identification>
     <creator type="composer">Composer</creator>