Posttonal Basics¶
Pitch Class Sets and Scales¶
In textbooks on post-tonal theory, the definition of pitch class sets is often ambiguous. In some applications a pitch class set is considered unordered. In others (for example in normal form calculation) the order is vital. To account for both cases pitch class sets in xenharmlib are always lists of unique pitch classes.
Xenharmlib implements pitch class set arithmetic as a byproduct of scale
arithmetic. Every scale object in xenharmlib has a
pc_indices property
that lists pitch classes in the order they appear in the scale:
from xenharmlib import EDOTuning
edo12 = EDOTuning(12)
scale = edo12.index_scale(
[2, 4, 7, 9, 11, 13, 16, 18]
)
print(scale.pc_indices)
[2, 4, 7, 9, 11, 1, 4, 6]
However, pc_indices
does not always return a collection of unique elements. For example,
in the list above, pitch class 4 appears twice. This is due to the
flexibility of xenharmlib’s scale definition. Unlike many traditional
textbook definitions, the scale object permits multiple equivalent
(albeit not identical) pitches to coexist and also allows pitches
to span multiple octaves. See the following diagram for illustration:
One way to ensure that the elements are unique is calling the
pcs_normalized
method of the scale:
pcsn_scale = scale.pcs_normalized()
print(pcsn_scale.pc_indices)
[1, 2, 4, 6, 7, 9, 11]
The normalization transposes every pitch into the first base interval. This has two effects: First, it guarantees the uniqueness of the pitch classes in the list. Secondly, it guarantees that pitch classes occur in a strictly ascending order.
However, this kind of transformation is not always desirable, as it eliminates the mode information of a scale (for instance, the C major and A minor scales result in the same scale when pcs normalized). A better way to make the list of pitch classes unique is period normalization which transposes every pitch into the base interval spanned by the root pitch of the original scale (highlighted in red below):
This transformation can be executed on the scale like this:
pn_scale = scale.period_normalized()
print(pn_scale.pc_indices)
[2, 4, 6, 7, 9, 11, 1]
Period normalization guarantees uniqueness, but not that pitch classes are in ascending order (as you can see from the jump from 11 to 1). They guarantee however that at most one such violation of the order occurs.
If visualized as a closed necklace the resulting pitch class set of both normalizations share the same geometric form, however the starting point (highlighted in green) is different:
Transposition¶
Period normalized scales have a nice quality to it: They form a closed subset of scales in regard to transposition, meaning that every time you transpose a period-normalized scale your result will be another period normalized scale:
pn_scale_t3 = pn_scale.transpose(3)
print(pn_scale_t3.pc_indices)
[5, 7, 9, 10, 0, 2, 4]
From the viewpoint of the
pc_indices property a
transposition is an addition mod period size (mod 12 in our example).
In the closed necklace visualization, it amounts to a rotation of the
necklace:
To learn more about scale normalization methods read the section on normalization in the Advanced Scale Guide.
We now have established how to apply the post-tonal \(T_n\) operation on a pitch class set by using scale operations and can now move on to the \(I\) (inversion) operation.
Inversion¶
Xenharmlib implements a generalization of the \(I\) operation, called
reflection(), that reflects the scale
around an arbitrary axis pitch. Without any parameters it reflects around
the zero element of the origin context (pitch 0 in tunings, C0 in
UpDownNotation).
scale = edo12.index_scale(
[1, 2, 4, 5, 6, 7, 9, 11]
)
refl = scale.reflection()
print(refl)
EDOPitchScale([-11, -9, -7, -6, -5, -4, -2, -1], 12-EDO)
Reflection is most easily understood visually. In the graphic below, the source and target pitches are represented by the same color. As shown, reflection is accomplished by calculating the interval from the reflection point (outlined in red) and applying the negative of that interval back to the reflection point:
From the perspective of the
pc_indices property
invoking reflection() without
a parameter applies the \(I\) operation on the pitch class set.
scale = edo12.index_scale(
[1, 2, 4, 5, 6, 7, 9, 11]
)
refl = scale.reflection()
print(refl.pc_indices)
[1, 3, 5, 6, 7, 8, 10, 11]
On the closed necklace graph of the pitch class set, this operation is equivalent to flipping the necklace across the horizontal line passing through 0.
If another pitch should serve as the reflection axis it can be provided as an argument:
scale = edo12.index_scale(
[1, 2, 4, 5, 6, 7, 9, 11]
)
refl = scale.reflection(
edo12.pitch(6)
)
print(refl)
EDOPitchScale([1, 3, 5, 6, 7, 8, 10, 11], 12-EDO)
Normal Form¶
The normal form transforms all scales that are related by rotation into a single representative, more formally for two scales \(S_1\) \(S_2\), rotation function \(R\) and normal form \(N\) it holds that:
In more concrete terms this means that all modes of a scale and all inversions of a chord have the same normal form, e.g. “C Major” and “D Dorian” have the same normal form or the “C4-E4-G4” chord has the same normal form as its inversion “E4-G4-C5”.
There are several ways to define the normal form, with the most well-known being those proposed by Alan Forte and John Rahn. Both methods seek to identify the rotation in which the scale’s intervals are most tightly clustered toward the left, though they differ slightly in their criteria, leading to different representatives in a few cases.
Xenharmlib provides both variants as part of the set class package:
nf_forte() for Forte’s definition and
nf_rahn() for Rahns’s definition respectively.
While in textbooks the normal form is often given in a notation like “[013468T]” (with T representing 10), xenharmlib’s normal form functions do not return a string but a scale object of the same type as the input:
from xenharmlib import EDOTuning
from xenharmlib.setc import nf_forte
edo12 = EDOTuning(12)
scale = edo12.index_scale([1, 4, 6, 7, 9, 11])
n_scale = nf_forte(scale)
print(n_scale)
EDOPitchScale([4, 6, 7, 9, 11, 13], 12-EDO)
To retrieve the normal form in its pitch class representation
the scale attribute
pc_indices
must be used:
print(n_scale.pc_indices)
[4, 6, 7, 9, 11, 1]
Prime Form¶
The prime form transforms all scales that are related by rotation, transposition and inversion into a single representative.
Xenharmlib provides both the Forte and the Rahn variant of the prime
form transformation: primeform_forte() and
primeform_rahn() respectively.
The prime form is calculated as follows:
Transform the scale to its normal order.
Zero-normalize the scale
Generate the inversion of the resulting scale, transform it to its normal order and zero-normalize it.
Compare scales from step 2 and 3 and choose the one with intervals most tightly packed to the left (using Forte’s or Rahn’s algorithm)
An example for 12-EDO:
from xenharmlib import EDOTuning
from xenharmlib.setc import primeform_forte
edo12 = EDOTuning(12)
scale = edo12.index_scale([1, 4, 6, 7, 9, 11])
n_scale = primeform_forte(scale)
print(n_scale)
EDOPitchScale([0, 2, 3, 5, 7, 9], 12-EDO)
To retrieve the prime form in its pure pitch class representation
use the scale attribute
pc_indices:
print(n_scale.pc_indices)
[0, 2, 3, 5, 7, 9]