Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/engraving/dom/mscore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ std::string MScore::errorToString(MsError err)
case MsError::CANNOT_REPEAT_SELECTION: return "CANNOT_REPEAT_SELECTION";
case MsError::TRANSPOSE_NO_FRET_DIAGRAM: return "TRANSPOSE_NO_FRET_DIAGRAM";
case MsError::CANNOT_EXPLODE_IMPLODE_LOCAL_TIMESIG: return "CANNOT_EXPLODE_IMPLODE_LOCAL_TIMESIG";
case MsError::NO_SPACE_FOR_TUPLET: return "NO_SPACE_FOR_TUPLET";
case MsError::TUPLET_NOTE_VALUE_TOO_SHORT: return "TUPLET_NOTE_VALUE_TOO_SHORT";
}

return {};
Expand Down
2 changes: 2 additions & 0 deletions src/engraving/dom/mscore.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ enum class MsError : unsigned char {
CANNOT_REPEAT_SELECTION,
TRANSPOSE_NO_FRET_DIAGRAM,
CANNOT_EXPLODE_IMPLODE_LOCAL_TIMESIG,
NO_SPACE_FOR_TUPLET,
TUPLET_NOTE_VALUE_TOO_SHORT,
};

/// \cond PLUGIN_API \private \endcond
Expand Down
2 changes: 2 additions & 0 deletions src/engraving/dom/score.h
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ class Score : public EngravingObject, public muse::Contextable
Tuplet* addTuplet(ChordRest* destinationChordRest, const Fraction& ratio, const TDuration& baseLen, TupletNumberType numberType,
TupletBracketType bracketType);
bool canAddTuplet(const ChordRest* chordRest, const Fraction& ratio, const TDuration& baseLen) const;
bool canAddTuplet(const Segment* segment, track_idx_t track, const Fraction& ratio, const TDuration& baseLen) const;
Fraction tupletEndTick(const ChordRest* chordRest, const Fraction& ratio, const TDuration& baseLen) const;
void selectNewTuplet(Tuplet* tuplet);

ChordRest* addClone(ChordRest* cr, const Fraction& tick, const TDuration& d);
Expand Down
69 changes: 60 additions & 9 deletions src/engraving/editing/edit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,23 @@ Tuplet* Score::addTuplet(ChordRest* destinationChordRest, Fraction ratio, Tuplet
return addTuplet(destinationChordRest, _ratio, TDuration(fr), numberType, bracketType);
}

static bool isValidTuplet(const Fraction& ratio, const TDuration& baseLen)
{
return baseLen.isValid() && !baseLen.isMeasure() && ratio.numerator() > 0 && ratio.denominator() > 0;
}

static Fraction tupletEnd(const Staff* staff, const Fraction& tick, Tuplet* parent, const Fraction& ratio, const TDuration& baseLen)
{
const Fraction ticks = baseLen.fraction() * ratio.denominator();
return tick + actualTicks(ticks, parent, staff->timeStretch(tick));
}

// A tuplet must not cross a barline or the end of its parent tuplet
static Fraction tupletLimit(const Tuplet* parent, const Measure* measure)
{
return parent ? parent->tick() + parent->actualTicks() : measure->endTick();
}

