After stumbling upon an interesting thread on the max forums, reading Godfried Toussaint’s interesting paper on the subject and checking out some other similar creations across the internets, including a vst, ruby and lisp implementation I decided this sounded interesting enough to devote some time to and perhaps a good excuse to learn some scripting for Max. I won’t bother going over how Euclid’s algorithm works for sequencing beats as it is detailed very well in the paper and this blog post by Ruin & Wesen. Instead I’ll focus on how I made the patch and the things I learnt along the way. For those who just want the patch to play with I’ll post the links here to save you a whole load of scrolling. I’ve also built a standalone version for those without Max who want to get in on the whole algorithmic beat generation thing.
euclid_patches.zip Max 5 Patcher file and js
euclid_patches.zip Max 5 standalone
Scripting the creation, connection and destruction of Max objects from inside Max itself is pretty much the last thing in the instruction manual. I think is because of the weight Cycling put into the idea that Max is a graphical programming language for non-programmer types. While I think it’s great that Max appeals to arty programming phobes I like programming and and I’m arrogant enough to think I’m pretty good at it. I also periodically yearn for ‘for’ loops and proper conditional logic while patching without resorting to convoluted chains of uzis, gates and less-than objects. I think some people are really good at using Max that way and can mangle a coll into doing their evil bidding through shear patch magic, but I get annoyed after three clicks when I know I could achieve the same task in a single line in a ‘proper’ programming language.
As such I’ve become increasingly enamoured with Max’s js and mxj objects that allow you to program in javascript and java respectively, as well as Thomas Grill’s really nice py external which allows for python scripting inside Max. For this project I quickly decided on js as I wanted to try out some auto patch generation. As most of the gui objects have their creation scripted I don’t have to bother monotonously naming each of them and pixel pushing them into alignment. This is js’s first really nice feature but I quickly learnt that it’s best if your js checks for the existence of an object before creating one or you have to delete all your objects before you reload your altered js program every time you make a change.
In the rhythm sequencer I wanted boxes for the number of steps, pulses, accents, offset, accent offset and midi note number and I wanted a set of these boxes for each rhythm part. This is what the first part of the code achieves.
//maximum number of parts
var maxparts = 16;
//x and y to start drawing boxes from
var px = 30;
var py = 500;
//inlets and outlets
inlets = 1;
outlets = 1;
//global variables
var p = this.patcher; //saves typing
var numparts;
//Maxobj variables for scripting
var step_boxes = new Array(maxparts);
var s_message = new Array(maxparts);
var pulse_boxes = new Array(maxparts);
var p_message = new Array(maxparts);
var accent_boxes = new Array(maxparts);
var a_message = new Array(maxparts);
var offset_boxes = new Array(maxparts);
var o_message = new Array(maxparts);
var aoffset_boxes = new Array(maxparts);
var ao_message = new Array(maxparts);
var note_boxes = new Array(maxparts);
var n_message = new Array(maxparts);
var seq;
var lcd1;
var lcd2;
var lcd_width = 300;
var lcd_height = 600;
//arrays for holding steps, pulses, accents and offsets and midi note numbers
var steps = new Array(maxparts);
var pulses = new Array(maxparts);
var accents = new Array(maxparts);
var offsets = new Array(maxparts);
var aoffsets = new Array(maxparts);
var outnote = new Array(maxparts);
//array for all the sequences
var riddims = new Array();
//useful number
var deg2rad = 2 * Math.PI / 360
//executed on startup
function loadbang() {
post("euclidean rhythm generator - robin price 2009\n");
//do comments
step_comment = p.getnamed("step_comment");
if (step_comment == null) {
step_comment = p.newdefault(px, py + 20, "comment", "@text", "Steps", "@varname", "step_comment", "@presentation", 1);
}
pulse_comment = p.getnamed("pulse_comment");
if (pulse_comment == null) {
pulse_comment = p.newdefault(px + 60, py + 20, "comment", "@text", "Pulses", "@varname", "pulse_comment", "@presentation", 1);
}
accent_comment = p.getnamed("accent_comment");
if (accent_comment == null) {
accent_comment = p.newdefault(px + 120, py + 20, "comment", "@text", "Accents", "@varname", "accent_comment", "@presentation", 1);
}
offset_comment = p.getnamed("offset_comment");
if (offset_comment == null) {
offset_comment = p.newdefault(px + 180, py + 20, "comment", "@text", "Offset", "@varname", "offset_comment", "@presentation", 1);
}
aoffset_comment = p.getnamed("aoffset_comment");
if (aoffset_comment == null) {
aoffset_comment = p.newdefault(px + 240, py + 20, "comment", "@varname", "aoffset_comment", "@presentation", 1);
aoffset_comment.message("set", "Accent\nOffset");
}
note_comment = p.getnamed("note_comment");
if (note_comment == null) {
note_comment = p.newdefault(px + 300, py + 20, "comment", "@varname", "note_comment", "@presentation", 1);
note_comment.message("set", "Output\nNote");
}
//other scripted objects
seq = p.getnamed("seq");
if (seq == null) {
seq = p.newdefault(1000, 1000, "seq~");
seq.message("sendbox", "varname", "seq");
}
lcd1 = p.getnamed("lcd1");
if (lcd1 == null) {
lcd1 = p.newdefault(900, 900, "lcd", "@border", 0, "@ignoreclick", 1, "@presentation", 1);
lcd1.message("varname", "lcd1");
lcd1.message("size", lcd_width, lcd_height);
}
lcd2 = p.getnamed("lcd2");
if (lcd2 == null) {
lcd2 = p.newdefault(900, 900, "lcd", "@border", 0, "@ignoreclick", 1, "@bgtransparent", 1, "@presentation", 1);
lcd2.message("varname", "lcd2");
lcd2.message("size", lcd_width, lcd_height);
p.bringtofront(lcd2);
}
for (i = 0; i < maxparts; i++) {
steps[i] = 16;
pulses[i] = 4;
accents[i] = 2;
offsets[i] = 0;
aoffsets[i] = 0;
outnote[i] = 60 + i;
//step boxes and messages
step_boxes[i] = p.getnamed("step_" + i);
if (step_boxes[i] == null) {
step_boxes[i] = p.newdefault(px, py - i*25, "number", "@minimum", 1, "@maximum", 128, "@varname", "step_" + i);
}
step_boxes[i].set(steps[i]);
s_message[i] = p.getnamed("step_message_" + i);
if (s_message[i] == null) {
s_message[i] = p.newdefault(px, 100 + py + i*25, "message", "@varname", "step_message_" + i);
}
s_message[i].message("set", "step", i, "\$1");
p.connect(step_boxes[i], 0, s_message[i], 0);
p.connect(s_message[i], 0, this.box, 0);
//pulse boxes and messages
pulse_boxes[i] = p.getnamed("pulse_" + i);
if (pulse_boxes[i] == null) {
pulse_boxes[i] = p.newdefault(px + 60, py - i*25, "number", "@minimum", 0, "@varname", "pulse_" + i);
}
pulse_boxes[i].set(pulses[i]);
pulse_boxes[i].message("maximum", steps[i]);
p_message[i] = p.getnamed("pulse_message_" + i);
if (p_message[i] == null) {
p_message[i] = p.newdefault(px + 60, 100 + py + i*25, "message", "@varname", "pulse_message_" + i);
}
p_message[i].message("set", "pulse", i, "\$1");
p.connect(pulse_boxes[i], 0, p_message[i], 0);
p.connect(p_message[i], 0, this.box, 0);
//accent boxes and messages
accent_boxes[i] = p.getnamed("accent_" + i);
if (accent_boxes[i] == null) {
accent_boxes[i] = p.newdefault(px + 120, py - i*25, "number", "@minimum", 0, "@varname", "accent_" + i);
}
accent_boxes[i].set(accents[i]);
accent_boxes[i].message("maximum", pulses[i]);
a_message[i] = p.getnamed("accent_message_" + i);
if (a_message[i] == null) {
a_message[i] = p.newdefault(px + 120, 100 + py + i*25, "message", "@varname", "accent_message_" + i);
}
a_message[i].message("set", "accent", i, "\$1");
p.connect(accent_boxes[i], 0, a_message[i], 0);
p.connect(a_message[i], 0, this.box, 0);
//offset boxes and messages
offset_boxes[i] = p.getnamed("offset_" + i);
if (offset_boxes[i] == null) {
offset_boxes[i] = p.newdefault(px + 180, py - i*25, "number", "@minimum", 0, "@varname", "offset_" + i);
}
offset_boxes[i].set(offsets[i]);
offset_boxes[i].message("maximum", steps[i] - 1);
o_message[i] = p.getnamed("offset_message_" + i);
if (o_message[i] == null) {
o_message[i] = p.newdefault(px + 180, 100 + py + i*25, "message", "@varname", "offset_message_" + i);
}
o_message[i].message("set", "offset", i, "\$1");
p.connect(offset_boxes[i], 0, o_message[i], 0);
p.connect(o_message[i], 0, this.box, 0);
//accent offset boxes and messages
aoffset_boxes[i] = p.getnamed("accent_offset_" + i);
if (aoffset_boxes[i] == null) {
aoffset_boxes[i] = p.newdefault(px + 240, py - i*25, "number", "@minimum", 0, "@varname", "accent_offset_" + i);
}
aoffset_boxes[i].set(aoffsets[i]);
aoffset_boxes[i].message("maximum", pulses[i] - 1);
ao_message[i] = p.getnamed("accent_offset_message_" + i);
if (ao_message[i] == null) {
ao_message[i] = p.newdefault(px + 240, 100 + py + i*25, "message", "@varname", "accent_offset_message_" + i);
}
ao_message[i].message("set", "accent_offset", i, "\$1");
p.connect(aoffset_boxes[i], 0, ao_message[i], 0);
p.connect(ao_message[i], 0, this.box, 0);
//note boxes and messages
note_boxes[i] = p.getnamed("note_" + i);
if (note_boxes[i] == null) {
note_boxes[i] = p.newdefault(px + 300, py - i*25, "number", "@minimum", 0, "@maximum", 127, "@format", 4, "@varname", "note_" + i);
}
note_boxes[i].set(outnote[i]);
n_message[i] = p.getnamed("note_message_" + i);
if (n_message[i] == null) {
n_message[i] = p.newdefault(px + 300, 100 + py + i*25, "message", "@varname", "note_message_" + i);
}
n_message[i].message("set", "note", i, "\$1");
p.connect(note_boxes[i], 0, n_message[i], 0);
p.connect(n_message[i], 0, this.box, 0);
}
notifyclients();
calculate(-1);
parts(1);
bang();
}
This seems fairly dense at first but mostly it’s just doing the same thing over and over again to create all the number boxes along with a message box that allow us to update our variable inside js. I chose this method of creating each number box connected to a personalised message box because js in Max has no native method of reading the contents of a screen object. Hence the workaround, each group of objects, step, pulse boxes or whatever are created connected to a message box which is connected to the js object. The message boxes are formatted so that when the user changes the number in any of the gui boxes a method is called that tells js which box changed and what number it now carries. This allows for a user interface where each of the boxes is updated as the other entries are changed. This lets you add nice features like number boxes that update the other boxes around them so they cannot display nonsensical values, i.e. a rhythm part with more pulses than steps to hold them.
Looking through the code you can see first off I define a maxparts
global variable which sets the maximum number of rhythm parts and thus groups of number boxes the script creates. I then create variables to control where the patch starts creating the objects in the patcher window and the number of inlets and outlets on the js box itself. The variable p
is just to save typing this.patcher
every time I want to do something with the patcher and numparts
describes the number of active rhythm parts at the present time. Next I create references to all of the objects I’ll create and access in the script, for the most part these references are stored in arrays as there are so many of them and doing so allows for thing like iterating across them. Then I create arrays to hold the settings for each rhythm part and an array that will hold all the sequences that have been generated.
The loadbang()
is executed when the patch is started just like a loadbang in the patch, this creates the comments, number and message boxes and a few other goodies. Note the form for each object.
step_comment = p.getnamed("step_comment");
if (step_comment == null) {
step_comment = p.newdefault(px, py + 20, "comment", "@text", "Steps", "@varname", "step_comment", "@presentation", 1);
}
First I try and get a reference to an object in the patch and then I check if that reference is null, if it is it’s obviously been clobbered or something so I recreate it. This way whenever you reload a js during its development it doesn’t create multiple copies of the same set of objects.
Next I define a set of functions that will be called by the user changing the values of the number boxes. These allow the script to take care of updating the other boxes and the seq~ object whenever anything changes. Here is the function for the step boxes.
function step(n, val) {
if (n >= 0 && n < maxparts && val >= 0 && val <= 128) {
//number of steps must always be greater than or equal to the number of pulses
if (val >= pulses[n] && val > offsets[n] && val >= accents[n]) {
steps[n] = val;
pulse_boxes[n].message("maximum", val);
offset_boxes[n].message("maximum", val - 1);
} else {
if (val < pulses[n]) {
pulses[n] = val;
pulse_boxes[n].message("set", val);
pulse_boxes[n].message("maximum", val);
if (val <= aoffsets[n]) {
aoffsets[n] = val;
aoffset_boxes[n].message("set", val - 1);
aoffset_boxes[n].message("maximum", val - 1);
}
}
if (val <= offsets[n]) {
offsets[n] = val - 1;
offset_boxes[n].message("set", val - 1);
offset_boxes[n].message("maximum", val - 1);
}
if (val < accents[n]) {
accents[n] = val;
accent_boxes[n].message("set", val);
accent_boxes[n].message("maximum", val);
}
steps[n] = val;
}
notifyclients();
calculate(n);
programme_seq(n);
bang();
} else {
post("steps message needs two integers between 0 0 and " + maxparts + " 128\n");
}
}
This function starts by checking the values it’s been passed (which number box got changed, n, and it’s new value, val) make sense and then goes on to update the internal representation steps[n]
and update the other number boxes in the rhythm part if the new value requires it. Finaly it causes the new rhythm to be calculated, programmes the seq~ and redraws the graphics which I’ll discuss after touching on the next noteworthy function.
function parts(i) {
if (arguments.length) { //bail if no arguments
a = arguments[0];
if (a < 0) a = 0;
if (a > maxparts) a = maxparts;
numparts = a;
for (z = 0; z < maxparts; z++) {
if (z < numparts) {
step_boxes[z].message("presentation", 1);
pulse_boxes[z].message("presentation", 1);
accent_boxes[z].message("presentation", 1);
offset_boxes[z].message("presentation", 1);
aoffset_boxes[z].message("presentation", 1);
note_boxes[z].message("presentation", 1);
programme_seq(z);
} else {
step_boxes[z].message("presentation", 0);
pulse_boxes[z].message("presentation", 0);
accent_boxes[z].message("presentation", 0);
offset_boxes[z].message("presentation", 0);
aoffset_boxes[z].message("presentation", 0);
note_boxes[z].message("presentation", 0);
clear_seq(z);
}
}
notifyclients();
bang();
} else {
post("parts message needs integer argument between 0 and " + maxparts + "\n");
}
}
This nugget of javascript allows us to change the number of currently active rhythm parts and update the visible elements of the patch so we’re not distracted by anything that isn’t doing anything.
As for calculating the rhythm this is taken care of by the calculate()
function which gets the euclidean sequence for a given number of pulses and steps, the sequence for a number of accents and pulses, applies any rotation to the either sequences (using code I stole from here) and applies the accents to the rhythm. Actually most of the work is done by sub functions eugen()
and apply_accents()
.
function eugen(s, p) {
var r = new Array();
if (p >= s || s == 1 || p == 0) { //test for input for sanity
if (p >= s) {
for (i = 0; i < s; i++) { //give trivial rhythm of a pulse on every step
r.push(1);
}
} else if (s == 1) {
if (p == 1) {
r.push(1);
} else {
r.push(0);
}
} else {
for (i = 0; i < s; i++) {
r.push(0);
}
}
} else { //sane input
pauses = s - p;
if (pauses >= p) { //first case more pauses than p
per_pulse = Math.floor(pauses / p);
remainder = pauses % p;
for (i = 0; i < p; i++) {
r.push(1);
for (j = 0; j < per_pulse; j++) {
r.push(0);
}
if (i < remainder) {
r.push(0);
}
}
} else { //second case more p than pauses
per_pause = Math.floor( (p - pauses) / pauses);
remainder = (p - pauses) % pauses;
for (i = 0; i < pauses; i++) {
r.push(1);
r.push(0);
for (j = 0; j < per_pause; j++) {
r.push(1);
}
if (i < remainder) {
r.push(1);
}
}
}
}
return r;
}
function apply_accents(r, a) {
var offset = 0;
var out = new Array(r.length);
for (b = 0; b < r.length; b++) {
if (r[b] == 1) {
if (a[offset] == 1) {
out[b] = 1;
} else {
out[b] = 0.5;
}
offset += 1;
} else {
out[b] = 0;
}
}
return out;
}
Eugen()
is basically the most important part of the whole thing as it implements the algorithm for the distribution of the beats. When the rhythm is programmed the seq~ object in the Max patch is updated by the programme_seq()
function. This sends a sequence of messages to the named seq~ object in the patch (no need for patch cables, we can send them direct) that programmes the id number and midi notes into the seq~.
function programme_seq(arg) {
clear_seq(arg);
r = riddims[arg];
for (l = 0; l < steps[arg]; l++) {
if (r[l] == 1) {
seq.message("add", 0, l / steps[arg], arg, outnote[arg], 127);
seq.message("add", 0, l / steps[arg] + (1 / steps[arg] - 1 / 256), arg, outnote[arg], 0);
} else if (r[l] == 0.5) {
seq.message("add", 0, l / steps[arg], arg, outnote[arg], 100);
seq.message("add", 0, l / steps[arg] + (1 / steps[arg] - 1 / 256), arg, outnote[arg], 0);
}
}
}
programme_seq.local = 1;
function clear_seq(arg) {
if (arg == -1) {
for (h = 0; h < maxparts; h++) {
seq.message("delete", 0, 0.0, 1.0, h);
}
} else {
seq.message("delete", 0, 0.0, 1.0, arg); //delete all entries for sequence
}
}
clear_seq.local = 1;
The last part of the js takes care of the ultra minimalistic sequence visualization. At first I tried using Max’s jsui object but I found it is an exceptional CPU hog so I created two lcd objects one for the sequence and one for the red line showing the playback location. The red line lcd has a transparent background and sits on top of the other sequence visualizer. This way when ever the playback line is redrawn we don’t have to redraw the rest of the sequence visualization. This is a really good example of using js’s procedural (as opposed to graphical) logic to send a set of messages to a Max object, this would be tricky (for me at least) to achieve with native Max graphical methods.
function draw() {
lcd1.message("clear");
var rowheight = (0.5 * lcd_height) / numparts;
for (y = numparts - 1; y >= 0; y--) {
var boxwidth = lcd_width / steps[y];
var arcwidth = 360 / steps[y];
var arcradius = (lcd_width/2) * (y + 1) / numparts;
var r = riddims[y];
for (x = 0; x < steps[y]; x++) {
if (r[x] == 1) {
lcd1.message("frgb", 0, 0, 0);
lcd1.message("pensize", 1, 1);
lcd1.message("paintarc", lcd_width/2 - arcradius, lcd_width/2 - arcradius, lcd_width/2 + arcradius, lcd_width/2 + arcradius, x * arcwidth, arcwidth + 2);
lcd1.message("paintrect", boxwidth*x, lcd_height - rowheight*(y+1), boxwidth*(x+1), lcd_height - rowheight*y);
} else if (r[x] == 0.5) {
lcd1.message("frgb", 100, 100, 100);
lcd1.message("pensize", 1, 1);
lcd1.message("paintarc", lcd_width/2 - arcradius, lcd_width/2 - arcradius, lcd_width/2 + arcradius, lcd_width/2 + arcradius, x * arcwidth, arcwidth + 2);
lcd1.message("paintrect", boxwidth*x, lcd_height - rowheight*(y+1), boxwidth*(x+1), lcd_height - rowheight*y);
} else {
lcd1.message("frgb", 255, 255, 255);
lcd1.message("pensize", 1, 1);
lcd1.message("paintarc", lcd_width/2 - arcradius, lcd_width/2 - arcradius, lcd_width/2 + arcradius, lcd_width/2 + arcradius, x * arcwidth, arcwidth + 2);
}
}
}
for (y = 0; y < numparts; y++) {
var boxwidth = lcd_width / steps[y];
for (x = 0; x < steps[y]; x++) {
lcd1.message("frgb", 255, 255, 255);
lcd1.message("linesegment", boxwidth*x, lcd_height - rowheight * y - 1, boxwidth * x, lcd_height - rowheight * (y+1));
}
}
}
function settime(time) {
lcd2.message("clear");
lcd2.message("frgb", 255, 0, 0);
lcd2.message("linesegment", ((time + 0.001) * lcd_width) % lcd_width, lcd_height/2, ((time + 0.001) * lcd_width) % lcd_width, lcd_height);
lcd2.message("linesegment", lcd_width/2, lcd_height/4, lcd_width/2 + (lcd_width/2) * Math.sin(-time*Math.PI*2 - Math.PI), lcd_width/2 + (lcd_width/2) * Math.cos(-time*Math.PI*2 - Math.PI));
}
That pretty much covers most of what my patch is doing, in the future I hope to update it with some interesting swing and shuffle options by sending the phasor~ signal through a table lookup waveshaper and some industrial sounding msp beatbox voices.
This is very cool. Nice one!
Awesome! I’m the new Steve Reich!
I agree that registering domain names is better than actually doing real work, and also that this blog entry is sickkkkkkkkkkkkkkkkkkkkkkk.