Skip to content

feat: read and write a hairpin's niente circle through WedgeStart - #503

Draft
webern wants to merge 3 commits into
mainfrom
m/mxdev-niente
Draft

webern wants to merge 3 commits into
mainfrom
m/mxdev-niente

Conversation

@webern

@webern webern commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

AI Summary

A niente hairpin has a small circle at its point: a crescendo that grows out of nothing, or a diminuendo that fades away to nothing. MusicXML spells it <wedge niente="yes">, on the start of a crescendo but on the stop of a diminuendo, because that is where the point is. A read and write through mx::api dropped the attribute, so the circle was lost.

WedgeStart gains one field, niente (Bool, unspecified by default, part of WedgeStart equality). It is set on the start for either direction: yes draws the circle, no or unspecified leave a plain point. WedgeStop is unchanged, so a caller cannot put a circle on the wide end of a hairpin.

On write, a crescendo's niente goes on its start, and a diminuendo's goes on the stop that ends it, found with the same pairing the writer already uses to give an octave-shift stop the size of its start: within a part, by number, a stop ends the most recently opened start, across measures and staves. A start of unspecified type is written as a crescendo, as before, so its niente goes on the start. A diminuendo with niente and no stop has nowhere to put it; the value is left out and reported as dropped data (error severity), for no as well as yes.

On read, one pass over each part's wedges in document order pairs every stop with its start before the measures are read, so a diminuendo takes the niente of its stop even when the stop is in a later measure. Reader and writer pair the same way: the writer pairs in the order it writes, and a continue segment, which mx::api reads as a start of unspecified type and writes as a crescendo start, is paired as a start on both sides. niente on the wide end (a diminuendo's start, a crescendo's stop), on a continue segment, or on a stop that ends a continue is not read, and each is reported once as dropped data with the measure where it appears.

The pairing rule now lives in one small header shared by the reader, the octave-shift sizes and the wedge niente. The octave-shift code that moved onto it behaves exactly as before.

What existing callers see: scores that do not set niente write exactly what they wrote before. Reading a file now fills niente where the file states it, and a file with niente where MusicXML ignores it now produces a dropped-data diagnostic where it was silently dropped before. No file in the corpus outside the two pinned here uses niente. No public signature changes.

Out of scope: a continue segment is still read as a start of unspecified type and written back as a crescendo start, which this change does not fix. An absent number and number 1 still pair separately, as they already do for every spanner in mx, although MusicXML says an absent number means 1.

Pins two files in the api round-trip baseline:

  • synthetic/wedge.3.0.xml
  • synthetic/wedge.3.1.xml

Testing

  • New tests in DirectionDataTest: niente on a crescendo with and without a stop; a start of unspecified type; a diminuendo whose stop is in a later measure; no on both shapes; unspecified writes nothing; overlapping numbered diminuendos; a reused number; a diminuendo with no stop reported once for yes and for no, with its location; niente read from a file on each end; a diminuendo, a continue and a stop; two unnumbered diminuendos open at once on different staves, built through the api and read from a file that lists the lower staff first; WedgeStart equality
  • New tests in OttavaSizeApiTest: a nested unnumbered octave shift, and an unnumbered shift beside one numbered 1 on another staff
  • make api-test: 7175 assertions in 861 test cases pass
  • make api-roundtrip: 475 of 475 pinned files pass
  • make api-roundtrip-discover: 476 PASS, 365 FAIL; the PASS list is the parent's 474 plus the two files pinned here
  • make audit-test: 19 tests pass
  • mx builds as a single-translation-unit unity build; make fmt-check passes

References

@webern
webern added this pull request to stack #494 November 6, 2026 04:54
@webern webern added feature new feature request non-breaking fixes or implementation that do not require breaking changes api Affects the mx::api layer impl Affects the mx::impl layer ai Issues opened by, or through, a coding agent. labels Nov 6, 2026
@webern webern changed the title m/mxdev niente feat: read and write a hairpin's niente circle through WedgeStart Nov 6, 2026
// WedgeStop that ends it; without one the circle is left out and a diagnostic says so. A
// hairpin whose wedgeType is unspecified is written as a crescendo, with the circle on its
// start.
Bool niente;

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 think this could imply an n instead of a circle, even if the spec calls it niente, so lets disambiguate here.

Suggested change
Bool niente;
Bool nienteCircle;

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'm concerned about this PR. It's a lot of code churn in many files for something that doesn't seem to appear in the wild corpus at all. Holding on this one for further consideration.

Base automatically changed from m/mxdev-gracetime to main November 7, 2026 11:34
<wedge niente="yes"> draws a small circle at the point of a hairpin: a
crescendo out of nothing or a diminuendo to nothing. It was dropped on
read and never written.

WedgeStart gains Bool niente, set on the start for either direction.
MusicXML states it on the start of a crescendo but on the stop of a
diminuendo, so the writer routes it: a crescendo (or a wedge of
unspecified type, which is written as a crescendo) carries it on its
start, and SpannerResolver hands a diminuendo's value to the stop that
closes it, as it already does for an octave-shift's size. A diminuendo
with niente and no stop is reported as dropped data.

The reader pairs the part's wedges in one pass before reading its
measures, so a diminuendo start takes the niente of its stop even when
the stop is in a later measure. niente on the wide end of a hairpin (a
diminuendo start or a crescendo stop) or on a continue segment means
nothing in MusicXML and is reported as dropped data.

The pairing rule (within a part, by number, a stop closes the most
recently opened start) now lives in one place, SpannerPairing.h, shared
by the reader, the octave-shift sizes and the wedge niente.

Pins synthetic/wedge.3.0.xml and synthetic/wedge.3.1.xml.
…error

The reader reads a <wedge type="continue"> as a WedgeStart of unspecified
type, which the writer writes and pairs as a crescendo start. The niente
pre-pass skipped continues instead, so after a diminuendo, a continue and
a stop it gave the stop's niente to the diminuendo, which the writer then
could not write: the stop now ends the continue. The pre-pass opens on
every wedge that is not a stop, so the reader and the writer pair a stop
with the same start.

A niente that is not read or not written is reported as dropped data with
error severity, as every other dropped value is.

The WedgeStart::niente comment now says how yes, no and unspecified are
drawn, that a diminuendo's circle needs the WedgeStop that ends it, and
that a hairpin of unspecified type carries the circle on its start.

Tests cover the continue case, a reused number, both values on a
diminuendo with no stop, the location of each diagnostic, and two
unnumbered diminuendos open at once on different staves, built through the
api and read from a file whose order differs from the order mx writes.
An unnumbered ottava nested in another, and an unnumbered ottava beside one numbered 1 on another staff, each take their stop size from their own start.
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 feature new feature request impl Affects the mx::impl layer non-breaking fixes or implementation that do not require breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant