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 "scoreautomationcontroller.h"
24 :
25 : #include <algorithm>
26 : #include <unordered_map>
27 : #include <unordered_set>
28 :
29 : #include "engraving/dom/score.h"
30 : #include "engraving/dom/repeatlist.h"
31 : #include "engraving/dom/segment.h"
32 : #include "engraving/dom/measure.h"
33 : #include "engraving/dom/dynamic.h"
34 : #include "engraving/dom/measurerepeat.h"
35 : #include "engraving/dom/part.h"
36 : #include "engraving/dom/staff.h"
37 : #include "engraving/dom/hairpin.h"
38 : #include "engraving/dom/breath.h"
39 : #include "engraving/dom/fermata.h"
40 : #include "engraving/dom/tempotext.h"
41 : #include "engraving/dom/gradualtempochange.h"
42 : #include "engraving/dom/volta.h"
43 :
44 : #include "engraving/automation/automationdata.h"
45 : #include "engraving/automation/dynamicvalues.h"
46 : #include "engraving/automation/tempovalues.h"
47 :
48 : #include "engraving/types/constants.h"
49 :
50 : #include "global/containers.h"
51 : #include "global/realfn.h"
52 : #include "global/log.h"
53 :
54 : using namespace mu::engraving;
55 :
56 : static constexpr real_t DYNAMIC_STEP = real_t::make(0.05);
57 :
58 : // Dynamics, measure repeats, fermatas, and tempo texts can only appear on these segment types
59 : static constexpr SegmentType RELEVANT_SEGMENT_TYPES = SegmentType::ChordRest | SegmentType::TimeTick;
60 :
61 86 : static BeatsPerSecond roundTempo(const BeatsPerSecond& bps)
62 : {
63 86 : return muse::RealRound(bps.val, Constants::TEMPO_PRECISION);
64 : }
65 :
66 103 : static AutomationPoint makeTempoPoint(real_t normalizedBps, std::optional<EID> itemId)
67 : {
68 103 : AutomationPoint point;
69 103 : point.value.outValue = std::clamp(normalizedBps, MIN_NORMALIZED_TEMPO, MAX_NORMALIZED_TEMPO);
70 103 : point.itemId = itemId;
71 103 : point.generated = true;
72 103 : return point;
73 : }
74 :
75 : //! TODO: map EXPONENTIAL/EASE_IN/EASE_OUT/EASE_IN_OUT to a real ease shape; every method is linear for now
76 15 : static AutomationPoint::Ease changeMethodToEase(ChangeMethod)
77 : {
78 15 : return AutomationPoint::Ease::none();
79 : }
80 :
81 : //! NOTE: segments are ordered by utick; returns the index of the last one starting at or before utick
82 16 : static std::optional<size_t> findSegmentIndex(const std::vector<RepeatSegmentInfo>& segments, utick_t utick)
83 : {
84 16 : const auto it = std::upper_bound(segments.begin(), segments.end(), utick,
85 32 : [](utick_t u, const RepeatSegmentInfo& seg) { return u < seg.utick; });
86 16 : if (it == segments.begin()) {
87 0 : return std::nullopt;
88 : }
89 :
90 32 : return size_t(std::prev(it) - segments.begin());
91 : }
92 :
93 180 : ScoreAutomationController::StaffRange::StaffRange(const Score* score, staff_idx_t staffIdxFrom, staff_idx_t staffIdxTo)
94 : {
95 180 : const staff_idx_t lastStaffIdx = score->nstaves() - 1;
96 :
97 180 : if (staffIdxFrom == muse::nidx) {
98 154 : from = 0;
99 154 : to = lastStaffIdx;
100 : } else {
101 26 : from = staffIdxFrom;
102 26 : to = std::min(staffIdxTo, lastStaffIdx);
103 : }
104 :
105 180 : isFull = (from == 0 && to == lastStaffIdx);
106 :
107 180 : if (!isFull) {
108 0 : for (staff_idx_t staffIdx = from; staffIdx <= to; ++staffIdx) {
109 0 : if (const Staff* staff = score->staff(staffIdx)) {
110 0 : staffIds.insert(staff->id());
111 : }
112 : }
113 : }
114 180 : }
115 :
116 501 : bool ScoreAutomationController::StaffRange::contains(staff_idx_t staffIdx) const
117 : {
118 501 : return isFull || (staffIdx >= from && staffIdx <= to);
119 : }
120 :
121 15 : bool ScoreAutomationController::StaffRange::contains(const muse::ID& staffId) const
122 : {
123 15 : return isFull || muse::contains(staffIds, staffId);
124 : }
125 :
126 39 : static std::optional<real_t> dynamicValue(DynamicType type, bool startValue)
127 : {
128 39 : if (auto it = ORDINARY_DYNAMIC_VALUES.find(type); it != ORDINARY_DYNAMIC_VALUES.end()) {
129 37 : return it->second;
130 : }
131 :
132 2 : if (auto it = SINGLE_NOTE_DYNAMIC_VALUES.find(type); it != SINGLE_NOTE_DYNAMIC_VALUES.end()) {
133 0 : return it->second;
134 : }
135 :
136 2 : if (auto it = COMPOUND_DYNAMIC_VALUES.find(type); it != COMPOUND_DYNAMIC_VALUES.end()) {
137 2 : return startValue ? it->second.first : it->second.second;
138 : }
139 :
140 0 : return std::nullopt;
141 : }
142 :
143 38 : static std::optional<real_t> startHairpinValue(const Hairpin* hairpin)
144 : {
145 38 : const DynamicType type = hairpin->dynamicTypeFrom();
146 38 : if (type == DynamicType::OTHER) {
147 37 : return std::nullopt;
148 : }
149 :
150 1 : return dynamicValue(type, false);
151 : }
152 :
153 38 : static DynamicType findEndDynamicType(const Hairpin* hairpin)
154 : {
155 38 : const DynamicType textType = hairpin->dynamicTypeTo();
156 38 : if (textType != DynamicType::OTHER) {
157 1 : return textType;
158 : }
159 :
160 37 : if (hairpin->spannerSegments().empty()) {
161 0 : const Segment* endSegment = hairpin->endSegment();
162 0 : if (!endSegment) {
163 0 : return DynamicType::OTHER;
164 : }
165 :
166 0 : const track_idx_t trackIdx = hairpin->track();
167 0 : const std::vector<EngravingItem*> dynamics = endSegment->findAnnotations(ElementType::DYNAMIC, trackIdx, trackIdx);
168 0 : for (const EngravingItem* item : dynamics) {
169 0 : if (item && item->isDynamic() && toDynamic(item)->playDynamic()) {
170 0 : return toDynamic(item)->dynamicType();
171 : }
172 : }
173 :
174 0 : return DynamicType::OTHER;
175 0 : }
176 :
177 37 : const LineSegment* seg = hairpin->backSegment();
178 37 : if (!seg) {
179 0 : return DynamicType::OTHER;
180 : }
181 :
182 : // Optimization: first check if there is a cached dynamic
183 37 : const EngravingItem* snappedItem = seg->ldata()->itemSnappedAfter();
184 37 : if (!snappedItem || !snappedItem->isDynamic() || !toDynamic(snappedItem)->playDynamic()) {
185 2 : snappedItem = toHairpinSegment(seg)->findElementToSnapAfter(false /*ignoreInvisible*/, true /*requirePlayable*/);
186 2 : if (!snappedItem || !snappedItem->isDynamic() || !toDynamic(snappedItem)->playDynamic()) {
187 0 : return DynamicType::OTHER;
188 : }
189 : }
190 :
191 37 : return toDynamic(snappedItem)->dynamicType();
192 : }
193 :
194 38 : static std::optional<real_t> endHairpinValue(const Hairpin* hairpin)
195 : {
196 38 : const DynamicType type = findEndDynamicType(hairpin);
197 38 : if (type == DynamicType::OTHER) {
198 0 : return std::nullopt;
199 : }
200 :
201 38 : return dynamicValue(type, true);
202 : }
203 :
204 263 : static int dynamicPriority(const EngravingItem* item)
205 : {
206 263 : return static_cast<int>(item->getProperty(Pid::VOICE_ASSIGNMENT).value<VoiceAssignment>());
207 : }
208 :
209 60 : static const AutomationPoint* activePoint(const AutomationCurveMap& curves, const AutomationCurveKey& key, utick_t tick)
210 : {
211 60 : static const AutomationCurve NO_CURVE;
212 :
213 60 : const auto keyCurveIt = curves.find(key);
214 60 : const AutomationCurve& keyCurve = keyCurveIt != curves.end() ? keyCurveIt->second : NO_CURVE;
215 60 : const auto keyIt = muse::findLessOrEqual(keyCurve, tick);
216 :
217 60 : if (key.voiceIdx().has_value()) {
218 0 : const AutomationCurveKey sharedKey = key.withoutVoice();
219 0 : const auto sharedCurveIt = curves.find(sharedKey);
220 0 : const AutomationCurve& sharedCurve = sharedCurveIt != curves.end() ? sharedCurveIt->second : NO_CURVE;
221 0 : const auto sharedIt = muse::findLessOrEqual(sharedCurve, tick);
222 :
223 0 : if (sharedIt != sharedCurve.cend()) {
224 0 : if (keyIt == keyCurve.cend() || sharedIt->first > keyIt->first) {
225 0 : return &sharedIt->second;
226 : }
227 : }
228 0 : }
229 :
230 60 : return keyIt != keyCurve.cend() ? &keyIt->second : nullptr;
231 : }
232 :
233 156 : static real_t currentTempoAt(const AutomationCurve& tempoCurve, utick_t tick)
234 : {
235 156 : const auto it = muse::findLessOrEqual(tempoCurve, tick);
236 156 : return it != tempoCurve.cend() ? it->second.value.outValue : DEFAULT_NORMALIZED_TEMPO;
237 : }
238 :
239 : struct ChangeRelevance {
240 : bool dynamics = false;
241 : bool tempo = false;
242 : };
243 :
244 968 : static ChangeRelevance classifyChanges(const ScoreChanges& changes)
245 : {
246 968 : ChangeRelevance result;
247 :
248 3869 : for (const ElementType type : changes.changedTypes) {
249 2920 : switch (type) {
250 64 : case ElementType::DYNAMIC:
251 : case ElementType::HAIRPIN:
252 : case ElementType::HAIRPIN_SEGMENT:
253 : case ElementType::MEASURE_REPEAT:
254 64 : result.dynamics = true;
255 64 : break;
256 46 : case ElementType::BREATH:
257 : case ElementType::LAYOUT_BREAK:
258 : case ElementType::FERMATA:
259 : case ElementType::GRADUAL_TEMPO_CHANGE:
260 : case ElementType::GRADUAL_TEMPO_CHANGE_SEGMENT:
261 : case ElementType::TEMPO_TEXT:
262 46 : result.tempo = true;
263 46 : break;
264 3 : case ElementType::VOLTA:
265 : case ElementType::VOLTA_SEGMENT:
266 : case ElementType::MARKER:
267 : case ElementType::JUMP:
268 3 : return { true, true };
269 2807 : default:
270 2807 : break;
271 : }
272 :
273 2917 : if (result.dynamics && result.tempo) {
274 16 : break;
275 : }
276 : }
277 :
278 : // VoiceAssignment change shifts which staves a dynamic/hairpin covers,
279 : // so the old points on staves outside the new assignment must also be cleared
280 965 : result.dynamics |= muse::contains(changes.changedPropertyIdSet, Pid::VOICE_ASSIGNMENT);
281 :
282 : static const std::unordered_set<Pid> REPEAT_PROPERTIES {
283 : Pid::REPEAT_START,
284 : Pid::REPEAT_END,
285 : Pid::REPEAT_JUMP,
286 : Pid::REPEAT_COUNT,
287 971 : };
288 :
289 965 : bool repeatPropertyChanged = false;
290 4810 : for (const Pid pid : REPEAT_PROPERTIES) {
291 3854 : if (muse::contains(changes.changedPropertyIdSet, pid)) {
292 9 : repeatPropertyChanged = true;
293 9 : break;
294 : }
295 : }
296 :
297 965 : if (repeatPropertyChanged) {
298 9 : result.dynamics = true;
299 9 : result.tempo = true;
300 : }
301 :
302 965 : return result;
303 : }
304 :
305 9559 : const TempoTimeline& ScoreAutomationController::tempoTimeline(bool expandRepeats) const
306 : {
307 9559 : if (m_tempoTimelineOverride) {
308 0 : return *m_tempoTimelineOverride;
309 : }
310 :
311 9559 : return expandRepeats ? m_tempoTimeline : m_flattenedTempoTimeline;
312 : }
313 :
314 26 : void ScoreAutomationController::setTempoMultiplier(const BeatsPerSecond& bps)
315 : {
316 26 : m_tempoTimeline.setTempoMultiplier(bps);
317 26 : m_flattenedTempoTimeline.setTempoMultiplier(bps);
318 26 : }
319 :
320 0 : void ScoreAutomationController::setTempoTimelineOverride(std::optional<TempoTimeline> timeline)
321 : {
322 0 : m_tempoTimelineOverride = std::move(timeline);
323 0 : }
324 :
325 144 : void ScoreAutomationController::init(Score* score)
326 : {
327 144 : m_score = score;
328 :
329 144 : if (!m_automationData) {
330 17 : m_automationData = std::make_shared<AutomationData>();
331 : }
332 :
333 144 : AutomationCurveMap curves = m_automationData->curves();
334 144 : fullRebuild(curves);
335 144 : }
336 :
337 151 : void ScoreAutomationController::fullRebuild(AutomationCurveMap& curves, bool includeTempo, bool includeDynamics)
338 : {
339 151 : const UpdateRequest request { 0, muse::nidx, muse::nidx, includeTempo, includeDynamics };
340 151 : update(request, curves);
341 151 : }
342 :
343 9531 : void ScoreAutomationController::ensureInitialized(Score* score)
344 : {
345 9531 : if (m_score) {
346 9413 : return;
347 : }
348 118 : init(score);
349 : }
350 :
351 457 : void ScoreAutomationController::insertTime(const Fraction& tick, const Fraction& len, const std::vector<RepeatSegmentInfo>& oldSegments)
352 : {
353 459 : TRACEFUNC;
354 :
355 457 : const int diff = len.ticks();
356 457 : if (!m_score || !m_automationData || m_automationData->isEmpty() || diff == 0) {
357 451 : return;
358 : }
359 :
360 6 : const int rawInsertTick = tick.ticks();
361 6 : const std::vector<RepeatSegmentInfo> newSegments = m_score->expandedRepeatList().segmentInfoList();
362 :
363 6 : AutomationCurveMap curves = m_automationData->curves();
364 :
365 24 : for (auto& entry : curves) {
366 18 : AutomationCurve& curve = entry.second;
367 18 : AutomationCurve shifted;
368 :
369 136 : for (const auto& [utick, point] : curve) {
370 : // Generated points are regenerated by the full update below - only authored points need relocating
371 118 : if (point.generated) {
372 106 : continue;
373 : }
374 :
375 : // Re-express the point in raw-tick coordinates local to its repeat pass,
376 : // so the shift below is correct even for a pass other than the first
377 16 : const std::optional<size_t> oldIdx = findSegmentIndex(oldSegments, utick);
378 16 : if (!oldIdx || *oldIdx >= newSegments.size()) {
379 0 : continue; // no corresponding pass anymore (edit changed the repeat structure itself)
380 : }
381 :
382 16 : const RepeatSegmentInfo& oldSeg = oldSegments[*oldIdx];
383 16 : const int oldRawTick = utick - (oldSeg.utick - oldSeg.tick);
384 :
385 16 : if (diff < 0 && oldRawTick >= rawInsertTick && oldRawTick < rawInsertTick - diff) {
386 4 : continue; // point sat inside the removed range
387 : }
388 :
389 12 : const int newRawTick = oldRawTick >= rawInsertTick ? oldRawTick + diff : oldRawTick;
390 :
391 : // Re-derive the utick for the *same* repeat pass under the new (post-edit) RepeatList
392 12 : const RepeatSegmentInfo& newSeg = newSegments[*oldIdx];
393 12 : const utick_t newUtick = utick_t(newRawTick + (newSeg.utick - newSeg.tick));
394 :
395 12 : shifted.emplace_hint(shifted.end(), newUtick, point);
396 : }
397 :
398 18 : curve = std::move(shifted);
399 18 : }
400 :
401 : // Kept as empty entries, not erased: replaceCurves() only clears a key when given it with an
402 : // empty curve - a key absent from the map below would leave stale data untouched instead
403 6 : fullRebuild(curves);
404 457 : }
405 :
406 972 : void ScoreAutomationController::update(const ScoreChanges& changes)
407 : {
408 972 : if (changes.isTextEditing) {
409 868 : return;
410 : }
411 :
412 968 : const ChangeRelevance relevance = classifyChanges(changes);
413 968 : if (!relevance.dynamics && !relevance.tempo) {
414 864 : return;
415 : }
416 :
417 104 : UpdateRequest request;
418 104 : request.includeTempo = relevance.tempo;
419 104 : request.includeDynamics = relevance.dynamics;
420 :
421 : // When tempo is relevant, the whole score must be reparsed in one full pass,
422 : // so tickFrom/staffIdxFrom/staffIdxTo are left at their defaults (0/nidx/nidx)
423 104 : if (!relevance.tempo) {
424 46 : request.tickFrom = std::max(changes.tickFrom, 0);
425 :
426 : // VoiceAssignment change shifts which staves a dynamic/hairpin covers,
427 : // so the old points on staves outside the new assignment must also be cleared
428 46 : const bool voiceAssignmentChanged = muse::contains(changes.changedPropertyIdSet, Pid::VOICE_ASSIGNMENT);
429 46 : if (!voiceAssignmentChanged) {
430 46 : request.staffIdxFrom = changes.staffIdxFrom;
431 46 : request.staffIdxTo = changes.staffIdxTo;
432 : }
433 : }
434 :
435 104 : if (!m_automationData) {
436 4 : m_automationData = std::make_shared<AutomationData>();
437 : }
438 :
439 104 : update(request, m_automationData->curves());
440 : }
441 :
442 15 : void ScoreAutomationController::editPoints(const AutomationCurveKey& key, AutomationPointEdits& edits)
443 : {
444 15 : IF_ASSERT_FAILED(m_automationData && m_score) {
445 0 : return;
446 : }
447 :
448 : // Tempo edits can affect later relative tempo markings, so we need a full rescan
449 15 : if (key.type == AutomationType::Tempo) {
450 1 : AutomationCurveMap curves = m_automationData->curves();
451 1 : AutomationCurve& tempoCurve = curves[key];
452 :
453 2 : for (const AutomationPointEdit& edit : edits) {
454 1 : if (const auto* setPoint = std::get_if<AutomationPointEdit::SetPoint>(&edit.change)) {
455 1 : tempoCurve[edit.tick] = setPoint->point;
456 0 : } else if (const auto* movePoint = std::get_if<AutomationPointEdit::MovePoint>(&edit.change)) {
457 0 : if (movePoint->from != edit.tick) {
458 0 : tempoCurve.erase(movePoint->from);
459 : }
460 0 : tempoCurve[edit.tick] = movePoint->point;
461 : } else {
462 0 : tempoCurve.erase(edit.tick);
463 : }
464 : }
465 :
466 1 : fullRebuild(curves, /*includeTempo*/ true, /*includeDynamics*/ false);
467 1 : return;
468 1 : }
469 :
470 : // Dynamics/Volume/Pan edits don't affect other points, so no rescan is needed
471 14 : mirrorEditsToRepeats(key, edits);
472 14 : m_automationData->editPoints(key, edits);
473 : }
474 :
475 255 : void ScoreAutomationController::update(const UpdateRequest& request, const AutomationCurveMap& curves)
476 : {
477 259 : TRACEFUNC;
478 :
479 255 : if (!m_score) {
480 101 : return;
481 : }
482 :
483 154 : const RepeatList& repeatList = m_score->expandedRepeatList();
484 154 : if (repeatList.empty()) {
485 0 : return;
486 : }
487 :
488 154 : const auto repeatFromIt = std::find_if(repeatList.cbegin(), repeatList.cend(),
489 154 : [tickFrom = request.tickFrom](const RepeatSegment* seg) {
490 154 : return seg->endTick() > tickFrom;
491 : });
492 :
493 154 : IF_ASSERT_FAILED(repeatFromIt != repeatList.cend()) {
494 0 : return;
495 : }
496 :
497 154 : const StaffRange range(m_score, request.staffIdxFrom, request.staffIdxTo);
498 154 : const RepeatSegment* firstSeg = *repeatFromIt;
499 :
500 154 : UpdateContext ctx;
501 154 : ctx.request = request;
502 154 : ctx.tempoMultiplier = m_tempoTimeline.tempoMultiplier();
503 :
504 : // The first utick the rebuild will regenerate; everything before it is kept as is
505 154 : ctx.clearFromUTick = std::max(firstSeg->utick, request.tickFrom + (firstSeg->utick - firstSeg->tick));
506 :
507 : // Copy only the curves this update can affect, dropping the generated points that the rebuild
508 : // below re-creates; all other curves stay untouched
509 154 : copyCurvesForRebuild(curves, range, ctx.clearFromUTick, request.includeTempo, request.includeDynamics, ctx.curves);
510 :
511 154 : AutomationCurve emptyTempoCurve;
512 :
513 154 : if (request.includeTempo) {
514 153 : ctx.tempoCurve = &ctx.curves[TEMPO_KEY];
515 153 : fillNoRepeatTempoCurve(*ctx.tempoCurve, ctx.noRepeatTempoCurve);
516 : } else {
517 : // Read-only access for e.g. compound dynamics,
518 : // which need the current tempo even when this update doesn't rebuild it
519 1 : const auto tempoIt = ctx.curves.find(TEMPO_KEY);
520 1 : ctx.tempoCurve = tempoIt != ctx.curves.end() ? &tempoIt->second : &emptyTempoCurve;
521 : }
522 :
523 : // Step 1: segment dynamics (+ tempo/pause when relevant): populates ctx.dynamicPriorities
524 154 : int prevSegRawEndTick = -1;
525 417 : for (auto it = repeatFromIt; it != repeatList.cend(); ++it) {
526 263 : const RepeatSegment* seg = *it;
527 263 : const int tickOffset = seg->utick - seg->tick;
528 :
529 263 : if (request.includeTempo && prevSegRawEndTick != -1 && seg->tick != prevSegRawEndTick) {
530 : // Playback jumped elsewhere in the score (repeat loop-back / volta switch) - this
531 : // pass's start needs its tempo reset from raw tick order; resolved after Step 2, once
532 : // any volta reset affecting the lookup has landed in noRepeatTempoCurve
533 104 : ctx.pendingTempoResets.push_back({ seg->utick, seg->tick });
534 : }
535 263 : prevSegRawEndTick = seg->endTick();
536 :
537 : // tickFrom limits only the first affected repeat segment: later segments may replay
538 : // measures from before tickFrom, and all their points were cleared, so rebuild them in full
539 263 : const int measureFrom = (it == repeatFromIt) ? std::max(seg->tick, request.tickFrom) : seg->tick;
540 :
541 263 : const std::vector<const Measure*>& segMeasures = seg->measureList();
542 1183 : for (size_t mi = 0; mi < segMeasures.size(); ++mi) {
543 920 : const Measure* measure = segMeasures[mi];
544 920 : if (measure->endTick().ticks() <= measureFrom) {
545 0 : continue;
546 : }
547 :
548 920 : if (request.includeTempo) {
549 912 : collectPauses(measure, tickOffset, ctx.pauses, ctx.noRepeatPauses);
550 912 : if (measure->isAnacrusis()) {
551 1 : std::optional<utick_t> nextMeasureUTick;
552 1 : if (mi + 1 < segMeasures.size()) {
553 1 : nextMeasureUTick = segMeasures[mi + 1]->tick().ticks() + tickOffset;
554 : }
555 1 : ctx.anacrusisMeasures.push_back({ measure, tickOffset, nextMeasureUTick });
556 : }
557 : }
558 :
559 : // A MeasureRepeat can only ever sit on the measure's first ChordRest segment
560 920 : const Segment* firstChordRestSegment = measure->first(SegmentType::ChordRest);
561 920 : if (firstChordRestSegment && firstChordRestSegment->tick().ticks() >= measureFrom) {
562 920 : collectMeasureRepeats(firstChordRestSegment, tickOffset, range, ctx.measureRepeats);
563 : }
564 :
565 3989 : for (const Segment* segment = measure->first(RELEVANT_SEGMENT_TYPES); segment;
566 3069 : segment = segment->next(RELEVANT_SEGMENT_TYPES)) {
567 3069 : if (segment->annotations().empty() || segment->tick().ticks() < measureFrom) {
568 2719 : continue;
569 : }
570 :
571 350 : addSegmentPoints(segment, tickOffset, range, ctx);
572 : }
573 : }
574 : }
575 :
576 154 : if (!ctx.anacrusisMeasures.empty()) {
577 1 : fixAnacrusisTempo(ctx);
578 : }
579 :
580 : // Step 2: spanner points (+ gradual tempo change when relevant):
581 : // ctx.dynamicPriorities fully populated; sets inValues on hairpin-end dynamics
582 417 : for (auto it = repeatFromIt; it != repeatList.cend(); ++it) {
583 263 : const RepeatSegment* seg = *it;
584 263 : addSpannerPoints(m_score, seg->tick, seg->endTick(), seg->utick - seg->tick, range, ctx);
585 : }
586 :
587 : // Step 3: resolve pending tempo resets now that noRepeatTempoCurve is fully built
588 154 : resolvePendingTempoResets(ctx);
589 :
590 : // Step 4: fill each voice curve with any points from the base (all-voice) curve it doesn't already have
591 154 : fillVoiceCurvesFromBase(ctx);
592 :
593 : // Step 5: measure repeats
594 154 : addMeasureRepeatPoints(ctx);
595 :
596 : // Step 6: Mirror directly-authored points to repeats
597 154 : mirrorAuthoredPointsToRepeats(ctx);
598 :
599 154 : if (request.includeTempo) {
600 : // TempoTimeline falls back to the default tempo when the curve has no points
601 153 : m_tempoTimeline.rebuild(*ctx.tempoCurve, ctx.pauses);
602 153 : m_flattenedTempoTimeline.rebuild(ctx.noRepeatTempoCurve, ctx.noRepeatPauses);
603 : }
604 :
605 : // Passed through as-is: replaceCurves() clears a key when given an empty curve for it
606 154 : m_automationData->replaceCurves(ctx.curves);
607 255 : }
608 :
609 153 : void ScoreAutomationController::fillNoRepeatTempoCurve(const AutomationCurve& tempoCurve, AutomationCurve& noRepeatTempoCurve)
610 : {
611 156 : TRACEFUNC;
612 :
613 153 : const RepeatList& repeatList = m_score->expandedRepeatList();
614 :
615 : // tempoCurve is expected to be authored-only
616 154 : for (const auto& [utick, point] : tempoCurve) {
617 1 : const auto segIt = repeatList.findRepeatSegmentFromUTick(utick);
618 1 : IF_ASSERT_FAILED(segIt != repeatList.cend()) {
619 0 : continue;
620 : }
621 :
622 1 : const int segTickOffset = (*segIt)->utick - (*segIt)->tick;
623 1 : noRepeatTempoCurve.emplace(utick - segTickOffset, point);
624 : }
625 153 : }
626 :
627 154 : void ScoreAutomationController::mirrorAuthoredPointsToRepeats(UpdateContext& ctx)
628 : {
629 157 : TRACEFUNC;
630 :
631 389 : for (auto& [key, curve] : ctx.curves) {
632 235 : if (curve.empty()) {
633 222 : continue;
634 : }
635 :
636 114 : AutomationPointEdits edits;
637 731 : for (const auto& [tick, point] : curve) {
638 617 : if (!point.generated) {
639 25 : edits.push_back({ tick, AutomationPointEdit::SetPoint { point } });
640 : }
641 : }
642 :
643 114 : if (edits.empty()) {
644 101 : continue;
645 : }
646 :
647 : //! NOTE: appends the corresponding mirrored edit(s) - one per other repeat pass - directly to edits
648 13 : mirrorEditsToRepeats(key, edits);
649 :
650 39 : for (const AutomationPointEdit& edit : edits) {
651 26 : const auto* setPoint = std::get_if<AutomationPointEdit::SetPoint>(&edit.change);
652 26 : IF_ASSERT_FAILED(setPoint) {
653 0 : continue;
654 : }
655 :
656 26 : curve.insert_or_assign(edit.tick, setPoint->point);
657 : }
658 114 : }
659 154 : }
660 :
661 154 : void ScoreAutomationController::copyCurvesForRebuild(const AutomationCurveMap& curves, const StaffRange& range, utick_t clearFromUTick,
662 : bool includeTempo, bool includeDynamics, AutomationCurveMap& destCurves)
663 : {
664 157 : TRACEFUNC;
665 :
666 178 : for (const auto& [key, curve] : curves) {
667 24 : const bool isDynamics = key.type == AutomationType::Dynamics;
668 24 : const bool isTempo = key.type == AutomationType::Tempo;
669 :
670 : // Instrument curves (Volume, Pan) aren't staff-scoped or score-derived, so only
671 : // user points survive here - mirrorAuthoredPointsToRepeats() recomputes the rest
672 24 : if (isDynamics) {
673 15 : if (!includeDynamics) {
674 0 : continue;
675 : }
676 :
677 15 : const std::optional<muse::ID> staffId = key.staffId();
678 15 : if (!staffId || !range.contains(*staffId)) {
679 0 : continue;
680 : }
681 : }
682 :
683 24 : AutomationCurve& curveCopy = destCurves.emplace_hint(destCurves.end(), key, AutomationCurve())->second;
684 :
685 70 : for (const auto& [tick, point] : curve) {
686 : // Dynamics: keep generated points before clearFromUTick (the incremental rebuild regenerates the rest)
687 : // Tempo: keep generated points only if this update doesn't include tempo (otherwise the whole curve is regenerated below)
688 : // Instrument: never keep generated points (there's no generation step for them, only user edits)
689 46 : const bool keepGenerated = isDynamics ? tick < clearFromUTick : isTempo && !includeTempo;
690 46 : if (!point.generated || keepGenerated) {
691 14 : curveCopy.emplace_hint(curveCopy.end(), tick, point);
692 : }
693 : }
694 : }
695 154 : }
696 :
697 350 : void ScoreAutomationController::addSegmentPoints(const Segment* segment, int tickOffset, const StaffRange& range,
698 : UpdateContext& ctx)
699 : {
700 352 : TRACEFUNC;
701 :
702 350 : double fermataStretch = 0.0;
703 350 : const Fermata* fermata = nullptr;
704 :
705 727 : for (const EngravingItem* annotation : segment->annotations()) {
706 377 : if (!annotation) {
707 0 : continue;
708 : }
709 :
710 377 : if (annotation->isDynamic()) {
711 225 : if (ctx.request.includeDynamics && range.contains(annotation->staffIdx())) {
712 225 : addDynamicPoints(toDynamic(annotation), tickOffset, range, ctx);
713 : }
714 152 : } else if (ctx.request.includeTempo && annotation->isFermata() && toFermata(annotation)->play()) {
715 12 : const Fermata* f = toFermata(annotation);
716 12 : if (f->timeStretch() > fermataStretch) {
717 12 : fermataStretch = f->timeStretch();
718 12 : fermata = f;
719 : }
720 140 : } else if (ctx.request.includeTempo && annotation->isTempoText()) {
721 77 : addTempoTextPoint(toTempoText(annotation), tickOffset, ctx);
722 : }
723 : }
724 :
725 350 : if (fermata && !muse::RealIsNull(fermataStretch) && !muse::RealIsEqual(fermataStretch, 1.0)) {
726 12 : addFermataStretchPoints(fermata, tickOffset, fermataStretch, ctx);
727 : }
728 350 : }
729 :
730 225 : void ScoreAutomationController::addDynamicPoints(const Dynamic* dynamic, int tickOffset, const StaffRange& range, UpdateContext& ctx)
731 : {
732 227 : TRACEFUNC;
733 :
734 225 : if (!dynamic->playDynamic()) {
735 0 : return;
736 : }
737 :
738 225 : const DynamicType dynamicType = dynamic->dynamicType();
739 225 : const utick_t tick = dynamic->tick().ticks() + tickOffset;
740 :
741 225 : DynamicInfo info;
742 225 : info.tick = tick;
743 :
744 225 : if (auto ordinaryIt = ORDINARY_DYNAMIC_VALUES.find(dynamicType); ordinaryIt != ORDINARY_DYNAMIC_VALUES.end()) {
745 176 : info.kind = DynamicInfo::Ordinary { ordinaryIt->second };
746 49 : } else if (auto singleNoteIt = SINGLE_NOTE_DYNAMIC_VALUES.find(dynamicType); singleNoteIt != SINGLE_NOTE_DYNAMIC_VALUES.end()) {
747 22 : DynamicInfo::SingleNote singleNote { singleNoteIt->second, std::nullopt };
748 22 : if (const Segment* nextSeg = dynamic->segment()->next()) {
749 22 : singleNote.nextTick = nextSeg->tick().ticks() + tickOffset;
750 : }
751 22 : info.kind = singleNote;
752 27 : } else if (auto compoundIt = COMPOUND_DYNAMIC_VALUES.find(dynamicType); compoundIt != COMPOUND_DYNAMIC_VALUES.end()) {
753 27 : const BeatsPerSecond tempo = denormalizeTempo(currentTempoAt(*ctx.tempoCurve, tick)) * ctx.tempoMultiplier;
754 27 : info.kind = DynamicInfo::Compound { compoundIt->second.first, compoundIt->second.second,
755 27 : tick + dynamic->velocityChangeLength(tempo).ticks() };
756 : } else {
757 0 : NOT_SUPPORTED;
758 0 : return;
759 : }
760 :
761 225 : const std::vector<AutomationCurveKey> keys = resolveKeys(dynamic, AutomationType::Dynamics, range);
762 225 : if (keys.empty()) {
763 0 : return;
764 : }
765 :
766 225 : info.priority = dynamicPriority(dynamic);
767 :
768 225 : info.eid = dynamic->eid();
769 225 : if (!info.eid.isValid()) {
770 53 : info.eid = dynamic->assignNewEID();
771 : }
772 :
773 453 : for (const AutomationCurveKey& key : keys) {
774 228 : addDynamicPoints(info, key, ctx);
775 : }
776 225 : }
777 :
778 228 : void ScoreAutomationController::addDynamicPoints(const DynamicInfo& info, const AutomationCurveKey& key, UpdateContext& ctx)
779 : {
780 228 : IF_ASSERT_FAILED(key.isValid()) {
781 0 : return;
782 : }
783 :
784 228 : AutomationCurve& curve = ctx.curves[key];
785 228 : std::map<utick_t, int>& tickPrioMap = ctx.dynamicPriorities[key];
786 :
787 228 : if (const auto* ordinary = std::get_if<DynamicInfo::Ordinary>(&info.kind)) {
788 179 : AutomationPoint point;
789 179 : point.value.inValue = AutomationPoint::ArrivalFromPrevious {};
790 179 : point.value.outValue = ordinary->value;
791 179 : point.itemId = info.eid;
792 179 : point.generated = true;
793 179 : addDynamicPoint(curve, tickPrioMap, info.tick, point, info.priority);
794 179 : return;
795 : }
796 :
797 49 : if (const auto* singleNote = std::get_if<DynamicInfo::SingleNote>(&info.kind)) {
798 22 : const AutomationPoint* prevPoint = singleNote->nextTick ? activePoint(ctx.curves, key, info.tick) : nullptr;
799 :
800 22 : AutomationPoint point;
801 22 : point.value.inValue = AutomationPoint::ArrivalFromPrevious {};
802 22 : point.value.outValue = singleNote->value;
803 22 : point.itemId = info.eid;
804 22 : point.generated = true;
805 22 : addDynamicPoint(curve, tickPrioMap, info.tick, point, info.priority);
806 :
807 22 : if (singleNote->nextTick) {
808 : // Recovers to whatever was active before this dynamic
809 22 : AutomationPoint nextPoint = prevPoint ? *prevPoint : AutomationPoint{};
810 22 : const AutomationPoint::Ease preservedEase = ease(nextPoint).value_or(AutomationPoint::Ease::none());
811 22 : nextPoint.value.inValue = AutomationPoint::ExplicitArrival { point.value.outValue, preservedEase };
812 22 : nextPoint.generated = true;
813 22 : tryAddDynamicPoint(curve, tickPrioMap, *singleNote->nextTick, nextPoint, info.priority);
814 : }
815 :
816 22 : return;
817 : }
818 :
819 27 : if (const auto* compound = std::get_if<DynamicInfo::Compound>(&info.kind)) {
820 27 : AutomationPoint startPoint;
821 27 : startPoint.value.inValue = AutomationPoint::ArrivalFromPrevious {};
822 27 : startPoint.value.outValue = compound->startValue;
823 27 : startPoint.itemId = info.eid;
824 27 : startPoint.generated = true;
825 27 : addDynamicPoint(curve, tickPrioMap, info.tick, startPoint, info.priority);
826 :
827 27 : AutomationPoint endPoint;
828 27 : endPoint.value.outValue = compound->endValue;
829 27 : endPoint.value.inValue = AutomationPoint::ExplicitArrival { endPoint.value.outValue, AutomationPoint::Ease::none() };
830 27 : endPoint.itemId = info.eid;
831 27 : endPoint.generated = true;
832 27 : tryAddDynamicPoint(curve, tickPrioMap, compound->endPointTick, endPoint, info.priority);
833 : }
834 : }
835 :
836 263 : void ScoreAutomationController::addSpannerPoints(const Score* score, int repeatStartTick, int repeatEndTick,
837 : int tickOffset, const StaffRange& range, UpdateContext& ctx)
838 : {
839 266 : TRACEFUNC;
840 :
841 263 : const SpannerMap& spannerMap = score->spannerMap();
842 263 : if (spannerMap.empty()) {
843 174 : return;
844 : }
845 :
846 89 : const int overlapStart = std::max(repeatStartTick + 1, ctx.clearFromUTick - tickOffset);
847 89 : const int overlapStop = repeatEndTick - 1;
848 89 : if (overlapStart > overlapStop) {
849 0 : return;
850 : }
851 :
852 89 : const auto& intervals = spannerMap.findOverlapping(overlapStart, overlapStop);
853 170 : for (const auto& interval : intervals) {
854 81 : const Spanner* spanner = interval.value;
855 81 : if (!spanner->playSpanner()) {
856 0 : continue;
857 : }
858 :
859 81 : if (spanner->isHairpin()) {
860 38 : if (!ctx.request.includeDynamics) {
861 0 : continue;
862 : }
863 38 : const Hairpin* hairpin = toHairpin(spanner);
864 38 : const std::vector<AutomationCurveKey> keys = resolveKeys(hairpin, AutomationType::Dynamics, range);
865 38 : if (!keys.empty()) {
866 38 : addHairpinPoints(hairpin, tickOffset, keys, ctx);
867 : }
868 81 : } else if (ctx.request.includeTempo && spanner->isGradualTempoChange()) {
869 15 : addGradualTempoChangePoints(toGradualTempoChange(spanner), tickOffset, ctx);
870 28 : } else if (ctx.request.includeTempo && spanner->isVolta()) {
871 6 : addVoltaTempoResetPoint(toVolta(spanner), ctx);
872 : }
873 : }
874 263 : }
875 :
876 38 : void ScoreAutomationController::addHairpinPoints(const Hairpin* hairpin, int tickOffset, const std::vector<AutomationCurveKey>& keys,
877 : UpdateContext& ctx)
878 : {
879 38 : HairpinInfo info;
880 38 : info.from = hairpin->tick().ticks() + tickOffset;
881 38 : info.to = info.from + hairpin->ticks().ticks();
882 :
883 : // --- Check start tick
884 : {
885 38 : const Segment* startSegment = hairpin->startSegment();
886 38 : const track_idx_t trackIdx = hairpin->track();
887 : const Dynamic* startDynamic = startSegment
888 38 : ? toDynamic(startSegment->findAnnotation(ElementType::DYNAMIC, trackIdx, trackIdx))
889 38 : : nullptr;
890 38 : if (startDynamic && muse::contains(COMPOUND_DYNAMIC_VALUES, startDynamic->dynamicType())) {
891 : // The hairpin starts with a compound dynamic; we should start the hairpin after the transition is complete
892 : // This solution should be replaced once we have better infrastructure to see relations between Dynamics and Hairpins.
893 5 : const BeatsPerSecond tempo = denormalizeTempo(currentTempoAt(*ctx.tempoCurve, info.from)) * ctx.tempoMultiplier;
894 5 : info.from += startDynamic->velocityChangeLength(tempo).ticks();
895 : }
896 : }
897 :
898 38 : IF_ASSERT_FAILED(info.from < info.to) {
899 0 : return;
900 : }
901 :
902 38 : info.eid = hairpin->eid();
903 38 : if (!info.eid.isValid()) {
904 15 : info.eid = hairpin->assignNewEID();
905 : }
906 :
907 38 : info.priority = dynamicPriority(hairpin);
908 38 : info.isCrescendo = hairpin->isCrescendo();
909 38 : info.nominalValueFrom = startHairpinValue(hairpin);
910 38 : info.nominalValueTo = endHairpinValue(hairpin);
911 :
912 76 : for (const AutomationCurveKey& key : keys) {
913 38 : addHairpinPoints(info, key, ctx);
914 : }
915 : }
916 :
917 38 : void ScoreAutomationController::addHairpinPoints(const HairpinInfo& info, const AutomationCurveKey& key, UpdateContext& ctx)
918 : {
919 38 : AutomationCurve& curve = ctx.curves[key];
920 38 : std::map<utick_t, int>& tickPrioMap = ctx.dynamicPriorities[key];
921 :
922 : // --- Determine valueFrom
923 38 : const AutomationPoint* prevPoint = activePoint(ctx.curves, key, info.from);
924 38 : const real_t prevOutValue = prevPoint ? prevPoint->value.outValue : real_t(0.0);
925 :
926 : // If the hairpin has no specific start value, use the currently-applicable value at the start tick of the hairpin
927 38 : const real_t valueFrom = info.nominalValueFrom.value_or(prevOutValue);
928 :
929 : {
930 38 : AutomationPoint startPoint;
931 38 : startPoint.value.outValue = valueFrom;
932 38 : startPoint.value.inValue = AutomationPoint::ArrivalFromPrevious {};
933 38 : startPoint.itemId = info.eid;
934 38 : startPoint.generated = true;
935 38 : tryAddDynamicPoint(curve, tickPrioMap, info.from, startPoint, info.priority);
936 : }
937 :
938 : // --- Determine valueTo
939 : // If there is an end dynamic marking, check if it matches the 'direction' of the hairpin (cresc. vs dim.)
940 38 : const bool useNominalValueTo = info.nominalValueTo.has_value()
941 44 : && (info.isCrescendo ? info.nominalValueTo.value() > valueFrom
942 6 : : info.nominalValueTo.value() < valueFrom);
943 :
944 : // --- Check end tick
945 : const real_t valueTo = useNominalValueTo
946 38 : ? info.nominalValueTo.value()
947 38 : : valueFrom + (info.isCrescendo ? DYNAMIC_STEP : -DYNAMIC_STEP);
948 :
949 38 : const auto endPointIt = curve.find(info.to);
950 38 : const bool hasPointAtEnd = endPointIt != curve.end();
951 :
952 38 : if (hasPointAtEnd) {
953 : // A point already exists at the end tick; encode the hairpin's arrival via inValue, but only
954 : // if this hairpin has at least as much priority as whoever placed that point
955 37 : const auto tickIt = tickPrioMap.find(info.to);
956 37 : const bool canModify = tickIt == tickPrioMap.end() || info.priority >= tickIt->second;
957 37 : if (canModify) {
958 37 : const AutomationPoint::Ease preservedEase = ease(endPointIt->second).value_or(AutomationPoint::Ease::none());
959 37 : endPointIt->second.value.inValue = AutomationPoint::ExplicitArrival { valueTo, preservedEase };
960 : }
961 37 : return;
962 : }
963 :
964 1 : if (info.from < info.to) {
965 1 : AutomationPoint point;
966 1 : point.value.outValue = valueTo;
967 1 : point.value.inValue = AutomationPoint::ExplicitArrival { point.value.outValue, AutomationPoint::Ease::none() };
968 1 : point.itemId = info.eid;
969 1 : point.generated = true;
970 1 : tryAddDynamicPoint(curve, tickPrioMap, info.to, point, info.priority);
971 : }
972 : }
973 :
974 154 : void ScoreAutomationController::fillVoiceCurvesFromBase(UpdateContext& ctx)
975 : {
976 157 : TRACEFUNC;
977 :
978 389 : for (auto& [key, curve] : ctx.curves) {
979 235 : if (!key.voiceIdx().has_value()) {
980 211 : continue;
981 : }
982 :
983 24 : const AutomationCurveKey baseKey = key.withoutVoice();
984 24 : const auto baseIt = ctx.curves.find(baseKey);
985 24 : if (baseIt == ctx.curves.end()) {
986 0 : continue;
987 : }
988 :
989 24 : curve.insert(baseIt->second.cbegin(), baseIt->second.cend());
990 24 : }
991 154 : }
992 :
993 1296 : void ScoreAutomationController::collectMeasureRepeats(const Segment* segment, int tickOffset, const StaffRange& range,
994 : MeasureRepeats& result)
995 : {
996 1296 : if (!segment->isChordRestType()) {
997 0 : return;
998 : }
999 :
1000 1299 : TRACEFUNC;
1001 :
1002 3450 : for (staff_idx_t staffIdx = range.from; staffIdx <= range.to; ++staffIdx) {
1003 2154 : const EngravingItem* item = segment->element(staff2track(staffIdx));
1004 2154 : if (!item || !item->isMeasureRepeat()) {
1005 2066 : continue;
1006 : }
1007 :
1008 88 : result.emplace_back(toMeasureRepeat(item), tickOffset);
1009 : }
1010 1296 : }
1011 :
1012 154 : void ScoreAutomationController::addMeasureRepeatPoints(UpdateContext& ctx)
1013 : {
1014 157 : TRACEFUNC;
1015 :
1016 154 : if (ctx.measureRepeats.empty()) {
1017 125 : return;
1018 : }
1019 :
1020 29 : std::unordered_map<uint64_t, std::vector<AutomationCurve*> > curvesByStaff;
1021 104 : for (auto& [key, curve] : ctx.curves) {
1022 75 : const std::optional<muse::ID> staffId = key.staffId();
1023 75 : if (staffId) {
1024 46 : curvesByStaff[staffId->toUint64()].push_back(&curve);
1025 : }
1026 : }
1027 :
1028 29 : if (curvesByStaff.empty()) {
1029 6 : return;
1030 : }
1031 :
1032 : struct MeasureRange {
1033 : int tickShift = 0;
1034 : utick_t srcFrom = 0;
1035 : utick_t srcTo = 0;
1036 : };
1037 :
1038 46 : for (const auto& [mr, tickOffset] : ctx.measureRepeats) {
1039 23 : const Staff* staff = mr->staff();
1040 23 : if (!staff) {
1041 0 : continue;
1042 : }
1043 :
1044 23 : const auto staffCurvesIt = curvesByStaff.find(staff->id().toUint64());
1045 23 : if (staffCurvesIt == curvesByStaff.end()) {
1046 0 : continue;
1047 : }
1048 :
1049 23 : std::vector<MeasureRange> ranges;
1050 23 : ranges.reserve(mr->numMeasures());
1051 :
1052 23 : const Measure* currMeasure = mr->firstMeasureOfGroup();
1053 47 : for (int num = 0; currMeasure && num < mr->numMeasures(); ++num, currMeasure = currMeasure->nextMeasure()) {
1054 24 : const Measure* referringMeasure = mr->referringMeasure(currMeasure);
1055 24 : IF_ASSERT_FAILED(referringMeasure && referringMeasure != currMeasure) {
1056 0 : continue;
1057 : }
1058 :
1059 24 : const int referringMeasureTick = referringMeasure->tick().ticks();
1060 24 : ranges.push_back({ currMeasure->tick().ticks() - referringMeasureTick,
1061 24 : referringMeasureTick + tickOffset,
1062 24 : referringMeasure->endTick().ticks() + tickOffset });
1063 : }
1064 :
1065 23 : if (ranges.empty()) {
1066 0 : continue;
1067 : }
1068 :
1069 68 : for (AutomationCurve* curve : staffCurvesIt->second) {
1070 45 : const MeasureRange& firstRange = ranges.front();
1071 45 : auto srcIt = curve->lower_bound(firstRange.srcFrom);
1072 45 : const auto curveEnd = curve->end();
1073 45 : if (srcIt == curveEnd) {
1074 44 : continue;
1075 : }
1076 :
1077 1 : const auto destHint = curve->lower_bound(firstRange.srcFrom + firstRange.tickShift);
1078 :
1079 3 : for (const MeasureRange& range : ranges) {
1080 : // Ranges are contiguous for consecutive measures, but a skipped measure (assert
1081 : // failure above) can leave a gap; catch up if srcIt fell behind this range's start
1082 2 : if (srcIt != curveEnd && srcIt->first < range.srcFrom) {
1083 0 : srcIt = curve->lower_bound(range.srcFrom);
1084 : }
1085 :
1086 6 : for (; srcIt != curveEnd && srcIt->first < range.srcTo; ++srcIt) {
1087 4 : if (srcIt->second.generated) {
1088 4 : curve->insert(destHint, { srcIt->first + range.tickShift, srcIt->second });
1089 : } else {
1090 0 : AutomationPoint mirroredPoint = srcIt->second;
1091 0 : mirroredPoint.generated = true;
1092 0 : curve->insert(destHint, { srcIt->first + range.tickShift, mirroredPoint });
1093 : }
1094 : }
1095 : }
1096 : }
1097 23 : }
1098 160 : }
1099 :
1100 912 : void ScoreAutomationController::collectPauses(const Measure* measure, int tickOffset, PausesMap& pauses,
1101 : PausesMap& noRepeatPauses)
1102 : {
1103 912 : const Fraction& startTick = measure->tick();
1104 912 : const int startUtick = startTick.ticks() + tickOffset;
1105 :
1106 : // Implement section break rest
1107 1721 : for (MeasureBase* mb = measure->prev(); mb && mb->endTick() == startTick; mb = mb->prev()) {
1108 809 : if (mb->pause()) {
1109 2 : pauses[startUtick] = mb->pause();
1110 : // Identical value across every pass that plays this raw tick, so last-write-wins is fine
1111 2 : noRepeatPauses[startTick.ticks()] = mb->pause();
1112 : }
1113 : }
1114 :
1115 : // Add pauses from the end of the previous measure (at measure->tick()):
1116 2590 : for (Segment* s = measure->first(); s && s->tick() == startTick; s = s->prev1()) {
1117 1678 : if (!s->isBreathType()) {
1118 1678 : continue;
1119 : }
1120 0 : double length = 0.0;
1121 0 : for (EngravingItem* e : s->elist()) {
1122 0 : if (e && e->isBreath()) {
1123 0 : length = std::max(length, toBreath(e)->pause());
1124 : }
1125 : }
1126 0 : if (!muse::RealIsNull(length)) {
1127 0 : pauses[startUtick] = length;
1128 0 : noRepeatPauses[startTick.ticks()] = length;
1129 : }
1130 : }
1131 :
1132 6357 : for (const Segment& segment : measure->segments()) {
1133 5445 : if (!segment.isBreathType()) {
1134 5442 : continue;
1135 : }
1136 :
1137 3 : double length = 0.0;
1138 3 : Fraction tick = segment.tick();
1139 : // find longest pause
1140 15 : for (track_idx_t i = 0, n = measure->score()->ntracks(); i < n; ++i) {
1141 12 : EngravingItem* e = segment.element(i);
1142 12 : if (e && e->isBreath()) {
1143 3 : length = std::max(length, toBreath(e)->pause());
1144 : }
1145 : }
1146 3 : if (!muse::RealIsNull(length)) {
1147 3 : pauses[tick.ticks() + tickOffset] = length;
1148 3 : noRepeatPauses[tick.ticks()] = length;
1149 : }
1150 : }
1151 912 : }
1152 :
1153 77 : void ScoreAutomationController::addTempoTextPoint(const TempoText* tt, int tickOffset, UpdateContext& ctx)
1154 : {
1155 77 : if (!tt->playTempoText()) {
1156 0 : return;
1157 : }
1158 :
1159 77 : if (tt->isNormal() && !tt->isRelative() && !ctx.tempoPrimo) {
1160 33 : ctx.tempoPrimo = roundTempo(tt->tempo());
1161 : }
1162 :
1163 77 : const utick_t tick = tt->segment()->tick().ticks() + tickOffset;
1164 :
1165 77 : EID itemId = tt->eid();
1166 77 : if (!itemId.isValid()) {
1167 27 : itemId = tt->assignNewEID();
1168 : }
1169 :
1170 77 : if (tt->isATempo() && tt->followText()) {
1171 : // This will effectively reset the tempo to the previous one when a progressive change was active
1172 12 : setTempoPoint(tick, tickOffset, currentTempoAt(*ctx.tempoCurve, tick), ctx, itemId);
1173 65 : } else if (tt->isTempoPrimo() && tt->followText()) {
1174 12 : setTempoPoint(tick, tickOffset, normalizeTempo(ctx.tempoPrimo.value_or(Constants::DEFAULT_TEMPO)), ctx, itemId);
1175 53 : } else if (tt->isRelative()) {
1176 0 : const real_t before = currentTempoAt(*ctx.tempoCurve, tick);
1177 0 : setTempoPoint(tick, tickOffset, before * tt->relativeValue(), ctx, itemId);
1178 : } else {
1179 53 : setTempoPoint(tick, tickOffset, normalizeTempo(roundTempo(tt->tempo())), ctx, itemId);
1180 : }
1181 : }
1182 :
1183 12 : void ScoreAutomationController::addFermataStretchPoints(const Fermata* fermata, int tickOffset, double stretch, UpdateContext& ctx)
1184 : {
1185 12 : const Segment* segment = fermata->segment();
1186 12 : const utick_t tick = segment->tick().ticks() + tickOffset;
1187 :
1188 12 : const auto beforeIt = muse::findLessOrEqual(*ctx.tempoCurve, tick);
1189 12 : const bool hasBefore = beforeIt != ctx.tempoCurve->cend();
1190 12 : const real_t before = hasBefore ? beforeIt->second.value.outValue : DEFAULT_NORMALIZED_TEMPO;
1191 :
1192 12 : EID itemId = fermata->eid();
1193 12 : if (!itemId.isValid()) {
1194 0 : itemId = fermata->assignNewEID();
1195 : }
1196 :
1197 12 : setTempoPoint(tick, tickOffset, before / stretch, ctx, itemId);
1198 :
1199 12 : const Segment* nextActiveSegment = segment->next1();
1200 12 : while (nextActiveSegment && !nextActiveSegment->isActive()) {
1201 0 : nextActiveSegment = nextActiveSegment->next1();
1202 : }
1203 :
1204 12 : const Fraction tempoEndTick = nextActiveSegment ? nextActiveSegment->tick() : segment->tick() + segment->ticks();
1205 12 : const utick_t etick = tempoEndTick.ticks() + tickOffset;
1206 :
1207 : // Restore to whatever was active before the fermata, so the point belongs to that marking, not the fermata
1208 12 : const auto etickIt = ctx.tempoCurve->lower_bound(etick);
1209 12 : if (etickIt == ctx.tempoCurve->end() || etickIt->first != etick) {
1210 12 : setTempoPoint(etickIt, etick, tickOffset, before, ctx, hasBefore ? beforeIt->second.itemId : std::nullopt);
1211 : }
1212 12 : }
1213 :
1214 15 : void ScoreAutomationController::addGradualTempoChangePoints(const GradualTempoChange* tempoChange, int tickOffset, UpdateContext& ctx)
1215 : {
1216 15 : const utick_t tickFrom = tempoChange->tick().ticks() + tickOffset;
1217 15 : const utick_t tickTo = tickFrom + tempoChange->ticks().ticks();
1218 15 : const real_t normalizedCurrent = currentTempoAt(*ctx.tempoCurve, tickFrom);
1219 :
1220 15 : EID itemId = tempoChange->eid();
1221 15 : if (!itemId.isValid()) {
1222 3 : itemId = tempoChange->assignNewEID();
1223 : }
1224 :
1225 15 : const auto tickFromIt = ctx.tempoCurve->lower_bound(tickFrom);
1226 15 : if (tickFromIt == ctx.tempoCurve->end() || tickFromIt->first != tickFrom) {
1227 : // Explicit flat arrival, so the ramp is anchored here rather than at whatever
1228 : // unrelated marking last happened to be an arrival
1229 15 : AutomationPoint startPoint;
1230 15 : startPoint.value.outValue = normalizedCurrent;
1231 15 : startPoint.value.inValue = AutomationPoint::ExplicitArrival { normalizedCurrent, AutomationPoint::Ease::none() };
1232 15 : startPoint.itemId = itemId;
1233 15 : startPoint.generated = true;
1234 15 : ctx.tempoCurve->emplace_hint(tickFromIt, tickFrom, startPoint);
1235 15 : setNoRepeatTempoPoint(tickFrom - tickOffset, startPoint, ctx);
1236 : }
1237 :
1238 15 : const real_t normalizedTarget = std::clamp(normalizedCurrent * tempoChange->tempoChangeFactor(),
1239 15 : MIN_NORMALIZED_TEMPO, MAX_NORMALIZED_TEMPO);
1240 15 : const AutomationPoint::ExplicitArrival arrival { normalizedTarget, changeMethodToEase(tempoChange->easingMethod()) };
1241 :
1242 : // Don't overwrite a tempo already set at tickTo (e.g. by a tempo marking) - just record the
1243 : // arrival so the ramp leading up to it still interpolates correctly
1244 : // See https://github.com/musescore/MuseScore/issues/12140
1245 15 : const auto tickToIt = ctx.tempoCurve->lower_bound(tickTo);
1246 15 : if (tickToIt != ctx.tempoCurve->end() && tickToIt->first == tickTo) {
1247 2 : tickToIt->second.value.inValue = arrival;
1248 :
1249 2 : const auto rawIt = ctx.noRepeatTempoCurve.find(tickTo - tickOffset);
1250 2 : if (rawIt != ctx.noRepeatTempoCurve.end()) {
1251 2 : rawIt->second.value.inValue = arrival;
1252 : }
1253 :
1254 2 : return;
1255 : }
1256 :
1257 13 : AutomationPoint endPoint;
1258 13 : endPoint.value.outValue = normalizedTarget;
1259 13 : endPoint.value.inValue = arrival;
1260 13 : endPoint.itemId = itemId;
1261 13 : endPoint.generated = true;
1262 13 : ctx.tempoCurve->emplace_hint(tickToIt, tickTo, endPoint);
1263 13 : setNoRepeatTempoPoint(tickTo - tickOffset, endPoint, ctx);
1264 : }
1265 :
1266 6 : void ScoreAutomationController::addVoltaTempoResetPoint(const Volta* volta, UpdateContext& ctx)
1267 : {
1268 6 : const Measure* startMeasure = volta->startMeasure();
1269 6 : const Measure* endMeasure = volta->endMeasure();
1270 6 : if (!startMeasure || !endMeasure || !endMeasure->repeatEnd()) {
1271 1 : return;
1272 : }
1273 :
1274 : // Changes noRepeatTempoCurve only, so later content (e.g. a seconda volta) sees the pre-volta tempo, not
1275 : // the volta's own changes; findLessOrEqual so an earlier volta's own reset at this tick counts
1276 5 : const auto beforeIt = muse::findLessOrEqual(ctx.noRepeatTempoCurve, startMeasure->tick().ticks());
1277 5 : const bool hasBefore = beforeIt != ctx.noRepeatTempoCurve.end();
1278 5 : const real_t tempoBeforeVolta = hasBefore ? beforeIt->second.value.outValue : DEFAULT_NORMALIZED_TEMPO;
1279 :
1280 5 : AutomationPoint point;
1281 5 : point.value.outValue = tempoBeforeVolta;
1282 : // Recovers to whatever was active before the volta, so the point belongs to that marking, not the volta
1283 5 : point.itemId = hasBefore ? beforeIt->second.itemId : std::nullopt;
1284 5 : point.generated = true;
1285 :
1286 : // Don't overwrite an explicit tempo marking already sitting at this exact tick
1287 : // (e.g. the seconda volta's own TempoText, written in Step 1 before this Step 2 pass runs)
1288 5 : ctx.noRepeatTempoCurve.try_emplace(endMeasure->endTick().ticks(), point);
1289 : }
1290 :
1291 154 : void ScoreAutomationController::resolvePendingTempoResets(UpdateContext& ctx)
1292 : {
1293 258 : for (const auto& [utick, rawTick] : ctx.pendingTempoResets) {
1294 104 : const auto utickIt = ctx.tempoCurve->lower_bound(utick);
1295 104 : if (utickIt != ctx.tempoCurve->end() && utickIt->first == utick) {
1296 102 : continue; // this pass's own marking already sits exactly here - don't overwrite it
1297 : }
1298 :
1299 97 : const auto rawIt = muse::findLessOrEqual(ctx.noRepeatTempoCurve, rawTick);
1300 97 : const real_t tempo = rawIt != ctx.noRepeatTempoCurve.end() ? rawIt->second.value.outValue
1301 97 : : DEFAULT_NORMALIZED_TEMPO;
1302 :
1303 97 : const real_t currentTempo = currentTempoAt(*ctx.tempoCurve, utick);
1304 97 : if (RealIsEqual(tempo, currentTempo)) {
1305 95 : continue; // no actual change - don't emit a redundant point
1306 : }
1307 :
1308 2 : setTempoPoint(utickIt, utick, utick - rawTick, tempo, ctx);
1309 : }
1310 154 : }
1311 :
1312 1 : void ScoreAutomationController::fixAnacrusisTempo(UpdateContext& ctx)
1313 : {
1314 : // An anacrusis measure with no tempo marking of its own should start at whatever tempo
1315 : // the next (first full) measure marks, rather than whatever tempo preceded it
1316 2 : for (const AnacrusisMeasureInfo& info : ctx.anacrusisMeasures) {
1317 1 : const utick_t measureTick = info.measure->tick().ticks() + info.tickOffset;
1318 1 : const auto measureIt = ctx.tempoCurve->lower_bound(measureTick);
1319 1 : if (measureIt != ctx.tempoCurve->end() && measureIt->first == measureTick) {
1320 0 : continue;
1321 : }
1322 :
1323 1 : if (!info.nextMeasureUTick) {
1324 0 : continue; // anacrusis is the last measure of its repeat pass - no "next" within this pass
1325 : }
1326 :
1327 1 : const auto nextIt = ctx.tempoCurve->find(*info.nextMeasureUTick);
1328 1 : if (nextIt != ctx.tempoCurve->end()) {
1329 0 : setTempoPoint(measureIt, measureTick, info.tickOffset, nextIt->second.value.outValue, ctx);
1330 : }
1331 : }
1332 1 : }
1333 :
1334 315 : bool ScoreAutomationController::tryAddDynamicPoint(AutomationCurve& curve, std::map<utick_t, int>& tickPrioMap, utick_t tick,
1335 : const AutomationPoint& point, int priority)
1336 : {
1337 : //! See: https://github.com/musescore/MuseScore/issues/23355
1338 315 : const auto prioIt = tickPrioMap.lower_bound(tick);
1339 315 : const bool hasPrio = prioIt != tickPrioMap.end() && prioIt->first == tick;
1340 :
1341 315 : if (hasPrio) {
1342 35 : if (priority <= prioIt->second) {
1343 35 : return false;
1344 : }
1345 :
1346 0 : curve[tick] = point;
1347 0 : prioIt->second = priority;
1348 0 : return true;
1349 : }
1350 :
1351 280 : const auto curveIt = curve.lower_bound(tick);
1352 280 : if (curveIt != curve.end() && curveIt->first == tick) {
1353 0 : return false; // point from a prior update pass — don't overwrite
1354 : }
1355 :
1356 280 : curve.emplace_hint(curveIt, tick, point);
1357 280 : tickPrioMap.emplace_hint(prioIt, tick, priority);
1358 :
1359 280 : return true;
1360 : }
1361 :
1362 228 : void ScoreAutomationController::addDynamicPoint(AutomationCurve& curve, std::map<utick_t, int>& tickPrioMap, utick_t tick,
1363 : const AutomationPoint& point, int priority)
1364 : {
1365 : // If a point with the same priority already exists at this tick, merge them:
1366 : // keep the existing arrival value and use this point's departure value
1367 228 : const auto prioIt = tickPrioMap.find(tick);
1368 228 : if (prioIt != tickPrioMap.end() && prioIt->second == priority) {
1369 1 : AutomationPoint& existing = curve[tick];
1370 1 : const AutomationPoint::InValue arrivalValue = existing.value.inValue;
1371 1 : existing = point;
1372 1 : existing.value.inValue = arrivalValue;
1373 1 : return;
1374 : }
1375 :
1376 227 : tryAddDynamicPoint(curve, tickPrioMap, tick, point, priority);
1377 : }
1378 :
1379 131 : void ScoreAutomationController::setNoRepeatTempoPoint(utick_t rawTick, const AutomationPoint& point, UpdateContext& ctx)
1380 : {
1381 131 : const auto it = ctx.noRepeatTempoCurve.lower_bound(rawTick);
1382 131 : const bool exists = it != ctx.noRepeatTempoCurve.end() && it->first == rawTick;
1383 131 : if (exists && !it->second.generated) {
1384 0 : return; // an authored point at this raw tick wins over the generated one
1385 : }
1386 :
1387 131 : if (exists) {
1388 46 : it->second = point;
1389 : } else {
1390 85 : ctx.noRepeatTempoCurve.emplace_hint(it, rawTick, point);
1391 : }
1392 : }
1393 :
1394 89 : void ScoreAutomationController::setTempoPoint(utick_t tick, int tickOffset, real_t normalizedBps, UpdateContext& ctx,
1395 : std::optional<EID> itemId)
1396 : {
1397 89 : const auto it = ctx.tempoCurve->lower_bound(tick);
1398 89 : const bool exists = it != ctx.tempoCurve->end() && it->first == tick;
1399 89 : if (exists && !it->second.generated) {
1400 83 : return; // an authored point at this tick wins over the generated one
1401 : }
1402 :
1403 89 : if (!exists) {
1404 83 : setTempoPoint(it, tick, tickOffset, normalizedBps, ctx, itemId);
1405 83 : return;
1406 : }
1407 :
1408 6 : const AutomationPoint point = makeTempoPoint(normalizedBps, itemId);
1409 6 : it->second = point;
1410 6 : setNoRepeatTempoPoint(tick - tickOffset, point, ctx);
1411 : }
1412 :
1413 97 : void ScoreAutomationController::setTempoPoint(AutomationCurve::iterator hint, utick_t tick, int tickOffset, real_t normalizedBps,
1414 : UpdateContext& ctx, std::optional<EID> itemId)
1415 : {
1416 97 : const AutomationPoint point = makeTempoPoint(normalizedBps, itemId);
1417 97 : ctx.tempoCurve->emplace_hint(hint, tick, point);
1418 97 : setNoRepeatTempoPoint(tick - tickOffset, point, ctx);
1419 97 : }
1420 :
1421 263 : std::vector<AutomationCurveKey> ScoreAutomationController::resolveKeys(const EngravingItem* item, AutomationType type,
1422 : const StaffRange& range)
1423 : {
1424 263 : const Score* score = item->score();
1425 263 : IF_ASSERT_FAILED(score) {
1426 0 : return {};
1427 : }
1428 :
1429 263 : std::vector<AutomationCurveKey> result;
1430 :
1431 276 : auto tryAddStaffKey = [type, score, &range, &result](staff_idx_t staffIdx, std::optional<size_t> voiceIdx = std::nullopt) {
1432 276 : if (!range.contains(staffIdx)) {
1433 0 : return;
1434 : }
1435 :
1436 276 : const Staff* staff = score->staff(staffIdx);
1437 276 : IF_ASSERT_FAILED(staff) {
1438 0 : return;
1439 : }
1440 :
1441 276 : if (!staff->isPrimaryStaff()) {
1442 10 : return; // ignore linked staves
1443 : }
1444 :
1445 266 : result.push_back(AutomationCurveKey::staff(type, staff->id(), voiceIdx));
1446 263 : };
1447 :
1448 263 : const VoiceAssignment voiceAssignment = item->getProperty(Pid::VOICE_ASSIGNMENT).value<VoiceAssignment>();
1449 263 : switch (voiceAssignment) {
1450 238 : case VoiceAssignment::ALL_VOICE_IN_INSTRUMENT: {
1451 238 : const Part* part = item->part();
1452 238 : IF_ASSERT_FAILED(part) {
1453 0 : return result;
1454 : }
1455 :
1456 238 : const TrackRange trackRange = part->trackRange();
1457 238 : const staff_idx_t startStaffIdx = track2staff(trackRange.startTrack);
1458 238 : const staff_idx_t endStaffIdx = track2staff(trackRange.endTrack);
1459 238 : result.reserve(endStaffIdx - startStaffIdx);
1460 :
1461 489 : for (staff_idx_t staffIdx = startStaffIdx; staffIdx < endStaffIdx; ++staffIdx) {
1462 251 : tryAddStaffKey(staffIdx);
1463 : }
1464 238 : } break;
1465 24 : case VoiceAssignment::CURRENT_VOICE_ONLY:
1466 24 : tryAddStaffKey(item->staffIdx(), item->voice());
1467 24 : break;
1468 1 : case VoiceAssignment::ALL_VOICE_IN_STAFF:
1469 1 : tryAddStaffKey(item->staffIdx());
1470 1 : break;
1471 : }
1472 :
1473 263 : return result;
1474 263 : }
1475 :
1476 27 : void ScoreAutomationController::mirrorEditsToRepeats(const AutomationCurveKey& key, AutomationPointEdits& edits)
1477 : {
1478 28 : TRACEFUNC;
1479 :
1480 27 : const RepeatList& repeatList = m_score->expandedRepeatList();
1481 27 : if (repeatList.size() <= 1) {
1482 0 : return;
1483 : }
1484 :
1485 : // Measure repeats are staff-specific; global/instrument-scoped points (e.g. Tempo, Volume, Pan)
1486 : // aren't tied to any staff, so they're mirrored to regular repeats only, not measure repeats
1487 27 : std::optional<StaffRange> staffRange;
1488 27 : if (const std::optional<muse::ID> staffId = key.staffId()) {
1489 26 : const Staff* staff = m_score->staffById(*staffId);
1490 26 : IF_ASSERT_FAILED(staff) {
1491 0 : return;
1492 : }
1493 :
1494 26 : staffRange.emplace(m_score, staff->idx(), staff->idx());
1495 : }
1496 :
1497 : // Bounded by the original size, since mirrored edits are appended to the same vector below
1498 27 : const size_t originalEditCount = edits.size();
1499 75 : for (size_t i = 0; i < originalEditCount; ++i) {
1500 : // Copied by value: push_back below may reallocate edits
1501 48 : AutomationPointEdit localEdit = edits[i];
1502 48 : const auto sourceIt = repeatList.findRepeatSegmentFromUTick(localEdit.tick);
1503 48 : IF_ASSERT_FAILED(sourceIt != repeatList.cend()) {
1504 0 : continue;
1505 : }
1506 :
1507 48 : const RepeatSegment* sourceSeg = *sourceIt;
1508 48 : const int sourceTickOffset = sourceSeg->utick - sourceSeg->tick;
1509 :
1510 : // Re-express the edit in tick coordinates local to sourceSeg
1511 48 : localEdit.tick -= sourceTickOffset;
1512 48 : if (auto* movePoint = std::get_if<AutomationPointEdit::MovePoint>(&localEdit.change)) {
1513 0 : movePoint->from -= sourceTickOffset;
1514 : }
1515 :
1516 144 : for (const RepeatSegment* targetSeg : repeatList) {
1517 96 : if (targetSeg != sourceSeg) {
1518 48 : const MirrorRange targetRange { targetSeg->tick, targetSeg->endTick(), targetSeg->utick - targetSeg->tick };
1519 48 : mirrorPointIfInRange(localEdit, targetRange, edits);
1520 : }
1521 96 : if (staffRange) {
1522 94 : mirrorToMeasureRepeats(targetSeg, *staffRange, localEdit, edits);
1523 : }
1524 : }
1525 : }
1526 27 : }
1527 :
1528 95 : void ScoreAutomationController::mirrorPointIfInRange(const AutomationPointEdit& localEdit, const MirrorRange& range,
1529 : AutomationPointEdits& allEdits)
1530 : {
1531 95 : const int localTick = static_cast<int>(localEdit.tick);
1532 95 : if (localTick < range.from || localTick >= range.toExclusive) {
1533 92 : return;
1534 : }
1535 :
1536 3 : const utick_t mirroredTick = static_cast<utick_t>(localTick + range.tickOffset);
1537 :
1538 3 : if (std::holds_alternative<AutomationPointEdit::ErasePoint>(localEdit.change)) {
1539 0 : allEdits.push_back({ mirroredTick, AutomationPointEdit::ErasePoint {} });
1540 0 : return;
1541 : }
1542 :
1543 3 : const auto* movePoint = std::get_if<AutomationPointEdit::MovePoint>(&localEdit.change);
1544 :
1545 : // Mirrored points are derived from the user's edit, not edited directly, so they are marked generated
1546 3 : AutomationPoint mirroredPoint = movePoint ? movePoint->point : std::get<AutomationPointEdit::SetPoint>(localEdit.change).point;
1547 3 : mirroredPoint.generated = true;
1548 :
1549 3 : if (movePoint && movePoint->from >= range.from && movePoint->from < range.toExclusive) {
1550 0 : const utick_t mirroredMoveFrom = static_cast<utick_t>(movePoint->from + range.tickOffset);
1551 0 : allEdits.push_back({ mirroredTick, AutomationPointEdit::MovePoint { mirroredPoint, mirroredMoveFrom } });
1552 0 : } else {
1553 3 : allEdits.push_back({ mirroredTick, AutomationPointEdit::SetPoint { mirroredPoint } });
1554 : }
1555 : }
1556 :
1557 94 : void ScoreAutomationController::mirrorToMeasureRepeats(const RepeatSegment* targetSeg, const StaffRange& range,
1558 : const AutomationPointEdit& localEdit, AutomationPointEdits& allEdits)
1559 : {
1560 94 : const int tickOffset = targetSeg->utick - targetSeg->tick;
1561 :
1562 : // A MeasureRepeat can only ever sit on the measure's first ChordRest segment
1563 94 : MeasureRepeats measureRepeats;
1564 470 : for (const Measure* measure : targetSeg->measureList()) {
1565 376 : if (const Segment* firstChordRestSegment = measure->first(SegmentType::ChordRest)) {
1566 376 : collectMeasureRepeats(firstChordRestSegment, tickOffset, range, measureRepeats);
1567 : }
1568 : }
1569 :
1570 141 : for (const auto& [mr, mrTickOffset] : measureRepeats) {
1571 47 : const Measure* currMeasure = mr->firstMeasureOfGroup();
1572 94 : for (int num = 0; currMeasure && num < mr->numMeasures(); ++num, currMeasure = currMeasure->nextMeasure()) {
1573 47 : const Measure* referringMeasure = mr->referringMeasure(currMeasure);
1574 47 : IF_ASSERT_FAILED(referringMeasure && referringMeasure != currMeasure) {
1575 0 : continue;
1576 : }
1577 :
1578 47 : const int measureFrom = referringMeasure->tick().ticks();
1579 47 : const int measureToExclusive = referringMeasure->endTick().ticks();
1580 47 : const int tickShift = currMeasure->tick().ticks() - measureFrom;
1581 :
1582 47 : const MirrorRange measureRange { measureFrom, measureToExclusive, tickShift + mrTickOffset };
1583 47 : mirrorPointIfInRange(localEdit, measureRange, allEdits);
1584 : }
1585 : }
1586 94 : }
|