//---------------------------------------------------------
// canAddTuplet
// Whether addTuplet() can create a tuplet with this ratio
Expand All @@ -287,7 +304,7 @@ Tuplet* Score::addTuplet(ChordRest* destinationChordRest, Fraction ratio, Tuplet

bool Score::canAddTuplet(const ChordRest* chordRest, const Fraction& ratio, const TDuration& baseLen) const
{
if (!chordRest || !baseLen.isValid() || baseLen.isMeasure() || ratio.numerator() <= 0 || ratio.denominator() <= 0) {
if (!chordRest || !isValidTuplet(ratio, baseLen)) {
return false;
}

Expand All @@ -297,18 +314,52 @@ bool Score::canAddTuplet(const ChordRest* chordRest, const Fraction& ratio, cons
}

// changeCRlen() can't change the length of a measure repeat
const Fraction ticks = baseLen.fraction() * ratio.denominator();
if (chordRest->isMeasureRepeat() && ticks != chordRest->ticks()) {
if (chordRest->isMeasureRepeat() && baseLen.fraction() * ratio.denominator() != chordRest->ticks()) {
return false;
}

return tupletEndTick(chordRest, ratio, baseLen) <= tupletLimit(chordRest->tuplet(), measure);
}

//---------------------------------------------------------
// canAddTuplet
// The same at segment in track, where no chord/rest of
// the track may start: an empty voice, or inside a
// longer chord/rest
//---------------------------------------------------------

bool Score::canAddTuplet(const Segment* segment, track_idx_t track, const Fraction& ratio, const TDuration& baseLen) const
{
if (!segment || track == muse::nidx) {
return false;
}

if (const ChordRest* chordRest = segment->cr(track)) {
return canAddTuplet(chordRest, ratio, baseLen);
}

const Measure* measure = segment->measure();
const Staff* staff = this->staff(track2staff(track));
if (!isValidTuplet(ratio, baseLen) || !measure || measure->isMMRest() || !staff) {
return false;
}

// the tuplet must not cross a barline or the end of a parent tuplet
Tuplet* parent = chordRest->tuplet();
const Fraction timeStretch = chordRest->staff()->timeStretch(chordRest->tick());
const Fraction end = chordRest->tick() + actualTicks(ticks, parent, timeStretch);
const Fraction limit = parent ? parent->tick() + parent->actualTicks() : measure->endTick();
// inside a longer chord/rest, the tuplet would be in that chord/rest's tuplet
const ChordRest* previous = segment->nextChordRest(track, /*backwards*/ true, /*stopAtMeasureBoundary*/ true);
Tuplet* parent = previous && previous->endTick() > segment->tick() ? previous->tuplet() : nullptr;

return tupletEnd(staff, segment->tick(), parent, ratio, baseLen) <= tupletLimit(parent, measure);
}

//---------------------------------------------------------
// tupletEndTick
// Where a tuplet of ratio.denominator() * baseLen that
// starts at chordRest ends
//---------------------------------------------------------

return end <= limit;
Fraction Score::tupletEndTick(const ChordRest* chordRest, const Fraction& ratio, const TDuration& baseLen) const
{
return tupletEnd(chordRest->staff(), chordRest->tick(), chordRest->tuplet(), ratio, baseLen);
}

//---------------------------------------------------------
Expand Down
66 changes: 62 additions & 4 deletions src/engraving/editing/edittuplet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@
#include "edittuplet.h"

#include <algorithm>
#include <map>
#include <set>
#include <unordered_set>

#include "../dom/chord.h"
#include "../dom/chordrest.h"
#include "../dom/durationtype.h"
#include "../dom/input.h"
#include "../dom/mscore.h"
#include "../dom/note.h"
#include "../dom/score.h"
#include "../dom/select.h"
Expand Down Expand Up @@ -73,6 +77,27 @@ static std::vector<ChordRest*> selectedTupletChordRests(const Score* score)
return chordRests;
}

// The selected chords/rests that get a tuplet: one whose tuplet doesn't fit is left as it is (as is a measure rest,
// which has no duration to use as the subdivision), and one that the tuplet before it in its track will replace gets none
static std::vector<ChordRest*> tupletStarts(const Score* score, const Fraction& ratio)
{
std::vector<ChordRest*> starts;
std::map<track_idx_t, Fraction> tupletEndTicks;
for (ChordRest* chordRest : selectedTupletChordRests(score)) {
const auto previous = tupletEndTicks.find(chordRest->track());
if (previous != tupletEndTicks.end() && chordRest->tick() < previous->second) {
continue;
}
if (!score->canAddTuplet(chordRest, ratio, chordRest->durationType())) {
continue;
}
tupletEndTicks[chordRest->track()] = score->tupletEndTick(chordRest, ratio, chordRest->durationType());
starts.push_back(chordRest);
}

return starts;
}

static void selectTupletStart(Score* score, const Tuplet* tuplet)
{
if (tuplet->elements().empty()) {
Expand All @@ -94,9 +119,20 @@ static void selectTupletStart(Score* score, const Tuplet* tuplet)
}
}

bool EditTuplet::checkAddTupletsWithSelectedDurations(const Score* score, const Fraction& ratio)
{
if (!selectedTupletChordRests(score).empty() && tupletStarts(score, ratio).empty()) {
MScore::setError(MsError::NO_SPACE_FOR_TUPLET);
return false;
}

return true;
}

std::vector<Tuplet*> EditTuplet::addTuplets(Transaction&, Score* score, const Fraction& ratio, TupletNumberType numberType,
TupletBracketType bracketType, bool autoBaseLen)
TupletBracketType bracketType, bool autoBaseLen, TupletSelectedDuration selectedDurationAs)
{
const bool asSubdivision = selectedDurationAs == TupletSelectedDuration::SUBDIVISION;
Selection& selection = score->selection();
const bool wasRangeSelection = selection.isRange();
const Fraction startTick = selection.tickStart();
Expand All @@ -106,15 +142,37 @@ std::vector<Tuplet*> EditTuplet::addTuplets(Transaction&, Score* score, const Fr
Box* startBox = selection.startBox();
Box* endBox = selection.endBox();

// ticks taken before any tuplet is created
std::vector<std::pair<ChordRest*, Fraction> > starts;
for (ChordRest* chordRest : asSubdivision ? tupletStarts(score, ratio) : selectedTupletChordRests(score)) {
starts.emplace_back(chordRest, chordRest->tick());
}

std::vector<Tuplet*> tuplets;
for (ChordRest* chordRest : selectedTupletChordRests(score)) {
if (Tuplet* tuplet = score->addTuplet(chordRest, dividingRatio(chordRest, ratio, autoBaseLen), numberType, bracketType)) {
// a subdivision tuplet can end after the range
Fraction rangeEndTick = endTick;
for (const auto& [chordRest, tick] : starts) {
// an earlier tuplet may have replaced it, e.g. on a linked staff
if (asSubdivision && score->findCR(tick, chordRest->track()) != chordRest) {
continue;
}

Tuplet* tuplet = nullptr;
if (asSubdivision) {
tuplet = score->addTuplet(chordRest, ratio, chordRest->durationType(), numberType, bracketType);
if (tuplet) {
rangeEndTick = std::max(rangeEndTick, tuplet->endTick());
}
} else {
tuplet = score->addTuplet(chordRest, dividingRatio(chordRest, ratio, autoBaseLen), numberType, bracketType);
}
if (tuplet) {
tuplets.push_back(tuplet);
}
}

if (wasRangeSelection) {
selection.setRangeTicks(startTick, endTick, startStaff, endStaff, startBox, endBox);
selection.setRangeTicks(startTick, rangeEndTick, startStaff, endStaff, startBox, endBox);
selection.updateSelectedElements();
} else if (!tuplets.empty()) {
selectTupletStart(score, tuplets.front());
Expand Down
10 changes: 9 additions & 1 deletion src/engraving/editing/edittuplet.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,17 @@ class Transaction;
class EditTuplet
{
public:
// Tuplets on the selected chords/rests that use their durations as the subdivision (3:2 on a quarter: 3 quarters
// in the time of 2). Tuplets that don't fit are left out, and so are measure rests; false (with
// MsError::NO_SPACE_FOR_TUPLET) if none does
static bool checkAddTupletsWithSelectedDurations(const Score* score, const Fraction& ratio);

// Divides each selected chord/rest into a tuplet (3:2 on a quarter: 3 eighths in the time of a quarter). Keeps a range
// selection, otherwise selects the start of the first tuplet. Returns the tuplets in score order
// With TupletSelectedDuration::SUBDIVISION, call checkAddTupletsWithSelectedDurations() first, so that a message
// shows if no tuplet fits
static std::vector<Tuplet*> addTuplets(Transaction& tx, Score* score, const Fraction& ratio, TupletNumberType numberType,
TupletBracketType bracketType, bool autoBaseLen);
TupletBracketType bracketType, bool autoBaseLen,
TupletSelectedDuration selectedDurationAs = TupletSelectedDuration::TUPLET_LENGTH);
};
}
Loading
Loading