Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-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 : #pragma once
23 :
24 : #include <algorithm>
25 : #include <variant>
26 : #include <optional>
27 : #include <type_traits>
28 :
29 : #include "types/sharedmap.h"
30 : #include "types/number.h"
31 :
32 : #include "containers.h"
33 : #include "realfn.h"
34 :
35 : namespace muse::mpe {
36 : struct AutomationPoint {
37 : //! NOTE: eases the segment through point (t, value)
38 : struct Ease {
39 : real_t t = real_t::make(0.5); // [0; 1]
40 : real_t value = real_t::make(0.5); // [0; 1]
41 :
42 961 : static constexpr Ease none() { return {}; }
43 346 : bool isNone() const { return *this == none(); }
44 610 : bool operator==(const Ease& e) const { return t == e.t && value == e.value; }
45 : };
46 :
47 : //! NOTE: arrival value equals whatever precedes this point in the curve
48 : struct ArrivalFromPrevious {
49 88 : bool operator==(const ArrivalFromPrevious&) const { return true; }
50 : };
51 :
52 : //! NOTE: arrival value is an explicit number
53 : struct ExplicitArrival {
54 : real_t value = 0.;
55 : Ease ease;
56 :
57 59 : bool operator==(const ExplicitArrival& o) const { return value == o.value && ease == o.ease; }
58 : };
59 :
60 : using InValue = std::variant<ArrivalFromPrevious, ExplicitArrival>;
61 :
62 : InValue inValue = ArrivalFromPrevious {};
63 : real_t outValue = 0.;
64 :
65 148 : bool operator==(const AutomationPoint& p) const
66 : {
67 148 : return inValue == p.inValue && outValue == p.outValue;
68 : }
69 : };
70 :
71 : template<typename KeyT>
72 : using AutomationCurve = SharedMap<KeyT, AutomationPoint>;
73 :
74 : //! NOTE: this point's ease, if it can have one; nullopt for ArrivalFromPrevious
75 911 : inline std::optional<AutomationPoint::Ease> ease(const AutomationPoint& point) noexcept
76 : {
77 1822 : return std::visit([](const auto& v) -> std::optional<AutomationPoint::Ease> {
78 : using T = std::decay_t<decltype(v)>;
79 : if constexpr (std::is_same_v<T, AutomationPoint::ArrivalFromPrevious>) {
80 340 : return std::nullopt;
81 : } else {
82 571 : return v.ease;
83 : }
84 911 : }, point.inValue);
85 : }
86 :
87 : //! NOTE: resolves what this point's arrival value actually is, given prevOutValue
88 : //! (the outValue of whatever precedes it in its curve, if any)
89 1084 : inline real_t resolveInValue(const AutomationPoint& point, std::optional<real_t> prevOutValue) noexcept
90 : {
91 2168 : return std::visit([&](const auto& v) -> real_t {
92 : using T = std::decay_t<decltype(v)>;
93 : if constexpr (std::is_same_v<T, AutomationPoint::ArrivalFromPrevious>) {
94 423 : return prevOutValue ? *prevOutValue : real_t(0.0);
95 : } else {
96 661 : return v.value;
97 : }
98 1084 : }, point.inValue);
99 : }
100 :
101 : //! NOTE: value at normalized position t ([0, 1]) across the segment arriving at point
102 : //! The segment is split into two quadratic Bezier arcs meeting at the bend point. The arcs
103 : //! share a tangent there, producing a smooth bend whose position follows bend.t. When there's
104 : //! no bend, this reduces to a straight line
105 382 : inline real_t evaluateAt(const AutomationPoint& point, std::optional<real_t> prevOutValue, real_t t) noexcept
106 : {
107 382 : const real_t prevOut = prevOutValue ? *prevOutValue : real_t(0.0);
108 382 : const real_t thisIn = resolveInValue(point, prevOutValue);
109 382 : const real_t range = thisIn - prevOut;
110 382 : const std::optional<AutomationPoint::Ease> pointEase = ease(point);
111 :
112 382 : if (!pointEase || pointEase->isNone() || pointEase->t <= 0.0 || pointEase->t >= 1.0) {
113 378 : return std::clamp(prevOut + range * t, real_t(0.0), real_t(1.0));
114 : }
115 :
116 4 : auto quadraticBezier = [](real_t s, real_t p0, real_t p1, real_t p2) {
117 4 : const real_t u = 1.0 - s;
118 4 : return std::clamp(u * u * p0 + 2.0 * u * s * p1 + s * s * p2, real_t(0.0), real_t(1.0));
119 : };
120 :
121 4 : const real_t fraction = std::clamp(pointEase->value, real_t(0.0), real_t(1.0));
122 4 : const real_t bendValue = prevOut + fraction * range;
123 4 : const real_t lo = std::min(prevOut, thisIn);
124 4 : const real_t hi = std::max(prevOut, thisIn);
125 4 : const real_t halfSlope = 0.5 * range;
126 4 : const real_t remainder = 1.0 - pointEase->t;
127 :
128 : // Clamped control points (q1, q2) for each arc
129 4 : const real_t q1 = std::clamp(bendValue - pointEase->t * halfSlope, lo, hi);
130 :
131 4 : if (t <= pointEase->t) {
132 4 : return quadraticBezier(t / pointEase->t, prevOut, q1, bendValue);
133 : }
134 :
135 0 : const real_t q2 = std::clamp(bendValue + remainder * halfSlope, lo, hi);
136 0 : return quadraticBezier((t - pointEase->t) / remainder, bendValue, q2, thisIn);
137 : }
138 :
139 : template<typename Key>
140 214 : inline real_t evaluateCurveAt(const AutomationCurve<Key>& curve, const typename AutomationCurve<Key>::key_type& pos)
141 : {
142 214 : if (curve.empty()) {
143 0 : return real_t {};
144 : }
145 :
146 214 : auto it = findLessOrEqual(curve, pos);
147 214 : if (it == curve.end()) {
148 : //! NOTE: hold the first point's value backwards in time, matching standard envelope semantics
149 1 : return curve.begin()->second.outValue;
150 : }
151 :
152 213 : real_t normalized = it->second.outValue;
153 213 : const auto next = std::next(it);
154 213 : if (next != curve.end()) {
155 180 : const real_t t = static_cast<real_t>(pos - it->first) / static_cast<real_t>(next->first - it->first);
156 180 : normalized = evaluateAt(next->second, it->second.outValue, t);
157 : }
158 :
159 213 : return normalized;
160 : }
161 :
162 : //! NOTE: resamples a curve into discrete samples for consumers that can't evaluate on read
163 : template<typename Key, typename Callback>
164 0 : void resampleCurve(const AutomationCurve<Key>& curve, Key stepInterval, Callback&& onSample)
165 : {
166 0 : for (auto it = curve.cbegin(); it != curve.cend(); ++it) {
167 0 : onSample(it->first, it->second.outValue);
168 :
169 0 : const auto next = std::next(it);
170 0 : if (next == curve.cend()) {
171 0 : continue;
172 : }
173 :
174 0 : const real_t nextArrival = resolveInValue(next->second, it->second.outValue);
175 0 : const std::optional<AutomationPoint::Ease> nextEase = ease(next->second);
176 0 : const bool isFlat = (!nextEase || nextEase->isNone()) && muse::RealIsEqual(nextArrival, it->second.outValue);
177 0 : if (isFlat) {
178 0 : continue;
179 : }
180 :
181 0 : const Key intervalDuration = next->first - it->first;
182 0 : const size_t steps = std::max(size_t((intervalDuration + stepInterval - 1) / stepInterval), size_t(1));
183 :
184 0 : for (size_t j = 1; j < steps; ++j) {
185 0 : const Key t = it->first + intervalDuration * j / steps;
186 0 : const real_t normalized = evaluateAt(next->second, it->second.outValue, real_t(j) / real_t(steps));
187 0 : onSample(t, normalized);
188 : }
189 : }
190 0 : }
191 : }
|