Source code for mutwo.converters.frontends.isis

"""Render signing 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 os
import typing

from mutwo import events

from mutwo import converters
from mutwo.converters.frontends import isis_constants
from mutwo import parameters
from mutwo.utilities import constants

__all__ = ("IsisScoreConverter", "IsisConverter")

ConvertableEvents = typing.Union[
    events.basic.SimpleEvent, events.basic.SequentialEvent[events.basic.SimpleEvent],
]
ExtractedData = typing.Tuple[
    # duration, consonants, vowel, pitch, volume
    parameters.abc.DurationType,
    typing.Tuple[str, ...],
    str,
    parameters.abc.Pitch,
    parameters.abc.Volume,
]


[docs]class IsisScoreConverter(converters.abc.EventConverter): """Class to convert mutwo events to a `ISiS score file. <https://isis-documentation.readthedocs.io/en/latest/score.html>`_ :param path: where to write the ISiS score file :param simple_event_to_pitch: Function to extract an instance of :class:`mutwo.parameters.abc.Pitch` from a simple event. :param simple_event_to_volume: :param simple_event_to_vowel: :param simple_event_to_consonants: :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. """ def __init__( self, path: str, simple_event_to_pitch: typing.Callable[ [events.basic.SimpleEvent], parameters.abc.Pitch ] = lambda simple_event: simple_event.pitch_or_pitches[ 0 ], # type: ignore simple_event_to_volume: typing.Callable[ [events.basic.SimpleEvent], parameters.abc.Volume ] = lambda simple_event: simple_event.volume, # type: ignore simple_event_to_vowel: typing.Callable[ [events.basic.SimpleEvent], str ] = lambda simple_event: simple_event.vowel, # type: ignore simple_event_to_consonants: typing.Callable[ [events.basic.SimpleEvent], typing.Tuple[str, ...] ] = lambda simple_event: simple_event.consonants, # type: ignore is_simple_event_rest: typing.Callable[ [events.basic.SimpleEvent], bool ] = lambda simple_event: not ( hasattr(simple_event, "pitch_or_pitches") and simple_event.pitch_or_pitches ), tempo: constants.Real = 60, global_transposition: int = 0, default_sentence_loudness: typing.Union[constants.Real, None] = None, n_events_per_line: int = 5, ): self.path = path 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_functions = ( simple_event_to_consonants, simple_event_to_vowel, simple_event_to_pitch, simple_event_to_volume, ) # ###################################################################### # # private methods # # ###################################################################### # def _make_key_from_extracted_data_per_event( self, key_name: str, extracted_data_per_event: typing.Tuple[ExtractedData], key: typing.Callable[[ExtractedData], typing.Tuple[str, ...]], seperate_with_comma: bool = True, ) -> str: objects_per_line: typing.List[typing.List[str]] = [[]] for nth_event, extracted_data in enumerate(extracted_data_per_event): objects_per_line[-1].extend(key(extracted_data)) if nth_event % self._n_events_per_line == 0: objects_per_line.append([]) object_join_string = ", " if seperate_with_comma else " " lines = [object_join_string.join(line) for line in objects_per_line if line] line_join_string = ",\n" if seperate_with_comma else "\n" line_join_string = "{}{}".format(line_join_string, " " * (len(key_name) + 2)) return "{}: {}".format(key_name, line_join_string.join(lines)) def _make_lyrics_section_from_extracted_data_per_event( self, extracted_data_per_event: typing.Tuple[ExtractedData], ) -> str: xsampa = self._make_key_from_extracted_data_per_event( "xsampa", extracted_data_per_event, lambda extracted_data: extracted_data[1] + (extracted_data[2],), False, ) lyric_section = "[lyrics]\n{}".format(xsampa) return lyric_section def _make_score_section_from_extracted_data_per_event( self, extracted_data_per_event: typing.Tuple[ExtractedData], ) -> str: midi_notes = self._make_key_from_extracted_data_per_event( "midiNotes", extracted_data_per_event, lambda extracted_data: (str(extracted_data[3].midi_pitch_number),), ) rhythm = self._make_key_from_extracted_data_per_event( "rhythm", extracted_data_per_event, lambda extracted_data: (str(extracted_data[0]),), ) loud_accents = self._make_key_from_extracted_data_per_event( "loud_accents", extracted_data_per_event, lambda extracted_data: (str(extracted_data[4].amplitude),), ) score_section = ( "[score]\n{}\nglobalTransposition: {}\n{}\n{}\ntempo: {}".format( midi_notes, self._global_transposition, rhythm, loud_accents, self._tempo, ) ) return score_section def _convert_simple_event( self, simple_event_to_convert: events.basic.SimpleEvent, absolute_entry_delay: parameters.abc.DurationType, ) -> typing.Tuple[ExtractedData]: duration = simple_event_to_convert.duration extracted_data: typing.List[object] = [duration] for extraction_function in self._extraction_functions: try: extracted_information = extraction_function(simple_event_to_convert) except AttributeError: return ( ( duration, tuple([]), "_", parameters.pitches.WesternPitch( "c", -1, concert_pitch=440, concert_pitch_octave=4, concert_pitch_pitch_class=9, ), parameters.volumes.DirectVolume(0), ), ) extracted_data.append(extracted_information) return (tuple(extracted_data),) # type: ignore def _convert_simultaneous_event( self, simultaneous_event_to_convert: events.basic.SimultaneousEvent, absolute_entry_delay: parameters.abc.DurationType, ): message = ( "Can't convert instance of SimultaneousEvent to ISiS Score. ISiS is only a" " monophonic synthesizer and can't read multiple simultaneous voices!" ) raise NotImplementedError(message) # ###################################################################### # # public api # # ###################################################################### #
[docs] def convert(self, event_to_convert: ConvertableEvents) -> None: """Render ISiS score file from the passed event. :param event_to_convert: The event that shall be rendered to a ISiS score file. **Example:** >>> from mutwo.events import events.basic, music >>> from mutwo.parameters import pitches >>> from mutwo.converters.frontends import isis >>> notes = events.basic.SequentialEvent( >>> [ >>> music.NoteLike(pitches.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 >>> isis_score_converter = isis.IsisScoreConverter('my_singing_score') >>> isis_score_converter.convert(notes) """ if isinstance(event_to_convert, 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=events.basic.SimpleEvent, mutate=False, ) extracted_data_per_event = self._convert_event(event_to_convert, 0) lyrics_section = self._make_lyrics_section_from_extracted_data_per_event( extracted_data_per_event # type: ignore ) score_section = self._make_score_section_from_extracted_data_per_event( extracted_data_per_event # type: ignore ) with open(self.path, "w") as f: f.write("\n\n".join([lyrics_section, score_section]))
[docs]class IsisConverter(converters.abc.Converter): """Generate audio files with `ISiS <https://forum.ircam.fr/projects/detail/isis/>`_. :param path: where to write the sound file :param isis_score_converter: The :class:`IsisScoreConverter` 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.converters.frontends.isis_constants`. :param remove_score_file: Set to True if :class:`IsisConverter` shall remove the ISiS score file after rendering. Defaults to False. **Disclaimer:** Before using the :class:`IsisConverter`, make sure ISiS has been correctly installed on your system. """ def __init__( self, path: str, isis_score_converter: IsisScoreConverter, *flag: str, remove_score_file: bool = False ): self.flags = flag self.path = path self.isis_score_converter = isis_score_converter self.remove_score_file = remove_score_file
[docs] def convert(self, event_to_convert: ConvertableEvents) -> None: """Render sound file via ISiS from mutwo event. :param event_to_convert: The event that shall be rendered. **Disclaimer:** Before using the :class:`IsisConverter`, make sure `ISiS <https://forum.ircam.fr/projects/detail/isis/>`_ has been correctly installed on your system. """ self.isis_score_converter.convert(event_to_convert) command = "{} -m {} -o {}".format( isis_constants.ISIS_PATH, self.isis_score_converter.path, self.path, ) for flag in self.flags: command += " {} ".format(flag) os.system(command) if self.remove_score_file: os.remove(self.isis_score_converter.path)