"""Load midi files to mutwo"""
import abc
import copy
import typing
import mido
import numpy as np
try:
import quicktions as fractions
except ImportError:
import fractions
from mutwo import core_converters
from mutwo import core_events
from mutwo import core_parameters
from mutwo import core_utilities
from mutwo import midi_converters
from mutwo import music_converters
from mutwo import music_parameters
__all__ = (
"PitchBendingNumberToPitchInterval",
"PitchBendingNumberToDirectPitchInterval",
"MidiPitchToMutwoPitch",
"MidiPitchToDirectPitch",
"MidiPitchToMutwoMidiPitch",
"MidiVelocityToMutwoVolume",
"MidiVelocityToWesternVolume",
"MidiFileToEvent",
)
[docs]class PitchBendingNumberToPitchInterval(core_converters.abc.Converter):
"""Convert midi pitch bend number to :class:`mutwo.music_parameters.abc.PitchInterval`.
:param maximum_pitch_bend_deviation: sets the maximum pitch bending range in cents.
This value depends on the particular used software synthesizer and its settings,
because it is up to the respective synthesizer how to interpret the pitch
bending messages. By default mutwo sets the value to 200 cents which
seems to be the most common interpretation among different manufacturers.
:type maximum_pitch_bend_deviation: int
"""
def __init__(self, maximum_pitch_bend_deviation: typing.Optional[float] = None):
if maximum_pitch_bend_deviation is None:
maximum_pitch_bend_deviation = (
midi_converters.configurations.DEFAULT_MAXIMUM_PITCH_BEND_DEVIATION_IN_CENTS
)
self._maximum_pitch_bend_deviation = maximum_pitch_bend_deviation
[docs] @abc.abstractmethod
def convert(
self,
pitch_bending_number_to_convert: midi_converters.constants.PitchBend,
) -> music_parameters.abc.PitchInterval:
...
[docs]class PitchBendingNumberToDirectPitchInterval(PitchBendingNumberToPitchInterval):
"""Convert midi pitch bend number to :class:`mutwo.music_parameters.DirectPitchInterval`."""
[docs] def convert(
self,
pitch_bending_number_to_convert: midi_converters.constants.PitchBend,
) -> music_parameters.DirectPitchInterval:
"""Convert pitch bending number to :class:`mutwo.music_parameters.DirectPitchInterval`
:param pitch_bending_number_to_convert: The pitch bending number
which shall be converted.
:type pitch_bending_number_to_convert: midi_converters.constants.PitchBend
"""
cent_deviation = core_utilities.scale(
pitch_bending_number_to_convert,
-midi_converters.constants.NEUTRAL_PITCH_BEND,
midi_converters.constants.NEUTRAL_PITCH_BEND,
-self._maximum_pitch_bend_deviation,
self._maximum_pitch_bend_deviation,
)
return music_parameters.DirectPitchInterval(float(cent_deviation))
[docs]class MidiPitchToMutwoPitch(core_converters.abc.Converter):
"""Convert midi pitch to :class:`mutwo.music_parameters.abc.Pitch`.
:param pitch_bending_number_to_pitch_interval: A callable object which
transforms a pitch bending number (integer) to a
:class:`mutwo.music_parameters.abc.PitchInterval`. Default to
:class:`PitchBendingNumberToDirectPitchInterval`.
:type pitch_bending_number_to_pitch_interval: typing.Callable[[midi_converters.constants.PitchBend], music_parameters.abc.PitchInterval]
"""
def __init__(
self,
pitch_bending_number_to_pitch_interval: typing.Callable[
[midi_converters.constants.PitchBend], music_parameters.abc.PitchInterval
] = PitchBendingNumberToDirectPitchInterval(),
):
self._pitch_bending_number_to_pitch_interval = (
pitch_bending_number_to_pitch_interval
)
[docs] @abc.abstractmethod
def convert(
self, midi_pitch_to_convert: midi_converters.constants.MidiPitch
) -> music_parameters.abc.Pitch:
...
[docs]class MidiPitchToDirectPitch(MidiPitchToMutwoPitch):
[docs] def convert(
self, midi_pitch_to_convert: midi_converters.constants.MidiPitch
) -> music_parameters.DirectPitch:
midi_note, pitch_bend = midi_pitch_to_convert
frequency = music_parameters.constants.MIDI_PITCH_FREQUENCY_TUPLE[midi_note]
direct_pitch = music_parameters.DirectPitch(frequency)
pitch_interval = self._pitch_bending_number_to_pitch_interval(pitch_bend)
return direct_pitch.add(pitch_interval)
[docs]class MidiPitchToMutwoMidiPitch(MidiPitchToMutwoPitch):
[docs] def convert(
self, midi_pitch_to_convert: midi_converters.constants.MidiPitch
) -> music_parameters.MidiPitch:
midi_note, pitch_bend = midi_pitch_to_convert
midi_pitch = music_parameters.MidiPitch(midi_note)
pitch_interval = self._pitch_bending_number_to_pitch_interval(pitch_bend)
return midi_pitch.add(pitch_interval)
[docs]class MidiVelocityToMutwoVolume(core_converters.abc.Converter):
"""Convert midi velocity (integer) to :class:`mutwo.music_parameters.abc.Volume`."""
[docs] @abc.abstractmethod
def convert(
self, midi_velocity: midi_converters.constants.MidiVelocity
) -> music_parameters.abc.Volume:
...
[docs]class MidiVelocityToWesternVolume(MidiVelocityToMutwoVolume):
[docs] def convert(
self, midi_velocity_to_convert: midi_converters.constants.MidiVelocity
) -> music_parameters.abc.Volume:
"""Convert midi velocity to :class:`mutwo.music_parameters.WesternVolume`
:param midi_velocity_to_convert: The velocity which shall be converted.
:type midi_velocity_to_convert: midi_converters.constants.MidiVelocity
**Example:**
>>> from mutwo import midi_converters
>>> midi_converters.MidiVelocityToWesternVolume().convert(127)
WesternVolume(fffff)
>>> midi_converters.MidiVelocityToWesternVolume().convert(0)
WesternVolume(ppppp)
"""
standard_dynamic_indicator_count = len(
music_parameters.constants.STANDARD_DYNAMIC_INDICATOR
)
dynamic_indicator_index = round(
core_utilities.scale(
midi_velocity_to_convert,
music_parameters.constants.MINIMUM_VELOCITY,
music_parameters.constants.MAXIMUM_VELOCITY,
0,
standard_dynamic_indicator_count - 1,
)
)
dynamic_indicator = music_parameters.constants.STANDARD_DYNAMIC_INDICATOR[
int(dynamic_indicator_index)
]
return music_parameters.WesternVolume(dynamic_indicator)
MessageTypeToMidiMessageList = dict[str, list[mido.Message | mido.MetaMessage]]
NotePair = tuple[mido.Message, mido.Message]
NotePairTuple = tuple[NotePair, ...]
StartAndStopTupleToNotePairList = dict[tuple[int, int], list[NotePair]]
[docs]class MidiFileToEvent(core_converters.abc.Converter):
"""Convert a midi file to a mutwo event.
:param mutwo_parameter_tuple_to_chronon: A callable which converts a
tuple of mutwo parameters (duration, pitch list, volume) to a
:class:`mutwo.core_events.SimpleEvent`. In default state mutwo
generates a :class:`mutwo.music_events.NoteLike`.
:type mutwo_parameter_tuple_to_chronon: typing.Callable[[tuple[core_constants.DurationType, music_parameters.abc.Pitch, music_parameters.abc.Volume]], core_events.SimpleEvent]
:param midi_pitch_to_mutwo_pitch: Callable object which converts
midi pitch (integer) to a :class:`mutwo.music_parameters.abc.Pitch`.
Default to :class:`MidiPitchToMutwoMidiPitch`.
:type midi_pitch_to_mutwo_pitch: typing.Callable[[midi_converters.constants.MidiPitch], music_parameters.abc.Pitch]
:param midi_velocity_to_mutwo_volume: Callable object which converts
midi velocity (integer) to a :class:`mutwo.music_parameters.abc.Voume`.
Default to :class:`MidiPitchToWesternVolume`.
:type midi_velocity_to_mutwo_volume: typing.Callable[[midi_converters.constants.MidiVelocity], music_parameters.abc.Volume]
**Warning:**
This is an unstable early version of the converter.
Expect bugs when using it!
**Disclaimer:**
This conversion is incomplete: Not all information from a
midi file will be used. In its current state the converter
only takes into account midi notes (pitch, velocity and duration)
and ignores all other midi messages.
"""
def __init__(
self,
mutwo_parameter_dict_to_chronon: typing.Callable[
[core_converters.MutwoParameterDict],
core_events.SimpleEvent,
] = music_converters.MutwoParameterDictToNoteLike(),
midi_pitch_to_mutwo_pitch: typing.Callable[
[midi_converters.constants.MidiPitch], music_parameters.abc.Pitch
] = MidiPitchToMutwoMidiPitch(),
midi_velocity_to_mutwo_volume: typing.Callable[
[midi_converters.constants.MidiVelocity], music_parameters.abc.Volume
] = MidiVelocityToWesternVolume(),
):
self._logger = core_utilities.get_cls_logger(type(self))
self._mutwo_parameter_dict_to_chronon = (
mutwo_parameter_dict_to_chronon
)
self._midi_pitch_to_mutwo_pitch = midi_pitch_to_mutwo_pitch
self._midi_velocity_to_mutwo_volume = midi_velocity_to_mutwo_volume
# ###################################################################### #
# static methods #
# ###################################################################### #
@staticmethod
def _get_message_type_to_midi_message_list(
midi_file_to_convert: mido.MidiFile,
) -> MessageTypeToMidiMessageList:
message_type_to_midi_message_list = {}
for midi_track in midi_file_to_convert.tracks:
absolute_tick = 0
for midi_message in midi_track:
message_type = midi_message.type
if message_type not in message_type_to_midi_message_list:
message_type_to_midi_message_list.update({message_type: []})
absolute_tick += midi_message.time
midi_message_with_absolute_tick = copy.deepcopy(midi_message)
midi_message_with_absolute_tick.time = int(absolute_tick)
message_type_to_midi_message_list[message_type].append(
midi_message_with_absolute_tick
)
for midi_message_list in message_type_to_midi_message_list.values():
midi_message_list.sort(key=lambda midi_message: midi_message.time)
return message_type_to_midi_message_list
@staticmethod
def _note_pair_tuple_to_start_and_stop_tuple_to_note_pair_list(
note_pair_tuple: NotePairTuple,
) -> StartAndStopTupleToNotePairList:
start_and_stop_tuple_to_note_pair_list = {}
for note_pair in note_pair_tuple:
start_and_stop_tuple = tuple(
note_message.time for note_message in note_pair # type: ignore
)
if start_and_stop_tuple not in start_and_stop_tuple_to_note_pair_list:
start_and_stop_tuple_to_note_pair_list.update(
{start_and_stop_tuple: []}
)
start_and_stop_tuple_to_note_pair_list[start_and_stop_tuple].append(
note_pair
)
return start_and_stop_tuple_to_note_pair_list
@staticmethod
def _add_chronon_to_consecution(
consecution: core_events.Consecution,
start: int,
chronon: core_events.SimpleEvent,
):
difference = start - consecution.duration.duration
if difference > 0:
rest = core_events.SimpleEvent(difference)
consecution.append(rest)
consecution.append(chronon)
@staticmethod
def _tick_to_duration(
tick: int, ticks_per_beat: int
) -> core_parameters.DirectDuration:
return core_parameters.DirectDuration(fractions.Fraction(tick, ticks_per_beat))
# ###################################################################### #
# private methods #
# ###################################################################### #
def _get_note_off_partner(
self,
note_on_message: mido.Message | mido.MetaMessage,
note_off_message_list: list[mido.Message | mido.MetaMessage],
) -> typing.Optional[mido.Message]:
def is_valid_note_off_message(
note_off_message: mido.Message | mido.MetaMessage,
) -> bool:
test_list = [
note_off_message.time >= note_on_message.time, # type: ignore
note_on_message.note == note_off_message.note, # type: ignore
note_on_message.channel == note_off_message.channel, # type: ignore
]
return all(test_list)
try:
note_off_message = next(
filter(is_valid_note_off_message, note_off_message_list)
)
assert isinstance(note_off_message, mido.Message)
except StopIteration:
self._logger.warning(
"Invalid midi file: "
"Found note on message without any suitable "
"note off message partner. The note on message is: "
f"'{note_on_message}'."
)
note_off_message = None
return note_off_message
def _get_note_pair_tuple(
self,
message_type_to_midi_message_list: MessageTypeToMidiMessageList,
) -> NotePairTuple:
try:
note_on_message_list = message_type_to_midi_message_list["note_on"]
except KeyError:
self._logger.debug("No 'note_on' messages were found!")
return tuple([])
try:
note_off_message_list = copy.deepcopy(
message_type_to_midi_message_list["note_off"]
)
except KeyError:
self._logger.warning(
"No 'note_off' messages were found! "
"This is strange, because 'note_on' messages could be found. "
"Maybe you have a midi file which doesn't use 'note_off'"
"messages but only 'note_on' messages with velocity=0?"
" This is currently not supported, see"
" also https://github.com/mutwo-org/mutwo.midi/issues/4."
)
return tuple([])
note_pair_list = []
for note_on_message in note_on_message_list:
note_off_message = self._get_note_off_partner(
note_on_message, note_off_message_list
)
if note_off_message is not None:
note_pair = (note_on_message, note_off_message)
self._logger.debug(
f"Found note_pair (on: {note_on_message}, off: {note_off_message})"
)
note_pair_list.append(note_pair)
del note_off_message_list[note_off_message_list.index(note_off_message)]
note_pair_list.sort(key=lambda note_pair: note_pair[0].time)
return tuple(note_pair_list)
def _note_pair_list_to_chronon(
self, note_pair_list: list[NotePair], ticks_per_beat: int
) -> core_events.SimpleEvent:
midi_pitch_list = []
velocity_list = []
for note_pair in note_pair_list:
note_on, _ = note_pair
# TODO(take pitch bend into account!)
midi_pitch_list.append((note_on.note, 0)) # type: ignore
velocity_list.append(note_on.velocity) # type: ignore
average_velocity = int(np.average(velocity_list))
mutwo_volume = self._midi_velocity_to_mutwo_volume(average_velocity)
mutwo_pitch_list = [
self._midi_pitch_to_mutwo_pitch(midi_pitch)
for midi_pitch in midi_pitch_list
]
note_on, note_off = note_pair_list[0]
tick = note_off.time - note_on.time # type: ignore
duration = MidiFileToEvent._tick_to_duration(tick, ticks_per_beat)
# Use default values defined in configurations modules to ensure
# stability in case user changes the values.
mutwo_parameter_dict = {
core_converters.configurations.DEFAULT_DURATION_TO_SEARCH_NAME: duration,
music_converters.configurations.DEFAULT_PITCH_LIST_TO_SEARCH_NAME: mutwo_pitch_list,
music_converters.configurations.DEFAULT_VOLUME_TO_SEARCH_NAME: mutwo_volume,
}
chronon = self._mutwo_parameter_dict_to_chronon(mutwo_parameter_dict)
self._logger.debug(
f"Midi data -> Mutwo data -> SimpleEvent:\n\t"
f"Midi data: (tick={tick},velocity_list={velocity_list},midi_pitch_list={midi_pitch_list})\n\t"
f"Mutwo data: {mutwo_parameter_dict}\n\t"
f"SimpleEvent: {chronon}"
)
return chronon
def _note_pair_tuple_to_simultaneous_event(
self, note_pair_tuple: NotePairTuple, ticks_per_beat: int
) -> core_events.Concurrence[
core_events.Consecution[core_events.SimpleEvent]
]:
simultaneous_event = core_events.Concurrence([])
start_and_stop_tuple_to_note_pair_list = (
MidiFileToEvent._note_pair_tuple_to_start_and_stop_tuple_to_note_pair_list(
note_pair_tuple
)
)
for start_and_stop_tuple in sorted(
start_and_stop_tuple_to_note_pair_list.keys(),
key=lambda start_and_stop_tuple: start_and_stop_tuple[0],
):
start_tick, _ = start_and_stop_tuple
start = self._tick_to_duration(start_tick, ticks_per_beat)
note_pair_list = start_and_stop_tuple_to_note_pair_list[
start_and_stop_tuple
]
chronon = self._note_pair_list_to_chronon(
note_pair_list, ticks_per_beat
)
is_added = False
for consecution in simultaneous_event:
duration = consecution.duration
difference = start - duration
if difference >= 0:
self._add_chronon_to_consecution(
consecution, start, chronon
)
is_added = True
break
if not is_added:
simultaneous_event.append(core_events.Consecution([]))
self._add_chronon_to_consecution(
simultaneous_event[-1], start, chronon
)
return simultaneous_event
def _note_pair_tuple_and_set_tempo_message_list_to_simultaneous_event(
self,
note_pair_tuple: NotePairTuple,
set_tempo_message_list: list[mido.Message | mido.MetaMessage],
ticks_per_beat: int,
) -> core_events.Concurrence[
core_events.Consecution[core_events.SimpleEvent]
]:
simultaneous_event = self._note_pair_tuple_to_simultaneous_event(
note_pair_tuple, ticks_per_beat
)
# TODO(apply tempo messages)
return simultaneous_event
def _midi_file_to_mutwo_event(
self, midi_file_to_convert: mido.MidiFile
) -> core_events.abc.Event:
ticks_per_beat = midi_file_to_convert.ticks_per_beat
message_type_to_midi_message_list = (
MidiFileToEvent._get_message_type_to_midi_message_list(midi_file_to_convert)
)
note_pair_tuple = self._get_note_pair_tuple(message_type_to_midi_message_list)
try:
set_tempo_message_list = message_type_to_midi_message_list["set_tempo"]
except KeyError:
set_tempo_message_list = []
return self._note_pair_tuple_and_set_tempo_message_list_to_simultaneous_event(
note_pair_tuple, set_tempo_message_list, ticks_per_beat
)
# ###################################################################### #
# public methods #
# ###################################################################### #
[docs] def convert(
self, midi_file_path_or_mido_midi_file: str | mido.MidiFile
) -> core_events.abc.Event:
"""Convert midi file to mutwo event.
:param midi_file_path_or_mido_midi_file: The midi file which shall
be converted. Can either be a file path or a :class:`MidiFile`
object from the `mido <https://github.com/mido/mido>`_ package.
:type midi_file_path_or_mido_midi_file: str | mido.MidiFile
"""
if isinstance(midi_file_path_or_mido_midi_file, str):
midi_file = mido.MidiFile(midi_file_path_or_mido_midi_file)
elif isinstance(midi_file_path_or_mido_midi_file, mido.MidiFile):
midi_file = midi_file_path_or_mido_midi_file
else:
raise TypeError(
(
f"Found '{midi_file_path_or_mido_midi_file}' of"
"unsupported type"
f"'{type(midi_file_path_or_mido_midi_file)}' for"
"parameter 'midi_file_path_or_mido_midi_file'! "
"Please enter either a file name (str) or a MidiFile"
" object (from the mido package)."
)
)
return self._midi_file_to_mutwo_event(midi_file)