feat: tuplet numbering and portion fidelity - #463
Conversation
TupletStart and TupletStop now carry a SpannerNumber instead of a raw number level, and tuplets join the other spanner families in SpannerResolver, so an author who labels both ends of a tuplet gets the numbers assigned for them. Nested tuplets overlap in the serialized stream and are handed 1 and 2; tuplets that follow one another both get 1. Two tuplets open at once with no number are reported. A tuplet may spell its ratio out in tuplet-actual and tuplet-normal or leave both out and let the note's time-modification speak for it. The reader records which shape the file used, and the writer states a portion only when it says something the note does not already say, which is what the inner tuplet of a nested pair needs. writeActual and writeNormal force the answer either way. Inside a portion, only the parts that were stated are written; an unspecified note value no longer turns into a maxima. Adds show-type, line-shape, and bracket on a tuplet stop.
Adds a hand-built nested tuplet fixture: a triplet of eighths inside a triplet of quarters, so the inner notes carry the product 9:4 and the inner tuplet is the one that has to state its own ratio. Bumps the pinned corpus count and adds the seventeen wild files and three synthetic ones that the tuplet work unlocks to the api round-trip pass-list.
| // a triplet or the 4 in 5:4 for quintuplet | ||
| Bool showNormalNumber; | ||
|
|
||
| // show the note value of the tuplet's own notes, |
There was a problem hiding this comment.
I can't tell from this how I'm supposed to use it or when I can leave it unspecified. Please use /mx-comments and write this for a human user that does not have your session context. Explain what it is, what it does, when it's required and when it's optional.
| // for example the eighth note in "3 eighths" | ||
| Bool showActualType; | ||
|
|
||
| // show the note value of the 'normal' notes, |
There was a problem hiding this comment.
I can't tell from this how I'm supposed to use it or when I can leave it unspecified. Please use /mx-comments and write this for a human user that does not have your session context. Explain what it is, what it does, when it's required and when it's optional.
| // Most users can ignore these; leave them unspecified. They control whether the ratio is | ||
| // spelled out in the file alongside the note's own time modification. unspecified (the | ||
| // default) applies the right rule automatically: state the ratio when it differs from what | ||
| // the note already says, which is what an inner tuplet needs, and leave it out otherwise. |
There was a problem hiding this comment.
The introduction of "inner tuplet" without more context is bad. "Inner tuplet" is not a common use case so you have to explain "In the case of nested tuplets..."
Human Summary
Adds support for nested tuplets and for controlling more about how a tuplet is spelled in the MusicXML. I'm not entirely sure if the LLM understand how nested tuplets work because I don't know well enough to check the synthetic file it created; but the unblocked wild corpus files more or less prove that users can do what they need to do now.
Summary
Two related pieces of tuplet work, plus three attributes that had no home in
mx::api.Numbering.
TupletStartandTupletStopcarry aSpannerNumberinstead of a rawint numberLevel, and tuplets join the other spanner families inSpannerResolver. Tuplets were thelast spanner family whose numbers the caller had to allocate by hand, which is exactly what
nested tuplets need. An author who gives both ends of a tuplet the same identity now gets the
numbers assigned for them: nested tuplets overlap in the serialized stream and are handed 1 and
2, while tuplets that follow one another both get 1. Two unnumbered tuplets open at once are
reported through
Diagnostics, since a reader defaults both to 1 and cannot tell them apart.Portion fidelity. A
<tuplet>may spell its ratio out in<tuplet-actual>and<tuplet-normal>or leave both out and let the note's<time-modification>speak for it. Thereader accepted both shapes and threw the difference away; the writer then put both portions on
every start, turning an unspecified note value into a
<tuplet-type>maxima</tuplet-type>thesource never had. That was the sole remaining round-trip blocker on seventeen wild corpus files.
The reader now records which shape the file used, and the writer states a portion only where it
says something the note does not already say. That rule is what nested tuplets need: an inner
triplet of eighths inside a triplet of quarters gives its notes a cumulative 9:4, so the inner
tuplet's own 3:2 differs from the note and is written, while the outer tuplet repeats its note's
3:2 and is left out.
TupletStart::writeActualandwriteNormalare fidelity fields in theClefData::writeStaffNumberstyle: unspecified applies the rule, yes/no force it either way, soa caller who finds the portions noisy can turn them off. Inside a portion, only the parts that
were stated are written.
New attributes.
show-type(twoBools, modeled likeshow-number),line-shape(
TupletLineShape), andbracketon a tuplet stop.Breaking for callers that set
numberLevel:number = SpannerNumber{level}to write it andnumber.level()to read it back. Callers that relied on both portions always being written needwriteActual/writeNormalset toyes.Testing
make api-test(6078 assertions in 698 test cases), including newTupletandTupletNumbersuites covering the reader, the writer's rule, the fidelity fields, thethree attributes, identity and explicit numbering, and both diagnostics
make core-roundtrip-testwith the pinned count 840 -> 841make api-roundtrip435 of 435 pinned, up from 415make fmt-checkTwenty files join
roundtrip-baseline.txt: seventeen wild(
foundsuite/Invention_5,ksuite/k011a_Tuplets,lysuite/ly23a,ly23b,ly23c,ly23d,mjbsuite/PreserveTimeModificationNormalType, the ninemusuitetuplet and duration-roundingfiles,
rpatters1/tuplet_singletons-ref) and three synthetic, one of which is the newdata/synthetic/tuplet-nested.3.0.xmlfixture.ly23d_Tuplets_Nested.xmlis the corpus's onegenuinely nested tuplet.
References
numbers that made nested tuplets awkward to write.
<normal-type>is the author's statement, and the same reasoning now applies tothe tuplet portions.
paired by
SpannerNumberrather than by number level.