Intervals

Intervals describe the difference between two frequency representations, for example the difference between C4 and E4 (a major third interval). The main purpose of the interval primitive is to define the span and harmonic function of a transposition.

Xenharmlib distinguishes between intervals that are enharmonically equal to preserve their harmonic function, so an augmented second and a minor third in Western notation produce different results with regard to transposition, even though they describe the same difference in pitch:

from xenharmlib import WesternNotation
western = WesternNotation()

A2 = western.shorthand_interval('A', 2)
m3 = western.shorthand_interval('m', 3)

result_a = western.note('D', 4).transpose(A2)
result_b = western.note('D', 4).transpose(m3)

print(result_a)
print(result_b)
print(result_a == result_b)
WesternNote(E#, 4)
WesternNote(F, 4)
True

The interval primitive also supports various arithmetic operators and transformation functions (for example inversion, compound-to-simple-mapping, intervals class normalization, etc). The following chapter will look at intervals more in-depth and give examples on various origin contexts.

Deriving Intervals

There are two way to derive an interval from two frequency representations: One is the interval() method of the origin context, the other is the interval() method found directly on the pitch or note:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

p10 = edo31.pitch(10)
p18 = edo31.pitch(18)

interval_1 = edo31.interval(p10, p18)
interval_2 = p10.interval(p18)

print(interval_1)
print(interval_2)
EDOPitchInterval(8, 31-EDO)
EDOPitchInterval(8, 31-EDO)

Depending on tuning and notation, there are other direct builder methods available to create intervals. While in every origin context intervals can be derived from two frequency representations, direct methods are highly dependend on context.

Difference-based Construction

You can create intervals by providing the pitch difference using the diff_interval() method. Depending on the index scheme, this pitch difference can be an integer or a LatticePoint object:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

P5 = edo31.diff_interval(18)
print(P5)
EDOPitchInterval(18, 31-EDO)

In notations where there is enharmonic ambiguity (e.g., the Western notation), the result can vary depending on the configured enharmonic strategy of the notation, i.e., a pitch difference of 4 can be interpreted as a m3 interval or an A2 interval. Since there is no way of knowing the harmonic function of an interval given in absolute pitch difference, xenharmlib makes a guess.

For tunings and notations whose pitch differences are represented by lattice points, xenharmlib also provides the more concise vec_interval() method:

from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)

# these two expressions create the same object
P5 = limit5.diff_interval(limit5.lattice.point((-1, 1, 0)))
P5_alt = limit5.vec_interval((-1, 1, 0))

Name-based Construction

On the notation layer intervals, can also be created by their shorthand name. In fact, every notation in xenharmlib comes with an interval naming scheme, so - like notes - every interval has a distinct name from which it can be uniquely identified. Details about the interval naming scheme can be obtained from the relevant notation chapter of this documentation.

from xenharmlib import WesternNotation
western = WesternNotation()

P5 = western.shorthand_interval('P', 5)
print(P5)
WesternNoteInterval(P, 5)

Construction Based on Closest Frequency Ratio

Origin contexts based on integer indices allow construction based on the approximation of frequency ratios. Given an arbitrary frequency ratio, the closest_interval() method returns the interval of an origin context that is closest to it:

from xenharmlib import EDOTuning
from xenharmlib import FrequencyRatio

edo31 = EDOTuning(31)
interval = edo31.closest_interval(FrequencyRatio(7, 4))
print(interval)
EDOPitchInterval(25, 31-EDO)

Simple and Compound

In relation to an equivalency interval, intervals can be categorized into “simple intervals” (smaller or equal to the equivalency interval) or “compound intervals” (bigger than the equivalency interval). In the Western system, for example, M2 is a simple interval while M9 is a compound interval.

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

interval = edo31.diff_interval(33)

print(interval.is_simple)
print(interval.is_compound)
False
True

Compound intervals can be reduced to their simple equivalent easily. For example, in the Western system M9 has the simple interval equivalent of M2:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

interval = edo31.diff_interval(33)
simple = interval.to_simple()

print(simple)
EDOPitchInterval(2, 31-EDO)

Direction, Inversion & Absolute

Intervals in xenharmlib are directional, meaning there is a difference between ascending and descending intervals. Each ascending interval has its descending counterpart and vice versa. Whether you receive an ascending or descending interval depends on the order of the notes from which an interval is constructed. If the first note is lower than the second, you will receive an ascending interval. If the first note is higher than the second, you will receive a descending interval:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

p10 = edo31.pitch(10)
p18 = edo31.pitch(18)

ascending = p10.interval(p18)
descending = p18.interval(p10)

print(ascending)
print(descending)
EDOPitchInterval(8, 31-EDO)
EDOPitchInterval(-8, 31-EDO)

An ascending interval can be transformed into a descending interval (and vice versa) by prefixing the interval object with a minus (-) sign. For an interval X, we call the object -X the negation of X, with X + (-X) always resulting in the unison interval:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

p10 = edo31.pitch(10)
p18 = edo31.pitch(18)

ascending = p10.interval(p18)
descending = -ascending

print(ascending)
print(descending)
print(ascending + descending)
EDOPitchInterval(8, 31-EDO)
EDOPitchInterval(-8, 31-EDO)
EDOPitchInterval(0, 31-EDO)

