/* Reversound is used to get the music sheet of a piece from a music file. Copyright (C) 2014 Gabriel AUGENDRE Copyright (C) 2014 Gabriel DIENY Copyright (C) 2014 Arthur GAUCHER Copyright (C) 2014 Gabriel LEPETIT-AIMON This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package gui.graphs; import generictools.Instant; import gui.PlayerControl; import gui.PlayerControlEvent; import javafx.application.Platform; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Parent; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.CheckBox; import javafx.scene.control.ScrollBar; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import processing.buffer.TemporalBuffer; import java.util.ArrayList; /** * Affiche un graphe en amplitudes du signal audio d'origine * Created by arthur on 08/04/14. */ public class AmpliView extends AbstractBufferGraph>{ private BorderPane pane; private int frame; private LineChart chart; private ScrollBar scrollBar; //max et min de l'axe y private float maxY = 70000; private float minY = -70000; final private ReadOnlyObjectWrapper autoMove; private CheckBox checkBoxCursorMove; private StackPane chartContainer; private int zoomLevel; private ArrayList zoomArrayList; private Rectangle cursorRect; /** * Le constructeur */ public AmpliView() { super("Amplimètre", Float.class); this.frame=0; autoMove = new ReadOnlyObjectWrapper(); zoomLevel = 18; zoomArrayList = new ArrayList(); } /** * Crée tous les élements pour l'affichage du graphe * @return BorderPane */ Parent createContent() { //conteneur du graphe chartContainer = new StackPane(); //définition des axes final NumberAxis xAxis = new NumberAxis(); final NumberAxis yAxis = new NumberAxis(); xAxis.setLabel("Echantillons"); xAxis.setLowerBound(0); // 262144 = 2^18 pour le niveau de zoom 18 xAxis.setUpperBound(262144); xAxis.setTickUnit(262144/8); xAxis.setAutoRanging(false); yAxis.setAutoRanging(false); yAxis.setUpperBound(maxY); yAxis.setLowerBound(minY); yAxis.setTickUnit((maxY - minY)/10); //création du graphe chart = new LineChart<>(xAxis,yAxis); chart.setAnimated(false); chart.setCreateSymbols(false); chart.setLegendVisible(false); //definition du style chart.getStylesheets().add(AmpliView.class.getResource("AmpliView.css").toExternalForm()); chart.getStyleClass().add("thick-chart"); //ajoute le graphe dans le conteneur chartContainer.getChildren().add(chart); //creation du cureseur cursorRect = new Rectangle(); cursorRect.setManaged(false); cursorRect.setFill(Color.rgb(39, 93, 153)); cursorRect.setWidth(1); //déplace le curseur à 0 (origine) moveCursor(true); chartContainer.getChildren().add(cursorRect); //quand la hauteur de la fenêtre change, redimensionner le curseur chartContainer.heightProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observableValue, Number oldHeight, Number newHeight) { resizeCursor(); } }); //quand la largeur de la fenêtre change, redimensionner les contrôles chartContainer.widthProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observableValue, Number oldWidth, Number newWidth) { resizeControls(); moveCursor((Boolean) autoMove.getValue()); } }); //créer la checkbox pour suivre ou non le curseur checkBoxCursorMove = new CheckBox("Suivre curseur"); checkBoxCursorMove.setSelected(true); autoMove.bind(checkBoxCursorMove.selectedProperty()); //création de la scrollbar pour se déplacer dans le graphe scrollBar = new ScrollBar(); scrollBar.setMin(0); scrollBar.setMax(buffer().size()); handleScrollBar(); //quand la scrollbar est déplacée, se déplacer dans le graphe scrollBar.valueProperty().addListener((changeListener, oldVal, newVal) -> { moveChart(newVal); }); //création du curseur pour le zoom (en fait, agit plutôt comme un point) final Rectangle zoomRect = new Rectangle(); //crréation d'une serie vide pour le graphe au début chart.getData().add(new XYChart.Series()); //ctrl+scroll, zoome ou dézoome chart.setOnScroll(new EventHandler() { @Override public void handle(ScrollEvent scrollEvent) { if (scrollEvent.isControlDown()) { setUpZoom(zoomRect, scrollEvent); //zoome if (scrollEvent.getDeltaY() > 0) { if (changeZoomLevel(true)) zoomIn(zoomRect); //dézoome } else if (scrollEvent.getDeltaY() < 0) { if (changeZoomLevel(false)) zoomOut(zoomRect); } handleScrollBar(); } } }); //zoome sur l'axe y avec ctrl+scroll haut/bas chart.setOnMouseClicked(new EventHandler() { @Override public void handle(MouseEvent mouseEvent) { if(mouseEvent.isControlDown()) { if (mouseEvent.getButton() == MouseButton.PRIMARY) { zoomYAxis(true); } else { zoomYAxis(false); } } } }); //définit la frame quand on clique et drag le curseur chart.setOnMousePressed(new EventHandler() { @Override public void handle(MouseEvent event) { if (event.isPrimaryButtonDown() && !event.isControlDown()) { setFrame(event); } } }); chart.setOnMouseDragged(new EventHandler() { @Override public void handle(MouseEvent event) { if (event.isPrimaryButtonDown() && !event.isControlDown()) { setFrame(event); } } }); //création du panel contenant la checkbox et la scrollbar final HBox controls = new HBox(); controls.setPadding(new Insets(2)); controls.getChildren().addAll(checkBoxCursorMove, scrollBar); //création du panel global pane = new BorderPane(); pane.setCenter(chartContainer); pane.setBottom(controls); calculateZoomLevels(); return pane; } /** * Définit une nouvelle frame * @param event L'événement de la souris */ private void setFrame(MouseEvent event){ final NumberAxis xAxis = (NumberAxis)chart.getXAxis(); final NumberAxis yAxis = (NumberAxis) chart.getYAxis(); final double yAxisInScene = yAxis.localToScene(0, 0).getX() + yAxis.getWidth(); final double mouseX = event.getX(); final int timeToSet = xAxis.getValueForDisplay(mouseX - yAxisInScene).intValue(); final int boundMax = xAxis.getUpperBound()>buffer().size() ? buffer().size() : (int)xAxis.getUpperBound(); if(timeToSet >= xAxis.getLowerBound() && timeToSet <= boundMax) PlayerControl.instance().setFrame(Instant.fromIndex(timeToSet, buffer())); } /** * Redimensionne la scrollbar et la checkbox */ private void resizeControls(){ Platform.runLater(() -> { scrollBar.setPrefWidth(chartContainer.getWidth() - checkBoxCursorMove.getWidth() - 5); }); } /** * Redimensionne le curseur */ private void resizeCursor(){ Platform.runLater(() -> { final NumberAxis yAxis = (NumberAxis) chart.getYAxis(); cursorRect.setY(yAxis.localToScene(0, 0).getY()); cursorRect.setHeight(yAxis.getHeight()); }); } /** * Redimensionne et repositionne la scrollbar en fonction du niveau de zoom */ private void handleScrollBar(){ Platform.runLater(new Runnable() { @Override public void run() { final NumberAxis xAxis = (NumberAxis)chart.getXAxis(); final double interval = xAxis.getUpperBound()-xAxis.getLowerBound(); scrollBar.setMax(buffer().size()); //redimensionne la barre scrollBar.setVisibleAmount(interval); //repositionne la barre scrollBar.setValue(xAxis.getLowerBound()); } }); } /** * Déplacer la vue du graphe * @param frame The frame to display */ private void moveChart(final Number frame){ final NumberAxis xAxis = (NumberAxis)chart.getXAxis(); final int interval = (int)(xAxis.getUpperBound() - xAxis.getLowerBound()); final int newLowerX = frame.intValue(); final int newUpperX = frame.intValue() + interval; xAxis.setLowerBound(newLowerX); xAxis.setUpperBound(newUpperX); setBufferWindow(); //repositionne le curseur à la bonne place moveCursor(false); } /** * Déplace le curseur en fonction de la frame * @param locked Booleen indicant si le curseur doit être suivi */ private void moveCursor(final Boolean locked){ final NumberAxis xAxis = (NumberAxis) chart.getXAxis(); final NumberAxis yAxis = (NumberAxis) chart.getYAxis(); final double yAxisInScene = yAxis.localToScene(0, 0).getX() + yAxis.getWidth(); if (locked) { final int interval = (int) (xAxis.getUpperBound() - xAxis.getLowerBound()); //si la frame est plus grande que la limite sup, translater la //fenêtre vers la droite if (frame > xAxis.getUpperBound()) { xAxis.setLowerBound(xAxis.getUpperBound()); xAxis.setUpperBound(xAxis.getLowerBound() + interval); setBufferWindow(); } //si la frame est plus petite que la limite inf, translater la //fenêtre vers la gauche if (frame < xAxis.getLowerBound()) { if (xAxis.getLowerBound() < interval) { xAxis.setLowerBound(0); xAxis.setUpperBound(interval); setBufferWindow(); } else { xAxis.setUpperBound(xAxis.getLowerBound()); xAxis.setLowerBound(xAxis.getUpperBound() - interval); setBufferWindow(); } } } Platform.runLater(new Runnable() { @Override //positionne le curseur public void run() { cursorRect.setX((frame - xAxis.getLowerBound()) * xAxis.getScale() + yAxisInScene); } }); } /** * Définit les coordonnées du rectangle en fonction de la position de la * souris * @param zoomRect Rectangle contenant les coordonnées de la souris pour * zoomer * @param scrollEvent Le scroll event */ private void setUpZoom(final Rectangle zoomRect, final ScrollEvent scrollEvent){ zoomRect.setX(scrollEvent.getX()); zoomRect.setY(scrollEvent.getY()); } /** * Zoome le graphe en prenant en compte la position de la souris * @param zoomRect Rectangle contenant les coordonnées de la souris pour * zoomer */ private void zoomIn(final Rectangle zoomRect) { final NumberAxis yAxis = (NumberAxis)chart.getYAxis(); final NumberAxis xAxis = (NumberAxis)chart.getXAxis(); final double yAxisInScene = yAxis.localToScene(0,0).getX() + yAxis.getWidth(); double localPoint = (zoomRect.getX()-yAxisInScene)/xAxis.getScale()+xAxis.getLowerBound(); /* zoom in only if the mouse is in the chart (prevent the zoom when mouse on axis or beyond the size of the buffer*/ if(localPoint >= xAxis.getLowerBound() && localPoint <= xAxis.getUpperBound()){ //&& localPoint <= buffer().size()){ //define new upper and lower bounds, dividing by 2 in function of mouse position if(localPoint > buffer().size()){ localPoint = buffer().size()/2; } double newLowerX = (localPoint + xAxis.getLowerBound()) / 2; double newUpperX = (localPoint + xAxis.getUpperBound()) / 2; xAxis.setLowerBound(newLowerX); xAxis.setUpperBound(newUpperX); //adapt scale if (xAxis.getUpperBound() - xAxis.getLowerBound() != xAxis.getTickUnit() * 8) xAxis.setTickUnit((xAxis.getUpperBound() - xAxis.getLowerBound())/8); setBufferWindow(); moveCursor(false); } } /** * Zoom out the chart taking into account the position of the mouse * @param zoomRect Rectangle containing mouse coordinates to zoom */ private void zoomOut(final Rectangle zoomRect) { final NumberAxis yAxis = (NumberAxis)chart.getYAxis(); final NumberAxis xAxis = (NumberAxis)chart.getXAxis(); final double yAxisInScene = yAxis.localToScene(0,0).getX() + yAxis.getWidth(); final double localPoint = (zoomRect.getX()-yAxisInScene)/xAxis.getScale()+xAxis.getLowerBound(); if(localPoint >= xAxis.getLowerBound() && localPoint <= xAxis.getUpperBound() ){ double newLowerX = 2*xAxis.getLowerBound() - localPoint; double newUpperX = 2*xAxis.getUpperBound() - localPoint; // 0 must always be at the origin of the chart if (newLowerX < 0) { newUpperX = newUpperX - newLowerX; newLowerX = 0; } xAxis.setLowerBound(newLowerX); xAxis.setUpperBound(newUpperX); if (xAxis.getUpperBound() - xAxis.getLowerBound() != xAxis.getTickUnit() * 8) xAxis.setTickUnit((xAxis.getUpperBound() - xAxis.getLowerBound())/8); setBufferWindow(); moveCursor(false); } } /** * Zoom in or out y axis * @param in Zooming in or out on y axis */ private void zoomYAxis(boolean in){ NumberAxis yAxis = (NumberAxis)chart.getYAxis(); //prevent from zooming in when y axis upper bound is 4375 if (in && yAxis.getUpperBound() != 4375) { yAxis.setUpperBound(yAxis.getUpperBound() / 2); yAxis.setLowerBound(yAxis.getLowerBound() / 2); //prevent from zooming out when y axis upper bound is 70000 } else if (!in && yAxis.getUpperBound() != 70000){ yAxis.setUpperBound(yAxis.getUpperBound() * 2); yAxis.setLowerBound(yAxis.getLowerBound() * 2); } } /** * Calculate a list of zoom levels which are power of 2 */ private void calculateZoomLevels(){ //30 min max final int MAX_SAMPLE_TIME = 44100*60*30; zoomArrayList = new ArrayList<>(); int i=1; double reste = MAX_SAMPLE_TIME % Math.pow(2, i); zoomArrayList.add(0); while(reste != MAX_SAMPLE_TIME){ zoomArrayList.add(i); reste = (MAX_SAMPLE_TIME) % Math.pow(2,i); i++; } } /** * Increase or decrease the level of zoom depending if you are zooming in * or out * @param in Zooming in or out * @return */ private boolean changeZoomLevel(boolean in){ boolean success = false; //can't go below zoom level 7 because chart is too extended that way, //not relevant if(in && zoomLevel != 7){ zoomLevel--; success = true; //can't go further max zoom level }else if(!in && zoomLevel != zoomArrayList.size()-1) { zoomLevel++; success = true; } return success; } /** * Call setWindow() of BufferViewState */ private void setBufferWindow(){ final NumberAxis xAxis = (NumberAxis)chart.getXAxis(); int upper = (int)xAxis.getUpperBound(); state().setWindow((int)xAxis.getLowerBound(),upper); } @Override public void playerControlEvent(PlayerControlEvent e) { switch (e.getType()){ case FRAME: this.frame = e.getFrame().mapToIndex(buffer()); moveCursor((Boolean)autoMove.getValue()); break; case PLAY_STATE: switch(e.getPlayingState()) { case STOPPED: //when player is stopped, move the window to the beginning //only if automove is checked if ((Boolean) autoMove.getValue()) moveChart(0); break; } } } @Override public void updateView() { XYChart.Series s = new XYChart.Series(); final int RESOLUTION = 1368; final NumberAxis xAxis = (NumberAxis) chart.getXAxis(); final int min = (int) xAxis.getLowerBound(); int max = (int) xAxis.getUpperBound() > buffer().size() ? buffer().size() : (int) xAxis.getUpperBound(); /* If zoom level is below 14 meaning that the zoom is big (small portion of the signal drawn) we can directly draw amplitudes of the buffer at a specific step depending on the portion of the signal drawn. But if we are too zoomed out, the step method does not work anymore because we are "randomly" drawing amplitudes of the buffer which are thus not meaningful of the real signal and resulting in really odd drawings. To overcome this problem, we can divide the buffer in small "chunks" and draw the max amplitude of every chunk, which is more meaningful. The chunk length will depend on the zoom level. */ if(zoomLevel <= 14) { final int inc = (max - min) > RESOLUTION ? (max - min) / RESOLUTION : 1; //only the points that are visible for (int i = min; i < max; i += inc) { s.getData().add(new XYChart.Data(i, buffer().get(i))); if (buffer().get(i) > maxY) maxY = buffer().get(i); if (buffer().get(i) < minY) minY = buffer().get(i); } } else { float maxAmp = 0; //calculate the length of a single "chunk" int samplesPerPixel = calculateSamplesPerChunk(); for(int i = min; i < max - samplesPerPixel; i+=samplesPerPixel) { //inside each chunk, search for the max amplitude for (int j = i; j < i + samplesPerPixel; j++) { if (buffer().get(j) > maxAmp) { maxAmp = buffer().get(j); } } //draw a vertical line between maxAmp and -maxAmp s.getData().add(new XYChart.Data(i, 0)); s.getData().add(new XYChart.Data(i, -maxAmp)); s.getData().add(new XYChart.Data(i, maxAmp)); s.getData().add(new XYChart.Data(i, 0)); maxY = maxAmp; maxAmp = 0; } } handleScrollBar(); Platform.runLater(new Runnable() { @Override public void run() { //set the series in the chart chart.getData().set(0, s); } }); } /** * Calculate the size of a chunk to draw amplitudes * @return Number of samples represented by a chunk */ private int calculateSamplesPerChunk(){ return (int)Math.round(Math.pow(2,zoomLevel) / 1368)*2; } @Override public void cleanView() { Platform.runLater(new Runnable() { @Override public void run() { chart.getData().clear(); chart.getData().add(new XYChart.Series()); NumberAxis xAxis = (NumberAxis)chart.getXAxis(); xAxis.setLowerBound(0); xAxis.setUpperBound(262144); xAxis.setTickUnit(262144/8); zoomLevel = 18; handleScrollBar(); } }); } @Override protected AbstractBufferGraph getGenericGraph() { return new AmpliView(); } @Override public Parent getMainNode() { return pane; } public void initView() { createContent(); state().setWindow(0, 262144); } }