from klotho import RhythmTree as RT, plot, play
from fractions import FractionProportional Duration¶
This notebook introduces the concept of proportional duration — the foundational principle behind Rhythm Trees and much of Klotho’s approach to time.
Before diving into tree structures and nesting, it’s important to understand how durations in a rhythm are specified as relative proportions rather than absolute values.
Imagine some duration of time, let’s say 1.
But... 1 what, exactly? It doesn’t actually matter. It could be 1 second, 1 hour, 1 whole-note, 1 quarter-note, etc... the exact duration is not what’s important. We’ll see why shortly.
rt = RT(subdivisions=(1,))
plot(rt, layout='ratios').play(bpm=120, beat='1/4')Ok, not too exciting, but you get the basic idea: this is one thing.
Now, divide that duration into four equal subdivisions. We’ll notate these subdivisions like this: (1 1 1 1). What does that look and sound like?
rt = RT(subdivisions=(1, 1, 1, 1))
plot(rt, layout='ratios').play(bpm=120, beat='1/4')Again, not that exciting, but you can see/hear what’s happening: this one thing is divided into four equal slices.
The durations in an RT are not absolute. They specify relative proportions:
print('Durations:', *[str(d) for d in rt.durations])
print('Sum: ', sum(rt.durations))Durations: 1/4 1/4 1/4 1/4
Sum: 1
Four equal ratios summing to 1. Why do they sum to 1? Because that was the size of our original block.
The proportions describe how the total block is divided, not how long anything is in absolute time.
What do we mean by this? Well, what do you think the resultant ratios will be if we define our four subdivisions as (5 5 5 5) instead of (1 1 1 1)? Let’s try...
rt = RT(subdivisions=(5, 5, 5, 5))
plot(rt, layout='ratios').play(bpm=120, beat='1/4')print('Durations:', *[str(d) for d in rt.durations])
print('Sum: ', sum(rt.durations))Durations: 1/4 1/4 1/4 1/4
Sum: 1
Ah ha, exactly the same. As they should be. Was that what you expected?
Also, there’s nothing inherently special about the number 4. We can divide this block of time into any number of segments:
for n in [3, 5, 7, 13]:
rt = RT(subdivisions=(1,)*n)
print(f'{n} equal slices:')
plot(rt, layout='ratios').play(bpm=120, beat='1/4')
print(f"{' + '.join(str(f) for f in rt.durations)} = {sum(rt.durations)}")
print()
print("...and so on...")3 equal slices:
1/3 + 1/3 + 1/3 = 1
5 equal slices:
1/5 + 1/5 + 1/5 + 1/5 + 1/5 = 1
7 equal slices:
1/7 + 1/7 + 1/7 + 1/7 + 1/7 + 1/7 + 1/7 = 1
13 equal slices:
1/13 + 1/13 + 1/13 + 1/13 + 1/13 + 1/13 + 1/13 + 1/13 + 1/13 + 1/13 + 1/13 + 1/13 + 1/13 = 1
...and so on...
The important thing to notice is that, no matter how many subdivisions we make, the total duration of the rhythm remains the same. This is why when we increase the number of slices, each slice becomes shorter and shorter—we’re “packing” more pieces into the same space.
What if the subdivisions are unequal? Like, (4 2 1 1)?
rt = RT(subdivisions=(4, 2, 1, 1))
plot(rt, layout='ratios').play(bpm=120, beat='1/4')print('Durations:', *[str(d) for d in rt.durations])
print('Sum: ', sum(rt.durations))Durations: 1/2 1/4 1/8 1/8
Sum: 1
Each segment accounts for a different proportion of the total block, with the last two being of the same proportion.
Let’s really drive home this notion of proportion. Above, we used subdivisions of (4 2 1 1). What would happen if we used (8 4 2 2) instead?
rt = RT(subdivisions=(8, 4, 2, 2))
plot(rt, layout='ratios').play(bpm=120, beat='1/4')print('Durations:', *[str(d) for d in rt.durations])
print('Sum: ', sum(rt.durations))Durations: 1/2 1/4 1/8 1/8
Sum: 1
Again, exactly the same. As they should be. Do you see why?
Let’s go back to our (4 2 1 1), example. What if we changed the first segment from 4 to 7, giving us (7 2 1 1)? What happens then?
rt = RT(subdivisions=(7, 2, 1, 1))
plot(rt, layout='ratios').play(bpm=120, beat='1/4')Alright, something changed. Do you see why? Let’s inspect the resultant ratios:
print('Durations:', *[str(d) for d in rt.durations])
print('Sum: ', sum(rt.durations))Durations: 7/11 2/11 1/11 1/11
Sum: 1
So, different ratios but the sum is still 1. Why?
The subdivisions (4 2 1 1) and (7 2 1 1) produce different ratios (and, thus, different rhythms), but in both cases the proportions account for the whole duration. These integers are relative proportions of a parent container, not absolute durations.
Let’s try a few more to build our intuition. How about (3 1 2)?
rt = RT(subdivisions=(3, 1, 2))
plot(rt, layout='ratios').play(bpm=120, beat='1/4')print('Durations:', *[str(d) for d in rt.durations])
print('Sum: ', sum(rt.durations))Durations: 1/2 1/6 1/3
Sum: 1
Three segments, three proportions: the first takes up half the total duration, the second a sixth, and the third a third. Three different fractions, but they still sum to 1 — the whole block.
What about something with more segments? Say, (1 3 2 1 5 2 3 1):
rt = RT(subdivisions=(1, 3, 2, 1, 5, 2, 3, 1))
plot(rt, layout='ratios').play(bpm=92, beat='1/4')print('Durations:', *[str(d) for d in rt.durations])
print('Sum: ', sum(rt.durations))Durations: 1/18 1/6 1/9 1/18 5/18 1/9 1/6 1/18
Sum: 1
More segments, more variety — but the same rule applies: the proportions always account for the entire block.
The key takeaway: no matter what integers we choose, they define relative proportions of a shared container. The absolute size of the container is irrelevant; only the ratios between the segments matter.
Rests¶
So far, every segment of our proportions has been a sounding duration. But what about silence?
Rests are notated as negative numbers. A negative value in the subdivisions takes up the same proportional share as its positive counterpart — it just means that segment is silent rather than sounding.
Let’s take our (4 2 1 1) example and make the second segment a rest by writing it as -2:
rt = RT(subdivisions=(4, -2, 1, 1))
plot(rt, layout='ratios').play(bpm=120, beat='1/4')Notice the greyed-out segment — that’s the rest. The proportions haven’t changed; |-2| still takes up the same share as 2 would. The only difference is that the segment is silent during playback.
We can place rests anywhere:
rt = RT(subdivisions=(-1, 1, -1, 1, -1, 1))
plot(rt, layout='ratios').play(bpm=120, beat='1/4')rt = RT(subdivisions=(3, 1, -2, 5, -1, 2))
plot(rt, layout='ratios').play(bpm=92, beat='1/4')The rule is simple: negative = silent, positive = sounding. The absolute value determines the proportion. There’s one constraint: the values cannot be zero (zero duration doesn’t make sense).
With proportional duration and rests, we have the raw materials for describing any rhythmic idea: how a block of time is divided, and which segments sound and which are silent. In the next notebook, we’ll look at the different types of rhythmic patterns that emerge from these simple principles.