1# -*- coding: utf-8 -*- 2# ----------------------------------------------------------------------------- 3# Name: base.py 4# Purpose: Music21 base classes and important utilities 5# 6# Authors: Michael Scott Cuthbert 7# Christopher Ariza 8# 9# Copyright: Copyright © 2006-2021 Michael Scott Cuthbert and the music21 10# Project 11# License: BSD, see license.txt 12# ----------------------------------------------------------------------------- 13''' 14`music21.base` is what you get in `music21` if you type ``import music21``. It 15contains all the most low-level objects that also appear in the music21 module 16(i.e., music21.base.Music21Object is the same as music21.Music21Object). 17 18Music21 base classes for :class:`~music21.stream.Stream` objects and all 19elements contained within them including Notes, etc. Additional objects for 20defining and manipulating elements are included. 21 22The namespace of this file, as all base.py files, is loaded into the package 23that contains this file via __init__.py. Everything in this file is thus 24available after importing `music21`. 25 26>>> import music21 27>>> music21.Music21Object 28<class 'music21.base.Music21Object'> 29 30>>> music21.VERSION_STR 31'7.1.0' 32 33Alternatively, after doing a complete import, these classes are available 34under the module "base": 35 36>>> base.Music21Object 37<class 'music21.base.Music21Object'> 38''' 39import copy 40import sys 41import types 42import unittest 43 44from collections import namedtuple 45from importlib.util import find_spec 46 47# for type annotation only 48import fractions 49from typing import ( 50 Any, 51 Dict, 52 FrozenSet, 53 Iterable, 54 List, 55 Optional, 56 Union, 57 Tuple, 58 Type, 59 TypeVar, 60) 61 62from music21 import common 63from music21.common.enums import ElementSearch, OffsetSpecial 64from music21.common.numberTools import opFrac 65from music21.common.types import OffsetQL, OffsetQLIn 66from music21 import environment 67from music21 import editorial 68from music21 import defaults 69from music21.derivation import Derivation 70from music21 import duration 71from music21 import prebase 72from music21 import sites 73from music21 import style # pylint: disable=unused-import 74from music21.sites import SitesException 75from music21.sorting import SortTuple, ZeroSortTupleLow, ZeroSortTupleHigh 76# needed for temporal manipulations; not music21 objects 77from music21 import tie 78from music21 import exceptions21 79from music21._version import __version__, __version_info__ 80from music21.test.testRunner import mainTest 81 82 83# This should actually be bound to Music21Object, but cannot import here. 84_M21T = TypeVar('_M21T', bound=prebase.ProtoM21Object) 85 86# all other music21 modules below... 87 88# ----------------------------------------------------------------------------- 89# version string and tuple must be the same 90 91VERSION = __version_info__ 92VERSION_STR = __version__ 93# ----------------------------------------------------------------------------- 94__all__ = [ 95 'Music21Exception', 96 'SitesException', 97 'Music21ObjectException', 98 'ElementException', 99 100 'Groups', 101 'Music21Object', 102 'ElementWrapper', 103 104 'VERSION', 105 'VERSION_STR', 106 'mainTest', 107] 108 109# N.B. for PyDev "all" import working, we need to list this 110# separately in "music21/__init__.py" 111# so make sure to update in both places 112 113# ----all exceptions are in the exceptions21 package. 114 115Music21Exception = exceptions21.Music21Exception 116 117 118# ?? pylint does not think that this was used... 119 120_MOD = 'base' 121environLocal = environment.Environment(_MOD) 122 123_missingImport = [] 124for modName in ('matplotlib', 'numpy'): 125 loader = find_spec(modName) 126 if loader is None: # pragma: no cover 127 _missingImport.append(modName) 128 129del find_spec 130del modName 131 132if _missingImport: # pragma: no cover 133 if environLocal['warnings'] in (1, '1', True): 134 environLocal.warn(common.getMissingImportStr(_missingImport), 135 header='music21:') 136 137 138class Music21ObjectException(exceptions21.Music21Exception): 139 pass 140 141 142class ElementException(exceptions21.Music21Exception): 143 pass 144 145 146# ----------------------------------------------------------------------------- 147# for contextSites searches... 148ContextTuple = namedtuple('ContextTuple', 'site offset recurseType') 149 150 151# pseudo class for returning splitAtX() type commands. 152class _SplitTuple(tuple): 153 ''' 154 >>> st = base._SplitTuple([1, 2]) 155 >>> st.spannerList = [3] 156 >>> st 157 (1, 2) 158 >>> st.spannerList 159 [3] 160 >>> a, b = st 161 >>> a 162 1 163 >>> b 164 2 165 >>> st.__class__ 166 <class 'music21.base._SplitTuple'> 167 ''' 168 def __new__(cls, tupEls): 169 # noinspection PyTypeChecker 170 return super(_SplitTuple, cls).__new__(cls, tuple(tupEls)) 171 172 def __init__(self, tupEls): # pylint: disable=super-init-not-called 173 self.spannerList = [] 174 175# ----------------------------------------------------------------------------- 176# make subclass of set once that is defined properly 177 178 179class Groups(list): # no need to inherit from slotted object 180 ''' 181 Groups is a list (subclass) of strings used to identify 182 associations that an element might have. 183 184 The Groups object enforces that all elements must be strings, and that 185 the same element cannot be provided more than once. 186 187 NOTE: In the future, spaces will not be allowed in group names. 188 189 190 >>> g = Groups() 191 >>> g.append('hello') 192 >>> g[0] 193 'hello' 194 195 >>> g.append('hello') # not added as already present 196 >>> len(g) 197 1 198 199 >>> g 200 ['hello'] 201 202 >>> g.append(5) 203 Traceback (most recent call last): 204 music21.exceptions21.GroupException: Only strings can be used as group names, not 5 205 ''' 206 # could be made into a set instance, but actually 207 # timing: a subclassed list and a set are almost the same speed 208 # and set does not allow calling by number 209 210 # this speeds up creation slightly... 211 __slots__ = () 212 213 def _validName(self, value: str): 214 if not isinstance(value, str): 215 raise exceptions21.GroupException('Only strings can be used as group names, ' 216 + f'not {value!r}') 217 # if ' ' in value: 218 # raise exceptions21.GroupException('Spaces are not allowed as group names') 219 220 def append(self, value: Union[int, str]) -> None: 221 self._validName(value) 222 if not list.__contains__(self, value): 223 list.append(self, value) 224 225 def __setitem__(self, i: int, y: Union[int, str]): 226 self._validName(y) 227 super().__setitem__(i, y) 228 229 def __eq__(self, other: 'Groups'): 230 ''' 231 Test Group equality. In normal lists, order matters; here it does not. More like a set. 232 233 >>> a = base.Groups() 234 >>> a.append('red') 235 >>> a.append('green') 236 >>> a 237 ['red', 'green'] 238 239 >>> b = base.Groups() 240 >>> b.append('green') 241 >>> a == b 242 False 243 244 >>> b.append('reD') # case insensitive 245 >>> a == b 246 True 247 248 >>> a == ['red', 'green'] # need both to be groups 249 False 250 251 >>> c = base.Groups() 252 >>> c.append('black') 253 >>> c.append('tuba') 254 >>> a == c 255 False 256 ''' 257 if not isinstance(other, Groups): 258 return False 259 260 if len(self) != len(other): 261 return False 262 sls = sorted(self) 263 slo = sorted(other) 264 265 for x in range(len(sls)): 266 if sls[x].lower() != slo[x].lower(): 267 return False 268 return True 269 270 271# ----------------------------------------------------------------------------- 272 273 274class Music21Object(prebase.ProtoM21Object): 275 ''' 276 Base class for all music21 objects. 277 278 All music21 objects have these pieces of information: 279 280 1. id: identification string unique to the objects container (optional). 281 Defaults to the `id()` of the element. 282 2. groups: a Groups object: which is a list of strings identifying 283 internal sub-collections (voices, parts, selections) to which this 284 element belongs 285 3. duration: Duration object representing the length of the object 286 4. activeSite: a reference to the currently active Stream or None 287 5. offset: a floating point value, generally in quarter lengths, 288 specifying the position of the object in a site. 289 6. priority: int representing the position of an object among all 290 objects at the same offset. 291 7. sites: a :class:`~music21.base.Sites` object that stores all 292 the Streams and Contexts that an 293 object is in. 294 8. derivation: a :class:`~music21.derivation.Derivation` object, or None, that shows 295 where the object came from. 296 9. style: a :class:`~music21.style.Style` object, that contains Style information 297 automatically created if it doesn't exist, so check `.hasStyleInformation` first 298 if that is not desired. 299 10. editorial: a :class:`~music21.editorial.Editorial` object 300 301 302 303 Each of these may be passed in as a named keyword to any music21 object. 304 305 Some of these may be intercepted by the subclassing object (e.g., duration 306 within Note) 307 ''' 308 309 classSortOrder: Union[int, float] = 20 # default classSortOrder 310 # these values permit fast class comparisons for performance critical cases 311 isStream = False 312 313 _styleClass: Type[style.Style] = style.Style 314 315 # define order to present names in documentation; use strings 316 _DOC_ORDER = [] 317 318 # documentation for all attributes (not properties or methods) 319 _DOC_ATTR = { 320 'id': '''A unique identification string; not to be confused with the 321 default `.id()` method. However, if not set, will return 322 the `id()` number''', 323 'groups': '''An instance of a :class:`~music21.base.Group` 324 object which describes 325 arbitrary `Groups` that this object belongs to.''', 326 'isStream': '''Boolean value for quickly identifying 327 :class:`~music21.stream.Stream` objects (False by default).''', 328 'classSortOrder': '''Property which returns an number (int or otherwise) 329 depending on the class of the Music21Object that 330 represents a priority for an object based on its class alone -- 331 used as a tie for stream sorting in case two objects have the 332 same offset and priority. Lower numbers are sorted to the left 333 of higher numbers. For instance, Clef, KeySignature, TimeSignature 334 all come (in that order) before Note. 335 336 All undefined classes have classSortOrder of 20 -- same as note.Note 337 338 >>> m21o = base.Music21Object() 339 >>> m21o.classSortOrder 340 20 341 342 >>> tc = clef.TrebleClef() 343 >>> tc.classSortOrder 344 0 345 346 >>> ks = key.KeySignature(3) 347 >>> ks.classSortOrder 348 2 349 350 351 New classes can define their own default classSortOrder 352 353 >>> class ExampleClass(base.Music21Object): 354 ... classSortOrder = 5 355 ... 356 >>> ec1 = ExampleClass() 357 >>> ec1.classSortOrder 358 5 359 ''', 360 } 361 362 def __init__(self, *arguments, **keywords): 363 # do not call super().__init__() since it just wastes time 364 # None is stored as the internal location of an obj w/o any sites 365 self._activeSite: Optional['music21.stream.Stream'] = None 366 # offset when no activeSite is available 367 self._naiveOffset: Union[float, fractions.Fraction] = 0.0 368 369 # offset when activeSite is already garbage collected/dead, 370 # as in short-lived sites 371 # like .getElementsByClass().stream() 372 self._activeSiteStoredOffset: Union[float, fractions.Fraction, None] = None 373 374 # store a derivation object to track derivations from other Streams 375 # pass a reference to this object 376 self._derivation: Optional[Derivation] = None 377 378 self._style: Optional[style.Style] = None 379 self._editorial = None 380 381 # private duration storage; managed by property 382 self._duration: Optional[duration.Duration] = None 383 self._priority = 0 # default is zero 384 385 # store cached values here: 386 self._cache: Dict[str, Any] = {} 387 388 389 if 'id' in keywords: 390 self.id = keywords['id'] 391 else: 392 self.id = id(self) 393 394 if 'groups' in keywords and keywords['groups'] is not None: 395 self.groups = keywords['groups'] 396 else: 397 self.groups = Groups() 398 399 if 'sites' in keywords: 400 self.sites = keywords['sites'] 401 else: 402 self.sites = sites.Sites() 403 404 # a duration object is not created until the .duration property is 405 # accessed with _getDuration(); this is a performance optimization 406 if 'duration' in keywords: 407 self.duration = keywords['duration'] 408 if 'activeSite' in keywords: 409 self.activeSite = keywords['activeSite'] 410 if 'style' in keywords: 411 self.style = keywords['style'] 412 if 'editorial' in keywords: 413 self.editorial = keywords['editorial'] 414 415 def mergeAttributes(self, other: 'Music21Object') -> None: 416 ''' 417 Merge all elementary, static attributes. Namely, 418 `id` and `groups` attributes from another music21 object. 419 Can be useful for copy-like operations. 420 421 >>> m1 = base.Music21Object() 422 >>> m2 = base.Music21Object() 423 >>> m1.id = 'music21Object1' 424 >>> m1.groups.append('group1') 425 >>> m2.mergeAttributes(m1) 426 >>> m2.id 427 'music21Object1' 428 >>> 'group1' in m2.groups 429 True 430 ''' 431 if other.id != id(other): 432 self.id = other.id 433 self.groups = copy.deepcopy(other.groups) 434 435 # PyCharm 2019 does not know that copy.deepcopy can take a memo argument 436 # noinspection PyArgumentList 437 def _deepcopySubclassable(self: _M21T, 438 memo=None, 439 ignoreAttributes=None, 440 removeFromIgnore=None) -> _M21T: 441 ''' 442 Subclassable __deepcopy__ helper so that the same attributes 443 do not need to be called 444 for each Music21Object subclass. 445 446 ignoreAttributes is a set of attributes not to copy via 447 the default deepcopy style. 448 More can be passed to it. 449 450 removeFromIgnore can be a set of attributes to remove 451 from ignoreAttributes of a superclass. 452 453 TODO: move to class attributes to cache. 454 ''' 455 defaultIgnoreSet = {'_derivation', '_activeSite', 'id', 456 'sites', '_duration', '_style', '_cache'} 457 if ignoreAttributes is None: 458 ignoreAttributes = defaultIgnoreSet 459 else: 460 ignoreAttributes = ignoreAttributes | defaultIgnoreSet 461 462 if removeFromIgnore is not None: 463 ignoreAttributes = ignoreAttributes - removeFromIgnore 464 465 # call class to get a new, empty instance 466 # TODO: this creates an extra duration object for notes... optimize... 467 new = self.__class__() 468 # environLocal.printDebug(['Music21Object.__deepcopy__', self, id(self)]) 469 # for name in dir(self): 470 if '_duration' in ignoreAttributes: 471 # this can be done much faster in most cases... 472 d = self._duration 473 if d is not None: 474 clientStore = self._duration._client 475 self._duration._client = None 476 newValue = copy.deepcopy(self._duration, memo) 477 self._duration._client = clientStore 478 newValue.client = new 479 setattr(new, '_duration', newValue) 480 481 if '_derivation' in ignoreAttributes: 482 # was: keep the old ancestor but need to update the client 483 # 2.1 : NO, add a derivation of __deepcopy__ to the client 484 newDerivation = Derivation(client=new) 485 newDerivation.origin = self 486 newDerivation.method = '__deepcopy__' 487 setattr(new, '_derivation', newDerivation) 488 489 if '_activeSite' in ignoreAttributes: 490 # TODO: Fix this so as not to allow incorrect _activeSite (???) 491 # keep a reference, not a deepcopy 492 # do not use property: .activeSite; set to same weakref obj 493 # TODO: restore jan 2020 (was Jan 2018) 494 # setattr(new, '_activeSite', None) 495 setattr(new, '_activeSite', self._activeSite) 496 497 if 'id' in ignoreAttributes: 498 value = getattr(self, 'id') 499 if value != id(self) or ( 500 common.isNum(value) 501 and value < defaults.minIdNumberToConsiderMemoryLocation 502 ): 503 newValue = value 504 setattr(new, 'id', newValue) 505 if 'sites' in ignoreAttributes: 506 # we make a copy of the sites value even though it is obsolete because 507 # the spanners will need to be preserved and then set to the new value 508 # elsewhere. The purgeOrphans call later will remove all but 509 # spanners and variants. 510 value = getattr(self, 'sites') 511 # this calls __deepcopy__ in Sites 512 newValue = copy.deepcopy(value, memo) 513 setattr(new, 'sites', newValue) 514 if '_style' in ignoreAttributes: 515 value = getattr(self, '_style', None) 516 if value is not None: 517 newValue = copy.deepcopy(value, memo) 518 setattr(new, '_style', newValue) 519 520 for name in self.__dict__: 521 if name.startswith('__'): 522 continue 523 if name in ignoreAttributes: 524 continue 525 526 attrValue = getattr(self, name) 527 # attributes that do not require special handling 528 try: 529 deeplyCopiedObject = copy.deepcopy(attrValue, memo) 530 setattr(new, name, deeplyCopiedObject) 531 except TypeError as te: # pragma: no cover 532 if not isinstance(attrValue, Music21Object): 533 # shallow copy then... 534 try: 535 shallowlyCopiedObject = copy.copy(attrValue) 536 setattr(new, name, shallowlyCopiedObject) 537 environLocal.printDebug( 538 '__deepcopy__: Could not deepcopy ' 539 + f'{name} in {self}, not a Music21Object' 540 + 'so making a shallow copy') 541 except TypeError: 542 # just link... 543 environLocal.printDebug( 544 '__deepcopy__: Could not copy (deep or shallow) ' 545 + f'{name} in {self}, not a Music21Object so just making a link' 546 ) 547 setattr(new, name, attrValue) 548 else: # raise error for our own problem. # pragma: no cover 549 raise Music21Exception( 550 '__deepcopy__: Cannot deepcopy Music21Object ' 551 + f'{name} probably because it requires a default value in instantiation.' 552 ) from te 553 554 return new 555 556 def __deepcopy__(self: _M21T, memo: Optional[Dict[int, Any]] = None) -> _M21T: 557 ''' 558 Helper method to copy.py's deepcopy function. Call it from there. 559 560 memo=None is the default as specified in copy.py 561 562 Problem: if an attribute is defined with an underscore (_priority) but 563 is also made available through a property (e.g. priority) using dir(self) 564 results in the copy happening twice. Thus, __dict__.keys() is used. 565 566 >>> from copy import deepcopy 567 >>> n = note.Note('A') 568 >>> n.offset = 1.0 569 >>> n.groups.append('flute') 570 >>> n.groups 571 ['flute'] 572 573 >>> idN = n.id 574 >>> idN > 10000 # pointer 575 True 576 577 >>> b = deepcopy(n) 578 >>> b.offset = 2.0 579 580 >>> n is b 581 False 582 >>> b.id != n.id 583 True 584 >>> n.pitch.accidental = '-' 585 >>> b.name 586 'A' 587 >>> n.offset 588 1.0 589 >>> b.offset 590 2.0 591 >>> n.groups[0] = 'bassoon' 592 >>> ('flute' in n.groups, 'flute' in b.groups) 593 (False, True) 594 ''' 595 # environLocal.printDebug(['calling Music21Object.__deepcopy__', self]) 596 new = self._deepcopySubclassable(memo) 597 # must do this after copying 598 new.purgeOrphans() 599 # environLocal.printDebug([self, 'end deepcopy', 'self._activeSite', self._activeSite]) 600 return new 601 602 def __getstate__(self) -> Dict[str, Any]: 603 state = self.__dict__.copy() 604 state['_derivation'] = None 605 state['_activeSite'] = None 606 return state 607 608 def __setstate__(self, state: Dict[str, Any]): 609 # defining self.__dict__ upon initialization currently breaks everything 610 self.__dict__ = state # pylint: disable=attribute-defined-outside-init 611 612 # -------------------------------------------------------------------------- 613 614 @property 615 def hasEditorialInformation(self): 616 ''' 617 Returns True if there is a :class:`~music21.editorial.Editorial` object 618 already associated with this object, False otherwise. 619 620 Calling .style on an object will always create a new 621 Style object, so even though a new Style object isn't too expensive 622 to create, this property helps to prevent creating new Styles more than 623 necessary. 624 625 >>> mObj = base.Music21Object() 626 >>> mObj.hasEditorialInformation 627 False 628 >>> mObj.editorial 629 <music21.editorial.Editorial {}> 630 >>> mObj.hasEditorialInformation 631 True 632 ''' 633 return not (self._editorial is None) 634 635 @property 636 def editorial(self) -> 'music21.editorial.Editorial': 637 ''' 638 a :class:`~music21.editorial.Editorial` object that stores editorial information 639 (comments, footnotes, harmonic information, ficta). 640 641 Created automatically as needed: 642 643 >>> n = note.Note('C4') 644 >>> n.editorial 645 <music21.editorial.Editorial {}> 646 >>> n.editorial.ficta = pitch.Accidental('sharp') 647 >>> n.editorial.ficta 648 <music21.pitch.Accidental sharp> 649 >>> n.editorial 650 <music21.editorial.Editorial {'ficta': <music21.pitch.Accidental sharp>}> 651 ''' 652 if self._editorial is None: 653 self._editorial = editorial.Editorial() 654 return self._editorial 655 656 @editorial.setter 657 def editorial(self, ed: 'music21.editorial.Editorial'): 658 self._editorial = ed 659 660 @property 661 def hasStyleInformation(self) -> bool: 662 ''' 663 Returns True if there is a :class:`~music21.style.Style` object 664 already associated with this object, False otherwise. 665 666 Calling .style on an object will always create a new 667 Style object, so even though a new Style object isn't too expensive 668 to create, this property helps to prevent creating new Styles more than 669 necessary. 670 671 >>> mObj = base.Music21Object() 672 >>> mObj.hasStyleInformation 673 False 674 >>> mObj.style 675 <music21.style.Style object at 0x10b0a2080> 676 >>> mObj.hasStyleInformation 677 True 678 ''' 679 return not (self._style is None) 680 681 @property 682 def style(self) -> 'music21.style.Style': 683 ''' 684 Returns (or Creates and then Returns) the Style object 685 associated with this object, or sets a new 686 style object. Different classes might use 687 different Style objects because they might have different 688 style needs (such as text formatting or bezier positioning) 689 690 Eventually will also query the groups to see if they have 691 any styles associated with them. 692 693 >>> n = note.Note() 694 >>> st = n.style 695 >>> st 696 <music21.style.NoteStyle object at 0x10ba96208> 697 >>> st.absoluteX = 20.0 698 >>> st.absoluteX 699 20.0 700 >>> n.style = style.Style() 701 >>> n.style.absoluteX is None 702 True 703 ''' 704 if self._style is None: 705 styleClass = self._styleClass 706 self._style = styleClass() 707 return self._style 708 709 @style.setter 710 def style(self, newStyle: Optional['music21.style.Style']): 711 self._style = newStyle 712 713 # -------------------------- 714 # convenience. used to be in note.Note, but belongs everywhere: 715 716 @property 717 def quarterLength(self) -> OffsetQL: 718 ''' 719 Set or Return the Duration as represented in Quarter Length, possibly as a fraction. 720 721 >>> n = note.Note() 722 >>> n.quarterLength = 2.0 723 >>> n.quarterLength 724 2.0 725 >>> n.quarterLength = 1/3 726 >>> n.quarterLength 727 Fraction(1, 3) 728 ''' 729 return self.duration.quarterLength 730 731 @quarterLength.setter 732 def quarterLength(self, value: OffsetQLIn): 733 self.duration.quarterLength = value 734 735 @property 736 def derivation(self) -> Derivation: 737 ''' 738 Return the :class:`~music21.derivation.Derivation` object for this element. 739 740 Or create one if none exists: 741 742 >>> n = note.Note() 743 >>> n.derivation 744 <Derivation of <music21.note.Note C> from None> 745 >>> import copy 746 >>> n2 = copy.deepcopy(n) 747 >>> n2.pitch.step = 'D' # for seeing easier... 748 >>> n2.derivation 749 <Derivation of <music21.note.Note D> from <music21.note.Note C> via '__deepcopy__'> 750 >>> n2.derivation.origin is n 751 True 752 753 Note that (for now at least) derivation.origin is NOT a weakref: 754 755 >>> del n 756 >>> n2.derivation 757 <Derivation of <music21.note.Note D> from <music21.note.Note C> via '__deepcopy__'> 758 >>> n2.derivation.origin 759 <music21.note.Note C> 760 ''' 761 if self._derivation is None: 762 self._derivation = Derivation(client=self) 763 return self._derivation 764 765 @derivation.setter 766 def derivation(self, newDerivation: Optional[Derivation]) -> None: 767 self._derivation = newDerivation 768 769 def clearCache(self, **keywords): 770 ''' 771 A number of music21 attributes (especially with Chords and RomanNumerals, etc.) 772 are expensive to compute and are therefore cached. Generally speaking 773 objects are responsible for making sure that their own caches are up to date, 774 but a power user might want to do something in an unusual way (such as manipulating 775 private attributes on a Pitch object) and need to be able to clear caches. 776 777 That's what this is here for. If all goes well, you'll never need to call it 778 unless you're expanding music21's core functionality. 779 780 `**keywords` is not used in Music21Object but is included for subclassing. 781 782 Look at :func:`~music21.common.decorators.cacheMethod` for the other half of this 783 utility. 784 785 New in v.6 -- exposes previously hidden functionality. 786 ''' 787 self._cache = {} 788 789 def getOffsetBySite( 790 self, 791 site: 'music21.stream.Stream', 792 *, 793 returnSpecial=False, 794 stringReturns=False, 795 ) -> Union[float, fractions.Fraction, str]: 796 ''' 797 If this class has been registered in a container such as a Stream, 798 that container can be provided here, and the offset in that object 799 can be returned. 800 801 >>> n = note.Note('A-4') # a Music21Object 802 >>> n.offset = 30 803 >>> n.getOffsetBySite(None) 804 30.0 805 806 >>> s1 = stream.Stream() 807 >>> s1.id = 'containingStream' 808 >>> s1.insert(20 / 3, n) 809 >>> n.getOffsetBySite(s1) 810 Fraction(20, 3) 811 >>> float(n.getOffsetBySite(s1)) 812 6.6666... 813 814 n.getOffsetBySite(None) should still return 30.0 815 816 >>> n.getOffsetBySite(None) 817 30.0 818 819 If the Stream does not contain the element and the element is not derived from 820 an element that does, then a SitesException is raised: 821 822 >>> s2 = stream.Stream() 823 >>> s2.id = 'notContainingStream' 824 >>> n.getOffsetBySite(s2) 825 Traceback (most recent call last): 826 music21.sites.SitesException: an entry for this object <music21.note.Note A-> is not 827 stored in stream <music21.stream.Stream notContainingStream> 828 829 Consider this use of derivations: 830 831 >>> import copy 832 >>> nCopy = copy.deepcopy(n) 833 >>> nCopy.derivation 834 <Derivation of <music21.note.Note A-> from <music21.note.Note A-> via '__deepcopy__'> 835 >>> nCopy.getOffsetBySite(s1) 836 Fraction(20, 3) 837 838 nCopy can still find the offset of `n` in `s1`! 839 This is the primary difference between element.getOffsetBySite(stream) 840 and stream.elementOffset(element) 841 842 >>> s1.elementOffset(nCopy) 843 Traceback (most recent call last): 844 music21.sites.SitesException: an entry for this object ... is not 845 stored in stream <music21.stream.Stream containingStream> 846 847 If the object is stored at the end of the Stream, then the highest time 848 is usually returned: 849 850 >>> s3 = stream.Stream() 851 >>> n3 = note.Note(type='whole') 852 >>> s3.append(n3) 853 >>> rb = bar.Barline() 854 >>> s3.storeAtEnd(rb) # s3.rightBarline = rb would do the same... 855 >>> rb.getOffsetBySite(s3) 856 4.0 857 858 However, setting returnSpecial to True will return OffsetSpecial.AT_END 859 860 >>> rb.getOffsetBySite(s3, returnSpecial=True) 861 <OffsetSpecial.AT_END> 862 863 Even with returnSpecial normal offsets are still returned as a float or Fraction: 864 865 >>> n3.getOffsetBySite(s3, returnSpecial=True) 866 0.0 867 868 Changed in v7. -- stringReturns renamed to returnSpecial. Returns an OffsetSpecial Enum. 869 ''' 870 if stringReturns and not returnSpecial: # pragma: no cover 871 returnSpecial = stringReturns 872 environLocal.warn('stringReturns is deprecated: use returnSpecial instead') 873 874 if site is None: 875 return self._naiveOffset 876 877 try: 878 a = None 879 tryOrigin = self 880 originMemo = set() 881 maxSearch = 100 882 while a is None: 883 try: 884 a = site.elementOffset(tryOrigin, returnSpecial=returnSpecial) 885 except AttributeError as ae: 886 raise SitesException( 887 f'You were using {site!r} as a site, when it is not a Stream...' 888 ) from ae 889 except Music21Exception as e: # currently StreamException, but will change 890 if tryOrigin in site._endElements: 891 if returnSpecial is True: 892 return OffsetSpecial.AT_END 893 else: 894 return site.highestTime 895 896 tryOrigin = self.derivation.origin 897 if id(tryOrigin) in originMemo: 898 raise e 899 originMemo.add(id(tryOrigin)) 900 maxSearch -= 1 # prevent infinite recursive searches... 901 if tryOrigin is None or maxSearch < 0: 902 raise e 903 return a 904 except SitesException as se: 905 raise SitesException( 906 f'an entry for this object {self!r} is not stored in stream {site!r}' 907 ) from se 908 909 def setOffsetBySite(self, 910 site: Optional['music21.stream.Stream'], 911 value: Union[int, float, fractions.Fraction]): 912 ''' 913 Change the offset for a site. These are equivalent: 914 915 n1.setOffsetBySite(stream1, 20) 916 917 and 918 919 stream1.setElementOffset(n1, 20) 920 921 Which you choose to use will depend on whether you are iterating over a list 922 of notes (etc.) or streams. 923 924 >>> import music21 925 >>> aSite = stream.Stream() 926 >>> aSite.id = 'aSite' 927 >>> a = music21.Music21Object() 928 >>> aSite.insert(0, a) 929 >>> aSite.setElementOffset(a, 20) 930 >>> a.setOffsetBySite(aSite, 30) 931 >>> a.getOffsetBySite(aSite) 932 30.0 933 934 And if it isn't in a Stream? Raises an exception and the offset does not change. 935 936 >>> b = note.Note('D') 937 >>> b.setOffsetBySite(aSite, 40) 938 Traceback (most recent call last): 939 music21.exceptions21.StreamException: Cannot set the offset for element 940 <music21.note.Note D>, not in Stream <music21.stream.Stream aSite>. 941 942 >>> b.offset 943 0.0 944 945 Setting offset for `None` changes the "naive offset" of an object: 946 947 >>> b.setOffsetBySite(None, 32) 948 >>> b.offset 949 32.0 950 >>> b.activeSite is None 951 True 952 953 Running `setOffsetBySite` also changes the `activeSite` of the object. 954 ''' 955 if site is not None: 956 site.setElementOffset(self, value) 957 else: 958 if isinstance(value, int): 959 value = float(value) 960 self._naiveOffset = value 961 962 def getOffsetInHierarchy(self, site) -> Union[float, fractions.Fraction]: 963 ''' 964 For an element which may not be in site, but might be in a Stream in site (or further 965 in streams), find the cumulative offset of the element in that site. 966 967 >>> s = stream.Score(id='mainScore') 968 >>> p = stream.Part() 969 >>> m = stream.Measure() 970 >>> n = note.Note() 971 >>> m.insert(5.0, n) 972 >>> p.insert(10.0, m) 973 >>> s.insert(0.0, p) 974 >>> n.getOffsetInHierarchy(s) 975 15.0 976 977 If no hierarchy beginning with site contains the element 978 and the element is not derived from 979 an element that does, then a SitesException is raised: 980 981 >>> s2 = stream.Score(id='otherScore') 982 >>> n.getOffsetInHierarchy(s2) 983 Traceback (most recent call last): 984 music21.sites.SitesException: Element <music21.note.Note C> 985 is not in hierarchy of <music21.stream.Score otherScore> 986 987 But if the element is derived from an element in 988 a hierarchy then it can get the offset: 989 990 >>> n2 = n.transpose('P5') 991 >>> n2.derivation.origin is n 992 True 993 >>> n2.derivation.method 994 'transpose' 995 >>> n2.getOffsetInHierarchy(s) 996 15.0 997 998 There is no corresponding `.setOffsetInHierarchy()` 999 since it's unclear what that would mean. 1000 1001 See also :meth:`music21.stream.iterator.RecursiveIterator.currentHierarchyOffset` 1002 for a method that is about 10x faster when running through a recursed stream. 1003 1004 *new in v.3* 1005 1006 OMIT_FROM_DOCS 1007 1008 Timing: 113microseconds for a search vs 1 microsecond for getOffsetBySite 1009 vs 0.4 for elementOffset. Hence the short-circuit for easy looking below... 1010 1011 TODO: If timing permits, replace .flatten() w/ and w/o retainContainers with this routine. 1012 1013 Currently not possible; for instance, if b = bwv66.6 1014 1015 %timeit b = corpus.parse('bwv66.6') -- 24.8ms 1016 %timeit b = corpus.parse('bwv66.6'); b.flatten() -- 42.9ms 1017 %timeit b = corpus.parse('bwv66.6'); b.recurse().stream() -- 83.1ms 1018 ''' 1019 try: 1020 return self.getOffsetBySite(site) 1021 except SitesException: 1022 pass 1023 1024 # do not use priorityTarget, just slows things down because will need to search 1025 # all anyhow, since site is not in self.sites.yieldSites() 1026 for cs in self.contextSites(): 1027 if cs.site is site: 1028 return cs.offset 1029 1030 raise SitesException(f'Element {self} is not in hierarchy of {site}') 1031 1032 def getSpannerSites(self, spannerClassList=None) -> List['music21.spanner.Spanner']: 1033 ''' 1034 Return a list of all :class:`~music21.spanner.Spanner` objects 1035 (or Spanner subclasses) that contain 1036 this element. This method provides a way for 1037 objects to be aware of what Spanners they 1038 reside in. Note that Spanners are not Streams 1039 but specialized Music21Objects that use a 1040 Stream subclass, SpannerStorage, internally to keep track 1041 of the elements that are spanned. 1042 1043 >>> n1 = note.Note('C4') 1044 >>> n2 = note.Note('D4') 1045 >>> sp1 = spanner.Slur(n1, n2) 1046 >>> n1.getSpannerSites() == [sp1] 1047 True 1048 1049 Note that not all Spanners are in the spanner module. They 1050 tend to reside in modules closer to their musical function: 1051 1052 >>> sp2 = dynamics.Crescendo(n2, n1) 1053 1054 The order that Spanners are returned is usually the order they 1055 were created, but on fast computers there can be ties, so use 1056 a set comparison if you expect multiple: 1057 1058 >>> set(n2.getSpannerSites()) == {sp1, sp2} 1059 True 1060 1061 Optionally a class name or list of class names can be 1062 specified and only Spanners of that class will be returned 1063 1064 >>> sp3 = dynamics.Diminuendo(n1, n2) 1065 >>> n2.getSpannerSites('Diminuendo') == [sp3] 1066 True 1067 1068 A larger class name can be used to get all subclasses: 1069 1070 >>> set(n2.getSpannerSites('DynamicWedge')) == {sp2, sp3} 1071 True 1072 >>> set(n2.getSpannerSites(['Slur', 'Diminuendo'])) == {sp1, sp3} 1073 True 1074 1075 1076 >>> set(n2.getSpannerSites(['Slur', 'Diminuendo'])) == {sp3, sp1} 1077 True 1078 1079 1080 Example: see which pairs of notes are in the same slur. 1081 1082 >>> n3 = note.Note('E4') 1083 >>> sp4 = spanner.Slur(n1, n3) 1084 1085 >>> for n in [n1, n2, n3]: 1086 ... for nOther in [n1, n2, n3]: 1087 ... if n is nOther: 1088 ... continue 1089 ... nSlurs = n.getSpannerSites('Slur') 1090 ... nOtherSlurs = nOther.getSpannerSites('Slur') 1091 ... for thisSlur in nSlurs: 1092 ... if thisSlur in nOtherSlurs: 1093 ... print(f'{n.name} shares a slur with {nOther.name}') 1094 C shares a slur with D 1095 C shares a slur with E 1096 D shares a slur with C 1097 E shares a slur with C 1098 ''' 1099 found = self.sites.getSitesByClass('SpannerStorage') 1100 post = [] 1101 if spannerClassList is not None: 1102 if not common.isIterable(spannerClassList): 1103 spannerClassList = [spannerClassList] 1104 1105 for obj in found: 1106 if obj is None: # pragma: no cover 1107 continue 1108 if spannerClassList is None: 1109 post.append(obj.spannerParent) 1110 else: 1111 for spannerClass in spannerClassList: 1112 if spannerClass in obj.spannerParent.classes: 1113 post.append(obj.spannerParent) 1114 break 1115 1116 return post 1117 1118 def purgeOrphans(self, excludeStorageStreams=True) -> None: 1119 ''' 1120 A Music21Object may, due to deep copying or other reasons, 1121 have a site (with an offset) which 1122 no longer contains the Music21Object. These lingering sites 1123 are called orphans. This methods gets rid of them. 1124 1125 The `excludeStorageStreams` are SpannerStorage and VariantStorage. 1126 ''' 1127 # environLocal.printDebug(['purging orphans']) 1128 orphans = [] 1129 # TODO: how can this be optimized to not use getSites, so as to 1130 # not unwrap weakrefs? 1131 for s in self.sites.yieldSites(excludeNone=True): 1132 # of the site does not actually have this Music21Object in 1133 # its elements list, it is an orphan and should be removed 1134 # note: this permits non-site context Streams to continue 1135 if s.isStream and not s.hasElement(self): 1136 if excludeStorageStreams: 1137 # only get those that are not Storage Streams 1138 if ('SpannerStorage' not in s.classes 1139 and 'VariantStorage' not in s.classes): 1140 # environLocal.printDebug(['removing orphan:', s]) 1141 orphans.append(id(s)) 1142 else: # get all 1143 orphans.append(id(s)) 1144 for i in orphans: 1145 self.sites.removeById(i) 1146 p = self._getActiveSite() # this can be simplified. 1147 if p is not None and id(p) == i: 1148 # noinspection PyArgumentList 1149 self._setActiveSite(None) 1150 1151 def purgeLocations(self, rescanIsDead=False) -> None: 1152 ''' 1153 Remove references to all locations in objects that no longer exist. 1154 ''' 1155 # NOTE: this method is overridden on Spanner 1156 # and Variant, so not an easy fix to remove... 1157 self.sites.purgeLocations(rescanIsDead=rescanIsDead) 1158 1159 # -------------------------------------------------------------------------------- 1160 # contexts... 1161 1162 def getContextByClass( 1163 self, 1164 className, 1165 *, 1166 getElementMethod=ElementSearch.AT_OR_BEFORE, 1167 sortByCreationTime=False, 1168 followDerivation=True, 1169 priorityTargetOnly=False, 1170 ) -> Optional['Music21Object']: 1171 # noinspection PyShadowingNames 1172 ''' 1173 A very powerful method in music21 of fundamental importance: Returns 1174 the element matching the className that is closest to this element in 1175 its current hierarchy (or the hierarchy of the derivation origin unless 1176 `followDerivation` is False. For instance, take this stream of changing time 1177 signatures: 1178 1179 >>> p = converter.parse('tinynotation: 3/4 C4 D E 2/4 F G A B 1/4 c') 1180 >>> p 1181 <music21.stream.Part 0x104ce64e0> 1182 1183 >>> p.show('t') 1184 {0.0} <music21.stream.Measure 1 offset=0.0> 1185 {0.0} <music21.clef.BassClef> 1186 {0.0} <music21.meter.TimeSignature 3/4> 1187 {0.0} <music21.note.Note C> 1188 {1.0} <music21.note.Note D> 1189 {2.0} <music21.note.Note E> 1190 {3.0} <music21.stream.Measure 2 offset=3.0> 1191 {0.0} <music21.meter.TimeSignature 2/4> 1192 {0.0} <music21.note.Note F> 1193 {1.0} <music21.note.Note G> 1194 {5.0} <music21.stream.Measure 3 offset=5.0> 1195 {0.0} <music21.note.Note A> 1196 {1.0} <music21.note.Note B> 1197 {7.0} <music21.stream.Measure 4 offset=7.0> 1198 {0.0} <music21.meter.TimeSignature 1/4> 1199 {0.0} <music21.note.Note C> 1200 {1.0} <music21.bar.Barline type=final> 1201 1202 Let's get the last two notes of the piece, the B and high c: 1203 1204 >>> m4 = p.measure(4) 1205 >>> c = m4.notes.first() 1206 >>> c 1207 <music21.note.Note C> 1208 1209 >>> m3 = p.measure(3) 1210 >>> b = m3.notes.last() 1211 >>> b 1212 <music21.note.Note B> 1213 1214 Now when we run `getContextByClass('TimeSignature')` on c, we get a 1215 time signature of 1/4. 1216 1217 >>> c.getContextByClass('TimeSignature') 1218 <music21.meter.TimeSignature 1/4> 1219 1220 Doing what we just did wouldn't be hard to do with other methods, 1221 though `getContextByClass` makes it easier. But the time signature 1222 context for b would be much harder to get without this method, since in 1223 order to do it, it searches backwards within the measure, finds that 1224 there's nothing there. It goes to the previous measure and searches 1225 that one backwards until it gets the proper TimeSignature of 2/4: 1226 1227 >>> b.getContextByClass('TimeSignature') 1228 <music21.meter.TimeSignature 2/4> 1229 1230 The method is smart enough to stop when it gets to the beginning of the 1231 part. This is all you need to know for most uses. The rest of the 1232 docs are for advanced uses: 1233 1234 The method searches both Sites as well as associated objects to find a 1235 matching class. Returns `None` if no match is found. 1236 1237 A reference to the caller is required to find the offset of the object 1238 of the caller. 1239 1240 The caller may be a Sites reference from a lower-level object. If so, 1241 we can access the location of that lower-level object. However, if we 1242 need a flat representation, the caller needs to be the source Stream, 1243 not its Sites reference. 1244 1245 The `getElementMethod` is an enum value (new in v.7) from 1246 :class:`~music21.common.enums.ElementSearch` that selects which 1247 Stream method is used to get elements for searching. (The historical form 1248 of supplying one of the following values as a string is also supported.) 1249 1250 >>> from music21.common.enums import ElementSearch 1251 >>> [x for x in ElementSearch] 1252 [<ElementSearch.BEFORE>, 1253 <ElementSearch.AFTER>, 1254 <ElementSearch.AT_OR_BEFORE>, 1255 <ElementSearch.AT_OR_AFTER>, 1256 <ElementSearch.BEFORE_OFFSET>, 1257 <ElementSearch.AFTER_OFFSET>, 1258 <ElementSearch.AT_OR_BEFORE_OFFSET>, 1259 <ElementSearch.AT_OR_AFTER_OFFSET>, 1260 <ElementSearch.BEFORE_NOT_SELF>, 1261 <ElementSearch.AFTER_NOT_SELF>, 1262 <ElementSearch.ALL>] 1263 1264 The "after" do forward contexts -- looking ahead. 1265 1266 Demonstrations of these keywords: 1267 1268 Because `b` is a `Note`, `.getContextByClass('Note')` will only find itself: 1269 1270 >>> b.getContextByClass('Note') is b 1271 True 1272 1273 To get the previous `Note`, use `getElementMethod=ElementSearch.BEFORE`: 1274 1275 >>> a = b.getContextByClass('Note', getElementMethod=ElementSearch.BEFORE) 1276 >>> a 1277 <music21.note.Note A> 1278 1279 This is similar to `.previous('Note')`, though that method is a bit more 1280 sophisticated: 1281 1282 >>> b.previous('Note') 1283 <music21.note.Note A> 1284 1285 To get the following `Note` use `getElementMethod=ElementSearch.AFTER`: 1286 1287 >>> c = b.getContextByClass('Note', getElementMethod=ElementSearch.AFTER) 1288 >>> c 1289 <music21.note.Note C> 1290 1291 This is similar to `.next('Note')`, though, again, that method is a bit more 1292 sophisticated: 1293 1294 >>> b.next('Note') 1295 <music21.note.Note C> 1296 1297 A Stream might contain several elements at the same offset, leading to 1298 potentially surprising results where searching by `ElementSearch.AT_OR_BEFORE` 1299 does not find an element that is technically the NEXT node but still at 0.0: 1300 1301 >>> s = stream.Stream() 1302 >>> s.insert(0, clef.BassClef()) 1303 >>> s.next() 1304 <music21.clef.BassClef> 1305 >>> s.getContextByClass(clef.Clef) is None 1306 True 1307 >>> s.getContextByClass(clef.Clef, getElementMethod=ElementSearch.AT_OR_AFTER) 1308 <music21.clef.BassClef> 1309 1310 This can be remedied by explicitly searching by offsets: 1311 1312 >>> s.getContextByClass(clef.Clef, getElementMethod=ElementSearch.AT_OR_BEFORE_OFFSET) 1313 <music21.clef.BassClef> 1314 1315 Or by not limiting the search by temporal position at all: 1316 1317 >>> s.getContextByClass(clef.Clef, getElementMethod=ElementSearch.ALL) 1318 <music21.clef.BassClef> 1319 1320 Notice that if searching for a `Stream` context, the element is not 1321 guaranteed to be in that Stream. This is obviously true in this case: 1322 1323 >>> p2 = stream.Part() 1324 >>> m = stream.Measure(number=1) 1325 >>> p2.insert(0, m) 1326 >>> n = note.Note('D') 1327 >>> m.insert(2.0, n) 1328 >>> try: 1329 ... n.getContextByClass('Part').elementOffset(n) 1330 ... except Music21Exception: 1331 ... print('not there') 1332 not there 1333 1334 But it is less clear with something like this: 1335 1336 >>> import copy 1337 >>> n2 = copy.deepcopy(n) 1338 >>> try: 1339 ... n2.getContextByClass('Measure').elementOffset(n2) 1340 ... except Music21Exception: 1341 ... print('not there') 1342 not there 1343 1344 A measure context is being found, but only through the derivation chain. 1345 1346 >>> n2.getContextByClass('Measure') 1347 <music21.stream.Measure 1 offset=0.0> 1348 1349 To prevent this error, use the `followDerivation=False` setting 1350 1351 >>> print(n2.getContextByClass('Measure', followDerivation=False)) 1352 None 1353 1354 Or if you want the offset of the element following the derivation chain, 1355 call `getOffsetBySite()` on the object: 1356 1357 >>> n2.getOffsetBySite(n2.getContextByClass('Measure')) 1358 2.0 1359 1360 * changed in v.5.7 -- added followDerivation=False and made 1361 everything but the class keyword only 1362 * added in v.6 -- added priorityTargetOnly -- see contextSites for description. 1363 * added in v.7 -- added getElementMethod `all` and `ElementSearch` enum. 1364 1365 Raises `ValueError` if `getElementMethod` is not a value in `ElementSearch`. 1366 1367 >>> n2.getContextByClass('TextExpression', getElementMethod='invalid') 1368 Traceback (most recent call last): 1369 ValueError: Invalid getElementMethod: invalid 1370 1371 OMIT_FROM_DOCS 1372 1373 Testing that this works: 1374 1375 >>> import gc 1376 >>> _ = gc.collect() 1377 >>> for site, positionStart, searchType in b.contextSites( 1378 ... returnSortTuples=True, 1379 ... sortByCreationTime=False, 1380 ... followDerivation=True 1381 ... ): 1382 ... print(site, positionStart, searchType) 1383 <music21.stream.Measure 3 offset=5.0> SortTuple(atEnd=0, offset=1.0, ...) elementsFirst 1384 <music21.stream.Part 0x1118cadd8> SortTuple(atEnd=0, offset=6.0, ...) flatten 1385 ''' 1386 OFFSET_METHODS = [ 1387 ElementSearch.BEFORE_OFFSET, 1388 ElementSearch.AFTER_OFFSET, 1389 ElementSearch.AT_OR_BEFORE_OFFSET, 1390 ElementSearch.AT_OR_AFTER_OFFSET, 1391 ] 1392 BEFORE_METHODS = [ 1393 ElementSearch.BEFORE, 1394 ElementSearch.BEFORE_OFFSET, 1395 ElementSearch.AT_OR_BEFORE, 1396 ElementSearch.AT_OR_BEFORE_OFFSET, 1397 ElementSearch.BEFORE_NOT_SELF, 1398 ] 1399 AFTER_METHODS = [ 1400 ElementSearch.AFTER, 1401 ElementSearch.AFTER_OFFSET, 1402 ElementSearch.AT_OR_AFTER, 1403 ElementSearch.AT_OR_AFTER, 1404 ElementSearch.AT_OR_AFTER_OFFSET, 1405 ElementSearch.AFTER_NOT_SELF, 1406 ] 1407 AT_METHODS = [ 1408 ElementSearch.AT_OR_BEFORE, 1409 ElementSearch.AT_OR_AFTER, 1410 ElementSearch.AT_OR_BEFORE_OFFSET, 1411 ElementSearch.AT_OR_AFTER_OFFSET, 1412 ] 1413 NOT_SELF_METHODS = [ 1414 ElementSearch.BEFORE_NOT_SELF, 1415 ElementSearch.AFTER_NOT_SELF, 1416 ] 1417 # ALL is just a no-op 1418 def payloadExtractor(checkSite, flatten, innerPositionStart): 1419 ''' 1420 change the site (stream) to a Tree (using caches if possible), 1421 then find the node before (or after) the positionStart and 1422 return the element there or None. 1423 1424 flatten can be True, 'semiFlat', or False. 1425 ''' 1426 siteTree = checkSite.asTree(flatten=flatten, classList=className) 1427 if getElementMethod in OFFSET_METHODS: 1428 # these methods match only by offset. Used in .getBeat among other places 1429 if getElementMethod in (ElementSearch.BEFORE_OFFSET, 1430 ElementSearch.AT_OR_AFTER_OFFSET): 1431 innerPositionStart = ZeroSortTupleLow.modify(offset=innerPositionStart.offset) 1432 else: 1433 innerPositionStart = ZeroSortTupleHigh.modify(offset=innerPositionStart.offset) 1434 1435 if getElementMethod in BEFORE_METHODS: 1436 contextNode = siteTree.getNodeBefore(innerPositionStart) 1437 else: 1438 contextNode = siteTree.getNodeAfter(innerPositionStart) 1439 1440 if contextNode is not None: 1441 payload = contextNode.payload 1442 return payload 1443 else: 1444 return None 1445 1446 def wellFormed(checkContextEl, checkSite) -> bool: 1447 ''' 1448 Long explanation for a short method. 1449 1450 It is possible for a contextEl to be returned that contradicts the 1451 'Before' or 'After' criterion due to the following: 1452 1453 (I'll take the example of Before; After is much harder 1454 to construct, but possible). 1455 1456 Assume that s is a Score, and tb2 = s.flatten()[1] and tb1 is the previous element 1457 (would be s.flatten()[0]) -- both are at offset 0 in s and are of the same class 1458 (so same sort order and priority) and are thus ordered entirely by insert 1459 order. 1460 1461 in s we have the following. 1462 1463 s.sortTuple = 0.0 <0.-20.0> # not inserted 1464 tb1.sortTuple = 0.0 <0.-31.1> 1465 tb2.sortTuple = 0.0 <0.-31.2> 1466 1467 in s.flatten() we have: 1468 1469 s.flatten().sortTuple = 0.0 <0.-20.0> # not inserted 1470 tb1.sortTuple = 0.0 <0.-31.3> 1471 tb2.sortTuple = 0.0 <0.-31.4> 1472 1473 Now tb2 is declared through s.flatten()[1], so its activeSite 1474 is s.flatten(). Calling .previous() finds tb1 in s.flatten(). This is normal. 1475 1476 tb1 calls .previous(). Search of first site finds nothing before tb1, 1477 so .getContextByClass() is ready to return None. But other sites need to be checked 1478 1479 Search returns to s. .getContextByClass() asks is there anything before tb1's 1480 sort tuple of 0.0 <0.-31.3> (.3 is the insertIndex) in s? Yes, it's tb2 at 1481 0.0 <0.-31.2> (because s was created before s.flatten(), the insert indices of objects 1482 in s are lower than the insert indices of objects in s.flatten() (perhaps insert indices 1483 should be eventually made global within the context of a stream, but not global 1484 overall? but that wasn't the solution here). So we go back to tb2 in s. Then in 1485 theory we should go to tb1 in s, then s, then None. This would have certain 1486 elements appear twice in a .previous() search, which is not optimal, but wouldn't be 1487 such a huge bug to make this method necessary. 1488 1489 That'd be the only bug that would occur if we did: sf = s.flatten(), tb2 = sf[1]. But 1490 consider the exact phrasing above: tb2 = s.flatten()[1]. 1491 s.flatten() is created for an instant, 1492 it is assigned to tb2's ._activeSite via weakRef, and then tb2's sortTuple is set 1493 via this temporary stream. 1494 1495 Suppose tb2 is from that temp s.flatten()[1]. Then tb1 = tb2.previous() which is found 1496 in s.flatten(). Suppose then that for some reason s._cache['flat'] gets cleaned up 1497 (It was a bug that s._cache was being cleaned by the positioning of notes during 1498 s_flat's setOffset, 1499 but _cache cleanups are allowed to happen at any time, 1500 so it's not a bug that it's being cleaned; assuming that it wouldn't be cleaned 1501 would be the bug) and garbage collection runs. 1502 1503 Now we get tb1.previous() would get tb2 in s. Okay, it's redundant but not a huge deal, 1504 and tb2.previous() gets tb1. tb1's ._activeSite is still a weakref to s.flatten(). 1505 When tb1's getContextByClass() is called, it needs its .sortTuple(). This looks 1506 first at .activeSite. That is None, so it gets it from .offset which is the .offset 1507 of the last .activeSite (even if it is dead. A lot of code depends on .offset 1508 still being available if .activeSite dies, so changing that is not an option for now). 1509 So its sortTuple is 0.0 <0.-31.3>. which has a .previous() of tb2 in s, which can 1510 call previous can get tb1, etc. So with really bad timing of cache cleanups and 1511 garbage collecting, it's possible to get an infinite loop. 1512 1513 There may be ways to set activeSite on .getContextByClass() call such that this routine 1514 is not necessary, but I could not find one that was not disruptive for normal 1515 usages. 1516 1517 There are some possible issues that one could raise about "wellFormed". 1518 Suppose for instance, that in one stream (b) we have [tb1, tb2] and 1519 then in another stream context (a), created earlier, 1520 we have [tb0, tb1, tb2]. tb1 is set up with (b) as an activeSite. Finding nothing 1521 previous, it goes to (a) and finds tb2; it then discovers that in (a), tb2 is after 1522 tb1 so it returns None for this context. One might say, "wait a second, why 1523 isn't tb0 returned? It's going to be skipped." To this, I would answer, the original 1524 context in which .previous() or .getContextByClass() was called was (b). There is 1525 no absolute obligation to find what was previous in a different site context. It is 1526 absolutely fair game to say, "there's nothing prior to tb1". Then why even 1527 search other streams/sites? Because it's quite common to create a new site 1528 for say a single measure or .getElementsByOffset(), etc., so that when leaving 1529 this extracted section, one wants to see how that fits into a larger stream hierarchy. 1530 ''' 1531 try: 1532 selfSortTuple = self.sortTuple(checkSite, raiseExceptionOnMiss=True) 1533 contextSortTuple = checkContextEl.sortTuple(checkSite, raiseExceptionOnMiss=True) 1534 except SitesException: 1535 # might be raised by selfSortTuple; should not be by contextSortTuple. 1536 # It just means that selfSortTuple isn't in the same stream 1537 # as contextSortTuple, such as 1538 # when crossing measure borders. Thus it's well-formed. 1539 return True 1540 1541 if getElementMethod in BEFORE_METHODS and selfSortTuple < contextSortTuple: 1542 # print(getElementMethod, selfSortTuple.shortRepr(), 1543 # contextSortTuple.shortRepr(), self, contextEl) 1544 return False 1545 elif getElementMethod in AFTER_METHODS and selfSortTuple > contextSortTuple: 1546 # print(getElementMethod, selfSortTuple.shortRepr(), 1547 # contextSortTuple.shortRepr(), self, contextEl) 1548 return False 1549 else: 1550 return True 1551 1552 if getElementMethod not in ElementSearch: 1553 raise ValueError(f'Invalid getElementMethod: {getElementMethod}') 1554 1555 if className and not common.isListLike(className): 1556 className = (className,) 1557 1558 if getElementMethod in AT_METHODS and not self.classSet.isdisjoint(className): 1559 return self 1560 1561 for site, positionStart, searchType in self.contextSites( 1562 returnSortTuples=True, 1563 sortByCreationTime=sortByCreationTime, 1564 followDerivation=followDerivation, 1565 priorityTargetOnly=priorityTargetOnly, 1566 ): 1567 if searchType in ('elementsOnly', 'elementsFirst'): 1568 contextEl = payloadExtractor(site, 1569 flatten=False, 1570 innerPositionStart=positionStart) 1571 1572 if contextEl is not None and wellFormed(contextEl, site): 1573 try: 1574 site.coreSelfActiveSite(contextEl) 1575 except SitesException: 1576 pass 1577 return contextEl 1578 # otherwise, continue to check for flattening... 1579 1580 if searchType != 'elementsOnly': # flatten or elementsFirst 1581 if (getElementMethod in AFTER_METHODS 1582 and (not className 1583 or not site.classSet.isdisjoint(className))): 1584 if getElementMethod in NOT_SELF_METHODS and self is site: 1585 pass 1586 elif getElementMethod not in NOT_SELF_METHODS: # for 'After' we can't do the 1587 # containing site because that comes before. 1588 return site # if the site itself is the context, return it... 1589 1590 contextEl = payloadExtractor(site, 1591 flatten='semiFlat', 1592 innerPositionStart=positionStart) 1593 if contextEl is not None and wellFormed(contextEl, site): 1594 try: 1595 site.coreSelfActiveSite(contextEl) 1596 except SitesException: 1597 pass 1598 return contextEl 1599 1600 if (getElementMethod in BEFORE_METHODS 1601 and (not className 1602 or not site.classSet.isdisjoint(className))): 1603 if getElementMethod in NOT_SELF_METHODS and self is site: 1604 pass 1605 else: 1606 return site # if the site itself is the context, return it... 1607 1608 # otherwise, continue to check in next contextSite. 1609 1610 # nothing found... 1611 return None 1612 1613 def contextSites( 1614 self, 1615 *, 1616 callerFirst=None, 1617 memo=None, 1618 offsetAppend=0.0, 1619 sortByCreationTime: Union[str, bool] = False, 1620 priorityTarget=None, 1621 returnSortTuples=False, 1622 followDerivation=True, 1623 priorityTargetOnly=False, 1624 ): 1625 ''' 1626 A generator that returns a list of namedtuples of sites to search for a context... 1627 1628 Each tuple contains three elements: 1629 1630 .site -- Stream object, 1631 .offset -- the offset or position (sortTuple) of this element in that Stream 1632 .recurseType -- the method of searching that should be applied to search for a context. 1633 1634 The recurseType methods are: 1635 1636 * 'flatten' -- flatten the stream and then look from this offset backwards. 1637 1638 * 'elementsOnly' -- only search the stream's personal 1639 elements from this offset backwards 1640 1641 * 'elementsFirst' -- search this stream backwards, 1642 and then flatten and search backwards 1643 1644 >>> c = corpus.parse('bwv66.6') 1645 >>> c.id = 'bach' 1646 >>> n = c[2][4][2] 1647 >>> n 1648 <music21.note.Note G#> 1649 1650 Returning sortTuples are important for distinguishing the order of multiple sites 1651 at the same offset. 1652 1653 >>> for csTuple in n.contextSites(returnSortTuples=True): 1654 ... yClearer = (csTuple.site, csTuple.offset.shortRepr(), csTuple.recurseType) 1655 ... print(yClearer) 1656 (<music21.stream.Measure 3 offset=9.0>, '0.5 <0.20...>', 'elementsFirst') 1657 (<music21.stream.Part Alto>, '9.5 <0.20...>', 'flatten') 1658 (<music21.stream.Score bach>, '9.5 <0.20...>', 'elementsOnly') 1659 1660 Streams have themselves as the first element in their context sites, at position 1661 zero and classSortOrder negative infinity. 1662 1663 This example shows the context sites for Measure 3 of the 1664 Alto part. We will get the measure object using direct access to 1665 indices to ensure that no other temporary 1666 streams are created; normally, we would do `c.parts['Alto'].measure(3)`. 1667 1668 >>> m = c[2][4] 1669 >>> m 1670 <music21.stream.Measure 3 offset=9.0> 1671 >>> for csTuple in m.contextSites(returnSortTuples=True): 1672 ... yClearer = (csTuple.site, csTuple.offset.shortRepr(), csTuple.recurseType) 1673 ... print(yClearer) 1674 (<music21.stream.Measure 3 offset=9.0>, '0.0 <-inf.-20...>', 'elementsFirst') 1675 (<music21.stream.Part Alto>, '9.0 <0.-20...>', 'flatten') 1676 (<music21.stream.Score bach>, '9.0 <0.-20...>', 'elementsOnly') 1677 1678 Here we make a copy of the earlier measure and we see that its contextSites 1679 follow the derivationChain from the original measure and still find the Part 1680 and Score of the original Measure 3 even though mCopy is not in any of these 1681 objects. 1682 1683 >>> import copy 1684 >>> mCopy = copy.deepcopy(m) 1685 >>> mCopy.number = 3333 1686 >>> for csTuple in mCopy.contextSites(): 1687 ... print(csTuple, mCopy in csTuple.site) 1688 ContextTuple(site=<music21.stream.Measure 3333 offset=0.0>, 1689 offset=0.0, 1690 recurseType='elementsFirst') False 1691 ContextTuple(site=<music21.stream.Part Alto>, 1692 offset=9.0, 1693 recurseType='flatten') False 1694 ContextTuple(site=<music21.stream.Score bach>, 1695 offset=9.0, 1696 recurseType='elementsOnly') False 1697 1698 If followDerivation were False, then the Part and Score would not be found. 1699 1700 >>> for csTuple in mCopy.contextSites(followDerivation=False): 1701 ... print(csTuple) 1702 ContextTuple(site=<music21.stream.Measure 3333 offset=0.0>, 1703 offset=0.0, 1704 recurseType='elementsFirst') 1705 1706 1707 >>> partIterator = c.parts 1708 >>> m3 = partIterator[1].measure(3) 1709 >>> for csTuple in m3.contextSites(): 1710 ... print(csTuple) 1711 ContextTuple(site=<music21.stream.Measure 3 offset=9.0>, 1712 offset=0.0, 1713 recurseType='elementsFirst') 1714 ContextTuple(site=<music21.stream.Part Alto>, 1715 offset=9.0, 1716 recurseType='flatten') 1717 ContextTuple(site=<music21.stream.Score bach>, 1718 offset=9.0, 1719 recurseType='elementsOnly') 1720 1721 Sorting order: 1722 1723 >>> p1 = stream.Part() 1724 >>> p1.id = 'p1' 1725 >>> m1 = stream.Measure() 1726 >>> m1.number = 1 1727 >>> n = note.Note() 1728 >>> m1.append(n) 1729 >>> p1.append(m1) 1730 >>> for csTuple in n.contextSites(): 1731 ... print(csTuple.site) 1732 <music21.stream.Measure 1 offset=0.0> 1733 <music21.stream.Part p1> 1734 1735 >>> p2 = stream.Part() 1736 >>> p2.id = 'p2' 1737 >>> m2 = stream.Measure() 1738 >>> m2.number = 2 1739 >>> m2.append(n) 1740 >>> p2.append(m2) 1741 1742 The keys could have appeared in any order, but by default 1743 we set set priorityTarget to activeSite. So this is the same as omitting. 1744 1745 >>> for y in n.contextSites(priorityTarget=n.activeSite): 1746 ... print(y[0]) 1747 <music21.stream.Measure 2 offset=0.0> 1748 <music21.stream.Part p2> 1749 <music21.stream.Measure 1 offset=0.0> 1750 <music21.stream.Part p1> 1751 1752 We can sort sites by creationTime... 1753 1754 >>> for csTuple in n.contextSites(sortByCreationTime=True): 1755 ... print(csTuple.site) 1756 <music21.stream.Measure 2 offset=0.0> 1757 <music21.stream.Part p2> 1758 <music21.stream.Measure 1 offset=0.0> 1759 <music21.stream.Part p1> 1760 1761 oldest first... 1762 1763 >>> for csTuple in n.contextSites(sortByCreationTime='reverse'): 1764 ... print(csTuple.site) 1765 <music21.stream.Measure 1 offset=0.0> 1766 <music21.stream.Part p1> 1767 <music21.stream.Measure 2 offset=0.0> 1768 <music21.stream.Part p2> 1769 1770 Note that by default we search all sites, but you might want to only search 1771 one, for instance: 1772 1773 >>> c = note.Note('C') 1774 >>> m1 = stream.Measure() 1775 >>> m1.append(c) 1776 1777 >>> d = note.Note('D') 1778 >>> m2 = stream.Measure() 1779 >>> m2.append([c, d]) 1780 1781 >>> c.activeSite = m1 1782 >>> c.next('Note') # uses contextSites 1783 <music21.note.Note D> 1784 1785 There is a particular site in which there is a Note after c, 1786 but we want to know if there is one in m1 or its hierarchy, so 1787 we can pass in activeSiteOnly to `.next()` which sets 1788 `priorityTargetOnly=True` for contextSites 1789 1790 >>> print(c.next('Note', activeSiteOnly=True)) 1791 None 1792 1793 1794 * removed in v3: priorityTarget cannot be set, in order 1795 to use `.sites.yieldSites()` 1796 * changed in v5.5: all arguments are keyword only. 1797 * changed in v6: added `priorityTargetOnly=False` to only search in the 1798 context of the priorityTarget. 1799 ''' 1800 from music21 import stream 1801 1802 if memo is None: 1803 memo = [] 1804 1805 if callerFirst is None: 1806 callerFirst = self 1807 if self.isStream and self not in memo: 1808 recursionType = self.recursionType 1809 environLocal.printDebug( 1810 f'Caller first is {callerFirst} with offsetAppend {offsetAppend}') 1811 if returnSortTuples: 1812 selfSortTuple = self.sortTuple().modify( 1813 offset=0.0, 1814 priority=float('-inf') 1815 ) 1816 yield ContextTuple(self, selfSortTuple, recursionType) 1817 else: 1818 yield ContextTuple(self, 0.0, recursionType) 1819 memo.append(self) 1820 1821 if priorityTarget is None and sortByCreationTime is False: 1822 priorityTarget = self.activeSite 1823 else: 1824 environLocal.printDebug(f'sortByCreationTime {sortByCreationTime}') 1825 1826 topLevel = self 1827 for siteObj in self.sites.yieldSites(sortByCreationTime=sortByCreationTime, 1828 priorityTarget=priorityTarget, 1829 excludeNone=True): 1830 if siteObj in memo: 1831 continue 1832 if isinstance(siteObj, stream.SpannerStorage): 1833 continue 1834 1835 try: 1836 st = self.sortTuple(siteObj) 1837 if followDerivation: 1838 # do not change this to ElementOffset because getOffsetBySite can 1839 # follow derivation chains. 1840 offsetInStream = self.getOffsetBySite(siteObj) 1841 else: 1842 offsetInStream = siteObj.elementOffset(self) 1843 1844 newOffset = opFrac(offsetInStream + offsetAppend) 1845 1846 positionInStream = st.modify(offset=newOffset) 1847 except SitesException: 1848 continue # not a valid site any more. Could be caught in derivationChain 1849 1850 recursionType = siteObj.recursionType 1851 if returnSortTuples: 1852 yield ContextTuple(siteObj, positionInStream, recursionType) 1853 else: 1854 yield ContextTuple(siteObj, positionInStream.offset, recursionType) 1855 1856 memo.append(siteObj) 1857 environLocal.printDebug( 1858 f'looking in contextSites for {siteObj}' 1859 + f' with position {positionInStream.shortRepr()}') 1860 for topLevel, inStreamPos, recurType in siteObj.contextSites( 1861 callerFirst=callerFirst, 1862 memo=memo, 1863 offsetAppend=positionInStream.offset, 1864 returnSortTuples=True, # ALWAYS 1865 sortByCreationTime=sortByCreationTime 1866 ): 1867 # get activeSite unless sortByCreationTime 1868 inStreamOffset = inStreamPos.offset 1869 # now take that offset and use it to modify the positionInStream 1870 # to get where Exactly the object would be if it WERE in this stream 1871 hypotheticalPosition = positionInStream.modify(offset=inStreamOffset) 1872 1873 if topLevel not in memo: 1874 # environLocal.printDebug('Yielding {}, {}, {} from contextSites'.format( 1875 # topLevel, 1876 # inStreamPos.shortRepr(), 1877 # recurType)) 1878 if returnSortTuples: 1879 yield ContextTuple(topLevel, hypotheticalPosition, recurType) 1880 else: 1881 yield ContextTuple(topLevel, inStreamOffset, recurType) 1882 memo.append(topLevel) 1883 if priorityTargetOnly: 1884 break 1885 1886 if followDerivation: 1887 for derivedObject in topLevel.derivation.chain(): 1888 environLocal.printDebug( 1889 'looking now in derivedObject, ' 1890 + f'{derivedObject} with offsetAppend {offsetAppend}') 1891 for derivedCsTuple in derivedObject.contextSites( 1892 callerFirst=None, 1893 memo=memo, 1894 offsetAppend=0.0, 1895 returnSortTuples=True, 1896 sortByCreationTime=sortByCreationTime): 1897 # get activeSite unless sortByCreationTime 1898 if derivedCsTuple.site in memo: 1899 continue 1900 1901 environLocal.printDebug( 1902 f'Yielding {derivedCsTuple} from derivedObject contextSites' 1903 ) 1904 offsetAdjustedCsTuple = ContextTuple( 1905 derivedCsTuple.site, 1906 derivedCsTuple.offset.modify(offset=derivedCsTuple[1].offset 1907 + offsetAppend), 1908 derivedCsTuple.recurseType) 1909 if returnSortTuples: 1910 yield offsetAdjustedCsTuple 1911 else: 1912 yield ContextTuple(offsetAdjustedCsTuple.site, 1913 offsetAdjustedCsTuple.offset.offset, 1914 offsetAdjustedCsTuple.recurseType) 1915 memo.append(derivedCsTuple.site) 1916 1917 environLocal.printDebug('--returning from derivedObject search') 1918 1919 def getAllContextsByClass(self, className): 1920 ''' 1921 Returns a generator that yields elements found by `.getContextByClass` and 1922 then finds the previous contexts for that element. 1923 1924 >>> s = stream.Stream() 1925 >>> s.append(meter.TimeSignature('2/4')) 1926 >>> s.append(note.Note('C')) 1927 >>> s.append(meter.TimeSignature('3/4')) 1928 >>> n = note.Note('D') 1929 >>> s.append(n) 1930 1931 1932 >>> for ts in n.getAllContextsByClass('TimeSignature'): 1933 ... print(ts, ts.offset) 1934 <music21.meter.TimeSignature 3/4> 1.0 1935 <music21.meter.TimeSignature 2/4> 0.0 1936 1937 TODO: make it so that it does not skip over multiple matching classes 1938 at the same offset. with sortTuple 1939 1940 ''' 1941 el = self.getContextByClass(className) 1942 while el is not None: 1943 yield el 1944 el = el.getContextByClass(className, getElementMethod='getElementBeforeOffset') 1945 1946 # ------------------------------------------------------------------------- 1947 1948 def next(self, className=None, *, activeSiteOnly=False): 1949 ''' 1950 Get the next element found in the activeSite (or other Sites) 1951 of this Music21Object. 1952 1953 The `className` can be used to specify one or more classes to match. 1954 1955 >>> s = corpus.parse('bwv66.6') 1956 >>> m3 = s.parts[0].measure(3) 1957 >>> m4 = s.parts[0].measure(4) 1958 >>> m3 1959 <music21.stream.Measure 3 offset=9.0> 1960 >>> m3.show('t') 1961 {0.0} <music21.layout.SystemLayout> 1962 {0.0} <music21.note.Note A> 1963 {0.5} <music21.note.Note B> 1964 {1.0} <music21.note.Note G#> 1965 {2.0} <music21.note.Note F#> 1966 {3.0} <music21.note.Note A> 1967 >>> m3.next() 1968 <music21.layout.SystemLayout> 1969 >>> nextM3 = m3.next('Measure') 1970 >>> nextM3 is m4 1971 True 1972 1973 Note that calling next() repeatedly gives...the same object. You'll want to 1974 call next on that object... 1975 1976 >>> m3.next('Measure') is s.parts[0].measure(4) 1977 True 1978 >>> m3.next('Measure') is s.parts[0].measure(4) 1979 True 1980 1981 So do this instead: 1982 1983 >>> o = m3 1984 >>> for i in range(5): 1985 ... print(o) 1986 ... o = o.next('Measure') 1987 <music21.stream.Measure 3 offset=9.0> 1988 <music21.stream.Measure 4 offset=13.0> 1989 <music21.stream.Measure 5 offset=17.0> 1990 <music21.stream.Measure 6 offset=21.0> 1991 <music21.stream.Measure 7 offset=25.0> 1992 1993 We can find the next element given a certain class with the `className`: 1994 1995 >>> n = m3.next('Note') 1996 >>> n 1997 <music21.note.Note A> 1998 >>> n.measureNumber 1999 3 2000 >>> n is m3.notes.first() 2001 True 2002 >>> n.next() 2003 <music21.note.Note B> 2004 2005 Notice though that when we get to the end of the set of measures, something 2006 interesting happens (maybe it shouldn't? don't count on this...): we descend 2007 into the last measure and give its elements instead. 2008 2009 We'll leave o where it is (m8 now) to demonstrate what happens, and also 2010 print its Part for more information... 2011 2012 >>> while o is not None: 2013 ... print(o, o.getContextByClass('Part')) 2014 ... o = o.next() 2015 <music21.stream.Measure 8 offset=29.0> <music21.stream.Part Soprano> 2016 <music21.note.Note F#> <music21.stream.Part Soprano> 2017 <music21.note.Note F#> <music21.stream.Part Soprano> 2018 <music21.note.Note F#> <music21.stream.Part Soprano> 2019 <music21.stream.Measure 9 offset=33.0> <music21.stream.Part Soprano> 2020 <music21.note.Note F#> <music21.stream.Part Soprano> 2021 <music21.note.Note F#> <music21.stream.Part Soprano> 2022 <music21.note.Note E#> <music21.stream.Part Soprano> 2023 <music21.note.Note F#> <music21.stream.Part Soprano> 2024 <music21.bar.Barline type=final> <music21.stream.Part Soprano> 2025 2026 * changed in v.6 -- added activeSiteOnly -- see description in `.contextSites()` 2027 ''' 2028 allSiteContexts = list(self.contextSites( 2029 returnSortTuples=True, 2030 priorityTargetOnly=activeSiteOnly, 2031 )) 2032 maxRecurse = 20 2033 2034 thisElForNext = self 2035 while maxRecurse: 2036 nextEl = thisElForNext.getContextByClass( 2037 className=className, 2038 getElementMethod='getElementAfterNotSelf', 2039 priorityTargetOnly=activeSiteOnly, 2040 ) 2041 2042 callContinue = False 2043 for singleSiteContext, unused_positionInContext, unused_recurseType in allSiteContexts: 2044 if nextEl is singleSiteContext: 2045 if nextEl and nextEl[0] is not self: # has elements 2046 return nextEl[0] 2047 2048 thisElForNext = nextEl 2049 callContinue = True 2050 break 2051 2052 if callContinue: 2053 maxRecurse -= 1 2054 continue 2055 2056 if nextEl is not self: 2057 return nextEl 2058 maxRecurse -= 1 2059 2060 if maxRecurse == 0: 2061 raise Music21Exception('Maximum recursion!') 2062 2063 def previous(self, className=None, *, activeSiteOnly=False): 2064 ''' 2065 Get the previous element found in the activeSite or other .sites of this 2066 Music21Object. 2067 2068 The `className` can be used to specify one or more classes to match. 2069 2070 >>> s = corpus.parse('bwv66.6') 2071 >>> m2 = s.parts[0].getElementsByClass('Measure')[2] # pickup measure 2072 >>> m3 = s.parts[0].getElementsByClass('Measure')[3] 2073 >>> m3 2074 <music21.stream.Measure 3 offset=9.0> 2075 >>> m3prev = m3.previous() 2076 >>> m3prev 2077 <music21.note.Note C#> 2078 >>> m3prev is m2.notes[-1] 2079 True 2080 >>> m3.previous('Measure') is m2 2081 True 2082 2083 We'll iterate backwards from the first note of the second measure of the Alto part. 2084 2085 >>> o = s.parts[1].getElementsByClass('Measure')[2][0] 2086 >>> while o: 2087 ... print(o) 2088 ... o = o.previous() 2089 <music21.note.Note E> 2090 <music21.stream.Measure 2 offset=5.0> 2091 <music21.note.Note E> 2092 <music21.note.Note E> 2093 <music21.note.Note E> 2094 <music21.note.Note F#> 2095 <music21.stream.Measure 1 offset=1.0> 2096 <music21.note.Note E> 2097 <music21.meter.TimeSignature 4/4> 2098 f# minor 2099 <music21.clef.TrebleClef> 2100 <music21.stream.Measure 0 offset=0.0> 2101 P2: Alto: Instrument 2 2102 <music21.stream.Part Alto> 2103 <music21.stream.Part Soprano> 2104 <music21.metadata.Metadata object at 0x11116d080> 2105 <music21.stream.Score 0x10513af98> 2106 2107 * changed in v.6 -- added activeSiteOnly -- see description in `.contextSites()` 2108 ''' 2109 # allSiteContexts = list(self.contextSites(returnSortTuples=True)) 2110 # maxRecurse = 20 2111 2112 prevEl = self.getContextByClass(className=className, 2113 getElementMethod='getElementBeforeNotSelf', 2114 priorityTargetOnly=activeSiteOnly, 2115 ) 2116 2117 # for singleSiteContext, unused_positionInContext, unused_recurseType in allSiteContexts: 2118 # if prevEl is singleSiteContext: 2119 # prevElPrev = prevEl.getContextByClass(prevEl.__class__, 2120 # getElementMethod='getElementBeforeNotSelf') 2121 # if prevElPrev and prevElPrev is not self: 2122 # return prevElPrev 2123 isInPart = False 2124 if self.isStream and prevEl is not None: 2125 # if self is a Part, ensure that the previous element is not in self 2126 for cs, unused1, unused2 in prevEl.contextSites(): 2127 if cs is self: 2128 isInPart = True 2129 break 2130 2131 if prevEl and prevEl is not self and not isInPart: 2132 return prevEl 2133 else: 2134 # okay, go up to next level 2135 activeS = self.activeSite # might be None... 2136 if activeS is None: 2137 return None 2138 if className is not None and not common.isListLike(className): 2139 className = (className,) 2140 asTree = activeS.asTree(classList=className, flatten=False) 2141 prevNode = asTree.getNodeBefore(self.sortTuple()) 2142 if prevNode is None: 2143 if className is None or not activeS.classSet.isdisjoint(className): 2144 return activeS 2145 else: 2146 return None 2147 else: 2148 return prevNode.payload 2149 2150 # end contexts... 2151 # -------------------------------------------------------------------------------- 2152 2153 # ------------------------------------------------------------------------- 2154 # properties 2155 2156 def _getActiveSite(self): 2157 # can be None 2158 if sites.WEAKREF_ACTIVE: 2159 if self._activeSite is None: # leave None 2160 return None 2161 else: # even if current activeSite is not a weakref, this will work 2162 # environLocal.printDebug(['_getActiveSite() called:', 2163 # 'self._activeSite', self._activeSite]) 2164 return common.unwrapWeakref(self._activeSite) 2165 else: # pragma: no cover 2166 return self._activeSite 2167 2168 def _setActiveSite(self, site: Union['music21.stream.Stream', None]): 2169 # environLocal.printDebug(['_setActiveSite() called:', 'self', self, 'site', site]) 2170 2171 # NOTE: this is a performance intensive call 2172 if site is not None: 2173 try: 2174 storedOffset = site.elementOffset(self) 2175 except SitesException as se: 2176 raise SitesException( 2177 'activeSite cannot be set for ' 2178 + f'object {self} not in the Stream {site}' 2179 ) from se 2180 2181 self._activeSiteStoredOffset = storedOffset 2182 # siteId = id(site) 2183 # if not self.sites.hasSiteId(siteId): # This should raise a warning, should not happen 2184 # # environLocal.warn('Adding a siteDict entry for a ' + 2185 # # 'site that should already be there!') 2186 # self.sites.add(site, idKey=siteId) 2187 else: 2188 self._activeSiteStoredOffset = None 2189 2190 if sites.WEAKREF_ACTIVE: 2191 if site is None: # leave None alone 2192 self._activeSite = None 2193 else: 2194 self._activeSite = common.wrapWeakref(site) 2195 else: # pragma: no cover 2196 self._activeSite = site 2197 2198 activeSite = property(_getActiveSite, 2199 _setActiveSite, 2200 doc=''' 2201 A reference to the most-recent object used to 2202 contain this object. In most cases, this will be a 2203 Stream or Stream sub-class. In most cases, an object's 2204 activeSite attribute is automatically set when an the 2205 object is attached to a Stream. 2206 2207 2208 >>> n = note.Note('C#4') 2209 >>> p = stream.Part() 2210 >>> p.insert(20.0, n) 2211 >>> n.activeSite is p 2212 True 2213 >>> n.offset 2214 20.0 2215 2216 >>> m = stream.Measure() 2217 >>> m.insert(10.0, n) 2218 >>> n.activeSite is m 2219 True 2220 >>> n.offset 2221 10.0 2222 >>> n.activeSite = p 2223 >>> n.offset 2224 20.0 2225 ''') 2226 2227 def _getOffset(self): 2228 '''Get the offset for the activeSite. 2229 2230 >>> n = note.Note() 2231 >>> m = stream.Measure() 2232 >>> m.id = 'm1' 2233 >>> m.insert(3.0, n) 2234 >>> n.activeSite is m 2235 True 2236 >>> n.offset 2237 3.0 2238 2239 Still works... 2240 2241 >>> n.offset 2242 3.0 2243 2244 There is a branch that does slow searches. 2245 See test/testSerialization to have it active. 2246 ''' 2247 # there is a problem if a new activeSite is being set and no offsets have 2248 # been provided for that activeSite; when self.offset is called, 2249 # the first case here would match 2250 # environLocal.printDebug(['Music21Object._getOffset', 'self.id', 2251 # self.id, 'id(self)', id(self), self.__class__]) 2252 activeSiteWeakRef = self._activeSite 2253 if activeSiteWeakRef is not None: 2254 activeSite = self.activeSite 2255 if activeSite is None: 2256 # it has died since last visit, as is the case with short-lived streams like 2257 # .getElementsByClass, so we will return the most recent position 2258 return self._activeSiteStoredOffset 2259 2260 try: 2261 o = activeSite.elementOffset(self) 2262 except SitesException: 2263 environLocal.printDebug( 2264 'Not in Stream: changing activeSite to None and returning _naiveOffset') 2265 self.activeSite = None 2266 o = self._naiveOffset 2267 else: 2268 o = self._naiveOffset 2269 2270 return o 2271 2272 def _setOffset(self, value): 2273 ''' 2274 Set the offset for the activeSite. 2275 ''' 2276 # assume that most times this is a number; in that case, the fastest 2277 # thing to do is simply try to set the offset w/ float(value) 2278 try: 2279 offset = opFrac(value) 2280 except TypeError: 2281 offset = value 2282 2283 if hasattr(value, 'quarterLength'): 2284 # probably a Duration object, but could be something else -- in any case, we'll take it. 2285 offset = value.quarterLength 2286 2287 if self.activeSite is not None: 2288 self.activeSite.setElementOffset(self, offset) 2289 else: 2290 self._naiveOffset = offset 2291 2292 offset = property(_getOffset, 2293 _setOffset, 2294 doc=''' 2295 The offset property sets or returns the position of this object 2296 as a float or fractions.Fraction value 2297 (generally in `quarterLengths`), depending on what is representable. 2298 2299 Offsets are measured from the start of the object's `activeSite`, 2300 that is, the most recently referenced `Stream` or `Stream` subclass such 2301 as `Part`, `Measure`, or `Voice`. It is a simpler 2302 way of calling `o.getOffsetBySite(o.activeSite, returnType='rational')`. 2303 2304 If we put a `Note` into a `Stream`, we will see the activeSite changes. 2305 2306 >>> import fractions 2307 >>> n1 = note.Note('D#3') 2308 >>> n1.activeSite is None 2309 True 2310 2311 >>> m1 = stream.Measure() 2312 >>> m1.number = 4 2313 >>> m1.insert(10.0, n1) 2314 >>> n1.offset 2315 10.0 2316 >>> n1.activeSite 2317 <music21.stream.Measure 4 offset=0.0> 2318 2319 >>> n1.activeSite is m1 2320 True 2321 2322 The most recently referenced `Stream` becomes an object's `activeSite` and 2323 thus the place where `.offset` looks to find its number. 2324 2325 >>> m2 = stream.Measure() 2326 >>> m2.insert(3.0/5, n1) 2327 >>> m2.number = 5 2328 >>> n1.offset 2329 Fraction(3, 5) 2330 >>> n1.activeSite is m2 2331 True 2332 2333 Notice though that `.offset` depends on the `.activeSite` which is the most 2334 recently accessed/referenced Stream. 2335 2336 Here we will iterate over the `elements` in `m1` and we 2337 will see that the `.offset` of `n1` now is its offset in 2338 `m1` even though we haven't done anything directly to `n1`. 2339 Simply iterating over a site is enough to change the `.activeSite` 2340 of its elements: 2341 2342 >>> for element in m1: 2343 ... pass 2344 >>> n1.offset 2345 10.0 2346 2347 2348 The property can also set the offset for the object if no 2349 container has been set: 2350 2351 2352 >>> n1 = note.Note() 2353 >>> n1.id = 'hi' 2354 >>> n1.offset = 20/3. 2355 >>> n1.offset 2356 Fraction(20, 3) 2357 >>> float(n1.offset) 2358 6.666... 2359 2360 2361 >>> s1 = stream.Stream() 2362 >>> s1.append(n1) 2363 >>> n1.offset 2364 0.0 2365 >>> s2 = stream.Stream() 2366 >>> s2.insert(30.5, n1) 2367 >>> n1.offset 2368 30.5 2369 2370 After calling `getElementById` on `s1`, the 2371 returned element's `offset` will be its offset in `s1`. 2372 2373 >>> n2 = s1.getElementById('hi') 2374 >>> n2 is n1 2375 True 2376 >>> n2.offset 2377 0.0 2378 2379 Iterating over the elements in a Stream will 2380 make its `offset` be the offset in iterated 2381 Stream. 2382 2383 >>> for thisElement in s2: 2384 ... thisElement.offset 2385 30.5 2386 2387 When in doubt, use `.getOffsetBySite(streamObj)` 2388 which is safer or streamObj.elementOffset(self) which is 3x faster. 2389 ''') 2390 2391 def sortTuple(self, useSite=False, raiseExceptionOnMiss=False): 2392 ''' 2393 Returns a collections.namedtuple called SortTuple(atEnd, offset, priority, classSortOrder, 2394 isNotGrace, insertIndex) 2395 which contains the six elements necessary to determine the sort order of any set of 2396 objects in a Stream. 2397 2398 1) atEnd = {0, 1}; Elements specified to always stay at 2399 the end of a stream (``stream.storeAtEnd``) 2400 sort after normal elements. 2401 2402 2) offset = float; Offset (with respect to the active site) is the next and most 2403 important parameter in determining the order of elements in a stream (the note on beat 1 2404 has offset 0.0, while the note on beat 2 might have offset 1.0). 2405 2406 3) priority = int; Priority is a 2407 user-specified property (default 0) that can set the order of 2408 elements which have the same 2409 offset (for instance, two Parts both at offset 0.0). 2410 2411 4) classSortOrder = int or float; ClassSortOrder 2412 is the third level of comparison that gives an ordering to elements with different classes, 2413 ensuring, for instance that Clefs (classSortOrder = 0) sort before Notes 2414 (classSortOrder = 20). 2415 2416 5) isNotGrace = {0, 1}; 0 = grace, 1 = normal. Grace notes sort before normal notes 2417 2418 6) The last tie breaker is the creation time (insertIndex) of the site object 2419 represented by the activeSite. 2420 2421 >>> n = note.Note() 2422 >>> n.offset = 4.0 2423 >>> n.priority = -3 2424 >>> n.sortTuple() 2425 SortTuple(atEnd=0, offset=4.0, priority=-3, classSortOrder=20, 2426 isNotGrace=1, insertIndex=0) 2427 2428 >>> st = n.sortTuple() 2429 2430 Check that all these values are the same as above... 2431 2432 >>> st.offset == n.offset 2433 True 2434 >>> st.priority == n.priority 2435 True 2436 2437 An object's classSortOrder comes from the Class object itself: 2438 2439 >>> st.classSortOrder == note.Note.classSortOrder 2440 True 2441 2442 SortTuples have a few methods that are documented in :class:`~music21.sorting.SortTuple`. 2443 The most useful one for documenting is `.shortRepr()`. 2444 2445 >>> st.shortRepr() 2446 '4.0 <-3.20.0>' 2447 2448 Inserting the note into the Stream will set the insertIndex. Most implementations of 2449 music21 will use a global counter rather than an actual timer. Note that this is a 2450 last resort, but useful for things such as multiple Parts inserted in order. It changes 2451 with each run, so we can't display it here... 2452 2453 >>> s = stream.Stream() 2454 >>> s.insert(n) 2455 >>> n.sortTuple() 2456 SortTuple(atEnd=0, offset=4.0, priority=-3, classSortOrder=20, 2457 isNotGrace=1, insertIndex=...) 2458 2459 >>> nInsertIndex = n.sortTuple().insertIndex 2460 2461 If we create another nearly identical note, the insertIndex will be different: 2462 2463 >>> n2 = note.Note() 2464 >>> n2.offset = 4.0 2465 >>> n2.priority = -3 2466 >>> s.insert(n2) 2467 >>> n2InsertIndex = n2.sortTuple().insertIndex 2468 >>> n2InsertIndex > nInsertIndex 2469 True 2470 2471 >>> rb = bar.Barline() 2472 >>> s.storeAtEnd(rb) 2473 >>> rb.sortTuple() 2474 SortTuple(atEnd=1, offset=0.0, priority=0, classSortOrder=-5, 2475 isNotGrace=1, insertIndex=...) 2476 2477 2478 Normally if there's a site specified and the element is not in the site, 2479 the offset of None will be used, but if raiseExceptionOnMiss is set to True 2480 then a SitesException will be raised: 2481 2482 >>> aloneNote = note.Note() 2483 >>> aloneNote.offset = 30 2484 >>> aloneStream = stream.Stream(id='aloneStream') # no insert 2485 >>> aloneNote.sortTuple(aloneStream) 2486 SortTuple(atEnd=0, offset=30.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=0) 2487 2488 >>> aloneNote.sortTuple(aloneStream, raiseExceptionOnMiss=True) 2489 Traceback (most recent call last): 2490 music21.sites.SitesException: an entry for this object 0x... is not stored in 2491 stream <music21.stream.Stream aloneStream> 2492 ''' 2493 if useSite is False: # False or a Site; since None is a valid site, default is False 2494 useSite = self.activeSite 2495 2496 if useSite is None: 2497 foundOffset = self.offset 2498 else: 2499 try: 2500 foundOffset = useSite.elementOffset(self, returnSpecial=True) 2501 except SitesException: 2502 if raiseExceptionOnMiss: 2503 raise 2504 # environLocal.warn(r) 2505 # activeSite may have vanished! or does not have the element 2506 foundOffset = self._naiveOffset 2507 2508 if foundOffset == OffsetSpecial.AT_END: 2509 offset = 0.0 2510 atEnd = 1 2511 else: 2512 offset = foundOffset 2513 atEnd = 0 2514 2515 if self.duration.isGrace: 2516 isNotGrace = 0 2517 else: 2518 isNotGrace = 1 2519 2520 if (useSite is not False 2521 and self.sites.hasSiteId(id(useSite))): 2522 insertIndex = self.sites.siteDict[id(useSite)].globalSiteIndex 2523 elif self.activeSite is not None: 2524 insertIndex = self.sites.siteDict[id(self.activeSite)].globalSiteIndex 2525 else: 2526 insertIndex = 0 2527 2528 return SortTuple(atEnd, offset, self.priority, 2529 self.classSortOrder, isNotGrace, insertIndex) 2530 2531 # ----------------------------------------------------------------- 2532 def _getDuration(self) -> Optional[duration.Duration]: 2533 ''' 2534 Gets the DurationObject of the object or None 2535 ''' 2536 # lazy duration creation 2537 if self._duration is None: 2538 self._duration = duration.Duration(0) 2539 return self._duration 2540 2541 def _setDuration(self, durationObj: duration.Duration): 2542 ''' 2543 Set the duration as a quarterNote length 2544 ''' 2545 durationObjAlreadyExists = not (self._duration is None) 2546 2547 try: 2548 ql = durationObj.quarterLength 2549 self._duration = durationObj 2550 durationObj.client = self 2551 if durationObjAlreadyExists: 2552 self.informSites({'changedElement': 'duration', 'quarterLength': ql}) 2553 2554 except AttributeError as ae: 2555 # need to permit Duration object assignment here 2556 raise Exception( 2557 f'this must be a Duration object, not {durationObj}' 2558 ) from ae 2559 2560 duration = property(_getDuration, _setDuration, 2561 doc=''' 2562 Get and set the duration of this object as a Duration object. 2563 ''') 2564 2565 def informSites(self, changedInformation=None): 2566 ''' 2567 trigger called whenever sites need to be informed of a change 2568 in the parameters of this object. 2569 2570 `changedInformation` is not used now, but it can be a dictionary 2571 of what has changed. 2572 2573 subclass this to do very interesting things. 2574 ''' 2575 for s in self.sites.get(): 2576 if hasattr(s, 'coreElementsChanged'): 2577 # noinspection PyCallingNonCallable 2578 s.coreElementsChanged(updateIsFlat=False, keepIndex=True) 2579 2580 def _getPriority(self): 2581 return self._priority 2582 2583 def _setPriority(self, value): 2584 ''' 2585 value is an int. 2586 2587 Informs all sites of the change. 2588 ''' 2589 if not isinstance(value, int): 2590 raise ElementException('priority values must be integers.') 2591 if self._priority != value: 2592 self._priority = value 2593 self.informSites({'changedElement': 'priority', 'priority': value}) 2594 2595 priority = property(_getPriority, 2596 _setPriority, 2597 doc=''' 2598 Get and set the priority integer value. 2599 2600 Priority specifies the order of processing from left (lowest number) 2601 to right (highest number) of objects at the same offset. For 2602 instance, if you want a key change and a clef change to happen at 2603 the same time but the key change to appear first, then set: 2604 keySigElement.priority = 1; clefElement.priority = 2 this might be 2605 a slightly counterintuitive numbering of priority, but it does 2606 mean, for instance, if you had two elements at the same offset, 2607 an allegro tempo change and an andante tempo change, then the 2608 tempo change with the higher priority number would apply to the 2609 following notes (by being processed second). 2610 2611 Default priority is 0; thus negative priorities are encouraged 2612 to have Elements that appear before non-priority set elements. 2613 2614 In case of tie, there are defined class sort orders defined in 2615 `music21.base.classSortOrder`. For instance, a key signature 2616 change appears before a time signature change before a 2617 note at the same offset. This produces the familiar order of 2618 materials at the start of a musical score. 2619 2620 >>> import music21 2621 >>> a = music21.Music21Object() 2622 >>> a.priority = 3 2623 >>> a.priority = 'high' 2624 Traceback (most recent call last): 2625 music21.base.ElementException: priority values must be integers. 2626 ''') 2627 2628 # ------------------------------------------------------------------------- 2629 # display and writing 2630 2631 def write(self, fmt=None, fp=None, **keywords): # pragma: no cover 2632 ''' 2633 Write out a file of music notation (or an image, etc.) in a given format. If 2634 fp is specified as a file path then the file will be placed there. If it is not 2635 given then a temporary file will be created. 2636 2637 If fmt is not given then the default of your Environment's 'writeFormat' will 2638 be used. For most people that is musicxml. 2639 2640 Returns the full path to the file. 2641 2642 Some formats, including .musicxml, create a copy of the stream, pack it into a well-formed 2643 score if necessary, and run :meth:`~music21.stream.Score.makeNotation`. To 2644 avoid this when writing .musicxml, use `makeNotation=False`, an advanced option 2645 that prioritizes speed but may not guarantee satisfactory notation. 2646 ''' 2647 if fmt is None: # get setting in environment 2648 fmt = environLocal['writeFormat'] 2649 elif fmt.startswith('.'): 2650 fmt = fmt[1:] 2651 2652 if fmt == 'mxl': 2653 keywords['compress'] = True 2654 2655 regularizedConverterFormat, unused_ext = common.findFormat(fmt) 2656 if regularizedConverterFormat is None: 2657 raise Music21ObjectException(f'cannot support showing in this format yet: {fmt}') 2658 2659 formatSubs = fmt.split('.') 2660 fmt = formatSubs[0] 2661 subformats = formatSubs[1:] 2662 2663 scClass = common.findSubConverterForFormat(regularizedConverterFormat) 2664 formatWriter = scClass() 2665 return formatWriter.write(self, 2666 regularizedConverterFormat, 2667 fp=fp, 2668 subformats=subformats, 2669 **keywords) 2670 2671 def _reprText(self, **keywords): 2672 ''' 2673 Return a text representation possible with line 2674 breaks. This methods can be overridden by subclasses 2675 to provide alternative text representations. 2676 ''' 2677 return repr(self) 2678 2679 def _reprTextLine(self): 2680 ''' 2681 Return a text representation without line breaks. This 2682 methods can be overridden by subclasses to provide 2683 alternative text representations. 2684 ''' 2685 return repr(self) 2686 2687 def show(self, fmt=None, app=None, **keywords): # pragma: no cover 2688 ''' 2689 Displays an object in a format provided by the 2690 fmt argument or, if not provided, the format set in the user's Environment 2691 2692 Valid formats include (but are not limited to):: 2693 2694 musicxml 2695 text 2696 midi 2697 lily (or lilypond) 2698 lily.png 2699 lily.pdf 2700 lily.svg 2701 braille 2702 vexflow 2703 musicxml.png 2704 2705 N.B. score.write('lily') returns a bare lilypond file, 2706 score.show('lily') runs it through lilypond and displays it as a png. 2707 2708 Some formats, including .musicxml, create a copy of the stream, pack it into a well-formed 2709 score if necessary, and run :meth:`~music21.stream.Score.makeNotation`. To 2710 avoid this when showing .musicxml, use `makeNotation=False`, an advanced option 2711 that prioritizes speed but may not guarantee satisfactory notation. 2712 ''' 2713 # note that all formats here must be defined in 2714 # common.VALID_SHOW_FORMATS 2715 if fmt is None: # get setting in environment 2716 if common.runningUnderIPython(): 2717 try: 2718 fmt = environLocal['ipythonShowFormat'] 2719 except environment.EnvironmentException: 2720 fmt = 'ipython.musicxml.png' 2721 else: 2722 fmt = environLocal['showFormat'] 2723 elif fmt.startswith('.'): 2724 fmt = fmt[1:] 2725 elif common.runningUnderIPython() and fmt.startswith('midi'): 2726 fmt = 'ipython.' + fmt 2727 2728 regularizedConverterFormat, unused_ext = common.findFormat(fmt) 2729 if regularizedConverterFormat is None: 2730 raise Music21ObjectException(f'cannot support showing in this format yet:{fmt}') 2731 2732 formatSubs = fmt.split('.') 2733 fmt = formatSubs[0] 2734 subformats = formatSubs[1:] 2735 2736 scClass = common.findSubConverterForFormat(regularizedConverterFormat) 2737 formatWriter = scClass() 2738 return formatWriter.show(self, 2739 regularizedConverterFormat, 2740 app=app, 2741 subformats=subformats, 2742 **keywords) 2743 2744 # ------------------------------------------------------------------------- 2745 # duration manipulation, processing, and splitting 2746 2747 def containerHierarchy( 2748 self, 2749 *, 2750 followDerivation=True, 2751 includeNonStreamDerivations=False 2752 ): 2753 ''' 2754 Return a list of Stream subclasses that this object 2755 is contained within or (if followDerivation is set) is derived from. 2756 2757 This gives access to the hierarchy that contained or 2758 created this object. 2759 2760 >>> s = corpus.parse('bach/bwv66.6') 2761 >>> noteE = s[1][2][3] 2762 >>> noteE 2763 <music21.note.Note E> 2764 >>> [e for e in noteE.containerHierarchy()] 2765 [<music21.stream.Measure 1 offset=1.0>, 2766 <music21.stream.Part Soprano>, 2767 <music21.stream.Score 0x1049a5668>] 2768 2769 2770 Note that derived objects also can follow the container hierarchy: 2771 2772 >>> import copy 2773 >>> n2 = copy.deepcopy(noteE) 2774 >>> [e for e in n2.containerHierarchy()] 2775 [<music21.stream.Measure 1 offset=1.0>, 2776 <music21.stream.Part Soprano>, 2777 <music21.stream.Score 0x1049a5668>] 2778 2779 Unless followDerivation is False: 2780 2781 >>> [e for e in n2.containerHierarchy(followDerivation=False)] 2782 [] 2783 2784 if includeNonStreamDerivations is True then n2's containerHierarchy will include 2785 n even though it's not a container. It gives a good idea of how the hierarchy is being 2786 constructed. 2787 2788 >>> [e for e in n2.containerHierarchy(includeNonStreamDerivations=True)] 2789 [<music21.note.Note E>, 2790 <music21.stream.Measure 1 offset=1.0>, 2791 <music21.stream.Part Soprano>, 2792 <music21.stream.Score 0x1049a5668>] 2793 2794 2795 The method follows activeSites, so set the activeSite as necessary. 2796 2797 >>> p = stream.Part(id='newPart') 2798 >>> m = stream.Measure(number=20) 2799 >>> p.insert(0, m) 2800 >>> m.insert(0, noteE) 2801 >>> noteE.activeSite 2802 <music21.stream.Measure 20 offset=0.0> 2803 >>> noteE.containerHierarchy() 2804 [<music21.stream.Measure 20 offset=0.0>, 2805 <music21.stream.Part newPart>] 2806 2807 * changed in v.5.7: followDerivation and includeNonStreamDerivations are now keyword only 2808 ''' 2809 post = [] 2810 focus = self 2811 endMe = 200 2812 while endMe > 0: 2813 endMe = endMe - 1 # do not go forever 2814 # collect activeSite unless activeSite is None; 2815 # if so, try to get rootDerivation 2816 candidate = focus.activeSite 2817 # environLocal.printDebug(['containerHierarchy(): activeSite found:', candidate]) 2818 if candidate is None: # nothing more to derive 2819 # if this is a Stream, we might find a root derivation 2820 if followDerivation is True and hasattr(focus, 'derivation'): 2821 # environLocal.printDebug(['containerHierarchy(): 2822 # found rootDerivation:', focus.rootDerivation]) 2823 alt = focus.derivation.rootDerivation 2824 if alt is None: 2825 return post 2826 else: 2827 candidate = alt 2828 else: 2829 return post 2830 if includeNonStreamDerivations is True or candidate.isStream: 2831 post.append(candidate) 2832 focus = candidate 2833 return post 2834 2835 def splitAtQuarterLength( 2836 self, 2837 quarterLength, 2838 *, 2839 retainOrigin=True, 2840 addTies=True, 2841 displayTiedAccidentals=False 2842 ) -> _SplitTuple: 2843 # noinspection PyShadowingNames 2844 ''' 2845 Split an Element into two Elements at a provided 2846 `quarterLength` (offset) into the Element. 2847 2848 Returns a specialized tuple that also has 2849 a .spannerList element which is a list of spanners 2850 that were created during the split, such as by splitting a trill 2851 note into more than one trill. 2852 2853 TODO: unite into a "split" function -- document obscure uses. 2854 2855 >>> a = note.Note('C#5') 2856 >>> a.duration.type = 'whole' 2857 >>> a.articulations = [articulations.Staccato()] 2858 >>> a.lyric = 'hi' 2859 >>> a.expressions = [expressions.Mordent(), expressions.Trill(), expressions.Fermata()] 2860 >>> st = a.splitAtQuarterLength(3) 2861 >>> b, c = st 2862 >>> b.duration.type 2863 'half' 2864 >>> b.duration.dots 2865 1 2866 >>> b.duration.quarterLength 2867 3.0 2868 >>> b.articulations 2869 [] 2870 >>> b.lyric 2871 'hi' 2872 >>> b.expressions 2873 [<music21.expressions.Mordent>, <music21.expressions.Trill>] 2874 >>> c.duration.type 2875 'quarter' 2876 >>> c.duration.dots 2877 0 2878 >>> c.duration.quarterLength 2879 1.0 2880 >>> c.articulations 2881 [<music21.articulations.Staccato>] 2882 >>> c.lyric 2883 >>> c.expressions 2884 [<music21.expressions.Fermata>] 2885 >>> c.getSpannerSites() 2886 [<music21.expressions.TrillExtension <music21.note.Note C#><music21.note.Note C#>>] 2887 2888 st is a _SplitTuple which can get the spanners from it for inserting into a Stream. 2889 2890 >>> st.spannerList 2891 [<music21.expressions.TrillExtension <music21.note.Note C#><music21.note.Note C#>>] 2892 2893 2894 2895 Make sure that ties and accidentals remain as they should be: 2896 2897 >>> d = note.Note('D#4') 2898 >>> d.duration.quarterLength = 3.0 2899 >>> d.tie = tie.Tie('start') 2900 >>> e, f = d.splitAtQuarterLength(2.0) 2901 >>> e.tie, f.tie 2902 (<music21.tie.Tie start>, <music21.tie.Tie continue>) 2903 >>> e.pitch.accidental.displayStatus is None 2904 True 2905 >>> f.pitch.accidental.displayStatus 2906 False 2907 2908 Should be the same for chords... 2909 2910 >>> g = chord.Chord(['C4', 'E4', 'G#4']) 2911 >>> g.duration.quarterLength = 3.0 2912 >>> g[1].tie = tie.Tie('start') 2913 >>> h, i = g.splitAtQuarterLength(2.0) 2914 >>> for j in range(3): 2915 ... (h[j].tie, i[j].tie) 2916 (<music21.tie.Tie start>, <music21.tie.Tie stop>) 2917 (<music21.tie.Tie start>, <music21.tie.Tie continue>) 2918 (<music21.tie.Tie start>, <music21.tie.Tie stop>) 2919 2920 >>> h[2].pitch.accidental.displayStatus, i[2].pitch.accidental.displayStatus 2921 (None, False) 2922 2923 2924 If quarterLength == self.quarterLength then the second element will be None. 2925 2926 >>> n = note.Note() 2927 >>> n.quarterLength = 0.5 2928 >>> a, b = n.splitAtQuarterLength(0.5) 2929 >>> b is None 2930 True 2931 >>> a is n 2932 True 2933 2934 (same with retainOrigin off) 2935 2936 >>> n = note.Note() 2937 >>> n.quarterLength = 0.5 2938 >>> a, b = n.splitAtQuarterLength(0.5, retainOrigin=False) 2939 >>> a is n 2940 False 2941 2942 2943 If quarterLength > self.quarterLength then a DurationException will be raised: 2944 2945 >>> n = note.Note() 2946 >>> n.quarterLength = 0.5 2947 >>> a, b = n.splitAtQuarterLength(0.7) 2948 Traceback (most recent call last): 2949 music21.duration.DurationException: cannot split a duration (0.5) 2950 at this quarterLength (7/10) 2951 2952 Changed in v7. -- all but quarterLength are keyword only 2953 ''' 2954 from music21 import chord 2955 from music21 import note 2956 quarterLength = opFrac(quarterLength) 2957 2958 if quarterLength > self.duration.quarterLength: 2959 raise duration.DurationException( 2960 f'cannot split a duration ({self.duration.quarterLength}) ' 2961 + f'at this quarterLength ({quarterLength})' 2962 ) 2963 2964 if retainOrigin is True: 2965 e = self 2966 else: 2967 e = copy.deepcopy(self) 2968 eRemain = copy.deepcopy(self) 2969 2970 # clear lyrics from remaining parts 2971 if hasattr(eRemain, 'lyrics') and not callable(eRemain.lyrics): 2972 # lyrics is a function on Streams... 2973 eRemain.lyrics = [] # pylint: disable=attribute-defined-outside-init 2974 2975 spannerList = [] 2976 for listType in ('expressions', 'articulations'): 2977 if hasattr(e, listType): 2978 temp = getattr(e, listType) 2979 setattr(e, listType, []) # pylint: disable=attribute-defined-outside-init 2980 setattr(eRemain, listType, []) 2981 for thisExpression in temp: # using thisExpression as a shortcut for expr or art. 2982 if hasattr(thisExpression, 'splitClient'): # special method (see Trill) 2983 spanners = thisExpression.splitClient([e, eRemain]) 2984 for s in spanners: 2985 spannerList.append(s) 2986 elif hasattr(thisExpression, 'tieAttach'): 2987 if thisExpression.tieAttach == 'first': 2988 eList = getattr(e, listType) 2989 eList.append(thisExpression) 2990 elif thisExpression.tieAttach == 'last': 2991 eRemainList = getattr(eRemain, listType) 2992 eRemainList.append(thisExpression) 2993 else: # default = 'all' 2994 eList = getattr(e, listType) 2995 eList.append(thisExpression) 2996 eRemainList = getattr(eRemain, listType) 2997 eRemainList.append(thisExpression) 2998 else: # default = 'all' 2999 eList = getattr(e, listType) 3000 eList.append(thisExpression) 3001 eRemainList = getattr(eRemain, listType) 3002 eRemainList.append(thisExpression) 3003 3004 if abs(quarterLength - self.duration.quarterLength) < 0: 3005 quarterLength = self.duration.quarterLength 3006 3007 lenEnd = self.duration.quarterLength - quarterLength 3008 lenStart = self.duration.quarterLength - lenEnd 3009 3010 d1 = duration.Duration() 3011 d1.quarterLength = lenStart 3012 3013 d2 = duration.Duration() 3014 d2.quarterLength = lenEnd 3015 3016 e.duration = d1 3017 eRemain.duration = d2 3018 3019 # some higher-level classes need this functionality 3020 # set ties 3021 if addTies and isinstance(e, (note.Note, note.Unpitched)): 3022 forceEndTieType = 'stop' 3023 if hasattr(e, 'tie') and e.tie is not None: 3024 # the last tie of what was formally a start should 3025 # continue 3026 if e.tie.type == 'start': 3027 # keep start if already set 3028 forceEndTieType = 'continue' 3029 # a stop was ending a previous tie; we know that 3030 # the first is now a continue 3031 elif e.tie.type == 'stop': 3032 forceEndTieType = 'stop' 3033 e.tie.type = 'continue' 3034 elif e.tie.type == 'continue': 3035 forceEndTieType = 'continue' 3036 # keep continue if already set 3037 elif hasattr(e, 'tie'): 3038 e.tie = tie.Tie('start') # pylint: disable=attribute-defined-outside-init 3039 # #need a tie object 3040 3041 if hasattr(eRemain, 'tie'): 3042 # pylint: disable=attribute-defined-outside-init 3043 eRemain.tie = tie.Tie(forceEndTieType) 3044 3045 elif addTies and isinstance(e, chord.Chord): 3046 for i in range(len(e.notes)): 3047 component = e.notes[i] 3048 remainComponent = eRemain.notes[i] 3049 forceEndTieType = 'stop' 3050 if component.tie is not None: 3051 # the last tie of what was formally a start should 3052 # continue 3053 if component.tie.type == 'start': 3054 # keep start if already set 3055 forceEndTieType = 'continue' 3056 # a stop was ending a previous tie; we know that 3057 # the first is now a continue 3058 elif component.tie.type == 'stop': 3059 forceEndTieType = 'stop' 3060 component.tie.type = 'continue' 3061 elif component.tie.type == 'continue': 3062 forceEndTieType = 'continue' 3063 # keep continue if already set 3064 else: 3065 component.tie = tie.Tie('start') # need a tie object 3066 remainComponent.tie = tie.Tie(forceEndTieType) 3067 3068 # hide accidentals on tied notes where previous note 3069 # had an accidental that was shown 3070 if addTies and hasattr(e, 'pitches'): 3071 for i, p in enumerate(e.pitches): 3072 remainP = eRemain.pitches[i] 3073 if hasattr(p, 'accidental') and p.accidental is not None: 3074 if not displayTiedAccidentals: # if False 3075 if p.accidental.displayType != 'even-tied': 3076 remainP.accidental.displayStatus = False 3077 else: # display tied accidentals 3078 remainP.accidental.displayType = 'even-tied' 3079 remainP.accidental.displayStatus = True 3080 3081 if eRemain.duration.quarterLength > 0.0: 3082 st = _SplitTuple([e, eRemain]) 3083 else: 3084 st = _SplitTuple([e, None]) 3085 3086 if spannerList: 3087 st.spannerList = spannerList 3088 3089 return st 3090 3091 def splitByQuarterLengths( 3092 self, 3093 quarterLengthList: List[Union[int, float]], 3094 addTies=True, 3095 displayTiedAccidentals=False 3096 ) -> _SplitTuple: 3097 ''' 3098 Given a list of quarter lengths, return a list of 3099 Music21Object objects, copied from this Music21Object, 3100 that are partitioned and tied with the specified quarter 3101 length list durations. 3102 3103 TODO: unite into a "split" function -- document obscure uses. 3104 3105 >>> n = note.Note() 3106 >>> n.quarterLength = 3 3107 >>> post = n.splitByQuarterLengths([1, 1, 1]) 3108 >>> [n.quarterLength for n in post] 3109 [1.0, 1.0, 1.0] 3110 ''' 3111 if opFrac(sum(quarterLengthList)) != self.duration.quarterLength: 3112 raise Music21ObjectException( 3113 'cannot split by quarter length list whose sum is not ' 3114 + 'equal to the quarterLength duration of the source: ' 3115 + f'{quarterLengthList}, {self.duration.quarterLength}' 3116 ) 3117 3118 # if nothing to do 3119 if len(quarterLengthList) == 1: 3120 # return a copy of self in a list 3121 return _SplitTuple([copy.deepcopy(self)]) 3122 elif len(quarterLengthList) <= 1: 3123 raise Music21ObjectException( 3124 f'cannot split by this quarter length list: {quarterLengthList}.') 3125 3126 eList = [] 3127 spannerList = [] # this does not fully work with trills over multiple splits yet. 3128 eRemain = copy.deepcopy(self) 3129 for qlIndex in range(len(quarterLengthList) - 1): 3130 qlSplit = quarterLengthList[qlIndex] 3131 st = eRemain.splitAtQuarterLength(qlSplit, 3132 addTies=addTies, 3133 displayTiedAccidentals=displayTiedAccidentals) 3134 newEl, eRemain = st 3135 eList.append(newEl) 3136 spannerList.extend(st.spannerList) 3137 3138 if eRemain is not None: 3139 eList.append(eRemain) 3140 3141 stOut = _SplitTuple(eList) 3142 stOut.spannerList = spannerList 3143 return stOut 3144 3145 def splitAtDurations(self: _M21T) -> _SplitTuple: 3146 ''' 3147 Takes a Music21Object (e.g., a note.Note) and returns a list of similar 3148 objects with only a single duration.DurationTuple in each. 3149 Ties are added if the object supports ties. 3150 3151 Articulations only appear on the first note. Same with lyrics. 3152 3153 Fermatas should be on last note, but not done yet. 3154 3155 >>> a = note.Note() 3156 >>> a.duration.clear() # remove defaults 3157 >>> a.duration.addDurationTuple(duration.durationTupleFromTypeDots('half', 0)) 3158 >>> a.duration.quarterLength 3159 2.0 3160 >>> a.duration.addDurationTuple(duration.durationTupleFromTypeDots('whole', 0)) 3161 >>> a.duration.quarterLength 3162 6.0 3163 >>> b = a.splitAtDurations() 3164 >>> b 3165 (<music21.note.Note C>, <music21.note.Note C>) 3166 >>> b[0].pitch == b[1].pitch 3167 True 3168 >>> b[0].duration 3169 <music21.duration.Duration 2.0> 3170 >>> b[0].duration.type 3171 'half' 3172 >>> b[1].duration.type 3173 'whole' 3174 >>> b[0].quarterLength, b[1].quarterLength 3175 (2.0, 4.0) 3176 3177 >>> c = note.Note() 3178 >>> c.quarterLength = 2.5 3179 >>> d, e = c.splitAtDurations() 3180 >>> d.duration.type 3181 'half' 3182 >>> e.duration.type 3183 'eighth' 3184 >>> d.tie.type 3185 'start' 3186 >>> print(e.tie) 3187 <music21.tie.Tie stop> 3188 3189 Assume c is tied to the next note. Then the last split note should also be tied 3190 3191 >>> c.tie = tie.Tie('start') 3192 >>> d, e = c.splitAtDurations() 3193 >>> d.tie.type 3194 'start' 3195 >>> e.tie.type 3196 'continue' 3197 3198 Rests have no ties: 3199 3200 >>> f = note.Rest() 3201 >>> f.quarterLength = 2.5 3202 >>> g, h = f.splitAtDurations() 3203 >>> (g.duration.type, h.duration.type) 3204 ('half', 'eighth') 3205 >>> f.tie is None 3206 True 3207 >>> g.tie is None 3208 True 3209 3210 3211 It should work for complex notes with tuplets. 3212 3213 (this duration occurs in Modena A, Le greygnour bien, from the ars subtilior, c. 1380; 3214 hence how I discovered this bug) 3215 3216 >>> n = note.Note() 3217 >>> n.duration.quarterLength = 0.5 + 0.0625 # eighth + 64th 3218 >>> t = duration.Tuplet(4, 3) 3219 >>> n.duration.appendTuplet(t) 3220 >>> first, last = n.splitAtDurations() 3221 >>> (first.duration, last.duration) 3222 (<music21.duration.Duration 0.375>, <music21.duration.Duration 0.046875>) 3223 3224 Notice that this duration could have been done w/o tuplets, so no tuplets in output: 3225 3226 >>> (first.duration.type, first.duration.dots, first.duration.tuplets) 3227 ('16th', 1, ()) 3228 >>> (last.duration.type, last.duration.dots, last.duration.tuplets) 3229 ('128th', 1, ()) 3230 3231 Test of one with tuplets that cannot be split: 3232 3233 >>> n = note.Note() 3234 >>> n.duration.quarterLength = 0.5 + 0.0625 # eighth + 64th 3235 >>> t = duration.Tuplet(3, 2, 'eighth') 3236 >>> n.duration.appendTuplet(t) 3237 >>> (n.duration.type, n.duration.dots, n.duration.tuplets) 3238 ('complex', 0, (<music21.duration.Tuplet 3/2/eighth>,)) 3239 3240 >>> first, last = n.splitAtDurations() 3241 >>> (first.duration, last.duration) 3242 (<music21.duration.Duration 1/3>, <music21.duration.Duration 1/24>) 3243 3244 >>> (first.duration.type, first.duration.dots, first.duration.tuplets) 3245 ('eighth', 0, (<music21.duration.Tuplet 3/2/eighth>,)) 3246 >>> (last.duration.type, last.duration.dots, last.duration.tuplets) 3247 ('64th', 0, (<music21.duration.Tuplet 3/2/64th>,)) 3248 3249 3250 TODO: unite this and other functions into a "split" function -- document obscure uses. 3251 3252 ''' 3253 atm = self.duration.aggregateTupletMultiplier() 3254 quarterLengthList = [c.quarterLength * atm for c in self.duration.components] 3255 splitList = self.splitByQuarterLengths(quarterLengthList) 3256 return splitList 3257 # ------------------------------------------------------------------------- 3258 # temporal and beat based positioning 3259 3260 @property 3261 def measureNumber(self) -> Optional[int]: 3262 # noinspection PyShadowingNames 3263 ''' 3264 Return the measure number of a :class:`~music21.stream.Measure` that contains this 3265 object if the object is in a measure. 3266 3267 Returns None if the object is not in a measure. Also note that by 3268 default Measure objects 3269 have measure number 0. 3270 3271 If an object belongs to multiple measures (not in the same hierarchy...) 3272 then it returns the 3273 measure number of the :meth:`~music21.base.Music21Object.activeSite` if that is a 3274 :class:`~music21.stream.Measure` object. Otherwise it will use 3275 :meth:`~music21.base.Music21Object.getContextByClass` 3276 to find the number of the measure it was most recently added to. 3277 3278 >>> m = stream.Measure() 3279 >>> m.number = 12 3280 >>> n = note.Note() 3281 >>> m.append(n) 3282 >>> n.measureNumber 3283 12 3284 3285 >>> n2 = note.Note() 3286 >>> n2.measureNumber is None 3287 True 3288 >>> m2 = stream.Measure() 3289 >>> m2.append(n2) 3290 >>> n2.measureNumber 3291 0 3292 3293 This updates live if the measure number changes: 3294 3295 >>> m2.number = 11 3296 >>> n2.measureNumber 3297 11 3298 3299 3300 The most recent measure added to is used unless activeSite is a measure: 3301 3302 >>> m.append(n2) 3303 >>> n2.measureNumber 3304 12 3305 >>> n2.activeSite = m2 3306 >>> n2.measureNumber 3307 11 3308 3309 Copies can retain measure numbers until set themselves: 3310 3311 >>> import copy 3312 >>> nCopy = copy.deepcopy(n2) 3313 >>> nCopy.measureNumber 3314 12 3315 >>> m3 = stream.Measure() 3316 >>> m3.number = 4 3317 >>> m3.append(nCopy) 3318 >>> nCopy.measureNumber 3319 4 3320 ''' 3321 mNumber = None # default for not defined 3322 if self.activeSite is not None and self.activeSite.isMeasure: 3323 mNumber = self.activeSite.number 3324 else: 3325 # testing sortByCreationTime == true; this may be necessary 3326 # as we often want the most recent measure 3327 for cs in self.contextSites(): 3328 m = cs[0] 3329 if m.isMeasure: 3330 mNumber = m.number 3331 return mNumber 3332 3333 def _getMeasureOffset(self, includeMeasurePadding=True) -> Union[float, fractions.Fraction]: 3334 # noinspection PyShadowingNames 3335 ''' 3336 Try to obtain the nearest Measure that contains this object, 3337 and return the offset of this object within that Measure. 3338 3339 If a Measure is found, and that Measure has padding 3340 defined as `paddingLeft` (for pickup measures, etc.), padding will be added to the 3341 native offset gathered from the object. 3342 3343 >>> n = note.Note() 3344 >>> n.quarterLength = 2 3345 >>> m = stream.Measure() 3346 >>> n._getMeasureOffset() # returns zero when not assigned 3347 0.0 3348 >>> n.quarterLength = 0.5 3349 3350 >>> m = stream.Measure() 3351 >>> m.repeatAppend(n, 4) 3352 >>> [n._getMeasureOffset() for n in m.notes] 3353 [0.0, 0.5, 1.0, 1.5] 3354 3355 >>> m.paddingLeft = 2 3356 >>> [n._getMeasureOffset() for n in m.notes] 3357 [2.0, 2.5, 3.0, 3.5] 3358 >>> [n._getMeasureOffset(includeMeasurePadding=False) for n in m.notes] 3359 [0.0, 0.5, 1.0, 1.5] 3360 ''' 3361 # TODO: v7 -- expose as public. 3362 activeS = self.activeSite 3363 if activeS is not None and activeS.isMeasure: 3364 # environLocal.printDebug(['found activeSite as Measure, using for offset']) 3365 offsetLocal = activeS.elementOffset(self) 3366 if includeMeasurePadding: 3367 offsetLocal += activeS.paddingLeft 3368 else: 3369 # environLocal.printDebug(['did not find activeSite as Measure, 3370 # doing context search', 'self.activeSite', self.activeSite]) 3371 # testing sortByCreationTime == true; this may be necessary 3372 # as we often want the most recent measure 3373 m = self.getContextByClass('Measure', sortByCreationTime=True) 3374 if m is not None: 3375 # environLocal.printDebug(['using found Measure for offset access']) 3376 try: 3377 offsetLocal = m.elementOffset(self) 3378 if includeMeasurePadding: 3379 offsetLocal += m.paddingLeft 3380 except SitesException: 3381 offsetLocal = self.offset 3382 3383 else: # hope that we get the right one 3384 # environLocal.printDebug( 3385 # ['_getMeasureOffset(): cannot find a Measure; using standard offset access']) 3386 offsetLocal = self.offset 3387 3388 # environLocal.printDebug( 3389 # ['_getMeasureOffset(): found local offset as:', offsetLocal, self]) 3390 return offsetLocal 3391 3392 def _getTimeSignatureForBeat(self) -> 'music21.meter.TimeSignature': 3393 ''' 3394 used by all the _getBeat, _getBeatDuration, _getBeatStrength functions. 3395 3396 extracted to make sure that all three of the routines use the same one. 3397 ''' 3398 ts: Optional['music21.meter.TimeSignature'] = self.getContextByClass( 3399 'TimeSignature', getElementMethod='getElementAtOrBeforeOffset') 3400 if ts is None: 3401 raise Music21ObjectException('this object does not have a TimeSignature in Sites') 3402 return ts 3403 3404 @property 3405 def beat(self) -> Union[fractions.Fraction, float]: 3406 # noinspection PyShadowingNames 3407 ''' 3408 Return the beat of this object as found in the most 3409 recently positioned Measure. Beat values count from 1 and 3410 contain a floating-point designation between 0 and 1 to 3411 show proportional progress through the beat. 3412 3413 >>> n = note.Note() 3414 >>> n.quarterLength = 0.5 3415 >>> m = stream.Measure() 3416 >>> m.timeSignature = meter.TimeSignature('3/4') 3417 >>> m.repeatAppend(n, 6) 3418 >>> [n.beat for n in m.notes] 3419 [1.0, 1.5, 2.0, 2.5, 3.0, 3.5] 3420 3421 3422 Fractions are returned for positions that cannot be represented perfectly using floats: 3423 3424 >>> m.timeSignature = meter.TimeSignature('6/8') 3425 >>> [n.beat for n in m.notes] 3426 [1.0, Fraction(4, 3), Fraction(5, 3), 2.0, Fraction(7, 3), Fraction(8, 3)] 3427 3428 >>> s = stream.Stream() 3429 >>> s.insert(0, meter.TimeSignature('3/4')) 3430 >>> s.repeatAppend(note.Note(), 8) 3431 >>> [n.beat for n in s.notes] 3432 [1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0] 3433 3434 3435 Notes inside flat streams can still find the original beat placement from outer 3436 streams: 3437 3438 >>> p = stream.Part() 3439 >>> ts = meter.TimeSignature('2/4') 3440 >>> p.insert(0, ts) 3441 3442 >>> n = note.Note('C4', type='eighth') 3443 >>> m1 = stream.Measure(number=1) 3444 >>> m1.repeatAppend(n, 4) 3445 3446 >>> m2 = stream.Measure(number=2) 3447 >>> m2.repeatAppend(n, 4) 3448 3449 >>> p.append([m1, m2]) 3450 >>> [n.beat for n in p.flatten().notes] 3451 [1.0, 1.5, 2.0, 2.5, 1.0, 1.5, 2.0, 2.5] 3452 3453 3454 Fractions print out as improper fraction strings 3455 3456 >>> m = stream.Measure() 3457 >>> m.timeSignature = meter.TimeSignature('4/4') 3458 >>> n = note.Note() 3459 >>> n.quarterLength = 1/3 3460 >>> m.repeatAppend(n, 12) 3461 >>> for n in m.notes[:5]: 3462 ... print(n.beat) 3463 1.0 3464 4/3 3465 5/3 3466 2.0 3467 7/3 3468 3469 If there is no TimeSignature object in sites then returns the special float 3470 'nan' meaning "Not a Number": 3471 3472 >>> isolatedNote = note.Note('E4') 3473 >>> isolatedNote.beat 3474 nan 3475 3476 Not-a-number objects do not compare equal to themselves: 3477 3478 >>> isolatedNote.beat == isolatedNote.beat 3479 False 3480 3481 Instead to test for nan, import the math module and use `isnan()`: 3482 3483 >>> import math 3484 >>> math.isnan(isolatedNote.beat) 3485 True 3486 3487 3488 Changed in v.6.3 -- returns `nan` if 3489 there is no TimeSignature in sites. Previously raised an exception. 3490 ''' 3491 try: 3492 ts = self._getTimeSignatureForBeat() 3493 return ts.getBeatProportion(ts.getMeasureOffsetOrMeterModulusOffset(self)) 3494 except Music21ObjectException: 3495 return float('nan') 3496 3497 @property 3498 def beatStr(self) -> str: 3499 ''' 3500 Return a string representation of the beat of 3501 this object as found in the most recently positioned 3502 Measure. Beat values count from 1 and contain a 3503 fractional designation to show progress through the beat. 3504 3505 3506 >>> n = note.Note(type='eighth') 3507 >>> m = stream.Measure() 3508 >>> m.timeSignature = meter.TimeSignature('3/4') 3509 >>> m.repeatAppend(n, 6) 3510 3511 >>> [n.beatStr for n in m.notes] 3512 ['1', '1 1/2', '2', '2 1/2', '3', '3 1/2'] 3513 3514 >>> m.timeSignature = meter.TimeSignature('6/8') 3515 >>> [n.beatStr for n in m.notes] 3516 ['1', '1 1/3', '1 2/3', '2', '2 1/3', '2 2/3'] 3517 3518 >>> s = stream.Stream() 3519 >>> s.insert(0, meter.TimeSignature('3/4')) 3520 >>> s.repeatAppend(note.Note(type='quarter'), 8) 3521 >>> [n.beatStr for n in s.notes] 3522 ['1', '2', '3', '1', '2', '3', '1', '2'] 3523 3524 If there is no TimeSignature object in sites then returns 'nan' for not a number. 3525 3526 >>> isolatedNote = note.Note('E4') 3527 >>> isolatedNote.beatStr 3528 'nan' 3529 3530 Changed in v.6.3 -- returns 'nan' if 3531 there is no TimeSignature in sites. Previously raised an exception. 3532 ''' 3533 try: 3534 ts = self._getTimeSignatureForBeat() 3535 return ts.getBeatProportionStr(ts.getMeasureOffsetOrMeterModulusOffset(self)) 3536 except Music21ObjectException: 3537 return 'nan' 3538 3539 @property 3540 def beatDuration(self) -> 'music21.duration.Duration': 3541 ''' 3542 Return a :class:`~music21.duration.Duration` of the beat 3543 active for this object as found in the most recently 3544 positioned Measure. 3545 3546 If extending beyond the Measure, or in a Stream with a TimeSignature, 3547 the meter modulus value will be returned. 3548 3549 >>> n = note.Note('C4', type='eighth') 3550 >>> n.duration 3551 <music21.duration.Duration 0.5> 3552 3553 >>> m = stream.Measure() 3554 >>> m.timeSignature = meter.TimeSignature('3/4') 3555 >>> m.repeatAppend(n, 6) 3556 >>> n0 = m.notes.first() 3557 >>> n0.beatDuration 3558 <music21.duration.Duration 1.0> 3559 3560 Notice that the beat duration is the same for all these notes 3561 and has nothing to do with the duration of the element itself 3562 3563 >>> [n.beatDuration.quarterLength for n in m.notes] 3564 [1.0, 1.0, 1.0, 1.0, 1.0, 1.0] 3565 3566 Changing the time signature changes the beat duration: 3567 3568 >>> m.timeSignature = meter.TimeSignature('6/8') 3569 >>> [n.beatDuration.quarterLength for n in m.notes] 3570 [1.5, 1.5, 1.5, 1.5, 1.5, 1.5] 3571 3572 Complex time signatures will give different note lengths: 3573 3574 >>> s = stream.Stream() 3575 >>> s.insert(0, meter.TimeSignature('2/4+3/4')) 3576 >>> s.repeatAppend(note.Note(type='quarter'), 8) 3577 >>> [n.beatDuration.quarterLength for n in s.notes] 3578 [2.0, 2.0, 3.0, 3.0, 3.0, 2.0, 2.0, 3.0] 3579 3580 3581 If there is no TimeSignature object in sites then returns a duration object 3582 of Zero length. 3583 3584 >>> isolatedNote = note.Note('E4') 3585 >>> isolatedNote.beatDuration 3586 <music21.duration.Duration 0.0> 3587 3588 Changed in v.6.3 -- returns a duration.Duration object of length 0 if 3589 there is no TimeSignature in sites. Previously raised an exception. 3590 ''' 3591 try: 3592 ts = self._getTimeSignatureForBeat() 3593 return ts.getBeatDuration(ts.getMeasureOffsetOrMeterModulusOffset(self)) 3594 except Music21ObjectException: 3595 return duration.Duration(0) 3596 3597 @property 3598 def beatStrength(self) -> float: 3599 ''' 3600 Return the metrical accent of this object 3601 in the most recently positioned Measure. Accent values 3602 are between zero and one, and are derived from the local 3603 TimeSignature's accent MeterSequence weights. If the offset 3604 of this object does not match a defined accent weight, a 3605 minimum accent weight will be returned. 3606 3607 3608 >>> n = note.Note(type='eighth') 3609 >>> m = stream.Measure() 3610 >>> m.timeSignature = meter.TimeSignature('3/4') 3611 >>> m.repeatAppend(n, 6) 3612 3613 The first note of a measure is (generally?) always beat strength 1.0: 3614 3615 >>> m.notes.first().beatStrength 3616 1.0 3617 3618 Notes on weaker beats have lower strength: 3619 3620 >>> [n.beatStrength for n in m.notes] 3621 [1.0, 0.25, 0.5, 0.25, 0.5, 0.25] 3622 3623 >>> m.timeSignature = meter.TimeSignature('6/8') 3624 >>> [n.beatStrength for n in m.notes] 3625 [1.0, 0.25, 0.25, 0.5, 0.25, 0.25] 3626 3627 3628 Importantly, the actual numbers here have no particular meaning. You cannot 3629 "add" two beatStrengths of 0.25 and say that they have the same beat strength 3630 as one note of 0.5. Only the ordinal relations really matter. Even taking 3631 an average of beat strengths is a tiny bit methodologically suspect (though 3632 it is common in research for lack of a better method). 3633 3634 We can also get the beatStrength for elements not in 3635 a measure, if the enclosing stream has a :class:`~music21.meter.TimeSignature`. 3636 We just assume that the time signature carries through to 3637 hypothetical following measures: 3638 3639 >>> n = note.Note('E-3', type='quarter') 3640 >>> s = stream.Stream() 3641 >>> s.insert(0.0, meter.TimeSignature('2/2')) 3642 >>> s.repeatAppend(n, 12) 3643 >>> [n.beatStrength for n in s.notes] 3644 [1.0, 0.25, 0.5, 0.25, 1.0, 0.25, 0.5, 0.25, 1.0, 0.25, 0.5, 0.25] 3645 3646 3647 Changing the meter changes the output, of course, as can be seen from the 3648 fourth quarter note onward: 3649 3650 >>> s.insert(4.0, meter.TimeSignature('3/4')) 3651 >>> [n.beatStrength for n in s.notes] 3652 [1.0, 0.25, 0.5, 0.25, 1.0, 0.5, 0.5, 1.0, 0.5, 0.5, 1.0, 0.5] 3653 3654 3655 The method returns correct numbers for the prevailing time signature 3656 even if no measures have been made: 3657 3658 >>> n = note.Note('E--3', type='half') 3659 >>> s = stream.Stream() 3660 >>> s.isMeasure 3661 False 3662 3663 >>> s.insert(0, meter.TimeSignature('2/2')) 3664 >>> s.repeatAppend(n, 16) 3665 >>> s.notes[0].beatStrength 3666 1.0 3667 >>> s.notes[1].beatStrength 3668 0.5 3669 >>> s.notes[4].beatStrength 3670 1.0 3671 >>> s.notes[5].beatStrength 3672 0.5 3673 3674 Getting the beatStrength of an object without a time signature in its context 3675 returns the not-a-number special object 'nan': 3676 3677 >>> n2 = note.Note(type='whole') 3678 >>> n2.beatStrength 3679 nan 3680 >>> from math import isnan 3681 >>> isnan(n2.beatStrength) 3682 True 3683 3684 Changed in v6.3 -- return 'nan' instead of raising an exception. 3685 ''' 3686 try: 3687 ts = self._getTimeSignatureForBeat() 3688 meterModulus = ts.getMeasureOffsetOrMeterModulusOffset(self) 3689 3690 return ts.getAccentWeight(meterModulus, 3691 forcePositionMatch=True, 3692 permitMeterModulus=False) 3693 except Music21ObjectException: 3694 return float('nan') 3695 3696 def _getSeconds(self) -> float: 3697 # do not search of duration is zero 3698 if self.duration.quarterLength == 0.0: 3699 return 0.0 3700 3701 ti = self.getContextByClass('TempoIndication') 3702 if ti is None: 3703 return float('nan') 3704 mm = ti.getSoundingMetronomeMark() 3705 # once we have mm, simply pass in this duration 3706 return mm.durationToSeconds(self.duration) 3707 3708 def _setSeconds(self, value: Union[int, float]) -> None: 3709 ti = self.getContextByClass('TempoIndication') 3710 if ti is None: 3711 raise Music21ObjectException('this object does not have a TempoIndication in Sites') 3712 mm = ti.getSoundingMetronomeMark() 3713 self.duration = mm.secondsToDuration(value) 3714 for s in self.sites.get(excludeNone=True): 3715 if self in s.elements: 3716 s.coreElementsChanged() # highest time is changed. 3717 3718 seconds = property(_getSeconds, _setSeconds, doc=''' 3719 Get or set the duration of this object in seconds, assuming 3720 that this object has a :class:`~music21.tempo.MetronomeMark` 3721 or :class:`~music21.tempo.MetricModulation` 3722 (or any :class:`~music21.tempo.TempoIndication`) in its past context. 3723 3724 >>> s = stream.Stream() 3725 >>> for i in range(3): 3726 ... s.append(note.Note(type='quarter')) 3727 ... s.append(note.Note(type='quarter', dots=1)) 3728 >>> s.insert(0, tempo.MetronomeMark(number=60)) 3729 >>> s.insert(2, tempo.MetronomeMark(number=120)) 3730 >>> s.insert(4, tempo.MetronomeMark(number=30)) 3731 >>> [n.seconds for n in s.notes] 3732 [1.0, 1.5, 0.5, 0.75, 2.0, 3.0] 3733 3734 Setting the number of seconds on a music21 object changes its duration: 3735 3736 >>> lastNote = s.notes[-1] 3737 >>> lastNote.duration.fullName 3738 'Dotted Quarter' 3739 >>> lastNote.seconds = 4.0 3740 >>> lastNote.duration.fullName 3741 'Half' 3742 3743 Any object of length 0 has zero-second length: 3744 3745 >>> tc = clef.TrebleClef() 3746 >>> tc.seconds 3747 0.0 3748 3749 If an object has positive duration but no tempo indication in its context, 3750 then the special number 'nan' for "not-a-number" is returned: 3751 3752 >>> r = note.Rest(type='whole') 3753 >>> r.seconds 3754 nan 3755 3756 Check for 'nan' with the `math.isnan()` routine: 3757 3758 >>> import math 3759 >>> math.isnan(r.seconds) 3760 True 3761 3762 Setting seconds for an element without a tempo-indication in its sites raises 3763 a Music21ObjectException: 3764 3765 >>> r.seconds = 2.0 3766 Traceback (most recent call last): 3767 music21.base.Music21ObjectException: this object does not have a TempoIndication in Sites 3768 3769 Note that if an object is in multiple Sites with multiple Metronome marks, 3770 the activeSite (or the hierarchy of the activeSite) 3771 determines its seconds for getting or setting: 3772 3773 >>> r = note.Rest(type='whole') 3774 >>> m1 = stream.Measure() 3775 >>> m1.insert(0, tempo.MetronomeMark(number=60)) 3776 >>> m1.append(r) 3777 >>> r.seconds 3778 4.0 3779 3780 >>> m2 = stream.Measure() 3781 >>> m2.insert(0, tempo.MetronomeMark(number=120)) 3782 >>> m2.append(r) 3783 >>> r.seconds 3784 2.0 3785 >>> r.activeSite = m1 3786 >>> r.seconds 3787 4.0 3788 >>> r.seconds = 1.0 3789 >>> r.duration.type 3790 'quarter' 3791 >>> r.activeSite = m2 3792 >>> r.seconds = 1.0 3793 >>> r.duration.type 3794 'half' 3795 3796 Changed in v6.3 -- return nan instead of raising an exception. 3797 ''') 3798 3799 3800# ------------------------------------------------------------------------------ 3801 3802 3803class ElementWrapper(Music21Object): 3804 ''' 3805 An ElementWrapper is a way of containing any object that is not a 3806 :class:`~music21.base.Music21Object`, so that that object can be positioned 3807 within a :class:`~music21.stream.Stream`. 3808 3809 The object stored within ElementWrapper is available from the 3810 :attr:`~music21.base.ElementWrapper.obj` attribute. All the attributes of 3811 the stored object (except .id and anything else that conflicts with a 3812 Music21Object attribute) are gettable and settable by querying the 3813 ElementWrapper. This feature makes it possible easily to mix 3814 Music21Objects and non-Music21Objects with similarly named attributes in 3815 the same Stream. 3816 3817 This example inserts 10 random wave files into a music21 Stream and then 3818 reports their filename and number of audio channels (in this example, it's 3819 always 2) if they fall on a strong beat in fast 6/8 3820 3821 >>> import music21 3822 >>> #_DOCS_SHOW import wave 3823 >>> import random 3824 >>> class Wave_read: #_DOCS_HIDE 3825 ... def getnchannels(self): return 2 #_DOCS_HIDE 3826 3827 >>> s = stream.Stream() 3828 >>> s.id = 'mainStream' 3829 >>> s.append(meter.TimeSignature('fast 6/8')) 3830 >>> for i in range(10): 3831 ... #_DOCS_SHOW fileName = 'thisSound_' + str(random.randint(1, 20)) + '.wav' 3832 ... fileName = 'thisSound_' + str(1 + ((i * 100) % 19)) + '.wav' #_DOCS_HIDE 3833 ... soundFile = Wave_read() #_DOCS_HIDE # #make a more predictable "random" set. 3834 ... #_DOCS_SHOW soundFile = wave.open(fileName) 3835 ... soundFile.fileName = fileName 3836 ... el = music21.ElementWrapper(soundFile) 3837 ... s.insert(i, el) 3838 3839 >>> for j in s.getElementsByClass('ElementWrapper'): 3840 ... if j.beatStrength > 0.4: 3841 ... (j.offset, j.beatStrength, j.getnchannels(), j.fileName) 3842 (0.0, 1.0, 2, 'thisSound_1.wav') 3843 (3.0, 1.0, 2, 'thisSound_16.wav') 3844 (6.0, 1.0, 2, 'thisSound_12.wav') 3845 (9.0, 1.0, 2, 'thisSound_8.wav') 3846 >>> for j in s.getElementsByClass('ElementWrapper'): 3847 ... if j.beatStrength > 0.4: 3848 ... (j.offset, j.beatStrength, j.getnchannels() + 1, j.fileName) 3849 (0.0, 1.0, 3, 'thisSound_1.wav') 3850 (3.0, 1.0, 3, 'thisSound_16.wav') 3851 (6.0, 1.0, 3, 'thisSound_12.wav') 3852 (9.0, 1.0, 3, 'thisSound_8.wav') 3853 3854 Test representation of an ElementWrapper 3855 3856 >>> for i, j in enumerate(s.getElementsByClass('ElementWrapper')): 3857 ... if i == 2: 3858 ... j.id = None 3859 ... else: 3860 ... j.id = str(i) + '_wrapper' 3861 ... if i <=2: 3862 ... print(j) 3863 <music21.base.ElementWrapper id=0_wrapper offset=0.0 obj='<...Wave_read object...'> 3864 <music21.base.ElementWrapper id=1_wrapper offset=1.0 obj='<...Wave_read object...'> 3865 <music21.base.ElementWrapper offset=2.0 obj='<...Wave_read object...>'> 3866 ''' 3867 _id = None 3868 obj = None 3869 3870 _DOC_ORDER = ['obj'] 3871 _DOC_ATTR = { 3872 'obj': 'The object this wrapper wraps. It should not be a Music21Object.', 3873 } 3874 3875 def __init__(self, obj=None): 3876 super().__init__() 3877 self.obj = obj # object stored here 3878 # the unlinkedDuration is the duration that is inherited from 3879 # Music21Object 3880 # self._unlinkedDuration = None 3881 3882 # ------------------------------------------------------------------------- 3883 3884 def _reprInternal(self): 3885 shortObj = (str(self.obj))[0:30] 3886 if len(str(self.obj)) > 30: 3887 shortObj += '...' 3888 if shortObj[0] == '<': 3889 shortObj += '>' 3890 3891 if self.id is not None: 3892 return f'id={self.id} offset={self.offset} obj={shortObj!r}' 3893 else: 3894 return f'offset={self.offset} obj={shortObj!r}' 3895 3896 def __eq__(self, other) -> bool: 3897 '''Test ElementWrapper equality 3898 3899 >>> import music21 3900 >>> n = note.Note('C#') 3901 >>> a = music21.ElementWrapper(n) 3902 >>> a.offset = 3.0 3903 >>> b = music21.ElementWrapper(n) 3904 >>> b.offset = 3.0 3905 >>> a == b 3906 True 3907 >>> a is not b 3908 True 3909 >>> c = music21.ElementWrapper(n) 3910 >>> c.offset = 2.0 3911 >>> c.offset 3912 2.0 3913 >>> a == c 3914 False 3915 ''' 3916 for other_prop in ('obj', 'offset', 'priority', 'groups', 'activeSite', 'duration'): 3917 if not hasattr(other, other_prop): 3918 return False 3919 3920 if (self.obj == other.obj 3921 and self.offset == other.offset 3922 and self.priority == other.priority 3923 and self.groups == other.groups 3924 and self.duration == self.duration): 3925 return True 3926 else: 3927 return False 3928 3929 def __setattr__(self, name: str, value: Any) -> None: 3930 # environLocal.printDebug(['calling __setattr__ of ElementWrapper', name, value]) 3931 3932 # if in the ElementWrapper already, set that first 3933 if name in self.__dict__: 3934 object.__setattr__(self, name, value) 3935 3936 # if not, change the attribute in the stored object 3937 storedObj = object.__getattribute__(self, 'obj') 3938 if (name not in ('offset', '_offset', '_activeSite') 3939 and storedObj is not None 3940 and hasattr(storedObj, name)): 3941 setattr(storedObj, name, value) 3942 # unless neither has the attribute, in which case add it to the ElementWrapper 3943 else: 3944 object.__setattr__(self, name, value) 3945 3946 def __getattr__(self, name: str) -> Any: 3947 '''This method is only called when __getattribute__() fails. 3948 Using this also avoids the potential recursion problems of subclassing 3949 __getattribute__()_ 3950 3951 see: https://stackoverflow.com/questions/371753/python-using-getattribute-method 3952 for examples 3953 ''' 3954 storedObj = Music21Object.__getattribute__(self, 'obj') 3955 if storedObj is None: 3956 raise AttributeError(f'Could not get attribute {name!r} in an object-less element') 3957 return object.__getattribute__(storedObj, name) 3958 3959 def isTwin(self, other: 'ElementWrapper') -> bool: 3960 ''' 3961 A weaker form of equality. a.isTwin(b) is true if 3962 a and b store either the same object OR objects that are equal. 3963 In other words, it is essentially the same object in a different context 3964 3965 >>> import copy 3966 >>> import music21 3967 3968 >>> aE = music21.ElementWrapper(obj='hello') 3969 3970 >>> bE = copy.copy(aE) 3971 >>> aE is bE 3972 False 3973 >>> aE == bE 3974 True 3975 >>> aE.isTwin(bE) 3976 True 3977 3978 >>> bE.offset = 14.0 3979 >>> bE.priority = -4 3980 >>> aE == bE 3981 False 3982 >>> aE.isTwin(bE) 3983 True 3984 ''' 3985 if not hasattr(other, 'obj'): 3986 return False 3987 3988 if self.obj is other.obj or self.obj == other.obj: 3989 return True 3990 else: 3991 return False 3992 3993 3994# ----------------------------------------------------------------------------- 3995 3996 3997class TestMock(Music21Object): 3998 pass 3999 4000 4001class Test(unittest.TestCase): 4002 4003 def testCopyAndDeepcopy(self): 4004 ''' 4005 Test copying all objects defined in this module 4006 ''' 4007 for part in sys.modules[self.__module__].__dict__: 4008 match = False 4009 for skip in ['_', '__', 'Test', 'Exception']: 4010 if part.startswith(skip) or part.endswith(skip): 4011 match = True 4012 if match: 4013 continue 4014 name = getattr(sys.modules[self.__module__], part) 4015 # noinspection PyTypeChecker 4016 if callable(name) and not isinstance(name, types.FunctionType): 4017 try: # see if obj can be made w/ args 4018 obj = name() 4019 except TypeError: 4020 continue 4021 try: 4022 i = copy.copy(obj) 4023 j = copy.deepcopy(obj) 4024 except TypeError as e: 4025 self.fail(f'Could not copy {obj}: {e}') 4026 4027 def testM21ObjRepr(self): 4028 from music21 import base 4029 a = base.Music21Object() 4030 address = hex(id(a)) 4031 self.assertEqual(repr(a), f'<music21.base.Music21Object object at {address}>') 4032 4033 def testObjectCreation(self): 4034 a = TestMock() 4035 a.groups.append('hello') 4036 a.id = 'hi' 4037 a.offset = 2.0 4038 self.assertEqual(a.offset, 2.0) 4039 4040 def testElementEquality(self): 4041 from music21 import note 4042 n = note.Note('F-') 4043 a = ElementWrapper(n) 4044 a.offset = 3.0 4045 c = ElementWrapper(n) 4046 c.offset = 3.0 4047 self.assertEqual(a, c) 4048 self.assertIsNot(a, c) 4049 b = ElementWrapper(n) 4050 b.offset = 2.0 4051 self.assertNotEqual(a, b) 4052 4053 def testNoteCreation(self): 4054 from music21 import note 4055 n = note.Note('A') 4056 n.offset = 1.0 # duration.Duration('quarter') 4057 n.groups.append('flute') 4058 4059 b = copy.deepcopy(n) 4060 b.offset = 2.0 # duration.Duration('half') 4061 4062 self.assertFalse(n is b) 4063 n.pitch.accidental = '-' 4064 self.assertEqual(b.name, 'A') 4065 self.assertEqual(n.offset, 1.0) 4066 self.assertEqual(b.offset, 2.0) 4067 n.groups[0] = 'bassoon' 4068 self.assertFalse('flute' in n.groups) 4069 self.assertTrue('flute' in b.groups) 4070 4071 def testOffsets(self): 4072 from music21 import note 4073 a = ElementWrapper(note.Note('A#')) 4074 a.offset = 23.0 4075 self.assertEqual(a.offset, 23.0) 4076 4077 def testObjectsAndElements(self): 4078 from music21 import note 4079 from music21 import stream 4080 note1 = note.Note('B-') 4081 note1.duration.type = 'whole' 4082 stream1 = stream.Stream() 4083 stream1.append(note1) 4084 unused_subStream = stream1.notes 4085 4086 def testM21BaseDeepcopy(self): 4087 ''' 4088 Test copying 4089 ''' 4090 a = Music21Object() 4091 a.id = 'test' 4092 b = copy.deepcopy(a) 4093 self.assertNotEqual(a, b) 4094 self.assertEqual(b.id, 'test') 4095 4096 def testM21BaseSites(self): 4097 ''' 4098 Basic testing of M21 base object sites 4099 ''' 4100 from music21 import stream 4101 from music21 import base # self import needed. 4102 a = base.Music21Object() 4103 b = stream.Stream() 4104 4105 # storing a single offset does not add a Sites entry 4106 a.offset = 30 4107 # all offsets are store in locations 4108 self.assertEqual(len(a.sites), 1) 4109 self.assertEqual(a._naiveOffset, 30.0) 4110 self.assertEqual(a.offset, 30.0) 4111 4112 # assigning a activeSite directly # v2.1. no longer allowed if not in site 4113 def assignActiveSite(aa, bb): 4114 aa.activeSite = bb 4115 4116 self.assertRaises(SitesException, assignActiveSite, a, b) 4117 # now we have two offsets in locations 4118 b.insert(a) 4119 self.assertEqual(len(a.sites), 2) 4120 self.assertEqual(a.activeSite, b) 4121 4122 a.offset = 40 4123 # still have activeSite 4124 self.assertEqual(a.activeSite, b) 4125 # now the offset returns the value for the current activeSite 4126 # b.setElementOffset(a, 40.0) 4127 self.assertEqual(a.offset, 40.0) 4128 4129 # assigning a activeSite to None 4130 a.activeSite = None 4131 # properly returns original offset 4132 self.assertEqual(a.offset, 30.0) 4133 # we still have two locations stored 4134 self.assertEqual(len(a.sites), 2) 4135 self.assertEqual(a.getOffsetBySite(b), 40.0) 4136 4137 def testM21BaseLocationsCopy(self): 4138 ''' 4139 Basic testing of M21 base object 4140 ''' 4141 from music21 import stream 4142 from music21 import base 4143 a = stream.Stream() 4144 a.id = 'a obj' 4145 b = base.Music21Object() 4146 b.id = 'b obj' 4147 4148 b.id = 'test' 4149 a.insert(0, b) 4150 c = copy.deepcopy(b) 4151 c.id = 'c obj' 4152 4153 # have two locations: None, and that set by assigning activeSite 4154 self.assertEqual(len(b.sites), 2) 4155 dummy = c.sites 4156 # c is in 1 site, None, because it is a copy of b 4157 self.assertEqual(len(c.sites), 1) 4158 4159 def testM21BaseLocationsCopyB(self): 4160 # the active site of a deepcopy should not be the same? 4161 # self.assertEqual(post[-1].activeSite, a) 4162 from music21 import stream 4163 from music21 import base 4164 a = stream.Stream() 4165 b = base.Music21Object() 4166 b.id = 'test' 4167 a.insert(30, b) 4168 b.activeSite = a 4169 4170 d = stream.Stream() 4171 self.assertEqual(b.activeSite, a) 4172 self.assertEqual(len(b.sites), 2) 4173 c = copy.deepcopy(b) 4174 self.assertIs(c.activeSite, None) 4175 d.insert(20, c) 4176 self.assertEqual(len(c.sites), 2) 4177 4178 # this works because the activeSite is being set on the object 4179 # the copied activeSite has been deepcopied, and cannot now be accessed 4180 # this fails! post[-1].getOffsetBySite(a) 4181 4182 def testSitesSearch(self): 4183 from music21 import note 4184 from music21 import stream 4185 from music21 import clef 4186 4187 n1 = note.Note('A') 4188 n2 = note.Note('B') 4189 4190 s1 = stream.Stream(id='s1') 4191 s1.insert(10, n1) 4192 s1.insert(100, n2) 4193 4194 c1 = clef.TrebleClef() 4195 c2 = clef.BassClef() 4196 4197 s2 = stream.Stream(id='s2') 4198 s2.insert(0, c1) 4199 s2.insert(100, c2) 4200 s2.insert(10, s1) # placing s1 here should result in c2 being before n2 4201 4202 self.assertEqual(s1.getOffsetBySite(s2), 10) 4203 # make sure in the context of s1 things are as we expect 4204 self.assertEqual(s2.flatten().getElementAtOrBefore(0), c1) 4205 self.assertEqual(s2.flatten().getElementAtOrBefore(100), c2) 4206 self.assertEqual(s2.flatten().getElementAtOrBefore(20), n1) 4207 self.assertEqual(s2.flatten().getElementAtOrBefore(110), n2) 4208 4209 # note: we cannot do this 4210 # self.assertEqual(s2.flatten().getOffsetBySite(n2), 110) 4211 # we can do this: 4212 self.assertEqual(n2.getOffsetBySite(s2.flatten()), 110) 4213 4214 # this seems more idiomatic 4215 self.assertEqual(s2.flatten().elementOffset(n2), 110) 4216 4217 # both notes can find the treble clef in the activeSite stream 4218 post = n1.getContextByClass(clef.TrebleClef) 4219 self.assertIsInstance(post, clef.TrebleClef) 4220 4221 post = n2.getContextByClass(clef.TrebleClef) 4222 self.assertIsInstance(post, clef.TrebleClef) 4223 4224 # n1 cannot find a bass clef because it is before the bass clef 4225 post = n1.getContextByClass(clef.BassClef) 4226 self.assertEqual(post, None) 4227 4228 # n2 can find a bass clef, due to its shifted position in s2 4229 post = n2.getContextByClass(clef.BassClef) 4230 self.assertIsInstance(post, clef.BassClef) 4231 4232 def testSitesMeasures(self): 4233 '''Can a measure determine the last Clef used? 4234 ''' 4235 from music21 import corpus 4236 from music21 import clef 4237 from music21 import stream 4238 a = corpus.parse('bach/bwv324.xml') 4239 measures = a.parts[0].getElementsByClass('Measure').stream() # measures of first part 4240 4241 # the activeSite of measures[1] is set to the new output stream 4242 self.assertEqual(measures[1].activeSite, measures) 4243 # the source Part should still be a context of this measure 4244 self.assertIn(a.parts[0], measures[1].sites) 4245 4246 # from the first measure, we can get the clef by using 4247 # getElementsByClass 4248 post = measures[0].getElementsByClass(clef.Clef) 4249 self.assertIsInstance(post[0], clef.TrebleClef) 4250 4251 # make sure we can find offset in a flat representation 4252 self.assertRaises(SitesException, a.parts[0].flatten().elementOffset, a.parts[0][3]) 4253 4254 # for the second measure 4255 post = a.parts[0][3].getContextByClass(clef.Clef) 4256 self.assertIsInstance(post, clef.TrebleClef) 4257 4258 # for the second measure accessed from measures 4259 # we can get the clef, now that getContextByClass uses semiFlat 4260 post = measures[3].getContextByClass(clef.Clef) 4261 self.assertIsInstance(post, clef.TrebleClef) 4262 4263 # add the measure to a new stream 4264 newStream = stream.Stream() 4265 newStream.insert(0, measures[3]) 4266 # all previous locations are still available as a context 4267 self.assertTrue(newStream in measures[3].sites) 4268 self.assertTrue(measures in measures[3].sites) 4269 self.assertTrue(a.parts[0] in measures[3].sites) 4270 # we can still access the clef through this measure on this 4271 # new stream 4272 post = newStream[0].getContextByClass(clef.Clef) 4273 self.assertTrue(isinstance(post, clef.TrebleClef), post) 4274 4275 def testSitesClef(self): 4276 from music21 import note 4277 from music21 import stream 4278 from music21 import clef 4279 sOuter = stream.Stream() 4280 sOuter.id = 'sOuter' 4281 sInner = stream.Stream() 4282 sInner.id = 'sInner' 4283 4284 n = note.Note() 4285 sInner.append(n) 4286 sOuter.append(sInner) 4287 4288 # append clef to outer stream 4289 altoClef = clef.AltoClef() 4290 altoClef.priority = -1 4291 sOuter.insert(0, altoClef) 4292 pre = sOuter.getElementAtOrBefore(0, [clef.Clef]) 4293 self.assertTrue(isinstance(pre, clef.AltoClef), pre) 4294 4295 # we should be able to find a clef from the lower-level stream 4296 post = sInner.getContextByClass(clef.Clef) 4297 self.assertTrue(isinstance(post, clef.AltoClef), post) 4298 4299 def testBeatAccess(self): 4300 '''Test getting beat data from various Music21Objects. 4301 ''' 4302 from music21 import corpus 4303 s = corpus.parse('bach/bwv66.6.xml') 4304 p1 = s.parts['Soprano'] 4305 4306 # this does not work; cannot get these values from Measures 4307 # self.assertEqual(p1.getElementsByClass('Measure')[3].beat, 3) 4308 4309 # clef/ks can get its beat; these objects are in a pickup, 4310 # and this give their bar offset relative to the bar 4311 eClef = p1.flatten().getElementsByClass('Clef').first() 4312 self.assertEqual(eClef.beat, 4.0) 4313 self.assertEqual(eClef.beatDuration.quarterLength, 1.0) 4314 self.assertEqual(eClef.beatStrength, 0.25) 4315 4316 eKS = p1.flatten().getElementsByClass('KeySignature').first() 4317 self.assertEqual(eKS.beat, 4.0) 4318 self.assertEqual(eKS.beatDuration.quarterLength, 1.0) 4319 self.assertEqual(eKS.beatStrength, 0.25) 4320 4321 # ts can get beatStrength, beatDuration 4322 eTS = p1.flatten().getElementsByClass('TimeSignature').first() 4323 self.assertEqual(eTS.beatDuration.quarterLength, 1.0) 4324 self.assertEqual(eTS.beatStrength, 0.25) 4325 4326 # compare offsets found with items positioned in Measures 4327 # as the first bar is a pickup, the measure offset here is returned 4328 # with padding (resulting in 3) 4329 post = [] 4330 for n in p1.flatten().notesAndRests: 4331 post.append(n._getMeasureOffset()) 4332 self.assertEqual(post, [3.0, 3.5, 0.0, 1.0, 2.0, 3.0, 0.0, 4333 1.0, 2.0, 3.0, 0.0, 0.5, 1.0, 2.0, 4334 3.0, 0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 4335 2.0, 3.0, 0.0, 1.0, 2.0, 3.0, 0.0, 4336 1.0, 2.0, 0.0, 2.0, 3.0, 0.0, 1.0, 1.5, 2.0]) 4337 4338 # compare derived beat string 4339 post = [] 4340 for n in p1.flatten().notesAndRests: 4341 post.append(n.beatStr) 4342 self.assertEqual(post, ['4', '4 1/2', '1', '2', '3', '4', '1', 4343 '2', '3', '4', '1', '1 1/2', '2', '3', 4344 '4', '1', '2', '3', '4', '1', '2', '3', 4345 '4', '1', '2', '3', '4', '1', '2', '3', 4346 '1', '3', '4', '1', '2', '2 1/2', '3']) 4347 4348 # for stream and Stream subclass, overridden methods not yet 4349 # specialized 4350 # _getMeasureOffset gets the offset within the activeSite 4351 # this shows that measure offsets are accommodating pickup 4352 post = [] 4353 for m in p1.getElementsByClass('Measure'): 4354 post.append(m._getMeasureOffset()) 4355 self.assertEqual(post, [0.0, 1.0, 5.0, 9.0, 13.0, 17.0, 21.0, 25.0, 29.0, 33.0]) 4356 4357 # all other methods define None 4358 post = [] 4359 for n in p1.getElementsByClass('Measure'): 4360 post.append(n.beat) 4361 self.assertEqual(post, [None, None, None, None, None, None, None, None, None, None]) 4362 4363 post = [] 4364 for n in p1.getElementsByClass('Measure'): 4365 post.append(n.beatStr) 4366 self.assertEqual(post, [None, None, None, None, None, None, None, None, None, None]) 4367 4368 post = [] 4369 for n in p1.getElementsByClass('Measure'): 4370 post.append(n.beatDuration) 4371 self.assertEqual(post, [None, None, None, None, None, None, None, None, None, None]) 4372 4373 def testGetBeatStrengthA(self): 4374 from music21 import stream 4375 from music21 import note 4376 from music21 import meter 4377 4378 n = note.Note('g') 4379 n.quarterLength = 1 4380 s = stream.Stream() 4381 s.insert(0, meter.TimeSignature('4/4')) 4382 s.repeatAppend(n, 8) 4383 # match = [] 4384 self.assertEqual([e.beatStrength for e in s.notes], 4385 [1.0, 0.25, 0.5, 0.25, 1.0, 0.25, 0.5, 0.25]) 4386 4387 n = note.Note('E--3', type='quarter') 4388 s = stream.Stream() 4389 s.insert(0.0, meter.TimeSignature('2/2')) 4390 s.repeatAppend(n, 12) 4391 match = [s.notes[i].beatStrength for i in range(12)] 4392 self.assertEqual([1.0, 0.25, 0.5, 0.25, 1.0, 0.25, 0.5, 0.25, 1.0, 0.25, 0.5, 0.25], match) 4393 4394 def testMeasureNumberAccess(self): 4395 '''Test getting measure number data from various Music21Objects. 4396 ''' 4397 from music21 import corpus 4398 from music21 import stream 4399 from music21 import note 4400 4401 s = corpus.parse('bach/bwv66.6.xml') 4402 p1 = s.parts['Soprano'] 4403 for classStr in ['Clef', 'KeySignature', 'TimeSignature']: 4404 self.assertEqual(p1.flatten().getElementsByClass( 4405 classStr)[0].measureNumber, 0) 4406 4407 match = [] 4408 for n in p1.flatten().notesAndRests: 4409 match.append(n.measureNumber) 4410 self.assertEqual(match, [0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4411 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 4412 8, 8, 8, 9, 9, 9, 9]) 4413 4414 # create a note and put it in different measures 4415 m1 = stream.Measure() 4416 m1.number = 3 4417 m2 = stream.Measure() 4418 m2.number = 74 4419 n = note.Note() 4420 self.assertEqual(n.measureNumber, None) # not in a Measure 4421 m1.append(n) 4422 self.assertEqual(n.measureNumber, 3) 4423 m2.append(n) 4424 self.assertEqual(n.measureNumber, 74) 4425 4426 def testPickupMeasuresBuilt(self): 4427 from music21 import stream 4428 from music21 import meter 4429 from music21 import note 4430 4431 s = stream.Score() 4432 4433 m1 = stream.Measure() 4434 m1.timeSignature = meter.TimeSignature('4/4') 4435 n1 = note.Note('d2') 4436 n1.quarterLength = 1.0 4437 m1.append(n1) 4438 # barDuration is based only on TS 4439 self.assertEqual(m1.barDuration.quarterLength, 4.0) 4440 # duration shows the highest offset in the bar 4441 self.assertEqual(m1.duration.quarterLength, 1.0) 4442 # presently, the offset of the added note is zero 4443 self.assertEqual(n1.getOffsetBySite(m1), 0.0) 4444 # the _getMeasureOffset method is called by all methods that evaluate 4445 # beat position; this takes padding into account 4446 self.assertEqual(n1._getMeasureOffset(), 0.0) 4447 self.assertEqual(n1.beat, 1.0) 4448 4449 # the Measure.padAsAnacrusis() method looks at the barDuration and, 4450 # if the Measure is incomplete, assumes its an anacrusis and adds 4451 # the appropriate padding 4452 m1.padAsAnacrusis() 4453 # app values are the same except _getMeasureOffset() 4454 self.assertEqual(m1.barDuration.quarterLength, 4.0) 4455 self.assertEqual(m1.duration.quarterLength, 1.0) 4456 self.assertEqual(n1.getOffsetBySite(m1), 0.0) 4457 # lowest offset inside of Measure still returns 0 4458 self.assertEqual(m1.lowestOffset, 0.0) 4459 # these values are now different 4460 self.assertEqual(n1._getMeasureOffset(), 3.0) 4461 self.assertEqual(n1.beat, 4.0) 4462 4463 # appending this measure to the Score 4464 s.append(m1) 4465 # score duration is correct: 1 4466 self.assertEqual(s.duration.quarterLength, 1.0) 4467 # lowest offset is that of the first bar 4468 self.assertEqual(s.lowestOffset, 0.0) 4469 self.assertEqual(s.highestTime, 1.0) 4470 4471 m2 = stream.Measure() 4472 n2 = note.Note('e2') 4473 n2.quarterLength = 4.0 4474 m2.append(n2) 4475 # based on contents 4476 self.assertEqual(m2.duration.quarterLength, 4.0) 4477 # we cannot get a bar duration b/c we have not associated a ts 4478 try: 4479 m2.barDuration.quarterLength 4480 except exceptions21.StreamException: 4481 pass 4482 4483 # append to Score 4484 s.append(m2) 4485 # m2 can now find a time signature by looking to activeSite stream 4486 self.assertEqual(m2.duration.quarterLength, 4.0) 4487 # highest time of score takes into account new measure 4488 self.assertEqual(s.highestTime, 5.0) 4489 # offset are contiguous when accessed in a flat form 4490 self.assertEqual([n.offset for n in s.flatten().notesAndRests], [0.0, 1.0]) 4491 4492 m3 = stream.Measure() 4493 n3 = note.Note('f#2') 4494 n3.quarterLength = 3.0 4495 m3.append(n3) 4496 4497 # add to stream 4498 s.append(m3) 4499 # m3 can now find a time signature by looking to activeSite stream 4500 self.assertEqual(m2.duration.quarterLength, 4.0) 4501 # highest time of score takes into account new measure 4502 self.assertEqual(s.highestTime, 8.0) 4503 # offset are contiguous when accessed in a flat form 4504 self.assertEqual([n.offset for n in s.flatten().notesAndRests], [0.0, 1.0, 5.0]) 4505 4506 def testPickupMeasuresImported(self): 4507 from music21 import corpus 4508 self.maxDiff = None 4509 s = corpus.parse('bach/bwv103.6') 4510 4511 p = s.parts['soprano'] 4512 m1 = p.getElementsByClass('Measure').first() 4513 4514 self.assertEqual([n.offset for n in m1.notesAndRests], [0.0, 0.5]) 4515 self.assertEqual(m1.paddingLeft, 3.0) 4516 4517 offsets = [n.offset for n in p.flatten().notesAndRests] 4518 # offsets for flat representation have proper spacing 4519 self.assertEqual(offsets, 4520 [0.0, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 4521 9.0, 10.0, 11.0, 12.0, 12.5, 13.0, 15.0, 16.0, 17.0, 4522 18.0, 18.5, 18.75, 19.0, 19.5, 20.0, 21.0, 22.0, 23.0, 24.0, 4523 25.0, 26.0, 27.0, 28.0, 29.0, 31.0, 32.0, 32.5, 33.0, 34.0, 4524 35.0, 36.0, 37.0, 38.0, 39.0, 40.0, 41.0, 42.0, 4525 43.0, 44.0, 44.5, 45.0, 47.0]) 4526 4527 def testHighestTime(self): 4528 from music21 import stream 4529 from music21 import note 4530 from music21 import bar 4531 s = stream.Stream() 4532 n1 = note.Note() 4533 n1.quarterLength = 30 4534 n2 = note.Note() 4535 n2.quarterLength = 20 4536 4537 b1 = bar.Barline() 4538 s.append(n1) 4539 self.assertEqual(s.highestTime, 30.0) 4540 s.coreSetElementOffset(b1, OffsetSpecial.AT_END, addElement=True) 4541 4542 self.assertEqual(b1.getOffsetBySite(s), 30.0) 4543 4544 s.append(n2) 4545 self.assertEqual(s.highestTime, 50.0) 4546 self.assertEqual(b1.getOffsetBySite(s), 50.0) 4547 4548 def testRecurseByClass(self): 4549 from music21 import note 4550 from music21 import stream 4551 from music21 import clef 4552 s1 = stream.Stream() 4553 s2 = stream.Stream() 4554 s3 = stream.Stream() 4555 4556 n1 = note.Note('C') 4557 n2 = note.Note('D') 4558 n3 = note.Note('E') 4559 c1 = clef.TrebleClef() 4560 c2 = clef.BassClef() 4561 c3 = clef.PercussionClef() 4562 4563 s1.append(c1) 4564 s1.append(n1) 4565 4566 s2.append(c2) 4567 s2.append(n2) 4568 4569 s3.append(c3) 4570 s3.append(n3) 4571 4572 # only get n1 here, as that is only level available 4573 self.assertEqual(s1.recurse().getElementsByClass('Note').first(), n1) 4574 self.assertEqual(s2.recurse().getElementsByClass('Note').first(), n2) 4575 self.assertEqual(s1.recurse().getElementsByClass('Clef').first(), c1) 4576 self.assertEqual(s2.recurse().getElementsByClass('Clef').first(), c2) 4577 4578 # attach s2 to s1 4579 s2.append(s1) 4580 # stream 1 gets both notes 4581 self.assertEqual(list(s2.recurse().getElementsByClass('Note')), [n2, n1]) 4582 4583 def testSetEditorial(self): 4584 b2 = Music21Object() 4585 self.assertIsNone(b2._editorial) 4586 b2_ed = b2.editorial 4587 self.assertIsInstance(b2_ed, editorial.Editorial) 4588 self.assertIsNotNone(b2._editorial) 4589 4590 def testStoreLastDeepCopyOf(self): 4591 from music21 import note 4592 4593 n1 = note.Note() 4594 n2 = copy.deepcopy(n1) 4595 self.assertEqual(id(n2.derivation.origin), id(n1)) 4596 4597 def testHasElement(self): 4598 from music21 import note 4599 from music21 import stream 4600 n1 = note.Note() 4601 s1 = stream.Stream() 4602 s1.append(n1) 4603 s2 = copy.deepcopy(s1) 4604 n2 = s2[0] # this is a new instance; not the same as n1 4605 self.assertFalse(s2.hasElement(n1)) 4606 self.assertTrue(s2.hasElement(n2)) 4607 4608 self.assertFalse(s1 in n2.sites) 4609 self.assertTrue(s2 in n2.sites) 4610 4611 def testGetContextByClassA(self): 4612 from music21 import stream 4613 from music21 import note 4614 from music21 import tempo 4615 4616 p = stream.Part() 4617 m1 = stream.Measure() 4618 m1.repeatAppend(note.Note(quarterLength=1), 4) 4619 m2 = copy.deepcopy(m1) 4620 mm1 = tempo.MetronomeMark(number=50, referent=0.25) 4621 m1.insert(0, mm1) 4622 mm2 = tempo.MetronomeMark(number=150, referent=0.5) 4623 m2.insert(0, mm2) 4624 p.append([m1, m2]) 4625 # p.show('t') 4626 # if done with default args, we get the same object, as we are using 4627 # getElementAtOrBefore 4628 self.assertEqual(str(mm2.getContextByClass('MetronomeMark')), 4629 '<music21.tempo.MetronomeMark Eighth=150>') 4630 # if we provide the getElementMethod parameter, we can use 4631 # getElementBeforeOffset 4632 self.assertEqual(str(mm2.getContextByClass('MetronomeMark', 4633 getElementMethod='getElementBeforeOffset')), 4634 '<music21.tempo.MetronomeMark lento 16th=50>') 4635 4636 def testElementWrapperOffsetAccess(self): 4637 from music21 import stream 4638 from music21 import meter 4639 from music21 import base 4640 4641 class Mock: 4642 pass 4643 4644 s = stream.Stream() 4645 s.append(meter.TimeSignature('fast 6/8')) 4646 storage = [] 4647 for i in range(2): 4648 mock = Mock() 4649 el = base.ElementWrapper(mock) 4650 storage.append(el) 4651 s.insert(i, el) 4652 4653 for ew in storage: 4654 self.assertTrue(s.hasElement(ew)) 4655 4656 match = [e.getOffsetBySite(s) for e in storage] 4657 self.assertEqual(match, [0.0, 1.0]) 4658 4659 self.assertEqual(s.elementOffset(storage[0]), 0.0) 4660 self.assertEqual(s.elementOffset(storage[1]), 1.0) 4661 4662 def testGetActiveSiteTimeSignature(self): 4663 from music21 import base 4664 from music21 import stream 4665 from music21 import meter 4666 4667 class Wave_read: 4668 def getnchannels(self): 4669 return 2 4670 4671 s = stream.Stream() 4672 s.append(meter.TimeSignature('fast 6/8')) 4673 # s.show('t') 4674 storage = [] 4675 for i in range(6): 4676 soundFile = Wave_read() # _DOCS_HIDE 4677 # el = music21.Music21Object() 4678 el = base.ElementWrapper(soundFile) 4679 storage.append(el) 4680 self.assertEqual(el.obj, soundFile) 4681 s.insert(i, el) 4682 4683 for ew in storage: 4684 self.assertTrue(s.hasElement(ew)) 4685 4686 matchOffset = [] 4687 matchBeatStrength = [] 4688 matchAudioChannels = [] 4689 4690 for j in s.getElementsByClass('ElementWrapper'): 4691 matchOffset.append(j.offset) 4692 matchBeatStrength.append(j.beatStrength) 4693 matchAudioChannels.append(j.getnchannels()) 4694 self.assertEqual(matchOffset, [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) 4695 self.assertEqual(matchBeatStrength, [1.0, 0.25, 0.25, 1.0, 0.25, 0.25]) 4696 self.assertEqual(matchAudioChannels, [2, 2, 2, 2, 2, 2]) 4697 4698 def testGetMeasureOffsetOrMeterModulusOffsetA(self): 4699 # test getting metric position in a Stream with a TS 4700 from music21 import stream 4701 from music21 import note 4702 from music21 import meter 4703 4704 s = stream.Stream() 4705 s.repeatAppend(note.Note(), 12) 4706 s.insert(0, meter.TimeSignature('3/4')) 4707 4708 match = [n.beat for n in s.notes] 4709 self.assertEqual(match, [1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0]) 4710 4711 match = [n.beatStr for n in s.notes] 4712 self.assertEqual(match, ['1', '2', '3', '1', '2', '3', '1', '2', '3', '1', '2', '3']) 4713 4714 match = [n.beatDuration.quarterLength for n in s.notes] 4715 self.assertEqual(match, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) 4716 4717 match = [n.beatStrength for n in s.notes] 4718 self.assertEqual(match, [1.0, 0.5, 0.5, 1.0, 0.5, 0.5, 1.0, 0.5, 0.5, 1.0, 0.5, 0.5]) 4719 4720 def testGetMeasureOffsetOrMeterModulusOffsetB(self): 4721 from music21 import stream 4722 from music21 import note 4723 from music21 import meter 4724 4725 s = stream.Stream() 4726 s.repeatAppend(note.Note(), 12) 4727 s.insert(0.0, meter.TimeSignature('3/4')) 4728 s.insert(3.0, meter.TimeSignature('4/4')) 4729 s.insert(7.0, meter.TimeSignature('2/4')) 4730 4731 match = [n.beat for n in s.notes] 4732 self.assertEqual(match, [1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 1.0, 2.0, 1.0]) 4733 4734 match = [n.beatStr for n in s.notes] 4735 self.assertEqual(match, ['1', '2', '3', '1', '2', '3', '4', '1', '2', '1', '2', '1']) 4736 4737 match = [n.beatStrength for n in s.notes] 4738 self.assertEqual(match, [1.0, 0.5, 0.5, 1.0, 0.25, 0.5, 0.25, 1.0, 0.5, 1.0, 0.5, 1.0]) 4739 4740 def testSecondsPropertyA(self): 4741 from music21 import stream 4742 from music21 import note 4743 from music21 import tempo 4744 s = stream.Stream() 4745 s.repeatAppend(note.Note(), 12) 4746 s.insert(0, tempo.MetronomeMark(number=120)) 4747 4748 self.assertEqual([n.seconds for n in s.notes], 4749 [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]) 4750 4751 # changing tempo mid-stream 4752 s.insert(6, tempo.MetronomeMark(number=240)) 4753 self.assertEqual([n.seconds for n in s.notes], 4754 [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]) 4755 4756 # adding notes based on seconds 4757 s2 = stream.Stream() 4758 s2.insert(0, tempo.MetronomeMark(number=120)) 4759 s2.append(note.Note()) 4760 s2.notes.first().seconds = 2.0 4761 self.assertEqual(s2.notes.first().quarterLength, 4.0) 4762 self.assertEqual(s2.duration.quarterLength, 4.0) 4763 4764 s2.append(note.Note('C4', type='half')) 4765 s2.notes[1].seconds = 0.5 4766 self.assertEqual(s2.notes[1].quarterLength, 1.0) 4767 self.assertEqual(s2.duration.quarterLength, 5.0) 4768 4769 s2.append(tempo.MetronomeMark(number=30)) 4770 s2.append(note.Note()) 4771 s2.notes[2].seconds = 0.5 4772 self.assertEqual(s2.notes[2].quarterLength, 0.25) 4773 self.assertEqual(s2.duration.quarterLength, 5.25) 4774 4775 def testGetContextByClass2015(self): 4776 from music21 import converter 4777 p = converter.parse('tinynotation: 3/4 C4 D E 2/4 F G A B 1/4 c') 4778 b = p.measure(3).notes[-1] 4779 c = b.getContextByClass('Note', getElementMethod='getElementAfterOffset') 4780 self.assertEqual(c.name, 'C') 4781 4782 m = p.measure(1) 4783 self.assertIsNotNone(m.getContextByClass('Clef', getElementMethod='all')) 4784 4785 def testGetContextByClassB(self): 4786 from music21 import stream 4787 from music21 import note 4788 from music21 import meter 4789 4790 s = stream.Score() 4791 4792 p1 = stream.Part() 4793 m1 = stream.Measure() 4794 m1.repeatAppend(note.Note(), 3) 4795 m1.timeSignature = meter.TimeSignature('3/4') 4796 m2 = stream.Measure() 4797 m2.repeatAppend(note.Note(), 3) 4798 p1.append(m1) 4799 p1.append(m2) 4800 4801 p2 = stream.Part() 4802 m3 = stream.Measure() 4803 m3.timeSignature = meter.TimeSignature('3/4') 4804 m3.repeatAppend(note.Note(), 3) 4805 m4 = stream.Measure() 4806 m4.repeatAppend(note.Note(), 3) 4807 p2.append(m3) 4808 p2.append(m4) 4809 4810 s.insert(0, p1) 4811 s.insert(0, p2) 4812 4813 p3 = stream.Part() 4814 m5 = stream.Measure() 4815 m5.timeSignature = meter.TimeSignature('3/4') 4816 m5.repeatAppend(note.Note(), 3) 4817 m6 = stream.Measure() 4818 m6.repeatAppend(note.Note(), 3) 4819 p3.append(m5) 4820 p3.append(m6) 4821 4822 p4 = stream.Part() 4823 m7 = stream.Measure() 4824 m7.timeSignature = meter.TimeSignature('3/4') 4825 m7.repeatAppend(note.Note(), 3) 4826 m8 = stream.Measure() 4827 m8.repeatAppend(note.Note(), 3) 4828 p4.append(m7) 4829 p4.append(m8) 4830 4831 s.insert(0, p3) 4832 s.insert(0, p4) 4833 4834 # self.targetMeasures = m4 4835 # n1 = m2[-1] # last element is a note 4836 n2 = m4[-1] # last element is a note 4837 4838 # environLocal.printDebug(['getContextByClass()']) 4839 # self.assertEqual(str(n1.getContextByClass('TimeSignature')), 4840 # '<music21.meter.TimeSignature 3/4>') 4841 environLocal.printDebug(['getContextByClass()']) 4842 self.assertEqual(str(n2.getContextByClass('TimeSignature')), 4843 '<music21.meter.TimeSignature 3/4>') 4844 4845 def testNextA(self): 4846 from music21 import stream 4847 from music21 import scale 4848 from music21 import note 4849 s = stream.Stream() 4850 sc = scale.MajorScale() 4851 notes = [] 4852 for i in sc.pitches: 4853 n = note.Note() 4854 s.append(n) 4855 notes.append(n) # keep for reference and testing 4856 4857 self.assertEqual(notes[0], s[0]) # leave as get index query. 4858 s0Next = s[0].next() 4859 self.assertEqual(notes[1], s0Next) 4860 self.assertEqual(notes[0], s[1].previous()) 4861 4862 self.assertEqual(id(notes[5]), id(s[4].next())) 4863 self.assertEqual(id(notes[3]), id(s[4].previous())) 4864 4865 # if a note has more than one site, what happens 4866 self.assertEqual(notes[6], s.notes[5].next()) 4867 self.assertEqual(notes[7], s.notes[6].next()) 4868 4869 def testNextB(self): 4870 from music21 import stream 4871 from music21 import note 4872 4873 m1 = stream.Measure() 4874 m1.number = 1 4875 n1 = note.Note('C') 4876 m1.append(n1) 4877 4878 m2 = stream.Measure() 4879 m2.number = 2 4880 n2 = note.Note('D') 4881 m2.append(n2) 4882 4883 # n1 cannot be connected to n2 as no common site 4884 n1next = n1.next() 4885 self.assertEqual(n1next, None) 4886 4887 p1 = stream.Part() 4888 p1.append(m1) 4889 p1.append(m2) 4890 n1next = n1.next() 4891 self.assertEqual(n1next, m2) 4892 self.assertEqual(n1.next('Note'), n2) 4893 4894 def testNextC(self): 4895 from music21 import corpus 4896 s = corpus.parse('bwv66.6') 4897 4898 # getting time signature and key sig 4899 p1 = s.parts[0] 4900 nLast = p1.flatten().notes[-1] 4901 self.assertEqual(str(nLast.previous('TimeSignature')), 4902 '<music21.meter.TimeSignature 4/4>') 4903 self.assertEqual(str(nLast.previous('KeySignature')), 4904 'f# minor') 4905 4906 # iterating at the Measure level, showing usage of flattenLocalSites 4907 measures = p1.getElementsByClass('Measure').stream() 4908 m3 = measures[3] 4909 m3prev = m3.previous() 4910 self.assertEqual(m3prev, measures[2][-1]) 4911 m3Prev2 = measures[3].previous().previous() 4912 self.assertEqual(m3Prev2, measures[2][-2]) 4913 self.assertTrue(m3Prev2.expressions) # fermata 4914 4915 m3n = measures[3].next() 4916 self.assertEqual(m3n, measures[3][0]) # same as next line 4917 self.assertEqual(measures[3].next('Note'), measures[3].notes[0]) 4918 self.assertEqual(m3n.next(), measures[3][1]) 4919 4920 m3nm = m3.next('Measure') 4921 m3nn = m3nm.next('Measure') 4922 self.assertEqual(m3nn, measures[5]) 4923 m3nnn = m3nn.next('Measure') 4924 self.assertEqual(m3nnn, measures[6]) 4925 4926 self.assertEqual(measures[3].previous('Measure').previous('Measure'), measures[1]) 4927 m0viaPrev = measures[3].previous('Measure').previous('Measure').previous('Measure') 4928 self.assertEqual(m0viaPrev, measures[0]) 4929 4930 m0viaPrev.activeSite = s.parts[0] # otherwise there are no instruments... 4931 sopranoInst = m0viaPrev.previous() 4932 self.assertEqual(str(sopranoInst), 'P1: Soprano: Instrument 1') 4933 4934 # set active site back to measure stream... 4935 self.assertEqual(str(measures[0].previous()), str(p1)) 4936 4937 def testActiveSiteCopyingA(self): 4938 from music21 import note 4939 from music21 import stream 4940 4941 n1 = note.Note() 4942 s1 = stream.Stream() 4943 s1.append(n1) 4944 self.assertEqual(n1.activeSite, s1) 4945 4946 n2 = copy.deepcopy(n1) 4947 self.assertIs(n2._activeSite, None) 4948 self.assertIs(n2.derivation.origin.activeSite, s1) 4949 4950 def testSpannerSites(self): 4951 from music21 import note 4952 from music21 import spanner 4953 from music21 import dynamics 4954 4955 n1 = note.Note('C4') 4956 n2 = note.Note('D4') 4957 sp1 = spanner.Slur(n1, n2) 4958 ss = n1.getSpannerSites() 4959 self.assertEqual(ss, [sp1]) 4960 4961 # test same for inherited classes and multiple sites, in order... 4962 sp2 = dynamics.Crescendo(n2, n1) 4963 # can return in arbitrary order esp. if speed is fast... 4964 # TODO: use Ordered Dict. 4965 self.assertEqual(set(n2.getSpannerSites()), {sp1, sp2}) 4966 4967 # Optionally a class name or list of class names can be 4968 # specified and only Spanners of that class will be returned 4969 4970 sp3 = dynamics.Diminuendo(n1, n2) 4971 self.assertEqual(n2.getSpannerSites('Diminuendo'), [sp3]) 4972 4973 # A larger class name can be used to get all subclasses: 4974 4975 self.assertEqual(set(n2.getSpannerSites('DynamicWedge')), {sp2, sp3}) 4976 self.assertEqual(set(n2.getSpannerSites(['Slur', 'Diminuendo'])), {sp1, sp3}) 4977 4978 # The order spanners are returned is generally the order that they were 4979 # added, but that is not guaranteed, so for safety sake, use set comparisons: 4980 4981 self.assertEqual(set(n2.getSpannerSites(['Slur', 'Diminuendo'])), {sp3, sp1}) 4982 4983 def testContextSitesA(self): 4984 from music21 import corpus 4985 self.maxDiff = None 4986 c = corpus.parse('bwv66.6') 4987 c.id = 'bach' 4988 n = c[2][4][2] 4989 self.assertEqual(repr(n), '<music21.note.Note G#>') 4990 siteList = [] 4991 for y in n.contextSites(): 4992 yTup = (y.site, y.offset, y.recurseType) 4993 siteList.append(repr(yTup)) 4994 self.assertEqual(siteList, 4995 ["(<music21.stream.Measure 3 offset=9.0>, 0.5, 'elementsFirst')", 4996 "(<music21.stream.Part Alto>, 9.5, 'flatten')", 4997 "(<music21.stream.Score bach>, 9.5, 'elementsOnly')"]) 4998 4999 m = c[2][4] 5000 self.assertEqual(repr(m), '<music21.stream.Measure 3 offset=9.0>') 5001 5002 siteList = [] 5003 for y in m.contextSites(): 5004 yTup = (y.site, y.offset, y.recurseType) 5005 siteList.append(repr(yTup)) 5006 self.assertEqual(siteList, 5007 ["(<music21.stream.Measure 3 offset=9.0>, 0.0, 'elementsFirst')", 5008 "(<music21.stream.Part Alto>, 9.0, 'flatten')", 5009 "(<music21.stream.Score bach>, 9.0, 'elementsOnly')"]) 5010 5011 m2 = copy.deepcopy(m) 5012 m2.number = 3333 5013 siteList = [] 5014 # environLocal.warn('#########################') 5015 for y in m2.contextSites(): 5016 yTup = (y.site, y.offset, y.recurseType) 5017 siteList.append(repr(yTup)) 5018 self.assertEqual(siteList, 5019 ["(<music21.stream.Measure 3333 offset=0.0>, 0.0, 'elementsFirst')", 5020 "(<music21.stream.Part Alto>, 9.0, 'flatten')", 5021 "(<music21.stream.Score bach>, 9.0, 'elementsOnly')"]) 5022 siteList = [] 5023 5024 cParts = c.parts.stream() # need this otherwise it could possibly be garbage collected. 5025 cParts.id = 'partStream' # to make it easier to see below, will be cached... 5026 pTemp = cParts[1] 5027 m3 = pTemp.measure(3) 5028 self.assertIs(m, m3) 5029 for y in m3.contextSites(): 5030 yTup = (y.site, y.offset, y.recurseType) 5031 siteList.append(repr(yTup)) 5032 5033 self.assertEqual(siteList, 5034 ["(<music21.stream.Measure 3 offset=9.0>, 0.0, 'elementsFirst')", 5035 "(<music21.stream.Part Alto>, 9.0, 'flatten')", 5036 "(<music21.stream.Score partStream>, 9.0, 'elementsOnly')", 5037 "(<music21.stream.Score bach>, 9.0, 'elementsOnly')"]) 5038 5039 def testContextSitesB(self): 5040 from music21 import stream 5041 from music21 import note 5042 p1 = stream.Part() 5043 p1.id = 'p1' 5044 m1 = stream.Measure() 5045 m1.number = 1 5046 n = note.Note() 5047 m1.append(n) 5048 p1.append(m1) 5049 siteList = [] 5050 for y in n.contextSites(): 5051 siteList.append(repr(y.site)) 5052 self.assertEqual(siteList, ['<music21.stream.Measure 1 offset=0.0>', 5053 '<music21.stream.Part p1>']) 5054 p2 = stream.Part() 5055 p2.id = 'p2' 5056 m2 = stream.Measure() 5057 m2.number = 2 5058 m2.append(n) 5059 p2.append(m2) 5060 5061 siteList = [] 5062 for y in n.contextSites(): 5063 siteList.append(repr(y.site)) 5064 self.assertEqual(siteList, ['<music21.stream.Measure 2 offset=0.0>', 5065 '<music21.stream.Part p2>', 5066 '<music21.stream.Measure 1 offset=0.0>', 5067 '<music21.stream.Part p1>']) 5068 5069 siteList = [] 5070 for y in n.contextSites(sortByCreationTime=True): 5071 siteList.append(repr(y.site)) 5072 self.assertEqual(siteList, ['<music21.stream.Measure 2 offset=0.0>', 5073 '<music21.stream.Part p2>', 5074 '<music21.stream.Measure 1 offset=0.0>', 5075 '<music21.stream.Part p1>']) 5076 5077 siteList = [] 5078 for y in n.contextSites(sortByCreationTime='reverse'): 5079 siteList.append(repr(y.site)) 5080 self.assertEqual(siteList, ['<music21.stream.Measure 1 offset=0.0>', 5081 '<music21.stream.Part p1>', 5082 '<music21.stream.Measure 2 offset=0.0>', 5083 '<music21.stream.Part p2>']) 5084 5085# great isolation test, but no asserts for now... 5086# def testPreviousA(self): 5087# from music21 import corpus 5088# s = corpus.parse('bwv66.6') 5089# o = s.parts[0].getElementsByClass('Measure')[2][1] 5090# i = 20 5091# while o and i: 5092# print(o) 5093# if isinstance(o, stream.Part): 5094# pass 5095# o = o.previous() 5096# i -= 1 5097# 5098 5099# def testPreviousB(self): 5100# ''' 5101# fixed a memo problem which could cause .previous() to run forever 5102# on flat/derived streams. 5103# ''' 5104# from music21 import corpus 5105# s = corpus.parse('luca/gloria') 5106# sf = s.flatten() 5107# o = sf[1] 5108# # o = s[2] 5109# i = 200 5110# while o and i: 5111# print(o, o.activeSite, o.sortTuple().shortRepr()) 5112# o = o.previous() 5113# # cc = s._cache 5114# # for x in cc: 5115# # if x.startswith('elementTree'): 5116# # print(repr(cc[x])) 5117# i -= 1 5118 5119 def testPreviousAfterDeepcopy(self): 5120 from music21 import stream 5121 from music21 import note 5122 e1 = note.Note('C') 5123 e2 = note.Note('D') 5124 s = stream.Stream() 5125 s.insert(0, e1) 5126 s.insert(1, e2) 5127 self.assertIs(e2.previous(), e1) 5128 self.assertIs(s[1].previous(), e1) 5129 t = copy.deepcopy(s) 5130 self.assertIs(t[1].previous(), t[0]) 5131 5132 e1 = note.Note('C') 5133 e2 = note.Note('D') 5134 5135 v = stream.Part() 5136 m1 = stream.Measure() 5137 m1.number = 1 5138 m1.insert(0, e1) 5139 v.insert(0, m1) 5140 m2 = stream.Measure() 5141 m2.insert(0, e2) 5142 m2.number = 2 5143 v.append(m2) 5144 self.assertIs(e2.previous('Note'), e1) 5145 self.assertIs(v[1][0], e2) 5146 self.assertIs(v[1][0].previous('Note'), e1) 5147 5148 w = v.transpose('M3') # same as deepcopy, 5149 # but more instructive in debugging since pitches change... 5150 # w = copy.deepcopy(v) 5151 eCopy1 = w[0][0] 5152 self.assertEqual(eCopy1.pitch.name, 'E') 5153 eCopy2 = w[1][0] 5154 self.assertEqual(eCopy2.pitch.name, 'F#') 5155 prev = eCopy2.previous('Note') 5156 self.assertIs(prev, eCopy1) 5157 5158 5159# ------------------------------------------------------------------------------ 5160# define presented order in documentation 5161_DOC_ORDER = [Music21Object, ElementWrapper] 5162 5163del (Any, 5164 Dict, 5165 FrozenSet, 5166 Iterable, 5167 List, 5168 Optional, 5169 Union, 5170 Tuple, 5171 TypeVar) 5172 5173 5174# ----------------------------------------------------------------------------- 5175if __name__ == '__main__': 5176 import music21 5177 music21.mainTest(Test) # , runTest='testPreviousB') 5178