LCOV - code coverage report
Current view: top level - src/engraving/automation/internal - automationrw.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 83.5 % 121 101
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 "automationrw.h"
      24              : 
      25              : #include "engraving/automation/automationdata.h"
      26              : 
      27              : #include "global/serialization/json.h"
      28              : #include "global/containers.h"
      29              : #include "global/log.h"
      30              : 
      31              : using namespace mu::engraving;
      32              : 
      33              : static const std::unordered_map<AutomationType, muse::String> AUTOMATION_TYPE_TO_STRING {
      34              :     { AutomationType::Dynamics, u"Dynamics" },
      35              :     { AutomationType::Tempo, u"Tempo" },
      36              :     { AutomationType::Volume, u"Volume" },
      37              :     { AutomationType::Pan, u"Pan" },
      38              : };
      39              : 
      40              : static constexpr const char* TYPE_KEY = "type";
      41              : static constexpr const char* PART_ID_KEY = "partId";
      42              : static constexpr const char* INSTRUMENT_ID_KEY = "instrumentId";
      43              : static constexpr const char* STAFF_ID_KEY = "staffId";
      44              : static constexpr const char* VOICE_ID_KEY = "voiceId";
      45              : static constexpr const char* OUT_VALUE_KEY = "outValue";
      46              : static constexpr const char* IN_VALUE_KEY = "inValue";
      47              : static constexpr const char* IN_VALUE_KIND_KEY = "inValueKind";
      48              : static constexpr const char* EASE_KEY = "ease";
      49              : static constexpr const char* CONTROL_POINT_T_KEY = "t";
      50              : static constexpr const char* CONTROL_POINT_VALUE_KEY = "value";
      51              : static constexpr const char* ITEM_ID_KEY = "itemId";
      52              : static constexpr const char* GENERATED_KEY = "generated";
      53              : static constexpr const char* TICK_KEY = "tick";
      54              : static constexpr const char* POINTS_KEY = "points";
      55              : 
      56              : static const std::string IN_VALUE_KIND_FROM_PREVIOUS = "FromPrevious";
      57              : 
      58            4 : static AutomationCurveKey readKey(const muse::JsonObject& obj)
      59              : {
      60           12 :     const AutomationType type = muse::key(AUTOMATION_TYPE_TO_STRING, obj.value(TYPE_KEY).toString(), AutomationType::Unknown);
      61              : 
      62            8 :     if (obj.contains(PART_ID_KEY)) {
      63            1 :         InstrumentTrackId trackId;
      64            3 :         trackId.partId = muse::ID(obj.value(PART_ID_KEY).toString().toStdString());
      65            3 :         trackId.instrumentId = obj.value(INSTRUMENT_ID_KEY).toString();
      66            1 :         return AutomationCurveKey::instrument(type, trackId);
      67            1 :     }
      68              : 
      69            6 :     if (obj.contains(STAFF_ID_KEY)) {
      70            6 :         const muse::ID staffId(obj.value(STAFF_ID_KEY).toString().toStdString());
      71              : 
      72            2 :         std::optional<size_t> voiceIdx;
      73            4 :         if (obj.contains(VOICE_ID_KEY)) {
      74            3 :             voiceIdx = static_cast<size_t>(obj.value(VOICE_ID_KEY).toInt());
      75              :         }
      76              : 
      77            2 :         return AutomationCurveKey::staff(type, staffId, voiceIdx);
      78              :     }
      79              : 
      80            1 :     return AutomationCurveKey::global(type);
      81              : }
      82              : 
      83            6 : static AutomationPoint readPoint(const muse::JsonObject& obj)
      84              : {
      85            6 :     AutomationPoint point;
      86           18 :     point.value.outValue = obj.value(OUT_VALUE_KEY).toDouble();
      87              : 
      88            8 :     const std::string inValueKind = obj.contains(IN_VALUE_KIND_KEY) ? obj.value(IN_VALUE_KIND_KEY).toStdString() : std::string();
      89            6 :     if (inValueKind == IN_VALUE_KIND_FROM_PREVIOUS) {
      90            1 :         point.value.inValue = AutomationPoint::ArrivalFromPrevious {};
      91              :     } else {
      92            5 :         AutomationPoint::Ease ease;
      93           10 :         if (obj.contains(EASE_KEY)) {
      94              :             // Array for forward-compat; only element 0 is used, since Ease only holds one point
      95            0 :             const muse::JsonArray easeArray = obj.value(EASE_KEY).toArray();
      96            0 :             if (!easeArray.empty()) {
      97            0 :                 const muse::JsonObject controlPointObj = easeArray.at(0).toObject();
      98            0 :                 ease.t = controlPointObj.value(CONTROL_POINT_T_KEY).toDouble();
      99            0 :                 ease.value = controlPointObj.value(CONTROL_POINT_VALUE_KEY).toDouble();
     100            0 :             }
     101            0 :         }
     102              : 
     103           15 :         const muse::real_t value = muse::real_t(obj.value(IN_VALUE_KEY).toDouble());
     104            5 :         point.value.inValue = AutomationPoint::ExplicitArrival { value, ease };
     105              :     }
     106              : 
     107           12 :     if (obj.contains(ITEM_ID_KEY)) {
     108            3 :         point.itemId = EID::fromStdString(obj.value(ITEM_ID_KEY).toString().toStdString());
     109              :     }
     110           12 :     if (obj.contains(GENERATED_KEY)) {
     111            9 :         point.generated = obj.value(GENERATED_KEY).toBool();
     112              :     }
     113              : 
     114           12 :     return point;
     115            6 : }
     116              : 
     117            4 : static void writeKey(const AutomationCurveKey& key, muse::JsonObject& obj)
     118              : {
     119           12 :     obj[TYPE_KEY] = muse::value(AUTOMATION_TYPE_TO_STRING, key.type);
     120              : 
     121            4 :     if (const std::optional<InstrumentTrackId> trackId = key.trackId()) {
     122            3 :         obj[PART_ID_KEY] = trackId->partId.toStdString();
     123            2 :         obj[INSTRUMENT_ID_KEY] = trackId->instrumentId;
     124            1 :         return;
     125            4 :     }
     126              : 
     127            3 :     if (const std::optional<muse::ID> staffId = key.staffId()) {
     128            6 :         obj[STAFF_ID_KEY] = staffId->toStdString();
     129              : 
     130            2 :         if (const std::optional<size_t> voiceIdx = key.voiceIdx()) {
     131            3 :             obj[VOICE_ID_KEY] = static_cast<int>(*voiceIdx);
     132              :         }
     133              :     }
     134              : }
     135              : 
     136            6 : static void writePoint(const AutomationPoint& point, muse::JsonObject& obj)
     137              : {
     138            6 :     if (std::holds_alternative<AutomationPoint::ArrivalFromPrevious>(point.value.inValue)) {
     139            2 :         obj[IN_VALUE_KIND_KEY] = IN_VALUE_KIND_FROM_PREVIOUS;
     140              :     } else {
     141            5 :         const AutomationPoint::ExplicitArrival& explicitArrival = std::get<AutomationPoint::ExplicitArrival>(point.value.inValue);
     142           10 :         obj[IN_VALUE_KEY] = explicitArrival.value;
     143            5 :         if (!explicitArrival.ease.isNone()) {
     144            0 :             muse::JsonObject controlPointObj;
     145            0 :             controlPointObj[CONTROL_POINT_T_KEY] = explicitArrival.ease.t.raw();
     146            0 :             controlPointObj[CONTROL_POINT_VALUE_KEY] = explicitArrival.ease.value.raw();
     147              : 
     148            0 :             muse::JsonArray easeArray;
     149            0 :             easeArray << controlPointObj;
     150            0 :             obj[EASE_KEY] = easeArray;
     151            0 :         }
     152              :     }
     153              : 
     154           12 :     obj[OUT_VALUE_KEY] = point.value.outValue;
     155              : 
     156            6 :     if (point.itemId.has_value()) {
     157            3 :         obj[ITEM_ID_KEY] = point.itemId->toStdString();
     158              :     }
     159            6 :     if (point.generated) {
     160            9 :         obj[GENERATED_KEY] = point.generated;
     161              :     }
     162            6 : }
     163              : 
     164         1011 : void AutomationRW::read(AutomationData& data, const muse::ByteArray& json)
     165              : {
     166         1019 :     TRACEFUNC;
     167              : 
     168         1011 :     if (json.empty()) {
     169         1003 :         return;
     170              :     }
     171              : 
     172            8 :     std::string err;
     173            8 :     const muse::JsonDocument doc = muse::JsonDocument::fromJson(json, &err);
     174            8 :     if (!err.empty() || !doc.isArray()) {
     175            0 :         LOGE() << "Failed to parse automation json: " << err;
     176            0 :         return;
     177              :     }
     178              : 
     179            8 :     AutomationCurveMap curves;
     180              : 
     181            8 :     const muse::JsonArray rootArray = doc.rootArray();
     182           12 :     for (size_t i = 0; i < rootArray.size(); ++i) {
     183            4 :         const muse::JsonObject curveObj = rootArray.at(i).toObject();
     184            4 :         const AutomationCurveKey key = readKey(curveObj);
     185            4 :         IF_ASSERT_FAILED(key.isValid()) {
     186            0 :             continue;
     187              :         }
     188              : 
     189           12 :         const muse::JsonArray pointArray = curveObj.value(POINTS_KEY).toArray();
     190            4 :         if (pointArray.empty()) {
     191            0 :             continue;
     192              :         }
     193              : 
     194            4 :         AutomationCurve& curve = curves[key];
     195           10 :         for (size_t j = 0; j < pointArray.size(); ++j) {
     196            6 :             const muse::JsonObject pointObj = pointArray.at(j).toObject();
     197            6 :             const AutomationPoint point = readPoint(pointObj);
     198           18 :             const utick_t tick = pointObj.value(TICK_KEY).toInt();
     199              :             // Points are written in ascending tick order, so end() is always the right hint
     200            6 :             curve.try_emplace(curve.end(), tick, point);
     201            6 :         }
     202            4 :     }
     203              : 
     204            8 :     data.setCurves(curves);
     205         1011 : }
     206              : 
     207            3 : muse::ByteArray AutomationRW::write(const AutomationData& data, bool writeGenerated)
     208              : {
     209            4 :     TRACEFUNC;
     210              : 
     211            3 :     muse::JsonArray rootArray;
     212            7 :     for (const auto& [key, curve] : data.curves()) {
     213            4 :         IF_ASSERT_FAILED(key.isValid()) {
     214            0 :             continue;
     215              :         }
     216              : 
     217            4 :         muse::JsonArray pointArray;
     218           10 :         for (const auto& [tick, point] : curve) {
     219            6 :             if (!writeGenerated && point.generated) {
     220            0 :                 continue;
     221              :             }
     222              : 
     223            6 :             muse::JsonObject pointObj;
     224           12 :             pointObj[TICK_KEY] = tick;
     225            6 :             writePoint(point, pointObj);
     226            6 :             pointArray << pointObj;
     227            6 :         }
     228              : 
     229            4 :         if (!pointArray.empty()) {
     230            4 :             muse::JsonObject curveObj;
     231            4 :             writeKey(key, curveObj);
     232            4 :             curveObj[POINTS_KEY] = pointArray;
     233            4 :             rootArray << curveObj;
     234            4 :         }
     235            4 :     }
     236              : 
     237            6 :     return muse::JsonDocument(rootArray).toJson();
     238            3 : }
        

Generated by: LCOV version 2.0-1