From 3525fc01b1cc63d1ce13640a191b181b324ae85c Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Sat, 26 Jul 2025 10:56:54 -0700 Subject: [PATCH 1/6] Two fixes in parseFlatElements: (1) prefer a gap to a complex duration hidden rest, not just for an inexpressible duration hidden rest. (2) jump to end of measure needs to pass the distance to end of measure, not the offset of end of measure. --- music21/_version.py | 2 +- music21/base.py | 2 +- music21/musicxml/m21ToXml.py | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/music21/_version.py b/music21/_version.py index 8f0c843b0..d84db94b3 100644 --- a/music21/_version.py +++ b/music21/_version.py @@ -50,7 +50,7 @@ ''' from __future__ import annotations -__version__ = '9.7.1' +__version__ = '9.7.2a4' def get_version_tuple(vv): v = vv.split('.') diff --git a/music21/base.py b/music21/base.py index dcddb37f4..d7770cadf 100644 --- a/music21/base.py +++ b/music21/base.py @@ -27,7 +27,7 @@ >>> music21.VERSION_STR -'9.7.1' +'9.7.2a4' 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 b8a21c1f3..3c7f1a578 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -38,6 +38,7 @@ from music21 import chord from music21 import common from music21.common.enums import AppendSpanners +from music21.common.numberTools import opFrac from music21 import defaults from music21 import duration from music21 import dynamics @@ -3277,7 +3278,9 @@ def parseFlatElements( self.parseOneElement(obj, AppendSpanners.NORMAL) for n in notesForLater: - if n.isRest and n.style.hideObjectOnPrint and n.duration.type == 'inexpressible': + if (n.isRest + and n.style.hideObjectOnPrint + and n.duration.type in ('inexpressible', 'complex')): # Prefer a gap in stream, to be filled with a tag by # fill_gap_with_forward_tag() rather than raising exceptions continue @@ -3322,7 +3325,7 @@ def parseFlatElements( else: # if necessary, jump to end of the measure. if self.offsetInMeasure < firstPassEndOffsetInMeasure: - self.moveForward(firstPassEndOffsetInMeasure) + self.moveForward(opFrac(firstPassEndOffsetInMeasure - self.offsetInMeasure)) self.currentVoiceId = None From 33a8a91c280b147ba75ce7b8ab9acd53dba43033 Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:52:05 -0700 Subject: [PATCH 2/6] Add test for MusicXML write of complex hidden rests (used to crash, now creates forward tag). --- music21/musicxml/test_m21ToXml.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py index 0c5466c70..1863e45eb 100644 --- a/music21/musicxml/test_m21ToXml.py +++ b/music21/musicxml/test_m21ToXml.py @@ -695,6 +695,27 @@ def test_instrumentDoesNotCreateForward(self): self.assertTrue(tree.findall('.//note')) self.assertFalse(tree.findall('.//forward')) + def test_complexHiddenRestDoesNotCrashButInsteadCreatesForward(self): + ''' + Complex hidden rests were raising an exception. Now they create a forward tag instead. + ''' + complexRest = note.Rest() + complexRest.quarterLength = 5.0 + complexRest.style.hideObjectOnPrint = True + n = note.Note() + n.quarterLength = 1.0 + m = stream.Measure() + m.append(complexRest) + m.append(n) + p = stream.Part() + p.append(m) + s = stream.Score() + s.append(p) + tree = self.getET(s, makeNotation=False) + self.assertTrue(tree.findall('.//forward')) + self.assertTrue(tree.findall('.//note')) + self.assertFalse(tree.findall('.//rest')) + def testOutOfBoundsExpressionDoesNotCreateForward(self): ''' A metronome mark at an offset exceeding the bar duration was causing From 122842e8979c14640e9c66dd3609c4453e4d8778 Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:27:21 -0700 Subject: [PATCH 3/6] Update that last test to count instances of note/rest/forward. --- music21/musicxml/test_m21ToXml.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py index 1863e45eb..0d804baf6 100644 --- a/music21/musicxml/test_m21ToXml.py +++ b/music21/musicxml/test_m21ToXml.py @@ -712,9 +712,9 @@ def test_complexHiddenRestDoesNotCrashButInsteadCreatesForward(self): s = stream.Score() s.append(p) tree = self.getET(s, makeNotation=False) - self.assertTrue(tree.findall('.//forward')) - self.assertTrue(tree.findall('.//note')) - self.assertFalse(tree.findall('.//rest')) + self.assertEqual(len(tree.findall('.//forward')), 1) + self.assertEqual(len(tree.findall('.//note')), 1) + self.assertEqual(len(tree.findall('.//rest')), 0) def testOutOfBoundsExpressionDoesNotCreateForward(self): ''' From 28fc8af509061dabe0782217740a11a7ca04a718 Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:09:04 -0700 Subject: [PATCH 4/6] Check also that forward duration is 5 quarter notes. --- music21/musicxml/test_m21ToXml.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py index 0d804baf6..c6d38f3dc 100644 --- a/music21/musicxml/test_m21ToXml.py +++ b/music21/musicxml/test_m21ToXml.py @@ -712,9 +712,17 @@ def test_complexHiddenRestDoesNotCrashButInsteadCreatesForward(self): s = stream.Score() s.append(p) tree = self.getET(s, makeNotation=False) - self.assertEqual(len(tree.findall('.//forward')), 1) - self.assertEqual(len(tree.findall('.//note')), 1) + # There should be one forward and one note + forwardList = tree.findall('.//forward') + noteList = tree.findall('.//note') + self.assertEqual(len(forwardList), 1) + self.assertEqual(len(noteList), 1) + # There should be no rests self.assertEqual(len(tree.findall('.//rest')), 0) + # The forward duration should be 5x longer than quarter-note duration + noteDurEl = noteList[0].find('.//duration') + forwardDurEl = forwardList[0].find('.//duration') + self.assertEqual(int(forwardDurEl.text) / int(noteDurEl.text), 5) def testOutOfBoundsExpressionDoesNotCreateForward(self): ''' From 67458d0ba844591b3f96c399d92d06bddfa0c4e0 Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:27:36 -0700 Subject: [PATCH 5/6] Test for write from SpannerAnchor-y measure getting the end offset of the measure wrong. --- music21/musicxml/test_m21ToXml.py | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py index c6d38f3dc..8461de903 100644 --- a/music21/musicxml/test_m21ToXml.py +++ b/music21/musicxml/test_m21ToXml.py @@ -724,6 +724,60 @@ def test_complexHiddenRestDoesNotCrashButInsteadCreatesForward(self): forwardDurEl = forwardList[0].find('.//duration') self.assertEqual(int(forwardDurEl.text) / int(noteDurEl.text), 5) + def test_writeFromSpannerAnchorsGetsMeasureEndOffsetRight(self): + ''' + Write to MusicXML from a Measure containing SpannerAnchors was not positioning + the current time offset correctly before starting the next written measure. + Now the next measure is positioned at the correct offset. + ''' + m1 = stream.Measure() + m1.append(note.Note()) + m1.append(note.Note()) + m1.append(note.Note()) + m1.append(note.Note()) + cresc = dynamics.Crescendo() + startAnchor = spanner.SpannerAnchor() + endAnchor = spanner.SpannerAnchor() + m1.insert(0.5, startAnchor) + m1.insert(1.5, endAnchor) + cresc.addSpannedElements(startAnchor, endAnchor) + m1.append(cresc) + p = stream.Part() + p.append(m1) + s = stream.Score() + s.append(p) + # write to MusicXML + tree = self.getET(s) + + # walk all the durations (notes, forwards, backups) and make sure they add up + # to where the end of the measure should be (4.0ql) + measEl = None + divisionsEl = None + for el in tree.iter(): + if el.tag == 'measure': + measEl = el + for el in measEl.iter(): + if el.tag == 'divisions': + divisionsEl = el + break + break + + self.assertIsNotNone(measEl) + self.assertIsNotNone(divisionsEl) + + divisionsInt = int(divisionsEl.text) + currOffsetQL = 0. + for el in measEl.findall('*'): + dur = el.find('duration') + if dur is not None: + durInt = int(dur.text) + durQL = common.opFrac(fractions.Fraction(durInt, divisionsInt)) + if el.tag == 'backup': + currOffsetQL = common.opFrac(currOffsetQL - durQL) + else: + currOffsetQL = common.opFrac(currOffsetQL + durQL) + self.assertEqual(currOffsetQL, 4.) + def testOutOfBoundsExpressionDoesNotCreateForward(self): ''' A metronome mark at an offset exceeding the bar duration was causing From 2fcf8747a9e2a75add27cb9acccf8e992eb4cfd2 Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:45:45 -0700 Subject: [PATCH 6/6] Remove rejected fixup of complex duration rest (that's what makeNotation is for). --- music21/musicxml/m21ToXml.py | 4 +--- music21/musicxml/test_m21ToXml.py | 29 ----------------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 3c7f1a578..acb7a44a3 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -3278,9 +3278,7 @@ def parseFlatElements( self.parseOneElement(obj, AppendSpanners.NORMAL) for n in notesForLater: - if (n.isRest - and n.style.hideObjectOnPrint - and n.duration.type in ('inexpressible', 'complex')): + if n.isRest and n.style.hideObjectOnPrint and n.duration.type == 'inexpressible': # Prefer a gap in stream, to be filled with a tag by # fill_gap_with_forward_tag() rather than raising exceptions continue diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py index 8461de903..aaa1dbdc3 100644 --- a/music21/musicxml/test_m21ToXml.py +++ b/music21/musicxml/test_m21ToXml.py @@ -695,35 +695,6 @@ def test_instrumentDoesNotCreateForward(self): self.assertTrue(tree.findall('.//note')) self.assertFalse(tree.findall('.//forward')) - def test_complexHiddenRestDoesNotCrashButInsteadCreatesForward(self): - ''' - Complex hidden rests were raising an exception. Now they create a forward tag instead. - ''' - complexRest = note.Rest() - complexRest.quarterLength = 5.0 - complexRest.style.hideObjectOnPrint = True - n = note.Note() - n.quarterLength = 1.0 - m = stream.Measure() - m.append(complexRest) - m.append(n) - p = stream.Part() - p.append(m) - s = stream.Score() - s.append(p) - tree = self.getET(s, makeNotation=False) - # There should be one forward and one note - forwardList = tree.findall('.//forward') - noteList = tree.findall('.//note') - self.assertEqual(len(forwardList), 1) - self.assertEqual(len(noteList), 1) - # There should be no rests - self.assertEqual(len(tree.findall('.//rest')), 0) - # The forward duration should be 5x longer than quarter-note duration - noteDurEl = noteList[0].find('.//duration') - forwardDurEl = forwardList[0].find('.//duration') - self.assertEqual(int(forwardDurEl.text) / int(noteDurEl.text), 5) - def test_writeFromSpannerAnchorsGetsMeasureEndOffsetRight(self): ''' Write to MusicXML from a Measure containing SpannerAnchors was not positioning