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
10 changes: 8 additions & 2 deletions src/private/mx/impl/TupletReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,15 @@ void TupletReader::guessNormalFromNote(api::TupletStart &outTupletStart)
outTupletStart.normalDurationName = converter.convert(grp.normalType());
outTupletStart.normalDots = static_cast<int>(grp.normalDot().size());
}
else if (myNote.type().has_value())
else
{
outTupletStart.normalDurationName = converter.convert(myNote.type()->value());
// An absent normal-type means the normal figure is the note's own written figure,
// its dots included (see the time-modification type in the MusicXML schema).
if (myNote.type().has_value())
{
outTupletStart.normalDurationName = converter.convert(myNote.type()->value());
}
outTupletStart.normalDots = static_cast<int>(myNote.dot().size());
}
}

Expand Down
62 changes: 62 additions & 0 deletions src/private/mxtest/api/NoteDataTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1807,4 +1807,66 @@ TEST(implicitStaffOnSingleStaffPartOmitsElement, NoteData)

T_END;

// issue #452: a <tuplet> on a dotted note whose <time-modification> does not restate the
// normal figure guesses that figure from the note, dots included, so the tuplet written back
// carries a tuplet-dot.
TEST(guessedTupletNormalKeepsTheNoteDots, NoteData)
{
const std::string xml = R"(<score-partwise version="3.0">
<part-list>
<score-part id="P1">
<part-name>MusicXML Part</part-name>
</score-part>
</part-list>
<part id="P1">
<measure number="1">
<attributes>
<divisions>2</divisions>
</attributes>
<note>
<pitch>
<step>C</step>
<octave>4</octave>
</pitch>
<duration>1</duration>
<voice>1</voice>
<type>quarter</type>
<dot/>
<time-modification>
<actual-notes>3</actual-notes>
<normal-notes>2</normal-notes>
</time-modification>
<notations>
<tuplet type="start" number="1"/>
</notations>
</note>
</measure>
</part>
</score-partwise>
)";

const auto score = mxtest::fromXml(xml);
const auto &starts =
score.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0).noteAttachmentData.tupletStarts;
REQUIRE(1 == starts.size());
CHECK(DurationName::quarter == starts.at(0).normalDurationName);
CHECK_EQUAL(1, starts.at(0).normalDots);

const auto written = mxtest::toXml(score);
const auto normalPos = written.find("<tuplet-normal>");
REQUIRE(normalPos != std::string::npos);
const auto normalEnd = written.find("</tuplet-normal>", normalPos);
REQUIRE(normalEnd != std::string::npos);
CHECK(written.substr(normalPos, normalEnd - normalPos).find("<tuplet-dot") != std::string::npos);

// reading the written document back reports the same dotted normal figure
const auto reread = mxtest::fromXml(written);
const auto &rereread =
reread.parts.at(0).measures.at(0).staves.at(0).voices.at(0).notes.at(0).noteAttachmentData.tupletStarts;
REQUIRE(1 == rereread.size());
CHECK_EQUAL(1, rereread.at(0).normalDots);
}

T_END;

#endif
138 changes: 138 additions & 0 deletions src/private/mxtest/impl/TupletReaderTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include "cpul/cpulTestHarness.h"
#include "mx/api/TupletData.h"
#include "mx/core/generated/Empty.h"
#include "mx/core/generated/EmptyPlacement.h"
#include "mx/core/generated/Note.h"
#include "mx/core/generated/NoteType.h"
#include "mx/core/generated/NoteTypeValue.h"
Expand Down Expand Up @@ -163,4 +165,140 @@ TEST(guessesActualDurationFromNoteTypeNotFromNormalType, TupletReader)

T_END

