Skip to content

Instantly share code, notes, and snippets.

@opencoca
Created June 29, 2026 08:19
Show Gist options
  • Select an option

  • Save opencoca/0be10c29bee907d1056564e1de350d9e to your computer and use it in GitHub Desktop.

Select an option

Save opencoca/0be10c29bee907d1056564e1de350d9e to your computer and use it in GitHub Desktop.
Musical Sonfication of Elementary Cellular Automata
<!-- <select id="random selector" onchange="selChange()">
<option value="random">random</option>
<option value="single">single cell</option>
</select> -->

Musical Sonfication of Elementary Cellular Automata

Cell states within a sub-window of an elementary cellular automata space is mapped to musical pitches. Rule number, CA space size, initial conditions, musical sub-window size and position, sound-font voice, pitch-class mapping (scale/mode) and starting octave are all controllable. Enjoy!

A Pen by OpenCo on CodePen.

License.

var ready = false;
var ac = new AudioContext();
var ruleSet;
var grid;
var spinner;
var myWidth;
var resultRects = [];
var musicWind;
var yOffset = 140;
// DOM
var randSelect;
var gridSizeSelect;
var ruleSelect;
var windowSelect;
var windowSelectPos = [];
var scaleSelect;
var octaveSelect;
var voiceSelect;
var playButton;
var isPlaying = false;
var instr;
function setup() {
frameRate(10);
myWidth = window.innerWidth || document.body.clientWidth;
createCanvas(myWidth, 800);
createDOMStuff();
ruleSet = new RuleSet(109);
ruleSet.drawRules();
grid = new Grid(gridSizeSelect.value(), ruleSet);
grid.generate();
grid.drawMatrix();
musicWind = new MusicWindow(7);
musicWind.drawWindow();
musicWind.makeScaleArray();
musicWind.fillCurrentWindow();
voiceChanged();
new Tone.Loop(() => {
if (ready && isPlaying) {
next();
}
}, 0.4).start();
Tone.Transport.start();
}
function makeTopBackground() {
var margin = 5;
strokeWeight(1);
stroke(0);
fill(150);
rect(margin, margin, width - margin * 2, yOffset - margin * 2, 10);
}
function redrawAll() {
background(255);
makeTopBackground();
ruleSet.drawRules(yOffset);
redrawGrid(true);
}
// RULE
var Rule = function(contextArr, result) {
this.leftBox = contextArr[0] === "x";
this.centerBox = contextArr[1] === "x";
this.rightBox = contextArr[2] === "x";
this.result = result || false;
this.toggleRule = function() {
this.result = !this.result;
ruleSet.updateFromToggle();
redrawAll();
};
this.drawRule = function(width, startX) {
console.log("yOff: " + yOffset);
var topMargin = yOffset - 80;
var boxWidth = width / 5;
stroke(0);
strokeWeight(1);
fill(this.leftBox ? 0 : 255);
rect(boxWidth + startX, topMargin, boxWidth, boxWidth);
fill(this.centerBox ? 0 : 255);
rect(boxWidth * 2 + startX, topMargin, boxWidth, boxWidth);
fill(this.rightBox ? 0 : 255);
rect(boxWidth * 3 + startX, topMargin, boxWidth, boxWidth);
fill(this.result ? 0 : 255);
resultRects.push([
boxWidth * 2 + startX,
topMargin + boxWidth,
boxWidth,
boxWidth
]);
console.log("added", resultRects[resultRects.length - 1]);
rect(boxWidth * 2 + startX, topMargin + boxWidth, boxWidth, boxWidth);
};
};
// RULESET
var RuleSet = function(rules) {
this.ruleNum;
var contexts = [
["x", "x", "x"],
["x", "x", "o"],
["x", "o", "x"],
["x", "o", "o"],
["o", "x", "x"],
["o", "x", "o"],
["o", "o", "x"],
["o", "o", "o"]
];
if (typeof rules === "number") {
this.ruleNum = rules;
rules = ruleNumToRuleArray(rules);
}
this.rules = {};
for (var i = 0; i < contexts.length; i++) {
this.rules[contexts[i].join("")] = new Rule(contexts[i], rules[i]);
}
if (typeof this.ruleNum === "undefined") {
this.ruleNum = ruleArrayToRuleNum(this.rules);
}
this.drawRules = function() {
var ruleWidth = Math.min(width / 8, 1200 / 8);
var xOffset = (width - ruleWidth * 8) / 2;
resultRects = [];
for (var i = 0; i < contexts.length; i++) {
var rule = this.rules[contexts[i].join("")];
rule.drawRule(ruleWidth, ruleWidth * i + xOffset, yOffset);
}
};
this.updateFromToggle = function() {
// var results = contexts.map(function(a) {return this.rules[a.join('')].result;});
var results = [];
for (var i = 0; i < contexts.length; i++) {
var rule = this.rules[contexts[i].join("")];
results.push(rule.result);
}
this.ruleNum = ruleArrayToRuleNum(results);
ruleSelect.value(this.ruleNum);
};
this.getRuleFromContext = function(context) {
var contextString = [
context[0] ? "x" : "o",
context[1] ? "x" : "o",
context[2] ? "x" : "o"
].join("");
return this.rules[contextString];
};
this.getRuleFromPosition = function(pos) {
return (rule = this.rules[contexts[pos].join("")]);
};
this.getResult = function(context) {
// context = [left,center,right];
return this.getRuleFromContext(context).result;
};
};
// GRID
var Grid = function(cellsAcross, ruleSet) {
this.cellsAcross = cellsAcross;
this.cellsDown;
this.matrix = [];
this.ruleSet = ruleSet;
this.boxWidth;
this.generate = function() {
var boxWidth = width / this.cellsAcross;
this.cellsDown = Math.floor((height - yOffset) / boxWidth) + 1;
console.log("cellsDown: " + this.cellsDown);
this.matrix = [];
var firstline;
var firstVal = randSelect.value();
console.log(firstVal);
switch (firstVal) {
case "random":
firstline = this.makeRandomGridLine();
break;
case "x":
firstline = this.makeSingleCellGridLine();
break;
case "xx":
firstline = this.makeXXLine();
break;
case "xxx":
firstline = this.makeXXXLine();
break;
case "x x":
firstline = this.makeXOXLine();
break;
case "symmetrical":
firstline = this.makeRandomSymmetricalLine();
break;
default:
console.log("ERROR");
break;
}
console.log(firstline);
this.matrix.push(firstline);
for (var i = 1; i < this.cellsDown; i++) {
this.addNextLine();
}
};
this.makeSingleCellGridLine = function() {
var mid = Math.floor(this.cellsAcross / 2);
var line = [];
for (var i = 0; i < this.cellsAcross; i++) {
line.push(false);
}
line[mid] = true;
return line;
};
this.makeRandomGridLine = function() {
var line = [];
for (var i = 0; i < this.cellsAcross; i++) {
line.push(Math.random() > 0.5);
}
return line;
};
this.makeRandomSymmetricalLine = function() {
var mid = Math.ceil(this.cellsAcross / 2);
console.log("cellsAcross: " + this.cellsAcross / 2);
var line = [];
for (var i = 0; i < mid; i++) {
line.push(Math.random() > 0.5);
}
console.log("1st: " + line);
var end = line.slice();
if (this.cellsAcross % 2 === 1) {
end.pop();
}
end = end.reverse();
console.log("2nd: " + end);
line = line.concat(end);
return line;
};
this.makeXXLine = function() {
var mid = Math.floor(this.cellsAcross / 2);
var line = [];
for (var i = 0; i < this.cellsAcross; i++) {
line.push(false);
}
line[mid] = true;
line[mid + 1] = true;
return line;
};
this.makeXXXLine = function() {
var mid = Math.floor(this.cellsAcross / 2);
var line = [];
for (var i = 0; i < this.cellsAcross; i++) {
line.push(false);
}
line[mid - 1] = true;
line[mid] = true;
line[mid + 1] = true;
return line;
};
this.makeXOXLine = function() {
var mid = Math.floor(this.cellsAcross / 2);
var line = [];
for (var i = 0; i < this.cellsAcross; i++) {
line.push(false);
}
line[mid - 1] = true;
line[mid + 1] = true;
return line;
};
this.addNextLine = function() {
var prev = this.matrix[this.matrix.length - 1];
var next = this.makeNextLine(prev);
this.matrix.push(next);
if (this.matrix.length > this.cellsDown) {
this.matrix.shift();
}
};
this.makeNextLine = function(prev) {
var next = [];
var context = [];
for (var i = 0; i < prev.length; i++) {
if (i === 0) {
context = [prev[prev.length - 1], prev[0], prev[1]];
} else if (i === prev.length - 1) {
context = [prev[i - 1], prev[i], prev[0]];
} else {
context = [prev[i - 1], prev[i], prev[i + 1]];
}
next.push(this.ruleSet.getResult(context));
}
return next;
};
this.progress = function() {
this.matrix.push(this.makeNextLine(this.matrix[this.matrix.length - 1]));
this.matrix.shift();
};
this.drawMatrix = function() {
var offset = yOffset;
this.boxWidth = width / this.cellsAcross;
for (var row = 0; row < this.matrix.length; row++) {
for (var col = 0; col < this.cellsAcross; col++) {
var cell = this.matrix[row][col];
var color = cell ? 0 : 255;
fill(color);
stroke(color);
rect(
this.boxWidth * col,
offset + this.boxWidth * row,
this.boxWidth,
this.boxWidth
);
}
}
};
};
// WINDOW
var MusicWindow = function(size) {
this.size = size;
this.scaleName = "Pentatonic";
this.playingNotes = [];
this.pitches;
this.windowStart = Math.floor((grid.cellsAcross - this.size) / 2);
this.previousWindow = [];
this.currentWindow = [];
for (var i = 0; i < this.size; i++) {
this.currentWindow.push(false);
}
this.drawWindow = function() {
noFill();
stroke(255, 204, 100);
strokeWeight(4);
rect(grid.boxWidth * this.windowStart, yOffset, this.size * grid.boxWidth, grid.boxWidth);
};
this.shiftLeft = function() {
if (this.windowStart > 0) {
this.windowStart -= 1;
}
this.updateWindow();
};
this.shiftRight = function() {
if (this.windowStart + this.size < grid.cellsAcross) {
this.windowStart += 1;
}
this.updateWindow();
};
this.recenter = function() {
this.windowStart = Math.floor((grid.cellsAcross - this.size) / 2);
this.updateWindow();
};
this.validateWindow = function() {
if (this.windowStart < 0) {
this.windowStart = 0;
}
if (this.size >= grid.cellsAcross) {
this.size = grid.cellsAcross;
recreateWindowSelect();
}
this.recenter();
this.updateWindow();
};
this.updateWindow = function() {
this.makeScaleArray();
this.fillCurrentWindow();
redrawGrid(false);
};
this.fillCurrentWindow = function() {
if (this.previousWindow.length === 0) {
for (var i = 0; i < this.size; i++) {
this.previousWindow.push(false);
}
} else {
this.previousWindow = this.currentWindow.slice();
}
this.currentWindow = grid.matrix[0].slice(
this.windowStart,
this.windowStart + this.size
);
};
this.makeScaleArray = function() {
var pitchClasses = Scales[this.scaleName];
this.pitches = [];
var octave = parseInt(octaveSelect.value());
for (var i = 0; i < this.size; i++) {
var noteOctave = Math.floor(i / pitchClasses.length);
var nextNote =
60 + 12 * (octave + noteOctave) + pitchClasses[i % pitchClasses.length];
this.pitches.push(nextNote);
this.playingNotes.push(null);
}
};
this.playCurrentArray = function() {
for (var i = 0; i < this.size; i++) {
if (!this.previousWindow[i] && this.currentWindow[i]) {
this.playingNotes[i] = instr.play(this.pitches[i]);
} else if (this.previousWindow[i] && !this.currentWindow[i]) {
if (this.playingNotes[i] !== null) {
this.playingNotes[i].stop();
this.playingNotes[i] = null;
}
}
}
};
};
// GLOBAL FUNCTIONS
function next() {
grid.progress();
musicWind.updateWindow();
musicWind.playCurrentArray();
}
function createDOMStuff() {
if (myWidth > 900) {
var line1Margin = 20;
var topLineDist = myWidth / 5;
createRandSelect(topLineDist * 1, line1Margin);
createGridSizeSelect(topLineDist * 2, line1Margin);
createRuleSelect(topLineDist * 3, line1Margin);
createWindowSelect(topLineDist * 1, line1Margin * 2);
windowSelectPos = [topLineDist * 1, line1Margin * 2];
createScaleSelect(topLineDist * 2, line1Margin * 2);
createOctaveSelect(topLineDist * 3, line1Margin * 2);
createVoiceSelect(topLineDist * 4, line1Margin * 2);
createPlayButton(topLineDist * 4, line1Margin);
} else {
var line1Margin = 20;
var topLineDist = myWidth / 3;
createRandSelect(topLineDist * 1, line1Margin);
createGridSizeSelect(topLineDist * 2, line1Margin);
createRuleSelect(topLineDist * 1, line1Margin * 2);
createPlayButton(topLineDist * 2, line1Margin * 2);
createWindowSelect(topLineDist * 1, line1Margin * 3);
windowSelectPos = [topLineDist * 1, line1Margin * 3];
createScaleSelect(topLineDist * 2, line1Margin * 3);
createOctaveSelect(topLineDist * 1, line1Margin * 4);
createVoiceSelect(topLineDist * 2, line1Margin * 4);
yOffset += 45;
}
}
var startOptions = ["random", "x", "xx", "xxx", "x x", "symmetrical"];
function createRandSelect(xPos, yPos) {
randSelect = createSelect();
for (var i = 0; i < startOptions.length; i++) {
randSelect.option(startOptions[i]);
}
randSelect.changed(function() {
redrawGrid(true);
});
randSelect.position(xPos, yPos);
randSelect.size(100, 20);
var label = createP("First Line:");
label.position(xPos - 80, yPos - 10);
}
function createGridSizeSelect(xPos, yPos) {
var label = createP("Cells Across:");
label.position(xPos - 80, yPos - 10);
gridSizeSelect = createSelect();
for (var i = 5; i < 100; i++) {
gridSizeSelect.option(i);
}
gridSizeSelect.value(51);
gridSizeSelect.changed(function() {
grid.cellsAcross = parseInt(gridSizeSelect.value());
grid.generate();
musicWind.validateWindow();
});
gridSizeSelect.position(xPos, yPos);
gridSizeSelect.size(100, 20);
}
function createRuleSelect(xPos, yPos) {
ruleSelect = createSelect();
for (var i = 0; i < 255; i++) {
ruleSelect.option(i);
}
ruleSelect.value(109);
ruleSelect.changed(ruleChangedBySelect);
ruleSelect.position(xPos, yPos);
ruleSelect.size(100, 20);
var label = createP("Rule Number:");
label.position(xPos - 80, yPos - 10);
}
function createWindowSelect(xPos, yPos) {
windowSelect = createSelect();
for (var i = 2; i < 20; i++) {
windowSelect.option(i);
}
windowSelect.value(7);
windowSelect.position(xPos, yPos);
windowSelect.size(100, 20);
windowSelect.changed(windowSelectChanged);
var label = createP("Window Size:");
label.position(xPos - 80, yPos - 10);
}
function recreateWindowSelect() {
if (typeof windowSelect !== "undefined") {
windowSelect.remove();
windowSelect = createSelect();
var maxWinSize = Math.min(12, grid.cellsAcross);
for (var i = 3; i <= maxWinSize; i++) {
windowSelect.option(i);
}
windowSelect.value(musicWind.size);
windowSelect.position(windowSelectPos[0], windowSelectPos[1]);
windowSelect.changed(windowSelectChanged);
windowSelect.size(100, 20);
}
}
function windowSelectChanged() {
musicWind.size = parseInt(windowSelect.value());
musicWind.updateWindow();
}
function createScaleSelect(xPos, yPos) {
scaleSelect = createSelect();
for (var i = 0; i < scaleList.length; i++) {
scaleSelect.option(scaleList[i]);
}
scaleSelect.value("Diatonic_Thirds");
scaleSelect.position(xPos, yPos);
scaleSelect.size(100, 20);
var label = createP("Scale:");
// scaleSelect.changed();
label.position(xPos - 80, yPos - 10);
}
function createOctaveSelect(xPos, yPos) {
octaveSelect = createSelect();
for (var i = -2; i < 3; i++) {
octaveSelect.option(i);
}
octaveSelect.value(0);
octaveSelect.position(xPos, yPos);
octaveSelect.size(100, 20);
var label = createP("Octave:");
octaveSelect.changed(function() {
musicWind.makeScaleArray();
});
label.position(xPos - 80, yPos - 10);
}
function createVoiceSelect(xPos, yPos) {
voiceSelect = createSelect();
for (var i = 0; i < voiceList.length; i++) {
voiceSelect.option(voiceList[i]);
}
voiceSelect.value("marimba");
voiceSelect.position(xPos, yPos);
voiceSelect.size(100, 20);
var label = createP("Voice:");
voiceSelect.changed(voiceChanged);
label.position(xPos - 80, yPos - 10);
}
function voiceChanged() {
var newVoice = voiceSelect.value();
ready = false;
makeSpinner();
if (typeof instr !== "undefined") {
instr.stop();
}
console.log("setting voice");
Soundfont.instrument(ac, newVoice).then(function(voice) {
console.log("finshed");
ready = true;
removeSpinner();
instr = voice;
});
}
function createPlayButton(xPos, yPos) {
playButton = createButton("Play");
playButton.mousePressed(play);
playButton.position(xPos, yPos);
playButton.attribute("color", "red");
playButton.size(100, 20);
console.log(playButton);
}
function play() {
isPlaying = !isPlaying;
console.log(isPlaying ? "Stop" : "Play");
playButton.label = isPlaying ? "Stop" : "Play";
}
function ruleChangedBySelect() {
var newRule = parseInt(ruleSelect.value());
ruleSet = new RuleSet(newRule);
grid.ruleSet = ruleSet;
redrawAll();
}
function randSelectValue() {
return randSelect.value() === "random";
}
function redrawGrid(needsRegenerate) {
if (needsRegenerate) {
grid.generate();
}
grid.drawMatrix();
musicWind.drawWindow();
}
function keyPressed() {
switch (keyCode) {
case LEFT_ARROW:
musicWind.shiftLeft();
break;
case RIGHT_ARROW:
musicWind.shiftRight();
break;
case UP_ARROW:
musicWind.recenter();
break;
default:
break;
}
}
function mousePressed() {
checkForRuleResultClick();
}
function checkForRuleResultClick() {
for (var i = 0; i < resultRects.length; i++) {
if (clickedInBox(resultRects[i])) {
console.log(resultRects[i]);
var rule = ruleSet.getRuleFromPosition(i);
rule.toggleRule();
console.log("PASSED", rule);
// ruleSet.rules[i].toggleRule();
}
}
}
function clickedInBox(arr) {
// arr [x,y,width]
var x = arr[0];
var y = arr[1];
var wd = arr[2];
return mouseX > x && mouseX < x + wd && mouseY > y && mouseY < y + wd;
}
//static global functions
function ruleNumToRuleArray(num) {
var arr = [];
for (var i = 7; i >= 0; i--) {
if (num >= Math.pow(2, i)) {
arr.push(true);
num -= Math.pow(2, i);
} else {
arr.push(false);
}
}
return arr;
}
function ruleArrayToRuleNum(arr) {
var ruleNum = 0;
for (var i = 0; i < 8; i++) {
if (arr[i]) {
ruleNum += Math.pow(2, 7 - i);
}
}
return ruleNum;
}
// SPINNER
function makeSpinner() {
var size = 120;
fill(20, 20, 20, 200);
rect(0, 0, width, height);
spinner = createDiv();
spinner.addClass("loader");
spinner.position((width - size) / 2, (height - size) / 2);
}
function removeSpinner() {
spinner.remove();
redrawAll();
}
var scaleList = [
"Major",
"Dorian",
"Pentatonic",
"Diatonic_Thirds",
"Pelang",
"Chromatic"
];
var Scales = {
Major: [0, 2, 4, 5, 7, 9, 11],
Dorian: [0, 2, 3, 5, 7, 9, 10],
Pentatonic: [0, 3, 5, 7, 10],
Diatonic_Thirds: [0, 4, 7, 11, 14, 17, 21],
Diatonic_Fourths: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55],
Pelang: [0, 1, 3, 7, 8],
Chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
};
var voiceList = [
"accordion",
"acoustic_bass",
"acoustic_grand_piano",
"acoustic_guitar_nylon",
"acoustic_guitar_steel",
"agogo",
"banjo",
"blown_bottle",
"bright_acoustic_piano",
"celesta",
"choir_aahs",
"church_organ",
"drawbar_organ",
"dulcimer",
"electric_bass_finger",
"electric_bass_pick",
"electric_grand_piano",
"electric_guitar_clean",
"electric_guitar_jazz",
"electric_guitar_muted",
"electric_piano_1",
"electric_piano_2",
"flute",
"glockenspiel",
"harmonica",
"harpsichord",
"kalimba",
"koto",
"lead_1_square",
"lead_2_sawtooth",
"lead_3_calliope",
"lead_4_chiff",
"lead_5_charang",
"lead_6_voice",
"lead_7_fifths",
"marimba",
"melodic_tom",
"music_box",
"ocarina",
"orchestral_harp",
"overdriven_guitar",
"percussive_organ",
"pizzicato_strings",
"recorder",
"reed_organ",
"rock_organ",
"shakuhachi",
"shamisen",
"shanai",
"sitar",
"slap_bass_1",
"slap_bass_2",
"steel_drums",
"string_ensemble_1",
"string_ensemble_2",
"synth_choir",
"synth_drum",
"synth_strings_1",
"synth_strings_2",
"taiko_drum",
"tango_accordion",
"timpani",
"tinkle_bell",
"tubular_bells",
"vibraphone",
"whistle",
"woodblock",
"xylophone"
];
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/addons/p5.dom.js"></script>
<script src="https://codepen.io/Dafuseder/pen/eRrmPM.js"></script>
<script src="https://codepen.io/Dafuseder/pen/XgYaPe.js"></script>
p {
font-family: "Verdana", Times, serif;
font-size: 11px;
}
h1 {
font-size: 0px;
}
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
font-size: 0px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment