Source code for solfege.scale

"""Classes to represent a scale of notes."""

import enum
import functools
import itertools
import typing
from types import MappingProxyType

from solfege import note

_KEYS_PROGRESSION = [
    "C",
    "D",
    "E",
    "F",
    "G",
    "A",
    "B",
]


_KEY_INDEX_MAP = MappingProxyType({k: i for i, k in enumerate(_KEYS_PROGRESSION)})


def _half_step(i: typing.Iterable[str]):
    return next(i)


def _whole_step(i: typing.Iterable[str]):
    _half_step(i)
    return _half_step(i)


[docs] class ScaleType(enum.Enum): """Supported scale types.""" MAJOR = 0 """Enum for selecting a Major scale.""" MINOR = enum.auto """Enum for selecting a Natural Minor scale."""
_SCALE_KEY_SIGNATURES = { "C#": ["c#", "d#", "e#", "f#", "g#", "a#", "b#"], "F#": ["f#", "g#", "a#", "c#", "d#", "e#"], "B": ["c#", "d#", "f#", "g#", "a#"], "E": ["f#", "g#", "c#", "d#"], "A": ["c#", "f#", "g#"], "D": ["f#", "c#"], "G": ["f#"], "C": [], "F": ["bb"], "Bb": ["bb", "eb"], "Eb": ["eb", "ab", "bb"], "Ab": ["ab", "bb", "db", "eb"], "Db": ["db", "eb", "gb", "ab", "bb"], "Gb": ["gb", "ab", "bb", "cb", "db", "eb"], "Cb": ["cb", "db", "eb", "fb", "gb", "ab", "bb"], # minors "A#m": ["a#", "b#", "c#", "d#", "e#", "f#", "g#"], "D#m": ["d#", "e#", "f#", "g#", "a#", "c#"], "G#m": ["g#", "a#", "c#", "d#", "f#"], "C#m": ["c#", "d#", "f#", "g#"], "F#m": ["f#", "g#", "c#"], "Bm": ["c#", "f#"], "Em": ["f#"], "Am": [], "Dm": ["bb"], "Gm": ["bb", "eb"], "Cm": ["eb", "ab", "bb"], "Fm": ["ab", "bb", "db", "eb"], "Bbm": ["bb", "db", "eb", "gb", "ab"], "ebm": ["eb", "gb", "ab", "bb", "cb", "db"], "abm": ["ab", "bb", "cb", "db", "eb", "fb", "gb"], } _SCALE_KEY_MAPPING = { key: {note_name.upper(): modifier for note_name, modifier in part} for key, part in _SCALE_KEY_SIGNATURES.items() } @functools.lru_cache(maxsize=100) def _scale_notes(starting_note: note.Note, major_minor: str): for key in itertools.cycle(_KEYS_PROGRESSION): modifier = _SCALE_KEY_MAPPING[starting_note.name + major_minor].get(key.upper()) or "" yield key + modifier def _rotate(o: typing.Container, n: int): repeated = itertools.cycle(o) return list(itertools.islice(repeated, n, len(o) + n)) _DIATONIC_SOLFEGE_NAMES = ("Do", "Re", "Mi", "Fa", "Sol", "La", "Ti", "Do") _MINOR_DIATONIC_SOLFEGE_NAMES = _rotate(_DIATONIC_SOLFEGE_NAMES[:-1], 5) _MINOR_DIATONIC_SOLFEGE_NAMES.append(_MINOR_DIATONIC_SOLFEGE_NAMES[0]) _CHROMATIC_SOLFEGE = ( {"b": "Ti", "#": "Di"}, # Do {"b": "Ra", "#": "Ri"}, # Re {"b": "Me", "#": "Fa"}, # Mi {"b": "Mi", "#": "Fi"}, # Fa {"b": "Se", "#": "Si"}, # Sol {"b": "Le", "#": "Li"}, # La {"b": "Te", "#": "Do"}, # Ti ) _MINOR_CHROMATIC_SOLFEGE = _rotate(_CHROMATIC_SOLFEGE, 5)
[docs] class Scale: """A representation of a musical scale.""" def __init__(self, starting_note: note.Note, type: ScaleType = ScaleType.MAJOR): """Initialize a scale. Args: starting_note (note.Note): the tonic note the scale starts on. type (ScaleType): Which type of scale to load in. """ self._starting_note = starting_note self._type = type self._starting_note_index = _KEY_INDEX_MAP[f"{self._starting_note.letter}"] self._diatonic_notes = list( note.Note(x, octave=starting_note.octave if i < 8 else starting_note.octave + 1) for i, x in enumerate( itertools.islice( _scale_notes( self._starting_note, major_minor=("m" if self._type == ScaleType.MINOR else ""), ), self._starting_note_index, self._starting_note_index + len(_KEYS_PROGRESSION) + 1, ) ) ) self._diatonic_position_map = { f"{note_.name}": (i if type == ScaleType.MAJOR else (i) % len(self._diatonic_notes)) for i, note_ in enumerate(self._diatonic_notes[:-1]) } def solfege(self, note_: note.Note) -> str: """Get the movable-do solfege name for a note in this scale. For minor keys, moving-do la-based minor is used. Args: note_ (note.Note): The note in question. Returns: str: The solfege name for that note. """ index = self._diatonic_position_map.get(note_.name) if index is not None: return ( _DIATONIC_SOLFEGE_NAMES[index] if self._type == ScaleType.MAJOR else _MINOR_DIATONIC_SOLFEGE_NAMES[index] ) base_index, base_note = next( ( (i, note_base) for i, note_base in enumerate(self._diatonic_notes) if note_base.letter == note_.letter ), (None, None), ) if base_index is None: raise ValueError( f"Either {note_} is not valid in the key of {self._starting_note.name}" ) if base_note.letter == note_.letter: # this is the note that was shifted chromatic_map = ( _CHROMATIC_SOLFEGE if self._type == ScaleType.MAJOR else _MINOR_CHROMATIC_SOLFEGE ) result = chromatic_map[base_index].get(note_.accidental) if result is not None: return result if base_note.accidental and not note_.accidental: if base_note.accidental == "#": # base note is sharp, return as if it were lowered return chromatic_map[base_index].get("b") else: return chromatic_map[base_index].get("#") raise ValueError("This case was not handled.")