High refreshing rate in JavaFX

2019-03-06 16:50发布

I'm trying to write a program with an equalizer, a frequency analyzer and a sound level meter. The model part seems to work very well but I'm experimenting some bugs with the IHM.

My last bug is with the level meter. After a while (from few milliseconds to few seconds), it freezes and don't update anymore. So, here is a (simplified) version of it. I added the runnable part to test and reproduce the bug. Of course, this bug appears sooner when I add other graphical components which also need to refresh very frequently. For example, the frequency analyze is represented by a line-chart with something like 1000 points.

public class LevelMeter2 extends Parent implements Runnable {

    private IntegerProperty levelMeterHeight = new SimpleIntegerProperty();

    private Rectangle led;

    private IntegerProperty height = new SimpleIntegerProperty();
    private IntegerProperty width = new SimpleIntegerProperty();
    private DoubleProperty linearValue = new SimpleDoubleProperty();
    private Color backgroundColor=Color.BLACK;
    private double minLinearValue, maxLinearValue;

    public LevelMeter2 (int height2, int width2) {
        this.height.set(height2);
        this.levelMeterHeight.bind(height.multiply(0.9));
        this.width.set(width2);

        linearValue.set(1.0);
        minLinearValue = Math.pow(10, -60.0/100);
        maxLinearValue = Math.pow(10, 3.0/100)-minLinearValue;

        Rectangle levelMeterShape = new Rectangle();
        levelMeterShape.widthProperty().bind(width);
        levelMeterShape.heightProperty().bind(height);
        levelMeterShape.setStroke(backgroundColor);

        this.getChildren().add(levelMeterShape);

        led = new Rectangle();
        led.widthProperty().bind(width.multiply(0.8));
        led.translateXProperty().bind(width.multiply(0.1));
        led.heightProperty().bind(levelMeterHeight.multiply(linearValue));
        led.setFill(Color.AQUA);
        Rotate rotate = new Rotate();
        rotate.pivotXProperty().bind(width.multiply(0.8).divide(2));
        rotate.pivotYProperty().bind(height.divide(2));
        rotate.setAngle(180);
        led.getTransforms().add(rotate);
        this.getChildren().add(led);
        }

    public double convertdBToLinearValue (double dB) {
        return ((double)Math.round(100 * ((Math.pow(10, dB/100)-minLinearValue)/maxLinearValue)) ) /100   ;
        //return (Math.pow(10, dB/100)-minLinearValue)/maxLinearValue;
    }

    public double convertLinearValueTodB (double linearValue) {
        return 100*Math.log10(linearValue*maxLinearValue+minLinearValue);
    }

    public void setValue (double dB) {
        if (dB>3) {
            dB=3;
        }   
        linearValue.setValue(convertdBToLinearValue(dB));
    }

    @Override
    public void run() {
        int i = 0;
        double value=-20;
        while (i<1000) {
            setValue(value);
            value = (Math.random()-0.5)*10+value;
            if (value>3) {
                value=3;
            }
            if (value<-60) {
                value=-60;
            }
            i++;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("END OF WHILE");
    }
}

And a "Main" to test it :

public class MainGraph extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        HBox pane = new HBox();
        LevelMeter2 levelMeter = new LevelMeter2(300,30);
        Thread t = new Thread(levelMeter);
        pane.getChildren().add(levelMeter);

        t.start();
        Scene scene = new Scene(pane, 300, 300);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Test IHM");
        primaryStage.setOnCloseRequest( event -> {
            System.out.println("FIN");
            System.exit(0);     
        });
        primaryStage.show();
    }    
}

What's wrong with my code ? How can I write a more robust code that will allow me high refresh rates of my IHM ? Or how can I prevent from freezing ?

Thank you for you help.

2条回答
相关推荐>>
2楼-- · 2019-03-06 16:59

I would suggest you move away from Threads and use something from JavaFX Animation package. In this example Timeline is used. This code is set to run at a rate of about 60 fps. You can adjust that using Duration.millis().

>Main

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

/**
 *
 * @author blj0011
 */
public class JavaFXApplication342 extends Application
{

