diff --git a/.gitignore b/.gitignore index 859a6b6..28e5329 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ # Ignore local binaries for xt-audio /win32-x64 + +# Ignore any .wav file output +*.wav diff --git a/pom.xml b/pom.xml index 0c11bde..3628642 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ sh.ball osci-render - 1.2.1 + 1.3.0 osci-render diff --git a/src/main/java/sh/ball/audio/AudioPlayer.java b/src/main/java/sh/ball/audio/AudioPlayer.java index 6a16bfa..921d8fe 100644 --- a/src/main/java/sh/ball/audio/AudioPlayer.java +++ b/src/main/java/sh/ball/audio/AudioPlayer.java @@ -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> { +public class AudioPlayer implements Renderer, 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> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE); private final Map effects = new HashMap<>(); + private ByteArrayOutputStream outputStream; + private boolean recording = false; + private int framesRecorded = 0; private List frame; private int currentShape = 0; private int audioFramesDrawn = 0; @@ -45,22 +54,35 @@ public class AudioPlayer implements Renderer> { 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> { 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> { 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); + } } diff --git a/src/main/java/sh/ball/audio/FrameProducer.java b/src/main/java/sh/ball/audio/FrameProducer.java index 4f2c236..02dafac 100644 --- a/src/main/java/sh/ball/audio/FrameProducer.java +++ b/src/main/java/sh/ball/audio/FrameProducer.java @@ -1,13 +1,13 @@ package sh.ball.audio; -public class FrameProducer implements Runnable { +public class FrameProducer implements Runnable { - private final Renderer renderer; - private final FrameSet frames; + private final Renderer renderer; + private final FrameSet frames; private boolean running; - public FrameProducer(Renderer renderer, FrameSet frames) { + public FrameProducer(Renderer renderer, FrameSet frames) { this.renderer = renderer; this.frames = frames; } @@ -24,14 +24,7 @@ public class FrameProducer 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); } } diff --git a/src/main/java/sh/ball/audio/FrameSet.java b/src/main/java/sh/ball/audio/FrameSet.java index 879bae1..e8a7e89 100644 --- a/src/main/java/sh/ball/audio/FrameSet.java +++ b/src/main/java/sh/ball/audio/FrameSet.java @@ -4,5 +4,5 @@ public interface FrameSet { T next(); - Object setFrameSettings(Object settings); + void setFrameSettings(Object settings); } diff --git a/src/main/java/sh/ball/audio/Renderer.java b/src/main/java/sh/ball/audio/Renderer.java index eb8028a..d3380eb 100644 --- a/src/main/java/sh/ball/audio/Renderer.java +++ b/src/main/java/sh/ball/audio/Renderer.java @@ -2,17 +2,21 @@ package sh.ball.audio; import sh.ball.audio.effect.Effect; -public interface Renderer extends Runnable { +public interface Renderer 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(); } diff --git a/src/main/java/sh/ball/gui/Controller.java b/src/main/java/sh/ball/gui/Controller.java index 04d919b..ac3d7e6 100644 --- a/src/main/java/sh/ball/gui/Controller.java +++ b/src/main/java/sh/ball/gui/Controller.java @@ -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> renderer; + private final Renderer, 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> producer; + private FrameProducer, 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> renderer) throws IOException { + public Controller(Renderer, 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) { diff --git a/src/main/java/sh/ball/parser/obj/ObjFrameSet.java b/src/main/java/sh/ball/parser/obj/ObjFrameSet.java index 2a8450c..672227b 100644 --- a/src/main/java/sh/ball/parser/obj/ObjFrameSet.java +++ b/src/main/java/sh/ball/parser/obj/ObjFrameSet.java @@ -12,15 +12,13 @@ public class ObjFrameSet implements FrameSet> { 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> { // 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> { object.resetRotation(); } } - - return camera.getPos(); } } diff --git a/src/main/java/sh/ball/parser/obj/ObjParser.java b/src/main/java/sh/ball/parser/obj/ObjParser.java index ba22bcb..68b39d6 100644 --- a/src/main/java/sh/ball/parser/obj/ObjParser.java +++ b/src/main/java/sh/ball/parser/obj/ObjParser.java @@ -50,12 +50,9 @@ public class ObjParser extends FileParser>> { @Override public FrameSet> 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 diff --git a/src/main/java/sh/ball/shapes/Shape.java b/src/main/java/sh/ball/shapes/Shape.java index 1e21a90..de39a5c 100644 --- a/src/main/java/sh/ball/shapes/Shape.java +++ b/src/main/java/sh/ball/shapes/Shape.java @@ -40,9 +40,7 @@ public abstract class Shape implements FrameSet> { } @Override - public Object setFrameSettings(Object settings) { - return null; - } + public void setFrameSettings(Object settings) {} /* SHAPE HELPER FUNCTIONS */ diff --git a/src/main/java/sh/ball/shapes/ShapeFrameSet.java b/src/main/java/sh/ball/shapes/ShapeFrameSet.java index b93be23..95b04a7 100644 --- a/src/main/java/sh/ball/shapes/ShapeFrameSet.java +++ b/src/main/java/sh/ball/shapes/ShapeFrameSet.java @@ -18,7 +18,5 @@ public class ShapeFrameSet implements FrameSet> { } @Override - public Object setFrameSettings(Object settings) { - return null; - } + public void setFrameSettings(Object settings) {} } diff --git a/src/main/resources/fxml/osci-render.fxml b/src/main/resources/fxml/osci-render.fxml index 80e159e..b71e2a6 100644 --- a/src/main/resources/fxml/osci-render.fxml +++ b/src/main/resources/fxml/osci-render.fxml @@ -12,16 +12,16 @@ - + - -