diff --git a/src/include/mx/api/ClefData.h b/src/include/mx/api/ClefData.h index 2f0fd89c6..c4feba6cd 100644 --- a/src/include/mx/api/ClefData.h +++ b/src/include/mx/api/ClefData.h @@ -21,6 +21,11 @@ enum class ClefSymbol jianpu }; +// Where a clef sits relative to its measure. A clef at the start of a measure normally prints +// before the barline; beforeBarline and afterBarline choose between the two, which matters for cue +// clefs and for the clef that resumes after a repeated section. midMeasure describes a clef change +// partway through the measure, positioned by tickTimePosition. unspecified leaves the choice to the +// notation program. enum class ClefLocation { unspecified, @@ -53,6 +58,12 @@ class ClefData bool isOctaveChangeSpecified; int tickTimePosition; ClefLocation location; + // Marks a supplementary clef placed on the staff alongside the regular one - a cue clef, or a + // second clef in effect at the same time. MusicXML writes this as clef@additional="yes"; such a + // clef sits at a non-standard line position, is not restated at the start of each system, and + // notation software disregards its line. unspecified (the default) omits the attribute, which + // describes an ordinary clef. + Bool additional; // Visibility of the clef via the MusicXML print-object attribute. // unspecified -> omit the attribute, yes/no -> write print-object verbatim. Bool printObject; @@ -86,6 +97,7 @@ MXAPI_EQUALS_MEMBER(octaveChange) MXAPI_EQUALS_MEMBER(isOctaveChangeSpecified) MXAPI_EQUALS_MEMBER(tickTimePosition) MXAPI_EQUALS_MEMBER(location) +MXAPI_EQUALS_MEMBER(additional) MXAPI_EQUALS_MEMBER(printObject) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(ClefData); diff --git a/src/private/mx/api/ClefData.cpp b/src/private/mx/api/ClefData.cpp index 4219b7dc6..ccd1e103f 100644 --- a/src/private/mx/api/ClefData.cpp +++ b/src/private/mx/api/ClefData.cpp @@ -12,7 +12,7 @@ namespace api ClefData::ClefData() : writeStaffNumber{Bool::unspecified}, symbol{DEFAULT_CLEF_SYMBOL}, line{DEFAULT_CLEF_LINE}, isLineSpecified{true}, octaveChange{DEFAULT_CLEF_OCTAVE_CHANGE}, isOctaveChangeSpecified{true}, tickTimePosition{0}, - location{ClefLocation::unspecified}, printObject{Bool::unspecified} + location{ClefLocation::unspecified}, additional{Bool::unspecified}, printObject{Bool::unspecified} { } diff --git a/src/private/mx/impl/MeasureReader.cpp b/src/private/mx/impl/MeasureReader.cpp index f6fa4f001..ff689f064 100644 --- a/src/private/mx/impl/MeasureReader.cpp +++ b/src/private/mx/impl/MeasureReader.cpp @@ -1114,6 +1114,11 @@ void MeasureReader::importClef(const core::Clef &inClef) const clefData.isOctaveChangeSpecified = false; } + if (inClef.additional().has_value()) + { + clefData.additional = converter.convert(*inClef.additional()); + } + if (inClef.printObject().has_value()) { clefData.printObject = converter.convert(*inClef.printObject()); diff --git a/src/private/mx/impl/PropertiesWriter.cpp b/src/private/mx/impl/PropertiesWriter.cpp index fdf165197..bd1ee6894 100644 --- a/src/private/mx/impl/PropertiesWriter.cpp +++ b/src/private/mx/impl/PropertiesWriter.cpp @@ -354,6 +354,22 @@ void PropertiesWriter::writeClef(int staffIndex, const api::ClefData &inClefData cg.setClefOctaveChange(inClefData.octaveChange); } + if (inClefData.additional != api::Bool::unspecified) + { + mxClef.setAdditional(converter.convert(inClefData.additional)); + } + + // after-barline states which side of the barline a start-of-measure clef prints on. MusicXML + // ignores it on mid-measure clefs, so those emit nothing. + if (inClefData.location == api::ClefLocation::afterBarline) + { + mxClef.setAfterBarline(core::YesNo::yes()); + } + else if (inClefData.location == api::ClefLocation::beforeBarline) + { + mxClef.setAfterBarline(core::YesNo::no()); + } + if (inClefData.printObject != api::Bool::unspecified) { mxClef.setPrintObject(converter.convert(inClefData.printObject)); diff --git a/src/private/mxtest/api/ClefDataTest.cpp b/src/private/mxtest/api/ClefDataTest.cpp index 38e7f069c..b2dba1316 100644 --- a/src/private/mxtest/api/ClefDataTest.cpp +++ b/src/private/mxtest/api/ClefDataTest.cpp @@ -31,6 +31,25 @@ ScoreData makeScoreWithClef(Bool printObject) staff.voices[0].notes.emplace_back(); return score; } + +ScoreData makeScoreWithAdditionalClef(Bool additional) +{ + ScoreData score = makeScoreWithClef(Bool::unspecified); + score.parts.front().measures.front().staves.front().clefs.front().additional = additional; + return score; +} + +ScoreData makeScoreWithClefLocation(ClefLocation location) +{ + ScoreData score = makeScoreWithClef(Bool::unspecified); + score.parts.front().measures.front().staves.front().clefs.front().location = location; + return score; +} + +const ClefData &firstClef(const ScoreData &score) +{ + return score.parts.front().measures.front().staves.front().clefs.front(); +} } // namespace mxtest::api::clef_data_test TEST(clefPrintObjectNoRoundTrip, ClefData) @@ -78,4 +97,156 @@ TEST(clefPrintObjectUnspecifiedIsOmitted, ClefData) T_END; +TEST(clefAdditionalYesRoundTrip, ClefData) +{ + const auto xml = mxtest::toXml(mxtest::api::clef_data_test::makeScoreWithAdditionalClef(Bool::yes)); + CHECK(xml.find("additional=\"yes\"") != std::string::npos); + + const auto out = mxtest::fromXml(xml); + CHECK_EQUAL(1, out.parts.size()); + CHECK_EQUAL(1, out.parts.front().measures.size()); + CHECK_EQUAL(1, out.parts.front().measures.front().staves.size()); + CHECK_EQUAL(1, out.parts.front().measures.front().staves.front().clefs.size()); + CHECK(Bool::yes == out.parts.front().measures.front().staves.front().clefs.front().additional); +} + +T_END; + +TEST(clefAdditionalNoRoundTrip, ClefData) +{ + const auto xml = mxtest::toXml(mxtest::api::clef_data_test::makeScoreWithAdditionalClef(Bool::no)); + CHECK(xml.find("additional=\"no\"") != std::string::npos); + + const auto out = mxtest::fromXml(xml); + CHECK_EQUAL(1, out.parts.size()); + CHECK_EQUAL(1, out.parts.front().measures.size()); + CHECK_EQUAL(1, out.parts.front().measures.front().staves.size()); + CHECK_EQUAL(1, out.parts.front().measures.front().staves.front().clefs.size()); + CHECK(Bool::no == out.parts.front().measures.front().staves.front().clefs.front().additional); +} + +T_END; + +TEST(clefAdditionalUnspecifiedIsOmitted, ClefData) +{ + const auto xml = mxtest::toXml(mxtest::api::clef_data_test::makeScoreWithAdditionalClef(Bool::unspecified)); + CHECK(xml.find("additional=") == std::string::npos); + + const auto out = mxtest::fromXml(xml); + CHECK_EQUAL(1, out.parts.size()); + CHECK_EQUAL(1, out.parts.front().measures.size()); + CHECK_EQUAL(1, out.parts.front().measures.front().staves.size()); + CHECK_EQUAL(1, out.parts.front().measures.front().staves.front().clefs.size()); + CHECK(Bool::unspecified == out.parts.front().measures.front().staves.front().clefs.front().additional); +} + +T_END; + +TEST(clefAfterBarlineYesRoundTrip, ClefData) +{ + using namespace mxtest::api::clef_data_test; + const auto xml = mxtest::toXml(makeScoreWithClefLocation(ClefLocation::afterBarline)); + CHECK(xml.find("after-barline=\"yes\"") != std::string::npos); + + const auto out = mxtest::fromXml(xml); + CHECK_EQUAL(1, out.parts.front().measures.front().staves.front().clefs.size()); + CHECK(ClefLocation::afterBarline == firstClef(out).location); +} + +T_END; + +TEST(clefAfterBarlineNoRoundTrip, ClefData) +{ + using namespace mxtest::api::clef_data_test; + const auto xml = mxtest::toXml(makeScoreWithClefLocation(ClefLocation::beforeBarline)); + CHECK(xml.find("after-barline=\"no\"") != std::string::npos); + + const auto out = mxtest::fromXml(xml); + CHECK_EQUAL(1, out.parts.front().measures.front().staves.front().clefs.size()); + CHECK(ClefLocation::beforeBarline == firstClef(out).location); +} + +T_END; + +TEST(clefAfterBarlineUnspecifiedIsOmitted, ClefData) +{ + using namespace mxtest::api::clef_data_test; + const auto xml = mxtest::toXml(makeScoreWithClefLocation(ClefLocation::unspecified)); + CHECK(xml.find("after-barline=") == std::string::npos); + + const auto out = mxtest::fromXml(xml); + CHECK_EQUAL(1, out.parts.front().measures.front().staves.front().clefs.size()); + CHECK(ClefLocation::unspecified == firstClef(out).location); +} + +T_END; + +TEST(clefAllFieldsSurviveWrite, ClefData) +{ + using namespace mxtest::api::clef_data_test; + ScoreData score = makeScoreWithClef(Bool::unspecified); + auto &clef = score.parts.front().measures.front().staves.front().clefs.front(); + clef.writeStaffNumber = Bool::yes; + clef.symbol = ClefSymbol::f; + clef.line = 4; + clef.isLineSpecified = true; + clef.octaveChange = -1; + clef.isOctaveChangeSpecified = true; + clef.tickTimePosition = 0; + clef.location = ClefLocation::afterBarline; + clef.additional = Bool::yes; + clef.printObject = Bool::no; + const ClefData expected = clef; + + const auto out = mxtest::fromXml(mxtest::toXml(score)); + const auto &actual = firstClef(out); + CHECK(expected.writeStaffNumber == actual.writeStaffNumber); + CHECK(expected.symbol == actual.symbol); + CHECK_EQUAL(expected.line, actual.line); + CHECK_EQUAL(expected.isLineSpecified, actual.isLineSpecified); + CHECK_EQUAL(expected.octaveChange, actual.octaveChange); + CHECK_EQUAL(expected.isOctaveChangeSpecified, actual.isOctaveChangeSpecified); + CHECK_EQUAL(expected.tickTimePosition, actual.tickTimePosition); + CHECK(expected.location == actual.location); + CHECK(expected.additional == actual.additional); + CHECK(expected.printObject == actual.printObject); + CHECK(expected == actual); +} + +T_END; + +TEST(clefUnspecifiedElementsSurviveWrite, ClefData) +{ + using namespace mxtest::api::clef_data_test; + ScoreData score = makeScoreWithClef(Bool::unspecified); + auto &clef = score.parts.front().measures.front().staves.front().clefs.front(); + clef.symbol = ClefSymbol::c; + clef.isLineSpecified = false; + clef.line = 3; + clef.isOctaveChangeSpecified = false; + clef.octaveChange = 0; + clef.writeStaffNumber = Bool::no; + const ClefData expected = clef; + + const auto xml = mxtest::toXml(score); + CHECK(xml.find("") == std::string::npos); + CHECK(xml.find("") == std::string::npos); + CHECK(xml.find("") != std::string::npos); + + const auto out = mxtest::fromXml(xml); + const auto &actual = firstClef(out); + CHECK(expected.symbol == actual.symbol); + CHECK_EQUAL(expected.line, actual.line); + CHECK_EQUAL(expected.isLineSpecified, actual.isLineSpecified); + CHECK_EQUAL(expected.octaveChange, actual.octaveChange); + CHECK_EQUAL(expected.isOctaveChangeSpecified, actual.isOctaveChangeSpecified); + + // writeStaffNumber records only a divergence from the automatic rule. On a single-staff part + // the automatic rule already omits the number, so no agrees with it and reads back as + // unspecified. The emitted XML is the same either way. + CHECK(Bool::unspecified == actual.writeStaffNumber); +} + +T_END; + #endif