diff --git a/data/api.features.xml b/data/api.features.xml
index 8d98ced92..ebe8feb3c 100644
--- a/data/api.features.xml
+++ b/data/api.features.xml
@@ -28,7 +28,7 @@
other
- api::MarkType accidentalMark*; all members mapped except AccidentalValue::other (no api::MarkType::accidentalMarkOther member).
+ In <notations>: api::MarkType accidentalMark*. In <ornaments> after an ornament: api::OrnamentAccidental (api::Accidental) in the ornament mark's OrnamentMarkData; after tremolo, other-ornament or wavy-line it falls back to an accidentalMark* mark. All members mapped except AccidentalValue::other (no api member). Only value, position and placement are kept.
Accidental as text (key/ornament context) (suspected).
diff --git a/src/include/mx/api/MarkData.h b/src/include/mx/api/MarkData.h
index 2fe21d5a1..cc4f33fc5 100644
--- a/src/include/mx/api/MarkData.h
+++ b/src/include/mx/api/MarkData.h
@@ -81,7 +81,9 @@ enum class MarkType
otherOrnament, ///< MusicXML's 'other-ornament' value
unknownOrnament, ///< Error state
- // accidental marks
+ // accidental marks: an accidental shown above or below the note on its own. An accidental that
+ // belongs to an ornament, like the sharp over a trill, is held by that ornament instead
+ // (OrnamentMarkData in MarkDataChoice.h).
accidentalMarkSharp,
accidentalMarkNatural,
accidentalMarkFlat,
diff --git a/src/include/mx/api/MarkDataChoice.h b/src/include/mx/api/MarkDataChoice.h
index 7b9b73b47..cffd1b818 100644
--- a/src/include/mx/api/MarkDataChoice.h
+++ b/src/include/mx/api/MarkDataChoice.h
@@ -7,10 +7,13 @@
#include "mx/api/ApiCommon.h"
#include "mx/api/DynamicsData.h"
#include "mx/api/Id.h"
+#include "mx/api/PitchData.h"
+#include "mx/api/PositionData.h"
#include
#include
#include
+#include
namespace mx
{
@@ -195,6 +198,44 @@ MXAPI_EQUALS_MEMBER(pitch)
MXAPI_EQUALS_END;
MXAPI_NOT_EQUALS_AND_VECTORS(HarmonicMarkData);
+// An accidental printed with an ornament: the sharp over a trill that raises its upper note, or the
+// accidentals above and below a turn that alter its upper and lower notes.
+struct OrnamentAccidental
+{
+ // The accidental to show. An entry left at none is not written.
+ Accidental accidental = Accidental::none;
+
+ // Where the accidental sits, measured from the ornament rather than from the note. Placement
+ // above puts the accidental over the ornament, below puts it under. The horizontal and vertical
+ // alignment fields are not used.
+ PositionData positionData;
+};
+
+MXAPI_EQUALS_BEGIN(OrnamentAccidental)
+MXAPI_EQUALS_MEMBER(accidental)
+MXAPI_EQUALS_MEMBER(positionData)
+MXAPI_EQUALS_END;
+MXAPI_NOT_EQUALS_AND_VECTORS(OrnamentAccidental);
+
+// Payload for an ornament that has accidentals. A trill whose upper note is sharpened is one
+// MarkType::trillMark mark holding an OrnamentAccidental with Accidental::sharp.
+//
+// These marks can hold accidentals: a trill, a turn of any kind, a shake, a mordent, an inverted
+// mordent, a schleifer and a haydn. A tremolo or an other-ornament cannot, because its choice
+// already carries TremoloMarkData or OtherMarkData; neither can a wavy line, which is not a mark.
+// An accidental written with one of those, or one that stands above or below the note on its own,
+// is a MarkType::accidentalMark* mark of its own. On any other mark this payload is not written.
+struct OrnamentMarkData
+{
+ // The ornament's accidentals, usually one. A turn may have one above it and one below it.
+ std::vector accidentals;
+};
+
+MXAPI_EQUALS_BEGIN(OrnamentMarkData)
+MXAPI_EQUALS_MEMBER(accidentals)
+MXAPI_EQUALS_END;
+MXAPI_NOT_EQUALS_AND_VECTORS(OrnamentMarkData);
+
// A variant class that carries data for MarkType values whose payload does not fit MarkData's
// common fields.
//
@@ -222,7 +263,8 @@ class MarkDataChoice
dynamic,
compoundDynamics,
otherNotation,
- harmonic
+ harmonic,
+ ornament
};
MarkDataChoice();
@@ -246,6 +288,10 @@ class MarkDataChoice
MarkDataChoice(HarmonicMarkData value);
+ // Builds an ornament payload, unless it has no accidentals, in which case the result is none:
+ // an ornament without accidentals needs no payload.
+ MarkDataChoice(OrnamentMarkData value);
+
Kind kind() const;
bool isNone() const;
bool isTremolo() const;
@@ -256,6 +302,7 @@ class MarkDataChoice
bool isCompoundDynamics() const;
bool isOtherNotation() const;
bool isHarmonic() const;
+ bool isOrnament() const;
// Returns a copy of the internally held TremoloMarkData.
//
@@ -293,11 +340,17 @@ class MarkDataChoice
// HarmonicMarkData is returned.
const HarmonicMarkData harmonic() const;
+ // Returns a copy of the internally held OrnamentMarkData.
+ //
+ // Check isOrnament() first. If this is not an ornament payload, a default constructed
+ // OrnamentMarkData, with no accidentals, is returned.
+ const OrnamentMarkData ornament() const;
+
bool operator==(const MarkDataChoice &other) const;
private:
std::variant
+ StandardDynamic, CompoundDynamicsData, OtherNotationMarkData, HarmonicMarkData, OrnamentMarkData>
myValue;
};
diff --git a/src/private/mx/api/MarkDataChoice.cpp b/src/private/mx/api/MarkDataChoice.cpp
index cadb16412..dde5fd8d9 100644
--- a/src/private/mx/api/MarkDataChoice.cpp
+++ b/src/private/mx/api/MarkDataChoice.cpp
@@ -58,6 +58,20 @@ MarkDataChoice::MarkDataChoice(HarmonicMarkData value) : myValue{std::move(value
{
}
+// An ornament with no accidentals is stored as none, so a plain trill compares equal however it
+// was built.
+MarkDataChoice::MarkDataChoice(OrnamentMarkData value)
+{
+ if (value.accidentals.empty())
+ {
+ myValue = std::monostate{};
+ }
+ else
+ {
+ myValue = std::move(value);
+ }
+}
+
MarkDataChoice::Kind MarkDataChoice::kind() const
{
if (std::holds_alternative(myValue))
@@ -92,6 +106,10 @@ MarkDataChoice::Kind MarkDataChoice::kind() const
{
return Kind::harmonic;
}
+ if (std::holds_alternative(myValue))
+ {
+ return Kind::ornament;
+ }
return Kind::none;
}
@@ -212,6 +230,20 @@ const HarmonicMarkData MarkDataChoice::harmonic() const
return HarmonicMarkData{};
}
+bool MarkDataChoice::isOrnament() const
+{
+ return std::holds_alternative(myValue);
+}
+
+const OrnamentMarkData MarkDataChoice::ornament() const
+{
+ if (const auto *value = std::get_if(&myValue))
+ {
+ return *value;
+ }
+ return OrnamentMarkData{};
+}
+
bool MarkDataChoice::operator==(const MarkDataChoice &other) const
{
return myValue == other.myValue;
diff --git a/src/private/mx/impl/AccidentalMarkFunctions.cpp b/src/private/mx/impl/AccidentalMarkFunctions.cpp
index 287aa7063..a72e6e01d 100644
--- a/src/private/mx/impl/AccidentalMarkFunctions.cpp
+++ b/src/private/mx/impl/AccidentalMarkFunctions.cpp
@@ -30,5 +30,14 @@ api::MarkData AccidentalMarkFunctions::parseAccidentalMark() const
return markData;
}
+
+api::OrnamentAccidental AccidentalMarkFunctions::parseOrnamentAccidental() const
+{
+ Converter converter;
+ api::OrnamentAccidental accidental;
+ accidental.accidental = converter.convert(myAccidentalMark.value());
+ accidental.positionData = impl::getPositionData(myAccidentalMark);
+ return accidental;
+}
} // namespace impl
} // namespace mx
diff --git a/src/private/mx/impl/AccidentalMarkFunctions.h b/src/private/mx/impl/AccidentalMarkFunctions.h
index 2c9ee74dd..b46153f4b 100644
--- a/src/private/mx/impl/AccidentalMarkFunctions.h
+++ b/src/private/mx/impl/AccidentalMarkFunctions.h
@@ -26,6 +26,10 @@ class AccidentalMarkFunctions
AccidentalMarkFunctions(AccidentalMarkFunctions &&) = default;
api::MarkData parseAccidentalMark() const;
+ // Reads an accidental-mark that follows an ornament inside as one of that
+ // ornament's accidentals. An accidental value the api has no member for comes back as none.
+ api::OrnamentAccidental parseOrnamentAccidental() const;
+
private:
const core::AccidentalMark &myAccidentalMark;
const impl::Cursor myCursor;
diff --git a/src/private/mx/impl/NotationsWriter.cpp b/src/private/mx/impl/NotationsWriter.cpp
index 55b2dbf67..10483ac08 100644
--- a/src/private/mx/impl/NotationsWriter.cpp
+++ b/src/private/mx/impl/NotationsWriter.cpp
@@ -67,6 +67,7 @@
#include "mx/impl/GlissandoFunctions.h"
#include "mx/impl/IdFunctions.h"
#include "mx/impl/MarkDataFunctions.h"
+#include "mx/impl/OrnamentsFunctions.h"
#include "mx/impl/PositionFunctions.h"
#include "mx/impl/ScoreWriter.h"
#include "mx/impl/WavyLineFunctions.h"
@@ -383,6 +384,12 @@ core::Notations NotationsWriter::getNotations() const
{
this->addOrnament(mark, ornaments);
}
+ else if (mark.choice.isOrnament())
+ {
+ myScoreWriter.getDiagnostics().report(
+ api::Severity::error, api::DiagnosticCode::droppedData, cursorLocation(myCursor),
+ "ornament accidentals on a mark that cannot hold them are not written");
+ }
if (isMarkAccidentalMark(mark.markType))
{
this->addAccidentalMark(mark, outNotations);
@@ -1170,14 +1177,48 @@ void NotationsWriter::addOrnament(const api::MarkData &mark, core::Ornaments &ou
return;
}
+ addOrnamentAccidentals(mark, group);
outOrnaments.addGroup(group);
}
-// An accidental mark is written at the notations level. MusicXML also allows one inside
-// , next to the ornament it decorates, but the api does not record which of the two
-// places a mark came from -- both arrive here as the same MarkType -- and an group is
-// required to carry an ornament, so putting one there would mean inventing an ornament. Marks
-// from either place are written here instead.
+// An ornament's accidentals go in its own group, right after it, which is where MusicXML says an
+// accidental belongs to the ornament before it. Only the ornaments the reader lets own accidentals
+// may write them; anything else would come back as standalone accidental marks.
+void NotationsWriter::addOrnamentAccidentals(const api::MarkData &mark, core::OrnamentsGroup &outGroup) const
+{
+ if (!mark.choice.isOrnament())
+ {
+ return;
+ }
+
+ if (!ornamentsFunctionsCanOwnAccidentals(outGroup.choice().kind()))
+ {
+ myScoreWriter.getDiagnostics().report(api::Severity::error, api::DiagnosticCode::droppedData,
+ cursorLocation(myCursor),
+ "ornament accidentals on a mark that cannot hold them are not written");
+ return;
+ }
+
+ const auto payload = mark.choice.ornament();
+ for (const auto &accidental : payload.accidentals)
+ {
+ if (accidental.accidental == api::Accidental::none)
+ {
+ myScoreWriter.getDiagnostics().report(api::Severity::error, api::DiagnosticCode::droppedData,
+ cursorLocation(myCursor),
+ "an ornament accidental of none is not written");
+ continue;
+ }
+
+ core::AccidentalMark accidentalMark;
+ accidentalMark.setValue(myConverter.convert(accidental.accidental));
+ setAttributesFromPositionData(accidental.positionData, accidentalMark);
+ outGroup.addAccidentalMark(accidentalMark);
+ }
+}
+
+// A standalone accidental mark is written directly in . An accidental that belongs to an
+// ornament is not a mark of its own; addOrnamentAccidentals writes it inside .
void NotationsWriter::addAccidentalMark(const api::MarkData &mark, core::Notations &outNotations) const
{
if (!isMarkAccidentalMark(mark.markType))
diff --git a/src/private/mx/impl/NotationsWriter.h b/src/private/mx/impl/NotationsWriter.h
index ce4d9b0c8..4939c492d 100644
--- a/src/private/mx/impl/NotationsWriter.h
+++ b/src/private/mx/impl/NotationsWriter.h
@@ -36,6 +36,7 @@ class NotationsWriter
core::NotationsChoice makeTechnicalNotationsChoice() const;
void addArticulation(const api::MarkData &markData, core::Articulations &outArticulations) const;
void addOrnament(const api::MarkData &markData, core::Ornaments &outOrnaments) const;
+ void addOrnamentAccidentals(const api::MarkData &markData, core::OrnamentsGroup &outGroup) const;
void addAccidentalMark(const api::MarkData &markData, core::Notations &outNotations) const;
void addTechnical(const api::MarkData &markData, core::Technical &outTechnical) const;
diff --git a/src/private/mx/impl/OrnamentsFunctions.cpp b/src/private/mx/impl/OrnamentsFunctions.cpp
index b8ba69628..8da487c85 100644
--- a/src/private/mx/impl/OrnamentsFunctions.cpp
+++ b/src/private/mx/impl/OrnamentsFunctions.cpp
@@ -40,6 +40,46 @@ void ornamentsFunctionsParseMordentSpecificAttributes(const core::Mordent &m, ap
}
}
+// A trill, a turn of any kind, a shake, a mordent, an inverted mordent, a schleifer or a haydn. A
+// tremolo and an other-ornament already carry their own payload, and a wavy line is not a mark.
+bool ornamentsFunctionsCanOwnAccidentals(core::OrnamentsGroupChoice::Kind kind)
+{
+ switch (kind)
+ {
+ case core::OrnamentsGroupChoice::Kind::trillMark:
+ case core::OrnamentsGroupChoice::Kind::turn:
+ case core::OrnamentsGroupChoice::Kind::delayedTurn:
+ case core::OrnamentsGroupChoice::Kind::invertedTurn:
+ case core::OrnamentsGroupChoice::Kind::delayedInvertedTurn:
+ case core::OrnamentsGroupChoice::Kind::verticalTurn:
+ case core::OrnamentsGroupChoice::Kind::invertedVerticalTurn:
+ case core::OrnamentsGroupChoice::Kind::shake:
+ case core::OrnamentsGroupChoice::Kind::mordent:
+ case core::OrnamentsGroupChoice::Kind::invertedMordent:
+ case core::OrnamentsGroupChoice::Kind::schleifer:
+ case core::OrnamentsGroupChoice::Kind::haydn:
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Names an ornament that cannot hold accidentals, for diagnostics.
+std::string ornamentsFunctionsHostName(core::OrnamentsGroupChoice::Kind kind)
+{
+ switch (kind)
+ {
+ case core::OrnamentsGroupChoice::Kind::tremolo:
+ return "a tremolo";
+ case core::OrnamentsGroupChoice::Kind::otherOrnament:
+ return "an other-ornament";
+ case core::OrnamentsGroupChoice::Kind::wavyLine:
+ return "a wavy-line";
+ default:
+ return "an ornament";
+ }
+}
+
OrnamentsFunctions::OrnamentsFunctions(const core::Ornaments &inOrnaments, MeasureCursor inCursor,
DiagnosticsContext diagnostics)
: myOrnaments{inOrnaments}, myCursor{inCursor}, myDiagnostics{diagnostics}
@@ -48,12 +88,10 @@ OrnamentsFunctions::OrnamentsFunctions(const core::Ornaments &inOrnaments, Measu
void OrnamentsFunctions::parseOrnaments(api::NoteAttachmentData &outAttachments) const
{
- parseOrnamentsSet(outAttachments);
- parseAccidentalMarkSet(outAttachments.marks);
-}
+ // Accidentals that cannot belong to the ornament before them are added after every ornament
+ // of this element, as standalone accidental marks.
+ std::vector standaloneAccidentals;
-void OrnamentsFunctions::parseOrnamentsSet(api::NoteAttachmentData &outAttachments) const
-{
for (const auto &group : myOrnaments.group())
{
const auto &choiceObj = group.choice();
@@ -63,6 +101,7 @@ void OrnamentsFunctions::parseOrnamentsSet(api::NoteAttachmentData &outAttachmen
if (choiceObj.kind() == core::OrnamentsGroupChoice::Kind::wavyLine)
{
parseWavyLine(choiceObj.asWavyLine(), outAttachments);
+ parseStandaloneAccidentals(group, standaloneAccidentals);
continue;
}
@@ -73,22 +112,62 @@ void OrnamentsFunctions::parseOrnamentsSet(api::NoteAttachmentData &outAttachmen
parseOrnament(choiceObj, markData);
markData.tickTimePosition = myCursor.tickTimePosition;
+ if (ornamentsFunctionsCanOwnAccidentals(choiceObj.kind()))
+ {
+ markData.choice = parseOwnedAccidentals(group);
+ }
+ else
+ {
+ parseStandaloneAccidentals(group, standaloneAccidentals);
+ }
+
if (markData.markType != api::MarkType::unknownOrnament)
{
outAttachments.marks.emplace_back(std::move(markData));
}
}
+
+ for (auto &accidental : standaloneAccidentals)
+ {
+ outAttachments.marks.emplace_back(std::move(accidental));
+ }
}
-void OrnamentsFunctions::parseAccidentalMarkSet(std::vector &outMarks) const
+api::OrnamentMarkData OrnamentsFunctions::parseOwnedAccidentals(const core::OrnamentsGroup &group) const
{
- for (const auto &group : myOrnaments.group())
+ api::OrnamentMarkData payload;
+ for (const auto &am : group.accidentalMark())
{
- for (const auto &am : group.accidentalMark())
+ AccidentalMarkFunctions funcs{am, myCursor};
+ auto accidental = funcs.parseOrnamentAccidental();
+ if (accidental.accidental == api::Accidental::none)
{
- AccidentalMarkFunctions funcs{am, myCursor};
- outMarks.emplace_back(funcs.parseAccidentalMark());
+ auto location = measureLocation(myCursor);
+ location.staffIndex = myCursor.staffIndex;
+ myDiagnostics.report(api::Severity::error, api::DiagnosticCode::droppedData, std::move(location),
+ "an ornament's accidental-mark \"" + std::string{am.value().toString()} +
+ "\" is not supported and is not read");
+ continue;
}
+ payload.accidentals.emplace_back(std::move(accidental));
+ }
+ return payload;
+}
+
+// MusicXML lets an accidental follow a tremolo, an other-ornament or a wavy line, but none of
+// these hold accidentals in the api, so the accidental is read as a standalone accidental mark.
+void OrnamentsFunctions::parseStandaloneAccidentals(const core::OrnamentsGroup &group,
+ std::vector &outMarks) const
+{
+ for (const auto &am : group.accidentalMark())
+ {
+ AccidentalMarkFunctions funcs{am, myCursor};
+ outMarks.emplace_back(funcs.parseAccidentalMark());
+ auto location = measureLocation(myCursor);
+ location.staffIndex = myCursor.staffIndex;
+ myDiagnostics.report(api::Severity::warning, api::DiagnosticCode::valueAdjusted, std::move(location),
+ "an accidental-mark after " + ornamentsFunctionsHostName(group.choice().kind()) +
+ " is read as a standalone accidental mark");
}
}
diff --git a/src/private/mx/impl/OrnamentsFunctions.h b/src/private/mx/impl/OrnamentsFunctions.h
index 1a377d316..4e64c862c 100644
--- a/src/private/mx/impl/OrnamentsFunctions.h
+++ b/src/private/mx/impl/OrnamentsFunctions.h
@@ -15,11 +15,17 @@ namespace mx
namespace core
{
class Ornaments;
+class OrnamentsGroup;
class OrnamentsGroupChoice;
} // namespace core
namespace impl
{
+// Whether an ornament of this kind holds the accidental marks that follow it inside
+// (api::OrnamentMarkData). The reader and the writer share this so that what one keeps on an
+// ornament the other writes back there.
+bool ornamentsFunctionsCanOwnAccidentals(core::OrnamentsGroupChoice::Kind kind);
+
class OrnamentsFunctions
{
public:
@@ -35,9 +41,9 @@ class OrnamentsFunctions
DiagnosticsContext myDiagnostics;
private:
- void parseOrnamentsSet(api::NoteAttachmentData &outAttachments) const;
- void parseAccidentalMarkSet(std::vector &outMarks) const;
void parseOrnament(const core::OrnamentsGroupChoice &choiceObj, api::MarkData &outMark) const;
+ api::OrnamentMarkData parseOwnedAccidentals(const core::OrnamentsGroup &group) const;
+ void parseStandaloneAccidentals(const core::OrnamentsGroup &group, std::vector &outMarks) const;
};
} // namespace impl
} // namespace mx
diff --git a/src/private/mxtest/api/MarkRoundTripTest.cpp b/src/private/mxtest/api/MarkRoundTripTest.cpp
index d3204a3b5..fe033edb5 100644
--- a/src/private/mxtest/api/MarkRoundTripTest.cpp
+++ b/src/private/mxtest/api/MarkRoundTripTest.cpp
@@ -373,4 +373,210 @@ TEST(CaesuraVariants, MarkRoundTrip)
T_END;
+// Returns the diagnostics carrying exactly the given message.
+inline std::vector markRoundTripDiagnosticsWithMessage(const Diagnostics &diagnostics,
+ const std::string &message)
+{
+ std::vector out;
+ for (const auto &diagnostic : diagnostics.all())
+ {
+ if (diagnostic.message == message)
+ {
+ out.push_back(diagnostic);
+ }
+ }
+ return out;
+}
+
+// Writes a one-note score holding the given mark and returns the XML, collecting the diagnostics
+// the write reported.
+inline std::string markRoundTripWriteWithDiagnostics(const MarkData &inMarkData, Diagnostics &outDiagnostics)
+{
+ ScoreData score;
+ score.parts.emplace_back();
+ auto ¬e = score.parts.back().measures.emplace_back().staves.emplace_back().voices[0].notes.emplace_back();
+ note.noteAttachmentData.marks.push_back(inMarkData);
+ auto document = fromScore(score, outDiagnostics);
+ if (!document.ok())
+ return {};
+ std::stringstream ss;
+ std::move(document).value().writeToStream(ss);
+ return ss.str();
+}
+
+TEST(OrnamentAccidentals, MarkRoundTrip)
+{
+ OrnamentAccidental sharp;
+ sharp.accidental = Accidental::sharp;
+ sharp.positionData.placement = Placement::above;
+ sharp.positionData.isDefaultYSpecified = true;
+ sharp.positionData.defaultY = 12.5;
+
+ MarkData trill{Placement::unspecified, MarkType::trillMark};
+ trill.choice = OrnamentMarkData{{sharp}};
+ const auto trillMarks = roundTripMarkData(trill);
+ REQUIRE(trillMarks.size() == 1);
+ CHECK(trillMarks.front().markType == MarkType::trillMark);
+ REQUIRE(trillMarks.front().choice.isOrnament());
+ CHECK(trill.choice == trillMarks.front().choice);
+
+ OrnamentAccidental natural;
+ natural.accidental = Accidental::natural;
+ natural.positionData.placement = Placement::below;
+ OrnamentAccidental flat;
+ flat.accidental = Accidental::flat;
+
+ MarkData mordent{Placement::above, MarkType::mordent};
+ mordent.choice = OrnamentMarkData{{natural, flat}};
+ const auto mordentMarks = roundTripMarkData(mordent);
+ REQUIRE(mordentMarks.size() == 1);
+ CHECK(mordentMarks.front().markType == MarkType::mordent);
+ CHECK(Placement::above == mordentMarks.front().positionData.placement);
+ CHECK(mordent.choice == mordentMarks.front().choice);
+}
+
+T_END;
+
+// An accidental left at none is not drawn, so it is not written, and the write reports the loss.
+// The ornament's other accidentals are still written.
+TEST(OrnamentAccidentalOfNoneIsDropped, MarkRoundTrip)
+{
+ OrnamentAccidental none;
+ OrnamentAccidental sharp;
+ sharp.accidental = Accidental::sharp;
+
+ MarkData trill{Placement::unspecified, MarkType::trillMark};
+ trill.choice = OrnamentMarkData{{none, sharp}};
+
+ Diagnostics diagnostics;
+ const auto xml = markRoundTripWriteWithDiagnostics(trill, diagnostics);
+ CHECK(xml.find(">sharp") != std::string::npos);
+ const auto dropped =
+ markRoundTripDiagnosticsWithMessage(diagnostics, "an ornament accidental of none is not written");
+ REQUIRE(dropped.size() == 1);
+ CHECK(Severity::error == dropped.front().severity);
+ CHECK(DiagnosticCode::droppedData == dropped.front().code);
+
+ const auto marks = roundTripMarkData(trill);
+ REQUIRE(marks.size() == 1);
+ CHECK(MarkDataChoice{OrnamentMarkData{{sharp}}} == marks.front().choice);
+}
+
+T_END;
+
+// Ornament accidentals on a mark that is not written in have nowhere to go.
+TEST(OrnamentAccidentalsOnANonOrnamentAreDropped, MarkRoundTrip)
+{
+ OrnamentAccidental sharp;
+ sharp.accidental = Accidental::sharp;
+ MarkData accent{Placement::unspecified, MarkType::accent};
+ accent.choice = OrnamentMarkData{{sharp}};
+
+ Diagnostics diagnostics;
+ const auto xml = markRoundTripWriteWithDiagnostics(accent, diagnostics);
+ CHECK(xml.find("") != std::string::npos);
+ CHECK(xml.find(" content.
+inline std::string noteDataTestOneNoteXml(const std::string ¬ations)
{
- const std::string xml = R"(
+ return R"(
MusicXML Part
@@ -2119,41 +2117,261 @@ TEST(accidentalMarkOnAnOrnamentIsWrittenAtTheNotationsLevel, NoteData)
1
quarter
-
-
- sharp
-
+)" + notations +
+ R"(
)";
+}
+
+inline const std::vector ¬eDataTestFirstNoteMarks(const ScoreData &score)
+{
+ return score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0).noteAttachmentData.marks;
+}
+
+// issue #443: an accidental-mark inside belongs to the ornament before it, so the sharp
+// over a trill is read as part of the trill and written back inside after it.
+TEST(accidentalMarkOnAnOrnamentBelongsToTheOrnament, NoteData)
+{
+ const auto xml = noteDataTestOneNoteXml(R"(
+
+ sharp
+ )");
const auto score = fromXml(xml);
- const auto &marks = score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0).noteAttachmentData.marks;
- REQUIRE(2 == marks.size());
+ const auto &marks = noteDataTestFirstNoteMarks(score);
+ REQUIRE(1 == marks.size());
CHECK(MarkType::trillMark == marks.at(0).markType);
- CHECK(MarkType::accidentalMarkSharp == marks.at(1).markType);
+ REQUIRE(marks.at(0).choice.isOrnament());
+ CHECK(MarkDataChoice::Kind::ornament == marks.at(0).choice.kind());
+ const auto accidentals = marks.at(0).choice.ornament().accidentals;
+ REQUIRE(1 == accidentals.size());
+ CHECK(Accidental::sharp == accidentals.at(0).accidental);
+ CHECK(Placement::above == accidentals.at(0).positionData.placement);
const auto written = toXml(score);
- CHECK(written.find("");
+ const auto trillPos = written.find("");
+ REQUIRE(ornamentsPos != std::string::npos);
+ REQUIRE(trillPos != std::string::npos);
+ REQUIRE(accidentalPos != std::string::npos);
+ REQUIRE(ornamentsEndPos != std::string::npos);
+ CHECK(ornamentsPos < trillPos);
+ CHECK(trillPos < accidentalPos);
+ CHECK(accidentalPos < ornamentsEndPos);
- // both marks are still there after the source's ornament attachment is normalized away
const auto reread = fromXml(written);
- const auto &rereadMarks =
- reread.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0).noteAttachmentData.marks;
- REQUIRE(2 == rereadMarks.size());
- bool foundTrill = false;
- bool foundSharp = false;
- for (const auto &mark : rereadMarks)
+ CHECK(marks == noteDataTestFirstNoteMarks(reread));
+}
+
+T_END;
+
+// A turn may carry one accidental above it and another below it; both stay with the turn, in
+// order.
+TEST(turnKeepsItsAccidentalsInOrder, NoteData)
+{
+ const auto xml = noteDataTestOneNoteXml(R"(
+
+ sharp
+ three-quarters-flat
+ )");
+
+ const auto score = fromXml(xml);
+ const auto &marks = noteDataTestFirstNoteMarks(score);
+ REQUIRE(1 == marks.size());
+ CHECK(MarkType::turn == marks.at(0).markType);
+ const auto accidentals = marks.at(0).choice.ornament().accidentals;
+ REQUIRE(2 == accidentals.size());
+ CHECK(Accidental::sharp == accidentals.at(0).accidental);
+ CHECK(Placement::above == accidentals.at(0).positionData.placement);
+ CHECK(Accidental::threeQuartersFlat == accidentals.at(1).accidental);
+ CHECK(Placement::below == accidentals.at(1).positionData.placement);
+
+ const auto written = toXml(score);
+ const auto sharpPos = written.find(">sharp<");
+ const auto flatPos = written.find(">three-quarters-flat<");
+ REQUIRE(sharpPos != std::string::npos);
+ REQUIRE(flatPos != std::string::npos);
+ CHECK(written.find(""));
+ CHECK(marks == noteDataTestFirstNoteMarks(fromXml(written)));
+}
+
+T_END;
+
+// Each ornament keeps its own accidentals when one element holds several ornaments.
+TEST(eachOrnamentKeepsItsOwnAccidentals, NoteData)
+{
+ const auto xml = noteDataTestOneNoteXml(R"(
+
+ flat
+
+ natural
+ )");
+
+ const auto score = fromXml(xml);
+ const auto &marks = noteDataTestFirstNoteMarks(score);
+ REQUIRE(2 == marks.size());
+ CHECK(MarkType::trillMark == marks.at(0).markType);
+ REQUIRE(1 == marks.at(0).choice.ornament().accidentals.size());
+ CHECK(Accidental::flat == marks.at(0).choice.ornament().accidentals.at(0).accidental);
+ CHECK(MarkType::mordent == marks.at(1).markType);
+ REQUIRE(1 == marks.at(1).choice.ornament().accidentals.size());
+ CHECK(Accidental::natural == marks.at(1).choice.ornament().accidentals.at(0).accidental);
+ CHECK(marks == noteDataTestFirstNoteMarks(fromXml(toXml(score))));
+}
+
+T_END;
+
+// Reads the XML and returns the score, collecting the diagnostics the read reported.
+inline ScoreData noteDataTestReadWithDiagnostics(const std::string &xml, Diagnostics &outDiagnostics)
+{
+ std::istringstream iss{xml};
+ auto document = MusicXml::fromStream(iss);
+ if (!document.ok())
+ return {};
+ auto scoreResult = getScore(document.value(), outDiagnostics);
+ if (!scoreResult.ok())
+ return {};
+ return scoreResult.value();
+}
+
+// Returns the diagnostics carrying exactly the given message.
+inline std::vector noteDataTestDiagnosticsWithMessage(const Diagnostics &diagnostics,
+ const std::string &message)
+{
+ std::vector out;
+ for (const auto &diagnostic : diagnostics.all())
{
- foundTrill = foundTrill || (mark.markType == MarkType::trillMark);
- foundSharp = foundSharp || (mark.markType == MarkType::accidentalMarkSharp);
+ if (diagnostic.message == message)
+ {
+ out.push_back(diagnostic);
+ }
}
- CHECK(foundTrill);
- CHECK(foundSharp);
+ return out;
+}
+
+// A tremolo cannot hold accidentals, so an accidental after one is read as a standalone accidental
+// mark, after the ornament marks, and the read says so.
+TEST(accidentalMarkAfterTremoloIsStandalone, NoteData)
+{
+ const auto xml = noteDataTestOneNoteXml(R"(
+ 3
+ flat
+
+ )");
+
+ Diagnostics diagnostics;
+ const auto score = noteDataTestReadWithDiagnostics(xml, diagnostics);
+ const auto &marks = noteDataTestFirstNoteMarks(score);
+ REQUIRE(3 == marks.size());
+ CHECK(MarkType::tremoloSingleThree == marks.at(0).markType);
+ CHECK(MarkType::trillMark == marks.at(1).markType);
+ CHECK(marks.at(1).choice.isNone());
+ CHECK(MarkType::accidentalMarkFlat == marks.at(2).markType);
+ CHECK(Placement::below == marks.at(2).positionData.placement);
+
+ const auto fallbacks = noteDataTestDiagnosticsWithMessage(
+ diagnostics, "an accidental-mark after a tremolo is read as a standalone accidental mark");
+ REQUIRE(1 == fallbacks.size());
+ CHECK(Severity::warning == fallbacks.front().severity);
+ CHECK(DiagnosticCode::valueAdjusted == fallbacks.front().code);
+}
+
+T_END;
+
+// An other-ornament and a wavy line cannot hold accidentals either; theirs are read as standalone
+// accidental marks too, one diagnostic each.
+TEST(accidentalMarkAfterOtherOrnamentOrWavyLineIsStandalone, NoteData)
+{
+ const auto xml = noteDataTestOneNoteXml(R"(
+ x
+ sharp
+
+ natural
+ )");
+
+ Diagnostics diagnostics;
+ const auto score = noteDataTestReadWithDiagnostics(xml, diagnostics);
+ const auto ¬e = score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0);
+ const auto &marks = note.noteAttachmentData.marks;
+ REQUIRE(3 == marks.size());
+ CHECK(MarkType::otherOrnament == marks.at(0).markType);
+ CHECK(MarkType::accidentalMarkSharp == marks.at(1).markType);
+ CHECK(MarkType::accidentalMarkNatural == marks.at(2).markType);
+ CHECK(1 == note.noteAttachmentData.wavyLineStarts.size());
+
+ CHECK(1 == noteDataTestDiagnosticsWithMessage(
+ diagnostics, "an accidental-mark after an other-ornament is read as a standalone accidental mark")
+ .size());
+ CHECK(1 == noteDataTestDiagnosticsWithMessage(
+ diagnostics, "an accidental-mark after a wavy-line is read as a standalone accidental mark")
+ .size());
+}
+
+T_END;
+
+// The api has no accidental for MusicXML's "other" value, so an ornament's "other" accidental is
+// left out, the read says so, and the ornament's other accidentals are kept.
+TEST(otherAccidentalOnAnOrnamentIsDropped, NoteData)
+{
+ const auto xml = noteDataTestOneNoteXml(R"(
+
+ other
+ flat
+ )");
+
+ Diagnostics diagnostics;
+ const auto score = noteDataTestReadWithDiagnostics(xml, diagnostics);
+ const auto &marks = noteDataTestFirstNoteMarks(score);
+ REQUIRE(1 == marks.size());
+ CHECK(MarkType::turn == marks.at(0).markType);
+ const auto accidentals = marks.at(0).choice.ornament().accidentals;
+ REQUIRE(1 == accidentals.size());
+ CHECK(Accidental::flat == accidentals.at(0).accidental);
+
+ const auto dropped = noteDataTestDiagnosticsWithMessage(
+ diagnostics, "an ornament's accidental-mark \"other\" is not supported and is not read");
+ REQUIRE(1 == dropped.size());
+ CHECK(Severity::error == dropped.front().severity);
+ CHECK(DiagnosticCode::droppedData == dropped.front().code);
+}
+
+T_END;
+
+// A note may have both an accidental of its own in and an ornament with an accidental;
+// each keeps its place on write.
+TEST(standaloneAndOrnamentAccidentalsOnOneNote, NoteData)
+{
+ const auto xml = noteDataTestOneNoteXml(R"( flat
+
+
+ sharp
+ )");
+
+ const auto score = fromXml(xml);
+ const auto &marks = noteDataTestFirstNoteMarks(score);
+ REQUIRE(2 == marks.size());
+ CHECK(MarkType::accidentalMarkFlat == marks.at(0).markType);
+ CHECK(MarkType::trillMark == marks.at(1).markType);
+ REQUIRE(1 == marks.at(1).choice.ornament().accidentals.size());
+
+ const auto written = toXml(score);
+ const auto flatPos = written.find(">flat<");
+ const auto ornamentsPos = written.find("");
+ const auto sharpPos = written.find(">sharp<");
+ REQUIRE(flatPos != std::string::npos);
+ REQUIRE(ornamentsPos != std::string::npos);
+ REQUIRE(sharpPos != std::string::npos);
+ CHECK(flatPos < ornamentsPos);
+ CHECK(ornamentsPos < sharpPos);
+ CHECK(marks == noteDataTestFirstNoteMarks(fromXml(written)));
}
T_END;
diff --git a/src/private/mxtest/api/WavyLineApiTest.cpp b/src/private/mxtest/api/WavyLineApiTest.cpp
index 28eac8ea5..3cdeab956 100644
--- a/src/private/mxtest/api/WavyLineApiTest.cpp
+++ b/src/private/mxtest/api/WavyLineApiTest.cpp
@@ -230,6 +230,46 @@ TEST(writesInsideOrnaments, WavyLine)
T_END
+// A trill's accidental stays with the trill: it is written right after the trill-mark, and the
+// wavy line that starts on the same note follows.
+TEST(trillAccidentalPrecedesWavyLineStart, WavyLine)
+{
+ using namespace wavyLineApiTest;
+ ScoreData score;
+ 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.push_back(makeGraceNote(Step::c, 4));
+ auto ¬e = voice.notes.back();
+ MarkData trill{Placement::above, MarkType::trillMark};
+ OrnamentAccidental sharp;
+ sharp.accidental = Accidental::sharp;
+ trill.choice = OrnamentMarkData{{sharp}};
+ note.noteAttachmentData.marks.push_back(trill);
+ note.noteAttachmentData.wavyLineStarts.push_back(WavyLineStart{});
+
+ const auto xml = toXml(score);
+ mxpugi::xml_document xdoc;
+ REQUIRE(xdoc.load_string(xml.c_str()));
+ const auto ornaments =
+ xdoc.document_element().child("part").child("measure").child("note").child("notations").child("ornaments");
+ REQUIRE(ornaments);
+ std::vector names;
+ for (const auto &child : ornaments.children())
+ {
+ names.emplace_back(child.name());
+ }
+ const std::vector expected{"trill-mark", "accidental-mark", "wavy-line"};
+ CHECK(expected == names);
+}
+
+T_END
+
// Requirement 9: the synthetic fixtures parse and surface pairing plus the shared attributes.
TEST(readsSyntheticWavyLineFixture, WavyLine)
{