Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Just intonation classes for music theory experiments in Python

Probably all of this is redundant with Scala, but

  1. I don't know how to use it
  2. I wanted to learn by doing:
  • Just Intonation
  • object-oriented Python
  • unit testing

Maybe odd-limit and prime-limit should be objects rather than properties? "The limit" is a set of intervals, and includes all intervals of <= a certain height, so .height should be the property name and OddLimit should be a class?

Examples:

# -*- coding: utf-8 -*-
"""
Created on Wed Jul 30 18:55:13 2014
Just intonation classes for music theory experiments in Python
"""
from __future__ import division, print_function
from fractions import gcd as _gcd
from fractions import Fraction
from numbers import Rational
from itertools import combinations
import math
try:
from math import log2
except ImportError:
def log2(x):
return math.log(x, 2)
abbreviations = {
'P1': (1, 1),
'm2': (16, 15),
'M2': (9, 8), # or 10:9
'S2': (8, 7), # Septimal major second
'SM2': (8, 7), # Septimal major second
's3': (7, 6), # Septimal minor third
'sm3': (7, 6), # Septimal minor third
'm3': (6, 5), # or 19:16
'M3': (5, 4),
'P4': (4, 3),
'P5': (3, 2),
'A5': (25, 16), # Augmented fifth
'm6': (8, 5), # or 11:7 undecimal minor sixth
'M6': (5, 3), # or 12:7 is septimal major sixth
'm7': (16, 9), # "small just minor seventh"
# or 9:5 "large just minor seventh"
'M7': (15, 8),
'P8': (2, 1),
}
def gcd(*numbers):
"""Return the greatest common divisor of the given integers"""
return reduce(_gcd, numbers)
def lcm(*numbers):
"""Return lowest common multiple."""
def lcm(a, b):
return (a * b) // gcd(a, b)
return reduce(lcm, numbers, 1)
def gpf(n):
"""
Find the greatest prime factor of n
"""
if n < 1:
raise ValueError('{} does not have a prime factorization'.format(n))
if n == 1:
# Technically the answer is None, but 1 is accepted in music theory?
return 1
divisor = 2
while n > 1:
if not n % divisor:
n /= divisor
divisor -= 1
divisor += 1
return divisor
def _F(a):
return Fraction(a.numerator, a.denominator)
def is_odd(n):
return bool(n % 2)
class Interval(object):
"""
A just intonation (JI) musical interval; the distance spanned by a dyad.
There are several ways to construct intervals. For instance, the perfect
fifth:
>>> Interval(3, 2)
Interval(3, 2)
>>> Interval('3:2')
Interval(3, 2)
>>> Interval('3/2')
Interval(3, 2)
Some abbreviations are also understood:
>>> Interval('P5')
Interval(3, 2)
Intervals are relative distances between pitches on a musical scale (in
logarithmic frequency space). Mathematical operations are handled
accordingly, so multiplication becomes addition, powers become
multiplication, etc.:
>>> Fraction(5, 4) * Fraction(6, 5)
Fraction(3, 2)
>>> Interval(5, 4) + Interval(6, 5)
Interval(3, 2)
>>> Interval(3, 2) - Interval(6, 5)
Interval(5, 4)
So a span of 3 octaves is:
>>> 3 * Interval(2, 1)
Interval(8, 1)
Other operations are supported where they make sense, such as octave
reduction of the 5th harmonic, which is found in the 2nd octave:
>>> Interval(5) % Interval('P8')
Interval(5, 4)
>>> Interval(5) // Interval('P8')
2
or the 8th harmonic being 3 octaves above the root
>>> Interval(8) / Interval('P8')
3
>>> Interval(8) / 3
Interval(2, 1)
Complement (musical inversion):
>>> Interval('P8') - Interval(3, 2)
Interval(4, 3)
>>> Interval('P8') - Interval(6, 5)
Interval(5, 3)
Negation (ratio inversion): An interval with a smaller first term is
considered to be below the root note instead of above it:
>>> -Interval(3, 2)
Interval(2, 3)
Comparisons:
>>> max(Interval(256, 243), Interval(9, 8))
Interval(9, 8)
>>> Interval(2, 3) < Interval(3, 2)
True
>>> abs(Interval(2, 3)) == Interval(3, 2)
True
Intervals have some properties:
>>> Interval('P5').complement
Interval(4, 3)
>>> Interval(9, 8).odd_limit
9
>>> Interval(9, 8).prime_limit
3
The module also has shortcut variables for the common intervals. For
instance, the Pythagorean comma can be calculated as:
>>> P5 * 12 - P8 * 7
Interval(531441, 524288)
References:
Interval math http://www.moz.ac.at/sem/lehre/lib/bib/software/cm/Notes_from_the_Metalevel/scales.html#sec_15-3-2
"""
# Use __new__ not __init__ so that these are immutable?
def __init__(self, numerator, denominator=None):
"""
Constructs a musical interval
"""
if numerator == 0:
raise ValueError('No such thing as 0 interval')
if denominator is None:
if isinstance(numerator, Interval):
"""
>>> Interval(Interval(3, 2))
Interval(3, 2)
"""
interval = numerator
self._numerator = interval.numerator
self._denominator = interval.denominator
return
elif isinstance(numerator, Rational):
"""
>>> Interval(Fraction(3, 2))
Interval(3, 2)
>>> Interval(3)
Interval(3, 1)
"""
rational = numerator
self._numerator = rational.numerator
self._denominator = rational.denominator
return
elif isinstance(numerator, basestring):
"""
Construction from strings
>>> Interval('3:2')
Interval(3, 2)
>>> Interval('3/2')
Interval(3, 2)
>>> Interval('3')
Interval(3, 1)
"""
input_string = numerator
try:
numerator, denominator = abbreviations[input_string]
except KeyError:
input_string = input_string.replace(':', '/')
try:
frac = Fraction(input_string)
except ValueError:
raise ValueError('Invalid literal for Interval: %r' %
numerator)
numerator = frac.numerator
denominator = frac.denominator
else:
raise TypeError("argument should be a string "
"or a Rational instance")
"""
Construction from numerator and denominator
>>> Interval(3, 2)
Interval(3, 2)
>>> Interval(6, 4)
Interval(3, 2)
"""
g = gcd(numerator, denominator)
self._numerator = numerator // g
self._denominator = denominator // g
self._terms = self._denominator, self._numerator
@property
def numerator(a):
"""
Numerator of the frequency ratio
"""
return a._numerator
@property
def denominator(a):
"""
Denominator of the frequency ratio
"""
return a._denominator
@property
def complement(a):
"""
The musical complement, or inversion, of the interval.
When summed, an interval and its complement produce an octave.
"""
return P8 - a
@property
def odd_limit(a):
"""
The lowest odd-limit the interval is in; the greatest odd number found
in the frequency ratio (after powers of 2 are removed). For instance,
the "7 odd-limit" includes 7:6 and 6:5, but not 9:7 or 15:14.
(If this returns 5, then the interval is in the 5 and 7 odd-limits,
but not in the 3 odd-limit.)
"generally preferred for the analysis of simultaneous intervals and
chords"
"""
# "To find its odd limit, simply divide n by 2 until you can no
# longer divide it without a remainder, then do the same for d.
# Then report the larger of the two numbers left over."
# Numerator and denominator are already in reduced form.
num = a._numerator
den = a._denominator
while True:
if is_odd(num) and is_odd(den):
return max(num, den)
elif is_odd(num):
den = den // 2
elif is_odd(den):
num = num // 2
else:
raise Exception('programming mistake: {}:{}'.format(num, den))
@property
def prime_limit(a):
"""
The prime-limit of the interval; the greatest prime factor of the
numbers in the interval's frequency ratio. For instance, the
"7-limit" includes 7:6 and 6:5, as well as 9:7 and 15:14.
Generally used for analysis of scales, since intervals with
large integers may still have low odd-limit relationships to other
notes in the scale even if they are high odd-limit relative to the
root.
"""
# "For a ratio n/d in lowest terms, to find its prime
# limit, take the product n*d and factor it. Then report the
# largest prime you used in the factorization."
return max(gpf(a._numerator), gpf(a._denominator))
@property
def kees_height(a):
"""
ref: http://xenharmonic.wikispaces.com/Kees+Height
"""
# "To find its odd limit, simply divide n by 2 until you can no
# longer divide it without a remainder, then do the same for d.
# Then report the larger of the two numbers left over."
# Numerator and denominator are already in reduced form.
if is_odd(a._numerator) and is_odd(a._denominator):
return max(a._numerator, a._denominator)
elif is_odd(a._numerator):
return a._numerator
else:
assert is_odd(a._denominator)
return a._denominator
@property
def benedetti_height(a):
"""
The measure of inharmonicity used by Giovanni Battista Benedetti,
which is simply the numerator multiplied by the denominator of the
frequency ratio in simplest form.
ref: http://xenharmonic.wikispaces.com/Benedetti+height
"""
return a._numerator * a._denominator
@property
def tenney_height(a):
"""
The "harmonic distance" function used by James Tenney, also called
"Tenney norm".
ref: http://www.plainsound.org/pdfs/JC&ToH.pdf
"""
return log2(a._numerator * a._denominator)
def __repr__(self):
"""
>>> repr(Interval(3, 2))
'Interval(3, 2)'
"""
return '%s(%s, %s)' % (self.__class__.__name__,
self._numerator, self._denominator)
def __str__(self):
"""
>>> str(Interval(3, 2))
'3:2'
"""
return '%s:%s' % (self._numerator, self._denominator)
def __add__(a, b):
"""
>>> Interval('P4') + Interval('P5')
Interval(2, 1)
"""
if isinstance(b, Interval):
return Interval(_F(a) * _F(b))
def __sub__(a, b):
"""
>>> Interval('P8') - Interval('P4')
Interval(3, 2)
"""
if isinstance(b, Interval):
return Interval(_F(a) / _F(b))
def __mul__(a, b):
"""
>>> Interval(2, 1) * 3
Interval(8, 1)
Multiplying by non-integers would produce non-Intervals(always?),
and so is not allowed.
"""
try:
if int(b) == b:
return Interval(_F(a) ** int(b))
else:
raise ValueError('Intervals can only be '
'multiplied by integers')
except AttributeError:
raise TypeError('Intervals can only be multiplied by integers')
__rmul__ = __mul__
"""
>>> 3 * Interval(2, 1)
Interval(8, 1)
"""
def __div__(a, b):
"""
a / b
Valid:
3*P8 / 3 = P8 (actually c**b = a <-> c = a^(1/b) <-> c = 10**(log10(a)/b))
3*P8 / P8 = 3 (actually b**c = a <-> c = log(a, base=b))
Invalid:
8 / P8 = ??
"""
if isinstance(b, Interval):
"""
>>> Interval(8) / Interval(2)
3
"""
result = math.log(_F(a), _F(b))
result_int = int(round(result))
# Convert to exact int result if possible
if _F(b)**result_int == _F(a):
return result_int
else:
return result
elif int(b) == b:
"""
>>> Interval(8) / 3
Interval(2, 1)
"""
b = int(b)
num = a.numerator
den = a.denominator
num_result = int(round(10**(math.log10(num) / b)))
den_result = int(round(10**(math.log10(den) / b)))
if num_result**b == num and den_result**b == den:
return Interval(num_result, den_result)
else:
raise ValueError('{} cannot be '
'divided exactly by {}'.format(a, b))
else:
return NotImplemented
__truediv__ = __div__
def __floordiv__(a, b):
if isinstance(b, Interval):
"""
>>> Interval(3) // Interval(2)
1
"""
result = math.log(_F(a), _F(b))
result_int = int(result)
return result_int
elif int(b) == b:
"""
>>> Interval(8) // 3
Interval(2, 1)
"""
# Basically doesn't make sense? But does if division does?
return a / b
else:
return NotImplemented
def __mod__(a, b):
"""
Octave equivalence, for instance:
>>> Interval(3) % Interval(2)
Interval(3, 2)
>>> Interval(5) % Interval(2)
Interval(5, 4)
"""
div = a // b
return a - b * div
def __pos__(a):
"""
>>> +Interval(3, 2)
Interval(3, 2)
"""
return a
def __neg__(a):
"""
>>> -Interval(3, 2)
Interval(2, 3)
"""
return Interval(a.denominator, a.numerator)
def __abs__(a):
"""
>>> abs(Interval(2, 3))
Interval(3, 2)
"""
if a < Interval(1, 1):
return -a
else:
return a
def __eq__(a, b):
"""a == b"""
if isinstance(b, Interval):
return (a._numerator == b.numerator and
a._denominator == b.denominator)
else:
return False # TODO: or NotImplemented??
def __lt__(a, b):
"""a < b"""
return _F(a) < _F(b)
def __gt__(a, b):
"""a > b"""
return _F(a) > _F(b)
def __le__(a, b):
"""a <= b"""
return _F(a) <= _F(b)
def __ge__(a, b):
"""a >= b"""
return _F(a) >= _F(b)
def __nonzero__(a):
"""a != 0"""
return True
def __int__(a):
"""int(a)"""
return int(a._numerator / a._denominator)
def __long__(a):
"""long(a)"""
return long(a._numerator / a._denominator)
def __float__(a):
"""float(a)"""
return float(a._numerator / a._denominator)
def __hash__(self):
"""hash(self)"""
# Does not mean that
return hash(_F(self))
class Pitch(object):
"""
A musical pitch, an absolute location in logarithmic frequency space
Can be summed with intervals to produce other pitches, etc.
Pitches and math on Intervals is defined in logarithmic space. So you
cannot do ``Pitch(440) * 2``. You have to do:
>>> Pitch(440) + Interval(2)
Pitch(880)
This may seem like a strange way to do things until you think about it
like this:
>>> A440 = Pitch(440) # 440 Hz 'concert A'
>>> P8 = Interval(2, 1) # up an octave
>>> A880 = Pitch(880)
>>> A440 + P8 == A880
True
This is consistent with the way math is done on musical scales.
>>> M3 + m3 == P5
True
Pitches try to have exact Fraction frequency if possible:
>>> Pitch(100) + Interval('M2')
Pitch('225/2')
>>> Pitch(100*9/8)
Pitch(112.5)
The distance between two pitches is an interval:
>>> Pitch(440) - Pitch(330)
Interval(4, 3)
>>> Pitch(440) - Interval(4, 3)
Pitch(330)
"""
def __init__(self, frequency):
"""
Constructs a musical pitch from a frequency
"""
if isinstance(frequency, Pitch):
self._frequency = frequency.frequency
elif isinstance(frequency, Fraction):
self._frequency = frequency
elif isinstance(frequency, float):
self._frequency = frequency
else:
self._frequency = Fraction(frequency)
if int(self._frequency) == self._frequency:
self._frequency = int(frequency)
if self._frequency < 0:
raise ValueError('Pitch frequency cannot be negative')
@property
def frequency(a):
"""
Frequency of the pitch in hertz
"""
return a._frequency
def __repr__(self):
"""repr(self)"""
if isinstance(self._frequency, Fraction):
return ("%s('%s')" % (self.__class__.__name__, self._frequency))
else:
# int(self._frequency) == self._frequency:
return ("%s(%s)" % (self.__class__.__name__, self._frequency))
def __str__(self):
"""str(self)"""
return '%s Hz' % float(self._frequency)
def __add__(a, b):
"""
>>> Pitch(440) + Interval(5, 4)
Pitch(550)
"""
if isinstance(b, Interval):
return Pitch(a._frequency * _F(b))
else:
return NotImplemented
def __sub__(a, b):
if isinstance(b, Interval):
"""
>>> Pitch(440) - Interval(4, 3)
Pitch(330)
"""
return Pitch(a._frequency / _F(b))
elif isinstance(b, Pitch):
"""
>>> Pitch(440) - Pitch(330)
Interval(4, 3)
"""
return Interval(_F(a._frequency) / _F(b.frequency))
else:
return NotImplemented
def __lt__(a, b):
"""a < b"""
return a._frequency < b.frequency
def __gt__(a, b):
"""a > b"""
return a._frequency > b.frequency
def __le__(a, b):
"""a <= b"""
return a._frequency <= b.frequency
def __ge__(a, b):
"""a >= b"""
return a._frequency >= b.frequency
def __nonzero__(a):
"""a != 0"""
return a._frequency != 0
def __neg__(a):
"""-a"""
raise ValueError('Pitch cannot be negative')
def __eq__(a, b):
"""a == b"""
return (a._frequency == b.frequency)
def __int__(a):
"""int(a)"""
return int(a._frequency)
def __long__(a):
"""int(a)"""
return long(a._frequency)
def __float__(a):
"""float(a)"""
return float(a._frequency)
class Chord():
"""
A combination of notes separated by just intervals
Chords can be constructed several ways. For instance, the major chord can
be constructed from a list of (possibly fractional) frequency ratio terms:
>>> Chord(4, 5, 6)
Chord(4, 5, 6)
>>> Chord('4:5:6')
Chord(4, 5, 6)
>>> Chord('1-5/4-3/2')
Chord(4, 5, 6)
>>> Chord('1/1 – 5/4 – 3/2')
Chord(4, 5, 6)
or a list of intervals relative to the root:
>>> Chord(Interval('M3'), Interval('P5'))
Chord(4, 5, 6)
>>> Chord('5/4', '3/2')
Chord(4, 5, 6)
>>> Chord((5, 4), (3, 2))
Chord(4, 5, 6)
Beware the difference:
>>> Chord('4 5 6')
Chord(4, 5, 6)
>>> Chord(Interval(4), Interval(5), Interval(6))
Chord(1, 4, 5, 6)
Intervals are relative to a root, not stacked:
>>> Chord(M3, m3)
Chord(20, 24, 25)
Intervals are sorted and duplicate tones removed:
>>> Chord(4, 6, 5, 6)
Chord(4, 5, 6)
But this does not necessarily mean that the *terms* are sorted. Intervals
can be negative from the root, which produces terms that decrease. (Chords
are not assumed to be in "root position" or "normal form"):
>>> Chord(-P5, -M3)
Chord(15, 10, 12)
>>> Chord(-P5, P5)
Chord(6, 4, 9)
Note that the order of terms is backwards for Chords vs Intervals:
>>> Chord(Interval(3, 2))
Chord(2, 3)
>>> Chord(Interval(5, 4), Interval(6, 4))
Chord(4, 5, 6)
(This is potentially confusing, but is the way they are typically
represented. It may be changed in the future.)
The terms, intervals from root, and intermediate stacked steps of the
Chord can be accessed:
>>> Chord(M3, P5).terms
(4, 5, 6)
>>> Chord(4, 5, 6).intervals
(Interval(5, 4), Interval(3, 2))
>>> Chord(4, 5, 6).steps
(Interval(5, 4), Interval(6, 5))
>>> Chord(4, 5, 6).all_steps
set([Interval(5, 4), Interval(3, 2), Interval(6, 5)])
as well as the limits:
>>> Chord(20, 25, 30, 36).odd_limit
25
>>> Chord(20, 25, 30, 36).prime_limit
5
Musical inversion moves the lowest tone an octave higher:
>>> Chord(4, 5, 6).inversion(1)
Chord(5, 6, 8)
>>> Chord(4, 5, 6).inversion(2)
Chord(3, 4, 5)
Negating a chord makes all the intervals negative relative to the root
(converting it from the otonal overtone series to the utonal undertone
series):
>>> -Chord(1, 2, 3)
Chord(6, 2, 3)
>>> Chord('1/1 1/2 1/3')
Chord(6, 2, 3)
NOT accepted: (0, 4, 7) equal temperament notation for major chord. (The
4th note of what scale?)
"""
def __init__(self, *args):
"""
Constructs a musical chord from a series of intervals relative to
the root
"""
if len(args) == 1 and isinstance(args[0], basestring):
"""
Handle construction from a single string of (possibly fractional)
terms:
Chord('4:5:6') == Chord(4, 5, 6)
Chord('1/1 – 5/4 – 3/2') == Chord(4, 5, 6)
Chord('1-5/4-3/2-5/3') == Chord(12, 15, 18, 20)
Chord('1 5/4 3/2 5/3') == Chord(12, 15, 18, 20)
Chord('1/1, 5/4, 3/2, 7/4') == Chord(4, 5, 6, 7)
but
Chord('3 4 5') == Chord('3:4:5') == Chord(3, 4, 5)??
or
Chord('3 4 5') == Chord(Interval(3), Interval(4), Interval(5)) ==
Chord(1, 3, 4, 5)??
and what about
Chord('4:5 2:3') == Chord(4, 5, 6)?
"""
args = args[0]
if ':' in args:
sep = ':'
elif ',' in args:
sep = ','
elif '' in args:
sep = ''
elif '-' in args:
sep = '-'
elif ' ' in args:
sep = ' '
else:
raise ValueError('String argument "{}" not '
'understood'.format(args))
terms = [Fraction(x) for x in args.split(sep)]
root = terms[0]
terms = [root] + sorted(set(terms[1:]))
self._intervals = tuple(Interval(x, root)
for x in terms[1:])
elif all([isinstance(x, Interval) for x in args]):
"""
List of Interval objects:
>>> Chord(M3, P5)
Chord(4, 5, 6)
>>> Chord(Interval(3, 2))
Chord(2, 3)
>>> Chord(Interval(5, 4), Interval(3, 2))
Chord(4, 5, 6)
>>> Chord(Interval('3:2'), Interval(2))
Chord(2, 3, 4)
"""
self._intervals = tuple(sorted(set(args)))
fractions = sorted([_F(x) for x in set(args)])
l = lcm(*[x.denominator for x in fractions])
terms = [l, ]
terms.extend([x.numerator * l/x.denominator for x in fractions])
assert all(int(x) == x for x in terms)
terms = [int(x) for x in terms]
elif all([isinstance(x, int) for x in args]):
"""
List of terms:
>>> Chord(4, 5, 6)
Chord(4, 5, 6)
>>> Chord(2, 4, 6) # Converted to reduced form
Chord(1, 2, 3)
>>> Chord(4, 6, 5) # Intervals are sorted
Chord(4, 5, 6)
>>> Chord(6, 4, 9) # First interval is negative
Chord(4, 5, 6)
"""
terms = list(args)
root = terms[0]
terms = [root] + sorted(set(terms[1:]))
self._intervals = tuple(Interval(x, root) for x in terms[1:])
else:
"""
List of Interval arguments:
>>> Chord((5, 4), (3, 2))
Chord(4, 5, 6)
>>> Chord('3:2', 2)
Chord(2, 3, 4)
"""
try:
fractions = sorted([_F(Interval(*x)) for x in args])
except TypeError:
try:
fractions = sorted([_F(Interval(x)) for x in args])
except TypeError:
raise ValueError('Chord construction args "{}" '
'not understood'.format(args))
l = lcm(*[x.denominator for x in fractions])
terms = [l, ]
terms.extend([x.numerator * l/x.denominator for x in fractions])
assert all(int(x) == x for x in terms)
terms = [int(x) for x in terms]
root = terms[0]
terms = [root] + sorted(set(terms[1:]))
self._intervals = tuple(Interval(x, root) for x in terms[1:])
g = gcd(*terms)
self._terms = tuple(n // g for n in terms)
self._steps = tuple([self._intervals[0]] +
[j-i for i, j in
zip(self._intervals[:-1], self._intervals[1:])])
ints = [Interval(1)] + list(self.intervals)
self._all_steps = set([b - a for a, b in list(combinations(ints, 2))])
assert set(self._intervals) <= self._all_steps
assert set(self._steps) <= self._all_steps
@property
def terms(a):
"""
List of terms in the frequency ratio that makes up the chord
"""
return a._terms
@property
def intervals(a):
"""
List of musical intervals that make up the chord, relative to the root
"""
return a._intervals
@property
def steps(a):
"""
List of musical intervals which, stacked together, produce the chord
"""
return a._steps
@property
def all_steps(a):
"""
Set of all music intervals that can be made by any tone in the chord
with any other tone
"""
return a._all_steps
@property
def odd_limit(a):
"""
The highest odd limit of any interval found in the Chord.
So the "dyadic odd-limit"? or intervallic
For example:
>>> Chord('1/1 – 5/4 – 3/2 – 9/5').odd_limit
25
even though
>>> Chord('1/1 – 5/4 – 3/2 – 9/5').intervals
(Interval(5, 4), Interval(3, 2), Interval(9, 5))
>>> Chord('1/1 – 5/4 – 3/2 – 9/5').steps
(Interval(5, 4), Interval(6, 5), Interval(6, 5))
because
>>> Interval('9/5') - Interval('5/4')
Interval(36, 25)
"""
# "To find the prime limit or odd limit of a list of ratios (such as
# a scale), simply calculate it for each of them individually and
# report the maximum.
#
# To find the prime or odd limit of a chord, first compute its
# table of dyads, e.g. for major triad C-E-G the dyads are C-E,
# E-G, and C-G. Then apply the procedure for a list of ratios
# given above."
return max(x.odd_limit for x in a._all_steps)
@property
def prime_limit(a):
"""
The highest prime limit of any interval found in the Chord
"intervallic limit"
'Whether Partch used the word "limit" to refer to odd or prime numbers
is a matter of some debate.'
'Odd-limit" is generally considered to be the more important when the
context is a consideration of concordance, whereas "prime-limit" is
generally the reference in most other cases.'
"""
return max(x.prime_limit for x in a._all_steps)
def inversion(self, n):
"""
Return the nth inversion of a chord. The first inversion moves the
root an octave up and uses the next term as the root. The second
inversion moves that tone up an octave, etc.
"""
x = self
for step in range(n):
terms = list(x._terms)
if terms[0] < terms[1]:
terms = terms + [terms.pop(0)*2] # I'm surprised this works
elif terms[1] < terms[0]:
terms = terms + [terms.pop(1)*2] # I'm surprised this works
else:
# shouldn't be possible, I think
raise Exception('Programming error')
root = terms[0]
terms = [root] + sorted(set(terms[1:]))
x = Chord(*terms)
return x
def __repr__(self):
"""repr(self)"""
return (self.__class__.__name__ + '(' +
', '.join((str(n) for n in self._terms)) + ')')
def __str__(self):
"""str(self)"""
return ':'.join((str(n) for n in self._terms))
def __eq__(a, b):
"""
>>> Chord(4, 6, 8) == Chord(2, 3, 4)
True
"""
return (a._terms == b._terms) # TODO: is this ok? or only public properties?
def __neg__(a):
"""
>>> -Chord(1, 2, 3)
Chord(6, 2, 3)
"""
return Chord(*(-x for x in a.intervals[::-1]))
def __abs__(a):
"""
>>> abs(Chord(6, 3, 4))
Chord(2, 3, 4)
"""
return Chord(*sorted(abs(x) for x in a.intervals))
# Convenience shortcuts
P1 = Interval('P1')
m2 = Interval('m2')
M2 = Interval('M2')
m3 = Interval('m3')
M3 = Interval('M3')
P4 = Interval('P4')
P5 = Interval('P5')
m6 = Interval('m6')
M6 = Interval('M6')
m7 = Interval('m7')
M7 = Interval('M7')
P8 = Interval('P8')
#####################################################
# TESTS
########################################################
def test_gpf():
# http://oeis.org/A006530/list
max_factors = (
(2, 2), (3, 3), (4, 2), (5, 5), (6, 3), (7, 7), (8, 2), (9, 3),
(10, 5), (11, 11), (12, 3), (13, 13), (14, 7), (15, 5), (16, 2),
(17, 17), (18, 3), (19, 19), (20, 5), (21, 7), (22, 11), (23, 23),
(24, 3), (25, 5), (26, 13), (27, 3), (28, 7), (29, 29), (30, 5),
(31, 31), (32, 2), (33, 11), (34, 17), (35, 7), (36, 3), (37, 37),
(38, 19), (39, 13), (40, 5), (41, 41), (42, 7), (43, 43), (44, 11),
(45, 5), (46, 23), (47, 47), (48, 3), (49, 7), (50, 5), (51, 17),
(52, 13), (53, 53), (54, 3), (55, 11), (56, 7), (57, 19), (58, 29),
(59, 59), (60, 5), (61, 61), (62, 31), (63, 7), (64, 2), (65, 13),
(66, 11), (67, 67), (68, 17), (69, 23), (70, 7), (71, 71), (72, 3),
(73, 73), (74, 37), (75, 5), (76, 19), (77, 11), (78, 13), (79, 79),
(80, 5), (81, 3), (82, 41), (83, 83), (84, 7), (85, 17), (86, 43),
(1000, 5), (1021, 1021),
)
for n, m in max_factors:
assert gpf(n) == m
def test_interval():
# Construction
assert Interval(3, 2) == Interval(3, 2)
assert Interval('P5') == Interval(3, 2)
assert Interval(Interval('P5')) == Interval(3, 2)
assert Interval('M3') == Interval(5, 4)
assert Interval('m3') == Interval(6, 5)
assert Interval('3:2') == Interval(3, 2)
assert Interval('4:3') == Interval(4, 3)
assert Interval('3/2') == Interval(3, 2)
# Addition and subtraction
assert Interval(4, 5) + Interval(5, 6) == Interval(2, 3)
assert M3 + m3 == Interval(3, 2)
assert -M3 == Interval(4, 5) # __neg__
assert +M3 == Interval(5, 4) # __pos__
assert abs(Interval(3, 2)) == Interval(3, 2)
assert abs(Interval(2, 3)) == Interval(3, 2)
# Shortcut objects
assert M2 + m2 == m3
assert M3 + m3 == P5
assert P4 + M2 == P5
assert P4 + M3 == M6
assert P4 + P4 == m7
assert P4 + P5 == P8
assert P5 + m2 == m6
assert P5 + M3 == M7
# Multiplication and division
assert P5*12 - P8*7 == Interval(531441, 524288) # Pythagorean comma
assert 3*P8 == Interval(8, 1)
assert P8*4 == Interval(16, 1)
assert Interval(8) / 3 == P8
assert Interval(8) / P8 == 3
assert Interval('P5') * 4 / 4 == Interval('P5')
assert (Interval(1, 3)*5) / 5 == Interval(1, 3)
assert (Interval(3, 2)*5) / 5 == Interval(3, 2)
# Floor division and modulo
assert Interval(3) // P8 == 1
assert Interval(4) // P8 == 2
assert Interval(5) // P8 == 2
assert Interval(7) // P8 == 2
assert Interval(8) // P8 == 3
assert Interval(8) // 3 == Interval(2, 1)
assert Interval(3) % Interval('P8') == Interval(3, 2)
assert Interval(5) % Interval('P8') == Interval(5, 4)
# "The harmonic seventh may be derived from the harmonic series as the
# interval between the seventh harmonic and the fourth harmonic"
assert Interval(7) - Interval(4) == Interval(7, 4)
# Properties
# assert Interval('6:5').name == 'minor third'
# http://en.wikipedia.org/wiki/Prime_limit#Examples
limits = {
(3, 2): (3, 3),
(4, 3): (3, 3),
(5, 4): (5, 5),
(5, 2): (5, 5),
(5, 3): (5, 5),
(7, 5): (7, 7),
(10, 7): (7, 7),
(9, 8): (9, 3),
(27, 16): (27, 3),
(81, 64): (81, 3),
(243, 128): (243, 3),
}
for interval, (odd_limit, prime_limit) in limits.iteritems():
assert Interval(*interval).odd_limit == odd_limit
assert Interval(*interval).prime_limit == prime_limit
# http://xenharmonic.wikispaces.com/3-limit
for x in ('128/81', '16/9', '243/128', '256/243', '27/16', '3/2', '32/27',
'4/3', '81/64', '9/8'):
assert Interval(x).prime_limit == 3
# http://xenharmonic.wikispaces.com/5-limit
for x in ('10/9', '15/8', '16/15', '27/20', '40/27', '5/3', '5/4', '6/5',
'8/5', '81/80', '9/5'):
assert Interval(x).prime_limit == 5
# http://xenharmonic.wikispaces.com/Odd+limit
for x in ('3/2', '5/4', '7/6', '10/7', '12/7', '9/8', '14/9'):
assert Interval(x).odd_limit <= 9
assert Interval('11/9').odd_limit > 9
assert Interval('15/7').odd_limit > 9
# http://www.patmissin.com/tunings/tun1.html
assert Interval('1:1').odd_limit == 1
assert Interval('1:1').prime_limit == 1
# http://xenharmonic.wikispaces.com/share/view/69124170
assert Interval('10:3').odd_limit == 5
assert Interval(12).odd_limit == 3
assert Interval(3).odd_limit == 3
# http://www.tonalsoft.com/enc/l/limit.aspx
for x in ('81/64', '81/32', '81/16', '81/8', '81/4', '81/2',
'27/32', '27/16', '27/8', '27/4', '27/2',
'9/16', '9/8', '9/4', '9/2', '9/1', '18/1', '36/1', '72/1',
'3/16', '3/8', '3/4', '3/2', '3/1', '6/1', '12/1', '24/1', '48/1',
'1/16', '1/8', '1/4', '1/2', '1/1', '2/1', '4/1', '8/1', '16/1', '32/1',
'1/48', '1/24', '1/12', '1/6', '1/3', '2/3', '4/3', '8/3', '16/3', '32/3',
'1/72', '1/36', '1/18', '1/9', '2/9', '4/9', '8/9', '16/9', '32/9',
'1/27', '2/27', '4/27', '8/27', '16/27', '32/27', '64/27',
'1/81', '2/81', '4/81', '8/81', '16/81', '32/81', '64/81', '128/81'):
assert Interval(x).prime_limit <= 3
# http://www.tonalsoft.com/enc/l/limit.aspx
# The 3-limit consists of the following ratios, and all their
# octave-equivalents:
for x in ('1/1', '4/3', '3/2'):
assert Interval(x).odd_limit <= 3
assert (Interval(x) + 2*P8).odd_limit <= 3
assert (Interval(x) - P8).odd_limit <= 3
# The 5-limit consists of the following ratios, and all their
# octave-equivalents:
for x in ('1/1', '6/5', '5/4', '4/3', '3/2', '8/5', '5/3'):
assert Interval(x).odd_limit <= 5
assert (Interval(x) + 2*P8).odd_limit <= 5
assert (Interval(x) - P8).odd_limit <= 5
# The 7-limit consists of the following ratios, and all their
# octave-equivalents:
for x in ('1/1', '8/7', '7/6', '6/5', '5/4', '4/3', '7/5', '10/7', '3/2',
'8/5', '5/3', '12/7', '7/4'):
assert Interval(x).odd_limit <= 7
assert (Interval(x) + P8).odd_limit <= 7
assert (Interval(x) - 3*P8).odd_limit <= 7
# The 9-limit consists of the following ratios, and all their
# octave-equivalents:
for x in ('1/1', '10/9', '9/8', '8/7', '7/6', '6/5', '5/4', '9/7', '4/3',
'7/5', '10/7', '3/2', '14/9', '8/5', '5/3', '12/7', '7/4',
'16/9', '9/5'):
assert Interval(x).odd_limit <= 9
assert (Interval(x) + P8).odd_limit <= 9
assert (Interval(x) - 2*P8).odd_limit <= 9
# The 11-limit consists of the following ratios', 'and all their
# octave-equivalents:
for x in ('1/1', '12/11', '11/10', '10/9', '9/8', '8/7', '7/6', '6/5',
'11/9', '5/4', '14/11', '9/7', '4/3', '11/8', '7/5', '10/7',
'16/11', '3/2', '14/9', '11/7', '8/5', '18/11', '5/3', '12/7',
'7/4', '16/9', '9/5', '20/11', '11/6'):
assert Interval(x).odd_limit <= 11
assert (Interval(x) + 3*P8).odd_limit <= 11
assert (Interval(x) - 2*P8).odd_limit <= 11
# http://xenharmonic.wikispaces.com/Kees+Height
assert Interval('5/3').kees_height == 5
assert Interval('4/3').kees_height == 3
assert Interval('2/1').kees_height == 1
# http://xenharmonic.wikispaces.com/Kees+Height
for frac, ben, tenney in (('3/2', 6, 2.585),
('6/5', 30, 4.907),
('9/7', 63, 5.977),
('13/11', 143, 7.160)):
assert Interval(frac).benedetti_height == ben
assert round(Interval(frac).tenney_height - tenney, 3) == 0
# http://xenharmonic.wikispaces.com/Tenney+Height
for frac, ket, tenney in (('1/1', '|0>', 0 ),
('2/1', '|1>', 1 ),
('3/2', '|-1 1>', 2.5849625007),
('5/4', '|-2 0 1>', 4.3219280948),
('7/4', '|-2 0 0 1>', 4.8073549220),):
assert round(Interval(frac).tenney_height - tenney, 8) == 0
def test_pitch():
# Construction
assert Pitch(100) == Pitch(100)
assert Pitch(Pitch(100)) == Pitch(100)
assert Pitch(100) - Interval(3) == Pitch('100/3')
assert Pitch(100/3).frequency == 100/3
# Addition and subtraction
assert Pitch(300) + Interval(3, 2) == Pitch(450)
assert Pitch(300) - Interval(3, 2) == Pitch(200)
assert Pitch(300) + -Interval(3, 2) == Pitch(200)
assert Pitch(300) + Interval(2, 3) == Pitch(200)
assert Pitch(300) - Pitch(200) == Interval(3, 2)
assert Pitch(200) - Pitch(300) == Interval(2, 3)
assert Pitch(550) - Pitch(440) == Interval(5, 4)
# Properties
assert Pitch(440).frequency == 440
def test_chord():
# Construction
assert Chord(2, 4, 6) == Chord(1, 2, 3)
assert Chord(4, 5, 6) == Chord(4, 5, 6)
assert Chord(4, 6, 5) == Chord(4, 5, 6)
assert Chord(4, 6, 5) == Chord(4, 5, 6) # Intervals are sorted
assert Chord(6, 4, 9) == Chord(6, 4, 9)
assert Chord(4, 6, 5, 6) == Chord(4, 5, 6) # Duplicate tones removed
assert Chord(Interval(3, 2)) == Chord(2, 3)
assert Chord(Interval(5, 4), Interval(3, 2)) == Chord(4, 5, 6)
assert Chord(Interval(5, 4), Interval(6, 4)) == Chord(4, 5, 6)
assert Chord(Interval('3:2'), Interval(2)) == Chord(2, 3, 4)
assert Chord(Interval('M3'), Interval('m3')) == Chord(20, 24, 25)
assert Chord(M3) == Chord(4, 5)
assert Chord(M3) == Interval(5, 4) # Is this ok?
assert Chord(M3, m3) == Chord(20, 24, 25)
assert Chord(M3, P5) == Chord(4, 5, 6)
assert Chord(P5, M3) == Chord(4, 5, 6)
assert Chord(-P5, +P5) == Chord(6, 4, 9) # Lowest interval is negative
assert Chord(+P5, -P5) == Chord(6, 4, 9)
assert Chord(P5, P5, P5) == Chord(2, 3)
assert Chord('4:5:6') == Chord(4, 5, 6)
assert Chord('4:6:5') == Chord(4, 5, 6) # Sorted
assert Chord('6:4:9') == Chord(6, 4, 9)
assert Chord('1 : 2 : 3') == Chord(1, 2, 3)
assert Chord('1 5/4 3/2 5/3') == Chord(12, 15, 18, 20)
assert Chord('1-5/4-3/2-5/3') == Chord(12, 15, 18, 20)
assert Chord('1/1 – 5/4 – 3/2') == Chord(4, 5, 6)
assert Chord('1:3:5:7:9') == Chord(1, 3, 5, 7, 9) # otonal
assert Chord('1/9:1/7:1/5:1/3:1/1') == Chord(35, 45, 63, 105, 315) # utonal
assert Chord('3/2', '4/3') == Chord(6, 8, 9)
assert Chord('4/3', '3/2') == Chord(6, 8, 9)
assert Chord(('3:2'), 2) == Chord(2, 3, 4)
assert Chord('3 4 5') == Chord(3, 4, 5) # List of terms, not intervals
assert Chord('3', '4', '5') == Chord(1, 3, 4, 5) # List of intervals
assert Chord((5, 4), (3, 2)) == Chord(4, 5, 6)
assert Chord((5, 4), (6, 5)) == Chord(20, 24, 25)
assert Chord((6, 5), (5, 4)) == Chord(20, 24, 25)
# Chord('major') = Chord(4, 5, 6)? or better name?
# Inversion
assert Chord(4, 5, 6).inversion(1) == Chord(5, 6, 8)
assert Chord(4, 5, 6).inversion(2) == Chord(3, 4, 5)
assert Chord(4, 5, 6).inversion(3) == Chord(4, 5, 6)
assert Chord(4, 5, 6, 7).inversion(1) == Chord(5, 6, 7, 8)
assert Chord(4, 5, 6, 7).inversion(2) == Chord(6, 7, 8, 10)
assert Chord(4, 5, 6, 7).inversion(3) == Chord(7, 8, 10, 12)
assert Chord(4, 5, 6, 7).inversion(4) == Chord(4, 5, 6, 7)
assert Chord(-P5, +P5).inversion(1) == Chord(P4, P5) == Chord(6, 8, 9)
# Negation
assert (-Chord(1, 2, 3)).intervals == (Interval(1, 3), Interval(1, 2))
assert -Chord(-P8, -P5) == Chord(2, 3, 4)
assert abs(-Chord(1, 2, 3)) == Chord(1, 2, 3)
assert abs(Chord(6, 3, 4)) == Chord(2, 3, 4)
# Properties
assert Chord(11, 13, 14, 12).terms == (11, 12, 13, 14)
assert Chord(4, 5, 6).intervals == (Interval(5, 4), Interval(3, 2))
assert Chord(P5, P5, P5).intervals == (Interval(3, 2),)
assert Chord(4, 5, 6).steps == (Interval(5, 4), Interval(6, 5))
# http://www.72note.com/erlich/limit.html
assert Chord('1/1 5/4 3/2 7/4').prime_limit == 7
assert Chord('1/1 8/7 21/16 3/2 7/4').prime_limit == 7
# http://x31eq.com/ass.htm
assert Chord('3:5:9:15').odd_limit == 9
assert Chord('3:7:9:21').odd_limit == 9
# http://www.tallkite.com/misc_files/alt-tuner_manual_and_primer.pdf
# "A major chord 1/1 – 5/4 – 3/2 has an odd limit of 5, regardless of
# the voicing."
assert Chord('1/1 – 5/4 – 3/2').odd_limit == 5
assert Chord('1/1 – 5/4 – 3/2').inversion(1).odd_limit == 5
assert Chord('1/1 – 5/4 – 3/2').inversion(2).odd_limit == 5
assert Chord('1/1 – 5/4 – 3/2 – 15/8').odd_limit == 15 # maj7 chord
assert Chord('1/1 – 6/5 – 16/25').odd_limit == 25 # dim triad
assert Chord('1/1 – 5/4 – 3/2 – 9/5').odd_limit == 25 # dom7 chord
assert Chord('1/1 – 5/4 – 3/2 – 7/4').odd_limit == 7 # dom7 chord
assert Chord('1/1 – 6/5 – 7/5').odd_limit == 7 # dim chord
# http://strasheela.sourceforge.net/strasheela/doc/Example-MicrotonalChordProgression.html
assert Chord('1/1, 5/4, 3/2').prime_limit == 5
assert Chord('1/4, 8/5, 4/3').prime_limit == 5
assert Chord('1/1, 5/4, 3/2, 7/4').prime_limit == 7
assert Chord('1/1, 8/5, 4/3, 8/7').prime_limit == 7
assert Chord('1/1, 9/8, 5/4, 11/8, 3/2, 7/4').prime_limit == 11
assert Chord('1/1, 16/9, 8/5, 16/11, 4/3, 8/7').prime_limit == 11
if __name__ == "__main__":
import doctest
doctest.testmod()
# TODO: this is screwing up IPython's _
import pytest
pytest.main(['--tb=short', __file__])
# import nose
# result = nose.run()
pass
# -*- coding: utf-8 -*-
"""
Created on Tue Aug 26 22:27:10 2014
So to play MIDI chords, put each note on its
own channel, pitch bend them by less than one semitone to correct the pitch
and then play them all at once
"""
from __future__ import division, print_function
import time
from fractions import Fraction
from threading import Timer # :D
# modded for pitch bend: https://github.com/endolith/pygame/blob/master/lib/midi.py
from pygame import midi
import random
import math
from numpy import arange
from numpy.random import randint
from just_intonation import (Interval, Pitch, Chord, P1, m2, M2, m3, M3,
P4, P5, m6, M6, m7, M7, P8)
rest = 0.5 # seconds
def log2(x):
return math.log(x, 2)
def freq_to_MIDI(freq):
A = 440
return 12 * log2(freq / A) + 69
midi.init()
if 'LoopBe' in midi.get_device_info(3)[1]:
try:
synth = midi.Output(3)
except Exception as e:
if 'Invalid device ID' in str(e):
synth.close()
synth = midi.Output(3)
else:
raise
else:
raise Exception('Loopback MIDI device not found')
channels = 16
program = 0
def play_freq(freq, duration=None, sustain=15):
"""
Use MIDI pitch bends to play arbitrary frequencies. Bends apply to the
entire channel, so to support polyphony we cycle through the channels
for each note, hopefully not letting them collide.
sustain and duration do the same thing, but one is blocking and the
other is not? might not be the right way to do it.
"""
MIDI_float = freq_to_MIDI(float(freq))
MIDI_note = int(round(MIDI_float))
frac = MIDI_float - MIDI_note
bend_amount = int(frac * 4096)
channel = play_freq.channel
play_freq.channel += 1
play_freq.channel %= channels
if play_freq.channel == 9:
"""
Key-based percussion is always on MIDI Channel 10, so skip it.
"""
play_freq.channel += 1
play_freq.channel %= channels
synth.set_instrument(program, channel)
synth.pitch_bend(bend_amount, channel)
velocity = random.randint(110, 127) # Humanize
synth.note_on(MIDI_note, velocity, channel)
if duration is not None:
time.sleep(duration)
synth.note_off(MIDI_note, 0, channel)
else:
# Prevents synth from choking on all the tails of notes
# Randomize by 10% so they don't all shut off at once
rand = random.uniform(0.9, 1.1)
Timer(sustain*rand, synth.note_off, (MIDI_note, 0, channel)).start()
play_freq.channel = 0
def play_interval(pitch, interval, rest=0.7):
"""
Play the interval starting with `pitch`.
"""
pitch = Pitch(pitch)
interval = Interval(interval)
freq_1 = float(pitch)
freq_2 = float(pitch + interval)
play_freq(freq_1)
time.sleep(rest)
play_freq(freq_2)
time.sleep(rest)
play_freq(freq_1)
play_freq(freq_2)
def play_arp(pitch, intervals, end=True, rest=rest, sustain=2.4):
"""
Play an arpeggio from root to list of intervals
interval starting with `pitch`.
"""
if isinstance(intervals, Chord):
intervals = intervals.intervals
root = Pitch(pitch)
play_freq(root, sustain=sustain)
time.sleep(rest)
for interval in intervals:
play_freq(root + interval, sustain=sustain)
time.sleep(rest)
for interval in intervals[-2::-1]:
play_freq(root + interval, sustain=sustain)
time.sleep(rest)
if end:
play_freq(root, sustain=sustain)
def play_chord(pitch, intervals, sustain=3):
"""
Play a chord from root to list of intervals
interval starting with `pitch`.
"""
print(intervals)
if isinstance(intervals, Chord):
intervals = intervals.intervals
root = Pitch(pitch)
play_freq(root, sustain=sustain)
print(root)
for interval in intervals:
print(interval)
play_freq(root + interval, sustain=sustain)
def play_seq(pitch, intervals, rest=rest):
"""
Play a sequence from root to list of intervals
interval starting with `pitch`.
"""
if isinstance(intervals[0], Chord):
intervals = intervals[0].intervals
elif all([isinstance(x, Interval) for x in intervals]):
pass
elif all([isinstance(x, tuple) for x in intervals]):
intervals = [Interval(*x) for x in intervals]
else:
raise ValueError('sequence not understood')
root = Pitch(pitch)
for x in intervals:
play_freq(root + x)
time.sleep(rest)
pitch = Pitch(110)
# Lydian mode
lydian = sorted(arange(7)*P5 % P8) + [P8]
# Locrian mode
locrian = sorted(arange(7)*P4 % P8) + [P8]
# play_seq(110, random.sample(locrian, 8))
# Equal:
def equal_major():
synth.set_instrument(program, channel=0)
synth.pitch_bend(channel=0)
for note in [57, 61, 64]:
synth.note_on(note, 127, 0)
Timer(5, synth.note_off, (note, 0, 0)).start()
time.sleep(1)
# synth.set_instrument(program,channel=1)
# synth.pitch_bend(channel=1)
# for note in [57, 61, 64]:
# synth.note_on(note, 127, 0)
# Timer(5, synth.note_off, (note, 0, 0)).start()
def just_major():
play_freq(Pitch(220), sustain=5)
time.sleep(1)
play_freq(Pitch(220) + M3, sustain=5)
time.sleep(1)
play_freq(Pitch(220) + P5, sustain=5)
# play_chord(220, Chord(M3, P5))
def equal_minor():
synth.set_instrument(program, channel=0)
synth.pitch_bend(channel=0)
for note in [57, 60, 64]:
synth.note_on(note, 127, 0)
Timer(5, synth.note_off, (note, 0, 0)).start()
time.sleep(1)
def just_minor():
play_freq(Pitch(220), sustain=5)
time.sleep(1)
play_freq(Pitch(220) + m3, sustain=5)
time.sleep(1)
play_freq(Pitch(220) + P5, sustain=5)
# play_chord(220, Chord(m3, P5))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment