kopia lustrzana https://github.com/jameshball/osci-render
Reoganise UI and fully implement device selection
rodzic
4c584fb96c
commit
3f07b5f433
|
@ -8,8 +8,12 @@ import java.util.List;
|
|||
|
||||
public interface AudioPlayer<S> extends Runnable {
|
||||
|
||||
void reset() throws Exception;
|
||||
|
||||
void stop();
|
||||
|
||||
boolean isPlaying();
|
||||
|
||||
void setQuality(double quality);
|
||||
|
||||
void addFrame(S frame);
|
||||
|
|
|
@ -18,6 +18,8 @@ public class FrequencyAnalyser<S> implements Runnable {
|
|||
private final int sampleRate;
|
||||
private final int powerOfTwo;
|
||||
|
||||
private volatile boolean stopped;
|
||||
|
||||
public FrequencyAnalyser(AudioPlayer<S> audioPlayer, int frameSize, int sampleRate) {
|
||||
this.audioPlayer = audioPlayer;
|
||||
this.frameSize = frameSize;
|
||||
|
@ -40,7 +42,7 @@ public class FrequencyAnalyser<S> 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<S> 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;
|
||||
|
|
|
@ -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<List<Shape>> {
|
|||
// Stereo audio
|
||||
private static final int NUM_OUTPUTS = 2;
|
||||
|
||||
private final AudioEngine audioEngine;
|
||||
private final Callable<AudioEngine> audioEngineBuilder;
|
||||
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
|
||||
private final Map<Object, Effect> effects = new HashMap<>();
|
||||
private final ReentrantLock renderLock = new ReentrantLock();
|
||||
private final List<Listener> 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<List<Shape>> {
|
|||
private double weight = Shape.DEFAULT_WEIGHT;
|
||||
private AudioDevice device;
|
||||
|
||||
public ShapeAudioPlayer(AudioEngine audioEngine) {
|
||||
this.audioEngine = audioEngine;
|
||||
public ShapeAudioPlayer(Callable<AudioEngine> audioEngineBuilder) throws Exception {
|
||||
this.audioEngineBuilder = audioEngineBuilder;
|
||||
this.audioEngine = audioEngineBuilder.call();
|
||||
}
|
||||
|
||||
private Vector2 generateChannels() throws InterruptedException {
|
||||
|
@ -155,11 +158,25 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
|||
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<Shape> frame) {
|
||||
try {
|
||||
|
|
|
@ -7,6 +7,8 @@ import java.util.concurrent.Callable;
|
|||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public interface AudioEngine {
|
||||
boolean isPlaying();
|
||||
|
||||
void play(Callable<Vector2> channelGenerator, ReentrantLock renderLock, AudioDevice device);
|
||||
|
||||
void stop();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Vector2> channelGenerator;
|
||||
|
||||
|
@ -41,8 +43,14 @@ public class XtAudioEngine implements AudioEngine {
|
|||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return playing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play(Callable<Vector2> 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;
|
||||
|
|
|
@ -54,14 +54,15 @@ public class Controller implements Initializable, FrequencyListener, Listener {
|
|||
private final AudioPlayer<List<Shape>> 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<List<Shape>> analyser;
|
||||
private final AudioDevice defaultDevice;
|
||||
private FrameProducer<List<Shape>> 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<List<Shape>> 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<List<Shape>> 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");
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -11,14 +11,14 @@
|
|||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane prefHeight="498.0" prefWidth="837.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<AnchorPane id="control-pane" layoutX="422.0" layoutY="19.0" prefHeight="185.0" prefWidth="402.0">
|
||||
<AnchorPane id="control-pane" layoutX="422.0" layoutY="10.0" prefHeight="195.0" prefWidth="402.0">
|
||||
<children>
|
||||
<Button fx:id="chooseFileButton" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Choose File" />
|
||||
<Label fx:id="fileLabel" layoutX="146.0" layoutY="19.0" maxWidth="270.0" prefHeight="18.0" prefWidth="246.0" text="cube.obj" />
|
||||
<Button fx:id="recordButton" layoutX="14.0" layoutY="54.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Record" />
|
||||
<Label fx:id="recordLabel" layoutX="146.0" layoutY="59.0" maxWidth="270.0" prefHeight="18.0" prefWidth="245.0" />
|
||||
<Label id="frequency" fx:id="frequencyLabel" layoutX="14.0" layoutY="113.0" prefHeight="58.0" prefWidth="376.0" text="L/R Frequency: " />
|
||||
<ComboBox fx:id="deviceComboBox" layoutX="169.0" layoutY="113.0" prefHeight="26.0" prefWidth="221.0" />
|
||||
<Button fx:id="chooseFileButton" layoutX="14.0" layoutY="53.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Choose File" />
|
||||
<Label fx:id="fileLabel" layoutX="146.0" layoutY="58.0" maxWidth="270.0" prefHeight="18.0" prefWidth="246.0" text="cube.obj" />
|
||||
<Button fx:id="recordButton" layoutX="14.0" layoutY="93.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Record" />
|
||||
<Label fx:id="recordLabel" layoutX="146.0" layoutY="98.0" maxWidth="270.0" prefHeight="18.0" prefWidth="245.0" />
|
||||
<Label id="frequency" fx:id="frequencyLabel" layoutX="14.0" layoutY="131.0" prefHeight="58.0" prefWidth="376.0" text="L/R Frequency: " />
|
||||
<ComboBox fx:id="deviceComboBox" layoutX="14.0" layoutY="11.0" prefHeight="26.0" prefWidth="376.0" />
|
||||
</children>
|
||||
</AnchorPane>
|
||||
<TitledPane animated="false" collapsible="false" layoutX="422.0" layoutY="213.0" prefHeight="272.0" prefWidth="402.0" text="Effects">
|
||||
|
@ -55,7 +55,7 @@
|
|||
<CheckBox fx:id="rotateCheckBox" layoutX="90.0" layoutY="147.0" mnemonicParsing="false" text="Rotate with Mouse (Esc to disable)" />
|
||||
</AnchorPane>
|
||||
</TitledPane>
|
||||
<TitledPane collapsible="false" layoutX="11.0" layoutY="18.0" prefHeight="248.0" prefWidth="402.0" text="Image settings">
|
||||
<TitledPane collapsible="false" layoutX="11.0" layoutY="9.0" prefHeight="257.0" prefWidth="402.0" text="Image settings">
|
||||
<content>
|
||||
<AnchorPane minHeight="0.0" minWidth="0.0">
|
||||
<Slider fx:id="rotateSpeedSlider" blockIncrement="0.05" layoutX="116.0" layoutY="90.0" majorTickUnit="1.0" max="10.0" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" />
|
||||
|
|
Ładowanie…
Reference in New Issue