Merge pull request #20 from jameshball/master

Release v1.1.2 minor changes
pull/35/head
James H Ball 2021-05-15 18:37:19 +01:00 zatwierdzone przez GitHub
commit d0f0c19e64
12 zmienionych plików z 209 dodań i 122 usunięć

Wyświetl plik

@ -6,7 +6,7 @@ Program for drawing objects, text, and images on an oscilloscope using audio out
This allows for 3D rendering of `.obj` files, `.svg` images, and `.txt` files.
Lots of this code was built as part of a 24hr hackathon: IC Hack 20. The original repository can be found here: https://github.com/wdhg/ICHack20 It won 'Best Newcomers Prize' at the event.
Some of this was built as part of a 24hr hackathon: IC Hack 20. The original repository can be found here: https://github.com/wdhg/ICHack20 It won 'Best Newcomers Prize' at the event.
### Video Demonstration
@ -18,9 +18,9 @@ Lots of this code was built as part of a 24hr hackathon: IC Hack 20. The origina
- Render `.svg` files
- Render text
- Rotation of objects
- Scaling image
- Translating image
- GUI for controlling renderer
- Scaling images
- Translating images
- Applying image effects
## Proposed Features
@ -34,7 +34,7 @@ Lots of this code was built as part of a 24hr hackathon: IC Hack 20. The origina
Using osci-render is very easy; run the program and choose the file you would like to render, and it will output as audio to visualise on your oscilloscope.
By default, the program loads the `cube.obj` example. If this is working, you're good to go and should be able to load your own objects, files, or images!
By default, the program loads the example rotating cube. If this is working, you're good to go and should be able to load your own objects, files, or images!
Control the output using the sliders and text boxes provided. Currently the following can be controlled:
@ -48,9 +48,58 @@ There are some additional controls for `.obj` files:
- Focal length of camera
- Position of camera
## Screenshots
<img width="250px" height="396px" src="gui.png">
## Running
Head over to [Releases](https://github.com/jameshball/osci-render/releases) and download the latest `.exe` or `.jar`.
`.exe` is highly recommended if you're on Windows as it is much simpler to get up and running.
### Running using .exe
- Download the latest `osci-render-VERSION.exe` from [Releases](https://github.com/jameshball/osci-render/releases)
- Open the `.exe` skipping any Windows security warnings (at your own risk!)
- It should open briefly and then close without any user input
- Check your start menu for `osci-render` or open `osci-render.exe` at `C:\Program Files\osci-render`
- Start rendering!
Updating to later versions is as simple as running the latest `osci-render-VERSION.exe` again.
To uninstall, use Windows control panel, as you would expect.
### Running using .jar
- Download the latest `osci-render-VERSION.jar` from [Releases](https://github.com/jameshball/osci-render/releases)
- Download and install [Java 15 or later](https://www.oracle.com/java/technologies/javase-jdk16-downloads.html)
- Donwload and unpack [JavaFX 16 or later](https://gluonhq.com/products/javafx/)
- Make sure you scroll down to `Latest Release`
- Download the SDK for your platform
- Unpack the `.zip` at your root directory (e.g. `C:\javafx-sdk-16` for me on Windows)
- The `lib` subfolder should be located at `/javafx-sdk-16/lib`
- Run the following command from your terminal to run the `.jar`, substituting the correct paths
- `java --enable-preview --module-path /javafx-sdk-16/lib --add-modules=javafx.controls,javafx.fxml -jar "path/to/osci-render-VERSION.jar"`
- Start rendering!
## Building
All dependencies are specified in the `pom.xml` file. Cloning this repo and using IntelliJ with Maven should make building a painless process.
I am using Maven for dependency management and to package the program. Doing the following will setup the project. I highly recommend using IntelliJ.
- Download and install [Java 15 or later](https://www.oracle.com/java/technologies/javase-jdk16-downloads.html)
- Run `git clone git@github.com:jameshball/osci-render.git`
- `cd` into the `osci-render` directory
- Run `mvn package`
- Open the project in IntelliJ
- Add `target/modules` as a library
- Right click the folder in IntelliJ and `Add as Library...`
- Select `osci-render` in the `Add to module` dropdown
- Navigate to `File -> Project Structure`
- Remove all external Maven libraries other than the `modules` folder just added
- You're good to go!
You should now be able to run `sh.ball.gui.Gui` and start the program 😊
## Contact

BIN
gui.png 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 28 KiB

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" />