From 428f7d19ec022443e05977141d8bfba39244e46a Mon Sep 17 00:00:00 2001 From: Tim Beyer <35711942+TimFelixBeyer@users.noreply.github.com> Date: Tue, 25 Jul 2023 22:35:31 +0200 Subject: [PATCH 1/5] Deal with small MusicXML offsets --- music21/musicxml/xmlToM21.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index c9d571185..b59625956 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -31,6 +31,7 @@ from music21 import duration from music21 import dynamics from music21.common.enums import OrnamentDelay +from music21.common.numberTools import opFrac, nearestMultiple from music21 import editorial from music21 import environment from music21 import exceptions21 @@ -2190,9 +2191,22 @@ def adjustTimeAttributesFromMeasure(self, m: stream.Measure): else: lastTimeSignatureQuarterLength = 4.0 # sensible default. - if mHighestTime >= lastTimeSignatureQuarterLength: + if mHighestTime == lastTimeSignatureQuarterLength: mOffsetShift = mHighestTime - + elif mHighestTime > lastTimeSignatureQuarterLength: + diff = mHighestTime - lastTimeSignatureQuarterLength + tol = 1e-6 + # If the measure is overfull by a "round" amount, assume that it was intended + # otherwise it was likely the result of malformed MusicXML. + if (diff > 0.5 + or nearestMultiple(diff, 0.0625)[1] < tol + or nearestMultiple(diff, opFrac(1/12))[1] < tol): + mOffsetShift = mHighestTime + else: + mOffsetShift = lastTimeSignatureQuarterLength + warnings.warn(f"""Warning: measure {m.number} in part {self.stream.partName} + is overfull: {mHighestTime} > {lastTimeSignatureQuarterLength}, + assuming {mOffsetShift} is correct.""") elif (mHighestTime == 0.0 and not m.recurse().notesAndRests.getElementsNotOfClass('Harmony') ): @@ -2204,7 +2218,6 @@ def adjustTimeAttributesFromMeasure(self, m: stream.Measure): m.insert(0.0, r) mOffsetShift = lastTimeSignatureQuarterLength self.lastMeasureWasShort = False - else: # use time signature # for the first measure, this may be a pickup # must detect this when writing, as next measures offsets will be @@ -2216,7 +2229,6 @@ def adjustTimeAttributesFromMeasure(self, m: stream.Measure): # environLocal.printDebug(['incompletely filled Measure found on musicxml # import; interpreting as an anacrusis:', 'paddingLeft:', m.paddingLeft]) mOffsetShift = mHighestTime - else: mOffsetShift = mHighestTime # lastTimeSignatureQuarterLength if self.lastMeasureWasShort is True: @@ -2596,9 +2608,7 @@ def xmlBackup(self, mxObj: ET.Element): ''' mxDuration = mxObj.find('duration') if durationText := strippedText(mxDuration): - change = common.numberTools.opFrac( - float(durationText) / self.divisions - ) + change = opFrac(float(durationText) / self.divisions) self.offsetMeasureNote -= change # check for negative offsets produced by # musicxml durations with float rounding issues @@ -2611,9 +2621,7 @@ def xmlForward(self, mxObj: ET.Element): ''' mxDuration = mxObj.find('duration') if durationText := strippedText(mxDuration): - change = common.numberTools.opFrac( - float(durationText) / self.divisions - ) + change = opFrac(float(durationText) / self.divisions) # Create hidden rest (in other words, a spacer) # old Finale documents close incomplete final measures with @@ -3578,7 +3586,7 @@ def xmlToDuration(self, mxNote, inputM21=None): mxDuration = mxNote.find('duration') if mxDuration is not None: noteDivisions = float(mxDuration.text.strip()) - qLen = common.numberTools.opFrac(noteDivisions / divisions) + qLen = opFrac(noteDivisions / divisions) else: qLen = 0.0 @@ -5510,7 +5518,7 @@ def parseAttributesTag(self, mxAttributes): meth(mxSub) # NOT to be done: directive -- deprecated since v2. elif tag == 'divisions': - self.divisions = common.opFrac(float(mxSub.text)) + self.divisions = opFrac(float(mxSub.text)) # TODO: musicxml4: for-part including part-clef # TODO: instruments -- int if more than one instrument plays most of the time # TODO: part-symbol From 2b4cf5d050e63a0d299f8c237e2d2f8491df8563 Mon Sep 17 00:00:00 2001 From: Tim Beyer <35711942+TimFelixBeyer@users.noreply.github.com> Date: Tue, 25 Jul 2023 22:37:06 +0200 Subject: [PATCH 2/5] Add test for measure length --- music21/musicxml/test_xmlToM21.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index 4847c1d16..b7194f1b0 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -1504,6 +1504,34 @@ def testImportImplicitMeasureNumber(self): m = s[stream.Measure].first() self.assertIs(m.showNumber, stream.enums.ShowNumber.NEVER) + def testAdjustTimeAttributesFromMeasure(self): + # Ignore import artifacts: + d = duration.Duration(3 + 3/480) + m = stream.Measure([meter.TimeSignature('6/8'), note.Note(duration=d)]) + PP = PartParser() + PP.lastMeasureOffset = 21.0 + PP.setLastMeasureInfo(m) + with self.assertWarns(MusicXMLWarning): + PP.adjustTimeAttributesFromMeasure(m) + self.assertEqual(PP.lastMeasureOffset, 24.0) + + # Keep 'round' overful measures and extremely overful measures, as they were + # likely intentional. + d = duration.Duration(3.125) + m = stream.Measure([meter.TimeSignature('6/8'), note.Note(duration=d)]) + PP = PartParser() + PP.lastMeasureOffset = 21.0 + PP.setLastMeasureInfo(m) + PP.adjustTimeAttributesFromMeasure(m) + self.assertEqual(PP.lastMeasureOffset, 24.125) + + d = duration.Duration(4.0) + m = stream.Measure([meter.TimeSignature('6/8'), note.Note(duration=d)]) + PP = PartParser() + PP.lastMeasureOffset = 21.0 + PP.setLastMeasureInfo(m) + PP.adjustTimeAttributesFromMeasure(m) + self.assertEqual(PP.lastMeasureOffset, 25.0) if __name__ == '__main__': import music21 From 1bc59ab7d668cc6277fdb8f8f527d99a86717d94 Mon Sep 17 00:00:00 2001 From: TimFelix <35711942+TimFelixBeyer@users.noreply.github.com> Date: Tue, 25 Jul 2023 22:59:04 +0200 Subject: [PATCH 3/5] Fix linting --- music21/musicxml/test_xmlToM21.py | 5 +++-- music21/musicxml/xmlToM21.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index b7194f1b0..dd252054d 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -1504,9 +1504,9 @@ def testImportImplicitMeasureNumber(self): m = s[stream.Measure].first() self.assertIs(m.showNumber, stream.enums.ShowNumber.NEVER) - def testAdjustTimeAttributesFromMeasure(self): + def testAdjustTimeAttributesFromMeasure(self): # Ignore import artifacts: - d = duration.Duration(3 + 3/480) + d = duration.Duration(3 + 3 / 480) m = stream.Measure([meter.TimeSignature('6/8'), note.Note(duration=d)]) PP = PartParser() PP.lastMeasureOffset = 21.0 @@ -1533,6 +1533,7 @@ def testAdjustTimeAttributesFromMeasure(self): PP.adjustTimeAttributesFromMeasure(m) self.assertEqual(PP.lastMeasureOffset, 25.0) + if __name__ == '__main__': import music21 music21.mainTest(Test) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index b59625956..aaae96a7e 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -2198,15 +2198,17 @@ def adjustTimeAttributesFromMeasure(self, m: stream.Measure): tol = 1e-6 # If the measure is overfull by a "round" amount, assume that it was intended # otherwise it was likely the result of malformed MusicXML. - if (diff > 0.5 - or nearestMultiple(diff, 0.0625)[1] < tol - or nearestMultiple(diff, opFrac(1/12))[1] < tol): + if (diff > 0.5 + or nearestMultiple(diff, 0.0625)[1] < tol + or nearestMultiple(diff, opFrac(1 / 12))[1] < tol): mOffsetShift = mHighestTime else: mOffsetShift = lastTimeSignatureQuarterLength - warnings.warn(f"""Warning: measure {m.number} in part {self.stream.partName} - is overfull: {mHighestTime} > {lastTimeSignatureQuarterLength}, - assuming {mOffsetShift} is correct.""") + warnings.warn( + f"Warning: measure {m.number} in part {self.stream.partName}" + f"is overfull: {mHighestTime} > {lastTimeSignatureQuarterLength}," + f"assuming {mOffsetShift} is correct." + ) elif (mHighestTime == 0.0 and not m.recurse().notesAndRests.getElementsNotOfClass('Harmony') ): From fea95c6295bee86feecc367c2cc8de842ceb6fc3 Mon Sep 17 00:00:00 2001 From: TimFelix <35711942+TimFelixBeyer@users.noreply.github.com> Date: Tue, 25 Jul 2023 23:20:43 +0200 Subject: [PATCH 4/5] Fix warning + increment version --- music21/_version.py | 2 +- music21/base.py | 2 +- music21/musicxml/xmlToM21.py | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/music21/_version.py b/music21/_version.py index c5a4c6fd4..518c9a43a 100644 --- a/music21/_version.py +++ b/music21/_version.py @@ -47,7 +47,7 @@ ''' from __future__ import annotations -__version__ = '9.2.0b1' +__version__ = '9.2.0b2' def get_version_tuple(vv): v = vv.split('.') diff --git a/music21/base.py b/music21/base.py index f58f7f7af..c2a6e2b37 100644 --- a/music21/base.py +++ b/music21/base.py @@ -27,7 +27,7 @@ >>> music21.VERSION_STR -'9.2.0b1' +'9.2.0b2' Alternatively, after doing a complete import, these classes are available under the module "base": diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index aaae96a7e..648178dc8 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -2200,14 +2200,15 @@ def adjustTimeAttributesFromMeasure(self, m: stream.Measure): # otherwise it was likely the result of malformed MusicXML. if (diff > 0.5 or nearestMultiple(diff, 0.0625)[1] < tol - or nearestMultiple(diff, opFrac(1 / 12))[1] < tol): + or nearestMultiple(diff, 1 / 12)[1] < tol): mOffsetShift = mHighestTime else: mOffsetShift = lastTimeSignatureQuarterLength warnings.warn( - f"Warning: measure {m.number} in part {self.stream.partName}" - f"is overfull: {mHighestTime} > {lastTimeSignatureQuarterLength}," - f"assuming {mOffsetShift} is correct." + f'Warning: measure {m.number} in part {self.stream.partName}' + f'is overfull: {mHighestTime} > {lastTimeSignatureQuarterLength},' + f'assuming {mOffsetShift} is correct.', + MusicXMLWarning ) elif (mHighestTime == 0.0 and not m.recurse().notesAndRests.getElementsNotOfClass('Harmony') From abed5008842d27fc41b6637647965b0471618a7c Mon Sep 17 00:00:00 2001 From: TimFelixBeyer Date: Wed, 4 Oct 2023 14:35:02 +0800 Subject: [PATCH 5/5] lowercase class instance names --- music21/musicxml/test_xmlToM21.py | 44 ++++++++-------- music21/musicxml/xmlToM21.py | 86 +++++++++++++++---------------- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index dd252054d..cb8475471 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -56,12 +56,12 @@ def testExceptionMessage(self): mxScorePart = self.EL('Elec.') mxPart = self.EL('thirty-tooth') - PP = PartParser(mxPart=mxPart, mxScorePart=mxScorePart) - PP.partId = '1' + pp = PartParser(mxPart=mxPart, mxScorePart=mxScorePart) + pp.partId = '1' msg = 'In part (Elec.), measure (0): found unknown MusicXML type: thirty-tooth' with self.assertRaises(MusicXMLImportException) as error: - PP.parse() + pp.parse() self.assertEqual(str(error.exception), msg) def testBarRepeatConversion(self): @@ -1478,9 +1478,9 @@ def testImportUnpitchedPercussion(self): ''' - PP = PartParser() + pp = PartParser() mxScorePart = EL(scorePart) - tmb = PP.getDefaultInstrument(mxScorePart) + tmb = pp.getDefaultInstrument(mxScorePart) self.assertIsInstance(tmb, instrument.Tambourine) self.assertEqual(tmb.percMapPitch, 54) # 1-indexed @@ -1488,11 +1488,11 @@ def testImportUnpitchedPercussion(self): scorePart = scorePart.replace('Tambourine', 'Cabasa') scorePart = scorePart.replace('Tamb.', 'Cab.') scorePart = scorePart.replace('55', '70') # 1-indexed - PP = PartParser() + pp = PartParser() mxScorePart = EL(scorePart) msg = '69 does not map to a valid instrument!' with self.assertWarnsRegex(MusicXMLWarning, msg): - unp = PP.getDefaultInstrument(mxScorePart) + unp = pp.getDefaultInstrument(mxScorePart) self.assertIsInstance(unp, instrument.UnpitchedPercussion) self.assertEqual(unp.percMapPitch, 69) @@ -1508,30 +1508,30 @@ def testAdjustTimeAttributesFromMeasure(self): # Ignore import artifacts: d = duration.Duration(3 + 3 / 480) m = stream.Measure([meter.TimeSignature('6/8'), note.Note(duration=d)]) - PP = PartParser() - PP.lastMeasureOffset = 21.0 - PP.setLastMeasureInfo(m) + pp = PartParser() + pp.lastMeasureOffset = 21.0 + pp.setLastMeasureInfo(m) with self.assertWarns(MusicXMLWarning): - PP.adjustTimeAttributesFromMeasure(m) - self.assertEqual(PP.lastMeasureOffset, 24.0) + pp.adjustTimeAttributesFromMeasure(m) + self.assertEqual(pp.lastMeasureOffset, 24.0) # Keep 'round' overful measures and extremely overful measures, as they were # likely intentional. d = duration.Duration(3.125) m = stream.Measure([meter.TimeSignature('6/8'), note.Note(duration=d)]) - PP = PartParser() - PP.lastMeasureOffset = 21.0 - PP.setLastMeasureInfo(m) - PP.adjustTimeAttributesFromMeasure(m) - self.assertEqual(PP.lastMeasureOffset, 24.125) + pp = PartParser() + pp.lastMeasureOffset = 21.0 + pp.setLastMeasureInfo(m) + pp.adjustTimeAttributesFromMeasure(m) + self.assertEqual(pp.lastMeasureOffset, 24.125) d = duration.Duration(4.0) m = stream.Measure([meter.TimeSignature('6/8'), note.Note(duration=d)]) - PP = PartParser() - PP.lastMeasureOffset = 21.0 - PP.setLastMeasureInfo(m) - PP.adjustTimeAttributesFromMeasure(m) - self.assertEqual(PP.lastMeasureOffset, 25.0) + pp = PartParser() + pp.lastMeasureOffset = 21.0 + pp.setLastMeasureInfo(m) + pp.adjustTimeAttributesFromMeasure(m) + self.assertEqual(pp.lastMeasureOffset, 25.0) if __name__ == '__main__': diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 648178dc8..ef09ac222 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -1625,10 +1625,10 @@ def getDefaultInstrument(self, mxScorePart: ET.Element | None = None) -> instrum ... + '' ... + '') >>> from xml.etree.ElementTree import fromstring as EL - >>> PP = musicxml.xmlToM21.PartParser() + >>> pp = musicxml.xmlToM21.PartParser() >>> mxScorePart = EL(scorePart) - >>> i = PP.getDefaultInstrument(mxScorePart) + >>> i = pp.getDefaultInstrument(mxScorePart) >>> i >>> i.instrumentName @@ -1647,10 +1647,10 @@ def getDefaultInstrument(self, mxScorePart: ET.Element | None = None) -> instrum ... + '' ... + '') >>> from xml.etree.ElementTree import fromstring as EL - >>> PP = musicxml.xmlToM21.PartParser() + >>> pp = musicxml.xmlToM21.PartParser() >>> mxScorePart = EL(scorePart) - >>> i = PP.getDefaultInstrument(mxScorePart) + >>> i = pp.getDefaultInstrument(mxScorePart) >>> i >>> i.instrumentName @@ -2067,15 +2067,15 @@ def setLastMeasureInfo(self, m: stream.Measure): Also sets self.lastTimeSignature from the timeSignature found in the measure, if any. - >>> PP = musicxml.xmlToM21.PartParser() + >>> pp = musicxml.xmlToM21.PartParser() Here are the defaults: - >>> PP.lastMeasureNumber + >>> pp.lastMeasureNumber 0 - >>> PP.lastNumberSuffix is None + >>> pp.lastNumberSuffix is None True - >>> PP.lastTimeSignature is None + >>> pp.lastTimeSignature is None True After setLastMeasureInfo: @@ -2084,15 +2084,15 @@ def setLastMeasureInfo(self, m: stream.Measure): >>> m.numberSuffix = 'b' >>> ts38 = meter.TimeSignature('3/8') >>> m.timeSignature = ts38 - >>> PP.setLastMeasureInfo(m) + >>> pp.setLastMeasureInfo(m) - >>> PP.lastMeasureNumber + >>> pp.lastMeasureNumber 4 - >>> PP.lastNumberSuffix + >>> pp.lastNumberSuffix 'b' - >>> PP.lastTimeSignature + >>> pp.lastTimeSignature - >>> PP.lastTimeSignature is ts38 + >>> pp.lastTimeSignature is ts38 True Note that if there was no timeSignature defined in m, @@ -2101,10 +2101,10 @@ def setLastMeasureInfo(self, m: stream.Measure): after the first measure there's going to be routines that need some sort of time signature: - >>> PP2 = musicxml.xmlToM21.PartParser() + >>> pp2 = musicxml.xmlToM21.PartParser() >>> m2 = stream.Measure(number=2) - >>> PP2.setLastMeasureInfo(m2) - >>> PP2.lastTimeSignature + >>> pp2.setLastMeasureInfo(m2) + >>> pp2.lastTimeSignature @@ -2112,14 +2112,14 @@ def setLastMeasureInfo(self, m: stream.Measure): to unnumbered measures, if a measure has the same number as the lastMeasureNumber, the lastNumberSuffix is not updated: - >>> PP3 = musicxml.xmlToM21.PartParser() - >>> PP3.lastMeasureNumber = 10 - >>> PP3.lastNumberSuffix = 'X1' + >>> pp3 = musicxml.xmlToM21.PartParser() + >>> pp3.lastMeasureNumber = 10 + >>> pp3.lastNumberSuffix = 'X1' >>> m10 = stream.Measure(number=10) >>> m10.numberSuffix = 'X2' - >>> PP3.setLastMeasureInfo(m10) - >>> PP3.lastNumberSuffix + >>> pp3.setLastMeasureInfo(m10) + >>> pp3.lastNumberSuffix 'X1' ''' if m.number == self.lastMeasureNumber: @@ -2157,22 +2157,22 @@ def adjustTimeAttributesFromMeasure(self, m: stream.Measure): >>> m = stream.Measure([meter.TimeSignature('4/4'), harmony.ChordSymbol('C7')]) >>> m.highestTime 0.0 - >>> PP = musicxml.xmlToM21.PartParser() - >>> PP.setLastMeasureInfo(m) - >>> PP.adjustTimeAttributesFromMeasure(m) + >>> pp = musicxml.xmlToM21.PartParser() + >>> pp.setLastMeasureInfo(m) + >>> pp.adjustTimeAttributesFromMeasure(m) >>> m.highestTime 4.0 - >>> PP.lastMeasureWasShort + >>> pp.lastMeasureWasShort False Incomplete final measure: >>> m = stream.Measure([meter.TimeSignature('6/8'), note.Note(), note.Note()]) >>> m.offset = 24.0 - >>> PP = musicxml.xmlToM21.PartParser() - >>> PP.lastMeasureOffset = 21.0 - >>> PP.setLastMeasureInfo(m) - >>> PP.adjustTimeAttributesFromMeasure(m) + >>> pp = musicxml.xmlToM21.PartParser() + >>> pp.lastMeasureOffset = 21.0 + >>> pp.setLastMeasureInfo(m) + >>> pp.adjustTimeAttributesFromMeasure(m) >>> m.paddingRight 1.0 ''' @@ -2254,39 +2254,39 @@ def applyMultiMeasureRest(self, r: note.Rest): ''' If there is an active MultiMeasureRestSpanner, add the Rest, r, to it: - >>> PP = musicxml.xmlToM21.PartParser() + >>> pp = musicxml.xmlToM21.PartParser() >>> mmrSpanner = spanner.MultiMeasureRest() >>> mmrSpanner - >>> PP.activeMultiMeasureRestSpanner = mmrSpanner - >>> PP.multiMeasureRestsToCapture = 2 + >>> pp.activeMultiMeasureRestSpanner = mmrSpanner + >>> pp.multiMeasureRestsToCapture = 2 >>> r1 = note.Rest(type='whole', id='r1') - >>> PP.applyMultiMeasureRest(r1) - >>> PP.multiMeasureRestsToCapture + >>> pp.applyMultiMeasureRest(r1) + >>> pp.multiMeasureRestsToCapture 1 - >>> PP.activeMultiMeasureRestSpanner + >>> pp.activeMultiMeasureRestSpanner - >>> PP.activeMultiMeasureRestSpanner is mmrSpanner + >>> pp.activeMultiMeasureRestSpanner is mmrSpanner True - >>> PP.stream.show('text') # Nothing... + >>> pp.stream.show('text') # Nothing... >>> r2 = note.Rest(type='whole', id='r2') - >>> PP.applyMultiMeasureRest(r2) - >>> PP.multiMeasureRestsToCapture + >>> pp.applyMultiMeasureRest(r2) + >>> pp.multiMeasureRestsToCapture 0 - >>> PP.activeMultiMeasureRestSpanner is None + >>> pp.activeMultiMeasureRestSpanner is None True # spanner added to stream - >>> PP.stream.show('text') + >>> pp.stream.show('text') {0.0} >>> r3 = note.Rest(type='whole', id='r3') - >>> PP.applyMultiMeasureRest(r3) - >>> PP.stream.show('text') + >>> pp.applyMultiMeasureRest(r3) + >>> pp.stream.show('text') {0.0} '''