feat: model unmeasured tremolos and the full slash range in mx::api - #383
Merged
Conversation
## Summary MusicXML's `<tremolo>` has three kinds and `mx::api` modeled two of them. An unmeasured tremolo (`<tremolo type="unmeasured">0</tremolo>`, the buzz roll) was labeled "this tremolo is not a mark" by `OrnamentsFunctions` and then discarded by `parseOrnamentsSet`, so it vanished on read and could not be written at all. `mx::core` models it completely; the gap was confined to the api and impl layers. - `MarkType::tremoloUnmeasured` joins the existing tremolo enumerators. Which kind of tremolo a mark is stays in `MarkType`, where it already lived for `tremoloSingleOne` through `tremoloSingleFive` and `tremoloStart`/`tremoloStop`, rather than becoming a second `type` field in the payload that could contradict it. - `TremoloMarkData` gains an optional `smufl` glyph name, following the precedent in `PercussionData` and `NoteData::noteheadSmufl`. It is read and written for every tremolo type, not only unmeasured. MusicXML defines no meaning for it elsewhere, but recording it verbatim costs nothing and stops `synthetic/tremolo.3.1.xml` from losing the attribute it exists to cover. - One-note tremolos now cover the whole `tremolo-marks` range. The reader handled 0 through 5 and let 6, 7 and 8 fall through to the `ornamentsMap` default, silently rewriting a seven-slash tremolo as three. `tremoloSingleSix`, `tremoloSingleSeven` and `tremoloSingleEight` close that, and both slash-count lookups become switches. A count of 0 on a single-type tremolo still reads back as three slashes. It is legal MusicXML -- the 3.0 schema allowed it years before `unmeasured` existed -- but degenerate, since zero slashes draw nothing. The fallback is now documented, with a TODO to log the downgrade if mx ever grows a warning channel. Adds `data/synthetic/tremolo.unmeasured.3.1.xml` and pins it in `roundtrip-baseline.txt`. It omits the font and color attributes its `tremolo.3.0` and `tremolo.3.1` siblings carry, because `MarkData` models neither; those two files still fail the round trip on that unrelated limitation. The corert pinned count moves to 837. ## Testing - [x] Two new NoteDataTest cases: an api round trip of `tremoloUnmeasured` carrying a glyph name, and a read of `data/synthetic/tremolo.unmeasured.3.1.xml` that pins the core to api path - [x] The existing `tremolos` case extended from five slashes to eight, with a size assertion so a future truncation fails instead of passing on a short vector - [x] Full api suite passes (5487 assertions in 498 test cases) - [x] `make core-roundtrip-test` passes (838 test cases, 837 pinned files) - [x] `make api-roundtrip` passes (297 of 297 pinned)
5 tasks
webern
approved these changes
Aug 4, 2026
Signed-off-by: Matthew James Briggs <matthew.james.briggs@gmail.com>
webern
approved these changes
Aug 4, 2026
Contributor
Author
|
The linux build error is an insidious merge issue. Two parallel PRs bumped the number of input files by one file. That means that the count tracker only got bumped once but the number of files got bumped twice. There's a counter that needs to be manually increased by 1. |
Owner
Yeah that check is kind of annoying. Easy fix, I can take care of it. |
webern
pushed a commit
that referenced
this pull request
Aug 5, 2026
## Human Summary mx::api could only ever write an undecorated `<harmonic/>`. The natural/artificial distinction and the base/touching/sounding-pitch detail were dropped in both directions, so an artificial harmonic was indistinguishable from a natural one. This adds them as a MarkDataChoice payload. Also fixes the harmonic writer dropping print-object, font, and color. The wider print-object gap it uncovered is called out in the Summary and left for a separate change. ## Summary MusicXML's `<technical><harmonic>` states how a harmonic is produced (`<natural/>` or `<artificial/>`) and which of its three pitches the written notehead represents (`<base-pitch/>`, `<touching-pitch/>`, `<sounding-pitch/>`). `mx::core::Harmonic` models both, as `choice` and `choice2`, but `mx::api` dropped them in both directions: `NotationsWriter::addTechnical` built a bare `core::Harmonic` and never called `setChoice`/`setChoice2`, and `TechnicalFunctions::parseTechicalMark` read only the attributes. Following the `TremoloMarkData` / `ArpeggiateMarkData` precedent, this adds a fourth `MarkDataChoice` alternative: - `HarmonicMarkData::kind` is a `HarmonicKind` of unspecified, natural, or artificial. - `HarmonicMarkData::pitch` is a `HarmonicPitch` of unspecified, basePitch, touchingPitch, or soundingPitch. Both default to unspecified, so authoring without the payload writes the plain `<harmonic/>` exactly as before, and the reader leaves `choice` as none for a bare element rather than inventing a natural/artificial value. The two notes of an artificial harmonic each carry their own mark, artificial with basePitch on the stopped note and artificial with touchingPitch on the diamond-notehead note, so neither note depends on its neighbor for meaning. Also fixes a write-side gap found while adding it. The harmonic writer used `setAttributesFromPositionData`, so the element's font, color, and print-object were all lost on write. It now uses `setAttributesFromMarkData` and sets print-object explicitly, because `setAttributesFromPrintData` writes color and font but never touches printObject. That omission still affects every other mark going through `setAttributesFromMarkData` (fermata, dynamics, arpeggiate, non-arpeggiate), and the `impl::setPrintObject` template in `PrintFunctions.h` that looks like it was meant to cover it is unused and has its condition inverted. Left for a separate change. `data/synthetic/harmonic.3.0.xml` already existed but had never passed; it is now pinned (296 -> 297). `lysuite/ly32a_Notations.xml` and `ksuite/k004a_Technical.xml` get past their harmonic content but still fail on unrelated `<lyric>` and `<alter>` ordering, so neither is pinned. ## Testing - [x] Three new cases in MarkRoundTripTest: the artificial pair (base pitch and touching pitch), all eleven kind/pitch combinations, and a bare `<harmonic/>` that must read back with no payload - [x] Harmonic tests pass (`*Harmonic*`: 43 assertions in 3 test cases) - [x] Full api suite passes (5512 assertions in 499 test cases) - [x] `make api-roundtrip` passes (297 of 297 pinned) - [x] `make core-roundtrip-test` passes (837 test cases) ## References - Related to #382 and #383, which also add `MarkDataChoice` alternatives. Whichever of the three lands first, the others need a union resolution in `MarkDataChoice.h` and `MarkDataChoice.cpp`; the three payloads are independent.
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
Unmeasured tremolos (the buzz roll) now round-trip through
mx::api, keeping their SMuFL glyph name. One-note tremolos cover MusicXML's full 1-8 slash range instead of silently collapsing 6, 7, and 8 down to three.Summary
MusicXML's
<tremolo>has three kinds andmx::apimodeled two of them. An unmeasured tremolo (<tremolo type="unmeasured">0</tremolo>) was labeled "this tremolo is not a mark" byOrnamentsFunctionsand then discarded byparseOrnamentsSet, so it vanished on read and could not be written at all.mx::coremodels it completely; the gap was confined to the api and impl layers.MarkType::tremoloUnmeasuredjoins the existing tremolo enumerators. Which kind of tremolo a mark is stays inMarkType, where it already lived fortremoloSingleOnethroughtremoloSingleFiveandtremoloStart/tremoloStop, rather than becoming a secondtypefield in the payload that could contradict it.TremoloMarkDatagains an optionalsmuflglyph name, following the precedent inPercussionDataandNoteData::noteheadSmufl. It is read and written for every tremolo type, not only unmeasured. MusicXML defines no meaning for it elsewhere, but recording it verbatim costs nothing and stopssynthetic/tremolo.3.1.xmlfrom losing the attribute it exists to cover.One-note tremolos now cover the whole
tremolo-marksrange. The reader handled 0 through 5 and let 6, 7, and 8 fall through to theornamentsMapdefault, silently rewriting a seven-slash tremolo as three.tremoloSingleSix,tremoloSingleSeven, andtremoloSingleEightclose that, and both slash-count lookups become switches. Note that SMuFL only provides precomposed glyphs for the first five; because those are combining marks, a renderer builds the higher counts by stacking them.A count of 0 on a single-type tremolo still reads back as three slashes. It is legal MusicXML -- the 3.0 schema allowed it years before
unmeasuredexisted -- but degenerate, since zero slashes draw nothing. The fallback is now documented, with a TODO to log the downgrade if mx ever grows a warning channel.Adds
data/synthetic/tremolo.unmeasured.3.1.xmland pins it in the api round-trip baseline. It omits the font and color attributes itstremolo.3.0andtremolo.3.1siblings carry, becauseMarkDatamodels neither; those two files still fail the round trip on that unrelated limitation. The corert pinned count moves to 837.Testing
make api-test MX_RUNNING_IN_DOCKER=1(498 test cases, 5487 assertions)make api-roundtrip MX_RUNNING_IN_DOCKER=1(297 pinned files, 0 failed)make core-roundtrip-test MX_RUNNING_IN_DOCKER=1(838 test cases, 837 pinned files)make audit MX_RUNNING_IN_DOCKER=1(regeneratesdata/corpus.xmland the new.features.xmlsidecar)NoteDataTest: an api round trip oftremoloUnmeasuredcarrying a glyph name, a read ofdata/synthetic/tremolo.unmeasured.3.1.xmlpinning the core to api path, and the existingtremoloscase extended from five slashes to eightdata/synthetic/tremolo.unmeasured.3.1.xmlvalidated withxmllint --nonet --schema docs/musicxml-3.1-8bbe8e5.xsdmxgit diff --checkmake fmt-check MX_RUNNING_IN_DOCKER=1clean for every file this PR touches. A local run also flagssrc/include/mx/api/ApiEquality.h, which this branch does not modify -- local clang-format is 19.1.6 while CI pins 18.