Pitches & Notes¶
As outlined in the introduction, frequency representations are the smallest building unit in the realm of harmonic primitives in xenharmlib. Other than second-order or third-order primitives, their method of construction is highly dependent on the context they originate from. We try to give a general overview of them here, but be sure to also check the specifics in the respective sections on tunings and notations.
We can roughly divide frequency representations into two classes: Pitches and notes. Pitches (which originate from tunings) are numeric representations of frequencies, while notes (originating from notations) are symbolic string representations of frequencies.
Numeric Representation¶
Depending on tuning, frequencies can be represented numerically either
through an integer or a lattice point. This parameter is called pitch
index in xenharmlib. The unified standard builder method to create
a frequency representation from a pitch index in a tuning is called
pitch():
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)
pitch = limit5.pitch(limit5.lattice.point((-1, 1, 0)))
print(pitch.frequency.to_float())
24.52739674693112
One purpose of the different tuning objects is to define the mapping
of pitch indices to frequencies, so the same numerical value can represent
a completely different frequency when tuning contexts are switched.
Observe how the above integer index 10 and the integer
vector (-1, 1, 1) are mapped to different frequencies
when applied to other tuning contexts:
from xenharmlib import EDOTuning
edo24 = EDOTuning(24)
edo31 = EDOTuning(31)
pitch_a = edo24.pitch(10)
pitch_b = edo24.pitch(31)
print(pitch_a.frequency.to_float())
print(pitch_b.frequency.to_float())
21.826764464562743
40.03046252816015
from xenharmlib import PrimeLimitTuning
from xenharmlib import MultiGenTuning
from xenharmlib import FrequencyRatio
limit5 = PrimeLimitTuning(5)
sg237 = MultiGenTuning(
[FrequencyRatio(p) for p in [2, 3, 7]],
eq_diff_vec=(1, 0, 0)
)
pitch_a = limit5.pitch(limit5.lattice.point((-1, 1, 1)))
pitch_b = sg237.pitch(sg237.lattice.point((-1, 1, 1)))
print(pitch_a.frequency.to_float())
print(pitch_b.frequency.to_float())
122.63698373465562
171.69177722851785
Pitches provide the pitch index from which they were created as a property for later inspection. In addition, pitches also provide a pitch class index that denotes the (integer or lattice point) equivalency class of the pitch index with regard to the equivalency interval (e.g., the octave) of the tuning:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
pitch = edo31.pitch(33)
print(pitch.pitch_index)
print(pitch.pc_index)
33
2
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
pitch = limit5.pitch(limit5.lattice.point((0, 1, 0)))
print(pitch.pitch_index)
print(pitch.pc_index)
LatticePoint(0, 1, 0)
LatticePoint(-1, 1, 0)
Tunings with lattice point indices provide a convenience builder method that does not expect a proper lattice point object, but a tuple from which then the tuning creates the lattice point object “under the hood”:
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
pitch = limit5.vec_pitch((0, 1, 0))
print(pitch.pitch_index)
print(pitch.pc_index)
LatticePoint(0, 1, 0)
LatticePoint(-1, 1, 0)
from xenharmlib import MultiGenTuning
from xenharmlib import FrequencyRatio
sg237 = MultiGenTuning(
[FrequencyRatio(p) for p in [2, 3, 7]],
eq_diff_vec=(1, 0, 0)
)
pitch = sg237.vec_pitch((0, 1, 0))
print(pitch.pitch_index)
print(pitch.pc_index)
LatticePoint(0, 1, 0)
LatticePoint(-1, 1, 0)
Symbolic Representation¶
If frequencies are represented by strings (or combinations of strings and
numbers) we speak of these symbolic representations as notes.
Notes are created by the note()
method of notations. The specific creation parameters depend on the type
of notation that is employed:
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)
Aup4 = n_edo31.note('^A', 4)
print(Aup4.frequency.to_float())
447.44087978031575
In the same way notations are wrappers around tunings, notes are wrappers around pitches. Every note object can be transformed into the corresponding pitch object:
from xenharmlib import WesternNotation
western = WesternNotation()
A4 = western.note('A', 4)
print(A4.pitch)
EDOPitch(57, 12-EDO)
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Aup4 = n_edo31.note('^A', 4)
print(Aup4.pitch)
EDOPitch(148, 31-EDO)
Pitches can also be transformed into a corresponding note object; however, because of enharmonic equivalence, this transformation is not necessarily unique. In the Western system, for example, there can be infinite corresponding note objects to a single pitch object (think of G#4, Ab4, Bbbb4, Cbbbb5 all referring to the same key on a piano), so xenharmlib makes a guess:
from xenharmlib import WesternNotation
western = WesternNotation()
tuning = western.tuning
pitch = tuning.pitch(8)
note = western.guess_note(pitch)
print(note)
WesternNote(G#, 0)
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
pitch = edo31.pitch(19)
note = n_edo31.guess_note(pitch)
print(note)
UpDownNote(Abb, 0, 31-EDO)
Most of the time it is not necessary to convert pitches and notes into one another, because they expose the same interface with regard to methods and properties; e.g., each note can be inspected with regard to its pitch index and pitch class index:
from xenharmlib import WesternNotation
western = WesternNotation()
A4 = western.note('A', 4)
print(A4.pitch_index)
print(A4.pc_index)
57
9
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Aup4 = n_edo31.note('^A', 4)
print(Aup4.pitch_index)
print(Aup4.pc_index)
148
24
Construction Based on Closest Frequency¶
Origin contexts based on integer indices allow construction based on the
approximation of frequencies. Given an arbitrary frequency, the
closest_freq_repr()
method returns the pitch or note of an origin context that is closest
to it:
from xenharmlib import EDOTuning
from xenharmlib import Frequency
edo31 = EDOTuning(31)
pitch = edo31.closest_freq_repr(Frequency(500))
print(pitch)
print(pitch.frequency.to_float())
EDOPitch(153, 31-EDO)
500.367260159388
from xenharmlib import WesternNotation
from xenharmlib import Frequency
western = WesternNotation()
note = western.closest_freq_repr(Frequency(450))
print(note)
print(note.frequency.to_float())
WesternNote(A, 4)
440.0
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
from xenharmlib import Frequency
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
note = n_edo31.closest_freq_repr(Frequency(500))
print(note)
print(note.frequency.to_float())
UpDownNote(Cb, 5, 31-EDO)
500.367260159388
We mentioned that only tunings and notations based on integer pitch indices support “closest approximation”. This is because in multi-generator tunings like prime limit tunings there are infinite representations between every two representations, meaning that a single “closest” pitch can not be found:
from xenharmlib import WesternNotation
from xenharmlib import PrimeLimitTuning
western = WesternNotation()
limit7 = PrimeLimitTuning(7)
try:
western.note('F#', 4).retune_closest(limit7)
except Exception as exc:
print(exc)
Not possible to find a closest representation to the given frequency, either because in this harmonic context frequencies can be approximated arbitrarily close (so there is no closest representation) or the way in which this harmonic context is defined is not restricted enough to mathematically deduce a method of approximation.
Identity, Comparison and Equivalency¶
All frequency representations can be compared to one another and tested for equality, regardless of the tuning or notation they originate from. This is because at the core they are, well, representations of frequencies, and frequencies can be compared to one another (which frequency is bigger?) or tested for equality (are these two frequencies the same?)
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
from xenharmlib import WesternNotation
edo24 = EDOTuning(24)
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
western = WesternNotation()
print(western.note('Gb', 4) == western.note('F#', 4))
print(edo24.pitch(14) == western.note('G', 0))
print(n_edo31.note('G', 4) < western.note('G', 4))
print(edo31.pitch(19) <= western.note('G', 0))
print(n_edo31.note('G', 4) == western.note('G', 4))
True
True
True
False
False
Equality/Identity also translates to Python’s built-in set type. If two frequency representations 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()
pitch_a = edo24.pitch(14)
pitch_b = western.note('G', 0)
# since the second element is equal to the first
# only the first element will be added to the set
print({pitch_a, pitch_b})
{EDOPitch(14, 24-EDO)}
If the origin contexts have the same equivalency interval constant, frequency representations can also be tested across contexts with regard to their equivalency:
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
from xenharmlib import WesternNotation
edo24 = EDOTuning(24)
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
western = WesternNotation()
print(edo24.pitch(14).is_equivalent(western.note('G', 5)))
print(n_edo31.note('G', 4).is_equivalent(western.note('G', 5)))
True
False
If, however, origin contexts have a different equivalency interval constant, testing for equivalency will fail, because the operation is undefined:
from xenharmlib import EDTuning
from xenharmlib import FrequencyRatio
from xenharmlib import WesternNotation
bohlen_pierce = EDTuning(13, FrequencyRatio(3))
western = WesternNotation()
print(bohlen_pierce.eq_interval.frequency_ratio)
print(western.eq_interval.frequency_ratio)
try:
western.note('C', 4).is_equivalent(bohlen_pierce.pitch(4))
except Exception as exc:
print(exc)
FrequencyRatio(3)
FrequencyRatio(2)
Equivalency can only be tested for notes from tunings with the same equivalency interval
Indices, Pitch Classes and Base Intervals¶
As we have outlined in the earlier part of this page, every frequency representation has a “pitch index” that denotes the distance from the “zero element” of an origin context. Depending on the origin context this distance can take the form of an integer or a lattice point.
To get an intuition for the concept of the pitch index, we can imagine the 12-EDO / Western Notation case: Here the zero element is simply the C0 key, and the pitch index is the number of successive key presses necessary to reach the pitch in question:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
pitch = edo31.pitch(8)
print(pitch.pitch_index)
8
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
pitch = limit5.vec_pitch((0, 1, 0))
print(pitch.pitch_index)
LatticePoint(0, 1, 0)
from xenharmlib import WesternNotation
western = WesternNotation()
A4 = western.note('A', 4)
print(A4.pitch_index)
57
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Aup4 = n_edo31.note('^A', 4)
print(Aup4.pitch_index)
148
Pitch indices can be “sorted into bins” by equivalency. In the above graphic, you can see that pitch indices 0, 12 and 24 are all Cs. The unified representative for that bin is called “pitch class index” and is defined as the lowest positive representative (in case of C: 0). If we divide the piano by the equivalency interval (in the Western system the octave), we can describe each key by two parameters: The pitch class and the base interval index:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
pitch = edo31.pitch(39)
print(pitch.pc_index)
print(pitch.bi_index)
8
1
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
pitch = limit5.vec_pitch((0, 1, 0))
print(pitch.pc_index)
print(pitch.bi_index)
LatticePoint(-1, 1, 0)
1
from xenharmlib import WesternNotation
western = WesternNotation()
A4 = western.note('A', 4)
print(A4.pc_index)
print(A4.bi_index)
9
4
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Aup4 = n_edo31.note('^A', 4)
print(Aup4.pc_index)
print(Aup4.bi_index)
24
4
Transposition¶
Frequency representations can be transposed. This is typically done
by providing an interval object of the same
origin context. One way to create an interval is to use the
interval() method
of the frequency representation:
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)
p3 = edo31.pitch(3)
p10 = edo31.pitch(10)
p18 = edo31.pitch(18)
interval = p10.interval(p18)
print(p3.transpose(interval))
EDOPitch(11, 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
pitch_a = limit5.vec_pitch((0, 0, 0))
pitch_b = limit5.vec_pitch((-2, 0, 1))
pitch_c = limit5.vec_pitch((-1, 1, 0))
interval = pitch_a.interval(pitch_b)
print(pitch_b.transpose(interval))
PrimeLimitPitch(25/16, 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
D4 = western.note('D', 4)
Gb4 = western.note('Gb', 4)
A4 = western.note('A', 4)
interval = Gb4.interval(A4)
print(D4.transpose(interval))
WesternNote(E#, 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)
interval = Gb4.interval(Aup4)
print(Gb4.transpose(interval))
UpDownNote(^A, 4, 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)
p3 = edo31.pitch(3)
print(p3.transpose(11))
EDOPitch(14, 31-EDO)
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
pitch = limit5.pitch(limit5.lattice.point((-2, 0, 1)))
transposed = pitch.transpose(limit5.lattice.point((-1, 1, 0)))
print(transposed)
PrimeLimitPitch(15/8, 5-Limit)
from xenharmlib import WesternNotation
western = WesternNotation()
D4 = western.note('D', 4)
print(D4.transpose(2))
WesternNote(E, 4)
from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
Aup4 = n_edo31.note('^A', 4)
print(Aup4.transpose(-1))
UpDownNote(A, 4, 31-EDO)
Retuning by Closest Correspondence¶
In the beginning we talked briefly about a family of “bridge functions” that make a symbolic representation into a numeric one (and vice versa). We have also shown how to find the closest representative in a tuning or notation for a given frequency.
If a target context allows approximation to a closest frequency, we can also directly map a frequency representation of one tuning to another:
from xenharmlib import EDOTuning
from xenharmlib import WesternNotation
from xenharmlib import UpDownNotation
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
western = WesternNotation()
retuned_a = western.note('F#', 4).retune_closest(edo31)
retuned_b = western.note('F#', 4).retune_closest(n_edo31)
retuned_c = edo31.pitch(18).retune_closest(western)
print(retuned_a)
print(retuned_b)
print(retuned_c)
EDOPitch(139, 31-EDO)
UpDownNote(F#, 4, 31-EDO)
WesternNote(G, 0)