Source code for neoscore.western.interval

import re

from neoscore.core.directions import DirectionY
from neoscore.core.exceptions import InvalidIntervalError


[docs]class Interval: """An abstract pitch interval.""" _shorthand_regex = re.compile("^([ad])([mMPdA])([1-9]\d*)$") _base_pc_deltas = {1: 0, 2: 2, 3: 4, 4: 5, 5: 7, 6: 9, 7: 11} _qualities_in_english = { "m": "minor", "M": "Major", "d": "diminished", "A": "Augmented", } _perfectable_distances = [1, 4, 5]
[docs] def __init__(self, specifier: str): """ The interval specifier should be a string in the form: ``[direction][quality][distance]`` where: * ``direction`` is one of: * ``'a'`` for ascending * ``'d'`` for descending * ``quality`` is one of: * ``'m'`` for minor * ``'M'`` for Major * ``'P'`` for Perfect * ``'d'`` for diminished * ``'A'`` for Augmented * ``distance`` is any positive integer indicating the interval distance. Some examples: * ``Interval('aM3')`` signifies an ascending major third * ``Interval('dA9')`` signifies a descending augmented ninth """ match = Interval._shorthand_regex.match(specifier) if match is None: raise InvalidIntervalError self._direction = DirectionY.UP if match.group(1) == "a" else DirectionY.DOWN self._quality = match.group(2) self._distance = int(match.group(3)) # Check against invalid edge case intervals if ( self.simple_distance in Interval._perfectable_distances and self._quality not in [ "P", "d", "A", ] ): # unisons, fourths, fifths, and their compounds # can only be perfect, augmented, or diminished raise InvalidIntervalError
def __repr__(self): direction_str = "a" if self.direction == DirectionY.UP else "d" return f"Interval('{direction_str}{self.quality}{self.distance}')" def __hash__(self): return hash(self.direction) ^ hash(self.quality) ^ hash(self.distance) def __eq__(self, other): return ( isinstance(other, Interval) and self.direction == other.direction and self.quality == other.quality and self.distance == other.distance ) @property def direction(self) -> DirectionY: return self._direction @property def quality(self) -> str: return self._quality @property def quality_in_english(self) -> str: return Interval._qualities_in_english[self._quality] @property def distance(self) -> int: return self._distance @property def staff_distance(self) -> float: """The vertical offset needed to show this interval on a staff. The value is given in pseudo-staff-units. >>> Interval('aM3').staff_distance -1.0 >>> Interval('dm3').staff_distance 1.0 >>> Interval('aP1').staff_distance 0.0 >>> Interval('dP8').staff_distance 3.5 """ return ((self.distance - 1) * self.direction.value) / 2 @property def simple_distance(self) -> float: """The simplified interval distance collapsing compound intervals. This value is the simplified version of ``self.distance`` where intervals larger than an octave are moved to be within one octave. Examples: >>> Interval('aM10').simple_distance 3 >>> Interval('dP12').simple_distance 5 """ return ((self.distance - 1) % 7) + 1