diff --git a/pom.xml b/pom.xml index d43cd25..3d2b231 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ sh.ball osci-render - 1.32.3 + 1.33.0 osci-render diff --git a/src/main/java/sh/ball/audio/effect/AnimationType.java b/src/main/java/sh/ball/audio/effect/AnimationType.java index 8b59e45..df731e0 100644 --- a/src/main/java/sh/ball/audio/effect/AnimationType.java +++ b/src/main/java/sh/ball/audio/effect/AnimationType.java @@ -1,16 +1,20 @@ package sh.ball.audio.effect; public enum AnimationType { - STATIC("Static"), - SEESAW("Seesaw"), - LINEAR("Linear"), - FORWARD("Forward"), - REVERSE("Reverse"); + STATIC("Static", "Static"), + SINE("Sine", "Sine"), + SQUARE("Square", "Square"), + SEESAW("Seesaw", "Seesaw"), + LINEAR("Linear", "Triangle"), + FORWARD("Forward", "Sawtooth"), + REVERSE("Reverse", "Reverse Sawtooth"); private final String name; + private final String displayName; - AnimationType(String name) { + AnimationType(String name, String displayName) { this.name = name; + this.displayName = displayName; } public static AnimationType fromString(String name) { @@ -22,8 +26,12 @@ public enum AnimationType { return null; } - @Override - public String toString() { + public String getName() { return name; } + + @Override + public String toString() { + return displayName; + } } diff --git a/src/main/java/sh/ball/audio/effect/EffectAnimator.java b/src/main/java/sh/ball/audio/effect/EffectAnimator.java index 41d2857..e0ceaa7 100644 --- a/src/main/java/sh/ball/audio/effect/EffectAnimator.java +++ b/src/main/java/sh/ball/audio/effect/EffectAnimator.java @@ -6,15 +6,12 @@ public class EffectAnimator extends PhaseEffect implements SettableEffect { public static final int DEFAULT_SAMPLE_RATE = 192000; - private static final double SPEED_SCALE = 20.0; - private final SettableEffect effect; private AnimationType type = AnimationType.STATIC; private boolean justSetToStatic = true; private double targetValue = 0.5; private double actualValue = 0.5; - private boolean linearDirection = true; private double min; private double max; @@ -31,7 +28,6 @@ public class EffectAnimator extends PhaseEffect implements SettableEffect { public void setAnimation(AnimationType type) { this.type = type; - this.linearDirection = true; if (type == AnimationType.STATIC) { justSetToStatic = true; } @@ -58,48 +54,26 @@ public class EffectAnimator extends PhaseEffect implements SettableEffect { double minValue = min; double maxValue = max; - double range = maxValue - minValue; - if (range <= 0) { - return vector; - } - double normalisedTargetValue = (targetValue - minValue) / range; - double normalisedActualValue = (actualValue - minValue) / range; + double phase = nextTheta(); + double percentage = phase / (2 * Math.PI); switch (type) { case SEESAW -> { - double scalar = 10 * Math.max(Math.min(normalisedActualValue, 1 - normalisedActualValue), 0.01); - double change = range * scalar * SPEED_SCALE * normalisedTargetValue / sampleRate; - if (actualValue + change > maxValue || actualValue - change < minValue) { - linearDirection = !linearDirection; - } - if (linearDirection) { - actualValue += change; - } else { - actualValue -= change; - } + // modified sigmoid function + actualValue = (percentage < 0.5) ? percentage * 2 : (1 - percentage) * 2; + actualValue = 1 / (1 + Math.exp(-16 * (actualValue - 0.5))); + actualValue = actualValue * (maxValue - minValue) + minValue; + } + case SINE -> { + actualValue = Math.sin(phase) * 0.5 + 0.5; + actualValue = actualValue * (maxValue - minValue) + minValue; } case LINEAR -> { - double change = range * SPEED_SCALE * normalisedTargetValue / sampleRate; - if (actualValue + change > maxValue || actualValue - change < minValue) { - linearDirection = !linearDirection; - } - if (linearDirection) { - actualValue += change; - } else { - actualValue -= change; - } - } - case FORWARD -> { - actualValue += range * 0.5 * SPEED_SCALE * normalisedTargetValue / sampleRate; - if (actualValue > maxValue) { - actualValue = minValue; - } - } - case REVERSE -> { - actualValue -= range * 0.5 * SPEED_SCALE * normalisedTargetValue / sampleRate; - if (actualValue < minValue) { - actualValue = maxValue; - } + actualValue = (percentage < 0.5) ? percentage * 2 : (1 - percentage) * 2; + actualValue = actualValue * (maxValue - minValue) + minValue; } + case SQUARE -> actualValue = (percentage < 0.5) ? maxValue : minValue; + case FORWARD -> actualValue = percentage * (maxValue - minValue) + minValue; + case REVERSE -> actualValue = (1 - percentage) * (maxValue - minValue) + minValue; } if (actualValue > maxValue) { actualValue = maxValue; diff --git a/src/main/java/sh/ball/audio/effect/PhaseEffect.java b/src/main/java/sh/ball/audio/effect/PhaseEffect.java index e4cf693..73984c0 100644 --- a/src/main/java/sh/ball/audio/effect/PhaseEffect.java +++ b/src/main/java/sh/ball/audio/effect/PhaseEffect.java @@ -2,14 +2,10 @@ package sh.ball.audio.effect; public abstract class PhaseEffect implements Effect { - // sufficiently large so that nextTheta doesn't commonly reach it, otherwise - // there are audio artifacts - private static final double LARGE_VAL = 2 << 20; - protected int sampleRate; protected double speed; - private double phase = -LARGE_VAL; + private double phase = 0; protected PhaseEffect(int sampleRate, double speed) { this.sampleRate = sampleRate; @@ -17,17 +13,17 @@ public abstract class PhaseEffect implements Effect { } public void resetTheta() { - phase = -LARGE_VAL; + phase = 0; } protected double nextTheta() { phase += speed / sampleRate; - if (phase >= LARGE_VAL) { - phase = -LARGE_VAL; + if (phase > 1) { + phase -= 1; } - return phase * Math.PI; + return phase * 2 * Math.PI; } public void setSpeed(double speed) { diff --git a/src/main/java/sh/ball/audio/effect/SineEffect.java b/src/main/java/sh/ball/audio/effect/SineEffect.java deleted file mode 100644 index 647ed26..0000000 --- a/src/main/java/sh/ball/audio/effect/SineEffect.java +++ /dev/null @@ -1,39 +0,0 @@ -package sh.ball.audio.effect; - -import sh.ball.shapes.Vector2; - -// Plays a sine wave at the given frequency and volume -public class SineEffect extends PhaseEffect { - - private static final double DEFAULT_VOLUME = 1; - - private double frequency; - private double volume; - - public SineEffect(int sampleRate, double frequency, double volume) { - super(sampleRate, 2); - this.frequency = frequency; - this.volume = Math.max(Math.min(volume, 1), 0); - } - - public SineEffect(int sampleRate, double frequency) { - this(sampleRate, frequency, DEFAULT_VOLUME); - } - - public void setVolume(double volume) { - this.volume = volume; - } - - public void setFrequency(double frequency) { - this.frequency = frequency; - } - - @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.cos(frequency * theta); - - return new Vector2(x, y); - } -} diff --git a/src/main/java/sh/ball/gui/GitHubReleaseDetector.java b/src/main/java/sh/ball/gui/GitHubReleaseDetector.java index 1dea015..2f88118 100644 --- a/src/main/java/sh/ball/gui/GitHubReleaseDetector.java +++ b/src/main/java/sh/ball/gui/GitHubReleaseDetector.java @@ -42,7 +42,7 @@ public class GitHubReleaseDetector { String latestRelease = null; for (int i = 0; i < releases.length(); i++) { JSONObject release = releases.getJSONObject(i); - String version = release.getString("tag_name").replaceAll("^v", ""); + String version = release.getString("tag_name"); // Compare versions and update the latestRelease variable if necessary if (compareVersions(version, latestRelease) > 0) { @@ -58,12 +58,14 @@ public class GitHubReleaseDetector { return null; } - private static int compareVersions(String v1, String v2) { + public static int compareVersions(String v1, String v2) { if (v1 == null) { return -1; } else if (v2 == null) { return 1; } + v1 = v1.replaceAll("^v", ""); + v2 = v2.replaceAll("^v", ""); // Split the version strings on "." characters String[] v1Parts = v1.split("\\."); String[] v2Parts = v2.split("\\."); diff --git a/src/main/java/sh/ball/gui/components/EffectComponentGroup.java b/src/main/java/sh/ball/gui/components/EffectComponentGroup.java index 896371b..8611e6b 100644 --- a/src/main/java/sh/ball/gui/components/EffectComponentGroup.java +++ b/src/main/java/sh/ball/gui/components/EffectComponentGroup.java @@ -34,7 +34,6 @@ public class EffectComponentGroup extends HBox { private final DoubleProperty min = new SimpleDoubleProperty(); private final DoubleProperty max = new SimpleDoubleProperty(); private final BooleanProperty micSelected = new SimpleBooleanProperty(false); - private final BooleanProperty enabled = new SimpleBooleanProperty(false); public EffectComponentGroup() { EffectComponentGroupController temp; @@ -67,8 +66,12 @@ public class EffectComponentGroup extends HBox { public synchronized void hideSpinner() { controller.spinner.setVisible(false); controller.spinner.setPrefWidth(0); + controller.animationSpinner.setVisible(false); + controller.animationSpinner.setPrefWidth(0); controller.label.setPrefWidth(90); controller.slider.setPrefWidth(170); + controller.animationSlider.setPrefWidth(170); + controller.animationSlider.setMajorTickUnit(20); HBox.setMargin(controller.midi, new Insets(11, 5, 0, -7)); } diff --git a/src/main/java/sh/ball/gui/components/EffectComponentGroupController.java b/src/main/java/sh/ball/gui/components/EffectComponentGroupController.java index 572bc4f..024aa68 100644 --- a/src/main/java/sh/ball/gui/components/EffectComponentGroupController.java +++ b/src/main/java/sh/ball/gui/components/EffectComponentGroupController.java @@ -70,8 +70,12 @@ public class EffectComponentGroupController implements Initializable, SubControl @FXML public Slider slider; @FXML + public Slider animationSlider; + @FXML public Spinner spinner; @FXML + public Spinner animationSpinner; + @FXML public SVGPath midi; @FXML public ComboBox comboBox; @@ -138,7 +142,12 @@ public class EffectComponentGroupController implements Initializable, SubControl spinner.valueProperty().addListener((o, oldValue, newValue) -> slider.setValue(newValue)); slider.valueProperty().addListener((o, oldValue, newValue) -> spinner.getValueFactory().setValue(newValue.doubleValue())); - List animations = List.of(AnimationType.STATIC, AnimationType.SEESAW, AnimationType.LINEAR, AnimationType.FORWARD, AnimationType.REVERSE); + animationSpinner.valueProperty().addListener((o, oldValue, newValue) -> animationSlider.setValue(newValue)); + animationSlider.valueProperty().addListener((o, oldValue, newValue) -> animationSpinner.getValueFactory().setValue(newValue.doubleValue())); + + animationSpinner.setValueFactory(new SpinnerValueFactory.DoubleSpinnerValueFactory(0, 100, 1, 0.05)); + + List animations = List.of(AnimationType.values()); comboBox.setItems(FXCollections.observableList(animations)); comboBox.setValue(AnimationType.STATIC); @@ -180,9 +189,12 @@ public class EffectComponentGroupController implements Initializable, SubControl document.createTextNode(effectCheckBox.selectedProperty().getValue().toString()) ); Element animation = document.createElement("animation"); - animation.appendChild(document.createTextNode(comboBox.getValue().toString())); + animation.appendChild(document.createTextNode(comboBox.getValue().getName())); + Element animationSpeed = document.createElement("animationSpeed"); + animationSpeed.appendChild(document.createTextNode(Double.toString(animationSlider.getValue()))); checkBox.appendChild(selected); checkBox.appendChild(animation); + checkBox.appendChild(animationSpeed); return List.of(checkBox); } @@ -203,11 +215,23 @@ public class EffectComponentGroupController implements Initializable, SubControl selected = checkBox.getElementsByTagName("selected").item(0).getTextContent(); if (checkBox.getElementsByTagName("animation").getLength() > 0) { String animation = checkBox.getElementsByTagName("animation").item(0).getTextContent(); - comboBox.setValue(AnimationType.fromString(animation)); + AnimationType type = AnimationType.fromString(animation); + comboBox.setValue(type); + + if (checkBox.getElementsByTagName("animationSpeed").getLength() > 0) { + String animationSpeed = checkBox.getElementsByTagName("animationSpeed").item(0).getTextContent(); + animationSlider.setValue(Double.parseDouble(animationSpeed)); + } else if (type != AnimationType.STATIC) { + // backwards compatibility - must translate the old animation that uses the slider value, + // to the new animation that uses the animation slider value + double percentage = (slider.getValue() - slider.getMin()) / (slider.getMax() - slider.getMin()); + animationSlider.setValue(percentage * 10); + } } } else { selected = checkBox.getTextContent(); comboBox.setValue(AnimationType.STATIC); + animationSlider.setValue(1.0); } effectCheckBox.setSelected(Boolean.parseBoolean(selected) || model.isAlwaysEnabled()); } @@ -227,7 +251,9 @@ public class EffectComponentGroupController implements Initializable, SubControl this.animator.setValue(slider.getValue()); updater.run(model.getType(), selected, this.animator); slider.setDisable(!selected); + animationSlider.setDisable(!selected); spinner.setDisable(!selected); + animationSpinner.setDisable(!selected); }; effectCheckBox.selectedProperty().addListener(listener); @@ -250,12 +276,23 @@ public class EffectComponentGroupController implements Initializable, SubControl slider.minProperty().addListener((e, old, min) -> this.animator.setMin(min.doubleValue())); slider.maxProperty().addListener((e, old, max) -> this.animator.setMax(max.doubleValue())); slider.valueProperty().addListener((e, old, value) -> this.animator.setValue(value.doubleValue())); + animationSlider.valueProperty().addListener((e, old, value) -> this.animator.setSpeed(value.doubleValue())); comboBox.valueProperty().addListener((options, old, animationType) -> { this.animator.setAnimation(animationType); if (animationType != AnimationType.STATIC) { - slider.setStyle("-thumb-color: #00ff00;"); + animationSlider.setVisible(true); + slider.setVisible(false); + if (!model.isSpinnerHidden()) { + animationSpinner.setVisible(true); + } + spinner.setVisible(false); } else { - slider.setStyle(""); + animationSlider.setVisible(false); + slider.setVisible(true); + animationSpinner.setVisible(false); + if (!model.isSpinnerHidden()) { + spinner.setVisible(true); + } } }); } @@ -284,7 +321,9 @@ public class EffectComponentGroupController implements Initializable, SubControl public void setInactive(boolean inactive) { slider.setDisable(inactive); + animationSlider.setDisable(inactive); spinner.setDisable(inactive); + animationSpinner.setDisable(inactive); } public void setValue(double value) { diff --git a/src/main/java/sh/ball/gui/controller/MainController.java b/src/main/java/sh/ball/gui/controller/MainController.java index a01a0dc..50933ba 100644 --- a/src/main/java/sh/ball/gui/controller/MainController.java +++ b/src/main/java/sh/ball/gui/controller/MainController.java @@ -55,6 +55,7 @@ import sh.ball.audio.midi.MidiListener; import sh.ball.audio.midi.MidiNote; import sh.ball.engine.ObjectServer; import sh.ball.engine.ObjectSet; +import sh.ball.gui.GitHubReleaseDetector; import sh.ball.gui.Gui; import sh.ball.gui.components.EffectComponentGroup; import sh.ball.oscilloscope.ByteWebSocketServer; @@ -1095,9 +1096,14 @@ public class MainController implements Initializable, FrequencyListener, MidiLis } } - private void loadSliderValues(List sliders, List labels, Element root) { + private void loadSliderValues(List sliders, List labels, Element root, String version) { for (int i = 0; i < sliders.size(); i++) { - NodeList nodes = root.getElementsByTagName(labels.get(i)); + String label = labels.get(i); + NodeList nodes = root.getElementsByTagName(label); + + // backwards compatibility (PhaseEffect speed was doubled in v1.33.0) + boolean phaseEffectSpeedDoubled = (label.equals("translationSpeed") || label.equals("rotateSpeed")) && (version == null || GitHubReleaseDetector.compareVersions(version, "1.33.0") < 0); + // backwards compatibility if (nodes.getLength() > 0) { Element sliderElement = (Element) nodes.item(0); @@ -1110,13 +1116,24 @@ public class MainController implements Initializable, FrequencyListener, MidiLis value = sliderElement.getElementsByTagName("value").item(0).getTextContent(); String min = sliderElement.getElementsByTagName("min").item(0).getTextContent(); String max = sliderElement.getElementsByTagName("max").item(0).getTextContent(); - slider.setMin(Double.parseDouble(min)); - slider.setMax(Double.parseDouble(max)); + double minValue = Double.parseDouble(min); + double maxValue = Double.parseDouble(max); + if (phaseEffectSpeedDoubled) { + minValue /= 2; + maxValue /= 2; + } + slider.setMin(minValue); + slider.setMax(maxValue); updateSliderUnits(slider); } - slider.setValue(Double.parseDouble(value)); - targetSliderValue[i] = Double.parseDouble(value); + double sliderValue = Double.parseDouble(value); + if (phaseEffectSpeedDoubled) { + sliderValue /= 2; + } + + slider.setValue(sliderValue); + targetSliderValue[i] = sliderValue; } } @@ -1310,8 +1327,12 @@ public class MainController implements Initializable, FrequencyListener, MidiLis generalController.disablePlayback(); Element root = document.getDocumentElement(); + String version = null; + if (root.getElementsByTagName("version").getLength() > 0) { + version = root.getElementsByTagName("version").item(0).getTextContent(); + } Element slidersElement = (Element) root.getElementsByTagName("sliders").item(0); - loadSliderValues(sliders, labels, slidersElement); + loadSliderValues(sliders, labels, slidersElement, version); // doesn't exist on newer projects - backwards compatibility Element objectRotation = (Element) root.getElementsByTagName("objectRotation").item(0); diff --git a/src/main/java/sh/ball/gui/controller/ProjectSelectController.java b/src/main/java/sh/ball/gui/controller/ProjectSelectController.java index 04311f3..dcd6f81 100644 --- a/src/main/java/sh/ball/gui/controller/ProjectSelectController.java +++ b/src/main/java/sh/ball/gui/controller/ProjectSelectController.java @@ -95,7 +95,7 @@ public class ProjectSelectController implements Initializable { Platform.runLater(() -> { String currentVersion = versionLabel.getText().replaceAll("^v", ""); if (!latestRelease.equals(currentVersion)) { - latestReleaseHyperlink.setText("v" + latestRelease + " is the latest version on GitHub!"); + latestReleaseHyperlink.setText(latestRelease + " is the latest version on GitHub!"); projectVBox.getChildren().add(projectVBox.getChildren().size() - 1, latestReleaseHyperlink); } }); diff --git a/src/main/resources/CHANGELOG.md b/src/main/resources/CHANGELOG.md index adc117e..52bd46c 100644 --- a/src/main/resources/CHANGELOG.md +++ b/src/main/resources/CHANGELOG.md @@ -1,3 +1,19 @@ +- 1.33.0 + - Overhaul how animations work in the interface for more precise and intuitive control over the animation speed + - When the animation type changes, the range of the slider changes and changing the slider changes the frequency of the animation + - The value of the slider when there is no animation (Static) is saved and restored when the animation is removed + - Animation speeds vary from 0Hz to 100Hz + - You should not notice any difference in how animation is enabled or controlled, but the animation speed slider now goes much faster + - As before, to control the range of the animation, just change the range of the slider from the "Slider" menu + - Add new animation type: "Sine" + - Add new animation type: "Square" + - Rename "Linear" animation to "Triangle" + - Rename "Forward" animation to "Sawtooth" + - Rename "Reverse" animation to "Reverse Sawtooth" + - These new changes should be backwards compatible with projects in prior versions + - If you notice any issues or differences in projects loaded from older versions, please let me know! + + - 1.32.3 - Only check for XTAudio devices if not on macOS as it is not available on macOS diff --git a/src/main/resources/css/main.css b/src/main/resources/css/main.css index eb3ab5a..c29c1d0 100644 --- a/src/main/resources/css/main.css +++ b/src/main/resources/css/main.css @@ -129,6 +129,10 @@ -fx-shape: "M460,530.874 1,265.87 460,0.866z"; } +.animationSlider > .thumb { + -thumb-color: #00ff00; +} + .thresholdSlider > .track { -fx-background-color: transparent; } diff --git a/src/main/resources/fxml/effectComponentGroup.fxml b/src/main/resources/fxml/effectComponentGroup.fxml index 7f6c389..962dbad 100644 --- a/src/main/resources/fxml/effectComponentGroup.fxml +++ b/src/main/resources/fxml/effectComponentGroup.fxml @@ -7,9 +7,10 @@ + - + @@ -17,16 +18,40 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/effects.fxml b/src/main/resources/fxml/effects.fxml index 952334f..e981663 100644 --- a/src/main/resources/fxml/effects.fxml +++ b/src/main/resources/fxml/effects.fxml @@ -68,7 +68,7 @@ - + @@ -127,7 +127,7 @@ - +