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