MusicXML import of unrecognized chords

A new method is provided generate a chordname directly from a MusicXML
harmony object.  This chordname is then parsed and rendered just as for a
chordname typed directly into the score.
This commit is contained in:
Marc Sabatella 2013-05-31 17:14:07 -06:00
parent 07962f4a5a
commit 0cd63cfe6f
9 changed files with 421 additions and 135 deletions

View file

@ -434,23 +434,30 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
{
QString tok1, tok1L, tok2, tok2L;
QString extensionDigits = "123456789";
QString special = "(), ";
QString leading = "( ";
QString trailing = "), ";
QString special = "()[], ";
QString leading = "([ ";
QString trailing = ")], ";
QString initial;
bool take6 = false, take7 = false, take9 = false, take11 = false, take13 = false;
int firstLeadingToken, lastLeadingToken;
#ifdef ALLOW_PARENTHESIZED_EXTENSION
int firstLeadingToken;
#endif
int lastLeadingToken;
int len = s.size();
int i, j;
configure(cl);
_name = s;
_parseable = true;
_understandable = true;
i = 0;
#ifdef ALLOW_PARENTHESIZED_EXTENSION
// eat leading parens
firstLeadingToken = _tokenList.size();
while (i < len && leading.contains(s[i]))
addToken(QString(s[i++]),QUALITY);
#endif
lastLeadingToken = _tokenList.size();
// get quality
for (j = 0, tok1 = "", tok1L = "", initial = ""; i < len; ++i, ++j) {
@ -506,26 +513,35 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
}
if (tok1 != "")
addToken(tok1,QUALITY);
#ifdef ALLOW_PARENTHESIZED_EXTENSION
else {
// leading tokens were not really QUALITY
for (int t = firstLeadingToken; t < lastLeadingToken; ++t)
_tokenList[t].tokenClass = EXTENSION;
}
#endif
if (!syntaxOnly) {
_xmlKind = _quality;
_xmlText = tok1;
if (symbols.contains(tok1))
_xmlParens = "no";
if (symbols.contains(tok1)) {
_xmlSymbols = "yes";
else
_xmlText = "";
}
else {
_xmlSymbols = "no";
_xmlText = tok1;
}
}
// eat trailing parens and commas
while (i < len && trailing.contains(s[i]))
addToken(QString(s[i++]),QUALITY);
#ifdef ALLOW_PARENTHESIZED_EXTENSION
// eat leading parens
firstLeadingToken = _tokenList.size();
while (i < len && leading.contains(s[i])) {
while (i < len && leading.contains(s[i]))
addToken(QString(s[i++]),EXTENSION);
}
#endif
lastLeadingToken = _tokenList.size();
// get extension - up to first non-digit
for (j = 0, tok1 = ""; i < len; ++i, ++j) {
@ -537,22 +553,29 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
if (_quality == "") {
if (_extension == "7" || _extension == "9" || _extension == "11" || _extension == "13") {
_quality = "dominant";
if (!syntaxOnly)
_xmlKind = "dominant";
take7 = true; take9 = true; take11 = true; take13 = true;
}
else {
_quality = "major";
if (!syntaxOnly)
_xmlKind = "major";
take6 = true; take7 = true; take9 = true; take11 = true; take13 = true;
}
}
if (tok1 != "")
addToken(tok1,EXTENSION);
#ifdef ALLOW_PARENTHESIZED_EXTENSION
else {
// leading tokens were not really EXTENSION
for (int t = firstLeadingToken; t < lastLeadingToken; ++t)
for (int t = firstLeadingToken; t < lastLeadingToken; ++t) {
_tokenList[t].tokenClass = MODIFIER;
if (!syntaxOnly)
_xmlParens = "yes";
}
}
#endif
if (!syntaxOnly) {
QStringList extl;
if (tok1 == "5")
@ -566,7 +589,7 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
else if (tok1 == "7") {
if (take7)
_xmlKind += "-seventh";
else
else if (_xmlKind != "half-diminished")
extl << "7";
}
else if (tok1 == "9") {
@ -576,26 +599,32 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
_xmlKind += "-seventh";
extl << "9";
}
else if (_xmlKind == "half-diminished")
extl << "9";
else
extl << "7" << "9";
}
else if (tok1 == "11") {
if (take11)
_xmlKind += "-eleventh";
_xmlKind += "-11th";
else if (take7) {
_xmlKind += "-seventh";
extl << "9" << "11";
}
else if (_xmlKind == "half-diminished")
extl << "9" << "11";
else
extl << "7" << "9" << "11";
}
else if (tok1 == "13") {
if (take13)
_xmlKind += "-thirteenth";
_xmlKind += "-13th";
else if (take7) {
_xmlKind += "-seventh";
extl << "9" << "11" << "13";
}
else if (_xmlKind == "half-diminished")
extl << "9" << "11" << "13";
else
extl << "7" << "9" << "11" << "13";
}
@ -613,6 +642,9 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
}
if (_xmlKind == "dominant-seventh")
_xmlKind = "dominant";
if (tok1 == "69")
_xmlText += "6";
else
_xmlText += tok1;
}
// eat trailing parens and commas
@ -621,7 +653,6 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
// get modifiers
bool addPending = false;
_xmlParens = "no";
_modifierList.clear();
while (i < len) {
// eat leading parens
@ -675,7 +706,7 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
else if (tok1L == "sus" && tok2L == "")
tok2L = "4";
else if (augmented.contains(tok1L) && tok2L == "") {
_xmlDegrees += "alt#5";
_quality = "augmented";
tok1L = "";
}
QString m = tok1L + tok2L;
@ -693,11 +724,18 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
else if (tok1L == "no")
degree = "sub" + tok2L;
else if (tok1L == "sus") {
#ifdef TREAT_SUS_AS_DEGREE
_xmlDegrees += "sub3";
tok1L = "add";
#else
// convert chords with sus into suspended "kind"
// extension then becomes a series of degree adds
QString seventh;
if (tok2L == "4")
_xmlKind = "suspended-fourth";
else if (tok2L == "2")
_xmlKind = "suspended-second";
_xmlText = tok1 + tok2;
if (_extension == "7" || _extension == "9" || _extension == "11" || _extension == "13")
degree = (_quality == "major") ? "add#7" : "add7";
else if (_extension != "")
@ -707,12 +745,13 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
_xmlDegrees += "add11";
_xmlDegrees += "add13";
}
if (_extension == "11") {
else if (_extension == "11") {
_xmlDegrees += "add9";
_xmlDegrees += "add11";
}
if (_extension == "9")
else if (_extension == "9")
_xmlDegrees += "add9";
#endif
}
else if (tok1L == "major") {
if (_xmlKind.startsWith("minor")) {
@ -759,10 +798,11 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
}
else if (tok1L == "tristan") {
_xmlKind = "Tristan";
_xmlText = "Tristan";
}
else if (addPending)
degree = "add" + tok1L + tok2L;
else if (tok1L == "")
else if (tok1L == "" && tok2L != "")
degree = "add" + tok2L;
else if (lower.contains(tok1L)) {
tok1L = "b";
@ -800,12 +840,215 @@ bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly)
_handle = "<" + _quality + "><" + _extension + ">" + _modifiers;
qDebug("parse: source = <%s>, handle = %s",qPrintable(s),qPrintable(_handle));
if (!syntaxOnly) {
qDebug("parse: xmlKind = <%s>, text = <%s>",qPrintable(_xmlKind),qPrintable(_xmlText));
qDebug("parse: xmlSymbols = %s, xmlParens = %s",qPrintable(_xmlSymbols),qPrintable(_xmlParens));
qDebug("parse: xmlDegrees = <%s>",qPrintable(_xmlDegrees.join(",")));
}
return _parseable;
}
//---------------------------------------------------------
// fromXml
//---------------------------------------------------------
QString ParsedChord::fromXml(const QString& kind, const QString& rawKindText, const QString& useSymbols, const QString& useParens, const QList<HDegree>& dl, const ChordList* cl)
{
QString kindText = rawKindText;
bool symbols = (useSymbols == "yes");
bool parens = (useParens == "yes");
bool implied = false;
bool extend = false;
int extension = 0;
_parseable = true;
_understandable = true;
// get quality info from kind
if (kind == "major-minor") {
_quality = "minor";
_modifierList += "major7";
extend = true;
}
else if (kind.contains("major")) {
_quality = "major";
if (kind == "major" || kind == "major-sixth")
implied = true;
}
else if (kind.contains("minor"))
_quality = "minor";
else if (kind.contains("dominant")) {
_quality = "dominant";
implied = true;
extension = 7;
}
else if (kind == "augmented-seventh") {
_quality = "augmented";
extension = 7;
extend = true;
}
else if (kind == "augmented")
_quality = "augmented";
else if (kind == "half-diminished") {
_quality = "half-diminished";
extension = 7;
extend = true;
}
else if (kind == "diminished-seventh") {
_quality = "diminished";
extension = 7;
}
else if (kind == "diminished")
_quality = "diminished";
else if (kind == "suspended-fourth") {
_quality = "major";
implied = true;
if (kindText == "")
_modifierList += "sus4";
else
_modifierList += kindText;
}
else if (kind == "suspended-second") {
_quality = "major";
implied = true;
if (kindText == "")
_modifierList += "sus2";
else
_modifierList += kindText;
}
else if (kind == "power") {
_quality = "major";
implied = true;
extension = 5;
}
else
_quality = kind;
// get extension info from kind
if (kind.contains("seventh"))
extension = 7;
else if (kind.contains("ninth"))
extension = 9;
else if (kind.contains("11th"))
extension = 11;
else if (kind.contains("13th"))
extension = 13;
else if (kind.contains("sixth"))
extension = 6;
// get modifier info from degree list
foreach (const HDegree& d, dl) {
QString mod;
int v = d.value();
switch (d.type()) {
case ADD:
case ALTER:
switch (d.alter()) {
case -1: mod = "b"; break;
case 1: mod = "#"; break;
case 0: mod = "add"; break;
}
break;
case SUBTRACT:
mod = "no";
break;
case UNDEF:
default:
break;
}
mod += QString("%1").arg(v);
if (mod == "add7" && kind.contains("suspended")) {
_quality = "dominant";
implied = true;
extension = 7;
extend = true;
mod = "";
#ifdef TREAT_SUS_AS_DEGREE
kindText = "";
#endif
}
else if (mod == "add9" && extend) {
if (extension < 9)
extension = 9;
mod = "";
}
else if (mod == "add11" && extend) {
if (extension < 11)
extension = 11;
mod = "";
}
else if (mod == "add13" && extend) {
extension = 13;
mod = "";
}
else if (mod == "add9" && kind.contains("sixth")) {
extension = 69;
mod = "";
}
if (mod != "")
_modifierList += mod;
}
// convert no3,add[42] into sus[42]
int no3 = _modifierList.indexOf("no3");
if (no3 >= 0 && (_modifierList.contains("add4") || _modifierList.contains("add2"))) {
int addn = _modifierList.indexOf("add4");
if (addn == -1)
addn = _modifierList.indexOf("add2");
QString& s = _modifierList[addn];
s.replace("add","sus");
_modifierList.removeAt(no3);
}
// record extension
if (extension)
_extension = QString("%1").arg(extension);
// construct name & handle
if (kindText != "" && !kind.contains("suspended") && kind != "major-minor") {
_name = kindText;
if (extension == 69)
_name += "9";
}
else if (implied)
_name = _extension;
else {
if (_quality == "major")
_name = symbols ? "^" : "maj";
else if (_quality == "minor")
_name = symbols ? "-" : "m";
else if (_quality == "augmented")
_name = symbols ? "+" : "aug";
else if (_quality == "diminished")
_name = symbols ? "o" : "dim";
else if (_quality == "half-diminished")
_name = symbols ? "0" : "m7b5";
else if (_quality == "major-minor")
_name = symbols ? "-(^7)" : "m(maj7)";
else
_name = _quality;
_name += _extension;
}
if (parens)
_name += "(";
foreach (QString mod, _modifierList) {
mod.replace("major","maj");
_name += mod;
}
if (parens)
_name += ")";
// parse name to construct handle & tokenList
parse(_name,cl,true);
// record original MusicXML
_xmlKind = kind;
_xmlText = kindText;
_xmlSymbols = useSymbols;
_xmlParens = useParens;
foreach (const HDegree& d, dl)
_xmlDegrees += d.text();
return _name;
}
//---------------------------------------------------------
// renderList
//---------------------------------------------------------
@ -913,11 +1156,13 @@ void ChordDescription::complete(ParsedChord* pc, const ChordList* cl)
}
if (xmlKind == "") {
xmlKind = pc->xmlKind();
xmlDegrees = pc->xmlDegrees();
}
// these fields are not read from chord description files
// so get them from the parsed representation in all cases
xmlText = pc->xmlText();
xmlSymbols = pc->xmlSymbols();
xmlParens = pc->xmlParens();
xmlDegrees = pc->xmlDegrees();
}
}
//---------------------------------------------------------

