From c9ffb2af53e333d0c2df959a95ec9cf953c9f4ba Mon Sep 17 00:00:00 2001 From: pablopupo <145598901+pablopupo@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:28:48 -0700 Subject: [PATCH 1/2] musicxml: place before on grace notes unpitchedToXml inserted before the earliest of or , but a grace note has no , so anything noteToXml had appended in between (voice, tie, instrument, footnote, level) came out ahead of and the file failed schema validation. List every element that can follow within so the insertion point is right with or without a duration. Bump version to 11.0.0b7 for the musicxml writer change. Fixes #1732. --- music21/_version.py | 2 +- music21/base.py | 2 +- music21/musicxml/m21ToXml.py | 24 +++++++++++++++++++++++- music21/musicxml/test_m21ToXml.py | 15 +++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/music21/_version.py b/music21/_version.py index eed25cb593..d45d6c0cd6 100644 --- a/music21/_version.py +++ b/music21/_version.py @@ -47,7 +47,7 @@ ''' from __future__ import annotations -__version__ = '11.0.0b6' +__version__ = '11.0.0b7' def get_version_tuple(vv): v = vv.split('.') diff --git a/music21/base.py b/music21/base.py index b5fc735fba..85ae8bbe72 100644 --- a/music21/base.py +++ b/music21/base.py @@ -26,7 +26,7 @@ >>> music21.VERSION_STR -'11.0.0b6' +'11.0.0b7' Alternatively, after doing a complete import, these classes are available under the module "base": diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 5c525dacee..23eb24d61c 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -4622,6 +4622,22 @@ def unpitchedToXml(self, quarter + + must come before , even when there is + no , as on a grace note in a voice: + + >>> MEX.currentVoiceId = 1 + >>> mxUnpitched = MEX.unpitchedToXml(graceUp) + >>> MEX.dump(mxUnpitched) + + + + D + 5 + + 1 + quarter + ''' mxNote = self.noteToXml(up, noteIndexInChord=noteIndexInChord, chordParent=chordParent) @@ -4629,7 +4645,13 @@ def unpitchedToXml(self, _setTagTextFromAttribute(up, mxUnpitched, 'display-step') _setTagTextFromAttribute(up, mxUnpitched, 'display-octave') - helpers.insertBeforeElements(mxNote, mxUnpitched, tagList=['duration', 'type']) + # anything that can follow within must be listed + # here, since a grace note has no to insert before (#1732) + helpers.insertBeforeElements( + mxNote, + mxUnpitched, + tagList=['duration', 'tie', 'instrument', 'footnote', 'level', 'voice', 'type'] + ) return mxNote diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py index 5e0e47fc41..fc98f8798f 100644 --- a/music21/musicxml/test_m21ToXml.py +++ b/music21/musicxml/test_m21ToXml.py @@ -229,6 +229,21 @@ def testLowVoiceNumbers(self): xmlOut = self.getXml(m) self.assertIn('hello', xmlOut) + def testUnpitchedGraceInVoice(self): + ''' + must precede even on a grace note, which has + no 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]) From 800cab138947422cf94362755af3a2fba847028d Mon Sep 17 00:00:00 2001 From: pablopupo <145598901+pablopupo@users.noreply.github.com> Date: Sun, 5 Jul 2026 08:49:56 -0700 Subject: [PATCH 2/2] musicxml: insert directly after per review Only , , and may precede within , so a new insertAfterElements helper places it after those instead of maintaining a list of everything that can follow it. --- music21/musicxml/helpers.py | 41 ++++++++++++++++++++++++++++++++++++ music21/musicxml/m21ToXml.py | 9 ++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/music21/musicxml/helpers.py b/music21/musicxml/helpers.py index 804cb0c7c7..9722295cc6 100644 --- a/music21/musicxml/helpers.py +++ b/music21/musicxml/helpers.py @@ -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('1') + >>> insert = EL('') + + >>> insertAfterElements(root, insert, tagList=['grace']) + >>> dump(root) + + + + 1 + + + Insert at the beginning when nothing in `tagList` matches: + + >>> insert2 = EL('') + >>> insertAfterElements(root, insert2, tagList=['bar']) + >>> dump(root) + + + + + 1 + + ''' + 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 diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 23eb24d61c..5098428eb1 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -4645,12 +4645,13 @@ def unpitchedToXml(self, _setTagTextFromAttribute(up, mxUnpitched, 'display-step') _setTagTextFromAttribute(up, mxUnpitched, 'display-octave') - # anything that can follow within must be listed - # here, since a grace note has no to insert before (#1732) - helpers.insertBeforeElements( + # within , only , , and may precede + # , so insert after those rather than trying to list + # everything that can follow it (#1732) + helpers.insertAfterElements( mxNote, mxUnpitched, - tagList=['duration', 'tie', 'instrument', 'footnote', 'level', 'voice', 'type'] + tagList=['grace', 'cue', 'chord'] ) return mxNote