From 3f07b5f433379948e2c7d38bc9a1c46a94d289af Mon Sep 17 00:00:00 2001 From: James Ball Date: Thu, 17 Jun 2021 21:36:06 +0100 Subject: [PATCH] Reoganise UI and fully implement device selection --- src/main/java/sh/ball/audio/AudioPlayer.java | 4 ++ .../java/sh/ball/audio/FrequencyAnalyser.java | 8 ++- .../java/sh/ball/audio/ShapeAudioPlayer.java | 23 +++++++-- .../sh/ball/audio/engine/AudioEngine.java | 2 + .../ball/audio/engine/DefaultAudioDevice.java | 19 ++++++- .../sh/ball/audio/engine/XtAudioEngine.java | 21 ++++---- src/main/java/sh/ball/gui/Controller.java | 49 +++++++++++++++---- src/main/java/sh/ball/gui/Gui.java | 2 +- src/main/resources/css/main.css | 9 ++-- src/main/resources/fxml/osci-render.fxml | 16 +++--- 10 files changed, 113 insertions(+), 40 deletions(-) diff --git a/src/main/java/sh/ball/audio/AudioPlayer.java b/src/main/java/sh/ball/audio/AudioPlayer.java index a3bb48b..8e77ea8 100644 --- a/src/main/java/sh/ball/audio/AudioPlayer.java +++ b/src/main/java/sh/ball/audio/AudioPlayer.java @@ -8,8 +8,12 @@ import java.util.List; public interface AudioPlayer extends Runnable { + void reset() throws Exception; + void stop(); + boolean isPlaying(); + void setQuality(double quality); void addFrame(S frame); diff --git a/src/main/java/sh/ball/audio/FrequencyAnalyser.java b/src/main/java/sh/ball/audio/FrequencyAnalyser.java index e31fbdf..1dd142a 100644 --- a/src/main/java/sh/ball/audio/FrequencyAnalyser.java +++ b/src/main/java/sh/ball/audio/FrequencyAnalyser.java @@ -18,6 +18,8 @@ public class FrequencyAnalyser implements Runnable { private final int sampleRate; private final int powerOfTwo; + private volatile boolean stopped; + public FrequencyAnalyser(AudioPlayer audioPlayer, int frameSize, int sampleRate) { this.audioPlayer = audioPlayer; this.frameSize = frameSize; @@ -40,7 +42,7 @@ public class FrequencyAnalyser implements Runnable { public void run() { byte[] buf = new byte[2 << powerOfTwo]; - while (true) { + while (!stopped) { try { audioPlayer.read(buf); } catch (InterruptedException e) { @@ -78,6 +80,10 @@ public class FrequencyAnalyser implements Runnable { } } + public void stop() { + stopped = true; + } + private double[] decode(final byte[] buf, boolean decodeLeft) { final double[] fbuf = new double[(buf.length / 2) / frameSize]; int byteNum = 0; diff --git a/src/main/java/sh/ball/audio/ShapeAudioPlayer.java b/src/main/java/sh/ball/audio/ShapeAudioPlayer.java index c399ce3..141a936 100644 --- a/src/main/java/sh/ball/audio/ShapeAudioPlayer.java +++ b/src/main/java/sh/ball/audio/ShapeAudioPlayer.java @@ -14,6 +14,7 @@ import sh.ball.shapes.Vector2; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; +import java.util.concurrent.Callable; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.ReentrantLock; @@ -29,12 +30,13 @@ public class ShapeAudioPlayer implements AudioPlayer> { // Stereo audio private static final int NUM_OUTPUTS = 2; - private final AudioEngine audioEngine; + private final Callable audioEngineBuilder; 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 AudioEngine audioEngine; private ByteArrayOutputStream outputStream; private boolean recording = false; private int framesRecorded = 0; @@ -46,8 +48,9 @@ public class ShapeAudioPlayer implements AudioPlayer> { private double weight = Shape.DEFAULT_WEIGHT; private AudioDevice device; - public ShapeAudioPlayer(AudioEngine audioEngine) { - this.audioEngine = audioEngine; + public ShapeAudioPlayer(Callable audioEngineBuilder) throws Exception { + this.audioEngineBuilder = audioEngineBuilder; + this.audioEngine = audioEngineBuilder.call(); } private Vector2 generateChannels() throws InterruptedException { @@ -155,11 +158,25 @@ public class ShapeAudioPlayer implements AudioPlayer> { audioEngine.play(this::generateChannels, renderLock, device); } + @Override + public void reset() throws Exception { + audioEngine.stop(); + while (isPlaying()) { + Thread.onSpinWait(); + } + audioEngine = audioEngineBuilder.call(); + } + @Override public void stop() { audioEngine.stop(); } + @Override + public boolean isPlaying() { + return audioEngine.isPlaying(); + } + @Override public void addFrame(List frame) { try { diff --git a/src/main/java/sh/ball/audio/engine/AudioEngine.java b/src/main/java/sh/ball/audio/engine/AudioEngine.java index 29415b0..f947cb9 100644 --- a/src/main/java/sh/ball/audio/engine/AudioEngine.java +++ b/src/main/java/sh/ball/audio/engine/AudioEngine.java @@ -7,6 +7,8 @@ import java.util.concurrent.Callable; import java.util.concurrent.locks.ReentrantLock; public interface AudioEngine { + boolean isPlaying(); + void play(Callable channelGenerator, ReentrantLock renderLock, AudioDevice device); void stop(); diff --git a/src/main/java/sh/ball/audio/engine/DefaultAudioDevice.java b/src/main/java/sh/ball/audio/engine/DefaultAudioDevice.java index b63ed6a..da530e8 100644 --- a/src/main/java/sh/ball/audio/engine/DefaultAudioDevice.java +++ b/src/main/java/sh/ball/audio/engine/DefaultAudioDevice.java @@ -1,5 +1,7 @@ package sh.ball.audio.engine; +import java.util.Objects; + public class DefaultAudioDevice implements AudioDevice { final String id; @@ -36,6 +38,21 @@ public class DefaultAudioDevice implements AudioDevice { @Override public String toString() { - return name + " @ " + sampleRate + "KHz"; + String simplifiedName = name.replaceFirst(" \\(Shared\\)", ""); + simplifiedName = simplifiedName.replaceFirst(" \\(NVIDIA High Definition Audio\\)", ""); + return simplifiedName + " @ " + sampleRate + "KHz"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DefaultAudioDevice that = (DefaultAudioDevice) o; + return sampleRate == that.sampleRate && Objects.equals(id, that.id) && Objects.equals(name, that.name) && sample == that.sample; + } + + @Override + public int hashCode() { + return Objects.hash(id, name, sampleRate, sample); } } diff --git a/src/main/java/sh/ball/audio/engine/XtAudioEngine.java b/src/main/java/sh/ball/audio/engine/XtAudioEngine.java index 37e3c40..d49d814 100644 --- a/src/main/java/sh/ball/audio/engine/XtAudioEngine.java +++ b/src/main/java/sh/ball/audio/engine/XtAudioEngine.java @@ -16,6 +16,8 @@ public class XtAudioEngine implements AudioEngine { private static final int NUM_OUTPUTS = 2; private volatile boolean stopped = false; + + private boolean playing = false; private ReentrantLock renderLock; private Callable channelGenerator; @@ -41,8 +43,14 @@ public class XtAudioEngine implements AudioEngine { return 0; } + @Override + public boolean isPlaying() { + return playing; + } + @Override public void play(Callable channelGenerator, ReentrantLock renderLock, AudioDevice device) { + playing = true; this.channelGenerator = channelGenerator; this.renderLock = renderLock; try (XtPlatform platform = XtAudio.init(null, null)) { @@ -72,6 +80,7 @@ public class XtAudioEngine implements AudioEngine { } } } + playing = false; } @Override @@ -158,18 +167,6 @@ public class XtAudioEngine implements AudioEngine { return service; } - private String getDeviceId(XtService service) { - String deviceId = service.getDefaultDeviceId(true); - if (deviceId == null) { - return getFirstDevice(service); - } - return deviceId; - } - - private String getFirstDevice(XtService service) { - return service.openDeviceList(EnumSet.of(Enums.XtEnumFlags.OUTPUT)).getId(0); - } - private AudioSample XtSampleToAudioSample(Enums.XtSample sample) { return switch (sample) { case UINT8 -> AudioSample.UINT8; diff --git a/src/main/java/sh/ball/gui/Controller.java b/src/main/java/sh/ball/gui/Controller.java index 7da5c7e..8d6c90c 100644 --- a/src/main/java/sh/ball/gui/Controller.java +++ b/src/main/java/sh/ball/gui/Controller.java @@ -54,14 +54,15 @@ public class Controller implements Initializable, FrequencyListener, Listener { private final AudioPlayer> audioPlayer; private final ExecutorService executor = Executors.newSingleThreadExecutor(); - private final int sampleRate; private final RotateEffect rotateEffect; private final TranslateEffect translateEffect; private final WobbleEffect wobbleEffect; private final ScaleEffect scaleEffect; - private AudioDevice defaultDevice; + private int sampleRate; + private FrequencyAnalyser> analyser; + private final AudioDevice defaultDevice; private FrameProducer> producer; private boolean recording = false; @@ -248,15 +249,43 @@ public class Controller implements Initializable, FrequencyListener, Listener { audioPlayer.addEffect(EffectType.ROTATE, rotateEffect); audioPlayer.addEffect(EffectType.TRANSLATE, translateEffect); - executor.submit(producer); audioPlayer.setDevice(defaultDevice); - System.out.println(audioPlayer.devices()); deviceComboBox.setItems(FXCollections.observableList(audioPlayer.devices())); - deviceComboBox.getSelectionModel().select(defaultDevice); - Thread renderThread = new Thread(audioPlayer); - renderThread.setUncaughtExceptionHandler((thread, throwable) -> throwable.printStackTrace()); - renderThread.start(); - FrequencyAnalyser> analyser = new FrequencyAnalyser<>(audioPlayer, 2, sampleRate); + deviceComboBox.setValue(defaultDevice); + + executor.submit(producer); + analyser = new FrequencyAnalyser<>(audioPlayer, 2, sampleRate); + startFrequencyAnalyser(analyser); + startAudioPlayerThread(); + + deviceComboBox.valueProperty().addListener((options, oldDevice, newDevice) -> { + if (newDevice != null) { + switchAudioDevice(newDevice); + } + }); + } + + private void switchAudioDevice(AudioDevice device) { + try { + audioPlayer.reset(); + } catch (Exception e) { + e.printStackTrace(); + } + audioPlayer.setDevice(device); + analyser.stop(); + sampleRate = device.sampleRate(); + analyser = new FrequencyAnalyser<>(audioPlayer, 2, sampleRate); + startFrequencyAnalyser(analyser); + startAudioPlayerThread(); + } + + private void startAudioPlayerThread() { + Thread audioPlayerThread = new Thread(audioPlayer); + audioPlayerThread.setUncaughtExceptionHandler((thread, throwable) -> throwable.printStackTrace()); + audioPlayerThread.start(); + } + + private void startFrequencyAnalyser(FrequencyAnalyser> analyser) { analyser.addListener(this); analyser.addListener(wobbleEffect); new Thread(analyser).start(); @@ -273,7 +302,7 @@ public class Controller implements Initializable, FrequencyListener, Listener { AudioInputStream input = audioPlayer.stopRecord(); try { File file = fileChooser.showSaveDialog(stage); - SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); Date date = new Date(System.currentTimeMillis()); if (file == null) { file = new File("out-" + formatter.format(date) + ".wav"); diff --git a/src/main/java/sh/ball/gui/Gui.java b/src/main/java/sh/ball/gui/Gui.java index 4c615d0..63d2e35 100644 --- a/src/main/java/sh/ball/gui/Gui.java +++ b/src/main/java/sh/ball/gui/Gui.java @@ -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(new XtAudioEngine())); + Controller controller = new Controller(new ShapeAudioPlayer(XtAudioEngine::new)); loader.setController(controller); Parent root = loader.load(); diff --git a/src/main/resources/css/main.css b/src/main/resources/css/main.css index 4d5699f..c6ebdeb 100644 --- a/src/main/resources/css/main.css +++ b/src/main/resources/css/main.css @@ -113,6 +113,7 @@ -fx-background-radius: 0; -fx-border-radius: 0; -fx-border-width: 0; + -fx-cell-size: 35; /* No alternate highlighting */ -fx-background-color: dark_color; @@ -137,7 +138,7 @@ .combo-box-base { - -fx-background-color: very_dark; + -fx-background-color: white; -fx-border-radius: 0; -fx-border-width: 1 1 1 0; -fx-border-color: white; @@ -148,16 +149,16 @@ .combo-box-base:hover { - -fx-color: very_dark; + -fx-color: white; } .combo-box-base:showing { - -fx-color: very_dark; + -fx-color: white; } .combo-box-base:focused { - -fx-background-color: very_dark; + -fx-background-color: white; -fx-background-radius: 0; -fx-background-insets: 0; } \ No newline at end of file diff --git a/src/main/resources/fxml/osci-render.fxml b/src/main/resources/fxml/osci-render.fxml index df6232a..272781c 100644 --- a/src/main/resources/fxml/osci-render.fxml +++ b/src/main/resources/fxml/osci-render.fxml @@ -11,14 +11,14 @@ - + -