"""Submodule for the parameter pitch.
'Pitch' is defined as any object that knows a :attr:`frequency` attribute.
The two major modern tuning systems Just intonation and Equal-divided-octave
are supported by the :class:`JustIntonationPitch` and :class:`EqualDividedOctavePitch` classes.
For using Western nomenclature (e.g. c, d, e, f, ...) :mod:`mutwo` offers the
:class:`WesternPitch` class (which inherits from :class:`EqualDividedOctavePitch`).
For a straight frequency-based approach one may use :class:`DirectPitch`.
If desired the default concert pitch can be adjusted after importing :mod:`mutwo`:
>>> from mutwo.parameters import pitches_constants
>>> pitches_constants.DEFAULT_CONCERT_PITCH = 443
All pitch objects with a concert pitch attribute that become initialised after
overriding the default concert pitch value will by default use the new
overridden default concert pitch value.
"""
import collections
import copy
import functools
import math
import numbers
import operator
import typing
import warnings
import primesieve # type: ignore
from primesieve import numpy as primesieve_numpy
try:
import quicktions as fractions # type: ignore
except ImportError:
import fractions # type: ignore
from mutwo.utilities import constants
from mutwo.utilities import decorators
from mutwo.utilities import prime_factors
from mutwo.utilities import tools
from mutwo import parameters
__all__ = (
"DirectPitch",
"JustIntonationPitch",
"EqualDividedOctavePitch",
"WesternPitch",
)
ConcertPitch = typing.Union[constants.Real, parameters.abc.Pitch]
PitchClassOrPitchClassName = typing.Union[constants.Real, str]
[docs]class DirectPitch(parameters.abc.Pitch):
"""A simple pitch class that gets directly initialised by its frequency.
:param frequency: The frequency of the ``DirectPitch`` object.
May be used when a converter class needs a pitch object, but there is
no need or desire for a complex abstraction of the respective pitch
(that classes like ``JustIntonationPitch`` or ``WesternPitch`` offer).
**Example:**
>>> from mutwo.parameters import pitches
>>> my_pitch = pitches.DirectPitch(440)
"""
def __init__(self, frequency: constants.Real):
self._frequency = float(frequency)
@property
def frequency(self) -> float:
"""The frequency of the pitch."""
return self._frequency
def __repr__(self) -> str:
return "DirectPitch(frequency = {})".format(self.frequency)
[docs]class JustIntonationPitch(parameters.abc.Pitch):
"""Pitch that is defined by a frequency ratio and a reference pitch.
:param ratio_or_exponents: The frequency ratio of the ``JustIntonationPitch``.
This can either be (A) a string that indicates the frequency ratio (for
instance: "1/1", "3/2", "9/2", etc.), or (B) a ``fractions.Fraction``
(or ``quicktions.Fraction``) object that indicates the frequency ratio
(for instance: ``fractions.Fraction(3, 2)``, ``fractions.Fraction(7, 4)``)
or (C) an Iterable that is filled with integer that represents the exponents
of the respective prime numbers of the decomposed frequency ratio. The prime
numbers are rising and start with 2. Therefore the tuple ``(2, 0, -1)``
would return the frequency ratio ``4/5`` because
``(2 ** 2) * (3 ** 0) * (5 ** -1) = 4/5``.
:param concert_pitch: The reference pitch of the tuning system (the pitch for a
frequency ratio of 1/1). Can either be another ``Pitch`` object or any number
to indicate a particular frequency in Hertz.
The resulting frequency is calculated by multiplying the frequency ratio
with the respective reference pitch.
>>> from mutwo.parameters import pitches
>>> # 3 different variations of initialising the same pitch
>>> pitches.JustIntonationPitch('3/2')
>>> import fractions
>>> pitches.JustIntonationPitch(fractions.Fraction(3, 2))
>>> pitches.JustIntonationPitch((-1, 1))
>>> # using a different concert pitch
>>> pitches.JustIntonationPitch('7/5', concert_pitch=432)
"""
def __init__(
self,
ratio_or_exponents: typing.Union[
str, fractions.Fraction, typing.Iterable[int]
] = "1/1",
concert_pitch: ConcertPitch = None,
):
if concert_pitch is None:
concert_pitch = parameters.pitches_constants.DEFAULT_CONCERT_PITCH
self.exponents = self._translate_ratio_or_fractions_argument_to_exponents(
ratio_or_exponents
)
self.concert_pitch = concert_pitch # type: ignore
# ###################################################################### #
# static private methods #
# ###################################################################### #
@staticmethod
def _adjust_exponent_lengths(exponents0: tuple, exponents1: tuple) -> tuple:
r"""Adjust two exponents, e.g. make their length equal.
The length of the longer JustIntonationPitch is the reference.
Arguments:
* exponents0: first exponents to adjust
* exponents1: second exponents to adjust
>>> v0 = (1, 0, -1)
>>> v1 = (1,)
>>> v0_adjusted, v1_adjusted = JustIntonationPitch._adjust_exponent_lengths(v0, v1)
>>> v0_adjusted
(1, 0, -1)
>>> v1_adjusted
(1, 0, 0)
"""
length0 = len(exponents0)
length1 = len(exponents1)
if length0 > length1:
return exponents0, exponents1 + (0,) * (length0 - length1)
else:
return exponents0 + (0,) * (length1 - length0), exponents1
@staticmethod
def _adjust_ratio(ratio: fractions.Fraction, border: int) -> fractions.Fraction:
r"""Multiply or divide a fractions.Fraction - Object with the border,
until it is equal or bigger than 1 and smaller than border.
Arguments:
* rratio: The Ratio, which shall be adjusted
* border
>>> ratio0 = fractions.Fraction(1, 3)
>>> ratio1 = fractions.Fraction(8, 3)
>>> border = 2
>>> JustIntonationPitch._adjust_ratio(ratio0, border)
fractions.Fraction(4, 3)
>>> JustIntonationPitch._adjust_ratio(ratio1, border)
fractions.Fraction(4, 3)
"""
if border > 1:
while ratio >= border:
ratio /= border
while ratio < 1:
ratio *= border
return ratio
@staticmethod
def _adjust_float(float_: float, border: int) -> float:
r"""Multiply float with border, until it is <= 1 and > than border.
Arguments:
* float_: The float, which shall be adjusted
* border
>>> float0 = 0.5
>>> float1 = 2
>>> border = 2
>>> JustIntonationPitch._adjust_float(float0, border)
1
>>> JustIntonationPitch._adjust_float(float1, border)
1
"""
if border > 1:
while float_ > border:
try:
float_ /= border
except OverflowError:
float_ //= border
while float_ < 1:
float_ *= border
return float_
@staticmethod
def _adjust_exponents(exponents: tuple, primes: tuple, border: int) -> tuple:
r"""Adjust a exponents and its primes depending on the border.
Arguments:
* exponents: The exponents, which shall be adjusted
* primes: Its corresponding primes
* border
>>> exponents0 = (1,)
>>> primes0 = (3,)
>>> border = 2
>>> JustIntonationPitch._adjust_exponents(exponents0, primes0, border)
((-1, 1), (2, 3))
""" # TODO(DOCSTRING) Make proper description what actually happens
if exponents:
if border > 1:
multiplied = functools.reduce(
operator.mul, (p ** e for p, e in zip(primes, exponents))
)
res = math.log(border / multiplied, border)
if res < 0:
res -= 1
res = int(res)
primes = (border,) + primes
exponents = (res,) + exponents
return exponents, primes
return (1,), (1,)
@staticmethod
def _discard_nulls(iterable: typing.Iterable[int]) -> typing.Tuple[int, ...]:
r"""Discard all zeros after the last not 0 - element of an arbitary iterable.
Return a tuple.
Arguments:
* iterable: the iterable, whose 0 - elements shall
be discarded
>>> tuple0 = (1, 0, 2, 3, 0, 0, 0)
>>> ls = [1, 3, 5, 0, 0, 0, 2, 0]
>>> JustIntonationPitch._discard_nulls(tuple0)
(1, 0, 2, 3)
>>> JustIntonationPitch._discard_nulls(ls)
(1, 3, 5, 0, 0, 0, 2)
"""
iterable = tuple(iterable)
c = 0
for i in reversed(iterable):
if i != 0:
break
c += 1
if c != 0:
return iterable[:-c]
return iterable
@staticmethod
def _exponents_to_pair(exponents: tuple, primes: tuple) -> tuple:
r"""Transform a JustIntonationPitch to a (numerator, denominator) - pair.
Arguments are:
* JustIntonationPitch -> The exponents of prime numbers
* primes -> the referring prime numbers
>>> myJustIntonationPitch0 = (1, 0, -1)
>>> myJustIntonationPitch1 = (0, 2, 0)
>>> myVal0 = (2, 3, 5)
>>> myVal1 = (3, 5, 7)
>>> JustIntonationPitch._exponents_to_pair(myJustIntonationPitch0, myVal0)
(2, 5)
>>> JustIntonationPitch._exponents_to_pair(myJustIntonationPitch0, myVal1)
(3, 7)
>>> JustIntonationPitch._exponents_to_pair(myJustIntonationPitch1, myVal1)
(25, 1)
"""
numerator = 1
denominator = 1
for number, exponent in zip(primes, exponents):
if exponent > 0:
numerator *= pow(number, exponent)
elif exponent < 0:
denominator *= pow(number, -exponent)
return numerator, denominator
@staticmethod
def _exponents_to_ratio(exponents: tuple, primes: tuple) -> fractions.Fraction:
r"""Transform a JustIntonationPitch to a fractions.Fraction - Object
(if installed to a quicktions.fractions.Fraction - Object,
otherwise to a fractions.fractions.Fraction - Object).
Arguments are:
* JustIntonationPitch -> The exponents of prime numbers
* primes -> the referring prime numbers for the underlying
._exponents - Argument (see JustIntonationPitch._exponents).
>>> myJustIntonationPitch0 = (1, 0, -1)
>>> myPrimes= (2, 3, 5)
>>> JustIntonationPitch._exponents_to_ratio(myJustIntonationPitch0, myPrimes)
2/5
"""
numerator, denominator = JustIntonationPitch._exponents_to_pair(
exponents, primes
)
return JustIntonationPitch._adjust_ratio(
fractions.Fraction(numerator, denominator), 1
)
@staticmethod
def _exponents_to_float(exponents: tuple, primes: tuple) -> float:
r"""Transform a JustIntonationPitch to a float.
Arguments are:
* JustIntonationPitch -> The exponents of prime numbers
* primes -> the referring prime numbers for the underlying
._exponents - Argument (see JustIntonationPitch._exponents).
* primes-shift -> how many prime numbers shall be skipped
(see JustIntonationPitch.primes_shift)
>>> myJustIntonationPitch0 = (1, 0, -1)
>>> myJustIntonationPitch1 = (0, 2, 0)
>>> myPrimes = (2, 3, 5)
>>> JustIntonationPitch._exponents_to_float(myJustIntonationPitch0, myPrimes)
0.4
"""
numerator, denominator = JustIntonationPitch._exponents_to_pair(
exponents, primes
)
try:
return numerator / denominator
except OverflowError:
return numerator // denominator
@staticmethod
def _ratio_to_exponents(ratio: fractions.Fraction) -> tuple:
r"""Transform a fractions.Fraction - Object to a vector of exponents.
Arguments are:
* ratio -> The fractions.Fraction, which shall be transformed
>>> try:
>>> from quicktions import fractions.Fraction
>>> except ImportError:
>>> from fractions import fractions.Fraction
>>> myRatio0 = fractions.Fraction(3, 2)
>>> JustIntonationPitch._ratio_to_exponents(myRatio0)
(-1, 1)
"""
factorised_numerator = prime_factors.factors(ratio.numerator)
factorised_denominator = prime_factors.factors(ratio.denominator)
factorised_num = prime_factors.factorise(ratio.numerator)
factorised_den = prime_factors.factorise(ratio.denominator)
biggest_prime = max(factorised_num + factorised_den)
exponents = [0] * primesieve.count_primes(biggest_prime)
for prime, fac in factorised_numerator:
if prime > 1:
exponents[primesieve.count_primes(prime) - 1] += fac
for prime, fac in factorised_denominator:
if prime > 1:
exponents[primesieve.count_primes(prime) - 1] -= fac
return tuple(exponents)
@staticmethod
def _indigestibility(num: int) -> float:
"""Calculate _indigestibility of a number
The implementation follows Clarence Barlows definition
given in 'The Ratio Book' (1992).
Arguments:
* num -> integer, whose _indigestibility value shall be calculated
>>> JustIntonationPitch._indigestibility(1)
0
>>> JustIntonationPitch._indigestibility(2)
1
>>> JustIntonationPitch._indigestibility(3)
2.6666666666666665
"""
decomposed = prime_factors.factorise(num)
return JustIntonationPitch._indigestibility_of_factorised(decomposed)
@staticmethod
def _indigestibility_of_factorised(decomposed):
decomposed = collections.Counter(decomposed)
decomposed = zip(decomposed.values(), decomposed.keys())
summed = ((power * pow(prime - 1, 2)) / prime for power, prime in decomposed)
return 2 * sum(summed)
@staticmethod
def _count_accidentals(accidentals: str) -> int:
accidental_counter = collections.Counter({"f": 0, "s": 0})
accidental_counter.update(accidentals)
for accidental in accidentals:
if accidental not in ("f", "s"):
message = "Found unknown accidental '{}' which will be ignored".format(
accidental
)
warnings.warn(message)
return (1 * accidental_counter["s"]) - (1 * accidental_counter["f"])
@staticmethod
def _get_accidentals(n_accidentals: int) -> str:
if n_accidentals > 0:
return "s" * n_accidentals
else:
return "f" * abs(n_accidentals)
# ###################################################################### #
# private methods #
# ###################################################################### #
def _translate_ratio_or_fractions_argument_to_exponents(
self,
ratio_or_exponents: typing.Union[str, fractions.Fraction, typing.Iterable[int]],
) -> typing.Tuple[int, ...]:
if isinstance(ratio_or_exponents, str):
numerator, denominator = ratio_or_exponents.split("/")
exponents = self._ratio_to_exponents(
fractions.Fraction(int(numerator), int(denominator))
)
elif isinstance(ratio_or_exponents, typing.Iterable):
exponents = tuple(ratio_or_exponents)
elif hasattr(ratio_or_exponents, "numerator") and hasattr(
ratio_or_exponents, "denominator"
):
exponents = self._ratio_to_exponents(
fractions.Fraction(
ratio_or_exponents.numerator, ratio_or_exponents.denominator
)
)
else:
message = (
"Unknown type '{}' of object '{}' for 'ratio_or_exponents' argument."
.format(type(ratio_or_exponents), ratio_or_exponents)
)
raise NotImplementedError(message)
return exponents
@decorators.add_return_option
def _math( # type: ignore
self, other: "JustIntonationPitch", operation: typing.Callable
) -> "JustIntonationPitch":
exponents0, exponents1 = JustIntonationPitch._adjust_exponent_lengths(
self.exponents, other.exponents
)
self.exponents = tuple(operation(x, y) for x, y in zip(exponents0, exponents1))
# ###################################################################### #
# magic methods #
# ###################################################################### #
def __float__(self) -> float:
"""Return the float of a JustIntonationPitch - object.
These are the same:
float(myJustIntonationPitch.ratio) == float(myJustIntonationPitch).
Note the difference that the second version might be slightly
more performant.
>>> jip0 = JustIntonationPitch((-1, 1))
>>> float(jip0)
1.5
>>> float(jip0.ratio)
1.5
"""
return self._exponents_to_float(self.exponents, self.primes)
def __repr__(self) -> str:
return "JustIntonationPitch({})".format(self.ratio)
def __add__(self, other: "JustIntonationPitch") -> "JustIntonationPitch":
return self._math(other, operator.add, mutate=False) # type: ignore
def __sub__(self, other: "JustIntonationPitch") -> "JustIntonationPitch":
return self._math(other, operator.sub, mutate=False) # type: ignore
def __abs__(self):
if self.numerator > self.denominator:
return copy.deepcopy(self)
else:
exponents = tuple(-v for v in iter(self.exponents))
return type(self)(exponents, self.concert_pitch)
# ###################################################################### #
# properties #
# ###################################################################### #
@property
def exponents(self) -> tuple:
return self._exponents
@exponents.setter
def exponents(self, exponents: typing.Iterable[int],) -> None:
self._exponents = self._discard_nulls(exponents)
@property
def primes(self) -> tuple:
r"""Return ascending list of primes, until the highest contained Prime.
>>> jip0 = JustIntonationPitch((0, 1, 2))
>>> jip0.exponents
(2, 3, 5)
>>> jip1 = JustIntonationPitch((0, -1, 0, 0, 1), 1)
>>> jip1.exponents
(2, 3, 5, 7, 11)
"""
return tuple(primesieve_numpy.n_primes(len(self.exponents)))
@property
def occupied_primes(self) -> tuple:
"""Return all occurring prime numbers of a JustIntonationPitch object."""
return tuple(
prime
for prime, exponent in zip(self.primes, self.exponents)
if exponent != 0
)
@property
def concert_pitch(self) -> parameters.abc.Pitch:
return self._concert_pitch
@concert_pitch.setter
def concert_pitch(self, concert_pitch: ConcertPitch) -> None:
if not isinstance(concert_pitch, parameters.abc.Pitch):
concert_pitch = DirectPitch(concert_pitch)
self._concert_pitch = concert_pitch
@property
def frequency(self) -> float:
return float(self.ratio * self.concert_pitch.frequency)
@property
def ratio(self) -> fractions.Fraction:
"""Return the JustIntonationPitch transformed to a Ratio.
>>> jip0 = JustIntonationPitch((0, 0, 1,))
>>> jip0.ratio
fractions.Fraction(5, 4)
>>> jip0 = JustIntonationPitch("3/2")
>>> jip0.ratio
fractions.Fraction(3, 2)
"""
return JustIntonationPitch._exponents_to_ratio(self.exponents, self.primes)
@property
def numerator(self) -> int:
"""Return the numerator of a JustIntonationPitch - object.
>>> jip0 = JustIntonationPitch((0, -1,))
>>> jip0.numerator
1
"""
numerator = 1
for number, exponent in zip(self.primes, self.exponents):
if exponent > 0:
numerator *= pow(number, exponent)
return numerator
@property
def denominator(self) -> int:
"""Return the denominator of a JustIntonationPitch - object.
>>> jip0 = JustIntonationPitch((0, 1,))
>>> jip0.denominator
1
"""
denominator = 1
for number, exponent in zip(self.primes, self.exponents):
if exponent < 0:
denominator *= pow(number, -exponent)
return denominator
@property
def cents(self) -> float:
return self.ratio_to_cents(self.ratio)
@property
def factorised(self) -> tuple:
"""Return factorised / decomposed version of itsef.
>>> jip0 = JustIntonationPitch((0, 0, 1,))
>>> jip0.factorised
(2, 2, 5)
>>> jip1 = JustIntonationPitch("7/6")
>>> jip1.factorised
(2, 3, 7)
"""
exponents = self.exponents
primes = self.primes
exponents_adjusted, primes_adjusted = type(self)._adjust_exponents(
exponents, primes, 1
)
decomposed = ([p] * abs(e) for p, e in zip(primes_adjusted, exponents_adjusted))
return tuple(functools.reduce(operator.add, decomposed))
@property
def factorised_numerator_and_denominator(self) -> tuple:
exponents = self.exponents
primes = self.primes
exponents_adjusted, primes_adjusted = type(self)._adjust_exponents(
exponents, primes, 1
)
numerator_denominator: typing.List[typing.List[typing.List[int]]] = [[[]], [[]]]
for prime, exponent in zip(primes_adjusted, exponents_adjusted):
if exponent > 0:
idx = 0
else:
idx = 1
numerator_denominator[idx].append([prime] * abs(exponent))
return tuple(
functools.reduce(operator.add, decomposed)
for decomposed in numerator_denominator
)
@property
def octave(self) -> int:
ct = self.cents
ref, exp = 1200, 0
while ref * exp <= ct:
exp += 1
while ref * exp > ct:
exp -= 1
return exp
@property
def helmholtz_ellis_just_intonation_notation_commas(
self,
) -> parameters.commas.CommaCompound:
"""Commas of JustIntonationPitch."""
prime_to_exponent = {
prime: exponent
for prime, exponent in zip(self.primes, self.exponents)
if exponent != 0 and prime not in (2, 3)
}
return parameters.commas.CommaCompound(
prime_to_exponent, parameters.pitches_constants.DEFAULT_PRIME_TO_COMMA
)
@property
def closest_pythagorean_interval(self) -> "JustIntonationPitch":
if len(self.helmholtz_ellis_just_intonation_notation_commas) > 0:
closest_pythagorean_interval = self - type(self)(
functools.reduce(
operator.mul, self.helmholtz_ellis_just_intonation_notation_commas
)
)
closest_pythagorean_interval.normalize()
else:
closest_pythagorean_interval = self.normalize(mutate=False) # type: ignore
return closest_pythagorean_interval
@property
def blueprint( # type: ignore
self, ignore: typing.Sequence[int] = (2,)
) -> typing.Tuple[typing.Tuple[int, ...], ...]:
blueprint = []
for factorised in self.factorised_numerator_and_denominator:
factorised = tuple(fac for fac in factorised if fac not in ignore)
counter = collections.Counter(collections.Counter(factorised).values())
if counter:
maxima = max(counter.keys())
blueprint.append(tuple(counter[idx + 1] for idx in range(maxima)))
else:
blueprint.append(tuple([]))
return tuple(blueprint)
@property
def tonality(self) -> bool:
"""Return the tonality (bool) of a JustIntonationPitch - object.
The tonality of a JustIntonationPitch - may be True (otonality) if
the exponent of the highest occurring prime number is a
positive number and False if the exponent is a
negative number (utonality).
>>> jip0 = JustIntonationPitch((-2. 1))
>>> jip0.tonality
True
>>> jip1 = JustIntonationPitch((-2, -1))
>>> jip1.tonality
False
>>> jip2 = JustIntonationPitch([])
>>> jip2.tonality
True
"""
if self.exponents:
maxima = max(self.exponents)
minima = min(self.exponents)
test = (
maxima <= 0 and minima < 0,
minima < 0
and self.exponents.index(minima) > self.exponents.index(maxima),
)
if any(test):
return False
return True
@property
def harmonic(self) -> int:
"""Return the nth - harmonic / subharmonic the pitch may represent.
May be positive for harmonic and negative for
subharmonic pitches. If the return - value is 0,
the interval may occur neither between the first harmonic
and any other pitch of the harmonic scale nor
between the first subharmonic in the and any other
pitch of the subharmonic scale.
>>> jip0 = JustIntonationPitch((0, 1))
>>> jip0.ratio
fractions.Fraction(3, 2)
>>> jip0.harmonic
3
>>> jip1 = JustIntonationPitch((-1,), 2)
>>> jip1.harmonic
-3
"""
ratio = self.ratio
if ratio.denominator % 2 == 0:
return ratio.numerator
elif ratio.numerator % 2 == 0:
return -ratio.denominator
elif ratio == fractions.Fraction(1, 1):
return 1
else:
return 0
@property
def primes_for_numerator_and_denominator(self) -> tuple:
return tuple(
tuple(sorted(set(prime_factors.factorise(n))))
for n in (self.numerator, self.denominator)
)
@property
def harmonicity_wilson(self) -> int:
decomposed = self.factorised
return int(sum(filter(lambda x: x != 2, decomposed)))
@property
def harmonicity_vogel(self) -> int:
decomposed = self.factorised
decomposed_filtered = tuple(filter(lambda x: x != 2, decomposed))
am_2 = len(decomposed) - len(decomposed_filtered)
return int(sum(decomposed_filtered) + am_2)
@property
def harmonicity_euler(self) -> int:
"""Return the 'gradus suavitatis' of euler.
A higher number means a less consonant interval /
a more complicated harmony.
euler(1/1) is definied as 1.
>>> jip0 = JustIntonationPitch((0, 1,))
>>> jip1 = JustIntonationPitch()
>>> jip2 = JustIntonationPitch((0, 0, 1,))
>>> jip3 = JustIntonationPitch((0, 0, -1,))
>>> jip0.harmonicity_euler
4
>>> jip1.harmonicity_euler
1
>>> jip2.harmonicity_euler
7
>>> jip3.harmonicity_euler
8
"""
decomposed = self.factorised
return 1 + sum(x - 1 for x in decomposed)
@property
def harmonicity_barlow(self) -> float:
r"""Calculate the barlow-harmonicity of an interval.
This implementation follows Clarence Barlows definition, given
in 'The Ratio Book' (1992).
A higher number means a more harmonic interval / a less
complex harmony.
barlow(1/1) is definied as infinite.
>>> jip0 = JustIntonationPitch((0, 1,))
>>> jip1 = JustIntonationPitch()
>>> jip2 = JustIntonationPitch((0, 0, 1,))
>>> jip3 = JustIntonationPitch((0, 0, -1,))
>>> jip0.harmonicity_barlow
0.27272727272727276
>>> jip1.harmonicity_barlow # 1/1 is infinite harmonic
inf
>>> jip2.harmonicity_barlow
0.11904761904761904
>>> jip3.harmonicity_barlow
-0.10638297872340426
"""
def sign(x):
return (1, -1)[x < 0]
numerator_denominator_decomposed = self.factorised_numerator_and_denominator
indigestibility_numerator = JustIntonationPitch._indigestibility_of_factorised(
numerator_denominator_decomposed[0]
)
indigestibility_denominator = JustIntonationPitch._indigestibility_of_factorised(
numerator_denominator_decomposed[1]
)
if indigestibility_numerator == 0 and indigestibility_denominator == 0:
return float("inf")
return sign(indigestibility_numerator - indigestibility_denominator) / (
indigestibility_numerator + indigestibility_denominator
)
@property
def harmonicity_simplified_barlow(self) -> float:
r"""Calculate a simplified barlow-harmonicity of an interval.
This implementation follows Clarence Barlows definition, given
in 'The Ratio Book' (1992), with the difference that
only positive numbers are returned and that (1/1) is
defined as 1 instead of infinite.
>>> jip0 = JustIntonationPitch((0, 1,))
>>> jip1 = JustIntonationPitch()
>>> jip2 = JustIntonationPitch((0, 0, 1,))
>>> jip3 = JustIntonationPitch((0, 0, -1,))
>>> jip0.harmonicity_simplified_barlow
0.27272727272727276
>>> jip1.harmonicity_simplified_barlow # 1/1 is not infinite but 1
1
>>> jip2.harmonicity_simplified_barlow
0.11904761904761904
>>> jip3.harmonicity_simplified_barlow # positive return value
0.10638297872340426
"""
barlow = abs(self.harmonicity_barlow)
if barlow == float("inf"):
return 1
return barlow
@property
def harmonicity_tenney(self) -> float:
r"""Calculate Tenneys harmonic distance of an interval
A higher number
means a more consonant interval / a less
complicated harmony.
tenney(1/1) is definied as 0.
>>> jip0 = JustIntonationPitch((0, 1,))
>>> jip1 = JustIntonationPitch()
>>> jip2 = JustIntonationPitch((0, 0, 1,))
>>> jip3 = JustIntonationPitch((0, 0, -1,))
>>> jip0.harmonicity_tenney
2.584962500721156
>>> jip1.harmonicity_tenney
0.0
>>> jip2.harmonicity_tenney
4.321928094887363
>>> jip3.harmonicity_tenney
-0.10638297872340426
"""
ratio = self.ratio
num = ratio.numerator
de = ratio.denominator
return math.log(num * de, 2)
@property
def level(self) -> int:
if self.primes:
return abs(
functools.reduce(
math.gcd, tuple(filter(lambda x: x != 0, self.exponents)) # type: ignore
)
)
else:
return 1
# ###################################################################### #
# public methods #
# ###################################################################### #
[docs] def get_closest_pythagorean_pitch_name(self, reference: str = "a") -> str:
""""""
# TODO(for future usage: type reference as typing.Literal[] instead of str)
# TODO(split method, make it more readable)
# TODO(Add documentation)
diatonic_pitch_name, accidentals = reference[0], reference[1:]
n_accidentals_in_reference = JustIntonationPitch._count_accidentals(accidentals)
position_of_diatonic_pitch_in_cycle_of_fifths = parameters.pitches_constants.DIATONIC_PITCH_NAME_CYCLE_OF_FIFTHS.index(
diatonic_pitch_name
)
closest_pythagorean_interval = self.closest_pythagorean_interval
try:
n_fifths = closest_pythagorean_interval.exponents[1]
# for 1/1
except IndexError:
n_fifths = 0
# 1. Find new diatonic pitch name
n_steps_in_diatonic_pitch_name = n_fifths % 7
nth_diatonic_pitch = (
position_of_diatonic_pitch_in_cycle_of_fifths
+ n_steps_in_diatonic_pitch_name
) % 7
new_diatonic_pitch = parameters.pitches_constants.DIATONIC_PITCH_NAME_CYCLE_OF_FIFTHS[
nth_diatonic_pitch
]
# 2. Find new accidentals
n_accidentals_in_closest_pythagorean_pitch = (
(position_of_diatonic_pitch_in_cycle_of_fifths + n_fifths) // 7
) + n_accidentals_in_reference
new_accidentals = JustIntonationPitch._get_accidentals(
n_accidentals_in_closest_pythagorean_pitch
)
return "".join((new_diatonic_pitch, new_accidentals))
[docs] @decorators.add_return_option
def register(self, octave: int) -> typing.Optional["JustIntonationPitch"]: # type: ignore
normalized_just_intonation_pitch = self.normalize(mutate=False) # type: ignore
factor = 2 ** abs(octave)
if octave < 1:
added = type(self)(fractions.Fraction(1, factor))
else:
added = type(self)(fractions.Fraction(factor, 1))
self.exponents = (normalized_just_intonation_pitch + added).exponents # type: ignore
[docs] @decorators.add_return_option
def move_to_closest_register( # type: ignore
self, reference: "JustIntonationPitch"
) -> typing.Optional["JustIntonationPitch"]:
reference_register = reference.octave
best = None
for adaption in range(-1, 2):
candidate: "JustIntonationPitch" = self.register(reference_register + adaption, mutate=False) # type: ignore
difference = abs((candidate - reference).cents)
set_best = True
if best and difference > best[1]:
set_best = False
if set_best:
best = (candidate, difference)
if best:
self.exponents = best[0].exponents
else:
raise NotImplementedError(
"Couldn't find closest register of '{}' to '{}'.".format(
self, reference
)
)
[docs] @decorators.add_return_option
def normalize(self, prime: int = 2) -> typing.Optional["JustIntonationPitch"]: # type: ignore
"""Normalize JustIntonationPitch."""
ratio = self.ratio
adjusted = type(self)._adjust_ratio(ratio, prime)
self.exponents = self._translate_ratio_or_fractions_argument_to_exponents(
adjusted
)
[docs] @decorators.add_return_option
def inverse( # type: ignore
self, axis: typing.Optional["JustIntonationPitch"] = None
) -> typing.Optional["JustIntonationPitch"]:
if axis is None:
exponents = tuple(map(lambda x: -x, self.exponents))
else:
distance = self - axis
exponents = (axis - distance).exponents
self.exponents = exponents
[docs] @decorators.add_return_option
def add( # type: ignore
self, other: "JustIntonationPitch"
) -> typing.Optional["JustIntonationPitch"]:
self._math(other, operator.add)
[docs] @decorators.add_return_option
def subtract( # type: ignore
self, other: "JustIntonationPitch"
) -> typing.Optional["JustIntonationPitch"]:
self._math(other, operator.sub)
[docs]class EqualDividedOctavePitch(parameters.abc.Pitch):
"""Pitch that is tuned to an Equal divided octave tuning system.
:param n_pitch_classes_per_octave: how many pitch classes in each octave
occur (for instance 12 for a chromatic system, 24 for quartertones, etc.)
:param pitch_class: The pitch class of the new :class:`EqualDividedOctavePitch` object.
:param octave: The octave of the new :class:`EqualDividedOctavePitch` object (where 0 is
the middle octave, 1 is one octave higher and -1 is one octave lower).
:param concert_pitch_pitch_class: The pitch class of the reference pitch (for
instance 9 in a chromatic 12 tone system where `a` should be the reference
pitch).
:param concert_pitch_octave: The octave of the reference pitch.
:param concert_pitch: The frequency of the reference pitch (for instance 440 for a).
>>> from mutwo.parameters import pitches
>>> # making a middle `a`
>>> pitches.EqualDividedOctavePitch(12, 9, 4, 9, 4, 440)
"""
def __init__(
self,
n_pitch_classes_per_octave: int,
pitch_class: constants.Real,
octave: int,
concert_pitch_pitch_class: constants.Real,
concert_pitch_octave: int,
concert_pitch: ConcertPitch = None,
):
if concert_pitch is None:
concert_pitch = parameters.pitches_constants.DEFAULT_CONCERT_PITCH
self._n_pitch_classes_per_octave = n_pitch_classes_per_octave
self.pitch_class = pitch_class
self.octave = octave
self.concert_pitch_pitch_class = concert_pitch_pitch_class
self.concert_pitch_octave = concert_pitch_octave
self.concert_pitch = concert_pitch # type: ignore
def _assert_correct_pitch_class(self, pitch_class: constants.Real) -> None:
"""Makes sure the respective pitch_class is within the allowed range."""
try:
assert all(
(pitch_class <= self.n_pitch_classes_per_octave - 1, pitch_class >= 0)
)
except AssertionError:
message = (
"Invalid pitch class {}!. Pitch_class has to be in range (min = 0, max"
" = {}).".format(pitch_class, self.n_pitch_classes_per_octave - 1)
)
raise ValueError(message)
@property
def n_pitch_classes_per_octave(self) -> int:
"""Defines in how many different pitch classes one octave get divided."""
return self._n_pitch_classes_per_octave
@property
def concert_pitch(self) -> parameters.abc.Pitch:
"""The referential concert pitch for the respective pitch object."""
return self._concert_pitch
@concert_pitch.setter
def concert_pitch(self, concert_pitch: ConcertPitch) -> None:
if not isinstance(concert_pitch, parameters.abc.Pitch):
concert_pitch = DirectPitch(concert_pitch)
self._concert_pitch = concert_pitch
@property
def concert_pitch_pitch_class(self) -> constants.Real:
"""The pitch class of the referential concert pitch."""
return self._concert_pitch_pitch_class
@concert_pitch_pitch_class.setter
def concert_pitch_pitch_class(self, pitch_class: constants.Real) -> None:
self._assert_correct_pitch_class(pitch_class)
self._concert_pitch_pitch_class = pitch_class
@property
def pitch_class(self) -> constants.Real:
"""The pitch class of the pitch."""
return self._pitch_class
@pitch_class.setter
def pitch_class(self, pitch_class: constants.Real) -> None:
self._assert_correct_pitch_class(pitch_class)
self._pitch_class = pitch_class
@property
def step_factor(self):
"""The factor with which to multiply a frequency to reach the next pitch."""
return pow(2, 1 / self.n_pitch_classes_per_octave)
@property
def n_cents_per_step(self) -> float:
"""This property describes how many cents are between two adjacent pitches."""
return self.ratio_to_cents(self.step_factor)
@property
def frequency(self) -> float:
n_octaves_distant_to_concert_pitch = self.octave - self.concert_pitch_octave
n_pitch_classes_distant_to_concert_pitch = (
self.pitch_class - self.concert_pitch_pitch_class
)
distance_to_concert_pitch_in_cents = (
n_octaves_distant_to_concert_pitch * 1200
) + (self.n_cents_per_step * n_pitch_classes_distant_to_concert_pitch)
distance_to_concert_pitch_as_factor = self.cents_to_ratio(
distance_to_concert_pitch_in_cents
)
return float(self.concert_pitch.frequency * distance_to_concert_pitch_as_factor)
def __sub__(self, other: "EqualDividedOctavePitch") -> constants.Real:
"""Calculates the interval between two ``EqualDividedOctave`` pitches."""
try:
assert self.n_pitch_classes_per_octave == other.n_pitch_classes_per_octave
except AssertionError:
message = (
"Can't calculate the interval between to different"
" EqualDividedOctavePitch objects with different value for"
" 'n_pitch_classes_per_octave'."
)
raise ValueError(message)
n_pitch_classes_difference = self.pitch_class - other.pitch_class
n_octaves_difference = self.octave - other.octave
return n_pitch_classes_difference + (
n_octaves_difference * self.n_pitch_classes_per_octave
)
def _math(
self,
n_pitch_classes_difference: constants.Real,
operator: typing.Callable[[constants.Real, constants.Real], constants.Real],
) -> None:
new_pitch_class = operator(self.pitch_class, n_pitch_classes_difference)
n_octaves_difference = new_pitch_class // self.n_pitch_classes_per_octave
new_pitch_class = new_pitch_class % self.n_pitch_classes_per_octave
new_octave = self.octave + n_octaves_difference
self.pitch_class = new_pitch_class
self.octave = int(new_octave)
[docs] @decorators.add_return_option
def add( # type: ignore
self, n_pitch_classes_difference: constants.Real
) -> typing.Union[None, "EqualDividedOctavePitch"]: # type: ignore
"""Transposes the ``EqualDividedOctavePitch`` by n_pitch_classes_difference."""
self._math(n_pitch_classes_difference, operator.add)
[docs] @decorators.add_return_option
def subtract( # type: ignore
self, n_pitch_classes_difference: constants.Real
) -> typing.Union[None, "EqualDividedOctavePitch"]: # type: ignore
"""Transposes the ``EqualDividedOctavePitch`` by n_pitch_classes_difference."""
self._math(n_pitch_classes_difference, operator.sub)
# TODO(add something similar to scamps SpellingPolicy (don't hard code
# if mutwo shall write a flat or sharp)
# TODO(add translation from octave number to notated octave (4 -> ', 5 -> '', ..))
[docs]class WesternPitch(EqualDividedOctavePitch):
"""Pitch with a traditional Western nomenclature.
:param pitch_class_or_pitch_class_name: Name or number of the pitch class of the
new ``WesternPitch`` object. The nomenclature is English (c, d, e, f, g, a, b).
It uses an equal divided octave system in 12 chromatic steps. Accidentals are
indicated by (s = sharp) and (f = flat). Further microtonal accidentals are
supported (see
:const:`mutwo.parameters.pitches_constants.ACCIDENTAL_NAME_TO_PITCH_CLASS_MODIFICATION`
for all supported accidentals).
:param octave: The octave of the new :class:`WesternPitch` object. Indications for the
specific octave follow the MIDI Standard where 4 is defined as one line.
>>> from mutwo.parameters import pitches
>>> pitches.WesternPitch('cs', 4) # c-sharp 4
>>> pitches.WesternPitch('aqs', 2) # a-quarter-sharp 2
"""
def __init__(
self,
pitch_class_or_pitch_class_name: PitchClassOrPitchClassName = 0,
octave: int = 4,
concert_pitch_pitch_class: constants.Real = None,
concert_pitch_octave: int = None,
concert_pitch: ConcertPitch = None,
):
if concert_pitch_pitch_class is None:
concert_pitch_pitch_class = (
parameters.pitches_constants.DEFAULT_CONCERT_PITCH_PITCH_CLASS_FOR_WESTERN_PITCH
)
if concert_pitch_octave is None:
concert_pitch_octave = (
parameters.pitches_constants.DEFAULT_CONCERT_PITCH_OCTAVE_FOR_WESTERN_PITCH
)
(
pitch_class,
pitch_class_name,
) = self._translate_pitch_class_or_pitch_class_name_to_pitch_class_and_pitch_class_name(
pitch_class_or_pitch_class_name
)
super().__init__(
12,
pitch_class,
octave,
concert_pitch_pitch_class,
concert_pitch_octave,
concert_pitch,
)
self._pitch_class_name = pitch_class_name
@staticmethod
def _translate_pitch_class_or_pitch_class_name_to_pitch_class_and_pitch_class_name(
pitch_class_or_pitch_class_name: PitchClassOrPitchClassName,
) -> tuple:
"""Helper function to initialise a WesternPitch from a number or a string.
A number has to represent the pitch class while the name has to use
the Western English nomenclature with the form
DIATONICPITCHCLASSNAME-ACCIDENTAL (e.g. "cs" for c-sharp,
"gqf" for g-quarter-flat, "b" for b)
"""
if isinstance(pitch_class_or_pitch_class_name, numbers.Real):
pitch_class = float(pitch_class_or_pitch_class_name)
pitch_class_name = WesternPitch._translate_pitch_class_to_pitch_class_name(
pitch_class_or_pitch_class_name
)
elif isinstance(pitch_class_or_pitch_class_name, str):
pitch_class = WesternPitch._translate_pitch_class_name_to_pitch_class(
pitch_class_or_pitch_class_name
)
pitch_class_name = pitch_class_or_pitch_class_name
else:
message = "Can't initalise pitch_class by '{}' of type '{}'.".format(
pitch_class_or_pitch_class_name, type(pitch_class_or_pitch_class_name)
)
raise TypeError(message)
return pitch_class, pitch_class_name
@staticmethod
def _translate_accidental_to_pitch_class_modifications(
accidental: str,
) -> constants.Real:
"""Helper function to translate an accidental to its pitch class modification.
Raises an error if the accidental hasn't been defined yet in
mutwo.parameters.parameters.parameters.pitches_constants.ACCIDENTAL_NAME_TO_PITCH_CLASS_MODIFICATION.
"""
try:
return parameters.pitches_constants.ACCIDENTAL_NAME_TO_PITCH_CLASS_MODIFICATION[
accidental
]
except KeyError:
message = (
"Can't initialise WesternPitch with unknown accidental {}!".format(
accidental
)
)
raise NotImplementedError(message)
@staticmethod
def _translate_pitch_class_name_to_pitch_class(pitch_class_name: str,) -> float:
"""Helper function to translate a pitch class name to its respective number.
+/-1 is defined as one chromatic step. Smaller floating point numbers
represent microtonal inflections..
"""
diatonic_pitch_class_name, accidental = (
pitch_class_name[0],
pitch_class_name[1:],
)
diatonic_pitch_class = parameters.pitches_constants.DIATONIC_PITCH_NAME_TO_PITCH_CLASS[
diatonic_pitch_class_name
]
pitch_class_modification = WesternPitch._translate_accidental_to_pitch_class_modifications(
accidental
)
return float(diatonic_pitch_class + pitch_class_modification)
@staticmethod
def _translate_difference_to_closest_diatonic_pitch_to_accidental(
difference_to_closest_diatonic_pitch: constants.Real,
) -> str:
"""Helper function to translate a number to the closest known accidental."""
closest_pitch_class_modification: fractions.Fraction = tools.find_closest_item(
difference_to_closest_diatonic_pitch,
tuple(
parameters.pitches_constants.PITCH_CLASS_MODIFICATION_TO_ACCIDENTAL_NAME.keys()
),
)
closest_accidental = parameters.pitches_constants.PITCH_CLASS_MODIFICATION_TO_ACCIDENTAL_NAME[
closest_pitch_class_modification
]
return closest_accidental
@staticmethod
def _translate_pitch_class_to_pitch_class_name(pitch_class: constants.Real) -> str:
"""Helper function to translate a pitch class in number to a string.
The returned pitch class name uses a Western nomenclature of English
diatonic note names. Accidental names are defined in
mutwo.parameters.pitches_constants.ACCIDENTAL_NAME_TO_PITCH_CLASS_MODIFICATION.
For floating point numbers the closest accidental will be chosen.
"""
diatonic_pitch_classes = tuple(
parameters.pitches_constants.DIATONIC_PITCH_NAME_TO_PITCH_CLASS.values()
)
closest_diatonic_pitch_class_index = tools.find_closest_index(
pitch_class, diatonic_pitch_classes
)
closest_diatonic_pitch_class = diatonic_pitch_classes[
closest_diatonic_pitch_class_index
]
closest_diatonic_pitch = tuple(
parameters.pitches_constants.DIATONIC_PITCH_NAME_TO_PITCH_CLASS.keys()
)[closest_diatonic_pitch_class_index]
difference_to_closest_diatonic_pitch = (
pitch_class - closest_diatonic_pitch_class
)
accidental = WesternPitch._translate_difference_to_closest_diatonic_pitch_to_accidental(
difference_to_closest_diatonic_pitch
)
pitch_class_name = "{}{}".format(closest_diatonic_pitch, accidental)
return pitch_class_name
def __repr__(self) -> str:
return "{}({})".format(type(self).__name__, self.name)
@property
def name(self) -> str:
"""The name of the pitch in Western nomenclature."""
return "{}{}".format(self._pitch_class_name, self.octave)
@property
def pitch_class_name(self) -> str:
"""The name of the pitch class in Western nomenclature.
Mutwo uses the English nomenclature for pitch class names:
(c, d, e, f, g, a, b)
"""
return self._pitch_class_name
@pitch_class_name.setter
def pitch_class_name(self, pitch_class_name: str):
self._pitch_class = self._translate_pitch_class_name_to_pitch_class(
pitch_class_name
)
self._pitch_class_name = pitch_class_name
@EqualDividedOctavePitch.pitch_class.setter # type: ignore
def pitch_class(self, pitch_class: constants.Real):
self._pitch_class_name = self._translate_pitch_class_to_pitch_class_name(
pitch_class
)
self._pitch_class = pitch_class