Refactor legacy AudioPlayer effects and reduce coupling of Controller

pull/35/head
James Ball 2021-05-15 18:35:53 +01:00
rodzic 954b8838d2
commit 46d7868c9f
10 zmienionych plików z 154 dodań i 116 usunięć

Wyświetl plik

@ -6,7 +6,7 @@
<groupId>sh.ball</groupId>
<artifactId>osci-render</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>
<name>osci-render</name>

Wyświetl plik

@ -1,6 +1,7 @@
package sh.ball.audio;
import sh.ball.audio.effect.Effect;
import xt.audio.*;
import xt.audio.Enums.XtSample;
import xt.audio.Enums.XtSetup;
import xt.audio.Enums.XtSystem;
@ -11,12 +12,6 @@ import xt.audio.Structs.XtDeviceStreamParams;
import xt.audio.Structs.XtFormat;
import xt.audio.Structs.XtMix;
import xt.audio.Structs.XtStreamParams;
import xt.audio.XtAudio;
import xt.audio.XtDevice;
import xt.audio.XtPlatform;
import xt.audio.XtSafeBuffer;
import xt.audio.XtService;
import xt.audio.XtStream;
import java.util.HashMap;
import java.util.Map;
@ -30,38 +25,24 @@ import java.util.List;
public class AudioPlayer implements Renderer<List<Shape>> {
private static final int SAMPLE_RATE = 192000;
private static final int BUFFER_SIZE = 20;
private final XtMix MIX = new XtMix(SAMPLE_RATE, XtSample.FLOAT32);
private final XtChannels CHANNELS = new XtChannels(0, 0, 2, 0);
private final XtFormat FORMAT = new XtFormat(MIX, CHANNELS);
private final XtFormat format;
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
private final Map<Object, Effect> effects = new HashMap<>();
private Map<Object, Effect> effects = new HashMap<>();
private List<Shape> frame;
private int currentShape = 0;
private int audioFramesDrawn = 0;
private double translateSpeed = 0;
private Vector2 translateVector = new Vector2();
private final Phase translatePhase = new Phase();
private double rotateSpeed = 0;
private final Phase rotatePhase = new Phase();
private double scale = 1;
private double weight = Shape.DEFAULT_WEIGHT;
private volatile boolean stopped;
public AudioPlayer() {
}
public AudioPlayer(double rotateSpeed, double translateSpeed, Vector2 translateVector, double scale, double weight) {
setRotationSpeed(rotateSpeed);
setTranslationSpeed(translateSpeed);
setTranslation(translateVector);
setScale(scale);
setQuality(weight);
public AudioPlayer(int sampleRate) {
XtMix mix = new XtMix(sampleRate, XtSample.FLOAT32);
XtChannels channels = new XtChannels(0, 0, 2, 0);
this.format = new XtFormat(mix, channels);
}
private int render(XtStream stream, XtBuffer buffer, Object user) throws InterruptedException {
@ -73,16 +54,13 @@ public class AudioPlayer implements Renderer<List<Shape>> {
Shape shape = getCurrentShape();
shape = shape.setWeight(weight);
shape = scale(shape);
shape = rotate(shape, FORMAT.mix.rate);
shape = translate(shape, FORMAT.mix.rate);
double totalAudioFrames = shape.getWeight() * shape.getLength();
double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames;
Vector2 nextVector = applyEffects(f, shape.nextVector(drawingProgress));
output[f * FORMAT.channels.outputs] = (float) nextVector.getX();
output[f * FORMAT.channels.outputs + 1] = (float) nextVector.getY();
output[f * format.channels.outputs] = (float) nextVector.getX();
output[f * format.channels.outputs + 1] = (float) nextVector.getY();
audioFramesDrawn++;
@ -107,60 +85,6 @@ public class AudioPlayer implements Renderer<List<Shape>> {
return vector;
}
private Shape rotate(Shape shape, double sampleRate) {
if (rotateSpeed != 0) {
shape = shape.rotate(
nextTheta(sampleRate, rotateSpeed, translatePhase)
);
}
return shape;
}
private Shape translate(Shape shape, double sampleRate) {
if (translateSpeed != 0 && !translateVector.equals(new Vector2())) {
return shape.translate(translateVector.scale(
Math.sin(nextTheta(sampleRate, translateSpeed, rotatePhase))
));
}
return shape;
}
private double nextTheta(double sampleRate, double frequency, Phase phase) {
phase.value += frequency / sampleRate;
if (phase.value >= 1.0) {
phase.value = -1.0;
}
return phase.value * Math.PI;
}
private Shape scale(Shape shape) {
if (scale != 1) {
return shape.scale(scale);
}
return shape;
}
public void setRotationSpeed(double speed) {
this.rotateSpeed = speed;
}
public void setTranslation(Vector2 translation) {
this.translateVector = translation;
}
public void setTranslationSpeed(double speed) {
translateSpeed = speed;
}
public void setScale(double scale) {
this.scale = scale;
}
@Override
public void setQuality(double quality) {
this.weight = quality;
@ -191,11 +115,11 @@ public class AudioPlayer implements Renderer<List<Shape>> {
if (defaultOutput == null) return;
try (XtDevice device = service.openDevice(defaultOutput)) {
if (device.supportsFormat(FORMAT)) {
if (device.supportsFormat(format)) {
XtBufferSize size = device.getBufferSize(FORMAT);
XtBufferSize size = device.getBufferSize(format);
XtStreamParams streamParams = new XtStreamParams(true, this::render, null, null);
XtDeviceStreamParams deviceParams = new XtDeviceStreamParams(streamParams, FORMAT, size.current);
XtDeviceStreamParams deviceParams = new XtDeviceStreamParams(streamParams, format, size.current);
try (XtStream stream = device.openStream(deviceParams, null);
XtSafeBuffer safe = XtSafeBuffer.register(stream, true)) {
stream.start();
@ -239,9 +163,4 @@ public class AudioPlayer implements Renderer<List<Shape>> {
effects.remove(identifier);
}
private static final class Phase {
private double value = 0;
}
}

Wyświetl plik

@ -2,5 +2,8 @@ package sh.ball.audio.effect;
public enum EffectType {
VECTOR_CANCELLING,
BIT_CRUSH
BIT_CRUSH,
SCALE,
ROTATE,
TRANSLATE
}

Wyświetl plik

@ -0,0 +1,28 @@
package sh.ball.audio.effect;
public abstract class PhaseEffect implements Effect {
protected final int sampleRate;
protected double speed;
private double phase;
protected PhaseEffect(int sampleRate, double speed) {
this.sampleRate = sampleRate;
this.speed = speed;
}
protected double nextTheta() {
phase += speed / sampleRate;
if (phase >= 1.0) {
phase = -1.0;
}
return phase * Math.PI;
}
public void setSpeed(double speed) {
this.speed = speed;
}
}

Wyświetl plik

@ -0,0 +1,23 @@
package sh.ball.audio.effect;
import sh.ball.shapes.Vector2;
public class RotateEffect extends PhaseEffect {
public RotateEffect(int sampleRate, double speed) {
super(sampleRate, speed);
}
public RotateEffect(int sampleRate) {
this(sampleRate, 0);
}
@Override
public Vector2 apply(int count, Vector2 vector) {
if (speed != 0) {
return vector.rotate(nextTheta());
}
return vector;
}
}

Wyświetl plik

@ -0,0 +1,25 @@
package sh.ball.audio.effect;
import sh.ball.shapes.Vector2;
public class ScaleEffect implements Effect {
private double scale;
public ScaleEffect(double scale) {
this.scale = scale;
}
public ScaleEffect() {
this(1);
}
public void setScale(double scale) {
this.scale = scale;
}
@Override
public Vector2 apply(int count, Vector2 vector) {
return vector.scale(scale);
}
}

Wyświetl plik

@ -0,0 +1,30 @@
package sh.ball.audio.effect;
import sh.ball.shapes.Vector2;
public class TranslateEffect extends PhaseEffect {
private Vector2 translation;
public TranslateEffect(int sampleRate, double speed, Vector2 translation) {
super(sampleRate, speed);
this.translation = translation;
}
public TranslateEffect(int sampleRate) {
this(sampleRate, 0, new Vector2());
}
@Override
public Vector2 apply(int count, Vector2 vector) {
if (speed != 0 && !translation.equals(new Vector2())) {
return vector.translate(translation.scale(Math.sin(nextTheta())));
}
return vector;
}
public void setTranslation(Vector2 translation) {
this.translation = translation;
}
}

Wyświetl plik

@ -1,8 +1,8 @@
package sh.ball.gui;
import javafx.scene.control.*;
import sh.ball.audio.AudioPlayer;
import sh.ball.audio.effect.Effect;
import sh.ball.audio.Renderer;
import sh.ball.audio.effect.*;
import sh.ball.audio.FrameProducer;
import java.io.File;
@ -24,8 +24,6 @@ import javafx.stage.Stage;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import sh.ball.audio.effect.EffectType;
import sh.ball.audio.effect.EventFactory;
import sh.ball.engine.Vector3;
import sh.ball.parser.obj.ObjFrameSettings;
import sh.ball.parser.obj.ObjParser;
@ -35,17 +33,18 @@ import sh.ball.shapes.Vector2;
public class Controller implements Initializable {
private static final int SAMPLE_RATE = 192000;
private static final InputStream DEFAULT_OBJ = Controller.class.getResourceAsStream("/models/cube.obj");
private final FileChooser fileChooser = new FileChooser();
// TODO: Reduce coupling on AudioPlayer
private final AudioPlayer renderer = new AudioPlayer();
private final Renderer<List<Shape>> renderer;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private FrameProducer<List<Shape>> producer = new FrameProducer<>(
renderer,
new ObjParser(DEFAULT_OBJ).parse()
);
private final RotateEffect rotateEffect = new RotateEffect(SAMPLE_RATE);
private final TranslateEffect translateEffect = new TranslateEffect(SAMPLE_RATE);
private final ScaleEffect scaleEffect = new ScaleEffect();
private FrameProducer<List<Shape>> producer;
private Stage stage;
@ -94,7 +93,12 @@ public class Controller implements Initializable {
@FXML
private Slider bitCrushSlider;
public Controller() throws IOException {
public Controller(Renderer<List<Shape>> renderer) throws IOException {
this.renderer = renderer;
this.producer = new FrameProducer<>(
renderer,
new ObjParser(DEFAULT_OBJ).parse()
);
}
private Map<Slider, SliderUpdater<Double>> initializeSliderMap() {
@ -102,11 +106,11 @@ public class Controller implements Initializable {
weightSlider,
new SliderUpdater<>(weightLabel::setText, renderer::setQuality),
rotateSpeedSlider,
new SliderUpdater<>(rotateSpeedLabel::setText, renderer::setRotationSpeed),
new SliderUpdater<>(rotateSpeedLabel::setText, rotateEffect::setSpeed),
translationSpeedSlider,
new SliderUpdater<>(translationSpeedLabel::setText, renderer::setTranslationSpeed),
new SliderUpdater<>(translationSpeedLabel::setText, translateEffect::setSpeed),
scaleSlider,
new SliderUpdater<>(scaleLabel::setText, renderer::setScale),
new SliderUpdater<>(scaleLabel::setText, scaleEffect::setScale),
focalLengthSlider,
new SliderUpdater<>(focalLengthLabel::setText, this::setFocalLength)
);
@ -135,7 +139,7 @@ public class Controller implements Initializable {
}
InvalidationListener translationUpdate = observable ->
renderer.setTranslation(new Vector2(
translateEffect.setTranslation(new Vector2(
tryParse(translationXTextField.getText()),
tryParse(translationYTextField.getText())
));
@ -150,7 +154,6 @@ public class Controller implements Initializable {
tryParse(cameraZTextField.getText())
)));
cameraXTextField.textProperty().addListener(cameraPosUpdate);
cameraYTextField.textProperty().addListener(cameraPosUpdate);
cameraZTextField.textProperty().addListener(cameraPosUpdate);
@ -175,6 +178,10 @@ public class Controller implements Initializable {
}
});
renderer.addEffect(EffectType.SCALE, scaleEffect);
renderer.addEffect(EffectType.ROTATE, rotateEffect);
renderer.addEffect(EffectType.TRANSLATE, translateEffect);
executor.submit(producer);
new Thread(renderer).start();
}

Wyświetl plik

@ -7,17 +7,20 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import sh.ball.audio.AudioPlayer;
import java.util.Objects;
public class Gui extends Application {
private static final int SAMPLE_RATE = 192000;
@Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/osci-render.fxml"));
Controller controller = new Controller(new AudioPlayer(SAMPLE_RATE));
loader.setController(controller);
Parent root = loader.load();
Controller controller = loader.getController();
controller.setStage(stage);
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/icons/icon.png"))));
stage.setTitle("osci-render");

Wyświetl plik

@ -12,7 +12,7 @@
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<GridPane alignment="center" hgap="10" prefWidth="400.0" vgap="10" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sh.ball.gui.Controller">
<GridPane alignment="center" hgap="10" prefWidth="400.0" vgap="10" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints />
</columnConstraints>
@ -26,9 +26,9 @@
<Slider fx:id="rotateSpeedSlider" layoutX="116.0" layoutY="79.0" max="10.0" prefHeight="14.0" prefWidth="198.0" />
<Label layoutX="37.0" layoutY="77.0" text="Rotate speed" />
<Label fx:id="rotateSpeedLabel" layoutX="316.0" layoutY="77.0" maxWidth="40.0" text="0" />
<Slider fx:id="translationSpeedSlider" layoutX="116.0" layoutY="105.0" max="10.0" prefHeight="14.0" prefWidth="198.0" value="1.0" />
<Slider fx:id="translationSpeedSlider" layoutX="116.0" layoutY="105.0" max="10.0" prefHeight="14.0" prefWidth="198.0" />
<Label layoutX="14.0" layoutY="103.0" text="Translation speed" />
<Label fx:id="translationSpeedLabel" layoutX="316.0" layoutY="103.0" maxWidth="40.0" text="1" />
<Label fx:id="translationSpeedLabel" layoutX="316.0" layoutY="103.0" maxWidth="40.0" text="0" />
<Slider fx:id="scaleSlider" layoutX="116.0" layoutY="130.0" max="10.0" prefHeight="14.0" prefWidth="198.0" value="1.0" />
<Label layoutX="80.0" layoutY="128.0" text="Scale" />
<Label fx:id="scaleLabel" layoutX="316.0" layoutY="128.0" maxWidth="40.0" text="1" />