diff --git a/src/include/mx/api/ScoreData.h b/src/include/mx/api/ScoreData.h index 81852c679..fab4c4f77 100644 --- a/src/include/mx/api/ScoreData.h +++ b/src/include/mx/api/ScoreData.h @@ -66,6 +66,11 @@ class ScoreData // The type attribute for `copyright` (above). std::optional copyrightType; + // Where the music came from: the edition, manuscript, or other publication this score was + // made from. This is MusicXML's element. It names the origin of the music, not of + // the file. Leave it empty when the score does not name one. + std::optional source; + EncodingData encoding; std::vector pageTextItems; @@ -118,6 +123,7 @@ MXAPI_EQUALS_MEMBER(arranger) MXAPI_EQUALS_MEMBER(publisher) MXAPI_EQUALS_MEMBER(copyright) MXAPI_EQUALS_MEMBER(copyrightType) +MXAPI_EQUALS_MEMBER(source) MXAPI_EQUALS_MEMBER(encoding) MXAPI_EQUALS_MEMBER(pageTextItems) MXAPI_EQUALS_MEMBER(pageImageItems) diff --git a/src/private/mx/api/ScoreData.cpp b/src/private/mx/api/ScoreData.cpp index 8790b6cb9..625219eff 100644 --- a/src/private/mx/api/ScoreData.cpp +++ b/src/private/mx/api/ScoreData.cpp @@ -14,7 +14,7 @@ namespace api ScoreData::ScoreData() : musicXmlVersion{api::MusicXmlVersion::unspecified}, declaredMusicXmlVersion{}, musicXmlType{"partwise"}, workTitle{}, workNumber{}, movementTitle{}, movementNumber{}, composer{}, lyricist{}, copyright{}, - copyrightType{"copyright"}, encoding{}, pageTextItems{}, defaults{}, parts{}, partGroups{}, + copyrightType{"copyright"}, source{}, encoding{}, pageTextItems{}, defaults{}, parts{}, partGroups{}, ticksPerQuarter{DEFAULT_TICKS_PER_QUARTER} { } diff --git a/src/private/mx/impl/ScoreReader.cpp b/src/private/mx/impl/ScoreReader.cpp index 7c5d79b9c..cbc10de32 100644 --- a/src/private/mx/impl/ScoreReader.cpp +++ b/src/private/mx/impl/ScoreReader.cpp @@ -234,6 +234,8 @@ api::ScoreData ScoreReader::getScoreData() const myOutScoreData.copyrightType = std::nullopt; } } + myOutScoreData.source = ident.source(); + api::EncodingData encodingData; if (ident.encoding().has_value()) diff --git a/src/private/mx/impl/ScoreWriter.cpp b/src/private/mx/impl/ScoreWriter.cpp index 5186266e9..d179360ad 100644 --- a/src/private/mx/impl/ScoreWriter.cpp +++ b/src/private/mx/impl/ScoreWriter.cpp @@ -144,6 +144,12 @@ core::ScorePartwise ScoreWriter::getScorePartwise() const hasIdentification = true; } + if (myScoreData.source.has_value()) + { + identification.setSource(myScoreData.source); + hasIdentification = true; + } + if (hasIdentification) { header.setIdentification(identification); diff --git a/src/private/mxtest/api/IdentificationSourceApiTest.cpp b/src/private/mxtest/api/IdentificationSourceApiTest.cpp new file mode 100644 index 000000000..1a72b67be --- /dev/null +++ b/src/private/mxtest/api/IdentificationSourceApiTest.cpp @@ -0,0 +1,69 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#include "mxtest/control/CompileControl.h" +#ifdef MX_COMPILE_API_TESTS + +#include "cpul/cpulTestHarness.h" +#include "mx/api/DocumentManager.h" +#include "mxtest/api/RoundTrip.h" +#include "mxtest/api/TestHelpers.h" + +using namespace std; +using namespace mx::api; +using namespace mxtest; + +// An otherwise empty one-part score, so the only thing under test is the header. +ScoreData identificationSourceMakeScore() +{ + ScoreData score; + score.parts.emplace_back(); + score.parts.back().uniqueId = "P1"; + score.parts.back().measures.emplace_back(); + score.parts.back().measures.back().staves.emplace_back(); + return score; +} + +TEST(sourceSurvivesRoundTrip, IdentificationSource) +{ + auto score = identificationSourceMakeScore(); + score.source = "Bach-Gesellschaft Ausgabe, Band 3"; + + const auto out = roundTrip(score); + REQUIRE(out.source.has_value()); + CHECK_EQUAL("Bach-Gesellschaft Ausgabe, Band 3", *out.source); +} + +TEST(sourceWritesTheMusicXmlElement, IdentificationSource) +{ + auto score = identificationSourceMakeScore(); + score.source = "Urtext"; + + const auto xml = toXml(score); + CHECK(xml.find("Urtext") != std::string::npos); +} + +TEST(anEmptySourceWritesNothing, IdentificationSource) +{ + const auto score = identificationSourceMakeScore(); + const auto xml = toXml(score); + CHECK(xml.find("") == std::string::npos); + + const auto out = roundTrip(score); + CHECK(!out.source.has_value()); +} + +// MusicXML allows , which says the score named a source and left it blank. +// That is a different fact from saying nothing at all, so both have to survive the round trip. +TEST(aBlankSourceIsNotTheSameAsNoSource, IdentificationSource) +{ + auto score = identificationSourceMakeScore(); + score.source = ""; + + const auto out = roundTrip(score); + REQUIRE(out.source.has_value()); + CHECK_EQUAL("", *out.source); +} + +#endif diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index 33789b08a..04c6675de 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -686,3 +686,12 @@ foundsuite/Invention_6.xml foundsuite/Invention_7.xml foundsuite/Invention_8.xml foundsuite/Invention_9.xml + +# Unblocked by ScoreData::source: / names the edition or +# manuscript a score was made from, and the api had nowhere to keep it, so the +# element was dropped on write. +musetrainer/Canon_in_D_easy.xml +musetrainer/Carol_of_the_Bells_easy_piano.xml +musetrainer/Lacrimosa_-_Requiem.xml +musetrainer/Prelude_I_in_C_major_BWV_846_-_Well_Tempered_Clavier_First_Book.xml +synthetic/source.3.0.xml