Module musx.interval
A class that implements musical intervals.
The Interval class supports the standard interval names and classification system, including the notion of descending or ascending intervals and simple or compound intervals. Intervals can be numerically compared for their size (span+quality) and can be used to transpose Pitches while preserving correct accidental spelling.
Expand source code
###############################################################################
"""
A class that implements musical intervals.
The Interval class supports the standard interval names and classification
system, including the notion of descending or ascending intervals and simple
or compound intervals. Intervals can be numerically compared for their size
(span+quality) and can be used to transpose Pitches while preserving correct
accidental spelling.
"""
__pdoc__ = {'IntervalBase': False}
from .pitch import Pitch
from collections import namedtuple
IntervalBase = namedtuple('IntervalBase', ['span', 'qual', 'xoct', 'sign'])
#_pdoc__ = {
# 'Interval.__new__': True, 'Interval._string_to_pitch': True,
# 'Interval._values_to_pitch': True, 'Interval.__str__': True,
# 'Interval.__repr__': True, 'Interval.__lt__': True, 'Interval.__eq__': True,
# 'Interval.__ne__': True, 'Interval.__ge__': True, 'Interval.__gt__': True
#}
class Interval (IntervalBase):
## Private class constants representing spans. There are eight interval
# spans ranging 0-7: _Uni=Unison, _2nd=Second, _3rd=Third, ... _8va=Octave
_Uni, _2nd, _3rd, _4th, _5th, _6th, _7th, _8va = (i for i in range(8))
## Private class constants representing qualities. There are thirteen
# qualities ranging 0-13: from quintuply-diminished intervals to
# quintuply-augmented intervals.
_5dim, _4dim, _3dim, _2dim, _dim, _min, _perf, _maj, _aug, _2aug, _3aug, _4aug, _5aug \
= (i for i in range(13))
## Private map of all possible interval quality names to their constants.
# Note that diminished can use either "o" or "d", and augmented
# can use "+" or "A".
_qual_map = {"ooooo": _5dim, "oooo": _4dim, "ooo": _3dim, "oo": _2dim, "o": _dim,
"ddddd": _5dim, "dddd": _4dim, "ddd": _3dim, "dd": _2dim, "d": _dim,
"m": _min, "P": _perf, "M": _maj,
"+": _aug, "++": _2aug, "+++": _3aug, "++++": _4aug, "+++++": _5aug,
"A": _aug, "AA": _2aug, "AAA": _3aug, "AAAA": _4aug, "AAAAA": _5aug}
## Reverse map from quality constants 0-12 onto their canonical names.
_qual_names = ["ooooo", "oooo", "ooo", "oo", "o",
"m", "P", "M",
"+", "++", "+++", "++++", "+++++"]
## Reverse map from span constants to span full names
_span_full_names = ['unison', 'second', 'third', 'fourth', 'fifth', 'sixth',
'seventh', 'octave']
## Reverse map from quality constants to quality full names
_qual_full_names = ['quintuply-diminished', 'quadruply-diminished',
'triply-diminished', 'doubly-diminished', 'diminished',
'minor', 'perfect', 'major', 'augmented',
'doubly-augmented', 'triply-augmented',
'quadruply-augmented', 'quintuply-augmented']
## Ordered list of all possible perfect interval span values.
_perf_spans = [_Uni, _4th, _5th, _8va]
## Ordered list of all possible imperfect interval span values.
_impf_spans = [_2nd, _3rd, _6th, _7th]
## Ordered list of all possible imperfect interval qualities.
_impf_quals = [_5dim, _4dim, _3dim, _2dim, _dim, _min, _maj, _aug, _2aug, _3aug, _4aug, _5aug]
## Ordered list of all possible perfect interval qualities.
_perf_quals = [_5dim, _4dim, _3dim, _2dim, _dim, _perf, _aug, _2aug, _3aug, _4aug, _5aug]
## Reverse map from (diatonic) span values to their semitone content.
_diatonic_semitones = [0, 2, 4, 5, 7, 9, 11, 12]
## A 2D map that returns the semitones for a given quality and span value.
_semitones_map = {
_min: {_2nd: 1, _3rd: 3, _6th: 8, _7th: 10},
_maj: {_2nd: 2, _3rd: 4, _6th: 9, _7th: 11},
_perf: {_Uni: 0, _4th: 5, _5th: 7, _8va: 12},
_dim: {_Uni: -1, _2nd: 0, _3rd: 2, _4th: 4, _5th: 6, _6th: 7, _7th: 9, _8va: 11},
_2dim: {_Uni: -2, _2nd: -1, _3rd: 1, _4th: 3, _5th: 5, _6th: 6, _7th: 8, _8va: 10},
_3dim: {_Uni: -3, _2nd: -2, _3rd: 0, _4th: 2, _5th: 4, _6th: 5, _7th: 7, _8va: 9},
_4dim: {_Uni: -4, _2nd: -3, _3rd: -1, _4th: 1, _5th: 3, _6th: 4, _7th: 6, _8va: 8},
_5dim: {_Uni: -5, _2nd: -4, _3rd: -2, _4th: 0, _5th: 2, _6th: 3, _7th: 5, _8va: 7},
_aug: {_Uni: 1, _2nd: 3, _3rd: 5, _4th: 6, _5th: 8, _6th: 10, _7th: 12, _8va: 13},
_2aug: {_Uni: 2, _2nd: 4, _3rd: 6, _4th: 7, _5th: 9, _6th: 11, _7th: 13, _8va: 14},
_3aug: {_Uni: 3, _2nd: 5, _3rd: 7, _4th: 8, _5th: 10, _6th: 12, _7th: 14, _8va: 15},
_4aug: {_Uni: 4, _2nd: 6, _3rd: 8, _4th: 9, _5th: 11, _6th: 13, _7th: 15, _8va: 16},
_5aug: {_Uni: 5, _2nd: 7, _3rd: 9, _4th: 10, _5th: 12, _6th: 14, _7th: 16, _8va: 17},
}
## A 2D map that returns the interval quality for a given span and semitonal value.
_span_semi_qual_map = {
_Uni: {-5: _5dim, -4: _4dim, -3: _3dim, -2: _2dim, -1: _dim, 0: _perf,
1: _aug, 2: _2aug, 3: _3aug, 4: _4aug, 5: _5aug},
_2nd: {-4: _5dim, -3: _4dim, -2: _3dim, -1: _2dim, 0: _dim, 1: _min,
2: _maj, 3: _aug, 4: _2aug, 5: _3aug, 6: _4aug, 7: _5aug},
_3rd: {-2: _5dim, -1: _4dim, 0: _3dim, 1: _2dim, 2: _dim, 3: _min,
4: _maj, 5: _aug, 6: _2aug, 7: _3aug, 8: _4aug, 9: _5aug},
_4th: {0: _5dim, 1: _4dim, 2: _3dim, 3: _2dim, 4: _dim, 5: _perf,
6: _aug, 7: _2aug, 8: _3aug, 9: _4aug, 10: _5aug},
_5th: {2: _5dim, 3: _4dim, 4: _3dim, 5: _2dim, 6: _dim, 7: _perf,
8: _aug, 9: _2aug, 10: _3aug, 11: _4aug, 12: _5aug},
_6th: {3: _5dim, 4: _4dim, 5: _3dim, 6: _2dim, 7: _dim, 8: _min,
# 9: _maj, 10: _aug, 11: _2aug, 12: _3aug, 13: _4aug, 14: _5aug},
9: _maj, 10: _aug, 11: _2aug, 12: _3aug, 1: _4aug, 2: _5aug},
_7th: {5: _5dim, 6: _4dim, 7: _3dim, 8: _2dim, 9: _dim, 10: _min,
# 11: _maj, 12: _aug, 13: _2aug, 14: _3aug, 15: _4aug, 16: _5aug,
11: _maj, 12: _aug, 1: _2aug, 2: _3aug, 3: _4aug, 4: _5aug},
_8va: {7: _5dim, 8: _4dim, 9: _3dim, 10: _2dim, 11: _dim, 12: _perf,
# 13: _aug, 14: _2aug, 15: _3aug, 16: _4aug, 17: _5aug,
1: _aug, 2: _2aug, 3: _3aug, 4: _4aug, 5: _5aug}
}
def __new__(cls, arg, other=None):
"""
Creates an Interval from a string, list, or two Pitches.
Calling signatures:
* Interval(string) - creates an Interval from a pitch string.
* Interval([s, q, x, s]) - creates a Pitch from a list of four
integers: a span, quality, extra octaves and sign. (see below).
* Interval(pitch1, pitch2) - creates an Interval from two Pitches.
The format of a Interval string is:
```py
interval = ["-"] , <quality> , <span>
<quality> = <diminished> | <minor> | <perfect> | <major> | <augmented>
<diminished> = <5d> , <4d> , <3d> , <2d> , <1d> ;
<5d> = "ooooo" | "ddddd"
<4d> = "oooo" | "dddd"
<3d> = "ooo" | "ddd"
<2d> = "oo" | "dd"
<1d> = "o" | "d"
<minor> = "m"
<perfect> = "P"
<major> = "M"
<augmented> = <5a>, <4a>, <3a>, <2a>, <1a>
<5d> = "+++++" | "aaaaa"
<4d> = "++++" | "aaaa"
<3d> = "+++" | "aaa"
<2d> = "++" | "aa"
<1d> = "+" | "a"
<span> = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ...
```
The __init__ function should check to make sure the arguments are either a string, a
list of four integers, or two pitches. If the input is a string then __init__ should
pass the string to the the private _init_from_string() method (see below). If the
input is a list of four ints, __init__ will pass them to the private _init_from_list()
method (see below). If the input is two pitches they will be passed to the private
_init_from_pitches() method (see below).
Parameters
----------
arg : string | list
If only arg is specified it should be either an
interval string or a list of four interval indexes. If both
arg and other are provided, both should be a Pitch.
other : Pitch
A Pitch if arg is a Pitch, otherwise None.
Raises
------
TypeError if the input is not a string, list of four integers, or two pitches.
"""
if other is None:
if isinstance(arg, str):
span, qual, xoct, sign = cls._init_from_string(arg)
return super(Interval, cls).__new__(cls, span, qual, xoct, sign)
elif isinstance(arg, list):
if len(arg) == 4 and all(isinstance(a, int) for a in arg):
span, qual, xoct, sign = cls._init_from_list(*arg)
return super(Interval, cls).__new__(cls, span, qual, xoct, sign)
else:
raise TypeError(f'{arg} is an invalid interval list.')
else:
raise TypeError(f'{arg} is an invalid interval reference.')
elif isinstance(arg, Pitch) and isinstance(other, Pitch):
span, qual, xoct, sign = cls._init_from_pitches(arg, other)
return super(Interval, cls).__new__(cls, span, qual, xoct, sign)
else:
raise TypeError(f"Invalid interval specification: {arg} and {other}.")
@classmethod
def _init_from_list(cls, span, qual, xoct, sign):
"""
A private method that checks four integer values (span, qual, xoct, sign) to make sure
they are valid index values for the span, qual, xoct and sign attributes. Legal values
are: span 0-7, qual 0-12, xoct 0-10, sign -1 or 1. If any value is out of range the
method will raise a ValueError for that value. If all values are legal the method will
make the following 'edge case' tests:
* span and quality values cannot produce negative semitones, i.e. an interval
whose 'top' would be lower that its 'bottom'. Here are the smallest VALID
interval for each span that could cause this: perfect unison, diminished-second,
triply-diminished third.
* Only the span of a fifth can be quintuply diminished.
* Only the span of a fourth can be quintuply augmented.
* No interval can surpass 127 semitones, LOL. The last legal intervals are: 'P75'
(a 10 octave perfect 5th), and a 'o76' (a 10 octave diminished 6th).
* If a user specifies an octave as a unison span with 1 extra octave, e.g. [0,*,1,*],
it should be converted to an octave span with 0 extra octaves, e.g. [7,*,0,*]
Only if all the edge case checks pass then _init_from_list() should assign
the four values to the attributes, e.g. self.span=span, self.qual=qual, and
so on. Otherwise if any edge case fails the method should raise a ValueError.
NOTE: The _init_from_list() method should be the only method in your implementation
that assign values to self.letter, self.accidental and self.octave.
"""
if 0 <= span <= 7:
if 0 <= qual <= 12:
if 0 <= xoct:
if sign in (1, -1):
if span in cls._perf_spans:
if qual in [cls._min, cls._maj]:
qn = cls._qual_full_names[qual]
sn = cls._span_full_names[span]
raise ValueError(f"Span '{sn}' is not compatible with quality '{qn}'.")
else:
if qual is cls._perf:
qn = cls._qual_full_names[qual]
sn = cls._span_full_names[span]
raise ValueError(f"Span '{sn}' is not compatible with quality '{qn}'.")
# only 4ths and fifths can be quintuply diminished/augmented
if qual == cls._5dim and span != cls._5th:
raise ValueError(f'{cls._span_full_names[span]}s cannot be quintuply diminished.')
if qual == cls._5aug and span != cls._4th:
raise ValueError(f'{cls._span_full_names[span]}s cannot be quintuply augmented.')
# check semitones to make sure the interval will not be negative or greater than 127
semi = cls._semitones_map[qual][span] + (xoct*12)
# print('init from list: span=', span, 'qual=', qual, 'semi=', semi, 'xoct=', xoct)
if semi < 0:
qn = cls._qual_full_names[qual]
sn = cls._span_full_names[span]
raise ValueError(f"A '{qn}-{sn}' would be negative"
", perhaps you want a descending interval?")
if semi > 127:
raise ValueError("Intervals cannot span more than 127 semitones.")
# respell unisons with 1 extra octave as octaves.
if span == 0 and xoct > 0:
span, xoct = cls._8va, xoct - 1
#self.span, self.qual, self.xoct, self.sign = span, qual, xoct, sign
return span, qual, xoct, sign
else:
raise(ValueError(f"'{sign}' is not an interval sign value 1 or -1."))
else:
raise(ValueError(f"'{xoct}' is not a compound octave value 0-10."))
else:
raise(ValueError(f"'{qual}' is not an interval quality 0-12."))
else:
raise(ValueError(f"'{span}' is not an interval span 0-7."))
@classmethod
def _init_from_string(cls, name):
"""
A private method that accepts an interval string and parses it into four
integer values: span, qual, xoct, sign. If all four values can be parsed
from the string they should be passed to the _init_from_list() method to
check the values and assign them to the instance's attributes. A ValueError
should be raised for any value that cannot be parsed from the string. See:
_init_from_list().
"""
if len(name) < 2:
raise ValueError(f"'{name}' is not a valid interval name.")
start = 0
if name[0] == '-':
sign = -1 # -1 is descending interval
start = 1
else:
sign = 1 # 1 is ascending interval
strlen = len(name)
index = start
# Find extent of quality chars.
while index < strlen and name[index] in "odmMP+A":
index += 1
# Split string into quality and size substrings.
qual = name[start:index]
digi = name[index::]
# digi string must be digits!
if not digi.isdigit():
raise ValueError(f"'{name}' is not a valid interval name.")
# Set the interval's "span" value, i.e. the number
# of lines and spaces it spans (0=Unison...7=Octave).
span = int(digi) - 1
if span < 0:
raise ValueError(f"'{name}' is not a valid interval name name.")
# All intervals are simplified to lie within one octave and the
# octave itself is a simple interval. Compound intervals
# (intervals larger than an octave) are stored simply but have
# their xoct (extra octaves) attribute set to a positive number.
# So a M2 would be span=1 xoct=0 and a M9 would be span=1 xoct=1.
xoct = 0
while span > 7:
span -= 7 # simplify span to 0-7
xoct += 1 # sum number of extra octaves
# Look up the quality value of the name.
qual = cls._qual_map.get(qual, None)
if qual is None:
raise ValueError(f"'{name}' is not a valid interval name.")
return cls._init_from_list(span, qual, xoct, sign)
@classmethod
def _init_from_pitches(cls, pitch1, pitch2):
"""
A private method that determines appropriate span, qual, xoct, sign
values from two pitches. If pitch1 is lower than or equal to pitch2
then an ascending interval is formed (sign=1) otherwise a descending
interval is formed (sign=-1). Once values for sign, span, qual and
xoct have been determined they should be passed to _init_from_list()
to initialize the interval's attributes.
"""
# (1) Determine the sign attribute value. If the left pitch is
# less than or equal to pitch2 then the interval is ascending
# and sign is 1. Otherwise its descending and sign is -1.
sign = 1 if pitch1 <= pitch2 else -1
# (2) Determine the span attribute value. Span measures the
# distance between the two pitch letters (L1 and L2). In an
# ascending interval if L1<=L2 then the span will be L2-L1
# otherwise it will be the complement of the positive distance:
# 8va - (L1-L2). In a descending interval if L1>L2 then the
# interval's span is the positive distance L1-L2 otherwise
# its the complement: 8va - (L2-L1). This can be calculated
# by one expression. In ascending intervals L2-L1 will be
# positive for ascending letters and negative for descending
# letters so mod 7 will produce the complement of the negative
# span.
span = ((pitch2.letter - pitch1.letter) * sign) % 7
# (3) If letters are the same (unison or octave) then use semitones to
# distinguish between unison and octave. The smallest possible octave is 8 semitones so less
# than that must be a unison.
# multiplying semitone difference by sign will ensure
# positive semitones. you cant use abs()
# because that won't work for
semi = (pitch2.keynum() - pitch1.keynum()) * sign
if pitch1.letter == pitch2.letter:
span = cls._Uni if semi < 8 else cls._8va
# determine the number of extra octaves by subtracting
# out octaves from semitones while semitones is greater
# than an octave.
xoct = 0
while semi > 12:
xoct += 1
semi -= 12
## determining quality. the remainder of semitones
qual = cls._span_semi_qual_map[span].get(semi, None)
if qual is None:
raise ValueError(f"{pitch1.string()} and {pitch2.string()}: no quality found for span {span}"
f", semitones {semi} and xoct {xoct}.")
# xoct cleanup for spans whose semitonal content was clipped
# because it is larger than 12 but should not increase xoct.
# For example: +[+++]8va, +[+]7th, ++++6th.
if span == cls._8va:
if qual > cls._perf:
xoct -= 1
semi += 12
elif span == cls._7th:
if qual > cls._aug:
xoct -= 1
semi += 12
elif span == cls._6th:
if qual > cls._3aug:
xoct -= 1
semi += 12
# print('init from pitch: sign=', sign, ', span=', span, ', semi=', semi, ', xoct=', xoct)
return cls._init_from_list(span, qual, xoct, sign)
def __str__(self):
"""
Returns the print representation of the key. Information includes
the the class name, the interval text, the span, qual, xoct and sign
values, and the id of the object. See: string().
Example
-------
`<Interval: oooo8 [7, 1, 0, 1] 0x1075bf6d0>`
"""
return f'<Interval: {self.string()} ' \
f'[{self.span}, {self.qual}, {self.xoct}, {self.sign}] {hex(id(self))}>'
def __repr__(self):
"""
The string the console prints shows the external form.
Example
-------
`Interval("oooo8")`
"""
return f'Interval("{self.string()}")'
def __lt__(self, other):
"""
Implements Interval < Interval.
This method should call self.pos() and other.pos() to get the
values to compare. See: pos().
Parameters
----------
other : Ratio
The interval to compare with this interval.
Returns
-------
True if this interval is less than other.
Raises
------
A TypeError if other is not an Interval.
"""
return self.pos() < other.pos()
def __le__(self, other):
"""
Implements Interval <= Interval.
This method should call self.pos() and other.pos() to get the
values to compare. See: pos().
Parameters
----------
other : Ratio
The interval to compare with this interval.
Returns
-------
True if this interval is less than or equal to other.
Raises
------
A TypeError if other is not an Interval.
"""
return self.pos() <= other.pos()
def __eq__(self, other):
"""
Implements Interval == Interval.
This method should call self.pos() and other.pos() to get the
values to compare. See: pos().
Parameters
----------
other : Ratio
The interval to compare with this interval.
Returns
-------
True if this interval is equal to other.
Raises
------
A TypeError if other is not an Interval.
"""
return self.pos() == other.pos()
def __ne__(self, other):
"""
Implements Interval != Interval.
This method should call self.pos() and other.pos() to get the
values to compare. See: pos().
Parameters
----------
other : Ratio
The interval to compare with this interval.
Returns
-------
True if this interval is not equal to other.
Raises
------
A TypeError if other is not an Interval.
"""
return self.pos() != other.pos()
def __ge__(self, other):
"""
Implements Interval >= Interval.
This method should call self.pos() and other.pos() to get the
values to compare. See: pos().
Parameters
----------
other : Ratio
The interval to compare with this interval.
Returns
-------
True if this interval is greater than or equal to other.
Raises
------
A TypeError if other is not an Interval.
"""
return self.pos() >= other.pos()
def __gt__(self, other):
"""
Implements Interval > Interval.
This method should call self.pos() and other.pos() to get the
values to compare. See: pos().
Parameters
----------
other : Ratio
The interval to compare with this interval.
Returns
-------
True if this interval is greater than the other.
Raises
------
A TypeError if other is not an Interval.
"""
if not isinstance(other, Interval):
raise TypeError(f'{other} is not an Interval.')
return self.pos() > other.pos()
def pos(self):
"""
Returns a numerical value for comparing the size of this interval to
another. The comparison depends on the span, extra octaves, and quality
of the intervals but not their signs. For two intervals, if the span of
the first (including extra octaves) is larger than the second then the
first interval is larger than the second regardless of the quality of
either interval. If the interval spans are the same then the first is
larger than the second if its quality is larger. This value can be
encoded as a 16 bit integer: (((span + (xoct * 7)) + 1) << 8) + qual
"""
return (((self.span + (self.xoct * 7)) + 1) << 8) + self.qual
def string(self):
"""
Returns a string containing the interval name.
For example, Interval('-P5').string() would return '-P5'.
"""
s = "-" if self.sign < 0 else ""
s += self._qual_names[self.qual]
s += str((self.span + (self.xoct * 7)) + 1)
return s
def full_name(self, *, sign=True):
"""
Returns the full interval name, e.g. 'doubly-augmented third'
or 'descending augmented sixth'
Parameters
----------
sign : bool
If true then "descending" will appear in the name if it is a descending interval.
"""
s = 'descending ' if sign and self.sign < 0 else ""
s += self._qual_full_names[self.qual]
s += ' '
s += self._span_full_names[self.span]
return s
def span_name(self):
"""
Returns the full name of the interval's span, e.g. a
unison would return "unison" and so on.
"""
return self._span_full_names[self.span]
def quality_name(self):
"""
Returns the full name of the interval's quality, e.g. a
perfect unison would return "perfect" and so on.
"""
return self._qual_full_names[self.qual]
def matches(self, other):
"""
Returns true if this interval and the other interval have the
same span, quality and sign. The extra octaves are ignored.
Parameters
----------
"""
return self.span == other.span and self.qual == other.qual \
and self.sign == other.sign
def lines_and_spaces(self):
"""
Returns the interval's number of lines and spaces, e.g.
a unison will return 1.
"""
return self.span + 1
def _to_iq(self, name):
"""
Returns a zero based interval quality from its external
string name. Raises an assertion if the name is invalid.
See:is_unison() and similar.
"""
iq = self._qual_map.get(name)
if iq is None:
raise ValueError(f"'{name}' is not a valid interval quality.")
return iq
def to_list(self):
"""
Returns the interval values as a list: [span, qual, xoct, sign]
"""
return [self.span, self.qual, self.xoct, self.sign]
def is_unison(self, qual=None):
"""
Returns true if the interval is a unison otherwise false.
Parameters
----------
qual : string
If specified the predicate tests for that specific quality of unison, which can be
any valid quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
"""
if self.span == self._Uni:
return True if qual is None else self.qual == self._to_iq(qual)
return False
def is_second(self, qual=None):
"""
Returns true if the interval is a second otherwise false.
Parameters
----------
qual : string
If specified the predicate tests for that specific
quality of second, which can be any quality symbol, e.g.
'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
"""
if self.span == self._2nd:
return True if qual is None else self.qual == self._to_iq(qual)
return False
def is_third(self, qual=None):
"""
Returns true if the interval is a third otherwise false.
Parameters
----------
qual : string
If specified the predicate tests for that specific
quality of third, which can be any quality symbol, e.g.
'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
"""
if self.span == self._3rd:
return True if qual is None else self.qual == self._to_iq(qual)
return False
def is_fourth(self, qual=None):
"""
Returns true if the interval is a fourth otherwise false.
Parameters
----------
qual : string
If specified the predicate tests for that specific
quality of fourth, which can be any quality symbol, e.g.
'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
"""
if self.span == self._4th:
return True if qual is None else self.qual == self._to_iq(qual)
return False
def is_fifth(self, qual=None):
"""
Returns true if the interval is a fifth otherwise false.
Parameters
----------
qual : string
If specified the predicate tests for that specific
quality of fifth, which can be any quality symbol, e.g.
'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
"""
if self.span == self._5th:
return True if qual is None else self.qual == self._to_iq(qual)
return False
def is_sixth(self, qual=None):
"""
Returns true if the interval is a sixth otherwise false.
Parameters
----------
qual : string
If specified the predicate tests for that specific
quality of sixth, which can be any quality symbol, e.g.
'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
"""
if self.span == self._6th:
return True if qual is None else self.qual == self._to_iq(qual)
return False
def is_seventh(self, qual=None):
"""
Returns true if the interval is a seventh otherwise false.
Parameters
----------
qual : string
If specified the predicate tests for that specific
quality of seventh, which can be any quality symbol, e.g.
'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
"""
if self.span == self._7th:
return True if qual is None else self.qual == self._to_iq(qual)
return False
def is_octave(self, qual=None):
"""
Returns true if the interval is an octave otherwise false.
Parameters
----------
qual : string
If specified the predicate tests for that specific
quality of octave, which can be any quality symbol, e.g.
'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
"""
if self.span == self._8va:
return True if qual is None else self.qual == self._to_iq(qual)
return False
def is_diminished(self):
"""
Returns a 'diminution count' 1-5 if the interval is diminished else False.
For example, if the interval is doubly-diminished then 2 is returned.
If the interval not diminished at all (e.g. is perfect, augmented, minor or
major) then False is returned.
"""
return self._dim - self.qual + 1 if self.qual <= self._dim else False
def is_minor(self):
"""
Returns true if the interval is minor, otherwise false.
"""
return self.qual == self._min
def is_perfect(self):
"""
Returns true if the interval is perfect, otherwise false.
"""
return self.qual == self._perf
def is_major(self):
"""
Returns true if the interval is major, otherwise false.
"""
return self.qual == self._maj
def is_augmented(self):
"""
Returns a 'augmentation count' 1-5 if the interval is augmented else False.
For example, if the interval is doubly-augmented then 2 is returned.
If the interval not augmented at all (e.g. is perfect, diminished, minor or
major) then False is returned.
"""
return 5 + (self.qual - self._5aug) if self.qual >= self._aug else False
def is_perfect_type(self):
"""
Returns true if the interval belongs to the 'perfect interval'
family, i.e. it is a Unison, 4th, 5th, or Octave.
"""
return self.span in [self._Uni, self._4th, self._5th, self._8va]
def is_imperfect_type(self):
"""
Returns true if this interval belongs to the 'imperfect interval'
family, i.e. it is a 2nd, 3rd, 6th, or 7th.
"""
return not self.is_perfect_type() # 2nd, 3rd, 6th, 7th
def is_simple(self):
"""
Returns true if this is a simple interval, i.e. its span is
less-than-or-equal to an octave.
"""
return self.xoct == 0 # simple means no extra octaves
def is_compound(self):
"""
Returns true if this is a compound interval, i.e. its span is
more than an octave (an octave is a simple interval).
"""
return not self.is_simple()
def is_ascending(self):
"""
Returns true if this interval's sign is 1.
"""
return self.sign == 1
def is_descending(self):
"""
Returns true if this interval's sign is -1.
"""
return self.sign == -1
def is_consonant(self):
"""
Returns true if the interval is a consonant interval otherwise False.
The perfect fourth should be considered consonant.
"""
if self.is_perfect_type():
return self.qual == self._perf
else:
return self.span in [self._3rd, self._6th] \
and self.qual in [self._min, self._maj]
def is_dissonant(self):
"""
Returns True if the interval is not a consonant interval otherwise False.
"""
return not self.is_consonant()
def complemented(self):
"""
Returns a complemented copy of the interval.
To complement an interval you invert its span and quality. To invert
the span, subtract it from the maximum span index (the octave index).
To invert the quality subtract it from the maximum quality index
(quintuply augmented).
"""
# new = copy.copy(self)
# new.complement()
# return new
return Interval([self._8va-self.span, self._5aug-self.qual, self.xoct, self.sign])
def semitones(self):
"""
Returns the number of semitones in the interval.
It is possible to determine the number of semitones by looking at the span and
quality indexes. For example, if the span is a perfect fifth
(span index 4) and the quality is perfect (quality index 6)
then the semitones will be 5 and augmented or diminished fifths
will add or subtract semitones accordingly.
The semitones will be negative for descending intervals otherwise positive.
"""
semi = self._semitones_map[self.qual][self.span]
return (semi + (self.xoct * 12)) * self.sign
def add(self, other):
"""
Adds a specified interval to this interval.
Parameters
----------
other : Interval
The interval to add to this one.
Returns
-------
A new interval expressing the total span of both intervals.
Raises
------
* A TypeError if other is not an interval.
* A NotImplementedError if either intervals are descending.
"""
if self.sign < 1 or other.sign < 1:
raise ValueError("Only ascending intervals may be added.")
newspan = self.span + other.span
newsemi = self.semitones() + other.semitones()
newxoct = 0
while newsemi > 12:
newspan -= 7
newsemi -= 12
newxoct += 1
# get newspan's number of semitones in the diatonic octave
assert 0 <= newspan < len(self._diatonic_semitones), f"invalid added span value: {newspan}."
diasemi = self._diatonic_semitones[newspan]
# Qualities of the diatonic spans -- Unison to Octave
diaqual = [self._perf, self._maj, self._maj, self._perf, self._perf,
self._maj, self._maj, self._perf][newspan]
# calculate the difference in semitones between the new interval and its diatonic version
semidif = newsemi - diasemi
# print('diasemi=', diasemi, 'diaqual=', diaqual, "semidif=", semidif)
# add that difference to the diatonic quality to calculate the new quality
if newspan in self._perf_spans:
newqual = self._perf_quals[self._perf_quals.index(diaqual) + semidif]
else:
newqual = self._impf_quals[self._impf_quals.index(diaqual) + semidif]
return Interval([newspan, newqual, newxoct, self.sign*other.sign])
def transpose(self, p):
"""
Transposes a Pitch or Pnum by the interval. Pnum transposition
has no octave or direction so if the interval is negative its
complement should be used and octaves should reduce to unisons
without complementing.
Parameters
----------
p : Pitch | Pnum
The Pitch or Pnum to transpose, otherwise the string name of a pitch or pnum (eg. 'Cb5' or 'Dff')
Returns
----------
The transposed Pitch or Pnum.
Raises
------
A TypeError if p is not a Pitch or Pnum.
"""
if isinstance(p, str) and len(str) > 0:
p = Pitch(p) if p[-1].isdigit() else Pitch.pnums[p]
if isinstance(p, Pitch):
return self._transpose_pitch(p)
if isinstance(p, Pitch.pnums):
return self._transpose_pnum(p)
raise TypeError(f"'{p}' is not a Pitch or Pnum.")
def _transpose_pnum(self, pnum):
"""
from mus.interval import Interval, _test_intervals
from mus.pitch import Pitch
Interval('m7').transpose(Pitch.pnums.E)
Interval('-M2').transpose(Pitch.pnums.E)
"""
def let_to_name(let): return f"{['C', 'D', 'E', 'F', 'G', 'A', 'B'][let]}"
def acc_to_name(acc): return f"{['bb', 'b', 'n', '#', '##', ][acc]}"
def acc_to_size(acc): return acc - 2 # size of accidental (bb=-2)
def size_to_acc(acc): return acc + 2
def let_to_size(let): return self._diatonic_semitones[let]
span = self.span
qual = self.qual
# semi will be negative for descending intervals
semi = self.semitones()
desc = self.is_descending()
# pnum space has no octaves so collapse octaves to unisons
if span == self._8va:
# collapse octaves but preserve quality, so an augmented octave
# becomes an augmented unison and NOT a diminished unison.
span, qual = self._Uni, qual
semi = self._semitones_map[qual][span]
# complement descending interval including its negative semitones
if desc:
# print("descending! old semi=", semi, "new semi=", semi % 12)
span, qual, semi = self._8va - span, self._5aug - qual, semi % 12
old_let = (pnum.value & 0xF0) >> 4
old_acc = (pnum.value & 0xF)
# letter of new pitch
new_let = (old_let + span) % self._8va
# semitonal size between the natural (diatonic) letters
nat_size = (let_to_size(new_let) - let_to_size(old_let)) % 12
# semitonal size of the interval
int_size = semi
### print(f'interval {self.string()}: span', span, 'qual', qual, 'sign', self.sign, 'semi', int_size)
# semitonal shift of the old accidental (where b= -1, ##= 2 etc.)
old_acc_siz = acc_to_size(old_acc)
# adjust the semitonal size of the natural interval by the size of the old accident
adj_nat_siz = nat_size - old_acc_siz
# subtract the adjusted natural size from the interval size to determine the size
# of the new accidental
new_acc_size = int_size - adj_nat_siz
### print(f'old pitch {pnum.name}: old_let', old_let, f'({let_to_name(old_let)})', 'old_acc', old_acc,
### f'({acc_to_name(old_acc)}))', 'old_acc_size', old_acc_siz, 'nat_size', nat_size,
### 'int_size', int_size, 'adj_nat_size', adj_nat_siz, 'new_acc_size', new_acc_size)
# invalid size means an impossible transposition, e.g. shifting F## by +2
if not (-2 <= new_acc_size <= 2):
raise ValueError(f"Transposition of '{pnum.name}' by '{self.string()}': "
"no pitch spelling possible.")
# adjust the size of the next accidental
new_acc = size_to_acc(new_acc_size)
new_pnum = Pitch.pnums(new_let << 4 | new_acc)
### print('new_acc', new_acc, f'({acc_to_name(new_acc)}))','new pitch', new_pnum, '\n')
return new_pnum
def _transpose_pitch(self, pitch):
"""
"""
pitch_let = pitch.letter
pitch_acc = pitch.accidental
pitch_oct = pitch.octave
# print('pitch_let', pitch_let, 'pitch_acc', pitch_acc, 'pitch_oct', pitch_oct)
# get and transpose the pnum
pnum = self._transpose_pnum(pitch.pnums(pitch_let << 4 | pitch_acc))
trans_let = (pnum.value & 0xF0) >> 4
trans_acc = (pnum.value & 0xF)
# transposed keynum is original pitch + interval semitones
trans_key = pitch.keynum() + self.semitones()
trans_oct = trans_key // 12
# decrement octave if note is B# or B##, increment if Cbb or Cb
if trans_let == pitch._let_B and trans_acc in [pitch._acc_s, pitch._acc_2s]:
trans_oct -= 1
elif trans_let == pitch._let_C and trans_acc in [pitch._acc_f, pitch._acc_2f]:
trans_oct += 1
return Pitch([trans_let, trans_acc, trans_oct])
# from mus.interval import Interval, _test_intervals
def _test_intervals():
print('Testing interval.py ... ', end='')
assert (Interval('M3') < Interval('o4'))
assert (Interval('M3') <= Interval('A3'))
assert (Interval('M3') <= Interval('M3'))
assert not (Interval('M3') > Interval('M3'))
assert (Interval('oo4') > Interval('M3'))
assert not (Interval('m3') != Interval('m3'))
assert 0 == Interval('P1').semitones()
assert 1 == Interval('+1').semitones()
assert 2 == Interval('++1').semitones()
assert 3 == Interval('+++1').semitones()
assert 4 == Interval('++++1').semitones()
assert 4 == Interval('M3').semitones()
assert 3 == Interval('m3').semitones()
assert 2 == Interval('o3').semitones()
assert 1 == Interval('oo3').semitones()
assert 0 == Interval('ooo3').semitones()
assert 0 == Interval('o2').semitones()
assert 1 == Interval('m2').semitones()
assert 2 == Interval('M2').semitones()
assert 3 == Interval('+2').semitones()
assert 4 == Interval('++2').semitones()
assert 5 == Interval('+++2').semitones()
assert 6 == Interval('++++2').semitones()
assert 12 == Interval('P8').semitones()
assert 11 == Interval('o8').semitones()
assert 10 == Interval('oo8').semitones()
assert 9 == Interval('ooo8').semitones()
assert 8 == Interval('oooo8').semitones()
assert 13 == Interval('+8').semitones()
assert 14 == Interval('++8').semitones()
assert 15 == Interval('+++8').semitones()
assert 16 == Interval('++++8').semitones()
assert 9 == Interval('oooo9').semitones()
assert 10 == Interval('ooo9').semitones()
assert 11 == Interval('oo9').semitones()
assert 12 == Interval('o9').semitones()
assert 13 == Interval('m9').semitones()
assert 14 == Interval('M9').semitones()
assert 15 == Interval('+9').semitones()
assert 16 == Interval('++9').semitones()
assert 17 == Interval('+++9').semitones()
assert 18 == Interval('++++9').semitones()
assert Interval('P8').is_simple()
assert Interval('+8').is_simple()
assert Interval('++8').is_simple()
assert Interval('+++8').is_simple()
assert Interval('++++8').is_simple()
assert Interval('m9').is_compound()
assert Interval('o9').is_compound()
assert Interval('oo9').is_compound()
assert Interval('ooo9').is_compound()
assert Interval('oooo9').is_compound()
assert Interval("P1").is_perfect()
assert not Interval("A1").is_perfect()
assert Interval("+1").is_perfect_type()
assert not Interval("P1").is_imperfect_type()
assert Interval("P4").is_perfect()
assert not Interval("A4").is_perfect()
assert Interval("+++++4").is_perfect_type()
assert not Interval("P4").is_imperfect_type()
assert Interval("P5").is_perfect()
assert not Interval("A5").is_perfect()
assert Interval("ddddd5").is_perfect_type()
assert not Interval("P5").is_imperfect_type()
assert Interval("P8").is_perfect()
assert not Interval("o8").is_perfect()
assert Interval("oooo8").is_perfect_type()
assert not Interval("P8").is_imperfect_type()
assert 1 == Interval("+8").is_augmented()
assert 3 == Interval("+++3").is_augmented()
assert 1 == Interval("o8").is_diminished()
assert 3 == Interval("ooo3").is_diminished()
assert 5 == Interval("ddddd5").is_diminished()
assert 4 == Interval("AAAA5").is_augmented()
assert not Interval("P5").is_diminished()
assert not Interval("AAAA5").is_diminished()
assert not Interval("P5").is_augmented()
assert not Interval("oooo5").is_augmented()
assert Interval('P1').is_consonant()
assert Interval('P4').is_consonant()
assert Interval('P5').is_consonant()
assert Interval('P8').is_consonant()
assert Interval('m3').is_consonant()
assert Interval('M6').is_consonant()
assert not Interval('+1').is_consonant()
assert not Interval('+++++4').is_consonant()
assert not Interval('ooooo5').is_consonant()
assert not Interval('o8').is_consonant()
assert not Interval('oo3').is_consonant()
assert not Interval('+6').is_consonant()
assert not Interval('P1').is_dissonant()
assert not Interval('P4').is_dissonant()
assert not Interval('P5').is_dissonant()
assert not Interval('P8').is_dissonant()
assert not Interval('m3').is_dissonant()
assert not Interval('M6').is_dissonant()
assert Interval('+1').is_dissonant()
assert Interval('+++++4').is_dissonant()
assert Interval('ooooo5').is_dissonant()
assert Interval('o8').is_dissonant()
assert Interval('oo3').is_dissonant()
assert Interval('+6').is_dissonant()
assert Interval('P1').is_unison()
assert Interval('+1').is_unison()
assert Interval('+1').is_unison('A')
assert Interval('+1').is_unison('+')
assert not Interval('+1').is_unison('o')
assert not Interval('P8').is_unison()
assert not Interval('m2').is_unison()
assert Interval('+8').is_octave()
assert Interval('+8').is_octave('A')
assert Interval('+8').is_octave('+')
assert not Interval('+8').is_octave('P')
assert not Interval('P1').is_octave()
assert not Interval('m2').is_octave()
assert Interval('+2').is_second('A')
assert Interval('+2').is_second('+')
assert not Interval('+2').is_second('m')
assert Interval('m2').is_second()
assert Interval('M2').is_second()
assert Interval('ddd9').is_second()
assert not Interval('m3').is_second()
assert Interval('+3').is_third('A')
assert Interval('+3').is_third('+')
assert not Interval('+3').is_third('++')
assert Interval('m3').is_third()
assert Interval('M3').is_third()
assert Interval('++10').is_third()
assert not Interval('m2').is_third()
assert Interval('+4').is_fourth('A')
assert Interval('+4').is_fourth('+')
assert not Interval('+4').is_fourth('oo')
assert Interval('P4').is_fourth()
assert Interval('d11').is_fourth()
assert not Interval('m2').is_fourth()
assert Interval('+5').is_fifth('A')
assert Interval('+5').is_fifth('+')
assert not Interval('+5').is_fifth('oo')
assert Interval('P5').is_fifth()
assert Interval('A12').is_fifth()
assert not Interval('m3').is_fifth()
assert Interval('M6').is_sixth('M')
assert Interval('+6').is_sixth('+')
assert not Interval('+6').is_sixth('oo')
assert Interval('+6').is_sixth()
assert Interval('m13').is_sixth()
assert not Interval('+5').is_sixth()
assert Interval('m7').is_seventh('m')
assert Interval('+7').is_seventh('+')
assert not Interval('+7').is_seventh('oo')
assert Interval('+7').is_seventh()
assert Interval('+++14').is_seventh()
assert not Interval('+3').is_seventh()
# SHOULD BE OK
assert 'Interval("-M2")' == Interval(Pitch("Cs4"), Pitch("B3")).__repr__()
assert 'Interval("m7")' == Interval(Pitch("Cs4"), Pitch("B4")).__repr__()
assert 'Interval("-M2")' == Interval(Pitch('E4'), Pitch('D4')).__repr__()
assert 'Interval("-m2")' == Interval(Pitch('E4'), Pitch('D#4')).__repr__()
assert 'Interval("-M2")' == Interval(Pitch("Cs4"), Pitch("B3")).__repr__()
assert 'Interval("m7")' == Interval(Pitch("Cs4"), Pitch("B4")).__repr__()
assert 'Interval("-M9")' == Interval(Pitch("Cs4"), Pitch("B2")).__repr__()
assert 'Interval("m14")' == Interval(Pitch("Cs4"), Pitch("B5")).__repr__()
assert 'Interval("-o14")' == Interval(Pitch("Bb5"), Pitch("Cs4")).__repr__()
assert 'Interval("P1")' == Interval(Pitch("B3"), Pitch("B3")).__repr__()
assert 'Interval("P8")' == Interval(Pitch("B3"), Pitch("B4")).__repr__()
assert 'Interval("-P8")' == Interval(Pitch("B3"), Pitch("B2")).__repr__()
assert 'Interval("+1")' == Interval(Pitch("B3"), Pitch("Bs3")).__repr__()
assert 'Interval("-+1")' == Interval(Pitch("B3"), Pitch("Bb3")).__repr__()
assert 'Interval("-o2")' == Interval(Pitch('Eb4'), Pitch('D#4')).__repr__()
assert 'Interval("ooo3")' == Interval(Pitch("B##3"), Pitch("Db4")).__repr__()
assert 'Interval("o2")' == Interval(Pitch("B#3"), Pitch("C4")).__repr__()
assert 'Interval("-+1")' == Interval(Pitch("B3"), Pitch("Bb3")).__repr__()
assert 'Interval("-o2")' == Interval(Pitch('Eb4'), Pitch('D#4')).__repr__()
assert 'Interval("+7")' == Interval(Pitch('Eb4'), Pitch('D#5')).__repr__()
assert 'Interval("++7")' == Interval(Pitch('Ef3'), Pitch('D##4')).__repr__()
assert 'Interval("+8")' == Interval(Pitch("C4"), Pitch("C#5")).__repr__()
assert 'Interval("++8")' == Interval(Pitch("C4"), Pitch("C##5")).__repr__()
assert 'Interval("+++8")' == Interval(Pitch("Cb4"), Pitch("C##5")).__repr__()
assert 'Interval("++++8")' == Interval(Pitch("Cbb4"), Pitch("C##5")).__repr__()
assert 'Interval("++++15")' == Interval(Pitch("Cbb4"), Pitch("C##6")).__repr__()
assert 'Interval("-+8")' == Interval(Pitch("C#5"), Pitch("C4")).__repr__()
assert 'Interval("-++8")' == Interval(Pitch("C##5"), Pitch("C4")).__repr__()
assert 'Interval("-+++8")' == Interval(Pitch("C##5"), Pitch("Cb4")).__repr__()
assert 'Interval("-++++8")' == Interval(Pitch("C##5"), Pitch("Cbb4")).__repr__()
assert 'Interval("-++++15")' == Interval(Pitch("C##5"), Pitch("Cbb3")).__repr__()
assert 'Interval("m7")' == Interval(Pitch("E4"), Pitch("D5")).__repr__()
assert 'Interval("M7")' == Interval(Pitch("E4"), Pitch("D#5")).__repr__()
assert 'Interval("+7")' == Interval(Pitch("E4"), Pitch("D##5")).__repr__()
assert 'Interval("++7")' == Interval(Pitch("Eb4"), Pitch("D##5")).__repr__()
assert 'Interval("+++7")' == Interval(Pitch("Ebb4"), Pitch("D##5")).__repr__()
assert 'Interval("M6")' == Interval(Pitch("F4"), Pitch("D5")).__repr__()
assert 'Interval("+6")' == Interval(Pitch("F4"), Pitch("D#5")).__repr__()
assert 'Interval("++6")' == Interval(Pitch("F4"), Pitch("D##5")).__repr__()
assert 'Interval("+++6")' == Interval(Pitch("Fb4"), Pitch("D##5")).__repr__()
assert 'Interval("++++6")' == Interval(Pitch("Fbb4"), Pitch("D##5")).__repr__()
assert 'Interval("+4")' == Interval(Pitch("F4"), Pitch("B4")).__repr__()
assert 'Interval("++4")' == Interval(Pitch("F4"), Pitch("B#4")).__repr__()
assert 'Interval("+++4")' == Interval(Pitch("F4"), Pitch("B##4")).__repr__()
assert 'Interval("++++4")' == Interval(Pitch("Fb4"), Pitch("B##4")).__repr__()
assert 'Interval("+++++4")' == Interval(Pitch("Fbb4"), Pitch("B##4")).__repr__()
assert 'Interval("-++++4")' == Interval(Pitch("B#4"), Pitch("Fbb4")).__repr__()
assert 'Interval("o5")' == Interval(Pitch("B4"), Pitch("F5")).__repr__()
assert 'Interval("ooo5")' == Interval(Pitch("B4"), Pitch("Fbb5")).__repr__()
assert 'Interval("oooo5")' == Interval(Pitch("B#4"), Pitch("Fbb5")).__repr__()
assert 'Interval("ooooo5")' == Interval(Pitch("B##4"), Pitch("Fbb5")).__repr__()
assert 'Interval("-ooooo5")' == Interval(Pitch("Fbb5"), Pitch("B##4")).__repr__()
assert 'Interval("P75")' == Interval(Pitch("C00"), Pitch("G9")).__repr__()
assert 'Interval("++74")' == Interval(Pitch("C00"), Pitch("F##9")).__repr__()
assert 'Interval("o76")' == Interval(Pitch("C00"), Pitch("Abb9")).__repr__()
assert [7, 6, 0, 1] == Interval([0, 6, 1, 1]).to_list() # oct written as unison + 1 xoct.
assert [7, 6, 1, 1] == Interval([0, 6, 2, 1]).to_list() # oct written as unison + 1 xoct.
assert Pitch.pnums.Bss == Interval("M7").transpose(Pitch.pnums.Css)
assert Pitch.pnums.Cff == Interval("m2").transpose(Pitch.pnums.Bff)
assert Pitch.pnums.Css == Interval("+1").transpose(Pitch.pnums.Cs)
assert Pitch.pnums.Gf == Interval("m3").transpose(Pitch.pnums.Ef)
assert Pitch.pnums.As == Interval("+6").transpose(Pitch.pnums.C)
assert Pitch.pnums.Bff == Interval("M7").transpose(Pitch.pnums.Cff)
# WORKING
assert 'Fff' == Interval('P1').transpose(Pitch.pnums.Fff).name
assert 'Ff' == Interval('+1').transpose(Pitch.pnums.Fff).name
assert 'F' == Interval('++1').transpose(Pitch.pnums.Fff).name
assert 'Fs' == Interval('+++1').transpose(Pitch.pnums.Fff).name
assert 'Fss' == Interval('++++1').transpose(Pitch.pnums.Fff).name
# EXPLICITLY DISALLOWED
# Interval('P1').transpose(Pitch.pnums.Fss)
# Interval('o1').transpose(Pitch.pnums.Fss)
# Interval('oo1').transpose(Pitch.pnums.Fss)
# Interval('ooo1').transpose(Pitch.pnums.Fss)
# Interval('oooo1').transpose(Pitch.pnums.Fss)
assert 'Fff' == Interval('P8').transpose(Pitch.pnums.Fff).name
assert 'Ff' == Interval('+8').transpose(Pitch.pnums.Fff).name
assert 'F' == Interval('++8').transpose(Pitch.pnums.Fff).name
assert 'Fs' == Interval('+++8').transpose(Pitch.pnums.Fff).name
assert 'Fss' == Interval('++++8').transpose(Pitch.pnums.Fff).name
assert 'Fss' == Interval('P8').transpose(Pitch.pnums.Fss).name
assert 'Fs' == Interval('o8').transpose(Pitch.pnums.Fss).name
assert 'F' == Interval('oo8').transpose(Pitch.pnums.Fss).name
assert 'Ff' == Interval('ooo8').transpose(Pitch.pnums.Fss).name
assert 'Fff' == Interval('oooo8').transpose(Pitch.pnums.Fss).name
assert 'Fs' == Interval('A8').transpose(Pitch.pnums.F).name
assert 'Fs' == Interval('-A8').transpose(Pitch.pnums.F).name
assert 'Pitch("G4")' == Interval("M2").transpose(Pitch('F4')).__repr__()
assert 'Pitch("F4")' == Interval("P1").transpose(Pitch('F4')).__repr__()
assert 'Pitch("F5")' == Interval("P8").transpose(Pitch('F4')).__repr__()
assert 'Pitch("F3")' == Interval("-P8").transpose(Pitch('F4')).__repr__()
assert 'Pitch("F#5")' == Interval("A8").transpose(Pitch('F4')).__repr__()
assert 'Pitch("Fb5")' == Interval("d8").transpose(Pitch('F4')).__repr__()
# BUG!!! THIS DOES NOT WORK. MAYBE THE
# Interval('-A1').transpose(Pitch.pnums.F)
# ERRORS
msg1, msg2 = "Received wrong type of exception.", \
"Expected exception did not happen."
# TYPE ERRORS
err = TypeError
try: Interval([])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([0, 6])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([0, 6, 0])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval(123.0)
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval((3, 6, 0, 1))
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval(Pitch("c4"), 1.0)
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval(1.0, Pitch("c4"))
except err: pass
except: assert False, msg1
else: assert False, msg2
# VALUE ERRORS
err = ValueError
try: Interval('M0')
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval('1')
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval('X1')
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("+++")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("+-+5")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("M1")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("M4")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("M5")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("M8")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("m1")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("m4")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("m5")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("m8")
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval('o1')
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval('oo2')
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval('oooo3')
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval('oooo3')
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval(Pitch("C00"), Pitch("G#9"))
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval(Pitch("B#3"), Pitch("Cb4"))
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval(Pitch("B##3"), Pitch("Cbb4"))
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval(Pitch("B##3"), Pitch("Cbb4"))
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval(Pitch("B##3"), Pitch("Dbb4"))
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([0, 6, 0, 0])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([8, 0, 0, 1])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([0, 13, 0, 1])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([0, 6, -1, 1])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([0, 6, 11, 1])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([0, 1, 0, 1])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([0, 2, 0, 1])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([0, 4, 0, 1])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([5, 6, 10, 1])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval([4, 6, 11, 1])
except err: pass
except: assert False, msg1
else: assert False, msg2
try: Interval("m7").transpose(Pitch.pnums.Cff)
except err: pass
except: assert False, msg1
else: assert False, msg2
# try: XXX
# except err: pass
# except: assert False, msg1
# else: assert False, msg2
print('Done!')
if __name__ == '__main__':
_test_intervals()
'''
from random import randint, choice
from mus import Pitch, Interval
def rand():
s = randint(0, 7)
q = choice([4, 6, 8]) if s in [0, 3, 4, 7] else choice([4, 5, 7, 8])
return [s, q, 0, 1]
y=rand()
for s in [1,4,5,8]:
for q in ["ooooo", "oooo", "ooo", "oo", "o", "P", "+", "++", "+++", "++++", "+++++"]:
x = Interval(q+str(s))
y = x.semitones()
z = x.semitones2()
print(y,z)
assert y == z , "OOPS"
for s in [2,3,6,7]:
for q in ["ooooo", "oooo", "ooo", "oo", "o", "m", "M", "+", "++", "+++", "++++", "+++++"]:
x = Interval(q+str(s))
y = x.semitones()
z = x.semitones2()
print(y,z)
assert y == z , "OOPS"
for s in [2,3,6,7]:
for q in ["ooooo", "oooo", "ooo", "oo", "o", "m", "M", "+", "++", "+++", "++++", "+++++"]:
print(Interval(q+str(s)))
'''
Classes
class Interval (arg, other=None)
-
IntervalBase(span, qual, xoct, sign)
Expand source code
class Interval (IntervalBase): ## Private class constants representing spans. There are eight interval # spans ranging 0-7: _Uni=Unison, _2nd=Second, _3rd=Third, ... _8va=Octave _Uni, _2nd, _3rd, _4th, _5th, _6th, _7th, _8va = (i for i in range(8)) ## Private class constants representing qualities. There are thirteen # qualities ranging 0-13: from quintuply-diminished intervals to # quintuply-augmented intervals. _5dim, _4dim, _3dim, _2dim, _dim, _min, _perf, _maj, _aug, _2aug, _3aug, _4aug, _5aug \ = (i for i in range(13)) ## Private map of all possible interval quality names to their constants. # Note that diminished can use either "o" or "d", and augmented # can use "+" or "A". _qual_map = {"ooooo": _5dim, "oooo": _4dim, "ooo": _3dim, "oo": _2dim, "o": _dim, "ddddd": _5dim, "dddd": _4dim, "ddd": _3dim, "dd": _2dim, "d": _dim, "m": _min, "P": _perf, "M": _maj, "+": _aug, "++": _2aug, "+++": _3aug, "++++": _4aug, "+++++": _5aug, "A": _aug, "AA": _2aug, "AAA": _3aug, "AAAA": _4aug, "AAAAA": _5aug} ## Reverse map from quality constants 0-12 onto their canonical names. _qual_names = ["ooooo", "oooo", "ooo", "oo", "o", "m", "P", "M", "+", "++", "+++", "++++", "+++++"] ## Reverse map from span constants to span full names _span_full_names = ['unison', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'octave'] ## Reverse map from quality constants to quality full names _qual_full_names = ['quintuply-diminished', 'quadruply-diminished', 'triply-diminished', 'doubly-diminished', 'diminished', 'minor', 'perfect', 'major', 'augmented', 'doubly-augmented', 'triply-augmented', 'quadruply-augmented', 'quintuply-augmented'] ## Ordered list of all possible perfect interval span values. _perf_spans = [_Uni, _4th, _5th, _8va] ## Ordered list of all possible imperfect interval span values. _impf_spans = [_2nd, _3rd, _6th, _7th] ## Ordered list of all possible imperfect interval qualities. _impf_quals = [_5dim, _4dim, _3dim, _2dim, _dim, _min, _maj, _aug, _2aug, _3aug, _4aug, _5aug] ## Ordered list of all possible perfect interval qualities. _perf_quals = [_5dim, _4dim, _3dim, _2dim, _dim, _perf, _aug, _2aug, _3aug, _4aug, _5aug] ## Reverse map from (diatonic) span values to their semitone content. _diatonic_semitones = [0, 2, 4, 5, 7, 9, 11, 12] ## A 2D map that returns the semitones for a given quality and span value. _semitones_map = { _min: {_2nd: 1, _3rd: 3, _6th: 8, _7th: 10}, _maj: {_2nd: 2, _3rd: 4, _6th: 9, _7th: 11}, _perf: {_Uni: 0, _4th: 5, _5th: 7, _8va: 12}, _dim: {_Uni: -1, _2nd: 0, _3rd: 2, _4th: 4, _5th: 6, _6th: 7, _7th: 9, _8va: 11}, _2dim: {_Uni: -2, _2nd: -1, _3rd: 1, _4th: 3, _5th: 5, _6th: 6, _7th: 8, _8va: 10}, _3dim: {_Uni: -3, _2nd: -2, _3rd: 0, _4th: 2, _5th: 4, _6th: 5, _7th: 7, _8va: 9}, _4dim: {_Uni: -4, _2nd: -3, _3rd: -1, _4th: 1, _5th: 3, _6th: 4, _7th: 6, _8va: 8}, _5dim: {_Uni: -5, _2nd: -4, _3rd: -2, _4th: 0, _5th: 2, _6th: 3, _7th: 5, _8va: 7}, _aug: {_Uni: 1, _2nd: 3, _3rd: 5, _4th: 6, _5th: 8, _6th: 10, _7th: 12, _8va: 13}, _2aug: {_Uni: 2, _2nd: 4, _3rd: 6, _4th: 7, _5th: 9, _6th: 11, _7th: 13, _8va: 14}, _3aug: {_Uni: 3, _2nd: 5, _3rd: 7, _4th: 8, _5th: 10, _6th: 12, _7th: 14, _8va: 15}, _4aug: {_Uni: 4, _2nd: 6, _3rd: 8, _4th: 9, _5th: 11, _6th: 13, _7th: 15, _8va: 16}, _5aug: {_Uni: 5, _2nd: 7, _3rd: 9, _4th: 10, _5th: 12, _6th: 14, _7th: 16, _8va: 17}, } ## A 2D map that returns the interval quality for a given span and semitonal value. _span_semi_qual_map = { _Uni: {-5: _5dim, -4: _4dim, -3: _3dim, -2: _2dim, -1: _dim, 0: _perf, 1: _aug, 2: _2aug, 3: _3aug, 4: _4aug, 5: _5aug}, _2nd: {-4: _5dim, -3: _4dim, -2: _3dim, -1: _2dim, 0: _dim, 1: _min, 2: _maj, 3: _aug, 4: _2aug, 5: _3aug, 6: _4aug, 7: _5aug}, _3rd: {-2: _5dim, -1: _4dim, 0: _3dim, 1: _2dim, 2: _dim, 3: _min, 4: _maj, 5: _aug, 6: _2aug, 7: _3aug, 8: _4aug, 9: _5aug}, _4th: {0: _5dim, 1: _4dim, 2: _3dim, 3: _2dim, 4: _dim, 5: _perf, 6: _aug, 7: _2aug, 8: _3aug, 9: _4aug, 10: _5aug}, _5th: {2: _5dim, 3: _4dim, 4: _3dim, 5: _2dim, 6: _dim, 7: _perf, 8: _aug, 9: _2aug, 10: _3aug, 11: _4aug, 12: _5aug}, _6th: {3: _5dim, 4: _4dim, 5: _3dim, 6: _2dim, 7: _dim, 8: _min, # 9: _maj, 10: _aug, 11: _2aug, 12: _3aug, 13: _4aug, 14: _5aug}, 9: _maj, 10: _aug, 11: _2aug, 12: _3aug, 1: _4aug, 2: _5aug}, _7th: {5: _5dim, 6: _4dim, 7: _3dim, 8: _2dim, 9: _dim, 10: _min, # 11: _maj, 12: _aug, 13: _2aug, 14: _3aug, 15: _4aug, 16: _5aug, 11: _maj, 12: _aug, 1: _2aug, 2: _3aug, 3: _4aug, 4: _5aug}, _8va: {7: _5dim, 8: _4dim, 9: _3dim, 10: _2dim, 11: _dim, 12: _perf, # 13: _aug, 14: _2aug, 15: _3aug, 16: _4aug, 17: _5aug, 1: _aug, 2: _2aug, 3: _3aug, 4: _4aug, 5: _5aug} } def __new__(cls, arg, other=None): """ Creates an Interval from a string, list, or two Pitches. Calling signatures: * Interval(string) - creates an Interval from a pitch string. * Interval([s, q, x, s]) - creates a Pitch from a list of four integers: a span, quality, extra octaves and sign. (see below). * Interval(pitch1, pitch2) - creates an Interval from two Pitches. The format of a Interval string is: ```py interval = ["-"] , <quality> , <span> <quality> = <diminished> | <minor> | <perfect> | <major> | <augmented> <diminished> = <5d> , <4d> , <3d> , <2d> , <1d> ; <5d> = "ooooo" | "ddddd" <4d> = "oooo" | "dddd" <3d> = "ooo" | "ddd" <2d> = "oo" | "dd" <1d> = "o" | "d" <minor> = "m" <perfect> = "P" <major> = "M" <augmented> = <5a>, <4a>, <3a>, <2a>, <1a> <5d> = "+++++" | "aaaaa" <4d> = "++++" | "aaaa" <3d> = "+++" | "aaa" <2d> = "++" | "aa" <1d> = "+" | "a" <span> = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ... ``` The __init__ function should check to make sure the arguments are either a string, a list of four integers, or two pitches. If the input is a string then __init__ should pass the string to the the private _init_from_string() method (see below). If the input is a list of four ints, __init__ will pass them to the private _init_from_list() method (see below). If the input is two pitches they will be passed to the private _init_from_pitches() method (see below). Parameters ---------- arg : string | list If only arg is specified it should be either an interval string or a list of four interval indexes. If both arg and other are provided, both should be a Pitch. other : Pitch A Pitch if arg is a Pitch, otherwise None. Raises ------ TypeError if the input is not a string, list of four integers, or two pitches. """ if other is None: if isinstance(arg, str): span, qual, xoct, sign = cls._init_from_string(arg) return super(Interval, cls).__new__(cls, span, qual, xoct, sign) elif isinstance(arg, list): if len(arg) == 4 and all(isinstance(a, int) for a in arg): span, qual, xoct, sign = cls._init_from_list(*arg) return super(Interval, cls).__new__(cls, span, qual, xoct, sign) else: raise TypeError(f'{arg} is an invalid interval list.') else: raise TypeError(f'{arg} is an invalid interval reference.') elif isinstance(arg, Pitch) and isinstance(other, Pitch): span, qual, xoct, sign = cls._init_from_pitches(arg, other) return super(Interval, cls).__new__(cls, span, qual, xoct, sign) else: raise TypeError(f"Invalid interval specification: {arg} and {other}.") @classmethod def _init_from_list(cls, span, qual, xoct, sign): """ A private method that checks four integer values (span, qual, xoct, sign) to make sure they are valid index values for the span, qual, xoct and sign attributes. Legal values are: span 0-7, qual 0-12, xoct 0-10, sign -1 or 1. If any value is out of range the method will raise a ValueError for that value. If all values are legal the method will make the following 'edge case' tests: * span and quality values cannot produce negative semitones, i.e. an interval whose 'top' would be lower that its 'bottom'. Here are the smallest VALID interval for each span that could cause this: perfect unison, diminished-second, triply-diminished third. * Only the span of a fifth can be quintuply diminished. * Only the span of a fourth can be quintuply augmented. * No interval can surpass 127 semitones, LOL. The last legal intervals are: 'P75' (a 10 octave perfect 5th), and a 'o76' (a 10 octave diminished 6th). * If a user specifies an octave as a unison span with 1 extra octave, e.g. [0,*,1,*], it should be converted to an octave span with 0 extra octaves, e.g. [7,*,0,*] Only if all the edge case checks pass then _init_from_list() should assign the four values to the attributes, e.g. self.span=span, self.qual=qual, and so on. Otherwise if any edge case fails the method should raise a ValueError. NOTE: The _init_from_list() method should be the only method in your implementation that assign values to self.letter, self.accidental and self.octave. """ if 0 <= span <= 7: if 0 <= qual <= 12: if 0 <= xoct: if sign in (1, -1): if span in cls._perf_spans: if qual in [cls._min, cls._maj]: qn = cls._qual_full_names[qual] sn = cls._span_full_names[span] raise ValueError(f"Span '{sn}' is not compatible with quality '{qn}'.") else: if qual is cls._perf: qn = cls._qual_full_names[qual] sn = cls._span_full_names[span] raise ValueError(f"Span '{sn}' is not compatible with quality '{qn}'.") # only 4ths and fifths can be quintuply diminished/augmented if qual == cls._5dim and span != cls._5th: raise ValueError(f'{cls._span_full_names[span]}s cannot be quintuply diminished.') if qual == cls._5aug and span != cls._4th: raise ValueError(f'{cls._span_full_names[span]}s cannot be quintuply augmented.') # check semitones to make sure the interval will not be negative or greater than 127 semi = cls._semitones_map[qual][span] + (xoct*12) # print('init from list: span=', span, 'qual=', qual, 'semi=', semi, 'xoct=', xoct) if semi < 0: qn = cls._qual_full_names[qual] sn = cls._span_full_names[span] raise ValueError(f"A '{qn}-{sn}' would be negative" ", perhaps you want a descending interval?") if semi > 127: raise ValueError("Intervals cannot span more than 127 semitones.") # respell unisons with 1 extra octave as octaves. if span == 0 and xoct > 0: span, xoct = cls._8va, xoct - 1 #self.span, self.qual, self.xoct, self.sign = span, qual, xoct, sign return span, qual, xoct, sign else: raise(ValueError(f"'{sign}' is not an interval sign value 1 or -1.")) else: raise(ValueError(f"'{xoct}' is not a compound octave value 0-10.")) else: raise(ValueError(f"'{qual}' is not an interval quality 0-12.")) else: raise(ValueError(f"'{span}' is not an interval span 0-7.")) @classmethod def _init_from_string(cls, name): """ A private method that accepts an interval string and parses it into four integer values: span, qual, xoct, sign. If all four values can be parsed from the string they should be passed to the _init_from_list() method to check the values and assign them to the instance's attributes. A ValueError should be raised for any value that cannot be parsed from the string. See: _init_from_list(). """ if len(name) < 2: raise ValueError(f"'{name}' is not a valid interval name.") start = 0 if name[0] == '-': sign = -1 # -1 is descending interval start = 1 else: sign = 1 # 1 is ascending interval strlen = len(name) index = start # Find extent of quality chars. while index < strlen and name[index] in "odmMP+A": index += 1 # Split string into quality and size substrings. qual = name[start:index] digi = name[index::] # digi string must be digits! if not digi.isdigit(): raise ValueError(f"'{name}' is not a valid interval name.") # Set the interval's "span" value, i.e. the number # of lines and spaces it spans (0=Unison...7=Octave). span = int(digi) - 1 if span < 0: raise ValueError(f"'{name}' is not a valid interval name name.") # All intervals are simplified to lie within one octave and the # octave itself is a simple interval. Compound intervals # (intervals larger than an octave) are stored simply but have # their xoct (extra octaves) attribute set to a positive number. # So a M2 would be span=1 xoct=0 and a M9 would be span=1 xoct=1. xoct = 0 while span > 7: span -= 7 # simplify span to 0-7 xoct += 1 # sum number of extra octaves # Look up the quality value of the name. qual = cls._qual_map.get(qual, None) if qual is None: raise ValueError(f"'{name}' is not a valid interval name.") return cls._init_from_list(span, qual, xoct, sign) @classmethod def _init_from_pitches(cls, pitch1, pitch2): """ A private method that determines appropriate span, qual, xoct, sign values from two pitches. If pitch1 is lower than or equal to pitch2 then an ascending interval is formed (sign=1) otherwise a descending interval is formed (sign=-1). Once values for sign, span, qual and xoct have been determined they should be passed to _init_from_list() to initialize the interval's attributes. """ # (1) Determine the sign attribute value. If the left pitch is # less than or equal to pitch2 then the interval is ascending # and sign is 1. Otherwise its descending and sign is -1. sign = 1 if pitch1 <= pitch2 else -1 # (2) Determine the span attribute value. Span measures the # distance between the two pitch letters (L1 and L2). In an # ascending interval if L1<=L2 then the span will be L2-L1 # otherwise it will be the complement of the positive distance: # 8va - (L1-L2). In a descending interval if L1>L2 then the # interval's span is the positive distance L1-L2 otherwise # its the complement: 8va - (L2-L1). This can be calculated # by one expression. In ascending intervals L2-L1 will be # positive for ascending letters and negative for descending # letters so mod 7 will produce the complement of the negative # span. span = ((pitch2.letter - pitch1.letter) * sign) % 7 # (3) If letters are the same (unison or octave) then use semitones to # distinguish between unison and octave. The smallest possible octave is 8 semitones so less # than that must be a unison. # multiplying semitone difference by sign will ensure # positive semitones. you cant use abs() # because that won't work for semi = (pitch2.keynum() - pitch1.keynum()) * sign if pitch1.letter == pitch2.letter: span = cls._Uni if semi < 8 else cls._8va # determine the number of extra octaves by subtracting # out octaves from semitones while semitones is greater # than an octave. xoct = 0 while semi > 12: xoct += 1 semi -= 12 ## determining quality. the remainder of semitones qual = cls._span_semi_qual_map[span].get(semi, None) if qual is None: raise ValueError(f"{pitch1.string()} and {pitch2.string()}: no quality found for span {span}" f", semitones {semi} and xoct {xoct}.") # xoct cleanup for spans whose semitonal content was clipped # because it is larger than 12 but should not increase xoct. # For example: +[+++]8va, +[+]7th, ++++6th. if span == cls._8va: if qual > cls._perf: xoct -= 1 semi += 12 elif span == cls._7th: if qual > cls._aug: xoct -= 1 semi += 12 elif span == cls._6th: if qual > cls._3aug: xoct -= 1 semi += 12 # print('init from pitch: sign=', sign, ', span=', span, ', semi=', semi, ', xoct=', xoct) return cls._init_from_list(span, qual, xoct, sign) def __str__(self): """ Returns the print representation of the key. Information includes the the class name, the interval text, the span, qual, xoct and sign values, and the id of the object. See: string(). Example ------- `<Interval: oooo8 [7, 1, 0, 1] 0x1075bf6d0>` """ return f'<Interval: {self.string()} ' \ f'[{self.span}, {self.qual}, {self.xoct}, {self.sign}] {hex(id(self))}>' def __repr__(self): """ The string the console prints shows the external form. Example ------- `Interval("oooo8")` """ return f'Interval("{self.string()}")' def __lt__(self, other): """ Implements Interval < Interval. This method should call self.pos() and other.pos() to get the values to compare. See: pos(). Parameters ---------- other : Ratio The interval to compare with this interval. Returns ------- True if this interval is less than other. Raises ------ A TypeError if other is not an Interval. """ return self.pos() < other.pos() def __le__(self, other): """ Implements Interval <= Interval. This method should call self.pos() and other.pos() to get the values to compare. See: pos(). Parameters ---------- other : Ratio The interval to compare with this interval. Returns ------- True if this interval is less than or equal to other. Raises ------ A TypeError if other is not an Interval. """ return self.pos() <= other.pos() def __eq__(self, other): """ Implements Interval == Interval. This method should call self.pos() and other.pos() to get the values to compare. See: pos(). Parameters ---------- other : Ratio The interval to compare with this interval. Returns ------- True if this interval is equal to other. Raises ------ A TypeError if other is not an Interval. """ return self.pos() == other.pos() def __ne__(self, other): """ Implements Interval != Interval. This method should call self.pos() and other.pos() to get the values to compare. See: pos(). Parameters ---------- other : Ratio The interval to compare with this interval. Returns ------- True if this interval is not equal to other. Raises ------ A TypeError if other is not an Interval. """ return self.pos() != other.pos() def __ge__(self, other): """ Implements Interval >= Interval. This method should call self.pos() and other.pos() to get the values to compare. See: pos(). Parameters ---------- other : Ratio The interval to compare with this interval. Returns ------- True if this interval is greater than or equal to other. Raises ------ A TypeError if other is not an Interval. """ return self.pos() >= other.pos() def __gt__(self, other): """ Implements Interval > Interval. This method should call self.pos() and other.pos() to get the values to compare. See: pos(). Parameters ---------- other : Ratio The interval to compare with this interval. Returns ------- True if this interval is greater than the other. Raises ------ A TypeError if other is not an Interval. """ if not isinstance(other, Interval): raise TypeError(f'{other} is not an Interval.') return self.pos() > other.pos() def pos(self): """ Returns a numerical value for comparing the size of this interval to another. The comparison depends on the span, extra octaves, and quality of the intervals but not their signs. For two intervals, if the span of the first (including extra octaves) is larger than the second then the first interval is larger than the second regardless of the quality of either interval. If the interval spans are the same then the first is larger than the second if its quality is larger. This value can be encoded as a 16 bit integer: (((span + (xoct * 7)) + 1) << 8) + qual """ return (((self.span + (self.xoct * 7)) + 1) << 8) + self.qual def string(self): """ Returns a string containing the interval name. For example, Interval('-P5').string() would return '-P5'. """ s = "-" if self.sign < 0 else "" s += self._qual_names[self.qual] s += str((self.span + (self.xoct * 7)) + 1) return s def full_name(self, *, sign=True): """ Returns the full interval name, e.g. 'doubly-augmented third' or 'descending augmented sixth' Parameters ---------- sign : bool If true then "descending" will appear in the name if it is a descending interval. """ s = 'descending ' if sign and self.sign < 0 else "" s += self._qual_full_names[self.qual] s += ' ' s += self._span_full_names[self.span] return s def span_name(self): """ Returns the full name of the interval's span, e.g. a unison would return "unison" and so on. """ return self._span_full_names[self.span] def quality_name(self): """ Returns the full name of the interval's quality, e.g. a perfect unison would return "perfect" and so on. """ return self._qual_full_names[self.qual] def matches(self, other): """ Returns true if this interval and the other interval have the same span, quality and sign. The extra octaves are ignored. Parameters ---------- """ return self.span == other.span and self.qual == other.qual \ and self.sign == other.sign def lines_and_spaces(self): """ Returns the interval's number of lines and spaces, e.g. a unison will return 1. """ return self.span + 1 def _to_iq(self, name): """ Returns a zero based interval quality from its external string name. Raises an assertion if the name is invalid. See:is_unison() and similar. """ iq = self._qual_map.get(name) if iq is None: raise ValueError(f"'{name}' is not a valid interval quality.") return iq def to_list(self): """ Returns the interval values as a list: [span, qual, xoct, sign] """ return [self.span, self.qual, self.xoct, self.sign] def is_unison(self, qual=None): """ Returns true if the interval is a unison otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of unison, which can be any valid quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._Uni: return True if qual is None else self.qual == self._to_iq(qual) return False def is_second(self, qual=None): """ Returns true if the interval is a second otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of second, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._2nd: return True if qual is None else self.qual == self._to_iq(qual) return False def is_third(self, qual=None): """ Returns true if the interval is a third otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of third, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._3rd: return True if qual is None else self.qual == self._to_iq(qual) return False def is_fourth(self, qual=None): """ Returns true if the interval is a fourth otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of fourth, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._4th: return True if qual is None else self.qual == self._to_iq(qual) return False def is_fifth(self, qual=None): """ Returns true if the interval is a fifth otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of fifth, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._5th: return True if qual is None else self.qual == self._to_iq(qual) return False def is_sixth(self, qual=None): """ Returns true if the interval is a sixth otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of sixth, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._6th: return True if qual is None else self.qual == self._to_iq(qual) return False def is_seventh(self, qual=None): """ Returns true if the interval is a seventh otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of seventh, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._7th: return True if qual is None else self.qual == self._to_iq(qual) return False def is_octave(self, qual=None): """ Returns true if the interval is an octave otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of octave, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._8va: return True if qual is None else self.qual == self._to_iq(qual) return False def is_diminished(self): """ Returns a 'diminution count' 1-5 if the interval is diminished else False. For example, if the interval is doubly-diminished then 2 is returned. If the interval not diminished at all (e.g. is perfect, augmented, minor or major) then False is returned. """ return self._dim - self.qual + 1 if self.qual <= self._dim else False def is_minor(self): """ Returns true if the interval is minor, otherwise false. """ return self.qual == self._min def is_perfect(self): """ Returns true if the interval is perfect, otherwise false. """ return self.qual == self._perf def is_major(self): """ Returns true if the interval is major, otherwise false. """ return self.qual == self._maj def is_augmented(self): """ Returns a 'augmentation count' 1-5 if the interval is augmented else False. For example, if the interval is doubly-augmented then 2 is returned. If the interval not augmented at all (e.g. is perfect, diminished, minor or major) then False is returned. """ return 5 + (self.qual - self._5aug) if self.qual >= self._aug else False def is_perfect_type(self): """ Returns true if the interval belongs to the 'perfect interval' family, i.e. it is a Unison, 4th, 5th, or Octave. """ return self.span in [self._Uni, self._4th, self._5th, self._8va] def is_imperfect_type(self): """ Returns true if this interval belongs to the 'imperfect interval' family, i.e. it is a 2nd, 3rd, 6th, or 7th. """ return not self.is_perfect_type() # 2nd, 3rd, 6th, 7th def is_simple(self): """ Returns true if this is a simple interval, i.e. its span is less-than-or-equal to an octave. """ return self.xoct == 0 # simple means no extra octaves def is_compound(self): """ Returns true if this is a compound interval, i.e. its span is more than an octave (an octave is a simple interval). """ return not self.is_simple() def is_ascending(self): """ Returns true if this interval's sign is 1. """ return self.sign == 1 def is_descending(self): """ Returns true if this interval's sign is -1. """ return self.sign == -1 def is_consonant(self): """ Returns true if the interval is a consonant interval otherwise False. The perfect fourth should be considered consonant. """ if self.is_perfect_type(): return self.qual == self._perf else: return self.span in [self._3rd, self._6th] \ and self.qual in [self._min, self._maj] def is_dissonant(self): """ Returns True if the interval is not a consonant interval otherwise False. """ return not self.is_consonant() def complemented(self): """ Returns a complemented copy of the interval. To complement an interval you invert its span and quality. To invert the span, subtract it from the maximum span index (the octave index). To invert the quality subtract it from the maximum quality index (quintuply augmented). """ # new = copy.copy(self) # new.complement() # return new return Interval([self._8va-self.span, self._5aug-self.qual, self.xoct, self.sign]) def semitones(self): """ Returns the number of semitones in the interval. It is possible to determine the number of semitones by looking at the span and quality indexes. For example, if the span is a perfect fifth (span index 4) and the quality is perfect (quality index 6) then the semitones will be 5 and augmented or diminished fifths will add or subtract semitones accordingly. The semitones will be negative for descending intervals otherwise positive. """ semi = self._semitones_map[self.qual][self.span] return (semi + (self.xoct * 12)) * self.sign def add(self, other): """ Adds a specified interval to this interval. Parameters ---------- other : Interval The interval to add to this one. Returns ------- A new interval expressing the total span of both intervals. Raises ------ * A TypeError if other is not an interval. * A NotImplementedError if either intervals are descending. """ if self.sign < 1 or other.sign < 1: raise ValueError("Only ascending intervals may be added.") newspan = self.span + other.span newsemi = self.semitones() + other.semitones() newxoct = 0 while newsemi > 12: newspan -= 7 newsemi -= 12 newxoct += 1 # get newspan's number of semitones in the diatonic octave assert 0 <= newspan < len(self._diatonic_semitones), f"invalid added span value: {newspan}." diasemi = self._diatonic_semitones[newspan] # Qualities of the diatonic spans -- Unison to Octave diaqual = [self._perf, self._maj, self._maj, self._perf, self._perf, self._maj, self._maj, self._perf][newspan] # calculate the difference in semitones between the new interval and its diatonic version semidif = newsemi - diasemi # print('diasemi=', diasemi, 'diaqual=', diaqual, "semidif=", semidif) # add that difference to the diatonic quality to calculate the new quality if newspan in self._perf_spans: newqual = self._perf_quals[self._perf_quals.index(diaqual) + semidif] else: newqual = self._impf_quals[self._impf_quals.index(diaqual) + semidif] return Interval([newspan, newqual, newxoct, self.sign*other.sign]) def transpose(self, p): """ Transposes a Pitch or Pnum by the interval. Pnum transposition has no octave or direction so if the interval is negative its complement should be used and octaves should reduce to unisons without complementing. Parameters ---------- p : Pitch | Pnum The Pitch or Pnum to transpose, otherwise the string name of a pitch or pnum (eg. 'Cb5' or 'Dff') Returns ---------- The transposed Pitch or Pnum. Raises ------ A TypeError if p is not a Pitch or Pnum. """ if isinstance(p, str) and len(str) > 0: p = Pitch(p) if p[-1].isdigit() else Pitch.pnums[p] if isinstance(p, Pitch): return self._transpose_pitch(p) if isinstance(p, Pitch.pnums): return self._transpose_pnum(p) raise TypeError(f"'{p}' is not a Pitch or Pnum.") def _transpose_pnum(self, pnum): """ from mus.interval import Interval, _test_intervals from mus.pitch import Pitch Interval('m7').transpose(Pitch.pnums.E) Interval('-M2').transpose(Pitch.pnums.E) """ def let_to_name(let): return f"{['C', 'D', 'E', 'F', 'G', 'A', 'B'][let]}" def acc_to_name(acc): return f"{['bb', 'b', 'n', '#', '##', ][acc]}" def acc_to_size(acc): return acc - 2 # size of accidental (bb=-2) def size_to_acc(acc): return acc + 2 def let_to_size(let): return self._diatonic_semitones[let] span = self.span qual = self.qual # semi will be negative for descending intervals semi = self.semitones() desc = self.is_descending() # pnum space has no octaves so collapse octaves to unisons if span == self._8va: # collapse octaves but preserve quality, so an augmented octave # becomes an augmented unison and NOT a diminished unison. span, qual = self._Uni, qual semi = self._semitones_map[qual][span] # complement descending interval including its negative semitones if desc: # print("descending! old semi=", semi, "new semi=", semi % 12) span, qual, semi = self._8va - span, self._5aug - qual, semi % 12 old_let = (pnum.value & 0xF0) >> 4 old_acc = (pnum.value & 0xF) # letter of new pitch new_let = (old_let + span) % self._8va # semitonal size between the natural (diatonic) letters nat_size = (let_to_size(new_let) - let_to_size(old_let)) % 12 # semitonal size of the interval int_size = semi ### print(f'interval {self.string()}: span', span, 'qual', qual, 'sign', self.sign, 'semi', int_size) # semitonal shift of the old accidental (where b= -1, ##= 2 etc.) old_acc_siz = acc_to_size(old_acc) # adjust the semitonal size of the natural interval by the size of the old accident adj_nat_siz = nat_size - old_acc_siz # subtract the adjusted natural size from the interval size to determine the size # of the new accidental new_acc_size = int_size - adj_nat_siz ### print(f'old pitch {pnum.name}: old_let', old_let, f'({let_to_name(old_let)})', 'old_acc', old_acc, ### f'({acc_to_name(old_acc)}))', 'old_acc_size', old_acc_siz, 'nat_size', nat_size, ### 'int_size', int_size, 'adj_nat_size', adj_nat_siz, 'new_acc_size', new_acc_size) # invalid size means an impossible transposition, e.g. shifting F## by +2 if not (-2 <= new_acc_size <= 2): raise ValueError(f"Transposition of '{pnum.name}' by '{self.string()}': " "no pitch spelling possible.") # adjust the size of the next accidental new_acc = size_to_acc(new_acc_size) new_pnum = Pitch.pnums(new_let << 4 | new_acc) ### print('new_acc', new_acc, f'({acc_to_name(new_acc)}))','new pitch', new_pnum, '\n') return new_pnum def _transpose_pitch(self, pitch): """ """ pitch_let = pitch.letter pitch_acc = pitch.accidental pitch_oct = pitch.octave # print('pitch_let', pitch_let, 'pitch_acc', pitch_acc, 'pitch_oct', pitch_oct) # get and transpose the pnum pnum = self._transpose_pnum(pitch.pnums(pitch_let << 4 | pitch_acc)) trans_let = (pnum.value & 0xF0) >> 4 trans_acc = (pnum.value & 0xF) # transposed keynum is original pitch + interval semitones trans_key = pitch.keynum() + self.semitones() trans_oct = trans_key // 12 # decrement octave if note is B# or B##, increment if Cbb or Cb if trans_let == pitch._let_B and trans_acc in [pitch._acc_s, pitch._acc_2s]: trans_oct -= 1 elif trans_let == pitch._let_C and trans_acc in [pitch._acc_f, pitch._acc_2f]: trans_oct += 1 return Pitch([trans_let, trans_acc, trans_oct])
Ancestors
- musx.interval.IntervalBase
- builtins.tuple
Methods
def add(self, other)
-
Adds a specified interval to this interval.
Parameters
other
:Interval
- The interval to add to this one.
Returns
A new interval expressing the total span of both intervals.
Raises
- A TypeError if other is not an interval.
- A NotImplementedError if either intervals are descending.
Expand source code
def add(self, other): """ Adds a specified interval to this interval. Parameters ---------- other : Interval The interval to add to this one. Returns ------- A new interval expressing the total span of both intervals. Raises ------ * A TypeError if other is not an interval. * A NotImplementedError if either intervals are descending. """ if self.sign < 1 or other.sign < 1: raise ValueError("Only ascending intervals may be added.") newspan = self.span + other.span newsemi = self.semitones() + other.semitones() newxoct = 0 while newsemi > 12: newspan -= 7 newsemi -= 12 newxoct += 1 # get newspan's number of semitones in the diatonic octave assert 0 <= newspan < len(self._diatonic_semitones), f"invalid added span value: {newspan}." diasemi = self._diatonic_semitones[newspan] # Qualities of the diatonic spans -- Unison to Octave diaqual = [self._perf, self._maj, self._maj, self._perf, self._perf, self._maj, self._maj, self._perf][newspan] # calculate the difference in semitones between the new interval and its diatonic version semidif = newsemi - diasemi # print('diasemi=', diasemi, 'diaqual=', diaqual, "semidif=", semidif) # add that difference to the diatonic quality to calculate the new quality if newspan in self._perf_spans: newqual = self._perf_quals[self._perf_quals.index(diaqual) + semidif] else: newqual = self._impf_quals[self._impf_quals.index(diaqual) + semidif] return Interval([newspan, newqual, newxoct, self.sign*other.sign])
def complemented(self)
-
Returns a complemented copy of the interval.
To complement an interval you invert its span and quality. To invert the span, subtract it from the maximum span index (the octave index). To invert the quality subtract it from the maximum quality index (quintuply augmented).
Expand source code
def complemented(self): """ Returns a complemented copy of the interval. To complement an interval you invert its span and quality. To invert the span, subtract it from the maximum span index (the octave index). To invert the quality subtract it from the maximum quality index (quintuply augmented). """ # new = copy.copy(self) # new.complement() # return new return Interval([self._8va-self.span, self._5aug-self.qual, self.xoct, self.sign])
def full_name(self, *, sign=True)
-
Returns the full interval name, e.g. 'doubly-augmented third' or 'descending augmented sixth'
Parameters
sign
:bool
- If true then "descending" will appear in the name if it is a descending interval.
Expand source code
def full_name(self, *, sign=True): """ Returns the full interval name, e.g. 'doubly-augmented third' or 'descending augmented sixth' Parameters ---------- sign : bool If true then "descending" will appear in the name if it is a descending interval. """ s = 'descending ' if sign and self.sign < 0 else "" s += self._qual_full_names[self.qual] s += ' ' s += self._span_full_names[self.span] return s
def is_ascending(self)
-
Returns true if this interval's sign is 1.
Expand source code
def is_ascending(self): """ Returns true if this interval's sign is 1. """ return self.sign == 1
def is_augmented(self)
-
Returns a 'augmentation count' 1-5 if the interval is augmented else False. For example, if the interval is doubly-augmented then 2 is returned. If the interval not augmented at all (e.g. is perfect, diminished, minor or major) then False is returned.
Expand source code
def is_augmented(self): """ Returns a 'augmentation count' 1-5 if the interval is augmented else False. For example, if the interval is doubly-augmented then 2 is returned. If the interval not augmented at all (e.g. is perfect, diminished, minor or major) then False is returned. """ return 5 + (self.qual - self._5aug) if self.qual >= self._aug else False
def is_compound(self)
-
Returns true if this is a compound interval, i.e. its span is more than an octave (an octave is a simple interval).
Expand source code
def is_compound(self): """ Returns true if this is a compound interval, i.e. its span is more than an octave (an octave is a simple interval). """ return not self.is_simple()
def is_consonant(self)
-
Returns true if the interval is a consonant interval otherwise False. The perfect fourth should be considered consonant.
Expand source code
def is_consonant(self): """ Returns true if the interval is a consonant interval otherwise False. The perfect fourth should be considered consonant. """ if self.is_perfect_type(): return self.qual == self._perf else: return self.span in [self._3rd, self._6th] \ and self.qual in [self._min, self._maj]
def is_descending(self)
-
Returns true if this interval's sign is -1.
Expand source code
def is_descending(self): """ Returns true if this interval's sign is -1. """ return self.sign == -1
def is_diminished(self)
-
Returns a 'diminution count' 1-5 if the interval is diminished else False. For example, if the interval is doubly-diminished then 2 is returned. If the interval not diminished at all (e.g. is perfect, augmented, minor or major) then False is returned.
Expand source code
def is_diminished(self): """ Returns a 'diminution count' 1-5 if the interval is diminished else False. For example, if the interval is doubly-diminished then 2 is returned. If the interval not diminished at all (e.g. is perfect, augmented, minor or major) then False is returned. """ return self._dim - self.qual + 1 if self.qual <= self._dim else False
def is_dissonant(self)
-
Returns True if the interval is not a consonant interval otherwise False.
Expand source code
def is_dissonant(self): """ Returns True if the interval is not a consonant interval otherwise False. """ return not self.is_consonant()
def is_fifth(self, qual=None)
-
Returns true if the interval is a fifth otherwise false.
Parameters
qual
:string
- If specified the predicate tests for that specific quality of fifth, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
Expand source code
def is_fifth(self, qual=None): """ Returns true if the interval is a fifth otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of fifth, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._5th: return True if qual is None else self.qual == self._to_iq(qual) return False
def is_fourth(self, qual=None)
-
Returns true if the interval is a fourth otherwise false.
Parameters
qual
:string
- If specified the predicate tests for that specific quality of fourth, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
Expand source code
def is_fourth(self, qual=None): """ Returns true if the interval is a fourth otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of fourth, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._4th: return True if qual is None else self.qual == self._to_iq(qual) return False
def is_imperfect_type(self)
-
Returns true if this interval belongs to the 'imperfect interval' family, i.e. it is a 2nd, 3rd, 6th, or 7th.
Expand source code
def is_imperfect_type(self): """ Returns true if this interval belongs to the 'imperfect interval' family, i.e. it is a 2nd, 3rd, 6th, or 7th. """ return not self.is_perfect_type() # 2nd, 3rd, 6th, 7th
def is_major(self)
-
Returns true if the interval is major, otherwise false.
Expand source code
def is_major(self): """ Returns true if the interval is major, otherwise false. """ return self.qual == self._maj
def is_minor(self)
-
Returns true if the interval is minor, otherwise false.
Expand source code
def is_minor(self): """ Returns true if the interval is minor, otherwise false. """ return self.qual == self._min
def is_octave(self, qual=None)
-
Returns true if the interval is an octave otherwise false.
Parameters
qual
:string
- If specified the predicate tests for that specific quality of octave, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
Expand source code
def is_octave(self, qual=None): """ Returns true if the interval is an octave otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of octave, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._8va: return True if qual is None else self.qual == self._to_iq(qual) return False
def is_perfect(self)
-
Returns true if the interval is perfect, otherwise false.
Expand source code
def is_perfect(self): """ Returns true if the interval is perfect, otherwise false. """ return self.qual == self._perf
def is_perfect_type(self)
-
Returns true if the interval belongs to the 'perfect interval' family, i.e. it is a Unison, 4th, 5th, or Octave.
Expand source code
def is_perfect_type(self): """ Returns true if the interval belongs to the 'perfect interval' family, i.e. it is a Unison, 4th, 5th, or Octave. """ return self.span in [self._Uni, self._4th, self._5th, self._8va]
def is_second(self, qual=None)
-
Returns true if the interval is a second otherwise false.
Parameters
qual
:string
- If specified the predicate tests for that specific quality of second, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
Expand source code
def is_second(self, qual=None): """ Returns true if the interval is a second otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of second, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._2nd: return True if qual is None else self.qual == self._to_iq(qual) return False
def is_seventh(self, qual=None)
-
Returns true if the interval is a seventh otherwise false.
Parameters
qual
:string
- If specified the predicate tests for that specific quality of seventh, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
Expand source code
def is_seventh(self, qual=None): """ Returns true if the interval is a seventh otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of seventh, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._7th: return True if qual is None else self.qual == self._to_iq(qual) return False
def is_simple(self)
-
Returns true if this is a simple interval, i.e. its span is less-than-or-equal to an octave.
Expand source code
def is_simple(self): """ Returns true if this is a simple interval, i.e. its span is less-than-or-equal to an octave. """ return self.xoct == 0 # simple means no extra octaves
def is_sixth(self, qual=None)
-
Returns true if the interval is a sixth otherwise false.
Parameters
qual
:string
- If specified the predicate tests for that specific quality of sixth, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
Expand source code
def is_sixth(self, qual=None): """ Returns true if the interval is a sixth otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of sixth, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._6th: return True if qual is None else self.qual == self._to_iq(qual) return False
def is_third(self, qual=None)
-
Returns true if the interval is a third otherwise false.
Parameters
qual
:string
- If specified the predicate tests for that specific quality of third, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
Expand source code
def is_third(self, qual=None): """ Returns true if the interval is a third otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of third, which can be any quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._3rd: return True if qual is None else self.qual == self._to_iq(qual) return False
def is_unison(self, qual=None)
-
Returns true if the interval is a unison otherwise false.
Parameters
qual
:string
- If specified the predicate tests for that specific quality of unison, which can be any valid quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq().
Expand source code
def is_unison(self, qual=None): """ Returns true if the interval is a unison otherwise false. Parameters ---------- qual : string If specified the predicate tests for that specific quality of unison, which can be any valid quality symbol, e.g. 'P', 'M' 'm' 'd' 'A' 'o' '+' and so on. See: _to_iq(). """ if self.span == self._Uni: return True if qual is None else self.qual == self._to_iq(qual) return False
def lines_and_spaces(self)
-
Returns the interval's number of lines and spaces, e.g. a unison will return 1.
Expand source code
def lines_and_spaces(self): """ Returns the interval's number of lines and spaces, e.g. a unison will return 1. """ return self.span + 1
def matches(self, other)
-
Returns true if this interval and the other interval have the same span, quality and sign. The extra octaves are ignored. Parameters
Expand source code
def matches(self, other): """ Returns true if this interval and the other interval have the same span, quality and sign. The extra octaves are ignored. Parameters ---------- """ return self.span == other.span and self.qual == other.qual \ and self.sign == other.sign
def pos(self)
-
Returns a numerical value for comparing the size of this interval to another. The comparison depends on the span, extra octaves, and quality of the intervals but not their signs. For two intervals, if the span of the first (including extra octaves) is larger than the second then the first interval is larger than the second regardless of the quality of either interval. If the interval spans are the same then the first is larger than the second if its quality is larger. This value can be encoded as a 16 bit integer: (((span + (xoct * 7)) + 1) << 8) + qual
Expand source code
def pos(self): """ Returns a numerical value for comparing the size of this interval to another. The comparison depends on the span, extra octaves, and quality of the intervals but not their signs. For two intervals, if the span of the first (including extra octaves) is larger than the second then the first interval is larger than the second regardless of the quality of either interval. If the interval spans are the same then the first is larger than the second if its quality is larger. This value can be encoded as a 16 bit integer: (((span + (xoct * 7)) + 1) << 8) + qual """ return (((self.span + (self.xoct * 7)) + 1) << 8) + self.qual
def quality_name(self)
-
Returns the full name of the interval's quality, e.g. a perfect unison would return "perfect" and so on.
Expand source code
def quality_name(self): """ Returns the full name of the interval's quality, e.g. a perfect unison would return "perfect" and so on. """ return self._qual_full_names[self.qual]
def semitones(self)
-
Returns the number of semitones in the interval.
It is possible to determine the number of semitones by looking at the span and quality indexes. For example, if the span is a perfect fifth (span index 4) and the quality is perfect (quality index 6) then the semitones will be 5 and augmented or diminished fifths will add or subtract semitones accordingly.
The semitones will be negative for descending intervals otherwise positive.
Expand source code
def semitones(self): """ Returns the number of semitones in the interval. It is possible to determine the number of semitones by looking at the span and quality indexes. For example, if the span is a perfect fifth (span index 4) and the quality is perfect (quality index 6) then the semitones will be 5 and augmented or diminished fifths will add or subtract semitones accordingly. The semitones will be negative for descending intervals otherwise positive. """ semi = self._semitones_map[self.qual][self.span] return (semi + (self.xoct * 12)) * self.sign
def span_name(self)
-
Returns the full name of the interval's span, e.g. a unison would return "unison" and so on.
Expand source code
def span_name(self): """ Returns the full name of the interval's span, e.g. a unison would return "unison" and so on. """ return self._span_full_names[self.span]
def string(self)
-
Returns a string containing the interval name. For example, Interval('-P5').string() would return '-P5'.
Expand source code
def string(self): """ Returns a string containing the interval name. For example, Interval('-P5').string() would return '-P5'. """ s = "-" if self.sign < 0 else "" s += self._qual_names[self.qual] s += str((self.span + (self.xoct * 7)) + 1) return s
def to_list(self)
-
Returns the interval values as a list: [span, qual, xoct, sign]
Expand source code
def to_list(self): """ Returns the interval values as a list: [span, qual, xoct, sign] """ return [self.span, self.qual, self.xoct, self.sign]
def transpose(self, p)
-
Transposes a Pitch or Pnum by the interval. Pnum transposition has no octave or direction so if the interval is negative its complement should be used and octaves should reduce to unisons without complementing.
Parameters
p
:Pitch | Pnum
- The Pitch or Pnum to transpose, otherwise the string name of a pitch or pnum (eg. 'Cb5' or 'Dff')
Returns
The transposed Pitch or Pnum.
Raises
A TypeError if p is not a Pitch or Pnum.
Expand source code
def transpose(self, p): """ Transposes a Pitch or Pnum by the interval. Pnum transposition has no octave or direction so if the interval is negative its complement should be used and octaves should reduce to unisons without complementing. Parameters ---------- p : Pitch | Pnum The Pitch or Pnum to transpose, otherwise the string name of a pitch or pnum (eg. 'Cb5' or 'Dff') Returns ---------- The transposed Pitch or Pnum. Raises ------ A TypeError if p is not a Pitch or Pnum. """ if isinstance(p, str) and len(str) > 0: p = Pitch(p) if p[-1].isdigit() else Pitch.pnums[p] if isinstance(p, Pitch): return self._transpose_pitch(p) if isinstance(p, Pitch.pnums): return self._transpose_pnum(p) raise TypeError(f"'{p}' is not a Pitch or Pnum.")