I'm thinking about whether there's a way to combine cellular automata and music. What's most interesting about cellular automata to me is their emergent behavior; we can't know the future form of a cellular automata, so it's completely dynamic. The music generated in this way would also be dynamically varied.

Linkļ¼ https://editor.p5js.org/StonesGate604/sketches/z6kcivP7c
const GRID_ROWS = 16;
const GRID_COLS = 16;
const CELL_SIZE = 30;
let grid;
let nextGrid;
let stepCol = 0;
let soundLoop;
let leadSynth;
let chordSynth;
let bassSynth;
let leadScale = ['E4', 'G4', 'A4', 'C5', 'D5', 'E5', 'G5', 'A5'];
let bassScale = ['C2', 'D2', 'F2', 'G2', 'A2'];
let chordSet = [
['C3', 'E3', 'G3'],
['F3', 'A3', 'C4'],
['G3', 'B3', 'D4'],
['A3', 'C4', 'E4']
];
function setup() {
createCanvas(GRID_COLS * CELL_SIZE, GRID_ROWS * CELL_SIZE);
grid = makeEmptyGrid();
nextGrid = makeEmptyGrid();
randomizeGrid();
leadSynth = new p5.PolySynth();
chordSynth = new p5.PolySynth();
bassSynth = new p5.MonoSynth();
soundLoop = new p5.SoundLoop(onStep, 0.25);
noStroke();
}
function draw() {
background(0);
drawGrid();
highlightCurrentColumn();
}
function mousePressed() {
if (getAudioContext().state !== 'running') {
userStartAudio();
}
if (!soundLoop.isPlaying) {
soundLoop.start();
}
}
function makeEmptyGrid() {
let arr = [];
for (let r = 0; r < GRID_ROWS; r++) {
arr[r] = [];
for (let c = 0; c < GRID_COLS; c++) {
arr[r][c] = 0;
}
}
return arr;
}
function randomizeGrid() {
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
grid[r][c] = random() < 0.25 ? 1 : 0;
}
}
}
function nextGeneration() {
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
const n = countNeighbors(r, c);
const current = grid[r][c];
let next = current;
if (current === 1 && (n < 2 || n > 3)) {
next = 0;
} else if (current === 0 && n === 3) {
next = 1;
}
nextGrid[r][c] = next;
}
}
let temp = grid;
grid = nextGrid;
nextGrid = temp;
}
function countNeighbors(r, c) {
let sum = 0;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const rr = (r + dr + GRID_ROWS) % GRID_ROWS;
const cc = (c + dc + GRID_COLS) % GRID_COLS;
sum += grid[rr][cc];
}
}
return sum;
}
function drawGrid() {
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
if (grid[r][c] === 1) {
if (r < 5) {
fill(0, 200, 255);
} else if (r < 11) {
fill(0, 255, 150);
} else {
fill(255, 200, 0);
}
} else {
fill(10);
}
rect(c * CELL_SIZE, r * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1);
}
}
}
function highlightCurrentColumn() {
fill(255, 255, 255, 40);
rect(stepCol * CELL_SIZE, 0, CELL_SIZE, height);
}
function onStep(time) {
playColumn(stepCol, time);
stepCol = (stepCol + 1) % GRID_COLS;
if (stepCol === 0) {
nextGeneration();
updateTempoFromGrid();
}
}
function playColumn(col, time) {
for (let r = 0; r < 5; r++) {
if (grid[r][col] === 1) {
const note = leadScale[r % leadScale.length];
leadSynth.play(note, 0.3, time, 0.15);
}
}
let chordActive = 0;
for (let r = 5; r < 11; r++) {
if (grid[r][col] === 1) chordActive++;
}
if (chordActive > 0) {
const chord = chordSet[col % chordSet.length];
chord.forEach(n => {
chordSynth.play(n, 0.2, time, 0.35);
});
}
let bassActive = 0;
for (let r = 11; r < GRID_ROWS; r++) {
if (grid[r][col] === 1) bassActive++;
}
if (bassActive > 0) {
const note = bassScale[col % bassScale.length];
bassSynth.play(note, 0.4, time, 0.3);
}
}
function updateTempoFromGrid() {
let alive = 0;
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
if (grid[r][c] === 1) alive++;
}
}
const density = alive / (GRID_ROWS * GRID_COLS);
const minInterval = 0.1;
const maxInterval = 0.5;
const interval = map(density, 0, 1, maxInterval, minInterval);
soundLoop.interval = interval;
}
For the cellular automata part, I mostly used the code from my midterm assignment, but made some modifications to make it simpler. After all, we don't need to consider visual factors now.
function mousePressed() {
if (getAudioContext().state !== 'running') {
userStartAudio();
}
if (!soundLoop.isPlaying) {
soundLoop.start();
}
}
function makeEmptyGrid() {
let arr = [];
for (let r = 0; r < GRID_ROWS; r++) {
arr[r] = [];
for (let c = 0; c < GRID_COLS; c++) {
arr[r][c] = 0;
}
}
return arr;
}
function randomizeGrid() {
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
grid[r][c] = random() < 0.25 ? 1 : 0;
}
}
}
function nextGeneration() {
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
const n = countNeighbors(r, c);
const current = grid[r][c];
let next = current;
if (current === 1 && (n < 2 || n > 3)) {
next = 0;
} else if (current === 0 && n === 3) {
next = 1;
}
nextGrid[r][c] = next;
}
}
let temp = grid;
grid = nextGrid;
nextGrid = temp;
}
function countNeighbors(r, c) {
let sum = 0;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const rr = (r + dr + GRID_ROWS) % GRID_ROWS;
const cc = (c + dc + GRID_COLS) % GRID_COLS;
sum += grid[rr][cc];
}
}
return sum;
}
function drawGrid() {
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
if (grid[r][c] === 1) {
if (r < 5) {
fill(0, 200, 255);
} else if (r < 11) {
fill(0, 255, 150);
} else {
fill(255, 200, 0);
}
} else {
fill(10);
}
rect(c * CELL_SIZE, r * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1);
}
}
}

