At UoW I also rewrote an old installation called The Intelligent Street. Mark d'Inverno and I had worked together on this one earlier and we now wanted it to run on a modern computer (OSX). We also wanted to redesign it a bit and make it into a standalone application.
The original Intelligent Street was a transnational sound installation were users could compose music using mobile phones and SMS. That, in turn, was an extended version of a yet older installation called 'The Street' by John Eacott. This totally reworked 'intelligent' version was premiered in November 2003 and realised as a joined effort between the Ambigence group (j.eacott, m.d'inverno, h.lörstad, f.rougier, f.olofsson) and the Sonic studio at the Interactive Institute in Piteå (way up in northern Sweden).
For the new OSX version, we dropped SMS as the only user interface and also removed the direct video+sound links between UK-SE that were part of the old setup. Except for that, the plan was to move it straight over to SuperCollider server (SC3) and rather spend time on polishing the overall sound and music.
I roughly estimated it would take just a few days to do the actual port. Most of the old code was written in SuperCollider version 2 (Mac OS9) and the generative music parts were done using SC2's patterns and Crucial libraries. So that code, I thought, would be pretty much forward compatible with our targeted SuperCollider 3. But sigh - it turned out that I had to rewrite it completely from scratch. The 'smart' tweaks and optimisations I had done in the old SC2 version in combination with the complexity of the engine made it necessary to redesign the thing bottom up. Even the generative patterns parts. Last I also dropped Crucial library for the synthesised instruments and did it all in bare-bone SC3.
But I guess it was worth the extra weeks of work. In the end, the system as a whole became more robust and better sounding. And standalone not to mention so hopefully it will survive a few years longer.
But I can also think of more creative work than rewriting old code. I've been doing that quite a lot recently. It feels like these old installations I've worked on earlier comes back to haunt me at regular intervals. And there is more and more of them for each year :-)
Proof: Intelligent Street running happily under OSX...
Idea
Yet another system Mark d'Inverno and I worked on but never finished had the working title 'shadowplay'. We had this idea about an audiovisual installation where people's limbs (or outlines of bodies) would represent grid worlds. Agents would live in these worlds and evolve differently depending on things like limb size, limb movement over time, limb shape and limb position. The agents would make different music/sounds depending on the world they live in. A limb world could be thought of as a musical part in a score. The worlds would sound simultaneous but panned to different speakers to help interaction.
The visitors would see the outline of their bodies projected on a big screen together with the agents represented visually in this picture as tiny dots. Hopefully, people could then hear the agents that got caught or breed inside their own limbs. We hoped to active a very direct feeling of caressing and breeding your own sounding agents.
There were plans for multi-user interaction: if different limbs/outlines touched (e.g. users shaking hands), agents could migrate from one world to another. There they would inject new genes in the population, inflicting the sound, maybe die or take over totally. Though to keep agents within the worlds they were made to bounce off outlines. But one could shake off agents by moving quickly or just leave the area. These 'lost' agents would then starve to death if not adopted by other users.
Tech
The whole thing was written in Processing and SuperCollider. Processing did the video and graphics: getting the DV input stream, doing blob tracking (using the 3rd party library blob detection) and drawing the agents and lines for the limbs. SuperCollider handled rest: the sound synthesis, the genetics, agent state and behaviours, keeping track of the worlds etc. We used a slightly modified version of our A4 agent framework I wrote about in the following post: /f0blog/work-with-mark-bottom-up-approach/.
The two programs communicated via a network (OSC) and would ideally run on different machines.
I had major problems with programming. The math was hairy and all the features were very taxing on the CPU. We never got further than a rough implementation.
I spent a lot of time benchmarking the code for the agents in the different versions of the multi-agent frameworks I posted about here earlier. The best performance boost was (of course) when I ported some vital parts of the SuperCollider code to C. So, for example, one CPU-intensive task that the agents had to do a lot was to find out about their surroundings. And another 'heavy' task was to calculate the distance to other objects. Each operation wasn't very demanding on its own, but when hundreds of agents would do this at the same time, we really needed the speed of the C primitives.
Below is the C code I came up with. It replaces some of the computational heavy parts in the Surroundings
and ALoaction
SuperCollider classes. To try them out you'd need to download the SuperCollider source code from github.com/supercollider/supercollider, add my code to the file PyrArrayPrimitives.cpp and then recompile the whole application. To use these primitives you also need to edit the file extArrayedCollection.sc from the A4.zip package posted here earlier.
One issue we then had was to distribute this 'hack'. SuperCollider doesn't have an API for adding extensions like this to the language (but there's a nice plugin architecture for the server). So I had to build dedicated SuperCollider applications including this speed hack.
int prArrayAsALocationIndex(struct VMGlobals *g, int numArgsPushed)
{
PyrSlot *a, *b;
PyrObject *obj;
int size, i;
double asIndex, w, worldSize;
a = g->sp - 1;
b = g->sp;
worldSize = b->ui;
obj = a->uo;
size = obj->size;
asIndex= 0;
for (i=0; i<size; ++i) {
getIndexedDouble(obj, i, &w);
asIndex= asIndex+(pow(worldSize, i)*w);
}
SetFloat(a, asIndex);
return errNone;
}
int prArrayAsALocationRoundedIndex(struct VMGlobals *g, int numArgsPushed)
{
PyrSlot *a, *b;
PyrObject *obj;
int size, i;
double asIndex, w, worldSize;
a = g->sp - 1;
b = g->sp;
worldSize = b->ui;
obj = a->uo;
size = obj->size;
asIndex= 0;
for (i=0; i<size; ++i) {
getIndexedDouble(obj, i, &w);
w= sc_round(w, 1.0);
w= sc_clip(w, 0, worldSize-1);
asIndex= asIndex+(pow(worldSize, i)*w);
}
SetFloat(a, asIndex);
return errNone;
}
int prArrayDistance(struct VMGlobals *g, int numArgsPushed)
{
PyrSlot *a, *b;
PyrObject *obj1, *obj2;
int size, i;
double w1, w2, distance;
a = g->sp - 1;
b = g->sp;
if (b->utag != tagObj || a->uo->classptr != b->uo->classptr) return errWrongType;
obj1 = a->uo;
obj2 = b->uo;
size = obj1->size;
distance= 0;
for (i=0; i<size; ++i) {
getIndexedDouble(obj1, i, &w1);
getIndexedDouble(obj2, sc_mod(i, size), &w2);
distance= distance+pow(w2-w1, 2);
}
SetFloat(a, fabs(sqrt(distance)));
return errNone;
}
int prArraySurroundings(struct VMGlobals *g, int numArgsPushed)
{
PyrSlot *a, *b, *c, *d, *areaArraySlots, *indexArraySlots, *outArraySlots;
PyrObject *obj, *areaArray, *indexArray, *outArray;
int areaSize, outSize, i, j, worldDim, area;
double w;
a = g->sp - 3; //list
b = g->sp - 2; //worldDim
c = g->sp - 1; //area - as float possible later?
d = g->sp; //boolean - exclude/include fix later
if (b->utag != tagInt) return errWrongType;
if (c->utag != tagInt) return errWrongType;
if (d->utag != tagTrue && d->utag != tagFalse) return errWrongType;
obj = a->uo;
worldDim = b->ui;
area = c->ui;
areaSize = area*2+1;
indexArray = newPyrArray(g->gc, worldDim, 0, true);
indexArraySlots = indexArray->slots;
indexArray->size = worldDim;
if (IsTrue(d)) { //--build index array excluding
areaArray = newPyrArray(g->gc, areaSize-1, 0, true);
areaArraySlots = areaArray->slots;
areaArray->size = areaSize-1;
int j = 0;
for (i=0; i<areaSize-1; ++i) {
int temp = 0-area+i;
if (temp==0) {j++;}
areaArraySlots[i].ucopy = temp+j;
}
outSize = pow(areaSize, worldDim)-1;
} else { //--build index array including
areaArray = newPyrArray(g->gc, areaSize, 0, true);
areaArraySlots = areaArray->slots;
areaArray->size = areaSize;
for (i=0; i<areaSize; ++i) {
areaArraySlots[i].ucopy = 0-area+i;
}
outSize = pow(areaSize, worldDim);
}
for (i=0; i<worldDim; ++i) {
SetObject(indexArraySlots+i, areaArray);
}
//indexArray is here... [[-1, 0, 1]] or [[-1, 0, 1], [-1, 0, 1]] etc. for area=1
//or [[-2, -1, 0, 1, 2]] or [[-2, -1, 0, 1, 2], [-2, -1, 0, 1, 2]] etc. for area=2
//--all tuples
outArray = newPyrArray(g->gc, outSize*sizeof(PyrObject), 0, true);
outArraySlots = outArray->slots;
outArray->size = outSize;
for (i=0; i<outSize; ++i) {
int k = i;
PyrObject *tempArray = newPyrArray(g->gc, worldDim, 0, true);
PyrSlot *tempArraySlots = tempArray->slots;
tempArray->size = worldDim;
for (j=worldDim-1; j>=0; --j) {
tempArraySlots[j].ucopy = areaArraySlots[k%areaSize].ucopy;
getIndexedDouble(obj, j, &w);
tempArraySlots[j].ucopy += w;
k /= areaSize;
}
SetObject(outArraySlots+i, tempArray);
}
a->uo = outArray;
return errNone;
}
Edit extArrayedCollection.sc (from A4.zip posted in an earlier blog entry) to look like this...
+ ArrayedCollection {
asALocationIndex {|size|
_ArrayAsALocationIndex
^this.primitiveFailed;
}
asALocationRoundedIndex {|size|
_ArrayAsALocationRoundedIndex
^this.primitiveFailed;
}
distance {|list|
_ArrayDistance
^this.primitiveFailed;
}
surroundings {|dimensions= 2, area= 1, exclude= true|
_ArraySurroundings
^this.primitiveFailed;
}
}
SuperCollider code for benchmarking...
//speedtest
({
var size= 100, cSize= 2, rule= 30;
var world, agents, y= 0, dict;
dict= (); /*lookup dictionary for rules*/
8.do{|i| dict.put(i.asBinaryDigits(3).join.asSymbol, rule.asBinaryDigits[7-i])};
ACell.rules= dict;
world= APattern(size); /*create 1d world*/
size.do{|i| ACell(ALocation(world, [i]))}; /*fill up 1d grid with agents*/
world.get(ALocation(world, [(size/2).round])).value= 1; /*middle agent value=1 as init*/
agents= world.items;
while({y<size}, {
/*here update. first all agents.sense then all agents.act*/
agents.do{|a| a.sense};
/*agents.do{|a| a.location.list_([(size/2).round])};*/
/*agents.do{|a| a.location= ALocation(a.location.world, [(size/2).round])};*/
agents.do{|a| a.act};
y= y+1;
});
}.bench)
After some time Mark d'Inverno and I shifted focus and decided to simplify our ideas. We agreed to work more from a bottom-up approach - letting the agents live within a grid world and visualise their behaviours. Other people have been doing quite some work in this area before, but not particularly many of them have been incorporating sound and music. So we had literature and examples to study and people to ask. It was of great help and I learned a lot about designing multi-agent systems from analysing, for example, Jon McCormack's nice Eden.
So starting out writing our own system, I did a set of classes for handling agents running around in a grid world of 1-3 dimensions. All agents were very simple-minded. They were visually represented with just dots and oh, they could bleep too.
Setting up simple scenarios for these classes helped to pinpoint different system models. It also showed my biggest problems coding this usually boiled down to in which order to do things. The model I tried in turn to 'model' was suggested by Rob Saunders in an article called 'Smash, bam and cell', in were all agents first sense their surroundings and then act. But I constantly had to restructure the code and design. This was harder than I had thought and I think I never came up with an all-around solution.
One example scenario we came up with was the runaway test. It is very simple but can help trying out different designs. It works something like this... Imagine a grid world of say 2 dimensions (we also coded this in 1 and 3D). Agents move about at random speed and direction. If an agent encounters another one blocking its next step, it turns around 180 degrees and flees i.e. moving away in the opposite direction. So far the sense/act cycle is simple: for every update (world tick) it first sense, then acts. But what happens if there's another agent blocking the escape route? So the agents really need to first sense, then if something is ahead, act and turn around 180, sense again and decide if it is possible to flee. Here it'll sense within the act method and that clutters the design. The better solution would probably be to let the agent just turn 180 and wait to flee until the next tick. But perhaps it could also sense behind itself in the first sense round and pause if both escape routes are blocked. There are many possible solutions and creating these small test scenarios helped me to generalise my classes. We also tried the classes by coding the test scenarios as discrete and continuous i.e. if the world was a rigid grid in which the agents only were allowed to move stepwise, or if the world allowed them to move about more smoothly in non-integer directions and speeds.
The SuperCollider code for version 4, including test scenarios and small examples is attached at the bottom of this post and below is some text trying to describe the classes in more detail.
Also, see these QuickTime movies of some test scenarios...
- one-dimensional continuous world
- two-dimensional continuous world
- one-dimensional discrete world
- one-dimensional discrete world
- one-dimensional discrete world
- one-dimensional discrete world
- two-dimensional discrete world
A4 description
There are 3 basic classes. ALocation
, AWorld
and AnItem
. I first describe them and their immediate subclasses. Then AnAgent
, AProxyAgent
and some subclasses of AWorld
. Then I explain a few of the classes used in the test/example programs. Last I write a little about the main loop.
May look at the files A4.sc and A4subs.sc for completion.
ALocation
BASICS:
A place within a world.
.new
takes 2 arguments: world and list
Instance variable world
is any (sub)class of AWorld
. Can be get
(i.e. accessed from outside).
Instance variable list
is a list of coordinates. Can be get
and set
. Example: [10]
for x=10 in an 1 dimensional world. [10, 20, 30]
for x, y, z in a 3D world. The length of the list must correspond to the number of dimensions in the world.
ADDITIONAL:
Locations can be compared with the ==
method. It takes another location as an argument. A location is equal to another if they exist in the same world and have the same list of coordinates.
The !=
method is the negation of ==
.
Distance between 2 locations can be found with the distance
method. It'll return the shortest distance between locations in any dimension.
With the at
method, one can query single dimensions. E.g. in a 2D world, location.at(0)
will return which column and location.at(1)
will return row. The argument is really just index in list above.
The surroundingLocations
method. With the arguments exclude(boolean) and area(int) this returns a list of new location objects. This is used for collecting locations to be searched for neighbours. If exclude argument flag is false, this (i.e. the current) location will be counted and included in the list. The locations returned are all positioned next to this location in a cube-like way, covering an area of size: area steps away. To put it in another way: with an area of 1, only directly adjacent locations are returned. An area of 2 gives adjacent and their adjacent locations (as a set of ALocation
objects) and so on.
So in a 1D world a location at (0) sent the message .surroundingLocations(false, 1)
will give us [loc[-1], loc[0], loc[1]]
. And likewise in a 2D world a location at (4, 5) sent the message .surroundingLocations(false, 1)
will return [loc[3, 4], loc[3, 5], loc[3, 6], loc[4, 4], loc[4, 5], loc[4, 6], loc[5, 4], loc[5, 5], loc[5, 6]]
. Here's the code that resembles this: ALocation(AWorld(2, 10), [4, 5]).surroundingLocations(false, 1)
. Last example: a location within a 3D world asked to return its surroundings with an area of 3 like this: ALocation(AWorld(3, 100), [40, 50]).surroundingLocations(false, 3).size
will return a list of 343 unique locations.
DETAILS:
When a location object is created it check its world's size and wrap around borders (by doing modulo(size) on the coordinates in the list).
The location class expects the world to be of uniform size in all dimensions.
- 050712 -
distance
might need to go in a subclass if we do networked worlds - Rob's comment. How to calculate the distance between worlds?
- 050712 - At Rob's suggestion: I'll try to rewrite this and the world classes using hashtable lookup. The matrix/location duality causes trouble keeping the same thing at two places.
- 050712 - Naming needs to be improved. Specially
AQLocation
- what to call it?
- 050726 - Removed the
maxDimension
and surroundMaxArea
limitations and its class variable
- 050726 - Now hashtable lookup + C primitives. Quite a lot faster overall and easier to keep things at the same place.
SUBCLASSES:
AQLocation
- a quantified location. The coordinates for this class can be floting point but when it places itself in the matrix it rounds of to nearest integer.
AWorld
BASICS:
A placeholder for items. Superclass for APattern
, AGrid
, ACube
, BugWorld
etc.
.new takes 3 arguments: dimensions, size and location
Instance variable dimensions
is an integer specifying the number of dimensions for this world. Can be get
.
Instance variable size
will decide the size of 1 dimension. The world is then created with a uniform size in all dimensions. Can be get
.
Instance variable location
if defined, will place the world at a location. If left out - no parent. Can be get
.
ADDITIONAL:
The clear
method takes a location object as an argument and puts nil
there.
With the remove
method - argument: an item - you remove an item from this world.
With put
you place an item in this world. Argument: item
The get
method returns whatever item(s) is in a location. Argument: location
Method items
returns a list of all items in this world.
neighbours
- arguments: item, exclude and area. Returns a list of any items within an item's area (1=adjcent locations) including or excluding the item's own location. If no items nearby then empty list.
neighboursSparse
is similar to neighbours
above (same arguments and function) but uses a different algorithm for finding nearby items. Where neighbours
calculates locations around the item in question and then check these locations for any items, this method might be quicker in a sparse world. It looks through all items in the world and checks if they're nearby.
Running the update
method goes through all items in this world, copies them to their own locations. This is to make sure all item's locations and the hashtable stays in sync.
save
will write this world, its current settings and all its items and their settings to disk. This allows for backup of longrunning tasks or 'presets' to be loaded.
DETAILS:
SUBCLASSES:
AWorld2
- remove
and put
methods are modified to allow for multiple items at the same location. AWorld
can only do one item in once location. Hopefully, I can merge the two classes later.
ASmartWorld
- is a world that can resolve location conflicts. There's a resolve
method to be called after the sense
cycle but before act
. This goes through all items and if more than one intends to move to the same location, only let one move - others stay put and their request is ignored. (Comment: need to find a better name than ASmartWorld
)
- And many other subclasses. E.g.
BugWorld
, APattern
, AGrid
, ACube
. Almost every test program has its own specialised class inheriting from these two subclasses.
AnItem
BASICS:
The lowest level thing that exists in a world. Abstract superclass class for ARock
, AMoss
, AnAgent
.new
takes 1 argument: location
ADDITIONAL:
The method remove
will remove this item from its world.
DETAILS:
The abstract init
method is used by some subclasses for initialisation.
- 050726 - is
remove
needed? Will the agent remove itself or will the world handle that eg if energy=0.
SUBCLASSES:
ARock
- does nothing different. Just exists at a location
AMoss
- has an energy that can be get
/set
.
AnAgent
- is an abstract class. See below
AnAgent
Subclass of AnItem
but is also an abstract class.
Makes sure the sense
and act
methods are there for all agents to follow.
SUBCLASSES:
- Many. E.g.
ACell
, ARunaway
, ABounce
, Bug
. Every test program has its own specialised class inheriting from this one. They all do sense
and act
in their own way.
AProxyAgent
Subclass of AnAgent
. It allows replacing the sense
and act
methods while running.
DETAILS:
When asked to sense and act, sense and act in this class will instead evaluate functions stored in the 2 class variables senseFunc
and actFunc
. These can be replaced and coded on the fly! So while the system is running, we can try out, completely rewrite or just slightly modify, behaviour for all agents. Their state is kept (individually) but behaviour changes.
- This is unique to other frameworks I've seen so far. I'd like to explore more and hopefully, we can use it in practice too - not just as a convenience for developing. With this feature, it's easy to replace the rules on the fly.
- Perhaps I redesign the whole framework to use proxies. So the
AnItem
class is really a placeholder (proxy) for anything. Then one can code whole agents with state and behaviour while running I think. And maybe proxy worlds too but I can't find a reason for that now.
APattern
BASICS:
A subclass of AWorld
with 1 dimension.
AGrid
BASICS:
A subclass of AWorld
that has 2 dimensions.
ACube
BASICS:
A subclass of AWorld
that has 3 dimensions.
ACell
used in A4_test1_cellautomata.scd
BASICS:
Subclass of AnAgent
. It doesn't move and is used for cellular automata and game-of-life.
Instance variable value
can be 0 or 1. Can be get
/set
.
There's also a rules
class variable that contains a dictionary for rule lookup.
ADDITIONAL:
The sense
method here collects and stores values from nearby neighbours (by asking the world for neighbours) including the cell's own value.
The act
method set the cell's own value to what is returned from the rules dictionary.
ALifeCell
used in A4_test2_gameoflife.scd
BASICS:
Subclass of ACell
. Just implements different sense
and act
methods.
ADDITIONAL:
The sense
method here is the same as ACell.sense
but excludes the cell's own value.
act
will first calculate the total sum of all neighbour's values and then do a lookup in the rules dictionary. The cell's own values is set to 0 or 1 depending on what the dictionary returns.
ARunaway
used in A4_test3_runaway1D.scd, A4_test4_runaway2D.scd and A4_test8_runaway3D.scd
BASICS:
A subclass of AnAgent
that sense if something at next location and if so, bleep, turn around 90 and flee.
Instance variable direction
is a list of directions in any dimension. In a 2D world: [0, 0]
stand still, [-1, 0]
go west, [1, 1]
go northeast and so on. Can be get
/set
.
Instance variable freq
decides which bleep frequency to play.
ADDITIONAL:
The sense
method updates the 2 private nextLocation
and nextPos
instance variables to figure out where to go and if that location is taken.
Helper method clearAhead
returns true if there's nothing in nextPos
getNextLocation
returns a new location object at here + directionlist.
getNewDirection
method turns directionlist around 90 degrees.
The move
method sets this location to nextLocation
The play
method will beep at a frequency. And pan the sound left/right depending on location.
ABounce
1D - used in A4_test6_bounce1D.scd
BASICS:
Subclass of ARunaway
. Implements getNextLocation
and getNewDirection
differently so that the agents bounce of each other rather than turn 90 degrees.
direction
is here a vector of angle and degree.
SUBCLASSES:
ABounce2D
and ABounce3D
for vector math in other dimensions.
The main loop of the program is usually very simple. For the CellularAutomaton and GameOfLife examples it just draws a rectangle if the cell's value is 1, then call .sense
on all agents and last call .act
for all agents.
while {true}, {
agents.do{|a| if(a.value==1, {a.paintRect})}
agents.do{|a| a.sense};
agents.do{|a| a.act};
}
Agents that move around (i.e. all other examples) need to resolve conflicts and update the world. Also they always draw themselves.
while {true}, {
agents.do{|a| a.paintRect}
agents.do{|a| a.sense};
world.resolve;
agents.do{|a| a.act};
world.update;
}
Updates:
- 061017: bugfix to the distance method in extArrayedCollection.sc
- 171228: new help file and scd instead of rtf
Another thing I played around with while at UoW was cellular automata. Here's a simple one-dimensional CA class for SuperCollider... external link (or attached as zip below)
And here is some more SuperCollider code I wrote to come to grips with CA...
/*cellular automata /redFrik*/
(
var w, u, width= 400, height= 300, cellWidth= 1, cellHeight= 1;
w= Window("ca - 1", Rect(128, 64, width, height), false);
u= UserView(w, Rect(0, 0, width, height));
u.background= Color.white;
u.drawFunc= {
var pat, dict, rule, ruleRand, y= 0;
/*
rule30= 30.asBinaryDigits; // [0, 0, 0, 1, 1, 1, 1, 0];
rule90= 90.asBinaryDigits; // [0, 1, 0, 1, 1, 0, 1, 0];
rule110= 110.asBinaryDigits; // [0, 1, 1, 0, 1, 1, 1, 0];
rule250= 250.asBinaryDigits; // [1, 1, 1, 1, 1, 0, 1, 0];
rule254= 254.asBinaryDigits; // [1, 1, 1, 1, 1, 1, 1, 0];
*/
/*-- select rule here --*/
//rule= 256.rand.postln;
//rule= 90;
rule= 30;
pat= 0.dup((width/cellWidth).round);
pat.put((pat.size/2).round, 1);
dict= ();
8.do{|i| dict.put(i.asBinaryDigits(3).join.asSymbol, rule.asBinaryDigits[7-i])};
//--render
Pen.fillColor= Color.black;
while({y*cellHeight<height}, {
pat.do{|c, x|
if(c==1, {
Pen.fillRect(Rect(x*cellWidth, y*cellHeight, cellWidth, cellHeight));
});
};
pat= [0]++pat.slide(3, 1).clump(3).collect{|c|
dict.at(c.join.asSymbol);
}++[0];
y= y+1;
});
};
w.front;
)
More interesting than these simple examples are of course things like game-of-life.
Here's one implementation of GOL for SuperCollider...
//game of life /redFrik
(
var envir, copy, neighbours, preset, rule, wrap;
var w, u, width= 200, height= 200, rows= 50, cols= 50, cellWidth, cellHeight;
w= Window("ca - 2 pen", Rect(128, 64, width, height), false);
u= UserView(w, Rect(0, 0, width, height));
u.background= Color.white;
cellWidth= width/cols;
cellHeight= height/rows;
wrap= true; //if borderless envir
/*-- select rule here --*/
//rule= #[[], [3]];
//rule= #[[5, 6, 7, 8], [3, 5, 6, 7, 8]];
//rule= #[[], [2]]; //rule "/2" seeds
//rule= #[[], [2, 3, 4]];
//rule= #[[1, 2, 3, 4, 5], [3]];
//rule= #[[1, 2, 5], [3, 6]];
//rule= #[[1, 3, 5, 7], [1, 3, 5, 7]];
//rule= #[[1, 3, 5, 8], [3, 5, 7]];
rule= #[[2, 3], [3]]; //rule "23/3" conway's life
//rule= #[[2, 3], [3, 6]]; //rule "23/36" highlife
//rule= #[[2, 3, 5, 6, 7, 8], [3, 6, 7, 8]];
//rule= #[[2, 3, 5, 6, 7, 8], [3, 7, 8]];
//rule= #[[2, 3, 8], [3, 5, 7]];
//rule= #[[2, 4, 5], [3]];
//rule= #[[2, 4, 5], [3, 6, 8]];
//rule= #[[3, 4], [3, 4]];
//rule= #[[3, 4, 6, 7, 8], [3, 6, 7, 8]]; //rule "34578/3678" day&night
//rule= #[[4, 5, 6, 7], [3, 5, 6, 7, 8]];
//rule= #[[4, 5, 6], [3, 5, 6, 7, 8]];
//rule= #[[4, 5, 6, 7, 8], [3]];
//rule= #[[5], [3, 4, 6]];
neighbours= #[[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]];
envir= Array2D(rows, cols);
copy= Array2D(rows, cols);
cols.do{|x| rows.do{|y| envir.put(x, y, 0)}};
/*-- select preset here --*/
//preset= #[[0, 0], [1, 0], [0, 1], [1, 1]]+(cols/2); //block
//preset= #[[0, 0], [1, 0], [2, 0]]+(cols/2); //blinker
//preset= #[[0, 0], [1, 0], [2, 0], [1, 1], [2, 1], [3, 1]]+(cols/2); //toad
//preset= #[[1, 0], [0, 1], [0, 2], [1, 2], [2, 2]]+(cols/2); //glider
//preset= #[[0, 0], [1, 0], [2, 0], [3, 0], [0, 1], [4, 1], [0, 2], [1, 3], [4, 3]]+(cols/2); //lwss
//preset= #[[1, 0], [5, 0], [6, 0], [7, 0], [0, 1], [1, 1], [6, 2]]+(cols/2); //diehard
//preset= #[[0, 0], [1, 0], [4, 0], [5, 0], [6, 0], [3, 1], [1, 2]]+(cols/2); //acorn
preset= #[[12, 0], [13, 0], [11, 1], [15, 1], [10, 2], [16, 2], [24, 2], [0, 3], [1, 3], [10, 3], [14, 3], [16, 3], [17, 3], [22, 3], [24, 3], [0, 4], [1, 4], [10, 4], [16, 4], [20, 4], [21, 4], [11, 5], [15, 5], [20, 5], [21, 5], [34, 5], [35, 5], [12, 6], [13, 6], [20, 6], [21, 6], [34, 6], [35, 6], [22, 7], [24, 7], [24, 8]]+(cols/4); //gosper glider gun
//preset= #[[0, 0], [2, 0], [2, 1], [4, 2], [4, 3], [6, 3], [4, 4], [6, 4], [7, 4], [6, 5]]+(cols/2); //infinite1
//preset= #[[0, 0], [2, 0], [4, 0], [1, 1], [2, 1], [4, 1], [3, 2], [4, 2], [0, 3], [0, 4], [1, 4], [2, 4], [4, 4]]+(cols/2); //infinite2
//preset= #[[0, 0], [1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0], [7, 0], [9, 0], [10, 0], [11, 0], [12, 0], [13, 0], [17, 0], [18, 0], [19, 0], [26, 0], [27, 0], [28, 0], [29, 0], [30, 0], [31, 0], [32, 0], [34, 0], [35, 0], [36, 0], [37, 0], [38, 0]]+(cols/4); //infinite3
//preset= Array.fill(cols*rows, {[cols.rand, rows.rand]});
preset.do{|point| envir.put(point[0], point[1], 1)};
i= 0;
u.drawFunc= {
i= i+1;
Pen.fillColor= Color.black;
cols.do{|x|
rows.do{|y|
if(envir.at(x, y)==1, {
Pen.addRect(Rect(x*cellWidth, height-(y*cellHeight), cellWidth, cellHeight));
});
};
};
Pen.fill;
cols.do{|x|
rows.do{|y|
var sum= 0;
neighbours.do{|point|
var nX= x+point[0];
var nY= y+point[1];
if(wrap, {
sum= sum+envir.at(nX%cols, nY%rows); //no borders
}, {
if((nX>=0)&&(nY>=0)&&(nX<cols)&&(nY<rows), {sum= sum+envir.at(nX, nY)}); //borders
});
};
if(rule[1].includes(sum), { //borne
copy.put(x, y, 1);
}, {
if(rule[0].includes(sum), { //lives on
copy.put(x, y, envir.at(x, y));
}, { //dies
copy.put(x, y, 0);
});
});
};
};
envir= copy.deepCopy;
};
Routine({while{w.isClosed.not} {u.refresh; i.postln; (1/20).wait}}).play(AppClock);
w.front;
)
Updates:
- 171228: converted rtf to scd and added help file
- 210525: much improved versions attached using Image to draw
A small project I did for Mark d'Invero's inaugural talk November 2004 was to create a sort of entrance music or ambience as people came in and took their seats. I transcribed one of Mark's jazz tunes called Val's song. The head of this song - repeated over and over - formed the basic musical material. Then I coded a few agents that would play and manipulate it. The agents got input from a video-analysis program I had written in Max/SoftVNS. So there was a DV-camera looking at the crowd taking their seats and the program looked at the total motion of 4 different areas of the hall. As the people settled the amount of motion decreased to nearly zero and this made a nice form curve for the piece as a whole... people coming in, mingling and slowly calming down right before the talk.
The agents were allowed control over certain aspects of the music like overall tempo, which scale to use, transposition, volume, sustain and legato of tones and the amount of reverb. And we used SuperCollider plus the RedDiskInSamplerGiga
with a grand-piano sample library to get a nice but discrete sound. Unfortunately very few noticed the music system as we did not allow it much volume and the crowd was pretty noisy.
Here's the basic song material Val's song in SuperCollider code...
/*val's song by m.d'inverno*/
s.boot;
TempoClock.default.tempo_(2.2)
(
a= Pdef(\x, Pbind( /*melody*/
\root, 3,
\octave, 5,
[\degree, \dur], Pseq([
[-3, 1.5], [0, 1], [1, 0.5], [2, 0.5], [3, 1], [4, 1], [2, 0.5],
[-3, 1.5], [0, 1], [2, 0.5], [1, 3],
[0, 0.75], [1, 0.75], [2, 0.75], [4, 0.75], [7, 0.75], [8, 0.75], [9, 0.75], [7, 0.75],
[8, 1.5], [7, 1], [9, 0.5], [8, 3],
[-3b, 1.5], [0, 1], [7, 0.5], [7, 2], [0, 1],
[0, 1.5], [7, 1.5], [7, 0.5], [8, 0.5], [9, 2],
[7, 0.5], [4, 1], [4, 1], [4, 0.5], [3, 0.5], [-1b, 1], [-1b, 1], [-2b, 0.5],
[-3, 3], [\rest, 3]
], inf)
));
b= Pdef(\y, Pbind( /*bass*/
\root, 3,
\octave, 4,
[\degree, \dur], Pseq([
[4, 3], [0, 2], [0, 1],
[4, 3], [-1, 2], [-1, 1],
[-2, 3], [-2, 2], [2, 1],
[-3, 3], [-3, 2], [-2b, 1],
[-2, 2], [0, 1], [2, 3],
[3, 1.5], [-2, 1.5], [0, 3],
[-3, 3], [-3, 2], [0b, 1],
[0, 3], [-3, 3]
], inf)
))
)
Ppar([a, b]).play;
And below is the code I stepped through as a small part of the actual talk. This was just to let people see how we could build music from scratch using SuperCollider. In the end Mark played along together with this on his electrical piano.
/*--setup--*/
s.boot;
a= [0, 0.25, 1, 0, 0, 0.5, 0, 0.25].scramble;
b= [1, 0, 0, 0, 1, 0.25, 0, 0, 0, 0.5, 0, 0.5, 1, 0, 1, 1].scramble;
c= TempoClock.new;
p= ProxySpace.push(s, clock: c);
~out.ar(2);
~pattern1.kr(1);
~pattern2.kr(1);
~pattern3.kr(1);
~pattern4.kr(1);
~out.play;
/*--talk demo--*/
/*definition*/
(
~bassdrum= {arg t_trig, amp= 1, release= 2.5, freq= 100;
Decay2.kr(t_trig, 0.01, release, amp)
* SinOsc.ar([freq, freq*0.9], 0, 0.5)
}
)
~out.add(~bassdrum)
~bassdrum.set(\t_trig, 1)
/*change parameters*/
~bassdrum.set(\freq, 70, \release, 0.3)
~bassdrum.set(\t_trig, 1)
/*add an delay effect*/
~bassdrum.filter(1, {arg in; z= 0.2; in+CombN.ar(in, z, z, 3)})
~bassdrum.set(\t_trig, 1)
~bassdrum.filter(1, {arg in; in})
/*play pattern*/
~pattern1= StreamKrDur(Pseq([1, 0, 0, 1, 0, 1, 1, 1], inf), 0.25)
c.sched(c.timeToNextBeat(1), {~bassdrum.map(\t_trig, ~pattern1)})
/*swing*/
~pattern1= StreamKrDur(Pseq([1, 0, 0, 1, 0, 1, 1, 1], inf), Pseq((0.25*[1.2, 0.8]), inf))
/*add more drums*/
(
~snaredrum= {arg t_trig, amp= 1, release= 0.12;
Decay2.kr(t_trig, 0.01, release, amp) * Pan2.ar(Resonz.ar(ClipNoise.ar(0.3), 1500, 0.5))
};
~out.add(~snaredrum);
)
/*play pattern*/
(
~pattern2= StreamKrDur(Pseq([0, 1, 0, 0], inf), 0.5);
c.sched(c.timeToNextBeat(1), {~snaredrum.map(\t_trig, ~pattern2)});
~pattern1= StreamKrDur(Pseq([0, 1, 0, 1, 0, 1, 0.25, 1], inf), 0.25);
~pattern2= StreamKrDur(Pseq([1, 0.25, 1, 1, 0, 0, 0, 0, 0.5, 0.5, 0, 0.5, 0, 0, 0, 0], inf), 0.125);
)
/*add a bass w/ random melody and change drum patterns*/
(
~bass= {arg freq= 60, amp= 0;
RLPF.ar(Saw.ar([freq, freq*1.01], amp), SinOsc.kr(0.2, 0, 200, 500), 0.2, 0.1)
};
~out.add(~bass);
)
(
~pattern3= StreamKrDur(Pseq([1, 0, 0.5, 0.5, 0, 1, 1, 0.5, 0.25, 1, 0, 1, 0.5, 0, 1, 0]*0.6, inf), Pseq((0.125*[1.2, 0.8]), inf));
~pattern4= StreamKrDur(Pseq(b*100+20, inf), Pseq((0.125*[1.2, 0.8]), inf));
c.sched(c.timeToNextBeat(1), {~bass.map(\amp, ~pattern3)});
c.sched(c.timeToNextBeat(1), {~bass.map(\freq, ~pattern4)});
)
~out.release(2);
p.clear;
For my work together with Mark d'Inverno I coded a few tools. One of the things that came up was a need for a neutral but nice-sounding way to test aspects of our musical-agents systems. So we got hold of a grand-piano sample library and I wrote a 'giga sampler' like class for SuperCollider. This allowed us to use this massive sample library (2.5GB) and let the agents play high-quality samples instead of cheap MIDI or boring synthesised sounds. So for testing melodies, harmonies and such, this was a good thing.
The trick with the giga sampler is that it preloads a bit from each sound file into RAM and then streams the rest from disk when needed. Or at least this is how I understand it. So using this technique, one can get quick access to a lot more samples than normally would fit in the memory of a sampler. A massive library like ours with full 88keys range, sampled in many different velocities, would occupy ~5GB of RAM (SuperCollider uses 32bit internally), nor could it be streamed from disk (the harddrive would be too slow to access and load the headers to play fast chords progressions without staggering etc).
I spent some time to make my class all-round useful and it can be downloaded from github.com/redFrik/redSampler or simply install by typing Quarks.install("redSampler")
in SuperCollider. The class of interest in that package is called RedDiskInSampler
.
And here is some testcode for it...
s.boot;
d= (); /*for mapping midinote to filename*/
r.free;
r= RedDiskInSamplerGiga(s); /*sampler*/
(
var folder= "/Users/asdf/Documents/soundfiles/bosen44100/"; //edit to match your samplebank
var velocities= #[40, 96]; /*velocities to load*/
var octavesToLoad= #[2, 3]; /*how many octaves to load*/
var scale= #['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
/*so number of samples to load is octavesToLoad.size*12*velocities.size*/
/*lowest note C2= midi 36*/
velocities.do{|x, i|
var tempDict= ();
d.put(x, tempDict);
octavesToLoad.do{|y, j|
scale.do{|z, k|
var midinote, pathname, key;
midinote= y*12+12+k;
key= z++y;
pathname= folder+/+x+/+key++".aif";
key= (key++"_"++x).asSymbol;
tempDict.put(midinote, key); /*like (45 : A2_96)*/
("loading key"+key+"from file"+pathname).postln;
r.preload(key, pathname);
};
};
};
)
r.play(\C2_96, 0, 3, 1)
r.play(\D2_40, 0, 3, 1)
Tdef(\test).play
a= r.loadedKeys;
(Tdef(\test, {
inf.do{|i|
r.play(a.choose, 0, 0.45, 0.1);
0.5.wait;
}
}))
(Tdef(\test, {
b= a.asArray.scramble;
inf.do{|i|
b.do{|x, j|
r.play(x, 0, 0.35, 0.1);
0.15.wait;
};
2.wait;
}
}))
(Tdef(\test, {
b= a.asArray.sort;
inf.do{|i|
b.do{|x, j|
r.play(x, 0, 0.25, 0.08, amp: 0.6);
0.1.wait;
};
1.wait;
}
}))
Tdef(\test).stop
r.free
Another thing Mark d'Inverno and I did was to try to list all the things our musical method agents possibly could do. This was, of course, an impossible task but still, it gave us an overview and was a pretty fun and crazy project.
version 040511 /fredrik
CHORD:
- transpose a note tone up/down
- transpose some notes tone up/down
- transpose all notes tone up/down
- transpose a note octave up/down
- transpose some notes octave up/down
- transpose all notes octave up/down
- making more/less dissonant by transposing notes up/down
- shift by inverting
- inverting parts of the chord
- removing the highest/lowest note
- removing a middle note
- removing every other note
- removing dissonances
- adding the whole chord octave up/down
- adding higher/lower note
- adding a note in the middle
- adding notes in between
- adding dissonant notes
- detune a note up/down from current tuning (e.g. +10 cents)
- detune some notes up/down from current tuning (e.g. +10 cents)
- detune all notes up/down from current tuning (e.g. +10 cents) (i.e. pitch bend the whole chord)
--
- transpose a note tone up/down in current modus
- transpose some notes tone up/down in current modus
- transpose all notes tone up/down in current modus
- making more/less dissonant by transposing notes up/down in current modus
- shift by inverting in current modus
- inverting parts of the chord in current modus
- removing the root of current modus
- removing middle notes in current modus (e.g. 3rd, 5th)
- removing extension notes in current modus (i.e. E13#11 -> E9#11 -> E9 -> E7 -> E)
- adding higher/lower note in current modus
- adding a note in the middle in current modus
- adding notes in between in current modus
- adding extension notes in current modus (i.e. E -> E7 -> E9 -> E9#11 -> E13#11)
- adding root from another modus (e.g. E/A)
- adding extension chord from another modus (e.g. F#/E7)
- replace with a parallel chord (e.g. C -> Am)
- detune a note up/down from current tuning to another tuning (e.g. from just to 14-tone equal tuning)
- detune some notes up/down from current tuning to another tuning (e.g. from just to 14-tone equal tuning)
- detune all notes up/down from current tuning to another tuning (e.g. from just to 14-tone equal tuning)
--
- replace with chord sequence current modus (e.g. II-V7-I)
- replace with chord sequence from another modus
- arpeggiate up/down
- rhythmicize some notes in sequence
- rhythmicize all notes in sequence
- rhythmicize some notes in parallel
- rhythmicize all notes in parallel
- change duration of a note
- change duration of some notes
- change duration of all notes
--
- replace with chord sequence current modus (e.g. II-V7-I) in the current time
- replace with chord sequence from another modus in the current time
- arpeggiate up/down in the current time
- rhythmicize some notes in sequence in the current time
- rhythmicize some notes in parallel in the current time
- rhythmicize all notes in sequence in the current time
- rhythmicize all notes in parallel in the current time
- change duration of a note in the current time
- change duration of some notes in the current time
- change duration of all notes in the current time
--
- change volume/attack/decay/sustain/release of a note
- change volume/attack/decay/sustain/release of some notes
- change volume/attack/decay/sustain/release of all notes
--
- change timbre/instrumentation of a note
- change timbre/instrumentation of some notes
- change timbre/instrumentation of all notes
--
- change position in space for a note
- change position in space for some notes
- change position in space for all notes
MELODY:
- transpose a note tone up/down in or outside the current modus
- transpose some notes tone up/down in or outside the current modus
- transpose all notes tone up/down in or outside the current modus
- transpose a note octave up/down
- transpose some notes octave up/down
- transpose all notes octave up/down
- invert melody in or outside the current modus
- scale interval range in or outside the current modus (i.e. shrink or expand)
- transpose a note to match another modus
- transpose some notes to match another modus
- transpose all notes to match another modus
- replace a note with a few others in or outside the current modus
- replace some notes with a few others in or outside the current modus
- detune a note up/down from the current tuning (e.g. +10 cents)
- detune some notes up/down from the current tuning (e.g. +10 cents)
- detune all notes up/down from the current tuning (e.g. +10 cents) (i.e. pitch bend the whole melody)
--
- remove a note (i.e. pause)
- remove some notes (i.e. pause)
- remove notes with duration < x
- remove notes with duration > y
- remove notes with duration < x and > y
- change duration of a note in or outside time
- change duration of some notes in or outside time
- change duration of all notes in or outside time
- change duration and onset of a note in or outside time (timescale)
- change duration and onset of some notes in or outside time (timescale)
- change duration and onset of all notes in or outside time (timescale whole melody)
- make duration and onset of all notes shorter and repeat (e.g. divide time by 2 and play twice)
- make duration and onset of all notes shorter and play a variation instead of repeating
--
- play melody in retrograde
- play notes in retrograde but keep rhythm/duration
- play rhythm/duration in retrograde but keep notes
- play and repeat only sections of the melody
- shift notes some steps left/right but keep rhythm/duration
- shift rhythm/duration some steps left/right but keep notes
- randomize notes but keep rhythm/duration
- randomize rhythm/duration but keep notes
- replace a note but keep rhythm/duration
- replace some notes but keep rhythm/duration
- replace all notes but keep rhythm/duration
- replace a rhythm/duration but keep notes
- replace some rhythm/duration but keep notes
- replace all rhythm/duration but keep notes
--
- decrease or increase the number of notes in the current scale (quantify notes i.e. minimal effect in iStreet)
- decrease or increase the number of possible rhythms (quantify rhythms)
--
- change rhythm/duration continuously (e.g. ritardando)
- change rhythm/duration discrete (e.g. ritardando in time)
--
- repeat a note and rhythm/duration x times in or outside time (i.e. delay effect)
- repeat some notes and rhythm/duration x times in or outside time (i.e. delay effect)
- repeat all notes and rhythm/duration x times in or outside time (i.e. delay effect)
--
- rearrange notes in inc/dec order but keep rhythm/duration
- rearrange rhythm/duration in inc/dec order but keep notes
--
- add another voice in parallel to the melody
- add many other voices in parallel to the melody
- add another voice mirroring the melody
- add many other voices mirroring the melody in different ways
- add another standalone voice to the melody
- add many other standalone voices to the melody
- add another standalone voice contrasting the melody
- add many other standalone voices contrasting the melody
--
- change volume/attack/decay/sustain/release of a note
- change volume/attack/decay/sustain/release of some notes
- change volume/attack/decay/sustain/release of all notes
--
- change timbre/instrumentation of a note
- change timbre/instrumentation of some notes
- change timbre/instrumentation of all notes
--
- change position in space for a note
- change position in space for some notes
- change position in space for all notes
--
- reharmonize melody with 'good sounding' chords
- reharmonize melody with weird chords
- play melody in a different context
- play melody in another mood (e.g. sad, energetic or irritated)
- incorporate elements from other melodies
- blend two or more melodies (e.g. average note for note or play sections of each one)
- improvise freely over the melody
RHYTHM PATTERN:
- scale pattern with a constant factor in or outside time
- scale pattern with a changing factor in or outside time (e.g. random walk LFO for fluctuation)
- make duration and onsets in pattern shorter and repeat (e.g. divide time by 2 and play twice)
- make duration and onsets in pattern shorter and play a variation instead of repeating
--
- remove an element (i.e. pause)
- remove some elements (i.e. pause)
- remove elements with duration < x
- remove elements with duration > y
- remove elements with duration < x and > y
- change duration of an element in or outside time
- change duration of some elements in or outside time
- change duration of all elements in or outside time
- replace an element with a few others in or outside time
- replace some elements with a few others in or outside time
- replace all elements with a few others in or outside time
- add an element at a random position in or outside time
- add some elements at a random position in or outside time
- add an element in the middle in or outside time
--
- change position of an element to random in or outside time
- change position of some elements to random in or outside time
- change position of some all elements to random in or outside time (scramble pattern)
--
- repeat an element x times in or outside time (i.e. delay effect)
- repeat some elements x times in or outside time (i.e. delay effect)
- repeat all elements x times in or outside time (i.e. delay effect)
--
- play pattern backwards
- play and repeat only sections of the pattern
- rearrange elements in inc/dec duration order
--
- quantise an element to current time
- quantise some elements to current time
- quantise all elements to current time
--
- add another voice with different timbre/instrumentation in parallel to the pattern
- add many other voices with different timbre/instrumentation in parallel to the pattern
- add another voice mirroring the pattern rhythmically
- add many other voices mirroring the pattern in different ways
- add another standalone voice to the pattern
- add many other standalone voices to the pattern
- add another standalone voice contrasting the pattern
- add many other standalone voices contrasting the pattern
--
- change volume/attack/decay/sustain/release of an element
- change volume/attack/decay/sustain/release of some elements
- change volume/attack/decay/sustain/release of all elements
--
- change timbre/instrumentation of an element
- change timbre/instrumentation of some elements
- change timbre/instrumentation of all elements
--
- change position in space for an element
- change position in space for some elements
- change position in space for all elements
--
- vary the pattern based on some scheme (e.g. Nick's BBCut)
- play pattern in another mood (e.g. sad, energetic or irritated)
- incorporate elements from other patterns
- blend two or more patterns (e.g. average elements and threshold or play sections of each one)
- improvise freely over the pattern
EFFECTS: (very much in progress)
- delay: dub delay, looping with infinite delay
- filter: high/band/low pass, ringing filters
- panning: surround
- time stretch
- pitch shift
- segmenting/cutting: warp, scratch
- phase modulation
- amplitude modulation: tremolo, LFO clipping/gate, ring modulation
- mixing with another sound
- frequency modulation: vibrato
- distortion: overdrive, bit crunch
- FFT manipulations: convolution, vocoder
- limiter, expander, compressor, gate
- feedback: modulate local amp, phase, freq etc.
- grain: segment with different envelopes, panning, amplitude, frequency etc.
- amplitude follower and map to another sound
- pitch tracker and map to another sound