From d65261ca3cbdeab37e67383ace05822a919425a0 Mon Sep 17 00:00:00 2001 From: James Ball Date: Wed, 26 May 2021 20:44:18 +0100 Subject: [PATCH] Make line endings consistent and refresh GUI --- README.md | 2 +- gui.png | Bin 28835 -> 59042 bytes pom.xml | 574 +++--- src/main/java/META-INF/MANIFEST.MF | 6 +- src/main/java/module-info.java | 32 +- src/main/java/sh/ball/audio/Effect.java | 14 +- .../java/sh/ball/audio/FrameProducer.java | 60 +- src/main/java/sh/ball/audio/FrameSet.java | 24 +- .../java/sh/ball/audio/FrequencyAnalyser.java | 210 +- .../java/sh/ball/audio/FrequencyListener.java | 12 +- src/main/java/sh/ball/audio/Renderer.java | 44 +- .../java/sh/ball/audio/effect/Effect.java | 14 +- .../sh/ball/audio/effect/EffectFactory.java | 92 +- .../java/sh/ball/audio/effect/EffectType.java | 24 +- .../sh/ball/audio/effect/PhaseEffect.java | 60 +- .../sh/ball/audio/effect/RotateEffect.java | 46 +- .../sh/ball/audio/effect/ScaleEffect.java | 50 +- .../sh/ball/audio/effect/TranslateEffect.java | 60 +- .../sh/ball/audio/effect/WobbleEffect.java | 88 +- src/main/java/sh/ball/audio/fft/FFT.java | 1788 ++++++++--------- src/main/java/sh/ball/engine/Line3D.java | 94 +- src/main/java/sh/ball/gui/Controller.java | 771 +++---- src/main/java/sh/ball/gui/EffectType.java | 10 +- src/main/java/sh/ball/gui/Gui.java | 134 +- src/main/java/sh/ball/gui/Settable.java | 14 +- src/main/java/sh/ball/parser/FileParser.java | 50 +- .../java/sh/ball/parser/ParserFactory.java | 56 +- src/main/java/sh/ball/parser/XmlUtil.java | 160 +- .../java/sh/ball/parser/obj/Listener.java | 12 +- .../java/sh/ball/parser/obj/ObjFrameSet.java | 144 +- .../sh/ball/parser/obj/ObjFrameSettings.java | 58 +- .../java/sh/ball/parser/obj/ObjParser.java | 140 +- .../ball/parser/obj/ObjSettingsFactory.java | 52 +- .../java/sh/ball/parser/svg/ClosePath.java | 56 +- src/main/java/sh/ball/parser/svg/CurveTo.java | 210 +- .../sh/ball/parser/svg/EllipticalArcTo.java | 292 +-- src/main/java/sh/ball/parser/svg/LineTo.java | 150 +- src/main/java/sh/ball/parser/svg/MoveTo.java | 86 +- .../java/sh/ball/parser/svg/SvgParser.java | 420 ++-- .../java/sh/ball/parser/svg/SvgState.java | 24 +- .../java/sh/ball/parser/txt/TextParser.java | 170 +- .../java/sh/ball/shapes/CubicBezierCurve.java | 116 +- .../sh/ball/shapes/QuadraticBezierCurve.java | 104 +- src/main/resources/css/main.css | 169 +- src/main/resources/fxml/osci-render.fxml | 160 +- src/main/resources/text/greek.txt | 6 +- src/main/resources/text/helloworld.txt | 2 +- 47 files changed, 3434 insertions(+), 3426 deletions(-) diff --git a/README.md b/README.md index 86cf7470..b8f8ffaa 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Additional effects can be applied to the image such as: ## Screenshots - + ## Running diff --git a/gui.png b/gui.png index 2fda0e2829c1c3f5730298b2c57469b34b9ca0f1..5f0f19e2decae2410f72870f5c0cff8139c14b5a 100644 GIT binary patch literal 59042 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 NU)8;W)wmP-KLGPWV>JK( diff --git a/pom.xml b/pom.xml index 607ad9ad..ad98833c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,288 +1,288 @@ - - - 4.0.0 - - sh.ball - osci-render - 1.6.1 - - osci-render - - - UTF-8 - 15 - 16 - ${project.build.directory}\modules - oscirender - sh.ball.gui.Gui - - - - - - maven-clean-plugin - 2.4.1 - - - auto-clean - initialize - - clean - - - - - - maven-jar-plugin - 3.2.0 - - - default-jar - none - - unwanted - unwanted - - - - - - maven-assembly-plugin - - - - sh.ball.gui.Gui - - - - jar-with-dependencies - - false - - - - make-assembly - package - - single - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 3.1.2 - - - copy-dependencies - generate-sources - - copy-dependencies - - - ${project.build.directory}/modules - true - true - true - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - ${maven.compiler.release} - ${maven.compiler.release} - - --enable-preview - --module-path - ${project.modulePath} - - - - - org.panteleyev - jpackage-maven-plugin - 1.4.0 - - - osci-render - ${project.version} - james.ball.sh - ${appModule}/${appMainClass} - ${project.build.directory}/modules;${project.build.directory}/classes - ${project.build.directory} - - - - - - - - - win - - src/main/resources/icons/icon.ico - true - osci-render - - - - debian - - deb - - - - - - org.moditect - moditect-maven-plugin - 1.0.0.Beta2 - - - add-module-infos - generate-resources - - add-module-info - - - true - ${project.build.directory}/modules - - - - org.unbescape - unbescape - 1.1.6.RELEASE - - - org.unbescape.html - - - - - com.github.mokiat - java-data-front - 2.0.0 - - - java.data.front - - - - - org.openjfx - javafx-controls - ${javafx.version} - - - javafx.controls.exposed - - - - - org.openjfx - javafx-fxml - ${javafx.version} - - - javafx.fxml.exposed - - - - - org.openjfx - javafx-graphics - ${javafx.version} - - - javafx.graphics.exposed - - - - - org.openjfx - javafx-base - ${javafx.version} - - - javafx.base.exposed - - - - - net.java.dev.jna - jna - 5.6.0 - - - com.sun.jna - - - - - org.jheaps - jheaps - 0.13 - - - org.jheaps - - - - - - - - - - - - jitpack.io - jitpack - https://jitpack.io - - - - - - com.github.sjoerdvankreel - xt.audio - 1.9 - - - org.openjfx - javafx-fxml - ${javafx.version} - - - org.openjfx - javafx-controls - ${javafx.version} - - - - org.unbescape - unbescape - 1.1.6.RELEASE - - - - org.jgrapht - jgrapht-core - 1.5.0 - - - com.github.mokiat - java-data-front - 2.0.0 - - - + + + 4.0.0 + + sh.ball + osci-render + 1.6.1 + + osci-render + + + UTF-8 + 15 + 16 + ${project.build.directory}\modules + oscirender + sh.ball.gui.Gui + + + + + + maven-clean-plugin + 2.4.1 + + + auto-clean + initialize + + clean + + + + + + maven-jar-plugin + 3.2.0 + + + default-jar + none + + unwanted + unwanted + + + + + + maven-assembly-plugin + + + + sh.ball.gui.Gui + + + + jar-with-dependencies + + false + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.2 + + + copy-dependencies + generate-sources + + copy-dependencies + + + ${project.build.directory}/modules + true + true + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${maven.compiler.release} + ${maven.compiler.release} + + --enable-preview + --module-path + ${project.modulePath} + + + + + org.panteleyev + jpackage-maven-plugin + 1.4.0 + + + osci-render + ${project.version} + james.ball.sh + ${appModule}/${appMainClass} + ${project.build.directory}/modules;${project.build.directory}/classes + ${project.build.directory} + + + + + + + + + win + + src/main/resources/icons/icon.ico + true + osci-render + + + + debian + + deb + + + + + + org.moditect + moditect-maven-plugin + 1.0.0.Beta2 + + + add-module-infos + generate-resources + + add-module-info + + + true + ${project.build.directory}/modules + + + + org.unbescape + unbescape + 1.1.6.RELEASE + + + org.unbescape.html + + + + + com.github.mokiat + java-data-front + 2.0.0 + + + java.data.front + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + javafx.controls.exposed + + + + + org.openjfx + javafx-fxml + ${javafx.version} + + + javafx.fxml.exposed + + + + + org.openjfx + javafx-graphics + ${javafx.version} + + + javafx.graphics.exposed + + + + + org.openjfx + javafx-base + ${javafx.version} + + + javafx.base.exposed + + + + + net.java.dev.jna + jna + 5.6.0 + + + com.sun.jna + + + + + org.jheaps + jheaps + 0.13 + + + org.jheaps + + + + + + + + + + + + jitpack.io + jitpack + https://jitpack.io + + + + + + com.github.sjoerdvankreel + xt.audio + 1.9 + + + org.openjfx + javafx-fxml + ${javafx.version} + + + org.openjfx + javafx-controls + ${javafx.version} + + + + org.unbescape + unbescape + 1.1.6.RELEASE + + + + org.jgrapht + jgrapht-core + 1.5.0 + + + com.github.mokiat + java-data-front + 2.0.0 + + + \ No newline at end of file diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF index 6818fd2a..3e526c0b 100644 --- a/src/main/java/META-INF/MANIFEST.MF +++ b/src/main/java/META-INF/MANIFEST.MF @@ -1,3 +1,3 @@ -Manifest-Version: 1.0 -Main-Class: sh.ball.gui.Gui - +Manifest-Version: 1.0 +Main-Class: sh.ball.gui.Gui + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e574beb6..b0fbc14a 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,17 +1,17 @@ -module oscirender { - requires javafx.controls; - requires javafx.controls.exposed; - requires javafx.graphics; - requires javafx.graphics.exposed; - requires javafx.fxml; - requires javafx.fxml.exposed; - requires javafx.base.exposed; - requires xt.audio; - requires java.xml; - requires java.data.front; - requires org.jgrapht.core; - requires org.unbescape.html; - requires java.desktop; - - opens sh.ball.gui; +module oscirender { + requires javafx.controls; + requires javafx.controls.exposed; + requires javafx.graphics; + requires javafx.graphics.exposed; + requires javafx.fxml; + requires javafx.fxml.exposed; + requires javafx.base.exposed; + requires xt.audio; + requires java.xml; + requires java.data.front; + requires org.jgrapht.core; + requires org.unbescape.html; + requires java.desktop; + + opens sh.ball.gui; } \ No newline at end of file diff --git a/src/main/java/sh/ball/audio/Effect.java b/src/main/java/sh/ball/audio/Effect.java index 1785fd75..95173e3e 100644 --- a/src/main/java/sh/ball/audio/Effect.java +++ b/src/main/java/sh/ball/audio/Effect.java @@ -1,7 +1,7 @@ -package sh.ball.audio; - -import sh.ball.shapes.Vector2; - -public interface Effect { - Vector2 apply(int count, Vector2 vector); -} +package sh.ball.audio; + +import sh.ball.shapes.Vector2; + +public interface Effect { + Vector2 apply(int count, Vector2 vector); +} diff --git a/src/main/java/sh/ball/audio/FrameProducer.java b/src/main/java/sh/ball/audio/FrameProducer.java index 02dafac2..d1544e61 100644 --- a/src/main/java/sh/ball/audio/FrameProducer.java +++ b/src/main/java/sh/ball/audio/FrameProducer.java @@ -1,30 +1,30 @@ -package sh.ball.audio; - -public class FrameProducer implements Runnable { - - private final Renderer renderer; - private final FrameSet frames; - - private boolean running; - - public FrameProducer(Renderer renderer, FrameSet frames) { - this.renderer = renderer; - this.frames = frames; - } - - @Override - public void run() { - running = true; - while (running) { - renderer.addFrame(frames.next()); - } - } - - public void stop() { - running = false; - } - - public void setFrameSettings(Object settings) { - frames.setFrameSettings(settings); - } -} +package sh.ball.audio; + +public class FrameProducer implements Runnable { + + private final Renderer renderer; + private final FrameSet frames; + + private boolean running; + + public FrameProducer(Renderer renderer, FrameSet frames) { + this.renderer = renderer; + this.frames = frames; + } + + @Override + public void run() { + running = true; + while (running) { + renderer.addFrame(frames.next()); + } + } + + public void stop() { + running = false; + } + + 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 62364616..5771b86e 100644 --- a/src/main/java/sh/ball/audio/FrameSet.java +++ b/src/main/java/sh/ball/audio/FrameSet.java @@ -1,12 +1,12 @@ -package sh.ball.audio; - -import sh.ball.parser.obj.Listener; - -public interface FrameSet { - - T next(); - - void setFrameSettings(Object settings); - - void addListener(Listener listener); -} +package sh.ball.audio; + +import sh.ball.parser.obj.Listener; + +public interface FrameSet { + + T next(); + + void setFrameSettings(Object settings); + + void addListener(Listener listener); +} diff --git a/src/main/java/sh/ball/audio/FrequencyAnalyser.java b/src/main/java/sh/ball/audio/FrequencyAnalyser.java index 48b730a8..a318057e 100644 --- a/src/main/java/sh/ball/audio/FrequencyAnalyser.java +++ b/src/main/java/sh/ball/audio/FrequencyAnalyser.java @@ -1,105 +1,105 @@ -package sh.ball.audio; - -import sh.ball.audio.fft.FFT; - -import java.util.ArrayList; -import java.util.List; - -public class FrequencyAnalyser implements Runnable { - - private static final float NORMALIZATION_FACTOR_2_BYTES = Short.MAX_VALUE + 1.0f; - - private final Renderer renderer; - private final List listeners = new ArrayList<>(); - private final int frameSize; - private final int sampleRate; - - public FrequencyAnalyser(Renderer renderer, int frameSize, int sampleRate) { - this.renderer = renderer; - this.frameSize = frameSize; - this.sampleRate = sampleRate; - } - - public void addListener(FrequencyListener listener) { - listeners.add(listener); - } - - private void notifyListeners(double leftFrequency, double rightFrequency) { - for (FrequencyListener listener : listeners) { - listener.updateFrequency(leftFrequency, rightFrequency); - } - } - - // Adapted from https://stackoverflow.com/questions/53997426/java-how-to-get-current-frequency-of-audio-input - @Override - public void run() { - byte[] buf = new byte[2 << 18]; // <--- increase this for higher frequency resolution - - while (true) { - try { - renderer.read(buf); - } catch (InterruptedException e) { - e.printStackTrace(); - } - double[] leftSamples = decode(buf, true); - double[] rightSamples = decode(buf, false); - - FFT leftFft = new FFT(leftSamples, null, false, true); - FFT rightFft = new FFT(rightSamples, null, false, true); - - double[] leftMags = leftFft.getMagnitudeSpectrum(); - double[] rightMags = rightFft.getMagnitudeSpectrum(); - double[] bins = leftFft.getBinLabels(sampleRate); - - int maxLeftIndex = 0; - double maxLeft = Double.NEGATIVE_INFINITY; - int maxRightIndex = 0; - double maxRight = Double.NEGATIVE_INFINITY; - for (int i = 1; i < leftMags.length; i++) { - if (bins[i] < 20 || bins[i] > 20000) { - continue; - } - if (leftMags[i] > maxLeft) { - maxLeftIndex = i; - maxLeft = leftMags[i]; - } - if (rightMags[i] > maxRight) { - maxRightIndex = i; - maxRight = rightMags[i]; - } - } - - notifyListeners(bins[maxLeftIndex], bins[maxRightIndex]); - } - } - - private double[] decode(final byte[] buf, boolean decodeLeft) { - final double[] fbuf = new double[(buf.length / 2) / frameSize]; - int byteNum = 0; - int i = 0; - for (int pos = 0; pos < buf.length; pos += frameSize) { - int sample = byteToIntLittleEndian(buf, pos, frameSize); - // normalize to [0,1] (not strictly necessary, but makes things easier) - double normalSample = sample / NORMALIZATION_FACTOR_2_BYTES; - if (decodeLeft) { - if (byteNum < 2) { - fbuf[i++] = normalSample; - } - } else if (byteNum >= 2) { - fbuf[i++] = normalSample; - } - byteNum++; - byteNum %= 4; - } - return fbuf; - } - - private static int byteToIntLittleEndian(final byte[] buf, final int offset, final int bytesPerSample) { - int sample = 0; - for (int byteIndex = 0; byteIndex < bytesPerSample; byteIndex++) { - final int aByte = buf[offset + byteIndex] & 0xff; - sample += aByte << 8 * (byteIndex); - } - return sample; - } -} +package sh.ball.audio; + +import sh.ball.audio.fft.FFT; + +import java.util.ArrayList; +import java.util.List; + +public class FrequencyAnalyser implements Runnable { + + private static final float NORMALIZATION_FACTOR_2_BYTES = Short.MAX_VALUE + 1.0f; + + private final Renderer renderer; + private final List listeners = new ArrayList<>(); + private final int frameSize; + private final int sampleRate; + + public FrequencyAnalyser(Renderer renderer, int frameSize, int sampleRate) { + this.renderer = renderer; + this.frameSize = frameSize; + this.sampleRate = sampleRate; + } + + public void addListener(FrequencyListener listener) { + listeners.add(listener); + } + + private void notifyListeners(double leftFrequency, double rightFrequency) { + for (FrequencyListener listener : listeners) { + listener.updateFrequency(leftFrequency, rightFrequency); + } + } + + // Adapted from https://stackoverflow.com/questions/53997426/java-how-to-get-current-frequency-of-audio-input + @Override + public void run() { + byte[] buf = new byte[2 << 18]; // <--- increase this for higher frequency resolution + + while (true) { + try { + renderer.read(buf); + } catch (InterruptedException e) { + e.printStackTrace(); + } + double[] leftSamples = decode(buf, true); + double[] rightSamples = decode(buf, false); + + FFT leftFft = new FFT(leftSamples, null, false, true); + FFT rightFft = new FFT(rightSamples, null, false, true); + + double[] leftMags = leftFft.getMagnitudeSpectrum(); + double[] rightMags = rightFft.getMagnitudeSpectrum(); + double[] bins = leftFft.getBinLabels(sampleRate); + + int maxLeftIndex = 0; + double maxLeft = Double.NEGATIVE_INFINITY; + int maxRightIndex = 0; + double maxRight = Double.NEGATIVE_INFINITY; + for (int i = 1; i < leftMags.length; i++) { + if (bins[i] < 20 || bins[i] > 20000) { + continue; + } + if (leftMags[i] > maxLeft) { + maxLeftIndex = i; + maxLeft = leftMags[i]; + } + if (rightMags[i] > maxRight) { + maxRightIndex = i; + maxRight = rightMags[i]; + } + } + + notifyListeners(bins[maxLeftIndex], bins[maxRightIndex]); + } + } + + private double[] decode(final byte[] buf, boolean decodeLeft) { + final double[] fbuf = new double[(buf.length / 2) / frameSize]; + int byteNum = 0; + int i = 0; + for (int pos = 0; pos < buf.length; pos += frameSize) { + int sample = byteToIntLittleEndian(buf, pos, frameSize); + // normalize to [0,1] (not strictly necessary, but makes things easier) + double normalSample = sample / NORMALIZATION_FACTOR_2_BYTES; + if (decodeLeft) { + if (byteNum < 2) { + fbuf[i++] = normalSample; + } + } else if (byteNum >= 2) { + fbuf[i++] = normalSample; + } + byteNum++; + byteNum %= 4; + } + return fbuf; + } + + private static int byteToIntLittleEndian(final byte[] buf, final int offset, final int bytesPerSample) { + int sample = 0; + for (int byteIndex = 0; byteIndex < bytesPerSample; byteIndex++) { + final int aByte = buf[offset + byteIndex] & 0xff; + sample += aByte << 8 * (byteIndex); + } + return sample; + } +} diff --git a/src/main/java/sh/ball/audio/FrequencyListener.java b/src/main/java/sh/ball/audio/FrequencyListener.java index a850e134..bb7deda0 100644 --- a/src/main/java/sh/ball/audio/FrequencyListener.java +++ b/src/main/java/sh/ball/audio/FrequencyListener.java @@ -1,6 +1,6 @@ -package sh.ball.audio; - -public interface FrequencyListener { - - void updateFrequency(double leftFrequency, double rightFrequency); -} +package sh.ball.audio; + +public interface FrequencyListener { + + void updateFrequency(double leftFrequency, double rightFrequency); +} diff --git a/src/main/java/sh/ball/audio/Renderer.java b/src/main/java/sh/ball/audio/Renderer.java index 669b95a2..af5902a0 100644 --- a/src/main/java/sh/ball/audio/Renderer.java +++ b/src/main/java/sh/ball/audio/Renderer.java @@ -1,22 +1,22 @@ -package sh.ball.audio; - -import sh.ball.audio.effect.Effect; - -public interface Renderer extends Runnable { - - void stop(); - - void setQuality(double quality); - - void addFrame(S frame); - - void addEffect(Object identifier, Effect effect); - - void removeEffect(Object identifier); - - void read(byte[] buffer) throws InterruptedException; - - void startRecord(); - - T stopRecord(); -} +package sh.ball.audio; + +import sh.ball.audio.effect.Effect; + +public interface Renderer extends Runnable { + + void stop(); + + void setQuality(double quality); + + void addFrame(S frame); + + void addEffect(Object identifier, Effect effect); + + void removeEffect(Object identifier); + + void read(byte[] buffer) throws InterruptedException; + + void startRecord(); + + T stopRecord(); +} diff --git a/src/main/java/sh/ball/audio/effect/Effect.java b/src/main/java/sh/ball/audio/effect/Effect.java index 0f70c57f..b9b5df13 100644 --- a/src/main/java/sh/ball/audio/effect/Effect.java +++ b/src/main/java/sh/ball/audio/effect/Effect.java @@ -1,7 +1,7 @@ -package sh.ball.audio.effect; - -import sh.ball.shapes.Vector2; - -public interface Effect { - Vector2 apply(int count, Vector2 vector); -} +package sh.ball.audio.effect; + +import sh.ball.shapes.Vector2; + +public interface Effect { + Vector2 apply(int count, Vector2 vector); +} diff --git a/src/main/java/sh/ball/audio/effect/EffectFactory.java b/src/main/java/sh/ball/audio/effect/EffectFactory.java index 66d04893..1dc763cf 100644 --- a/src/main/java/sh/ball/audio/effect/EffectFactory.java +++ b/src/main/java/sh/ball/audio/effect/EffectFactory.java @@ -1,46 +1,46 @@ -package sh.ball.audio.effect; - -import sh.ball.shapes.Vector2; - -public class EffectFactory { - public static Effect vectorCancelling(int frequency) { - return (count, v) -> count % frequency == 0 ? v.scale(-1) : v; - } - - public static Effect bitCrush(double value) { - return (count, v) -> { - double x = v.getX(); - double y = v.getY(); - return new Vector2(round(x, value), round(y, value)); - }; - } - - private static double round(double value, double places) { - if (places < 0) throw new IllegalArgumentException(); - - long factor = (long) Math.pow(10, places); - value = value * factor; - long tmp = Math.round(value); - return (double) tmp / factor; - } - - public static Effect horizontalDistort(double value) { - return (count, v) -> { - if (count % 2 == 0) { - return v.translate(new Vector2(value, 0)); - } else { - return v.translate(new Vector2(-value, 0)); - } - }; - } - - public static Effect verticalDistort(double value) { - return (count, v) -> { - if (count % 2 == 0) { - return v.translate(new Vector2(0, value)); - } else { - return v.translate(new Vector2(0, -value)); - } - }; - } -} +package sh.ball.audio.effect; + +import sh.ball.shapes.Vector2; + +public class EffectFactory { + public static Effect vectorCancelling(int frequency) { + return (count, v) -> count % frequency == 0 ? v.scale(-1) : v; + } + + public static Effect bitCrush(double value) { + return (count, v) -> { + double x = v.getX(); + double y = v.getY(); + return new Vector2(round(x, value), round(y, value)); + }; + } + + private static double round(double value, double places) { + if (places < 0) throw new IllegalArgumentException(); + + long factor = (long) Math.pow(10, places); + value = value * factor; + long tmp = Math.round(value); + return (double) tmp / factor; + } + + public static Effect horizontalDistort(double value) { + return (count, v) -> { + if (count % 2 == 0) { + return v.translate(new Vector2(value, 0)); + } else { + return v.translate(new Vector2(-value, 0)); + } + }; + } + + public static Effect verticalDistort(double value) { + return (count, v) -> { + if (count % 2 == 0) { + return v.translate(new Vector2(0, value)); + } else { + return v.translate(new Vector2(0, -value)); + } + }; + } +} diff --git a/src/main/java/sh/ball/audio/effect/EffectType.java b/src/main/java/sh/ball/audio/effect/EffectType.java index faca7903..843541d2 100644 --- a/src/main/java/sh/ball/audio/effect/EffectType.java +++ b/src/main/java/sh/ball/audio/effect/EffectType.java @@ -1,12 +1,12 @@ -package sh.ball.audio.effect; - -public enum EffectType { - VECTOR_CANCELLING, - BIT_CRUSH, - SCALE, - ROTATE, - TRANSLATE, - VERTICAL_DISTORT, - HORIZONTAL_DISTORT, - WOBBLE -} +package sh.ball.audio.effect; + +public enum EffectType { + VECTOR_CANCELLING, + BIT_CRUSH, + SCALE, + ROTATE, + TRANSLATE, + VERTICAL_DISTORT, + HORIZONTAL_DISTORT, + WOBBLE +} diff --git a/src/main/java/sh/ball/audio/effect/PhaseEffect.java b/src/main/java/sh/ball/audio/effect/PhaseEffect.java index 299d33fe..2775fdc8 100644 --- a/src/main/java/sh/ball/audio/effect/PhaseEffect.java +++ b/src/main/java/sh/ball/audio/effect/PhaseEffect.java @@ -1,30 +1,30 @@ -package sh.ball.audio.effect; - -public abstract class PhaseEffect implements Effect { - - private static final double LARGE_VAL = 2 << 20; - - protected final int sampleRate; - - protected double speed; - private double phase = -LARGE_VAL; - - protected PhaseEffect(int sampleRate, double speed) { - this.sampleRate = sampleRate; - this.speed = speed; - } - - protected double nextTheta() { - phase += speed / sampleRate; - - if (phase >= LARGE_VAL) { - phase = -LARGE_VAL; - } - - return phase * Math.PI; - } - - public void setSpeed(double speed) { - this.speed = speed; - } -} +package sh.ball.audio.effect; + +public abstract class PhaseEffect implements Effect { + + private static final double LARGE_VAL = 2 << 20; + + protected final int sampleRate; + + protected double speed; + private double phase = -LARGE_VAL; + + protected PhaseEffect(int sampleRate, double speed) { + this.sampleRate = sampleRate; + this.speed = speed; + } + + protected double nextTheta() { + phase += speed / sampleRate; + + if (phase >= LARGE_VAL) { + phase = -LARGE_VAL; + } + + return phase * Math.PI; + } + + public void setSpeed(double speed) { + this.speed = speed; + } +} diff --git a/src/main/java/sh/ball/audio/effect/RotateEffect.java b/src/main/java/sh/ball/audio/effect/RotateEffect.java index 5be9abb6..30ed9a12 100644 --- a/src/main/java/sh/ball/audio/effect/RotateEffect.java +++ b/src/main/java/sh/ball/audio/effect/RotateEffect.java @@ -1,23 +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; - } -} +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; + } +} diff --git a/src/main/java/sh/ball/audio/effect/ScaleEffect.java b/src/main/java/sh/ball/audio/effect/ScaleEffect.java index 2794cd65..94f50fdb 100644 --- a/src/main/java/sh/ball/audio/effect/ScaleEffect.java +++ b/src/main/java/sh/ball/audio/effect/ScaleEffect.java @@ -1,25 +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); - } -} +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); + } +} diff --git a/src/main/java/sh/ball/audio/effect/TranslateEffect.java b/src/main/java/sh/ball/audio/effect/TranslateEffect.java index b5ded488..d7c4ec29 100644 --- a/src/main/java/sh/ball/audio/effect/TranslateEffect.java +++ b/src/main/java/sh/ball/audio/effect/TranslateEffect.java @@ -1,30 +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; - } -} +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; + } +} diff --git a/src/main/java/sh/ball/audio/effect/WobbleEffect.java b/src/main/java/sh/ball/audio/effect/WobbleEffect.java index 0a1a4f1e..acff3d57 100644 --- a/src/main/java/sh/ball/audio/effect/WobbleEffect.java +++ b/src/main/java/sh/ball/audio/effect/WobbleEffect.java @@ -1,44 +1,44 @@ -package sh.ball.audio.effect; - -import sh.ball.audio.FrequencyListener; -import sh.ball.shapes.Vector2; - -public class WobbleEffect extends PhaseEffect implements FrequencyListener { - - private static final double DEFAULT_VOLUME = 0.2; - - private double frequency; - private double lastFrequency; - private double volume; - - public WobbleEffect(int sampleRate, double volume) { - super(sampleRate, 2); - this.volume = Math.max(Math.min(volume, 1), 0); - } - - public WobbleEffect(int sampleRate) { - this(sampleRate, DEFAULT_VOLUME); - } - - public void update() { - frequency = lastFrequency; - } - - public void setVolume(double volume) { - this.volume = volume; - } - - @Override - public void updateFrequency(double leftFrequency, double rightFrequency) { - lastFrequency = leftFrequency; - } - - @Override - public Vector2 apply(int count, Vector2 vector) { - double theta = nextTheta(); - double x = vector.getX() + volume * Math.sin(frequency * theta); - double y = vector.getY() + volume * Math.sin(frequency * theta); - - return new Vector2(x, y); - } -} +package sh.ball.audio.effect; + +import sh.ball.audio.FrequencyListener; +import sh.ball.shapes.Vector2; + +public class WobbleEffect extends PhaseEffect implements FrequencyListener { + + private static final double DEFAULT_VOLUME = 0.2; + + private double frequency; + private double lastFrequency; + private double volume; + + public WobbleEffect(int sampleRate, double volume) { + super(sampleRate, 2); + this.volume = Math.max(Math.min(volume, 1), 0); + } + + public WobbleEffect(int sampleRate) { + this(sampleRate, DEFAULT_VOLUME); + } + + public void update() { + frequency = lastFrequency; + } + + public void setVolume(double volume) { + this.volume = volume; + } + + @Override + public void updateFrequency(double leftFrequency, double rightFrequency) { + lastFrequency = leftFrequency; + } + + @Override + public Vector2 apply(int count, Vector2 vector) { + double theta = nextTheta(); + double x = vector.getX() + volume * Math.sin(frequency * theta); + double y = vector.getY() + volume * Math.sin(frequency * theta); + + return new Vector2(x, y); + } +} diff --git a/src/main/java/sh/ball/audio/fft/FFT.java b/src/main/java/sh/ball/audio/fft/FFT.java index 4aa04092..4e95a147 100644 --- a/src/main/java/sh/ball/audio/fft/FFT.java +++ b/src/main/java/sh/ball/audio/fft/FFT.java @@ -1,894 +1,894 @@ -/* - * @(#)FFT.java 1.0 April 5, 2005. - * - * Cory McKay - * McGill Univarsity - * - * https://sourceforge.net/p/jaudio/svn/2/tree/jAudio%201.0/src/jAudioFeatureExtractor/jAudioTools/FFT.java - * - - LICENSE copied from https://github.com/dmcennis/jAudioGIT/blob/master/License.txt - - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -(This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.) - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - jAudio DSP package - Copyright (C) 2005 Danie McEnnis, Cory McKay, University of McGill - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - USA - -Daniel McEnnis: maintainer -dmcennis@gmail.com -160 Johnson St -Marion OH 43302 - */ - -package sh.ball.audio.fft; - - -/** - * This class performs a complex to complex Fast Fourier Transform. Forward and inverse - * transforms may both be performed. The transforms may be performed with or without - * the application of a Hanning window. - * - *

