feat: Scene API
This commit is contained in:
12
pom.xml
12
pom.xml
@@ -139,5 +139,17 @@
|
||||
<artifactId>lanterna</artifactId>
|
||||
<version>3.1.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>javacv-platform</artifactId>
|
||||
<version>1.5.10</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>ffmpeg-platform</artifactId>
|
||||
<version>6.1.1-1.5.10</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,6 +1,6 @@
|
||||
package cz.jzitnik;
|
||||
|
||||
import cz.jzitnik.game.GameSetup;
|
||||
import cz.jzitnik.game.setup.GameSetup;
|
||||
import cz.jzitnik.utils.DependencyManager;
|
||||
import org.reflections.Reflections;
|
||||
|
||||
|
||||
11
src/main/java/cz/jzitnik/events/PlayVideo.java
Normal file
11
src/main/java/cz/jzitnik/events/PlayVideo.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package cz.jzitnik.events;
|
||||
|
||||
import cz.jzitnik.utils.events.Event;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class PlayVideo implements Event {
|
||||
private String fileName;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package cz.jzitnik.events.handlers;
|
||||
|
||||
import com.googlecode.lanterna.TextColor;
|
||||
import com.googlecode.lanterna.screen.Screen;
|
||||
import com.googlecode.lanterna.screen.TerminalScreen;
|
||||
import cz.jzitnik.annotations.EventHandler;
|
||||
import cz.jzitnik.annotations.injectors.InjectDependency;
|
||||
import cz.jzitnik.annotations.injectors.InjectState;
|
||||
import cz.jzitnik.events.PlayVideo;
|
||||
import cz.jzitnik.game.ResourceManager;
|
||||
import cz.jzitnik.states.TerminalState;
|
||||
import cz.jzitnik.utils.DependencyManager;
|
||||
import cz.jzitnik.utils.events.AbstractEventHandler;
|
||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||
import org.bytedeco.javacv.Frame;
|
||||
import org.bytedeco.javacv.Java2DFrameConverter;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
@EventHandler(PlayVideo.class)
|
||||
public class PlayVideoHandler extends AbstractEventHandler<PlayVideo> {
|
||||
public PlayVideoHandler(DependencyManager dm) {
|
||||
super(dm);
|
||||
}
|
||||
|
||||
@InjectDependency
|
||||
private ResourceManager resourceManager;
|
||||
|
||||
@InjectState
|
||||
private TerminalState terminalState;
|
||||
|
||||
@Override
|
||||
public void handle(PlayVideo event) {
|
||||
}
|
||||
|
||||
|
||||
private static TextColor.RGB toColor(int rgb) {
|
||||
int r = (rgb >> 16) & 0xFF;
|
||||
int g = (rgb >> 8) & 0xFF;
|
||||
int b = rgb & 0xFF;
|
||||
return new TextColor.RGB(r, g, b);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import cz.jzitnik.annotations.EventHandler;
|
||||
import cz.jzitnik.annotations.injectors.InjectDependency;
|
||||
import cz.jzitnik.annotations.injectors.InjectState;
|
||||
import cz.jzitnik.events.FullRoomDraw;
|
||||
import cz.jzitnik.events.PlayVideo;
|
||||
import cz.jzitnik.events.TerminalResizeEvent;
|
||||
import cz.jzitnik.game.GameState;
|
||||
import cz.jzitnik.states.ScreenBuffer;
|
||||
|
||||
@@ -3,16 +3,34 @@ package cz.jzitnik.game;
|
||||
import cz.jzitnik.annotations.State;
|
||||
import cz.jzitnik.game.objects.Interactable;
|
||||
import cz.jzitnik.screens.Screen;
|
||||
import cz.jzitnik.utils.DependencyManager;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@State
|
||||
@Getter
|
||||
@Setter
|
||||
public class GameState {
|
||||
private final DependencyManager dependencyManager;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private GameRoom currentRoom;
|
||||
@Getter
|
||||
@Setter
|
||||
private Player player;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private Interactable interacting;
|
||||
|
||||
@Getter
|
||||
private Screen screen;
|
||||
|
||||
public void setScreen(Screen screen) {
|
||||
if (screen != null) {
|
||||
dependencyManager.inject(screen);
|
||||
}
|
||||
this.screen = screen;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,8 @@ public class ResourceManager {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream getResourceAsStream(String path) {
|
||||
return classLoader.getResourceAsStream(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package cz.jzitnik.game;
|
||||
package cz.jzitnik.game.setup;
|
||||
|
||||
import cz.jzitnik.annotations.Dependency;
|
||||
import cz.jzitnik.annotations.injectors.InjectDependency;
|
||||
import cz.jzitnik.annotations.injectors.InjectState;
|
||||
import cz.jzitnik.game.GameRoom;
|
||||
import cz.jzitnik.game.GameState;
|
||||
import cz.jzitnik.game.Player;
|
||||
import cz.jzitnik.game.ResourceManager;
|
||||
import cz.jzitnik.game.items.GameItem;
|
||||
import cz.jzitnik.game.items.WoodenSword;
|
||||
import cz.jzitnik.game.objects.Chest;
|
||||
import cz.jzitnik.game.utils.RoomCords;
|
||||
import cz.jzitnik.screens.scenes.IntroScene;
|
||||
import cz.jzitnik.utils.DependencyManager;
|
||||
|
||||
@Dependency
|
||||
@@ -21,6 +26,8 @@ public class GameSetup {
|
||||
private DependencyManager dependencyManager;
|
||||
|
||||
public void setup() {
|
||||
gameState.setScreen(new IntroScene(dependencyManager));
|
||||
|
||||
GameRoom mainRoom = new GameRoom(ResourceManager.Resource.ROOM1);
|
||||
GameRoom rightRoom = new GameRoom(ResourceManager.Resource.ROOM2);
|
||||
GameRoom topRightRoom = new GameRoom(ResourceManager.Resource.ROOM3);
|
||||
152
src/main/java/cz/jzitnik/screens/VideoPlayScreen.java
Normal file
152
src/main/java/cz/jzitnik/screens/VideoPlayScreen.java
Normal file
@@ -0,0 +1,152 @@
|
||||
package cz.jzitnik.screens;
|
||||
|
||||
import com.googlecode.lanterna.TextColor;
|
||||
import com.googlecode.lanterna.screen.TerminalScreen;
|
||||
import cz.jzitnik.annotations.injectors.InjectDependency;
|
||||
import cz.jzitnik.annotations.injectors.InjectState;
|
||||
import cz.jzitnik.game.ResourceManager;
|
||||
import cz.jzitnik.states.TerminalState;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||
import org.bytedeco.javacv.Frame;
|
||||
import org.bytedeco.javacv.Java2DFrameConverter;
|
||||
import com.googlecode.lanterna.screen.Screen.RefreshType;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
@Slf4j
|
||||
public abstract class VideoPlayScreen extends Screen {
|
||||
@InjectDependency
|
||||
private ResourceManager resourceManager;
|
||||
|
||||
@InjectState
|
||||
private TerminalState terminalState;
|
||||
|
||||
private final String videoPath;
|
||||
protected boolean isRenderedAlready;
|
||||
|
||||
public VideoPlayScreen(String videoPath) {
|
||||
this.videoPath = videoPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fullRender() {
|
||||
if (!isRenderedAlready) {
|
||||
isRenderedAlready = true;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
protected void render() {
|
||||
File tempVideo = null;
|
||||
try (InputStream resource = resourceManager.getResourceAsStream(videoPath)) {
|
||||
|
||||
TerminalScreen screen = terminalState.getTerminalScreen();
|
||||
var tg = terminalState.getTextGraphics();
|
||||
|
||||
tempVideo = File.createTempFile("lanterna-video", ".mp4");
|
||||
tempVideo.deleteOnExit();
|
||||
Files.copy(resource, tempVideo.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(tempVideo);
|
||||
Java2DFrameConverter converter = new Java2DFrameConverter()) {
|
||||
|
||||
grabber.start();
|
||||
|
||||
double fps = grabber.getFrameRate();
|
||||
long frameDelayNs = (long) (1_000_000_000 / fps);
|
||||
|
||||
long videoStartTime = System.nanoTime();
|
||||
long frameIndex = 0;
|
||||
|
||||
// Track previous terminal size
|
||||
int prevTermWidth = screen.getTerminalSize().getColumns();
|
||||
int prevTermHeight = screen.getTerminalSize().getRows();
|
||||
|
||||
Frame frame;
|
||||
while ((frame = grabber.grabImage()) != null) {
|
||||
|
||||
long now = System.nanoTime();
|
||||
long expectedTime = videoStartTime + frameIndex * frameDelayNs;
|
||||
|
||||
// Skip frames if we're behind schedule
|
||||
while (now > expectedTime) {
|
||||
frame = grabber.grabImage();
|
||||
if (frame == null) break;
|
||||
frameIndex++;
|
||||
expectedTime = videoStartTime + frameIndex * frameDelayNs;
|
||||
now = System.nanoTime();
|
||||
}
|
||||
|
||||
if (frame == null) break;
|
||||
|
||||
BufferedImage img = converter.convert(frame);
|
||||
if (img == null) continue;
|
||||
|
||||
int termWidth = screen.getTerminalSize().getColumns();
|
||||
int termHeight = screen.getTerminalSize().getRows();
|
||||
int targetWidth = termWidth;
|
||||
int targetHeight = termHeight * 2;
|
||||
|
||||
BufferedImage scaledImg = img;
|
||||
if (img.getWidth() != targetWidth || img.getHeight() != targetHeight) {
|
||||
scaledImg = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
|
||||
scaledImg.getGraphics().drawImage(img, 0, 0, targetWidth, targetHeight, null);
|
||||
}
|
||||
|
||||
for (int y = 0; y < termHeight; y++) {
|
||||
for (int x = 0; x < termWidth; x++) {
|
||||
int topPixel = scaledImg.getRGB(x, y * 2);
|
||||
int bottomPixel = scaledImg.getRGB(x, y * 2 + 1);
|
||||
|
||||
TextColor.RGB bg = toColor(topPixel);
|
||||
TextColor.RGB fg = toColor(bottomPixel);
|
||||
|
||||
tg.setBackgroundColor(bg);
|
||||
tg.setForegroundColor(fg);
|
||||
tg.setCharacter(x, y, '▄');
|
||||
}
|
||||
}
|
||||
|
||||
RefreshType refreshType;
|
||||
if (termWidth != prevTermWidth || termHeight != prevTermHeight) {
|
||||
refreshType = RefreshType.COMPLETE;
|
||||
prevTermWidth = termWidth;
|
||||
prevTermHeight = termHeight;
|
||||
} else {
|
||||
refreshType = RefreshType.DELTA;
|
||||
}
|
||||
|
||||
screen.refresh(refreshType);
|
||||
|
||||
frameIndex++;
|
||||
long nextFrameTime = videoStartTime + frameIndex * frameDelayNs;
|
||||
long sleepTime = nextFrameTime - System.nanoTime();
|
||||
if (sleepTime > 0) {
|
||||
Thread.sleep(sleepTime / 1_000_000, (int) (sleepTime % 1_000_000));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to play video", e);
|
||||
} finally {
|
||||
if (tempVideo != null && tempVideo.exists()) {
|
||||
if (!tempVideo.delete()) {
|
||||
System.err.println("Warning: failed to delete temp video file " + tempVideo.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TextColor.RGB toColor(int rgb) {
|
||||
int r = (rgb >> 16) & 0xFF;
|
||||
int g = (rgb >> 8) & 0xFF;
|
||||
int b = rgb & 0xFF;
|
||||
return new TextColor.RGB(r, g, b);
|
||||
}
|
||||
}
|
||||
19
src/main/java/cz/jzitnik/screens/scenes/BasicVideoScene.java
Normal file
19
src/main/java/cz/jzitnik/screens/scenes/BasicVideoScene.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package cz.jzitnik.screens.scenes;
|
||||
|
||||
import cz.jzitnik.events.KeyboardPressEvent;
|
||||
import cz.jzitnik.events.MouseAction;
|
||||
import cz.jzitnik.screens.VideoPlayScreen;
|
||||
|
||||
public final class BasicVideoScene extends VideoPlayScreen {
|
||||
public BasicVideoScene(String videoPath) {
|
||||
super(videoPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMouseAction(MouseAction event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleKeyboardAction(KeyboardPressEvent event) {
|
||||
}
|
||||
}
|
||||
16
src/main/java/cz/jzitnik/screens/scenes/IntroScene.java
Normal file
16
src/main/java/cz/jzitnik/screens/scenes/IntroScene.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package cz.jzitnik.screens.scenes;
|
||||
|
||||
import cz.jzitnik.screens.Screen;
|
||||
import cz.jzitnik.utils.DependencyManager;
|
||||
|
||||
public class IntroScene extends Scene {
|
||||
public IntroScene(DependencyManager dependencyManager) {
|
||||
super(
|
||||
new Screen[]{
|
||||
new VideoSceneWithAudio("video.mp4", "audio.ogg")
|
||||
},
|
||||
OnEndAction.SWITCH_TO_GAME
|
||||
);
|
||||
dependencyManager.inject(this);
|
||||
}
|
||||
}
|
||||
76
src/main/java/cz/jzitnik/screens/scenes/Scene.java
Normal file
76
src/main/java/cz/jzitnik/screens/scenes/Scene.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package cz.jzitnik.screens.scenes;
|
||||
|
||||
import cz.jzitnik.annotations.injectors.InjectDependency;
|
||||
import cz.jzitnik.annotations.injectors.InjectState;
|
||||
import cz.jzitnik.events.FullRoomDraw;
|
||||
import cz.jzitnik.events.KeyboardPressEvent;
|
||||
import cz.jzitnik.events.MouseAction;
|
||||
import cz.jzitnik.game.GameState;
|
||||
import cz.jzitnik.screens.Screen;
|
||||
import cz.jzitnik.utils.DependencyManager;
|
||||
import cz.jzitnik.utils.events.EventManager;
|
||||
|
||||
public abstract class Scene extends Screen {
|
||||
private final Screen[] parts;
|
||||
private Screen currentPart;
|
||||
private int currentIndex = 0;
|
||||
private boolean isRenderedAlready = false;
|
||||
private final OnEndAction onEndAction;
|
||||
|
||||
@InjectDependency
|
||||
private DependencyManager dependencyManager;
|
||||
|
||||
@InjectState
|
||||
private GameState gameState;
|
||||
|
||||
@InjectDependency
|
||||
private EventManager eventManager;
|
||||
|
||||
public enum OnEndAction {
|
||||
SWITCH_TO_GAME
|
||||
}
|
||||
|
||||
public Scene(Screen[] parts, OnEndAction onEndAction) {
|
||||
this.parts = parts;
|
||||
this.currentPart = parts[0];
|
||||
this.onEndAction = onEndAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fullRender() {
|
||||
if (!isRenderedAlready) {
|
||||
isRenderedAlready = true;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
private void render() {
|
||||
while (currentPart != null) {
|
||||
dependencyManager.inject(currentPart);
|
||||
currentPart.fullRender();
|
||||
|
||||
try {
|
||||
currentPart = parts[++currentIndex];
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
currentPart = null;
|
||||
}
|
||||
}
|
||||
|
||||
switch (onEndAction) {
|
||||
case SWITCH_TO_GAME -> {
|
||||
gameState.setScreen(null);
|
||||
eventManager.emitEvent(new FullRoomDraw(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMouseAction(MouseAction event) {
|
||||
currentPart.handleMouseAction(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleKeyboardAction(KeyboardPressEvent event) {
|
||||
currentPart.handleKeyboardAction(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cz.jzitnik.screens.scenes;
|
||||
|
||||
import cz.jzitnik.events.KeyboardPressEvent;
|
||||
import cz.jzitnik.events.MouseAction;
|
||||
import cz.jzitnik.screens.VideoPlayScreen;
|
||||
import cz.jzitnik.sound.SoundPlayer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public final class VideoSceneWithAudio extends VideoPlayScreen {
|
||||
private final SoundPlayer soundPlayer = new SoundPlayer();
|
||||
private final String audioPath;
|
||||
|
||||
public VideoSceneWithAudio(String videoPath, String audioPath) {
|
||||
super(videoPath);
|
||||
this.audioPath = audioPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fullRender() {
|
||||
if (!isRenderedAlready) {
|
||||
isRenderedAlready = true;
|
||||
playSound();
|
||||
render();
|
||||
this.soundPlayer.stopCurrentSound();
|
||||
}
|
||||
}
|
||||
|
||||
private void playSound() {
|
||||
new Thread(() -> soundPlayer.playSound(audioPath, 100, 100)).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMouseAction(MouseAction event) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleKeyboardAction(KeyboardPressEvent event) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,76 +1,107 @@
|
||||
package cz.jzitnik.sound;
|
||||
|
||||
import cz.jzitnik.annotations.Dependency;
|
||||
import cz.jzitnik.annotations.injectors.InjectDependency;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
import javax.sound.sampled.*;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Dependency
|
||||
public class SoundPlayer {
|
||||
@InjectDependency
|
||||
private ClassLoader classLoader;
|
||||
private final AtomicReference<SourceDataLine> currentLine = new AtomicReference<>();
|
||||
|
||||
public void playSound(String filePath, int backendVolume, int masterVolume) {
|
||||
long threadId = Thread.currentThread().getId();
|
||||
|
||||
public void playSound(String filePath, int backendVolume, int masterVolume)
|
||||
throws LineUnavailableException, IOException, UnsupportedAudioFileException {
|
||||
if (!filePath.endsWith(".ogg") || masterVolume == 0) {
|
||||
return; // No sound if master volume is 0
|
||||
}
|
||||
|
||||
log.info("Loading resource: {}", "sounds/" + filePath);
|
||||
var file = classLoader.getResourceAsStream("sounds/" + filePath);
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
AudioInputStream audioStream = AudioSystem.getAudioInputStream(file);
|
||||
AudioFormat baseFormat = audioStream.getFormat();
|
||||
|
||||
AudioFormat targetFormat = new AudioFormat(
|
||||
AudioFormat.Encoding.PCM_SIGNED,
|
||||
baseFormat.getSampleRate(),
|
||||
16,
|
||||
baseFormat.getChannels(),
|
||||
baseFormat.getChannels() * 2,
|
||||
baseFormat.getSampleRate(),
|
||||
false
|
||||
);
|
||||
|
||||
AudioInputStream dataIn = AudioSystem.getAudioInputStream(targetFormat, audioStream);
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
DataLine.Info info = new DataLine.Info(SourceDataLine.class, targetFormat);
|
||||
|
||||
try (SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info)) {
|
||||
if (line != null) {
|
||||
line.open(targetFormat);
|
||||
line.start();
|
||||
|
||||
float finalVolume = (backendVolume / 100.0f) * (masterVolume / 100.0f);
|
||||
log.info("Applying volume: {} (backend: {}, master: {})", finalVolume, backendVolume, masterVolume);
|
||||
|
||||
int bytesRead;
|
||||
while ((bytesRead = dataIn.read(buffer, 0, buffer.length)) != -1) {
|
||||
applyVolume(buffer, bytesRead, finalVolume);
|
||||
line.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
line.drain();
|
||||
try (var fileStream = getClass().getClassLoader().getResourceAsStream(filePath)) {
|
||||
if (fileStream == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dataIn.close();
|
||||
audioStream.close();
|
||||
try (AudioInputStream audioStream = AudioSystem.getAudioInputStream(fileStream)) {
|
||||
AudioFormat baseFormat = audioStream.getFormat();
|
||||
AudioFormat targetFormat = new AudioFormat(
|
||||
AudioFormat.Encoding.PCM_SIGNED,
|
||||
baseFormat.getSampleRate(),
|
||||
16,
|
||||
baseFormat.getChannels(),
|
||||
baseFormat.getChannels() * 2,
|
||||
baseFormat.getSampleRate(),
|
||||
false
|
||||
);
|
||||
|
||||
try (AudioInputStream dataIn = AudioSystem.getAudioInputStream(targetFormat, audioStream)) {
|
||||
DataLine.Info info = new DataLine.Info(SourceDataLine.class, targetFormat);
|
||||
|
||||
SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info);
|
||||
currentLine.set(line);
|
||||
|
||||
if (line != null) {
|
||||
line.open(targetFormat);
|
||||
|
||||
if (line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
|
||||
FloatControl gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
float finalVolume = (backendVolume / 100.0f) * (masterVolume / 100.0f);
|
||||
float dB = (float) (Math.log10(Math.max(finalVolume, 0.0001f)) * 20.0f);
|
||||
gainControl.setValue(dB);
|
||||
}
|
||||
|
||||
line.start();
|
||||
log.info("[{}] Line started.", threadId);
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = dataIn.read(buffer, 0, buffer.length)) != -1) {
|
||||
if (Thread.currentThread().isInterrupted() || !line.isOpen()) {
|
||||
log.info("[{}] Loop interrupted.", threadId);
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
line.write(buffer, 0, bytesRead);
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup (only if not already closed)
|
||||
if (line.isOpen()) {
|
||||
line.drain();
|
||||
line.stop();
|
||||
line.close();
|
||||
}
|
||||
}
|
||||
} catch (LineUnavailableException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} catch (UnsupportedAudioFileException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
currentLine.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyVolume(byte[] buffer, int bytesRead, float volume) {
|
||||
for (int i = 0; i < bytesRead; i += 2) { // 16-bit PCM samples are 2 bytes each
|
||||
int sample = (buffer[i] & 0xFF) | (buffer[i + 1] << 8);
|
||||
sample = (int) (sample * volume);
|
||||
buffer[i] = (byte) (sample & 0xFF);
|
||||
buffer[i + 1] = (byte) ((sample >> 8) & 0xFF);
|
||||
/**
|
||||
* Call this method from your Main Thread / VideoSceneWithAudio
|
||||
* to stop the sound INSTANTLY.
|
||||
*/
|
||||
public void stopCurrentSound() {
|
||||
SourceDataLine line = currentLine.get();
|
||||
if (line != null && line.isOpen()) {
|
||||
log.info("Force stopping sound...");
|
||||
line.flush();
|
||||
line.stop();
|
||||
line.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
src/main/java/cz/jzitnik/states/PlayerConfig.java
Normal file
8
src/main/java/cz/jzitnik/states/PlayerConfig.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package cz.jzitnik.states;
|
||||
|
||||
import cz.jzitnik.annotations.State;
|
||||
|
||||
@State
|
||||
public class PlayerConfig {
|
||||
private int masterVolume = 100;
|
||||
}
|
||||
@@ -16,8 +16,7 @@ import org.reflections.Reflections;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class DependencyManager {
|
||||
@@ -105,13 +104,20 @@ public class DependencyManager {
|
||||
|
||||
public void inject(Object instance) {
|
||||
StateManager stateManager = (StateManager) data.get(StateManager.class);
|
||||
Class<?> clazz = instance.getClass();
|
||||
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
List<Field> allFields = new ArrayList<>();
|
||||
Class<?> current = instance.getClass();
|
||||
|
||||
while (current != null && current != Object.class) {
|
||||
allFields.addAll(Arrays.asList(current.getDeclaredFields()));
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
|
||||
for (Field field : allFields) {
|
||||
if (field.isAnnotationPresent(InjectDependency.class)) {
|
||||
field.setAccessible(true);
|
||||
|
||||
if (!data.containsKey(field.getType())) continue;
|
||||
if (!data.containsKey(field.getType()) && field.getType() != getClass()) continue;
|
||||
|
||||
Object dependency = field.getType() == getClass() ? this : data.get(field.getType());
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package cz.jzitnik.utils;
|
||||
|
||||
import cz.jzitnik.annotations.Dependency;
|
||||
import cz.jzitnik.annotations.State;
|
||||
import cz.jzitnik.annotations.injectors.InjectDependency;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.reflections.Reflections;
|
||||
|
||||
@@ -15,15 +16,22 @@ import java.util.Optional;
|
||||
public class StateManager {
|
||||
private final HashMap<Class<?>, Object> data = new HashMap<>();
|
||||
|
||||
public StateManager(Reflections reflections) {
|
||||
public StateManager(Reflections reflections, DependencyManager dependencyManager) {
|
||||
var classes = reflections.getTypesAnnotatedWith(State.class);
|
||||
|
||||
for (Class<?> clazz : classes) {
|
||||
try {
|
||||
var instance = clazz.getDeclaredConstructor().newInstance();
|
||||
data.put(clazz, instance);
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
|
||||
NoSuchMethodException e) {
|
||||
} catch (NoSuchMethodException e) {
|
||||
try {
|
||||
var instance = clazz.getDeclaredConstructor(DependencyManager.class).newInstance(dependencyManager);
|
||||
data.put(clazz, instance);
|
||||
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
|
||||
IllegalAccessException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
log.error("Failed to instantiate state class: {}", clazz.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/main/resources/audio.ogg
Normal file
BIN
src/main/resources/audio.ogg
Normal file
Binary file not shown.
BIN
src/main/resources/video.mp4
Normal file
BIN
src/main/resources/video.mp4
Normal file
Binary file not shown.
Reference in New Issue
Block a user