LCOV - code coverage report
Current view: top level - src/engraving/editing - implodeexplode.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 88.1 % 210 185
Test Date: 2026-11-05 04:07:02

            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              : }
        

Generated by: LCOV version 2.0-1