From c519902ee31d29dca729b33eece328eb3bce0538 Mon Sep 17 00:00:00 2001 From: Klaus Rettinghaus Date: Sat, 24 Aug 2024 20:04:24 +0200 Subject: [PATCH] MusicXML: add support for numerals Backport of #24174 --- importexport/musicxml/exportxml.cpp | 57 ++++- importexport/musicxml/importmxmlpass2.cpp | 46 +++- mtest/musicxml/io/testHarmony6_ref.xml | 32 ++- mtest/musicxml/io/testHarmony9.xml | 274 ++++++++++++++++++++++ mtest/musicxml/io/tst_mxml_io.cpp | 1 + 5 files changed, 383 insertions(+), 27 deletions(-) create mode 100644 mtest/musicxml/io/testHarmony9.xml diff --git a/importexport/musicxml/exportxml.cpp b/importexport/musicxml/exportxml.cpp index cf3a88c30535b..0f1c2282c3097 100644 --- a/importexport/musicxml/exportxml.cpp +++ b/importexport/musicxml/exportxml.cpp @@ -7816,27 +7816,64 @@ 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"); + static const QRegularExpression romanRegex("[iv]+", QRegularExpression::CaseInsensitiveOption); + if (textName.contains(romanRegex)) { + _xml.stag("numeral"); + QString k = "numeral-root text=\"" + textName + "\""; + _xml.tag(k, "1"); + _xml.etag(); + // only check for major or minor + _xml.tag("kind", textName.at(0).isUpper() ? "major" : "minor"); + 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 d9742f9d26ac8..4068150c71e97 100644 --- a/importexport/musicxml/importmxmlpass2.cpp +++ b/importexport/musicxml/importmxmlpass2.cpp @@ -6893,7 +6893,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; @@ -6943,7 +6943,34 @@ void MusicXMLParserPass2::harmony(const QString& partId, Measure* measure, const 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") { + QString numeralRoot = _e.readElementText(); + //QString numeralRootText = _e.attributes().value("text").toString(); + // TODO analyze text and import as roman numerals + ha->setHarmonyType(HarmonyType::NASHVILLE); + ha->setFunction(numeralRoot); + } + 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, @@ -6957,7 +6984,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; @@ -7030,7 +7066,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->hFunction().isEmpty()) d = ha->fromXml(kind, kindText, symbols, parens, degreeList); if (d) { ha->setId(d->id); @@ -7038,7 +7074,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/tst_mxml_io.cpp b/mtest/musicxml/io/tst_mxml_io.cpp index 4e38bbfc4f545..d937ac7a34edb 100644 --- a/mtest/musicxml/io/tst_mxml_io.cpp +++ b/mtest/musicxml/io/tst_mxml_io.cpp @@ -156,6 +156,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"); }