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