Scales¶
A scale in xenharmlib is a set of unique frequency representations, sorted in ascending order, from lowest frequency to highest frequency. The scale primitive allows equivalent frequency representations to coexist inside its structure, but not identical ones. On creation xenharmlib will sort the frequency representations and remove duplicate items:
from xenharmlib import WesternNotation
western = WesternNotation()
C4 = western.note('C', 4)
E4 = western.note('E', 4)
G4 = western.note('G', 4)
C5 = western.note('C', 5)
# E4 will only appear once in the scale, however
# C4 and C5 are allowed to coexist
scale = western.scale([E4, G4, E4, C4, C5])
print(scale)
WesternNoteScale([C4, E4, G4, C5])
Scales in xenharmlib are not restricted to the span of the equivalency interval (e.g. the octave), meaning the scale primitive can also be used to analyze polychords and related structures:
C4 = western.note('C', 4)
E4 = western.note('E', 4)
G4 = western.note('G', 4)
B4 = western.note('B', 4)
D5 = western.note('D', 5)
polychord = western.scale([C4, E4, G4, B4, D5])
print(polychord)
WesternNoteScale([C4, E4, G4, B4, D5])
Scales can be built from any iterable object that includes pitches or notes of the same origin context. This includes, e.g., Python’s lists, tuples, sets, generators, and xenharmlib’s sequence primitive.
C4 = western.note('C', 4)
E4 = western.note('E', 4)
G4 = western.note('G', 4)
# from list, set, tuple and generator
scale_a = western.scale([C4, E4, G4])
scale_b = western.scale({C4, E4, G4})
scale_c = western.scale((C4, E4, G4))
scale_d = western.scale(western.note(pcs, 4) for pcs in 'CEG')
# from xenharmlib's sequence primitive
seq = western.seq([C4, G4, E4, E4, C4])
scale_e = western.scale(seq)
print(scale_a == scale_b == scale_c == scale_d == scale_e)
True
For post-tonal theory, the scale primitive can be used as a pitch set or pitch class set. Since Xenharmlib implements a more general framework of music theory, double-digit pitch class indices are not abbreviated with ‘T’ for ten or ‘E’ for eleven, but returned in their numerical form.
Xenharmlib’s support for multi-generator tunings and notations also means that pitch or pitch class indices are not necessarily represented by integers, but can also be lattice points:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
p41 = edo31.pitch(41)
p49 = edo31.pitch(49)
p62 = edo31.pitch(62)
scale = edo31.scale([p41, p49, p62])
print(scale.pitch_indices)
print(scale.pc_indices)
[41, 49, 62]
[10, 18, 0]
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
E0 = limit5.rs_pitch('5/4')
G0 = limit5.rs_pitch('3/2')
C1 = limit5.rs_pitch('2/1')
scale = limit5.scale([E0, G0, C1])
print(scale.pitch_indices)
print(scale.pc_indices)
[LatticePoint(-2, 0, 1), LatticePoint(-1, 1, 0), LatticePoint(1, 0, 0)]
[LatticePoint(-2, 0, 1), LatticePoint(-1, 1, 0), LatticePoint(0, 0, 0)]
from xenharmlib import WesternNotation
western = WesternNotation()
E4 = western.note('E', 4)
G4 = western.note('G', 4)
C5 = western.note('C', 5)
scale = western.scale([E4, G4, C5])
print(scale.pitch_indices)
print(scale.pc_indices)
[52, 55, 60]
[4, 7, 0]
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
E4 = n_edo31.note('E', 4)
Gup4 = n_edo31.note('^G', 4)
C5 = n_edo31.note('C', 5)
scale = n_edo31.scale([E4, Gup4, C5])
print(scale.pitch_indices)
print(scale.pc_indices)
[134, 143, 155]
[10, 19, 0]
Deriving Scales¶
The builder method most agnostic to the origin context is the
scale() method,
which takes a list of frequency representations originating from the
same context and creates a scale primitive:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
p0 = edo31.pitch(0)
p10 = edo31.pitch(10)
p18 = edo31.pitch(18)
scale = edo31.scale([p0, p10, p18])
print(scale)
EDOPitchScale([0, 10, 18], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
C0 = limit5.rs_pitch('1/1')
E0 = limit5.rs_pitch('5/4')
G0 = limit5.rs_pitch('3/2')
scale = limit5.scale([C0, E0, G0])
print(scale)
PrimeLimitPitchScale([1, 5/4, 3/2], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
C4 = western.note('C', 4)
E4 = western.note('E', 4)
G4 = western.note('G', 4)
scale = western.scale([C4, E4, G4])
print(scale)
WesternNoteScale([C4, E4, G4])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
E4 = n_edo31.note('E', 4)
Gup4 = n_edo31.note('^G', 4)
C5 = n_edo31.note('C', 5)
scale = n_edo31.scale([E4, Gup4, C5])
print(scale)
UpDownNoteScale([E4, ^G4, C5], 31-EDO)
Element-wise Construction¶
Scales can be constructed element-wise. In line with xenharmlib’s immutable object design, this is not done with an append method like in standard Python, but with a method that returns a scale with an additional element:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
C0 = edo31.pitch(0)
D0 = edo31.pitch(5)
E0 = edo31.pitch(10)
# start with empty scale
scale = edo31.scale()
for e in [C0, D0, E0]:
scale = scale.with_element(e)
print(scale)
EDOPitchScale([0], 31-EDO)
EDOPitchScale([0, 5], 31-EDO)
EDOPitchScale([0, 5, 10], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
C0 = limit5.rs_pitch('1/1')
D0 = limit5.rs_pitch('9/8')
E0 = limit5.rs_pitch('5/4')
# start with empty scale
scale = limit5.scale()
for e in [C0, D0, E0]:
scale = scale.with_element(e)
print(scale)
PrimeLimitPitchScale([1], 5-Limit)
PrimeLimitPitchScale([1, 9/8], 5-Limit)
PrimeLimitPitchScale([1, 9/8, 5/4], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
C4 = western.note('C', 4)
D4 = western.note('D', 4)
E4 = western.note('E', 4)
# start with empty scale
scale = western.scale()
for e in [C4, D4, E4]:
scale = scale.with_element(e)
print(scale)
WesternNoteScale([C4])
WesternNoteScale([C4, D4])
WesternNoteScale([C4, D4, E4])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
E4 = n_edo31.note('E', 4)
Gup4 = n_edo31.note('^G', 4)
C5 = n_edo31.note('C', 5)
# start with empty scale
scale = n_edo31.scale()
for e in [E4, Gup4, C5]:
scale = scale.with_element(e)
print(scale)
UpDownNoteScale([E4], 31-EDO)
UpDownNoteScale([E4, ^G4], 31-EDO)
UpDownNoteScale([E4, ^G4, C5], 31-EDO)
Index-based Construction¶
Scales can also conveniently be constructed by providing a list of indices, which - depending on origin context - can be integers or lattice points. This type of construction also works on the notation layer by employing the notation’s enharmonic strategy.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale = edo31.index_scale([0, 10, 18])
print(scale)
EDOPitchScale([0, 10, 18], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale = limit5.index_scale(
[
limit5.lattice.point((0, 0, 0)),
limit5.lattice.point((-3, 2, 0)),
limit5.lattice.point((-2, 0, 1)),
]
)
print(scale)
PrimeLimitPitchScale([1, 9/8, 5/4], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale = western.index_scale([0, 5, 7])
print(scale)
WesternNoteScale([C0, F0, G0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
scale = n_edo31.index_scale([1, 10, 17])
print(scale)
UpDownNoteScale([Dbb0, E0, Fx0], 31-EDO)
Tunings with lattice point indices provide the convenience builder method
vec_scale() that expects a
tuple instead of a lattice point object, making the construction less
verbose:
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale = limit5.vec_scale(
[(1, 0, 0), (-1, 0, 1), (0, 1, 0)]
)
print(scale)
PrimeLimitPitchScale([2, 5/2, 3], 5-Limit)
from xenharmlib import MultiGenTuning
from xenharmlib import FrequencyRatio
sg237 = MultiGenTuning(
[FrequencyRatio(p) for p in [2, 3, 7]],
eq_diff_vec=(1, 0, 0)
)
scale = sg237.vec_scale(
[(0, 0, 0), (-1, -1, 1), (-1, 1, 0)]
)
print(scale)
MultiGenPitchScale([(0, 0, 0), (-1, -1, 1), (-1, 1, 0)], G=(2, 3, 7))
PC Shortform Construction¶
A common shortform to define scales is to simply define a succession of pitch classes or pitch class symbols with the silent assumption that each pitch class or symbol points to the next highest pitch of that class in relation to its predecessor pitch.
If, for example, we spell the C major scale as \(C, D, E, F, G, A, B\) we assume an equivalency interval section for the first element (e.g. we assume \(C\) to stand for \(C0\)) and then move upwards to find the next D, resulting in \(D0\), from there moving upwards to find the next E, finding \(E0\), and so forth)
The same structure applies if we have a pitch class set from a textbook
given in the form [8, 11, 0, 3, 4]: We assume a base interval
for the first pitch and then successively move upwards until we find
a pitch with the respective class and continue to do so, until we
have a scale whose .pc_indices property is exactly like our
input values:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale = edo31.pc_scale([18, 0, 10])
print(scale)
EDOPitchScale([18, 31, 41], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale = limit5.rs_pc_scale(
['3/2', '1/1', '5/4']
)
print(scale)
PrimeLimitPitchScale([3/2, 2, 5/2], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale = western.pc_scale(['C', 'D', 'E', 'F', 'G#', 'A', 'B'])
print(scale)
WesternNoteScale([C0, D0, E0, F0, G#0, A0, B0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
scale = n_edo31.pc_scale(['^E', 'G', 'Ab', 'D#'])
print(scale)
UpDownNoteScale([^E0, G0, Ab0, D#1], 31-EDO)
Construction Based on Closest Frequency¶
In origin contexts based on integer indices, you can build a scale from
a list of frequencies with the
closest_scale()
method that finds the closest representation for each frequency
and constructs a scale from it.
The frequency approximation feature can be especially useful for spectralist composition techniques, where the composer takes a sound and extracts the most prominent overtone frequencies to emulate them with chords of another instrument.
from xenharmlib import EDOTuning
from xenharmlib import Frequency
edo31 = EDOTuning(31)
# overtone frequencies measured e.g. by
# a spectrogram plugin or librosa
frequencies = [
Frequency(f) for f in [230, 410, 460, 500, 690]
]
scale = edo31.closest_scale(frequencies)
print(scale)
EDOPitchScale([118, 144, 149, 153, 167], 31-EDO)
from xenharmlib import EDTuning
from xenharmlib import Frequency
from xenharmlib import FrequencyRatio
bohlen_pierce = EDTuning(13, FrequencyRatio(3))
# overtone frequencies measured e.g. by
# a spectrogramm plugin or librosa
frequencies = [
Frequency(f) for f in [230, 410, 460, 500, 690]
]
scale = bohlen_pierce.closest_scale(frequencies)
print(scale)
EDPitchScale([31, 38, 39, 40, 44], 13ed3)
from xenharmlib import WesternNotation
from xenharmlib import Frequency
western = WesternNotation()
# overtone frequencies measured e.g. by
# a spectrogramm plugin or librosa
frequencies = [
Frequency(f) for f in [230, 410, 460, 500, 690]
]
scale = western.closest_scale(frequencies)
print(scale)
WesternNoteScale([A#3, G#4, A#4, B4, F5])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
from xenharmlib import Frequency
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
# overtone frequencies measured e.g. by
# a spectrogramm plugin or librosa
frequencies = [
Frequency(f) for f in [230, 410, 460, 500, 690]
]
scale = n_edo31.closest_scale(frequencies)
print(scale)
UpDownNoteScale([A#3, G#4, A#4, Cb5, E#5], 31-EDO)
Relation to Other Primitives¶
Scales can be transformed into three other harmonic primitives: Sequences, Interval Sequences and Interval Fans:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale = edo31.index_scale([2, 4, 5])
print(scale.to_seq())
print(scale.to_interval_seq())
print(scale.to_interval_fan())
EDOPitchSeq([2, 4, 5], 31-EDO)
EDOPitchIntervalSeq([2, 1], 31-EDO)
EDOPitchIntervalFan([0, 2, 3], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale = limit5.rs_scale(
['3/2', '15/8', '9/4']
)
print(scale.to_seq())
print(scale.to_interval_seq())
print(scale.to_interval_fan())
PrimeLimitPitchSeq([3/2, 15/8, 9/4], 5-Limit)
PrimeLimitPitchIntervalSeq([5/4, 6/5], 5-Limit)
PrimeLimitPitchIntervalFan([1, 5/4, 3/2], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale = western.scale(
[western.note(pcs, 0) for pcs in 'CEG']
)
print(scale.to_seq())
print(scale.to_interval_seq())
print(scale.to_interval_fan())
WesternNoteSeq([C0, E0, G0])
WesternNoteIntervalSeq([M3, m3])
WesternNoteIntervalFan([P1, M3, P5])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
scale = n_edo31.pc_scale(['^E', 'G', 'Ab'])
print(scale.to_seq())
print(scale.to_interval_seq())
print(scale.to_interval_fan())
UpDownNoteSeq([^E0, G0, Ab0], 31-EDO)
UpDownNoteIntervalSeq([vm3, m2], 31-EDO)
UpDownNoteIntervalFan([P1, vm3, vd4], 31-EDO)
It is also possible to create a scale from any of the other primitives. While sequences can just be given to the scale constructor, interval sequences and interval fans need a pitch or note as a reference point:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
seq = edo31.index_seq([3, 5, 10, 18, 15, 10, 0])
print(edo31.scale(seq))
iseq = edo31.diff_interval_seq([5, 8, 5, 5, 5, 8])
print(edo31.pitch(5).scale(iseq))
ifan = edo31.diff_interval_fan([0, 5, 10, 11, 15])
print(edo31.pitch(5).scale(ifan))
EDOPitchScale([0, 3, 5, 10, 15, 18], 31-EDO)
EDOPitchScale([5, 10, 18, 23, 28, 33, 41], 31-EDO)
EDOPitchScale([5, 10, 15, 16, 20], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
seq = limit5.rs_seq(['3/2', '15/8', '9/4', '1/1', '15/8'])
print(limit5.scale(seq))
iseq = limit5.rs_interval_seq(['9/4', '9/4', '15/4', '9/4'])
print(limit5.scale(iseq))
ifan = limit5.rs_interval_fan(['1/1', '15/8', '3/2'])
print(limit5.scale(ifan))
PrimeLimitPitchScale([1, 3/2, 15/8, 9/4], 5-Limit)
PrimeLimitPitchScale([9/4, 15/4], 5-Limit)
PrimeLimitPitchScale([1, 3/2, 15/8], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
seq = western.seq([western.note(pcs, 4) for pcs in 'CEGDEFE'])
print(western.scale(seq))
m2 = western.shorthand_interval('m', 2)
M2 = western.shorthand_interval('M', 2)
iseq = western.interval_seq([m2, m2, M2, m2, M2, m2, m2])
print(western.note('D#', 3).scale(iseq))
P1 = western.unison_interval
m3 = western.shorthand_interval('m', 3)
P5 = western.shorthand_interval('P', 5)
ifan = western.interval_seq([P1, m3, P5])
print(western.note('D#', 3).scale(ifan))
WesternNoteScale([C4, D4, E4, F4, G4])
WesternNoteScale([D#3, E3, F3, G3, Ab3, Bb3, Cb4, Dbb4])
WesternNoteScale([D#3, F#3, C#4])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
seq = n_edo31.seq([n_edo31.note(pcs, 4) for pcs in 'CEGDEFE'])
print(n_edo31.scale(seq))
up_m2 = n_edo31.shorthand_interval('^m', 2)
down_M2 = n_edo31.shorthand_interval('vM', 2)
iseq = n_edo31.interval_seq(
[up_m2, up_m2, down_M2, up_m2, down_M2, up_m2, down_M2]
)
print(n_edo31.note('D#', 3).scale(iseq))
P1 = n_edo31.unison_interval
m3 = n_edo31.shorthand_interval('m', 3)
P5 = n_edo31.shorthand_interval('P', 5)
ifan = n_edo31.interval_seq([P1, m3, P5])
print(n_edo31.note('D#', 3).scale(ifan))
UpDownNoteScale([C4, D4, E4, F4, G4], 31-EDO)
UpDownNoteScale([D#3, ^E3, ^^F3, ^G3, ^^Ab3, ^Bb3, ^^Cb4, ^Db4], 31-EDO)
UpDownNoteScale([D#3, F#3, C#4], 31-EDO)
Identity and Equivalency¶
Two scales are considered identical if each element in one scale has a matching element in the other scale with exactly the same frequency. This relation works across origin contexts, for example, 12-EDO and 24-EDO scales, or Western Note Scales can be identical:
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
from xenharmlib import WesternNotation
edo24 = EDOTuning(24)
n_edo24 = UpDownNotation(edo24)
western = WesternNotation()
Cm_1 = edo24.index_scale([0, 8, 14])
Cm_2 = n_edo24.pc_scale('CEG')
Cm_3 = western.pc_scale('CEG')
print(Cm_1 == Cm_2 == Cm_3)
True
Keep in mind that even if two scales might have the same symbolic string representation in a notation, they are not necessarily equal. A G4 in 31-EDO has, for example, a different frequency than a G4 in 12-EDO:
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
Cm_1 = n_edo31.pc_scale('CEG', 4)
Cm_2 = n_edo12.pc_scale('CEG', 4)
print(Cm_1)
print(Cm_2)
print(Cm_1 == Cm_2)
UpDownNoteScale([C4, E4, G4], 31-EDO)
UpDownNoteScale([C4, E4, G4], 12-EDO)
False
As long as two origin contexts have the same equivalency interval scales can also be compared on the criterion of equivalency instead of equality. Scales provide two definitions of equivalency: Set equivalency, which implies that each scale element has an equivalent element in the other scale (and vice versa), and the (stronger) sequential equivalency, which also demands that equivalent elements are at the same position in the scale. To get an intuition for the difference, consider the following example:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([0, 10, 49])
scale_b = edo31.index_scale([18, 31, 41])
scale_c = edo31.index_scale([31, 41, 49])
print(scale_a.is_set_equivalent(scale_b))
print(scale_a.is_seq_equivalent(scale_b))
print(scale_a.is_seq_equivalent(scale_c))
True
False
True
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['3/2', '2/1', '5/2'])
scale_b = limit5.rs_scale(['5/4', '3/2', '2/1'])
scale_c = limit5.rs_scale(['3/1', '4/1', '5/1'])
print(scale_a.is_set_equivalent(scale_b))
print(scale_a.is_seq_equivalent(scale_b))
print(scale_a.is_seq_equivalent(scale_c))
True
False
True
from xenharmlib import WesternNotation
western = WesternNotation()
E4 = western.note('E', 4)
G4 = western.note('G', 4)
C5 = western.note('C', 5)
E5 = western.note('E', 5)
G5 = western.note('G', 5)
scale_a = western.scale([G4, C5, E5])
scale_b = western.scale([E4, C5, G5])
scale_c = western.scale([G4, C5, E5])
print(scale_a.is_set_equivalent(scale_b))
print(scale_a.is_seq_equivalent(scale_b))
print(scale_a.is_seq_equivalent(scale_c))
True
False
True
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
E4 = n_edo31.note('E', 4)
Gup4 = n_edo31.note('^G', 4)
C5 = n_edo31.note('C', 5)
E5 = n_edo31.note('E', 5)
Gup5 = n_edo31.note('^G', 5)
scale_a = n_edo31.scale([Gup4, C5, E5])
scale_b = n_edo31.scale([E4, C5, Gup5])
scale_c = n_edo31.scale([Gup4, C5, E5])
print(scale_a.is_set_equivalent(scale_b))
print(scale_a.is_seq_equivalent(scale_b))
print(scale_a.is_seq_equivalent(scale_c))
True
False
True
Transposition¶
Scales can be transposed with the
transpose()
method. This is typically done by providing an
interval object of the same origin context:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale = edo31.index_scale([3, 10, 19])
interval = edo31.diff_interval(8)
print(scale.transpose(interval))
EDOPitchScale([11, 18, 27], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale = limit5.rs_scale(['1/1', '5/4', '3/2'])
interval = limit5.rs_interval('9/8')
print(scale.transpose(interval))
PrimeLimitPitchScale([9/8, 45/32, 27/16], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale = western.scale([western.note(pcs, 4) for pcs in 'CDEFGAB'])
interval = western.shorthand_interval('A', 3)
print(scale.transpose(interval))
WesternNoteScale([E#4, Fx4, Gx4, A#4, B#4, Cx5, Dx5])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale = n_edo31.scale([Gb4, Aup4, Bb4])
interval = n_edo31.shorthand_interval('^m', 2)
print(scale.transpose(interval))
UpDownNoteScale([^Abb4, ^^Bb4, ^Cb5], 31-EDO)
The transpose() method also
accepts a pitch difference. A pitch difference is an integer or
lattice point that defines the numeric distance between two frequency
representations. In the case of notations with enharmonic ambiguity,
xenharmlib makes a guess which note should be picked.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale = edo31.index_scale([3, 10, 19])
print(scale.transpose(8))
EDOPitchScale([11, 18, 27], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale = limit5.rs_scale(['1/1', '5/4', '3/2'])
pitch_diff = limit5.lattice.point((-1, 0, 1))
print(scale.transpose(pitch_diff))
PrimeLimitPitchScale([5/2, 25/8, 15/4], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale = western.scale([western.note(pcs, 4) for pcs in 'CDEFGAB'])
print(scale.transpose(2))
WesternNoteScale([D4, E4, F#4, G4, A4, B4, C#5])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale = n_edo31.scale([Gb4, Aup4, Bb4])
print(scale.transpose(18))
UpDownNoteScale([Db5, Fb5, F5], 31-EDO)
Rotation¶
Scale objects can be rotated upwards and downwards. On upward rotation, the first element is transposed upwards by the equivalency interval until it is bigger than the last element. On downward rotation the last element is transposed downwards by the equivalency interval until is smaller than the first element.
Rotation is also sometimes called the mode of a scale (for example the “Dorian Mode” is the first upwards rotation of the C major scale). In the context of chords, rotation is called inversion.
Rotation is done using the rotation method of scales, which takes an integer parameter: A positive integer \(k\) will rotate the scale upwards \(k\) times, a negative integer \(-k\) will rotate the scale downwards \(k\) times.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale = edo31.index_scale([3, 10, 18, 24, 29])
print(scale.rotation(3))
EDOPitchScale([24, 29, 34, 41, 49], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale = limit5.rs_scale(['1/1', '5/4', '3/2'])
print(scale.rotation(-1))
PrimeLimitPitchScale([3/4, 1, 5/4], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale = western.scale([western.note(pcs, 4) for pcs in 'CEGB'])
print(scale.rotation(2))
WesternNoteScale([G4, B4, C5, E5])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale = n_edo31.scale([Gb4, Aup4, Bb4])
print(scale.rotation(-1))
UpDownNoteScale([Bb3, Gb4, ^A4], 31-EDO)
Sequential Properties¶
Scales implement the typical properties of Python’s lists und tuples.
Containment can be tested with the in operator and item and
slice retrieval can be achieved with the [] operator.
Also, of course, iteration is supported:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale = edo31.index_scale([3, 10, 18, 24, 29])
print(edo31.pitch(10) in scale)
print(scale[2])
print(scale[1:4])
for pitch in scale:
print('-->', pitch)
True
EDOPitch(18, 31-EDO)
EDOPitchScale([10, 18, 24], 31-EDO)
--> EDOPitch(3, 31-EDO)
--> EDOPitch(10, 31-EDO)
--> EDOPitch(18, 31-EDO)
--> EDOPitch(24, 31-EDO)
--> EDOPitch(29, 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale = limit5.rs_scale(['1/1', '5/4', '3/2', '25/16'])
print(limit5.rs_pitch('5/4') in scale)
print(scale[2])
print(scale[1:4])
for pitch in scale:
print('-->', pitch)
True
PrimeLimitPitch(3/2, 5-Limit)
PrimeLimitPitchScale([5/4, 3/2, 25/16], 5-Limit)
--> PrimeLimitPitch(1, 5-Limit)
--> PrimeLimitPitch(5/4, 5-Limit)
--> PrimeLimitPitch(3/2, 5-Limit)
--> PrimeLimitPitch(25/16, 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale = western.scale([western.note(pcs, 4) for pcs in 'CEGB'])
print(western.note('E', 4) in scale)
print(scale[2])
print(scale[1:])
for note in scale:
print('-->', note)
True
WesternNote(G, 4)
WesternNoteScale([E4, G4, B4])
--> WesternNote(C, 4)
--> WesternNote(E, 4)
--> WesternNote(G, 4)
--> WesternNote(B, 4)
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale = n_edo31.scale([Gb4, Aup4, Bb4])
print(n_edo31.note('^A', 4) in scale)
print(scale[1])
print(scale[:2])
for note in scale:
print('-->', note)
True
UpDownNote(^A, 4, 31-EDO)
UpDownNoteScale([Gb4, ^A4], 31-EDO)
--> UpDownNote(Gb, 4, 31-EDO)
--> UpDownNote(^A, 4, 31-EDO)
--> UpDownNote(Bb, 4, 31-EDO)
Index Masks and Partial Scales¶
When examining a scale, you are often interested in focusing only on fragments
of it. In many cases, the []-operator is sufficient to extract the
desired partial scale from the original, e.g. if you want the major septimal
chord scale from a major scale:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
# stack 7 fifths. since 0 is C, start with -18 (F)
c_maj_scale = edo31.index_scale(
k * 18 for k in range(-1, 6)
).pcs_normalized()
c7chord = c_maj_scale[::2]
print(c7chord)
EDOPitchScale([0, 10, 18, 28], 31-EDO)
from xenharmlib import PrimeLimitTuning
from xenharmlib import FrequencyRatio
limit3 = PrimeLimitTuning(3)
# stack 7 pure fifths. since 1/1 is C start with 2/3 (F)
c_maj_scale = limit3.ratio_scale(
(FrequencyRatio(3, 2) ** k) for k in range(-1, 6)
).pcs_normalized()
c7chord = c_maj_scale[::2]
print(c7chord)
PrimeLimitPitchScale([1, 81/64, 3/2, 243/128], 3-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
c_maj_scale = western.pc_scale('CDEFGAB')
c7chord = c_maj_scale[::2]
print(c7chord)
WesternNoteScale([C0, E0, G0, B0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_maj_scale = n_edo12.pc_scale('CDEFGAB')
c7chord = c_maj_scale[::2]
print(c7chord)
UpDownNoteScale([C0, E0, G0, B0], 12-EDO)
In other cases, the []-operator will not take you far. What if instead
of the major septimal chord scale you want to generate the sus4 chord scale?
The appropriate indices (0, 3, 4) don’t have a uniform distance, so
they can not be represented by a single slice expression. For reason of this
limitation, xenharmlib implements a generalization of a slice called an index
mask and the scale method partial() that
expects an index mask as its parameter:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
# stack 7 fifths. since 0 is C, start with -18 (F)
c_maj_scale = edo31.index_scale(
k * 18 for k in range(-1, 6)
).pcs_normalized()
c_sus4_chord = c_maj_scale.partial((0, 3, 4))
print(c_sus4_chord)
EDOPitchScale([0, 13, 18], 31-EDO)
from xenharmlib import PrimeLimitTuning
from xenharmlib import FrequencyRatio
limit3 = PrimeLimitTuning(3)
# stack 7 pure fifths. since 1/1 is C start with 2/3 (F)
c_maj_scale = limit3.ratio_scale(
(FrequencyRatio(3, 2) ** k) for k in range(-1, 6)
).pcs_normalized()
c_sus4_chord = c_maj_scale.partial((0, 3, 4))
print(c_sus4_chord)
PrimeLimitPitchScale([1, 4/3, 3/2], 3-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
c_maj_scale = western.pc_scale('CDEFGAB')
c_sus4_chord = c_maj_scale.partial((0, 3, 4))
print(c_sus4_chord)
WesternNoteScale([C0, F0, G0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_maj_scale = n_edo12.pc_scale('CDEFGAB')
c_sus4_chord = c_maj_scale.partial((0, 3, 4))
print(c_sus4_chord)
UpDownNoteScale([C0, F0, G0], 12-EDO)
An index mask can be defined in multiple ways. In the above snippet, it is
simply expressed as a tuple of consecutive integers. For big connected
blocks of integers, this can be tedious, so xenharmlib offers a short
variant with Python’s ellipsis symbol (...). An ellipsis indicates
that all indices between two values should be part of the mask as well:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
# stack 7 fifths. since 0 is C, start with -18 (F)
c_maj_scale = edo31.index_scale(
k * 18 for k in range(-1, 6)
).pcs_normalized()
partial = c_maj_scale.partial((1, ..., 4, 6))
print(partial)
EDOPitchScale([5, 10, 13, 18, 28], 31-EDO)
from xenharmlib import PrimeLimitTuning
from xenharmlib import FrequencyRatio
limit3 = PrimeLimitTuning(3)
# stack 7 pure fifths. since 1/1 is C start with 2/3 (F)
c_maj_scale = limit3.ratio_scale(
(FrequencyRatio(3, 2) ** k) for k in range(-1, 6)
).pcs_normalized()
partial = c_maj_scale.partial((1, ..., 4, 6))
print(partial)
PrimeLimitPitchScale([9/8, 81/64, 4/3, 3/2, 243/128], 3-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
c_maj_scale = western.pc_scale('CDEFGAB')
partial = c_maj_scale.partial((1, ..., 4, 6))
print(partial)
WesternNoteScale([D0, E0, F0, G0, B0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_maj_scale = n_edo12.pc_scale('CDEFGAB')
partial = c_maj_scale.partial((1, ..., 4, 6))
print(partial)
UpDownNoteScale([D0, E0, F0, G0, B0], 12-EDO)
Similar to Python’s slices, the first value and the last value of mask definitions can be omitted entailing the familiar behavior: If a mask begins with an ellipsis, the first value is implicitly 0. If a mask ends with an ellipsis, the last value is the last index of the scale the mask is applied to:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
# stack 7 fifths. since 0 is C, start with -18 (F)
c_maj_scale = edo31.index_scale(
k * 18 for k in range(-1, 6)
).pcs_normalized()
partial1 = c_maj_scale.partial((..., 3, 6))
partial2 = c_maj_scale.partial((3, ...))
print(partial1)
print(partial2)
EDOPitchScale([0, 5, 10, 13, 28], 31-EDO)
EDOPitchScale([13, 18, 23, 28], 31-EDO)
from xenharmlib import PrimeLimitTuning
from xenharmlib import FrequencyRatio
limit3 = PrimeLimitTuning(3)
# stack 7 pure fifths. since 1/1 is C start with 2/3 (F)
c_maj_scale = limit3.ratio_scale(
(FrequencyRatio(3, 2) ** k) for k in range(-1, 6)
).pcs_normalized()
partial1 = c_maj_scale.partial((..., 3, 6))
partial2 = c_maj_scale.partial((3, ...))
print(partial1)
print(partial2)
PrimeLimitPitchScale([1, 9/8, 81/64, 4/3, 243/128], 3-Limit)
PrimeLimitPitchScale([4/3, 3/2, 27/16, 243/128], 3-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
c_maj_scale = western.pc_scale('CDEFGAB')
partial1 = c_maj_scale.partial((..., 3, 6))
partial2 = c_maj_scale.partial((3, ...))
print(partial1)
print(partial2)
WesternNoteScale([C0, D0, E0, F0, B0])
WesternNoteScale([F0, G0, A0, B0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_maj_scale = n_edo12.pc_scale('CDEFGAB')
partial1 = c_maj_scale.partial((..., 3, 6))
partial2 = c_maj_scale.partial((3, ...))
print(partial1)
print(partial2)
UpDownNoteScale([C0, D0, E0, F0, B0], 12-EDO)
UpDownNoteScale([F0, G0, A0, B0], 12-EDO)
Please note that, in contrast to slices, the index after an ellipsis is included in the mask:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
# stack 7 fifths. since 0 is C, start with -18 (F)
c_maj_scale = edo31.index_scale(
k * 18 for k in range(-1, 6)
).pcs_normalized()
partial_scale = c_maj_scale.partial((1, ..., 3))
scale_slice = c_maj_scale[1:3]
print(partial_scale)
print(scale_slice)
EDOPitchScale([5, 10, 13], 31-EDO)
EDOPitchScale([5, 10], 31-EDO)
from xenharmlib import PrimeLimitTuning
from xenharmlib import FrequencyRatio
limit3 = PrimeLimitTuning(3)
# stack 7 pure fifths. since 1/1 is C start with 2/3 (F)
c_maj_scale = limit3.ratio_scale(
(FrequencyRatio(3, 2) ** k) for k in range(-1, 6)
).pcs_normalized()
partial_scale = c_maj_scale.partial((1, ..., 3))
scale_slice = c_maj_scale[1:3]
print(partial_scale)
print(scale_slice)
PrimeLimitPitchScale([9/8, 81/64, 4/3], 3-Limit)
PrimeLimitPitchScale([9/8, 81/64], 3-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
c_maj_scale = western.pc_scale('CDEFGAB')
partial_scale = c_maj_scale.partial((1, ..., 3))
scale_slice = c_maj_scale[1:3]
print(partial_scale)
print(scale_slice)
WesternNoteScale([D0, E0, F0])
WesternNoteScale([D0, E0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_maj_scale = n_edo12.pc_scale('CDEFGAB')
partial_scale = c_maj_scale.partial((1, ..., 3))
scale_slice = c_maj_scale[1:3]
print(partial_scale)
print(scale_slice)
UpDownNoteScale([D0, E0, F0], 12-EDO)
UpDownNoteScale([D0, E0], 12-EDO)
For convenience reasons, one-element mask expressions can also be given without being wrapped by the tuple constructor:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
# stack 7 fifths. since 0 is C, start with -18 (F)
c_maj_scale = edo31.index_scale(
k * 18 for k in range(-1, 6)
).pcs_normalized()
partial_scale = c_maj_scale.partial((1, ..., 3))
scale_slice = c_maj_scale[1:3]
partial = c_maj_scale.partial(1)
print(partial)
EDOPitchScale([5], 31-EDO)
from xenharmlib import PrimeLimitTuning
from xenharmlib import FrequencyRatio
limit3 = PrimeLimitTuning(3)
# stack 7 pure fifths. since 1/1 is C start with 2/3 (F)
c_maj_scale = limit3.ratio_scale(
(FrequencyRatio(3, 2) ** k) for k in range(-1, 6)
).pcs_normalized()
partial = c_maj_scale.partial(1)
print(partial)
PrimeLimitPitchScale([9/8], 3-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
c_maj_scale = western.pc_scale('CDEFGAB')
partial = c_maj_scale.partial(1)
print(partial)
WesternNoteScale([D0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_maj_scale = n_edo12.pc_scale('CDEFGAB')
partial = c_maj_scale.partial(1)
print(partial)
UpDownNoteScale([D0], 12-EDO)
If you want to receive the complement of a mask, you can resort to the
partial_not() method:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
# stack 7 fifths. since 0 is C, start with -18 (F)
c_maj_scale = edo31.index_scale(
k * 18 for k in range(-1, 6)
).pcs_normalized()
scale = c_maj_scale.partial_not((0, 1, 2, 6, ...))
print(scale)
EDOPitchScale([13, 18, 23], 31-EDO)
from xenharmlib import PrimeLimitTuning
from xenharmlib import FrequencyRatio
limit3 = PrimeLimitTuning(3)
# stack 7 pure fifths. since 1/1 is C start with 2/3 (F)
c_maj_scale = limit3.ratio_scale(
(FrequencyRatio(3, 2) ** k) for k in range(-1, 6)
).pcs_normalized()
scale = c_maj_scale.partial_not((0, 1, 2, 6, ...))
print(scale)
PrimeLimitPitchScale([4/3, 3/2, 27/16], 3-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
c_maj_scale = western.pc_scale('CDEFGAB')
scale = c_maj_scale.partial_not((0, 1, 2, 6, ...))
print(scale)
WesternNoteScale([F0, G0, A0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_maj_scale = n_edo12.pc_scale('CDEFGAB')
scale = c_maj_scale.partial_not((0, 1, 2, 6, ...))
print(scale)
UpDownNoteScale([F0, G0, A0], 12-EDO)
If you want to receive both, use the
partition() method:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
# stack 7 fifths. since 0 is C, start with -18 (F)
c_maj_scale = edo31.index_scale(
k * 18 for k in range(-1, 6)
).pcs_normalized()
c_sus4, leftover = c_maj_scale.partition((0, 3, 4))
print(c_sus4)
print(leftover)
EDOPitchScale([0, 13, 18], 31-EDO)
EDOPitchScale([5, 10, 23, 28], 31-EDO)
from xenharmlib import PrimeLimitTuning
from xenharmlib import FrequencyRatio
limit3 = PrimeLimitTuning(3)
# stack 7 pure fifths. since 1/1 is C start with 2/3 (F)
c_maj_scale = limit3.ratio_scale(
(FrequencyRatio(3, 2) ** k) for k in range(-1, 6)
).pcs_normalized()
c_sus4, leftover = c_maj_scale.partition((0, 3, 4))
print(c_sus4)
print(leftover)
PrimeLimitPitchScale([1, 4/3, 3/2], 3-Limit)
PrimeLimitPitchScale([9/8, 81/64, 27/16, 243/128], 3-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
c_maj_scale = western.pc_scale('CDEFGAB')
c_sus4, leftover = c_maj_scale.partition((0, 3, 4))
print(c_sus4)
print(leftover)
WesternNoteScale([C0, F0, G0])
WesternNoteScale([D0, E0, A0, B0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_maj_scale = n_edo12.pc_scale('CDEFGAB')
c_sus4, leftover = c_maj_scale.partition((0, 3, 4))
print(c_sus4)
print(leftover)
UpDownNoteScale([C0, F0, G0], 12-EDO)
UpDownNoteScale([D0, E0, A0, B0], 12-EDO)
Normalizations¶
Scales can be normalized in various ways. From the Quickstart you already
know the pcs_normalized() method,
which transposes all elements of the scale to the first base interval.
However, there are other methods of normalization that can be useful depending
on what you want to do.
One downside of pitch class set normalization is that the scale does not
necessarily retain its original root note. Often you want to reduce a scale
to one base interval, but keep the root note. For this, xenharmlib offers the
period_normalized()
method:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale = edo31.index_scale([2, 8, 10, 19, 24, 29, 31, 35])
print(scale)
pn_scale = scale.period_normalized()
print(pn_scale)
EDOPitchScale([2, 8, 10, 19, 24, 29, 31, 35], 31-EDO)
EDOPitchScale([2, 4, 8, 10, 19, 24, 29, 31], 31-EDO)
from xenharmlib import PrimeLimitTuning
from xenharmlib import FrequencyRatio
limit11 = PrimeLimitTuning(11)
scale = limit11.ratio_scale(
FrequencyRatio(2*k) for k in range(1, 12)
)
print(scale)
pn_scale = scale.period_normalized()
print(pn_scale)
PrimeLimitPitchScale([2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22], 11-Limit)
PrimeLimitPitchScale([2, 9/4, 5/2, 11/4, 3, 7/2], 11-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale = western.pc_scale('CEGBDF', 4)
print(scale)
pn_scale = scale.period_normalized()
print(pn_scale)
WesternNoteScale([C4, E4, G4, B4, D5, F5])
WesternNoteScale([C4, D4, E4, F4, G4, B4])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
scale = n_edo12.pc_scale(
['F', 'G#', 'C', 'F#', 'G']
)
print(scale)
pn_scale = scale.period_normalized()
print(pn_scale)
UpDownNoteScale([F0, G#0, C1, F#1, G1], 12-EDO)
UpDownNoteScale([F0, F#0, G0, G#0, C1], 12-EDO)
Sometimes you want to compare the structure of different scales without
considering the root note; for example, you want to know if a scale is
a major scale by comparing it with the definition of the C major scale.
Here, the method
zero_normalized()
can help that will transpose the scale so the root note is the zero element
(C0 in UpDownNotation, the pitch with index 0 on the pitch layer):
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_maj_scale = n_edo12.natural_scale()
scale = n_edo12.pc_scale(
['A', 'B', 'C#', 'D', 'E', 'F#', 'G#']
)
zn_scale = scale.zero_normalized()
print(zn_scale)
print(zn_scale.is_notated_same(c_maj_scale))
UpDownNoteScale([C0, D0, E0, F0, G0, A0, B0], 12-EDO)
True
Note that zero normalization does not guarantee that the elements of the resulting scale are contained in one base interval:
polychord_Db_G = n_edo12.pc_scale(
['G', 'B', 'D', 'F', 'Ab', 'C#']
)
zn_scale = polychord_Db_G.zero_normalized()
print(zn_scale)
UpDownNoteScale([C0, E0, G0, Bb0, Db1, F#1], 12-EDO)
Often in textbooks, scales are described in a form where the root note is C0 and the scale elements all reside in the first base interval. This form is equivalent to the combined application of zero normalization and period normalization:
norm = polychord_Db_G.period_normalized().zero_normalized()
print(norm)
UpDownNoteScale([C0, Db0, E0, F#0, G0, Bb0], 12-EDO)
For this type of normalization, there is a convenient shortcut available,
the method zp_normalized()
(“zero-period normalized”):
norm = polychord_Db_G.zp_normalized()
print(norm)
UpDownNoteScale([C0, Db0, E0, F#0, G0, Bb0], 12-EDO)
Another common normal form is depicting the scale as a series between two
equivalent notes (for example depicting the notes of F major between F4
and F5). This form can be calculated using the method
plusone_normalized():
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
scale = n_edo12.pc_scale(
['F', 'G#', 'C', 'F#', 'G']
)
print(scale)
pn_scale = scale.plusone_normalized()
print(pn_scale)
UpDownNoteScale([F0, G#0, C1, F#1, G1], 12-EDO)
UpDownNoteScale([F0, F#0, G0, G#0, C1, F1], 12-EDO)
Reflection¶
Reflection is a generalization of the post-tonal inversion operation on pitch class sets. A reflection is defined as calculating the interval from each scale element to a designated axis note/pitch and applying the resulting interval on the axis to get the target note/pitch.
Without an argument, the reflection()
method assumes the axis to be the zero element of the origin context.
c_hypodorian = n_edo12.pc_scale(
['B', 'C', 'D', 'E', 'F', 'G', 'A']
)
refl = c_hypodorian.reflection()
print(refl)
UpDownNoteScale([Eb-2, F-2, G-2, Ab-2, Bb-2, C-1, Db-1], 12-EDO)
Observe how reflection across the zero element is equivalent to applying the pitch class set inversion operation:
print(c_hypodorian.pc_indices)
print(refl.pc_indices)
[11, 0, 2, 4, 5, 7, 9]
[3, 5, 7, 8, 10, 0, 1]
A scale can also be reflected using a different axis:
c_hypodorian = n_edo12.pc_scale(
['B', 'C', 'D', 'E', 'F', 'G', 'A']
)
axis = n_edo12.note('B', 0)
refl = c_hypodorian.reflection(axis)
print(refl)
UpDownNoteScale([C#0, D#0, E#0, F#0, G#0, A#0, B0], 12-EDO)
Retuning by Closest Correspondence¶
In the beginning we talked about the possibility to construct scales from
a list of frequencies.
The retune_closest() provides an extra
level of convenience: With it you can take a scale from one origin context
and transform it into a scale from a different origin context:
from xenharmlib import EDOTuning
from xenharmlib import WesternNotation
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
western = WesternNotation()
scale = n_edo31.pc_scale(['C', 'D#', 'E', 'G'], 4)
print(scale)
retuned = scale.retune_closest(western)
print(retuned)
UpDownNoteScale([C4, D#4, E4, G4], 31-EDO)
WesternNoteScale([C4, D#4, E4, G4])
Please note that retuning by closest frequency is not necessarily a one-to-one correspondence. If we try to reverse the retuning result, we will not receive the scale we started with:
retuned_back = retuned.retune_closest(n_edo31)
print(retuned_back)
print(scale == retuned_back)
UpDownNoteScale([C4, Eb4, E4, G4], 31-EDO)
False
This is because in the Western equal-tempered system “D#” and “Eb” represent the same frequency; however, in a 31-EDO system they are distinct:
print(western.note('D#', 4) == western.note('Eb', 4))
print(n_edo31.note('D#', 4) == n_edo31.note('Eb', 4))
True
False
Retuning from 31-EDO to a Western system is like lowering the resolution in an image. Once you have done it, the more “fine-grained” data is lost. Since distinct notes or pitches in one system can “collapse” into one in a system with less granularity, retuning scales can also have the effect that the resulting scale is shorter than the original one:
scale = n_edo31.pc_scale(['C', 'D#', 'Eb', 'E', 'G'], 4)
print(scale)
retuned = scale.retune_closest(western)
print(retuned)
UpDownNoteScale([C4, D#4, Eb4, E4, G4], 31-EDO)
WesternNoteScale([C4, D#4, E4, G4])
Set operations¶
Scales support most of the typical set operations that you are familiar with from the built-in Python sets (with slightly different names):
Please note that set operations without additional parameters test for equality and not equivalency, so if one scale includes C0 and the second scale includes C1, they are not considered the same element with regard to intersection, difference or symmetric difference.
Union (\(A \cup B\))¶
The union() operation (also possible to
use with the infix operator |) returns a new scale with all elements
from both scales.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([3, 10, 18, 24, 29])
scale_b = edo31.index_scale([4, 10, 18, 24])
print(scale_a.union(scale_b))
print(scale_a | scale_b)
EDOPitchScale([3, 4, 10, 18, 24, 29], 31-EDO)
EDOPitchScale([3, 4, 10, 18, 24, 29], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2', '25/16'])
scale_b = limit5.rs_scale(['3/2', '25/16', '15/8', '9/4'])
print(scale_a.union(scale_b))
print(scale_a | scale_b)
PrimeLimitPitchScale([1, 5/4, 3/2, 25/16, 15/8, 9/4], 5-Limit)
PrimeLimitPitchScale([1, 5/4, 3/2, 25/16, 15/8, 9/4], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale_a = western.pc_scale('CDEGB')
scale_b = western.pc_scale('DEGBCD')
print(scale_a.union(scale_b))
print(scale_a | scale_b)
WesternNoteScale([C0, D0, E0, G0, B0, C1, D1])
WesternNoteScale([C0, D0, E0, G0, B0, C1, D1])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
C4 = n_edo31.note('C', 4)
E4 = n_edo31.note('E', 4)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale_a = n_edo31.scale([E4, Gb4, Aup4, Bb4])
scale_b = n_edo31.scale([C4, Gb4, Aup4, Bb4])
print(scale_a.union(scale_b))
print(scale_a | scale_b)
UpDownNoteScale([C4, E4, Gb4, ^A4, Bb4], 31-EDO)
UpDownNoteScale([C4, E4, Gb4, ^A4, Bb4], 31-EDO)
Intersection (\(A \cap B\))¶
The intersection() operation (also
possible to use with the infix operator &) returns a new scale
containing only elements that exist in both scales.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([3, 10, 18, 24, 29])
scale_b = edo31.index_scale([4, 10, 18, 24])
print(scale_a.intersection(scale_b))
print(scale_a & scale_b)
EDOPitchScale([10, 18, 24], 31-EDO)
EDOPitchScale([10, 18, 24], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2', '25/16'])
scale_b = limit5.rs_scale(['3/2', '25/16', '15/8', '9/4'])
print(scale_a.intersection(scale_b))
print(scale_a & scale_b)
PrimeLimitPitchScale([3/2, 25/16], 5-Limit)
PrimeLimitPitchScale([3/2, 25/16], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale_a = western.pc_scale('CDEGB')
scale_b = western.pc_scale('DEGBCD')
print(scale_a.intersection(scale_b))
print(scale_a & scale_b)
WesternNoteScale([D0, E0, G0, B0])
WesternNoteScale([D0, E0, G0, B0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
C4 = n_edo31.note('C', 4)
E4 = n_edo31.note('E', 4)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale_a = n_edo31.scale([E4, Gb4, Aup4, Bb4])
scale_b = n_edo31.scale([C4, Gb4, Aup4, Bb4])
print(scale_a.intersection(scale_b))
print(scale_a & scale_b)
UpDownNoteScale([Gb4, ^A4, Bb4], 31-EDO)
UpDownNoteScale([Gb4, ^A4, Bb4], 31-EDO)
It is possible to modify the intersection operation so that equivalent pitches (e.g., C4 and C5) are considered equal. This approach is especially useful if you want to find common pitch classes that bridge one scale to the other.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([0, 4, 18, 24])
scale_b = edo31.index_scale([20, 24, 31, 35])
print(scale_a.intersection(scale_b, ignore_bi_index=True))
EDOPitchScale([0, 4, 24, 31, 35], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2', '15/8'])
scale_b = limit5.rs_scale(['6/5', '15/8', '2/1', '5/2'])
print(scale_a.intersection(scale_b, ignore_bi_index=True))
PrimeLimitPitchScale([1, 5/4, 15/8, 2, 5/2], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
C4 = western.note('C', 4)
E4 = western.note('E', 4)
Eb4 = western.note('Eb', 4)
G4 = western.note('G', 4)
B4 = western.note('B', 4)
C5 = western.note('C', 5)
E5 = western.note('E', 5)
scale_a = western.scale([C4, E4, G4, B4])
scale_b = western.scale([Eb4, B4, C5, E5])
print(scale_a.intersection(scale_b, ignore_bi_index=True))
WesternNoteScale([C4, E4, B4, C5, E5])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Cb4 = n_edo31.note('Cb', 4)
Eup4 = n_edo31.note('^E', 4)
Eb4 = n_edo31.note('Eb', 4)
G4 = n_edo31.note('G', 4)
B4 = n_edo31.note('B', 4)
Cb5 = n_edo31.note('Cb', 5)
Eup5 = n_edo31.note('^E', 5)
scale_a = n_edo31.scale([Cb4, Eup4, G4, B4])
scale_b = n_edo31.scale([Eb4, B4, Cb5, Eup5])
print(scale_a.intersection(scale_b, ignore_bi_index=True))
UpDownNoteScale([Cb4, ^E4, B4, Cb5, ^E5], 31-EDO)
Difference (\(A \setminus B\))¶
The difference() operation (also
possible to use with the infix operator -) returns a new scale
contatining only elements that exist in the first scale, but not in
the second one.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([3, 10, 18, 24, 29])
scale_b = edo31.index_scale([4, 10, 18, 24])
print(scale_a.difference(scale_b))
print(scale_a - scale_b)
EDOPitchScale([3, 29], 31-EDO)
EDOPitchScale([3, 29], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2', '25/16'])
scale_b = limit5.rs_scale(['3/2', '25/16', '15/8', '9/4'])
print(scale_a.difference(scale_b))
print(scale_a - scale_b)
PrimeLimitPitchScale([1, 5/4], 5-Limit)
PrimeLimitPitchScale([1, 5/4], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale_a = western.pc_scale('CDEGB')
scale_b = western.pc_scale('DEGBCD')
print(scale_a.difference(scale_b))
print(scale_a - scale_b)
WesternNoteScale([C0])
WesternNoteScale([C0])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
C4 = n_edo31.note('C', 4)
E4 = n_edo31.note('E', 4)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale_a = n_edo31.scale([E4, Gb4, Aup4, Bb4])
scale_b = n_edo31.scale([C4, Gb4, Aup4, Bb4])
print(scale_a.difference(scale_b))
print(scale_a - scale_b)
UpDownNoteScale([E4], 31-EDO)
UpDownNoteScale([E4], 31-EDO)
It is possible to modify the difference operation so that equivalent pitches (e.g. C4 and C5) are considered equal.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([0, 4, 18, 24])
scale_b = edo31.index_scale([20, 24, 31, 35])
print(scale_a.difference(scale_b, ignore_bi_index=True))
EDOPitchScale([18], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2', '15/8'])
scale_b = limit5.rs_scale(['6/5', '15/8', '2/1', '5/2'])
print(scale_a.difference(scale_b, ignore_bi_index=True))
PrimeLimitPitchScale([3/2], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
C4 = western.note('C', 4)
E4 = western.note('E', 4)
Eb4 = western.note('Eb', 4)
G4 = western.note('G', 4)
B4 = western.note('B', 4)
C5 = western.note('C', 5)
E5 = western.note('E', 5)
scale_a = western.scale([C4, E4, G4, B4])
scale_b = western.scale([Eb4, B4, C5, E5])
print(scale_a.difference(scale_b, ignore_bi_index=True))
WesternNoteScale([G4])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Cb4 = n_edo31.note('Cb', 4)
Eup4 = n_edo31.note('^E', 4)
Eb4 = n_edo31.note('Eb', 4)
G4 = n_edo31.note('G', 4)
B4 = n_edo31.note('B', 4)
Cb5 = n_edo31.note('Cb', 5)
Eup5 = n_edo31.note('^E', 5)
scale_a = n_edo31.scale([Cb4, Eup4, G4, B4])
scale_b = n_edo31.scale([Eb4, B4, Cb5, Eup5])
print(scale_a.difference(scale_b, ignore_bi_index=True))
UpDownNoteScale([G4], 31-EDO)
Symmetric Difference (\(A \triangle B\))¶
The symmetric_difference() operation
(also possible to use with the infix operator ^) returns a new
scale containing only elements that exist in the first scale, but not
in the second one combined with elements that exist in the second scale
but not in the first one.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([3, 10, 18, 24, 29])
scale_b = edo31.index_scale([4, 10, 18, 24])
print(scale_a.symmetric_difference(scale_b))
print(scale_a ^ scale_b)
EDOPitchScale([3, 4, 29], 31-EDO)
EDOPitchScale([3, 4, 29], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2', '25/16'])
scale_b = limit5.rs_scale(['3/2', '25/16', '15/8', '9/4'])
print(scale_a.symmetric_difference(scale_b))
print(scale_a ^ scale_b)
PrimeLimitPitchScale([1, 5/4, 15/8, 9/4], 5-Limit)
PrimeLimitPitchScale([1, 5/4, 15/8, 9/4], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
scale_a = western.pc_scale('CDEGB')
scale_b = western.pc_scale('DEGBCD')
print(scale_a.symmetric_difference(scale_b))
print(scale_a ^ scale_b)
WesternNoteScale([C0, C1, D1])
WesternNoteScale([C0, C1, D1])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
C4 = n_edo31.note('C', 4)
E4 = n_edo31.note('E', 4)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale_a = n_edo31.scale([E4, Gb4, Aup4, Bb4])
scale_b = n_edo31.scale([C4, Gb4, Aup4, Bb4])
print(scale_a.symmetric_difference(scale_b))
print(scale_a ^ scale_b)
UpDownNoteScale([C4, E4], 31-EDO)
UpDownNoteScale([C4, E4], 31-EDO)
It is possible to modify the symmetric difference operation so that equivalent pitches (e.g. C4 and C5) are considered equal.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([0, 4, 18, 24])
scale_b = edo31.index_scale([20, 24, 31, 35])
print(scale_a.symmetric_difference(scale_b, ignore_bi_index=True))
EDOPitchScale([18, 20], 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2', '15/8'])
scale_b = limit5.rs_scale(['6/5', '15/8', '2/1', '5/2'])
print(scale_a.symmetric_difference(scale_b, ignore_bi_index=True))
PrimeLimitPitchScale([6/5, 3/2], 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
C4 = western.note('C', 4)
E4 = western.note('E', 4)
Eb4 = western.note('Eb', 4)
G4 = western.note('G', 4)
B4 = western.note('B', 4)
C5 = western.note('C', 5)
E5 = western.note('E', 5)
scale_a = western.scale([C4, E4, G4, B4])
scale_b = western.scale([Eb4, B4, C5, E5])
print(scale_a.symmetric_difference(scale_b, ignore_bi_index=True))
WesternNoteScale([Eb4, G4])
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Cb4 = n_edo31.note('Cb', 4)
Eup4 = n_edo31.note('^E', 4)
Eb4 = n_edo31.note('Eb', 4)
G4 = n_edo31.note('G', 4)
B4 = n_edo31.note('B', 4)
Cb5 = n_edo31.note('Cb', 5)
Eup5 = n_edo31.note('^E', 5)
scale_a = n_edo31.scale([Cb4, Eup4, G4, B4])
scale_b = n_edo31.scale([Eb4, B4, Cb5, Eup5])
print(scale_a.symmetric_difference(scale_b, ignore_bi_index=True))
UpDownNoteScale([Eb4, G4], 31-EDO)
Subset Relations (\(A \subset B\), \(A \subseteq B\))¶
The is_subset() operation (also possible
to use with the infix operator <= for “subset or equal” and
< for “proper subset”) returns True if all the elements in
one scale exist in the other.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([3, 10, 18, 24, 29])
scale_b = edo31.index_scale([3, 10, 18, 24])
print(scale_b.is_subset(scale_a))
print(scale_a.is_subset(scale_a, proper=True))
print(scale_b <= scale_a)
print(scale_a < scale_a)
True
False
True
False
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2', '25/16'])
scale_b = limit5.rs_scale(['5/4', '3/2', '25/16'])
print(scale_b.is_subset(scale_a))
print(scale_a.is_subset(scale_a, proper=True))
print(scale_b <= scale_a)
print(scale_a < scale_a)
True
False
True
False
from xenharmlib import WesternNotation
western = WesternNotation()
scale_a = western.pc_scale('CDEGB')
scale_b = western.pc_scale('CDEG')
print(scale_b.is_subset(scale_a))
print(scale_a.is_subset(scale_a, proper=True))
print(scale_b <= scale_a)
print(scale_a < scale_a)
True
False
True
False
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
E4 = n_edo31.note('E', 4)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale_a = n_edo31.scale([E4, Gb4, Aup4, Bb4])
scale_b = n_edo31.scale([Gb4, Aup4, Bb4])
print(scale_b.is_subset(scale_a))
print(scale_a.is_subset(scale_a, proper=True))
print(scale_b <= scale_a)
print(scale_a < scale_a)
True
False
True
False
It is possible to modify the subset operation so that equivalent pitches (e.g., C4 and C5) are considered equal.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([0, 4, 18])
scale_b = edo31.index_scale([31, 35, 49])
print(scale_b.is_subset(scale_a, ignore_bi_index=True))
print(scale_b.is_subset(scale_a, proper=True, ignore_bi_index=True))
True
False
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2'])
scale_b = limit5.rs_scale(['2/1', '5/2', '3/1'])
print(scale_b.is_subset(scale_a, ignore_bi_index=True))
print(scale_b.is_subset(scale_a, proper=True, ignore_bi_index=True))
True
False
from xenharmlib import WesternNotation
western = WesternNotation()
C4 = western.note('C', 4)
E4 = western.note('E', 4)
G4 = western.note('G', 4)
C5 = western.note('C', 5)
E5 = western.note('E', 5)
G5 = western.note('G', 5)
scale_a = western.scale([C4, E4, G4])
scale_b = western.scale([C5, E5, G5])
print(scale_b.is_subset(scale_a, ignore_bi_index=True))
print(scale_b.is_subset(scale_a, proper=True, ignore_bi_index=True))
True
False
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Cb4 = n_edo31.note('Cb', 4)
Eup4 = n_edo31.note('^E', 4)
G4 = n_edo31.note('G', 4)
Cb5 = n_edo31.note('Cb', 5)
Eup5 = n_edo31.note('^E', 5)
G5 = n_edo31.note('G', 5)
scale_a = n_edo31.scale([Cb4, Eup4, G4])
scale_b = n_edo31.scale([Cb5, Eup5, G5])
print(scale_b.is_subset(scale_a, ignore_bi_index=True))
print(scale_b.is_subset(scale_a, proper=True, ignore_bi_index=True))
True
False
Disjoint Sets (\(A \cap B = \emptyset\))¶
Two scales can be tested if they do not contain any common elements
(are disjoint) by the is_disjoint()
operation.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([3, 10, 18, 24, 29])
scale_b = edo31.index_scale([4, 10, 18, 24])
scale_c = edo31.index_scale([4, 11, 19, 25])
print(scale_a.is_disjoint(scale_b))
print(scale_a.is_disjoint(scale_c))
False
True
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2', '25/16'])
scale_b = limit5.rs_scale(['3/2', '25/16', '15/8', '9/4'])
scale_c = limit5.rs_scale(['15/2', '125/16', '75/8', '45/4'])
print(scale_a.is_disjoint(scale_b))
print(scale_a.is_disjoint(scale_c))
False
True
from xenharmlib import WesternNotation
western = WesternNotation()
scale_a = western.pc_scale('CDEGB')
scale_b = western.pc_scale('DEGBCD')
scale_c = western.pc_scale('FA')
print(scale_a.is_disjoint(scale_b))
print(scale_a.is_disjoint(scale_c))
False
True
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
C4 = n_edo31.note('C', 4)
E4 = n_edo31.note('E', 4)
Eb4 = n_edo31.note('Eb', 4)
Gb4 = n_edo31.note('Gb', 4)
Aup4 = n_edo31.note('^A', 4)
Bb4 = n_edo31.note('Bb', 4)
scale_a = n_edo31.scale([Eb4, Gb4, Bb4])
scale_b = n_edo31.scale([Gb4, Aup4, Bb4])
scale_c = n_edo31.scale([C4, E4])
print(scale_a.is_disjoint(scale_b))
print(scale_a.is_disjoint(scale_c))
False
True
It is possible to modify the disjoint test so that equivalent pitches (e.g. C4 and C5) are considered equal.
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
scale_a = edo31.index_scale([0, 4, 18])
scale_b = edo31.index_scale([31, 35, 49])
print(scale_a.is_disjoint(scale_b, ignore_bi_index=True))
False
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
scale_a = limit5.rs_scale(['1/1', '5/4', '3/2'])
scale_b = limit5.rs_scale(['2/1', '5/2', '3/1'])
print(scale_a.is_disjoint(scale_b, ignore_bi_index=True))
False
from xenharmlib import WesternNotation
western = WesternNotation()
C4 = western.note('C', 4)
E4 = western.note('E', 4)
G4 = western.note('G', 4)
C5 = western.note('C', 5)
E5 = western.note('E', 5)
G5 = western.note('G', 5)
scale_a = western.scale([C4, E4, G4])
scale_b = western.scale([C5, E5, G5])
print(scale_a.is_disjoint(scale_b, ignore_bi_index=True))
False
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Cb4 = n_edo31.note('Cb', 4)
Eup4 = n_edo31.note('^E', 4)
G4 = n_edo31.note('G', 4)
Cb5 = n_edo31.note('Cb', 5)
Eup5 = n_edo31.note('^E', 5)
G5 = n_edo31.note('G', 5)
scale_a = n_edo31.scale([Cb4, Eup4, G4])
scale_b = n_edo31.scale([Cb5, Eup5, G5])
print(scale_a.is_disjoint(scale_b, ignore_bi_index=True))
False
Enharmonic ambiguity and set operations¶
When we speak of enharmonic equivalence, we mean that two notes refer to the same pitch, but are notated differently; that is: we differentiate between the sound of a note and its function. The question if Eb and D# in 12-EDO are the same can only be answered if we further specify: Same in regard to what?
In regard to notation Eb and D# are distinct; however, in regard
to sound, they are the same. Spoken in mathematical terms:
Notation systems in which there is enharmonic equivalence
define two equivalency relations on the space of notes. We already came
across both of them in the chapter about notes: The ==
operator considers notes as equal if their frequency is the same,
while the method is_notated_same checks for notation
equality.
The existence of two different equivalency relations on the space of notes poses a problem for us when we want to define scales and operations on them: Note scales are defined as ‘an ordered set of unique notes’, meaning that no two notes in a scale are the same. Now should this refer to notation or sound? After all, in cases where we are interested in the functional relationship of scales, we would want to treat D# and Eb as distinct, while in other cases we want them to be considered equal.
Xenharmlib defines the uniqueness restriction of the scale as “unique in
pitch”, meaning that no two notes e_flat and d_sharp with
e_flat == d_sharp can exist in the scale at the same time. However,
it provides special variants for some set operations that honor notational
differences (More on that later).
The reason for choosing the == equivalency as the base for the
uniqueness property is to provide consistency with the underlying pitch
scale object:
e_flat = n_edo12.note('Eb', 0)
d_sharp = n_edo12.note('D#', 0)
scale_a = n_edo12.scale([e_flat])
scale_b = n_edo12.scale([d_sharp])
note_union = scale_a.union(scale_b)
pitch_union = scale_a.pitch_scale.union(
scale_b.pitch_scale
)
# among others, these would break if we treat
# Eb and D# as distinct in note scales
assert note_union.frequencies == pitch_union.frequencies
assert len(note_union) == len(pitch_union)
pitch_intervals = [
i.pitch_interval for i in note_union.to_intervals()
]
assert pitch_intervals == pitch_union.to_intervals()
This choice also allows implementing the scale’s == operator
as a consistent logical extension of the note’s == operator,
creating a closed set algebra for scales under == with all
basic set operations. Think of it this way: Notes are variable names
holding the pitch as their value, with some variables being equal:
How does xenharmlib then choose the note representation of a pitch when more than one enharmonically equivalent note is present? During construction of a note scale, it chooses on the principle of first encounter:
scale = n_edo12.scale(
[
n_edo12.note('D#', 0),
n_edo12.note('Eb', 0),
]
)
assert len(scale) == 1
assert scale[0].pc_symbol == 'D#'
When using set operations Xenharmlib always prefers the representation of the scale that executed the operation:
c = n_edo12.note('C', 0)
g = n_edo12.note('G', 0)
e_flat = n_edo12.note('Eb', 0)
d_sharp = n_edo12.note('D#', 0)
scale_a = n_edo12.scale([c, e_flat])
scale_b = n_edo12.scale([d_sharp, g])
# A v B / A ^ B
union_a_b = scale_a.union(scale_b)
assert union_a_b.is_notated_same(
n_edo12.scale([c, e_flat, g])
)
intersection_a_b = scale_a.intersection(scale_b)
assert intersection_a_b.is_notated_same(
n_edo12.scale([e_flat])
)
# B v A / B ^ A
union_b_a = scale_b.union(scale_a)
assert union_b_a.is_notated_same(
n_edo12.scale([c, d_sharp, g])
)
intersection_b_a = scale_b.intersection(scale_a)
assert intersection_b_a.is_notated_same(
n_edo12.scale([d_sharp])
)
Using the == operator, we see that even though the results
are different in terms of representation, commutativity with regard
to union and intersection is still preserved:
assert union_a_b == union_b_a
assert intersection_a_b == intersection_b_a
In contrast, using the is_notated_same equivalency relation
on the two result pairs does not preserve commutativity:
assert not union_a_b.is_notated_same(union_b_a)
assert not intersection_a_b.is_notated_same(intersection_b_a)
Since we chose the == operator as a basis for scale uniqueness,
there is no way to define a closed set algebra for the is_notated_same
relation at the same time, because the union operation would not be well
defined.
However, on certain set operations, we can define the option to use
the is_notated_same relation. For intersection, we
can define that same-sounding, but differently notated notes should
not be part of the intersection. For difference, we can define
that such notes will not be removed from the first scale. In the
same way, we can define the relationships is_disjoint,
is_subset, and is_superset.
These stricter variants of the set operations are called:
c = n_edo12.note('C', 0)
g = n_edo12.note('G', 0)
e_flat = n_edo12.note('Eb', 0)
d_sharp = n_edo12.note('D#', 0)
scale_a = n_edo12.scale([c, e_flat])
scale_b = n_edo12.scale([d_sharp, g])
intersection_a_b = scale_a.note_intersection(scale_b)
assert len(intersection_a_b) == 0
diff_a_b = scale_a.note_difference(scale_b)
assert len(diff_a_b) == 2
In combination with the ignore_bi_index flag, we can, for
example, build a function that returns the pitch class symbols of
the common notes of two scales while being aware of the key:
def common_notes_key_aware(scale_a, scale_b):
scale_i = scale_a.note_intersection(
scale_b,
ignore_bi_index=True
).pcs_normalized()
return scale_i
# F# major and Gb major have exactly the same
# pitches, however, they are notated differently
f_sharp_maj = n_edo12.natural_scale().transpose(
n_edo12.shorthand_interval('A', 4)
)
g_flat_maj = n_edo12.natural_scale().transpose(
n_edo12.shorthand_interval('d', 5)
)
# D# minor is the relative key to F# major
d_sharp_min = f_sharp_maj.rotation(5)
print(common_notes_key_aware(f_sharp_maj, g_flat_maj))
print(common_notes_key_aware(g_flat_maj, d_sharp_min))
print(common_notes_key_aware(f_sharp_maj, d_sharp_min))
UpDownNoteScale([], 12-EDO)
UpDownNoteScale([], 12-EDO)
UpDownNoteScale([C#0, D#0, E#0, F#0, G#0, A#0, B0], 12-EDO)