diff --git a/music21/_version.py b/music21/_version.py index ec82c2e69..78ca3e86b 100644 --- a/music21/_version.py +++ b/music21/_version.py @@ -47,7 +47,7 @@ ''' from __future__ import annotations -__version__ = '9.4.0b1' +__version__ = '9.4.0b2' def get_version_tuple(vv): v = vv.split('.') diff --git a/music21/base.py b/music21/base.py index d542c3ae6..63406b1ad 100644 --- a/music21/base.py +++ b/music21/base.py @@ -27,7 +27,7 @@ >>> music21.VERSION_STR -'9.4.0b1' +'9.4.0b2' Alternatively, after doing a complete import, these classes are available under the module "base": diff --git a/music21/common/enums.py b/music21/common/enums.py index 5ef9efb6d..f23347ffc 100644 --- a/music21/common/enums.py +++ b/music21/common/enums.py @@ -157,9 +157,9 @@ class OffsetSpecial(StrEnum): * New in v7. ''' - AT_END: str = 'highestTime' - LOWEST_OFFSET: str = 'lowestOffset' - HIGHEST_OFFSET: str = 'highestOffset' + AT_END = 'highestTime' + LOWEST_OFFSET = 'lowestOffset' + HIGHEST_OFFSET = 'highestOffset' class GatherSpanners(BooleanEnum): diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py index d6ef8a319..3bbb4acd5 100644 --- a/music21/musicxml/testPrimitive.py +++ b/music21/musicxml/testPrimitive.py @@ -18264,7 +18264,83 @@ ''' -hiddenRests = ''' +hiddenRestsFinale = ''' + + + + + Finale 2014 for Mac + + + + + MusicXML Part + + + + + + 2 + + + G + 2 + + + + + E + 5 + + 4 + 1 + half + up + + + 2 + 1 + + + + E + 4 + + 2 + 1 + quarter + up + + + 8 + + + 4 + 2 + + + + F + 4 + + 2 + 2 + quarter + down + + + 2 + 2 + + + + +''' + +hiddenRestsNoFinale = ''' @@ -18387,7 +18463,6 @@ ''' - tupletsImplied = ''' @@ -20041,10 +20116,10 @@ mixedVoices1a, mixedVoices1b, mixedVoices2, # 37 colors01, triplets01, textBoxes01, octaveShifts33d, # 40 unicodeStrNoNonAscii, unicodeStrWithNonAscii, # 44 - tremoloTest, hiddenRests, multiDigitEnding, tupletsImplied, pianoStaffPolymeter, # 46 - arpeggio32d, multiStaffArpeggios, multiMeasureEnding, # 51 - pianoStaffPolymeterWithClefOctaveChange, multipleFingeringsOnChord, # 54 - pianoStaffWithOttava # 56 + tremoloTest, hiddenRestsFinale, hiddenRestsNoFinale, multiDigitEnding, # 46 + tupletsImplied, pianoStaffPolymeter, arpeggio32d, multiStaffArpeggios, # 50 + multiMeasureEnding, pianoStaffPolymeterWithClefOctaveChange, # 54 + multipleFingeringsOnChord, pianoStaffWithOttava # 56 ] diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index 5b1361bcc..ab1e99879 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -1247,9 +1247,20 @@ def testHiddenRests(self): from music21 import corpus from music21.musicxml import testPrimitive + # With most software, tags should map to no objects at all # Voice 1: Half note, (quarter), quarter note # Voice 2: (half), quarter note, (quarter) - s = converter.parse(testPrimitive.hiddenRests) + s = converter.parse(testPrimitive.hiddenRestsNoFinale) + v1, v2 = s.recurse().voices + # No rests should have been added + self.assertFalse(v1.getElementsByClass(note.Rest)) + self.assertFalse(v2.getElementsByClass(note.Rest)) + + # Finale uses tags to represent hidden rests, + # so we want to have rests here + # Voice 1: Half note, (quarter), quarter note + # Voice 2: (half), quarter note, (quarter) + s = converter.parse(testPrimitive.hiddenRestsFinale) v1, v2 = s.recurse().voices self.assertEqual(v1.duration.quarterLength, v2.duration.quarterLength) @@ -1290,7 +1301,7 @@ def testHiddenRestImpliedVoice(self): self.assertEqual(len(MP.stream.voices), 2) self.assertEqual(len(MP.stream.voices[0].elements), 1) - self.assertEqual(len(MP.stream.voices[1].elements), 2) + self.assertEqual(len(MP.stream.voices[1].elements), 1) self.assertEqual(MP.stream.voices[1].id, 'non-integer-value') def testMultiDigitEnding(self): diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 3025157e6..0caa04f55 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -867,6 +867,21 @@ def xmlRootToScore(self, mxScore, inputM21=None): self.spannerBundle.remove(sp) s.coreElementsChanged() + # Fill gaps with rests where needed + for m in s[stream.Measure]: + for v in m.voices: + if v: # do not bother with empty voices + # the musicDataMethods use insertCore, thus the voices need to run + # coreElementsChanged + v.coreElementsChanged() + # Fill mid-measure gaps, and find end of measure gaps by ref to measure stream + # https://github.com/cuthbertlab/music21/issues/444 + # but only when the score comes from Finale + if any('Finale' in software for software in md.software): + v.makeRests(refStreamOrTimeRange=m, + fillGaps=True, + inPlace=True, + hideRests=True) s.definesExplicitSystemBreaks = self.definesExplicitSystemBreaks s.definesExplicitPageBreaks = self.definesExplicitPageBreaks for p in s.parts: @@ -1761,32 +1776,8 @@ def parseMeasures(self): for mxMeasure in self.mxPart.iterfind('measure'): self.xmlMeasureToMeasure(mxMeasure) - self.removeEndForwardRest() part.coreElementsChanged() - def removeEndForwardRest(self): - ''' - If the last measure ended with a forward tag, as happens - in some pieces that end with incomplete measures, - and voices are not involved, - remove the rest there (for backwards compatibility, esp. - since bwv66.6 uses it) - - * New in v7. - ''' - if self.lastMeasureParser is None: # pragma: no cover - return # should not happen - lmp = self.lastMeasureParser - self.lastMeasureParser = None # clean memory - - if lmp.endedWithForwardTag is None: - return - if lmp.useVoices is True: - return - endedForwardRest = lmp.endedWithForwardTag - if lmp.stream.recurse().notesAndRests.last() is endedForwardRest: - lmp.stream.remove(endedForwardRest, recurse=True) - def separateOutPartStaves(self) -> list[stream.PartStaff]: ''' Take a `Part` with multiple staves and make them a set of `PartStaff` objects. @@ -2247,7 +2238,7 @@ def adjustTimeAttributesFromMeasure(self, m: stream.Measure): else: self.lastMeasureWasShort = False - self.lastMeasureOffset += mOffsetShift + self.lastMeasureOffset = opFrac(self.lastMeasureOffset + mOffsetShift) def applyMultiMeasureRest(self, r: note.Rest): ''' @@ -2567,19 +2558,8 @@ def parse(self): if methName is not None: meth = getattr(self, methName) meth(mxObj) - - if self.useVoices is True: - for v in self.stream.iter().voices: - if v: # do not bother with empty voices - # the musicDataMethods use insertCore, thus the voices need to run - # coreElementsChanged - v.coreElementsChanged() - # Fill mid-measure gaps, and find end of measure gaps by ref to measure stream - # https://github.com/cuthbertlab/music21/issues/444 - v.makeRests(refStreamOrTimeRange=self.stream, - fillGaps=True, - inPlace=True, - hideRests=True) + for v in self.stream[stream.Voice]: + v.coreElementsChanged() self.stream.coreElementsChanged() if (self.restAndNoteCount['rest'] == 1 @@ -2602,7 +2582,7 @@ def xmlBackup(self, mxObj: ET.Element): >>> mxBackup = EL('100') >>> MP.xmlBackup(mxBackup) >>> MP.offsetMeasureNote - 0.9979 + Fraction(9979, 10000) >>> MP.xmlBackup(mxBackup) >>> MP.offsetMeasureNote @@ -2611,7 +2591,8 @@ def xmlBackup(self, mxObj: ET.Element): mxDuration = mxObj.find('duration') if durationText := strippedText(mxDuration): change = opFrac(float(durationText) / self.divisions) - self.offsetMeasureNote -= change + self.offsetMeasureNote = opFrac(self.offsetMeasureNote - change) + # check for negative offsets produced by # musicxml durations with float rounding issues # https://github.com/cuthbertLab/music21/issues/971 @@ -2624,19 +2605,8 @@ def xmlForward(self, mxObj: ET.Element): mxDuration = mxObj.find('duration') if durationText := strippedText(mxDuration): change = opFrac(float(durationText) / self.divisions) - - # Create hidden rest (in other words, a spacer) - # old Finale documents close incomplete final measures with - # this will be removed afterward by removeEndForwardRest() - r = note.Rest(quarterLength=change) - r.style.hideObjectOnPrint = True - self.addToStaffReference(mxObj, r) - self.insertInMeasureOrVoice(mxObj, r) - # Allow overfilled measures for now -- TODO(someday): warn? - self.offsetMeasureNote += change - # xmlToNote() sets None - self.endedWithForwardTag = r + self.offsetMeasureNote = opFrac(self.offsetMeasureNote + change) def xmlPrint(self, mxPrint: ET.Element): ''' @@ -2795,8 +2765,7 @@ def xmlToNote(self, mxNote: ET.Element) -> None: self.nLast = c # update # only increment Chords after completion - self.offsetMeasureNote += offsetIncrement - self.endedWithForwardTag = None + self.offsetMeasureNote = opFrac(self.offsetMeasureNote + offsetIncrement) def xmlToChord(self, mxNoteList: list[ET.Element]) -> chord.ChordBase: # noinspection PyShadowingNames