Adding sound (not music) to CGoL

For general discussion about Conway's Game of Life.
Post Reply
User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Adding sound (not music) to CGoL

Post by pcallahan » June 28th, 2022, 5:15 pm

This is something I started to work on more than 20 years ago but was never really happy with the results. I tried again and I am making better progress. The question I had is what would Life "sound like" if patterns were capable of producing vibrations. I realize there is not one unambiguous answer. I thought it might be interesting to explore Life patterns moving across an elastic medium, roughly approximated as masses connected by Hookean springs to their four von Neumann neighbors (left, right, above, below) and constrained to move up and down. Masses have position and velocity. Their position relative to their neighbors produces a restorative force that changes velocity, and velocity changes position.

Despite taking liberties in such a simulation, e.g. not worrying too much about energy but rescaling as needed, I get the effect of waves propagating from Life cells. One improvement over my thoughts 20 years ago is to assign a mass to live cells that is greater than empty space. This way, even unchanging cells can have an effect on waves moving past them. Another change, though it seems obvious, is to update the vibrating medium more often than I update the Life pattern. For example, in my current code (after a lot of tweaking) I am assigning live cells a mass of 5 compared to 1 for empty cells, and I am doing 200 updates to the grid for every change to the Life pattern.

The approach is as follows:
  • Initialization: Assign mass 5 to live cells and mass 1 to all others. Assign position 0 and velocity 0 to all grid positions.
  • At each step, update position and velocity using a very rough discrete approximation to the wave equation, dividing restorative force by mass to get acceleration. The outer boundary is fixed at position 0, resulting in an echo that diminishes relative to new oscillation.
  • Every 200 steps, update the Life pattern. For each cell birth, assign the position to 50 and the mass to 5. For each cell death, assign the position to -50 and the mass to 1.
I've been able to animate the wave fronts from a series of images as well as produce audio. The audio can be recorded from a specific cell so it is location dependent. The sound isn't musical at all, but I can get the hum of a glider with increasing volume as it approaches, as well as a noticeable doppler shift when it recedes. I made images showing the wavefronts from a non-interacting glider (left hand side heading southeast), blinker (center), and block (somewhat above and to the right of center). I was able to make a 600-frame animated gif, but it's too big to upload here. I will work on better video options. Meanwhile, here are some still images.

To see the effect of the block, look very slightly right and a little above center and you can observe ripples in the mostly symmetric wave produced by the blinker. This is the result of its 4 cells having a mass of 5 compared to the unit mass of most other cells.

Wavefront after 6001 steps (30 Life generations). You can see the echo from the left side:
wave006001.png
wave006001.png (76.47 KiB) Viewed 876 times
After 85501 steps (427 Life generations):
wave085501.png
wave085501.png (76.56 KiB) Viewed 876 times
After 178501 steps (892 Life generations):
wave178501.png
wave178501.png (81.42 KiB) Viewed 876 times

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Adding sound (not music) to CGoL

Post by pcallahan » June 29th, 2022, 12:28 pm

Here's a link to an mp4 of 1250 generations of the R pentomino. https://www.youtube.com/watch?v=2jPJW24vFqw

It rescales the wave amplitude to keep it from getting too large, so there is a global effect when the center explosion calms down and restarts. I can make a video with sound once I figure out how. What I would really like is an interactive tool where you could click to place one or more "microphones" and mix them. The doppler effect is visible in moving gliders:
pic1200.png
pic1200.png (289.11 KiB) Viewed 815 times
The parameters aren't quite what I wrote in the original post. I'm still tuning this.

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Adding sound (not music) to CGoL

Post by pcallahan » July 1st, 2022, 2:04 am

There is some interest in this topic based on likes, so I put a little more effort into generating audio. It's a work in progress. The waves I generate directly are very low pitch. When I try to increase them in the wave calculation, the video is too busy to watch (though that's a secondary concern). I cheated somewhat to make these two videos by using a video editor to raise the pitch. These illustrate an approaching and receding glider, but I don't notice a doppler effect. I think I need to tweak the parameters to reduce the speed of sound for that. I have been able to see and hear it with other settings.

