Rhythmic algorithms

Hello,

I was reading this article on rhythm, especially the section on Rhythmic algorithms.

And I was wondering if somebody has tried to implement in SC (and would like to share it) specific rhythmic algorithms, except the euclidian algorithm, like :

  • a Golomb Ruler (which distributes beats as unevenly as possible), which seems the opposite of the euclidian algorhithm,
  • or the Perfect Balanced Rhythms, quoted in the article.
  • or any other kinds of rhythmic algorithms…

Many thanks,

Christophe

4 Likes

Hi Christophe,

This is somewhat tangential to your post, but I recently came across this book: Creating Rhythms, and got it on a whim. I haven’t had a chance to get very far into it yet, but it seems very interesting. Based on their website, it seems that the authors have a bunch of b
ooks on recursive geometric patterns and the like.

…Anyway, they link to a page with MIDI files for the rhythms (along with audio recordings demonstrating) and the code for generating these rhythms in C.

So, not specifically relating to SC, but it might be interesting for you as a general investigation into rhythm?

2 Likes

Thanks for the input.
And I had the surprise to discover that algos of this book have already been ported to SC:
http://sccode.org/1-57L
I need to check.

Christophe

3 Likes

Good topic. I looked at the source code of the Quad Algorithmic Rhythm VCV rack module and found a few things.

Golomb rulers are implemented with a lookup table which can be found here: Golomb ruler - Wikipedia

Perfectly balanced rhythms are also implemented using an array of presets, but they can be created somewhat systematically. More info about PB rhythms here: https://www.nime.org/proceedings/2016/nime2016_paper0077.pdf

Well-formed rhythms I understand the least, but this paper seems to be the best resource: Computational Creation and Morphing of Multilevel Rhythms by Control of Evenness | Computer Music Journal | MIT Press

4 Likes

Great topic indeed! On the subject of Perfectly Balanced rhythms, the paper linked by @nathan lists a few basic axioms that make it quite easy to construct PB rhythms:

  1. “Rhythms with equally-sized steps (isochronous rhythms, which are also perfectly even) are PB.”
  2. “The sum of any two or more PB rhythms [having the same fundamental period] is also PB.”
  3. “For any N that is a product of no more than two distinct primes, all possible perfectly balanced rhythms can be formed by summing regular K-gons where K is prime and K divides N.”

That 3rd axiom might be confusing if you haven’t read the paper, but all it means is that the complete set of PB rhythms can be achieved by applying the first 2 axioms, as long as the period (N) is composed of less than 3 distinct primes. This is true for all N < 30, which for most music genres is all you really need.

So, I will focus on implementing the first 2 axioms in SC:

  1. Isochronous rhythms are trivial to implement in SC, but since I will be adding them together, I think the simplest method is to use Pbjorklund (part of the Bjorklund quark). In a Pbjorklund pattern, if k divides n evenly, it is isochronous (regardless of the offset). So the following examples are all isochronous:
Pbjorklund(k: 4, n: 16).asStream.nextN(16);
Pbjorklund(k: 3, n: 9, offset: 2).asStream.nextN(9);
Pbjorklund(k: 2, n: 10, offset: 1).asStream.nextN(10);
  1. Now you can simply add isochronous Pbjorklunds together to achieve any PB rhythm you want. This will work as long as the Pbjorklunds satisfy the following conditions:
  • All n are the same and n has no more than 2 distinct prime factors (true for all n < 30).
  • All k are divisors of n.
    So with a bare minimum of mental math, you can be confident that the following is a PB rhythm:
(
(Pbjorklund(3, 24, offset: 4) + 
Pbjorklund(2, 24) + 
Pbjorklund(2, 24, offset: 3)).clip(0, 1).asStream.nextN(25);
)

(I clipped the values between 0 and 1 to ensure that it remains binary.)

I have implemented this “rhythm summing” process in the following function:

