Spaces:
Configuration error
Configuration error
update enviroments
Browse files- Dockerfile +3 -1
- notation.py +1011 -0
Dockerfile
CHANGED
@@ -23,11 +23,13 @@ RUN pip list
|
|
23 |
# Copy the rest of your application's code
|
24 |
COPY . /app/
|
25 |
|
|
|
|
|
|
|
26 |
# RUN cd /tmp && mkdir cache1
|
27 |
|
28 |
ENV NUMBA_CACHE_DIR=/tmp
|
29 |
|
30 |
-
|
31 |
# Expose the port your app runs on
|
32 |
EXPOSE 7860
|
33 |
|
|
|
23 |
# Copy the rest of your application's code
|
24 |
COPY . /app/
|
25 |
|
26 |
+
# Replace the librosa notation.py with notation.py from your project
|
27 |
+
COPY notation.py /usr/local/lib/python3.10/site-packages/librosa/core/notation.py
|
28 |
+
|
29 |
# RUN cd /tmp && mkdir cache1
|
30 |
|
31 |
ENV NUMBA_CACHE_DIR=/tmp
|
32 |
|
|
|
33 |
# Expose the port your app runs on
|
34 |
EXPOSE 7860
|
35 |
|
notation.py
ADDED
@@ -0,0 +1,1011 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
"""Music notation utilities"""
|
4 |
+
|
5 |
+
import re
|
6 |
+
import numpy as np
|
7 |
+
from numba import jit
|
8 |
+
from .intervals import INTERVALS
|
9 |
+
from .._cache import cache
|
10 |
+
from ..util.exceptions import ParameterError
|
11 |
+
from typing import Dict, List, Union, overload
|
12 |
+
from ..util.decorators import vectorize
|
13 |
+
from .._typing import _ScalarOrSequence, _FloatLike_co, _SequenceLike
|
14 |
+
|
15 |
+
|
16 |
+
__all__ = [
|
17 |
+
"key_to_degrees",
|
18 |
+
"key_to_notes",
|
19 |
+
"mela_to_degrees",
|
20 |
+
"mela_to_svara",
|
21 |
+
"thaat_to_degrees",
|
22 |
+
"list_mela",
|
23 |
+
"list_thaat",
|
24 |
+
"fifths_to_note",
|
25 |
+
"interval_to_fjs",
|
26 |
+
]
|
27 |
+
|
28 |
+
THAAT_MAP = dict(
|
29 |
+
bilaval=[0, 2, 4, 5, 7, 9, 11],
|
30 |
+
khamaj=[0, 2, 4, 5, 7, 9, 10],
|
31 |
+
kafi=[0, 2, 3, 5, 7, 9, 10],
|
32 |
+
asavari=[0, 2, 3, 5, 7, 8, 10],
|
33 |
+
bhairavi=[0, 1, 3, 5, 7, 8, 10],
|
34 |
+
kalyan=[0, 2, 4, 6, 7, 9, 11],
|
35 |
+
marva=[0, 1, 4, 6, 7, 9, 11],
|
36 |
+
poorvi=[0, 1, 4, 6, 7, 8, 11],
|
37 |
+
todi=[0, 1, 3, 6, 7, 8, 11],
|
38 |
+
bhairav=[0, 1, 4, 5, 7, 8, 11],
|
39 |
+
)
|
40 |
+
|
41 |
+
# Enumeration will start from 1
|
42 |
+
MELAKARTA_MAP = {
|
43 |
+
k: i
|
44 |
+
for i, k in enumerate(
|
45 |
+
[
|
46 |
+
"kanakangi",
|
47 |
+
"ratnangi",
|
48 |
+
"ganamurthi",
|
49 |
+
"vanaspathi",
|
50 |
+
"manavathi",
|
51 |
+
"tanarupi",
|
52 |
+
"senavathi",
|
53 |
+
"hanumathodi",
|
54 |
+
"dhenuka",
|
55 |
+
"natakapriya",
|
56 |
+
"kokilapriya",
|
57 |
+
"rupavathi",
|
58 |
+
"gayakapriya",
|
59 |
+
"vakulabharanam",
|
60 |
+
"mayamalavagaula",
|
61 |
+
"chakravakom",
|
62 |
+
"suryakantham",
|
63 |
+
"hatakambari",
|
64 |
+
"jhankaradhwani",
|
65 |
+
"natabhairavi",
|
66 |
+
"keeravani",
|
67 |
+
"kharaharapriya",
|
68 |
+
"gaurimanohari",
|
69 |
+
"varunapriya",
|
70 |
+
"mararanjini",
|
71 |
+
"charukesi",
|
72 |
+
"sarasangi",
|
73 |
+
"harikambhoji",
|
74 |
+
"dheerasankarabharanam",
|
75 |
+
"naganandini",
|
76 |
+
"yagapriya",
|
77 |
+
"ragavardhini",
|
78 |
+
"gangeyabhushani",
|
79 |
+
"vagadheeswari",
|
80 |
+
"sulini",
|
81 |
+
"chalanatta",
|
82 |
+
"salagam",
|
83 |
+
"jalarnavam",
|
84 |
+
"jhalavarali",
|
85 |
+
"navaneetham",
|
86 |
+
"pavani",
|
87 |
+
"raghupriya",
|
88 |
+
"gavambodhi",
|
89 |
+
"bhavapriya",
|
90 |
+
"subhapanthuvarali",
|
91 |
+
"shadvidhamargini",
|
92 |
+
"suvarnangi",
|
93 |
+
"divyamani",
|
94 |
+
"dhavalambari",
|
95 |
+
"namanarayani",
|
96 |
+
"kamavardhini",
|
97 |
+
"ramapriya",
|
98 |
+
"gamanasrama",
|
99 |
+
"viswambhari",
|
100 |
+
"syamalangi",
|
101 |
+
"shanmukhapriya",
|
102 |
+
"simhendramadhyamam",
|
103 |
+
"hemavathi",
|
104 |
+
"dharmavathi",
|
105 |
+
"neethimathi",
|
106 |
+
"kanthamani",
|
107 |
+
"rishabhapriya",
|
108 |
+
"latangi",
|
109 |
+
"vachaspathi",
|
110 |
+
"mechakalyani",
|
111 |
+
"chitrambari",
|
112 |
+
"sucharitra",
|
113 |
+
"jyotisvarupini",
|
114 |
+
"dhatuvardhini",
|
115 |
+
"nasikabhushani",
|
116 |
+
"kosalam",
|
117 |
+
"rasikapriya",
|
118 |
+
],
|
119 |
+
1,
|
120 |
+
)
|
121 |
+
}
|
122 |
+
|
123 |
+
|
124 |
+
# Pre-compiled regular expressions for note and key parsing
|
125 |
+
NOTE_RE = re.compile(
|
126 |
+
r"^(?P<note>[A-Ga-g])"
|
127 |
+
r"(?P<accidental>[#♯𝄪b!♭𝄫♮]*)"
|
128 |
+
r"(?P<octave>[+-]?\d+)?"
|
129 |
+
r"(?P<cents>[+-]\d+)?$"
|
130 |
+
)
|
131 |
+
|
132 |
+
KEY_RE = re.compile(
|
133 |
+
r"^(?P<tonic>[A-Ga-g])" r"(?P<accidental>[#♯b!♭]?)" r":(?P<scale>(maj|min)(or)?)$"
|
134 |
+
)
|
135 |
+
|
136 |
+
|
137 |
+
def thaat_to_degrees(thaat: str) -> np.ndarray:
|
138 |
+
"""Construct the svara indices (degrees) for a given thaat
|
139 |
+
|
140 |
+
Parameters
|
141 |
+
----------
|
142 |
+
thaat : str
|
143 |
+
The name of the thaat
|
144 |
+
|
145 |
+
Returns
|
146 |
+
-------
|
147 |
+
indices : np.ndarray
|
148 |
+
A list of the seven svara indices (starting from 0=Sa)
|
149 |
+
contained in the specified thaat
|
150 |
+
|
151 |
+
See Also
|
152 |
+
--------
|
153 |
+
key_to_degrees
|
154 |
+
mela_to_degrees
|
155 |
+
list_thaat
|
156 |
+
|
157 |
+
Examples
|
158 |
+
--------
|
159 |
+
>>> librosa.thaat_to_degrees('bilaval')
|
160 |
+
array([ 0, 2, 4, 5, 7, 9, 11])
|
161 |
+
|
162 |
+
>>> librosa.thaat_to_degrees('todi')
|
163 |
+
array([ 0, 1, 3, 6, 7, 8, 11])
|
164 |
+
"""
|
165 |
+
return np.asarray(THAAT_MAP[thaat.lower()])
|
166 |
+
|
167 |
+
|
168 |
+
def mela_to_degrees(mela: Union[str, int]) -> np.ndarray:
|
169 |
+
"""Construct the svara indices (degrees) for a given melakarta raga
|
170 |
+
|
171 |
+
Parameters
|
172 |
+
----------
|
173 |
+
mela : str or int
|
174 |
+
Either the name or integer index ([1, 2, ..., 72]) of the melakarta raga
|
175 |
+
|
176 |
+
Returns
|
177 |
+
-------
|
178 |
+
degrees : np.ndarray
|
179 |
+
A list of the seven svara indices (starting from 0=Sa)
|
180 |
+
contained in the specified raga
|
181 |
+
|
182 |
+
See Also
|
183 |
+
--------
|
184 |
+
thaat_to_degrees
|
185 |
+
key_to_degrees
|
186 |
+
list_mela
|
187 |
+
|
188 |
+
Examples
|
189 |
+
--------
|
190 |
+
Melakarta #1 (kanakangi):
|
191 |
+
|
192 |
+
>>> librosa.mela_to_degrees(1)
|
193 |
+
array([0, 1, 2, 5, 7, 8, 9])
|
194 |
+
|
195 |
+
Or using a name directly:
|
196 |
+
|
197 |
+
>>> librosa.mela_to_degrees('kanakangi')
|
198 |
+
array([0, 1, 2, 5, 7, 8, 9])
|
199 |
+
"""
|
200 |
+
|
201 |
+
if isinstance(mela, str):
|
202 |
+
index = MELAKARTA_MAP[mela.lower()] - 1
|
203 |
+
elif 0 < mela <= 72:
|
204 |
+
index = mela - 1
|
205 |
+
else:
|
206 |
+
raise ParameterError(f"mela={mela} must be in range [1, 72]")
|
207 |
+
|
208 |
+
# always have Sa [0]
|
209 |
+
degrees = [0]
|
210 |
+
|
211 |
+
# Fill in Ri and Ga
|
212 |
+
lower = index % 36
|
213 |
+
if 0 <= lower < 6:
|
214 |
+
# Ri1, Ga1
|
215 |
+
degrees.extend([1, 2])
|
216 |
+
elif 6 <= lower < 12:
|
217 |
+
# Ri1, Ga2
|
218 |
+
degrees.extend([1, 3])
|
219 |
+
elif 12 <= lower < 18:
|
220 |
+
# Ri1, Ga3
|
221 |
+
degrees.extend([1, 4])
|
222 |
+
elif 18 <= lower < 24:
|
223 |
+
# Ri2, Ga2
|
224 |
+
degrees.extend([2, 3])
|
225 |
+
elif 24 <= lower < 30:
|
226 |
+
# Ri2, Ga3
|
227 |
+
degrees.extend([2, 4])
|
228 |
+
else:
|
229 |
+
# Ri3, Ga3
|
230 |
+
degrees.extend([3, 4])
|
231 |
+
|
232 |
+
# Determine Ma
|
233 |
+
if index < 36:
|
234 |
+
# Ma1
|
235 |
+
degrees.append(5)
|
236 |
+
else:
|
237 |
+
# Ma2
|
238 |
+
degrees.append(6)
|
239 |
+
|
240 |
+
# always have Pa [7]
|
241 |
+
degrees.append(7)
|
242 |
+
|
243 |
+
# Determine Dha and Ni
|
244 |
+
upper = index % 6
|
245 |
+
if upper == 0:
|
246 |
+
# Dha1, Ni1
|
247 |
+
degrees.extend([8, 9])
|
248 |
+
elif upper == 1:
|
249 |
+
# Dha1, Ni2
|
250 |
+
degrees.extend([8, 10])
|
251 |
+
elif upper == 2:
|
252 |
+
# Dha1, Ni3
|
253 |
+
degrees.extend([8, 11])
|
254 |
+
elif upper == 3:
|
255 |
+
# Dha2, Ni2
|
256 |
+
degrees.extend([9, 10])
|
257 |
+
elif upper == 4:
|
258 |
+
# Dha2, Ni3
|
259 |
+
degrees.extend([9, 11])
|
260 |
+
else:
|
261 |
+
# Dha3, Ni3
|
262 |
+
degrees.extend([10, 11])
|
263 |
+
|
264 |
+
return np.array(degrees)
|
265 |
+
|
266 |
+
|
267 |
+
@cache(level=10)
|
268 |
+
def mela_to_svara(
|
269 |
+
mela: Union[str, int], *, abbr: bool = True, unicode: bool = True
|
270 |
+
) -> List[str]:
|
271 |
+
"""Spell the Carnatic svara names for a given melakarta raga
|
272 |
+
|
273 |
+
This function exists to resolve enharmonic equivalences between
|
274 |
+
pitch classes:
|
275 |
+
|
276 |
+
- Ri2 / Ga1
|
277 |
+
- Ri3 / Ga2
|
278 |
+
- Dha2 / Ni1
|
279 |
+
- Dha3 / Ni2
|
280 |
+
|
281 |
+
For svara outside the raga, names are chosen to preserve orderings
|
282 |
+
so that all Ri precede all Ga, and all Dha precede all Ni.
|
283 |
+
|
284 |
+
Parameters
|
285 |
+
----------
|
286 |
+
mela : str or int
|
287 |
+
the name or numerical index of the melakarta raga
|
288 |
+
|
289 |
+
abbr : bool
|
290 |
+
If `True`, use single-letter svara names: S, R, G, ...
|
291 |
+
|
292 |
+
If `False`, use full names: Sa, Ri, Ga, ...
|
293 |
+
|
294 |
+
unicode : bool
|
295 |
+
If `True`, use unicode symbols for numberings, e.g., Ri\u2081
|
296 |
+
|
297 |
+
If `False`, use low-order ASCII, e.g., Ri1.
|
298 |
+
|
299 |
+
Returns
|
300 |
+
-------
|
301 |
+
svara : list of strings
|
302 |
+
|
303 |
+
The svara names for each of the 12 pitch classes.
|
304 |
+
|
305 |
+
See Also
|
306 |
+
--------
|
307 |
+
key_to_notes
|
308 |
+
mela_to_degrees
|
309 |
+
list_mela
|
310 |
+
|
311 |
+
Examples
|
312 |
+
--------
|
313 |
+
Melakarta #1 (Kanakangi) uses R1, G1, D1, N1
|
314 |
+
|
315 |
+
>>> librosa.mela_to_svara(1)
|
316 |
+
['S', 'R₁', 'G₁', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃']
|
317 |
+
|
318 |
+
#19 (Jhankaradhwani) uses R2 and G2 so the third svara are Ri:
|
319 |
+
|
320 |
+
>>> librosa.mela_to_svara(19)
|
321 |
+
['S', 'R₁', 'R₂', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃']
|
322 |
+
|
323 |
+
#31 (Yagapriya) uses R3 and G3, so third and fourth svara are Ri:
|
324 |
+
|
325 |
+
>>> librosa.mela_to_svara(31)
|
326 |
+
['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃']
|
327 |
+
|
328 |
+
#34 (Vagadheeswari) uses D2 and N2, so Ni1 becomes Dha2:
|
329 |
+
|
330 |
+
>>> librosa.mela_to_svara(34)
|
331 |
+
['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'N₂', 'N₃']
|
332 |
+
|
333 |
+
#36 (Chalanatta) uses D3 and N3, so Ni2 becomes Dha3:
|
334 |
+
|
335 |
+
>>> librosa.mela_to_svara(36)
|
336 |
+
['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'D₃', 'N₃']
|
337 |
+
|
338 |
+
# You can also query by raga name instead of index:
|
339 |
+
|
340 |
+
>>> librosa.mela_to_svara('chalanatta')
|
341 |
+
['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'D₃', 'N₃']
|
342 |
+
"""
|
343 |
+
|
344 |
+
# The following will be constant for all ragas
|
345 |
+
svara_map = [
|
346 |
+
"Sa",
|
347 |
+
"Ri\u2081",
|
348 |
+
"", # Ri2/Ga1
|
349 |
+
"", # Ri3/Ga2
|
350 |
+
"Ga\u2083",
|
351 |
+
"Ma\u2081",
|
352 |
+
"Ma\u2082",
|
353 |
+
"Pa",
|
354 |
+
"Dha\u2081",
|
355 |
+
"", # Dha2/Ni1
|
356 |
+
"", # Dha3/Ni2
|
357 |
+
"Ni\u2083",
|
358 |
+
]
|
359 |
+
|
360 |
+
if isinstance(mela, str):
|
361 |
+
mela_idx = MELAKARTA_MAP[mela.lower()] - 1
|
362 |
+
elif 0 < mela <= 72:
|
363 |
+
mela_idx = mela - 1
|
364 |
+
else:
|
365 |
+
raise ParameterError(f"mela={mela} must be in range [1, 72]")
|
366 |
+
|
367 |
+
# Determine Ri2/Ga1
|
368 |
+
lower = mela_idx % 36
|
369 |
+
if lower < 6:
|
370 |
+
# First six will have Ri1/Ga1
|
371 |
+
svara_map[2] = "Ga\u2081"
|
372 |
+
else:
|
373 |
+
# All others have either Ga2/Ga3
|
374 |
+
# So we'll call this Ri2
|
375 |
+
svara_map[2] = "Ri\u2082"
|
376 |
+
|
377 |
+
# Determine Ri3/Ga2
|
378 |
+
if lower < 30:
|
379 |
+
# First thirty should get Ga2
|
380 |
+
svara_map[3] = "Ga\u2082"
|
381 |
+
else:
|
382 |
+
# Only the last six have Ri3
|
383 |
+
svara_map[3] = "Ri\u2083"
|
384 |
+
|
385 |
+
upper = mela_idx % 6
|
386 |
+
|
387 |
+
# Determine Dha2/Ni1
|
388 |
+
if upper == 0:
|
389 |
+
# these are the only ones with Ni1
|
390 |
+
svara_map[9] = "Ni\u2081"
|
391 |
+
else:
|
392 |
+
# Everyone else has Dha2
|
393 |
+
svara_map[9] = "Dha\u2082"
|
394 |
+
|
395 |
+
# Determine Dha3/Ni2
|
396 |
+
if upper == 5:
|
397 |
+
# This one has Dha3
|
398 |
+
svara_map[10] = "Dha\u2083"
|
399 |
+
else:
|
400 |
+
# Everyone else has Ni2
|
401 |
+
svara_map[10] = "Ni\u2082"
|
402 |
+
|
403 |
+
if abbr:
|
404 |
+
t_abbr = str.maketrans({"a": "", "h": "", "i": ""})
|
405 |
+
svara_map = [s.translate(t_abbr) for s in svara_map]
|
406 |
+
|
407 |
+
if not unicode:
|
408 |
+
t_uni = str.maketrans({"\u2081": "1", "\u2082": "2", "\u2083": "3"})
|
409 |
+
svara_map = [s.translate(t_uni) for s in svara_map]
|
410 |
+
|
411 |
+
return list(svara_map)
|
412 |
+
|
413 |
+
|
414 |
+
def list_mela() -> Dict[str, int]:
|
415 |
+
"""List melakarta ragas by name and index.
|
416 |
+
|
417 |
+
Melakarta raga names are transcribed from [#]_, with the exception of #45
|
418 |
+
(subhapanthuvarali).
|
419 |
+
|
420 |
+
.. [#] Bhagyalekshmy, S. (1990).
|
421 |
+
Ragas in Carnatic music.
|
422 |
+
South Asia Books.
|
423 |
+
|
424 |
+
Returns
|
425 |
+
-------
|
426 |
+
mela_map : dict
|
427 |
+
A dictionary mapping melakarta raga names to indices (1, 2, ..., 72)
|
428 |
+
|
429 |
+
Examples
|
430 |
+
--------
|
431 |
+
>>> librosa.list_mela()
|
432 |
+
{'kanakangi': 1,
|
433 |
+
'ratnangi': 2,
|
434 |
+
'ganamurthi': 3,
|
435 |
+
'vanaspathi': 4,
|
436 |
+
...}
|
437 |
+
|
438 |
+
See Also
|
439 |
+
--------
|
440 |
+
mela_to_degrees
|
441 |
+
mela_to_svara
|
442 |
+
list_thaat
|
443 |
+
"""
|
444 |
+
return MELAKARTA_MAP.copy()
|
445 |
+
|
446 |
+
|
447 |
+
def list_thaat() -> List[str]:
|
448 |
+
"""List supported thaats by name.
|
449 |
+
|
450 |
+
Returns
|
451 |
+
-------
|
452 |
+
thaats : list
|
453 |
+
A list of supported thaats
|
454 |
+
|
455 |
+
Examples
|
456 |
+
--------
|
457 |
+
>>> librosa.list_thaat()
|
458 |
+
['bilaval',
|
459 |
+
'khamaj',
|
460 |
+
'kafi',
|
461 |
+
'asavari',
|
462 |
+
'bhairavi',
|
463 |
+
'kalyan',
|
464 |
+
'marva',
|
465 |
+
'poorvi',
|
466 |
+
'todi',
|
467 |
+
'bhairav']
|
468 |
+
|
469 |
+
See Also
|
470 |
+
--------
|
471 |
+
list_mela
|
472 |
+
thaat_to_degrees
|
473 |
+
"""
|
474 |
+
return list(THAAT_MAP.keys())
|
475 |
+
|
476 |
+
|
477 |
+
@cache(level=10)
|
478 |
+
def key_to_notes(key: str, *, unicode: bool = True) -> List[str]:
|
479 |
+
"""Lists all 12 note names in the chromatic scale, as spelled according to
|
480 |
+
a given key (major or minor).
|
481 |
+
|
482 |
+
This function exists to resolve enharmonic equivalences between different
|
483 |
+
spellings for the same pitch (e.g. C♯ vs D♭), and is primarily useful when producing
|
484 |
+
human-readable outputs (e.g. plotting) for pitch content.
|
485 |
+
|
486 |
+
Note names are decided by the following rules:
|
487 |
+
|
488 |
+
1. If the tonic of the key has an accidental (sharp or flat), that accidental will be
|
489 |
+
used consistently for all notes.
|
490 |
+
|
491 |
+
2. If the tonic does not have an accidental, accidentals will be inferred to minimize
|
492 |
+
the total number used for diatonic scale degrees.
|
493 |
+
|
494 |
+
3. If there is a tie (e.g., in the case of C:maj vs A:min), sharps will be preferred.
|
495 |
+
|
496 |
+
Parameters
|
497 |
+
----------
|
498 |
+
key : string
|
499 |
+
Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``),
|
500 |
+
key must be lower-case (``maj`` or ``min``).
|
501 |
+
|
502 |
+
Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported.
|
503 |
+
|
504 |
+
Examples: ``C:maj, Db:min, A♭:min``.
|
505 |
+
|
506 |
+
unicode : bool
|
507 |
+
If ``True`` (default), use Unicode symbols (♯𝄪♭𝄫)for accidentals.
|
508 |
+
|
509 |
+
If ``False``, Unicode symbols will be mapped to low-order ASCII representations::
|
510 |
+
|
511 |
+
♯ -> #, 𝄪 -> ##, ♭ -> b, 𝄫 -> bb
|
512 |
+
|
513 |
+
Returns
|
514 |
+
-------
|
515 |
+
notes : list
|
516 |
+
``notes[k]`` is the name for semitone ``k`` (starting from C)
|
517 |
+
under the given key. All chromatic notes (0 through 11) are
|
518 |
+
included.
|
519 |
+
|
520 |
+
See Also
|
521 |
+
--------
|
522 |
+
midi_to_note
|
523 |
+
|
524 |
+
Examples
|
525 |
+
--------
|
526 |
+
`C:maj` will use all sharps
|
527 |
+
|
528 |
+
>>> librosa.key_to_notes('C:maj')
|
529 |
+
['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B']
|
530 |
+
|
531 |
+
`A:min` has the same notes
|
532 |
+
|
533 |
+
>>> librosa.key_to_notes('A:min')
|
534 |
+
['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B']
|
535 |
+
|
536 |
+
`A♯:min` will use sharps, but spell note 0 (`C`) as `B♯`
|
537 |
+
|
538 |
+
>>> librosa.key_to_notes('A#:min')
|
539 |
+
['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B']
|
540 |
+
|
541 |
+
`G♯:maj` will use a double-sharp to spell note 7 (`G`) as `F𝄪`:
|
542 |
+
|
543 |
+
>>> librosa.key_to_notes('G#:maj')
|
544 |
+
['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'F𝄪', 'G♯', 'A', 'A♯', 'B']
|
545 |
+
|
546 |
+
`F♭:min` will use double-flats
|
547 |
+
|
548 |
+
>>> librosa.key_to_notes('Fb:min')
|
549 |
+
['D𝄫', 'D♭', 'E𝄫', 'E♭', 'F♭', 'F', 'G♭', 'A𝄫', 'A♭', 'B𝄫', 'B♭', 'C♭']
|
550 |
+
"""
|
551 |
+
|
552 |
+
# Parse the key signature
|
553 |
+
match = KEY_RE.match(key)
|
554 |
+
|
555 |
+
if not match:
|
556 |
+
raise ParameterError(f"Improper key format: {key:s}")
|
557 |
+
|
558 |
+
pitch_map = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11}
|
559 |
+
acc_map = {"#": 1, "": 0, "b": -1, "!": -1, "♯": 1, "♭": -1}
|
560 |
+
|
561 |
+
tonic = match.group("tonic").upper()
|
562 |
+
accidental = match.group("accidental")
|
563 |
+
offset = acc_map[accidental]
|
564 |
+
|
565 |
+
scale = match.group("scale")[:3].lower()
|
566 |
+
|
567 |
+
# Determine major or minor
|
568 |
+
major = scale == "maj"
|
569 |
+
|
570 |
+
# calculate how many clockwise steps we are on CoF (== # sharps)
|
571 |
+
if major:
|
572 |
+
tonic_number = ((pitch_map[tonic] + offset) * 7) % 12
|
573 |
+
else:
|
574 |
+
tonic_number = ((pitch_map[tonic] + offset) * 7 + 9) % 12
|
575 |
+
|
576 |
+
# Decide if using flats or sharps
|
577 |
+
# Logic here is as follows:
|
578 |
+
# 1. respect the given notation for the tonic.
|
579 |
+
# Sharp tonics will always use sharps, likewise flats.
|
580 |
+
# 2. If no accidental in the tonic, try to minimize accidentals.
|
581 |
+
# 3. If there's a tie for accidentals, use sharp for major and flat for minor.
|
582 |
+
|
583 |
+
if offset < 0:
|
584 |
+
# use flats explicitly
|
585 |
+
use_sharps = False
|
586 |
+
|
587 |
+
elif offset > 0:
|
588 |
+
# use sharps explicitly
|
589 |
+
use_sharps = True
|
590 |
+
|
591 |
+
elif 0 <= tonic_number < 6:
|
592 |
+
use_sharps = True
|
593 |
+
|
594 |
+
elif tonic_number > 6:
|
595 |
+
use_sharps = False
|
596 |
+
|
597 |
+
# Basic note sequences for simple keys
|
598 |
+
notes_sharp = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]
|
599 |
+
notes_flat = ["C", "D♭", "D", "E♭", "E", "F", "G♭", "G", "A♭", "A", "B♭", "B"]
|
600 |
+
|
601 |
+
# These apply when we have >= 6 sharps
|
602 |
+
sharp_corrections = [
|
603 |
+
(5, "E♯"),
|
604 |
+
(0, "B♯"),
|
605 |
+
(7, "F𝄪"),
|
606 |
+
(2, "C𝄪"),
|
607 |
+
(9, "G𝄪"),
|
608 |
+
(4, "D𝄪"),
|
609 |
+
(11, "A𝄪"),
|
610 |
+
]
|
611 |
+
|
612 |
+
# These apply when we have >= 6 flats
|
613 |
+
flat_corrections = [
|
614 |
+
(11, "C♭"),
|
615 |
+
(4, "F♭"),
|
616 |
+
(9, "B𝄫"),
|
617 |
+
(2, "E𝄫"),
|
618 |
+
(7, "A𝄫"),
|
619 |
+
(0, "D𝄫"),
|
620 |
+
] # last would be (5, 'G𝄫')
|
621 |
+
|
622 |
+
# Apply a mod-12 correction to distinguish B#:maj from C:maj
|
623 |
+
n_sharps = tonic_number
|
624 |
+
if tonic_number == 0 and tonic == "B":
|
625 |
+
n_sharps = 12
|
626 |
+
|
627 |
+
if use_sharps:
|
628 |
+
# This will only execute if n_sharps >= 6
|
629 |
+
for n in range(0, n_sharps - 6 + 1):
|
630 |
+
index, name = sharp_corrections[n]
|
631 |
+
notes_sharp[index] = name
|
632 |
+
|
633 |
+
notes = notes_sharp
|
634 |
+
else:
|
635 |
+
n_flats = (12 - tonic_number) % 12
|
636 |
+
|
637 |
+
# This will only execute if tonic_number <= 6
|
638 |
+
for n in range(0, n_flats - 6 + 1):
|
639 |
+
index, name = flat_corrections[n]
|
640 |
+
notes_flat[index] = name
|
641 |
+
|
642 |
+
notes = notes_flat
|
643 |
+
|
644 |
+
# Finally, apply any unicode down-translation if necessary
|
645 |
+
if not unicode:
|
646 |
+
translations = str.maketrans({"♯": "#", "𝄪": "##", "♭": "b", "𝄫": "bb"})
|
647 |
+
notes = list(n.translate(translations) for n in notes)
|
648 |
+
|
649 |
+
return notes
|
650 |
+
|
651 |
+
|
652 |
+
def key_to_degrees(key: str) -> np.ndarray:
|
653 |
+
"""Construct the diatonic scale degrees for a given key.
|
654 |
+
|
655 |
+
Parameters
|
656 |
+
----------
|
657 |
+
key : str
|
658 |
+
Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``),
|
659 |
+
key must be lower-case (``maj`` or ``min``).
|
660 |
+
|
661 |
+
Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported.
|
662 |
+
|
663 |
+
Examples: ``C:maj, Db:min, A♭:min``.
|
664 |
+
|
665 |
+
Returns
|
666 |
+
-------
|
667 |
+
degrees : np.ndarray
|
668 |
+
An array containing the semitone numbers (0=C, 1=C#, ... 11=B)
|
669 |
+
for each of the seven scale degrees in the given key, starting
|
670 |
+
from the tonic.
|
671 |
+
|
672 |
+
See Also
|
673 |
+
--------
|
674 |
+
key_to_notes
|
675 |
+
|
676 |
+
Examples
|
677 |
+
--------
|
678 |
+
>>> librosa.key_to_degrees('C:maj')
|
679 |
+
array([ 0, 2, 4, 5, 7, 9, 11])
|
680 |
+
|
681 |
+
>>> librosa.key_to_degrees('C#:maj')
|
682 |
+
array([ 1, 3, 5, 6, 8, 10, 0])
|
683 |
+
|
684 |
+
>>> librosa.key_to_degrees('A:min')
|
685 |
+
array([ 9, 11, 0, 2, 4, 5, 7])
|
686 |
+
|
687 |
+
"""
|
688 |
+
notes = dict(
|
689 |
+
maj=np.array([0, 2, 4, 5, 7, 9, 11]), min=np.array([0, 2, 3, 5, 7, 8, 10])
|
690 |
+
)
|
691 |
+
|
692 |
+
match = KEY_RE.match(key)
|
693 |
+
|
694 |
+
if not match:
|
695 |
+
raise ParameterError(f"Improper key format: {key:s}")
|
696 |
+
|
697 |
+
pitch_map = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11}
|
698 |
+
acc_map = {"#": 1, "": 0, "b": -1, "!": -1, "♯": 1, "♭": -1}
|
699 |
+
tonic = match.group("tonic").upper()
|
700 |
+
accidental = match.group("accidental")
|
701 |
+
offset = acc_map[accidental]
|
702 |
+
|
703 |
+
scale = match.group("scale")[:3].lower()
|
704 |
+
|
705 |
+
return (notes[scale] + pitch_map[tonic] + offset) % 12
|
706 |
+
|
707 |
+
|
708 |
+
@cache(level=10)
|
709 |
+
def fifths_to_note(*, unison: str, fifths: int, unicode: bool = True) -> str:
|
710 |
+
"""Calculate the note name for a given number of perfect fifths
|
711 |
+
from a specified unison.
|
712 |
+
|
713 |
+
This function is primarily intended as a utility routine for
|
714 |
+
Functional Just System (FJS) notation conversions.
|
715 |
+
|
716 |
+
This function does not assume the "circle of fifths" or equal temperament,
|
717 |
+
so 12 fifths will not generally produce a note of the same pitch class
|
718 |
+
due to the accumulation of accidentals.
|
719 |
+
|
720 |
+
Parameters
|
721 |
+
----------
|
722 |
+
unison : str
|
723 |
+
The name of the starting (unison) note, e.g., 'C' or 'Bb'.
|
724 |
+
Unicode accidentals are supported.
|
725 |
+
|
726 |
+
fifths : integer
|
727 |
+
The number of perfect fifths to deviate from unison.
|
728 |
+
|
729 |
+
unicode : bool
|
730 |
+
If ``True`` (default), use Unicode symbols (♯𝄪♭𝄫)for accidentals.
|
731 |
+
|
732 |
+
If ``False``, accidentals will be encoded as low-order ASCII representations::
|
733 |
+
|
734 |
+
♯ -> #, 𝄪 -> ##, ♭ -> b, 𝄫 -> bb
|
735 |
+
|
736 |
+
Returns
|
737 |
+
-------
|
738 |
+
note : str
|
739 |
+
The name of the requested note
|
740 |
+
|
741 |
+
Examples
|
742 |
+
--------
|
743 |
+
>>> librosa.fifths_to_note(unison='C', fifths=6)
|
744 |
+
'F♯'
|
745 |
+
|
746 |
+
>>> librosa.fifths_to_note(unison='G', fifths=-3)
|
747 |
+
'B♭'
|
748 |
+
|
749 |
+
>>> librosa.fifths_to_note(unison='Eb', fifths=11, unicode=False)
|
750 |
+
'G#'
|
751 |
+
"""
|
752 |
+
# Starting the circle of fifths at F makes accidentals easier to count
|
753 |
+
COFMAP = "FCGDAEB"
|
754 |
+
|
755 |
+
acc_map = {
|
756 |
+
"#": 1,
|
757 |
+
"": 0,
|
758 |
+
"b": -1,
|
759 |
+
"!": -1,
|
760 |
+
"♯": 1,
|
761 |
+
"𝄪": 2,
|
762 |
+
"♭": -1,
|
763 |
+
"𝄫": -2,
|
764 |
+
"♮": 0,
|
765 |
+
}
|
766 |
+
|
767 |
+
if unicode:
|
768 |
+
acc_map_inv = {1: "♯", 2: "𝄪", -1: "♭", -2: "𝄫", 0: ""}
|
769 |
+
else:
|
770 |
+
acc_map_inv = {1: "#", 2: "##", -1: "b", -2: "bb", 0: ""}
|
771 |
+
|
772 |
+
match = NOTE_RE.match(unison)
|
773 |
+
|
774 |
+
if not match:
|
775 |
+
raise ParameterError(f"Improper note format: {unison:s}")
|
776 |
+
|
777 |
+
# Find unison in the alphabet
|
778 |
+
pitch = match.group("note").upper()
|
779 |
+
|
780 |
+
# Find the number of accidentals to start from
|
781 |
+
offset = np.sum([acc_map[o] for o in match.group("accidental")])
|
782 |
+
|
783 |
+
# Find the raw target note
|
784 |
+
circle_idx = COFMAP.index(pitch)
|
785 |
+
raw_output = COFMAP[(circle_idx + fifths) % 7]
|
786 |
+
|
787 |
+
# Now how many accidentals have we accrued?
|
788 |
+
# Equivalently, count times we cross a B<->F boundary
|
789 |
+
acc_index = offset + (circle_idx + fifths) // 7
|
790 |
+
|
791 |
+
# Compress multiple-accidentals as needed
|
792 |
+
acc_str = acc_map_inv[np.sign(acc_index) * 2] * int(
|
793 |
+
abs(acc_index) // 2
|
794 |
+
) + acc_map_inv[np.sign(acc_index)] * int(abs(acc_index) % 2)
|
795 |
+
|
796 |
+
return raw_output + acc_str
|
797 |
+
|
798 |
+
|
799 |
+
@jit(nopython=True, nogil=True, cache=False)
|
800 |
+
def __o_fold(d):
|
801 |
+
"""Compute the octave-folded interval.
|
802 |
+
|
803 |
+
This maps intervals to the range [1, 2).
|
804 |
+
|
805 |
+
This is part of the FJS notation converter.
|
806 |
+
It is equivalent to the `red` function described in the FJS
|
807 |
+
documentation.
|
808 |
+
"""
|
809 |
+
return d * (2.0 ** -np.floor(np.log2(d)))
|
810 |
+
|
811 |
+
|
812 |
+
@jit(nopython=True, nogil=True, cache=False)
|
813 |
+
def __bo_fold(d):
|
814 |
+
"""Compute the balanced, octave-folded interval.
|
815 |
+
|
816 |
+
This maps intervals to the range [sqrt(2)/2, sqrt(2)).
|
817 |
+
|
818 |
+
This is part of the FJS notation converter.
|
819 |
+
It is equivalent to the `reb` function described in the FJS
|
820 |
+
documentation, but with a simpler implementation.
|
821 |
+
"""
|
822 |
+
return d * (2.0 ** -np.round(np.log2(d)))
|
823 |
+
|
824 |
+
|
825 |
+
@jit(nopython=True, nogil=True, cache=False)
|
826 |
+
def __fifth_search(interval, tolerance):
|
827 |
+
"""Accelerated helper function for finding the number of fifths
|
828 |
+
to get within tolerance of a given interval.
|
829 |
+
|
830 |
+
This implementation will give up after 32 fifths
|
831 |
+
"""
|
832 |
+
log_tolerance = np.abs(np.log2(tolerance))
|
833 |
+
for power in range(32):
|
834 |
+
for sign in [1, -1]:
|
835 |
+
if (
|
836 |
+
np.abs(np.log2(__bo_fold(interval / 3.0 ** (power * sign))))
|
837 |
+
<= log_tolerance
|
838 |
+
):
|
839 |
+
return power * sign
|
840 |
+
power += 1
|
841 |
+
return power
|
842 |
+
|
843 |
+
|
844 |
+
# Translation grids for superscripts and subscripts
|
845 |
+
SUPER_TRANS = str.maketrans("0123456789", "⁰¹²³⁴⁵⁶⁷⁸⁹")
|
846 |
+
SUB_TRANS = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
|
847 |
+
|
848 |
+
|
849 |
+
@overload
|
850 |
+
def interval_to_fjs(
|
851 |
+
interval: _FloatLike_co,
|
852 |
+
*,
|
853 |
+
unison: str = ...,
|
854 |
+
tolerance: float = ...,
|
855 |
+
unicode: bool = ...,
|
856 |
+
) -> str:
|
857 |
+
...
|
858 |
+
|
859 |
+
|
860 |
+
@overload
|
861 |
+
def interval_to_fjs(
|
862 |
+
interval: _SequenceLike[_FloatLike_co],
|
863 |
+
*,
|
864 |
+
unison: str = ...,
|
865 |
+
tolerance: float = ...,
|
866 |
+
unicode: bool = ...,
|
867 |
+
) -> np.ndarray:
|
868 |
+
...
|
869 |
+
|
870 |
+
|
871 |
+
@overload
|
872 |
+
def interval_to_fjs(
|
873 |
+
interval: _ScalarOrSequence[_FloatLike_co],
|
874 |
+
*,
|
875 |
+
unison: str = ...,
|
876 |
+
tolerance: float = ...,
|
877 |
+
unicode: bool = ...,
|
878 |
+
) -> Union[str, np.ndarray]:
|
879 |
+
...
|
880 |
+
|
881 |
+
|
882 |
+
@vectorize(otypes="U", excluded=set(["unison", "tolerance", "unicode"]))
|
883 |
+
def interval_to_fjs(
|
884 |
+
interval: _ScalarOrSequence[_FloatLike_co],
|
885 |
+
*,
|
886 |
+
unison: str = "C",
|
887 |
+
tolerance: float = 65.0 / 63,
|
888 |
+
unicode: bool = True,
|
889 |
+
) -> Union[str, np.ndarray]:
|
890 |
+
"""Convert an interval to Functional Just System (FJS) notation.
|
891 |
+
|
892 |
+
See https://misotanni.github.io/fjs/en/index.html for a thorough overview
|
893 |
+
of the FJS notation system, and the examples below.
|
894 |
+
|
895 |
+
FJS conversion works by identifying a Pythagorean interval which is within
|
896 |
+
a specified tolerance of the target interval, which provides the core note
|
897 |
+
name. If the interval is derived from ratios other than perfect fifths,
|
898 |
+
then the remaining factors are encoded as superscripts for otonal
|
899 |
+
(increasing) intervals and subscripts for utonal (decreasing) intervals.
|
900 |
+
|
901 |
+
Parameters
|
902 |
+
----------
|
903 |
+
interval : float > 0 or iterable of floats
|
904 |
+
A (just) interval to notate in FJS.
|
905 |
+
|
906 |
+
unison : str
|
907 |
+
The name of the unison note (corresponding to `interval=1`).
|
908 |
+
|
909 |
+
tolerance : float
|
910 |
+
The tolerance threshold for identifying the core note name.
|
911 |
+
|
912 |
+
unicode : bool
|
913 |
+
If ``True`` (default), use Unicode symbols (♯𝄪♭𝄫)for accidentals,
|
914 |
+
and superscripts/subscripts for otonal and utonal accidentals.
|
915 |
+
|
916 |
+
If ``False``, accidentals will be encoded as low-order ASCII representations::
|
917 |
+
|
918 |
+
♯ -> #, 𝄪 -> ##, ♭ -> b, 𝄫 -> bb
|
919 |
+
|
920 |
+
Otonal and utonal accidentals will be denoted by `^##` and `_##`
|
921 |
+
respectively (see examples below).
|
922 |
+
|
923 |
+
Raises
|
924 |
+
------
|
925 |
+
ParameterError
|
926 |
+
If the provided interval is not positive
|
927 |
+
|
928 |
+
If the provided interval cannot be identified with a
|
929 |
+
just intonation prime factorization.
|
930 |
+
|
931 |
+
Returns
|
932 |
+
-------
|
933 |
+
note_fjs : str or np.ndarray(dtype=str)
|
934 |
+
The interval(s) relative to the given unison in FJS notation.
|
935 |
+
|
936 |
+
Examples
|
937 |
+
--------
|
938 |
+
Pythagorean intervals appear as expected, with no otonal
|
939 |
+
or utonal extensions:
|
940 |
+
|
941 |
+
>>> librosa.interval_to_fjs(3/2, unison='C')
|
942 |
+
'G'
|
943 |
+
>>> librosa.interval_to_fjs(4/3, unison='F')
|
944 |
+
'B♭'
|
945 |
+
|
946 |
+
A ptolemaic major third will appear with an otonal '5':
|
947 |
+
|
948 |
+
>>> librosa.interval_to_fjs(5/4, unison='A')
|
949 |
+
'C♯⁵'
|
950 |
+
|
951 |
+
And a ptolemaic minor third will appear with utonal '5':
|
952 |
+
|
953 |
+
>>> librosa.interval_to_fjs(6/5, unison='A')
|
954 |
+
'C₅'
|
955 |
+
|
956 |
+
More complex intervals will have compound accidentals.
|
957 |
+
For example:
|
958 |
+
|
959 |
+
>>> librosa.interval_to_fjs(25/14, unison='F#')
|
960 |
+
'E²⁵₇'
|
961 |
+
>>> librosa.interval_to_fjs(25/14, unison='F#', unicode=False)
|
962 |
+
'E^25_7'
|
963 |
+
|
964 |
+
Array inputs are also supported:
|
965 |
+
|
966 |
+
>>> librosa.interval_to_fjs([3/2, 4/3, 5/3])
|
967 |
+
array(['G', 'F', 'A⁵'], dtype='<U2')
|
968 |
+
|
969 |
+
"""
|
970 |
+
# suppressing the type check here because mypy won't introspect through
|
971 |
+
# numpy vectorization
|
972 |
+
if interval <= 0: # type: ignore
|
973 |
+
raise ParameterError(f"Interval={interval} must be strictly positive")
|
974 |
+
|
975 |
+
# Find the approximate number of fifth-steps to get within tolerance
|
976 |
+
# of the target interval
|
977 |
+
fifths = __fifth_search(interval, tolerance)
|
978 |
+
|
979 |
+
# determine the base note name
|
980 |
+
note_name = fifths_to_note(unison=unison, fifths=fifths, unicode=unicode)
|
981 |
+
|
982 |
+
# Get the prime factor expansion from the interval table
|
983 |
+
try:
|
984 |
+
# Balance the interval into the octave for lookup
|
985 |
+
interval_b = __o_fold(interval)
|
986 |
+
powers = INTERVALS[np.around(interval_b, decimals=6)]
|
987 |
+
except KeyError as exc:
|
988 |
+
raise ParameterError(f"Unknown interval={interval}") from exc
|
989 |
+
|
990 |
+
# Ignore pythagorean spelling
|
991 |
+
powers = {p: powers[p] for p in powers if p > 3}
|
992 |
+
|
993 |
+
# Split into otonal and utonal accidentals
|
994 |
+
otonal = np.prod([p ** powers[p] for p in powers if powers[p] > 0])
|
995 |
+
utonal = np.prod([p ** -powers[p] for p in powers if powers[p] < 0])
|
996 |
+
|
997 |
+
suffix = ""
|
998 |
+
if otonal > 1:
|
999 |
+
if unicode:
|
1000 |
+
suffix += f"{otonal:d}".translate(SUPER_TRANS)
|
1001 |
+
else:
|
1002 |
+
suffix += f"^{otonal}"
|
1003 |
+
|
1004 |
+
if utonal > 1:
|
1005 |
+
if unicode:
|
1006 |
+
suffix += f"{utonal:d}".translate(SUB_TRANS)
|
1007 |
+
else:
|
1008 |
+
suffix += f"_{utonal}"
|
1009 |
+
|
1010 |
+
return note_name + suffix
|
1011 |
+
|