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
9 changes: 9 additions & 0 deletions src/private/mx/impl/MeasureWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions src/private/mxtest/api/roundtrip-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
68 changes: 68 additions & 0 deletions src/private/mxtest/impl/MeasureWriterTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <memory>
#include <vector>

using namespace mx;
using namespace mx::impl;
Expand Down Expand Up @@ -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
// <chord/> 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<api::Step>(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<bool> 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<int>(chordTags.size()));
CHECK(!chordTags.at(0)); // first grace note starts the grace chord
CHECK(chordTags.at(1)); // second grace note carries <chord/>
CHECK(!chordTags.at(2)); // first main-chord note must NOT carry <chord/>
CHECK(chordTags.at(3)); // second main-chord note carries <chord/>
}

T_END

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