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