(
~polybjorklund = {
    |k = 4, n = 12, offset = 0, weight = 1|
    // k: number of "hits" per phrase (use an array for polyrhythms, integer for monorhythm)
    // n: number of beats in a phrase (use an array for polymeters, integer for monometer)
    // offset: rotate the rhythms by some integer (use an array to get different offsets for each sub-rhythm)
    // weight: an array of 1s and -1s (e.g. [1, 1, -1]), a way of adding or subtracting each sub-rhythm from the final result.
    var results = ();
    var output = 0;
    k = k.asArray; n = n.asArray; offset = offset.asArray; weight = weight.asArray;
    weight.asSet.do{|w| results[w] = 0};
    maxItem([k.size, n.size, offset.size, weight.size]).collect({|i|
        var thisPolygon = Pbjorklund(k.wrapAt(i), n.wrapAt(i), inf, offset.wrapAt(i));
        results[weight.wrapAt(i)] = (results[weight.wrapAt(i)] + thisPolygon).clip(0, 1);
    });
    results.keysValuesDo({|weight, pattern|
        output = output + (pattern * weight);
    });
    output.clip(0, 1);
};
)

This will actually generate the sums and differences of Euclidean rhythms, but you can ensure they are perfectly balanced by following the rules I laid out above and using the default weight = 1. For example, the 24-note pattern above can be generated like so:

~polybjorklund.([3, 2, 2], 24, [4, 0, 3]).asStream.nextN(25);

But it is also fun to throw some random numbers at it and see what pops out!

It occurred to me that Golomb rulers, at least for the small numbers we use in music, can be calculated in linear time without a lookup.

If a Golomb ruler is a series of numbers where the differences between adjacent numbers are all different, then it’s easier to work with the differences.

I’ll write these sums in a little bit funny way (pay attention to numbers being struck out) to highlight a pattern:

1 + 2 + 3 = 3
1 + 2 + 3 = 4
1 + 2 + 3 = 5

1 + 2 + 3 + 4 = 6 (maybe hard to see the strike-through for 4 here)
1 + 2 + 3 + 4 = 7
1 + 2 + 3 + 4 = 8
1 + 2 + 3 + 4 = 9

1 + 2 + 3 + 4 + 5 = 10

So a (less than) linear time algorithm would be:

  • Find k for n such that k is the smallest integer where (k * (k + 1) / 2) > n. (That’s Euler’s way of doing the sum of 1…k in one step.) This can be done by reducing to k^2 + k - 2n >= 0 and applying the quadratic formula (rounding up to the next integer).

  • Make an array of integers 1…k.

  • Remove n - (k * (k-1) / 2) from it.

  • Scramble, integrate, and there’s your Golomb ruler.

hjh

Deriving the ‘k’ formula:

k2 + k - 2n < 0

In ak^2 + bk + c = 0, then a = 1, b = 1, c = -2n

Quadratic formula: Roots are (-b +- sqrt(b2 - 4ac)) / 2a
(-1 +- sqrt(1 - 4*1*(-2n))) / 2
(sqrt(1 + 8n) - 1) / 2  (dropping +/- because we only need the positive root)

Some code:

(
~golombDeltas = { |n|
	// use quadratic formula to find the 'k'
	// such that 'k' is the largest integer where sum(1..k) <= n
	var root = ((sqrt(1 + (8 * n)) - 1) / 2);
	var rootInt = root.trunc.asInteger;
	var residual;

	// now sum(1..root+1) >= n
	var deltas = (1 .. rootInt+1);

	if(root > rootInt) {
		// (k+1) * (k+2) = k^2 + 3k + 2
		residual = ((rootInt.squared + (3*rootInt) + 2) div: 2) - n;
		deltas.remove(residual);
	} {
		deltas = deltas.keep(rootInt)
	};

	deltas
};

~golombRuler = { |n|
	~golombDeltas.(n).scramble.integrate
};
)

This sounds rather pleasant IMO:

