Custom Multi-Generator Tunings

Overview

Multi-Generator Tunings are a generalization of Prime Limit Tunings. While harmonic primitives in Prime Limit Tuning are characterized by instances of prime generators like

\(\frac{n}{d} = p_1^{x_1} \cdot p_2^{x_2} \cdot ... \cdot p_k^{x_k}\)

Multi-generator tunings relax the requirement for generators, so they can be any frequency ratio, even irrational ones, e.g.

\(2^{x_1} \cdot (3 \cdot (\frac{80}{81})^{\frac{1}{4}})^{x_2}\)

Multi-Generator Tunings are created by providing the generator frequency ratios and a period vector that represents a lattice point defining the period, so another way to create a 3-Limit Tuning is this:

from xenharmlib import MultiGenTuning
from xenharmlib import FrequencyRatio

limit3 = MultiGenTuning(
    generators=(FrequencyRatio(2), FrequencyRatio(3)),
    eq_diff_vec=(1, 0) # equivalency interval ratio 2
)

Irrational frequency ratios (like the one in the introductory example) can be created using frequency ratio arithmetic. For example, the following code example creates a multi-generator quarter-comma-meantone tuning:

from fractions import Fraction
from xenharmlib import MultiGenTuning
from xenharmlib import FrequencyRatio

g3 = FrequencyRatio(3) * FrequencyRatio(80, 81) ** Fraction(1, 4)
tuning = MultiGenTuning(
    (FrequencyRatio(2), g3),
    eq_diff_vec=(1, 0)
)

Even EDO tunings can be constructed by custom multi-generator tunings, e.g., an unnecessarily complicated way to construct 31-EDO is this:

edo31 = MultiGenTuning(
    (FrequencyRatio(2) ** Fraction(1, 31),),
    eq_diff_vec=(31,)
)

Like in Prime-Limit-Tunings, pitch indices and pitch differences in Multi-Generator Tunings are lattice points:

from fractions import Fraction
from xenharmlib import MultiGenTuning
from xenharmlib import FrequencyRatio

g3 = FrequencyRatio(3) * FrequencyRatio(80, 81) ** Fraction(1, 4)
qcm = MultiGenTuning(
    (FrequencyRatio(2), g3),
    eq_diff_vec=(1, 0)
)
G0 = qcm.pitch(qcm.lattice.point((-1, 1)))

print(G0)
MultiGenPitch((-1, 1), G=(2, 2*5**(1/4)))

As a shortform, multi-generator tunings also support builder methods that only demand the vector tuple instead of the full lattice point object:

from xenharmlib import MultiGenTuning

g3 = FrequencyRatio(3) * FrequencyRatio(80, 81) ** Fraction(1, 4)
qcm = MultiGenTuning(
    (FrequencyRatio(2), g3),
    eq_diff_vec=(1, 0)
)
G0 = qcm.vec_pitch((-1, 1))

print(G0)
MultiGenPitch((-1, 1), G=(2, 2*5**(1/4)))

Prime-Limit Isomorphism

You might have noticed that for our quarter-comma meantone example we used slightly similar generators than we would use if we would create a 3-Limit tuning. The difference in the second generator is only marginal:

g3 = FrequencyRatio(3) * FrequencyRatio(80, 81) ** Fraction(1, 4)
print((FrequencyRatio(3) / g3).cents)
5.3765723992

What did we do here? We took the original frequency ratio of 3 and changed it slightly to get a different harmonic profile. This construction is called tempering: You take a Prime Limit Tuning and change various generators slightly to produce a new harmonic system:

Keeping the structure of the lattice points the same (both 3-Limit tuning and our quarter-comma-meantone tuning are indexed by lattice points having two dimensions), we can easily create tempered versions of prime limit pre-images:

from xenharmlib import PrimeLimitTuning
from xenharmlib import MultiGenTuning
from xenharmlib import play

limit3 = PrimeLimitTuning(3)

g3 = FrequencyRatio(3) * FrequencyRatio(80, 81) ** Fraction(1, 4)
qcm = MultiGenTuning(
    (FrequencyRatio(2), g3),
    eq_diff_vec=(1, 0)
)

l3_minor_triad = limit3.rs_scale(['1/1', '81/64', '3/2'])

# the tempered triad
qcm_minor_triad = qcm.vec_scale(
    l3_minor_triad.monzos
)