Harmonic Lattices

tuning

August 18, 2021

F#5E5B4E#5G#5C#5F#5E5B4A4A4D5G5C5Bb4F5Db5D5G5C5

In my post about Pure Intervals, I used two ways to bring order to the infinite amount of ratios of natural numbers: The harmonic series and the Lambdoma. Later, I investigated the 3-limit and 5-limit tuning systems, which are another way to organize the landscape of Just Intonation.

Another powerful way to vizualize the "harmonic space" of any collection of ratios is using a so called lattice.

Note: This post is not optimized for dark mode.

Table of Contents

Lattices

A lattice organizes ratios by prime factorization. Before understanding how that works, let's have a look:

3 Limit Lattice

The (not really useful) "lattice" of a 3 limit pentatonic scale looks like this:

113243982716
  • The y direction (vertical) represents powers of 3 (fifths + octave), in this case from -1 to 3.
  • The x direction (horizontal) represents powers of 2 (octaves), in this case -4, -3, -1, 0 and 2, deliberately chosen to move all pitches in the same octave (between 1 and 2).

We could further fill the x direction with different octaves of the same pitch:

121121916989423433132342782716

To hear how the fractions sound, click on the colored circles

If we agree on the fact that octaves can be seen as the same pitch class (octave equivalence), we can ignore the dimension of the octave completely:

119843322716

This leaves us just 1 dimension, where the 3 is the x axis. This "trick" saves us 1 dimension for the upcoming higher limit systems.

5 limit lattice

In a 5 limit system, we can keep the 3 (fifths) as the x axis, and add the 5 (thirds) on the y axis:

32981581165545343169161585

The slider controls the maximum complexity of intervals, using Tenney Height, which will be the topic of a future post.

In many cases, the lattice will be drawn like that:

32981581165545343169161585

Sheering the y axis to the right results in triangles, where those that point upwards are major and those that point downwards are minor.

7 limit lattice

In a 7 limit system, we need one more dimension for 7ths (btw: it's just a coincidence that 7 = 7ths). To draw it in 3d, we can just use a 45 degree angle:

329815811655453744316916158587

With more and more fractions, it gets difficult to overview the lattice. As we now have 3 dimensions, we could draw it in 3d space:

B4G#5C#5F#5E5B4A4D5G5C5Bb4F5G5

11 limit lattice

If we add just another dimension with 11 limit (adding augmented 4ths), we run out of brain power.. vizualizing 4 dimensions in 2d is kind of impossible. But we can just use another angle for this new dimension that won't get too much in our way:

32981154741184316985871611

13 limit lattice

To go full on crazy, we can add a 5th dimension:

329811547411813843169858716111613

Calculating Prime Vectors aka monzos

Let's talk about the details on how to calculate the vector of any ratio. For example, a 3 limit tuning system consists of ratios generated by

r=2x3yr = 2^x * 3^y

To calculate the coordinates of any ratio inside 3 limit, we need to factorize the ratio into its prime components. We can then use the the exponents as a vector:

11=1=(00)\frac{1}{1} = 1 = \begin{pmatrix} 0\\0 \end{pmatrix}
32=2131=(11)\frac{3}{2} = 2^{-1}*3^{1} = \begin{pmatrix} -1\\1 \end{pmatrix}
43=223=2231=(21)\frac{4}{3} = \frac{2*2}{3} = 2^{2}*3^{-1} = \begin{pmatrix} 2\\-1 \end{pmatrix}
98=33222=2332=(32)\frac{9}{8} = \frac{3*3}{2*2*2} = 2^{-3}*3^{2} = \begin{pmatrix} -3\\2 \end{pmatrix}
2716=3332222=2433=(43)\frac{27}{16} = \frac{3*3*3}{2*2*2*2} = 2^{-4}*3^{3} = \begin{pmatrix} -4\\3 \end{pmatrix}

The resulting vectors can then be used to draw the ratio to the correct coordinates.

The same calculation can be used with more dimensions by just adding more prime factors.

Those prime vectors are also called monzo.

In Code

What we want:

expect(primevector(4)).toEqual([2]); // 2^2
expect(primevector(12)).toEqual([2, 1]); // 2^2 + 3^1
expect(primevector(126)).toEqual([1, 2, 0, 1]);
expect(primevector(15)).toEqual([0, 1, 1]);

How I did it:

export function primevector(n) {
  const limit = primelimit(n);
  const dimensions = primes(2, limit);
  const powers = primepowers(n);
  return dimensions.map((d) => powers.find(([f]) => f === d)?.[1] || 0);
}

export function primelimit(n) {
  return max(primefactors(n));
}

export function primefactors(n) {
  const factors = [];
  let divisor = 2;
  while (n >= 2) {
    if (n % divisor == 0) {
      factors.push(divisor);
      n = n / divisor;
    } else {
      divisor++;
    }
  }
  return factors;
}

export function primes(from, to) {
  const primes = [];
  for (let i = from; i <= to; ++i) {
    if (isPrime(i)) {
      primes.push(i);
    }
  }
  return primes;
}

export function isPrime(num) {
  for (var i = 2; i < num; i++) {
    if (num % i === 0) {
      return false;
    }
  }
  return true;
}

export function primepowers(n) {
  const factors = primefactors(n);
  const powers = [];
  let latest;
  for (let factor of factors) {
    if (!latest || latest !== factor) {
      powers.push([factor, 1]);
    } else {
      powers[powers.length - 1][1] += 1;
    }
    latest = factor;
  }
  return powers;
}

Conclusion

In this post, we saw how to construct harmonic lattices for any number of dimensions. In a future post, I want to talk about alternative just intonation systems like combination product sets, which can also be vizualized in a lattice.

Felix Roos 2022