View file

@ -123,6 +123,7 @@ class ChordToken {
class ParsedChord {
public:
bool parse(const QString&, const ChordList*, bool syntaxOnly = false);
QString fromXml(const QString&, const QString&, const QString&, const QString&, const QList<HDegree>&, const ChordList*);
const QList<RenderAction>& renderList(const ChordList*);
bool parseable() const { return _parseable; }
bool understandable() const { return _understandable; }
@ -131,11 +132,12 @@ class ParsedChord {
const QString& xmlSymbols() const { return _xmlSymbols; }
const QString& xmlParens() const { return _xmlParens; }
const QStringList& xmlDegrees() const { return _xmlDegrees; }
bool operator==(const ParsedChord& c) { return (this->_handle == c._handle); }
bool operator!=(const ParsedChord& c) { return !(*this == c); }
operator QString() { return _handle; }
operator QString() const { return _handle; }
bool operator==(const ParsedChord& c) const { return (this->_handle == c._handle); }
bool operator!=(const ParsedChord& c) const { return !(*this == c); }
ParsedChord();
private:
QString _name;
QString _handle;
QString _quality;
QString _extension;

View file

@ -130,7 +130,7 @@ Harmony::Harmony(const Harmony& h)
_leftParen = h._leftParen;
_rightParen = h._rightParen;
_degreeList = h._degreeList;
_parsedForm = h._parsedForm;
_parsedForm = h._parsedForm ? new ParsedChord(*h._parsedForm) : 0;
_textName = h._textName;
_userName = h._userName;
}
@ -358,7 +358,7 @@ static int convertRoot(const QString& s, bool germanNames)
// return true if chord is recognized
//---------------------------------------------------------
bool Harmony::parseHarmony(const QString& ss, int* root, int* base)
const ChordDescription* Harmony::parseHarmony(const QString& ss, int* root, int* base)
{
_id = -1;
if (_parsedForm) {
@ -382,12 +382,12 @@ bool Harmony::parseHarmony(const QString& ss, int* root, int* base)
int n = s.size();
if (n < 1)
return false;
return 0;
bool germanNames = score()->styleB(ST_useGermanNoteNames);
int r = convertRoot(s, germanNames);
if (r == INVALID_TPC) {
qDebug("1:parseHarmony failed <%s>", qPrintable(ss));
return false;
return 0;
}
*root = r;
int idx = ((n > 1) && ((s[1] == 'b') || (s[1] == '#'))) ? 2 : 1;
@ -402,38 +402,21 @@ bool Harmony::parseHarmony(const QString& ss, int* root, int* base)
s = s.mid(idx).simplified();
_userName = s;
const ChordList* cl = score()->style()->chordList();
const ChordDescription* cd = 0;
if (useLiteral)
cd = descr(s);
else {
_parsedForm = new ParsedChord();
bool parseable = _parsedForm->parse(s,cl);
// attempt to match chord using exact string match
// but also match using parsed forms as fallback
foreach (const ChordDescription* cd, *cl) {
foreach (QString ss, cd->names) {
if (s == ss) {
_id = cd->id;
_textName = ss;
qDebug("exact chord match succeeded <%s>",qPrintable(ss));
return true;
_parsedForm->parse(s,cl);
cd = descr(s,_parsedForm);
}
}
if (parseable && _id == -1 && !useLiteral) {
foreach (ParsedChord ssParsed, cd->parsedChords) {
if (*_parsedForm == ssParsed) {
if (cd) {
_id = cd->id;
_textName = cd->names.front();
qDebug("flexible chordmatch succeeded <%s> %s", qPrintable(s), qPrintable(ssParsed));
break;
}
}
}
}
// exact match failed, so fall back on parsed match if one was found
if (_id != -1)
return true;
// no match found
else
_textName = _userName;
qDebug("2:parseHarmony failed <%s><%s> (%s)", qPrintable(ss), qPrintable(s), qPrintable(*_parsedForm));
return parseable;
return cd;
}
//---------------------------------------------------------
@ -484,9 +467,11 @@ void Harmony::endEdit()
void Harmony::setHarmony(const QString& s)
{
int r, b;
const ChordDescription* cd = 0;
if (parseHarmony(s, &r, &b))
cd = getDescription();
const ChordDescription* cd = parseHarmony(s, &r, &b);
if (!cd && _parsedForm && _parsedForm->parseable()) {
cd = generateDescription();
_id = cd->id;
}
if (cd) {
setRootTpc(r);
setBaseTpc(b);
@ -533,11 +518,9 @@ QString HDegree::text() const
case 1: degree += "#"; break;
default: break;
}
/* QString s = QString("%1").arg(_value);
QString ss; // = degree + s;
QString s = QString("%1").arg(_value);
QString ss = degree + s;
return ss;
*/
return QString();
}
//---------------------------------------------------------
@ -570,7 +553,7 @@ const ChordDescription* Harmony::fromXml(const QString& kind, const QList<HDegr
//---------------------------------------------------------
// fromXml
// lookup harmony in harmony data base
// using musicXml "kind" string and degree list
// using musicXml "kind" string only
//---------------------------------------------------------
const ChordDescription* Harmony::fromXml(const QString& kind)
@ -584,6 +567,22 @@ const ChordDescription* Harmony::fromXml(const QString& kind)
return 0;
}
//---------------------------------------------------------
// fromXml
// construct harmony directly from XML
// build name first
// then generate chord description from that
//---------------------------------------------------------
const ChordDescription* Harmony::fromXml(const QString& kind, const QString& kindText, const QString& symbols, const QString& parens, const QList<HDegree>& dl)
{
ParsedChord* pc = new ParsedChord;
_textName = pc->fromXml(kind, kindText, symbols, parens, dl, score()->style()->chordList());
_parsedForm = pc;
const ChordDescription* cd = getDescription(_textName,pc);
return cd;
}
//---------------------------------------------------------
// descr
// look up id in chord list
@ -595,6 +594,33 @@ const ChordDescription* Harmony::descr() const
return score()->style()->chordDescription(_id);
}
//---------------------------------------------------------
// descr
// look up name in chord list
// optionally look up by parsed chord as fallback
// return chord description if found, or null
//---------------------------------------------------------
const ChordDescription* Harmony::descr(const QString& name, const ParsedChord* pc) const
{
const ChordList* cl = score()->style()->chordList();
const ChordDescription* match = 0;
foreach (const ChordDescription* cd, *cl) {
foreach (const QString& s, cd->names) {
if (s == name)
return cd;
else if (pc) {
foreach (const ParsedChord& sParsed, cd->parsedChords) {
if (sParsed == *pc)
match = cd;
}
}
}
}
// exact match failed, so fall back on parsed match if one was found
return match;
}
//---------------------------------------------------------
// getDescription
// look up id in chord list
@ -618,29 +644,25 @@ const ChordDescription* Harmony::getDescription()
//---------------------------------------------------------
// getDescription
// same but lookup by name
// same but lookup by name and optionally parsed chord
//---------------------------------------------------------
const ChordDescription* Harmony::getDescription(const QString& name)
const ChordDescription* Harmony::getDescription(const QString& name, const ParsedChord* pc)
{
const ChordDescription* cd = 0;
foreach (cd, *score()->style()->chordList()) {
foreach (const QString& n, cd->names) {
if (name == n) {
const ChordDescription* cd = descr(name,pc);
if (cd)
_id = cd->id;
_textName = cd->names.front();
return cd;
}
}
}
else {
cd = generateDescription();
_id = cd->id;
}
return cd;
}
//---------------------------------------------------------
// generateDescription
// generate new chord description from _textName
// add top chord list using private id
// add to chord list using private id
//---------------------------------------------------------
const ChordDescription* Harmony::generateDescription()

View file

@ -105,8 +105,9 @@ class Harmony : public Text {
bool rightParen() const { return _rightParen; }
const ChordDescription* descr() const;
const ChordDescription* descr(const QString&, const ParsedChord* pc = 0) const;
const ChordDescription* getDescription();
const ChordDescription* getDescription(const QString&);
const ChordDescription* getDescription(const QString&, const ParsedChord* pc = 0);
const ChordDescription* generateDescription();
virtual void layout();
@ -138,7 +139,7 @@ class Harmony : public Text {
QString harmonyName() const;
void render(const TextStyle* ts = 0);
bool parseHarmony(const QString& s, int* root, int* base);
const ChordDescription* parseHarmony(const QString& s, int* root, int* base);
const QString& extensionName() const;
@ -153,6 +154,7 @@ class Harmony : public Text {
virtual bool isEmpty() const;
virtual qreal baseLine() const;
const ChordDescription* fromXml(const QString&, const QString&, const QString&, const QString&, const QList<HDegree>&);
const ChordDescription* fromXml(const QString& s, const QList<HDegree>&);
const ChordDescription* fromXml(const QString& s);
virtual void spatiumChanged(qreal oldValue, qreal newValue);

View file

@ -5055,7 +5055,7 @@ void MusicXml::xmlHarmony(QDomElement e, int tick, Measure* measure, int staff)
QString printFrame(e.attribute("print-frame"));
QString printStyle(e.attribute("print-style"));
QString kind, kindText;
QString kind, kindText, symbols, parens;
QList<HDegree> degreeList;
if (harmony) {
@ -5096,6 +5096,8 @@ void MusicXml::xmlHarmony(QDomElement e, int tick, Measure* measure, int staff)
// print-style, halign, valign
kindText = e.attribute("text");
symbols = e.attribute("use-symbols");
parens = e.attribute("parentheses-degrees");
kind = e.text();
}
else if (tag == "inversion") {
@ -5172,7 +5174,19 @@ void MusicXml::xmlHarmony(QDomElement e, int tick, Measure* measure, int staff)
//TODO-WS ha->setTick(tick);
const ChordDescription* d = ha->fromXml(kind, degreeList);
const ChordDescription* d = ha->fromXml(kind, kindText, symbols, parens, degreeList);
if (d) {
ha->setId(d->id);
ha->setTextName(d->names.front());
ha->render();
}
#if 0
else {
// This code won't be hit if MusicXML was at all straightforward,
// as fromXml() is normally able to construct a chord description by itself.
// The following is retained from previous revisions as a fallback,
// in case fromXml fails but there is a match to be found in the tags from the chord list.
d = ha->fromXml(kind, degreeList);
if (d == 0) {
QString degrees;
foreach(const HDegree &d, degreeList) {
@ -5182,7 +5196,6 @@ void MusicXml::xmlHarmony(QDomElement e, int tick, Measure* measure, int staff)
}
qDebug("unknown chord txt: <%s> kind: <%s> degrees: %s",
qPrintable(kindText), qPrintable(kind), qPrintable(degrees));
// Strategy II: lookup "kind", merge in degree list and try to find
// harmony in list
@ -5205,6 +5218,8 @@ void MusicXml::xmlHarmony(QDomElement e, int tick, Measure* measure, int staff)
// ha->resolveDegreeList();
ha->render();
}
}
#endif
ha->setVisible(printObject == "yes");
// TODO-LV: do this only if ha points to a valid harmony

View file

@ -47,7 +47,7 @@
<root>
<root-step>G</root-step>
</root>
<kind text="">major</kind>
<kind>major</kind>
</harmony>
<note>
<pitch>
@ -68,7 +68,7 @@
<root>
<root-step>C</root-step>
</root>
<kind text="">major</kind>
<kind>major</kind>
<frame>
<frame-strings>6</frame-strings>
<frame-frets>5</frame-frets>

View file

@ -46,7 +46,7 @@
<root>
<root-step>C</root-step>
</root>
<kind text="">major</kind>
<kind>major</kind>
</harmony>
<note>
<pitch>
@ -102,7 +102,7 @@
<root>
<root-step>C</root-step>
</root>
<kind text="+">augmented</kind>
<kind use-symbols="yes">augmented</kind>
</harmony>
<note>
<pitch>

View file

@ -74,7 +74,7 @@
<root>
<root-step>C</root-step>
</root>
<kind text="omit5">major</kind>
<kind>major</kind>
<degree>
<degree-value>5</degree-value>
<degree-alter>0</degree-alter>
@ -112,7 +112,7 @@
<root>
<root-step>C</root-step>
</root>
<kind text="2">major</kind>
<kind>major</kind>
<degree>
<degree-value>2</degree-value>
<degree-alter>0</degree-alter>
@ -150,7 +150,7 @@
<root>
<root-step>C</root-step>
</root>
<kind text="Maj7#5">major-seventh</kind>
<kind text="Maj7">major-seventh</kind>
<degree>
<degree-value>5</degree-value>
<degree-alter>1</degree-alter>
@ -189,7 +189,7 @@
<root-step>E</root-step>
<root-alter>-1</root-alter>
</root>
<kind text="9sus">suspended-fourth</kind>
<kind text="sus">suspended-fourth</kind>
<degree>
<degree-value>7</degree-value>
<degree-alter>0</degree-alter>

View file

@ -1129,7 +1129,7 @@
<chord id="213">
<name>add2</name>
<xml>major</xml>
<degree>add9</degree>
<degree>add2</degree>
<voicing>C D E G</voicing>
</chord>
<chord id="214">