LCOV - code coverage report
Current view: top level - src/engraving/editing - clonevoice.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 69.5 % 210 146
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) 2025 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 "clonevoice.h"
      24              : 
      25              : #include "../dom/beam.h"
      26              : #include "../dom/chord.h"
      27              : #include "../dom/factory.h"
      28              : #include "../dom/interval.h"
      29              : #include "../dom/linkedobjects.h"
      30              : #include "../dom/measure.h"
      31              : #include "../dom/note.h"
      32              : #include "../dom/rest.h"
      33              : #include "../dom/score.h"
      34              : #include "../dom/segment.h"
      35              : #include "../dom/spanner.h"
      36              : #include "../dom/staff.h"
      37              : #include "../dom/tiemap.h"
      38              : #include "../dom/tremolotwochord.h"
      39              : #include "../dom/tupletmap.h"
      40              : 
      41              : #include "transpose.h"
      42              : 
      43              : using namespace mu::engraving;
      44              : 
      45           11 : static void doCloneVoice(Score* destScore, track_idx_t srcTrack, track_idx_t dstTrack, Segment* sourceSeg, const Fraction& lTick,
      46              :                          bool link = true)
      47              : {
      48           11 :     Score* sourceScore = sourceSeg->score();
      49           11 :     Fraction start = sourceSeg->tick();
      50           11 :     TieMap tieMap;
      51           11 :     TupletMap tupletMap;      // tuplets cannot cross measure boundaries
      52           11 :     TremoloTwoChord* tremolo = nullptr;
      53              : 
      54          166 :     auto maybeLinkedClone = [link](EngravingItem* item) -> EngravingItem* {
      55              :         // If we want to maintain the link (exchange voice) create a linked clone
      56              :         // If we want new, unlinked elements (implode/explode) create a clone
      57          166 :         if (link) {
      58            0 :             return item->linkedClone();
      59              :         } else {
      60          166 :             return item->clone();
      61              :         }
      62           11 :     };
      63              : 
      64          196 :     for (Segment* oseg = sourceSeg; oseg && oseg->tick() < lTick; oseg = oseg->next1()) {
      65          185 :         Segment* ns = 0;            //create segment later, on demand
      66          185 :         Measure* dm = destScore->tick2measure(oseg->tick());
      67              : 
      68          185 :         EngravingItem* oe = oseg->element(srcTrack);
      69              : 
      70          185 :         if (oe && !oe->generated() && oe->isChordRest()) {
      71          132 :             ChordRest* ocr = toChordRest(oe);
      72          132 :             ChordRest* ncr = toChordRest(maybeLinkedClone(ocr));
      73          132 :             ncr->setScore(destScore);
      74          132 :             ncr->setTrack(dstTrack);
      75              : 
      76              :             //Don't clone gaps to a first voice
      77          132 :             if (!(ncr->track() % VOICES) && ncr->isRest()) {
      78            7 :                 toRest(ncr)->setGap(false);
      79              :             }
      80              : 
      81              :             //Handle beams
      82          132 :             if (ocr->beam() && !ocr->beam()->empty() && ocr->beam()->elements().front() == ocr) {
      83            6 :                 Beam* nb = ocr->beam()->clone();
      84            6 :                 nb->clear();
      85            6 :                 nb->setTrack(dstTrack);
      86            6 :                 nb->setScore(destScore);
      87            6 :                 nb->add(ncr);
      88            6 :                 ncr->setBeam(nb);
      89              :             }
      90              : 
      91              :             // clone Tuplets
      92          132 :             Tuplet* ot = ocr->tuplet();
      93          132 :             if (ot) {
      94           21 :                 ot->setTrack(srcTrack);
      95           21 :                 Tuplet* nt = tupletMap.findNew(ot);
      96           21 :                 if (nt == 0) {
      97            7 :                     nt = toTuplet(maybeLinkedClone(ot));
      98            7 :                     nt->setTrack(dstTrack);
      99            7 :                     nt->setOwnershipParent(dm);
     100            7 :                     tupletMap.add(ot, nt);
     101              : 
     102            7 :                     Tuplet* nt1 = nt;
     103            7 :                     while (ot->tuplet()) {
     104            0 :                         Tuplet* nt2 = tupletMap.findNew(ot->tuplet());
     105            0 :                         if (nt2 == 0) {
     106            0 :                             nt2 = toTuplet(maybeLinkedClone(ot->tuplet()));
     107            0 :                             nt2->setTrack(dstTrack);
     108            0 :                             nt2->setOwnershipParent(dm);
     109            0 :                             tupletMap.add(ot->tuplet(), nt2);
     110              :                         }
     111            0 :                         nt1->setTuplet(nt2);
     112            0 :                         ot = ot->tuplet();
     113            0 :                         nt1 = nt2;
     114              :                     }
     115              :                 }
     116           21 :                 ncr->setTuplet(nt);
     117              :             }
     118              : 
     119              :             // clone additional settings
     120          132 :             if (oe->isRest()) {
     121           21 :                 Rest* ore = toRest(ocr);
     122              :                 // If we would clone a full measure rest just don't clone this rest
     123           21 :                 if (ore->isFullMeasureRest() && (dstTrack % VOICES)) {
     124            0 :                     continue;
     125              :                 }
     126              :             }
     127              : 
     128          111 :             auto cloneChord = [&](Chord* oldChord, Chord* newChord) {
     129          111 :                 size_t n = oldChord->notes().size();
     130          222 :                 for (size_t i = 0; i < n; ++i) {
     131          111 :                     Note* on = oldChord->notes().at(i);
     132          111 :                     Note* nn = newChord->notes().at(i);
     133          111 :                     staff_idx_t idx = track2staff(dstTrack);
     134          111 :                     Fraction tick = oseg->tick();
     135          111 :                     Interval v = destScore->staff(idx) ? destScore->staff(idx)->transpose(tick) : Interval();
     136          111 :                     nn->setTpc1(on->tpc1());
     137          111 :                     if (v.isZero()) {
     138           80 :                         nn->setTpc2(on->tpc1());
     139              :                     } else {
     140           31 :                         v.flip();
     141           31 :                         nn->setTpc2(Transpose::transposeTpc(nn->tpc1(), v, true));
     142              :                     }
     143              : 
     144          111 :                     if (on->tieFor()) {
     145            9 :                         Tie* tie = toTie(maybeLinkedClone(on->tieFor()));
     146            9 :                         tie->setScore(destScore);
     147            9 :                         nn->setTieFor(tie);
     148            9 :                         tie->setStartNote(nn);
     149            9 :                         tie->setTrack(nn->track());
     150            9 :                         tie->setEndNote(nn);
     151            9 :                         tieMap.add(on->tieFor(), tie);
     152              :                     }
     153          111 :                     if (on->tieBack()) {
     154            9 :                         Tie* tie = tieMap.findNew(on->tieBack());
     155            9 :                         if (tie) {
     156            9 :                             nn->setTieBack(tie);
     157            9 :                             tie->setEndNote(nn);
     158              :                         } else {
     159            0 :                             LOGD("cloneVoices: cannot find tie");
     160              :                         }
     161              :                     }
     162              :                     // add back spanners (going back from end to start spanner element
     163              :                     // makes sure the 'other' spanner anchor element is already set up)
     164              :                     // 'on' is the old spanner end note and 'nn' is the new spanner end note
     165          111 :                     for (Spanner* oldSp : on->spannerBack()) {
     166            0 :                         Note* newStart = Spanner::startElementFromSpanner(oldSp, nn);
     167            0 :                         if (newStart) {
     168            0 :                             Spanner* newSp = toSpanner(maybeLinkedClone(oldSp));
     169            0 :                             newSp->setNoteSpan(newStart, nn);
     170            0 :                             destScore->doUndoAddElement(newSp);
     171              :                         } else {
     172            0 :                             LOGD("cloneVoices: cannot find spanner start note");
     173              :                         }
     174              :                     }
     175              :                 }
     176              :                 // two note tremolo
     177          111 :                 if (oldChord->tremoloTwoChord()) {
     178            0 :                     if (oldChord == oldChord->tremoloTwoChord()->chord1()) {
     179            0 :                         if (tremolo) {
     180            0 :                             LOGD("unconnected two note tremolo");
     181              :                         }
     182            0 :                         tremolo = item_cast<TremoloTwoChord*>(maybeLinkedClone(oldChord->tremoloTwoChord()));
     183            0 :                         tremolo->setScore(newChord->score());
     184            0 :                         tremolo->setOwnershipParent(newChord);
     185            0 :                         tremolo->setTrack(newChord->track());
     186            0 :                         tremolo->setChords(newChord, nullptr);
     187            0 :                         newChord->setTremoloTwoChord(tremolo);
     188            0 :                     } else if (oldChord == oldChord->tremoloTwoChord()->chord2()) {
     189            0 :                         if (!tremolo) {
     190            0 :                             LOGD("first note for two note tremolo missing");
     191              :                         } else {
     192            0 :                             tremolo->setChords(tremolo->chord1(), newChord);
     193            0 :                             newChord->setTremoloTwoChord(tremolo);
     194              :                         }
     195              :                     } else {
     196            0 :                         LOGD("inconsistent two note tremolo");
     197              :                     }
     198              :                 }
     199          111 :             };
     200          132 :             if (oe->isChord()) {
     201          111 :                 cloneChord(toChord(ocr), toChord(ncr));
     202          111 :                 for (size_t i = 0; i < toChord(ocr)->graceNotes().size(); ++i) {
     203            0 :                     Chord* ogc = toChord(ocr)->graceNotes().at(i);
     204            0 :                     Chord* ngc = toChord(ncr)->graceNotes().at(i);
     205            0 :                     cloneChord(ogc, ngc);
     206              :                 }
     207              :             }
     208              : 
     209              :             // Add element
     210          132 :             if (link) {
     211              :                 // To current staff only (exchange voice)
     212            0 :                 if (!ns) {
     213            0 :                     ns = dm->undoGetSegment(oseg->segmentType(), oseg->tick());
     214              :                 }
     215            0 :                 ncr->setOwnershipParent(ns);
     216            0 :                 destScore->doUndoAddElement(ncr);
     217              :             } else {
     218              :                 // To all linked staves (implode/explode)
     219          132 :                 destScore->undoAddCR(ncr, dm, oseg->tick());
     220              :             }
     221              :         }
     222              : 
     223          185 :         Segment* tst = dm->segments().firstCRSegment();
     224          185 :         if (srcTrack % VOICES && !(dstTrack % VOICES) && (!tst || (!tst->element(dstTrack)))) {
     225            9 :             Rest* rest = Factory::createRest(destScore->dummy());
     226            9 :             rest->setTicks(dm->ticks());
     227            9 :             rest->setDurationType(DurationType::V_MEASURE);
     228            9 :             rest->setTrack(dstTrack);
     229            9 :             if (link) {
     230            0 :                 Segment* segment = dm->undoGetSegment(SegmentType::ChordRest, dm->tick());
     231            0 :                 rest->setOwnershipParent(segment);
     232            0 :                 destScore->doUndoAddElement(rest);
     233              :             } else {
     234            9 :                 destScore->undoAddCR(rest, dm, dm->tick());
     235              :             }
     236              :         }
     237              : 
     238          185 :         const std::vector<EngravingItem*> annotations = oseg->annotations();
     239          241 :         for (EngravingItem* annotation : annotations) {
     240           56 :             if (!annotation->elementAppliesToTrack(srcTrack)) {
     241           40 :                 continue;
     242              :             }
     243              : 
     244           16 :             EngravingItem* newAnnotation = maybeLinkedClone(annotation);
     245           16 :             newAnnotation->setTrack(dstTrack);
     246              : 
     247           16 :             if (!ns) {
     248           13 :                 ns = dm->undoGetSegment(oseg->segmentType(), oseg->tick());
     249              :             }
     250           16 :             newAnnotation->setOwnershipParent(ns);
     251              : 
     252              :             // Add element
     253           16 :             if (link) {
     254              :                 // To current staff only (exchange voice)
     255            0 :                 destScore->doUndoAddElement(newAnnotation);
     256              :             } else {
     257              :                 // To all linked staves (implode/explode)
     258           16 :                 destScore->undoAddElement(newAnnotation);
     259              :             }
     260              :         }
     261          185 :     }
     262              : 
     263              :     // Find and add corresponding slurs and hairpins
     264           13 :     static const std::set<ElementType> SPANNERS_TO_COPY { ElementType::SLUR, ElementType::HAMMER_ON_PULL_OFF, ElementType::HAIRPIN };
     265           11 :     auto spanners = sourceScore->spannerMap().findOverlapping(start.ticks(), lTick.ticks());
     266           24 :     for (auto i = spanners.begin(); i < spanners.end(); i++) {
     267           13 :         Spanner* sp      = i->value;
     268           13 :         Fraction spStart = sp->tick();
     269           13 :         Fraction spEnd = spStart + sp->ticks();
     270              : 
     271           13 :         if (muse::contains(SPANNERS_TO_COPY, sp->type()) && (spStart >= start && spEnd < lTick)) {
     272           13 :             if (!sp->elementAppliesToTrack(srcTrack)) {
     273           11 :                 continue;
     274              :             }
     275            2 :             Spanner* ns = toSpanner(maybeLinkedClone(sp));
     276              : 
     277            2 :             ns->setScore(destScore);
     278            2 :             ns->setOwnershipParent(0);
     279            2 :             ns->setTrack(dstTrack);
     280            2 :             ns->setTrack2(dstTrack);
     281              : 
     282              :             // set start/end element for slur
     283            2 :             ChordRest* cr1 = sp->startCR();
     284            2 :             ChordRest* cr2 = sp->endCR();
     285              : 
     286            2 :             ns->setStartElement(0);
     287            2 :             ns->setEndElement(0);
     288            2 :             if (cr1 && cr1->links()) {
     289            0 :                 for (EngravingObject* e : *cr1->links()) {
     290            0 :                     ChordRest* cr = toChordRest(e);
     291            0 :                     if (cr == cr1) {
     292            0 :                         continue;
     293              :                     }
     294            0 :                     if ((cr->score() == destScore) && (cr->tick() == ns->tick()) && cr->track() == dstTrack) {
     295            0 :                         ns->setStartElement(cr);
     296            0 :                         break;
     297              :                     }
     298              :                 }
     299              :             }
     300            2 :             if (cr2 && cr2->links()) {
     301            0 :                 for (EngravingObject* e : *cr2->links()) {
     302            0 :                     ChordRest* cr = toChordRest(e);
     303            0 :                     if (cr == cr2) {
     304            0 :                         continue;
     305              :                     }
     306            0 :                     if ((cr->score() == destScore) && (cr->tick() == ns->tick2()) && cr->track() == dstTrack) {
     307            0 :                         ns->setEndElement(cr);
     308            0 :                         break;
     309              :                     }
     310              :                 }
     311              :             }
     312            2 :             if (link) {
     313            0 :                 destScore->doUndoAddElement(ns);
     314              :             } else {
     315            2 :                 destScore->undoAddElement(ns);
     316              :             }
     317              :         }
     318              :     }
     319           11 : }
     320              : 
     321           11 : void CloneVoice::cloneVoice(
     322              :     Segment* sourceSeg, // first source segment
     323              :     const Fraction& lTick, // last tick to clone
     324              :     Segment* destSeg, // first destination segment
     325              :     track_idx_t strack,
     326              :     track_idx_t dtrack,
     327              :     bool deleteSource,
     328              :     bool link // if true  add elements in destination segment only
     329              :               // if false add elements in every linked staff
     330              :     )
     331              : {
     332           11 :     Score* s = destSeg->score();
     333           11 :     Fraction ticks = destSeg->tick() + lTick - sourceSeg->tick();
     334              : 
     335              :     // Clear destination voice (in case of not linked and otrack = -1 we would delete our source
     336          196 :     for (Segment* seg = destSeg; seg && seg->tick() < ticks; seg = seg->next1()) {
     337          185 :         EngravingItem* el = seg->element(dtrack);
     338          185 :         if (el && el->isChordRest()) {
     339           16 :             link ? s->doUndoRemoveElement(el) : s->undoRemoveElement(el);
     340              :         }
     341              :     }
     342              : 
     343           11 :     doCloneVoice(s, strack, dtrack, sourceSeg, ticks, link);
     344              : 
     345           11 :     if (deleteSource) {
     346            3 :         auto spanners = s->spannerMap().findOverlapping(sourceSeg->tick().ticks(), lTick.ticks());
     347            3 :         for (auto i = spanners.begin(); i < spanners.end(); i++) {
     348            0 :             Spanner* sp = i->value;
     349            0 :             if (sp->isSlur() && (sp->track() == strack || sp->track2() == strack)) {
     350            0 :                 link ? s->doUndoRemoveElement(sp) : s->undoRemoveElement(sp);
     351              :             }
     352              :         }
     353           48 :         for (Segment* seg = destSeg; seg && seg->tick() < ticks; seg = seg->next1()) {
     354           45 :             EngravingItem* el = seg->element(strack);
     355           45 :             if (el && el->isChordRest()) {
     356           36 :                 link ? s->doUndoRemoveElement(el) : s->undoRemoveElement(el);
     357              :             }
     358              : 
     359           45 :             const std::vector<EngravingItem*> annotations = seg->annotations();
     360           93 :             for (EngravingItem* annotation : annotations) {
     361           48 :                 if (annotation && annotation->track() == strack) {
     362            6 :                     if (annotation->hasVoiceAssignmentProperties()) {
     363            3 :                         VoiceAssignment voiceAssignment = annotation->getProperty(Pid::VOICE_ASSIGNMENT).value<VoiceAssignment>();
     364            3 :                         if (voiceAssignment != VoiceAssignment::CURRENT_VOICE_ONLY) {
     365            0 :                             continue;
     366              :                         }
     367              :                     }
     368            6 :                     link ? s->doUndoRemoveElement(annotation) : s->undoRemoveElement(annotation);
     369              :                 }
     370              :             }
     371           45 :         }
     372              :         // Set rests if first voice in a staff
     373            3 :         if (!(strack % VOICES)) {
     374              :             // TODO: take `link` into account?
     375            0 :             s->setRest(destSeg->tick(), strack, ticks, false, nullptr);
     376              :         }
     377            3 :     }
     378           11 : }
        

Generated by: LCOV version 2.0-1