feat: ending numbers and display text in mx::api - #388
Open
rpatters1 wants to merge 1 commit into
Open
Conversation
## Summary
MusicXML `<ending>` carries a list-valued `number` attribute -- the passes the ending is
played on -- and an element text body holding the printed label, which the spec uses when
that label differs from the numbers. An ending played on passes 1, 2, and 3 is commonly
labeled `1-3`. `mx::core::Ending` models both, as an `EndingNumber` wrapping a
`std::vector<int>` and a string value, but `mx::api` dropped all of it: `BarlineData`
carried `EndingType endingType` and a single `int endingNumber`, so
`MeasureReader::parseBarline` kept `values().front()` and discarded the rest, and neither
direction ever touched `Ending::value()`.
`BarlineData::endingType` and `BarlineData::endingNumber` are replaced by one
`std::optional<EndingData> ending`:
- `EndingData::numbers` is the full list of passes. Empty means a blank ending, MusicXML's
way of saying a bracket is present but which passes it covers is unknown -- a state the
old `int` could not express, since 0 meant unspecified.
- `EndingData::text` is the printed label. Left empty, the writer emits no text node and
the ending displays its numbers, so authoring without a label produces the same XML as
before.
`EndingType::none` is removed. Presence is now the optional, so the enum's three values
match `Converter::endingMap` one for one and an ending can no longer exist with a type
that says there is no ending. **Breaking:** both fields are public `ScoreData`.
The writer always sets `number`, which the schema requires; an empty list serializes as
`number=""`. `Converter` gains the reverse `core::StartStopDiscontinue` to
`api::EndingType` conversion, which replaces a hand-written switch in the reader with the
existing bridge table.
`lysuite/ly45f_Repeats_InvalidEndings.xml` is now pinned (363 -> 364). It failed the
strict compare on exactly this gap, `expected 'number=1, 2, 3', actual 'number=1'`, and is
the only file whose status changed across the 838-file corpus.
`data/synthetic/ending.3.0.xml` and `ending.4.0.xml` still fail on the `<ending>`
attributes mx::api does not model -- `print-object`, `end-length`, `text-x`, `text-y`,
font, color, and `system` -- so neither is pinned. No new corpus files, so no pinned-count
or audit regeneration.
## Testing
- [x] Seven new cases in RepeatApiTest: multi-number round trip with a label, exact
serialization of `number="1, 2, 3"` and the text node, the read path, an ending with
no label, a blank ending, absence, and a read of the `data/synthetic/ending.3.0.xml`
shape whose text body differs from its number
- [x] Full api suite passes (5921 assertions in 528 test cases)
- [x] `make api-roundtrip` passes (364 of 364 pinned)
- [x] `make core-roundtrip-test` passes (839 test cases)
- [x] `make core-unit` passes (212 assertions in 41 test cases)
- [x] `make fmt-check` clean on touched files
## References
- Closes webern#387
rpatters1
added a commit
to rpatters1/denigma
that referenced
this pull request
Aug 6, 2026
mx replaced BarlineData::endingType and the single int endingNumber with one optional EndingData carrying the whole pass list and the printed label, so both halves of the Finale ending record can now travel. RepeatPassList is copied whole instead of contributing only its first value, and the Info message apologizing for the discarded passes is gone. An ending played on passes 1, 2, and 3 exports as number="1, 2, 3" rather than number="1". The label comes from RepeatEndingStart::createEndingText(), which returns custom RepeatEndingText verbatim when the author set one and otherwise builds the pass list, appending a period when the Add Period repeat option is on. It is assigned only when it differs from the label MusicXML would render from the number attribute on its own, which is the pass list joined the way core::EndingNumber::toString joins it. A document with Add Period on therefore keeps the periods Finale draws, and one without it writes no text node at all. The closing barline repeats the numbers so both barlines identify the same bracket, but never the text, because MusicXML draws a label at every <ending> that carries one. The gap entry for ending display text and multiple numbers is closed. What remains unmappable is the ending's appearance, so the entry is replaced by one covering print-object, end-length, text-x, text-y, system, and the print-style group, each named with the Finale field that would feed it. Hidden endings are the notable loss, since they are structural in Finale rather than decorative. Pins mx to f52c88e, the head of rpatters1/mx@ending-numbers-and-text and open upstream as webern/mx#388. It supersedes 2671b8e, whose dynamics and harmonic work it carries in merged form as webern/mx#382 and #384. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rpatters1
added a commit
to rpatters1/denigma
that referenced
this pull request
Aug 6, 2026
mx replaced BarlineData::endingType and the single int endingNumber with one optional EndingData carrying the whole pass list and the printed label, so both halves of the Finale ending record can now travel. RepeatPassList is copied whole instead of contributing only its first value, and the Info message apologizing for the discarded passes is gone. An ending played on passes 1, 2, and 3 exports as number="1, 2, 3" rather than number="1". The label comes from RepeatEndingStart::createEndingText(), which returns custom RepeatEndingText verbatim when the author set one and otherwise builds the pass list, appending a period when the Add Period repeat option is on. It is assigned only when it differs from the label MusicXML would render from the number attribute on its own, which is the pass list joined the way core::EndingNumber::toString joins it. A document with Add Period on therefore keeps the periods Finale draws, and one without it writes no text node at all. The closing barline repeats the numbers so both barlines identify the same bracket, but never the text, because MusicXML draws a label at every <ending> that carries one. The gap entry for ending display text and multiple numbers is closed. What remains unmappable is the ending's appearance, so the entry is replaced by one covering print-object, end-length, text-x, text-y, system, and the print-style group, each named with the Finale field that would feed it. Hidden endings are the notable loss, since they are structural in Finale rather than decorative. Pins mx to f52c88e, the head of rpatters1/mx@ending-numbers-and-text and open upstream as webern/mx#388. It supersedes 2671b8e, whose dynamics and harmonic work it carries in merged form as webern/mx#382 and #384. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
webern
approved these changes
Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human Summary
An ending bracket can cover more than one pass, and its printed label is not always just
its numbers: a volta played on passes 1, 2, and 3 is sometimes drawn
1-3. The labelneed not be numeric at all;
to continueis an example.This PR permits a client to author and read every pass an ending covers, along with
whatever label is printed under the bracket, through a new optional
BarlineData::ending.Summary
MusicXML's ending element carries a list-valued number attribute, the passes the ending is
played on, and an element text body holding the printed label, which the spec uses when that
label differs from the numbers.
mx::core::Endingmodels both, as anEndingNumberwrapping avector of ints and a string value, but
mx::apidropped all of it:BarlineDatacarried anEndingTypeand a single int, soMeasureReader::parseBarlinekept only the first value of thenumber list, and neither direction ever touched
Ending::value().Both fields are replaced by one
std::optional<EndingData> ending:EndingData::numbersis the full list of passes. Empty means a blank ending, MusicXML's wayof saying a bracket is present but which passes it covers is unknown, a state the old int could
not express since 0 meant unspecified.
EndingData::textis the printed label. Left empty, the writer emits no text node and theending displays its numbers, so authoring without a label produces the same XML as before.
EndingType::noneis removed. Presence is now the optional itself, so the enum's three valuesmatch
Converter::endingMapone for one, and an ending can no longer exist carrying a type thatsays there is no ending.
Breaking:
BarlineData::endingType,BarlineData::endingNumber, andEndingType::noneareremoved from the public api.
The writer always sets the number attribute, which the schema requires; an empty list serializes
as
number="".Convertergains the reversecore::StartStopDiscontinuetoapi::EndingTypeconversion, which replaces a hand-written switch in the reader with the existing bridge table.
The repeat fields on
BarlineDataare deliberately untouched. An ending and a repeat areindependent children of the same barline and routinely share one, but
repeatremains a barebool with flat siblings; reshaping it is a separate breaking change (#249).
lysuite/ly45f_Repeats_InvalidEndings.xmlis now pinned (363 -> 364). It failed the strictcompare on exactly this gap, expected
number=1, 2, 3against actualnumber=1, and is the onlyfile whose status changed across the 838-file corpus.
data/synthetic/ending.3.0.xmlandending.4.0.xmlstill fail on the ending attributes mx::api does not model: print-object,end-length, text-x, text-y, font, color, and system. Neither is pinned. No new corpus files, so
no pinned-count or audit regeneration.
Testing
of
number="1, 2, 3"and the text node, the read path, an ending with no label, a blank ending,absence, and a read of the
data/synthetic/ending.3.0.xmlshape whose text body differs from itsnumber
make api-roundtrippasses (364 of 364 pinned)make core-roundtrip-testpasses (839 test cases)make core-unitpasses (212 assertions in 41 test cases)make fmt-checkclean on touched filesReferences
BarlineData::repeatTimesto the sibling repeat elementisSpecifiedbool pairs with an explicit optional value type #249