LCOV - code coverage report
Current view: top level - src/engraving/dom - tempotimeline.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 95.9 % 145 139
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-Studio-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2026 MuseScore Limited 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 "tempotimeline.h"
      24              : 
      25              : #include <algorithm>
      26              : #include <cmath>
      27              : #include <iterator>
      28              : 
      29              : #include "automation/tempovalues.h"
      30              : #include "types/constants.h"
      31              : 
      32              : #include "global/realfn.h"
      33              : #include "global/log.h"
      34              : 
      35              : using namespace mu::engraving;
      36              : 
      37              : static constexpr int MIN_TEMPO_RESAMPLE_STEPS = 8;
      38              : 
      39          310 : static TempoValues resampleTempoValues(const AutomationCurve& tempoCurve)
      40              : {
      41          313 :     TRACEFUNC;
      42              : 
      43              :     // No tempo markings at all - hold the default tempo
      44          310 :     if (tempoCurve.empty()) {
      45          717 :         return { { 0, Constants::DEFAULT_TEMPO.val } };
      46              :     }
      47              : 
      48              :     // Start with every authored point, denormalized back to bps,
      49              :     // plus a default-tempo point at tick 0 if the curve doesn't already start there
      50           71 :     TempoValues authored;
      51           71 :     authored.reserve(tempoCurve.size() + 1);
      52           71 :     if (tempoCurve.begin()->first > 0) {
      53            5 :         authored.emplace_back(0, Constants::DEFAULT_TEMPO.val);
      54              :     }
      55              : 
      56          299 :     for (const auto& [utick, point] : tempoCurve) {
      57          228 :         authored.emplace_back(utick, denormalizeTempo(point.value.outValue).val);
      58              :     }
      59              : 
      60              :     // Fill in the ramp shape of gradual tempo changes (accelerando/ritardando) between where
      61              :     // they start and where they arrive.
      62              :     // anchorUTick/anchorNorm track where the CURRENT ramp started, only advancing on a real
      63              :     // ramp arrival, so a marking inside another ramp's span doesn't restart it.
      64              :     // currentNorm is the actual last-known tempo, updated by every point - used to check if a
      65              :     // new ramp is flat, so it doesn't get bridged to an old, unrelated ramp's anchor
      66           71 :     TempoValues resampled;
      67              : 
      68           71 :     int anchorUTick = tempoCurve.begin()->first;
      69           71 :     real_t anchorNorm = tempoCurve.begin()->second.value.outValue;
      70           71 :     real_t currentNorm = anchorNorm;
      71              : 
      72              :     // Resampled ticks only increase across the whole loop below, so this hint can walk
      73              :     // `authored` forward once instead of a find() at every resampled tick
      74           71 :     auto authoredHint = authored.begin();
      75              : 
      76          299 :     for (auto it = std::next(tempoCurve.begin()); it != tempoCurve.end(); ++it) {
      77          157 :         if (!std::holds_alternative<AutomationPoint::ExplicitArrival>(it->second.value.inValue)) {
      78          103 :             currentNorm = it->second.value.outValue;
      79          131 :             continue;
      80              :         }
      81              : 
      82           54 :         const std::optional<AutomationPoint::Ease> pointEase = ease(it->second);
      83           54 :         const real_t arrivalNorm = muse::mpe::resolveInValue(it->second.value, std::nullopt);
      84           54 :         const bool isFlat = (!pointEase || pointEase->isNone()) && muse::RealIsEqual(arrivalNorm.raw(), currentNorm.raw());
      85           54 :         if (isFlat) {
      86           28 :             anchorUTick = it->first;
      87           28 :             anchorNorm = it->second.value.outValue;
      88           28 :             currentNorm = anchorNorm;
      89           28 :             continue;
      90              :         }
      91              : 
      92           26 :         const int segmentTicks = it->first - anchorUTick;
      93           26 :         const int steps = std::max(MIN_TEMPO_RESAMPLE_STEPS, segmentTicks / Constants::DIVISION);
      94          208 :         for (int s = 1; s < steps; ++s) {
      95          182 :             const int utick = anchorUTick + segmentTicks * s / steps;
      96              : 
      97          274 :             while (authoredHint != authored.end() && authoredHint->first < utick) {
      98           92 :                 ++authoredHint;
      99              :             }
     100          182 :             if (authoredHint != authored.end() && authoredHint->first == utick) {
     101            1 :                 continue; // there is already an authored point at this tick
     102              :             }
     103          181 :             if (!resampled.empty() && resampled.back().first == utick) {
     104            0 :                 continue; // rounding produced the same tick as the previous step in this ramp
     105              :             }
     106              : 
     107          181 :             const muse::real_t t = muse::real_t(s) / muse::real_t(steps);
     108          181 :             const real_t normalized = muse::mpe::evaluateAt(it->second.value, anchorNorm, t);
     109          181 :             const BeatsPerSecond bps = denormalizeTempo(normalized);
     110          181 :             resampled.emplace_back(utick, bps.val);
     111              :         }
     112              : 
     113           26 :         anchorUTick = it->first;
     114           26 :         anchorNorm = it->second.value.outValue;
     115           26 :         currentNorm = anchorNorm;
     116              :     }
     117              : 
     118           71 :     if (resampled.empty()) {
     119           56 :         return authored;
     120              :     }
     121              : 
     122           15 :     TempoValues merged;
     123           15 :     merged.reserve(authored.size() + resampled.size());
     124           15 :     std::merge(authored.begin(), authored.end(), resampled.begin(), resampled.end(), std::back_inserter(merged),
     125          273 :                [](const std::pair<int, double>& a, const std::pair<int, double>& b) { return a.first < b.first; });
     126           15 :     return merged;
     127          310 : }
     128              : 
     129          310 : void TempoTimeline::rebuild(const AutomationCurve& tempoCurve, const PausesMap& pauses)
     130              : {
     131          310 :     rebuild(resampleTempoValues(tempoCurve), pauses);
     132          310 : }
     133              : 
     134          311 : void TempoTimeline::rebuild(const TempoValues& tempoValues, const PausesMap& pauses)
     135              : {
     136          314 :     TRACEFUNC;
     137              : 
     138          311 :     m_points.clear();
     139          311 :     m_pauses = pauses;
     140          311 :     m_utick2utimeHint = 0;
     141          311 :     m_utime2utickHint = 0;
     142          311 :     m_tempoHint = 0;
     143              : 
     144          311 :     m_points.reserve(tempoValues.size() + pauses.size());
     145              : 
     146              :     // Merge-walk values (tick->bps) and pauses (tick->held seconds) by tick in one pass
     147          311 :     auto vit = tempoValues.begin();
     148          311 :     auto pit = pauses.begin();
     149              : 
     150         1289 :     while (vit != tempoValues.end() || pit != pauses.end()) {
     151          667 :         const bool valueIsNext = pit == pauses.end() || (vit != tempoValues.end() && vit->first <= pit->first);
     152          667 :         const int utick = valueIsNext ? vit->first : pit->first;
     153              : 
     154          667 :         if (valueIsNext) {
     155          656 :             double time = 0.0;
     156          656 :             if (!m_points.empty()) {
     157          345 :                 const TempoTimePoint& prev = m_points.back();
     158          345 :                 time = prev.time + double(utick - prev.utick) / (Constants::DIVISION * prev.bps);
     159              :             }
     160              :             // Clamp to the valid tempo range so a zero/negative bps can't make time math divide by zero
     161          656 :             const double bps = std::clamp(vit->second, Constants::MIN_TEMPO.val, Constants::MAX_TEMPO.val);
     162          656 :             m_points.push_back({ utick, time, bps, 0.0 });
     163          656 :             ++vit;
     164              :         }
     165              : 
     166              :         // A pause can land on the same tick as the value point just emitted above
     167          667 :         if (pit != pauses.end() && pit->first == utick) {
     168           11 :             if (m_points.empty()) {
     169              :                 // No tempo value at or before this pause - anchor with the default tempo
     170            0 :                 m_points.push_back({ utick, 0.0, Constants::DEFAULT_TEMPO.val, 0.0 });
     171              :             }
     172           11 :             const TempoTimePoint& prev = m_points.back();
     173           22 :             const double time = prev.utick == utick
     174           11 :                                 ? prev.time
     175           11 :                                 : prev.time + double(utick - prev.utick) / (Constants::DIVISION * prev.bps);
     176           11 :             m_points.push_back({ utick, time + pit->second, prev.bps, pit->second });
     177           11 :             ++pit;
     178              :         }
     179              :     }
     180          311 : }
     181              : 
     182         5269 : double TempoTimeline::utick2utime(int utick) const
     183              : {
     184         5269 :     const size_t n = m_points.size();
     185         5269 :     if (n == 0) {
     186            0 :         return 0.;
     187              :     }
     188              : 
     189         5269 :     bool hintValid = false;
     190         5269 :     if (m_utick2utimeHint < n) {
     191         5269 :         const int hintUtick = m_points[m_utick2utimeHint].utick;
     192         5269 :         const bool isLastPoint = (m_utick2utimeHint + 1 == n);
     193         5269 :         hintValid = utick >= hintUtick
     194         5269 :                     && (isLastPoint || utick < m_points[m_utick2utimeHint + 1].utick);
     195              :     }
     196              : 
     197         5269 :     if (!hintValid) {
     198          136 :         const auto it = std::upper_bound(m_points.begin(), m_points.end(), utick,
     199          658 :                                          [](int u, const TempoTimePoint& p) { return u < p.utick; });
     200          272 :         m_utick2utimeHint = it == m_points.begin() ? 0 : size_t(std::prev(it) - m_points.begin());
     201              :     }
     202              : 
     203         5269 :     const TempoTimePoint& from = m_points[m_utick2utimeHint];
     204         5269 :     const double nominalTime = from.time + double(utick - from.utick) / (Constants::DIVISION * from.bps);
     205         5269 :     return nominalTime / m_tempoMultiplier.val;
     206              : }
     207              : 
     208          748 : int TempoTimeline::utime2utick(double time) const
     209              : {
     210          748 :     const size_t n = m_points.size();
     211          748 :     if (n == 0) {
     212            0 :         return 0;
     213              :     }
     214              : 
     215          748 :     const double nominalSecs = time * m_tempoMultiplier.val;
     216              : 
     217          748 :     bool hintValid = false;
     218          748 :     if (m_utime2utickHint < n) {
     219          748 :         const double hintTime = m_points[m_utime2utickHint].time;
     220          748 :         const bool isLastPoint = (m_utime2utickHint + 1 == n);
     221          748 :         hintValid = nominalSecs >= hintTime
     222          748 :                     && (isLastPoint || nominalSecs < m_points[m_utime2utickHint + 1].time);
     223              :     }
     224              : 
     225          748 :     if (!hintValid) {
     226           15 :         const auto it = std::upper_bound(m_points.begin(), m_points.end(), nominalSecs,
     227           67 :                                          [](double s, const TempoTimePoint& p) { return s < p.time; });
     228           30 :         m_utime2utickHint = it == m_points.begin() ? 0 : size_t(std::prev(it) - m_points.begin());
     229              :     }
     230              : 
     231          748 :     const size_t idx = m_utime2utickHint;
     232          748 :     const TempoTimePoint& from = m_points[idx];
     233              : 
     234          748 :     if (idx + 1 < n) {
     235           43 :         const TempoTimePoint& next = m_points[idx + 1];
     236           43 :         if (next.pause > 0.0 && nominalSecs >= next.time - next.pause) {
     237            2 :             return next.utick; // still inside next's pause window: hold there
     238              :         }
     239              :     }
     240              : 
     241          746 :     const double elapsed = nominalSecs - from.time;
     242          746 :     return from.utick + int(std::lround(elapsed * Constants::DIVISION * from.bps));
     243              : }
     244              : 
     245         1969 : BeatsPerSecond TempoTimeline::tempo(int utick) const
     246              : {
     247         1969 :     const size_t n = m_points.size();
     248         1969 :     if (n == 0) {
     249            0 :         return Constants::DEFAULT_TEMPO;
     250              :     }
     251              : 
     252         1969 :     bool hintValid = false;
     253         1969 :     if (m_tempoHint < n) {
     254         1969 :         const int hintUtick = m_points[m_tempoHint].utick;
     255         1969 :         const bool isLastPoint = (m_tempoHint + 1 == n);
     256         1969 :         hintValid = utick >= hintUtick
     257         1969 :                     && (isLastPoint || utick < m_points[m_tempoHint + 1].utick);
     258              :     }
     259              : 
     260         1969 :     if (!hintValid) {
     261           64 :         const auto it = std::upper_bound(m_points.begin(), m_points.end(), utick,
     262          297 :                                          [](int u, const TempoTimePoint& p) { return u < p.utick; });
     263          128 :         m_tempoHint = it == m_points.begin() ? 0 : size_t(std::prev(it) - m_points.begin());
     264              :     }
     265              : 
     266         1969 :     return BeatsPerSecond(m_points[m_tempoHint].bps);
     267              : }
     268              : 
     269           52 : void TempoTimeline::setTempoMultiplier(const BeatsPerSecond& bps)
     270              : {
     271           52 :     IF_ASSERT_FAILED(bps.val > 0.0) {
     272            0 :         return;
     273              :     }
     274              : 
     275           52 :     m_tempoMultiplier = bps;
     276              : }
        

Generated by: LCOV version 2.0-1