diff --git a/importexport/musicxml/exportxml.cpp b/importexport/musicxml/exportxml.cpp index 01571aab0ab79..7c5cb7a4c8a38 100644 --- a/importexport/musicxml/exportxml.cpp +++ b/importexport/musicxml/exportxml.cpp @@ -1122,7 +1122,7 @@ static int lcm(int a, int b) static Fraction stretch(Score* score, int st, Fraction tick) { Staff* staff { score->staff(track2staff(st)) }; - Fraction res { staff->timeStretch(tick) }; + Fraction res { staff ? staff->timeStretch(tick) : Fraction()}; #ifdef DEBUG_TICK qDebug() << "track " << st << " tick " << fractionToQString(tick) << " stretch " << fractionToQString(res); #endif @@ -4668,7 +4668,7 @@ void ExportMusicXml::systemText(StaffTextBase const* const text, int staff) { const int offset = calculateTimeDeltaInDivisions(text->tick(), tick(), div); - if (text->plainText() == "") { + if (text->plainText().isEmpty()) { // sometimes empty Texts are present, exporting would result // in invalid MusicXML (as an empty direction-type would be created) return; @@ -6289,7 +6289,7 @@ static void identification(XmlWriter& xml, Score const* const score) QStringList creators; // the creator types commonly found in MusicXML creators << "arranger" << "composer" << "lyricist" << "poet" << "translator"; - for (const QString &type : qAsConst(creators)) { + for (QString &type : creators) { QString creator = score->metaTag(type); if (!creator.isEmpty()) xml.tag(QString("creator type=\"%1\"").arg(type), creator); @@ -6738,11 +6738,11 @@ static void findPitchesUsed(const Part* part, pitchSet& set) // add grace and non-grace note pitches to the result set const Chord* c = static_cast(el); if (c) { - for (const Chord* g : c->graceNotesBefore()) { + for (Chord*& g : c->graceNotesBefore()) { addChordPitchesToSet(g, set); } addChordPitchesToSet(c, set); - for (const Chord* g : c->graceNotesAfter()) { + for (Chord*& g : c->graceNotesAfter()) { addChordPitchesToSet(g, set); } } @@ -6943,11 +6943,11 @@ void ExportMusicXml::writeElement(Element* el, const Measure* m, int sstaff, boo // ise grace after if (c) { const auto ll = &c->lyrics(); - for (Chord* g : c->graceNotesBefore()) { + for (Chord*& g : c->graceNotesBefore()) { chord(g, sstaff, ll, useDrumset); } chord(c, sstaff, ll, useDrumset); - for (Chord* g : c->graceNotesAfter()) { + for (Chord*& g : c->graceNotesAfter()) { chord(g, sstaff, ll, useDrumset); } } @@ -7810,7 +7810,7 @@ void ExportMusicXml::harmony(Harmony const* const h, FretDiagram const* const fd QStringList l = h->xmlDegrees(); if (!l.isEmpty()) { - for (QString tag : qAsConst(l)) { + for (QString& tag : l) { QString degreeText; if (h->xmlKind().startsWith("suspended") && tag.startsWith("add") && tag[3].isDigit() @@ -7879,27 +7879,86 @@ void ExportMusicXml::harmony(Harmony const* const h, FretDiagram const* const fd // export an unrecognized Chord // which may contain arbitrary text // - const QString textNameEscaped = h->hTextName().toHtmlEscaped(); + const QString textName = h->hTextName(); switch (h->harmonyType()) { case HarmonyType::NASHVILLE: { - _xml.tag("function", h->hFunction()); - QString k = "kind text=\"" + textNameEscaped + "\""; - _xml.tag(k, "none"); + QString alter; + QString functionText = h->hFunction(); + if (functionText.isEmpty()) { + // we just dump the text as deprecated function + _xml.tag("function", textName); + _xml.tag("kind", "none"); + break; + } + else if (!functionText.at(0).isDigit()) { + alter = functionText.at(0); + functionText = functionText.at(1); + } + _xml.stag("numeral"); + _xml.tag("numeral-root", functionText); + if (alter == "b") + _xml.tag("numeral-alter", "-1"); + else if (alter == "#") + _xml.tag("numeral-alter", "1"); + _xml.etag(); + if (!h->xmlKind().isEmpty()) { + QString s = "kind"; + QString kindText = h->musicXmlText(); + if (!h->musicXmlText().isEmpty()) + s += " text=\"" + kindText + "\""; + if (h->xmlSymbols() == "yes") + s += " use-symbols=\"yes\""; + if (h->xmlParens() == "yes") + s += " parentheses-degrees=\"yes\""; + _xml.tag(s, h->xmlKind()); + } + else { + // default is major + _xml.tag("kind", "major"); + } } break; case HarmonyType::ROMAN: { - // TODO: parse? - _xml.tag("function", h->hTextName()); // note: HTML escape done by tag() - QString k = "kind text=\"\""; - _xml.tag(k, "none"); + int alter = 0; + static const QRegularExpression roman("(b|#)?([ivIV]+)"); + if (textName.contains(roman)) { + _xml.stag("numeral"); + if (textName.at(0) == "b") + alter = -1; + else if (textName.at(0) == "#") + alter = 1; + const QString numberStr = textName.mid(alter? 1 : 0); + int harmony = 1; + if (numberStr.contains("v", Qt::CaseInsensitive)) { + if (numberStr.startsWith("i", Qt::CaseInsensitive)) + harmony = 4; + else + harmony = 4 + numberStr.size(); + } + else + harmony = numberStr.size(); + QString k = "numeral-root text=\"" + numberStr + "\""; + _xml.tag(k, harmony); + if (alter) + _xml.tag("numeral-alter", alter); + _xml.etag(); + // simple check for major or minor + _xml.tag("kind", numberStr.at(0).isUpper() ? "major" : "minor"); + // infer inversion from ending digits + if (textName.endsWith("64")) + _xml.tag("inversion", 2); + else if (textName.endsWith("6")) + _xml.tag("inversion", 1); + break; + } } - break; + // fallthrough case HarmonyType::STANDARD: default: { _xml.stag("root"); _xml.tag("root-step text=\"\"", "C"); _xml.etag(); // root - QString k = "kind text=\"" + textNameEscaped + "\""; + QString k = "kind text=\"" + textName.toHtmlEscaped() + "\""; _xml.tag(k, "none"); } break; diff --git a/importexport/musicxml/importmxmlpass2.cpp b/importexport/musicxml/importmxmlpass2.cpp index 3a6062c8892b4..cce57db4a860f 100644 --- a/importexport/musicxml/importmxmlpass2.cpp +++ b/importexport/musicxml/importmxmlpass2.cpp @@ -6913,7 +6913,7 @@ void MusicXMLParserPass2::harmony(const QString& partId, Measure* measure, const //QString printStyle = _e.attributes().value("print-style").toString(); const QColor color { _e.attributes().value("color").toString() }; - QString kind, kindText, functionText, symbols, parens; + QString kind, kindText, functionText, inversionText, symbols, parens; QList degreeList; FretDiagram* fd = nullptr; @@ -6963,8 +6963,37 @@ void MusicXMLParserPass2::harmony(const QString& partId, Measure* measure, const functionText = _e.readElementText(); ha->setHarmonyType(HarmonyType::ROMAN); } - //else if (_e.name() == "function") { // MusicXML 4.0 replacement for "function" - // TODO: parse to decide between ROMAN and NASHVILLE + else if (_e.name() == "numeral") { + ha->setRootTpc(Tpc::TPC_INVALID); + ha->setBaseTpc(Tpc::TPC_INVALID); + while (_e.readNextStartElement()) { + if (_e.name() == "numeral-root") { + functionText = _e.attributes().value("text").toString(); + const QString numeralRoot = _e.readElementText(); + if (functionText.isEmpty() || functionText.at(0).isDigit()) { + ha->setHarmonyType(HarmonyType::NASHVILLE); + ha->setFunction(numeralRoot); + } + else + ha->setHarmonyType(HarmonyType::ROMAN); + } + else if (_e.name() == "numeral-alter") { + const int alter = _e.readElementText().toInt(); + switch (alter) { + case -1: + ha->setFunction("b" + ha->hFunction()); + break; + case 1: + ha->setFunction("#" + ha->hFunction()); + break; + default: + break; + } + } + else + skipLogCurrElem(); + } + } else if (_e.name() == "kind") { // attributes: use-symbols yes-no // text, stack-degrees, parentheses-degree, bracket-degrees, @@ -6978,7 +7007,16 @@ void MusicXMLParserPass2::harmony(const QString& partId, Measure* measure, const } } else if (_e.name() == "inversion") { - skipLogCurrElem(); + const int inversion = _e.readElementText().toInt(); + switch (inversion) { + case 1: inversionText = "6"; + break; + case 2: inversionText = "64"; + break; + default: + inversionText = ""; + break; + } } else if (_e.name() == "bass") { QString step; @@ -7051,7 +7089,7 @@ void MusicXMLParserPass2::harmony(const QString& partId, Measure* measure, const } const ChordDescription* d = nullptr; - if (ha->rootTpc() != Tpc::TPC_INVALID) + if (ha->rootTpc() != Tpc::TPC_INVALID || ha->harmonyType() == HarmonyType::NASHVILLE) d = ha->fromXml(kind, kindText, symbols, parens, degreeList); if (d) { ha->setId(d->id); @@ -7059,7 +7097,7 @@ void MusicXMLParserPass2::harmony(const QString& partId, Measure* measure, const } else { ha->setId(-1); - QString textName = functionText + kindText; + QString textName = functionText + kindText + inversionText; ha->setTextName(textName); } ha->render(); diff --git a/mtest/musicxml/io/testHarmony6_ref.xml b/mtest/musicxml/io/testHarmony6_ref.xml index aaa30548bf54f..c20825af11ac7 100644 --- a/mtest/musicxml/io/testHarmony6_ref.xml +++ b/mtest/musicxml/io/testHarmony6_ref.xml @@ -74,8 +74,10 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri - i - none + + 1 + + minor @@ -93,8 +95,10 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri - 1 - none + + 1 + + minor @@ -134,8 +138,10 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri - xyz - none + + C + + none @@ -153,8 +159,8 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri - - none + xyz + none @@ -194,8 +200,10 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri - <>&" - none + + C + + none @@ -213,8 +221,8 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri - - none + <>&" + none diff --git a/mtest/musicxml/io/testHarmony9.xml b/mtest/musicxml/io/testHarmony9.xml new file mode 100644 index 0000000000000..07b49df3bebcf --- /dev/null +++ b/mtest/musicxml/io/testHarmony9.xml @@ -0,0 +1,274 @@ + + + + + Nashville Number System + + + Klaus Rettinghaus + + MuseScore 0.7.0 + 2007-09-10 + + + + + + + + + + Akustischer Bass + Bass + + Akustischer Bass + + + + 1 + 37 + 78.7402 + 0 + + + + + + + 1 + + 2 + + + + F + 4 + + + 0 + 0 + -1 + + + + + 1 + + major + + + + D + 3 + + 1 + 1 + quarter + down + + + + 7 + + half-diminished + + + + C + 1 + 3 + + 1 + 1 + quarter + up + + + + 6 + + minor + + + + B + 2 + + 1 + 1 + quarter + up + + + + 4 + + minor + + + + B + -1 + 2 + + 1 + 1 + quarter + flat + up + + + + + + 5 + + dominant + + + + A + 2 + + 1 + 1 + quarter + up + + + + 1 + + major-sixth + + + + D + 3 + + 1 + 1 + quarter + down + + + + 2 + -1 + + major + + + + E + -1 + 3 + + 1 + 1 + quarter + flat + down + + + + 3 + -1 + + major + + + + F + 3 + + 1 + 1 + quarter + natural + down + + + + + + 1 + + power + + + + D + 3 + + 1 + 1 + quarter + down + + + + 6 + + major-seventh + + + + D + 1 + 3 + + 1 + 1 + quarter + sharp + down + + + + 2 + + minor-sixth + + + + E + 3 + + 1 + 1 + quarter + down + + + + 5 + + suspended-fourth + + + + A + 2 + + 1 + 1 + quarter + up + + + + + + 4 + 1 + + + light-heavy + + + + diff --git a/mtest/musicxml/io/testNumerals.xml b/mtest/musicxml/io/testNumerals.xml new file mode 100644 index 0000000000000..5ade4cbe6f22a --- /dev/null +++ b/mtest/musicxml/io/testNumerals.xml @@ -0,0 +1,223 @@ + + + + + Numeral test file + + + + MuseScore 0.7.0 + 2007-09-10 + + + + + + + + + + Piano + Klav. + + Piano + + + + 1 + 1 + 78.7402 + 0 + + + + + + + 1 + + 0 + + + + F + 4 + + + + + 1 + + major + + + + C + 3 + + 2 + 1 + half + up + + + + 2 + + minor + + + + D + 3 + + 2 + 1 + half + down + + + + + + 3 + + minor + + + + E + 3 + + 2 + 1 + half + down + + + + 4 + + major + + + + F + 3 + + 2 + 1 + half + down + + + + + + 5 + + major + + + + G + 3 + + 4 + 1 + whole + + + + + + 1 + + major + + + + C + 3 + + 2 + 1 + half + up + + + + 2 + + minor + + + + D + 3 + + 2 + 1 + half + down + + + + + + 3 + + minor + + + + E + 3 + + 2 + 1 + half + down + + + + 4 + + major + + + + F + 3 + + 2 + 1 + half + down + + + + + + 5 + + dominant + + + + G + 3 + + 4 + 1 + whole + + + light-heavy + + + + diff --git a/mtest/musicxml/io/tst_mxml_io.cpp b/mtest/musicxml/io/tst_mxml_io.cpp index faf1e4cad2b27..dced312d14c2a 100644 --- a/mtest/musicxml/io/tst_mxml_io.cpp +++ b/mtest/musicxml/io/tst_mxml_io.cpp @@ -159,6 +159,7 @@ private slots: void harmony6() { mxmlMscxExportTestRef("testHarmony6"); } void harmony7() { mxmlMscxExportTestRef("testHarmony7"); } void harmony8() { mxmlIoTest("testHarmony8"); } + void harmony9() { mxmlIoTest("testHarmony9"); } void hello() { mxmlIoTest("testHello"); } void helloReadCompr() { mxmlReadTestCompr("testHello"); } void helloReadWriteCompr() { mxmlReadWriteTestCompr("testHello"); } @@ -253,6 +254,7 @@ private slots: void notesRests1() { mxmlIoTest("testNotesRests1"); } void notesRests2() { mxmlIoTest("testNotesRests2"); } void numberedLyrics() { mxmlIoTestRef("testNumberedLyrics"); } + void numerals() { mxmlIoTest("testNumerals"); } void overlappingSpanners() { mxmlIoTest("testOverlappingSpanners"); } void partNames() { mxmlImportTestRef("testPartNames"); } void partNames2() { mxmlIoTest("testPartNames2"); }