Source code for mutwo.music_parameters.scales

"""Write musical scales e.g. minor scale, pelog scale.

The mutwo scale model is influenced by the music21 scale model.
"""

import functools
import operator
import typing

from mutwo import core_constants
from mutwo import core_utilities
from mutwo import music_parameters
from mutwo import music_utilities

__all__ = ("ScaleFamily", "RepeatingScaleFamily", "Scale")


IntervalSequence: typing.TypeAlias = typing.Sequence[music_parameters.abc.PitchInterval]
WeightSequence: typing.TypeAlias = typing.Sequence[core_constants.Real]

IntervalTuple: typing.TypeAlias = tuple[music_parameters.abc.PitchInterval, ...]
WeightTuple: typing.TypeAlias = tuple[core_constants.Real, ...]

PitchTuple: typing.TypeAlias = tuple[music_parameters.abc.Pitch, ...]

ScaleDegree: typing.TypeAlias = int
ScaleIndex: typing.TypeAlias = int

PeriodRepetitionCount: typing.TypeAlias = int
"""How often the repeating interval has been applied (e.g. the octave)."""

ScalePosition: typing.TypeAlias = tuple[ScaleDegree, PeriodRepetitionCount]

PeriodRepetitionCountTuple: typing.TypeAlias = tuple[PeriodRepetitionCount, ...]
ScaleDegreeTuple: typing.TypeAlias = tuple[ScaleDegree, ...]
ScaleDegreeSequence: typing.TypeAlias = typing.Sequence[ScaleDegree]
PeriodRepetitionCountSequence: typing.TypeAlias = typing.Sequence[PeriodRepetitionCount]


