Spaces:
Configuration error
Configuration error
| #!/usr/bin/env python | |
| # -*- coding: utf-8 -*- | |
| """Music notation utilities""" | |
| import re | |
| import numpy as np | |
| from numba import jit | |
| from .intervals import INTERVALS | |
| from .._cache import cache | |
| from ..util.exceptions import ParameterError | |
| from typing import Dict, List, Union, overload | |
| from ..util.decorators import vectorize | |
| from .._typing import _ScalarOrSequence, _FloatLike_co, _SequenceLike | |
| __all__ = [ | |
| "key_to_degrees", | |
| "key_to_notes", | |
| "mela_to_degrees", | |
| "mela_to_svara", | |
| "thaat_to_degrees", | |
| "list_mela", | |
| "list_thaat", | |
| "fifths_to_note", | |
| "interval_to_fjs", | |
| ] | |
| 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], | |
| ) | |
| # Enumeration will start from 1 | |
| 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, | |
| ) | |
| } | |
| # Pre-compiled regular expressions for note and key parsing | |
| NOTE_RE = re.compile( | |
| r"^(?P<note>[A-Ga-g])" | |
| r"(?P<accidental>[#♯𝄪b!♭𝄫♮]*)" | |
| r"(?P<octave>[+-]?\d+)?" | |
| r"(?P<cents>[+-]\d+)?$" | |
| ) | |
| KEY_RE = re.compile( | |
| r"^(?P<tonic>[A-Ga-g])" r"(?P<accidental>[#♯b!♭]?)" r":(?P<scale>(maj|min)(or)?)$" | |
| ) | |
| def thaat_to_degrees(thaat: str) -> np.ndarray: | |
| """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: Union[str, int]) -> np.ndarray: | |
| """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(f"mela={mela} must be in range [1, 72]") | |
| # always have Sa [0] | |
| degrees = [0] | |
| # Fill in Ri and Ga | |
| lower = index % 36 | |
| if 0 <= lower < 6: | |
| # Ri1, Ga1 | |
| degrees.extend([1, 2]) | |
| elif 6 <= lower < 12: | |
| # Ri1, Ga2 | |
| degrees.extend([1, 3]) | |
| elif 12 <= lower < 18: | |
| # Ri1, Ga3 | |
| degrees.extend([1, 4]) | |
| elif 18 <= lower < 24: | |
| # Ri2, Ga2 | |
| degrees.extend([2, 3]) | |
| elif 24 <= lower < 30: | |
| # Ri2, Ga3 | |
| degrees.extend([2, 4]) | |
| else: | |
| # Ri3, Ga3 | |
| degrees.extend([3, 4]) | |
| # Determine Ma | |
| if index < 36: | |
| # Ma1 | |
| degrees.append(5) | |
| else: | |
| # Ma2 | |
| degrees.append(6) | |
| # always have Pa [7] | |
| degrees.append(7) | |
| # Determine Dha and Ni | |
| upper = index % 6 | |
| if upper == 0: | |
| # Dha1, Ni1 | |
| degrees.extend([8, 9]) | |
| elif upper == 1: | |
| # Dha1, Ni2 | |
| degrees.extend([8, 10]) | |
| elif upper == 2: | |
| # Dha1, Ni3 | |
| degrees.extend([8, 11]) | |
| elif upper == 3: | |
| # Dha2, Ni2 | |
| degrees.extend([9, 10]) | |
| elif upper == 4: | |
| # Dha2, Ni3 | |
| degrees.extend([9, 11]) | |
| else: | |
| # Dha3, Ni3 | |
| degrees.extend([10, 11]) | |
| return np.array(degrees) | |
| def mela_to_svara( | |
| mela: Union[str, int], *, abbr: bool = True, unicode: bool = True | |
| ) -> List[str]: | |
| """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₃'] | |
| """ | |
| # The following will be constant for all ragas | |
| svara_map = [ | |
| "Sa", | |
| "Ri\u2081", | |
| "", # Ri2/Ga1 | |
| "", # Ri3/Ga2 | |
| "Ga\u2083", | |
| "Ma\u2081", | |
| "Ma\u2082", | |
| "Pa", | |
| "Dha\u2081", | |
| "", # Dha2/Ni1 | |
| "", # Dha3/Ni2 | |
| "Ni\u2083", | |
| ] | |
| if isinstance(mela, str): | |
| mela_idx = MELAKARTA_MAP[mela.lower()] - 1 | |
| elif 0 < mela <= 72: | |
| mela_idx = mela - 1 | |
| else: | |
| raise ParameterError(f"mela={mela} must be in range [1, 72]") | |
| # Determine Ri2/Ga1 | |
| lower = mela_idx % 36 | |
| if lower < 6: | |
| # First six will have Ri1/Ga1 | |
| svara_map[2] = "Ga\u2081" | |
| else: | |
| # All others have either Ga2/Ga3 | |
| # So we'll call this Ri2 | |
| svara_map[2] = "Ri\u2082" | |
| # Determine Ri3/Ga2 | |
| if lower < 30: | |
| # First thirty should get Ga2 | |
| svara_map[3] = "Ga\u2082" | |
| else: | |
| # Only the last six have Ri3 | |
| svara_map[3] = "Ri\u2083" | |
| upper = mela_idx % 6 | |
| # Determine Dha2/Ni1 | |
| if upper == 0: | |
| # these are the only ones with Ni1 | |
| svara_map[9] = "Ni\u2081" | |
| else: | |
| # Everyone else has Dha2 | |
| svara_map[9] = "Dha\u2082" | |
| # Determine Dha3/Ni2 | |
| if upper == 5: | |
| # This one has Dha3 | |
| svara_map[10] = "Dha\u2083" | |
| else: | |
| # Everyone else has Ni2 | |
| svara_map[10] = "Ni\u2082" | |
| if abbr: | |
| t_abbr = str.maketrans({"a": "", "h": "", "i": ""}) | |
| svara_map = [s.translate(t_abbr) for s in svara_map] | |
| if not unicode: | |
| t_uni = str.maketrans({"\u2081": "1", "\u2082": "2", "\u2083": "3"}) | |
| svara_map = [s.translate(t_uni) for s in svara_map] | |
| return list(svara_map) | |
| def list_mela() -> Dict[str, int]: | |
| """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[str]: | |
| """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()) | |
| def key_to_notes(key: str, *, unicode: bool = True) -> List[str]: | |
| """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♭'] | |
| """ | |
| # Parse the key signature | |
| match = KEY_RE.match(key) | |
| if not match: | |
| raise ParameterError(f"Improper key format: {key:s}") | |
| 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() | |
| # Determine major or minor | |
| major = scale == "maj" | |
| # calculate how many clockwise steps we are on CoF (== # sharps) | |
| if major: | |
| tonic_number = ((pitch_map[tonic] + offset) * 7) % 12 | |
| else: | |
| tonic_number = ((pitch_map[tonic] + offset) * 7 + 9) % 12 | |
| # Decide if using flats or sharps | |
| # Logic here is as follows: | |
| # 1. respect the given notation for the tonic. | |
| # Sharp tonics will always use sharps, likewise flats. | |
| # 2. If no accidental in the tonic, try to minimize accidentals. | |
| # 3. If there's a tie for accidentals, use sharp for major and flat for minor. | |
| if offset < 0: | |
| # use flats explicitly | |
| use_sharps = False | |
| elif offset > 0: | |
| # use sharps explicitly | |
| use_sharps = True | |
| elif 0 <= tonic_number < 6: | |
| use_sharps = True | |
| elif tonic_number > 6: | |
| use_sharps = False | |
| # Basic note sequences for simple keys | |
| 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"] | |
| # These apply when we have >= 6 sharps | |
| sharp_corrections = [ | |
| (5, "E♯"), | |
| (0, "B♯"), | |
| (7, "F𝄪"), | |
| (2, "C𝄪"), | |
| (9, "G𝄪"), | |
| (4, "D𝄪"), | |
| (11, "A𝄪"), | |
| ] | |
| # These apply when we have >= 6 flats | |
| flat_corrections = [ | |
| (11, "C♭"), | |
| (4, "F♭"), | |
| (9, "B𝄫"), | |
| (2, "E𝄫"), | |
| (7, "A𝄫"), | |
| (0, "D𝄫"), | |
| ] # last would be (5, 'G𝄫') | |
| # Apply a mod-12 correction to distinguish B#:maj from C:maj | |
| n_sharps = tonic_number | |
| if tonic_number == 0 and tonic == "B": | |
| n_sharps = 12 | |
| if use_sharps: | |
| # This will only execute if n_sharps >= 6 | |
| 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 | |
| # This will only execute if tonic_number <= 6 | |
| for n in range(0, n_flats - 6 + 1): | |
| index, name = flat_corrections[n] | |
| notes_flat[index] = name | |
| notes = notes_flat | |
| # Finally, apply any unicode down-translation if necessary | |
| if not unicode: | |
| translations = str.maketrans({"♯": "#", "𝄪": "##", "♭": "b", "𝄫": "bb"}) | |
| notes = list(n.translate(translations) for n in notes) | |
| return notes | |
| def key_to_degrees(key: str) -> np.ndarray: | |
| """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 = KEY_RE.match(key) | |
| if not match: | |
| raise ParameterError(f"Improper key format: {key:s}") | |
| 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 | |
| def fifths_to_note(*, unison: str, fifths: int, unicode: bool = True) -> str: | |
| """Calculate the note name for a given number of perfect fifths | |
| from a specified unison. | |
| This function is primarily intended as a utility routine for | |
| Functional Just System (FJS) notation conversions. | |
| This function does not assume the "circle of fifths" or equal temperament, | |
| so 12 fifths will not generally produce a note of the same pitch class | |
| due to the accumulation of accidentals. | |
| Parameters | |
| ---------- | |
| unison : str | |
| The name of the starting (unison) note, e.g., 'C' or 'Bb'. | |
| Unicode accidentals are supported. | |
| fifths : integer | |
| The number of perfect fifths to deviate from unison. | |
| unicode : bool | |
| If ``True`` (default), use Unicode symbols (♯𝄪♭𝄫)for accidentals. | |
| If ``False``, accidentals will be encoded as low-order ASCII representations:: | |
| ♯ -> #, 𝄪 -> ##, ♭ -> b, 𝄫 -> bb | |
| Returns | |
| ------- | |
| note : str | |
| The name of the requested note | |
| Examples | |
| -------- | |
| >>> librosa.fifths_to_note(unison='C', fifths=6) | |
| 'F♯' | |
| >>> librosa.fifths_to_note(unison='G', fifths=-3) | |
| 'B♭' | |
| >>> librosa.fifths_to_note(unison='Eb', fifths=11, unicode=False) | |
| 'G#' | |
| """ | |
| # Starting the circle of fifths at F makes accidentals easier to count | |
| COFMAP = "FCGDAEB" | |
| acc_map = { | |
| "#": 1, | |
| "": 0, | |
| "b": -1, | |
| "!": -1, | |
| "♯": 1, | |
| "𝄪": 2, | |
| "♭": -1, | |
| "𝄫": -2, | |
| "♮": 0, | |
| } | |
| if unicode: | |
| acc_map_inv = {1: "♯", 2: "𝄪", -1: "♭", -2: "𝄫", 0: ""} | |
| else: | |
| acc_map_inv = {1: "#", 2: "##", -1: "b", -2: "bb", 0: ""} | |
| match = NOTE_RE.match(unison) | |
| if not match: | |
| raise ParameterError(f"Improper note format: {unison:s}") | |
| # Find unison in the alphabet | |
| pitch = match.group("note").upper() | |
| # Find the number of accidentals to start from | |
| offset = np.sum([acc_map[o] for o in match.group("accidental")]) | |
| # Find the raw target note | |
| circle_idx = COFMAP.index(pitch) | |
| raw_output = COFMAP[(circle_idx + fifths) % 7] | |
| # Now how many accidentals have we accrued? | |
| # Equivalently, count times we cross a B<->F boundary | |
| acc_index = offset + (circle_idx + fifths) // 7 | |
| # Compress multiple-accidentals as needed | |
| acc_str = acc_map_inv[np.sign(acc_index) * 2] * int( | |
| abs(acc_index) // 2 | |
| ) + acc_map_inv[np.sign(acc_index)] * int(abs(acc_index) % 2) | |
| return raw_output + acc_str | |
| def __o_fold(d): | |
| """Compute the octave-folded interval. | |
| This maps intervals to the range [1, 2). | |
| This is part of the FJS notation converter. | |
| It is equivalent to the `red` function described in the FJS | |
| documentation. | |
| """ | |
| return d * (2.0 ** -np.floor(np.log2(d))) | |
| def __bo_fold(d): | |
| """Compute the balanced, octave-folded interval. | |
| This maps intervals to the range [sqrt(2)/2, sqrt(2)). | |
| This is part of the FJS notation converter. | |
| It is equivalent to the `reb` function described in the FJS | |
| documentation, but with a simpler implementation. | |
| """ | |
| return d * (2.0 ** -np.round(np.log2(d))) | |
| def __fifth_search(interval, tolerance): | |
| """Accelerated helper function for finding the number of fifths | |
| to get within tolerance of a given interval. | |
| This implementation will give up after 32 fifths | |
| """ | |
| log_tolerance = np.abs(np.log2(tolerance)) | |
| for power in range(32): | |
| for sign in [1, -1]: | |
| if ( | |
| np.abs(np.log2(__bo_fold(interval / 3.0 ** (power * sign)))) | |
| <= log_tolerance | |
| ): | |
| return power * sign | |
| power += 1 | |
| return power | |
| # Translation grids for superscripts and subscripts | |
| SUPER_TRANS = str.maketrans("0123456789", "⁰¹²³⁴⁵⁶⁷⁸⁹") | |
| SUB_TRANS = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉") | |
| def interval_to_fjs( | |
| interval: _FloatLike_co, | |
| *, | |
| unison: str = ..., | |
| tolerance: float = ..., | |
| unicode: bool = ..., | |
| ) -> str: | |
| ... | |
| def interval_to_fjs( | |
| interval: _SequenceLike[_FloatLike_co], | |
| *, | |
| unison: str = ..., | |
| tolerance: float = ..., | |
| unicode: bool = ..., | |
| ) -> np.ndarray: | |
| ... | |
| def interval_to_fjs( | |
| interval: _ScalarOrSequence[_FloatLike_co], | |
| *, | |
| unison: str = ..., | |
| tolerance: float = ..., | |
| unicode: bool = ..., | |
| ) -> Union[str, np.ndarray]: | |
| ... | |
| def interval_to_fjs( | |
| interval: _ScalarOrSequence[_FloatLike_co], | |
| *, | |
| unison: str = "C", | |
| tolerance: float = 65.0 / 63, | |
| unicode: bool = True, | |
| ) -> Union[str, np.ndarray]: | |
| """Convert an interval to Functional Just System (FJS) notation. | |
| See https://misotanni.github.io/fjs/en/index.html for a thorough overview | |
| of the FJS notation system, and the examples below. | |
| FJS conversion works by identifying a Pythagorean interval which is within | |
| a specified tolerance of the target interval, which provides the core note | |
| name. If the interval is derived from ratios other than perfect fifths, | |
| then the remaining factors are encoded as superscripts for otonal | |
| (increasing) intervals and subscripts for utonal (decreasing) intervals. | |
| Parameters | |
| ---------- | |
| interval : float > 0 or iterable of floats | |
| A (just) interval to notate in FJS. | |
| unison : str | |
| The name of the unison note (corresponding to `interval=1`). | |
| tolerance : float | |
| The tolerance threshold for identifying the core note name. | |
| unicode : bool | |
| If ``True`` (default), use Unicode symbols (♯𝄪♭𝄫)for accidentals, | |
| and superscripts/subscripts for otonal and utonal accidentals. | |
| If ``False``, accidentals will be encoded as low-order ASCII representations:: | |
| ♯ -> #, 𝄪 -> ##, ♭ -> b, 𝄫 -> bb | |
| Otonal and utonal accidentals will be denoted by `^##` and `_##` | |
| respectively (see examples below). | |
| Raises | |
| ------ | |
| ParameterError | |
| If the provided interval is not positive | |
| If the provided interval cannot be identified with a | |
| just intonation prime factorization. | |
| Returns | |
| ------- | |
| note_fjs : str or np.ndarray(dtype=str) | |
| The interval(s) relative to the given unison in FJS notation. | |
| Examples | |
| -------- | |
| Pythagorean intervals appear as expected, with no otonal | |
| or utonal extensions: | |
| >>> librosa.interval_to_fjs(3/2, unison='C') | |
| 'G' | |
| >>> librosa.interval_to_fjs(4/3, unison='F') | |
| 'B♭' | |
| A ptolemaic major third will appear with an otonal '5': | |
| >>> librosa.interval_to_fjs(5/4, unison='A') | |
| 'C♯⁵' | |
| And a ptolemaic minor third will appear with utonal '5': | |
| >>> librosa.interval_to_fjs(6/5, unison='A') | |
| 'C₅' | |
| More complex intervals will have compound accidentals. | |
| For example: | |
| >>> librosa.interval_to_fjs(25/14, unison='F#') | |
| 'E²⁵₇' | |
| >>> librosa.interval_to_fjs(25/14, unison='F#', unicode=False) | |
| 'E^25_7' | |
| Array inputs are also supported: | |
| >>> librosa.interval_to_fjs([3/2, 4/3, 5/3]) | |
| array(['G', 'F', 'A⁵'], dtype='<U2') | |
| """ | |
| # suppressing the type check here because mypy won't introspect through | |
| # numpy vectorization | |
| if interval <= 0: # type: ignore | |
| raise ParameterError(f"Interval={interval} must be strictly positive") | |
| # Find the approximate number of fifth-steps to get within tolerance | |
| # of the target interval | |
| fifths = __fifth_search(interval, tolerance) | |
| # determine the base note name | |
| note_name = fifths_to_note(unison=unison, fifths=fifths, unicode=unicode) | |
| # Get the prime factor expansion from the interval table | |
| try: | |
| # Balance the interval into the octave for lookup | |
| interval_b = __o_fold(interval) | |
| powers = INTERVALS[np.around(interval_b, decimals=6)] | |
| except KeyError as exc: | |
| raise ParameterError(f"Unknown interval={interval}") from exc | |
| # Ignore pythagorean spelling | |
| powers = {p: powers[p] for p in powers if p > 3} | |
| # Split into otonal and utonal accidentals | |
| otonal = np.prod([p ** powers[p] for p in powers if powers[p] > 0]) | |
| utonal = np.prod([p ** -powers[p] for p in powers if powers[p] < 0]) | |
| suffix = "" | |
| if otonal > 1: | |
| if unicode: | |
| suffix += f"{otonal:d}".translate(SUPER_TRANS) | |
| else: | |
| suffix += f"^{otonal}" | |
| if utonal > 1: | |
| if unicode: | |
| suffix += f"{utonal:d}".translate(SUB_TRANS) | |
| else: | |
| suffix += f"_{utonal}" | |
| return note_name + suffix | |