You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
NotationsWriter emits all spanner stops before all starts, breaking single-note spanners #429
[NOTE: This is a more serious issue than just tuplets on single notes. See the glissando examples below.]
NotationsWriter::writeNotations writes every entry of NoteData::noteAttachmentData::tupletStops before every entry of tupletStarts:
for (constauto &tupletStop : myNoteData.noteAttachmentData.tupletStops) { ... } // all stopsfor (constauto &tupletStart : myNoteData.noteAttachmentData.tupletStarts) { ... } // then all starts
That convention is right when a note closes one tuplet and opens another. It is wrong for a tuplet contained in a single note, where both ends belong to the same note and MusicXML wants start and then stop.
Symptom. A Finale document with a nested tuplet whose inner tuplet covers exactly one note (the last note of the outer tuplet) produces, on that note:
Number 2 closes before it opens. A reader encounters a stop for a tuplet that was never open, then a start that is never closed. The correct sequence is:
The numbers themselves are correct and pair correctly; only the order is wrong.
Why the author cannot work around it.tupletStarts and tupletStops are separate vectors, so there is no way to express "this start precedes that stop on the same note". Any ordering the author chooses within each vector is discarded by the two-loop structure.
Suggested shape
Order a note's tuplet notations so that a start whose matching stop is on the same note is emitted before that stop. Since numberLevel already pairs the two ends, that can be decided locally: when a TupletStart and a TupletStop on one note share a numberLevel, write the start first, and otherwise keep the existing stops-before-starts order for the tuplets that merely pass through.
Note
This is the ordering half only. <normal-type> for the same nested-tuplet territory is #428, and the two are independent. Tuplets also still carry a raw numberLevel int rather than an api::SpannerNumber, so they get none of the writer-side number assignment from #320; that is a third, separate matter and is not the cause of this bug.
Here is another automatically submitted by Claude. I think I have got it to where it will prompt me before submitting any more. I thought you might find some sample fixtures helpful for this case.
Here is an example of a single bar containing two 1-note tuplets (both 3 in the time of two quarters represented as a dotted half and dotted half rest.)
changed the title [-]NotationsWriter emits all tuplet stops before all starts, breaking a single-note tuplet[/-][+]NotationsWriter emits all spanner stops before all starts, breaking single-note spanners[/+]on Aug 25, 2026
The stop-before-start inversion is not specific to tuplets, and the case that motivates it is ordinary notation rather than an edge case.
NotationsWriter applies the same convention to every note-attached notation family, so any span contained in a single note comes out inverted. Glissandi and slides reach that state routinely. Finale offers only beat-attachment or notehead-attachment for custom lines, with no general entry attachment, so a glissando or slide drawn to a rest cannot attach to the rest: both ends anchor to the notehead and the line simply extends toward the rest. Every gliss-to-rest in a Finale document therefore becomes a span whose start and stop are on one note.
Attached is a fixture with one glissando and one tab slide drawn that way, plus Finale's own export of it for comparison. mx currently writes:
Finale writes start then stop for both. As with the tuplet case the number pairing is correct and only the order is wrong, but number 1 closes before it opens, so a reader sees a stop for a span that was never open followed by a start that never closes.
The loops involved:
tupletStops before tupletStarts
glissandoStops before glissandoStarts (in addGlissandoAndSlide)
wavyLineStops and continues before wavyLineStarts
Glissando and slide share one loop pair, with the element chosen inside by GlissandoType, so the two cases in the fixture exercise the same ordering code. Wavy lines are structurally identical but I have no single-note case for them: Finale gives a trill without a drag no extension line at all, so producing one would mean hand-authoring the MusicXML.
So the fix is probably one rule rather than three: when a start and a stop on one note belong to the same span, emit the start first, and otherwise keep the existing stops-before-starts order for spans that merely pass through. That ordering matters only within a family, so it can be decided locally from the pairing key each family already has: numberLevel for tuplets, SpannerNumber for the others.
One caveat if you plan to cover this in the round-trip corpus. The only signature this produces is reorder:notations, and per docs/ai/design/api-roundtrip-classifier.md a FAIL whose signatures are all reorder: is treated as expected mx::api behavior rather than a bug candidate. A corpus entry may therefore be absorbed by normalization without the inversion being noticed, so a targeted test is the surer route.
Here is an example with the spanners inverted (serialized by MX):
Activity
rpatters1 commentedon Aug 24, 2026
Here is another automatically submitted by Claude. I think I have got it to where it will prompt me before submitting any more. I thought you might find some sample fixtures helpful for this case.
Here is an example of a single bar containing two 1-note tuplets (both 3 in the time of two quarters represented as a dotted half and dotted half rest.)
Finale's (correct) export:
tuplet_singletons-ref.xml
MX's export with the stop/start out of sequence
tuplet_singletons.xml
Match Finale's glissando and wavy-line classification
[-]NotationsWriter emits all tuplet stops before all starts, breaking a single-note tuplet[/-][+]NotationsWriter emits all spanner stops before all starts, breaking single-note spanners[/+]rpatters1 commentedon Aug 25, 2026
The stop-before-start inversion is not specific to tuplets, and the case that motivates it is ordinary notation rather than an edge case.
NotationsWriterapplies the same convention to every note-attached notation family, so any span contained in a single note comes out inverted. Glissandi and slides reach that state routinely. Finale offers only beat-attachment or notehead-attachment for custom lines, with no general entry attachment, so a glissando or slide drawn to a rest cannot attach to the rest: both ends anchor to the notehead and the line simply extends toward the rest. Every gliss-to-rest in a Finale document therefore becomes a span whose start and stop are on one note.Attached is a fixture with one glissando and one tab slide drawn that way, plus Finale's own export of it for comparison. mx currently writes:
Finale writes start then stop for both. As with the tuplet case the number pairing is correct and only the order is wrong, but number 1 closes before it opens, so a reader sees a stop for a span that was never open followed by a start that never closes.
The loops involved:
tupletStopsbeforetupletStartsglissandoStopsbeforeglissandoStarts(inaddGlissandoAndSlide)wavyLineStopsand continues beforewavyLineStartsGlissando and slide share one loop pair, with the element chosen inside by
GlissandoType, so the two cases in the fixture exercise the same ordering code. Wavy lines are structurally identical but I have no single-note case for them: Finale gives a trill without a drag no extension line at all, so producing one would mean hand-authoring the MusicXML.So the fix is probably one rule rather than three: when a start and a stop on one note belong to the same span, emit the start first, and otherwise keep the existing stops-before-starts order for spans that merely pass through. That ordering matters only within a family, so it can be decided locally from the pairing key each family already has:
numberLevelfor tuplets,SpannerNumberfor the others.One caveat if you plan to cover this in the round-trip corpus. The only signature this produces is
reorder:notations, and perdocs/ai/design/api-roundtrip-classifier.mda FAIL whose signatures are allreorder:is treated as expectedmx::apibehavior rather than a bug candidate. A corpus entry may therefore be absorbed by normalization without the inversion being noticed, so a targeted test is the surer route.Here is an example with the spanners inverted (serialized by MX):
gliss_to_rest.xml
Here is an example of the same file serialized by Finale:
gliss_to_rest-ref.xml
This is what it looks like in Finale:
Add the gliss-to-rest fixture and retarget the ordering test
Export note-attached spanners, and fix lyric line identity (#122)
webern commentedon Aug 26, 2026
I guess I didn't consider spanners that could start and stop on the same note at the time.
fix: write single-note spanner starts before their stops
test: pin gliss_to_rest-ref.xml in the api-roundtrip baseline
webern commentedon Aug 28, 2026
Fixed but leaving it open to see if more cases arise.
Advance the MX and musx pins, and fix nested-tuplet normal-type (#125)