Skip to content

Commit

Permalink
Merge pull request #1445 from cuthbertLab/rn_write_as_chord
Browse files Browse the repository at this point in the history
romanText/numerals substantial improvements; MXL4.0
  • Loading branch information
mscuthbert authored Sep 28, 2022
2 parents 210ae11 + 676b36d commit 076879d
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 38 deletions.
2 changes: 1 addition & 1 deletion music21/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
'''
from __future__ import annotations

__version__ = '9.0.0a1'
__version__ = '9.0.0a2'


def get_version_tuple(vv):
Expand Down
2 changes: 1 addition & 1 deletion music21/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<class 'music21.base.Music21Object'>
>>> music21.VERSION_STR
'9.0.0a1'
'9.0.0a2'
Alternatively, after doing a complete import, these classes are available
under the module "base":
Expand Down
2 changes: 1 addition & 1 deletion music21/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class DefaultsException(Exception):
title = 'Music21 Fragment'
author = 'Music21'
software = 'music21 v.' + _version.__version__ # used in xml encoding source software
musicxmlVersion = '3.1'
musicxmlVersion = '4.0'

meterNumerator = 4
meterDenominator = 'quarter'
Expand Down
6 changes: 3 additions & 3 deletions music21/harmony.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ def romanNumeral(self):
if not self.pitches:
return roman.RomanNumeral()


# what is manipulating this so that write as chord matters?
storedWriteAsChord = self._writeAsChord
self.writeAsChord = True
if self.key is None:
Expand Down Expand Up @@ -760,7 +760,7 @@ def changeAbbreviationFor(chordType, changeTo):
CHORD_TYPES[chordType][1].insert(0, changeTo)


def chordSymbolFigureFromChord(inChord, includeChordType=False):
def chordSymbolFigureFromChord(inChord: chord.Chord, includeChordType=False):
# noinspection SpellCheckingInspection
'''
Analyze the given chord, and attempt to describe its pitches using a
Expand Down Expand Up @@ -1323,7 +1323,7 @@ def convertFBNotationStringToDegrees(innerKind, fbNotation):
return cs


def chordSymbolFromChord(inChord):
def chordSymbolFromChord(inChord: chord.Chord) -> ChordSymbol:
'''
Get the :class:`~music21.harmony.chordSymbol` object from the chord, using
:meth:`music21.harmony.Harmony.chordSymbolFigureFromChord`
Expand Down
177 changes: 167 additions & 10 deletions music21/musicxml/m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@

environLocal = environment.Environment('musicxml.m21ToXml')


if t.TYPE_CHECKING:
from music21 import roman

# ------------------------------------------------------------------------------

def typeToMusicXMLType(value):
Expand Down Expand Up @@ -2668,7 +2672,6 @@ def __init__(self,

# The staffGroup to which this part belongs (if it belongs to one)
self.staffGroup: t.Optional[layout.StaffGroup] = None

self.spannerBundle = partObj.spannerBundle

def parse(self):
Expand Down Expand Up @@ -3099,8 +3102,9 @@ class MeasureExporter(XMLExporterBase):
[
('Note', 'noteToXml'),
('NoChord', 'noChordToXml'),
('ChordWithFretBoard', 'chordWithFretBoardToXml'),
('ChordSymbol', 'chordSymbolToXml'),
('ChordWithFretBoard', 'chordWithFretBoardToXml'), # these three
('ChordSymbol', 'chordSymbolToXml'), # must come before
('RomanNumeral', 'romanNumeralToXml'), # ChordBase
('ChordBase', 'chordToXml'),
('Unpitched', 'unpitchedToXml'),
('Rest', 'restToXml'),
Expand Down Expand Up @@ -3252,9 +3256,12 @@ def parseFlatElements(self, m, *, backupAfterwards=False):

notesForLater = []
for obj in objGroup:
# we do all non-note elements (including ChordSymbols)
# we do all non-note elements (including ChordSymbols not written as chord)
# first before note elements, in musicxml
if isinstance(obj, note.GeneralNote) and not isinstance(obj, harmony.Harmony):
if isinstance(obj, note.GeneralNote) and (
not (isHarm := isinstance(obj, harmony.Harmony))
or (isHarm and obj.writeAsChord)
):
notesForLater.append(obj)
else:
self.parseOneElement(obj)
Expand Down Expand Up @@ -4143,7 +4150,7 @@ def restToXml(self, r: note.Rest):

return mxNote

def chordToXml(self, c: chord.ChordBase):
def chordToXml(self, c: chord.ChordBase) -> list[Element]:
# noinspection PyShadowingNames
'''
Returns a list of <note> tags, all but the first with a <chord/> tag on them.
Expand Down Expand Up @@ -5333,10 +5340,159 @@ def noChordToXml(self, cs: harmony.NoChord):
self.xmlRoot.append(mxHarmony)
return mxHarmony

