Source code for music21.tree.verticality

# ----------------------------------------------------------------------------
# Name:         tree/verticality.py
# Purpose:      Object for dealing with vertical simultaneities in a
#               fast way w/o Chord's overhead
#
# Authors:      Joséphine Wolf Oberholtzer
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2013-2016 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ----------------------------------------------------------------------------
'''
Object for dealing with vertical simultaneities in a fast way w/o Chord's overhead.
'''
from __future__ import annotations

from collections.abc import Iterable, Sequence
import copy
import itertools
import typing as t
from typing import overload
import unittest

from music21 import chord
from music21 import common
from music21 import environment
from music21 import exceptions21
from music21 import note
from music21 import prebase
from music21 import tie
# from music21 import key
# from music21 import pitch
from music21.common.types import OffsetQL, OffsetQLIn
from music21.tree import spans

if t.TYPE_CHECKING:
    from music21.tree.trees import OffsetTree
    from music21.voiceLeading import VoiceLeadingQuartet

environLocal = environment.Environment('tree.verticality')

type PitchedTimespanQuartet = tuple[
    tuple[spans.PitchedTimespan, spans.PitchedTimespan],
    tuple[spans.PitchedTimespan, spans.PitchedTimespan],
]


class VerticalityException(exceptions21.TreeException):
    pass


