Introduction¶
Harmonic primitives in xenharmlib are objects representing basic harmonic structures. They are created from and always relate to an origin context, i.e., a musical system that defines the meaning of their symbolic expression (e.g. a G#-4 has a different frequency in the context of 31-EDO than in the context of 12-EDO)
Origin Contexts¶
An origin context can take on the form of a tuning (a representation system built on integers or lattice points) or a notation representing musical objects with strings (like Eb-4). Generally speaking, notations are symbolic wrappers around tunings, meaning each primitive created from a notation can be reduced to an equivalent tuning primitive. The reverse is also true, however, with some restrictions: Because most of the time there are multiple symbolic representations for the same numeric pitch (for example, in Western tuning Eb and D# represent the pitch index), xenharmlib must make a guess on what symbolic representation to choose when translating a harmonic primitive from a tuning to a notation.
Harmonic primitives are built from builder methods of the origin context. These builder methods share (for the most part) an interface across contexts, making it possible to make analytical tools that are agnostic to the origin context, i.e., allowing to create one function that works on objects built from an EDO Tuning as well as on objects originating from an UpDownNotation context.
The smallest unit of representation, from which all harmonic primitives can be derived, is the frequency representation, which in tuning contexts is called pitch and in notation contexts note.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
p10 = edo31.pitch(10)
print(p10.frequency.to_float())
20.448744438412696
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
G0 = limit5.pitch(limit5.lattice.point((-1, 1, 0)))
print(G0.frequency.to_float())
24.52739674693112
from xenharmlib import WesternNotation
western = WesternNotation()
A4 = western.note('A', 4)
print(A4.frequency.to_float())
440.0
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Abb4 = n_edo31.note('Abb', 4)
print(Abb4.frequency.to_float())
400.1127908225046
From these “smallest” units other harmonic primitives can be built, which we call second-order harmonic primitives:
A structure of 2 frequency representations is called an interval
A sorted list of unique frequency representations is called a scale
A succession of multiple frequency representations is called a sequence
On top of these we have what we call third-order harmonic primitives:
A structure which represents the intervals between each successive frequency representation in a scale or sequence is called an interval sequence.
A structure which represents intervals relating the elements of a scale or sequence to a fixed tonic pitch is called an interval fan
Third-order harmonic primitives allow generalizations like “major scale” (with the root note being left undefined) and play a big role for utilities dealing with structures in the context of transformation theory.
Every primitive refers to its origin context by the origin_context
property. Together with the unified interface of builder methods, this can
be leveraged to create generalized utilities. Observe, for example, how the
following function can be applied to sequences of all origins:
from xenharmlib import periodic
def steps_and_skips(sequence):
context = sequence.origin_context
scale = context.scale(sequence).period_normalized()
result = []
for a, b in zip(sequence, sequence[1:]):
i_a = periodic.index(scale, a)
i_b = periodic.index(scale, b)
if abs(i_a - i_b) == 1:
result.append('step')
if abs(i_a - i_b) > 1:
result.append('skip')
if abs(i_a - i_b) == 0:
result.append('-')
return result
The function transforms notes in a sequence into a period-normalized scale and then uses that scale to calculate scale degrees of movement in the sequence, allowing us to differentiate between steps and skips in the sequence, regardless of sequence origin:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
seq = edo31.index_seq([0, 5, 10, 0, 10])
result = steps_and_skips(seq)
print(result)
['step', 'step', 'skip', 'skip']
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
seq = limit5.rs_seq(['1/1', '9/8', '5/4', '1/1', '3/2'])
result = steps_and_skips(seq)
print(result)
['step', 'step', 'skip', 'skip']
from xenharmlib import WesternNotation
western = WesternNotation()
C4 = western.note('C', 4)
D4 = western.note('D', 4)
E4 = western.note('E', 4)
seq = western.seq([C4, D4, E4, C4, E4])
result = steps_and_skips(seq)
print(result)
['step', 'step', 'skip', 'skip']
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
C4 = n_edo31.note('C', 4)
Dbb4 = n_edo31.note('Dbb', 4)
Eup4 = n_edo31.note('^E', 4)
seq = n_edo31.seq([C4, Dbb4, Eup4, C4, Eup4])
result = steps_and_skips(seq)
print(result)
['step', 'step', 'skip', 'skip']
Constants¶
Origin contexts define various constants that give necessary definitions to
the methods of harmonic primitives and utilities. We will start with the
eq_interval
property that returns an interval object defining the equivalency
interval:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
print(edo31.eq_interval)
EDOPitchInterval(31, 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
print(limit5.eq_interval)
PrimeLimitPitchInterval(2, 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
print(western.eq_interval)
WesternNoteInterval(P, 8)
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
print(n_edo31.eq_interval)
UpDownNoteInterval(P, 8, 31-EDO)
Especially in notations where there can be multiple enharmonic ways to define the equivalency interval, the constant returns the definitive object necessary to do calculations, so e.g. the method transforming a compound interval into a simple one does not only return an interval with the correct frequency ratio, but also with the correct spelling.
In the same way, the
unison_interval
constant provides the “neutral element” of the interval space. The
unison interval is for example the reference point for xenharmlib
to find out if an interval is descending or ascending.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
print(edo31.unison_interval)
EDOPitchInterval(0, 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
print(limit5.unison_interval)
PrimeLimitPitchInterval(1, 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
print(western.unison_interval)
WesternNoteInterval(P, 1)
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
print(n_edo31.unison_interval)
UpDownNoteInterval(P, 1, 31-EDO)
The zero_element
constant defines the frequency representation that is considered
the reference element for transposing harmonic primitives to their
zero-normalized form:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
print(edo31.zero_element)
EDOPitch(0, 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
print(limit5.zero_element)
PrimeLimitPitch(1, 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
print(western.zero_element)
WesternNote(C, 0)
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
print(n_edo31.zero_element)
UpDownNote(C, 0, 31-EDO)
Since xenharmlib supports both integer and lattice point indexing,
reference points for different indexing strategies are also
given as constants, namely
zero_index
(the pitch index of the zero element), and
unison_diff
(the pitch difference of the unison interval), and
eq_diff
(the pitch difference of the equivalency interval)
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
print(edo31.zero_index)
print(edo31.unison_diff)
print(edo31.eq_diff)
0
0
31
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
print(limit5.zero_index)
print(limit5.unison_diff)
print(limit5.eq_diff)
LatticePoint(0, 0, 0)
LatticePoint(0, 0, 0)
LatticePoint(1, 0, 0)
from xenharmlib import WesternNotation
western = WesternNotation()
print(western.zero_index)
print(western.unison_diff)
print(western.eq_diff)
0
0
12
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
print(n_edo31.zero_index)
print(n_edo31.unison_diff)
print(n_edo31.eq_diff)
0
0
31