@@ -2047,6 +2047,7 @@ def consolidateCompletedTuplets(
20472047 - be consecutive (with respect to :class:`~music21.note.GeneralNote` objects)
20482048 - be all rests, or all :class:`~music21.note.NotRest`s with equal `.pitches`
20492049 - all have :attr:`~music21.duration.Duration.expressionIsInferred` = `True`.
2050+ - not begin during a tuplet
20502051 - sum to the tuplet's total length
20512052 - if `NotRest`, all must be tied (if `onlyIfTied` is True)
20522053
@@ -2065,7 +2066,7 @@ def consolidateCompletedTuplets(
20652066 >>> [el.quarterLength for el in s.notesAndRests]
20662067 [0.5, Fraction(1, 6), Fraction(1, 6), Fraction(1, 6)]
20672068
2068- `mustBeTied ` is `True` by default:
2069+ `onlyIfTied ` is `True` by default:
20692070
20702071 >>> s2 = stream.Stream()
20712072 >>> n = note.Note(quarterLength=1/3)
@@ -2092,69 +2093,42 @@ def consolidateCompletedTuplets(
20922093
20932094 Does nothing if there are multiple (nested) tuplets.
20942095 '''
2096+ search_state = duration .TupletSearchState (onlyIfTied = onlyIfTied )
20952097 iterator : Iterable [stream .Stream ]
20962098 if recurse :
20972099 iterator = s .recurse (streamsOnly = True , includeSelf = True )
20982100 else :
20992101 iterator = [s ]
21002102 for container in iterator :
2101- to_consolidate : list [note .GeneralNote ] = []
2102- partial_tuplet_sum : OffsetQL = 0.0
2103- last_tuplet : duration .Tuplet | None = None
2104- completion_target : OffsetQL | None = None
2103+ search_state .reset ()
21052104 for gn in container .notesAndRests :
2106- if not (
2107- gn .duration .expressionIsInferred
2108- and len (gn .duration .tuplets ) < 2
2109- and (gn .isRest or gn .tie is not None or not onlyIfTied )
2110- ):
2111- continue
2112- prev_gn = gn .previous (note .GeneralNote , activeSiteOnly = True )
2113- if (
2114- prev_gn in to_consolidate
2115- and (
2116- (isinstance (gn , note .Rest ) and isinstance (prev_gn , note .Rest ))
2117- or (
2118- isinstance (gn , note .NotRest )
2119- and isinstance (prev_gn , note .NotRest )
2120- and gn .pitches == prev_gn .pitches
2121- )
2122- )
2123- and opFrac (prev_gn .offset + prev_gn .quarterLength ) == gn .offset
2124- and len (gn .duration .tuplets ) == 1 and gn .duration .tuplets [0 ] == last_tuplet
2125- ):
2126- partial_tuplet_sum = opFrac (partial_tuplet_sum + gn .quarterLength )
2127- to_consolidate .append (gn )
2128-
2129- if partial_tuplet_sum == completion_target :
2105+ search_state .advance_tuplet_sum (gn )
2106+ if search_state .should_be_tested (gn ):
2107+ try :
2108+ search_state .append (gn )
2109+ except ValueError :
2110+ # Not in a tuplet, keep scanning.
2111+ pass
2112+ elif search_state .to_consolidate :
2113+ # Found during an incomplete tuplet, but doesn't match it.
2114+ search_state .mark_no_consolidation ()
2115+
2116+ if search_state .partial_tuplet_sum == search_state .completion_target :
2117+ if search_state .can_consolidate ():
21302118 # set flag to remake tuplet brackets
21312119 container .streamStatus .tuplets = False
2132- first_note_in_group = to_consolidate [0 ]
2133- for other_note in to_consolidate [1 :]:
2120+ first_note_in_group = search_state .to_consolidate [0 ]
2121+ if t .TYPE_CHECKING :
2122+ assert first_note_in_group is not None
2123+ for other_note in search_state .to_consolidate [1 :]:
2124+ if t .TYPE_CHECKING :
2125+ assert other_note is not None
21342126 container .remove (other_note )
21352127 first_note_in_group .duration .clear ()
21362128 first_note_in_group .duration .tuplets = ()
2137- first_note_in_group .quarterLength = completion_target
2129+ first_note_in_group .quarterLength = search_state .completion_target
2130+ search_state .reset ()
21382131
2139- # reset search values
2140- to_consolidate = []
2141- partial_tuplet_sum = 0.0
2142- last_tuplet = None
2143- completion_target = None
2144- else :
2145- # reset to current values
2146- if gn .duration .tuplets :
2147- partial_tuplet_sum = gn .quarterLength
2148- last_tuplet = gn .duration .tuplets [0 ]
2149- if t .TYPE_CHECKING :
2150- assert last_tuplet is not None
2151- completion_target = last_tuplet .totalTupletLength ()
2152- to_consolidate = [gn ]
2153- else :
2154- to_consolidate = []
2155- partial_tuplet_sum = 0.0
2156- last_tuplet = None
2157- completion_target = None
21582132
21592133@contextlib .contextmanager
21602134def saveAccidentalDisplayStatus (s ) -> t .Generator [None , None , None ]:
@@ -2372,6 +2346,21 @@ def testMakeTiesChangingTimeSignatures(self):
23722346 self .assertEqual (len (pp [stream .Measure ][2 ].notes ), 1 )
23732347 self .assertEqual (pp [stream .Measure ][2 ].notes .first ().duration .quarterLength , 24.0 )
23742348
2349+ def testConsolidateCompletedTupletsNoFalsePositive (self ):
2350+ from fractions import Fraction
2351+ from music21 import converter
2352+
2353+ s = converter .parse ('tinyNotation: 2/4 trip{c8 d8 e8} trip{e8 e8 r8}' )
2354+ for el in s [note .GeneralNote ]:
2355+ el .duration .expressionIsInferred = True
2356+ consolidateCompletedTuplets (s , recurse = True , onlyIfTied = False )
2357+
2358+ # Before, the 3 e8's were consolidated, breaking both tuplets.
2359+ self .assertEqual (
2360+ [gn .quarterLength for gn in s [note .GeneralNote ]],
2361+ [Fraction (1 , 3 )] * 6 ,
2362+ )
2363+
23752364 def testSaveAccidentalDisplayStatus (self ):
23762365 from music21 import interval
23772366 from music21 import stream
0 commit comments