diff --git a/src/main/java/sh/ball/audio/AudioPlayer.java b/src/main/java/sh/ball/audio/AudioPlayer.java index c60b63e..0551daf 100644 --- a/src/main/java/sh/ball/audio/AudioPlayer.java +++ b/src/main/java/sh/ball/audio/AudioPlayer.java @@ -2,244 +2,23 @@ package sh.ball.audio; import sh.ball.audio.effect.Effect; -import java.io.*; -import java.util.*; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; +public interface AudioPlayer extends Runnable { -import sh.ball.audio.engine.AudioEngine; -import sh.ball.shapes.Shape; -import sh.ball.shapes.Vector2; + void stop(); -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioInputStream; -import java.util.concurrent.Semaphore; -import java.util.concurrent.locks.ReentrantLock; + void setQuality(double quality); -public class AudioPlayer implements Renderer, AudioInputStream> { + void addFrame(S frame); - // Arbitrary max count for effects - private static final int MAX_COUNT = 10000; - private static final int BUFFER_SIZE = 5; - // Is this always true? Might need to check from AudioEngine - private static final int BITS_PER_SAMPLE = 16; - private static final boolean SIGNED = true; - private static final boolean BIG_ENDIAN = false; - // Stereo audio - private static final int NUM_OUTPUTS = 2; + void addEffect(Object identifier, Effect effect); - private final AudioEngine audioEngine; - private final BlockingQueue> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE); - private final Map effects = new HashMap<>(); - private final ReentrantLock renderLock = new ReentrantLock(); - private final List listeners = new ArrayList<>(); + void removeEffect(Object identifier); - private ByteArrayOutputStream outputStream; - private boolean recording = false; - private int framesRecorded = 0; - private List frame; - private int currentShape = 0; - private int audioFramesDrawn = 0; - private int count = 0; + void read(byte[] buffer) throws InterruptedException; - private double weight = Shape.DEFAULT_WEIGHT; + void startRecord(); - public AudioPlayer(AudioEngine audioEngine) { - this.audioEngine = audioEngine; - } + int samplesPerSecond(); - private Vector2 generateChannels() throws InterruptedException { - Shape shape = getCurrentShape().setWeight(weight); - - double totalAudioFrames = shape.getWeight() * shape.getLength(); - double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames; - Vector2 nextVector = applyEffects(count, shape.nextVector(drawingProgress)); - - Vector2 channels = cutoff(nextVector); - writeChannels((float) channels.getX(), (float) channels.getY()); - - audioFramesDrawn++; - - if (++count > MAX_COUNT) { - count = 0; - } - - if (audioFramesDrawn > totalAudioFrames) { - audioFramesDrawn = 0; - currentShape++; - } - - if (currentShape >= frame.size()) { - currentShape = 0; - frame = frameQueue.take(); - } - - return channels; - } - - private void writeChannels(float leftChannel, float rightChannel) { - int left = (int)(leftChannel * Short.MAX_VALUE); - int right = (int)(rightChannel * Short.MAX_VALUE); - - byte b0 = (byte) left; - byte b1 = (byte)(left >> 8); - byte b2 = (byte) right; - byte b3 = (byte)(right >> 8); - - if (recording) { - outputStream.write(b0); - outputStream.write(b1); - outputStream.write(b2); - outputStream.write(b3); - } - - for (Listener listener : listeners) { - listener.write(b0); - listener.write(b1); - listener.write(b2); - listener.write(b3); - listener.notifyIfFull(); - } - - framesRecorded++; - } - - private Vector2 cutoff(Vector2 vector) { - if (vector.getX() < -1) { - vector = vector.setX(-1); - } else if (vector.getX() > 1) { - vector = vector.setX(1); - } - if (vector.getY() < -1) { - vector = vector.setY(-1); - } else if (vector.getY() > 1) { - vector = vector.setY(1); - } - return vector; - } - - private Vector2 applyEffects(int frame, Vector2 vector) { - for (Effect effect : effects.values()) { - vector = effect.apply(frame, vector); - } - return vector; - } - - @Override - public void setQuality(double quality) { - this.weight = quality; - } - - private Shape getCurrentShape() { - if (frame.size() == 0) { - return new Vector2(); - } - - return frame.get(currentShape); - } - - @Override - public void run() { - try { - frame = frameQueue.take(); - } catch (InterruptedException e) { - throw new RuntimeException("Initial frame not found. Cannot continue."); - } - - audioEngine.play(this::generateChannels, renderLock); - } - - @Override - public void stop() { - audioEngine.stop(); - } - - @Override - public void addFrame(List frame) { - try { - frameQueue.put(frame); - } catch (InterruptedException e) { - e.printStackTrace(); - System.err.println("Frame missed."); - } - } - - @Override - public void addEffect(Object identifier, Effect effect) { - effects.put(identifier, effect); - } - - @Override - public void removeEffect(Object identifier) { - effects.remove(identifier); - } - - @Override - public void read(byte[] buffer) throws InterruptedException { - Listener listener = new Listener(buffer); - try { - renderLock.lock(); - listeners.add(listener); - } finally { - renderLock.unlock(); - } - listener.waitUntilFull(); - try { - renderLock.lock(); - listeners.remove(listener); - } finally { - renderLock.unlock(); - } - } - - @Override - public void startRecord() { - outputStream = new ByteArrayOutputStream(); - framesRecorded = 0; - recording = true; - } - - @Override - public int samplesPerSecond() { - return audioEngine.sampleRate(); - } - - @Override - public AudioInputStream stopRecord() { - recording = false; - byte[] input = outputStream.toByteArray(); - outputStream = null; - - AudioFormat audioFormat = new AudioFormat(audioEngine.sampleRate(), BITS_PER_SAMPLE, NUM_OUTPUTS, SIGNED, BIG_ENDIAN); - - return new AudioInputStream(new ByteArrayInputStream(input), audioFormat, framesRecorded); - } - - private static class Listener { - private final byte[] buffer; - private final Semaphore sema; - - private int offset; - - private Listener(byte[] buffer) { - this.buffer = buffer; - this.sema = new Semaphore(0); - } - - private void waitUntilFull() throws InterruptedException { - sema.acquire(); - } - - private void notifyIfFull() { - if (offset >= buffer.length) { - sema.release(); - } - } - - private void write(byte b) { - if (offset < buffer.length) { - buffer[offset++] = b; - } - } - } + T stopRecord(); } diff --git a/src/main/java/sh/ball/audio/FrameProducer.java b/src/main/java/sh/ball/audio/FrameProducer.java index d1544e6..8aa04fd 100644 --- a/src/main/java/sh/ball/audio/FrameProducer.java +++ b/src/main/java/sh/ball/audio/FrameProducer.java @@ -2,13 +2,13 @@ package sh.ball.audio; public class FrameProducer implements Runnable { - private final Renderer renderer; + private final AudioPlayer audioPlayer; private final FrameSet frames; private boolean running; - public FrameProducer(Renderer renderer, FrameSet frames) { - this.renderer = renderer; + public FrameProducer(AudioPlayer audioPlayer, FrameSet frames) { + this.audioPlayer = audioPlayer; this.frames = frames; } @@ -16,7 +16,7 @@ public class FrameProducer implements Runnable { public void run() { running = true; while (running) { - renderer.addFrame(frames.next()); + audioPlayer.addFrame(frames.next()); } } diff --git a/src/main/java/sh/ball/audio/FrequencyAnalyser.java b/src/main/java/sh/ball/audio/FrequencyAnalyser.java index a65e108..8b80414 100644 --- a/src/main/java/sh/ball/audio/FrequencyAnalyser.java +++ b/src/main/java/sh/ball/audio/FrequencyAnalyser.java @@ -12,14 +12,14 @@ public class FrequencyAnalyser implements Runnable { // increase this for higher frequency resolution, but less frequent frequency calculation private static final int DEFAULT_POWER_OF_TWO = 18; - private final Renderer renderer; + private final AudioPlayer audioPlayer; private final List listeners = new ArrayList<>(); private final int frameSize; private final int sampleRate; private final int powerOfTwo; - public FrequencyAnalyser(Renderer renderer, int frameSize, int sampleRate) { - this.renderer = renderer; + public FrequencyAnalyser(AudioPlayer audioPlayer, int frameSize, int sampleRate) { + this.audioPlayer = audioPlayer; this.frameSize = frameSize; this.sampleRate = sampleRate; this.powerOfTwo = (int) (DEFAULT_POWER_OF_TWO - Math.log(DEFAULT_SAMPLE_RATE / sampleRate) / Math.log(2)); @@ -42,7 +42,7 @@ public class FrequencyAnalyser implements Runnable { while (true) { try { - renderer.read(buf); + audioPlayer.read(buf); } catch (InterruptedException e) { e.printStackTrace(); } diff --git a/src/main/java/sh/ball/audio/Renderer.java b/src/main/java/sh/ball/audio/Renderer.java deleted file mode 100644 index ae6f20f..0000000 --- a/src/main/java/sh/ball/audio/Renderer.java +++ /dev/null @@ -1,24 +0,0 @@ -package sh.ball.audio; - -import sh.ball.audio.effect.Effect; - -public interface Renderer extends Runnable { - - void stop(); - - void setQuality(double quality); - - void addFrame(S frame); - - void addEffect(Object identifier, Effect effect); - - void removeEffect(Object identifier); - - void read(byte[] buffer) throws InterruptedException; - - void startRecord(); - - int samplesPerSecond(); - - T stopRecord(); -} diff --git a/src/main/java/sh/ball/audio/ShapeAudioPlayer.java b/src/main/java/sh/ball/audio/ShapeAudioPlayer.java new file mode 100644 index 0000000..862c394 --- /dev/null +++ b/src/main/java/sh/ball/audio/ShapeAudioPlayer.java @@ -0,0 +1,245 @@ +package sh.ball.audio; + +import sh.ball.audio.effect.Effect; + +import java.io.*; +import java.util.*; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +import sh.ball.audio.engine.AudioEngine; +import sh.ball.shapes.Shape; +import sh.ball.shapes.Vector2; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import java.util.concurrent.Semaphore; +import java.util.concurrent.locks.ReentrantLock; + +public class ShapeAudioPlayer implements AudioPlayer, AudioInputStream> { + + // Arbitrary max count for effects + private static final int MAX_COUNT = 10000; + private static final int BUFFER_SIZE = 5; + // Is this always true? Might need to check from AudioEngine + private static final int BITS_PER_SAMPLE = 16; + private static final boolean SIGNED = true; + private static final boolean BIG_ENDIAN = false; + // Stereo audio + private static final int NUM_OUTPUTS = 2; + + private final AudioEngine audioEngine; + private final BlockingQueue> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE); + private final Map effects = new HashMap<>(); + private final ReentrantLock renderLock = new ReentrantLock(); + private final List listeners = new ArrayList<>(); + + private ByteArrayOutputStream outputStream; + private boolean recording = false; + private int framesRecorded = 0; + private List frame; + private int currentShape = 0; + private int audioFramesDrawn = 0; + private int count = 0; + + private double weight = Shape.DEFAULT_WEIGHT; + + public ShapeAudioPlayer(AudioEngine audioEngine) { + this.audioEngine = audioEngine; + } + + private Vector2 generateChannels() throws InterruptedException { + Shape shape = getCurrentShape().setWeight(weight); + + double totalAudioFrames = shape.getWeight() * shape.getLength(); + double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames; + Vector2 nextVector = applyEffects(count, shape.nextVector(drawingProgress)); + + Vector2 channels = cutoff(nextVector); + writeChannels((float) channels.getX(), (float) channels.getY()); + + audioFramesDrawn++; + + if (++count > MAX_COUNT) { + count = 0; + } + + if (audioFramesDrawn > totalAudioFrames) { + audioFramesDrawn = 0; + currentShape++; + } + + if (currentShape >= frame.size()) { + currentShape = 0; + frame = frameQueue.take(); + } + + return channels; + } + + private void writeChannels(float leftChannel, float rightChannel) { + int left = (int)(leftChannel * Short.MAX_VALUE); + int right = (int)(rightChannel * Short.MAX_VALUE); + + byte b0 = (byte) left; + byte b1 = (byte)(left >> 8); + byte b2 = (byte) right; + byte b3 = (byte)(right >> 8); + + if (recording) { + outputStream.write(b0); + outputStream.write(b1); + outputStream.write(b2); + outputStream.write(b3); + } + + for (Listener listener : listeners) { + listener.write(b0); + listener.write(b1); + listener.write(b2); + listener.write(b3); + listener.notifyIfFull(); + } + + framesRecorded++; + } + + private Vector2 cutoff(Vector2 vector) { + if (vector.getX() < -1) { + vector = vector.setX(-1); + } else if (vector.getX() > 1) { + vector = vector.setX(1); + } + if (vector.getY() < -1) { + vector = vector.setY(-1); + } else if (vector.getY() > 1) { + vector = vector.setY(1); + } + return vector; + } + + private Vector2 applyEffects(int frame, Vector2 vector) { + for (Effect effect : effects.values()) { + vector = effect.apply(frame, vector); + } + return vector; + } + + @Override + public void setQuality(double quality) { + this.weight = quality; + } + + private Shape getCurrentShape() { + if (frame.size() == 0) { + return new Vector2(); + } + + return frame.get(currentShape); + } + + @Override + public void run() { + try { + frame = frameQueue.take(); + } catch (InterruptedException e) { + throw new RuntimeException("Initial frame not found. Cannot continue."); + } + + audioEngine.play(this::generateChannels, renderLock); + } + + @Override + public void stop() { + audioEngine.stop(); + } + + @Override + public void addFrame(List frame) { + try { + frameQueue.put(frame); + } catch (InterruptedException e) { + e.printStackTrace(); + System.err.println("Frame missed."); + } + } + + @Override + public void addEffect(Object identifier, Effect effect) { + effects.put(identifier, effect); + } + + @Override + public void removeEffect(Object identifier) { + effects.remove(identifier); + } + + @Override + public void read(byte[] buffer) throws InterruptedException { + Listener listener = new Listener(buffer); + try { + renderLock.lock(); + listeners.add(listener); + } finally { + renderLock.unlock(); + } + listener.waitUntilFull(); + try { + renderLock.lock(); + listeners.remove(listener); + } finally { + renderLock.unlock(); + } + } + + @Override + public void startRecord() { + outputStream = new ByteArrayOutputStream(); + framesRecorded = 0; + recording = true; + } + + @Override + public int samplesPerSecond() { + return audioEngine.sampleRate(); + } + + @Override + public AudioInputStream stopRecord() { + recording = false; + byte[] input = outputStream.toByteArray(); + outputStream = null; + + AudioFormat audioFormat = new AudioFormat(audioEngine.sampleRate(), BITS_PER_SAMPLE, NUM_OUTPUTS, SIGNED, BIG_ENDIAN); + + return new AudioInputStream(new ByteArrayInputStream(input), audioFormat, framesRecorded); + } + + private static class Listener { + private final byte[] buffer; + private final Semaphore sema; + + private int offset; + + private Listener(byte[] buffer) { + this.buffer = buffer; + this.sema = new Semaphore(0); + } + + private void waitUntilFull() throws InterruptedException { + sema.acquire(); + } + + private void notifyIfFull() { + if (offset >= buffer.length) { + sema.release(); + } + } + + private void write(byte b) { + if (offset < buffer.length) { + buffer[offset++] = b; + } + } + } +} diff --git a/src/main/java/sh/ball/gui/Controller.java b/src/main/java/sh/ball/gui/Controller.java index 53f2b42..864e382 100644 --- a/src/main/java/sh/ball/gui/Controller.java +++ b/src/main/java/sh/ball/gui/Controller.java @@ -49,7 +49,7 @@ public class Controller implements Initializable, FrequencyListener, Listener { private static final InputStream DEFAULT_OBJ = Controller.class.getResourceAsStream("/models/cube.obj"); private final FileChooser fileChooser = new FileChooser(); - private final Renderer, AudioInputStream> renderer; + private final AudioPlayer, AudioInputStream> audioPlayer; private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final int sampleRate; @@ -121,12 +121,12 @@ public class Controller implements Initializable, FrequencyListener, Listener { @FXML private Slider wobbleSlider; - public Controller(Renderer, AudioInputStream> renderer) throws IOException { - this.renderer = renderer; + public Controller(AudioPlayer, AudioInputStream> audioPlayer) throws IOException { + this.audioPlayer = audioPlayer; FrameSet> frames = new ObjParser(DEFAULT_OBJ).parse(); frames.addListener(this); - this.producer = new FrameProducer<>(renderer, frames); - this.sampleRate = renderer.samplesPerSecond(); + this.producer = new FrameProducer<>(audioPlayer, frames); + this.sampleRate = audioPlayer.samplesPerSecond(); this.rotateEffect = new RotateEffect(sampleRate); this.translateEffect = new TranslateEffect(sampleRate); this.wobbleEffect = new WobbleEffect(sampleRate); @@ -136,7 +136,7 @@ public class Controller implements Initializable, FrequencyListener, Listener { private Map> initializeSliderMap() { return Map.of( weightSlider, - renderer::setQuality, + audioPlayer::setQuality, rotateSpeedSlider, rotateEffect::setSpeed, translationSpeedSlider, @@ -238,15 +238,15 @@ public class Controller implements Initializable, FrequencyListener, Listener { updateObjectRotateSpeed(); - renderer.addEffect(EffectType.SCALE, scaleEffect); - renderer.addEffect(EffectType.ROTATE, rotateEffect); - renderer.addEffect(EffectType.TRANSLATE, translateEffect); + audioPlayer.addEffect(EffectType.SCALE, scaleEffect); + audioPlayer.addEffect(EffectType.ROTATE, rotateEffect); + audioPlayer.addEffect(EffectType.TRANSLATE, translateEffect); executor.submit(producer); - Thread renderThread = new Thread(renderer); + Thread renderThread = new Thread(audioPlayer); renderThread.setUncaughtExceptionHandler((thread, throwable) -> throwable.printStackTrace()); renderThread.start(); - FrequencyAnalyser, AudioInputStream> analyser = new FrequencyAnalyser<>(renderer, 2, sampleRate); + FrequencyAnalyser, AudioInputStream> analyser = new FrequencyAnalyser<>(audioPlayer, 2, sampleRate); analyser.addListener(this); analyser.addListener(wobbleEffect); new Thread(analyser).start(); @@ -257,10 +257,10 @@ public class Controller implements Initializable, FrequencyListener, Listener { if (recording) { recordLabel.setText("Recording..."); recordButton.setText("Stop Recording"); - renderer.startRecord(); + audioPlayer.startRecord(); } else { recordButton.setText("Record"); - AudioInputStream input = renderer.stopRecord(); + AudioInputStream input = audioPlayer.stopRecord(); try { File file = fileChooser.showSaveDialog(stage); SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); @@ -315,10 +315,10 @@ public class Controller implements Initializable, FrequencyListener, Listener { private void updateEffect(EffectType type, boolean checked, Effect effect) { if (checked) { - renderer.addEffect(type, effect); + audioPlayer.addEffect(type, effect); effectTypes.get(type).setDisable(false); } else { - renderer.removeEffect(type); + audioPlayer.removeEffect(type); effectTypes.get(type).setDisable(true); } } @@ -329,7 +329,7 @@ public class Controller implements Initializable, FrequencyListener, Listener { String path = file.getAbsolutePath(); FrameSet> frames = ParserFactory.getParser(path).parse(); frames.addListener(this); - producer = new FrameProducer<>(renderer, frames); + producer = new FrameProducer<>(audioPlayer, frames); updateObjectRotateSpeed(); updateFocalLength(); diff --git a/src/main/java/sh/ball/gui/Gui.java b/src/main/java/sh/ball/gui/Gui.java index 69d938e..4c615d0 100644 --- a/src/main/java/sh/ball/gui/Gui.java +++ b/src/main/java/sh/ball/gui/Gui.java @@ -10,7 +10,7 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; -import sh.ball.audio.AudioPlayer; +import sh.ball.audio.ShapeAudioPlayer; import sh.ball.audio.engine.XtAudioEngine; import sh.ball.engine.Vector3; @@ -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 AudioPlayer(new XtAudioEngine())); + Controller controller = new Controller(new ShapeAudioPlayer(new XtAudioEngine())); loader.setController(controller); Parent root = loader.load();