Source code for music21.spanner

# ------------------------------------------------------------------------------
# Name:         spanner.py
# Purpose:      The Spanner base-class and subclasses
#
# Authors:      Christopher Ariza
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2010-2012 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
A spanner is a music21 object that represents a connection usually between
two or more music21 objects that might live in different streams but need
some sort of connection between them.  A slur is one type of spanner -- it might
connect notes in different Measure objects or even between different parts.

This package defines some of the most common spanners.  Other spanners
can be found in modules such as :ref:`moduleDynamics` (for things such as crescendos).
'''
from __future__ import annotations

from collections.abc import Sequence, Iterable
import copy
from dataclasses import dataclass
import typing as t
import unittest

from music21 import base
from music21 import common
from music21.common.numberTools import opFrac
from music21.common.types import OffsetQL
from music21 import defaults
from music21 import environment
from music21 import exceptions21
from music21 import prebase
from music21 import sites
from music21 import style
if t.TYPE_CHECKING:
    from music21 import stream

environLocal = environment.Environment('spanner')


# ------------------------------------------------------------------------------
class SpannerException(exceptions21.Music21Exception):
    pass


class SpannerBundleException(exceptions21.Music21Exception):
    pass


# ------------------------------------------------------------------------------
[docs] class Spanner(base.Music21Object): # suppress this inspection because it fails when class is defined in # the __doc__ # noinspection PyTypeChecker ''' Spanner objects live on Streams in the same manner as other Music21Objects, but represent and store connections between one or more other Music21Objects. Commonly used Spanner subclasses include the :class:`~music21.spanner.Slur`, :class:`~music21.spanner.RepeatBracket`, :class:`~music21.spanner.Crescendo`, and :class:`~music21.spanner.Diminuendo` objects. In some cases you will want to subclass Spanner for specific purposes. In the first demo, we create a spanner to represent a written-out accelerando, such as Elliott Carter uses in his second string quartet (he marks them with an arrow). >>> class CarterAccelerandoSign(spanner.Spanner): ... pass >>> n1 = note.Note('C4') >>> n2 = note.Note('D4') >>> n3 = note.Note('E4') >>> sp1 = CarterAccelerandoSign(n1, n2, n3) # or as a list: [n1, n2, n3] >>> sp1.getSpannedElements() [<music21.note.Note C>, <music21.note.Note D>, <music21.note.Note E>] We can iterate over a spanner to get the contexts: >>> print(' '.join([repr(n) for n in sp1])) <music21.note.Note C> <music21.note.Note D> <music21.note.Note E> Now we put the notes and the spanner into a Stream object. Note that the convention is to put the spanner at the beginning of the innermost Stream that contains all the Spanners: >>> s = stream.Stream() >>> s.append([n1, n2, n3]) >>> s.insert(0, sp1) Now we can get at the spanner in one of three ways. (1) it is just a normal element in the stream: >>> for e in s: ... print(e) <music21.note.Note C> <...CarterAccelerandoSign <music21.note.Note C><music21.note.Note D><music21.note.Note E>> <music21.note.Note D> <music21.note.Note E> (2) we can get a stream of spanners (equiv. to getElementsByClass(spanner.Spanner)) by calling the .spanner property on the stream. >>> spannerCollection = s.spanners # a stream object >>> for thisSpanner in spannerCollection: ... print(thisSpanner) <...CarterAccelerandoSign <music21.note.Note C><music21.note.Note D><music21.note.Note E>> (3) we can get the spanner by looking at the list getSpannerSites() on any object that has a spanner: >>> n2.getSpannerSites() [<music21...CarterAccelerandoSign <music21.note.Note C><music21.note.Note D><music21.note.Note E>>] In this example we will slur a few notes and then iterate over the stream to see which are slurred: >>> n1 = note.Note('C4') >>> n2 = note.Note('D4') >>> n3 = note.Note('E4') >>> n4 = note.Note('F4') >>> n5 = note.Note('G4') >>> n6 = note.Note('A4') Create a slur over the second and third notes at instantiation: >>> slur1 = spanner.Slur([n2, n3]) Slur the fifth and the sixth notes by adding them to an existing slur: >>> slur2 = spanner.Slur() >>> slur2.addSpannedElements([n5, n6]) Now add them all to a stream: >>> part1 = stream.Part() >>> part1.append([n1, n2, n3, n4, n5, n6]) >>> part1.insert(0, slur1) >>> part1.insert(0, slur2) Say we wanted to know which notes in a piece started a slur, here's how we could do it: >>> for n in part1.notes: ... ss = n.getSpannerSites() ... for thisSpanner in ss: ... if 'Slur' in thisSpanner.classes: ... if thisSpanner.isFirst(n): ... print(n.nameWithOctave) D4 G4 Alternatively, you could iterate over the spanners of part1 and get their first elements: >>> for thisSpanner in part1.spanners: ... firstNote = thisSpanner.getSpannedElements()[0] ... print(firstNote.nameWithOctave) D4 G4 The second method is shorter, but the first is likely to be useful in cases where you are doing other things to each note object along the way. Oh, and of course, slurs do print properly in musicxml: >>> #_DOCS_SHOW part1.show() .. image:: images/slur1_example.* :width: 400 (the Carter example would not print an arrow since that element has no corresponding musicxml representation). *Implementation notes:* The elements that are included in a spanner are stored in a Stream subclass called :class:`~music21.stream.SpannerStorage` found as the `.spannerStorage` attribute. That Stream has an attribute called `client` which links to the original spanner. Thus, `spannerStorage` is smart enough to know where it's stored, but it makes deleting/garbage-collecting a spanner a tricky operation: Ex. Prove that the spannedElement Stream is linked to container via `client`: >>> sp1.spannerStorage.client is sp1 True Spanners have a `.completeStatus` attribute which can be used to find out if all spanned elements have been added yet. It's up to the processing agent to set this, but it could be useful in deciding where to append a spanner. >>> sp1.completeStatus False When we're done adding elements: >>> sp1.completeStatus = True ''' equalityAttributes = ('spannerStorage',) def __init__(self, *spannedElements: base.Music21Object|Sequence[base.Music21Object], **keywords): super().__init__(**keywords) # store a Stream inside of Spanner from music21 import stream # create a stream subclass, spanner storage; pass a reference # to this spanner for getting this spanner from the SpannerStorage # directly # TODO: Move here! along with VariantStorage to variant. self.spannerStorage = stream.SpannerStorage(client=self) # we do not want to auto sort based on offset or class, as # both are meaningless inside this Stream (and only have meaning # in Stream external to this) self.spannerStorage.autoSort = False # add arguments as a list or single item proc: list[base.Music21Object] = [] for spannedElement in spannedElements: if isinstance(spannedElement, base.Music21Object): proc.append(spannedElement) elif spannedElement is not None: proc += spannedElement self.addSpannedElements(proc) # parameters that spanners need in loading and processing # local id is the id for the local area; used by musicxml self.idLocal: str|None = None # after all spannedElements have been gathered, setting complete # will mark that all parts have been gathered. self.completeStatus: bool = False # data for fill: # fillElementTypes is a list of types of object to search for. This # can be set to something different in the __init__ of a particular # type of Spanner. # Set here to the empty list, so that by default, fill() does nothing. self.fillElementTypes: list[t.Type] = [] # After a fill operation, filledStatus will be set to True. # Parsers and other clients can also set this to False or # True to mark whether or not a fill operation is needed # (False means fill is needed, True means fill is not # needed, presumably because the fill was done by hand). # Initialized to 'unknown'. self.filledStatus: bool|t.Literal['unknown'] = 'unknown' def _reprInternal(self): msg = [] for c in self.getSpannedElements(): objRef = c msg.append(repr(objRef)) return ''.join(msg) def _deepcopySubclassable(self, memo=None, *, ignoreAttributes=None): ''' see __deepcopy__ for tests and docs ''' # NOTE: this is a performance critical operation defaultIgnoreSet = {'spannerStorage'} if ignoreAttributes is None: ignoreAttributes = defaultIgnoreSet else: ignoreAttributes = ignoreAttributes | defaultIgnoreSet new = t.cast(Spanner, super()._deepcopySubclassable(memo, ignoreAttributes=ignoreAttributes)) # we are temporarily putting in the PREVIOUS elements, to replace them later # with replaceSpannedElement() new.spannerStorage = type(self.spannerStorage)(client=new) for c in self.spannerStorage._elements: new.spannerStorage.coreAppend(c) # updateIsSorted too? new.spannerStorage.coreElementsChanged(updateIsFlat=False) return new def __deepcopy__(self, memo=None): ''' This produces a new, independent object containing references to the same spannedElements. SpannedElements linked in this Spanner must be manually re-set, likely using the replaceSpannedElement() method. Notice that we put the references to the same object so that later we can replace them; otherwise in a deepcopy of a stream, the notes in the stream will become independent of the notes in the spanner. >>> import copy >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> c1 = clef.AltoClef() >>> sp1 = spanner.Spanner(n1, n2, c1) >>> sp2 = copy.deepcopy(sp1) >>> len(sp2.spannerStorage) 3 >>> sp1 is sp2 False >>> sp2[0] is sp1[0] True >>> sp2[2] is sp1[2] True >>> sp1[0] is n1 True >>> sp2[0] is n1 True ''' return self._deepcopySubclassable(memo) # -------------------------------------------------------------------------- # as spannedElements is private Stream, unwrap/wrap methods need to override # Music21Object to get at these objects # this is the same as with Variants
[docs] def purgeOrphans(self, excludeStorageStreams=True): if self.spannerStorage: # might not be defined in the middle of a deepcopy. self.spannerStorage.purgeOrphans(excludeStorageStreams) base.Music21Object.purgeOrphans(self, excludeStorageStreams)
[docs] def purgeLocations(self, rescanIsDead=False): # must override Music21Object to purge locations from the contained # Stream # base method to perform purge on the Stream if self.spannerStorage: # might not be defined in the middle of a deepcopy. self.spannerStorage.purgeLocations(rescanIsDead=rescanIsDead) base.Music21Object.purgeLocations(self, rescanIsDead=rescanIsDead)
# --------------------------------------------------------------------------
[docs] def __getitem__(self, key): ''' >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> c1 = clef.BassClef() >>> sl = spanner.Spanner(n1, n2, c1) >>> sl[0] == n1 True >>> sl[-1] == c1 True >>> sl[clef.BassClef][0] == c1 True ''' # delegate to Stream subclass return self.spannerStorage.__getitem__(key)
def __iter__(self): return iter(self.spannerStorage) def __len__(self): # Check _elements to avoid StreamIterator overhead. # Safe, because impossible to put spanned elements at end. return len(self.spannerStorage._elements)
[docs] def getSpannedElements(self): ''' Return all the elements of `.spannerStorage` for this Spanner as a list of Music21Objects. >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> sl = spanner.Spanner() >>> sl.addSpannedElements(n1) >>> sl.getSpannedElements() == [n1] True >>> sl.addSpannedElements(n2) >>> sl.getSpannedElements() == [n1, n2] True >>> sl.getSpannedElementIds() == [id(n1), id(n2)] True >>> c1 = clef.TrebleClef() >>> sl.addSpannedElements(c1) >>> sl.getSpannedElements() == [n1, n2, c1] # make sure that not sorting True ''' # Check _elements to avoid StreamIterator overhead. # Safe, because impossible to put spanned elements at end. return list(self.spannerStorage._elements)
[docs] def getSpannedElementsByClass(self, classFilterList): ''' >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> c1 = clef.AltoClef() >>> sl = spanner.Spanner() >>> sl.addSpannedElements([n1, n2, c1]) >>> sl.getSpannedElementsByClass('Note') == [n1, n2] True >>> sl.getSpannedElementsByClass(clef.Clef) == [c1] True ''' # returns an iterator postStream = self.spannerStorage.getElementsByClass(classFilterList) # return raw elements list for speed return list(postStream)
[docs] def getSpannedElementIds(self): ''' Return all id() for all stored objects. Was performance critical, until most uses removed in v7. Used only as a testing tool now. Spanner.__contains__() was optimized in 839c7e5. ''' return [id(n) for n in self.spannerStorage._elements]
[docs] def addSpannedElements( self, spannedElements: Sequence[base.Music21Object]|base.Music21Object, *otherElements: base.Music21Object, ): ''' Associate one or more elements with this Spanner. The order in which elements are added is retained and may or may not be significant to the spanner. >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> n3 = note.Note('e') >>> n4 = note.Note('d-') >>> n5 = note.Note('c') >>> sl = spanner.Spanner() >>> sl.addSpannedElements(n1) >>> sl.addSpannedElements(n2, n3) >>> sl.addSpannedElements([n4, n5]) >>> sl.getSpannedElementIds() == [id(n) for n in [n1, n2, n3, n4, n5]] True ''' # presently, this does not look for redundancies # add mypy disables because isListLike() performs type-narrowing if not common.isListLike(spannedElements): spannedElements = [spannedElements] # type: ignore[list-item] if otherElements: # copy spannedElements = spannedElements[:] # type: ignore[index] # assume all other arguments are music21 objects spannedElements += otherElements # type: ignore[operator] for c in spannedElements: # type: ignore[union-attr] if c is None: continue if not self.hasSpannedElement(c): # not already in storage self.spannerStorage.coreAppend(c) else: pass # it makes sense to not have multiple copies # environLocal.printDebug([ # 'attempting to add an object ' # f'({c}) that is already found in the SpannerStorage stream ' # f'of spanner {self};\n' # 'this may not be an error.' # ]) self.spannerStorage.coreElementsChanged()
[docs] def insertFirstSpannedElement(self, firstEl: base.Music21Object) -> None: ''' Add a single element as the first in the spanner. >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> n3 = note.Note('e') >>> n4 = note.Note('d-') >>> n5 = note.Note('c') >>> sl = spanner.Spanner() >>> sl.addSpannedElements(n2, n3) >>> sl.addSpannedElements([n4, n5]) >>> sl.insertFirstSpannedElement(n1) >>> sl.getSpannedElementIds() == [id(n) for n in [n1, n2, n3, n4, n5]] True If the element is already in the spanner, it will not be added again, nor moved. (to move it to the front, remove it before running) >>> sl.insertFirstSpannedElement(n4) >>> sl.getSpannedElementIds() == [id(n) for n in [n1, n2, n3, n4, n5]] True ''' origNumElements: int = len(self) self.addSpannedElements(firstEl) if origNumElements == 0: # no need to move to first element, it's already there return # now move it from last to first element (if it is not last element, # it was already in the spanner, and this API is a no-op). if self.spannerStorage.elements[-1] is firstEl: self.spannerStorage.elements = ( (firstEl,) + self.spannerStorage.elements[:-1] )
[docs] def hasSpannedElement(self, spannedElement: base.Music21Object) -> bool: ''' Return True if this Spanner has the spannedElement. >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> span = spanner.Spanner() >>> span.addSpannedElements(n1) >>> span.hasSpannedElement(n1) True >>> span.hasSpannedElement(n2) False Note that a simple `in` does the same thing: >>> n1 in span True >>> n2 in span False ''' return spannedElement in self
def __contains__(self, spannedElement): # Cannot check `in` spannerStorage._elements, # because it would check __eq__, not identity. for x in self.spannerStorage._elements: if x is spannedElement: return True return False
[docs] def replaceSpannedElement(self, old, new) -> None: ''' When copying a Spanner, we need to update the spanner with new references for copied (if the Notes of a Slur have been copied, that Slur's Note references need references to the new Notes). Given the old spanned element, this method will replace the old with the new. The `old` parameter can be either an object or object id. >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> c1 = clef.AltoClef() >>> c2 = clef.BassClef() >>> sl = spanner.Spanner(n1, n2, c1) >>> sl.replaceSpannedElement(c1, c2) >>> sl[-1] == c2 True ''' if old is None: return None # do nothing if common.isNum(old): # this must be id(obj), not obj.id e = self.spannerStorage.coreGetElementByMemoryLocation(old) # e here is the old element that was spanned by this Spanner # environLocal.printDebug(['current Spanner.getSpannedElementIdsIds()', # self.getSpannedElementIds()]) # environLocal.printDebug(['Spanner.replaceSpannedElement:', 'getElementById result', # e, 'old target', old]) if e is not None: # environLocal.printDebug(['Spanner.replaceSpannedElement:', 'old', e, 'new', new]) # do not do all Sites: only care about this one self.spannerStorage.replace(e, new, allDerived=False) else: # do not do all Sites: only care about this one self.spannerStorage.replace(old, new, allDerived=False)
# environLocal.printDebug(['Spanner.replaceSpannedElement:', 'old', e, 'new', new]) # while this Spanner now has proper elements in its spannerStorage Stream, # the element replaced likely has a site left-over from its previous Spanner # environLocal.printDebug(['replaceSpannedElement()', 'id(old)', id(old), # 'id(new)', id(new)])
[docs] def fill( self, searchStream=None, # stream.Stream|None, but cannot import stream here *, includeEndBoundary: bool = False, mustFinishInSpan: bool = False, mustBeginInSpan: bool = True, includeElementsThatEndAtStart: bool = False ): ''' Fills in the intermediate elements of a spanner, that are found in searchStream between the first element's offset and the last element's offset+duration. If searchStream is None, the first element's activeSite is used. If the first element's activeSite is None, a SpannerException is raised. Ottava is an example of a Spanner that can be filled. The Ottava does not need to be inserted into the stream in order to be filled. >>> m = stream.Measure([note.Note('A'), note.Note('B'), note.Note('C')]) >>> ott1 = spanner.Ottava(m.notes[0], m.notes[2]) >>> ott1.fill(m) >>> ott1 <music21.spanner.Ottava 8va transposing<...Note A><...Note B><...Note C>> If the searchStream is not passed in, fill still happens in this case, because the first note's activeSite is used instead. >>> ott2 = spanner.Ottava(m.notes[0], m.notes[2]) >>> ott2.fill() >>> ott2 <music21.spanner.Ottava 8va transposing<...Note A><...Note B><...Note C>> If the searchStream is not passed, and the spanner's first element doesn't have an activeSite, a SpannerException is raised. >>> ott3 = spanner.Ottava(note.Note('D'), note.Note('E')) >>> ott3.fill() Traceback (most recent call last): music21.spanner.SpannerException: Spanner.fill() requires a searchStream or getFirst().activeSite ''' if not self.fillElementTypes: # nothing to fill return if self.filledStatus is True: # Don't fill twice. If client wants a refill they can set filledStatus to False. return startElement: base.Music21Object|None = self.getFirst() if startElement is None: # no spanned elements? Nothing to fill. return if searchStream is None: searchStream = startElement.activeSite if searchStream is None: raise SpannerException( 'Spanner.fill() requires a searchStream or getFirst().activeSite' ) searchStream = t.cast('stream.Stream', searchStream) endElement: base.Music21Object|None = self.getLast() if endElement is startElement: endElement = None savedEndElementOffset: OffsetQL | None = None savedEndElementActiveSite: stream.Stream | None = None if endElement is not None: # Start and end elements are different; we can't just append everything, we need # to save the end element, remove it, add everything, then add the end element # again. Note that if there are actually more than 2 elements before we start # filling, the new intermediate elements will come after the existing ones, # regardless of offset. But first and last will still be the same two elements # as before, which is the most important thing. # But doing this (remove/restore) clears endElement.offset and endElement.activeSite. # That's rude; put 'em back when we're done. savedEndElementOffset = endElement.offset savedEndElementActiveSite = endElement.activeSite self.spannerStorage.remove(endElement) try: startOffsetInHierarchy: OffsetQL = startElement.getOffsetInHierarchy(searchStream) except sites.SitesException: # print('start element not in searchStream') if endElement is not None: self.addSpannedElements(endElement) if savedEndElementOffset is not None: endElement.offset = savedEndElementOffset if savedEndElementActiveSite is not None: endElement.activeSite = savedEndElementActiveSite return endOffsetInHierarchy: OffsetQL if endElement is not None: try: endOffsetInHierarchy = opFrac(endElement.getOffsetInHierarchy(searchStream) + endElement.quarterLength) except sites.SitesException: # print('end element not in searchStream') self.addSpannedElements(endElement) if savedEndElementOffset is not None: endElement.offset = savedEndElementOffset if savedEndElementActiveSite is not None: endElement.activeSite = savedEndElementActiveSite return else: endOffsetInHierarchy = opFrac(startOffsetInHierarchy + startElement.quarterLength) matchIterator = (searchStream .recurse() .getElementsByOffsetInHierarchy( startOffsetInHierarchy, endOffsetInHierarchy, includeEndBoundary=includeEndBoundary, mustFinishInSpan=mustFinishInSpan, mustBeginInSpan=mustBeginInSpan, includeElementsThatEndAtStart=includeElementsThatEndAtStart) .getElementsByClass(self.fillElementTypes) ) for foundElement in matchIterator: if foundElement is startElement: # it's already in the spanner, skip it continue if foundElement is endElement: # we'll add it below, skip it continue self.addSpannedElements(foundElement) if endElement is not None: # add it back in as the end element self.addSpannedElements(endElement) if savedEndElementOffset is not None: endElement.offset = savedEndElementOffset if savedEndElementActiveSite is not None: endElement.activeSite = savedEndElementActiveSite self.filledStatus = True
[docs] def isFirst(self, spannedElement): ''' Given a spannedElement, is it first? >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> n3 = note.Note('e') >>> n4 = note.Note('c') >>> n5 = note.Note('d-') >>> sl = spanner.Spanner() >>> sl.addSpannedElements(n1, n2, n3, n4, n5) >>> sl.isFirst(n2) False >>> sl.isFirst(n1) True >>> sl.isLast(n1) False >>> sl.isLast(n5) True ''' return self.getFirst() is spannedElement
[docs] def getFirst(self): ''' Get the object of the first spannedElement (or None if it's an empty spanner). >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> n3 = note.Note('e') >>> n4 = note.Note('c') >>> n5 = note.Note('d-') >>> sl = spanner.Spanner() >>> sl.addSpannedElements(n1, n2, n3, n4, n5) >>> sl.getFirst() is n1 True >>> spanner.Slur().getFirst() is None True ''' try: return self.spannerStorage[0] except (IndexError, exceptions21.StreamException): return None
[docs] def isLast(self, spannedElement): ''' Given a spannedElement, is it last? Returns True or False. ''' return self.getLast() is spannedElement
[docs] def getLast(self): ''' Get the object of the last spannedElement (or None if it's an empty spanner). >>> n1 = note.Note('g') >>> n2 = note.Note('f#') >>> n3 = note.Note('e') >>> n4 = note.Note('c') >>> n5 = note.Note('d-') >>> sl = spanner.Spanner() >>> sl.addSpannedElements(n1, n2, n3, n4, n5) >>> sl.getLast() is n5 True >>> spanner.Slur().getLast() is None True ''' try: return self.spannerStorage[-1] except (IndexError, exceptions21.StreamException): return None
# ------------------------------------------------------------------------------
[docs] @dataclass(frozen=True) class PendingAssignmentRef: ''' A dataclass containing information about a pending first spanned element assignment. See :meth:`SpannerBundle.setPendingSpannedElementAssignment` for documentation and tests. ''' spanner: Spanner className: str offsetInScore: OffsetQL|None = None
[docs] class SpannerAnchor(base.Music21Object): ''' A simple Music21Object that can be used to define the beginning or end of a Spanner, in the place of a GeneralNote. This is useful for (e.g.) a Crescendo that ends partway through a note (e.g. in a violin part). Exporters (like MusicXML) are configured to remove the SpannerAnchor itself on output, exporting only the Spanner start and stop locations. Here's an example of a whole note that has a Crescendo for the first half of the note, and a Diminuendo for the second half of the note. >>> n = note.Note('C4', quarterLength=4) >>> measure = stream.Measure([n], number=1) >>> part = stream.Part([measure], id='violin') >>> score = stream.Score([part]) Add a crescendo from the note's start to the first anchor, place in the middle of the note, and then a diminuendo from that first anchor to the second, placed at the end of the note. >>> anchor1 = spanner.SpannerAnchor() >>> anchor2 = spanner.SpannerAnchor() >>> measure.insert(2.0, anchor1) >>> measure.insert(4.0, anchor2) >>> cresc = dynamics.Crescendo(n, anchor1) >>> dim = dynamics.Diminuendo(anchor1, anchor2) >>> score.append((cresc, dim)) >>> score.show('text') {0.0} <music21.stream.Part violin> {0.0} <music21.stream.Measure 1 offset=0.0> {0.0} <music21.note.Note C> {2.0} <music21.spanner.SpannerAnchor at 2.0> {4.0} <music21.spanner.SpannerAnchor at 4.0> {4.0} <music21.dynamics.Crescendo <music21.note.Note C><...SpannerAnchor at 2.0>> {4.0} <music21.dynamics.Diminuendo <...SpannerAnchor at 2.0><...SpannerAnchor at 4.0>> ''' def __init__(self, **keywords): super().__init__(**keywords) def _reprInternal(self) -> str: offset: OffsetQL = self.offset if self.activeSite is None: # find a site that is either a Measure or a Voice siteList: list = self.sites.getSitesByClass('Measure') if not siteList: siteList = self.sites.getSitesByClass('Voice') if not siteList: return 'unanchored' offset = self.getOffsetInHierarchy(siteList[0]) ql: OffsetQL = self.duration.quarterLength if ql == 0: return f'at {offset}' return f'at {offset}-{offset + ql}'
[docs] class SpannerBundle(prebase.ProtoM21Object): ''' An advanced utility object for collecting and processing collections of Spanner objects. This is necessary because often processing routines that happen at many levels still need access to the same collection of spanners. Because SpannerBundles are so commonly used with :class:`~music21.stream.Stream` objects, the Stream has a :attr:`~music21.stream.Stream.spannerBundle` property that stores and caches a SpannerBundle of the Stream. If a Stream or Stream subclass is provided as an argument, all Spanners on this Stream will be accumulated herein. Not to be confused with SpannerStorage (which is a Stream class inside a spanner that stores Elements that are spanned) * Changed in v7: only argument must be a List of spanners. Creators of SpannerBundles are required to check that this constraint is True ''' # TODO: make SpannerBundle a Generic type def __init__(self, spanners: list[Spanner]|None = None): self._cache: dict[str, t.Any] = {} # cache is defined on Music21Object not ProtoM21Object self._storage: list[Spanner] = [] if spanners: self._storage = spanners[:] # a simple List, not a Stream # special spanners, stored in storage, can be identified in the # SpannerBundle as missing a first spannedElement; the next obj that meets # the class expectation will then be assigned and the spannedElement # cleared self._pendingSpannedElementAssignment: list[PendingAssignmentRef] = []
[docs] def append(self, other: Spanner): ''' Adds a Spanner to the bundle. Will be done automatically when adding a Spanner to a Stream. ''' self._storage.append(other) self._cache.clear()
def __len__(self): return len(self._storage) def __iter__(self): return self._storage.__iter__()
[docs] def __getitem__(self, key) -> Spanner: return self._storage[key]
[docs] def remove(self, item: Spanner): ''' Remove a stored Spanner from the bundle with an instance. Each reference must have a matching id() value. >>> su1 = spanner.Slur() >>> su1.idLocal = 1 >>> su2 = spanner.Slur() >>> su2.idLocal = 2 >>> sb = spanner.SpannerBundle() >>> sb.append(su1) >>> sb.append(su2) >>> len(sb) 2 >>> sb <music21.spanner.SpannerBundle of size 2> >>> sb.remove(su2) >>> len(sb) 1 ''' if item in self._storage: self._storage.remove(item) else: raise SpannerBundleException(f'cannot match object for removal: {item}') self._cache.clear()
def _reprInternal(self): return f'of size {len(self)}'
[docs] def getSpannerStorageIds(self) -> list[int]: ''' Return all SpannerStorage ids from all contained Spanners. ''' post: list[int] = [] for x in self._storage: post.append(id(x.spannerStorage)) return post
[docs] def getByIdLocal(self, idLocal: int|None = None) -> SpannerBundle: ''' Get spanners by `idLocal`. Returns a new SpannerBundle object >>> su = spanner.Slur() >>> su.idLocal = 1 >>> rb = spanner.RepeatBracket() >>> rb.idLocal = 2 >>> sb = spanner.SpannerBundle() >>> sb.append(su) >>> sb.append(rb) >>> len(sb) 2 >>> sb.getByIdLocal(2) <music21.spanner.SpannerBundle of size 1> >>> sb.getByIdLocal(2)[0] <music21.spanner.RepeatBracket > >>> len(sb.getByIdLocal(1)) 1 >>> sb.getByIdLocal(3) <music21.spanner.SpannerBundle of size 0> ''' cacheKey = f'idLocal-{idLocal}' if cacheKey not in self._cache or self._cache[cacheKey] is None: out: list[Spanner] = [] for sp in self._storage: if sp.idLocal == idLocal: out.append(sp) self._cache[cacheKey] = self.__class__(out) return self._cache[cacheKey]
[docs] def getByCompleteStatus(self, completeStatus: bool) -> SpannerBundle: ''' Get spanners by matching status of `completeStatus` to the same attribute. >>> su1 = spanner.Slur() >>> su1.idLocal = 1 >>> su1.completeStatus = True >>> su2 = spanner.Slur() >>> su2.idLocal = 2 >>> sb = spanner.SpannerBundle() >>> sb.append(su1) >>> sb.append(su2) >>> sb2 = sb.getByCompleteStatus(True) >>> len(sb2) 1 >>> sb2 = sb.getByIdLocal(1).getByCompleteStatus(True) >>> sb2[0] == su1 True ''' # cannot cache, as complete status may change internally post: list[Spanner] = [] for sp in self._storage: if sp.completeStatus == completeStatus: post.append(sp) return self.__class__(post)
[docs] def getBySpannedElement(self, spannedElement: Spanner) -> SpannerBundle: ''' Given a spanner spannedElement (an object), return a new SpannerBundle of all Spanner objects that have this object as a spannedElement. >>> n1 = note.Note() >>> n2 = note.Note() >>> n3 = note.Note() >>> su1 = spanner.Slur(n1, n2) >>> su2 = spanner.Slur(n2, n3) >>> sb = spanner.SpannerBundle() >>> sb.append(su1) >>> sb.append(su2) >>> list(sb.getBySpannedElement(n1)) == [su1] True >>> list(sb.getBySpannedElement(n2)) == [su1, su2] True >>> list(sb.getBySpannedElement(n3)) == [su2] True ''' # NOTE: this is a performance critical operation # idTarget = id(spannedElement) # post = self.__class__() # for sp in self._storage: # storage is a list # if idTarget in sp.getSpannedElementIds(): # post.append(sp) # return post idTarget = id(spannedElement) cacheKey = f'getBySpannedElement-{idTarget}' if cacheKey not in self._cache or self._cache[cacheKey] is None: out: list[Spanner] = [] for sp in self._storage: # storage is a list of spanners # __contains__() will test for identity, not equality # see Spanner.hasSpannedElement(), which just calls __contains__() if spannedElement in sp: out.append(sp) self._cache[cacheKey] = self.__class__(out) return self._cache[cacheKey]
[docs] def replaceSpannedElement( self, old: base.Music21Object, new: base.Music21Object ) -> list[Spanner]: # noinspection PyShadowingNames ''' Given a spanner spannedElement (an object), replace all old spannedElements with new spannedElements for all Spanner objects contained in this bundle. The `old` parameter must be an object, not an object id. If no replacements are found, no errors are raised. Returns a list of spanners that had elements replaced. >>> n1 = note.Note('C') >>> n2 = note.Note('D') >>> su1 = spanner.Line(n1, n2) >>> su2 = spanner.Glissando(n2, n1) >>> sb = spanner.SpannerBundle() >>> sb.append(su1) >>> sb.append(su2) >>> su1 <music21.spanner.Line <music21.note.Note C><music21.note.Note D>> >>> su2 <music21.spanner.Glissando <music21.note.Note D><music21.note.Note C>> >>> n3 = note.Note('E') >>> replacedSpanners = sb.replaceSpannedElement(n2, n3) >>> replacedSpanners == [su1, su2] True >>> su1 <music21.spanner.Line <music21.note.Note C><music21.note.Note E>> >>> su2 <music21.spanner.Glissando <music21.note.Note E><music21.note.Note C>> * Changed in v7: id() is no longer allowed for `old`. >>> sb.replaceSpannedElement(id(n1), n2) Traceback (most recent call last): TypeError: send elements to replaceSpannedElement(), not ids. ''' if isinstance(old, int): raise TypeError('send elements to replaceSpannedElement(), not ids.') replacedSpanners: list[Spanner] = [] # post = self.__class__() # return a bundle of spanners that had changes self._cache.clear() for sp in self._storage: # Spanners in a list # environLocal.printDebug(['looking at spanner', sp, sp.getSpannedElementIds()]) sp._cache = {} # accurate, so long as Spanner.__contains__() checks identity, not equality # see discussion at https://github.com/cuthbertLab/music21/pull/905 if old in sp: sp.replaceSpannedElement(old, new) replacedSpanners.append(sp) # post.append(sp) # environLocal.printDebug(['replaceSpannedElement()', sp, 'old', old, # 'id(old)', id(old), 'new', new, 'id(new)', id(new)]) self._cache.clear() return replacedSpanners
[docs] def getByClass(self, searchClass: str|type|tuple[type, ...]) -> 'SpannerBundle': ''' Given a spanner class, return a new SpannerBundle of all Spanners of the desired class. >>> su1 = spanner.Slur() >>> su2 = layout.StaffGroup() >>> su3 = layout.StaffGroup() >>> sb = spanner.SpannerBundle() >>> sb.append(su1) >>> sb.append(su2) >>> sb.append(su3) `searchClass` should be a Class. >>> slurs = sb.getByClass(spanner.Slur) >>> slurs <music21.spanner.SpannerBundle of size 1> >>> list(slurs) == [su1] True >>> list(sb.getByClass(spanner.Slur)) == [su1] True >>> list(sb.getByClass(layout.StaffGroup)) == [su2, su3] True A tuple of classes can also be given: >>> len(sb.getByClass((spanner.Slur, layout.StaffGroup))) 3 Note that the ability to search via a string will be removed in version 10. ''' # NOTE: this is called very frequently and is optimized. cacheKey = f'getByClass-{searchClass}' searchStr = searchClass if isinstance(searchClass, str) else '' searchClasses = () if isinstance(searchClass, str) else searchClass if cacheKey not in self._cache or self._cache[cacheKey] is None: out: list[Spanner] = [] for sp in self._storage: if searchStr and searchStr in sp.classes: out.append(sp) else: if isinstance(sp, searchClasses): # PyCharm thinks this is a type, not a Spanner # noinspection PyTypeChecker out.append(sp) self._cache[cacheKey] = self.__class__(out) return self._cache[cacheKey]
[docs] def setIdLocalByClass(self, className, maxId=6): # noinspection PyShadowingNames ''' (See :meth:`setIdLocals` for an explanation of what an idLocal is.) Automatically set idLocal values for all members of the provided class. This is necessary in cases where spanners are newly created in potentially overlapping boundaries and need to be tagged for MusicXML or other output. Note that, if some Spanners already have idLocals, they will be overwritten. The `maxId` parameter sets the largest number that is available for this class. In MusicXML it is 6. Currently, this method just iterates over the spanners of this class and counts the number from 1-6 and then recycles numbers. It does not check whether more than 6 overlapping spanners of the same type exist, nor does it reset the count to 1 after all spanners of that class have been closed. The example below demonstrates that the position of the contents of the spanner have no bearing on its idLocal (since we don't even put anything into the spanners). >>> su1 = spanner.Slur() >>> su2 = layout.StaffGroup() >>> su3 = spanner.Slur() >>> sb = spanner.SpannerBundle() >>> sb.append(su1) >>> sb.append(su2) >>> sb.append(su3) >>> [sp.idLocal for sp in sb.getByClass(spanner.Slur)] [None, None] >>> sb.setIdLocalByClass('Slur') >>> [sp.idLocal for sp in sb.getByClass(spanner.Slur)] [1, 2] ''' # note that this overrides previous values for i, sp in enumerate(self.getByClass(className)): # 6 seems to be limit in musicxml processing sp.idLocal = (i % maxId) + 1
[docs] def setIdLocals(self): # noinspection PyShadowingNames ''' Utility method for outputting MusicXML (and potentially other formats) for spanners. Each Spanner type (slur, line, glissando, etc.) in MusicXML has a number assigned to it. We call this number, `idLocal`. idLocal is a number from 1 to 6. This does not mean that your piece can only have six slurs total! But it does mean that within a single part, only up to 6 slurs can happen simultaneously. But as soon as a slur stops, its idLocal can be reused. This method sets all idLocals for all classes in this SpannerBundle. This will assure that each class has a unique idLocal number. Calling this method is destructive: existing idLocal values will be lost. >>> su1 = spanner.Slur() >>> su2 = layout.StaffGroup() >>> su3 = spanner.Slur() >>> sb = spanner.SpannerBundle() >>> sb.append(su1) >>> sb.append(su2) >>> sb.append(su3) >>> [sp.idLocal for sp in sb.getByClass('Slur')] [None, None] >>> sb.setIdLocals() >>> [(sp, sp.idLocal) for sp in sb] [(<music21.spanner.Slur>, 1), (<music21.layout.StaffGroup>, 1), (<music21.spanner.Slur>, 2)] :class:`~music21.dynamics.DynamicWedge` objects are commingled. That is, :class:`~music21.dynamics.Crescendo` and :class:`~music21.dynamics.Diminuendo` are not numbered separately: >>> sb2 = spanner.SpannerBundle() >>> c = dynamics.Crescendo() >>> d = dynamics.Diminuendo() >>> sb2.append(c) >>> sb2.append(d) >>> sb2.setIdLocals() >>> [(sp, sp.idLocal) for sp in sb2] [(<music21.dynamics.Crescendo>, 1), (<music21.dynamics.Diminuendo>, 2)] ''' # Crescendo and Diminuendo share the same numbering # So number by DynamicWedge instead (next parent class) skip_classes = ('Crescendo', 'Diminuendo') classes = set() for sp in self._storage: for klass in sp.classes: if klass not in skip_classes: classes.add(klass) break for className in classes: self.setIdLocalByClass(className)
[docs] def getByClassIdLocalComplete(self, className, idLocal, completeStatus): ''' Get all spanners of a specified class `className`, an id `idLocal`, and a `completeStatus`. This is a convenience routine for multiple filtering when searching for relevant Spanners to pair with. >>> su1 = spanner.Slur() >>> su2 = layout.StaffGroup() >>> su2.idLocal = 3 >>> sb = spanner.SpannerBundle() >>> sb.append(su1) >>> sb.append(su2) >>> list(sb.getByClassIdLocalComplete(layout.StaffGroup, 3, False)) == [su2] True >>> su2.completeStatus = True >>> list(sb.getByClassIdLocalComplete(layout.StaffGroup, 3, False)) == [] True ''' # TODO: write utility classes that just modify lists and cast to a spannerBundle # at the end. return self.getByClass(className).getByIdLocal( idLocal).getByCompleteStatus(completeStatus)
[docs] def setPendingSpannedElementAssignment( self, sp: Spanner, className: str, offsetInScore: OffsetQL|None = None, ) -> PendingAssignmentRef: ''' A SpannerBundle can be set up so that a particular spanner (sp) is looking for an element of class (className) to be set as first element. Any future element that matches the className (and offsetInScore, if specified) which is passed to the SpannerBundle via freePendingSpannedElementAssignment() will get it. Call `popPendingSpannedElementAssignments` after parsing to recover pending spanners that never matched their expected elements — e.g., a MusicXML `<direction>` whose `<offset>` lands between notes, needing a `SpannerAnchor` instead. First, let's see the usage without an explicit offset: Create some notes and a rest. >>> n1 = note.Note('C') >>> r1 = note.Rest() >>> n2 = note.Note('D') Notes start without any associated spanners. >>> n1.getSpannerSites() [] Create an empty Slur and a SpannerBundle to hold it. >>> su1 = spanner.Slur() >>> sb1 = spanner.SpannerBundle() >>> sb1.append(su1) >>> su1.getSpannedElements() [] Now call this method to register `su1` as awaiting the next note element. The method returns a `PendingAssignmentRef` (frozen dataclass) with information about what is registered as pending assignment. >>> sb1.setPendingSpannedElementAssignment(su1, 'Note') PendingAssignmentRef(spanner=<music21.spanner.Slur>, className='Note', offsetInScore=None) Elements are then potentially added to spanners by calling `freePendingSpannedElementAssignment` on the SpannerBundle. That method will not attach a rest like `r1` because it is not a `'Note'`: >>> sb1.freePendingSpannedElementAssignment(r1) >>> r1 in su1 False But the next Note will be attached: >>> sb1.freePendingSpannedElementAssignment(n1) >>> su1 <music21.spanner.Slur <music21.note.Note C>> >>> n1 in su1 True >>> n1.getSpannerSites() [<music21.spanner.Slur <music21.note.Note C>>] Once the pending assignment has been satisfied, the registration is cleared from the SpannerBundle, so no future notes that would have satisfied the assignment get assigned: >>> sb1.freePendingSpannedElementAssignment(n2) >>> n2 in su1 False >>> n2.getSpannerSites() [] And we can see that the SpannerBundle `sb1` has no spanners still awaiting (pending) elements to assign: >>> sb1.popPendingSpannedElementAssignments() [] Now a similar example using `offsetInScore`. When `offsetInScore` is specified at registration, only candidates passed with a matching offset to `freePendingSpannedElementAssignment` will be attached. Create two notes and a rest. >>> n3 = note.Note('C#') >>> wrongOffsetNote = note.Note('B') >>> r2 = note.Rest() Create a slur with `n3` already in it. >>> sb2 = spanner.SpannerBundle() >>> su2 = spanner.Slur([n3]) >>> sb2.append(su2) >>> su2.getSpannedElements() [<music21.note.Note C#>] Register `su2` as pending and looking for the next Note at offset 4.0: >>> ref = sb2.setPendingSpannedElementAssignment(su2, 'Note', 4.0) >>> ref PendingAssignmentRef(spanner=<music21.spanner.Slur <music21.note.Note C#>>, className='Note', offsetInScore=4.0) Call `freePendingSpannedElementAssignment` to attach an element of the right class and offset. Note that the offset must be passed to the method; it is not necessarily the offset of the object itself (most often it is the `offsetInHierarchy` of the Part object). A note offered at the wrong offset is not attached: >>> sb2.freePendingSpannedElementAssignment(wrongOffsetNote, 5.0) >>> wrongOffsetNote in su2 False (Passing an offset here is important since, when parsing MusicXML, if a `<direction>` has `<offset>` specified, the next note might not be the one at the start of the spanner) The freePending method will not get a rest even at the correct offsetInScore because it is not the class being searched for. >>> sb2.freePendingSpannedElementAssignment(r2, 4.0) >>> r2 in su2 False We are all out of possible notes, we can see which PendingAssignmentRef elements are still awaiting elements to attach to. In this case, there is one, our Slur: >>> unmatched_pendingAssignmentRefs = sb2.popPendingSpannedElementAssignments() >>> len(unmatched_pendingAssignmentRefs) 1 >>> unmatched_pendingAssignmentRefs[0] PendingAssignmentRef(spanner=<music21.spanner.Slur <music21.note.Note C#>>, className='Note', offsetInScore=4.0) >>> unmatched_pendingAssignmentRefs[0].spanner is su2 True Parsers can use the information from the unmatched PendingAssignmentRef to add additional elements to the score at places where a match was expected. Given this ref, the most logical choice would be to create a `SpannerAnchor` object and put it at score offset 4.0 and insert it as the spanner's first element with `insertFirstSpannedElement`. (If a parser needs more information about where the Spanner and SpannerAnchor should go, such as a staff number, etc. it can keep a dictionary mapping `id(PendingAssignmentRef)` to a staffKey, etc.) And now the happy path: a pending assignment that finds a matching note with the right offset. (We'll demonstrate also that you need to pass in your own fractions.) >>> frac = common.numberTools.opFrac >>> n4 = note.Note('G') >>> n5 = note.Note('A') >>> su3 = spanner.Slur([n4]) >>> sb3 = spanner.SpannerBundle() >>> sb3.append(su3) >>> _ = sb3.setPendingSpannedElementAssignment(su3, 'Note', frac(77.33333)) >>> sb3.freePendingSpannedElementAssignment(n5, frac(77.33333)) >>> su3 <music21.spanner.Slur <music21.note.Note A><music21.note.Note G>> >>> n5 in su3 True An important detail demonstrated above is that a freed element is always inserted as the first element of the spanner, even when the spanner already has other elements. (Note A is before note G in the slur). The insert-at-front behavior is important for MusicXML parsing, since a `<direction>` specified is often encountered before the note that should *start* the spanner (because of voices, other classes, etc.), so when the starting note arrives it needs to go to the start. ''' ref = PendingAssignmentRef( spanner=sp, className=className, offsetInScore=offsetInScore, ) self._pendingSpannedElementAssignment.append(ref) return ref
[docs] def freePendingSpannedElementAssignment( self, spannedElementCandidate, offsetInScore: OffsetQL|None = None ): ''' Assigns and frees up a pendingSpannedElementAssignment if one is active and the candidate matches the class (and offsetInScore, if specified). See setPendingSpannedElementAssignment for documentation and tests. It is set up via a first-in, first-out priority. ''' if not self._pendingSpannedElementAssignment: return remove = None for i, ref in enumerate(self._pendingSpannedElementAssignment): if ref.className in spannedElementCandidate.classSet: if (offsetInScore is None or offsetInScore == ref.offsetInScore): ref.spanner.insertFirstSpannedElement(spannedElementCandidate) remove = i break if remove is not None: self._pendingSpannedElementAssignment.pop(remove)
[docs] def popPendingSpannedElementAssignments(self) -> list[PendingAssignmentRef]: ''' Removes and returns all pendingSpannedElementAssignments. This can be called when there will be no more calls to freePendingSpannedElementAssignment, and SpannerAnchors need to be created for each remaining pending assignment. The SpannerAnchors should be created at the appropriate offset, dictated by the assignment's offsetInScore. >>> sb = spanner.SpannerBundle() >>> sl = spanner.Slur() >>> sb.append(sl) >>> sb.setPendingSpannedElementAssignment(sl, 'Note', 0.0) PendingAssignmentRef(spanner=<music21.spanner.Slur>, className='Note', offsetInScore=0.0) `popPendingSpannedElementAssignments` returns the full list and leaves an empty list behind: >>> expectedPending = list(sb._pendingSpannedElementAssignment) >>> pending = sb.popPendingSpannedElementAssignments() >>> pending == expectedPending True >>> sb._pendingSpannedElementAssignment [] ''' output: list[PendingAssignmentRef] = self._pendingSpannedElementAssignment self._pendingSpannedElementAssignment = [] return output
# ------------------------------------------------------------------------------ # connect two or more notes anywhere in the score
[docs] class Slur(Spanner): ''' A slur represented as a spanner between two Notes. Slurs have `.placement` options ('above' or 'below') and `.lineType` ('dashed' or None) ''' def __init__(self, *spannedElements, **keywords): super().__init__(*spannedElements, **keywords) self.placement = None # can above or below, after musicxml self.lineType = None # can be 'dashed' or None
# from music21 import note # self.fillElementTypes = [note.NotRest] # TODO: add property for placement # ------------------------------------------------------------------------------
[docs] class MultiMeasureRest(Spanner): ''' A grouping symbol that indicates that a collection of rests lasts multiple measures. ''' _styleClass = style.TextStyle _DOC_ATTR: dict[str, str] = { 'useSymbols': ''' Boolean to indicate whether rest symbols (breve, longa, etc.) should be used when displaying the rest. Your music21 inventor is a medievalist, so this defaults to True. Change defaults.multiMeasureRestUseSymbols to change globally. ''', 'maxSymbols': ''' An int, specifying the maximum number of rests to display as symbols. Default is 11. If useSymbols is False then this setting does nothing. Change defaults.multiMeasureRestMaxSymbols to change globally. ''', } def __init__(self, *spannedElements, **keywords): super().__init__(*spannedElements, **keywords) # from music21 import note # self.fillElementTypes = [note.Rest] self._overriddenNumber = None self.useSymbols = keywords.get('useSymbols', defaults.multiMeasureRestUseSymbols) self.maxSymbols = keywords.get('maxSymbols', defaults.multiMeasureRestMaxSymbols) def _reprInternal(self): plural = 's' if self.numRests != 1 else '' return f'{self.numRests} measure{plural}' @property def numRests(self): ''' Returns the number of measures involved in the multi-measure rest. Calculated automatically from the number of rests in the spanner. Or can be set manually to override the number. >>> mmr = spanner.MultiMeasureRest() >>> for i in range(6): ... mmr.addSpannedElements([note.Rest(type='whole')]) >>> mmr.numRests 6 >>> mmr.numRests = 10 >>> mmr.numRests 10 ''' if self._overriddenNumber is not None: return self._overriddenNumber else: return len(self) @numRests.setter def numRests(self, overridden): self._overriddenNumber = overridden
# ------------------------------------------------------------------------------ # first/second repeat bracket
[docs] class RepeatBracket(Spanner): ''' A grouping of one or more measures, presumably in sequence, that mark an alternate repeat. These gather what are sometimes called first-time bars and second-time bars. It is assumed that numbering starts from 1. Numberings above 2 are permitted. The `number` keyword argument can be used to pass in the desired number. `overrideDisplay` if set will display something other than the number. For instance `ouvert` and `clos` for medieval music. However, if you use it for something like '1-3' be sure to set number properly too. >>> m = stream.Measure() >>> sp = spanner.RepeatBracket(m, number=1) >>> sp # can be one or more measures <music21.spanner.RepeatBracket 1 <music21.stream.Measure 0 offset=0.0>> >>> sp.number = 3 >>> sp <music21.spanner.RepeatBracket 3 <music21.stream.Measure 0 offset=0.0>> >>> sp.numberRange # the list of repeat numbers [3] >>> sp.number '3' Range of repeats as string: >>> sp.number = '1-3' >>> sp.numberRange [1, 2, 3] >>> sp.number '1-3' Range of repeats as list: >>> sp.number = [2, 3] >>> sp.numberRange [2, 3] >>> sp.number '2, 3' Comma separated numbers: >>> sp.number = '1, 2, 3' >>> sp.numberRange [1, 2, 3] >>> sp.number '1-3' Disjunct numbers: >>> sp.number = '1, 2, 3, 7' >>> sp.numberRange [1, 2, 3, 7] >>> sp.number '1, 2, 3, 7' Override the display. >>> sp.overrideDisplay = '1-3, 7' >>> sp <music21.spanner.RepeatBracket 1-3, 7 <music21.stream.Measure 0 offset=0.0>> number is not affected by display overrides: >>> sp.number '1, 2, 3, 7' ''' _DOC_ATTR = { 'numberRange': ''' Get a contiguous list of repeat numbers that are applicable for this instance. Will always have at least one element, but [0] means undefined. >>> rb = spanner.RepeatBracket() >>> rb.numberRange [0] >>> rb.number = '1,2' >>> rb.numberRange [1, 2] ''', 'overrideDisplay': ''' Override the string representation of this bracket, or use None to not override. ''', } def __init__(self, *spannedElements, number: int|str|Iterable[int] = 0, overrideDisplay: str|None = None, **keywords): super().__init__(*spannedElements, **keywords) # from music21 import stream # self.fillElementTypes = [stream.Measure] # store a range, inclusive of the single number assignment self.numberRange: list[int] = [] self.overrideDisplay = overrideDisplay self.number = number @property def _numberSpanIsAdjacent(self) -> bool: ''' Are there exactly two numbers that should be written as 3, 4 not 3-4. ''' return len(self.numberRange) == 2 and self.numberRange[0] == self.numberRange[1] - 1 @property def _numberSpanIsContiguous(self) -> bool: ''' Can we write as '3, 4' or '5-10' and not as '1, 5, 6, 11'. ''' return common.contiguousList(self.numberRange) # property to enforce numerical numbers def _getNumber(self) -> str: ''' This must return a string, as we may have single numbers or lists. For a raw numerical list, look at `.numberRange`. ''' if len(self.numberRange) == 1: if self.numberRange[0] == 0: return '' return str(self.numberRange[0]) else: if not self._numberSpanIsContiguous: return ', '.join([str(x) for x in self.numberRange]) elif self._numberSpanIsAdjacent: return f'{self.numberRange[0]}, {self.numberRange[-1]}' else: # range of values return f'{self.numberRange[0]}-{self.numberRange[-1]}' def _setNumber(self, value: int|str|Iterable[int]): ''' Set the bracket number. There may be a range of values provided. ''' if value == '': # undefined. self.numberRange = [0] elif common.holdsType(value, int): self.numberRange = [] # clear for x in value: self.numberRange.append(x) elif isinstance(value, str): # assume defined a range with a dash; assumed inclusive if '-' in value: start, end = value.split('-') self.numberRange = list(range(int(start), int(end) + 1)) elif ',' in value: self.numberRange = [] # clear for one_letter_value in value.split(','): one_number = int(one_letter_value.strip()) self.numberRange.append(one_number) elif value.isdigit(): self.numberRange = [int(value)] else: raise SpannerException(f'number for RepeatBracket must be a number, not {value!r}') elif common.isInt(value): self.numberRange = [] # clear if value not in self.numberRange: self.numberRange.append(value) else: raise SpannerException(f'number for RepeatBracket must be a number, not {value!r}') number = property(_getNumber, _setNumber, doc=''' Get or set the number -- returning a string always. >>> rb = spanner.RepeatBracket() >>> rb.number '' >>> rb.number = '5-7' >>> rb.number '5-7' >>> rb.numberRange [5, 6, 7] >>> rb.number = 1 ''') def _reprInternal(self): if self.overrideDisplay is not None: msg = self.overrideDisplay + ' ' else: msg = self.number + ' ' return msg + super()._reprInternal()
# ------------------------------------------------------------------------------ # line-based spanners
[docs] class Ottava(Spanner): ''' An octave shift line: >>> ottava = spanner.Ottava(type='8va') >>> ottava.type '8va' >>> ottava.type = 15 >>> ottava.type '15ma' >>> ottava.type = (8, 'down') >>> ottava.type '8vb' >>> print(ottava) <music21.spanner.Ottava 8vb transposing> An Ottava spanner can either be transposing or non-transposing. In a transposing Ottava spanner, the notes in the stream should be in their written octave (as if the spanner were not there) and all the notes in the spanner will be transposed on Stream.toSoundingPitch(). A non-transposing spanner has notes that are at the pitch that they would sound (therefore the Ottava spanner is a decorative line). >>> ottava.transposing True >>> n1 = note.Note('D4') >>> n2 = note.Note('E4') >>> n2.offset = 2.0 >>> ottava.addSpannedElements([n1, n2]) >>> s = stream.Stream([ottava, n1, n2]) >>> s.atSoundingPitch = False >>> s2 = s.toSoundingPitch() >>> s2.show('text') {0.0} <music21.spanner.Ottava 8vb non-transposing<music21.note.Note D><music21.note.Note E>> {0.0} <music21.note.Note D> {2.0} <music21.note.Note E> >>> for n in s2.notes: ... print(n.nameWithOctave) D3 E3 All valid types are given below: >>> ottava.validOttavaTypes ('8va', '8vb', '15ma', '15mb', '22da', '22db') OMIT_FROM_DOCS Test the round-trip back: >>> s3 = s2.toWrittenPitch() >>> s3.show('text') {0.0} <music21.spanner.Ottava 8vb transposing<music21.note.Note D><music21.note.Note E>> {0.0} <music21.note.Note D> {2.0} <music21.note.Note E> >>> for n in s3.notes: ... print(n.nameWithOctave) D4 E4 ''' validOttavaTypes = ('8va', '8vb', '15ma', '15mb', '22da', '22db') def __init__(self, *spannedElements, type: str = '8va', # pylint: disable=redefined-builtin transposing: bool = True, placement: t.Literal['above', 'below'] = 'above', **keywords): from music21 import note super().__init__(*spannedElements, **keywords) self.fillElementTypes = [note.NotRest] self._type = None # can be 8va, 8vb, 15ma, 15mb self.type = type self.placement = placement # can above or below, after musicxml self.transposing = transposing def _getType(self): return self._type def _setType(self, newType): if common.isNum(newType) and newType in (8, 15): if newType == 8: self._type = '8va' else: self._type = '15ma' # try to parse as list of size, dir elif common.isListLike(newType) and len(newType) >= 1: stub = [] if newType[0] in (8, '8'): stub.append(str(newType[0])) stub.append('v') elif newType[0] in (15, '15'): stub.append(str(newType[0])) stub.append('m') if len(newType) >= 2 and newType[1] == 'down': stub.append('b') else: # default if not provided stub.append('a') self._type = ''.join(stub) else: if (not isinstance(newType, str) or newType.lower() not in self.validOttavaTypes): raise SpannerException( f'cannot create Ottava of type: {newType}') self._type = newType.lower() type = property(_getType, _setType, doc=''' Get or set Ottava type. This can be set by as complete string (such as 8va or 15mb) or with a pair specifying size and direction. >>> os = spanner.Ottava() >>> os.type = '8vb' >>> os.type '8vb' >>> os.type = 15, 'down' >>> os.type '15mb' ''') def _reprInternal(self): transposing = 'transposing' if not self.transposing: transposing = 'non-transposing' return f'{self.type} {transposing}' + super()._reprInternal()
[docs] def shiftMagnitude(self): ''' Get basic parameters of shift. Returns either 8, 15, or 22 depending on the amount of shift ''' if self._type.startswith('8'): return 8 elif self._type.startswith('15'): return 15 elif self._type.startswith('22'): return 22 else: raise SpannerException(f'Cannot get shift magnitude from {self._type!r}')
[docs] def shiftDirection(self, reverse=False): ''' Returns up or down depending on the type of shift. ''' # an 8va mark means that the notes must be shifted down with the mark if self._type.endswith('a'): if reverse: return 'down' else: return 'up' # an 8vb means that the notes must be shifted upward with the mark if self._type.endswith('b'): if reverse: return 'up' else: return 'down'
[docs] def interval(self, reverse=False): ''' Return an interval.Interval() object representing this ottava. >>> ottava = spanner.Ottava(type='15mb') >>> i = ottava.interval() >>> i <music21.interval.Interval P-15> ''' from music21.interval import Interval if self.shiftDirection(reverse=reverse) == 'down': header = 'P-' else: header = 'P' header += str(self.shiftMagnitude()) return Interval(header)
[docs] def performTransposition(self): ''' On a transposing spanner, switch to non-transposing, and transpose all notes and chords in the spanner. The note/chords will all be transposed to their sounding pitch (at least as far as the ottava is concerned; transposing instruments are handled separately). >>> ottava = spanner.Ottava(type='8va') >>> n1 = note.Note('D#4') >>> n2 = note.Note('E#4') >>> ottava.addSpannedElements([n1, n2]) >>> ottava.transposing True >>> ottava.performTransposition() >>> ottava.transposing False >>> n1.nameWithOctave 'D#5' ''' if not self.transposing: return self.transposing = False myInterval = self.interval() for n in self.getSpannedElements(): if not hasattr(n, 'pitches'): continue for p in n.pitches: p.transpose(myInterval, inPlace=True)
[docs] def undoTransposition(self): ''' Change a non-transposing spanner to a transposing spanner, and transpose back all the notes and chords in the spanner. The notes/chords will all be transposed to their written pitch (at least as far as the ottava is concerned; transposing instruments are handled separately). >>> ottava = spanner.Ottava(type='8va') >>> n1 = note.Note('D#4') >>> n2 = note.Note('E#4') >>> ottava.addSpannedElements([n1, n2]) >>> ottava.transposing = False >>> ottava.undoTransposition() >>> ottava.transposing True >>> n1.nameWithOctave 'D#3' ''' if self.transposing: return self.transposing = True myInterval = self.interval(reverse=True) for n in self.getSpannedElements(): if not hasattr(n, 'pitches'): continue for p in n.pitches: p.transpose(myInterval, inPlace=True)
[docs] class Line(Spanner): ''' A line or bracket represented as a spanner above two Notes. Brackets can take many line types. >>> b = spanner.Line() >>> b.lineType = 'dotted' >>> b.lineType 'dotted' >>> b = spanner.Line(endHeight=20) >>> b.endHeight 20 ''' validLineTypes = ('solid', 'dashed', 'dotted', 'wavy') validTickTypes = ('up', 'down', 'arrow', 'both', 'none') def __init__( self, *spannedElements, lineType: str = 'solid', tick: str = 'down', startTick: str = 'down', endTick: str = 'down', startHeight: int|float|None = None, endHeight: int|float|None = None, **keywords ): super().__init__(*spannedElements, **keywords) # from music21 import note # self.fillElementTypes = [note.GeneralNote] DEFAULT_TICK = 'down' self._endTick = DEFAULT_TICK # can be up/down/arrow/both/None self._startTick = DEFAULT_TICK # can be up/down/arrow/both/None self._endHeight = None # for up/down, specified in tenths self._startHeight = None # for up/down, specified in tenths DEFAULT_LINE_TYPE = 'solid' self._lineType = DEFAULT_LINE_TYPE # can be solid, dashed, dotted, wavy DEFAULT_PLACEMENT = 'above' self.placement = DEFAULT_PLACEMENT # can above or below, after musicxml if lineType != DEFAULT_LINE_TYPE: self.lineType = lineType # use property if startTick != DEFAULT_TICK: self.startTick = startTick # use property if endTick != DEFAULT_TICK: self.endTick = endTick # use property if tick != DEFAULT_TICK: self.tick = tick # use property if endHeight is not None: self.endHeight = endHeight # use property if startHeight is not None: self.startHeight = startHeight # use property def _getEndTick(self): return self._endTick def _setEndTick(self, value): if value.lower() not in self.validTickTypes: raise SpannerException(f'not a valid value: {value}') self._endTick = value.lower() endTick = property(_getEndTick, _setEndTick, doc=''' Get or set the endTick property. ''') def _getStartTick(self): return self._startTick def _setStartTick(self, value): if value.lower() not in self.validTickTypes: raise SpannerException(f'not a valid value: {value}') self._startTick = value.lower() startTick = property(_getStartTick, _setStartTick, doc=''' Get or set the startTick property. ''') def _getTick(self): return self._startTick # just returning start def _setTick(self, value): if value.lower() not in self.validTickTypes: raise SpannerException(f'not a valid value: {value}') self._startTick = value.lower() self._endTick = value.lower() tick = property(_getTick, _setTick, doc=''' Set the start and end tick to the same value >>> b = spanner.Line() >>> b.tick = 'arrow' >>> b.startTick 'arrow' >>> b.endTick 'arrow' ''') def _getLineType(self): return self._lineType def _setLineType(self, value): if value is not None and value.lower() not in self.validLineTypes: raise SpannerException(f'not a valid value: {value}') # not sure if we should permit setting as None if value is not None: self._lineType = value.lower() lineType = property(_getLineType, _setLineType, doc=''' Get or set the lineType property. Valid line types are listed in .validLineTypes. >>> b = spanner.Line() >>> b.lineType = 'dotted' >>> b.lineType = 'navyblue' Traceback (most recent call last): music21.spanner.SpannerException: not a valid value: navyblue >>> b.validLineTypes ('solid', 'dashed', 'dotted', 'wavy') ''') def _getEndHeight(self): return self._endHeight def _setEndHeight(self, value): if not (common.isNum(value) and value >= 0): raise SpannerException(f'not a valid value: {value}') self._endHeight = value endHeight = property(_getEndHeight, _setEndHeight, doc=''' Get or set the endHeight property. >>> b = spanner.Line() >>> b.endHeight = -20 Traceback (most recent call last): music21.spanner.SpannerException: not a valid value: -20 ''') def _getStartHeight(self): return self._startHeight def _setStartHeight(self, value): if not (common.isNum(value) and value >= 0): raise SpannerException(f'not a valid value: {value}') self._startHeight = value startHeight = property(_getStartHeight, _setStartHeight, doc=''' Get or set the startHeight property. >>> b = spanner.Line() >>> b.startHeight = None Traceback (most recent call last): music21.spanner.SpannerException: not a valid value: None ''')
[docs] class Glissando(Spanner): ''' A between two Notes specifying a glissando or similar alteration. Different line types can be specified. Glissandos can have a label and a lineType. Label is a string or None. lineType defaults to 'wavy'. >>> gl = spanner.Glissando() >>> gl.lineType 'wavy' >>> print(gl.label) None >>> gl.label = 'gliss.' Note -- not a Line subclass for now, but that might change. ''' validLineTypes = ('solid', 'dashed', 'dotted', 'wavy') validSlideTypes = ('chromatic', 'continuous', 'diatonic', 'white', 'black') def __init__(self, *spannedElements, lineType: str = 'wavy', label: str|None = None, **keywords): super().__init__(*spannedElements, **keywords) # from music21 import note # self.fillElementTypes = [note.NotRest] GLISSANDO_DEFAULT_LINE_TYPE = 'wavy' self._lineType = GLISSANDO_DEFAULT_LINE_TYPE self._slideType = 'chromatic' self.label = None if lineType != GLISSANDO_DEFAULT_LINE_TYPE: self.lineType = lineType # use property if label is not None: self.label = label # use property def _getLineType(self): return self._lineType def _setLineType(self, value): if value.lower() not in self.validLineTypes: raise SpannerException(f'not a valid value: {value}') self._lineType = value.lower() lineType = property(_getLineType, _setLineType, doc=''' Get or set the lineType property. See Line for valid line types. ''') @property def slideType(self): ''' Get or set the slideType which determines how the glissando or slide is to be played. Values are 'chromatic' (default), 'continuous' (like a slide or smear), 'diatonic' (like a harp gliss), 'white' (meaning a white-key gliss as on a marimba), or 'black' (black-key gliss). 'continuous' slides export to MusicXML as a <slide> object. All others export as <glissando>. ''' return self._slideType @slideType.setter def slideType(self, value): if value.lower() not in self.validSlideTypes: raise SpannerException(f'not a valid value: {value}') self._slideType = value.lower()
# ------------------------------------------------------------------------------ # pylint: disable=redefined-outer-name class Test(unittest.TestCase): def setUp(self): from music21.musicxml.m21ToXml import GeneralObjectExporter self.GEX = GeneralObjectExporter() def xmlStr(self, obj): xmlBytes = self.GEX.parse(obj) return xmlBytes.decode('utf-8') def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) def testBasic(self): # how parts might be grouped from music21 import stream from music21 import note from music21 import layout s = stream.Score() p1 = stream.Part() p2 = stream.Part() sg1 = layout.StaffGroup(p1, p2) # place all on Stream s.insert(0, p1) s.insert(0, p2) s.insert(0, sg1) self.assertEqual(len(s), 3) self.assertEqual(sg1.getSpannedElements(), [p1, p2]) # make sure spanners is unified # how slurs might be defined n1 = note.Note() n2 = note.Note() n3 = note.Note() p1.append(n1) p1.append(n2) p1.append(n3) slur1 = Slur() slur1.addSpannedElements([n1, n3]) p1.append(slur1) self.assertEqual(len(s), 3) self.assertEqual(slur1.getSpannedElements(), [n1, n3]) # a note can access what spanners it is part of self.assertEqual(n1.getSpannerSites(), [slur1]) # can a spanner hold spanners: yes sl1 = Slur() sl2 = Slur() sl3 = Slur() sp = Spanner([sl1, sl2, sl3]) self.assertEqual(len(sp.getSpannedElements()), 3) self.assertEqual(sp.getSpannedElements(), [sl1, sl2, sl3]) self.assertEqual(sl1.getSpannerSites(), [sp]) def testSpannerAnchorRepr(self): from music21 import stream from music21 import spanner # SpannerAnchor with no activeSite sa1 = spanner.SpannerAnchor() self.assertEqual(repr(sa1), '<music21.spanner.SpannerAnchor unanchored>') # SpannerAnchor with activeSite, but no duration m = stream.Measure() m.insert(0.5, sa1) self.assertEqual(repr(sa1), '<music21.spanner.SpannerAnchor at 0.5>') # SpannerAnchor with activeSite and duration sa1.quarterLength = 2.5 self.assertEqual(repr(sa1), '<music21.spanner.SpannerAnchor at 0.5-3.0>') def testSpannerRepr(self): from music21 import spanner su1 = spanner.Slur() self.assertEqual(repr(su1), '<music21.spanner.Slur>') def testSpannerFill(self): from music21 import stream from music21 import note from music21 import spanner theNotes = [note.Note('A'), note.Note('B'), note.Note('C'), note.Note('D')] m = stream.Measure(theNotes) # Spanner with no fillElementTypes sp = spanner.Spanner(theNotes[0], theNotes[3]) sp.fill(m) # should not have done anything noFillElements = [theNotes[0], theNotes[3]] self.assertEqual(len(sp), 2) for i, el in enumerate(sp.getSpannedElements()): self.assertIs(el, noFillElements[i]) # Ottava with filledStatus == True ott1 = spanner.Ottava(noFillElements) ott1.filledStatus = True # pretend it has already been filled ott1.fill(m) # should not have done anything self.assertEqual(len(sp), 2) for i, el in enumerate(sp.getSpannedElements()): self.assertIs(el, noFillElements[i]) # same Ottava but with filledStatus == False ott1.filledStatus = False ott1.fill(m) # ott1 should have been filled self.assertIs(ott1.filledStatus, True) self.assertEqual(len(ott1), 4) for i, el in enumerate(ott1.getSpannedElements()): self.assertIs(el, theNotes[i]) # Ottava with no elements ott2 = spanner.Ottava() ott2.fill(m) self.assertEqual(len(ott2), 0) # Ottava with only element not in searchStream expectedElements = [note.Note('E')] ott3 = spanner.Ottava(expectedElements) ott3.fill(m) self.assertEqual(len(ott3), 1) self.assertIs(ott3.getFirst(), expectedElements[0]) # Ottava with start element not in searchStream, end element is expectedElements = [note.Note('F'), m.notes[0]] ott4 = spanner.Ottava(expectedElements) ott4.fill(m) self.assertEqual(len(ott4), 2) for i, el in enumerate(ott4.getSpannedElements()): self.assertIs(el, expectedElements[i]) # Ottava with endElement not in searchStream, startElement is expectedElements = [m.notes[0], note.Note('G')] ott5 = spanner.Ottava(expectedElements) ott5.fill(m) self.assertEqual(len(ott5), 2) for i, el in enumerate(ott5.getSpannedElements()): self.assertIs(el, expectedElements[i]) def testSpannerBundle(self): from music21 import spanner from music21 import stream su1 = spanner.Slur() su1.idLocal = 1 su2 = spanner.Slur() su2.idLocal = 2 sb = spanner.SpannerBundle() sb.append(su1) sb.append(su2) self.assertEqual(len(sb), 2) self.assertEqual(sb[0], su1) self.assertEqual(sb[1], su2) su3 = spanner.Slur() su4 = spanner.Slur() s = stream.Stream() s.append(su3) s.append(su4) sb2 = spanner.SpannerBundle(list(s)) self.assertEqual(len(sb2), 2) self.assertEqual(sb2[0], su3) self.assertEqual(sb2[1], su4) def testDeepcopySpanner(self): from music21 import spanner from music21 import note # how slurs might be defined n1 = note.Note() # n2 = note.Note() n3 = note.Note() su1 = Slur() su1.addSpannedElements([n1, n3]) self.assertEqual(n1.getSpannerSites(), [su1]) self.assertEqual(n3.getSpannerSites(), [su1]) su2 = copy.deepcopy(su1) self.assertEqual(su1.getSpannedElements(), [n1, n3]) self.assertEqual(su2.getSpannedElements(), [n1, n3]) self.assertEqual(n1.getSpannerSites(), [su1, su2]) self.assertEqual(n3.getSpannerSites(), [su1, su2]) sb1 = spanner.SpannerBundle([su1, su2]) sb2 = copy.deepcopy(sb1) self.assertEqual(sb1[0].getSpannedElements(), [n1, n3]) self.assertEqual(sb2[0].getSpannedElements(), [n1, n3]) # spanners stored within are not the same objects self.assertNotEqual(id(sb2[0]), id(sb1[0])) def testReplaceSpannedElement(self): from music21 import note from music21 import spanner n1 = note.Note() n2 = note.Note() n3 = note.Note() n4 = note.Note() n5 = note.Note() su1 = spanner.Slur() su1.addSpannedElements([n1, n3]) self.assertEqual(su1.getSpannedElements(), [n1, n3]) self.assertEqual(n1.getSpannerSites(), [su1]) su1.replaceSpannedElement(n1, n2) self.assertEqual(su1.getSpannedElements(), [n2, n3]) # this note now has no spanner sites self.assertEqual(n1.getSpannerSites(), []) self.assertEqual(n2.getSpannerSites(), [su1]) # replace n2 w/ n1 su1.replaceSpannedElement(n2, n1) self.assertEqual(su1.getSpannedElements(), [n1, n3]) self.assertEqual(n2.getSpannerSites(), []) self.assertEqual(n1.getSpannerSites(), [su1]) su2 = spanner.Slur() su2.addSpannedElements([n3, n4]) su3 = spanner.Slur() su3.addSpannedElements([n4, n5]) # n1a = note.Note() # n2a = note.Note() n3a = note.Note() n4a = note.Note() # n5a = note.Note() sb1 = spanner.SpannerBundle([su1, su2, su3]) self.assertEqual(len(sb1), 3) self.assertEqual(list(sb1), [su1, su2, su3]) # n3 is found in su1 and su2 sb1.replaceSpannedElement(n3, n3a) self.assertEqual(len(sb1), 3) self.assertEqual(list(sb1), [su1, su2, su3]) self.assertEqual(sb1[0].getSpannedElements(), [n1, n3a]) # check su2 self.assertEqual(sb1[1].getSpannedElements(), [n3a, n4]) sb1.replaceSpannedElement(n4, n4a) self.assertEqual(sb1[1].getSpannedElements(), [n3a, n4a]) # check su3 self.assertEqual(sb1[2].getSpannedElements(), [n4a, n5]) def testRepeatBracketA(self): from music21 import spanner from music21 import stream m1 = stream.Measure() rb1 = spanner.RepeatBracket(m1) # if added again; it is not really added, it simply is ignored rb1.addSpannedElements(m1) self.assertEqual(len(rb1), 1) def testRepeatBracketB(self): from music21 import note from music21 import spanner from music21 import stream from music21 import bar p = stream.Part() m1 = stream.Measure() m1.repeatAppend(note.Note('c4'), 4) p.append(m1) m2 = stream.Measure() m2.repeatAppend(note.Note('d#4'), 4) p.append(m2) m3 = stream.Measure() m3.repeatAppend(note.Note('g#4'), 4) m3.rightBarline = bar.Repeat(direction='end') p.append(m3) p.append(spanner.RepeatBracket(m3, number=1)) m4 = stream.Measure() m4.repeatAppend(note.Note('a4'), 4) m4.rightBarline = bar.Repeat(direction='end') p.append(m4) p.append(spanner.RepeatBracket(m4, number=2)) m5 = stream.Measure() m5.repeatAppend(note.Note('b4'), 4) m5.rightBarline = bar.Repeat(direction='end') p.append(m5) p.append(spanner.RepeatBracket(m5, number=3)) m6 = stream.Measure() m6.repeatAppend(note.Note('c#5'), 4) p.append(m6) # all spanners should be at the part level self.assertEqual(len(p.spanners), 3) # noinspection DuplicatedCode def testRepeatBracketC(self): from music21 import note from music21 import spanner from music21 import stream from music21 import bar p = stream.Part() m1 = stream.Measure() m1.repeatAppend(note.Note('c4'), 4) p.append(m1) m2 = stream.Measure() m2.repeatAppend(note.Note('d#4'), 4) p.append(m2) m3 = stream.Measure() m3.repeatAppend(note.Note('g#4'), 4) m3.rightBarline = bar.Repeat(direction='end') p.append(m3) rb1 = spanner.RepeatBracket(number=1) rb1.addSpannedElements(m2, m3) self.assertEqual(len(rb1), 2) p.insert(0, rb1) m4 = stream.Measure() m4.repeatAppend(note.Note('a4'), 4) m4.rightBarline = bar.Repeat(direction='end') p.append(m4) p.append(spanner.RepeatBracket(m4, number=2)) m5 = stream.Measure() m5.repeatAppend(note.Note('b4'), 4) p.append(m5) # p.show() # all spanners should be at the part level self.assertEqual(len(p.spanners), 2) # p.show() raw = self.xmlStr(p) self.assertGreater(raw.find('''<ending number="1" type="start" />'''), 1) self.assertGreater(raw.find('''<ending number="2" type="stop" />'''), 1) self.assertGreater(raw.find('''<ending number="2" type="start" />'''), 1) # noinspection DuplicatedCode def testRepeatBracketD(self): from music21 import note from music21 import spanner from music21 import stream from music21 import bar p = stream.Part() m1 = stream.Measure() m1.repeatAppend(note.Note('c4'), 4) p.append(m1) m2 = stream.Measure() m2.repeatAppend(note.Note('d#4'), 4) p.append(m2) m3 = stream.Measure() m3.repeatAppend(note.Note('g#4'), 4) m3.rightBarline = bar.Repeat(direction='end') p.append(m3) rb1 = spanner.RepeatBracket(number=1) rb1.addSpannedElements(m2, m3) self.assertEqual(len(rb1), 2) p.insert(0, rb1) m4 = stream.Measure() m4.repeatAppend(note.Note('a4'), 4) p.append(m4) m5 = stream.Measure() m5.repeatAppend(note.Note('b4'), 4) m5.rightBarline = bar.Repeat(direction='end') p.append(m5) rb2 = spanner.RepeatBracket(number=2) rb2.addSpannedElements(m4, m5) self.assertEqual(len(rb2), 2) p.insert(0, rb2) m6 = stream.Measure() m6.repeatAppend(note.Note('a4'), 4) p.append(m6) m7 = stream.Measure() m7.repeatAppend(note.Note('b4'), 4) p.append(m7) m8 = stream.Measure() m8.repeatAppend(note.Note('a4'), 4) m8.rightBarline = bar.Repeat(direction='end') p.append(m8) rb3 = spanner.RepeatBracket(number=3) rb3.addSpannedElements(m6, m8) self.assertEqual(len(rb3), 2) p.insert(0, rb3) m9 = stream.Measure() m9.repeatAppend(note.Note('a4'), 4) p.append(m9) m10 = stream.Measure() m10.repeatAppend(note.Note('b4'), 4) p.append(m10) m11 = stream.Measure() m11.repeatAppend(note.Note('a4'), 4) p.append(m11) m12 = stream.Measure() m12.repeatAppend(note.Note('a4'), 4) m12.rightBarline = bar.Repeat(direction='end') p.append(m12) rb4 = spanner.RepeatBracket(number=4) rb4.addSpannedElements(m9, m10, m11, m12) self.assertEqual(len(rb4), 4) p.insert(0, rb4) # p.show() # all spanners should be at the part level self.assertEqual(len(p.getElementsByClass(stream.Measure)), 12) self.assertEqual(len(p.spanners), 4) raw = self.xmlStr(p) self.assertGreater(raw.find('''<ending number="1" type="start" />'''), 1) self.assertGreater(raw.find('''<ending number="2" type="stop" />'''), 1) self.assertGreater(raw.find('''<ending number="2" type="start" />'''), 1) p1 = copy.deepcopy(p) raw = self.xmlStr(p1) self.assertGreater(raw.find('''<ending number="1" type="start" />'''), 1) self.assertGreater(raw.find('''<ending number="2" type="stop" />'''), 1) self.assertGreater(raw.find('''<ending number="2" type="start" />'''), 1) p2 = copy.deepcopy(p1) raw = self.xmlStr(p2) self.assertGreater(raw.find('''<ending number="1" type="start" />'''), 1) self.assertGreater(raw.find('''<ending number="2" type="stop" />'''), 1) self.assertGreater(raw.find('''<ending number="2" type="start" />'''), 1) def testRepeatBracketE(self): from music21 import note from music21 import spanner from music21 import stream from music21 import bar p = stream.Part() m1 = stream.Measure(number=1) m1.repeatAppend(note.Note('c4'), 1) p.append(m1) m2 = stream.Measure(number=2) m2.repeatAppend(note.Note('d#4'), 1) p.append(m2) m3 = stream.Measure(number=3) m3.repeatAppend(note.Note('g#4'), 1) m3.rightBarline = bar.Repeat(direction='end') p.append(m3) p.append(spanner.RepeatBracket(m3, number=1)) m4 = stream.Measure(number=4) m4.repeatAppend(note.Note('a4'), 1) m4.rightBarline = bar.Repeat(direction='end') p.append(m4) p.append(spanner.RepeatBracket(m4, number=2)) m5 = stream.Measure(number=5) m5.repeatAppend(note.Note('b4'), 1) m5.rightBarline = bar.Repeat(direction='end') p.append(m5) p.append(spanner.RepeatBracket(m5, number=3)) m6 = stream.Measure(number=6) m6.repeatAppend(note.Note('c#5'), 1) p.append(m6) # all spanners should be at the part level self.assertEqual(len(p.spanners), 3) # try copying once p1 = copy.deepcopy(p) self.assertEqual(len(p1.spanners), 3) m5 = p1.getElementsByClass(stream.Measure)[-2] sp3 = p1.spanners[2] self.assertTrue(sp3.hasSpannedElement(m5)) # for m in p1.getElementsByClass(stream.Measure): # print(m, id(m)) # for sp in p1.spanners: # print(sp, id(sp), [c for c in sp.getSpannedElementIds()]) # p1.show() p2 = copy.deepcopy(p1) self.assertEqual(len(p2.spanners), 3) m5 = p2.getElementsByClass(stream.Measure)[-2] sp3 = p2.spanners[2] self.assertTrue(sp3.hasSpannedElement(m5)) p3 = copy.deepcopy(p2) self.assertEqual(len(p3.spanners), 3) m5 = p3.getElementsByClass(stream.Measure)[-2] sp3 = p3.spanners[2] self.assertTrue(sp3.hasSpannedElement(m5)) def testOttavaShiftA(self): ''' Test basic octave shift creation and output, as well as passing objects through make measure calls. ''' from music21 import stream from music21 import note from music21 import chord from music21.spanner import Ottava # need to do it this way for classSet s = stream.Stream() s.repeatAppend(chord.Chord(['c-3', 'g4']), 12) # s.repeatAppend(note.Note(), 12) n1 = s.notes[0] n2 = s.notes[-1] sp1 = Ottava(n1, n2) # default is 8va s.append(sp1) raw = self.xmlStr(s) self.assertEqual(raw.count('octave-shift'), 2) self.assertEqual(raw.count('type="down"'), 1) # s.show() s = stream.Stream() s.repeatAppend(note.Note(), 12) n1 = s.notes[0] n2 = s.notes[-1] sp1 = Ottava(n1, n2, type='8vb') s.append(sp1) # s.show() raw = self.xmlStr(s) self.assertEqual(raw.count('octave-shift'), 2) self.assertEqual(raw.count('type="up"'), 1) s = stream.Stream() s.repeatAppend(note.Note(), 12) n1 = s.notes[0] n2 = s.notes[-1] sp1 = Ottava(n1, n2, type='15ma') s.append(sp1) # s.show() raw = self.xmlStr(s) self.assertEqual(raw.count('octave-shift'), 2) self.assertEqual(raw.count('type="down"'), 1) s = stream.Stream() s.repeatAppend(note.Note(), 12) n1 = s.notes[0] n2 = s.notes[-1] sp1 = Ottava(n1, n2, type='15mb') s.append(sp1) # s.show() raw = self.xmlStr(s) self.assertEqual(raw.count('octave-shift'), 2) self.assertEqual(raw.count('type="up"'), 1) def testOttavaShiftB(self): ''' Test a single note octave ''' from music21 import stream from music21 import note from music21 import spanner s = stream.Stream() n = note.Note('c4') sp = spanner.Ottava(n) s.append(n) s.append(sp) # s.show() raw = self.xmlStr(s) self.assertEqual(raw.count('octave-shift'), 2) self.assertEqual(raw.count('type="down"'), 1) def testCrescendoA(self): from music21 import stream from music21 import note from music21 import dynamics s = stream.Stream() # n1 = note.Note('C') # n2 = note.Note('D') # n3 = note.Note('E') # # s.append(n1) # s.append(note.Note('A')) # s.append(n2) # s.append(note.Note('B')) # s.append(n3) # s.repeatAppend(chord.Chord(['c-3', 'g4']), 12) s.repeatAppend(note.Note(type='half'), 4) n1 = s.notes[0] n1.pitch.step = 'D' # s.insert(n1.offset, dynamics.Dynamic('fff')) n2 = s.notes[len(s.notes) // 2] n2.pitch.step = 'E' # s.insert(n2.offset, dynamics.Dynamic('ppp')) n3 = s.notes[-1] n3.pitch.step = 'F' # s.insert(n3.offset, dynamics.Dynamic('ff')) sp1 = dynamics.Diminuendo(n1, n2) sp2 = dynamics.Crescendo(n2, n3) s.append(sp1) s.append(sp2) # s._reprText() # s.show('t') raw = self.xmlStr(s) # print(raw) self.assertEqual(raw.count('<wedge'), 4) # self.assertEqual(raw.count('octave-shift'), 2) def testLineA(self): from music21 import stream from music21 import note from music21 import spanner s = stream.Stream() s.repeatAppend(note.Note(), 12) n1 = s.notes[0] n2 = s.notes[len(s.notes) // 2] n3 = s.notes[-1] sp1 = spanner.Line(n1, n2, startTick='up', lineType='dotted') sp2 = spanner.Line(n2, n3, startTick='down', lineType='dashed', endHeight=40) s.append(sp1) s.append(sp2) # s.show('t') raw = self.xmlStr(s) # print(raw) self.assertEqual(raw.count('<bracket'), 4) def testLineB(self): from music21 import stream from music21 import note from music21 import spanner s = stream.Stream() s.repeatAppend(note.Note(), 12) n1 = s.notes[4] n2 = s.notes[-1] n3 = s.notes[0] n4 = s.notes[2] sp1 = spanner.Line(n1, n2, startTick='up', endTick='down', lineType='solid') sp2 = spanner.Line(n3, n4, startTick='arrow', endTick='none', lineType='solid') s.append(sp1) s.append(sp2) # s.show() raw = self.xmlStr(s) self.assertEqual(raw.count('<bracket'), 4) self.assertEqual(raw.count('line-end="arrow"'), 1) self.assertEqual(raw.count('line-end="none"'), 1) self.assertEqual(raw.count('line-end="up"'), 1) self.assertEqual(raw.count('line-end="down"'), 1) def testGlissandoA(self): from music21 import stream from music21 import note from music21 import spanner s = stream.Stream() s.repeatAppend(note.Note(), 3) for i, n in enumerate(s.notes): n.transpose(i + (i % 2 * 12), inPlace=True) # note: this does not support glissandi between non-adjacent notes n1 = s.notes[0] n2 = s.notes[len(s.notes) // 2] n3 = s.notes[-1] sp1 = spanner.Glissando(n1, n2) sp2 = spanner.Glissando(n2, n3) sp2.lineType = 'dashed' s.append(sp1) s.append(sp2) s = s.makeNotation() # s.show('t') raw = self.xmlStr(s) # print(raw) self.assertEqual(raw.count('<glissando'), 4) self.assertEqual(raw.count('line-type="dashed"'), 2) def testGlissandoB(self): from music21 import stream from music21 import note from music21 import spanner s = stream.Stream() s.repeatAppend(note.Note(), 12) for i, n in enumerate(s.notes): n.transpose(i + (i % 2 * 12), inPlace=True) # note: this does not support glissandi between non-adjacent notes n1 = s.notes[0] n2 = s.notes[1] sp1 = spanner.Glissando(n1, n2) sp1.lineType = 'solid' sp1.label = 'gliss.' s.append(sp1) # s.show() raw = self.xmlStr(s) self.assertEqual(raw.count('<glissando'), 2) self.assertEqual(raw.count('line-type="solid"'), 2) self.assertEqual(raw.count('>gliss.<'), 1) # def testDashedLineA(self): # from music21 import stream, note, spanner, chord, dynamics # s = stream.Stream() # s.repeatAppend(note.Note(), 12) # for i, n in enumerate(s.notes): # n.transpose(i + (i % 2 * 12), inPlace=True) # # # note: Musedata presently does not support these # n1 = s.notes[0] # n2 = s.notes[len(s.notes) // 2] # n3 = s.notes[-1] # sp1 = spanner.DashedLine(n1, n2) # sp2 = spanner.DashedLine(n2, n3) # s.append(sp1) # s.append(sp2) # raw = s.musicxml # self.assertEqual(raw.count('<dashes'), 4) def testOneElementSpanners(self): from music21 import note from music21.spanner import Spanner n1 = note.Note() sp = Spanner() sp.addSpannedElements(n1) sp.completeStatus = True self.assertTrue(sp.completeStatus) self.assertTrue(sp.isFirst(n1)) self.assertTrue(sp.isLast(n1)) def testRemoveSpanners(self): from music21 import stream from music21 import note from music21.spanner import Spanner, Slur p = stream.Part() m1 = stream.Measure() m2 = stream.Measure() m1.number = 1 m2.number = 2 n1 = note.Note('C#4', type='whole') n2 = note.Note('D#4', type='whole') m1.insert(0, n1) m2.insert(0, n2) p.append(m1) p.append(m2) sl = Slur([n1, n2]) p.insert(0, sl) for x in p: if isinstance(x, Spanner): p.remove(x) self.assertEqual(len(p.spanners), 0) def testFreezeSpanners(self): from music21 import stream from music21 import note from music21 import converter from music21.spanner import Slur p = stream.Part() m1 = stream.Measure() m2 = stream.Measure() m1.number = 1 m2.number = 2 n1 = note.Note('C#4', type='whole') n2 = note.Note('D#4', type='whole') m1.insert(0, n1) m2.insert(0, n2) p.append(m1) p.append(m2) sl = Slur([n1, n2]) p.insert(0, sl) unused_data = converter.freezeStr(p, fmt='pickle') def testDeepcopyJustSpannerAndNotes(self): from music21 import note from music21 import clef from music21.spanner import Spanner n1 = note.Note('g') n2 = note.Note('f#') c1 = clef.AltoClef() sp1 = Spanner(n1, n2, c1) sp2 = copy.deepcopy(sp1) self.assertEqual(len(sp2.spannerStorage), 3) self.assertIsNot(sp1, sp2) self.assertIs(sp2[0], sp1[0]) self.assertIs(sp2[2], sp1[2]) self.assertIs(sp1[0], n1) self.assertIs(sp2[0], n1) def testDeepcopySpannerInStreamNotNotes(self): from music21 import note from music21 import clef from music21 import stream from music21.spanner import Spanner n1 = note.Note('g') n2 = note.Note('f#') c1 = clef.AltoClef() sp1 = Spanner(n1, n2, c1) st1 = stream.Stream() st1.insert(0.0, sp1) st2 = copy.deepcopy(st1) sp2 = st2.spanners[0] self.assertEqual(len(sp2.spannerStorage), 3) self.assertIsNot(sp1, sp2) self.assertIs(sp2[0], sp1[0]) self.assertIs(sp2[2], sp1[2]) self.assertIs(sp1[0], n1) self.assertIs(sp2[0], n1) def testDeepcopyNotesInStreamNotSpanner(self): from music21 import note from music21 import clef from music21 import stream from music21.spanner import Spanner n1 = note.Note('g') n2 = note.Note('f#') c1 = clef.AltoClef() sp1 = Spanner(n1, n2, c1) st1 = stream.Stream() st1.insert(0.0, n1) st1.insert(1.0, n2) st2 = copy.deepcopy(st1) n3 = st2.notes[0] self.assertEqual(len(n3.getSpannerSites()), 1) sp2 = n3.getSpannerSites()[0] self.assertIs(sp1, sp2) self.assertIsNot(n1, n3) self.assertIs(sp2[2], sp1[2]) self.assertIs(sp1[0], n1) self.assertIs(sp2[0], n1) def testDeepcopyNotesAndSpannerInStream(self): from music21 import note from music21 import stream from music21.spanner import Spanner n1 = note.Note('G4') n2 = note.Note('F#4') sp1 = Spanner(n1, n2) st1 = stream.Stream() st1.insert(0.0, sp1) st1.insert(0.0, n1) st1.insert(1.0, n2) st2 = copy.deepcopy(st1) n3 = st2.notes[0] self.assertEqual(len(n3.getSpannerSites()), 1) sp2 = n3.getSpannerSites()[0] self.assertIsNot(sp1, sp2) self.assertIsNot(n1, n3) sp3 = st2.spanners[0] self.assertIs(sp2, sp3) self.assertIs(sp1[0], n1) self.assertIs(sp2[0], n3) def testDeepcopyStreamWithSpanners(self): from music21 import note from music21 import stream from music21.spanner import Slur n1 = note.Note() su1 = Slur((n1,)) s = stream.Stream() s.insert(0.0, su1) s.insert(0.0, n1) self.assertIs(s.spanners[0].getFirst(), n1) self.assertIs(s.notes[0].getSpannerSites()[0], su1) s2 = copy.deepcopy(s) su2 = s2.spanners[0] n2 = s2.notes[0] self.assertIsNot(su2, su1) self.assertIsNot(n2, n1) self.assertIs(s2.spanners[0].getFirst(), n2) self.assertIs(s2.notes[0].getSpannerSites()[0], su2) self.assertIsNot(s.notes[0].getSpannerSites()[0], su2) self.assertEqual(len(s2.spannerBundle), 1) tn2 = s2.spannerBundle.getBySpannedElement(n2) self.assertEqual(len(tn2), 1) def testGetSpannedElementIds(self): from music21 import note from music21.spanner import Spanner n1 = note.Note('g') n2 = note.Note('f#') n3 = note.Note('e') n4 = note.Note('d-') n5 = note.Note('c') sl = Spanner() sl.addSpannedElements(n1) sl.addSpannedElements(n2, n3) sl.addSpannedElements([n4, n5]) idList = [id(n) for n in [n1, n2, n3, n4, n5]] slList = sl.getSpannedElementIds() self.assertEqual(idList, slList) def testHammerOnPullOff(self): from music21 import converter from music21.musicxml import testPrimitive s = converter.parse(testPrimitive.notations32a) num_hammer_on = len(s.flatten().getElementsByClass('HammerOn')) num_pull_off = len(s.flatten().getElementsByClass('PullOff')) self.assertEqual(num_hammer_on, 1) self.assertEqual(num_pull_off, 1) hammer_on = s.flatten().getElementsByClass('HammerOn')[0] hammer_n0 = hammer_on.getSpannedElements()[0] hammer_n1 = hammer_on.getSpannedElements()[1] self.assertEqual(hammer_n0.getSpannerSites()[0], hammer_on) self.assertEqual(hammer_n1.getSpannerSites()[0], hammer_on) pull_off = s.flatten().getElementsByClass('PullOff')[0] pull_n0 = pull_off.getSpannedElements()[0] pull_n1 = pull_off.getSpannedElements()[1] self.assertEqual(pull_n0.getSpannerSites()[0], pull_off) self.assertEqual(pull_n1.getSpannerSites()[0], pull_off) # ------------------------------------------------------------------------------ # define presented order in documentation _DOC_ORDER: list[type] = [Spanner] if __name__ == '__main__': import music21 music21.mainTest(Test)