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