diff --git a/src/main/java/sh/ball/audio/engine/JavaAudioEngine.java b/src/main/java/sh/ball/audio/engine/JavaAudioEngine.java index ff2fcc47..37670397 100644 --- a/src/main/java/sh/ball/audio/engine/JavaAudioEngine.java +++ b/src/main/java/sh/ball/audio/engine/JavaAudioEngine.java @@ -2,33 +2,91 @@ package sh.ball.audio.engine; import sh.ball.shapes.Vector2; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.SourceDataLine; import java.util.List; import java.util.concurrent.Callable; public class JavaAudioEngine implements AudioEngine { + private static final int DEFAULT_SAMPLE_RATE = 192000; + private static final int NUM_CHANNELS = 2; + private static final int LATENCY_MS = 10; + private static final int MAX_FRAME_LATENCY = 512; + private static final int BIT_DEPTH = 16; + private static final int FRAME_SIZE = NUM_CHANNELS * BIT_DEPTH / 8; + private static final boolean BIG_ENDIAN = false; + private static final boolean SIGNED_SAMPLE = true; + public static final double EPSILON = 0.1; + + private volatile boolean stopped = false; + + private Callable channelGenerator; + private SourceDataLine source; + @Override public boolean isPlaying() { - return false; + return source.isRunning(); } @Override public void play(Callable channelGenerator, AudioDevice device) throws Exception { + this.channelGenerator = channelGenerator; + AudioFormat format = new AudioFormat((float) device.sampleRate(), BIT_DEPTH, NUM_CHANNELS, SIGNED_SAMPLE, BIG_ENDIAN); + + this.source = AudioSystem.getSourceDataLine(format); + source.open(format); + + int frameLatency = Math.max((int) (device.sampleRate() * LATENCY_MS * 0.0005), MAX_FRAME_LATENCY); + int bufferSize = frameLatency * FRAME_SIZE; + int remainingBufferSpace = source.getBufferSize() - bufferSize; + + byte[] buffer = new byte[bufferSize * 2]; + + source.start(); + while (!stopped) { + int delta = source.available() - remainingBufferSpace; + if (delta > 0) { + int requiredSamples = (delta + bufferSize) / FRAME_SIZE; + + if (requiredSamples * NUM_CHANNELS > buffer.length / 2) { + buffer = new byte[requiredSamples * NUM_CHANNELS * 2]; + } + + for (int i = 0; i < requiredSamples; i++) { + try { + Vector2 channels = channelGenerator.call(); + short left = (short) (channels.getX() * Short.MAX_VALUE); + short right = (short) (channels.getY() * Short.MAX_VALUE); + buffer[i * 4] = (byte) left; + buffer[i * 4 + 1] = (byte) (left >> 8); + buffer[i * 4 + 2] = (byte) right; + buffer[i * 4 + 3] = (byte) (right >> 8); + } catch (Exception e) { + e.printStackTrace(); + } + } + + source.write(buffer, 0, requiredSamples * FRAME_SIZE); + } + } + source.stop(); } @Override public void stop() { - + stopped = true; } @Override public List devices() { - return null; + return List.of(getDefaultDevice()); } @Override public AudioDevice getDefaultDevice() { - return null; + return new DefaultAudioDevice("default", "default", DEFAULT_SAMPLE_RATE, AudioSample.INT16); } } diff --git a/src/main/java/sh/ball/gui/Gui.java b/src/main/java/sh/ball/gui/Gui.java index 63d2e35f..7df4208f 100644 --- a/src/main/java/sh/ball/gui/Gui.java +++ b/src/main/java/sh/ball/gui/Gui.java @@ -11,7 +11,7 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; import sh.ball.audio.ShapeAudioPlayer; -import sh.ball.audio.engine.XtAudioEngine; +import sh.ball.audio.engine.JavaAudioEngine; import sh.ball.engine.Vector3; import java.util.Objects; @@ -24,7 +24,7 @@ public class Gui extends Application { System.setProperty("prism.lcdtext", "false"); FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/osci-render.fxml")); - Controller controller = new Controller(new ShapeAudioPlayer(XtAudioEngine::new)); + Controller controller = new Controller(new ShapeAudioPlayer(JavaAudioEngine::new)); loader.setController(controller); Parent root = loader.load();