renator commited on
Commit
6857109
·
1 Parent(s): 349e46d

update enviroments

Browse files
Files changed (2) hide show
  1. Dockerfile +3 -1
  2. 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
+