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
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.)
For a direct comparison, here is the R-pentomino with sound using this approach:
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
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:
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.
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.
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.
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);
}
}
}