Source code for music21.key

# ------------------------------------------------------------------------------
# Name:         key.py
# Purpose:      Classes for keys
#
# Authors:      Michael Scott Asato Cuthbert
#               Christopher Ariza
#
# Copyright:    Copyright © 2009-22 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
This module defines objects for representing key signatures as well as key
areas. The :class:`~music21.key.KeySignature` is used in
:class:`~music21.stream.Measure` objects for defining notated key signatures.

The :class:`~music21.key.Key` object is a fuller representation not just of
a key signature but also of the key of a region.
'''
from __future__ import annotations

import copy
import re
import typing as t
from typing import overload
import unittest
import warnings

from music21 import base
from music21.common.decorators import cacheMethod
from music21.common.types import StepName
from music21 import environment
from music21 import exceptions21
from music21 import interval
from music21 import note
from music21 import pitch
from music21 import scale
from music21 import style

environLocal = environment.Environment('key')

type TransposeTypes = int|str|interval.Interval|interval.GenericInterval


# ------------------------------------------------------------------------------
# store a cache of already-found values
_sharpsToPitchCache: dict[int, pitch.Pitch] = {}


[docs] def convertKeyStringToMusic21KeyString(textString): ''' Utility function to change strings in the form of "Eb" to "E-" (for E-flat major) and leaves alone proper music21 strings (like "E-" or "f#"). A little bit complex because of parsing bb as B-flat minor and Bb as B-flat major. >>> key.convertKeyStringToMusic21KeyString('Eb') 'E-' >>> key.convertKeyStringToMusic21KeyString('f#') 'f#' >>> key.convertKeyStringToMusic21KeyString('bb') 'b-' >>> key.convertKeyStringToMusic21KeyString('Bb') 'B-' >>> key.convertKeyStringToMusic21KeyString('b#') 'b#' >>> key.convertKeyStringToMusic21KeyString('c') 'c' >>> key.convertKeyStringToMusic21KeyString('Bbb') 'B--' >>> key.convertKeyStringToMusic21KeyString('bbb') 'b--' >>> key.convertKeyStringToMusic21KeyString('Ebb') 'E--' ''' if (not textString.endswith('b')) or textString == 'b': pass elif textString == 'bb': textString = 'b-' elif textString == 'Bb': textString = 'B-' elif len(textString) == 2 and textString[-1] == 'b': textString = textString[0] + '-' elif set(textString[1:]) == {'b'}: textString = textString[0] + '-' * (len(textString) - 1) return textString
[docs] def sharpsToPitch(sharpCount): ''' Given a positive/negative number of sharps, return a Pitch object set to the appropriate major key value. >>> key.sharpsToPitch(1) <music21.pitch.Pitch G> >>> key.sharpsToPitch(2) <music21.pitch.Pitch D> >>> key.sharpsToPitch(-2) <music21.pitch.Pitch B-> >>> key.sharpsToPitch(-6) <music21.pitch.Pitch G-> Note that these are :class:`music21.pitch.Pitch` objects not just names: >>> k1 = key.sharpsToPitch(6) >>> k1 <music21.pitch.Pitch F#> >>> k1.step 'F' >>> k1.accidental <music21.pitch.Accidental sharp> OMIT_FROM_DOCS The second time we do something it should be in the cache, so let's make sure it still works: >>> key.sharpsToPitch(1) <music21.pitch.Pitch G> >>> key.sharpsToPitch(1) <music21.pitch.Pitch G> >>> 1 in key._sharpsToPitchCache True >>> key._sharpsToPitchCache[1] <music21.pitch.Pitch G> ''' if sharpCount is None: sharpCount = 0 # fix for C major if sharpCount in _sharpsToPitchCache: # return a deepcopy of the pitch return copy.deepcopy(_sharpsToPitchCache[sharpCount]) pitchInit = pitch.Pitch('C') pitchInit.octave = None # keyPc = (self.sharps * 7) % 12 if sharpCount > 0: intervalStr = 'P5' elif sharpCount < 0: intervalStr = 'P-5' else: return pitchInit # C intervalObj = interval.Interval(intervalStr) for i in range(abs(sharpCount)): pitchInit = intervalObj.transposePitch(pitchInit) pitchInit.octave = None _sharpsToPitchCache[sharpCount] = pitchInit return pitchInit
# store a cache of already-found values # _pitchToSharpsCache = {} fifthsOrder = ('F', 'C', 'G', 'D', 'A', 'E', 'B') modeSharpsAlter = {'major': 0, 'ionian': 0, 'minor': -3, 'aeolian': -3, 'dorian': -2, 'phrygian': -4, 'lydian': 1, 'mixolydian': -1, 'locrian': -5, }
[docs] def pitchToSharps(value: str|pitch.Pitch|note.Note, mode: str|None = None) -> int: ''' Given a pitch string or :class:`music21.pitch.Pitch` or :class:`music21.note.Note` object, return the number of sharps found in that mode. The `mode` parameter can be 'major', 'minor', or most of the common church/jazz modes ('dorian', 'mixolydian', etc.) including Locrian. If `mode` is omitted or not found, the default mode is major. (extra points to anyone who can find the earliest reference to the Locrian mode in print. David Cohen and I (MSAC) have been looking for this for years). >>> key.pitchToSharps('c') 0 >>> key.pitchToSharps('c', 'minor') -3 >>> key.pitchToSharps('a', 'minor') 0 >>> key.pitchToSharps('d') 2 >>> key.pitchToSharps('e-') -3 >>> key.pitchToSharps('a') 3 >>> key.pitchToSharps('e', 'minor') 1 >>> key.pitchToSharps('f#', 'major') 6 >>> key.pitchToSharps('g-', 'major') -6 >>> key.pitchToSharps('c#') 7 >>> key.pitchToSharps('g#') 8 >>> key.pitchToSharps('e', 'dorian') 2 >>> key.pitchToSharps('d', 'dorian') 0 >>> key.pitchToSharps('g', 'mixolydian') 0 >>> key.pitchToSharps('e-', 'lydian') -2 >>> key.pitchToSharps('e-', 'lydian') -2 >>> key.pitchToSharps('a', 'phrygian') -1 >>> key.pitchToSharps('e', 'phrygian') 0 >>> key.pitchToSharps('f#') 6 >>> key.pitchToSharps(note.Note('f-')) -8 >>> key.pitchToSharps(pitch.Pitch('f--')) -15 >>> key.pitchToSharps('f--', 'locrian') -20 >>> key.pitchToSharps('a', 'ionian') 3 >>> key.pitchToSharps('a', 'aeolian') 0 But quarter tones don't work: >>> key.pitchToSharps('C~') Traceback (most recent call last): music21.key.KeyException: Cannot determine sharps for quarter-tone keys! silly! ''' pitchValue: pitch.Pitch if isinstance(value, str): pitchValue = pitch.Pitch(value) elif isinstance(value, pitch.Pitch): pitchValue = value elif isinstance(value, note.Note): pitchValue = value.pitch else: raise KeyException(f'Cannot get a sharp number from value: {value}') # the -1 is because we begin with F not C. sharps = fifthsOrder.index(pitchValue.step) - 1 if pitchValue.accidental is not None: if pitchValue.accidental.isTwelveTone() is False: raise KeyException('Cannot determine sharps for quarter-tone keys! silly!') vaa = int(pitchValue.accidental.alter) sharps = sharps + 7 * vaa if mode is not None and mode in modeSharpsAlter: sharps += modeSharpsAlter[mode] return sharps
class KeySignatureException(exceptions21.Music21Exception): pass class KeyException(exceptions21.Music21Exception): pass class KeyWarning(Warning): pass # ------------------------------------------------------------------------------
[docs] class KeySignature(base.Music21Object): ''' A KeySignature object specifies the signature to be used for a piece; it normally takes one argument: an `int` giving the number of sharps, or, if negative, the number of flats. If you are starting with the name of a key, see the :class:`~music21.key.Key` object. >>> A = key.KeySignature(3) >>> A <music21.key.KeySignature of 3 sharps> >>> Eflat = key.KeySignature(-3) >>> Eflat <music21.key.KeySignature of 3 flats> If you want to get a real Key, then use the :class:`~music21.key.Key` object instead: >>> illegal = key.KeySignature('c#') Traceback (most recent call last): music21.key.KeySignatureException: Cannot get a KeySignature from this "number" of sharps: 'c#'; did you mean to use a key.Key() object instead? >>> legal = key.Key('c#') >>> legal.sharps 4 >>> legal <music21.key.Key of c# minor> To set a non-traditional Key Signature, create a KeySignature object with `sharps=None`, and then set the `alteredPitches` list: >>> unusual = key.KeySignature(sharps=None) >>> unusual.alteredPitches = ['E-', 'G#'] >>> unusual <music21.key.KeySignature of pitches: [E-, G#]> >>> unusual.isNonTraditional True To set a pitch as displayed in a particular octave, create a non-traditional KeySignature and then set pitches with octaves: >>> unusual = key.KeySignature(sharps=None) >>> unusual.alteredPitches = ['F#4'] >>> unusual <music21.key.KeySignature of pitches: [F#4]> If the accidental applies to all octaves but is being displayed differently then you are done, but if you want them to apply only to the octave displayed in then set `.accidentalsApplyOnlyToOctave` to `True`: >>> unusual.accidentalsApplyOnlyToOctave False >>> unusual.accidentalsApplyOnlyToOctave = True **Equality** Two KeySignatures are equal if they pass all `super().__eq__` checks and their sharps are equal. Currently, non-standard key signatures are not checked (this may change). >>> sharp2 = key.KeySignature(2) >>> flat4 = key.KeySignature(-4) >>> sharp2 == flat4 False >>> sharp2 == key.KeySignature(2) True * Changed in v7: `sharps` defaults to 0 (key of no flats/sharps) rather than `None` for nontraditional keys. ''' _styleClass = style.TextStyle equalityAttributes = ('sharps',) # note that musicxml permits non-traditional keys by specifying # one or more altered tones; these are given as pairs of # step names and semitone alterations classSortOrder = 2 def __init__(self, sharps: int|None = 0, **keywords): super().__init__(**keywords) # position on the circle of fifths, where 1 is one sharp, -1 is one flat try: if sharps is not None and (sharps != int(sharps)): raise KeySignatureException( f'Cannot get a KeySignature from this "number" of sharps: {sharps!r}; ' + 'did you mean to use a key.Key() object instead?') except ValueError as ve: raise KeySignatureException( f'Cannot get a KeySignature from this "number" of sharps: {sharps!r}; ' + 'did you mean to use a key.Key() object instead?' ) from ve self._sharps = sharps # need to store a list of pitch objects, used for creating a # non-traditional key self._alteredPitches: list[pitch.Pitch]|None = None self.accidentalsApplyOnlyToOctave = False # -------------------------------------------------------------------------- def _strDescription(self): output = '' ns = self.sharps if ns is None: output = 'pitches: [' + ', '.join([str(p) for p in self.alteredPitches]) + ']' elif ns > 1: output = f'{ns} sharps' elif ns == 1: output = '1 sharp' elif ns == 0: output = 'no sharps or flats' elif ns == -1: output = '1 flat' else: output = f'{abs(ns)} flats' return output def _reprInternal(self): return 'of ' + self._strDescription()
[docs] def asKey(self, mode: str|None = None, tonic: str|None = None): ''' Return a `key.Key` object representing this KeySignature object as a key in the given mode or in the given tonic. If `mode` is None, and `tonic` is not provided, major is assumed. If both mode and tonic are provided, the tonic is ignored. >>> ks = key.KeySignature(2) >>> ks.asKey() <music21.key.Key of D major> >>> ks.asKey(mode='minor') <music21.key.Key of b minor> If `mode` is None, an attempt is made to solve for the mode: >>> ks.asKey(tonic='A') <music21.key.Key of A mixolydian> But will raise `KeyException` if an impossible solution is requested: >>> ks.asKey(tonic='D#') Traceback (most recent call last): music21.key.KeyException: Could not solve for mode from sharps=2, tonic=D# Ionian and Aeolian are supplied instead of major or minor when deriving mode in this way: >>> ks2 = key.KeySignature() >>> ks2.asKey() <music21.key.Key of C major> >>> ks2.asKey(tonic='C') <music21.key.Key of C ionian> * New in v7: `tonic` argument to solve for mode. ''' our_sharps = self.sharps or 0 # || 0 in case of None -- non-standard key-signature if mode is not None and tonic is not None: warnings.warn(f'ignoring provided tonic: {tonic}', KeyWarning, stacklevel=2) if mode is None and tonic is None: mode = 'major' if mode is None and tonic is not None: majorSharpsToMode = {v: k for k, v in modeSharpsAlter.items()} majorSharps = pitchToSharps(tonic) try: mode = majorSharpsToMode[our_sharps - majorSharps] except KeyError as ke: raise KeyException( f'Could not solve for mode from sharps={self.sharps}, tonic={tonic}') from ke mode = t.cast(str, mode) mode = mode.lower() if mode not in modeSharpsAlter: raise KeyException(f'Mode {mode} is unknown') sharpAlterationFromMajor = modeSharpsAlter[mode] pitchObj = sharpsToPitch(our_sharps - sharpAlterationFromMajor) return Key(pitchObj.name, mode)
@property # type: ignore @cacheMethod def alteredPitches(self) -> list[pitch.Pitch]: # unfortunately, mypy cannot deal with @property on decorated methods. # noinspection PyShadowingNames ''' Return or set a list of music21.pitch.Pitch objects that are altered by this KeySignature. That is, all Pitch objects that will receive an accidental. >>> a = key.KeySignature(3) >>> a.alteredPitches [<music21.pitch.Pitch F#>, <music21.pitch.Pitch C#>, <music21.pitch.Pitch G#>] >>> b = key.KeySignature(1) >>> b.alteredPitches [<music21.pitch.Pitch F#>] >>> c = key.KeySignature(9) >>> [str(p) for p in c.alteredPitches] ['F#', 'C#', 'G#', 'D#', 'A#', 'E#', 'B#', 'F##', 'C##'] >>> d = key.KeySignature(-3) >>> d.alteredPitches [<music21.pitch.Pitch B->, <music21.pitch.Pitch E->, <music21.pitch.Pitch A->] >>> e = key.KeySignature(-1) >>> e.alteredPitches [<music21.pitch.Pitch B->] >>> f = key.KeySignature(-6) >>> [str(p) for p in f.alteredPitches] ['B-', 'E-', 'A-', 'D-', 'G-', 'C-'] >>> g = key.KeySignature(-8) >>> [str(p) for p in g.alteredPitches] ['B-', 'E-', 'A-', 'D-', 'G-', 'C-', 'F-', 'B--'] Non-standard, non-traditional key signatures can set their own altered pitches cache. >>> nonTrad = key.KeySignature(sharps=None) >>> nonTrad.alteredPitches = ['B-', 'F#', 'E-', 'G#'] >>> nonTrad.alteredPitches [<music21.pitch.Pitch B->, <music21.pitch.Pitch F#>, <music21.pitch.Pitch E->, <music21.pitch.Pitch G#>] OMIT_FROM_DOCS Ensure at least something is provided when the user hasn't provided enough info: >>> nonTrad2 = key.KeySignature(sharps=None) >>> nonTrad2.alteredPitches [] ''' if self._alteredPitches is not None: return self._alteredPitches post: list[pitch.Pitch] = [] if self.sharps is None: return post if self.sharps > 0: pKeep = pitch.Pitch('B') if self.sharps > 8: pass for i in range(self.sharps): pKeep.transpose('P5', inPlace=True) p = copy.deepcopy(pKeep) p.octave = None post.append(p) elif self.sharps < 0: pKeep = pitch.Pitch('F') for i in range(abs(self.sharps)): pKeep.transpose('P4', inPlace=True) p = copy.deepcopy(pKeep) p.octave = None post.append(p) return post @alteredPitches.setter def alteredPitches(self, newAlteredPitches: list[str|pitch.Pitch|note.Note] ) -> None: self.clearCache() newList: list[pitch.Pitch] = [] for p in newAlteredPitches: if isinstance(p, str): newList.append(pitch.Pitch(p)) elif isinstance(p, pitch.Pitch): newList.append(p) elif isinstance(p, note.Note): newList.append(copy.deepcopy(p.pitch)) self._alteredPitches = newList @property def isNonTraditional(self) -> bool: ''' Returns bool if this is a non-traditional KeySignature: >>> g = key.KeySignature(3) >>> g.isNonTraditional False >>> g = key.KeySignature(sharps=None) >>> g.alteredPitches = [pitch.Pitch('E`')] >>> g.isNonTraditional True >>> g <music21.key.KeySignature of pitches: [E`]> >>> g.accidentalByStep('E') <music21.pitch.Accidental half-flat> ''' if self.sharps is None and self.alteredPitches: return True else: return False
[docs] def accidentalByStep(self, step: StepName) -> pitch.Accidental|None: ''' Given a step (C, D, E, F, etc.) return the accidental for that note in this key (using the natural minor for minor) or None if there is none. >>> g = key.KeySignature(1) >>> g.accidentalByStep('F') <music21.pitch.Accidental sharp> >>> g.accidentalByStep('G') is None True >>> f = key.KeySignature(-1) >>> bbNote = note.Note('B-5') >>> f.accidentalByStep(bbNote.step) <music21.pitch.Accidental flat> Fix a wrong note in F-major: >>> wrongBNote = note.Note('B#4') >>> if f.accidentalByStep(wrongBNote.step) != wrongBNote.pitch.accidental: ... wrongBNote.pitch.accidental = f.accidentalByStep(wrongBNote.step) >>> wrongBNote <music21.note.Note B-> Set all notes to the correct notes for a key using the note's Key Context. Before: >>> s1 = stream.Stream() >>> s1.append(key.KeySignature(4)) # E-major or C-sharp-minor >>> s1.append(note.Note('C', type='half')) >>> s1.append(note.Note('E-', type='half')) >>> s1.append(key.KeySignature(-4)) # A-flat-major or F-minor >>> s1.append(note.Note('A', type='whole')) >>> s1.append(note.Note('F#', type='whole')) >>> #_DOCS_SHOW s1.show() .. image:: images/keyAccidentalByStep_Before.* :width: 400 After: >>> for n in s1.notes: ... n.pitch.accidental = n.getContextByClass(key.KeySignature).accidentalByStep(n.step) >>> #_DOCS_SHOW s1.show() .. image:: images/keyAccidentalByStep.* :width: 400 OMIT_FROM_DOCS >>> s1.show('text') {0.0} <music21.key.KeySignature of 4 sharps> {0.0} <music21.note.Note C#> {2.0} <music21.note.Note E> {4.0} <music21.key.KeySignature of 4 flats> {4.0} <music21.note.Note A-> {8.0} <music21.note.Note F> Test to make sure there are not linked accidentals (fixed bug 22 Nov. 2010) >>> nB1 = note.Note('B', type='whole') >>> nB2 = note.Note('B', type='whole') >>> s1.append(nB1) >>> s1.append(nB2) >>> for n in s1.notes: ... n.pitch.accidental = n.getContextByClass(key.KeySignature).accidentalByStep(n.step) >>> (nB1.pitch.accidental, nB2.pitch.accidental) (<music21.pitch.Accidental flat>, <music21.pitch.Accidental flat>) >>> nB1.pitch.accidental.name = 'sharp' >>> (nB1.pitch.accidental, nB2.pitch.accidental) (<music21.pitch.Accidental sharp>, <music21.pitch.Accidental flat>) ''' # temp measure to fix dbl flats, etc. for thisAlteration in reversed(self.alteredPitches): if thisAlteration.step.lower() == step.lower(): return copy.deepcopy(thisAlteration.accidental) # get a new one each time otherwise we have linked accidentals, YUK! return None
# -------------------------------------------------------------------------- # methods @overload def transpose(self, value: TransposeTypes, *, inPlace: t.Literal[False] = False) -> t.Self: ... @overload def transpose(self, value: TransposeTypes, *, inPlace: t.Literal[True]) -> None: ...
[docs] def transpose(self, value: TransposeTypes, *, inPlace: bool = False) -> t.Self|None: ''' Transpose the KeySignature by the user-provided value. If the value is an integer, the transposition is treated in half steps. If the value is a string, any Interval string specification can be provided. Alternatively, a :class:`music21.interval.Interval` object can be supplied. >>> a = key.KeySignature(2) >>> a <music21.key.KeySignature of 2 sharps> >>> a.asKey('major') <music21.key.Key of D major> >>> b = a.transpose('p5') >>> b <music21.key.KeySignature of 3 sharps> >>> b.asKey('major') <music21.key.Key of A major> >>> b.sharps 3 >>> c = b.transpose('-m2') >>> c.asKey('major') <music21.key.Key of G# major> >>> c.sharps 8 >>> d = c.transpose('-a3') >>> d.asKey('major') <music21.key.Key of E- major> >>> d.sharps -3 Transposition by semitone (or other chromatic interval) >>> c = key.KeySignature(0) >>> dFlat = c.transpose(1) >>> dFlat <music21.key.KeySignature of 5 flats> >>> d = dFlat.transpose(1) >>> d <music21.key.KeySignature of 2 sharps> >>> eFlat = d.transpose(1) >>> eFlat <music21.key.KeySignature of 3 flats> ''' intervalObj: interval.Interval|interval.GenericInterval if isinstance(value, interval.Interval): # it is an Interval class intervalObj = value elif isinstance(value, interval.GenericInterval): intervalObj = value else: # try to process intervalObj = interval.Interval(value) if not inPlace: post = copy.deepcopy(self) else: post = self k1 = post.asKey('major') p1 = k1.tonic p2 = intervalObj.transposePitch(p1) if isinstance(value, int) and abs(pitchToSharps(p2)) > 6: p2 = p2.getEnharmonic() post.sharps = pitchToSharps(p2) post.clearCache() # mode is already set if not inPlace: return post else: return None
[docs] def transposePitchFromC(self, p: pitch.Pitch, *, inPlace=False) -> pitch.Pitch|None: ''' Takes a pitch in C major and transposes it so that it has the same step position in the current key signature. Example: B is the leading tone in C major, so given a key signature of 3 flats, get the leading tone in E-flat major: >>> ks = key.KeySignature(-3) >>> p1 = pitch.Pitch('B') >>> p2 = ks.transposePitchFromC(p1) >>> p2.name 'D' Original pitch is unchanged: >>> p1.name 'B' >>> ks2 = key.KeySignature(2) >>> p2 = ks2.transposePitchFromC(p1) >>> p2.name 'C#' For out of scale pitches the relationship still works; note also that original octave is preserved. >>> p3 = pitch.Pitch('G-4') >>> p4 = ks.transposePitchFromC(p3) >>> p4.nameWithOctave 'B--4' If inPlace is True then the original pitch is modified and nothing is returned. >>> p5 = pitch.Pitch('C5') >>> ks.transposePitchFromC(p5, inPlace=True) >>> p5.nameWithOctave 'E-5' ''' transInterval = None transTimes = 0 originalOctave = p.octave if not inPlace: p = copy.deepcopy(p) if self.sharps == 0: if inPlace: return None return p elif self.sharps < 0: transTimes = abs(self.sharps) transInterval = interval.Interval('P4') else: transTimes = self.sharps transInterval = interval.Interval('P5') for i in range(transTimes): transInterval.transposePitch(p, inPlace=True) if originalOctave is not None: p.octave = originalOctave if not inPlace: return p else: return None
[docs] def getScale(self, mode='major'): ''' Return a :class:`music21.scale.Scale` object (or, actually, a subclass such as :class:`~music21.scale.MajorScale` or :class:`~music21.scale.MinorScale`) that is representative of this key signature and mode. Raises KeySignatureException if mode is not in [None, 'major', 'minor']. >>> ks = key.KeySignature(3) >>> ks <music21.key.KeySignature of 3 sharps> >>> ks.getScale('major') <music21.scale.MajorScale A major> >>> ks.getScale('minor') <music21.scale.MinorScale F# minor> ''' pitchObj = self.asKey(mode).tonic if mode in (None, 'major'): return scale.MajorScale(pitchObj) elif mode == 'minor': return scale.MinorScale(pitchObj) else: raise KeySignatureException(f'No mapping to a scale exists for this mode yet: {mode}')
# -------------------------------------------------------------------------- # properties def _getSharps(self) -> int|None: return self._sharps def _setSharps(self, value: int|None): if value != self._sharps: self._sharps = value self.clearCache() sharps = property(_getSharps, _setSharps, doc=''' Get or set the number of sharps. If the number is negative then it sets the number of flats. Equivalent to musicxml's 'fifths' attribute. >>> ks1 = key.KeySignature(2) >>> ks1.sharps 2 >>> ks1.sharps = -4 >>> ks1 <music21.key.KeySignature of 4 flats> Can be set to None for a non-traditional key signature ''')
[docs] class Key(KeySignature, scale.DiatonicScale): ''' Note that a key is a sort of hypothetical/conceptual object. It probably has a scale (or scales) associated with it and a :class:`~music21.key.KeySignature`, but not necessarily. A Key object has all the attributes of a KeySignature and all the attributes of a :class:`~music21.scale.DiatonicScale`. >>> cm = key.Key('c') # lowercase = c minor. >>> cm <music21.key.Key of c minor> >>> cm.mode 'minor' >>> cm.tonic <music21.pitch.Pitch C> >>> cm.sharps -3 >>> cm.pitchFromDegree(3) <music21.pitch.Pitch E-4> >>> cm.pitchFromDegree(7) <music21.pitch.Pitch B-4> >>> cSharpMaj = key.Key('C#') # uppercase = C# major >>> cSharpMaj <music21.key.Key of C# major> >>> cSharpMaj.sharps 7 >>> fFlatMaj = key.Key('F-') >>> fFlatMaj <music21.key.Key of F- major> >>> fFlatMaj.sharps -8 >>> fFlatMaj.accidentalByStep('B') <music21.pitch.Accidental double-flat> >>> eDor = key.Key('E', 'dorian') >>> eDor <music21.key.Key of E dorian> >>> eDor.sharps 2 >>> eDor.pitches [<music21.pitch.Pitch E4>, <music21.pitch.Pitch F#4>, <music21.pitch.Pitch G4>, <music21.pitch.Pitch A4>, <music21.pitch.Pitch B4>, <music21.pitch.Pitch C#5>, <music21.pitch.Pitch D5>, <music21.pitch.Pitch E5>] If you prefer not to use uppercase and lowercase to distinguish major and minor, the shorthand of CM or Cm can also be used: >>> key.Key('EM') <music21.key.Key of E major> >>> key.Key('F#m') <music21.key.Key of f# minor> **Equality** Two Keys are equal if their tonics' names are equal, their sharps are equal, and their modes are equal (and they pass all superclass tests). >>> k = key.Key(pitch.Pitch('C4')) >>> k2 = key.Key(pitch.Pitch('C5')) >>> k == k2 True >>> k.mode = 'minor' >>> k == k2 False * Changed in v8: keys now compare equal regardless of the octave of their tonics. ''' _sharps = 0 _mode: str|None = None tonic: pitch.Pitch def __init__(self, tonic: str|pitch.Pitch|note.Note = 'C', mode=None, **keywords): if isinstance(tonic, (note.Note, pitch.Pitch)): tonicStr = tonic.name else: tonicStr = tonic if mode is None: if 'm' in tonicStr: mode = 'minor' tonicStr = re.sub('m', '', tonicStr) elif 'M' in tonicStr: mode = 'major' tonicStr = re.sub('M', '', tonicStr) elif tonicStr.lower() == tonicStr: mode = 'minor' else: mode = 'major' else: mode = mode.lower() sharps = pitchToSharps(tonicStr, mode) tonicPitch: pitch.Pitch if isinstance(tonic, pitch.Pitch): tonicPitch = tonic elif isinstance(tonic, note.Note): tonicPitch = tonic.pitch else: tonicPitch = pitch.Pitch(tonicStr) KeySignature.__init__(self, sharps, **keywords) scale.DiatonicScale.__init__(self, tonic=tonicPitch, **keywords) self.tonic = tonicPitch self.type: str = mode self.mode: str = mode # build the network for the appropriate scale if self._abstract is not None: self._abstract.buildNetwork(self.type) # optionally filled attributes # store a floating point value between 0 and 1 regarding # correlation coefficient between the detected key and the algorithm for detecting the key self.correlationCoefficient = None # store an ordered list of alternative Key objects self.alternateInterpretations: list[Key] = [] def _reprInternal(self): return 'of ' + str(self) def __str__(self): # string representation needs to be complete, as is used # for metadata comparisons tonic = self.tonicPitchNameWithCase return f'{tonic} {self.mode}'
[docs] def __eq__(self, other): if not KeySignature.__eq__(self, other): return False # don't check on Scale equality return self.tonic.name == other.tonic.name and self.mode == other.mode
def __hash__(self): return id(self) >> 4 @property def relative(self) -> Key: ''' If the Key is major or minor, return the relative minor or major. Otherwise, just returns self -- this is the best way to not have random crashes in the middle of large datasets. Note that this uses .sharps as a speedup, so if that has been changed, there will be a problem: >>> k = key.Key('E-') >>> k <music21.key.Key of E- major> >>> k.relative <music21.key.Key of c minor> >>> k.relative.relative <music21.key.Key of E- major> >>> key.Key('D', 'dorian').relative <music21.key.Key of D dorian> ''' if self.mode not in ('minor', 'major'): return self if self.mode == 'major': return KeySignature(self.sharps).asKey('minor') else: # minor return KeySignature(self.sharps).asKey('major') @property def parallel(self) -> Key: ''' If the Key is major or minor, return the parallel minor or major. Otherwise, just returns self -- this is the best way to not have random crashes in the middle of large datasets. >>> k = key.Key('D') >>> k <music21.key.Key of D major> >>> k.parallel <music21.key.Key of d minor> >>> k.parallel.parallel <music21.key.Key of D major> >>> key.Key('D', 'dorian').parallel <music21.key.Key of D dorian> ''' if self.mode not in ('minor', 'major'): return self if self.mode == 'major': return Key(self.tonic, 'minor') else: # minor return Key(self.tonic, 'major') @property def tonicPitchNameWithCase(self) -> str: ''' Return the pitch name as a string with the proper case (upper = major; lower = minor). Useful, but simple: >>> k = key.Key('c#') >>> k.tonicPitchNameWithCase 'c#' >>> k = key.Key('B') >>> k.tonicPitchNameWithCase 'B' >>> k.mode = 'minor' >>> k.tonicPitchNameWithCase 'b' Anything else will return the default (capital). >>> k.mode = 'dorian' >>> k.tonicPitchNameWithCase 'B' ''' tonicStr = self.tonic.name if self.mode == 'major': tonicStr = tonicStr.upper() elif self.mode == 'minor': tonicStr = tonicStr.lower() return tonicStr
[docs] def deriveByDegree(self, degree, pitchRef): ''' Given a degree and pitchReference derive a new Key object that has the same mode but a different tonic. Example: What minor key has scale degree 3 as B-flat? >>> minorKey = key.Key(mode='minor') >>> newMinor = minorKey.deriveByDegree(3, 'B-') >>> newMinor <music21.key.Key of g minor> Note that in minor, the natural form is used: >>> minorKey.deriveByDegree(7, 'E') <music21.key.Key of f# minor> >>> minorKey.deriveByDegree(6, 'G') <music21.key.Key of b minor> To use the harmonic form, change `.abstract` on the key to another abstract scale: >>> minorKey.abstract = scale.AbstractHarmonicMinorScale() >>> minorKey.deriveByDegree(7, 'E') <music21.key.Key of f minor> >>> minorKey.deriveByDegree(6, 'G') <music21.key.Key of b minor> Currently because of a limitation in bidirectional scale searching, melodic minor scales cannot be used as abstracts for deriving by degree. * New in v6: preserve mode in key.Key.deriveByDegree. ''' ret = super().deriveByDegree(degree, pitchRef) ret.mode = self.mode # clear these since they no longer apply. ret.correlationCoefficient = None ret.alternateInterpretations = [] return ret
def _tonalCertaintyCorrelationCoefficient(self): # possible measures: if not self.alternateInterpretations: raise KeySignatureException( 'cannot process ambiguity without a list of .alternateInterpretations') focus = [] focus.append(self.correlationCoefficient) for subKey in self.alternateInterpretations: cc = subKey.correlationCoefficient if cc > 0: focus.append(cc) if len(focus) < 2: if self.correlationCoefficient <= 0: return 0.0 else: return self.correlationCoefficient # take abs magnitude as one factor; assume between 0 and 1 # greater certainty often has a larger number absMagnitude = focus[0] # take distance from first to second; greater certainty # seems to have a greater span leaderSpan = focus[0] - focus[1] # combine factors with a weighting for each # estimate range as 2, normalize between zero and 1 return (absMagnitude * 1) + (leaderSpan * 2)
[docs] def tonalCertainty(self, method='correlationCoefficient') -> float: ''' Provide a measure of tonal ambiguity for Key determined with one of many methods. The `correlationCoefficient` assumes that the alternateInterpretations list has been filled from the use of a KeyWeightKeyAnalysis subclass. >>> littlePiece = converter.parse("tinyNotation: 4/4 c4 d e f g a b c' e' g' e' c'") >>> k = littlePiece.analyze('key') >>> k <music21.key.Key of C major> >>> k.tonalCertainty() 1.1648... Three most likely alternateInterpretations: >>> k.alternateInterpretations[0:3] [<music21.key.Key of a minor>, <music21.key.Key of F major>, <music21.key.Key of d minor>] >>> k.correlationCoefficient 0.9068... >>> k.alternateInterpretations[0].correlationCoefficient 0.7778... least likely interpretation: >>> k.alternateInterpretations[-1] <music21.key.Key of F# major> Note that this method only exists if the key has come from an analysis method. Otherwise it raises a KeySignatureException. >>> k2 = key.Key('b-') >>> k2.tonalCertainty() Traceback (most recent call last): music21.key.KeySignatureException: cannot process ambiguity without a list of .alternateInterpretations >>> k2.alternateInterpretations [] ''' if method == 'correlationCoefficient': return self._tonalCertaintyCorrelationCoefficient() else: raise ValueError(f'Unknown method: {method}')
@overload def transpose(self, value: TransposeTypes, *, inPlace: t.Literal[False] = False ) -> t.Self: ... @overload def transpose(self, value: TransposeTypes, *, inPlace: t.Literal[True] ) -> None: ...
[docs] def transpose(self, value: TransposeTypes, *, inPlace: bool = False ) -> t.Self|None: ''' Transpose the Key by the user-provided value. If the value is an integer, the transposition is treated in half steps. If the value is a string, any Interval string specification can be provided. Alternatively, a :class:`music21.interval.Interval` object can be supplied. >>> dMajor = key.Key('D') >>> dMajor <music21.key.Key of D major> >>> aMaj = dMajor.transpose('p5') >>> aMaj <music21.key.Key of A major> >>> aMaj.sharps 3 >>> aMaj.tonic <music21.pitch.Pitch A> >>> aMaj.mode 'major' `inPlace=True` works here and returns None while changing the Key itself. >>> changingKey = key.Key('g') >>> changingKey <music21.key.Key of g minor> >>> changingKey.sharps -2 >>> changingKey.transpose('m-3', inPlace=True) >>> changingKey <music21.key.Key of e minor> >>> changingKey.sharps 1 >>> changingKey.transpose(1, inPlace=True) >>> changingKey <music21.key.Key of f minor> >>> changingKey.sharps -4 >>> changingKey.transpose(1, inPlace=True) >>> changingKey <music21.key.Key of f# minor> >>> changingKey.transpose(1, inPlace=True) >>> changingKey <music21.key.Key of g minor> >>> changingKey.transpose(1, inPlace=True) >>> changingKey <music21.key.Key of g# minor> ''' if inPlace is True: super().transpose(value, inPlace=inPlace) post = self else: post = super().transpose(value, inPlace=inPlace) postKey = post.asKey(self.mode) post.tonic = postKey.tonic post.clearCache() # mode is already set if not inPlace: return post return None
# ------------------------------------------------------------------------------ class Test(unittest.TestCase): def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) def testBasic(self): a = KeySignature() self.assertEqual(a.sharps, 0) def testSetTonic(self): from music21 import chord k = Key() # Set tonic attribute from single pitch b = pitch.Pitch('B') k.tonic = b self.assertIs(k.tonic, b) # Initialize with tonic from chord - no longer allowed. # Call root explicitly b_flat_maj = chord.Chord('Bb4 D5 F5').root() k = Key(tonic=b_flat_maj) self.assertEqual(k.tonic.name, 'B-') def testTonalAmbiguityA(self): from music21 import corpus from music21 import stream # s = corpus.parse('bwv64.2') # k = s.analyze('KrumhanslSchmuckler') # k.tonalCertainty(method='correlationCoefficient') s = corpus.parse('bwv66.6') k = s.analyze('KrumhanslSchmuckler') ta = k.tonalCertainty(method='correlationCoefficient') self.assertTrue(2 > ta > 0.1) s = corpus.parse('schoenberg/opus19', 6) k = s.analyze('KrumhanslSchmuckler') ta = k.tonalCertainty(method='correlationCoefficient') self.assertTrue(2 > ta > 0.1) sc1 = scale.MajorScale('g') sc2 = scale.MajorScale('d') sc3 = scale.MajorScale('a') sc5 = scale.MajorScale('f#') s = stream.Stream() for p in sc1.pitches: s.append(note.Note(p)) k = s.analyze('KrumhanslSchmuckler') ta = k.tonalCertainty(method='correlationCoefficient') self.assertTrue(2 > ta > 0.1) s = stream.Stream() for p in sc1.pitches + sc2.pitches + sc2.pitches + sc3.pitches: s.append(note.Note(p)) k = s.analyze('KrumhanslSchmuckler') ta = k.tonalCertainty(method='correlationCoefficient') self.assertTrue(2 > ta > 0.1) s = stream.Stream() for p in sc1.pitches + sc5.pitches: s.append(note.Note(p)) k = s.analyze('KrumhanslSchmuckler') ta = k.tonalCertainty(method='correlationCoefficient') self.assertTrue(2 > ta > 0.1) s = stream.Stream() for p in ('c', 'g', 'c', 'c', 'e'): s.append(note.Note(p)) k = s.analyze('KrumhanslSchmuckler') ta = k.tonalCertainty(method='correlationCoefficient') self.assertTrue(2 > ta > 0.1) # s = corpus.parse('bwv66.2') # k = s.analyze('KrumhanslSchmuckler') # k.tonalCertainty(method='correlationCoefficient') # s = corpus.parse('bwv48.3') def testAsKey(self): ks = KeySignature(2) k = ks.asKey(mode=None, tonic=None) self.assertEqual(k.mode, 'major') self.assertEqual(k.tonicPitchNameWithCase, 'D') k = ks.asKey(tonic='E') self.assertEqual(k.mode, 'dorian') self.assertEqual(k.tonicPitchNameWithCase, 'E') expected = 'ignoring provided tonic: E' with self.assertWarnsRegex(KeyWarning, expected) as cm: # warn user we ignored their tonic k = ks.asKey(mode='minor', tonic='E') self.assertEqual(k.mode, 'minor') self.assertEqual(k.tonicPitchNameWithCase, 'b') expected = 'Could not solve for mode from sharps=2, tonic=A-' with self.assertRaisesRegex(KeyException, expected) as cm: k = ks.asKey(mode=None, tonic='A-') # test exception chained from KeyError self.assertIsInstance(cm.exception.__cause__, KeyError) # ------------------------------------------------------------------------------ # define presented order in documentation _DOC_ORDER = [KeySignature, Key] if __name__ == '__main__': import music21 music21.mainTest(Test)