I’ve been conducting some work on the automated generation of nested tuplets using OpenMusic rhythm trees. I’m going to talk about one solution that I’ve developed which takes a metric structure as its input, and arbitrarily divides and populates the resulting tree:
This is a proof of concept example, and the output is not necessarily the most musical example(!), but it does demonstrate a viable approach to top-down rhythm tree construction, and formatting. Before we look at how the function/loop works, let's have a quick look at rhythm trees more generally.
Here is a most basic example of a rhythm tree being supplied to a VOICE object that is set to display only rhythmic information:
On the left we have a basic rhythm tree which states that the metric structure is 4/4 (4 4), and there are four beats within it (1 1 1 1). On the right, we have the same metric structure as before (4 4), yet we have five beats (1 1 1 1 1), which is longer than the duration of the metric structure, resulting in five beats in the time of four: 5:4. The process of subdividing beats is similar:
The left shows that we have the same metric structure as before, a bar of 4/4, and four crotchet beats. However, on the left the first beat has been subdivided into two quavers (1 (1 1)) – one beat long, divided into two. On the right we have told it that it will be one beat long, divided into three: (1 (1 1 1)). This is the basic principle that OpenMusic follows: the length of the beat, and how the beat is divided, if at all. For a more detailed discussion of OpenMusic rhythmic tree structure, please see the OpenMusic Documentation on this subject.
Having briefly recapped on the rhythm tree format, we can turn to the process I'd like to discuss, and is included in the downloadable patch at the bottom, r_tree_gen. The first thing to address is the input r_tree_gen expects: a list of metrical structures, or more simply, a list of time signatures:
( (number_of_beats duration_of_beat) (number_of_beats duration_of_beat) ... )
The internal structure of r_tree_gen looks like this:
This information we provide r_tree_gen is then passed to two different functions within the OMLOOP: break_up_beats, and populate_beats. The purpose of break_up_beats is to randomly create a number of sub-beats from the main metric structures provided, and the purpose of populate_beats is to randomly create rhythmic units to populate the new beat structure provided by break_up_beats. Let’s look at break_up_beats.
break_up_beats is passed the number_of_beats from the specified metric structure we initially gave r_tree_gen, and then proceeds to do several things with it. Let’s think of this as two separate processes:
a) Divide the number_of_beats by two, and round to the nearest whole number;
b) Generate a number randomly between ‘1’ and the result of a);
The output of BUB1 goes to two places, the accumulator, and the collector. The flow here is slightly convoluted, so bear with me. The accumulator serves two functions: it is primarily used to control the flow of the process, and secondarily to pass information to the collect. For each pass of the loop, the accumulator stores the value output by BUB1. It then passes this value through its second outlet to the collector, and it passes the total value it has accumulated so far out of its first outlet. The total accumulated is checked against the value passed to the whole process - number_of_beats from earlier - and while the accumulator’s value is less than or equal to the value of number_of_beats, the loop continues - passing the output of BUB1 to the collector each time. When the total value of the accumulator surpasses that of number_of_beats, this part of the r_tree_gen ends, and the total new beat structure is passed to the next function in the process: populate_beats. To summarise what happens in BUB2:
a) Take the output of BUB1, and accumulate it.
b) Check the total accumulated value against the loop’s input, number_of_beats. If it surpasses this value, end the loop, otherwise continue.
c) Pass the last accepted value that was input into the accumulator to the collector from the accumulator’s second output. This will be the new metric structure passed to populate_beats.
For the metric structure I’ve used in this example, I typically get an output from break_up_beats that look like this: 5:(1 3 4) 3:(1 2 3) 12:(3 5 7 8) 4:(1 3 4).
Now we can look at what happens in populate_beats.
The purpose of populate_beats is to take the metric structures passes to it from break_up_beats, and create rhythms which extend beyond the expected duration of that beat, thus creating a tuplet. The heart of this process is the repeat-n unit, which actually populates the beat. The repeat-n will randomly create a number of divisions, which will at random take a rhythmic value from the nth-random: -1, or 1. Negative values denote a rest of that value, as opposed to an attack of that value; as such, the nth-random is effectively deciding between a rest, or an attack. The number of times the nth-random does this is equivalent to the length of the beat, plus a random number of times, and indicated by the om-random(1 3). The output of repeat-n is passed to the list object to make sure it is in the correct format: ((1 1 -1 1 1 1 -1 -1 -1)). This is then appended to the input of populate_beats, which was passed to it from break_up_beats, resulting in something like this: (5 (-1 -1 1 1 -1 1 1 1)). As you can see, this matches the formatting of a beat’s length, and the subdivisions within in, as demonstrated earlier. This value is then passed to the collector. This process happens for each value passed to it from break_up_beats, and finally, the output from the collect is enclosed in another level of parenthesis by a list object to make sure it is in the correct format, and then passed to the next stage of the process. The output of populate_beats when supplied with the input of (1 3 4), gives me: (((1 (1 1 -1)) (3 (-1 -1 -1 1 -1 1)) (4 (1 -1 -1 -1 1 1)))). In summary:
a) Randomly add a value between 1 and 3 to the value passed from break_up_beats;
b) Taking the value from a), randomly take a value from nth-random(-1 1) a) times using repeat-n;
c) Enclose the output of b) using a list object;
d) The output of c) is appended with the initial value passed from break_up_beats;
e) Collect the output of d) as many times as there are items passed to populate_beats from break_up_beats;
f) Finally, enclose the output from collect’s second outlet using a list object to make sure it is in the correct format for the rest of the process.
We are now done describing what goes on in the two core functions of this loop, break_up_beats, and populate_beats. We can now look at what happens in the rest of the top-level loop.
r_tree_gen, the loop which we initially gave our structure of ((5 4) (3 16) (12 128) (4 2)) to really only does two things. It passes the values we give it to the two functions we’ve talked about, and it also formats their outputs in a way that the OpenMusic VOICE objects understands. We can go through this process linearly:
a) Pass the first value of each item in the metric structure – the number of beats – to break_up_beats, which then passes its output to populate_beats;
b) Enclose the metric structure in an additional layer of parentheses by a list object, formatting like so: ((x x));
c) Combine the output of b) with the output of populate_beats, so you get an output which resembles: (((5 4)) (((1 (1 1 -1)) (3 (-1 -1 -1 1 -1 1)) (4 (1 -1 -1 -1 1 1)))));
d) We do this for each item we have in the parent metric structure ((5 4) (3 16) (12 128) (4 2)), four times, and collect them;
e) The penultimate step for this process is to enclose everything in a final parenthesis layer using another list object, giving an output such as this: ((((5 4) ((3 (-1 -1 -1 -1 1)) (5 (1 -1 1 1 1 1 1)))) ((3 16) ((2 (-1 -1 -1)) (3 (-1 -1 -1 1 -1)))) ((12 128) ((3 (-1 -1 1 1 -1 1)) (5 (-1 1 -1 1 -1 1 -1)) (8 (1 -1 -1 1 -1 1 1 -1 1)))) ((4 2) ((1 (1 -1 1)) (3 (-1 1 -1 1))))));
Finally, we append an ‘?’ to the output of e), thus ensuring that the rhythm tree is in the format that the VOICE object expects.
And that is how the process works. This is just a proof of concept idea – and offers little control or finesse beyond specifying what time-signatures you want. It also won’t run for a single bar – there must be at least two (I’ll provide a patch that works for a single bar). This process will however practically demonstrate how a rhythm tree can be constructed from the top-down using basic and simple processes. There are many places within the function where more sophistication can be developed and applied – if you do so, I’d be very happy to see your solutions and applications!
File download: here.