// issue #452: an absent normal-type means the normal figure is the note's own written
// figure, so its dots belong to the guessed normal duration.
TEST(guessesNormalDotsFromNoteDots, TupletReader)
{
core::Tuplet tuplet;
tuplet.setType(core::StartStop::start());

core::TimeModification timeMod;
timeMod.setActualNotes(3);
timeMod.setNormalNotes(2);

core::NoteType noteType;
noteType.setValue(core::NoteTypeValue::quarter());

core::Note note;
note.setType(noteType);
note.setTimeModification(timeMod);
note.addDot(core::EmptyPlacement{});

Cursor cursor{1, 480};
TupletReader reader{tuplet, cursor, note};

std::vector<api::TupletStart> starts;
std::vector<api::TupletStop> stops;
reader.parseTuplet(starts, stops);

REQUIRE(starts.size() == 1);
CHECK(api::DurationName::quarter == starts.front().normalDurationName);
CHECK_EQUAL(1, starts.front().normalDots);
CHECK_EQUAL(1, starts.front().actualDots);
}

T_END

// The count matters rather than the presence of one dot: a double-dotted note has two.
TEST(guessesTwoNormalDotsFromTwoNoteDots, TupletReader)
{
core::Tuplet tuplet;
tuplet.setType(core::StartStop::start());

core::TimeModification timeMod;
timeMod.setActualNotes(3);
timeMod.setNormalNotes(2);

core::NoteType noteType;
noteType.setValue(core::NoteTypeValue::quarter());

core::Note note;
note.setType(noteType);
note.setTimeModification(timeMod);
note.addDot(core::EmptyPlacement{});
note.addDot(core::EmptyPlacement{});

Cursor cursor{1, 480};
TupletReader reader{tuplet, cursor, note};

std::vector<api::TupletStart> starts;
std::vector<api::TupletStop> stops;
reader.parseTuplet(starts, stops);

REQUIRE(starts.size() == 1);
CHECK_EQUAL(2, starts.front().normalDots);
}

T_END

// An explicit normal-type/normal-dot still wins: the note's own dots must not overwrite
// the normal side that the file spelled out.
TEST(explicitNormalDotsWinOverNoteDots, TupletReader)
{
core::Tuplet tuplet;
tuplet.setType(core::StartStop::start());

core::TimeModificationGroup group;
group.setNormalType(core::NoteTypeValue::half());
group.addNormalDot(core::Empty{});

core::TimeModification timeMod;
timeMod.setActualNotes(3);
timeMod.setNormalNotes(2);
timeMod.setGroup(group);

core::NoteType noteType;
noteType.setValue(core::NoteTypeValue::quarter());

core::Note note;
note.setType(noteType);
note.setTimeModification(timeMod);
note.addDot(core::EmptyPlacement{});
note.addDot(core::EmptyPlacement{});

Cursor cursor{1, 480};
TupletReader reader{tuplet, cursor, note};

std::vector<api::TupletStart> starts;
std::vector<api::TupletStop> stops;
reader.parseTuplet(starts, stops);

REQUIRE(starts.size() == 1);
CHECK(api::DurationName::half == starts.front().normalDurationName);
CHECK_EQUAL(1, starts.front().normalDots);
}

T_END

// A note with no dots guesses zero dots rather than the unspecified sentinel, so that a
// second round trip reads the same value back.
TEST(guessesZeroNormalDotsWhenNoteHasNoDots, TupletReader)
{
core::Tuplet tuplet;
tuplet.setType(core::StartStop::start());

core::TimeModification timeMod;
timeMod.setActualNotes(3);
timeMod.setNormalNotes(2);

core::NoteType noteType;
noteType.setValue(core::NoteTypeValue::quarter());

core::Note note;
note.setType(noteType);
note.setTimeModification(timeMod);

Cursor cursor{1, 480};
TupletReader reader{tuplet, cursor, note};

std::vector<api::TupletStart> starts;
std::vector<api::TupletStop> stops;
reader.parseTuplet(starts, stops);

REQUIRE(starts.size() == 1);
CHECK_EQUAL(0, starts.front().normalDots);
}

T_END

#endif
Loading