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 "chordlist.h"
24 :
25 : #include "global/containers.h"
26 : #include "global/io/buffer.h"
27 : #include "global/io/file.h"
28 : #include "global/io/fileinfo.h"
29 :
30 : #include "types/constants.h"
31 :
32 : #include "rw/compat/compatutils.h"
33 : #include "rw/xmlreader.h"
34 : #include "rw/xmlwriter.h"
35 :
36 : #include "style/style.h"
37 :
38 : #include "mscore.h"
39 : #include "pitchspelling.h"
40 :
41 : #include "log.h"
42 :
43 : using namespace mu;
44 : using namespace muse;
45 : using namespace muse::io;
46 :
47 : namespace mu::engraving {
48 : //---------------------------------------------------------
49 : // HChord
50 : //---------------------------------------------------------
51 :
52 22077 : HChord::HChord(const String& str)
53 : {
54 : static const char* const scaleNames[2][12] = {
55 : { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" },
56 : { "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B" }
57 : };
58 22077 : m_keys = 0;
59 22077 : StringList sl = str.split(u' ', muse::SkipEmptyParts);
60 100142 : for (const String& s : sl) {
61 398428 : for (int i = 0; i < 12; ++i) {
62 398370 : if (s == scaleNames[0][i] || s == scaleNames[1][i]) {
63 78007 : operator+=(i);
64 78007 : break;
65 : }
66 : }
67 : }
68 22077 : }
69 :
70 : //---------------------------------------------------------
71 : // HChord
72 : //---------------------------------------------------------
73 :
74 0 : HChord::HChord(int a, int b, int c, int d, int e, int f, int g, int h, int i, int k, int l)
75 : {
76 0 : m_keys = 0;
77 0 : if (a >= 0) {
78 0 : operator+=(a);
79 : }
80 0 : if (b >= 0) {
81 0 : operator+=(b);
82 : }
83 0 : if (c >= 0) {
84 0 : operator+=(c);
85 : }
86 0 : if (d >= 0) {
87 0 : operator+=(d);
88 : }
89 0 : if (e >= 0) {
90 0 : operator+=(e);
91 : }
92 0 : if (f >= 0) {
93 0 : operator+=(f);
94 : }
95 0 : if (g >= 0) {
96 0 : operator+=(g);
97 : }
98 0 : if (h >= 0) {
99 0 : operator+=(h);
100 : }
101 0 : if (i >= 0) {
102 0 : operator+=(i);
103 : }
104 0 : if (k >= 0) {
105 0 : operator+=(k);
106 : }
107 0 : if (l >= 0) {
108 0 : operator+=(l);
109 : }
110 0 : }
111 :
112 : //---------------------------------------------------------
113 : // rotate
114 : // rotate 12 Bits
115 : //---------------------------------------------------------
116 :
117 0 : void HChord::rotate(int semiTones)
118 : {
119 0 : while (semiTones > 0) {
120 0 : if (m_keys & 0x800) {
121 0 : m_keys = ((m_keys & ~0x800) << 1) + 1;
122 : } else {
123 0 : m_keys <<= 1;
124 : }
125 0 : --semiTones;
126 : }
127 0 : while (semiTones < 0) {
128 0 : if (m_keys & 1) {
129 0 : m_keys = (m_keys >> 1) | 0x800;
130 : } else {
131 0 : m_keys >>= 1;
132 : }
133 0 : ++semiTones;
134 : }
135 0 : }
136 :
137 : //---------------------------------------------------------
138 : // name
139 : //---------------------------------------------------------
140 :
141 0 : String HChord::name(int tpc) const
142 : {
143 0 : static const HChord C0(0, 3, 6, 9);
144 0 : static const HChord C1(0, 3);
145 :
146 0 : String buf = tpc2name(tpc, NoteSpellingType::STANDARD, NoteCaseType::AUTO, false);
147 0 : HChord c(*this);
148 :
149 0 : int key = tpc2pitch(tpc);
150 :
151 0 : c.rotate(-key); // transpose to C
152 :
153 : // special cases
154 0 : if (c == C0) {
155 0 : buf += u"dim";
156 0 : return buf;
157 : }
158 0 : if (c == C1) {
159 0 : buf += u"no5";
160 0 : return buf;
161 : }
162 :
163 0 : bool seven = false;
164 0 : bool sharp9 = false;
165 0 : bool nat11 = false;
166 0 : bool sharp11 = false;
167 0 : bool nat13 = false;
168 0 : bool flat13 = false;
169 :
170 : // minor?
171 0 : if (c.contains(3)) {
172 0 : if (!c.contains(4)) {
173 0 : buf += u"m";
174 : } else {
175 0 : sharp9 = true;
176 : }
177 : }
178 :
179 : // 7
180 0 : if (c.contains(11)) {
181 0 : buf += u"Maj7";
182 0 : seven = true;
183 0 : } else if (c.contains(10)) {
184 0 : buf += u"7";
185 0 : seven = true;
186 : }
187 :
188 : // 4
189 0 : if (c.contains(5)) {
190 0 : if (!c.contains(4)) {
191 0 : buf += u"sus4";
192 : } else {
193 0 : nat11 = true;
194 : }
195 : }
196 :
197 : // 5
198 0 : if (c.contains(7)) {
199 0 : if (c.contains(6)) {
200 0 : sharp11 = true;
201 : }
202 0 : if (c.contains(8)) {
203 0 : flat13 = true;
204 : }
205 : } else {
206 0 : if (c.contains(6)) {
207 0 : buf += u"b5";
208 : }
209 0 : if (c.contains(8)) {
210 0 : buf += u"#5";
211 : }
212 : }
213 :
214 : // 6
215 0 : if (c.contains(9)) {
216 0 : if (!seven) {
217 0 : buf += u"6";
218 : } else {
219 0 : nat13 = true;
220 : }
221 : }
222 :
223 : // 9
224 0 : if (c.contains(1)) {
225 0 : buf += u"b9";
226 : }
227 0 : if (c.contains(2)) {
228 0 : buf += u"9";
229 : }
230 0 : if (sharp9) {
231 0 : buf += u"#9";
232 : }
233 :
234 : // 11
235 0 : if (nat11) {
236 0 : buf += u"11 ";
237 : }
238 0 : if (sharp11) {
239 0 : buf += u"#11";
240 : }
241 :
242 : // 13
243 0 : if (flat13) {
244 0 : buf += u"b13";
245 : }
246 0 : if (nat13) {
247 0 : if (c.contains(1) || c.contains(2) || sharp9 || nat11 || sharp11) {
248 0 : buf += u"13";
249 : } else {
250 0 : buf += u"add13";
251 : }
252 : }
253 0 : return buf;
254 0 : }
255 :
256 : //---------------------------------------------------------
257 : // voicing
258 : //---------------------------------------------------------
259 :
260 60 : String HChord::voicing() const
261 : {
262 60 : String s = u"C";
263 60 : const char* names[] = { "C", " Db", " D", " Eb", " E", " F", " Gb", " G", " Ab", " A", " Bb", " B" };
264 :
265 720 : for (int i = 1; i < 12; i++) {
266 660 : if (contains(i)) {
267 174 : s += String::fromAscii(names[i]);
268 : }
269 : }
270 60 : return s;
271 0 : }
272 :
273 : //---------------------------------------------------------
274 : // print
275 : //---------------------------------------------------------
276 :
277 0 : void HChord::print() const
278 : {
279 0 : const char* names[] = { "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B" };
280 :
281 0 : for (int i = 0; i < 12; i++) {
282 0 : if (contains(i)) {
283 0 : LOGD(" %s", names[i]);
284 : }
285 : }
286 0 : }
287 :
288 : //---------------------------------------------------------
289 : // add
290 : //---------------------------------------------------------
291 :
292 16758 : void HChord::add(const std::vector<HDegree>& degreeList)
293 : {
294 : // LOGD("HChord::add ");print();
295 : // convert degrees to semitones
296 : static const int degreeTable[] = {
297 : // 1 2 3 4 5 6 7
298 : // C D E F G A B
299 : 0, 2, 4, 5, 7, 9, 11
300 : };
301 : // factor in the degrees
302 32694 : for (const HDegree& d : degreeList) {
303 15936 : int i = (d.value() - 1) % 7;
304 15936 : if (i < 0) {
305 2 : continue; // ?
306 : }
307 15934 : int dv = degreeTable[i] + d.alter();
308 15934 : int dv1 = degreeTable[i];
309 :
310 15934 : if (d.value() == 7 && d.alter() == 0) {
311 0 : dv -= 1;
312 : }
313 :
314 15934 : if (d.type() == HDegreeType::ADD) {
315 11296 : *this += dv;
316 4638 : } else if (d.type() == HDegreeType::ALTER) {
317 0 : if (contains(dv1)) {
318 0 : *this -= dv1;
319 0 : *this += dv;
320 : } else {
321 : // LOGD("HDegreeType::ALTER: chord does not contain degree %d(%d):",
322 : // d.value(), d.alter());
323 : // print();
324 0 : *this += dv; // DEBUG: default to add
325 : }
326 4638 : } else if (d.type() == HDegreeType::SUBTRACT) {
327 4638 : if (contains(dv1)) {
328 4621 : *this -= dv1;
329 : } else {
330 17 : LOGD("SUB: chord does not contain degree %d(%d):",
331 : d.value(), d.alter());
332 : }
333 : } else {
334 0 : LOGD("degree type %d not supported", static_cast<int>(d.type()));
335 : }
336 :
337 : // LOGD(" HCHord::added "); print();
338 : }
339 16758 : }
340 :
341 : //---------------------------------------------------------
342 : // readRenderList
343 : //---------------------------------------------------------
344 :
345 111012 : static void readRenderList(String val, std::vector<RenderActionPtr>& renderList, int mscVersion)
346 : {
347 111012 : renderList.clear();
348 111012 : StringList sl = val.split(u' ', muse::SkipEmptyParts);
349 322341 : for (const String& s : sl) {
350 211329 : if (s.startsWith(u"m:")) {
351 39614 : StringList ssl = s.split(u':', muse::SkipEmptyParts);
352 39614 : if (ssl.size() == 3) {
353 : // m:x:y
354 39614 : double movex = ssl.at(1).toDouble();
355 39614 : double movey = ssl.at(2).toDouble();
356 :
357 39614 : if (mscVersion < 460) {
358 5759 : movex = compat::CompatUtils::convertChordExtModUnits(movex);
359 5759 : movey = compat::CompatUtils::convertChordExtModUnits(movey);
360 : }
361 :
362 39614 : renderList.emplace_back(new RenderActionMove(movex, movey));
363 : }
364 211329 : } else if (s.startsWith(u"ms:")) {
365 20130 : StringList ssl = s.split(u':', muse::SkipEmptyParts);
366 20130 : if (ssl.size() == 3) {
367 : // ms:x:y
368 20130 : double movex = ssl[1].toDouble();
369 20130 : double movey = ssl[2].toDouble();
370 :
371 20130 : renderList.emplace_back(new RenderActionMoveScaled(movex, movey));
372 : }
373 171715 : } else if (s == u":mx") {
374 1830 : renderList.emplace_back(new RenderActionMoveXHeight(true));
375 149755 : } else if (s == u":mxs") {
376 915 : renderList.emplace_back(new RenderActionMoveXHeightScaled(true));
377 148840 : } else if (s.startsWith(u"sc:")) {
378 3660 : StringList ssl = s.split(u':', muse::SkipEmptyParts);
379 3660 : if (ssl.size() == 2) {
380 3660 : double scale = ssl.at(1).toDouble();
381 3660 : renderList.emplace_back(new RenderActionScale(scale));
382 : }
383 148840 : } else if (s == u":push") {
384 3279 : renderList.emplace_back(new RenderActionPush());
385 141901 : } else if (s == u":pop") {
386 1449 : renderList.emplace_back(new RenderActionPop());
387 140452 : } else if (s == u":popx") {
388 0 : renderList.emplace_back(new RenderActionPopX());
389 140452 : } else if (s == u":popy") {
390 1830 : renderList.emplace_back(new RenderActionPopY());
391 138622 : } else if (s == u":n") {
392 5314 : renderList.emplace_back(new RenderActionNote());
393 133308 : } else if (s == u":a") {
394 5314 : renderList.emplace_back(new RenderActionAccidental());
395 127994 : } else if (s == u":pl") {
396 915 : renderList.emplace_back(new RenderActionParenLeft());
397 127079 : } else if (s == u":pr") {
398 915 : renderList.emplace_back(new RenderActionParenRight());
399 : } else {
400 126164 : renderList.emplace_back(new RenderActionSet(s));
401 : }
402 : }
403 111012 : }
404 :
405 : //---------------------------------------------------------
406 : // writeRenderList
407 : //---------------------------------------------------------
408 :
409 0 : static void writeRenderList(XmlWriter& xml, const std::vector<RenderActionPtr>& al, const AsciiStringView& name)
410 : {
411 0 : String s;
412 :
413 0 : for (const RenderActionPtr& a : al) {
414 0 : if (!s.isEmpty()) {
415 0 : s += u" ";
416 : }
417 0 : switch (a->actionType()) {
418 0 : case RenderAction::RenderActionType::SET: {
419 0 : const RenderActionSetPtr set = std::static_pointer_cast<RenderActionSet>(a);
420 0 : s += set->text();
421 0 : break;
422 0 : }
423 0 : case RenderAction::RenderActionType::MOVE: {
424 0 : const RenderActionMovePtr move = std::static_pointer_cast<RenderActionMove>(a);
425 :
426 0 : if (!RealIsNull(move->x()) || !RealIsNull(move->y())) {
427 0 : String scaled = move->scaled() ? u"s" : u"";
428 0 : s += String(u"m%1:%2:%3").arg(scaled).arg(move->x()).arg(move->y());
429 0 : }
430 0 : break;
431 0 : }
432 0 : case RenderAction::RenderActionType::MOVEXHEIGHT: {
433 0 : const RenderActionMoveXHeightPtr movex = std::static_pointer_cast<RenderActionMoveXHeight>(a);
434 0 : String scaled = movex->scaled() ? u"s" : u"";
435 0 : s += String(u":mx%1").arg(scaled);
436 0 : break;
437 0 : }
438 0 : case RenderAction::RenderActionType::SCALE: {
439 0 : const RenderActionScalePtr scale = std::static_pointer_cast<RenderActionScale>(a);
440 0 : s+= String(u"sc:%1").arg(scale->scale());
441 0 : break;
442 0 : }
443 0 : case RenderAction::RenderActionType::PUSH:
444 0 : s += u":push";
445 0 : break;
446 0 : case RenderAction::RenderActionType::POP: {
447 0 : const RenderActionPopPtr pop = std::static_pointer_cast<RenderActionPop>(a);
448 0 : String coord = pop->popX() && pop->popY() ? u"" : (pop->popX() ? u"x" : u"y");
449 0 : s += String(u":pop%1").arg(coord);
450 0 : break;
451 0 : }
452 0 : case RenderAction::RenderActionType::NOTE:
453 0 : s += u":n";
454 0 : break;
455 0 : case RenderAction::RenderActionType::ACCIDENTAL:
456 0 : s += u":a";
457 0 : break;
458 0 : case RenderAction::RenderActionType::PAREN: {
459 0 : const RenderActionParenPtr paren = std::static_pointer_cast<RenderActionParen>(a);
460 0 : s += paren->direction() == DirectionH::LEFT ? u":pl" : u":pr";
461 0 : break;
462 0 : }
463 0 : case RenderAction::RenderActionType::STOPHALIGN:
464 : // Internal, skip
465 0 : break;
466 : }
467 : }
468 0 : xml.tag(name, s);
469 0 : }
470 :
471 : //---------------------------------------------------------
472 : // read
473 : //---------------------------------------------------------
474 :
475 101122 : void ChordToken::read(XmlReader& e, int mscVersion)
476 : {
477 101122 : String c = e.attribute("class");
478 101122 : if (c == "quality") {
479 0 : tokenClass = ChordTokenClass::QUALITY;
480 101122 : } else if (c == "extension") {
481 28980 : tokenClass = ChordTokenClass::EXTENSION;
482 72142 : } else if (c == "modifier") {
483 48961 : tokenClass = ChordTokenClass::MODIFIER;
484 23181 : } else if (c == "type") {
485 12810 : tokenClass = ChordTokenClass::TYPE;
486 10371 : } else if (c == "accidental") {
487 4575 : tokenClass = ChordTokenClass::ACCIDENTAL;
488 : } else {
489 5796 : tokenClass = ChordTokenClass::ALL;
490 : }
491 306264 : while (e.readNextStartElement()) {
492 205142 : const AsciiStringView tag(e.name());
493 205142 : if (tag == "name") {
494 104020 : names << e.readText();
495 101122 : } else if (tag == "render") {
496 101122 : readRenderList(e.readText(), renderList, mscVersion);
497 : }
498 : }
499 101122 : }
500 :
501 : //---------------------------------------------------------
502 : // write
503 : //---------------------------------------------------------
504 :
505 0 : void ChordToken::write(XmlWriter& xml) const
506 : {
507 0 : XmlWriter::Attributes attrs;
508 0 : switch (tokenClass) {
509 0 : case ChordTokenClass::QUALITY:
510 0 : attrs.push_back({ "class", "quality" });
511 0 : break;
512 0 : case ChordTokenClass::EXTENSION:
513 0 : attrs.push_back({ "class", "extension" });
514 0 : break;
515 0 : case ChordTokenClass::MODIFIER:
516 0 : attrs.push_back({ "class", "modifier" });
517 0 : break;
518 0 : case ChordTokenClass::TYPE:
519 0 : attrs.push_back({ "class", "type" });
520 0 : break;
521 0 : case ChordTokenClass::ACCIDENTAL:
522 0 : attrs.push_back({ "accidental", "type" });
523 0 : break;
524 0 : default:
525 0 : break;
526 : }
527 0 : xml.startElement("token", attrs);
528 0 : for (const String& s : names) {
529 0 : xml.tag("name", s);
530 : }
531 0 : writeRenderList(xml, renderList, "render");
532 0 : xml.endElement();
533 0 : }
534 :
535 : //---------------------------------------------------------
536 : // configure
537 : //---------------------------------------------------------
538 :
539 16837 : void ParsedChord::configure(const ChordList* cl)
540 : {
541 16837 : if (!cl) {
542 0 : return;
543 : }
544 : // TODO: allow this to be parameterized via chord list
545 16837 : m_major << u"ma" << u"maj" << u"major" << u"t" << u"^";
546 16837 : m_minor << u"mi" << u"min" << u"minor" << u"-" << u"=";
547 16837 : m_diminished << u"dim" << u"dim." << u"o";
548 16837 : m_augmented << u"aug" << u"+";
549 16837 : m_lower << u"b" << u"-" << u"dim";
550 16837 : m_raise << u"#" << u"+" << u"aug";
551 16837 : m_mod << u"sus" << u"add" << u"no" << u"omit" << u"^" << u"type";
552 16837 : m_symbols << u"t" << u"^" << u"-" << u"+" << u"o" << u"0";
553 : }
554 :
555 : //---------------------------------------------------------
556 : // correctXmlText
557 : // remove digits from _xmlText, optionally replace with s
558 : // needed for m(Maj9) et al
559 : //---------------------------------------------------------
560 :
561 529 : void ParsedChord::correctXmlText(const String& s)
562 : {
563 529 : String xmlText = m_xmlText;
564 529 : xmlText.remove(std::regex("[0-9]"));
565 529 : if (!s.empty()) {
566 529 : size_t pos = xmlText.lastIndexOf(u')');
567 529 : if (pos == muse::nidx) {
568 529 : pos = xmlText.size();
569 : }
570 529 : xmlText.insert(pos, s);
571 : }
572 529 : m_xmlText = xmlText;
573 529 : }
574 :
575 : //---------------------------------------------------------
576 : // parse
577 : // returns true if chord was parseable
578 : //---------------------------------------------------------
579 :
580 16837 : bool ParsedChord::parse(const String& s, const ChordList* cl, bool syntaxOnly, bool preferMinor)
581 : {
582 16837 : String tok1, tok1L, tok2, tok2L;
583 16837 : String extensionDigits = u"123456789";
584 16837 : String special = u"()[],/\\ ";
585 16837 : String leading = u"([ ";
586 16837 : String trailing = u")],/\\ ";
587 16837 : String initial;
588 16837 : bool take6 = false, take7 = false, take9 = false, take11 = false, take13 = false;
589 16837 : size_t lastLeadingToken = 0;
590 16837 : size_t len = s.size();
591 16837 : size_t i = 0;
592 16837 : int thirdKey = 0, seventhKey = 0;
593 16837 : bool susChord = false;
594 16837 : std::vector<HDegree> hdl;
595 16837 : int key[] = { 0, 0, 2, 4, 5, 7, 9, 11, 0, 2, 4, 5, 7, 9, 11 };
596 :
597 16837 : configure(cl);
598 16837 : m_name = s;
599 16837 : m_parseable = true;
600 16837 : m_understandable = true;
601 :
602 16837 : lastLeadingToken = m_tokenList.size();
603 : // get quality
604 33686 : for (tok1 = u"", tok1L = u"", initial = u""; i < len; ++i) {
605 : // up to first (non-zero) digit, paren, or comma
606 31558 : if (extensionDigits.contains(s.at(i)) || special.contains(s.at(i))) {
607 14709 : break;
608 : }
609 16849 : tok1.append(s.at(i));
610 16849 : tok1L.append(s.at(i).toLower());
611 29162 : if (tok1L == u"m" || m_major.contains(tok1L) || m_minor.contains(tok1L) || m_diminished.contains(tok1L)
612 29162 : || m_augmented.contains(tok1L)) {
613 9218 : initial = tok1;
614 : }
615 : }
616 : // special case for "madd", which needs to parse as m,add rather than ma,dd
617 16837 : if (tok1L.startsWith(u"madd")) {
618 162 : initial = tok1.at(0);
619 : }
620 : // quality and first modifier ran together with no separation - eg, mima7, augadd
621 : // keep quality portion, reset index to read modifier portion later
622 : // Prevent greedy capture of first character as chord quality in the following cases
623 : static const std::array<String, 3> modifiersStartingWithQualityCharacters {
624 : u"tristan",
625 : u"omit",
626 : u"type"
627 16837 : };
628 16837 : if (!initial.empty() && initial != tok1 && !muse::contains(modifiersStartingWithQualityCharacters, tok1L)) {
629 357 : i -= (tok1.size() - initial.size());
630 357 : tok1 = initial;
631 357 : tok1L = initial.toLower();
632 : }
633 : // determine quality
634 16837 : if (tok1 == u"M" || m_major.contains(tok1L)) {
635 1655 : m_quality = u"major";
636 1655 : take6 = true;
637 1655 : take7 = true;
638 1655 : take9 = true;
639 1655 : take11 = true;
640 1655 : take13 = true;
641 1655 : if (!syntaxOnly) {
642 1648 : m_chord = HChord(u"C E G");
643 : }
644 15182 : } else if (tok1 == u"m" || m_minor.contains(tok1L)) {
645 2889 : m_quality = u"minor";
646 2889 : take6 = true;
647 2889 : take7 = true;
648 2889 : take9 = true;
649 2889 : take11 = true;
650 2889 : take13 = true;
651 2889 : if (!syntaxOnly) {
652 2870 : m_chord = HChord(u"C Eb G");
653 : }
654 12293 : } else if (m_diminished.contains(tok1L)) {
655 581 : m_quality = u"diminished";
656 581 : take7 = true;
657 581 : if (!syntaxOnly) {
658 579 : m_chord = HChord(u"C Eb Gb");
659 : }
660 11712 : } else if (m_augmented.contains(tok1L)) {
661 515 : m_quality = u"augmented";
662 515 : take7 = true;
663 515 : if (!syntaxOnly) {
664 514 : m_chord = HChord(u"C E G#");
665 : }
666 11197 : } else if (tok1L == "0") {
667 6 : m_quality = u"half-diminished";
668 6 : if (!syntaxOnly) {
669 3 : m_chord = HChord(u"C Eb Gb Bb");
670 : }
671 11191 : } else if (tok1L.empty()) {
672 : // empty quality - this will turn out to be major or dominant (or minor if preferMinor)
673 9608 : m_quality = u"";
674 9608 : if (!syntaxOnly) {
675 9572 : m_chord = preferMinor ? HChord(u"C Eb G") : HChord(u"C E G");
676 : }
677 9608 : if (preferMinor) {
678 0 : m_name = u"=" + m_name;
679 : }
680 : } else {
681 : // anything else is not a quality after all, but a modifier
682 : // reset to read again as modifier
683 1583 : m_quality = u"";
684 1583 : tok1 = u"";
685 1583 : tok1L = u"";
686 1583 : i = static_cast<int>(lastLeadingToken);
687 1583 : if (!syntaxOnly) {
688 1572 : m_chord = HChord(u"C E G");
689 : }
690 : }
691 16837 : if (tok1L == "=") {
692 0 : tok1 = u"";
693 0 : tok1L = u"";
694 : }
695 16837 : if (!tok1.empty()) {
696 5646 : addToken(tok1, ChordTokenClass::QUALITY);
697 : }
698 16837 : if (!syntaxOnly) {
699 16758 : m_xmlKind = m_quality;
700 16758 : m_xmlParens = u"no";
701 16758 : if (m_symbols.contains(tok1)) {
702 302 : m_xmlSymbols = u"yes";
703 302 : m_xmlText = u"";
704 : } else {
705 16456 : m_xmlSymbols = u"no";
706 16456 : m_xmlText = tok1;
707 : }
708 : }
709 : // eat trailing parens and commas
710 16837 : while (i < len && trailing.contains(s.at(i))) {
711 0 : addToken(String(s.at(i++)), ChordTokenClass::QUALITY);
712 : }
713 :
714 16837 : size_t prevIdx = i;
715 :
716 : // Get type - either "typen" or "III"
717 : // Eat up to first number
718 25392 : for (tok1 = u""; i < len; ++i) {
719 23202 : if (s.at(i).isDigit()) {
720 14647 : break;
721 : }
722 8555 : tok1.append(s.at(i));
723 : }
724 :
725 16837 : if (tok1 == "type") {
726 0 : addToken(tok1, ChordTokenClass::TYPE);
727 : // Read number
728 0 : for (tok1 = u""; i < len; ++i) {
729 0 : if (!s.at(i).isDigit()) {
730 0 : break;
731 : }
732 0 : addToken(s.at(i), ChordTokenClass::TYPE);
733 : }
734 16837 : } else if (tok1.contains(std::wregex(L"[IVX]+"))) {
735 : // Get roman numerals
736 39 : for (i = 0; i < tok1.size(); i++) {
737 27 : addToken(tok1.at(i), ChordTokenClass::TYPE);
738 : }
739 : } else {
740 16825 : i = prevIdx;
741 : }
742 :
743 16837 : lastLeadingToken = m_tokenList.size();
744 : // get extension - up to first non-digit other than comma or slash
745 32455 : for (tok1 = u""; i < len; ++i) {
746 27207 : if (!s.at(i).isDigit() && s.at(i) != ',' && s.at(i) != '/') {
747 11589 : break;
748 : }
749 15618 : tok1.append(s.at(i));
750 : }
751 16837 : m_extension = tok1;
752 16837 : if (m_quality.empty()) {
753 11191 : if (m_extension == "7" || m_extension == "9" || m_extension == "11" || m_extension == "13") {
754 7593 : m_quality = preferMinor ? u"minor" : u"dominant";
755 7593 : if (!syntaxOnly) {
756 7584 : m_xmlKind = preferMinor ? u"minor" : u"dominant";
757 : }
758 7593 : take7 = true;
759 7593 : take9 = true;
760 7593 : take11 = true;
761 7593 : take13 = true;
762 : } else {
763 3598 : m_quality = preferMinor ? u"minor" : u"major";
764 3598 : if (!syntaxOnly) {
765 3560 : m_xmlKind = preferMinor ? u"minor" : u"major";
766 : }
767 3598 : take6 = true;
768 3598 : take7 = true;
769 3598 : take9 = true;
770 3598 : take11 = true;
771 3598 : take13 = true;
772 : }
773 : }
774 16837 : if (!tok1.empty()) {
775 12611 : addToken(tok1, ChordTokenClass::EXTENSION);
776 : }
777 16837 : if (!syntaxOnly) {
778 16758 : if (m_quality == "minor") {
779 2870 : thirdKey = 3;
780 : } else {
781 13888 : thirdKey = 4;
782 : }
783 16758 : if (m_quality == "major") {
784 5208 : seventhKey = 11;
785 11550 : } else if (m_quality == "diminished") {
786 579 : seventhKey = 9;
787 : } else {
788 10971 : seventhKey = 10;
789 : }
790 16758 : m_xmlText += m_extension;
791 16758 : StringList extl;
792 16758 : if (tok1 == "2") {
793 55 : String d = u"add" + tok1;
794 55 : m_xmlDegrees << d;
795 55 : m_xmlText.remove(tok1);
796 55 : m_chord += 2;
797 16758 : } else if (tok1 == "4") {
798 55 : String d = u"add" + tok1;
799 55 : m_xmlDegrees << d;
800 55 : m_xmlText.remove(tok1);
801 55 : m_chord += 5;
802 16703 : } else if (tok1 == "5") {
803 434 : m_xmlKind = u"power";
804 434 : m_chord -= thirdKey;
805 16214 : } else if (tok1 == "6") {
806 569 : if (take6) {
807 569 : m_xmlKind += u"-sixth";
808 : } else {
809 0 : extl << u"6";
810 : }
811 569 : m_chord += 9;
812 15645 : } else if (tok1 == "7") {
813 6170 : if (take7) {
814 6170 : m_xmlKind += u"-seventh";
815 0 : } else if (m_xmlKind != "half-diminished") {
816 0 : extl << u"7";
817 : }
818 6170 : m_chord += seventhKey;
819 9475 : } else if (tok1 == "9") {
820 2301 : if (take9) {
821 2192 : m_xmlKind += u"-ninth";
822 109 : } else if (take7) {
823 109 : m_xmlKind += u"-seventh";
824 109 : extl << u"9";
825 109 : correctXmlText(u"7");
826 0 : } else if (m_xmlKind == u"half-diminished") {
827 0 : extl << u"9";
828 0 : correctXmlText();
829 : } else {
830 0 : extl << u"7" << u"9";
831 0 : correctXmlText();
832 : }
833 2301 : m_chord += seventhKey;
834 2301 : m_chord += 2;
835 7174 : } else if (tok1 == u"11") {
836 349 : if (take11) {
837 349 : m_xmlKind += u"-11th";
838 0 : } else if (take7) {
839 0 : m_xmlKind += u"-seventh";
840 0 : extl << u"9" << u"11";
841 0 : correctXmlText(u"7");
842 0 : } else if (m_xmlKind == u"half-diminished") {
843 0 : extl << u"9" << u"11";
844 0 : correctXmlText();
845 : } else {
846 0 : extl << u"7" << u"9" << u"11";
847 0 : correctXmlText();
848 : }
849 349 : m_chord += seventhKey;
850 349 : m_chord += 2;
851 349 : m_chord += 5;
852 6825 : } else if (tok1 == u"13") {
853 2401 : if (take13) {
854 2346 : m_xmlKind += u"-13th";
855 55 : } else if (take7) {
856 55 : m_xmlKind += u"-seventh";
857 55 : extl << u"9" << u"11" << u"13";
858 55 : correctXmlText(u"7");
859 0 : } else if (m_xmlKind == u"half-diminished") {
860 0 : extl << u"9" << u"11" << u"13";
861 0 : correctXmlText();
862 : } else {
863 0 : extl << u"7" << u"9" << u"11" << u"13";
864 0 : correctXmlText();
865 : }
866 2401 : m_chord += seventhKey;
867 2401 : m_chord += 2;
868 2401 : m_chord += 5;
869 2401 : m_chord += 9;
870 4424 : } else if (tok1 == u"69" || tok1 == u"6,9" || tok1 == u"6/9") {
871 239 : if (take6) {
872 239 : m_xmlKind += u"-sixth";
873 239 : extl << u"9";
874 239 : correctXmlText(u"6");
875 : } else {
876 0 : extl << u"6" << u"9";
877 0 : correctXmlText();
878 : }
879 239 : m_extension = u"69";
880 239 : m_chord += 9;
881 239 : m_chord += 2;
882 : }
883 17271 : for (const String& e : extl) {
884 513 : String d = u"add" + e;
885 513 : m_xmlDegrees << d;
886 513 : }
887 16758 : if (m_xmlKind == u"dominant-seventh") {
888 3930 : m_xmlKind = u"dominant";
889 : }
890 16758 : }
891 : // eat trailing parens and commas
892 16837 : while (i < len && trailing.contains(s.at(i))) {
893 0 : addToken(String(s.at(i++)), ChordTokenClass::EXTENSION);
894 : }
895 :
896 : // get modifiers
897 16837 : bool addPending = false;
898 16837 : bool susPending = false;
899 16837 : m_modifierList.clear();
900 38195 : while (i < len) {
901 : // eat leading parens
902 23146 : while (i < len && leading.contains(s.at(i))) {
903 1788 : addToken(String(s.at(i++)), ChordTokenClass::MODIFIER);
904 1788 : m_xmlParens = u"yes";
905 : }
906 : // get first token - up to first digit, paren, or comma
907 56784 : for (tok1 = u"", tok1L = u"", initial = u""; i < len; ++i) {
908 55695 : if (s.at(i).isDigit() || special.contains(s.at(i))) {
909 20269 : break;
910 : }
911 35426 : tok1.append(s.at(i));
912 35426 : tok1L.append(s.at(i).toLower());
913 35426 : if (m_mod.contains(tok1L)) {
914 5924 : initial = tok1;
915 : }
916 : }
917 : // if we reached the end of the string and never got a token,
918 : // then nothing to do, and no sense in looking for a second token
919 21358 : if (i == len && tok1.empty()) {
920 0 : break;
921 : }
922 21358 : if (!initial.empty() && initial != tok1) {
923 : // two modifiers ran together with no separation - eg, susb9
924 : // keep first, reset index to read second later
925 2962 : i -= (tok1.size() - initial.size());
926 2962 : tok1 = initial;
927 2962 : tok1L = initial.toLower();
928 : }
929 : // for "add" and "sus", just add the token and then read argument as a separate modifier
930 : // this allows the argument to itself be a two-part string
931 : // thus allowing addb9 -> add;b,9
932 21358 : if (tok1L == "add") {
933 1681 : addToken(tok1, ChordTokenClass::MODIFIER);
934 1681 : addPending = true;
935 5059 : continue;
936 : }
937 : // When there are no numbers after, default to sus4 further down
938 19677 : if (tok1L == "sus" && i != len) {
939 3378 : addToken(tok1, ChordTokenClass::MODIFIER);
940 3378 : susPending = true;
941 3378 : continue;
942 : }
943 : // eat spaces
944 16355 : while (i < len && s.at(i) == u' ') {
945 56 : ++i;
946 : }
947 : // get second token - a number <= 13
948 36202 : for (tok2 = u""; i < len; ++i) {
949 26386 : if (!s.at(i).isDigit()) {
950 6462 : break;
951 : }
952 19924 : if (tok2.size() == 1 && (tok2.at(0) != u'1' || s.at(i) > u'3')) {
953 21 : break;
954 : }
955 19903 : tok2.append(s.at(i));
956 : }
957 : // check suffix - 'st', 'nd', 'rd', 'th'
958 16299 : String suffix;
959 : static const int SUFFIX_LEN = 2;
960 27502 : for (int j = 0; j < SUFFIX_LEN && i + j < len; j++) {
961 11203 : suffix.append(s.at(i + j));
962 : }
963 :
964 16299 : if (suffix.contains(std::wregex(L"st|nd|rd|th"))) {
965 17 : i += SUFFIX_LEN;
966 : } else {
967 16282 : suffix.clear();
968 : }
969 :
970 16299 : tok2L = tok2.toLower();
971 : // re-attach "add"
972 16299 : if (addPending) {
973 1681 : if (m_raise.contains(tok1L)) {
974 239 : tok1L = u"#";
975 1442 : } else if (m_lower.contains(tok1L)) {
976 79 : tok1L = u"b";
977 1363 : } else if (tok1 == "M" || m_major.contains(tok1L)) {
978 1 : tok1L = u"major";
979 : }
980 1681 : tok2L = tok1L + tok2L;
981 1681 : tok1L = u"add";
982 : }
983 : // re-attach "sus"
984 16299 : if (susPending) {
985 3378 : if (m_raise.contains(tok1L)) {
986 1540 : tok1L = u"#";
987 1838 : } else if (m_lower.contains(tok1L)) {
988 1100 : tok1L = u"b";
989 738 : } else if (tok1 == "M" || m_major.contains(tok1L)) {
990 0 : tok1L = u"major";
991 : }
992 3378 : tok2L = tok1L + tok2L;
993 3378 : tok1L = u"sus";
994 : }
995 : // standardize spelling
996 16299 : if (tok1 == "M" || m_major.contains(tok1L)) {
997 132 : tok1L = u"major";
998 16167 : } else if (tok1L == "omit") {
999 55 : tok1L = u"no";
1000 16112 : } else if (tok1L == "sus" && tok2L.empty()) {
1001 517 : tok2L = u"4";
1002 15595 : } else if (m_augmented.contains(tok1L) && tok2L.empty()) {
1003 222 : if (m_quality == "dominant" && m_extension == "7") {
1004 57 : m_quality = u"augmented";
1005 57 : if (!syntaxOnly) {
1006 56 : m_xmlKind = u"augmented-seventh";
1007 56 : m_xmlText = m_extension + tok1;
1008 56 : m_chord -= 7;
1009 56 : m_chord += 8;
1010 : }
1011 57 : tok1L = u"";
1012 : } else {
1013 165 : tok1L = u"#";
1014 165 : tok2L = u"5";
1015 : }
1016 15373 : } else if (m_diminished.contains(tok1)) {
1017 0 : m_quality = u"diminished";
1018 0 : if (!syntaxOnly) {
1019 0 : m_xmlKind = u"diminished";
1020 0 : m_xmlText = m_extension + tok1;
1021 0 : m_chord -= 4;
1022 0 : m_chord += 3;
1023 0 : m_chord -= 7;
1024 0 : m_chord += 6;
1025 : }
1026 0 : tok1L = u"";
1027 15373 : } else if ((m_lower.contains(tok1L) || m_raise.contains(tok1L)) && tok2L.empty()) {
1028 : // trailing alteration - treat as applying to extension (and convert to modifier)
1029 : // this handles C5b, C9#, etc
1030 54 : tok2L = m_extension;
1031 54 : if (!syntaxOnly) {
1032 54 : m_xmlKind = (m_quality == u"dominant") ? u"major" : m_quality;
1033 54 : m_xmlText.remove(m_extension);
1034 54 : if (m_extension == "5") {
1035 54 : m_chord += thirdKey;
1036 : } else {
1037 0 : m_chord -= seventhKey;
1038 : }
1039 : }
1040 54 : if (m_quality == "dominant") {
1041 0 : m_quality = u"major";
1042 : }
1043 54 : m_extension = u"";
1044 54 : if (m_lower.contains(tok1L)) {
1045 54 : tok1L = u"b";
1046 : } else {
1047 0 : tok1L = u"#";
1048 : }
1049 15319 : } else if (m_lower.contains(tok1L)) {
1050 4365 : tok1L = u"b";
1051 10954 : } else if (m_raise.contains(tok1L)) {
1052 5129 : tok1L = u"#";
1053 : }
1054 16299 : String m = tok1L + tok2L + suffix;
1055 16299 : if (!m.empty()) {
1056 16236 : m_modifierList << m;
1057 : }
1058 16299 : if (!tok1.empty()) {
1059 14151 : addToken(tok1, ChordTokenClass::MODIFIER);
1060 : }
1061 16299 : if (!tok2.empty()) {
1062 15081 : addToken(tok2, ChordTokenClass::MODIFIER);
1063 : }
1064 16299 : if (!suffix.empty()) {
1065 17 : addToken(suffix, ChordTokenClass::MODIFIER);
1066 : }
1067 16299 : if (!syntaxOnly) {
1068 : int d;
1069 16281 : if (tok2L.empty()) {
1070 475 : d = 0;
1071 : } else {
1072 15806 : d = tok2L.toInt();
1073 : }
1074 16281 : if (d > 13) {
1075 0 : d = 13;
1076 : }
1077 16281 : String degree;
1078 16281 : bool alter = false;
1079 16281 : if (tok1L == "add") {
1080 1666 : if (d) {
1081 1345 : hdl.push_back(HDegree(d, 0, HDegreeType::ADD));
1082 321 : } else if (!tok2L.empty()) {
1083 : // this was result of addPending
1084 : // alteration; tok1 = alter, tok2 = value
1085 321 : d = tok2.toInt();
1086 321 : if (m_raise.contains(tok1) || tok1 == "M" || m_major.contains(tok1.toLower())) {
1087 240 : if (d == 7) {
1088 55 : m_chord += 11;
1089 55 : tok2L = u"#7";
1090 185 : } else if (m_raise.contains(tok1)) {
1091 185 : hdl.push_back(HDegree(d, 1, HDegreeType::ADD));
1092 : }
1093 81 : } else if (m_lower.contains(tok1)) {
1094 79 : if (d == 7) {
1095 0 : m_chord += 10;
1096 : } else {
1097 79 : hdl.push_back(HDegree(d, -1, HDegreeType::ADD));
1098 : }
1099 2 : } else if (d) {
1100 0 : hdl.push_back(HDegree(d, 0, HDegreeType::ADD));
1101 : }
1102 : }
1103 1666 : degree = u"add" + tok2L;
1104 14615 : } else if (tok1L == u"no") {
1105 349 : degree = u"sub" + tok2L;
1106 349 : if (d) {
1107 348 : hdl.push_back(HDegree(d, 0, HDegreeType::SUBTRACT));
1108 : }
1109 14266 : } else if (tok1L == "sus") {
1110 : // convert chords with sus into suspended "kind"
1111 : // extension then becomes a series of degree adds
1112 3886 : if (tok2L == "4") {
1113 897 : m_xmlKind = u"suspended-fourth";
1114 2989 : } else if (tok2L == "2") {
1115 335 : m_xmlKind = u"suspended-second";
1116 : }
1117 3886 : m_xmlText = tok1L + tok2;
1118 3886 : if (m_extension == "7" || m_extension == "9" || m_extension == "11" || m_extension == "13") {
1119 3077 : m_xmlDegrees << ((m_quality == u"major") ? u"add#7" : u"add7");
1120 : // hack for programs that cannot assemble names well
1121 : // even though the kind is suspended, set text to also include the extension
1122 : // in export, we will set the degree text to null
1123 3077 : m_xmlText = m_extension + m_xmlText;
1124 3077 : degree = u"";
1125 809 : } else if (!m_extension.empty()) {
1126 14 : degree = u"add" + m_extension;
1127 : }
1128 3886 : if (m_extension == u"13") {
1129 957 : m_xmlDegrees << u"add9";
1130 957 : m_xmlDegrees << u"add11";
1131 957 : m_xmlDegrees << u"add13";
1132 2929 : } else if (m_extension == u"11") {
1133 2 : m_xmlDegrees << u"add9";
1134 2 : m_xmlDegrees << u"add11";
1135 2927 : } else if (m_extension == u"9") {
1136 588 : m_xmlDegrees << u"add9";
1137 : }
1138 3886 : susChord = true;
1139 3886 : m_chord -= thirdKey;
1140 3886 : if (d) {
1141 1234 : m_chord += key[d];
1142 : }
1143 10380 : } else if (tok1L == u"major") {
1144 131 : if (m_xmlKind.startsWith(u"minor")) {
1145 126 : m_xmlKind = u"major-minor";
1146 126 : if (m_extension == u"9" || tok2L == u"9") {
1147 65 : m_xmlDegrees << u"add9";
1148 : }
1149 126 : if (m_extension == "11" || tok2L == u"11") {
1150 0 : m_xmlDegrees << u"add9";
1151 0 : m_xmlDegrees << u"add11";
1152 : }
1153 126 : if (m_extension == u"13" || tok2L == u"13") {
1154 2 : m_xmlDegrees << u"add9";
1155 2 : m_xmlDegrees << u"add11";
1156 2 : m_xmlDegrees << u"add13";
1157 : }
1158 126 : m_xmlText += tok1 + tok2;
1159 126 : correctXmlText(u"7");
1160 : } else {
1161 5 : tok1L = u"add";
1162 : }
1163 131 : m_chord -= 10;
1164 131 : m_chord += 11;
1165 131 : if (d && d != 7) {
1166 9 : hdl.push_back(HDegree(d, 0, HDegreeType::ADD));
1167 : }
1168 10249 : } else if (tok1L == u"alt") {
1169 55 : m_xmlDegrees << u"altb5";
1170 55 : m_xmlDegrees << u"add#5";
1171 55 : m_xmlDegrees << u"addb9";
1172 55 : m_xmlDegrees << u"add#9";
1173 55 : m_chord -= 7;
1174 55 : m_chord += 6;
1175 55 : m_chord += 8;
1176 55 : m_chord += 1;
1177 55 : m_chord += 3;
1178 10194 : } else if (tok1L == u"blues") {
1179 : // this isn't really well-defined, but it might as well mean something
1180 110 : if (m_extension == u"11" || m_extension == u"13") {
1181 0 : m_xmlDegrees << u"alt#9";
1182 : } else {
1183 110 : m_xmlDegrees << u"add#9";
1184 : }
1185 110 : m_chord += 3;
1186 10084 : } else if (tok1L == u"lyd") {
1187 110 : if (m_extension == u"13") {
1188 0 : m_xmlDegrees << u"alt#11";
1189 : } else {
1190 110 : m_xmlDegrees << u"add#11";
1191 : }
1192 110 : m_chord += 6;
1193 9974 : } else if (tok1L == "phryg") {
1194 55 : if (!m_xmlKind.startsWith(u"minor")) {
1195 55 : m_xmlKind = u"minor-seventh";
1196 : }
1197 55 : if (m_extension == "11" || m_extension == "13") {
1198 0 : m_xmlDegrees << u"altb9";
1199 : } else {
1200 55 : m_xmlDegrees << u"addb9";
1201 : }
1202 55 : m_xmlText += tok1;
1203 55 : m_chord = HChord(u"C Db Eb G Bb");
1204 9919 : } else if (tok1L == "tristan") {
1205 56 : m_xmlKind = u"Tristan";
1206 56 : m_xmlText = tok1;
1207 56 : m_chord = HChord(u"C F# A# D#");
1208 9863 : } else if (addPending) {
1209 0 : degree = u"add" + tok1L + tok2L;
1210 0 : if (m_raise.contains(tok1L)) {
1211 0 : hdl.push_back(HDegree(d, 1, HDegreeType::ADD));
1212 0 : } else if (m_lower.contains(tok1L)) {
1213 0 : hdl.push_back(HDegree(d, -1, HDegreeType::ADD));
1214 : } else {
1215 0 : hdl.push_back(HDegree(d, 0, HDegreeType::ADD));
1216 : }
1217 9863 : } else if (susPending) {
1218 0 : degree = u"sus" + tok1L + tok2L;
1219 0 : if (m_raise.contains(tok1L)) {
1220 0 : hdl.push_back(HDegree(d, 1, HDegreeType::ADD));
1221 0 : } else if (m_lower.contains(tok1L)) {
1222 0 : hdl.push_back(HDegree(d, -1, HDegreeType::ADD));
1223 : } else {
1224 0 : hdl.push_back(HDegree(d, 0, HDegreeType::ADD));
1225 : }
1226 9863 : } else if (tok1L.empty() && !tok2L.empty()) {
1227 56 : degree = u"add" + tok2L;
1228 56 : hdl.push_back(HDegree(d, 0, HDegreeType::ADD));
1229 9807 : } else if (m_lower.contains(tok1L)) {
1230 4417 : tok1L = u"b";
1231 4417 : alter = true;
1232 5390 : } else if (m_raise.contains(tok1L)) {
1233 5293 : tok1L = u"#";
1234 5293 : alter = true;
1235 97 : } else if (tok1L.empty()) {
1236 : // token was already handled fully
1237 : } else {
1238 35 : m_understandable = false;
1239 35 : if (s.startsWith(tok1)) {
1240 : // unrecognized token right from very beginning
1241 18 : m_xmlKind = u"other";
1242 18 : m_xmlText = tok1;
1243 : }
1244 : }
1245 16281 : if (alter) {
1246 9710 : if (tok2L == "4" && m_xmlKind == "suspended-fourth") {
1247 0 : degree = u"alt";
1248 9710 : } else if (tok2L == "5") {
1249 2818 : degree = u"alt";
1250 6892 : } else if (tok2L == "9" && (m_extension == "11" || m_extension == "13")) {
1251 932 : degree = u"alt";
1252 5960 : } else if (tok2L == "11" && m_extension == "13") {
1253 869 : degree = u"alt";
1254 : } else {
1255 5091 : degree = u"add";
1256 : }
1257 9710 : degree += tok1L + tok2L;
1258 9710 : if (m_chord.contains(key[d]) && !(susChord && (d == 11))) {
1259 4290 : hdl.push_back(HDegree(d, 0, HDegreeType::SUBTRACT));
1260 : }
1261 9710 : if (tok1L == "#") {
1262 5293 : if (d == 7) {
1263 86 : m_chord += 11;
1264 : } else {
1265 5207 : hdl.push_back(HDegree(d, 1, HDegreeType::ADD));
1266 : }
1267 4417 : } else if (tok1L == "b") {
1268 4417 : if (d == 7) {
1269 0 : m_chord += 10;
1270 : } else {
1271 4417 : hdl.push_back(HDegree(d, -1, HDegreeType::ADD));
1272 : }
1273 : }
1274 : }
1275 16281 : if (!degree.empty()) {
1276 11795 : m_xmlDegrees << degree;
1277 : }
1278 16281 : }
1279 : // eat trailing parens and commas
1280 18118 : while (i < len && trailing.contains(s.at(i))) {
1281 1819 : addToken(String(s.at(i++)), ChordTokenClass::MODIFIER);
1282 : }
1283 16299 : addPending = false;
1284 16299 : susPending = false;
1285 16299 : }
1286 16837 : if (!syntaxOnly) {
1287 16758 : m_chord.add(hdl);
1288 : // fix "add" / "alt" conflicts
1289 : // so add9,altb9 -> addb9
1290 16758 : StringList altList = m_xmlDegrees.filter(u"alt");
1291 21432 : for (const String& d : altList) {
1292 4674 : String unalt(d);
1293 4674 : unalt.replace(std::regex("alt[b#]"), u"add");
1294 4674 : if (m_xmlDegrees.removeAll(unalt)) {
1295 660 : String alt(d);
1296 660 : alt.replace(u"alt", u"add");
1297 660 : size_t i1 = m_xmlDegrees.indexOf(d);
1298 660 : m_xmlDegrees.replace(i1, alt);
1299 660 : }
1300 4674 : }
1301 16758 : }
1302 :
1303 : // construct handle
1304 16837 : if (!m_modifierList.empty()) {
1305 11526 : StringList sortedModifiers = m_modifierList;
1306 11526 : std::sort(sortedModifiers.begin(), sortedModifiers.end());
1307 11526 : m_modifiers = u"<" + sortedModifiers.join(u"><") + u">";
1308 11526 : }
1309 16837 : m_handle = u"<" + m_quality + u"><" + m_extension + u">" + m_modifiers;
1310 :
1311 : // force <minor><7><b5> to export as half-diminished
1312 16837 : if (!syntaxOnly && m_handle == u"<minor><7><b5>") {
1313 254 : m_xmlKind = u"half-diminished";
1314 254 : m_xmlText = s;
1315 254 : m_xmlDegrees.clear();
1316 : }
1317 16837 : if (MScore::debugMode) {
1318 135 : LOGD("parse: source = <%s>, handle = %s", muPrintable(s), muPrintable(m_handle));
1319 135 : if (!syntaxOnly) {
1320 60 : LOGD("parse: HChord = <%s> (%d)", muPrintable(m_chord.voicing()), m_chord.getKeys());
1321 60 : LOGD("parse: xmlKind = <%s>, text = <%s>", muPrintable(m_xmlKind), muPrintable(m_xmlText));
1322 60 : LOGD("parse: xmlSymbols = %s, xmlParens = %s", muPrintable(m_xmlSymbols), muPrintable(m_xmlParens));
1323 60 : LOGD("parse: xmlDegrees = <%s>", muPrintable(m_xmlDegrees.join(u",")));
1324 : }
1325 : }
1326 16837 : return m_parseable;
1327 16837 : }
1328 :
1329 : //---------------------------------------------------------
1330 : // fromXml
1331 : //---------------------------------------------------------
1332 :
1333 79 : String ParsedChord::fromXml(const String& rawKind, const String& rawKindText, const String& useSymbols, const String& useParens,
1334 : const std::vector<HDegree>& dl, const ChordList* cl)
1335 : {
1336 79 : String kind = rawKind;
1337 79 : String kindText = rawKindText;
1338 79 : bool syms = (useSymbols == "yes");
1339 79 : bool parens = (useParens == "yes");
1340 79 : bool implied = false;
1341 79 : bool extend = false;
1342 79 : int extension = 0;
1343 79 : m_parseable = true;
1344 79 : m_understandable = true;
1345 :
1346 : // get quality info from kind
1347 79 : if (kind == "major-minor") {
1348 1 : m_quality = u"minor";
1349 1 : m_modifierList << u"major7";
1350 1 : extend = true;
1351 78 : } else if (kind.contains(u"major")) {
1352 36 : m_quality = u"major";
1353 36 : if (kind == "major" || kind == "major-sixth") {
1354 29 : implied = true;
1355 : }
1356 42 : } else if (kind.contains(u"minor")) {
1357 17 : m_quality = u"minor";
1358 25 : } else if (kind.contains(u"dominant")) {
1359 6 : m_quality = u"dominant";
1360 6 : implied = true;
1361 6 : extension = 7;
1362 19 : } else if (kind == "augmented-seventh") {
1363 1 : m_quality = u"augmented";
1364 1 : extension = 7;
1365 1 : extend = true;
1366 18 : } else if (kind == "augmented") {
1367 1 : m_quality = u"augmented";
1368 17 : } else if (kind == "half-diminished") {
1369 4 : m_quality = u"half-diminished";
1370 4 : if (syms) {
1371 3 : extension = 7;
1372 3 : extend = true;
1373 : }
1374 13 : } else if (kind == "diminished-seventh") {
1375 1 : m_quality = u"diminished";
1376 1 : extension = 7;
1377 12 : } else if (kind == "diminished") {
1378 1 : m_quality = u"diminished";
1379 11 : } else if (kind == "suspended-fourth") {
1380 4 : m_quality = u"major";
1381 4 : implied = true;
1382 4 : m_modifierList << u"sus4";
1383 7 : } else if (kind == "suspended-second") {
1384 3 : m_quality = u"major";
1385 3 : implied = true;
1386 3 : m_modifierList << u"sus2";
1387 4 : } else if (kind == "power") {
1388 2 : m_quality = u"major";
1389 2 : implied = true;
1390 2 : extension = 5;
1391 2 : } else if (kind == "pedal") {
1392 : // Ignore, assume major
1393 0 : m_quality = u"major";
1394 0 : implied = true;
1395 : } else {
1396 2 : m_quality = kind;
1397 : }
1398 :
1399 : // get extension info from kind
1400 79 : if (kind.contains(u"seventh")) {
1401 10 : extension = 7;
1402 69 : } else if (kind.contains(u"ninth")) {
1403 3 : extension = 9;
1404 66 : } else if (kind.contains(u"11th")) {
1405 3 : extension = 11;
1406 63 : } else if (kind.contains(u"13th")) {
1407 3 : extension = 13;
1408 60 : } else if (kind.contains(u"sixth")) {
1409 6 : extension = 6;
1410 : }
1411 :
1412 : // get modifier info from degree list
1413 89 : for (const HDegree& d : dl) {
1414 10 : String mod;
1415 10 : int v = d.value();
1416 10 : switch (d.type()) {
1417 9 : case HDegreeType::ADD:
1418 : case HDegreeType::ALTER:
1419 9 : switch (d.alter()) {
1420 1 : case -1: mod = u"b";
1421 1 : break;
1422 1 : case 1: mod = u"#";
1423 1 : break;
1424 7 : case 0: mod = u"add";
1425 7 : break;
1426 : }
1427 9 : break;
1428 1 : case HDegreeType::SUBTRACT:
1429 1 : mod = u"no";
1430 1 : break;
1431 0 : case HDegreeType::UNDEF:
1432 : default:
1433 0 : break;
1434 : }
1435 10 : mod += String::number(v);
1436 10 : if (mod == "add7" && kind.contains(u"suspended")) {
1437 2 : m_quality = u"dominant";
1438 2 : implied = true;
1439 2 : extension = 7;
1440 2 : extend = true;
1441 2 : mod = u"";
1442 8 : } else if (mod == "add#7" && kind.contains(u"suspended")) {
1443 0 : m_quality = u"major";
1444 0 : implied = false;
1445 0 : extension = 7;
1446 0 : extend = true;
1447 0 : mod = u"";
1448 8 : } else if (mod == "add9" && extend) {
1449 2 : if (extension < 9) {
1450 2 : extension = 9;
1451 : }
1452 2 : mod = u"";
1453 6 : } else if (mod == "add11" && extend) {
1454 0 : if (extension < 11) {
1455 0 : extension = 11;
1456 : }
1457 0 : mod = u"";
1458 6 : } else if (mod == "add13" && extend) {
1459 0 : extension = 13;
1460 0 : mod = u"";
1461 6 : } else if (mod == "add9" && kind.contains(u"sixth")) {
1462 0 : extension = 69;
1463 0 : mod = u"";
1464 : }
1465 10 : if (!mod.empty()) {
1466 6 : m_modifierList << mod;
1467 : }
1468 10 : }
1469 : // convert no3,add[42] into sus[42]
1470 79 : size_t no3 = m_modifierList.indexOf(u"no3");
1471 79 : if (no3 != muse::nidx) {
1472 0 : size_t addn = m_modifierList.indexOf(u"add4");
1473 0 : if (addn == muse::nidx) {
1474 0 : addn = m_modifierList.indexOf(u"add2");
1475 : }
1476 0 : if (addn != muse::nidx) {
1477 0 : String& s = m_modifierList[addn];
1478 0 : s.replace(u"add", u"sus");
1479 0 : m_modifierList.removeAt(no3);
1480 : }
1481 : }
1482 : // convert kind=minor-seventh, degree=altb5 to kind=half-diminished (suppression of degree=altb comes later)
1483 79 : if (kind == "minor-seventh" && m_modifierList.size() == 1 && m_modifierList.front() == "b5") {
1484 0 : kind = u"half-diminished";
1485 : }
1486 : // force parens where necessary)
1487 79 : if (!parens && extension == 0 && !m_modifierList.empty()) {
1488 10 : String firstMod = m_modifierList.front();
1489 10 : if (!firstMod.empty() && (firstMod.startsWith(u'#') || firstMod.startsWith(u'b'))) {
1490 0 : parens = true;
1491 : }
1492 10 : }
1493 :
1494 : // record extension
1495 79 : if (extension) {
1496 35 : m_extension = String::number(extension);
1497 : }
1498 :
1499 : // validate kindText
1500 79 : if (!kindText.empty() && kind != "none" && kind != "other") {
1501 42 : ParsedChord validate;
1502 42 : validate.parse(kindText, cl, false);
1503 : // kindText should parse to produce same kind, no degrees
1504 42 : if (validate.m_xmlKind != kind || !validate.m_xmlDegrees.empty()) {
1505 0 : kindText = u"";
1506 : }
1507 42 : }
1508 :
1509 : // construct name & handle
1510 79 : m_name = u"";
1511 79 : if (!kindText.empty()) {
1512 43 : if (!m_extension.empty() && kind.contains(u"suspended")) {
1513 0 : m_name += m_extension;
1514 : }
1515 43 : m_name += kindText;
1516 43 : if (extension == 69) {
1517 0 : m_name += u"9";
1518 : }
1519 36 : } else if (implied) {
1520 30 : m_name = m_extension;
1521 : } else {
1522 6 : if (m_quality == "major") {
1523 0 : m_name = syms ? u"^" : u"maj";
1524 6 : } else if (m_quality == "minor") {
1525 2 : m_name = syms ? u"-" : u"m";
1526 4 : } else if (m_quality == "augmented") {
1527 1 : m_name = syms ? u"+" : u"aug";
1528 3 : } else if (m_quality == "diminished") {
1529 0 : m_name = syms ? u"o" : u"dim";
1530 3 : } else if (m_quality == "half-diminished") {
1531 3 : m_name = syms ? u"0" : u"m7b5";
1532 : } else {
1533 0 : m_name = m_quality;
1534 : }
1535 6 : m_name += m_extension;
1536 : }
1537 79 : if (parens) {
1538 0 : m_name += u"(";
1539 : }
1540 93 : for (String mod : m_modifierList) {
1541 14 : mod.replace(u"major", u"maj");
1542 14 : if (!kindText.empty() && kind.contains(u"suspended") && mod.startsWith(u"sus")) {
1543 2 : continue;
1544 12 : } else if (!kindText.empty() && kind == "major-minor" && mod.startsWith(u"maj")) {
1545 1 : continue;
1546 : }
1547 11 : m_name += mod;
1548 14 : }
1549 79 : if (parens) {
1550 0 : m_name += u")";
1551 : }
1552 :
1553 : // parse name to construct handle & tokenList
1554 79 : parse(m_name, cl, true);
1555 :
1556 : // record original MusicXML
1557 79 : m_xmlKind = kind;
1558 79 : m_xmlText = kindText;
1559 79 : m_xmlSymbols = useSymbols;
1560 79 : m_xmlParens = useParens;
1561 89 : for (const HDegree& d : dl) {
1562 10 : if (kind == "half-diminished" && d.type() == HDegreeType::ALTER && d.alter() == -1 && d.value() == 5) {
1563 0 : continue;
1564 : }
1565 10 : m_xmlDegrees << d.text();
1566 : }
1567 :
1568 158 : return m_name;
1569 79 : }
1570 :
1571 : //---------------------------------------------------------
1572 : // position
1573 : //---------------------------------------------------------
1574 :
1575 436 : double ChordList::position(const StringList& names, bool stackModifiers, bool superScript, ChordTokenClass ctc, size_t modifierIdx,
1576 : size_t nmodifiers) const
1577 : {
1578 436 : String name = names.empty() ? u"" : names.front();
1579 436 : switch (ctc) {
1580 170 : case ChordTokenClass::EXTENSION:
1581 170 : return m_eadjust;
1582 168 : case ChordTokenClass::MODIFIER: {
1583 168 : double yAdj = 0.0;
1584 168 : if (superScript) {
1585 : // Fake superscript by aligning with capheight
1586 0 : yAdj += 0.15;
1587 168 : } else if (stackModifiers && nmodifiers > 1) {
1588 : static constexpr double LINE_SPACING = 0.4; // Space between modifiers in units of modiferHeight
1589 72 : const double modifierHeight = m_mmag * m_stackedmmag; // Modifier height in units of root capheight
1590 72 : const double stackHeight = (nmodifiers * modifierHeight) + ((nmodifiers - 1) * modifierHeight * LINE_SPACING); // Height of total modifier stack (bottom baseline to top capheight)
1591 72 : const double base = stackHeight / 2; // Baseline of bottom modifier in the stack
1592 72 : yAdj += base - modifierIdx * modifierHeight * (1 + LINE_SPACING);
1593 : }
1594 :
1595 168 : if (!name.isEmpty()) {
1596 168 : yAdj += m_madjust;
1597 : }
1598 168 : return yAdj;
1599 : }
1600 98 : default:
1601 98 : return 0.0;
1602 : }
1603 436 : }
1604 :
1605 : //---------------------------------------------------------
1606 : // renderList
1607 : //---------------------------------------------------------
1608 :
1609 11134 : const std::vector<RenderActionPtr >& ParsedChord::renderList(const ChordList* cl, bool stacked)
1610 : {
1611 : // generate anew on each call,
1612 : // in case chord list has changed since last time
1613 11134 : if (!m_renderList.empty()) {
1614 5451 : m_renderList.clear();
1615 : }
1616 :
1617 11134 : if (m_tokenList.empty()) {
1618 232 : return m_renderList;
1619 : }
1620 :
1621 10902 : size_t modIdx = 0;
1622 10902 : const double modListSize = m_modifierList.size();
1623 10902 : const size_t finalModIdx = modListSize - 1;
1624 10902 : const bool stackModifiersEnabled = m_modifierList.size() > 1 && stacked;
1625 10902 : const bool superScriptEnabled = !stackModifiersEnabled && cl->chordPreset() == ChordStylePreset::JAZZ;
1626 :
1627 : // Special stack layout if there is a single 'add' or 'sus' with multiple degrees
1628 : // Standard stack layout if there is only one
1629 10902 : static const std::wregex SUS_ADD_REGEX = std::wregex(L"sus|add");
1630 10902 : bool stackSusOrAdd = false;
1631 24358 : for (const String& mod : m_modifierList) {
1632 13462 : if (!(stackModifiersEnabled || superScriptEnabled) || !mod.contains(SUS_ADD_REGEX)) {
1633 12574 : continue;
1634 : }
1635 888 : if (!stackSusOrAdd) {
1636 882 : stackSusOrAdd = true;
1637 : } else {
1638 6 : stackSusOrAdd = false;
1639 6 : break;
1640 : }
1641 : }
1642 :
1643 10902 : bool adjust = cl ? cl->autoAdjust() : false;
1644 10902 : bool firstModifierToken = true;
1645 10902 : bool closingParenPending = false;
1646 52850 : for (auto tokIt = m_tokenList.begin(); tokIt != m_tokenList.end(); tokIt++) {
1647 41948 : const ChordToken& tok = *tokIt;
1648 41948 : const String n = tok.names.front();
1649 83878 : if ((n == u"/" || n == u"," || n == u"\\" || n == u" ")
1650 83878 : && (stackSusOrAdd || (stackModifiersEnabled && tok.tokenClass == ChordTokenClass::MODIFIER))) {
1651 9 : continue;
1652 : }
1653 :
1654 41939 : const bool tokIsSusOrAdd = std::regex_match(n.toStdWString(), SUS_ADD_REGEX);
1655 41939 : const bool stackModifier = stackModifiersEnabled && !(stackSusOrAdd && tokIsSusOrAdd);
1656 41939 : const bool superScriptModifier = superScriptEnabled && !(stackSusOrAdd && tokIsSusOrAdd);
1657 41939 : String curMod = m_modifierList.size() > 1 ? m_modifierList.at(modIdx) : u"";
1658 41939 : if (stackSusOrAdd && stackModifier && modIdx == 0) {
1659 2595 : curMod.remove(SUS_ADD_REGEX);
1660 : }
1661 41939 : const bool modifierEnd = curMod.endsWith(n) && modIdx != finalModIdx;
1662 :
1663 41939 : std::vector<RenderActionPtr > rl;
1664 41939 : std::vector<ChordToken> definedTokens;
1665 41939 : bool found = false;
1666 : // potential definitions for token
1667 41939 : if (cl) {
1668 85522 : for (const ChordToken& ct : cl->chordTokenList) {
1669 88520 : for (const String& ctn : ct.names) {
1670 44937 : if (ctn == n) {
1671 1232 : definedTokens.push_back(ct);
1672 : }
1673 : }
1674 : }
1675 : }
1676 : // find matching class, fallback on ChordTokenClass::ALL
1677 41939 : ChordTokenClass ctc = ChordTokenClass::ALL;
1678 42282 : for (const ChordToken& matchingTok : definedTokens) {
1679 922 : if (tok.tokenClass == matchingTok.tokenClass) {
1680 579 : rl = matchingTok.renderList;
1681 579 : ctc = tok.tokenClass;
1682 579 : found = true;
1683 579 : break;
1684 343 : } else if (matchingTok.tokenClass == ChordTokenClass::ALL) {
1685 10 : rl = matchingTok.renderList;
1686 10 : found = true;
1687 : }
1688 : }
1689 :
1690 : // build render list
1691 : // check for adjustments
1692 41939 : double yAdjust = adjust ? cl->position(tok.names, stackModifier, superScriptModifier, ctc, finalModIdx - modIdx, modListSize) : 0.0;
1693 :
1694 : // Modifier behaviour
1695 41939 : if (tok.tokenClass == ChordTokenClass::MODIFIER) {
1696 : // Stop adjusting when first non-adjusted modifier found
1697 29627 : if (RealIsNull(yAdjust) && !(stackSusOrAdd && tokIsSusOrAdd)) {
1698 28679 : adjust = false;
1699 : }
1700 :
1701 : // This is the first modifier. Discount subsequent items from horizontal alignment
1702 29627 : if (cl->excludeModsHAlign() && modIdx == 0) {
1703 0 : m_renderList.emplace_back(new RenderActionStopHAlign());
1704 : }
1705 :
1706 : // Jazz superscript
1707 29627 : if (superScriptModifier) {
1708 : // Set scale
1709 0 : m_renderList.emplace_back(new RenderActionScale(cl->stackedModifierMag()));
1710 : // Move to x-height
1711 0 : m_renderList.emplace_back(new RenderActionPush());
1712 0 : m_renderList.emplace_back(new RenderActionMoveXHeight(true));
1713 : }
1714 :
1715 : // Stacked modifiers
1716 29627 : if (stackModifier) {
1717 : // Pad modifier stack before
1718 8173 : if (firstModifierToken) {
1719 1769 : m_renderList.emplace_back(new RenderActionMove(0.15, 0));
1720 1769 : firstModifierToken = false;
1721 : }
1722 :
1723 : // Check if the next token is a closing paren
1724 : // Delay starting a new line until after the paren
1725 : // (parens are excluded from the modifier string)
1726 8173 : const auto& nextTokIt = std::next(tokIt);
1727 8173 : if (nextTokIt != m_tokenList.end() && modifierEnd) {
1728 2310 : const ChordToken& nextTok = *nextTokIt;
1729 2310 : if (nextTok.names.front() == ")") {
1730 3 : closingParenPending = true;
1731 : }
1732 : }
1733 :
1734 21001 : auto startsWithAcc = [](const String& s) -> bool {
1735 21001 : return s.startsWith(u"b") || s.startsWith(u"#");
1736 : };
1737 16346 : auto startsWithSusOrAdd = [](const String& s) -> bool {
1738 16346 : return s.startsWith(u"sus") || s.startsWith(u"add");
1739 : };
1740 : // Align vertically stacked modifier's x position by pushing it at the start of every modifier and popping at the end
1741 : // Make sure degrees are aligned when there are accidentals
1742 : // -- nextMod now searches to the end of modifierList to support stacks of 3+ modifiers
1743 16346 : auto it = std::find_if(
1744 8173 : m_modifierList.begin() + (modIdx + 1),
1745 : m_modifierList.end(),
1746 4655 : [&startsWithAcc](const String& s) { return startsWithAcc(s); }
1747 : );
1748 8173 : const String& nextMod = (it != m_modifierList.end()) ? *it : u"";
1749 8173 : bool nextModStartsWithAcc = (it != m_modifierList.end());
1750 8173 : bool curModStartsWithAcc = startsWithAcc(curMod);
1751 8173 : bool nIsAcc = startsWithAcc(n);
1752 8173 : bool curModStartsWithSusOrAdd = startsWithSusOrAdd(curMod);
1753 8173 : bool nIsSusOrAdd = startsWithSusOrAdd(n);
1754 8173 : bool nIsParen = (n == u"(") || (n == u")");
1755 :
1756 8173 : if (curModStartsWithSusOrAdd) { // Align sus or add left
1757 30 : if (nIsSusOrAdd) {
1758 12 : m_renderList.emplace_back(new RenderActionPush());
1759 : }
1760 8143 : } else if (nIsAcc && nextModStartsWithAcc) { // current line has accidental, next line has accidental -> push before accidental
1761 2282 : m_renderList.emplace_back(new RenderActionPush());
1762 5861 : } else if (!curModStartsWithAcc && !nextModStartsWithAcc) { // current line has no accidental, next line has no accidental -> push before degree
1763 65 : m_renderList.emplace_back(new RenderActionPush());
1764 5796 : } else if (curModStartsWithAcc && !nextModStartsWithAcc && !nIsAcc) { // current line has accidental, next line has no accidental -> push before degree
1765 1754 : m_renderList.emplace_back(new RenderActionPush());
1766 4042 : } else if (!curModStartsWithAcc && nextModStartsWithAcc && !nIsParen) { // current line has no accidental, next line has accidental -> push before degree
1767 9 : m_renderList.emplace_back(new RenderActionPush()); // and move by next line's accidental's width to align
1768 9 : String nextAcc = nextMod;
1769 9 : static const std::wregex DEGREE_REGEX = std::wregex(L"[0-9]+");
1770 9 : nextAcc.replace(DEGREE_REGEX, u"");
1771 : // Apply scaling and use renderList to match the behavior of standard accidental rendering
1772 9 : m_renderList.emplace_back(new RenderActionScale(cl->stackedModifierMag()));
1773 9 : const ChordToken accTok = cl->token(nextAcc, ChordTokenClass::MODIFIER);
1774 9 : if (accTok.isValid()) {
1775 0 : for (const RenderActionPtr& a : accTok.renderList) {
1776 0 : if (a->actionType() == RenderAction::RenderActionType::SET) {
1777 0 : m_renderList.emplace_back(new RenderActionMoveTextWidth(u"s" + nextAcc));
1778 : } else {
1779 0 : m_renderList.emplace_back(a);
1780 : }
1781 : }
1782 : }
1783 9 : m_renderList.emplace_back(new RenderActionScale(1 / cl->stackedModifierMag()));
1784 9 : }
1785 :
1786 : // Set scale
1787 8173 : m_renderList.emplace_back(new RenderActionScale(cl->stackedModifierMag()));
1788 : // Move to x-height
1789 8173 : m_renderList.emplace_back(new RenderActionPush());
1790 8173 : m_renderList.emplace_back(new RenderActionMoveXHeight(true));
1791 8173 : }
1792 : }
1793 :
1794 41939 : if (!RealIsNull(yAdjust)) {
1795 72 : m_renderList.emplace_back(new RenderActionMove(0.0, yAdjust));
1796 : }
1797 41939 : if (found) {
1798 589 : m_renderList.insert(m_renderList.end(), rl.begin(), rl.end());
1799 : } else {
1800 : // no definition for token, so render as literal
1801 41350 : m_renderList.emplace_back(new RenderActionSet(tok.names.front()));
1802 : }
1803 : // Reset adjust
1804 41939 : if (!RealIsNull(yAdjust)) {
1805 72 : m_renderList.emplace_back(new RenderActionMove(0.0, -yAdjust));
1806 : }
1807 :
1808 41939 : if (tok.tokenClass == ChordTokenClass::MODIFIER && !n.empty() && superScriptModifier) {
1809 0 : m_renderList.emplace_back(new RenderActionPopY());
1810 0 : m_renderList.emplace_back(new RenderActionScale(1 / cl->stackedModifierMag()));
1811 : }
1812 :
1813 : // Stacked modifiers
1814 41939 : if (tok.tokenClass == ChordTokenClass::MODIFIER && !n.empty() && stackModifier) {
1815 : // Reset move to x-height
1816 8173 : m_renderList.emplace_back(new RenderActionPopY());
1817 : // Reset scale
1818 8173 : m_renderList.emplace_back(new RenderActionScale(1 / cl->stackedModifierMag()));
1819 8173 : if (modifierEnd != closingParenPending) {
1820 2310 : modIdx++;
1821 2310 : closingParenPending = false;
1822 :
1823 : // Restore x position
1824 2310 : m_renderList.emplace_back(new RenderActionPopX());
1825 : }
1826 : }
1827 41948 : }
1828 :
1829 10902 : return m_renderList;
1830 : }
1831 :
1832 : //---------------------------------------------------------
1833 : // addToken
1834 : //---------------------------------------------------------
1835 :
1836 56199 : void ParsedChord::addToken(String s, ChordTokenClass tc)
1837 : {
1838 56199 : if (s.empty()) {
1839 0 : return;
1840 : }
1841 56199 : ChordToken tok;
1842 56199 : tok.names << s;
1843 56199 : tok.tokenClass = tc;
1844 56199 : m_tokenList.push_back(tok);
1845 56199 : }
1846 :
1847 : //---------------------------------------------------------
1848 : // ChordDescription
1849 : // this form is used when reading from file
1850 : // a private id is assigned for id = 0
1851 : //---------------------------------------------------------
1852 :
1853 5280 : ChordDescription::ChordDescription(int i)
1854 : {
1855 5280 : if (!i) {
1856 : // i = --(cl->privateID);
1857 0 : i = --ChordList::privateID;
1858 : }
1859 5280 : id = i;
1860 5280 : generated = false;
1861 5280 : renderListGenerated = false;
1862 5280 : exportOk = true;
1863 5280 : }
1864 :
1865 : //---------------------------------------------------------
1866 : // ChordDescription
1867 : // this form is used when generating from name
1868 : // a private id is always assigned
1869 : //---------------------------------------------------------
1870 :
1871 262 : ChordDescription::ChordDescription(const String& name)
1872 : {
1873 262 : id = --ChordList::privateID;
1874 262 : generated = true;
1875 262 : names.push_back(name);
1876 262 : renderListGenerated = false;
1877 262 : exportOk = false;
1878 262 : }
1879 :
1880 : //---------------------------------------------------------
1881 : // complete
1882 : // generate missing renderList and semantic (Xml) info
1883 : //---------------------------------------------------------
1884 :
1885 9942 : void ChordDescription::complete(ParsedChord* pc, const ChordList* cl)
1886 : {
1887 9942 : ParsedChord tempPc;
1888 9942 : if (!pc) {
1889 : // generate parsed chord for its rendering & semantic (xml) info
1890 9680 : pc = &tempPc;
1891 9680 : String n;
1892 9680 : if (!names.empty()) {
1893 9680 : n = names.front();
1894 : }
1895 9680 : pc->parse(n, cl);
1896 9680 : }
1897 9942 : parsedChords.push_back(*pc);
1898 9942 : if (renderList.empty() || renderListStacked.empty() || renderListGenerated) {
1899 5567 : renderList = pc->renderList(cl, false);
1900 5567 : renderListStacked = pc->renderList(cl, true);
1901 5567 : renderListGenerated = true;
1902 : }
1903 9942 : if (xmlKind.empty()) {
1904 436 : xmlKind = pc->xmlKind();
1905 436 : xmlDegrees = pc->xmlDegrees();
1906 : }
1907 : // these fields are not read from chord description files
1908 : // so get them from the parsed representation in all cases
1909 9942 : xmlText = pc->xmlText();
1910 9942 : xmlSymbols = pc->xmlSymbols();
1911 9942 : xmlParens = pc->xmlParens();
1912 9942 : if (chord.getKeys() == 0) {
1913 349 : chord = HChord(pc->keys());
1914 : }
1915 9942 : m_quality = pc->quality();
1916 9942 : }
1917 :
1918 : //---------------------------------------------------------
1919 : // read
1920 : //---------------------------------------------------------
1921 :
1922 9680 : void ChordDescription::read(XmlReader& e, int mscVersion)
1923 : {
1924 9680 : int ni = 0;
1925 9680 : id = e.attribute("id").toInt();
1926 38898 : while (e.readNextStartElement()) {
1927 29218 : const AsciiStringView tag(e.name());
1928 29218 : if (tag == "name") {
1929 5408 : String n = e.readText();
1930 : // stack names for this file on top of the list
1931 5408 : names.insert(ni++, n);
1932 29218 : } else if (tag == "xml") {
1933 5121 : xmlKind = e.readText();
1934 18689 : } else if (tag == "degree") {
1935 8905 : xmlDegrees.push_back(e.readText());
1936 9784 : } else if (tag == "voicing") {
1937 5208 : chord = HChord(e.readText());
1938 4576 : } else if (tag == "render") {
1939 4576 : readRenderList(e.readText(), renderList, mscVersion);
1940 4576 : renderListGenerated = false;
1941 : } else {
1942 0 : e.unknown();
1943 : }
1944 : }
1945 9680 : }
1946 :
1947 : //---------------------------------------------------------
1948 : // write
1949 : //---------------------------------------------------------
1950 :
1951 0 : void ChordDescription::write(XmlWriter& xml) const
1952 : {
1953 0 : if (generated && !exportOk) {
1954 0 : return;
1955 : }
1956 0 : if (id > 0) {
1957 0 : xml.startElement("chord", { { "id", id } });
1958 : } else {
1959 0 : xml.startElement("chord");
1960 : }
1961 0 : for (const String& s : names) {
1962 0 : xml.tag("name", s);
1963 : }
1964 0 : xml.tag("xml", xmlKind);
1965 0 : xml.tag("voicing", chord.voicing());
1966 0 : for (const String& s : xmlDegrees) {
1967 0 : xml.tag("degree", s);
1968 : }
1969 0 : writeRenderList(xml, renderList, "render");
1970 0 : xml.endElement();
1971 0 : }
1972 :
1973 : //---------------------------------------------------------
1974 : // ChordList
1975 : //---------------------------------------------------------
1976 :
1977 : int ChordList::privateID = -1000;
1978 :
1979 : //---------------------------------------------------------
1980 : // configureAutoAdjust
1981 : //---------------------------------------------------------
1982 :
1983 1474 : void ChordList::configureAutoAdjust(double emag, double eadjust, double mmag, double madjust, double stackedmmag, bool stackModifiers,
1984 : bool excludeModsHAlign, String symbolFont, const ChordStylePreset& preset)
1985 : {
1986 1474 : m_stackModifiers = stackModifiers;
1987 1474 : m_excludeModsHAlign = excludeModsHAlign;
1988 1474 : m_emag = emag;
1989 1474 : m_eadjust = eadjust;
1990 1474 : m_mmag = mmag;
1991 1474 : m_stackedmmag = stackedmmag;
1992 1474 : m_madjust = madjust;
1993 1474 : m_symbolTextFont = symbolFont;
1994 1474 : m_chordPreset = preset;
1995 1474 : }
1996 :
1997 : //---------------------------------------------------------
1998 : // read
1999 : //---------------------------------------------------------
2000 :
2001 1504 : void ChordList::read(XmlReader& e, int mscVersion)
2002 : {
2003 1504 : int fontIdx = static_cast<int>(fonts.size());
2004 1504 : m_autoAdjust = false;
2005 128579 : while (e.readNextStartElement()) {
2006 127075 : const AsciiStringView tag(e.name());
2007 127075 : if (tag == "font") {
2008 9510 : ChordFont f;
2009 9510 : f.family = e.attribute("family", u"default");
2010 9510 : if (f.family == u"MuseJazz") {
2011 0 : f.family = u"MuseJazz Text";
2012 9510 : } else if (f.family == u"ScoreText") {
2013 2742 : f.family = m_symbolTextFont;
2014 2742 : f.musicSymbolText = true;
2015 : }
2016 9510 : f.mag = 1.0;
2017 9510 : f.fontClass = e.attribute("class");
2018 122502 : while (e.readNextStartElement()) {
2019 112992 : if (e.name() == "sym") {
2020 110599 : ChordSymbol cs;
2021 110599 : cs.fontIdx = fontIdx;
2022 110599 : cs.name = e.attribute("name");
2023 110599 : cs.value = e.attribute("value");
2024 110599 : String code = e.attribute("code");
2025 110599 : String symClass = e.attribute("class");
2026 110599 : if (!code.empty()) {
2027 34059 : bool ok = true;
2028 34059 : char32_t val = code.toUInt(&ok, 0);
2029 34059 : if (!ok && mscVersion >= 400 && mscVersion < 440) {
2030 0 : val = code.toUInt(&ok, 16);
2031 : }
2032 34059 : if (!ok) {
2033 8233 : cs.code = 0;
2034 8233 : cs.value = code;
2035 25826 : } else if (Char::requiresSurrogates(val)) {
2036 1070 : cs.code = 0;
2037 1070 : cs.value = String::fromUcs4(val);
2038 : } else {
2039 24756 : cs.code = val;
2040 24756 : cs.value = String(cs.code);
2041 : }
2042 : } else {
2043 76540 : cs.code = 0;
2044 : }
2045 110599 : if (cs.value.empty()) {
2046 4549 : cs.value = cs.name;
2047 : }
2048 110599 : cs.name = symClass + cs.name;
2049 110599 : m_symbols.insert({ cs.name, cs });
2050 110599 : e.readNext();
2051 112992 : } else if (e.name() == "mag") {
2052 2393 : f.mag = e.readDouble();
2053 : } else {
2054 0 : e.unknown();
2055 : }
2056 : }
2057 9510 : if (m_autoAdjust) {
2058 9456 : if (f.fontClass == "extension") {
2059 2364 : f.mag *= m_emag;
2060 7092 : } else if (f.fontClass == "modifier") {
2061 2364 : f.mag *= m_mmag;
2062 : }
2063 : }
2064 9510 : fonts.push_back(f);
2065 9510 : ++fontIdx;
2066 127075 : } else if (tag == "autoAdjust") {
2067 1449 : String nmag = e.attribute("mag");
2068 1449 : if (!nmag.empty()) {
2069 534 : m_nmag = nmag.toDouble();
2070 : }
2071 1449 : String nadjust = e.attribute("adjust");
2072 1449 : if (!nadjust.empty()) {
2073 534 : m_nadjust = nadjust.toDouble();
2074 : }
2075 1449 : m_autoAdjust = e.readBool();
2076 117565 : } else if (tag == "token") {
2077 101122 : ChordToken t;
2078 101122 : t.read(e, mscVersion);
2079 101122 : chordTokenList.push_back(t);
2080 116116 : } else if (tag == "chord") {
2081 9680 : int id = e.intAttribute("id");
2082 : // if no id attribute (id == 0), then assign it a private id
2083 : // user chords that match these ChordDescriptions will be treated as normal recognized chords
2084 : // except that the id will not be written to the score file
2085 9680 : ChordDescription cd = (id && muse::contains(*this, id))
2086 9680 : ? muse::take(*this, id)
2087 9680 : : ChordDescription(id);
2088 :
2089 : // record updated id
2090 9680 : id = cd.id;
2091 : // read rest of description
2092 9680 : cd.read(e, mscVersion);
2093 : // restore updated id
2094 9680 : cd.id = id;
2095 : // throw away previously parsed chords
2096 9680 : cd.parsedChords.clear();
2097 : // generate any missing info (including new parsed chords)
2098 9680 : cd.complete(0, this);
2099 : // add to list
2100 9680 : insert({ id, cd });
2101 14994 : } else if (tag == "renderRoot") {
2102 1475 : readRenderList(e.readText(), renderListRoot, mscVersion);
2103 3839 : } else if (tag == "renderFunction") {
2104 1449 : readRenderList(e.readText(), renderListFunction, mscVersion);
2105 2390 : } else if ((tag == "renderBase" && mscVersion < 460) || tag == "renderBass") {
2106 1475 : readRenderList(e.readText(), renderListBass, mscVersion);
2107 915 : } else if (tag == "renderBassOffset") {
2108 915 : readRenderList(e.readText(), renderListBassOffset, mscVersion);
2109 : } else {
2110 0 : e.unknown();
2111 : }
2112 : }
2113 1504 : }
2114 :
2115 : //---------------------------------------------------------
2116 : // write
2117 : //---------------------------------------------------------
2118 :
2119 0 : void ChordList::write(XmlWriter& xml) const
2120 : {
2121 0 : int fontIdx = 0;
2122 0 : for (const ChordFont& f : fonts) {
2123 0 : xml.startElement("font", { { "id", fontIdx }, { "family", f.family } });
2124 0 : xml.tag("mag", f.mag);
2125 0 : for (const auto& p : m_symbols) {
2126 0 : const ChordSymbol& s = p.second;
2127 0 : if (s.fontIdx == fontIdx) {
2128 0 : if (s.code.isNull()) {
2129 0 : xml.tag("sym", { { "name", s.name }, { "value", s.value } });
2130 : } else {
2131 : // write hex numbers with a "0x" prefix, so they can convert back properly on read
2132 0 : xml.tag("sym", { { "name", s.name }, { "code", u"0x" + String::number(s.code.unicode(), 16) } });
2133 : }
2134 : }
2135 : }
2136 0 : xml.endElement();
2137 0 : ++fontIdx;
2138 : }
2139 0 : if (m_autoAdjust) {
2140 0 : xml.tag("autoAdjust", { { "mag", m_nmag }, { "adjust", m_nadjust } });
2141 : }
2142 0 : for (const ChordToken& t : chordTokenList) {
2143 0 : t.write(xml);
2144 : }
2145 0 : if (!renderListRoot.empty()) {
2146 0 : writeRenderList(xml, renderListRoot, "renderRoot");
2147 : }
2148 0 : if (!renderListFunction.empty()) {
2149 0 : writeRenderList(xml, renderListFunction, "renderFunction");
2150 : }
2151 0 : if (!renderListBass.empty()) {
2152 0 : writeRenderList(xml, renderListBass, "renderBass");
2153 : }
2154 0 : if (!renderListBassOffset.empty()) {
2155 0 : writeRenderList(xml, renderListBassOffset, "renderBassOffset");
2156 : }
2157 0 : for (const auto& p : *this) {
2158 0 : const ChordDescription& cd = p.second;
2159 0 : cd.write(xml);
2160 : }
2161 0 : }
2162 :
2163 : //---------------------------------------------------------
2164 : // read
2165 : // read Chord List, return false on error
2166 : //---------------------------------------------------------
2167 :
2168 1503 : bool ChordList::read(const String& name)
2169 : {
2170 : // LOGD("ChordList::read <%s>", muPrintable(name));
2171 1503 : muse::io::path_t path;
2172 1503 : FileInfo ftest(name);
2173 1503 : if (ftest.isAbsolute()) {
2174 0 : path = name;
2175 : } else {
2176 1503 : path = u":/engraving/styles/" + name;
2177 : }
2178 :
2179 : // default to chords_std.xml
2180 1503 : if (!FileInfo::exists(path)) {
2181 0 : path = u":/engraving/styles/chords_std.xml";
2182 : }
2183 :
2184 1503 : if (name.isEmpty()) {
2185 0 : return false;
2186 : }
2187 1503 : File f(path);
2188 1503 : if (!f.open(IODevice::ReadOnly)) {
2189 0 : LOGE() << "Cannot open chord description: " << f.filePath();
2190 0 : return false;
2191 : }
2192 :
2193 1503 : return read(&f);
2194 1503 : }
2195 :
2196 1503 : bool ChordList::read(IODevice* device)
2197 : {
2198 1503 : XmlReader e(device);
2199 :
2200 1503 : while (e.readNextStartElement()) {
2201 1503 : if (e.name() == "museScore") {
2202 1503 : const String version = e.attribute("version");
2203 1503 : const StringList sl = version.split(u'.');
2204 1503 : const int mscVersion = sl.size() == 2 ? sl[0].toInt() * 100 + sl[1].toInt() : 0;
2205 1503 : read(e, mscVersion);
2206 1503 : return true;
2207 1503 : }
2208 : }
2209 0 : return false;
2210 1503 : }
2211 :
2212 : //---------------------------------------------------------
2213 : // writeChordList
2214 : //---------------------------------------------------------
2215 :
2216 0 : bool ChordList::write(const String& name) const
2217 : {
2218 0 : FileInfo info(name);
2219 :
2220 0 : if (info.suffix().isEmpty()) {
2221 0 : String path = info.filePath();
2222 0 : path += u".xml";
2223 0 : info = FileInfo(path);
2224 0 : }
2225 :
2226 0 : auto outBuf = Buffer::opened(IODevice::WriteOnly);
2227 0 : write(&outBuf);
2228 0 : outBuf.close();
2229 :
2230 0 : Ret ret = File::writeFile(info.filePath(), outBuf.data());
2231 0 : if (!ret) {
2232 0 : LOGE() << "failed to write chord list: " << ret.toString();
2233 0 : return false;
2234 : }
2235 :
2236 0 : return true;
2237 0 : }
2238 :
2239 0 : bool ChordList::write(IODevice* device) const
2240 : {
2241 0 : XmlWriter xml(device);
2242 0 : xml.startDocument();
2243 0 : xml.startElement("museScore", { { "version", Constants::MSC_VERSION_STR } });
2244 :
2245 0 : write(xml);
2246 :
2247 0 : xml.endElement();
2248 :
2249 0 : return true;
2250 0 : }
2251 :
2252 : //---------------------------------------------------------
2253 : // loaded
2254 : //---------------------------------------------------------
2255 :
2256 1813 : bool ChordList::loaded() const
2257 : {
2258 : // track whether a description file has been loaded
2259 : // since chords.xml really doesn't load enough to stand alone,
2260 : // we need a way to track when a "real" chord list has been loaded
2261 : // for lack of anything better, key off renderListRoot
2262 1813 : return !renderListRoot.empty();
2263 : }
2264 :
2265 : //---------------------------------------------------------
2266 : // unload
2267 : //---------------------------------------------------------
2268 :
2269 66 : void ChordList::unload()
2270 : {
2271 66 : clear();
2272 66 : m_symbols.clear();
2273 66 : fonts.clear();
2274 66 : renderListRoot.clear();
2275 66 : renderListBass.clear();
2276 66 : chordTokenList.clear();
2277 66 : m_autoAdjust = false;
2278 66 : }
2279 :
2280 6141 : const ChordDescription* ChordList::description(int id) const
2281 : {
2282 6141 : auto it = this->find(id);
2283 6141 : if (it == this->end()) {
2284 25 : return nullptr;
2285 : }
2286 6116 : return &it->second;
2287 : }
2288 :
2289 14093 : ChordSymbol ChordList::symbol(const String& s) const
2290 : {
2291 14093 : return muse::value(m_symbols, s);
2292 : }
2293 :
2294 747 : ChordToken ChordList::token(const String& s, ChordTokenClass type) const
2295 : {
2296 33319 : for (const ChordToken& tok : chordTokenList) {
2297 32710 : if (tok.tokenClass != type || !tok.names.contains(s)) {
2298 32572 : continue;
2299 : }
2300 :
2301 138 : return tok;
2302 : }
2303 :
2304 609 : return ChordToken();
2305 : }
2306 :
2307 1813 : void ChordList::checkChordList(const MStyle& style)
2308 : {
2309 : // make sure we have a chordlist
2310 1813 : if (!loaded()) {
2311 1472 : double emag = style.styleD(Sid::chordExtensionMag);
2312 1472 : double eadjust = style.styleD(Sid::chordExtensionAdjust);
2313 1472 : double mmag = style.styleD(Sid::chordModifierMag);
2314 1472 : double madjust = style.styleD(Sid::chordModifierAdjust);
2315 1472 : double stackedmmag = style.styleD(Sid::chordStackedModifierMag);
2316 1472 : bool stackModifiers = style.styleB(Sid::verticallyStackModifiers);
2317 1472 : bool excludeModsHAlign = style.styleB(Sid::chordAlignmentExcludeModifiers);
2318 1472 : String symbolFont = style.styleSt(Sid::musicalTextFont);
2319 1472 : ChordStylePreset preset = style.styleV(Sid::chordStyle).value<ChordStylePreset>();
2320 1472 : configureAutoAdjust(emag, eadjust, mmag, madjust, stackedmmag, stackModifiers, excludeModsHAlign, symbolFont, preset);
2321 :
2322 1472 : if (style.value(Sid::chordsXmlFile).toBool()) {
2323 25 : read(u"chords.xml");
2324 : }
2325 :
2326 1472 : read(style.value(Sid::chordDescriptionFile).value<String>());
2327 1472 : }
2328 1813 : }
2329 :
2330 : //---------------------------------------------------------
2331 : // print
2332 : // only for debugging
2333 : //---------------------------------------------------------
2334 :
2335 0 : void RenderAction::print(RenderActionType type, const String& info) const
2336 : {
2337 : static const char* names[] = {
2338 : "SET", "MOVE", "MOVEXHEIGHT", "PUSH", "POP",
2339 : "NOTE", "ACCIDENTAL", "STOPHALIGN", "SCALE", "PAREN"
2340 : };
2341 0 : LOGD("%10s %s", names[int(type)], muPrintable(info));
2342 0 : }
2343 :
2344 0 : void RenderActionMove::print() const
2345 : {
2346 0 : String info = String(u"%1 %2").arg(m_vec.x(), m_vec.y());
2347 0 : RenderAction::print(actionType(), info);
2348 0 : }
2349 :
2350 0 : void RenderActionSet::print() const
2351 : {
2352 0 : String info = String(u"<%1>").arg(m_text);
2353 0 : RenderAction::print(actionType(), info);
2354 0 : }
2355 :
2356 0 : void RenderActionPop::print() const
2357 : {
2358 0 : String info = String(u"pop x: %1 pop y: %2").arg(m_popx, m_popy);
2359 0 : RenderAction::print(actionType(), info);
2360 0 : }
2361 :
2362 0 : void RenderActionScale::print() const
2363 : {
2364 0 : String info = String(u"%1").arg(m_scale);
2365 0 : RenderAction::print(actionType(), info);
2366 0 : }
2367 :
2368 0 : void RenderActionMoveScaled::print() const
2369 : {
2370 0 : String info = String(u"SCALED %1 %2").arg(x(), y());
2371 0 : RenderAction::print(actionType(), info);
2372 0 : }
2373 :
2374 0 : void RenderActionMoveXHeight::print() const
2375 : {
2376 0 : String info = String(u"up: %1").arg(m_up);
2377 0 : RenderAction::print(actionType(), info);
2378 0 : }
2379 :
2380 0 : void RenderActionParen::print() const
2381 : {
2382 0 : String info = String(u"direction: %1").arg(direction() == DirectionH::LEFT ? u"left" : u"right");
2383 0 : RenderAction::print(actionType(), info);
2384 0 : }
2385 : }
|