LCOV - code coverage report
Current view: top level - src/engraving/playback - playbackeventsrenderer.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 60.6 % 175 106
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 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 "playbackeventsrenderer.h"
      24              : 
      25              : #include "log.h"
      26              : 
      27              : #include "dom/chord.h"
      28              : #include "dom/harmony.h"
      29              : #include "dom/measure.h"
      30              : #include "dom/note.h"
      31              : #include "dom/sig.h"
      32              : #include "dom/staff.h"
      33              : #include "dom/utils.h"
      34              : 
      35              : #include "types/bps.h"
      36              : 
      37              : #include "utils/arrangementutils.h"
      38              : 
      39              : #include "metaparsers/chordarticulationsparser.h"
      40              : #include "metaparsers/notearticulationsparser.h"
      41              : 
      42              : #include "renderers/chordarticulationsrenderer.h"
      43              : 
      44              : #include "filters/chordfilter.h"
      45              : 
      46              : using namespace mu::engraving;
      47              : using namespace muse;
      48              : using namespace muse::mpe;
      49              : 
      50          631 : static ArticulationMap makeStandardArticulationMap(const ArticulationsProfilePtr profile, timestamp_t timestamp, duration_t duration)
      51              : {
      52          631 :     IF_ASSERT_FAILED(profile) {
      53            0 :         return {};
      54              :     }
      55              : 
      56              :     ArticulationMeta meta(ArticulationType::Standard,
      57              :                           profile->pattern(ArticulationType::Standard),
      58              :                           timestamp,
      59              :                           duration,
      60              :                           0,
      61          631 :                           0);
      62              : 
      63          631 :     ArticulationMap articulations;
      64          631 :     articulations.emplace(ArticulationType::Standard, mpe::ArticulationAppliedData(std::move(meta), 0, mpe::HUNDRED_PERCENT));
      65          631 :     articulations.preCalculateAverageData();
      66              : 
      67          631 :     return articulations;
      68          631 : }
      69              : 
      70          627 : static muse::mpe::NoteEvent buildMetronomeEvent(const TimeSigFrac& timeSig, const double bps,
      71              :                                                 const BeatType beatType, const muse::mpe::timestamp_t actualTimestamp,
      72              :                                                 const muse::mpe::ArticulationsProfilePtr profile)
      73              : {
      74          627 :     int ticksPerBeat = timeSig.ticks() / timeSig.numerator();
      75          627 :     duration_t duration = durationFromTempoAndTicks(bps, ticksPerBeat);
      76              : 
      77              :     pitch_level_t eventPitchLevel = beatType == BeatType::DOWNBEAT
      78          627 :                                     ? pitchLevel(PitchClass::E, 5) // high wood block
      79          471 :                                     : pitchLevel(PitchClass::F, 5); // low wood block
      80              : 
      81          627 :     const ArticulationMap articulations = makeStandardArticulationMap(profile, actualTimestamp, duration);
      82              : 
      83              :     return mpe::NoteEvent(actualTimestamp,
      84              :                           duration,
      85              :                           0,
      86              :                           0,
      87              :                           eventPitchLevel,
      88              :                           dynamicLevelFromType(mpe::DynamicType::mf),
      89              :                           articulations,
      90         1254 :                           bps);
      91         1254 : }
      92              : 
      93         1071 : void PlaybackEventsRenderer::render(const EngravingItem* item, const int tickPositionOffset,
      94              :                                     const ArticulationsProfilePtr profile, const PlaybackContextPtr playbackCtx,
      95              :                                     PlaybackEventsMap& result) const
      96              : {
      97         1071 :     IF_ASSERT_FAILED(item->isChord()) {
      98            0 :         return;
      99              :     }
     100              : 
     101         1071 :     renderNoteEvents(toChord(item), tickPositionOffset, profile, playbackCtx, result);
     102              : }
     103              : 
     104            2 : void PlaybackEventsRenderer::render(const EngravingItem* item, const mpe::timestamp_t actualTimestamp,
     105              :                                     const mpe::duration_t actualDuration, const mpe::dynamic_level_t actualDynamicLevel,
     106              :                                     const PlaybackContextPtr playbackCtx, const ArticulationsProfilePtr profile,
     107              :                                     PlaybackEventsMap& result) const
     108              : {
     109            2 :     if (item->isChord()) {
     110            1 :         const Chord* chord = toChord(item);
     111            1 :         mpe::PlaybackEventList& events = result[actualTimestamp];
     112              : 
     113            4 :         for (const Note* note : chord->notes()) {
     114            3 :             renderFixedNoteEvent(note, actualTimestamp, actualDuration,
     115              :                                  actualDynamicLevel, playbackCtx, profile, events);
     116              :         }
     117            1 :     } else if (item->isNote()) {
     118            1 :         renderFixedNoteEvent(toNote(item), actualTimestamp, actualDuration,
     119            1 :                              actualDynamicLevel, playbackCtx, profile, result[actualTimestamp]);
     120              :     } else {
     121            0 :         UNREACHABLE;
     122              :     }
     123            2 : }
     124              : 
     125            0 : void PlaybackEventsRenderer::renderChordSymbol(const Harmony* chordSymbol,
     126              :                                                const int ticksPositionOffset,
     127              :                                                const mpe::ArticulationsProfilePtr profile,
     128              :                                                const PlaybackContextPtr playbackCtx,
     129              :                                                mpe::PlaybackEventsMap& result) const
     130              : {
     131            0 :     if (!chordSymbol->isRealizable()) {
     132            0 :         return;
     133              :     }
     134              : 
     135            0 :     const Staff* staff = chordSymbol->staff();
     136            0 :     IF_ASSERT_FAILED(staff) {
     137            0 :         return;
     138              :     }
     139              : 
     140            0 :     const RealizedHarmony& realized = chordSymbol->getRealizedHarmony();
     141            0 :     const RealizedHarmony::PitchMap& notes = realized.notes();
     142              : 
     143            0 :     const Score* score = chordSymbol->score();
     144            0 :     int positionTick = chordSymbol->tick().ticks();
     145            0 :     int positionTickWithOffset = positionTick + ticksPositionOffset;
     146              : 
     147            0 :     timestamp_t eventTimestamp = timestampFromTicks(score, positionTickWithOffset);
     148            0 :     PlaybackEventList& events = result[eventTimestamp];
     149              : 
     150            0 :     int durationTicks = realized.getActualDuration(positionTickWithOffset).ticks();
     151            0 :     duration_t duration = timestampFromTicks(score, positionTickWithOffset + durationTicks) - eventTimestamp;
     152              : 
     153            0 :     voice_layer_idx_t voiceIdx = static_cast<voice_layer_idx_t>(chordSymbol->voice());
     154            0 :     staff_layer_idx_t staffIdx = static_cast<staff_layer_idx_t>(chordSymbol->staffIdx());
     155            0 :     Key key = staff->key(chordSymbol->tick());
     156              : 
     157            0 :     ArticulationMap articulations = makeStandardArticulationMap(profile, eventTimestamp, duration);
     158              : 
     159            0 :     double bps = score->multipliedTempoAtUtick(positionTickWithOffset).val;
     160              : 
     161            0 :     for (auto it = notes.cbegin(); it != notes.cend(); ++it) {
     162            0 :         int pitch = it->first;
     163            0 :         int tpc = pitch2tpc(pitch, key, Prefer::NEAREST);
     164            0 :         int octave = playingOctave(pitch, tpc);
     165            0 :         pitch_level_t pitchLevel = notePitchLevel(tpc, octave);
     166              : 
     167            0 :         events.emplace_back(mpe::NoteEvent(eventTimestamp,
     168              :                                            duration,
     169              :                                            voiceIdx,
     170              :                                            staffIdx,
     171              :                                            pitchLevel,
     172              :                                            playbackCtx->appliableDynamicLevel(chordSymbol->track(), positionTickWithOffset),
     173              :                                            articulations,
     174            0 :                                            bps));
     175              :     }
     176            0 : }
     177              : 
     178            0 : void PlaybackEventsRenderer::renderChordSymbol(const Harmony* chordSymbol, const mpe::timestamp_t actualTimestamp,
     179              :                                                const mpe::duration_t actualDuration, const mpe::dynamic_level_t actualDynamicLevel,
     180              :                                                const ArticulationsProfilePtr profile, mpe::PlaybackEventsMap& result) const
     181              : {
     182            0 :     if (!chordSymbol->isRealizable()) {
     183            0 :         return;
     184              :     }
     185              : 
     186            0 :     const Staff* staff = chordSymbol->staff();
     187            0 :     IF_ASSERT_FAILED(staff) {
     188            0 :         return;
     189              :     }
     190              : 
     191            0 :     const RealizedHarmony& realized = chordSymbol->getRealizedHarmony();
     192            0 :     const RealizedHarmony::PitchMap& notes = realized.notes();
     193              : 
     194            0 :     PlaybackEventList& events = result[actualTimestamp];
     195              : 
     196            0 :     voice_layer_idx_t voiceIdx = static_cast<voice_layer_idx_t>(chordSymbol->voice());
     197            0 :     staff_layer_idx_t staffIdx = static_cast<staff_layer_idx_t>(chordSymbol->staffIdx());
     198            0 :     Key key = staff->key(chordSymbol->tick());
     199              : 
     200            0 :     ArticulationMap articulations = makeStandardArticulationMap(profile, actualTimestamp, actualDuration);
     201              : 
     202            0 :     for (auto it = notes.cbegin(); it != notes.cend(); ++it) {
     203            0 :         int pitch = it->first;
     204            0 :         int tpc = pitch2tpc(pitch, key, Prefer::NEAREST);
     205            0 :         int octave = playingOctave(pitch, tpc);
     206            0 :         pitch_level_t pitchLevel = notePitchLevel(tpc, octave);
     207              : 
     208            0 :         events.emplace_back(mpe::NoteEvent(actualTimestamp,
     209              :                                            actualDuration,
     210              :                                            voiceIdx,
     211              :                                            staffIdx,
     212              :                                            pitchLevel,
     213              :                                            actualDynamicLevel,
     214              :                                            articulations,
     215            0 :                                            Constants::DEFAULT_TEMPO.val));
     216              :     }
     217            0 : }
     218              : 
     219          148 : void PlaybackEventsRenderer::renderMetronome(const Score* score, const Measure* measure, const int ticksPositionOffset,
     220              :                                              const muse::mpe::ArticulationsProfilePtr profile, mpe::PlaybackEventsMap& result) const
     221              : {
     222          148 :     IF_ASSERT_FAILED(score) {
     223            0 :         return;
     224              :     }
     225              : 
     226          148 :     int measureStartTick = measure->tick().ticks();
     227          148 :     int measureEndTick = measure->endTick().ticks();
     228              : 
     229          148 :     TimeSigFrac timeSignatureFraction = score->sigmap()->timesig(measureStartTick).nominal();
     230              : 
     231          148 :     const BeatsPerSecond measureBps = score->multipliedTempoAtUtick(measureStartTick + ticksPositionOffset);
     232          148 :     const int step = timeSignatureFraction.isBeatedCompound(measureBps.val)
     233          148 :                      ? timeSignatureFraction.beatTicks() : timeSignatureFraction.dUnitTicks();
     234              : 
     235          148 :     int startTick = measureStartTick;
     236          148 :     int rtick = 0;
     237              : 
     238          148 :     if (measure->isAnacrusis()) {
     239            0 :         int remainingTicks = measure->ticks().ticks() % step;
     240            0 :         startTick += remainingTicks;
     241            0 :         rtick = remainingTicks + timeSignatureFraction.ticksPerMeasure() - measure->ticks().ticks();
     242              :     }
     243              : 
     244              :     // Tempo can change mid-measure (automation point or ramp), so re-read it for every click
     245              :     // rather than reusing the tempo from the start of the measure; the click grid itself stays fixed
     246          750 :     for (int tick = startTick; tick < measureEndTick; tick += step, rtick += step) {
     247          602 :         const BeatsPerSecond bps = score->multipliedTempoAtUtick(tick + ticksPositionOffset);
     248              : 
     249          602 :         timestamp_t eventTimestamp = timestampFromTicks(score, tick + ticksPositionOffset);
     250          602 :         BeatType beatType = timeSignatureFraction.rtick2beatType(rtick);
     251          602 :         mpe::NoteEvent event = buildMetronomeEvent(timeSignatureFraction, bps.val, beatType, eventTimestamp, profile);
     252              : 
     253          602 :         result[eventTimestamp].emplace_back(std::move(event));
     254          602 :     }
     255              : }
     256              : 
     257            0 : void PlaybackEventsRenderer::renderMetronome(const Score* score, const int tick, const mpe::timestamp_t actualTimestamp,
     258              :                                              const muse::mpe::ArticulationsProfilePtr profile, mpe::PlaybackEventsMap& result) const
     259              : {
     260            0 :     IF_ASSERT_FAILED(score) {
     261            0 :         return;
     262              :     }
     263              : 
     264            0 :     TimeSigFrac timeSignatureFraction = score->sigmap()->timesig(tick).timesig();
     265            0 :     BeatsPerSecond bps = score->multipliedTempo(Fraction::fromTicks(tick));
     266            0 :     BeatType beatType = score->tick2beatType(Fraction::fromTicks(tick));
     267            0 :     mpe::NoteEvent event = buildMetronomeEvent(timeSignatureFraction, bps.val, beatType, actualTimestamp, profile);
     268              : 
     269            0 :     result[actualTimestamp].emplace_back(std::move(event));
     270            0 : }
     271              : 
     272            5 : void PlaybackEventsRenderer::renderCountIn(const Score* score, const int startTick, const muse::mpe::timestamp_t actualTimestamp,
     273              :                                            const muse::mpe::ArticulationsProfilePtr profile,
     274              :                                            muse::mpe::PlaybackEventsMap& result, muse::mpe::duration_t& countInDuration) const
     275              : {
     276            5 :     const Measure* measure = score->tick2measure(Fraction::fromTicks(startTick));
     277            5 :     if (!measure) {
     278            0 :         return;
     279              :     }
     280              : 
     281            5 :     int measureStartTick = measure->tick().ticks();
     282            5 :     TimeSigFrac timeSignatureFraction = score->sigmap()->timesig(measureStartTick).nominal();
     283            5 :     BeatsPerSecond bps = score->multipliedTempo(Fraction::fromTicks(measureStartTick));
     284            5 :     int ticksPerMeasure = timeSignatureFraction.ticksPerMeasure();
     285              : 
     286            5 :     int step = timeSignatureFraction.isBeatedCompound(bps.val)
     287            5 :                ? timeSignatureFraction.beatTicks() : timeSignatureFraction.dUnitTicks();
     288              : 
     289            5 :     duration_t stepDuration = durationFromTempoAndTicks(bps.val, step);
     290              : 
     291              :     // Add extra clicks if...
     292            5 :     int endTick = ticksPerMeasure + (startTick - measureStartTick); // ... not starting playback at beginning of measure
     293            5 :     int remainingTicks = 0;
     294              : 
     295            5 :     if (measure->isAnacrusis()) { // ... measure is incomplete (anacrusis)
     296            1 :         int measureTicks = measure->ticks().ticks();
     297            1 :         endTick += ticksPerMeasure - measureTicks;
     298            1 :         remainingTicks = measureTicks % step;
     299              :     }
     300              : 
     301            5 :     MeasureBeat measureBeat = findBeat(score, startTick);
     302            5 :     int closestMainBeatTick = score->sigmap()->bar2tick(measureBeat.measureIndex, std::ceil(measureBeat.beat));
     303            5 :     remainingTicks += closestMainBeatTick - startTick;
     304              : 
     305            5 :     timestamp_t eventTimestamp = actualTimestamp;
     306              : 
     307           30 :     for (int tick = 0; tick < endTick; tick += step) {
     308           25 :         int rtick = tick % ticksPerMeasure;
     309           25 :         BeatType beatType = timeSignatureFraction.rtick2beatType(rtick);
     310           25 :         mpe::NoteEvent event = buildMetronomeEvent(timeSignatureFraction, bps.val, beatType, eventTimestamp, profile);
     311              : 
     312           25 :         result[eventTimestamp].emplace_back(std::move(event));
     313           25 :         eventTimestamp += stepDuration;
     314           25 :     }
     315              : 
     316            5 :     countInDuration = eventTimestamp - actualTimestamp;
     317            5 :     if (remainingTicks > 0) {
     318            1 :         countInDuration -= durationFromTempoAndTicks(bps.val, remainingTicks);
     319              :     }
     320              : }
     321              : 
     322         1071 : void PlaybackEventsRenderer::renderNoteEvents(const Chord* chord, const int tickPositionOffset,
     323              :                                               const mpe::ArticulationsProfilePtr profile, const PlaybackContextPtr playbackCtx,
     324              :                                               PlaybackEventsMap& result) const
     325              : {
     326         1071 :     IF_ASSERT_FAILED(chord) {
     327            0 :         return;
     328              :     }
     329              : 
     330         1071 :     RenderingContext ctx = engraving::buildRenderingCtx(chord, tickPositionOffset, profile, playbackCtx);
     331              : 
     332         1071 :     if (!ChordFilter::isItemPlayable(chord, ctx)) {
     333            0 :         return;
     334              :     }
     335              : 
     336         1071 :     ChordArticulationsParser::buildChordArticulationMap(chord, ctx, ctx.commonArticulations);
     337              : 
     338         1071 :     PlaybackEventList newEvents;
     339         1071 :     ChordArticulationsRenderer::render(chord, ArticulationType::Last, ctx, newEvents);
     340              : 
     341         1071 :     if (!newEvents.empty()) {
     342         1047 :         PlaybackEventList& list = result[ctx.nominalTimestamp];
     343         1047 :         list.insert(list.end(), std::make_move_iterator(newEvents.begin()), std::make_move_iterator(newEvents.end()));
     344              :     }
     345         1071 : }
     346              : 
     347            4 : void PlaybackEventsRenderer::renderFixedNoteEvent(const Note* note, const mpe::timestamp_t actualTimestamp,
     348              :                                                   const mpe::duration_t actualDuration,
     349              :                                                   const mpe::dynamic_level_t actualDynamicLevel,
     350              :                                                   const PlaybackContextPtr playbackCtx,
     351              :                                                   const mpe::ArticulationsProfilePtr profile, mpe::PlaybackEventList& result) const
     352              : {
     353            4 :     static const ArticulationMap articulations;
     354              : 
     355            4 :     const Score* score = note->score();
     356            4 :     const int durationTicks = ticksFromTempoAndDuration(Constants::DEFAULT_TEMPO.val, actualDuration);
     357            4 :     const int tick = note->tick().ticks();
     358            4 :     const int utick = score ? score->repeatList().tick2utick(tick) : tick;
     359            4 :     const int tickOffset = utick - tick;
     360              : 
     361            4 :     RenderingContext ctx{ actualTimestamp,
     362              :                           actualDuration,
     363              :                           actualDynamicLevel,
     364              :                           tick, /*nominalPositionStartTick*/
     365              :                           durationTicks, /*nominalPositionEndTick*/
     366              :                           durationTicks, /*nominalDurationTicks*/
     367              :                           tickOffset,
     368              :                           Constants::DEFAULT_TEMPO,
     369              :                           TimeSigMap::DEFAULT_TIME_SIGNATURE,
     370              :                           articulations,
     371              :                           score,
     372              :                           profile,
     373            4 :                           playbackCtx };
     374              : 
     375            4 :     NoteArticulationsParser::parsePlayingTechnique(ctx, note->track(), ctx.commonArticulations, false /*sustainAllowed*/);
     376            4 :     NoteArticulationsParser::parseGhostNote(note, ctx, ctx.commonArticulations);
     377            4 :     NoteArticulationsParser::parseNoteHead(note, ctx, ctx.commonArticulations);
     378            4 :     NoteArticulationsParser::parseSymbols(note, ctx, ctx.commonArticulations);
     379              : 
     380            4 :     if (ctx.commonArticulations.empty()) {
     381            4 :         ctx.commonArticulations = makeStandardArticulationMap(profile, actualTimestamp, actualDuration);
     382              :     } else {
     383            0 :         ctx.commonArticulations.preCalculateAverageData();
     384              :     }
     385              : 
     386            4 :     NominalNoteCtx noteCtx(note, ctx);
     387            4 :     result.emplace_back(buildNoteEvent(noteCtx));
     388            8 : }
        

Generated by: LCOV version 2.0-1