1 /*-
2 * Copyright (c) 2017-2018 Hans Petter Selasky. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23 * SUCH DAMAGE.
24 */
25
26 #include "midipp_musicxml.h"
27 #include "midipp_checkbox.h"
28 #include "midipp_chords.h"
29 #include "midipp_decode.h"
30
31 #define MXML_MAX_TAGS 8
32
33 static const QString
MppReadStrFilter(const QString & str)34 MppReadStrFilter(const QString &str)
35 {
36 QString retval = str;
37
38 retval.replace(QChar('\n'), QChar(' '));
39 retval.replace(QChar('.'), QChar(' '));
40 retval.replace(QChar('('), QChar('['));
41 retval.replace(QChar(')'), QChar(']'));
42 retval = retval.trimmed();
43 return (retval);
44 }
45
46 static const char *MppGetNoteString[12] = {
47 "C",
48 "Db",
49 "D",
50 "Eb",
51 "E",
52 "F",
53 "Gb",
54 "G",
55 "Ab",
56 "A",
57 "Hb",
58 "H",
59 };
60
61 static int
MppGetNoteNumber(const QString & step,const QString & alter,const QString & octave)62 MppGetNoteNumber(const QString &step, const QString &alter, const QString &octave)
63 {
64 int retval = 0;
65
66 if (step == "C")
67 retval += C0;
68 else if (step == "D")
69 retval += D0;
70 else if (step == "E")
71 retval += E0;
72 else if (step == "F")
73 retval += F0;
74 else if (step == "G")
75 retval += G0;
76 else if (step == "A")
77 retval += A0;
78 else if (step == "H" || step == "B")
79 retval += H0;
80
81 if (alter == "-1")
82 retval -= 1;
83 else if (alter == "1" || alter == "+1")
84 retval += 1;
85
86 retval += octave.toInt() * 12;
87
88 /* range check */
89 if (retval < 0) {
90 retval = (12 + (retval % 12)) % 12;
91 } else if (retval > 127) {
92 retval = 120 + (retval % 12);
93 if (retval > 127)
94 retval -= 12;
95 }
96 return (retval);
97 }
98
99 static const QString
MppReadMusicXML(const QByteArray & data,uint32_t flags,uint32_t ipart,uint32_t nmeasure)100 MppReadMusicXML(const QByteArray &data, uint32_t flags, uint32_t ipart, uint32_t nmeasure)
101 {
102 QXmlStreamReader::TokenType token = QXmlStreamReader::NoToken;
103 QXmlStreamReader xml(data);
104 QString output;
105 QString output_string;
106 QString output_scores;
107 QString title;
108 QString composer;
109 QString lyricist;
110 QString arranger;
111 QString text;
112 QString scores;
113 QString pitch_step;
114 QString pitch_alter;
115 QString pitch_octave;
116 QString note_duration;
117 QString bass_step;
118 QString bass_alter;
119 QString divisions;
120 QString root_step;
121 QString root_alter;
122 QString duration;
123 QString syllabic;
124 QString kind;
125 QString tags[MXML_MAX_TAGS];
126 uint32_t imeasure = 0;
127 uint8_t do_new_line = 0;
128 uint8_t is_chord = 0;
129 size_t si = 0;
130
131 while (!xml.atEnd()) {
132 if (token == QXmlStreamReader::NoToken)
133 token = xml.readNext();
134
135 switch (token) {
136 case QXmlStreamReader::Invalid:
137 goto done;
138 case QXmlStreamReader::StartElement:
139 if (si < MXML_MAX_TAGS)
140 tags[si] = xml.name().toString();
141 si++;
142
143 if (tags[0] == "score-partwise") {
144 if (si == 1) {
145 title = QString();
146 composer = QString();
147 lyricist = QString();
148 arranger = QString();
149 } else if (tags[1] == "work") {
150 if (si == 3 && tags[2] == "work-title") {
151 token = xml.readNext();
152 if (token != QXmlStreamReader::Characters)
153 continue;
154 title = MppReadStrFilter(xml.text().toString());
155 }
156 } else if (tags[1] == "identification") {
157 if (si == 3 && tags[2] == "creator") {
158 QString type = xml.attributes().value("type").toString();
159 if (type == "composer") {
160 token = xml.readNext();
161 if (token != QXmlStreamReader::Characters)
162 continue;
163 composer = MppReadStrFilter(xml.text().toString());
164 } else if (type == "lyricist") {
165 token = xml.readNext();
166 if (token != QXmlStreamReader::Characters)
167 continue;
168 lyricist = MppReadStrFilter(xml.text().toString());
169 } else if (type == "arranger") {
170 token = xml.readNext();
171 if (token != QXmlStreamReader::Characters)
172 continue;
173 arranger = MppReadStrFilter(xml.text().toString());
174 }
175 }
176 } else if (tags[1] == "part") {
177 if (ipart != 0) {
178 /* wrong part number */
179 } else if (si == 2) {
180 divisions = QString();
181 imeasure = 0;
182 } else if (tags[2] == "measure") {
183 if (si == 3) {
184
185 } else if (tags[3] == "attributes" && tags[4] == "divisions") {
186 if (si == 5) {
187 token = xml.readNext();
188 if (token != QXmlStreamReader::Characters)
189 continue;
190 divisions = xml.text().toString().trimmed();
191 }
192 } else if (tags[3] == "print") {
193 if (si == 4) {
194 if (xml.attributes().value("new-page") == "yes") {
195 if (!output.isEmpty())
196 output += "\nJP\n\n";
197 }
198 }
199 } else if (tags[3] == "harmony") {
200 if (si == 4) {
201 root_step = QString();
202 root_alter = QString();
203 kind = QString();
204 bass_step = QString();
205 bass_alter = QString();
206 } else if (si == 6 && tags[4] == "root" && tags[5] == "root-step") {
207 token = xml.readNext();
208 if (token != QXmlStreamReader::Characters)
209 continue;
210 root_step = xml.text().toString().trimmed();
211 } else if (si == 6 && tags[4] == "root" && tags[5] == "root-alter") {
212 token = xml.readNext();
213 if (token != QXmlStreamReader::Characters)
214 continue;
215 root_alter = xml.text().toString().trimmed();
216 } else if (si == 5 && tags[4] == "kind") {
217 kind = xml.attributes().value("text").toString();
218 if (kind.isEmpty()) {
219 token = xml.readNext();
220 if (token != QXmlStreamReader::Characters)
221 continue;
222 /*
223 * Try to translate kind into something which
224 * MidiPlayerPro understands:
225 */
226 kind = xml.text().toString().trimmed();
227 if (kind == "major")
228 kind = "";
229 else if (kind == "minor")
230 kind = "m";
231 else if (kind == "augmented")
232 kind = "+";
233 else if (kind == "diminished")
234 kind = "dim";
235 else if (kind == "dominant")
236 kind = "7";
237 else if (kind == "major-seventh")
238 kind = "M7";
239 else if (kind == "minor-seventh")
240 kind = "m7";
241 else if (kind == "diminished-seventh")
242 kind = "o7";
243 else if (kind == "augmented-seventh")
244 kind = "+7";
245 else if (kind == "half-diminished")
246 kind = QString::fromUtf8("ø");
247 else if (kind == "major-minor")
248 kind = "mM7";
249 else if (kind == "major-sixth")
250 kind = "M6";
251 else if (kind == "minor-sixth")
252 kind = "m6";
253 else if (kind == "dominant-ninth")
254 kind = "dom9";
255 else if (kind == "major-ninth")
256 kind = "M9";
257 else if (kind == "minor-ninth")
258 kind = "m9";
259 else if (kind == "dominant-11th")
260 kind = "dom11";
261 else if (kind == "major-11th")
262 kind = "M11";
263 else if (kind == "minor-11th")
264 kind = "m11";
265 else if (kind == "dominant-13th")
266 kind = "dom13";
267 else if (kind == "major-13th")
268 kind = "M13";
269 else if (kind == "minor-13th")
270 kind = "m13";
271 else if (kind == "suspended-second")
272 kind = "sus2";
273 else if (kind == "suspended-fourth")
274 kind = "sus4";
275 else if (kind == "power")
276 kind = "5";
277 else
278 kind = ""; /* major */
279 }
280 } else if (si == 6 && tags[4] == "bass" && tags[5] == "bass-step") {
281 token = xml.readNext();
282 if (token != QXmlStreamReader::Characters)
283 continue;
284 bass_step = xml.text().toString().trimmed();
285 } else if (si == 6 && tags[4] == "bass" && tags[5] == "bass-alter") {
286 token = xml.readNext();
287 if (token != QXmlStreamReader::Characters)
288 continue;
289 bass_alter = xml.text().toString().trimmed();
290 }
291
292 } else if (tags[3] == "note") {
293 if (si == 4) {
294 pitch_step = QString();
295 pitch_alter = QString();
296 pitch_octave = QString();
297 note_duration = QString();
298 is_chord = 0;
299 text = QString();
300 syllabic = QString();
301 duration = QString();
302 } else if (tags[4] == "chord") {
303 if (si == 5) {
304 is_chord = 1;
305 }
306 } else if (tags[4] == "pitch") {
307 if (si == 5) {
308
309 } else if (tags[5] == "step") {
310 if (si == 6) {
311 token = xml.readNext();
312 if (token != QXmlStreamReader::Characters)
313 continue;
314 pitch_step = xml.text().toString().trimmed();
315 }
316 } else if (tags[5] == "alter") {
317 if (si == 6) {
318 token = xml.readNext();
319 if (token != QXmlStreamReader::Characters)
320 continue;
321 pitch_alter = xml.text().toString().trimmed();
322 }
323 } else if (tags[5] == "octave") {
324 if (si == 6) {
325 token = xml.readNext();
326 if (token != QXmlStreamReader::Characters)
327 continue;
328 pitch_octave = xml.text().toString().trimmed();
329 }
330 }
331 } else if (tags[4] == "duration") {
332 if (si == 5) {
333 token = xml.readNext();
334 if (token != QXmlStreamReader::Characters)
335 continue;
336 duration = xml.text().toString().trimmed();
337 }
338 } else if (tags[4] == "lyric") {
339 if (si == 5) {
340
341 } else if (tags[5] == "syllabic") {
342 if (si == 6) {
343 token = xml.readNext();
344 if (token != QXmlStreamReader::Characters)
345 continue;
346 syllabic = xml.text().toString().trimmed();
347 }
348 } else if (tags[5] == "text") {
349 if (si == 6) {
350 token = xml.readNext();
351 if (token != QXmlStreamReader::Characters)
352 continue;
353 text = MppReadStrFilter(xml.text().toString());
354 }
355 }
356 }
357 }
358 }
359 }
360 }
361 break;
362 case QXmlStreamReader::EndElement:
363 if (tags[0] == "score-partwise") {
364 if (si == 1) {
365 QString creator = composer;
366 if (!lyricist.isEmpty()) {
367 creator += " // ";
368 creator += lyricist;
369 }
370 if (!arranger.isEmpty()) {
371 creator += " || ";
372 creator += arranger;
373 }
374 output = QString("S\"(") + title + QString(")") +
375 creator + QString("\"\n\nL0:\n") + output;
376 } else if (tags[1] == "part") {
377 if (si == 2) {
378 /* end of part */
379 imeasure = 0;
380 ipart--;
381
382 if (output_string.isEmpty() == 0 ||
383 output_scores.isEmpty() == 0) {
384 output += "\nS\"";
385 output += output_string;
386 output += "\"\n\n";
387 output += output_scores;
388 output_string = QString();
389 output_scores = QString();
390 }
391 } else if (ipart != 0) {
392 /* wrong part number */
393 } else if (tags[2] == "measure") {
394 if (si == 3) {
395 /* end of measure */
396 if (do_new_line != 0) {
397 do_new_line = 0;
398 output_scores += "\n";
399 }
400 imeasure++;
401
402 if ((imeasure % nmeasure) == (nmeasure - 1)) {
403 if (output_string.isEmpty() == 0 ||
404 output_scores.isEmpty() == 0) {
405 /* check if the last syllabic is split */
406 if (flags & MXML_FLAG_KEEP_TEXT) {
407 if (syllabic == "begin" || syllabic == "middle")
408 output_string += "-";
409 }
410 output += "\nS\"";
411 output += output_string;
412 output += "\"\n\n";
413 output += output_scores;
414 output_string = QString();
415 output_scores = QString();
416 }
417 }
418 } else if (tags[3] == "harmony") {
419 if (si == 4) {
420 /* end of harmony */
421 QString harmony[2];
422 uint8_t x;
423 uint8_t which;
424 uint32_t bass;
425 uint32_t root;
426 MppChord_t mask;
427
428 root = MppGetNoteNumber(root_step, root_alter, "5");
429
430 if (bass_step.isEmpty())
431 bass = root;
432 else
433 bass = MppGetNoteNumber(bass_step, bass_alter, "5");
434
435 harmony[0] = QString(MppGetNoteString[root % 12]) + kind;
436 if (root != bass) {
437 harmony[0] += QString("/") +
438 QString(MppGetNoteString[bass % 12]);
439 }
440
441 /* fallback to major */
442 harmony[1] = QString(MppGetNoteString[root % 12]);
443 if (root != bass) {
444 harmony[1] += QString("/") +
445 QString(MppGetNoteString[bass % 12]);
446 }
447
448 for (which = 0; which != 2; which++) {
449 MppStringToChordGeneric(mask, root, bass,
450 MPP_BAND_STEP_12, harmony[which]);
451 if (mask.test(0))
452 break;
453 }
454 if (which == 2)
455 goto skip_harmony;
456
457 if (do_new_line != 0) {
458 do_new_line = 0;
459 output_scores += "\n";
460 }
461
462 if (flags & MXML_FLAG_CONV_CHORDS)
463 output_string += ".";
464
465 if (flags & MXML_FLAG_KEEP_CHORDS) {
466 output_string += "(";
467 output_string += harmony[which];
468 output_string += ")";
469 }
470
471 if (flags & MXML_FLAG_CONV_CHORDS) {
472 output_scores += "U1 ";
473 output_scores += MppKeyStr((3 * 12 + (bass % 12)) * MPP_BAND_STEP_12);
474 output_scores += " ";
475 output_scores += MppKeyStr((4 * 12 + (bass % 12)) * MPP_BAND_STEP_12);
476 output_scores += " ";
477
478 for (x = 0; x != MPP_MAX_CHORD_BANDS; x++) {
479 if (mask.test(x) == 0)
480 continue;
481 output_scores += MppKeyStr(
482 ((5 * 12 + (root % 12)) * MPP_BAND_STEP_12) +
483 (x * MPP_BAND_STEP_CHORD));
484 output_scores += " ";
485 }
486 output_scores += QString("/* ") + harmony[which] + QString(" */\n");
487 }
488 skip_harmony:;
489 }
490 } else if (tags[3] == "note") {
491 if (si == 4) {
492 /* end of note */
493
494 if (flags & MXML_FLAG_KEEP_SCORES) {
495 if (do_new_line == 0) {
496 if (!pitch_step.isEmpty()) {
497 output_scores += "U1 ";
498 output_string += ".";
499 }
500 } else if (is_chord == 0) {
501 output_scores += "\n";
502 if (!pitch_step.isEmpty()) {
503 output_scores += "U1 ";
504 output_string += ".";
505 } else {
506 do_new_line = 0;
507 }
508 }
509 }
510 if (flags & MXML_FLAG_KEEP_TEXT) {
511 output_string += text;
512 if (syllabic == "single" || syllabic == "end")
513 output_string += " ";
514 }
515 if (flags & MXML_FLAG_KEEP_SCORES) {
516 if (!pitch_step.isEmpty()) {
517 uint8_t key;
518
519 key = MppGetNoteNumber(pitch_step, pitch_alter,
520 pitch_octave);
521 output_scores += mid_key_str[key];
522 output_scores += " ";
523 do_new_line = 1;
524 }
525 }
526 }
527 }
528 }
529 }
530 }
531 if (si == 0)
532 break;
533 si--;
534 if (si < MXML_MAX_TAGS)
535 tags[si] = QString();
536 break;
537 default:
538 break;
539 }
540 token = QXmlStreamReader::NoToken;
541 }
542 done:
543 return (output);
544 }
545
546 static int
MppReadMusicXMLParts(const QByteArray & data)547 MppReadMusicXMLParts(const QByteArray &data)
548 {
549 QXmlStreamReader::TokenType token =
550 QXmlStreamReader::NoToken;
551 QXmlStreamReader xml(data);
552 QString tags[MXML_MAX_TAGS];
553 size_t si = 0;
554 int parts = 0;
555
556 while (!xml.atEnd()) {
557 if (token == QXmlStreamReader::NoToken)
558 token = xml.readNext();
559
560 switch (token) {
561 case QXmlStreamReader::Invalid:
562 goto done;
563 case QXmlStreamReader::StartElement:
564 if (si < MXML_MAX_TAGS)
565 tags[si] = xml.name().toString();
566 si++;
567 break;
568 case QXmlStreamReader::EndElement:
569 if (si == 2 && tags[0] == "score-partwise" && tags[1] == "part")
570 parts++;
571 if (si == 0 || parts < 0)
572 goto error;
573 si--;
574 if (si < MXML_MAX_TAGS)
575 tags[si] = QString();
576 break;
577 default:
578 break;
579 }
580 token = QXmlStreamReader::NoToken;
581 }
582 done:
583 if (xml.hasError())
584 goto error;
585 return (parts);
586 error:
587 return (0);
588 }
589
MppMusicXmlImport(const QByteArray & data)590 MppMusicXmlImport :: MppMusicXmlImport(const QByteArray &data) : QDialog()
591 {
592 int nparts = MppReadMusicXMLParts(data);
593
594 if (nparts == 0)
595 return;
596
597 QLabel *lbl;
598
599 gl = new QGridLayout(this);
600
601 setWindowTitle(tr("MusicXML import"));
602 setWindowIcon(QIcon(MppIconFile));
603
604 lbl = new QLabel(tr("Keep melody scores"));
605 gl->addWidget(lbl, 0,0,1,1, Qt::AlignRight|Qt::AlignVCenter);
606
607 lbl = new QLabel(tr("Keep lyrics text"));
608 gl->addWidget(lbl, 1,0,1,1, Qt::AlignRight|Qt::AlignVCenter);
609
610 lbl = new QLabel(tr("Keep chords"));
611 gl->addWidget(lbl, 2,0,1,1, Qt::AlignRight|Qt::AlignVCenter);
612
613 lbl = new QLabel(tr("Convert chords to scores"));
614 gl->addWidget(lbl, 3,0,1,1, Qt::AlignRight|Qt::AlignVCenter);
615
616 lbl = new QLabel(tr("Erase destination view"));
617 gl->addWidget(lbl, 4,0,1,1, Qt::AlignRight|Qt::AlignVCenter);
618
619 lbl = new QLabel(tr("Measures per line"));
620 gl->addWidget(lbl, 5,0,1,1, Qt::AlignRight|Qt::AlignVCenter);
621
622 lbl = new QLabel(tr("Part number to import"));
623 gl->addWidget(lbl, 6,0,1,1, Qt::AlignRight|Qt::AlignVCenter);
624
625 cbx_melody = new MppCheckBox();
626 cbx_melody->setCheckState(Qt::Checked);
627 gl->addWidget(cbx_melody, 0,1,1,1, Qt::AlignCenter);
628
629 cbx_text = new MppCheckBox();
630 cbx_text->setCheckState(Qt::Checked);
631 gl->addWidget(cbx_text, 1,1,1,1, Qt::AlignCenter);
632
633 cbx_chords = new MppCheckBox();
634 cbx_chords->setCheckState(Qt::Checked);
635 gl->addWidget(cbx_chords, 2,1,1,1, Qt::AlignCenter);
636
637 cbx_convert = new MppCheckBox();
638 cbx_convert->setCheckState(Qt::Checked);
639 gl->addWidget(cbx_convert, 3,1,1,1, Qt::AlignCenter);
640
641 cbx_erase = new MppCheckBox();
642 cbx_erase->setCheckState(Qt::Checked);
643 gl->addWidget(cbx_erase, 4,1,1,1, Qt::AlignCenter);
644
645 spn_nmeasure = new QSpinBox();
646 spn_nmeasure->setRange(1,99);
647 spn_nmeasure->setValue(4);
648 gl->addWidget(spn_nmeasure, 5,1,1,1, Qt::AlignCenter);
649
650 spn_partnumber = new QSpinBox();
651 spn_partnumber->setRange(1,nparts);
652 gl->addWidget(spn_partnumber, 6,1,1,1, Qt::AlignCenter);
653
654 btn_done = new QPushButton(tr("Done"));
655 connect(btn_done, SIGNAL(released()), this, SLOT(accept()));
656 gl->addWidget(btn_done, 7,1,1,1);
657
658 exec();
659
660 uint32_t flags = 0;
661
662 if (cbx_melody->isChecked())
663 flags |= MXML_FLAG_KEEP_SCORES;
664 if (cbx_text->isChecked())
665 flags |= MXML_FLAG_KEEP_TEXT;
666 if (cbx_chords->isChecked())
667 flags |= MXML_FLAG_KEEP_CHORDS;
668 if (cbx_convert->isChecked())
669 flags |= MXML_FLAG_CONV_CHORDS;
670
671 output = MppReadMusicXML(data, flags,
672 spn_partnumber->value() - 1, spn_nmeasure->value());
673 }
674
~MppMusicXmlImport()675 MppMusicXmlImport :: ~MppMusicXmlImport()
676 {
677 }
678