diff --git a/src/private/mx/impl/MeasureWriter.cpp b/src/private/mx/impl/MeasureWriter.cpp index d050e6133..94bbb6d8e 100644 --- a/src/private/mx/impl/MeasureWriter.cpp +++ b/src/private/mx/impl/MeasureWriter.cpp @@ -497,6 +497,7 @@ void MeasureWriter::writeVoices(const api::StaffData &inStaff) bool isFirstNote = true; int currentChordTickPosition = -1; int previousChordTickPosition = -2; + bool previousChordNoteWasGrace = false; myHistory.setVoiceIndex(voice.first); auto noteIter = voice.second.notes.cbegin(); auto noteEnd = voice.second.notes.cend(); @@ -526,8 +527,16 @@ void MeasureWriter::writeVoices(const api::StaffData &inStaff) { isStartOfChord = true; } + else if (noteIter->isGrace != previousChordNoteWasGrace) + { + // Grace notes occupy no ticks, so a grace-note chord and the chord it + // ornaments share a tick position; the grace/non-grace transition is the + // chord boundary. + isStartOfChord = true; + } previousChordTickPosition = currentChordTickPosition; + previousChordNoteWasGrace = noteIter->isGrace; } if (myMeasureKeysIter != myMeasureKeysEnd) diff --git a/src/private/mxtest/api/roundtrip-baseline.txt b/src/private/mxtest/api/roundtrip-baseline.txt index 6626069b0..f0c4d98c4 100644 --- a/src/private/mxtest/api/roundtrip-baseline.txt +++ b/src/private/mxtest/api/roundtrip-baseline.txt @@ -419,3 +419,9 @@ musuite/testNoteAttributes2_ref.xml # attribute now round-trips. lysuite/ly31c_MetronomeMarks.xml musuite/testTempo1.xml + +# Unblocked by the grace-chord boundary fix: a grace-note chord and the chord +# it ornaments share a tick position; the grace/non-grace transition now ends +# the chord instead of absorbing the main chord's first note. +lysuite/ly24b_ChordAsGraceNote.xml +musuite/testGrace2.xml diff --git a/src/private/mxtest/impl/MeasureWriterTest.cpp b/src/private/mxtest/impl/MeasureWriterTest.cpp index 4f2bfe947..1628d2bf8 100644 --- a/src/private/mxtest/impl/MeasureWriterTest.cpp +++ b/src/private/mxtest/impl/MeasureWriterTest.cpp @@ -8,12 +8,20 @@ #include "cpul/cpulTestHarness.h" #include "mx/core/generated/Attributes.h" #include "mx/core/generated/Clef.h" +#include "mx/core/generated/FullNoteGroup.h" +#include "mx/core/generated/GraceNormalNoteGroup.h" +#include "mx/core/generated/GraceNoteChoice.h" +#include "mx/core/generated/GraceNoteGroup.h" #include "mx/core/generated/MusicDataChoice.h" +#include "mx/core/generated/NormalNoteGroup.h" +#include "mx/core/generated/Note.h" +#include "mx/core/generated/NoteChoice.h" #include "mx/core/generated/StaffDetails.h" #include "mx/impl/MeasureWriter.h" #include "mx/impl/ScoreWriter.h" #include +#include using namespace mx; using namespace mx::impl; @@ -273,6 +281,66 @@ TEST(ZeroTicksPerQuarterKeepsOtherProperties, MeasureWriter) T_END +TEST(GraceChordThenChordAtSameTick, MeasureWriter) +{ + // Grace notes occupy no ticks, so a grace-note chord shares its tick position with the + // chord it ornaments. The first note of the main chord must still start a new chord (no + // tag), not be absorbed into the grace chord (#345). + mxtest::TestParameters params; + params.ticksPerQuarter = 4; + params.measureIndex = 0; + params.partIndex = 0; + params.numStaves = 1; + mxtest::TestItems t = mxtest::setupTestItems(params); + auto &staff = t.measureData->staves.at(0); + auto &voice = staff.voices[0]; + + auto makeNote = [](int step, bool isGrace) { + api::NoteData note{}; + note.isChord = true; + note.isGrace = isGrace; + note.tickTimePosition = 0; + note.pitchData.step = static_cast(step); + note.pitchData.octave = 4; + note.durationData.durationName = api::DurationName::quarter; + note.durationData.durationTimeTicks = isGrace ? 0 : 4; + return note; + }; + + voice.notes.push_back(makeNote(0, true)); + voice.notes.push_back(makeNote(2, true)); + voice.notes.push_back(makeNote(4, false)); + voice.notes.push_back(makeNote(6, false)); + + const auto partwiseMeasure = t.measureWriter->getPartwiseMeasure(); + std::vector chordTags; + for (const auto &mdc : partwiseMeasure.musicData()) + { + if (!mdc.isNote()) + { + continue; + } + const auto &choice = mdc.asNote().choice(); + if (choice.isGraceNoteGroup()) + { + chordTags.push_back( + choice.asGraceNoteGroup().graceNoteChoice().asGraceNormalNoteGroup().fullNote().chord()); + } + else if (choice.isNormalNoteGroup()) + { + chordTags.push_back(choice.asNormalNoteGroup().fullNote().chord()); + } + } + + CHECK_EQUAL(4, static_cast(chordTags.size())); + CHECK(!chordTags.at(0)); // first grace note starts the grace chord + CHECK(chordTags.at(1)); // second grace note carries + CHECK(!chordTags.at(2)); // first main-chord note must NOT carry + CHECK(chordTags.at(3)); // second main-chord note carries +} + +T_END + TEST(MultiStaffClefKeepsNumber, MeasureWriter) { // Counterpart to PropertiesButNoNotes: in a multi-staff part the clef number is required