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 @@
-
+
diff --git a/src/main/resources/fxml/projectSelect.fxml b/src/main/resources/fxml/projectSelect.fxml
index 668db52..235a8b3 100644
--- a/src/main/resources/fxml/projectSelect.fxml
+++ b/src/main/resources/fxml/projectSelect.fxml
@@ -48,7 +48,7 @@
-
+