Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/include/mx/api/PartData.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ class PartData
/// the part. Subsequent transposition changes are not currently supported by mx::api.
std::optional<TransposeData> transposition;

// Most users can ignore this; leave it false. MusicXML states a part's staff count in
// <staves>, which it needs only when the part has more than one staff -- a part with no
// <staves> has one. mx writes the element whenever the part has more than one staff, so
// this only controls the redundant <staves>1</staves> 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<MeasureData> measures;

// MusicXML requires <part-name>, but real files often leave it empty and
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion src/private/mx/impl/MeasureWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ void MeasureWriter::writeMeasureGlobals()
myPropertiesWriter->writeDivisions(myHistory.getCursor().getGlobalTicksPerQuarter());
}

if (myMeasureData.staves.size() > 1)
// <staves> is needed only when the part has more than one staff. PartData::writeStaffCount
// adds the redundant <staves>1</staves> 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<int>(myMeasureData.staves.size()));
}
Expand Down
12 changes: 9 additions & 3 deletions src/private/mx/impl/PartReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ void readNameDisplay(const core::PartName &nameElement, const std::optional<core

PartReader::PartReader(const core::ScorePart &inScorePart, const core::PartwisePart &inPartwisePartRef,
int globalTicksPerMeasure, const core::ScorePartwise &inScore, int inDivisionsValue)
: myPartwisePart{inPartwisePartRef}, myScorePart{inScorePart}, myNumStaves{-1},
: myPartwisePart{inPartwisePartRef}, myScorePart{inScorePart}, myNumStaves{-1}, myIsStavesElementPresent{false},
myGlobalTicksPerMeasure{globalTicksPerMeasure}, myScore{inScore}, myPartIndex{-1},
myConstructedDivisionsValue{inDivisionsValue}
{
Expand All @@ -91,7 +91,7 @@ PartReader::PartReader(const core::ScorePart &inScorePart, const core::PartwiseP
const auto partIndex = findPartIndex(ppId);
MX_ASSERT(partIndex >= 0);
myPartIndex = partIndex;
myNumStaves = calculateNumStaves();
myNumStaves = calculateNumStaves(myIsStavesElementPresent);
}

api::PartData PartReader::getPartData()
Expand All @@ -100,6 +100,10 @@ api::PartData PartReader::getPartData()
myOutPartData = api::PartData{};
parseScorePart();

// A single-staff part does not need <staves>, 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;

Expand Down Expand Up @@ -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())
Expand All @@ -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)
{
Expand Down
5 changes: 4 additions & 1 deletion src/private/mx/impl/PartReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <staves> 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;
Expand Down
77 changes: 77 additions & 0 deletions src/private/mxtest/api/StaffCountApiTest.cpp
Original file line number Diff line number Diff line change
@@ -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("<staves>") == 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("<staves>1</staves>") != 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 <staves> 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("<staves>2</staves>") != 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
7 changes: 7 additions & 0 deletions src/private/mxtest/api/roundtrip-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
# <staves>, so mx omitted it, and these three sources spell out the redundant
# <staves>1</staves>.
lysuite/ly02b_Rests_PitchedRests.xml
lysuite/ly32b_Articulations_Texts.xml
lysuite/ly33b_Spanners_Tie.xml
Loading