The FFT is performed by this class' constructor. The real and imaginary results - * are both stored, and the magnitude spectrum, power spectrum and phase angles may - * also be accessed (along with appropriate frequency bin labels for the magnitude - * and power spectra). - * - * @author Cory McKay - */ -public class FFT -{ - /* FIELDS ******************************************************************/ - - - // The results of the FFT. - private double[] real_output; - private double[] imaginary_output; - - // The phase angles - private double[] output_angle; - - // Magnitude and power spectra - private double[] output_magnitude; - private double[] output_power; - - - /* CONSTRUCTOR *************************************************************/ - - - /** - * Performs the Fourier transform and stores the real and imaginary results. - * Input signals are zero-padded if they do not have a length equal to a - * power of 2. - * - * @param real_input The real part of the signal to be transformed. - * @param imaginary_input The imaginary part of the signal to be. - * transformed. This may be null if the signal - * is entirely real. - * @param inverse_transform A value of false implies that a forward - * transform is to be applied, and a value of - * true means that an inverse transform is tob - * be applied. - * @param use_hanning_window A value of true means that a Hanning window - * will be applied to the real_input. A value - * of valse will result in the application of - * a Hanning window. - * @throws Exception Throws an exception if the real and imaginary - * inputs are of different sizes or if less than - * three input samples are provided. - */ - public FFT( double[] real_input, - double[] imaginary_input, - boolean inverse_transform, - boolean use_hanning_window ) { - // Throw an exception if non-matching input signals are provided - if (imaginary_input != null) - if (real_input.length != imaginary_input.length) - throw new RuntimeException("Imaginary and real inputs are of different sizes."); - - // Throw an exception if less than three samples are provided - if (real_input.length < 3) - throw new RuntimeException( "Only " + real_input.length + " samples provided.\n" + - "At least three are needed." ); - - // Verify that the input size has a number of samples that is a - // power of 2. If not, then increase the size of the array using - // zero-padding. Also creates a zero filled imaginary component - // of the input if none was specified. - int valid_size = ensureIsPowerOfN(real_input.length, 2); - if (valid_size != real_input.length) - { - double[] temp = new double[valid_size]; - for (int i = 0; i < real_input.length; i++) - temp[i] = real_input[i]; - for (int i = real_input.length; i < valid_size; i++) - temp[i] = 0.0; - real_input = temp; - - if (imaginary_input == null) - { - imaginary_input = new double[valid_size]; - for (int i = 0; i < imaginary_input.length; i++) - imaginary_input[i] = 0.0; - } - else - { - temp = new double[valid_size]; - for (int i = 0; i < imaginary_input.length; i++) - temp[i] = imaginary_input[i]; - for (int i = imaginary_input.length; i < valid_size; i++) - temp[i] = 0.0; - imaginary_input = temp; - } - } - else if (imaginary_input == null) - { - imaginary_input = new double[valid_size]; - for (int i = 0; i < imaginary_input.length; i++) - imaginary_input[i] = 0.0; - } - - // Instantiate the arrays to hold the output and copy the input - // to them, since the algorithm used here is self-processing - real_output = new double[valid_size]; - System.arraycopy(real_input, 0, real_output, 0, valid_size); - imaginary_output = new double[valid_size]; - System.arraycopy(imaginary_input, 0, imaginary_output, 0, valid_size); - - // Apply a Hanning window to the real values if this option is - // selected - if (use_hanning_window) - { - for (int i = 0; i < real_output.length; i++) - { - double hanning = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / valid_size); - real_output[i] *= hanning; - } - } - - // Determine whether this is a forward or inverse transform - int forward_transform = 1; - if (inverse_transform) - forward_transform = -1; - - // Reorder the input data into reverse binary order - double scale = 1.0; - int j = 0; - for (int i = 0; i < valid_size; ++i) - { - if (j >= i) - { - double tempr = real_output[j] * scale; - double tempi = imaginary_output[j] * scale; - real_output[j] = real_output[i] * scale; - imaginary_output[j] = imaginary_output[i] * scale; - real_output[i] = tempr; - imaginary_output[i] = tempi; - } - int m = valid_size / 2; - while (m >= 1 && j >= m) - { - j -= m; - m /= 2; - } - j += m; - } - - // Perform the spectral recombination stage by stage - int stage = 0; - int max_spectra_for_stage; - int step_size; - for( max_spectra_for_stage = 1, step_size = 2 * max_spectra_for_stage; - max_spectra_for_stage < valid_size; - max_spectra_for_stage = step_size, step_size = 2 * max_spectra_for_stage) - { - double delta_angle = forward_transform * Math.PI / max_spectra_for_stage; - - // Loop once for each individual spectra - for (int spectra_count = 0; spectra_count < max_spectra_for_stage; ++spectra_count) - { - double angle = spectra_count * delta_angle; - double real_correction = Math.cos(angle); - double imag_correction = Math.sin(angle); - - int right = 0; - for (int left = spectra_count; left < valid_size; left += step_size) - { - right = left + max_spectra_for_stage; - double temp_real = real_correction * real_output[right] - - imag_correction * imaginary_output[right]; - double temp_imag = real_correction * imaginary_output[right] + - imag_correction * real_output[right]; - real_output[right] = real_output[left] - temp_real; - imaginary_output[right] = imaginary_output[left] - temp_imag; - real_output[left] += temp_real; - imaginary_output[left] += temp_imag; - } - } - max_spectra_for_stage = step_size; - } - - // Set the angle and magnitude to null originally - output_angle = null; - output_power = null; - output_magnitude = null; - } - - /* PUBLIC METHODS **********************************************************/ - - /** - * Returns the magnitudes spectrum. It only makes sense to call - * this method if this object was instantiated as a forward Fourier - * transform. - * - *

