# ------------------------------------------------------------------------------
# Name: variant.py
# Purpose: Ossia and other variant reading tools
#
# Authors: Christopher Ariza
# Evan Lynch
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2012 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
# currently the tinyNotation demos use alignment to show variation, making this necessary.
# pylint: disable=line-too-long
# all other lines are linted.
'''
Contains :class:`~music21.variant.Variant` and its subclasses, as well as functions for merging
and showing different variant streams. These functions and the variant class should only be
used when variants of a score are the same length and contain the same measure structure at
this time.
'''
from __future__ import annotations
from collections.abc import Sequence
import copy
import difflib
import unittest
import warnings
from music21 import base
from music21 import clef
from music21 import common
from music21 import environment
from music21 import exceptions21
from music21 import meter
from music21 import note
from music21 import search
from music21 import stream
from music21.stream.enums import GivenElementsBehavior
environLocal = environment.Environment('variant')
# ------------------------------------------------------------------------------
# classes
class VariantException(exceptions21.Music21Exception):
pass
[docs]
class Variant(base.Music21Object):
'''
A Music21Object that stores elements like a Stream, but does not
represent itself externally to a Stream; i.e., the contents of a Variant are not flattened.
This is accomplished not by subclassing, but by object composition: similar to the Spanner,
the Variant contains a Stream as a private attribute. Calls to this Stream, for the Variant,
are automatically delegated by use of the __getattr__ method. Special cases are overridden
or managed as necessary: e.g., the Duration of a Variant is generally always zero.
To use Variants from a Stream, see the :func:`~music21.stream.Stream.activateVariants` method.
>>> v = variant.Variant()
>>> v.repeatAppend(note.Note(), 8)
>>> len(v.notes)
8
>>> v.highestTime
0.0
>>> v.containedHighestTime
8.0
>>> v.duration # handled by Music21Object
<music21.duration.Duration 0.0>
>>> v.isStream
False
>>> s = stream.Stream()
>>> s.append(v)
>>> s.append(note.Note())
>>> s.highestTime
1.0
>>> s.show('t')
{0.0} <music21.variant.Variant object of length 8.0>
{0.0} <music21.note.Note C>
>>> s.flatten().show('t')
{0.0} <music21.variant.Variant object of length 8.0>
{0.0} <music21.note.Note C>
'''
classSortOrder = stream.Stream.classSortOrder - 2 # variants should always come first?
# this copies the init of Streams
def __init__(
self,
givenElements: (
None
| base.Music21Object
| Sequence[base.Music21Object]
) = None,
name: str|None = None,
givenElementsBehavior: GivenElementsBehavior = GivenElementsBehavior.OFFSETS,
**music21ObjectKeywords,
):
super().__init__(**music21ObjectKeywords)
self.exposeTime = False
self._stream = stream.VariantStorage(givenElements=givenElements,
givenElementsBehavior=givenElementsBehavior)
self._replacementQuarterLength = None
if name is not None:
self.groups.append(name)
# --------------------------------------------------------------------------
# as _stream is a private Stream, unwrap/wrap methods need to override
# Music21Object to get at these objects
# this is the same as with Spanners
[docs]
def purgeOrphans(self, excludeStorageStreams=True):
self._stream.purgeOrphans(excludeStorageStreams)
base.Music21Object.purgeOrphans(self, excludeStorageStreams)
[docs]
def purgeLocations(self, rescanIsDead=False):
# must override Music21Object to purge locations from the contained
self._stream.purgeLocations(rescanIsDead=rescanIsDead)
base.Music21Object.purgeLocations(self, rescanIsDead=rescanIsDead)
def _reprInternal(self):
return 'object of length ' + str(self.containedHighestTime)
def __getattr__(self, attr):
'''
This defers all calls not defined in this Class to calls on the privately contained Stream.
'''
# environLocal.printDebug(['relaying unmatched attribute request '
# + attr + ' to private Stream'])
# must mask pitches so as not to recurse
# TODO: check that recurse does not go into this
if attr in ['flat', 'pitches']:
raise AttributeError
# needed for unpickling where ._stream doesn't exist until later
if attr != '_stream' and hasattr(self, '_stream'):
return getattr(self._stream, attr)
else:
raise AttributeError
[docs]
def __getitem__(self, key):
return self._stream.__getitem__(key)
def __len__(self):
return len(self._stream)
def __iter__(self):
return self._stream.__iter__()
[docs]
def getElementIds(self):
if 'elementIds' not in self._cache or self._cache['elementIds'] is None:
self._cache['elementIds'] = [id(c) for c in self._stream._elements]
return self._cache['elementIds']
[docs]
def replaceElement(self, old, new):
'''
When copying a Variant, we need to update the Variant with new
references for copied elements. Given the old element,
this method will replace the old with the new.
The `old` parameter can be either an object or object id.
This method is very similar to the replaceSpannedElement method on Spanner.
'''
if old is None:
return None # do nothing
if common.isNum(old):
# this must be id(obj), not obj.id
e = self._stream.coreGetElementByMemoryLocation(old)
if e is not None:
self._stream.replace(e, new, allDerived=False)
else:
# do not do all Sites: only care about this one
self._stream.replace(old, new, allDerived=False)
# --------------------------------------------------------------------------
# Stream simulation/overrides
@property
def highestTime(self):
'''
This property masks calls to Stream.highestTime. Assuming `exposeTime`
is False, this always returns zero, making the Variant always take zero time.
>>> v = variant.Variant()
>>> v.append(note.Note(quarterLength=4))
>>> v.highestTime
0.0
'''
if self.exposeTime:
return self._stream.highestTime
else:
return 0.0
@property
def highestOffset(self):
'''
This property masks calls to Stream.highestOffset. Assuming `exposeTime`
is False, this always returns zero, making the Variant always take zero time.
>>> v = variant.Variant()
>>> v.append(note.Note(quarterLength=4))
>>> v.highestOffset
0.0
'''
if self.exposeTime:
return self._stream.highestOffset
else:
return 0.0
[docs]
def show(self, fmt=None, app=None):
'''
Call show() on the Stream contained by this Variant.
This method must be overridden, otherwise Music21Object.show() is called.
>>> v = variant.Variant()
>>> v.repeatAppend(note.Note(quarterLength=0.25), 8)
>>> v.show('t')
{0.0} <music21.note.Note C>
{0.25} <music21.note.Note C>
{0.5} <music21.note.Note C>
{0.75} <music21.note.Note C>
{1.0} <music21.note.Note C>
{1.25} <music21.note.Note C>
{1.5} <music21.note.Note C>
{1.75} <music21.note.Note C>
'''
self._stream.show(fmt=fmt, app=app)
# --------------------------------------------------------------------------
# properties particular to this class
@property
def containedHighestTime(self):
'''
This property calls the contained Stream.highestTime.
>>> v = variant.Variant()
>>> v.append(note.Note(quarterLength=4))
>>> v.containedHighestTime
4.0
'''
return self._stream.highestTime
@property
def containedHighestOffset(self):
'''
This property calls the contained Stream.highestOffset.
>>> v = variant.Variant()
>>> v.append(note.Note(quarterLength=4))
>>> v.append(note.Note())
>>> v.containedHighestOffset
4.0
'''
return self._stream.highestOffset
@property
def containedSite(self):
'''
Return the Stream contained in this Variant.
'''
return self._stream
def _getReplacementQuarterLength(self):
if self._replacementQuarterLength is None:
return self._stream.duration.quarterLength
else:
return self._replacementQuarterLength
def _setReplacementQuarterLength(self, value):
self._replacementQuarterLength = value
replacementQuarterLength = property(
_getReplacementQuarterLength,
_setReplacementQuarterLength,
doc='''
Set or Return the quarterLength in the main stream which this variant
object replaces in the variant version of the stream. If replacementQuarterLength is
not set, it is assumed to be the same length as the variant. If it is set to 0,
the variant should be interpreted as an insertion. Setting replacementQuarterLength
to None will return the value to the default which is the length of the variant
itself.
* New in v10.3: renamed from ``replacementDuration``.
''')
def _getReplacementDuration(self):
return self.replacementQuarterLength
def _setReplacementDuration(self, value):
warnings.warn(
"'replacementDuration' is deprecated as of v11 and will be removed in v12; "
+ 'use the synonym replacementQuarterLength instead.',
exceptions21.Music21DeprecationWarning,
stacklevel=2)
self.replacementQuarterLength = value
replacementDuration = property(
_getReplacementDuration,
_setReplacementDuration,
doc='''
Synonym for :attr:`replacementQuarterLength`.
.. note::
``replacementDuration`` is deprecated as of v11 (music21
avoids referring to an offset/quarterLength value as a "Duration").
Setting it raises a deprecation warning, and it will be removed in v12. Use
:attr:`replacementQuarterLength` instead.
''')
@property
def lengthType(self):
'''
Returns 'deletion' if variant is shorter than the region it replaces, 'elongation'
if the variant is longer than the region it replaces, and 'replacement' if it is
the same length.
'''
lengthDifference = self.replacementQuarterLength - self.containedHighestTime
if lengthDifference > 0.0:
return 'deletion'
elif lengthDifference < 0.0:
return 'elongation'
else:
return 'replacement'
[docs]
def replacedElements(self, contextStream=None, classList=None,
keepOriginalOffsets=False, includeSpacers=False):
# noinspection PyShadowingNames
'''
Returns a Stream containing the elements which this variant replaces in a
given context stream.
This Stream will have length self.replacementQuarterLength.
In regions that are strictly replaced, only elements that share a class with
an element in the variant
are captured. Elsewhere, all elements are captured.
>>> s = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2 b-4 a4 g4 a8 g8 f4 e4 d2 a2 d4 e4 f4 g4 a2 b-4 a4 g4 a8 b-8 c'4 c4 f1", makeNotation=False)
>>> s.makeMeasures(inPlace=True)
>>> v1stream = converter.parse("tinynotation: 4/4 a2. b-8 a8", makeNotation=False)
>>> v2stream1 = converter.parse("tinynotation: 4/4 d4 f4 a2", makeNotation=False)
>>> v2stream2 = converter.parse("tinynotation: 4/4 d4 f4 AA2", makeNotation=False)
>>> v1 = variant.Variant()
>>> v1measure = stream.Measure()
>>> v1.insert(0.0, v1measure)
>>> for e in v1stream.notesAndRests:
... v1measure.insert(e.offset, e)
>>> v2 = variant.Variant()
>>> v2measure1 = stream.Measure()
>>> v2measure2 = stream.Measure()
>>> v2.insert(0.0, v2measure1)
>>> v2.insert(4.0, v2measure2)
>>> for e in v2stream1.notesAndRests:
... v2measure1.insert(e.offset, e)
>>> for e in v2stream2.notesAndRests:
... v2measure2.insert(e.offset, e)
>>> v3 = variant.Variant()
>>> v2.replacementQuarterLength = 4.0
>>> v3.replacementQuarterLength = 4.0
>>> s.insert(4.0, v1) # replacement variant
>>> s.insert(12.0, v2) # insertion variant (2 bars replace 1 bar)
>>> s.insert(20.0, v3) # deletion variant (0 bars replace 1 bar)
>>> v1.replacedElements(s).show('text')
{0.0} <music21.stream.Measure 2 offset=0.0>
{0.0} <music21.note.Note A>
{2.0} <music21.note.Note B->
{3.0} <music21.note.Note A>
>>> v2.replacedElements(s).show('text')
{0.0} <music21.stream.Measure 4 offset=0.0>
{0.0} <music21.note.Note D>
{2.0} <music21.note.Note A>
>>> v3.replacedElements(s).show('text')
{0.0} <music21.stream.Measure 6 offset=0.0>
{0.0} <music21.note.Note A>
{2.0} <music21.note.Note B->
{3.0} <music21.note.Note A>
>>> v3.replacedElements(s, keepOriginalOffsets=True).show('text')
{20.0} <music21.stream.Measure 6 offset=20.0>
{0.0} <music21.note.Note A>
{2.0} <music21.note.Note B->
{3.0} <music21.note.Note A>
A second example:
>>> v = variant.Variant()
>>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
... ('a', 'quarter'),('b', 'quarter')]
>>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'),
... ('e', 'quarter'), ('e', 'quarter')]
>>> variantData = [variantDataM1, variantDataM2]
>>> for d in variantData:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... v.append(m)
>>> v.groups = ['paris']
>>> v.replacementQuarterLength = 4.0
>>> s = stream.Stream()
>>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')]
>>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'),
... ('a', 'eighth'), ('a', 'quarter'), ('b', 'quarter')]
>>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
>>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
>>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4]
>>> for d in streamData:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... s.append(m)
>>> s.insert(4.0, v)
>>> v.replacedElements(s).show('t')
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.note.Note B>
{0.5} <music21.note.Note C>
{1.5} <music21.note.Note A>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
'''
spacerFilter = lambda r: r.hasStyleInformation and r.style.hideObjectOnPrint
if contextStream is None:
contextStream = self.activeSite
if contextStream is None:
environLocal.printDebug(
'No contextStream or activeSite, finding most recently added site (dangerous)')
contextStream = self.getContextByClass('Stream')
if contextStream is None:
raise VariantException('Cannot find a Stream context for this object.')
if self not in contextStream.getElementsByClass(self.__class__):
raise VariantException(f'Variant not found in stream {contextStream}')
vStart = self.getOffsetBySite(contextStream)
if includeSpacers is True:
spacerDuration = (self
.getElementsByClass(note.Rest)
.addFilter(spacerFilter)
.first().duration.quarterLength)
else:
spacerDuration = 0.0
if self.lengthType in ('replacement', 'elongation'):
vEnd = vStart + self.replacementQuarterLength + spacerDuration
classes = []
for e in self.elements:
classes.append(e.classes[0])
if classList is not None:
classes.extend(classList)
returnStream = contextStream.getElementsByOffset(vStart, vEnd,
includeEndBoundary=False,
mustFinishInSpan=False,
mustBeginInSpan=True,
classList=classes).stream()
elif self.lengthType == 'deletion':
vMiddle = vStart + self.containedHighestTime
vEnd = vStart + self.replacementQuarterLength
classes = [] # collect all classes found in this variant
for e in self.elements:
classes.append(e.classes[0])
if classList is not None:
classes.extend(classList)
returnPart1 = contextStream.getElementsByOffset(vStart, vMiddle,
includeEndBoundary=False,
mustFinishInSpan=False,
mustBeginInSpan=True,
classList=classes).stream()
returnPart2 = contextStream.getElementsByOffset(vMiddle, vEnd,
includeEndBoundary=False,
mustFinishInSpan=False,
mustBeginInSpan=True).stream()
returnStream = returnPart1
for e in returnPart2.elements:
oInPart = e.getOffsetBySite(returnPart2)
returnStream.insert(vMiddle - vStart + oInPart, e)
else:
raise VariantException('lengthType must be replacement, elongation, or deletion')
if self in returnStream:
returnStream.remove(self)
# This probably makes sense to do, but activateVariants
# for example only uses the offset in the original.
# Also, we are not changing measure numbers and should
# not as that will cause activateVariants to fail.
if keepOriginalOffsets is False:
for e in returnStream:
e.setOffsetBySite(returnStream, e.getOffsetBySite(returnStream) - vStart)
return returnStream
[docs]
def removeReplacedElementsFromStream(self, referenceStream=None, classList=None):
'''
Remove replaced elements from a referenceStream or activeSite.
>>> v = variant.Variant()
>>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
... ('a', 'quarter'),('b', 'quarter')]
>>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
>>> variantData = [variantDataM1, variantDataM2]
>>> for d in variantData:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... v.append(m)
>>> v.groups = ['paris']
>>> v.replacementQuarterLength = 4.0
>>> s = stream.Stream()
>>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')]
>>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
... ('a', 'quarter'), ('b', 'quarter')]
>>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
>>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
>>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4]
>>> for d in streamData:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... s.append(m)
>>> s.insert(4.0, v)
>>> v.removeReplacedElementsFromStream(s)
>>> s.show('t')
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.note.Note A>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note G>
{4.0} <music21.variant.Variant object of length 8.0>
{8.0} <music21.stream.Measure 0 offset=8.0>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note A>
{12.0} <music21.stream.Measure 0 offset=12.0>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note A>
'''
if referenceStream is None:
referenceStream = self.activeSite
if referenceStream is None:
environLocal.printDebug('No referenceStream or activeSite, '
+ 'finding most recently added site (dangerous)')
referenceStream = self.getContextByClass('Stream')
if referenceStream is None:
raise VariantException('Cannot find a Stream context for this object.')
if self not in referenceStream.getElementsByClass(self.__class__):
raise VariantException(f'Variant not found in stream {referenceStream}')
replacedElements = self.replacedElements(referenceStream, classList)
for el in replacedElements:
referenceStream.remove(el)
# ------Public Merge Functions
[docs]
def mergeVariants(streamX, streamY, variantName='variant', *, inPlace=False):
# noinspection PyShadowingNames
'''
Takes two streams objects or their derivatives (Score, Part, Measure, etc.) which
should be variant versions of the same stream,
and merges them (determines differences and stores those differences as variant objects
in streamX) via the appropriate merge
function for their type. This will not know how to deal with scores meant for
mergePartAsOssia(). If this is the intention, use
that function instead.
>>> streamX = converter.parse('tinynotation: 4/4 a4 b c d', makeNotation=False)
>>> streamY = converter.parse('tinynotation: 4/4 a4 b- c e', makeNotation=False)
>>> mergedStream = variant.mergeVariants(streamX, streamY,
... variantName='docVariant', inPlace=False)
>>> mergedStream.show('text')
{0.0} <music21.meter.TimeSignature 4/4>
{0.0} <music21.note.Note A>
{1.0} <music21.variant.Variant object of length 1.0>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note C>
{3.0} <music21.variant.Variant object of length 1.0>
{3.0} <music21.note.Note D>
>>> v0 = mergedStream.getElementsByClass(variant.Variant).first()
>>> v0
<music21.variant.Variant object of length 1.0>
>>> v0.first()
<music21.note.Note B->
>>> streamZ = converter.parse('tinynotation: 4/4 a4 b c d e f g a', makeNotation=False)
>>> variant.mergeVariants(streamX, streamZ, variantName='docVariant', inPlace=False)
Traceback (most recent call last):
music21.variant.VariantException: Could not determine what merging method to use.
Try using a more specific merging function.
Example: Create a main score (aScore) and a variant score (vScore), each with
two parts (ap1/vp1
and ap2/vp2) and some small variants between ap1/vp1 and ap2/vp2, marked with * below.
>>> aScore = stream.Score()
>>> vScore = stream.Score()
>>> # *
>>> ap1 = converter.parse('tinynotation: 4/4 a4 b c d e2 f g2 f4 g ')
>>> vp1 = converter.parse('tinynotation: 4/4 a4 b c e e2 f g2 f4 a ')
>>> # * * *
>>> ap2 = converter.parse('tinynotation: 4/4 a4 g f e f2 e d2 g4 f ')
>>> vp2 = converter.parse('tinynotation: 4/4 a4 g f e f2 g f2 g4 d ')
>>> ap1.id = 'aPart1'
>>> ap2.id = 'aPart2'
>>> aScore.insert(0.0, ap1)
>>> aScore.insert(0.0, ap2)
>>> vScore.insert(0.0, vp1)
>>> vScore.insert(0.0, vp2)
Create one merged score where everything different in vScore from aScore is called a variant.
>>> mergedScore = variant.mergeVariants(aScore, vScore, variantName='docVariant', inPlace=False)
>>> mergedScore.show('text')
{0.0} <music21.stream.Part aPart1>
{0.0} <music21.variant.Variant object of length 4.0>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.meter.TimeSignature 4/4>
{0.0} <music21.note.Note A>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note C>
{3.0} <music21.note.Note D>
{4.0} <music21.stream.Measure 2 offset=4.0>
{0.0} <music21.note.Note E>
{2.0} <music21.note.Note F>
{8.0} <music21.variant.Variant object of length 4.0>
{8.0} <music21.stream.Measure 3 offset=8.0>
{0.0} <music21.note.Note G>
{2.0} <music21.note.Note F>
{3.0} <music21.note.Note G>
{4.0} <music21.bar.Barline type=final>
{0.0} <music21.stream.Part aPart2>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.meter.TimeSignature 4/4>
{0.0} <music21.note.Note A>
{1.0} <music21.note.Note G>
{2.0} <music21.note.Note F>
{3.0} <music21.note.Note E>
{4.0} <music21.variant.Variant object of length 8.0>
{4.0} <music21.stream.Measure 2 offset=4.0>
{0.0} <music21.note.Note F>
{2.0} <music21.note.Note E>
{8.0} <music21.stream.Measure 3 offset=8.0>
{0.0} <music21.note.Note D>
{2.0} <music21.note.Note G>
{3.0} <music21.note.Note F>
{4.0} <music21.bar.Barline type=final>
>>> mergedPart = variant.mergeVariants(ap2, vp2, variantName='docVariant', inPlace=False)
>>> mergedPart.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
...
{4.0} <music21.variant.Variant object of length 8.0>
{4.0} <music21.stream.Measure 2 offset=4.0>
...
{4.0} <music21.bar.Barline type=final>
'''
classesX = streamX.classes
if 'Score' in classesX:
return mergeVariantScores(streamX, streamY, variantName, inPlace=inPlace)
elif streamX.getElementsByClass(stream.Measure):
return mergeVariantMeasureStreams(streamX, streamY, variantName, inPlace=inPlace)
elif (streamX.iter().notesAndRests
and streamX.duration.quarterLength == streamY.duration.quarterLength):
return mergeVariantsEqualDuration([streamX, streamY], [variantName], inPlace=inPlace)
else:
raise VariantException(
'Could not determine what merging method to use. '
+ 'Try using a more specific merging function.')
[docs]
def mergeVariantScores(aScore, vScore, variantName='variant', *, inPlace=False):
# noinspection PyShadowingNames
'''
Takes two scores and merges them with mergeVariantMeasureStreams, part-by-part.
>>> aScore, vScore = stream.Score(), stream.Score()
>>> ap1 = converter.parse('tinynotation: 4/4 a4 b c d e2 f2 g2 f4 g4 ')
>>> vp1 = converter.parse('tinynotation: 4/4 a4 b c e e2 f2 g2 f4 a4 ')
>>> ap2 = converter.parse('tinynotation: 4/4 a4 g f e f2 e2 d2 g4 f4 ')
>>> vp2 = converter.parse('tinynotation: 4/4 a4 g f e f2 g2 f2 g4 d4 ')
>>> aScore.insert(0.0, ap1)
>>> aScore.insert(0.0, ap2)
>>> vScore.insert(0.0, vp1)
>>> vScore.insert(0.0, vp2)
>>> mergedScores = variant.mergeVariantScores(aScore, vScore,
... variantName='docVariant', inPlace=False)
>>> mergedScores.show('text')
{0.0} <music21.stream.Part ...>
{0.0} <music21.variant.Variant object of length 4.0>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.meter.TimeSignature 4/4>
{0.0} <music21.note.Note A>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note C>
{3.0} <music21.note.Note D>
{4.0} <music21.stream.Measure 2 offset=4.0>
{0.0} <music21.note.Note E>
{2.0} <music21.note.Note F>
{8.0} <music21.variant.Variant object of length 4.0>
{8.0} <music21.stream.Measure 3 offset=8.0>
{0.0} <music21.note.Note G>
{2.0} <music21.note.Note F>
{3.0} <music21.note.Note G>
{4.0} <music21.bar.Barline type=final>
{0.0} <music21.stream.Part ...>
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.meter.TimeSignature 4/4>
{0.0} <music21.note.Note A>
{1.0} <music21.note.Note G>
{2.0} <music21.note.Note F>
{3.0} <music21.note.Note E>
{4.0} <music21.variant.Variant object of length 8.0>
{4.0} <music21.stream.Measure 2 offset=4.0>
{0.0} <music21.note.Note F>
{2.0} <music21.note.Note E>
{8.0} <music21.stream.Measure 3 offset=8.0>
{0.0} <music21.note.Note D>
{2.0} <music21.note.Note G>
{3.0} <music21.note.Note F>
{4.0} <music21.bar.Barline type=final>
'''
if len(aScore.iter().parts) != len(vScore.iter().parts):
raise VariantException(
'These scores do not have the same number of parts and cannot be merged.')
if inPlace is True:
returnObj = aScore
else:
returnObj = aScore.coreCopyAsDerivation('mergeVariantScores')
for returnPart, vPart in zip(returnObj.parts, vScore.parts):
mergeVariantMeasureStreams(returnPart, vPart, variantName, inPlace=True)
if inPlace is False:
return returnObj
[docs]
def mergeVariantMeasureStreams(streamX, streamY, variantName='variant', *, inPlace=False):
'''
Takes two streams of measures and returns a stream (new if inPlace is False) with the second
merged with the first as variants. This function differs from mergeVariantsEqualDuration by
dealing with streams that are of different length. This function matches measures that are
exactly equal and creates variant objects for regions of measures that differ at all. If more
refined variants are sought (with variation within the bar considered and related but different
bars associated with each other), use variant.refineVariant().
In this example, the second bar has been deleted in the second version,
a new bar has been inserted between the
original third and fourth bars, and two bars have been added at the end.
>>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
... ('a', 'quarter'), ('a', 'quarter')]
>>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
... ('a', 'quarter'),('b', 'quarter')]
>>> data1M3 = [('c', 'quarter'), ('d', 'quarter'),
... ('e', 'quarter'), ('e', 'quarter')]
>>> data1M4 = [('d', 'quarter'), ('g', 'eighth'), ('g', 'eighth'),
... ('a', 'quarter'), ('b', 'quarter')]
>>> data2M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
... ('a', 'quarter'), ('a', 'quarter')]
>>> data2M2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
>>> data2M3 = [('e', 'quarter'), ('g', 'eighth'), ('g', 'eighth'),
... ('a', 'quarter'), ('b', 'quarter')]
>>> data2M4 = [('d', 'quarter'), ('g', 'eighth'), ('g', 'eighth'),
... ('a', 'quarter'), ('b', 'quarter')]
>>> data2M5 = [('f', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
... ('a', 'quarter'), ('b', 'quarter')]
>>> data2M6 = [('g', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
>>> data1 = [data1M1, data1M2, data1M3, data1M4]
>>> data2 = [data2M1, data2M2, data2M3, data2M4, data2M5, data2M6]
>>> stream1 = stream.Stream()
>>> stream2 = stream.Stream()
>>> mNumber = 1
>>> for d in data1:
... m = stream.Measure()
... m.number = mNumber
... mNumber += 1
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... stream1.append(m)
>>> mNumber = 1
>>> for d in data2:
... m = stream.Measure()
... m.number = mNumber
... mNumber += 1
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... stream2.append(m)
>>> #_DOCS_SHOW stream1.show()
.. image:: images/variant_measuresStreamMergeStream1.*
:width: 600
>>> #_DOCS_SHOW stream2.show()
.. image:: images/variant_measuresStreamMergeStream2.*
:width: 600
>>> mergedStream = variant.mergeVariantMeasureStreams(stream1, stream2, 'paris', inPlace=False)
>>> mergedStream.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.note.Note A>
{1.0} <music21.note.Note B>
{1.5} <music21.note.Note C>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note A>
{4.0} <music21.variant.Variant object of length 0.0>
{4.0} <music21.stream.Measure 2 offset=4.0>
{0.0} <music21.note.Note B>
{0.5} <music21.note.Note C>
{1.0} <music21.note.Note A>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
{8.0} <music21.stream.Measure 3 offset=8.0>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.note.Note E>
{12.0} <music21.variant.Variant object of length 4.0>
{12.0} <music21.stream.Measure 4 offset=12.0>
{0.0} <music21.note.Note D>
{1.0} <music21.note.Note G>
{1.5} <music21.note.Note G>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
{16.0} <music21.variant.Variant object of length 8.0>
>>> mergedStream[variant.Variant][0].replacementQuarterLength
4.0
>>> mergedStream[variant.Variant][1].replacementQuarterLength
0.0
>>> parisStream = mergedStream.activateVariants('paris', inPlace=False)
>>> parisStream.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
{0.0} <music21.note.Note A>
{1.0} <music21.note.Note B>
{1.5} <music21.note.Note C>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note A>
{4.0} <music21.variant.Variant object of length 4.0>
{4.0} <music21.stream.Measure 2 offset=4.0>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.note.Note E>
{8.0} <music21.variant.Variant object of length 0.0>
{8.0} <music21.stream.Measure 3 offset=8.0>
{0.0} <music21.note.Note E>
{1.0} <music21.note.Note G>
{1.5} <music21.note.Note G>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
{12.0} <music21.stream.Measure 4 offset=12.0>
{0.0} <music21.note.Note D>
{1.0} <music21.note.Note G>
{1.5} <music21.note.Note G>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
{16.0} <music21.variant.Variant object of length 0.0>
{16.0} <music21.stream.Measure 5 offset=16.0>
{0.0} <music21.note.Note F>
{0.5} <music21.note.Note C>
{1.5} <music21.note.Note A>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
{20.0} <music21.stream.Measure 6 offset=20.0>
{0.0} <music21.note.Note G>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.note.Note E>
>>> parisStream[variant.Variant][0].replacementQuarterLength
0.0
>>> parisStream[variant.Variant][1].replacementQuarterLength
4.0
>>> parisStream[variant.Variant][2].replacementQuarterLength
8.0
'''
if inPlace is True:
returnObj = streamX
else:
returnObj = streamX.coreCopyAsDerivation('mergeVariantMeasureStreams')
regions = _getRegionsFromStreams(returnObj, streamY)
for (regionType, xRegionStartMeasure, xRegionEndMeasure,
yRegionStartMeasure, yRegionEndMeasure) in regions:
# Note that the 'end' measure indices are 1 greater
# than the 0-indexed number of the measure.
if xRegionStartMeasure >= len(returnObj.getElementsByClass(stream.Measure)):
startOffset = returnObj.duration.quarterLength
# This deals with insertion at the end case where
# returnObj.measure(xRegionStartMeasure + 1) does not exist.
else:
startOffset = returnObj.measure(xRegionStartMeasure + 1).getOffsetBySite(returnObj)
yRegion = None
replacementQuarterLength = 0.0
if regionType == 'equal':
# yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure)
continue # Do nothing
elif regionType == 'replace':
xRegion = returnObj.measures(xRegionStartMeasure + 1, xRegionEndMeasure)
replacementQuarterLength = xRegion.duration.quarterLength
yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure)
elif regionType == 'delete':
xRegion = returnObj.measures(xRegionStartMeasure + 1, xRegionEndMeasure)
replacementQuarterLength = xRegion.duration.quarterLength
yRegion = None
elif regionType == 'insert':
yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure)
replacementQuarterLength = 0.0
else:
raise VariantException(f'Unknown regionType {regionType!r}')
addVariant(returnObj, startOffset, yRegion,
variantName=variantName, replacementQuarterLength=replacementQuarterLength)
if inPlace is True:
return
else:
return returnObj
[docs]
def mergeVariantsEqualDuration(streams, variantNames, *, inPlace=False):
'''
Pass this function a list of streams (they must be of the same
length or a VariantException will be raised).
It will return a stream which merges the differences between the
streams into variant objects keeping the
first stream in the list as the default. If inPlace is True, the
first stream in the list will be modified,
otherwise a new stream will be returned. Pass a list of names to
associate variants with their sources, if this list
does not contain an entry for each non-default variant,
naming may not behave properly. Variants that have the
same differences from the default will be saved as separate
variant objects (i.e. more than once under different names).
Also, note that a streams with bars of differing lengths will not behave properly.
>>> stream1 = stream.Stream()
>>> stream2paris = stream.Stream()
>>> stream3london = stream.Stream()
>>> data1 = [('a', 'quarter'), ('b', 'eighth'),
... ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
... ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')]
>>> data2 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter'),
... ('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), ('a', 'quarter'),
... ('b', 'quarter'), ('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter')]
>>> data3 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
... ('a', 'quarter'), ('a', 'quarter'),
... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
... ('c', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')]
>>> for pitchName, durType in data1:
... n = note.Note(pitchName)
... n.duration.type = durType
... stream1.append(n)
>>> for pitchName, durType in data2:
... n = note.Note(pitchName)
... n.duration.type = durType
... stream2paris.append(n)
>>> for pitchName, durType in data3:
... n = note.Note(pitchName)
... n.duration.type = durType
... stream3london.append(n)
>>> mergedStreams = variant.mergeVariantsEqualDuration(
... [stream1, stream2paris, stream3london], ['paris', 'london'])
>>> mergedStreams.show('t')
{0.0} <music21.note.Note A>
{1.0} <music21.variant.Variant object of length 1.0>
{1.0} <music21.note.Note B>
{1.5} <music21.note.Note C>
{2.0} <music21.note.Note A>
{3.0} <music21.variant.Variant object of length 1.0>
{3.0} <music21.note.Note A>
{4.0} <music21.note.Note B>
{4.5} <music21.variant.Variant object of length 1.5>
{4.5} <music21.note.Note C>
{5.0} <music21.note.Note A>
{6.0} <music21.note.Note A>
{7.0} <music21.variant.Variant object of length 1.0>
{7.0} <music21.note.Note B>
{8.0} <music21.note.Note C>
{9.0} <music21.variant.Variant object of length 2.0>
{9.0} <music21.note.Note D>
{10.0} <music21.note.Note E>
>>> mergedStreams.activateVariants('london').show('t')
{0.0} <music21.note.Note A>
{1.0} <music21.variant.Variant object of length 1.0>
{1.0} <music21.note.Note B>
{1.5} <music21.note.Note C>
{2.0} <music21.note.Note A>
{3.0} <music21.variant.Variant object of length 1.0>
{3.0} <music21.note.Note A>
{4.0} <music21.note.Note B>
{4.5} <music21.variant.Variant object of length 1.5>
{4.5} <music21.note.Note C>
{5.0} <music21.note.Note A>
{6.0} <music21.note.Note A>
{7.0} <music21.variant.Variant object of length 1.0>
{7.0} <music21.note.Note C>
{8.0} <music21.note.Note C>
{9.0} <music21.variant.Variant object of length 2.0>
{9.0} <music21.note.Note D>
{10.0} <music21.note.Note E>
If the streams contain parts and measures, the merge function will iterate
through them and determine
and store variant differences within each measure/part.
>>> stream1 = stream.Stream()
>>> stream2 = stream.Stream()
>>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
... ('a', 'quarter'), ('a', 'quarter')]
>>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
... ('a', 'quarter'),('b', 'quarter')]
>>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
>>> data2M1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')]
>>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
... ('a', 'quarter'), ('b', 'quarter')]
>>> data2M3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
>>> data1 = [data1M1, data1M2, data1M3]
>>> data2 = [data2M1, data2M2, data2M3]
>>> tempPart = stream.Part()
>>> for d in data1:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... tempPart.append(m)
>>> stream1.append(tempPart)
>>> tempPart = stream.Part()
>>> for d in data2:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... tempPart.append(m)
>>> stream2.append(tempPart)
>>> mergedStreams = variant.mergeVariantsEqualDuration([stream1, stream2], ['paris'])
>>> mergedStreams.show('t')
{0.0} <music21.stream.Part ...>
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.note.Note A>
{1.0} <music21.variant.Variant object of length 1.0>
{1.0} <music21.note.Note B>
{1.5} <music21.note.Note C>
{2.0} <music21.note.Note A>
{3.0} <music21.variant.Variant object of length 1.0>
{3.0} <music21.note.Note A>
{4.0} <music21.stream.Measure 0 offset=4.0>
{0.0} <music21.note.Note B>
{0.5} <music21.variant.Variant object of length 1.5>
{0.5} <music21.note.Note C>
{1.0} <music21.note.Note A>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
{8.0} <music21.stream.Measure 0 offset=8.0>
{0.0} <music21.note.Note C>
{1.0} <music21.variant.Variant object of length 3.0>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.note.Note E>
>>> #_DOCS_SHOW mergedStreams.show()
.. image:: images/variant_measuresAndParts.*
:width: 600
>>> for p in mergedStreams.getElementsByClass(stream.Part):
... for m in p.getElementsByClass(stream.Measure):
... m.activateVariants('paris', inPlace=True)
>>> mergedStreams.show('t')
{0.0} <music21.stream.Part ...>
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.note.Note A>
{1.0} <music21.variant.Variant object of length 1.0>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note A>
{3.0} <music21.variant.Variant object of length 1.0>
{3.0} <music21.note.Note G>
{4.0} <music21.stream.Measure 0 offset=4.0>
{0.0} <music21.note.Note B>
{0.5} <music21.variant.Variant object of length 1.5>
{0.5} <music21.note.Note C>
{1.5} <music21.note.Note A>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
{8.0} <music21.stream.Measure 0 offset=8.0>
{0.0} <music21.note.Note C>
{1.0} <music21.variant.Variant object of length 3.0>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note A>
>>> #_DOCS_SHOW mergedStreams.show()
.. image:: images/variant_measuresAndParts2.*
:width: 600
If barlines do not match up, an exception will be thrown. Here two streams that are identical
are merged, except one is in 3/4, the other in 4/4. This throws an exception.
>>> streamDifferentMeasures = stream.Stream()
>>> dataDiffM1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter')]
>>> dataDiffM2 = [ ('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter')]
>>> dataDiffM3 = [('a', 'quarter'), ('b', 'quarter'), ('c', 'quarter')]
>>> dataDiffM4 = [('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
>>> dataDiff = [dataDiffM1, dataDiffM2, dataDiffM3, dataDiffM4]
>>> streamDifferentMeasures.insert(0.0, meter.TimeSignature('3/4'))
>>> tempPart = stream.Part()
>>> for d in dataDiff:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... tempPart.append(m)
>>> streamDifferentMeasures.append(tempPart)
>>> mergedStreams = variant.mergeVariantsEqualDuration(
... [stream1, streamDifferentMeasures], ['paris'])
Traceback (most recent call last):
music21.variant.VariantException: _mergeVariants cannot merge streams
which are of different lengths
'''
if inPlace is True:
returnObj = streams[0]
else:
returnObj = streams[0].coreCopyAsDerivation('mergeVariantsEqualDuration')
# Adds a None element at beginning (corresponding to default variant streams[0])
variantNames.insert(0, None)
while len(streams) > len(variantNames): # Adds Blank names if too few
variantNames.append(None)
while len(streams) < len(variantNames): # Removes extra names
variantNames.pop()
zipped = list(zip(streams, variantNames))
for s, variantName in zipped[1:]:
if returnObj.highestTime != s.highestTime:
raise VariantException('cannot merge streams of different lengths')
returnObjParts = returnObj.getElementsByClass(stream.Part)
if returnObjParts: # If parts exist, iterate through them.
sParts = s.getElementsByClass(stream.Part)
for i, returnObjPart in enumerate(returnObjParts):
sPart = sParts[i]
returnObjMeasures = returnObjPart.getElementsByClass(stream.Measure)
if returnObjMeasures:
# If measures exist and parts exist, iterate through them both.
for j, returnObjMeasure in enumerate(returnObjMeasures):
sMeasure = sPart.getElementsByClass(stream.Measure)[j]
_mergeVariants(
returnObjMeasure, sMeasure, variantName=variantName, inPlace=True)
else: # If parts exist but no measures.
_mergeVariants(returnObjPart, sPart, variantName=variantName, inPlace=True)
else:
returnObjMeasures = returnObj.getElementsByClass(stream.Measure)
if returnObjMeasures: # If no parts, but still measures, iterate through them.
for j, returnObjMeasure in enumerate(returnObjMeasures):
sMeasure = s.getElementsByClass(stream.Measure)[j]
_mergeVariants(returnObjMeasure, sMeasure,
variantName=variantName, inPlace=True)
else: # If no parts and no measures.
_mergeVariants(returnObj, s, variantName=variantName, inPlace=True)
return returnObj
[docs]
def mergePartAsOssia(mainPart, ossiaPart, ossiaName,
inPlace=False, compareByMeasureNumber=False, recurseInMeasures=False):
# noinspection PyShadowingNames
'''
Some MusicXML files are generated with full parts that have only a few non-rest measures
instead of ossia parts, such as those
created by Sibelius 7. This function
takes two streams (mainPart and ossiaPart), the second interpreted as an ossia.
It outputs a stream with the ossia part merged into the stream as a
group of variants.
If compareByMeasureNumber is True, then the ossia measures will be paired with the
measures in the mainPart that have the
same measure.number. Otherwise, they will be paired by offset. In most cases
these should have the same result.
Note that this method has no way of knowing if a variant is supposed to be a
different duration than the segment of stream which it replaces
because that information is not contained in the format of score this method is
designed to deal with.
>>> mainStream = converter.parse('tinynotation: 4/4 A4 B4 C4 D4 E1 F2 E2 E8 F8 F4 G2 G2 G4 F4 F4 F4 F4 F4 G1 ')
>>> ossiaStream = converter.parse('tinynotation: 4/4 r1 r1 r1 E4 E4 F4 G4 r1 F2 F2 r1 ')
>>> mainStream.makeMeasures(inPlace=True)
>>> ossiaStream.makeMeasures(inPlace=True)
>>> mainPart = stream.Part()
>>> for m in mainStream:
... mainPart.insert(m.offset, m)
>>> ossiaPart = stream.Part()
>>> for m in ossiaStream:
... ossiaPart.insert(m.offset, m)
>>> s = stream.Stream()
>>> s.insert(0.0, ossiaPart)
>>> s.insert(0.0, mainPart)
>>> #_DOCS_SHOW s.show()
>>> mainPartWithOssiaVariantsFT = variant.mergePartAsOssia(mainPart, ossiaPart,
... ossiaName='Parisian_Variant',
... inPlace=False,
... compareByMeasureNumber=False,
... recurseInMeasures=True)
>>> mainPartWithOssiaVariantsTT = variant.mergePartAsOssia(mainPart, ossiaPart,
... ossiaName='Parisian_Variant',
... inPlace=False,
... compareByMeasureNumber=True,
... recurseInMeasures=True)
>>> mainPartWithOssiaVariantsFF = variant.mergePartAsOssia(mainPart, ossiaPart,
... ossiaName='Parisian_Variant',
... inPlace=False,
... compareByMeasureNumber=False,
... recurseInMeasures=False)
>>> mainPartWithOssiaVariantsTF = variant.mergePartAsOssia(mainPart, ossiaPart,
... ossiaName='Parisian_Variant',
... inPlace=False,
... compareByMeasureNumber=True,
... recurseInMeasures=False)
>>> mainPartWithOssiaVariantsFT.show('text') == mainPartWithOssiaVariantsTT.show('text')
{0.0} <music21.stream.Measure ...
True
>>> mainPartWithOssiaVariantsFF.show('text') == mainPartWithOssiaVariantsFT.show('text')
{0.0} <music21.stream.Measure ...
True
>>> mainPartWithOssiaVariantsFT.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
...
{12.0} <music21.stream.Measure 4 offset=12.0>
{0.0} <music21.variant.Variant object of length 3.0>
{0.0} <music21.note.Note E>
{0.5} <music21.note.Note F>
{1.0} <music21.note.Note F>
{2.0} <music21.note.Note G>
{16.0} <music21.stream.Measure 5 offset=16.0>
...
{20.0} <music21.stream.Measure 6 offset=20.0>
{0.0} <music21.variant.Variant object of length 4.0>
{0.0} <music21.note.Note F>
{1.0} <music21.note.Note F>
{2.0} <music21.note.Note F>
{3.0} <music21.note.Note F>
...
>>> mainPartWithOssiaVariantsFF.activateVariants('Parisian_Variant').show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
...
{12.0} <music21.variant.Variant object of length 4.0>
{12.0} <music21.stream.Measure 4 offset=12.0>
{0.0} <music21.note.Note E>
{1.0} <music21.note.Note E>
{2.0} <music21.note.Note F>
{3.0} <music21.note.Note G>
{16.0} <music21.stream.Measure 5 offset=16.0>
...
{20.0} <music21.variant.Variant object of length 4.0>
{20.0} <music21.stream.Measure 6 offset=20.0>
{0.0} <music21.note.Note F>
{2.0} <music21.note.Note F>
...
'''
if inPlace is True:
returnObj = mainPart
else:
returnObj = mainPart.coreCopyAsDerivation('mergePartAsOssia')
if compareByMeasureNumber is True:
for ossiaMeasure in ossiaPart.getElementsByClass(stream.Measure):
if ossiaMeasure.notes: # If the measure is not just rests
ossiaNumber = ossiaMeasure.number
returnMeasure = returnObj.measure(ossiaNumber)
if recurseInMeasures is True:
mergeVariantsEqualDuration(
[returnMeasure, ossiaMeasure],
[ossiaName],
inPlace=True
)
else:
ossiaOffset = returnMeasure.getOffsetBySite(returnObj)
addVariant(returnObj,
ossiaOffset,
ossiaMeasure,
variantName=ossiaName,
variantGroups=None,
replacementQuarterLength=None
)
else:
for ossiaMeasure in ossiaPart.getElementsByClass(stream.Measure):
if ossiaMeasure.notes: # If the measure is not just rests
ossiaOffset = ossiaMeasure.getOffsetBySite(ossiaPart)
if recurseInMeasures is True:
returnMeasure = returnObj.getElementsByOffset(
ossiaOffset
).getElementsByClass(stream.Measure).first()
mergeVariantsEqualDuration(
[returnMeasure, ossiaMeasure],
[ossiaName],
inPlace=True
)
else:
addVariant(returnObj, ossiaOffset, ossiaMeasure,
variantName=ossiaName, variantGroups=None,
replacementQuarterLength=None)
if inPlace is True:
return
else:
return returnObj
# ------ Public Helper Functions
[docs]
def addVariant(
s: stream.Stream,
startOffset: int|float,
sVariant: stream.Stream|Variant,
variantName=None,
variantGroups=None,
replacementQuarterLength=None,
*,
replacementDuration=None,
):
# noinspection PyShadowingNames
'''
Takes a stream, the location of the variant to be added to
that stream (startOffset), the content of the
variant to be added (sVariant), and the quarterLength of the section of the stream which
the variant replaces (replacementQuarterLength).
If replacementQuarterLength is 0,
this is an insertion. If sVariant is
None, this is a deletion.
* Changed in v10.3: the ``replacementDuration`` keyword renamed to
``replacementQuarterLength``.
* Changed in v11: passing the old ``replacementDuration`` keyword now raises a
deprecation warning; it will be removed in v12.
>>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
... ('a', 'quarter'), ('a', 'quarter')]
>>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
>>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
... ('a', 'quarter'),('b', 'quarter')]
>>> data1 = [data1M1, data1M2, data1M3]
>>> tempPart = stream.Part()
>>> stream1 = stream.Stream()
>>> for d in data1:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... stream1.append(m)
>>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
... ('a', 'quarter'), ('b', 'quarter')]
>>> stream2 = stream.Stream()
>>> m = stream.Measure()
>>> for pitchName, durType in data2M2:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
>>> stream2.append(m)
>>> variant.addVariant(stream1, 4.0, stream2,
... variantName='rhythmic_switch', replacementQuarterLength=4.0)
>>> stream1.show('text')
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.note.Note A>
{1.0} <music21.note.Note B>
{1.5} <music21.note.Note C>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note A>
{4.0} <music21.variant.Variant object of length 4.0>
{4.0} <music21.stream.Measure 0 offset=4.0>
{0.0} <music21.note.Note B>
{0.5} <music21.note.Note C>
{1.0} <music21.note.Note A>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
{8.0} <music21.stream.Measure 0 offset=8.0>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note D>
{2.0} <music21.note.Note E>
{3.0} <music21.note.Note E>
>>> stream1 = stream.Stream()
>>> stream1.repeatAppend(note.Note('e'), 6)
>>> variant1 = variant.Variant()
>>> variant1.repeatAppend(note.Note('f'), 3)
>>> startOffset = 3.0
>>> variant.addVariant(stream1, startOffset, variant1,
... variantName='paris', replacementQuarterLength=3.0)
>>> stream1.show('text')
{0.0} <music21.note.Note E>
{1.0} <music21.note.Note E>
{2.0} <music21.note.Note E>
{3.0} <music21.variant.Variant object of length 6.0>
{3.0} <music21.note.Note E>
{4.0} <music21.note.Note E>
{5.0} <music21.note.Note E>
'''
if replacementDuration is not None:
# 'replacementDuration' was renamed to 'replacementQuarterLength' in v10.3.
# Deprecated as of v11, to be removed in v12.
warnings.warn(
"addVariant()'s 'replacementDuration' keyword was renamed to "
+ "'replacementQuarterLength' in v10.3; 'replacementDuration' is deprecated "
+ 'and will be removed in v12.',
exceptions21.Music21DeprecationWarning,
stacklevel=2)
if replacementQuarterLength is not None:
raise TypeError(
"addVariant() received both 'replacementQuarterLength' and its former "
+ "name 'replacementDuration'; pass only 'replacementQuarterLength'.")
replacementQuarterLength = replacementDuration
tempVariant = Variant()
if variantGroups is not None:
tempVariant.groups = variantGroups
if variantName is not None:
tempVariant.groups.append(variantName)
tempVariant.replacementQuarterLength = replacementQuarterLength
if sVariant is None: # deletion
pass
else: # replacement or insertion
if isinstance(sVariant, stream.Measure): # sVariant is a measure put it in a variant and insert.
tempVariant.append(sVariant)
else: # sVariant is not a measure
sVariantMeasures = sVariant.getElementsByClass(stream.Measure)
if not sVariantMeasures: # If there are no measures, work element-wise
for e in sVariant:
offset = e.getOffsetBySite(sVariant) + startOffset
tempVariant.insert(offset, e)
else: # if there are measures work measure-wise
for m in sVariantMeasures:
tempVariant.append(m)
s.insert(startOffset, tempVariant)
[docs]
def refineVariant(s, sVariant, *, inPlace=False):
# noinspection PyShadowingNames
'''
Given a stream and variant contained in that stream, returns a
stream with that variant 'refined.'
It is refined in the sense that, (with the best estimates) measures which have been determined
to be related are merged within the measure.
Suppose a four-bar phrase in a piece is a slightly
different five-bar phrase in a variant. In the variant, every F# has been replaced by an F,
and the last bar is repeated. Given these streams, mergeVariantMeasureStreams would return
the first stream with a single variant object containing the entire 5 bars of the variant.
Calling refineVariant on this stream and that variant object would result in a variant object
in the measures for each F#/F pair, and a variant object containing the added bar at the end.
For a more detailed explanation of how similar measures are properly associated with each other
look at the documentation for _getBestListAndScore.
Note that this code does not work properly yet.
>>> v = variant.Variant()
>>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
... ('a', 'quarter'),('b', 'quarter')]
>>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
>>> variantData = [variantDataM1, variantDataM2]
>>> for d in variantData:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... v.append(m)
>>> v.groups = ['paris']
>>> v.replacementQuarterLength = 8.0
>>> s = stream.Stream()
>>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')]
>>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'),
... ('a', 'eighth'), ('a', 'quarter'), ('b', 'quarter')]
>>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
>>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
>>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4]
>>> for d in streamData:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... s.append(m)
>>> s.insert(4.0, v)
>>> variant.refineVariant(s, v, inPlace=True)
>>> s.show('text')
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.note.Note A>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note G>
{4.0} <music21.stream.Measure 0 offset=4.0>
{0.0} <music21.note.Note B>
{0.5} <music21.variant.Variant object of length 1.5>
{0.5} <music21.note.Note C>
{1.5} <music21.note.Note A>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note B>
{8.0} <music21.stream.Measure 0 offset=8.0>
{0.0} <music21.note.Note C>
{1.0} <music21.variant.Variant object of length 3.0>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note A>
{12.0} <music21.stream.Measure 0 offset=12.0>
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note A>
{3.0} <music21.note.Note A>
'''
# stream that will be returned
if sVariant not in s.getElementsByClass(Variant):
raise VariantException(f'{sVariant} not found in stream {s}.')
if inPlace is True:
returnObject = s
variantRegion = sVariant
else:
sVariantIndex = s.getElementsByClass(Variant).index(sVariant)
returnObject = s.coreCopyAsDerivation('refineVariant')
variantRegion = returnObject.getElementsByClass(Variant)(sVariantIndex)
# useful parameters from variant and its location
variantGroups = sVariant.groups
replacementDuration = sVariant.replacementQuarterLength
startOffset = sVariant.getOffsetBySite(s)
# endOffset = replacementDuration + startOffset
# region associated with the given variant in the stream
returnRegion = variantRegion.replacedElements(returnObject)
# associating measures in variantRegion to those in returnRegion ->
# This is done via 0 indexed lists corresponding to measures
returnRegionMeasureList = list(range(len(returnRegion)))
badnessDict = {}
listDict = {}
variantMeasureList, unused_badness = _getBestListAndScore(returnRegion,
variantRegion,
badnessDict,
listDict)
# badness is a measure of how different the streams are.
# The list returned, variantMeasureList, minimizes that quantity.
# mentioned lists are compared via difflib for optimal edit regions
# (equal, delete, insert, replace)
sm = difflib.SequenceMatcher()
sm.set_seqs(returnRegionMeasureList, variantMeasureList)
regions = sm.get_opcodes()
# each region is processed for variants.
for regionType, returnStart, returnEnd, variantStart, variantEnd in regions:
startOffset = returnRegion[returnStart].getOffsetBySite(returnRegion)
# endOffset = (returnRegion[returnEnd - 1].getOffsetBySite(returnRegion)
# + returnRegion[returnEnd - 1].duration.quarterLength)
variantSubRegion = None
if regionType == 'equal':
returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd)
variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd)
mergeVariantsEqualDuration(
[returnSubRegion, variantSubRegion],
variantGroups,
inPlace=True
)
continue
elif regionType == 'replace':
returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd)
replacementDuration = returnSubRegion.duration.quarterLength
variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd)
elif regionType == 'delete':
returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd)
replacementDuration = returnSubRegion.duration.quarterLength
variantSubRegion = None
elif regionType == 'insert':
variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd)
replacementDuration = 0.0
else:
raise VariantException(f'Unknown regionType {regionType!r}')
addVariant(returnRegion,
startOffset,
variantSubRegion,
variantGroups=variantGroups,
replacementQuarterLength=replacementDuration
)
# The original variant object has been replaced by more refined
# variant objects and so should be deleted.
returnObject.remove(variantRegion)
if inPlace:
return None
else:
return returnObject
def _mergeVariantMeasureStreamsCarefully(streamX, streamY, variantName, *, inPlace=False):
'''
There seem to be some problems with this function, and it isn't well tested.
It is not recommended to use it at this time.
'''
# stream that will be returned
if inPlace is True:
returnObject = streamX
variantObject = streamY
else:
returnObject = copy.deepcopy(streamX)
variantObject = copy.deepcopy(streamY)
# associating measures in variantRegion to those in returnRegion ->
# This is done via 0 indexed lists corresponding to measures
returnObjectMeasureList = list(range(len(returnObject.getElementsByClass(stream.Measure))))
badnessDict = {}
listDict = {}
variantObjectMeasureList, unused_badness = _getBestListAndScore(
returnObject.getElementsByClass(stream.Measure),
variantObject.getElementsByClass(stream.Measure),
badnessDict,
listDict
)
# badness is a measure of how different the streams are.
# The list returned, variantMeasureList, minimizes that quantity.
# mentioned lists are compared via difflib for optimal edit regions
# (equal, delete, insert, replace)
sm = difflib.SequenceMatcher()
sm.set_seqs(returnObjectMeasureList, variantObjectMeasureList)
regions = sm.get_opcodes()
# each region is processed for variants.
for regionType, returnStart, returnEnd, variantStart, variantEnd in regions:
startOffset = returnObject.measure(returnStart + 1).getOffsetBySite(returnObject)
if regionType == 'equal':
returnSubRegion = returnObject.measures(returnStart + 1, returnEnd)
variantSubRegion = variantObject.measures(variantStart + 1, variantEnd)
mergeVariantMeasureStreams(
returnSubRegion,
variantSubRegion,
variantName,
inPlace=True
)
continue
elif regionType == 'replace':
returnSubRegion = returnObject.measures(returnStart + 1, returnEnd)
replacementDuration = returnSubRegion.duration.quarterLength
variantSubRegion = variantObject.measures(variantStart + 1, variantEnd)
elif regionType == 'delete':
returnSubRegion = returnObject.measures(returnStart + 1, returnEnd)
replacementDuration = returnSubRegion.duration.quarterLength
variantSubRegion = None
elif regionType == 'insert':
variantSubRegion = variantObject.measures(variantStart + 1, variantEnd)
replacementDuration = 0.0
else: # pragma: no cover
raise VariantException(f'Unknown regionType: {regionType}')
addVariant(
returnObject,
startOffset,
variantSubRegion,
variantGroups=[variantName],
replacementQuarterLength=replacementDuration
)
if not inPlace:
return returnObject
[docs]
def getMeasureHashes(s):
# noinspection PyShadowingNames
'''
Takes in a stream containing measures and returns a list of hashes,
one for each measure. This is currently implemented
with search.translateStreamToString().
>>> s = converter.parse("tinynotation: 2/4 c4 d8. e16 FF4 a'4 b-2")
>>> sm = s.makeMeasures()
>>> hashes = variant.getMeasureHashes(sm)
>>> hashes
['<P>K@<', ')PQP', 'FZ']
'''
hashes = []
if isinstance(s, list):
for m in s:
hashes.append(search.translateStreamToString(m.notesAndRests))
return hashes
else:
for m in s.getElementsByClass(stream.Measure):
hashes.append(search.translateStreamToString(m.notesAndRests))
return hashes
# ----- Private Helper Functions
def _getBestListAndScore(streamX, streamY, badnessDict, listDict,
isNone=False, streamXIndex=-1, streamYIndex=-1):
# noinspection PyShadowingNames
'''
This is a recursive function which makes a map between two related streams of measures.
It is designed for streams of measures that contain few if any measures that are actually
identical and that have a different number of measures (within reason). For example,
if one stream has 10 bars of eighth notes and the second stream has the same ten bars
of eighth notes except with some dotted rhythms mixed in and the fifth bar is repeated.
The first, streamX, is the reference stream. This function returns a list of
integers with length len(streamY) which maps each measure of StreamY to the measure
in streamX it is most likely associated with. For example, if the returned list is
[0, 2, 3, 'addedBar', 4]. This indicates that streamY is most similar to streamX
after the second bar of streamX has been removed and a new bar inserted between
bars 4 and 5. Note that this list has measures 0-indexed. This function generates this map by
minimizing the difference or 'badness' for the sequence of measures on the whole as determined
by the helper function _simScore which compares measures for similarity. 'addedBar' appears
in the list where this function has determined that the bar appearing
in streamY does not have a counterpart in streamX anywhere and is an insertion.
>>> badnessDict = {}
>>> listDict = {}
>>> stream1 = stream.Stream()
>>> stream2 = stream.Stream()
>>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
... ('a', 'quarter'), ('a', 'quarter')]
>>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
... ('a', 'quarter'),('b', 'quarter')]
>>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
>>> data2M1 = [('a', 'quarter'), ('b', 'quarter'), ('c', 'quarter'), ('g#', 'quarter')]
>>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
... ('a', 'quarter'), ('b', 'quarter')]
>>> data2M3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
>>> data2M4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
>>> data1 = [data1M1, data1M2, data1M3]
>>> data2 = [data2M1, data2M2, data2M3, data2M4]
>>> for d in data1:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... stream1.append(m)
>>> for d in data2:
... m = stream.Measure()
... for pitchName, durType in d:
... n = note.Note(pitchName)
... n.duration.type = durType
... m.append(n)
... stream2.append(m)
>>> kList, kBadness = variant._getBestListAndScore(stream1, stream2,
... badnessDict, listDict, isNone=False)
>>> kList
[0, 1, 2, 'addedBar']
'''
# Initialize 'Best' Values for maximizing algorithm
bestScore = 1
bestNormalizedScore = 1
bestList = []
# Base Cases:
if streamYIndex >= len(streamY):
listDict[(streamXIndex, streamYIndex, isNone)] = []
badnessDict[(streamXIndex, streamYIndex, isNone)] = 0.0
return [], 0
# Query Dict for existing results
if (streamXIndex, streamYIndex, isNone) in badnessDict:
badness = badnessDict[(streamXIndex, streamYIndex, isNone)]
bestList = listDict[(streamXIndex, streamYIndex, isNone)]
return bestList, badness
# Get salient similarity score
if streamXIndex == -1 and streamYIndex == -1:
simScore = 0
elif isNone:
simScore = 0.5
else:
simScore = _diffScore(streamX[streamXIndex], streamY[streamYIndex])
# Check the added bar case:
kList, kBadness = _getBestListAndScore(streamX, streamY, badnessDict, listDict,
isNone=True, streamXIndex=streamXIndex, streamYIndex=streamYIndex + 1)
if kList is None:
kList = []
if kList:
normalizedBadness = kBadness / len(kList)
else:
normalizedBadness = 0
if normalizedBadness <= bestNormalizedScore:
bestScore = kBadness
bestNormalizedScore = normalizedBadness
bestList = kList
# Check the other cases
for k in range(streamXIndex + 1, len(streamX)):
kList, kBadness = _getBestListAndScore(streamX, streamY, badnessDict,
listDict, isNone=False,
streamXIndex=k, streamYIndex=streamYIndex + 1)
if kList is None:
kList = []
if kList:
normalizedBadness = kBadness / len(kList)
else:
normalizedBadness = 0
if normalizedBadness <= bestNormalizedScore:
bestScore = kBadness
bestNormalizedScore = normalizedBadness
bestList = kList
# Prepare and Return Results
returnList = copy.deepcopy(bestList)
if isNone:
returnList.insert(0, 'addedBar')
elif streamXIndex == -1:
pass
else:
returnList.insert(0, streamXIndex)
badness = bestScore + simScore
badnessDict[(streamXIndex, streamYIndex, isNone)] = badness
listDict[(streamXIndex, streamYIndex, isNone)] = returnList
return returnList, badness
def _diffScore(measureX, measureY):
'''
Helper function for _getBestListAndScore which compares two measures and returns a value
associated with their similarity. The higher the normalized (0, 1) value the poorer the match.
This should be calibrated such that the value that appears in _getBestListAndScore for
isNone is true (i.e. testing when a bar does not associate with any existing bars the reference
stream), is well-matched with the similarity scores generated by this function.
>>> m1 = stream.Measure()
>>> m2 = stream.Measure()
>>> m1.append([note.Note('e'), note.Note('f'), note.Note('g'), note.Note('a')])
>>> m2.append([note.Note('e'), note.Note('f'), note.Note('g#'), note.Note('a')])
>>> variant._diffScore(m1, m2)
0.4...
'''
hashes = getMeasureHashes([measureX, measureY])
if hashes[0] == hashes[1]:
baseValue = 0.0
else:
baseValue = 0.4
numberDelta = measureX.number - measureY.number
distanceModifier = float(numberDelta) * 0.001
return baseValue + distanceModifier
def _getRegionsFromStreams(streamX, streamY):
# noinspection PyShadowingNames
'''
Takes in two streams, returns a list of 5-tuples via difflib.get_opcodes()
working on measure differences.
>>> s1 = converter.parse("tinynotation: 2/4 d4 e8. f16 GG4 b'4 b-2 c4 d8. e16 FF4 a'4 b-2")
``*0:Eq *1:Rep * *3:Eq *6:In``
>>> s2 = converter.parse("tinynotation: 2/4 d4 e8. f16 FF4 b'4 c4 d8. e16 FF4 a'4 b-2 b-2")
>>> s1m = s1.makeMeasures()
>>> s2m = s2.makeMeasures()
>>> regions = variant._getRegionsFromStreams(s1m, s2m)
>>> regions
[('equal', 0, 1, 0, 1),
('replace', 1, 3, 1, 2),
('equal', 3, 6, 2, 5),
('insert', 6, 6, 5, 6)]
'''
hashesX = getMeasureHashes(streamX)
hashesY = getMeasureHashes(streamY)
sm = difflib.SequenceMatcher()
sm.set_seqs(hashesX, hashesY)
regions = sm.get_opcodes()
return regions
def _mergeVariants(streamA, streamB, *, variantName=None, inPlace=False):
'''
This is a helper function for mergeVariantsEqualDuration which takes two streams
(which cannot contain container
streams like measures and parts) and merges the second into the first via variant objects.
If the first already contains variant objects, containsVariants should be set to true and the
function will compare streamB to the streamA as well as the
variant streams contained in streamA.
Note that variant streams in streamB will be ignored and lost.
>>> stream1 = stream.Stream()
>>> stream2 = stream.Stream()
>>> data1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
... ('a', 'quarter'), ('a', 'quarter'),
... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
... ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')]
>>> data2 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter'),
... ('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), ('a', 'quarter'),
... ('b', 'quarter'), ('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter')]
>>> for pitchName, durType in data1:
... n = note.Note(pitchName)
... n.duration.type = durType
... stream1.append(n)
>>> for pitchName, durType in data2:
... n = note.Note(pitchName)
... n.duration.type = durType
... stream2.append(n)
>>> mergedStreams = variant._mergeVariants(stream1, stream2, variantName='paris')
>>> mergedStreams.show('t')
{0.0} <music21.note.Note A>
{1.0} <music21.variant.Variant object of length 1.0>
{1.0} <music21.note.Note B>
{1.5} <music21.note.Note C>
{2.0} <music21.note.Note A>
{3.0} <music21.variant.Variant object of length 1.0>
{3.0} <music21.note.Note A>
{4.0} <music21.note.Note B>
{4.5} <music21.variant.Variant object of length 1.5>
{4.5} <music21.note.Note C>
{5.0} <music21.note.Note A>
{6.0} <music21.note.Note A>
{7.0} <music21.note.Note B>
{8.0} <music21.note.Note C>
{9.0} <music21.variant.Variant object of length 2.0>
{9.0} <music21.note.Note D>
{10.0} <music21.note.Note E>
>>> mergedStreams.activateVariants('paris').show('t')
{0.0} <music21.note.Note A>
{1.0} <music21.variant.Variant object of length 1.0>
{1.0} <music21.note.Note B>
{2.0} <music21.note.Note A>
{3.0} <music21.variant.Variant object of length 1.0>
{3.0} <music21.note.Note G>
{4.0} <music21.note.Note B>
{4.5} <music21.variant.Variant object of length 1.5>
{4.5} <music21.note.Note C>
{5.5} <music21.note.Note A>
{6.0} <music21.note.Note A>
{7.0} <music21.note.Note B>
{8.0} <music21.note.Note C>
{9.0} <music21.variant.Variant object of length 2.0>
{9.0} <music21.note.Note B>
{10.0} <music21.note.Note A>
>>> stream1.append(note.Note('e'))
>>> mergedStreams = variant._mergeVariants(stream1, stream2, variantName=['paris'])
Traceback (most recent call last):
music21.variant.VariantException: _mergeVariants cannot merge streams
which are of different lengths
'''
# TODO: Add the feature for merging a stream to a stream with existing variants
# (it has to compare against both the stream and the contained variant)
if (streamA.getElementsByClass(stream.Measure)
or streamA.getElementsByClass(stream.Part)
or streamB.getElementsByClass(stream.Measure)
or streamB.getElementsByClass(stream.Part)):
raise VariantException(
'_mergeVariants cannot merge streams which contain measures or parts.'
)
if streamA.highestTime != streamB.highestTime:
raise VariantException(
'_mergeVariants cannot merge streams which are of different lengths'
)
if inPlace is True:
returnObj = streamA
else:
returnObj = copy.deepcopy(streamA)
i = 0
j = 0
inVariant = False
streamANotes = streamA.flatten().notesAndRests
streamBNotes = streamB.flatten().notesAndRests
noteBuffer = []
variantStart = 0.0
while i < len(streamANotes) and j < len(streamBNotes):
if i == len(streamANotes):
i = len(streamANotes) - 1
if j == len(streamBNotes):
break
if (streamANotes[i].getOffsetBySite(streamA.flatten())
== streamBNotes[j].getOffsetBySite(streamB.flatten())):
# Comparing Notes at same offset
# TODO: Will not work until __eq__ overwritten for Generalized Notes
if streamANotes[i] != streamBNotes[j]:
# If notes are different, start variant if not started and append note.
if inVariant is False:
variantStart = streamBNotes[j].getOffsetBySite(streamB.flatten())
inVariant = True
noteBuffer = []
noteBuffer.append(streamBNotes[j])
else:
noteBuffer.append(streamBNotes[j])
else: # If notes are the same, end and insert variant if in variant.
if inVariant is True:
returnObj.insert(
variantStart,
_generateVariant(
noteBuffer,
streamB,
variantStart,
variantName
)
)
inVariant = False
noteBuffer = []
else:
inVariant = False
i += 1
j += 1
continue
elif (streamANotes[i].getOffsetBySite(streamA.flatten())
> streamBNotes[j].getOffsetBySite(streamB.flatten())):
if inVariant is False:
variantStart = streamBNotes[j].getOffsetBySite(streamB.flatten())
noteBuffer = []
noteBuffer.append(streamBNotes[j])
inVariant = True
else:
noteBuffer.append(streamBNotes[j])
j += 1
continue
else: # Less-than
i += 1
continue
if inVariant is True: # insert final variant if exists
returnObj.insert(
variantStart,
_generateVariant(
noteBuffer,
streamB,
variantStart,
variantName
)
)
inVariant = False
noteBuffer = []
if inPlace is True:
return None
else:
return returnObj
def _generateVariant(noteList, originStream, start, variantName=None):
# noinspection PyShadowingNames
'''
Helper function for mergeVariantsEqualDuration which takes a list of
consecutive notes from a stream and returns
a variant object containing the notes from the list at the offsets
derived from their original context.
>>> originStream = stream.Stream()
>>> data = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
... ('a', 'quarter'), ('a', 'quarter'),
... ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
... ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')]
>>> for pitchName, durType in data:
... n = note.Note(pitchName)
... n.duration.type = durType
... originStream.append(n)
>>> noteList = []
>>> for n in originStream.notes[2:5]:
... noteList.append(n)
>>> start = originStream.notes[2].offset
>>> variantName='paris'
>>> v = variant._generateVariant(noteList, originStream, start, variantName)
>>> v.show('text')
{0.0} <music21.note.Note C>
{0.5} <music21.note.Note A>
{1.5} <music21.note.Note A>
>>> v.groups
['paris']
'''
returnVariant = Variant()
for n in noteList:
returnVariant.insert(n.getOffsetBySite(originStream.flatten()) - start, n)
if variantName is not None:
returnVariant.groups.append(variantName)
return returnVariant
# ------- Variant Manipulation Methods
[docs]
def makeAllVariantsReplacements(streamWithVariants,
variantNames=None,
inPlace=False,
recurse=False):
# noinspection PyShadowingNames,GrazieInspection
'''
This function takes a stream and a list of variantNames
(default works on all variants), and changes all insertion
(elongations with replacementDuration 0)
and deletion variants (with containedHighestTime 0) into variants with non-zero
replacementDuration and non-null elements
by adding measures on the front of insertions and measures on the end
of deletions. This is designed to make it possible to format all variants in a
readable way as a graphical ossia (via lilypond). If inPlace is True
it will perform this action on the stream itself; otherwise it will return a
modified copy. If recurse is True, this
method will work on variants within container objects within the stream (like parts).
>>> # * * *
>>> s = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2 b-4 a4 g4 a8 g8 f4 e4 d2 a2 d4 e4 f4 g4 a2 b-4 a4 g4 a8 b-8 c'4 c4 f1")
>>> s2 = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2. b-8 a8 g4 a8 g8 f4 e4 d2 a2 d4 f4 a2 d4 f4 AA2 d4 e4 f4 g4 g4 a8 b-8 c'4 c4 f1")
>>> # replacement insertion deletion
>>> s.makeMeasures(inPlace=True)
>>> s2.makeMeasures(inPlace=True)
>>> variant.mergeVariants(s, s2, variantName='london', inPlace=True)
>>> newStream = stream.Score(s)
>>> returnStream = variant.makeAllVariantsReplacements(newStream, recurse=False)
>>> for v in returnStream.parts[0][variant.Variant]:
... (v.offset, v.lengthType, v.replacementQuarterLength)
(4.0, 'replacement', 4.0)
(16.0, 'elongation', 0.0)
(20.0, 'deletion', 4.0)
>>> returnStream = variant.makeAllVariantsReplacements(
... newStream, variantNames=['france'], recurse=True)
>>> for v in returnStream.parts[0][variant.Variant]:
... (v.offset, v.lengthType, v.replacementQuarterLength)
(4.0, 'replacement', 4.0)
(16.0, 'elongation', 0.0)
(20.0, 'deletion', 4.0)
>>> variant.makeAllVariantsReplacements(newStream, recurse=True, inPlace=True)
>>> for v in newStream.parts[0][variant.Variant]:
... (v.offset, v.lengthType, v.replacementQuarterLength, v.containedHighestTime)
(4.0, 'replacement', 4.0, 4.0)
(12.0, 'elongation', 4.0, 12.0)
(20.0, 'deletion', 8.0, 4.0)
'''
if inPlace is True:
returnStream = streamWithVariants
else:
returnStream = copy.deepcopy(streamWithVariants)
if recurse is True:
for s in returnStream.recurse(streamsOnly=True):
_doVariantFixingOnStream(s, variantNames=variantNames)
else:
_doVariantFixingOnStream(returnStream, variantNames=variantNames)
if inPlace is True:
return
else:
return returnStream
def _doVariantFixingOnStream(s, variantNames=None):
# noinspection PyShadowingNames,GrazieInspection
'''
This is a helper function for makeAllVariantsReplacements.
It iterates through the appropriate variants
and performs the variant changing operation to eliminate strict deletion and insertion variants.
>>> # * * * * *
>>> s = converter.parse("tinynotation: 4/4 d4 e4 f4 g4 a2 b-4 a4 g4 a8 g8 f4 e4 d2 a2 d4 e4 f4 g4 a2 b-4 a4 g4 a8 b-8 c'4 c4 f1 ", makeNotation=False)
>>> s2 = converter.parse("tinynotation: 4/4 a4 b c d d4 e4 f4 g4 a2. b-8 a8 g4 a8 g8 f4 e4 d2 a2 d4 f4 a2 d4 f4 AA2 d4 e4 f4 g4 g4 a8 b-8 c'4 c4 ", makeNotation=False)
>>> # initial insertion replacement insertion deletion final deletion
>>> s.makeMeasures(inPlace=True)
>>> s2.makeMeasures(inPlace=True)
>>> variant.mergeVariants(s, s2, variantName='london', inPlace=True)
>>> variant._doVariantFixingOnStream(s, 'london')
>>> s.show('text')
{0.0} <music21.variant.Variant object of length 8.0>
{0.0} <music21.stream.Measure 1 offset=0.0>
...
{4.0} <music21.variant.Variant object of length 4.0>
{4.0} <music21.stream.Measure 2 offset=4.0>
...
{12.0} <music21.variant.Variant object of length 12.0>
{12.0} <music21.stream.Measure 4 offset=12.0>
...
{20.0} <music21.variant.Variant object of length 4.0>
{20.0} <music21.stream.Measure 6 offset=20.0>
...
{24.0} <music21.variant.Variant object of length 4.0>
{24.0} <music21.stream.Measure 7 offset=24.0>
...
>>> for v in s[variant.Variant]:
... (v.offset, v.lengthType, v.replacementQuarterLength)
(0.0, 'elongation', 4.0)
(4.0, 'replacement', 4.0)
(12.0, 'elongation', 4.0)
(20.0, 'deletion', 8.0)
(24.0, 'deletion', 8.0)
This also works on streams with variants that contain notes and rests rather than measures.
>>> s = converter.parse('tinyNotation: 4/4 e4 b b b f4 f f f g4 a a a ', makeNotation=False)
>>> v1Stream = converter.parse('tinyNotation: 4/4 a4 a a a ', makeNotation=False)
>>> # initial insertion deletion
>>> v1 = variant.Variant(v1Stream.notes)
>>> v1.replacementQuarterLength = 0.0
>>> v1.groups = ['london']
>>> s.insert(0.0, v1)
>>> v2 = variant.Variant()
>>> v2.replacementQuarterLength = 4.0
>>> v2.groups = ['london']
>>> s.insert(4.0, v2)
>>> variant._doVariantFixingOnStream(s, 'london')
>>> for v in s[variant.Variant]:
... (v.offset, v.lengthType, v.replacementQuarterLength, v.containedHighestTime)
(0.0, 'elongation', 1.0, 5.0)
(4.0, 'deletion', 5.0, 1.0)
'''
for v in s.getElementsByClass(Variant):
if isinstance(variantNames, list): # If variantNames are controlled
if set(v.groups) and not set(variantNames):
# and if this variant is not in the controlled list
continue # then skip it
else:
continue # huh????
lengthType = v.lengthType
replacementDuration = v.replacementQuarterLength
highestTime = v.containedHighestTime
if lengthType == 'elongation' and replacementDuration == 0.0:
variantType = 'insertion'
elif lengthType == 'deletion' and highestTime == 0.0:
variantType = 'deletion'
else:
continue
if v.getOffsetBySite(s) == 0.0:
isInitial = True
isFinal = False
elif v.getOffsetBySite(s) + v.replacementQuarterLength == s.duration.quarterLength:
isInitial = False
isFinal = True
else:
isInitial = False
isFinal = False
# If a non-final deletion or an INITIAL insertion,
# add the next element after the variant.
if ((variantType == 'insertion' and (isInitial is True))
or (variantType == 'deletion' and (isFinal is False))):
targetElement = _getNextElements(s, v)
# Delete initial clefs, etc. from initial insertion targetElement if it exists
if isinstance(targetElement, stream.Stream):
# Must use .elements, because of removal of elements
for e in targetElement.elements:
if isinstance(e, (clef.Clef, meter.TimeSignature)):
targetElement.remove(e)
v.append(copy.deepcopy(targetElement)) # Appends a copy
# If a non-initial insertion or a FINAL deletion,
# add the previous element after the variant.
# #elif ((variantType == 'deletion' and (isFinal is True)) or
# (type == 'insertion' and (isInitial is False))):
else:
targetElement = _getPreviousElement(s, v)
newVariantOffset = targetElement.getOffsetBySite(s)
# Need to shift elements to make way for new element at front
offsetShift = targetElement.duration.quarterLength
for e in v.containedSite:
oldOffset = e.getOffsetBySite(v.containedSite)
e.setOffsetBySite(v.containedSite, oldOffset + offsetShift)
v.insert(0.0, copy.deepcopy(targetElement))
s.remove(v)
s.insert(newVariantOffset, v)
# Give it a new replacementDuration including the added element
oldReplacementQuarterLength = v.replacementQuarterLength
v.replacementQuarterLength = common.opFrac(
oldReplacementQuarterLength + targetElement.duration.quarterLength)
def _getNextElements(s, v, numberOfElements=1):
# noinspection PyShadowingNames, GrazieInspection
'''
This is a helper function for makeAllVariantsReplacements() which returns the next element in s
of the type of elements found in the variant v so that it can be added to v.
>>> # * *
>>> s1 = converter.parse('tinyNotation: 4/4 b4 c d e f4 g a b d4 e f g ', makeNotation=False)
>>> s2 = converter.parse('tinyNotation: 4/4 e4 f g a b4 c d e d4 e f g ', makeNotation=False)
>>> # insertion deletion
>>> s1.makeMeasures(inPlace=True)
>>> s2.makeMeasures(inPlace=True)
>>> mergedStream = variant.mergeVariants(s1, s2, 'london')
>>> for v in mergedStream.getElementsByClass(variant.Variant):
... returnElement = variant._getNextElements(mergedStream, v)
... print(returnElement)
<music21.stream.Measure 1 offset=0.0>
<music21.stream.Measure 3 offset=8.0>
This also works on streams with variants that contain notes and rests rather than measures.
>>> s = converter.parse('tinyNotation: 4/4 e4 b b b f4 f f f g4 a a a ', makeNotation=False)
>>> v1Stream = converter.parse('tinyNotation: 4/4 a4 a a a ', makeNotation=False)
>>> # initial insertion
>>> v1 = variant.Variant(v1Stream.notes)
>>> v1.replacementQuarterLength = 0.0
>>> v1.groups = ['london']
>>> s.insert(0.0, v1)
>>> v2 = variant.Variant()
>>> v2.replacementQuarterLength = 4.0
>>> v2.groups = ['london']
>>> s.insert(4.0, v2)
>>> for v in s[variant.Variant]:
... returnElement = variant._getNextElements(s, v)
... print(returnElement)
<music21.note.Note E>
<music21.note.Note G>
'''
replacedElements = v.replacedElements(s)
lengthType = v.lengthType
# Get class of elements in variant or replaced Region
if lengthType == 'elongation':
vClass = type(v.getElementsByClass(['Measure', 'Note', 'Rest']).first())
if isinstance(vClass, note.GeneralNote):
vClass = note.GeneralNote
else:
vClass = type(replacedElements.getElementsByClass(['Measure', 'Note', 'Rest']).first())
if isinstance(vClass, note.GeneralNote):
vClass = note.GeneralNote
# Get next element in s after v which is of type vClass
if lengthType == 'elongation':
variantOffset = v.getOffsetBySite(s)
potentialTargets = s.getElementsByOffset(variantOffset,
offsetEnd=s.highestTime,
includeEndBoundary=True,
mustFinishInSpan=False,
mustBeginInSpan=True,
classList=[vClass])
returnElement = potentialTargets.first()
else:
replacementDuration = v.replacementQuarterLength
variantOffset = v.getOffsetBySite(s)
potentialTargets = s.getElementsByOffset(variantOffset + replacementDuration,
offsetEnd=s.highestTime,
includeEndBoundary=True,
mustFinishInSpan=False,
mustBeginInSpan=True,
classList=[vClass])
returnElement = potentialTargets.first()
return returnElement
def _getPreviousElement(s, v):
# noinspection PyShadowingNames,GrazieInspection
'''
This is a helper function for makeAllVariantsReplacements() which returns
the previous element in s
of the type of elements found in the variant v so that it can be added to v.
>>> # * *
>>> s1 = converter.parse('tinyNotation: 4/4 a4 b c d b4 c d e f4 g a b ')
>>> s2 = converter.parse('tinyNotation: 4/4 a4 b c d e4 f g a b4 c d e ')
>>> # insertion deletion
>>> s1.makeMeasures(inPlace=True)
>>> s2.makeMeasures(inPlace=True)
>>> mergedStream = variant.mergeVariants(s1, s2, 'london')
>>> for v in mergedStream[variant.Variant]:
... returnElement = variant._getPreviousElement(mergedStream, v)
... print(returnElement)
<music21.stream.Measure 1 offset=0.0>
<music21.stream.Measure 2 offset=4.0>
This also works on streams with variants that contain notes and rests rather than measures.
>>> s = converter.parse('tinyNotation: 4/4 b4 b b a e4 b b b g4 e e e ', makeNotation=False)
>>> v1Stream = converter.parse('tinyNotation: 4/4 f4 f f f ', makeNotation=False)
>>> # insertion final deletion
>>> v1 = variant.Variant(v1Stream.notes)
>>> v1.replacementQuarterLength = 0.0
>>> v1.groups = ['london']
>>> s.insert(4.0, v1)
>>> v2 = variant.Variant()
>>> v2.replacementQuarterLength = 4.0
>>> v2.groups = ['london']
>>> s.insert(8.0, v2)
>>> for v in s[variant.Variant]:
... returnElement = variant._getPreviousElement(s, v)
... print(returnElement)
<music21.note.Note A>
<music21.note.Note B>
'''
replacedElements = v.replacedElements(s)
lengthType = v.lengthType
# Get class of elements in variant or replaced Region
foundStream = None
if lengthType == 'elongation':
foundStream = v.getElementsByClass(['Measure', 'Note', 'Rest'])
else:
foundStream = replacedElements.getElementsByClass(['Measure', 'Note', 'Rest'])
if not foundStream:
raise VariantException('Cannot find any Measures, Notes, or Rests in variant')
vClass = type(foundStream[0])
if isinstance(vClass, note.GeneralNote):
vClass = note.GeneralNote
# Get next element in s after v which is of type vClass
variantOffset = v.getOffsetBySite(s)
potentialTargets = s.getElementsByOffset(
0.0,
offsetEnd=variantOffset,
includeEndBoundary=False,
mustFinishInSpan=False,
mustBeginInSpan=True,
).getElementsByClass(vClass)
returnElement = potentialTargets.last()
return returnElement
[docs]
def makeVariantBlocks(s):
'''
Unknown and undocumented. Used only in lily/translate -- for musicdiff.
'''
from music21 import variant
variantsToBeDone = s.getElementsByClass(variant.Variant)
for v in variantsToBeDone:
startOffset = s.elementOffset(v)
endOffset = v.replacementQuarterLength + startOffset
conflictingVariants = s.getElementsByOffset(offsetStart=startOffset,
offsetEnd=endOffset,
includeEndBoundary=False,
mustFinishInSpan=False,
mustBeginInSpan=True,
classList=[variant.Variant])
for cV in conflictingVariants:
oldReplacementDuration = cV.replacementQuarterLength
if s.elementOffset(cV) == startOffset:
continue # do nothing
else:
shiftOffset = s.elementOffset(cV) - startOffset
r = note.Rest()
r.duration.quarterLength = shiftOffset
r.style.hideObjectOnPrint = True
for el in cV._stream:
oldOffset = el.getOffsetBySite(cV._stream)
cV._stream.coreSetElementOffset(el, oldOffset + shiftOffset)
cV.coreElementsChanged()
cV.insert(0.0, r)
cV.replacementQuarterLength = oldReplacementDuration
s.remove(cV)
s.insert(startOffset, cV)
variantsToBeDone.append(cV)
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
def testCopyAndDeepcopy(self):
from music21.test.commonTest import testCopyAll
testCopyAll(self, globals())
def pitchOut(self, listIn):
out = '['
for p in listIn:
out += str(p) + ', '
out = out[0:len(out) - 2]
out += ']'
return out
def testBasicA(self):
o = Variant()
o.append(note.Note('G3', quarterLength=2.0))
o.append(note.Note('f3', quarterLength=2.0))
self.assertEqual(o.highestOffset, 0)
self.assertEqual(o.highestTime, 0)
o.exposeTime = True
self.assertEqual(o.highestOffset, 2.0)
self.assertEqual(o.highestTime, 4.0)
def testBasicB(self):
'''
Testing relaying attributes requests to private Stream with __getattr__
'''
v = Variant()
v.append(note.Note('G3', quarterLength=2.0))
v.append(note.Note('f3', quarterLength=2.0))
# these are Stream attributes
self.assertEqual(v.highestOffset, 0.0)
self.assertEqual(v.highestTime, 0.0)
self.assertEqual(len(v.notes), 2)
self.assertTrue(v.getElementsByClass('Note'))
v.pop(1) # remove the last item
self.assertEqual(v.highestOffset, 0.0)
self.assertEqual(v.highestTime, 0.0)
self.assertEqual(len(v.notes), 1)
def testVariantGroupA(self):
'''
Variant groups are used to distinguish
'''
v1 = Variant()
v1.groups.append('alt-a')
v1 = Variant()
v1.groups.append('alt-b')
self.assertIn('alt-b', v1.groups)
def testVariantClassA(self):
m1 = stream.Measure()
v1 = Variant()
v1.append(m1)
self.assertIn('Variant', v1.classes)
self.assertFalse(v1.getElementsByClass(Variant))
self.assertTrue(v1.getElementsByClass(stream.Measure))
def testDeepCopyVariantA(self):
from music21 import variant
s = stream.Stream()
s.repeatAppend(note.Note('G4'), 8)
vn1 = note.Note('F#4')
vn2 = note.Note('A-4')
v1 = variant.Variant()
v1.insert(0, vn1)
v1.insert(0, vn2)
v1Copy = copy.deepcopy(v1)
# copies stored objects; they point to the different Notes vn1/vn2
self.assertIsNot(v1Copy[0], v1[0])
self.assertIsNot(v1Copy[1], v1[1])
self.assertIs(v1[0], vn1)
self.assertIsNot(v1Copy[0], vn1)
# normal in-place variant functionality
s.insert(5, v1)
self.assertEqual(self.pitchOut(s.pitches),
'[G4, G4, G4, G4, G4, G4, G4, G4]')
sv = s.activateVariants(inPlace=False)
self.assertEqual(self.pitchOut(sv.pitches),
'[G4, G4, G4, G4, G4, F#4, A-4, G4, G4]')
# test functionality on a deepcopy
sCopy = copy.deepcopy(s)
self.assertEqual(len(sCopy.getElementsByClass(variant.Variant)), 1)
self.assertEqual(self.pitchOut(sCopy.pitches),
'[G4, G4, G4, G4, G4, G4, G4, G4]')
sCopy.activateVariants(inPlace=True)
self.assertEqual(self.pitchOut(sCopy.pitches),
'[G4, G4, G4, G4, G4, F#4, A-4, G4, G4]')
def testDeepCopyVariantB(self):
from music21 import variant
s = stream.Stream()
s.repeatAppend(note.Note('G4'), 8)
vn1 = note.Note('F#4')
vn2 = note.Note('A-4')
v1 = variant.Variant()
v1.insert(0, vn1)
v1.insert(0, vn2)
s.insert(5, v1)
# as we deepcopy the elements in the variants, we have new Notes
sCopy = copy.deepcopy(s)
sCopy.activateVariants(inPlace=True)
self.assertEqual(self.pitchOut(sCopy.pitches),
'[G4, G4, G4, G4, G4, F#4, A-4, G4, G4]')
# can transpose the note in place
sCopy.notes[5].transpose(12, inPlace=True)
self.assertEqual(self.pitchOut(sCopy.pitches),
'[G4, G4, G4, G4, G4, F#5, A-4, G4, G4]')
# however, if the Variant deepcopy still references the original
# notes it had, then when we try to activate the variant in the
# in original Stream, we would get unexpected results (the octave shift)
s.activateVariants(inPlace=True)
self.assertEqual(self.pitchOut(s.pitches),
'[G4, G4, G4, G4, G4, F#4, A-4, G4, G4]')
class TestExternal(unittest.TestCase):
show = True
def testMergeJacopoVariants(self):
from music21 import corpus
j1 = corpus.parse('trecento/PMFC_06-Jacopo-03a')
j2 = corpus.parse('trecento/PMFC_06-Jacopo-03b')
jMerged = mergeVariantScores(j1, j2)
if self.show:
jMerged.show('musicxml.png')
if __name__ == '__main__':
import music21
music21.mainTest(Test) # , TestExternal)