diff --git a/data/corpus.xml b/data/corpus.xml
index 7496c6c9a..0719fdbaf 100644
--- a/data/corpus.xml
+++ b/data/corpus.xml
@@ -397,6 +397,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
@@ -811,6 +812,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
@@ -1222,6 +1224,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
@@ -1646,6 +1649,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
@@ -2060,6 +2064,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
@@ -2474,6 +2479,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
@@ -2873,6 +2879,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
@@ -3424,6 +3431,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
@@ -3673,6 +3681,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
@@ -3917,6 +3926,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
@@ -4161,6 +4171,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
@@ -4739,6 +4750,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
@@ -5862,7 +5874,7 @@
synthetic/word-font.3.0.xml
-
+
custom/segno_coda_roundtrip.3.0.xml
custom/segno_coda_roundtrip.3.1.xml
@@ -5878,7 +5890,7 @@
foundsuite/Invention_12.xml
-
+
synthetic/accent.3.0.xml
synthetic/accidental-mark.3.0.xml
synthetic/accidental-mark.3.1.xml
@@ -6184,6 +6196,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
@@ -7210,7 +7223,7 @@
synthetic/fermata.3.1.xml
-
+
@@ -7229,7 +7242,7 @@
foundsuite/Invention_4.xml
-
+
synthetic/accidental-mark.3.0.xml
synthetic/accidental-mark.3.1.xml
synthetic/delayed-inverted-turn.3.0.xml
@@ -7246,6 +7259,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
@@ -7549,6 +7563,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
@@ -7711,10 +7726,10 @@
-
+
-
+
custom/transposition.musicxml
@@ -7731,7 +7746,7 @@
lysuite/ly14a_StaffDetails_LineChanges.xml
-
+
synthetic/accent.3.0.xml
synthetic/accidental-mark.3.0.xml
synthetic/accidental-mark.3.1.xml
@@ -8091,6 +8106,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
@@ -9358,20 +9374,20 @@
synthetic/staccatissimo.3.0.xml
-
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
foundsuite/Berlioz_Le_Corsaire.xml
@@ -9387,9 +9403,10 @@
musuite/testTremolo.xml
recsuite/ActorPreludeSample.xml
-
+
synthetic/tremolo.3.0.xml
synthetic/tremolo.3.1.xml
+ synthetic/tremolo.unmeasured.3.1.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 685c286e9..aa4be00ef 100644
--- a/src/private/mxtest/api/roundtrip-baseline.txt
+++ b/src/private/mxtest/api/roundtrip-baseline.txt
@@ -540,6 +540,12 @@ musuite/testMultiMeasureRest2.xml
musuite/testMultiMeasureRest3.xml
synthetic/multiple-rest.3.0.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
+
# Passed on macOS but not on Linux until ScoreData::sort() was made stable. It is
# 14 chords, and every member of a chord shares one tick, so an unstable sort let
# the standard library choose which one came first -- and MusicXML spells a chord
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);
}