def chordSymbolToXml(self, cs):
def romanNumeralToXml(self, rn: roman.RomanNumeral) -> t.Union[Element, list[Element]]:
'''
Convert a RomanNumeral object to either a chord (if .writeAsChord is True)
or a Harmony XML Element.
>>> rnI = roman.RomanNumeral('I', 'C')
>>> MEX = musicxml.m21ToXml.MeasureExporter()
>>> MEX.currentDivisions = 10
>>> listMxChords = MEX.romanNumeralToXml(rnI)
>>> len(listMxChords)
3
>>> MEX.dump(listMxChords[1])
<note>
<chord />
<pitch>
<step>E</step>
<octave>4</octave>
</pitch>
<duration>10</duration>
<type>quarter</type>
</note>
If writeAsChord is False, we create a MusicXML 4.0 <numeral> tag.
This does not work in the current version of MuseScore (which only
supports MusicXML 3.1) but outputs decently well in Finale.
>>> rnI.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(rnI)
>>> MEX.dump(mxHarmonyFromRN)
<harmony>
<numeral>
<numeral-root text="I">1</numeral-root>
<numeral-key print-object="no">
<numeral-fifths>0</numeral-fifths>
<numeral-mode>major</numeral-mode>
</numeral-key>
</numeral>
<kind>major</kind>
</harmony>
>>> complexRn = roman.RomanNumeral('#iio65', 'e-')
>>> complexRn.followsKeyChange = True
>>> complexRn.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(complexRn)
>>> MEX.dump(mxHarmonyFromRN)
<harmony>
<numeral>
<numeral-root text="#iio65">2</numeral-root>
<numeral-alter location="left">1.0</numeral-alter>
<numeral-key>
<numeral-fifths>-6</numeral-fifths>
<numeral-mode>minor</numeral-mode>
</numeral-key>
</numeral>
<kind>diminished-seventh</kind>
<inversion>1</inversion>
</harmony>
>>> maj6 = roman.RomanNumeral('VI7', 'd')
>>> maj6.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(maj6)
>>> MEX.dump(mxHarmonyFromRN)
<harmony>
<numeral>
<numeral-root text="VI7">6</numeral-root>
<numeral-key print-object="no">
<numeral-fifths>-1</numeral-fifths>
<numeral-mode>natural minor</numeral-mode>
</numeral-key>
</numeral>
<kind>major-seventh</kind>
</harmony>
>>> min6 = roman.RomanNumeral('vi', 'd')
>>> min6.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(min6)
>>> mxHarmonyFromRN.find('.//numeral-mode').text
'melodic minor'
>>> dim7 = roman.RomanNumeral('viiø65', 'd')
>>> dim7.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(dim7)
>>> mxHarmonyFromRN.find('.//numeral-mode').text
'harmonic minor'
>>> mxHarmonyFromRN.find('kind').text
'half-diminished'
>>> maj7 = roman.RomanNumeral('VII64', 'd')
>>> maj7.writeAsChord = False
>>> mxHarmonyFromRN = MEX.romanNumeralToXml(maj7)
>>> mxHarmonyFromRN.find('.//numeral-mode').text
'natural minor'
'''
if rn.writeAsChord is True:
return self.chordToXml(rn)

# because parsing "kind" is very hard, it's easier to
# create a new chordSymbol in order to get the musicxml "kind"
# a little slower than needs to be.
cs = harmony.chordSymbolFromChord(rn)
cs.offset = rn.offset # needed for not getting an extra offset tag w/ forward.
mxHarmony = self.chordSymbolToXml(cs, append=False)
mxRoot = mxHarmony.find('root')
mxHarmony.remove(mxRoot)
mxBass = mxHarmony.find('bass')
if mxBass is not None:
mxHarmony.remove(mxBass)

# use v4 RomanNumerals
mxNumeral = Element('numeral')
mxNumeralRoot = SubElement(mxNumeral, 'numeral-root')
mxNumeralRoot.set('text', rn.primaryFigure)
mxNumeralRoot.text = str(rn.scaleDegree)
if rn.frontAlterationAccidental:
mxNumeralAlter = SubElement(mxNumeral, 'numeral-alter')
# float is allowed
mxNumeralAlter.text = str(rn.frontAlterationAccidental.alter)
mxNumeralAlter.set('location', 'left')
if rn.key:
mxNumeralKey = SubElement(mxNumeral, 'numeral-key')
if not rn.followsKeyChange:
mxNumeralKey.set('print-object', 'no')
mxNumeralFifths = SubElement(mxNumeralKey, 'numeral-fifths')
mxNumeralFifths.text = str(rn.key.sharps)
mxNumeralMode = SubElement(mxNumeralKey, 'numeral-mode')
modeText = ''
if rn.key.mode == 'major':
modeText = 'major'
elif rn.scaleDegree not in (6, 7):
modeText = 'minor'
elif rn.scaleDegree == 6:
# simplest way to figure this out.
if (rn.root().pitchClass - rn.key.tonic.pitchClass) % 12 == 8:
modeText = 'natural minor'
else:
modeText = 'melodic minor'
else:
if (rn.root().pitchClass - rn.key.tonic.pitchClass) % 12 == 10:
modeText = 'natural minor'
else:
modeText = 'harmonic minor'
mxNumeralMode.text = modeText

