LCOV - code coverage report
Current view: top level - src/engraving/playback/renderers - noterenderer.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 85.4 % 157 134
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) 2024 MuseScore Limited and others
       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 "noterenderer.h"
      24              : 
      25              : #include "dom/arpeggio.h"
      26              : #include "dom/note.h"
      27              : #include "dom/staff.h"
      28              : #include "dom/swing.h"
      29              : #include "dom/tremolosinglechord.h"
      30              : #include "dom/tremolotwochord.h"
      31              : 
      32              : #include "glissandosrenderer.h"
      33              : 
      34              : #include "playback/metaparsers/chordarticulationsparser.h"
      35              : #include "playback/metaparsers/notearticulationsparser.h"
      36              : 
      37              : #include "playback/utils/repeatutils.h"
      38              : 
      39              : using namespace mu::engraving;
      40              : using namespace muse;
      41              : using namespace muse::mpe;
      42              : 
      43         1550 : bool NoteRenderer::shouldRender(const Note* note, const RenderingContext& ctx, const muse::mpe::ArticulationMap& articulations)
      44              : {
      45         1550 :     if (!note->play()) {
      46            0 :         return false;
      47              :     }
      48              : 
      49         1550 :     const Tie* tie = note->tieBack();
      50              : 
      51         1550 :     if (tie && tie->playSpanner()) {
      52          155 :         const Note* startNote = tie->startNote();
      53          155 :         const Note* endNote = tie->endNote();
      54          155 :         if (!startNote || !endNote) {
      55            0 :             return false;
      56              :         }
      57              : 
      58          155 :         if (tie->isPartialTie()) {
      59              :             // Play the partially tied note if there is no outgoing note in the previous repeat
      60           25 :             if (!findOutgoingNoteInPreviousRepeat(note, ctx).isValid()) {
      61           11 :                 return true;
      62              :             }
      63              :         }
      64              : 
      65          144 :         const Chord* startChord = startNote->chord();
      66          144 :         const Chord* endChord = endNote->chord();
      67              : 
      68              :         // Helper function to check if tremolo should play
      69          228 :         auto shouldTremoloPlay = [](const Chord* chord) -> bool {
      70          228 :             if (chord->tremoloType() == TremoloType::INVALID_TREMOLO) {
      71          149 :                 return false;
      72              :             }
      73              : 
      74           79 :             const TremoloSingleChord* singleTremolo = chord->tremoloSingleChord();
      75           79 :             if (singleTremolo) {
      76           79 :                 return singleTremolo->playTremolo();
      77              :             }
      78              : 
      79            0 :             const TremoloTwoChord* twoTremolo = chord->tremoloTwoChord();
      80            0 :             if (twoTremolo) {
      81            0 :                 return twoTremolo->playTremolo();
      82              :             }
      83              : 
      84            0 :             return false;
      85              :         };
      86              : 
      87              :         // Only render tied notes if tremolo is actually enabled for playback
      88          144 :         if (shouldTremoloPlay(startChord) || shouldTremoloPlay(endChord)) {
      89           77 :             return true;
      90              :         }
      91              : 
      92           67 :         if (startChord->arpeggio() && endChord->arpeggio()) {
      93            4 :             return false;
      94              :         }
      95              : 
      96              :         //!Note Checking whether the tied note has any multi-note articulation attached
      97              :         //!     If so, we can't ignore such note
      98          124 :         for (const auto& pair : articulations) {
      99           66 :             if (muse::mpe::isMultiNoteArticulation(pair.first) && !muse::mpe::isRangedArticulation(pair.first)) {
     100            5 :                 return true;
     101              :             }
     102              :         }
     103              : 
     104          116 :         const auto& intervals = startChord->score()->spannerMap().findOverlapping(startChord->tick().ticks(),
     105           58 :                                                                                   startChord->endTick().ticks(),
     106              :                                                                                   /*excludeCollisions*/ true);
     107           72 :         for (const auto& interval : intervals) {
     108           15 :             const Spanner* sp = interval.value;
     109           15 :             if (sp->isTrill() && sp->playSpanner() && sp->endElement() == startChord) {
     110            1 :                 return true;
     111              :             }
     112              :         }
     113              : 
     114           57 :         return false;
     115              :     }
     116              : 
     117         1395 :     return true;
     118              : }
     119              : 
     120         1087 : void NoteRenderer::render(const Note* note, const RenderingContext& ctx, mpe::PlaybackEventList& result)
     121              : {
     122         1087 :     IF_ASSERT_FAILED(note) {
     123           32 :         return;
     124              :     }
     125              : 
     126         1087 :     NominalNoteCtx noteCtx = buildNominalNoteCtx(note, ctx);
     127         1087 :     if (!shouldRender(note, ctx, noteCtx.articulations)) {
     128           25 :         return;
     129              :     }
     130              : 
     131         1062 :     const Tie* tieFor = note->tieFor();
     132         1062 :     if (tieFor && tieFor->playSpanner()) {
     133           35 :         if (tieFor->isPartialTie()) {
     134            4 :             renderPartialTie(note, noteCtx);
     135           31 :         } else if (!tieFor->isLaissezVib()) {
     136           31 :             renderNormalTie(note, noteCtx);
     137              :         }
     138              :     }
     139              : 
     140         1062 :     applySwingIfNeed(note, noteCtx);
     141              : 
     142         1062 :     if (noteCtx.articulations.contains(ArticulationType::DiscreteGlissando)) {
     143            4 :         GlissandosRenderer::renderDiscreteGlissando(note, noteCtx, result);
     144            4 :         return;
     145              :     }
     146              : 
     147         1058 :     if (noteCtx.articulations.contains(ArticulationType::ContinuousGlissando)) {
     148            3 :         GlissandosRenderer::renderContinuousGlissando(note, noteCtx, result);
     149            3 :         return;
     150              :     }
     151              : 
     152         1055 :     mpe::NoteEvent ev = buildNoteEvent(noteCtx);
     153              : 
     154         1055 :     if (ev.arrangementCtx().actualTimestamp >= 0) {
     155         1055 :         result.emplace_back(std::move(ev));
     156              :     } else {
     157            0 :         ArrangementContext arrCtx = ev.arrangementCtx();
     158            0 :         arrCtx.actualDuration = arrCtx.actualDuration + arrCtx.actualTimestamp;
     159            0 :         arrCtx.actualTimestamp = 0;
     160              : 
     161            0 :         PitchContext pitchCtx = ev.pitchCtx();
     162            0 :         ExpressionContext expCtx = ev.expressionCtx();
     163              : 
     164            0 :         result.emplace_back(mpe::NoteEvent(std::move(arrCtx), std::move(pitchCtx), std::move(expCtx)));
     165            0 :     }
     166         2142 : }
     167              : 
     168            7 : void NoteRenderer::renderPartialTie(const Note* outgoingNote, NominalNoteCtx& outgoingNoteCtx)
     169              : {
     170            7 :     const RenderingContext& outgoingChordCtx = outgoingNoteCtx.chordCtx;
     171            7 :     const PartiallyTiedNoteInfo incomingNoteInfo = findIncomingNoteInNextRepeat(outgoingNote, outgoingChordCtx);
     172            7 :     if (!incomingNoteInfo.isValid()) {
     173            2 :         return;
     174              :     }
     175              : 
     176            5 :     const int incomingNotePositionTickOffset = incomingNoteInfo.repeat->utick - incomingNoteInfo.repeat->tick;
     177           15 :     RenderingContext incomingChordCtx = buildRenderingCtx(incomingNoteInfo.note->chord(), incomingNotePositionTickOffset,
     178           10 :                                                           outgoingChordCtx.profile, outgoingChordCtx.playbackCtx);
     179              : 
     180            5 :     ChordArticulationsParser::buildChordArticulationMap(incomingNoteInfo.note->chord(), incomingChordCtx,
     181              :                                                         incomingChordCtx.commonArticulations);
     182              : 
     183            5 :     NominalNoteCtx incomingNoteCtx = buildNominalNoteCtx(incomingNoteInfo.note, incomingChordCtx);
     184              : 
     185            5 :     if (shouldRender(incomingNoteInfo.note, incomingChordCtx, incomingNoteCtx.articulations)) {
     186            0 :         return;
     187              :     }
     188              : 
     189            5 :     const Tie* tieFor = incomingNoteInfo.note->tieFor();
     190            5 :     if (tieFor && tieFor->playSpanner() && !tieFor->isLaissezVib()) {
     191            0 :         renderNormalTie(incomingNoteInfo.note, incomingNoteCtx);
     192              :     }
     193              : 
     194            5 :     addTiedNote(incomingNoteCtx, outgoingNoteCtx);
     195            5 :     updateArticulationBoundaries(outgoingNoteCtx.timestamp, outgoingNoteCtx.duration, outgoingNoteCtx.articulations);
     196            5 : }
     197              : 
     198           31 : void NoteRenderer::renderNormalTie(const Note* firstNote, NominalNoteCtx& firstNoteCtx)
     199              : {
     200           62 :     std::unordered_set<const Note*> renderedNotes { firstNote };
     201              : 
     202           31 :     const RenderingContext& firstChordCtx = firstNoteCtx.chordCtx;
     203           31 :     const Tie* currTie = firstNote->tieFor();
     204              : 
     205           62 :     while (currTie && currTie->playSpanner()) {
     206           38 :         const Note* currNote = currTie->endNote();
     207           38 :         if (!currNote || !currNote->play()) {
     208            7 :             break;
     209              :         }
     210              : 
     211           38 :         if (muse::contains(renderedNotes, currNote)) {
     212            0 :             break; // prevents infinite loop
     213              :         }
     214              : 
     215           38 :         if (!notesInSameRepeat(firstChordCtx.score, firstNote, currNote, firstChordCtx.positionTickOffset)) {
     216            4 :             const TieJumpPointList* jumpPoints = firstNote->tieJumpPoints();
     217            4 :             if (jumpPoints && !jumpPoints->empty()) {
     218            3 :                 renderPartialTie(firstNote, firstNoteCtx);
     219              :             }
     220            4 :             break;
     221              :         }
     222              : 
     223           34 :         const Chord* chord = currNote->chord();
     224           34 :         if (!chord) {
     225            0 :             break;
     226              :         }
     227              : 
     228           68 :         RenderingContext currChordCtx = buildRenderingCtx(chord, firstChordCtx.positionTickOffset,
     229           34 :                                                           firstChordCtx.profile, firstChordCtx.playbackCtx);
     230           34 :         ChordArticulationsParser::buildChordArticulationMap(chord, currChordCtx, currChordCtx.commonArticulations);
     231              : 
     232           34 :         const NominalNoteCtx currNoteCtx = buildNominalNoteCtx(currNote, currChordCtx);
     233           34 :         if (shouldRender(currNote, currChordCtx, currNoteCtx.articulations)) {
     234            3 :             if (currNoteCtx.articulations.contains(ArticulationType::DiscreteGlissando)) {
     235            1 :                 firstNoteCtx.duration += GlissandosRenderer::discreteGlissandoStepDuration(currNote, currNoteCtx.duration);
     236              :             }
     237              : 
     238            3 :             break;
     239              :         }
     240              : 
     241           31 :         addTiedNote(currNoteCtx, firstNoteCtx);
     242              : 
     243           31 :         currTie = currNote->tieFor();
     244           31 :         renderedNotes.insert(currNote);
     245           37 :     }
     246              : 
     247           31 :     if (firstNoteCtx.articulations.size() > 1) {
     248            4 :         firstNoteCtx.articulations.erase(mpe::ArticulationType::Standard);
     249              :     }
     250              : 
     251           31 :     updateArticulationBoundaries(firstNoteCtx.timestamp, firstNoteCtx.duration, firstNoteCtx.articulations);
     252           31 : }
     253              : 
     254           36 : void NoteRenderer::addTiedNote(const NominalNoteCtx& tiedNoteCtx, NominalNoteCtx& firstNoteCtx)
     255              : {
     256           36 :     if (tiedNoteCtx.articulations.size() == 1) {
     257           33 :         if (tiedNoteCtx.articulations.begin()->first == mpe::ArticulationType::Standard) {
     258           25 :             firstNoteCtx.duration += tiedNoteCtx.duration;
     259           25 :             return;
     260              :         }
     261              :     }
     262              : 
     263           11 :     const float avgDurationFactor = percentageToFactor(tiedNoteCtx.articulations.averageDurationFactor());
     264           11 :     firstNoteCtx.duration += tiedNoteCtx.duration * avgDurationFactor;
     265              : 
     266              :     // Ignore these articulations so we won't re-apply them to the total duration
     267              :     static const ArticulationTypeSet ARTICULATION_TO_IGNORE_TYPES {
     268              :         ArticulationType::Staccato,
     269              :         ArticulationType::Staccatissimo,
     270           13 :     };
     271              : 
     272           25 :     for (const auto& pair : tiedNoteCtx.articulations) {
     273           14 :         if (!muse::contains(ARTICULATION_TO_IGNORE_TYPES, pair.first)) {
     274           14 :             firstNoteCtx.articulations.insert(pair);
     275              :         }
     276              :     }
     277              : }
     278              : 
     279           36 : void NoteRenderer::updateArticulationBoundaries(const timestamp_t noteTimestamp, const duration_t noteDuration,
     280              :                                                 ArticulationMap& articulations)
     281              : {
     282           36 :     const timestamp_t noteTimestampTo = noteTimestamp + noteDuration;
     283           36 :     IF_ASSERT_FAILED(noteTimestampTo > 0) {
     284            0 :         return;
     285              :     }
     286              : 
     287           75 :     for (const auto& pair : articulations) {
     288           39 :         const ArticulationAppliedData& articulation = pair.second;
     289              : 
     290           39 :         const duration_percentage_t occupiedFrom = mpe::occupiedPercentage(articulation.meta.timestamp,
     291              :                                                                            noteTimestampTo);
     292           39 :         const duration_percentage_t occupiedTo = mpe::occupiedPercentage(articulation.meta.timestamp + articulation.meta.overallDuration,
     293              :                                                                          noteTimestampTo);
     294              : 
     295           39 :         articulations.updateOccupiedRange(pair.first, occupiedFrom, occupiedTo);
     296              :     }
     297              : }
     298              : 
     299         1062 : void NoteRenderer::applySwingIfNeed(const Note* note, NominalNoteCtx& noteCtx)
     300              : {
     301         1062 :     const Chord* chord = note->chord();
     302         1062 :     if (!chord || chord->tuplet()) {
     303         1062 :         return;
     304              :     }
     305              : 
     306         1062 :     const SwingParameters swing = chord->staff()->swing(chord->tick());
     307         1062 :     if (!swing.isOn()) {
     308         1062 :         return;
     309              :     }
     310              : 
     311              :     //! NOTE: Swing must be applied to the "raw" note duration, but not to the additional duration (e.g, from a tied note)
     312            0 :     const Swing::ChordDurationAdjustment swingDurationAdjustment = Swing::applySwing(chord, swing);
     313            0 :     const duration_t additionalDuration = noteCtx.duration - noteCtx.chordCtx.nominalDuration;
     314            0 :     noteCtx.timestamp = noteCtx.timestamp + noteCtx.chordCtx.nominalDuration * swingDurationAdjustment.remainingDurationMultiplier;
     315            0 :     noteCtx.duration = noteCtx.chordCtx.nominalDuration * swingDurationAdjustment.durationMultiplier + additionalDuration;
     316              : }
     317              : 
     318         1126 : NominalNoteCtx NoteRenderer::buildNominalNoteCtx(const Note* note, const RenderingContext& ctx)
     319              : {
     320         1126 :     NominalNoteCtx noteCtx(note, ctx);
     321         1126 :     NoteArticulationsParser::buildNoteArticulationMap(note, ctx, noteCtx.articulations);
     322              : 
     323         1126 :     return noteCtx;
     324            0 : }
        

Generated by: LCOV version 2.0-1