Skip to content

feat: ending numbers and display text in mx::api - #388

Open
rpatters1 wants to merge 1 commit into
webern:mainfrom
rpatters1:ending-numbers-and-text
Open

feat: ending numbers and display text in mx::api#388
rpatters1 wants to merge 1 commit into
webern:mainfrom
rpatters1:ending-numbers-and-text

Conversation

@rpatters1

Copy link
Copy Markdown
Contributor

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 label
need not be numeric at all; to continue is 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::Ending models both, as an EndingNumber wrapping a
vector of ints and a string value, but mx::api dropped all of it: BarlineData carried an
EndingType and a single int, so MeasureReader::parseBarline kept only the first value of the
number list, and neither direction ever touched Ending::value().

Both fields 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 itself, so the enum's three values
match Converter::endingMap one for one, and an ending can no longer exist carrying a type that
says there is no ending.

Breaking: BarlineData::endingType, BarlineData::endingNumber, and EndingType::none are
removed from the public api.

The writer always sets the number attribute, 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.

The repeat fields on BarlineData are deliberately untouched. An ending and a repeat are
independent children of the same barline and routinely share one, but repeat remains a bare
bool with flat siblings; reshaping it is a separate breaking change (#249).

lysuite/ly45f_Repeats_InvalidEndings.xml is now pinned (363 -> 364). It failed the strict
compare on exactly this gap, expected number=1, 2, 3 against 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. Neither is pinned. No new corpus files, so
no pinned-count or audit regeneration.

Testing

  • 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
  • Full api suite passes (5921 assertions in 528 test cases)
  • make api-roundtrip passes (364 of 364 pinned)
  • make core-roundtrip-test passes (839 test cases)
  • make core-unit passes (212 assertions in 41 test cases)
  • make fmt-check clean on touched files

References

## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

api: expose ending display text and multiple ending numbers

2 participants