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 "automationdata.h"
24 :
25 : #include "log.h"
26 :
27 : using namespace mu::engraving;
28 :
29 : static const AutomationCurve EMPTY_CURVE;
30 :
31 31 : static void diffPoints(const AutomationCurveKey& key, const AutomationCurve& oldCurve, const AutomationCurve& newCurve,
32 : AutomationChanges& changes)
33 : {
34 31 : auto oldIt = oldCurve.begin();
35 31 : auto newIt = newCurve.begin();
36 :
37 31 : bool changed = false;
38 31 : utick_t firstTick = 0;
39 31 : utick_t lastTick = 0;
40 :
41 : // An ArrivalFromPrevious point can be bit-identical yet resolve differently if its predecessor's
42 : // outValue changed; only meaningful when that predecessor is the same point in both curves
43 31 : std::optional<real_t> prevOldOut;
44 31 : std::optional<real_t> prevNewOut;
45 31 : bool predecessorAligned = true;
46 :
47 206 : while (oldIt != oldCurve.end() && newIt != newCurve.end()) {
48 : utick_t tick;
49 : bool differs;
50 :
51 175 : if (oldIt->first < newIt->first) {
52 15 : tick = oldIt->first; // point removed
53 15 : differs = true;
54 15 : prevOldOut = oldIt->second.value.outValue;
55 15 : predecessorAligned = false;
56 15 : ++oldIt;
57 160 : } else if (newIt->first < oldIt->first) {
58 20 : tick = newIt->first; // point added
59 20 : differs = true;
60 20 : prevNewOut = newIt->second.value.outValue;
61 20 : predecessorAligned = false;
62 20 : ++newIt;
63 : } else {
64 140 : tick = oldIt->first;
65 140 : differs = oldIt->second != newIt->second // point changed
66 256 : || (predecessorAligned
67 116 : && resolveInValue(oldIt->second.value, prevOldOut) != resolveInValue(newIt->second.value, prevNewOut));
68 140 : prevOldOut = oldIt->second.value.outValue;
69 140 : prevNewOut = newIt->second.value.outValue;
70 140 : predecessorAligned = true;
71 140 : ++oldIt;
72 140 : ++newIt;
73 : }
74 :
75 175 : if (differs) {
76 42 : if (!changed) {
77 17 : firstTick = tick;
78 : }
79 42 : lastTick = tick;
80 42 : changed = true;
81 : }
82 : }
83 :
84 31 : if (oldIt != oldCurve.end() || newIt != newCurve.end()) {
85 7 : const bool oldHasTail = oldIt != oldCurve.end();
86 7 : const utick_t tailFirstTick = oldHasTail ? oldIt->first : newIt->first;
87 7 : const utick_t tailLastTick = oldHasTail ? oldCurve.rbegin()->first : newCurve.rbegin()->first;
88 :
89 7 : if (!changed) {
90 3 : firstTick = tailFirstTick;
91 : }
92 7 : lastTick = tailLastTick;
93 7 : changed = true;
94 : }
95 :
96 31 : if (changed) {
97 20 : changes.extend(key, firstTick, lastTick);
98 : }
99 31 : }
100 :
101 268 : const AutomationCurveMap& AutomationData::curves() const
102 : {
103 268 : return m_curveMap;
104 : }
105 :
106 876 : const AutomationCurve& AutomationData::curve(const AutomationCurveKey& key) const
107 : {
108 876 : auto curveIt = m_curveMap.find(key);
109 876 : if (curveIt == m_curveMap.end()) {
110 788 : return EMPTY_CURVE;
111 : }
112 :
113 88 : return curveIt->second;
114 : }
115 :
116 7 : bool AutomationData::isEmpty() const
117 : {
118 7 : return m_curveMap.empty();
119 : }
120 :
121 20 : void AutomationData::setCurves(const AutomationCurveMap& curves)
122 : {
123 21 : TRACEFUNC;
124 :
125 20 : if (m_curveMap.empty() && !curves.empty()) {
126 13 : m_curveMap = curves;
127 13 : AutomationChanges changes;
128 13 : changes.isFullReset = true;
129 13 : notifyChanged(changes);
130 13 : return;
131 13 : }
132 :
133 7 : size_t overlapCount = 0;
134 7 : AutomationChanges changes;
135 :
136 9 : for (const auto& [key, newCurve] : curves) {
137 2 : const auto oldIt = m_curveMap.find(key);
138 2 : const bool existed = oldIt != m_curveMap.end();
139 2 : if (existed) {
140 2 : ++overlapCount;
141 : }
142 :
143 2 : const AutomationCurve& oldCurve = existed ? oldIt->second : EMPTY_CURVE;
144 2 : diffPoints(key, oldCurve, newCurve, changes);
145 : }
146 :
147 10 : for (const auto& [key, oldCurve] : m_curveMap) {
148 3 : if (!curves.contains(key)) {
149 1 : diffPoints(key, oldCurve, EMPTY_CURVE, changes);
150 : }
151 : }
152 :
153 : // Every key was touched by this change
154 7 : const size_t allKeysCount = curves.size() + m_curveMap.size() - overlapCount;
155 7 : if (changes.affectedKeys.size() == allKeysCount) {
156 6 : changes.isFullReset = true;
157 : }
158 :
159 7 : m_curveMap = curves;
160 :
161 7 : notifyChanged(changes);
162 20 : }
163 :
164 157 : void AutomationData::replaceCurves(const AutomationCurveMap& curves)
165 : {
166 160 : TRACEFUNC;
167 :
168 157 : if (m_curveMap.empty() && !curves.empty()) {
169 146 : m_curveMap = curves;
170 146 : AutomationChanges changes;
171 146 : changes.isFullReset = true;
172 146 : notifyChanged(changes);
173 146 : return;
174 146 : }
175 :
176 11 : const size_t oldSize = m_curveMap.size();
177 11 : size_t overlapCount = 0;
178 11 : AutomationChanges changes;
179 :
180 39 : for (const auto& [key, newCurve] : curves) {
181 28 : const auto oldIt = m_curveMap.find(key);
182 28 : const bool existed = oldIt != m_curveMap.end();
183 28 : if (existed) {
184 26 : ++overlapCount;
185 : }
186 :
187 28 : const AutomationCurve& oldCurve = existed ? oldIt->second : EMPTY_CURVE;
188 28 : diffPoints(key, oldCurve, newCurve, changes);
189 :
190 28 : if (newCurve.empty()) {
191 9 : if (existed) {
192 8 : m_curveMap.erase(oldIt);
193 : }
194 19 : } else if (existed) {
195 18 : oldIt->second = newCurve;
196 : } else {
197 1 : m_curveMap.emplace(key, newCurve);
198 : }
199 : }
200 :
201 : // Every key was touched by this change
202 11 : const size_t allKeysCount = oldSize + curves.size() - overlapCount;
203 11 : if (allKeysCount > 0 && changes.affectedKeys.size() == allKeysCount) {
204 2 : changes.isFullReset = true;
205 : }
206 :
207 11 : notifyChanged(changes);
208 157 : }
209 :
210 19 : void AutomationData::editPoints(const AutomationCurveKey& key, const AutomationPointEdits& edits)
211 : {
212 20 : TRACEFUNC;
213 :
214 19 : if (edits.empty()) {
215 0 : return;
216 : }
217 :
218 19 : const auto curveIt = m_curveMap.try_emplace(key).first;
219 19 : AutomationCurve& curve = curveIt->second;
220 19 : AutomationChanges changes;
221 19 : bool changed = false;
222 :
223 51 : for (const auto& edit : edits) {
224 32 : if (std::holds_alternative<AutomationPointEdit::ErasePoint>(edit.change)) {
225 7 : const auto it = curve.find(edit.tick);
226 7 : if (it != curve.end()) {
227 7 : curve.erase(it);
228 7 : changes.extend(edit.tick, edit.tick);
229 7 : changed = true;
230 : }
231 7 : continue;
232 7 : }
233 :
234 25 : utick_t rangeFrom = edit.tick;
235 25 : utick_t rangeTo = edit.tick;
236 25 : bool moved = false;
237 :
238 25 : const auto* move = std::get_if<AutomationPointEdit::MovePoint>(&edit.change);
239 25 : if (move && move->from != edit.tick) {
240 0 : curve.erase(move->from);
241 0 : rangeFrom = std::min(rangeFrom, move->from);
242 0 : rangeTo = std::max(rangeTo, move->from);
243 0 : moved = true;
244 : }
245 :
246 25 : const AutomationPoint& point = move ? move->point : std::get<AutomationPointEdit::SetPoint>(edit.change).point;
247 :
248 25 : auto pointIt = curve.lower_bound(edit.tick);
249 25 : const bool exists = pointIt != curve.end() && pointIt->first == edit.tick;
250 :
251 25 : if (exists && pointIt->second == point) {
252 1 : if (moved) {
253 0 : changes.extend(rangeFrom, rangeTo);
254 0 : changed = true;
255 : }
256 1 : continue;
257 : }
258 :
259 24 : if (exists) {
260 0 : pointIt->second = point;
261 : } else {
262 24 : curve.emplace_hint(pointIt, edit.tick, point);
263 : }
264 :
265 24 : changes.extend(rangeFrom, rangeTo);
266 24 : changed = true;
267 : }
268 :
269 19 : if (curve.empty()) {
270 1 : m_curveMap.erase(curveIt);
271 : }
272 :
273 19 : if (changed) {
274 18 : changes.affectedKeys.insert(key);
275 : }
276 :
277 19 : notifyChanged(changes);
278 19 : }
279 :
280 19 : muse::async::Channel<AutomationChanges> AutomationData::changed() const
281 : {
282 19 : return m_changesChannel;
283 : }
284 :
285 0 : std::string AutomationData::dump() const
286 : {
287 0 : auto typeName = [](AutomationType type) -> const char* {
288 0 : switch (type) {
289 0 : case AutomationType::Dynamics: return "Dynamics";
290 0 : case AutomationType::Tempo: return "Tempo";
291 0 : case AutomationType::Volume: return "Volume";
292 0 : case AutomationType::Pan: return "Pan";
293 0 : case AutomationType::Unknown: break;
294 : }
295 0 : return "Unknown";
296 : };
297 :
298 0 : auto scopeStr = [](const AutomationCurveKey& key) -> std::string {
299 0 : if (const std::optional<InstrumentTrackId> trackId = key.trackId()) {
300 0 : return "instrument = " + trackId->partId.toStdString() + "/" + trackId->instrumentId.toStdString();
301 0 : }
302 :
303 0 : if (const std::optional<muse::ID> staffId = key.staffId()) {
304 0 : std::string scope = "staff = " + staffId->toStdString();
305 0 : if (const std::optional<size_t> voiceIdx = key.voiceIdx()) {
306 0 : scope += ", voice = " + std::to_string(*voiceIdx);
307 : }
308 0 : return scope;
309 0 : }
310 :
311 0 : return "global";
312 : };
313 :
314 0 : auto inValueStr = [](const AutomationPoint::InValue& inValue) -> std::string {
315 0 : if (std::holds_alternative<AutomationPoint::ArrivalFromPrevious>(inValue)) {
316 0 : return "in = <from previous>";
317 : }
318 :
319 0 : const AutomationPoint::ExplicitArrival& arrival = std::get<AutomationPoint::ExplicitArrival>(inValue);
320 0 : std::string result = "in = " + std::to_string(arrival.value.raw());
321 0 : if (!arrival.ease.isNone()) {
322 0 : result += " (ease t = " + std::to_string(arrival.ease.t.raw()) + ", value = " + std::to_string(arrival.ease.value.raw()) + ")";
323 : }
324 0 : return result;
325 0 : };
326 :
327 0 : std::string result = "AutomationData: " + std::to_string(m_curveMap.size()) + " curves";
328 :
329 0 : for (const auto& [key, curve] : m_curveMap) {
330 0 : result += "\n[" + std::string(typeName(key.type)) + " " + scopeStr(key) + "] " + std::to_string(curve.size()) + " points";
331 :
332 0 : for (const auto& [tick, point] : curve) {
333 0 : result += "\n tick = " + std::to_string(tick) + ", " + inValueStr(point.value.inValue)
334 0 : + ", out = " + std::to_string(point.value.outValue.raw());
335 0 : if (point.generated) {
336 0 : result += ", [generated]";
337 : }
338 0 : if (point.itemId.has_value()) {
339 0 : result += ", itemId = " + point.itemId->toStdString();
340 : }
341 : }
342 : }
343 :
344 0 : return result;
345 0 : }
346 :
347 196 : void AutomationData::notifyChanged(const AutomationChanges& changes)
348 : {
349 196 : if (changes.isEmpty()) {
350 2 : return;
351 : }
352 :
353 194 : m_changesChannel.send(changes);
354 : }
|