I'm trying to generate an audio waveform by reading an existing .wav file that updates as the music is playing in real time. For example, if the song is played, I want the waveform to be continuously generated as the song progresses. So far I was able to read data from a .wav file and generate a STATIC waveform using javafx for roughly the first 2500 samples of the song. I compared the waveform to what it looks like in audacity and it matches it pretty well. Here's what it looks like:
However, I'm confused on how I would make the waveform generate not just statically for a particular window, but generate a constantly updated waveform through the duration of the entire song. I was looking into Realtime charts and using ScheduledExecutorService but I can't seem to make the line graph draw a new point faster than once every second without it causing issues. Is there another way I should go about doing this? I've spent a lot of time on this and don't want to give up now. I attached my code below, give it a look. Thanks.
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.shape.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.geometry.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.scene.text.Text;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.Group;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.TargetDataLine;
import java.net.*;
import java.io.*;
import java.nio.*;
public class Practice2 extends Application {
public static AudioInputStream audioInputStream;
#Override
public void start(Stage primaryStage) {
int channelCounter = 2; //used to jump between left and right channel in the loop
try {
File file = new File("testData/record.wav");
byte[] data = new byte[(int)file.length()];
BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
in.read(data);
in.close();
File leftChannelFile = new File("audioData/leftChannelAmpData.txt");
File rightChannelFile = new File("audioData/rightChannelAmpData.txt");
//Defining the x axis
NumberAxis xAxis = new NumberAxis(0, 2490, 1);
xAxis.setLabel("Samples");
//Defining the y axis
NumberAxis yAxis = new NumberAxis (-675, 675, 1);
yAxis.setLabel("Amplitude");
//Creating the line chart
LineChart linechart = new LineChart(xAxis, yAxis);
linechart.setPrefHeight(1350); //picked 1350 and 2500 because it is slightly less than the height and width of my monitor. Will definitely change these values in the future
linechart.setPrefWidth(2500);
linechart.setMaxHeight(1350);
linechart.setMaxWidth(2500);
//Prepare XYChart.Series objects by setting data
XYChart.Series series = new XYChart.Series();
series.setName("Amplitude from sample");
for (int i=44; i < data.length - 1; i+=2) { //start at the 44th byte of the .wav file. This skips the header info and reads the raw data
if (channelCounter % 2 == 0) { //This represents the loop for the left channel
channelCounter++;
short tempAmp = calculateAmpValue(data[i], data[i+1]); // Since the amplitude value for each sample is represented by 2 bytes instead of 1, this function takes the next 2 bytes and gives an integer representation for it (-32728 to 32727)
if (i < 2491)
series.getData().add(new XYChart.Data(i-43, tempAmp / 50)); //divide by 50 so the amplitude values can fit within the range of my graph.
}
else if (channelCounter % 2 == 1) { //this represents the loop for the right channel. ***I'M ONLY BUILDING A WAVEFORM FOR THE LEFT CHANNEL AT THE MOMENT. ***
channelCounter++;
short tempAmp = calculateAmpValue(data[i], data[i+1]);
}
}
//Setting the data to Line chart
linechart.getData().add(series);
Group root = new Group(linechart);
Scene scene = new Scene(root, 2500, 1350);
primaryStage.setTitle("Generated Waveform");
primaryStage.setScene(scene);
primaryStage.show();
}
catch (FileNotFoundException ex){ex.printStackTrace();}
catch (IOException ex){ex.printStackTrace();}
}
public static short calculateAmpValue(byte byte1, byte byte2) {
ByteBuffer bb = ByteBuffer.allocate(2);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.put(byte1);
bb.put(byte2);
short ampVal = bb.getShort(0);
return ampVal;
}
public static void main(String[] args) {
Application.launch(args);
}
}
If I were looking to draw a real-time chart with JavaFX, I'd probably assume I'd be using AnimationTimer for the drawing. It runs at 60 fps. So whatever is being drawn would reflect changes made in that time period. If the audio is 44100 fps, that's 735 audio data points per animation-cycle period. I'm not clear what you are attempting, though. Are you trying to write the equivalent of an oscilloscope?
Related
It seems that when I move the circle outside the group of squares, it moves the squares even though there is no code that does that. I'm 99% percent sure it is because the group is trying to auto center itself, but it only does it if I change the scale of the group. Here is the code that demonstrates the problem I'm currently having.(Note that I don't want the square to move at all)
import javafx.application.Application;
import javafx.scene.shape.Circle;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.util.Duration;
import javafx.scene.layout.Pane;
import javafx.scene.Scene;
import javafx.event.EventHandler;
import javafx.event.ActionEvent;
import javafx.scene.Group;
import javafx.stage.Stage;
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.Color;
public class Test extends Application
{
private static Circle circle = new Circle(0, 0, 10);
private static Group group = new Group(circle);
private static Group mainPane = new Group(group);
private Scene scene = new Scene(mainPane, 600, 600);
public void start(Stage stage){
circle.setViewOrder(-1);
for(int i = 0; i < 100; i++){
Rectangle backGround = new Rectangle(-10, -10, 20, 20);
backGround.setLayoutX((i - (int)(i * .1) * 10) * 20 - 100);
backGround.setLayoutY((int)(i * .1) * 20 - 100);
group.getChildren().add(backGround);
}
/*This line causes the problem*/group.setScaleX(1.5);
/*This line causes the problem*/group.setScaleY(1.5);
group.setLayoutX(200);
group.setLayoutY(200);
circle.setFill(Color.RED);
Timeline time = new Timeline();
time.setCycleCount(Timeline.INDEFINITE);
KeyFrame frame = new KeyFrame(Duration.millis(1), new EventHandler<ActionEvent>(){
#Override public void handle(ActionEvent e){
circle.setLayoutX(circle.getLayoutX() + .1);
if(circle.getLayoutX() > 150) circle.setLayoutX(0);
}
});
time.getKeyFrames().add(frame);
time.playFromStart();
stage.setScene(scene);
stage.show();
}
}
So I believe the problem is that scaleX / scaleY uses the center of the node as the pivot point. Combine that with the fact that the Group grows as the Circle moves to the right, and the scaling causes the Group to "move" because the new center becomes the pivot point.
When I replace:
/*This line causes the problem*/group.setScaleX(1.5);
/*This line causes the problem*/group.setScaleY(1.5);
With:
// javafx.scene.transform.Scale
Scale scale = new Scale(1.5, 1.5);
group.getTransforms().add(scale);
Your problem goes away. Unlike with scaleX / scaleY, the pivot point used by Scale is not automatically adjusted as the Group grows. Note the above Scale is using (0,0) as the pivot point. I assume that's okay, but if you want then you can set the pivot point to the initial center point of the Group (just don't constantly update the pivot point, or you'll probably run into the same problem as before).
The test program below reproduces the problem. I understand why the exception is thrown but I would like to know how can I work around it or use a different construct in JavaFX to get what I want.
The full application is a robot simulator with multiple robots that move autonomously, independently, and simultaneously around a field. Each robot has its own SequentialTransition for its particular set of movements. The program adds the SequentialTransitions to a ParallelTransition, which it then plays. Everything was fine until I put in a listener that notices if a robot runs into an obstacle. I've simplified the collision detection in the test program to apply to only one robot and one wall. The point of the error is marked with //** BROKEN!! IllegalStateException on next line.
I really do want to stop the SequentialTransition for a robot that runs into an obstacle but let the other robot(s) continue. How can I do this?
The error comes up in Java 8 but also in Java 11 and JavaFX 15.
package sample;
import javafx.animation.ParallelTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private static final double FIELD_WIDTH = 600;
private static final double FIELD_HEIGHT = 600;
private Pane field = new Pane();
ParallelTransition parallel = new ParallelTransition();
SequentialTransition sequentialRobot1 = new SequentialTransition();
SequentialTransition sequentialRobot2 = new SequentialTransition();
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
// Place one field boundary for testing.
Line northBoundary = new Line(0, 0, FIELD_WIDTH, 0);
northBoundary.setStrokeWidth(5.0);
field.getChildren().add(northBoundary);
// Place the robots on the field.
// The first robot.
Rectangle robotBody1 = new Rectangle(100, 300, 60, 60);
robotBody1.setArcHeight(15);
robotBody1.setArcWidth(15);
robotBody1.setStroke(Color.BLACK);
robotBody1.setFill(Color.CRIMSON);
field.getChildren().add(robotBody1);
robotBody1.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
if (northBoundary.getBoundsInParent().intersects(robotBody1.getBoundsInParent())) {
//** BROKEN!! IllegalStateException on next line
sequentialRobot1.stop();
System.out.println("Collision detected");
parallel.play();
}
});
TranslateTransition translateTransition1 = new TranslateTransition();
translateTransition1.setNode(robotBody1);
translateTransition1.setByX(0);
translateTransition1.setByY(-300);
translateTransition1.setDuration(Duration.seconds(1));
translateTransition1.setOnFinished(event -> {
robotBody1.setLayoutX(robotBody1.getLayoutX() + robotBody1.getTranslateX());
robotBody1.setLayoutY(robotBody1.getLayoutY() + robotBody1.getTranslateY());
robotBody1.setTranslateX(0);
robotBody1.setTranslateY(0);
});
sequentialRobot1.getChildren().add(translateTransition1);
// The second robot.
Rectangle robotBody2 = new Rectangle(300, 300, 60, 60);
robotBody2.setArcHeight(15);
robotBody2.setArcWidth(15);
robotBody2.setStroke(Color.BLACK);
robotBody2.setFill(Color.CYAN);
field.getChildren().add(robotBody2);
TranslateTransition translateTransition2 = new TranslateTransition();
translateTransition2.setNode(robotBody2);
translateTransition2.setByX(0);
translateTransition2.setByY(-100);
translateTransition2.setDuration(Duration.seconds(1));
translateTransition2.setOnFinished(event -> {
robotBody2.setLayoutX(robotBody2.getLayoutX() + robotBody2.getTranslateX());
robotBody2.setLayoutY(robotBody2.getLayoutY() + robotBody2.getTranslateY());
robotBody2.setTranslateX(0);
robotBody2.setTranslateY(0);
});
sequentialRobot2.getChildren().add(translateTransition2);
parallel.getChildren().addAll(sequentialRobot1, sequentialRobot2);
parallel.play();
primaryStage.setTitle("Field");
primaryStage.setScene(new Scene(field, FIELD_WIDTH, FIELD_HEIGHT, Color.GRAY));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
#Slaw's idea of using an AnimationTimer is probably the best direction to go, but the obvious answer is to not use the ParallelTransition at all. Since the robot animations are going to be independent, just use the SequentialTransitions and start them all at the same time by calling play() on each.
I have a program that at some point (may) displays two warnings - one about errors - those are in red, and one about warnings - those are in orange.
I wonder however if there is a way - using css - to have just one warning with some text red and some text orange.
Here is an example of what I want to achieve (the two can be separated into "sections"):
RED ERROR1
RED ERROR2
RED ERROR3
ORANGE WARNING1
ORANGE WARNING2
I've seen some answers pointing to RichTextFX like this one, however I don't see (or don't know) how that could apply to generic Alerts. Is that even possible, without writing some custom ExpandedAlert class?
The Alert class inherits from Dialog, which provides a pretty rich API and allows arbitrarily complex scene graphs to be set via the content property.
If you just want static text with different colors, the simplest approach is probably to add labels to a VBox; though you could also use more complex structures such as TextFlow or the third-party RichTextFX mentioned in the question if you need.
A simple example is:
import java.util.Random;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class App extends Application {
private final Random rng = new Random();
private void showErrorAlert(Stage stage) {
Alert alert = new Alert(Alert.AlertType.ERROR);
int numErrors = 2 + rng.nextInt(3);
int numWarnings = 2 + rng.nextInt(3);
VBox errorList = new VBox();
for (int i = 1 ; i <= numErrors ; i++) {
Label label = new Label("Error "+i);
label.setStyle("-fx-text-fill: red; ");
errorList.getChildren().add(label);
}
for (int i = 1 ; i <= numWarnings ; i++) {
Label label = new Label("Warning "+i);
label.setStyle("-fx-text-fill: orange; ");
errorList.getChildren().add(label);
}
alert.getDialogPane().setContent(errorList);
alert.initOwner(stage);
alert.show();
}
#Override
public void start(Stage stage) {
Button showErrors = new Button("Show Errors");
showErrors.setOnAction(e -> showErrorAlert(stage));
BorderPane root = new BorderPane(showErrors);
Scene scene = new Scene(root, 400, 400);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
which gives this result:
once again, I scratch my head while working with JavaFX-Charts... :-(
I have a usecase for which I need to replace all series in the chart, I create everything from scratch. When adding after clearing the previous data, the chart assigns the wrong style-classes for default-color to some of the added series...
More precisely, the assigned class-numbers get shifted... why on earth did they implement that with modulo, which by default limits to only 8 (!) different color-symbol-combinations...?!
The first 8 (hint: mod 8...) default-colors are correct:
default-color0
...
default-color7
and for the first run, it continues, also correct, again with 0:
default-color0
...
default-color6 (for 15 series in my example)
for the second run, I get:
default-color0
...
default-color7
default-color7 <-- shift from here
default-color0
...
default-color5
as you can see, the it is shifted, having two times 7 and then only running until 5.
For another run, the shift continues:
default-color0
...
default-color7
default-color6 <-- shift from here
default-color7
default-color0
...
default-color4
the amount of shifting is dependent on the number of series - for 16, everything is fine, for 14, it shifts 2 each time and so on.
In the chart this is nicely visible if you watch the legend changing...
I tried different methods of replacing the data, but none of them was successful, see comments in the code below.
An actual solution, but in my eyes not a very pretty one, is to manually overwrite the style-class, which isn't that easy as well, because of the aggressive habits of the task-class... wtf?!
whatever - if anyone has any helpful idea on that? maybe I'm missing something?
Note: To see the weired results, remove the lines marked as DIRTY-WORKAROUND-SOLUTION. Otherwise it would work... ;-)
Bonus-Question: maybe I'm silly, working with that JavaFX-Charts... some things are cool, but often times, I have a weired feeling about it... are there better alternatives you would recommend me to checo out?
Thanks!
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
public class ChartSeriesReplacing extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
VBox root = new VBox();
ScatterChart<Number, Number> chart = new ScatterChart<>(new NumberAxis(), new NumberAxis());
Button refresh = new Button("Refresh");
refresh.setOnAction(clicked -> {
List<XYChart.Series<Number, Number>> seriesList = new ArrayList<>();
for (int i = 0; i < 15; i++) {
XYChart.Series<Number, Number> series = new XYChart.Series<>();
series.setName(i + "");
XYChart.Data<Number, Number> data = new XYChart.Data<>(Math.random(), Math.random());
int finalI = i;
data.nodeProperty().addListener(__ -> {
if (data.getNode() != null) {
data.getNode().getStyleClass().addListener((InvalidationListener) ___ -> {
System.out.println("[pre] Data #" + finalI + ": Style = " + data.getNode().getStyleClass());
// DIRTY-WORKAROUND-SOLUTION:
// (has to live in listener, otherwise the chart erases any custom styling on adding... :-/ )
String colorString = "default-color" + finalI % 8;
data.getNode().getStyleClass().removeIf(s -> s.startsWith("default-color") && !s.equals(colorString));
if (!data.getNode().getStyleClass().contains(colorString)) {
data.getNode().getStyleClass().add(colorString);
}
// --------------------------
System.out.println("[post] Data #" + finalI + ": Style = " + data.getNode().getStyleClass());
});
}
});
series.getData().add(data);
seriesList.add(series);
}
System.out.println("-----------------");
// What I tried:
// 1)
chart.getData().setAll(seriesList);
// 2)
// chart.dataProperty().setValue(FXCollections.observableArrayList(seriesList));
// 3)
// chart.getData().clear();
// 3a)
// chart.getData().addAll(seriesList);
// 3b)
// seriesList.forEach(series->chart.getData().add(series));
});
root.getChildren().add(chart);
root.getChildren().add(refresh);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
Java-Version jdk1.8.0_171 (x64)
I am playing around with the 3d model importers from interactivemesh.org in Javafx. The import of the models in a scene works without error. However, the models are being displayed in a weird way. Some of the faces that are behind other faces are being displayed even though they should be covered by the front faces. I have tried the tdsImporter, as well as obj and the fxml importer, all encountered the same issue. The models are shown correctly in the model browser, so I guess something is wrong with my code. Here is what the model looks like (tried it on different computers):
The HST Model from interactivemesh.org
Also the source code I use for the 3ds import:
import com.interactivemesh.jfx.importer.tds.TdsModelImporter;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class Test3d extends Application {
Group group = new Group();
#Override
public void start(Stage meineStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("test.fxml"));
Scene meineScene = new Scene(root, 1280, 800);
meineStage.setTitle("Startbildschirm");
meineStage.setScene(meineScene);
meineStage.show();
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.getTransforms().addAll(
new Rotate(0, Rotate.Y_AXIS),
new Rotate(-45, Rotate.X_AXIS),
new Rotate(-45, Rotate.Z_AXIS),
new Translate(0, 0, -110));
meineScene.setCamera(camera);
camera.setNearClip(0.1);
camera.setFarClip(200);
TdsModelImporter tdsImporter = new TdsModelImporter();
tdsImporter.read("hst.3ds");
Node[] tdsMesh = (Node[]) tdsImporter.getImport();
tdsImporter.close();
for (int i = 0; i < tdsMesh.length; i++) {
tdsMesh[i].setScaleX(0.1);
tdsMesh[i].setScaleY(0.1);
tdsMesh[i].setScaleZ(0.1);
tdsMesh[i].getTransforms().setAll(new Rotate(60, Rotate.Y_AXIS), new Rotate(-90, Rotate.X_AXIS));
}
Group root1 = new Group(tdsMesh);
meineScene.setRoot(root1);
}
public static void main(String[] args) {
launch(args);
}
}
Does anybody have an idea what the problem could be and how to fix it?
According to the Scene javadoc:
An application may request depth buffer support or scene anti-aliasing support at the creation of a Scene. [...] A scene containing 3D shapes or 2D shapes with 3D transforms may use depth buffer support for proper depth sorted rendering; [...] A scene with 3D shapes may enable scene anti-aliasing to improve its rendering quality.
The depthBuffer and antiAliasing flags are conditional features. With the respective default values of: false and SceneAntialiasing.DISABLED.
So in your code, try:
Scene meineScene = new Scene(root, 1280, 800, true);
or even better:
Scene meineScene = new Scene(root, 1280, 800, true, SceneAntialiasing.BALANCED);