mxHarmony.insert(0, mxNumeral)
self.xmlRoot.append(mxHarmony)
return mxHarmony

def chordSymbolToXml(self, cs: harmony.ChordSymbol, *, append=True):
# noinspection PyShadowingNames
'''
Convert a ChordSymbol object to an mxHarmony object.
Convert a ChordSymbol object to either a chord (if .writeAsChord is True)
or a Harmony XML Element.
>>> cs = harmony.ChordSymbol()
>>> cs.root('E-')
Expand Down Expand Up @@ -5489,7 +5645,7 @@ def chordSymbolToXml(self, cs):
return

mxKind = SubElement(mxHarmony, 'kind')
cKind = cs.chordKind
cKind = cs.chordKind or 'none'
for xmlAlias in harmony.CHORD_ALIASES:
if harmony.CHORD_ALIASES[xmlAlias] == cKind:
cKind = xmlAlias
Expand Down Expand Up @@ -5546,7 +5702,8 @@ def chordSymbolToXml(self, cs):
self.setEditorial(mxHarmony, cs)
# staff: see joinPartStaffs()

self.xmlRoot.append(mxHarmony)
if append:
self.xmlRoot.append(mxHarmony)
return mxHarmony

def setOffsetOptional(self, m21Obj, mxObj=None, *, setSound=True):
Expand Down
35 changes: 35 additions & 0 deletions music21/musicxml/test_m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,41 @@ def test_inexpressible_hidden_rests_become_forward_tags(self):
# No <forward> tag is necessary to finish the incomplete measure (3.9979 -> 4.0)
self.assertEqual(len(tree.findall('.//forward')), 1)

def test_roman_musicxml_two_kinds(self):
from music21.roman import RomanNumeral

# normal roman numerals take up no time in xml output.
rn1 = RomanNumeral('I', 'C')
rn1.duration.type = 'half'
rn2 = RomanNumeral('V', 'C')
rn2.duration.type = 'half'

m = stream.Measure()
m.insert(0.0, rn1)
m.insert(2.0, rn2)

# with writeAsChord=True, they get their own Chord objects and durations.
self.assertTrue(rn1.writeAsChord)
xmlOut = GeneralObjectExporter().parse(m).decode('utf-8')
self.assertNotIn('<forward>', xmlOut)
self.assertIn('<chord', xmlOut)

rn1.writeAsChord = False
rn2.writeAsChord = False
xmlOut = GeneralObjectExporter().parse(m).decode('utf-8')
self.assertIn('<numeral', xmlOut)
self.assertIn('<forward>', xmlOut)
self.assertNotIn('<chord', xmlOut)
self.assertNotIn('<offset', xmlOut)
self.assertNotIn('<rest', xmlOut)

rn1.duration.quarterLength = 0.0
rn2.duration.quarterLength = 0.0
xmlOut = GeneralObjectExporter().parse(m).decode('utf-8')
self.assertIn('<rest', xmlOut)
self.assertNotIn('<forward>', xmlOut)



class TestExternal(unittest.TestCase):
show = True
Expand Down
2 changes: 1 addition & 1 deletion music21/musicxml/xmlToM21.py
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ def __init__(self):
self.partGroupList = []
self.parts = []

self.musicXmlVersion = '3.1'
self.musicXmlVersion = defaults.musicxmlVersion

def scoreFromFile(self, filename):
'''
Expand Down
6 changes: 4 additions & 2 deletions music21/roman.py
Original file line number Diff line number Diff line change
Expand Up @@ -2246,6 +2246,7 @@ def __init__(
updatePitches=True,
sixthMinor=Minor67Default.QUALITY,
seventhMinor=Minor67Default.QUALITY,
**keywords,
):
self.primaryFigure: str = ''
self.secondaryRomanNumeral: t.Optional[RomanNumeral] = None
Expand Down Expand Up @@ -2301,10 +2302,11 @@ def __init__(
self.sixthMinor = sixthMinor
self.seventhMinor = seventhMinor

super().__init__(figure, updatePitches=updatePitches)
super().__init__(figure, updatePitches=updatePitches, **keywords)
self.writeAsChord = True # override from Harmony/ChordSymbol
self._parsingComplete = True
self._functionalityScore: t.Optional[int] = None
self.editorial.followsKeyChange = False
self.followsKeyChange = False

# SPECIAL METHODS #

Expand Down
Loading

0 comments on commit 076879d

Please sign in to comment.