Game of music life

version 1

I have always been fascinated by the emergent nature of the Game of Life—a phenomenon driven by the inherent mechanisms of a program and full of unpredictability. In the midterm, I successfully reproduced a basic version of the Game of Life; and after learning about the p5.js audio library in class, I decided to try to combine the concepts of visual algorithms and sound interaction.

image.png

The combination involves dividing the pattern created from the Game of Life into three segments, representing melody, bass, and chord respectively, which are then played using a sequencer.

I don't plan to change too much; I'll continue using the framework I used during the midterms with the Game of Life.

image.png


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 current generation's Game of Life pattern column by column, it only adjusts the beat interval based on the global live cell density to evolve the cellular automaton. Therefore, it is "updated by the sound clock," rather than the traditional method driven by the frame rate of the draw() function.

work flow of 1st version

work flow of 1st version

Then, the number of surviving cells in each block is calculated on the generated pattern, and the corresponding musical notes are played.

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);
    }
  }

code

One slightly off-putting aspect is that the entire program is driven by a sound loop rather than by a draw function. Of course, p5js still draws each frame, but the frame rate does not change between frames.

Gemini's documentation says that SoundLoop is much more advanced at a lower level, but in my case it's basically…

let myLoop;

function onSoundLoop(timeFromNow) {
  mySound.play(timeFromNow); 
  background(random(255));
}

equal to

let lastTime = 0; 
let interval = 1000; 

function draw() {
  background(220);

  let currentTime = millis();
  if (currentTime - lastTime > interval) {
    fill(random(255), random(255), random(255));
    circle(width/2, height/2, 100);
    lastTime = currentTime;
  }
  
}

So the triggering mechanism of the entire program is like this.