Skip to content
Merged
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
6 changes: 6 additions & 0 deletions src/include/mx/api/ScoreData.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ class ScoreData
DefaultsData defaults;
std::vector<PartData> parts;
std::vector<PartGroupData> partGroups;

/// The score's time resolution: how many `tickTimePosition`/duration ticks make up one
/// quarter note. Corresponds to the `<divisions>` MusicXML element. Choose a value with
/// enough factors for the durations you need (the default accommodates tuplets through
/// septuplets). Zero means the score has no time resolution at all — no durations exist
/// anywhere — and no `<divisions>` is written.
int ticksPerQuarter;

/// Specifies page breaks, system breaks, and changes to system and page layout. Global/default page and
Expand Down
8 changes: 7 additions & 1 deletion src/private/mx/impl/MeasureWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,13 @@ void MeasureWriter::writeMeasureGlobals()

if (myHistory.getCursor().isFirstMeasureInPart)
{
myPropertiesWriter->writeDivisions(myHistory.getCursor().getGlobalTicksPerQuarter());
// A score with no time resolution (ticksPerQuarter <= 0: the source declared no
// <divisions> and contains no durations) must not have one invented for it; writing
// 0 here would be clamped to a bogus positive value by core::PositiveDivisions.
if (myHistory.getCursor().getGlobalTicksPerQuarter() > 0)
{
myPropertiesWriter->writeDivisions(myHistory.getCursor().getGlobalTicksPerQuarter());
}

if (myMeasureData.staves.size() > 1)
{
Expand Down
48 changes: 48 additions & 0 deletions src/private/mxtest/api/roundtrip-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,51 @@ rpatters1/timesigs_composite-ref.musicxml
# the serialized stream once voice 1 is written ahead of voice 2, plus a wedge.
# Pins explicit slur/wedge numbers surviving read -> write -> read verbatim.
rpatters1/slurs_overbars.musicxml

# Unblocked by the spurious-divisions fix: a score whose source declares no
# <divisions> anywhere (and has no durations) reads as ticksPerQuarter = 0,
# and the writer used to invent <attributes><divisions>0.000001</divisions>
# for the first measure of every part (LCM of an empty set = 0, clamped to a
# bogus positive by core::PositiveDivisions). The writer now omits <divisions>
# when the score has no time resolution.
lysuite/ly51b_Header_Quotes.xml
mjbsuite/HasMusicXmlVersionFalse.xml
mjbsuite/HasMusicXmlVersionTrue.xml
synthetic/beat-type.3.0.xml
synthetic/beats.3.0.xml
synthetic/bracket.3.0.xml
synthetic/dashes.3.0.xml
synthetic/encoding-description.3.0.xml
synthetic/first.4.0.xml
synthetic/group.3.0.xml
synthetic/instrument-abbreviation.3.0.xml
synthetic/instrument-name.3.0.xml
synthetic/instrument-sound-enum.3.0.xml
synthetic/interchangeable.3.0.xml
synthetic/lyric-font.3.0.xml
synthetic/miscellaneous-field.3.0.xml
synthetic/movement-number.3.0.xml
synthetic/movement-title.3.0.xml
synthetic/music-font.3.0.xml
synthetic/n.3.1.xml
synthetic/numeral-fifths.4.0.xml
synthetic/numeral-mode.4.0.xml
synthetic/numeral.4.0.xml
synthetic/other-dynamics.3.0.xml
synthetic/pf.3.1.xml
synthetic/second.4.0.xml
synthetic/senza-misura.3.0.xml
synthetic/sfzp.3.1.xml
synthetic/software.3.0.xml
synthetic/staff-size.4.0.xml
synthetic/straight.4.0.xml
synthetic/swing-style.4.0.xml
synthetic/swing-type.4.0.xml
synthetic/swing.4.0.xml
synthetic/time-relation.3.0.xml
synthetic/virtual-library.3.0.xml
synthetic/virtual-name.3.0.xml
synthetic/voice.3.0.xml
synthetic/word-font.3.0.xml
synthetic/work-number.3.0.xml
synthetic/work-title.3.0.xml
49 changes: 49 additions & 0 deletions src/private/mxtest/impl/MeasureWriterTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,55 @@ TEST(PropertiesButNoNotes, MeasureWriter)

T_END

TEST(ZeroTicksPerQuarterWritesNoDivisions, MeasureWriter)
{
// A score with no time resolution (ticksPerQuarter == 0, e.g. a source that declared no
// <divisions> and has no durations) must not have a bogus <divisions> invented for it.
mxtest::TestParameters params;
params.ticksPerQuarter = 0;
params.measureIndex = 0;
params.partIndex = 0;
params.numStaves = 1;
mxtest::TestItems t = mxtest::setupTestItems(params);

const auto partwiseMeasure = t.measureWriter->getPartwiseMeasure();
CHECK(partwiseMeasure.musicData().empty());
}

T_END

TEST(ZeroTicksPerQuarterKeepsOtherProperties, MeasureWriter)
{
// With ticksPerQuarter == 0 the <attributes> element is still written when it has real
// content (here a clef); only the <divisions> child is suppressed.
mxtest::TestParameters params;
params.ticksPerQuarter = 0;
params.measureIndex = 0;
params.partIndex = 0;
params.numStaves = 1;
mxtest::TestItems t = mxtest::setupTestItems(params);
auto &staff = t.measureData->staves.at(0);
staff.clefs.emplace_back(api::ClefData{});
auto &clef = staff.clefs.back();
clef.symbol = api::ClefSymbol::g;
clef.line = 2;

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

CHECK(mdcIter != mdcEnd);
CHECK(mdcIter->isAttributes());
const auto &props = mdcIter->asAttributes();
CHECK(!props.divisions().has_value());
CHECK_EQUAL(1, props.clef().size());

CHECK(++mdcIter == mdcEnd);
}

T_END

TEST(MultiStaffClefKeepsNumber, MeasureWriter)
{
// Counterpart to PropertiesButNoNotes: in a multi-staff part the clef number is required
Expand Down
Loading