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) 2021 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 <algorithm>
24 : #include <cmath>
25 :
26 : #include <unordered_map>
27 :
28 : #include "containers.h"
29 : #include "translation.h"
30 :
31 : #include "types/typesconv.h"
32 : #include "types/constants.h"
33 :
34 : #include "score.h"
35 : #include "segment.h"
36 : #include "tempotext.h"
37 :
38 : #include "log.h"
39 :
40 : using namespace mu;
41 : using namespace mu::engraving;
42 :
43 : namespace mu::engraving {
44 : //TODO: textChanged() needs to be called during/after editing
45 :
46 : //---------------------------------------------------------
47 : // tempoStyle
48 : //---------------------------------------------------------
49 :
50 : static const ElementStyle tempoStyle {
51 : { Sid::tempoSystemFlag, Pid::SYSTEM_FLAG },
52 : { Sid::tempoPlacement, Pid::PLACEMENT },
53 : { Sid::tempoMinDistance, Pid::MIN_DISTANCE },
54 : };
55 :
56 : //---------------------------------------------------------
57 : // TempoText
58 : //---------------------------------------------------------
59 :
60 811 : TempoText::TempoText(DummyParentOr<Segment> parent)
61 811 : : TextBase(ElementType::TEMPO_TEXT, parent, TextStyleType::TEMPO, ElementFlag::SYSTEM | ElementFlag::ON_STAFF)
62 : {
63 811 : initElementStyle(&tempoStyle);
64 811 : m_tempoTextType = TempoTextType::NORMAL;
65 811 : m_tempo = 2.0; // propertyDefault(P_TEMPO).toDouble();
66 811 : m_followText = false;
67 811 : m_relative = 1.0;
68 811 : m_isRelative = false;
69 :
70 811 : resetProperty(Pid::MUSIC_SYMBOL_SIZE);
71 811 : }
72 :
73 2 : void TempoText::setTempoTextType(TempoTextType ttt)
74 : {
75 2 : m_tempoTextType = ttt;
76 2 : }
77 :
78 8 : double TempoText::tempoBpm() const
79 : {
80 : //! NOTE: find tempo in format " = 180"
81 8 : std::regex regex("\\s*=\\s*(\\d+[.]{0,1}\\d*)");
82 8 : std::smatch match;
83 8 : std::string stru8 = xmlText().toStdString();
84 8 : std::regex_search(stru8, match, regex);
85 8 : if (match.empty() || match.size() < 1) {
86 0 : return 0;
87 : }
88 :
89 8 : double tempo = String::fromStdString(match[1].str()).toDouble();
90 8 : return tempo;
91 8 : }
92 :
93 : //---------------------------------------------------------
94 : // TempoPattern
95 : //---------------------------------------------------------
96 :
97 : struct TempoPattern {
98 : const char* pattern;
99 : double f;
100 : TDuration d;
101 836 : TempoPattern(const char* s, double v, DurationType val, int dots = 0)
102 836 : : pattern(s), f(v), d(val)
103 : {
104 836 : d.setDots(dots);
105 836 : }
106 : };
107 :
108 : // note: findTempoDuration requires the longer patterns to be before the shorter patterns in tp
109 :
110 : static const TempoPattern tp[] = {
111 : TempoPattern("\uECA5\\s*\uECB7\\s*\uECB7", 1.75 / 60.0, DurationType::V_QUARTER, 2), // double dotted 1/4
112 : TempoPattern("\uECA5\\s*\uECB7", 1.5 / 60.0, DurationType::V_QUARTER, 1), // dotted 1/4
113 : TempoPattern("\uECA5", 1.0 / 60.0, DurationType::V_QUARTER), // 1/4
114 : TempoPattern("\uECA3\\s*\uECB7\\s*\uECB7", 1.75 / 30.0, DurationType::V_HALF, 2), // double dotted 1/2
115 : TempoPattern("\uECA3\\s*\uECB7", 1.5 / 30.0, DurationType::V_HALF, 1), // dotted 1/2
116 : TempoPattern("\uECA3", 1.0 / 30.0, DurationType::V_HALF), // 1/2
117 : TempoPattern("\uECA7\\s*\uECB7\\s*\uECB7", 1.75 / 120.0, DurationType::V_EIGHTH, 2), // double dotted 1/8
118 : TempoPattern("\uECA7\\s*\uECB7", 1.5 / 120.0, DurationType::V_EIGHTH, 1), // dotted 1/8
119 : TempoPattern("\uECA7", 1.0 / 120.0, DurationType::V_EIGHTH), // 1/8
120 : TempoPattern("\uECA2\\s*\uECB7", 1.5 / 15.0, DurationType::V_WHOLE, 1), // dotted whole
121 : TempoPattern("\uECA2", 1.0 / 15.0, DurationType::V_WHOLE), // whole
122 : TempoPattern("\uECA9\\s*\uECB7", 1.5 / 240.0, DurationType::V_16TH, 1), // dotted 1/16
123 : TempoPattern("\uECA9", 1.0 / 240.0, DurationType::V_16TH), // 1/16
124 : TempoPattern("\uECAB\\s*\uECB7", 1.5 / 480.0, DurationType::V_32ND, 1), // dotted 1/32
125 : TempoPattern("\uECAB", 1.0 / 480.0, DurationType::V_32ND), // 1/32
126 : TempoPattern("\uECA1", 1.0 / 7.5, DurationType::V_BREVE), // longa
127 : TempoPattern("\uECA0", 1.0 / 7.5, DurationType::V_BREVE), // double whole
128 : TempoPattern("\uECAD", 1.0 / 960.0, DurationType::V_64TH), // 1/64
129 : TempoPattern("\uECAF", 1.0 / 1920.0, DurationType::V_128TH), // 1/128
130 : TempoPattern("\uECB1", 1.0 / 3840.0, DurationType::V_256TH), // 1/256
131 : TempoPattern("\uECB3", 1.0 / 7680.0, DurationType::V_512TH), // 1/512
132 : TempoPattern("\uECB5", 1.0 / 15360.0, DurationType::V_1024TH), // 1/1024
133 : };
134 :
135 : //---------------------------------------------------------
136 : // findTempoDuration
137 : // find the duration part (note + dot) of a tempo text in string s
138 : // return the match position or -1 if not found
139 : // set len to the match length and dur to the duration value
140 : //---------------------------------------------------------
141 :
142 625 : int TempoText::findTempoDuration(const String& s, int& len, TDuration& dur)
143 : {
144 625 : len = 0;
145 625 : dur = TDuration();
146 625 : std::string su8 = s.toStdString();
147 10235 : for (const auto& i : tp) {
148 9832 : std::regex regex(i.pattern);
149 9832 : std::smatch match;
150 9832 : std::regex_search(su8, match, regex);
151 9832 : if (!match.empty()) {
152 222 : len = static_cast<int>(String::fromStdString(match[0].str()).size());
153 222 : dur = i.d;
154 444 : return std::distance(su8.cbegin(), match[0].first);
155 : }
156 10054 : }
157 403 : return -1;
158 625 : }
159 :
160 8 : TDuration TempoText::duration() const
161 : {
162 8 : int dummy = 0;
163 8 : TDuration result;
164 :
165 8 : findTempoDuration(plainText(), dummy, result);
166 :
167 8 : return result;
168 : }
169 :
170 : static const TempoPattern tpSym[] = {
171 : TempoPattern("<sym>metNoteQuarterUp</sym><sym>space</sym><sym>metAugmentationDot</sym><sym>metAugmentationDot</sym>",
172 : 1.75 / 60.0, DurationType::V_QUARTER, 2), // double dotted 1/4
173 : TempoPattern("<sym>metNoteQuarterUp</sym><sym>space</sym><sym>metAugmentationDot</sym>", 1.5 / 60.0, DurationType::V_QUARTER,
174 : 1), // dotted 1/4
175 : TempoPattern("<sym>metNoteQuarterUp</sym>", 1.0 / 60.0, DurationType::V_QUARTER), // 1/4
176 : TempoPattern("<sym>metNoteHalfUp</sym><sym>space</sym><sym>metAugmentationDot</sym><sym>metAugmentationDot</sym>",
177 : 1.75 / 30.0, DurationType::V_HALF, 2), // double dotted 1/2
178 : TempoPattern("<sym>metNoteHalfUp</sym><sym>space</sym><sym>metAugmentationDot</sym>", 1.5 / 30.0, DurationType::V_HALF, 1), // dotted 1/2
179 : TempoPattern("<sym>metNoteHalfUp</sym>", 1.0 / 30.0, DurationType::V_HALF), // 1/2
180 : TempoPattern("<sym>metNote8thUp</sym><sym>space</sym><sym>metAugmentationDot</sym><sym>metAugmentationDot</sym>", 1.75 / 120.0,
181 : DurationType::V_EIGHTH, 2), // double dotted 1/8
182 : TempoPattern("<sym>metNote8thUp</sym><sym>space</sym><sym>metAugmentationDot</sym>", 1.5 / 120.0, DurationType::V_EIGHTH,
183 : 1), // dotted 1/8
184 : TempoPattern("<sym>metNote8thUp</sym>", 1.0 / 120.0, DurationType::V_EIGHTH), // 1/8
185 : TempoPattern("<sym>metNoteWhole</sym><sym>space</sym><sym>metAugmentationDot</sym>", 1.5 / 15.0, DurationType::V_WHOLE,
186 : 1), // dotted whole
187 : TempoPattern("<sym>metNoteWhole</sym>", 1.0 / 15.0, DurationType::V_WHOLE), // whole
188 : TempoPattern("<sym>metNote16thUp</sym><sym>space</sym><sym>metAugmentationDot</sym>", 1.5 / 240.0, DurationType::V_16TH, 1), // dotted 1/16
189 : TempoPattern("<sym>metNote16thUp</sym>", 1.0 / 240.0, DurationType::V_16TH), // 1/16
190 : TempoPattern("<sym>metNote32ndUp</sym><sym>space</sym><sym>metAugmentationDot</sym>", 1.5 / 480.0, DurationType::V_32ND, 1), // dotted 1/32
191 : TempoPattern("<sym>metNote32ndUp</sym>", 1.0 / 480.0, DurationType::V_32ND), // 1/32
192 : TempoPattern("<sym>metNoteDoubleWholeSquare</sym>", 1.0 / 7.5, DurationType::V_BREVE), // longa
193 : TempoPattern("<sym>metNoteDoubleWhole</sym>", 1.0 / 7.5, DurationType::V_BREVE), // double whole
194 : TempoPattern("<sym>metNote64thUp</sym>", 1.0 / 960.0, DurationType::V_64TH), // 1/64
195 : TempoPattern("<sym>metNote128thUp</sym>", 1.0 / 1920.0, DurationType::V_128TH), // 1/128
196 : TempoPattern("<sym>metNote256thUp</sym>", 1.0 / 3840.0, DurationType::V_256TH), // 1/256
197 : TempoPattern("<sym>metNote512thUp</sym>", 1.0 / 7680.0, DurationType::V_512TH), // 1/512
198 : TempoPattern("<sym>metNote1024thUp</sym>", 1.0 / 15360.0, DurationType::V_1024TH), // 1/1024
199 : };
200 :
201 : //---------------------------------------------------------
202 : // duration2tempoTextString
203 : // find the tempoText string representation for duration
204 : //---------------------------------------------------------
205 :
206 163 : String TempoText::duration2tempoTextString(const TDuration dur)
207 : {
208 651 : for (const TempoPattern& pa : tpSym) {
209 651 : if (pa.d == dur) {
210 163 : String res = String::fromUtf8(pa.pattern);
211 163 : return res;
212 0 : }
213 : }
214 0 : return u"";
215 : }
216 :
217 : //---------------------------------------------------------
218 : // tempo
219 : //---------------------------------------------------------
220 :
221 777 : BeatsPerSecond TempoText::tempo() const
222 : {
223 777 : if (m_isRelative) {
224 0 : const BeatsPerSecond tempoBefore = score()->tempo(tick() - Fraction::eps());
225 0 : const BeatsPerSecond v = tempoBefore * m_relative;
226 0 : return BeatsPerSecond(std::clamp(v.val, Constants::MIN_TEMPO.val, Constants::MAX_TEMPO.val));
227 : }
228 777 : return m_tempo;
229 : }
230 :
231 : //---------------------------------------------------------
232 : // undoChangeProperty
233 : //---------------------------------------------------------
234 :
235 0 : void TempoText::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps)
236 : {
237 0 : if (id == Pid::TEMPO_FOLLOW_TEXT) {
238 0 : EngravingObject::undoChangeProperty(id, v, ps);
239 0 : if (m_followText) {
240 0 : updateTempo();
241 : }
242 : } else {
243 0 : EngravingObject::undoChangeProperty(id, v, ps);
244 : }
245 0 : }
246 :
247 : //---------------------------------------------------------
248 : // updateTempo
249 : //---------------------------------------------------------
250 :
251 0 : void TempoText::updateTempo()
252 : {
253 : // cache regexp, they are costly to create
254 0 : static std::unordered_map<String, std::regex> regexps;
255 0 : static std::unordered_map<String, std::regex> regexps2;
256 0 : String s = plainText();
257 0 : s.replace(u",", u".");
258 0 : s.replace(u"<sym>space</sym>", u" ");
259 0 : s.replace(u"≒", u"=");
260 0 : s.replace(u"≈", u"=");
261 0 : s.replace(u"~", u"=");
262 0 : s.replace(u"ca.", u"");
263 0 : s.replace(u"c.", u"");
264 0 : s.replace(u"approx.", u"");
265 0 : std::string su8 = s.toStdString();
266 0 : for (const TempoPattern& pa : tp) {
267 0 : String pattern = String::fromUtf8(pa.pattern);
268 0 : std::regex re;
269 0 : if (!muse::contains(regexps, String::fromUtf8(pa.pattern))) {
270 0 : re = std::regex(String(u"%1\\s*=\\s*(\\d+[.]{0,1}\\d*)\\s*").arg(pattern).toStdString());
271 0 : regexps[pattern] = re;
272 : }
273 0 : re = muse::value(regexps, pattern);
274 0 : std::smatch match;
275 0 : std::regex_search(su8, match, re);
276 0 : if (!match.empty()) {
277 0 : if (match.size() == 2) {
278 0 : BeatsPerSecond nt = BeatsPerSecond(String::fromStdString(match[1].str()).toDouble() * pa.f);
279 0 : if (m_isRelative || nt != m_tempo) {
280 0 : undoChangeProperty(Pid::TEMPO, PropertyValue(nt), propertyFlags(Pid::TEMPO));
281 0 : m_relative = 1.0;
282 0 : m_isRelative = false;
283 : }
284 0 : break;
285 : }
286 : } else {
287 0 : for (const TempoPattern& pa2 : tp) {
288 0 : String pattern2 = String::fromUtf8(pa2.pattern);
289 0 : String key = String(u"%1_%2").arg(pattern, pattern2);
290 0 : std::regex re2;
291 0 : if (!muse::contains(regexps2, key)) {
292 0 : re2 = std::regex(String(u"%1\\s*=\\s*%2\\s*").arg(pattern, pattern2).toStdString());
293 0 : regexps2[key] = re2;
294 : }
295 0 : re2 = muse::value(regexps2, key);
296 0 : std::smatch match2;
297 0 : std::regex_search(su8, match2, re2);
298 0 : if (!match2.empty()) {
299 0 : m_relative = pa2.f / pa.f;
300 0 : m_isRelative = true;
301 0 : return;
302 : }
303 0 : }
304 : }
305 0 : }
306 0 : }
307 :
308 : //---------------------------------------------------------
309 : // setTempo
310 : //---------------------------------------------------------
311 :
312 799 : void TempoText::setTempo(BeatsPerSecond v)
313 : {
314 799 : if (v.val < Constants::MIN_TEMPO.val) {
315 0 : v = Constants::MIN_TEMPO;
316 799 : } else if (v.val > Constants::MAX_TEMPO.val) {
317 0 : v = Constants::MAX_TEMPO;
318 : }
319 799 : m_tempo = v;
320 799 : m_relative = 1.0;
321 799 : m_isRelative = false;
322 799 : }
323 :
324 : //---------------------------------------------------------
325 : // getProperty
326 : //---------------------------------------------------------
327 :
328 8291 : PropertyValue TempoText::getProperty(Pid propertyId) const
329 : {
330 8291 : switch (propertyId) {
331 565 : case Pid::PLAY:
332 565 : return m_playTempoText;
333 0 : case Pid::TEMPO:
334 0 : return m_tempo;
335 0 : case Pid::TEMPO_FOLLOW_TEXT:
336 0 : return m_followText;
337 10 : case Pid::TEMPO_ALIGN_RIGHT_OF_REHEARSAL_MARK:
338 10 : return m_alignRightOfRehearsalMark;
339 7716 : default:
340 7716 : return TextBase::getProperty(propertyId);
341 : }
342 : }
343 :
344 : //---------------------------------------------------------
345 : // setProperty
346 : //---------------------------------------------------------
347 :
348 16862 : bool TempoText::setProperty(Pid propertyId, const PropertyValue& v)
349 : {
350 16862 : switch (propertyId) {
351 0 : case Pid::PLAY:
352 0 : setPlayTempoText(v.toBool());
353 0 : break;
354 0 : case Pid::TEMPO:
355 0 : setTempo(v.value<BeatsPerSecond>());
356 0 : break;
357 0 : case Pid::TEMPO_FOLLOW_TEXT:
358 0 : setFollowText(v.toBool());
359 0 : break;
360 0 : case Pid::TEMPO_ALIGN_RIGHT_OF_REHEARSAL_MARK:
361 0 : m_alignRightOfRehearsalMark = v.toBool();
362 0 : break;
363 16862 : default:
364 16862 : if (!TextBase::setProperty(propertyId, v)) {
365 0 : return false;
366 : }
367 16862 : break;
368 : }
369 16862 : triggerLayout();
370 16862 : return true;
371 : }
372 :
373 : //---------------------------------------------------------
374 : // propertyDefault
375 : //---------------------------------------------------------
376 :
377 7519 : PropertyValue TempoText::propertyDefault(Pid id) const
378 : {
379 7519 : switch (id) {
380 565 : case Pid::PLAY:
381 565 : return true;
382 565 : case Pid::TEXT_STYLE:
383 565 : return TextStyleType::TEMPO;
384 0 : case Pid::TEMPO:
385 0 : return BeatsPerSecond(2.0);
386 0 : case Pid::TEMPO_FOLLOW_TEXT:
387 0 : return false;
388 0 : case Pid::TEMPO_ALIGN_RIGHT_OF_REHEARSAL_MARK:
389 0 : return true;
390 6389 : default:
391 6389 : return TextBase::propertyDefault(id);
392 : }
393 : }
394 :
395 : //---------------------------------------------------------
396 : // duration2userName
397 : //---------------------------------------------------------
398 :
399 0 : String TempoText::duration2userName(const TDuration t)
400 : {
401 0 : String dots;
402 0 : switch (t.dots()) {
403 0 : case 1: dots = muse::mtrc("engraving", "Dotted %1").arg(TConv::translatedUserName(t.type()));
404 0 : break;
405 0 : case 2: dots = muse::mtrc("engraving", "Double dotted %1").arg(TConv::translatedUserName(t.type()));
406 0 : break;
407 0 : case 3: dots = muse::mtrc("engraving", "Triple dotted %1").arg(TConv::translatedUserName(t.type()));
408 0 : break;
409 0 : case 4: dots = muse::mtrc("engraving", "Quadruple dotted %1").arg(TConv::translatedUserName(t.type()));
410 0 : break;
411 0 : default:
412 0 : dots = TConv::translatedUserName(t.type());
413 0 : break;
414 : }
415 0 : return dots;
416 0 : }
417 :
418 : //---------------------------------------------------------
419 : // accessibleInfo
420 : //---------------------------------------------------------
421 :
422 0 : String TempoText::accessibleInfo() const
423 : {
424 0 : String info = tempoInfo();
425 0 : if (info.empty()) {
426 0 : return TextBase::accessibleInfo();
427 : }
428 :
429 0 : return String(u"%1: %2").arg(EngravingItem::accessibleInfo(), info);
430 0 : }
431 :
432 0 : String TempoText::tempoInfo() const
433 : {
434 0 : TDuration t1;
435 0 : TDuration t2;
436 : int len1;
437 : int len2;
438 0 : String text = plainText();
439 0 : String firstPart = text.split(u" = ").front();
440 0 : String secondPart = text.split(u" = ").back();
441 0 : int x1 = findTempoDuration(firstPart, len1, t1);
442 0 : int x2 = -1;
443 0 : if (m_relative) {
444 0 : x2 = findTempoDuration(secondPart, len2, t2);
445 : }
446 :
447 0 : String info;
448 :
449 0 : if (x1 != -1) {
450 0 : String dots1 = duration2userName(t1);
451 0 : if (x2 != -1) {
452 0 : String dots2 = duration2userName(t2);
453 : //: %1 and %2 are note durations. If your language does not have different terms
454 : //: for "quarter note" and "quarter" (for example), or if the translations for
455 : //: the durations as separate strings are not suitable to be used as adjectives
456 : //: here, translate "%1 note" with "%1" and "%2 note" with "%2", so that just the
457 : //: duration will be shown.
458 0 : info = muse::mtrc("engraving", "%1 note = %2 note").arg(dots1, dots2);
459 0 : } else {
460 : //: %1 is a note duration. If your language does not have different terms for
461 : //: "quarter note" and "quarter" (for example), or if the translations for the
462 : //: durations as separate strings are not suitable to be used as adjectives here,
463 : //: translate "%1 note" with "%1", so that just the duration will be shown.
464 0 : info = muse::mtrc("engraving", "%1 note = %2").arg(dots1, secondPart);
465 : }
466 0 : }
467 :
468 0 : return info;
469 0 : }
470 :
471 0 : void TempoText::commitText()
472 : {
473 0 : if (m_followText) {
474 0 : updateTempo();
475 : }
476 :
477 0 : TextBase::commitText();
478 0 : }
479 : }
|