1# -*- coding: utf-8 -*- 2# ----------------------------------------------------------------------------- 3# Name: stream/core.py 4# Purpose: mixin class for the core elements of Streams 5# 6# Authors: Michael Scott Cuthbert 7# Christopher Ariza 8# 9# Copyright: Copyright © 2008-2015 Michael Scott Cuthbert and the music21 Project 10# License: BSD, see license.txt 11# ----------------------------------------------------------------------------- 12''' 13the Stream Core Mixin handles the core attributes of streams that 14should be thought of almost as private values and not used except 15by advanced programmers who need the highest speed in programming. 16 17Nothing here promises to be stable. The music21 team can make 18any changes here for efficiency reasons while being considered 19backwards compatible so long as the public methods that call this 20remain stable. 21 22All functions here will eventually begin with `.core`. 23''' 24import copy 25from typing import List, Dict, Union, Tuple, Optional 26from fractions import Fraction 27import unittest 28 29from music21.base import Music21Object 30from music21.common.enums import OffsetSpecial 31from music21.common.numberTools import opFrac 32from music21 import spanner 33from music21 import tree 34from music21.exceptions21 import StreamException, ImmutableStreamException 35from music21.stream.iterator import StreamIterator 36 37# pylint: disable=attribute-defined-outside-init 38class StreamCoreMixin: 39 ''' 40 Core aspects of a Stream's behavior. Any of these can change at any time. 41 ''' 42 def __init__(self): 43 # hugely important -- keeps track of where the _elements are 44 # the _offsetDict is a dictionary where id(element) is the 45 # index and the value is a tuple of offset and element. 46 # offsets can be floats, Fractions, or a member of the enum OffsetSpecial 47 self._offsetDict: Dict[int, Tuple[Union[float, Fraction, str], Music21Object]] = {} 48 49 # self._elements stores Music21Object objects. 50 self._elements: List[Music21Object] = [] 51 52 # self._endElements stores Music21Objects found at 53 # the highestTime of this Stream. 54 self._endElements: List[Music21Object] = [] 55 56 self.isSorted = True 57 # should isFlat become readonly? 58 self.isFlat = True # does it have no embedded Streams 59 60 # someday... 61 # self._elementTree = tree.trees.ElementTree(source=self) 62 63 def coreInsert( 64 self, 65 offset: Union[float, Fraction], 66 element: Music21Object, 67 *, 68 ignoreSort=False, 69 setActiveSite=True 70 ): 71 ''' 72 N.B. -- a "core" method, not to be used by general users. Run .insert() instead. 73 74 A faster way of inserting elements that does no checks, 75 just insertion. 76 77 Only be used in contexts that we know we have a proper, single Music21Object. 78 Best for usage when taking objects in a known Stream and creating a new Stream 79 80 When using this method, the caller is responsible for calling Stream.coreElementsChanged 81 after all operations are completed. 82 83 Do not mix coreInsert with coreAppend operations. 84 85 Returns boolean if the Stream is now sorted. 86 ''' 87 # environLocal.printDebug(['coreInsert', 'self', self, 88 # 'offset', offset, 'element', element]) 89 # need to compare highest time before inserting the element in 90 # the elements list 91 storeSorted = False 92 if not ignoreSort: 93 # # if sorted and our insertion is > the highest time, then 94 # # are still inserted 95 # if self.isSorted is True and self.highestTime <= offset: 96 # storeSorted = True 97 if self.isSorted is True: 98 ht = self.highestTime 99 if ht < offset: 100 storeSorted = True 101 elif ht == offset: 102 if not self._elements: 103 storeSorted = True 104 else: 105 highestSortTuple = self._elements[-1].sortTuple() 106 thisSortTuple = list(element.sortTuple()) 107 thisSortTuple[1] = offset 108 thisSortTuple = tuple(thisSortTuple) 109 110 if highestSortTuple < thisSortTuple: 111 storeSorted = True 112 113 self.coreSetElementOffset( 114 element, 115 float(offset), # why is this not opFrac? 116 addElement=True, 117 setActiveSite=setActiveSite 118 ) 119 element.sites.add(self) 120 # need to explicitly set the activeSite of the element 121 # will be sorted later if necessary 122 self._elements.append(element) 123 # self._elementTree.insert(float(offset), element) 124 return storeSorted 125 126 def coreAppend( 127 self, 128 element: Music21Object, 129 *, 130 setActiveSite=True 131 ): 132 ''' 133 N.B. -- a "core" method, not to be used by general users. Run .append() instead. 134 135 Low level appending; like `coreInsert` does not error check, 136 determine elements changed, or similar operations. 137 138 When using this method, the caller is responsible for calling 139 Stream.coreElementsChanged after all operations are completed. 140 ''' 141 # NOTE: this is not called by append, as that is optimized 142 # for looping multiple elements 143 ht = self.highestTime 144 self.coreSetElementOffset(element, ht, addElement=True) 145 element.sites.add(self) 146 # need to explicitly set the activeSite of the element 147 if setActiveSite: 148 self.coreSelfActiveSite(element) 149 self._elements.append(element) 150 151 # Make this faster 152 # self._elementTree.insert(self.highestTime, element) 153 # does not change sorted state 154 self._setHighestTime(ht + element.duration.quarterLength) 155 # -------------------------------------------------------------------------- 156 # adding and editing Elements and Streams -- all need to call coreElementsChanged 157 # most will set isSorted to False 158 159 def coreSetElementOffset( 160 self, 161 element: Music21Object, 162 offset: Union[int, float, Fraction, str], 163 *, 164 addElement=False, 165 setActiveSite=True 166 ): 167 ''' 168 Sets the Offset for an element, very quickly. 169 Caller is responsible for calling :meth:`~music21.stream.core.coreElementsChanged` 170 afterward. 171 172 >>> s = stream.Stream() 173 >>> s.id = 'Stream1' 174 >>> n = note.Note('B-4') 175 >>> s.insert(10, n) 176 >>> n.offset 177 10.0 178 >>> s.coreSetElementOffset(n, 20.0) 179 >>> n.offset 180 20.0 181 >>> n.getOffsetBySite(s) 182 20.0 183 ''' 184 # Note: not documenting 'highestTime' is on purpose, since can only be done for 185 # elements already stored at end. Infinite loop. 186 try: 187 offset = opFrac(offset) 188 except TypeError: 189 if offset not in OffsetSpecial: # pragma: no cover 190 raise StreamException(f'Cannot set offset to {offset!r} for {element}') 191 192 idEl = id(element) 193 if not addElement and idEl not in self._offsetDict: 194 raise StreamException( 195 f'Cannot set the offset for element {element}, not in Stream {self}.') 196 self._offsetDict[idEl] = (offset, element) # fast 197 if setActiveSite: 198 self.coreSelfActiveSite(element) 199 200 def coreElementsChanged( 201 self, 202 *, 203 updateIsFlat=True, 204 clearIsSorted=True, 205 memo=None, 206 keepIndex=False, 207 ): 208 ''' 209 NB -- a "core" stream method that is not necessary for most users. 210 211 This method is called automatically any time the elements in the Stream are changed. 212 However, it may be called manually in case sites or other advanced features of an 213 element have been modified. It was previously a private method and for most users 214 should still be treated as such. 215 216 The various arguments permit optimizing the clearing of cached data in situations 217 when completely dropping all cached data is excessive. 218 219 >>> a = stream.Stream() 220 >>> a.isFlat 221 True 222 223 Here we manipulate the private `._elements` storage (which generally shouldn't 224 be done) using coreAppend and thus need to call `.coreElementsChanged` directly. 225 226 >>> a.coreAppend(stream.Stream()) 227 >>> a.isFlat # this is wrong. 228 True 229 230 >>> a.coreElementsChanged() 231 >>> a.isFlat 232 False 233 ''' 234 # experimental 235 if not self._mutable: 236 raise ImmutableStreamException( 237 'coreElementsChanged should not be triggered on an immutable stream' 238 ) 239 240 if memo is None: 241 memo = [] 242 243 if id(self) in memo: 244 return 245 memo.append(id(self)) 246 247 # WHY??? THIS SEEMS OVERKILL, esp. since the first call to .sort() in .flatten() will 248 # invalidate it! TODO: Investigate if this is necessary and then remove if not necessary 249 # should not need to do this... 250 251 # if this Stream is a flat representation of something, and its 252 # elements have changed, than we must clear the cache of that 253 # ancestor so that subsequent calls get a new representation of this derivation; 254 # we can do that by calling coreElementsChanged on 255 # the derivation.origin 256 if self._derivation is not None: 257 sdm = self._derivation.method 258 if sdm in ('flat', 'semiflat'): 259 origin: 'music21.stream.Stream' = self._derivation.origin 260 origin.clearCache() 261 262 # may not always need to clear cache of all living sites, but may 263 # always be a good idea since .flatten() has changed etc. 264 # should not need to do derivation.origin sites. 265 for livingSite in self.sites: 266 livingSite.coreElementsChanged(memo=memo) 267 268 # clear these attributes for setting later 269 if clearIsSorted: 270 self.isSorted = False 271 272 if updateIsFlat: 273 self.isFlat = True 274 # do not need to look in _endElements 275 for e in self._elements: 276 # only need to find one case, and if so, no longer flat 277 # fastest method here is isinstance() 278 # if isinstance(e, Stream): 279 if e.isStream: 280 self.isFlat = False 281 break 282 # resetting the cache removes lowest and highest time storage 283 # a slight performance optimization: not creating unless needed 284 if self._cache: 285 indexCache = None 286 if keepIndex and 'index' in self._cache: 287 indexCache = self._cache['index'] 288 # always clear cache when elements have changed 289 # for instance, Duration will change. 290 # noinspection PyAttributeOutsideInit 291 self._cache = {} # cannot call clearCache() because defined on Stream via Music21Object 292 if keepIndex and indexCache is not None: 293 self._cache['index'] = indexCache 294 295 def coreCopyAsDerivation(self, methodName: str, *, recurse=True, deep=True): 296 ''' 297 Make a copy of this stream with the proper derivation set. 298 299 >>> s = stream.Stream() 300 >>> n = note.Note() 301 >>> s.append(n) 302 >>> s2 = s.coreCopyAsDerivation('exampleCopy') 303 >>> s2.derivation.method 304 'exampleCopy' 305 >>> s2.derivation.origin is s 306 True 307 >>> s2[0].derivation.method 308 'exampleCopy' 309 ''' 310 if deep: 311 post = copy.deepcopy(self) 312 else: # pragma: no cover 313 post = copy.copy(self) 314 post.derivation.method = methodName 315 if recurse and deep: 316 post.setDerivationMethod(methodName, recurse=True) 317 return post 318 319 def coreHasElementByMemoryLocation(self, objId: int) -> bool: 320 ''' 321 NB -- a "core" stream method that is not necessary for most users. use hasElement(obj) 322 323 Return True if an element object id, provided as an argument, is contained in this Stream. 324 325 >>> s = stream.Stream() 326 >>> n1 = note.Note('g') 327 >>> n2 = note.Note('g#') 328 >>> s.append(n1) 329 >>> s.coreHasElementByMemoryLocation(id(n1)) 330 True 331 >>> s.coreHasElementByMemoryLocation(id(n2)) 332 False 333 ''' 334 if objId in self._offsetDict: 335 return True 336 337 for e in self._elements: 338 if id(e) == objId: # pragma: no cover 339 return True 340 for e in self._endElements: 341 if id(e) == objId: # pragma: no cover 342 return True 343 return False 344 345 def coreGetElementByMemoryLocation(self, objId): 346 ''' 347 NB -- a "core" stream method that is not necessary for most users. 348 349 Low-level tool to get an element based only on the object id. 350 351 This is not the same as getElementById, which refers to the id 352 attribute which may be manually set and not unique. 353 354 However, some implementations of python will reuse object locations, sometimes 355 quickly, so don't keep these around. 356 357 Used by spanner and variant. 358 359 >>> s = stream.Stream() 360 >>> n1 = note.Note('g') 361 >>> n2 = note.Note('g#') 362 >>> s.append(n1) 363 >>> s.coreGetElementByMemoryLocation(id(n1)) is n1 364 True 365 >>> s.coreGetElementByMemoryLocation(id(n2)) is None 366 True 367 >>> b = bar.Barline() 368 >>> s.storeAtEnd(b) 369 >>> s.coreGetElementByMemoryLocation(id(b)) is b 370 True 371 ''' 372 # NOTE: this may be slightly faster than other approaches 373 # as it does not sort. 374 for e in self._elements: 375 if id(e) == objId: 376 return e 377 for e in self._endElements: 378 if id(e) == objId: 379 return e 380 return None 381 382 # -------------------------------------------------------------------------- 383 def coreGuardBeforeAddElement(self, element, *, checkRedundancy=True): 384 ''' 385 Before adding an element, this method performs 386 important checks on that element. 387 388 Used by: 389 390 - :meth:`~music21.stream.Stream.insert` 391 - :meth:`~music21.stream.Stream.append` 392 - :meth:`~music21.stream.Stream.storeAtEnd` 393 - `Stream.__init__()` 394 395 Returns None or raises a StreamException 396 397 >>> s = stream.Stream() 398 >>> s.coreGuardBeforeAddElement(s) 399 Traceback (most recent call last): 400 music21.exceptions21.StreamException: this Stream cannot be contained within itself 401 402 >>> s.append(s.iter()) 403 Traceback (most recent call last): 404 music21.exceptions21.StreamException: cannot insert StreamIterator into a Stream 405 Iterate over it instead (User's Guide chs. 6 and 26) 406 407 >>> s.insert(4, 3.14159) 408 Traceback (most recent call last): 409 music21.exceptions21.StreamException: to put a non Music21Object in a stream, 410 create a music21.ElementWrapper for the item 411 ''' 412 if element is self: # cannot add this Stream into itself 413 raise StreamException('this Stream cannot be contained within itself') 414 if not isinstance(element, Music21Object): 415 if isinstance(element, StreamIterator): 416 raise StreamException('cannot insert StreamIterator into a Stream\n' 417 "Iterate over it instead (User's Guide chs. 6 and 26)") 418 raise StreamException('to put a non Music21Object in a stream, ' 419 'create a music21.ElementWrapper for the item') 420 if checkRedundancy: 421 # using id() here b/c we do not want to get __eq__ comparisons 422 idElement = id(element) 423 if idElement in self._offsetDict: 424 # now go slow for safety -- maybe something is amiss in the index. 425 # this should not happen, but we have slipped many times in not clearing out 426 # old _offsetDict entries. 427 for search_place in (self._elements, self._endElements): 428 for eInStream in search_place: 429 if eInStream is element: 430 raise StreamException( 431 f'the object ({element!r}, id()={id(element)} ' 432 + f'is already found in this Stream ({self!r}, id()={id(self)})' 433 ) 434 # something was old... delete from _offsetDict 435 # environLocal.warn('stale object') 436 del self._offsetDict[idElement] # pragma: no cover 437 # if we do not purge locations here, we may have ids() for 438 # Streams that no longer exist stored in the locations entry for element. 439 # Note that dead locations are also purged from .sites during 440 # all get() calls. 441 element.purgeLocations() 442 443 def coreStoreAtEnd(self, element, setActiveSite=True): 444 ''' 445 NB -- this is a "core" method. Use .storeAtEnd() instead. 446 447 Core method for adding end elements. 448 To be called by other methods. 449 ''' 450 self.coreSetElementOffset(element, OffsetSpecial.AT_END, addElement=True) 451 element.sites.add(self) 452 # need to explicitly set the activeSite of the element 453 if setActiveSite: 454 self.coreSelfActiveSite(element) 455 # self._elements.append(element) 456 self._endElements.append(element) 457 458 @property 459 def spannerBundle(self): 460 ''' 461 A low-level object for Spanner management. This is a read-only property. 462 ''' 463 if 'spannerBundle' not in self._cache or self._cache['spannerBundle'] is None: 464 spanners = self.recurse(classFilter=(spanner.Spanner,), restoreActiveSites=False) 465 self._cache['spannerBundle'] = spanner.SpannerBundle(list(spanners)) 466 return self._cache['spannerBundle'] 467 468 def asTimespans(self, classList=None, flatten=True): 469 r''' 470 Convert stream to a :class:`~music21.tree.trees.TimespanTree` instance, a 471 highly optimized data structure for searching through elements and 472 offsets. 473 474 >>> score = tree.makeExampleScore() 475 >>> scoreTree = score.asTimespans() 476 >>> print(scoreTree) 477 <TimespanTree {20} (0.0 to 8.0) <music21.stream.Score exampleScore>> 478 <ElementTimespan (0.0 to 0.0) <music21.clef.BassClef>> 479 <ElementTimespan (0.0 to 0.0) <music21.meter.TimeSignature 2/4>> 480 <ElementTimespan (0.0 to 0.0) <music21.instrument.Instrument 'PartA: : '>> 481 <ElementTimespan (0.0 to 0.0) <music21.clef.BassClef>> 482 <ElementTimespan (0.0 to 0.0) <music21.meter.TimeSignature 2/4>> 483 <ElementTimespan (0.0 to 0.0) <music21.instrument.Instrument 'PartB: : '>> 484 <PitchedTimespan (0.0 to 1.0) <music21.note.Note C>> 485 <PitchedTimespan (0.0 to 2.0) <music21.note.Note C#>> 486 <PitchedTimespan (1.0 to 2.0) <music21.note.Note D>> 487 <PitchedTimespan (2.0 to 3.0) <music21.note.Note E>> 488 <PitchedTimespan (2.0 to 4.0) <music21.note.Note G#>> 489 <PitchedTimespan (3.0 to 4.0) <music21.note.Note F>> 490 <PitchedTimespan (4.0 to 5.0) <music21.note.Note G>> 491 <PitchedTimespan (4.0 to 6.0) <music21.note.Note E#>> 492 <PitchedTimespan (5.0 to 6.0) <music21.note.Note A>> 493 <PitchedTimespan (6.0 to 7.0) <music21.note.Note B>> 494 <PitchedTimespan (6.0 to 8.0) <music21.note.Note D#>> 495 <PitchedTimespan (7.0 to 8.0) <music21.note.Note C>> 496 <ElementTimespan (8.0 to 8.0) <music21.bar.Barline type=final>> 497 <ElementTimespan (8.0 to 8.0) <music21.bar.Barline type=final>> 498 ''' 499 hashedAttributes = hash((tuple(classList or ()), flatten)) 500 cacheKey = "timespanTree" + str(hashedAttributes) 501 if cacheKey not in self._cache or self._cache[cacheKey] is None: 502 hashedTimespanTree = tree.fromStream.asTimespans(self, 503 flatten=flatten, 504 classList=classList) 505 self._cache[cacheKey] = hashedTimespanTree 506 return self._cache[cacheKey] 507 508 def coreSelfActiveSite(self, el): 509 ''' 510 Set the activeSite of el to be self. 511 512 Override for SpannerStorage, VariantStorage, which should never 513 become the activeSite 514 ''' 515 el.activeSite = self 516 517 def asTree(self, flatten=False, classList=None, useTimespans=False, groupOffsets=False): 518 ''' 519 Returns an elementTree of the score, using exact positioning. 520 521 See tree.fromStream.asTree() for more details. 522 523 >>> score = tree.makeExampleScore() 524 >>> scoreTree = score.asTree(flatten=True) 525 >>> scoreTree 526 <ElementTree {20} (0.0 <0.-25...> to 8.0) <music21.stream.Score exampleScore>> 527 ''' 528 hashedAttributes = hash((tuple(classList or ()), 529 flatten, 530 useTimespans, 531 groupOffsets)) 532 cacheKey = "elementTree" + str(hashedAttributes) 533 if cacheKey not in self._cache or self._cache[cacheKey] is None: 534 hashedElementTree = tree.fromStream.asTree(self, 535 flatten=flatten, 536 classList=classList, 537 useTimespans=useTimespans, 538 groupOffsets=groupOffsets) 539 self._cache[cacheKey] = hashedElementTree 540 return self._cache[cacheKey] 541 542 def coreGatherMissingSpanners( 543 self, 544 *, 545 recurse=True, 546 requireAllPresent=True, 547 insert=True, 548 constrainingSpannerBundle: Optional[spanner.SpannerBundle] = None 549 ) -> Optional[List[spanner.Spanner]]: 550 ''' 551 find all spanners that are referenced by elements in the 552 (recursed if recurse=True) stream and either inserts them in the Stream 553 (if insert is True) or returns them if insert is False. 554 555 If requireAllPresent is True (default) then only those spanners whose complete 556 spanned elements are in the Stream are returned. 557 558 Because spanners are stored weakly in .sites this is only guaranteed to find 559 the spanners in cases where the spanner is in another stream that is still active. 560 561 Here's a little helper function since we'll make the same Stream several times, 562 with two slurred notes, but without the slur itself. Python's garbage collection 563 will get rid of the slur if we do not prevent it 564 565 >>> preventGarbageCollection = [] 566 >>> def getStream(): 567 ... s = stream.Stream() 568 ... n = note.Note('C') 569 ... m = note.Note('D') 570 ... sl = spanner.Slur(n, m) 571 ... preventGarbageCollection.append(sl) 572 ... s.append([n, m]) 573 ... return s 574 575 Okay now we have a Stream with two slurred notes, but without the slur. 576 `coreGatherMissingSpanners()` will put it in at the beginning. 577 578 >>> s = getStream() 579 >>> s.show('text') 580 {0.0} <music21.note.Note C> 581 {1.0} <music21.note.Note D> 582 >>> s.coreGatherMissingSpanners() 583 >>> s.show('text') 584 {0.0} <music21.note.Note C> 585 {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>> 586 {1.0} <music21.note.Note D> 587 588 Now, the same Stream, but insert is False, so it will return a list of 589 Spanners that should be inserted, rather than inserting them. 590 591 >>> s = getStream() 592 >>> spList = s.coreGatherMissingSpanners(insert=False) 593 >>> spList 594 [<music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>] 595 >>> s.show('text') 596 {0.0} <music21.note.Note C> 597 {1.0} <music21.note.Note D> 598 599 600 Now we'll remove the second note so not all elements of the slur 601 are present, which by default will not insert the Slur: 602 603 >>> s = getStream() 604 >>> s.remove(s[-1]) 605 >>> s.show('text') 606 {0.0} <music21.note.Note C> 607 >>> s.coreGatherMissingSpanners() 608 >>> s.show('text') 609 {0.0} <music21.note.Note C> 610 611 But with `requireAllPresent=False`, the spanner appears! 612 613 >>> s.coreGatherMissingSpanners(requireAllPresent=False) 614 >>> s.show('text') 615 {0.0} <music21.note.Note C> 616 {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>> 617 618 With `recurse=False`, then spanners are not gathered inside the inner 619 stream: 620 621 >>> t = stream.Part() 622 >>> s = getStream() 623 >>> t.insert(0, s) 624 >>> t.coreGatherMissingSpanners(recurse=False) 625 >>> t.show('text') 626 {0.0} <music21.stream.Stream 0x104935b00> 627 {0.0} <music21.note.Note C> 628 {1.0} <music21.note.Note D> 629 630 631 But the default acts with recursion: 632 633 >>> t.coreGatherMissingSpanners() 634 >>> t.show('text') 635 {0.0} <music21.stream.Stream 0x104935b00> 636 {0.0} <music21.note.Note C> 637 {1.0} <music21.note.Note D> 638 {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>> 639 640 641 Spanners already in the stream are not put there again: 642 643 >>> s = getStream() 644 >>> sl = s.notes.first().getSpannerSites()[0] 645 >>> sl 646 <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>> 647 >>> s.insert(0, sl) 648 >>> s.coreGatherMissingSpanners() 649 >>> s.show('text') 650 {0.0} <music21.note.Note C> 651 {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>> 652 {1.0} <music21.note.Note D> 653 654 Also does not happen with recursion. 655 656 >>> t = stream.Part() 657 >>> s = getStream() 658 >>> sl = s.notes.first().getSpannerSites()[0] 659 >>> s.insert(0, sl) 660 >>> t.insert(0, s) 661 >>> t.coreGatherMissingSpanners() 662 >>> t.show('text') 663 {0.0} <music21.stream.Stream 0x104935b00> 664 {0.0} <music21.note.Note C> 665 {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>> 666 {1.0} <music21.note.Note D> 667 668 If `constrainingSpannerBundle` is set then only spanners also present in 669 that spannerBundle are added. This can be useful, for instance, in restoring 670 spanners from an excerpt that might already have spanners removed. In 671 Jacob Tyler Walls's brilliant phrasing, it prevents regrowing zombie spanners 672 that you thought you had killed. 673 674 Here we will constrain only to spanners also present in another Stream: 675 676 >>> s = getStream() 677 >>> s2 = stream.Stream() 678 >>> s.coreGatherMissingSpanners(constrainingSpannerBundle=s2.spannerBundle) 679 >>> s.show('text') 680 {0.0} <music21.note.Note C> 681 {1.0} <music21.note.Note D> 682 683 Now with the same constraint, but we will put the Slur into the other stream. 684 685 >>> sl = s.notes.first().getSpannerSites()[0] 686 >>> s2.insert(0, sl) 687 >>> s.coreGatherMissingSpanners(constrainingSpannerBundle=s2.spannerBundle) 688 >>> s.show('text') 689 {0.0} <music21.note.Note C> 690 {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>> 691 {1.0} <music21.note.Note D> 692 ''' 693 sb = self.spannerBundle 694 if recurse is True: 695 sIter = self.recurse() 696 else: 697 sIter = self.iter() 698 699 collectList = [] 700 for el in list(sIter): 701 for sp in el.getSpannerSites(): 702 if sp in sb: 703 continue 704 if sp in collectList: 705 continue 706 if constrainingSpannerBundle is not None and sp not in constrainingSpannerBundle: 707 continue 708 if requireAllPresent: 709 allFound = True 710 for spannedElement in sp.getSpannedElements(): 711 if spannedElement not in sIter: 712 allFound = False 713 break 714 if allFound is False: 715 continue 716 collectList.append(sp) 717 718 if insert is False: 719 return collectList 720 elif collectList: # do not run elementsChanged if nothing here. 721 for sp in collectList: 722 self.coreInsert(0, sp) 723 self.coreElementsChanged(updateIsFlat=False) 724 725# timing before: Macbook Air 2012, i7 726# In [3]: timeit('s = stream.Stream()', setup='from music21 import stream', number=100000) 727# Out[3]: 1.6291376419831067 728 729# after adding subclass -- actually faster, showing the rounding error: 730# In [2]: timeit('s = stream.Stream()', setup='from music21 import stream', number=100000) 731# Out[2]: 1.5247003990225494 732 733 734class Test(unittest.TestCase): 735 pass 736 737 738if __name__ == '__main__': 739 import music21 740 music21.mainTest(Test) 741