reversound/src/gui/graphs/AmpliView.java
2014-06-16 16:48:34 +02:00

625 lines
18 KiB
Java

/*
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 <http://www.gnu.org/licenses/>.
*/
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<TemporalBuffer<Float>>{
private BorderPane pane;
private int frame;
private LineChart<Number,Number> 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<Integer> zoomArrayList;
private Rectangle cursorRect;
/**
* Le constructeur
*/
public AmpliView() {
super("Amplimètre", Float.class);
this.frame=0;
autoMove = new ReadOnlyObjectWrapper();
zoomLevel = 18;
zoomArrayList = new ArrayList<Integer>();
}
/**
* 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<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number oldHeight, Number newHeight) {
resizeCursor();
}
});
//quand la largeur de la fenêtre change, redimensionner les contrôles
chartContainer.widthProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> 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<ScrollEvent>() {
@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<MouseEvent>() {
@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<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
if (event.isPrimaryButtonDown() && !event.isControlDown()) {
setFrame(event);
}
}
});
chart.setOnMouseDragged(new EventHandler<MouseEvent>() {
@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); }
}