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
11 changes: 11 additions & 0 deletions src/include/mx/api/StaffData.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ class StaffData
{
public:
int staffLines = -1;

// Specifies the staff space size relative the the global staff space size
double staffSize = -1.0;

// Specifies the scaling of the notation. The MusicXml spec calls out the case
// of percussion staves with wider spaced lines as an example where this differs
// from staffSize. For example it might be staffSize=150, staffScaling = 100.
double staffScaling = -1.0;

std::vector<ClefData> clefs;

// for the use case where key signatures
Expand Down Expand Up @@ -55,6 +64,8 @@ inline bool voicesAreEqual(const std::map<int, VoiceData> &l, const std::map<int

MXAPI_EQUALS_BEGIN(StaffData)
MXAPI_EQUALS_MEMBER(staffLines)
MXAPI_DOUBLES_EQUALS_MEMBER(staffSize)
MXAPI_DOUBLES_EQUALS_MEMBER(staffScaling)
MXAPI_EQUALS_MEMBER(clefs)
MXAPI_EQUALS_MEMBER(keys)
MXAPI_EQUALS_MEMBER(directions)
Expand Down
16 changes: 14 additions & 2 deletions src/private/mx/impl/MeasureReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ void MeasureReader::importStaffDetails(const core::Attributes &inMxAttributes) c
{
for (const auto &staffDetails : inMxAttributes.staffDetails())
{
if (!staffDetails.group().has_value())
if (!staffDetails.group().has_value() && !staffDetails.staffSize().has_value())
{
continue;
}
Expand All @@ -753,7 +753,19 @@ void MeasureReader::importStaffDetails(const core::Attributes &inMxAttributes) c
continue;
}

myOutMeasureData.staves.at(static_cast<size_t>(staffIndex)).staffLines = staffDetails.group()->staffLines();
auto &staffData = myOutMeasureData.staves.at(static_cast<size_t>(staffIndex));
if (staffDetails.group().has_value())
{
staffData.staffLines = staffDetails.group()->staffLines();
}
if (staffDetails.staffSize().has_value())
{
staffData.staffSize = staffDetails.staffSize()->value().value().value();
if (staffDetails.staffSize()->scaling().has_value())
{
staffData.staffScaling = staffDetails.staffSize()->scaling()->value().value();
}
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/private/mx/impl/MeasureWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,15 @@ void MeasureWriter::writeMeasureGlobals()

for (const auto &staff : myMeasureData.staves)
{
if (staff.staffLines >= 0)
if (staff.staffLines >= 0 || staff.staffSize >= 0.0)
{
int desiredStaffIndex = -1;
if (myHistory.getCursor().getNumStaves() > 1)
{
desiredStaffIndex = localStaffCounter;
}
myPropertiesWriter->writeStaffDetails(desiredStaffIndex, staff.staffLines);
myPropertiesWriter->writeStaffDetails(desiredStaffIndex, staff.staffLines, staff.staffSize,
staff.staffScaling);
}

auto clefIter = staff.clefs.cbegin();
Expand Down
30 changes: 27 additions & 3 deletions src/private/mx/impl/PropertiesWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ void PropertiesWriter::writeNumStaves(int value)
}

void PropertiesWriter::writeStaffDetails(int staffIndex, int staffLines)
{
writeStaffDetails(staffIndex, staffLines, -1.0, -1.0);
}

void PropertiesWriter::writeStaffDetails(int staffIndex, int staffLines, double staffSize)
{
writeStaffDetails(staffIndex, staffLines, staffSize, -1.0);
}

void PropertiesWriter::writeStaffDetails(int staffIndex, int staffLines, double staffSize, double staffScaling)
{
core::StaffDetails staffDetails{};

Expand All @@ -190,9 +200,23 @@ void PropertiesWriter::writeStaffDetails(int staffIndex, int staffLines)
staffDetails.setNumber(core::StaffNumber{staffIndex + 1});
}

core::StaffDetailsGroup sdg{};
sdg.setStaffLines(staffLines);
staffDetails.setGroup(sdg);
if (staffLines >= 0)
{
core::StaffDetailsGroup sdg{};
sdg.setStaffLines(staffLines);
staffDetails.setGroup(sdg);
}

if (staffSize >= 0.0)

@webern webern Jun 30, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is >= correct (and not >)? Is zero valid and would we want to write it?

@rpatters1 rpatters1 Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I'm going to assume 0 is not valid and make changes accordingly. My question is whether we should use 0.0 as the sentinel rather than a negative value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLM says:

In MusicXML, is a non-negative-decimal, so 0 is schema-valid. Semantically it is odd: the element is a percentage of default staff scaling, so normal values are around 100, smaller staves might be something like 75, and 0 would mean a zero-height staff space. But because the schema allows non-negative values, the API sentinel should stay negative, and staffSize >= 0.0 is the right “specified” check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could apply a stricter policy if you wish. It wouldn't bother me.

{
core::StaffSize size{};
size.setValue(core::NonNegativeDecimal{core::Decimal{staffSize}});
if (staffScaling >= 0.0)
{
size.setScaling(core::NonNegativeDecimal{core::Decimal{staffScaling}});
}
staffDetails.setStaffSize(size);
}

myAttributes.addStaffDetails(staffDetails);
myHasContent = true;
Expand Down
2 changes: 2 additions & 0 deletions src/private/mx/impl/PropertiesWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class PropertiesWriter
void writeTime(const api::TimeSignatureData &value);
void writeNumStaves(int value);
void writeStaffDetails(int staffIndex, int staffLines);
void writeStaffDetails(int staffIndex, int staffLines, double staffSize);
void writeStaffDetails(int staffIndex, int staffLines, double staffSize, double staffScaling);
void writeClef(int staffIndex, const api::ClefData &inClefData);
void writePartSymbol(const api::PartSymbolData &inPartSymbolData);
void writeTranspose(const api::TransposeData &inTransposeData);
Expand Down
86 changes: 86 additions & 0 deletions src/private/mxtest/api/MeasureDataTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,90 @@ TEST(staffLinesRoundTrip, MeasureData)

T_END;

TEST(staffSizeRoundTrip, MeasureData)
{
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();
staff.staffSize = 80.5;
staff.voices[0].notes.emplace_back();

const auto xml = mxtest::toXml(score);
CHECK(xml.find("<staff-details>") != std::string::npos);
CHECK(xml.find("<staff-size>80.5</staff-size>") != std::string::npos);
CHECK(xml.find("<staff-lines>") == std::string::npos);

const auto outScore = mxtest::fromXml(xml);
CHECK_EQUAL(1, outScore.parts.size());
CHECK_EQUAL(1, outScore.parts.front().measures.size());
CHECK_EQUAL(1, outScore.parts.front().measures.front().staves.size());
CHECK(80.5 == outScore.parts.front().measures.front().staves.front().staffSize);
CHECK(-1.0 == outScore.parts.front().measures.front().staves.front().staffScaling);
CHECK_EQUAL(-1, outScore.parts.front().measures.front().staves.front().staffLines);
}

T_END;

TEST(staffSizeScalingRoundTrip, MeasureData)
{
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();
staff.staffSize = 80.5;
staff.staffScaling = 75.5;
staff.voices[0].notes.emplace_back();

const auto xml = mxtest::toXml(score);
CHECK(xml.find("<staff-details>") != std::string::npos);
CHECK(xml.find("<staff-size scaling=\"75.5\">80.5</staff-size>") != std::string::npos);
CHECK(xml.find("<staff-lines>") == std::string::npos);

const auto outScore = mxtest::fromXml(xml);
CHECK_EQUAL(1, outScore.parts.size());
CHECK_EQUAL(1, outScore.parts.front().measures.size());
CHECK_EQUAL(1, outScore.parts.front().measures.front().staves.size());
CHECK(80.5 == outScore.parts.front().measures.front().staves.front().staffSize);
CHECK(75.5 == outScore.parts.front().measures.front().staves.front().staffScaling);
CHECK_EQUAL(-1, outScore.parts.front().measures.front().staves.front().staffLines);
}

T_END;

TEST(staffLinesAndStaffSizeRoundTrip, MeasureData)
{
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();
staff.staffLines = 1;
staff.staffSize = 80.5;
staff.voices[0].notes.emplace_back();

const auto xml = mxtest::toXml(score);
CHECK(xml.find("<staff-details>") != std::string::npos);
CHECK(xml.find("<staff-lines>1</staff-lines>") != std::string::npos);
CHECK(xml.find("<staff-size>80.5</staff-size>") != std::string::npos);

const auto outScore = mxtest::fromXml(xml);
CHECK_EQUAL(1, outScore.parts.size());
CHECK_EQUAL(1, outScore.parts.front().measures.size());
CHECK_EQUAL(1, outScore.parts.front().measures.front().staves.size());
CHECK_EQUAL(1, outScore.parts.front().measures.front().staves.front().staffLines);
CHECK(80.5 == outScore.parts.front().measures.front().staves.front().staffSize);
CHECK(-1.0 == outScore.parts.front().measures.front().staves.front().staffScaling);
}

T_END;

#endif
100 changes: 100 additions & 0 deletions src/private/mxtest/impl/MeasureWriterTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,104 @@ TEST(staffDetailsWritesStaffLines, MeasureWriter)

T_END

TEST(staffDetailsWritesStaffSize, MeasureWriter)
{
mxtest::TestParameters params;
params.ticksPerQuarter = 101;
params.measureIndex = 0;
params.partIndex = 0;
params.numStaves = 1;
mxtest::TestItems t = mxtest::setupTestItems(params);
auto &staff = t.measureData->staves.at(0);
staff.staffSize = 80.5;

const auto partwiseMeasure = t.measureWriter->getPartwiseMeasure();
auto musicData = partwiseMeasure.musicData();
auto mdcIter = musicData.begin();
const auto mdcEnd = musicData.end();

CHECK(mdcIter != mdcEnd);
CHECK(mdcIter->isAttributes());

const auto &props = mdcIter->asAttributes();
CHECK_EQUAL(1, props.staffDetails().size());

const auto &details = props.staffDetails().front();
CHECK(!details.group().has_value());
CHECK(details.staffSize().has_value());
CHECK(80.5 == details.staffSize()->value().value().value());
CHECK(!details.staffSize()->scaling().has_value());
CHECK(!details.number().has_value());
}

T_END

TEST(staffDetailsWritesStaffSizeScaling, MeasureWriter)
{
mxtest::TestParameters params;
params.ticksPerQuarter = 101;
params.measureIndex = 0;
params.partIndex = 0;
params.numStaves = 1;
mxtest::TestItems t = mxtest::setupTestItems(params);
auto &staff = t.measureData->staves.at(0);
staff.staffSize = 80.5;
staff.staffScaling = 75.5;

const auto partwiseMeasure = t.measureWriter->getPartwiseMeasure();
auto musicData = partwiseMeasure.musicData();
auto mdcIter = musicData.begin();
const auto mdcEnd = musicData.end();

CHECK(mdcIter != mdcEnd);
CHECK(mdcIter->isAttributes());

const auto &props = mdcIter->asAttributes();
CHECK_EQUAL(1, props.staffDetails().size());

const auto &details = props.staffDetails().front();
CHECK(!details.group().has_value());
CHECK(details.staffSize().has_value());
CHECK(80.5 == details.staffSize()->value().value().value());
CHECK(details.staffSize()->scaling().has_value());
CHECK(75.5 == details.staffSize()->scaling()->value().value());
CHECK(!details.number().has_value());
}

T_END

TEST(staffDetailsWritesStaffLinesAndStaffSize, MeasureWriter)
{
mxtest::TestParameters params;
params.ticksPerQuarter = 101;
params.measureIndex = 0;
params.partIndex = 0;
params.numStaves = 1;
mxtest::TestItems t = mxtest::setupTestItems(params);
auto &staff = t.measureData->staves.at(0);
staff.staffLines = 1;
staff.staffSize = 80.5;

const auto partwiseMeasure = t.measureWriter->getPartwiseMeasure();
auto musicData = partwiseMeasure.musicData();
auto mdcIter = musicData.begin();
const auto mdcEnd = musicData.end();

CHECK(mdcIter != mdcEnd);
CHECK(mdcIter->isAttributes());

const auto &props = mdcIter->asAttributes();
CHECK_EQUAL(1, props.staffDetails().size());

const auto &details = props.staffDetails().front();
CHECK(details.group().has_value());
CHECK_EQUAL(1, details.group()->staffLines());
CHECK(details.staffSize().has_value());
CHECK(80.5 == details.staffSize()->value().value().value());
CHECK(!details.staffSize()->scaling().has_value());
CHECK(!details.number().has_value());
}

T_END

#endif
Loading