(
p = Ppar(
	(-7, -4 .. 8).collect { |degree, i|
		Pbind(
			\addDegree, Pseq([Pn(0, { rrand(2, 4) }), 1], inf),
			\degree, degree + Pkey(\addDegree),
			\dur, Pn(Plazy {
				Pseq(~golombDeltas.value(#[32, 16].choose).scramble, 1) * 0.25
			}, inf),
			\amp, Pif(Pkey(\addDegree) > 0, 0.2, 0.1)
		)
	}
).play;
)

EDIT: Added now to ddwChucklib-livecode: Add clGenGolomb; Bugfix clGenRepeat1 · jamshark70/ddwChucklib-livecode@c3914e1 · GitHub :grin:

\loadAllCl.eval;
s.boot;

/make(fmclavVC:fmc/melBP:fmc);

// \artic... there's a little bug, can't find it now
/fmc = "\par("\golomb("*", 0.25, 2)", "\seq("112")", "\seq("4445")", "\seq("67")", "\seq("9990")")::\artic(">_")";

/fmc+

hjh

1 Like

Some loose remarks on this:

.) miSCellaneous_lib contains PSPdiv that can be used to produce complicated polyrhythms. The Sieve and Psieve classes can be used for rhythms as a special application. See Ex.5, audio examples, in the tutorial “Sieves and Psieve patterns”.

.) In general, I find it interesting to design rhythms besides integer relations (which can be regarded as a bias from vocal resp. instrumental music). With computers we are free to generate any rhythm, not being forced to think about playability. Recently I had a discussion with a student, inspired by Bjorklund I wrote a little function to generate rhythmic cells with beats of two arbitrarily related lengths, see below.

(
SynthDef("snare", { arg out = 0, amp = 0.1, sinfreq = 180, att = 0.01, rel = 0.2, ffreq = 2000, pan = 0;
	var snd1 = WhiteNoise.ar(amp);
	var snd2 = SinOsc.ar(sinfreq, 0, amp);
	var env = EnvGen.kr(Env.perc(att, rel), doneAction: 2);
	var sum = HPF.ar(snd1, ffreq) + snd2 * env;
	OffsetOut.ar(out, Pan2.ar(sum, pan));
}).add;

SynthDef("kick", { arg out = 0, amp = 0.3, sinfreq = 60, glissf = 0.9, att = 0.01, rel = 0.45, pan = 0;
	var gliss = XLine.kr(sinfreq, sinfreq * glissf, rel);
	var snd = SinOsc.ar(gliss);
	var env = EnvGen.kr(Env.perc(att, rel), doneAction: 2);
	snd = snd * env * amp;
	OffsetOut.ar(out, Pan2.ar(snd, pan));
}).add;
)

// This Function divides a sum into two sections of beats of equal lengths
// beat numbers and ratio can be given and are understood depending on ratioType

(
~div_1 = { |sum = 1, beatNum_1 = 1, beatNum_2 = 1, ratio = 1, ratioType = \sum|
	var sum_1, sum_2, beat_1, beat_2;

	// we want:
	// sum_1 = beatNum_1 * beat_1
	// sum_2 = beatNum_2 * beat_2
	// sum_1 + sum_2 = sum
	(ratioType == \sum).if {
		// convention #1: ratio = sum_1 / sum_2  -->
		// sum_2 = sum_1 / ratio
		// sum_1 + sum_2 = sum  -->
		// sum_1 * (1 / ratio + 1) = sum -->
		sum_1 = sum / (1 / ratio + 1);
		sum_2 = sum_1 / ratio;
		beat_1 = sum_1 / beatNum_1;
		beat_2 = sum_2 / beatNum_2;
	}{
		(ratioType != \beat).if { "assume ratioType \beat".warn };
		// convention #2: ratio = beat_1 / beat_2  -->
		// beat_2 = beat_1 / ratio
		// (beat_1 * beatNum_1) + (beat_2 * beatNum_2) = sum -->
		// (beat_1 * beatNum_1) + (beat_1 / ratio * beatNum_2) = sum -->
		beat_1 = sum / (beatNum_1 + (beatNum_2 / ratio));
		beat_2 = beat_1 / ratio;
	};
	(beat_1 ! beatNum_1 ++ (beat_2 ! beatNum_2)).flat
};
)


