refactor: Microphone rewrite and fix bug

This commit is contained in:
2026-01-01 17:57:09 +01:00
parent 3071651ab6
commit bf8ca30d6a
15 changed files with 93 additions and 83 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ target/
.idea/jarRepositories.xml .idea/jarRepositories.xml
.idea/compiler.xml .idea/compiler.xml
.idea/libraries/ .idea/libraries/
.idea/FuzzierSettings.xml
*.iws *.iws
*.iml *.iml
*.ipr *.ipr

19
pom.xml
View File

@@ -77,6 +77,14 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<repositories>
<repository>
<id>be.0110.repo-releases</id>
<name>0110.be repository</name>
<url>https://mvn.0110.be/releases</url>
</repository>
</repositories>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
@@ -153,5 +161,16 @@
<artifactId>ffmpeg-platform</artifactId> <artifactId>ffmpeg-platform</artifactId>
<version>6.1.1-1.5.10</version> <version>6.1.1-1.5.10</version>
</dependency> </dependency>
<dependency>
<groupId>be.tarsos.dsp</groupId>
<artifactId>core</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>be.tarsos.dsp</groupId>
<artifactId>jvm</artifactId>
<version>2.5</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -6,6 +6,6 @@ import lombok.Getter;
@Getter @Getter
@Config @Config
public class Debugging { public class Debugging {
private final boolean renderColliders = true; private final boolean renderColliders = false;
private final boolean renderPlayerCollider = true; private final boolean renderPlayerCollider = false;
} }

View File

@@ -0,0 +1,10 @@
package cz.jzitnik.config;
import cz.jzitnik.annotations.Config;
import lombok.Getter;
@Getter
@Config
public class MicrophoneConfig {
private final float volumeThreshold = 3f;
}

View File

@@ -5,7 +5,6 @@ import cz.jzitnik.annotations.injectors.InjectConfig;
import cz.jzitnik.annotations.injectors.InjectDependency; import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.config.Debugging; import cz.jzitnik.config.Debugging;
import cz.jzitnik.events.FullRoomDraw;
import cz.jzitnik.events.RerenderScreen; import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.game.GameRoom; import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.GameState; import cz.jzitnik.game.GameState;

View File

