1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: scale.py 4# Purpose: music21 classes for representing scales 5# 6# Authors: Christopher Ariza 7# Michael Scott Cuthbert 8# Jose Cabal-Ugaz 9# 10# Copyright: Copyright © 2009-2011 Michael Scott Cuthbert and the music21 Project 11# License: BSD, see license.txt 12# ------------------------------------------------------------------------------ 13''' 14The various Scale objects provide a bi-directional object representation 15of octave repeating and non-octave repeating scales built by network 16of :class:`~music21.interval.Interval` objects as modeled 17in :class:`~music21.intervalNetwork.IntervalNetwork`. 18 19 20The main public interface to these resources are subclasses 21of :class:`~music21.scale.ConcreteScale`, such 22as :class:`~music21.scale.MajorScale`, :class:`~music21.scale.MinorScale`, 23and :class:`~music21.scale.MelodicMinorScale`. 24 25 26More unusual scales are also available, such 27as :class:`~music21.scale.OctatonicScale`, :class:`~music21.scale.SieveScale`, 28and :class:`~music21.scale.RagMarwa`. 29 30 31All :class:`~music21.scale.ConcreteScale` subclasses provide the ability 32to get a pitches across any range, get a pitch for scale step, get a 33scale step for pitch, and, for any given pitch ascend or descend to the 34next pitch. In all cases :class:`~music21.pitch.Pitch` objects are returned. 35 36>>> sc1 = scale.MajorScale('a') 37>>> [str(p) for p in sc1.getPitches('g2', 'g4')] 38['G#2', 'A2', 'B2', 'C#3', 'D3', 'E3', 'F#3', 'G#3', 'A3', 'B3', 'C#4', 'D4', 'E4', 'F#4'] 39 40>>> sc2 = scale.MelodicMinorScale('a') 41>>> [str(p) for p in sc2.getPitches('g2', 'g4', direction='descending')] 42['G4', 'F4', 'E4', 'D4', 'C4', 'B3', 'A3', 'G3', 'F3', 'E3', 'D3', 'C3', 'B2', 'A2', 'G2'] 43 44>>> [str(p) for p in sc2.getPitches('g2', 'g4', direction='ascending')] 45['G#2', 'A2', 'B2', 'C3', 'D3', 'E3', 'F#3', 'G#3', 'A3', 'B3', 'C4', 'D4', 'E4', 'F#4'] 46''' 47__all__ = [ 48 'intervalNetwork', 'scala', 49 'DIRECTION_BI', 'DIRECTION_ASCENDING', 'DIRECTION_DESCENDING', 50 'TERMINUS_LOW', 'TERMINUS_HIGH', 51 'ScaleException', 'Scale', 52 'AbstractScale', 'AbstractDiatonicScale', 'AbstractOctatonicScale', 53 'AbstractHarmonicMinorScale', 'AbstractMelodicMinorScale', 54 'AbstractCyclicalScale', 'AbstractOctaveRepeatingScale', 55 'AbstractRagAsawari', 'AbstractRagMarwa', 'AbstractWeightedHexatonicBlues', 56 'ConcreteScale', 'DiatonicScale', 'MajorScale', 57 'MinorScale', 'DorianScale', 'PhrygianScale', 'LydianScale', 'MixolydianScale', 58 'HypodorianScale', 'HypophrygianScale', 'HypolydianScale', 'HypomixolydianScale', 59 'LocrianScale', 'HypolocrianScale', 'HypoaeolianScale', 60 'HarmonicMinorScale', 'MelodicMinorScale', 61 'OctatonicScale', 'OctaveRepeatingScale', 'CyclicalScale', 'ChromaticScale', 62 'WholeToneScale', 'SieveScale', 'ScalaScale', 'RagAsawari', 63 'RagMarwa', 'WeightedHexatonicBlues', 64] 65 66import abc 67import copy 68import unittest 69from typing import Optional, Union, List 70 71from music21.scale import intervalNetwork 72from music21.scale import scala 73# ------------------------- 74from music21 import base 75from music21 import common 76from music21 import environment 77from music21 import exceptions21 78from music21 import note 79from music21 import pitch 80from music21 import interval 81from music21 import sieve 82from music21.converter.subConverters import SubConverter 83 84_MOD = 'scale' 85environLocal = environment.Environment(_MOD) 86 87DIRECTION_BI = intervalNetwork.DIRECTION_BI 88DIRECTION_ASCENDING = intervalNetwork.DIRECTION_ASCENDING 89DIRECTION_DESCENDING = intervalNetwork.DIRECTION_DESCENDING 90 91TERMINUS_LOW = intervalNetwork.TERMINUS_LOW 92TERMINUS_HIGH = intervalNetwork.TERMINUS_HIGH 93 94 95# a dictionary mapping an abstract scale class, tonic.nameWithOctave, 96# and degree to a pitchNameWithOctave. 97_pitchDegreeCache = {} 98 99 100# ------------------------------------------------------------------------------ 101class ScaleException(exceptions21.Music21Exception): 102 pass 103 104 105class Scale(base.Music21Object): 106 ''' 107 Generic base class for all scales, both abstract and concrete. 108 ''' 109 110 def __init__(self): 111 super().__init__() 112 self.type = 'Scale' # could be mode, could be other indicator 113 114 @property 115 def name(self): 116 ''' 117 Return or construct the name of this scale 118 ''' 119 return self.type 120 121 @property 122 def isConcrete(self): 123 '''To be concrete, a Scale must have a defined tonic. 124 An abstract Scale is not Concrete, nor is a Concrete scale 125 without a defined tonic. Thus, is always false. 126 ''' 127 return False 128 129 @staticmethod 130 def extractPitchList(other, comparisonAttribute='nameWithOctave', removeDuplicates=True): 131 ''' 132 Utility function and staticmethod 133 134 Given a data format as "other" (a ConcreteScale, Chord, Stream, List of Pitches, 135 or single Pitch), 136 extract all unique Pitches using comparisonAttribute to test for them. 137 138 >>> pStrList = ['A4', 'D4', 'E4', 'F-4', 'D4', 'D5', 'A', 'D#4'] 139 >>> pList = [pitch.Pitch(p) for p in pStrList] 140 >>> nList = [note.Note(p) for p in pStrList] 141 >>> s = stream.Stream() 142 >>> for n in nList: 143 ... s.append(n) 144 145 Here we only remove the second 'D4' because the default comparison is `nameWithOctave` 146 147 >>> [str(p) for p in scale.Scale.extractPitchList(pList)] 148 ['A4', 'D4', 'E4', 'F-4', 'D5', 'A', 'D#4'] 149 150 Now we remove the F-4, D5, and A also because we are working with 151 `comparisonAttribute=pitchClass`. 152 Note that we're using a Stream as `other` now... 153 154 >>> [str(p) for p in scale.Scale.extractPitchList(s, comparisonAttribute='pitchClass')] 155 ['A4', 'D4', 'E4', 'D#4'] 156 157 Now let's get rid of all but one diatonic `D` 158 by using :meth:`~music21.pitch.Pitch.step` as our 159 `comparisonAttribute`. Note that we can just give a list of 160 strings as well, and they become :class:`~music21.pitch.Pitch` objects. Oh, we will also 161 show that `extractPitchList` works on any scale: 162 163 >>> sc = scale.Scale() 164 >>> [str(p) for p in sc.extractPitchList(pStrList, comparisonAttribute='step')] 165 ['A4', 'D4', 'E4', 'F-4'] 166 ''' 167 pre = [] 168 # if a ConcreteScale, Chord or Stream 169 if hasattr(other, 'pitches'): 170 pre = other.pitches 171 # if a list 172 elif common.isIterable(other): 173 # assume a list of pitches; possible permit conversions? 174 pre = [] 175 for p in other: 176 if hasattr(p, 'pitch'): 177 pre.append(p.pitch) 178 elif isinstance(p, pitch.Pitch): 179 pre.append(p) 180 else: 181 pre.append(pitch.Pitch(p)) 182 elif hasattr(other, 'pitch'): 183 pre = [other.pitch] # get pitch attribute 184 185 if removeDuplicates is False: 186 return pre 187 188 uniquePitches = {} 189 190 post = [] 191 192 for p in pre: 193 hashValue = getattr(p, comparisonAttribute) 194 if hashValue not in uniquePitches: 195 uniquePitches[hashValue] = True 196 post.append(p) 197 198 return post 199 200# ------------------------------------------------------------------------------ 201 202 203class AbstractScale(Scale): 204 ''' 205 An abstract scale is specific scale formation, but does not have a 206 defined pitch collection or pitch reference. For example, all Major 207 scales can be represented by an AbstractScale; a ConcreteScale, 208 however, is a specific Major Scale, such as G Major. 209 210 These classes provide an interface to, and create and manipulate, 211 the stored :class:`~music21.intervalNetwork.IntervalNetwork` 212 object. Thus, they are rarely created or manipulated directly by 213 most users. 214 215 The AbstractScale additionally stores an `_alteredDegrees` dictionary. 216 Subclasses can define altered nodes in AbstractScale that are passed 217 to the :class:`~music21.intervalNetwork.IntervalNetwork`. 218 219 # TODO: make a subclass of IntervalNetwork and remove the ._net aspect. 220 ''' 221 222 def __init__(self): 223 super().__init__() 224 # store interval network within abstract scale 225 self._net = None 226 # in most cases tonic/final of scale is step one, but not always 227 self.tonicDegree = 1 # step of tonic 228 229 # declare if this scale is octave duplicating 230 # can be used as to optimize pitch gathering 231 self.octaveDuplicating = True 232 233 # passed to interval network 234 self.deterministic = True 235 # store parameter for interval network-based node modifications 236 # entries are in the form: 237 # step: {'direction':DIRECTION_BI, 'interval':Interval} 238 self._alteredDegrees = {} 239 240 def __eq__(self, other): 241 ''' 242 Two abstract scales are the same if they have the same class and the 243 same tonicDegree and the same intervalNetwork. 244 245 >>> as1 = scale.AbstractScale() 246 >>> as2 = scale.AbstractScale() 247 >>> as1 == as2 248 True 249 >>> as1.isConcrete 250 False 251 ''' 252 # have to test each so as not to confuse with a subclass 253 if (isinstance(other, self.__class__) 254 and isinstance(self, other.__class__) 255 and self.tonicDegree == other.tonicDegree 256 and self._net == other._net): 257 return True 258 else: 259 return False 260 261 @abc.abstractmethod 262 def buildNetwork(self): 263 ''' 264 Calling the buildNetwork, with or without parameters, 265 is main job of the AbstractScale class. This needs to be subclassed by a derived class 266 ''' 267 raise NotImplementedError 268 269 def buildNetworkFromPitches(self, pitchList): 270 ''' 271 Builds the network (list of motions) for an abstract scale 272 from a list of pitch.Pitch objects. If 273 the concluding note (usually the "octave") is not given, 274 then it'll be created automatically. 275 276 Here we treat the augmented triad as a scale: 277 278 >>> p1 = pitch.Pitch('C4') 279 >>> p2 = pitch.Pitch('E4') 280 >>> p3 = pitch.Pitch('G#4') 281 >>> abstractScale = scale.AbstractScale() 282 >>> abstractScale.buildNetworkFromPitches([p1, p2, p3]) 283 >>> abstractScale.octaveDuplicating 284 True 285 >>> abstractScale._net 286 <music21.scale.intervalNetwork.IntervalNetwork object at 0x...> 287 288 Now see it return a new "scale" of the augmentedTriad on D5 289 290 >>> abstractScale._net.realizePitch('D5') 291 [<music21.pitch.Pitch D5>, <music21.pitch.Pitch F#5>, 292 <music21.pitch.Pitch A#5>, <music21.pitch.Pitch D6>] 293 294 Also possible with implicit octaves: 295 296 >>> abstract_scale = scale.AbstractScale() 297 >>> abstract_scale.buildNetworkFromPitches(['C', 'F']) 298 >>> abstract_scale.octaveDuplicating 299 True 300 >>> abstract_scale._net.realizePitch('G') 301 [<music21.pitch.Pitch G4>, <music21.pitch.Pitch C5>, <music21.pitch.Pitch G5>] 302 ''' 303 pitchListReal = [] 304 for p in pitchList: 305 if isinstance(p, str): 306 pitchListReal.append(pitch.Pitch(p)) 307 elif isinstance(p, note.Note): 308 pitchListReal.append(p.pitch) 309 else: # assume this is a pitch object 310 pitchListReal.append(p) 311 pitchList = pitchListReal 312 313 self.fixDefaultOctaveForPitchList(pitchList) 314 315 if not common.isListLike(pitchList) or not pitchList: 316 raise ScaleException(f'Cannot build a network from this pitch list: {pitchList}') 317 intervalList = [] 318 for i in range(len(pitchList) - 1): 319 intervalList.append(interval.notesToInterval(pitchList[i], pitchList[i + 1])) 320 if pitchList[-1].name == pitchList[0].name: # the completion of the scale has been given. 321 # print('hi %s ' % pitchList) 322 # this scale is only octave duplicating if the top note is exactly 323 # 1 octave above the bottom; if it spans more than one octave, 324 # all notes must be identical in each octave 325 # if abs(pitchList[-1].ps - pitchList[0].ps) == 12: 326 span = interval.notesToInterval(pitchList[0], pitchList[-1]) 327 # environLocal.printDebug(['got span', span, span.name]) 328 if span.name == 'P8': 329 self.octaveDuplicating = True 330 else: 331 self.octaveDuplicating = False 332 else: 333 p = copy.deepcopy(pitchList[0]) 334 if p.octave is None: 335 p.octave = p.implicitOctave 336 if pitchList[-1] > pitchList[0]: # ascending 337 while p.ps < pitchList[-1].ps: 338 p.octave += 1 339 else: 340 while p.ps > pitchList[-1].ps: 341 p.octave += -1 342 343 intervalList.append(interval.notesToInterval(pitchList[-1], p)) 344 span = interval.notesToInterval(pitchList[0], p) 345 # environLocal.printDebug(['got span', span, span.name]) 346 if span.name == 'P8': 347 self.octaveDuplicating = True 348 else: 349 self.octaveDuplicating = False 350 351 # if abs(p.ps - pitchList[0].ps) == 12: 352 # self.octaveDuplicating == True 353 # else: 354 # self.octaveDuplicating == False 355 356 # environLocal.printDebug(['intervalList', intervalList, 357 # 'self.octaveDuplicating', self.octaveDuplicating]) 358 self._net = intervalNetwork.IntervalNetwork(intervalList, 359 octaveDuplicating=self.octaveDuplicating) 360 361 @staticmethod 362 def fixDefaultOctaveForPitchList(pitchList): 363 ''' 364 Suppose you have a set of octaveless Pitches that you use to make a scale. 365 366 Something like: 367 368 >>> pitchListStrs = 'a b c d e f g a'.split() 369 >>> pitchList = [pitch.Pitch(p) for p in pitchListStrs] 370 371 Here's the problem, between `pitchList[1]` and `pitchList[2]` the `.implicitOctave` 372 stays the same, so the `.ps` drops: 373 374 >>> (pitchList[1].implicitOctave, pitchList[2].implicitOctave) 375 (4, 4) 376 >>> (pitchList[1].ps, pitchList[2].ps) 377 (71.0, 60.0) 378 379 Hence this helper staticmethod that makes it so that for octaveless pitches ONLY, each 380 one has a .ps above the previous: 381 382 >>> pl2 = scale.AbstractScale.fixDefaultOctaveForPitchList(pitchList) 383 >>> (pl2[1].implicitOctave, pl2[2].implicitOctave, pl2[3].implicitOctave) 384 (4, 5, 5) 385 >>> (pl2[1].ps, pl2[2].ps) 386 (71.0, 72.0) 387 388 Note that the list is modified inPlace: 389 390 >>> pitchList is pl2 391 True 392 >>> pitchList[2] is pl2[2] 393 True 394 ''' 395 # fix defaultOctave for pitchList 396 lastPs = 0 397 lastOctave = pitchList[0].implicitOctave 398 for p in pitchList: 399 if p.octave is None: 400 if lastPs > p.ps: 401 p.defaultOctave = lastOctave 402 while lastPs > p.ps: 403 lastOctave += 1 404 p.defaultOctave = lastOctave 405 406 lastPs = p.ps 407 lastOctave = p.implicitOctave 408 409 return pitchList 410 411 def getDegreeMaxUnique(self): 412 ''' 413 Return the maximum number of scale steps, or the number to use as a 414 modulus. 415 ''' 416 # access from property 417 return self._net.degreeMaxUnique 418 419 # def reverse(self): 420 # '''Reverse all intervals in this scale. 421 # ''' 422 # pass 423 424 # expose interface from network. these methods must be called (and not 425 # ._net directly because they can pass the alteredDegrees dictionary 426 427 def getRealization(self, 428 pitchObj, 429 stepOfPitch, 430 minPitch=None, 431 maxPitch=None, 432 direction=DIRECTION_ASCENDING, 433 reverse=False): 434 ''' 435 Realize the abstract scale as a list of pitch objects, 436 given a pitch object, the step of that pitch object, 437 and a min and max pitch. 438 ''' 439 if self._net is None: 440 raise ScaleException('no IntervalNetwork is defined by this "scale".') 441 442 post = self._net.realizePitch(pitchObj, 443 stepOfPitch, 444 minPitch=minPitch, 445 maxPitch=maxPitch, 446 alteredDegrees=self._alteredDegrees, 447 direction=direction, 448 reverse=reverse) 449 # here, we copy the list of pitches so as not to allow editing of 450 # cached pitch values later 451 return copy.deepcopy(post) 452 453 def getIntervals(self, 454 stepOfPitch=None, 455 minPitch=None, 456 maxPitch=None, 457 direction=DIRECTION_ASCENDING, 458 reverse=False): 459 ''' 460 Realize the abstract scale as a list of pitch 461 objects, given a pitch object, the step of 462 that pitch object, and a min and max pitch. 463 ''' 464 if self._net is None: 465 raise ScaleException('no network is defined.') 466 467 post = self._net.realizeIntervals(stepOfPitch, 468 minPitch=minPitch, 469 maxPitch=maxPitch, 470 alteredDegrees=self._alteredDegrees, 471 direction=direction, 472 reverse=reverse) 473 # here, we copy the list of pitches so as not to allow editing of 474 # cached pitch values later 475 return post 476 477 def getPitchFromNodeDegree(self, 478 pitchReference, 479 nodeName, 480 nodeDegreeTarget, 481 direction=DIRECTION_ASCENDING, 482 minPitch=None, 483 maxPitch=None, 484 equateTermini=True): 485 ''' 486 Get a pitch for desired scale degree. 487 ''' 488 post = self._net.getPitchFromNodeDegree( 489 pitchReference=pitchReference, # pitch defined here 490 nodeName=nodeName, # defined in abstract class 491 nodeDegreeTarget=nodeDegreeTarget, # target looking for 492 direction=direction, 493 minPitch=minPitch, 494 maxPitch=maxPitch, 495 alteredDegrees=self._alteredDegrees, 496 equateTermini=equateTermini 497 ) 498 return copy.deepcopy(post) 499 500 def realizePitchByDegree(self, 501 pitchReference, 502 nodeId, 503 nodeDegreeTargets, 504 direction=DIRECTION_ASCENDING, 505 minPitch=None, 506 maxPitch=None): 507 ''' 508 Given one or more scale degrees, return a list of 509 all matches over the entire range. 510 511 See :meth:`~music21.intervalNetwork.IntervalNetwork.realizePitchByDegree`. 512 in `intervalNetwork.IntervalNetwork`. 513 514 Create an abstract pentatonic scale: 515 516 >>> pitchList = ['C#4', 'D#4', 'F#4', 'G#4', 'A#4'] 517 >>> abstractScale = scale.AbstractScale() 518 >>> abstractScale.buildNetworkFromPitches([pitch.Pitch(p) for p in pitchList]) 519 ''' 520 # TODO: rely here on intervalNetwork for caching 521 post = self._net.realizePitchByDegree( 522 pitchReference=pitchReference, # pitch defined here 523 nodeId=nodeId, # defined in abstract class 524 nodeDegreeTargets=nodeDegreeTargets, # target looking for 525 direction=direction, 526 minPitch=minPitch, 527 maxPitch=maxPitch, 528 alteredDegrees=self._alteredDegrees) 529 return copy.deepcopy(post) 530 531 def getRelativeNodeDegree(self, 532 pitchReference, 533 nodeName, 534 pitchTarget, 535 comparisonAttribute='pitchClass', 536 direction=DIRECTION_ASCENDING): 537 ''' 538 Expose functionality from 539 :class:`~music21.intervalNetwork.IntervalNetwork`, passing on the 540 stored alteredDegrees dictionary. 541 ''' 542 post = self._net.getRelativeNodeDegree( 543 pitchReference=pitchReference, 544 nodeName=nodeName, 545 pitchTarget=pitchTarget, 546 comparisonAttribute=comparisonAttribute, 547 direction=direction, 548 alteredDegrees=self._alteredDegrees 549 ) 550 return copy.deepcopy(post) 551 552 def nextPitch(self, 553 pitchReference, 554 nodeName, 555 pitchOrigin, 556 direction=DIRECTION_ASCENDING, 557 stepSize=1, 558 getNeighbor: Union[str, bool] = True): 559 ''' 560 Expose functionality from :class:`~music21.intervalNetwork.IntervalNetwork`, 561 passing on the stored alteredDegrees dictionary. 562 ''' 563 post = self._net.nextPitch(pitchReference=pitchReference, 564 nodeName=nodeName, 565 pitchOrigin=pitchOrigin, 566 direction=direction, 567 stepSize=stepSize, 568 alteredDegrees=self._alteredDegrees, 569 getNeighbor=getNeighbor 570 ) 571 return copy.deepcopy(post) 572 573 def getNewTonicPitch(self, 574 pitchReference, 575 nodeName, 576 direction=DIRECTION_ASCENDING, 577 minPitch=None, 578 maxPitch=None): 579 ''' 580 Define a pitch target and a node. 581 ''' 582 post = self._net.getPitchFromNodeDegree( 583 pitchReference=pitchReference, 584 nodeName=nodeName, 585 nodeDegreeTarget=1, # get the pitch of the tonic 586 direction=direction, 587 minPitch=minPitch, 588 maxPitch=maxPitch, 589 alteredDegrees=self._alteredDegrees 590 ) 591 return copy.deepcopy(post) 592 593 # -------------------------------------------------------------------------- 594 595 def getScalaData(self, direction=DIRECTION_ASCENDING): 596 ''' 597 Get the interval sequence as a :class:~music21.scala.ScalaData object 598 for a particular scale: 599 ''' 600 # get one octave of intervals 601 intervals = self.getIntervals(direction=direction) 602 ss = scala.ScalaData() 603 ss.setIntervalSequence(intervals) 604 ss.description = repr(self) 605 return ss 606 607 def write(self, fmt=None, fp=None, direction=DIRECTION_ASCENDING, **keywords): 608 ''' 609 Write the scale in a format. Here, prepare scala format if requested. 610 ''' 611 if fmt is not None: 612 fileFormat, ext = common.findFormat(fmt) 613 if fp is None: 614 fpLocal = environLocal.getTempFile(ext) 615 else: 616 fpLocal = fp 617 618 if fileFormat == 'scala': 619 ss = self.getScalaData(direction=direction) 620 sf = scala.ScalaFile(ss) # pass storage to the file 621 sf.open(fpLocal, 'w') 622 sf.write() 623 sf.close() 624 return fpLocal 625 return Scale.write(self, fmt=fmt, fp=fp, **keywords) 626 627 def show(self, fmt=None, app=None, direction=DIRECTION_ASCENDING, **keywords): 628 ''' 629 Show the scale in a format. Here, prepare scala format if requested. 630 ''' 631 if fmt is not None: 632 fileFormat, unused_ext = common.findFormat(fmt) 633 if fileFormat == 'scala': 634 returnedFilePath = self.write(fileFormat, direction=direction) 635 SubConverter().launch(returnedFilePath, fmt=fileFormat, app=app) 636 return 637 Scale.show(self, fmt=fmt, app=app, **keywords) 638 639# ------------------------------------------------------------------------------ 640# abstract subclasses 641 642 643class AbstractDiatonicScale(AbstractScale): 644 ''' 645 An abstract representation of a Diatonic scale w/ or without mode. 646 647 >>> as1 = scale.AbstractDiatonicScale('major') 648 >>> as1.type 649 'Abstract diatonic' 650 >>> as1.mode 651 'major' 652 >>> as1.octaveDuplicating 653 True 654 655 ''' 656 def __init__(self, mode: Optional[str] = None): 657 super().__init__() 658 self.mode = mode 659 self.type = 'Abstract diatonic' 660 self.tonicDegree = None # step of tonic 661 self.dominantDegree = None # step of dominant 662 # all diatonic scales are octave duplicating 663 self.octaveDuplicating = True 664 self.relativeMinorDegree: int = -1 665 self.relativeMajorDegree: int = -1 666 self.buildNetwork(mode=mode) 667 668 def __eq__(self, other): 669 ''' 670 Two AbstractDiatonicScale objects are equal if 671 their tonicDegrees, their dominantDegrees, and 672 their networks are the same. 673 674 >>> as1 = scale.AbstractDiatonicScale('major') 675 >>> as2 = scale.AbstractDiatonicScale('lydian') 676 >>> as1 == as2 677 False 678 679 Note that their modes do not need to be the same. 680 For instance for the case of major and Ionian which have 681 the same networks: 682 683 >>> as3 = scale.AbstractDiatonicScale('ionian') 684 >>> (as1.mode, as3.mode) 685 ('major', 'ionian') 686 >>> as1 == as3 687 True 688 ''' 689 if not isinstance(other, self.__class__): 690 return False 691 if not isinstance(self, other.__class__): 692 return False 693 694 # have to test each so as not to confuse with a subclass 695 if (self.type == other.type 696 and self.tonicDegree == other.tonicDegree 697 and self.dominantDegree == other.dominantDegree 698 and self._net == other._net): 699 return True 700 else: 701 return False 702 703 def buildNetwork(self, mode=None): 704 ''' 705 Given sub-class dependent parameters, build and assign the IntervalNetwork. 706 707 >>> sc = scale.AbstractDiatonicScale() 708 >>> sc.buildNetwork('Lydian') # N.B. case insensitive 709 >>> [str(p) for p in sc.getRealization('f4', 1, 'f2', 'f6')] 710 ['F2', 'G2', 'A2', 'B2', 'C3', 'D3', 'E3', 711 'F3', 'G3', 'A3', 'B3', 'C4', 'D4', 'E4', 712 'F4', 'G4', 'A4', 'B4', 'C5', 'D5', 'E5', 713 'F5', 'G5', 'A5', 'B5', 'C6', 'D6', 'E6', 'F6'] 714 715 Unknown modes raise an exception: 716 717 >>> sc.buildNetwork('blues-like') 718 Traceback (most recent call last): 719 music21.scale.ScaleException: Cannot create a scale of the following mode: 'blues-like' 720 721 Changed in v.6 -- case insensitive modes 722 ''' 723 # reference: http://cnx.org/content/m11633/latest/ 724 # most diatonic scales will start with this collection 725 srcList = ('M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2') 726 self.tonicDegree = 1 727 self.dominantDegree = 5 728 729 if isinstance(mode, str): 730 mode = mode.lower() 731 732 if mode in (None, 'major', 'ionian'): # c to C 733 intervalList = srcList 734 self.relativeMajorDegree = 1 735 self.relativeMinorDegree = 6 736 elif mode == 'dorian': 737 intervalList = srcList[1:] + srcList[:1] # d to d 738 self.relativeMajorDegree = 7 739 self.relativeMinorDegree = 5 740 elif mode == 'phrygian': 741 intervalList = srcList[2:] + srcList[:2] # e to e 742 self.relativeMajorDegree = 6 743 self.relativeMinorDegree = 4 744 elif mode == 'lydian': 745 intervalList = srcList[3:] + srcList[:3] # f to f 746 self.relativeMajorDegree = 5 747 self.relativeMinorDegree = 3 748 elif mode == 'mixolydian': 749 intervalList = srcList[4:] + srcList[:4] # g to g 750 self.relativeMajorDegree = 4 751 self.relativeMinorDegree = 2 752 elif mode in ('aeolian', 'minor'): 753 intervalList = srcList[5:] + srcList[:5] # a to A 754 self.relativeMajorDegree = 3 755 self.relativeMinorDegree = 1 756 elif mode == 'locrian': 757 intervalList = srcList[6:] + srcList[:6] # b to B 758 self.relativeMajorDegree = 2 759 self.relativeMinorDegree = 7 760 elif mode == 'hypodorian': 761 intervalList = srcList[5:] + srcList[:5] # a to a 762 self.tonicDegree = 4 763 self.dominantDegree = 6 764 self.relativeMajorDegree = 3 765 self.relativeMinorDegree = 1 766 elif mode == 'hypophrygian': 767 intervalList = srcList[6:] + srcList[:6] # b to b 768 self.tonicDegree = 4 769 self.dominantDegree = 7 770 self.relativeMajorDegree = 2 771 self.relativeMinorDegree = 7 772 elif mode == 'hypolydian': # c to c 773 intervalList = srcList 774 self.tonicDegree = 4 775 self.dominantDegree = 6 776 self.relativeMajorDegree = 1 777 self.relativeMinorDegree = 6 778 elif mode == 'hypomixolydian': 779 intervalList = srcList[1:] + srcList[:1] # d to d 780 self.tonicDegree = 4 781 self.dominantDegree = 7 782 self.relativeMajorDegree = 7 783 self.relativeMinorDegree = 5 784 elif mode == 'hypoaeolian': 785 intervalList = srcList[2:] + srcList[:2] # e to e 786 self.tonicDegree = 4 787 self.dominantDegree = 6 788 self.relativeMajorDegree = 6 789 self.relativeMinorDegree = 4 790 elif mode == 'hypolocrian': 791 intervalList = srcList[3:] + srcList[:3] # f to f 792 self.tonicDegree = 4 793 self.dominantDegree = 6 794 self.relativeMajorDegree = 5 795 self.relativeMinorDegree = 3 796 else: 797 raise ScaleException(f'Cannot create a scale of the following mode: {mode!r}') 798 799 self._net = intervalNetwork.IntervalNetwork( 800 intervalList, 801 octaveDuplicating=self.octaveDuplicating, 802 pitchSimplification=None) 803 804 805class AbstractOctatonicScale(AbstractScale): 806 ''' 807 Abstract scale representing the two octatonic scales. 808 ''' 809 810 def __init__(self, mode=None): 811 super().__init__() 812 self.type = 'Abstract Octatonic' 813 # all octatonic scales are octave duplicating 814 self.octaveDuplicating = True 815 # here, accept None 816 self.buildNetwork(mode=mode) 817 818 def buildNetwork(self, mode=None): 819 ''' 820 Given sub-class dependent parameters, build and assign the IntervalNetwork. 821 822 823 >>> sc = scale.AbstractDiatonicScale() 824 >>> sc.buildNetwork('lydian') 825 >>> [str(p) for p in sc.getRealization('f4', 1, 'f2', 'f6')] 826 ['F2', 'G2', 'A2', 'B2', 'C3', 'D3', 'E3', 827 'F3', 'G3', 'A3', 'B3', 'C4', 'D4', 'E4', 828 'F4', 'G4', 'A4', 'B4', 'C5', 'D5', 'E5', 829 'F5', 'G5', 'A5', 'B5', 'C6', 'D6', 'E6', 'F6'] 830 ''' 831 srcList = ('M2', 'm2', 'M2', 'm2', 'M2', 'm2', 'M2', 'm2') 832 if mode in (None, 2, 'M2'): 833 intervalList = srcList # start with M2 834 self.tonicDegree = 1 835 elif mode in (1, 'm2'): 836 intervalList = srcList[1:] + srcList[:1] # start with m2 837 self.tonicDegree = 1 838 else: 839 raise ScaleException(f'cannot create a scale of the following mode: {mode}') 840 self._net = intervalNetwork.IntervalNetwork(intervalList, 841 octaveDuplicating=self.octaveDuplicating, 842 pitchSimplification='maxAccidental') 843 # might also set weights for tonic and dominant here 844 845 846class AbstractHarmonicMinorScale(AbstractScale): 847 ''' 848 A true bi-directional scale that with the augmented 849 second to a leading tone. 850 851 This is the only scale to use the "_alteredDegrees" property. 852 ''' 853 854 def __init__(self, mode=None): 855 super().__init__() 856 self.type = 'Abstract Harmonic Minor' 857 self.octaveDuplicating = True 858 self.dominantDegree: int = -1 859 self.buildNetwork() 860 861 def buildNetwork(self): 862 intervalList = ['M2', 'm2', 'M2', 'M2', 'm2', 'M2', 'M2'] # a to A 863 self.tonicDegree = 1 864 self.dominantDegree = 5 865 self._net = intervalNetwork.IntervalNetwork(intervalList, 866 octaveDuplicating=self.octaveDuplicating, 867 pitchSimplification=None) 868 869 # raise the seventh in all directions 870 # 7 here is scale step/degree, not node id 871 self._alteredDegrees[7] = { 872 'direction': intervalNetwork.DIRECTION_BI, 873 'interval': interval.Interval('a1') 874 } 875 876 877class AbstractMelodicMinorScale(AbstractScale): 878 ''' 879 A directional scale. 880 ''' 881 882 def __init__(self, mode=None): 883 super().__init__() 884 self.type = 'Abstract Melodic Minor' 885 self.octaveDuplicating = True 886 self.dominantDegree: int = -1 887 self.buildNetwork() 888 889 def buildNetwork(self): 890 self.tonicDegree = 1 891 self.dominantDegree = 5 892 self._net = intervalNetwork.IntervalNetwork( 893 octaveDuplicating=self.octaveDuplicating, 894 pitchSimplification=None) 895 self._net.fillMelodicMinor() 896 897 898class AbstractCyclicalScale(AbstractScale): 899 ''' 900 A scale of any size built with an interval list of any form. 901 The resulting scale may be non octave repeating. 902 ''' 903 904 def __init__(self, mode=None): 905 super().__init__() 906 self.type = 'Abstract Cyclical' 907 self.octaveDuplicating = False 908 self.buildNetwork(mode=mode) 909 # cannot assume that abstract cyclical scales are octave duplicating 910 # until we have the intervals in use 911 912 def buildNetwork(self, mode): 913 ''' 914 Here, mode is the list of intervals. 915 ''' 916 if not common.isListLike(mode): 917 mode = [mode] # place in list 918 self.tonicDegree = 1 919 self._net = intervalNetwork.IntervalNetwork(mode, 920 octaveDuplicating=self.octaveDuplicating) 921 922 923class AbstractOctaveRepeatingScale(AbstractScale): 924 ''' 925 A scale of any size built with an interval list 926 that assumes octave completion. An additional 927 interval to complete the octave will be added 928 to the provided intervals. This does not guarantee 929 that the octave will be repeated in one octave, 930 only the next octave above the last interval will 931 be provided. 932 ''' 933 934 def __init__(self, mode=None): 935 super().__init__() 936 self.type = 'Abstract Octave Repeating' 937 938 if mode is None: 939 # supply a default 940 mode = ['P8'] 941 self.buildNetwork(mode=mode) 942 943 # by definition, these are forced to be octave duplicating 944 # though, do to some intervals, duplication may not happen every oct 945 self.octaveDuplicating = True 946 947 def buildNetwork(self, mode): 948 ''' 949 Here, mode is the list of intervals. 950 ''' 951 if not common.isListLike(mode): 952 mode = [mode] # place in list 953 # get the interval to complete the octave 954 955 intervalSum = interval.add(mode) 956 iComplement = intervalSum.complement 957 if iComplement is not None: 958 mode.append(iComplement) 959 960 self.tonicDegree = 1 961 self._net = intervalNetwork.IntervalNetwork(mode, 962 octaveDuplicating=self.octaveDuplicating) 963 964 965class AbstractRagAsawari(AbstractScale): 966 ''' 967 A pseudo raga-scale. 968 ''' 969 def __init__(self): 970 super().__init__() 971 self.type = 'Abstract Rag Asawari' 972 self.octaveDuplicating = True 973 self.dominantDegree: int = -1 974 self.buildNetwork() 975 976 def buildNetwork(self): 977 self.tonicDegree = 1 978 self.dominantDegree = 5 979 nodes = ({'id': 'terminusLow', 'degree': 1}, # c 980 {'id': 0, 'degree': 2}, # d 981 {'id': 1, 'degree': 4}, # f 982 {'id': 2, 'degree': 5}, # g 983 {'id': 3, 'degree': 6}, # a- 984 {'id': 'terminusHigh', 'degree': 8}, # c 985 986 {'id': 4, 'degree': 7}, # b- 987 {'id': 5, 'degree': 6}, # a- 988 {'id': 6, 'degree': 5}, # g 989 {'id': 7, 'degree': 4}, # f 990 {'id': 8, 'degree': 3}, # e- 991 {'id': 9, 'degree': 2}, # d 992 ) 993 edges = ( 994 # ascending 995 {'interval': 'M2', 996 'connections': ( 997 [TERMINUS_LOW, 0, DIRECTION_ASCENDING], # c to d 998 )}, 999 {'interval': 'm3', 1000 'connections': ( 1001 [0, 1, DIRECTION_ASCENDING], # d to f 1002 )}, 1003 {'interval': 'M2', 1004 'connections': ( 1005 [1, 2, DIRECTION_ASCENDING], # f to g 1006 )}, 1007 {'interval': 'm2', 1008 'connections': ( 1009 [2, 3, DIRECTION_ASCENDING], # g to a- 1010 )}, 1011 {'interval': 'M3', 1012 'connections': ( 1013 [3, TERMINUS_HIGH, DIRECTION_ASCENDING], # a- to c 1014 )}, 1015 # descending 1016 {'interval': 'M2', 1017 'connections': ( 1018 [TERMINUS_HIGH, 4, DIRECTION_DESCENDING], # c to b- 1019 )}, 1020 {'interval': 'M2', 1021 'connections': ( 1022 [4, 5, DIRECTION_DESCENDING], # b- to a- 1023 )}, 1024 {'interval': 'm2', 1025 'connections': ( 1026 [5, 6, DIRECTION_DESCENDING], # a- to g 1027 )}, 1028 {'interval': 'M2', 1029 'connections': ( 1030 [6, 7, DIRECTION_DESCENDING], # g to f 1031 )}, 1032 {'interval': 'M2', 1033 'connections': ( 1034 [7, 8, DIRECTION_DESCENDING], # f to e- 1035 )}, 1036 {'interval': 'm2', 1037 'connections': ( 1038 [8, 9, DIRECTION_DESCENDING], # e- to d 1039 )}, 1040 {'interval': 'M2', 1041 'connections': ( 1042 [9, TERMINUS_LOW, DIRECTION_DESCENDING], # d to c 1043 )}, 1044 ) 1045 1046 self._net = intervalNetwork.IntervalNetwork( 1047 octaveDuplicating=self.octaveDuplicating, 1048 pitchSimplification='mostCommon') 1049 # using representation stored in interval network 1050 self._net.fillArbitrary(nodes, edges) 1051 1052 1053class AbstractRagMarwa(AbstractScale): 1054 ''' 1055 A pseudo raga-scale. 1056 ''' 1057 def __init__(self): 1058 super().__init__() 1059 self.type = 'Abstract Rag Marwa' 1060 self.octaveDuplicating = True 1061 self.dominantDegree: int = -1 1062 self.buildNetwork() 1063 1064 def buildNetwork(self): 1065 self.tonicDegree = 1 1066 self.dominantDegree = 5 1067 nodes = ({'id': 'terminusLow', 'degree': 1}, # c 1068 {'id': 0, 'degree': 2}, # d- 1069 {'id': 1, 'degree': 3}, # e 1070 {'id': 2, 'degree': 4}, # f# 1071 {'id': 3, 'degree': 5}, # a 1072 {'id': 4, 'degree': 6}, # b 1073 {'id': 5, 'degree': 7}, # a (could use id 3 again?) 1074 {'id': 'terminusHigh', 'degree': 8}, # c 1075 1076 {'id': 6, 'degree': 7}, # d- (above terminus) 1077 {'id': 7, 'degree': 6}, # b 1078 {'id': 8, 'degree': 5}, # a 1079 {'id': 9, 'degree': 4}, # f# 1080 {'id': 10, 'degree': 3}, # e 1081 {'id': 11, 'degree': 2}, # d- 1082 ) 1083 edges = ( 1084 # ascending 1085 {'interval': 'm2', 1086 'connections': ( 1087 [TERMINUS_LOW, 0, DIRECTION_ASCENDING], # c to d- 1088 )}, 1089 {'interval': 'A2', 1090 'connections': ( 1091 [0, 1, DIRECTION_ASCENDING], # d- to e 1092 )}, 1093 {'interval': 'M2', 1094 'connections': ( 1095 [1, 2, DIRECTION_ASCENDING], # e to f# 1096 )}, 1097 {'interval': 'm3', 1098 'connections': ( 1099 [2, 3, DIRECTION_ASCENDING], # f# to a 1100 )}, 1101 {'interval': 'M2', 1102 'connections': ( 1103 [3, 4, DIRECTION_ASCENDING], # a to b 1104 )}, 1105 {'interval': '-M2', 1106 'connections': ( 1107 [4, 5, DIRECTION_ASCENDING], # b to a (downward) 1108 )}, 1109 {'interval': 'm3', 1110 'connections': ( 1111 [5, TERMINUS_HIGH, DIRECTION_ASCENDING], # a to c 1112 )}, 1113 1114 # descending 1115 {'interval': '-m2', 1116 'connections': ( 1117 [TERMINUS_HIGH, 6, DIRECTION_DESCENDING], # c to d- (up) 1118 )}, 1119 {'interval': 'd3', 1120 'connections': ( 1121 [6, 7, DIRECTION_DESCENDING], # d- to b 1122 )}, 1123 {'interval': 'M2', 1124 'connections': ( 1125 [7, 8, DIRECTION_DESCENDING], # b to a 1126 )}, 1127 {'interval': 'm3', 1128 'connections': ( 1129 [8, 9, DIRECTION_DESCENDING], # a to f# 1130 )}, 1131 {'interval': 'M2', 1132 'connections': ( 1133 [9, 10, DIRECTION_DESCENDING], # f# to e 1134 )}, 1135 {'interval': 'A2', 1136 'connections': ( 1137 [10, 11, DIRECTION_DESCENDING], # e to d- 1138 )}, 1139 {'interval': 'm2', 1140 'connections': ( 1141 [11, TERMINUS_LOW, DIRECTION_DESCENDING], # d- to c 1142 )}, 1143 ) 1144 1145 self._net = intervalNetwork.IntervalNetwork( 1146 octaveDuplicating=self.octaveDuplicating, 1147 ) 1148 # using representation stored in interval network 1149 self._net.fillArbitrary(nodes, edges) 1150 1151 1152class AbstractWeightedHexatonicBlues(AbstractScale): 1153 ''' 1154 A dynamic, probabilistic mixture of minor pentatonic and a hexatonic blues scale 1155 ''' 1156 1157 def __init__(self): 1158 super().__init__() 1159 self.type = 'Abstract Weighted Hexatonic Blues' 1160 # probably not, as all may not have some pitches in each octave 1161 self.octaveDuplicating = True 1162 self.deterministic = False 1163 self.dominantDegree = 5 1164 self.buildNetwork() 1165 1166 def buildNetwork(self): 1167 self.tonicDegree = 1 1168 self.dominantDegree = 5 1169 nodes = ({'id': 'terminusLow', 'degree': 1}, # c 1170 {'id': 0, 'degree': 2}, # e- 1171 {'id': 1, 'degree': 3}, # f 1172 {'id': 2, 'degree': 4}, # f# 1173 {'id': 3, 'degree': 5}, # g 1174 {'id': 4, 'degree': 6}, # b- 1175 {'id': 'terminusHigh', 'degree': 7}, # c 1176 ) 1177 edges = ( 1178 # all bidirectional 1179 {'interval': 'm3', 1180 'connections': ( 1181 [TERMINUS_LOW, 0, DIRECTION_BI], # c to e- 1182 )}, 1183 {'interval': 'M2', 1184 'connections': ( 1185 [0, 1, DIRECTION_BI], # e- to f 1186 )}, 1187 {'interval': 'M2', 1188 'connections': ( 1189 [1, 3, DIRECTION_BI], # f to g 1190 )}, 1191 {'interval': 'a1', 1192 'connections': ( 1193 [1, 2, DIRECTION_BI], # f to f# 1194 )}, 1195 {'interval': 'm2', 1196 'connections': ( 1197 [2, 3, DIRECTION_BI], # f# to g 1198 )}, 1199 {'interval': 'm3', 1200 'connections': ( 1201 [3, 4, DIRECTION_BI], # g to b- 1202 )}, 1203 {'interval': 'M2', 1204 'connections': ( 1205 [4, TERMINUS_HIGH, DIRECTION_BI], # b- to c 1206 )}, 1207 ) 1208 1209 self._net = intervalNetwork.IntervalNetwork( 1210 octaveDuplicating=self.octaveDuplicating, 1211 deterministic=self.deterministic,) 1212 # using representation stored in interval network 1213 self._net.fillArbitrary(nodes, edges) 1214 1215 1216# ------------------------------------------------------------------------------ 1217class ConcreteScale(Scale): 1218 ''' 1219 A concrete scale is specific scale formation with 1220 a defined pitch collection (a `tonic` Pitch) that 1221 may or may not be bound by specific range. For 1222 example, a specific Major Scale, such as G 1223 Major, from G2 to G4. 1224 1225 This class is can either be used directly or more 1226 commonly as a base class for all concrete scales. 1227 1228 Here we treat a diminished triad as a scale: 1229 1230 >>> myScale = scale.ConcreteScale(pitches=['C4', 'E-4', 'G-4', 'A4']) 1231 >>> myScale.getTonic() 1232 <music21.pitch.Pitch C4> 1233 >>> myScale.next('G-2') 1234 <music21.pitch.Pitch A2> 1235 >>> [str(p) for p in myScale.getPitches('E-5', 'G-7')] 1236 ['E-5', 'G-5', 'A5', 'C6', 'E-6', 'G-6', 'A6', 'C7', 'E-7', 'G-7'] 1237 1238 1239 A scale that lasts two octaves and uses quarter tones (D~) 1240 1241 >>> complexScale = scale.ConcreteScale(pitches=[ 1242 ... 'C#3', 'E-3', 'F3', 'G3', 'B3', 'D~4', 'F#4', 'A4', 'C#5']) 1243 >>> complexScale.getTonic() 1244 <music21.pitch.Pitch C#3> 1245 >>> complexScale.next('G3', direction=scale.DIRECTION_DESCENDING) 1246 <music21.pitch.Pitch F3> 1247 1248 >>> [str(p) for p in complexScale.getPitches('C3', 'C7')] 1249 ['C#3', 'E-3', 'F3', 'G3', 'B3', 'D~4', 'F#4', 1250 'A4', 'C#5', 'E-5', 'F5', 'G5', 'B5', 'D~6', 'F#6', 'A6'] 1251 1252 Descending form: 1253 1254 >>> [str(p) for p in complexScale.getPitches('C7', 'C5')] 1255 ['A6', 'F#6', 'D~6', 'B5', 'G5', 'F5', 'E-5', 'C#5'] 1256 ''' 1257 usePitchDegreeCache = False 1258 1259 def __init__(self, 1260 tonic: Optional[Union[str, pitch.Pitch, note.Note]] = None, 1261 pitches: Optional[List[Union[pitch.Pitch, str]]] = None): 1262 super().__init__() 1263 1264 self.type = 'Concrete' 1265 # store an instance of an abstract scale 1266 # subclasses might use multiple abstract scales? 1267 self._abstract = None 1268 1269 # determine whether this is a limited range 1270 self.boundRange = False 1271 1272 if (tonic is None 1273 and pitches is not None 1274 and common.isListLike(pitches) 1275 and pitches): 1276 tonic = pitches[0] 1277 1278 # here, tonic is a pitch 1279 # the abstract scale defines what step the tonic is expected to be 1280 # found on 1281 # no default tonic is defined; as such, it is mostly an abstract scale 1282 if tonic is None: 1283 self.tonic = None # pitch.Pitch() 1284 elif isinstance(tonic, str): 1285 self.tonic = pitch.Pitch(tonic) 1286 elif isinstance(tonic, note.GeneralNote): 1287 self.tonic = tonic.pitch 1288 else: # assume this is a pitch object 1289 self.tonic = tonic 1290 1291 if (pitches is not None 1292 and common.isListLike(pitches) 1293 and pitches): 1294 self._abstract = AbstractScale() 1295 self._abstract.buildNetworkFromPitches(pitches) 1296 if tonic in pitches: 1297 self._abstract.tonicDegree = pitches.index(tonic) + 1 1298 1299 @property 1300 def isConcrete(self): 1301 ''' 1302 Return True if the scale is Concrete, that is, it has a defined Tonic. 1303 1304 >>> sc1 = scale.MajorScale('c') 1305 >>> sc1.isConcrete 1306 True 1307 >>> sc2 = scale.MajorScale() 1308 >>> sc2.isConcrete 1309 False 1310 1311 To be concrete, a Scale must have a 1312 defined tonic. An abstract Scale is not Concrete 1313 ''' 1314 if self.tonic is None: 1315 return False 1316 else: 1317 return True 1318 1319 def __eq__(self, other): 1320 ''' 1321 For concrete equality, the stored abstract objects must evaluate as equal, 1322 as well as local attributes. 1323 1324 >>> sc1 = scale.MajorScale('c') 1325 >>> sc2 = scale.MajorScale('c') 1326 >>> sc3 = scale.MinorScale('c') 1327 >>> sc4 = scale.MajorScale('g') 1328 >>> sc5 = scale.MajorScale() # an abstract scale, as no tonic defined 1329 1330 >>> sc1 == sc2 1331 True 1332 >>> sc1 == sc3 1333 False 1334 >>> sc1 == sc4 1335 False 1336 >>> sc1.abstract == sc4.abstract # can compare abstract forms 1337 True 1338 >>> sc4 == sc5 # implicit abstract comparison 1339 True 1340 >>> sc5 == sc2 # implicit abstract comparison 1341 True 1342 >>> sc5 == sc3 # implicit abstract comparison 1343 False 1344 1345 ''' 1346 # have to test each so as not to confuse with a subclass 1347 # TODO: add pitch range comparison if defined 1348 if other is None: 1349 return False 1350 if (not hasattr(self, 'isConcrete')) or (not hasattr(other, 'isConcrete')): 1351 return False 1352 1353 if not self.isConcrete or not other.isConcrete: 1354 # if tonic is none, then we automatically do an abstract comparison 1355 return self._abstract == other._abstract 1356 1357 else: 1358 if (isinstance(other, self.__class__) 1359 and isinstance(self, other.__class__) 1360 and self._abstract == other._abstract 1361 and self.boundRange == other.boundRange 1362 and self.tonic == other.tonic): 1363 return True 1364 else: 1365 return False 1366 1367 @property 1368 def name(self): 1369 ''' 1370 Return or construct the name of this scale 1371 1372 >>> sc = scale.DiatonicScale() # abstract, as no defined tonic 1373 >>> sc.name 1374 'Abstract diatonic' 1375 ''' 1376 if self.tonic is None: 1377 return ' '.join(['Abstract', self.type]) 1378 else: 1379 return ' '.join([self.tonic.name, self.type]) 1380 1381 def _reprInternal(self): 1382 return self.name 1383 1384 # -------------------------------------------------------------------------- 1385 1386 def getTonic(self): 1387 ''' 1388 Return the tonic. 1389 1390 >>> sc = scale.ConcreteScale(tonic='e-4') 1391 >>> sc.getTonic() 1392 <music21.pitch.Pitch E-4> 1393 ''' 1394 return self.tonic 1395 1396 @property 1397 def abstract(self): 1398 ''' 1399 Return the AbstractScale instance governing this ConcreteScale. 1400 1401 >>> sc1 = scale.MajorScale('d') 1402 >>> sc2 = scale.MajorScale('b-') 1403 >>> sc1 == sc2 1404 False 1405 >>> sc1.abstract == sc2.abstract 1406 True 1407 1408 Abstract scales can also be set afterwards: 1409 1410 >>> scVague = scale.ConcreteScale() 1411 >>> scVague.abstract = scale.AbstractDiatonicScale('major') 1412 >>> scVague.tonic = pitch.Pitch('D') 1413 >>> [p.name for p in scVague.getPitches()] 1414 ['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D'] 1415 1416 >>> scVague.abstract = scale.AbstractOctatonicScale() 1417 >>> [p.name for p in scVague.getPitches()] 1418 ['D', 'E', 'F', 'G', 'A-', 'B-', 'C-', 'D-', 'D'] 1419 1420 New and beta in v.6 -- changing `.abstract` is now allowed. 1421 ''' 1422 # copy before returning? (No... too slow) 1423 return self._abstract 1424 1425 @abstract.setter 1426 def abstract(self, newAbstract: AbstractScale): 1427 if not isinstance(newAbstract, AbstractScale): 1428 raise TypeError(f'abstract must be an AbstractScale, not {type(newAbstract)}') 1429 self._abstract = newAbstract 1430 1431 1432 def getDegreeMaxUnique(self): 1433 ''' 1434 Convenience routine to get this from the AbstractScale. 1435 ''' 1436 return self._abstract.getDegreeMaxUnique() 1437 1438 def transpose(self, value, *, inPlace=False): 1439 ''' 1440 Transpose this Scale by the given interval 1441 1442 note: it does not makes sense to transpose an abstract scale; 1443 thus, only concrete scales can be transposed. 1444 1445 >>> sc1 = scale.MajorScale('C') 1446 >>> sc2 = sc1.transpose('p5') 1447 >>> sc2 1448 <music21.scale.MajorScale G major> 1449 >>> sc3 = sc2.transpose('p5') 1450 >>> sc3 1451 <music21.scale.MajorScale D major> 1452 1453 >>> sc3.transpose('p5', inPlace=True) 1454 >>> sc3 1455 <music21.scale.MajorScale A major> 1456 ''' 1457 if inPlace: 1458 post = self 1459 else: 1460 post = copy.deepcopy(self) 1461 if self.tonic is None: 1462 # could raise an error; just assume a 'c' 1463 post.tonic = pitch.Pitch('C4') 1464 post.tonic.transpose(value, inPlace=True) 1465 else: 1466 post.tonic.transpose(value, inPlace=True) 1467 # may need to clear cache here 1468 if not inPlace: 1469 return post 1470 1471 def tune( 1472 self, 1473 streamObj, 1474 minPitch=None, 1475 maxPitch=None, 1476 direction=None 1477 ) -> None: 1478 ''' 1479 Given a Stream object containing Pitches, match all pitch names 1480 and or pitch space values and replace the target pitch with 1481 copies of pitches stored in this scale. 1482 1483 This is always applied recursively to all sub-Streams. 1484 ''' 1485 # we may use a directed or subset of the scale to tune 1486 # in the future, we might even match contour or direction 1487 pitchColl = self.getPitches(minPitch=minPitch, 1488 maxPitch=maxPitch, 1489 direction=direction 1490 ) 1491 pitchCollNames = [p.name for p in pitchColl] 1492 1493 def tuneOnePitch(p): 1494 # some pitches might be quarter / 3/4 tones; need to convert 1495 # these to microtonal representations so that we can directly 1496 # compare pitch names 1497 pAlt = p.convertQuarterTonesToMicrotones(inPlace=False) 1498 # need to permit enharmonic comparisons: G# and A- should 1499 # in most cases match 1500 testEnharmonics = pAlt.getAllCommonEnharmonics(alterLimit=2) 1501 testEnharmonics.append(pAlt) 1502 for pEnh in testEnharmonics: 1503 if pEnh.name not in pitchCollNames: 1504 continue 1505 # get the index from the names and extract the pitch by 1506 # index 1507 pDst = pitchColl[pitchCollNames.index(pEnh.name)] 1508 # get a deep copy for each note 1509 pDstNew = copy.deepcopy(pDst) 1510 pDstNew.octave = pEnh.octave # copy octave 1511 # need to adjust enharmonic 1512 pDstNewEnh = pDstNew.getAllCommonEnharmonics(alterLimit=2) 1513 match = None 1514 for x in pDstNewEnh: 1515 # try to match enharmonic with original alt 1516 if x.name == pAlt.name: 1517 match = x 1518 if match is None: # get original 1519 dst.append(pDstNew) 1520 else: 1521 dst.append(match) 1522 1523 # for p in streamObj.pitches: # this is always recursive 1524 for e in streamObj.recurse().notes: # get notes and chords 1525 if e.isChord: 1526 elementPitches = e.pitches 1527 else: # simulate a lost 1528 elementPitches = [e.pitch] 1529 1530 dst = [] # store dst in a list of resetting chord pitches 1531 for p in elementPitches: 1532 tuneOnePitch(p) 1533 # reassign the changed pitch 1534 if dst: 1535 if e.isChord: 1536 # note: we may not have matched all pitches 1537 e.pitches = dst 1538 else: # only one 1539 e.pitch = dst[0] 1540 1541 def romanNumeral(self, degree): 1542 ''' 1543 Return a RomanNumeral object built on the specified scale degree. 1544 1545 >>> sc1 = scale.MajorScale('a-4') 1546 >>> h1 = sc1.romanNumeral(1) 1547 >>> h1.root() 1548 <music21.pitch.Pitch A-4> 1549 1550 >>> h5 = sc1.romanNumeral(5) 1551 >>> h5.root() 1552 <music21.pitch.Pitch E-5> 1553 >>> h5 1554 <music21.roman.RomanNumeral V in A- major> 1555 ''' 1556 from music21 import roman 1557 return roman.RomanNumeral(degree, self) 1558 1559 def getPitches( 1560 self, 1561 minPitch=None, 1562 maxPitch=None, 1563 direction=None 1564 ) -> List[pitch.Pitch]: 1565 ''' 1566 Return a list of Pitch objects, using a 1567 deepcopy of a cached version if available. 1568 ''' 1569 # get from interval network of abstract scale 1570 1571 if self._abstract is None: 1572 return [] 1573 1574 # TODO: get and store in cache; return a copy 1575 # or generate from network stored in abstract 1576 if self.tonic is None: 1577 # note: could raise an error here, but instead will 1578 # use a pseudo-tonic 1579 pitchObj = pitch.Pitch('C4') 1580 else: 1581 pitchObj = self.tonic 1582 stepOfPitch = self._abstract.tonicDegree 1583 1584 if isinstance(minPitch, str): 1585 minPitch = pitch.Pitch(minPitch) 1586 if isinstance(maxPitch, str): 1587 maxPitch = pitch.Pitch(maxPitch) 1588 1589 if (minPitch is not None 1590 and maxPitch is not None 1591 and minPitch > maxPitch 1592 and direction is None): 1593 reverse = True 1594 (minPitch, maxPitch) = (maxPitch, minPitch) 1595 elif direction == DIRECTION_DESCENDING: 1596 reverse = True # reverse presentation so pitches go high to low 1597 else: 1598 reverse = False 1599 1600 if direction is None: 1601 direction = DIRECTION_ASCENDING 1602 1603 # this creates new pitches on each call 1604 return self._abstract.getRealization(pitchObj, 1605 stepOfPitch, 1606 minPitch=minPitch, 1607 maxPitch=maxPitch, 1608 direction=direction, 1609 reverse=reverse) 1610 # raise ScaleException('Cannot generate a scale from a DiatonicScale class') 1611 1612 # this needs to stay separate from getPitches; both are needed 1613 pitches = property(getPitches, 1614 doc='''Get a default pitch list from this scale. 1615 ''') 1616 1617 def getChord( 1618 self, 1619 minPitch=None, 1620 maxPitch=None, 1621 direction=DIRECTION_ASCENDING, 1622 **keywords 1623 ) -> 'music21.chord.Chord': 1624 ''' 1625 Return a realized chord containing all the 1626 pitches in this scale within a particular 1627 inclusive range defined by two pitches. 1628 1629 All keyword arguments are passed on to the 1630 Chord, permitting specification of 1631 `quarterLength` and similar parameters. 1632 ''' 1633 from music21 import chord 1634 return chord.Chord(self.getPitches(minPitch=minPitch, 1635 maxPitch=maxPitch, 1636 direction=direction), **keywords) 1637 1638 # this needs to stay separate from getChord 1639 chord = property(getChord, 1640 doc=''' 1641 Return a Chord object from this harmony over a default range. 1642 Use the `getChord()` method if you need greater control over the 1643 parameters of the chord. 1644 ''') 1645 1646 def pitchFromDegree( 1647 self, 1648 degree, 1649 minPitch=None, 1650 maxPitch=None, 1651 direction=DIRECTION_ASCENDING, 1652 equateTermini=True): 1653 ''' 1654 Given a scale degree, return a deepcopy of the appropriate pitch. 1655 1656 >>> sc = scale.MajorScale('e-') 1657 >>> sc.pitchFromDegree(2) 1658 <music21.pitch.Pitch F4> 1659 >>> sc.pitchFromDegree(7) 1660 <music21.pitch.Pitch D5> 1661 1662 OMIT_FROM_DOCS 1663 1664 Test deepcopy 1665 1666 >>> d = sc.pitchFromDegree(7) 1667 >>> d.accidental = pitch.Accidental('sharp') 1668 >>> d 1669 <music21.pitch.Pitch D#5> 1670 >>> sc.pitchFromDegree(7) 1671 <music21.pitch.Pitch D5> 1672 ''' 1673 cacheKey = None 1674 if (self.usePitchDegreeCache and self.tonic 1675 and not minPitch and not maxPitch and getattr(self, 'type', None)): 1676 tonicCacheKey = self.tonic.nameWithOctave 1677 cacheKey = (self.__class__, self.type, tonicCacheKey, degree, direction, equateTermini) 1678 if cacheKey in _pitchDegreeCache: 1679 return pitch.Pitch(_pitchDegreeCache[cacheKey]) 1680 1681 post = self._abstract.getPitchFromNodeDegree( 1682 pitchReference=self.tonic, # pitch defined here 1683 nodeName=self._abstract.tonicDegree, # defined in abstract class 1684 nodeDegreeTarget=degree, # target looking for 1685 direction=direction, 1686 minPitch=minPitch, 1687 maxPitch=maxPitch, 1688 equateTermini=equateTermini) 1689 1690 if cacheKey: 1691 _pitchDegreeCache[cacheKey] = post.nameWithOctave 1692 1693 return post 1694 1695 # if 0 < degree <= self._abstract.getDegreeMaxUnique(): 1696 # return self.getPitches()[degree - 1] 1697 # else: 1698 # raise('Scale degree is out of bounds: must be between 1 and %s.' % ( 1699 # self._abstract.getDegreeMaxUnique())) 1700 1701 def pitchesFromScaleDegrees( 1702 self, 1703 degreeTargets, 1704 minPitch=None, 1705 maxPitch=None, 1706 direction=DIRECTION_ASCENDING): 1707 ''' 1708 Given one or more scale degrees, return a list 1709 of all matches over the entire range. 1710 1711 >>> sc = scale.MajorScale('e-') 1712 >>> sc.pitchesFromScaleDegrees([3, 7]) 1713 [<music21.pitch.Pitch G4>, <music21.pitch.Pitch D5>] 1714 >>> [str(p) for p in sc.pitchesFromScaleDegrees([3, 7], 'c2', 'c6')] 1715 ['D2', 'G2', 'D3', 'G3', 'D4', 'G4', 'D5', 'G5'] 1716 1717 >>> sc = scale.HarmonicMinorScale('a') 1718 >>> [str(p) for p in sc.pitchesFromScaleDegrees([3, 7], 'c2', 'c6')] 1719 ['C2', 'G#2', 'C3', 'G#3', 'C4', 'G#4', 'C5', 'G#5', 'C6'] 1720 ''' 1721 # TODO: rely here on intervalNetwork for caching 1722 post = self._abstract.realizePitchByDegree( 1723 pitchReference=self.tonic, # pitch defined here 1724 nodeId=self._abstract.tonicDegree, # defined in abstract class 1725 nodeDegreeTargets=degreeTargets, # target looking for 1726 direction=direction, 1727 minPitch=minPitch, 1728 maxPitch=maxPitch) 1729 return post 1730 1731 def intervalBetweenDegrees( 1732 self, 1733 degreeStart, 1734 degreeEnd, 1735 direction=DIRECTION_ASCENDING, 1736 equateTermini=True): 1737 ''' 1738 Given two degrees, provide the interval as an interval.Interval object. 1739 1740 >>> sc = scale.MajorScale('e-') 1741 >>> sc.intervalBetweenDegrees(3, 7) 1742 <music21.interval.Interval P5> 1743 ''' 1744 # get pitches for each degree 1745 pStart = self.pitchFromDegree(degreeStart, direction=direction, 1746 equateTermini=equateTermini) 1747 pEnd = self.pitchFromDegree(degreeEnd, direction=direction, 1748 equateTermini=equateTermini) 1749 if pStart is None: 1750 raise ScaleException(f'cannot get a pitch for scale degree: {pStart}') 1751 if pEnd is None: 1752 raise ScaleException(f'cannot get a pitch for scale degree: {pEnd}') 1753 return interval.Interval(pStart, pEnd) 1754 1755 def getScaleDegreeFromPitch(self, 1756 pitchTarget, 1757 direction=DIRECTION_ASCENDING, 1758 comparisonAttribute='name'): 1759 ''' 1760 For a given pitch, return the appropriate scale degree. 1761 If no scale degree is available, None is returned. 1762 1763 Note -- by default it will find based on note name not on 1764 PitchClass because this is used so commonly by tonal functions. 1765 So if it's important that D# and E- are the same, set the 1766 comparisonAttribute to `pitchClass` 1767 1768 >>> sc = scale.MajorScale('e-') 1769 >>> sc.getScaleDegreeFromPitch('e-2') 1770 1 1771 >>> sc.getScaleDegreeFromPitch('d') 1772 7 1773 >>> sc.getScaleDegreeFromPitch('d#', comparisonAttribute='name') is None 1774 True 1775 >>> sc.getScaleDegreeFromPitch('d#', comparisonAttribute='pitchClass') 1776 1 1777 >>> sc.getScaleDegreeFromPitch('e') is None 1778 True 1779 >>> sc.getScaleDegreeFromPitch('e', comparisonAttribute='step') 1780 1 1781 1782 >>> sc = scale.HarmonicMinorScale('a') 1783 >>> sc.getScaleDegreeFromPitch('c') 1784 3 1785 >>> sc.getScaleDegreeFromPitch('g#') 1786 7 1787 >>> sc.getScaleDegreeFromPitch('g') is None 1788 True 1789 1790 >>> cMaj = key.Key('C') 1791 >>> cMaj.getScaleDegreeFromPitch(pitch.Pitch('E-'), 1792 ... direction=scale.DIRECTION_ASCENDING, 1793 ... comparisonAttribute='step') 1794 3 1795 ''' 1796 post = self._abstract.getRelativeNodeDegree(pitchReference=self.tonic, 1797 nodeName=self._abstract.tonicDegree, 1798 pitchTarget=pitchTarget, 1799 comparisonAttribute=comparisonAttribute, 1800 direction=direction) 1801 return post 1802 1803 def getScaleDegreeAndAccidentalFromPitch(self, 1804 pitchTarget, 1805 direction=DIRECTION_ASCENDING, 1806 comparisonAttribute='name'): 1807 ''' 1808 Given a scale (or :class:`~music21.key.Key` object) and a pitch, return a two-element 1809 tuple of the degree of the scale and an accidental (or None) needed to get this 1810 pitch. 1811 1812 >>> cMaj = key.Key('C') 1813 >>> cMaj.getScaleDegreeAndAccidentalFromPitch(pitch.Pitch('E')) 1814 (3, None) 1815 >>> cMaj.getScaleDegreeAndAccidentalFromPitch(pitch.Pitch('E-')) 1816 (3, <music21.pitch.Accidental flat>) 1817 1818 1819 The Direction of a melodic minor scale is significant 1820 1821 >>> aMin = scale.MelodicMinorScale('a') 1822 >>> aMin.getScaleDegreeAndAccidentalFromPitch(pitch.Pitch('G'), 1823 ... direction=scale.DIRECTION_DESCENDING) 1824 (7, None) 1825 >>> aMin.getScaleDegreeAndAccidentalFromPitch(pitch.Pitch('G'), 1826 ... direction=scale.DIRECTION_ASCENDING) 1827 (7, <music21.pitch.Accidental flat>) 1828 >>> aMin.getScaleDegreeAndAccidentalFromPitch(pitch.Pitch('G-'), 1829 ... direction=scale.DIRECTION_ASCENDING) 1830 (7, <music21.pitch.Accidental double-flat>) 1831 1832 Returns (None, None) if for some reason this scale does not have this step 1833 (a whole-tone scale, for instance) 1834 ''' 1835 scaleStep = self.getScaleDegreeFromPitch(pitchTarget, direction, comparisonAttribute) 1836 if scaleStep is not None: 1837 return (scaleStep, None) 1838 else: 1839 scaleStepNormal = self.getScaleDegreeFromPitch(pitchTarget, 1840 direction, 1841 comparisonAttribute='step') 1842 if scaleStepNormal is None: 1843 raise ScaleException( 1844 'Cannot get any scale degree from getScaleDegreeFromPitch for pitchTarget ' 1845 + f"{pitchTarget}, direction {direction}, comparisonAttribute='step'") 1846 pitchesFound = self.pitchesFromScaleDegrees([scaleStepNormal]) 1847 1848 if not pitchesFound: 1849 return (None, None) 1850 else: 1851 foundPitch = pitchesFound[0] 1852 if foundPitch.accidental is None: 1853 foundAlter = 0 1854 else: 1855 foundAlter = foundPitch.accidental.alter 1856 1857 if pitchTarget.accidental is None: 1858 pitchAlter = 0 1859 else: 1860 pitchAlter = pitchTarget.accidental.alter 1861 1862 alterDiff = pitchAlter - foundAlter 1863 1864 if alterDiff == 0: 1865 # should not happen... 1866 return (scaleStepNormal, None) 1867 else: 1868 alterAccidental = pitch.Accidental(alterDiff) 1869 return (scaleStepNormal, alterAccidental) 1870 1871 # uses "traditional" chromatic solfeg and mostly Shearer Hullah (if needed) 1872 # noinspection SpellCheckingInspection 1873 _solfegSyllables = {1: {-2: 'def', 1874 -1: 'de', 1875 0: 'do', 1876 1: 'di', 1877 2: 'dis', 1878 }, 1879 2: {-2: 'raf', 1880 -1: 'ra', 1881 0: 're', 1882 1: 'ri', 1883 2: 'ris', 1884 }, 1885 3: {-2: 'mef', 1886 -1: 'me', 1887 0: 'mi', 1888 1: 'mis', 1889 2: 'mish', 1890 }, 1891 4: {-2: 'fef', 1892 -1: 'fe', 1893 0: 'fa', 1894 1: 'fi', 1895 2: 'fis', 1896 }, 1897 5: {-2: 'sef', 1898 -1: 'se', 1899 0: 'sol', 1900 1: 'si', 1901 2: 'sis', 1902 }, 1903 6: {-2: 'lef', 1904 -1: 'le', 1905 0: 'la', 1906 1: 'li', 1907 2: 'lis', 1908 }, 1909 7: {-2: 'tef', 1910 -1: 'te', 1911 0: 'ti', 1912 1: 'tis', 1913 2: 'tish', 1914 }, 1915 } 1916 # TOO SLOW! 1917 # _humdrumSolfegSyllables = copy.deepcopy(_solfegSyllables) 1918 # _humdrumSolfegSyllables[3][1] = 'my' 1919 # _humdrumSolfegSyllables[5] = {-2: 'sef', -1: 'se', 0: 'so', 1:'si', 2:'sis'} 1920 # _humdrumSolfegSyllables[7][1] = 'ty' 1921 # noinspection SpellCheckingInspection 1922 _humdrumSolfegSyllables = { 1923 1: {-2: 'def', 1924 -1: 'de', 1925 0: 'do', 1926 1: 'di', 1927 2: 'dis', 1928 }, 1929 2: {-2: 'raf', 1930 -1: 'ra', 1931 0: 're', 1932 1: 'ri', 1933 2: 'ris', 1934 }, 1935 3: {-2: 'mef', 1936 -1: 'me', 1937 0: 'mi', 1938 1: 'my', 1939 2: 'mish', 1940 }, 1941 4: {-2: 'fef', 1942 -1: 'fe', 1943 0: 'fa', 1944 1: 'fi', 1945 2: 'fis', 1946 }, 1947 5: {-2: 'sef', 1948 -1: 'se', 1949 0: 'so', 1950 1: 'si', 1951 2: 'sis', 1952 }, 1953 6: {-2: 'lef', 1954 -1: 'le', 1955 0: 'la', 1956 1: 'li', 1957 2: 'lis', 1958 }, 1959 7: {-2: 'tef', 1960 -1: 'te', 1961 0: 'ti', 1962 1: 'ty', 1963 2: 'tish', 1964 }, 1965 } 1966 1967 def solfeg(self, 1968 pitchTarget=None, 1969 direction=DIRECTION_ASCENDING, 1970 variant='music21', 1971 chromatic=True): 1972 ''' 1973 Returns the chromatic solfeg (or diatonic if chromatic is False) 1974 for a given pitch in a given scale. 1975 1976 The `variant` method lets one specify either the default `music21` 1977 or `humdrum` solfeg representation 1978 for altered notes. 1979 1980 >>> eflatMaj = key.Key('E-') 1981 >>> eflatMaj.solfeg(pitch.Pitch('G')) 1982 'mi' 1983 >>> eflatMaj.solfeg('A') 1984 'fi' 1985 >>> eflatMaj.solfeg('A', chromatic=False) 1986 'fa' 1987 >>> eflatMaj.solfeg(pitch.Pitch('G#'), variant='music21') # default 1988 'mis' 1989 >>> eflatMaj.solfeg(pitch.Pitch('G#'), variant='humdrum') 1990 'my' 1991 ''' 1992 if isinstance(pitchTarget, str): 1993 pitchTarget = pitch.Pitch(pitchTarget) 1994 (scaleDeg, accidental) = self.getScaleDegreeAndAccidentalFromPitch(pitchTarget, direction) 1995 if variant == 'music21': 1996 syllableDict = self._solfegSyllables 1997 elif variant == 'humdrum': 1998 syllableDict = self._humdrumSolfegSyllables 1999 else: 2000 raise ScaleException(f'Unknown solfeg variant {variant}') 2001 2002 if scaleDeg > 7: 2003 raise ScaleException('Cannot call solfeg on non-7-degree scales') 2004 if scaleDeg is None: 2005 raise ScaleException('Unknown scale degree for this pitch') 2006 2007 if chromatic is True: 2008 if accidental is None: 2009 return syllableDict[scaleDeg][0] 2010 else: 2011 return syllableDict[scaleDeg][accidental.alter] 2012 else: 2013 return syllableDict[scaleDeg][0] 2014 2015 def next(self, 2016 pitchOrigin=None, 2017 direction: Union[str, int, bool] = 'ascending', 2018 stepSize=1, 2019 getNeighbor: Union[str, bool] = True): 2020 ''' 2021 Get the next pitch above (or if direction is 'descending', below) 2022 a `pitchOrigin` or None. If the `pitchOrigin` is None, the tonic pitch is 2023 returned. This is useful when starting a chain of iterative calls. 2024 2025 The `direction` attribute may be either ascending or descending. 2026 Default is `ascending`. Optionally, positive or negative integers 2027 may be provided as directional stepSize scalars. 2028 2029 An optional `stepSize` argument can be used to set the number 2030 of scale steps that are stepped through. Thus, .next(stepSize=2) 2031 will give not the next pitch in the scale, but the next after this one. 2032 2033 The `getNeighbor` will return a pitch from the scale 2034 if `pitchOrigin` is not in the scale. This value can be 2035 True, 'ascending', or 'descending'. 2036 2037 >>> sc = scale.MajorScale('e-') 2038 >>> print(sc.next('e-5')) 2039 F5 2040 >>> print(sc.next('e-5', stepSize=2)) 2041 G5 2042 >>> print(sc.next('e-6', stepSize=3)) 2043 A-6 2044 2045 This uses the getNeighbor attribute to 2046 find the next note above f#5 in the E-flat 2047 major scale: 2048 2049 >>> sc.next('f#5') 2050 <music21.pitch.Pitch G5> 2051 2052 >>> sc = scale.HarmonicMinorScale('g') 2053 >>> sc.next('g4', 'descending') 2054 <music21.pitch.Pitch F#4> 2055 >>> sc.next('F#4', 'descending') 2056 <music21.pitch.Pitch E-4> 2057 >>> sc.next('E-4', 'descending') 2058 <music21.pitch.Pitch D4> 2059 >>> sc.next('E-4', 'ascending', 1) 2060 <music21.pitch.Pitch F#4> 2061 >>> sc.next('E-4', 'ascending', 2) 2062 <music21.pitch.Pitch G4> 2063 ''' 2064 if pitchOrigin is None: 2065 return self.tonic 2066 2067 # allow numerical directions 2068 if common.isNum(direction): 2069 if direction != 0: 2070 # treat as a positive or negative step scalar 2071 if direction > 0: 2072 stepScalar = direction 2073 direction = DIRECTION_ASCENDING 2074 else: # negative non-zero 2075 stepScalar = abs(direction) 2076 direction = DIRECTION_DESCENDING 2077 else: 2078 raise ScaleException('direction cannot be zero') 2079 else: # when direction is a string, use scalar of 1 2080 stepScalar = 1 2081 2082 # pick reverse direction for neighbor 2083 if getNeighbor is True: 2084 if direction == DIRECTION_ASCENDING: 2085 getNeighbor = DIRECTION_DESCENDING 2086 elif direction == DIRECTION_DESCENDING: 2087 getNeighbor = DIRECTION_ASCENDING 2088 2089 post = self._abstract.nextPitch( 2090 pitchReference=self.tonic, 2091 nodeName=self._abstract.tonicDegree, 2092 pitchOrigin=pitchOrigin, 2093 direction=direction, 2094 stepSize=stepSize * stepScalar, # multiplied 2095 getNeighbor=getNeighbor 2096 ) 2097 return post 2098 2099 def isNext(self, 2100 other, 2101 pitchOrigin, 2102 direction='ascending', 2103 stepSize=1, 2104 getNeighbor: Union[str, bool] = True, 2105 comparisonAttribute='name'): 2106 ''' 2107 Given another pitch, as well as an origin and a direction, 2108 determine if this other pitch is in the next in the scale. 2109 2110 >>> sc1 = scale.MajorScale('g') 2111 >>> sc1.isNext('d4', 'c4', 'ascending') 2112 True 2113 ''' 2114 if isinstance(other, str): # convert to pitch 2115 other = pitch.Pitch(other) 2116 elif hasattr(other, 'pitch'): # possibly a note 2117 other = other.pitch # just get pitch component 2118 elif not isinstance(other, pitch.Pitch): 2119 return False # cannot compare to non-pitch 2120 2121 nPitch = self.next(pitchOrigin, 2122 direction=direction, 2123 stepSize=stepSize, 2124 getNeighbor=getNeighbor) 2125 if nPitch is None: 2126 return None 2127 2128 if (getattr(nPitch, comparisonAttribute) 2129 == getattr(other, comparisonAttribute)): 2130 return True 2131 else: 2132 return False 2133 2134 # -------------------------------------------------------------------------- 2135 # comparison and evaluation 2136 2137 def match(self, other, comparisonAttribute='name'): 2138 ''' 2139 Given another object of the forms that `extractPitchList` can take, 2140 (e.g., a :class:`~music21.stream.Stream`, a :class:`~music21.scale.ConcreteScale`, 2141 a list of :class:`~music21.pitch.Pitch` objects), 2142 return a named dictionary of pitch lists with keys 'matched' and 'notMatched'. 2143 2144 >>> sc1 = scale.MajorScale('g') 2145 >>> sc2 = scale.MajorScale('d') 2146 >>> sc3 = scale.MajorScale('a') 2147 >>> sc4 = scale.MajorScale('e') 2148 2149 >>> from pprint import pprint as pp 2150 >>> pp(sc1.match(sc2)) 2151 {'matched': [<music21.pitch.Pitch D4>, <music21.pitch.Pitch E4>, 2152 <music21.pitch.Pitch F#4>, <music21.pitch.Pitch G4>, 2153 <music21.pitch.Pitch A4>, <music21.pitch.Pitch B4>], 2154 'notMatched': [<music21.pitch.Pitch C#5>]} 2155 2156 >>> pp(sc2.match(sc3)) 2157 {'matched': [<music21.pitch.Pitch A4>, <music21.pitch.Pitch B4>, 2158 <music21.pitch.Pitch C#5>, <music21.pitch.Pitch D5>, 2159 <music21.pitch.Pitch E5>, <music21.pitch.Pitch F#5>], 2160 'notMatched': [<music21.pitch.Pitch G#5>]} 2161 2162 >>> pp(sc1.match(sc4)) 2163 {'matched': [<music21.pitch.Pitch E4>, <music21.pitch.Pitch F#4>, 2164 <music21.pitch.Pitch A4>, <music21.pitch.Pitch B4>], 2165 'notMatched': [<music21.pitch.Pitch G#4>, 2166 <music21.pitch.Pitch C#5>, 2167 <music21.pitch.Pitch D#5>]} 2168 ''' 2169 # strip out unique pitches in a list 2170 otherPitches = self.extractPitchList(other, 2171 comparisonAttribute=comparisonAttribute) 2172 2173 # need to deal with direction here? or get an aggregate scale 2174 matched, notMatched = self._abstract._net.match( 2175 pitchReference=self.tonic, 2176 nodeId=self._abstract.tonicDegree, 2177 pitchTarget=otherPitches, # can supply a list here 2178 comparisonAttribute=comparisonAttribute) 2179 2180 post = { 2181 'matched': matched, 2182 'notMatched': notMatched, 2183 } 2184 return post 2185 2186 def findMissing(self, 2187 other, 2188 comparisonAttribute='pitchClass', 2189 minPitch=None, 2190 maxPitch=None, 2191 direction=DIRECTION_ASCENDING, 2192 alteredDegrees=None): 2193 ''' 2194 Given another object of the forms that `extractPitches` takes 2195 (e.g., a :class:`~music21.stream.Stream`, 2196 a :class:`~music21.scale.ConcreteScale`, 2197 a list of :class:`~music21.pitch.Pitch` objects), 2198 return a list of pitches that are found in this Scale but are not 2199 found in the provided object. 2200 2201 >>> sc1 = scale.MajorScale('g4') 2202 >>> [str(p) for p in sc1.findMissing(['d'])] 2203 ['G4', 'A4', 'B4', 'C5', 'E5', 'F#5', 'G5'] 2204 ''' 2205 # strip out unique pitches in a list 2206 otherPitches = self.extractPitchList(other, 2207 comparisonAttribute=comparisonAttribute) 2208 post = self._abstract._net.findMissing( 2209 pitchReference=self.tonic, 2210 nodeId=self._abstract.tonicDegree, 2211 pitchTarget=otherPitches, # can supply a list here 2212 comparisonAttribute=comparisonAttribute, 2213 minPitch=minPitch, 2214 maxPitch=maxPitch, 2215 direction=direction, 2216 alteredDegrees=alteredDegrees, 2217 ) 2218 return post 2219 2220 def deriveRanked(self, 2221 other, 2222 resultsReturned=4, 2223 comparisonAttribute='pitchClass', 2224 removeDuplicates=False): 2225 ''' 2226 Return a list of closest-matching :class:`~music21.scale.ConcreteScale` objects 2227 based on this :class:`~music21.scale.AbstractScale`, 2228 provided as a :class:`~music21.stream.Stream`, a :class:`~music21.scale.ConcreteScale`, 2229 or a list of :class:`~music21.pitch.Pitch` objects. 2230 Returned integer values represent the number of matches. 2231 2232 If you are working with Diatonic Scales, you will probably 2233 want to change the `comparisonAttribute` to `name`. 2234 2235 >>> sc1 = scale.MajorScale() 2236 >>> sc1.deriveRanked(['c', 'e', 'b']) 2237 [(3, <music21.scale.MajorScale G major>), 2238 (3, <music21.scale.MajorScale C major>), 2239 (2, <music21.scale.MajorScale B major>), 2240 (2, <music21.scale.MajorScale A major>)] 2241 2242 >>> sc1.deriveRanked(['d-', 'e', 'b']) 2243 [(3, <music21.scale.MajorScale B major>), 2244 (3, <music21.scale.MajorScale A major>), 2245 (3, <music21.scale.MajorScale E major>), 2246 (3, <music21.scale.MajorScale D major>)] 2247 2248 >>> sc1.deriveRanked(['d-', 'e', 'b'], comparisonAttribute='name') 2249 [(2, <music21.scale.MajorScale B major>), 2250 (2, <music21.scale.MajorScale A major>), 2251 (2, <music21.scale.MajorScale G major>), 2252 (2, <music21.scale.MajorScale E major>)] 2253 2254 >>> sc1.deriveRanked(['c', 'e', 'e', 'e', 'b']) 2255 [(5, <music21.scale.MajorScale G major>), 2256 (5, <music21.scale.MajorScale C major>), 2257 (4, <music21.scale.MajorScale B major>), 2258 (4, <music21.scale.MajorScale A major>)] 2259 2260 >>> sc1.deriveRanked(['c#', 'e', 'g#']) 2261 [(3, <music21.scale.MajorScale B major>), 2262 (3, <music21.scale.MajorScale A major>), 2263 (3, <music21.scale.MajorScale E major>), 2264 (3, <music21.scale.MajorScale C- major>)] 2265 2266 Test that a Concrete Scale (that is, with no _abstract defined) still has similar 2267 characteristics to the original. 2268 2269 Create a scale like a Harmonic minor but with flat 2 and sharp 4 2270 2271 >>> e = scale.ConcreteScale(pitches=['A4', 'B-4', 'C5', 'D#5', 'E5', 'F5', 'G#5', 'A5']) 2272 >>> f = e.deriveRanked(['C', 'E', 'G']) 2273 >>> f 2274 [(3, <music21.scale.ConcreteScale E Concrete>), 2275 (3, <music21.scale.ConcreteScale D- Concrete>), 2276 (3, <music21.scale.ConcreteScale C# Concrete>), 2277 (2, <music21.scale.ConcreteScale B Concrete>)] 2278 2279 >>> ' '.join([str(p) for p in f[0][1].pitches]) 2280 'E4 F4 G4 A#4 B4 C5 D#5 E5' 2281 2282 2283 ''' 2284 # possibly return dictionary with named parameters 2285 # default return all scales that match all provided pitches 2286 # instead of results returned, define how many matched pitches necessary 2287 otherPitches = self.extractPitchList(other, 2288 comparisonAttribute=comparisonAttribute, 2289 removeDuplicates=removeDuplicates) 2290 2291 pairs = self._abstract._net.find(pitchTarget=otherPitches, 2292 resultsReturned=resultsReturned, 2293 comparisonAttribute=comparisonAttribute, 2294 alteredDegrees=self._abstract._alteredDegrees) 2295 post = [] 2296 for weight, p in pairs: 2297 sc = self.__class__(tonic=p) 2298 if sc._abstract is None: 2299 sc._abstract = copy.deepcopy(self._abstract) 2300 2301 post.append((weight, sc)) 2302 return post 2303 2304 def derive(self, other, comparisonAttribute='pitchClass'): 2305 ''' 2306 Return the closest-matching :class:`~music21.scale.ConcreteScale` 2307 based on the pitch collection provided as a 2308 :class:`~music21.stream.Stream`, a :class:`~music21.scale.ConcreteScale`, 2309 or a list of :class:`~music21.pitch.Pitch` objects. 2310 2311 How the "closest-matching" scale is defined still needs to be 2312 refined and will probably change in the future. 2313 2314 >>> sc1 = scale.MajorScale() 2315 >>> sc1.derive(['c#', 'e', 'g#']) 2316 <music21.scale.MajorScale B major> 2317 2318 >>> sc1.derive(['e-', 'b-', 'd'], comparisonAttribute='name') 2319 <music21.scale.MajorScale B- major> 2320 ''' 2321 otherPitches = self.extractPitchList(other, 2322 comparisonAttribute=comparisonAttribute) 2323 2324 # weight target membership 2325 pairs = self._abstract._net.find(pitchTarget=otherPitches, 2326 comparisonAttribute=comparisonAttribute) 2327 2328 newScale = self.__class__(tonic=pairs[0][1]) 2329 if newScale._abstract is None: 2330 newScale._abstract = copy.deepcopy(self._abstract) 2331 return newScale 2332 2333 def deriveAll(self, other, comparisonAttribute='pitchClass'): 2334 ''' 2335 Return a list of all Scales of the same class as `self` 2336 where all the pitches in `other` are contained. 2337 2338 Similar to "deriveRanked" but only returns those scales 2339 no matter how many which contain all the pitches. 2340 2341 Just returns a list in order. 2342 2343 If you are working with Diatonic Scales, you will 2344 probably want to change the `comparisonAttribute` to `name`. 2345 2346 >>> sc1 = scale.MajorScale() 2347 >>> sc1.deriveAll(['c', 'e', 'b']) 2348 [<music21.scale.MajorScale G major>, <music21.scale.MajorScale C major>] 2349 2350 >>> [sc.name for sc in sc1.deriveAll(['d-', 'e', 'b'])] 2351 ['B major', 'A major', 'E major', 'D major', 'C- major'] 2352 2353 >>> sc1.deriveAll(['d-', 'e', 'b'], comparisonAttribute='name') 2354 [] 2355 2356 Find all instances of this pentatonic scale in major scales: 2357 2358 >>> scList = sc1.deriveAll(['c#', 'd#', 'f#', 'g#', 'a#'], comparisonAttribute='name') 2359 >>> [sc.name for sc in scList] 2360 ['B major', 'F# major', 'C# major'] 2361 ''' 2362 # possibly return dictionary with named parameters 2363 # default return all scales that match all provided pitches 2364 # instead of results returned, define how many matched pitches necessary 2365 otherPitches = self.extractPitchList(other, 2366 comparisonAttribute=comparisonAttribute) 2367 2368 pairs = self._abstract._net.find(pitchTarget=otherPitches, 2369 resultsReturned=None, 2370 comparisonAttribute=comparisonAttribute, 2371 alteredDegrees=self._abstract._alteredDegrees) 2372 post = [] 2373 numPitches = len(otherPitches) 2374 2375 for weight, p in pairs: 2376 if weight == numPitches: # only want matches where all notes match 2377 sc = self.__class__(tonic=p) 2378 if sc._abstract is None: 2379 sc._abstract = copy.deepcopy(self._abstract) 2380 post.append(sc) 2381 return post 2382 2383 def deriveByDegree(self, degree, pitchRef): 2384 ''' 2385 Given a scale degree and a pitch, return a 2386 new :class:`~music21.scale.ConcreteScale` that satisfies 2387 that condition. 2388 2389 Find a major scale with C as the 7th degree: 2390 2391 >>> sc1 = scale.MajorScale() 2392 >>> sc1.deriveByDegree(7, 'c') 2393 <music21.scale.MajorScale D- major> 2394 2395 TODO: Does not yet work for directional scales 2396 ''' 2397 p = self._abstract.getNewTonicPitch( 2398 pitchReference=pitchRef, 2399 nodeName=degree, 2400 ) 2401 # except intervalNetwork.IntervalNetworkException: 2402 # p = self._abstract.getNewTonicPitch( 2403 # pitchReference=pitchRef, 2404 # nodeName=degree, 2405 # direction=DIRECTION_DESCENDING, 2406 # ) 2407 2408 if p is None: 2409 raise ScaleException('cannot derive new tonic') 2410 2411 newScale = self.__class__(tonic=p) 2412 if newScale._abstract is None: 2413 newScale._abstract = copy.deepcopy(self._abstract) 2414 return newScale 2415 2416 # -------------------------------------------------------------------------- 2417 # alternative outputs 2418 2419 def getScalaData(self): 2420 ''' 2421 Return a configured :class:`~music21.scala.ScalaData` 2422 Object for this scale. It can be used to find interval 2423 distances in cents between degrees. 2424 ''' 2425 ss = self.abstract.getScalaData() 2426 # customize with more specific representation 2427 ss.description = repr(self) 2428 return ss 2429 2430 def write(self, fmt=None, fp=None, direction=DIRECTION_ASCENDING): 2431 ''' 2432 Write the scale in a format. 2433 Here, prepare scala format if requested. 2434 ''' 2435 if fmt is not None: 2436 fileFormat, unused_ext = common.findFormat(fmt) 2437 if fileFormat == 'scala': 2438 return self.abstract.write(fmt=fmt, fp=fp, direction=direction) 2439 return Scale.write(self, fmt=fmt, fp=fp) 2440 2441 def show(self, fmt=None, app=None, direction=DIRECTION_ASCENDING): 2442 ''' 2443 Show the scale in a format. Here, prepare scala format 2444 if requested. 2445 ''' 2446 if fmt is not None: 2447 fileFormat, unused_ext = common.findFormat(fmt) 2448 if fileFormat == 'scala': 2449 self.abstract.show(fmt=fmt, app=app, direction=direction) 2450 return 2451 Scale.show(self, fmt=fmt, app=app) 2452 2453# ------------------------------------------------------------------------------ 2454# concrete scales and subclasses 2455 2456 2457class DiatonicScale(ConcreteScale): 2458 ''' 2459 A concrete diatonic scale. Each DiatonicScale 2460 has one instance of a :class:`~music21.scale.AbstractDiatonicScale`. 2461 ''' 2462 usePitchDegreeCache = True 2463 2464 def __init__(self, tonic=None): 2465 super().__init__(tonic=tonic) 2466 self._abstract = AbstractDiatonicScale() 2467 self.type = 'diatonic' 2468 2469 def getTonic(self): 2470 ''' 2471 Return the tonic of the diatonic scale. 2472 2473 >>> sc = scale.MajorScale('e-') 2474 >>> sc.getTonic() 2475 <music21.pitch.Pitch E-4> 2476 >>> sc = scale.MajorScale('F#') 2477 >>> sc.getTonic() 2478 <music21.pitch.Pitch F#4> 2479 2480 If no tonic has been defined, it will return an Exception. 2481 (same is true for `getDominant`, `getLeadingTone`, etc.) 2482 2483 >>> sc = scale.DiatonicScale() 2484 >>> sc.getTonic() 2485 Traceback (most recent call last): 2486 music21.scale.intervalNetwork.IntervalNetworkException: pitchReference cannot be None 2487 ''' 2488 # NOTE: override method on ConcreteScale that simply returns _tonic 2489 return self.pitchFromDegree(self._abstract.tonicDegree) 2490 2491 def getDominant(self): 2492 ''' 2493 Return the dominant. 2494 2495 >>> sc = scale.MajorScale('e-') 2496 >>> sc.getDominant() 2497 <music21.pitch.Pitch B-4> 2498 >>> sc = scale.MajorScale('F#') 2499 >>> sc.getDominant() 2500 <music21.pitch.Pitch C#5> 2501 ''' 2502 return self.pitchFromDegree(self._abstract.dominantDegree) 2503 2504 def getLeadingTone(self): 2505 ''' 2506 Return the leading tone. 2507 2508 >>> sc = scale.MinorScale('c') 2509 >>> sc.getLeadingTone() 2510 <music21.pitch.Pitch B4> 2511 2512 Note that the leading tone isn't necessarily 2513 the same as the 7th scale degree in minor: 2514 2515 >>> sc.pitchFromDegree(7) 2516 <music21.pitch.Pitch B-4> 2517 ''' 2518 # NOTE: must be adjust for modes that do not have a proper leading tone 2519 seventhDegree = self.pitchFromDegree(7) 2520 distanceInSemitones = seventhDegree.midi - self.tonic.midi 2521 if distanceInSemitones != 11: 2522 # if not a major seventh, raise/lower the seventh degree 2523 alterationInSemitones = 11 - distanceInSemitones 2524 seventhDegree.accidental = pitch.Accidental( 2525 seventhDegree.alter + alterationInSemitones 2526 ) 2527 return seventhDegree 2528 2529 def getParallelMinor(self): 2530 ''' 2531 Return a parallel minor scale based on this 2532 concrete scale. 2533 2534 >>> sc1 = scale.MajorScale(pitch.Pitch('a')) 2535 >>> [str(p) for p in sc1.pitches] 2536 ['A4', 'B4', 'C#5', 'D5', 'E5', 'F#5', 'G#5', 'A5'] 2537 >>> sc2 = sc1.getParallelMinor() 2538 >>> [str(p) for p in sc2.pitches] 2539 ['A4', 'B4', 'C5', 'D5', 'E5', 'F5', 'G5', 'A5'] 2540 2541 Running getParallelMinor() again doesn't change anything 2542 2543 >>> sc3 = sc2.getParallelMinor() 2544 >>> [str(p) for p in sc3.pitches] 2545 ['A4', 'B4', 'C5', 'D5', 'E5', 'F5', 'G5', 'A5'] 2546 ''' 2547 return MinorScale(self.tonic) 2548 2549 def getParallelMajor(self): 2550 ''' 2551 Return a concrete relative major scale 2552 2553 >>> sc1 = scale.MinorScale(pitch.Pitch('g')) 2554 >>> [str(p) for p in sc1.pitches] 2555 ['G4', 'A4', 'B-4', 'C5', 'D5', 'E-5', 'F5', 'G5'] 2556 2557 >>> sc2 = sc1.getParallelMajor() 2558 >>> [str(p) for p in sc2.pitches] 2559 ['G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F#5', 'G5'] 2560 ''' 2561 return MajorScale(self.tonic) 2562 2563 def getRelativeMinor(self): 2564 ''' 2565 Return a relative minor scale based on this concrete scale. 2566 2567 >>> sc1 = scale.MajorScale(pitch.Pitch('a')) 2568 >>> [str(p) for p in sc1.pitches] 2569 ['A4', 'B4', 'C#5', 'D5', 'E5', 'F#5', 'G#5', 'A5'] 2570 >>> sc2 = sc1.getRelativeMinor() 2571 >>> [str(p) for p in sc2.pitches] 2572 ['F#5', 'G#5', 'A5', 'B5', 'C#6', 'D6', 'E6', 'F#6'] 2573 ''' 2574 return MinorScale(self.pitchFromDegree(self.abstract.relativeMinorDegree)) 2575 2576 def getRelativeMajor(self): 2577 ''' 2578 Return a concrete relative major scale 2579 2580 >>> sc1 = scale.MinorScale(pitch.Pitch('g')) 2581 >>> [str(p) for p in sc1.pitches] 2582 ['G4', 'A4', 'B-4', 'C5', 'D5', 'E-5', 'F5', 'G5'] 2583 2584 >>> sc2 = sc1.getRelativeMajor() 2585 >>> [str(p) for p in sc2.pitches] 2586 ['B-4', 'C5', 'D5', 'E-5', 'F5', 'G5', 'A5', 'B-5'] 2587 2588 Though it's unlikely you would want to do it, 2589 `getRelativeMajor` works on other diatonic scales than 2590 just Major and Minor. 2591 2592 >>> sc2 = scale.DorianScale('d') 2593 >>> [str(p) for p in sc2.pitches] 2594 ['D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5', 'D5'] 2595 2596 >>> [str(p) for p in sc2.getRelativeMajor().pitches] 2597 ['C5', 'D5', 'E5', 'F5', 'G5', 'A5', 'B5', 'C6'] 2598 ''' 2599 return MajorScale(self.pitchFromDegree(self.abstract.relativeMajorDegree)) 2600 2601 2602# ------------------------------------------------------------------------------ 2603# diatonic scales and modes 2604class MajorScale(DiatonicScale): 2605 '''A Major Scale 2606 2607 >>> sc = scale.MajorScale(pitch.Pitch('d')) 2608 >>> sc.pitchFromDegree(7).name 2609 'C#' 2610 ''' 2611 2612 def __init__(self, tonic=None): 2613 super().__init__(tonic=tonic) 2614 self.type = 'major' 2615 # build the network for the appropriate scale 2616 self._abstract.buildNetwork(self.type) 2617 2618 # N.B. do not subclass methods, since generally RomanNumerals use Keys 2619 # and Key is a subclass of DiatonicScale not MajorScale or MinorScale 2620 2621 2622class MinorScale(DiatonicScale): 2623 '''A natural minor scale, or the Aeolian mode. 2624 2625 >>> sc = scale.MinorScale(pitch.Pitch('g')) 2626 >>> [str(p) for p in sc.pitches] 2627 ['G4', 'A4', 'B-4', 'C5', 'D5', 'E-5', 'F5', 'G5'] 2628 ''' 2629 2630 def __init__(self, tonic=None): 2631 super().__init__(tonic=tonic) 2632 self.type = 'minor' 2633 self._abstract.buildNetwork(self.type) 2634 2635 2636class DorianScale(DiatonicScale): 2637 ''' 2638 A scale built on the Dorian (D-D white-key) mode. 2639 2640 >>> sc = scale.DorianScale(pitch.Pitch('d')) 2641 >>> [str(p) for p in sc.pitches] 2642 ['D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5', 'D5'] 2643 ''' 2644 2645 def __init__(self, tonic=None): 2646 super().__init__(tonic=tonic) 2647 self.type = 'dorian' 2648 self._abstract.buildNetwork(self.type) 2649 2650 2651class PhrygianScale(DiatonicScale): 2652 '''A Phrygian scale (E-E white key) 2653 2654 >>> sc = scale.PhrygianScale(pitch.Pitch('e')) 2655 >>> [str(p) for p in sc.pitches] 2656 ['E4', 'F4', 'G4', 'A4', 'B4', 'C5', 'D5', 'E5'] 2657 ''' 2658 2659 def __init__(self, tonic=None): 2660 super().__init__(tonic=tonic) 2661 self.type = 'phrygian' 2662 self._abstract.buildNetwork(self.type) 2663 2664 2665class LydianScale(DiatonicScale): 2666 ''' 2667 A Lydian scale (that is, the F-F white-key scale; does not have the 2668 probability of B- emerging as in a historical Lydian collection). 2669 2670 >>> sc = scale.LydianScale(pitch.Pitch('f')) 2671 >>> [str(p) for p in sc.pitches] 2672 ['F4', 'G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F5'] 2673 2674 >>> sc = scale.LydianScale(pitch.Pitch('c')) 2675 >>> [str(p) for p in sc.pitches] 2676 ['C4', 'D4', 'E4', 'F#4', 'G4', 'A4', 'B4', 'C5'] 2677 ''' 2678 2679 def __init__(self, tonic=None): 2680 super().__init__(tonic=tonic) 2681 self.type = 'lydian' 2682 self._abstract.buildNetwork(self.type) 2683 2684 2685class MixolydianScale(DiatonicScale): 2686 '''A mixolydian scale 2687 2688 >>> sc = scale.MixolydianScale(pitch.Pitch('g')) 2689 >>> [str(p) for p in sc.pitches] 2690 ['G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F5', 'G5'] 2691 2692 >>> sc = scale.MixolydianScale(pitch.Pitch('c')) 2693 >>> [str(p) for p in sc.pitches] 2694 ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B-4', 'C5'] 2695 ''' 2696 2697 def __init__(self, tonic=None): 2698 super().__init__(tonic=tonic) 2699 self.type = 'mixolydian' 2700 self._abstract.buildNetwork(self.type) 2701 2702 2703class HypodorianScale(DiatonicScale): 2704 ''' 2705 A hypodorian scale: a dorian scale where the given pitch is scale degree 4. 2706 2707 >>> sc = scale.HypodorianScale(pitch.Pitch('d')) 2708 >>> [str(p) for p in sc.pitches] 2709 ['A3', 'B3', 'C4', 'D4', 'E4', 'F4', 'G4', 'A4'] 2710 >>> sc = scale.HypodorianScale(pitch.Pitch('c')) 2711 >>> [str(p) for p in sc.pitches] 2712 ['G3', 'A3', 'B-3', 'C4', 'D4', 'E-4', 'F4', 'G4'] 2713 ''' 2714 2715 def __init__(self, tonic=None): 2716 super().__init__(tonic=tonic) 2717 self.type = 'hypodorian' 2718 self._abstract.buildNetwork(self.type) 2719 2720 2721class HypophrygianScale(DiatonicScale): 2722 '''A hypophrygian scale 2723 2724 >>> sc = scale.HypophrygianScale(pitch.Pitch('e')) 2725 >>> sc.abstract.octaveDuplicating 2726 True 2727 >>> [str(p) for p in sc.pitches] 2728 ['B3', 'C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4'] 2729 >>> sc.getTonic() 2730 <music21.pitch.Pitch E4> 2731 >>> sc.getDominant() 2732 <music21.pitch.Pitch A4> 2733 >>> sc.pitchFromDegree(1) # scale degree 1 is treated as lowest 2734 <music21.pitch.Pitch B3> 2735 ''' 2736 def __init__(self, tonic=None): 2737 super().__init__(tonic=tonic) 2738 self.type = 'hypophrygian' 2739 self._abstract.buildNetwork(self.type) 2740 2741 2742class HypolydianScale(DiatonicScale): 2743 '''A hypolydian scale 2744 2745 >>> sc = scale.HypolydianScale(pitch.Pitch('f')) 2746 >>> [str(p) for p in sc.pitches] 2747 ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'] 2748 >>> sc = scale.HypolydianScale(pitch.Pitch('c')) 2749 >>> [str(p) for p in sc.pitches] 2750 ['G3', 'A3', 'B3', 'C4', 'D4', 'E4', 'F#4', 'G4'] 2751 ''' 2752 def __init__(self, tonic=None): 2753 super().__init__(tonic=tonic) 2754 self.type = 'hypolydian' 2755 self._abstract.buildNetwork(self.type) 2756 2757 2758class HypomixolydianScale(DiatonicScale): 2759 '''A hypomixolydian scale 2760 2761 >>> sc = scale.HypomixolydianScale(pitch.Pitch('g')) 2762 >>> [str(p) for p in sc.pitches] 2763 ['D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5', 'D5'] 2764 >>> sc = scale.HypomixolydianScale(pitch.Pitch('c')) 2765 >>> [str(p) for p in sc.pitches] 2766 ['G3', 'A3', 'B-3', 'C4', 'D4', 'E4', 'F4', 'G4'] 2767 ''' 2768 def __init__(self, tonic=None): 2769 super().__init__(tonic=tonic) 2770 self.type = 'hypomixolydian' 2771 self._abstract.buildNetwork(self.type) 2772 2773 2774class LocrianScale(DiatonicScale): 2775 '''A so-called "locrian" scale 2776 2777 >>> sc = scale.LocrianScale(pitch.Pitch('b')) 2778 >>> [str(p) for p in sc.pitches] 2779 ['B4', 'C5', 'D5', 'E5', 'F5', 'G5', 'A5', 'B5'] 2780 2781 >>> sc = scale.LocrianScale(pitch.Pitch('c')) 2782 >>> [str(p) for p in sc.pitches] 2783 ['C4', 'D-4', 'E-4', 'F4', 'G-4', 'A-4', 'B-4', 'C5'] 2784 ''' 2785 2786 def __init__(self, tonic=None): 2787 super().__init__(tonic=tonic) 2788 self.type = 'locrian' 2789 self._abstract.buildNetwork(self.type) 2790 2791 2792class HypolocrianScale(DiatonicScale): 2793 '''A hypolocrian scale 2794 2795 >>> sc = scale.HypolocrianScale(pitch.Pitch('b')) 2796 >>> [str(p) for p in sc.pitches] 2797 ['F4', 'G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F5'] 2798 2799 >>> sc = scale.HypolocrianScale(pitch.Pitch('c')) 2800 >>> [str(p) for p in sc.pitches] 2801 ['G-3', 'A-3', 'B-3', 'C4', 'D-4', 'E-4', 'F4', 'G-4'] 2802 ''' 2803 2804 def __init__(self, tonic=None): 2805 super().__init__(tonic=tonic) 2806 self.type = 'hypolocrian' 2807 self._abstract.buildNetwork(self.type) 2808 2809 2810class HypoaeolianScale(DiatonicScale): 2811 '''A hypoaeolian scale 2812 2813 >>> sc = scale.HypoaeolianScale(pitch.Pitch('a')) 2814 >>> [str(p) for p in sc.pitches] 2815 ['E4', 'F4', 'G4', 'A4', 'B4', 'C5', 'D5', 'E5'] 2816 2817 >>> sc = scale.HypoaeolianScale(pitch.Pitch('c')) 2818 >>> [str(p) for p in sc.pitches] 2819 ['G3', 'A-3', 'B-3', 'C4', 'D4', 'E-4', 'F4', 'G4'] 2820 ''' 2821 2822 def __init__(self, tonic=None): 2823 super().__init__(tonic=tonic) 2824 self.type = 'hypoaeolian' 2825 self._abstract.buildNetwork(self.type) 2826 2827 2828# ------------------------------------------------------------------------------ 2829# other diatonic scales 2830class HarmonicMinorScale(DiatonicScale): 2831 ''' 2832 The harmonic minor collection, realized as a scale. 2833 2834 (The usage of this collection as a scale, is quite ahistorical for 2835 Western European classical music, but it is common in other parts of the 2836 world, but where the term "HarmonicMinor" would not be appropriate). 2837 2838 >>> sc = scale.HarmonicMinorScale('e4') 2839 >>> [str(p) for p in sc.pitches] 2840 ['E4', 'F#4', 'G4', 'A4', 'B4', 'C5', 'D#5', 'E5'] 2841 >>> sc.getTonic() 2842 <music21.pitch.Pitch E4> 2843 >>> sc.getDominant() 2844 <music21.pitch.Pitch B4> 2845 >>> sc.pitchFromDegree(1) # scale degree 1 is treated as lowest 2846 <music21.pitch.Pitch E4> 2847 2848 >>> sc = scale.HarmonicMinorScale() 2849 >>> sc 2850 <music21.scale.HarmonicMinorScale Abstract harmonic minor> 2851 >>> sc.deriveRanked(['C', 'E', 'G'], comparisonAttribute='name') 2852 [(3, <music21.scale.HarmonicMinorScale F harmonic minor>), 2853 (3, <music21.scale.HarmonicMinorScale E harmonic minor>), 2854 (2, <music21.scale.HarmonicMinorScale B harmonic minor>), 2855 (2, <music21.scale.HarmonicMinorScale A harmonic minor>)] 2856 ''' 2857 2858 def __init__(self, tonic=None): 2859 super().__init__(tonic=tonic) 2860 self.type = 'harmonic minor' 2861 2862 # note: this changes the previously assigned AbstractDiatonicScale 2863 # from the DiatonicScale base class 2864 2865 self._abstract = AbstractHarmonicMinorScale() 2866 # network building happens on object creation 2867 # self._abstract.buildNetwork() 2868 2869 2870class MelodicMinorScale(DiatonicScale): 2871 ''' 2872 A melodic minor scale, which is not the same ascending or descending 2873 2874 >>> sc = scale.MelodicMinorScale('e4') 2875 ''' 2876 2877 def __init__(self, tonic=None): 2878 super().__init__(tonic=tonic) 2879 self.type = 'melodic minor' 2880 2881 # note: this changes the previously assigned AbstractDiatonicScale 2882 # from the DiatonicScale base class 2883 self._abstract = AbstractMelodicMinorScale() 2884 2885 2886# ------------------------------------------------------------------------------ 2887# other scales 2888 2889class OctatonicScale(ConcreteScale): 2890 ''' 2891 A concrete Octatonic scale in one of two modes 2892 ''' 2893 usePitchDegreeCache = True 2894 2895 def __init__(self, tonic=None, mode=None): 2896 super().__init__(tonic=tonic) 2897 self._abstract = AbstractOctatonicScale(mode=mode) 2898 self.type = 'Octatonic' 2899 2900 2901class OctaveRepeatingScale(ConcreteScale): 2902 ''' 2903 A concrete cyclical scale, based on a cycle of intervals. 2904 2905 2906 >>> sc = scale.OctaveRepeatingScale('c4', ['m3', 'M3']) 2907 >>> sc.pitches 2908 [<music21.pitch.Pitch C4>, <music21.pitch.Pitch E-4>, 2909 <music21.pitch.Pitch G4>, <music21.pitch.Pitch C5>] 2910 >>> [str(p) for p in sc.getPitches('g2', 'g6')] 2911 ['G2', 'C3', 'E-3', 'G3', 'C4', 'E-4', 'G4', 'C5', 'E-5', 'G5', 'C6', 'E-6', 'G6'] 2912 >>> sc.getScaleDegreeFromPitch('c4') 2913 1 2914 >>> sc.getScaleDegreeFromPitch('e-') 2915 2 2916 2917 No `intervalList` defaults to a single minor second: 2918 2919 >>> sc2 = scale.OctaveRepeatingScale() 2920 >>> sc2.pitches 2921 [<music21.pitch.Pitch C4>, <music21.pitch.Pitch D-4>, <music21.pitch.Pitch C5>] 2922 ''' 2923 2924 def __init__(self, tonic=None, intervalList: Optional[List] = None): 2925 super().__init__(tonic=tonic) 2926 mode = intervalList if intervalList else ['m2'] 2927 self._abstract = AbstractOctaveRepeatingScale(mode=mode) 2928 self.type = 'Octave Repeating' 2929 2930 2931class CyclicalScale(ConcreteScale): 2932 ''' 2933 A concrete cyclical scale, based on a cycle of intervals. 2934 2935 >>> sc = scale.CyclicalScale('c4', 'p5') # can give one list 2936 >>> sc.pitches 2937 [<music21.pitch.Pitch C4>, <music21.pitch.Pitch G4>] 2938 >>> [str(p) for p in sc.getPitches('g2', 'g6')] 2939 ['B-2', 'F3', 'C4', 'G4', 'D5', 'A5', 'E6'] 2940 >>> sc.getScaleDegreeFromPitch('g4') # as single interval cycle, all are 1 2941 1 2942 >>> sc.getScaleDegreeFromPitch('b-2', direction='bi') 2943 1 2944 2945 No `intervalList` defaults to a single minor second: 2946 2947 >>> sc2 = scale.CyclicalScale() 2948 >>> sc2.pitches 2949 [<music21.pitch.Pitch C4>, <music21.pitch.Pitch D-4>] 2950 ''' 2951 2952 def __init__(self, tonic=None, intervalList: Optional[List] = None): 2953 super().__init__(tonic=tonic) 2954 mode = intervalList if intervalList else ['m2'] 2955 self._abstract = AbstractCyclicalScale(mode=mode) 2956 self.type = 'Cyclical' 2957 2958 2959class ChromaticScale(ConcreteScale): 2960 ''' 2961 A concrete cyclical scale, based on a cycle of half steps. 2962 2963 2964 >>> sc = scale.ChromaticScale('g2') 2965 >>> [str(p) for p in sc.pitches] 2966 ['G2', 'A-2', 'A2', 'B-2', 'B2', 'C3', 'C#3', 'D3', 'E-3', 'E3', 'F3', 'F#3', 'G3'] 2967 >>> [str(p) for p in sc.getPitches('g2', 'g6')] 2968 ['G2', 'A-2', ..., 'F#3', 'G3', 'A-3', ..., 'F#4', 'G4', 'A-4', ..., 'G5', ..., 'F#6', 'G6'] 2969 >>> sc.abstract.getDegreeMaxUnique() 2970 12 2971 >>> sc.pitchFromDegree(1) 2972 <music21.pitch.Pitch G2> 2973 >>> sc.pitchFromDegree(2) 2974 <music21.pitch.Pitch A-2> 2975 >>> sc.pitchFromDegree(3) 2976 <music21.pitch.Pitch A2> 2977 >>> sc.pitchFromDegree(8) 2978 <music21.pitch.Pitch D3> 2979 >>> sc.pitchFromDegree(12) 2980 <music21.pitch.Pitch F#3> 2981 >>> sc.getScaleDegreeFromPitch('g2', comparisonAttribute='pitchClass') 2982 1 2983 >>> sc.getScaleDegreeFromPitch('F#6', comparisonAttribute='pitchClass') 2984 12 2985 ''' 2986 usePitchDegreeCache = True 2987 2988 def __init__(self, tonic=None): 2989 super().__init__(tonic=tonic) 2990 self._abstract = AbstractCyclicalScale(mode=[ 2991 'm2', 'm2', 'm2', 2992 'm2', 'm2', 'm2', 'm2', 'm2', 'm2', 'm2', 'm2', 'm2']) 2993 self._abstract._net.pitchSimplification = 'mostCommon' 2994 self.type = 'Chromatic' 2995 2996 2997class WholeToneScale(ConcreteScale): 2998 '''A concrete whole-tone scale. 2999 3000 3001 >>> sc = scale.WholeToneScale('g2') 3002 >>> [str(p) for p in sc.pitches] 3003 ['G2', 'A2', 'B2', 'C#3', 'D#3', 'E#3', 'G3'] 3004 >>> [str(p) for p in sc.getPitches('g2', 'g5')] 3005 ['G2', 'A2', 'B2', 'C#3', 'D#3', 'E#3', 'G3', 'A3', 'B3', 'C#4', 3006 'D#4', 'E#4', 'G4', 'A4', 'B4', 'C#5', 'D#5', 'E#5', 'G5'] 3007 >>> sc.abstract.getDegreeMaxUnique() 3008 6 3009 >>> sc.pitchFromDegree(1) 3010 <music21.pitch.Pitch G2> 3011 >>> sc.pitchFromDegree(2) 3012 <music21.pitch.Pitch A2> 3013 >>> sc.pitchFromDegree(6) 3014 <music21.pitch.Pitch E#3> 3015 >>> sc.getScaleDegreeFromPitch('g2', comparisonAttribute='pitchClass') 3016 1 3017 >>> sc.getScaleDegreeFromPitch('F6', comparisonAttribute='pitchClass') 3018 6 3019 ''' 3020 usePitchDegreeCache = True 3021 3022 def __init__(self, tonic=None): 3023 super().__init__(tonic=tonic) 3024 self._abstract = AbstractCyclicalScale(mode=['M2', 'M2', 'M2', 'M2', 'M2', 'M2']) 3025 self.type = 'Whole tone' 3026 3027 3028class SieveScale(ConcreteScale): 3029 ''' 3030 A scale created from a Xenakis sieve logical string, based on the 3031 :class:`~music21.sieve.Sieve` object definition. The complete period of the 3032 sieve is realized as intervals and used to create a scale. 3033 3034 3035 >>> sc = scale.SieveScale('c4', '3@0') 3036 >>> sc.pitches 3037 [<music21.pitch.Pitch C4>, <music21.pitch.Pitch E-4>] 3038 >>> sc = scale.SieveScale('d4', '3@0') 3039 >>> sc.pitches 3040 [<music21.pitch.Pitch D4>, <music21.pitch.Pitch F4>] 3041 >>> sc = scale.SieveScale('c2', '(-3@2 & 4) | (-3@1 & 4@1) | (3@2 & 4@2) | (-3 & 4@3)') 3042 >>> [str(p) for p in sc.pitches] 3043 ['C2', 'D2', 'E2', 'F2', 'G2', 'A2', 'B2', 'C3'] 3044 3045 3046 OMIT_FROM_DOCS 3047 3048 Test that an empty SieveScale can be created... 3049 3050 >>> sc = scale.SieveScale() 3051 ''' 3052 3053 def __init__(self, 3054 tonic=None, 3055 sieveString='2@0', 3056 eld: Union[int, float] = 1): 3057 super().__init__(tonic=tonic) 3058 3059 # self.tonic is a Pitch 3060 if self.tonic is not None: 3061 tonic = self.tonic 3062 else: 3063 tonic = pitch.Pitch('C4') 3064 self._pitchSieve = sieve.PitchSieve(sieveString, 3065 pitchLower=str(tonic), 3066 pitchUpper=str(tonic.transpose(48)), eld=eld) 3067 # four octave default 3068 3069 # environLocal.printDebug([self._pitchSieve.sieveObject.represent(), 3070 # self._pitchSieve.getIntervalSequence()]) 3071 # mode here is a list of intervals 3072 intervalSequence = self._pitchSieve.getIntervalSequence() 3073 self._abstract = AbstractCyclicalScale(mode=intervalSequence) 3074 self.type = 'Sieve' 3075 3076 3077class ScalaScale(ConcreteScale): 3078 ''' 3079 A scale created from a Scala scale .scl file. Any file 3080 in the Scala archive can be given by name. Additionally, a file 3081 path to a Scala .scl file, or a raw string representation, can be used. 3082 3083 3084 >>> sc = scale.ScalaScale('g4', 'mbira banda') 3085 >>> [str(p) for p in sc.pitches] 3086 ['G4', 'A4(-15c)', 'B4(-11c)', 'C#5(-7c)', 'D~5(+6c)', 'E5(+14c)', 'F~5(+1c)', 'G#5(+2c)'] 3087 3088 3089 if only a single string is given and it's too long to be a tonic 3090 or it ends in .scl, assume it's the name of a scala scale and 3091 set the tonic to C4 3092 3093 3094 >>> sc = scale.ScalaScale('pelog_9') 3095 >>> [str(p) for p in sc.pitches] 3096 ['C4', 'C#~4(-17c)', 'D~4(+17c)', 'F~4(-17c)', 'F#~4(+17c)', 'G#4(-0c)', 'A~4(-17c)', 'C5(-0c)'] 3097 3098 3099 If no scale with that name can be found then it raises an exception: 3100 3101 3102 >>> sc = scale.ScalaScale('badFileName.scl') 3103 Traceback (most recent call last): 3104 music21.scale.ScaleException: Could not find a file named badFileName.scl in the scala database 3105 ''' 3106 3107 def __init__(self, tonic=None, scalaString=None): 3108 if (tonic is not None 3109 and scalaString is None 3110 and isinstance(tonic, str) 3111 and (len(tonic) >= 4 3112 or tonic.endswith('scl'))): 3113 # just a scale was wanted 3114 scalaString = tonic 3115 tonic = 'C4' 3116 3117 super().__init__(tonic=tonic) 3118 3119 self._scalaData = None 3120 self.description = None 3121 3122 # this might be a raw scala file list 3123 if scalaString is not None and scalaString.count('\n') > 3: 3124 # if no match, this might be a complete Scala string 3125 self._scalaData = scala.ScalaData(scalaString) 3126 self._scalaData.parse() 3127 elif scalaString is not None: 3128 # try to load a named scale from a file path or stored 3129 # on the scala archive 3130 # returns None or a scala storage object 3131 readFile = scala.parse(scalaString) 3132 if readFile is None: 3133 raise ScaleException( 3134 f'Could not find a file named {scalaString} in the scala database') 3135 self._scalaData = readFile 3136 else: # grab a default 3137 self._scalaData = scala.parse('fj-12tet.scl') 3138 3139 intervalSequence = self._scalaData.getIntervalSequence() 3140 self._abstract = AbstractCyclicalScale(mode=intervalSequence) 3141 self._abstract._net.pitchSimplification = 'mostCommon' 3142 self.type = f'Scala: {self._scalaData.fileName}' 3143 self.description = self._scalaData.description 3144 3145 3146class RagAsawari(ConcreteScale): 3147 ''' 3148 A concrete pseudo-raga scale. 3149 3150 >>> sc = scale.RagAsawari('c2') 3151 >>> [str(p) for p in sc.pitches] 3152 ['C2', 'D2', 'F2', 'G2', 'A-2', 'C3'] 3153 >>> [str(p) for p in sc.getPitches(direction='descending')] 3154 ['C3', 'B-2', 'A-2', 'G2', 'F2', 'E-2', 'D2', 'C2'] 3155 ''' 3156 3157 def __init__(self, tonic=None): 3158 super().__init__(tonic=tonic) 3159 self._abstract = AbstractRagAsawari() 3160 self.type = 'Rag Asawari' 3161 3162 3163class RagMarwa(ConcreteScale): 3164 ''' 3165 A concrete pseudo-raga scale. 3166 3167 >>> sc = scale.RagMarwa('c2') 3168 3169 this gets a pitch beyond the terminus b/c of descending form max 3170 3171 >>> [str(p) for p in sc.pitches] 3172 ['C2', 'D-2', 'E2', 'F#2', 'A2', 'B2', 'A2', 'C3', 'D-3'] 3173 ''' 3174 3175 def __init__(self, tonic=None): 3176 super().__init__(tonic=tonic) 3177 self._abstract = AbstractRagMarwa() 3178 self.type = 'Rag Marwa' 3179 # >>> sc.getPitches(direction='descending') 3180 # [C2, D2, E2, G2, A2, C3] 3181 3182 3183class WeightedHexatonicBlues(ConcreteScale): 3184 ''' 3185 A concrete scale based on a dynamic mixture of a minor pentatonic 3186 and the hexatonic blues scale. 3187 ''' 3188 3189 def __init__(self, tonic=None): 3190 super().__init__(tonic=tonic) 3191 self._abstract = AbstractWeightedHexatonicBlues() 3192 self.type = 'Weighted Hexatonic Blues' 3193 3194 3195# ------------------------------------------------------------------------------ 3196class Test(unittest.TestCase): 3197 3198 def pitchOut(self, listIn): 3199 out = '[' 3200 for p in listIn: 3201 out += str(p) + ', ' 3202 out = out[0:len(out) - 2] 3203 out += ']' 3204 return out 3205 3206 def testBasicLegacy(self): 3207 from music21 import scale 3208 3209 n1 = note.Note() 3210 3211 cMajor = scale.MajorScale(n1) 3212 3213 self.assertEqual(cMajor.name, 'C major') 3214 self.assertEqual(cMajor.getPitches()[6].step, 'B') 3215 3216 seventh = cMajor.pitchFromDegree(7) 3217 self.assertEqual(seventh.step, 'B') 3218 3219 dom = cMajor.getDominant() 3220 self.assertEqual(dom.step, 'G') 3221 3222 n2 = note.Note() 3223 n2.step = 'A' 3224 3225 aMinor = cMajor.getRelativeMinor() 3226 self.assertEqual(aMinor.name, 'A minor', 'Got a different name: ' + aMinor.name) 3227 3228 notes = [note1.name for note1 in aMinor.getPitches()] 3229 self.assertEqual(notes, ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'A']) 3230 3231 n3 = note.Note() 3232 n3.name = 'B-' 3233 n3.octave = 5 3234 3235 bFlatMinor = scale.MinorScale(n3) 3236 self.assertEqual(bFlatMinor.name, 'B- minor', 'Got a different name: ' + bFlatMinor.name) 3237 notes2 = [note1.name for note1 in bFlatMinor.getPitches()] 3238 self.assertEqual(notes2, ['B-', 'C', 'D-', 'E-', 'F', 'G-', 'A-', 'B-']) 3239 self.assertEqual(bFlatMinor.getPitches()[0], n3.pitch) 3240 self.assertEqual(bFlatMinor.getPitches()[6].octave, 6) 3241 3242 # harmonic = bFlatMinor.getConcreteHarmonicMinorScale() 3243 # niceHarmonic = [note1.name for note1 in harmonic] 3244 # self.assertEqual(niceHarmonic, ['B-', 'C', 'D-', 'E-', 'F', 'G-', 'A', 'B-']) 3245 # 3246 # harmonic2 = bFlatMinor.getAbstractHarmonicMinorScale() 3247 # self.assertEqual([note1.name for note1 in harmonic2], niceHarmonic) 3248 # for note1 in harmonic2: 3249 # self.assertEqual(note1.octave, 0) 3250 # 3251 # melodic = bFlatMinor.getConcreteMelodicMinorScale() 3252 # niceMelodic = [note1.name for note1 in melodic] 3253 # self.assertEqual(niceMelodic, 3254 # ['B-', 'C', 'D-', 'E-', 'F', 'G', 'A', 'B-', 'A-', 'G-', 3255 # 'F', 'E-', 'D-', 'C', 'B-']) 3256 3257 # melodic2 = bFlatMinor.getAbstractMelodicMinorScale() 3258 # self.assertEqual([note1.name for note1 in melodic2], niceMelodic) 3259 # for note1 in melodic2: 3260 # self.assertEqual(note1.octave, 0) 3261 3262 cNote = bFlatMinor.pitchFromDegree(2) 3263 self.assertEqual(cNote.name, 'C') 3264 fNote = bFlatMinor.getDominant() 3265 self.assertEqual(fNote.name, 'F') 3266 3267 bFlatMajor = bFlatMinor.getParallelMajor() 3268 self.assertEqual(bFlatMajor.name, 'B- major') 3269 # scale = [note1.name for note1 in bFlatMajor.getConcreteMajorScale()] 3270 # self.assertEqual(scale, ['B-', 'C', 'D', 'E-', 'F', 'G', 'A', 'B-']) 3271 3272 dFlatMajor = bFlatMinor.getRelativeMajor() 3273 self.assertEqual(dFlatMajor.name, 'D- major') 3274 self.assertEqual(dFlatMajor.getTonic().name, 'D-') 3275 self.assertEqual(dFlatMajor.getDominant().name, 'A-') 3276 3277 def testBasic(self): 3278 from music21 import corpus 3279 from music21 import stream 3280 from music21 import scale 3281 # deriving a scale from a Stream 3282 3283 # just get default, c-minor, as derive will check all tonics 3284 sc2 = scale.MinorScale() 3285 3286 # we can get a range of pitches 3287 self.assertEqual(self.pitchOut(sc2.getPitches('c2', 'c5')), 3288 '[C2, D2, E-2, F2, G2, A-2, B-2, C3, D3, E-3, F3, G3, A-3, B-3, ' 3289 + 'C4, D4, E-4, F4, G4, A-4, B-4, C5]') 3290 3291 # we can transpose the Scale 3292 sc3 = sc2.transpose('-m3') 3293 self.assertEqual(self.pitchOut(sc3.getPitches('c2', 'c5')), 3294 '[C2, D2, E2, F2, G2, A2, B2, C3, D3, E3, F3, G3, A3, B3, ' 3295 + 'C4, D4, E4, F4, G4, A4, B4, C5]') 3296 3297 # getting pitches from scale degrees 3298 self.assertEqual(str(sc3.pitchFromDegree(3)), 'C4') 3299 self.assertEqual(str(sc3.pitchFromDegree(7)), 'G4') 3300 self.assertEqual(self.pitchOut(sc3.pitchesFromScaleDegrees([1, 5, 6])), 3301 '[A3, E4, F4, A4]') 3302 self.assertEqual( 3303 self.pitchOut( 3304 sc3.pitchesFromScaleDegrees( 3305 [2, 3], 3306 minPitch='c6', 3307 maxPitch='c9' 3308 ) 3309 ), 3310 '[C6, B6, C7, B7, C8, B8, C9]') 3311 3312 # given a pitch, get the scale degree 3313 sc4 = scale.MajorScale('A-') 3314 self.assertEqual(sc4.getScaleDegreeFromPitch('a-'), 1) 3315 # default is name matching 3316 self.assertEqual(sc4.getScaleDegreeFromPitch('g#'), None) 3317 # can set pitchClass comparison attribute 3318 self.assertEqual(sc4.getScaleDegreeFromPitch('g#', 3319 comparisonAttribute='pitchClass'), 1) 3320 self.assertEqual(sc4.getScaleDegreeFromPitch('e-', 3321 comparisonAttribute='name'), 5) 3322 3323 # showing scales 3324 # this assumes that the tonic is not the first scale degree 3325 sc1 = scale.HypophrygianScale('c4') 3326 self.assertEqual(str(sc1.pitchFromDegree(1)), 'G3') 3327 self.assertEqual(str(sc1.pitchFromDegree(4)), 'C4') 3328 # sc1.show() 3329 3330 sc1 = scale.MajorScale() 3331 # deriving a new scale from the pitches found in a collection 3332 s = corpus.parse('bwv66.6') 3333 sc3 = sc1.derive(s.parts['soprano']) 3334 self.assertEqual(str(sc3), '<music21.scale.MajorScale A major>') 3335 3336 sc3 = sc1.derive(s.parts['tenor']) 3337 self.assertEqual(str(sc3), '<music21.scale.MajorScale A major>') 3338 3339 sc3 = sc2.derive(s.parts['bass']) 3340 self.assertEqual(str(sc3), '<music21.scale.MinorScale F# minor>') 3341 3342 # composing with a scale 3343 s = stream.Stream() 3344 p = 'd#4' 3345 # sc = PhrygianScale('e') 3346 sc = MajorScale('E4') 3347 for d, x in [('ascending', 1), ('descending', 2), ('ascending', 3), 3348 ('descending', 4), ('ascending', 3), ('descending', 2), 3349 ('ascending', 1)]: 3350 # use duration type instead of quarter length 3351 for y in (1, 0.5, 0.5, 0.25, 0.25, 0.25, 0.25): 3352 p = sc.next(p, direction=d, stepSize=x) 3353 n = note.Note(p) 3354 n.quarterLength = y 3355 s.append(n) 3356 self.assertEqual(self.pitchOut(s.pitches), 3357 '[E4, F#4, G#4, A4, B4, C#5, D#5, B4, G#4, E4, C#4, A3, F#3, D#3, ' 3358 + 'G#3, C#4, F#4, B4, E5, A5, D#6, G#5, C#5, F#4, B3, E3, A2, D#2, G#2, ' 3359 + 'C#3, F#3, B3, E4, A4, D#5, B4, G#4, E4, C#4, A3, F#3, D#3, E3, F#3, ' 3360 + 'G#3, A3, B3, C#4, D#4]') 3361 # s.show() 3362 3363 # composing with an octatonic scale. 3364 s1 = stream.Part() 3365 s2 = stream.Part() 3366 p1 = 'b4' 3367 p2 = 'b3' 3368 sc = OctatonicScale('C4') 3369 for d, x in [('ascending', 1), ('descending', 2), ('ascending', 3), 3370 ('descending', 2), ('ascending', 1)]: 3371 for y in (1, 0.5, 0.25, 0.25): 3372 p1 = sc.next(p1, direction=d, stepSize=x) 3373 n = note.Note(p1) 3374 n.quarterLength = y 3375 s1.append(n) 3376 if d == 'ascending': 3377 d = 'descending' 3378 elif d == 'descending': 3379 d = 'ascending' 3380 for y in [1, 0.5, 0.25, 0.25]: 3381 p2 = sc.next(p2, direction=d, stepSize=x) 3382 n = note.Note(p2) 3383 n.quarterLength = y 3384 s2.append(n) 3385 s = stream.Score() 3386 s.insert(0, s1) 3387 s.insert(0, s2) 3388 # s.show() 3389 3390 # compare two different major scales 3391 sc1 = MajorScale('g') 3392 sc2 = MajorScale('a') 3393 sc3 = MinorScale('f#') 3394 # exact comparisons 3395 self.assertNotEqual(sc1, sc2) 3396 self.assertEqual(sc1.abstract, sc2.abstract) 3397 self.assertNotEqual(sc1, sc3) 3398 self.assertNotEqual(sc1.abstract, sc3.abstract) 3399 from pprint import pformat 3400 # getting details on comparison 3401 self.assertEqual(pformat(sc1.match(sc2)), '''{'matched': [<music21.pitch.Pitch A4>, 3402 <music21.pitch.Pitch B4>, 3403 <music21.pitch.Pitch D5>, 3404 <music21.pitch.Pitch E5>, 3405 <music21.pitch.Pitch F#5>], 3406 'notMatched': [<music21.pitch.Pitch C#5>, <music21.pitch.Pitch G#5>]}''', pformat(sc1.match(sc2))) 3407 3408 def testCyclicalScales(self): 3409 sc = CyclicalScale('c4', ['m2', 'm2']) 3410 3411 # we get spelling based on maxAccidental parameter 3412 self.assertEqual(self.pitchOut(sc.getPitches('g4', 'g6')), 3413 '[G4, A-4, A4, B-4, C-5, C5, D-5, D5, E-5, F-5, F5, G-5, ' 3414 + 'G5, A-5, A5, B-5, C-6, C6, D-6, D6, E-6, F-6, F6, G-6, G6]') 3415 3416 # these values are different because scale degree 1 has different 3417 # pitches in different registers, as this is a non-octave repeating 3418 # scale 3419 3420 self.assertEqual(sc.abstract.getDegreeMaxUnique(), 2) 3421 3422 self.assertEqual(str(sc.pitchFromDegree(1)), 'C4') 3423 self.assertEqual(str(sc.pitchFromDegree(1, 'c2', 'c3')), 'B#1') 3424 3425 # scale storing parameters 3426 # how to get a spelling in different ways 3427 # ex: octatonic should always compare on pitchClass 3428 3429 # a very short cyclical scale 3430 sc = CyclicalScale('c4', 'p5') # can give one list 3431 self.assertEqual(self.pitchOut(sc.pitches), '[C4, G4]') 3432 3433 self.assertEqual(self.pitchOut(sc.getPitches('g2', 'g6')), '[B-2, F3, C4, G4, D5, A5, E6]') 3434 3435 # as single interval cycle, all are 1 3436 # environLocal.printDebug(['calling get scale degree from pitch']) 3437 self.assertEqual(sc.getScaleDegreeFromPitch('g4'), 1) 3438 self.assertEqual(sc.getScaleDegreeFromPitch('b-2', 3439 direction=DIRECTION_ASCENDING), 1) 3440 3441 # test default args 3442 sc2 = CyclicalScale() 3443 self.assertEqual(self.pitchOut(sc2.getPitches()), '[C4, D-4]') 3444 3445 def testDeriveByDegree(self): 3446 from music21 import scale # to get correct reprs 3447 sc1 = scale.MajorScale() 3448 self.assertEqual(str(sc1.deriveByDegree(7, 'G#')), 3449 '<music21.scale.MajorScale A major>') 3450 3451 sc1 = scale.HarmonicMinorScale() 3452 # what scale has g# as its 7th degree 3453 self.assertEqual(str(sc1.deriveByDegree(7, 'G#')), 3454 '<music21.scale.HarmonicMinorScale A harmonic minor>') 3455 self.assertEqual(str(sc1.deriveByDegree(2, 'E')), 3456 '<music21.scale.HarmonicMinorScale D harmonic minor>') 3457 3458 # TODO(CA): add serial rows as scales 3459 3460 # # This test does not yet work. 3461 # def testDeriveByDegreeBiDirectional(self): 3462 # sc1 = MelodicMinorScale() 3463 # sc1.deriveByDegree(6, 'G') 3464 3465 3466 3467 def testMelodicMinorA(self): 3468 mm = MelodicMinorScale('a') 3469 self.assertEqual(self.pitchOut(mm.pitches), '[A4, B4, C5, D5, E5, F#5, G#5, A5]') 3470 3471 self.assertEqual(self.pitchOut(mm.getPitches(direction='ascending')), 3472 '[A4, B4, C5, D5, E5, F#5, G#5, A5]') 3473 3474 self.assertEqual(self.pitchOut(mm.getPitches('c1', 'c3', direction='descending')), 3475 '[C3, B2, A2, G2, F2, E2, D2, C2, B1, A1, G1, F1, E1, D1, C1]') 3476 3477 # TODO: this shows a problem with a bidirectional scale: we are 3478 # always starting at the tonic and moving up or down; so this is still 3479 # giving a descended portion, even though an ascending portion was requested 3480 self.assertEqual(self.pitchOut(mm.getPitches('c1', 'c3', direction='ascending')), 3481 '[C1, D1, E1, F#1, G#1, A1, B1, C2, D2, E2, F#2, G#2, A2, B2, C3]') 3482 3483 self.assertEqual(self.pitchOut(mm.getPitches('c1', 'c3', direction='descending')), 3484 '[C1, D1, E1, F1, G1, A1, B1, C2, D2, E2, F2, G2, A2, B2, C3]') 3485 3486 self.assertEqual(self.pitchOut(mm.getPitches('a5', 'a6', direction='ascending')), 3487 '[A5, B5, C6, D6, E6, F#6, G#6, A6]') 3488 3489 self.assertEqual(self.pitchOut(mm.getPitches('a5', 'a6', direction='descending')), 3490 '[A6, G6, F6, E6, D6, C6, B5, A5]') 3491 3492 self.assertEqual(mm.getScaleDegreeFromPitch('a3'), 1) 3493 self.assertEqual(mm.getScaleDegreeFromPitch('b3'), 2) 3494 3495 # ascending, by default, has 7th scale degree as g# 3496 self.assertEqual(mm.getScaleDegreeFromPitch('g#3'), 7) 3497 3498 # in descending, G# is not present 3499 self.assertEqual(mm.getScaleDegreeFromPitch('g#3', direction='descending'), None) 3500 3501 # but, g is 3502 self.assertEqual(mm.getScaleDegreeFromPitch('g3', direction='descending'), 7) 3503 3504 # the bi directional representation has a version of each instance 3505 # merged 3506 self.assertEqual(self.pitchOut(mm.getPitches('a4', 'a5', direction='bi')), 3507 '[A4, B4, C5, D5, E5, F#5, F5, G#5, G5, A5]') 3508 3509 # in a bi-directional representation, both g and g# are will return 3510 # scale degree 7 3511 self.assertEqual(mm.getScaleDegreeFromPitch('g8', direction='bi'), 7) 3512 self.assertEqual(mm.getScaleDegreeFromPitch('g#1', direction='bi'), 7) 3513 self.assertEqual(mm.getScaleDegreeFromPitch('f8', direction='bi'), 6) 3514 self.assertEqual(mm.getScaleDegreeFromPitch('f#1', direction='bi'), 6) 3515 3516 self.assertEqual(mm.next('e5', 'ascending').nameWithOctave, 'F#5') 3517 # self.assertEqual(mm.next('f#5', 'ascending').nameWithOctave, 'F#5') 3518 3519 self.assertEqual(mm.next('e5', 'descending').nameWithOctave, 'D5') 3520 3521 self.assertEqual(mm.next('g#2', 'ascending').nameWithOctave, 'A2') 3522 # self.assertEqual(mm.next('g2', 'descending').nameWithOctave, 'f2') 3523 3524 def testMelodicMinorB(self): 3525 '''Need to test descending form of getting pitches with no defined min and max 3526 ''' 3527 from music21 import stream 3528 mm = MelodicMinorScale('a') 3529 # self.assertEqual(str(mm.getPitches(None, None, direction='ascending')), 3530 # '[A4, B4, C5, D5, E5, F#5, G#5, A5]') 3531 3532 self.assertEqual(mm.pitchFromDegree(2, direction='ascending').nameWithOctave, 'B4') 3533 3534 self.assertEqual(mm.pitchFromDegree(5, direction='ascending').nameWithOctave, 'E5') 3535 3536 self.assertEqual(mm.pitchFromDegree(6, direction='ascending').nameWithOctave, 'F#5') 3537 3538 self.assertEqual(mm.pitchFromDegree(6, direction='descending').nameWithOctave, 'F5') 3539 3540 # todo: this is ambiguous case 3541 # self.assertEqual(mm.pitchFromDegree(6, direction='bi').nameWithOctave, 'F5') 3542 3543 self.assertEqual(self.pitchOut(mm.getPitches(None, None, direction='descending')), 3544 '[A5, G5, F5, E5, D5, C5, B4, A4]') 3545 self.assertEqual(self.pitchOut(mm.getPitches(None, None, direction='ascending')), 3546 '[A4, B4, C5, D5, E5, F#5, G#5, A5]') 3547 3548 self.assertEqual(str(mm.next('a3', 'ascending')), 'B3') 3549 3550 self.assertEqual(str(mm.next('f#5', 'ascending')), 'G#5') 3551 self.assertEqual(str(mm.next('G#5', 'ascending')), 'A5') 3552 3553 self.assertEqual(str(mm.next('f5', 'descending')), 'E5') 3554 self.assertEqual(str(mm.next('G5', 'descending')), 'F5') 3555 self.assertEqual(str(mm.next('A5', 'descending')), 'G5') 3556 3557 self.assertEqual(str(mm.next('f#5', 'descending')), 'F5') 3558 self.assertEqual(str(mm.next('f#5', 'descending', 3559 getNeighbor='descending')), 'E5') 3560 3561 self.assertEqual(str(mm.next('f5', 'ascending')), 'F#5') 3562 self.assertEqual(str(mm.next('f5', 'ascending', 3563 getNeighbor='descending')), 'F#5') 3564 3565 # composing with a scale 3566 s = stream.Stream() 3567 p = 'f#3' 3568 # sc = PhrygianScale('e') 3569 sc = MelodicMinorScale('g4') 3570 for direction in range(8): 3571 if direction % 2 == 0: 3572 d = 'ascending' 3573 qls = [1, 0.5, 0.5, 2, 0.25, 0.25, 0.25, 0.25] 3574 else: 3575 d = 'descending' 3576 qls = [1, 0.5, 1, 0.5, 0.5] 3577 for y in qls: 3578 p = sc.next(p, direction=d, stepSize=1) 3579 n = note.Note(p) 3580 n.quarterLength = y 3581 s.append(n) 3582 s.makeAccidentals(inPlace=True) 3583 3584 self.assertEqual( 3585 self.pitchOut(s.pitches), 3586 '[G3, A3, B-3, C4, D4, E4, F#4, G4, F4, E-4, D4, C4, B-3, C4, D4, E4, F#4, ' 3587 + 'G4, A4, B-4, C5, B-4, A4, G4, F4, E-4, E4, F#4, G4, A4, B-4, C5, D5, E5, ' 3588 + 'E-5, D5, C5, B-4, A4, B-4, C5, D5, E5, F#5, G5, A5, B-5, A5, G5, F5, E-5, D5]') 3589 3590 # s.show() 3591 3592 def testPlagalModes(self): 3593 hs = HypophrygianScale('c4') 3594 self.assertEqual(self.pitchOut(hs.pitches), '[G3, A-3, B-3, C4, D-4, E-4, F4, G4]') 3595 self.assertEqual(str(hs.pitchFromDegree(1)), 'G3') 3596 3597 def testRagAsawari(self): 3598 sc = RagAsawari('c4') 3599 self.assertEqual(str(sc.pitchFromDegree(1)), 'C4') 3600 3601 # 3602 # ascending should be: [C2, D2, F2, G2, A-2, C3] 3603 3604 self.assertEqual(str(sc.next('c4', 'ascending')), 'D4') 3605 self.assertEqual(self.pitchOut(sc.pitches), '[C4, D4, F4, G4, A-4, C5]') 3606 # self.assertEqual(str(hs.pitchFromDegree(1)), 'G3') 3607 3608 self.assertEqual(self.pitchOut(sc.getPitches('c2', 'c4', direction='ascending')), 3609 '[C2, D2, F2, G2, A-2, C3, D3, F3, G3, A-3, C4]') 3610 3611 self.assertEqual(self.pitchOut(sc.getPitches('c2', 'c4', direction='descending')), 3612 '[C4, B-3, A-3, G3, F3, E-3, D3, C3, B-2, A-2, G2, F2, E-2, D2, C2]') 3613 3614 self.assertEqual(str(sc.next('c1', 'ascending')), 'D1') 3615 self.assertEqual(str(sc.next('d1', 'ascending')), 'F1') 3616 self.assertEqual(str(sc.next('f1', 'descending')), 'E-1') 3617 3618 self.assertEqual(str(sc.next('e-1', 'ascending', getNeighbor='descending')), 'F1') 3619 3620 self.assertEqual(str(sc.pitchFromDegree(1)), 'C1') 3621 # there is no third step in ascending form 3622 self.assertEqual(str(sc.pitchFromDegree(3)), 'None') 3623 self.assertEqual(str(sc.pitchFromDegree(3, direction='descending')), 'E-4') 3624 3625 self.assertEqual(str(sc.pitchFromDegree(7)), 'None') 3626 self.assertEqual(str(sc.pitchFromDegree(7, direction='descending')), 'B-4') 3627 3628 def testRagMarwaA(self): 3629 sc = RagMarwa('c4') 3630 self.assertEqual(str(sc.pitchFromDegree(1)), 'C4') 3631 3632 self.assertEqual(str(sc.next('c4', 'ascending')), 'D-4') 3633 3634 self.assertEqual(self.pitchOut(sc.pitches), '[C4, D-4, E4, F#4, A4, B4, A4, C5, D-5]') 3635 3636 self.assertEqual(self.pitchOut(sc.getPitches('c2', 'c3', direction='ascending')), 3637 '[C2, D-2, E2, F#2, A2, B2, A2, C3]') 3638 3639 self.assertEqual(self.pitchOut(sc.getPitches('c2', 'c4', direction='ascending')), 3640 '[C2, D-2, E2, F#2, A2, B2, A2, C3, D-3, E3, F#3, A3, B3, A3, C4]') 3641 3642 self.assertEqual(self.pitchOut(sc.getPitches('c3', 'd-4', direction='descending')), 3643 '[D-4, C4, D-4, B3, A3, F#3, E3, D-3, C3]') 3644 3645 # is this correct: this cuts off the d-4, as it is outside of the range 3646 self.assertEqual(self.pitchOut(sc.getPitches('c3', 'c4', direction='descending')), 3647 '[C4, B3, A3, F#3, E3, D-3, C3]') 3648 3649 self.assertEqual(str(sc.next('c1', 'ascending')), 'D-1') 3650 self.assertEqual(str(sc.next('d-1', 'ascending')), 'E1') 3651 self.assertEqual(str(sc.next('e1', 'ascending')), 'F#1') 3652 self.assertEqual(str(sc.next('f#1', 'ascending')), 'A1') 3653 # this is probabilistic 3654 # self.assertEqual(str(sc.next('A1', 'ascending')), 'B1') 3655 self.assertEqual(str(sc.next('B1', 'ascending')), 'A1') 3656 3657 self.assertEqual(str(sc.next('B2', 'descending')), 'A2') 3658 self.assertEqual(str(sc.next('A2', 'descending')), 'F#2') 3659 self.assertEqual(str(sc.next('E2', 'descending')), 'D-2') 3660 # this is correct! 3661 self.assertEqual(str(sc.next('C2', 'descending')), 'D-2') 3662 self.assertEqual(str(sc.next('D-2', 'ascending')), 'E2') 3663 3664 def testRagMarwaB(self): 3665 sc = RagMarwa('c4') 3666 3667 # for rag marwa, and given only the pitch a, the scale can move to 3668 # either b or c; this selection is determined by weighted random 3669 # selection. 3670 post = [] 3671 for unused_x in range(100): 3672 post.append(sc.getScaleDegreeFromPitch('A1', 'ascending')) 3673 self.assertGreater(post.count(5), 30) 3674 self.assertGreater(post.count(7), 30) 3675 3676 # for rag marwa, and given only the pitch d-, the scale can move to 3677 # either b or c; this selection is determined by weighted random 3678 # selection; can be 2 or 7 3679 post = [] 3680 for unused_x in range(100): 3681 post.append(sc.getScaleDegreeFromPitch('D-3', 'descending')) 3682 self.assertGreater(post.count(2), 30) 3683 self.assertGreater(post.count(7), 30) 3684 3685 def testRagMarwaC(self): 3686 sc = RagMarwa('c4') 3687 3688 self.assertEqual(sc.abstract._net.realizeTermini('c1', 'terminusLow'), 3689 (pitch.Pitch('C1'), pitch.Pitch('C2'))) 3690 3691 self.assertEqual(sc.abstract._net.realizeMinMax('c1', 'terminusLow'), 3692 (pitch.Pitch('C1'), pitch.Pitch('D-2'))) 3693 3694 # descending from d-2, we can either go to c2 or b1 3695 post = [] 3696 for unused_x in range(100): 3697 post.append(str(sc.next('D-2', 'descending'))) 3698 self.assertGreater(post.count('C2'), 30) 3699 self.assertGreater(post.count('B1'), 30) 3700 3701 def testWeightedHexatonicBluesA(self): 3702 sc = WeightedHexatonicBlues('c4') 3703 3704 i = 0 3705 j = 0 3706 for dummy in range(50): 3707 # over 50 iterations, it must be one of these two options 3708 match = self.pitchOut(sc.getPitches('c3', 'c4')) 3709 if match == '[C3, E-3, F3, G3, B-3, C4]': 3710 i += 1 3711 if match == '[C3, E-3, F3, F#3, G3, B-3, C4]': 3712 j += 1 3713 self.assertEqual(match in [ 3714 '[C3, E-3, F3, G3, B-3, C4]', 3715 '[C3, E-3, F3, F#3, G3, B-3, C4]'], 3716 True) 3717 # check that we got at least one; this may fail rarely 3718 self.assertGreaterEqual(i, 1) 3719 self.assertGreaterEqual(j, 1) 3720 3721 # test descending 3722 i = 0 3723 j = 0 3724 for dummy in range(50): 3725 # over 50 iterations, it must be one of these two options 3726 match = self.pitchOut(sc.getPitches('c3', 'c4', direction='descending')) 3727 if match == '[C4, B-3, G3, F3, E-3, C3]': 3728 i += 1 3729 if match == '[C4, B-3, G3, F#3, F3, E-3, C3]': 3730 j += 1 3731 self.assertEqual(match in [ 3732 '[C4, B-3, G3, F3, E-3, C3]', 3733 '[C4, B-3, G3, F#3, F3, E-3, C3]'], 3734 True) 3735 # check that we got at least one; this may fail rarely 3736 self.assertGreaterEqual(i, 1) 3737 self.assertGreaterEqual(j, 1) 3738 3739 self.assertEqual(str(sc.pitchFromDegree(1)), 'C4') 3740 self.assertEqual(str(sc.next('c4', 'ascending')), 'E-4') 3741 3742 # degree 4 is always the blues note in this model 3743 self.assertEqual(str(sc.pitchFromDegree(4)), 'F#4') 3744 3745 # This never worked consistently and was not an important enough part of the project tp 3746 # continue to debug. 3747 # for unused_trial in range(15): 3748 # self.assertTrue(str(sc.next('f#3', 'ascending')) in ['G3', 'F#3']) 3749 # # presently this might return the same note, if the 3750 # # F# is taken as out of the scale and then found back in the Scale 3751 # # in generation 3752 # self.assertTrue(str(sc.next('f#3', 'descending')) in ['F3', 'F#3']) 3753 3754 def testNextA(self): 3755 sc = MajorScale('c4') 3756 3757 # ascending works in pitch space 3758 self.assertEqual(str(sc.next('a4', 'ascending', 1)), 'B4') 3759 self.assertEqual(str(sc.next('b4', 'ascending', 1)), 'C5') 3760 self.assertEqual(str(sc.next('b5', 'ascending', 1)), 'C6') 3761 self.assertEqual(str(sc.next('b3', 'ascending', 1)), 'C4') 3762 3763 # descending works in pitch space 3764 self.assertEqual(str(sc.next('c3', 'descending', 1)), 'B2') 3765 self.assertEqual(str(sc.next('c8', 'descending', 1)), 'B7') 3766 3767 sc = MajorScale('a4') 3768 3769 self.assertEqual(str(sc.next('g#2', 'ascending', 1)), 'A2') 3770 self.assertEqual(str(sc.next('g#4', 'ascending', 1)), 'A4') 3771 3772 def testIntervalBetweenDegrees(self): 3773 sc = MajorScale('c4') 3774 self.assertEqual(str(sc.intervalBetweenDegrees(3, 4)), '<music21.interval.Interval m2>') 3775 self.assertEqual(str(sc.intervalBetweenDegrees(1, 7)), '<music21.interval.Interval M7>') 3776 self.assertEqual(str(sc.intervalBetweenDegrees(1, 5)), '<music21.interval.Interval P5>') 3777 self.assertEqual(str(sc.intervalBetweenDegrees(2, 4)), '<music21.interval.Interval m3>') 3778 3779 # with a probabilistic non deterministic scale, 3780 # an exception may be raised for step that may not exist 3781 sc = WeightedHexatonicBlues('g3') 3782 exceptCount = 0 3783 for dummy in range(10): 3784 post = None 3785 try: 3786 post = sc.intervalBetweenDegrees(3, 4) 3787 except ScaleException: 3788 exceptCount += 1 3789 if post is not None: 3790 self.assertEqual(str(post), '<music21.interval.Interval A1>') 3791 self.assertLess(exceptCount, 3) 3792 3793 def testScalaScaleA(self): 3794 from music21 import scale 3795 # noinspection SpellCheckingInspection 3796 msg = '''! fj-12tet.scl 3797! 3798Franck Jedrzejewski continued fractions approx. of 12-tet 3799 12 3800! 380189/84 380255/49 380344/37 380463/50 38054/3 380699/70 3807442/295 380827/17 380937/22 381098/55 381115/8 38122/1 3813''' 3814 # provide a raw scala string 3815 sc = scale.ScalaScale('c4', msg) 3816 self.assertEqual(str(sc), '<music21.scale.ScalaScale C Scala: fj-12tet.scl>') 3817 pitchesOut = self.pitchOut(sc.getPitches('c2', 'c4')) 3818 self.assertTrue(common.whitespaceEqual(pitchesOut, 3819 ''' 3820 [C2, C#2(+0c), D2(-0c), E-2(-0c), E2(+0c), F2(-2c), F#2(+0c), 3821 G2, G#2(+1c), A2(+0c), B-2(+0c), B2(-12c), 3822 C3, C#3(+0c), D3(-0c), E-3(-0c), E3(+0c), F3(-2c), F#3(+0c), 3823 G3, G#3(+1c), A3(+0c), B-3(+0c), B3(-12c), 3824 C4]'''), pitchesOut) 3825 3826 def testScalaScaleOutput(self): 3827 from music21 import scale 3828 sc = scale.MajorScale('c4') 3829 ss = sc.getScalaData() 3830 self.assertEqual(ss.pitchCount, 7) 3831 msg = '''! 3832<music21.scale.MajorScale C major> 38337 3834! 3835200.0 3836400.0 3837500.0 3838700.0 3839900.0 38401100.0 38411200.0 3842''' 3843 self.assertEqual(ss.getFileString(), msg) 3844 3845 # noinspection SpellCheckingInspection 3846 3847 def testScalaScaleB(self): 3848 # test importing from scala archive 3849 from music21 import stream 3850 from music21 import meter 3851 3852 sc = ScalaScale('e2', 'fj 12tet') 3853 self.assertEqual(sc._abstract._net.pitchSimplification, 'mostCommon') 3854 # this is showing that there are slight microtonal adjustments 3855 # but they are less than one cent large 3856 pList = sc.pitches 3857 self.assertEqual( 3858 self.pitchOut(pList), 3859 '[E2, F2(+0c), F#2(-0c), G2(-0c), G#2(+0c), A2(-2c), B-2(+0c), B2(-0c), ' 3860 + 'C3(+1c), C#3(+0c), D3(+0c), E-3(-12c), E3(+0c)]') 3861 3862 # 7 tone scale 3863 sc = ScalaScale('c2', 'mbira zimb') 3864 self.assertEqual( 3865 self.pitchOut(sc.pitches), 3866 '[C2, C#2(-2c), D~2(+21c), E~2(+22c), F#~2(-8c), G~2(+21c), A~2(+2c), B~2(-2c)]') 3867 3868 # 21 tone scale 3869 sc = ScalaScale('c2', 'mbira_mude') 3870 self.assertEqual( 3871 self.pitchOut(sc.pitches), 3872 '[C2, C#~2(+24c), E-2(-11c), F#2(-25c), F#2(+12c), G~2(+20c), B~2(-4c), B-2(-24c), ' 3873 + 'F3(-22c), D~3(+17c), F#~3(-2c), G#3(-13c), A3(+15c), C#~3(-24c), A3(+17c), ' 3874 + 'B~3(-2c), C#~4(-22c), D~4(-4c), E~4(+10c), F#~4(-18c), G#4(+5c), B`4(+15c)]') 3875 # sc.show() 3876 3877 # two octave slendro scale 3878 sc = ScalaScale('c2', 'slendro_pliat') 3879 self.assertEqual(self.pitchOut(sc.pitches), 3880 '[C2, D~2(-15c), E~2(+4c), G2(+5c), A~2(-23c), C3, D~3(-15c), E~3(+4c), ' 3881 + 'G3(+5c), A~3(-23c)]') 3882 3883 # 5 note slendro scale 3884 sc = ScalaScale('c2', 'slendro_ang2') 3885 self.assertEqual(self.pitchOut(sc.pitches), 3886 '[C2, E-2(-22c), F~2(+19c), G~2(-10c), B`2(-8c), C3]') 3887 3888 # 5 note slendro scale 3889 sc = ScalaScale('c2', 'slendroc5.scl') 3890 self.assertEqual(self.pitchOut(sc.pitches), 3891 '[C2, D~2(-14c), E~2(+4c), G2(+5c), A~2(-22c), C3]') 3892 3893 s = stream.Stream() 3894 s.append(meter.TimeSignature('6/4')) 3895 3896 sc1 = ScalaScale('c2', 'slendro_ang2') 3897 sc2 = ScalaScale('c2', 'slendroc5.scl') 3898 p1 = stream.Part() 3899 p1.append([note.Note(p, lyric=p.microtone) for p in sc1.pitches]) 3900 p2 = stream.Part() 3901 p2.append([note.Note(p, lyric=p.microtone) for p in sc2.pitches]) 3902 s.insert(0, p1) 3903 s.insert(0, p2) 3904 # s.show() 3905 3906 def testConcreteScaleA(self): 3907 # testing of arbitrary concrete scales 3908 sc = ConcreteScale(pitches=['C#3', 'E-3', 'F3', 'G3', 'B3', 'D~4', 'F#4', 'A4', 'C#5']) 3909 self.assertEqual(str(sc.getTonic()), 'C#3') 3910 3911 self.assertFalse(sc.abstract.octaveDuplicating) 3912 3913 self.assertEqual(self.pitchOut(sc.pitches), 3914 '[C#3, E-3, F3, G3, B3, D~4, F#4, A4, C#5]') 3915 3916 self.assertEqual(self.pitchOut(sc.getPitches('C#3', 'C#5')), 3917 '[C#3, E-3, F3, G3, B3, D~4, F#4, A4, C#5]') 3918 3919 self.assertEqual( 3920 self.pitchOut(sc.getPitches('C#1', 'C#5')), 3921 '[C#1, E-1, F1, G1, B1, D~2, F#2, A2, C#3, E-3, F3, G3, B3, D~4, F#4, A4, C#5]') 3922 3923 # a portion of the scale 3924 self.assertEqual(self.pitchOut(sc.getPitches('C#4', 'C#5')), 3925 '[D~4, F#4, A4, C#5]') 3926 3927 self.assertEqual(self.pitchOut(sc.getPitches('C#7', 'C#5')), 3928 '[C#7, A6, F#6, D~6, B5, G5, F5, E-5, C#5]') 3929 3930 sc = ConcreteScale(pitches=['C#3', 'E-3', 'F3', 'G3', 'B3', 'C#4']) 3931 self.assertEqual(str(sc.getTonic()), 'C#3') 3932 self.assertTrue(sc.abstract.octaveDuplicating) 3933 3934 def testTuneA(self): 3935 # fokker_12.scl Fokker's 7-limit 12-tone just scale 3936 # pyth_12.scl 12 12-tone Pythagorean scale 3937 from music21 import corpus 3938 3939 s = corpus.parse('bwv66.6') 3940 p1 = s.parts[0] 3941 # p1.show('midi') 3942 3943 self.assertEqual(self.pitchOut(p1.pitches[0:10]), 3944 '[C#5, B4, A4, B4, C#5, E5, C#5, B4, A4, C#5]') 3945 3946 sc = ScalaScale('C4', 'fokker_12.scl') 3947 self.assertEqual( 3948 self.pitchOut(sc.pitches), 3949 '[C4, C#4(+19c), D4(+4c), D~4(+17c), E4(-14c), F4(-2c), F#4(-10c), G4(+2c), ' 3950 + 'G#4(+21c), A4(-16c), A~4(+19c), B4(-12c), C5]') 3951 sc.tune(s) 3952 3953 p1 = s.parts[0] 3954 # problem of not matching enharmonics 3955 self.assertEqual( 3956 self.pitchOut(p1.pitches[0:10]), 3957 '[C#5(+19c), B4(-12c), A4(-16c), B4(-12c), C#5(+19c), E5(-14c), C#5(+19c), ' 3958 + 'B4(-12c), A4(-16c), C#5(+19c)]') 3959 # p1.show('midi') 3960 3961 def testTuneB(self): 3962 # fokker_12.scl Fokker's 7-limit 12-tone just scale 3963 # pyth_12.scl 12 12-tone Pythagorean scale 3964 from music21 import corpus 3965 3966 sc = ScalaScale('C4', 'fokker_12.scl') 3967 pl = sc.pitches 3968 self.assertEqual( 3969 self.pitchOut(pl), 3970 '[C4, C#4(+19c), D4(+4c), D~4(+17c), E4(-14c), F4(-2c), F#4(-10c), G4(+2c), ' 3971 + 'G#4(+21c), A4(-16c), A~4(+19c), B4(-12c), C5]') 3972 3973 s = corpus.parse('bwv66.6') 3974 sc.tune(s) 3975 # s.show('midi') 3976 self.assertEqual( 3977 self.pitchOut(s.parts[0].pitches[0:10]), 3978 '[C#5(+19c), B4(-12c), A4(-16c), B4(-12c), C#5(+19c), E5(-14c), C#5(+19c), ' 3979 + 'B4(-12c), A4(-16c), C#5(+19c)]') 3980 3981 self.assertEqual(self.pitchOut(s.parts[1].pitches[0:10]), 3982 '[E4(-14c), F#4(-10c), E4(-14c), E4(-14c), E4(-14c), ' 3983 + 'E4(-14c), A4(-16c), G#4(+21c), E4(-14c), G#4(+21c)]') 3984 3985 def testTunePythagorean(self): 3986 ''' 3987 Applies a pythagorean tuning to a section of D. Luca's Gloria 3988 and then uses Marchetto da Padova's very high sharps and very low 3989 flats (except B-flat) to inflect the accidentals 3990 ''' 3991 from music21 import corpus 3992 from music21 import instrument 3993 3994 s = corpus.parse('luca/gloria').measures(70, 79) 3995 for p in s.parts: 3996 inst = p.recurse().getElementsByClass(instrument.Instrument).first() 3997 inst.midiProgram = 52 3998 sc = ScalaScale('F2', 'pyth_12.scl') 3999 sc.tune(s) 4000 for p in s.flatten().pitches: 4001 if p.accidental is not None: 4002 if p.accidental.name == 'sharp': 4003 p.microtone = p.microtone.cents + 45 4004 elif p.accidental.name == 'flat' and p.step == 'B': 4005 p.microtone = p.microtone.cents - 20 4006 elif p.accidental.name == 'flat': 4007 p.microtone = p.microtone.cents - 45 4008 # s.show() 4009 # s = s.transpose('P-4') 4010 # print(s[0].measure(77).notes[1].microtone) 4011 # s.show('midi') 4012 4013 def testChromaticScaleA(self): 4014 cs = ChromaticScale('c4') 4015 self.assertEqual(self.pitchOut(cs.pitches), 4016 '[C4, C#4, D4, E-4, E4, F4, F#4, G4, A-4, A4, B-4, B4, C5]') 4017 4018 def testSieveScaleA(self): 4019 # sc = SieveScale('d4', '3@0') 4020 # self.assertEqual(str(sc.getPitches('c2', 'c4')), '[D2, E#2, G#2, B2, D3, E#3, G#3, B3]') 4021 4022 sc = SieveScale('d4', '1@0', eld=2) 4023 self.assertEqual(self.pitchOut(sc.getPitches('c2', 'c4')), 4024 '[C2, D2, F-2, G-2, A-2, B-2, C3, D3, F-3, G-3, A-3, B-3, C4]') 4025 4026 sc = SieveScale('d4', '1@0', eld=0.5) 4027 self.assertEqual( 4028 self.pitchOut(sc.getPitches('c2', 'c4')), 4029 '[C2, C~2, D-2, D`2, D2, D~2, E-2, E`2, F-2, F`2, F2, F~2, G-2, ' 4030 + 'G`2, G2, G~2, A-2, A`2, A2, A~2, B-2, B`2, C-3, C`3, C3, C~3, D-3, ' 4031 + 'D`3, D3, D~3, E-3, E`3, F-3, F`3, F3, F~3, G-3, G`3, G3, G~3, A-3, ' 4032 + 'A`3, A3, A~3, B-3, B`3, C-4, C`4, C4]' 4033 ) 4034 4035 sc = SieveScale('d4', '1@0', eld=0.25) 4036 self.assertEqual( 4037 self.pitchOut(sc.getPitches('c2', 'c3')), 4038 '[C2, C2(+25c), C~2, C#2(-25c), D-2, D`2(-25c), D`2, D2(-25c), D2, ' 4039 + 'D2(+25c), D~2, D#2(-25c), E-2, E`2(-25c), E`2, E2(-25c), F-2, F`2(-25c), ' 4040 + 'F`2, F2(-25c), F2, F2(+25c), F~2, F#2(-25c), G-2, G`2(-25c), G`2, G2(-25c), ' 4041 + 'G2, G2(+25c), G~2, G#2(-25c), A-2, A`2(-25c), A`2, A2(-25c), A2, A2(+25c), ' 4042 + 'A~2, A#2(-25c), B-2, B`2(-25c), B`2, B2(-25c), C-3, C`3(-25c), C`3, ' 4043 + 'C3(-25c), C3]' 4044 ) 4045 4046 def testDerivedScaleNoOctaves(self): 4047 from music21 import scale 4048 d = scale.ConcreteScale(pitches=['a', 'b', 'c', 'd', 'e', 'f', 'g#', 'a']) 4049 e = d.deriveRanked(['C', 'E', 'G'], comparisonAttribute='name') 4050 self.assertEqual(str(e), ''.join(['[(3, <music21.scale.ConcreteScale F Concrete>), ', 4051 '(3, <music21.scale.ConcreteScale E Concrete>), ', 4052 '(2, <music21.scale.ConcreteScale B Concrete>), ', 4053 '(2, <music21.scale.ConcreteScale A Concrete>)]'])) 4054 4055 def testDerivedScaleAbsurdOctaves(self): 4056 e = ConcreteScale(pitches=['A4', 'B4', 'C4', 'D4', 'E4', 'F4', 'G4', 'A4']) 4057 with self.assertRaises(intervalNetwork.IntervalNetworkException): 4058 e.deriveRanked(['C4', 'E4', 'G4'], comparisonAttribute='name') 4059 4060 4061# ------------------------------------------------------------------------------ 4062# define presented order in documentation 4063_DOC_ORDER = [ConcreteScale, AbstractScale] 4064 4065 4066if __name__ == '__main__': 4067 # sys.arg test options will be used in mainTest() 4068 import music21 4069 music21.mainTest(Test) # , runTest='testDeriveByDegreeBiDirectional') 4070 4071# store implicit tonic or Not 4072# if not set, then comparisons fall to abstract 4073