# ----------------------------------------------------------------------------
# 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)