Skip to content

MusicXML: add support for numerals #24174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5946,35 +5946,35 @@ static void directionJump(XmlWriter& xml, const Jump* const jp)
bool isDaCapo = false;
bool isDalSegno = false;
if (jtp == JumpType::DC) {
if (jp->xmlText() == "") {
if (jp->xmlText().empty()) {
words = u"D.C.";
} else {
words = jp->xmlText();
}
isDaCapo = true;
} else if (jtp == JumpType::DC_AL_FINE) {
if (jp->xmlText() == "") {
if (jp->xmlText().empty()) {
words = u"D.C. al Fine";
} else {
words = jp->xmlText();
}
isDaCapo = true;
} else if (jtp == JumpType::DC_AL_CODA) {
if (jp->xmlText() == "") {
if (jp->xmlText().empty()) {
words = u"D.C. al Coda";
} else {
words = jp->xmlText();
}
isDaCapo = true;
} else if (jtp == JumpType::DS_AL_CODA) {
if (jp->xmlText() == "") {
if (jp->xmlText().empty()) {
words = u"D.S. al Coda";
} else {
words = jp->xmlText();
}
isDalSegno = true;
} else if (jtp == JumpType::DS_AL_FINE) {
if (jp->xmlText() == "") {
if (jp->xmlText().empty()) {
words = u"D.S. al Fine";
} else {
words = jp->xmlText();
Expand All @@ -5996,7 +5996,7 @@ static void directionJump(XmlWriter& xml, const Jump* const jp)
if (isDaCapo) {
sound = u"dacapo=\"yes\"";
} else if (isDalSegno) {
if (jp->jumpTo() == "") {
if (jp->xmlText().empty()) {
sound = u"dalsegno=\"1\"";
} else {
sound = u"dalsegno=\"" + jp->jumpTo() + u"\"";
Expand Down Expand Up @@ -8683,7 +8683,7 @@ void ExportMusicXml::harmony(Harmony const* const h, FretDiagram const* const fd
if (!h->xmlKind().isEmpty()) {
String s = u"kind";
String kindText = h->musicXmlText();
if (h->musicXmlText() != u"") {
if (!h->musicXmlText().empty()) {
s += u" text=\"" + kindText + u"\"";
}
if (h->xmlSymbols() == u"yes") {
Expand Down Expand Up @@ -8781,16 +8781,83 @@ void ExportMusicXml::harmony(Harmony const* const h, FretDiagram const* const fd
const String textName = h->hTextName();
switch (h->harmonyType()) {
case HarmonyType::NASHVILLE: {
m_xml.tag("function", h->hFunction());
m_xml.tag("kind", { { "text", textName } }, "none");
String alter;
String functionText = h->hFunction();
if (functionText.empty()) {
// we just dump the text as deprecated function
m_xml.tag("function", textName);
m_xml.tag("kind", "none");
break;
} else if (!functionText.at(0).isDigit()) {
alter = functionText.at(0);
functionText = functionText.at(1);
}
m_xml.startElement("numeral");
m_xml.tag("numeral-root", functionText);
if (alter == u"b") {
m_xml.tag("numeral-alter", "-1");
} else if (alter == u"#") {
m_xml.tag("numeral-alter", "1");
}
m_xml.endElement();
if (!h->xmlKind().isEmpty()) {
String s = u"kind";
String kindText = h->musicXmlText();
if (!h->musicXmlText().empty()) {
s += u" text=\"" + kindText + u"\"";
}
if (h->xmlSymbols() == "yes") {
s += u" use-symbols=\"yes\"";
}
if (h->xmlParens() == "yes") {
s += u" parentheses-degrees=\"yes\"";
}
m_xml.tagRaw(s, h->xmlKind());
} else {
// default is major
m_xml.tag("kind", "major");
}
}
break;
case HarmonyType::ROMAN: {
// TODO: parse?
m_xml.tag("function", h->hTextName()); // note: HTML escape done by tag()
m_xml.tag("kind", { { "text", "" } }, "none");
int alter = 0;
static const std::wregex roman(L"(b|#)?([ivIV]+)");
if (textName.contains(roman)) {
StringList matches = textName.search(roman, { 1, 2 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can someone (@cbjeukendrup?) explain to me what exactly this does? I don't understand the code in src/framework/global/types/string.cpp, and esp. not how to do this with plain Qt methods

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks for the parts of the regex (in parentheses) and stores them in the string list.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'parts of the regex' ? So splits the b or # from i, v, I, V ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. But this could surely be done in other ways.

Copy link
Contributor

@Jojo-Schmitz Jojo-Schmitz Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now resorted to use textName.at(0) for the first and textName.mid(alter? 1 : 0) on the (potential) 2nd part of the expression, IMHO a lot simpler

m_xml.startElement("numeral");
if (matches.at(0) == u"b") {
alter = -1;
} else if (matches.at(0) == u"#") {
alter = 1;
}
const String numberStr = matches.at(1);
size_t harmoy = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is just a name, but isn't is supposed to be harmony?

if (numberStr.contains(u"v", CaseSensitivity::CaseInsensitive)) {
if (numberStr.startsWith(u"i", CaseSensitivity::CaseInsensitive)) {
harmoy = 4;
} else {
harmoy = 4 + numberStr.size();
}
} else {
harmoy = numberStr.size();
}
m_xml.tag("numeral-root", { { "text", numberStr } }, harmoy);
if (alter) {
m_xml.tag("numeral-alter", alter);
}
m_xml.endElement();
// simple check for major or minor
m_xml.tag("kind", numberStr.at(0).isUpper() ? "major" : "minor");
// infer inversion from ending digits
if (textName.endsWith(u"64")) {
m_xml.tag("inversion", 2);
} else if (textName.endsWith(u"6")) {
m_xml.tag("inversion", 1);
}
break;
}
}
break;
// fallthrough
case HarmonyType::STANDARD:
default: {
m_xml.startElement("root");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6913,8 +6913,8 @@ Note* MusicXmlParserPass2::note(const String& partId,
// handle notations
if (cr) {
notations.addToScore(cr, note,
noteStartTime.ticks(), m_slurs, m_glissandi, m_spanners, m_trills, m_ties, m_unstartedTieNotes, m_unendedTieNotes, arpMap,
delayedArps);
noteStartTime.ticks(), m_slurs, m_glissandi, m_spanners, m_trills, m_ties, m_unstartedTieNotes,
m_unendedTieNotes, arpMap, delayedArps);

// if no tie added yet, convert the "tie" into "tied" and add it.
if (note && !note->tieFor() && !note->tieBack() && !tieType.empty()) {
Expand Down Expand Up @@ -7374,7 +7374,7 @@ void MusicXmlParserPass2::harmony(const String& partId, Measure* measure, const
const String placement = m_e.attribute("placement");
const bool printObject = m_e.asciiAttribute("print-object") != "no";

String kind, kindText, functionText, symbols, parens;
String kind, kindText, functionText, inversionText, symbols, parens;
std::list<HDegree> degreeList;

FretDiagram* fd = nullptr;
Expand Down Expand Up @@ -7418,8 +7418,36 @@ void MusicXmlParserPass2::harmony(const String& partId, Measure* measure, const
ha->setRootTpc(Tpc::TPC_INVALID);
ha->setBaseTpc(Tpc::TPC_INVALID);
functionText = m_e.readText();
// TODO: parse to decide between ROMAN and NASHVILLE
ha->setHarmonyType(HarmonyType::ROMAN);
} else if (m_e.name() == "numeral") {
ha->setRootTpc(Tpc::TPC_INVALID);
ha->setBaseTpc(Tpc::TPC_INVALID);
while (m_e.readNextStartElement()) {
if (m_e.name() == "numeral-root") {
functionText = m_e.attribute("text");
const String numeralRoot = m_e.readText();
if (functionText.isEmpty() || functionText.at(0).isDigit()) {
ha->setHarmonyType(HarmonyType::NASHVILLE);
ha->setFunction(numeralRoot);
} else {
ha->setHarmonyType(HarmonyType::ROMAN);
}
} else if (m_e.name() == "numeral-alter") {
const int alter = m_e.readText().toInt();
switch (alter) {
case -1:
ha->setFunction(u"b" + ha->hFunction());
break;
case 1:
ha->setFunction(u"#" + ha->hFunction());
break;
default:
break;
}
} else {
skipLogCurrElem();
}
}
} else if (m_e.name() == "kind") {
// attributes: use-symbols yes-no
// text, stack-degrees, parentheses-degree, bracket-degrees,
Expand All @@ -7432,8 +7460,16 @@ void MusicXmlParserPass2::harmony(const String& partId, Measure* measure, const
ha->setRootTpc(Tpc::TPC_INVALID);
}
} else if (m_e.name() == "inversion") {
// attributes: print-style
skipLogCurrElem();
const int inversion = m_e.readText().toInt();
switch (inversion) {
case 1: inversionText = u"6";
break;
case 2: inversionText = u"64";
break;
default:
inversionText = u"";
break;
}
} else if (m_e.name() == "bass") {
String step;
int alter = 0;
Expand Down Expand Up @@ -7501,15 +7537,15 @@ void MusicXmlParserPass2::harmony(const String& 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);
ha->setTextName(d->names.front());
} else {
ha->setId(-1);
String textName = functionText + kindText;
String textName = functionText + kindText + inversionText;
ha->setTextName(textName);
}
ha->render();
Expand Down
32 changes: 20 additions & 12 deletions src/importexport/musicxml/tests/data/testHarmony6_ref.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri
</measure>
<measure number="2">
<harmony print-frame="no">
<function>i</function>
<kind text="">none</kind>
<numeral>
<numeral-root text="i">1</numeral-root>
</numeral>
<kind>minor</kind>
</harmony>
<note>
<pitch>
Expand All @@ -93,8 +95,10 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri
</measure>
<measure number="3">
<harmony print-frame="no">
<function>1</function>
<kind text="m">none</kind>
<numeral>
<numeral-root>1</numeral-root>
</numeral>
<kind text="m">minor</kind>
</harmony>
<note>
<pitch>
Expand Down Expand Up @@ -134,8 +138,10 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri
</measure>
<measure number="5">
<harmony print-frame="no">
<function>xyz</function>
<kind text="">none</kind>
<root>
<root-step text="">C</root-step>
</root>
<kind text="xyz">none</kind>
</harmony>
<note>
<pitch>
Expand All @@ -153,8 +159,8 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri
</measure>
<measure number="6">
<harmony print-frame="no">
<function></function>
<kind text="xyz">none</kind>
<function>xyz</function>
<kind>none</kind>
</harmony>
<note>
<pitch>
Expand Down Expand Up @@ -194,8 +200,10 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri
</measure>
<measure number="8">
<harmony print-frame="no">
<function>&lt;&gt;&amp;&quot;</function>
<kind text="">none</kind>
<root>
<root-step text="">C</root-step>
</root>
<kind text="&lt;&gt;&amp;&quot;">none</kind>
</harmony>
<note>
<pitch>
Expand All @@ -213,8 +221,8 @@ containing recognized text, unrecognized plaintext and unrecognized text requiri
</measure>
<measure number="9">
<harmony print-frame="no">
<function></function>
<kind text="&lt;&gt;&amp;&quot;">none</kind>
<function>&lt;&gt;&amp;&quot;</function>
<kind>none</kind>
</harmony>
<note>
<pitch>
Expand Down
Loading