from __future__ import annotations
import collections
import dataclasses
import functools
import typing
import quicktions as fractions
from mutwo import core_utilities
from mutwo import music_parameters
__all__ = (
"NaturalHarmonic",
"String",
"UnpitchedInstrument",
"ContinuousPitchedInstrument",
"DiscreetPitchedInstrument",
"ContinuousPitchedStringInstrument",
"DiscreetPitchedStringInstrument",
"Orchestration",
"OrchestrationMixin",
)
[docs]class NaturalHarmonic(music_parameters.Partial):
"""Model the natural harmonic of a :class:`String`.
:param index: The partials index.
:type index: int
:param string: The :class:`String` on which this harmonic
shall be played.
:type string: String
"""
[docs] @dataclasses.dataclass(frozen=True)
class Node(object):
"""A position on a string which, if touched, produces a harmonic.
:param interval: The `interval` remarks the position on the string
where the player needs to press in order to produce a harmonic.
The interval needs to be added to the :class:`String` tuning
in order to gain a pitch. The position where this pitch is played
normally on the given string is the node position where the
harmonic can be produced.
:type interval: music_parameters.abc.PitchInterval
:param natural_harmonic: The natural harmonic which is produced when
pressing at the nodes position.
:type natural_harmonic: NaturalHarmonic
:param string: The string on which to play the node,
:type string: String
"""
interval: music_parameters.abc.PitchInterval
natural_harmonic: NaturalHarmonic
string: String
@functools.cached_property
def pitch(self) -> music_parameters.abc.Pitch:
"""At which position to press the string to produce the harmonic."""
return self.string.tuning_original + self.interval
def __init__(self, index: int, string: String):
self._string = string
super().__init__(index, tonality=True) # We can't play undertones
@property
def string(self) -> String:
"""The :class:`String` on which the harmonic is played."""
return self._string
@functools.cached_property
def pitch(self) -> music_parameters.JustIntonationPitch:
"""The resulting sounding pitch of a :class:`NaturalHarmonic`.
**Example:**
>>> from mutwo import music_parameters
>>> string = music_parameters.String(0, music_parameters.WesternPitch("g", 3))
>>> natural_harmonic = music_parameters.NaturalHarmonic(3, string)
>>> natural_harmonic.pitch
WesternPitch('d', 5)
"""
return self.string.tuning + self.interval
@functools.cached_property
def node_tuple(self) -> tuple[NaturalHarmonic.Node, ...]:
"""Find all :class:`NaturalHarmonic.Node` on which harmonic is playable.
**Example:**
>>> from mutwo import music_parameters
>>> natural_harmonic = music_parameters.NaturalHarmonic(
... 2,
... music_parameters.String(0, music_parameters.WesternPitch('g', 3)),
... )
>>> natural_harmonic.node_tuple
(NaturalHarmonic.Node(interval=JustIntonationPitch('2/1'), natural_harmonic=NaturalHarmonic(index=2, tonality=True), string=String(0, WesternPitch('g', 3))),)
"""
node_list = []
for node_index in range(1, self.index):
ratio = fractions.Fraction(self.index, node_index)
if ratio.numerator == self.index:
node_list.append(
self.Node(
music_parameters.JustIntonationPitch(ratio), self, self.string
)
)
return tuple(reversed(node_list))
[docs]@dataclasses.dataclass(frozen=True)
class String(object):
""":class:`String` represents a string of an instrument.
:param index: The index of a :class:`String`. This is important
in order to differentiate how far two strings are from
each other.
:type index: int
:param tuning: The pitch to which the string is tuned to.
:type tuning: music_parameters.abc.Pitch
:param tuning_original: If the standard tuning of a string
differs from its current tuning (e.g. if a scordatura
is used) this parameter can be set to the standard tuning.
This is useful in case one wants to notate the fingering
of a harmonic and not the sounding result. The ``pitch``
attribute of :class:`NaturalHarmonic.Node` uses `tuning_original`
for calculation instead of `tuning. If `tuning_original`
is ``None`` it is auto-set to `tuning`. Default to ``None``.
:type tuning_original: typing.Optional[music_parameters.abc.Pitch]
:param max_natural_harmonic_index: Although we can imagine infinite
number of natural harmonics, in the real world it's not so
easy to play higher flageolet. It's therefore a good idea
to denote a limit of the highest natural harmonic. This
limit defines the highest :class:`NaturalHarmonic` which is
returned when accessing :class:`String`s
``natural_harmonic_tuple`` property. No matter what is
set to ``max_natural_harmonic_index``, you can still get
infinitely high :class:`NaturalHarmonic` of a :class:`String`
with its ``index_to_natural_harmonic`` method. Default to 6.
:type max_natural_harmonic_index: int
**Example:**
>>> from mutwo import music_parameters
>>> g_string = music_parameters.String(0, music_parameters.WesternPitch('g', 3))
>>> g_string
String(WesternPitch('g', 3))
>>> retuned_g_string = music_parameters.String(
... 0,
... music_parameters.WesternPitch('g', 3),
... tuning_original=music_parameters.JustIntonationPitch('8/11'),
... )
>>> retuned_g_string
String(WesternPitch('g', 3))
"""
index: int
tuning: music_parameters.abc.Pitch
tuning_original: typing.Optional[music_parameters.abc.Pitch] = None
max_natural_harmonic_index: int = 6
def __post_init__(self):
object.__setattr__(self, "tuning_original", self.tuning_original or self.tuning)
object.__setattr__(self, "_index_to_natural_harmonic", {})
def __repr__(self) -> str:
return f"{type(self).__name__}({self.tuning})"
@functools.cached_property
def natural_harmonic_tuple(self) -> tuple[NaturalHarmonic, ...]:
"""All :class:`NaturalHarmonic` with index from 2 until ``max_natural_harmonic_index``."""
return tuple(
(
self.index_to_natural_harmonic(i)
for i in range(2, self.max_natural_harmonic_index + 1)
)
)
[docs] def index_to_natural_harmonic(self, natural_harmonic_index: int) -> NaturalHarmonic:
"""Find natural harmonic with given partial index.
:param natural_harmonic_index: The partial index; e.g. 2 is the first
overtone (an octave above the root), 3 the second overtone (octave
plus fifth), etc.
:type natural_harmonic_index: int
**Example:**
>>> from mutwo import music_parameters
>>> g_string = music_parameters.String(
... 0, music_parameters.WesternPitch('g', 3)
... )
>>> g_string.index_to_natural_harmonic(5)
NaturalHarmonic(index=5, tonality=True)
"""
try:
return self._index_to_natural_harmonic[natural_harmonic_index]
except KeyError:
h = self._index_to_natural_harmonic[
natural_harmonic_index
] = NaturalHarmonic(natural_harmonic_index, self)
return h
[docs]@dataclasses.dataclass(frozen=True)
class StringInstrumentMixin(object):
"""Mixin to model instrument with strings.
:param string_tuple: All strings which the instrument has.
:type string_tuple: tuple[String, ...]
This class provides additional attributes and methods for
an instrument with strings.
The class itself is not an :class:`Instrument`, but only a mixin.
In order to use it with an :class:`Instrument`, you need to
inherit from both and explicitly call '__init__' of both
super classes inside your '__init__' method.
It's recommended to simply use builtin :class:`ContinuousPitchedStringInstrument`
or :class:`DiscreetPitchedStringInstrument`.
Harmonic pitches have no effect on the `__contains__`
method of an `Instrument`. This means the expression
``pitch in instrument``
ignores all pitches returned by the `harmonic_pitch_tuple`
property. This is because it is assumed that the user needs
an explicit additional test to check if a pitch can be played
by harmonics (because often we may find ourselves in a situation
where we don't want a harmonic).
"""
string_tuple: tuple[String, ...]
@functools.cached_property
def harmonic_pitch_tuple(self) -> tuple[music_parameters.abc.Pitch, ...]:
"""List all pitches which can be played with natural harmonics.
This tuple depends on ``max_natural_harmonic_index`` attribute of
:class:`String`.
"""
pitch_list = []
for s in self.string_tuple:
for h in s.natural_harmonic_tuple:
if (p := h.pitch) not in pitch_list:
pitch_list.append(p)
return tuple(sorted(pitch_list))
@functools.cached_property
def harmonic_pitch_ambitus(self) -> music_parameters.abc.PitchAmbitus:
"""Get flageolet :class:`music_parameters.abc.PitchAmbitus`."""
hp_tuple = self.harmonic_pitch_tuple
return music_parameters.OctaveAmbitus(hp_tuple[0], hp_tuple[-1])
[docs] def get_harmonic_pitch_variant_tuple(
self,
pitch: music_parameters.abc.Pitch,
period: typing.Optional[music_parameters.abc.PitchInterval] = None,
tolerance: music_parameters.abc.PitchInterval = music_parameters.DirectPitchInterval(
2
),
) -> tuple[music_parameters.abc.Pitch, ...]:
"""Find natural harmonic pitch variants (in all registers) of ``pitch``
:param pitch: The pitch which variants shall be found.
:type pitch: music_parameters.abc.Pitch
:param period: The repeating period (usually an octave). If the
period is set to `None` the function will fallback to the
objects method :meth:`~mutwo.music_parameters.abc.PitchAmbitus.pitch_to_period`.
Default to `None`.
:type period: typing.Optional[music_parameters.abc.PitchInterval]
:param tolerance: Because harmonics are just tuned they may
differ from tempered pitches. In order to still fetch harmonics
the ``tolerance`` parameter can help. This is a
:class:`music_parameters.abc.PitchInterval`: if the difference
is within the intervals range, it is still considered as equal
and the harmonic is returned. Default to `DirectPitchInterval`
with 2 cents.
:type tolerance: music_parameters.abc.PitchInterval
"""
t_interval = abs(tolerance.interval)
g = self.harmonic_pitch_ambitus.get_pitch_variant_tuple
h_t = self.harmonic_pitch_tuple
return tuple(
p
for p in g(pitch, period)
if any(
[abs(p.get_pitch_interval(h_p).interval) < t_interval for h_p in h_t]
)
)
[docs] def pitch_to_natural_harmonic_tuple(
self,
pitch: music_parameters.abc.Pitch,
tolerance: music_parameters.abc.PitchInterval = music_parameters.DirectPitchInterval(
2
),
) -> tuple[NaturalHarmonic, ...]:
"""Find all :class:`NaturalHarmonic` which produces ``pitch``.
:param pitch: The pitch which shall be equal to the returned
harmonics pitch.
:type pitch: music_parameters.abc.Pitch
:param tolerance: Because harmonics are just tuned they may
differ from tempered pitches. In order to still fetch harmonics
the ``tolerance`` parameter can help. This is a
:class:`music_parameters.abc.PitchInterval`: if the difference
is within the intervals range, it is still considered as equal
and the harmonic is returned. Default to `DirectPitchInterval`
with 2 cents.
:type tolerance: music_parameters.abc.PitchInterval
"""
t_interval = abs(tolerance.interval)
natural_harmonic_list = []
for s in self.string_tuple:
for h in s.natural_harmonic_tuple:
if abs((h.pitch.get_pitch_interval(pitch)).interval) < t_interval:
natural_harmonic_list.append(h)
return tuple(natural_harmonic_list)
[docs]class UnpitchedInstrument(music_parameters.abc.Instrument):
"""Model a musical instruments without any clear pitches.
**Example:**
>>> from mutwo import music_parameters
>>> bass_drum = music_parameters.UnpitchedInstrument("bass drum", "bd.")
"""
@property
def is_pitched(self) -> bool:
return False
[docs]class ContinuousPitchedInstrument(music_parameters.abc.PitchedInstrument):
"""Model a musical instrument with continuous pitches (e.g. not fretted).
:param pitch_ambitus: The pitch ambitus of the instrument.
:type pitch_ambitus: music_parameters.abc.PitchAmbitus
:param name: The name of the instrument.
:type name: str
:param short_name: The abbreviation of the instrument.
If set to ``None`` it will be the same like `name`.
Default to ``None``.
:type short_name: typing.Optional[str]
:param pitch_count_range: Set how many simultaneous
pitches the instrument can play. Default to
`ranges.Range(1, 2)`, which means that the instrument
is monophonic.
:type pitch_count_range: ranges.Range
:param transposition_pitch_interval: Some instruments are
written with a transposition (so sounding pitch and
written pitch differs). This parameter can be used
to set the transposition interval in case sounding
and written differs. The `transposition_pitch_interval`
is added to the sounding pitches in order to reach the
written pitches. If set to ``None`` this will be
set to `DirectPitchInterval(0)` which is no transposition.
Default to ``None``.
:type transposition_pitch_interval: typing.Optional[PitchInterval]
**Example:**
>>> from mutwo import music_parameters
>>> vl = music_parameters.ContinuousPitchedInstrument(
... music_parameters.OctaveAmbitus(
... music_parameters.WesternPitch('g', 3),
... music_parameters.WesternPitch('e', 7),
... ),
... "violin",
... "vl.",
... )
"""
def __init__(
self, pitch_ambitus: music_parameters.abc.PitchAmbitus, *args, **kwargs
):
super().__init__(*args, **kwargs)
self._pitch_ambitus = pitch_ambitus
def __contains__(self, pitch: typing.Any) -> bool:
"""Test if pitch is playable by instrument.
:param pitch: Pitch to test.
:type pitch: typing.Any
**Example:**
>>> from mutwo import music_parameters
>>> music_parameters.WesternPitch('c', 1) in music_parameters.BfClarinet()
False
"""
return pitch in self.pitch_ambitus
@property
def pitch_ambitus(self) -> music_parameters.abc.PitchAmbitus:
return self._pitch_ambitus
[docs] def get_pitch_variant_tuple(
self,
pitch: music_parameters.abc.Pitch,
period: typing.Optional[music_parameters.abc.PitchInterval] = None,
) -> tuple[music_parameters.abc.Pitch, ...]:
return self.pitch_ambitus.get_pitch_variant_tuple(pitch, period)
[docs]class DiscreetPitchedInstrument(music_parameters.abc.PitchedInstrument):
"""Model a musical instrument with discreet pitches (e.g. fretted).
:param pitch_tuple: A tuple of all playable pitches of the instrument.
:type pitch_tuple: tuple[music_parameters.abc.Pitch, ...]
:param name: The name of the instrument.
:type name: str
:param short_name: The abbreviation of the instrument.
If set to ``None`` it will be the same like `name`.
Default to ``None``.
:type short_name: typing.Optional[str]
:param pitch_count_range: Set how many simultaneous
pitches the instrument can play. Default to
`ranges.Range(1, 2)`, which means that the instrument
is monophonic.
:type pitch_count_range: ranges.Range
:param transposition_pitch_interval: Some instruments are
written with a transposition (so sounding pitch and
written pitch differs). This parameter can be used
to set the transposition interval in case sounding
and written differs. The `transposition_pitch_interval`
is added to the sounding pitches in order to reach the
written pitches. If set to ``None`` this will be
set to `DirectPitchInterval(0)` which is no transposition.
Default to ``None``.
:type transposition_pitch_interval: typing.Optional[PitchInterval]
**Example:**
>>> from mutwo import music_parameters
>>> pentatonic_idiophone = music_parameters.DiscreetPitchedInstrument(
... (
... music_parameters.JustIntonationPitch('1/1'),
... music_parameters.JustIntonationPitch('9/8'),
... music_parameters.JustIntonationPitch('5/4'),
... music_parameters.JustIntonationPitch('3/2'),
... music_parameters.JustIntonationPitch('7/4'),
... ),
... "idiophone",
... "id.",
... )
"""
def __init__(
self, pitch_tuple: tuple[music_parameters.abc.Pitch, ...], *args, **kwargs
):
super().__init__(*args, **kwargs)
self._pitch_tuple = tuple(sorted(core_utilities.uniqify_sequence(pitch_tuple)))
self._pitch_ambitus = music_parameters.OctaveAmbitus(
pitch_tuple[0], pitch_tuple[-1]
)
def __contains__(self, pitch: typing.Any) -> bool:
return pitch in self.pitch_tuple
@property
def pitch_ambitus(self) -> music_parameters.abc.PitchAmbitus:
return self._pitch_ambitus
@property
def pitch_tuple(self) -> tuple[music_parameters.abc.Pitch, ...]:
return self._pitch_tuple
[docs] def get_pitch_variant_tuple(
self,
pitch: music_parameters.abc.Pitch,
period: typing.Optional[music_parameters.abc.PitchInterval] = None,
) -> tuple[music_parameters.abc.Pitch, ...]:
return tuple(
filter(
lambda p: p in self.pitch_tuple,
self.pitch_ambitus.get_pitch_variant_tuple(pitch, period),
)
)
[docs]class ContinuousPitchedStringInstrument(
ContinuousPitchedInstrument, StringInstrumentMixin
):
def __init__(self, *args, string_tuple: tuple[String, ...], **kwargs):
ContinuousPitchedInstrument.__init__(self, *args, **kwargs)
StringInstrumentMixin.__init__(self, string_tuple)
[docs]class DiscreetPitchedStringInstrument(DiscreetPitchedInstrument, StringInstrumentMixin):
def __init__(self, *args, string_tuple: tuple[String, ...], **kwargs):
DiscreetPitchedInstrument.__init__(self, *args, **kwargs)
StringInstrumentMixin.__init__(self, string_tuple)
[docs]class OrchestrationMixin(object):
"""Helper base class from which adhoc created `Orchestration` object inherits.
This class has some methods which extend the functionality of
Pythons builtin `collections.namedtuple`.
"""
[docs] def get_subset(self, *instrument_name: str):
r"""Return a sub-Orchestration of `Orchestration`.
:param \*instrument_name: Name of the instrument which should be
included in the subset.
:type \*instrument_name: str
This method doesn't change the original `Orchestration`
but creates a new object.
**Example:**
>>> from mutwo import music_parameters
>>> orch = music_parameters.Orchestration(
... oboe0=music_parameters.Oboe(),
... oboe1=music_parameters.Oboe(),
... oboe2=music_parameters.Oboe(),
... )
>>> orch.get_subset('oboe0', 'oboe2')
Orchestration(oboe0=Oboe(name='oboe', short_name='ob.', pitch_count_range=Range[1, 2), transposition_pitch_interval=DirectPitchInterval(interval = 0)), oboe2=Oboe(name='oboe', short_name='ob.', pitch_count_range=Range[1, 2), transposition_pitch_interval=DirectPitchInterval(interval = 0)))
"""
return Orchestration(**{name: getattr(self, name) for name in instrument_name})
[docs]def Orchestration(**instrument_name_to_instrument: music_parameters.abc.Instrument):
r"""Create a name space for the instrumentation of a composition.
:param \**instrument_name_to_instrument: Pick any instrument name
and map it to a specific instrument.
:type \**instrument_name_to_instrument: music_parameters.abc.Instrument
This returns an adapted `namedtuple instance <https://docs.python.org/3/library/collections.html#collections.namedtuple>`_
where the keys are the instrument names and the values are the
:class:`mutwo.music_parameters.abc.Instrument` objects.
The returned `Orchestration` object has some additional methods.
They are documented in the :class`OrchestrationMixin`.
**Example:**
>>> from mutwo import music_parameters
>>> music_parameters.Orchestration(
... oboe0=music_parameters.Oboe(),
... oboe1=music_parameters.Oboe(),
... )
Orchestration(oboe0=Oboe(name='oboe', short_name='ob.', pitch_count_range=Range[1, 2), transposition_pitch_interval=DirectPitchInterval(interval = 0)), oboe1=Oboe(name='oboe', short_name='ob.', pitch_count_range=Range[1, 2), transposition_pitch_interval=DirectPitchInterval(interval = 0)))
"""
instrument_name_tuple, instrument_tuple = tuple([]), tuple([])
if instrument_name_to_instrument:
instrument_name_tuple, instrument_tuple = zip(
*instrument_name_to_instrument.items()
)
return type(
"Orchestration",
(
collections.namedtuple("Orchestration", instrument_name_tuple),
OrchestrationMixin,
),
{},
)(*instrument_tuple)
# Helper
def _setdefault(kwargs: dict, default_dict: dict) -> dict:
for key, value in default_dict.items():
kwargs.setdefault(key, value)
return kwargs