Skip to content

Commit

Permalink
MusicXML: add support for numerals
Browse files Browse the repository at this point in the history
Backport of musescore#24174
  • Loading branch information
rettinghaus authored and Jojo-Schmitz committed Nov 19, 2024
1 parent 69efa78 commit 8ea7e88
Show file tree
Hide file tree
Showing 6 changed files with 640 additions and 36 deletions.
95 changes: 77 additions & 18 deletions importexport/musicxml/exportxml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<const Chord*>(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);
}
}
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand Down
50 changes: 44 additions & 6 deletions importexport/musicxml/importmxmlpass2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<HDegree> degreeList;

FretDiagram* fd = nullptr;
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -7051,15 +7089,15 @@ 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);
ha->setTextName(d->names.front());
}
else {
ha->setId(-1);
QString textName = functionText + kindText;
QString textName = functionText + kindText + inversionText;
ha->setTextName(textName);
}
ha->render();
Expand Down
32 changes: 20 additions & 12 deletions mtest/musicxml/io/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

0 comments on commit 8ea7e88

Please sign in to comment.