Source code for mutwo.music_converters.pitches

import itertools
import typing

try:
    import quicktions as fractions  # type: ignore
except ImportError:
    import fractions  # type: ignore

import gradient_free_optimizers

from mutwo import core_converters
from mutwo import music_parameters


__all__ = ("ImproveWesternPitchListSequenceReadability",)


[docs]class ImproveWesternPitchListSequenceReadability(core_converters.abc.Converter): """Adjust accidentals of pitches for a tonal-like visual representation :param simultaneous_pitch_weight: Factor with which the weights of the resulting fitness from pitches of the same pitch list will be multiplied. Use higher value if a good form of simultaneous pitches is more important for you. Default to 1. :type simultaneous_pitch_weight: float :param sequential_pitch_weight: Factor with which the weights of the resulting fitness from pitches of neighbouring pitch lists will be multiplied. Use higher value if a good form of sequential pitches is more important for you. Default to 0.7. :type sequential_pitch_weight: float :param iteration_count: How many iterations the heuristic algorithm shall run. Use higher number for better (but slower) results. Default to 10000. :type iteration_count: int :param optimizer_class: Sets optimizer class used within the converter. This can be any optimizer defined in the `gradient_free_optimizers <https://github.com/SimonBlanke/Gradient-Free-Optimizers>`_ package. Default to :class:`gradient_free_optimizers.RandomSearchOptimizer`. :type: gradient_free_optimizers.optimizers.base_optimizer.BaseOptimizer, :param verbosity_list: From 'gradient_free_optimizers' documentation: "The verbosity list determines what part of the optimization information will be printed in the command line.". The complete list would be `["progress_bar", "print_results", "print_times"]`. Default to [] (no logging, silent). :param seed: The random seed used within the algorithm. Can be `None` for not-deterministic output. Default to 100. :type seed: typing.Optional[int] This converter aims to adjust :class:`music_parameters.WesternPitch`s in order to improve the quality of western notation created with these pitches. Non-tonal music should be notated in a way to make it look as tonal as possible (e.g. it should notate intervals musicians are used to, it should avoid augmented or diminished intervals). The converter aims to maximize simple intervals (without changing the actual pitch content) by heuristic techniques. The converter may not return the best solution, but a very good approximation. **Disclaimer:** This converter doesn't work with microtonal pitches! This is due to the fact that :class:`mutwo.music_parameters.WesternPitchInterval` doesn't support microtonal pitches yet. """ PitchVariantListTuple = tuple[list[tuple[music_parameters.WesternPitch, ...]], ...] PitchNameTupleToIntervalQualityDict = dict[tuple[str], bool] SearchSpace = dict[str, int] RealSearchSpace = dict[str, tuple[music_parameters.WesternPitch]] _space_separator = "_" def __init__( self, simultaneous_pitch_weight: float = 1, sequential_pitch_weight: float = 0.7, iteration_count: int = 10000, optimizer_class: gradient_free_optimizers.optimizers.base_optimizer.BaseOptimizer = gradient_free_optimizers.RandomSearchOptimizer, verbosity_list: list[str] = [], seed: typing.Optional[int] = 100, ): self._simultaneous_pitch_weight = simultaneous_pitch_weight self._sequential_pitch_weight = sequential_pitch_weight self._iteration_count = iteration_count self._optimizer_class = optimizer_class self._verbosity_list = verbosity_list self._seed = seed @staticmethod def _get_pitch_variant_tuple( western_pitch_to_find_variants_for: music_parameters.WesternPitch, ) -> tuple[music_parameters.WesternPitch, ...]: try: assert not western_pitch_to_find_variants_for.is_microtonal except AssertionError: raise ValueError( "Found illegal microtonal pitch " f"'{western_pitch_to_find_variants_for}'. " "Converter doesn't support microtonal pitches!" ) western_pitch_base = music_parameters.WesternPitch( western_pitch_to_find_variants_for.pitch_class, western_pitch_to_find_variants_for.octave, ) return (western_pitch_base,) + western_pitch_base.enharmonic_pitch_tuple @staticmethod def _get_pitch_variant_list_tuple( western_pitch_list_sequence_to_convert: typing.Sequence[ list[music_parameters.WesternPitch] ], ) -> PitchVariantListTuple: pitch_variant_list_list = [] for western_pitch_list in western_pitch_list_sequence_to_convert: pitch_variant_list = [] for western_pitch in western_pitch_list: pitch_variant_list.append( ImproveWesternPitchListSequenceReadability._get_pitch_variant_tuple( western_pitch ) ) pitch_variant_list_list.append(pitch_variant_list) return tuple(pitch_variant_list_list) @staticmethod def _get_pitch_name_tuple_to_interval_quality_dict( pitch_variant_list_tuple: PitchVariantListTuple, ) -> PitchNameTupleToIntervalQualityDict: pitch_class_name_set = set([]) for pitch_variant_list in pitch_variant_list_tuple: for pitch_variant in pitch_variant_list: for pitch in pitch_variant: pitch_class_name_set.add(pitch.pitch_class_name) pitch_name_tuple_to_interval_quality_dict = {} for ( pitch_class_name0, pitch_class_name1, ) in itertools.combinations_with_replacement(pitch_class_name_set, 2): pitch_interval = music_parameters.WesternPitch( pitch_class_name0 ).get_pitch_interval(music_parameters.WesternPitch(pitch_class_name1)) can_be_simplified = pitch_interval.can_be_simplified pitch_name_tuple_to_interval_quality_dict.update( { tuple( sorted([pitch_class_name0, pitch_class_name1]) ): can_be_simplified } ) return pitch_name_tuple_to_interval_quality_dict @staticmethod def _get_search_space_and_real_search_space( pitch_variant_list_tuple: PitchVariantListTuple, ) -> tuple[SearchSpace, RealSearchSpace]: search_space = {} real_search_space = {} for pitch_list_index, pitch_variant_list in enumerate(pitch_variant_list_tuple): for pitch_variant_index, pitch_variant in enumerate(pitch_variant_list): index_name = "{}{}{}".format( pitch_list_index, ImproveWesternPitchListSequenceReadability._space_separator, pitch_variant_index, ) search_space.update({index_name: tuple(range(len(pitch_variant)))}) real_search_space.update({index_name: pitch_variant}) return search_space, real_search_space def _get_objective_function( self, pitch_name_tuple_to_interval_quality_dict: PitchNameTupleToIntervalQualityDict, real_search_space: RealSearchSpace, ) -> typing.Callable[[dict], float]: def compare_two_pitches( pitch0: music_parameters.WesternPitch, pitch1: music_parameters.WesternPitch ) -> int: return pitch_name_tuple_to_interval_quality_dict[ tuple(sorted([pitch0.pitch_class_name, pitch1.pitch_class_name])) ] def objective_function(solution: dict[str, int]) -> float: fitness = 0 western_pitch_list_tuple = ImproveWesternPitchListSequenceReadability._get_western_pitch_list_tuple( solution, real_search_space ) for western_pitch_list in western_pitch_list_tuple: for pitch0, pitch1 in itertools.combinations(western_pitch_list, 2): fitness += ( compare_two_pitches(pitch0, pitch1) * self._simultaneous_pitch_weight ) for western_pitch_list0, western_pitch_list1 in zip( western_pitch_list_tuple, western_pitch_list_tuple[1:] ): for pitch0, pitch1 in itertools.product( western_pitch_list0, western_pitch_list1 ): fitness += ( compare_two_pitches(pitch0, pitch1) * self._sequential_pitch_weight ) # We prefer if there are not so many double-sharps and # double-flats, therefore we will punish the algorithm # if there is any (we have to do this because from only # interval relationship point-of-view there are often # equally acceptable solutions with a lot of accidentals). for western_pitch_list in western_pitch_list_tuple: for western_pitch in western_pitch_list: if western_pitch.accidental_name in ( music_parameters.constants.PITCH_CLASS_MODIFICATION_TO_ACCIDENTAL_NAME_DICT[ fractions.Fraction(2, 1) ], music_parameters.constants.PITCH_CLASS_MODIFICATION_TO_ACCIDENTAL_NAME_DICT[ -fractions.Fraction(2, 1) ], ): # This is an arbitrary heuristic value, could also be # something else than 2. fitness += 2 break # This is a MAX optimization; therefore we have to return # negative value because it is always +1 if interval # `can_be_simplified` (= isn't perfect yet). return -fitness return objective_function @staticmethod def _get_western_pitch_list_tuple( champion_name_to_champion_value_dict: dict[str, int], real_search_space: RealSearchSpace, ) -> tuple[list[music_parameters.WesternPitch], ...]: western_pitch_list_list = [] for ( champion_name, western_pitch_index, ) in champion_name_to_champion_value_dict.items(): pitch_list_index, _ = ( int(index) for index in champion_name.split( ImproveWesternPitchListSequenceReadability._space_separator ) ) while pitch_list_index >= len(western_pitch_list_list): western_pitch_list_list.append([]) western_pitch = real_search_space[champion_name][western_pitch_index] western_pitch_list_list[pitch_list_index].append(western_pitch) return tuple(western_pitch_list_list)
[docs] def convert( self, western_pitch_list_sequence_to_convert: typing.Sequence[ list[music_parameters.WesternPitch] ], ) -> tuple[list[music_parameters.WesternPitch], ...]: """Simplify western pitch notation. :param western_pitch_list_sequence_to_convert: A sequence filled with lists of :class:`mutwo.music_parameters.WesternPitch`. The pitches will be simplified. :type western_pitch_list_sequence_to_convert: typing.Sequence[list[music_parameters.WesternPitch]] :return: A tuple with lists that contain :class:`music_parameters.WesternPitch`. The raw pitch content will be the same as the input data, but the accidentals and diatonic pitch class names may differ. """ pitch_variant_list_tuple = ( ImproveWesternPitchListSequenceReadability._get_pitch_variant_list_tuple( western_pitch_list_sequence_to_convert ) ) pitch_name_tuple_to_interval_quality_dict = ImproveWesternPitchListSequenceReadability._get_pitch_name_tuple_to_interval_quality_dict( pitch_variant_list_tuple ) # We have to use two different search spaces: # The first one which only holds indices and the second # one which holds the actual pitch objects (resolutions to # these indices). This behaviour is necessary, because # the optimizer provided by gradient_free_optimizers # can't handle anything else but simple numeric # values in its search space (because it applies # arithmetic operations on it). ( search_space, real_search_space, ) = ImproveWesternPitchListSequenceReadability._get_search_space_and_real_search_space( pitch_variant_list_tuple ) objective_function = self._get_objective_function( pitch_name_tuple_to_interval_quality_dict, real_search_space ) optimizer = self._optimizer_class(search_space, random_state=self._seed) optimizer.search( objective_function, n_iter=self._iteration_count, verbosity=self._verbosity_list, ) western_pitch_list_tuple = ( ImproveWesternPitchListSequenceReadability._get_western_pitch_list_tuple( optimizer.best_para, real_search_space ) ) return western_pitch_list_tuple