Skip to content

feat: an ornament owns the accidental marks written after it - #496

Merged
webern merged 3 commits into
mainfrom
m/mxdev-accmark
Nov 7, 2026
Merged

webern merged 3 commits into
mainfrom
m/mxdev-accmark

Conversation

@webern

@webern webern commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

AI Summary

MusicXML allows an accidental-mark in two places: directly in notations, where it stands above or below the note on its own, and inside ornaments right after an ornament, where it belongs to that ornament (the sharp over a trill that raises its upper note, the accidentals above and below a turn). mx read both as flat MarkType::accidentalMark* marks, losing which ornament an accidental belonged to, and since #462 wrote every one back at the notations level, outside the ornament.

An ornament mark now owns its accidentals. MarkDataChoice gains a new alternative, Kind::ornament, holding OrnamentMarkData:

  • OrnamentMarkData::accidentals is the ornament's list of OrnamentAccidental, in order. A trill, a turn of any kind, a shake, a mordent, an inverted mordent, a schleifer and a haydn can hold one. An OrnamentMarkData with no accidentals collapses to none, so a plain trill compares equal however it was built.
  • OrnamentAccidental::accidental is the accidental to show (api::Accidental).
  • OrnamentAccidental::positionData is where it sits, measured from the ornament rather than from the note, as MusicXML defines it.

The reader fills the list for each ornament from the accidental marks that follow it, and the writer puts each one back inside ornaments right after its ornament. Accidental marks in notations are unchanged.

Defined fallbacks, each with a diagnostic:

  • An accidental after a tremolo, an other-ornament or a wavy line has no ornament mark that can hold it (the tremolo and other-ornament already carry TremoloMarkData and OtherMarkData; a wavy line is not a mark). It is read as a standalone accidental mark, as before, with a warning.
  • An accidental-mark inside ornaments with the value other has no api::Accidental, so it is left out with an error. It used to become a MarkType::unspecified mark that was never written.
  • On write, an entry left at Accidental::none, and an OrnamentMarkData on a mark that cannot hold it (a tremolo, an other-ornament, or a mark that is not an ornament), are left out with an error.

Behaviour change

Reading <trill-mark/><accidental-mark>sharp</accidental-mark> used to give two marks, a trill and an accidentalMarkSharp. It now gives one trill mark whose choice.ornament().accidentals holds the sharp, and writing puts the sharp back inside ornaments after the trill. Code that looked in marks for a trill's accidental must now look in the trill. Accidental marks you author, and ones read from notations, are unchanged and still write at the notations level.

Out of scope

The accidental mark's drawing attributes (parentheses, bracket, size, SMuFL glyph, id, font and color) are still not read or written, for standalone and ornament accidentals alike. They come in the next layer of this stack. The two synthetic accidental-mark fixtures need them, so this change pins no new file.

Testing

  • New and updated api tests: trill, turn with two accidentals, two ornaments each with their own accidental, every ornament that can own accidentals, standalone plus ornament accidental on one note, wavy-line start after a trill's accidental, each fallback and its diagnostic, choice accessors, equality and the empty-list collapse
  • make api-test: 6768 assertions in 798 test cases pass
  • make api-roundtrip: 458 of 458 pinned files pass
  • make api-roundtrip-discover: 459 pass, 382 fail, the same pass list as main. lysuite/ly32a_Notations.xml and recsuite/MozaChloSample.xml still fail for unrelated reasons, but every accidental-mark difference in their round-trip diff is gone
  • make audit-test passes (19 tests); unity build of target mx passes; make fmt-check passes

References

@webern webern changed the title m/mxdev accmark feat: an ornament owns the accidental marks written after it Nov 6, 2026
@webern webern added ai Issues opened by, or through, a coding agent. api Affects the mx::api layer impl Affects the mx::impl layer feature new feature request breaking fixes or implementation that require breaking changes labels Nov 6, 2026
@webern
webern added this pull request to stack #498 November 6, 2026 01:12
@webern webern mentioned this pull request Nov 6, 2026
7 tasks done
MusicXML allows an <accidental-mark> in two places: directly in
<notations>, where it stands above or below the note on its own, and
inside <ornaments> right after an ornament, where it belongs to that
ornament (the sharp over a trill, the accidentals above and below a
turn). Both used to read as flat MarkType::accidentalMark* marks, with
the link to the ornament lost, and every one was written back at the
<notations> level.

MarkDataChoice gains a Kind::ornament alternative, OrnamentMarkData,
holding a list of OrnamentAccidental entries (an api::Accidental and
its position, measured from the ornament). A trill, any turn, a shake,
a mordent, an inverted mordent, a schleifer or a haydn holds the
accidentals that follow it, in order. An OrnamentMarkData with no
accidentals collapses to none.

Behaviour change: reading
<trill-mark/><accidental-mark>sharp</accidental-mark> used to give two
marks, a trill and an accidentalMarkSharp. It now gives one trill mark
whose choice.ornament().accidentals holds the sharp, and writing it puts
the sharp back inside <ornaments> right after the trill. Code that looked
in marks for a trill's accidental must look in the trill. Accidental
marks you author, and ones read from <notations>, are unchanged and
still write at the <notations> level.

An accidental after a tremolo, an other-ornament or a wavy line has no
ornament mark to hold it, so it is still read as a standalone accidental
mark after the ornament marks, with a warning. On write, an entry left
at Accidental::none, and ornament accidentals on a mark that is not
written in <ornaments>, are dropped with an error diagnostic. An
accidental-mark inside <ornaments> whose value the api has no member for
(other) is dropped on read with an error diagnostic.

