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>
</option>
</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" />
</component>
</project>

View File

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

View File

@@ -1,5 +1,7 @@
import cz.jzitnik.Game;
package cz.jzitnik;
void main() {
new Game().start();
public class Main {
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;
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.states.RunningState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.StateManager;
import cz.jzitnik.utils.ThreadManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
@EventHandler(ExitEvent.class)
public class ExitEventHandler extends AbstractEventHandler<ExitEvent> {
@InjectDependency
private ThreadManager threadManager;
@InjectState
private RunningState runningState;
public ExitEventHandler(DependencyManager dm) {
super(dm);
}
@Override
public void handle(ExitEvent event) {
StateManager stateManager = dm.getDependencyOrThrow(StateManager.class);
RunningState runningState = stateManager.getOrThrow(RunningState.class);
threadManager.shutdownAll();
runningState.setRunning(false);
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)));
}
gameState.setTerminalTooSmall(false);
renderState.setTerminalTooSmall(false);
} catch (ArrayIndexOutOfBoundsException e) {
// Screen too small to fit the room
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.objects.GameObject;
import cz.jzitnik.game.objects.Interactable;
import cz.jzitnik.states.RenderState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.UIClickHandlerRepository;
import cz.jzitnik.utils.events.AbstractEventHandler;
@@ -30,6 +31,9 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
@InjectState
private GameState gameState;
@InjectState
private RenderState renderState;
@Override
public void handle(MouseAction event) {
if (gameState.getScreen() != null) {
@@ -37,7 +41,7 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
return;
}
if (gameState.isTerminalTooSmall()) {
if (renderState.isTerminalTooSmall()) {
return;
}

View File

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

View File

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

View File

@@ -9,4 +9,5 @@ import lombok.Setter;
@Setter
public class RenderState {
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();
}
}
}