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 : }
|