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
10 changes: 9 additions & 1 deletion src/engraving/editing/edit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4681,6 +4681,14 @@ void Score::removeChordRest(ChordRest* cr, bool clearSegment)

void Score::cmdDeleteTuplet(Tuplet* tuplet, bool replaceWithRest)
{
// Tuplet::tick() can be wrong (e.g. 0) on MIDI-imported scores because the
// MIDI importer does not call setTick() on Tuplet objects. Save the actual
// tick from the first element BEFORE removing elements so setRest() places
// the replacement rest at the correct position.
const Fraction actualTick = tuplet->elements().empty()
? tuplet->tick()
: tuplet->elements().front()->tick();

std::vector<DurationElement*> elements = tuplet->elements();
for (DurationElement* de : elements) {
if (de->isChordRest()) {
Expand All @@ -4691,7 +4699,7 @@ void Score::cmdDeleteTuplet(Tuplet* tuplet, bool replaceWithRest)
}
}
if (replaceWithRest) {
setRest(tuplet->tick(), tuplet->track(), tuplet->ticks(), true, tuplet->tuplet());
setRest(actualTick, tuplet->track(), tuplet->ticks(), true, tuplet->tuplet());
}
}

Expand Down
52 changes: 52 additions & 0 deletions src/engraving/tests/tuplet_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "engraving/dom/factory.h"
#include "engraving/dom/masterscore.h"
#include "engraving/dom/measure.h"
#include "engraving/dom/rest.h"
#include "engraving/dom/staff.h"
#include "engraving/dom/timesig.h"
#include "engraving/dom/tuplet.h"
Expand Down Expand Up @@ -197,3 +198,54 @@ TEST_F(Engraving_TupletTests, saveLoad)
EXPECT_TRUE(ScoreComp::saveCompareScore(score, u"save-load.mscx", TUPLET_DATA_DIR + u"save-load.mscx"));
delete score;
}

TEST_F(Engraving_TupletTests, deleteWithCorruptedTick)
{
// Regression test: cmdDeleteTuplet must use the first element's tick, not
// tuplet->tick(), which can be 0 on MIDI-imported scores. Without the fix,
// the replacement rest lands at tick 0 and corrupts measure 1.
MasterScore* score = ScoreRW::readScore(TUPLET_DATA_DIR + u"tuplet1.mscx");
ASSERT_TRUE(score);

// tuplet1.mscx: measure 1 = whole-measure rest, measure 2 = 4 quarter chords
Measure* m1 = score->firstMeasure();
Measure* m2 = m1 ? m1->nextMeasure() : nullptr;
ASSERT_TRUE(m1 && m2);

Segment* seg = m2->first(SegmentType::ChordRest);
ASSERT_TRUE(seg);
ChordRest* cr = toChordRest(seg->element(0));
ASSERT_TRUE(cr && cr->isChord());
const Fraction tupletTick = cr->tick();

ASSERT_TRUE(createTuplet(3, cr));

// After cmdCreateTuplet the original cr may be replaced; find the tuplet
// by re-querying the segment at the same tick.
Segment* segAfter = m2->first(SegmentType::ChordRest);
ASSERT_TRUE(segAfter);
ChordRest* crAfter = toChordRest(segAfter->element(0));
ASSERT_TRUE(crAfter);
Tuplet* tuplet = crAfter->tuplet();
ASSERT_TRUE(tuplet);

// Simulate MIDI import: corrupt the tuplet tick to 0
tuplet->setTick(Fraction(0, 1));

score->startCmd(TranslatableString::untranslatable("Test delete tuplet with corrupted tick"));
score->cmdDeleteTuplet(tuplet, true);
score->endCmd();

// Measure 1's whole-measure rest must be unchanged
Segment* seg0 = m1->first(SegmentType::ChordRest);
ASSERT_TRUE(seg0);
EngravingItem* el0 = seg0->element(0);
ASSERT_TRUE(el0 && el0->isRest());
EXPECT_EQ(toRest(el0)->ticks(), m1->ticks());

// The replacement rest must be at the actual tuplet position
Segment* segAtTuplet = score->tick2segment(tupletTick, false, SegmentType::ChordRest);
EXPECT_TRUE(segAtTuplet && segAtTuplet->element(0) && segAtTuplet->element(0)->isRest());

delete score;
}
Loading