diff --git a/src/include/mx/api/PartData.h b/src/include/mx/api/PartData.h index eed1e575e..8eb37a053 100644 --- a/src/include/mx/api/PartData.h +++ b/src/include/mx/api/PartData.h @@ -210,6 +210,13 @@ class PartData /// the part. Subsequent transposition changes are not currently supported by mx::api. std::optional transposition; + // Most users can ignore this; leave it false. MusicXML states a part's staff count in + // , which it needs only when the part has more than one staff -- a part with no + // has one. mx writes the element whenever the part has more than one staff, so + // this only controls the redundant 1 on a single-staff part. Reading a + // file sets it when the source spelled that out. Same idea as ClefData::writeStaffNumber. + bool writeStaffCount = false; + std::vector measures; // MusicXML requires , but real files often leave it empty and @@ -312,6 +319,7 @@ MXAPI_EQUALS_MEMBER(displayAbbreviationPositionData) MXAPI_EQUALS_MEMBER(groups) MXAPI_EQUALS_MEMBER(instrumentData) MXAPI_EQUALS_MEMBER(transposition) +MXAPI_EQUALS_MEMBER(writeStaffCount) MXAPI_EQUALS_MEMBER(measures) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(PartData); diff --git a/src/private/mx/impl/MeasureWriter.cpp b/src/private/mx/impl/MeasureWriter.cpp index 3df64a875..782d0f83c 100644 --- a/src/private/mx/impl/MeasureWriter.cpp +++ b/src/private/mx/impl/MeasureWriter.cpp @@ -136,7 +136,10 @@ void MeasureWriter::writeMeasureGlobals() myPropertiesWriter->writeDivisions(myHistory.getCursor().getGlobalTicksPerQuarter()); } - if (myMeasureData.staves.size() > 1) + // is needed only when the part has more than one staff. PartData::writeStaffCount + // adds the redundant 1 back for a source that spelled it out. + const bool isStaffCountRequested = myScoreWriter.getPart(myHistory.getCursor().partIndex).writeStaffCount; + if (myMeasureData.staves.size() > 1 || isStaffCountRequested) { myPropertiesWriter->writeNumStaves(static_cast(myMeasureData.staves.size())); } diff --git a/src/private/mx/impl/PartReader.cpp b/src/private/mx/impl/PartReader.cpp index 748dc74b0..b406da912 100644 --- a/src/private/mx/impl/PartReader.cpp +++ b/src/private/mx/impl/PartReader.cpp @@ -78,7 +78,7 @@ void readNameDisplay(const core::PartName &nameElement, const std::optional= 0); myPartIndex = partIndex; - myNumStaves = calculateNumStaves(); + myNumStaves = calculateNumStaves(myIsStavesElementPresent); } api::PartData PartReader::getPartData() @@ -100,6 +100,10 @@ api::PartData PartReader::getPartData() myOutPartData = api::PartData{}; parseScorePart(); + // A single-staff part does not need , so mx omits it there. Record an override only + // when the source spelled the redundant element out. + myOutPartData.writeStaffCount = myIsStavesElementPresent && myNumStaves == 1; + myCurrentCursor = MeasureCursor{myNumStaves, myGlobalTicksPerMeasure}; myCurrentCursor.partIndex = myPartIndex; @@ -142,8 +146,9 @@ MeasureCursor PartReader::getCursor() const return myCurrentCursor; } -int PartReader::calculateNumStaves() const +int PartReader::calculateNumStaves(bool &outIsStavesElementPresent) const { + outIsStavesElementPresent = false; int numStaves = 1; for (const auto &measure : myPartwisePart.measure()) @@ -168,6 +173,7 @@ int PartReader::calculateNumStaves() const const auto &attributes = mdc.asAttributes(); if (attributes.staves().has_value()) { + outIsStavesElementPresent = true; int temp = *attributes.staves(); if (temp > numStaves) { diff --git a/src/private/mx/impl/PartReader.h b/src/private/mx/impl/PartReader.h index c0cec2d8a..2081f34a5 100644 --- a/src/private/mx/impl/PartReader.h +++ b/src/private/mx/impl/PartReader.h @@ -38,6 +38,7 @@ class PartReader const core::PartwisePart &myPartwisePart; const core::ScorePart &myScorePart; int myNumStaves; + bool myIsStavesElementPresent; const int myGlobalTicksPerMeasure; const core::ScorePartwise &myScore; int myPartIndex; @@ -49,7 +50,9 @@ class PartReader mutable std::mutex myMutex; mutable api::PartData myOutPartData; - int calculateNumStaves() const; + // Returns the part's staff count. outIsStavesElementPresent reports whether the source said + // so with a element, which the count alone cannot tell you when it is one. + int calculateNumStaves(bool &outIsStavesElementPresent) const; void parseScorePart() const; void parseScoreInstrument(const core::ScoreInstrument &scoreInstrument) const; void parseVirtualInstrument(const core::VirtualInstrument &virtualInstrument) const; diff --git a/src/private/mxtest/api/StaffCountApiTest.cpp b/src/private/mxtest/api/StaffCountApiTest.cpp new file mode 100644 index 000000000..e2af19c00 --- /dev/null +++ b/src/private/mxtest/api/StaffCountApiTest.cpp @@ -0,0 +1,77 @@ +// 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; + +// A one-measure score whose single part has the requested number of staves, each holding one +// quarter note so the staves are real. +ScoreData staffCountMakeScore(int numStaves) +{ + ScoreData score; + score.ticksPerQuarter = 4; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.uniqueId = "P1"; + part.measures.emplace_back(); + auto &measure = part.measures.back(); + + for (int i = 0; i < numStaves; ++i) + { + measure.staves.emplace_back(); + auto &voice = measure.staves.back().voices[0]; + voice.notes.emplace_back(); + voice.notes.back().durationData.durationName = DurationName::quarter; + voice.notes.back().durationData.durationTimeTicks = 4; + } + + return score; +} + +TEST(aSingleStaffPartOmitsTheStaffCount, StaffCount) +{ + const auto score = staffCountMakeScore(1); + const auto xml = toXml(score); + CHECK(xml.find("") == std::string::npos); + + const auto out = roundTrip(score); + CHECK(!out.parts.at(0).writeStaffCount); +} + +TEST(aSingleStaffPartCanAskForTheStaffCount, StaffCount) +{ + auto score = staffCountMakeScore(1); + score.parts.at(0).writeStaffCount = true; + + const auto xml = toXml(score); + CHECK(xml.find("1") != std::string::npos); + + const auto out = roundTrip(score); + CHECK(out.parts.at(0).writeStaffCount); + REQUIRE(out.parts.at(0).measures.at(0).staves.size() == 1); +} + +// A part with more than one staff needs no matter what the field says, since without it +// every note would land on staff one. +TEST(aMultiStaffPartAlwaysWritesTheStaffCount, StaffCount) +{ + const auto score = staffCountMakeScore(2); + const auto xml = toXml(score); + CHECK(xml.find("2") != std::string::npos); + + const auto out = roundTrip(score); + CHECK(!out.parts.at(0).writeStaffCount); + REQUIRE(out.parts.at(0).measures.at(0).staves.size() == 2); +} + +#endif diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index 36787ec58..839341b06 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -702,3 +702,10 @@ synthetic/source.3.0.xml recsuite/Chant.xml synthetic/direction.3.1.xml synthetic/direction.4.0.xml + +# Unblocked by PartData::writeStaffCount: a single-staff part does not need +# , so mx omitted it, and these three sources spell out the redundant +# 1. +lysuite/ly02b_Rests_PitchedRests.xml +lysuite/ly32b_Articulations_Texts.xml +lysuite/ly33b_Spanners_Tie.xml