Interval objects provide the sign property to inspect the direction of an interval, returning 1 for an ascending interval, -1 for a descending interval, and 0 for the unison interval.

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

p10 = edo31.pitch(10)
p18 = edo31.pitch(18)

ascending = p10.interval(p18)
descending = -ascending
unison = p10.interval(p10)

print(ascending.sign)
print(descending.sign)
print(unison.sign)
1
-1
0

Intervals support the abs() function that returns the corresponding ascending interval for a descending interval (or the interval itself if it is already ascending or unison)

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

p10 = edo31.pitch(10)
p18 = edo31.pitch(18)

descending = p18.interval(p10)
ascending = abs(descending)

print(descending)
print(ascending)
EDOPitchInterval(-8, 31-EDO)
EDOPitchInterval(8, 31-EDO)

In contrast, inversion of an interval X is defined as the interval Y, so that X + Y equals the equivalency interval [1].

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

p10 = edo31.pitch(10)
p18 = edo31.pitch(18)

interval = p10.interval(p18)
inv_interval = interval.inversion()

print(interval)
print(inv_interval)
print(interval + inv_interval)
EDOPitchInterval(8, 31-EDO)
EDOPitchInterval(23, 31-EDO)
EDOPitchInterval(31, 31-EDO)

Identity and Comparison

While pitch and note objects are representations of frequencies, intervals are representations of frequency ratios. Similar to pitches and notes, two intervals are considered equal if their frequency_ratio property is equal. This equality relation works across different origin contexts:

from xenharmlib import EDOTuning
edo12 = EDOTuning(12)
edo24 = EDOTuning(24)

P5_12 = edo12.diff_interval(7)
P5_24 = edo24.diff_interval(14)

print(P5_12.frequency_ratio)
print(P5_24.frequency_ratio)
print(P5_12 == P5_24)
FrequencyRatio(2**(7/12))
FrequencyRatio(2**(7/12))
True

Likewise, comparison operators work by comparing frequency ratios:

from xenharmlib import EDOTuning
edo12 = EDOTuning(12)

P5 = edo12.diff_interval(7)
M3 = edo12.diff_interval(4)

print(M3 < P5)
print(M3 > P5)
True
False

Since descending intervals are inverting the frequency ratio of their ascending counterpart (e.g., negating an ascending pure fifth with ratio \(\frac{3}{2}\) we will receive a descending pure fifth with ratio \(\frac{2}{3}\)) descending frequencies are always considered smaller than the unison interval, even if they might be bigger in absolute size:

from xenharmlib import EDOTuning
edo12 = EDOTuning(12)

desc_P5 = edo12.diff_interval(-7)
P1 = edo12.diff_interval(0)

print(desc_P5 < P1)
True

Equality/Identity also translates to Python’s built-in set type. If two intervals are considered equal, combining them in a Python set will result in a one-element set:

from xenharmlib import EDOTuning
from xenharmlib import WesternNotation

edo24 = EDOTuning(24)
western = WesternNotation()

interval_a = edo24.diff_interval(14)
interval_b = western.shorthand_interval('P', 5)

# since the second element is equal to the first
# only the first element will be added to the set
print({interval_a, interval_b})
{EDOPitchInterval(14, 24-EDO)}

Subtraction and Addition

Intervals support addition and subtraction. The plus (+) operator stacks two intervals on top of one another, meaning that transposing by the sum of two intervals is the same as transposing first by the first interval and then by the second. The minus (-) operator is defined as the addition of the negation of the second interval, i.e. transposing by \(X-Y\) is equal to transposing by \(X\) first and then transposing by \(-Y\)

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

interval_a = edo31.diff_interval(9)
interval_b = edo31.diff_interval(7)

print(interval_a + interval_b)
print(interval_a - interval_b)
EDOPitchInterval(16, 31-EDO)
EDOPitchInterval(2, 31-EDO)

Continued Stacking / Multiplication

Intervals can be multiplied by an integer, which is the same as stacking an interval upon itself a number of times. Continued stacking of the same interval has many applications, for example, in the circle of fifths or the generation of scales:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

P5 = edo31.diff_interval(18)

print(3 * P5)
EDOPitchInterval(54, 31-EDO)

Interval Class Calculation

Similar to pitch classes that represent sets of equivalent pitches, intervals can be sorted into interval classes that represent intervals of the same “intervallic content” or “color”. This equivalency is defined as follows:

  • Two intervals that only differ in direction belong inside the same class (C1 to G1 has the same color as G1 to C1)

  • If one interval is the simplified version of another compound interval, they belong inside the same class (C1 to G1 has the same color as C1 to G2 or C1 to G5)

  • If one interval is the inversion of another, they belong to the same class (C1 to G1 has the same color as G1 to C2)

Each interval class has a normalized representative that is calculated from an interval of the same class as follows:

  1. Calculate the absolute of the interval

  2. Transform the result into a simple interval

  3. Compare the result and its inversion and choose whichever is smaller

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

interval = edo31.diff_interval(-18)
ic = interval.ic_normalized()
print(ic)
EDOPitchInterval(13, 31-EDO)

Instead of receiving an interval object, you can also get the interval class index (which is the numeric representation used in most posttonal theory textbooks):

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

interval = edo31.diff_interval(-18)
print(interval.ic_index)
13

Footnotes