1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         variant.py
4# Purpose:      Translate MusicXML and music21 objects
5#
6# Authors:      Christopher Ariza
7#               Evan Lynch
8#               Michael Scott Cuthbert
9#
10# Copyright:    Copyright © 2012 Michael Scott Cuthbert and the music21 Project
11# License:      BSD, see license.txt
12# ------------------------------------------------------------------------------
13# currently the tinyNotation demos use alignment to show variation, making this necessary.
14
15# pylint: disable=line-too-long
16# all other lines are linted.
17'''
18Contains :class:`~music21.variant.Variant` and its subclasses, as well as functions for merging
19and showing different variant streams. These functions and the variant class should only be
20used when variants of a score are the same length and contain the same measure structure at
21this time.
22'''
23from typing import Union
24import unittest
25
26import copy
27import difflib
28
29from music21 import base
30from music21 import clef
31from music21 import common
32from music21 import environment
33from music21 import exceptions21
34from music21 import meter
35from music21 import note
36from music21 import search
37from music21 import stream
38
39_MOD = 'variant'
40environLocal = environment.Environment(_MOD)
41
42
43# ------Public Merge Functions
44def mergeVariants(streamX, streamY, variantName='variant', *, inPlace=False):
45    # noinspection PyShadowingNames
46    '''
47    Takes two streams objects or their derivatives (Score, Part, Measure, etc.) which
48    should be variant versions of the same stream,
49    and merges them (determines differences and stores those differences as variant objects
50    in streamX) via the appropriate merge
51    function for their type. This will not know how to deal with scores meant for
52    mergePartAsOssia(). If this is the intention, use
53    that function instead.
54
55    >>> streamX = converter.parse('tinynotation: 4/4 a4 b  c d', makeNotation=False)
56    >>> streamY = converter.parse('tinynotation: 4/4 a4 b- c e', makeNotation=False)
57
58    >>> mergedStream = variant.mergeVariants(streamX, streamY,
59    ...                                      variantName='docVariant', inPlace=False)
60    >>> mergedStream.show('text')
61    {0.0} <music21.meter.TimeSignature 4/4>
62    {0.0} <music21.note.Note A>
63    {1.0} <music21.variant.Variant object of length 1.0>
64    {1.0} <music21.note.Note B>
65    {2.0} <music21.note.Note C>
66    {3.0} <music21.variant.Variant object of length 1.0>
67    {3.0} <music21.note.Note D>
68
69    >>> v0 = mergedStream.getElementsByClass('Variant').first()
70    >>> v0
71    <music21.variant.Variant object of length 1.0>
72    >>> v0.first()
73    <music21.note.Note B->
74
75    >>> streamZ = converter.parse('tinynotation: 4/4 a4 b c d e f g a', makeNotation=False)
76    >>> variant.mergeVariants(streamX, streamZ, variantName='docVariant', inPlace=False)
77    Traceback (most recent call last):
78    music21.variant.VariantException: Could not determine what merging method to use.
79            Try using a more specific merging function.
80
81
82    Example: Create a main score (aScore) and a variant score (vScore), each with
83    two parts (ap1/vp1
84    and ap2/vp2) and some small variants between ap1/vp1 and ap2/vp2, marked with * below.
85
86    >>> aScore = stream.Score()
87    >>> vScore = stream.Score()
88
89    >>> #                                                 *
90    >>> ap1 = converter.parse('tinynotation: 4/4   a4 b c d    e2 f   g2 f4 g ')
91    >>> vp1 = converter.parse('tinynotation: 4/4   a4 b c e    e2 f   g2 f4 a ')
92
93    >>> #                                                         *    *    *
94    >>> ap2 = converter.parse('tinynotation: 4/4   a4 g f e    f2 e   d2 g4 f ')
95    >>> vp2 = converter.parse('tinynotation: 4/4   a4 g f e    f2 g   f2 g4 d ')
96
97    >>> ap1.id = 'aPart1'
98    >>> ap2.id = 'aPart2'
99
100    >>> aScore.insert(0.0, ap1)
101    >>> aScore.insert(0.0, ap2)
102    >>> vScore.insert(0.0, vp1)
103    >>> vScore.insert(0.0, vp2)
104
105    Create one merged score where everything different in vScore from aScore is called a variant.
106
107    >>> mergedScore = variant.mergeVariants(aScore, vScore, variantName='docVariant', inPlace=False)
108    >>> mergedScore.show('text')
109    {0.0} <music21.stream.Part aPart1>
110        {0.0} <music21.variant.Variant object of length 4.0>
111        {0.0} <music21.stream.Measure 1 offset=0.0>
112            {0.0} <music21.clef.TrebleClef>
113            {0.0} <music21.meter.TimeSignature 4/4>
114            {0.0} <music21.note.Note A>
115            {1.0} <music21.note.Note B>
116            {2.0} <music21.note.Note C>
117            {3.0} <music21.note.Note D>
118        {4.0} <music21.stream.Measure 2 offset=4.0>
119            {0.0} <music21.note.Note E>
120            {2.0} <music21.note.Note F>
121        {8.0} <music21.variant.Variant object of length 4.0>
122        {8.0} <music21.stream.Measure 3 offset=8.0>
123            {0.0} <music21.note.Note G>
124            {2.0} <music21.note.Note F>
125            {3.0} <music21.note.Note G>
126            {4.0} <music21.bar.Barline type=final>
127    {0.0} <music21.stream.Part aPart2>
128        {0.0} <music21.stream.Measure 1 offset=0.0>
129            {0.0} <music21.clef.TrebleClef>
130            {0.0} <music21.meter.TimeSignature 4/4>
131            {0.0} <music21.note.Note A>
132            {1.0} <music21.note.Note G>
133            {2.0} <music21.note.Note F>
134            {3.0} <music21.note.Note E>
135        {4.0} <music21.variant.Variant object of length 8.0>
136        {4.0} <music21.stream.Measure 2 offset=4.0>
137            {0.0} <music21.note.Note F>
138            {2.0} <music21.note.Note E>
139        {8.0} <music21.stream.Measure 3 offset=8.0>
140            {0.0} <music21.note.Note D>
141            {2.0} <music21.note.Note G>
142            {3.0} <music21.note.Note F>
143            {4.0} <music21.bar.Barline type=final>
144
145
146    >>> mergedPart = variant.mergeVariants(ap2, vp2, variantName='docVariant', inPlace=False)
147    >>> mergedPart.show('text')
148    {0.0} <music21.stream.Measure 1 offset=0.0>
149    ...
150    {4.0} <music21.variant.Variant object of length 8.0>
151    {4.0} <music21.stream.Measure 2 offset=4.0>
152    ...
153        {4.0} <music21.bar.Barline type=final>
154    '''
155    classesX = streamX.classes
156    if 'Score' in classesX:
157        return mergeVariantScores(streamX, streamY, variantName, inPlace=inPlace)
158    elif streamX.getElementsByClass('Measure'):
159        return mergeVariantMeasureStreams(streamX, streamY, variantName, inPlace=inPlace)
160    elif (streamX.iter().notesAndRests
161            and streamX.duration.quarterLength == streamY.duration.quarterLength):
162        return mergeVariantsEqualDuration([streamX, streamY], [variantName], inPlace=inPlace)
163    else:
164        raise VariantException(
165            'Could not determine what merging method to use. '
166            + 'Try using a more specific merging function.')
167
168
169def mergeVariantScores(aScore, vScore, variantName='variant', *, inPlace=False):
170    # noinspection PyShadowingNames
171    '''
172    Takes two scores and merges them with mergeVariantMeasureStreams, part-by-part.
173
174    >>> aScore, vScore = stream.Score(), stream.Score()
175
176    >>> ap1 = converter.parse('tinynotation: 4/4   a4 b c d    e2 f2   g2 f4 g4 ')
177    >>> vp1 = converter.parse('tinynotation: 4/4   a4 b c e    e2 f2   g2 f4 a4 ')
178
179    >>> ap2 = converter.parse('tinynotation: 4/4   a4 g f e    f2 e2   d2 g4 f4 ')
180    >>> vp2 = converter.parse('tinynotation: 4/4   a4 g f e    f2 g2   f2 g4 d4 ')
181
182    >>> aScore.insert(0.0, ap1)
183    >>> aScore.insert(0.0, ap2)
184    >>> vScore.insert(0.0, vp1)
185    >>> vScore.insert(0.0, vp2)
186
187    >>> mergedScores = variant.mergeVariantScores(aScore, vScore,
188    ...                                           variantName='docVariant', inPlace=False)
189    >>> mergedScores.show('text')
190    {0.0} <music21.stream.Part ...>
191        {0.0} <music21.variant.Variant object of length 4.0>
192        {0.0} <music21.stream.Measure 1 offset=0.0>
193            {0.0} <music21.clef.TrebleClef>
194            {0.0} <music21.meter.TimeSignature 4/4>
195            {0.0} <music21.note.Note A>
196            {1.0} <music21.note.Note B>
197            {2.0} <music21.note.Note C>
198            {3.0} <music21.note.Note D>
199        {4.0} <music21.stream.Measure 2 offset=4.0>
200            {0.0} <music21.note.Note E>
201            {2.0} <music21.note.Note F>
202        {8.0} <music21.variant.Variant object of length 4.0>
203        {8.0} <music21.stream.Measure 3 offset=8.0>
204            {0.0} <music21.note.Note G>
205            {2.0} <music21.note.Note F>
206            {3.0} <music21.note.Note G>
207            {4.0} <music21.bar.Barline type=final>
208    {0.0} <music21.stream.Part ...>
209        {0.0} <music21.stream.Measure 1 offset=0.0>
210            {0.0} <music21.clef.TrebleClef>
211            {0.0} <music21.meter.TimeSignature 4/4>
212            {0.0} <music21.note.Note A>
213            {1.0} <music21.note.Note G>
214            {2.0} <music21.note.Note F>
215            {3.0} <music21.note.Note E>
216        {4.0} <music21.variant.Variant object of length 8.0>
217        {4.0} <music21.stream.Measure 2 offset=4.0>
218            {0.0} <music21.note.Note F>
219            {2.0} <music21.note.Note E>
220        {8.0} <music21.stream.Measure 3 offset=8.0>
221            {0.0} <music21.note.Note D>
222            {2.0} <music21.note.Note G>
223            {3.0} <music21.note.Note F>
224            {4.0} <music21.bar.Barline type=final>
225    '''
226    if len(aScore.iter().parts) != len(vScore.iter().parts):
227        raise VariantException(
228            'These scores do not have the same number of parts and cannot be merged.')
229
230    if inPlace is True:
231        returnObj = aScore
232    else:
233        returnObj = aScore.coreCopyAsDerivation('mergeVariantScores')
234
235    for returnPart, vPart in zip(returnObj.parts, vScore.parts):
236        mergeVariantMeasureStreams(returnPart, vPart, variantName, inPlace=True)
237
238    if inPlace is False:
239        return returnObj
240
241
242def mergeVariantMeasureStreams(streamX, streamY, variantName='variant', *, inPlace=False):
243    '''
244    Takes two streams of measures and returns a stream (new if inPlace is False) with the second
245    merged with the first as variants. This function differs from mergeVariantsEqualDuration by
246    dealing with streams that are of different length. This function matches measures that are
247    exactly equal and creates variant objects for regions of measures that differ at all. If more
248    refined variants are sought (with variation within the bar considered and related but different
249    bars associated with each other), use variant.refineVariant().
250
251    In this example, the second bar has been deleted in the second version,
252    a new bar has been inserted between the
253    original third and fourth bars, and two bars have been added at the end.
254
255
256    >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
257    ...            ('a', 'quarter'), ('a', 'quarter')]
258    >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
259    ...            ('a', 'quarter'),('b', 'quarter')]
260    >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'),
261    ...            ('e', 'quarter'), ('e', 'quarter')]
262    >>> data1M4 = [('d', 'quarter'), ('g', 'eighth'), ('g', 'eighth'),
263    ...            ('a', 'quarter'), ('b', 'quarter')]
264
265    >>> data2M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
266    ...            ('a', 'quarter'), ('a', 'quarter')]
267    >>> data2M2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
268    >>> data2M3 = [('e', 'quarter'), ('g', 'eighth'), ('g', 'eighth'),
269    ...            ('a', 'quarter'), ('b', 'quarter')]
270    >>> data2M4 = [('d', 'quarter'), ('g', 'eighth'), ('g', 'eighth'),
271    ...            ('a', 'quarter'), ('b', 'quarter')]
272    >>> data2M5 = [('f', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
273    ...            ('a', 'quarter'), ('b', 'quarter')]
274    >>> data2M6 = [('g', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
275
276    >>> data1 = [data1M1, data1M2, data1M3, data1M4]
277    >>> data2 = [data2M1, data2M2, data2M3, data2M4, data2M5, data2M6]
278    >>> stream1 = stream.Stream()
279    >>> stream2 = stream.Stream()
280    >>> mNumber = 1
281    >>> for d in data1:
282    ...    m = stream.Measure()
283    ...    m.number = mNumber
284    ...    mNumber += 1
285    ...    for pitchName, durType in d:
286    ...        n = note.Note(pitchName)
287    ...        n.duration.type = durType
288    ...        m.append(n)
289    ...    stream1.append(m)
290    >>> mNumber = 1
291    >>> for d in data2:
292    ...    m = stream.Measure()
293    ...    m.number = mNumber
294    ...    mNumber += 1
295    ...    for pitchName, durType in d:
296    ...        n = note.Note(pitchName)
297    ...        n.duration.type = durType
298    ...        m.append(n)
299    ...    stream2.append(m)
300    >>> #_DOCS_SHOW stream1.show()
301
302
303    .. image:: images/variant_measuresStreamMergeStream1.*
304        :width: 600
305
306    >>> #_DOCS_SHOW stream2.show()
307
308
309    .. image:: images/variant_measuresStreamMergeStream2.*
310        :width: 600
311
312    >>> mergedStream = variant.mergeVariantMeasureStreams(stream1, stream2, 'paris', inPlace=False)
313    >>> mergedStream.show('text')
314    {0.0} <music21.stream.Measure 1 offset=0.0>
315        {0.0} <music21.note.Note A>
316        {1.0} <music21.note.Note B>
317        {1.5} <music21.note.Note C>
318        {2.0} <music21.note.Note A>
319        {3.0} <music21.note.Note A>
320    {4.0} <music21.variant.Variant object of length 0.0>
321    {4.0} <music21.stream.Measure 2 offset=4.0>
322        {0.0} <music21.note.Note B>
323        {0.5} <music21.note.Note C>
324        {1.0} <music21.note.Note A>
325        {2.0} <music21.note.Note A>
326        {3.0} <music21.note.Note B>
327    {8.0} <music21.stream.Measure 3 offset=8.0>
328        {0.0} <music21.note.Note C>
329        {1.0} <music21.note.Note D>
330        {2.0} <music21.note.Note E>
331        {3.0} <music21.note.Note E>
332    {12.0} <music21.variant.Variant object of length 4.0>
333    {12.0} <music21.stream.Measure 4 offset=12.0>
334        {0.0} <music21.note.Note D>
335        {1.0} <music21.note.Note G>
336        {1.5} <music21.note.Note G>
337        {2.0} <music21.note.Note A>
338        {3.0} <music21.note.Note B>
339    {16.0} <music21.variant.Variant object of length 8.0>
340
341    >>> mergedStream.variants[0].replacementDuration
342    4.0
343    >>> mergedStream.variants[1].replacementDuration
344    0.0
345
346    >>> parisStream = mergedStream.activateVariants('paris', inPlace=False)
347    >>> parisStream.show('text')
348    {0.0} <music21.stream.Measure 1 offset=0.0>
349        {0.0} <music21.note.Note A>
350        {1.0} <music21.note.Note B>
351        {1.5} <music21.note.Note C>
352        {2.0} <music21.note.Note A>
353        {3.0} <music21.note.Note A>
354    {4.0} <music21.variant.Variant object of length 4.0>
355    {4.0} <music21.stream.Measure 2 offset=4.0>
356        {0.0} <music21.note.Note C>
357        {1.0} <music21.note.Note D>
358        {2.0} <music21.note.Note E>
359        {3.0} <music21.note.Note E>
360    {8.0} <music21.variant.Variant object of length 0.0>
361    {8.0} <music21.stream.Measure 3 offset=8.0>
362        {0.0} <music21.note.Note E>
363        {1.0} <music21.note.Note G>
364        {1.5} <music21.note.Note G>
365        {2.0} <music21.note.Note A>
366        {3.0} <music21.note.Note B>
367    {12.0} <music21.stream.Measure 4 offset=12.0>
368        {0.0} <music21.note.Note D>
369        {1.0} <music21.note.Note G>
370        {1.5} <music21.note.Note G>
371        {2.0} <music21.note.Note A>
372        {3.0} <music21.note.Note B>
373    {16.0} <music21.variant.Variant object of length 0.0>
374    {16.0} <music21.stream.Measure 5 offset=16.0>
375        {0.0} <music21.note.Note F>
376        {0.5} <music21.note.Note C>
377        {1.5} <music21.note.Note A>
378        {2.0} <music21.note.Note A>
379        {3.0} <music21.note.Note B>
380    {20.0} <music21.stream.Measure 6 offset=20.0>
381        {0.0} <music21.note.Note G>
382        {1.0} <music21.note.Note D>
383        {2.0} <music21.note.Note E>
384        {3.0} <music21.note.Note E>
385
386    >>> parisStream.variants[0].replacementDuration
387    0.0
388    >>> parisStream.variants[1].replacementDuration
389    4.0
390    >>> parisStream.variants[2].replacementDuration
391    8.0
392    '''
393    if inPlace is True:
394        returnObj = streamX
395    else:
396        returnObj = streamX.coreCopyAsDerivation('mergeVariantMeasureStreams')
397
398    regions = _getRegionsFromStreams(returnObj, streamY)
399    for (regionType, xRegionStartMeasure, xRegionEndMeasure,
400            yRegionStartMeasure, yRegionEndMeasure) in regions:
401        # Note that the 'end' measure indices are 1 greater
402        # than the 0-indexed number of the measure.
403        if xRegionStartMeasure >= len(returnObj.getElementsByClass('Measure')):
404            startOffset = returnObj.duration.quarterLength
405            # This deals with insertion at the end case where
406            # returnObj.measure(xRegionStartMeasure + 1) does not exist.
407        else:
408            startOffset = returnObj.measure(xRegionStartMeasure + 1).getOffsetBySite(returnObj)
409
410        yRegion = None
411        replacementDuration = 0.0
412
413        if regionType == 'equal':
414            # yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure)
415            continue  # Do nothing
416        elif regionType == 'replace':
417            xRegion = returnObj.measures(xRegionStartMeasure + 1, xRegionEndMeasure)
418            replacementDuration = xRegion.duration.quarterLength
419            yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure)
420        elif regionType == 'delete':
421            xRegion = returnObj.measures(xRegionStartMeasure + 1, xRegionEndMeasure)
422            replacementDuration = xRegion.duration.quarterLength
423            yRegion = None
424        elif regionType == 'insert':
425            yRegion = streamY.measures(yRegionStartMeasure + 1, yRegionEndMeasure)
426            replacementDuration = 0.0
427        else:
428            raise VariantException(f'Unknown regionType {regionType!r}')
429        addVariant(returnObj, startOffset, yRegion,
430                   variantName=variantName, replacementDuration=replacementDuration)
431
432    if inPlace is True:
433        return
434    else:
435        return returnObj
436
437
438def mergeVariantsEqualDuration(streams, variantNames, *, inPlace=False):
439    '''
440    Pass this function a list of streams (they must be of the same
441    length or a VariantException will be raised).
442    It will return a stream which merges the differences between the
443    streams into variant objects keeping the
444    first stream in the list as the default. If inPlace is True, the
445    first stream in the list will be modified,
446    otherwise a new stream will be returned. Pass a list of names to
447    associate variants with their sources, if this list
448    does not contain an entry for each non-default variant,
449    naming may not behave properly. Variants that have the
450    same differences from the default will be saved as separate
451    variant objects (i.e. more than once under different names).
452    Also, note that a streams with bars of differing lengths will not behave properly.
453
454
455    >>> stream1 = stream.Stream()
456    >>> stream2paris = stream.Stream()
457    >>> stream3london = stream.Stream()
458    >>> data1 = [('a', 'quarter'), ('b', 'eighth'),
459    ...    ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
460    ...    ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
461    ...    ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')]
462    >>> data2 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter'),
463    ...    ('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), ('a', 'quarter'),
464    ...    ('b', 'quarter'), ('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter')]
465    >>> data3 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
466    ...    ('a', 'quarter'), ('a', 'quarter'),
467    ...    ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
468    ...    ('c', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')]
469    >>> for pitchName, durType in data1:
470    ...    n = note.Note(pitchName)
471    ...    n.duration.type = durType
472    ...    stream1.append(n)
473    >>> for pitchName, durType in data2:
474    ...    n = note.Note(pitchName)
475    ...    n.duration.type = durType
476    ...    stream2paris.append(n)
477    >>> for pitchName, durType in data3:
478    ...    n = note.Note(pitchName)
479    ...    n.duration.type = durType
480    ...    stream3london.append(n)
481    >>> mergedStreams = variant.mergeVariantsEqualDuration(
482    ...       [stream1, stream2paris, stream3london], ['paris', 'london'])
483    >>> mergedStreams.show('t')
484    {0.0} <music21.note.Note A>
485    {1.0} <music21.variant.Variant object of length 1.0>
486    {1.0} <music21.note.Note B>
487    {1.5} <music21.note.Note C>
488    {2.0} <music21.note.Note A>
489    {3.0} <music21.variant.Variant object of length 1.0>
490    {3.0} <music21.note.Note A>
491    {4.0} <music21.note.Note B>
492    {4.5} <music21.variant.Variant object of length 1.5>
493    {4.5} <music21.note.Note C>
494    {5.0} <music21.note.Note A>
495    {6.0} <music21.note.Note A>
496    {7.0} <music21.variant.Variant object of length 1.0>
497    {7.0} <music21.note.Note B>
498    {8.0} <music21.note.Note C>
499    {9.0} <music21.variant.Variant object of length 2.0>
500    {9.0} <music21.note.Note D>
501    {10.0} <music21.note.Note E>
502
503    >>> mergedStreams.activateVariants('london').show('t')
504    {0.0} <music21.note.Note A>
505    {1.0} <music21.variant.Variant object of length 1.0>
506    {1.0} <music21.note.Note B>
507    {1.5} <music21.note.Note C>
508    {2.0} <music21.note.Note A>
509    {3.0} <music21.variant.Variant object of length 1.0>
510    {3.0} <music21.note.Note A>
511    {4.0} <music21.note.Note B>
512    {4.5} <music21.variant.Variant object of length 1.5>
513    {4.5} <music21.note.Note C>
514    {5.0} <music21.note.Note A>
515    {6.0} <music21.note.Note A>
516    {7.0} <music21.variant.Variant object of length 1.0>
517    {7.0} <music21.note.Note C>
518    {8.0} <music21.note.Note C>
519    {9.0} <music21.variant.Variant object of length 2.0>
520    {9.0} <music21.note.Note D>
521    {10.0} <music21.note.Note E>
522
523    If the streams contain parts and measures, the merge function will iterate
524    through them and determine
525    and store variant differences within each measure/part.
526
527    >>> stream1 = stream.Stream()
528    >>> stream2 = stream.Stream()
529    >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
530    ...            ('a', 'quarter'), ('a', 'quarter')]
531    >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
532    ...            ('a', 'quarter'),('b', 'quarter')]
533    >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
534    >>> data2M1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')]
535    >>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
536    ...            ('a', 'quarter'), ('b', 'quarter')]
537    >>> data2M3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
538    >>> data1 = [data1M1, data1M2, data1M3]
539    >>> data2 = [data2M1, data2M2, data2M3]
540    >>> tempPart = stream.Part()
541    >>> for d in data1:
542    ...    m = stream.Measure()
543    ...    for pitchName, durType in d:
544    ...        n = note.Note(pitchName)
545    ...        n.duration.type = durType
546    ...        m.append(n)
547    ...    tempPart.append(m)
548    >>> stream1.append(tempPart)
549    >>> tempPart = stream.Part()
550    >>> for d in data2:
551    ...    m = stream.Measure()
552    ...    for pitchName, durType in d:
553    ...        n = note.Note(pitchName)
554    ...        n.duration.type = durType
555    ...        m.append(n)
556    ...    tempPart.append(m)
557    >>> stream2.append(tempPart)
558    >>> mergedStreams = variant.mergeVariantsEqualDuration([stream1, stream2], ['paris'])
559    >>> mergedStreams.show('t')
560    {0.0} <music21.stream.Part ...>
561        {0.0} <music21.stream.Measure 0 offset=0.0>
562            {0.0} <music21.note.Note A>
563            {1.0} <music21.variant.Variant object of length 1.0>
564            {1.0} <music21.note.Note B>
565            {1.5} <music21.note.Note C>
566            {2.0} <music21.note.Note A>
567            {3.0} <music21.variant.Variant object of length 1.0>
568            {3.0} <music21.note.Note A>
569        {4.0} <music21.stream.Measure 0 offset=4.0>
570            {0.0} <music21.note.Note B>
571            {0.5} <music21.variant.Variant object of length 1.5>
572            {0.5} <music21.note.Note C>
573            {1.0} <music21.note.Note A>
574            {2.0} <music21.note.Note A>
575            {3.0} <music21.note.Note B>
576        {8.0} <music21.stream.Measure 0 offset=8.0>
577            {0.0} <music21.note.Note C>
578            {1.0} <music21.variant.Variant object of length 3.0>
579            {1.0} <music21.note.Note D>
580            {2.0} <music21.note.Note E>
581            {3.0} <music21.note.Note E>
582    >>> #_DOCS_SHOW mergedStreams.show()
583
584
585    .. image:: images/variant_measuresAndParts.*
586        :width: 600
587
588
589    >>> for p in mergedStreams.getElementsByClass('Part'):
590    ...    for m in p.getElementsByClass('Measure'):
591    ...        m.activateVariants('paris', inPlace=True)
592    >>> mergedStreams.show('t')
593    {0.0} <music21.stream.Part ...>
594        {0.0} <music21.stream.Measure 0 offset=0.0>
595            {0.0} <music21.note.Note A>
596            {1.0} <music21.variant.Variant object of length 1.0>
597            {1.0} <music21.note.Note B>
598            {2.0} <music21.note.Note A>
599            {3.0} <music21.variant.Variant object of length 1.0>
600            {3.0} <music21.note.Note G>
601        {4.0} <music21.stream.Measure 0 offset=4.0>
602            {0.0} <music21.note.Note B>
603            {0.5} <music21.variant.Variant object of length 1.5>
604            {0.5} <music21.note.Note C>
605            {1.5} <music21.note.Note A>
606            {2.0} <music21.note.Note A>
607            {3.0} <music21.note.Note B>
608        {8.0} <music21.stream.Measure 0 offset=8.0>
609            {0.0} <music21.note.Note C>
610            {1.0} <music21.variant.Variant object of length 3.0>
611            {1.0} <music21.note.Note B>
612            {2.0} <music21.note.Note A>
613            {3.0} <music21.note.Note A>
614    >>> #_DOCS_SHOW mergedStreams.show()
615
616
617    .. image:: images/variant_measuresAndParts2.*
618        :width: 600
619
620    If barlines do not match up, an exception will be thrown. Here two streams that are identical
621    are merged, except one is in 3/4, the other in 4/4. This throws an exception.
622
623    >>> streamDifferentMeasures = stream.Stream()
624    >>> dataDiffM1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter')]
625    >>> dataDiffM2 = [ ('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter')]
626    >>> dataDiffM3 = [('a', 'quarter'), ('b', 'quarter'), ('c', 'quarter')]
627    >>> dataDiffM4 = [('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
628    >>> dataDiff = [dataDiffM1, dataDiffM2, dataDiffM3, dataDiffM4]
629    >>> streamDifferentMeasures.insert(0.0, meter.TimeSignature('3/4'))
630    >>> tempPart = stream.Part()
631    >>> for d in dataDiff:
632    ...    m = stream.Measure()
633    ...    for pitchName, durType in d:
634    ...        n = note.Note(pitchName)
635    ...        n.duration.type = durType
636    ...        m.append(n)
637    ...    tempPart.append(m)
638    >>> streamDifferentMeasures.append(tempPart)
639    >>> mergedStreams = variant.mergeVariantsEqualDuration(
640    ...                 [stream1, streamDifferentMeasures], ['paris'])
641    Traceback (most recent call last):
642    music21.variant.VariantException: _mergeVariants cannot merge streams
643        which are of different lengths
644    '''
645
646    if inPlace is True:
647        returnObj = streams[0]
648    else:
649        returnObj = streams[0].coreCopyAsDerivation('mergeVariantsEqualDuration')
650
651    # Adds a None element at beginning (corresponding to default variant streams[0])
652    variantNames.insert(0, None)
653    while len(streams) > len(variantNames):  # Adds Blank names if too few
654        variantNames.append(None)
655    while len(streams) < len(variantNames):  # Removes extra names
656        variantNames.pop()
657
658    zipped = list(zip(streams, variantNames))
659
660    for s, variantName in zipped[1:]:
661        if returnObj.highestTime != s.highestTime:
662            raise VariantException('cannot merge streams of different lengths')
663
664        returnObjParts = returnObj.getElementsByClass('Part')
665        if returnObjParts:  # If parts exist, iterate through them.
666            sParts = s.getElementsByClass('Part')
667            for i, returnObjPart in enumerate(returnObjParts):
668                sPart = sParts[i]
669
670                returnObjMeasures = returnObjPart.getElementsByClass('Measure')
671                if returnObjMeasures:
672                    # If measures exist and parts exist, iterate through them both.
673                    for j, returnObjMeasure in enumerate(returnObjMeasures):
674                        sMeasure = sPart.getElementsByClass('Measure')[j]
675                        _mergeVariants(
676                            returnObjMeasure, sMeasure, variantName=variantName, inPlace=True)
677
678                else:  # If parts exist but no measures.
679                    _mergeVariants(returnObjPart, sPart, variantName=variantName, inPlace=True)
680        else:
681            returnObjMeasures = returnObj.getElementsByClass('Measure')
682            if returnObjMeasures:  # If no parts, but still measures, iterate through them.
683                for j, returnObjMeasure in enumerate(returnObjMeasures):
684                    returnObjMeasure = returnObjMeasures[j]
685                    sMeasure = s.getElementsByClass('Measure')[j]
686                    _mergeVariants(returnObjMeasure, sMeasure,
687                                   variantName=variantName, inPlace=True)
688            else:  # If no parts and no measures.
689                _mergeVariants(returnObj, s, variantName=variantName, inPlace=True)
690
691    return returnObj
692
693
694def mergePartAsOssia(mainPart, ossiaPart, ossiaName,
695                     inPlace=False, compareByMeasureNumber=False, recurseInMeasures=False):
696    # noinspection PyShadowingNames
697    '''
698    Some MusicXML files are generated with full parts that have only a few non-rest measures
699    instead of ossia parts, such as those
700    created by Sibelius 7. This function
701    takes two streams (mainPart and ossiaPart), the second interpreted as an ossia.
702    It outputs a stream with the ossia part merged into the stream as a
703    group of variants.
704
705    If compareByMeasureNumber is True, then the ossia measures will be paired with the
706    measures in the mainPart that have the
707    same measure.number. Otherwise, they will be paired by offset. In most cases
708    these should have the same result.
709
710    Note that this method has no way of knowing if a variant is supposed to be a
711    different duration than the segment of stream which it replaces
712    because that information is not contained in the format of score this method is
713    designed to deal with.
714
715
716    >>> 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      ')
717    >>> ossiaStream = converter.parse('tinynotation: 4/4  r1            r1    r1        E4 E4 F4 G4   r1         F2    F2      r1      ')
718    >>> mainStream.makeMeasures(inPlace=True)
719    >>> ossiaStream.makeMeasures(inPlace=True)
720
721    >>> mainPart = stream.Part()
722    >>> for m in mainStream:
723    ...    mainPart.insert(m.offset, m)
724    >>> ossiaPart = stream.Part()
725    >>> for m in ossiaStream:
726    ...    ossiaPart.insert(m.offset, m)
727
728    >>> s = stream.Stream()
729    >>> s.insert(0.0, ossiaPart)
730    >>> s.insert(0.0, mainPart)
731    >>> #_DOCS_SHOW s.show()
732
733    >>> mainPartWithOssiaVariantsFT = variant.mergePartAsOssia(mainPart, ossiaPart,
734    ...                                                            ossiaName='Parisian_Variant',
735    ...                                                            inPlace=False,
736    ...                                                            compareByMeasureNumber=False,
737    ...                                                            recurseInMeasures=True)
738    >>> mainPartWithOssiaVariantsTT = variant.mergePartAsOssia(mainPart, ossiaPart,
739    ...                                                            ossiaName='Parisian_Variant',
740    ...                                                            inPlace=False,
741    ...                                                            compareByMeasureNumber=True,
742    ...                                                            recurseInMeasures=True)
743    >>> mainPartWithOssiaVariantsFF = variant.mergePartAsOssia(mainPart, ossiaPart,
744    ...                                                            ossiaName='Parisian_Variant',
745    ...                                                            inPlace=False,
746    ...                                                            compareByMeasureNumber=False,
747    ...                                                            recurseInMeasures=False)
748    >>> mainPartWithOssiaVariantsTF = variant.mergePartAsOssia(mainPart, ossiaPart,
749    ...                                                            ossiaName='Parisian_Variant',
750    ...                                                            inPlace=False,
751    ...                                                            compareByMeasureNumber=True,
752    ...                                                            recurseInMeasures=False)
753
754    >>> mainPartWithOssiaVariantsFT.show('text') == mainPartWithOssiaVariantsTT.show('text')
755    {0.0} <music21.stream.Measure ...
756    True
757
758    >>> mainPartWithOssiaVariantsFF.show('text') == mainPartWithOssiaVariantsFT.show('text')
759    {0.0} <music21.stream.Measure ...
760    True
761
762    >>> mainPartWithOssiaVariantsFT.show('text')
763    {0.0} <music21.stream.Measure 1 offset=0.0>
764    ...
765    {12.0} <music21.stream.Measure 4 offset=12.0>
766        {0.0} <music21.variant.Variant object of length 3.0>
767        {0.0} <music21.note.Note E>
768        {0.5} <music21.note.Note F>
769        {1.0} <music21.note.Note F>
770        {2.0} <music21.note.Note G>
771    {16.0} <music21.stream.Measure 5 offset=16.0>
772    ...
773    {20.0} <music21.stream.Measure 6 offset=20.0>
774        {0.0} <music21.variant.Variant object of length 4.0>
775        {0.0} <music21.note.Note F>
776        {1.0} <music21.note.Note F>
777        {2.0} <music21.note.Note F>
778        {3.0} <music21.note.Note F>
779    ...
780
781    >>> mainPartWithOssiaVariantsFF.activateVariants('Parisian_Variant').show('text')
782    {0.0} <music21.stream.Measure 1 offset=0.0>
783    ...
784    {12.0} <music21.variant.Variant object of length 4.0>
785    {12.0} <music21.stream.Measure 4 offset=12.0>
786        {0.0} <music21.note.Note E>
787        {1.0} <music21.note.Note E>
788        {2.0} <music21.note.Note F>
789        {3.0} <music21.note.Note G>
790    {16.0} <music21.stream.Measure 5 offset=16.0>
791    ...
792    {20.0} <music21.variant.Variant object of length 4.0>
793    {20.0} <music21.stream.Measure 6 offset=20.0>
794        {0.0} <music21.note.Note F>
795        {2.0} <music21.note.Note F>
796    ...
797
798    '''
799    if inPlace is True:
800        returnObj = mainPart
801    else:
802        returnObj = mainPart.coreCopyAsDerivation('mergePartAsOssia')
803
804    if compareByMeasureNumber is True:
805        for ossiaMeasure in ossiaPart.getElementsByClass('Measure'):
806            if ossiaMeasure.notes:  # If the measure is not just rests
807                ossiaNumber = ossiaMeasure.number
808                returnMeasure = returnObj.measure(ossiaNumber)
809                if recurseInMeasures is True:
810                    mergeVariantsEqualDuration(
811                        [returnMeasure, ossiaMeasure],
812                        [ossiaName],
813                        inPlace=True
814                    )
815                else:
816                    ossiaOffset = returnMeasure.getOffsetBySite(returnObj)
817                    addVariant(returnObj,
818                               ossiaOffset,
819                               ossiaMeasure,
820                               variantName=ossiaName,
821                               variantGroups=None,
822                               replacementDuration=None
823                               )
824    else:
825        for ossiaMeasure in ossiaPart.getElementsByClass('Measure'):
826            if ossiaMeasure.notes:  # If the measure is not just rests
827                ossiaOffset = ossiaMeasure.getOffsetBySite(ossiaPart)
828                if recurseInMeasures is True:
829                    returnMeasure = returnObj.getElementsByOffset(
830                        ossiaOffset
831                    ).getElementsByClass(stream.Measure).first()
832                    mergeVariantsEqualDuration(
833                        [returnMeasure, ossiaMeasure],
834                        [ossiaName],
835                        inPlace=True
836                    )
837                else:
838                    addVariant(returnObj, ossiaOffset, ossiaMeasure,
839                               variantName=ossiaName, variantGroups=None, replacementDuration=None)
840
841    if inPlace is True:
842        return
843    else:
844        return returnObj
845
846
847# ------ Public Helper Functions
848
849def addVariant(
850    s: stream.Stream,
851    startOffset: Union[int, float],
852    sVariant: Union[stream.Stream, 'Variant'],
853    variantName=None,
854    variantGroups=None,
855    replacementDuration=None
856):
857    # noinspection PyShadowingNames
858    '''
859    Takes a stream, the location of the variant to be added to
860    that stream (startOffset), the content of the
861    variant to be added (sVariant), and the duration of the section of the stream which the variant
862    replaces (replacementDuration).
863
864    If replacementDuration is 0,
865    this is an insertion. If sVariant is
866    None, this is a deletion.
867
868
869    >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
870    ...            ('a', 'quarter'), ('a', 'quarter')]
871    >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
872    >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
873    ...            ('a', 'quarter'),('b', 'quarter')]
874    >>> data1 = [data1M1, data1M2, data1M3]
875    >>> tempPart = stream.Part()
876    >>> stream1 = stream.Stream()
877    >>> for d in data1:
878    ...    m = stream.Measure()
879    ...    for pitchName, durType in d:
880    ...        n = note.Note(pitchName)
881    ...        n.duration.type = durType
882    ...        m.append(n)
883    ...    stream1.append(m)
884
885    >>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
886    ...            ('a', 'quarter'), ('b', 'quarter')]
887    >>> stream2 = stream.Stream()
888    >>> m = stream.Measure()
889    >>> for pitchName, durType in data2M2:
890    ...    n = note.Note(pitchName)
891    ...    n.duration.type = durType
892    ...    m.append(n)
893    >>> stream2.append(m)
894    >>> variant.addVariant(stream1, 4.0, stream2,
895    ...                    variantName='rhythmic_switch', replacementDuration=4.0)
896    >>> stream1.show('text')
897    {0.0} <music21.stream.Measure 0 offset=0.0>
898        {0.0} <music21.note.Note A>
899        {1.0} <music21.note.Note B>
900        {1.5} <music21.note.Note C>
901        {2.0} <music21.note.Note A>
902        {3.0} <music21.note.Note A>
903    {4.0} <music21.variant.Variant object of length 4.0>
904    {4.0} <music21.stream.Measure 0 offset=4.0>
905        {0.0} <music21.note.Note B>
906        {0.5} <music21.note.Note C>
907        {1.0} <music21.note.Note A>
908        {2.0} <music21.note.Note A>
909        {3.0} <music21.note.Note B>
910    {8.0} <music21.stream.Measure 0 offset=8.0>
911        {0.0} <music21.note.Note C>
912        {1.0} <music21.note.Note D>
913        {2.0} <music21.note.Note E>
914        {3.0} <music21.note.Note E>
915
916    >>> stream1 = stream.Stream()
917    >>> stream1.repeatAppend(note.Note('e'), 6)
918    >>> variant1 = variant.Variant()
919    >>> variant1.repeatAppend(note.Note('f'), 3)
920    >>> startOffset = 3.0
921    >>> variant.addVariant(stream1, startOffset, variant1,
922    ...                    variantName='paris', replacementDuration=3.0)
923    >>> stream1.show('text')
924    {0.0} <music21.note.Note E>
925    {1.0} <music21.note.Note E>
926    {2.0} <music21.note.Note E>
927    {3.0} <music21.variant.Variant object of length 6.0>
928    {3.0} <music21.note.Note E>
929    {4.0} <music21.note.Note E>
930    {5.0} <music21.note.Note E>
931    '''
932    tempVariant = Variant()
933
934    if variantGroups is not None:
935        tempVariant.groups = variantGroups
936    if variantName is not None:
937        tempVariant.groups.append(variantName)
938
939    tempVariant.replacementDuration = replacementDuration
940
941    if sVariant is None:  # deletion
942        pass
943    else:  # replacement or insertion
944        if isinstance(sVariant, stream.Measure):  # sVariant is a measure put it in a variant and insert.
945            tempVariant.append(sVariant)
946        else:  # sVariant is not a measure
947            sVariantMeasures = sVariant.getElementsByClass('Measure')
948            if not sVariantMeasures:  # If there are no measures, work element-wise
949                for e in sVariant:
950                    offset = e.getOffsetBySite(sVariant) + startOffset
951                    tempVariant.insert(offset, e)
952            else:  # if there are measures work measure-wise
953                for m in sVariantMeasures:
954                    tempVariant.append(m)
955
956    s.insert(startOffset, tempVariant)
957
958
959
960def refineVariant(s, sVariant, *, inPlace=False):
961    # noinspection PyShadowingNames
962    '''
963    Given a stream and variant contained in that stream, returns a
964    stream with that variant 'refined.'
965
966    It is refined in the sense that, (with the best estimates) measures which have been determined
967    to be related are merged within the measure.
968
969    Suppose a four-bar phrase in a piece is a slightly
970    different five-bar phrase in a variant. In the variant, every F# has been replaced by an F,
971    and the last bar is repeated. Given these streams, mergeVariantMeasureStreams would return
972    the first stream with a single variant object containing the entire 5 bars of the variant.
973    Calling refineVariant on this stream and that variant object would result in a variant object
974    in the measures for each F#/F pair, and a variant object containing the added bar at the end.
975    For a more detailed explanation of how similar measures are properly associated with each other
976    look at the documentation for _getBestListAndScore
977
978    Note that this code does not work properly yet.
979
980
981    >>> v = variant.Variant()
982    >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
983    ...                  ('a', 'quarter'),('b', 'quarter')]
984    >>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
985    >>> variantData = [variantDataM1, variantDataM2]
986    >>> for d in variantData:
987    ...    m = stream.Measure()
988    ...    for pitchName, durType in d:
989    ...        n = note.Note(pitchName)
990    ...        n.duration.type = durType
991    ...        m.append(n)
992    ...    v.append(m)
993    >>> v.groups = ['paris']
994    >>> v.replacementDuration = 8.0
995
996    >>> s = stream.Stream()
997    >>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')]
998    >>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'),
999    ...                 ('a', 'eighth'), ('a', 'quarter'), ('b', 'quarter')]
1000    >>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
1001    >>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
1002    >>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4]
1003    >>> for d in streamData:
1004    ...    m = stream.Measure()
1005    ...    for pitchName, durType in d:
1006    ...        n = note.Note(pitchName)
1007    ...        n.duration.type = durType
1008    ...        m.append(n)
1009    ...    s.append(m)
1010    >>> s.insert(4.0, v)
1011
1012    >>> variant.refineVariant(s, v, inPlace=True)
1013    >>> s.show('text')
1014    {0.0} <music21.stream.Measure 0 offset=0.0>
1015        {0.0} <music21.note.Note A>
1016        {1.0} <music21.note.Note B>
1017        {2.0} <music21.note.Note A>
1018        {3.0} <music21.note.Note G>
1019    {4.0} <music21.stream.Measure 0 offset=4.0>
1020        {0.0} <music21.note.Note B>
1021        {0.5} <music21.variant.Variant object of length 1.5>
1022        {0.5} <music21.note.Note C>
1023        {1.5} <music21.note.Note A>
1024        {2.0} <music21.note.Note A>
1025        {3.0} <music21.note.Note B>
1026    {8.0} <music21.stream.Measure 0 offset=8.0>
1027        {0.0} <music21.note.Note C>
1028        {1.0} <music21.variant.Variant object of length 3.0>
1029        {1.0} <music21.note.Note B>
1030        {2.0} <music21.note.Note A>
1031        {3.0} <music21.note.Note A>
1032    {12.0} <music21.stream.Measure 0 offset=12.0>
1033        {0.0} <music21.note.Note C>
1034        {1.0} <music21.note.Note B>
1035        {2.0} <music21.note.Note A>
1036        {3.0} <music21.note.Note A>
1037
1038    '''
1039    # stream that will be returned
1040    if sVariant not in s.variants:
1041        raise VariantException(f'{sVariant} not found in stream {s}.')
1042
1043    if inPlace is True:
1044        returnObject = s
1045        variantRegion = sVariant
1046    else:
1047        sVariantIndex = s.variants.index(sVariant)
1048
1049        returnObject = s.coreCopyAsDerivation('refineVariant')
1050        variantRegion = returnObject.variants(sVariantIndex)
1051
1052
1053    # useful parameters from variant and its location
1054    variantGroups = sVariant.groups
1055    replacementDuration = sVariant.replacementDuration
1056    startOffset = sVariant.getOffsetBySite(s)
1057    # endOffset = replacementDuration + startOffset
1058
1059    # region associated with the given variant in the stream
1060    returnRegion = variantRegion.replacedElements(returnObject)
1061
1062    # associating measures in variantRegion to those in returnRegion ->
1063    #       This is done via 0 indexed lists corresponding to measures
1064    returnRegionMeasureList = list(range(len(returnRegion)))
1065    badnessDict = {}
1066    listDict = {}
1067    variantMeasureList, unused_badness = _getBestListAndScore(returnRegion,
1068                                                              variantRegion,
1069                                                              badnessDict,
1070                                                              listDict)
1071
1072    # badness is a measure of how different the streams are.
1073    #        The list returned, variantMeasureList, minimizes that quantity.
1074
1075    # mentioned lists are compared via difflib for optimal edit regions
1076    #       (equal, delete, insert, replace)
1077    sm = difflib.SequenceMatcher()
1078    sm.set_seqs(returnRegionMeasureList, variantMeasureList)
1079    regions = sm.get_opcodes()
1080
1081    # each region is processed for variants.
1082    for regionType, returnStart, returnEnd, variantStart, variantEnd in regions:
1083        startOffset = returnRegion[returnStart].getOffsetBySite(returnRegion)
1084        # endOffset = (returnRegion[returnEnd-1].getOffsetBySite(returnRegion) +
1085        #              returnRegion[returnEnd-1].duration.quarterLength)
1086        variantSubRegion = None
1087        if regionType == 'equal':
1088            returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd)
1089            variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd)
1090            mergeVariantsEqualDuration(
1091                [returnSubRegion, variantSubRegion],
1092                variantGroups,
1093                inPlace=True
1094            )
1095            continue
1096        elif regionType == 'replace':
1097            returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd)
1098            replacementDuration = returnSubRegion.duration.quarterLength
1099            variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd)
1100        elif regionType == 'delete':
1101            returnSubRegion = returnRegion.measures(returnStart + 1, returnEnd)
1102            replacementDuration = returnSubRegion.duration.quarterLength
1103            variantSubRegion = None
1104        elif regionType == 'insert':
1105            variantSubRegion = variantRegion.measures(variantStart + 1, variantEnd)
1106            replacementDuration = 0.0
1107        else:
1108            raise VariantException(f'Unknown regionType {regionType!r}')
1109
1110        addVariant(returnRegion,
1111                   startOffset,
1112                   variantSubRegion,
1113                   variantGroups=variantGroups,
1114                   replacementDuration=replacementDuration
1115                   )
1116
1117    # The original variant object has been replaced by more refined
1118    #     variant objects and so should be deleted.
1119    returnObject.remove(variantRegion)
1120
1121    if inPlace:
1122        return None
1123    else:
1124        return returnObject
1125
1126
1127def _mergeVariantMeasureStreamsCarefully(streamX, streamY, variantName, *, inPlace=False):
1128    '''
1129    There seem to be some problems with this function and it isn't well tested.
1130    It is not recommended to use it at this time.
1131
1132    '''
1133    # stream that will be returned
1134    if inPlace is True:
1135        returnObject = streamX
1136        variantObject = streamY
1137    else:
1138        returnObject = copy.deepcopy(streamX)
1139        variantObject = copy.deepcopy(streamY)
1140
1141    # associating measures in variantRegion to those in returnRegion ->
1142    #    This is done via 0 indexed lists corresponding to measures
1143    returnObjectMeasureList = list(range(len(returnObject.getElementsByClass('Measure'))))
1144    badnessDict = {}
1145    listDict = {}
1146    variantObjectMeasureList, unused_badness = _getBestListAndScore(
1147        returnObject.getElementsByClass('Measure'),
1148        variantObject.getElementsByClass('Measure'),
1149        badnessDict,
1150        listDict
1151    )
1152
1153    # badness is a measure of how different the streams are.
1154    # The list returned, variantMeasureList, minimizes that quantity.
1155
1156    # mentioned lists are compared via difflib for optimal edit regions
1157    #     (equal, delete, insert, replace)
1158    sm = difflib.SequenceMatcher()
1159    sm.set_seqs(returnObjectMeasureList, variantObjectMeasureList)
1160    regions = sm.get_opcodes()
1161
1162    # each region is processed for variants.
1163    for regionType, returnStart, returnEnd, variantStart, variantEnd in regions:
1164        startOffset = returnObject.measure(returnStart + 1).getOffsetBySite(returnObject)
1165        if regionType == 'equal':
1166            returnSubRegion = returnObject.measures(returnStart + 1, returnEnd)
1167            variantSubRegion = variantObject.measures(variantStart + 1, variantEnd)
1168            mergeVariantMeasureStreams(
1169                returnSubRegion,
1170                variantSubRegion,
1171                variantName,
1172                inPlace=True
1173            )
1174            continue
1175        elif regionType == 'replace':
1176            returnSubRegion = returnObject.measures(returnStart + 1, returnEnd)
1177            replacementDuration = returnSubRegion.duration.quarterLength
1178            variantSubRegion = variantObject.measures(variantStart + 1, variantEnd)
1179        elif regionType == 'delete':
1180            returnSubRegion = returnObject.measures(returnStart + 1, returnEnd)
1181            replacementDuration = returnSubRegion.duration.quarterLength
1182            variantSubRegion = None
1183        elif regionType == 'insert':
1184            variantSubRegion = variantObject.measures(variantStart + 1, variantEnd)
1185            replacementDuration = 0.0
1186        else:  # pragma: no cover
1187            raise VariantException(f'Unknown regionType: {regionType}')
1188
1189
1190        addVariant(
1191            returnObject,
1192            startOffset,
1193            variantSubRegion,
1194            variantGroups=[variantName],
1195            replacementDuration=replacementDuration
1196        )
1197
1198    if not inPlace:
1199        return returnObject
1200
1201
1202def getMeasureHashes(s):
1203    # noinspection PyShadowingNames
1204    '''
1205    Takes in a stream containing measures and returns a list of hashes,
1206    one for each measure. Currently
1207    implemented with search.translateStreamToString()
1208
1209    >>> s = converter.parse("tinynotation: 2/4 c4 d8. e16 FF4 a'4 b-2")
1210    >>> sm = s.makeMeasures()
1211    >>> hashes = variant.getMeasureHashes(sm)
1212    >>> hashes
1213    ['<P>K@<', ')PQP', 'FZ']
1214    '''
1215    hashes = []
1216    if isinstance(s, list):
1217        for m in s:
1218            hashes.append(search.translateStreamToString(m.notesAndRests))
1219        return hashes
1220    else:
1221        for m in s.getElementsByClass('Measure'):
1222            hashes.append(search.translateStreamToString(m.notesAndRests))
1223        return hashes
1224
1225
1226# ----- Private Helper Functions
1227def _getBestListAndScore(streamX, streamY, badnessDict, listDict,
1228                         isNone=False, streamXIndex=-1, streamYIndex=-1):
1229    # noinspection PyShadowingNames
1230    '''
1231    This is a recursive function which makes a map between two related streams of measures.
1232    It is designed for streams of measures that contain few if any measures that are actually
1233    identical and that have a different number of measures (within reason). For example,
1234    if one stream has 10 bars of eighth notes and the second stream has the same ten bars
1235    of eighth notes except with some dotted rhythms mixed in and the fifth bar is repeated.
1236    The first, streamX, is the reference stream. This function returns a list of
1237    integers with length len(streamY) which maps each measure of StreamY to the measure
1238    in streamX it is most likely associated with. For example, if the returned list is
1239    [0, 2, 3, 'addedBar', 4]. This indicates that streamY is most similar to streamX
1240    after the second bar of streamX has been removed and a new bar inserted between
1241    bars 4 and 5. Note that this list has measures 0-indexed. This function generates this map by
1242    minimizing the difference or 'badness' for the sequence of measures on the whole as determined
1243    by the helper function _simScore which compares measures for similarity. 'addedBar' appears
1244    in the list where this function has determined that the bar appearing
1245    in streamY does not have a counterpart in streamX anywhere and is an insertion.
1246
1247
1248    >>> badnessDict = {}
1249    >>> listDict = {}
1250    >>> stream1 = stream.Stream()
1251    >>> stream2 = stream.Stream()
1252
1253    >>> data1M1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
1254    ...            ('a', 'quarter'), ('a', 'quarter')]
1255    >>> data1M2 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
1256    ...            ('a', 'quarter'),('b', 'quarter')]
1257    >>> data1M3 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
1258
1259    >>> data2M1 = [('a', 'quarter'), ('b', 'quarter'), ('c', 'quarter'), ('g#', 'quarter')]
1260    >>> data2M2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
1261    ...            ('a', 'quarter'), ('b', 'quarter')]
1262    >>> data2M3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
1263    >>> data2M4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
1264    >>> data1 = [data1M1, data1M2, data1M3]
1265    >>> data2 = [data2M1, data2M2, data2M3, data2M4]
1266    >>> for d in data1:
1267    ...    m = stream.Measure()
1268    ...    for pitchName, durType in d:
1269    ...        n = note.Note(pitchName)
1270    ...        n.duration.type = durType
1271    ...        m.append(n)
1272    ...    stream1.append(m)
1273    >>> for d in data2:
1274    ...    m = stream.Measure()
1275    ...    for pitchName, durType in d:
1276    ...        n = note.Note(pitchName)
1277    ...        n.duration.type = durType
1278    ...        m.append(n)
1279    ...    stream2.append(m)
1280    >>> kList, kBadness = variant._getBestListAndScore(stream1, stream2,
1281    ...                                                badnessDict, listDict, isNone=False)
1282    >>> kList
1283    [0, 1, 2, 'addedBar']
1284    '''
1285    # Initialize 'Best' Values for maximizing algorithm
1286    bestScore = 1
1287    bestNormalizedScore = 1
1288    bestList = []
1289
1290    # Base Cases:
1291    if streamYIndex >= len(streamY):
1292        listDict[(streamXIndex, streamYIndex, isNone)] = []
1293        badnessDict[(streamXIndex, streamYIndex, isNone)] = 0.0
1294        return [], 0
1295
1296    # Query Dict for existing results
1297    if (streamXIndex, streamYIndex, isNone) in badnessDict:
1298        badness = badnessDict[(streamXIndex, streamYIndex, isNone)]
1299        bestList = listDict[(streamXIndex, streamYIndex, isNone)]
1300        return bestList, badness
1301
1302    # Get salient similarity score
1303    if streamXIndex == -1 and streamYIndex == -1:
1304        simScore = 0
1305    elif isNone:
1306        simScore = 0.5
1307    else:
1308        simScore = _diffScore(streamX[streamXIndex], streamY[streamYIndex])
1309
1310
1311    # Check the added bar case:
1312    kList, kBadness = _getBestListAndScore(streamX, streamY, badnessDict, listDict,
1313                            isNone=True, streamXIndex=streamXIndex, streamYIndex=streamYIndex + 1)
1314    if kList is None:
1315        kList = []
1316    if kList:
1317        normalizedBadness = kBadness / len(kList)
1318    else:
1319        normalizedBadness = 0
1320
1321    if normalizedBadness <= bestNormalizedScore:
1322        bestScore = kBadness
1323        bestNormalizedScore = normalizedBadness
1324        bestList = kList
1325
1326    # Check the other cases
1327    for k in range(streamXIndex + 1, len(streamX)):
1328        kList, kBadness = _getBestListAndScore(streamX, streamY, badnessDict,
1329                                               listDict, isNone=False,
1330                                               streamXIndex=k, streamYIndex=streamYIndex + 1)
1331        if kList is None:
1332            kList = []
1333        if kList:
1334            normalizedBadness = kBadness / len(kList)
1335        else:
1336            normalizedBadness = 0
1337
1338        if normalizedBadness <= bestNormalizedScore:
1339            bestScore = kBadness
1340            bestNormalizedScore = normalizedBadness
1341            bestList = kList
1342
1343    # Prepare and Return Results
1344    returnList = copy.deepcopy(bestList)
1345    if isNone:
1346        returnList.insert(0, 'addedBar')
1347    elif streamXIndex == -1:
1348        pass
1349    else:
1350        returnList.insert(0, streamXIndex)
1351    badness = bestScore + simScore
1352
1353    badnessDict[(streamXIndex, streamYIndex, isNone)] = badness
1354    listDict[(streamXIndex, streamYIndex, isNone)] = returnList
1355    return returnList, badness
1356
1357
1358def _diffScore(measureX, measureY):
1359    '''
1360    Helper function for _getBestListAndScore which compares to measures and returns a value
1361    associated with their similarity. The higher the normalized (0, 1) value the poorer the match.
1362    This should be calibrated such that the value that appears in _getBestListAndScore for
1363    isNone is true (i.e. testing when a bar does not associate with any existing bars the reference
1364    stream), is well matched with the similarity scores generated by this function.
1365
1366
1367    >>> m1 = stream.Measure()
1368    >>> m2 = stream.Measure()
1369    >>> m1.append([note.Note('e'), note.Note('f'), note.Note('g'), note.Note('a')])
1370    >>> m2.append([note.Note('e'), note.Note('f'), note.Note('g#'), note.Note('a')])
1371    >>> variant._diffScore(m1, m2)
1372    0.4...
1373
1374    '''
1375    hashes = getMeasureHashes([measureX, measureY])
1376    if hashes[0] == hashes[1]:
1377        baseValue = 0.0
1378    else:
1379        baseValue = 0.4
1380
1381    numberDelta = measureX.number - measureY.number
1382
1383    distanceModifier = float(numberDelta) * 0.001
1384
1385
1386    return baseValue + distanceModifier
1387
1388
1389def _getRegionsFromStreams(streamX, streamY):
1390    # noinspection PyShadowingNames
1391    '''
1392    Takes in two streams, returns a list of 5-tuples via difflib.get_opcodes()
1393    working on measure differences.
1394
1395
1396    >>> s1 = converter.parse("tinynotation: 2/4 d4 e8. f16 GG4 b'4 b-2 c4 d8. e16 FF4 a'4 b-2")
1397
1398                                                *0:Eq  *1:Rep        * *3:Eq             *6:In
1399
1400    >>> s2 = converter.parse("tinynotation: 2/4 d4 e8. f16 FF4 b'4 c4 d8. e16 FF4 a'4 b-2 b-2")
1401    >>> s1m = s1.makeMeasures()
1402    >>> s2m = s2.makeMeasures()
1403    >>> regions = variant._getRegionsFromStreams(s1m, s2m)
1404    >>> regions
1405    [('equal', 0, 1, 0, 1),
1406     ('replace', 1, 3, 1, 2),
1407     ('equal', 3, 6, 2, 5),
1408     ('insert', 6, 6, 5, 6)]
1409
1410    '''
1411    hashesX = getMeasureHashes(streamX)
1412    hashesY = getMeasureHashes(streamY)
1413    sm = difflib.SequenceMatcher()
1414    sm.set_seqs(hashesX, hashesY)
1415    regions = sm.get_opcodes()
1416    return regions
1417
1418
1419def _mergeVariants(streamA, streamB, *, variantName=None, inPlace=False):
1420    '''
1421    This is a helper function for mergeVariantsEqualDuration which takes two streams
1422    (which cannot contain container
1423    streams like measures and parts) and merges the second into the first via variant objects.
1424    If the first already contains variant objects, containsVariants should be set to true and the
1425    function will compare streamB to the streamA as well as the
1426    variant streams contained in streamA.
1427    Note that variant streams in streamB will be ignored and lost.
1428
1429
1430    >>> stream1 = stream.Stream()
1431    >>> stream2 = stream.Stream()
1432    >>> data1 = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
1433    ...    ('a', 'quarter'), ('a', 'quarter'),
1434    ...    ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
1435    ...    ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')]
1436    >>> data2 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter'),
1437    ...    ('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'), ('a', 'quarter'),
1438    ...    ('b', 'quarter'), ('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter')]
1439    >>> for pitchName, durType in data1:
1440    ...    n = note.Note(pitchName)
1441    ...    n.duration.type = durType
1442    ...    stream1.append(n)
1443    >>> for pitchName, durType in data2:
1444    ...    n = note.Note(pitchName)
1445    ...    n.duration.type = durType
1446    ...    stream2.append(n)
1447    >>> mergedStreams = variant._mergeVariants(stream1, stream2, variantName='paris')
1448    >>> mergedStreams.show('t')
1449    {0.0} <music21.note.Note A>
1450    {1.0} <music21.variant.Variant object of length 1.0>
1451    {1.0} <music21.note.Note B>
1452    {1.5} <music21.note.Note C>
1453    {2.0} <music21.note.Note A>
1454    {3.0} <music21.variant.Variant object of length 1.0>
1455    {3.0} <music21.note.Note A>
1456    {4.0} <music21.note.Note B>
1457    {4.5} <music21.variant.Variant object of length 1.5>
1458    {4.5} <music21.note.Note C>
1459    {5.0} <music21.note.Note A>
1460    {6.0} <music21.note.Note A>
1461    {7.0} <music21.note.Note B>
1462    {8.0} <music21.note.Note C>
1463    {9.0} <music21.variant.Variant object of length 2.0>
1464    {9.0} <music21.note.Note D>
1465    {10.0} <music21.note.Note E>
1466
1467    >>> mergedStreams.activateVariants('paris').show('t')
1468    {0.0} <music21.note.Note A>
1469    {1.0} <music21.variant.Variant object of length 1.0>
1470    {1.0} <music21.note.Note B>
1471    {2.0} <music21.note.Note A>
1472    {3.0} <music21.variant.Variant object of length 1.0>
1473    {3.0} <music21.note.Note G>
1474    {4.0} <music21.note.Note B>
1475    {4.5} <music21.variant.Variant object of length 1.5>
1476    {4.5} <music21.note.Note C>
1477    {5.5} <music21.note.Note A>
1478    {6.0} <music21.note.Note A>
1479    {7.0} <music21.note.Note B>
1480    {8.0} <music21.note.Note C>
1481    {9.0} <music21.variant.Variant object of length 2.0>
1482    {9.0} <music21.note.Note B>
1483    {10.0} <music21.note.Note A>
1484
1485    >>> stream1.append(note.Note('e'))
1486    >>> mergedStreams = variant._mergeVariants(stream1, stream2, variantName=['paris'])
1487    Traceback (most recent call last):
1488    music21.variant.VariantException: _mergeVariants cannot merge streams
1489        which are of different lengths
1490    '''
1491    # TODO: Add the feature for merging a stream to a stream with existing variants
1492    # (it has to compare against both the stream and the contained variant)
1493    if (streamA.getElementsByClass('Measure')
1494            or streamA.getElementsByClass('Part')
1495            or streamB.getElementsByClass('Measure')
1496            or streamB.getElementsByClass('Part')):
1497        raise VariantException(
1498            '_mergeVariants cannot merge streams which contain measures or parts.'
1499        )
1500
1501    if streamA.highestTime != streamB.highestTime:
1502        raise VariantException(
1503            '_mergeVariants cannot merge streams which are of different lengths'
1504        )
1505
1506    if inPlace is True:
1507        returnObj = streamA
1508    else:
1509        returnObj = copy.deepcopy(streamA)
1510
1511    i = 0
1512    j = 0
1513    inVariant = False
1514    streamANotes = streamA.flatten().notesAndRests
1515    streamBNotes = streamB.flatten().notesAndRests
1516
1517    noteBuffer = []
1518    variantStart = 0.0
1519
1520    while i < len(streamANotes) and j < len(streamBNotes):
1521        if i == len(streamANotes):
1522            i = len(streamANotes) - 1
1523        if j == len(streamBNotes):
1524            break
1525        if (streamANotes[i].getOffsetBySite(streamA.flatten())
1526                == streamBNotes[j].getOffsetBySite(streamB.flatten())):
1527            # Comparing Notes at same offset
1528            #    TODO: Will not work until __eq__ overwritten for Generalized Notes
1529            if streamANotes[i] != streamBNotes[j]:
1530                # If notes are different, start variant if not started and append note.
1531                if inVariant is False:
1532                    variantStart = streamBNotes[j].getOffsetBySite(streamB.flatten())
1533                    inVariant = True
1534                    noteBuffer = []
1535                    noteBuffer.append(streamBNotes[j])
1536                else:
1537                    noteBuffer.append(streamBNotes[j])
1538            else:  # If notes are the same, end and insert variant if in variant.
1539                if inVariant is True:
1540                    returnObj.insert(
1541                        variantStart,
1542                        _generateVariant(
1543                            noteBuffer,
1544                            streamB,
1545                            variantStart,
1546                            variantName
1547                        )
1548                    )
1549                    inVariant = False
1550                    noteBuffer = []
1551                else:
1552                    inVariant = False
1553
1554            i += 1
1555            j += 1
1556            continue
1557
1558        elif (streamANotes[i].getOffsetBySite(streamA.flatten())
1559              > streamBNotes[j].getOffsetBySite(streamB.flatten())):
1560            if inVariant is False:
1561                variantStart = streamBNotes[j].getOffsetBySite(streamB.flatten())
1562                noteBuffer = []
1563                noteBuffer.append(streamBNotes[j])
1564                inVariant = True
1565            else:
1566                noteBuffer.append(streamBNotes[j])
1567            j += 1
1568            continue
1569
1570        else:  # Less-than
1571            i += 1
1572            continue
1573
1574    if inVariant is True:  # insert final variant if exists
1575        returnObj.insert(
1576            variantStart,
1577            _generateVariant(
1578                noteBuffer,
1579                streamB,
1580                variantStart,
1581                variantName
1582            )
1583        )
1584        inVariant = False
1585        noteBuffer = []
1586
1587    if inPlace is True:
1588        return None
1589    else:
1590        return returnObj
1591
1592
1593def _generateVariant(noteList, originStream, start, variantName=None):
1594    # noinspection PyShadowingNames
1595    '''
1596    Helper function for mergeVariantsEqualDuration which takes a list of
1597    consecutive notes from a stream and returns
1598    a variant object containing the notes from the list at the offsets
1599    derived from their original context.
1600
1601    >>> originStream = stream.Stream()
1602    >>> data = [('a', 'quarter'), ('b', 'eighth'), ('c', 'eighth'),
1603    ...    ('a', 'quarter'), ('a', 'quarter'),
1604    ...    ('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'), ('a', 'quarter'),
1605    ...    ('b', 'quarter'), ('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter')]
1606    >>> for pitchName, durType in data:
1607    ...    n = note.Note(pitchName)
1608    ...    n.duration.type = durType
1609    ...    originStream.append(n)
1610    >>> noteList = []
1611    >>> for n in originStream.notes[2:5]:
1612    ...    noteList.append(n)
1613    >>> start = originStream.notes[2].offset
1614    >>> variantName='paris'
1615    >>> v = variant._generateVariant(noteList, originStream, start, variantName)
1616    >>> v.show('text')
1617    {0.0} <music21.note.Note C>
1618    {0.5} <music21.note.Note A>
1619    {1.5} <music21.note.Note A>
1620
1621    >>> v.groups
1622    ['paris']
1623
1624    '''
1625    returnVariant = Variant()
1626    for n in noteList:
1627        returnVariant.insert(n.getOffsetBySite(originStream.flatten()) - start, n)
1628    if variantName is not None:
1629        returnVariant.groups.append(variantName)
1630    return returnVariant
1631
1632
1633# ------- Variant Manipulation Methods
1634def makeAllVariantsReplacements(streamWithVariants,
1635                                variantNames=None,
1636                                inPlace=False,
1637                                recurse=False):
1638    # noinspection PyShadowingNames
1639    '''
1640    This function takes a stream and a list of variantNames
1641    (default works on all variants), and changes all insertion
1642    (elongations with replacementDuration 0)
1643    and deletion variants (with containedHighestTime 0) into variants with non-zero
1644    replacementDuration and non-null elements
1645    by adding measures on the front of insertions and measures on the end
1646    of deletions. This is designed to make it possible to format all variants in a
1647    readable way as a graphical ossia (via lilypond). If inPlace is True
1648    it will perform this action on the stream itself; otherwise it will return a
1649    modified copy. If recurse is True, this
1650    method will work on variants within container objects within the stream (like parts).
1651
1652    >>> #                                                          *                                            *                                *
1653    >>> 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")
1654    >>> 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")
1655    >>> #                                                          replacement                            insertion                            deletion
1656    >>> s.makeMeasures(inPlace=True)
1657    >>> s2.makeMeasures(inPlace=True)
1658    >>> variant.mergeVariants(s, s2, variantName='london', inPlace=True)
1659
1660    >>> newStream = stream.Score(s)
1661
1662    >>> returnStream = variant.makeAllVariantsReplacements(newStream, recurse=False)
1663    >>> for v in returnStream.parts[0].variants:
1664    ...     (v.offset, v.lengthType, v.replacementDuration)
1665    (4.0, 'replacement', 4.0)
1666    (16.0, 'elongation', 0.0)
1667    (20.0, 'deletion', 4.0)
1668
1669    >>> returnStream = variant.makeAllVariantsReplacements(
1670    ...                            newStream, variantNames=['france'], recurse=True)
1671    >>> for v in returnStream.parts[0].variants:
1672    ...     (v.offset, v.lengthType, v.replacementDuration)
1673    (4.0, 'replacement', 4.0)
1674    (16.0, 'elongation', 0.0)
1675    (20.0, 'deletion', 4.0)
1676
1677    >>> variant.makeAllVariantsReplacements(newStream, recurse=True, inPlace=True)
1678    >>> for v in newStream.parts[0].variants:
1679    ...     (v.offset, v.lengthType, v.replacementDuration, v.containedHighestTime)
1680    (4.0, 'replacement', 4.0, 4.0)
1681    (12.0, 'elongation', 4.0, 12.0)
1682    (20.0, 'deletion', 8.0, 4.0)
1683
1684    '''
1685
1686    if inPlace is True:
1687        returnStream = streamWithVariants
1688    else:
1689        returnStream = copy.deepcopy(streamWithVariants)
1690
1691    if recurse is True:
1692        for s in returnStream.recurse(streamsOnly=True):
1693            _doVariantFixingOnStream(s, variantNames=variantNames)
1694    else:
1695        _doVariantFixingOnStream(returnStream, variantNames=variantNames)
1696
1697
1698    if inPlace is True:
1699        return
1700    else:
1701        return returnStream
1702
1703
1704def _doVariantFixingOnStream(s, variantNames=None):
1705    # noinspection PyShadowingNames
1706    '''
1707    This is a helper function for makeAllVariantsReplacements.
1708    It iterates through the appropriate variants
1709    and performs the variant changing operation to eliminate strict deletion and insertion variants.
1710
1711    >>> #                                           *                           *                                            *                                *                           *
1712    >>> 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)
1713    >>> 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)
1714    >>> #                                        initial insertion              replacement                            insertion                            deletion                        final deletion
1715    >>> s.makeMeasures(inPlace=True)
1716    >>> s2.makeMeasures(inPlace=True)
1717    >>> variant.mergeVariants(s, s2, variantName='london', inPlace=True)
1718
1719    >>> variant._doVariantFixingOnStream(s, 'london')
1720    >>> s.show('text')
1721    {0.0} <music21.variant.Variant object of length 8.0>
1722    {0.0} <music21.stream.Measure 1 offset=0.0>
1723    ...
1724    {4.0} <music21.variant.Variant object of length 4.0>
1725    {4.0} <music21.stream.Measure 2 offset=4.0>
1726    ...
1727    {12.0} <music21.variant.Variant object of length 12.0>
1728    {12.0} <music21.stream.Measure 4 offset=12.0>
1729    ...
1730    {20.0} <music21.variant.Variant object of length 4.0>
1731    {20.0} <music21.stream.Measure 6 offset=20.0>
1732    ...
1733    {24.0} <music21.variant.Variant object of length 4.0>
1734    {24.0} <music21.stream.Measure 7 offset=24.0>
1735    ...
1736
1737    >>> for v in s.variants:
1738    ...     (v.offset, v.lengthType, v.replacementDuration)
1739    (0.0, 'elongation', 4.0)
1740    (4.0, 'replacement', 4.0)
1741    (12.0, 'elongation', 4.0)
1742    (20.0, 'deletion', 8.0)
1743    (24.0, 'deletion', 8.0)
1744
1745
1746    This also works on streams with variants that contain notes and rests rather than measures.
1747
1748    >>> s = converter.parse('tinyNotation: 4/4                     e4 b b b   f4 f f f   g4 a a a       ', makeNotation=False)
1749    >>> v1Stream = converter.parse('tinyNotation: 4/4   a4 a a a                                       ', makeNotation=False)
1750    >>> #                                               initial insertion     deletion
1751    >>> v1 = variant.Variant(v1Stream.notes)
1752    >>> v1.replacementDuration = 0.0
1753    >>> v1.groups = ['london']
1754    >>> s.insert(0.0, v1)
1755
1756    >>> v2 = variant.Variant()
1757    >>> v2.replacementDuration = 4.0
1758    >>> v2.groups = ['london']
1759    >>> s.insert(4.0, v2)
1760
1761    >>> variant._doVariantFixingOnStream(s, 'london')
1762    >>> for v in s.variants:
1763    ...     (v.offset, v.lengthType, v.replacementDuration, v.containedHighestTime)
1764    (0.0, 'elongation', 1.0, 5.0)
1765    (4.0, 'deletion', 5.0, 1.0)
1766    '''
1767
1768    for v in s.variants:
1769        if isinstance(variantNames, list):  # If variantNames are controlled
1770            if set(v.groups) and not set(variantNames):
1771                # and if this variant is not in the controlled list
1772                continue  # then skip it
1773            else:
1774                continue  # huh????
1775        lengthType = v.lengthType
1776        replacementDuration = v.replacementDuration
1777        highestTime = v.containedHighestTime
1778
1779        if lengthType == 'elongation' and replacementDuration == 0.0:
1780            variantType = 'insertion'
1781        elif lengthType == 'deletion' and highestTime == 0.0:
1782            variantType = 'deletion'
1783        else:
1784            continue
1785
1786        if v.getOffsetBySite(s) == 0.0:
1787            isInitial = True
1788            isFinal = False
1789        elif v.getOffsetBySite(s) + v.replacementDuration == s.duration.quarterLength:
1790            isInitial = False
1791            isFinal = True
1792        else:
1793            isInitial = False
1794            isFinal = False
1795
1796        # If a non-final deletion or an INITIAL insertion,
1797        #  add the next element after the variant.
1798        if ((variantType == 'insertion' and (isInitial is True))
1799                or (variantType == 'deletion' and (isFinal is False))):
1800            targetElement = _getNextElements(s, v)
1801
1802            # Delete initial clefs, etc. from initial insertion targetElement if it exists
1803            if isinstance(targetElement, stream.Stream):
1804                # Must use .elements, because of removal of elements
1805                for e in targetElement.elements:
1806                    if isinstance(e, (clef.Clef, meter.TimeSignature)):
1807                        targetElement.remove(e)
1808
1809            v.append(copy.deepcopy(targetElement))  # Appends a copy
1810
1811        # If a non-initial insertion or a FINAL deletion,
1812        #     add the previous element after the variant.
1813        # #elif ((variantType == 'deletion' and (isFinal is True)) or
1814        #         (type == 'insertion' and (isInitial is False))):
1815        else:
1816            targetElement = _getPreviousElement(s, v)
1817            newVariantOffset = targetElement.getOffsetBySite(s)
1818            # Need to shift elements to make way for new element at front
1819            offsetShift = targetElement.duration.quarterLength
1820            for e in v.containedSite:
1821                oldOffset = e.getOffsetBySite(v.containedSite)
1822                e.setOffsetBySite(v.containedSite, oldOffset + offsetShift)
1823            v.insert(0.0, copy.deepcopy(targetElement))
1824            s.remove(v)
1825            s.insert(newVariantOffset, v)
1826
1827            # Give it a new replacementDuration including the added element
1828        oldReplacementDuration = v.replacementDuration
1829        v.replacementDuration = oldReplacementDuration + targetElement.duration.quarterLength
1830
1831
1832def _getNextElements(s, v, numberOfElements=1):
1833    # noinspection PyShadowingNames
1834    '''
1835    This is a helper function for makeAllVariantsReplacements() which returns the next element in s
1836    of the type of elements found in the variant v so that if can be added to v.
1837
1838
1839    >>> #                                                   *                       *
1840    >>> s1 = converter.parse('tinyNotation: 4/4             b4 c d e    f4 g a b   d4 e f g   ', makeNotation=False)
1841    >>> s2 = converter.parse('tinyNotation: 4/4 e4 f g a    b4 c d e               d4 e f g   ', makeNotation=False)
1842    >>> #                                       insertion               deletion
1843    >>> s1.makeMeasures(inPlace=True)
1844    >>> s2.makeMeasures(inPlace=True)
1845    >>> mergedStream = variant.mergeVariants(s1, s2, 'london')
1846    >>> for v in mergedStream.variants:
1847    ...     returnElement = variant._getNextElements(mergedStream, v)
1848    ...     print(returnElement)
1849    <music21.stream.Measure 1 offset=0.0>
1850    <music21.stream.Measure 3 offset=8.0>
1851
1852    This also works on streams with variants that contain notes and rests rather than measures.
1853
1854    >>> s = converter.parse('tinyNotation: 4/4                     e4 b b b   f4 f f f   g4 a a a       ', makeNotation=False)
1855    >>> v1Stream = converter.parse('tinyNotation: 4/4   a4 a a a                                       ', makeNotation=False)
1856    >>> #                                               initial insertion
1857    >>> v1 = variant.Variant(v1Stream.notes)
1858    >>> v1.replacementDuration = 0.0
1859    >>> v1.groups = ['london']
1860    >>> s.insert(0.0, v1)
1861
1862    >>> v2 = variant.Variant()
1863    >>> v2.replacementDuration = 4.0
1864    >>> v2.groups = ['london']
1865    >>> s.insert(4.0, v2)
1866    >>> for v in s.variants:
1867    ...     returnElement = variant._getNextElements(s, v)
1868    ...     print(returnElement)
1869    <music21.note.Note E>
1870    <music21.note.Note G>
1871    '''
1872    replacedElements = v.replacedElements(s)
1873    lengthType = v.lengthType
1874    # Get class of elements in variant or replaced Region
1875    if lengthType == 'elongation':
1876        vClass = type(v.getElementsByClass(['Measure', 'Note', 'Rest']).first())
1877        if isinstance(vClass, note.GeneralNote):
1878            vClass = note.GeneralNote
1879    else:
1880        vClass = type(replacedElements.getElementsByClass(['Measure', 'Note', 'Rest']).first())
1881        if isinstance(vClass, note.GeneralNote):
1882            vClass = note.GeneralNote
1883
1884    # Get next element in s after v which is of type vClass
1885    if lengthType == 'elongation':
1886        variantOffset = v.getOffsetBySite(s)
1887        potentialTargets = s.getElementsByOffset(variantOffset,
1888                                                  offsetEnd=s.highestTime,
1889                                                  includeEndBoundary=True,
1890                                                  mustFinishInSpan=False,
1891                                                  mustBeginInSpan=True,
1892                                                  classList=[vClass])
1893        returnElement = potentialTargets.first()
1894
1895    else:
1896        replacementDuration = v.replacementDuration
1897        variantOffset = v.getOffsetBySite(s)
1898        potentialTargets = s.getElementsByOffset(variantOffset + replacementDuration,
1899                                                  offsetEnd=s.highestTime,
1900                                                  includeEndBoundary=True,
1901                                                  mustFinishInSpan=False,
1902                                                  mustBeginInSpan=True,
1903                                                  classList=[vClass])
1904        returnElement = potentialTargets.first()
1905
1906
1907    return returnElement
1908
1909
1910def _getPreviousElement(s, v):
1911    # noinspection PyShadowingNames
1912    '''
1913    This is a helper function for makeAllVariantsReplacements() which returns
1914    the previous element in s
1915    of the type of elements found in the variant v so that if can be added to v.
1916
1917
1918    >>> #                                                   *                       *
1919    >>> s1 = converter.parse('tinyNotation: 4/4 a4 b c d                b4 c d e    f4 g a b    ')
1920    >>> s2 = converter.parse('tinyNotation: 4/4 a4 b c d    e4 f g a    b4 c d e                ')
1921    >>> #                                                   insertion               deletion
1922    >>> s1.makeMeasures(inPlace=True)
1923    >>> s2.makeMeasures(inPlace=True)
1924    >>> mergedStream = variant.mergeVariants(s1, s2, 'london')
1925    >>> for v in mergedStream.variants:
1926    ...     returnElement = variant._getPreviousElement(mergedStream, v)
1927    ...     print(returnElement)
1928    <music21.stream.Measure 1 offset=0.0>
1929    <music21.stream.Measure 2 offset=4.0>
1930
1931    This also works on streams with variants that contain notes and rests rather than measures.
1932
1933    >>> s = converter.parse('tinyNotation: 4/4         b4 b b a            e4 b b b      g4 e e e       ', makeNotation=False)
1934    >>> v1Stream = converter.parse('tinyNotation: 4/4           f4 f f f                                ', makeNotation=False)
1935    >>> #                                                       insertion                final deletion
1936    >>> v1 = variant.Variant(v1Stream.notes)
1937    >>> v1.replacementDuration = 0.0
1938    >>> v1.groups = ['london']
1939    >>> s.insert(4.0, v1)
1940
1941    >>> v2 = variant.Variant()
1942    >>> v2.replacementDuration = 4.0
1943    >>> v2.groups = ['london']
1944    >>> s.insert(8.0, v2)
1945    >>> for v in s.variants:
1946    ...     returnElement = variant._getPreviousElement(s, v)
1947    ...     print(returnElement)
1948    <music21.note.Note A>
1949    <music21.note.Note B>
1950    '''
1951
1952    replacedElements = v.replacedElements(s)
1953    lengthType = v.lengthType
1954    # Get class of elements in variant or replaced Region
1955    foundStream = None
1956    if lengthType == 'elongation':
1957        foundStream = v.getElementsByClass(['Measure', 'Note', 'Rest'])
1958    else:
1959        foundStream = replacedElements.getElementsByClass(['Measure', 'Note', 'Rest'])
1960
1961    if not foundStream:
1962        raise VariantException('Cannot find any Measures, Notes, or Rests in variant')
1963    vClass = type(foundStream[0])
1964    if isinstance(vClass, note.GeneralNote):
1965        vClass = note.GeneralNote
1966
1967    # Get next element in s after v which is of type vClass
1968    variantOffset = v.getOffsetBySite(s)
1969    potentialTargets = s.getElementsByOffset(
1970        0.0,
1971        offsetEnd=variantOffset,
1972        includeEndBoundary=False,
1973        mustFinishInSpan=False,
1974        mustBeginInSpan=True,
1975    ).getElementsByClass(vClass)
1976    returnElement = potentialTargets.last()
1977
1978    return returnElement
1979
1980
1981# ------------------------------------------------------------------------------
1982# classes
1983
1984
1985class VariantException(exceptions21.Music21Exception):
1986    pass
1987
1988
1989class Variant(base.Music21Object):
1990    '''
1991    A Music21Object that stores elements like a Stream, but does not
1992    represent itself externally to a Stream; i.e., the contents of a Variant are not flattened.
1993
1994    This is accomplished not by subclassing, but by object composition: similar to the Spanner,
1995    the Variant contains a Stream as a private attribute. Calls to this Stream, for the Variant,
1996    are automatically delegated by use of the __getattr__ method. Special cases are overridden
1997    or managed as necessary: e.g., the Duration of a Variant is generally always zero.
1998
1999    To use Variants from a Stream, see the :func:`~music21.stream.Stream.activateVariants` method.
2000
2001
2002    >>> v = variant.Variant()
2003    >>> v.repeatAppend(note.Note(), 8)
2004    >>> len(v.notes)
2005    8
2006    >>> v.highestTime
2007    0.0
2008    >>> v.containedHighestTime
2009    8.0
2010
2011    >>> v.duration  # handled by Music21Object
2012    <music21.duration.Duration 0.0>
2013    >>> v.isStream
2014    False
2015
2016    >>> s = stream.Stream()
2017    >>> s.append(v)
2018    >>> s.append(note.Note())
2019    >>> s.highestTime
2020    1.0
2021    >>> s.show('t')
2022    {0.0} <music21.variant.Variant object of length 8.0>
2023    {0.0} <music21.note.Note C>
2024    >>> s.flatten().show('t')
2025    {0.0} <music21.variant.Variant object of length 8.0>
2026    {0.0} <music21.note.Note C>
2027    '''
2028
2029    classSortOrder = stream.Stream.classSortOrder - 2  # variants should always come first?
2030
2031    # this copies the init of Streams
2032    def __init__(self, givenElements=None, *args, **keywords):
2033        super().__init__()
2034        self.exposeTime = False
2035        self._stream = stream.VariantStorage(givenElements=givenElements,
2036                                             *args, **keywords)
2037
2038        self._replacementDuration = None
2039
2040        if 'name' in keywords:
2041            self.groups.append(keywords['name'])
2042
2043
2044    def _deepcopySubclassable(self, memo=None, ignoreAttributes=None, removeFromIgnore=None):
2045        '''
2046        see __deepcopy__ on Spanner for tests and docs
2047        '''
2048        # NOTE: this is a performance critical operation
2049        defaultIgnoreSet = {'_cache'}
2050        if ignoreAttributes is None:
2051            ignoreAttributes = defaultIgnoreSet
2052        else:
2053            ignoreAttributes = ignoreAttributes | defaultIgnoreSet
2054
2055        new = super()._deepcopySubclassable(memo, ignoreAttributes, removeFromIgnore)
2056
2057        return new
2058
2059    def __deepcopy__(self, memo=None):
2060        return self._deepcopySubclassable(memo)
2061
2062    # --------------------------------------------------------------------------
2063    # as _stream is a private Stream, unwrap/wrap methods need to override
2064    # Music21Object to get at these objects
2065    # this is the same as with Spanners
2066
2067    def purgeOrphans(self, excludeStorageStreams=True):
2068        self._stream.purgeOrphans(excludeStorageStreams)
2069        base.Music21Object.purgeOrphans(self, excludeStorageStreams)
2070
2071    def purgeLocations(self, rescanIsDead=False):
2072        # must override Music21Object to purge locations from the contained
2073        self._stream.purgeLocations(rescanIsDead=rescanIsDead)
2074        base.Music21Object.purgeLocations(self, rescanIsDead=rescanIsDead)
2075
2076    def _reprInternal(self):
2077        return 'object of length ' + str(self.containedHighestTime)
2078
2079    def __getattr__(self, attr):
2080        '''
2081        This defers all calls not defined in this Class to calls on the privately contained Stream.
2082        '''
2083        # environLocal.printDebug(['relaying unmatched attribute request '
2084        #               + attr + ' to private Stream'])
2085
2086        # must mask pitches so as not to recurse
2087        # TODO: check tt recurse does not go into this
2088        if attr in ['flat', 'pitches']:
2089            raise AttributeError
2090
2091        # needed for unpickling where ._stream doesn't exist until later...
2092        if attr != '_stream' and hasattr(self, '_stream'):
2093            return getattr(self._stream, attr)
2094        else:
2095            raise AttributeError
2096
2097    def __getitem__(self, key):
2098        return self._stream.__getitem__(key)
2099
2100
2101    def __len__(self):
2102        return len(self._stream)
2103
2104
2105    def getElementIds(self):
2106        if 'elementIds' not in self._cache or self._cache['elementIds'] is None:
2107            self._cache['elementIds'] = [id(c) for c in self._stream._elements]
2108        return self._cache['elementIds']
2109
2110
2111    def replaceElement(self, old, new):
2112        '''
2113        When copying a Variant, we need to update the Variant with new
2114        references for copied elements. Given the old element,
2115        this method will replace the old with the new.
2116
2117        The `old` parameter can be either an object or object id.
2118
2119        This method is very similar to the replaceSpannedElement method on Spanner.
2120        '''
2121        if old is None:
2122            return None  # do nothing
2123        if common.isNum(old):
2124            # this must be id(obj), not obj.id
2125            e = self._stream.coreGetElementByMemoryLocation(old)
2126            if e is not None:
2127                self._stream.replace(e, new, allDerived=False)
2128        else:
2129            # do not do all Sites: only care about this one
2130            self._stream.replace(old, new, allDerived=False)
2131
2132    # --------------------------------------------------------------------------
2133    # Stream  simulation/overrides
2134    @property
2135    def highestTime(self):
2136        '''
2137        This property masks calls to Stream.highestTime. Assuming `exposeTime`
2138        is False, this always returns zero, making the Variant always take zero time.
2139
2140        >>> v = variant.Variant()
2141        >>> v.append(note.Note(quarterLength=4))
2142        >>> v.highestTime
2143        0.0
2144        '''
2145        if self.exposeTime:
2146            return self._stream.highestTime
2147        else:
2148            return 0.0
2149
2150    @property
2151    def highestOffset(self):
2152        '''
2153        This property masks calls to Stream.highestOffset. Assuming `exposeTime`
2154        is False, this always returns zero, making the Variant always take zero time.
2155
2156        >>> v = variant.Variant()
2157        >>> v.append(note.Note(quarterLength=4))
2158        >>> v.highestOffset
2159        0.0
2160        '''
2161        if self.exposeTime:
2162            return self._stream.highestOffset
2163        else:
2164            return 0.0
2165
2166    def show(self, fmt=None, app=None):
2167        '''
2168        Call show() on the Stream contained by this Variant.
2169
2170        This method must be overridden, otherwise Music21Object.show() is called.
2171
2172
2173        >>> v = variant.Variant()
2174        >>> v.repeatAppend(note.Note(quarterLength=0.25), 8)
2175        >>> v.show('t')
2176        {0.0} <music21.note.Note C>
2177        {0.25} <music21.note.Note C>
2178        {0.5} <music21.note.Note C>
2179        {0.75} <music21.note.Note C>
2180        {1.0} <music21.note.Note C>
2181        {1.25} <music21.note.Note C>
2182        {1.5} <music21.note.Note C>
2183        {1.75} <music21.note.Note C>
2184        '''
2185        self._stream.show(fmt=fmt, app=app)
2186
2187    # --------------------------------------------------------------------------
2188    # properties particular to this class
2189
2190    @property
2191    def containedHighestTime(self):
2192        '''
2193        This property calls the contained Stream.highestTime.
2194
2195        >>> v = variant.Variant()
2196        >>> v.append(note.Note(quarterLength=4))
2197        >>> v.containedHighestTime
2198        4.0
2199        '''
2200        return self._stream.highestTime
2201
2202    @property
2203    def containedHighestOffset(self):
2204        '''
2205        This property calls the contained Stream.highestOffset.
2206
2207        >>> v = variant.Variant()
2208        >>> v.append(note.Note(quarterLength=4))
2209        >>> v.append(note.Note())
2210        >>> v.containedHighestOffset
2211        4.0
2212        '''
2213        return self._stream.highestOffset
2214
2215    @property
2216    def containedSite(self):
2217        '''
2218        Return the Stream contained in this Variant.
2219        '''
2220        return self._stream
2221
2222    def _getReplacementDuration(self):
2223        if self._replacementDuration is None:
2224            return self._stream.duration.quarterLength
2225        else:
2226            return self._replacementDuration
2227
2228    def _setReplacementDuration(self, value):
2229        self._replacementDuration = value
2230
2231    replacementDuration = property(_getReplacementDuration, _setReplacementDuration, doc='''
2232        Set or Return the quarterLength duration in the main stream which this variant
2233        object replaces in the variant version of the stream. If replacementDuration is
2234        not set, it is assumed to be the same length as the variant. If, it is set to 0,
2235        the variant should be interpreted as an insertion. Setting replacementDuration
2236        to None will return the value to the default which is the duration of the variant
2237        itself.
2238        ''')
2239
2240    @property
2241    def lengthType(self):
2242        '''
2243        Returns 'deletion' if variant is shorter than the region it replaces, 'elongation'
2244        if the variant is longer than the region it replaces, and 'replacement' if it is
2245        the same length.
2246        '''
2247        lengthDifference = self.replacementDuration - self.containedHighestTime
2248        if lengthDifference > 0.0:
2249            return 'deletion'
2250        elif lengthDifference < 0.0:
2251            return 'elongation'
2252        else:
2253            return 'replacement'
2254
2255    def replacedElements(self, contextStream=None, classList=None,
2256                         keepOriginalOffsets=False, includeSpacers=False):
2257        # noinspection PyShadowingNames
2258        '''
2259        Returns a Stream containing the elements which this variant replaces in a
2260        given context stream.
2261        This Stream will have length self.replacementDuration.
2262
2263        In regions that are strictly replaced, only elements that share a class with
2264        an element in the variant
2265        are captured. Elsewhere, all elements are captured.
2266
2267        >>> 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)
2268        >>> s.makeMeasures(inPlace=True)
2269        >>> v1stream = converter.parse("tinynotation: 4/4        a2. b-8 a8", makeNotation=False)
2270        >>> v2stream1 = converter.parse("tinynotation: 4/4                                       d4 f4 a2", makeNotation=False)
2271        >>> v2stream2 = converter.parse("tinynotation: 4/4                                                  d4 f4 AA2", makeNotation=False)
2272
2273        >>> v1 = variant.Variant()
2274        >>> v1measure = stream.Measure()
2275        >>> v1.insert(0.0, v1measure)
2276        >>> for e in v1stream.notesAndRests:
2277        ...    v1measure.insert(e.offset, e)
2278
2279        >>> v2 = variant.Variant()
2280        >>> v2measure1 = stream.Measure()
2281        >>> v2measure2 = stream.Measure()
2282        >>> v2.insert(0.0, v2measure1)
2283        >>> v2.insert(4.0, v2measure2)
2284        >>> for e in v2stream1.notesAndRests:
2285        ...    v2measure1.insert(e.offset, e)
2286        >>> for e in v2stream2.notesAndRests:
2287        ...    v2measure2.insert(e.offset, e)
2288
2289        >>> v3 = variant.Variant()
2290        >>> v2.replacementDuration = 4.0
2291        >>> v3.replacementDuration = 4.0
2292
2293        >>> s.insert(4.0, v1)    # replacement variant
2294        >>> s.insert(12.0, v2)  # insertion variant (2 bars replace 1 bar)
2295        >>> s.insert(20.0, v3)  # deletion variant (0 bars replace 1 bar)
2296
2297        >>> v1.replacedElements(s).show('text')
2298        {0.0} <music21.stream.Measure 2 offset=0.0>
2299            {0.0} <music21.note.Note A>
2300            {2.0} <music21.note.Note B->
2301            {3.0} <music21.note.Note A>
2302
2303        >>> v2.replacedElements(s).show('text')
2304        {0.0} <music21.stream.Measure 4 offset=0.0>
2305            {0.0} <music21.note.Note D>
2306            {2.0} <music21.note.Note A>
2307
2308        >>> v3.replacedElements(s).show('text')
2309        {0.0} <music21.stream.Measure 6 offset=0.0>
2310            {0.0} <music21.note.Note A>
2311            {2.0} <music21.note.Note B->
2312            {3.0} <music21.note.Note A>
2313
2314        >>> v3.replacedElements(s, keepOriginalOffsets=True).show('text')
2315        {20.0} <music21.stream.Measure 6 offset=20.0>
2316            {0.0} <music21.note.Note A>
2317            {2.0} <music21.note.Note B->
2318            {3.0} <music21.note.Note A>
2319
2320
2321        A second example:
2322
2323
2324        >>> v = variant.Variant()
2325        >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
2326        ...                  ('a', 'quarter'),('b', 'quarter')]
2327        >>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'),
2328        ...                  ('e', 'quarter'), ('e', 'quarter')]
2329        >>> variantData = [variantDataM1, variantDataM2]
2330        >>> for d in variantData:
2331        ...    m = stream.Measure()
2332        ...    for pitchName, durType in d:
2333        ...        n = note.Note(pitchName)
2334        ...        n.duration.type = durType
2335        ...        m.append(n)
2336        ...    v.append(m)
2337        >>> v.groups = ['paris']
2338        >>> v.replacementDuration = 4.0
2339
2340        >>> s = stream.Stream()
2341        >>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')]
2342        >>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'),
2343        ...                 ('a', 'eighth'), ('a', 'quarter'), ('b', 'quarter')]
2344        >>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
2345        >>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
2346        >>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4]
2347        >>> for d in streamData:
2348        ...    m = stream.Measure()
2349        ...    for pitchName, durType in d:
2350        ...        n = note.Note(pitchName)
2351        ...        n.duration.type = durType
2352        ...        m.append(n)
2353        ...    s.append(m)
2354        >>> s.insert(4.0, v)
2355
2356        >>> v.replacedElements(s).show('t')
2357        {0.0} <music21.stream.Measure 0 offset=0.0>
2358            {0.0} <music21.note.Note B>
2359            {0.5} <music21.note.Note C>
2360            {1.5} <music21.note.Note A>
2361            {2.0} <music21.note.Note A>
2362            {3.0} <music21.note.Note B>
2363        '''
2364        spacerFilter = lambda r: r.hasStyleInformation and r.style.hideObjectOnPrint
2365
2366        if contextStream is None:
2367            contextStream = self.activeSite
2368            if contextStream is None:
2369                environLocal.printDebug(
2370                    'No contextStream or activeSite, finding most recently added site (dangerous)')
2371                contextStream = self.getContextByClass('Stream')
2372                if contextStream is None:
2373                    raise VariantException('Cannot find a Stream context for this object...')
2374
2375        if self not in contextStream.variants:
2376            raise VariantException(f'Variant not found in stream {contextStream}')
2377
2378        vStart = self.getOffsetBySite(contextStream)
2379
2380        if includeSpacers is True:
2381            spacerDuration = (self
2382                              .getElementsByClass('Rest')
2383                              .addFilter(spacerFilter)
2384                              .first().duration.quarterLength)
2385        else:
2386            spacerDuration = 0.0
2387
2388
2389        if self.lengthType == 'replacement' or self.lengthType == 'elongation':
2390            vEnd = vStart + self.replacementDuration + spacerDuration
2391            classes = []
2392            for e in self.elements:
2393                classes.append(e.classes[0])
2394            if classList is not None:
2395                classes.extend(classList)
2396            returnStream = contextStream.getElementsByOffset(vStart, vEnd,
2397                includeEndBoundary=False,
2398                mustFinishInSpan=False,
2399                mustBeginInSpan=True,
2400                classList=classes).stream()
2401
2402        elif self.lengthType == 'deletion':
2403            vMiddle = vStart + self.containedHighestTime
2404            vEnd = vStart + self.replacementDuration
2405            classes = []  # collect all classes found in this variant
2406            for e in self.elements:
2407                classes.append(e.classes[0])
2408            if classList is not None:
2409                classes.extend(classList)
2410            returnPart1 = contextStream.getElementsByOffset(vStart, vMiddle,
2411                includeEndBoundary=False,
2412                mustFinishInSpan=False,
2413                mustBeginInSpan=True,
2414                classList=classes).stream()
2415            returnPart2 = contextStream.getElementsByOffset(vMiddle, vEnd,
2416                includeEndBoundary=False,
2417                mustFinishInSpan=False,
2418                mustBeginInSpan=True).stream()
2419
2420            returnStream = returnPart1
2421            for e in returnPart2.elements:
2422                oInPart = e.getOffsetBySite(returnPart2)
2423                returnStream.insert(vMiddle - vStart + oInPart, e)
2424        else:
2425            raise VariantException('lengthType must be replacement, elongation, or deletion')
2426
2427        if self in returnStream:
2428            returnStream.remove(self)
2429
2430        # This probably makes sense to do, but activateVariants
2431        #    for example only uses the offset in the original
2432        # anyways. Also, we are not changing measure numbers and should
2433        #    not as that will cause activateVariants to fail.
2434        if keepOriginalOffsets is False:
2435            for e in returnStream:
2436                e.setOffsetBySite(returnStream, e.getOffsetBySite(returnStream) - vStart)
2437
2438        return returnStream
2439
2440    def removeReplacedElementsFromStream(self, referenceStream=None, classList=None):
2441        '''
2442        remove replaced elements from a referenceStream or activeSite
2443
2444
2445        >>> v = variant.Variant()
2446        >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'), ('a', 'quarter'),
2447        ...                  ('a', 'quarter'),('b', 'quarter')]
2448        >>> variantDataM2 = [('c', 'quarter'), ('d', 'quarter'), ('e', 'quarter'), ('e', 'quarter')]
2449        >>> variantData = [variantDataM1, variantDataM2]
2450        >>> for d in variantData:
2451        ...    m = stream.Measure()
2452        ...    for pitchName, durType in d:
2453        ...        n = note.Note(pitchName)
2454        ...        n.duration.type = durType
2455        ...        m.append(n)
2456        ...    v.append(m)
2457        >>> v.groups = ['paris']
2458        >>> v.replacementDuration = 4.0
2459
2460        >>> s = stream.Stream()
2461        >>> streamDataM1 = [('a', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('g', 'quarter')]
2462        >>> streamDataM2 = [('b', 'eighth'), ('c', 'quarter'), ('a', 'eighth'),
2463        ...                 ('a', 'quarter'), ('b', 'quarter')]
2464        >>> streamDataM3 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
2465        >>> streamDataM4 = [('c', 'quarter'), ('b', 'quarter'), ('a', 'quarter'), ('a', 'quarter')]
2466        >>> streamData = [streamDataM1, streamDataM2, streamDataM3, streamDataM4]
2467        >>> for d in streamData:
2468        ...    m = stream.Measure()
2469        ...    for pitchName, durType in d:
2470        ...        n = note.Note(pitchName)
2471        ...        n.duration.type = durType
2472        ...        m.append(n)
2473        ...    s.append(m)
2474        >>> s.insert(4.0, v)
2475
2476        >>> v.removeReplacedElementsFromStream(s)
2477        >>> s.show('t')
2478        {0.0} <music21.stream.Measure 0 offset=0.0>
2479            {0.0} <music21.note.Note A>
2480            {1.0} <music21.note.Note B>
2481            {2.0} <music21.note.Note A>
2482            {3.0} <music21.note.Note G>
2483        {4.0} <music21.variant.Variant object of length 8.0>
2484        {8.0} <music21.stream.Measure 0 offset=8.0>
2485            {0.0} <music21.note.Note C>
2486            {1.0} <music21.note.Note B>
2487            {2.0} <music21.note.Note A>
2488            {3.0} <music21.note.Note A>
2489        {12.0} <music21.stream.Measure 0 offset=12.0>
2490            {0.0} <music21.note.Note C>
2491            {1.0} <music21.note.Note B>
2492            {2.0} <music21.note.Note A>
2493            {3.0} <music21.note.Note A>
2494        '''
2495        if referenceStream is None:
2496            referenceStream = self.activeSite
2497            if referenceStream is None:
2498                environLocal.printDebug('No referenceStream or activeSite, '
2499                                        + 'finding most recently added site (dangerous)')
2500                referenceStream = self.getContextByClass('Stream')
2501                if referenceStream is None:
2502                    raise VariantException('Cannot find a Stream context for this object...')
2503        if self not in referenceStream.variants:
2504            raise VariantException(f'Variant not found in stream {referenceStream}')
2505
2506        replacedElements = self.replacedElements(referenceStream, classList)
2507        for el in replacedElements:
2508            referenceStream.remove(el)
2509
2510
2511# ------------------------------------------------------------------------------
2512class Test(unittest.TestCase):
2513
2514    def pitchOut(self, listIn):
2515        out = '['
2516        for p in listIn:
2517            out += str(p) + ', '
2518        out = out[0:len(out) - 2]
2519        out += ']'
2520        return out
2521
2522    def testBasicA(self):
2523        o = Variant()
2524        o.append(note.Note('G3', quarterLength=2.0))
2525        o.append(note.Note('f3', quarterLength=2.0))
2526
2527        self.assertEqual(o.highestOffset, 0)
2528        self.assertEqual(o.highestTime, 0)
2529
2530        o.exposeTime = True
2531
2532        self.assertEqual(o.highestOffset, 2.0)
2533        self.assertEqual(o.highestTime, 4.0)
2534
2535
2536    def testBasicB(self):
2537        '''
2538        Testing relaying attributes requests to private Stream with __getattr__
2539        '''
2540        v = Variant()
2541        v.append(note.Note('G3', quarterLength=2.0))
2542        v.append(note.Note('f3', quarterLength=2.0))
2543        # these are Stream attributes
2544        self.assertEqual(v.highestOffset, 0.0)
2545        self.assertEqual(v.highestTime, 0.0)
2546
2547        self.assertEqual(len(v.notes), 2)
2548        self.assertTrue(v.hasElementOfClass('Note'))
2549        v.pop(1)  # remove the last item
2550
2551        self.assertEqual(v.highestOffset, 0.0)
2552        self.assertEqual(v.highestTime, 0.0)
2553        self.assertEqual(len(v.notes), 1)
2554
2555
2556    def testVariantGroupA(self):
2557        '''Variant groups are used to distinguish
2558        '''
2559        v1 = Variant()
2560        v1.groups.append('alt-a')
2561
2562        v1 = Variant()
2563        v1.groups.append('alt-b')
2564        self.assertIn('alt-b', v1.groups)
2565
2566
2567    def testVariantClassA(self):
2568        m1 = stream.Measure()
2569        v1 = Variant()
2570        v1.append(m1)
2571
2572        self.assertIn('Variant', v1.classes)
2573
2574        self.assertFalse(v1.hasElementOfClass('Variant'))
2575        self.assertTrue(v1.hasElementOfClass('Measure'))
2576
2577    def testDeepCopyVariantA(self):
2578        s = stream.Stream()
2579        s.repeatAppend(note.Note('G4'), 8)
2580        vn1 = note.Note('F#4')
2581        vn2 = note.Note('A-4')
2582
2583        v1 = Variant()
2584        v1.insert(0, vn1)
2585        v1.insert(0, vn2)
2586        v1Copy = copy.deepcopy(v1)
2587        # copies stored objects; they point to the different Notes vn1/vn2
2588        self.assertIsNot(v1Copy[0], v1[0])
2589        self.assertIsNot(v1Copy[1], v1[1])
2590        self.assertIs(v1[0], vn1)
2591        self.assertIsNot(v1Copy[0], vn1)
2592
2593        # normal in-place variant functionality
2594        s.insert(5, v1)
2595        self.assertEqual(self.pitchOut(s.pitches),
2596            '[G4, G4, G4, G4, G4, G4, G4, G4]')
2597        sv = s.activateVariants(inPlace=False)
2598        self.assertEqual(self.pitchOut(sv.pitches),
2599            '[G4, G4, G4, G4, G4, F#4, A-4, G4, G4]')
2600
2601        # test functionality on a deepcopy
2602        sCopy = copy.deepcopy(s)
2603        self.assertEqual(len(sCopy.variants), 1)
2604        self.assertEqual(self.pitchOut(sCopy.pitches),
2605            '[G4, G4, G4, G4, G4, G4, G4, G4]')
2606        sCopy.activateVariants(inPlace=True)
2607        self.assertEqual(self.pitchOut(sCopy.pitches),
2608            '[G4, G4, G4, G4, G4, F#4, A-4, G4, G4]')
2609
2610    def testDeepCopyVariantB(self):
2611        s = stream.Stream()
2612        s.repeatAppend(note.Note('G4'), 8)
2613        vn1 = note.Note('F#4')
2614        vn2 = note.Note('A-4')
2615        v1 = Variant()
2616        v1.insert(0, vn1)
2617        v1.insert(0, vn2)
2618        s.insert(5, v1)
2619
2620        # as we deepcopy the elements in the variants, we have new Notes
2621        sCopy = copy.deepcopy(s)
2622        sCopy.activateVariants(inPlace=True)
2623        self.assertEqual(self.pitchOut(sCopy.pitches),
2624            '[G4, G4, G4, G4, G4, F#4, A-4, G4, G4]')
2625        # can transpose the note in place
2626        sCopy.notes[5].transpose(12, inPlace=True)
2627        self.assertEqual(self.pitchOut(sCopy.pitches),
2628            '[G4, G4, G4, G4, G4, F#5, A-4, G4, G4]')
2629
2630        # however, if the Variant deepcopy still references the original
2631        # notes it had, then when we try to activate the variant in the
2632        # in original Stream, we would get unexpected results (the octave shift)
2633
2634        s.activateVariants(inPlace=True)
2635        self.assertEqual(self.pitchOut(s.pitches),
2636            '[G4, G4, G4, G4, G4, F#4, A-4, G4, G4]')
2637
2638
2639class TestExternal(unittest.TestCase):
2640    show = True
2641
2642    def testMergeJacopoVariants(self):
2643        from music21 import corpus
2644        j1 = corpus.parse('trecento/PMFC_06-Jacopo-03a')
2645        j2 = corpus.parse('trecento/PMFC_06-Jacopo-03b')
2646        jMerged = mergeVariantScores(j1, j2)
2647        if self.show:
2648            jMerged.show('musicxml.png')
2649
2650
2651if __name__ == '__main__':
2652    import music21
2653    music21.mainTest(Test)  # , TestExternal)
2654