|
|
|
|
|
|
|
|
"""Music notation utilities""" |
|
|
|
|
|
import re |
|
|
import numpy as np |
|
|
from .._cache import cache |
|
|
from ..util.exceptions import ParameterError |
|
|
from ..util.decorators import deprecate_positional_args |
|
|
|
|
|
__all__ = [ |
|
|
"key_to_degrees", |
|
|
"key_to_notes", |
|
|
"mela_to_degrees", |
|
|
"mela_to_svara", |
|
|
"thaat_to_degrees", |
|
|
"list_mela", |
|
|
"list_thaat", |
|
|
] |
|
|
|
|
|
THAAT_MAP = dict( |
|
|
bilaval=[0, 2, 4, 5, 7, 9, 11], |
|
|
khamaj=[0, 2, 4, 5, 7, 9, 10], |
|
|
kafi=[0, 2, 3, 5, 7, 9, 10], |
|
|
asavari=[0, 2, 3, 5, 7, 8, 10], |
|
|
bhairavi=[0, 1, 3, 5, 7, 8, 10], |
|
|
kalyan=[0, 2, 4, 6, 7, 9, 11], |
|
|
marva=[0, 1, 4, 6, 7, 9, 11], |
|
|
poorvi=[0, 1, 4, 6, 7, 8, 11], |
|
|
todi=[0, 1, 3, 6, 7, 8, 11], |
|
|
bhairav=[0, 1, 4, 5, 7, 8, 11], |
|
|
) |
|
|
|
|
|
|
|
|
MELAKARTA_MAP = { |
|
|
k: i |
|
|
for i, k in enumerate( |
|
|
[ |
|
|
"kanakangi", |
|
|
"ratnangi", |
|
|
"ganamurthi", |
|
|
"vanaspathi", |
|
|
"manavathi", |
|
|
"tanarupi", |
|
|
"senavathi", |
|
|
"hanumathodi", |
|
|
"dhenuka", |
|
|
"natakapriya", |
|
|
"kokilapriya", |
|
|
"rupavathi", |
|
|
"gayakapriya", |
|
|
"vakulabharanam", |
|
|
"mayamalavagaula", |
|
|
"chakravakom", |
|
|
"suryakantham", |
|
|
"hatakambari", |
|
|
"jhankaradhwani", |
|
|
"natabhairavi", |
|
|
"keeravani", |
|
|
"kharaharapriya", |
|
|
"gaurimanohari", |
|
|
"varunapriya", |
|
|
"mararanjini", |
|
|
"charukesi", |
|
|
"sarasangi", |
|
|
"harikambhoji", |
|
|
"dheerasankarabharanam", |
|
|
"naganandini", |
|
|
"yagapriya", |
|
|
"ragavardhini", |
|
|
"gangeyabhushani", |
|
|
"vagadheeswari", |
|
|
"sulini", |
|
|
"chalanatta", |
|
|
"salagam", |
|
|
"jalarnavam", |
|
|
"jhalavarali", |
|
|
"navaneetham", |
|
|
"pavani", |
|
|
"raghupriya", |
|
|
"gavambodhi", |
|
|
"bhavapriya", |
|
|
"subhapanthuvarali", |
|
|
"shadvidhamargini", |
|
|
"suvarnangi", |
|
|
"divyamani", |
|
|
"dhavalambari", |
|
|
"namanarayani", |
|
|
"kamavardhini", |
|
|
"ramapriya", |
|
|
"gamanasrama", |
|
|
"viswambhari", |
|
|
"syamalangi", |
|
|
"shanmukhapriya", |
|
|
"simhendramadhyamam", |
|
|
"hemavathi", |
|
|
"dharmavathi", |
|
|
"neethimathi", |
|
|
"kanthamani", |
|
|
"rishabhapriya", |
|
|
"latangi", |
|
|
"vachaspathi", |
|
|
"mechakalyani", |
|
|
"chitrambari", |
|
|
"sucharitra", |
|
|
"jyotisvarupini", |
|
|
"dhatuvardhini", |
|
|
"nasikabhushani", |
|
|
"kosalam", |
|
|
"rasikapriya", |
|
|
], |
|
|
1, |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
def thaat_to_degrees(thaat): |
|
|
"""Construct the svara indices (degrees) for a given thaat |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
thaat : str |
|
|
The name of the thaat |
|
|
|
|
|
Returns |
|
|
------- |
|
|
indices : np.ndarray |
|
|
A list of the seven svara indices (starting from 0=Sa) |
|
|
contained in the specified thaat |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
key_to_degrees |
|
|
mela_to_degrees |
|
|
list_thaat |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
>>> librosa.thaat_to_degrees('bilaval') |
|
|
array([ 0, 2, 4, 5, 7, 9, 11]) |
|
|
|
|
|
>>> librosa.thaat_to_degrees('todi') |
|
|
array([ 0, 1, 3, 6, 7, 8, 11]) |
|
|
""" |
|
|
return np.asarray(THAAT_MAP[thaat.lower()]) |
|
|
|
|
|
|
|
|
def mela_to_degrees(mela): |
|
|
"""Construct the svara indices (degrees) for a given melakarta raga |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
mela : str or int |
|
|
Either the name or integer index ([1, 2, ..., 72]) of the melakarta raga |
|
|
|
|
|
Returns |
|
|
------- |
|
|
degrees : np.ndarray |
|
|
A list of the seven svara indices (starting from 0=Sa) |
|
|
contained in the specified raga |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
thaat_to_degrees |
|
|
key_to_degrees |
|
|
list_mela |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
Melakarta #1 (kanakangi): |
|
|
|
|
|
>>> librosa.mela_to_degrees(1) |
|
|
array([0, 1, 2, 5, 7, 8, 9]) |
|
|
|
|
|
Or using a name directly: |
|
|
|
|
|
>>> librosa.mela_to_degrees('kanakangi') |
|
|
array([0, 1, 2, 5, 7, 8, 9]) |
|
|
""" |
|
|
|
|
|
if isinstance(mela, str): |
|
|
index = MELAKARTA_MAP[mela.lower()] - 1 |
|
|
elif 0 < mela <= 72: |
|
|
index = mela - 1 |
|
|
else: |
|
|
raise ParameterError("mela={} must be in range [1, 72]".format(mela)) |
|
|
|
|
|
|
|
|
degrees = [0] |
|
|
|
|
|
|
|
|
lower = index % 36 |
|
|
if 0 <= lower < 6: |
|
|
|
|
|
degrees.extend([1, 2]) |
|
|
elif 6 <= lower < 12: |
|
|
|
|
|
degrees.extend([1, 3]) |
|
|
elif 12 <= lower < 18: |
|
|
|
|
|
degrees.extend([1, 4]) |
|
|
elif 18 <= lower < 24: |
|
|
|
|
|
degrees.extend([2, 3]) |
|
|
elif 24 <= lower < 30: |
|
|
|
|
|
degrees.extend([2, 4]) |
|
|
else: |
|
|
|
|
|
degrees.extend([3, 4]) |
|
|
|
|
|
|
|
|
if index < 36: |
|
|
|
|
|
degrees.append(5) |
|
|
else: |
|
|
|
|
|
degrees.append(6) |
|
|
|
|
|
|
|
|
degrees.append(7) |
|
|
|
|
|
|
|
|
upper = index % 6 |
|
|
if upper == 0: |
|
|
|
|
|
degrees.extend([8, 9]) |
|
|
elif upper == 1: |
|
|
|
|
|
degrees.extend([8, 10]) |
|
|
elif upper == 2: |
|
|
|
|
|
degrees.extend([8, 11]) |
|
|
elif upper == 3: |
|
|
|
|
|
degrees.extend([9, 10]) |
|
|
elif upper == 4: |
|
|
|
|
|
degrees.extend([9, 11]) |
|
|
else: |
|
|
|
|
|
degrees.extend([10, 11]) |
|
|
|
|
|
return np.array(degrees) |
|
|
|
|
|
|
|
|
@deprecate_positional_args |
|
|
@cache(level=10) |
|
|
def mela_to_svara(mela, *, abbr=True, unicode=True): |
|
|
"""Spell the Carnatic svara names for a given melakarta raga |
|
|
|
|
|
This function exists to resolve enharmonic equivalences between |
|
|
pitch classes: |
|
|
|
|
|
- Ri2 / Ga1 |
|
|
- Ri3 / Ga2 |
|
|
- Dha2 / Ni1 |
|
|
- Dha3 / Ni2 |
|
|
|
|
|
For svara outside the raga, names are chosen to preserve orderings |
|
|
so that all Ri precede all Ga, and all Dha precede all Ni. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
mela : str or int |
|
|
the name or numerical index of the melakarta raga |
|
|
|
|
|
abbr : bool |
|
|
If `True`, use single-letter svara names: S, R, G, ... |
|
|
|
|
|
If `False`, use full names: Sa, Ri, Ga, ... |
|
|
|
|
|
unicode : bool |
|
|
If `True`, use unicode symbols for numberings, e.g., Ri\u2081 |
|
|
|
|
|
If `False`, use low-order ASCII, e.g., Ri1. |
|
|
|
|
|
Returns |
|
|
------- |
|
|
svara : list of strings |
|
|
|
|
|
The svara names for each of the 12 pitch classes. |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
key_to_notes |
|
|
mela_to_degrees |
|
|
list_mela |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
Melakarta #1 (Kanakangi) uses R1, G1, D1, N1 |
|
|
|
|
|
>>> librosa.mela_to_svara(1) |
|
|
['S', 'R₁', 'G₁', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] |
|
|
|
|
|
#19 (Jhankaradhwani) uses R2 and G2 so the third svara are Ri: |
|
|
|
|
|
>>> librosa.mela_to_svara(19) |
|
|
['S', 'R₁', 'R₂', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] |
|
|
|
|
|
#31 (Yagapriya) uses R3 and G3, so third and fourth svara are Ri: |
|
|
|
|
|
>>> librosa.mela_to_svara(31) |
|
|
['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] |
|
|
|
|
|
#34 (Vagadheeswari) uses D2 and N2, so Ni1 becomes Dha2: |
|
|
|
|
|
>>> librosa.mela_to_svara(34) |
|
|
['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'N₂', 'N₃'] |
|
|
|
|
|
#36 (Chalanatta) uses D3 and N3, so Ni2 becomes Dha3: |
|
|
|
|
|
>>> librosa.mela_to_svara(36) |
|
|
['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'D₃', 'N₃'] |
|
|
|
|
|
# You can also query by raga name instead of index: |
|
|
|
|
|
>>> librosa.mela_to_svara('chalanatta') |
|
|
['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'D₃', 'N₃'] |
|
|
""" |
|
|
|
|
|
|
|
|
svara_map = [ |
|
|
"Sa", |
|
|
"Ri\u2081", |
|
|
None, |
|
|
None, |
|
|
"Ga\u2083", |
|
|
"Ma\u2081", |
|
|
"Ma\u2082", |
|
|
"Pa", |
|
|
"Dha\u2081", |
|
|
None, |
|
|
None, |
|
|
"Ni\u2083", |
|
|
] |
|
|
|
|
|
if isinstance(mela, str): |
|
|
mela_idx = MELAKARTA_MAP[mela.lower()] - 1 |
|
|
elif 0 < mela <= 72: |
|
|
mela_idx = mela - 1 |
|
|
else: |
|
|
raise ParameterError("mela={} must be in range [1, 72]".format(mela)) |
|
|
|
|
|
|
|
|
lower = mela_idx % 36 |
|
|
if lower < 6: |
|
|
|
|
|
svara_map[2] = "Ga\u2081" |
|
|
else: |
|
|
|
|
|
|
|
|
svara_map[2] = "Ri\u2082" |
|
|
|
|
|
|
|
|
if lower < 30: |
|
|
|
|
|
svara_map[3] = "Ga\u2082" |
|
|
else: |
|
|
|
|
|
svara_map[3] = "Ri\u2083" |
|
|
|
|
|
upper = mela_idx % 6 |
|
|
|
|
|
|
|
|
if upper == 0: |
|
|
|
|
|
svara_map[9] = "Ni\u2081" |
|
|
else: |
|
|
|
|
|
svara_map[9] = "Dha\u2082" |
|
|
|
|
|
|
|
|
if upper == 5: |
|
|
|
|
|
svara_map[10] = "Dha\u2083" |
|
|
else: |
|
|
|
|
|
svara_map[10] = "Ni\u2082" |
|
|
|
|
|
if abbr: |
|
|
svara_map = [ |
|
|
s.translate(str.maketrans({"a": "", "h": "", "i": ""})) for s in svara_map |
|
|
] |
|
|
|
|
|
if not unicode: |
|
|
svara_map = [ |
|
|
s.translate(str.maketrans({"\u2081": "1", "\u2082": "2", "\u2083": "3"})) |
|
|
for s in svara_map |
|
|
] |
|
|
|
|
|
return list(svara_map) |
|
|
|
|
|
|
|
|
def list_mela(): |
|
|
"""List melakarta ragas by name and index. |
|
|
|
|
|
Melakarta raga names are transcribed from [#]_, with the exception of #45 |
|
|
(subhapanthuvarali). |
|
|
|
|
|
.. [#] Bhagyalekshmy, S. (1990). |
|
|
Ragas in Carnatic music. |
|
|
South Asia Books. |
|
|
|
|
|
Returns |
|
|
------- |
|
|
mela_map : dict |
|
|
A dictionary mapping melakarta raga names to indices (1, 2, ..., 72) |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
>>> librosa.list_mela() |
|
|
{'kanakangi': 1, |
|
|
'ratnangi': 2, |
|
|
'ganamurthi': 3, |
|
|
'vanaspathi': 4, |
|
|
...} |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
mela_to_degrees |
|
|
mela_to_svara |
|
|
list_thaat |
|
|
""" |
|
|
return MELAKARTA_MAP.copy() |
|
|
|
|
|
|
|
|
def list_thaat(): |
|
|
"""List supported thaats by name. |
|
|
|
|
|
Returns |
|
|
------- |
|
|
thaats : list |
|
|
A list of supported thaats |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
>>> librosa.list_thaat() |
|
|
['bilaval', |
|
|
'khamaj', |
|
|
'kafi', |
|
|
'asavari', |
|
|
'bhairavi', |
|
|
'kalyan', |
|
|
'marva', |
|
|
'poorvi', |
|
|
'todi', |
|
|
'bhairav'] |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
list_mela |
|
|
thaat_to_degrees |
|
|
""" |
|
|
return list(THAAT_MAP.keys()) |
|
|
|
|
|
|
|
|
@deprecate_positional_args |
|
|
@cache(level=10) |
|
|
def key_to_notes(key, *, unicode=True): |
|
|
"""Lists all 12 note names in the chromatic scale, as spelled according to |
|
|
a given key (major or minor). |
|
|
|
|
|
This function exists to resolve enharmonic equivalences between different |
|
|
spellings for the same pitch (e.g. C♯ vs D♭), and is primarily useful when producing |
|
|
human-readable outputs (e.g. plotting) for pitch content. |
|
|
|
|
|
Note names are decided by the following rules: |
|
|
|
|
|
1. If the tonic of the key has an accidental (sharp or flat), that accidental will be |
|
|
used consistently for all notes. |
|
|
|
|
|
2. If the tonic does not have an accidental, accidentals will be inferred to minimize |
|
|
the total number used for diatonic scale degrees. |
|
|
|
|
|
3. If there is a tie (e.g., in the case of C:maj vs A:min), sharps will be preferred. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
key : string |
|
|
Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), |
|
|
key must be lower-case (``maj`` or ``min``). |
|
|
|
|
|
Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported. |
|
|
|
|
|
Examples: ``C:maj, Db:min, A♭:min``. |
|
|
|
|
|
unicode : bool |
|
|
If ``True`` (default), use Unicode symbols (♯𝄪♭𝄫)for accidentals. |
|
|
|
|
|
If ``False``, Unicode symbols will be mapped to low-order ASCII representations:: |
|
|
|
|
|
♯ -> #, 𝄪 -> ##, ♭ -> b, 𝄫 -> bb |
|
|
|
|
|
Returns |
|
|
------- |
|
|
notes : list |
|
|
``notes[k]`` is the name for semitone ``k`` (starting from C) |
|
|
under the given key. All chromatic notes (0 through 11) are |
|
|
included. |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
midi_to_note |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
`C:maj` will use all sharps |
|
|
|
|
|
>>> librosa.key_to_notes('C:maj') |
|
|
['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] |
|
|
|
|
|
`A:min` has the same notes |
|
|
|
|
|
>>> librosa.key_to_notes('A:min') |
|
|
['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] |
|
|
|
|
|
`A♯:min` will use sharps, but spell note 0 (`C`) as `B♯` |
|
|
|
|
|
>>> librosa.key_to_notes('A#:min') |
|
|
['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] |
|
|
|
|
|
`G♯:maj` will use a double-sharp to spell note 7 (`G`) as `F𝄪`: |
|
|
|
|
|
>>> librosa.key_to_notes('G#:maj') |
|
|
['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'F𝄪', 'G♯', 'A', 'A♯', 'B'] |
|
|
|
|
|
`F♭:min` will use double-flats |
|
|
|
|
|
>>> librosa.key_to_notes('Fb:min') |
|
|
['D𝄫', 'D♭', 'E𝄫', 'E♭', 'F♭', 'F', 'G♭', 'A𝄫', 'A♭', 'B𝄫', 'B♭', 'C♭'] |
|
|
""" |
|
|
|
|
|
|
|
|
match = re.match( |
|
|
r"^(?P<tonic>[A-Ga-g])" |
|
|
r"(?P<accidental>[#♯b!♭]?)" |
|
|
r":(?P<scale>(maj|min)(or)?)$", |
|
|
key, |
|
|
) |
|
|
if not match: |
|
|
raise ParameterError("Improper key format: {:s}".format(key)) |
|
|
|
|
|
pitch_map = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} |
|
|
acc_map = {"#": 1, "": 0, "b": -1, "!": -1, "♯": 1, "♭": -1} |
|
|
|
|
|
tonic = match.group("tonic").upper() |
|
|
accidental = match.group("accidental") |
|
|
offset = acc_map[accidental] |
|
|
|
|
|
scale = match.group("scale")[:3].lower() |
|
|
|
|
|
|
|
|
major = scale == "maj" |
|
|
|
|
|
|
|
|
if major: |
|
|
tonic_number = ((pitch_map[tonic] + offset) * 7) % 12 |
|
|
else: |
|
|
tonic_number = ((pitch_map[tonic] + offset) * 7 + 9) % 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if offset < 0: |
|
|
|
|
|
use_sharps = False |
|
|
|
|
|
elif offset > 0: |
|
|
|
|
|
use_sharps = True |
|
|
|
|
|
elif 0 <= tonic_number < 6: |
|
|
use_sharps = True |
|
|
|
|
|
elif tonic_number > 6: |
|
|
use_sharps = False |
|
|
|
|
|
|
|
|
notes_sharp = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"] |
|
|
notes_flat = ["C", "D♭", "D", "E♭", "E", "F", "G♭", "G", "A♭", "A", "B♭", "B"] |
|
|
|
|
|
|
|
|
sharp_corrections = [ |
|
|
(5, "E♯"), |
|
|
(0, "B♯"), |
|
|
(7, "F𝄪"), |
|
|
(2, "C𝄪"), |
|
|
(9, "G𝄪"), |
|
|
(4, "D𝄪"), |
|
|
(11, "A𝄪"), |
|
|
] |
|
|
|
|
|
|
|
|
flat_corrections = [ |
|
|
(11, "C♭"), |
|
|
(4, "F♭"), |
|
|
(9, "B𝄫"), |
|
|
(2, "E𝄫"), |
|
|
(7, "A𝄫"), |
|
|
(0, "D𝄫"), |
|
|
] |
|
|
|
|
|
|
|
|
n_sharps = tonic_number |
|
|
if tonic_number == 0 and tonic == "B": |
|
|
n_sharps = 12 |
|
|
|
|
|
if use_sharps: |
|
|
|
|
|
for n in range(0, n_sharps - 6 + 1): |
|
|
index, name = sharp_corrections[n] |
|
|
notes_sharp[index] = name |
|
|
|
|
|
notes = notes_sharp |
|
|
else: |
|
|
n_flats = (12 - tonic_number) % 12 |
|
|
|
|
|
|
|
|
for n in range(0, n_flats - 6 + 1): |
|
|
index, name = flat_corrections[n] |
|
|
notes_flat[index] = name |
|
|
|
|
|
notes = notes_flat |
|
|
|
|
|
|
|
|
if not unicode: |
|
|
translations = str.maketrans({"♯": "#", "𝄪": "##", "♭": "b", "𝄫": "bb"}) |
|
|
notes = list(n.translate(translations) for n in notes) |
|
|
|
|
|
return notes |
|
|
|
|
|
|
|
|
def key_to_degrees(key): |
|
|
"""Construct the diatonic scale degrees for a given key. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
key : str |
|
|
Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), |
|
|
key must be lower-case (``maj`` or ``min``). |
|
|
|
|
|
Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported. |
|
|
|
|
|
Examples: ``C:maj, Db:min, A♭:min``. |
|
|
|
|
|
Returns |
|
|
------- |
|
|
degrees : np.ndarray |
|
|
An array containing the semitone numbers (0=C, 1=C#, ... 11=B) |
|
|
for each of the seven scale degrees in the given key, starting |
|
|
from the tonic. |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
key_to_notes |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
>>> librosa.key_to_degrees('C:maj') |
|
|
array([ 0, 2, 4, 5, 7, 9, 11]) |
|
|
|
|
|
>>> librosa.key_to_degrees('C#:maj') |
|
|
array([ 1, 3, 5, 6, 8, 10, 0]) |
|
|
|
|
|
>>> librosa.key_to_degrees('A:min') |
|
|
array([ 9, 11, 0, 2, 4, 5, 7]) |
|
|
|
|
|
""" |
|
|
notes = dict( |
|
|
maj=np.array([0, 2, 4, 5, 7, 9, 11]), min=np.array([0, 2, 3, 5, 7, 8, 10]) |
|
|
) |
|
|
|
|
|
match = re.match( |
|
|
r"^(?P<tonic>[A-Ga-g])" |
|
|
r"(?P<accidental>[#♯b!♭]?)" |
|
|
r":(?P<scale>(maj|min)(or)?)$", |
|
|
key, |
|
|
) |
|
|
if not match: |
|
|
raise ParameterError("Improper key format: {:s}".format(key)) |
|
|
|
|
|
pitch_map = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} |
|
|
acc_map = {"#": 1, "": 0, "b": -1, "!": -1, "♯": 1, "♭": -1} |
|
|
tonic = match.group("tonic").upper() |
|
|
accidental = match.group("accidental") |
|
|
offset = acc_map[accidental] |
|
|
|
|
|
scale = match.group("scale")[:3].lower() |
|
|
|
|
|
return (notes[scale] + pitch_map[tonic] + offset) % 12 |
|
|
|