feat: Microphone detection

This commit is contained in:
2025-12-31 23:16:04 +01:00
parent 47b487b0f1
commit ae07a848bf
14 changed files with 183 additions and 15 deletions

2
.idea/misc.xml generated
View File

@@ -8,7 +8,7 @@
</list> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_22" default="true" project-jdk-name="22" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View File

@@ -2,6 +2,7 @@ package cz.jzitnik;
import cz.jzitnik.game.setup.GameSetup; import cz.jzitnik.game.setup.GameSetup;
import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.ThreadManager;
import org.reflections.Reflections; import org.reflections.Reflections;
public class Game { public class Game {
@@ -11,7 +12,10 @@ public class Game {
Cli cli = dependencyManager.getDependencyOrThrow(Cli.class); Cli cli = dependencyManager.getDependencyOrThrow(Cli.class);
GameSetup gameSetup = dependencyManager.getDependencyOrThrow(GameSetup.class); GameSetup gameSetup = dependencyManager.getDependencyOrThrow(GameSetup.class);
ThreadManager threadManager = dependencyManager.getDependencyOrThrow(ThreadManager.class);
gameSetup.setup(); gameSetup.setup();
threadManager.startAll();
cli.run(); cli.run();
} }

View File

@@ -1,5 +1,7 @@
import cz.jzitnik.Game; package cz.jzitnik;
void main() { public class Main {
new Game().start(); public static void main(String[] args) {
new Game().start();
}
} }

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ThreadRegistry {
}

View File

@@ -1,22 +1,30 @@
package cz.jzitnik.events.handlers; package cz.jzitnik.events.handlers;
import cz.jzitnik.annotations.EventHandler; import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.ExitEvent; import cz.jzitnik.events.ExitEvent;
import cz.jzitnik.states.RunningState; import cz.jzitnik.states.RunningState;
import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.StateManager; import cz.jzitnik.utils.StateManager;
import cz.jzitnik.utils.ThreadManager;
import cz.jzitnik.utils.events.AbstractEventHandler; import cz.jzitnik.utils.events.AbstractEventHandler;
@EventHandler(ExitEvent.class) @EventHandler(ExitEvent.class)
public class ExitEventHandler extends AbstractEventHandler<ExitEvent> { public class ExitEventHandler extends AbstractEventHandler<ExitEvent> {
@InjectDependency
private ThreadManager threadManager;
@InjectState
private RunningState runningState;
public ExitEventHandler(DependencyManager dm) { public ExitEventHandler(DependencyManager dm) {
super(dm); super(dm);
} }
@Override @Override
public void handle(ExitEvent event) { public void handle(ExitEvent event) {
StateManager stateManager = dm.getDependencyOrThrow(StateManager.class); threadManager.shutdownAll();
RunningState runningState = stateManager.getOrThrow(RunningState.class);
runningState.setRunning(false); runningState.setRunning(false);
System.exit(0); // Pls don't blame me System.exit(0); // Pls don't blame me
} }

View File

@@ -121,11 +121,11 @@ public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
eventManager.emitEvent(new RerenderScreen(partsToRerender.toArray(RerenderScreen.ScreenPart[]::new))); eventManager.emitEvent(new RerenderScreen(partsToRerender.toArray(RerenderScreen.ScreenPart[]::new)));
} }
gameState.setTerminalTooSmall(false); renderState.setTerminalTooSmall(false);
} catch (ArrayIndexOutOfBoundsException e) { } catch (ArrayIndexOutOfBoundsException e) {
// Screen too small to fit the room // Screen too small to fit the room
eventManager.emitEvent(new TerminalTooSmallEvent()); eventManager.emitEvent(new TerminalTooSmallEvent());
gameState.setTerminalTooSmall(true); renderState.setTerminalTooSmall(true);
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import cz.jzitnik.events.MouseMoveEvent;
import cz.jzitnik.game.GameState; import cz.jzitnik.game.GameState;
import cz.jzitnik.game.objects.GameObject; import cz.jzitnik.game.objects.GameObject;
import cz.jzitnik.game.objects.Interactable; import cz.jzitnik.game.objects.Interactable;
import cz.jzitnik.states.RenderState;
import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.UIClickHandlerRepository; import cz.jzitnik.utils.UIClickHandlerRepository;
import cz.jzitnik.utils.events.AbstractEventHandler; import cz.jzitnik.utils.events.AbstractEventHandler;
@@ -30,6 +31,9 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
@InjectState @InjectState
private GameState gameState; private GameState gameState;
@InjectState
private RenderState renderState;
@Override @Override
public void handle(MouseAction event) { public void handle(MouseAction event) {
if (gameState.getScreen() != null) { if (gameState.getScreen() != null) {
@@ -37,7 +41,7 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
return; return;
} }
if (gameState.isTerminalTooSmall()) { if (renderState.isTerminalTooSmall()) {
return; return;
} }

View File

@@ -16,6 +16,7 @@ import cz.jzitnik.game.GameState;
import cz.jzitnik.game.Player; import cz.jzitnik.game.Player;
import cz.jzitnik.game.ResourceManager; import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.utils.RoomCords; import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.RenderState;
import cz.jzitnik.states.ScreenBuffer; import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState; import cz.jzitnik.states.TerminalState;
import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.DependencyManager;
@@ -54,9 +55,12 @@ public class PlayerMoveEventHandler extends AbstractEventHandler<PlayerMoveEvent
@InjectConfig @InjectConfig
private PlayerConfig playerConfig; private PlayerConfig playerConfig;
@InjectState
private RenderState renderState;
@Override @Override
public void handle(PlayerMoveEvent event) { public void handle(PlayerMoveEvent event) {
if (gameState.isTerminalTooSmall()) { if (renderState.isTerminalTooSmall()) {
return; return;
} }

View File

@@ -11,7 +11,7 @@ import lombok.Setter;
@RequiredArgsConstructor @RequiredArgsConstructor
@State @State
public class GameState { public class GameState {
private final DependencyManager dependencyManager; private final DependencyManager dependencyManager; // Maybe transient in the future
@Getter @Getter
@Setter @Setter
@@ -28,10 +28,6 @@ public class GameState {
@Getter @Getter
private Screen screen; private Screen screen;
@Getter
@Setter
private boolean terminalTooSmall = false;
public void setScreen(Screen screen) { public void setScreen(Screen screen) {
if (screen != null) { if (screen != null) {
dependencyManager.inject(screen); dependencyManager.inject(screen);

View File

@@ -9,4 +9,5 @@ import lombok.Setter;
@Setter @Setter
public class RenderState { public class RenderState {
private boolean isFirstRender = true; private boolean isFirstRender = true;
private boolean terminalTooSmall = false;
} }

View File

@@ -0,0 +1,10 @@
package cz.jzitnik.states;
import cz.jzitnik.annotations.State;
import lombok.Data;
@Data
@State
public class SoundState {
private double soundVolume;
}

View File

@@ -0,0 +1,75 @@
package cz.jzitnik.threads;
import cz.jzitnik.annotations.ThreadRegistry;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.states.SoundState;
import cz.jzitnik.utils.ShutdownableThread;
import javax.sound.sampled.*;
@ThreadRegistry
public class MicrophoneThread extends ShutdownableThread {
@InjectState
private SoundState soundState;
private volatile boolean running = true;
@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)) {
System.err.println("Line not supported: " + info);
return;
}
TargetDataLine line = null;
try {
line = (TargetDataLine) AudioSystem.getLine(info);
line.open(format);
line.start();
byte[] buffer = new byte[2048];
while (running && !Thread.currentThread().isInterrupted()) {
int bytesRead = line.read(buffer, 0, buffer.length);
if (bytesRead > 0) {
double volume = calculateRMS(buffer, bytesRead);
soundState.setSoundVolume(volume);
}
}
} catch (LineUnavailableException e) {
System.err.println("Microphone line unavailable: " + e.getMessage());
} finally {
if (line != null) {
line.stop();
line.close();
}
}
}
public void shutdown() {
this.running = false;
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

@@ -0,0 +1,6 @@
package cz.jzitnik.utils;
public abstract class ShutdownableThread extends Thread {
public abstract void run();
public abstract void shutdown();
}

View File

@@ -0,0 +1,47 @@
package cz.jzitnik.utils;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.ThreadRegistry;
import org.reflections.Reflections;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
@Dependency
public class ThreadManager {
private final HashSet<ShutdownableThread> threads = new HashSet<>();
private final DependencyManager dependencyManager;
public ThreadManager(Reflections reflections, DependencyManager dependencyManager) {
this.dependencyManager = dependencyManager;
var classes = reflections.getTypesAnnotatedWith(ThreadRegistry.class);
for (Class<?> clazz : classes) {
if (!ShutdownableThread.class.isAssignableFrom(clazz)) {
continue;
}
try {
var instance = (ShutdownableThread) clazz.getDeclaredConstructor().newInstance();
threads.add(instance);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
public void startAll() {
for (ShutdownableThread thread : threads) {
dependencyManager.inject(thread);
thread.start();
}
}
public void shutdownAll() {
for (ShutdownableThread thread : threads) {
thread.shutdown();
}
}
}