Just Intonation and the Stern-Brocot Tree

Posted on Posted in Thoughts

Since I work in Just Intonation I’m always looking for new ways to organize and think about ratios. Recently I came across the mathematical idea called the Stern-Brocot Tree from a very nice video posted by a friend. This is a way to represent all whole number ratios in their lowest terms as a binary tree growing from 1:1. Any ratio can be reached through a number of successive left or right moves down the tree. For specifics please consult the above links of other sources as I will not cover them here.


I decided to implement ways of traversing the tree in a SuperCollider class (this depends on the MathLib and dewdrop_lib Quarks). This works based on three representations of any position: the actual ratio, a matrix, and a continued fraction. By jumping back and forth between these three forms, you can easily move left, right, up, compare properties of different ratios, etc. It is functional but pretty rough and subject to major refactoring whenever I get around to it.

My current focus with the tree involves ratios between 1:1 and 2:1 (otonality) and the difference tones between them. Since playing with this I’ve noticed a few interesting things (that I’m sure others have seen before too).

Within the tree structure we have this notion of a path, which is a sequence of right (R) and left (L) steps downward. In Just Intonation theory, one of the most important types of ratios is the superparticular ((n+1)/n). If we look at the tree we can see that each of these are reached by starting from 1:1, moving R to 2:1, followed by consecutive L moves. This means every superparticular ratio falls in the path of RLLLLL…. etc. This can be represented as an array based on the continued fraction form of the ratio. After studying this representation we can see that each element is the number of steps in a direction, starting with a R, and alternating LRLR after that. For example, the path to 4:3 is RLL and the continued fraction is [1,2] (one R followed by 2 Ls). 5:4 would be [1,3], 6:5 is [1,4] and so on.

Difference Tones against 1:1
Say you want to know what difference tone you will get between a given fundamental and some ratio above it where both pitches are in the same octave (between 1:1 and 2:1) as each other. The math for that is simple: just subtract the two. However I’ve found that there is a correlation between the difference tone of any ratio between 1:1 and 2:1 and its path within the tree. Take (3/2) – (1/1) = 1/2. The path to 3:2 is [1,1] and the path to 1:2 is [0,1] (the 0 means that we do not have any R moves, so our first move is an L). How about (5/3) – (1/1) = 2/3. 5:3 is [1,1,1] and 2:3 is [0,1,1]. Seeing a pattern? It turns out that the path of the difference tone against 1:1 for any ratio between 1:1 and 2:1 is the path to the ratio with the first R move set to 0. For 7:4 [1,1,2], the difference against 1:1 is [0,1,2].

Axis and Mirroring
The next concept is that of a mirror. All this means is that you take the opposite path (if you moved R before, move L this time) around a central ratio that we will call an axis. If our axis is 1:1, the mirror is simply the reciprocal (the mirror of 2:3 is 3:2). Why do I have a new term if this already has a name? Because if we mirror around ratios other than 1:1 the result is not the reciprocal. Using 1:1 as an axis is the special case where the mirror and the reciprocal are the same thing.

Lets look at this in terms of the path to a ratio first. The mirror of 2:1 [1] around the axis 1:1 is 1:2 [0,1]. The mirror of 3:2 [1,1] around 1:1 is 2:3 [0,1,1]. Like we said earlier, if you want to move L first instead of R, you make the first slot in the array 0. With the difference between a ratio and 1:1 we replaced the first element with 0. Here we insert a 0 before the remaining operations if we are going left of 1:1, and we remove the 0 if we are going right of 1:1.

But why? Well, the path to 1:1 is [0] since we don’t need to go anywhere to get to it, but that still doesn’t tell us the whole story.

For this we have to understand how a change of direction in the tree is achieved. First lets look at the paths to the whole numbers. 2:1 [1], 3:1 [2], 4:1 [3]; each of these are consecutive R moves from 1:1 [0]. Now look at 3:2 [1,1] and 3:1 [2] in the tree. They are both children of 2:1 and are also mirrors around 2:1 since one is to the L and the other to the R. We can represent this in the path by separating 2:1’s path from the paths of both ratios. If we think for a second about the path in terms of Rs and Ls rather than just numbers:

2:1 = [1] = R
3:1 = [2] = RR
3:2 = [1,1] = RL

it becomes rather obvious that we are simply moving from the first R. So 3:2 and 3:1 are mirrors around the axis 2:1 since their paths are inversions of each other starting from the axis. Here’s one more example with 3:2 as the axis:

3:2 = [1,1] = RL
7:5 = [1,2,1] = RLLR
8:5 = [1,1,1,1] = RLRL

Now that we can think of it as Rs and Ls, we can manipulate the continues fraction to represent that:

3:2 = {1,1}
7:5 = [{1,(1}+1),1]
8:5 = [{1,1},1,1]

Difference Tones against 3:2
In exploring more of the difference tones within the tree I found a very special property of 3:2. The difference tone of 3:2 and any of its children is the same as the difference between 3:2 and the child’s mirror around 3:2. Let’s take the ratios we were just looking at. The difference between 3/2 and 7/5 is 1/10. The mirror of 7:5 around the axis 3:2 is 8:5. The difference between 8/5 and 3/2 is 1/10. The same is true for any child of 3:2 and its mirror. Also interesting is that the denominator of all mirrors around 3:2 will be the same (e.g. 7/5, 8/5).

Some SC Code Examples
Here are some things you can do with the SuperCollider class so far:

// get the path from a ratio
SternBrocotTree.asContinuedFraction(3/2); // returns [1,1]
// or make one based on its path
SternBrocotTree.fromContinuedFraction([1,1]).asRational; // returns 3/2

// there are also extensions to the relevant classes to make this easier
(3/2).asContinuedFraction; // returns [1,1]
[1,1].fromContinuedFraction.asRational; // returns 3/2

// moving around the tree
1.sbLeft.asRational; // returns 1/2
(3/2).sbRight.asRational; // returns 5/3
(3/2).sbUp; // returns 2

// test whether two ratios are adjacent
SternBrocotTree.areAdjacent(3/2,5/3); // returns true
SternBrocotTree.areAdjacent(3/2,5/4); // returns false

// mirror around an axis
(5/4).sbMirrorAround(3/2).asRational; // returns 7/4

I’ve only been playing with this for a few days but I’ve found it quite interesting and useful. There’s plenty more where this came from I’m sure!