@@ -7,9 +7,6 @@ import java.util.*;
public class AStarAlg { public class AStarAlg {
// Boundaries matching your Player switch statement
// Player: if (x <= 30) return -> means 30 is the edge, valid area is > 30?
// User Update: "he can be on 30". So valid range is [30, 155]
private static final int MIN_X = 30; private static final int MIN_X = 30;
private static final int MAX_X = 155; private static final int MAX_X = 155;
private static final int MIN_Y = 10; private static final int MIN_Y = 10;
@@ -19,14 +16,12 @@ public class AStarAlg {
PriorityQueue<Node> openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f)); PriorityQueue<Node> openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f));
Set<String> closedSet = new HashSet<>(); Set<String> closedSet = new HashSet<>();
// We use Chebyshev distance for the heuristic (best for 8-way movement)
Node startNode = new Node(start.getX(), start.getY(), 0, getHeuristic(start, target), null); Node startNode = new Node(start.getX(), start.getY(), 0, getHeuristic(start, target), null);
openSet.add(startNode); openSet.add(startNode);
while (!openSet.isEmpty()) { while (!openSet.isEmpty()) {
Node current = openSet.poll(); Node current = openSet.poll();
// Reached target?
if (current.x == target.getX() && current.y == target.getY()) { if (current.x == target.getX() && current.y == target.getY()) {
return reconstructPath(current); return reconstructPath(current);
} }
@@ -39,13 +34,7 @@ public class AStarAlg {
String neighborKey = neighbor.x + "," + neighbor.y; String neighborKey = neighbor.x + "," + neighbor.y;
if (closedSet.contains(neighborKey)) continue; if (closedSet.contains(neighborKey)) continue;
// Check collisions and boundaries
if (!isValidPosition(neighbor.x, neighbor.y, colliders)) { if (!isValidPosition(neighbor.x, neighbor.y, colliders)) {
// EDGE CASE FIX:
// If the Player (target) is standing exactly on a wall/edge that the Mob considers invalid,
// A* will usually fail.
// We add a check: if this neighbor IS the target, we allow it.
// This lets the mob walk right up to the player's face even if they are hugging the wall.
if (neighbor.x != target.getX() || neighbor.y != target.getY()) { if (neighbor.x != target.getX() || neighbor.y != target.getY()) {
continue; continue;
} }
@@ -65,7 +54,6 @@ public class AStarAlg {
private static List<Node> getNeighbors(Node current, RoomCords target) { private static List<Node> getNeighbors(Node current, RoomCords target) {
List<Node> neighbors = new ArrayList<>(); List<Node> neighbors = new ArrayList<>();
// Added Diagonal Directions
int[][] directions = { int[][] directions = {
{0, 1}, {0, -1}, {1, 0}, {-1, 0}, // Up, Down, Right, Left {0, 1}, {0, -1}, {1, 0}, {-1, 0}, // Up, Down, Right, Left
{1, 1}, {1, -1}, {-1, 1}, {-1, -1} // Diagonals {1, 1}, {1, -1}, {-1, 1}, {-1, -1} // Diagonals
@@ -81,14 +69,10 @@ public class AStarAlg {
} }
private static boolean isValidPosition(int x, int y, List<GameRoomPart> colliders) { private static boolean isValidPosition(int x, int y, List<GameRoomPart> colliders) {
// Updated checks to be inclusive so 30 is valid.
// Valid X: 30 to 155
if (x < MIN_X || x > MAX_X) return false; if (x < MIN_X || x > MAX_X) return false;
// Valid Y: 10 to 110
if (y < MIN_Y || y > MAX_Y) return false; if (y < MIN_Y || y > MAX_Y) return false;
// Check Colliders
RoomCords temp = new RoomCords(x, y); RoomCords temp = new RoomCords(x, y);
for (GameRoomPart part : colliders) { for (GameRoomPart part : colliders) {
if (part.isWithin(temp)) { if (part.isWithin(temp)) {
@@ -109,7 +93,6 @@ public class AStarAlg {
return path; return path;
} }
// Changed to Chebyshev Distance for better diagonal estimation
private static int getHeuristic(RoomCords a, RoomCords b) { private static int getHeuristic(RoomCords a, RoomCords b) {
int dx = Math.abs(a.getX() - b.getX()); int dx = Math.abs(a.getX() - b.getX());
int dy = Math.abs(a.getY() - b.getY()); int dy = Math.abs(a.getY() - b.getY());

View File

@@ -4,12 +4,13 @@ import cz.jzitnik.annotations.injectors.InjectConfig;
import cz.jzitnik.annotations.injectors.InjectDependency; import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.config.Debugging; import cz.jzitnik.config.Debugging;
import cz.jzitnik.config.MicrophoneConfig;
import cz.jzitnik.game.GameState; import cz.jzitnik.game.GameState;
import cz.jzitnik.game.ResourceManager; import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.mobs.Mob; import cz.jzitnik.game.mobs.Mob;
import cz.jzitnik.game.utils.RoomCords; import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.MicrophoneState;
import cz.jzitnik.states.ScreenBuffer; import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.SoundState;
import cz.jzitnik.states.TerminalState; import cz.jzitnik.states.TerminalState;
import cz.jzitnik.utils.events.EventManager; import cz.jzitnik.utils.events.EventManager;
import cz.jzitnik.utils.roomtasks.RoomTask; import cz.jzitnik.utils.roomtasks.RoomTask;
@@ -46,11 +47,14 @@ public class BlindMobFollowingPlayerTask extends RoomTask {
private Debugging debugging; private Debugging debugging;
@InjectState @InjectState
private SoundState soundState; private MicrophoneState microphoneState;
@InjectConfig
private MicrophoneConfig microphoneConfig;
@Override @Override
public void run() { public void run() {
if (playerCords == null || (soundState.isMicrophoneSetup() && soundState.getSoundVolume() > 2f)) { if (playerCords == null || (microphoneState.isMicrophoneSetup() && microphoneState.getMicrophoneVolume() > microphoneConfig.getVolumeThreshold())) {
playerCords = gameState.getPlayer().getPlayerCords().clone(); playerCords = gameState.getPlayer().getPlayerCords().clone();
} }

View File

@@ -20,7 +20,7 @@ public class GameSetup {
private DependencyManager dependencyManager; private DependencyManager dependencyManager;
public void setup() { public void setup() {
//gameState.setScreen(new IntroScene(dependencyManager)); gameState.setScreen(new IntroScene(dependencyManager));
GameRoom mainRoom = new MainRoom(dependencyManager, resourceManager); GameRoom mainRoom = new MainRoom(dependencyManager, resourceManager);
GameRoom rightRoom = new GameRoom(ResourceManager.Resource.ROOM2); GameRoom rightRoom = new GameRoom(ResourceManager.Resource.ROOM2);

View File

@@ -2,6 +2,7 @@ package cz.jzitnik.game.setup.mobs;
import cz.jzitnik.game.ResourceManager; import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.mobs.HittableMob; import cz.jzitnik.game.mobs.HittableMob;
import cz.jzitnik.game.mobs.tasks.BlindMobFollowingPlayerTask;
import cz.jzitnik.game.mobs.tasks.MobFollowingPlayerTask; import cz.jzitnik.game.mobs.tasks.MobFollowingPlayerTask;
import cz.jzitnik.game.utils.RoomCords; import cz.jzitnik.game.utils.RoomCords;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -11,8 +12,8 @@ public class Zombie extends HittableMob {
public Zombie(ResourceManager resourceManager, RoomCords cords) { public Zombie(ResourceManager resourceManager, RoomCords cords) {
super(resourceManager.getResource(ResourceManager.Resource.PLAYER_FRONT), null, cords, 10); super(resourceManager.getResource(ResourceManager.Resource.PLAYER_FRONT), null, cords, 10);
//setTask(new BlindMobFollowingPlayerTask(this, 1, 100)); setTask(new BlindMobFollowingPlayerTask(this, 1, 100));
setTask(new MobFollowingPlayerTask(this, 1, 100)); //setTask(new MobFollowingPlayerTask(this, 1, 100));
} }
@Override @Override

View File

@@ -51,7 +51,7 @@ public abstract class Scene extends Screen {
if (!isRenderedAlready) { if (!isRenderedAlready) {
isRenderedAlready = true; isRenderedAlready = true;
render(); render();
} else if (currentPart != null && !onEndAction.getClass().equals(OnEndAction.Repeat.class)) { } else if (currentPart != null && onEndAction.getClass().equals(OnEndAction.Repeat.class)) {
currentPart.fullRender(); currentPart.fullRender();
} }
} }
@@ -77,7 +77,7 @@ public abstract class Scene extends Screen {
} else if (onEndAction.getClass().equals(OnEndAction.SwitchToScreen.class)) { } else if (onEndAction.getClass().equals(OnEndAction.SwitchToScreen.class)) {
OnEndAction.SwitchToScreen switchToScreen = (OnEndAction.SwitchToScreen) onEndAction; OnEndAction.SwitchToScreen switchToScreen = (OnEndAction.SwitchToScreen) onEndAction;
gameState.setScreen(switchToScreen.getScreen()); gameState.setScreen(switchToScreen.getScreen());
eventManager.emitEvent(new FullRoomDraw(true)); switchToScreen.getScreen().fullRender();
} }
} }

View File

@@ -2,7 +2,6 @@ package cz.jzitnik.sound;
import javax.sound.sampled.*; import javax.sound.sampled.*;
import javax.sound.sampled.*;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -13,7 +12,7 @@ public class SoundPlayer {
private final AtomicReference<SourceDataLine> currentLine = new AtomicReference<>(); private final AtomicReference<SourceDataLine> currentLine = new AtomicReference<>();
public void playSound(String filePath, int backendVolume, int masterVolume) { public void playSound(String filePath, int backendVolume, int masterVolume) {
long threadId = Thread.currentThread().getId(); long threadId = Thread.currentThread().threadId();
if (!filePath.endsWith(".ogg") || masterVolume == 0) { if (!filePath.endsWith(".ogg") || masterVolume == 0) {
return; return;

View File

@@ -5,7 +5,7 @@ import lombok.Data;
@Data @Data
@State @State
public class SoundState { public class MicrophoneState {
private double soundVolume; private double microphoneVolume;
private boolean microphoneSetup = false; private boolean microphoneSetup = false;
} }

View File

@@ -1,78 +1,73 @@
package cz.jzitnik.threads; package cz.jzitnik.threads;
import be.tarsos.dsp.AudioDispatcher;
import be.tarsos.dsp.AudioEvent;
import be.tarsos.dsp.AudioProcessor;
import be.tarsos.dsp.filters.HighPass;
import be.tarsos.dsp.io.jvm.AudioDispatcherFactory;
import cz.jzitnik.annotations.ThreadRegistry; import cz.jzitnik.annotations.ThreadRegistry;
import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.states.SoundState; import cz.jzitnik.states.MicrophoneState;
import cz.jzitnik.utils.ShutdownableThread; import cz.jzitnik.utils.ShutdownableThread;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.sound.sampled.*; import javax.sound.sampled.LineUnavailableException;
@Slf4j @Slf4j
@ThreadRegistry @ThreadRegistry
public class MicrophoneThread extends ShutdownableThread { public class MicrophoneThread extends ShutdownableThread {
@InjectState @InjectState
private SoundState soundState; private MicrophoneState microphoneState;
private volatile boolean running = true; private AudioDispatcher dispatcher;
@Override @Override
public void run() { public void run() {
AudioFormat format = new AudioFormat(44100, 16, 1, true, false);
DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
if (!AudioSystem.isLineSupported(info)) {
log.error("Line not supported: {}", info);
return;
}
TargetDataLine line = null;
try { try {
line = (TargetDataLine) AudioSystem.getLine(info); dispatcher = AudioDispatcherFactory.fromDefaultMicrophone(44100, 2048, 0);
line.open(format);
line.start();
byte[] buffer = new byte[2048]; dispatcher.addAudioProcessor(new HighPass(120, 44100));
while (running && !Thread.currentThread().isInterrupted()) { dispatcher.addAudioProcessor(new AudioProcessor() {
int bytesRead = line.read(buffer, 0, buffer.length); private static final double NOISE_GATE_THRESHOLD = 1.5;
if (bytesRead > 0) { @Override
double volume = calculateRMS(buffer, bytesRead); public boolean process(AudioEvent audioEvent) {
soundState.setSoundVolume(volume); double rms = audioEvent.getRMS();
soundState.setMicrophoneSetup(true);
double volume = rms * 100;
if (volume < NOISE_GATE_THRESHOLD) {
volume = 0;
}
microphoneState.setMicrophoneVolume(volume);
microphoneState.setMicrophoneSetup(true);
return true;
} }
}
@Override
public void processingFinished() {
log.info("Microphone processing stopped.");
}
});
dispatcher.run();
} catch (LineUnavailableException e) { } catch (LineUnavailableException e) {
log.error("Microphone line unavailable: {}", e.getMessage()); log.error("Microphone line unavailable: {}", e.getMessage());
} finally { } catch (Exception e) {
if (line != null) { log.error("Error in MicrophoneThread: {}", e.getMessage(), e);
line.stop();
line.close();
}
} }
} }
@Override
public void shutdown() { public void shutdown() {
this.running = false; if (dispatcher != null && !dispatcher.isStopped()) {
dispatcher.stop();
}
this.interrupt(); this.interrupt();
} }
private double calculateRMS(byte[] buffer, int bytesRead) {
long sum = 0;
for (int i = 0; i < bytesRead; i += 2) {
int low = buffer[i];
int high = buffer[i + 1];
int sample = (high << 8) | (low & 0xFF);
sum += (long) sample * sample;
}
double rms = Math.sqrt(sum / (bytesRead / 2.0));
return (rms / 32768.0) * 100;
}
} }

View File

@@ -1,7 +1,7 @@
package cz.jzitnik.utils; package cz.jzitnik.utils;
// Don't blame me that I'm using field injection instead of construction injection. I just like it more leave me alone. // Don't blame me that I'm using field injection instead of construction injection. I just like it more, leave me alone.
// Yes I know I'll suffer in the unit tests. (who said there will be any? hmmm) // Yes, I know I'll suffer in the unit tests. (who said there will be any? hmmm)
import com.google.common.collect.ClassToInstanceMap; import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.MutableClassToInstanceMap; import com.google.common.collect.MutableClassToInstanceMap;

View File

@@ -84,7 +84,6 @@ public class RerenderUtils {
} }
} }
// TODO: Remove duplicates
for (Mob object: currentRoom.getMobs()) { for (Mob object: currentRoom.getMobs()) {
RoomCords startObjectCords = object.getCords(); RoomCords startObjectCords = object.getCords();
BufferedImage texture = object.getTexture(); BufferedImage texture = object.getTexture();