~div_1.(12, 4, 2, 1/2, \sum)

-> [ 1, 1, 1, 1, 4, 4 ]


~div_1.(12, 4, 2, 1/2, \beat)

-> [ 1.5, 1.5, 1.5, 1.5, 3, 3 ]


~div_1.(12, 4, 2, 2.sqrt, \beat)

-> -> [ 2.2163883751088, 2.2163883751088, 2.2163883751088, 2.2163883751088, 1.5672232497824, 1.5672232497824 ]



~div_1.(12, 4, 0, inf)

-> [ 3, 3, 3, 3 ]



(
// change of single parameters
~sum = 1;
~beatNum_1 = 3;
~beatNum_2 = 2;
~ratio = 1;
~ratioType = \sum;

~rhy = Pn(Plazy {  Pseq(~div_1.(~sum, ~beatNum_1, ~beatNum_2, ~ratio, ~ratioType)) });

~kick = Pbind(
	\instrument, \kick,
	\sinfreq, [100, 50, 101],
	\dur, 1/2,
	\pan, [-0.5, 0, 0.5],
	\amp, 0.1
);
~sn = Pbind(
	\instrument, \snare,
	\dur, ~rhy / 2,
	\pan, Pwhite(-0.3, 0.3),
	\amp, 0.1
);
x = Ppar([~kick, ~sn]).trace.play;
)


s.scope

// change on the fly (snare)

~ratio = 0.7;

~beatNum_1 = 10;

~sum = 3;

~ratioType = \beat;

~ratio = 0.2;

x.stop



(
// dynamic change of ratios with Pseg (different periods in kick and snare)
// only taken over for the next array with Plazy

~sum = 1;
~beatNum_1 = 2;
~beatNum_2 = 4;
~ratio_1 = 1;
~ratio_2 = 1;
~ratioType = \beat;

~rhy_kick = Pn(Plazy {  Pseq(~div_1.(~sum, ~beatNum_1, ~beatNum_2, ~ratio_1, ~ratioType)) });
~rhy_snare = Pn(Plazy {  Pseq(~div_1.(~sum, ~beatNum_2, ~beatNum_1, ~ratio_2, ~ratioType)) });

// variable sum
// ~rhy_kick = Pn(Plazy {  Pseq(~div_1.(rrand(1, 1.5), ~beatNum_1, ~beatNum_2, ~ratio_1, ~ratioType)) });
// ~rhy_snare = Pn(Plazy {  Pseq(~div_1.(rrand(1, 1.5), ~beatNum_2, ~beatNum_1, ~ratio_2, ~ratioType)) });


~kick = Pbind(
	\instrument, \kick,
	\do, Pseg(Pseq([1, 10], inf), Pseq([5, 5], inf), Pseq([3, -3], inf)).collect(~ratio_2 = _),
	\sinfreq, [200, 50, 101],
	\dur, ~rhy_kick,
	\att, 0.01,
	\rel, 0.1,
	\pan, [-0.5, 0, 0.5],
	\amp, 0.2
);

~sn = Pbind(
	\instrument, \snare,
	\do, Pseg(Pseq([10, 1], inf), Pseq([3, 3], inf), Pseq([3, -3], inf)).collect(~ratio_1 = _),
	\dur, ~rhy_snare,
	\pan, Pwhite(-0.3, 0.3),
	\amp, 0.1
);
x = Ppar([~kick, ~sn]).trace.play;
)


~ratio_1 = 0.6;

~ratio_2 = 0.4;

~ratioType = \sum;

x.stop