diff --git a/pom.xml b/pom.xml
index 72361bb5..1be87b0e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
UTF-8
11
- 15
+ 17-ea+8
diff --git a/src/main/java/com/utsusynth/utsu/UtsuModule.java b/src/main/java/com/utsusynth/utsu/UtsuModule.java
index 410eeea0..9958f772 100644
--- a/src/main/java/com/utsusynth/utsu/UtsuModule.java
+++ b/src/main/java/com/utsusynth/utsu/UtsuModule.java
@@ -146,11 +146,13 @@ private PreferencesManager providePreferencesManager(
defaultBuilder.put("resampler", assetManager.getResamplerFile().getAbsolutePath());
defaultBuilder.put("wavtool", assetManager.getWavtoolFile().getAbsolutePath());
defaultBuilder.put("voicebank", assetManager.getVoicePath().getAbsolutePath());
+ defaultBuilder.put("metronome", assetManager.getMetronomeFile().getAbsolutePath());
return new PreferencesManager(
settingsPath,
documentBuilderFactory,
TransformerFactory.newDefaultInstance(),
- defaultBuilder.build());
+ defaultBuilder.build()
+ );
}
@Provides
diff --git a/src/main/java/com/utsusynth/utsu/controller/common/IconManager.java b/src/main/java/com/utsusynth/utsu/controller/common/IconManager.java
index d486979b..f908ed2f 100644
--- a/src/main/java/com/utsusynth/utsu/controller/common/IconManager.java
+++ b/src/main/java/com/utsusynth/utsu/controller/common/IconManager.java
@@ -5,6 +5,7 @@
import javafx.scene.layout.Pane;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.SVGPath;
/** Singleton class, supplier of icon shapes and settings. */
public class IconManager {
@@ -51,6 +52,14 @@ public void setStopIcon(Pane parent) {
parent.getChildren().add(stopIcon);
}
+ public void setMetronomeIcon(Pane parent) {
+ SVGPath metronomeIcon = new SVGPath();
+ metronomeIcon.setContent("M7 25C7 25 10.662 7.0634 14.2803 7.0634C17.8986 7.0634 14.013 7 17.3848 7C20.7567 7 25 24.8431 25 24.8431L7 25Z");
+ metronomeIcon.getStyleClass().addAll("playback-icon", "not-selected");
+ parent.getChildren().clear();
+ parent.getChildren().add(metronomeIcon);
+ }
+
public void selectIcon(Pane selectMe) {
Node child = selectMe.getChildren().get(0);
if (child instanceof Group) {
diff --git a/src/main/java/com/utsusynth/utsu/controller/song/SongController.java b/src/main/java/com/utsusynth/utsu/controller/song/SongController.java
index 9190c06a..a2235001 100644
--- a/src/main/java/com/utsusynth/utsu/controller/song/SongController.java
+++ b/src/main/java/com/utsusynth/utsu/controller/song/SongController.java
@@ -47,10 +47,7 @@
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyCodeCombination;
-import javafx.scene.input.KeyCombination;
-import javafx.scene.input.KeyEvent;
+import javafx.scene.input.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.shape.Rectangle;
@@ -124,6 +121,9 @@ public class SongController implements EditorController, Localizable {
@FXML // fx:id="stopIcon"
private AnchorPane stopIcon; // Value injected by FXMLLoader
+ @FXML // fx:id="metronomeIcon"
+ private AnchorPane metronomeIcon;
+
@FXML // fx:id="quantizeChoiceBox"
private ChoiceBox quantizeChoiceBox; // Value injected by FXMLLoader
@@ -349,6 +349,14 @@ public List getVoicebankSuffixes() {
iconManager.setStopIcon(stopIcon);
stopIcon.setOnMousePressed(event -> iconManager.selectIcon(stopIcon));
stopIcon.setOnMouseReleased(event -> iconManager.deselectIcon(stopIcon));
+ iconManager.setMetronomeIcon(metronomeIcon);
+ preferencesManager.getMetronomeEnabled().addListener(((observable, oldValue, newValue) -> {
+ if (newValue == true) {
+ iconManager.selectIcon(metronomeIcon);
+ } else {
+ iconManager.deselectIcon(metronomeIcon);
+ }
+ }));
refreshView();
@@ -1370,4 +1378,8 @@ public void invokePlugin(File plugin) {
}
}
}
+
+ public void toggleMetronome(MouseEvent mouseEvent) {
+ preferencesManager.getMetronomeEnabled().set(!preferencesManager.getMetronomeEnabled().getValue());
+ }
}
diff --git a/src/main/java/com/utsusynth/utsu/engine/Engine.java b/src/main/java/com/utsusynth/utsu/engine/Engine.java
index 35b3f81d..a4012d1f 100644
--- a/src/main/java/com/utsusynth/utsu/engine/Engine.java
+++ b/src/main/java/com/utsusynth/utsu/engine/Engine.java
@@ -1,12 +1,12 @@
package com.utsusynth.utsu.engine;
import com.google.common.base.Function;
-import com.utsusynth.utsu.common.utils.RegionBounds;
import com.utsusynth.utsu.common.StatusBar;
import com.utsusynth.utsu.common.data.LyricConfigData;
import com.utsusynth.utsu.common.exception.ErrorLogger;
import com.utsusynth.utsu.common.quantize.Quantizer;
import com.utsusynth.utsu.common.utils.PitchUtils;
+import com.utsusynth.utsu.common.utils.RegionBounds;
import com.utsusynth.utsu.engine.wavtool.UtsuWavtool;
import com.utsusynth.utsu.engine.wavtool.Wavtool;
import com.utsusynth.utsu.files.CacheManager;
@@ -142,6 +142,16 @@ public boolean startPlayback(
instrumentalPlayer.dispose();
}
});
+
+ new Metronome(
+ mediaPlayer,
+ song.getTempo(),
+ String.format("file:%s", this.preferencesManager.getMetronomeFile().getAbsolutePath()),
+ this.preferencesManager.getMetronomeEnabled()
+ );
+
+
+
mediaPlayer.play();
}
return finalSong.isPresent();
diff --git a/src/main/java/com/utsusynth/utsu/engine/Metronome.java b/src/main/java/com/utsusynth/utsu/engine/Metronome.java
new file mode 100644
index 00000000..26ce233b
--- /dev/null
+++ b/src/main/java/com/utsusynth/utsu/engine/Metronome.java
@@ -0,0 +1,86 @@
+package com.utsusynth.utsu.engine;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.collections.ObservableMap;
+import javafx.scene.media.Media;
+import javafx.scene.media.MediaMarkerEvent;
+import javafx.scene.media.MediaPlayer;
+import javafx.util.Duration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Metronome {
+ public Metronome(
+ MediaPlayer songPlayer,
+ Double t,
+ String clickUri,
+ BooleanProperty enabled
+ ) {
+ attach(
+ songPlayer,
+ new MediaPlayer(new Media(
+ clickUri
+ )),
+ generateMarkers(songPlayer.getTotalDuration().toSeconds(), t),
+ enabled
+ );
+ }
+
+ void attach(
+ MediaPlayer songPlayer,
+ MediaPlayer metronomePlayer,
+ List intervals,
+ ObservableBooleanValue enabled
+ ) {
+ ObservableMap mediaMarkers = songPlayer.getMedia().getMarkers();
+ ThreadLocal key = new ThreadLocal<>();
+ key.set(0);
+
+ intervals.forEach(mme -> {
+ key.set(key.get() + 1);
+ mediaMarkers.put(
+ String.valueOf(key.get()),
+ mme
+ );
+ });
+
+ ChangeListener enabledListener = (observable, oldValue, newValue) -> {
+ metronomePlayer.stop();
+ };
+ enabled.addListener(enabledListener);
+ songPlayer.setOnMarker((MediaMarkerEvent mme) -> {
+ if (enabled.get()) {
+ metronomePlayer.stop();
+ metronomePlayer.play();
+ }
+ });
+
+ Runnable onStop = songPlayer.getOnStopped();
+ songPlayer.setOnStopped(() -> {
+ metronomePlayer.stop();
+ metronomePlayer.dispose();
+
+ enabled.removeListener(enabledListener);
+ onStop.run();
+ });
+ }
+
+ List generateMarkers(
+ double seconds,
+ double bpm
+ ) {
+
+ ArrayList events = new ArrayList<>();
+ double bps = bpm * (1.0 / 60.0);
+ double ms_beat = (1.0 / bps) * (1000.0);
+
+ for (int i = 0; i < seconds * 1000; i+=ms_beat) {
+ events.add(new Duration(i));
+ }
+
+ return events;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/utsusynth/utsu/files/AssetManager.java b/src/main/java/com/utsusynth/utsu/files/AssetManager.java
index 5dd4c151..d4d2d10e 100644
--- a/src/main/java/com/utsusynth/utsu/files/AssetManager.java
+++ b/src/main/java/com/utsusynth/utsu/files/AssetManager.java
@@ -68,6 +68,7 @@ public boolean initializeAssets() throws IOException, SecurityException, URISynt
}
copyFile(SOUNDS_SOURCE, soundsPath, "silence.wav", "SILENCE_WAV");
copyFile(SOUNDS_SOURCE, soundsPath, "piano.wav", "PIANO_WAV");
+ copyFile(SOUNDS_SOURCE, soundsPath, "metronome.wav", "METRONOME_WAV");
initializeVoicebank();
// Initialize executables.
@@ -140,6 +141,10 @@ public File getPianoFile() {
return new File(soundsPath, "piano.wav");
}
+ public File getMetronomeFile() {
+ return new File(soundsPath, "metronome.wav");
+ }
+
public File getVoicePath() {
return voicePath;
}
diff --git a/src/main/java/com/utsusynth/utsu/files/PreferencesManager.java b/src/main/java/com/utsusynth/utsu/files/PreferencesManager.java
index 7d27fc93..0129da9f 100644
--- a/src/main/java/com/utsusynth/utsu/files/PreferencesManager.java
+++ b/src/main/java/com/utsusynth/utsu/files/PreferencesManager.java
@@ -36,6 +36,8 @@ public class PreferencesManager {
private BooleanProperty showVoicebankFaceTemp;
private BooleanProperty showVoicebankBodyTemp;
+ private BooleanProperty isMetronomeEnabled;
+
public PreferencesManager(
@SettingsPath File settingsPath,
DocumentBuilderFactory documentBuilderFactory,
@@ -325,4 +327,18 @@ public File getVoicebankDefault() {
public void setVoicebank(File voicebank) {
preferences.put("voicebank", voicebank.getAbsolutePath());
}
+
+ public File getMetronomeFile() {
+ return preferences.containsKey("metronome")
+ ? new File(preferences.get("metronome"))
+ : new File(defaultPreferences.get("metronome"));
+ }
+
+ public BooleanProperty getMetronomeEnabled() {
+ if (isMetronomeEnabled == null) {
+ isMetronomeEnabled = new SimpleBooleanProperty();
+ isMetronomeEnabled.setValue(false);
+ }
+ return isMetronomeEnabled;
+ }
}
diff --git a/src/main/resources/assets/sounds/metronome.wav b/src/main/resources/assets/sounds/metronome.wav
new file mode 100644
index 00000000..425eabe2
Binary files /dev/null and b/src/main/resources/assets/sounds/metronome.wav differ
diff --git a/src/main/resources/css/metronome.svg b/src/main/resources/css/metronome.svg
new file mode 100644
index 00000000..4dd57903
--- /dev/null
+++ b/src/main/resources/css/metronome.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/main/resources/fxml/SongScene.fxml b/src/main/resources/fxml/SongScene.fxml
index 98abb585..c831e1f8 100644
--- a/src/main/resources/fxml/SongScene.fxml
+++ b/src/main/resources/fxml/SongScene.fxml
@@ -26,6 +26,7 @@
+