From f636512646a7a2872c87b18f4fc13ebbb561ffcf Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Thu, 2 Jul 2020 17:45:26 -0400 Subject: [PATCH 01/15] protoyping svara conversion --- librosa/core/time_frequency.py | 180 +++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/librosa/core/time_frequency.py b/librosa/core/time_frequency.py index 94fefc8940..5e9c045835 100644 --- a/librosa/core/time_frequency.py +++ b/librosa/core/time_frequency.py @@ -1936,3 +1936,183 @@ def key_to_degrees(key): scale = match.group('scale')[:3].lower() return (notes[scale] + pitch_map[tonic] + offset) % 12 + + +def midi_to_svara_h(midi, Sa=60, abbr=True, octave=True, unicode=True): + """Convert MIDI numbers to Hindustani svara + + Parameters + ---------- + midi : numeric + The MIDI numbers to convert + + Sa : number > 0 + MIDI number of the reference Sa. + + Default: 60 (261.6 Hz, `C4`) + + abbr : bool + If `True` (default) return abbreviated names ('S', 'r', 'R', 'g', 'G', ...) + + If `False`, return long-form names ('Sa', 're', 'Re', 'ga', 'Ga', ...) + + octave : bool + If `True`, decorate svara in neighboring octaves with over- or under-dots. + + If `False`, ignore octave height information. + + unicode : bool + If `True`, use unicode symbols to decorate octave information. + + If `False`, use low-order ASCII (' and ,) for octave decorations. + + This only takes effect if `octave=True`. + + Returns + ------- + svara : str or list of str + The svara corresponding to the given MIDI number + + See Also + -------- + hz_to_svara_h + note_to_svara_h + svara_h_to_note + + Examples + -------- + """ + + SVARA_MAP_LONG = ['Sa', 're', 'Re', 'ga', 'Ga', 'ma', 'Ma', + 'Pa', 'dha', 'Dha', 'ni', 'Ni'] + + SVARA_MAP_SHORT = list('SrRgGmMPdDnN') + + if not np.isscalar(midi): + return [midi_to_svara_h(m, Sa=Sa, abbr=abbr, octave=octave, unicode=unicode) + for m in midi] + + svara_num = int(np.round(midi - Sa)) + + if abbr: + svara = SVARA_MAP_SHORT[svara_num % 12] + else: + svara = SVARA_MAP_LONG[svara_num % 12] + + if octave: + if 24 > svara_num >= 12: + if unicode: + svara = svara[0] + "\u0307" + svara[1:] + else: + svara += "'" + elif -12 <= svara_num < 0: + if unicode: + svara = svara[0] + "\u0323" + svara[1:] + else: + svara += "," + + return svara + + +def hz_to_svara_h(frequencies, Sa=None, abbr=True, octave=True, unicode=True): + '''Convert frequencies (in Hz) to Hindustani svara + + Note that this conversion assumes 12-tone equal temperament. + + Parameters + ---------- + + Returns + ------- + + See Also + -------- + midi_to_svara_h + + Examples + -------- + ''' + + if Sa is None: + Sa = note_to_hz('C2') + + midis = hz_to_midi(frequencies) + return midi_to_svara_h(midis, Sa=hz_to_midi(Sa), + abbr=abbr, octave=octave, unicode=unicode) + + +def note_to_svara_h(notes, Sa=None, abbr=True, octave=True, unicode=True): + '''Convert western notes to Hindustani svara + + Note that this conversion assumes 12-tone equal temperament. + + Parameters + ---------- + + Returns + ------- + + See Also + -------- + + Examples + -------- + ''' + if Sa is None: + Sa = 'C2' + + midis = note_to_midi(notes, round_midi=False) + + return midi_to_svara_h(midis, Sa=note_to_midi(Sa), abbr=abbr, octave=octave, + unicode=unicode) + + +# TODO: maybe we need to put a reference sa here? +# depends on if this is used for absolute or relative indexing +def thaat_to_degrees(thaat): + '''Construct the svara indices (degrees) for a given thaat + ''' + + THAAT_MAP = dict(bilaval =[0, 2, 4, 5, 7, 9, 11], + khamaj =[0, 2, 4, 5, 7, 9, 10], + kafi =[0, 2, 3, 5, 7, 9, 10], + asavari =[0, 2, 3, 5, 7, 8, 10], + bhairavi =[0, 1, 3, 5, 7, 8, 10], + kalyan =[0, 2, 4, 6, 7, 9, 11], + marva =[0, 1, 4, 6, 7, 9, 11], + poorvi =[0, 1, 4, 6, 7, 8, 11], + todi =[0, 1, 3, 6, 7, 8, 11], + bhairav =[0, 1, 4, 5, 7, 8, 11]) + + return np.asarray(THAAT_MAP[thaat.tolower()]) + + +# Carnatic notes + +# melas < 36 +# M1 +# melas >= 36 +# M2 + +# p = m % 36 +# R1, G1 ==> 0 <= p < 6 +# R1, G2 ==> 6 <= p < 12 +# R1, G3 ==> 12 <= p < 18 +# R2, G1 XXX +# R2, G2 ==> 18 <= p < 24 +# R2, G3 ==> 24 <= p < 30 +# R3, G1 XXX +# R3, G2 XXX +# R3, G3 ==> 30 <= p < 36 + +# q = m % 6 +# D1, N1 q == 1 +# D1, N2 q == 2 +# D1, N3 q == 3 +# D2, N1 XXX +# D2, N2 q == 4 +# D2, N3 q == 5 +# D3, N1 XXX +# D3, N2 XXX +# D3, N3 q == 0 + From d0e5a541d625915abcf0023ad8a23968c31ebb6c Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Fri, 3 Jul 2020 12:34:23 -0400 Subject: [PATCH 02/15] added carnatic support in converters --- librosa/core/time_frequency.py | 454 +++++++++++++++++++++++++++++---- 1 file changed, 403 insertions(+), 51 deletions(-) diff --git a/librosa/core/time_frequency.py b/librosa/core/time_frequency.py index 5e9c045835..4081fabcac 100644 --- a/librosa/core/time_frequency.py +++ b/librosa/core/time_frequency.py @@ -35,7 +35,12 @@ 'samples_like', 'times_like', 'key_to_notes', - 'key_to_degrees'] + 'key_to_degrees', + 'midi_to_svara_h', 'midi_to_svara_c', + 'note_to_svara_h', 'note_to_svara_c', + 'hz_to_svara_h', 'hz_to_svara_c', + 'thaat_to_degrees', + 'mela_to_svara', 'mela_to_degrees'] def frames_to_samples(frames, hop_length=512, n_fft=None): @@ -1971,22 +1976,21 @@ def midi_to_svara_h(midi, Sa=60, abbr=True, octave=True, unicode=True): Returns ------- svara : str or list of str - The svara corresponding to the given MIDI number + The svara corresponding to the given MIDI number(s) See Also -------- hz_to_svara_h note_to_svara_h - svara_h_to_note Examples -------- """ - SVARA_MAP_LONG = ['Sa', 're', 'Re', 'ga', 'Ga', 'ma', 'Ma', - 'Pa', 'dha', 'Dha', 'ni', 'Ni'] + SVARA_MAP = ['Sa', 're', 'Re', 'ga', 'Ga', 'ma', 'Ma', + 'Pa', 'dha', 'Dha', 'ni', 'Ni'] - SVARA_MAP_SHORT = list('SrRgGmMPdDnN') + SVARA_MAP_SHORT = list(s[0] for s in SVARA_MAP) if not np.isscalar(midi): return [midi_to_svara_h(m, Sa=Sa, abbr=abbr, octave=octave, unicode=unicode) @@ -1997,7 +2001,7 @@ def midi_to_svara_h(midi, Sa=60, abbr=True, octave=True, unicode=True): if abbr: svara = SVARA_MAP_SHORT[svara_num % 12] else: - svara = SVARA_MAP_LONG[svara_num % 12] + svara = SVARA_MAP[svara_num % 12] if octave: if 24 > svara_num >= 12: @@ -2067,52 +2071,400 @@ def note_to_svara_h(notes, Sa=None, abbr=True, octave=True, unicode=True): unicode=unicode) -# TODO: maybe we need to put a reference sa here? -# depends on if this is used for absolute or relative indexing +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]) def thaat_to_degrees(thaat): '''Construct the svara indices (degrees) for a given thaat + + Parameters + ---------- + thaat : str + The name of the thaat + + Returns + ------- + indices : np.ndarray + A list of the seven svara indicies (starting from 0=Sa) + contained in the specified thaat + + See Also + -------- + key_to_degrees + mela_to_degrees + ''' + return np.asarray(THAAT_MAP[thaat.lower()]) + + +def midi_to_svara_c(midi, mela, Sa=60, abbr=True, octave=True, unicode=True): + '''Convert MIDI numbers to Carnatic svara within a given melakarta raga + + Parameters + ---------- + midi : numeric + The MIDI numbers to convert + + mela : int or str + The name or index of the melakarta raga + + Sa : number > 0 + MIDI number of the reference Sa. + + Default: 60 (261.6 Hz, `C4`) + + abbr : bool + If `True` (default) return abbreviated names ('S', 'R1', 'R2', 'G1', 'G2', ...) + + If `False`, return long-form names ('Sa', 'Ri1', 'Ri2', 'Ga1', 'Ga2', ...) + + octave : bool + If `True`, decorate svara in neighboring octaves with over- or under-dots. + + If `False`, ignore octave height information. + + unicode : bool + If `True`, use unicode symbols to decorate octave information and subscript + numbers. + + If `False`, use low-order ASCII (' and ,) for octave decorations. + + Returns + ------- + svara : str or list of str + The svara corresponding to the given MIDI number(s) + + See Also + -------- + hz_to_svara_c + note_to_svara_c + mela_to_degrees + mela_to_svara + ''' + if not np.isscalar(midi): + return [midi_to_svara_c(m, mela, Sa=Sa, abbr=abbr, + octave=octave, unicode=unicode) + for m in midi] + + svara_num = int(np.round(midi - Sa)) + + svara_map = mela_to_svara(mela, abbr=abbr, unicode=unicode) + + svara = svara_map[svara_num % 12] + + if octave: + if 24 > svara_num >= 12: + if unicode: + svara = svara[0] + "\u0307" + svara[1:] + else: + svara += "'" + elif -12 <= svara_num < 0: + if unicode: + svara = svara[0] + "\u0323" + svara[1:] + else: + svara += "," + + return svara + + +def hz_to_svara_c(frequencies, mela, Sa=None, abbr=True, octave=True, unicode=True): + '''Convert frequencies (in Hz) to Carnatic svara + + Note that this conversion assumes 12-tone equal temperament. + + Parameters + ---------- + + Returns + ------- + + See Also + -------- + midi_to_svara_c + + Examples + -------- + ''' + + if Sa is None: + Sa = note_to_hz('C2') + + midis = hz_to_midi(frequencies) + return midi_to_svara_c(midis, mela=mela, Sa=hz_to_midi(Sa), + abbr=abbr, octave=octave, unicode=unicode) + + +def note_to_svara_c(notes, mela Sa=None, abbr=True, octave=True, unicode=True): + '''Convert western notes to Carnatic svara + + Note that this conversion assumes 12-tone equal temperament. + + Parameters + ---------- + + Returns + ------- + + See Also + -------- + + Examples + -------- + ''' + if Sa is None: + Sa = 'C2' + + midis = note_to_midi(notes, round_midi=False) + + return midi_to_svara_c(midis, mela, Sa=note_to_midi(Sa), + abbr=abbr, octave=octave, + unicode=unicode) + + +MELAKARTA_MAP = {k: i + for i, k in enumerate(['kanakanki', 'ratnangi', 'ganamurti', + 'vanaspati', 'manavati', 'tanarupi', + 'senavati', 'hanumatodi', 'dhenuka', + 'natakapriya', 'kokilapriya', 'rupavati', + 'gayakapriya', 'vakulabharanam', 'mayamalavagoulai', + 'chakravaham', 'suryakantam', 'hatakambhari', + 'jhankaradhwani', 'natabhairavi', 'keeravani', + 'kharaharapriya', 'gowrimanohari', 'varunapriya', + 'mararanjani', 'charukesi', 'sarasangi', + 'harikambhoji', 'dheerasankarabharanam', 'naganandini', + 'yagapriya', 'ragavardhini', 'gangeyabhusani', + 'vagadheeswari', 'sulini', 'chalanattai', + 'salagam', 'jalarnavam', 'jhalavarali', + 'navaneetam', 'pavani', 'raghupriya', + 'gavambodhi', 'bhavapriya', 'subhapantuvarali', + 'shadvigamargini', 'suvarnangi', 'divyamani', + 'dhavalambari', 'namanarayani', 'kamavardhini', + 'ramapriya', 'gamanasrama', 'viswambhari', + 'syamalangi', 'shanmukhapriya', 'simhendramadhyamam', + 'hemavati', 'dharmavati', 'nitimati', + 'kantamani', 'rishabhapriya', 'latangi', + 'vachaspati', 'mechakalyani', 'chitrambhari', + 'sucharitra', 'jyotiswarupini', 'dhatuvardhini', + 'nasikabhushani', 'kasalam', 'rasikapriya'])} + +def mela_to_degrees(mela): + '''Construct the svara indices (degrees) for a given melakarta raga + + Parameters + ---------- + mela : str or int + Either the name or integer index ([0, 71]) of the melakarta raga + + Returns + ------- + degrees : np.ndarray + ''' - 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]) - - return np.asarray(THAAT_MAP[thaat.tolower()]) - - -# Carnatic notes - -# melas < 36 -# M1 -# melas >= 36 -# M2 - -# p = m % 36 -# R1, G1 ==> 0 <= p < 6 -# R1, G2 ==> 6 <= p < 12 -# R1, G3 ==> 12 <= p < 18 -# R2, G1 XXX -# R2, G2 ==> 18 <= p < 24 -# R2, G3 ==> 24 <= p < 30 -# R3, G1 XXX -# R3, G2 XXX -# R3, G3 ==> 30 <= p < 36 - -# q = m % 6 -# D1, N1 q == 1 -# D1, N2 q == 2 -# D1, N3 q == 3 -# D2, N1 XXX -# D2, N2 q == 4 -# D2, N3 q == 5 -# D3, N1 XXX -# D3, N2 XXX -# D3, N3 q == 0 + if isinstance(mela, str): + index = MELAKARTA_MAP[mela.lower()] + elif 0 <= mela < 72: + index = mela + else: + raise ParameterError('mela={} must be in range [0, 72['.format(mela)) + + # 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) + + +@cache(level=10) +def mela_to_svara(mela, abbr=True, unicode=True): + '''Spell the Carnatic svara names for a given melakarta raga + + This function exists to resolve enharmonic equivalences between + pitch classes: + + - Ri2 / Ga1 + - Ri3 / Ga2 + - Dha2 / Ni1 + - Dha3 / Ni2 + + For svara outside the raga, names are chosen to preserve orderings + so that all Ri precede all Ga, and all Dha precede all Ni. + + Parameters + ---------- + mela : str or int + the name or numerical index of the melakarta raga + + abbr : bool + If `True`, use single-letter svara names: S, R, G, ... + + If `False`, use full names: Sa, Ri, Ga, ... + + unicode : bool + If `True`, use unicode symbols for numberings, e.g., Ri\u2081 + + If `False`, use low-order ASCII, e.g., Ri1. + + Returns + ------- + svara : list of strings + + The svara names for each of the 12 pitch classes. + + See Also + -------- + key_to_notes + mela_to_degrees + + Examples + -------- + Melakarta #0 (Kanakanki) uses R1, G1, D1, N1 + + >>> librosa.mela_to_svara(0) + ['S', 'R₁', 'G₁', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] + + #18 (Jhankaradhwani) uses R2 and G2 so the third svara are Ri: + + >>> librosa.mela_to_svara(18) + ['S', 'R₁', 'R₂', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] + + #30 (Yagapriya) uses R3 and G3, so third and fourth svara are Ri: + + >>> librosa.mela_to_svara(30) + ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] + + #33 (Vagadheeswari) uses D2 and N2, so Ni1 becomes Dha2: + + >>> librosa.mela_to_svara(33) + ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'N₂', 'N₃'] + + #35 (Chalanattai) uses D3 and N3, so Ni2 becomes Dha3: + + >>> librosa.mela_to_svara(35) + ['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', + None, # Ri2/Ga1 + None, # Ri3/Ga2 + 'Ga\u2083', + 'Ma\u2081', 'Ma\u2082', + 'Pa', + 'Dha\u2081', + None, # Dha2/Ni1 + None, # Dha3/Ni2 + 'Ni\u2083'] + + if isinstance(mela, str): + mela_idx = MELAKARTA_MAP[mela.lower()] + elif 0 <= mela < 72: + mela_idx = mela + else: + raise ParameterError('mela={} must be in range [0, 72['.format(mela)) + + # 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: + svara_map = [s.translate(str.maketrans({'a': '', 'h': '', 'i': ''})) + for s in svara_map] + + if not unicode: + svara_map = [s.translate(str.maketrans({'\u2081': '1', + '\u2082': '2', + '\u2083': '3'})) + for s in svara_map] + return list(svara_map) From f4c540b1b5bf316e276f07899c30e682baca05ed Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Fri, 3 Jul 2020 12:54:17 -0400 Subject: [PATCH 03/15] fixed a typo [ci skip] --- librosa/core/time_frequency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/librosa/core/time_frequency.py b/librosa/core/time_frequency.py index 4081fabcac..4c3c2bbbb6 100644 --- a/librosa/core/time_frequency.py +++ b/librosa/core/time_frequency.py @@ -2200,7 +2200,7 @@ def hz_to_svara_c(frequencies, mela, Sa=None, abbr=True, octave=True, unicode=Tr abbr=abbr, octave=octave, unicode=unicode) -def note_to_svara_c(notes, mela Sa=None, abbr=True, octave=True, unicode=True): +def note_to_svara_c(notes, mela, Sa=None, abbr=True, octave=True, unicode=True): '''Convert western notes to Carnatic svara Note that this conversion assumes 12-tone equal temperament. From 491b6af25b7c9dd80982f674d830ac15b73f2245 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Fri, 3 Jul 2020 18:57:59 -0400 Subject: [PATCH 04/15] wired up specshow and axis decoration for svara --- librosa/display.py | 90 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/librosa/display.py b/librosa/display.py index c223358080..18e85fe022 100644 --- a/librosa/display.py +++ b/librosa/display.py @@ -19,6 +19,7 @@ TimeFormatter NoteFormatter + SvaraFormatter LogHzFormatter ChromaFormatter TonnetzFormatter @@ -210,6 +211,76 @@ def __call__(self, x, pos=None): return core.hz_to_note(int(x), octave=self.octave, cents=cents, key=self.key) +class SvaraFormatter(Formatter): + '''Ticker formatter for Svara + + Parameters + ---------- + octave : bool + If ``True``, display the octave number along with the note name. + + Otherwise, only show the note name (and cent deviation) + + major : bool + If ``True``, ticks are always labeled. + + If ``False``, ticks are only labeled if the span is less than 2 octaves + + Sa : number > 0 + Frequency (in Hz) of Sa + + mela : str or int + For Carnatic svara, the index or name of the melakarta raga in question + + To use Hindustani svara, set ``mela=None`` + + See also + -------- + NoteFormatter + matplotlib.ticker.Formatter + librosa.hz_to_svara_c + librosa.hz_to_svara_h + + + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> values = librosa.midi_to_hz(np.arange(48, 72)) + >>> fig, ax = plt.subplots(nrows=2) + >>> ax[0].bar(np.arange(len(values)), values) + >>> ax[0].set(ylabel='Hz') + >>> ax[1].bar(np.arange(len(values)), values) + >>> ax[1].yaxis.set_major_formatter(librosa.display.SvaraFormatter(261)) + >>> ax[1].set(ylabel='Note') + ''' + def __init__(self, Sa, octave=True, major=True, abbr=False, mela=None): + + if Sa is None: + raise ParameterError('Sa frequency is required for svara display formatting') + + self.Sa = Sa + self.octave = octave + self.major = major + self.abbr = abbr + self.mela = mela + + def __call__(self, x, pos=None): + + if x <= 0: + return '' + + # Only use cent precision if our vspan is less than an octave + vmin, vmax = self.axis.get_view_interval() + + if not self.major and vmax > 4 * max(1, vmin): + return '' + + if self.mela is None: + return core.hz_to_svara_h(x, self.Sa, octave=self.octave, abbr=self.abbr) + else: + return core.hz_to_svara_c(x, self.mela, Sa=self.Sa, octave=self.octave, abbr=self.abbr) + + class LogHzFormatter(Formatter): '''Ticker formatter for logarithmic frequency @@ -513,6 +584,7 @@ def specshow(data, x_coords=None, y_coords=None, tuning=0.0, bins_per_octave=12, key='C:maj', + Sa=None, mela=None, ax=None, **kwargs): '''Display a spectrogram/chromagram/cqt/etc. @@ -545,6 +617,7 @@ def specshow(data, x_coords=None, y_coords=None, - 'mel' : frequencies are determined by the mel scale. - 'cqt_hz' : frequencies are determined by the CQT scale. - 'cqt_note' : pitches are determined by the CQT scale. + - `cqt_svara` : like `cqt_note` but using Hindustani or Carnatic svara All frequency types are plotted in units of Hz. @@ -697,8 +770,8 @@ def specshow(data, x_coords=None, y_coords=None, __scale_axes(axes, y_axis, 'y') # Construct tickers and locators - __decorate_axis(axes.xaxis, x_axis, key=key) - __decorate_axis(axes.yaxis, y_axis, key=key) + __decorate_axis(axes.xaxis, x_axis, key=key, Sa=Sa, mela=mela) + __decorate_axis(axes.yaxis, y_axis, key=key, Sa=Sa, mela=mela) return out @@ -732,6 +805,7 @@ def __mesh_coords(ax_type, coords, n, **kwargs): 'cqt': __coord_cqt_hz, 'cqt_hz': __coord_cqt_hz, 'cqt_note': __coord_cqt_hz, + 'cqt_svara': __coord_cqt_hz, 'chroma': __coord_chroma, 'time': __coord_time, 's': __coord_time, @@ -791,7 +865,7 @@ def __scale_axes(axes, ax_type, which): kwargs[thresh] = core.note_to_hz('C2') kwargs[scale] = 0.5 - elif ax_type in ['cqt', 'cqt_hz', 'cqt_note']: + elif ax_type in ['cqt', 'cqt_hz', 'cqt_note', 'cqt_svara']: mode = 'log' kwargs[base] = 2 @@ -805,7 +879,7 @@ def __scale_axes(axes, ax_type, which): scaler(mode, **kwargs) -def __decorate_axis(axis, ax_type, key='C:maj'): +def __decorate_axis(axis, ax_type, key='C:maj', Sa=None, mela=None): '''Configure axis tickers, locators, and labels''' if ax_type == 'tonnetz': @@ -870,6 +944,14 @@ def __decorate_axis(axis, ax_type, key='C:maj'): subs=2.0**(np.arange(1, 12)/12.0))) axis.set_label_text('Note') + elif ax_type == 'cqt_svara': + axis.set_major_formatter(SvaraFormatter(Sa=Sa, mela=mela)) + axis.set_major_locator(LogLocator(base=2.0)) + axis.set_minor_formatter(SvaraFormatter(Sa=Sa, mela=mela, major=False)) + axis.set_minor_locator(LogLocator(base=2.0, + subs=2.0**(np.arange(1, 12)/12.0))) + axis.set_label_text('Svara') + elif ax_type in ['cqt_hz']: axis.set_major_formatter(LogHzFormatter()) axis.set_major_locator(LogLocator(base=2.0)) From e57b80d81bbd5b1b540719595c8021d5b1b69be1 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Fri, 3 Jul 2020 19:10:44 -0400 Subject: [PATCH 05/15] fixed sa positioning [ci skip] --- librosa/display.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/librosa/display.py b/librosa/display.py index 18e85fe022..ee1b017e20 100644 --- a/librosa/display.py +++ b/librosa/display.py @@ -946,10 +946,13 @@ def __decorate_axis(axis, ax_type, key='C:maj', Sa=None, mela=None): elif ax_type == 'cqt_svara': axis.set_major_formatter(SvaraFormatter(Sa=Sa, mela=mela)) - axis.set_major_locator(LogLocator(base=2.0)) + # Find the offset of Sa relative to 2**k Hz + sa_offset = 2.0**(np.log2(Sa) - np.floor(np.log2(Sa))) + + axis.set_major_locator(LogLocator(base=2.0, subs=(sa_offset,))) axis.set_minor_formatter(SvaraFormatter(Sa=Sa, mela=mela, major=False)) axis.set_minor_locator(LogLocator(base=2.0, - subs=2.0**(np.arange(1, 12)/12.0))) + subs=sa_offset * 2.0**(np.arange(1, 12)/12.0))) axis.set_label_text('Svara') elif ax_type in ['cqt_hz']: From 23a3baa0a9a60c90fcc30f7a1732babdd898ca63 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 4 Jul 2020 11:01:24 -0400 Subject: [PATCH 06/15] made Sa required parameter for conversion [ci skip] --- librosa/core/time_frequency.py | 24 ++++++++++++------------ librosa/display.py | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/librosa/core/time_frequency.py b/librosa/core/time_frequency.py index 4c3c2bbbb6..3a2705e924 100644 --- a/librosa/core/time_frequency.py +++ b/librosa/core/time_frequency.py @@ -1943,7 +1943,7 @@ def key_to_degrees(key): return (notes[scale] + pitch_map[tonic] + offset) % 12 -def midi_to_svara_h(midi, Sa=60, abbr=True, octave=True, unicode=True): +def midi_to_svara_h(midi, Sa, abbr=True, octave=True, unicode=True): """Convert MIDI numbers to Hindustani svara Parameters @@ -1993,7 +1993,7 @@ def midi_to_svara_h(midi, Sa=60, abbr=True, octave=True, unicode=True): SVARA_MAP_SHORT = list(s[0] for s in SVARA_MAP) if not np.isscalar(midi): - return [midi_to_svara_h(m, Sa=Sa, abbr=abbr, octave=octave, unicode=unicode) + return [midi_to_svara_h(m, Sa, abbr=abbr, octave=octave, unicode=unicode) for m in midi] svara_num = int(np.round(midi - Sa)) @@ -2018,7 +2018,7 @@ def midi_to_svara_h(midi, Sa=60, abbr=True, octave=True, unicode=True): return svara -def hz_to_svara_h(frequencies, Sa=None, abbr=True, octave=True, unicode=True): +def hz_to_svara_h(frequencies, Sa, abbr=True, octave=True, unicode=True): '''Convert frequencies (in Hz) to Hindustani svara Note that this conversion assumes 12-tone equal temperament. @@ -2041,11 +2041,11 @@ def hz_to_svara_h(frequencies, Sa=None, abbr=True, octave=True, unicode=True): Sa = note_to_hz('C2') midis = hz_to_midi(frequencies) - return midi_to_svara_h(midis, Sa=hz_to_midi(Sa), + return midi_to_svara_h(midis, hz_to_midi(Sa), abbr=abbr, octave=octave, unicode=unicode) -def note_to_svara_h(notes, Sa=None, abbr=True, octave=True, unicode=True): +def note_to_svara_h(notes, Sa, abbr=True, octave=True, unicode=True): '''Convert western notes to Hindustani svara Note that this conversion assumes 12-tone equal temperament. @@ -2067,7 +2067,7 @@ def note_to_svara_h(notes, Sa=None, abbr=True, octave=True, unicode=True): midis = note_to_midi(notes, round_midi=False) - return midi_to_svara_h(midis, Sa=note_to_midi(Sa), abbr=abbr, octave=octave, + return midi_to_svara_h(midis, note_to_midi(Sa), abbr=abbr, octave=octave, unicode=unicode) @@ -2103,7 +2103,7 @@ def thaat_to_degrees(thaat): return np.asarray(THAAT_MAP[thaat.lower()]) -def midi_to_svara_c(midi, mela, Sa=60, abbr=True, octave=True, unicode=True): +def midi_to_svara_c(midi, mela, Sa, abbr=True, octave=True, unicode=True): '''Convert MIDI numbers to Carnatic svara within a given melakarta raga Parameters @@ -2148,7 +2148,7 @@ def midi_to_svara_c(midi, mela, Sa=60, abbr=True, octave=True, unicode=True): mela_to_svara ''' if not np.isscalar(midi): - return [midi_to_svara_c(m, mela, Sa=Sa, abbr=abbr, + return [midi_to_svara_c(m, Sa, mela, abbr=abbr, octave=octave, unicode=unicode) for m in midi] @@ -2173,7 +2173,7 @@ def midi_to_svara_c(midi, mela, Sa=60, abbr=True, octave=True, unicode=True): return svara -def hz_to_svara_c(frequencies, mela, Sa=None, abbr=True, octave=True, unicode=True): +def hz_to_svara_c(frequencies, Sa, mela, abbr=True, octave=True, unicode=True): '''Convert frequencies (in Hz) to Carnatic svara Note that this conversion assumes 12-tone equal temperament. @@ -2196,11 +2196,11 @@ def hz_to_svara_c(frequencies, mela, Sa=None, abbr=True, octave=True, unicode=Tr Sa = note_to_hz('C2') midis = hz_to_midi(frequencies) - return midi_to_svara_c(midis, mela=mela, Sa=hz_to_midi(Sa), + return midi_to_svara_c(midis, hz_to_midi(Sa), mela, abbr=abbr, octave=octave, unicode=unicode) -def note_to_svara_c(notes, mela, Sa=None, abbr=True, octave=True, unicode=True): +def note_to_svara_c(notes, Sa, mela, abbr=True, octave=True, unicode=True): '''Convert western notes to Carnatic svara Note that this conversion assumes 12-tone equal temperament. @@ -2222,7 +2222,7 @@ def note_to_svara_c(notes, mela, Sa=None, abbr=True, octave=True, unicode=True): midis = note_to_midi(notes, round_midi=False) - return midi_to_svara_c(midis, mela, Sa=note_to_midi(Sa), + return midi_to_svara_c(midis, note_to_midi(Sa), mela abbr=abbr, octave=octave, unicode=unicode) diff --git a/librosa/display.py b/librosa/display.py index ee1b017e20..47eadc0745 100644 --- a/librosa/display.py +++ b/librosa/display.py @@ -278,7 +278,7 @@ def __call__(self, x, pos=None): if self.mela is None: return core.hz_to_svara_h(x, self.Sa, octave=self.octave, abbr=self.abbr) else: - return core.hz_to_svara_c(x, self.mela, Sa=self.Sa, octave=self.octave, abbr=self.abbr) + return core.hz_to_svara_c(x, self.Sa, self.mela, octave=self.octave, abbr=self.abbr) class LogHzFormatter(Formatter): From 3e1ba632081ffb9d6fd136e2e6730138f2a9db85 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 4 Jul 2020 14:40:44 -0400 Subject: [PATCH 07/15] fixing some syntax errors [ci skip] --- librosa/core/time_frequency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/librosa/core/time_frequency.py b/librosa/core/time_frequency.py index 3a2705e924..45aaa44ecb 100644 --- a/librosa/core/time_frequency.py +++ b/librosa/core/time_frequency.py @@ -2222,7 +2222,7 @@ def note_to_svara_c(notes, Sa, mela, abbr=True, octave=True, unicode=True): midis = note_to_midi(notes, round_midi=False) - return midi_to_svara_c(midis, note_to_midi(Sa), mela + return midi_to_svara_c(midis, note_to_midi(Sa), mela, abbr=abbr, octave=octave, unicode=unicode) From f7e1795abfd7e723d1adce187ceac3a7325baccb Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 4 Jul 2020 16:21:30 -0400 Subject: [PATCH 08/15] refactored time_frequency to convert and notation --- librosa/__init__.py | 29 +- librosa/core/__init__.py | 3 +- librosa/core/audio.py | 2 +- librosa/core/constantq.py | 2 +- .../core/{time_frequency.py => convert.py} | 510 +----------------- librosa/core/notation.py | 509 +++++++++++++++++ librosa/core/pitch.py | 6 +- librosa/core/spectrum.py | 12 +- librosa/feature/spectral.py | 2 +- librosa/filters.py | 4 +- ...test_time_frequency.py => test_convert.py} | 4 +- 11 files changed, 558 insertions(+), 525 deletions(-) rename librosa/core/{time_frequency.py => convert.py} (76%) create mode 100644 librosa/core/notation.py rename tests/{test_time_frequency.py => test_convert.py} (99%) diff --git a/librosa/__init__.py b/librosa/__init__.py index 34f5326c02..a525cb1f76 100644 --- a/librosa/__init__.py +++ b/librosa/__init__.py @@ -16,6 +16,7 @@ get_duration get_samplerate + Time-domain processing ---------------------- .. autosummary:: @@ -27,6 +28,7 @@ mu_compress mu_expand + Signal generation ----------------- .. autosummary:: @@ -36,6 +38,7 @@ tone chirp + Spectral representations ------------------------ .. autosummary:: @@ -58,6 +61,7 @@ magphase + Phase recovery -------------- .. autosummary:: @@ -98,6 +102,7 @@ pcen + Time unit conversion -------------------- .. autosummary:: @@ -114,6 +119,7 @@ blocks_to_samples blocks_to_time + Frequency unit conversion ------------------------- .. autosummary:: @@ -121,13 +127,16 @@ hz_to_note hz_to_midi + hz_to_svara_h + hz_to_svara_c midi_to_hz midi_to_note + midi_to_svara_h + midi_to_svara_c note_to_hz note_to_midi - - key_to_notes - key_to_degrees + note_to_svara_h + note_to_svara_c hz_to_mel hz_to_octs @@ -138,6 +147,20 @@ tuning_to_A4 +Music notation +-------------- +.. autosummary:: + :toctree: generated/ + + key_to_notes + key_to_degrees + + mela_to_svara + mela_to_degrees + + thaat_to_degrees + + Frequency range generation -------------------------- .. autosummary:: diff --git a/librosa/core/__init__.py b/librosa/core/__init__.py index a9bf2075bd..8e956dd3c2 100644 --- a/librosa/core/__init__.py +++ b/librosa/core/__init__.py @@ -2,13 +2,14 @@ # -*- coding: utf-8 -*- """ Core IO and DSP functions""" -from .time_frequency import * # pylint: disable=wildcard-import +from .convert import * # pylint: disable=wildcard-import from .audio import * # pylint: disable=wildcard-import from .spectrum import * # pylint: disable=wildcard-import from .pitch import * # pylint: disable=wildcard-import from .constantq import * # pylint: disable=wildcard-import from .harmonic import * # pylint: disable=wildcard-import from .fft import * # pylint: disable=wildcard-import +from .notation import * # pylint: disable=wildcard-import __all__ = [_ for _ in dir() if not _.startswith('_')] diff --git a/librosa/core/audio.py b/librosa/core/audio.py index 2fd22a1808..07c664b68b 100644 --- a/librosa/core/audio.py +++ b/librosa/core/audio.py @@ -13,7 +13,7 @@ from numba import jit from .fft import get_fftlib -from .time_frequency import frames_to_samples, time_to_samples +from .convert import frames_to_samples, time_to_samples from .._cache import cache from .. import util from ..util.exceptions import ParameterError diff --git a/librosa/core/constantq.py b/librosa/core/constantq.py index 90689aa8b6..fb2061d61a 100644 --- a/librosa/core/constantq.py +++ b/librosa/core/constantq.py @@ -9,7 +9,7 @@ from . import audio from .fft import get_fftlib -from .time_frequency import cqt_frequencies, note_to_hz +from .convert import cqt_frequencies, note_to_hz from .spectrum import stft, istft from .pitch import estimate_tuning from .._cache import cache diff --git a/librosa/core/time_frequency.py b/librosa/core/convert.py similarity index 76% rename from librosa/core/time_frequency.py rename to librosa/core/convert.py index 45aaa44ecb..fd9f863123 100644 --- a/librosa/core/time_frequency.py +++ b/librosa/core/convert.py @@ -1,10 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -'''Time and frequency utilities''' +'''Unit conversion utilities''' import re import numpy as np -from .._cache import cache +from . import notation from ..util.exceptions import ParameterError __all__ = ['frames_to_samples', 'frames_to_time', @@ -34,13 +34,9 @@ 'multi_frequency_weighting', 'samples_like', 'times_like', - 'key_to_notes', - 'key_to_degrees', 'midi_to_svara_h', 'midi_to_svara_c', 'note_to_svara_h', 'note_to_svara_c', - 'hz_to_svara_h', 'hz_to_svara_c', - 'thaat_to_degrees', - 'mela_to_svara', 'mela_to_degrees'] + 'hz_to_svara_h', 'hz_to_svara_c'] def frames_to_samples(frames, hop_length=512, n_fft=None): @@ -643,7 +639,7 @@ def midi_to_note(midi, octave=True, cents=False, key='C:maj', unicode=True): if not np.isscalar(midi): return [midi_to_note(x, octave=octave, cents=cents, key=key, unicode=unicode) for x in midi] - note_map = key_to_notes(key=key, unicode=unicode) + note_map = notation.key_to_notes(key=key, unicode=unicode) note_num = int(np.round(midi)) note_cents = int(100 * np.around(midi - note_num, 2)) @@ -1722,227 +1718,6 @@ def samples_like(X, hop_length=512, n_fft=None, axis=-1): return frames_to_samples(frames, hop_length=hop_length, n_fft=n_fft) -@cache(level=10) -def key_to_notes(key, unicode=True): - '''Lists all 12 note names in the chromatic scale, as spelled according to - a given key (major or minor). - - This function exists to resolve enharmonic equivalences between different - spellings for the same pitch (e.g. C♯ vs D♭), and is primarily useful when producing - human-readable outputs (e.g. plotting) for pitch content. - - Note names are decided by the following rules: - - 1. If the tonic of the key has an accidental (sharp or flat), that accidental will be - used consistently for all notes. - - 2. If the tonic does not have an accidental, accidentals will be inferred to minimize - the total number used for diatonic scale degrees. - - 3. If there is a tie (e.g., in the case of C:maj vs A:min), sharps will be preferred. - - Parameters - ---------- - key : string - Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), - key must be lower-case (``maj`` or ``min``). - - Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported. - - Examples: ``C:maj, Db:min, A♭:min``. - - unicode: bool - If ``True`` (default), use Unicode symbols (♯𝄪♭𝄫)for accidentals. - - If ``False``, Unicode symbols will be mapped to low-order ASCII representations:: - - ♯ -> #, 𝄪 -> ##, ♭ -> b, 𝄫 -> bb - - Returns - ------- - notes : list - ``notes[k]`` is the name for semitone ``k`` (starting from C) - under the given key. All chromatic notes (0 through 11) are - included. - - See Also - -------- - midi_to_note - - Examples - -------- - `C:maj` will use all sharps - - >>> librosa.key_to_notes('C:maj') - ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] - - `A:min` has the same notes - - >>> librosa.key_to_notes('A:min') - ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] - - `A♯:min` will use sharps, but spell note 0 (`C`) as `B♯` - - >>> librosa.key_to_notes('A#:min') - ['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] - - `G♯:maj` will use a double-sharp to spell note 7 (`G`) as `F𝄪`: - - >>> librosa.key_to_notes('G#:maj') - ['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'F𝄪', 'G♯', 'A', 'A♯', 'B'] - - `F♭:min` will use double-flats - - >>> librosa.key_to_notes('Fb:min') - ['D𝄫', 'D♭', 'E𝄫', 'E♭', 'F♭', 'F', 'G♭', 'A𝄫', 'A♭', 'B𝄫', 'B♭', 'C♭'] - ''' - - # Parse the key signature - match = re.match(r'^(?P[A-Ga-g])' - r'(?P[#♯b!♭]?)' - r':(?P(maj|min)(or)?)$', - key) - if not match: - raise ParameterError('Improper key format: {:s}'.format(key)) - - pitch_map = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11} - acc_map = {'#': 1, '': 0, 'b': -1, '!': -1, '♯': 1, '♭': -1} - - tonic = match.group('tonic').upper() - accidental = match.group('accidental') - offset = acc_map[accidental] - - scale = match.group('scale')[:3].lower() - - # 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): - """Construct the diatonic scale degrees for a given key. - - Parameters - ---------- - key : str - Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), - key must be lower-case (``maj`` or ``min``). - - Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported. - - Examples: ``C:maj, Db:min, A♭:min``. - - Returns - ------- - degrees : np.ndarray - An array containing the semitone numbers (0=C, 1=C#, ... 11=B) - for each of the seven scale degrees in the given key, starting - from the tonic. - - See Also - -------- - key_to_notes - - Examples - -------- - >>> librosa.key_to_degrees('C:maj') - array([ 0, 2, 4, 5, 7, 9, 11]) - - >>> librosa.key_to_degrees('C#:maj') - array([ 1, 3, 5, 6, 8, 10, 0]) - - >>> librosa.key_to_degrees('A:min') - array([ 9, 11, 0, 2, 4, 5, 7]) - - """ - notes = dict(maj=np.array([0, 2, 4, 5, 7, 9, 11]), - min=np.array([0, 2, 3, 5, 7, 8, 10])) - - match = re.match(r'^(?P[A-Ga-g])' - r'(?P[#♯b!♭]?)' - r':(?P(maj|min)(or)?)$', - key) - if not match: - raise ParameterError('Improper key format: {:s}'.format(key)) - - pitch_map = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11} - acc_map = {'#': 1, '': 0, 'b': -1, '!': -1, '♯': 1, '♭': -1} - tonic = match.group('tonic').upper() - accidental = match.group('accidental') - offset = acc_map[accidental] - - scale = match.group('scale')[:3].lower() - - return (notes[scale] + pitch_map[tonic] + offset) % 12 - - def midi_to_svara_h(midi, Sa, abbr=True, octave=True, unicode=True): """Convert MIDI numbers to Hindustani svara @@ -2071,38 +1846,6 @@ def note_to_svara_h(notes, Sa, abbr=True, octave=True, unicode=True): unicode=unicode) -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]) -def thaat_to_degrees(thaat): - '''Construct the svara indices (degrees) for a given thaat - - Parameters - ---------- - thaat : str - The name of the thaat - - Returns - ------- - indices : np.ndarray - A list of the seven svara indicies (starting from 0=Sa) - contained in the specified thaat - - See Also - -------- - key_to_degrees - mela_to_degrees - ''' - return np.asarray(THAAT_MAP[thaat.lower()]) - - def midi_to_svara_c(midi, mela, Sa, abbr=True, octave=True, unicode=True): '''Convert MIDI numbers to Carnatic svara within a given melakarta raga @@ -2154,7 +1897,7 @@ def midi_to_svara_c(midi, mela, Sa, abbr=True, octave=True, unicode=True): svara_num = int(np.round(midi - Sa)) - svara_map = mela_to_svara(mela, abbr=abbr, unicode=unicode) + svara_map = notation.mela_to_svara(mela, abbr=abbr, unicode=unicode) svara = svara_map[svara_num % 12] @@ -2225,246 +1968,3 @@ def note_to_svara_c(notes, Sa, mela, abbr=True, octave=True, unicode=True): return midi_to_svara_c(midis, note_to_midi(Sa), mela, abbr=abbr, octave=octave, unicode=unicode) - - -MELAKARTA_MAP = {k: i - for i, k in enumerate(['kanakanki', 'ratnangi', 'ganamurti', - 'vanaspati', 'manavati', 'tanarupi', - 'senavati', 'hanumatodi', 'dhenuka', - 'natakapriya', 'kokilapriya', 'rupavati', - 'gayakapriya', 'vakulabharanam', 'mayamalavagoulai', - 'chakravaham', 'suryakantam', 'hatakambhari', - 'jhankaradhwani', 'natabhairavi', 'keeravani', - 'kharaharapriya', 'gowrimanohari', 'varunapriya', - 'mararanjani', 'charukesi', 'sarasangi', - 'harikambhoji', 'dheerasankarabharanam', 'naganandini', - 'yagapriya', 'ragavardhini', 'gangeyabhusani', - 'vagadheeswari', 'sulini', 'chalanattai', - 'salagam', 'jalarnavam', 'jhalavarali', - 'navaneetam', 'pavani', 'raghupriya', - 'gavambodhi', 'bhavapriya', 'subhapantuvarali', - 'shadvigamargini', 'suvarnangi', 'divyamani', - 'dhavalambari', 'namanarayani', 'kamavardhini', - 'ramapriya', 'gamanasrama', 'viswambhari', - 'syamalangi', 'shanmukhapriya', 'simhendramadhyamam', - 'hemavati', 'dharmavati', 'nitimati', - 'kantamani', 'rishabhapriya', 'latangi', - 'vachaspati', 'mechakalyani', 'chitrambhari', - 'sucharitra', 'jyotiswarupini', 'dhatuvardhini', - 'nasikabhushani', 'kasalam', 'rasikapriya'])} - -def mela_to_degrees(mela): - '''Construct the svara indices (degrees) for a given melakarta raga - - Parameters - ---------- - mela : str or int - Either the name or integer index ([0, 71]) of the melakarta raga - - Returns - ------- - degrees : np.ndarray - - ''' - - if isinstance(mela, str): - index = MELAKARTA_MAP[mela.lower()] - elif 0 <= mela < 72: - index = mela - else: - raise ParameterError('mela={} must be in range [0, 72['.format(mela)) - - # 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) - - -@cache(level=10) -def mela_to_svara(mela, abbr=True, unicode=True): - '''Spell the Carnatic svara names for a given melakarta raga - - This function exists to resolve enharmonic equivalences between - pitch classes: - - - Ri2 / Ga1 - - Ri3 / Ga2 - - Dha2 / Ni1 - - Dha3 / Ni2 - - For svara outside the raga, names are chosen to preserve orderings - so that all Ri precede all Ga, and all Dha precede all Ni. - - Parameters - ---------- - mela : str or int - the name or numerical index of the melakarta raga - - abbr : bool - If `True`, use single-letter svara names: S, R, G, ... - - If `False`, use full names: Sa, Ri, Ga, ... - - unicode : bool - If `True`, use unicode symbols for numberings, e.g., Ri\u2081 - - If `False`, use low-order ASCII, e.g., Ri1. - - Returns - ------- - svara : list of strings - - The svara names for each of the 12 pitch classes. - - See Also - -------- - key_to_notes - mela_to_degrees - - Examples - -------- - Melakarta #0 (Kanakanki) uses R1, G1, D1, N1 - - >>> librosa.mela_to_svara(0) - ['S', 'R₁', 'G₁', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] - - #18 (Jhankaradhwani) uses R2 and G2 so the third svara are Ri: - - >>> librosa.mela_to_svara(18) - ['S', 'R₁', 'R₂', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] - - #30 (Yagapriya) uses R3 and G3, so third and fourth svara are Ri: - - >>> librosa.mela_to_svara(30) - ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] - - #33 (Vagadheeswari) uses D2 and N2, so Ni1 becomes Dha2: - - >>> librosa.mela_to_svara(33) - ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'N₂', 'N₃'] - - #35 (Chalanattai) uses D3 and N3, so Ni2 becomes Dha3: - - >>> librosa.mela_to_svara(35) - ['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', - None, # Ri2/Ga1 - None, # Ri3/Ga2 - 'Ga\u2083', - 'Ma\u2081', 'Ma\u2082', - 'Pa', - 'Dha\u2081', - None, # Dha2/Ni1 - None, # Dha3/Ni2 - 'Ni\u2083'] - - if isinstance(mela, str): - mela_idx = MELAKARTA_MAP[mela.lower()] - elif 0 <= mela < 72: - mela_idx = mela - else: - raise ParameterError('mela={} must be in range [0, 72['.format(mela)) - - # 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: - svara_map = [s.translate(str.maketrans({'a': '', 'h': '', 'i': ''})) - for s in svara_map] - - if not unicode: - svara_map = [s.translate(str.maketrans({'\u2081': '1', - '\u2082': '2', - '\u2083': '3'})) - for s in svara_map] - - return list(svara_map) diff --git a/librosa/core/notation.py b/librosa/core/notation.py new file mode 100644 index 0000000000..977c645dde --- /dev/null +++ b/librosa/core/notation.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +'''Music notation utilties''' + +import re +import numpy as np +from .._cache import cache +from ..util.exceptions import ParameterError + +__all__ = ['key_to_degrees', 'key_to_notes', + 'mela_to_degrees', 'mela_to_svara', + 'thaat_to_degrees'] + +THAAT_MAP = dict(bilaval = [0, 2, 4, 5, 7, 9, 11], + khamaj = [0, 2, 4, 5, 7, 9, 10], + kafi = [0, 2, 3, 5, 7, 9, 10], + asavari = [0, 2, 3, 5, 7, 8, 10], + bhairavi = [0, 1, 3, 5, 7, 8, 10], + kalyan = [0, 2, 4, 6, 7, 9, 11], + marva = [0, 1, 4, 6, 7, 9, 11], + poorvi = [0, 1, 4, 6, 7, 8, 11], + todi = [0, 1, 3, 6, 7, 8, 11], + bhairav = [0, 1, 4, 5, 7, 8, 11]) + +MELAKARTA_MAP = {k: i + for i, k in enumerate(['kanakanki', 'ratnangi', 'ganamurti', + 'vanaspati', 'manavati', 'tanarupi', + 'senavati', 'hanumatodi', 'dhenuka', + 'natakapriya', 'kokilapriya', 'rupavati', + 'gayakapriya', 'vakulabharanam', 'mayamalavagoulai', + 'chakravaham', 'suryakantam', 'hatakambhari', + 'jhankaradhwani', 'natabhairavi', 'keeravani', + 'kharaharapriya', 'gowrimanohari', 'varunapriya', + 'mararanjani', 'charukesi', 'sarasangi', + 'harikambhoji', 'dheerasankarabharanam', 'naganandini', + 'yagapriya', 'ragavardhini', 'gangeyabhusani', + 'vagadheeswari', 'sulini', 'chalanattai', + 'salagam', 'jalarnavam', 'jhalavarali', + 'navaneetam', 'pavani', 'raghupriya', + 'gavambodhi', 'bhavapriya', 'subhapantuvarali', + 'shadvigamargini', 'suvarnangi', 'divyamani', + 'dhavalambari', 'namanarayani', 'kamavardhini', + 'ramapriya', 'gamanasrama', 'viswambhari', + 'syamalangi', 'shanmukhapriya', 'simhendramadhyamam', + 'hemavati', 'dharmavati', 'nitimati', + 'kantamani', 'rishabhapriya', 'latangi', + 'vachaspati', 'mechakalyani', 'chitrambhari', + 'sucharitra', 'jyotiswarupini', 'dhatuvardhini', + 'nasikabhushani', 'kasalam', 'rasikapriya'])} + + +def thaat_to_degrees(thaat): + '''Construct the svara indices (degrees) for a given thaat + + Parameters + ---------- + thaat : str + The name of the thaat + + Returns + ------- + indices : np.ndarray + A list of the seven svara indicies (starting from 0=Sa) + contained in the specified thaat + + See Also + -------- + key_to_degrees + mela_to_degrees + ''' + return np.asarray(THAAT_MAP[thaat.lower()]) + + +def mela_to_degrees(mela): + '''Construct the svara indices (degrees) for a given melakarta raga + + Parameters + ---------- + mela : str or int + Either the name or integer index ([0, 71]) of the melakarta raga + + Returns + ------- + degrees : np.ndarray + + ''' + + if isinstance(mela, str): + index = MELAKARTA_MAP[mela.lower()] + elif 0 <= mela < 72: + index = mela + else: + raise ParameterError('mela={} must be in range [0, 72['.format(mela)) + + # 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) + + +@cache(level=10) +def mela_to_svara(mela, abbr=True, unicode=True): + '''Spell the Carnatic svara names for a given melakarta raga + + This function exists to resolve enharmonic equivalences between + pitch classes: + + - Ri2 / Ga1 + - Ri3 / Ga2 + - Dha2 / Ni1 + - Dha3 / Ni2 + + For svara outside the raga, names are chosen to preserve orderings + so that all Ri precede all Ga, and all Dha precede all Ni. + + Parameters + ---------- + mela : str or int + the name or numerical index of the melakarta raga + + abbr : bool + If `True`, use single-letter svara names: S, R, G, ... + + If `False`, use full names: Sa, Ri, Ga, ... + + unicode : bool + If `True`, use unicode symbols for numberings, e.g., Ri\u2081 + + If `False`, use low-order ASCII, e.g., Ri1. + + Returns + ------- + svara : list of strings + + The svara names for each of the 12 pitch classes. + + See Also + -------- + key_to_notes + mela_to_degrees + + Examples + -------- + Melakarta #0 (Kanakanki) uses R1, G1, D1, N1 + + >>> librosa.mela_to_svara(0) + ['S', 'R₁', 'G₁', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] + + #18 (Jhankaradhwani) uses R2 and G2 so the third svara are Ri: + + >>> librosa.mela_to_svara(18) + ['S', 'R₁', 'R₂', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] + + #30 (Yagapriya) uses R3 and G3, so third and fourth svara are Ri: + + >>> librosa.mela_to_svara(30) + ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] + + #33 (Vagadheeswari) uses D2 and N2, so Ni1 becomes Dha2: + + >>> librosa.mela_to_svara(33) + ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'N₂', 'N₃'] + + #35 (Chalanattai) uses D3 and N3, so Ni2 becomes Dha3: + + >>> librosa.mela_to_svara(35) + ['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', + None, # Ri2/Ga1 + None, # Ri3/Ga2 + 'Ga\u2083', + 'Ma\u2081', 'Ma\u2082', + 'Pa', + 'Dha\u2081', + None, # Dha2/Ni1 + None, # Dha3/Ni2 + 'Ni\u2083'] + + if isinstance(mela, str): + mela_idx = MELAKARTA_MAP[mela.lower()] + elif 0 <= mela < 72: + mela_idx = mela + else: + raise ParameterError('mela={} must be in range [0, 72['.format(mela)) + + # 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: + svara_map = [s.translate(str.maketrans({'a': '', 'h': '', 'i': ''})) + for s in svara_map] + + if not unicode: + svara_map = [s.translate(str.maketrans({'\u2081': '1', + '\u2082': '2', + '\u2083': '3'})) + for s in svara_map] + + return list(svara_map) + + +@cache(level=10) +def key_to_notes(key, unicode=True): + '''Lists all 12 note names in the chromatic scale, as spelled according to + a given key (major or minor). + + This function exists to resolve enharmonic equivalences between different + spellings for the same pitch (e.g. C♯ vs D♭), and is primarily useful when producing + human-readable outputs (e.g. plotting) for pitch content. + + Note names are decided by the following rules: + + 1. If the tonic of the key has an accidental (sharp or flat), that accidental will be + used consistently for all notes. + + 2. If the tonic does not have an accidental, accidentals will be inferred to minimize + the total number used for diatonic scale degrees. + + 3. If there is a tie (e.g., in the case of C:maj vs A:min), sharps will be preferred. + + Parameters + ---------- + key : string + Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), + key must be lower-case (``maj`` or ``min``). + + Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported. + + Examples: ``C:maj, Db:min, A♭:min``. + + unicode: bool + If ``True`` (default), use Unicode symbols (♯𝄪♭𝄫)for accidentals. + + If ``False``, Unicode symbols will be mapped to low-order ASCII representations:: + + ♯ -> #, 𝄪 -> ##, ♭ -> b, 𝄫 -> bb + + Returns + ------- + notes : list + ``notes[k]`` is the name for semitone ``k`` (starting from C) + under the given key. All chromatic notes (0 through 11) are + included. + + See Also + -------- + midi_to_note + + Examples + -------- + `C:maj` will use all sharps + + >>> librosa.key_to_notes('C:maj') + ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] + + `A:min` has the same notes + + >>> librosa.key_to_notes('A:min') + ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] + + `A♯:min` will use sharps, but spell note 0 (`C`) as `B♯` + + >>> librosa.key_to_notes('A#:min') + ['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] + + `G♯:maj` will use a double-sharp to spell note 7 (`G`) as `F𝄪`: + + >>> librosa.key_to_notes('G#:maj') + ['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'F𝄪', 'G♯', 'A', 'A♯', 'B'] + + `F♭:min` will use double-flats + + >>> librosa.key_to_notes('Fb:min') + ['D𝄫', 'D♭', 'E𝄫', 'E♭', 'F♭', 'F', 'G♭', 'A𝄫', 'A♭', 'B𝄫', 'B♭', 'C♭'] + ''' + + # Parse the key signature + match = re.match(r'^(?P[A-Ga-g])' + r'(?P[#♯b!♭]?)' + r':(?P(maj|min)(or)?)$', + key) + if not match: + raise ParameterError('Improper key format: {:s}'.format(key)) + + pitch_map = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11} + acc_map = {'#': 1, '': 0, 'b': -1, '!': -1, '♯': 1, '♭': -1} + + tonic = match.group('tonic').upper() + accidental = match.group('accidental') + offset = acc_map[accidental] + + scale = match.group('scale')[:3].lower() + + # 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): + """Construct the diatonic scale degrees for a given key. + + Parameters + ---------- + key : str + Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), + key must be lower-case (``maj`` or ``min``). + + Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported. + + Examples: ``C:maj, Db:min, A♭:min``. + + Returns + ------- + degrees : np.ndarray + An array containing the semitone numbers (0=C, 1=C#, ... 11=B) + for each of the seven scale degrees in the given key, starting + from the tonic. + + See Also + -------- + key_to_notes + + Examples + -------- + >>> librosa.key_to_degrees('C:maj') + array([ 0, 2, 4, 5, 7, 9, 11]) + + >>> librosa.key_to_degrees('C#:maj') + array([ 1, 3, 5, 6, 8, 10, 0]) + + >>> librosa.key_to_degrees('A:min') + array([ 9, 11, 0, 2, 4, 5, 7]) + + """ + notes = dict(maj=np.array([0, 2, 4, 5, 7, 9, 11]), + min=np.array([0, 2, 3, 5, 7, 8, 10])) + + match = re.match(r'^(?P[A-Ga-g])' + r'(?P[#♯b!♭]?)' + r':(?P(maj|min)(or)?)$', + key) + if not match: + raise ParameterError('Improper key format: {:s}'.format(key)) + + pitch_map = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11} + acc_map = {'#': 1, '': 0, 'b': -1, '!': -1, '♯': 1, '♭': -1} + tonic = match.group('tonic').upper() + accidental = match.group('accidental') + offset = acc_map[accidental] + + scale = match.group('scale')[:3].lower() + + return (notes[scale] + pitch_map[tonic] + offset) % 12 diff --git a/librosa/core/pitch.py b/librosa/core/pitch.py index f37692f3f4..cf9095f799 100644 --- a/librosa/core/pitch.py +++ b/librosa/core/pitch.py @@ -8,7 +8,7 @@ from .spectrum import _spectrogram -from . import time_frequency +from . import convert from .._cache import cache from .. import util from .. import sequence @@ -152,7 +152,7 @@ def pitch_tuning(frequencies, resolution=0.01, bins_per_octave=12): # Compute the residual relative to the number of bins residual = np.mod(bins_per_octave * - time_frequency.hz_to_octs(frequencies), 1.0) + convert.hz_to_octs(frequencies), 1.0) # Are we on the wrong side of the semitone? # A residual of 0.95 is more likely to be a deviation of -0.05 @@ -291,7 +291,7 @@ def piptrack(y=None, sr=22050, S=None, n_fft=2048, hop_length=None, fmin = np.maximum(fmin, 0) fmax = np.minimum(fmax, float(sr) / 2) - fft_freqs = time_frequency.fft_frequencies(sr=sr, n_fft=n_fft) + fft_freqs = convert.fft_frequencies(sr=sr, n_fft=n_fft) # Do the parabolic interpolation everywhere, # then figure out where the peaks are diff --git a/librosa/core/spectrum.py b/librosa/core/spectrum.py index 7b67993c9e..690f88347d 100644 --- a/librosa/core/spectrum.py +++ b/librosa/core/spectrum.py @@ -11,7 +11,7 @@ from numba import jit -from . import time_frequency +from . import convert from .fft import get_fftlib from .audio import resample from .._cache import cache @@ -567,7 +567,7 @@ def __reassign_frequencies(y, sr=22050, S=None, n_fft=2048, hop_length=None, # Meyer, & Ainsworth 1998 pp. 283-284 correction = -np.imag(S_dh / S_h) - freqs = time_frequency.fft_frequencies(sr=sr, n_fft=n_fft) + freqs = convert.fft_frequencies(sr=sr, n_fft=n_fft) freqs = freqs[:, np.newaxis] + correction * (0.5 * sr / np.pi) return freqs, S_h @@ -736,7 +736,7 @@ def __reassign_times(y, sr=22050, S=None, n_fft=2048, hop_length=None, else: pad_length = n_fft - times = time_frequency.frames_to_time( + times = convert.frames_to_time( np.arange(S_h.shape[1]), sr=sr, hop_length=hop_length, n_fft=pad_length ) @@ -990,9 +990,9 @@ def reassigned_spectrogram(y, sr=22050, S=None, n_fft=2048, hop_length=None, else: pad_length = n_fft - bin_freqs = time_frequency.fft_frequencies(sr=sr, n_fft=n_fft) + bin_freqs = convert.fft_frequencies(sr=sr, n_fft=n_fft) - frame_times = time_frequency.frames_to_time( + frame_times = convert.frames_to_time( frames=np.arange(S.shape[1]), sr=sr, hop_length=hop_length, @@ -1682,7 +1682,7 @@ def perceptual_weighting(S, frequencies, kind='A', **kwargs): >>> fig.colorbar(imgp, ax=ax[1], format="%+2.0f dB") ''' - offset = time_frequency.frequency_weighting( + offset = convert.frequency_weighting( frequencies, kind=kind).reshape((-1, 1)) return offset + power_to_db(S, **kwargs) diff --git a/librosa/feature/spectral.py b/librosa/feature/spectral.py index 04bd5b4872..6734e31955 100644 --- a/librosa/feature/spectral.py +++ b/librosa/feature/spectral.py @@ -11,7 +11,7 @@ from .. import filters from ..util.exceptions import ParameterError -from ..core.time_frequency import fft_frequencies +from ..core.convert import fft_frequencies from ..core.audio import zero_crossings, to_mono from ..core.spectrum import power_to_db, _spectrogram from ..core.constantq import cqt, hybrid_cqt diff --git a/librosa/filters.py b/librosa/filters.py index ca6746186e..62c014c738 100644 --- a/librosa/filters.py +++ b/librosa/filters.py @@ -46,8 +46,8 @@ from . import util from .util.exceptions import ParameterError -from .core.time_frequency import note_to_hz, hz_to_midi, midi_to_hz, hz_to_octs -from .core.time_frequency import fft_frequencies, mel_frequencies +from .core.convert import note_to_hz, hz_to_midi, midi_to_hz, hz_to_octs +from .core.convert import fft_frequencies, mel_frequencies __all__ = ['mel', 'chroma', diff --git a/tests/test_time_frequency.py b/tests/test_convert.py similarity index 99% rename from tests/test_time_frequency.py rename to tests/test_convert.py index dfcd60b949..a4f13e7007 100644 --- a/tests/test_time_frequency.py +++ b/tests/test_convert.py @@ -404,12 +404,12 @@ def test_Z_weighting(min_db): @pytest.mark.parametrize( - "kind", list(librosa.core.time_frequency.WEIGHTING_FUNCTIONS)) + "kind", list(librosa.core.convert.WEIGHTING_FUNCTIONS)) def test_frequency_weighting(kind): freq = np.linspace(2e1, 2e4) assert np.allclose( librosa.frequency_weighting(freq, kind), - librosa.core.time_frequency.WEIGHTING_FUNCTIONS[kind](freq), + librosa.core.convert.WEIGHTING_FUNCTIONS[kind](freq), 0, atol=1e-3) From 60f95378f87282abfd8a5ade29d84efa7f2cf3d8 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sun, 5 Jul 2020 11:26:50 -0400 Subject: [PATCH 09/15] added a C offset to cqt tick locations --- librosa/display.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/librosa/display.py b/librosa/display.py index 47eadc0745..b7d5f950a4 100644 --- a/librosa/display.py +++ b/librosa/display.py @@ -938,7 +938,10 @@ def __decorate_axis(axis, ax_type, key='C:maj', Sa=None, mela=None): elif ax_type == 'cqt_note': axis.set_major_formatter(NoteFormatter(key=key)) - axis.set_major_locator(LogLocator(base=2.0)) + # Where is C1 relative to 2**k hz? + log_C1 = np.log2(core.note_to_hz('C1')) + C_offset = 2.0**(log_C1 - np.floor(log_C1)) + axis.set_major_locator(LogLocator(base=2.0, subs=(C_offset,))) axis.set_minor_formatter(NoteFormatter(key=key, major=False)) axis.set_minor_locator(LogLocator(base=2.0, subs=2.0**(np.arange(1, 12)/12.0))) @@ -957,6 +960,9 @@ def __decorate_axis(axis, ax_type, key='C:maj', Sa=None, mela=None): elif ax_type in ['cqt_hz']: axis.set_major_formatter(LogHzFormatter()) + log_C1 = np.log2(core.note_to_hz('C1')) + C_offset = 2.0**(log_C1 - np.floor(log_C1)) + axis.set_major_locator(LogLocator(base=2.0, subs=(C_offset,))) axis.set_major_locator(LogLocator(base=2.0)) axis.set_minor_formatter(LogHzFormatter(major=False)) axis.set_minor_locator(LogLocator(base=2.0, From 069556a94ba4278dd37fcb453dddd8fd9980a572 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 11 Jul 2020 10:16:10 -0400 Subject: [PATCH 10/15] updated mela indexing to be 1-based --- librosa/core/notation.py | 41 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/librosa/core/notation.py b/librosa/core/notation.py index 977c645dde..f55b8e791f 100644 --- a/librosa/core/notation.py +++ b/librosa/core/notation.py @@ -22,6 +22,7 @@ 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(['kanakanki', 'ratnangi', 'ganamurti', 'vanaspati', 'manavati', 'tanarupi', @@ -46,7 +47,7 @@ 'kantamani', 'rishabhapriya', 'latangi', 'vachaspati', 'mechakalyani', 'chitrambhari', 'sucharitra', 'jyotiswarupini', 'dhatuvardhini', - 'nasikabhushani', 'kasalam', 'rasikapriya'])} + 'nasikabhushani', 'kasalam', 'rasikapriya'], 1)} def thaat_to_degrees(thaat): @@ -77,7 +78,7 @@ def mela_to_degrees(mela): Parameters ---------- mela : str or int - Either the name or integer index ([0, 71]) of the melakarta raga + Either the name or integer index ([1, 2, ..., 72]) of the melakarta raga Returns ------- @@ -86,11 +87,11 @@ def mela_to_degrees(mela): ''' if isinstance(mela, str): - index = MELAKARTA_MAP[mela.lower()] - elif 0 <= mela < 72: - index = mela + index = MELAKARTA_MAP[mela.lower()] - 1 + elif 0 < mela <= 72: + index = mela - 1 else: - raise ParameterError('mela={} must be in range [0, 72['.format(mela)) + raise ParameterError('mela={} must be in range [1, 72]'.format(mela)) # always have Sa [0] degrees = [0] @@ -194,29 +195,29 @@ def mela_to_svara(mela, abbr=True, unicode=True): Examples -------- - Melakarta #0 (Kanakanki) uses R1, G1, D1, N1 + Melakarta #1 (Kanakanki) uses R1, G1, D1, N1 - >>> librosa.mela_to_svara(0) + >>> librosa.mela_to_svara(1) ['S', 'R₁', 'G₁', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] - #18 (Jhankaradhwani) uses R2 and G2 so the third svara are Ri: + #19 (Jhankaradhwani) uses R2 and G2 so the third svara are Ri: - >>> librosa.mela_to_svara(18) + >>> librosa.mela_to_svara(19) ['S', 'R₁', 'R₂', 'G₂', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] - #30 (Yagapriya) uses R3 and G3, so third and fourth svara are Ri: + #31 (Yagapriya) uses R3 and G3, so third and fourth svara are Ri: - >>> librosa.mela_to_svara(30) + >>> librosa.mela_to_svara(31) ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'N₁', 'N₂', 'N₃'] - #33 (Vagadheeswari) uses D2 and N2, so Ni1 becomes Dha2: + #34 (Vagadheeswari) uses D2 and N2, so Ni1 becomes Dha2: - >>> librosa.mela_to_svara(33) + >>> librosa.mela_to_svara(34) ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'N₂', 'N₃'] - #35 (Chalanattai) uses D3 and N3, so Ni2 becomes Dha3: + #36 (Chalanattai) uses D3 and N3, so Ni2 becomes Dha3: - >>> librosa.mela_to_svara(35) + >>> librosa.mela_to_svara(36) ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'D₃', 'N₃'] ''' @@ -233,11 +234,11 @@ def mela_to_svara(mela, abbr=True, unicode=True): 'Ni\u2083'] if isinstance(mela, str): - mela_idx = MELAKARTA_MAP[mela.lower()] - elif 0 <= mela < 72: - mela_idx = mela + mela_idx = MELAKARTA_MAP[mela.lower()] - 1 + elif 0 < mela <= 72: + mela_idx = mela - 1 else: - raise ParameterError('mela={} must be in range [0, 72['.format(mela)) + raise ParameterError('mela={} must be in range [1, 72]'.format(mela)) # Determine Ri2/Ga1 lower = mela_idx % 36 From 947a355f35ea8588acb03309999317ad2e8fd97b Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 11 Jul 2020 10:16:46 -0400 Subject: [PATCH 11/15] Fixed typo kasalam->kosalam --- librosa/core/notation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/librosa/core/notation.py b/librosa/core/notation.py index f55b8e791f..bb9dbf54bc 100644 --- a/librosa/core/notation.py +++ b/librosa/core/notation.py @@ -47,7 +47,7 @@ 'kantamani', 'rishabhapriya', 'latangi', 'vachaspati', 'mechakalyani', 'chitrambhari', 'sucharitra', 'jyotiswarupini', 'dhatuvardhini', - 'nasikabhushani', 'kasalam', 'rasikapriya'], 1)} + 'nasikabhushani', 'kosalam', 'rasikapriya'], 1)} def thaat_to_degrees(thaat): From 81e29d8698c98474f18a4bd3bfd63fb11aee3cff Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Thu, 16 Jul 2020 11:08:00 -0400 Subject: [PATCH 12/15] added raga list helpers --- librosa/core/convert.py | 2 ++ librosa/core/notation.py | 72 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/librosa/core/convert.py b/librosa/core/convert.py index fd9f863123..704023b8d7 100644 --- a/librosa/core/convert.py +++ b/librosa/core/convert.py @@ -1889,6 +1889,7 @@ def midi_to_svara_c(midi, mela, Sa, abbr=True, octave=True, unicode=True): note_to_svara_c mela_to_degrees mela_to_svara + list_mela ''' if not np.isscalar(midi): return [midi_to_svara_c(m, Sa, mela, abbr=abbr, @@ -1956,6 +1957,7 @@ def note_to_svara_c(notes, Sa, mela, abbr=True, octave=True, unicode=True): See Also -------- + list_mela Examples -------- diff --git a/librosa/core/notation.py b/librosa/core/notation.py index bb9dbf54bc..fafe863654 100644 --- a/librosa/core/notation.py +++ b/librosa/core/notation.py @@ -9,7 +9,8 @@ __all__ = ['key_to_degrees', 'key_to_notes', 'mela_to_degrees', 'mela_to_svara', - 'thaat_to_degrees'] + 'thaat_to_degrees', + 'list_mela', 'list_thaat'] THAAT_MAP = dict(bilaval = [0, 2, 4, 5, 7, 9, 11], khamaj = [0, 2, 4, 5, 7, 9, 10], @@ -83,7 +84,12 @@ def mela_to_degrees(mela): Returns ------- degrees : np.ndarray - + A list of the seven svara indicies (starting from 0=Sa) + contained in the specified raga + + Examples + -------- + >>> ''' if isinstance(mela, str): @@ -192,6 +198,7 @@ def mela_to_svara(mela, abbr=True, unicode=True): -------- key_to_notes mela_to_degrees + list_mela Examples -------- @@ -219,6 +226,11 @@ def mela_to_svara(mela, abbr=True, unicode=True): >>> 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('chalanattai') + ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'D₃', 'N₃'] ''' # The following will be constant for all ragas @@ -289,6 +301,62 @@ def mela_to_svara(mela, abbr=True, unicode=True): return list(svara_map) +def list_mela(): + """List melakarta ragas by name and index. + + Returns + ------- + mela_map : dict + A dictionary mapping melakarta raga names to indices (1, 2, ..., 72) + + Examples + -------- + >>> librosa.list_mela() + {'kanakanki': 1, + 'ratnangi': 2, + 'ganamurti': 3, + 'vanaspati': 4, + ...} + + See Also + -------- + mela_to_degrees + mela_to_svara + list_thaat + """ + return MELAKARTA_MAP.copy() + + +def list_thaat(): + """List supported thaats by name. + + Returns + ------- + thaats : list + A list of supported thaats + + Examples + -------- + >>> librosa.list_thaat() + ['bilaval', + 'khamaj', + 'kafi', + 'asavari', + 'bhairavi', + 'kalyan', + 'marva', + 'poorvi', + 'todi', + 'bhairav'] + + See Also + -------- + list_mela + thaat_to_degrees + """ + return list(THAAT_MAP.keys()) + + @cache(level=10) def key_to_notes(key, unicode=True): '''Lists all 12 note names in the chromatic scale, as spelled according to From 57035b62d25160cf7204992e82242c55a281987b Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Thu, 16 Jul 2020 11:12:28 -0400 Subject: [PATCH 13/15] added docstrings to notation helpers --- librosa/core/notation.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/librosa/core/notation.py b/librosa/core/notation.py index fafe863654..595c9e12fc 100644 --- a/librosa/core/notation.py +++ b/librosa/core/notation.py @@ -69,6 +69,15 @@ def thaat_to_degrees(thaat): -------- 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()]) @@ -87,9 +96,23 @@ def mela_to_degrees(mela): A list of the seven svara indicies (starting from 0=Sa) contained in the specified raga + See Also + -------- + thaat_to_degrees + key_to_degres + list_mela + Examples -------- - >>> + Melakarta #1 (kanakanki): + + >>> librosa.mela_to_degrees(1) + array([0, 1, 2, 5, 7, 8, 9]) + + Or using a name directly: + + >>> librosa.mela_to_degrees('kanakanki') + array([0, 1, 2, 5, 7, 8, 9]) ''' if isinstance(mela, str): From 0ad81350954a7c6665893fe80d87dec9486878da Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Thu, 16 Jul 2020 13:09:48 -0400 Subject: [PATCH 14/15] finished docstrings for svara converters --- librosa/core/convert.py | 187 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 172 insertions(+), 15 deletions(-) diff --git a/librosa/core/convert.py b/librosa/core/convert.py index 704023b8d7..ce05d1ed27 100644 --- a/librosa/core/convert.py +++ b/librosa/core/convert.py @@ -1723,14 +1723,12 @@ def midi_to_svara_h(midi, Sa, abbr=True, octave=True, unicode=True): Parameters ---------- - midi : numeric - The MIDI numbers to convert + midi : numeric or np.ndarray + The MIDI number or numbers to convert Sa : number > 0 MIDI number of the reference Sa. - Default: 60 (261.6 Hz, `C4`) - abbr : bool If `True` (default) return abbreviated names ('S', 'r', 'R', 'g', 'G', ...) @@ -1757,9 +1755,30 @@ def midi_to_svara_h(midi, Sa, abbr=True, octave=True, unicode=True): -------- hz_to_svara_h note_to_svara_h + midi_to_svara_c + midi_to_note Examples -------- + The first three svara with Sa at midi number 60: + + >>> librosa.midi_svara_h([60, 61, 62], Sa=60) + ['S', 'r', 'R'] + + With Sa=67, midi 60-62 are in the octave below: + + >>> librosa.midi_to_svara_h([60, 61, 62], Sa=67) + ['ṃ', 'Ṃ', 'P̣'] + + Or without unicode decoration: + + >>> librosa.midi_to_svara_h([60, 61, 62], Sa=67, unicode=False) + ['m,', 'M,', 'P,'] + + Or going up an octave, with Sa=60, and using unabbreviated notes + + >>> librosa.midi_to_svara_h([72, 73, 74], Sa=60, abbr=False) + ['Ṡa', 'ṙe', 'Ṙe'] """ SVARA_MAP = ['Sa', 're', 'Re', 'ga', 'Ga', 'ma', 'Ma', @@ -1800,20 +1819,54 @@ def hz_to_svara_h(frequencies, Sa, abbr=True, octave=True, unicode=True): Parameters ---------- + frequencies : positive number or np.ndarray + The frequencies (in Hz) to convert + + Sa : positive number + Frequency (in Hz) of the reference Sa. + + abbr : bool + If `True` (default) return abbreviated names ('S', 'r', 'R', 'g', 'G', ...) + + If `False`, return long-form names ('Sa', 're', 'Re', 'ga', 'Ga', ...) + + octave : bool + If `True`, decorate svara in neighboring octaves with over- or under-dots. + + If `False`, ignore octave height information. + + unicode : bool + If `True`, use unicode symbols to decorate octave information. + + If `False`, use low-order ASCII (' and ,) for octave decorations. + + This only takes effect if `octave=True`. Returns ------- + svara : str or list of str + The svara corresponding to the given frequency/frequencies See Also -------- midi_to_svara_h + note_to_svara_h + hz_to_svara_c + hz_to_note Examples -------- - ''' + Convert Sa in three octaves: - if Sa is None: - Sa = note_to_hz('C2') + >>> librosa.hz_to_svara_h([261/2, 261, 261*2], Sa=261) + ['Ṣ', 'S', 'Ṡ'] + + Convert one octave worth of frequencies with full names: + + >>> freqs = librosa.cqt_frequencies(12, fmin=261) + >>> librosa.hz_to_svara_h(freqs, Sa=freqs[0], abbr=False) + ['Sa', 're', 'Re', 'ga', 'Ga', 'ma', 'Ma', 'Pa', 'dha', 'Dha', 'ni', 'Ni'] + ''' midis = hz_to_midi(frequencies) return midi_to_svara_h(midis, hz_to_midi(Sa), @@ -1827,18 +1880,50 @@ def note_to_svara_h(notes, Sa, abbr=True, octave=True, unicode=True): Parameters ---------- + notes : str or list of str + Notes to convert (e.g., `'C#'` or `['C4', 'Db4', 'D4']` + + Sa : str + Note corresponding to Sa (e.g., `'C'` or `'C5'`). + + If no octave information is provided, it will default to octave 0 + (``C0`` ~= 16 Hz) + + abbr : bool + If `True` (default) return abbreviated names ('S', 'r', 'R', 'g', 'G', ...) + + If `False`, return long-form names ('Sa', 're', 'Re', 'ga', 'Ga', ...) + + octave : bool + If `True`, decorate svara in neighboring octaves with over- or under-dots. + + If `False`, ignore octave height information. + + unicode : bool + If `True`, use unicode symbols to decorate octave information. + + If `False`, use low-order ASCII (' and ,) for octave decorations. + + This only takes effect if `octave=True`. Returns ------- + svara : str or list of str + The svara corresponding to the given notes See Also -------- + midi_to_svara_h + hz_to_svara_h + note_to_svara_c + note_to_midi + note_to_hz Examples -------- + >>> librosa.note_to_svara_h(['C4', 'G4', 'C5', 'G5'], Sa='C5') + ['Ṣ', 'P̣', 'S', 'P'] ''' - if Sa is None: - Sa = 'C2' midis = note_to_midi(notes, round_midi=False) @@ -1924,20 +2009,58 @@ def hz_to_svara_c(frequencies, Sa, mela, abbr=True, octave=True, unicode=True): Parameters ---------- + frequencies : positive number or np.ndarray + The frequencies (in Hz) to convert + + Sa : positive number + Frequency (in Hz) of the reference Sa. + + mela : int [1, 72] or string + The melakarta raga to use. + + abbr : bool + If `True` (default) return abbreviated names ('S', 'R1', 'R2', 'G1', 'G2', ...) + + If `False`, return long-form names ('Sa', 'Ri1', 'Ri2', 'Ga1', 'Ga2', ...) + + octave : bool + If `True`, decorate svara in neighboring octaves with over- or under-dots. + + If `False`, ignore octave height information. + + unicode : bool + If `True`, use unicode symbols to decorate octave information. + + If `False`, use low-order ASCII (' and ,) for octave decorations. + + This only takes effect if `octave=True`. Returns ------- + svara : str or list of str + The svara corresponding to the given frequency/frequencies See Also -------- + note_to_svara_c midi_to_svara_c + hz_to_svara_h + hz_to_note + list_mela Examples -------- - ''' + Convert Sa in three octaves: + + >>> librosa.hz_to_svara_c([261/2, 261, 261*2], Sa=261, mela='kanakanki') + ['Ṣ', 'S', 'Ṡ'] - if Sa is None: - Sa = note_to_hz('C2') + Convert one octave worth of frequencies using melakarta #36: + + >>> freqs = librosa.cqt_frequencies(12, fmin=261) + >>> librosa.hz_to_svara_c(freqs, Sa=freqs[0], mela=36) + ['S', 'R₁', 'R₂', 'R₃', 'G₃', 'M₁', 'M₂', 'P', 'D₁', 'D₂', 'D₃', 'N₃'] + ''' midis = hz_to_midi(frequencies) return midi_to_svara_c(midis, hz_to_midi(Sa), mela, @@ -1951,20 +2074,54 @@ def note_to_svara_c(notes, Sa, mela, abbr=True, octave=True, unicode=True): Parameters ---------- + notes : str or list of str + Notes to convert (e.g., `'C#'` or `['C4', 'Db4', 'D4']` + + Sa : str + Note corresponding to Sa (e.g., `'C'` or `'C5'`). + + If no octave information is provided, it will default to octave 0 + (``C0`` ~= 16 Hz) + + mela : str or int [1, 72] + Melakarta raga name or index + + abbr : bool + If `True` (default) return abbreviated names ('S', 'R1', 'R2', 'G1', 'G2', ...) + + If `False`, return long-form names ('Sa', 'Ri1', 'Ri2', 'Ga1', 'Ga2', ...) + + octave : bool + If `True`, decorate svara in neighboring octaves with over- or under-dots. + + If `False`, ignore octave height information. + + unicode : bool + If `True`, use unicode symbols to decorate octave information. + + If `False`, use low-order ASCII (' and ,) for octave decorations. + + This only takes effect if `octave=True`. Returns ------- + svara : str or list of str + The svara corresponding to the given notes See Also -------- + midi_to_svara_c + hz_to_svara_c + note_to_svara_h + note_to_midi + note_to_hz list_mela Examples -------- + >>> librosa.note_to_svara_h(['C4', 'G4', 'C5', 'D5', 'G5'], Sa='C5', mela=1) + ['Ṣ', 'P̣', 'S', 'G₁', 'P'] ''' - if Sa is None: - Sa = 'C2' - midis = note_to_midi(notes, round_midi=False) return midi_to_svara_c(midis, note_to_midi(Sa), mela, From d50122ff2a548232b049a4c8ed612c0b002d3ed0 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Thu, 16 Jul 2020 14:16:53 -0400 Subject: [PATCH 15/15] updated baseline images for CQT display --- .../test_display/test_coords.png | Bin 17029 -> 17072 bytes .../test_display/test_cqt_note.png | Bin 29851 -> 29746 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/baseline_images/test_display/test_coords.png b/tests/baseline_images/test_display/test_coords.png index f025662734c6a3618159721264f9ca283f9d5c34..2d5ebb7d1492469e485cb62d5b2ce431a6608c8c 100644 GIT binary patch literal 17072 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 y()YJ>fbj`Y0yW32=nK2z0}Y%J{h#0LyShfWK literal 17029 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 WWhM!!{h@GkjN*A!xy-Xyy#5as{G!zW diff --git a/tests/baseline_images/test_display/test_cqt_note.png b/tests/baseline_images/test_display/test_cqt_note.png index 5f6668ca86b699c80baf65aa387077cf117a2ff2..b3d034f71ded912363e1d2ee0e3de4ffa6583e00 100644 GIT binary patch literal 29746 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 UNSpZr4C5F?MMt^h{-am_3m|qBD*ylh