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

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

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

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)

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

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)

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)

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

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

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:

An image of a piano with pitch index labels
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

pitch = edo31.pitch(8)
print(pitch.pitch_index)
8

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:

An image of a piano with pitch index labels
from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

pitch = edo31.pitch(39)
print(pitch.pc_index)
print(pitch.bi_index)
8
1

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)

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)

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)