1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: segment.py 4# Purpose: music21 class representing a figured bass note and notation 5# realization. 6# Authors: Jose Cabal-Ugaz 7# 8# Copyright: Copyright © 2011 Michael Scott Cuthbert and the music21 Project 9# License: BSD, see license.txt 10# ------------------------------------------------------------------------------ 11import collections 12import copy 13import itertools 14import unittest 15 16from typing import Dict, Optional, Union 17 18from music21 import chord 19from music21 import environment 20from music21 import exceptions21 21from music21 import note 22from music21 import pitch 23from music21 import scale 24from music21.figuredBass import possibility 25from music21.figuredBass import realizerScale 26from music21.figuredBass import resolution 27from music21.figuredBass import rules 28 29_MOD = 'figuredBass.segment' 30 31_defaultRealizerScale: Dict[str, Optional[realizerScale.FiguredBassScale]] = { 32 'scale': None, # singleton 33} 34 35 36class Segment: 37 _DOC_ORDER = ['allSinglePossibilities', 38 'singlePossibilityRules', 39 'allCorrectSinglePossibilities', 40 'consecutivePossibilityRules', 41 'specialResolutionRules', 42 'allCorrectConsecutivePossibilities', 43 'resolveDominantSeventhSegment', 44 'resolveDiminishedSeventhSegment', 45 'resolveAugmentedSixthSegment'] 46 _DOC_ATTR = { 47 'bassNote': '''A :class:`~music21.note.Note` whose pitch 48 forms the bass of each possibility.''', 49 'numParts': '''The number of parts (including the bass) that possibilities 50 should contain, which 51 comes directly from :attr:`~music21.figuredBass.rules.Rules.numParts` 52 in the Rules object.''', 53 'pitchNamesInChord': '''A list of allowable pitch names. 54 This is derived from bassNote.pitch and notationString 55 using :meth:`~music21.figuredBass.realizerScale.FiguredBassScale.getPitchNames`.''', 56 'allPitchesAboveBass': '''A list of allowable pitches in the upper parts of a possibility. 57 This is derived using 58 :meth:`~music21.figuredBass.segment.getPitches`, providing bassNote.pitch, 59 :attr:`~music21.figuredBass.rules.Rules.maxPitch` 60 from the Rules object, and 61 :attr:`~music21.figuredBass.segment.Segment.pitchNamesInChord` as arguments.''', 62 'segmentChord': ''':attr:`~music21.figuredBass.segment.Segment.allPitchesAboveBass` 63 represented as a :class:`~music21.chord.Chord`.''', 64 'fbRules': 'A deepcopy of the :class:`~music21.figuredBass.rules.Rules` object provided.', 65 } 66 67 def __init__(self, 68 bassNote: Union[str, note.Note] = 'C3', 69 notationString: Optional[str] = None, 70 fbScale: Optional[realizerScale.FiguredBassScale] = None, 71 fbRules: Optional[rules.Rules] = None, 72 numParts=4, 73 maxPitch: Union[str, pitch.Pitch] = 'B5', 74 listOfPitches=None): 75 ''' 76 A Segment corresponds to a 1:1 realization of a bassNote and notationString 77 of a :class:`~music21.figuredBass.realizer.FiguredBassLine`. 78 It is created by passing six arguments: a 79 :class:`~music21.figuredBass.realizerScale.FiguredBassScale`, a bassNote, a notationString, 80 a :class:`~music21.figuredBass.rules.Rules` object, a number of parts and a maximum pitch. 81 Realizations of a Segment are represented 82 as possibility tuples (see :mod:`~music21.figuredBass.possibility` for more details). 83 84 Methods in Python's `itertools <http://docs.python.org/library/itertools.html>`_ 85 module are used extensively. Methods 86 which generate possibilities or possibility progressions return iterators, 87 which are turned into lists in the examples 88 for display purposes only. 89 90 if fbScale is None, a realizerScale.FiguredBassScale() is created 91 92 if fbRules is None, a rules.Rules() instance is created. Each Segment gets 93 its own deepcopy of the one given. 94 95 96 Here, a Segment is created using the default values: a FiguredBassScale in C, 97 a bassNote of C3, an empty notationString, and a default 98 Rules object. 99 100 >>> from music21.figuredBass import segment 101 >>> s1 = segment.Segment() 102 >>> s1.bassNote 103 <music21.note.Note C> 104 >>> s1.numParts 105 4 106 >>> s1.pitchNamesInChord 107 ['C', 'E', 'G'] 108 >>> [str(p) for p in s1.allPitchesAboveBass] 109 ['C3', 'E3', 'G3', 'C4', 'E4', 'G4', 'C5', 'E5', 'G5'] 110 >>> s1.segmentChord 111 <music21.chord.Chord C3 E3 G3 C4 E4 G4 C5 E5 G5> 112 ''' 113 if isinstance(bassNote, str): 114 bassNote = note.Note(bassNote) 115 if isinstance(maxPitch, str): 116 maxPitch = pitch.Pitch(maxPitch) 117 118 if fbScale is None: 119 if _defaultRealizerScale['scale'] is None: 120 _defaultRealizerScale['scale'] = realizerScale.FiguredBassScale() 121 fbScale = _defaultRealizerScale['scale'] # save making it 122 123 if fbRules is None: 124 self.fbRules = rules.Rules() 125 else: 126 self.fbRules = copy.deepcopy(fbRules) 127 128 self._specialResolutionRuleChecking = None 129 self._singlePossibilityRuleChecking = None 130 self._consecutivePossibilityRuleChecking = None 131 132 self.bassNote = bassNote 133 self.numParts = numParts 134 self._maxPitch = maxPitch 135 if notationString is None and listOfPitches is not None: 136 # must be a chord symbol or roman num. 137 self.pitchNamesInChord = listOfPitches 138 # ------ Added to accommodate harmony.ChordSymbol and roman.RomanNumeral objects ------ 139 else: 140 self.pitchNamesInChord = fbScale.getPitchNames(self.bassNote.pitch, notationString) 141 142 self.allPitchesAboveBass = getPitches(self.pitchNamesInChord, 143 self.bassNote.pitch, 144 self._maxPitch) 145 self.segmentChord = chord.Chord(self.allPitchesAboveBass, 146 quarterLength=bassNote.quarterLength) 147 self._environRules = environment.Environment(_MOD) 148 149 # ------------------------------------------------------------------------------ 150 # EXTERNAL METHODS 151 152 def singlePossibilityRules(self, fbRules=None): 153 ''' 154 A framework for storing single possibility rules and methods to be applied 155 in :meth:`~music21.figuredBass.segment.Segment.allCorrectSinglePossibilities`. 156 Takes in a :class:`~music21.figuredBass.rules.Rules` object, fbRules. 157 If None then a new rules object is created. 158 159 Items are added within this method in the following form: 160 161 162 (willRunOnlyIfTrue, methodToRun, keepSolutionsWhichReturn, optionalArgs) 163 164 165 These items are compiled internally when 166 :meth:`~music21.figuredBass.segment.Segment.allCorrectSinglePossibilities` 167 is called on a Segment. Here, the compilation of rules and 168 methods bases on a default fbRules is shown. 169 170 >>> from music21.figuredBass import segment 171 >>> segmentA = segment.Segment() 172 >>> allSingleRules = segmentA.singlePossibilityRules() 173 >>> segment.printRules(allSingleRules) 174 Will run: Method: Keep solutions which return: Arguments: 175 True isIncomplete False ['C', 'E', 'G'] 176 True upperPartsWithinLimit True 12 177 True voiceCrossing False None 178 179 180 Here, a modified fbRules is provided, which allows for incomplete possibilities. 181 182 183 >>> from music21.figuredBass import rules 184 >>> fbRules = rules.Rules() 185 >>> fbRules.forbidIncompletePossibilities = False 186 >>> allSingleRules = segmentA.singlePossibilityRules(fbRules) 187 >>> segment.printRules(allSingleRules) 188 Will run: Method: Keep solutions which return: Arguments: 189 False isIncomplete False ['C', 'E', 'G'] 190 True upperPartsWithinLimit True 12 191 True voiceCrossing False None 192 ''' 193 if fbRules is None: 194 fbRules = rules.Rules() 195 196 singlePossibRules = [ 197 (fbRules.forbidIncompletePossibilities, 198 possibility.isIncomplete, 199 False, 200 [self.pitchNamesInChord]), 201 (True, 202 possibility.upperPartsWithinLimit, 203 True, 204 [fbRules.upperPartsMaxSemitoneSeparation]), 205 (fbRules.forbidVoiceCrossing, 206 possibility.voiceCrossing, 207 False) 208 ] 209 210 return singlePossibRules 211 212 def consecutivePossibilityRules(self, fbRules=None): 213 ''' 214 A framework for storing consecutive possibility rules and methods to be applied 215 in :meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities`. 216 Takes in a :class:`~music21.figuredBass.rules.Rules` object, fbRules; if None 217 then a new rules.Rules() object is created. 218 219 220 Items are added within this method in the following form: 221 222 223 (willRunOnlyIfTrue, methodToRun, keepSolutionsWhichReturn, optionalArgs) 224 225 226 These items are compiled internally when 227 :meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities` 228 is called on a Segment. Here, the compilation of rules and methods 229 bases on a default fbRules is shown. 230 231 >>> from music21.figuredBass import segment 232 >>> segmentA = segment.Segment() 233 >>> allConsecutiveRules = segmentA.consecutivePossibilityRules() 234 235 >>> segment.printRules(allConsecutiveRules) 236 Will run: Method: Keep solutions which return: Arguments: 237 True partsSame True [] 238 False upperPartsSame True None 239 True voiceOverlap False None 240 True partMovementsWithinLimits True [] 241 True parallelFifths False None 242 True parallelOctaves False None 243 True hiddenFifth False None 244 True hiddenOctave False None 245 False couldBeItalianA6Resolution True [<music21.pitch.Pitch C3>, 246 <music21.pitch.Pitch C3>, 247 <music21.pitch.Pitch E3>, 248 <music21.pitch.Pitch G3>], True 249 250 251 Now, a modified fbRules is provided, allowing hidden octaves and 252 voice overlap, and limiting the soprano line to stepwise motion. 253 254 255 >>> from music21.figuredBass import rules 256 >>> fbRules = rules.Rules() 257 >>> fbRules.forbidVoiceOverlap = False 258 >>> fbRules.forbidHiddenOctaves = False 259 >>> fbRules.partMovementLimits.append((1, 2)) 260 >>> allConsecutiveRules = segmentA.consecutivePossibilityRules(fbRules) 261 >>> segment.printRules(allConsecutiveRules) 262 Will run: Method: Keep solutions which return: Arguments: 263 True partsSame True [] 264 False upperPartsSame True None 265 False voiceOverlap False None 266 True partMovementsWithinLimits True [(1, 2)] 267 True parallelFifths False None 268 True parallelOctaves False None 269 True hiddenFifth False None 270 False hiddenOctave False None 271 False couldBeItalianA6Resolution True [<music21.pitch.Pitch C3>, 272 <music21.pitch.Pitch C3>, 273 <music21.pitch.Pitch E3>, 274 <music21.pitch.Pitch G3>], True 275 ''' 276 if fbRules is None: 277 fbRules = rules.Rules() 278 279 isItalianAugmentedSixth = self.segmentChord.isItalianAugmentedSixth() 280 281 consecutivePossibRules = [ 282 (True, possibility.partsSame, True, [fbRules._partsToCheck]), 283 (fbRules._upperPartsRemainSame, possibility.upperPartsSame, True), 284 (fbRules.forbidVoiceOverlap, possibility.voiceOverlap, False), 285 (True, possibility.partMovementsWithinLimits, True, [fbRules.partMovementLimits]), 286 (fbRules.forbidParallelFifths, possibility.parallelFifths, False), 287 (fbRules.forbidParallelOctaves, possibility.parallelOctaves, False), 288 (fbRules.forbidHiddenFifths, possibility.hiddenFifth, False), 289 (fbRules.forbidHiddenOctaves, possibility.hiddenOctave, False), 290 (fbRules.resolveAugmentedSixthProperly and isItalianAugmentedSixth, 291 possibility.couldBeItalianA6Resolution, 292 True, 293 [_unpackTriad(self.segmentChord), fbRules.restrictDoublingsInItalianA6Resolution]) 294 ] 295 296 return consecutivePossibRules 297 298 def specialResolutionRules(self, fbRules=None): 299 ''' 300 A framework for storing methods which perform special resolutions 301 on Segments. Unlike the methods in 302 :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules` and 303 :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`, 304 these methods deal with the Segment itself, and rely on submethods 305 to resolve the individual possibilities accordingly depending on what 306 the resolution Segment is. 307 308 If fbRules is None, then a new rules.Rules() object is created. 309 310 Items are added within this method in the following form: 311 312 313 (willRunOnlyIfTrue, methodToRun, optionalArgs) 314 315 316 These items are compiled internally 317 when :meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities` 318 is called on a Segment. Here, the compilation of rules and methods 319 based on a default fbRules is shown. 320 321 >>> from music21.figuredBass import segment 322 >>> segmentA = segment.Segment() 323 >>> allSpecialResRules = segmentA.specialResolutionRules() 324 >>> segment.printRules(allSpecialResRules, maxLength=3) 325 Will run: Method: Arguments: 326 False resolveDominantSeventhSegment None 327 False resolveDiminishedSeventhSegment False 328 False resolveAugmentedSixthSegment None 329 330 331 Dominant Seventh Segment: 332 333 334 >>> from music21 import note 335 >>> segmentA = segment.Segment(bassNote=note.Note('B2'), notationString='6,5') 336 >>> allSpecialResRules = segmentA.specialResolutionRules() 337 >>> segment.printRules(allSpecialResRules, maxLength=3) 338 Will run: Method: Arguments: 339 True resolveDominantSeventhSegment None 340 False resolveDiminishedSeventhSegment False 341 False resolveAugmentedSixthSegment None 342 343 344 Fully-Diminished Seventh Segment: 345 346 347 >>> segmentA = segment.Segment(bassNote=note.Note('B2'), notationString='-7') 348 >>> allSpecialResRules = segmentA.specialResolutionRules() 349 >>> segment.printRules(allSpecialResRules, maxLength=3) 350 Will run: Method: Arguments: 351 False resolveDominantSeventhSegment None 352 True resolveDiminishedSeventhSegment False 353 False resolveAugmentedSixthSegment None 354 355 356 Augmented Sixth Segment: 357 358 359 >>> segmentA = segment.Segment(bassNote=note.Note('A-2'), notationString='#6,b5') 360 >>> allSpecialResRules = segmentA.specialResolutionRules() 361 >>> segment.printRules(allSpecialResRules, maxLength=3) 362 Will run: Method: Arguments: 363 False resolveDominantSeventhSegment None 364 False resolveDiminishedSeventhSegment False 365 True resolveAugmentedSixthSegment None 366 ''' 367 if fbRules is None: 368 fbRules = rules.Rules() 369 370 isDominantSeventh = self.segmentChord.isDominantSeventh() 371 isDiminishedSeventh = self.segmentChord.isDiminishedSeventh() 372 isAugmentedSixth = self.segmentChord.isAugmentedSixth() 373 374 specialResRules = [ 375 (fbRules.resolveDominantSeventhProperly and isDominantSeventh, 376 self.resolveDominantSeventhSegment), 377 (fbRules.resolveDiminishedSeventhProperly and isDiminishedSeventh, 378 self.resolveDiminishedSeventhSegment, 379 [fbRules.doubledRootInDim7]), 380 (fbRules.resolveAugmentedSixthProperly and isAugmentedSixth, 381 self.resolveAugmentedSixthSegment) 382 ] 383 384 return specialResRules 385 386 def resolveDominantSeventhSegment(self, segmentB): 387 ''' 388 Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord` 389 spells out a dominant seventh chord. If no applicable method in 390 :mod:`~music21.figuredBass.resolution` can be used, the Segment is resolved 391 as an ordinary Segment. 392 393 394 >>> from music21.figuredBass import segment 395 >>> from music21 import note 396 >>> segmentA = segment.Segment(bassNote=note.Note('G2'), notationString='7') 397 >>> allDomPossib = segmentA.allCorrectSinglePossibilities() 398 >>> allDomPossibList = list(allDomPossib) 399 >>> len(allDomPossibList) 400 8 401 >>> allDomPossibList[2] 402 (<music21.pitch.Pitch D4>, <music21.pitch.Pitch B3>, 403 <music21.pitch.Pitch F3>, <music21.pitch.Pitch G2>) 404 >>> allDomPossibList[5] 405 (<music21.pitch.Pitch D5>, <music21.pitch.Pitch B4>, 406 <music21.pitch.Pitch F4>, <music21.pitch.Pitch G2>) 407 408 Here, the Soprano pitch of resolution (C6) exceeds default maxPitch of B5, so 409 it's filtered out. 410 411 >>> [p.nameWithOctave for p in allDomPossibList[7]] 412 ['B5', 'F5', 'D5', 'G2'] 413 414 415 >>> segmentB = segment.Segment(bassNote=note.Note('C3'), notationString='') 416 >>> domResPairs = segmentA.resolveDominantSeventhSegment(segmentB) 417 >>> domResPairsList = list(domResPairs) 418 >>> len(domResPairsList) 419 7 420 >>> domResPairsList[2] 421 ((<music21.pitch.Pitch D4>, <...B3>, <...F3>, <...G2>), 422 (<...C4>, <...C4>, <...E3>, <...C3>)) 423 >>> domResPairsList[5] 424 ((<...D5>, <...B4>, <...F4>, <...G2>), (<...C5>, <...C5>, <...E4>, <...C3>)) 425 ''' 426 domChord = self.segmentChord 427 if not domChord.isDominantSeventh(): 428 # Put here for stand-alone purposes. 429 raise SegmentException('Dominant seventh resolution: Not a dominant seventh Segment.') 430 domChordInfo = _unpackSeventhChord(domChord) 431 dominantScale = scale.MajorScale().derive(domChord) 432 minorScale = dominantScale.getParallelMinor() 433 434 tonic = dominantScale.getTonic() 435 subdominant = dominantScale.pitchFromDegree(4) 436 majSubmediant = dominantScale.pitchFromDegree(6) 437 minSubmediant = minorScale.pitchFromDegree(6) 438 439 resChord = segmentB.segmentChord 440 domInversion = (domChord.inversion() == 2) 441 resInversion = (resChord.inversion()) 442 resolveV43toI6 = domInversion and resInversion == 1 443 444 if (domChord.inversion() == 0 445 and resChord.root().name == tonic.name 446 and (resChord.isMajorTriad() or resChord.isMinorTriad())): 447 # V7 to I resolutions are always incomplete, with a missing fifth. 448 segmentB.fbRules.forbidIncompletePossibilities = False 449 450 dominantResolutionMethods = [ 451 (resChord.root().name == tonic.name and resChord.isMajorTriad(), 452 resolution.dominantSeventhToMajorTonic, 453 [resolveV43toI6, domChordInfo]), 454 (resChord.root().name == tonic.name and resChord.isMinorTriad(), 455 resolution.dominantSeventhToMinorTonic, 456 [resolveV43toI6, domChordInfo]), 457 ((resChord.root().name == majSubmediant.name 458 and resChord.isMinorTriad() 459 and domInversion == 0), 460 resolution.dominantSeventhToMinorSubmediant, 461 [domChordInfo]), 462 ((resChord.root().name == minSubmediant.name 463 and resChord.isMajorTriad() 464 and domInversion == 0), 465 resolution.dominantSeventhToMajorSubmediant, 466 [domChordInfo]), 467 ((resChord.root().name == subdominant.name 468 and resChord.isMajorTriad() 469 and domInversion == 0), 470 resolution.dominantSeventhToMajorSubdominant, 471 [domChordInfo]), 472 ((resChord.root().name == subdominant.name 473 and resChord.isMinorTriad() 474 and domInversion == 0), 475 resolution.dominantSeventhToMinorSubdominant, 476 [domChordInfo]) 477 ] 478 479 try: 480 return self._resolveSpecialSegment(segmentB, dominantResolutionMethods) 481 except SegmentException: 482 self._environRules.warn( 483 'Dominant seventh resolution: No proper resolution available. ' 484 + 'Executing ordinary resolution.') 485 return self._resolveOrdinarySegment(segmentB) 486 487 def resolveDiminishedSeventhSegment(self, segmentB, doubledRoot=False): 488 ''' 489 Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord` 490 spells out a diminished seventh chord. If no applicable method in 491 :mod:`~music21.figuredBass.resolution` can be used, the Segment is resolved 492 as an ordinary Segment. 493 494 >>> from music21.figuredBass import segment 495 >>> from music21 import note 496 >>> segmentA = segment.Segment(bassNote=note.Note('B2'), notationString='b7') 497 >>> allDimPossib = segmentA.allCorrectSinglePossibilities() 498 >>> allDimPossibList = list(allDimPossib) 499 >>> len(allDimPossibList) 500 7 501 >>> [p.nameWithOctave for p in allDimPossibList[4]] 502 ['D5', 'A-4', 'F4', 'B2'] 503 >>> [p.nameWithOctave for p in allDimPossibList[6]] 504 ['A-5', 'F5', 'D5', 'B2'] 505 506 507 >>> segmentB = segment.Segment(bassNote=note.Note('C3'), notationString='') 508 >>> dimResPairs = segmentA.resolveDiminishedSeventhSegment(segmentB) 509 >>> dimResPairsList = list(dimResPairs) 510 >>> len(dimResPairsList) 511 7 512 >>> dimResPairsList[4] 513 ((<...D5>, <...A-4>, <...F4>, <...B2>), (<...E5>, <...G4>, <...E4>, <...C3>)) 514 >>> dimResPairsList[6] 515 ((<...A-5>, <...F5>, <...D5>, <...B2>), (<...G5>, <...E5>, <...E5>, <...C3>)) 516 ''' 517 dimChord = self.segmentChord 518 if not dimChord.isDiminishedSeventh(): 519 # Put here for stand-alone purposes. 520 raise SegmentException( 521 'Diminished seventh resolution: Not a diminished seventh Segment.') 522 dimChordInfo = _unpackSeventhChord(dimChord) 523 dimScale = scale.HarmonicMinorScale().deriveByDegree(7, dimChord.root()) 524 # minorScale = dimScale.getParallelMinor() 525 526 tonic = dimScale.getTonic() 527 subdominant = dimScale.pitchFromDegree(4) 528 529 resChord = segmentB.segmentChord 530 if dimChord.inversion() == 1: # Doubled root in context 531 if resChord.inversion() == 0: 532 doubledRoot = True 533 elif resChord.inversion() == 1: 534 doubledRoot = False 535 536 diminishedResolutionMethods = [ 537 (resChord.root().name == tonic.name and resChord.isMajorTriad(), 538 resolution.diminishedSeventhToMajorTonic, 539 [doubledRoot, dimChordInfo]), 540 (resChord.root().name == tonic.name and resChord.isMinorTriad(), 541 resolution.diminishedSeventhToMinorTonic, 542 [doubledRoot, dimChordInfo]), 543 (resChord.root().name == subdominant.name and resChord.isMajorTriad(), 544 resolution.diminishedSeventhToMajorSubdominant, 545 [dimChordInfo]), 546 (resChord.root().name == subdominant.name and resChord.isMinorTriad(), 547 resolution.diminishedSeventhToMinorSubdominant, 548 [dimChordInfo]) 549 ] 550 551 try: 552 return self._resolveSpecialSegment(segmentB, diminishedResolutionMethods) 553 except SegmentException: 554 self._environRules.warn( 555 'Diminished seventh resolution: No proper resolution available. ' 556 + 'Executing ordinary resolution.') 557 return self._resolveOrdinarySegment(segmentB) 558 559 def resolveAugmentedSixthSegment(self, segmentB): 560 ''' 561 Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord` 562 spells out a 563 French, German, or Swiss augmented sixth chord. Italian augmented sixth Segments 564 are solved as an 565 ordinary Segment using :meth:`~music21.figuredBass.possibility.couldBeItalianA6Resolution`. 566 If no 567 applicable method in :mod:`~music21.figuredBass.resolution` can be used, the Segment 568 is resolved 569 as an ordinary Segment. 570 571 572 >>> from music21.figuredBass import segment 573 >>> from music21 import note 574 >>> segmentA = segment.Segment(bassNote=note.Note('A-2'), notationString='#6,b5,3') 575 >>> segmentA.pitchNamesInChord # spell out a Gr+6 chord 576 ['A-', 'C', 'E-', 'F#'] 577 >>> allAugSixthPossib = segmentA.allCorrectSinglePossibilities() 578 >>> allAugSixthPossibList = list(allAugSixthPossib) 579 >>> len(allAugSixthPossibList) 580 7 581 582 >>> allAugSixthPossibList[1] 583 (<music21.pitch.Pitch C4>, <music21.pitch.Pitch F#3>, <...E-3>, <...A-2>) 584 >>> allAugSixthPossibList[4] 585 (<music21.pitch.Pitch C5>, <music21.pitch.Pitch F#4>, <...E-4>, <...A-2>) 586 587 588 >>> segmentB = segment.Segment(bassNote=note.Note('G2'), notationString='') 589 >>> allAugResPossibPairs = segmentA.resolveAugmentedSixthSegment(segmentB) 590 >>> allAugResPossibPairsList = list(allAugResPossibPairs) 591 >>> len(allAugResPossibPairsList) 592 7 593 >>> allAugResPossibPairsList[1] 594 ((<...C4>, <...F#3>, <...E-3>, <...A-2>), (<...B3>, <...G3>, <...D3>, <...G2>)) 595 >>> allAugResPossibPairsList[4] 596 ((<...C5>, <...F#4>, <...E-4>, <...A-2>), (<...B4>, <...G4>, <...D4>, <...G2>)) 597 ''' 598 augSixthChord = self.segmentChord 599 if not augSixthChord.isAugmentedSixth(): 600 # Put here for stand-alone purposes. 601 raise SegmentException('Augmented sixth resolution: Not an augmented sixth Segment.') 602 if augSixthChord.isItalianAugmentedSixth(): 603 return self._resolveOrdinarySegment(segmentB) 604 elif augSixthChord.isFrenchAugmentedSixth(): 605 augSixthType = 1 606 elif augSixthChord.isGermanAugmentedSixth(): 607 augSixthType = 2 608 elif augSixthChord.isSwissAugmentedSixth(): 609 augSixthType = 3 610 else: 611 self._environRules.warn( 612 'Augmented sixth resolution: ' 613 + 'Augmented sixth type not supported. Executing ordinary resolution.') 614 return self._resolveOrdinarySegment(segmentB) 615 616 tonic = resolution._transpose(augSixthChord.bass(), 'M3') 617 majorScale = scale.MajorScale(tonic) 618 # minorScale = scale.MinorScale(tonic) 619 resChord = segmentB.segmentChord 620 augSixthChordInfo = _unpackSeventhChord(augSixthChord) 621 622 augmentedSixthResolutionMethods = [ 623 ((resChord.inversion() == 2 624 and resChord.root().name == tonic.name 625 and resChord.isMajorTriad()), 626 resolution.augmentedSixthToMajorTonic, [augSixthType, augSixthChordInfo]), 627 ((resChord.inversion() == 2 628 and resChord.root().name == tonic.name 629 and resChord.isMinorTriad()), 630 resolution.augmentedSixthToMinorTonic, 631 [augSixthType, augSixthChordInfo]), 632 ((majorScale.pitchFromDegree(5).name == resChord.bass().name 633 and resChord.isMajorTriad()), 634 resolution.augmentedSixthToDominant, 635 [augSixthType, augSixthChordInfo]) 636 ] 637 638 try: 639 return self._resolveSpecialSegment(segmentB, augmentedSixthResolutionMethods) 640 except SegmentException: 641 self._environRules.warn( 642 'Augmented sixth resolution: No proper resolution available. ' 643 + 'Executing ordinary resolution.') 644 return self._resolveOrdinarySegment(segmentB) 645 646 def allSinglePossibilities(self): 647 ''' 648 Returns an iterator through a set of naive possibilities for 649 a Segment, using :attr:`~music21.figuredBass.segment.Segment.numParts`, 650 the pitch of :attr:`~music21.figuredBass.segment.Segment.bassNote`, and 651 :attr:`~music21.figuredBass.segment.Segment.allPitchesAboveBass`. 652 653 >>> from music21.figuredBass import segment 654 >>> segmentA = segment.Segment() 655 >>> allPossib = segmentA.allSinglePossibilities() 656 >>> allPossib.__class__ 657 <... 'itertools.product'> 658 659 660 The number of naive possibilities is always the length of 661 :attr:`~music21.figuredBass.segment.Segment.allPitchesAboveBass` 662 raised to the (:attr:`~music21.figuredBass.segment.Segment.numParts` - 1) 663 power. The power is 1 less than the number of parts because 664 the bass pitch is constant. 665 666 667 >>> allPossibList = list(allPossib) 668 >>> len(segmentA.allPitchesAboveBass) 669 9 670 >>> segmentA.numParts 671 4 672 >>> len(segmentA.allPitchesAboveBass) ** (segmentA.numParts-1) 673 729 674 >>> len(allPossibList) 675 729 676 677 >>> for i in (81, 275, 426): 678 ... [str(p) for p in allPossibList[i]] 679 ['E3', 'C3', 'C3', 'C3'] 680 ['C4', 'C4', 'G4', 'C3'] 681 ['G4', 'G3', 'C4', 'C3'] 682 ''' 683 iterables = [self.allPitchesAboveBass] * (self.numParts - 1) 684 iterables.append([pitch.Pitch(self.bassNote.pitch.nameWithOctave)]) 685 return itertools.product(*iterables) 686 687 def allCorrectSinglePossibilities(self): 688 ''' 689 Uses :meth:`~music21.figuredBass.segment.Segment.allSinglePossibilities` and 690 returns an iterator through a set of correct possibilities for 691 a Segment, all possibilities which pass all filters in 692 :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`. 693 694 695 >>> from music21.figuredBass import segment 696 >>> segmentA = segment.Segment() 697 >>> allPossib = segmentA.allSinglePossibilities() 698 >>> allCorrectPossib = segmentA.allCorrectSinglePossibilities() 699 700 701 Most of the 729 naive possibilities were filtered out using the default rules set, 702 leaving only 21. 703 704 705 >>> allPossibList = list(allPossib) 706 >>> len(allPossibList) 707 729 708 >>> allCorrectPossibList = list(allCorrectPossib) 709 >>> len(allCorrectPossibList) 710 21 711 712 >>> for i in (5, 12, 20): 713 ... [str(p) for p in allCorrectPossibList[i]] 714 ['E4', 'G3', 'G3', 'C3'] 715 ['C5', 'G4', 'E4', 'C3'] 716 ['G5', 'G5', 'E5', 'C3'] 717 ''' 718 self._singlePossibilityRuleChecking = _compileRules( 719 self.singlePossibilityRules(self.fbRules)) 720 allA = self.allSinglePossibilities() 721 return [possibA for possibA in allA if self._isCorrectSinglePossibility(possibA)] 722 723 def allCorrectConsecutivePossibilities(self, segmentB): 724 ''' 725 Returns an iterator through correct (possibA, possibB) pairs. 726 727 728 * If segmentA (self) is a special Segment, meaning that one of the Segment 729 resolution methods in :meth:`~music21.figuredBass.segment.Segment.specialResolutionRules` 730 needs to be applied, then this method returns every correct possibility of segmentA 731 matched up with exactly one resolution possibility. 732 733 734 * If segmentA is an ordinary, non-special Segment, then this method returns every 735 combination of correct possibilities of segmentA and correct possibilities of segmentB 736 which passes all filters 737 in :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`. 738 739 740 Two notes on segmentA being a special Segment: 741 742 743 1. By default resolution possibilities are not filtered 744 using :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules` 745 rules of segmentB. Filter by setting 746 :attr:`~music21.figuredBass.rules.Rules.applySinglePossibRulesToResolution` to True. 747 748 749 2. By default, (possibA, possibB) pairs are not filtered 750 using :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules` 751 rules of segmentA. Filter by setting 752 :attr:`~music21.figuredBass.rules.Rules.applyConsecutivePossibRulesToResolution` 753 to True. 754 755 >>> from music21.figuredBass import segment 756 >>> from music21 import note 757 >>> segmentA = segment.Segment(bassNote=note.Note('C3'), notationString='') 758 >>> segmentB = segment.Segment(bassNote=note.Note('D3'), notationString='4,3') 759 760 761 Here, an ordinary resolution is being executed, because segmentA is an ordinary Segment. 762 763 764 >>> consecutivePairs1 = segmentA.allCorrectConsecutivePossibilities(segmentB) 765 >>> consecutivePairsList1 = list(consecutivePairs1) 766 >>> len(consecutivePairsList1) 767 31 768 >>> consecutivePairsList1[29] 769 ((<...G5>, <...G5>, <...E5>, <...C3>), (<...G5>, <...F5>, <...B4>, <...D3>)) 770 771 772 Here, a special resolution is being executed, because segmentA below is a 773 special Segment. 774 775 776 >>> segmentA = segment.Segment(bassNote=note.Note('D3'), notationString='4,3') 777 >>> segmentB = segment.Segment(bassNote=note.Note('C3'), notationString='') 778 >>> consecutivePairs2 = segmentA.allCorrectConsecutivePossibilities(segmentB) 779 >>> consecutivePairsList2 = list(consecutivePairs2) 780 >>> len(consecutivePairsList2) 781 6 782 >>> consecutivePairsList2[5] 783 ((<...G5>, <...F5>, <...B4>, <...D3>), (<...G5>, <...E5>, <...C5>, <...C3>)) 784 ''' 785 if not (self.numParts == segmentB.numParts): 786 raise SegmentException('Two segments with unequal numParts cannot be compared.') 787 if not (self._maxPitch == segmentB._maxPitch): 788 raise SegmentException('Two segments with unequal maxPitch cannot be compared.') 789 self._specialResolutionRuleChecking = _compileRules( 790 self.specialResolutionRules(self.fbRules), 3) 791 for (resolutionMethod, args) in self._specialResolutionRuleChecking[True]: 792 return resolutionMethod(segmentB, *args) 793 return self._resolveOrdinarySegment(segmentB) 794 795 # ------------------------------------------------------------------------------ 796 # INTERNAL METHODS 797 798 def _isCorrectSinglePossibility(self, possibA): 799 ''' 800 Takes in a possibility (possibA) from a segmentA (self) and returns True 801 if the possibility is correct given 802 :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules` 803 from segmentA. 804 ''' 805 for (method, isCorrect, args) in self._singlePossibilityRuleChecking[True]: 806 if not (method(possibA, *args) == isCorrect): 807 return False 808 return True 809 810 def _isCorrectConsecutivePossibility(self, possibA, possibB): 811 ''' 812 Takes in a (possibA, possibB) pair from a segmentA (self) and segmentB, 813 and returns True if the pair is correct given 814 :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules` 815 from segmentA. 816 ''' 817 for (method, isCorrect, args) in self._consecutivePossibilityRuleChecking[True]: 818 if not (method(possibA, possibB, *args) == isCorrect): 819 return False 820 return True 821 822 def _resolveOrdinarySegment(self, segmentB): 823 ''' 824 An ordinary segment is defined as a segment which needs no special resolution, where the 825 segment does not spell out a special chord, for example, a dominant seventh. 826 827 828 Finds iterators through all possibA and possibB by calling 829 :meth:`~music21.figuredBass.segment.Segment.allCorrectSinglePossibilities` 830 on self (segmentA) and segmentB, respectively. 831 Returns an iterator through (possibA, possibB) pairs for which 832 :meth:`~music21.figuredBass.segment.Segment._isCorrectConsecutivePossibility` returns True. 833 834 >>> from music21.figuredBass import segment 835 ''' 836 self._consecutivePossibilityRuleChecking = _compileRules( 837 self.consecutivePossibilityRules(self.fbRules)) 838 correctA = self.allCorrectSinglePossibilities() 839 correctB = segmentB.allCorrectSinglePossibilities() 840 correctAB = itertools.product(correctA, correctB) 841 return filter(lambda possibAB: self._isCorrectConsecutivePossibility(possibA=possibAB[0], 842 possibB=possibAB[1]), 843 correctAB) 844 845 def _resolveSpecialSegment(self, segmentB, specialResolutionMethods): 846 resolutionMethodExecutor = _compileRules(specialResolutionMethods, 3) 847 for (resolutionMethod, args) in resolutionMethodExecutor[True]: 848 iterables = [] 849 for arg in args: 850 iterables.append(itertools.repeat(arg)) 851 resolutions = map(resolutionMethod, self.allCorrectSinglePossibilities(), *iterables) 852 correctAB = zip(self.allCorrectSinglePossibilities(), resolutions) 853 correctAB = filter(lambda possibAB: possibility.pitchesWithinLimit( 854 possibA=possibAB[1], 855 maxPitch=segmentB._maxPitch), 856 correctAB) 857 if self.fbRules.applyConsecutivePossibRulesToResolution: 858 correctAB = filter(lambda possibAB: self._isCorrectConsecutivePossibility( 859 possibA=possibAB[0], 860 possibB=possibAB[1]), 861 correctAB) 862 if self.fbRules.applySinglePossibRulesToResolution: 863 segmentB._singlePossibilityRuleChecking = _compileRules( 864 segmentB.singlePossibilityRules(segmentB.fbRules)) 865 correctAB = filter(lambda possibAB: segmentB._isCorrectSinglePossibility( 866 possibA=possibAB[1]), 867 correctAB) 868 return correctAB 869 870 raise SegmentException('No standard resolution available.') 871 872 873class OverlaidSegment(Segment): 874 ''' 875 Class to allow Segments to be overlaid with non-chord notes. 876 ''' 877 878 def allSinglePossibilities(self): 879 iterables = [self.allPitchesAboveBass] * (self.numParts - 1) # Parts 1 -> n-1 880 iterables.append([pitch.Pitch(self.bassNote.pitch.nameWithOctave)]) # Part n 881 for (partNumber, partPitch) in self.fbRules._partPitchLimits: 882 iterables[partNumber - 1] = [pitch.Pitch(partPitch.nameWithOctave)] 883 return itertools.product(*iterables) 884 885 886# HELPER METHODS 887# -------------- 888def getPitches(pitchNames=('C', 'E', 'G'), 889 bassPitch: Union[str, pitch.Pitch] = 'C3', 890 maxPitch: Union[str, pitch.Pitch] = 'C8'): 891 ''' 892 Given a list of pitchNames, a bassPitch, and a maxPitch, returns a sorted list of 893 pitches between the two limits (inclusive) which correspond to items in pitchNames. 894 895 >>> from music21.figuredBass import segment 896 >>> from music21 import pitch 897 898 >>> pitches = segment.getPitches() 899 >>> print(', '.join([p.nameWithOctave for p in pitches])) 900 C3, E3, G3, C4, E4, G4, C5, E5, G5, C6, E6, G6, C7, E7, G7, C8 901 902 >>> pitches = segment.getPitches(['G', 'B', 'D', 'F'], bassPitch=pitch.Pitch('B2')) 903 >>> print(', '.join([p.nameWithOctave for p in pitches])) 904 B2, D3, F3, G3, B3, D4, F4, G4, B4, D5, F5, G5, B5, D6, F6, G6, B6, D7, F7, G7, B7 905 906 >>> pitches = segment.getPitches(['F##', 'A#', 'C#'], bassPitch=pitch.Pitch('A#3')) 907 >>> print(', '.join([p.nameWithOctave for p in pitches])) 908 A#3, C#4, F##4, A#4, C#5, F##5, A#5, C#6, F##6, A#6, C#7, F##7, A#7 909 ''' 910 if isinstance(bassPitch, str): 911 bassPitch = pitch.Pitch(bassPitch) 912 if isinstance(maxPitch, str): 913 maxPitch = pitch.Pitch(maxPitch) 914 915 iter1 = itertools.product(pitchNames, range(maxPitch.octave + 1)) 916 iter2 = map(lambda x: pitch.Pitch(x[0] + str(x[1])), iter1) 917 iter3 = itertools.filterfalse(lambda samplePitch: bassPitch > samplePitch, iter2) 918 iter4 = itertools.filterfalse(lambda samplePitch: samplePitch > maxPitch, iter3) 919 allPitches = list(iter4) 920 allPitches.sort() 921 return allPitches 922 923 924def _unpackSeventhChord(seventhChord): 925 bass = seventhChord.bass() 926 root = seventhChord.root() 927 third = seventhChord.getChordStep(3) 928 fifth = seventhChord.getChordStep(5) 929 seventh = seventhChord.getChordStep(7) 930 seventhChordInfo = [bass, root, third, fifth, seventh] 931 return seventhChordInfo 932 933 934def _unpackTriad(threePartChord): 935 bass = threePartChord.bass() 936 root = threePartChord.root() 937 third = threePartChord.getChordStep(3) 938 fifth = threePartChord.getChordStep(5) 939 threePartChordInfo = [bass, root, third, fifth] 940 return threePartChordInfo 941 942 943def _compileRules(rulesList, maxLength=4): 944 ruleChecking = collections.defaultdict(list) 945 for ruleIndex in range(len(rulesList)): 946 args = [] 947 if len(rulesList[ruleIndex]) == maxLength: 948 args = rulesList[ruleIndex][-1] 949 if maxLength == 4: 950 (shouldRunMethod, method, isCorrect) = rulesList[ruleIndex][0:3] 951 ruleChecking[shouldRunMethod].append((method, isCorrect, args)) 952 elif maxLength == 3: 953 (shouldRunMethod, method) = rulesList[ruleIndex][0:2] 954 ruleChecking[shouldRunMethod].append((method, args)) 955 956 return ruleChecking 957 958 959def printRules(rulesList, maxLength=4): 960 ''' 961 Method which can print to the console rules inputted into 962 :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`, 963 :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`, and 964 :meth:`~music21.figuredBass.segment.Segment.specialResolutionRules`. 965 For the first two methods, maxLength is 4. For the third method, maxLength is 3. 966 967 OMIT_FROM_DOCS 968 maxLength is the maximum length of a rule, a rule which includes arguments, 969 because arguments are optional. 970 ''' 971 MAX_SIZE = 30 972 for rule in rulesList: 973 if len(rule[1].__name__) >= MAX_SIZE: 974 MAX_SIZE = len(rule[1].__name__) + 2 975 976 def padMethod(m): 977 methodName = m.__name__[0:MAX_SIZE] 978 if len(methodName) < MAX_SIZE: 979 methodName += ' ' * (MAX_SIZE - len(methodName)) 980 return methodName 981 982 methodStr = 'Method:' + ' ' * (MAX_SIZE - 7) 983 if maxLength == 4: 984 print(f'Will run: {methodStr}Keep solutions which return: Arguments:') 985 elif maxLength == 3: 986 print(f'Will run: {methodStr}Arguments:') 987 988 for ruleIndex in range(len(rulesList)): 989 ruleToPrint = None 990 args = [] 991 if len(rulesList[ruleIndex]) == maxLength: 992 args = rulesList[ruleIndex][-1] 993 if not args: 994 argsString = 'None' 995 else: 996 argsString = '' 997 for itemIndex in range(len(args)): 998 argsString += str(args[itemIndex]) 999 if not itemIndex == len(args) - 1: 1000 argsString += ', ' 1001 if maxLength == 4: 1002 (shouldRunMethod, method, isCorrect) = rulesList[ruleIndex][0:3] 1003 method = padMethod(method) 1004 ruleToPrint = f'{str(shouldRunMethod):11}{method}{str(isCorrect):30}{argsString}' 1005 elif maxLength == 3: 1006 (shouldRunMethod, method) = rulesList[ruleIndex][0:2] 1007 method = padMethod(method) 1008 ruleToPrint = f'{str(shouldRunMethod):11}{method}{argsString}' 1009 print(ruleToPrint) 1010 1011 1012class SegmentException(exceptions21.Music21Exception): 1013 pass 1014 1015# ------------------------------------------------------------------------------ 1016 1017 1018class Test(unittest.TestCase): 1019 pass 1020 1021 1022if __name__ == '__main__': 1023 import music21 1024 music21.mainTest(Test) 1025 1026