Skip to content

feat: tuplet numbering and portion fidelity - #463

Merged
webern merged 3 commits into
mainfrom
m/mxdev-tupnest
Sep 21, 2026
Merged

webern merged 3 commits into
mainfrom
m/mxdev-tupnest

Conversation

@webern

@webern webern commented Sep 20, 2026

Copy link
Copy Markdown
Owner

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. TupletStart and TupletStop carry a SpannerNumber instead of a raw int numberLevel, and tuplets join the other spanner families in SpannerResolver. Tuplets were the
last 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. The
reader 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> the
source 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::writeActual and writeNormal are fidelity fields in the
ClefData::writeStaffNumber style: unspecified applies the rule, yes/no force it either way, so
a 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 (two Bools, modeled like show-number), line-shape
(TupletLineShape), and bracket on a tuplet stop.

Breaking for callers that set numberLevel: number = SpannerNumber{level} to write it and
number.level() to read it back. Callers that relied on both portions always being written need
writeActual/writeNormal set to yes.

Testing

  • make api-test (6078 assertions in 698 test cases), including new Tuplet and
    TupletNumber suites covering the reader, the writer's rule, the fidelity fields, the
    three attributes, identity and explicit numbering, and both diagnostics
  • make core-roundtrip-test with the pinned count 840 -> 841
  • make api-roundtrip 435 of 435 pinned, up from 415
  • make fmt-check

Twenty files join roundtrip-baseline.txt: seventeen wild
(foundsuite/Invention_5, ksuite/k011a_Tuplets, lysuite/ly23a, ly23b, ly23c, ly23d,
mjbsuite/PreserveTimeModificationNormalType, the nine musuite tuplet and duration-rounding
files, rpatters1/tuplet_singletons-ref) and three synthetic, one of which is the new
data/synthetic/tuplet-nested.3.0.xml fixture. ly23d_Tuplets_Nested.xml is the corpus's one
genuinely nested tuplet.

References

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.
@webern webern added feature new feature request api Affects the mx::api layer impl Affects the mx::impl layer testing breaking fixes or implementation that require breaking changes ai Issues opened by, or through, a coding agent. labels Sep 20, 2026
Comment thread src/include/mx/api/TupletData.h Outdated
// a triplet or the 4 in 5:4 for quintuplet
Bool showNormalNumber;

// show the note value of the tuplet's own notes,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/include/mx/api/TupletData.h Outdated
// for example the eighth note in "3 eighths"
Bool showActualType;

// show the note value of the 'normal' notes,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/include/mx/api/TupletData.h Outdated
// 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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..."

@webern
webern marked this pull request as ready for review September 21, 2026 06:56
@webern
webern merged commit 0d3dba2 into main Sep 21, 2026
8 checks passed
@webern
webern deleted the m/mxdev-tupnest branch September 21, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Issues opened by, or through, a coding agent. api Affects the mx::api layer breaking fixes or implementation that require breaking changes feature new feature request impl Affects the mx::impl layer testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant