From 2b4ab322e50c770476e152c56f36af2fdfcf8c27 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 28 Feb 2022 20:00:41 -0500 Subject: [PATCH 01/18] Add routines for reexpressing tuplets --- music21/duration.py | 10 +- music21/musicxml/m21ToXml.py | 49 ++++---- music21/stream/base.py | 22 ++-- music21/stream/makeNotation.py | 197 ++++++++++++++++++++++++++++++++- 4 files changed, 239 insertions(+), 39 deletions(-) diff --git a/music21/duration.py b/music21/duration.py index c9c41e5631..aed16e29ad 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -1494,11 +1494,11 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): A Duration object is made of one or more immutable DurationTuple objects stored on the `components` list. A Duration created by setting `quarterLength` sets the attribute - `expressionIsInferred` to True, which indicates that consuming functions or applications + :attr:`expressionIsInferred` to True, which indicates that callers + (such as :meth:`~music21.stream.makeNotation.splitElementsToCompleteTuplets`) can express this Duration using another combination of components that sums to the `quarterLength`. Otherwise, `expressionIsInferred` is set to False, indicating that components are not allowed to mutate. - (N.B.: `music21` does not yet implement such mutating components.) Multiple DurationTuples in a single Duration may be used to express tied notes, or may be used to split duration across barlines or beam groups. @@ -1565,6 +1565,12 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin): '_client' ) + _DOC_ATTR = {'expressionIsInferred': + ''' + Boolean indicating whether this duration was created from a + number rather than a type and thus can be reexpressed. + '''} + # INITIALIZER # def __init__(self, *arguments, **keywords): diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index c8ae1f2fbe..f4e0b8a79f 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -1635,16 +1635,6 @@ def setScoreLayouts(self): self.firstScoreLayout = scoreLayout def _populatePartExporterList(self): - if self.makeNotation: - # hide any rests created at this late stage, because we are - # merely trying to fill up MusicXML display, not impose things on users - for p in self.parts: - p.makeRests(refStreamOrTimeRange=self.refStreamOrTimeRange, - inPlace=True, - hideRests=True, - timeRangeFromBarDuration=True, - ) - count = 0 sp = list(self.parts) for innerStream in sp: @@ -1661,9 +1651,7 @@ def _populatePartExporterList(self): def parsePartlikeScore(self): ''' Called by .parse() if the score has individual parts. - - Calls makeRests() for the part (if `ScoreExporter.makeNotation` is True), - then creates a `PartExporter` for each part, and runs .parse() on that part. + Creates a `PartExporter` for each part, and runs .parse() on that part. Appends the PartExporter to `self.partExporterList` and runs .parse() on that part. Appends the PartExporter to self. @@ -2542,10 +2530,18 @@ def parse(self): self.stream = self.stream.splitAtDurations(recurse=True)[0] # Suppose that everything below this is a measure - if self.makeNotation and not self.stream.getElementsByClass(stream.Measure): - self.fixupNotationFlat() - elif self.makeNotation: - self.fixupNotationMeasured() + if self.makeNotation: + # hide any rests created at this late stage, because we are + # merely trying to fill up MusicXML display, not impose things on users + self.stream.makeRests(refStreamOrTimeRange=self.refStreamOrTimeRange, + inPlace=True, + hideRests=True, + timeRangeFromBarDuration=True, + ) + if not self.stream.getElementsByClass(stream.Measure): + self.fixupNotationFlat() + elif self.makeNotation: + self.fixupNotationMeasured() elif not self.stream.getElementsByClass(stream.Measure): raise MusicXMLExportException( 'Cannot export with makeNotation=False if there are no measures') @@ -2721,8 +2717,8 @@ def fixupNotationMeasured(self): Checks to see if there are any attributes in the part stream and moves them into the first measure if necessary. - Checks if makeAccidentals is run, and haveBeamsBeenMade is done, and - haveTupletBracketsBeenMade is done. + Checks if makeAccidentals is run, and remakes beams and tuplet brackets + on the assumption they may have changed since makeRests() was called. Changed in v7 -- no longer accepts `measureStream` argument. ''' @@ -2752,16 +2748,21 @@ def fixupNotationMeasured(self): if outerTimeSignatures: first_measure.timeSignature = outerTimeSignatures.first() - # see if accidentals/beams/tuplets should be processed + # see if accidentals/beams should be processed if not part.streamStatus.haveAccidentalsBeenMade(): part.makeAccidentals(inPlace=True) + # beams and tuplets should be processed anyway (affected by earlier makeRests) if not part.streamStatus.beams: try: part.makeBeams(inPlace=True) - except exceptions21.StreamException: # no measures or no time sig? - pass - if part.streamStatus.haveTupletBracketsBeenMade() is False: - stream.makeNotation.makeTupletBrackets(part, inPlace=True) + except exceptions21.StreamException as se: # no measures or no time sig? + warnings.warn(MusicXMLWarning, str(se)) + # tuplets should be processed anyway (affected by earlier makeRests) + # technically, beams could be affected also, but we don't want to destroy + # existing beam information (e.g. single-syllable vocal flags) + for m in measures: + for m_or_v in [m, *m.voices]: + stream.makeNotation.makeTupletBrackets(m_or_v, inPlace=True) if not self.spannerBundle: self.spannerBundle = part.spannerBundle diff --git a/music21/stream/base.py b/music21/stream/base.py index 177ec11e89..5ef6c955df 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -28,6 +28,7 @@ import pathlib import unittest import sys +import warnings from collections import namedtuple from fractions import Fraction @@ -6839,20 +6840,22 @@ def makeNotation(self, measureStream.makeTies(meterStream, inPlace=True) - # measureStream.makeBeams(inPlace=True) + for m in measureStream: + makeNotation.splitElementsToCompleteTuplets(m, recurse=True, addTies=True) + makeNotation.consolidateCompletedTuplets(m, recurse=True, onlyIfTied=True) + if not measureStream.streamStatus.beams: try: measureStream.makeBeams(inPlace=True) except meter.MeterException as me: - environLocal.warn(['skipping makeBeams exception', me]) + warnings.warn(['skipping makeBeams exception', me]) # note: this needs to be after makeBeams, as placing this before # makeBeams was causing the duration's tuplet to lose its type setting # check for tuplet brackets one measure at a time # this means that they will never extend beyond one measure for m in measureStream: - if not m.streamStatus.tuplets: - makeNotation.makeTupletBrackets(m, inPlace=True) + makeNotation.makeTupletBrackets(m, inPlace=True) if not inPlace: return returnStream @@ -12944,11 +12947,12 @@ def makeNotation(self, ts = defaultMeters[0] m.timeSignature = ts # a Stream; get the first element - # environLocal.printDebug(['have time signature', m.timeSignature]) - if not m.streamStatus.beams: - m.makeBeams(inPlace=True) - if not m.streamStatus.tuplets: - makeNotation.makeTupletBrackets(m, inPlace=True) + makeNotation.splitElementsToCompleteTuplets(m, recurse=True, addTies=True) + makeNotation.consolidateCompletedTuplets(m, recurse=True, onlyIfTied=True) + + m.makeBeams(inPlace=True) + for m_or_v in [m, *m.voices]: + makeNotation.makeTupletBrackets(m_or_v, inPlace=True) if not inPlace: return m diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index 05c176605c..e8f7c20014 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1459,10 +1459,8 @@ def makeTupletBrackets(s: 'music21.stream.Stream', *, inPlace=False): # if tuplet next and previous not None, increment elif tupletPrevious is not None and tupletNext is not None: - # do not need to change tuplet type; should be None - pass - # environLocal.printDebug(['completion count, target:', - # completionCount, completionTarget]) + # clear any previous type from prior calls + tupletObj.type = None returnObj.streamStatus.tuplets = True @@ -1865,6 +1863,197 @@ def setStemDirectionOneGroup( n.stemDirection = groupStemDirection +def splitElementsToCompleteTuplets( + s: 'music21.stream.Stream', + *, + recurse: bool = False, + addTies: bool = True +) -> None: + ''' + Split notes or rests if doing so will complete any incomplete tuplets. + The element being split must have a duration that equals or exceeds the + remainder of the incomplete tuplet. + + The first note is edited; the additional notes are inserted in place. + (Destructive edit, so make a copy first if desired.) + Relies on :meth:`~music21.stream.base.splitAtQuarterLength`. + + New in v7.3. + + >>> from music21.stream.makeNotation import splitElementsToCompleteTuplets + >>> s = stream.Stream([note.Note(quarterLength=1/3), note.Note(quarterLength=1), note.Note(quarterLength=2/3)]) + >>> splitElementsToCompleteTuplets(s) + >>> [el.quarterLength for el in s.notes] + [Fraction(1, 3), Fraction(2, 3), Fraction(1, 3), Fraction(2, 3)] + + With `recurse`: + + >>> m = stream.Measure([note.Note(quarterLength=1/6)]) + >>> m.insert(5/6, note.Note(quarterLength=1/6)) + >>> m.makeRests(inPlace=True, fillGaps=True) + >>> p = stream.Part([m]) + >>> splitElementsToCompleteTuplets(p, recurse=True) + >>> [el.quarterLength for el in p.recurse().notesAndRests] + [Fraction(1, 6), Fraction(1, 3), Fraction(1, 3), Fraction(1, 6)] + ''' + if recurse: + iter = s.recurse(streamsOnly=True, includeSelf=True) + else: + iter = [s] + for container in iter: + general_notes = list(container.notesAndRests) + last_tuplet: Optional['music21.duration.Tuplet'] = None + partial_tuplet_sum = 0.0 + for gn in general_notes: + if ( + gn.duration.tuplets + and gn.duration.expressionIsInferred + and (last_tuplet is None or last_tuplet == gn.duration.tuplets[0]) + ): + last_tuplet = gn.duration.tuplets[0] + partial_tuplet_sum = opFrac(gn.quarterLength + partial_tuplet_sum) + else: + last_tuplet = None + partial_tuplet_sum = 0.0 + continue + ql_to_complete = opFrac( + gn.duration.tuplets[0].totalTupletLength() - partial_tuplet_sum) + if ql_to_complete == 0.0: + last_tuplet = None + partial_tuplet_sum = 0.0 + continue + next_gn = gn.next(note.GeneralNote, activeSiteOnly=True) + if next_gn and next_gn.offset != opFrac(gn.offset + gn.quarterLength): + continue + if next_gn and next_gn.duration.expressionIsInferred: + if ql_to_complete > 0 and next_gn.quarterLength > ql_to_complete: + unused_left_edited_in_place, right = next_gn.splitAtQuarterLength( + ql_to_complete, addTies=addTies) + container.insert(next_gn.offset + ql_to_complete, right) + + +def consolidateCompletedTuplets( + s: 'music21.stream.Stream', + *, + recurse: bool = False, + onlyIfTied: bool = True, +) -> None: + ''' + Locate consecutive notes or rests in `s` (or its substreams if `recurse` is True) + that are unnecessarily expressed as tuplets and replace them with a single + element. These groups must: + + - be consecutive (with respect to the sequence of :class:`~music21.note.GeneralNote` objects) + - be all rests, or all :class:`~music21.note.NotRest`s with equal `.pitches` + - all have :attr:`~music21.duration.Duration.expressionIsInferred` = `True`. + - sum to the tuplet's total length + - if `NotRest`, all must be tied (if `onlyIfTied` is True) + + The groups are consolidated by prolonging the first note or rest in the group + and removing the subsequent elements from the stream. (Destructive edit, + so make a copy first if desired.) + + New in v7.3. + + >>> s = stream.Stream() + >>> r = note.Rest(quarterLength=1/6) + >>> s.repeatAppend(r, 5) + >>> s.insert(5/6, note.Note(duration=r.duration)) + >>> from music21.stream.makeNotation import consolidateCompletedTuplets + >>> consolidateCompletedTuplets(s) + >>> [el.quarterLength for el in s.notesAndRests] + [0.5, Fraction(1, 6), Fraction(1, 6), Fraction(1, 6)] + + `mustBeTied` is `True` by default: + + >>> s2 = stream.Stream() + >>> n = note.Note(quarterLength=1/3) + >>> s2.repeatAppend(n, 3) + >>> consolidateCompletedTuplets(s) + >>> [el.quarterLength for el in s2.notesAndRests] + [Fraction(1, 3), Fraction(1, 3), Fraction(1, 3)] + + >>> consolidateCompletedTuplets(s2, onlyIfTied=False) + >>> [el.quarterLength for el in s2.notesAndRests] + [1.0] + + Does nothing if tuplet definitions are not the same. (In which case, see + :class:`~music21.duration.TupletFixer` instead). + + >>> s3 = stream.Stream([note.Rest(quarterLength=1/3), note.Rest(quarterLength=1/6)]) + >>> for my_rest in s3.notesAndRests: + ... print(my_rest.duration.tuplets) + (,) + (,) + >>> consolidateCompletedTuplets(s) + >>> [el.quarterLength for el in s3.notesAndRests] + [Fraction(1, 3), Fraction(1, 6)] + + Does nothing if there are multiple (nested) tuplets. + ''' + def is_reexpressible(gn: note.GeneralNote) -> bool: + return ( + gn.duration.expressionIsInferred + and len(gn.duration.tuplets) < 2 + and (gn.isRest or gn.tie is not None or not onlyIfTied) + ) + + if recurse: + iter = s.recurse(streamsOnly=True, includeSelf=True) + else: + iter = [s] + for container in iter: + reexpressible = [gn for gn in container.notesAndRests if is_reexpressible(gn)] + to_consolidate: List['music21.note.GeneralNote'] = [] + partial_tuplet_sum = 0.0 + last_tuplet: Optional['music21.duration.Tuplet'] = None + completion_target: Optional[common.types.OffsetQL] = None + for gn in reexpressible: + prev_gn = gn.previous(note.GeneralNote, activeSiteOnly=True) + if ( + prev_gn in to_consolidate + and ( + (isinstance(gn, note.Rest) and isinstance(prev_gn, note.Rest)) + or ( + isinstance(gn, note.NotRest) + and isinstance(prev_gn, note.NotRest) + and gn.pitches == prev_gn.pitches + ) + ) + and opFrac(prev_gn.offset + prev_gn.quarterLength) == gn.offset + and len(gn.duration.tuplets) == 1 and gn.duration.tuplets[0] == last_tuplet + ): + partial_tuplet_sum = opFrac(partial_tuplet_sum + gn.quarterLength) + to_consolidate.append(gn) + + if partial_tuplet_sum == completion_target: + # set flag to remake tuplet brackets + container.streamStatus.tuplets = False + first_note_in_group = to_consolidate[0] + for other_note in to_consolidate[1:]: + container.remove(other_note) + first_note_in_group.duration.clear() + first_note_in_group.duration.tuplets = () + first_note_in_group.quarterLength = completion_target + + # reset search values + to_consolidate = [] + partial_tuplet_sum = 0.0 + last_tuplet = None + completion_target = None + else: + # reset to current values + if gn.duration.tuplets: + partial_tuplet_sum = gn.quarterLength + last_tuplet = gn.duration.tuplets[0] + completion_target = last_tuplet.totalTupletLength() + to_consolidate = [gn] + else: + to_consolidate = [] + partial_tuplet_sum = 0.0 + last_tuplet = None + completion_target = None + # ----------------------------------------------------------------------------- From 0192707686515f9f9a23f2ecee514a556de5e372 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Mar 2022 21:15:03 -0500 Subject: [PATCH 02/18] move splitAtDurations() call in musicxml export --- music21/musicxml/m21ToXml.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index f4e0b8a79f..3e9a2a7965 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2526,9 +2526,6 @@ def parse(self): if self.stream.atSoundingPitch is True: self.stream.toWrittenPitch(inPlace=True) - # Split complex durations in place (fast if none found) - self.stream = self.stream.splitAtDurations(recurse=True)[0] - # Suppose that everything below this is a measure if self.makeNotation: # hide any rests created at this late stage, because we are @@ -2538,6 +2535,11 @@ def parse(self): hideRests=True, timeRangeFromBarDuration=True, ) + + # Split complex durations in place (fast if none found) + # Do this after makeRests since makeRests might create complex durations + self.stream = self.stream.splitAtDurations(recurse=True)[0] + if not self.stream.getElementsByClass(stream.Measure): self.fixupNotationFlat() elif self.makeNotation: From e126cadd8ba3a65c7ca4e9bd9593873f5136517a Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Mar 2022 21:20:33 -0500 Subject: [PATCH 03/18] Loosen order test in testMultipleInstrumentsPiano --- music21/musicxml/m21ToXml.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 3e9a2a7965..1ef8cbc1be 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -6905,8 +6905,9 @@ def testMultipleInstrumentsPiano(self): tree = scEx.parse() self.assertEqual( - [el.text for el in tree.findall('.//instrument-name')], - ['Electric Piano', 'Voice', 'Electric Organ', 'Piano'] + # allow for non-deterministic ordering: caused by instrument.deduplicate() (?) + {el.text for el in tree.findall('.//instrument-name')}, + {'Electric Piano', 'Voice', 'Electric Organ', 'Piano'} ) self.assertEqual(len(tree.findall('.//measure/note/instrument')), 6) From dd20c3eca7081578744854c442e5e9230dea482d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Mar 2022 22:00:20 -0500 Subject: [PATCH 04/18] simplifiy condition --- music21/musicxml/m21ToXml.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 1ef8cbc1be..0b53b8e32c 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2540,10 +2540,10 @@ def parse(self): # Do this after makeRests since makeRests might create complex durations self.stream = self.stream.splitAtDurations(recurse=True)[0] - if not self.stream.getElementsByClass(stream.Measure): - self.fixupNotationFlat() - elif self.makeNotation: + if self.stream.getElementsByClass(stream.Measure): self.fixupNotationMeasured() + else: + self.fixupNotationFlat() elif not self.stream.getElementsByClass(stream.Measure): raise MusicXMLExportException( 'Cannot export with makeNotation=False if there are no measures') From 65291f449c5b03d45ee704f593fb5adf6a02a03e Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Mar 2022 22:01:16 -0500 Subject: [PATCH 05/18] adjust comment --- music21/musicxml/m21ToXml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 0b53b8e32c..3b1d6c9bd4 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2719,8 +2719,8 @@ def fixupNotationMeasured(self): Checks to see if there are any attributes in the part stream and moves them into the first measure if necessary. - Checks if makeAccidentals is run, and remakes beams and tuplet brackets - on the assumption they may have changed since makeRests() was called. + Checks if makeAccidentals is run, and haveBeamsBeenMade is done, and + remake tuplets on the assumption that makeRests() may necessitate changes. Changed in v7 -- no longer accepts `measureStream` argument. ''' From e3517466ec7117299a355b17a626df0b9c9f33d5 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Mar 2022 22:01:57 -0500 Subject: [PATCH 06/18] another adjusted comment --- music21/musicxml/m21ToXml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 3b1d6c9bd4..f2353225a7 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2753,7 +2753,6 @@ def fixupNotationMeasured(self): # see if accidentals/beams should be processed if not part.streamStatus.haveAccidentalsBeenMade(): part.makeAccidentals(inPlace=True) - # beams and tuplets should be processed anyway (affected by earlier makeRests) if not part.streamStatus.beams: try: part.makeBeams(inPlace=True) From 31a685484a5cdbc86f7f88d3f3e0181faf488eec Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Mar 2022 22:03:18 -0500 Subject: [PATCH 07/18] fix beams warning cleanup --- music21/stream/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/stream/base.py b/music21/stream/base.py index 5ef6c955df..829c4f5324 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -6848,7 +6848,7 @@ def makeNotation(self, try: measureStream.makeBeams(inPlace=True) except meter.MeterException as me: - warnings.warn(['skipping makeBeams exception', me]) + warnings.warn(str(me)) # note: this needs to be after makeBeams, as placing this before # makeBeams was causing the duration's tuplet to lose its type setting From f80ad768e41126627b84a7f58c486a7a3d03135c Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Mar 2022 22:07:11 -0500 Subject: [PATCH 08/18] pylint fixes --- music21/stream/makeNotation.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index e8f7c20014..ccfc07b975 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1881,7 +1881,9 @@ def splitElementsToCompleteTuplets( New in v7.3. >>> from music21.stream.makeNotation import splitElementsToCompleteTuplets - >>> s = stream.Stream([note.Note(quarterLength=1/3), note.Note(quarterLength=1), note.Note(quarterLength=2/3)]) + >>> s = stream.Stream( + ... [note.Note(quarterLength=1/3), note.Note(quarterLength=1), note.Note(quarterLength=2/3)] + ... ) >>> splitElementsToCompleteTuplets(s) >>> [el.quarterLength for el in s.notes] [Fraction(1, 3), Fraction(2, 3), Fraction(1, 3), Fraction(2, 3)] @@ -1897,10 +1899,10 @@ def splitElementsToCompleteTuplets( [Fraction(1, 6), Fraction(1, 3), Fraction(1, 3), Fraction(1, 6)] ''' if recurse: - iter = s.recurse(streamsOnly=True, includeSelf=True) + iterator = s.recurse(streamsOnly=True, includeSelf=True) else: - iter = [s] - for container in iter: + iterator = [s] + for container in iterator: general_notes = list(container.notesAndRests) last_tuplet: Optional['music21.duration.Tuplet'] = None partial_tuplet_sum = 0.0 @@ -1943,7 +1945,7 @@ def consolidateCompletedTuplets( that are unnecessarily expressed as tuplets and replace them with a single element. These groups must: - - be consecutive (with respect to the sequence of :class:`~music21.note.GeneralNote` objects) + - be consecutive (with respect to :class:`~music21.note.GeneralNote` objects) - be all rests, or all :class:`~music21.note.NotRest`s with equal `.pitches` - all have :attr:`~music21.duration.Duration.expressionIsInferred` = `True`. - sum to the tuplet's total length @@ -1999,10 +2001,10 @@ def is_reexpressible(gn: note.GeneralNote) -> bool: ) if recurse: - iter = s.recurse(streamsOnly=True, includeSelf=True) + iterator = s.recurse(streamsOnly=True, includeSelf=True) else: - iter = [s] - for container in iter: + iterator = [s] + for container in iterator: reexpressible = [gn for gn in container.notesAndRests if is_reexpressible(gn)] to_consolidate: List['music21.note.GeneralNote'] = [] partial_tuplet_sum = 0.0 From a8ec2ba541ae075e5d6e56873b97da359c6711cc Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Mar 2022 22:10:14 -0500 Subject: [PATCH 09/18] keep splitAtDurations() call if makeNotation=False --- music21/musicxml/m21ToXml.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index f2353225a7..478ff2f096 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2547,6 +2547,10 @@ def parse(self): elif not self.stream.getElementsByClass(stream.Measure): raise MusicXMLExportException( 'Cannot export with makeNotation=False if there are no measures') + else: + # QUESTION: destructive edit OK with makeNotation=False? + self.stream = self.stream.splitAtDurations(recurse=True)[0] + # make sure that all instances of the same class have unique ids self.spannerBundle.setIdLocals() From 295653a75f3ea3a5180bc2a5d2e27f80ad4e6ef0 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Mar 2022 22:12:58 -0500 Subject: [PATCH 10/18] fix doc --- music21/stream/makeNotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index ccfc07b975..abb5043456 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1871,7 +1871,7 @@ def splitElementsToCompleteTuplets( ) -> None: ''' Split notes or rests if doing so will complete any incomplete tuplets. - The element being split must have a duration that equals or exceeds the + The element being split must have a duration that exceeds the remainder of the incomplete tuplet. The first note is edited; the additional notes are inserted in place. From 54e8876e6298b045716cb7f5488dad0d193bf6f0 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 5 Mar 2022 17:48:05 -0500 Subject: [PATCH 11/18] Don't splitAtDurations() if makeNotation=False --- music21/musicxml/m21ToXml.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 478ff2f096..5a69577819 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2547,9 +2547,6 @@ def parse(self): elif not self.stream.getElementsByClass(stream.Measure): raise MusicXMLExportException( 'Cannot export with makeNotation=False if there are no measures') - else: - # QUESTION: destructive edit OK with makeNotation=False? - self.stream = self.stream.splitAtDurations(recurse=True)[0] # make sure that all instances of the same class have unique ids self.spannerBundle.setIdLocals() From 8461838fe6cf02323809ffcb4aefcef74b39820b Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 27 Mar 2022 09:36:53 -0400 Subject: [PATCH 12/18] Rewrite existing test case --- music21/musicxml/partStaffExporter.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/music21/musicxml/partStaffExporter.py b/music21/musicxml/partStaffExporter.py index fc35ad5e29..8a454590b7 100644 --- a/music21/musicxml/partStaffExporter.py +++ b/music21/musicxml/partStaffExporter.py @@ -938,7 +938,13 @@ def testJoinPartStaffsF(self): from music21 import musicxml sch = corpus.parse('schoenberg/opus19', 2) - SX = musicxml.m21ToXml.ScoreExporter(sch.flatten()) + # NB: Using ScoreExporter directly is an advanced use case: + # does not run makeNotation(), so here GeneralObjectExporter is used first + gex = musicxml.m21ToXml.GeneralObjectExporter() + with self.assertWarnsRegex(Warning, 'not well-formed'): + # No part layer. Measure directly under Score. + obj = gex.fromGeneralObject(sch.flatten()) + SX = musicxml.m21ToXml.ScoreExporter(obj) SX.scorePreliminaries() SX.parseFlatScore() # Previously, an exception was raised by getRootForPartStaff() From 6300ae7d1d5468c1d870becc380e7d743f92efbb Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 15 Apr 2022 09:56:06 -0400 Subject: [PATCH 13/18] Bump version added --- music21/stream/makeNotation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index abb5043456..b08794743c 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1878,7 +1878,7 @@ def splitElementsToCompleteTuplets( (Destructive edit, so make a copy first if desired.) Relies on :meth:`~music21.stream.base.splitAtQuarterLength`. - New in v7.3. + New in v8. >>> from music21.stream.makeNotation import splitElementsToCompleteTuplets >>> s = stream.Stream( @@ -1955,7 +1955,7 @@ def consolidateCompletedTuplets( and removing the subsequent elements from the stream. (Destructive edit, so make a copy first if desired.) - New in v7.3. + New in v8. >>> s = stream.Stream() >>> r = note.Rest(quarterLength=1/6) From 59f578e33c287e11a1b3078c332a86c2555c9606 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Fri, 5 Aug 2022 22:55:09 -1000 Subject: [PATCH 14/18] merge master fixes --- music21/stream/base.py | 2 +- music21/stream/makeNotation.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/music21/stream/base.py b/music21/stream/base.py index 94dd8a7fda..cbf1533f1d 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -6723,7 +6723,7 @@ def makeNotation(self: StreamType, makeNotation.makeTies(returnStream, meterStream=meterStream, inPlace=True) - for m in measureStream: + for m in returnStream.getElementsByClass(Measure): makeNotation.splitElementsToCompleteTuplets(m, recurse=True, addTies=True) makeNotation.consolidateCompletedTuplets(m, recurse=True, onlyIfTied=True) diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index 5c8cae10e6..38bf3539a4 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1861,6 +1861,7 @@ def splitElementsToCompleteTuplets( recurse: bool = False, addTies: bool = True ) -> None: + # noinspection PyShadowingNames ''' Split notes or rests if doing so will complete any incomplete tuplets. The element being split must have a duration that exceeds the @@ -1890,13 +1891,15 @@ def splitElementsToCompleteTuplets( >>> [el.quarterLength for el in p.recurse().notesAndRests] [Fraction(1, 6), Fraction(1, 3), Fraction(1, 3), Fraction(1, 6)] ''' + iterator: t.Iterable[Stream] if recurse: iterator = s.recurse(streamsOnly=True, includeSelf=True) else: iterator = [s] + for container in iterator: general_notes = list(container.notesAndRests) - last_tuplet: Optional['music21.duration.Tuplet'] = None + last_tuplet: t.Optional['music21.duration.Tuplet'] = None partial_tuplet_sum = 0.0 for gn in general_notes: if ( @@ -1920,7 +1923,7 @@ def splitElementsToCompleteTuplets( if next_gn and next_gn.offset != opFrac(gn.offset + gn.quarterLength): continue if next_gn and next_gn.duration.expressionIsInferred: - if ql_to_complete > 0 and next_gn.quarterLength > ql_to_complete: + if 0 < ql_to_complete < next_gn.quarterLength: unused_left_edited_in_place, right = next_gn.splitAtQuarterLength( ql_to_complete, addTies=addTies) container.insert(next_gn.offset + ql_to_complete, right) @@ -1932,6 +1935,7 @@ def consolidateCompletedTuplets( recurse: bool = False, onlyIfTied: bool = True, ) -> None: + # noinspection PyShadowingNames ''' Locate consecutive notes or rests in `s` (or its substreams if `recurse` is True) that are unnecessarily expressed as tuplets and replace them with a single @@ -1998,10 +2002,10 @@ def is_reexpressible(gn: note.GeneralNote) -> bool: iterator = [s] for container in iterator: reexpressible = [gn for gn in container.notesAndRests if is_reexpressible(gn)] - to_consolidate: List['music21.note.GeneralNote'] = [] + to_consolidate: t.List['music21.note.GeneralNote'] = [] partial_tuplet_sum = 0.0 - last_tuplet: Optional['music21.duration.Tuplet'] = None - completion_target: Optional[common.types.OffsetQL] = None + last_tuplet: t.Optional['music21.duration.Tuplet'] = None + completion_target: t.Optional[common.types.OffsetQL] = None for gn in reexpressible: prev_gn = gn.previous(note.GeneralNote, activeSiteOnly=True) if ( From c62eb62058f3a6eca416732e076e554d35cc9f0e Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Fri, 5 Aug 2022 23:16:28 -1000 Subject: [PATCH 15/18] fix two more merge problems --- music21/stream/base.py | 2 +- music21/stream/makeNotation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/stream/base.py b/music21/stream/base.py index cbf1533f1d..c764a1a210 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -6727,7 +6727,7 @@ def makeNotation(self: StreamType, makeNotation.splitElementsToCompleteTuplets(m, recurse=True, addTies=True) makeNotation.consolidateCompletedTuplets(m, recurse=True, onlyIfTied=True) - if not measureStream.streamStatus.beams: + if not returnStream.streamStatus.beams: try: makeNotation.makeBeams(returnStream, inPlace=True) except meter.MeterException as me: diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index 38bf3539a4..4a61defdf0 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1891,7 +1891,7 @@ def splitElementsToCompleteTuplets( >>> [el.quarterLength for el in p.recurse().notesAndRests] [Fraction(1, 6), Fraction(1, 3), Fraction(1, 3), Fraction(1, 6)] ''' - iterator: t.Iterable[Stream] + iterator: t.Iterable['music21.stream.Stream'] if recurse: iterator = s.recurse(streamsOnly=True, includeSelf=True) else: From 1727f8bd08202f22a2ee70519bc359b07f8f2165 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Fri, 5 Aug 2022 23:23:05 -1000 Subject: [PATCH 16/18] mypy --- music21/stream/makeNotation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index 4a61defdf0..d0a1f2058d 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -1996,6 +1996,7 @@ def is_reexpressible(gn: note.GeneralNote) -> bool: and (gn.isRest or gn.tie is not None or not onlyIfTied) ) + iterator: t.Iterable['music21.stream.Stream'] if recurse: iterator = s.recurse(streamsOnly=True, includeSelf=True) else: @@ -2003,9 +2004,9 @@ def is_reexpressible(gn: note.GeneralNote) -> bool: for container in iterator: reexpressible = [gn for gn in container.notesAndRests if is_reexpressible(gn)] to_consolidate: t.List['music21.note.GeneralNote'] = [] - partial_tuplet_sum = 0.0 + partial_tuplet_sum: OffsetQL = 0.0 last_tuplet: t.Optional['music21.duration.Tuplet'] = None - completion_target: t.Optional[common.types.OffsetQL] = None + completion_target: t.Optional[OffsetQL] = None for gn in reexpressible: prev_gn = gn.previous(note.GeneralNote, activeSiteOnly=True) if ( From 126e5ca9aa6c63ea6c8fb09ee006dfa30e191ce7 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Fri, 5 Aug 2022 23:31:26 -1000 Subject: [PATCH 17/18] change typing of duration._tuplets --- music21/duration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/duration.py b/music21/duration.py index c26e57ece1..c7541110e1 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -1601,7 +1601,7 @@ def __init__(self, *arguments, **keywords): self._unlinkedType: t.Optional[str] = None self._dotGroups: t.Tuple[int, ...] = (0,) - self._tuplets: t.Union[t.Tuple['Tuplet', ...], t.Tuple] = () # an empty tuple + self._tuplets: t.Tuple['Tuplet', ...] = () # an empty tuple self._qtrLength: OffsetQL = 0.0 # DurationTuples go here From efac0cb942d5698daf92ffa16414a35207129057 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Fri, 5 Aug 2022 23:37:10 -1000 Subject: [PATCH 18/18] do we need an assert? --- music21/stream/makeNotation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index d0a1f2058d..dd897f73cd 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -2045,6 +2045,8 @@ def is_reexpressible(gn: note.GeneralNote) -> bool: if gn.duration.tuplets: partial_tuplet_sum = gn.quarterLength last_tuplet = gn.duration.tuplets[0] + if t.TYPE_CHECKING: + assert last_tuplet is not None completion_target = last_tuplet.totalTupletLength() to_consolidate = [gn] else: