La hora del live coder is performance that explores possible strategies for visualizing temporal canons using FluentCan and hydra. Additionally Eikosanies
were used as the music's tuning system. The following is a tutorial for anyone interested in these techniques.
Personally, the most interesting thing about performance is in the communication between supercollider
and hydra
, since it is possible to control and even compile the hydra
code from supercollider
.
The second most interesting thing is that using FluentCan
in conjunction with hydra
(in this case) one can create graphical temporal canons, which is something I have not yet seen anywhere else.
The code repository and this tutorial can be found here: https://github.com/diegovdc/la-hora-del-live-coder
Please note that although this technique is used for compiling visual canons in hydra
, it can be adapted to any situation where the control of what and how gets compiled by hydra
should be defined by an external program.
En Hydra:
The most basic (althought) trivial implementation looks like this (see below for a non trivial implementation)
// `main` is a function that gets compiled every time there is a `compile` osc event
main = (oscPayload) => shape(oscPayload[0]).out();
// each time a /compile message is sent, then the main function is compiled...
msg.on("/compile", (oscPayload) => {
main(oscPayload);
});
This implementation is trivial because we don't need to recompile hydra
to pass a new value to the shape
function. We could simply do this and save a lot of CPU cycles:
shape(() => numberOfSides).out();
msg.on("/changeSides", (oscPayload) => {
numberOfSides = oscPayload[0];
});
In the performance, however, FluentCan
would produce canons with a different number of voices each time, so I wanted to automatically change in hydra
the number of shapes
that were produce to match the number of voices of the canon.
So I did the following.
First a layering function was need. If we want three layers (or voices) at a given time, then we want the layer function to be able to do something like this (but dynamically):
shape().diff(shape()).diff(shape());
Using the JS library Ramda layer looks simply like this.
layer = R.invoker(1, "diff");
But layer
could also be written like this in vanilla js:
layer = (newLayer, accumulatedLayers) => accumulatedLayers.diff(newLayer);
An example use of layer is this:
layer1 = layer(shape(2), shape(1)); //shape(1).diff(shape(2))
layer2 = layer(shape(3), layer1); // shape(1).diff(shape(2)).diff(shape(3))
But we want to create more interesting layers so this next function (makeLayer
) is in charge of creating each layer (i.e. each shape()
). It receives the data from a single voice (actually just the key) to create each of the shapes individually.
The voices
object holds all the the different voices data (see below); and this stateful object will be used to access the current voice values on every iteration of the hydra
graphics loop.
// this version returns a colored shape, but it can be as complex as desired
makeLayer = (voice) =>
solid(
() => voices[voice].r || 0,
() => voices[voice].g || 0,
() => voices[voice].b || 0
).mask(shape(() => voices[voice].shape || 3));
Next we define the main
function which gets recompiled via osc.
Here R.reduce
is used to iterate by the number of voices and accumulate our layers into a single signal. (Using js native .reduce
or even for loop
are good also alternatives).
So if there are 2 voices one would get something like:
shape(1).diff(shape(2));
And with 3 voices something like:
shape(1).diff(shape(2)).diff(shape(3));
However for our function to work we need a base layer. Something neutral, like solid(0,0,0)
, works well. So we are actually going to get something like this:
solid(0, 0, 0).diff(shape(1)).diff(shape(2)).diff(shape(3));
And so on... All generated automatically.
One would just need to substitute shape
by whatever the output of makeLayer is.
main = (voices) =>
R.reduce(
(acc, voice) => layer(makeLayer(voice), acc), // voice is just a key in the voices object, i.e. `"v1"`, or `"v2"`
solid(0, 0, 0), // this is the initial or base layer
Object.keys(voices)
).out(); // we call .out on the accumulated layers
The content of the voices
object is defined separately, so that when a voice's parameter is updated (on every sound event), the object changes but the hydra
code need not be recompiled.
This object looks like this:
{
v1: {
voiceIndex: 1,
r: 0.5,
g: 1,
b: 0,
// more params
},
v2: {
voiceIndex: 2,
r: 0,
g: 0.1,
b: 0.7
// more params
},
// more voices
}
To update the voices object we use a message called /canosc
.
msg.on("/canosc", (msg_) => {
// don't mind this next line too much, it just parses the osc message into an object like: {voiceIndex: 1, r: 0.5, g: 1, b: 0, ...otherParams}
var msg__ = R.fromPairs(R.splitEvery(2, msg_));
// the parsed voice is assigned to the voices object which the looks like this:
// {v1: {r:0.5, ...otherParams}, v2: {...params}, ...}
voices["v" + msg__.voiceIndex] = msg__;
});
Resources in the actual code: https://github.com/diegovdc/la-hora-del-live-coder/blob/master/index.js#L14-L23 y https://github.com/diegovdc/la-hora-del-live-coder/blob/master/index.js#L41-L51
For compiling hydra
the supercollider
simply looks like this:
~osc = (net: NetAddr("localhost", 57101));
~compileHydra = {|c| Task({1.wait; ~osc.net.sendMsg(\compile, c.canon.canon.size);}).play}; // the Task is used for delaying compilation for some reason I can not remember
The SuperCollider
or rather, the FluentCan
code for sending each voices data looks like this:
First we define some helper functions:
// Merge two Event objects.
~merge = {|a, b| merge(a, b, { |a, b| b ? a })};
// Calculate the tranposition for the visual parameters corresponding to each voice
~transpf = {|fns, vals, event| fns.wrapAt(event.voiceIndex).(vals.wrapAt(event.eventIndex))};
Creating a FluentCan model with the osc connection
~osc = (net: NetAddr("localhost", 57101));
m = FluentCan(osc: ~osc);
~compileHydra = {|c| Task({1.wait; ~osc.net.sendMsg(\compile, c.canon.canon.size);}).play};
Creating a canon:
(
c = m.def(\1)
.notes([60, 62,63]]) // do, re, mi
.period(3) // the notes sequences lasts 3 seconds before repeating
// these next two lines define the voices ratios and transpositions
.tempos([1, 2]) // the second voice is twice as fast as the other
.transps([0, 7]) // the second voice is transposed a fifth above
.play;
~compileHydra.(c); // ~compileHydra receives the canon so that it can calculate the number of voices of the canon before sending the osc message
)
Calculating the visual data and sending it on every sound event.
For the osc message we send an Event
object which is the most similar data structure to a JavaScript Object
.
To do that merge the original data of the Event
with the visual/hydra related data produced below
(
c.canon.player.onEvent({|ev|
// do something else, like producing sound...
// To send osc dat we call.
CanPlayer.makeOSC(
~merge.(ev,
(
r: ~transpf.([_*1], [2], ev),
g: ~transpf.([_*1], [0], ev),
b: ~transpf.([_*0.2], [1], ev),
scale: ~transpf.([_*1], [1], ev),
noise: ~transpf.([_*0], [0], ev),
shape: ~transpf.([_*1], [2], ev),
repeat: ~transpf.([_+0], [1], ev),
x: ~transpf.([_*0], [0], ev),
xd: ~transpf.([_*0], [0], ev),
y: ~transpf.([_*0], [0], ev),
yd: ~transpf.([_*0], [0], ev),
)
)).(); // Note here the function call. `CanPlayer.makeOSC` returns a function which upon being called sends the osc message.
})
)
And thats it!