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 "implodeexplode.h"
24 :
25 : #include "../dom/chord.h"
26 : #include "../dom/factory.h"
27 : #include "../dom/measure.h"
28 : #include "../dom/mscore.h"
29 : #include "../dom/note.h"
30 : #include "../dom/score.h"
31 : #include "../dom/segment.h"
32 : #include "../dom/staff.h"
33 : #include "../dom/utils.h"
34 : #include "../rw/xmlreader.h"
35 :
36 : #include "clonevoice.h"
37 : #include "noteinput.h"
38 : #include "paste.h"
39 : #include "transaction/transaction.h"
40 :
41 : using namespace mu::engraving;
42 :
43 2 : bool ImplodeExplode::explode(Score* score)
44 : {
45 2 : size_t srcStaff = score->selection().staffStart();
46 2 : size_t lastStaff = score->selection().staffEnd();
47 2 : size_t srcTrack = srcStaff * VOICES;
48 :
49 : // reset selection to top staff only
50 : // force complete measures
51 2 : Segment* startSegment = score->selection().startSegment();
52 2 : Segment* endSegment = score->selection().endSegment();
53 2 : Measure* startMeasure = startSegment->measure();
54 2 : Measure* endMeasure = nullptr;
55 2 : if (!endSegment) {
56 0 : endMeasure = score->lastMeasure();
57 2 : } else if (endSegment->tick() == endSegment->measure()->tick()) {
58 0 : endMeasure = endSegment->measure()->prevMeasure() ? endSegment->measure()->prevMeasure() : score->firstMeasure();
59 : } else {
60 2 : endMeasure = endSegment->measure();
61 : }
62 :
63 2 : Fraction lTick = endMeasure->endTick();
64 2 : bool voice = false;
65 :
66 6 : for (Measure* m = startMeasure; m && m->tick() != lTick; m = m->nextMeasure()) {
67 5 : if (m->hasVoices(srcStaff)) {
68 1 : voice = true;
69 1 : break;
70 : }
71 : }
72 : // force complete measures
73 2 : score->deselectAll();
74 2 : score->select(startMeasure, SelectType::RANGE, srcStaff);
75 2 : score->select(endMeasure, SelectType::RANGE, srcStaff);
76 2 : startSegment = score->selection().startSegment();
77 2 : endSegment = score->selection().endSegment();
78 2 : if (!voice) {
79 1 : if (srcStaff == lastStaff - 1) {
80 : // only one staff was selected up front - determine number of staves
81 : // loop through all chords looking for maximum number of notes
82 0 : int n = 0;
83 0 : for (Segment* s = startSegment; s && s != endSegment; s = s->next1()) {
84 0 : EngravingItem* e = s->element(srcTrack);
85 0 : if (e && e->isChord()) {
86 0 : Chord* c = toChord(e);
87 0 : n = std::max(n, int(c->notes().size()));
88 0 : for (Chord* graceChord : c->graceNotes()) {
89 0 : n = std::max(n, int(graceChord->notes().size()));
90 : }
91 : }
92 : }
93 0 : lastStaff = std::min(score->nstaves(), srcStaff + n);
94 : }
95 :
96 : // Check that all source and dest measures have the same time stretch - allows explode within a local time signature,
97 : // but don't yet support it between differing local time signatures.
98 1 : Fraction timeStretch(1, 0);
99 5 : for (Measure* m = startMeasure; m && m->tick() <= endMeasure->tick(); m = m->nextMeasure()) {
100 20 : for (size_t staffIdx = srcStaff; staffIdx < lastStaff; ++staffIdx) {
101 16 : Fraction mTimeStretch = score->staff(staffIdx)->timeStretch(m->tick());
102 16 : if (!timeStretch.isValid()) {
103 1 : timeStretch = mTimeStretch;
104 15 : } else if (timeStretch != mTimeStretch) {
105 0 : MScore::setError(MsError::CANNOT_EXPLODE_IMPLODE_LOCAL_TIMESIG);
106 0 : return false;
107 : }
108 : }
109 : }
110 :
111 1 : const muse::ByteArray mimeData(score->selection().mimeData());
112 : // copy to all destination staves
113 1 : Segment* firstCRSegment = startMeasure->tick2segment(startMeasure->tick());
114 4 : for (size_t i = 1; srcStaff + i < lastStaff; ++i) {
115 3 : track_idx_t track = (srcStaff + i) * VOICES;
116 3 : ChordRest* cr = toChordRest(firstCRSegment->element(track));
117 3 : if (cr) {
118 3 : XmlReader e(mimeData);
119 3 : Paste::pasteStaff(score->transactionManager()->currentOrDummyTransaction(), score, e, cr->segment(), cr->staffIdx());
120 3 : }
121 : }
122 :
123 44 : auto doExplode = [score](Chord* c, size_t lastStaff, size_t srcStaff, size_t i) -> void
124 : {
125 44 : std::vector<Note*> notes = c->notes();
126 44 : size_t nnotes = notes.size();
127 : // keep note "i" from top, which is backwards from nnotes - 1
128 : // reuse notes if there are more instruments than notes
129 44 : size_t stavesPerNote = std::max((lastStaff - srcStaff) / nnotes, static_cast<size_t>(1));
130 44 : size_t keepIndex = static_cast<size_t>(std::max(static_cast<int>(nnotes) - 1 - static_cast<int>(i / stavesPerNote), 0));
131 44 : Note* keepNote = c->notes()[keepIndex];
132 216 : for (Note* n : notes) {
133 172 : if (n != keepNote) {
134 128 : score->undoRemoveElement(n);
135 : }
136 : }
137 44 : };
138 :
139 : // loop through each staff removing all but one note from each chord
140 5 : for (size_t i = 0; srcStaff + i < lastStaff; ++i) {
141 4 : track_idx_t track = (srcStaff + i) * VOICES;
142 64 : for (Segment* s = startSegment; s && s != endSegment; s = s->next1()) {
143 60 : EngravingItem* e = s->element(track);
144 60 : if (e && e->isChord()) {
145 44 : Chord* c = toChord(e); //chord, laststaff, srcstaff
146 44 : doExplode(c, lastStaff, srcStaff, i);
147 44 : for (Chord* graceChord : c->graceNotes()) {
148 0 : doExplode(graceChord, lastStaff, srcStaff, i);
149 : }
150 : }
151 : }
152 : }
153 1 : } else {
154 : track_idx_t sTracks[VOICES];
155 : track_idx_t dTracks[VOICES];
156 1 : if (srcStaff == lastStaff - 1) {
157 0 : lastStaff = std::min(score->nstaves(), srcStaff + VOICES);
158 : }
159 :
160 5 : for (voice_idx_t i = 0; i < VOICES; i++) {
161 4 : sTracks[i] = muse::nidx;
162 4 : dTracks[i] = muse::nidx;
163 : }
164 1 : size_t matchedTrackCount = 0;
165 :
166 16 : for (Segment* seg = startSegment; seg && seg->tick() < lTick; seg = seg->next1()) {
167 19 : for (track_idx_t candidateSrcTrack = srcTrack; candidateSrcTrack < srcTrack + VOICES && matchedTrackCount != VOICES;
168 : candidateSrcTrack++) {
169 4 : bool isUnclaimedSrcTrack = true;
170 20 : for (voice_idx_t claimedIdx = 0; claimedIdx < VOICES; claimedIdx++) {
171 16 : if (candidateSrcTrack == sTracks[claimedIdx]) {
172 0 : isUnclaimedSrcTrack = false;
173 0 : break;
174 : }
175 : }
176 :
177 8 : if (!seg->measure()->hasVoice(candidateSrcTrack) || seg->measure()->isOnlyGapRests(candidateSrcTrack)
178 8 : || !isUnclaimedSrcTrack) {
179 0 : continue;
180 : }
181 4 : sTracks[matchedTrackCount] = candidateSrcTrack;
182 :
183 4 : for (size_t candidateDstTrack = srcTrack + matchedTrackCount * VOICES; candidateDstTrack < lastStaff * VOICES;
184 : candidateDstTrack++) {
185 4 : if (candidateSrcTrack == candidateDstTrack) {
186 1 : dTracks[matchedTrackCount] = candidateDstTrack;
187 1 : break;
188 : }
189 15 : for (Measure* m = seg->measure(); m && m->tick() < lTick; m = m->nextMeasure()) {
190 12 : if (!m->hasVoice(candidateDstTrack) || (m->hasVoice(candidateDstTrack) && m->isOnlyRests(candidateDstTrack))) {
191 12 : dTracks[matchedTrackCount] = candidateDstTrack;
192 : } else {
193 0 : dTracks[matchedTrackCount] = muse::nidx;
194 0 : break;
195 : }
196 : }
197 3 : if (dTracks[matchedTrackCount] != muse::nidx) {
198 3 : break;
199 : }
200 : }
201 4 : matchedTrackCount++;
202 : }
203 : }
204 :
205 1 : IF_ASSERT_FAILED(matchedTrackCount > 0) {
206 0 : return false;
207 : }
208 1 : lastStaff = track2staff(dTracks[matchedTrackCount - 1]) + 1;
209 :
210 : // Check that all source and dest measures have the same time stretch - allows explode within a local time signature,
211 : // but don't yet support it between differing local time signatures.
212 1 : Fraction timeStretch(1, 0);
213 5 : for (Measure* m = startMeasure; m && m->tick() <= endMeasure->tick(); m = m->nextMeasure()) {
214 20 : for (size_t staffIdx = srcStaff; staffIdx < lastStaff; ++staffIdx) {
215 16 : Fraction mTimeStretch = score->staff(staffIdx)->timeStretch(m->tick());
216 16 : if (!timeStretch.isValid()) {
217 1 : timeStretch = mTimeStretch;
218 15 : } else if (timeStretch != mTimeStretch) {
219 0 : MScore::setError(MsError::CANNOT_EXPLODE_IMPLODE_LOCAL_TIMESIG);
220 0 : return false;
221 : }
222 : }
223 : }
224 :
225 5 : for (track_idx_t i = srcTrack, j = 0; i < lastStaff * VOICES && j < VOICES; i += VOICES, j++) {
226 4 : track_idx_t strack = sTracks[j % VOICES];
227 4 : track_idx_t dtrack = dTracks[j % VOICES];
228 4 : if (strack != muse::nidx && strack != dtrack && dtrack != muse::nidx) {
229 3 : CloneVoice::cloneVoice(startSegment, lTick, startSegment, strack, dtrack, true, false);
230 : }
231 : }
232 : }
233 :
234 : // select exploded region
235 2 : score->deselectAll();
236 2 : score->select(startMeasure, SelectType::RANGE, srcStaff);
237 2 : score->select(endMeasure, SelectType::RANGE, lastStaff - 1);
238 :
239 2 : return true;
240 : }
241 :
242 5 : bool ImplodeExplode::implode(Score* score)
243 : {
244 5 : staff_idx_t dstStaff = score->selection().staffStart();
245 5 : staff_idx_t endStaff = score->selection().staffEnd();
246 5 : track_idx_t dstTrack = dstStaff * VOICES;
247 5 : track_idx_t startTrack = dstStaff * VOICES;
248 5 : track_idx_t endTrack = endStaff * VOICES;
249 5 : Segment* startSegment = score->selection().startSegment();
250 5 : Segment* endSegment = score->selection().endSegment();
251 5 : Measure* startMeasure = startSegment->measure();
252 5 : Measure* endMeasure = endSegment ? endSegment->measure() : score->lastMeasure();
253 5 : Fraction startTick = startSegment->tick();
254 5 : Fraction endTick = endSegment ? endSegment->tick() : score->lastMeasure()->endTick();
255 5 : assert(startMeasure && endMeasure);
256 :
257 : // Check that all source and dest measures have the same time stretch - allows implode within a local time signature,
258 : // but don't yet support it between differing local time signatures.
259 5 : Fraction timeStretch(1, 0);
260 22 : for (Measure* m = startMeasure; m && m->tick() <= endMeasure->tick(); m = m->nextMeasure()) {
261 62 : for (size_t staffIdx = dstStaff; staffIdx < endStaff; ++staffIdx) {
262 45 : Fraction mTimeStretch = score->staff(staffIdx)->timeStretch(m->tick());
263 45 : if (!timeStretch.isValid()) {
264 5 : timeStretch = mTimeStretch;
265 40 : } else if (timeStretch != mTimeStretch) {
266 0 : MScore::setError(MsError::CANNOT_EXPLODE_IMPLODE_LOCAL_TIMESIG);
267 0 : return false;
268 : }
269 : }
270 : }
271 :
272 : // if single staff selected, combine voices
273 : // otherwise combine staves
274 5 : if (dstStaff == endStaff - 1) {
275 : // loop through segments adding notes to chord on top staff
276 21 : for (Segment* s = startSegment; s && s != endSegment; s = s->next1()) {
277 19 : if (!s->isChordRestType()) {
278 3 : continue;
279 : }
280 16 : EngravingItem* dst = s->element(dstTrack);
281 16 : if (dst && dst->isChord()) {
282 13 : Chord* dstChord = toChord(dst);
283 : // see if we are tying in to this chord
284 13 : Chord* tied = 0;
285 25 : for (Note* n : dstChord->notes()) {
286 13 : if (n->tieBackNonPartial()) {
287 1 : tied = n->tieBack()->startNote()->chord();
288 1 : break;
289 : }
290 : }
291 : // loop through each subsequent staff (or track within staff)
292 : // looking for notes to add
293 52 : for (track_idx_t srcTrack = startTrack + 1; srcTrack < endTrack; srcTrack++) {
294 39 : EngravingItem* src = s->element(srcTrack);
295 39 : if (src && src->isChord()) {
296 22 : Chord* srcChord = toChord(src);
297 : // when combining voices, skip if not same duration
298 22 : if (srcChord->ticks() != dstChord->ticks()) {
299 1 : continue;
300 : }
301 : // add notes
302 42 : for (Note* n : srcChord->notes()) {
303 21 : NoteVal nv(n->pitch());
304 21 : nv.tpc1 = n->tpc1();
305 : // skip duplicates
306 21 : if (dstChord->findNote(nv.pitch)) {
307 2 : continue;
308 : }
309 19 : bool forceAccidental = n->accidental() && n->accidental()->role() == AccidentalRole::USER;
310 19 : Transaction& tx = score->transactionManager()->currentOrDummyTransaction();
311 19 : Note* nn = NoteInput::addNote(tx, score, dstChord, nv, forceAccidental);
312 : // move articulations
313 23 : for (Articulation* artic : srcChord->articulations()) {
314 4 : if (dstChord->hasArticulation(artic)) {
315 3 : continue;
316 : }
317 1 : score->undoChangeParent(artic, dstChord, dstChord->staffIdx());
318 : }
319 : // add tie to this note if original chord was tied
320 19 : if (tied) {
321 : // find note to tie to
322 3 : for (Note* tn : tied->notes()) {
323 2 : if (nn->pitch() == tn->pitch() && nn->tpc() == tn->tpc() && !tn->tieFor()) {
324 : // found note to tie
325 1 : Tie* tie = Factory::createTie(tn);
326 1 : tie->setStartNote(tn);
327 1 : tie->setEndNote(nn);
328 1 : tie->setTick(tie->startNote()->tick());
329 1 : tie->setTick2(tie->endNote()->tick());
330 1 : tie->setTrack(tn->track());
331 1 : score->undoAddElement(tie);
332 : }
333 : }
334 : }
335 : }
336 : }
337 : // delete chordrest from source track if possible
338 38 : if (src && src->voice()) {
339 21 : score->undoRemoveElement(src);
340 : }
341 : }
342 : }
343 : // TODO - use first voice that actually has a note and implode remaining voices on it?
344 : // see https://musescore.org/en/node/174111
345 3 : else if (dst) {
346 : // destination track has something, but it isn't a chord
347 : // remove rests from other voices if in "voice mode"
348 8 : for (voice_idx_t i = 1; i < VOICES; ++i) {
349 6 : EngravingItem* e = s->element(dstTrack + i);
350 6 : if (e && e->isRest()) {
351 4 : score->undoRemoveElement(e);
352 : }
353 : }
354 : }
355 : }
356 : // delete orphaned spanners (TODO: figure out solution to reconnect orphaned spanners to their cloned notes)
357 2 : score->checkSpanner(startTick, endTick);
358 : } else {
359 : track_idx_t tracks[VOICES];
360 15 : for (voice_idx_t i = 0; i < VOICES; i++) {
361 12 : tracks[i] = muse::nidx;
362 : }
363 3 : voice_idx_t full = 0;
364 :
365 : // identify tracks to combine, storing the source track numbers in tracks[]
366 : // first four non-empty tracks to win
367 40 : for (track_idx_t track = startTrack; track < endTrack && full < VOICES; ++track) {
368 37 : Measure* m = startMeasure;
369 : do {
370 118 : if (m->hasVoice(track) && !m->isOnlyRests(track)) {
371 10 : tracks[full++] = track;
372 10 : break;
373 : }
374 108 : } while ((m != endMeasure) && (m = m->nextMeasure()));
375 : }
376 :
377 : // clone source tracks into destination
378 15 : for (track_idx_t i = dstTrack; i < dstTrack + VOICES; i++) {
379 12 : track_idx_t strack = tracks[i % VOICES];
380 12 : if (strack != muse::nidx && strack != i) {
381 8 : CloneVoice::cloneVoice(startSegment, endTick, startSegment, strack, i, false, false);
382 : }
383 : }
384 : }
385 :
386 : // select destination staff only
387 5 : score->deselectAll();
388 5 : score->select(startMeasure, SelectType::RANGE, dstStaff);
389 5 : score->select(endMeasure, SelectType::RANGE, dstStaff);
390 :
391 5 : return true;
392 : }
|