aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/java/coffee
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2026-01-25 21:01:28 -0800
committerElizabeth Hunt <me@liz.coffee>2026-01-25 21:01:28 -0800
commit83cb0653209f3300e220e76d3f57a000ef4219a6 (patch)
tree5115f2f6c7cba0227253d832010536bec2e36c4d /core/src/main/java/coffee
parentde98c427504cde19c7d5ada5c0a238ca91147a4f (diff)
downloadthe-abstraction-engine-v2-83cb0653209f3300e220e76d3f57a000ef4219a6.tar.gz
the-abstraction-engine-v2-83cb0653209f3300e220e76d3f57a000ef4219a6.zip
Make life grid a torus and vastly simplify audio 'visualization'
Diffstat (limited to 'core/src/main/java/coffee')
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/AbstractionEngineGame.java5
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java7
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/life/EntropyAudioSystem.java150
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/life/LifeInput.java2
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/life/LifeSystem.java2
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java16
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java34
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/mainmenu/MainMenuAudio.java223
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/settings/SettingsInstruction.java13
-rw-r--r--core/src/main/java/coffee/liz/ecs/math/Mat2.java13
10 files changed, 204 insertions, 261 deletions
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/AbstractionEngineGame.java b/core/src/main/java/coffee/liz/abstractionengine/app/AbstractionEngineGame.java
index 9d07220..afb9c09 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/AbstractionEngineGame.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/AbstractionEngineGame.java
@@ -1,6 +1,7 @@
package coffee.liz.abstractionengine.app;
import coffee.liz.abstractionengine.app.screen.ScrollLogo;
+import coffee.liz.abstractionengine.app.settings.SettingsInstruction;
import coffee.liz.ecs.math.Vec2;
import coffee.liz.ecs.math.Vec2f;
import com.badlogic.gdx.Game;
@@ -15,11 +16,13 @@ import com.badlogic.gdx.utils.viewport.FitViewport;
public class AbstractionEngineGame extends Game {
public static final Vec2<Float> WORLD_SIZE = Vec2f.builder().x(200f).y(200f).build();
private static final String RENDERABLE_CHARS = "λ" + FreeTypeFontGenerator.DEFAULT_CHARS;
+ private static final String FONT = "fonts/JetBrainsMonoNerdFont-Regular.ttf";
public SpriteBatch batch;
public BitmapFont font;
public FitViewport viewport;
public ShapeRenderer shapeRenderer;
+ public SettingsInstruction settings;
/**
* Game initialization hook.
@@ -28,7 +31,7 @@ public class AbstractionEngineGame extends Game {
viewport = new FitViewport(WORLD_SIZE.getX(), WORLD_SIZE.getY());
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
- font = initFont(24, "fonts/JetBrainsMonoNerdFont-Regular.ttf");
+ font = initFont(24, FONT);
this.setScreen(new ScrollLogo(this));
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java b/core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java
index 43b83a5..8013735 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java
@@ -1,5 +1,6 @@
package coffee.liz.abstractionengine.app.actor;
+import coffee.liz.abstractionengine.app.AbstractionEngineGame;
import coffee.liz.abstractionengine.app.Theme;
import coffee.liz.abstractionengine.app.life.CellState;
import coffee.liz.abstractionengine.app.life.LifeInput;
@@ -19,7 +20,6 @@ import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.time.Duration;
-import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -31,7 +31,9 @@ public class LifeGridActor extends Actor {
private final ShapeRenderer shapeRenderer;
private final World<LifeInput> world;
private final Vec2<Integer> gridDimensions;
+ private final AbstractionEngineGame game;
private final Color cellColor = new Color();
+
@Setter
private Viewport viewport;
@Setter
@@ -49,9 +51,10 @@ public class LifeGridActor extends Actor {
if (viewport == null) {
return;
}
+
final Vec2<Float> cellSize = computeCellSize();
final Set<Vec2<Integer>> forcedAlive = computeForcedAlive(cellSize);
- world.update(new LifeInput(forcedAlive), Duration.ofMillis((int) (delta * 1000)));
+ world.update(new LifeInput(forcedAlive, game.settings), Duration.ofMillis((int) (delta * 1000)));
}
/**
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/life/EntropyAudioSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/life/EntropyAudioSystem.java
new file mode 100644
index 0000000..0bab310
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/life/EntropyAudioSystem.java
@@ -0,0 +1,150 @@
+package coffee.liz.abstractionengine.app.life;
+
+import coffee.liz.abstractionengine.app.utils.FunctionUtils;
+import coffee.liz.ecs.math.Vec2;
+import coffee.liz.ecs.math.Vec2i;
+import coffee.liz.ecs.model.Entity;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.audio.AudioDevice;
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.utils.Disposable;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.IntStream;
+
+public class EntropyAudioSystem implements System<LifeInput>, Disposable {
+ private static final int SAMPLE_RATE = 44100;
+ private static final int CHUNKS_X = 4;
+ private static final int CHUNKS_Y = 4;
+ private static final int NUM_CHUNKS = CHUNKS_X * CHUNKS_Y;
+ private static final Duration AUDIO_THREAD_TIMEOUT = Duration.ofMillis(100);
+
+ private static final int[] PENTATONIC_INTERVALS = {0, 3, 5, 7, 10};
+ private static final float BASE_FREQUENCY = 110f;
+ private static final float[] FREQUENCIES = new float[NUM_CHUNKS];
+ static {
+ for (int i = 0; i < NUM_CHUNKS; i++) {
+ final int octave = i / PENTATONIC_INTERVALS.length;
+ final int degree = i % PENTATONIC_INTERVALS.length;
+ final int semitones = octave * 12 + PENTATONIC_INTERVALS[degree];
+ FREQUENCIES[i] = BASE_FREQUENCY * (float) Math.pow(2.0, semitones / 12.0);
+ }
+ }
+
+ private static final float MIN_PULSE_RATE = 1.0f;
+ private static final float MAX_PULSE_RATE = 8.0f;
+ private static final float MASTER_VOLUME = 0.15f;
+ private static final float SMOOTHING_FACTOR = 0.0005f;
+
+ private final AudioDevice device = Gdx.audio.newAudioDevice(SAMPLE_RATE, true);
+ private final float[] targetEntropies = new float[NUM_CHUNKS];
+ private final float[] smoothedEntropies = new float[NUM_CHUNKS];
+ private final float[] phases = new float[NUM_CHUNKS];
+ private final float[] pulsePhases = new float[NUM_CHUNKS];
+
+ private Thread audioThread;
+ private volatile boolean running = true;
+
+ public void start() {
+ audioThread = new Thread(this::audioLoop, "EntropyAudio");
+ audioThread.setDaemon(true);
+ audioThread.start();
+ }
+
+ @Override
+ public Collection<Class<? extends System<LifeInput>>> getDependencies() {
+ return Set.of(LifeSystem.class);
+ }
+
+ @Override
+ public void update(final World<LifeInput> world, final LifeInput state, final Duration dt) {
+ final LifeSystem lifeSystem = world.getSystem(LifeSystem.class);
+ final Vec2<Integer> gridDimensions = lifeSystem.getDimensions();
+ final List<List<Set<Entity>>> rows = lifeSystem.getRows();
+
+ final Vec2<Integer> chunkDimensions = Vec2i.builder().x(gridDimensions.getX() / CHUNKS_X)
+ .y(gridDimensions.getY() / CHUNKS_Y).build();
+
+ for (int cy = 0; cy < CHUNKS_Y; cy++) {
+ for (int cx = 0; cx < CHUNKS_X; cx++) {
+ final int chunkIndex = cy * CHUNKS_X + cx;
+ final Vec2<Integer> chunk = Vec2i.builder().x(cx).y(cy).build();
+ final Vec2<Integer> bottomLeft = chunkDimensions.scale(chunk);
+ final Vec2<Integer> topRight = bottomLeft.plus(chunkDimensions);
+
+ final long alive = IntStream.range(bottomLeft.getY(), topRight.getY())
+ .mapToLong(y -> IntStream.range(bottomLeft.getX(), topRight.getX()).mapToLong(
+ x -> rows.get(y).get(x).stream().filter(e -> e.get(CellState.class).isAlive()).count())
+ .sum())
+ .sum();
+ targetEntropies[chunkIndex] = computeShannonEntropy(
+ alive / (float) (chunkDimensions.getY() * chunkDimensions.getX()));
+ }
+ }
+ }
+
+ private float computeShannonEntropy(final float p) {
+ if (p <= 0.0f || p >= 1.0f) {
+ return 0.0f;
+ }
+
+ final float entropy = -p * MathUtils.log2(p) - (1 - p) * MathUtils.log2(1 - p);
+ return MathUtils.clamp(entropy, 0f, 1f);
+ }
+
+ private void audioLoop() {
+ final int bufferSize = 2048;
+ final float[] buffer = new float[bufferSize];
+ final float dtPerSample = 1.0f / SAMPLE_RATE;
+
+ while (running) {
+ for (int i = 0; i < bufferSize; i++) {
+ float sample = 0.0f;
+
+ for (int c = 0; c < NUM_CHUNKS; c++) {
+ final float target = targetEntropies[c];
+ smoothedEntropies[c] += (target - smoothedEntropies[c]) * SMOOTHING_FACTOR;
+ final float entropy = smoothedEntropies[c];
+
+ if (entropy < 0.01f) {
+ continue;
+ }
+
+ final float frequency = FREQUENCIES[c];
+ final float pulseRate = MIN_PULSE_RATE + entropy * (MAX_PULSE_RATE - MIN_PULSE_RATE);
+ final float volume = entropy * MASTER_VOLUME;
+
+ phases[c] += frequency * dtPerSample;
+ if (phases[c] > 1.0f) {
+ phases[c] -= 1.0f;
+ }
+
+ pulsePhases[c] += pulseRate * dtPerSample;
+ if (pulsePhases[c] > 1.0f) {
+ pulsePhases[c] -= 1.0f;
+ }
+
+ final float wave = (float) Math.sin(phases[c] * 2 * Math.PI);
+ final float pulse = 0.5f + 0.5f * (float) Math.sin(pulsePhases[c] * 2 * Math.PI);
+ sample += wave * volume * pulse;
+ }
+
+ buffer[i] = Math.max(-1.0f, Math.min(1.0f, sample));
+ }
+
+ device.writeSamples(buffer, 0, bufferSize);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ running = false;
+ FunctionUtils.runUninterrupted(() -> audioThread.join(AUDIO_THREAD_TIMEOUT));
+ device.dispose();
+ }
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/life/LifeInput.java b/core/src/main/java/coffee/liz/abstractionengine/app/life/LifeInput.java
index 2213ecc..8edf08d 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/life/LifeInput.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/life/LifeInput.java
@@ -1,5 +1,6 @@
package coffee.liz.abstractionengine.app.life;
+import coffee.liz.abstractionengine.app.settings.SettingsInstruction;
import coffee.liz.ecs.math.Vec2;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@@ -10,4 +11,5 @@ import java.util.Set;
@Data
public class LifeInput {
private final Set<Vec2<Integer>> forceAliveCells;
+ private final SettingsInstruction gameInstructions;
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/life/LifeSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/life/LifeSystem.java
index 14c783f..aea42c9 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/life/LifeSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/life/LifeSystem.java
@@ -46,7 +46,7 @@ public class LifeSystem extends BaseGridIndexSystem<LifeInput> {
sinceUpdate = sinceUpdate.plus(dt);
if (BETWEEN_UPDATES.compareTo(sinceUpdate) <= 0) {
- Mat2.convolve(this.rows, CONVOLUTION_RADIUS, () -> 0, (entities, rel, prev) -> {
+ Mat2.convolveTorus(this.rows, CONVOLUTION_RADIUS, () -> 0, (entities, rel, prev) -> {
if (rel.equals(Vec2i.ZERO)) {
return prev;
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java
index c3fe2c8..fc6d2e6 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java
@@ -1,19 +1,29 @@
package coffee.liz.abstractionengine.app.screen;
+import coffee.liz.abstractionengine.AbstractionEngineGridWorld;
import coffee.liz.abstractionengine.app.AbstractionEngineGame;
+import coffee.liz.abstractionengine.grid.component.GridInputState;
+import coffee.liz.ecs.DAGWorld;
+import coffee.liz.ecs.math.Vec2;
+import coffee.liz.ecs.math.Vec2i;
+import coffee.liz.ecs.model.World;
import com.badlogic.gdx.Screen;
import lombok.RequiredArgsConstructor;
+import java.util.Map;
+
@RequiredArgsConstructor
public class GameScreen implements Screen {
private final AbstractionEngineGame game;
+ private static final Vec2<Integer> GAME_GRID_SIZE = Vec2i.builder().x(30).y(30).build();
+ private final World<GridInputState> gameWorld = new AbstractionEngineGridWorld(GAME_GRID_SIZE);
/**
* Screen lifecycle hook.
*/
@Override
public void show() {
-
+ // gameWorld.
}
/**
@@ -23,7 +33,7 @@ public class GameScreen implements Screen {
* time since last frame
*/
@Override
- public void render(float delta) {
+ public void render(final float delta) {
}
@@ -36,7 +46,7 @@ public class GameScreen implements Screen {
* new height in pixels
*/
@Override
- public void resize(int width, int height) {
+ public void resize(final int width, final int height) {
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java
index 06d0b03..45775ea 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java
@@ -4,7 +4,7 @@ import coffee.liz.abstractionengine.app.AbstractionEngineGame;
import coffee.liz.abstractionengine.app.Theme;
import coffee.liz.abstractionengine.app.actor.Button;
import coffee.liz.abstractionengine.app.actor.LifeGridActor;
-import coffee.liz.abstractionengine.app.screen.mainmenu.MainMenuAudio;
+import coffee.liz.abstractionengine.app.life.EntropyAudioSystem;
import coffee.liz.abstractionengine.app.life.CellState;
import coffee.liz.abstractionengine.app.life.LifeInput;
import coffee.liz.abstractionengine.app.life.LifeSystem;
@@ -32,17 +32,16 @@ public class MainMenu implements Screen {
private static final float BUTTON_WIDTH = 50f;
private static final float BUTTON_HEIGHT = 12f;
- private static final float BUTTON_SPACING = 4f;
private static final float PARALLAX_STRENGTH = 3f;
- private final World<LifeInput> world = new DAGWorld<>(Map.of(LifeSystem.class, new LifeSystem(GRID_DIMENSIONS)));
+ private final World<LifeInput> world = new DAGWorld<>(Map.of(LifeSystem.class, new LifeSystem(GRID_DIMENSIONS),
+ EntropyAudioSystem.class, new EntropyAudioSystem()));
private final AbstractionEngineGame game;
private final Set<Vec2<Integer>> initialAlive = createGliderPattern();
private Stage stage;
private LifeGridActor lifeGridActor;
- private MainMenuAudio audioSystem;
/**
* Screen lifecycle hook.
@@ -53,27 +52,17 @@ public class MainMenu implements Screen {
.add(initialAlive.contains(pos) ? CellState.LIVE : CellState.DEAD));
stage = new Stage(game.viewport, game.batch);
- lifeGridActor = new LifeGridActor(game.shapeRenderer, world, GRID_DIMENSIONS);
+ world.getSystem(EntropyAudioSystem.class).start();
+ lifeGridActor = new LifeGridActor(game.shapeRenderer, world, GRID_DIMENSIONS, game);
lifeGridActor.setViewport(game.viewport);
stage.addActor(lifeGridActor);
- createButtons();
-
- audioSystem = new MainMenuAudio(GRID_DIMENSIONS);
- audioSystem.start();
-
- Gdx.input.setInputProcessor(stage);
- }
-
- private void createButtons() {
- final float worldWidth = game.viewport.getWorldWidth();
- final float worldHeight = game.viewport.getWorldHeight();
- final float centerX = (worldWidth - BUTTON_WIDTH) / 2f;
- final float startY = worldHeight / 2f + BUTTON_HEIGHT;
-
- final Button playButton = createButton("play", centerX, startY);
+ final Button playButton = createButton("play", (game.viewport.getWorldWidth() - BUTTON_WIDTH) / 2f,
+ (game.viewport.getWorldHeight() - BUTTON_HEIGHT) / 2f);
playButton.setOnClick(() -> game.setScreen(new GameScreen(game)));
stage.addActor(playButton);
+
+ Gdx.input.setInputProcessor(stage);
}
private Button createButton(final String text, final float x, final float y) {
@@ -85,6 +74,7 @@ public class MainMenu implements Screen {
}
private static Set<Vec2<Integer>> createGliderPattern() {
+ // TODO: create multiple to showcase audio.
final int[][] glider = {{0, 1, 0}, {0, 1, 1}, {1, 0, 1},};
final int offsetX = 5;
final int offsetY = 40;
@@ -107,8 +97,6 @@ public class MainMenu implements Screen {
public void render(final float delta) {
game.viewport.apply();
- audioSystem.update(world.query(Set.of(GridPosition.class, CellState.class)));
-
ScreenUtils.clear(Theme.BG);
updateParallax();
stage.act(delta);
@@ -165,7 +153,7 @@ public class MainMenu implements Screen {
*/
@Override
public void dispose() {
- audioSystem.dispose();
+ world.getSystem(EntropyAudioSystem.class).dispose();
world.query(Set.of()).forEach(world::removeEntity);
stage.dispose();
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/mainmenu/MainMenuAudio.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/mainmenu/MainMenuAudio.java
deleted file mode 100644
index bb8beda..0000000
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/mainmenu/MainMenuAudio.java
+++ /dev/null
@@ -1,223 +0,0 @@
-package coffee.liz.abstractionengine.app.screen.mainmenu;
-
-import coffee.liz.abstractionengine.app.life.CellState;
-import coffee.liz.abstractionengine.app.utils.FunctionUtils;
-import coffee.liz.abstractionengine.grid.component.GridPosition;
-import coffee.liz.ecs.math.Vec2;
-import coffee.liz.ecs.model.Entity;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.audio.AudioDevice;
-import com.badlogic.gdx.utils.Disposable;
-import lombok.Getter;
-
-import java.time.Duration;
-import java.util.Collection;
-
-/**
- * Uses grid activity and centroid position to shape chord density and
- * positional tones.
- */
-public class MainMenuAudio implements Runnable, Disposable {
- private static final int SAMPLE_RATE = 44100;
- private static final int BUFFER_SIZE = 1024;
- private static final float TWO_PI = (float) (2.0 * Math.PI);
- private static final float ONE = 1.0f;
-
- private static final float BASE_FREQ = 130.81f;
- private static final float[] CHORD_RATIOS = {1.0f, 1.25f, 1.5f, 2.0f, 2.5f, 3.0f, 4.0f, 5.0f, 6.0f};
- private static final float[] CHORD_VOLUMES = {0.25f, 0.2f, 0.22f, 0.18f, 0.15f, 0.14f, 0.12f, 0.1f, 0.08f};
- private static final int CHORD_THRESHOLD_PADDING = 3;
- private static final float CHORD_GAIN_SCALAR = 2.5f;
- private static final float DETUNE_DEPTH = 0.003f;
- private static final float SHIMMER_DEPTH = 0.01f;
- private static final int SHIMMER_START_HARMONIC = 3;
-
- private static final float[] SCALE_RATIOS = {1.0f, 9f / 8f, 5f / 4f, 4f / 3f, 3f / 2f, 5f / 3f, 15f / 8f, 2.0f};
- private static final float POSITION_BASE_FREQ = 261.63f;
- private static final float POSITION_Y_FREQ_MULTIPLIER = 1.5f;
-
- private static final float LFO_FREQ = 0.05f;
- private static final float SHIMMER_FREQ = 0.3f;
- private static final float SMOOTHING = 0.997f;
- private static final float POSITION_SMOOTHING = 0.99f;
- private static final float MASTER_VOLUME = 0.25f;
- private static final float MIN_ACTIVITY_LEVEL = 0.15f;
- private static final float DEFAULT_ACTIVITY_LEVEL = 0.15f;
- private static final float MAX_ACTIVITY_FRACTION = 0.3f;
- private static final float MIN_CENTROID_ACTIVITY = 0.01f;
- private static final float MIN_POSITION_INTENSITY = 0.01f;
- private static final float POSITION_VOLUME_SCALE = 0.3f;
- private static final float POSITION_Y_GAIN = 0.7f;
- private static final float LFO_HALF_RANGE = 0.5f;
- private static final float MIDPOINT = 0.5f;
- private static final int AUDIO_THREAD_JOIN_TIMEOUT_MS = 100;
-
- private final AudioDevice device;
- private final Vec2<Integer> gridDimensions;
- private final float maxActivity;
- private final float[] buffer = new float[BUFFER_SIZE];
- private final float[] chordPhases = new float[CHORD_RATIOS.length];
-
- private float positionXPhase = 0.0f;
- private float positionYPhase = 0.0f;
- private float lfoPhase = 0.0f;
- private float shimmerPhase = 0.0f;
-
- private float smoothedActivity = DEFAULT_ACTIVITY_LEVEL;
- private float smoothedX = MIDPOINT;
- private float smoothedY = MIDPOINT;
- private float smoothedPositionIntensity = 0.0f;
-
- private volatile float activity = DEFAULT_ACTIVITY_LEVEL;
- private volatile float centroidX = MIDPOINT;
- private volatile float centroidY = MIDPOINT;
- private volatile float centroidWeight = 0.0f;
- private volatile boolean running = true;
-
- @Getter
- private Thread audioThread;
-
- /**
- * Creates a main menu audio controller.
- *
- * @param gridDimensions
- * grid size used for scaling activity
- */
- public MainMenuAudio(final Vec2<Integer> gridDimensions) {
- this.device = Gdx.audio.newAudioDevice(SAMPLE_RATE, true);
- this.gridDimensions = gridDimensions;
- this.maxActivity = gridDimensions.getX() * gridDimensions.getY() * MAX_ACTIVITY_FRACTION;
- }
-
- /**
- * Starts the background audio thread.
- */
- public void start() {
- audioThread = new Thread(this, "main-menu-audio");
- audioThread.setDaemon(true);
- audioThread.start();
- }
-
- /**
- * Updates the audio inputs from current entities.
- *
- * @param entities
- * grid entities to sample
- */
- public void update(final Collection<Entity> entities) {
- float totalActivity = 0.0f;
- float weightedX = 0.0f;
- float weightedY = 0.0f;
-
- for (final Entity entity : entities) {
- if (!entity.has(GridPosition.class) || !entity.has(CellState.class)) {
- continue;
- }
- final Vec2<Integer> pos = entity.get(GridPosition.class).getPosition();
- final float alive = entity.get(CellState.class).getAlivePercentage();
-
- totalActivity += alive;
- weightedX += pos.getX() * alive;
- weightedY += pos.getY() * alive;
- }
-
- activity = Math.min(ONE, totalActivity / maxActivity);
-
- if (totalActivity > MIN_CENTROID_ACTIVITY) {
- centroidX = (weightedX / totalActivity) / gridDimensions.getX();
- centroidY = (weightedY / totalActivity) / gridDimensions.getY();
- centroidWeight = Math.min(ONE, totalActivity / maxActivity);
- } else {
- centroidWeight = 0.0f;
- }
- }
-
- /**
- * Audio thread loop.
- */
- @Override
- public void run() {
- while (running) {
- smoothedActivity = smoothedActivity * SMOOTHING + activity * (1.0f - SMOOTHING);
- smoothedX = smoothedX * POSITION_SMOOTHING + centroidX * (1.0f - POSITION_SMOOTHING);
- smoothedY = smoothedY * POSITION_SMOOTHING + centroidY * (1.0f - POSITION_SMOOTHING);
- smoothedPositionIntensity = smoothedPositionIntensity * SMOOTHING + centroidWeight * (1.0f - SMOOTHING);
-
- final float level = Math.max(MIN_ACTIVITY_LEVEL, smoothedActivity);
-
- for (int i = 0; i < BUFFER_SIZE; i++) {
- buffer[i] = sampleChord(level) + samplePosition();
- buffer[i] *= MASTER_VOLUME;
- }
- device.writeSamples(buffer, 0, BUFFER_SIZE);
- }
- }
-
- private float sampleChord(final float level) {
- final float lfo = (float) Math.sin(lfoPhase * TWO_PI) * LFO_HALF_RANGE + MIDPOINT;
- lfoPhase += LFO_FREQ / SAMPLE_RATE;
- if (lfoPhase > ONE) {
- lfoPhase -= ONE;
- }
-
- final float shimmer = (float) Math.sin(shimmerPhase * TWO_PI) * LFO_HALF_RANGE + MIDPOINT;
- shimmerPhase += SHIMMER_FREQ / SAMPLE_RATE;
- if (shimmerPhase > ONE) {
- shimmerPhase -= ONE;
- }
-
- float sample = 0.0f;
- for (int t = 0; t < CHORD_RATIOS.length; t++) {
- final float toneThreshold = t / (float) (CHORD_RATIOS.length + CHORD_THRESHOLD_PADDING);
- final float toneGain = Math.min(ONE, Math.max(0, (level - toneThreshold) * CHORD_GAIN_SCALAR));
-
- final float detune = ONE + (lfo - MIDPOINT) * DETUNE_DEPTH;
- final float shimmerMod = t > SHIMMER_START_HARMONIC ? (ONE + (shimmer - MIDPOINT) * SHIMMER_DEPTH) : ONE;
-
- final float freq = BASE_FREQ * CHORD_RATIOS[t] * detune * shimmerMod;
- chordPhases[t] += freq / SAMPLE_RATE;
- if (chordPhases[t] > ONE) {
- chordPhases[t] -= ONE;
- }
-
- sample += (float) Math.sin(chordPhases[t] * TWO_PI) * CHORD_VOLUMES[t] * toneGain;
- }
-
- return sample;
- }
-
- private float samplePosition() {
- if (smoothedPositionIntensity < MIN_POSITION_INTENSITY) {
- return 0.0f;
- }
-
- final int xIndex = Math.min((int) (smoothedX * SCALE_RATIOS.length), SCALE_RATIOS.length - 1);
- final int yIndex = Math.min((int) (smoothedY * SCALE_RATIOS.length), SCALE_RATIOS.length - 1);
-
- final float xFreq = POSITION_BASE_FREQ * SCALE_RATIOS[xIndex];
- final float yFreq = POSITION_BASE_FREQ * POSITION_Y_FREQ_MULTIPLIER * SCALE_RATIOS[yIndex];
-
- positionXPhase += xFreq / SAMPLE_RATE;
- if (positionXPhase > ONE) {
- positionXPhase -= ONE;
- }
- positionYPhase += yFreq / SAMPLE_RATE;
- if (positionYPhase > ONE) {
- positionYPhase -= ONE;
- }
-
- final float positionVolume = smoothedPositionIntensity * POSITION_VOLUME_SCALE;
- return (float) Math.sin(positionXPhase * TWO_PI) * positionVolume
- + (float) Math.sin(positionYPhase * TWO_PI) * positionVolume * POSITION_Y_GAIN;
- }
-
- /**
- * Releases audio resources.
- */
- @Override
- public void dispose() {
- running = false;
- device.dispose();
- FunctionUtils.runUninterrupted(() -> getAudioThread().join(Duration.ofMillis(AUDIO_THREAD_JOIN_TIMEOUT_MS)));
- }
-}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/settings/SettingsInstruction.java b/core/src/main/java/coffee/liz/abstractionengine/app/settings/SettingsInstruction.java
new file mode 100644
index 0000000..baaee84
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/settings/SettingsInstruction.java
@@ -0,0 +1,13 @@
+package coffee.liz.abstractionengine.app.settings;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@AllArgsConstructor
+@Getter
+public class SettingsInstruction {
+ @Builder.Default
+ private boolean playMusic = true;
+}
diff --git a/core/src/main/java/coffee/liz/ecs/math/Mat2.java b/core/src/main/java/coffee/liz/ecs/math/Mat2.java
index 8be945c..9975227 100644
--- a/core/src/main/java/coffee/liz/ecs/math/Mat2.java
+++ b/core/src/main/java/coffee/liz/ecs/math/Mat2.java
@@ -31,7 +31,8 @@ public final class Mat2 {
}
/**
- * Convolves a {@link Convolver} across a matrix.
+ * Convolves a {@link Convolver} across a matrix reaching neighbors around the
+ * grid like a torus.
*
* @param mat
* is the row-indexed 2d matrix to convolve.
@@ -49,7 +50,7 @@ public final class Mat2 {
* @param <R>
* is the type of the resulting type of each convolution.
*/
- public static <T, R, U> List<List<U>> convolve(final List<List<T>> mat, final Vec2<Integer> axes,
+ public static <T, R, U> List<List<U>> convolveTorus(final List<List<T>> mat, final Vec2<Integer> axes,
final Supplier<R> init, final Convolver<T, R> convolver, final BiFunction<T, R, U> finalReduction) {
final List<List<R>> rows = new ArrayList<>();
for (int y = 0; y < mat.size(); y++) {
@@ -58,13 +59,9 @@ public final class Mat2 {
final T center = mat.get(y).get(x);
R result = init.get();
for (int dy = -axes.getY(); dy <= axes.getY(); dy++) {
- final int ry = y + dy;
- if (ry < 0 || ry >= mat.size())
- continue;
+ final int ry = Math.floorMod(y + dy, mat.size());
for (int dx = -axes.getX(); dx <= axes.getX(); dx++) {
- final int rx = x + dx;
- if (rx < 0 || rx >= mat.get(ry).size())
- continue;
+ final int rx = Math.floorMod(x + dx, mat.get(ry).size());
result = convolver.convolve(mat.get(ry).get(rx), Vec2i.builder().x(dx).y(dy).build(), result);
}
}