The videos roughly indicate the position of the "microphone" with a red circle:
https://youtu.be/imePowEqUHA https://youtu.be/BiRnYrDn7bY

(Note added later: if the 25 second video goes by too fast, you can slow it down to 1/2 or 1/4 the original speed and still hear the sound. I am not sure how YouTube adjusts the audio, but it is not what I would get by stretching out the wave to infrasound. Another liberty I took in sound generation, was to calculate an exponential moving average of the actual position and subtract that from the wave. Otherwise, the waveform itself cannot be centered at 0 over the duration. If I can find something I'm happy with I'll document it.)

Most of my code is kind of a mess, but I decoupled the wave calculation into its own Java class. This shows the tunable parameters and the method used. I used the same parameters to make the above videos.

Code: Select all

package org.pcallahan;

import java.util.Arrays;
import java.util.List;

public class WaveCalculator {
  private static double SPRING_CONSTANT = 0.002;
  private static double DAMPING = 0.9995;
  private static double LIVE_CELL_MASS = 5.0;
  private static double DEAD_CELL_MASS = 1.0;
  private static double PLUCK_DISPLACEMENT = 10.0;
  private static int STEPS_PER_GENERATION = 640;

  private final double[][] position;
  private final double[][] velocity;
  private final double[][] mass;

  public WaveCalculator(int numRows, int numColumns, List<Position> initialCells) {
    this.position = new double[numRows][numColumns];
    this.velocity = new double[numRows][numColumns];
    this.mass = new double[numRows][numColumns];

    for (var row : mass) {
      Arrays.fill(row, DEAD_CELL_MASS);
    }

    for (var cell : initialCells) {
      mass[cell.i()][cell.j()] = LIVE_CELL_MASS;
    }
  }

  private static void copyValues(double[] from, double[] to) {
    for (var i = 0; i < from.length; i++) {
      to[i] = from[i];
    }
  }

  /**
   * Calculate one step of a simplified wave equation in which each mass is subject to a
   * restorative force proportional to difference of its position (z coordinate) from the
   * average of its neighbors.
   */
  private void waveStep() {
    // Store original positions that change during each step.
    double[] rowAbove = new double[position[0].length];
    double[] row = new double[rowAbove.length];
    copyValues(position[0], row);

    for (var i = 1; i < position.length - 1; i++) {
      // Swap rows, so previous row is now rowAbove. The copy current row positions into row.
      double[] tempRow = row;
      row = rowAbove;
      rowAbove = tempRow;
      copyValues(position[i], row);

      // Apply the step assuming fixed boundary positions, updating velocity and position.
      for (var j = 1; j < position[i].length - 1; j++) {
        var avgPosition = 0.25 * (row[j - 1] + row[j + 1] + rowAbove[j] + position[i + 1][j]);
        var acceleration = SPRING_CONSTANT * (avgPosition - row[j]) / mass[i][j];
        velocity[i][j] = velocity[i][j] * DAMPING + acceleration;
        position[i][j] += velocity[i][j];
      }
    }
  }

  /**
   * Rescale to keep average position at 0 and standard deviation at 1. This does not maintain
   * constant energy but divides by standard deviation to keep the wave from going far off in amplitude.
   */
  private void rescale() {
    int n = position.length * position[0].length;
    double sum = 0.0;
    for (var i = 0; i < position.length; i++) {
      for (var j = 0; j < position[0].length; j++) {
        sum += position[i][j];
      }
    }
    double mean = sum / n;
    double sumSquares = 0.0;
    for (var i = 0; i < position.length; i++) {
      for (var j = 0; j < position[0].length; j++) {
        sumSquares += (position[i][j] - mean) * (position[i][j] - mean);
      }
    }
    double standardDeviation = Math.sqrt(sumSquares / n);
    for (var i = 0; i < position.length; i++) {
      for (var j = 0; j < position[0].length; j++) {
        position[i][j] = (position[i][j] - mean) / standardDeviation;
        velocity[i][j] /= standardDeviation;
      }
    }
  }

  public record Position(int i, int j) {
  }

