LCOV - code coverage report
Current view: top level - src/engraving/editing - exchangevoices.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 67.8 % 115 78
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 "exchangevoices.h"
      24              : 
      25              : #include "../dom/excerpt.h"
      26              : #include "../dom/measure.h"
      27              : #include "../dom/rest.h"
      28              : #include "../dom/score.h"
      29              : #include "../dom/segment.h"
      30              : #include "../dom/staff.h"
      31              : #include "../types/types.h"
      32              : 
      33              : #include "clonevoice.h"
      34              : #include "transaction/undoablecommand.h"
      35              : 
      36              : using namespace mu::engraving;
      37              : 
      38              : namespace {
      39              : class ExchangeVoicesInMeasure : public UndoableCommand
      40              : {
      41            8 :     OBJECT_ALLOCATOR(engraving, ExchangeVoicesInMeasure)
      42              : 
      43              : public:
      44            8 :     ExchangeVoicesInMeasure(Measure* measure, track_idx_t srcTrack, track_idx_t dstTrack, staff_idx_t staff)
      45            8 :         : m_measure(measure), m_srcTrack(srcTrack), m_dstTrack(dstTrack), m_staff(staff) {}
      46              : 
      47            3 :     void undo() override
      48              :     {
      49            3 :         doExchange(m_dstTrack, m_srcTrack);
      50            3 :     }
      51              : 
      52            8 :     void redo() override
      53              :     {
      54            8 :         doExchange(m_srcTrack, m_dstTrack);
      55            8 :     }
      56              : 
      57            8 :     UNDO_TYPE(CommandType::ExchangeVoice)
      58            0 :     UNDO_NAME("ExchangeVoice")
      59           24 :     UNDO_CHANGED_OBJECTS({ m_measure })
      60              : 
      61              : private:
      62              :     void doExchange(track_idx_t srcTrack, track_idx_t dstTrack);
      63              : 
      64              :     Measure* m_measure = nullptr;
      65              :     track_idx_t m_srcTrack = muse::nidx;
      66              :     track_idx_t m_dstTrack = muse::nidx;
      67              :     staff_idx_t m_staff = muse::nidx;
      68              : };
      69              : 
      70           11 : void ExchangeVoicesInMeasure::doExchange(track_idx_t srcTrack, track_idx_t dstTrack)
      71              : {
      72           46 :     for (Segment* s = m_measure->first(SegmentType::ChordRest); s; s = s->next(SegmentType::ChordRest)) {
      73           35 :         s->swapElements(srcTrack, dstTrack);
      74              :     }
      75              : 
      76           11 :     Fraction start = m_measure->tick();
      77           11 :     Fraction end = start + m_measure->ticks();
      78           11 :     auto spanners = m_measure->score()->spannerMap().findOverlapping(start.ticks(), end.ticks() - 1);
      79           19 :     for (auto i = spanners.begin(); i < spanners.end(); i++) {
      80            8 :         Spanner* sp = i->value;
      81            8 :         Fraction spStart = sp->tick();
      82            8 :         Fraction spEnd = spStart + sp->ticks();
      83            8 :         LOGD("Start %d End %d Diff %d \n Measure Start %d End %d", spStart.ticks(), spEnd.ticks(), (spEnd - spStart).ticks(),
      84              :              start.ticks(), end.ticks());
      85            8 :         if (sp->isSlur() && (spStart >= start || spEnd < end)) {
      86            6 :             if (sp->track() == srcTrack && spStart >= start) {
      87            4 :                 sp->setTrack(dstTrack);
      88            2 :             } else if (sp->track() == dstTrack && spStart >= start) {
      89            1 :                 sp->setTrack(srcTrack);
      90              :             }
      91            6 :             if (sp->track2() == srcTrack && spEnd < end) {
      92            5 :                 sp->setTrack2(dstTrack);
      93            1 :             } else if (sp->track2() == dstTrack && spEnd < end) {
      94            0 :                 sp->setTrack2(srcTrack);
      95              :             }
      96              :         }
      97              :     }
      98           11 :     m_measure->checkMultiVoices(m_staff); // probably true, but check for invisible notes & rests
      99           11 : }
     100              : }
     101              : 
     102            3 : void ExchangeVoices::exchangeVoicesInSelection(Score* score, voice_idx_t srcVoice, voice_idx_t dstVoice)
     103              : {
     104            3 :     if (!score->selection().isRange()) {
     105            0 :         MScore::setError(MsError::NO_STAFF_SELECTED);
     106            0 :         return;
     107              :     }
     108            3 :     Fraction t1 = score->selection().tickStart();
     109            3 :     Fraction t2 = score->selection().tickEnd();
     110              : 
     111            3 :     Measure* m1 = score->tick2measure(t1);
     112            3 :     Measure* m2 = score->tick2measure(t2);
     113              : 
     114            3 :     if (score->excerpt()) {
     115            0 :         return;
     116              :     }
     117              : 
     118            3 :     if (t2 > m2->tick()) {
     119            3 :         m2 = m2->nextMeasure();
     120              :     }
     121              : 
     122              :     for (;;) {
     123            8 :         exchangeVoices(score, m1, srcVoice, dstVoice, score->selection().staffStart(), score->selection().staffEnd());
     124            8 :         m1 = m1->nextMeasure();
     125            8 :         if ((m1 == 0) || (m2 && (m1->tick() == m2->tick()))) {
     126            3 :             break;
     127              :         }
     128              :     }
     129              : }
     130              : 
     131            8 : void ExchangeVoices::exchangeVoices(Score* score, Measure* measure, voice_idx_t srcVoice, voice_idx_t dstVoice, staff_idx_t srcStaff,
     132              :                                     staff_idx_t dstStaff)
     133              : {
     134            8 :     Fraction tick = measure->tick();
     135              : 
     136           16 :     for (staff_idx_t staffIdx = srcStaff; staffIdx < dstStaff; ++staffIdx) {
     137            8 :         track_idx_t srcStaffTrack = staffIdx * VOICES;
     138            8 :         track_idx_t srcTrack = srcStaffTrack + srcVoice;
     139            8 :         track_idx_t dstTrack = srcStaffTrack + dstVoice;
     140            8 :         int trackDiff = static_cast<int>(dstVoice - srcVoice);
     141              : 
     142              :         //handle score and complete measures first
     143            8 :         score->undo(new ExchangeVoicesInMeasure(measure, srcTrack, dstTrack, staffIdx));
     144              : 
     145           16 :         for (Staff* st : score->staff(staffIdx)->staffList()) {
     146            8 :             track_idx_t staffTrack = st->idx() * VOICES;
     147            8 :             Measure* measure2 = st->score()->tick2measure(tick);
     148            8 :             Excerpt* ex = st->score()->excerpt();
     149              : 
     150            8 :             if (ex) {
     151            0 :                 const TracksMap& tracks = ex->tracksMapping();
     152            0 :                 std::vector<track_idx_t> srcTrackList = muse::values(tracks, srcTrack);
     153            0 :                 std::vector<track_idx_t> dstTrackList = muse::values(tracks, dstTrack);
     154              : 
     155            0 :                 for (track_idx_t srcTrack2 : srcTrackList) {
     156              :                     // don't care about other linked staves
     157            0 :                     if (!(staffTrack <= srcTrack2) || !(srcTrack2 < staffTrack + VOICES)) {
     158            0 :                         continue;
     159              :                     }
     160              : 
     161            0 :                     track_idx_t tempTrack = srcTrack;
     162            0 :                     std::vector<track_idx_t> testTracks = muse::values(tracks, tempTrack + trackDiff);
     163            0 :                     bool hasVoice = false;
     164            0 :                     for (track_idx_t testTrack : testTracks) {
     165            0 :                         if (staffTrack <= testTrack && testTrack < staffTrack + VOICES && muse::contains(dstTrackList, testTrack)) {
     166            0 :                             hasVoice = true;
     167              :                             // voice is simply exchangeable now (deal directly)
     168            0 :                             score->undo(new ExchangeVoicesInMeasure(measure2, srcTrack2, testTrack, staffTrack / 4));
     169              :                         }
     170              :                     }
     171              : 
     172              :                     // only source voice is in this staff
     173            0 :                     if (!hasVoice) {
     174            0 :                         CloneVoice::cloneVoice(measure->first(), measure2->endTick(), measure2->first(), tempTrack, srcTrack2, false, true);
     175            0 :                         muse::remove(srcTrackList, srcTrack2);
     176              :                     }
     177            0 :                 }
     178              : 
     179            0 :                 for (track_idx_t dstTrack2 : dstTrackList) {
     180              :                     // don't care about other linked staves
     181            0 :                     if (!(staffTrack <= dstTrack2) || !(dstTrack2 < staffTrack + VOICES)) {
     182            0 :                         continue;
     183              :                     }
     184              : 
     185            0 :                     track_idx_t tempTrack = dstTrack;
     186            0 :                     std::vector<track_idx_t> testTracks = muse::values(tracks, tempTrack - trackDiff);
     187            0 :                     bool hasVoice = false;
     188            0 :                     for (track_idx_t testTrack : testTracks) {
     189            0 :                         if (staffTrack <= testTrack && testTrack < staffTrack + VOICES && muse::contains(srcTrackList, testTrack)) {
     190            0 :                             hasVoice = true;
     191              :                         }
     192              :                     }
     193              : 
     194              :                     // only destination voice is in this staff
     195            0 :                     if (!hasVoice) {
     196            0 :                         CloneVoice::cloneVoice(measure->first(), measure2->endTick(), measure2->first(), tempTrack, dstTrack2, false, true);
     197            0 :                         muse::remove(dstTrackList, dstTrack2);
     198              :                     }
     199            0 :                 }
     200            8 :             } else if (srcStaffTrack != staffTrack) {
     201              :                 // linked staff in same score (all voices present can be assumed)
     202            0 :                 score->undo(new ExchangeVoicesInMeasure(measure2, staffTrack + srcVoice, staffTrack + dstVoice, st->idx()));
     203              :             }
     204            8 :         }
     205              :     }
     206              : 
     207              :     // make sure voice 0 is complete
     208              : 
     209            8 :     if (srcVoice == 0 || dstVoice == 0) {
     210           16 :         for (staff_idx_t staffIdx = srcStaff; staffIdx < dstStaff; ++staffIdx) {
     211              :             // check for complete timeline of voice 0
     212            8 :             Fraction ctick  = measure->tick();
     213            8 :             track_idx_t track = staffIdx * VOICES;
     214           36 :             for (Segment* s = measure->first(SegmentType::ChordRest); s; s = s->next(SegmentType::ChordRest)) {
     215           28 :                 ChordRest* cr = toChordRest(s->element(track));
     216           28 :                 if (!cr) {
     217           19 :                     continue;
     218              :                 }
     219            9 :                 if (cr->isRest()) {
     220            4 :                     Rest* r = toRest(cr);
     221            4 :                     if (r->isGap()) {
     222            1 :                         r->undoChangeProperty(Pid::GAP, false);
     223              :                     }
     224              :                 }
     225            9 :                 if (ctick < s->tick()) {
     226            0 :                     score->setRest(ctick, track, s->tick() - ctick, false, 0); // fill gap
     227              :                 }
     228            9 :                 ctick = cr->endTick();
     229              :             }
     230            8 :             Fraction etick = measure->endTick();
     231            8 :             if (ctick < etick) {
     232            5 :                 score->setRest(ctick, track, etick - ctick, false, 0); // fill gap
     233              :             }
     234              :         }
     235              :     }
     236            8 : }
        

Generated by: LCOV version 2.0-1