Only the left side of the spectrum is returned, as the folded - * portion of the spectrum is redundant for the purpose of the magnitude - * spectrum. This means that the bins only go up to half of the - * sampling rate. - * - * @return The magnitude of each frequency bin. - */ - public double[] getMagnitudeSpectrum() - { - // Only calculate the magnitudes if they have not yet been calculated - if (output_magnitude == null) - { - int number_unfolded_bins = imaginary_output.length / 2; - output_magnitude = new double[number_unfolded_bins]; - for(int i = 0; i < output_magnitude.length; i++) - output_magnitude[i] = ( Math.sqrt(real_output[i] * real_output[i] + imaginary_output[i] * imaginary_output[i]) ) / real_output.length; - } - - // Return the magnitudes - return output_magnitude; - } - - - /** - * Returns the power spectrum. It only makes sense to call - * this method if this object was instantiated as a forward Fourier - * transform. - * - *

Only the left side of the spectrum is returned, as the folded - * portion of the spectrum is redundant for the purpose of the power - * spectrum. This means that the bins only go up to half of the - * sampling rate. - * - * @return The magnitude of each frequency bin. - */ - public double[] getPowerSpectrum() - { - // Only calculate the powers if they have not yet been calculated - if (output_power == null) - { - int number_unfolded_bins = imaginary_output.length / 2; - output_power = new double[number_unfolded_bins]; - for(int i = 0; i < output_power.length; i++) - output_power[i] = (real_output[i] * real_output[i] + imaginary_output[i] * imaginary_output[i]) / real_output.length; - } - - // Return the power - return output_power; - } - - - /** - * Returns the phase angle for each frequency bin. It only makes sense to - * call this method if this object was instantiated as a forward Fourier - * transform. - * - *

Only the left side of the spectrum is returned, as the folded - * portion of the spectrum is redundant for the purpose of the phase - * angles. This means that the bins only go up to half of the - * sampling rate. - * - * @return The phase angle for each frequency bin in degrees. - */ - public double[] getPhaseAngles() - { - // Only calculate the angles if they have not yet been calculated - if (output_angle == null) - { - int number_unfolded_bins = imaginary_output.length / 2; - output_angle = new double[number_unfolded_bins]; - for(int i = 0; i < output_angle.length; i++) - { - if(imaginary_output[i] == 0.0 && real_output[i] == 0.0) - output_angle[i] = 0.0; - else - output_angle[i] = Math.atan(imaginary_output[i] / real_output[i]) * 180.0 / Math.PI; - - if(real_output[i] < 0.0 && imaginary_output[i] == 0.0) - output_angle[i] = 180.0; - else if(real_output[i] < 0.0 && imaginary_output[i] == -0.0) - output_angle[i] = -180.0; - else if(real_output[i] < 0.0 && imaginary_output[i] > 0.0) - output_angle[i] += 180.0; - else if(real_output[i] < 0.0 && imaginary_output[i] < 0.0) - output_angle[i] += -180.0; - } - } - - // Return the phase angles - return output_angle; - } - - - /** - * Returns the frequency bin labels for each bin referred to by the - * real values, imaginary values, magnitudes and phase angles as - * determined by the given sampling rate. - * - * @param sampling_rate The sampling rate that was used to perform - * the FFT. - * @return The bin labels. - */ - public double[] getBinLabels(double sampling_rate) - { - int number_bins = real_output.length; - double bin_width = sampling_rate / (double) number_bins; - int number_unfolded_bins = imaginary_output.length / 2; - double[] labels = new double[number_unfolded_bins]; - labels[0] = 0.0; - for (int bin = 1; bin < labels.length; bin++) - labels[bin] = bin * bin_width; - return labels; - } - - - /** - * Returns the real values as calculated by the FFT. - * - * @return The real values. - */ - public double[] getRealValues() - { - return real_output; - } - - - /** - * Returns the real values as calculated by the FFT. - * - * @return The real values. - */ - public double[] getImaginaryValues() - { - return imaginary_output; - } - - /* PRIVATE METHODS *********************************************************/ - - /** - * If the given x is a power of the given n, then x is returned. - * If not, then the next value above the given x that is a power - * of n is returned. - * - *

IMPORTANT: Both x and n must be greater than zero. - * - * @param x The value to ensure is a power of n. - * @param n The power to base x's validation on. - */ - private static int ensureIsPowerOfN(int x, int n) - { - double log_value = logBaseN((double) x, (double) n); - int log_int = (int) log_value; - int valid_size = pow(n, log_int); - if (valid_size != x) - valid_size = pow(n, log_int + 1); - return valid_size; - } - - /** - * Returns the logarithm of the specified base of the given number. - * - *

IMPORTANT: Both x and n must be greater than zero. - * - * @param x The value to find the log of. - * @param n The base of the logarithm. - */ - private static double logBaseN(double x, double n) - { - return (Math.log10(x) / Math.log10(n)); - } - - /** - * Returns the given a raised to the power of the given b. - * - *

IMPORTANT: b must be greater than zero. - * - * @param a The base. - * @param b The exponent. - */ - private static int pow(int a, int b) - { - int result = a; - for (int i = 1; i < b; i++) - result *= a; - return result; - } -} +/* + * @(#)FFT.java 1.0 April 5, 2005. + * + * Cory McKay + * McGill Univarsity + * + * https://sourceforge.net/p/jaudio/svn/2/tree/jAudio%201.0/src/jAudioFeatureExtractor/jAudioTools/FFT.java + * + + LICENSE copied from https://github.com/dmcennis/jAudioGIT/blob/master/License.txt + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +(This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.) + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + jAudio DSP package + Copyright (C) 2005 Danie McEnnis, Cory McKay, University of McGill + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Daniel McEnnis: maintainer +dmcennis@gmail.com +160 Johnson St +Marion OH 43302 + */ + +package sh.ball.audio.fft; + + +/** + * This class performs a complex to complex Fast Fourier Transform. Forward and inverse + * transforms may both be performed. The transforms may be performed with or without + * the application of a Hanning window. + * + *

The FFT is performed by this class' constructor. The real and imaginary results + * are both stored, and the magnitude spectrum, power spectrum and phase angles may + * also be accessed (along with appropriate frequency bin labels for the magnitude + * and power spectra). + * + * @author Cory McKay + */ +public class FFT +{ + /* FIELDS ******************************************************************/ + + + // The results of the FFT. + private double[] real_output; + private double[] imaginary_output; + + // The phase angles + private double[] output_angle; + + // Magnitude and power spectra + private double[] output_magnitude; + private double[] output_power; + + + /* CONSTRUCTOR *************************************************************/ + + + /** + * Performs the Fourier transform and stores the real and imaginary results. + * Input signals are zero-padded if they do not have a length equal to a + * power of 2. + * + * @param real_input The real part of the signal to be transformed. + * @param imaginary_input The imaginary part of the signal to be. + * transformed. This may be null if the signal + * is entirely real. + * @param inverse_transform A value of false implies that a forward + * transform is to be applied, and a value of + * true means that an inverse transform is tob + * be applied. + * @param use_hanning_window A value of true means that a Hanning window + * will be applied to the real_input. A value + * of valse will result in the application of + * a Hanning window. + * @throws Exception Throws an exception if the real and imaginary + * inputs are of different sizes or if less than + * three input samples are provided. + */ + public FFT( double[] real_input, + double[] imaginary_input, + boolean inverse_transform, + boolean use_hanning_window ) { + // Throw an exception if non-matching input signals are provided + if (imaginary_input != null) + if (real_input.length != imaginary_input.length) + throw new RuntimeException("Imaginary and real inputs are of different sizes."); + + // Throw an exception if less than three samples are provided + if (real_input.length < 3) + throw new RuntimeException( "Only " + real_input.length + " samples provided.\n" + + "At least three are needed." ); + + // Verify that the input size has a number of samples that is a + // power of 2. If not, then increase the size of the array using + // zero-padding. Also creates a zero filled imaginary component + // of the input if none was specified. + int valid_size = ensureIsPowerOfN(real_input.length, 2); + if (valid_size != real_input.length) + { + double[] temp = new double[valid_size]; + for (int i = 0; i < real_input.length; i++) + temp[i] = real_input[i]; + for (int i = real_input.length; i < valid_size; i++) + temp[i] = 0.0; + real_input = temp; + + if (imaginary_input == null) + { + imaginary_input = new double[valid_size]; + for (int i = 0; i < imaginary_input.length; i++) + imaginary_input[i] = 0.0; + } + else + { + temp = new double[valid_size]; + for (int i = 0; i < imaginary_input.length; i++) + temp[i] = imaginary_input[i]; + for (int i = imaginary_input.length; i < valid_size; i++) + temp[i] = 0.0; + imaginary_input = temp; + } + } + else if (imaginary_input == null) + { + imaginary_input = new double[valid_size]; + for (int i = 0; i < imaginary_input.length; i++) + imaginary_input[i] = 0.0; + } + + // Instantiate the arrays to hold the output and copy the input + // to them, since the algorithm used here is self-processing + real_output = new double[valid_size]; + System.arraycopy(real_input, 0, real_output, 0, valid_size); + imaginary_output = new double[valid_size]; + System.arraycopy(imaginary_input, 0, imaginary_output, 0, valid_size); + + // Apply a Hanning window to the real values if this option is + // selected + if (use_hanning_window) + { + for (int i = 0; i < real_output.length; i++) + { + double hanning = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / valid_size); + real_output[i] *= hanning; + } + } + + // Determine whether this is a forward or inverse transform + int forward_transform = 1; + if (inverse_transform) + forward_transform = -1; + + // Reorder the input data into reverse binary order + double scale = 1.0; + int j = 0; + for (int i = 0; i < valid_size; ++i) + { + if (j >= i) + { + double tempr = real_output[j] * scale; + double tempi = imaginary_output[j] * scale; + real_output[j] = real_output[i] * scale; + imaginary_output[j] = imaginary_output[i] * scale; + real_output[i] = tempr; + imaginary_output[i] = tempi; + } + int m = valid_size / 2; + while (m >= 1 && j >= m) + { + j -= m; + m /= 2; + } + j += m; + } + + // Perform the spectral recombination stage by stage + int stage = 0; + int max_spectra_for_stage; + int step_size; + for( max_spectra_for_stage = 1, step_size = 2 * max_spectra_for_stage; + max_spectra_for_stage < valid_size; + max_spectra_for_stage = step_size, step_size = 2 * max_spectra_for_stage) + { + double delta_angle = forward_transform * Math.PI / max_spectra_for_stage; + + // Loop once for each individual spectra + for (int spectra_count = 0; spectra_count < max_spectra_for_stage; ++spectra_count) + { + double angle = spectra_count * delta_angle; + double real_correction = Math.cos(angle); + double imag_correction = Math.sin(angle); + + int right = 0; + for (int left = spectra_count; left < valid_size; left += step_size) + { + right = left + max_spectra_for_stage; + double temp_real = real_correction * real_output[right] - + imag_correction * imaginary_output[right]; + double temp_imag = real_correction * imaginary_output[right] + + imag_correction * real_output[right]; + real_output[right] = real_output[left] - temp_real; + imaginary_output[right] = imaginary_output[left] - temp_imag; + real_output[left] += temp_real; + imaginary_output[left] += temp_imag; + } + } + max_spectra_for_stage = step_size; + } + + // Set the angle and magnitude to null originally + output_angle = null; + output_power = null; + output_magnitude = null; + } + + /* PUBLIC METHODS **********************************************************/ + + /** + * Returns the magnitudes spectrum. It only makes sense to call + * this method if this object was instantiated as a forward Fourier + * transform. + * + *

Only the left side of the spectrum is returned, as the folded + * portion of the spectrum is redundant for the purpose of the magnitude + * spectrum. This means that the bins only go up to half of the + * sampling rate. + * + * @return The magnitude of each frequency bin. + */ + public double[] getMagnitudeSpectrum() + { + // Only calculate the magnitudes if they have not yet been calculated + if (output_magnitude == null) + { + int number_unfolded_bins = imaginary_output.length / 2; + output_magnitude = new double[number_unfolded_bins]; + for(int i = 0; i < output_magnitude.length; i++) + output_magnitude[i] = ( Math.sqrt(real_output[i] * real_output[i] + imaginary_output[i] * imaginary_output[i]) ) / real_output.length; + } + + // Return the magnitudes + return output_magnitude; + } + + + /** + * Returns the power spectrum. It only makes sense to call + * this method if this object was instantiated as a forward Fourier + * transform. + * + *

Only the left side of the spectrum is returned, as the folded + * portion of the spectrum is redundant for the purpose of the power + * spectrum. This means that the bins only go up to half of the + * sampling rate. + * + * @return The magnitude of each frequency bin. + */ + public double[] getPowerSpectrum() + { + // Only calculate the powers if they have not yet been calculated + if (output_power == null) + { + int number_unfolded_bins = imaginary_output.length / 2; + output_power = new double[number_unfolded_bins]; + for(int i = 0; i < output_power.length; i++) + output_power[i] = (real_output[i] * real_output[i] + imaginary_output[i] * imaginary_output[i]) / real_output.length; + } + + // Return the power + return output_power; + } + + + /** + * Returns the phase angle for each frequency bin. It only makes sense to + * call this method if this object was instantiated as a forward Fourier + * transform. + * + *

Only the left side of the spectrum is returned, as the folded + * portion of the spectrum is redundant for the purpose of the phase + * angles. This means that the bins only go up to half of the + * sampling rate. + * + * @return The phase angle for each frequency bin in degrees. + */ + public double[] getPhaseAngles() + { + // Only calculate the angles if they have not yet been calculated + if (output_angle == null) + { + int number_unfolded_bins = imaginary_output.length / 2; + output_angle = new double[number_unfolded_bins]; + for(int i = 0; i < output_angle.length; i++) + { + if(imaginary_output[i] == 0.0 && real_output[i] == 0.0) + output_angle[i] = 0.0; + else + output_angle[i] = Math.atan(imaginary_output[i] / real_output[i]) * 180.0 / Math.PI; + + if(real_output[i] < 0.0 && imaginary_output[i] == 0.0) + output_angle[i] = 180.0; + else if(real_output[i] < 0.0 && imaginary_output[i] == -0.0) + output_angle[i] = -180.0; + else if(real_output[i] < 0.0 && imaginary_output[i] > 0.0) + output_angle[i] += 180.0; + else if(real_output[i] < 0.0 && imaginary_output[i] < 0.0) + output_angle[i] += -180.0; + } + } + + // Return the phase angles + return output_angle; + } + + + /** + * Returns the frequency bin labels for each bin referred to by the + * real values, imaginary values, magnitudes and phase angles as + * determined by the given sampling rate. + * + * @param sampling_rate The sampling rate that was used to perform + * the FFT. + * @return The bin labels. + */ + public double[] getBinLabels(double sampling_rate) + { + int number_bins = real_output.length; + double bin_width = sampling_rate / (double) number_bins; + int number_unfolded_bins = imaginary_output.length / 2; + double[] labels = new double[number_unfolded_bins]; + labels[0] = 0.0; + for (int bin = 1; bin < labels.length; bin++) + labels[bin] = bin * bin_width; + return labels; + } + + + /** + * Returns the real values as calculated by the FFT. + * + * @return The real values. + */ + public double[] getRealValues() + { + return real_output; + } + + + /** + * Returns the real values as calculated by the FFT. + * + * @return The real values. + */ + public double[] getImaginaryValues() + { + return imaginary_output; + } + + /* PRIVATE METHODS *********************************************************/ + + /** + * If the given x is a power of the given n, then x is returned. + * If not, then the next value above the given x that is a power + * of n is returned. + * + *

IMPORTANT: Both x and n must be greater than zero. + * + * @param x The value to ensure is a power of n. + * @param n The power to base x's validation on. + */ + private static int ensureIsPowerOfN(int x, int n) + { + double log_value = logBaseN((double) x, (double) n); + int log_int = (int) log_value; + int valid_size = pow(n, log_int); + if (valid_size != x) + valid_size = pow(n, log_int + 1); + return valid_size; + } + + /** + * Returns the logarithm of the specified base of the given number. + * + *

IMPORTANT: Both x and n must be greater than zero. + * + * @param x The value to find the log of. + * @param n The base of the logarithm. + */ + private static double logBaseN(double x, double n) + { + return (Math.log10(x) / Math.log10(n)); + } + + /** + * Returns the given a raised to the power of the given b. + * + *

IMPORTANT: b must be greater than zero. + * + * @param a The base. + * @param b The exponent. + */ + private static int pow(int a, int b) + { + int result = a; + for (int i = 1; i < b; i++) + result *= a; + return result; + } +} diff --git a/src/main/java/sh/ball/engine/Line3D.java b/src/main/java/sh/ball/engine/Line3D.java index f3d94f5a..a898fd10 100644 --- a/src/main/java/sh/ball/engine/Line3D.java +++ b/src/main/java/sh/ball/engine/Line3D.java @@ -1,47 +1,47 @@ -package sh.ball.engine; - -import java.util.Objects; - -public class Line3D { - - private final Vector3 start; - private final Vector3 end; - - public Line3D(Vector3 start, Vector3 end) { - this.start = start; - this.end = end; - } - - public Vector3 getStart() { - return start; - } - - public Vector3 getEnd() { - return end; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Line3D line3D = (Line3D) o; - return Objects.equals(start, line3D.start) && Objects.equals(end, line3D.end); - } - - @Override - public int hashCode() { - return Objects.hash(start, end); - } - - @Override - public String toString() { - return "Line3D{" + - "start=" + start + - ", end=" + end + - '}'; - } -} +package sh.ball.engine; + +import java.util.Objects; + +public class Line3D { + + private final Vector3 start; + private final Vector3 end; + + public Line3D(Vector3 start, Vector3 end) { + this.start = start; + this.end = end; + } + + public Vector3 getStart() { + return start; + } + + public Vector3 getEnd() { + return end; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Line3D line3D = (Line3D) o; + return Objects.equals(start, line3D.start) && Objects.equals(end, line3D.end); + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } + + @Override + public String toString() { + return "Line3D{" + + "start=" + start + + ", end=" + end + + '}'; + } +} diff --git a/src/main/java/sh/ball/gui/Controller.java b/src/main/java/sh/ball/gui/Controller.java index aacf231e..588f592c 100644 --- a/src/main/java/sh/ball/gui/Controller.java +++ b/src/main/java/sh/ball/gui/Controller.java @@ -1,381 +1,390 @@ -package sh.ball.gui; - -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.application.Platform; -import javafx.scene.control.*; -import javafx.util.Duration; -import sh.ball.audio.*; -import sh.ball.audio.effect.*; - -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; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.function.Consumer; - -import javafx.beans.InvalidationListener; -import javafx.fxml.FXML; -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.Effect; -import sh.ball.audio.effect.EffectType; -import sh.ball.engine.Vector3; -import sh.ball.parser.obj.Listener; -import sh.ball.parser.obj.ObjSettingsFactory; -import sh.ball.parser.obj.ObjParser; -import sh.ball.parser.ParserFactory; -import sh.ball.shapes.Shape; -import sh.ball.shapes.Vector2; - -public class Controller implements Initializable, FrequencyListener, Listener { - - 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(); - 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 WobbleEffect wobbleEffect = new WobbleEffect(SAMPLE_RATE); - private final ScaleEffect scaleEffect = new ScaleEffect(); - - private FrameProducer, AudioInputStream> producer; - private boolean recording = false; - - private Stage stage; - - @FXML - private Label frequencyLabel; - @FXML - private Button chooseFileButton; - @FXML - private Label fileLabel; - @FXML - private Button recordButton; - @FXML - private Label recordLabel; - @FXML - private TextField translationXTextField; - @FXML - private TextField translationYTextField; - @FXML - private Slider weightSlider; - @FXML - private Slider rotateSpeedSlider; - @FXML - private Slider translationSpeedSlider; - @FXML - private Slider scaleSlider; - @FXML - private TitledPane objTitledPane; - @FXML - private Slider focalLengthSlider; - @FXML - private TextField cameraXTextField; - @FXML - private TextField cameraYTextField; - @FXML - private TextField cameraZTextField; - @FXML - private Slider objectRotateSpeedSlider; - @FXML - private CheckBox rotateCheckBox; - @FXML - private CheckBox vectorCancellingCheckBox; - @FXML - private Slider vectorCancellingSlider; - @FXML - private CheckBox bitCrushCheckBox; - @FXML - private Slider bitCrushSlider; - @FXML - private CheckBox verticalDistortCheckBox; - @FXML - private Slider verticalDistortSlider; - @FXML - private CheckBox horizontalDistortCheckBox; - @FXML - private Slider horizontalDistortSlider; - @FXML - private CheckBox wobbleCheckBox; - @FXML - private Slider wobbleSlider; - - public Controller(Renderer, AudioInputStream> renderer) throws IOException { - this.renderer = renderer; - FrameSet> frames = new ObjParser(DEFAULT_OBJ).parse(); - frames.addListener(this); - this.producer = new FrameProducer<>(renderer, frames); - } - - private Map> initializeSliderMap() { - return Map.of( - weightSlider, - renderer::setQuality, - rotateSpeedSlider, - rotateEffect::setSpeed, - translationSpeedSlider, - translateEffect::setSpeed, - scaleSlider, - scaleEffect::setScale, - focalLengthSlider, - d -> updateFocalLength(), - objectRotateSpeedSlider, - d -> updateObjectRotateSpeed() - ); - } - - private Map effectTypes; - - private void initializeEffectTypes() { - effectTypes = Map.of( - EffectType.VECTOR_CANCELLING, - vectorCancellingSlider, - EffectType.BIT_CRUSH, - bitCrushSlider, - EffectType.VERTICAL_DISTORT, - verticalDistortSlider, - EffectType.HORIZONTAL_DISTORT, - horizontalDistortSlider, - EffectType.WOBBLE, - wobbleSlider - ); - } - - @Override - public void initialize(URL url, ResourceBundle resourceBundle) { - Map> sliders = initializeSliderMap(); - initializeEffectTypes(); - - for (Slider slider : sliders.keySet()) { - slider.valueProperty().addListener((source, oldValue, newValue) -> - sliders.get(slider).accept(slider.getValue()) - ); - } - - translationXTextField.textProperty().addListener(e -> updateTranslation()); - translationYTextField.textProperty().addListener(e -> updateTranslation()); - - cameraXTextField.focusedProperty().addListener(e -> updateCameraPos()); - cameraYTextField.focusedProperty().addListener(e -> updateCameraPos()); - cameraZTextField.focusedProperty().addListener(e -> updateCameraPos()); - - InvalidationListener vectorCancellingListener = e -> - updateEffect(EffectType.VECTOR_CANCELLING, vectorCancellingCheckBox.isSelected(), - EffectFactory.vectorCancelling((int) vectorCancellingSlider.getValue())); - InvalidationListener bitCrushListener = e -> - updateEffect(EffectType.BIT_CRUSH, bitCrushCheckBox.isSelected(), - EffectFactory.bitCrush(bitCrushSlider.getValue())); - InvalidationListener verticalDistortListener = e -> - updateEffect(EffectType.VERTICAL_DISTORT, verticalDistortCheckBox.isSelected(), - EffectFactory.verticalDistort(verticalDistortSlider.getValue())); - InvalidationListener horizontalDistortListener = e -> - updateEffect(EffectType.HORIZONTAL_DISTORT, horizontalDistortCheckBox.isSelected(), - EffectFactory.horizontalDistort(horizontalDistortSlider.getValue())); - InvalidationListener wobbleListener = e -> { - wobbleEffect.setVolume(wobbleSlider.getValue()); - updateEffect(EffectType.WOBBLE, wobbleCheckBox.isSelected(), wobbleEffect); - }; - - vectorCancellingSlider.valueProperty().addListener(vectorCancellingListener); - vectorCancellingCheckBox.selectedProperty().addListener(vectorCancellingListener); - - bitCrushSlider.valueProperty().addListener(bitCrushListener); - bitCrushCheckBox.selectedProperty().addListener(bitCrushListener); - - verticalDistortSlider.valueProperty().addListener(verticalDistortListener); - verticalDistortCheckBox.selectedProperty().addListener(verticalDistortListener); - - horizontalDistortSlider.valueProperty().addListener(horizontalDistortListener); - horizontalDistortCheckBox.selectedProperty().addListener(horizontalDistortListener); - - wobbleSlider.valueProperty().addListener(wobbleListener); - wobbleCheckBox.selectedProperty().addListener(wobbleListener); - wobbleCheckBox.selectedProperty().addListener(e -> wobbleEffect.update()); - - 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) { - chooseFile(file); - } - }); - - recordButton.setOnAction(event -> toggleRecord()); - - updateObjectRotateSpeed(); - - renderer.addEffect(EffectType.SCALE, scaleEffect); - renderer.addEffect(EffectType.ROTATE, rotateEffect); - renderer.addEffect(EffectType.TRANSLATE, translateEffect); - - executor.submit(producer); - new Thread(renderer).start(); - FrequencyAnalyser, AudioInputStream> analyser = new FrequencyAnalyser<>(renderer, 2, SAMPLE_RATE); - analyser.addListener(this); - analyser.addListener(wobbleEffect); - new Thread(analyser).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 updateFocalLength() { - double focalLength = focalLengthSlider.getValue(); - producer.setFrameSettings(ObjSettingsFactory.focalLength(focalLength)); - } - - private void updateObjectRotateSpeed() { - double rotateSpeed = objectRotateSpeedSlider.getValue(); - producer.setFrameSettings( - ObjSettingsFactory.rotateSpeed((Math.exp(3 * rotateSpeed) - 1) / 50) - ); - } - - private void updateTranslation() { - translateEffect.setTranslation(new Vector2( - tryParse(translationXTextField.getText()), - tryParse(translationYTextField.getText()) - )); - } - - private void updateCameraPos() { - producer.setFrameSettings(ObjSettingsFactory.cameraPosition(new Vector3( - tryParse(cameraXTextField.getText()), - tryParse(cameraYTextField.getText()), - tryParse(cameraZTextField.getText()) - ))); - } - - private double tryParse(String value) { - try { - return Double.parseDouble(value); - } catch (NumberFormatException e) { - return 0; - } - } - - private void updateEffect(EffectType type, boolean checked, Effect effect) { - if (checked) { - renderer.addEffect(type, effect); - effectTypes.get(type).setDisable(false); - } else { - renderer.removeEffect(type); - effectTypes.get(type).setDisable(true); - } - } - - private void chooseFile(File file) { - try { - producer.stop(); - String path = file.getAbsolutePath(); - FrameSet> frames = ParserFactory.getParser(path).parse(); - frames.addListener(this); - producer = new FrameProducer<>(renderer, frames); - - updateObjectRotateSpeed(); - updateFocalLength(); - executor.submit(producer); - - KeyFrame kf1 = new KeyFrame(Duration.seconds(0), e -> wobbleEffect.setVolume(0)); - KeyFrame kf2 = new KeyFrame(Duration.seconds(1), e -> { - wobbleEffect.update(); - wobbleEffect.setVolume(wobbleSlider.getValue()); - }); - Timeline timeline = new Timeline(kf1, kf2); - Platform.runLater(timeline::play); - - if (file.exists() && !file.isDirectory()) { - fileLabel.setText(path); - objTitledPane.setDisable(!ObjParser.isObjFile(path)); - } else { - objTitledPane.setDisable(true); - } - } catch (IOException | ParserConfigurationException | SAXException ioException) { - ioException.printStackTrace(); - } - } - - public void setStage(Stage stage) { - this.stage = stage; - } - - protected boolean mouseRotate() { - return rotateCheckBox.isSelected(); - } - - protected void disableMouseRotate() { - rotateCheckBox.setSelected(false); - } - - protected void setObjRotate(Vector3 vector) { - producer.setFrameSettings(ObjSettingsFactory.rotation(vector)); - } - - @Override - public void updateFrequency(double leftFrequency, double rightFrequency) { - Platform.runLater(() -> - frequencyLabel.setText(String.format("L Frequency: %d Hz\nR Frequency: %d Hz", Math.round(leftFrequency), Math.round(rightFrequency))) - ); - } - - @Override - public void update(Object pos) { - if (pos instanceof Vector3 vector) { - Platform.runLater(() -> { - cameraXTextField.setText(String.valueOf(vector.getX())); - cameraYTextField.setText(String.valueOf(vector.getY())); - cameraZTextField.setText(String.valueOf(vector.getZ())); - }); - } - } -} +package sh.ball.gui; + +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.scene.control.*; +import javafx.util.Duration; +import sh.ball.audio.*; +import sh.ball.audio.effect.*; + +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; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +import javafx.beans.InvalidationListener; +import javafx.fxml.FXML; +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.Effect; +import sh.ball.audio.effect.EffectType; +import sh.ball.engine.Vector3; +import sh.ball.parser.obj.Listener; +import sh.ball.parser.obj.ObjSettingsFactory; +import sh.ball.parser.obj.ObjParser; +import sh.ball.parser.ParserFactory; +import sh.ball.shapes.Shape; +import sh.ball.shapes.Vector2; + +public class Controller implements Initializable, FrequencyListener, Listener { + + 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(); + 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 WobbleEffect wobbleEffect = new WobbleEffect(SAMPLE_RATE); + private final ScaleEffect scaleEffect = new ScaleEffect(); + + private FrameProducer, AudioInputStream> producer; + private boolean recording = false; + + private Stage stage; + + @FXML + private Label frequencyLabel; + @FXML + private Button chooseFileButton; + @FXML + private Label fileLabel; + @FXML + private Button recordButton; + @FXML + private Label recordLabel; + @FXML + private TextField translationXTextField; + @FXML + private TextField translationYTextField; + @FXML + private Slider weightSlider; + @FXML + private Slider rotateSpeedSlider; + @FXML + private Slider translationSpeedSlider; + @FXML + private Slider scaleSlider; + @FXML + private TitledPane objTitledPane; + @FXML + private Slider focalLengthSlider; + @FXML + private TextField cameraXTextField; + @FXML + private TextField cameraYTextField; + @FXML + private TextField cameraZTextField; + @FXML + private Slider objectRotateSpeedSlider; + @FXML + private CheckBox rotateCheckBox; + @FXML + private CheckBox vectorCancellingCheckBox; + @FXML + private Slider vectorCancellingSlider; + @FXML + private CheckBox bitCrushCheckBox; + @FXML + private Slider bitCrushSlider; + @FXML + private CheckBox verticalDistortCheckBox; + @FXML + private Slider verticalDistortSlider; + @FXML + private CheckBox horizontalDistortCheckBox; + @FXML + private Slider horizontalDistortSlider; + @FXML + private CheckBox wobbleCheckBox; + @FXML + private Slider wobbleSlider; + + public Controller(Renderer, AudioInputStream> renderer) throws IOException { + this.renderer = renderer; + FrameSet> frames = new ObjParser(DEFAULT_OBJ).parse(); + frames.addListener(this); + this.producer = new FrameProducer<>(renderer, frames); + } + + private Map> initializeSliderMap() { + return Map.of( + weightSlider, + renderer::setQuality, + rotateSpeedSlider, + rotateEffect::setSpeed, + translationSpeedSlider, + translateEffect::setSpeed, + scaleSlider, + scaleEffect::setScale, + focalLengthSlider, + d -> updateFocalLength(), + objectRotateSpeedSlider, + d -> updateObjectRotateSpeed() + ); + } + + private Map effectTypes; + + private void initializeEffectTypes() { + effectTypes = Map.of( + EffectType.VECTOR_CANCELLING, + vectorCancellingSlider, + EffectType.BIT_CRUSH, + bitCrushSlider, + EffectType.VERTICAL_DISTORT, + verticalDistortSlider, + EffectType.HORIZONTAL_DISTORT, + horizontalDistortSlider, + EffectType.WOBBLE, + wobbleSlider + ); + } + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + Map> sliders = initializeSliderMap(); + initializeEffectTypes(); + + for (Slider slider : sliders.keySet()) { + slider.valueProperty().addListener((source, oldValue, newValue) -> + sliders.get(slider).accept(slider.getValue()) + ); + } + + translationXTextField.textProperty().addListener(e -> updateTranslation()); + translationYTextField.textProperty().addListener(e -> updateTranslation()); + + cameraXTextField.focusedProperty().addListener(e -> updateCameraPos()); + cameraYTextField.focusedProperty().addListener(e -> updateCameraPos()); + cameraZTextField.focusedProperty().addListener(e -> updateCameraPos()); + + InvalidationListener vectorCancellingListener = e -> + updateEffect(EffectType.VECTOR_CANCELLING, vectorCancellingCheckBox.isSelected(), + EffectFactory.vectorCancelling((int) vectorCancellingSlider.getValue())); + InvalidationListener bitCrushListener = e -> + updateEffect(EffectType.BIT_CRUSH, bitCrushCheckBox.isSelected(), + EffectFactory.bitCrush(bitCrushSlider.getValue())); + InvalidationListener verticalDistortListener = e -> + updateEffect(EffectType.VERTICAL_DISTORT, verticalDistortCheckBox.isSelected(), + EffectFactory.verticalDistort(verticalDistortSlider.getValue())); + InvalidationListener horizontalDistortListener = e -> + updateEffect(EffectType.HORIZONTAL_DISTORT, horizontalDistortCheckBox.isSelected(), + EffectFactory.horizontalDistort(horizontalDistortSlider.getValue())); + InvalidationListener wobbleListener = e -> { + wobbleEffect.setVolume(wobbleSlider.getValue()); + updateEffect(EffectType.WOBBLE, wobbleCheckBox.isSelected(), wobbleEffect); + }; + + vectorCancellingSlider.valueProperty().addListener(vectorCancellingListener); + vectorCancellingCheckBox.selectedProperty().addListener(vectorCancellingListener); + + bitCrushSlider.valueProperty().addListener(bitCrushListener); + bitCrushCheckBox.selectedProperty().addListener(bitCrushListener); + + verticalDistortSlider.valueProperty().addListener(verticalDistortListener); + verticalDistortCheckBox.selectedProperty().addListener(verticalDistortListener); + + horizontalDistortSlider.valueProperty().addListener(horizontalDistortListener); + horizontalDistortCheckBox.selectedProperty().addListener(horizontalDistortListener); + + wobbleSlider.valueProperty().addListener(wobbleListener); + wobbleCheckBox.selectedProperty().addListener(wobbleListener); + wobbleCheckBox.selectedProperty().addListener(e -> wobbleEffect.update()); + + 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) { + chooseFile(file); + } + }); + + recordButton.setOnAction(event -> toggleRecord()); + + updateObjectRotateSpeed(); + + renderer.addEffect(EffectType.SCALE, scaleEffect); + renderer.addEffect(EffectType.ROTATE, rotateEffect); + renderer.addEffect(EffectType.TRANSLATE, translateEffect); + + executor.submit(producer); + new Thread(renderer).start(); + FrequencyAnalyser, AudioInputStream> analyser = new FrequencyAnalyser<>(renderer, 2, SAMPLE_RATE); + analyser.addListener(this); + analyser.addListener(wobbleEffect); + new Thread(analyser).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 updateFocalLength() { + double focalLength = focalLengthSlider.getValue(); + producer.setFrameSettings(ObjSettingsFactory.focalLength(focalLength)); + } + + private void updateObjectRotateSpeed() { + double rotateSpeed = objectRotateSpeedSlider.getValue(); + producer.setFrameSettings( + ObjSettingsFactory.rotateSpeed((Math.exp(3 * rotateSpeed) - 1) / 50) + ); + } + + private void updateTranslation() { + translateEffect.setTranslation(new Vector2( + tryParse(translationXTextField.getText()), + tryParse(translationYTextField.getText()) + )); + } + + private void updateCameraPos() { + producer.setFrameSettings(ObjSettingsFactory.cameraPosition(new Vector3( + tryParse(cameraXTextField.getText()), + tryParse(cameraYTextField.getText()), + tryParse(cameraZTextField.getText()) + ))); + } + + private double tryParse(String value) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + return 0; + } + } + + private void updateEffect(EffectType type, boolean checked, Effect effect) { + if (checked) { + renderer.addEffect(type, effect); + effectTypes.get(type).setDisable(false); + } else { + renderer.removeEffect(type); + effectTypes.get(type).setDisable(true); + } + } + + private void chooseFile(File file) { + try { + producer.stop(); + String path = file.getAbsolutePath(); + FrameSet> frames = ParserFactory.getParser(path).parse(); + frames.addListener(this); + producer = new FrameProducer<>(renderer, frames); + + updateObjectRotateSpeed(); + updateFocalLength(); + executor.submit(producer); + + KeyFrame kf1 = new KeyFrame(Duration.seconds(0), e -> wobbleEffect.setVolume(0)); + KeyFrame kf2 = new KeyFrame(Duration.seconds(1), e -> { + wobbleEffect.update(); + wobbleEffect.setVolume(wobbleSlider.getValue()); + }); + Timeline timeline = new Timeline(kf1, kf2); + Platform.runLater(timeline::play); + + if (file.exists() && !file.isDirectory()) { + fileLabel.setText(path); + objTitledPane.setDisable(!ObjParser.isObjFile(path)); + } else { + objTitledPane.setDisable(true); + } + } catch (IOException | ParserConfigurationException | SAXException ioException) { + ioException.printStackTrace(); + } + } + + public void setStage(Stage stage) { + this.stage = stage; + } + + protected boolean mouseRotate() { + return rotateCheckBox.isSelected(); + } + + protected void disableMouseRotate() { + rotateCheckBox.setSelected(false); + } + + protected void setObjRotate(Vector3 vector) { + producer.setFrameSettings(ObjSettingsFactory.rotation(vector)); + } + + @Override + public void updateFrequency(double leftFrequency, double rightFrequency) { + Platform.runLater(() -> + frequencyLabel.setText(String.format("L/R Frequency:\n%d Hz / %d Hz", Math.round(leftFrequency), Math.round(rightFrequency))) + ); + } + + @Override + public void update(Object pos) { + if (pos instanceof Vector3 vector) { + Platform.runLater(() -> { + cameraXTextField.setText(String.valueOf(round(vector.getX(), 3))); + cameraYTextField.setText(String.valueOf(round(vector.getY(), 3))); + cameraZTextField.setText(String.valueOf(round(vector.getZ(), 3))); + }); + } + } + + private static double round(double value, double places) { + if (places < 0) throw new IllegalArgumentException(); + + long factor = (long) Math.pow(10, places); + value = value * factor; + long tmp = Math.round(value); + return (double) tmp / factor; + } +} diff --git a/src/main/java/sh/ball/gui/EffectType.java b/src/main/java/sh/ball/gui/EffectType.java index c13ffab2..0e42cee1 100644 --- a/src/main/java/sh/ball/gui/EffectType.java +++ b/src/main/java/sh/ball/gui/EffectType.java @@ -1,5 +1,5 @@ -package sh.ball.gui; - -enum EffectType { - VECTOR_CANCELLING -} +package sh.ball.gui; + +enum EffectType { + VECTOR_CANCELLING +} diff --git a/src/main/java/sh/ball/gui/Gui.java b/src/main/java/sh/ball/gui/Gui.java index 3b8249bb..ecfdb2c7 100644 --- a/src/main/java/sh/ball/gui/Gui.java +++ b/src/main/java/sh/ball/gui/Gui.java @@ -1,68 +1,66 @@ -package sh.ball.gui; - -import javafx.application.Application; -import javafx.application.Platform; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseEvent; -import javafx.scene.text.Font; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import sh.ball.audio.AudioPlayer; -import sh.ball.engine.Vector3; - -import java.util.Objects; - -public class Gui extends Application { - - private static final int SAMPLE_RATE = 192000; - - @Override - public void start(Stage stage) throws Exception { - Thread.currentThread().setPriority(Thread.MAX_PRIORITY); - System.setProperty("prism.lcdtext", "false"); - - 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(); - - stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/icons/icon.png")))); - stage.setTitle("osci-render"); - Scene scene = new Scene(root); - scene.getStylesheets().add(getClass().getResource("/css/main.css").toExternalForm()); - scene.addEventFilter(MouseEvent.MOUSE_MOVED, event -> { - if (controller.mouseRotate()) { - controller.setObjRotate(new Vector3( - 3 * Math.PI * (event.getSceneY() / scene.getHeight()), - 3 * Math.PI * (event.getSceneX() / scene.getWidth()), - 0 - )); - } - }); - scene.addEventHandler(KeyEvent.KEY_PRESSED, t -> { - if (t.getCode() == KeyCode.ESCAPE) { - controller.disableMouseRotate(); - } - }); - stage.setScene(scene); - stage.setResizable(false); - - controller.setStage(stage); - - stage.show(); - - stage.setOnCloseRequest(t -> { - Platform.exit(); - System.exit(0); - }); - } - - public static void main(String[] args) { - launch(args); - } -} +package sh.ball.gui; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import sh.ball.audio.AudioPlayer; +import sh.ball.engine.Vector3; + +import java.util.Objects; + +public class Gui extends Application { + + private static final int SAMPLE_RATE = 192000; + + @Override + public void start(Stage stage) throws Exception { + Thread.currentThread().setPriority(Thread.MAX_PRIORITY); + System.setProperty("prism.lcdtext", "false"); + + 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(); + + stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/icons/icon.png")))); + stage.setTitle("osci-render"); + Scene scene = new Scene(root); + scene.getStylesheets().add(getClass().getResource("/css/main.css").toExternalForm()); + scene.addEventFilter(MouseEvent.MOUSE_MOVED, event -> { + if (controller.mouseRotate()) { + controller.setObjRotate(new Vector3( + 3 * Math.PI * (event.getSceneY() / scene.getHeight()), + 3 * Math.PI * (event.getSceneX() / scene.getWidth()), + 0 + )); + } + }); + scene.addEventHandler(KeyEvent.KEY_PRESSED, t -> { + if (t.getCode() == KeyCode.ESCAPE) { + controller.disableMouseRotate(); + } + }); + stage.setScene(scene); + stage.setResizable(false); + + controller.setStage(stage); + + stage.show(); + + stage.setOnCloseRequest(t -> { + Platform.exit(); + System.exit(0); + }); + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/src/main/java/sh/ball/gui/Settable.java b/src/main/java/sh/ball/gui/Settable.java index 2da87b79..5f98c69f 100644 --- a/src/main/java/sh/ball/gui/Settable.java +++ b/src/main/java/sh/ball/gui/Settable.java @@ -1,7 +1,7 @@ -package sh.ball.gui; - -@FunctionalInterface -public interface Settable { - - void set(T value); -} +package sh.ball.gui; + +@FunctionalInterface +public interface Settable { + + void set(T value); +} diff --git a/src/main/java/sh/ball/parser/FileParser.java b/src/main/java/sh/ball/parser/FileParser.java index 897a85b5..3d0697e6 100644 --- a/src/main/java/sh/ball/parser/FileParser.java +++ b/src/main/java/sh/ball/parser/FileParser.java @@ -1,25 +1,25 @@ -package sh.ball.parser; - -import java.io.IOException; -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - -public abstract class FileParser { - - public abstract String getFileExtension(); - - protected void checkFileExtension(String path) throws IllegalArgumentException { - if (!hasCorrectFileExtension(path)) { - throw new IllegalArgumentException( - "File to parse is not a ." + getFileExtension() + " file."); - } - } - - public boolean hasCorrectFileExtension(String path) { - return path.matches(".*\\." + getFileExtension()); - } - - public abstract T parse() - throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException; -} +package sh.ball.parser; + +import java.io.IOException; +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +public abstract class FileParser { + + public abstract String getFileExtension(); + + protected void checkFileExtension(String path) throws IllegalArgumentException { + if (!hasCorrectFileExtension(path)) { + throw new IllegalArgumentException( + "File to parse is not a ." + getFileExtension() + " file."); + } + } + + public boolean hasCorrectFileExtension(String path) { + return path.matches(".*\\." + getFileExtension()); + } + + public abstract T parse() + throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException; +} diff --git a/src/main/java/sh/ball/parser/ParserFactory.java b/src/main/java/sh/ball/parser/ParserFactory.java index 395dfb1e..d9f8b369 100644 --- a/src/main/java/sh/ball/parser/ParserFactory.java +++ b/src/main/java/sh/ball/parser/ParserFactory.java @@ -1,28 +1,28 @@ -package sh.ball.parser; - -import org.xml.sax.SAXException; -import sh.ball.audio.FrameSet; -import sh.ball.parser.obj.ObjParser; -import sh.ball.parser.svg.SvgParser; -import sh.ball.parser.txt.TextParser; -import sh.ball.shapes.Shape; - -import javax.xml.parsers.ParserConfigurationException; -import java.io.File; -import java.io.IOException; -import java.util.List; - -public class ParserFactory { - - public static FileParser>> getParser(String filePath) throws IOException, ParserConfigurationException, SAXException { - if (ObjParser.isObjFile(filePath)) { - return new ObjParser(filePath); - } else if (SvgParser.isSvgFile(filePath)) { - return new SvgParser(filePath); - } else if (TextParser.isTxtFile(filePath)) { - return new TextParser(filePath); - } - throw new IOException("No known parser that can parse " + new File(filePath).getName()); - } - -} +package sh.ball.parser; + +import org.xml.sax.SAXException; +import sh.ball.audio.FrameSet; +import sh.ball.parser.obj.ObjParser; +import sh.ball.parser.svg.SvgParser; +import sh.ball.parser.txt.TextParser; +import sh.ball.shapes.Shape; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class ParserFactory { + + public static FileParser>> getParser(String filePath) throws IOException, ParserConfigurationException, SAXException { + if (ObjParser.isObjFile(filePath)) { + return new ObjParser(filePath); + } else if (SvgParser.isSvgFile(filePath)) { + return new SvgParser(filePath); + } else if (TextParser.isTxtFile(filePath)) { + return new TextParser(filePath); + } + throw new IOException("No known parser that can parse " + new File(filePath).getName()); + } + +} diff --git a/src/main/java/sh/ball/parser/XmlUtil.java b/src/main/java/sh/ball/parser/XmlUtil.java index a926b830..0e9fe5ce 100644 --- a/src/main/java/sh/ball/parser/XmlUtil.java +++ b/src/main/java/sh/ball/parser/XmlUtil.java @@ -1,80 +1,80 @@ -package sh.ball.parser; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.RandomAccess; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -public final class XmlUtil { - - private XmlUtil() { - } - - public static List asList(NodeList n) { - return n.getLength() == 0 ? - Collections.emptyList() : new NodeListWrapper(n); - } - - static final class NodeListWrapper extends AbstractList - implements RandomAccess { - - private final NodeList list; - - NodeListWrapper(NodeList l) { - list = l; - } - - public Node get(int index) { - return list.item(index); - } - - public int size() { - return list.getLength(); - } - } - - public static String getNodeValue(Node node, String namedItem) { - Node attribute = node.getAttributes().getNamedItem(namedItem); - return attribute == null ? null : attribute.getNodeValue(); - } - - public static Document getXMLDocument(InputStream input) - throws IOException, SAXException, ParserConfigurationException { - // Opens XML reader for svg file. - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - - // Remove validation which massively slows down file parsing - factory.setNamespaceAware(false); - factory.setValidating(false); - factory.setFeature("http://xml.org/sax/features/namespaces", false); - factory.setFeature("http://xml.org/sax/features/validation", false); - factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); - factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - - DocumentBuilder builder = factory.newDocumentBuilder(); - - return builder.parse(input); - } - - public static List getAttributesOnTags(Document xml, String tagName, String attribute) { - List attributes = new ArrayList<>(); - - for (Node elem : asList(xml.getElementsByTagName(tagName))) { - attributes.add(elem.getAttributes().getNamedItem(attribute)); - } - - return attributes; - } -} +package sh.ball.parser; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.RandomAccess; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +public final class XmlUtil { + + private XmlUtil() { + } + + public static List asList(NodeList n) { + return n.getLength() == 0 ? + Collections.emptyList() : new NodeListWrapper(n); + } + + static final class NodeListWrapper extends AbstractList + implements RandomAccess { + + private final NodeList list; + + NodeListWrapper(NodeList l) { + list = l; + } + + public Node get(int index) { + return list.item(index); + } + + public int size() { + return list.getLength(); + } + } + + public static String getNodeValue(Node node, String namedItem) { + Node attribute = node.getAttributes().getNamedItem(namedItem); + return attribute == null ? null : attribute.getNodeValue(); + } + + public static Document getXMLDocument(InputStream input) + throws IOException, SAXException, ParserConfigurationException { + // Opens XML reader for svg file. + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + // Remove validation which massively slows down file parsing + factory.setNamespaceAware(false); + factory.setValidating(false); + factory.setFeature("http://xml.org/sax/features/namespaces", false); + factory.setFeature("http://xml.org/sax/features/validation", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + + DocumentBuilder builder = factory.newDocumentBuilder(); + + return builder.parse(input); + } + + public static List getAttributesOnTags(Document xml, String tagName, String attribute) { + List attributes = new ArrayList<>(); + + for (Node elem : asList(xml.getElementsByTagName(tagName))) { + attributes.add(elem.getAttributes().getNamedItem(attribute)); + } + + return attributes; + } +} diff --git a/src/main/java/sh/ball/parser/obj/Listener.java b/src/main/java/sh/ball/parser/obj/Listener.java index 763adb4b..cb973eda 100644 --- a/src/main/java/sh/ball/parser/obj/Listener.java +++ b/src/main/java/sh/ball/parser/obj/Listener.java @@ -1,6 +1,6 @@ -package sh.ball.parser.obj; - -public interface Listener { - - void update(Object obj); -} +package sh.ball.parser.obj; + +public interface Listener { + + void update(Object obj); +} diff --git a/src/main/java/sh/ball/parser/obj/ObjFrameSet.java b/src/main/java/sh/ball/parser/obj/ObjFrameSet.java index 1c406703..6b891a04 100644 --- a/src/main/java/sh/ball/parser/obj/ObjFrameSet.java +++ b/src/main/java/sh/ball/parser/obj/ObjFrameSet.java @@ -1,72 +1,72 @@ -package sh.ball.parser.obj; - -import sh.ball.audio.FrameSet; -import sh.ball.engine.Camera; -import sh.ball.engine.Vector3; -import sh.ball.engine.WorldObject; -import sh.ball.shapes.Shape; - -import java.util.ArrayList; -import java.util.List; - -public class ObjFrameSet implements FrameSet> { - - private final WorldObject object; - private final Camera camera; - private final List listeners = new ArrayList<>(); - - private Vector3 rotation = new Vector3(); - private Double rotateSpeed = 0.0; - - public ObjFrameSet(WorldObject object, Camera camera) { - this.object = object; - this.camera = camera; - } - - @Override - public void addListener(Listener listener) { - listeners.add(listener); - notifyListener(listener); - } - - private void notifyListener(Listener listener) { - listener.update(camera.getPos()); - } - - private void notifyListeners() { - listeners.forEach(this::notifyListener); - } - - @Override - public List next() { - if (rotateSpeed == 0) { - object.setRotation(rotation); - } else { - object.rotate(rotation.scale(rotateSpeed)); - } - return camera.draw(object); - } - - // TODO: Refactor! - @Override - public void setFrameSettings(Object settings) { - if (settings instanceof ObjFrameSettings obj) { - if (obj.focalLength != null && camera.getFocalLength() != obj.focalLength) { - camera.setFocalLength(obj.focalLength); - } - if (obj.cameraPos != null && camera.getPos() != obj.cameraPos) { - camera.setPos(obj.cameraPos); - notifyListeners(); - } - if (obj.rotation != null) { - this.rotation = obj.rotation; - } - if (obj.rotateSpeed != null) { - this.rotateSpeed = obj.rotateSpeed; - } - if (obj.resetRotation) { - object.resetRotation(); - } - } - } -} +package sh.ball.parser.obj; + +import sh.ball.audio.FrameSet; +import sh.ball.engine.Camera; +import sh.ball.engine.Vector3; +import sh.ball.engine.WorldObject; +import sh.ball.shapes.Shape; + +import java.util.ArrayList; +import java.util.List; + +public class ObjFrameSet implements FrameSet> { + + private final WorldObject object; + private final Camera camera; + private final List listeners = new ArrayList<>(); + + private Vector3 rotation = new Vector3(); + private Double rotateSpeed = 0.0; + + public ObjFrameSet(WorldObject object, Camera camera) { + this.object = object; + this.camera = camera; + } + + @Override + public void addListener(Listener listener) { + listeners.add(listener); + notifyListener(listener); + } + + private void notifyListener(Listener listener) { + listener.update(camera.getPos()); + } + + private void notifyListeners() { + listeners.forEach(this::notifyListener); + } + + @Override + public List next() { + if (rotateSpeed == 0) { + object.setRotation(rotation); + } else { + object.rotate(rotation.scale(rotateSpeed)); + } + return camera.draw(object); + } + + // TODO: Refactor! + @Override + public void setFrameSettings(Object settings) { + if (settings instanceof ObjFrameSettings obj) { + if (obj.focalLength != null && camera.getFocalLength() != obj.focalLength) { + camera.setFocalLength(obj.focalLength); + } + if (obj.cameraPos != null && camera.getPos() != obj.cameraPos) { + camera.setPos(obj.cameraPos); + notifyListeners(); + } + if (obj.rotation != null) { + this.rotation = obj.rotation; + } + if (obj.rotateSpeed != null) { + this.rotateSpeed = obj.rotateSpeed; + } + if (obj.resetRotation) { + object.resetRotation(); + } + } + } +} diff --git a/src/main/java/sh/ball/parser/obj/ObjFrameSettings.java b/src/main/java/sh/ball/parser/obj/ObjFrameSettings.java index b83e2f76..7cda3527 100644 --- a/src/main/java/sh/ball/parser/obj/ObjFrameSettings.java +++ b/src/main/java/sh/ball/parser/obj/ObjFrameSettings.java @@ -1,29 +1,29 @@ -package sh.ball.parser.obj; - -import sh.ball.engine.Vector3; - -public class ObjFrameSettings { - - protected Double focalLength; - protected Vector3 cameraPos; - protected Vector3 rotation; - protected Double rotateSpeed; - protected boolean resetRotation = false; - - protected ObjFrameSettings(double focalLength) { - this.focalLength = focalLength; - } - - protected ObjFrameSettings(Vector3 cameraPos) { - this.cameraPos = cameraPos; - } - - protected ObjFrameSettings(Vector3 rotation, Double rotateSpeed) { - this.rotation = rotation; - this.rotateSpeed = rotateSpeed; - } - - protected ObjFrameSettings(boolean resetRotation) { - this.resetRotation = resetRotation; - } -} +package sh.ball.parser.obj; + +import sh.ball.engine.Vector3; + +public class ObjFrameSettings { + + protected Double focalLength; + protected Vector3 cameraPos; + protected Vector3 rotation; + protected Double rotateSpeed; + protected boolean resetRotation = false; + + protected ObjFrameSettings(double focalLength) { + this.focalLength = focalLength; + } + + protected ObjFrameSettings(Vector3 cameraPos) { + this.cameraPos = cameraPos; + } + + protected ObjFrameSettings(Vector3 rotation, Double rotateSpeed) { + this.rotation = rotation; + this.rotateSpeed = rotateSpeed; + } + + protected ObjFrameSettings(boolean resetRotation) { + this.resetRotation = resetRotation; + } +} diff --git a/src/main/java/sh/ball/parser/obj/ObjParser.java b/src/main/java/sh/ball/parser/obj/ObjParser.java index 68b39d62..8435edfb 100644 --- a/src/main/java/sh/ball/parser/obj/ObjParser.java +++ b/src/main/java/sh/ball/parser/obj/ObjParser.java @@ -1,70 +1,70 @@ -package sh.ball.parser.obj; - -import sh.ball.audio.FrameSet; -import sh.ball.engine.Camera; -import sh.ball.engine.Vector3; -import sh.ball.engine.WorldObject; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import sh.ball.parser.FileParser; -import sh.ball.shapes.Shape; - -public class ObjParser extends FileParser>> { - - private final boolean isDefaultPosition; - private final InputStream input; - private final Camera camera; - - private WorldObject object; - - public ObjParser(InputStream input, float cameraX, float cameraY, float cameraZ, - float focalLength, boolean isDefaultPosition) { - this.input = input; - this.isDefaultPosition = isDefaultPosition; - Vector3 cameraPos = new Vector3(cameraX, cameraY, cameraZ); - this.camera = new Camera(focalLength, cameraPos); - } - - public ObjParser(InputStream input, float focalLength) { - this(input, 0, 0, 0, focalLength, true); - } - - public ObjParser(InputStream input) { - this(input, (float) Camera.DEFAULT_FOCAL_LENGTH); - } - - public ObjParser(String path) throws FileNotFoundException { - this(new FileInputStream(path), (float) Camera.DEFAULT_FOCAL_LENGTH); - } - - @Override - public String getFileExtension() { - return "obj"; - } - - @Override - public FrameSet> parse() throws IllegalArgumentException, IOException { - object = new WorldObject(input); - camera.findZPos(object); - - return new ObjFrameSet(object, camera); - } - - // If camera position arguments haven't been specified, automatically work out the position of - // the camera based on the size of the object in the camera's view. - public void setFocalLength(double focalLength) { - camera.setFocalLength(focalLength); - if (isDefaultPosition) { - camera.findZPos(object); - } - } - - public static boolean isObjFile(String path) { - return path.matches(".*\\.obj"); - } -} +package sh.ball.parser.obj; + +import sh.ball.audio.FrameSet; +import sh.ball.engine.Camera; +import sh.ball.engine.Vector3; +import sh.ball.engine.WorldObject; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import sh.ball.parser.FileParser; +import sh.ball.shapes.Shape; + +public class ObjParser extends FileParser>> { + + private final boolean isDefaultPosition; + private final InputStream input; + private final Camera camera; + + private WorldObject object; + + public ObjParser(InputStream input, float cameraX, float cameraY, float cameraZ, + float focalLength, boolean isDefaultPosition) { + this.input = input; + this.isDefaultPosition = isDefaultPosition; + Vector3 cameraPos = new Vector3(cameraX, cameraY, cameraZ); + this.camera = new Camera(focalLength, cameraPos); + } + + public ObjParser(InputStream input, float focalLength) { + this(input, 0, 0, 0, focalLength, true); + } + + public ObjParser(InputStream input) { + this(input, (float) Camera.DEFAULT_FOCAL_LENGTH); + } + + public ObjParser(String path) throws FileNotFoundException { + this(new FileInputStream(path), (float) Camera.DEFAULT_FOCAL_LENGTH); + } + + @Override + public String getFileExtension() { + return "obj"; + } + + @Override + public FrameSet> parse() throws IllegalArgumentException, IOException { + object = new WorldObject(input); + camera.findZPos(object); + + return new ObjFrameSet(object, camera); + } + + // If camera position arguments haven't been specified, automatically work out the position of + // the camera based on the size of the object in the camera's view. + public void setFocalLength(double focalLength) { + camera.setFocalLength(focalLength); + if (isDefaultPosition) { + camera.findZPos(object); + } + } + + public static boolean isObjFile(String path) { + return path.matches(".*\\.obj"); + } +} diff --git a/src/main/java/sh/ball/parser/obj/ObjSettingsFactory.java b/src/main/java/sh/ball/parser/obj/ObjSettingsFactory.java index 4ff25ac7..7baa86fc 100644 --- a/src/main/java/sh/ball/parser/obj/ObjSettingsFactory.java +++ b/src/main/java/sh/ball/parser/obj/ObjSettingsFactory.java @@ -1,26 +1,26 @@ -package sh.ball.parser.obj; - -import sh.ball.engine.Vector3; - -public class ObjSettingsFactory { - - public static ObjFrameSettings focalLength(double focalLength) { - return new ObjFrameSettings(focalLength); - } - - public static ObjFrameSettings cameraPosition(Vector3 cameraPos) { - return new ObjFrameSettings(cameraPos); - } - - public static ObjFrameSettings rotation(Vector3 rotation) { - return new ObjFrameSettings(rotation, null); - } - - public static ObjFrameSettings rotateSpeed(double rotateSpeed) { - return new ObjFrameSettings(null, rotateSpeed); - } - - public static ObjFrameSettings resetRotation() { - return new ObjFrameSettings(true); - } -} +package sh.ball.parser.obj; + +import sh.ball.engine.Vector3; + +public class ObjSettingsFactory { + + public static ObjFrameSettings focalLength(double focalLength) { + return new ObjFrameSettings(focalLength); + } + + public static ObjFrameSettings cameraPosition(Vector3 cameraPos) { + return new ObjFrameSettings(cameraPos); + } + + public static ObjFrameSettings rotation(Vector3 rotation) { + return new ObjFrameSettings(rotation, null); + } + + public static ObjFrameSettings rotateSpeed(double rotateSpeed) { + return new ObjFrameSettings(null, rotateSpeed); + } + + public static ObjFrameSettings resetRotation() { + return new ObjFrameSettings(true); + } +} diff --git a/src/main/java/sh/ball/parser/svg/ClosePath.java b/src/main/java/sh/ball/parser/svg/ClosePath.java index 45cbfe8c..221c0f32 100644 --- a/src/main/java/sh/ball/parser/svg/ClosePath.java +++ b/src/main/java/sh/ball/parser/svg/ClosePath.java @@ -1,28 +1,28 @@ -package sh.ball.parser.svg; - -import java.util.List; - -import sh.ball.shapes.Line; -import sh.ball.shapes.Shape; - -class ClosePath { - - // Parses close path commands (Z and z commands) - private static List parseClosePath(SvgState state, List args) { - if (!state.currPoint.equals(state.initialPoint)) { - Line line = new Line(state.currPoint, state.initialPoint); - state.currPoint = state.initialPoint; - return List.of(line); - } else { - return List.of(); - } - } - - static List absolute(SvgState state, List args) { - return parseClosePath(state, args); - } - - static List relative(SvgState state, List args) { - return parseClosePath(state, args); - } -} +package sh.ball.parser.svg; + +import java.util.List; + +import sh.ball.shapes.Line; +import sh.ball.shapes.Shape; + +class ClosePath { + + // Parses close path commands (Z and z commands) + private static List parseClosePath(SvgState state, List args) { + if (!state.currPoint.equals(state.initialPoint)) { + Line line = new Line(state.currPoint, state.initialPoint); + state.currPoint = state.initialPoint; + return List.of(line); + } else { + return List.of(); + } + } + + static List absolute(SvgState state, List args) { + return parseClosePath(state, args); + } + + static List relative(SvgState state, List args) { + return parseClosePath(state, args); + } +} diff --git a/src/main/java/sh/ball/parser/svg/CurveTo.java b/src/main/java/sh/ball/parser/svg/CurveTo.java index dea909f8..0b0efe51 100644 --- a/src/main/java/sh/ball/parser/svg/CurveTo.java +++ b/src/main/java/sh/ball/parser/svg/CurveTo.java @@ -1,105 +1,105 @@ -package sh.ball.parser.svg; - -import java.util.ArrayList; -import java.util.List; - -import sh.ball.shapes.CubicBezierCurve; -import sh.ball.shapes.QuadraticBezierCurve; -import sh.ball.shapes.Shape; -import sh.ball.shapes.Vector2; - -class CurveTo { - - // Parses curveto commands (C, c, S, s, Q, q, T, and t commands) - // isCubic should be true for parsing C, c, S, and s commands - // isCubic should be false for parsing Q, q, T, and t commands - // isSmooth should be true for parsing S, s, T, and t commands - // isSmooth should be false for parsing C, c, Q, and q commands - private static List parseCurveTo(SvgState state, List args, boolean isAbsolute, - boolean isCubic, boolean isSmooth) { - int expectedArgs = isCubic ? 4 : 2; - if (!isSmooth) { - expectedArgs += 2; - } - - if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) { - throw new IllegalArgumentException("SVG curveto command has incorrect number of arguments."); - } - - List curves = new ArrayList<>(); - - for (int i = 0; i < args.size(); i += expectedArgs) { - Vector2 controlPoint1; - Vector2 controlPoint2 = new Vector2(); - - if (isSmooth) { - if (isCubic) { - controlPoint1 = state.prevCubicControlPoint == null ? state.currPoint - : state.prevCubicControlPoint.reflectRelativeToVector(state.currPoint); - } else { - controlPoint1 = state.prevQuadraticControlPoint == null ? state.currPoint - : state.prevQuadraticControlPoint.reflectRelativeToVector(state.currPoint); - } - } else { - controlPoint1 = new Vector2(args.get(i), args.get(i + 1)); - } - - if (isCubic) { - controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3)); - } - - Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2), - args.get(i + expectedArgs - 1)); - - if (!isAbsolute) { - controlPoint1 = state.currPoint.translate(controlPoint1); - controlPoint2 = state.currPoint.translate(controlPoint2); - newPoint = state.currPoint.translate(newPoint); - } - - if (isCubic) { - curves.add(new CubicBezierCurve(state.currPoint, controlPoint1, controlPoint2, newPoint)); - state.currPoint = newPoint; - state.prevCubicControlPoint = controlPoint2; - } else { - curves.add(new QuadraticBezierCurve(state.currPoint, controlPoint1, newPoint)); - state.currPoint = newPoint; - state.prevQuadraticControlPoint = controlPoint1; - } - } - - return curves; - } - - static List absolute(SvgState state, List args) { - return parseCurveTo(state, args, true, true, false); - } - - static List relative(SvgState state, List args) { - return parseCurveTo(state, args, false, true, false); - } - - static List smoothAbsolute(SvgState state, List args) { - return parseCurveTo(state, args, true, true, true); - } - - static List smoothRelative(SvgState state, List args) { - return parseCurveTo(state, args, false, true, true); - } - - static List quarticAbsolute(SvgState state, List args) { - return parseCurveTo(state, args, true, false, false); - } - - static List quarticRelative(SvgState state, List args) { - return parseCurveTo(state, args, false, false, false); - } - - static List quarticSmoothAbsolute(SvgState state, List args) { - return parseCurveTo(state, args, true, false, true); - } - - static List quarticSmoothRelative(SvgState state, List args) { - return parseCurveTo(state, args, false, false, true); - } -} +package sh.ball.parser.svg; + +import java.util.ArrayList; +import java.util.List; + +import sh.ball.shapes.CubicBezierCurve; +import sh.ball.shapes.QuadraticBezierCurve; +import sh.ball.shapes.Shape; +import sh.ball.shapes.Vector2; + +class CurveTo { + + // Parses curveto commands (C, c, S, s, Q, q, T, and t commands) + // isCubic should be true for parsing C, c, S, and s commands + // isCubic should be false for parsing Q, q, T, and t commands + // isSmooth should be true for parsing S, s, T, and t commands + // isSmooth should be false for parsing C, c, Q, and q commands + private static List parseCurveTo(SvgState state, List args, boolean isAbsolute, + boolean isCubic, boolean isSmooth) { + int expectedArgs = isCubic ? 4 : 2; + if (!isSmooth) { + expectedArgs += 2; + } + + if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) { + throw new IllegalArgumentException("SVG curveto command has incorrect number of arguments."); + } + + List curves = new ArrayList<>(); + + for (int i = 0; i < args.size(); i += expectedArgs) { + Vector2 controlPoint1; + Vector2 controlPoint2 = new Vector2(); + + if (isSmooth) { + if (isCubic) { + controlPoint1 = state.prevCubicControlPoint == null ? state.currPoint + : state.prevCubicControlPoint.reflectRelativeToVector(state.currPoint); + } else { + controlPoint1 = state.prevQuadraticControlPoint == null ? state.currPoint + : state.prevQuadraticControlPoint.reflectRelativeToVector(state.currPoint); + } + } else { + controlPoint1 = new Vector2(args.get(i), args.get(i + 1)); + } + + if (isCubic) { + controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3)); + } + + Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2), + args.get(i + expectedArgs - 1)); + + if (!isAbsolute) { + controlPoint1 = state.currPoint.translate(controlPoint1); + controlPoint2 = state.currPoint.translate(controlPoint2); + newPoint = state.currPoint.translate(newPoint); + } + + if (isCubic) { + curves.add(new CubicBezierCurve(state.currPoint, controlPoint1, controlPoint2, newPoint)); + state.currPoint = newPoint; + state.prevCubicControlPoint = controlPoint2; + } else { + curves.add(new QuadraticBezierCurve(state.currPoint, controlPoint1, newPoint)); + state.currPoint = newPoint; + state.prevQuadraticControlPoint = controlPoint1; + } + } + + return curves; + } + + static List absolute(SvgState state, List args) { + return parseCurveTo(state, args, true, true, false); + } + + static List relative(SvgState state, List args) { + return parseCurveTo(state, args, false, true, false); + } + + static List smoothAbsolute(SvgState state, List args) { + return parseCurveTo(state, args, true, true, true); + } + + static List smoothRelative(SvgState state, List args) { + return parseCurveTo(state, args, false, true, true); + } + + static List quarticAbsolute(SvgState state, List args) { + return parseCurveTo(state, args, true, false, false); + } + + static List quarticRelative(SvgState state, List args) { + return parseCurveTo(state, args, false, false, false); + } + + static List quarticSmoothAbsolute(SvgState state, List args) { + return parseCurveTo(state, args, true, false, true); + } + + static List quarticSmoothRelative(SvgState state, List args) { + return parseCurveTo(state, args, false, false, true); + } +} diff --git a/src/main/java/sh/ball/parser/svg/EllipticalArcTo.java b/src/main/java/sh/ball/parser/svg/EllipticalArcTo.java index f1db2fca..986c5775 100644 --- a/src/main/java/sh/ball/parser/svg/EllipticalArcTo.java +++ b/src/main/java/sh/ball/parser/svg/EllipticalArcTo.java @@ -1,146 +1,146 @@ -package sh.ball.parser.svg; - -import java.awt.geom.AffineTransform; -import java.awt.geom.Arc2D; -import java.util.ArrayList; -import java.util.List; - -import sh.ball.shapes.Line; -import sh.ball.shapes.Shape; -import sh.ball.shapes.Vector2; - -class EllipticalArcTo { - - private static final int EXPECTED_ARGS = 7; - - private static List parseEllipticalArc(SvgState state, List args, boolean isAbsolute) { - if (args.size() % EXPECTED_ARGS != 0 || args.size() < EXPECTED_ARGS) { - throw new IllegalArgumentException( - "SVG elliptical arc command has incorrect number of arguments."); - } - - List arcs = new ArrayList<>(); - - for (int i = 0; i < args.size(); i += EXPECTED_ARGS) { - Vector2 newPoint = new Vector2(args.get(i + 5), args.get(i + 6)); - - newPoint = isAbsolute ? newPoint : state.currPoint.translate(newPoint); - - arcs.addAll(createArc( - state.currPoint, - args.get(i), - args.get(i + 1), - args.get(i + 2), - args.get(i + 3) == 1, - args.get(i + 4) == 1, - newPoint - )); - - state.currPoint = newPoint; - } - - return arcs; - } - - // The following algorithm is completely based on https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes - private static List createArc(Vector2 start, double rx, double ry, float theta, boolean largeArcFlag, boolean sweepFlag, Vector2 end) { - double x2 = end.getX(); - double y2 = end.getY(); - // Ensure radii are valid - if (rx == 0 || ry == 0) { - return List.of(new Line(start, end)); - } - double x1 = start.getX(); - double y1 = start.getY(); - // Compute the half distance between the current and the final point - double dx2 = (x1 - x2) / 2.0; - double dy2 = (y1 - y2) / 2.0; - // Convert theta from degrees to radians - theta = (float) Math.toRadians(theta % 360); - - // - // Step 1 : Compute (x1', y1') - // - double x1prime = Math.cos(theta) * dx2 + Math.sin(theta) * dy2; - double y1prime = -Math.sin(theta) * dx2 + Math.cos(theta) * dy2; - // Ensure radii are large enough - rx = Math.abs(rx); - ry = Math.abs(ry); - double Prx = rx * rx; - double Pry = ry * ry; - double Px1prime = x1prime * x1prime; - double Py1prime = y1prime * y1prime; - double d = Px1prime / Prx + Py1prime / Pry; - if (d > 1) { - rx = Math.abs(Math.sqrt(d) * rx); - ry = Math.abs(Math.sqrt(d) * ry); - Prx = rx * rx; - Pry = ry * ry; - } - - // - // Step 2 : Compute (cx', cy') - // - double sign = (largeArcFlag == sweepFlag) ? -1.0 : 1.0; - // Forcing the inner term to be positive. It should be >= 0 but can sometimes be negative due - // to double precision. - double coef = sign * - Math.sqrt(Math.abs((Prx * Pry) - (Prx * Py1prime) - (Pry * Px1prime)) / ((Prx * Py1prime) + (Pry * Px1prime))); - double cxprime = coef * ((rx * y1prime) / ry); - double cyprime = coef * -((ry * x1prime) / rx); - - // - // Step 3 : Compute (cx, cy) from (cx', cy') - // - double sx2 = (x1 + x2) / 2.0; - double sy2 = (y1 + y2) / 2.0; - double cx = sx2 + Math.cos(theta) * cxprime - Math.sin(theta) * cyprime; - double cy = sy2 + Math.sin(theta) * cxprime + Math.cos(theta) * cyprime; - - // - // Step 4 : Compute the angleStart (theta1) and the angleExtent (dtheta) - // - double ux = (x1prime - cxprime) / rx; - double uy = (y1prime - cyprime) / ry; - double vx = (-x1prime - cxprime) / rx; - double vy = (-y1prime - cyprime) / ry; - double p, n; - // Compute the angle start - n = Math.sqrt((ux * ux) + (uy * uy)); - p = ux; // (1 * ux) + (0 * uy) - sign = (uy < 0) ? -1.0 : 1.0; - double angleStart = Math.toDegrees(sign * Math.acos(p / n)); - // Compute the angle extent - n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)); - p = ux * vx + uy * vy; - sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0; - double angleExtent = Math.toDegrees(sign * Math.acos(p / n)); - if (!sweepFlag && angleExtent > 0) { - angleExtent -= 360; - } else if (sweepFlag && angleExtent < 0) { - angleExtent += 360; - } - angleExtent %= 360; - angleStart %= 360; - - Arc2D.Float arc = new Arc2D.Float(); - arc.x = (float) (cx - rx); - arc.y = (float) (cy - ry); - arc.width = (float) (rx * 2.0); - arc.height = (float) (ry * 2.0); - arc.start = (float) -angleStart; - arc.extent = (float) -angleExtent; - - AffineTransform transform = AffineTransform.getRotateInstance(theta, arc.getX() + arc.getWidth()/2, arc.getY() + arc.getHeight()/2); - - return Shape.convert(transform.createTransformedShape(arc)); - } - - static List absolute(SvgState state, List args) { - return parseEllipticalArc(state, args, true); - } - - static List relative(SvgState state, List args) { - return parseEllipticalArc(state, args, false); - } -} +package sh.ball.parser.svg; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Arc2D; +import java.util.ArrayList; +import java.util.List; + +import sh.ball.shapes.Line; +import sh.ball.shapes.Shape; +import sh.ball.shapes.Vector2; + +class EllipticalArcTo { + + private static final int EXPECTED_ARGS = 7; + + private static List parseEllipticalArc(SvgState state, List args, boolean isAbsolute) { + if (args.size() % EXPECTED_ARGS != 0 || args.size() < EXPECTED_ARGS) { + throw new IllegalArgumentException( + "SVG elliptical arc command has incorrect number of arguments."); + } + + List arcs = new ArrayList<>(); + + for (int i = 0; i < args.size(); i += EXPECTED_ARGS) { + Vector2 newPoint = new Vector2(args.get(i + 5), args.get(i + 6)); + + newPoint = isAbsolute ? newPoint : state.currPoint.translate(newPoint); + + arcs.addAll(createArc( + state.currPoint, + args.get(i), + args.get(i + 1), + args.get(i + 2), + args.get(i + 3) == 1, + args.get(i + 4) == 1, + newPoint + )); + + state.currPoint = newPoint; + } + + return arcs; + } + + // The following algorithm is completely based on https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + private static List createArc(Vector2 start, double rx, double ry, float theta, boolean largeArcFlag, boolean sweepFlag, Vector2 end) { + double x2 = end.getX(); + double y2 = end.getY(); + // Ensure radii are valid + if (rx == 0 || ry == 0) { + return List.of(new Line(start, end)); + } + double x1 = start.getX(); + double y1 = start.getY(); + // Compute the half distance between the current and the final point + double dx2 = (x1 - x2) / 2.0; + double dy2 = (y1 - y2) / 2.0; + // Convert theta from degrees to radians + theta = (float) Math.toRadians(theta % 360); + + // + // Step 1 : Compute (x1', y1') + // + double x1prime = Math.cos(theta) * dx2 + Math.sin(theta) * dy2; + double y1prime = -Math.sin(theta) * dx2 + Math.cos(theta) * dy2; + // Ensure radii are large enough + rx = Math.abs(rx); + ry = Math.abs(ry); + double Prx = rx * rx; + double Pry = ry * ry; + double Px1prime = x1prime * x1prime; + double Py1prime = y1prime * y1prime; + double d = Px1prime / Prx + Py1prime / Pry; + if (d > 1) { + rx = Math.abs(Math.sqrt(d) * rx); + ry = Math.abs(Math.sqrt(d) * ry); + Prx = rx * rx; + Pry = ry * ry; + } + + // + // Step 2 : Compute (cx', cy') + // + double sign = (largeArcFlag == sweepFlag) ? -1.0 : 1.0; + // Forcing the inner term to be positive. It should be >= 0 but can sometimes be negative due + // to double precision. + double coef = sign * + Math.sqrt(Math.abs((Prx * Pry) - (Prx * Py1prime) - (Pry * Px1prime)) / ((Prx * Py1prime) + (Pry * Px1prime))); + double cxprime = coef * ((rx * y1prime) / ry); + double cyprime = coef * -((ry * x1prime) / rx); + + // + // Step 3 : Compute (cx, cy) from (cx', cy') + // + double sx2 = (x1 + x2) / 2.0; + double sy2 = (y1 + y2) / 2.0; + double cx = sx2 + Math.cos(theta) * cxprime - Math.sin(theta) * cyprime; + double cy = sy2 + Math.sin(theta) * cxprime + Math.cos(theta) * cyprime; + + // + // Step 4 : Compute the angleStart (theta1) and the angleExtent (dtheta) + // + double ux = (x1prime - cxprime) / rx; + double uy = (y1prime - cyprime) / ry; + double vx = (-x1prime - cxprime) / rx; + double vy = (-y1prime - cyprime) / ry; + double p, n; + // Compute the angle start + n = Math.sqrt((ux * ux) + (uy * uy)); + p = ux; // (1 * ux) + (0 * uy) + sign = (uy < 0) ? -1.0 : 1.0; + double angleStart = Math.toDegrees(sign * Math.acos(p / n)); + // Compute the angle extent + n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)); + p = ux * vx + uy * vy; + sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0; + double angleExtent = Math.toDegrees(sign * Math.acos(p / n)); + if (!sweepFlag && angleExtent > 0) { + angleExtent -= 360; + } else if (sweepFlag && angleExtent < 0) { + angleExtent += 360; + } + angleExtent %= 360; + angleStart %= 360; + + Arc2D.Float arc = new Arc2D.Float(); + arc.x = (float) (cx - rx); + arc.y = (float) (cy - ry); + arc.width = (float) (rx * 2.0); + arc.height = (float) (ry * 2.0); + arc.start = (float) -angleStart; + arc.extent = (float) -angleExtent; + + AffineTransform transform = AffineTransform.getRotateInstance(theta, arc.getX() + arc.getWidth()/2, arc.getY() + arc.getHeight()/2); + + return Shape.convert(transform.createTransformedShape(arc)); + } + + static List absolute(SvgState state, List args) { + return parseEllipticalArc(state, args, true); + } + + static List relative(SvgState state, List args) { + return parseEllipticalArc(state, args, false); + } +} diff --git a/src/main/java/sh/ball/parser/svg/LineTo.java b/src/main/java/sh/ball/parser/svg/LineTo.java index 98a3eecc..d59b7a33 100644 --- a/src/main/java/sh/ball/parser/svg/LineTo.java +++ b/src/main/java/sh/ball/parser/svg/LineTo.java @@ -1,75 +1,75 @@ -package sh.ball.parser.svg; - -import java.util.ArrayList; -import java.util.List; - -import sh.ball.shapes.Line; -import sh.ball.shapes.Shape; -import sh.ball.shapes.Vector2; - -class LineTo { - - // Parses lineto commands (L, l, H, h, V, and v commands) - // isHorizontal and isVertical should be true for parsing L and l commands - // Only isHorizontal should be true for parsing H and h commands - // Only isVertical should be true for parsing V and v commands - private static List parseLineTo(SvgState state, List args, boolean isAbsolute, - boolean isHorizontal, boolean isVertical) { - int expectedArgs = isHorizontal && isVertical ? 2 : 1; - - if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) { - throw new IllegalArgumentException("SVG lineto command has incorrect number of arguments."); - } - - List lines = new ArrayList<>(); - - for (int i = 0; i < args.size(); i += expectedArgs) { - Vector2 newPoint; - - if (expectedArgs == 1) { - newPoint = new Vector2(args.get(i), args.get(i)); - } else { - newPoint = new Vector2(args.get(i), args.get(i + 1)); - } - - if (isHorizontal && !isVertical) { - newPoint = isAbsolute ? newPoint.setY(state.currPoint.getY()) : newPoint.setY(0); - } else if (isVertical && !isHorizontal) { - newPoint = isAbsolute ? newPoint.setX(state.currPoint.getX()) : newPoint.setX(0); - } - - if (!isAbsolute) { - newPoint = state.currPoint.translate(newPoint); - } - - lines.add(new Line(state.currPoint, newPoint)); - state.currPoint = newPoint; - } - - return lines; - } - - static List absolute(SvgState state, List args) { - return parseLineTo(state, args, true, true, true); - } - - static List relative(SvgState state, List args) { - return parseLineTo(state, args, false, true, true); - } - - static List horizontalAbsolute(SvgState state, List args) { - return parseLineTo(state, args, true, true, false); - } - - static List horizontalRelative(SvgState state, List args) { - return parseLineTo(state, args, false, true, false); - } - - static List verticalAbsolute(SvgState state, List args) { - return parseLineTo(state, args, true, false, true); - } - - static List verticalRelative(SvgState state, List args) { - return parseLineTo(state, args, false, false, true); - } -} +package sh.ball.parser.svg; + +import java.util.ArrayList; +import java.util.List; + +import sh.ball.shapes.Line; +import sh.ball.shapes.Shape; +import sh.ball.shapes.Vector2; + +class LineTo { + + // Parses lineto commands (L, l, H, h, V, and v commands) + // isHorizontal and isVertical should be true for parsing L and l commands + // Only isHorizontal should be true for parsing H and h commands + // Only isVertical should be true for parsing V and v commands + private static List parseLineTo(SvgState state, List args, boolean isAbsolute, + boolean isHorizontal, boolean isVertical) { + int expectedArgs = isHorizontal && isVertical ? 2 : 1; + + if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) { + throw new IllegalArgumentException("SVG lineto command has incorrect number of arguments."); + } + + List lines = new ArrayList<>(); + + for (int i = 0; i < args.size(); i += expectedArgs) { + Vector2 newPoint; + + if (expectedArgs == 1) { + newPoint = new Vector2(args.get(i), args.get(i)); + } else { + newPoint = new Vector2(args.get(i), args.get(i + 1)); + } + + if (isHorizontal && !isVertical) { + newPoint = isAbsolute ? newPoint.setY(state.currPoint.getY()) : newPoint.setY(0); + } else if (isVertical && !isHorizontal) { + newPoint = isAbsolute ? newPoint.setX(state.currPoint.getX()) : newPoint.setX(0); + } + + if (!isAbsolute) { + newPoint = state.currPoint.translate(newPoint); + } + + lines.add(new Line(state.currPoint, newPoint)); + state.currPoint = newPoint; + } + + return lines; + } + + static List absolute(SvgState state, List args) { + return parseLineTo(state, args, true, true, true); + } + + static List relative(SvgState state, List args) { + return parseLineTo(state, args, false, true, true); + } + + static List horizontalAbsolute(SvgState state, List args) { + return parseLineTo(state, args, true, true, false); + } + + static List horizontalRelative(SvgState state, List args) { + return parseLineTo(state, args, false, true, false); + } + + static List verticalAbsolute(SvgState state, List args) { + return parseLineTo(state, args, true, false, true); + } + + static List verticalRelative(SvgState state, List args) { + return parseLineTo(state, args, false, false, true); + } +} diff --git a/src/main/java/sh/ball/parser/svg/MoveTo.java b/src/main/java/sh/ball/parser/svg/MoveTo.java index 534c3c26..3d84d133 100644 --- a/src/main/java/sh/ball/parser/svg/MoveTo.java +++ b/src/main/java/sh/ball/parser/svg/MoveTo.java @@ -1,43 +1,43 @@ -package sh.ball.parser.svg; - -import java.util.ArrayList; -import java.util.List; - -import sh.ball.shapes.Shape; -import sh.ball.shapes.Vector2; - -class MoveTo { - - // Parses moveto commands (M and m commands) - private static List parseMoveTo(SvgState state, List args, boolean isAbsolute) { - if (args.size() % 2 != 0 || args.size() < 2) { - throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments."); - } - - Vector2 vec = new Vector2(args.get(0), args.get(1)); - - if (isAbsolute) { - state.currPoint = vec; - state.initialPoint = state.currPoint; - if (args.size() > 2) { - return LineTo.absolute(state, args.subList(2, args.size() - 1)); - } - } else { - state.currPoint = state.currPoint.translate(vec); - state.initialPoint = state.currPoint; - if (args.size() > 2) { - return LineTo.relative(state, args.subList(2, args.size() - 1)); - } - } - - return new ArrayList<>(); - } - - static List absolute(SvgState state, List args) { - return parseMoveTo(state, args, true); - } - - static List relative(SvgState state, List args) { - return parseMoveTo(state, args, false); - } -} +package sh.ball.parser.svg; + +import java.util.ArrayList; +import java.util.List; + +import sh.ball.shapes.Shape; +import sh.ball.shapes.Vector2; + +class MoveTo { + + // Parses moveto commands (M and m commands) + private static List parseMoveTo(SvgState state, List args, boolean isAbsolute) { + if (args.size() % 2 != 0 || args.size() < 2) { + throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments."); + } + + Vector2 vec = new Vector2(args.get(0), args.get(1)); + + if (isAbsolute) { + state.currPoint = vec; + state.initialPoint = state.currPoint; + if (args.size() > 2) { + return LineTo.absolute(state, args.subList(2, args.size() - 1)); + } + } else { + state.currPoint = state.currPoint.translate(vec); + state.initialPoint = state.currPoint; + if (args.size() > 2) { + return LineTo.relative(state, args.subList(2, args.size() - 1)); + } + } + + return new ArrayList<>(); + } + + static List absolute(SvgState state, List args) { + return parseMoveTo(state, args, true); + } + + static List relative(SvgState state, List args) { + return parseMoveTo(state, args, false); + } +} diff --git a/src/main/java/sh/ball/parser/svg/SvgParser.java b/src/main/java/sh/ball/parser/svg/SvgParser.java index d160b866..7cf1a786 100644 --- a/src/main/java/sh/ball/parser/svg/SvgParser.java +++ b/src/main/java/sh/ball/parser/svg/SvgParser.java @@ -1,210 +1,210 @@ -package sh.ball.parser.svg; - -import static sh.ball.parser.XmlUtil.asList; -import static sh.ball.parser.XmlUtil.getAttributesOnTags; -import static sh.ball.parser.XmlUtil.getNodeValue; -import static sh.ball.parser.XmlUtil.getXMLDocument; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BiFunction; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import javax.xml.parsers.ParserConfigurationException; - -import org.unbescape.html.HtmlEscape; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.xml.sax.SAXException; -import sh.ball.audio.FrameSet; -import sh.ball.parser.FileParser; -import sh.ball.shapes.ShapeFrameSet; -import sh.ball.shapes.Shape; -import sh.ball.shapes.Vector2; - -public class SvgParser extends FileParser>> { - - private final Map, List>> commandMap; - private final SvgState state; - private final InputStream input; - - private Document svg; - - public SvgParser(InputStream input) { - this.input = input; - this.state = new SvgState(); - this.commandMap = new HashMap<>(); - initialiseCommandMap(); - } - - public SvgParser(String path) throws FileNotFoundException { - this(new FileInputStream(path)); - checkFileExtension(path); - } - - // Map command chars to function calls. - private void initialiseCommandMap() { - commandMap.put('M', MoveTo::absolute); - commandMap.put('m', MoveTo::relative); - commandMap.put('L', LineTo::absolute); - commandMap.put('l', LineTo::relative); - commandMap.put('H', LineTo::horizontalAbsolute); - commandMap.put('h', LineTo::horizontalRelative); - commandMap.put('V', LineTo::verticalAbsolute); - commandMap.put('v', LineTo::verticalRelative); - commandMap.put('C', CurveTo::absolute); - commandMap.put('c', CurveTo::relative); - commandMap.put('S', CurveTo::smoothAbsolute); - commandMap.put('s', CurveTo::smoothRelative); - commandMap.put('Q', CurveTo::quarticAbsolute); - commandMap.put('q', CurveTo::quarticRelative); - commandMap.put('T', CurveTo::quarticSmoothAbsolute); - commandMap.put('t', CurveTo::quarticSmoothRelative); - commandMap.put('A', EllipticalArcTo::absolute); - commandMap.put('a', EllipticalArcTo::relative); - commandMap.put('Z', ClosePath::absolute); - commandMap.put('z', ClosePath::relative); - } - - // Does error checking against SVG path and returns array of SVG commands and arguments - private String[] preProcessPath(String path) throws IllegalArgumentException { - // Replace all commas with spaces and then remove unnecessary whitespace - path = path.replace(',', ' '); - path = path.replace("-", " -"); - path = path.replaceAll("\\s+", " "); - path = path.replaceAll("(^\\s|\\s$)", ""); - - // If there are any characters in the path that are illegal - if (path.matches("[^mlhvcsqtazMLHVCSQTAZ\\-.\\d\\s]")) { - throw new IllegalArgumentException("Illegal characters in SVG path."); - // If there are more than 1 letters or delimiters next to one another - } else if (path.matches("[a-zA-Z.\\-]{2,}")) { - throw new IllegalArgumentException( - "Multiple letters or delimiters found next to one another in SVG path."); - // First character in path must be a command - } else if (path.matches("^[a-zA-Z]")) { - throw new IllegalArgumentException("Start of SVG path is not a letter."); - } - - // Split on SVG path characters to get a list of instructions, keeping the SVG commands - return path.split("(?=[mlhvcsqtazMLHVCSQTAZ])"); - } - - private static List splitCommand(String command) { - List nums = new ArrayList<>(); - String[] decimalSplit = command.split("\\."); - - try { - if (decimalSplit.length == 1) { - nums.add(Float.parseFloat(decimalSplit[0])); - } else { - nums.add(Float.parseFloat(decimalSplit[0] + "." + decimalSplit[1])); - - for (int i = 2; i < decimalSplit.length; i++) { - nums.add(Float.parseFloat("." + decimalSplit[i])); - } - } - } catch (Exception e) { - System.out.println(Arrays.toString(decimalSplit)); - System.out.println(command); - } - - return nums; - } - - @Override - public String getFileExtension() { - return "svg"; - } - - @Override - public FrameSet> parse() - throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { - this.svg = getXMLDocument(input); - List svgElem = asList(svg.getElementsByTagName("svg")); - List shapes = new ArrayList<>(); - - if (svgElem.size() != 1) { - throw new IllegalArgumentException("SVG has either zero or more than one svg element."); - } - - // Get all d attributes within path elements in the SVG file. - for (Node node : getAttributesOnTags(svg, "path", "d")) { - shapes.addAll(parsePath(node.getNodeValue())); - } - - return new ShapeFrameSet(Shape.normalize(shapes)); - } - - /* Given a character, will return the glyph associated with it. - * Assumes that the .svg loaded has character glyphs. */ - public List parseGlyphsWithUnicode(char unicode) { - for (Node node : asList(svg.getElementsByTagName("glyph"))) { - String unicodeString = getNodeValue(node, "unicode"); - - if (unicodeString == null) { - throw new IllegalArgumentException("Glyph should have unicode attribute."); - } - - /* Removes all html escaped characters, allowing it to be directly compared to the unicode - * parameter. */ - String decodedString = HtmlEscape.unescapeHtml(unicodeString); - - if (String.valueOf(unicode).equals(decodedString)) { - return parsePath(getNodeValue(node, "d")); - } - } - - return List.of(); - } - - // Performs path parsing on a single d=path - private List parsePath(String path) { - if (path == null) { - return List.of(); - } - - state.currPoint = new Vector2(); - state.prevCubicControlPoint = null; - state.prevQuadraticControlPoint = null; - String[] commands = preProcessPath(path); - List svgShapes = new ArrayList<>(); - - for (String command : commands) { - char commandChar = command.charAt(0); - List nums = null; - - if (commandChar != 'z' && commandChar != 'Z') { - // Split the command into number strings and convert them into floats. - nums = Arrays.stream(command.substring(1).split(" ")) - .filter(Predicate.not(String::isBlank)) - .flatMap((numString) -> splitCommand(numString).stream()) - .collect(Collectors.toList()); - } - - // Use the nums to get a list of sh.ball.shapes, using the first character in the command to specify - // the function to use. - svgShapes.addAll(commandMap.get(commandChar).apply(state, nums)); - - if (!String.valueOf(commandChar).matches("[csCS]")) { - state.prevCubicControlPoint = null; - } - if (!String.valueOf(commandChar).matches("[qtQT]")) { - state.prevQuadraticControlPoint = null; - } - } - - return svgShapes; - } - - public static boolean isSvgFile(String path) { - return path.matches(".*\\.svg"); - } -} +package sh.ball.parser.svg; + +import static sh.ball.parser.XmlUtil.asList; +import static sh.ball.parser.XmlUtil.getAttributesOnTags; +import static sh.ball.parser.XmlUtil.getNodeValue; +import static sh.ball.parser.XmlUtil.getXMLDocument; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.xml.parsers.ParserConfigurationException; + +import org.unbescape.html.HtmlEscape; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; +import sh.ball.audio.FrameSet; +import sh.ball.parser.FileParser; +import sh.ball.shapes.ShapeFrameSet; +import sh.ball.shapes.Shape; +import sh.ball.shapes.Vector2; + +public class SvgParser extends FileParser>> { + + private final Map, List>> commandMap; + private final SvgState state; + private final InputStream input; + + private Document svg; + + public SvgParser(InputStream input) { + this.input = input; + this.state = new SvgState(); + this.commandMap = new HashMap<>(); + initialiseCommandMap(); + } + + public SvgParser(String path) throws FileNotFoundException { + this(new FileInputStream(path)); + checkFileExtension(path); + } + + // Map command chars to function calls. + private void initialiseCommandMap() { + commandMap.put('M', MoveTo::absolute); + commandMap.put('m', MoveTo::relative); + commandMap.put('L', LineTo::absolute); + commandMap.put('l', LineTo::relative); + commandMap.put('H', LineTo::horizontalAbsolute); + commandMap.put('h', LineTo::horizontalRelative); + commandMap.put('V', LineTo::verticalAbsolute); + commandMap.put('v', LineTo::verticalRelative); + commandMap.put('C', CurveTo::absolute); + commandMap.put('c', CurveTo::relative); + commandMap.put('S', CurveTo::smoothAbsolute); + commandMap.put('s', CurveTo::smoothRelative); + commandMap.put('Q', CurveTo::quarticAbsolute); + commandMap.put('q', CurveTo::quarticRelative); + commandMap.put('T', CurveTo::quarticSmoothAbsolute); + commandMap.put('t', CurveTo::quarticSmoothRelative); + commandMap.put('A', EllipticalArcTo::absolute); + commandMap.put('a', EllipticalArcTo::relative); + commandMap.put('Z', ClosePath::absolute); + commandMap.put('z', ClosePath::relative); + } + + // Does error checking against SVG path and returns array of SVG commands and arguments + private String[] preProcessPath(String path) throws IllegalArgumentException { + // Replace all commas with spaces and then remove unnecessary whitespace + path = path.replace(',', ' '); + path = path.replace("-", " -"); + path = path.replaceAll("\\s+", " "); + path = path.replaceAll("(^\\s|\\s$)", ""); + + // If there are any characters in the path that are illegal + if (path.matches("[^mlhvcsqtazMLHVCSQTAZ\\-.\\d\\s]")) { + throw new IllegalArgumentException("Illegal characters in SVG path."); + // If there are more than 1 letters or delimiters next to one another + } else if (path.matches("[a-zA-Z.\\-]{2,}")) { + throw new IllegalArgumentException( + "Multiple letters or delimiters found next to one another in SVG path."); + // First character in path must be a command + } else if (path.matches("^[a-zA-Z]")) { + throw new IllegalArgumentException("Start of SVG path is not a letter."); + } + + // Split on SVG path characters to get a list of instructions, keeping the SVG commands + return path.split("(?=[mlhvcsqtazMLHVCSQTAZ])"); + } + + private static List splitCommand(String command) { + List nums = new ArrayList<>(); + String[] decimalSplit = command.split("\\."); + + try { + if (decimalSplit.length == 1) { + nums.add(Float.parseFloat(decimalSplit[0])); + } else { + nums.add(Float.parseFloat(decimalSplit[0] + "." + decimalSplit[1])); + + for (int i = 2; i < decimalSplit.length; i++) { + nums.add(Float.parseFloat("." + decimalSplit[i])); + } + } + } catch (Exception e) { + System.out.println(Arrays.toString(decimalSplit)); + System.out.println(command); + } + + return nums; + } + + @Override + public String getFileExtension() { + return "svg"; + } + + @Override + public FrameSet> parse() + throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { + this.svg = getXMLDocument(input); + List svgElem = asList(svg.getElementsByTagName("svg")); + List shapes = new ArrayList<>(); + + if (svgElem.size() != 1) { + throw new IllegalArgumentException("SVG has either zero or more than one svg element."); + } + + // Get all d attributes within path elements in the SVG file. + for (Node node : getAttributesOnTags(svg, "path", "d")) { + shapes.addAll(parsePath(node.getNodeValue())); + } + + return new ShapeFrameSet(Shape.normalize(shapes)); + } + + /* Given a character, will return the glyph associated with it. + * Assumes that the .svg loaded has character glyphs. */ + public List parseGlyphsWithUnicode(char unicode) { + for (Node node : asList(svg.getElementsByTagName("glyph"))) { + String unicodeString = getNodeValue(node, "unicode"); + + if (unicodeString == null) { + throw new IllegalArgumentException("Glyph should have unicode attribute."); + } + + /* Removes all html escaped characters, allowing it to be directly compared to the unicode + * parameter. */ + String decodedString = HtmlEscape.unescapeHtml(unicodeString); + + if (String.valueOf(unicode).equals(decodedString)) { + return parsePath(getNodeValue(node, "d")); + } + } + + return List.of(); + } + + // Performs path parsing on a single d=path + private List parsePath(String path) { + if (path == null) { + return List.of(); + } + + state.currPoint = new Vector2(); + state.prevCubicControlPoint = null; + state.prevQuadraticControlPoint = null; + String[] commands = preProcessPath(path); + List svgShapes = new ArrayList<>(); + + for (String command : commands) { + char commandChar = command.charAt(0); + List nums = null; + + if (commandChar != 'z' && commandChar != 'Z') { + // Split the command into number strings and convert them into floats. + nums = Arrays.stream(command.substring(1).split(" ")) + .filter(Predicate.not(String::isBlank)) + .flatMap((numString) -> splitCommand(numString).stream()) + .collect(Collectors.toList()); + } + + // Use the nums to get a list of sh.ball.shapes, using the first character in the command to specify + // the function to use. + svgShapes.addAll(commandMap.get(commandChar).apply(state, nums)); + + if (!String.valueOf(commandChar).matches("[csCS]")) { + state.prevCubicControlPoint = null; + } + if (!String.valueOf(commandChar).matches("[qtQT]")) { + state.prevQuadraticControlPoint = null; + } + } + + return svgShapes; + } + + public static boolean isSvgFile(String path) { + return path.matches(".*\\.svg"); + } +} diff --git a/src/main/java/sh/ball/parser/svg/SvgState.java b/src/main/java/sh/ball/parser/svg/SvgState.java index 39339486..42e63af4 100644 --- a/src/main/java/sh/ball/parser/svg/SvgState.java +++ b/src/main/java/sh/ball/parser/svg/SvgState.java @@ -1,12 +1,12 @@ -package sh.ball.parser.svg; - -import sh.ball.shapes.Vector2; - -/* Intentionally package-private class for carrying around the current state of SVG parsing. */ -class SvgState { - - Vector2 currPoint; - Vector2 initialPoint; - Vector2 prevCubicControlPoint; - Vector2 prevQuadraticControlPoint; -} +package sh.ball.parser.svg; + +import sh.ball.shapes.Vector2; + +/* Intentionally package-private class for carrying around the current state of SVG parsing. */ +class SvgState { + + Vector2 currPoint; + Vector2 initialPoint; + Vector2 prevCubicControlPoint; + Vector2 prevQuadraticControlPoint; +} diff --git a/src/main/java/sh/ball/parser/txt/TextParser.java b/src/main/java/sh/ball/parser/txt/TextParser.java index a1c53d3c..6b223237 100644 --- a/src/main/java/sh/ball/parser/txt/TextParser.java +++ b/src/main/java/sh/ball/parser/txt/TextParser.java @@ -1,85 +1,85 @@ -package sh.ball.parser.txt; - -import java.io.*; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; -import sh.ball.audio.FrameSet; -import sh.ball.parser.FileParser; -import sh.ball.shapes.ShapeFrameSet; -import sh.ball.parser.svg.SvgParser; -import sh.ball.shapes.Shape; -import sh.ball.shapes.Vector2; - -public class TextParser extends FileParser>> { - - private static final char WIDE_CHAR = 'W'; - private static final double HEIGHT_SCALAR = 1.6; - private static final String DEFAULT_FONT = "/fonts/SourceCodePro-ExtraLight.svg"; - - private final Map> charToShape; - private final InputStream input; - private final InputStream font; - - public TextParser(InputStream input, InputStream font) { - this.input = input; - this.font = font; - this.charToShape = new HashMap<>(); - } - - public TextParser(String path) throws FileNotFoundException { - this(new FileInputStream(path), TextParser.class.getResourceAsStream(DEFAULT_FONT)); - checkFileExtension(path); - } - - @Override - public String getFileExtension() { - return "txt"; - } - - @Override - public FrameSet> parse() throws IllegalArgumentException, IOException, ParserConfigurationException, SAXException { - List text = new BufferedReader(new InputStreamReader(input, Charset.defaultCharset())).lines().collect(Collectors.toList()); - SvgParser fontParser = new SvgParser(font); - fontParser.parse(); - List shapes = new ArrayList<>(); - - /* WIDE_CHAR used as an example character that will be wide in most languages. - * This helps determine the correct character width for the font chosen. */ - charToShape.put(WIDE_CHAR, fontParser.parseGlyphsWithUnicode(WIDE_CHAR)); - - for (String line : text) { - for (char c : line.toCharArray()) { - if (!charToShape.containsKey(c)) { - List glyph = fontParser.parseGlyphsWithUnicode(c); - charToShape.put(c, glyph); - } - } - } - - double width = Shape.width(charToShape.get(WIDE_CHAR)); - double height = HEIGHT_SCALAR * Shape.height(charToShape.get(WIDE_CHAR)); - - for (int i = 0; i < text.size(); i++) { - char[] lineChars = text.get(i).toCharArray(); - for (int j = 0; j < lineChars.length; j++) { - shapes.addAll(Shape.translate( - charToShape.get(lineChars[j]), - new Vector2(j * width, -i * height) - )); - } - } - - return new ShapeFrameSet(Shape.flip(Shape.normalize(shapes))); - } - - public static boolean isTxtFile(String path) { - return path.matches(".*\\.txt"); - } -} +package sh.ball.parser.txt; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; +import sh.ball.audio.FrameSet; +import sh.ball.parser.FileParser; +import sh.ball.shapes.ShapeFrameSet; +import sh.ball.parser.svg.SvgParser; +import sh.ball.shapes.Shape; +import sh.ball.shapes.Vector2; + +public class TextParser extends FileParser>> { + + private static final char WIDE_CHAR = 'W'; + private static final double HEIGHT_SCALAR = 1.6; + private static final String DEFAULT_FONT = "/fonts/SourceCodePro-ExtraLight.svg"; + + private final Map> charToShape; + private final InputStream input; + private final InputStream font; + + public TextParser(InputStream input, InputStream font) { + this.input = input; + this.font = font; + this.charToShape = new HashMap<>(); + } + + public TextParser(String path) throws FileNotFoundException { + this(new FileInputStream(path), TextParser.class.getResourceAsStream(DEFAULT_FONT)); + checkFileExtension(path); + } + + @Override + public String getFileExtension() { + return "txt"; + } + + @Override + public FrameSet> parse() throws IllegalArgumentException, IOException, ParserConfigurationException, SAXException { + List text = new BufferedReader(new InputStreamReader(input, Charset.defaultCharset())).lines().collect(Collectors.toList()); + SvgParser fontParser = new SvgParser(font); + fontParser.parse(); + List shapes = new ArrayList<>(); + + /* WIDE_CHAR used as an example character that will be wide in most languages. + * This helps determine the correct character width for the font chosen. */ + charToShape.put(WIDE_CHAR, fontParser.parseGlyphsWithUnicode(WIDE_CHAR)); + + for (String line : text) { + for (char c : line.toCharArray()) { + if (!charToShape.containsKey(c)) { + List glyph = fontParser.parseGlyphsWithUnicode(c); + charToShape.put(c, glyph); + } + } + } + + double width = Shape.width(charToShape.get(WIDE_CHAR)); + double height = HEIGHT_SCALAR * Shape.height(charToShape.get(WIDE_CHAR)); + + for (int i = 0; i < text.size(); i++) { + char[] lineChars = text.get(i).toCharArray(); + for (int j = 0; j < lineChars.length; j++) { + shapes.addAll(Shape.translate( + charToShape.get(lineChars[j]), + new Vector2(j * width, -i * height) + )); + } + } + + return new ShapeFrameSet(Shape.flip(Shape.normalize(shapes))); + } + + public static boolean isTxtFile(String path) { + return path.matches(".*\\.txt"); + } +} diff --git a/src/main/java/sh/ball/shapes/CubicBezierCurve.java b/src/main/java/sh/ball/shapes/CubicBezierCurve.java index f7dce208..6867e9a3 100644 --- a/src/main/java/sh/ball/shapes/CubicBezierCurve.java +++ b/src/main/java/sh/ball/shapes/CubicBezierCurve.java @@ -1,58 +1,58 @@ -package sh.ball.shapes; - -public class CubicBezierCurve extends Shape { - - private final Vector2 p0; - private final Vector2 p1; - private final Vector2 p2; - private final Vector2 p3; - - public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight) { - this.p0 = p0; - this.p1 = p1; - this.p2 = p2; - this.p3 = p3; - this.weight = weight; - this.length = new Line(p0, p3).length; - } - - public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) { - this(p0, p1, p2, p3, DEFAULT_WEIGHT); - } - - @Override - public Vector2 nextVector(double t) { - return p0.scale(Math.pow(1 - t, 3)) - .add(p1.scale(3 * Math.pow(1 - t, 2) * t)) - .add(p2.scale(3 * (1 - t) * Math.pow(t, 2))) - .add(p3.scale(Math.pow(t, 3))); - } - - @Override - public CubicBezierCurve rotate(double theta) { - return new CubicBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), - p3.rotate(theta), weight); - } - - @Override - public CubicBezierCurve scale(double factor) { - return scale(new Vector2(factor)); - } - - @Override - public CubicBezierCurve scale(Vector2 vector) { - return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), - p3.scale(vector), weight); - } - - @Override - public CubicBezierCurve translate(Vector2 vector) { - return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector), - p3.translate(vector), weight); - } - - @Override - public CubicBezierCurve setWeight(double weight) { - return new CubicBezierCurve(p0, p1, p2, p3, weight); - } -} +package sh.ball.shapes; + +public class CubicBezierCurve extends Shape { + + private final Vector2 p0; + private final Vector2 p1; + private final Vector2 p2; + private final Vector2 p3; + + public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight) { + this.p0 = p0; + this.p1 = p1; + this.p2 = p2; + this.p3 = p3; + this.weight = weight; + this.length = new Line(p0, p3).length; + } + + public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) { + this(p0, p1, p2, p3, DEFAULT_WEIGHT); + } + + @Override + public Vector2 nextVector(double t) { + return p0.scale(Math.pow(1 - t, 3)) + .add(p1.scale(3 * Math.pow(1 - t, 2) * t)) + .add(p2.scale(3 * (1 - t) * Math.pow(t, 2))) + .add(p3.scale(Math.pow(t, 3))); + } + + @Override + public CubicBezierCurve rotate(double theta) { + return new CubicBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), + p3.rotate(theta), weight); + } + + @Override + public CubicBezierCurve scale(double factor) { + return scale(new Vector2(factor)); + } + + @Override + public CubicBezierCurve scale(Vector2 vector) { + return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), + p3.scale(vector), weight); + } + + @Override + public CubicBezierCurve translate(Vector2 vector) { + return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector), + p3.translate(vector), weight); + } + + @Override + public CubicBezierCurve setWeight(double weight) { + return new CubicBezierCurve(p0, p1, p2, p3, weight); + } +} diff --git a/src/main/java/sh/ball/shapes/QuadraticBezierCurve.java b/src/main/java/sh/ball/shapes/QuadraticBezierCurve.java index 4de3c0c8..6795bdd7 100644 --- a/src/main/java/sh/ball/shapes/QuadraticBezierCurve.java +++ b/src/main/java/sh/ball/shapes/QuadraticBezierCurve.java @@ -1,52 +1,52 @@ -package sh.ball.shapes; - -public class QuadraticBezierCurve extends Shape { - - private final Vector2 p0; - private final Vector2 p1; - private final Vector2 p2; - - public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight) { - this.p0 = p0; - this.p1 = p1; - this.p2 = p2; - this.weight = weight; - this.length = new Line(p0, p2).length; - } - - public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) { - this(p0, p1, p2, DEFAULT_WEIGHT); - } - - @Override - public Vector2 nextVector(double t) { - return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2))) - .add(p2.sub(p1).scale(Math.pow(t, 2))); - } - - @Override - public QuadraticBezierCurve rotate(double theta) { - return new QuadraticBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), weight); - } - - @Override - public QuadraticBezierCurve scale(double factor) { - return new QuadraticBezierCurve(p0.scale(factor), p1.scale(factor), p2.scale(factor), weight); - } - - @Override - public QuadraticBezierCurve scale(Vector2 vector) { - return new QuadraticBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), weight); - } - - @Override - public QuadraticBezierCurve translate(Vector2 vector) { - return new QuadraticBezierCurve(p0.translate(vector), p1.translate(vector), - p2.translate(vector), weight); - } - - @Override - public QuadraticBezierCurve setWeight(double weight) { - return new QuadraticBezierCurve(p0, p1, p2, weight); - } -} +package sh.ball.shapes; + +public class QuadraticBezierCurve extends Shape { + + private final Vector2 p0; + private final Vector2 p1; + private final Vector2 p2; + + public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight) { + this.p0 = p0; + this.p1 = p1; + this.p2 = p2; + this.weight = weight; + this.length = new Line(p0, p2).length; + } + + public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) { + this(p0, p1, p2, DEFAULT_WEIGHT); + } + + @Override + public Vector2 nextVector(double t) { + return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2))) + .add(p2.sub(p1).scale(Math.pow(t, 2))); + } + + @Override + public QuadraticBezierCurve rotate(double theta) { + return new QuadraticBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), weight); + } + + @Override + public QuadraticBezierCurve scale(double factor) { + return new QuadraticBezierCurve(p0.scale(factor), p1.scale(factor), p2.scale(factor), weight); + } + + @Override + public QuadraticBezierCurve scale(Vector2 vector) { + return new QuadraticBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), weight); + } + + @Override + public QuadraticBezierCurve translate(Vector2 vector) { + return new QuadraticBezierCurve(p0.translate(vector), p1.translate(vector), + p2.translate(vector), weight); + } + + @Override + public QuadraticBezierCurve setWeight(double weight) { + return new QuadraticBezierCurve(p0, p1, p2, weight); + } +} diff --git a/src/main/resources/css/main.css b/src/main/resources/css/main.css index 6003a6d7..2ec1850b 100644 --- a/src/main/resources/css/main.css +++ b/src/main/resources/css/main.css @@ -1,84 +1,87 @@ -.root { - dark_color: #282828; - darker_color: #1d1d1d; - very_dark: #111111; - grey_color: #555555; - accent_color: #00CC00; - -fx-background-color: dark_color; - -fx-text-background-color: white; - -fx-text-inner-color: white; -} - - - -.titled-pane, .text { - -fx-font-size: 13; - -fx-font-smoothing-type: gray; - -fx-text-fill: white; -} - -.titled-pane > .title -{ - -fx-background-color: very_dark; - -fx-background-radius: 0; -} - -.titled-pane > *.content -{ - -fx-background-color: darker_color; - -fx-border-width: 0; -} - -.titled-pane:focused > .title > .arrow-button .arrow -{ - -fx-background-color: darker_color; -} - -.button { - -fx-background-color: very_dark; - -fx-border-color: white; - -fx-border-width: 1; - -fx-text-fill: white; -} - -.slider .thumb { - -fx-background-color: very_dark; - -fx-border-color: white; - -fx-border-radius: 1.0em; /* makes sure this remains circular */ - -fx-effect: inherit; -} -.slider .track { - -fx-background-color: grey_color; - -fx-padding: 0.2em; -} - -.check-box > .box { - -fx-background-radius: 0; - -fx-background-color: very_dark; - -fx-border-color: white; - -fx-border-width: 1; -} -.check-box > .box > .mark { - -fx-shape: "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"; -} - -.check-box:selected > .box > .mark, -.check-box:indeterminate > .box > .mark { - -fx-background-color: white; -} - -.text-field { - -fx-background-color: very_dark; - -fx-border-color: white; - -fx-border-width: 1; - -fx-border-radius: 0; - -fx-padding: 0.2em; -} - -.text-field .text { - -fx-text-fill: white; -} - -#frequency .text { - -fx-font-size: 20; +.root { + dark_color: #424242; + darker_color: #212121; + very_dark: #111111; + grey_color: #555555; + accent_color: #00CC00; + -fx-background-color: dark_color; + -fx-text-background-color: white; + -fx-text-inner-color: white; +} + +.titled-pane, .text { + -fx-font-size: 13; + -fx-font-smoothing-type: gray; + -fx-text-fill: white; +} + +.titled-pane > .title +{ + -fx-background-color: very_dark; + -fx-background-radius: 0; +} + +.titled-pane > *.content +{ + -fx-background-color: darker_color; + -fx-border-width: 0; +} + +.titled-pane:focused > .title > .arrow-button .arrow +{ + -fx-background-color: darker_color; +} + +.button { + -fx-background-color: very_dark; + -fx-border-color: white; + -fx-border-width: 1; + -fx-text-fill: white; +} + +.slider .thumb { + -fx-background-color: very_dark; + -fx-border-color: white; + -fx-border-radius: 1.0em; /* makes sure this remains circular */ + -fx-effect: inherit; +} +.slider .track { + -fx-background-color: grey_color; + -fx-padding: 0.2em; +} + +.check-box > .box { + -fx-background-radius: 0; + -fx-background-color: very_dark; + -fx-border-color: white; + -fx-border-width: 1; +} +.check-box > .box > .mark { + -fx-shape: "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"; +} + +.check-box:selected > .box > .mark, +.check-box:indeterminate > .box > .mark { + -fx-background-color: white; +} + +.text-field { + -fx-background-color: very_dark; + -fx-border-color: white; + -fx-border-width: 1; + -fx-border-radius: 0; + -fx-padding: 0.2em; +} + +.text-field .text { + -fx-text-fill: white; +} + +#frequency .text { + -fx-font-size: 20; +} + +#control-pane, .titled-pane { + -fx-background-color: darker_color; + -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 0); } \ No newline at end of file diff --git a/src/main/resources/fxml/osci-render.fxml b/src/main/resources/fxml/osci-render.fxml index f4db712c..b1563ef1 100644 --- a/src/main/resources/fxml/osci-render.fxml +++ b/src/main/resources/fxml/osci-render.fxml @@ -7,88 +7,86 @@ - - - - - - - - - - - - - -