Source code for mutwo.ekmelily_converters.ekmelily

"""Build tuning files for `Lilypond <https://lilypond.org/>`_ extension `Ekmelily <http://www.ekmelic-music.org/en/extra/ekmelily.htm>`_.

By default the smallest step which Lilypond supports is one quartertone. With
the help of Ekmelily it is easily possible to add more complex micro- or
macrotonal tunings to Lilypond. The converter in this module aims to make it easier
to build tuning files to be used with the 'ekmel-main.ily' script from Thomas Richter.

**Disclaimer:**

For now the converters only support making notation tables for English note names.
"""

import dataclasses
import itertools
import typing
import warnings

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

from mutwo import core_converters
from mutwo import core_constants
from mutwo import ekmelily_converters
from mutwo import music_parameters

__all__ = (
    "EkmelilyAccidental",
    "EkmelilyTuningFileConverter",
    "HEJIEkmelilyTuningFileConverter",
)


[docs]@dataclasses.dataclass(frozen=True) class EkmelilyAccidental(object): """Representation of an Ekmelily accidental. :param accidental_name: The name of the accidental that follows after the diatonic pitch name (e.g. 's' or 'qf') :type accidental_name: str :param accidental_glyph_tuple: The name of accidental glyphs that should appear before the notehead. For a list of available glyphs, check the documentation of `Ekmelos <http://www.ekmelic-music.org/en/extra/ekmelos.htm>`_. Furthermore one can find mappings from mutwo data to Ekmelos glyph names in :const:`~mutwo.ext.ekmelily_converters.constants.PRIME_AND_EXPONENT_AND_TRADITIONAL_ACCIDENTAL_TO_ACCIDENTAL_GLYPH_DICT` and :const:`~mutwo.ext.ekmelily_converters.constants.TEMPERED_ACCIDENTAL_TO_ACCIDENTAL_GLYPH_DICT`. :type accidental_glyph_tuple: tuple[str, ...] :param deviation_in_cents: How many cents shall an altered pitch differ from its diatonic / natural counterpart. :type deviation_in_cents: float :param available_diatonic_pitch_index_tuple: Sometimes one may want to define accidentals which are only available for certain diatonic music_parameters. For this case, one can use this argument and specify all diatonic music_parameters which should know this accidental. If this argument keeps undefined, the accidental will be added to all seven diatonic music_parameters. :type available_diatonic_pitch_index_tuple: typing.Optional[tuple[int, ...]], optional **Example:** >>> from mutwo.ext.converter.frontends import ekmelily >>> natural = ekmelily.EkmelilyAccidental('', ("#xE261",), 0) >>> sharp = ekmelily.EkmelilyAccidental('s', ("#xE262",), 100) >>> flat = ekmelily.EkmelilyAccidental('f', ("#xE260",), -100) """ accidental_name: str accidental_glyph_tuple: tuple[str, ...] deviation_in_cents: float available_diatonic_pitch_index_tuple: typing.Optional[tuple[int, ...]] = None def __hash__(self) -> int: return hash( ( self.accidental_name, self.accidental_glyph_tuple, self.deviation_in_cents, self.available_diatonic_pitch_index_tuple, ) )
[docs]class EkmelilyTuningFileConverter(core_converters.abc.Converter): """Build Ekmelily tuning files from Ekmelily accidentals. :param path: Path where the new Ekmelily tuning file shall be written. The suffix '.ily' is recommended, but not necessary. :type path: str :param ekmelily_accidental_sequence: A sequence which contains all :class:`EkmelilyAccidental` that shall be written to the tuning file, :type ekmelily_accidental_sequence: typing.Sequence[EkmelilyAccidental] :param global_scale: From the `Lilypond documentation <https://lilypond.org/doc/v2.20/Documentation/notation/scheme-functions>`_: "This determines the tuning of music_parameters with no accidentals or key signatures. The first pitch is c. Alterations are calculated relative to this scale. The number of music_parameters in this scale determines the number of scale steps that make up an octave. Usually the 7-note major scale." :type global_scale: tuple[fractions.Fraction, ...], optional **Example:** >>> from mutwo.converter.frontends import ekmelily >>> natural = ekmelily.EkmelilyAccidental('', ("#xE261",), 0) >>> sharp = ekmelily.EkmelilyAccidental('s', ("#xE262",), 100) >>> flat = ekmelily.EkmelilyAccidental('f', ("#xE260",), -100) >>> eigth_tone_sharp = ekmelily.EkmelilyAccidental('es', ("#xE2C7",), 25) >>> eigth_tone_flat = ekmelily.EkmelilyAccidental('ef', ("#xE2C2",), -25) >>> converter = ekmelily.EkmelilyTuningFileConverter( >>> 'ekme-test.ily', (natural, sharp, flat, eigth_tone_sharp, eigth_tone_flat) >>> ) >>> converter.convert() """ def __init__( self, path: str, ekmelily_accidental_sequence: typing.Sequence[EkmelilyAccidental], # should have exactly 7 fractions (one for each diatonic pitch) global_scale: typing.Optional[tuple[fractions.Fraction, ...]] = None, ): if global_scale is None: # set to default 12 EDO, a' = 440 Hertz global_scale = ekmelily_converters.configurations.DEFAULT_GLOBAL_SCALE global_scale = EkmelilyTuningFileConverter._correct_global_scale(global_scale) self._path = path self._global_scale = global_scale self._ekmelily_accidental_sequence = ekmelily_accidental_sequence ( self._accidental_to_alteration_code_mapping, self._alteration_code_to_alteration_fraction_mapping, ) = EkmelilyTuningFileConverter._make_accidental_to_alteration_code_mapping_and_alteration_code_to_alteration_fraction_mapping( self._ekmelily_accidental_sequence ) # ###################################################################### # # static methods # # ###################################################################### # @staticmethod def _correct_global_scale( global_scale: tuple[fractions.Fraction, ...] ) -> tuple[fractions.Fraction, ...]: # Lilypond doesn't allow negative values for first item in global scale. # Therefore Mutwo makes sure that the first item isn't a negative number. corrected_global_scale = list(global_scale) if corrected_global_scale[0] != 0: message = ( "Found value '{}' for first scale degree in global scale. Autoset value" " to 0 (Lilypond doesn't allow values != 0 for the first scale degree)".format( corrected_global_scale[0] ) ) warnings.warn(message) corrected_global_scale[0] = fractions.Fraction(0, 1) return tuple(corrected_global_scale) @staticmethod def _deviation_in_cents_to_alteration_fraction( deviation_in_cents: core_constants.Real, max_denominator: int = 1000 ) -> fractions.Fraction: # simplify fraction to avoid too complex calculations during Lilyponds midi # render (otherwise Lilypond won't be able to render Midi / it will take # too long) return fractions.Fraction(deviation_in_cents / 200).limit_denominator( max_denominator ) @staticmethod def _alteration_fraction_to_deviation_in_cents( alteration_fraction: fractions.Fraction, ) -> float: return float(alteration_fraction * 200) @staticmethod def _accidental_index_to_alteration_code(accidental_index: int) -> str: # convert index of accidental to hex code in a format that is # readable by Lilypond return "#x{}".format(str(hex(accidental_index))[2:].upper()) @staticmethod def _find_and_group_accidentals_by_specific_deviation_in_cents( ekmelily_accidental_sequence: typing.Sequence[EkmelilyAccidental], deviation_in_cents: float, ) -> tuple[tuple[EkmelilyAccidental, ...], tuple[EkmelilyAccidental, ...]]: positive_list, negative_list = [], [] for accidental in ekmelily_accidental_sequence: if accidental.deviation_in_cents == deviation_in_cents: positive_list.append(accidental) elif ( accidental.deviation_in_cents == -deviation_in_cents and deviation_in_cents != 0 ): negative_list.append(accidental) return tuple(positive_list), tuple(negative_list) @staticmethod def _group_accidentals_by_deviations_in_cents( ekmelily_accidental_sequence: typing.Sequence[EkmelilyAccidental], ) -> tuple[tuple[float, tuple[tuple[EkmelilyAccidental, ...], ...]], ...,]: """Put all accidentals with the same absolute deviation to the same tuple. The first element of each tuple is the absolute deviation in cents, the second element is tuple with two elements where the first element contains all positive accidentals and the second tuple all negative accidentals. """ available_deviations_in_cents = sorted( set( map( lambda accidental: abs(accidental.deviation_in_cents), ekmelily_accidental_sequence, ) ) ) accidentals_grouped_by_deviations_in_cents = tuple( ( deviation_in_cents, EkmelilyTuningFileConverter._find_and_group_accidentals_by_specific_deviation_in_cents( ekmelily_accidental_sequence, deviation_in_cents ), ) for deviation_in_cents in available_deviations_in_cents ) return accidentals_grouped_by_deviations_in_cents @staticmethod def _process_single_accidental( accidental_to_alteration_code_mapping: dict[EkmelilyAccidental, str], alteration_code_to_alteration_fraction_mapping: dict[str, fractions.Fraction], accidental: typing.Optional[EkmelilyAccidental], accidental_index: int, is_positive: bool, absolute_deviation_in_cents: float, ): alteration_code = ( EkmelilyTuningFileConverter._accidental_index_to_alteration_code( accidental_index ) ) if accidental: accidental_to_alteration_code_mapping.update({accidental: alteration_code}) if is_positive: alteration_fraction = ( EkmelilyTuningFileConverter._deviation_in_cents_to_alteration_fraction( absolute_deviation_in_cents ) ) alteration_code_to_alteration_fraction_mapping.update( {alteration_code: alteration_fraction} ) @staticmethod def _process_accidental_pair( accidental_to_alteration_code_mapping: dict[EkmelilyAccidental, str], alteration_code_to_alteration_fraction_mapping: dict[str, fractions.Fraction], positive_accidental: typing.Optional[EkmelilyAccidental], positive_alteration_index: int, negative_accidental: typing.Optional[EkmelilyAccidental], negative_alteration_index: int, absolute_deviation_in_cents: float, ) -> None: # Add accidental data to accidental_to_alteration_code_mapping # and alteration_code_to_alteration_fraction_mapping. for is_positive, accidental, accidental_index in ( (True, positive_accidental, positive_alteration_index), (False, negative_accidental, negative_alteration_index), ): EkmelilyTuningFileConverter._process_single_accidental( accidental_to_alteration_code_mapping, alteration_code_to_alteration_fraction_mapping, accidental, accidental_index, is_positive, absolute_deviation_in_cents, ) @staticmethod def _get_accidental_from_accidental_iterator( accidental_iterator: typing.Iterator, ) -> typing.Optional[EkmelilyAccidental]: try: accidental = next(accidental_iterator) except StopIteration: accidental = None return accidental @staticmethod def _process_accidental_group( accidental_to_alteration_code_mapping: dict[EkmelilyAccidental, str], alteration_code_to_alteration_fraction_mapping: dict[str, fractions.Fraction], nth_alteration: int, accidental_group: tuple[tuple[EkmelilyAccidental, ...], ...], absolute_deviation_in_cents: float, ) -> int: # Define alteration codes and alteration fractions for accidentals # in one accidental group and add the calculated data to both mappings. positive_accidentals, negative_accidentals = accidental_group positive_accidentals_iterator = iter(positive_accidentals) negative_accidentals_iterator = iter(negative_accidentals) for _ in range(max((len(positive_accidentals), len(negative_accidentals)))): positive_alteration_index, negative_alteration_index = ( nth_alteration, nth_alteration + 1, ) positive_accidental = ( EkmelilyTuningFileConverter._get_accidental_from_accidental_iterator( positive_accidentals_iterator ) ) negative_accidental = ( EkmelilyTuningFileConverter._get_accidental_from_accidental_iterator( negative_accidentals_iterator ) ) EkmelilyTuningFileConverter._process_accidental_pair( accidental_to_alteration_code_mapping, alteration_code_to_alteration_fraction_mapping, positive_accidental, positive_alteration_index, negative_accidental, negative_alteration_index, absolute_deviation_in_cents, ) nth_alteration += 2 return nth_alteration @staticmethod def _make_accidental_to_alteration_code_mapping_and_alteration_code_to_alteration_fraction_mapping( ekmelily_accidental_sequence: typing.Sequence[EkmelilyAccidental], ) -> tuple[dict[EkmelilyAccidental, str], dict[str, fractions.Fraction]]: accidentals_grouped_by_deviations_in_cents = ( EkmelilyTuningFileConverter._group_accidentals_by_deviations_in_cents( ekmelily_accidental_sequence ) ) accidental_to_alteration_code_mapping: dict[EkmelilyAccidental, str] = {} alteration_code_to_alteration_fraction_mapping: dict[ str, fractions.Fraction ] = {} nth_alteration = 0 for ( absolute_deviation_in_cents, accidental_group, ) in accidentals_grouped_by_deviations_in_cents: nth_alteration = EkmelilyTuningFileConverter._process_accidental_group( accidental_to_alteration_code_mapping, alteration_code_to_alteration_fraction_mapping, nth_alteration, accidental_group, absolute_deviation_in_cents, ) return ( accidental_to_alteration_code_mapping, alteration_code_to_alteration_fraction_mapping, ) # ###################################################################### # # private methods # # ###################################################################### # def _make_tuning_table(self) -> str: tuning_table_entries = [ " (-1 {})".format(" ".join((str(ratio) for ratio in self._global_scale))) ] for ( alteration_code, alteration_fraction, ) in self._alteration_code_to_alteration_fraction_mapping.items(): alteration = "({} . {})".format(alteration_code, alteration_fraction) tuning_table_entries.append(alteration) tuning_table = "ekmTuning = #'(\n{})".format("\n ".join(tuning_table_entries)) return tuning_table def _get_pitch_entry_from_accidental_and_diatonic_pitch( self, accidental: EkmelilyAccidental, nth_diatonic_pitch: int, diatonic_pitch: str, ) -> typing.Optional[str]: is_addable = True if accidental.available_diatonic_pitch_index_tuple is not None: is_addable = ( nth_diatonic_pitch in accidental.available_diatonic_pitch_index_tuple ) if is_addable: pitch_name = "{}{}".format(diatonic_pitch, accidental.accidental_name) alteration_code = self._accidental_to_alteration_code_mapping[accidental] pitch_entry = "({} {} . {})".format( pitch_name, nth_diatonic_pitch, alteration_code ) return pitch_entry return None def _make_languages_table(self) -> str: language_table_entries = [] for accidental in self._ekmelily_accidental_sequence: for nth_diatonic_pitch, diatonic_pitch in enumerate( music_parameters.constants.DIATONIC_PITCH_CLASS_CONTAINER ): pitch_entry = self._get_pitch_entry_from_accidental_and_diatonic_pitch( accidental, nth_diatonic_pitch, diatonic_pitch ) if pitch_entry is not None: language_table_entries.append(pitch_entry) # for now: only support english language languages_table = "ekmLanguages = #'(\n(english . (\n {})))".format( "\n ".join(language_table_entries) ) return languages_table def _make_notations_table(self) -> str: notations_table_entries: list[str] = [] for accidental in self._ekmelily_accidental_sequence: alteration_code = self._accidental_to_alteration_code_mapping[accidental] accidental_notation = "({} {})".format( alteration_code, " ".join(accidental.accidental_glyph_tuple) ) notations_table_entries.append(accidental_notation) notations_table = "ekmNotations = #'(\n(default .(\n {})))".format( "\n ".join(notations_table_entries) ) return notations_table # ###################################################################### # # public api # # ###################################################################### #
[docs] def convert(self): """Render tuning file to :attr:`path`.""" ekmelily_tuning_file = ( self._make_tuning_table(), self._make_languages_table(), self._make_notations_table(), r'\include "ekmel-main.ily"', ) ekmelily_tuning_file = "\n\n".join(ekmelily_tuning_file) with open(self._path, "w") as f: f.write(ekmelily_tuning_file)
[docs]class HEJIEkmelilyTuningFileConverter(EkmelilyTuningFileConverter): """Build Ekmelily tuning files for `Helmholtz-Ellis JI Pitch Notation <https://marsbat.space/pdfs/notation.pdf>`_. :param path: Path where the new Ekmelily tuning file shall be written. The suffix '.ily' is recommended, but not necessary. :type path: str :param prime_to_highest_allowed_exponent: Mapping of prime number to highest exponent that should occur. Take care not to add higher exponents than the HEJI Notation supports. See :const:`~mutwo.ekmelily_converters.configurations.DEFAULT_PRIME_TO_HIGHEST_ALLOWED_EXPONENT_DICT` for the default mapping. :type prime_to_highest_allowed_exponent: dict[int, int], optional :param reference_pitch: The reference pitch (1/1). Should be a diatonic pitch name (see :const:`~mutwo.parameters.music_parameters.constants.DIATONIC_PITCH_CLASS_CONTAINER`) in English nomenclature. For any other reference pitch than 'c', Lilyponds midi rendering for music_parameters with the diatonic pitch 'c' will be slightly out of tune (because the first value of `global_scale` always have to be 0). :type reference_pitch: str, optional :param prime_to_heji_accidental_name: Mapping of a prime number to a string which indicates the respective prime number in the resulting accidental name. See :const:`~mutwo.ekmelily_converters.configurations.DEFAULT_PRIME_TO_HEJI_ACCIDENTAL_NAME_DICT` for the default mapping. :type prime_to_heji_accidental_name: dict[int, str], optional :param otonality_indicator: String which indicates that the respective prime alteration is otonal. See :const:`~mutwo.ekmelily_converters.configurations.DEFAULT_OTONALITY_INDICATOR` for the default value. :type otonality_indicator: str, optional :param utonality_indicator: String which indicates that the respective prime alteration is utonal. See :const:`~mutwo.ekmelily_converters.configurations.DEFAULT_OTONALITY_INDICATOR` for the default value. :type utonality_indicator: str, optional :param exponent_to_exponent_indicator: Function to convert the exponent of a prime number to string which indicates the respective exponent. See :func:`~mutwo.ekmelily_converters.configurations.DEFAULT_EXPONENT_TO_EXPONENT_INDICATOR` for the default function. :type exponent_to_exponent_indicator: typing.Callable[[int], str], optional :param tempered_pitch_indicator: String which indicates that the respective accidental is tempered (12 EDO). See :const:`~mutwo.ekmelily_converters.configurations.DEFAULT_TEMPERED_PITCH_INDICATOR` for the default value. :type tempered_pitch_indicator: str, optional :param set_microtonal_tuning: If set to ``False`` the converter won't apply any microtonal music_parameters. In this case all chromatic music_parameters will return normal 12EDO music_parameters. Default to ``True``. :type set_microtonal_tuning: bool """ def __init__( self, path: str = None, prime_to_highest_allowed_exponent: typing.Optional[dict[int, int]] = None, reference_pitch: str = "c", prime_to_heji_accidental_name: typing.Optional[dict[int, str]] = None, otonality_indicator: str = None, utonality_indicator: str = None, exponent_to_exponent_indicator: typing.Callable[[int], str] = None, tempered_pitch_indicator: str = None, set_microtonal_tuning: bool = True, ): # set default values if path is None: path = "ekme-heji-ref-{}".format(reference_pitch) if not set_microtonal_tuning: path += "-not-tuned" path += ".ily" if prime_to_highest_allowed_exponent is None: prime_to_highest_allowed_exponent = ( ekmelily_converters.configurations.DEFAULT_PRIME_TO_HIGHEST_ALLOWED_EXPONENT_DICT ) if prime_to_heji_accidental_name is None: prime_to_heji_accidental_name = ( ekmelily_converters.configurations.DEFAULT_PRIME_TO_HEJI_ACCIDENTAL_NAME_DICT ) if otonality_indicator is None: otonality_indicator = ( ekmelily_converters.configurations.DEFAULT_OTONALITY_INDICATOR ) if utonality_indicator is None: utonality_indicator = ( ekmelily_converters.configurations.DEFAULT_UTONALITY_INDICATOR ) if exponent_to_exponent_indicator is None: exponent_to_exponent_indicator = ( ekmelily_converters.configurations.DEFAULT_EXPONENT_TO_EXPONENT_INDICATOR ) if tempered_pitch_indicator is None: tempered_pitch_indicator = ( ekmelily_converters.configurations.DEFAULT_TEMPERED_PITCH_INDICATOR ) difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches = HEJIEkmelilyTuningFileConverter._find_difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches( reference_pitch ) if set_microtonal_tuning: global_scale = HEJIEkmelilyTuningFileConverter._make_global_scale( difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches ) else: global_scale = None ekmelily_accidental_sequence = ( HEJIEkmelilyTuningFileConverter._make_ekmelily_accidental_sequence( difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches, prime_to_highest_allowed_exponent, prime_to_heji_accidental_name, otonality_indicator, utonality_indicator, exponent_to_exponent_indicator, tempered_pitch_indicator, set_microtonal_tuning, ) ) super().__init__(path, ekmelily_accidental_sequence, global_scale=global_scale) # ###################################################################### # # static methods # # ###################################################################### # @staticmethod def _find_difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches( reference_pitch: str, ) -> tuple[float, ...]: difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches = [] reference_pitch_index = ( music_parameters.constants.DIATONIC_PITCH_NAME_CYCLE_OF_FIFTH_TUPLE.index( reference_pitch ) ) for ( diatonic_pitch_name ) in music_parameters.constants.DIATONIC_PITCH_CLASS_CONTAINER: pitch_index = music_parameters.constants.DIATONIC_PITCH_NAME_CYCLE_OF_FIFTH_TUPLE.index( diatonic_pitch_name ) n_exponents_difference_from_reference = pitch_index - reference_pitch_index difference_from_tempered_diatonic_pitch = ( ekmelily_converters.constants.DIFFERENCE_BETWEEN_PYTHAGOREAN_AND_TEMPERED_FIFTH * n_exponents_difference_from_reference ) difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches.append( difference_from_tempered_diatonic_pitch ) return tuple(difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches) @staticmethod def _make_global_scale( difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches: tuple[ float, ... ], ) -> tuple[fractions.Fraction, ...]: new_global_scale = [] for ( nth_diatonic_pitch, difference_in_cents_from_tempered_pitch_class, ) in enumerate( difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches ): default_cents_for_diatonic_pitch = ( EkmelilyTuningFileConverter._alteration_fraction_to_deviation_in_cents( ekmelily_converters.configurations.DEFAULT_GLOBAL_SCALE[ nth_diatonic_pitch ] ) ) n_cents = ( default_cents_for_diatonic_pitch + difference_in_cents_from_tempered_pitch_class ) alteration_fraction = ( EkmelilyTuningFileConverter._deviation_in_cents_to_alteration_fraction( n_cents, max_denominator=1000 ) ) new_global_scale.append(alteration_fraction) return tuple(new_global_scale) @staticmethod def _make_pythagorean_accidentals( set_microtonal_tuning: bool, ) -> tuple[EkmelilyAccidental, ...]: accidentals = [] for ( alteration_name ) in ( ekmelily_converters.constants.PYTHAGOREAN_ACCIDENTAL_TO_CENT_DEVIATION_DICT.keys() ): glyph = ekmelily_converters.constants.PRIME_AND_EXPONENT_AND_TRADITIONAL_ACCIDENTAL_TO_ACCIDENTAL_GLYPH_DICT[ None, None, alteration_name ] if set_microtonal_tuning: cents_deviation = ekmelily_converters.constants.PYTHAGOREAN_ACCIDENTAL_TO_CENT_DEVIATION_DICT[ alteration_name ] else: cents_deviation = int( music_parameters.constants.ACCIDENTAL_NAME_TO_PITCH_CLASS_MODIFICATION_DICT[ alteration_name ] * 200 ) accidental = EkmelilyAccidental(alteration_name, (glyph,), cents_deviation) accidentals.append(accidental) return tuple(accidentals) @staticmethod def _process_prime( prime_to_heji_accidental_name: dict[int, str], pythagorean_accidental: str, prime: int, exponent: int, otonality_indicator: str, utonality_indicator: str, exponent_to_exponent_indicator: typing.Callable[[int], str], ) -> tuple[str, str, float]: glyph_key = ( (prime, exponent, pythagorean_accidental) if prime == 5 else (prime, exponent, None) ) glyph = ekmelily_converters.constants.PRIME_AND_EXPONENT_AND_TRADITIONAL_ACCIDENTAL_TO_ACCIDENTAL_GLYPH_DICT[ glyph_key ] accidental_name = "{}{}{}".format( (otonality_indicator, utonality_indicator)[exponent < 0], prime_to_heji_accidental_name[prime], exponent_to_exponent_indicator(abs(exponent) - 1), ) cents_deviation = music_parameters.JustIntonationPitch( music_parameters.configurations.DEFAULT_PRIME_TO_COMMA_DICT[prime].ratio ** exponent ).interval return accidental_name, glyph, cents_deviation @staticmethod def _make_higher_prime_accidental( pythagorean_accidental: str, pythagorean_accidental_cents_deviation: float, exponents: tuple[int, ...], prime_to_highest_allowed_exponent: dict[int, int], prime_to_heji_accidental_name: dict[int, str], otonality_indicator: str, utonality_indicator: str, exponent_to_exponent_indicator: typing.Callable[[int], str], set_microtonal_tuning: bool, ) -> EkmelilyAccidental: accidental_parts = ["{}".format(pythagorean_accidental)] cents_deviation = float(pythagorean_accidental_cents_deviation) glyphs = [] for prime, exponent in zip( prime_to_highest_allowed_exponent.keys(), exponents, ): if exponent != 0: ( accidental_change, glyph, cents_deviation_change, ) = HEJIEkmelilyTuningFileConverter._process_prime( prime_to_heji_accidental_name, pythagorean_accidental, prime, exponent, otonality_indicator, utonality_indicator, exponent_to_exponent_indicator, ) accidental_parts.append(accidental_change) glyphs.append(glyph) cents_deviation += cents_deviation_change # add traditional accidentals (sharp, flat, etc.) if no syntonic # comma is available (if there is any syntonic comma there is # already the necessary pythagorean accidental) if exponents[0] == 0: glyphs.insert( 0, ekmelily_converters.constants.PRIME_AND_EXPONENT_AND_TRADITIONAL_ACCIDENTAL_TO_ACCIDENTAL_GLYPH_DICT[ (None, None, pythagorean_accidental) ], ) # start with highest primes glyphs.reverse() accidental_name = "".join(accidental_parts) cents_deviation = round(cents_deviation, 4) new_accidental = EkmelilyAccidental( accidental_name, tuple(glyphs), cents_deviation ) return new_accidental @staticmethod def _make_accidentals_for_higher_primes( prime_to_highest_allowed_exponent: dict[int, int], prime_to_heji_accidental_name: dict[int, str], otonality_indicator: str, utonality_indicator: str, exponent_to_exponent_indicator: typing.Callable[[int], str], set_microtonal_tuning: bool, ) -> tuple[EkmelilyAccidental, ...]: allowed_exponents = tuple( tuple(range(-maxima_exponent, maxima_exponent + 1)) for _, maxima_exponent in sorted( prime_to_highest_allowed_exponent.items(), key=lambda prime_to_maxima_exponent: prime_to_maxima_exponent[0], ) ) accidentals = [] for exponents in itertools.product(*allowed_exponents): if any(tuple(exp != 0 for exp in exponents)): for ( pythagorean_accidental, pythagorean_accidental_cents_deviation, ) in ( ekmelily_converters.constants.PYTHAGOREAN_ACCIDENTAL_TO_CENT_DEVIATION_DICT.items() ): accidental = ( HEJIEkmelilyTuningFileConverter._make_higher_prime_accidental( pythagorean_accidental, pythagorean_accidental_cents_deviation, exponents, prime_to_highest_allowed_exponent, prime_to_heji_accidental_name, otonality_indicator, utonality_indicator, exponent_to_exponent_indicator, set_microtonal_tuning, ) ) accidentals.append(accidental) return tuple(accidentals) @staticmethod def _make_tempered_accidentals( difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches: tuple[ float, ... ], tempered_pitch_indicator: str, ) -> tuple[EkmelilyAccidental, ...]: accidentals = [] for ( nth_diatonic_pitch, difference_in_cents_from_tempered_pitch_class, ) in enumerate( difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches, ): for ( tempered_accidental, accidental_glyph, ) in ( ekmelily_converters.constants.TEMPERED_ACCIDENTAL_TO_ACCIDENTAL_GLYPH_DICT.items() ): accidental_name = "{}{}".format( tempered_accidental, tempered_pitch_indicator ) deviation_in_cents = ( ekmelily_converters.constants.TEMPERED_ACCIDENTAL_TO_CENT_DEVIATION_DICT[ tempered_accidental ] - difference_in_cents_from_tempered_pitch_class ) accidental = EkmelilyAccidental( accidental_name, (accidental_glyph,), deviation_in_cents, available_diatonic_pitch_index_tuple=(nth_diatonic_pitch,), ) accidentals.append(accidental) return tuple(accidentals) @staticmethod def _make_ekmelily_accidental_sequence( difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches: tuple[ float, ... ], prime_to_highest_allowed_exponent: dict[int, int], prime_to_heji_accidental_name: dict[int, str], otonality_indicator: str, utonality_indicator: str, exponent_to_exponent_indicator: typing.Callable[[int], str], tempered_pitch_indicator: str, set_microtonal_tuning: bool, ) -> tuple[EkmelilyAccidental, ...]: pythagorean_accidentals = ( HEJIEkmelilyTuningFileConverter._make_pythagorean_accidentals( set_microtonal_tuning ) ) accidentals_for_higher_primes = ( HEJIEkmelilyTuningFileConverter._make_accidentals_for_higher_primes( prime_to_highest_allowed_exponent, prime_to_heji_accidental_name, otonality_indicator, utonality_indicator, exponent_to_exponent_indicator, set_microtonal_tuning, ) ) tempered_accidentals = ( HEJIEkmelilyTuningFileConverter._make_tempered_accidentals( difference_in_cents_from_tempered_pitch_class_for_diatonic_pitches, tempered_pitch_indicator, ) ) return ( pythagorean_accidentals + accidentals_for_higher_primes + tempered_accidentals )