From 445638bbd3eede4087864f664d24cd15eb32c32a Mon Sep 17 00:00:00 2001 From: Robert Patterson Date: Mon, 3 Aug 2026 06:58:54 -0500 Subject: [PATCH 1/2] feat: model unmeasured tremolos and the full slash range in mx::api ## Summary MusicXML's `` has three kinds and `mx::api` modeled two of them. An unmeasured tremolo (`0`, 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) --- data/corpus.xml | 157 ++++++++++-------- .../tremolo.unmeasured.3.1.features.xml | 84 ++++++++++ data/synthetic/tremolo.unmeasured.3.1.xml | 30 ++++ src/include/mx/api/MarkData.h | 25 ++- src/include/mx/api/MarkDataChoice.h | 27 ++- src/private/mx/api/MarkData.cpp | 41 ++--- src/private/mx/impl/NotationsWriter.cpp | 16 +- src/private/mx/impl/OrnamentsFunctions.cpp | 76 +++++---- src/private/mxtest/api/NoteDataTest.cpp | 75 ++++++++- src/private/mxtest/api/roundtrip-baseline.txt | 6 + .../mxtest/corert/CoreRoundtripTest.cpp | 4 +- 11 files changed, 404 insertions(+), 137 deletions(-) create mode 100644 data/synthetic/tremolo.unmeasured.3.1.features.xml create mode 100644 data/synthetic/tremolo.unmeasured.3.1.xml diff --git a/data/corpus.xml b/data/corpus.xml index 2e23e3030..9f0bac605 100644 --- a/data/corpus.xml +++ b/data/corpus.xml @@ -4,13 +4,13 @@ usage (descending). Do not edit by hand; regenerate with the tool. --> - + - + - + @@ -32,7 +32,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -396,6 +396,7 @@ synthetic/transpose.3.1.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -426,9 +427,9 @@ synthetic/work-title.3.0.xml - + - + custom/musescore-slur-start-stop.musicxml @@ -445,7 +446,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -809,6 +810,7 @@ synthetic/transpose.3.1.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -839,7 +841,7 @@ synthetic/work-title.3.0.xml - + custom/musescore-slur-start-stop.musicxml custom/segno_coda_roundtrip.3.0.xml @@ -855,7 +857,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -1219,6 +1221,7 @@ synthetic/transpose.3.1.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -1249,7 +1252,7 @@ synthetic/work-title.3.0.xml - + @@ -1278,7 +1281,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -1642,6 +1645,7 @@ synthetic/transpose.3.1.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -1672,9 +1676,9 @@ synthetic/work-title.3.0.xml - + - + custom/musescore-slur-start-stop.musicxml @@ -1691,7 +1695,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -2055,6 +2059,7 @@ synthetic/transpose.3.1.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -2085,9 +2090,9 @@ synthetic/work-title.3.0.xml - + - + custom/musescore-slur-start-stop.musicxml @@ -2104,7 +2109,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -2468,6 +2473,7 @@ synthetic/transpose.3.1.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -2621,7 +2627,7 @@ synthetic/words-symbol.4.0.xml - + @@ -2660,7 +2666,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -2863,6 +2869,7 @@ synthetic/toe.3.0.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -3115,7 +3122,7 @@ synthetic/words-symbol.4.0.xml - + custom/musescore-slur-start-stop.musicxml custom/segno_coda_roundtrip.3.0.xml @@ -3131,7 +3138,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -3412,6 +3419,7 @@ synthetic/toe.3.0.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -3441,7 +3449,7 @@ synthetic/work-title.3.0.xml - + custom/musescore-slur-start-stop.musicxml custom/segno_coda_roundtrip.3.0.xml @@ -3457,7 +3465,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -3660,6 +3668,7 @@ synthetic/toe.3.0.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -3684,7 +3693,7 @@ synthetic/work-title.3.0.xml - + custom/musescore-slur-start-stop.musicxml custom/segno_coda_roundtrip.3.0.xml @@ -3700,7 +3709,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -3903,6 +3912,7 @@ synthetic/toe.3.0.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -3927,7 +3937,7 @@ synthetic/work-title.3.0.xml - + custom/musescore-slur-start-stop.musicxml custom/segno_coda_roundtrip.3.0.xml @@ -3943,7 +3953,7 @@ foundsuite/Deutscher Tanz D.820.1.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -4146,6 +4156,7 @@ synthetic/toe.3.0.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -4597,7 +4608,7 @@ - + @@ -4617,7 +4628,7 @@ foundsuite/Invention_10.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -4723,6 +4734,7 @@ synthetic/toe.3.0.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -5844,7 +5856,7 @@ synthetic/word-font.3.0.xml - + custom/segno_coda_roundtrip.3.0.xml custom/segno_coda_roundtrip.3.1.xml @@ -5860,7 +5872,7 @@ foundsuite/Invention_12.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -6166,6 +6178,7 @@ synthetic/toe.3.0.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -6439,7 +6452,7 @@ foundsuite/PepAiraSco.xml foundsuite/PezR44Sco.xml foundsuite/RonCLunSco.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml kiritan_singing/kiritan_singing_01.xml kiritan_singing/kiritan_singing_02.xml kiritan_singing/kiritan_singing_03.xml @@ -6459,7 +6472,7 @@ foundsuite/PepAiraSco.xml foundsuite/PezR44Sco.xml foundsuite/RonCLunSco.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml kiritan_singing/kiritan_singing_01.xml kiritan_singing/kiritan_singing_02.xml kiritan_singing/kiritan_singing_03.xml @@ -6483,7 +6496,7 @@ foundsuite/PepAiraSco.xml foundsuite/PezR44Sco.xml foundsuite/RonCLunSco.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml kiritan_singing/kiritan_singing_01.xml kiritan_singing/kiritan_singing_02.xml kiritan_singing/kiritan_singing_03.xml @@ -6505,7 +6518,7 @@ foundsuite/PepAiraSco.xml foundsuite/PezR44Sco.xml foundsuite/RonCLunSco.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml kiritan_singing/kiritan_singing_01.xml kiritan_singing/kiritan_singing_02.xml kiritan_singing/kiritan_singing_03.xml @@ -6524,7 +6537,7 @@ foundsuite/PepAiraSco.xml foundsuite/PezR44Sco.xml foundsuite/RonCLunSco.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml kiritan_singing/kiritan_singing_01.xml kiritan_singing/kiritan_singing_02.xml kiritan_singing/kiritan_singing_03.xml @@ -6565,7 +6578,7 @@ foundsuite/PepAiraSco.xml foundsuite/PezR44Sco.xml foundsuite/RonCLunSco.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml kiritan_singing/kiritan_singing_01.xml kiritan_singing/kiritan_singing_02.xml kiritan_singing/kiritan_singing_03.xml @@ -6603,7 +6616,7 @@ foundsuite/PepAiraSco.xml foundsuite/PezR44Sco.xml foundsuite/RonCLunSco.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml kiritan_singing/kiritan_singing_01.xml kiritan_singing/kiritan_singing_02.xml kiritan_singing/kiritan_singing_03.xml @@ -6970,7 +6983,7 @@ foundsuite/O_Holy_Night-Adam-1871.xml foundsuite/O_Holy_Night.xml foundsuite/SCHUBERT An die Sonne.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/Bombe.xml ksuite/k011a_Tuplets.xml @@ -6990,7 +7003,7 @@ foundsuite/O_Holy_Night-Adam-1871.xml foundsuite/O_Holy_Night.xml foundsuite/SCHUBERT An die Sonne.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/Bombe.xml ksuite/k011a_Tuplets.xml @@ -7013,7 +7026,7 @@ foundsuite/RonCLunSco.xml foundsuite/Silent_Night-Hartwig.xml foundsuite/Silent_Night_Young_1.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k006a_Header_Scaling_Decimals.xml ksuite/k013a_OrchestralScoreFinale.xml @@ -7033,7 +7046,7 @@ foundsuite/O_Holy_Night-Adam-1871.xml foundsuite/O_Holy_Night.xml foundsuite/SCHUBERT An die Sonne.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/Bombe.xml ksuite/k011a_Tuplets.xml @@ -7110,7 +7123,7 @@ foundsuite/O_Holy_Night-Adam-1871.xml foundsuite/O_Holy_Night.xml foundsuite/SCHUBERT An die Sonne.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/Bombe.xml ksuite/k011a_Tuplets.xml @@ -7192,7 +7205,7 @@ synthetic/fermata.3.1.xml - + @@ -7211,7 +7224,7 @@ foundsuite/Invention_4.xml - + synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml synthetic/delayed-inverted-turn.3.0.xml @@ -7228,6 +7241,7 @@ synthetic/shake.3.0.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/turn.3.0.xml synthetic/vertical-turn.3.0.xml @@ -7312,7 +7326,7 @@ - + @@ -7334,7 +7348,7 @@ lysuite/ly33f_Trill_EndingOnGraceNote.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -7530,6 +7544,7 @@ synthetic/toe.3.0.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -7692,10 +7707,10 @@ - + - + custom/transposition.musicxml @@ -7705,14 +7720,14 @@ foundsuite/Berlioz_Le_Corsaire.xml foundsuite/PezR44Sco.xml foundsuite/SCHUBERT An die Sonne.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k013a_OrchestralScoreFinale.xml ksuite/k013b_OrchestralScoreSibelius.xml logicpro/logic01a_homoSapiens.xml lysuite/ly14a_StaffDetails_LineChanges.xml - + synthetic/accent.3.0.xml synthetic/accidental-mark.3.0.xml synthetic/accidental-mark.3.1.xml @@ -8072,6 +8087,7 @@ synthetic/transpose.3.1.xml synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml synthetic/trill-mark.3.0.xml synthetic/triple-tongue.3.0.xml synthetic/tuplet-dot.3.0.xml @@ -8117,7 +8133,7 @@ foundsuite/Berlioz_Le_Corsaire.xml foundsuite/PezR44Sco.xml foundsuite/SCHUBERT An die Sonne.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k013a_OrchestralScoreFinale.xml ksuite/k013b_OrchestralScoreSibelius.xml logicpro/logic01a_homoSapiens.xml @@ -8179,7 +8195,7 @@ foundsuite/Berlioz_Le_Corsaire.xml foundsuite/RonCLunSco.xml foundsuite/Schubert_der_Mueller.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k013b_OrchestralScoreSibelius.xml ksuite/k015a_System_Layout.xml lysuite/ly14a_StaffDetails_LineChanges.xml @@ -8283,7 +8299,7 @@ foundsuite/Berlioz_Le_Corsaire.xml foundsuite/Black Note Study Op 10 no 5.xml foundsuite/Moments Musicaux Op16 No4.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/Bombe.xml ksuite/k011a_Tuplets.xml lysuite/ly23c_Tuplet_Display_NonStandard.xml @@ -9164,7 +9180,7 @@ - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k001b_Articulations_Above.xml ksuite/k001c_Articulations_Below.xml ksuite/k004a_Technical.xml @@ -9291,7 +9307,7 @@ - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k001b_Articulations_Above.xml ksuite/k001c_Articulations_Below.xml ksuite/k004a_Technical.xml @@ -9339,20 +9355,20 @@ synthetic/staccatissimo.3.0.xml - + - - - + + + - - - - + + + + foundsuite/Berlioz_Le_Corsaire.xml @@ -9368,9 +9384,10 @@ musuite/testTremolo.xml recsuite/ActorPreludeSample.xml - + synthetic/tremolo.3.0.xml synthetic/tremolo.3.1.xml + synthetic/tremolo.unmeasured.3.1.xml @@ -9407,7 +9424,7 @@ - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k013a_OrchestralScoreFinale.xml ksuite/k013b_OrchestralScoreSibelius.xml lysuite/ly41c_StaffGroups.xml @@ -9610,7 +9627,7 @@ - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k013a_OrchestralScoreFinale.xml ksuite/k013b_OrchestralScoreSibelius.xml lysuite/ly41i_PartNameDisplay_Override.xml @@ -10603,7 +10620,7 @@ foundsuite/Schubert_der_Mueller.xml - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k006a_Header_Scaling_Decimals.xml ksuite/k013b_OrchestralScoreSibelius.xml @@ -11098,7 +11115,7 @@ - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k006a_Header_Scaling_Decimals.xml ksuite/k013b_OrchestralScoreSibelius.xml @@ -11227,7 +11244,7 @@ - foundsuite/Απτάλικο.xml + foundsuite/Απτάλικο.xml ksuite/k013b_OrchestralScoreSibelius.xml recsuite/Echigo_Jishi.xml diff --git a/data/synthetic/tremolo.unmeasured.3.1.features.xml b/data/synthetic/tremolo.unmeasured.3.1.features.xml new file mode 100644 index 000000000..5c15ba635 --- /dev/null +++ b/data/synthetic/tremolo.unmeasured.3.1.features.xml @@ -0,0 +1,84 @@ + + + + synthetic/tremolo.unmeasured.3.1.xml + 3.1 + + + grace + + + measure + + number + + + + notations + + + note + + + octave + + + ornaments + + + part + + id + + + + part-group + + type + + + + part-list + + + part-name + + + pitch + + + score-part + + id + + + + score-partwise + + version + + + + staff + + + step + + + tremolo + + default-x + default-y + placement + relative-x + relative-y + smufl + type + + + + voice + + + diff --git a/data/synthetic/tremolo.unmeasured.3.1.xml b/data/synthetic/tremolo.unmeasured.3.1.xml new file mode 100644 index 000000000..f61bf4f56 --- /dev/null +++ b/data/synthetic/tremolo.unmeasured.3.1.xml @@ -0,0 +1,30 @@ + + + + + + + x + + + + + + + + + + A + 1 + + 1 + 1 + + + 0 + + + + + + diff --git a/src/include/mx/api/MarkData.h b/src/include/mx/api/MarkData.h index b3a57b6dd..84a5a539d 100644 --- a/src/include/mx/api/MarkData.h +++ b/src/include/mx/api/MarkData.h @@ -89,13 +89,21 @@ enum class MarkType mordent, invertedMordent, schleifer, - tremoloSingleOne, ///< A tremolo on a single note (a glyph, not a spanner) with 1 slash - tremoloSingleTwo, ///< A tremolo on a single note (a glyph, not a spanner) with 2 slashes - tremoloSingleThree, ///< A tremolo on a single note (a glyph, not a spanner) with 3 slashes - tremoloSingleFour, ///< A tremolo on a single note (a glyph, not a spanner) with 4 slashes - tremoloSingleFive, ///< A tremolo on a single note (a glyph, not a spanner) with 5 slashes + // One-note tremolos: slashes struck through the stem of a single note, not a spanner across + // two. MusicXML allows 1 through 8 of them. SMuFL provides a precomposed glyph for only the + // first five (tremolo1 through tremolo5); because those are combining marks, a renderer builds + // the higher counts by stacking them. + tremoloSingleOne, ///< A one-note tremolo with 1 slash + tremoloSingleTwo, ///< A one-note tremolo with 2 slashes + tremoloSingleThree, ///< A one-note tremolo with 3 slashes, the customary number + tremoloSingleFour, ///< A one-note tremolo with 4 slashes + tremoloSingleFive, ///< A one-note tremolo with 5 slashes + tremoloSingleSix, ///< A one-note tremolo with 6 slashes + tremoloSingleSeven, ///< A one-note tremolo with 7 slashes + tremoloSingleEight, ///< A one-note tremolo with 8 slashes tremoloStart, ///< The first note of a measured (two-note) tremolo; slash count is in MarkData::choice tremoloStop, ///< The second note of a measured (two-note) tremolo; slash count is in MarkData::choice + tremoloUnmeasured, ///< An unmeasured tremolo (buzz roll); its glyph is named in MarkData::choice otherOrnament, ///< MusicXML's 'other-ornament' value unknownOrnament, ///< Error state @@ -272,10 +280,9 @@ struct MarkData Bool fingeringAlternate; // Payload for mark types whose data does not fit the common fields above. Its Kind SHOULD - // correspond to markType (e.g. MarkDataChoice::Kind::tremolo pairs with - // MarkType::tremoloStart/tremoloStop), but this is a convention that MarkData does not - // enforce. New mark-specific data belongs here, as a new MarkDataChoice alternative -- see - // MarkDataChoice.h. + // correspond to markType (e.g. MarkDataChoice::Kind::tremolo pairs with the MarkType::tremolo* + // values), but this is a convention that MarkData does not enforce. New mark-specific data + // belongs here, as a new MarkDataChoice alternative -- see MarkDataChoice.h. MarkDataChoice choice; MarkData(); diff --git a/src/include/mx/api/MarkDataChoice.h b/src/include/mx/api/MarkDataChoice.h index f801bb526..c7838f560 100644 --- a/src/include/mx/api/MarkDataChoice.h +++ b/src/include/mx/api/MarkDataChoice.h @@ -15,17 +15,32 @@ namespace mx namespace api { -// Payload for MarkType::tremoloStart / MarkType::tremoloStop: the measured-tremolo slash count -// (MusicXML text value, 0-8). Absent means "not specified" (the writer falls back to a -// default). The tremoloSingle* mark types encode their slash count in the enumerator itself and do -// not use this payload. +// Payload for the tremolo mark types. Which kind of tremolo a mark is -- a one-note tremolo, one +// end of a two-note (measured) tremolo, or an unmeasured tremolo -- is stated by the MarkType; this +// payload carries what the MarkType cannot say. struct TremoloMarkData { + // How many slashes are drawn through the stem of a two-note tremolo, 0 to 8. Set this on the + // MarkType::tremoloStart and MarkType::tremoloStop marks that bracket the pair, using the same + // count on both. Leave it absent to accept a three-slash default. + // + // A one-note tremolo states its slash count in the MarkType itself + // (MarkType::tremoloSingleOne through MarkType::tremoloSingleFive), and an unmeasured tremolo + // has no slash count, so neither uses this field. std::optional tremoloMarks; + + // The canonical SMuFL glyph name to draw for an unmeasured tremolo (MarkType::tremoloUnmeasured), + // from SMuFL's Tremolos range -- for example "pendereckiTremolo" or "unmeasuredTremolo". Leave + // it absent to accept SMuFL's "buzzRoll", the glyph MusicXML assumes when no name is given. + // + // MusicXML allows this on a one-note or two-note tremolo too, but defines no meaning for it + // there, and most applications ignore it. + std::optional smufl; }; MXAPI_EQUALS_BEGIN(TremoloMarkData) MXAPI_EQUALS_MEMBER(tremoloMarks) +MXAPI_EQUALS_MEMBER(smufl) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(TremoloMarkData); @@ -93,8 +108,8 @@ MXAPI_NOT_EQUALS_AND_VECTORS(NonArpeggiateMarkData); // unclear which fields apply to which mark. New mark-specific payloads belong here, as a new // alternative, rather than as a new direct field on MarkData. // -// The choice's Kind SHOULD correspond to MarkData::markType (e.g. Kind::tremolo pairs with -// MarkType::tremoloStart/tremoloStop), but this is a convention that this class does not enforce. +// The choice's Kind SHOULD correspond to MarkData::markType (e.g. Kind::tremolo pairs with the +// MarkType::tremolo* values), but this is a convention that this class does not enforce. // // Defaults to none (no mark-specific payload). class MarkDataChoice diff --git a/src/private/mx/api/MarkData.cpp b/src/private/mx/api/MarkData.cpp index ca546eb77..0f46f3d2a 100644 --- a/src/private/mx/api/MarkData.cpp +++ b/src/private/mx/api/MarkData.cpp @@ -122,7 +122,9 @@ bool isMarkOrnament(MarkType markType) (markType == MarkType::schleifer) || (markType == MarkType::tremoloSingleOne) || (markType == MarkType::tremoloSingleTwo) || (markType == MarkType::tremoloSingleThree) || (markType == MarkType::tremoloSingleFour) || (markType == MarkType::tremoloSingleFive) || - (markType == MarkType::tremoloStart) || (markType == MarkType::tremoloStop) || + (markType == MarkType::tremoloSingleSix) || (markType == MarkType::tremoloSingleSeven) || + (markType == MarkType::tremoloSingleEight) || (markType == MarkType::tremoloStart) || + (markType == MarkType::tremoloStop) || (markType == MarkType::tremoloUnmeasured) || (markType == MarkType::otherOrnament) || (markType == MarkType::unknownOrnament); } @@ -167,8 +169,10 @@ bool isMarkTremolo(MarkType markType) { return (markType == MarkType::tremoloSingleOne) || (markType == MarkType::tremoloSingleTwo) || (markType == MarkType::tremoloSingleThree) || (markType == MarkType::tremoloSingleFour) || - (markType == MarkType::tremoloSingleFive) || (markType == MarkType::tremoloStart) || - (markType == MarkType::tremoloStop); + (markType == MarkType::tremoloSingleFive) || (markType == MarkType::tremoloSingleSix) || + (markType == MarkType::tremoloSingleSeven) || (markType == MarkType::tremoloSingleEight) || + (markType == MarkType::tremoloStart) || (markType == MarkType::tremoloStop) || + (markType == MarkType::tremoloUnmeasured); } bool isMarkCustom(MarkType markType) @@ -201,29 +205,26 @@ MarkType getMarkTypeFromCustomString(const std::string &inString) int numTremoloSlashes(MarkType markType) { - if (markType == MarkType::tremoloSingleOne) + switch (markType) { + case MarkType::tremoloSingleOne: return 1; - } - - if (markType == MarkType::tremoloSingleTwo) - { + case MarkType::tremoloSingleTwo: return 2; - } - - if (markType == MarkType::tremoloSingleThree) - { + case MarkType::tremoloSingleThree: return 3; - } - - if (markType == MarkType::tremoloSingleFour) - { + case MarkType::tremoloSingleFour: return 4; - } - - if (markType == MarkType::tremoloSingleFive) - { + case MarkType::tremoloSingleFive: return 5; + case MarkType::tremoloSingleSix: + return 6; + case MarkType::tremoloSingleSeven: + return 7; + case MarkType::tremoloSingleEight: + return 8; + default: + break; } return VALUE_UNSPECIFIED; diff --git a/src/private/mx/impl/NotationsWriter.cpp b/src/private/mx/impl/NotationsWriter.cpp index 49f1613ca..b6efc9299 100644 --- a/src/private/mx/impl/NotationsWriter.cpp +++ b/src/private/mx/impl/NotationsWriter.cpp @@ -39,6 +39,7 @@ #include "mx/core/generated/PlacementText.h" #include "mx/core/generated/ShowTuplet.h" #include "mx/core/generated/Slur.h" +#include "mx/core/generated/SmuflGlyphName.h" #include "mx/core/generated/String.h" #include "mx/core/generated/StringNumber.h" #include "mx/core/generated/StrongAccent.h" @@ -696,17 +697,30 @@ void NotationsWriter::addOrnament(const api::MarkData &mark, core::Ornaments &ou case core::OrnamentsGroupChoice::Kind::tremolo: { core::Tremolo tremolo; setAttributesFromPositionData(mark.positionData, tremolo); + const auto tremoloData = mark.choice.isTremolo() ? mark.choice.tremolo() : api::TremoloMarkData{}; + if (mark.markType == api::MarkType::tremoloStart || mark.markType == api::MarkType::tremoloStop) { tremolo.setType(mark.markType == api::MarkType::tremoloStart ? core::TremoloType::start() : core::TremoloType::stop()); - tremolo.setValue(core::TremoloMarks{mark.choice.tremolo().tremoloMarks.value_or(3)}); + tremolo.setValue(core::TremoloMarks{tremoloData.tremoloMarks.value_or(3)}); + } + else if (mark.markType == api::MarkType::tremoloUnmeasured) + { + tremolo.setType(core::TremoloType::unmeasured()); + tremolo.setValue(core::TremoloMarks{0}); } else { tremolo.setType(core::TremoloType::single()); tremolo.setValue(core::TremoloMarks{api::numTremoloSlashes(mark.markType)}); } + + if (tremoloData.smufl.has_value() && !tremoloData.smufl->empty()) + { + tremolo.setSmufl(core::SmuflGlyphName{*tremoloData.smufl}); + } + group.setChoice(core::OrnamentsGroupChoice::tremolo(tremolo)); break; } diff --git a/src/private/mx/impl/OrnamentsFunctions.cpp b/src/private/mx/impl/OrnamentsFunctions.cpp index e5176ae42..2b7bf5840 100644 --- a/src/private/mx/impl/OrnamentsFunctions.cpp +++ b/src/private/mx/impl/OrnamentsFunctions.cpp @@ -161,52 +161,72 @@ void OrnamentsFunctions::parseOrnament(const core::OrnamentsGroupChoice &choiceO case core::OrnamentsGroupChoice::Kind::tremolo: { const auto &tremolo = choiceObj.asTremolo(); const auto type = tremolo.type().value_or(core::TremoloType::single()).tag(); + outMark.name = "tremolo"; + parseMarkDataAttributes(tremolo, outMark); + + api::TremoloMarkData tremoloData; + + if (tremolo.smufl().has_value()) + { + tremoloData.smufl = tremolo.smufl()->value(); + } if (type == core::TremoloType::Tag::start || type == core::TremoloType::Tag::stop) { - outMark.name = "tremolo"; - parseMarkDataAttributes(tremolo, outMark); outMark.markType = (type == core::TremoloType::Tag::start) ? api::MarkType::tremoloStart : api::MarkType::tremoloStop; - outMark.choice = api::TremoloMarkData{tremolo.value().value()}; + tremoloData.tremoloMarks = tremolo.value().value(); + outMark.choice = tremoloData; break; } - if (type != core::TremoloType::Tag::single) + if (type == core::TremoloType::Tag::unmeasured) { - // unmeasured tremolo -- not yet representable - outMark.name = "this tremolo is not a mark"; - outMark.markType = api::MarkType::unknownOrnament; - return; + // An unmeasured tremolo has no slash count; MusicXML fixes its element text at 0. + outMark.markType = api::MarkType::tremoloUnmeasured; + outMark.choice = tremoloData; + break; } - outMark.name = "tremolo"; - parseMarkDataAttributes(tremolo, outMark); - const auto numSlashes = tremolo.value().value(); - - if (numSlashes == 0) + if (tremoloData.smufl.has_value()) { - outMark.markType = api::MarkType::tremoloSingleThree; + outMark.choice = tremoloData; } - else if (numSlashes == 1) + + switch (tremolo.value().value()) { + case 1: outMark.markType = api::MarkType::tremoloSingleOne; - } - else if (numSlashes == 2) - { + break; + case 2: outMark.markType = api::MarkType::tremoloSingleTwo; - } - else if (numSlashes == 3) - { - outMark.markType = api::MarkType::tremoloSingleThree; - } - else if (numSlashes == 4) - { + break; + case 4: outMark.markType = api::MarkType::tremoloSingleFour; - } - else if (numSlashes == 5) - { + break; + case 5: outMark.markType = api::MarkType::tremoloSingleFive; + break; + case 6: + outMark.markType = api::MarkType::tremoloSingleSix; + break; + case 7: + outMark.markType = api::MarkType::tremoloSingleSeven; + break; + case 8: + outMark.markType = api::MarkType::tremoloSingleEight; + break; + default: + // Three slashes, the customary one-note tremolo. Reached by a count of 0, which is a + // legal but degenerate single-type tremolo that would draw nothing -- an unmeasured + // tremolo is also written with a count of 0, but says so with type="unmeasured" and is + // handled above. + // + // TODO: the remap is silent and lossy -- a count of 0 reads back as 3 and is written + // out that way. mx has no warning channel, so there is nowhere to report it. Log the + // downgrade here if a logging framework is ever added. + outMark.markType = api::MarkType::tremoloSingleThree; + break; } break; diff --git a/src/private/mxtest/api/NoteDataTest.cpp b/src/private/mxtest/api/NoteDataTest.cpp index 1749a7f4e..51228ace0 100644 --- a/src/private/mxtest/api/NoteDataTest.cpp +++ b/src/private/mxtest/api/NoteDataTest.cpp @@ -588,6 +588,12 @@ TEST(tremolos, NoteData) marks.emplace_back(mark); mark = MarkData{MarkType::tremoloSingleFive}; marks.emplace_back(mark); + mark = MarkData{MarkType::tremoloSingleSix}; + marks.emplace_back(mark); + mark = MarkData{MarkType::tremoloSingleSeven}; + marks.emplace_back(mark); + mark = MarkData{MarkType::tremoloSingleEight}; + marks.emplace_back(mark); // round trip it through xml auto &mgr = DocumentManager::getInstance(); @@ -614,7 +620,10 @@ TEST(tremolos, NoteData) const auto &onote = ovoice.notes.back(); const auto &omarks = onote.noteAttachmentData.marks; - for (int i = 1; i <= 5; ++i) + // MusicXML's tremolo-marks type allows 1 through 8 slashes. + REQUIRE(8 == omarks.size()); + + for (int i = 1; i <= 8; ++i) { const auto &markData = omarks.at(static_cast(i - 1)); CHECK_EQUAL(i, numTremoloSlashes(markData.markType)); @@ -700,6 +709,70 @@ TEST(measuredTremoloFromSyntheticFile, NoteData) T_END; +TEST(unmeasuredTremolo, NoteData) +{ + ScoreData score; + score.ticksPerQuarter = 1; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &staff = measure.staves.back(); + auto &voice = staff.voices[0]; + + voice.notes.emplace_back(); + auto ¬e = voice.notes.back(); + note.durationData.durationName = DurationName::quarter; + note.durationData.durationTimeTicks = 1; + note.tickTimePosition = 0; + MarkData mark{MarkType::tremoloUnmeasured}; + TremoloMarkData tremoloData; + tremoloData.smufl = "pendereckiTremolo"; + mark.choice = tremoloData; + note.noteAttachmentData.marks.emplace_back(mark); + + const auto out = mxtest::roundTrip(score); + + const auto &omarks = + out.parts.back().measures.back().staves.back().voices.at(0).notes.back().noteAttachmentData.marks; + REQUIRE(1 == omarks.size()); + CHECK(MarkType::tremoloUnmeasured == omarks.at(0).markType); + REQUIRE(omarks.at(0).choice.isTremolo()); + CHECK(!omarks.at(0).choice.tremolo().tremoloMarks.has_value()); + REQUIRE(omarks.at(0).choice.tremolo().smufl.has_value()); + CHECK_EQUAL("pendereckiTremolo", *omarks.at(0).choice.tremolo().smufl); +} + +T_END; + +// Parse the synthetic unmeasured-tremolo file and confirm mx::api surfaces it as a +// tremoloUnmeasured mark carrying the file's smufl glyph name. This pins the core -> api read path +// for unmeasured tremolos, which were previously dropped. +TEST(unmeasuredTremoloFromSyntheticFile, NoteData) +{ + const std::string path = mxtest::getResourcesDirectoryPath() + "synthetic/tremolo.unmeasured.3.1.xml"; + auto &docMgr = DocumentManager::getInstance(); + const auto docIdResult = docMgr.createFromFile(path); + REQUIRE(docIdResult.ok()); + const int docId = docIdResult.value(); + const auto scoreResult = docMgr.getData(docId); + docMgr.destroyDocument(docId); + REQUIRE(scoreResult.ok()); + const auto &score = scoreResult.value(); + + const auto &marks = + score.parts.back().measures.back().staves.back().voices.at(0).notes.back().noteAttachmentData.marks; + REQUIRE(1 == marks.size()); + CHECK(MarkType::tremoloUnmeasured == marks.at(0).markType); + REQUIRE(marks.at(0).choice.isTremolo()); + CHECK(!marks.at(0).choice.tremolo().tremoloMarks.has_value()); + REQUIRE(marks.at(0).choice.tremolo().smufl.has_value()); + CHECK_EQUAL("pendereckiTremolo", *marks.at(0).choice.tremolo().smufl); +} + +T_END; + TEST(miscFields, NoteData) { ScoreData score; diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index d113e78d2..611223073 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -538,3 +538,9 @@ lysuite/ly03d_Rhythm_DottedDurations_Factors.xml musuite/testMultiMeasureRest1.xml musuite/testMultiMeasureRest2.xml musuite/testMultiMeasureRest3.xml + +# Unmeasured tremolos: MarkType::tremoloUnmeasured plus TremoloMarkData::smufl. The reader +# previously discarded as "not a mark" and neither direction touched +# the smufl attribute. The sibling tremolo.3.0/3.1 fixtures still fail, on the unrelated fact that +# MarkData carries no font or color data. +synthetic/tremolo.unmeasured.3.1.xml diff --git a/src/private/mxtest/corert/CoreRoundtripTest.cpp b/src/private/mxtest/corert/CoreRoundtripTest.cpp index e1ad70216..0608f8e76 100644 --- a/src/private/mxtest/corert/CoreRoundtripTest.cpp +++ b/src/private/mxtest/corert/CoreRoundtripTest.cpp @@ -127,12 +127,12 @@ const CoreRoundtripRegistrar g_coreRoundtripRegistrar; } // namespace -// Pinned counts: 836 eligible files, none skipped. Count drift is a failure +// Pinned counts: 837 eligible files, none skipped. Count drift is a failure // even with zero individual fails, so a corpus or version-gate change is a // conscious decision, not silent decay. Registered last (registration is // discovery order; "zz" keeps it last alphabetically for shuffled runs too). TEST_CASE("zz-corert-pinned-counts", "[core-roundtrip]") { - CHECK(mxtest::corert::discoverInputFiles().size() == 836); + CHECK(mxtest::corert::discoverInputFiles().size() == 837); CHECK(g_skippedCount == 0); } From 17a12643b3f0925760be578793c31683c2c1181b Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Wed, 5 Aug 2026 08:34:42 +0200 Subject: [PATCH 2/2] bump test count --- src/private/mxtest/corert/CoreRoundtripTest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/private/mxtest/corert/CoreRoundtripTest.cpp b/src/private/mxtest/corert/CoreRoundtripTest.cpp index 0608f8e76..ecfe343e6 100644 --- a/src/private/mxtest/corert/CoreRoundtripTest.cpp +++ b/src/private/mxtest/corert/CoreRoundtripTest.cpp @@ -127,12 +127,12 @@ const CoreRoundtripRegistrar g_coreRoundtripRegistrar; } // namespace -// Pinned counts: 837 eligible files, none skipped. Count drift is a failure +// Pinned counts: 838 eligible files, none skipped. Count drift is a failure // even with zero individual fails, so a corpus or version-gate change is a // conscious decision, not silent decay. Registered last (registration is // discovery order; "zz" keeps it last alphabetically for shuffled runs too). TEST_CASE("zz-corert-pinned-counts", "[core-roundtrip]") { - CHECK(mxtest::corert::discoverInputFiles().size() == 837); + CHECK(mxtest::corert::discoverInputFiles().size() == 838); CHECK(g_skippedCount == 0); }