Source code for mutwo.isis_converters.isis

"""Render singing signals from mutwo data via `ISiS <https://forum.ircam.fr/projects/detail/isis/>`_.

ISiS (IRCAM Singing Synthesis) is a `"command line application for singing
synthesis that can be used to generate singing signals by means of synthesizing
them from melody and lyrics."
<https://isis-documentation.readthedocs.io/en/latest/Intro.html#the-isis-command-line>`_.
"""

import configparser
import os
import typing

from mutwo import core_converters
from mutwo import core_constants
from mutwo import core_events
from mutwo import isis_converters
from mutwo import isis_utilities
from mutwo import music_parameters

__all__ = ("EventToIsisScore", "EventToSingingSynthesis")

ConvertableEventUnion = typing.Union[
    core_events.SimpleEvent,
    core_events.SequentialEvent[core_events.SimpleEvent],
]
ExtractedDataDict = dict[
    # duration, consonants, vowel, pitch, volume
    str,
    typing.Any,
]


[docs]class EventToIsisScore(core_converters.abc.EventConverter): """Class to convert mutwo events to a `ISiS score file. <https://isis-documentation.readthedocs.io/en/latest/score.html>`_ :param simple_event_to_pitch: Function to extract an instance of :class:`mutwo.music_parameters.abc.Pitch` from a simple event. :param simple_event_to_volume: :param simple_event_to_vowel: :param simple_event_to_consonant_tuple: :param is_simple_event_rest: :param tempo: Tempo in beats per minute (BPM). Defaults to 60. :param global_transposition: global transposition in midi numbers. Defaults to 0. :param n_events_per_line: How many events the score shall contain per line. Defaults to 5. """ _extracted_data_dict_rest = { "consonant_tuple": tuple([]), "vowel": "_", "pitch": music_parameters.WesternPitch( "c", -1, concert_pitch=440, concert_pitch_octave=4, concert_pitch_pitch_class=9, ), "volume": music_parameters.DirectVolume(0), } def __init__( self, simple_event_to_pitch: typing.Callable[ [core_events.SimpleEvent], music_parameters.abc.Pitch ] = lambda simple_event: simple_event.pitch_list[ # type: ignore 0 ], simple_event_to_volume: typing.Callable[ [core_events.SimpleEvent], music_parameters.abc.Volume ] = lambda simple_event: simple_event.volume, # type: ignore simple_event_to_vowel: typing.Callable[ [core_events.SimpleEvent], str ] = lambda simple_event: simple_event.vowel, # type: ignore simple_event_to_consonant_tuple: typing.Callable[ [core_events.SimpleEvent], tuple[str, ...] ] = lambda simple_event: simple_event.consonant_tuple, # type: ignore is_simple_event_rest: typing.Callable[ [core_events.SimpleEvent], bool ] = lambda simple_event: not ( hasattr(simple_event, "pitch_list") and simple_event.pitch_list # type: ignore ), tempo: core_constants.Real = 60, global_transposition: int = 0, default_sentence_loudness: typing.Union[core_constants.Real, None] = None, n_events_per_line: int = 5, ): self._tempo = tempo self._global_transposition = global_transposition self._default_sentence_loudness = default_sentence_loudness self._n_events_per_line = n_events_per_line self._is_simple_event_rest = is_simple_event_rest self._extraction_function_dict = { "consonant_tuple": simple_event_to_consonant_tuple, "vowel": simple_event_to_vowel, "pitch": simple_event_to_pitch, "volume": simple_event_to_volume, } # ###################################################################### # # private methods # # ###################################################################### # def _add_lyric_section( self, score_config_file: configparser.ConfigParser, extracted_data_dict_per_event_tuple: tuple[ExtractedDataDict, ...], ): score_config_file[isis_converters.constants.SECTION_LYRIC_NAME] = { "xsampa": " ".join( map( lambda extracted_data: " ".join( extracted_data["consonant_tuple"] + (extracted_data["vowel"],) ), extracted_data_dict_per_event_tuple, ) ) } def _add_score_section( self, score_config_file: configparser.ConfigParser, extracted_data_dict_per_event_tuple: tuple[ExtractedDataDict, ...], ): score_section = { "globalTransposition": self._global_transposition, "tempo": self._tempo, } for parameter_name, lambda_function in ( ( "midiNotes", lambda extracted_data: str(extracted_data["pitch"].midi_pitch_number), ), ( "rhythm", lambda extracted_data: str(extracted_data["duration"]), ), ( "loud_accents", lambda extracted_data: str(extracted_data["volume"].amplitude), ), ): score_section.update( { parameter_name: ", ".join( map(lambda_function, extracted_data_dict_per_event_tuple) ) } ) score_config_file[isis_converters.constants.SECTION_SCORE_NAME] = score_section def _convert_simple_event( self, simple_event_to_convert: core_events.SimpleEvent, _: core_constants.DurationType, ) -> tuple[ExtractedDataDict]: duration = simple_event_to_convert.duration.duration_in_floats extracted_data_dict: dict[str, typing.Any] = {"duration": duration} for ( extracted_data_name, extraction_function, ) in self._extraction_function_dict.items(): try: extracted_information = extraction_function(simple_event_to_convert) except AttributeError: return (dict(duration=duration, **self._extracted_data_dict_rest),) extracted_data_dict.update({extracted_data_name: extracted_information}) return (extracted_data_dict,) def _convert_simultaneous_event( self, _: core_events.SimultaneousEvent, __: core_constants.DurationType, ): raise isis_utilities.MonophonicSynthesizerError() # ###################################################################### # # public api # # ###################################################################### #
[docs] def convert(self, event_to_convert: ConvertableEventUnion, path: str) -> None: """Render ISiS score file from the passed event. :param event_to_convert: The event that shall be rendered to a ISiS score file. :type event_to_convert: typing.Union[core_events.SimpleEvent, core_events.SequentialEvent[core_events.SimpleEvent]] :param path: where to write the ISiS score file :type path: str **Example:** >>> from mutwo import core_events >>> from mutwo import music_events >>> from mutwo import music_parameters >>> from mutwo import isis_converters >>> notes = core_events.SequentialEvent( >>> [ >>> music_events.NoteLike(music_parameters.WesternPitch(pitch_name), 0.5, 0.5) >>> for pitch_name in 'c f d g'.split(' ') >>> ] >>> ) >>> for consonants, vowel, note in zip([[], [], ['t'], []], ['a', 'o', 'e', 'a'], notes): >>> note.vowel = vowel >>> note.consonants = consonants >>> event_to_isis_score = isis.EventToIsisScore('my_singing_score') >>> event_to_isis_score.convert(notes) """ # ISiS can't handle two sequental rests, therefore we have to tie two # adjacent rests together. if isinstance(event_to_convert, core_events.abc.ComplexEvent): event_to_convert = event_to_convert.tie_by( lambda event0, event1: self._is_simple_event_rest(event0) and self._is_simple_event_rest(event1), event_type_to_examine=core_events.SimpleEvent, mutate=False, # type: ignore ) extracted_data_dict_per_event_tuple = self._convert_event(event_to_convert, 0) # ":" delimiter is used in ISiS example score files # see https://isis-documentation.readthedocs.io/en/latest/score.html#score-example score_config_file = configparser.ConfigParser(delimiters=":") self._add_lyric_section(score_config_file, extracted_data_dict_per_event_tuple) self._add_score_section(score_config_file, extracted_data_dict_per_event_tuple) with open(path, "w") as f: score_config_file.write(f)
[docs]class EventToSingingSynthesis(core_converters.abc.Converter): """Generate audio files with `ISiS <https://forum.ircam.fr/projects/detail/isis/>`_. :param isis_score_converter: The :class:`EventToIsisScore` that shall be used to render the ISiS score file from a mutwo event. :param *flag: Flag that shall be added when calling ISiS. Several of the supported ISiS flags can be found in :mod:`mutwo.isis_converters.constants`. :param remove_score_file: Set to True if :class:`EventToSingingSynthesis` shall remove the ISiS score file after rendering. Defaults to False. :param isis_executable_path: The path to the ISiS executable (binary file). If not specified the value of :const:`mutwo.isis_converters.configurations.DEFAULT_ISIS_EXECUTABLE_PATH` will be used. **Disclaimer:** Before using the :class:`EventToSingingSynthesis`, make sure ISiS has been correctly installed on your system. """ def __init__( self, isis_score_converter: EventToIsisScore, *flag: str, remove_score_file: bool = False, isis_executable_path: typing.Optional[str] = None, ): if not isis_executable_path: isis_executable_path = ( isis_converters.configurations.DEFAULT_ISIS_EXECUTABLE_PATH ) self.flags = flag self.isis_score_converter = isis_score_converter self.remove_score_file = remove_score_file self._isis_executable_path = isis_executable_path
[docs] def convert( self, event_to_convert: ConvertableEventUnion, path: str, score_path: typing.Optional[str] = None, ) -> None: """Render sound file via ISiS from mutwo event. :param event_to_convert: The event that shall be rendered. :param path: The path / filename of the resulting sound file :param score_path: The path where the score file shall be written to. **Disclaimer:** Before using the :class:`EventToSingingSynthesis`, make sure `ISiS <https://forum.ircam.fr/projects/detail/isis/>`_ has been correctly installed on your system. """ if not score_path: score_path = f"{path.split('.')[0]}.isis_score.cfg" self.isis_score_converter.convert(event_to_convert, score_path) command = "{} -m {} -o {}".format( self._isis_executable_path, score_path, path, ) for flag in self.flags: command += " {} ".format(flag) os.system(command) if self.remove_score_file: os.remove(score_path)