  /**
   * Implement this to carry out an operation for each step of the calculator, such as saving
   * a waveform to generate sound at a cell position.
   */
  public interface WaveStepHandler {
    void handle(double[][] position, double[][] velocity, double[][] mass);
  }

  public void doGeneration(List<Position> births, List<Position> deaths, WaveStepHandler handler) {
    // Apply pattern change to wave medium.
    for (var cell : births) {
      position[cell.i()][cell.j()] -= PLUCK_DISPLACEMENT;
      mass[cell.i()][cell.j()] = LIVE_CELL_MASS;
    }
    for (var cell : deaths) {
      position[cell.i()][cell.j()] += PLUCK_DISPLACEMENT;
      mass[cell.i()][cell.j()] = DEAD_CELL_MASS;
    }

    // Now calculate wave steps between generations and delegate to handler.
    for (int i = 0; i < STEPS_PER_GENERATION; i++) {
      waveStep();
      rescale();
      handler.handle(position, velocity, mass);
    }
  }

  public double[][] getPosition() {
    return position;
  }

  public double[][] getVelocity() {
    return velocity;
  }

  public double[][] getMass() {
    return mass;
  }
}
Update: Another approach that would obviously be a lot faster to generate is to associate a short "chime" with each transition and mix them back into a single wave. Instead of making them go in unison, each would reach the virtual microphone with a speed of sound delay and attenuation with distance. You would get the same effect of an approaching and receding glider. The chimes themselves would not be doppler shifted, but the beat would be different for approaching and receding gliders, assuming the speed of sound is not too great compared to glider speed (maybe it could be fixed at the CGoL speed of light). This approach is less physically interesting, since there would be no way for still Life cells to affect sound by their mass in the propagation medium, but that's a very subtle effect that may be negligible.

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Adding sound (not music) to CGoL

Post by pcallahan » July 5th, 2022, 3:59 pm

Here's a more manageable alternative for adding sound to Life. Instead of trying to create the waveform directly using simulated physics, just add sound for different events: birth, death, and survival. Then combine these waves using distance attenuation and speed of sound to generate the wave at a cell location.

You have more control over what you get, but there are fewer surprises. This video https://youtu.be/SOYB5RrIohQ illustrates the sound of a p46 PRNG oscillator as recorded near passing LWSSes, and a deletion reaction earlier in the stream. Both of these are readily audible, and the rest is the lower-volume background of periodic components. (Note:I used 25 fps where each frame is one generation, but this plays well at 2x and is somewhat more exciting to watch.)

Update: For a direct comparison, here is the R-pentomino with sound using this approach: https://youtu.be/cDm-4LeynXM The microphone is placed similarly to the second example using simulated physics.

Limitations: You won't really get a Doppler effect. There is no way to know that a cell that is part of a spaceship is actually moving (because it isn't!). While frequencies are fixed, with slower speeds of sound you might a get shift in the beat of changes. Unlike the physical simulation, cells are "invisible" and cannot change the oscillation around them. To compensate, I assign a lower frequency constant hum to survival events to indicate the cells' presence.

This Python script generates the sound within Golly. It requires SciPy for WAV file generation as well as fast sound mixing (explained below). After filling in some parameters from dialog boxes, you have to click on the recording location. I emphasize this, because I think it is easy to ignore the status line:

Code: Select all

import golly as g
import itertools
import math
import numpy
import scipy.io.wavfile
import scipy.signal

SAMPLING_RATE = 16000
MAX_LEVEL = 32000

def get_cells():
  celllist = g.getcells(g.getrect())
  # pair up even and odd cell coordinates in list
  return set(zip(celllist[::2], celllist[1::2]))
 
def distance(cell, microphone):
  # Add 1 so it is never 0 (imagine the microphone is held slightly above the cell position)
  dx = microphone[0] - cell[0]
  dy = microphone[1] - cell[1]
  return math.sqrt(dx * dx + dy * dy + 1)

def attenuation(cell, microphone):
  # This uses an inverse square law, though inverse makes more sense for 2D.
  #  It is a matter of preference how much you want close vs. distant events to contribute to audio.
  r = distance(cell, microphone)
  return 1.0 / r / r