    @Override
    public void start(Stage primaryStage)
    {

        LevelMeter2 levelMeter = new LevelMeter2(300, 30);

        Button button = new Button("Start");
        button.setOnAction((event) -> {
            switch (button.getText()) {
                case "Start":
                    levelMeter.startAnimation();
                    button.setText("Stop");
                    break;
                case "Stop":
                    levelMeter.stopAnimation();
                    button.setText("Start");
                    break;
            }
        });

        HBox pane = new HBox(levelMeter, button);
        Scene scene = new Scene(pane, 300, 300);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Test IHM");
        primaryStage.setOnCloseRequest(event -> {
            System.out.println("FIN");
            System.exit(0);
        });
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args)
    {
        launch(args);
    }

}

LevelMeter2

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Parent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;

public final class LevelMeter2 extends Parent
{

    private final IntegerProperty levelMeterHeight = new SimpleIntegerProperty();

    Timeline timeline;
    double value = -20;

    private final Rectangle led;

    private final IntegerProperty height = new SimpleIntegerProperty();
    private final IntegerProperty width = new SimpleIntegerProperty();
    private final DoubleProperty linearValue = new SimpleDoubleProperty();
    private final Color backgroundColor = Color.BLACK;
    private final double minLinearValue;
    private final double maxLinearValue;

    public LevelMeter2(int height2, int width2)
    {
        this.height.set(height2);
        this.levelMeterHeight.bind(height.multiply(0.9));
        this.width.set(width2);

        linearValue.set(1.0);
        minLinearValue = Math.pow(10, -60.0 / 100);
        maxLinearValue = Math.pow(10, 3.0 / 100) - minLinearValue;

        Rectangle levelMeterShape = new Rectangle();
        levelMeterShape.widthProperty().bind(width);
        levelMeterShape.heightProperty().bind(height);
        levelMeterShape.setStroke(backgroundColor);

        this.getChildren().add(levelMeterShape);

        led = new Rectangle();
        led.widthProperty().bind(width.multiply(0.8));
        led.translateXProperty().bind(width.multiply(0.1));
        led.heightProperty().bind(levelMeterHeight.multiply(linearValue));
        led.setFill(Color.AQUA);
        Rotate rotate = new Rotate();
        rotate.pivotXProperty().bind(width.multiply(0.8).divide(2));
        rotate.pivotYProperty().bind(height.divide(2));
        rotate.setAngle(180);
        led.getTransforms().add(rotate);
        getChildren().add(led);

        timeline = new Timeline(new KeyFrame(Duration.millis(16), (event) -> {
            setValue(value);

            value = (Math.random() - 0.5) * 10 + value;
            if (value > 3) {
                value = 3;
            }
            if (value < -60) {
                value = -60;
            }

        }));
        timeline.setCycleCount(Timeline.INDEFINITE);
    }

    public double convertdBToLinearValue(double dB)
    {
        return ((double) Math.round(100 * ((Math.pow(10, dB / 100) - minLinearValue) / maxLinearValue))) / 100;
    }

    public double convertLinearValueTodB(double linearValue)
    {
        return 100 * Math.log10(linearValue * maxLinearValue + minLinearValue);
    }

    public void setValue(double dB)
    {
        if (dB > 3) {
            dB = 3;
        }
        linearValue.setValue(convertdBToLinearValue(dB));
    }

    public void startAnimation()
    {
        timeline.play();
    }

    public void stopAnimation()
    {
        timeline.stop();
    }
}

Multiple LevelMeters Example:

Main

import java.util.ArrayList;
import java.util.List;
import javafx.animation.ParallelTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 *
 * @author blj0011
 */
public class JavaFXApplication342 extends Application
{

    @Override
    public void start(Stage primaryStage)
    {
        List<LevelMeter2> levelMeter2s = new ArrayList();
        List<Timeline> metersTimelines = new ArrayList();
        for (int i = 0; i < 9; i++) {
            LevelMeter2 levelMeter2 = new LevelMeter2(300, 30);
            levelMeter2s.add(levelMeter2);
            metersTimelines.add(levelMeter2.getTimeline());
        }

        ParallelTransition parallelTransition = new ParallelTransition();
        parallelTransition.getChildren().addAll(metersTimelines);

        Button button = new Button("Start");
        button.setOnAction((event) -> {
            switch (button.getText()) {
                case "Start":
                    parallelTransition.play();
                    button.setText("Stop");
                    break;
                case "Stop":
                    parallelTransition.stop();
                    button.setText("Start");
                    break;
            }
        });

        HBox hBox = new HBox();
        hBox.getChildren().addAll(levelMeter2s);

        VBox vBox = new VBox(hBox, new StackPane(button));
        Scene scene = new Scene(vBox, 300, 350);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Test IHM");
        primaryStage.setOnCloseRequest(event -> {
            System.out.println("FIN");
            System.exit(0);
        });
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args)
    {
        launch(args);
    }

}

