LCOV - code coverage report
Current view: top level - src/engraving/automation - automationdata.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 76.1 % 205 156
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-Studio-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2026 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : 
      23              : #include "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              : }
        

Generated by: LCOV version 2.0-1