[docs] class Verticality(prebase.ProtoM21Object): r''' A collection of information about elements that are sounding at a given offset or just finished at that offset or are continuing from before, etc.. Create a timespan-stream from a score: >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) Find the verticality at offset 6.5, or beat 2.5 of measure 2 (there's a one beat pickup) >>> verticality = scoreTree.getVerticalityAt(6.5) >>> verticality <music21.tree.verticality.Verticality 6.5 {E3 D4 G#4 B4}> The representation of a verticality gives the pitches from lowest to highest (in sounding notes). A verticality knows its offset, but because elements might end at different times, it doesn't know its endTime >>> verticality.offset 6.5 >>> verticality.endTime Traceback (most recent call last): AttributeError: 'Verticality' object has no attribute 'endTime' However, we can find when the next verticality starts by looking at the nextVerticality >>> nv = verticality.nextVerticality >>> nv <music21.tree.verticality.Verticality 7.0 {A2 C#4 E4 A4}> >>> nv.offset 7.0 Or more simply: >>> verticality.nextStartOffset 7.0 (There is also a previousVerticality, but not a previousStartOffset) What we just demonstrated is actually very powerful: a Verticality keeps a record of exactly where it is in the timespanTree -- scores can be recreated with this information. Getting back to the task at hand, we can find all the PitchedTimespans (and from there the elements) that start at exactly 6.5. There's one, it's a passing tone D in the tenor. It lasts from offset 6.5 to offset 7.0, with respect to the beginning of the score, not to the beginning of the measure. That is to say, it's an eighth note. >>> verticality.startTimespans (<PitchedTimespan (6.5 to 7.0) <music21.note.Note D>>,) And we can get all the PitchedTimespans that were already sounding at the moment (that is to say, the non-passing tones): >>> verticality.overlapTimespans (<PitchedTimespan (6.0 to 7.0) <music21.note.Note B>>, <PitchedTimespan (6.0 to 7.0) <music21.note.Note G#>>, <PitchedTimespan (6.0 to 7.0) <music21.note.Note E>>) And we can get all the things that stop right at this moment. It's the E in the tenor preceding the passing tone D: >>> verticality.stopTimespans (<PitchedTimespan (6.0 to 6.5) <music21.note.Note E>>,) ''' # CLASS VARIABLES # __slots__ = ( 'offsetTree', 'timespanTree', 'overlapTimespans', 'startTimespans', 'offset', 'stopTimespans', ) _DOC_ATTR: dict[str, str] = { 'offsetTree': r''' Returns the tree initially set else None ''', 'timespanTree': r''' Returns the tree initially set if it was a TimespanTree, else None ''', 'overlapTimespans': r''' Gets timespans overlapping the start offset of a verticality. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(0.5) >>> verticality <music21.tree.verticality.Verticality 0.5 {G#3 B3 E4 B4}> >>> verticality.overlapTimespans (<PitchedTimespan (0.0 to 1.0) <music21.note.Note E>>,) ''', 'startTimespans': r''' Gets the timespans starting at a verticality's start offset. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(1.0) >>> verticality <music21.tree.verticality.Verticality 1.0 {F#3 C#4 F#4 A4}> >>> for timespan in verticality.startTimespans: ... timespan ... <PitchedTimespan (1.0 to 2.0) <music21.note.Note A>> <PitchedTimespan (1.0 to 2.0) <music21.note.Note F#>> <PitchedTimespan (1.0 to 2.0) <music21.note.Note C#>> <PitchedTimespan (1.0 to 2.0) <music21.note.Note F#>> ''', 'offset': r''' Gets the start offset of a verticality. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(1.0) >>> verticality <music21.tree.verticality.Verticality 1.0 {F#3 C#4 F#4 A4}> >>> verticality.offset 1.0 ''', 'stopTimespans': r''' Gets the timespans stopping at a verticality's start offset. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(1.0) >>> verticality <music21.tree.verticality.Verticality 1.0 {F#3 C#4 F#4 A4}> Note that none of the elements in the stopTimespans are listed in the repr for the Verticality >>> for timespan in verticality.stopTimespans: ... timespan ... <PitchedTimespan (0.0 to 1.0) <music21.note.Note E>> <PitchedTimespan (0.5 to 1.0) <music21.note.Note B>> <PitchedTimespan (0.5 to 1.0) <music21.note.Note B>> <PitchedTimespan (0.5 to 1.0) <music21.note.Note G#>> ''', } # INITIALIZER # def __init__( self, offset: OffsetQL = 0.0, overlapTimespans: tuple[spans.ElementTimespan, ...] = (), startTimespans: tuple[spans.ElementTimespan, ...] = (), stopTimespans: tuple[spans.ElementTimespan, ...] = (), timespanTree=None, ): from music21.tree.timespanTree import TimespanTree self.offsetTree: OffsetTree | None = timespanTree self.timespanTree: TimespanTree | None = None if isinstance(timespanTree, TimespanTree): self.timespanTree = timespanTree self.offset: OffsetQL = offset if not isinstance(startTimespans, tuple): raise VerticalityException( f'startTimespans must be a tuple of ElementTimespans, not {startTimespans!r}') if not isinstance(stopTimespans, tuple): raise VerticalityException( f'stopTimespans must be a tuple of ElementTimespans, not {stopTimespans!r}') if not isinstance(overlapTimespans, tuple): raise VerticalityException( f'overlapTimespans must be a tuple of ElementTimespans, not {overlapTimespans!r}') self.startTimespans: tuple[spans.ElementTimespan, ...] = startTimespans self.stopTimespans: tuple[spans.ElementTimespan, ...] = stopTimespans self.overlapTimespans: tuple[spans.ElementTimespan, ...] = overlapTimespans # SPECIAL METHODS # def _reprInternal(self): sortedPitches = sorted(self.pitchSet) enclosedNames = '{' + ' '.join(x.nameWithOctave for x in sortedPitches) + '}' return f'{self.offset} {enclosedNames}' # PUBLIC PROPERTIES # @property def bassTimespan(self): r''' Gets the bass timespan in this verticality. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(1.0) >>> verticality <music21.tree.verticality.Verticality 1.0 {F#3 C#4 F#4 A4}> >>> verticality.bassTimespan <PitchedTimespan (1.0 to 2.0) <music21.note.Note F#>> ''' overallLowestPitch = None lowestTimespan = None for ts in self.startAndOverlapTimespans: if not hasattr(ts, 'pitches'): continue tsPitches = ts.pitches if not tsPitches: continue lowestPitch = sorted(tsPitches)[0] if overallLowestPitch is None: overallLowestPitch = lowestPitch lowestTimespan = ts if lowestPitch <= overallLowestPitch: overallLowestPitch = lowestPitch lowestTimespan = ts return lowestTimespan @property def beatStrength(self): r''' Gets the beat strength of a verticality. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(1.0) >>> verticality.beatStrength 1.0 Note that it will return None if there are no startTimespans at this point: >>> verticality = scoreTree.getVerticalityAt(1.25) >>> verticality <music21.tree.verticality.Verticality 1.25 {F#3 C#4 F#4 A4}> >>> verticality.startTimespans () >>> verticality.beatStrength is None True ''' try: thisTimespan = self.startTimespans[0] except IndexError: return None return thisTimespan.element.beatStrength
[docs] def toChord(self): ''' Creates a chord.Chord object of default length (1.0 or the duration of some note object) from the verticality. Does nothing about ties, etc. -- it's a very dumb chord, but useful for querying consonance, etc. See makeElement() for the smart version. It may be a zero- or one-pitch chord. >>> score = corpus.parse('bwv66.6') >>> scoreTree = score.asTimespans() >>> verticality = scoreTree.getVerticalityAt(4.0) >>> verticality.toChord() <music21.chord.Chord G#3 B3 E4 E5> ''' c = chord.Chord(sorted(self.pitchSet)) return c
@property def measureNumber(self): r''' Gets the measure number of the verticality's starting elements. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(7.0) >>> verticality.measureNumber 2 ''' return self.startTimespans[0].measureNumber @property def nextStartOffset(self) -> float|None: r''' Gets the next start-offset in the verticality's offset-tree. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(1.0) >>> verticality.nextStartOffset 2.0 If a verticality has no tree attached, then it will return None ''' tree = self.offsetTree if tree is None: return None offset = tree.getPositionAfter(self.offset) return offset @property def nextVerticality(self): r''' Gets the next verticality after a verticality. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(1.0) >>> print(verticality) <music21.tree.verticality.Verticality 1.0 {F#3 C#4 F#4 A4}> >>> nextVerticality = verticality.nextVerticality >>> print(nextVerticality) <music21.tree.verticality.Verticality 2.0 {G#3 B3 E4 B4}> Verticality objects created by an offset-tree hold a reference back to that offset-tree. This means that they determine their next or previous verticality dynamically based on the state of the offset-tree only when asked. Because of this, it is safe to mutate the offset-tree by inserting or removing timespans while iterating over it. >>> scoreTree.removeTimespanList(nextVerticality.startTimespans) >>> verticality.nextVerticality <music21.tree.verticality.Verticality 3.0 {A3 E4 C#5}> ''' tree = self.offsetTree if tree is None: return None offset = tree.getPositionAfter(self.offset) if offset is None: return None return tree.getVerticalityAt(offset) @property def pitchSet(self): r''' Gets the pitch set of all elements in a verticality. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(1.0) >>> for pitch in sorted(verticality.pitchSet): ... pitch ... <music21.pitch.Pitch F#3> <music21.pitch.Pitch C#4> <music21.pitch.Pitch F#4> <music21.pitch.Pitch A4> ''' pitchNameSet = set() pitchSet = set() for timespan in self.startAndOverlapTimespans: if not hasattr(timespan, 'pitches'): continue for p in timespan.pitches: pName = p.nameWithOctave if pName in pitchNameSet: continue pitchNameSet.add(pName) pitchSet.add(p) return pitchSet @property def pitchClassSet(self): # noinspection PyShadowingNames r''' Gets a set of all pitches in a verticality with distinct pitchClasses >>> n1 = note.Note('C4') >>> n2 = note.Note('B#5') >>> s = stream.Stream() >>> s.insert(4.0, n1) >>> s.insert(4.0, n2) >>> scoreTree = s.asTimespans() >>> verticality = scoreTree.getVerticalityAt(4.0) >>> pitchSet = verticality.pitchSet >>> list(sorted(pitchSet)) [<music21.pitch.Pitch C4>, <music21.pitch.Pitch B#5>] PitchClassSet will return only one pitch. Which of these is returned is arbitrary. >>> pitchClassSet = verticality.pitchClassSet >>> #_DOCS_SHOW list(sorted(pitchClassSet)) >>> print('[<music21.pitch.Pitch B#5>]') #_DOCS_HIDE [<music21.pitch.Pitch B#5>] ''' outPitchSet = set() pitchClassSet = set() for currentPitch in self.pitchSet: pitchClass = currentPitch.pitchClass if pitchClass in pitchClassSet: continue pitchClassSet.add(pitchClass) outPitchSet.add(currentPitch) return outPitchSet @property def previousVerticality(self): r''' Gets the previous verticality before a verticality. >>> score = corpus.parse('bwv66.6') >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(1.0) >>> print(verticality) <music21.tree.verticality.Verticality 1.0 {F#3 C#4 F#4 A4}> >>> previousVerticality = verticality.previousVerticality >>> print(previousVerticality) <music21.tree.verticality.Verticality 0.5 {G#3 B3 E4 B4}> Continue it: >>> v = scoreTree.getVerticalityAt(1.0) >>> while v is not None: ... print(v) ... v = v.previousVerticality <music21.tree.verticality.Verticality 1.0 {F#3 C#4 F#4 A4}> <music21.tree.verticality.Verticality 0.5 {G#3 B3 E4 B4}> <music21.tree.verticality.Verticality 0.0 {A3 E4 C#5}> Verticality objects created by an offset-tree hold a reference back to that offset-tree. This means that they determine their next or previous verticality dynamically based on the state of the offset-tree only when asked. Because of this, it is safe to mutate the offset-tree by inserting or removing timespans while iterating over it. >>> scoreTree.removeTimespanList(previousVerticality.startTimespans) >>> verticality.previousVerticality <music21.tree.verticality.Verticality 0.0 {A3 E4 C#5}> ''' tree = self.offsetTree if tree is None: return None offset = tree.getPositionBefore(self.offset) if offset is None: return None return tree.getVerticalityAt(offset) @property def startAndOverlapTimespans(self): ''' Return a tuple adding the start and overlap timespans into one. >>> n1 = note.Note('C4') >>> n2 = note.Note('D4') >>> s = stream.Stream() >>> s.insert(4.0, n1) >>> s.insert(4.5, n2) >>> scoreTree = s.asTimespans() >>> verticality = scoreTree.getVerticalityAt(4.5) >>> verticality.startTimespans (<PitchedTimespan (4.5 to 5.5) <music21.note.Note D>>,) >>> verticality.overlapTimespans (<PitchedTimespan (4.0 to 5.0) <music21.note.Note C>>,) >>> verticality.startAndOverlapTimespans (<PitchedTimespan (4.5 to 5.5) <music21.note.Note D>>, <PitchedTimespan (4.0 to 5.0) <music21.note.Note C>>) >>> verticality = scoreTree.getVerticalityAt(4.0) >>> verticality.startAndOverlapTimespans (<PitchedTimespan (4.0 to 5.0) <music21.note.Note C>>,) ''' if self.overlapTimespans is None: return tuple(self.startTimespans) return tuple(self.startTimespans[:] + self.overlapTimespans[:]) @property def timeToNextEvent(self) -> OffsetQL|None: ''' Returns a float or Fraction of the quarterLength to the next event (usually the next Verticality, but also to the end of the piece). Returns None if there is no next event, such as when the verticality is divorced from its tree. ''' nextOffset = self.nextStartOffset if nextOffset is None: if self.timespanTree is None: return None nextOffset = self.timespanTree.endTime return common.opFrac(nextOffset - self.offset) # makeElement
[docs] def makeElement( self, quarterLength: OffsetQLIn|None = None, *, addTies=True, addPartIdAsGroup=False, removeRedundantPitches=True, gatherArticulations='single', gatherExpressions='single', copyPitches=True, ) -> note.Rest|chord.Chord: # noinspection PyDunderSlots, PyShadowingNames r''' Makes a Chord or Rest from this verticality and quarterLength. >>> score = tree.examples.makeExampleScore() >>> scoreTree = tree.fromStream.asTimespans(score, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality = scoreTree.getVerticalityAt(4.0) >>> verticality <music21.tree.verticality.Verticality 4.0 {E#3 G3}> >>> verticality.startTimespans (<PitchedTimespan (4.0 to 5.0) <music21.note.Note G>>, <PitchedTimespan (4.0 to 6.0) <music21.note.Note E#>>) >>> el = verticality.makeElement(2.0) >>> el <music21.chord.Chord E#3 G3> >>> el.duration.quarterLength 2.0 >>> el.duration.type 'half' If there is nothing there, then a Rest is created >>> verticality = scoreTree.getVerticalityAt(400.0) >>> verticality <music21.tree.verticality.Verticality 400.0 {}> >>> el = verticality.makeElement(1/3) >>> el <music21.note.Rest 1/3ql> >>> el.duration.fullName 'Eighth Triplet (1/3 QL)' >>> n1 = note.Note('C4') >>> n2 = note.Note('C4') >>> s = stream.Score() >>> s.insert(0, n1) >>> s.insert(0.5, n2) >>> scoreTree = s.asTimespans() >>> verticality = scoreTree.getVerticalityAt(0.5) >>> c = verticality.makeElement(0.5) >>> c <music21.chord.Chord C4> >>> c = verticality.makeElement(0.5, removeRedundantPitches=False) >>> c <music21.chord.Chord C4 C4> Generally the pitches of the new element are not connected to the original pitch: >>> c[0].pitch.name = 'E' >>> c[1].pitch.name = 'F' >>> (n1.name, n2.name) ('C', 'C') But if `copyPitches` is False then the original pitch will be used: >>> n1.name = 'D' >>> n2.name = 'E' >>> c = verticality.makeElement(0.5, removeRedundantPitches=False, copyPitches=False) >>> c <music21.chord.Chord D4 E4> >>> c[0].pitch.name = 'F' >>> c[1].pitch.name = 'G' >>> (n1.name, n2.name) ('F', 'G') gatherArticulations and gatherExpressions can be True, False, or (default) 'single'. * If False, no articulations (or expressions) are transferred to the chord. * If True, all articulations are transferred to the chord. * If 'single', then no more than one articulation of each class (chosen from the lowest note) will be added. This way, the chord does not get 4 fermatas, etc. >>> n1 = note.Note('C4') >>> n2 = note.Note('D4') >>> s = stream.Stream() >>> s.insert(0, n1) >>> s.insert(0.5, n2) >>> class AllAttachArticulation(articulations.Articulation): ... def __init__(self): ... super().__init__() ... self.tieAttach = 'all' >>> class OtherAllAttachArticulation(articulations.Articulation): ... def __init__(self): ... super().__init__() ... self.tieAttach = 'all' >>> n1.articulations.append(articulations.Accent()) >>> n1.articulations.append(AllAttachArticulation()) >>> n1.expressions.append(expressions.Fermata()) >>> n2.articulations.append(articulations.Staccato()) >>> n2.articulations.append(AllAttachArticulation()) >>> n2.articulations.append(OtherAllAttachArticulation()) >>> n2.expressions.append(expressions.Fermata()) >>> scoreTree = s.asTimespans() >>> verticality = scoreTree.getVerticalityAt(0.0) >>> c = verticality.makeElement(1.0) >>> c.expressions [<music21.expressions.Fermata>] >>> c.articulations [<music21.articulations.Accent>, <...AllAttachArticulation>] >>> verticality = scoreTree.getVerticalityAt(0.5) Here there will be no expressions, because there is no note ending at 0.75 and Fermatas attach to the last note: >>> c = verticality.makeElement(0.25) >>> c.expressions [] >>> c = verticality.makeElement(0.5) >>> c.expressions [<music21.expressions.Fermata>] Only two articulations, since accent attaches to beginning and staccato attaches to last and we are beginning after the start of the first note (with an accent) and cutting right through the second note (with a staccato) >>> c.articulations [<...AllAttachArticulation>, <...OtherAllAttachArticulation>] >>> c = verticality.makeElement(0.5, gatherArticulations=True) >>> c.articulations [<...AllAttachArticulation>, <...AllAttachArticulation>, <...OtherAllAttachArticulation>] >>> c = verticality.makeElement(0.5, gatherArticulations=False) >>> c.articulations [] >>> verticality = scoreTree.getVerticalityAt(1.0) >>> c = verticality.makeElement(0.5) >>> c.expressions [<music21.expressions.Fermata>] >>> c.articulations [<music21.articulations.Staccato>, <...AllAttachArticulation>, <...OtherAllAttachArticulation>] * New in v6.3: copyPitches option. OMIT_FROM_DOCS Test that copyPitches works with expressions: >>> c = verticality.makeElement(0.5, copyPitches=False) >>> c <music21.chord.Chord D4> >>> c.pitches[0].accidental = pitch.Accidental('sharp') >>> n2 <music21.note.Note D#> * Changed in v7.3: if quarterLength is not given, the duration to the next quarterLength is used. ''' if quarterLength is None: event_duration = self.timeToNextEvent if event_duration is None: quarterLength = 1.0 else: quarterLength = event_duration # already opFrac else: quarterLength = common.opFrac(quarterLength) if not self.pitchSet: r = note.Rest() r.duration.quarterLength = quarterLength return r # easy stuff done, time to get to the hard stuff c = chord.Chord() c.duration.quarterLength = quarterLength dur = c.duration seenPitches = set() notesToAdd = {} startStopSet = {'start', 'stop'} pitchBust = 0 # used if removeRedundantPitches is False. # noinspection PyShadowingNames def newNote(ts: spans.PitchedTimespan, n: note.Note) -> note.Note: ''' Make a copy of the note and clear some settings ''' nNew = copy.deepcopy(n) nNew.duration = dur if not copyPitches: nNew.pitch = n.pitch if nNew.stemDirection != 'noStem': nNew.stemDirection = 'unspecified' if not addTies: return nNew offsetDifference = common.opFrac(self.offset - ts.offset) if t.TYPE_CHECKING: assert quarterLength is not None endTimeDifference = common.opFrac(ts.endTime - (self.offset + quarterLength)) if t.TYPE_CHECKING: assert endTimeDifference is not None # noinspection PyTypeChecker if offsetDifference == 0 and endTimeDifference <= 0: addTie = None elif offsetDifference > 0: # noinspection PyTypeChecker if endTimeDifference > 0: addTie = 'continue' else: addTie = 'stop' elif endTimeDifference > 0: addTie = 'start' else: raise VerticalityException('What possibility was missed?', offsetDifference, endTimeDifference, ts, self) if nNew.tie is not None and {nNew.tie.type, addTie} == startStopSet: nNew.tie.type = 'continue' elif nNew.tie is not None and nNew.tie.type == 'continue': nNew.tie.placement = None elif addTie is None and nNew.tie is not None: nNew.tie.placement = None elif addTie: nNew.tie = tie.Tie(addTie) return nNew # noinspection PyShadowingNames def conditionalAdd(ts: spans.PitchedTimespan, n: note.Note) -> None: ''' Add an element only if it is not already in the chord. If it has more tie information than the previously added note, then remove the previously added note and add it ''' from music21 import stream nonlocal pitchBust # love Py3!!! p = n.pitch pitchKey = p.nameWithOctave pitchGroup = None if addPartIdAsGroup: partContext = n.getContextByClass(stream.Part) if partContext is not None: pidStr = str(partContext.id) pitchGroup = pidStr.replace(' ', '_') # spaces are not allowed as group names n.pitch.groups.append(pitchGroup) n.groups.append(pitchGroup) if pitchKey not in seenPitches: seenPitches.add(pitchKey) notesToAdd[pitchKey] = newNote(ts, n) return elif not removeRedundantPitches: notesToAdd[pitchKey + str(pitchBust)] = newNote(ts, n) pitchBust += 1 return elif addPartIdAsGroup and pitchGroup is not None: notesToAdd[pitchKey].groups.append(pitchGroup) notesToAdd[pitchKey].pitch.groups.append(pitchGroup) if not addTies: return # else add derivation once multiple derivations are allowed. oldNoteTie = notesToAdd[pitchKey].tie if oldNoteTie is not None and oldNoteTie.type == 'continue': return # previous note was as good or better possibleNewNote = newNote(ts, n) possibleNewNote.groups = notesToAdd[pitchKey].groups if possibleNewNote.tie is None: return # do nothing if oldNoteTie is None: notesToAdd[pitchKey] = possibleNewNote # a better note to add elif {oldNoteTie.type, possibleNewNote.tie.type} == startStopSet: other_t = notesToAdd[pitchKey].tie if other_t is not None: # this test is not needed, except for mypy. other_t.type = 'continue' elif possibleNewNote.tie.type == 'continue': notesToAdd[pitchKey] = possibleNewNote # a better note to add elif possibleNewNote.tie.type == oldNoteTie.type: return else: raise VerticalityException('Did I miss one? ', possibleNewNote.tie, oldNoteTie) for timeSpan in self.startAndOverlapTimespans: if not isinstance(timeSpan, spans.PitchedTimespan): continue el = timeSpan.element if isinstance(el, chord.ChordBase): firstNoteElement: note.Note | None = None for subEl in el: if isinstance(subEl, note.Note): firstNoteElement = subEl break if firstNoteElement is None: continue if el.articulations or el.expressions: # make a deepcopy of the first note element # adding all articulations and expressions from the Chord to it. originalPitch = firstNoteElement.pitch firstNoteElement = copy.deepcopy(firstNoteElement) firstNoteElement.articulations += el.articulations firstNoteElement.expressions += el.expressions if not copyPitches: firstNoteElement.pitch = originalPitch conditionalAdd(timeSpan, firstNoteElement) if len(el) > 1: for subEl in list(el)[1:]: if isinstance(subEl, note.Note): conditionalAdd(timeSpan, subEl) elif isinstance(el, note.Note): conditionalAdd(timeSpan, el) seenArticulations = set() seenExpressions = set() for n in sorted(notesToAdd.values(), key=lambda x: x.pitch.ps): c.add(n) if gatherArticulations: for art in n.articulations: if art.tieAttach == 'first' and n.tie is not None and n.tie.type != 'start': continue if art.tieAttach == 'last' and n.tie is not None and n.tie.type != 'stop': continue if gatherArticulations == 'single' and type(art) in seenArticulations: continue c.articulations.append(art) seenArticulations.add(type(art)) if gatherExpressions: for exp in n.expressions: if exp.tieAttach == 'first' and n.tie is not None and n.tie.type != 'start': continue if exp.tieAttach == 'last' and n.tie is not None and n.tie.type != 'stop': continue if gatherExpressions == 'single' and type(exp) in seenExpressions: continue c.expressions.append(exp) seenExpressions.add(type(exp)) return c
# Analysis type things @overload def getAllVoiceLeadingQuartets( self, *, includeRests=True, includeOblique=True, includeNoMotion=False, returnObjects: t.Literal[False], partPairNumbers: list[tuple[int, int]] | None = None ) -> list[PitchedTimespanQuartet]: ... @overload def getAllVoiceLeadingQuartets( self, *, includeRests=True, includeOblique=True, includeNoMotion=False, returnObjects: t.Literal[True] = True, partPairNumbers: list[tuple[int, int]] | None = None ) -> list[VoiceLeadingQuartet]: ...
[docs] def getAllVoiceLeadingQuartets( self, *, includeRests=True, includeOblique=True, includeNoMotion=False, returnObjects: bool = True, partPairNumbers: list[tuple[int, int]] | None = None ) -> list[VoiceLeadingQuartet] | list[PitchedTimespanQuartet]: # noinspection PyShadowingNames,PyCallingNonCallable ''' >>> c = corpus.parse('luca/gloria').measures(1, 8) >>> tsCol = tree.fromStream.asTimespans(c, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality22 = tsCol.getVerticalityAt(22.0) >>> from pprint import pprint as pp >>> for vlq in verticality22.getAllVoiceLeadingQuartets(): ... pp(vlq) <music21.voiceLeading.VoiceLeadingQuartet v1n1=G4, v1n2=C4, v2n1=E4, v2n2=F4> <music21.voiceLeading.VoiceLeadingQuartet v1n1=G4, v1n2=C4, v2n1=A3, v2n2=A3> <music21.voiceLeading.VoiceLeadingQuartet v1n1=E4, v1n2=F4, v2n1=A3, v2n2=A3> >>> for vlq in verticality22.getAllVoiceLeadingQuartets(includeRests=False): ... pp(vlq) <music21.voiceLeading.VoiceLeadingQuartet v1n1=E4, v1n2=F4, v2n1=A3, v2n2=A3> >>> for vlq in verticality22.getAllVoiceLeadingQuartets(includeOblique=False): ... pp(vlq) <music21.voiceLeading.VoiceLeadingQuartet v1n1=G4, v1n2=C4, v2n1=E4, v2n2=F4> >>> verticality22.getAllVoiceLeadingQuartets(includeOblique=False, includeRests=False) [] Raw output, returns a 2-element tuple of 2-element tuples of PitchedTimespans >>> for vlqRaw in verticality22.getAllVoiceLeadingQuartets(returnObjects=False): ... pp(vlqRaw) ((<PitchedTimespan (19.5 to 21.0) <music21.note.Note G>>, <PitchedTimespan (22.0 to 22.5) <music21.note.Note C>>), (<PitchedTimespan (21.0 to 22.0) <music21.note.Note E>>, <PitchedTimespan (22.0 to 23.0) <music21.note.Note F>>)) ((<PitchedTimespan (19.5 to 21.0) <music21.note.Note G>>, <PitchedTimespan (22.0 to 22.5) <music21.note.Note C>>), (<PitchedTimespan (21.5 to 22.5) <music21.note.Note A>>, <PitchedTimespan (21.5 to 22.5) <music21.note.Note A>>)) ((<PitchedTimespan (21.0 to 22.0) <music21.note.Note E>>, <PitchedTimespan (22.0 to 23.0) <music21.note.Note F>>), (<PitchedTimespan (21.5 to 22.5) <music21.note.Note A>>, <PitchedTimespan (21.5 to 22.5) <music21.note.Note A>>)) >>> for vlq in verticality22.getAllVoiceLeadingQuartets(partPairNumbers=[(0, 1)]): ... pp(vlq) <music21.voiceLeading.VoiceLeadingQuartet v1n1=G4, v1n2=C4, v2n1=E4, v2n2=F4> >>> for vlq in verticality22.getAllVoiceLeadingQuartets(partPairNumbers=[(0, 2), (1, 2)]): ... pp(vlq) <music21.voiceLeading.VoiceLeadingQuartet v1n1=G4, v1n2=C4, v2n1=A3, v2n2=A3> <music21.voiceLeading.VoiceLeadingQuartet v1n1=E4, v1n2=F4, v2n1=A3, v2n2=A3> * Changed in v8: all parameters are keyword only. ''' if not self.timespanTree: raise VerticalityException('Cannot iterate without .timespanTree defined') from music21.voiceLeading import VoiceLeadingQuartet pairedMotionList: list[ tuple[spans.PitchedTimespan, spans.PitchedTimespan] ] = self.getPairedMotion( includeRests=includeRests, includeOblique=includeOblique ) allPairedMotion = itertools.combinations(pairedMotionList, 2) filteredList: list[PitchedTimespanQuartet] = [] filteredQuartet: list[VoiceLeadingQuartet] = [] verticalityStreamParts = self.timespanTree.source.parts pairedMotion: PitchedTimespanQuartet for pairedMotion in allPairedMotion: if not hasattr(pairedMotion[0][0], 'pitches'): continue # not a PitchedTimespan if includeNoMotion is False: if (pairedMotion[0][0].pitches == pairedMotion[0][1].pitches and pairedMotion[1][0].pitches == pairedMotion[1][1].pitches): continue if partPairNumbers is not None: isAppropriate = False for pp in partPairNumbers: thisQuartetTopPart = pairedMotion[0][0].part thisQuartetBottomPart = pairedMotion[1][0].part if ((verticalityStreamParts[pp[0]] == thisQuartetTopPart or verticalityStreamParts[pp[0]] == thisQuartetBottomPart) and (verticalityStreamParts[pp[1]] == thisQuartetTopPart or verticalityStreamParts[pp[1]] == thisQuartetBottomPart)): isAppropriate = True break if not isAppropriate: continue if returnObjects is False: filteredList.append(pairedMotion) else: n11 = pairedMotion[0][0].element n12 = pairedMotion[0][1].element n21 = pairedMotion[1][0].element n22 = pairedMotion[1][1].element # fail on Chords for now. if (isinstance(n11, note.Note) and isinstance(n12, note.Note) and isinstance(n21, note.Note) and isinstance(n22, note.Note)): vlq = VoiceLeadingQuartet(n11, n12, n21, n22) filteredQuartet.append(vlq) if returnObjects: return filteredQuartet return filteredList
[docs] def getPairedMotion( self, *, includeRests: bool = True, includeOblique: bool = True ) -> list[tuple[spans.PitchedTimespan, spans.PitchedTimespan]]: ''' Get a list of two-element tuples that are in the same part [TODO: or containing stream??] and which move at this verticality. >>> c = corpus.parse('luca/gloria').measures(1, 8) >>> tsCol = tree.fromStream.asTimespans(c, flatten=True, ... classList=(note.Note, chord.Chord)) >>> verticality22 = tsCol.getVerticalityAt(22.0) >>> for pm in verticality22.getPairedMotion(): ... print(pm) (<PitchedTimespan (19.5 to 21.0) <music21.note.Note G>>, <PitchedTimespan (22.0 to 22.5) <music21.note.Note C>>) (<PitchedTimespan (21.0 to 22.0) <music21.note.Note E>>, <PitchedTimespan (22.0 to 23.0) <music21.note.Note F>>) (<PitchedTimespan (21.5 to 22.5) <music21.note.Note A>>, <PitchedTimespan (21.5 to 22.5) <music21.note.Note A>>) Note that the second pair contains a one-beat rest at 21.0-22.0; so `includeRests=False` will get rid of that: >>> for pm in verticality22.getPairedMotion(includeRests=False): ... print(pm) (<PitchedTimespan (21.0 to 22.0) <music21.note.Note E>>, <PitchedTimespan (22.0 to 23.0) <music21.note.Note F>>) (<PitchedTimespan (21.5 to 22.5) <music21.note.Note A>>, <PitchedTimespan (21.5 to 22.5) <music21.note.Note A>>) Oblique here means a pair that does not move (it could be called noMotion, because there's no motion here in a two-note pair, but we still call it includeOblique so it's consistent with getAllVoiceLeadingQuartets). >>> for pm in verticality22.getPairedMotion(includeOblique=False): ... print(pm) (<PitchedTimespan (19.5 to 21.0) <music21.note.Note G>>, <PitchedTimespan (22.0 to 22.5) <music21.note.Note C>>) (<PitchedTimespan (21.0 to 22.0) <music21.note.Note E>>, <PitchedTimespan (22.0 to 23.0) <music21.note.Note F>>) >>> for pm in verticality22.getPairedMotion(includeOblique=False, includeRests=False): ... print(pm) (<PitchedTimespan (21.0 to 22.0) <music21.note.Note E>>, <PitchedTimespan (22.0 to 23.0) <music21.note.Note F>>) Changed in v9.3 -- arguments are keyword only ''' if not self.timespanTree: return [] stopTss = self.stopTimespans startTss = self.startTimespans overlapTss = self.overlapTimespans allPairedMotions: list[tuple[spans.PitchedTimespan, spans.PitchedTimespan]] = [] for startingTs in startTss: if not isinstance(startingTs, spans.PitchedTimespan): continue previousTs = self.timespanTree.findPreviousPitchedTimespanInSameStreamByClass( startingTs) if previousTs is None or not isinstance(previousTs, spans.PitchedTimespan): continue # first not in piece in this part if includeRests is False: if previousTs not in stopTss: continue if includeOblique is False and startingTs.pitches == previousTs.pitches: continue tsTuple = (previousTs, startingTs) allPairedMotions.append(tsTuple) if includeOblique is True: for overlapTs in overlapTss: if not isinstance(overlapTs, spans.PitchedTimespan): continue tsTuple = (overlapTs, overlapTs) allPairedMotions.append(tsTuple) return allPairedMotions
# -----------------------------------------------------------------------------
[docs] class VerticalitySequence(prebase.ProtoM21Object, Sequence[Verticality]): r''' A segment of verticalities. ''' # INITIALIZER # def __init__(self, verticalities: Iterable[Verticality]): self._verticalities = tuple(verticalities) # SPECIAL METHODS #
[docs] def __getitem__(self, item): return self._verticalities[item]
def __len__(self): return len(self._verticalities) # noinspection PyProtectedMember def _reprInternal(self): internalRepr = ',\n\t'.join('(' + x._reprInternal() + ')' for x in self) out = f'[\n\t{internalRepr}\n\t]' return out # PUBLIC METHODS #
[docs] def unwrap(self): from music21.tree.analysis import Horizontality unwrapped = {} for timespan in self[0].overlapTimespans: if timespan.part not in unwrapped: unwrapped[timespan.part] = [] unwrapped[timespan.part].append(timespan) for timespan in self[0].startTimespans: if timespan.part not in unwrapped: unwrapped[timespan.part] = [] unwrapped[timespan.part].append(timespan) for verticality in self[1:]: for timespan in verticality.startTimespans: if timespan.part not in unwrapped: unwrapped[timespan.part] = [] unwrapped[timespan.part].append(timespan) for part, timespans in unwrapped.items(): horizontality = Horizontality(timespans=timespans) unwrapped[part] = horizontality return unwrapped
# ----------------------------------------------------------------------------- class Test(unittest.TestCase): pass # ----------------------------------------------------------------------------- _DOC_ORDER = (Verticality, VerticalitySequence) # ----------------------------------------------------------------------------- if __name__ == '__main__': import music21 music21.mainTest(Test)