Closes #443.
A tremolo or other-ornament mark given an OrnamentMarkData choice wrote
its accidentals inside <ornaments> after the ornament, with no
diagnostic, but the reader reads an accidental there as a standalone
accidental mark, so the score did not read back as it was written. The
writer now asks the same question the reader does (which ornaments can
own accidentals) and drops them on any other ornament with an error
diagnostic, as it already did for a mark that is not an ornament.

The OrnamentMarkData comment now says which marks can hold accidentals
and what happens to an accidental written with a tremolo, an
other-ornament or a wavy line. A test checks that every ornament that
can own accidentals writes and reads them back.
Covers an accidental after an other-ornament and after a wavy line (read
as standalone marks, one warning each), an "other" accidental on a turn
(dropped with an error, the turn's other accidental kept), and a note
with both a standalone accidental and a trill that owns one.
@webern
webern marked this pull request as ready for review November 7, 2026 12:45
@webern
webern merged commit bcb0d08 into main Nov 7, 2026
9 checks passed
@webern
webern deleted the m/mxdev-accmark branch November 7, 2026 12:47
webern added a commit that referenced this pull request Nov 7, 2026
## AI Summary

An accidental-mark lost how it is drawn when read into mx::api and
written back. Only its accidental value, position and placement were
kept; its parentheses, bracket, size, SMuFL glyph, id, font and color
were dropped on read and could not be authored. This applied both to a
standalone accidental mark in notations and to an accidental that
belongs to an ornament, such as the sharp over a trill.

What changes in mx::api:

- A new SymbolSize enum in ApiCommon.h (unspecified, full, cue,
graceCue, large) carries MusicXML's size attribute. unspecified leaves
the size to the application.
- MarkDataChoice gains an AccidentalMarkData alternative
(Kind::accidentalMark, isAccidentalMark(), accidentalMark()) for the
accidentalMark* mark types:
- parentheses and bracket (Bool): yes draws the accidental in
parentheses or square brackets, common ways to show an editorial
accidental. unspecified leaves it to the application.
  - size (SymbolSize): how large the accidental is drawn.
- smufl: a specific SMuFL accidental glyph, for example accidentalSharp.
A name that is not a SMuFL accidental (one starting with acc, medRenFla,
medRenNatura, medRenShar or kievanAccidental) is not written, and the
write reports an error diagnostic with the dropped-data code.
  - id: the element's id.
The accidental's font and color go in the existing MarkData::printData.
A payload with every field at its default is stored as none, so a mark
built from its MarkType alone still compares equal after a write and a
read.
- OrnamentAccidental gains the same five fields directly, plus PrintData
for its font and color. An owned accidental has no MarkData around it,
so its fields sit on the one struct a caller already reads.

One reader helper and one writer helper fill both shapes.

## Behaviour change for callers reading files

Reading an accidental mark that sets any of parentheses, bracket, size,
smufl or id now gives a choice holding AccidentalMarkData where it used
to be none. Its MarkData::printData, and an OrnamentAccidental's
printData, now holds the font and color from the file where it used to
be left at its default. Code that builds accidental marks is unaffected,
and nothing was removed or renamed, so this is labelled non-breaking.

## Out of scope

- MarkData::name is still filled in by the reader for an accidental mark
(for example "sharp") but left empty by the MarkData constructor, as it
already is for ornaments. The writer ignores name for these marks.
Changing the constructor would change what existing callers build, so it
is left alone here.
- printObject on an accidental mark's PrintData, and the alignment
fields of its PositionData, have no MusicXML attribute to go to and are
not written. The headers say so. Other marks drop these the same way
without a diagnostic, and adding one for every mark belongs in its own
change.
- A MarkDataChoice payload of another kind on an accidental mark is
ignored, as on every other mark. An ornament payload on an accidental
mark is still reported, as before.

## Pinned files

- synthetic/accidental-mark.3.0.xml
- synthetic/accidental-mark.3.1.xml

## Testing

- [x] MarkRoundTripTest: AccidentalMarkAttributes, AccidentalMarkSizes,
AccidentalMarkDefaultsToNoPayload, OrnamentAccidentalAttributes,
AccidentalMarkSmuflNotAnAccidental,
AccidentalMarkChoiceAccessorsAndEquality (every new field's equality,
both shapes)
- [x] NoteDataTest: readsSyntheticAccidentalMarkFixture reads every
attribute of the trill's sharp from the 3.1 fixture;
standaloneAccidentalMarkKeepsHowItIsDrawn reads a standalone
accidental-mark's attributes, checks the written xml carries each one,
and checks a bare one reads with no payload
- [x] make api-test: 6884 assertions in 806 test cases pass
- [x] make api-roundtrip: 460 of 460 pinned files pass, two more than
before
- [x] make api-roundtrip-discover: 461 PASS, 380 FAIL; the PASS list is
main's plus the two pinned files
- [x] make audit-test passes; make fmt-check passes
- [x] Unity build of target mx (CMAKE_UNITY_BUILD, batch size 0)
succeeds

## References

- Stacked on #496
- Progresses #403 (the accidental-mark id)
- Completes the accidental-mark work started in #443 and #496
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

the writer drops tuplet position, midi-unpitched and accidental marks

1 participant