def time_offset(cell, microphone, speed_of_sound):
  return distance(cell, microphone) / speed_of_sound

def sound_event(cell, microphone, generation, speed_of_sound, frame_rate):
  return (int(SAMPLING_RATE * (generation + time_offset(cell, microphone, speed_of_sound)) / frame_rate),
          attenuation(cell, microphone))

def event_vector(sound_events):
  values = [0] * (max(i for (i, v) in sound_events) + 1)
  for i, v in sound_events:
    values[i] += v
  return values

# Produce a sinusoidal wave that shifts from one frequency to another over time duration,
# adjusting volume according to a sine function from 0 to pi.
def chime(frequency_0, frequency_f, duration, volume):
  n_samples = duration * SAMPLING_RATE
  values = []
  for i in range(int(n_samples)):
    t = i / n_samples
    phase = (frequency_0 * t + 0.5 * (frequency_f - frequency_0) * t * t) * duration * 2 * math.pi
    values.append(math.sin(phase) * math.sin(t * math.pi) * volume)
  return values

n_generations = int(g.getstring('Number of generations to record', '100'))
frame_rate = float(g.getstring('Generations per second to record', '25'))
speed_of_sound = float(g.getstring('Speed of sound (distance per generation)', '2.0'))
output_file = g.getstring('Name of output file', '/tmp/life_sound.wav')

oldcursor = g.getcursor()
g.setcursor("Select")
g.show('Click cell location of microphone.')

while True:
  fields = g.getevent().split()
  if len(fields) > 0 and fields[0] == 'click':
    microphone = (int(fields[1]), int(fields[2])) 
    break
g.setcursor(oldcursor)

# get initial generation as a basis for subsequent events.
old_cells = get_cells()
g.run(1)

births = []
deaths = []
survivals = []
for i in range(n_generations):
  g.show('Recording generation %d' % i)
  cells = get_cells()

  for cell in cells:
    if cell not in old_cells:
      births.append(sound_event(cell, microphone, i, speed_of_sound, frame_rate))
  for cell in old_cells:
    if cell not in cells:
      deaths.append(sound_event(cell, microphone, i, speed_of_sound, frame_rate))
    else:
      survivals.append(sound_event(cell, microphone, i, speed_of_sound, frame_rate))

  old_cells = cells
  g.run(1)

g.show('making audio output')

birth_wave = scipy.signal.fftconvolve(event_vector(births), chime(880, 1318.51, 1.0 / frame_rate, 1.5))
death_wave = scipy.signal.fftconvolve(event_vector(deaths), chime(880, 440, 1.0 / frame_rate, 1.0))
survival_wave = scipy.signal.fftconvolve(event_vector(survivals), chime(220, 220, 3.0 / frame_rate, 0.25))

combined = []
for birth, death, survival in itertools.zip_longest(birth_wave, death_wave, survival_wave, fillvalue=0):
  combined.append(birth + death + survival)

data = numpy.array(combined)
median = numpy.median(data)
data = data - median
scale = max(numpy.max(data) - median, median - numpy.min(data)) 
scaled = numpy.int16(MAX_LEVEL * data/scale)
g.show('writing audio output')
scipy.io.wavfile.write(output_file, 16000, scaled)
g.show('')
I used some custom Java code to generate the synchronized video. It would be nice to do all this within Golly, and I suppose I could, but I'd need to bring up a sound player and run it with generations set to 25 per second (delay of 40ms).

The sound in the video uses default parameters except for the number of generations, which I increased to 3000 for a 2 minute video.

Note on sound mixing: I originally mixed waveforms by adding values to a float array directly. This got slow for long files and then it hit me that I was doing a convolution. SciPy has an implementation of FFT convolution that is very fast, and I use it for mixing each sound segment at the appropriate places in the audio timeline and with the required attenuation.

Potential enhancements We're not limited to considering birth, death, and survival as the only events. For example, We could look at short cell histories (e.g. 5 or 10 generations) and associate specific chimes with ones we care about (glider or spaceship for instance, or certain short periodicities). This would add to the artificial element but could enhance it both as entertainment and potentially a useful way to engage more senses in the understanding of a pattern.

