LCOV - code coverage report
Current view: top level - src/engraving/playback/renderers - bendsrenderer.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 93.0 % 214 199
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 "bendsrenderer.h"
      24              : 
      25              : #include "noterenderer.h"
      26              : #include "gracechordcontext.h"
      27              : 
      28              : #include "playback/metaparsers/chordarticulationsparser.h"
      29              : #include "playback/utils/expressionutils.h"
      30              : 
      31              : #include "dom/note.h"
      32              : #include "dom/guitarbend.h"
      33              : 
      34              : using namespace mu::engraving;
      35              : using namespace muse;
      36              : 
      37              : static constexpr bool IGNORE_UNPLAYABLE = false;
      38              : 
      39           36 : static const Chord* principalChord(const Chord* chord)
      40              : {
      41           36 :     if (chord->isGrace()) {
      42            8 :         const EngravingObject* parent = chord->ownershipParent();
      43            8 :         if (parent && parent->isChord()) {
      44            8 :             return toChord(parent);
      45              :         }
      46              :     }
      47              : 
      48           28 :     return chord;
      49              : }
      50              : 
      51           29 : static const GuitarBend* currentBend(const Note* currNote)
      52              : {
      53           29 :     const GuitarBend* diveBack = currNote->diveBack();
      54           29 :     if (diveBack && diveBack->playSpanner() && diveBack->bendType() == GuitarBendType::SCOOP) {
      55            1 :         return diveBack;
      56              :     }
      57              : 
      58           28 :     return currNote->bendFor();
      59              : }
      60              : 
      61           15 : static const Note* nextNote(const Note* currNote, const GuitarBend* currBend)
      62              : {
      63           15 :     return currBend && currBend->endNote() != currNote ? currBend->endNote() : nullptr;
      64              : }
      65              : 
      66            4 : static mpe::pitch_level_t pitchOffset(const GuitarBend* bend)
      67              : {
      68            4 :     return mpe::PITCH_LEVEL_STEP / 2 * bend->bendAmountInQuarterTones();
      69              : }
      70              : 
      71            6 : bool BendsRenderer::isMultibendPart(const Note* note)
      72              : {
      73            6 :     if (!note->tieFor() && !note->tieBack()) {
      74            4 :         return note->bendFor() || note->bendBack();
      75              :     }
      76              : 
      77            2 :     const Note* currNote = note->firstTiedNote(IGNORE_UNPLAYABLE);
      78            2 :     std::unordered_set<const Note*> visited;
      79              : 
      80            3 :     while (currNote && currNote->play()) {
      81            3 :         if (!visited.insert(currNote).second) {
      82            0 :             break;
      83              :         }
      84              : 
      85            3 :         if (currNote->bendFor() || note->bendBack()) {
      86            2 :             return true;
      87              :         }
      88              : 
      89            1 :         const Tie* tieFor = currNote->tieFor();
      90            1 :         if (!tieFor || !tieFor->playSpanner()) {
      91            0 :             return false;
      92              :         }
      93              : 
      94            1 :         currNote = tieFor->endNote();
      95              :     }
      96              : 
      97            0 :     return false;
      98            2 : }
      99              : 
     100           15 : void BendsRenderer::render(const Note* note, const RenderingContext& ctx, mpe::PlaybackEventList& result)
     101              : {
     102           15 :     IF_ASSERT_FAILED(note) {
     103            0 :         return;
     104              :     }
     105              : 
     106              :     //! NOTE: ignore the grace note and render only the principal note
     107           15 :     if (note->isPreBendOrDiveStart()) {
     108            1 :         return;
     109              :     }
     110              : 
     111           14 :     if (const GuitarBend* bendBack = note->bendBack()) {
     112              :         static const std::unordered_set<GuitarBendType> SHOULD_RENDER_NOTE_WITH_BEND_BACK {
     113              :             GuitarBendType::PRE_BEND,
     114              :             GuitarBendType::PRE_DIVE,
     115              :             GuitarBendType::SCOOP,
     116            8 :         };
     117              : 
     118            6 :         if (!muse::contains(SHOULD_RENDER_NOTE_WITH_BEND_BACK, bendBack->bendType())) {
     119            4 :             return;
     120              :         }
     121              :     }
     122              : 
     123           10 :     if (!NoteRenderer::shouldRender(note, ctx, ctx.commonArticulations)) {
     124            0 :         return;
     125              :     }
     126              : 
     127           10 :     renderMultibend(note, ctx, result);
     128              : }
     129              : 
     130           10 : void BendsRenderer::renderMultibend(const Note* startNote, const RenderingContext& startNoteCtx,
     131              :                                     mpe::PlaybackEventList& result)
     132              : {
     133           10 :     const Note* currNote = startNote;
     134           10 :     const GuitarBend* currBend = nullptr;
     135           10 :     std::unordered_set<const Note*> visited;
     136              : 
     137           10 :     mpe::PlaybackEventList bendEvents;
     138           10 :     BendTimeFactorMap bendTimeFactorMap;
     139              : 
     140           30 :     while (currNote) {
     141           29 :         if (!visited.insert(currNote).second) {
     142            9 :             break;
     143              :         }
     144              : 
     145           29 :         RenderingContext currNoteCtx = buildRenderingContext(currNote, startNoteCtx);
     146           29 :         if (!currNote->tieBack() || !currNote->tieBack()->playSpanner()) {
     147           24 :             renderNote(currNote, currNoteCtx, bendEvents);
     148              :         }
     149              : 
     150           29 :         currBend = currentBend(currNote);
     151              : 
     152              :         // No bend on this note, but it's tied to the next one — check that note as well
     153           29 :         if (!currBend && currNote->tieFor() && currNote->tieFor()->playSpanner()) {
     154            3 :             currNote = currNote->tieFor()->endNote();
     155            3 :             continue;
     156              :         }
     157              : 
     158           26 :         if (!currBend || !currBend->playSpanner() || bendEvents.empty()) {
     159            8 :             break;
     160              :         }
     161              : 
     162           18 :         if (currBend->bendType() == GuitarBendType::SLIGHT_BEND) {
     163            1 :             renderSlightBend(currNote, currBend, currNoteCtx, bendEvents);
     164            1 :             break;
     165              :         }
     166              : 
     167           17 :         if (currBend->bendType() == GuitarBendType::DIP) {
     168            2 :             renderDip(currNote, currBend, currNoteCtx, bendEvents);
     169              : 
     170            2 :             if (currNote->tieFor() && currNote->tieFor()->playSpanner()) {
     171            2 :                 currNote = currNote->tieFor()->endNote();
     172            2 :                 continue;
     173              :             }
     174            0 :             break;
     175              :         }
     176              : 
     177           15 :         if (currBend->bendType() == GuitarBendType::SCOOP) {
     178            1 :             renderScoop(currNote, currBend, currNoteCtx, bendEvents);
     179            1 :             currBend = currNote->bendFor();
     180            1 :             currNote = nextNote(currNote, currBend);
     181            1 :             continue;
     182              :         }
     183              : 
     184           14 :         const mpe::PlaybackEvent& newEvent = bendEvents.back();
     185           14 :         if (std::holds_alternative<mpe::NoteEvent>(newEvent)) {
     186           14 :             const mpe::ArrangementContext& arrangementCtx = std::get<mpe::NoteEvent>(newEvent).arrangementCtx();
     187           14 :             const mpe::timestamp_t timestampTo = arrangementCtx.actualTimestamp + arrangementCtx.actualDuration;
     188           14 :             bendTimeFactorMap.insert_or_assign(timestampTo, timeFactors(currBend));
     189              :         }
     190              : 
     191           14 :         currNote = nextNote(currNote, currBend);
     192           29 :     }
     193              : 
     194           10 :     if (!bendEvents.empty()) {
     195           10 :         mpe::NoteEvent event = buildBendEvent(startNote, startNoteCtx, bendEvents, bendTimeFactorMap);
     196           10 :         result.emplace_back(std::move(event));
     197           10 :     }
     198           10 : }
     199              : 
     200           24 : void BendsRenderer::renderNote(const Note* note, const RenderingContext& ctx, mpe::PlaybackEventList& result)
     201              : {
     202           24 :     for (const auto& pair : ctx.commonArticulations) {
     203            7 :         if (!muse::contains(GRACE_NOTE_ARTICULATION_TYPES, pair.first)) {
     204            0 :             continue;
     205              :         }
     206              : 
     207              :         // This note is either a grace note or a principal note for other grace notes
     208            7 :         const GraceChordCtx graceCtx = GraceChordCtx::buildCtx(principalChord(note->chord()), pair.first, ctx);
     209              : 
     210            7 :         if (note->isGrace()) {
     211            4 :             renderGraceNote(note, graceCtx, result);
     212              :         } else {
     213            3 :             NoteRenderer::render(note, graceCtx.principalChordCtx, result);
     214              :         }
     215              : 
     216            7 :         return;
     217            7 :     }
     218              : 
     219           17 :     NoteRenderer::render(note, ctx, result);
     220              : }
     221              : 
     222            4 : void BendsRenderer::renderGraceNote(const Note* note, const GraceChordCtx& ctx, mpe::PlaybackEventList& result)
     223              : {
     224            5 :     for (const auto& pair : ctx.graceChordCtxList) {
     225            6 :         for (const Note* graceNote : pair.first->notes()) {
     226            5 :             if (note == graceNote) {
     227            4 :                 NoteRenderer::render(note, pair.second, result);
     228            4 :                 return;
     229              :             }
     230              :         }
     231              :     }
     232              : }
     233              : 
     234            1 : void BendsRenderer::renderSlightBend(const Note* note, const GuitarBend* bend, const RenderingContext& ctx, mpe::PlaybackEventList& result)
     235              : {
     236            1 :     NominalNoteCtx slightNoteCtx(note, ctx);
     237            1 :     slightNoteCtx.duration = 0; // aux notes have no duration
     238              : 
     239            1 :     const BendTimeFactors factors = timeFactors(bend);
     240              : 
     241              :     // Hold the origin pitch
     242            1 :     if (!RealIsNull(factors.startFactor)) {
     243            0 :         slightNoteCtx.timestamp = ctx.nominalTimestamp + ctx.nominalDuration * factors.startFactor;
     244            0 :         result.emplace_back(buildNoteEvent(slightNoteCtx));
     245              :     }
     246              : 
     247              :     // Go up
     248            1 :     slightNoteCtx.timestamp = ctx.nominalTimestamp + ctx.nominalDuration * factors.endFactor;
     249            1 :     slightNoteCtx.pitchLevel += pitchOffset(bend);
     250            1 :     result.emplace_back(buildNoteEvent(slightNoteCtx));
     251            2 : }
     252              : 
     253            2 : void BendsRenderer::renderDip(const Note* note, const GuitarBend* bend, const RenderingContext& ctx, mpe::PlaybackEventList& result)
     254              : {
     255            2 :     NominalNoteCtx dipNoteCtx(note, ctx);
     256            2 :     dipNoteCtx.duration = 0; // aux notes have no duration
     257              : 
     258            2 :     const mpe::pitch_level_t originPitchLevel = dipNoteCtx.pitchLevel;
     259            2 :     const BendTimeFactors factors = timeFactors(bend);
     260              : 
     261              :     // Hold the origin pitch
     262            2 :     dipNoteCtx.timestamp = ctx.nominalTimestamp + ctx.nominalDuration * factors.startFactor;
     263            2 :     result.emplace_back(buildNoteEvent(dipNoteCtx));
     264              : 
     265              :     // Go down / up
     266            2 :     dipNoteCtx.pitchLevel += pitchOffset(bend);
     267            2 :     dipNoteCtx.timestamp = ctx.nominalTimestamp + ctx.nominalDuration * factors.targetFactor;
     268            2 :     result.emplace_back(buildNoteEvent(dipNoteCtx));
     269              : 
     270              :     // Go back to the origin pitch
     271            2 :     if (!RealIsEqual(factors.targetFactor, factors.endFactor)) {
     272            2 :         dipNoteCtx.timestamp = ctx.nominalTimestamp + ctx.nominalDuration * factors.endFactor;
     273            2 :         dipNoteCtx.pitchLevel = originPitchLevel;
     274            2 :         result.emplace_back(buildNoteEvent(dipNoteCtx));
     275              :     }
     276            8 : }
     277              : 
     278            1 : void BendsRenderer::renderScoop(const Note* note, const GuitarBend* bend, const RenderingContext& ctx, mpe::PlaybackEventList& result)
     279              : {
     280            1 :     NominalNoteCtx scoopNoteCtx(note, ctx);
     281            1 :     scoopNoteCtx.duration = 0; // aux notes have no duration
     282              : 
     283            1 :     const mpe::pitch_level_t originPitchLevel = scoopNoteCtx.pitchLevel;
     284            1 :     const BendTimeFactors factors = timeFactors(bend);
     285              : 
     286              :     // Start with a pitch offset
     287            1 :     scoopNoteCtx.pitchLevel += pitchOffset(bend);
     288            1 :     result.emplace_back(buildNoteEvent(scoopNoteCtx));
     289              : 
     290              :     // Hold it
     291            1 :     if (!RealIsNull(factors.startFactor)) {
     292            0 :         scoopNoteCtx.timestamp = ctx.nominalTimestamp + ctx.nominalDuration * factors.startFactor;
     293            0 :         result.emplace_back(buildNoteEvent(scoopNoteCtx));
     294              :     }
     295              : 
     296              :     // Go back to the origin pitch
     297            1 :     scoopNoteCtx.timestamp = ctx.nominalTimestamp + ctx.nominalDuration * factors.endFactor;
     298            1 :     scoopNoteCtx.pitchLevel = originPitchLevel;
     299            1 :     result.emplace_back(buildNoteEvent(scoopNoteCtx));
     300            3 : }
     301              : 
     302           18 : BendsRenderer::BendTimeFactors BendsRenderer::timeFactors(const GuitarBend* bend)
     303              : {
     304           18 :     const float startFactor = std::clamp(bend->startTimeFactor(), 0.f, 1.f);
     305           18 :     const float endFactor = std::clamp(bend->endTimeFactor(), 0.f, 1.f);
     306              : 
     307           18 :     float targetFactor = endFactor;
     308           18 :     if (bend->targetTimeFactor().has_value()) {
     309            2 :         targetFactor = std::clamp(bend->targetTimeFactor().value(), 0.f, 1.f);
     310              :     }
     311              : 
     312           18 :     IF_ASSERT_FAILED(RealIsEqualOrLess(startFactor, endFactor)
     313              :                      && RealIsEqualOrLess(startFactor, targetFactor)
     314              :                      && RealIsEqualOrLess(targetFactor, endFactor)) {
     315            0 :         return BendTimeFactors { 0.f, 1.f, 1.f };
     316              :     }
     317              : 
     318           18 :     return BendTimeFactors { startFactor, targetFactor, endFactor };
     319              : }
     320              : 
     321           29 : RenderingContext BendsRenderer::buildRenderingContext(const Note* note, const RenderingContext& initialCtx)
     322              : {
     323              :     // Use the principal chord to build the context
     324           29 :     const Chord* chord = principalChord(note->chord());
     325              : 
     326           29 :     RenderingContext ctx = engraving::buildRenderingCtx(chord, initialCtx.positionTickOffset,
     327           29 :                                                         initialCtx.profile, initialCtx.playbackCtx,
     328           58 :                                                         initialCtx.commonArticulations);
     329              : 
     330           29 :     ChordArticulationsParser::buildChordArticulationMap(chord, ctx, ctx.commonArticulations);
     331              : 
     332           29 :     return ctx;
     333            0 : }
     334              : 
     335           10 : mpe::NoteEvent BendsRenderer::buildBendEvent(const Note* startNote, const RenderingContext& startNoteCtx,
     336              :                                              const mpe::PlaybackEventList& bendNoteEvents, const BendTimeFactorMap& timeFactorMap)
     337              : {
     338           10 :     NominalNoteCtx noteCtx(startNote, startNoteCtx);
     339              : 
     340           10 :     const mpe::NoteEvent& startNoteEvent = std::get<mpe::NoteEvent>(bendNoteEvents.front());
     341           10 :     noteCtx.articulations = startNoteEvent.expressionCtx().articulations;
     342           10 :     noteCtx.timestamp = startNoteEvent.arrangementCtx().actualTimestamp;
     343           10 :     noteCtx.duration = startNoteEvent.arrangementCtx().actualDuration;
     344              : 
     345           10 :     PitchOffsets pitchOffsets;
     346           10 :     pitchOffsets.reserve(bendNoteEvents.size());
     347              : 
     348           10 :     auto multibendIt = noteCtx.articulations.find(mpe::ArticulationType::Multibend);
     349           10 :     if (multibendIt != noteCtx.articulations.end()) {
     350            9 :         const mpe::ArticulationMeta& meta = multibendIt->second.meta;
     351            9 :         pitchOffsets.emplace_back(meta.timestamp, 0);
     352              :     }
     353              : 
     354           33 :     for (size_t i = 1; i < bendNoteEvents.size(); ++i) {
     355           23 :         const mpe::PlaybackEvent& event = bendNoteEvents.at(i);
     356           23 :         if (!std::holds_alternative<mpe::NoteEvent>(event)) {
     357            0 :             continue;
     358              :         }
     359              : 
     360           23 :         const mpe::NoteEvent& noteEvent = std::get<mpe::NoteEvent>(event);
     361           23 :         const mpe::ArrangementContext& arrangementCtx = noteEvent.arrangementCtx();
     362              : 
     363           23 :         if (arrangementCtx.actualDuration != 0) {
     364           14 :             noteCtx.duration = arrangementCtx.actualTimestamp + arrangementCtx.actualDuration - noteCtx.timestamp;
     365              :         }
     366              : 
     367           23 :         const mpe::pitch_level_t offset = noteEvent.pitchCtx().nominalPitchLevel - noteCtx.pitchLevel;
     368           23 :         pitchOffsets.emplace_back(arrangementCtx.actualTimestamp, offset);
     369              :     }
     370              : 
     371           10 :     if (multibendIt != noteCtx.articulations.end()) {
     372            9 :         mpe::ArticulationMeta& meta = multibendIt->second.meta;
     373            9 :         meta.timestamp = noteCtx.timestamp;
     374            9 :         meta.overallDuration = noteCtx.duration;
     375              :     }
     376              : 
     377           10 :     mpe::PitchCurve curve = buildPitchCurve(noteCtx.timestamp, noteCtx.duration, pitchOffsets, timeFactorMap);
     378           10 :     mpe::NoteEvent result = buildNoteEvent(noteCtx, curve);
     379              : 
     380           10 :     return result;
     381           10 : }
     382              : 
     383           10 : mpe::PitchCurve BendsRenderer::buildPitchCurve(mpe::timestamp_t noteTimestamp, mpe::duration_t noteDuration,
     384              :                                                const PitchOffsets& pitchOffsets, const BendTimeFactorMap& timeFactorMap)
     385              : {
     386           32 :     auto findFactorsAtTime = [&timeFactorMap](mpe::timestamp_t time) -> const BendTimeFactors& {
     387           32 :         auto it = muse::findLessOrEqual(timeFactorMap, time);
     388           32 :         if (it == timeFactorMap.end()) {
     389              :             static const BendTimeFactors dummy;
     390           18 :             return dummy;
     391              :         }
     392              : 
     393           14 :         return it->second;
     394           10 :     };
     395              : 
     396           10 :     mpe::PitchCurve result;
     397           10 :     result.emplace(0, 0);
     398              : 
     399           10 :     mpe::percentage_t prevNominalOffsetPrecent = 0;
     400              : 
     401           42 :     for (const auto& pair : pitchOffsets) {
     402           32 :         const BendTimeFactors& factors = findFactorsAtTime(pair.first);
     403           32 :         const float ratio = static_cast<float>(pair.first - noteTimestamp) / noteDuration;
     404              : 
     405           32 :         const mpe::percentage_t nominalOffsetPercent = static_cast<mpe::percentage_t>(ratio * 100.f) * mpe::ONE_PERCENT;
     406           32 :         const mpe::percentage_t nominalPercentDiff = nominalOffsetPercent - prevNominalOffsetPrecent;
     407              : 
     408           32 :         const mpe::percentage_t actualOffsetStartPercent = prevNominalOffsetPrecent + nominalPercentDiff * factors.startFactor;
     409           32 :         const mpe::percentage_t actualOffsetEndPercent = prevNominalOffsetPrecent + nominalPercentDiff * factors.endFactor;
     410              : 
     411           32 :         prevNominalOffsetPrecent = nominalOffsetPercent;
     412              : 
     413           32 :         const auto& prevOffset = result.rbegin();
     414           32 :         result.insert_or_assign(std::min(actualOffsetStartPercent, mpe::HUNDRED_PERCENT), prevOffset->second);
     415           32 :         result.insert_or_assign(std::min(actualOffsetEndPercent, mpe::HUNDRED_PERCENT), pair.second);
     416              :     }
     417              : 
     418           10 :     return result;
     419            0 : }
        

Generated by: LCOV version 2.0-1