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/compiler.xml
.idea/libraries/
.idea/FuzzierSettings.xml
*.iws
*.iml
*.ipr

19
pom.xml
View File

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

View File

@@ -6,6 +6,6 @@ import lombok.Getter;
@Getter
@Config
public class Debugging {
private final boolean renderColliders = true;
private final boolean renderPlayerCollider = true;
private final boolean renderColliders = false;
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.InjectState;
import cz.jzitnik.config.Debugging;
import cz.jzitnik.events.FullRoomDraw;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.GameState;

View File

@@ -7,9 +7,6 @@ import java.util.*;
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 MAX_X = 155;
private static final int MIN_Y = 10;
@@ -19,14 +16,12 @@ public class AStarAlg {
PriorityQueue<Node> openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f));
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);
openSet.add(startNode);
while (!openSet.isEmpty()) {
Node current = openSet.poll();
// Reached target?
if (current.x == target.getX() && current.y == target.getY()) {
return reconstructPath(current);
}
@@ -39,13 +34,7 @@ public class AStarAlg {
String neighborKey = neighbor.x + "," + neighbor.y;
if (closedSet.contains(neighborKey)) continue;
// Check collisions and boundaries
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()) {
continue;
}
@@ -65,7 +54,6 @@ public class AStarAlg {
private static List<Node> getNeighbors(Node current, RoomCords target) {
List<Node> neighbors = new ArrayList<>();
// Added Diagonal Directions
int[][] directions = {
{0, 1}, {0, -1}, {1, 0}, {-1, 0}, // Up, Down, Right, Left
{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) {
// Updated checks to be inclusive so 30 is valid.
// Valid X: 30 to 155
if (x < MIN_X || x > MAX_X) return false;
// Valid Y: 10 to 110
if (y < MIN_Y || y > MAX_Y) return false;
// Check Colliders
RoomCords temp = new RoomCords(x, y);
for (GameRoomPart part : colliders) {
if (part.isWithin(temp)) {
@@ -109,7 +93,6 @@ public class AStarAlg {
return path;
}
// Changed to Chebyshev Distance for better diagonal estimation
private static int getHeuristic(RoomCords a, RoomCords b) {
int dx = Math.abs(a.getX() - b.getX());
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.InjectState;
import cz.jzitnik.config.Debugging;
import cz.jzitnik.config.MicrophoneConfig;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.mobs.Mob;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.MicrophoneState;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.SoundState;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.utils.events.EventManager;
import cz.jzitnik.utils.roomtasks.RoomTask;
@@ -46,11 +47,14 @@ public class BlindMobFollowingPlayerTask extends RoomTask {
private Debugging debugging;
@InjectState
private SoundState soundState;
private MicrophoneState microphoneState;
@InjectConfig
private MicrophoneConfig microphoneConfig;
@Override
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();
}

View File

@@ -20,7 +20,7 @@ public class GameSetup {
private DependencyManager dependencyManager;
public void setup() {
//gameState.setScreen(new IntroScene(dependencyManager));
gameState.setScreen(new IntroScene(dependencyManager));
GameRoom mainRoom = new MainRoom(dependencyManager, resourceManager);
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.mobs.HittableMob;
import cz.jzitnik.game.mobs.tasks.BlindMobFollowingPlayerTask;
import cz.jzitnik.game.mobs.tasks.MobFollowingPlayerTask;
import cz.jzitnik.game.utils.RoomCords;
import lombok.extern.slf4j.Slf4j;
@@ -11,8 +12,8 @@ public class Zombie extends HittableMob {
public Zombie(ResourceManager resourceManager, RoomCords cords) {
super(resourceManager.getResource(ResourceManager.Resource.PLAYER_FRONT), null, cords, 10);
//setTask(new BlindMobFollowingPlayerTask(this, 1, 100));
setTask(new MobFollowingPlayerTask(this, 1, 100));
setTask(new BlindMobFollowingPlayerTask(this, 1, 100));
//setTask(new MobFollowingPlayerTask(this, 1, 100));
}
@Override

View File

@@ -51,7 +51,7 @@ public abstract class Scene extends Screen {
if (!isRenderedAlready) {
isRenderedAlready = true;
render();
} else if (currentPart != null && !onEndAction.getClass().equals(OnEndAction.Repeat.class)) {
} else if (currentPart != null && onEndAction.getClass().equals(OnEndAction.Repeat.class)) {
currentPart.fullRender();
}
}
@@ -77,7 +77,7 @@ public abstract class Scene extends Screen {
} else if (onEndAction.getClass().equals(OnEndAction.SwitchToScreen.class)) {
OnEndAction.SwitchToScreen switchToScreen = (OnEndAction.SwitchToScreen) onEndAction;
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 java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
@@ -13,7 +12,7 @@ public class SoundPlayer {
private final AtomicReference<SourceDataLine> currentLine = new AtomicReference<>();
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) {
return;

View File

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

View File

@@ -1,78 +1,73 @@
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.injectors.InjectState;
import cz.jzitnik.states.SoundState;
import cz.jzitnik.states.MicrophoneState;
import cz.jzitnik.utils.ShutdownableThread;
import lombok.extern.slf4j.Slf4j;
import javax.sound.sampled.*;
import javax.sound.sampled.LineUnavailableException;
@Slf4j
@ThreadRegistry
public class MicrophoneThread extends ShutdownableThread {
@InjectState
private SoundState soundState;
private MicrophoneState microphoneState;
private volatile boolean running = true;
private AudioDispatcher dispatcher;
@Override
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 {
line = (TargetDataLine) AudioSystem.getLine(info);
line.open(format);
line.start();
dispatcher = AudioDispatcherFactory.fromDefaultMicrophone(44100, 2048, 0);
byte[] buffer = new byte[2048];
dispatcher.addAudioProcessor(new HighPass(120, 44100));
while (running && !Thread.currentThread().isInterrupted()) {
int bytesRead = line.read(buffer, 0, buffer.length);
dispatcher.addAudioProcessor(new AudioProcessor() {
private static final double NOISE_GATE_THRESHOLD = 1.5;
if (bytesRead > 0) {
double volume = calculateRMS(buffer, bytesRead);
soundState.setSoundVolume(volume);
soundState.setMicrophoneSetup(true);
@Override
public boolean process(AudioEvent audioEvent) {
double rms = audioEvent.getRMS();
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) {
log.error("Microphone line unavailable: {}", e.getMessage());
} finally {
if (line != null) {
line.stop();
line.close();
}
} catch (Exception e) {
log.error("Error in MicrophoneThread: {}", e.getMessage(), e);
}
}
@Override
public void shutdown() {
this.running = false;
if (dispatcher != null && !dispatcher.isStopped()) {
dispatcher.stop();
}
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;
// 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)
// 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)
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.MutableClassToInstanceMap;

View File

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