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 : }
|