diff --git a/music21/stream/base.py b/music21/stream/base.py index 121bcc0bf..a8f2a9bcf 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -2699,6 +2699,35 @@ def storeAtEnd(self, itemOrList, ignoreSort=False): # Streams cannot reside in end elements, thus do not update is flat self.coreElementsChanged(updateIsFlat=False) + def isAtEnd(self, element: base.Music21Object) -> bool: + ''' + Return True if `element` is stored at the end of this Stream + (via :meth:`~music21.stream.base.Stream.storeAtEnd`). + + >>> s = stream.Stream() + >>> n = note.Note('C') + >>> s.append(n) + >>> b = bar.Barline() + >>> s.storeAtEnd(b) + >>> s.isAtEnd(b) + True + >>> s.isAtEnd(n) + False + + If the element is not in this Stream, :exc:`~music21.sites.SitesException` + is raised: + + >>> s.isAtEnd(note.Note('E')) + Traceback (most recent call last): + music21.sites.SitesException: an entry for this object 0x... is not stored in stream + + + * New in v11. + + AI-assisted. + ''' + return self.elementOffset(element, returnSpecial=True) is OffsetSpecial.AT_END + # -------------------------------------------------------------------------- # all the following call either insert() or append() @@ -9500,7 +9529,10 @@ def findNextElementNotCoincident( # is unsorted originally, this "looking ahead" could become O(n^2). originallySorted = useStream.isSorted rests_lacking_durations: list[note.Rest] = [] - for i, e in enumerate(useStream._elements): + for i, e in enumerate(useStream.elements): + if useStream.isAtEnd(e): + # Elements stored at the end carry no quantizable offset. + continue if processOffsets: o = useStream.elementOffset(e) sign = 1 diff --git a/music21/stream/tests.py b/music21/stream/tests.py index 5f2835685..0686170f9 100644 --- a/music21/stream/tests.py +++ b/music21/stream/tests.py @@ -5063,6 +5063,52 @@ def testStoreAtEndFailures(self): with self.assertRaises(StreamException): s.storeAtEnd(b2) + def testIsAtEnd(self): + ''' + https://github.com/cuthbertLab/music21/issues/1069 + + AI-assisted. + ''' + s = Stream() + n = note.Note('C') + s.append(n) + b = bar.Barline() + s.storeAtEnd(b) + self.assertTrue(s.isAtEnd(b)) + self.assertFalse(s.isAtEnd(n)) + + m = Measure() + nM = note.Note('D') + m.append(nM) + rb = bar.Barline('final') + m.storeAtEnd(rb) + self.assertTrue(m.isAtEnd(rb)) + self.assertFalse(m.isAtEnd(nM)) + + outsider = note.Note('E') + with self.assertRaises(sites.SitesException): + s.isAtEnd(outsider) + + def testQuantizeKeepsElementsStoredAtEnd(self): + ''' + quantize() iterates .elements and skips elements stored at the end, so a + barline keeps its AT_END position instead of being given a real offset. + + AI-assisted. + ''' + s = Stream() + n = note.Note() + n.quarterLength = 0.26 + s.repeatInsert(n, [0.1, 0.49, 0.9]) + b = bar.Barline() + s.storeAtEnd(b) + + q = s.quantize(processOffsets=True, processDurations=True, inPlace=False) + + quantizedBarline = q._endElements[0] + self.assertTrue(q.isAtEnd(quantizedBarline)) + self.assertEqual([q.elementOffset(e) for e in q._elements], [0.0, 0.5, 1.0]) + def testElementsHighestTimeB(self): ''' Test adding elements at the highest time position