Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2026 MuseScore Limited
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "regroup.h"
24 :
25 : #include "editspanner.h"
26 : #include "transaction/transaction.h"
27 :
28 : #include "../dom/chord.h"
29 : #include "../dom/factory.h"
30 : #include "../dom/input.h"
31 : #include "../dom/measure.h"
32 : #include "../dom/note.h"
33 : #include "../dom/rest.h"
34 : #include "../dom/score.h"
35 : #include "../dom/segment.h"
36 : #include "../dom/select.h"
37 : #include "../dom/sig.h"
38 : #include "../dom/slur.h"
39 : #include "../dom/staff.h"
40 : #include "../dom/tie.h"
41 : #include "../dom/utils.h"
42 :
43 : #include "log.h"
44 :
45 : using namespace mu::engraving;
46 :
47 : //---------------------------------------------------------
48 : // regroupNotesAndRests
49 : // * combine consecutive rests into fewer rests of longer duration.
50 : // * combine tied notes/chords into fewer notes of longer duration.
51 : // Only operates on one voice - protects manual layout adjustment, etc.
52 : //---------------------------------------------------------
53 :
54 56 : void Regroup::regroupNotesAndRests(Transaction& tx, Score* score, const Fraction& startTick, const Fraction& endTick,
55 : track_idx_t track)
56 : {
57 56 : InputState& is = score->inputState();
58 56 : Segment* inputSegment = is.segment(); // store this so we can get back to it later.
59 56 : Segment* seg = score->tick2segment(startTick, true, SegmentType::ChordRest);
60 1104 : for (Measure* msr = seg->measure(); msr && msr->tick() < endTick; msr = msr->nextMeasure()) {
61 1048 : Fraction maxTick = endTick > msr->endTick() ? msr->endTick() : endTick;
62 1048 : if (!seg || seg->measure() != msr) {
63 992 : seg = msr->first(SegmentType::ChordRest);
64 : }
65 7502 : for (; seg; seg = seg->next(SegmentType::ChordRest)) {
66 6454 : ChordRest* curr = seg->cr(track);
67 6454 : if (!curr) {
68 5488 : continue; // this voice is empty here (CR overlaps with CR in other track)
69 : }
70 966 : if (curr->endTick() > maxTick) {
71 0 : break; // outside range
72 : }
73 966 : if (curr->isRest() && !(curr->tuplet()) && !(toRest(curr)->isGap())) {
74 : // combine consecutive rests
75 382 : ChordRest* lastRest = curr;
76 1033 : for (Segment* s = seg->next(SegmentType::ChordRest); s; s = s->next(SegmentType::ChordRest)) {
77 903 : ChordRest* cr = s->cr(track);
78 903 : if (!cr) {
79 0 : continue; // this voice is empty here
80 : }
81 903 : if (!cr->isRest() || cr->tuplet() || cr->endTick() > maxTick || toRest(cr)->isGap()) {
82 252 : break; // next element in the same voice is not a rest, or it exceeds the selection, or it is a gap
83 : }
84 651 : lastRest = cr;
85 : }
86 382 : Fraction restTicks = (lastRest->endTick() - curr->tick()) * curr->staff()->timeStretch(curr->tick());
87 382 : seg = score->setNoteRest(seg, curr->track(), NoteVal(), restTicks, DirectionV::AUTO, false, {}, true);
88 584 : } else if (curr->isChord()) {
89 : // combine tied chords
90 582 : Chord* chord = toChord(curr);
91 582 : Chord* lastTiedChord = chord;
92 1349 : for (Chord* next = chord->nextTiedChord(); next && next->endTick() <= maxTick; next = next->nextTiedChord()) {
93 767 : lastTiedChord = next;
94 : }
95 582 : if (!lastTiedChord) {
96 0 : lastTiedChord = chord;
97 : }
98 582 : Fraction noteTicks = (lastTiedChord->endTick() - chord->tick()) * chord->staff()->timeStretch(chord->tick());
99 582 : if (!(curr->tuplet())) {
100 : // store start/end note for backward/forward ties ending/starting on the group of notes being rewritten
101 576 : size_t numNotes = chord->notes().size();
102 1152 : std::vector<Note*> tieBack(numNotes);
103 576 : std::vector<Note*> tieFor(numNotes);
104 1166 : for (size_t i = 0; i < numNotes; i++) {
105 590 : Note* n = chord->notes()[i];
106 590 : Note* nn = lastTiedChord->notes()[i];
107 590 : if (n->tieBack()) {
108 82 : tieBack[i] = n->tieBack()->startNote();
109 : } else {
110 508 : tieBack[i] = 0;
111 : }
112 590 : if (nn->tieFor()) {
113 15 : tieFor[i] = nn->tieFor()->endNote();
114 : } else {
115 575 : tieFor[i] = 0;
116 : }
117 : }
118 576 : Fraction tick = seg->tick();
119 576 : track_idx_t tr = chord->track();
120 576 : Fraction sd = noteTicks;
121 576 : std::vector<Tie*> ties;
122 576 : Segment* segment = seg;
123 576 : ChordRest* cr = toChordRest(segment->element(tr));
124 576 : Chord* nchord = toChord(chord->clone());
125 1166 : for (size_t i = 0; i < numNotes; i++) { // strip ties from cloned chord
126 590 : Note* n = nchord->notes()[i];
127 590 : if (Tie* tieFor2 = n->tieFor()) {
128 349 : n->setTieFor(nullptr);
129 349 : delete tieFor2;
130 : }
131 590 : if (Tie* tieBack2 = n->tieBack()) {
132 0 : n->setTieBack(nullptr);
133 0 : delete tieBack2;
134 : }
135 : }
136 576 : Chord* startChord = nchord;
137 576 : Measure* measure = nullptr;
138 576 : bool firstpart = true;
139 : for (;;) {
140 576 : if (tr % VOICES) {
141 8 : score->expandVoice(segment, tr);
142 : }
143 : // the returned gap ends at the measure boundary or at tuplet end
144 576 : Fraction dd = score->makeGap(segment, tr, sd, cr->tuplet());
145 576 : if (dd.isZero()) {
146 0 : break;
147 : }
148 576 : measure = segment->measure();
149 576 : std::vector<TDuration> dl;
150 1728 : dl = toRhythmicDurationList(dd, false, segment->rtick(), score->sigmap()->timesig(
151 576 : tick.ticks()).nominal(), measure, 1,
152 1152 : score->staff(track2staff(track))->timeStretch(tick));
153 576 : size_t n = dl.size();
154 1259 : for (size_t i = 0; i < n; ++i) {
155 683 : const TDuration& d = dl[i];
156 683 : Chord* nchord2 = toChord(nchord->clone());
157 683 : if (!firstpart) {
158 107 : nchord2->removeMarkings(true);
159 : }
160 683 : nchord2->setDurationType(d);
161 683 : nchord2->setTicks(d.fraction());
162 683 : std::vector<Note*> nl1 = nchord->notes();
163 683 : std::vector<Note*> nl2 = nchord2->notes();
164 683 : if (!firstpart) {
165 214 : for (size_t j = 0; j < nl1.size(); ++j) {
166 107 : Tie* tie = Factory::createTie(nl1[j]);
167 107 : tie->setStartNote(nl1[j]);
168 107 : tie->setEndNote(nl2[j]);
169 107 : tie->setTick(tie->startNote()->tick());
170 107 : tie->setTick2(tie->endNote()->tick());
171 107 : tie->setTrack(tr);
172 107 : nl1[j]->setTieFor(tie);
173 107 : nl2[j]->setTieBack(tie);
174 107 : ties.push_back(tie);
175 : }
176 : }
177 683 : score->undoAddCR(nchord2, measure, tick);
178 683 : segment = nchord2->segment();
179 683 : tick += nchord2->actualTicks();
180 683 : nchord = nchord2;
181 683 : firstpart = false;
182 683 : }
183 576 : sd -= dd;
184 576 : if (sd.isZero()) {
185 576 : break;
186 : }
187 0 : Segment* nseg = score->tick2segment(tick, false, SegmentType::ChordRest);
188 0 : if (nseg == 0) {
189 0 : break;
190 : }
191 0 : segment = nseg;
192 0 : cr = toChordRest(segment->element(tr));
193 0 : if (cr == 0) {
194 0 : if (tr % VOICES) {
195 0 : cr = score->addRest(segment, tr, TDuration(DurationType::V_MEASURE), 0);
196 : } else {
197 0 : break;
198 : }
199 : }
200 576 : }
201 576 : if (is.slur()) {
202 : // extend slur
203 0 : is.slur()->undoChangeProperty(Pid::SPANNER_TICKS, nchord->tick() - is.slur()->tick());
204 0 : for (EngravingObject* e : is.slur()->linkList()) {
205 0 : Slur* slur = toSlur(e);
206 0 : for (EngravingObject* ee : nchord->linkList()) {
207 0 : EngravingItem* e1 = static_cast<EngravingItem*>(ee);
208 0 : if (e1->score() == slur->score() && e1->track() == slur->track2()) {
209 0 : tx.push(new ChangeSpannerElements(slur, slur->startElement(), e1));
210 0 : break;
211 : }
212 0 : }
213 0 : }
214 : }
215 : // recreate previously stored pending ties
216 1166 : for (size_t i = 0; i < numNotes; i++) {
217 590 : Note* n = startChord->notes()[i];
218 590 : Note* nn = nchord->notes()[i];
219 590 : if (tieBack[i]) {
220 82 : Tie* tie = Factory::createTie(tieBack[i]);
221 82 : tie->setStartNote(tieBack[i]);
222 82 : tie->setEndNote(n);
223 82 : tie->setTick(tie->startNote()->tick());
224 82 : tie->setTick2(tie->endNote()->tick());
225 82 : tie->setTrack(track);
226 82 : n->setTieBack(tie);
227 82 : tieBack[i]->setTieFor(tie);
228 82 : ties.push_back(tie);
229 : }
230 590 : if (tieFor[i]) {
231 15 : Tie* tie = Factory::createTie(nn);
232 15 : tie->setStartNote(nn);
233 15 : tie->setEndNote(tieFor[i]);
234 15 : tie->setTick(tie->startNote()->tick());
235 15 : tie->setTick2(tie->endNote()->tick());
236 15 : tie->setTrack(track);
237 15 : nn->setTieFor(tie);
238 15 : tieFor[i]->setTieBack(tie);
239 15 : ties.push_back(tie);
240 : }
241 : }
242 576 : if (!ties.empty()) { // at least one tie was created
243 377 : for (Tie* tie : ties) {
244 204 : score->undoAddElement(tie);
245 : }
246 173 : score->connectTies();
247 : }
248 576 : }
249 : }
250 : }
251 : }
252 : // now put the input state back where it was before
253 56 : is.setSegment(inputSegment);
254 56 : }
255 :
256 9 : void Regroup::regroupNotesAndRestsInSelection(Transaction& tx, Score* score)
257 : {
258 9 : bool noSelection = score->selection().isNone();
259 9 : if (noSelection) {
260 0 : score->cmdSelectAll();
261 9 : } else if (!score->selection().isRange()) {
262 0 : LOGD("no system or staff selected");
263 0 : return;
264 : }
265 :
266 : // save selection values because selection changes during grouping
267 9 : Fraction sTick = score->selection().tickStart();
268 9 : Fraction eTick = score->selection().tickEnd();
269 9 : staff_idx_t sStaff = score->selection().staffStart();
270 9 : staff_idx_t eStaff = score->selection().staffEnd();
271 9 : Box* sBox = score->selection().startBox();
272 9 : Box* eBox = score->selection().endBox();
273 :
274 22 : for (staff_idx_t staff = sStaff; staff < eStaff; staff++) {
275 13 : track_idx_t sTrack = staff * VOICES;
276 13 : track_idx_t eTrack = sTrack + VOICES;
277 65 : for (track_idx_t track = sTrack; track < eTrack; track++) {
278 52 : if (score->selectionFilter().canSelectVoice(track)) {
279 52 : regroupNotesAndRests(tx, score, sTick, eTick, track);
280 : }
281 : }
282 : }
283 :
284 9 : if (noSelection) {
285 0 : score->deselectAll();
286 0 : return;
287 : }
288 :
289 : // Reset selection to original selection
290 9 : score->selection().setRangeTicks(sTick, eTick, sStaff, eStaff, sBox, eBox);
291 9 : score->selection().updateSelectedElements();
292 : }
|