In this code, the sound is not driven by the draw() function, but by p5.SoundLoop as a global metronome: each time the SoundLoop scans the Game of Life pattern of the current generation column by column, the live cells in the top row trigger melody notes of different pitches, the middle row plays a chord determined by the column number whenever there is a live cell, and the live cells in the bottom row decide whether to play a bass note; the program only evolves the cellular automaton and adjusts the beat interval according to the global live cell density when the SoundLoop has just completed a full scan of all 16 columns. Therefore, it is "driven by the sound clock to update the pattern", rather than the traditional method driven by the frame rate of draw().
function highlightCurrentColumn() {
fill(255, 255, 255, 40);
rect(stepCol * CELL_SIZE, 0, CELL_SIZE, height);
}
function onStep(time) {
playColumn(stepCol, time);
stepCol = (stepCol + 1) % GRID_COLS;
if (stepCol === 0) {
nextGeneration();
updateTempoFromGrid();
}
}
function playColumn(col, time) {
for (let r = 0; r < 5; r++) {
if (grid[r][col] === 1) {
const note = leadScale[r % leadScale.length];
leadSynth.play(note, 0.3, time, 0.15);
}
}
let chordActive = 0;
for (let r = 5; r < 11; r++) {
if (grid[r][col] === 1) chordActive++;
}
if (chordActive > 0) {
const chord = chordSet[col % chordSet.length];
chord.forEach(n => {
chordSynth.play(n, 0.2, time, 0.35);
});
}
let bassActive = 0;
for (let r = 11; r < GRID_ROWS; r++) {
if (grid[r][col] === 1) bassActive++;
}
if (bassActive > 0) {
const note = bassScale[col % bassScale.length];
bassSynth.play(note, 0.4, time, 0.3);
}
}
function updateTempoFromGrid() {
let alive = 0;
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
if (grid[r][c] === 1) alive++;
}
}
const density = alive / (GRID_ROWS * GRID_COLS);
const minInterval = 0.1;
const maxInterval = 0.5;
const interval = map(density, 0, 1, maxInterval, minInterval);
soundLoop.interval = interval;
}
Adjusting the rhythm by detecting living cells
function updateTempoFromGrid() {
let alive = 0;
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
if (grid[r][c] === 1) alive++;
}
}
const density = alive / (GRID_ROWS * GRID_COLS);
const minInterval = 0.1;
const maxInterval = 0.5;
const interval = map(density, 0, 1, maxInterval, minInterval);
soundLoop.interval = interval;
}