from typing import List, Union, Optional
from fractions import Fraction
import numbers
__all__ = ["QuarterDuration", "QuarterDurationMixin"]
from musicscore.config import (
NOTETYPES,
BEATWISE_EXCEPTIONS,
DOTEDTUPLETRATIO,
TYPEANDDOTEXCEPTIONS,
)
from musicscore.exceptions import QuarterDurationIsNotWritable
[docs]class QuarterDuration(numbers.Rational):
"""
A Class specifically designed for durations measured in quarters. The core of this class is a value of type ''fractions.Fraction'' with a
denominator limit of 1000, thus it can manage conversion of floats to fractions without usual inaccuracies of quintuples etc. See
value property for more information.
QuarterDuration has all needed magic methods for numeral comparison and conversion.
"""
def __init__(self, *value):
self._value = None
self.value = value
self._beat_subdivision = None
self._beat_quarter_duration = 1
self._type_and_dots = None
def _get_beatwise_sections(
self,
beats: List["Beat"],
offset: Union[int, float, "QuarterDuration", "Fraction"] = 0,
):
"""
:param beats:
:param offset: offset in the first beat
:return: [sections as list of QuarterDurations, leftover as QuarterDruation] leftover is the remaining quarter_duration which
exceeds the sum of all beats quarter durations. If there is no left over the second value in the list is None.
offset=0.5, beats=[Beat(1), Beat(1), Beat(1), Beat(1)] => [[0.5, 3], None]
See tests for more examples. In BEATWISE_EXCEPTIONS exceptions can be declared.
"""
def _check_for_exception():
exception = BEATWISE_EXCEPTIONS.get(offset)
if exception:
out = exception.get(sum(output[0]))
if out:
output[0] = [QuarterDuration(value) for value in out]
return output
output = [None, [], None]
if offset:
output[0] = beats[0].quarter_duration - offset
beats.pop(0)
leftover = self - output[0] if output[0] is not None else self
out_of_reach = leftover - sum(b.quarter_duration for b in beats)
if out_of_reach > 0:
leftover -= out_of_reach
output[2] = out_of_reach
for beat in beats:
if leftover >= beat.quarter_duration:
current_value = beat.quarter_duration
leftover -= current_value
else:
current_value = leftover
leftover = 0
if not output[1]:
output[1] = [current_value]
else:
if _is_writable(output[1][-1] + current_value):
output[1][-1] += current_value
else:
output[1].append(current_value)
if leftover == 0:
break
if output[0]:
output = [[output[0], *output[1]], output[2]]
else:
output = [output[1], output[2]]
exception = _check_for_exception()
if exception:
return exception
return output
def _get_type_and_dots(self):
if self.value == 0:
return None, 0
if not self.beat_subdivision:
self.beat_subdivision = self.denominator
try:
type_and_dots = (
TYPEANDDOTEXCEPTIONS.get(self.beat_quarter_duration)
.get(self.beat_subdivision)
.get(self.as_integer_ratio())
)
if type_and_dots:
return type_and_dots
except AttributeError:
pass
type = NOTETYPES.get(self.as_integer_ratio())
if type:
return type, 0
else:
qd = QuarterDuration(self.value * 2 / 3)
type = NOTETYPES.get(qd.as_integer_ratio())
if type:
return type, 1
else:
qd = QuarterDuration(self.value * 4 / 7)
type = NOTETYPES.get(qd.as_integer_ratio())
if type:
return type, 2
else:
raise QuarterDurationIsNotWritable(
f"quarter duration {self} is not writable."
)
@property
def beat_subdivision(self):
return self._beat_subdivision
@beat_subdivision.setter
def beat_subdivision(self, val):
self._beat_subdivision = val
self._type_and_dots = None
@property
def beat_quarter_duration(self):
return self._beat_quarter_duration
@beat_quarter_duration.setter
def beat_quarter_duration(self, val):
self._beat_quarter_duration = val
self._type_and_dots = None
@property
def denominator(self):
"""
:return: Fraction's denominator.
:rtype: int
>>> QuarterDuration(1, 6).denominator
6
"""
return self.value.denominator
@property
def numerator(self):
"""
:return: Fraction's numerator.
:rtype: int
>>> QuarterDuration(1, 6).numerator
1
"""
return self.value.numerator
@property
def type_and_dots(self):
if self._type_and_dots is None:
self._type_and_dots = self._get_type_and_dots()
return self._type_and_dots
@property
def value(self):
"""
:return: QuarterDuration's value
:rtype: fractions.Fraction with limit_denominator(1000)
>>> QuarterDuration(3, 7).value
Fraction(3, 7)
>>> QuarterDuration(0.2).value
Fraction(1, 5)
>>> QuarterDuration(Fraction(1, 5)).value
Fraction(1, 5)
>>> QuarterDuration(1/5).value
Fraction(1, 5)
"""
return self._value
@value.setter
def value(self, val):
if val is None or val == ():
self._value = None
elif isinstance(val, Fraction):
self._value = val
elif isinstance(val, str):
self._value = Fraction(val).limit_denominator(1000)
elif hasattr(val, "__iter__"):
if len(val) == 1:
self._value = Fraction(val[0]).limit_denominator(1000)
elif len(val) == 2:
self._value = Fraction(*val).limit_denominator(1000)
else:
raise ValueError
else:
try:
self._value = Fraction(val).limit_denominator(1000)
except TypeError:
raise TypeError("Wrong type for QuarterDuration.value")
[docs] def as_integer_ratio(self):
"""
:return: (numerator, denominator)
:rtype: tuple
>>> QuarterDuration(1, 5).as_integer_ratio()
(1, 5)
"""
return self.value.as_integer_ratio()
[docs] def get_number_of_dots(self) -> Optional[int]:
"""
:return: Number of note dots associated with quarter duration
"""
return self.type_and_dots[1]
[docs] def get_tuplet_ratio(self) -> Optional[tuple]:
if self.value == 0:
return None
if not self.beat_subdivision:
self.beat_subdivision = self.denominator
if self.beat_quarter_duration % 3 == 0:
if self.beat_subdivision > 9:
raise NotImplementedError(
"Beats with dotted quarter duration and subdivision > 9"
)
else:
tupletratio = DOTEDTUPLETRATIO.get(self.beat_subdivision)
if tupletratio:
return self.beat_subdivision, tupletratio
else:
return None
else:
if self.beat_subdivision < 3:
return None
elif self.beat_subdivision > 64:
raise NotImplementedError("Beats subdivision > 64")
normal_notes = [2, 4, 8, 16, 32]
if self.beat_subdivision in normal_notes:
return None
else:
for normal in reversed(normal_notes):
if self.beat_subdivision > normal:
return self.beat_subdivision, normal
raise NotImplementedError(
f"Quarter duration {self} in a beat with {self.beat_subdivision} and quarter duration {self.beat_quarter_duration}"
)
[docs] def get_type(self) -> Optional[str]:
"""
:return: Note type associated with quarter duration
"""
return self.type_and_dots[0]
def __repr__(self):
return f"{self.value.numerator}/{self.value.denominator}"
def __str__(self):
return f"QuarterDuration: {str(self.value)}"
def __abs__(self):
return QuarterDuration(self.value.__abs__())
def __add__(self, other):
return QuarterDuration(self.value.__add__(_convert_other(other)))
def __ceil__(self):
return QuarterDuration(self.value.__ceil__())
def __eq__(self, other):
return self.value.__eq__(_convert_other(other))
def __floor__(self):
return QuarterDuration(self.value.__floor__())
def __floordiv__(self, other):
return QuarterDuration(self.value.__floordiv__(_convert_other(other)))
def __gt__(self, other):
return self.value.__gt__(_convert_other(other))
def __ge__(self, other):
return self.value.__ge__(_convert_other(other))
def __hash__(self):
return self.value.__hash__()
def __le__(self, other):
return self.value.__le__(_convert_other(other))
def __lt__(self, other):
return QuarterDuration(self.value.__lt__(_convert_other(other)))
def __mod__(self, other):
return QuarterDuration(self.value.__mod__(_convert_other(other)))
def __mul__(self, other):
return QuarterDuration(self.value.__mul__(_convert_other(other)))
def __neg__(self):
return self.value.__neg__()
def __pos__(self):
return self.value.__pos__()
def __pow__(self, power):
return QuarterDuration(self.value.__pow__(power))
def __radd__(self, other):
return QuarterDuration(self.value.__radd__(_convert_other(other)))
def __rfloordiv__(self, other):
return QuarterDuration(self.value.__rfloordiv__(_convert_other(other)))
def __rmod__(self, other):
return QuarterDuration(self.value.__rmod__(_convert_other(other)))
def __rmul__(self, other):
return QuarterDuration(self.value.__rmul__(_convert_other(other)))
def __round__(self, n=None):
return QuarterDuration(self.value.__round__(n))
def __rpow__(self, other):
return QuarterDuration(self.value.__rpow__(_convert_other(other)))
def __rtruediv__(self, other):
return QuarterDuration(self.value.__rtruediv__(_convert_other(other)))
def __truediv__(self, other):
return QuarterDuration(self.value.__truediv__(_convert_other(other)))
def __trunc__(self):
return self.value.__trunc__()
def __copy__(self):
return self.__class__(self.value)
def __deepcopy__(self, memodict={}):
return self.__class__(self.value)
def _is_writable(quarter_duration: Union[float, int, Fraction, "QuarterDuration"]):
"""
Function to check if a quarter duration is writable or must be split into two durations.
:param quarter_duration:
:return: boolean
>>> _is_writable(5)
False
>>> _is_writable(7/8)
False
>>> _is_writable(3/8)
True
"""
writables = {
1 / 64,
1 / 32,
3 / 64,
1 / 16,
3 / 32,
1 / 8,
3 / 16,
1 / 4,
3 / 8,
1 / 2,
3 / 4,
1,
3 / 2,
2,
3,
4,
6,
8,
12,
}
if quarter_duration in writables:
return True
else:
return False
def _convert_other(other) -> bool:
if isinstance(other, QuarterDuration):
return other.value
try:
return Fraction(other).limit_denominator(1000)
except TypeError:
return False
[docs]class QuarterDurationMixin:
"""
Mixin for all Classes with a quarter_duration. Used in :obj:`~musicscore.note.Note`, :obj:`~musicscore.chord.Chord` and
:obj:`~musicscore.beat.Beat`
"""
def __init__(self, quarter_duration=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self._quarter_duration = None
self.quarter_duration = quarter_duration
def _set_quarter_duration(self, val):
if isinstance(val, QuarterDuration):
self._quarter_duration = val
else:
_check_quarter_duration_value(val)
self._quarter_duration = QuarterDuration(val)
@property
def quarter_duration(self) -> QuarterDuration:
"""
Set and get the duration measured in quarters.
Setting value can be of types ``int``, ``float``, ``fractions.Fraction``, :obj:`~musicscore.quarterduration.QuarterDuration`
"""
return self._quarter_duration
@quarter_duration.setter
def quarter_duration(self, val):
if val is not None:
self._set_quarter_duration(val)
else:
self._quarter_duration = None
def _check_quarter_duration_value(val):
if (
not isinstance(val, int)
and not isinstance(val, float)
and not isinstance(val, Fraction)
and not isinstance(val, QuarterDuration)
):
raise TypeError(f"Wrong type for quarter duration {val}: {type(val)}")
if val < 0:
raise ValueError()
return True