[docs]class ScaleFamily(object): """A :class:`ScaleFamily` is a sorted sequence of :class:`mutwo.music_parameters.abc.PitchInterval`. Scale families are frozen/immutable. The equivalent of a `ScaleFamily` in `music21` is an `AbstractScale`. `Mutwos` implementation supports assigning weights to each interval to express an hierarchy within the scale. You need to explicitly add the prime (1/1) to the interval set if the tonic should appear within the given scale. """ def __init__( self, interval_sequence: IntervalSequence, weight_sequence: typing.Optional[WeightSequence] = None, scale_degree_sequence: typing.Optional[ScaleDegreeSequence] = None, period_repetition_count_sequence: typing.Optional[ PeriodRepetitionCountSequence ] = None, ): interval_tuple = tuple(interval_sequence) weight_tuple = ScaleFamily._weight_sequence_to_weight_tuple( weight_sequence, interval_tuple ) scale_degree_tuple = ScaleFamily._scale_degree_sequence_to_scale_tuple( scale_degree_sequence, interval_tuple ) period_repetition_count_tuple = ScaleFamily._period_repetition_count_sequence_to_period_repetition_count_tuple( period_repetition_count_sequence, interval_tuple ) is_rising = ScaleFamily._is_interval_tuple_rising(interval_tuple) self._interval_tuple = interval_tuple self._weight_tuple = weight_tuple self._scale_degree_tuple = scale_degree_tuple self._period_repetition_count_tuple = period_repetition_count_tuple self._is_rising = is_rising def __eq__(self, other: typing.Any) -> bool: return core_utilities.test_if_objects_are_equal_by_parameter_tuple( self, other, ("interval_tuple", "weight_tuple") ) @staticmethod def _sequence_to_tuple( sequence: typing.Optional[typing.Sequence[typing.Any]], interval_tuple: IntervalTuple, auto_create_tuple: typing.Callable[[IntervalTuple], tuple[typing.Any, ...]], item_name: str, ) -> tuple[core_constants.Real, ...]: sequence = sequence or auto_create_tuple(interval_tuple) tuple_ = tuple(sequence) assert len(tuple_) == len( interval_tuple ), f"We need as many {item_name} entries as there are interval entries!" return tuple_ @staticmethod def _scale_degree_sequence_to_scale_tuple( scale_degree_sequence: typing.Optional[ScaleDegreeSequence], interval_tuple: IntervalTuple, ) -> ScaleDegreeTuple: return ScaleFamily._sequence_to_tuple( scale_degree_sequence, interval_tuple, lambda interval_tuple: tuple(range(len(interval_tuple))), "interval", ) @staticmethod def _weight_sequence_to_weight_tuple( weight_sequence: typing.Optional[WeightSequence], interval_tuple: IntervalTuple ) -> tuple[core_constants.Real, ...]: return ScaleFamily._sequence_to_tuple( weight_sequence, interval_tuple, lambda interval_tuple: tuple(1 for _ in interval_tuple), "weight", ) @staticmethod def _period_repetition_count_sequence_to_period_repetition_count_tuple( period_repetition_count_sequence: typing.Optional[ PeriodRepetitionCountSequence ], interval_tuple: IntervalTuple, ) -> tuple[core_constants.Real, ...]: return ScaleFamily._sequence_to_tuple( period_repetition_count_sequence, interval_tuple, lambda interval_tuple: tuple(0 for _ in interval_tuple), "period repetition count", ) @staticmethod def _is_interval_tuple_rising(interval_tuple: IntervalTuple) -> bool: rising_interval_tuple, falling_interval_tuple = ( tuple(sorted(interval_tuple, reverse=reverse)) for reverse in (False, True) ) is_rising = interval_tuple == rising_interval_tuple if not is_rising and interval_tuple != falling_interval_tuple: raise music_utilities.UnsortedIntervalTupleError(interval_tuple) return is_rising @property def interval_tuple(self) -> IntervalTuple: return self._interval_tuple @property def weight_tuple(self) -> WeightTuple: return self._weight_tuple @property def scale_degree_tuple(self) -> ScaleDegreeTuple: return self._scale_degree_tuple @property def period_repetition_count_tuple(self) -> PeriodRepetitionCountTuple: return self._period_repetition_count_tuple @property def is_rising(self) -> bool: return self._is_rising @functools.cached_property def is_falling(self) -> bool: return not self.is_rising @functools.cached_property def scale_degree_count(self) -> int: return len(set(self.scale_degree_tuple))
[docs]class RepeatingScaleFamily(ScaleFamily): """A `RepeatingScaleFamily` is a sorted sequence of repeating intervals over a repetition interval. Most musical scales are based on :class:`RepeatingScaleFamily`, because most musical scales repeat after one octave. """ def __init__( self, repeating_interval_sequence: IntervalSequence, repetition_interval: music_parameters.abc.PitchInterval = music_parameters.DirectPitchInterval( 1200 ), min_pitch_interval: music_parameters.abc.PitchInterval = music_parameters.DirectPitchInterval( -1200 * 5 ), max_pitch_interval: music_parameters.abc.PitchInterval = music_parameters.DirectPitchInterval( 1200 * 5 ), repeating_weight_sequence: typing.Optional[WeightSequence] = None, repeating_scale_degree_sequence: typing.Optional[ScaleDegreeSequence] = None, repeating_period_repetition_count_sequence: typing.Optional[ PeriodRepetitionCountSequence ] = None, ): # TODO(Make this somehow readable.) repeating_interval_tuple = tuple(repeating_interval_sequence) repeating_weight_tuple = ScaleFamily._weight_sequence_to_weight_tuple( repeating_weight_sequence, repeating_interval_tuple ) repeating_scale_degree_tuple = ( ScaleFamily._scale_degree_sequence_to_scale_tuple( repeating_scale_degree_sequence, repeating_interval_tuple ) ) repeating_period_repetition_count_tuple = ScaleFamily._period_repetition_count_sequence_to_period_repetition_count_tuple( repeating_period_repetition_count_sequence, repeating_interval_tuple ) is_rising = ScaleFamily._is_interval_tuple_rising(repeating_interval_tuple) interval_range = ( repeating_interval_tuple[0].interval - repeating_interval_tuple[-1].interval ) assert abs(interval_range) < abs(repetition_interval.interval), ( "Repetition interval has to be bigger than " "ambitus of repeating interval sequence!" ) interval_data_list = [ (interval, weight, scale_degree, period_repetition_count) for interval, weight, scale_degree, period_repetition_count in zip( repeating_interval_tuple, repeating_weight_tuple, repeating_scale_degree_tuple, repeating_period_repetition_count_tuple, ) if interval >= min_pitch_interval and interval < max_pitch_interval ] for local_repetition_interval in ( repetition_interval, repetition_interval.inverse(mutate=False), ): last_interval_data_list = list( zip( repeating_interval_tuple, repeating_weight_tuple, repeating_scale_degree_tuple, repeating_period_repetition_count_tuple, ) ) if local_repetition_interval.interval > 0: period_repetition_delta = 1 else: period_repetition_delta = -1 while 1: new_interval_data_list = [] is_valid = True for ( interval, weight, scale_degree, period_repetition_count, ) in last_interval_data_list: if ( (new_interval := interval + local_repetition_interval) < max_pitch_interval ) and (new_interval >= min_pitch_interval): new_interval_data_list.append( ( new_interval, weight, scale_degree, period_repetition_count + period_repetition_delta, ) ) else: is_valid = False interval_data_list.extend(new_interval_data_list) if not is_valid: break last_interval_data_list = new_interval_data_list interval_data_list.sort(key=operator.itemgetter(0), reverse=not is_rising) super().__init__(*zip(*interval_data_list))
[docs]class Scale(object): """A :class:`Scale` is a sorted sequence of :class:`~mutwo.music_parameters.abc.Pitch`. The equivalent of a `Scale` in `music21` is an `ConcreteScale`. :class:`Scale` uses a specific nomenclature to differentiate between different positions in a musical scale: 1. `scale_degree`: A scale degree is an octave independent position of a :class:`~mutwo.music_parameters.abc.Pitch`. 2. `scale_index`: A scale index is an absolute index of a :class:`~mutwo.music_parameters.abc.Pitch` within a specific scale. """ def __init__(self, tonic: music_parameters.abc.Pitch, scale_family: ScaleFamily): self.tonic = tonic self.scale_family = scale_family # ###################################################################### # # magic methods # # ###################################################################### # def __contains__(self, item: typing.Any) -> bool: return item in self.pitch_tuple def __eq__(self, other: typing.Any) -> bool: return core_utilities.test_if_objects_are_equal_by_parameter_tuple( self, other, ("tonic", "scale_family") ) # ###################################################################### # # private methods # # ###################################################################### # def _reset(self): for property_name in ("pitch_tuple", "scale_position_tuple"): try: delattr(self, property_name) except AttributeError: pass # ###################################################################### # # public properties # # ###################################################################### # @property def tonic(self) -> music_parameters.abc.Pitch: return self._tonic @tonic.setter def tonic(self, tonic: music_parameters.abc.Pitch): self._tonic = tonic self._reset() @property def scale_family(self) -> ScaleFamily: return self._scale_family @scale_family.setter def scale_family(self, scale_family: ScaleFamily): self._scale_family = scale_family self._reset() @functools.cached_property def pitch_tuple(self) -> PitchTuple: return tuple( self.tonic + interval for interval in self.scale_family.interval_tuple ) @property def weight_tuple(self) -> WeightTuple: return self.scale_family.weight_tuple @property def scale_degree_tuple(self) -> ScaleDegreeTuple: return self.scale_family.scale_degree_tuple @property def period_repetition_count_tuple(self) -> PeriodRepetitionCountTuple: return self.scale_family.period_repetition_count_tuple @functools.cached_property def scale_position_tuple(self) -> tuple[ScalePosition, ...]: return tuple(zip(self.scale_degree_tuple, self.period_repetition_count_tuple)) @property def is_rising(self) -> bool: return self.scale_family.is_rising @property def is_falling(self) -> bool: return self.scale_family.is_falling @property def scale_degree_count(self) -> int: return self.scale_family.scale_degree_count # ###################################################################### # # public methods # # ###################################################################### #
[docs] def pitch_to_scale_degree(self, pitch: music_parameters.abc.Pitch) -> ScaleDegree: return self.scale_degree_tuple[self.pitch_tuple.index(pitch)]
[docs] def scale_position_to_pitch( self, scale_position: ScalePosition ) -> music_parameters.abc.Pitch: return self.pitch_tuple[self.scale_position_tuple.index(scale_position)]
[docs] def pitch_to_scale_position( self, pitch: music_parameters.abc.Pitch ) -> music_parameters.abc.Pitch: return self.scale_position_tuple[self.pitch_tuple.index(pitch)]
[docs] def pitch_to_scale_index(self, pitch: music_parameters.abc.Pitch) -> ScaleIndex: return self.pitch_tuple.index(pitch)
[docs] def scale_index_to_pitch( self, scale_index: ScaleIndex ) -> music_parameters.abc.Pitch: return self.pitch_tuple[scale_index]