Source code for musicscore.quarterduration

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