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 81% rename from librosa/core/time_frequency.py rename to librosa/core/convert.py index 94fefc8940..ce05d1ed27 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,8 +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'] def frames_to_samples(frames, hop_length=512, n_fft=None): @@ -638,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)) @@ -1717,222 +1718,412 @@ 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). +def midi_to_svara_h(midi, Sa, abbr=True, octave=True, unicode=True): + """Convert MIDI numbers to Hindustani svara - 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. + Parameters + ---------- + midi : numeric or np.ndarray + The MIDI number or numbers to convert + + Sa : number > 0 + MIDI number 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 MIDI number(s) + + See Also + -------- + 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', + 'Pa', 'dha', 'Dha', 'ni', 'Ni'] + + SVARA_MAP_SHORT = list(s[0] for s in SVARA_MAP) - Note names are decided by the following rules: + if not np.isscalar(midi): + return [midi_to_svara_h(m, 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[svara_num % 12] - 1. If the tonic of the key has an accidental (sharp or flat), that accidental will be - used consistently for all notes. + 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 - 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. +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. Parameters ---------- - key : string - Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), - key must be lower-case (``maj`` or ``min``). + frequencies : positive number or np.ndarray + The frequencies (in Hz) to convert - Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported. + Sa : positive number + Frequency (in Hz) of the reference Sa. - Examples: ``C:maj, Db:min, A♭:min``. + abbr : bool + If `True` (default) return abbreviated names ('S', 'r', 'R', 'g', 'G', ...) - unicode: bool - If ``True`` (default), use Unicode symbols (♯𝄪♭𝄫)for accidentals. + If `False`, return long-form names ('Sa', 're', 'Re', 'ga', 'Ga', ...) - If ``False``, Unicode symbols will be mapped to low-order ASCII representations:: + octave : bool + If `True`, decorate svara in neighboring octaves with over- or under-dots. - ♯ -> #, 𝄪 -> ##, ♭ -> b, 𝄫 -> bb + 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 ------- - 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. + svara : str or list of str + The svara corresponding to the given frequency/frequencies See Also -------- - midi_to_note + midi_to_svara_h + note_to_svara_h + hz_to_svara_c + hz_to_note Examples -------- - `C:maj` will use all sharps + Convert Sa in three octaves: + + >>> librosa.hz_to_svara_h([261/2, 261, 261*2], Sa=261) + ['Ṣ', 'S', 'Ṡ'] + + Convert one octave worth of frequencies with full names: - >>> librosa.key_to_notes('C:maj') - ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] + >>> 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), + abbr=abbr, octave=octave, unicode=unicode) + + +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. + + Parameters + ---------- + notes : str or list of str + Notes to convert (e.g., `'C#'` or `['C4', 'Db4', 'D4']` - `A:min` has the same notes + Sa : str + Note corresponding to Sa (e.g., `'C'` or `'C5'`). - >>> librosa.key_to_notes('A:min') - ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] + If no octave information is provided, it will default to octave 0 + (``C0`` ~= 16 Hz) - `A♯:min` will use sharps, but spell note 0 (`C`) as `B♯` + abbr : bool + If `True` (default) return abbreviated names ('S', 'r', 'R', 'g', 'G', ...) - >>> librosa.key_to_notes('A#:min') - ['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] + If `False`, return long-form names ('Sa', 're', 'Re', 'ga', 'Ga', ...) - `G♯:maj` will use a double-sharp to spell note 7 (`G`) as `F𝄪`: + octave : bool + If `True`, decorate svara in neighboring octaves with over- or under-dots. - >>> librosa.key_to_notes('G#:maj') - ['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'F𝄪', 'G♯', 'A', 'A♯', 'B'] + If `False`, ignore octave height information. - `F♭:min` will use double-flats + unicode : bool + If `True`, use unicode symbols to decorate octave information. - >>> librosa.key_to_notes('Fb:min') - ['D𝄫', 'D♭', 'E𝄫', 'E♭', 'F♭', 'F', 'G♭', 'A𝄫', 'A♭', 'B𝄫', 'B♭', 'C♭'] + 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'] ''' - # 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)) + midis = note_to_midi(notes, round_midi=False) - 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} + return midi_to_svara_h(midis, note_to_midi(Sa), abbr=abbr, octave=octave, + unicode=unicode) - tonic = match.group('tonic').upper() - accidental = match.group('accidental') - offset = acc_map[accidental] - scale = match.group('scale')[:3].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 - # Determine major or minor - major = (scale == 'maj') + Parameters + ---------- + midi : numeric + The MIDI numbers to convert - # 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 + mela : int or str + The name or index of the melakarta raga - # 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. + Sa : number > 0 + MIDI number of the reference Sa. - if offset < 0: - # use flats explicitly - use_sharps = False + Default: 60 (261.6 Hz, `C4`) - elif offset > 0: - # use sharps explicitly - use_sharps = True + abbr : bool + If `True` (default) return abbreviated names ('S', 'R1', 'R2', 'G1', 'G2', ...) - elif 0 <= tonic_number < 6: - use_sharps = True + If `False`, return long-form names ('Sa', 'Ri1', 'Ri2', 'Ga1', 'Ga2', ...) - elif tonic_number > 6: - use_sharps = False + octave : bool + If `True`, decorate svara in neighboring octaves with over- or under-dots. - # 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'] + If `False`, ignore octave height information. - # These apply when we have >= 6 sharps - sharp_corrections = [(5, 'E♯'), (0, 'B♯'), (7, 'F𝄪'), - (2, 'C𝄪'), (9, 'G𝄪'), (4, 'D𝄪'), (11, 'A𝄪')] + unicode : bool + If `True`, use unicode symbols to decorate octave information and subscript + numbers. - # 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𝄫') + If `False`, use low-order ASCII (' and ,) for octave decorations. - # 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 + 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 + list_mela + ''' + if not np.isscalar(midi): + return [midi_to_svara_c(m, Sa, mela, abbr=abbr, + octave=octave, unicode=unicode) + for m in midi] - 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 + svara_num = int(np.round(midi - Sa)) - notes = notes_sharp - else: - n_flats = (12 - tonic_number) % 12 + svara_map = notation.mela_to_svara(mela, abbr=abbr, unicode=unicode) - # 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 + svara = svara_map[svara_num % 12] - notes = notes_flat + 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 += "," - # 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 svara - return notes +def hz_to_svara_c(frequencies, Sa, mela, abbr=True, octave=True, unicode=True): + '''Convert frequencies (in Hz) to Carnatic svara -def key_to_degrees(key): - """Construct the diatonic scale degrees for a given key. + Note that this conversion assumes 12-tone equal temperament. Parameters ---------- - key : str - Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), - key must be lower-case (``maj`` or ``min``). + 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', ...) - Single accidentals (``b!♭`` for flat, or ``#♯`` for sharp) are supported. + If `False`, return long-form names ('Sa', 'Ri1', 'Ri2', 'Ga1', 'Ga2', ...) - Examples: ``C:maj, Db:min, A♭:min``. + 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 ------- - 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. + svara : str or list of str + The svara corresponding to the given frequency/frequencies See Also -------- - key_to_notes + note_to_svara_c + midi_to_svara_c + hz_to_svara_h + hz_to_note + list_mela Examples -------- - >>> librosa.key_to_degrees('C:maj') - array([ 0, 2, 4, 5, 7, 9, 11]) + Convert Sa in three octaves: - >>> librosa.key_to_degrees('C#:maj') - array([ 1, 3, 5, 6, 8, 10, 0]) + >>> librosa.hz_to_svara_c([261/2, 261, 261*2], Sa=261, mela='kanakanki') + ['Ṣ', 'S', 'Ṡ'] - >>> librosa.key_to_degrees('A:min') - array([ 9, 11, 0, 2, 4, 5, 7]) + Convert one octave worth of frequencies using melakarta #36: - """ - notes = dict(maj=np.array([0, 2, 4, 5, 7, 9, 11]), - min=np.array([0, 2, 3, 5, 7, 8, 10])) + >>> 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₃'] + ''' - 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)) + midis = hz_to_midi(frequencies) + return midi_to_svara_c(midis, hz_to_midi(Sa), mela, + abbr=abbr, octave=octave, unicode=unicode) - 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() +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. + + 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'] + ''' + midis = note_to_midi(notes, round_midi=False) - return (notes[scale] + pitch_map[tonic] + offset) % 12 + return midi_to_svara_c(midis, note_to_midi(Sa), mela, + abbr=abbr, octave=octave, + unicode=unicode) diff --git a/librosa/core/notation.py b/librosa/core/notation.py new file mode 100644 index 0000000000..595c9e12fc --- /dev/null +++ b/librosa/core/notation.py @@ -0,0 +1,601 @@ +#!/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', + 'list_mela', 'list_thaat'] + +THAAT_MAP = dict(bilaval = [0, 2, 4, 5, 7, 9, 11], + khamaj = [0, 2, 4, 5, 7, 9, 10], + kafi = [0, 2, 3, 5, 7, 9, 10], + asavari = [0, 2, 3, 5, 7, 8, 10], + bhairavi = [0, 1, 3, 5, 7, 8, 10], + kalyan = [0, 2, 4, 6, 7, 9, 11], + marva = [0, 1, 4, 6, 7, 9, 11], + poorvi = [0, 1, 4, 6, 7, 8, 11], + todi = [0, 1, 3, 6, 7, 8, 11], + bhairav = [0, 1, 4, 5, 7, 8, 11]) + +# Enumeration will start from 1 +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', 'kosalam', 'rasikapriya'], 1)} + + +def thaat_to_degrees(thaat): + '''Construct the svara indices (degrees) for a given thaat + + Parameters + ---------- + thaat : str + The name of the thaat + + Returns + ------- + indices : np.ndarray + A list of the seven svara indicies (starting from 0=Sa) + contained in the specified thaat + + See Also + -------- + key_to_degrees + mela_to_degrees + list_thaat + + Examples + -------- + >>> librosa.thaat_to_degrees('bilaval') + array([ 0, 2, 4, 5, 7, 9, 11]) + + >>> librosa.thaat_to_degrees('todi') + array([ 0, 1, 3, 6, 7, 8, 11]) + ''' + return np.asarray(THAAT_MAP[thaat.lower()]) + + +def mela_to_degrees(mela): + '''Construct the svara indices (degrees) for a given melakarta raga + + Parameters + ---------- + mela : str or int + Either the name or integer index ([1, 2, ..., 72]) of the melakarta raga + + Returns + ------- + degrees : np.ndarray + A list of the seven svara 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): + index = MELAKARTA_MAP[mela.lower()] - 1 + elif 0 < mela <= 72: + index = mela - 1 + else: + raise ParameterError('mela={} must be in range [1, 72]'.format(mela)) + + # 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 + list_mela + + Examples + -------- + Melakarta #1 (Kanakanki) 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 (Chalanattai) 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('chalanattai') + ['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()] - 1 + elif 0 < mela <= 72: + mela_idx = mela - 1 + else: + raise ParameterError('mela={} must be in range [1, 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) + + +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 + 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/display.py b/librosa/display.py index c223358080..b7d5f950a4 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.Sa, self.mela, 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': @@ -864,14 +938,31 @@ def __decorate_axis(axis, ax_type, key='C:maj'): 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))) axis.set_label_text('Note') + elif ax_type == 'cqt_svara': + axis.set_major_formatter(SvaraFormatter(Sa=Sa, mela=mela)) + # 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=sa_offset * 2.0**(np.arange(1, 12)/12.0))) + axis.set_label_text('Svara') + 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, 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/baseline_images/test_display/test_coords.png b/tests/baseline_images/test_display/test_coords.png index f025662734..2d5ebb7d14 100644 Binary files a/tests/baseline_images/test_display/test_coords.png and b/tests/baseline_images/test_display/test_coords.png differ diff --git a/tests/baseline_images/test_display/test_cqt_note.png b/tests/baseline_images/test_display/test_cqt_note.png index 5f6668ca86..b3d034f71d 100644 Binary files a/tests/baseline_images/test_display/test_cqt_note.png and b/tests/baseline_images/test_display/test_cqt_note.png differ 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)