Added: For completeness, here's the Java code I used for generating the video. This has the pattern hard-coded and outputs a series of 3000 images that can be combined with ffmpeg. I believe it's self-contained, but I haven't taken a close look at it.

Code: Select all

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class GameOfLife {

  static class PlotImage {
    final int width;
    final int height;
    final int scale;
    final BufferedImage image;
    final Graphics2D g;
    final int xOffset;
    final int yOffset;

    PlotImage(int width, int height, int scale) {
      this.width = width;
      this.height = height;
      this.scale = scale;
      image = new BufferedImage(width * scale, height * scale, BufferedImage.TYPE_INT_ARGB);
      g = image.createGraphics();
      xOffset = (image.getWidth() - scale) / 2;
      yOffset = (image.getHeight() - scale) / 2;
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      clear();
    }

    void clear() {
      g.setColor(Color.WHITE);
      g.fillRect(0, 0, image.getWidth(), image.getHeight());
    }

    void plot(Cell cell) {
      g.setColor(Color.BLUE);
      g.fillOval(xOffset + cell.x() * scale, yOffset + cell.y() * scale, scale, scale);
    }

    void mark(Cell cell) {
      g.setColor(Color.RED);
      g.drawRect(xOffset + cell.x() * scale - scale, yOffset + cell.y() * scale - scale, scale * 3, scale * 3);
    }

    void writeImage(String fileName) {
      try {
        ImageIO.write(image, "png", new File(fileName));
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }


  record Cell(int x, int y) {
  }

  record CellValue(Cell cell, int value) {
  }

  static Cell cell(int x, int y) {
    return new Cell(x, y);
  }

  private static final int[] LIFE_RULE = {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0};

  private static Stream<CellValue> neighborhood(Cell cell) {
    return IntStream.rangeClosed(-1, 1).
      mapToObj(dy -> IntStream.rangeClosed(-1, 1).
        mapToObj(dx -> new CellValue(cell(cell.x + dx, cell.y + dy), dx == 0 && dy == 0 ? 9 : 1))).
      flatMap(Function.identity());
  }

  static Set<Cell> generate(Set<Cell> cells) {
    return cells.stream().flatMap(GameOfLife::neighborhood).
      collect(Collectors.groupingBy(CellValue::cell, Collectors.summingInt(CellValue::value))).
      entrySet().stream().filter(entry -> LIFE_RULE[entry.getValue()] == 1).map(Map.Entry::getKey).
      collect(Collectors.toSet());
  }

  static Set<Cell> PRNG_46 = Set.of(
    cell(69, -37),
    cell(70, -37),
    cell(76, -37),
    cell(77, -37),
    cell(69, -36),
    cell(70, -36),
    cell(76, -36),
    cell(77, -36),
    cell(70, -28),
    cell(76, -28),
    cell(69, -27),
    cell(70, -27),
    cell(71, -27),
    cell(75, -27),
    cell(76, -27),
    cell(77, -27),
    cell(68, -26),
    cell(69, -26),
    cell(71, -26),
    cell(75, -26),
    cell(77, -26),
    cell(78, -26),
    cell(71, -23),
    cell(75, -23),
    cell(71, -22),
    cell(75, -22),
    cell(70, -17),
    cell(69, -16),
    cell(71, -16),
    cell(-44, -15),
    cell(68, -15),
    cell(69, -15),
    cell(71, -15),
    cell(72, -15),
    cell(-45, -14),
    cell(-44, -14),
    cell(-35, -14),
    cell(-34, -14),
    cell(68, -14),
    cell(72, -14),
    cell(-59, -13),
    cell(-58, -13),
    cell(-46, -13),
    cell(-45, -13),
    cell(-44, -13),
    cell(-42, -13),
    cell(-36, -13),
    cell(-33, -13),
    cell(67, -13),
    cell(68, -13),
    cell(69, -13),
    cell(71, -13),
    cell(72, -13),
    cell(73, -13),
    cell(-59, -12),
    cell(-58, -12),
    cell(-47, -12),
    cell(-46, -12),
    cell(-37, -12),
    cell(-36, -12),
    cell(-34, -12),
    cell(-33, -12),
    cell(-32, -12),
    cell(68, -12),
    cell(72, -12),
    cell(-46, -11),
    cell(-45, -11),
    cell(-36, -11),
    cell(-35, -11),
    cell(-33, -11),
    cell(68, -11),
    cell(72, -11),
    cell(-45, -10),
    cell(-34, -10),
    cell(69, -10),
    cell(71, -10),
    cell(76, -10),
    cell(77, -10),
    cell(70, -9),
    cell(76, -9),
    cell(77, -9),
    cell(-45, -8),
    cell(-46, -7),
    cell(-45, -7),
    cell(-59, -6),
    cell(-58, -6),
    cell(-47, -6),
    cell(-46, -6),
    cell(-32, -6),
    cell(-31, -6),
    cell(0, -6),
    cell(1, -6),
    cell(-59, -5),
    cell(-58, -5),
    cell(-46, -5),
    cell(-45, -5),
    cell(-44, -5),
    cell(-42, -5),
    cell(-32, -5),
    cell(-31, -5),
    cell(0, -5),
    cell(1, -5),
    cell(-76, -4),
    cell(-75, -4),
    cell(-45, -4),
    cell(-44, -4),
    cell(-76, -3),
    cell(-75, -3),
    cell(-44, -3),
    cell(-36, 0),
    cell(-33, 0),
    cell(-58, 1),
    cell(-57, 1),
    cell(-56, 1),
    cell(-55, 1),
    cell(-32, 1),
    cell(-12, 1),
    cell(-11, 1),
    cell(-10, 1),
    cell(-9, 1),
    cell(-59, 2),
    cell(-55, 2),
    cell(-36, 2),
    cell(-32, 2),
    cell(-13, 2),
    cell(-9, 2),
    cell(50, 2),
    cell(51, 2),
    cell(-55, 3),
    cell(-35, 3),
    cell(-34, 3),
    cell(-33, 3),
    cell(-32, 3),
    cell(-9, 3),
    cell(51, 3),
    cell(52, 3),
    cell(-59, 4),
    cell(-56, 4),
    cell(-13, 4),
    cell(-10, 4),
    cell(50, 4),
    cell(75, 4),
    cell(76, 4),
    cell(-76, 5),
    cell(-75, 5),
    cell(-74, 5),
    cell(-70, 5),
    cell(-69, 5),
    cell(-68, 5),
    cell(75, 5),
    cell(77, 5),
    cell(-77, 6),
    cell(-74, 6),
    cell(-70, 6),
    cell(-67, 6),
    cell(77, 6),
    cell(-77, 7),
    cell(-76, 7),
    cell(-74, 7),
    cell(-70, 7),
    cell(-68, 7),
    cell(-67, 7),
    cell(3, 7),
    cell(77, 7),
    cell(78, 7),
    cell(-20, 8),
    cell(-14, 8),
    cell(-8, 8),
    cell(-7, 8),
    cell(2, 8),
    cell(3, 8),
    cell(19, 8),
    cell(20, 8),
    cell(-56, 9),
    cell(-55, 9),
    cell(-35, 9),
    cell(-33, 9),
    cell(-32, 9),
    cell(-20, 9),
    cell(-14, 9),
    cell(-8, 9),
    cell(-7, 9),
    cell(1, 9),
    cell(2, 9),
    cell(19, 9),
    cell(20, 9),
    cell(-57, 10),
    cell(-56, 10),
    cell(-41, 10),
    cell(-40, 10),
    cell(-36, 10),
    cell(-33, 10),
    cell(-32, 10),
    cell(-29, 10),
    cell(-28, 10),
    cell(-27, 10),
    cell(2, 10),
    cell(3, 10),
    cell(6, 10),
    cell(7, 10),
    cell(-55, 11),
    cell(-41, 11),
    cell(-40, 11),
    cell(-36, 11),
    cell(-29, 11),
    cell(-28, 11),
    cell(-24, 11),
    cell(-23, 11),
    cell(-11, 11),
    cell(-10, 11),
    cell(-36, 12),
    cell(-35, 12),
    cell(-31, 12),
    cell(-30, 12),
    cell(-29, 12),
    cell(-34, 13),
    cell(-30, 13),
    cell(-20, 13),
    cell(-14, 13),
    cell(39, 13),
    cell(-20, 14),
    cell(-14, 14),
    cell(2, 14),
    cell(3, 14),
    cell(6, 14),
    cell(7, 14),
    cell(39, 14),
    cell(40, 14),
    cell(-34, 15),
    cell(-30, 15),
    cell(-8, 15),
    cell(-7, 15),
    cell(1, 15),
    cell(2, 15),
    cell(19, 15),
    cell(20, 15),
    cell(38, 15),
    cell(40, 15),
    cell(-36, 16),
    cell(-35, 16),
    cell(-31, 16),
    cell(-30, 16),
    cell(-29, 16),
    cell(-8, 16),
    cell(-7, 16),
    cell(2, 16),
    cell(3, 16),
    cell(19, 16),
    cell(20, 16),
    cell(-41, 17),
    cell(-40, 17),
    cell(-36, 17),
    cell(-29, 17),
    cell(-28, 17),
    cell(-14, 17),
    cell(-13, 17),
    cell(3, 17),
    cell(-41, 18),
    cell(-40, 18),
    cell(-36, 18),
    cell(-33, 18),
    cell(-32, 18),
    cell(-29, 18),
    cell(-28, 18),
    cell(-27, 18),
    cell(-14, 18),
    cell(-13, 18),
    cell(-35, 19),
    cell(-33, 19),
    cell(-32, 19),
    cell(-44, 20),
    cell(-45, 21),
    cell(-44, 21),
    cell(15, 21),
    cell(16, 21),
    cell(-45, 22),
    cell(-43, 22),
    cell(-25, 22),
    cell(14, 22),
    cell(-76, 23),
    cell(-75, 23),
    cell(-69, 23),
    cell(-68, 23),
    cell(-26, 23),
    cell(-25, 23),
    cell(1, 23),
    cell(2, 23),
    cell(13, 23),
    cell(16, 23),
    cell(17, 23),
    cell(-76, 24),
    cell(-75, 24),
    cell(-69, 24),
    cell(-68, 24),
    cell(-27, 24),
    cell(-26, 24),
    cell(-25, 24),
    cell(-23, 24),
    cell(-13, 24),
    cell(-12, 24),
    cell(1, 24),
    cell(2, 24),
    cell(13, 24),
    cell(16, 24),
    cell(35, 24),
    cell(-47, 25),
    cell(-46, 25),
    cell(-28, 25),
    cell(-27, 25),
    cell(-13, 25),
    cell(-12, 25),
    cell(13, 25),
    cell(15, 25),
    cell(27, 25),
    cell(28, 25),
    cell(33, 25),
    cell(36, 25),
    cell(-45, 26),
    cell(-27, 26),
    cell(-26, 26),
    cell(14, 26),
    cell(15, 26),
    cell(28, 26),
    cell(29, 26),
    cell(33, 26),
    cell(34, 26),
    cell(35, 26),
    cell(36, 26),
    cell(37, 26),
    cell(48, 26),
    cell(49, 26),
    cell(-60, 27),
    cell(-59, 27),
    cell(-48, 27),
    cell(-47, 27),
    cell(-44, 27),
    cell(-26, 27),
    cell(27, 27),
    cell(33, 27),
    cell(34, 27),
    cell(35, 27),
    cell(37, 27),
    cell(38, 27),
    cell(48, 27),
    cell(49, 27),
    cell(-60, 28),
    cell(-59, 28),
    cell(-47, 28),
    cell(-44, 28),
    cell(14, 28),
    cell(15, 28),
    cell(34, 28),
    cell(35, 28),
    cell(37, 28),
    cell(-46, 29),
    cell(-44, 29),
    cell(-26, 29),
    cell(13, 29),
    cell(15, 29),
    cell(35, 29),
    cell(36, 29),
    cell(-46, 30),
    cell(-45, 30),
    cell(-27, 30),
    cell(-26, 30),
    cell(1, 30),
    cell(2, 30),
    cell(13, 30),
    cell(16, 30),
    cell(-28, 31),
    cell(-27, 31),
    cell(-13, 31),
    cell(-12, 31),
    cell(1, 31),
    cell(2, 31),
    cell(13, 31),
    cell(16, 31),
    cell(17, 31),
    cell(35, 31),
    cell(36, 31),
    cell(-46, 32),
    cell(-45, 32),
    cell(-27, 32),
    cell(-26, 32),
    cell(-25, 32),
    cell(-23, 32),
    cell(-13, 32),
    cell(-12, 32),
    cell(14, 32),
    cell(34, 32),
    cell(35, 32),
    cell(37, 32),
    cell(-46, 33),
    cell(-44, 33),
    cell(-26, 33),
    cell(-25, 33),
    cell(15, 33),
    cell(16, 33),
    cell(33, 33),
    cell(34, 33),
    cell(35, 33),
    cell(37, 33),
    cell(38, 33),
    cell(48, 33),
    cell(49, 33),
    cell(-60, 34),
    cell(-59, 34),
    cell(-47, 34),
    cell(-44, 34),
    cell(-25, 34),
    cell(33, 34),
    cell(34, 34),
    cell(35, 34),
    cell(36, 34),
    cell(37, 34),
    cell(48, 34),
    cell(49, 34),
    cell(-60, 35),
    cell(-59, 35),
    cell(-48, 35),
    cell(-47, 35),
    cell(-44, 35),
    cell(33, 35),
    cell(36, 35),
    cell(-45, 36),
    cell(35, 36),
    cell(-47, 37),
    cell(-46, 37)
    );


  public static void main(String argv[]) {

    var cells = PRNG_46;
    var plot = new PlotImage(250, 120, 5);
    for (var i = 0; i < 3000; i++) {
      plot.clear();
      for (var cell : cells) {
        plot.plot(cell);
      }
      plot.mark(cell(9, -7));
      if (i % 10 == 0) {
        System.out.println("generation " + i);
      }
      plot.writeImage(
        "/tmp/" + String.format("%06d", i) + ".png");
      cells = generate(cells);
    }
  }
}

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Adding sound (not music) to CGoL

Post by pcallahan » July 27th, 2022, 11:02 am

I uploaded another wave-generation video: https://www.youtube.com/watch?v=bvaER-P1-HA As I explain in the description:
I started with Gosper's period-30 glider gun, running 6.25 generations per second for 920 generations, firing new gliders northeast. I use a simplified wave equation to make the ripples. The sound is generated from the waves, but raised 4 octaves in pitch to make it audible. There is damping outside a set radius to eliminate reflections. You can see the effect of gliders moving into the damped area at the end of the video.
The framerate is 25 fps, so the pattern is only updated every 4 frames, but the ripples update more frequently. I have given up trying to get audible sounds from this approach. Raising the pitch artificially doesn't seem like that much of a cheat. Note that the audio is sampled at each step of the wave calculation, which is 640 times per frame.

The damping is a new development I worked on yesterday. The boundary reflections were getting on my nerves, and what finally worked was to leave the waves undamped but begin damping them to an increasing degree outside a given radius. (On second thought, it may not work that well. I still see reflections.) The gliders eventually tunnel into this region, leaving glowing spots but no waves.

It's kind of cool (I think) but not musical at all. My favorite part is how the gliders eventually produce a flat wavefront perpendicular to the stream. You can see this in a still image, along with the gliders moving into the damped area:
wave003659.png
wave003659.png (239.33 KiB) Viewed 561 times
Update: It may help to add a picture of the generated wave (this is before any manipulation).
Screen Shot 2022-07-27 at 10.31.13 AM.png
Screen Shot 2022-07-27 at 10.31.13 AM.png (973 KiB) Viewed 538 times
I sampled the sound in the upper right quadrant somewhat to the left of the glider stream. At the beginning, the sound from the glider gun dominates but as the stream increases in size it changes to a more regular hum. Note that I'm continually rescaling the amplitudes when I do this, so this does not indicate the absolute volume. The area in the middle is presumably where there is more wave motion away from the sampling location than nearby.

Post Reply