Skip to content
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
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__ = '11.0.0b6'
__version__ = '11.0.0b7'

def get_version_tuple(vv):
v = vv.split('.')
Expand Down
2 changes: 1 addition & 1 deletion music21/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<class 'music21.base.Music21Object'>

>>> music21.VERSION_STR
'11.0.0b6'
'11.0.0b7'

Alternatively, after doing a complete import, these classes are available
under the module "base":
Expand Down
41 changes: 41 additions & 0 deletions music21/musicxml/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,47 @@ def insertBeforeElements(root, insert, tagList=None):
root.insert(min(insertIndices), insert)


def insertAfterElements(root, insert, tagList=None):
# noinspection PyShadowingNames
'''
Insert element `insert` into element `root` directly after the last
instance of any child tag given in `tagList`. Insert at the beginning
if `tagList` is `None` or no instance is found.

>>> from xml.etree.ElementTree import fromstring as EL
>>> from music21.musicxml.helpers import insertAfterElements, dump
>>> root = EL('<note><grace /><voice>1</voice></note>')
>>> insert = EL('<unpitched/>')

>>> insertAfterElements(root, insert, tagList=['grace'])
>>> dump(root)
<note>
<grace />
<unpitched />
<voice>1</voice>
</note>

Insert at the beginning when nothing in `tagList` matches:

>>> insert2 = EL('<foo/>')
>>> insertAfterElements(root, insert2, tagList=['bar'])
>>> dump(root)
<note>
<foo />
<grace />
<unpitched />
<voice>1</voice>
</note>
'''
insertIndex = 0
if tagList:
# Iterate children only, not grandchildren
for i, child in enumerate(root.findall('*')):
if child.tag in tagList:
insertIndex = i + 1
root.insert(insertIndex, insert)


def measureNumberComesBefore(mNum1: str, mNum2: str) -> bool:
'''
Determine whether `measureNumber1` strictly precedes
Expand Down
25 changes: 24 additions & 1 deletion music21/musicxml/m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -4622,14 +4622,37 @@ def unpitchedToXml(self,
</unpitched>
<type>quarter</type>
</note>

<unpitched> must come before <voice>, even when there is
no <duration>, as on a grace note in a voice:

>>> MEX.currentVoiceId = 1
>>> mxUnpitched = MEX.unpitchedToXml(graceUp)
>>> MEX.dump(mxUnpitched)
<note>
<grace slash="yes" />
<unpitched>
<display-step>D</display-step>
<display-octave>5</display-octave>
</unpitched>
<voice>1</voice>
<type>quarter</type>
</note>
'''
mxNote = self.noteToXml(up, noteIndexInChord=noteIndexInChord, chordParent=chordParent)

mxUnpitched = Element('unpitched')
_setTagTextFromAttribute(up, mxUnpitched, 'display-step')
_setTagTextFromAttribute(up, mxUnpitched, 'display-octave')

helpers.insertBeforeElements(mxNote, mxUnpitched, tagList=['duration', 'type'])
# within <note>, only <grace>, <cue>, and <chord> may precede
# <unpitched>, so insert after those rather than trying to list
# everything that can follow it (#1732)
helpers.insertAfterElements(
mxNote,
mxUnpitched,
tagList=['grace', 'cue', 'chord']
)

return mxNote

Expand Down
15 changes: 15 additions & 0 deletions music21/musicxml/test_m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,21 @@ def testLowVoiceNumbers(self):
xmlOut = self.getXml(m)
self.assertIn('<voice>hello</voice>', xmlOut)

def testUnpitchedGraceInVoice(self):
'''
<unpitched> must precede <voice> even on a grace note, which has
no <duration> element to insert before.
'''
# https://github.com/cuthbertLab/music21/issues/1732
grace = note.Unpitched(duration=duration.GraceDuration())
v1 = stream.Voice([grace])
v2 = stream.Voice([note.Note()])
m = stream.Measure([v1, v2])

tree = self.getET(m)
mxNote = tree.find('part/measure/note')
self.assertEqual([el.tag for el in mxNote], ['grace', 'unpitched', 'voice', 'type'])

def testVoiceNumberOffsetsThreeStaffsInGroup(self):
n1_1 = note.Note()
v1_1 = stream.Voice([n1_1])
Expand Down
Loading