kopia lustrzana https://github.com/jameshball/osci-render
commit
a1539eb6f8
|
@ -6,3 +6,6 @@
|
|||
|
||||
# Ignore local binaries for xt-audio
|
||||
/win32-x64
|
||||
|
||||
# Ignore any .wav file output
|
||||
*.wav
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -6,7 +6,7 @@
|
|||
|
||||
<groupId>sh.ball</groupId>
|
||||
<artifactId>osci-render</artifactId>
|
||||
<version>1.2.1</version>
|
||||
<version>1.3.0</version>
|
||||
|
||||
<name>osci-render</name>
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import xt.audio.Structs.XtFormat;
|
|||
import xt.audio.Structs.XtMix;
|
||||
import xt.audio.Structs.XtStreamParams;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
|
@ -21,16 +22,24 @@ import java.util.concurrent.BlockingQueue;
|
|||
import sh.ball.shapes.Shape;
|
||||
import sh.ball.shapes.Vector2;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class AudioPlayer implements Renderer<List<Shape>> {
|
||||
public class AudioPlayer implements Renderer<List<Shape>, AudioInputStream> {
|
||||
|
||||
private static final int BUFFER_SIZE = 20;
|
||||
private static final int BITS_PER_SAMPLE = 16;
|
||||
private static final boolean SIGNED = true;
|
||||
private static final boolean BIG_ENDIAN = false;
|
||||
|
||||
private final XtFormat format;
|
||||
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
|
||||
private final Map<Object, Effect> effects = new HashMap<>();
|
||||
|
||||
private ByteArrayOutputStream outputStream;
|
||||
private boolean recording = false;
|
||||
private int framesRecorded = 0;
|
||||
private List<Shape> frame;
|
||||
private int currentShape = 0;
|
||||
private int audioFramesDrawn = 0;
|
||||
|
@ -45,22 +54,35 @@ public class AudioPlayer implements Renderer<List<Shape>> {
|
|||
this.format = new XtFormat(mix, channels);
|
||||
}
|
||||
|
||||
private int render(XtStream stream, XtBuffer buffer, Object user) throws InterruptedException {
|
||||
private int render(XtStream stream, XtBuffer buffer, Object user) throws InterruptedException, IOException {
|
||||
XtSafeBuffer safe = XtSafeBuffer.get(stream);
|
||||
safe.lock(buffer);
|
||||
float[] output = (float[]) safe.getOutput();
|
||||
|
||||
for (int f = 0; f < buffer.frames; f++) {
|
||||
Shape shape = getCurrentShape();
|
||||
|
||||
shape = shape.setWeight(weight);
|
||||
Shape shape = getCurrentShape().setWeight(weight);
|
||||
|
||||
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();
|
||||
float nextX = cutoff((float) nextVector.getX());
|
||||
float nextY = cutoff((float) nextVector.getY());
|
||||
|
||||
output[f * format.channels.outputs] = nextX;
|
||||
output[f * format.channels.outputs + 1] = nextY;
|
||||
|
||||
if (recording) {
|
||||
int left = (int)(nextX * Short.MAX_VALUE);
|
||||
int right = (int)(nextY * Short.MAX_VALUE);
|
||||
|
||||
outputStream.write((byte) left);
|
||||
outputStream.write((byte)(left >> 8));
|
||||
outputStream.write((byte) right);
|
||||
outputStream.write((byte)(right >> 8));
|
||||
|
||||
framesRecorded++;
|
||||
}
|
||||
|
||||
audioFramesDrawn++;
|
||||
|
||||
|
@ -78,6 +100,15 @@ public class AudioPlayer implements Renderer<List<Shape>> {
|
|||
return 0;
|
||||
}
|
||||
|
||||
private float cutoff(float value) {
|
||||
if (value < -1) {
|
||||
return -1;
|
||||
} else if (value > 1) {
|
||||
return 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private Vector2 applyEffects(int frame, Vector2 vector) {
|
||||
for (Effect effect : effects.values()) {
|
||||
vector = effect.apply(frame, vector);
|
||||
|
@ -163,4 +194,21 @@ public class AudioPlayer implements Renderer<List<Shape>> {
|
|||
effects.remove(identifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startRecord() {
|
||||
outputStream = new ByteArrayOutputStream();
|
||||
framesRecorded = 0;
|
||||
recording = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioInputStream stopRecord() {
|
||||
recording = false;
|
||||
byte[] input = outputStream.toByteArray();
|
||||
outputStream = null;
|
||||
|
||||
AudioFormat audioFormat = new AudioFormat(format.mix.rate, BITS_PER_SAMPLE, format.channels.outputs, SIGNED, BIG_ENDIAN);
|
||||
|
||||
return new AudioInputStream(new ByteArrayInputStream(input), audioFormat, framesRecorded);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package sh.ball.audio;
|
||||
|
||||
public class FrameProducer<T> implements Runnable {
|
||||
public class FrameProducer<S, T> implements Runnable {
|
||||
|
||||
private final Renderer<T> renderer;
|
||||
private final FrameSet<T> frames;
|
||||
private final Renderer<S, T> renderer;
|
||||
private final FrameSet<S> frames;
|
||||
|
||||
private boolean running;
|
||||
|
||||
public FrameProducer(Renderer<T> renderer, FrameSet<T> frames) {
|
||||
public FrameProducer(Renderer<S, T> renderer, FrameSet<S> frames) {
|
||||
this.renderer = renderer;
|
||||
this.frames = frames;
|
||||
}
|
||||
|
@ -24,14 +24,7 @@ public class FrameProducer<T> implements Runnable {
|
|||
running = false;
|
||||
}
|
||||
|
||||
public Object setFrameSettings(Object settings) {
|
||||
return setFrameSettings(settings, false);
|
||||
}
|
||||
|
||||
public Object setFrameSettings(Object settings, boolean flushFrames) {
|
||||
if (flushFrames) {
|
||||
renderer.flushFrames();
|
||||
}
|
||||
return frames.setFrameSettings(settings);
|
||||
public void setFrameSettings(Object settings) {
|
||||
frames.setFrameSettings(settings);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,5 +4,5 @@ public interface FrameSet<T> {
|
|||
|
||||
T next();
|
||||
|
||||
Object setFrameSettings(Object settings);
|
||||
void setFrameSettings(Object settings);
|
||||
}
|
||||
|
|
|
@ -2,17 +2,21 @@ package sh.ball.audio;
|
|||
|
||||
import sh.ball.audio.effect.Effect;
|
||||
|
||||
public interface Renderer<T> extends Runnable {
|
||||
public interface Renderer<S, T> extends Runnable {
|
||||
|
||||
void stop();
|
||||
|
||||
void setQuality(double quality);
|
||||
|
||||
void addFrame(T frame);
|
||||
void addFrame(S frame);
|
||||
|
||||
void flushFrames();
|
||||
|
||||
void addEffect(Object identifier, Effect effect);
|
||||
|
||||
void removeEffect(Object identifier);
|
||||
|
||||
void startRecord();
|
||||
|
||||
T stopRecord();
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
@ -26,12 +28,14 @@ import javafx.fxml.Initializable;
|
|||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javax.sound.sampled.AudioFileFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.xml.sax.SAXException;
|
||||
import sh.ball.audio.effect.TranslateEffect;
|
||||
import sh.ball.engine.Vector3;
|
||||
import sh.ball.parser.obj.ObjFrameSettings;
|
||||
import sh.ball.parser.obj.ObjSettingsFactory;
|
||||
import sh.ball.parser.obj.ObjParser;
|
||||
import sh.ball.parser.ParserFactory;
|
||||
|
@ -45,14 +49,15 @@ public class Controller implements Initializable {
|
|||
private static final double DEFAULT_ROTATE_SPEED = 0.1;
|
||||
|
||||
private final FileChooser fileChooser = new FileChooser();
|
||||
private final Renderer<List<Shape>> renderer;
|
||||
private final Renderer<List<Shape>, AudioInputStream> renderer;
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
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 FrameProducer<List<Shape>, AudioInputStream> producer;
|
||||
private boolean recording = false;
|
||||
|
||||
private Stage stage;
|
||||
|
||||
|
@ -61,6 +66,10 @@ public class Controller implements Initializable {
|
|||
@FXML
|
||||
private Label fileLabel;
|
||||
@FXML
|
||||
private Button recordButton;
|
||||
@FXML
|
||||
private Label recordLabel;
|
||||
@FXML
|
||||
private TextField translationXTextField;
|
||||
@FXML
|
||||
private TextField translationYTextField;
|
||||
|
@ -101,7 +110,7 @@ public class Controller implements Initializable {
|
|||
@FXML
|
||||
private Slider bitCrushSlider;
|
||||
|
||||
public Controller(Renderer<List<Shape>> renderer) throws IOException {
|
||||
public Controller(Renderer<List<Shape>, AudioInputStream> renderer) throws IOException {
|
||||
this.renderer = renderer;
|
||||
this.producer = new FrameProducer<>(
|
||||
renderer,
|
||||
|
@ -163,7 +172,7 @@ public class Controller implements Initializable {
|
|||
tryParse(cameraXTextField.getText()),
|
||||
tryParse(cameraYTextField.getText()),
|
||||
tryParse(cameraZTextField.getText())
|
||||
)), true);
|
||||
)));
|
||||
|
||||
cameraXTextField.textProperty().addListener(cameraPosUpdate);
|
||||
cameraYTextField.textProperty().addListener(cameraPosUpdate);
|
||||
|
@ -195,6 +204,15 @@ public class Controller implements Initializable {
|
|||
bitCrushSlider.valueProperty().addListener(bitCrushListener);
|
||||
bitCrushCheckBox.selectedProperty().addListener(bitCrushListener);
|
||||
|
||||
fileChooser.setInitialFileName("out.wav");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", "*.*"),
|
||||
new FileChooser.ExtensionFilter("WAV Files", "*.wav"),
|
||||
new FileChooser.ExtensionFilter("Wavefront OBJ Files", "*.obj"),
|
||||
new FileChooser.ExtensionFilter("SVG Files", "*.svg"),
|
||||
new FileChooser.ExtensionFilter("Text Files", "*.txt")
|
||||
);
|
||||
|
||||
chooseFileButton.setOnAction(e -> {
|
||||
File file = fileChooser.showOpenDialog(stage);
|
||||
if (file != null) {
|
||||
|
@ -202,6 +220,8 @@ public class Controller implements Initializable {
|
|||
}
|
||||
});
|
||||
|
||||
recordButton.setOnAction(event -> toggleRecord());
|
||||
|
||||
setObjectRotateSpeed(DEFAULT_ROTATE_SPEED);
|
||||
|
||||
renderer.addEffect(EffectType.SCALE, scaleEffect);
|
||||
|
@ -212,14 +232,34 @@ public class Controller implements Initializable {
|
|||
new Thread(renderer).start();
|
||||
}
|
||||
|
||||
private void toggleRecord() {
|
||||
recording = !recording;
|
||||
if (recording) {
|
||||
recordLabel.setText("Recording...");
|
||||
recordButton.setText("Stop Recording");
|
||||
renderer.startRecord();
|
||||
} else {
|
||||
recordButton.setText("Record");
|
||||
AudioInputStream input = renderer.stopRecord();
|
||||
try {
|
||||
File file = fileChooser.showSaveDialog(stage);
|
||||
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");
|
||||
}
|
||||
AudioSystem.write(input, AudioFileFormat.Type.WAVE, file);
|
||||
input.close();
|
||||
recordLabel.setText("Saved to " + file.getAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
recordLabel.setText("Error saving file");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setFocalLength(double focalLength) {
|
||||
Vector3 pos = (Vector3) producer.setFrameSettings(
|
||||
ObjSettingsFactory.focalLength(focalLength),
|
||||
true
|
||||
);
|
||||
cameraXTextField.setText(String.valueOf(pos.getX()));
|
||||
cameraYTextField.setText(String.valueOf(pos.getY()));
|
||||
cameraZTextField.setText(String.valueOf(pos.getZ()));
|
||||
producer.setFrameSettings(ObjSettingsFactory.focalLength(focalLength));
|
||||
}
|
||||
|
||||
private void setObjectRotateSpeed(double rotateSpeed) {
|
||||
|
|
|
@ -12,15 +12,13 @@ public class ObjFrameSet implements FrameSet<List<Shape>> {
|
|||
|
||||
private final WorldObject object;
|
||||
private final Camera camera;
|
||||
private final boolean isDefaultPosition;
|
||||
|
||||
private Vector3 rotation = new Vector3();
|
||||
private Double rotateSpeed = 0.0;
|
||||
|
||||
public ObjFrameSet(WorldObject object, Camera camera, boolean isDefaultPosition) {
|
||||
public ObjFrameSet(WorldObject object, Camera camera) {
|
||||
this.object = object;
|
||||
this.camera = camera;
|
||||
this.isDefaultPosition = isDefaultPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -31,13 +29,10 @@ public class ObjFrameSet implements FrameSet<List<Shape>> {
|
|||
|
||||
// TODO: Refactor!
|
||||
@Override
|
||||
public Object setFrameSettings(Object settings) {
|
||||
public void setFrameSettings(Object settings) {
|
||||
if (settings instanceof ObjFrameSettings obj) {
|
||||
if (obj.focalLength != null && camera.getFocalLength() != obj.focalLength) {
|
||||
camera.setFocalLength(obj.focalLength);
|
||||
if (isDefaultPosition) {
|
||||
camera.findZPos(object);
|
||||
}
|
||||
}
|
||||
if (obj.cameraPos != null && camera.getPos() != obj.cameraPos) {
|
||||
camera.setPos(obj.cameraPos);
|
||||
|
@ -52,7 +47,5 @@ public class ObjFrameSet implements FrameSet<List<Shape>> {
|
|||
object.resetRotation();
|
||||
}
|
||||
}
|
||||
|
||||
return camera.getPos();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,12 +50,9 @@ public class ObjParser extends FileParser<FrameSet<List<Shape>>> {
|
|||
@Override
|
||||
public FrameSet<List<Shape>> parse() throws IllegalArgumentException, IOException {
|
||||
object = new WorldObject(input);
|
||||
camera.findZPos(object);
|
||||
|
||||
if (isDefaultPosition) {
|
||||
camera.findZPos(object);
|
||||
}
|
||||
|
||||
return new ObjFrameSet(object, camera, isDefaultPosition);
|
||||
return new ObjFrameSet(object, camera);
|
||||
}
|
||||
|
||||
// If camera position arguments haven't been specified, automatically work out the position of
|
||||
|
|
|
@ -40,9 +40,7 @@ public abstract class Shape implements FrameSet<List<Shape>> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Object setFrameSettings(Object settings) {
|
||||
return null;
|
||||
}
|
||||
public void setFrameSettings(Object settings) {}
|
||||
|
||||
/* SHAPE HELPER FUNCTIONS */
|
||||
|
||||
|
|
|
@ -18,7 +18,5 @@ public class ShapeFrameSet implements FrameSet<List<Shape>> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Object setFrameSettings(Object settings) {
|
||||
return null;
|
||||
}
|
||||
public void setFrameSettings(Object settings) {}
|
||||
}
|
||||
|
|
|
@ -12,16 +12,16 @@
|
|||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
|
||||
<GridPane alignment="center" hgap="10" prefHeight="719.0" prefWidth="400.0" vgap="10" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<GridPane alignment="center" hgap="10" prefHeight="790.0" prefWidth="400.0" vgap="10" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints />
|
||||
</rowConstraints>
|
||||
<AnchorPane prefHeight="737.0" prefWidth="400.0">
|
||||
<Button fx:id="chooseFileButton" layoutX="8.0" layoutY="15.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="119.0" text="Choose File" />
|
||||
<SplitPane dividerPositions="0.31499312242090777, 0.6158872077028884" layoutX="6.0" layoutY="63.0" orientation="VERTICAL" prefHeight="649.0" prefWidth="388.0">
|
||||
<AnchorPane prefHeight="810.0" prefWidth="400.0">
|
||||
<Button fx:id="chooseFileButton" layoutX="8.0" layoutY="24.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Choose File" />
|
||||
<SplitPane dividerPositions="0.31499312242090777, 0.6158872077028884" layoutX="6.0" layoutY="118.0" orientation="VERTICAL" prefHeight="666.0" prefWidth="388.0">
|
||||
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="178.0" prefWidth="396.0">
|
||||
<Slider fx:id="rotateSpeedSlider" blockIncrement="0.05" layoutX="116.0" layoutY="90.0" majorTickUnit="1.0" max="10.0" prefHeight="38.0" prefWidth="247.0" showTickLabels="true" showTickMarks="true" />
|
||||
<Label layoutX="37.0" layoutY="88.0" text="Rotate speed" />
|
||||
|
@ -69,10 +69,12 @@
|
|||
<TextField fx:id="rotateYTextField" layoutX="218.0" layoutY="139.0" prefHeight="26.0" prefWidth="58.0" text="0" />
|
||||
<Label layoutX="283.0" layoutY="143.0" text="z :" />
|
||||
<TextField fx:id="rotateZTextField" layoutX="302.0" layoutY="139.0" prefHeight="26.0" prefWidth="58.0" text="0" />
|
||||
<Button fx:id="resetRotationButton" layoutX="145.0" layoutY="183.0" mnemonicParsing="false" text="Reset Rotation" />
|
||||
<Button fx:id="resetRotationButton" layoutX="145.0" layoutY="189.0" mnemonicParsing="false" text="Reset Rotation" />
|
||||
</AnchorPane>
|
||||
</TitledPane>
|
||||
</SplitPane>
|
||||
<Label fx:id="fileLabel" layoutX="134.0" layoutY="20.0" maxWidth="250.0" />
|
||||
<Label fx:id="fileLabel" layoutX="140.0" layoutY="29.0" maxWidth="250.0" />
|
||||
<Button fx:id="recordButton" layoutX="8.0" layoutY="68.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Record" />
|
||||
<Label fx:id="recordLabel" layoutX="140.0" layoutY="73.0" maxWidth="250.0" />
|
||||
</AnchorPane>
|
||||
</GridPane>
|
||||
|
|
Ładowanie…
Reference in New Issue