LevelMeter2

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Parent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;

public final class LevelMeter2 extends Parent
{

    private final IntegerProperty levelMeterHeight = new SimpleIntegerProperty();

    Timeline timeline;
    double value = -20;

    private final Rectangle led;

    private final IntegerProperty height = new SimpleIntegerProperty();
    private final IntegerProperty width = new SimpleIntegerProperty();
    private final DoubleProperty linearValue = new SimpleDoubleProperty();
    private final Color backgroundColor = Color.BLACK;
    private final double minLinearValue;
    private final double maxLinearValue;

    public LevelMeter2(int height2, int width2)
    {
        this.height.set(height2);
        this.levelMeterHeight.bind(height.multiply(0.9));
        this.width.set(width2);

        linearValue.set(1.0);
        minLinearValue = Math.pow(10, -60.0 / 100);
        maxLinearValue = Math.pow(10, 3.0 / 100) - minLinearValue;

        Rectangle levelMeterShape = new Rectangle();
        levelMeterShape.widthProperty().bind(width);
        levelMeterShape.heightProperty().bind(height);
        levelMeterShape.setStroke(backgroundColor);

        this.getChildren().add(levelMeterShape);

        led = new Rectangle();
        led.widthProperty().bind(width.multiply(0.8));
        led.translateXProperty().bind(width.multiply(0.1));
        led.heightProperty().bind(levelMeterHeight.multiply(linearValue));
        led.setFill(Color.AQUA);
        Rotate rotate = new Rotate();
        rotate.pivotXProperty().bind(width.multiply(0.8).divide(2));
        rotate.pivotYProperty().bind(height.divide(2));
        rotate.setAngle(180);
        led.getTransforms().add(rotate);
        getChildren().add(led);

        timeline = new Timeline(new KeyFrame(Duration.millis(25), (event) -> {
            setValue(value);

            value = (Math.random() - 0.5) * 10 + value;
            if (value > 3) {
                value = 3;
            }
            if (value < -60) {
                value = -60;
            }

        }));
        timeline.setCycleCount(Timeline.INDEFINITE);
    }

    public double convertdBToLinearValue(double dB)
    {
        return ((double) Math.round(100 * ((Math.pow(10, dB / 100) - minLinearValue) / maxLinearValue))) / 100;
    }

    public double convertLinearValueTodB(double linearValue)
    {
        return 100 * Math.log10(linearValue * maxLinearValue + minLinearValue);
    }

    public void setValue(double dB)
    {
        if (dB > 3) {
            dB = 3;
        }
        linearValue.setValue(convertdBToLinearValue(dB));
    }

    public void startAnimation()
    {
        timeline.play();
    }

    public void stopAnimation()
    {
        timeline.stop();
    }

    public Timeline getTimeline()
    {
        return timeline;
    }
}
查看更多
家丑人穷心不美
3楼-- · 2019-03-06 17:15

Your implementation of run() appears to be updating the scene graph from a background thread. As discussed in Concurrency in JavaFX:

The JavaFX scene graph…is not thread-safe and can only be accessed and modified from the UI thread also known as the JavaFX Application thread. Implementing long-running tasks on the JavaFX Application thread inevitably makes an application UI unresponsive."

Instead, use a Task, illustrated here and here. Your implementation of call() can collect data asynchronously and notify the GUI of the current state via updateValue(). Your valueProperty() listener can then invoke setValue() safely. Because "Updates are coalesced to prevent saturation of the FX event queue," your application will perform satisfactorily even on older hardware.

Alternatively, if your audio source is one of the supported Media types, this AudioBarChartApp updates the data model of a BarChart in an AudioSpectrumListener registered with the corresponding MediaPlayer. The image below displays pink noise.

private XYChart.Data<String, Number>[] series1Data;
…
audioSpectrumListener = (double timestamp, double duration,
                         float[] magnitudes, float[] phases) -> {
    for (int i = 0; i < series1Data.length; i++) {
        series1Data[i].setYValue(magnitudes[i] + 60);
    }
};

pink noise

查看更多
登录 后发表回答