Introduction

Harmonic primitives in xenharmlib are objects representing basic harmonic structures. They are created from and always relate to an origin context, i.e., a musical system that defines the meaning of their symbolic expression (e.g. a G#-4 has a different frequency in the context of 31-EDO than in the context of 12-EDO)

Origin Contexts

An origin context can take on the form of a tuning (a representation system built on integers or lattice points) or a notation representing musical objects with strings (like Eb-4). Generally speaking, notations are symbolic wrappers around tunings, meaning each primitive created from a notation can be reduced to an equivalent tuning primitive. The reverse is also true, however, with some restrictions: Because most of the time there are multiple symbolic representations for the same numeric pitch (for example, in Western tuning Eb and D# represent the pitch index), xenharmlib must make a guess on what symbolic representation to choose when translating a harmonic primitive from a tuning to a notation.

Harmonic primitives are built from builder methods of the origin context. These builder methods share (for the most part) an interface across contexts, making it possible to make analytical tools that are agnostic to the origin context, i.e., allowing to create one function that works on objects built from an EDO Tuning as well as on objects originating from an UpDownNotation context.

The smallest unit of representation, from which all harmonic primitives can be derived, is the frequency representation, which in tuning contexts is called pitch and in notation contexts note.

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

p10 = edo31.pitch(10)
print(p10.frequency.to_float())
20.448744438412696

From these “smallest” units other harmonic primitives can be built, which we call second-order harmonic primitives:

  • A structure of 2 frequency representations is called an interval

  • A sorted list of unique frequency representations is called a scale

  • A succession of multiple frequency representations is called a sequence

On top of these we have what we call third-order harmonic primitives:

  • A structure which represents the intervals between each successive frequency representation in a scale or sequence is called an interval sequence.

  • A structure which represents intervals relating the elements of a scale or sequence to a fixed tonic pitch is called an interval fan

Third-order harmonic primitives allow generalizations like “major scale” (with the root note being left undefined) and play a big role for utilities dealing with structures in the context of transformation theory.

Every primitive refers to its origin context by the origin_context property. Together with the unified interface of builder methods, this can be leveraged to create generalized utilities. Observe, for example, how the following function can be applied to sequences of all origins:

from xenharmlib import periodic

def steps_and_skips(sequence):

    context = sequence.origin_context
    scale = context.scale(sequence).period_normalized()
    result = []

    for a, b in zip(sequence, sequence[1:]):

        i_a = periodic.index(scale, a)
        i_b = periodic.index(scale, b)

        if abs(i_a - i_b) == 1:
            result.append('step')
        if abs(i_a - i_b) > 1:
            result.append('skip')
        if abs(i_a - i_b) == 0:
            result.append('-')

    return result

The function transforms notes in a sequence into a period-normalized scale and then uses that scale to calculate scale degrees of movement in the sequence, allowing us to differentiate between steps and skips in the sequence, regardless of sequence origin:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([0, 5, 10, 0, 10])
result = steps_and_skips(seq)
print(result)
['step', 'step', 'skip', 'skip']

Constants

Origin contexts define various constants that give necessary definitions to the methods of harmonic primitives and utilities. We will start with the eq_interval property that returns an interval object defining the equivalency interval:

from xenharmlib import EDOTuning

edo31 = EDOTuning(31)
print(edo31.eq_interval)
EDOPitchInterval(31, 31-EDO)

Especially in notations where there can be multiple enharmonic ways to define the equivalency interval, the constant returns the definitive object necessary to do calculations, so e.g. the method transforming a compound interval into a simple one does not only return an interval with the correct frequency ratio, but also with the correct spelling.

In the same way, the unison_interval constant provides the “neutral element” of the interval space. The unison interval is for example the reference point for xenharmlib to find out if an interval is descending or ascending.

from xenharmlib import EDOTuning

edo31 = EDOTuning(31)
print(edo31.unison_interval)
EDOPitchInterval(0, 31-EDO)

The zero_element constant defines the frequency representation that is considered the reference element for transposing harmonic primitives to their zero-normalized form:

from xenharmlib import EDOTuning

edo31 = EDOTuning(31)
print(edo31.zero_element)
EDOPitch(0, 31-EDO)

Since xenharmlib supports both integer and lattice point indexing, reference points for different indexing strategies are also given as constants, namely zero_index (the pitch index of the zero element), and unison_diff (the pitch difference of the unison interval), and eq_diff (the pitch difference of the equivalency interval)

from xenharmlib import EDOTuning

edo31 = EDOTuning(31)
print(edo31.zero_index)
print(edo31.unison_diff)
print(edo31.eq_diff)
0
0
31