From 6b4ec42c01257756b573b48a6579137be8cac532 Mon Sep 17 00:00:00 2001 From: gaugendre Date: Wed, 11 Jun 2014 15:35:06 +0200 Subject: [PATCH] adding code Switching from INSA svn to GitHub --- src/JNLP/Reversound.jnlp | 21 + src/META-INF/MANIFEST.MF | 3 + src/actions/ExpertModeAction.java | 30 + src/actions/ImportFileAction.java | 48 ++ src/actions/QuitAction.java | 23 + src/actions/ShowInternalFrameAction.java | 34 + src/conteneurs/CustomGamme.java | 69 ++ src/conteneurs/Gamme.java | 24 + src/conteneurs/Note.java | 104 +++ src/conteneurs/NoteEvent.java | 69 ++ src/conteneurs/NoteEventList.java | 49 ++ src/conteneurs/NoteGamme.java | 247 +++++++ src/conteneurs/NoteID.java | 51 ++ src/conteneurs/NoteList.java | 66 ++ src/conteneurs/NoteProfiled.java | 50 ++ src/conteneurs/ObservableList.java | 108 ++++ src/conteneurs/ObservableObject.java | 127 ++++ src/conteneurs/ObservableObjectEvent.java | 57 ++ src/conteneurs/Profil.java | 73 +++ src/conteneurs/RegularGamme.java | 74 +++ src/conteneurs/Spectre.java | 345 ++++++++++ src/generictools/Complex.java | 116 ++++ src/generictools/Instant.java | 245 +++++++ src/generictools/Pair.java | 56 ++ src/generictools/Strings.java | 24 + src/generictools/WindowFunction.java | 145 +++++ src/gui/Box.java | 143 +++++ src/gui/ClipView.java | 5 + src/gui/Connector.java | 24 + src/gui/InputBox.java | 29 + src/gui/MainWindow.java | 441 +++++++++++++ src/gui/PlayerControl.java | 220 +++++++ src/gui/PlayerControlEvent.java | 57 ++ src/gui/ProcessBox.java | 190 ++++++ src/gui/ProcessSumUp.java | 167 +++++ src/gui/ProgressBar.css | 1 + src/gui/ProgressIndicator.css | 1 + src/gui/TimeSlider.java | 162 +++++ src/gui/graphs/AbstractBufferGraph.java | 65 ++ src/gui/graphs/AbstractGraph.java | 98 +++ src/gui/graphs/AbstractObservableGraph.java | 136 ++++ src/gui/graphs/AmpliFreqView.java | 195 ++++++ src/gui/graphs/AmpliView.css | 7 + src/gui/graphs/AmpliView.java | 604 ++++++++++++++++++ src/gui/graphs/AudioBarChart.css | 1 + src/gui/graphs/Bar.java | 95 +++ src/gui/graphs/BarExtraValues.java | 59 ++ src/gui/graphs/BargramView.java | 491 ++++++++++++++ src/gui/graphs/EventViewer.java | 74 +++ src/gui/graphs/GraphicView.java | 351 ++++++++++ src/gui/graphs/NoteViewer.java | 32 + src/gui/graphs/Overlay.css | 13 + src/gui/graphs/OverlayGraph.java | 312 +++++++++ src/gui/graphs/ProfilView.java | 48 ++ src/gui/graphs/SpectreView.java | 94 +++ src/gui/graphs/Spectrogram.java | 91 +++ src/gui/graphs/SpectrogramView.java | 460 +++++++++++++ src/gui/graphs/TraceChart.java | 227 +++++++ src/gui/viewer_state/BufferViewerState.java | 152 +++++ .../viewer_state/ObservableViewerState.java | 57 ++ src/gui/viewer_state/Viewer.java | 10 + src/gui/viewer_state/ViewerState.java | 113 ++++ src/processing/AudioInputToBuffer.java | 404 ++++++++++++ src/processing/BufferPlayer.java | 201 ++++++ src/processing/PostProcessScript.java | 218 +++++++ src/processing/ProcessControl.java | 429 +++++++++++++ src/processing/SpectreFilter.java | 102 +++ src/processing/SpectreFilterFactory.java | 25 + src/processing/buffer/Buffer.java | 345 ++++++++++ src/processing/buffer/BufferEvent.java | 78 +++ .../buffer/BufferEventListener.java | 8 + src/processing/buffer/NoteBuffer.java | 107 ++++ src/processing/buffer/Storage.java | 77 +++ src/processing/buffer/StorageArrayList.java | 169 +++++ src/processing/buffer/StorageIterator.java | 20 + src/processing/buffer/TemporalBuffer.java | 64 ++ src/processing/processes/EventToNote.java | 141 ++++ src/processing/processes/FFT.java | 230 +++++++ .../processes/FreqEdgeDetector.java | 227 +++++++ src/processing/processes/FreqEdgeToEvent.java | 163 +++++ src/processing/processes/IndexedProcess.java | 135 ++++ src/processing/processes/Process.java | 494 ++++++++++++++ src/processing/processes/ProcessEvent.java | 48 ++ .../properties/BooleanProperty.java | 49 ++ src/processing/properties/ChoiceProperty.java | 131 ++++ src/processing/properties/FloatProperty.java | 126 ++++ src/processing/properties/IntProperty.java | 129 ++++ .../properties/PropertiesManager.java | 81 +++ src/processing/properties/Property.java | 168 +++++ src/processing/properties/StringProperty.java | 56 ++ 90 files changed, 11908 insertions(+) create mode 100644 src/JNLP/Reversound.jnlp create mode 100644 src/META-INF/MANIFEST.MF create mode 100644 src/actions/ExpertModeAction.java create mode 100644 src/actions/ImportFileAction.java create mode 100644 src/actions/QuitAction.java create mode 100644 src/actions/ShowInternalFrameAction.java create mode 100644 src/conteneurs/CustomGamme.java create mode 100644 src/conteneurs/Gamme.java create mode 100644 src/conteneurs/Note.java create mode 100644 src/conteneurs/NoteEvent.java create mode 100644 src/conteneurs/NoteEventList.java create mode 100644 src/conteneurs/NoteGamme.java create mode 100644 src/conteneurs/NoteID.java create mode 100644 src/conteneurs/NoteList.java create mode 100644 src/conteneurs/NoteProfiled.java create mode 100644 src/conteneurs/ObservableList.java create mode 100644 src/conteneurs/ObservableObject.java create mode 100644 src/conteneurs/ObservableObjectEvent.java create mode 100644 src/conteneurs/Profil.java create mode 100644 src/conteneurs/RegularGamme.java create mode 100644 src/conteneurs/Spectre.java create mode 100644 src/generictools/Complex.java create mode 100644 src/generictools/Instant.java create mode 100644 src/generictools/Pair.java create mode 100644 src/generictools/Strings.java create mode 100644 src/generictools/WindowFunction.java create mode 100644 src/gui/Box.java create mode 100644 src/gui/ClipView.java create mode 100644 src/gui/Connector.java create mode 100644 src/gui/InputBox.java create mode 100644 src/gui/MainWindow.java create mode 100644 src/gui/PlayerControl.java create mode 100644 src/gui/PlayerControlEvent.java create mode 100644 src/gui/ProcessBox.java create mode 100644 src/gui/ProcessSumUp.java create mode 100644 src/gui/ProgressBar.css create mode 100644 src/gui/ProgressIndicator.css create mode 100644 src/gui/TimeSlider.java create mode 100644 src/gui/graphs/AbstractBufferGraph.java create mode 100644 src/gui/graphs/AbstractGraph.java create mode 100644 src/gui/graphs/AbstractObservableGraph.java create mode 100644 src/gui/graphs/AmpliFreqView.java create mode 100644 src/gui/graphs/AmpliView.css create mode 100644 src/gui/graphs/AmpliView.java create mode 100644 src/gui/graphs/AudioBarChart.css create mode 100644 src/gui/graphs/Bar.java create mode 100644 src/gui/graphs/BarExtraValues.java create mode 100644 src/gui/graphs/BargramView.java create mode 100644 src/gui/graphs/EventViewer.java create mode 100644 src/gui/graphs/GraphicView.java create mode 100644 src/gui/graphs/NoteViewer.java create mode 100644 src/gui/graphs/Overlay.css create mode 100644 src/gui/graphs/OverlayGraph.java create mode 100644 src/gui/graphs/ProfilView.java create mode 100644 src/gui/graphs/SpectreView.java create mode 100644 src/gui/graphs/Spectrogram.java create mode 100644 src/gui/graphs/SpectrogramView.java create mode 100644 src/gui/graphs/TraceChart.java create mode 100644 src/gui/viewer_state/BufferViewerState.java create mode 100644 src/gui/viewer_state/ObservableViewerState.java create mode 100644 src/gui/viewer_state/Viewer.java create mode 100644 src/gui/viewer_state/ViewerState.java create mode 100644 src/processing/AudioInputToBuffer.java create mode 100644 src/processing/BufferPlayer.java create mode 100644 src/processing/PostProcessScript.java create mode 100644 src/processing/ProcessControl.java create mode 100644 src/processing/SpectreFilter.java create mode 100644 src/processing/SpectreFilterFactory.java create mode 100644 src/processing/buffer/Buffer.java create mode 100644 src/processing/buffer/BufferEvent.java create mode 100644 src/processing/buffer/BufferEventListener.java create mode 100644 src/processing/buffer/NoteBuffer.java create mode 100644 src/processing/buffer/Storage.java create mode 100644 src/processing/buffer/StorageArrayList.java create mode 100644 src/processing/buffer/StorageIterator.java create mode 100644 src/processing/buffer/TemporalBuffer.java create mode 100644 src/processing/processes/EventToNote.java create mode 100644 src/processing/processes/FFT.java create mode 100644 src/processing/processes/FreqEdgeDetector.java create mode 100644 src/processing/processes/FreqEdgeToEvent.java create mode 100644 src/processing/processes/IndexedProcess.java create mode 100644 src/processing/processes/Process.java create mode 100644 src/processing/processes/ProcessEvent.java create mode 100644 src/processing/properties/BooleanProperty.java create mode 100644 src/processing/properties/ChoiceProperty.java create mode 100644 src/processing/properties/FloatProperty.java create mode 100644 src/processing/properties/IntProperty.java create mode 100644 src/processing/properties/PropertiesManager.java create mode 100644 src/processing/properties/Property.java create mode 100644 src/processing/properties/StringProperty.java diff --git a/src/JNLP/Reversound.jnlp b/src/JNLP/Reversound.jnlp new file mode 100644 index 0000000..2d7a6d9 --- /dev/null +++ b/src/JNLP/Reversound.jnlp @@ -0,0 +1,21 @@ + + + + Reversound + Gabriel Augendre + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000..1331d14 --- /dev/null +++ b/src/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: gui.MainWindow +Permissions: all-permissions diff --git a/src/actions/ExpertModeAction.java b/src/actions/ExpertModeAction.java new file mode 100644 index 0000000..fbb5f9f --- /dev/null +++ b/src/actions/ExpertModeAction.java @@ -0,0 +1,30 @@ +package actions; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +/** + * Created by Gabriel on 28/03/2014. + */ + +public class ExpertModeAction extends AbstractAction { + public static final String ACTIVE = "ExpertModeActive"; + + /** + * Create the ExpertModeAction. + * @param default_value Is the expert mode enabled ? + */ + public ExpertModeAction(boolean default_value) { + super("Mode expert"); + putValue(ACTIVE,default_value); + } + + @Override + public void actionPerformed(ActionEvent e) { + putValue(ACTIVE, !((Boolean) getValue(ACTIVE))); + } + + /* public void setExpertMode(Boolean enabled) { + putValue(ACTIVE,enabled); + }*/ +} \ No newline at end of file diff --git a/src/actions/ImportFileAction.java b/src/actions/ImportFileAction.java new file mode 100644 index 0000000..73738f8 --- /dev/null +++ b/src/actions/ImportFileAction.java @@ -0,0 +1,48 @@ +package actions; + +import generictools.Strings; + +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.event.ActionEvent; +import java.io.File; + +/** + * Ask to user to open a File and start reading. + */ +public class ImportFileAction extends AbstractAction { + public static final String FILE = "Fichier"; + private final JComboBox sourceList; + private final JFileChooser chooser = new JFileChooser(); + private final JFrame frame; + + public ImportFileAction(JFrame frame, JComboBox sourceList){ + super("Importer un fichier..."); + this.frame = frame; + this.sourceList = sourceList; + } + + /** + * Invoked when the user clicks either on file import button or menu item. + * Loads the file in memory + * @param e An event. + */ + @Override + public void actionPerformed(ActionEvent e) { + FileNameExtensionFilter filter = new FileNameExtensionFilter("Fichier Audio wav", "wav"); + chooser.setFileFilter(filter); + int returnVal = chooser.showOpenDialog(frame); + if(returnVal == JFileChooser.APPROVE_OPTION) { + if (sourceList.getItemCount() > 1) + sourceList.removeItemAt(1); + File audioFile = new File(chooser.getSelectedFile().getAbsolutePath()); + putValue(FILE, audioFile); + sourceList.addItem(audioFile.getName()); + sourceList.setPrototypeDisplayValue(Strings.longestString("Fichier", audioFile.getName())); + sourceList.setSelectedIndex(1); + } + } + + + +} diff --git a/src/actions/QuitAction.java b/src/actions/QuitAction.java new file mode 100644 index 0000000..f6bda5d --- /dev/null +++ b/src/actions/QuitAction.java @@ -0,0 +1,23 @@ +package actions; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.WindowEvent; + +/** + * Created by harry2410 on 18/04/2014. + */ +public class QuitAction extends AbstractAction { + + private final JFrame frame; + + public QuitAction(JFrame frame) { + super("Quitter"); + this.frame = frame; + } + + @Override + public void actionPerformed(ActionEvent e) { + frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING)); + } +} diff --git a/src/actions/ShowInternalFrameAction.java b/src/actions/ShowInternalFrameAction.java new file mode 100644 index 0000000..69b083b --- /dev/null +++ b/src/actions/ShowInternalFrameAction.java @@ -0,0 +1,34 @@ +package actions; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.beans.PropertyVetoException; + + +/** + * Created by Gabriel on 28/03/2014. + */ +public class ShowInternalFrameAction extends AbstractAction { + private final JDesktopPane desktopPane; + private final JInternalFrame internalFrame; + + public ShowInternalFrameAction(JDesktopPane desktopPane, JInternalFrame internalFrame) { + super(internalFrame.getTitle()); + this.desktopPane = desktopPane; + this.internalFrame = internalFrame; + } + + @Override + public void actionPerformed(ActionEvent e) { + if (!internalFrame.isVisible()) { + desktopPane.add(internalFrame); + internalFrame.setVisible(true); + } else { + try { + internalFrame.setSelected(true); + } catch (PropertyVetoException e1) { + e1.printStackTrace(System.err); + } + } + } +} \ No newline at end of file diff --git a/src/conteneurs/CustomGamme.java b/src/conteneurs/CustomGamme.java new file mode 100644 index 0000000..1ec127e --- /dev/null +++ b/src/conteneurs/CustomGamme.java @@ -0,0 +1,69 @@ +package conteneurs; + + +import java.util.ArrayList; + +/** + * CustomGamme permet de créer une gamme en ne sélectionnant uniquement les fréquences qui nous intéressent. + */ +public class CustomGamme extends Gamme{ + private final ArrayList freqID; + + public CustomGamme(ArrayList listeFreq) { + freqID = listeFreq; + } + + /** Cherche l'indice d'une fréquence donnée en paramètre. pour une gamme quelconque. La méthode de recherche des indices les plus proches utilise la dichotomie. + * @param freq la fréquence donnée en paramètre + * @return l'indice de la fréquence que l'on cherche */ + public ArrayList getIndex (float freq) { + int indexPrec = 0; + int indexSuiv = this.freqID.size()-1; + int newIndex = 0; + ArrayList res = new ArrayList<>(); + + + // Gère le cas où on demande une fréquence qui n'est pas comprise entre le minimum et le maximum des fréquences de cette gamme. + if(freq > this.freqID.get(this.freqID.size()-1) || freq < this.freqID.get(0)){ + res.add(-1); + return res; + } + + + //Trouve les 2 indices qui encadrent la fréquence + while( (indexSuiv-indexPrec) > 1){ + newIndex = Math.round((indexSuiv + indexPrec)/2); + // System.out.println("test : "+newIndex); + if(freq > this.getFreq(newIndex)){ + indexPrec = newIndex; + } else { + indexSuiv = newIndex; + } + } + + /** Retourne un seul indice si une des fréquences est exactement une de la liste. Sinon, retourne les deux indices les plus proches */ + if(freq == this.getFreq(indexSuiv)){ + res.add(indexSuiv); + } else if(freq == this.getFreq(indexPrec)){ + res.add(indexPrec); + } else { + res.add(indexPrec); + res.add(indexSuiv); + float coeff = (freq - this.getFreq(indexPrec)) / (this.getFreq(indexSuiv)-this.getFreq(indexPrec)); + res.add(coeff); + } + return res; + } + + /** Renvoie le nombre de fréquences que contient la gamme */ + public int gammeSize(){ + return this.freqID.size(); + } + + /** Renvoie la fréquence à l'indice donné dans la liste. La fréquence est stockée en Hz + * @param index l'indice de la fréquence à renvoyer + * @return la fréquence en Hz */ + public float getFreq (int index) { + return this.freqID.get(index); + } +} diff --git a/src/conteneurs/Gamme.java b/src/conteneurs/Gamme.java new file mode 100644 index 0000000..447a8bd --- /dev/null +++ b/src/conteneurs/Gamme.java @@ -0,0 +1,24 @@ +package conteneurs; + +import java.util.ArrayList; + +public abstract class Gamme { + /** Renvoie l'indice associé à une fréquence donnée + * Si jamais la fréquence n'existe pas, on prend la plus proche */ + public abstract ArrayList getIndex(float freq); + + /** Renvoie la fréquence associée à un indice donné */ + public abstract float getFreq(int index); + + /** Renvoie le nombre de fréquences que contient la gamme */ + public abstract int gammeSize(); + + + public ArrayList toStringList(){ + ArrayList r = new ArrayList<>(gammeSize()); + for (int i = 0; i < gammeSize(); i++) + r.add(String.valueOf(getFreq(i))); + + return r; + } +} diff --git a/src/conteneurs/Note.java b/src/conteneurs/Note.java new file mode 100644 index 0000000..e17db94 --- /dev/null +++ b/src/conteneurs/Note.java @@ -0,0 +1,104 @@ +package conteneurs; + +import generictools.Instant; +import processing.buffer.NoteBuffer; + +/** + * Created by gaby on 02/06/14. + */ +public class Note extends ObservableObject { + + private int noteID; + private int bufferID = -1; + private Instant start; + private Instant end; + private NoteBuffer buffer; + + //Caractérise la durée de la note (en diviseur de la ronde: 1 pour ronde, 2 pour blanche, 4 pour noire ...) + private String unifiedDuration = null; + + public Note(int noteID, Instant start, Instant end) { + this.noteID = noteID; + this.start = start; + this.end = end; + } + + /** Renvoie l'instant de départ de la note + * @return l'instant sous forme d'Instant. */ + public Instant getStart() { + return start; + } + + /** Définit l'instant de départ d'une note */ + public void setStart(Instant start) { + this.start = start; + emit(ObservableObjectEvent.Type.DATA_CHANGED); + } + + /** Permet de récupérer l'instant de fin de la note */ + public Instant getEnd() { + return end; + } + + /** Redefinit l'instant de fin de la note + * @param end l'instant de fin */ + public void setEnd(Instant end) { + this.end = end; + emit(ObservableObjectEvent.Type.DATA_CHANGED); + } + + /** Permet de récupérer l'index de la note (exemple: la 49 ... )*/ + public int getNoteID() { + return noteID; + } + + + /** @return le nom de la note*/ + public String getNoteName(){ + return NoteGamme.getNoteName(noteID); + } + + /**@return le nom anglais de la note*/ + public String getEnglishNoteName(){return NoteGamme.getEnglishNoteName(noteID);} + + /** @return la fréquence de la note*/ + public float getFreq(){ + return NoteGamme.getStatFreq(noteID); + } + + /** @return Identifiant de la note dans le buffer de note */ + public int getBufferID() { + return bufferID; + } + + public void setBufferID(int bufferID) { + this.bufferID = bufferID; + emit(ObservableObjectEvent.Type.ID_CHANGED); + } + + public NoteBuffer getBuffer() { + return buffer; + } + + public void setBuffer(NoteBuffer buffer) { + this.buffer = buffer; + emit(ObservableObjectEvent.Type.ID_CHANGED); + } + + public String getUnifiedDuration() { + return unifiedDuration; + } + + public void setUnifiedDuration(String unifiedDuration) { + this.unifiedDuration = unifiedDuration; + } + + public float getDuration(){ + return end.substract(start); + } + + @Override + public Class getType() { + return Note.class; + } +} diff --git a/src/conteneurs/NoteEvent.java b/src/conteneurs/NoteEvent.java new file mode 100644 index 0000000..92ea563 --- /dev/null +++ b/src/conteneurs/NoteEvent.java @@ -0,0 +1,69 @@ +package conteneurs; + +/** + * Created by gaby on 14/05/14. + */ +public class NoteEvent extends ObservableObject { + private NoteProfiled note; + private Type type; + private float eventAmpli; + + public NoteEvent(NoteProfiled note, Type type, float eventAmpli) { + this.note = note; + this.type = type; + this.eventAmpli = eventAmpli; + } + + /** Permet de creer un evenement de type "Apparition de note" + * @param eventAmpli l'amplitude du changement + * @param note la note qui contient le profil */ + public static NoteEvent apparitionEvent(NoteProfiled note, float eventAmpli){ + return new NoteEvent(note, Type.APPARITION, eventAmpli); + } + + /** Permet de creer un evenement de type "Disparition de note + * @param eventAmpli l'amplitude du changement + * @param note la note qui contient le profil */ + public static NoteEvent disparitionEvent(NoteProfiled note, float eventAmpli){ + return new NoteEvent(note, Type.DISPARITION, eventAmpli); + } + + /** Renvoie la note associee a cette instance d'evenement */ + public NoteProfiled getNote() { + return note; + } + + /** Redefinit la note associee a cette instance d'evenement */ + public void setNote(NoteProfiled note) { + this.note = note; + emit(ObservableObjectEvent.Type.DATA_CHANGED); + } + + public float getEventAmpli() { + return eventAmpli; + } + + public void setEventAmpli(float eventAmpli) { + this.eventAmpli = eventAmpli; + emit(ObservableObjectEvent.Type.DATA_CHANGED); + } + + /** Renvoie le type de l'evenement : Disparition ou Apparition */ + public Type getEventType(){ + return type; + } + + @Override + public Class getType() { + return NoteEvent.class; + } + + public enum Type{ + APPARITION, + DISPARITION + } + + public String toString(){ + return note.toString() + (type==Type.APPARITION?" Apparition: ":" Disparation: ") +eventAmpli; + } +} diff --git a/src/conteneurs/NoteEventList.java b/src/conteneurs/NoteEventList.java new file mode 100644 index 0000000..b26dcfa --- /dev/null +++ b/src/conteneurs/NoteEventList.java @@ -0,0 +1,49 @@ +package conteneurs; + +import java.util.ArrayList; + +/** + * Created by gaby on 21/05/14. + * Contient une liste d'evenement de notes, a savoir une liste d'apparitions et de disparitions de notes + */ +public class NoteEventList extends ObservableList { + public NoteEventList() { + } + + /** Renvoie l'evenement associé à la note donnee en paramètre + * @param noteID l'identifiant de la note + * @return l'evenement sous forme de NoteEvent */ + public NoteEvent getByNoteId(int noteID){ + int id = 0; + for(id = 0; id < size(); id++) + if(get(id).getNote().getIdNote()==noteID) + break; + if(id==size()) + return null; + + return get(id); + } + + /** Renvoie la gamme dans laquelle les notes associées aux evenements de cette liste se trouvent */ + public Gamme getEventsGamme(){ + ArrayList list = new ArrayList<>(size()); + for (int i = 0; i < size(); i++) + list.add(get(i).getNote().getFreq()); + + return new CustomGamme(list); + } + + @Override + public void add(NoteEvent observableItem) { + int addId = 0; + float freq = observableItem.getNote().getFreq(); + for(addId=size()-1; addId>=0; addId--){ + if(get(addId).getNote().getFreq() < freq){ + break; + } + } + list.add(addId+1, observableItem); + + + } +} diff --git a/src/conteneurs/NoteGamme.java b/src/conteneurs/NoteGamme.java new file mode 100644 index 0000000..af62487 --- /dev/null +++ b/src/conteneurs/NoteGamme.java @@ -0,0 +1,247 @@ +package conteneurs; + +import java.util.ArrayList; + +/** + * Created by Gabriel on 16/04/2014. + * Gamme qui ne garde que les fréquences qui correspondent à des notes utilisées dans les gammes classiques tempérées + */ +public class NoteGamme extends Gamme { + + static final float MAX_FREQ = 2100; + static final float MIN_FREQ = 18.354002f; + private ArrayList freqID; + + private NoteGamme(){ + this.freqID = new ArrayList(); + + } + + private static NoteGamme noteGamme = new NoteGamme(); + public static NoteGamme gamme(){ + return noteGamme; + } + + /** public void computeFreqNote(){ + /**for(float freq = 440; freq < this.MAX_FREQ; freq = freq*(float)Math.pow(2, 1/12.0) ){ + this.freqID.addNote(freq); + System.out.println(freqID.get(freqID.size()-1)); + } + for(float freq = 440; freq > MIN_FREQ; freq = freq/(float)Math.pow(2,1/12.0)){ + this.freqID.addNote(0,freq); + System.out.println(freqID.get(0)); + } + System.out.println(freqID.size()); + + for(float f = MIN_FREQ; f < MAX_FREQ; f = f*(float)Math.pow(2, 1/12.0)){ + freqID.addNote(f); + System.out.println(f); + } + } */ + + @Override + public ArrayList getIndex(float freq) { + return getStatIndex(freq); + } + + public static ArrayList getStatIndex(float freq){ + ArrayList res = new ArrayList<>(); + + // Gère le cas où on demande une fréquence qui n'est pas comprise entre le minimum et le maximum des fréquences de cette gamme. + if(freq < MIN_FREQ ||freq > MAX_FREQ){ + res.add(-1); + return res; + } + + //Cherche l'indice du dessus + float f = MIN_FREQ; + int index = 0; + while(f <= freq){ + f = f * (float)Math.pow(2, 1/12.0); + index++; + } + //Calcul du coefficient d'interpolation: 0 si freq = fréquence inférieure, 1 si freq = freq supérieure + float coeff = (freq - (f/(float)Math.pow(2,1/12.0)))/(f - (f/(float)Math.pow(2,1/12.0))); + + res.add(index-1); + res.add(index); + res.add(coeff); + return res; + } + + /** Renvoie l'indice de la note a partir de son nom + * @param name son nom en tant que chaine de caractere + * @return l'indice de la note */ + public static int getIndex(String name){ + int id = ('0'+name.charAt(name.length()-1))*12; + + if(name.startsWith("RE#")) + id+=1; + else if(name.startsWith("MI")) + id+=2; + else if(name.startsWith("FA")) + id+=3; + else if(name.startsWith("FA#")) + id+=4; + else if(name.startsWith("SOL")) + id+=5; + else if(name.startsWith("SOL#")) + id+=6; + else if(name.startsWith("LA")) + id+=7; + else if(name.startsWith("LA#")) + id+=8; + else if(name.startsWith("SI")) + id+=9; + else if(name.startsWith("DO")) + id+=10; + else if(name.startsWith("DO#")) + id+=11; + + return id; + } + + @Override + public float getFreq(int index) { + return getStatFreq(index); + } + + public static float getStatFreq(int index){ + return MIN_FREQ * (float)Math.pow(2,index/12.0); + } + + @Override + public int gammeSize() { + return gammeStatSize(); + } + + public static int gammeStatSize(){ + int size = 0; + for(float f = MIN_FREQ; f < MAX_FREQ; f = f*(float)Math.pow(2,1/12.0)){ + size ++; + } + return size; + } + + /** Renvoie le nom d'une note en fonction de la fréquence + * Si la fréquence n'est pas proche d'une note (coeff < 5%), l'indice renvoyé est -1 + * @param freq la fréquence dont on cherche la note qui correspond + * @return le nom de la note + */ + public static String getNoteName(float freq){ + + ArrayList res = getStatIndex(freq); + int index = -1; + float coeff = (float)res.get(2); + if(coeff < 0.05){ + index = (int)res.get(0); + } else if (coeff > 0.95){ + index = (int)res.get(1); + } + if(index != -1) + return getNoteName(index); + + return null; + } + + /** Renvoie la note en fonction de l'indice de la note, (indice 0 : RE) */ + public static String getNoteName(int index){ + String noteName = ""; + if (index >= gammeStatSize() || index <0) + return null; + + int octave = (index/12); + index = index%12; + switch(index){ + case 0: noteName = "RE"; + break; + case 1: noteName = "RE#"; + break; + case 2: noteName = "MI"; + break; + case 3: noteName = "FA"; + break; + case 4: noteName = "FA#"; + break; + case 5: noteName = "SOL"; + break; + case 6: noteName = "SOL#"; + break; + case 7: noteName = "LA"; + break; + case 8: noteName = "LA#"; + break; + case 9: noteName = "SI"; + break; + case 10: noteName = "DO"; + octave++; + break; + case 11: noteName = "DO#"; + octave++; + break; + } + noteName = noteName + octave; + return noteName; + } + + /** Renvoie le nom de la note en version anglaise */ + public static String getEnglishNoteName(int index){ + String noteName = ""; + if (index >= gammeStatSize() || index <0) + return null; + + int octave = (index/12); + index = index%12; + switch(index){ + case 0: noteName = "d"; + break; + case 1: noteName = "dis"; + break; + case 2: noteName = "c"; + break; + case 3: noteName = "f"; + break; + case 4: noteName = "fis"; + break; + case 5: noteName = "e"; + break; + case 6: noteName = "eis"; + break; + case 7: noteName = "a"; + break; + case 8: noteName = "ais"; + break; + case 9: noteName = "b"; + break; + case 10: noteName = "c"; + octave++; + break; + case 11: noteName = "cis"; + octave++; + break; + } + for (int i = 2; i < octave; i++) + noteName+="\'"; + + return noteName; + } + + /** Renvoie la frequence maximale de la NoteGamme */ + public static float getMaxFreq() { + return MAX_FREQ; + } + + /** Renvoie la frequence minimale de la NoteGamme */ + public static float getMinFreq() { + return MIN_FREQ; + } + + @Override + public ArrayList toStringList(){ + ArrayList r = new ArrayList<>(gammeSize()); + for (int i = 0; i < gammeSize(); i++) + r.add(String.valueOf(getNoteName(i))); + + return r; + } +} diff --git a/src/conteneurs/NoteID.java b/src/conteneurs/NoteID.java new file mode 100644 index 0000000..0ea5b15 --- /dev/null +++ b/src/conteneurs/NoteID.java @@ -0,0 +1,51 @@ +package conteneurs; + + import processing.buffer.NoteBuffer; + +/** Created by gaby on 02/06/14. + * Contient l'identifiant d'une note. Une note jouée est déclarée à tous les instants où elle est jouée, + * mais l'identifiant de la note est commun car il s'agit de la meme note. Fait le lien entre tous les instants + * ou une note est jouee */ +public class NoteID extends ObservableObject implements ObservableObject.Listener{ + private NoteBuffer buffer; + private int bufferID=-1; + + public NoteID(Note note){ + bufferID = note.getBufferID(); + buffer = note.getBuffer(); + note.addListener(this); + } + + @Override + public Class getType() { + return NoteID.class; + } + + public int getBufferID() { + return bufferID; + } + + public NoteBuffer getBuffer() { + return buffer; + } + + public Note note(){ + if(buffer==null) + return null; + + return buffer.getNote(bufferID); + } + + public boolean isValid(){ + return buffer!=null && bufferID!=-1; + } + + @Override + public void observableOjectEvent(ObservableObjectEvent e) { + emit(e.getType()); + if(e.getType()== ObservableObjectEvent.Type.ID_CHANGED){ + buffer = ((Note)e.getEmmiter()).getBuffer(); + bufferID = ((Note)e.getEmmiter()).getBufferID(); + } + } +} diff --git a/src/conteneurs/NoteList.java b/src/conteneurs/NoteList.java new file mode 100644 index 0000000..3acda45 --- /dev/null +++ b/src/conteneurs/NoteList.java @@ -0,0 +1,66 @@ +package conteneurs; + +import processing.buffer.NoteBuffer; + +import java.util.ArrayList; + +/** + * Created by gaby on 02/06/14. + */ +public class NoteList extends ObservableList { + + + public NoteList() { + } + + + /** Renvoie la note dans la liste de note dont on donne l'identifiant en paramètre */ + public Note getNoteById(int noteID) { + int id = getIdById(noteID); + if(id>-1) + return get(id).note(); + return null; + } + + /** Renvoie l'identifiant de la note à partir de son index (exemple : la 49) */ + public int getIdById(int noteID){ + for (int id = 0; id < size() && get(id).note().getNoteID() <= noteID; id++) + if (get(id).note().getNoteID() == noteID) + return id; + + return -1; + } + + /** Permet d'enlever une note à la liste de note */ + public boolean remove(Note note){ + int id = getIdById(note.getNoteID()); + + if(id>-1){ + remove(id); + return true; + } + return false; + } + + /** Permet d'ajouter une note à la liste de note */ + public void add(Note note){ + add(new NoteID(note)); + } + + /** Permet d'ajouter une note à partir de son identifiant de note */ + public void add(NoteID noteID) { + + int addId = 0; + float freq = noteID.note().getFreq(); + for(addId=size()-1; addId>=0; addId--){ + if(get(addId).note().getFreq() < freq){ + break; + }else if(get(addId).note().getFreq() == freq){ + set(addId, noteID); + return; + } + } + list.add(addId+1, noteID); + } + +} diff --git a/src/conteneurs/NoteProfiled.java b/src/conteneurs/NoteProfiled.java new file mode 100644 index 0000000..501be20 --- /dev/null +++ b/src/conteneurs/NoteProfiled.java @@ -0,0 +1,50 @@ +package conteneurs; + +/** + * Created by gaby on 14/05/14. + */ +public class NoteProfiled extends ObservableObject{ + private int idNote; + private Profil profil; + + public NoteProfiled(int idNote, Profil profil) { + this.idNote = idNote; + this.profil = profil; + } + + + @Override + public Class getType() { + return NoteProfiled.class; + } + + public Profil getProfil() { + return profil; + } + + /** Permet de definir le profil d'une note, cad l'amplitude des harmoniques + * @param profil le profil de la note */ + public void setProfil(Profil profil) { + this.profil = profil; + emit(ObservableObjectEvent.Type.DATA_CHANGED); + } + + /** Indice qui caracterise la hauteur de la note */ + public int getIdNote() { + return idNote; + } + + /** Renvoie la frequence d'une note à partir de son indice (exemple: la 49) */ + public float getFreq(){ + return NoteGamme.getStatFreq(idNote); + } + + /** Renvoie le nom de la note a partir de sa hauteur */ + public String getNoteName(){ + return NoteGamme.getNoteName(idNote); + } + + public String toString(){ + return getNoteName(); + } +} diff --git a/src/conteneurs/ObservableList.java b/src/conteneurs/ObservableList.java new file mode 100644 index 0000000..8e66b37 --- /dev/null +++ b/src/conteneurs/ObservableList.java @@ -0,0 +1,108 @@ +package conteneurs; + +import java.util.ArrayList; + +/** + * Created by gaby on 14/05/14. + * Liste écoutable d'objet écoutable + * + * Cet objet est l'adaptation d'une ArrayList classique à laquelle on ajoute la gestion évenementielle lorsqu'on lui + * ajoute ou retire des échantillons ou quand ceux ci sont modifiés. + */ +public class ObservableList extends ObservableObject implements ObservableObject.Listener { + + ArrayList list = new ArrayList<>(); + + public ObservableList() { + + } + + /** + * Ajoute un objet à la liste écoutable + * @param observableItem objet écoutable à ajouter + */ + public void add(T observableItem){ + list.add(observableItem); + observableItem.addListener(this); + emit(ObservableObjectEvent.Type.DATA_CHANGED); + } + + /** + * Remplace un objet dans la liste + * @param idItem identifiant de l'objet à remplacer + * @param observaleItem nouvelle objet écoutable + * @return vrai si idItem est un identifiant correct + */ + public boolean set(int idItem, T observaleItem){ + if(!isIdCorrect(idItem)) + return false; + + list.set(idItem, observaleItem).removeListener(this); + observaleItem.addListener(this); + emit(ObservableObjectEvent.Type.DATA_CHANGED); + return true; + } + + /** + * Supprime un objet de la liste + * @param observableItem Objet observable à supprimer + * @return Vrai si l'objet était contenu dans la liste et a été supprimé + */ + public boolean remove(T observableItem){ + return remove(list.indexOf(observableItem)); + } + + /** + * Supprime un objet de la liste + * @param idItem Identifiant de l*bjet observable à supprimer + * @return Vrai si l'objet était contenu dans la liste et a été supprimé + */ + public boolean remove(int idItem){ + if(!isIdCorrect(idItem)) + return false; + + list.remove(idItem).removeListener(this); + emit(ObservableObjectEvent.Type.DATA_CHANGED); + return true; + } + + /** + * Retourne l'objet correspondant à l'index + * @param idItem Index de l'objet à récupérer + * @return L'objet observable correpondant. /!\ Peut renvoyer null /!\ + */ + public T get(int idItem){ + if(!isIdCorrect(idItem)) + return null; + + return list.get(idItem); + } + + + /** + * Retourne la taille de la liste + * @return Taille + */ + public int size(){ + return list.size(); + } + + + private boolean isIdCorrect(int id){ + return id>=0&&id listeners = new ArrayList<>(); + + public ObservableObject() { + } + + /** + * Ajoute des écouteurs à l'objet écoutable + * @param l Objet écouteur + */ + public void addListener(Listener l){ + if(listeners.contains(l)) + return; + + listeners.add(l); + } + + + /** + * Supprime un écouteurs + * @param l écouteurs à supprimer + */ + public void removeListener(Listener l){ + if(!listeners.contains(l)) + return; + listeners.remove(l); + } + + /** + * Supprime un écouteur par sont index dans la liste des écouteurs + * @param idListener index des écouteurs + */ + public void removeListener(int idListener){ + if(idListener<0 && idListener>=listeners.size()) + return; + } + + /** + * Supprime un écouteur selon sa signature + * @param signature signature de l'écouteur à supprimer + */ + public void removeSignedListener(String signature){ + int id = getSignedListenerID(signature); + + if(id==-1) + return; + removeListener(id); + } + + /** + * Retourne l'index d'un écouteur dans la liste des écouteurs en fonction de se signature + * @param signature Signature de l'écouteur recherché + * @return Index de l'écouteur + */ + public int getSignedListenerID(String signature){ + for (int i = 0; i < listeners.size(); i++) + if (listeners.get(i) instanceof SignedListener) + if (((SignedListener) listeners.get(i)).signature().equals(signature)) + return i; + + return -1; + } + + /** + * Transmet un évenement à tous les écouteurs + * @param e évenement à propager + */ + public void emit(ObservableObjectEvent e){ + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).observableOjectEvent(e); + } + } + + /** + * Créé et transmet un évenement d'un type à tous les écouteurs + * @param t type de l'évenement à propager + */ + public void emit(ObservableObjectEvent.Type t){ + emit(new ObservableObjectEvent(this, t)); + } + + /** + * Interface définissant les objets écouteurs + */ + static public interface Listener { + /** + * Traitement de l'évenement d'objet observable + * @param e evenement + */ + public void observableOjectEvent(ObservableObjectEvent e); + } + + /** + * Classe abstraite définissant un objet écouteur auquelle on ajoute une signature pour pouvoir + * retrouver sans le stocké un écouteur grace à un String unique par écouteurs. + */ + static public abstract class SignedListener implements Listener{ + public void SignedListener(){ + } + + abstract public String signature(); + } + + public String toString(){ + return "Observable object"; + } + + /** + * Renvoie le type de l'objet écoutable + * @return Type de l'objet + */ + public abstract Class getType(); +} + diff --git a/src/conteneurs/ObservableObjectEvent.java b/src/conteneurs/ObservableObjectEvent.java new file mode 100644 index 0000000..72e58cd --- /dev/null +++ b/src/conteneurs/ObservableObjectEvent.java @@ -0,0 +1,57 @@ +package conteneurs; + +/** + * Created by gaby on 06/05/14. + * + * Evenement d(objet observable + */ +public class ObservableObjectEvent { + ObservableObject emmiter; + Type type; + + /** + * Constructeur de l'évenement + * @param emmiter Objet écoutable source de l'évenement + * @param type type de l'évenement + */ + public ObservableObjectEvent(ObservableObject emmiter, Type type) { + this.emmiter = emmiter; + this.type = type; + } + + /** + * Retourne l'objet écoutable à l'origine de l'évenement + * @return emmeteur + */ + public ObservableObject getEmmiter() { + return emmiter; + } + + + /** + * @return type de l'évenement + */ + public Type getType() { + return type; + } + + public String toString(){ + String r = emmiter.toString() + " Event: "; + switch (type){ + case DATA_CHANGED: + r+="Modified"; + break; + } + return r; + } + + /** + * Type d'évenement: + * DATA_CHANGED: les données ont été modifiés + * ID_CHANGED: les identifiants de localisation de l'objet ont été modifiées (propre aux Note) + */ + public enum Type { + DATA_CHANGED, + ID_CHANGED + } +} diff --git a/src/conteneurs/Profil.java b/src/conteneurs/Profil.java new file mode 100644 index 0000000..edc4501 --- /dev/null +++ b/src/conteneurs/Profil.java @@ -0,0 +1,73 @@ +package conteneurs; + +import java.util.ArrayList; + +/** + * Associe à un numéro d'harmonique, une amplitude + */ +public class Profil extends ObservableObject{ + private ArrayList amplitudes = new ArrayList<>(); + + public Profil() { + + } + + /** Définit/Modifie l'amplitude d'une fréquence à partir de son indice */ + public void setAmplitude (float newAmplitude, int idHarmonique) { + + if(newAmplitude==0 && idHarmonique == amplitudes.size()-1) { + amplitudes.remove(idHarmonique); + return; + } + + ensureSize(idHarmonique+1); + this.amplitudes.set(idHarmonique, newAmplitude); + emit(ObservableObjectEvent.Type.DATA_CHANGED); + } + + /** Renvoie l'amplitude de l'harmonique à l'indice considéré */ + public float getAmplitude (int idHarmonique) { + return idHarmonique getHarmoniqueID(){ + ArrayList r = new ArrayList<>(amplitudes.size()); + for (int i = 0; i < amplitudes.size(); i++) + if (amplitudes.get(i) != 0) + r.add(i); + + r.trimToSize(); + return r; + } + + /** Vérifie le nombre d'harmoniques > la taille donnee en parametre + * @param size la taille en entier + * @return boolean vrai si size getIndex(float freq) { + if(freq > this.getEndFreq()){ + ArrayList res = new ArrayList<>(); + res.add(-1); + return res; + } else if( (freq-this.startFreq)%this.step == 0 ){ + ArrayList res = new ArrayList<>(); + res.add(Math.round(freq-this.startFreq)/step); + return res; + } else { + int indexPrec = (int)Math.floor((freq-this.startFreq)/this.step); + float coeff = (freq - this.getFreq(indexPrec))/ this.step; + ArrayList res = new ArrayList<>(); + res.add(indexPrec); + res.add(indexPrec+1); + res.add(coeff); + return res; + } + + } + + /** Renvoie la fréquence associée à un indice donne en paramètre */ + public float getFreq(int index) { + if(index < this.lengthGamme) { + return this.startFreq + index * this.step; + } + return -1; + } + + /** Renvoie le nombre de fréquences contenues dans la gamme */ + public int gammeSize() { + return this.lengthGamme; + } + + /** Renvoie la fréquence de début: par défaut c'est 0 */ + public float getStartFreq() { + return this.startFreq; + } + + /** Renvoie la fréquence maximale de la gamme */ + float getEndFreq() { + return (this.lengthGamme-1)*this.step + this.startFreq; + } + + public float getStep() { + return this.step; + } +} diff --git a/src/conteneurs/Spectre.java b/src/conteneurs/Spectre.java new file mode 100644 index 0000000..4711be2 --- /dev/null +++ b/src/conteneurs/Spectre.java @@ -0,0 +1,345 @@ +package conteneurs; + +import generictools.Pair; + +import java.util.ArrayList; + +/** + * Spectre fait le lien entre les fréquences et leur amplitude. Les deux sont stockés dans des tableaux, par ordre de fréquences croissantes. + * L'association fréquences - index est faite dans la gamme + * L'association amplitude - index est faite dans le spectre + * Les index sont communs, donc on a le lien directement entre les 2. + */ +public class Spectre extends ObservableObject{ + private Gamme gamme; + private ArrayList amplitudes; + + private float moyennePos; + private float moyenneNeg; + private float eTypePos; + private float eTypeNeg; + + private static final int INDEX_PREV = 0; + private static final int INDEX_NEXT = 1; + private static final int INDEX_COEFF = 2; + private static final int INDEX_UNIQUE = 0; + + public enum InterpolationType{ + DEFAULT, + SQUARRED, + CUBIC, + MOYENNE + } + + public Spectre(Gamme gamme) { + this.gamme = gamme; + this.amplitudes = new ArrayList<>(gamme.gammeSize()); + for (int i = 0; i < gamme.gammeSize(); i++) { + this.amplitudes.add(0f); + } + + } + + /** + * Définit toutes les amplitudes d'un coup + * @param newAmplitudes amplitudes à charger + */ + public void setAllAmplitudes(ArrayList newAmplitudes){ + if(newAmplitudes.size() == this.gamme.gammeSize()) { + this.amplitudes = newAmplitudes; + } else if(newAmplitudes.size() > this.gamme.gammeSize()) { + this.amplitudes = (ArrayList)newAmplitudes.subList(0,this.gamme.gammeSize()); + } else { + this.amplitudes = newAmplitudes; + while(this.amplitudes.size() != this.gamme.gammeSize()){ + this.amplitudes.add(0f); + } + } + emit(ObservableObjectEvent.Type.DATA_CHANGED); + + } + + /** Change l'amplitude à un index donné + * @param id l'indice de la fréquence dont on veut définir l'amplitude (rappel: elles sont classées dans l'ordre croissant + * @param newAmplitude la valeur de l'amplitude de la fréquence à modifier + */ + public void setAmplitudeAt (int id, float newAmplitude) { + if(id>=0 && id listRes = this.gamme.getIndex(freq); + if(listRes.size() == 1){ + this.setAmplitudeAt((Integer)listRes.get(INDEX_UNIQUE), newAmplitude); // TEST + emit(ObservableObjectEvent.Type.DATA_CHANGED); + } + } + + /** Change l'amplitude à d'une fréquence donnée en dB. + * Pour cela, on cherche l'indice correspondant à cette fréquence + * @param freq la fréquence dont l'amplitude est à modifier/définir + * @param dBAmplitude la valeur de l'amplitude de la fréquence à modifier en dB + */ + public void setdBAmplitudeAt (float freq, float dBAmplitude) { setAmplitudeAt(freq, dBToAmpli(dBAmplitude));} + + /** Renvoie l'amplitude d'une fréquence + * @param id l'indice de la fréquence dont on cherche l'amplitude + * @return l'amplitude de la fréquence. */ + public float getAmplitude (int id) { + return this.amplitudes.get(id); + } + + /** Renvoie l'amplitude d'une fréquence en dB + * @param id l'indice de la fréquence dont on cherche l'amplitude + * @return l'amplitude de la fréquence en dB. */ + public float getdBAmplitude (int id) { + return ampliTodB(this.amplitudes.get(id)); + } + + /** Renvoie l'amplitude d'une fréquence donnée en paramètre, -1 si la fréquence n'existe pas + * @param freq la fréquence donnée dont on cherche l'amplitude + * @return l'amplitude de la fréquence. + */ + public Pair getAmplitude(float freq, InterpolationType iType) { + ArrayList listRes = this.gamme.getIndex(freq); + + if(listRes.size() == 1 && ((Integer)listRes.get(0)!=-1)){ + return new Pair<>(this.amplitudes.get((Integer)listRes.get(INDEX_UNIQUE)), 1f); + } else if((Integer)listRes.get(0) == -1) { + return new Pair<>(0f, 0f); + } else { + float amplitudePrev = this.amplitudes.get((Integer)listRes.get(INDEX_PREV)); + float amplitudeNext = this.amplitudes.get((Integer)listRes.get(INDEX_NEXT)); + float coeff = (Float)listRes.get(INDEX_COEFF); + // Pondère en fonction du coefficient donné pour situer la fréquence entre les 2 les plus proches + + Pair res = null; + switch(iType){ + case DEFAULT: res = new Pair<> ( ( amplitudePrev * (1-coeff) ) + (amplitudeNext * coeff), coeff); + break; + case SQUARRED: res = new Pair<> ( (float)Math.sqrt((amplitudePrev*amplitudePrev*(1-coeff)*(1-coeff) ) + (amplitudeNext*amplitudeNext*coeff*coeff)), coeff); + break; + case MOYENNE: res = new Pair<> ((amplitudeNext+amplitudePrev)/2, coeff); + break; + case CUBIC: + float y1 = (Integer)listRes.get(INDEX_PREV)>1? this.amplitudes.get((Integer) listRes.get(INDEX_PREV) - 1):0; + float y2 = amplitudePrev; + float y3 = amplitudeNext; + float y4 = this.amplitudes.get((Integer) listRes.get(INDEX_NEXT) + 1); + float x1 = this.gamme.getFreq((Integer) listRes.get(INDEX_PREV)-1); + float x2 = this.gamme.getFreq((Integer) listRes.get(INDEX_PREV)); + float x3 = this.gamme.getFreq((Integer) listRes.get(INDEX_NEXT)); + float x4 = (Integer)listRes.get(INDEX_NEXT)(lagrangeInterp(x1, x2, x3, x4, y1, y2, y3, y4, freq), coeff); + break; + } + return res; + } + } + + /** Calcule la valeur en un point grâce à une interpolation par un polynome de degré 3, + * qui doit passer par les 4 points dont les coordonnées sont données en paramètre */ + private float lagrangeInterp(float x1, float x2, float x3, float x4, float y1, float y2, float y3, float y4, float freq){ + float ampli = y1 * ( ((freq-x2)*(freq-x3)*(freq-x4))/((x1-x2)*(x1-x3)*(x1-x4)) ); + ampli += y2 * ( ((freq-x1)*(freq-x3)*(freq-x4))/((x2-x1)*(x2-x3)*(x2-x4)) ); + ampli += y3 * ( ((freq-x1)*(freq-x2)*(freq-x4))/((x3-x1)*(x3-x2)*(x3-x4)) ); + ampli += y4 * ( ((freq-x1)*(freq-x2)*(freq-x3))/((x4-x1)*(x4-x2)*(x4-x3)) ); + return ampli; + } + + /** Renvoie l'amplitude d'une fréquence donnée en paramètre, -1 si la fréquence n'existe pas + * @param freq la fréquence donnée dont on cherche l'amplitude + * @return l'amplitude de la fréquence en dB. + */ + public Pair getdBAmplitude(float freq) { Pair r = getAmplitude(freq, InterpolationType.DEFAULT); r.setFirst(ampliTodB(r.first())); return r;} + + /** Change la gamme sur laquelle ce spectre travaille: on ne sélectionne que certaines fréquences, ou on ne prend pas les mêmes, ou bien de manière pas linéaire + * La gamme donnée en paramètre contient certaines fréquences, pour trouver les amplitudes soit on les a directement, soit on les interpole suivant si on les avait déjà ou non. + * Renvoie un indice sur la qualité de l'interpolation réalisée sur les fréquences. + * @param newGamme la nouvelle Gamme contenant les nouvelles fréquences dont on doit calculer les amplitudes + * @return l'indice de qualité de l'interpolation: 0 au plus mauvais (on tombe toujours pile au milieu entre les 2 fréquences), 1 si on tombe toujours sur une fréquence pré existante */ + public float castToGamme (Gamme newGamme) { + ArrayList newAmplitudes = new ArrayList<>(); + float sumIndicesQ = 0; + float indiceQualite = 1; + + //System.out.println("newGamme size: "+newGamme.gammeSize()); + newAmplitudes.add(0f); + for(int i = 1; i < newGamme.gammeSize()-1; i++){ + Pair coupleFreqCoef = this.getAmplitude(newGamme.getFreq(i), InterpolationType.CUBIC); + newAmplitudes.add(coupleFreqCoef.first()); + sumIndicesQ += Math.abs((coupleFreqCoef.second()-0.5)*2); + } + newAmplitudes.add(0f); + float moyenneIndiceQ = sumIndicesQ / newGamme.gammeSize(); + //Calcul l'indice de qualité de l'ensemble du traitement + //System.out.println("Moyenne indices qualités : "+moyenneIndiceQ); + + indiceQualite = ((moyenneIndiceQ * newGamme.gammeSize()) + (newAmplitudes.size() - newGamme.gammeSize())) / newAmplitudes.size(); + indiceQualite = indiceQualite*indiceQualite; + + this.gamme = newGamme; + this.amplitudes = newAmplitudes; + emit(ObservableObjectEvent.Type.DATA_CHANGED); + + return indiceQualite; + } + + /** Crée une RegularGamme à partir de la gamme actuelle, sans aucune extrapolation. + * Selectionne la fréquence la plus proche pour caler celle de la gamme existante + */ + //public void castToRegularGamme(RegularGamme newGamme){ + public void castToRegularGamme(){ + ArrayList newAmplitudes = new ArrayList<>(); + RegularGamme newGammeTest = new RegularGamme(0f, Math.round(this.gamme.getFreq(this.getGamme().gammeSize()-1)), 1f); + for(int i = 0; i < newGammeTest.gammeSize(); i++){ + newAmplitudes.add(0f); + } + for (int i = 0; i < this.gamme.gammeSize()-1; i++) { + //System.out.println(this.gamme.getFreq(i)); + newAmplitudes.set(Math.round(this.gamme.getFreq(i)), this.amplitudes.get(i)); + } + this.gamme = newGammeTest; + this.amplitudes = newAmplitudes; + } + + /** Permet de copier un spectre. Utile si jamais on ne veut pas modifier directement un buffer partagé par plusieurs algo, + * et si on veut faire un traitement sur celui-là + * @return la copie de ce spectre, avec une nouvelle adresse mémoire du coup */ + public Spectre copy(){ + Spectre newSpectre = new Spectre(this.gamme); + ArrayList newAmpli = new ArrayList(this.amplitudes.size()); + + for (int i = 0; i < this.amplitudes.size(); i++) { + newAmpli.add(this.amplitudes.get(i)); + } + newSpectre.setAllAmplitudes(newAmpli); + + return newSpectre; + } + + /** Renvoie la gamme liée à cette instance de spectre */ + public Gamme getGamme() { + return this.gamme; + } + + public ArrayList getAmplitudes() { + return amplitudes; + } + + /** Calcule les moyennes des amplitudes positives et négatives ainsi que leurs écarts-types. + * Sert à faire un filtrage du bruit sur les spectres */ + public void calculStats(){ + int gammeSize = this.getGamme().gammeSize(); + + int nPos = 0; + int nNeg = 0; + // Calcul de la moyenne + for (int i = 0; i < gammeSize; i++) { + if(this.getAmplitude(i)>=0) { + moyennePos += this.getAmplitude(i); + nPos++; + } else { + moyenneNeg += this.getAmplitude(i); + nNeg++; + } + } + + moyennePos = moyennePos / nPos; + moyenneNeg = moyenneNeg / nNeg; + + // Calcul de l'écart-type + for (int i = 0; i < gammeSize; i++) { + if(this.getAmplitude(i)>=0) + eTypePos += (this.getAmplitude(i) - moyennePos)*(this.getAmplitude(i) - moyennePos ); + else + eTypeNeg += (this.getAmplitude(i) - moyenneNeg)*(this.getAmplitude(i) - moyenneNeg ); + } + eTypePos = (float)Math.sqrt(eTypePos/nPos); + eTypeNeg = (float)Math.sqrt(eTypeNeg/nNeg); + } + + /** Filtre le bruit sur le principe de moyenne + ou - écart-type, que l'on doit calculer + * VALEUR_SEUIL : Moyenne + (2*écart-type) --> 95,4% des valeurs à l'intérieur. + * @return le spectre sans le bruit */ + public Spectre noiseFilter(){ + this.calculStats(); + if(this == null){ + return null; + } + Spectre res = this.copy(); + + //final float VALEUR_SEUIL_POS = 0.7f*(float)(this.eTypePos)+this.moyennePos; + //final float VALEUR_SEUIL_NEG = -0.7f*(float)(this.eTypeNeg)+this.moyenneNeg; + + + final float VALEUR_SEUIL_POS = this.moyennePos; + final float VALEUR_SEUIL_NEG = this.moyenneNeg; + + for (int i = 1; i < getGamme().gammeSize(); i++) { + if(res.getAmplitude(i) < VALEUR_SEUIL_POS && res.getAmplitude(i)>VALEUR_SEUIL_NEG){ + res.setAmplitudeAt(i, 0); + } + } + res.setAmplitudeAt(0, VALEUR_SEUIL_POS); + return res; + } + + /** + * Renvoie la valeur en dB d'une intensité en pression + * @param ampli valeur en pression + * @return valeur en dB + */ + static public float ampliTodB(float ampli){return 10f*(float)Math.log10(ampli);} + + /** + * Renvoie la valeur en pression d'une intensité en dB + * @param dB valeur en dB + * @return valeur en pression + */ + static public float dBToAmpli(float dB){return (float)Math.pow(dB/10f, 10);} + + public float getMoyennePos() { + return moyennePos; + } + + public float getMoyenneNeg() { + return moyenneNeg; + } + + public float geteTypePos() { + return eTypePos; + } + + public float geteTypeNeg() { + return eTypeNeg; + } + + @Override + public String toString() { + return "Spectre"; + } + + @Override + public Class getType() { + return Spectre.class; + } +} diff --git a/src/generictools/Complex.java b/src/generictools/Complex.java new file mode 100644 index 0000000..fdd2593 --- /dev/null +++ b/src/generictools/Complex.java @@ -0,0 +1,116 @@ +package generictools; + +/** + * Created by Gabriel on 25/03/2014. + * Permet de créer des nombres complexes, utiles pour la FFT notamment + */ +public class Complex { + + private float realPart; + private float imagPart; + public Complex (float realPart, float imagPart){ + this.realPart = realPart; + this.imagPart = imagPart; + } + + public Complex( Complex c){ + this(c.getRealPart(), c.getImagPart()); + } + + /** + * Transforme un complexe sous la forme (module, argument) en (réel, imaginaire). + * @param mod Le module du complexe à transformer + * @param arg L'argument du complexe à transformer + * @return Le complexe nouvellement formé + */ + public static Complex ComplexFromExp(float mod, float arg){ + return new Complex( mod*(float)Math.cos(arg), mod*(float)Math.sin(arg)); + } + + /** + * Calcule le conjugué d'un complexe. + * @return Le conjugué du complexe actuel (this) + */ + public Complex conj(){ + return new Complex(this.realPart, (-1f*this.imagPart) ); + } + + /** + * Somme deux complexes. + * @param c2 Le complexe auquel ajouter le complexe actuel (this) + * @return La somme des deux complexes + */ + public Complex sum(Complex c2){ + return new Complex(realPart+c2.getRealPart(), imagPart+c2.getImagPart()); + } + + /** + * Soustrait c2 au complexe actuel (this). + * @param c2 Le nombre à ôter + * @return Le complexe résultant de la soustraction + */ + public Complex sub(Complex c2){ + return new Complex(realPart-c2.getRealPart(), imagPart-c2.getImagPart()); + } + + /** + * Multiplie deux complexes entre eux. + * @param c2 Le complexe par lequel multiplier le complexe actuel (this) + * @return Le produit des deux complexes + */ + public Complex mult(Complex c2){ + return new Complex( ((realPart * c2.getRealPart()) - (imagPart * c2.getImagPart())), ((realPart*c2.getImagPart()) + (c2.getRealPart()*imagPart)) ); + } + + /** + * Prend le carré de ce complex; + * @return Le produit des deux complexes + */ + public Complex square(){ + return new Complex((realPart-imagPart)*(realPart+imagPart), 2*imagPart*realPart); + } + /** Renvoie la somme, en modifiant directement le complexe appelé */ + public Complex simpleSum(Complex c2){ + realPart += c2.getRealPart(); + imagPart += c2.getImagPart(); + return this; + } + /** Renvoie la multiplication, en modifiant directement le complexe appelé */ + public Complex simpleMult(Complex c2){ + float tmp = realPart; + realPart = ((realPart * c2.getRealPart()) - (imagPart * c2.getImagPart())); + imagPart = ((tmp*c2.getImagPart()) + (c2.getRealPart()*imagPart)); + return this; + } + + /** Permet de faire une multiplication de l'objet par lui-meme sans recreer une instance de Complexe */ + public void simpleSquare(){ + float tmp = realPart; + realPart = (realPart-imagPart)*(realPart+imagPart); + imagPart = 2*imagPart*tmp; + } + + + public float getRealPart(){ + return this.realPart; + } + public float getImagPart(){ + return this.imagPart; + } + + /** Definit ce complexe egal a celui donne en paramètre */ + public void setToEquals(Complex c){ + this.realPart = c.getRealPart(); + this.imagPart = c.getImagPart(); + } + + /** Renvoie le module du complexe */ + public float getMod(){ + return (float)Math.sqrt((this.realPart*this.realPart)+(this.imagPart*this.imagPart)); + } + /** Renvoie l'argument du complexe */ + public float getArg(){ + return (float)Math.atan(this.imagPart/this.realPart); + } + +} diff --git a/src/generictools/Instant.java b/src/generictools/Instant.java new file mode 100644 index 0000000..d4ce95d --- /dev/null +++ b/src/generictools/Instant.java @@ -0,0 +1,245 @@ +package generictools; + +import processing.buffer.Buffer; +import processing.buffer.TemporalBuffer; + +/** + * Created by gaby on 15/04/14. + * + * Instant + * + * Cette classe permet de repérer de manière absolue un instant dans le fichier traiter + * Il permet ensuite de convertir cette instant en index de TemporalBuffer selon la fréquence d'échantillonage + * et l'instant d'origine de ce dernier. + */ +public class Instant { + private float timeMS=0; + + /** + * Construit un instant en le définissant par un temps dans le buffer de référence + * @param time temps en ms. + */ + public Instant(float time){ + timeMS = time; + } + + /** + * Construit un instant en le définissant par un temps dans le buffer de référence + * @param ms temps en ms + * @return Instant correspondant + */ + public static Instant fromTime(float ms){return new Instant(ms);} + + /** + * Construit un instant en le définissant par un temps dans le buffer de référence + * @param ms temps en ms + * @param s temps en s + * @param min temps en min + * @param h temps en heure + * @return Instant correpsondant + */ + public static Instant fromTime(float ms, int s, int min, int h){return new Instant(ms+1000*(s+60*(min+60*h)));} + + /** + * Construit un instant en le définissant par un temps dans le buffer de référence + * @param ms temps en ms + * @param s temps en s + * @param min temps en min + * @return Instant correpsondant + */ + public static Instant fromTime(float ms, int s, int min){ return fromTime(ms,s,min,0);} + + /** + * Construit un instant en le définissant par un temps dans le buffer de référence + * @param ms temps en ms + * @param s temps en s + * @return Instant correpsondant + */ + public static Instant fromTime(float ms, int s){ return fromTime(ms, s, 0, 0);} + + + /** + * Construit un instant en le définissant par un index, un fréquence d'échantillonage, + * l'instant d'origine est confondu avec celui du buffer de référence + * @param index index de l'instant + * @param sampleRate fréquence d'échantillonage + * @return Instant correspondant + */ + public static Instant fromIndex(int index, int sampleRate){ + return new Instant(1000f*(float)index/(float)sampleRate); + } + + /** + * Construit un instant en le définissant par un index, un fréquence d'échantillonage et un instant d'origine + * @param index index de l'instant + * @param sampleRate fréquence d'échantillonage + * @param originInstant Instant d'origine + * @return Instant correspondant + */ + public static Instant fromIndex(int index, int sampleRate, Instant originInstant){ + return fromIndex(index,sampleRate).add(originInstant); + } + + /** + * Construit un instant en le définissant par un index, et un temporal buffer + * @param index index de l'instant + * @param temporalBuffer buffer temporel dans lequel on désire connaitre l'instant + * @return Instant correspondant + */ + public static Instant fromIndex(int index, TemporalBuffer temporalBuffer){ return fromIndex(index, temporalBuffer.getSampleRate(), temporalBuffer.getOriginInstant());} + + /** + * Construit un instant en le définissant par un index, et un buffer qui est implicitement converti en buffer temporel, + * si le buffer n'est pas un buffer temporel renvoit Instant(0) + * @param index index de l'instant + * @param buffer buffer temporel dans lequel on désire connaitre l'instant + * @return Instant correspondant ou Instant(0) + */ + public static Instant fromIndex(int index, Buffer buffer){ + if(buffer instanceof TemporalBuffer) + return fromIndex(index, (TemporalBuffer)buffer); + return new Instant(0); + } + + /** + * Récupère la durée en ms séparant ce buffer de celui passé en argument + * @param t Instant à comparer + * @return durée + */ + public float substract(Instant t){ + return timeMS - t.timeMS; + }; + + + /** + * Compare cet instant avec celui passé en argument + * @param t instant à comparer + * @return Vrai si cet instant est plus grand que t + */ + public boolean isGreaterThan(Instant t){ + return timeMS>t.timeMS; + } + + /** + * Compare cet instant avec celui passé en argument + * @param t instant à comparer + * @return Vrai si cet instant est plus petit que t + */ + public boolean isLowerThan(Instant t){ + return timeMS=t.timeMS; + } + + /** + * Compare cet instant avec celui passé en argument + * @param t instant à comparer + * @return Vrai si cet instant est plus petit ou égal que t + */ + public boolean isLowerOrEquThan(Instant t){ + return timeMS<=t.timeMS; + } + + /** + * Compare cet instant avec celui passé en argument + * @param t instant à comparer + * @return Vrai si cet instant est égal à t + */ + public boolean equals(Object t){ + if(t instanceof Instant) + return equals((Instant)t); + return false; + } + + /** + * Compare cet instant avec celui passé en argument + * @param t instant à comparer + * @return Vrai si cet instant est égal à t + */ + public boolean equals(Instant t){ + return timeMS == t.timeMS; + } + + + /** + * Translate cet instant d'un temps en ms /!\ Modifie cet instant + * @param ms temps à translater + * @return cet instant + */ + public Instant translate(float ms){ + timeMS += ms; + return this; + } + + /** + * Renvoie un instant translate d'un temps en ms, sans modifier cet instant + * @param ms temps à translater + * @return l'instant translater + */ + public Instant translated(float ms){ + return new Instant(timeMS+ms); + } + + public String toString(){ + return (int)Math.floor(timeMS/60000) + ":" +(int)Math.floor(timeMS%60000/1000)+ ":" +Math.round(timeMS%1000); + } + + + int mapToIndex(int sampleRate){ + return mapToIndex(sampleRate, new Instant(0)); + } + + /** + * Retourne l'index correspondant à cet Instant en fonction de la fréquence d'échantillonage et de l'instant d'origine + * @param sampleRate Fréquence d'échantillonage + * @param originInstant Instant d'origine + * @return index correspondant + */ + int mapToIndex(int sampleRate, Instant originInstant){ return Math.round((timeMS-originInstant.timeMS)*(float)sampleRate/1000f);} + + /** + * Retourne l'index correspondant à cet Instant dans un buffer temporel + * @param temporalBuffer buffer temporel + * @return index correspondant + */ + public int mapToIndex(TemporalBuffer temporalBuffer){ + return mapToIndex(temporalBuffer.getSampleRate(), temporalBuffer.getOriginInstant()); + } + + /** + * Retourne le temps de l'instant dans le buffer de référence + * @return temps en ms + */ + public float toMs(){ + return timeMS; + } + + /** + * Ajoute à cet instant un autre instant /!\ Cet objet est modifié + * @param i2 instant à ajouter + * @return cet instant + */ + public Instant add(Instant i2){ + timeMS += i2.timeMS; + return this; + } + + /** + * Retire de cet instant un autre instant /!\ Cet objet est modifié + * @param i2 instant à ajouter + * @return cet instant + */ + public Instant remove(Instant i2){ + timeMS -= i2.timeMS; + return this; + } + + +} \ No newline at end of file diff --git a/src/generictools/Pair.java b/src/generictools/Pair.java new file mode 100644 index 0000000..d21d04b --- /dev/null +++ b/src/generictools/Pair.java @@ -0,0 +1,56 @@ +package generictools; + +/** + * Created by gaby on 21/03/14. + */ +public class Pair { + + private T1 v1; + private T2 v2; + + /** + * Constuit en objet pair qui contient deux variable + * @param first Première valeur + * @param second Seconde Valeur + */ + public Pair(T1 first, T2 second) { + v1 = first; + v2 = second; + } + + public Pair() { + } + + /** + * Renvoie la première valeur de la paire + * @return La premiére valeur + */ + public T1 first() { + return v1; + } + + /** + * Modifie la première valeur + * @param first La nouvelle valeur + */ + public void setFirst(T1 first) { + this.v1 = first; + } + + + /** + * Renvoie la seconde valeur de la paire + * @return La seconde valeur + */ + public T2 second() { + return v2; + } + + /** + * Modifie la seconde valeur + * @param second La seconde valeur + */ + public void setSecond(T2 second) { + this.v2 = second; + } +} \ No newline at end of file diff --git a/src/generictools/Strings.java b/src/generictools/Strings.java new file mode 100644 index 0000000..1487f70 --- /dev/null +++ b/src/generictools/Strings.java @@ -0,0 +1,24 @@ +package generictools; + +/** Created by Gabriel Augendre on 18/04/2014. + * Renvoie la chaine de caractere la plus longue parmi celles donnees en paramètre */ +public class Strings { + public static String longestString(String s1, String s2, String... sk) { + if (sk.length == 0) { + if (s1.length() > s2.length()) + return s1; + return s2; + } else { + String[] array = new String[sk.length+2]; + int max = array[0].length(); + int maxIndex = 0; + for (int i = 0; i < array.length; i++) { + if (array[i].length() > max) { + max = array[i].length(); + maxIndex = i; + } + } + return array[maxIndex]; + } + } +} diff --git a/src/generictools/WindowFunction.java b/src/generictools/WindowFunction.java new file mode 100644 index 0000000..00b73f9 --- /dev/null +++ b/src/generictools/WindowFunction.java @@ -0,0 +1,145 @@ +package generictools; + +import processing.buffer.Buffer; + +import java.util.ArrayList; + +/** + * Created by Gabriel on 08/04/2014. + * Contient les coefficients de la fenetre de pondération (masque à appliquer au signal) utilisée par la FFT. + * Générée une unique fois pour un traitement de FFT. + */ +public class WindowFunction { + + private int startID; + private final int width; + private final Type windowType; + private final Buffer temporalSignal; + private ArrayList coef; + private int divideFactor; + + // Caractérise la forme de la fenetre + public enum Type{ + RECTANGLE, + HANN, + HAMMING, + TRIANGLE, + BLACKMAN + } + + public WindowFunction(Type windowType, int width, Buffer temporalSignal){ + this.width = width; + this.windowType = windowType; + this.temporalSignal = temporalSignal; + this.coef = new ArrayList(width); + this.divideFactor = 1; + calcCoef(); + } + + /** Calcule l'ensemble des coefficients (masque à multiplier au signal) pour la largeur qui a été donnée + * quand la fenetre a été construite */ + public void calcCoef() { + final float coeffHamming = 0.53836f; + final float coeffBlackman2 = 0.076849f; + final float coeffBlackman0 = 0.42659f; + final float coeffBlackman1 = 0.49656f; + + switch (windowType){ + case RECTANGLE: + for (int i = 0; i < width; i++) { + coef.add(rectangle(width, i)); + } + break; + case HANN: + for (int i = 0; i < width; i++) { + coef.add(hann(width, i)); + } + break; + case HAMMING: + for (int i = 0; i < width; i++) { + coef.add(hamming(width, i, coeffHamming)); + } + break; + case TRIANGLE: + for (int i = 0; i < width; i++) { + coef.add(triangle(width, i)); + } + break; + case BLACKMAN: + for (int i = 0; i < width; i++) { + coef.add(blackman(width, i, coeffBlackman0, coeffBlackman1,coeffBlackman2)); + } + break; + + } + } + + + /** Pondère la valeur du buffer avec celle de la fenetre */ + public float getValue(int actualID){ + Float valueBefore = temporalSignal.get(actualID); + int id = actualID-startID; + if(valueBefore == null || id < 0 || id >= width/divideFactor ) + return 0; + + return coef.get(id*divideFactor)*valueBefore; + } + + /** Renvoie la valeur associée à la fenêtre rectangle au point actualID donné en paramètre */ + private static float rectangle(int width, int actualID) { + if (actualID >= 0 && actualID <= width) { + return 1f; + } else { + return 0f; + } + } + + /** Renvoie la valeur associée à la fenêtre Hann au point actualID donné en paramètre */ + private static float hann(int width, int actualID){ + if (actualID >= 0 && actualID <= width) { + return (0.5f*( 1 - (float)Math.cos( (2*Math.PI*(actualID)) / (width-1)) )); + } else { + return 0f; + } + } + + /** Renvoie la valeur associée à la fenêtre Hamming au point actualID donné en paramètre */ + private static float hamming(int width, int actualID, float coeff){ + + if (actualID >= 0 && actualID <= width) { + return (coeff - ((1f - coeff) * (float)Math.cos(2*Math.PI*(actualID) / (width-1)))); + } else { + return 0f; + } + } + + /** Renvoie la valeur associée à la fenêtre triangle au point actualID donné en paramètre */ + private static float triangle(int width, int actualID){ + if (actualID >= 0 && actualID <= width) { + return ( 1f - Math.abs(2 * (float) (actualID) / width) ); + } else { + return 0f; + } + } + + /** Renvoie la valeur associée à la fenêtre Blackman au point actualID donné en paramètre */ + private static float blackman(int width, int actualID, float c0, float c1, float c2){ + if (actualID >= 0 && actualID <= width) { + return (float)( c0 - c1*Math.cos(2*Math.PI*actualID / width) + c2*Math.cos(4* Math.PI*actualID / width)); + } else { + return 0f; + } + } + + public void setStartID(int index){ + this.startID = index; + } + + /** + * Permet la gestion d'un nombre de point sur lequel on calcule la FFT variable en fonction de la fréquence + * @param divideFactor le facteur de division par lequel on divise le nombre de points maximums (puissance de 2) + */ + public void setDivideFactor(int divideFactor) { + this.divideFactor = divideFactor; + } +} diff --git a/src/gui/Box.java b/src/gui/Box.java new file mode 100644 index 0000000..ce8e2c8 --- /dev/null +++ b/src/gui/Box.java @@ -0,0 +1,143 @@ +package gui; + +import generictools.Pair; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.effect.DropShadow; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; +import javafx.scene.text.Text; + +import java.util.ArrayList; + +/** + * Classe générale permettant de gérer l'affichage des Process et des Inputs dans la fenêtre de contrôle. + */ +public class Box extends Group { + + protected final static int NODE_WIDTH = ProcessSumUp.NODE_WIDTH; + protected final static int NODE_HEIGHT = ProcessSumUp.NODE_HEIGHT; + protected final static int TEXT_LENGTH = ProcessSumUp.TEXT_LENGTH; + + private final Rectangle rectangle = new Rectangle(NODE_WIDTH, NODE_HEIGHT); + protected final Text titre = new Text(); + + private final ArrayList clickableElements = new ArrayList<>(); + + private final Pair coordinates = new Pair<>(); + private final ArrayList successorList = new ArrayList<>(); + + public Box() { + this(Color.AQUA, new Text("Inconnu")); + } + + public Text getTitre() { + return titre; + } + + /** + * Renvoie les éléments dont on souhaite qu'ils affichent la fenêtre lors d'un clic. + * Il s'agit typiquement du rectangle de fond ainsi que du titre. + * @return L'arraylist des éléments + */ + public ArrayList getClickableElements() { + return new ArrayList<>(clickableElements); + } + + /** + * Constructeur + * @param C La couleur de la boîte. + * @param T Le texte à afficher. + */ + public Box(final Color C, final Text T) { + super(); + + //Initialisation de la boîte + rectangle.setFill(C); + rectangle.setArcHeight(10); + rectangle.setArcWidth(10); + + DropShadow dS = new DropShadow(); + dS.setRadius(10); + rectangle.setEffect(dS); + + + //Initialisation du texte + titre.setText(T.getText()); + titre.setFill(Color.BLACK); + titre.setFont(Font.font("Arial", 15)); + titre.setX(20); + titre.setY(24); + + //Troncature du texte si trop long + if (titre.getText().length() > TEXT_LENGTH+3) { + titre.setText(titre.getText().substring(0,TEXT_LENGTH)+"..."); + } + + this.getChildren().add(rectangle); + this.getChildren().add(titre); + + //On indique quels sont les éléments cliquables + clickableElements.add(rectangle); + clickableElements.add(titre); + } + + /** + * Ajoute un successeur à la liste des successeurs de cette box. + * @param s Le successeur à ajouter. + */ + public void addSuccessor(final ProcessBox s) { + this.successorList.add(s); + } + + /** + * Modifie les coordonnées de la boîte dans la grille. + * @param m Abscisses (colonnes). + * @param n Ordonnées (lignes). + */ + private void setCoordinates(int m, int n) { + coordinates.setFirst(m); + coordinates.setSecond(n); + } + + /** + * Génère les coordonnées du/des successeurs. + * @param c La coordonnée en abscisses de la boîte considérée. + * @param l La coordonnée en ordonnées de la boîte considérée. + * @param boxList La liste des successeurs. + */ + public void generateSuccCoord(int c, int l, ArrayList boxList) { + this.setCoordinates(c, l); + if (this instanceof InputBox) { + InputBox iBox = (InputBox) this; + boxList.add(iBox); + } + if (this instanceof ProcessBox) { + ProcessBox pBox = (ProcessBox) this; + if (!pBox.getProcess().isBoxDisplayed()) { + boxList.add(pBox); + pBox.getProcess().setBoxDisplayed(true); + } + } + for (int i = 0; i < getSuccessorList().size(); i++) { + successorList.get(i).generateSuccCoord(c+1, l+i, boxList); + } + } + + /** + * Renvoie les coordonnées de la boîte dans la grille. + * @return Une paire d'entiers représentant les coordonnées. En premier les abscisses, en second les ordonnées. + */ + public Pair getCoordinates() { + return coordinates; + } + + /** + * Renvoie la liste des successeurs. + * @return Une ArrayList de ProcessBox représentant les successeurs de la boîte considérée. + */ + public ArrayList getSuccessorList() { + return successorList; + } +} diff --git a/src/gui/ClipView.java b/src/gui/ClipView.java new file mode 100644 index 0000000..657b532 --- /dev/null +++ b/src/gui/ClipView.java @@ -0,0 +1,5 @@ +package gui; + +public interface ClipView { + public void currentFrameChanged(); +} diff --git a/src/gui/Connector.java b/src/gui/Connector.java new file mode 100644 index 0000000..77c06be --- /dev/null +++ b/src/gui/Connector.java @@ -0,0 +1,24 @@ +package gui; + +import javafx.geometry.Point2D; +import javafx.scene.shape.Line; + +/** + * Permet de connecter deux Box afin de voir les liens de successions dans les Process. + */ +public class Connector extends Line { + private Connector(double startX, double startY, double endX, double endY) { + super(startX, startY, endX, endY); + } + + /** + * Crée une ligne entre deux noeuds de la fenêtre principale. + * @param startBox La box à relier à la seconde. + * @param endBox La seconde box à relier. + */ + public Connector(Box startBox, Box endBox) { + final Point2D start = startBox.localToScene(Box.NODE_WIDTH,Box.NODE_HEIGHT/2); + final Point2D end = endBox.localToScene(0.0,Box.NODE_HEIGHT/2); + new Connector(start.getX(), start.getY(), end.getX(), end.getY()); + } +} diff --git a/src/gui/InputBox.java b/src/gui/InputBox.java new file mode 100644 index 0000000..ceead96 --- /dev/null +++ b/src/gui/InputBox.java @@ -0,0 +1,29 @@ +package gui; + +import javafx.scene.paint.Color; +import javafx.scene.text.Text; +import processing.AudioInputToBuffer; + +/** + * Définit la Box d'une Input, simple rectangle gris avec le nom de l'input. + */ +public class InputBox extends Box { + private final AudioInputToBuffer input; + + /** + * Génère la box + * @param input Le buffer d'input à associer. + */ + public InputBox(AudioInputToBuffer input) { + super(Color.WHITESMOKE, new Text(input.getName())); + this.input = input; + } + + /** + * Permet de récupérer le buffer d'input associé à la box. + * @return Le buffer d'input associé à la box. + */ + public AudioInputToBuffer getInput() { + return input; + } +} diff --git a/src/gui/MainWindow.java b/src/gui/MainWindow.java new file mode 100644 index 0000000..84bdd54 --- /dev/null +++ b/src/gui/MainWindow.java @@ -0,0 +1,441 @@ +package gui; + + +import actions.ExpertModeAction; +import actions.ImportFileAction; +import actions.QuitAction; +import actions.ShowInternalFrameAction; +import generictools.Pair; +import gui.graphs.GraphicView; +import processing.AudioInputToBuffer; +import processing.ProcessControl; +import processing.buffer.Buffer; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.beans.PropertyChangeListener; +import java.beans.PropertyVetoException; +import java.io.File; +import java.util.ArrayList; + +/** + * La fenêtre principale du programme. + * @author Gabriel Augendre <gabriel.augendre@insa-lyon.fr> + */ +class MainWindow { + + // Initialize constants + final private int INITIAL_WIDTH_NON_EXPERT = 700; + final private int INITIAL_HEIGHT_NON_EXPERT = 120; + final private int INITIAL_WIDTH_EXPERT = 1200; + final private int INITIAL_HEIGHT_EXPERT = 500; + final private boolean EXPERT_MODE_ENABLED_BY_DEFAULT = true; + + private final ExpertModeAction expertModeAction = new ExpertModeAction(EXPERT_MODE_ENABLED_BY_DEFAULT); + private final JPanel mainPanel = new JPanel(new BorderLayout(2,2)); + private final JFrame frame = new JFrame(); + private final JMenuBar menuBar = new JMenuBar(); + private final JMenu fileMenu = new JMenu("Fichier"); + private final JToolBar toolBar = new JToolBar(); + private final JMenu displayMenu = new JMenu("Affichage"); + private final JComboBox sourceList = new JComboBox<>(); + private final JButton computeButton = new JButton("Compute"); + private final JButton stopRecordButton = new JButton("Stop recording"); + private final JButton stopComputeButton = new JButton("Stop computing"); + private final JButton resetButton = new JButton("Reset"); + private final JButton enableExpertMode = new JButton(expertModeAction); + private final ImportFileAction importFileAction = new ImportFileAction(frame, sourceList); + private final JButton targetFileButton = new JButton(importFileAction); + private final JDesktopPane expertDisplay = new JDesktopPane(); + private final ArrayList> iFrameList = new ArrayList<>(); + private final TimeSlider slider = new TimeSlider(); + private final QuitAction quitAction = new QuitAction(frame); + private final JMenuItem importItem = new JMenuItem(importFileAction); + private final JMenuItem quitItem = new JMenuItem(quitAction); + + private final JMenuItem exportItem = new JMenuItem(new AbstractAction("Exporter") { + @Override + public void actionPerformed(ActionEvent e) { + ProcessControl.instance().launchPostProcess(); + } + }); + + private final WindowListener exitListener = new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + super.windowClosing(e); + int confirm = JOptionPane.showOptionDialog(null, + "Êtes-vous certain de vouloir fermer la fenêtre ?\nToutes les modifications seront perdues.", + "Confirmer la sortie", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); + if (confirm == 0) { + ProcessControl.instance().kill(); + frame.dispose(); + } + } + }; + + // Set Look and Feel (Mac OS X > Nimbus > System) + static { + try { + boolean b = true; + for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { + UIManager.setLookAndFeel(info.getClassName()); + if ("Mac OS X".equals(info.getName())) { + b = false; + break; + } + } + if (b) { + for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { + if ("Nimbus".equals(info.getName())) { + UIManager.setLookAndFeel(info.getClassName()); + break; + } + } + } + } catch (Exception d) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception f) { + f.printStackTrace(System.err); + } + } + } + + /** + * Constructeur par défaut, le seul. + */ + private MainWindow() { + + + createMainFrame(); + createExpertDisplay(); + createToolBar(); + createMenuBar(); + + createMagicWindow(); + + frame.setVisible(true); + mainPanel.repaint(); + + + //ProcessControl.instance().clockTick(); + } + + /** + * Création de la fenêtre principale. + */ + private void createMainFrame() { + frame.setTitle("Reversound"); + + // The size depends on the expert mode state + frame.setSize( + EXPERT_MODE_ENABLED_BY_DEFAULT?INITIAL_WIDTH_EXPERT:INITIAL_WIDTH_NON_EXPERT, + EXPERT_MODE_ENABLED_BY_DEFAULT?INITIAL_HEIGHT_EXPERT:INITIAL_HEIGHT_NON_EXPERT); + if (EXPERT_MODE_ENABLED_BY_DEFAULT) frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + + // Content for this frame> < + frame.setContentPane(mainPanel); + + // Do nothing on close + frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + + // A menu bar (because we need it) ;-) + frame.setJMenuBar(menuBar); + + // Resizable depends on the expert mode state + frame.setResizable(EXPERT_MODE_ENABLED_BY_DEFAULT); + + // Define what we do if expert mode is enabled or disabled + expertModeAction.addPropertyChangeListener(evt -> { + if(evt.getPropertyName().equals(ExpertModeAction.ACTIVE)){ + frame.setResizable((Boolean)evt.getNewValue()); + if (!(Boolean)evt.getNewValue()) { + frame.setSize(INITIAL_WIDTH_NON_EXPERT, INITIAL_HEIGHT_NON_EXPERT); + } else { + frame.setSize(INITIAL_WIDTH_EXPERT, INITIAL_HEIGHT_EXPERT); + } + } + }); + + //Set close operation + frame.addWindowListener(exitListener); + } + + /** + * Initialize the toolbar and source list. + */ + private void createToolBar() { + + // Put data into source list + sourceList.addItem("Micro"); + sourceList.setPrototypeDisplayValue("Fichier"); + + // Transmettre au contrôleur la source choisie. + sourceList.addItemListener(e -> { + final int INDEX = sourceList.getSelectedIndex(); + System.out.println(sourceList.getItemAt(INDEX)); + + if(INDEX == 0) + ProcessControl.instance().setInput(ProcessControl.instance().getMainInputID(), AudioInputToBuffer.fromMicro()); + else + ProcessControl.instance().setInput( + ProcessControl.instance().getMainInputID(), + AudioInputToBuffer.fromFile( + (File) (importFileAction.getValue(ImportFileAction.FILE)) + )); + }); + + //Modifie légèrement le look des boutons, surtout sur Mac OS X + computeButton.putClientProperty("JButton.buttonType", "textured"); + stopRecordButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + stopRecordButton.putClientProperty("JButton.segmentPosition", "first"); + stopComputeButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + stopComputeButton.putClientProperty("JButton.segmentPosition", "last"); + targetFileButton.putClientProperty("JButton.buttonType", "textured"); + resetButton.putClientProperty("JButton.buttonType", "textured"); + + // Add elements to toolBar + toolBar.add(computeButton); + toolBar.add(stopRecordButton); + toolBar.add(stopComputeButton); + toolBar.add(resetButton); + toolBar.add(sourceList); + toolBar.add(targetFileButton); + + // Configure le bouton reset + resetButton.setToolTipText("Réinitialise le programme à son état d'origine."); + resetButton.addActionListener(e -> reset()); + + // On n'affiche pas les boutons stop au début. + setStopButtonsState(false); + + // On relie les boutons au contrôle. + computeButton.addActionListener(e -> { + ProcessControl.instance().start(); + setStopButtonsState(true); + stopRecordButton.setText("Stop recording"); + }); + stopRecordButton.addActionListener(e -> { + if (stopRecordButton.getText().equals("Stop recording")) { + stopRecordButton.setText("Record"); + ProcessControl.instance().stopRecord(); + } else { + stopRecordButton.setText("Stop recording"); + ProcessControl.instance().start(); + } + }); + stopComputeButton.addActionListener(e -> { + ProcessControl.instance().stop(); + setStopButtonsState(false); + }); + + // Add button to enable/disable the expert mode + { + setExpertButtonText(EXPERT_MODE_ENABLED_BY_DEFAULT); + + expertModeAction.addPropertyChangeListener(evt -> { + if(!evt.getPropertyName().equals(ExpertModeAction.ACTIVE))return; + setExpertButtonText((boolean)evt.getNewValue()); + }); + + //enableExpertMode.setSelected(EXPERT_MODE_ENABLED_BY_DEFAULT); + enableExpertMode.putClientProperty("JButton.buttonType", "textured"); + + toolBar.add(enableExpertMode); + } + + // Set not draggable + toolBar.setFloatable(false); + + // Add the toolbar to the main panel + mainPanel.add(toolBar,BorderLayout.NORTH); + } + + /** + * Setup the menus and addNote all elements. + */ + private void createMenuBar() { + + // Setup the file menu + { + fileMenu.setMnemonic(KeyEvent.VK_F); + + // Add items : Import, quit + { + //importItem.setText("Importer"); //Importer, sous-menu de Fichier + importItem.setMnemonic(KeyEvent.VK_I); + fileMenu.add(importItem); + exportItem.setMnemonic(KeyEvent.VK_E); + fileMenu.add(exportItem); + quitItem.setMnemonic(KeyEvent.VK_Q); + fileMenu.add(quitItem); + } + + menuBar.add(fileMenu); + } + + // Setup the display menu + { + displayMenu.setMnemonic(KeyEvent.VK_A); + + // Add an item : Enable expert mode + { + final JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(expertModeAction); + menuItem.setSelected(EXPERT_MODE_ENABLED_BY_DEFAULT); + displayMenu.add(menuItem); + expertModeAction.addPropertyChangeListener(evt -> { + if(!evt.getPropertyName().equals(ExpertModeAction.ACTIVE))return; + menuItem.setSelected((boolean)evt.getNewValue()); + }); + } + + menuBar.add(displayMenu); + } + } + + /** + * Create the expert display. + */ + private void createExpertDisplay() { + // Hide or show the pane if mode enabled by default. + expertDisplay.setVisible(EXPERT_MODE_ENABLED_BY_DEFAULT); + + // Add the pane to the main panel + mainPanel.add(expertDisplay, BorderLayout.CENTER); + + // When the expertMode is enabled, display the panel + expertModeAction.addPropertyChangeListener(evt -> { + if(evt.getPropertyName().equals(ExpertModeAction.ACTIVE)) { + expertDisplay.setVisible((Boolean) evt.getNewValue()); + } + }); + + mainPanel.add(slider, BorderLayout.SOUTH); + } + + /** + * Display a buffer in a JInternalFrame. If the frame was previously created, show it. Else, create a new one. + * @param bufferID The ID of the buffer to be displayed. + */ + public void displayBuffer(int bufferID) { + for (Pair paire : iFrameList) { + JInternalFrame iFr = paire.first(); + int bID = paire.second(); + + if (bID == bufferID) { + if (!iFr.isVisible()) { + expertDisplay.add(iFr); + iFr.setVisible(true); + } + try { + iFr.setSelected(true); + } catch (PropertyVetoException e1) { + e1.printStackTrace(System.err); + } + return; + } + } + Buffer buffer = ProcessControl.instance().getBuffer(bufferID); + final JInternalFrame iFrame = new JInternalFrame(); + + GraphicView spectreView = GraphicView.createGraphicView(buffer,true); + addInternalFrame(iFrame, spectreView.getName(), bufferID, spectreView); + + } + + + /** + * Ajoute la fenêtre de contrôle à l'affichage Expert. + * Appelle addInternalFrame(final JInternalFrame IFRAME, final String TITLE, final GraphicView CONTENT). + */ + private void createMagicWindow() { + JInternalFrame iFrame = new JInternalFrame(); + GraphicView v = new ProcessSumUp(this); + addInternalFrame(iFrame, v.getName(), v); + } + + + /** + * Ajoute une fenêtre à l'affichage Expert et la configure selon les besoins de l'application. + * @param IFRAME La fenêtre à ajouter. + * @param TITLE Le titre de la fenêtre. + * @param BUFFER_ID L'identifiant du buffer à ajouter à la fenêtre pour l'affichage. + * @param CONTENT Le contenu de la fenêtre. + */ + private void addInternalFrame(final JInternalFrame IFRAME, final String TITLE, final int BUFFER_ID, final GraphicView CONTENT) { + IFRAME.setTitle(TITLE); + iFrameList.add(new Pair<>(IFRAME, BUFFER_ID)); + IFRAME.setVisible(true); + expertDisplay.add(IFRAME); + final int WINDOW_WIDTH = 350; + IFRAME.setBounds((iFrameList.size() - 2) * 100 + 5, 310, WINDOW_WIDTH, 300); + IFRAME.setResizable(true); + IFRAME.setClosable(true); + IFRAME.setMaximizable(true); + IFRAME.setContentPane(CONTENT); + displayMenu.add(new ShowInternalFrameAction(expertDisplay, IFRAME)); + } + + /** + * Appelle addInternalFrame avec un BUFFER_ID de -1 (pour la fenêtre de contrôle uniquement. + * @param IFRAME La fenêtre à ajouter. + * @param TITLE Le titre de la fenêtre. + * @param CONTENT Le contenu de la fenêtre. + */ + private void addInternalFrame(final JInternalFrame IFRAME, final String TITLE, final GraphicView CONTENT) { + addInternalFrame(IFRAME, TITLE, -1, CONTENT); + IFRAME.setClosable(false); + final int WINDOW_WIDTH = (int)GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds().getWidth(); + IFRAME.setBounds(5,5,WINDOW_WIDTH-10,300); + } + + /** + * Change l'état des boutons stop, compute et de la liste des sources de l'application. + * @param state False si on souhaite cacher les boutons stop, true sinon. + */ + private void setStopButtonsState(final boolean state) { + stopRecordButton.setVisible(state); + stopRecordButton.setEnabled(state); + stopComputeButton.setVisible(state); + stopComputeButton.setEnabled(state); + computeButton.setVisible(!state); + computeButton.setEnabled(!state); + sourceList.setVisible(!state); + sourceList.setEnabled(!state); + targetFileButton.setVisible(!state); + targetFileButton.setEnabled(!state); + } + + /** + * Change le texte du bouton Expert en fonction d'un booléen. + * @param b True si le mode expert est désactivé, false sinon. + */ + private void setExpertButtonText(boolean b) { + enableExpertMode.setText( + !b? + "Activer le mode expert": + "Désactiver le mode expert" + ); + } + + /** + * Réinitialise l'application à son état d'origine. + */ + private void reset() { + if (sourceList.getItemCount() > 1) + sourceList.removeItemAt(1); + sourceList.setPrototypeDisplayValue("Fichier"); + ProcessControl.instance().reset(); + setStopButtonsState(false); + } + + /** + * Enter point to run this program. + * @param args None + */ + public static void main(String[] args) { + new MainWindow(); + } +} diff --git a/src/gui/PlayerControl.java b/src/gui/PlayerControl.java new file mode 100644 index 0000000..521d043 --- /dev/null +++ b/src/gui/PlayerControl.java @@ -0,0 +1,220 @@ +package gui; + +import generictools.Instant; +import processing.BufferPlayer; +import processing.ProcessControl; +import processing.buffer.TemporalBuffer; + +import java.util.ArrayList; + +/** + * Created by Gabriel on 12/03/14. + * Controleur de lecture + * + * Il s'assure que toutes les vues soient synchronisées et est capable de démarrer, mettre en pause ou arrêter la lecture + */ + +public class PlayerControl{ + + public enum PlayingState{ + PLAYING, + PAUSED, + STOPPED + } + + private PlayingState playingState; + private Instant frame = new Instant(0); + + private final ArrayList views = new ArrayList<>(); + + private BufferPlayer player = new BufferPlayer(ProcessControl.instance().getMainInput().getBuffer()); + + static private PlayerControl pC = new PlayerControl(); + + /** + * Retourne l'instance du singleton du PlayerControl + * @return singleton du PlayerControl + */ + static public PlayerControl instance(){return pC;} + + private PlayerControl() { + + } + + /** + * Propage l'information "play" à travers toutes les vues controlées. + */ + public void play() { + player.play(); + playingState = PlayingState.PLAYING; + + emit(PlayerControlEvent.Type.PLAY_STATE); + } + + /** + * Propage l'information "play" à travers toutes les vues controlées. + * @param frame La frame depuis laquelle partir. + */ + public void play(Instant frame) { + setFrame(frame); + play(); + } + + /** + * Propage l'information "pause" à travers toutes les vues controlées. + */ + public void pause() { + player.pause(); + playingState = PlayingState.PAUSED; + + emit(PlayerControlEvent.Type.PLAY_STATE); + } + + /** + * Propage l'information "interrupt" à travers toutes les vues controlées. + */ + public void stop() { + player.pause(); + setFrame(Instant.fromIndex(0,ProcessControl.instance().getMainInput().getBuffer())); + playingState = PlayingState.STOPPED; + emit(PlayerControlEvent.Type.PLAY_STATE); + } + + /** + * Retourne l'état du player (si il est en lecture, en pause ou arrêté + * @return l'état du lecteur + */ + public PlayingState getPlayingState(){return playingState;} + + + public void mute(){ + player.setMuted(true); + } + public void unMute(){ + player.setMuted(false); + } + public boolean isMute(){ + return player.isMuted(); + } + + /** + * Bascule l'état de mute: si muted unmute, sinon mute + */ + public void toggleMute(){ + if(isMute()) + unMute(); + else + mute(); + } + + /** + * Modifie le volume du player + * @param volume 0 = mute, 1 = fullVolume, 2 = saturation permanente... + */ + public void setVolume(float volume){ + player.setVolume(volume); + } + + public float getVolume(){ + return player.getVolume(); + } + + /** + * Force la position dans le buffer de référence, l'information est propagé à toutes les vues et lecteurs controlés + * @param frame + */ + public void setFrame(Instant frame){ + player.setFrame(frame); + } + + /** + * Méthode appelée par le lecteur lorsque la position actuelle à changé + * @param frame nouvelle poition + */ + public void frameChanged(Instant frame){ + this.frame = frame; + emit(PlayerControlEvent.Type.FRAME); + } + + /** + * Renvoie la position actuelle dans le buffer de référence + * @return position actuelle + */ + public Instant getFrame(){return frame;} + + /** + * Raccourci statique de getFrame() + * @return position actuelle + */ + public static Instant frame(){return PlayerControl.instance().getFrame();} + + public void setBuffer(TemporalBuffer buffer){ + player = new BufferPlayer(buffer); + } + + + /** + * Ajoute une vue au PlayerControl. + */ + public boolean addListener(Listener listener) { + if(views.contains(listener)) + return false; + + views.add(listener); + return true; + } + + /** + * Supprime une vue au PlayerControl + * @param listener vue à supprimer + * @return Faux si la vue n'était pas ajoutée + */ + public boolean removeListener(Listener listener){ + if(!views.contains(listener)) + return false; + + views.remove(listener); + return true; + } + + private void emit(PlayerControlEvent.Type type){ + PlayerControlEvent e = new PlayerControlEvent(type); + for(Listener l: views) + l.playerControlEvent(e); + } + + + /** + * Interface définissant les objets capable de recevoir les évenements de lecture + */ + public interface Listener{ + /** + * Traite l'évenement de lecture + * @param e évenement + */ + public default void playerControlEvent(PlayerControlEvent e) { + switch (e.getType()){ + case FRAME: + break; + + case PLAY_STATE: + switch (e.getPlayingState()){ + case PLAYING: + break; + case PAUSED: + break; + case STOPPED: + break; + } + break; + + case VOLUME: + break; + + case MUTE_STATE: + break; + } + } + } + +} diff --git a/src/gui/PlayerControlEvent.java b/src/gui/PlayerControlEvent.java new file mode 100644 index 0000000..42c97c4 --- /dev/null +++ b/src/gui/PlayerControlEvent.java @@ -0,0 +1,57 @@ +package gui; + +import generictools.Instant; + +/** + * Created by gaby on 08/05/14. + */ +public class PlayerControlEvent { + public enum Type{ + FRAME, + PLAY_STATE, + VOLUME, + MUTE_STATE + } + private final Type type; + + private final Instant frame; + private final PlayerControl.PlayingState playingState; + private final float volume; + private final boolean isMuted; + + public PlayerControlEvent(Type type, Instant frame, PlayerControl.PlayingState playingState, float volume, boolean isMuted) { + this.type = type; + this.frame = frame; + this.playingState = playingState; + this.volume = volume; + this.isMuted = isMuted; + } + public PlayerControlEvent(Type type){ + this.type = type; + frame = PlayerControl.instance().getFrame(); + playingState = PlayerControl.instance().getPlayingState(); + volume = PlayerControl.instance().getVolume(); + isMuted = PlayerControl.instance().isMute(); + } + + + public Type getType() { + return type; + } + + public Instant getFrame() { + return frame; + } + + public PlayerControl.PlayingState getPlayingState() { + return playingState; + } + + public float getVolume() { + return volume; + } + + public boolean isMuted() { + return isMuted; + } +} diff --git a/src/gui/ProcessBox.java b/src/gui/ProcessBox.java new file mode 100644 index 0000000..86bd461 --- /dev/null +++ b/src/gui/ProcessBox.java @@ -0,0 +1,190 @@ +package gui; + +import javafx.scene.control.Button; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.GridPane; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; +import processing.processes.Process; +import processing.processes.ProcessEvent; + +/** + * Définit la box d'un process qui aura des contrôles particuliers et différents d'une InputBox + */ +public class ProcessBox extends Box implements Process.Listener { + private final Process process; + private final Button startStopButton = new Button(); + private final ProgressIndicator spinningWheel = new ProgressIndicator(0); + private final ProgressBar progressBar = new ProgressBar(0); + private final Button recomputeButton = new Button(); + + private final String playChar = "▶"; + private final String pauseChar = "||"; + private final String resetChar = "↻"; + + private boolean pauseState = false; + private boolean waitingState = false; + private boolean progressState = false; + private boolean nameState = false; + private boolean interruptState = false; + + private GridPane gridPane = new GridPane(); + private ScrollPane scrollPane = new ScrollPane(gridPane); + + /** + * Constructeur. + * @param process Le processus à associer à la box. + */ + public ProcessBox(Process process) { + super(Color.LIGHTYELLOW, new Text(process.getName())); + this.process = process; + + super.titre.setX(70); + + //On s'occupe d'abord du bouton de play/pause du process + startStopButton.setText(playChar); + this.getChildren().add(startStopButton); + startStopButton.setTranslateX(5); + startStopButton.setTranslateY(5); + startStopButton.setPrefWidth(25); + startStopButton.setMaxWidth(25); + startStopButton.setOnAction(actionEvent -> { + if (startStopButton.getText().equals(playChar)) { + process.start(); + startStopButton.setText(pauseChar); + } + else { + process.pause(); + startStopButton.setText(playChar); + } + }); + process.addListener(this); + + recomputeButton.setText(resetChar); + this.getChildren().add(recomputeButton); + recomputeButton.setTranslateX(35); + recomputeButton.setTranslateY(5); + recomputeButton.setPrefWidth(25); + recomputeButton.setMaxWidth(25); + recomputeButton.setOnAction(actionEvent -> process.recomputeAll()); + + //Ensuite de la roue qui tourne + this.getChildren().add(spinningWheel); + spinningWheel.setTranslateY(7); + spinningWheel.setTranslateX(Box.NODE_WIDTH - 50); + spinningWheel.setMaxHeight(45); + spinningWheel.getStylesheets().add(ProcessBox.class.getResource("ProgressIndicator.css").toExternalForm()); + + //Et enfin de la progress bar + this.getChildren().add(progressBar); + progressBar.setTranslateY(40); + progressBar.setTranslateX(5); + progressBar.setPrefWidth(Box.NODE_WIDTH - 10); + progressBar.getStylesheets().add(ProcessBox.class.getResource("ProgressBar.css").toExternalForm()); + + this.getChildren().add(scrollPane); + scrollPane.setTranslateX(5); + scrollPane.setTranslateY(50); + scrollPane.setPrefSize(Box.NODE_WIDTH-10, Box.NODE_HEIGHT-55); + + gridPane.setPrefWidth(Box.NODE_WIDTH-40); + gridPane.setTranslateX(5); + gridPane.setTranslateY(5); + gridPane.setVgap(2); + gridPane.setHgap(5); + + for (int i = 0; i < process.propertyManager().getNbProperties(); i++) { + //On ajoute le nom de la propriété à la colonne 0 ligne i + gridPane.add(new Text(process.propertyManager().getProperty(i).getName()), 0, i); + //Et le node correspondant à la colonne 1 ligne i + gridPane.add(process.propertyManager().getProperty(i).createEditNode(), 1, i); + } + } + + /** + * Renvoie un objet Process. + * @return Le process associé à la box. + */ + public Process getProcess() { + return process; + } + + + @Override + public void processEvent(ProcessEvent event) { + switch (event.getType()) { + case PAUSED_STATE_CHANGED: + pauseState = true; + break; + case WAITING_STATE: + waitingState = true; + break; + case PROGRESS_CHANGED: + progressState = true; + break; + case NAME_CHANGED: + nameState = true; + break; + case INTERRUPT_AFTER: + interruptState = true; + break; + } + } + + /** + * Change l'état de la roue d'indication de travail en cours. + * @param b true si la roue doit tourner, false sinon + */ + private void setSpinState(boolean b) { + if (b) { + spinningWheel.setProgress(0); + spinningWheel.setMaxHeight(45); + } + else { + spinningWheel.setProgress(-1); + spinningWheel.setMaxHeight(25); + } + } + + private void setProgress(double d) { + progressBar.setProgress(d); + } + + private void setName(String s) {this.getTitre().setText(s);} + + private void setButton(boolean b) { + if (b) { + startStopButton.setText(playChar); + } else { + startStopButton.setText(pauseChar); + } + } + + /** + * Met à jour les différents états en fonction des événements qui ont déjà eu lieu. + * Permet de différer la mise à jour et de ne pas la faire à chaque nouvel événement. + */ + public void update() { + if (pauseState) { + pauseState = false; + setButton(process.isPaused() || !process.isRunning()); + } + if (waitingState) { + waitingState = false; + setSpinState(process.isWaiting()); + } + if (progressState) { + progressState = false; + setProgress(process.getProgress()); + } + if (nameState) { + nameState = false; + setName(process.getName()); + } + if (interruptState) { + interruptState = false; + } + } +} diff --git a/src/gui/ProcessSumUp.java b/src/gui/ProcessSumUp.java new file mode 100644 index 0000000..fd755d3 --- /dev/null +++ b/src/gui/ProcessSumUp.java @@ -0,0 +1,167 @@ +package gui; + +import gui.graphs.GraphicView; +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.ScrollPane; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import processing.AudioInputToBuffer; +import processing.ProcessControl; +import processing.processes.Process; + +import java.util.ArrayList; + +/** + * Stocke les inputs et les process pour les afficher dans une fenêtre et permettre leur contrôle. + */ +public class ProcessSumUp extends GraphicView { + + //TODO: Contrôler les propriétés des traitements (right clic) + + private final MainWindow mW; + + protected static final int NODE_WIDTH = 300; + protected static final int NODE_HEIGHT = 200; + protected static final int TEXT_LENGTH = NODE_WIDTH/10-9; + + private final StackPane group = new StackPane(); + private final ScrollPane scrollPane = new ScrollPane(group); + private final GridPane grid = new GridPane(); + private final Text frameRateLabel = new Text(10,10,""); + + private final ArrayList boxList = new ArrayList<>(); + + + /** + * Constructeur. + * @param mainWindow L'instance de fenêtre principale du programme. + */ + public ProcessSumUp(MainWindow mainWindow) { + super("Fenêtre de contrôle"); + initView(); + ProcessControl.instance().setProcessesView(this); + mW = mainWindow; + } + + /** + * Initialisation de la vue. + */ + public void initView() { + Platform.runLater(() -> { + setScene(new Scene(scrollPane, Color.WHITE)); + frameRateLabel.setFont(new Font("Candara", 5)); + frameRateLabel.setFill(Color.AZURE); + }); + } + + /** + * Mise à jour de la vue. + */ + public void updateView(){ + Platform.runLater(this::generateGraph); + } + + private void generateGraph() { + grid.getChildren().clear(); + group.getChildren().clear(); + group.getChildren().add(grid); + + + for (int i = 0; i < ProcessControl.instance().getInputNbr(); i++) { + final AudioInputToBuffer input = ProcessControl.instance().getInput(i); + final InputBox inputNode = createInputNode(input); + + emptyBoxList(); + + setSuccessors(inputNode); + inputNode.generateSuccCoord(0, i, boxList); + for (int j = 0; j < boxList.size(); j++) { + Box b = boxList.get(j); + grid.add(b, b.getCoordinates().first(), b.getCoordinates().second()); + /*if (j < boxList.size()-1) { + group.getChildren().addNote(new Connector(b, boxList.get(j+1))); + group.getChildren().addNote(new Line(Box.NODE_WIDTH, Box.NODE_HEIGHT/2, Box.NODE_WIDTH+20, Box.NODE_WIDTH/2)); + }*/ + } + } + + + group.getChildren().add(frameRateLabel); + + } + + /** + * Mise à jour du contenu des noeuds du panneau de contrôle. + * @param frameRate Le taux de rafraîchissement. + */ + public void update(Float frameRate){ + Platform.runLater(() -> { + frameRateLabel.setText(frameRate.toString() + " fps"); + boxList.stream().filter(b -> b instanceof ProcessBox).forEach(b -> { + ProcessBox pb = (ProcessBox) b; + pb.update(); + }); + }); + } + + private void emptyBoxList() { + boxList.stream().filter(b -> b instanceof ProcessBox).forEach(b -> { + ProcessBox pb = (ProcessBox) b; + pb.getProcess().setBoxDisplayed(false); + }); + boxList.clear(); + } + + private void setSuccessors(InputBox box) { + for (Process p : box.getInput().getBuffer().getUsedBy()) { + setSuccessors(p, box); + } + } + private void setSuccessors(ProcessBox box) { + for (Process p : box.getProcess().getOutput().getUsedBy()) { + setSuccessors(p, box); + } + } + + private void setSuccessors(Process p, Box box) { + ProcessBox processBox = createProcessNode(p); + box.addSuccessor(processBox); + setSuccessors(processBox); + } + + private InputBox createInputNode(final AudioInputToBuffer input){ + InputBox g = new InputBox(input); + + for (Node n : g.getClickableElements()) { + n.addEventFilter(MouseEvent.MOUSE_CLICKED, + e -> { + mW.displayBuffer(ProcessControl.instance().getBufferIndex(input.getBuffer())); + e.consume(); + } + ); + } + + return g; + } + + private ProcessBox createProcessNode(final Process process){ + ProcessBox g = new ProcessBox(process); + + for (Node n : g.getClickableElements()) { + n.addEventFilter(MouseEvent.MOUSE_CLICKED, + e -> { + mW.displayBuffer(ProcessControl.instance().getBufferIndex(process.getOutput())); + e.consume(); + } + ); + } + + return g; + } +} diff --git a/src/gui/ProgressBar.css b/src/gui/ProgressBar.css new file mode 100644 index 0000000..628282c --- /dev/null +++ b/src/gui/ProgressBar.css @@ -0,0 +1 @@ +.progress-bar .bar {-fx-padding:3px; -fx-background-insets:0;} \ No newline at end of file diff --git a/src/gui/ProgressIndicator.css b/src/gui/ProgressIndicator.css new file mode 100644 index 0000000..ffd968d --- /dev/null +++ b/src/gui/ProgressIndicator.css @@ -0,0 +1 @@ +.progress-indicator .percentage {-fx-font-size: 0.01em;} \ No newline at end of file diff --git a/src/gui/TimeSlider.java b/src/gui/TimeSlider.java new file mode 100644 index 0000000..eb392fb --- /dev/null +++ b/src/gui/TimeSlider.java @@ -0,0 +1,162 @@ +package gui; + +import generictools.Instant; +import gui.viewer_state.BufferViewerState; +import gui.viewer_state.Viewer; +import processing.ProcessControl; +import processing.buffer.TemporalBuffer; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +/** + * Un panel pour le contrôle du volum et du temps. + */ +public class TimeSlider extends JPanel implements PlayerControl.Listener, Viewer { + + private boolean sliderUnlocked = false; + private final PlayerControl control = PlayerControl.instance(); + + private final JSlider slider; + private final JLabel posLabel; + private final JButton playPause; + private final JButton volUp = new JButton("+"); + private final JButton volDown = new JButton("-"); + private final JButton mute = new JButton("Mute"); + + /** + * Génère le panel. + */ + public TimeSlider(){ + super(); + + BoxLayout layout = new BoxLayout(this, BoxLayout.LINE_AXIS); + this.setLayout(layout); + + playPause = new JButton("Play"); + add(playPause); + + add(volDown); + volDown.addActionListener(e -> PlayerControl.instance().setVolume(PlayerControl.instance().getVolume() - 0.1f)); + + add(volUp); + volUp.addActionListener(e -> PlayerControl.instance().setVolume(PlayerControl.instance().getVolume() + 0.1f)); + + add(mute); + mute.addActionListener(e -> { + if (PlayerControl.instance().isMute()) { + PlayerControl.instance().unMute(); + mute.setText("Mute"); + } + else { + PlayerControl.instance().mute(); + mute.setText("Unmute"); + } + }); + + + slider = new JSlider(0,1,0); + slider.setPreferredSize(new Dimension(300,25)); + add(slider); + + posLabel = new JLabel(); + posLabel.setBorder(BorderFactory.createLoweredBevelBorder()); + posLabel.setPreferredSize(new Dimension(120,25)); + add(posLabel); + + + ProcessControl p = ProcessControl.instance(); + BufferViewerState.sizeSensible(this, p.getMainInput().getBuffer()).setVisibility(true); + + control.addListener(this); + slider.addChangeListener(e -> sliderMoved()); + slider.addMouseListener(new MouseListener() { + @Override + public void mouseClicked(MouseEvent e) {} + + @Override + public void mousePressed(MouseEvent e) { + sliderUnlocked = true; + } + + @Override + public void mouseReleased(MouseEvent e) { + sliderUnlocked = false; + } + + @Override + public void mouseEntered(MouseEvent e) {} + + @Override + public void mouseExited(MouseEvent e) {} + }); + + playPause.addActionListener(e -> { + if (playPause.getText().equals("Pause")) + control.pause(); + else + control.play(); + }); + + } + + private void sliderMoved() { + if(sliderUnlocked) { + control.setFrame(Instant.fromIndex(slider.getValue(), ProcessControl.instance().getMainInput().getBuffer())); + } + } + + + @Override + public void playerControlEvent(PlayerControlEvent e) { + switch (e.getType()){ + case FRAME: + TemporalBuffer b = ProcessControl.instance().getMainInput().getBuffer(); + if(!sliderUnlocked) + slider.setValue(e.getFrame().mapToIndex(b)); + + posLabel.setText(e.getFrame() + " / "+ Instant.fromIndex(ProcessControl.instance().getMainInput().getBuffer().size(),44100)); + break; + + case PLAY_STATE: + switch (e.getPlayingState()){ + case PLAYING: + playPause.setText("Pause"); + break; + case PAUSED: + playPause.setText("Play"); + break; + case STOPPED: + playPause.setText("Play"); + break; + } + break; + + case MUTE_STATE: + if(e.isMuted()) + mute.setText("Unmute"); + else + mute.setText("Mute"); + break; + } } + + @Override + public void updateView() { + slider.setMaximum(ProcessControl.instance().getMainInput().getBuffer().size()); + posLabel.setText(posLabel.getText().substring(0, posLabel.getText().indexOf("/")+1)+ Instant.fromIndex(ProcessControl.instance().getMainInput().getBuffer().size(), 44100)); + + if(!slider.isEnabled()) { + slider.setEnabled(true); + PlayerControl.instance().setFrame(Instant.fromTime(0)); + } + } + + @Override + public void cleanView() { + slider.setMaximum(1); + slider.setEnabled(false); + posLabel.setText(" -- / --"); + } +} diff --git a/src/gui/graphs/AbstractBufferGraph.java b/src/gui/graphs/AbstractBufferGraph.java new file mode 100644 index 0000000..8535d86 --- /dev/null +++ b/src/gui/graphs/AbstractBufferGraph.java @@ -0,0 +1,65 @@ +package gui.graphs; + +import conteneurs.ObservableObject; +import gui.PlayerControl; +import gui.viewer_state.BufferViewerState; +import gui.viewer_state.Viewer; +import javafx.event.EventHandler; +import javafx.scene.input.ContextMenuEvent; +import processing.buffer.Buffer; +import processing.buffer.TemporalBuffer; + +/** + * Created by gaby on 08/05/14. + * Classe abstraite définissant les attributs et méthode communes au graph de buffer + */ +public abstract class AbstractBufferGraph extends AbstractGraph implements Viewer, PlayerControl.Listener{ + private T buffer; + private BufferViewerState state; + + public AbstractBufferGraph(String name, Class type) { + super(name, type); + } + + @Override + public AbstractGraph setupGraph(Buffer b, GraphicView graphicView, boolean syncWithPlayer){ + AbstractBufferGraph g = getGenericGraph(); + g.setGraphicView(graphicView); + g.setBuffer(b); + + if(syncWithPlayer) { + PlayerControl.instance().addListener(g); + } + + g.setupView(); + g.setBufferViewerState(BufferViewerState.continousWindow(g, b)); + return g; + } + + @Override + public AbstractGraph setupGraph(ObservableObject o, GraphicView graphicView) { + return null; + } + + /** + * Vérifie que le graph est valide (qu'il n'a pas été créé via un contructeur) + * @return Vrai si valide + */ + @Override + public boolean isValid() { + return buffer!=null; + } + + @Override + protected void visibilityChange(boolean isVisible) { + state.setVisibility(isVisible); + } + + abstract protected AbstractBufferGraph getGenericGraph(); + + private void setBuffer(T buffer){this.buffer = buffer;} + private void setBufferViewerState(BufferViewerState b){state = b;} + + public T buffer(){return buffer;} + public BufferViewerState state(){return state;} +} diff --git a/src/gui/graphs/AbstractGraph.java b/src/gui/graphs/AbstractGraph.java new file mode 100644 index 0000000..b40b332 --- /dev/null +++ b/src/gui/graphs/AbstractGraph.java @@ -0,0 +1,98 @@ +package gui.graphs; + +import conteneurs.ObservableObject; +import javafx.application.Platform; +import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.input.ContextMenuEvent; +import processing.buffer.Buffer; + +import javax.swing.*; +import java.util.ArrayList; + +/** + * Created by gaby on 07/05/14. + * Classe abstraite définissant les attributs et méthode communes au graph + */ +public abstract class AbstractGraph { + + protected final String name; + private final Class type; + private GraphicView graphicView; + private boolean visibility = false; + + protected AbstractGraph(String name, Class type) { + this.name = name; + this.type = type; + } + + /** + * Doit renvoyer le noeud JavaFX élémentaire du graph + * @return Le noeud parent + */ + public abstract Parent getMainNode(); + + /** + * Revoie le liste des éléments du menu contextuel + * @return + */ + public void createContextMenu(JPopupMenu contextMenu){ + try { + contextMenu.remove(1); + }catch (IllegalArgumentException e){ + + } + } + + /** + * Vérifie que le graph est valide (qu'il n'a pas été construit via un contructeur) + * @return Vrai si valide + */ + public abstract boolean isValid(); + + public void setVisibility(boolean isVisible){ + visibilityChange(isVisible); + visibility = isVisible; + } + + public boolean visibility(){ + return this.visibility; + } + + protected abstract void visibilityChange(boolean isVisible); + + protected void setupView(){ + AbstractGraph thisGraph = this; + Platform.runLater(new Runnable() { + @Override + public void run() { + initView(); + +// getMainNode().setOnContextMenuRequested(new EventHandler() { +// @Override +// public void handle(ContextMenuEvent e) { +// graphicView.popupContextMenu(Math.round((float)e.getX()),Math.round((float)e.getY()),thisGraph); +// } +// }); + } + }); + } + protected abstract void initView(); + + abstract public AbstractGraph setupGraph(Buffer b, GraphicView graphicView, boolean syncWithPlayer); + abstract public AbstractGraph setupGraph(ObservableObject o, GraphicView graphicView); + + public String getName() { + return name; + } + + public Class getType() { + return type; + } + + public GraphicView getGraphicView() { + return graphicView; + } + protected void setGraphicView(GraphicView graphicView){this.graphicView = graphicView;} +} diff --git a/src/gui/graphs/AbstractObservableGraph.java b/src/gui/graphs/AbstractObservableGraph.java new file mode 100644 index 0000000..4feb4ab --- /dev/null +++ b/src/gui/graphs/AbstractObservableGraph.java @@ -0,0 +1,136 @@ +package gui.graphs; + +import conteneurs.ObservableObject; +import gui.PlayerControl; +import gui.PlayerControlEvent; +import gui.viewer_state.BufferViewerState; +import gui.viewer_state.ObservableViewerState; +import gui.viewer_state.Viewer; +import gui.viewer_state.ViewerState; +import javafx.event.EventHandler; +import javafx.scene.input.ContextMenuEvent; +import processing.buffer.Buffer; +import processing.buffer.TemporalBuffer; + +/** + * Created by gaby on 09/05/14. + * Classe abstraite définissant les attributs et méthode communes au graph d'objet observable + */ +public abstract class AbstractObservableGraph extends AbstractGraph implements Viewer, PlayerControl.Listener { + + private T toShow=null; + private Buffer buffer = null; + private int currentID = -1; + private ViewerState state; + + protected AbstractObservableGraph(String name, Class type) { + super(name, type); + } + + @Override + public AbstractGraph setupGraph(Buffer buffer, GraphicView graphicView, boolean syncWithPlayer) { + /*Class[] classes = b.getType().getClasses(); + for (int i = 0; i < classes.length; i++) { + if(classes[i] == ObservableObject.class) + break; + else if(i == classes.length-1) + return null; + }*/ + + AbstractObservableGraph g = getGenericGraph(); + g.setGraphicView(graphicView); + g.setBuffer(buffer); + + + if(syncWithPlayer) { + /*if(buffer instanceof TemporalBuffer) + PlayerControl.instance().addListener(g); + else + return null; + /*/ + PlayerControl.instance().addListener(g); + //*/ + } + + + g.setupView(); + + + g.setState(BufferViewerState.singleFrameWindow(g,buffer)); + return g; + } + + @Override + public AbstractGraph setupGraph(ObservableObject o, GraphicView graphicView) { + AbstractObservableGraph g = getGenericGraph(); + g.setGraphicView(graphicView); + g.setState(new ObservableViewerState(g,o)); + g.setToShow(o); + + g.setupView(); + return g; + } + + @Override + public boolean isValid() { + return buffer!=null||toShow!=null; + } + + @Override + protected void visibilityChange(boolean isVisible) { + state.setVisibility(isVisible); + } + + @Override + public void playerControlEvent(PlayerControlEvent e) { + switch (e.getType()){ + case FRAME: + int id = e.getFrame().mapToIndex((TemporalBuffer)buffer); + currentID = id; + ((BufferViewerState)state).setSingleFrame(id); + break; + } + } + + @Override + public void updateView() { + if(buffer != null) { + setToShow(buffer.get(currentID)); + } + + if(toShow!=null) + updateViewer(); + } + + protected abstract AbstractObservableGraph getGenericGraph(); + + public T toShow() { + return toShow; + } + + public void setToShow(T toShow) { + this.toShow = toShow; + + if(state instanceof ObservableViewerState) + ((ObservableViewerState)state).setObservableObject(toShow); + } + + public Buffer buffer() { + return buffer; + } + + private void setBuffer(Buffer buffer) { + this.buffer = buffer; + } + + + public ViewerState state() { + return state; + } + + private void setState(ViewerState state) { + this.state = state; + } + + protected abstract void updateViewer(); +} diff --git a/src/gui/graphs/AmpliFreqView.java b/src/gui/graphs/AmpliFreqView.java new file mode 100644 index 0000000..d960ca5 --- /dev/null +++ b/src/gui/graphs/AmpliFreqView.java @@ -0,0 +1,195 @@ +package gui.graphs; + +import conteneurs.*; +import javafx.application.Platform; +import javafx.collections.*;import javafx.collections.ObservableList;import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.chart.BarChart; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.text.Font; + +import javax.swing.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener;import java.util.ArrayList; + +/** + * Created by gaby on 01/06/14. + */ +public abstract class AmpliFreqView extends AbstractObservableGraph{ + + protected BarChart currentBarChart; + private ArrayList categories = new ArrayList<>(); + private Boolean ampliInDb = false; + + public enum Type{ + REGULAR_FREQ, + NOTES + } + + AmpliFreqView(String name, Class type) { + super( name, type); + } + + private void createContent() { + + // Définition des axes du graphe + CategoryAxis xAxis = new CategoryAxis(); + NumberAxis yAxis = new NumberAxis(0, 1, 1); + currentBarChart = new BarChart<>(xAxis,yAxis ); + + + createXAxis(); + + + // Définition de certaines propriétés graphiques du graphe + currentBarChart.getStylesheets().add(SpectreView.class.getResource("AudioBarChart.css").toExternalForm()); + currentBarChart.setLegendVisible(false); + currentBarChart.setAnimated(false); + currentBarChart.setBarGap(0); + currentBarChart.setCategoryGap(0); + currentBarChart.setVerticalGridLinesVisible(false); + + + // Initialisation du graphe + xAxis.setTickLabelFont(new Font(10)); + yAxis.setLabel("Amplitude"); + yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis, null, "")); + } + + /** + * Doit renvoyer le noeud JavaFX élémentaire du graph + * + * @return Le noeud parent + */ + @Override + public Parent getMainNode() { + return currentBarChart; + } + + /** + * Revoie le liste des éléments du menu contextuel + * + * @param contextMenu + */ + @Override + public void createContextMenu(JPopupMenu contextMenu) { + JMenu choixDB = new JMenu("Axe amplitude"); + JRadioButtonMenuItem linAmpli = new JRadioButtonMenuItem("Amplitude linéaire",!ampliInDb); + linAmpli.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if(e.getStateChange()==ItemEvent.SELECTED) { + ampliInDb = false; + state().invalidate(); + } + } + }); + JRadioButtonMenuItem dBAmpli = new JRadioButtonMenuItem("Amplitude en déciBel",ampliInDb); + dBAmpli.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if(e.getStateChange()==ItemEvent.SELECTED){ + ampliInDb = true; + ((NumberAxis)currentBarChart.getYAxis()).setUpperBound(1); + state().invalidate(); + } + } + }); + ButtonGroup b2 = new ButtonGroup(); + b2.add(linAmpli); + choixDB.add(linAmpli); + b2.add(dBAmpli); + choixDB.add(dBAmpli); + + contextMenu.add(choixDB); + } + + @Override + public void updateViewer() { + Platform.runLater( + new Runnable() { + @Override + public void run() { + + currentBarChart.setTitle(""); + if (toShow() == null) { + cleanView(); + return; + } + //s = SpectreFilterFactory.hearingCorrection().setIntensity(0).filterSpectre(s); + float yMax = 0f; + + + for (int i = 0; i < categories.size(); i++) { + + float ampli = getAmplitude(i); + + if(ampliInDb) + ampli = Spectre.ampliTodB(ampli); + + currentBarChart.getData().get(0).getData().get(i).setYValue(ampli); + if (ampli > yMax) { + yMax = ampli; + } + } + + if (yMax > Float.MAX_VALUE) { + cleanView(); + currentBarChart.setTitle("Spectre incorrect"); + return; + } + + NumberAxis yAxis = ((NumberAxis) currentBarChart.getYAxis()); + // Changement du max d'axe Y si le nouveau max est plus grand que l'ancien. + yMax = yMax > yAxis.getUpperBound() ? yMax * 1.01f : (float) yAxis.getUpperBound() * 0.995f; + + yAxis.setTickUnit(Math.pow(10, (int) (Math.log10(yMax) - .2))); + yAxis.setUpperBound(yMax); + } + } + ); + } + + + @Override + public void cleanView() { + Platform.runLater(new Runnable() { + @Override + public void run() { + currentBarChart.setTitle("Spectre indisponible"); + } + }); + + } + + public void setXAxisCategories(ArrayList categories){ + this.categories = categories; + Platform.runLater(new Runnable(){ + @Override + public void run() { + createXAxis(); + state().invalidate(); + } + }); + } + + private void createXAxis(){ + + if(currentBarChart.getData().isEmpty()) + currentBarChart.getData().add(new XYChart.Series<>()); + else + currentBarChart.getData().get(0).getData().clear(); + + for (int i = 0; i < categories.size(); i++) + currentBarChart.getData().get(0).getData().add(new XYChart.Data<>(categories.get(i), 0)); + } + + protected abstract float getAmplitude(int idCategory); + + + protected void initView() { + createContent(); + } +} diff --git a/src/gui/graphs/AmpliView.css b/src/gui/graphs/AmpliView.css new file mode 100644 index 0000000..bf7e51b --- /dev/null +++ b/src/gui/graphs/AmpliView.css @@ -0,0 +1,7 @@ +.thick-chart .chart-series-line { + -fx-stroke-width: 1px; +} +.thick-chart .chart-series-line-fat { + -fx-stroke-width: 2px; +} +.default-color0.chart-series-line { -fx-stroke: #0b1b64; } \ No newline at end of file diff --git a/src/gui/graphs/AmpliView.java b/src/gui/graphs/AmpliView.java new file mode 100644 index 0000000..8fe2f18 --- /dev/null +++ b/src/gui/graphs/AmpliView.java @@ -0,0 +1,604 @@ +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); } +} + diff --git a/src/gui/graphs/AudioBarChart.css b/src/gui/graphs/AudioBarChart.css new file mode 100644 index 0000000..d4c171a --- /dev/null +++ b/src/gui/graphs/AudioBarChart.css @@ -0,0 +1 @@ +.chart-bar { -fx-background-color: #69de01; -fx-background-insets: 0; -fx-background-radius: 0; } /* .data0.chart-bar { -fx-background-color: #69de01; } .data1.chart-bar { -fx-background-color: #69de01; } .data2.chart-bar { -fx-background-color: #69de01; } .data3.chart-bar { -fx-background-color: #69de01; } .data4.chart-bar { -fx-background-color: #69de01; } .data5.chart-bar { -fx-background-color: #69de01; } .data6.chart-bar { -fx-background-color: #69de01; } .data7.chart-bar { -fx-background-color: #69de01; } .data8.chart-bar { -fx-background-color: #75e101; } .data9.chart-bar { -fx-background-color: #75e101; } .data10.chart-bar { -fx-background-color: #75e101; } .data11.chart-bar { -fx-background-color: #75e101; } .data12.chart-bar { -fx-background-color: #75e101; } .data13.chart-bar { -fx-background-color: #75e101; } .data14.chart-bar { -fx-background-color: #75e101; } .data15.chart-bar { -fx-background-color: #75e101; } .data16.chart-bar { -fx-background-color: #86e701; } .data17.chart-bar { -fx-background-color: #86e701; } .data18.chart-bar { -fx-background-color: #86e701; } .data19.chart-bar { -fx-background-color: #86e701; } .data20.chart-bar { -fx-background-color: #86e701; } .data21.chart-bar { -fx-background-color: #86e701; } .data22.chart-bar { -fx-background-color: #86e701; } .data23.chart-bar { -fx-background-color: #86e701; } .data24.chart-bar { -fx-background-color: #9aee01; } .data25.chart-bar { -fx-background-color: #9aee01; } .data26.chart-bar { -fx-background-color: #9aee01; } .data27.chart-bar { -fx-background-color: #9aee01; } .data28.chart-bar { -fx-background-color: #9aee01; } .data29.chart-bar { -fx-background-color: #9aee01; } .data30.chart-bar { -fx-background-color: #9aee01; } .data31.chart-bar { -fx-background-color: #9aee01; } .data32.chart-bar { -fx-background-color: #b0f000; } .data33.chart-bar { -fx-background-color: #b0f000; } .data34.chart-bar { -fx-background-color: #b0f000; } .data35.chart-bar { -fx-background-color: #b0f000; } .data36.chart-bar { -fx-background-color: #b0f000; } .data37.chart-bar { -fx-background-color: #b0f000; } .data38.chart-bar { -fx-background-color: #b0f000; } .data39.chart-bar { -fx-background-color: #b0f000; } .data40.chart-bar { -fx-background-color: #c6f000; } .data41.chart-bar { -fx-background-color: #c6f000; } .data42.chart-bar { -fx-background-color: #c6f000; } .data43.chart-bar { -fx-background-color: #c6f000; } .data44.chart-bar { -fx-background-color: #c6f000; } .data45.chart-bar { -fx-background-color: #c6f000; } .data46.chart-bar { -fx-background-color: #c6f000; } .data47.chart-bar { -fx-background-color: #c6f000; } .data48.chart-bar { -fx-background-color: #dbf000; } .data49.chart-bar { -fx-background-color: #dbf000; } .data50.chart-bar { -fx-background-color: #dbf000; } .data51.chart-bar { -fx-background-color: #dbf000; } .data52.chart-bar { -fx-background-color: #dbf000; } .data53.chart-bar { -fx-background-color: #dbf000; } .data54.chart-bar { -fx-background-color: #dbf000; } .data55.chart-bar { -fx-background-color: #dbf000; } .data56.chart-bar { -fx-background-color: #edf000; } .data57.chart-bar { -fx-background-color: #edf000; } .data58.chart-bar { -fx-background-color: #edf000; } .data59.chart-bar { -fx-background-color: #edf000; } .data60.chart-bar { -fx-background-color: #edf000; } .data61.chart-bar { -fx-background-color: #edf000; } .data62.chart-bar { -fx-background-color: #edf000; } .data63.chart-bar { -fx-background-color: #edf000; } .data64.chart-bar { -fx-background-color: #fbf002; } .data65.chart-bar { -fx-background-color: #fbf002; } .data66.chart-bar { -fx-background-color: #fbf002; } .data67.chart-bar { -fx-background-color: #fbf002; } .data68.chart-bar { -fx-background-color: #fbf002; } .data69.chart-bar { -fx-background-color: #fbf002; } .data70.chart-bar { -fx-background-color: #fbf002; } .data71.chart-bar { -fx-background-color: #fbf002; } .data72.chart-bar { -fx-background-color: #ffe500; } .data73.chart-bar { -fx-background-color: #ffe500; } .data74.chart-bar { -fx-background-color: #ffe500; } .data75.chart-bar { -fx-background-color: #ffe500; } .data76.chart-bar { -fx-background-color: #ffe500; } .data77.chart-bar { -fx-background-color: #ffe500; } .data78.chart-bar { -fx-background-color: #ffe500; } .data79.chart-bar { -fx-background-color: #ffe500; } .data80.chart-bar { -fx-background-color: #ffca00; } .data81.chart-bar { -fx-background-color: #ffca00; } .data82.chart-bar { -fx-background-color: #ffca00; } .data83.chart-bar { -fx-background-color: #ffca00; } .data84.chart-bar { -fx-background-color: #ffca00; } .data85.chart-bar { -fx-background-color: #ffca00; } .data86.chart-bar { -fx-background-color: #ffca00; } .data87.chart-bar { -fx-background-color: #ffca00; } .data88.chart-bar { -fx-background-color: #ffa700; } .data89.chart-bar { -fx-background-color: #ffa700; } .data90.chart-bar { -fx-background-color: #ffa700; } .data91.chart-bar { -fx-background-color: #ffa700; } .data92.chart-bar { -fx-background-color: #ffa700; } .data93.chart-bar { -fx-background-color: #ffa700; } .data94.chart-bar { -fx-background-color: #ffa700; } .data95.chart-bar { -fx-background-color: #ffa700; } .data96.chart-bar { -fx-background-color: #ff8100; } .data97.chart-bar { -fx-background-color: #ff8100; } .data98.chart-bar { -fx-background-color: #ff8100; } .data99.chart-bar { -fx-background-color: #ff8100; } .data100.chart-bar { -fx-background-color: #ff8100; } .data101.chart-bar { -fx-background-color: #ff8100; } .data102.chart-bar { -fx-background-color: #ff8100; } .data103.chart-bar { -fx-background-color: #ff8100; } .data104.chart-bar { -fx-background-color: #ff5900; } .data105.chart-bar { -fx-background-color: #ff5900; } .data106.chart-bar { -fx-background-color: #ff5900; } .data107.chart-bar { -fx-background-color: #ff5900; } .data108.chart-bar { -fx-background-color: #ff5900; } .data109.chart-bar { -fx-background-color: #ff5900; } .data110.chart-bar { -fx-background-color: #ff5900; } .data111.chart-bar { -fx-background-color: #ff5900; } .data112.chart-bar { -fx-background-color: #ff3400; } .data113.chart-bar { -fx-background-color: #ff3400; } .data114.chart-bar { -fx-background-color: #ff3400; } .data115.chart-bar { -fx-background-color: #ff3400; } .data116.chart-bar { -fx-background-color: #ff3400; } .data117.chart-bar { -fx-background-color: #ff3400; } .data118.chart-bar { -fx-background-color: #ff3400; } .data119.chart-bar { -fx-background-color: #ff3400; } .data120.chart-bar { -fx-background-color: #ff1600; } .data121.chart-bar { -fx-background-color: #ff1600; } .data122.chart-bar { -fx-background-color: #ff1600; } .data123.chart-bar { -fx-background-color: #ff1600; } .data124.chart-bar { -fx-background-color: #ff1600; } .data125.chart-bar { -fx-background-color: #ff1600; } .data126.chart-bar { -fx-background-color: #ff1600; } .data127.chart-bar { -fx-background-color: #ff1600; }*/ \ No newline at end of file diff --git a/src/gui/graphs/Bar.java b/src/gui/graphs/Bar.java new file mode 100644 index 0000000..352fdd6 --- /dev/null +++ b/src/gui/graphs/Bar.java @@ -0,0 +1,95 @@ +package gui.graphs; + +/** + * Adapted by arthur from Candle.java in javafx's Ensemble8 demo. + * + * Modified to change bar color more efficiently and to accept a color + * in its attributes + */ +/* + * Copyright (c) 2008, 2013 Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import javafx.geometry.Insets; +import javafx.scene.Group; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; + +/** Bar node used for drawing a bar */ +public class Bar extends Group { + private Region bar = new Region(); + private Color color; + + /** + * Constructeur + * @param color La couleur de la barre + */ + Bar(Color color) { + setAutoSizeChildren(false); + getChildren().addAll(bar); + this.color = color; + updateStyleClasses(); + } + + /** + * Définit une couleur + * @param color La couleur de la barre + */ + public void setSeriesAndDataStyleClasses(Color color) { + this.color = color; + updateStyleClasses(); + } + + /** + * Met à jour la position et taille des barres + * @param closeOffset La longueur de la barre + * @param barHeight La hauteur de la barre + * @param color La couleur de la barre + */ + public void update(double closeOffset, double barHeight, Color color) { + updateStyleClasses(); + if (barHeight == -1) { + barHeight = bar.prefHeight(-1); + } + bar.resizeRelocate(0, -barHeight / 2, closeOffset, barHeight); + this.color = color; + } + + /** + * Applique la couleur pour l'affichage + */ + private void updateStyleClasses() { + bar.setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY))); + } +} diff --git a/src/gui/graphs/BarExtraValues.java b/src/gui/graphs/BarExtraValues.java new file mode 100644 index 0000000..7a0b015 --- /dev/null +++ b/src/gui/graphs/BarExtraValues.java @@ -0,0 +1,59 @@ +package gui.graphs; + +/** + * Modified by arthur in javafx's Ensemble8 demo. + */ + +/* + * Copyright (c) 2008, 2013 Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +import javafx.scene.paint.Color; + +/** Data extra values for storing close value and color. */ +public class BarExtraValues { + private double close; + private Color color; + + public BarExtraValues(double close, Color color) { + this.close = close; + this.color = color; + } + + public double getClose() { + return close; + } + + public Color getColor(){ + return color; + } +} diff --git a/src/gui/graphs/BargramView.java b/src/gui/graphs/BargramView.java new file mode 100644 index 0000000..d706123 --- /dev/null +++ b/src/gui/graphs/BargramView.java @@ -0,0 +1,491 @@ +package gui.graphs; + +import conteneurs.Gamme; +import conteneurs.NoteGamme; +import conteneurs.RegularGamme; +import generictools.Instant; +import gui.PlayerControl; +import gui.PlayerControlEvent; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.scene.Parent; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ScrollEvent; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; +import processing.buffer.TemporalBuffer; + +import javax.swing.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.ArrayList; + +/** + * Affiche un graphe en "barres" représentant l'amplitude d'une fréquence + * donnée en fonction du temps. + * Created by arthur on 20/05/14. + */ +public abstract class BargramView> extends AbstractBufferGraph{ + protected TraceChart chart; + private StackPane pane; + private Rectangle cursorRect; + private Rectangle highlightedLine; + + final int RESOLUTION = 50; + private float windowWidthMs = 5000; + private Instant boundMin = new Instant(0); + protected Gamme yAxisGamme; + + + /** + * Constructeur + * @param name Le nom du graphe + * @param type Le type de buffer à afficher + */ + public BargramView(String name, Class type){ + super(name, type); + } + + /** + * Crée tous les éléments de l'affichage + * @return Le panel + */ + Parent createContent(){ + + //définition des axes + CategoryAxis yAxis = new CategoryAxis(); + yAxis.setAutoRanging(false); + yAxis.setTickLabelFont(Font.font(8)); + yAxis.setTickLabelRotation(0); + setYAxisGamme(NoteGamme.gamme()); + NumberAxis xAxis = new NumberAxis(0, 5000,500); + xAxis.setLabel("Temps (ms)"); + + //création d'un graphe TraceChart + chart = new TraceChart(xAxis,yAxis); + chart.setAnimated(false); + chart.setVerticalGridLinesVisible(false); + chart.setHorizontalGridLinesVisible(false); + chart.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))); + + //création du curseur + cursorRect = new Rectangle(); + cursorRect.setManaged(false); + cursorRect.setFill(Color.rgb(39, 93, 153)); + cursorRect.setWidth(1); + cursorRect.setHeight(215); + + //création d'une ligne de couleur horizontale pour mettre en + //surbrillance une ligne de fréquence + highlightedLine = new Rectangle(); + highlightedLine.setManaged(false); + highlightedLine.setFill((Color.GREEN.deriveColor(0,1,1,0.2))); + highlightedLine.setWidth(150); + highlightedLine.setHeight(4); + highlightedLine.setMouseTransparent(true); + + //creation du panel qui contient tous les éléments de l'affichage + pane = new StackPane(); + pane.getChildren().add(chart); + pane.getChildren().add(cursorRect); + pane.getChildren().add(highlightedLine); + + //quand la largeur de la fenêtre change, déplacer le curseur + pane.widthProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observableValue, Number oldHeight, Number newHeight) { + moveCursor(true); + } + }); + + //quand la hauteur de la fenêtre change, redimensionner le curseur + pane.heightProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observableValue, Number oldWidth, Number newWidth) { + resizeCursor(); + } + }); + + //quand la souris bouge, redessiner la ligne de surbrillance + pane.setOnMouseMoved(new EventHandler() { + @Override + public void handle(MouseEvent event) { + highlightLine(selectTickLine(event)); + float time = readTime(event); + getOnMouseMoved(readFreqID(event), Instant.fromTime(time - time % (windowWidthMs / RESOLUTION))).run(); + } + }); + + //quand le bouton gauche de la souris est appuyé, changer la frame + pane.setOnMousePressed(new EventHandler() { + @Override + public void handle(MouseEvent event) { + if (event.isPrimaryButtonDown()) { + setFrame(event); + float time = readTime(event); + getOnMouseClicked(readFreqID(event), Instant.fromTime(time - time % (windowWidthMs / RESOLUTION))).run(); + } + } + }); + pane.setOnMouseDragged(new EventHandler() { + @Override + public void handle(MouseEvent event) { + if (event.isPrimaryButtonDown()) { + setFrame(event); + float time = readTime(event); + getOnMouseClicked(readFreqID(event), Instant.fromTime(time - time % (windowWidthMs / RESOLUTION))).run(); + } + } + }); + + //avec ctrl+molette, zoomer ou dézoomer + pane.setOnScroll(new EventHandler() { + @Override + public void handle(ScrollEvent scrollEvent) { + if(scrollEvent.isControlDown()){ + setWindowWidthMs((float) -scrollEvent.getDeltaY() + windowWidthMs); + } + } + }); + + //rendre la ligne de surbrillance visible quand la souris rentre + //dans le panel + chart.setOnMouseEntered(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + highlightedLine.setVisible(true); + } + }); + //rendre la ligne de surbrillance invisible quand la souris sort + //dans le panel + chart.setOnMouseExited(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + highlightedLine.setVisible(false); + } + }); + highlightedLine.setVisible(false); + + //donne une série vide à afficher au chart pour éviter les erreurs + chart.setData(FXCollections.observableArrayList(new XYChart.Series())); + + return pane; + } + + /** + * Définit une nouvelle frame + * @param event L'événement de la souris + */ + private void setFrame(MouseEvent event){ + final Instant timeToSet = Instant.fromTime(readTime(event)); + + if(timeToSet.isGreaterThan(boundMin) && timeToSet.isLowerThan(boundMin.translated(windowWidthMs))) + PlayerControl.instance().setFrame(timeToSet); + + } + /** + * Déplace le curseur en fonction de la frame actuelle + * @param locked Booléen indiquant si le curseur doit être suivi + */ + private void moveCursor(final Boolean locked){ + final NumberAxis xAxis = (NumberAxis) chart.getXAxis(); + final CategoryAxis yAxis = (CategoryAxis) chart.getYAxis(); + final double yAxisInScene = yAxis.localToScene(0, 0).getX() + yAxis.getWidth(); + + if (locked) { + //si la frame est plus grande que la limite sup, translater la + //fenêtre vers la droite + if (PlayerControl.frame().toMs() >= boundMin.toMs()+windowWidthMs) + setBoundMin(boundMin.translated(windowWidthMs)); + + //si la frame est plus petite que la limite inf, translater la + //fenêtre vers la gauche + if (PlayerControl.frame().toMs() < boundMin.toMs()) + setBoundMin(boundMin.translated(-windowWidthMs)); + } + + Platform.runLater(new Runnable() { + @Override + public void run() { + //positionne le curseur + cursorRect.setX((PlayerControl.frame().toMs() - boundMin.toMs()) * xAxis.getScale() + yAxisInScene); + } + }); + } + + /** + * Met en surbrillance une ligne horizontale correspondant à une + * valeur sur l'axe y + * @param category La valeur de la donnée sur l'axe y à mettre en + * surbrillance + */ + private void highlightLine(String category) { + Platform.runLater(new Runnable() { + @Override + public void run() { + final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis(); + double tickPosition = yAxis.getDisplayPosition(category) + yAxis.localToScene(0,0).getY(); + highlightedLine.setY(tickPosition - highlightedLine.getHeight()/2); + + resizeHiglightedLine(); + } + }); + } + + /** + * Redimensionne la ligne de surbrillance + */ + private void resizeHiglightedLine(){ + final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis(); + highlightedLine.setX(0); + highlightedLine.setWidth(pane.getWidth() > 0 ? pane.getWidth() : 100); + highlightedLine.setHeight(yAxis.getHeight() / yAxis.getCategories().size()); + } + + /** + * Sélectionne la donnée de l'axe y sous le curseur de la souris + * @param event L'événement de la souris + * @return La donnée de l'axe y + */ + private String selectTickLine(MouseEvent event){ + final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis(); + return yAxis.getValueForDisplay(event.getY() - yAxis.localToScene(0,0).getY()); + } + + private int readFreqID(MouseEvent event){ + final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis(); + return yAxis.getCategories().indexOf(yAxis.getValueForDisplay(event.getY() - yAxis.localToScene(0,0).getY())); + } + private float readTime(MouseEvent event){ + final NumberAxis xAxis = (NumberAxis)chart.getXAxis(); + final CategoryAxis yAxis = (CategoryAxis) chart.getYAxis(); + final double yAxisInScene = yAxis.localToScene(0, 0).getX() + yAxis.getWidth(); + final double mouseX = event.getX(); + return xAxis.getValueForDisplay(mouseX - yAxisInScene).floatValue(); + } + + + /** + * Redimensionne le curseur + */ + private void resizeCursor(){ + Platform.runLater(() -> { + final CategoryAxis yAxis = (CategoryAxis) chart.getYAxis(); + + /*si l'axe y n'est pas complètement initialisé, cela peut arriver + si le graphe met un peu plus de temps à s'initialiser, positionner + le curseur avec des valeurs fixes*/ + if (yAxis.getHeight() == 0){ + cursorRect.setY(17); + moveCursor(true); + } else { + cursorRect.setY(yAxis.localToScene(0, 0).getY()); + cursorRect.setHeight(yAxis.getHeight()); + } + }); + } + + /** + * Définit une limite minimale pour dessiner + * @param bound + */ + private void setBoundMin(Instant bound){ + if(bound.toMs() < 0) + bound = Instant.fromTime(0); + + boundMin = bound; + applyWindowChange(); + } + + /** + * Définit la taille de la fenêtre du signal à afficher + * @param windowWidthMs La taille de la fenêtre + */ + private void setWindowWidthMs(float windowWidthMs){ + this.windowWidthMs = windowWidthMs; + applyWindowChange(); + } + + /** + * Appelle setWindow() de BufferViewerState + */ + private void applyWindowChange(){ + + final NumberAxis xAxis = (NumberAxis)chart.getXAxis(); + xAxis.setLowerBound(boundMin.toMs()); + xAxis.setUpperBound(boundMin.toMs()+windowWidthMs); + + + cleanView(); + state().setWindow(boundMin.mapToIndex(buffer()), boundMin.translated(windowWidthMs).mapToIndex(buffer())); + + } + + + /** + * Définit la gamme qui peut alterner entre regular et chromaatique + * @param yAxisGamme La gamme + */ + public void setYAxisGamme(Gamme yAxisGamme){ + this.yAxisGamme = yAxisGamme; + Platform.runLater(new Runnable(){ + @Override + public void run() { + changeYAxis(); + state().invalidate(); + } + }); + } + + public Gamme getGamme(){ + return yAxisGamme; + } + + /** + * Alterne la gamme à afficher entre regular et chromatic + */ + private void changeYAxis(){ + CategoryAxis yAxis = (CategoryAxis)chart.getYAxis(); + ArrayList categories = new ArrayList(); + + for(int i=0 ; iobservableArrayList(categories)); + yAxis.invalidateRange(categories); + } + + public void playerControlEvent(PlayerControlEvent e) { + if(!visibility()) + return; + + switch (e.getType()) { + case FRAME: + moveCursor(true); + break; + } + } + + @Override + public Parent getMainNode() { + return pane; + } + + /** + * Revoie la liste des éléments du menu contextuel + * + * @param contextMenu Le menu + */ + @Override + public void createContextMenu(JPopupMenu contextMenu) { + JMenu choixAxesX = new JMenu("Axe fréquence"); + JRadioButtonMenuItem noteGamme = new JRadioButtonMenuItem("Gamme chromatique"); + noteGamme.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if(e.getStateChange()==ItemEvent.SELECTED) + setYAxisGamme(NoteGamme.gamme()); + } + }); + JRadioButtonMenuItem regularGamme = new JRadioButtonMenuItem("Gamme linéaire"); + regularGamme.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if(e.getStateChange()==ItemEvent.SELECTED) + setYAxisGamme(new RegularGamme(NoteGamme.getMaxFreq(), NoteGamme.getMaxFreq(), 1000)); + } + }); + ButtonGroup b = new ButtonGroup(); + b.add(noteGamme); + b.add(regularGamme); + choixAxesX.add(noteGamme); + choixAxesX.add(regularGamme); + + contextMenu.add(choixAxesX); + } + + @Override + protected void initView() { + createContent(); + cleanView(); + state().setSlowingFactorOnDataChange(5); + setBoundMin(new Instant(0)); + } + + @Override + public void updateView(){ + XYChart.Series series = new XYChart.Series<>(); + ObservableList categories = ((CategoryAxis)chart.getYAxis()).getCategories(); + + final float INC = windowWidthMs/RESOLUTION; + + // On parcours la fenêtre selon le temps et si l'échantillon à l'instant considéré est non nul ... + for (float time = boundMin.toMs(); time <= boundMin.toMs()+windowWidthMs ; time+=INC) { + if (buffer().get(time) != null) { + + //On parcours l'échantillon fréquence par fréquence + for (int idFreq = 0; idFreq < categories.size(); idFreq++) { + Color color = getColor(idFreq, Instant.fromTime(time)); + if (color != null && (color.getBrightness() < .95 ||color.getSaturation() > .05 )) { + + series.getData().add(new XYChart.Data(time, categories.get(idFreq), + new BarExtraValues(time+INC, color))); + + } + + } + } + } + Platform.runLater(new Runnable() { + @Override + public void run() { + chart.getData().set(0, series); + } + }); + } + + protected abstract Color getColor(int idFreq, Instant time); + + protected Runnable getOnMouseMoved(int idFreq, Instant time){return new Runnable() { + @Override + public void run() { + + } + };} + + protected Runnable getOnMouseClicked(int idFreq, Instant time){return new Runnable() { + @Override + public void run() { + + } + };} + + @Override + public void cleanView() { + Platform.runLater(new Runnable() { + @Override + public void run() { + chart.setData(FXCollections.observableArrayList(new XYChart.Series())); + + } + }); + } + +} diff --git a/src/gui/graphs/EventViewer.java b/src/gui/graphs/EventViewer.java new file mode 100644 index 0000000..c8e73ac --- /dev/null +++ b/src/gui/graphs/EventViewer.java @@ -0,0 +1,74 @@ +package gui.graphs; + +import conteneurs.*; +import generictools.Instant; +import javafx.scene.paint.Color; +import processing.buffer.TemporalBuffer; + +/** + * Created by gaby on 31/05/14. + */ +public class EventViewer extends BargramView> { + OverlayGraph overlay; + public EventViewer() { + super("Evenorama", NoteEventList.class); + + } + + @Override + protected Color getColor(int idFreq, Instant time) { + NoteEventList list = buffer().get(time); + if(list == null) + return null; + + NoteEvent event = null; + if(getGamme()== NoteGamme.gamme()) + event = list.getByNoteId(idFreq); + + if(event==null) + return null; + + float saturation = (float)Math.min(.2f + event.getEventAmpli()/1000000f, .92); + return Color.hsb(event.getEventType()==NoteEvent.Type.APPARITION?85:13, saturation, .73); + } + + @Override + protected Runnable getOnMouseMoved(int idFreq, Instant time) { + NoteEventList list = buffer().get(time); + Runnable hide = new Runnable() { + @Override + public void run() { + overlay.setVisibility(false); + } + }; + if(list == null) + return hide; + + if(getGamme()!= NoteGamme.gamme()) + return hide; + + final NoteEvent event = list.getByNoteId(idFreq); + + if(event == null) + return hide; + + return new Runnable() { + @Override + public void run() { + ((AbstractObservableGraph)overlay.getGraph()).setToShow(event.getNote().getProfil()); + overlay.setVisibility(true); + } + }; + } + + @Override + protected AbstractBufferGraph getGenericGraph() { + return new EventViewer(); + } + + @Override + protected void initView() { + super.initView(); + overlay = getGraphicView().createOverlay(getGraphicView().createGraphs(new Profil()).get(0), OverlayGraph.AutoPosition.MOUSE, false); + } +} diff --git a/src/gui/graphs/GraphicView.java b/src/gui/graphs/GraphicView.java new file mode 100644 index 0000000..1f0cd25 --- /dev/null +++ b/src/gui/graphs/GraphicView.java @@ -0,0 +1,351 @@ +package gui.graphs; + +import conteneurs.ObservableObject; +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.event.EventHandler; +import javafx.scene.Scene; +import javafx.scene.input.ContextMenuEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import processing.buffer.Buffer; + +import javax.swing.*; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.ArrayList; + +/** + * Created by Gabriel on 12/03/14. + * Classe gérant l'adaptation entre JavaFX et swing et chargée d'afficher les graphs et leurs overlays + */ +public class GraphicView extends JFXPanel{ + + static private ArrayList typeSupported = new ArrayList<>(); + static private ArrayList> genericGraphes = new ArrayList<>(); + + static public void initGraph(){ + + registerGraph(new AmpliView()); + registerGraph(new Spectrogram()); + registerGraph(new SpectreView()); + registerGraph(new EventViewer()); + registerGraph(new ProfilView()); + registerGraph(new NoteViewer()); + } + + static public void registerGraph(AbstractGraph graph){ + int idType = typeSupported.indexOf(graph.getType());; + + if(idType==-1){ + typeSupported.add(graph.getType()); + genericGraphes.add(new ArrayList<>()); + idType = typeSupported.size()-1; + } + + genericGraphes.get(idType).add(graph); + } + + + private final String name; + private JMenu switchMenu = null; + private BorderPane layout = new BorderPane(); + private ArrayList graphs = new ArrayList<>(); + private Pane overlayLayout = new Pane(); + private ArrayList overlays = new ArrayList<>(); + private int idCurrentGraph=-1; + private boolean visibility = false; + + protected GraphicView( String name) { + super(); + this.name = name; + + + this.addAncestorListener(new AncestorListener() { + @Override + public void ancestorAdded(AncestorEvent event) { + visibilityChange(true); + } + + @Override + public void ancestorRemoved(AncestorEvent event) { + visibilityChange(false); + } + + @Override + public void ancestorMoved(AncestorEvent event) { + + } + }); + } + + public void visibilityChange(boolean isVisible){ + if(visibility == isVisible) + return; + + visibility = isVisible; + for (int i = 0; i < graphs.size(); i++) { + graphs.get(i).visibilityChange(isVisible&&i==idCurrentGraph); + } + + for(OverlayGraph o: overlays) + o.viewVisibiliyChange(); + + } + + public boolean getVisibility(){ + return visibility; + } + + + private void setMainGraph(int idGraph){ + if(idGraph<0 || idGraph>graphs.size()) + return; + + if(getGraph(idCurrentGraph)!=null) + getGraph(idCurrentGraph).setVisibility(false); + + for(OverlayGraph o: overlays) + o.setVisibility(false); + + Platform.runLater(new Runnable() { + @Override + public void run() { + layout.setCenter(graphs.get(idGraph).getMainNode()); + idCurrentGraph = idGraph; + graphs.get(idGraph).setVisibility(true); + } + }); + + } + + + + + + + public static GraphicView createGraphicView(Buffer buffer, boolean syncWithPlayer){ + + GraphicView g = new GraphicView(buffer.getName()); + ArrayList graphs = createGraphs(buffer, g, syncWithPlayer); + + if(graphs.size()==0) + return null; + + for(AbstractGraph graph: graphs) + g.addGraph(graph); + + + if(!finishSetup(g)) + return null; + + return g; + } + + public static GraphicView createGraphicView(ObservableObject o) { + + GraphicView g = new GraphicView(o.toString()); + ArrayList graphs = createGraphs(o, g); + + if(graphs.size()==0) + return null; + + for(AbstractGraph graph: graphs) + g.addGraph(graph); + + if(!finishSetup(g)) + return null; + + return g; + } + + public static ArrayList createGraphs(Buffer buffer, GraphicView graphicView, boolean syncWithPlayer){ + + if(typeSupported.isEmpty()) { + initGraph(); + if(typeSupported.isEmpty()) + return new ArrayList<>(); + } + + int idType = typeSupported.indexOf(buffer.getType()); + if(idType==-1) + return new ArrayList<>(); + + ArrayList r = new ArrayList<>(genericGraphes.get(idType).size()); + + for (int i = 0; i < genericGraphes.get(idType).size(); i++) + r.add(genericGraphes.get(idType).get(i).setupGraph(buffer, graphicView, syncWithPlayer)); + + return r; + } + public ArrayList createGraphs(Buffer buffer, boolean syncWithPlayer){ + return createGraphs(buffer, this, syncWithPlayer); + } + + public static ArrayList createGraphs(ObservableObject observableObject, GraphicView graphicView){ + if (typeSupported.isEmpty()) { + initGraph(); + if (typeSupported.isEmpty()) + return new ArrayList<>(); + } + + int idType = typeSupported.indexOf(observableObject.getType()); + if(idType==-1) + return new ArrayList<>(); + + ArrayList r = new ArrayList<>(genericGraphes.get(idType).size()); + + for (int i = 0; i < genericGraphes.get(idType).size(); i++) { + AbstractGraph abstractGraph = genericGraphes.get(idType).get(i).setupGraph(observableObject,graphicView); + if(abstractGraph!=null) + r.add(abstractGraph); + } + + return r; + } + + public ArrayList createGraphs(ObservableObject observableObject){ + return createGraphs(observableObject, this); + } + + public OverlayGraph createOverlay(AbstractGraph graph, OverlayGraph.AutoPosition position, boolean showByDefault){ + OverlayGraph o = null; + switch(position){ + case CUSTOM: + o = new OverlayGraph(graph, this); + break; + case CENTERED: + o = OverlayGraph.centeredOverlay(graph, 300,450); + break; + case PERFECT_CENTERED: + o = OverlayGraph.perfectCenteredOverlay(graph); + break; + case MOUSE: + o = OverlayGraph.followMouseGraph(graph, 200, 250); + break; + } + + o.setVisibility(showByDefault); + addOverlay(o); + return o; + } + + public void addOverlay(OverlayGraph overlayGraph){ + if(overlays.contains(overlayGraph)) + return; + + overlays.add(overlayGraph); + overlayLayout.getChildren().add(overlayGraph); + } + + public void removeOverlay(OverlayGraph overlayGraph){ + int id = overlays.indexOf(overlayGraph); + if(id==-1) + return; + + overlays.remove(id); + overlayLayout.getChildren().remove(id); + } + + public ArrayList getOverlays() { + return overlays; + } + + private static boolean finishSetup(GraphicView g){ + if(g.getNbrGraph()==0) + return false; + + if(g.getNbrGraph()>1) { + JMenu menu = new JMenu("Type de graphe"); + ButtonGroup radioGroup = new ButtonGroup(); + for (int i = 0; i < g.getNbrGraph(); i++) { + AbstractGraph graph = g.getGraph(i); + JRadioButtonMenuItem graphItem = new JRadioButtonMenuItem(graph.getName(), i == 0); + final int idGraph = i; + graphItem.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) + g.setMainGraph(idGraph); + } + }); + + radioGroup.add(graphItem); + menu.add(graphItem); + } + g.setSwitchMenu(menu); + } + + g.initGraphicView(); + g.setMainGraph(0); + + return true; + } + + private void initGraphicView(){ + Platform.runLater(new Runnable() { + @Override + public void run() { + setScene(new Scene(new StackPane(layout, overlayLayout))); + layout.setOnContextMenuRequested(new EventHandler() { + @Override + public void handle(ContextMenuEvent contextMenuEvent) { + + popupContextMenu((int)contextMenuEvent.getX(), (int)contextMenuEvent.getY()); + contextMenuEvent.consume(); + } + }); + + overlayLayout.setMouseTransparent(true); + for(OverlayGraph o: overlays) + o.setGraphicView(GraphicView.this); + } + }); + } + + private void setSwitchMenu(JMenu menu){switchMenu = menu;} + + private int addGraph(AbstractGraph graph){graphs.add(graph); return graphs.size()-1;} + + public int getNbrGraph(){return graphs.size();} + + public AbstractGraph getGraph(int id){ + if(id<0|| id>graphs.size()) + return null; + return graphs.get(id); + } + + public int getIdMainGraph(){return idCurrentGraph;} + + public AbstractGraph getMainGraph(){return graphs.get(idCurrentGraph);} + + + public void popupContextMenu(int x, int y){ + JPopupMenu contextMenu = new JPopupMenu(); + if(switchMenu!=null) { + contextMenu.add(switchMenu); + contextMenu.addSeparator(); + } + + getMainGraph().createContextMenu(contextMenu); + contextMenu.show(this, x, y); + } + public void popupOverlayContextMenu(int x, int y, AbstractGraph graph){ + + JPopupMenu contextMenu = new JPopupMenu(); + + graph.createContextMenu(contextMenu); + contextMenu.show(this, x, y); + } + + + public String getName() { + return name; + } + + private BorderPane getBorderPane(){return layout;} + +} diff --git a/src/gui/graphs/NoteViewer.java b/src/gui/graphs/NoteViewer.java new file mode 100644 index 0000000..dae7f4d --- /dev/null +++ b/src/gui/graphs/NoteViewer.java @@ -0,0 +1,32 @@ +package gui.graphs; + +import conteneurs.NoteList; +import generictools.Instant; +import javafx.scene.paint.Color; +import processing.buffer.NoteBuffer; + +/** + * Created by gaby on 03/06/14. + */ +public class NoteViewer extends BargramView { + public NoteViewer() { + super("Tablature", NoteList.class); + } + + @Override + protected Color getColor(int idFreq, Instant time) { + NoteList list = buffer().get(time); + if(list == null) + return null; + + if(list.getNoteById(idFreq)!=null) + return Color.rgb(51,108,212); + + return null; + } + + @Override + protected AbstractBufferGraph getGenericGraph() { + return new NoteViewer(); + } +} diff --git a/src/gui/graphs/Overlay.css b/src/gui/graphs/Overlay.css new file mode 100644 index 0000000..1af6be8 --- /dev/null +++ b/src/gui/graphs/Overlay.css @@ -0,0 +1,13 @@ + + +.overlay{ + + -fx-border-radius: 10 20 10 20; + -fx-background-radius: 25 10 25 10; + + -fx-padding: 10; + -fx-background-color: #ffffff; + + /* top-left, top-right, bottom-right, and bottom-left corners, in that order. */ +} + diff --git a/src/gui/graphs/OverlayGraph.java b/src/gui/graphs/OverlayGraph.java new file mode 100644 index 0000000..ef6c7bb --- /dev/null +++ b/src/gui/graphs/OverlayGraph.java @@ -0,0 +1,312 @@ +package gui.graphs; + +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.scene.effect.DropShadow; +import javafx.scene.input.ContextMenuEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.*; + + +/** + * Created by gaby on 11/05/14. + * Classe permettant d'afficher un overlay au dessus du graphs principal dans une GraphicView + */ +public class OverlayGraph extends BorderPane implements ChangeListener, EventHandler { + private AbstractGraph graph; + + private GraphicView graphicView; + + private AutoPosition autoPosition = AutoPosition.CUSTOM; + + private double xMousePos=0; + private double yMousePos=0; + private double xMouseOffset=0; + private double yMouseOffset=0; + + public OverlayGraph() { + super(); + Platform.runLater(new Runnable() { + @Override + public void run() { + + getStyleClass().add("overlay"); + getStylesheets().add(AmpliView.class.getResource("Overlay.css").toExternalForm()); + + DropShadow dS = new DropShadow(); + dS.setRadius(10); + dS.setOffsetY(2); + dS.setOffsetX(2); + setEffect(dS); + } + }); + } + + public OverlayGraph(AbstractGraph graph){ + this(); + setGraph(graph); + } + public OverlayGraph(AbstractGraph graph, GraphicView graphicView){ + this(graph); + setGraphicView(graphicView); + } + + public void setGraph(AbstractGraph graph){ + if(graph==null) + return; + + if(this.graph!=null) + this.graph.setVisibility(false); + graph.setVisibility(getVisibility()); + this.graph = graph; + + Platform.runLater(new Runnable() { + @Override + public void run() { + setCenter(graph.getMainNode()); + } + }); + } + + static public OverlayGraph centeredOverlay(AbstractGraph graph, int height, int width){ + OverlayGraph r = new OverlayGraph(graph); + r.setOverlayHeight(height); + r.setOverlayWidth(width); + r.setAutoPosition(AutoPosition.CENTERED); + return r; + } + static public OverlayGraph perfectCenteredOverlay(AbstractGraph graph){ + OverlayGraph r = new OverlayGraph(graph); + r.setAutoPosition(AutoPosition.PERFECT_CENTERED); + return r; + } + static public OverlayGraph followMouseGraph(AbstractGraph graph, int height, int width){ + OverlayGraph r = new OverlayGraph(graph); + r.setAutoPosition(AutoPosition.MOUSE); + r.setOverlayHeight(height); + r.setOverlayWidth(width); + return r; + } + + public AbstractGraph getGraph() { + return graph; + } + + public boolean getVisibility(){return isVisible();} + + public void setVisibility(boolean visibility){ + setVisible(visibility); + graph.setVisibility(visibility); + + } + + public void viewVisibiliyChange(){ + graph.setVisibility(getVisibility()||graphicView.getVisibility()); + } + + + /** + * Fait apparaitre le graph à la position indiqué + * Si le positionnement automatique est en mode MOUSE la position de l'overlay est forcé + * Si il est en mode CENTERED ou PERFECT_CENTERED, le passe automatiquement en mode CUSTOM + * @param x Position horizontale absolue dans la GraphicView + * @param y Position verticale absolue dans la GraphicView + */ + public void popupAt(double x, double y){ + if(autoPosition == AutoPosition.CENTERED || autoPosition==AutoPosition.PERFECT_CENTERED) + setAutoPosition(AutoPosition.CUSTOM); + + if(autoPosition==AutoPosition.CUSTOM) { + setTranslateX(x); + setTranslateY(y); + }else{ + xMousePos = x; + yMousePos = y; + moveToMouse(); + } + } + + /** + * Modifie la position horizontale de l'overlay. + * Passe automatiquement le positionnement automatique en mode CUSTOM + * @param x Position horizontale absolue dans la GraphicView + */ + public void setX(double x){ + if(autoPosition!=AutoPosition.CUSTOM) + setAutoPosition(AutoPosition.CUSTOM); + setTranslateX(x); + } + /** + * Modifie la position verticale de l'overlay. + * Passe automatiquement le positionnement automatique en mode CUSTOM + * @param y Position verticale absolue dans la GraphicView + */ + public void setY(double y){ + if(autoPosition!=AutoPosition.CUSTOM) + setAutoPosition(AutoPosition.CUSTOM); + setTranslateX(y); + } + + /** + * Modifie la hauteur de l'overlay + * Passe automatiquement le positionnement automatique en mose CENTERED si il était en mode PERFECT_CENTERED + * @param height Hauteur de l'overlay + */ + public void setOverlayHeight(double height){ + if(autoPosition==AutoPosition.PERFECT_CENTERED) + setAutoPosition(AutoPosition.CENTERED); + super.setPrefHeight(height); + } + /** + * Modifie la largeur de l'overlay + * Passe automatiquement le positionnement automatique en mose CENTERED si il était en mode PERFECT_CENTERED + * @param width Largeur de l'overlay + */ + public void setOverlayWidth(double width){ + if(autoPosition==AutoPosition.PERFECT_CENTERED) + setAutoPosition(AutoPosition.CENTERED); + super.setPrefWidth(width); + } + + + public double getxMouseOffset() { + return xMouseOffset; + } + + public void setxMouseOffset(double xMouseOffset) { + this.xMouseOffset = xMouseOffset; + } + + public double getyMouseOffset() { + return yMouseOffset; + } + + public void setyMouseOffset(double yMouseOffset) { + this.yMouseOffset = yMouseOffset; + } + + public void setGraphicView(GraphicView graphicView){ + if(graphicView==this.graphicView) + return; + + if(this.graphicView!=null) { + if (autoPosition == AutoPosition.CENTERED || autoPosition == AutoPosition.PERFECT_CENTERED) { + graphicView.getScene().widthProperty().removeListener(this); + graphicView.getScene().heightProperty().removeListener(this); + }else if (autoPosition == AutoPosition.MOUSE) { + graphicView.getScene().removeEventHandler(MouseEvent.MOUSE_MOVED, this); + } + this.graphicView.removeOverlay(this); + + } + + setOnContextMenuRequested(new EventHandler() { + @Override + public void handle(ContextMenuEvent contextMenuEvent) { + graphicView.popupOverlayContextMenu((int)contextMenuEvent.getSceneX(), (int)contextMenuEvent.getSceneY(), graph); + contextMenuEvent.consume(); + } + }); + + this.graphicView = graphicView; + applyConnection(); + } + + /** + * Modifie la propriété de placement automatique: + * -CUSTOM: laisse le choix au programateur de placé ou il veut l'overlay + * -CENTERED: centre la vue dans la fenetre (en tenant compte de la hauteur et largeur de l'overlay) + * -MOUSE: positionne l'overlay sous la souris (en tenant compte du décalage mouseOffset) + * @param autoPosition + */ + public void setAutoPosition(AutoPosition autoPosition){ + if(this.autoPosition == autoPosition) + return; + if(graphicView!=null) { + if (this.autoPosition == AutoPosition.CENTERED || autoPosition == AutoPosition.PERFECT_CENTERED) { + graphicView.getScene().widthProperty().removeListener(this); + graphicView.getScene().heightProperty().removeListener(this); + } else if (this.autoPosition == AutoPosition.MOUSE) + graphicView.getScene().removeEventHandler(MouseEvent.MOUSE_MOVED, this); + } + this.autoPosition = autoPosition; + applyConnection(); + } + public AutoPosition getAutoPosition(){return autoPosition;} + + private void applyConnection(){ + if(graphicView==null) + return; + + if(autoPosition == AutoPosition.CENTERED || autoPosition==AutoPosition.PERFECT_CENTERED) { + graphicView.getScene().widthProperty().addListener(this); + graphicView.getScene().heightProperty().addListener(this); + centerGeometry(); + } + + + if(autoPosition == AutoPosition.MOUSE) { + graphicView.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, this); + moveToMouse(); + } + } + + + private void centerGeometry(){ + if(autoPosition==AutoPosition.PERFECT_CENTERED){ + Platform.runLater(new Runnable() { + @Override + public void run() { + double h = graphicView.getParent().getHeight(); + double w = graphicView.getParent().getWidth(); + setTranslateX(w/20.0); + setTranslateY(h/ 20.0); + OverlayGraph.super.setPrefHeight(h*18.0/20.0); + OverlayGraph.super.setPrefWidth(w*18.0/20.0); + } + }); + }else { + Platform.runLater(new Runnable() { + @Override + public void run() { + double h = graphicView.getParent().getHeight(); + double w = graphicView.getParent().getWidth(); + setTranslateX(w - getPrefWidth() / 2.0); + setTranslateY(h - getPrefHeight() / 2.0); + } + }); + } + } + private void moveToMouse(){ + Platform.runLater(new Runnable() { + @Override + public void run() { + setTranslateX(xMousePos - xMouseOffset); + setTranslateY(yMousePos - yMouseOffset); + } + }); + + } + + public enum AutoPosition{ + CUSTOM, + CENTERED, + PERFECT_CENTERED, + MOUSE + } + + @Override + public void changed(ObservableValue observableValue, Number number, Number number2) { + centerGeometry(); + } + + @Override + public void handle(MouseEvent mouseEvent) { + xMousePos = mouseEvent.getX(); + yMousePos = mouseEvent.getY(); + moveToMouse(); + } + +} diff --git a/src/gui/graphs/ProfilView.java b/src/gui/graphs/ProfilView.java new file mode 100644 index 0000000..6d05bc5 --- /dev/null +++ b/src/gui/graphs/ProfilView.java @@ -0,0 +1,48 @@ +package gui.graphs; + +import conteneurs.*;import javafx.scene.chart.CategoryAxis; + +import javax.swing.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener;import java.util.ArrayList; + + +/** + * Created by Gabriel on 01/04/2014. + */ +public class ProfilView extends AmpliFreqView{ + + + ProfilView() { + super( "Visualisateur de profil", Profil.class); + } + + @Override + protected AbstractObservableGraph getGenericGraph() { + return new ProfilView(); + } + + protected void initView() { + super.initView(); + currentBarChart.getXAxis().setTickLabelRotation(0); + createCategory(8); + } + + private void createCategory(int nbCategory){ + ArrayList categories = new ArrayList<>(nbCategory); + + if(nbCategory>0) + categories.add("Fonda"); + + for (int i = 1; i < nbCategory; i++) + categories.add("Har " + String.valueOf(i)); + + + setXAxisCategories(categories); + } + + @Override + protected float getAmplitude(int idCategory) { + return toShow().getAmplitude(idCategory); + }} + diff --git a/src/gui/graphs/SpectreView.java b/src/gui/graphs/SpectreView.java new file mode 100644 index 0000000..55c5e55 --- /dev/null +++ b/src/gui/graphs/SpectreView.java @@ -0,0 +1,94 @@ +package gui.graphs; + +import conteneurs.Gamme; +import conteneurs.NoteGamme; +import conteneurs.RegularGamme; +import conteneurs.Spectre; + +import javax.swing.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; + + +/** + * Created by Gabriel on 01/04/2014. + */ +public class SpectreView extends AmpliFreqView{ + + private Gamme xAxisGamme; + + public enum Type{ + REGULAR_FREQ, + NOTES + } + + SpectreView() { + super( "Visualisateur de spectre", Spectre.class); + } + + @Override + protected AbstractObservableGraph getGenericGraph() { + return new SpectreView(); + } + + /** + * Revoie le liste des éléments du menu contextuel + * + * @param contextMenu + */ + @Override + public void createContextMenu(JPopupMenu contextMenu) { + super.createContextMenu(contextMenu); + + JMenu choixAxesX = new JMenu("Axe fréquence"); + JRadioButtonMenuItem noteGamme = new JRadioButtonMenuItem("Gamme chromatique",xAxisGamme instanceof NoteGamme); + noteGamme.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if(e.getStateChange()==ItemEvent.SELECTED) + setXAxisGamme(NoteGamme.gamme()); + } + }); + JRadioButtonMenuItem regularGamme = new JRadioButtonMenuItem("Gamme linéaire",xAxisGamme instanceof RegularGamme); + regularGamme.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if(e.getStateChange()==ItemEvent.SELECTED) + setXAxisGamme(new RegularGamme((float)Math.pow(10, (int)Math.log10(NoteGamme.getMinFreq())),(float)Math.pow(10, (int)Math.log10(NoteGamme.getMaxFreq())),1000)); + } + }); + ButtonGroup b = new ButtonGroup(); + b.add(noteGamme); + choixAxesX.add(noteGamme); + b.add(regularGamme); + choixAxesX.add(regularGamme); + + contextMenu.add(choixAxesX); + } + + @Override + public void setToShow(Spectre toShow) { + if(toShow != null) { + toShow = toShow.copy(); + if(toShow.getGamme() != xAxisGamme) + toShow.castToGamme(xAxisGamme); + } + super.setToShow(toShow); + } + + public void setXAxisGamme(Gamme xAxisGamme){ + this.xAxisGamme = xAxisGamme; + if(toShow()!=null) + toShow().castToGamme(xAxisGamme); + setXAxisCategories(xAxisGamme.toStringList()); + } + + protected void initView() { + super.initView(); + setXAxisGamme(NoteGamme.gamme()); + } + + @Override + protected float getAmplitude(int idCategory) { + return toShow().getAmplitude(idCategory); + }} diff --git a/src/gui/graphs/Spectrogram.java b/src/gui/graphs/Spectrogram.java new file mode 100644 index 0000000..1787679 --- /dev/null +++ b/src/gui/graphs/Spectrogram.java @@ -0,0 +1,91 @@ +package gui.graphs; + +import conteneurs.Spectre; +import generictools.Instant; +import javafx.scene.paint.Color; +import processing.buffer.TemporalBuffer; + +/** + * Created by gaby on 31/05/14. + */ +public class Spectrogram extends BargramView> { + + Instant optiCastTime = new Instant(-1); + Spectre optiSpectre = null; + + private final float MAX = 4000000; + + public Spectrogram() { + super("Spectro", Spectre.class); + + } + + @Override + protected Color getColor(int idFreq, Instant time) { + if(!time.equals(optiCastTime)) { + optiCastTime = time; + optiSpectre = buffer().get(time); + if(optiSpectre!=null) { + optiSpectre = optiSpectre.copy(); + if (optiSpectre.getGamme() != getGamme()) + optiSpectre.castToGamme(getGamme()); + } + } + + if(optiSpectre == null) + return null; + + return colorRamp(optiSpectre.getAmplitude(idFreq), MAX); + } + + public Color colorRamp(float pos, float maxPos){ + + if(pos == 0 || maxPos == 0) + return Color.hsb(0,0,1); + + float h1 = 360; + float h2 = 330; + float h3 = 310; + if(pos<0){ + h1 = 190; + h2 = 200; + h3 = 222; + pos = -pos; + } + + pos = Math.min(pos, maxPos-1); + + float x = pos/maxPos; + x = x*x; + + if(x <= .08) { + x/=.08; + return Color.hsb(h1, x*.65f, 1); + } + + x-=.08; + if(x <= .52) { + x/=.52; + return Color.hsb(((1f-x) * h1) + (x * h2), ((1f-x) * .65f) + (x * .8f), (1f-x) + (x * .75f)); + } + x-=.52; + x/=.4; + + if(((1f-x) * h2) + (x * h3)>222 && ((1f-x) * h2) + (x * h3)<310) + System.out.println("x:"+x+" h2:"+h2+"/ h3:"+h3); + + try { + return Color.hsb(((1f-x) * h2) + (x * h3), ((1f-x) * .8f) + (x * 1f), ((1f-x) * .75f) + (x * .53f)); + }catch (Exception e){ + + e.printStackTrace(); + } + return Color.hsb(h3, 1f, .53f); + } + + + @Override + protected AbstractBufferGraph getGenericGraph() { + return new Spectrogram(); + } +} diff --git a/src/gui/graphs/SpectrogramView.java b/src/gui/graphs/SpectrogramView.java new file mode 100644 index 0000000..5791f7b --- /dev/null +++ b/src/gui/graphs/SpectrogramView.java @@ -0,0 +1,460 @@ +package gui.graphs; + +import conteneurs.Gamme; +import conteneurs.NoteGamme; +import conteneurs.RegularGamme; +import conteneurs.Spectre; +import generictools.Instant; +import gui.PlayerControl; +import gui.PlayerControlEvent; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.scene.Parent; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; +import processing.buffer.TemporalBuffer; + +import javax.swing.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.ArrayList; + +/** + * Created by arthur on 14/05/14. + */ +public class SpectrogramView extends AbstractBufferGraph> { + + private TraceChart chart; + private StackPane pane; + private Gamme yAxisGamme; + private Rectangle cursorRect; + + private int inc; + private int boundMax; + private int gammeSize; + private Rectangle highlightedLine; + + private ObservableList categories; + + public SpectrogramView(){ + super("Spectrogramme",Spectre.class); + } + + public Parent createContent(){ + + CategoryAxis yAxis = new CategoryAxis(); + yAxis.setTickLabelFont(Font.font(8)); + setYAxisGamme(NoteGamme.gamme()); + NumberAxis xAxis = new NumberAxis(0, 10000,1000); + + //creating the chart + chart = new TraceChart(xAxis,yAxis); + chart.setAnimated(false); + chart.setVerticalGridLinesVisible(false); + chart.setHorizontalGridLinesVisible(false); + chart.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))); + + yAxisGamme = NoteGamme.gamme(); + + cursorRect = new Rectangle(); + cursorRect.setManaged(false); + //cursorRect.setFill(Color.RED.deriveColor(0, 1, 1, 0.5)); + cursorRect.setFill(Color.rgb(39,93,153)); + cursorRect.setWidth(1); + cursorRect.setHeight(215); + moveCursor(true); + + highlightedLine = new Rectangle(); + highlightedLine.setManaged(false); + highlightedLine.setFill((Color.GREEN.deriveColor(0,1,1,0.2))); + highlightedLine.setWidth(150); + highlightedLine.setHeight(4); + + //note = new Label(); + + pane = new StackPane(); + pane.getChildren().add(chart); + pane.getChildren().add(cursorRect); + pane.getChildren().add(highlightedLine); + //pane.getChildren().addNote(note); + + //when the window height changes, resize the cursor + pane.widthProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observableValue, Number oldHeight, Number newHeight) { + moveCursor(true); + //resizeHiglightedLine(); + } + }); + + //when the window height changes, resize the controls and replace the cursor + pane.heightProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observableValue, Number oldWidth, Number newWidth) { + resizeCursor(); + } + }); + + pane.setOnMouseMoved(new EventHandler() { + @Override + public void handle(MouseEvent event) { + highlightLine(selectTickLine(event)); + } + }); + + pane.setOnMousePressed(new EventHandler() { + @Override + public void handle(MouseEvent event) { + if(event.isPrimaryButtonDown()) + setFrame(event); + } + }); + + pane.setOnMouseDragged(new EventHandler() { + @Override + public void handle(MouseEvent event) { + if(event.isPrimaryButtonDown()) + setFrame(event); + } + }); + + setBoundsAndInc(); + setBufferWindow(); + + + return pane; + } + + private void setFrame(MouseEvent event){ + final NumberAxis xAxis = (NumberAxis)chart.getXAxis(); + final CategoryAxis yAxis = (CategoryAxis) chart.getYAxis(); + final double yAxisInScene = yAxis.localToScene(0, 0).getX() + yAxis.getWidth(); + final double mouseX = event.getX(); + final float timeToSet = xAxis.getValueForDisplay(mouseX - yAxisInScene).floatValue(); + + if(timeToSet >= xAxis.getLowerBound() && timeToSet <= boundMax) + PlayerControl.instance().setFrame(Instant.fromTime(timeToSet)); + + } + + private void highlightLine(String category) { + Platform.runLater(new Runnable() { + @Override + public void run() { + final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis(); + double tickPosition = yAxis.getDisplayPosition(category) + yAxis.localToScene(0,0).getY(); + //note.setText(category); + highlightedLine.setY(tickPosition - highlightedLine.getHeight()/2); + + resizeHiglightedLine(); + } + }); + } + + private void resizeHiglightedLine(){ + final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis(); + highlightedLine.setX(0); + highlightedLine.setWidth(pane.getWidth() > 0 ? pane.getWidth() : 100); + highlightedLine.setHeight(yAxis.getHeight()/categories.size()); + } + + private String selectTickLine(MouseEvent event){ + final CategoryAxis yAxis = (CategoryAxis)chart.getYAxis(); + + return yAxis.getValueForDisplay(event.getY() - yAxis.localToScene(0,0).getY()); + } + + /** + * Resize the cursor + */ + private void resizeCursor(){ + Platform.runLater(() -> { + final CategoryAxis yAxis = (CategoryAxis) chart.getYAxis(); + + //if yAxis is not totally initialized, move it correctly + // and prevent from setting height to 0 + if (yAxis.getHeight() == 0){ + cursorRect.setY(17); + moveCursor(true); + } else { + cursorRect.setY(yAxis.localToScene(0, 0).getY()); + cursorRect.setHeight(yAxis.getHeight()); + } + }); + } + + /** + * Move the cursor according to the frameMS + * @param locked Boolean indicating if the cursor has to be followed + */ + private void moveCursor(final Boolean locked){ + final NumberAxis xAxis = (NumberAxis) chart.getXAxis(); + final CategoryAxis yAxis = (CategoryAxis) chart.getYAxis(); + final double yAxisInScene = yAxis.localToScene(0, 0).getX() + yAxis.getWidth(); + + if (locked) { + final int interval = (int) (xAxis.getUpperBound() - xAxis.getLowerBound()); + //if frame greater than upper bound, move the window to the right + if (PlayerControl.frame().toMs() > xAxis.getUpperBound()) { + xAxis.setLowerBound(xAxis.getUpperBound()); + xAxis.setUpperBound(xAxis.getLowerBound() + interval); + setBufferWindow(); + } + //if frame lower than lower bound, move the window to the left + if (PlayerControl.frame().toMs() < 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 + public void run() { + cursorRect.setX((PlayerControl.frame().toMs() - xAxis.getLowerBound()) * xAxis.getScale() + yAxisInScene); + } + }); + + } + + public void setYAxisGamme(Gamme yAxisGamme){ + this.yAxisGamme = yAxisGamme; + Platform.runLater(new Runnable(){ + @Override + public void run() { + changeYAxis(); + state().invalidate(); + } + }); + } + + private void changeYAxis(){ + CategoryAxis yAxis = (CategoryAxis)chart.getYAxis(); + ArrayList categories = new ArrayList(); + + for(int i=0 ; isizeMS ? sizeMS : (int)xAxis.getUpperBound(); + final int boundMin = (int) xAxis.getLowerBound(); + inc = (boundMax - boundMin) > RESOLUTION ? Math.round((boundMax - boundMin) / (float)RESOLUTION) : 1; + } + + public void playerControlEvent(PlayerControlEvent e) { + if(!visibility()) + return; + + switch (e.getType()) { + case FRAME: + moveCursor(true); + break; +// case PLAY_STATE: +// switch(e.getPlayingState()) { +// case PLAYING: +// //Workaround to reload buffer to show if new values after +// // opening window +// // Is there a way to know if buffer finished loading ? +// setBufferWindow(); +// break; +// case STOPPED: +// /*NumberAxis xAxis = (NumberAxis)chart.getXAxis(); +// xAxis.setLowerBound(0); +// xAxis.setUpperBound(10000); +// setBufferWindow();*/ +// break; +// } + } + } + + @Override + protected AbstractBufferGraph getGenericGraph() { + return new SpectrogramView(); + } + + @Override + public Parent getMainNode() { + return pane; + } + + /** + * Revoie le liste des éléments du menu contextuel + * + * @param contextMenu + */ + @Override + public void createContextMenu(JPopupMenu contextMenu) { + JMenu choixAxesY = new JMenu("Axe fréquence"); + JRadioButtonMenuItem noteGamme = new JRadioButtonMenuItem("Gamme chromatique"); + noteGamme.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if(e.getStateChange()==ItemEvent.SELECTED) + setYAxisGamme(NoteGamme.gamme()); + } + }); + JRadioButtonMenuItem regularGamme = new JRadioButtonMenuItem("Gamme linéaire"); + regularGamme.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if(e.getStateChange()==ItemEvent.SELECTED) + setYAxisGamme(new RegularGamme(0f, 20000f, 1000)); + } + }); + ButtonGroup b = new ButtonGroup(); + b.add(noteGamme); + b.add(regularGamme); + choixAxesY.add(noteGamme); + choixAxesY.add(regularGamme); + + contextMenu.add(choixAxesY); + } + + @Override + protected void initView() { + createContent(); + cleanView(); + state().setSlowingFactorOnDataChange(5); + } + + @Override + public void updateView() { + + + XYChart.Series series = new XYChart.Series<>(); + final int MAX = 4000000; + NumberAxis xAxis = (NumberAxis)chart.getXAxis(); + + final int sizeMS = (int)PlayerControl.frame().fromIndex(buffer().size(),buffer()).toMs(); + boundMax = xAxis.getUpperBound()>sizeMS ? sizeMS : (int)xAxis.getUpperBound(); + + for (float j = (float)xAxis.getLowerBound() ; j < boundMax ; j+=inc) { + + if (buffer().get(j) != null) { + if (buffer().get(j).getGamme().gammeSize() != gammeSize) { + gammeSize = buffer().get(j).getGamme().gammeSize(); + } + + for (int i = 0; i < gammeSize; i++) { + + if (buffer().get(j).getGamme() != yAxisGamme) + buffer().get(j).castToGamme(yAxisGamme); + + if (buffer().get(j).getAmplitude(i) > 1000) { + + Float ampli =buffer().get(j).getAmplitude(i); + if(ampli == null) + ampli = 0f; + + series.getData().add(new XYChart.Data(j, categories.get(i), + new BarExtraValues(j + inc, colorRamp(ampli, MAX)))); + } + + } + } + } + Platform.runLater(new Runnable() { + @Override + public void run() { + chart.getData().set(0, series); + } + }); + + Platform.runLater(new Runnable() { + @Override + public void run() { + + } + }); + } + + @Override + public void cleanView() { + Platform.runLater(new Runnable() { + @Override + public void run() { + chart.setData(FXCollections.observableArrayList(new XYChart.Series())); + } + }); + } + + @Override + protected void visibilityChange(boolean isVisible) { + super.visibilityChange(isVisible); + if(isVisible==true) + setBufferWindow(); + } + + public Color colorRamp(float pos, float maxPos){ + if(pos == 0 || maxPos == 0) + return Color.hsb(0,0,1); + + pos = Math.min(pos, maxPos); + + float x = pos/maxPos; + x = x*x; + + if(x <= .08) { + x/=.08; + return Color.hsb(0, x*.45f, 1); + } + + x-=.08; + if(x <= .52) { + x/=.52; + return Color.hsb(1 - x * 360 + (x * 336), (1-x) * .45f + (x * .62f), 1 - x + (x * .75f)); + } + x-=.52; + x/=.4; + return Color.hsb(1-x *336 + (x*264), 1-x *.62f + (x*.100f), 1-x*.75f +(x*.53f)); + } +} diff --git a/src/gui/graphs/TraceChart.java b/src/gui/graphs/TraceChart.java new file mode 100644 index 0000000..c1abcf4 --- /dev/null +++ b/src/gui/graphs/TraceChart.java @@ -0,0 +1,227 @@ +package gui.graphs; + +/* + * Copyright (c) 2008, 2013 Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.chart.Axis; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.shape.Path; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Un graphe XY customisé pour afficher des barres de longueur varriable et de + * coordonnées variables. + * + * Adapté de CandleStickChart.java dans la démo Ensemble8 de javafx. + * Originellement définit comme : "A CandleChart is a style of candle-chart used primarily + * to describe price movements of a security, derivative, + * or currency over time. + * The Data Y value is used for the opening price and then the close, high and + * low values are stored in the Data's extra value property using a + * CandleExtraValues object." + * + * Modifié pour dessiner des barres horizontales plutôt que verticales et pour + * accepter un String comme donnée en Y + */ +public class TraceChart extends XYChart { + + // -------------- CONSTRUCTORS ---------------------------------------------- + /** + * Construct a new CandleStickChart with the given axis. + */ + public TraceChart(Axis xAxis, Axis yAxis) { + super(xAxis, yAxis); + setAnimated(false); + xAxis.setAnimated(false); + yAxis.setAnimated(false); + } + + /** + * Construct a new TraceChart with the given axis and data. + */ + public TraceChart(Axis xAxis, Axis yAxis, ObservableList> data) { + this(xAxis, yAxis); + setData(data); + } + + // -------------- METHODS ------------------------------------------------------------------------------------------ + /** Called to update and layout the content for the plot */ + @Override protected void layoutPlotChildren() { + // we have nothing to layout if no data is present + if (getData() == null) { + return; + } + // update candle positions + for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { + XYChart.Series series = getData().get(seriesIndex); + Iterator> iter = getDisplayedDataIterator(series); + Path seriesPath = null; + if (series.getNode() instanceof Path) { + seriesPath = (Path) series.getNode(); + seriesPath.getElements().clear(); + } + while (iter.hasNext()) { + XYChart.Data item = iter.next(); + double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item)); + double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item)); + Node itemNode = item.getNode(); + BarExtraValues extra = (BarExtraValues) item.getExtraValue(); + if (itemNode instanceof Bar && extra != null) { + Bar bar = (Bar) itemNode; + + double close = getXAxis().getDisplayPosition((extra.getClose())); + // calculate candle width + double barHeight = -1; + if (getYAxis() instanceof CategoryAxis) { + CategoryAxis yAxis = (CategoryAxis) getYAxis(); + barHeight = yAxis.getHeight()/yAxis.getCategories().size(); // no gap between ticks*/ + } + // update candle + bar.update(close - x, barHeight, extra.getColor()); + + // position the candle + bar.setLayoutX(x); + bar.setLayoutY(y); + } + } + } + } + + @Override protected void dataItemChanged(XYChart.Data item) { + } + + @Override protected void dataItemAdded(XYChart.Series series, int itemIndex, XYChart.Data item) { + Node bar = createBar(item); + getPlotChildren().add(bar); + // always draw average line on top + if (series.getNode() != null) { + series.getNode().toFront(); + } + } + + @Override protected void dataItemRemoved(XYChart.Data item, XYChart.Series series) { + final Node bar = item.getNode(); + getPlotChildren().remove(bar); + } + + @Override protected void seriesAdded(XYChart.Series series, int seriesIndex) { + // handle any data already in series + for (int j = 0; j < series.getData().size(); j++) { + XYChart.Data item = series.getData().get(j); + Node bar = createBar(item); + getPlotChildren().add(bar); + } + // create series path + Path seriesPath = new Path(); + //useless :) + //seriesPath.getStyleClass().setAll("candlestick-average-line", "series" + seriesIndex); + series.setNode(seriesPath); + getPlotChildren().add(seriesPath); + } + + @Override protected void seriesRemoved(XYChart.Series series) { + // remove all candle nodes + for (XYChart.Data d : series.getData()) { + final Node bar = d.getNode(); + getPlotChildren().remove(bar); + } + } + + /** + * Create a new Bar node to represent a single data item + */ + private Node createBar(final XYChart.Data item) { + Node bar = item.getNode(); + BarExtraValues extra = (BarExtraValues) item.getExtraValue(); + // check if bar has already been created + if (bar instanceof Bar) { + ((Bar) bar).setSeriesAndDataStyleClasses(extra.getColor()); + } else { + bar = new Bar(extra.getColor()); + item.setNode(bar); + } + return bar; + } + + + + /** + * This is called when the range has been invalidated and we need to update it. If the axis are auto + * ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the + * axis passing it that data. + */ + @Override + protected void updateAxisRange() { + // For candle stick chart we need to override this method as we need to let the axis know that they need to be able + // to cover the whole area occupied by the high to low range not just its center data value + final Axis xAxis = getXAxis(); + final Axis yAxis = getYAxis(); + List xData = null; + List yData = null; + if (xAxis.isAutoRanging()) { + xData = new ArrayList(); + } + if (yAxis.isAutoRanging()) { + yData = new ArrayList(); + } + if (xData != null || yData != null) { + for (XYChart.Series series : getData()) { + for (XYChart.Data data : series.getData()) { + if (yData != null) { + yData.add(data.getYValue()); + } + if (xData != null) { + BarExtraValues extras = (BarExtraValues) data.getExtraValue(); + if (extras != null) { + //yData.addNote(extras.getHigh()); + //yData.addNote(extras.getLow()); + } else { + xData.add(data.getXValue()); + } + } + } + } + if (xData != null) { + xAxis.invalidateRange(xData); + } + if (yData != null) { + yAxis.invalidateRange(yData); + } + } + } +} \ No newline at end of file diff --git a/src/gui/viewer_state/BufferViewerState.java b/src/gui/viewer_state/BufferViewerState.java new file mode 100644 index 0000000..7cb05b0 --- /dev/null +++ b/src/gui/viewer_state/BufferViewerState.java @@ -0,0 +1,152 @@ +package gui.viewer_state; + +import processing.buffer.Buffer; +import processing.buffer.BufferEvent; +import processing.buffer.BufferEventListener; + +/** + * Created by gaby on 09/04/14. + * + * Classe controlleur s'interposant entre les buffers et leur visualisateur, + * il gère les mises à jour différée dans le temps et les modifications de fenêtres du visualisateur + */ +public class BufferViewerState extends ViewerState implements BufferEventListener{ + + public enum Type{ + SINGLE_FRAME, + CONTINUOUS_WINDOW, + ALL_DATA, + NO_DATA + } + + final private Type type; + final private boolean bufferSizeSensible; + + private int idMin=0, idMax=0; + private Buffer buffer; + + + private BufferViewerState(Buffer buffer, Viewer viewer, Type type, boolean bufferSizeSensible){ + super(viewer); + this.type = type; + this.bufferSizeSensible = bufferSizeSensible; + this.buffer = buffer; + buffer.addBufferListener(this); + init(); + } + + static public BufferViewerState singleFrameWindow( Viewer viewer, Buffer buffer){ + return new BufferViewerState(buffer, viewer, Type.SINGLE_FRAME, false); + } + static public BufferViewerState continousWindow(Viewer viewer, Buffer buffer){ + return new BufferViewerState(buffer, viewer, Type.CONTINUOUS_WINDOW, false); + } + static public BufferViewerState allDataWindow(Viewer viewer, Buffer buffer){ + return new BufferViewerState(buffer, viewer, Type.ALL_DATA, true); + } + static public BufferViewerState sizeSensible(Viewer viewer, Buffer buffer){ + return new BufferViewerState(buffer, viewer, Type.NO_DATA, true); + } + + public void setWindow(int idMin, int idMax){ + this.idMax = idMax; + this.idMin = idMin; + + invalidate(); + } + public void setSingleFrame(int id){ + idMax = id; + idMin = id; + + invalidate(); + } + + + + public Type getType(){ + return type; + } + + public int getMaximumIndex(){ + if(type==Type.CONTINUOUS_WINDOW) + return idMax; + + return -1; + } + + public int getMinimumIndex(){ + if(type==Type.CONTINUOUS_WINDOW) + return idMin; + + return -1; + } + + public int getSingleIndex(){ + if(type == Type.SINGLE_FRAME) + return idMin; + + return -1; + } + + public boolean windowContains(int sampleID){ + switch (type){ + case NO_DATA: + return false; + case ALL_DATA: + return true; + case CONTINUOUS_WINDOW: + return sampleID >= idMin && sampleID <= idMax; + case SINGLE_FRAME: + return sampleID == idMin; + } + return false; + } + + @Override + public void bufferEvent(BufferEvent e) { + if (e.getType() == BufferEvent.Type.SAMPLE_ADDED || e.getType() == BufferEvent.Type.SAMPLE_CHANGED){ + if (windowContains(e.getSampleID())) + setState(State.UPDATE_NEEDED, slowingFactorOnDataChange()); + }else if(e.getType()== BufferEvent.Type.SAMPLE_DELETED){ + if(type==Type.SINGLE_FRAME) + if(e.getSampleID()==idMin) + + setState(State.CLEAN_NEEDED, slowingFactorOnDataChange()); + else if(windowContains(e.getSampleID())) + setState(State.UPDATE_NEEDED, slowingFactorOnDataChange()); + + }else if(e.getType()== BufferEvent.Type.INVALIDATE) + invalidate(); + + if(bufferSizeSensible&&(e.getType()== BufferEvent.Type.SAMPLE_ADDED||e.getType()== BufferEvent.Type.SAMPLE_DELETED)) + setState(State.UPDATE_NEEDED, slowingFactorOnDataChange()); + + } + + @Override + protected boolean canDraw() { + switch(type){ + case CONTINUOUS_WINDOW: + return idMin< buffer.size(); + case SINGLE_FRAME: + return buffer.get(idMin) != null; + } + return !buffer.isEmpty(); + } + + @Override + protected void visibilityChanged() { + if(visibility) + buffer.addBufferListener(this); + else + buffer.removeBufferListener(this); + } + + public Buffer getBuffer() { + return buffer; + } + + public boolean isSizeSensitive(){ + return bufferSizeSensible; + } +} diff --git a/src/gui/viewer_state/ObservableViewerState.java b/src/gui/viewer_state/ObservableViewerState.java new file mode 100644 index 0000000..bc4f362 --- /dev/null +++ b/src/gui/viewer_state/ObservableViewerState.java @@ -0,0 +1,57 @@ +package gui.viewer_state; + +import conteneurs.ObservableObject; +import conteneurs.ObservableObjectEvent; + +/** + * Created by gaby on 07/05/14. + * + * Classe controlleur s'interposant entre les objets observables et leur visualisateur, + * il gère les mises à jour différée dans le temps + */ +public class ObservableViewerState extends ViewerState implements ObservableObject.Listener { + ObservableObject observableObject; + + public ObservableViewerState(Viewer viewer, ObservableObject observableObject) { + super(viewer); + setObservableObject(observableObject); + init(); + } + + public ObservableViewerState(Viewer viewer){ + this(viewer, null); + } + + @Override + public void observableOjectEvent(ObservableObjectEvent e) { + setState(State.UPDATE_NEEDED, slowingFactorOnDataChange()); + } + + @Override + protected boolean canDraw() { + return observableObject!=null; + } + + public void setObservableObject(ObservableObject observableObject){ + if(this.observableObject!=null) + this.observableObject.removeListener(this); + + this.observableObject = observableObject; + if(this.observableObject!=null) + this.observableObject.addListener(this); + + invalidate(); + } + + public ObservableObject getObservableObject() { + return observableObject; + } + + @Override + protected void visibilityChanged() { + if(isVisible()){ + observableObject.addListener(this); + }else + observableObject.removeListener(this); + } +} diff --git a/src/gui/viewer_state/Viewer.java b/src/gui/viewer_state/Viewer.java new file mode 100644 index 0000000..e44d77c --- /dev/null +++ b/src/gui/viewer_state/Viewer.java @@ -0,0 +1,10 @@ +package gui.viewer_state; + +/** + * Created by gaby on 09/04/14. + * Interface définissant les visualisateurs + */ +public interface Viewer { + public void updateView(); + public void cleanView(); +} diff --git a/src/gui/viewer_state/ViewerState.java b/src/gui/viewer_state/ViewerState.java new file mode 100644 index 0000000..571a2ee --- /dev/null +++ b/src/gui/viewer_state/ViewerState.java @@ -0,0 +1,113 @@ +package gui.viewer_state; + +import processing.ProcessControl; + +/** + * Created by gaby on 06/05/14. + * Classe abstraite définissant les attributs communs au viewerState: relation avec les Graphics View + */ +public abstract class ViewerState { + + public enum State{ + UPDATE_NEEDED, + UPDATED, + CLEAN_NEEDED + } + private State state = State.CLEAN_NEEDED; + + final protected Viewer viewer; + protected boolean visibility; + private int slowingUpdate = 0; + private int slowingFactorOnDataChange = 0; + + protected ViewerState(Viewer viewer) { + this.viewer = viewer; + visibility = false; + } + + protected void init(){ + ProcessControl.instance().registerViewerState(this); + setVisibility(false); + } + + public void invalidate(){ + if(!canDraw()) + state = State.CLEAN_NEEDED; + else + state = State.UPDATE_NEEDED; + } + + public void applyState(){ + if(slowingUpdate>0) { + slowingUpdate--; + return; + }else if(slowingUpdate == -1) + slowingUpdate = 0; + + if(state == State.UPDATED) + return; + State sCopy = state; + state = State.UPDATED; + if(sCopy == State.UPDATE_NEEDED) + viewer.updateView(); + else if(sCopy== State.CLEAN_NEEDED) + viewer.cleanView(); + } + + public boolean isVisible() { + return visibility; + } + + public void setVisibility(boolean visibility) { + if(visibility==this.visibility) + return; + + this.visibility = visibility; + visibilityChanged(); + } + + public Viewer getViewer() { + return viewer; + } + + public State getState() { + return state; + } + + final public boolean setState(State state){ + return setState(state, 0); + } + + final public boolean setState(State state, int slowFactor){ + if(slowFactor!=0){ + if(slowFactor==-1) + slowingUpdate = -1; + else if(slowingUpdate==0) + slowingUpdate = slowFactor; + } + if(state == State.UPDATED) + return this.state == State.UPDATED; + + this.state = state; + return true; + } + + public void forceState(State state){ + this.state = state; + slowingUpdate = -1; + } + + abstract protected boolean canDraw(); + + protected void visibilityChanged(){ + + } + + public int slowingFactorOnDataChange() { + return slowingFactorOnDataChange; + } + + public void setSlowingFactorOnDataChange(int slowingFactorOnDataChange) { + this.slowingFactorOnDataChange = slowingFactorOnDataChange; + } +} \ No newline at end of file diff --git a/src/processing/AudioInputToBuffer.java b/src/processing/AudioInputToBuffer.java new file mode 100644 index 0000000..3ee1588 --- /dev/null +++ b/src/processing/AudioInputToBuffer.java @@ -0,0 +1,404 @@ +package processing; + +import processing.buffer.Storage; +import processing.buffer.TemporalBuffer; + +import javax.sound.sampled.*; +import java.io.File; +import java.io.IOException; + +/** + * Created by gaby on 27/03/14. + */ +public class AudioInputToBuffer implements Runnable{ + private TemporalBuffer buffer; + + private AudioInputStream input; + private Integer channel; + + private TargetDataLine line = null; + private File file = null; + + private String name = "Input"; + + private Thread thread; + private boolean paused = false; + private boolean running = false; + + + + /** + * Ce constructeur créé autant de buffer que de channel dans input. + * Les buffers auront une fréquence d'échantillonage par défaut de 44100 Hz. + * @param input flux audio d'entrée + */ + private AudioInputToBuffer(AudioInputStream input) { + channel = 0; + + this.input = input; + + init(44100); + } + + /** + * Ce constructeur créé un buffer alimenté par la chaine du flux correspondant à l'index en arguments + * Les buffers auront une fréquence d'échantillonage par défaut de 44100 Hz. + * @param input flux audio d'entrée + * @param channel index de la chaine + */ + private AudioInputToBuffer(AudioInputStream input, int channel) { + this(input, channel, 44100); + } + + + /** + * Ce constructeur créé autant de buffer que d'index valide dans la liste des chaines du flux + * De plus on force la fréquence d'échantillonage des buffers créés pour qu'elle soit égales à sampleRate + * @param input flux audio d'entrée + */ + private AudioInputToBuffer(AudioInputStream input, int channel, int sampleRate) { + this.channel = channel; + if(input.getFormat().getChannels()<=channel) + this.channel=0; + + this.input = input; + init(sampleRate); + } + + /** + * Génére un AudioInputToBuffer à partir d'une entrée audio du systeme + * (normalement le micro... mais honnêtement je ne peux rien certifier) + * @return l'AudioInputToBuffer + */ + static public AudioInputToBuffer fromMicro(){ + Mixer.Info[] mixersInfo = AudioSystem.getMixerInfo(); + for (Mixer.Info aMixersInfo : mixersInfo) { + Mixer mixer = AudioSystem.getMixer(aMixersInfo); + Line.Info[] linesInfo = mixer.getTargetLineInfo(); + for (Line.Info aLinesInfo : linesInfo) { + TargetDataLine l = null; + try { + l = (TargetDataLine) mixer.getLine(aLinesInfo); + } catch (LineUnavailableException e) { + e.printStackTrace(); + return null; + } + + + AudioInputToBuffer r = new AudioInputToBuffer(new AudioInputStream(l)); + r.line = l; + r.setName("Micro"); + + System.out.println(r.line.getFormat()); + return r; + } + } + + System.out.println("AudioInputStream.fromMicro(): Aucun port d'entrée trouvée"); + + return null; + } + + /** + * Génére un AudioInputToBuffer à partir d'une entrée audio du systeme + * (normalement le micro... mais honnêtement je ne peux rien certifier) + * @param channel Spécifie les chaines de l'entrée à utiliser + * @param sampleRate Spécifie la fréquence d'échantillonage à utiliser + * @return l'AudioInputToBuffer + */ + static public AudioInputToBuffer fromMicro(Integer channel, int sampleRate){ + Mixer.Info[] mixersInfo = AudioSystem.getMixerInfo(); + for (Mixer.Info aMixersInfo : mixersInfo) { + Mixer mixer = AudioSystem.getMixer(aMixersInfo); + Line.Info[] linesInfo = mixer.getTargetLineInfo(); + for (Line.Info aLinesInfo : linesInfo) { + if (mixer.isLineSupported(aLinesInfo)) { + TargetDataLine l = null; + try { + l = (TargetDataLine) mixer.getLine(aLinesInfo); + } catch (LineUnavailableException e) { + e.printStackTrace(); + return null; + } + AudioInputToBuffer r = new AudioInputToBuffer(new AudioInputStream(l), channel, sampleRate); + r.line = l; + r.setName("Micro"); + return r; + } + } + } + + System.out.println("AudioInputStream.fromMicro(): Aucun port d'entrée trouvée"); + + return null; + } + + + /** + * Génére un AudioInputToBuffer à partir d'un fichier audio + * (si bien sur le format est correct, fin en fait que si c'est du wav pour l'instant) + * @return l'AudioInputToBuffer + */ + static public AudioInputToBuffer fromFile(File file){ + try { + if(!file.canRead()){ + System.out.println("AudioInputToBuffer.fromFile(): Erreur fichier illisible"); + return null; + } + + AudioInputToBuffer a = new AudioInputToBuffer(AudioSystem.getAudioInputStream(file), 0); + a.setName(file.getName()); + a.file = file; + return a; + } catch (UnsupportedAudioFileException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Génére un AudioInputToBuffer à partir d'un fichier audio + * (si bien sur le format est correct, fin en fait que si c'est du wav pour l'instant) + * @param channel Spécifie les chaines de l'entrée à utiliser + * @param sampleRate Spécifie la fréquence d'échantillonage à utiliser + * @return l'AudioInputToBuffer + */ + static public AudioInputToBuffer fromFile(File file, Integer channel, int sampleRate){ + try { + AudioInputToBuffer a = new AudioInputToBuffer(AudioSystem.getAudioInputStream(file), channel, sampleRate); + a.setName(file.getName()); + a.file = file; + return a; + } catch (UnsupportedAudioFileException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + + + private void init(int sampleRate){ + //On reformate le flux audio pour qu'il soit en PCM_SIGNED, little-endian et avec un nombre de channel = channel + int sampleSizeBytes = input.getFormat().getSampleSizeInBits()/8; + + int channel = input.getFormat().getChannels(); + + this.input = AudioSystem.getAudioInputStream(new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sampleRate, sampleSizeBytes * 8, channel, sampleSizeBytes * channel, sampleRate * channel, false), input); + + buffer = new TemporalBuffer<>(Storage.Type.ArrayList, sampleRate); + buffer.setName(name); + buffer.setType(Float.class); + buffer.setProcessingOrder(processing.processes.Process.ProcessingOrder.ASCENDING_ORDER); + + thread = new Thread(this); + } + + + /** + * Démarre l'écriture dans le buffer + * @return vrai si elle n'était pas déjà lancée + */ + public boolean start(){ + + if(paused) { + paused = false; + return true; + } + + if(thread.isAlive()) + return false; + + thread = new Thread(this); + + if(line != null){ + line.flush(); + try { + line.open(); + } catch (LineUnavailableException e) { + e.printStackTrace(); + } + line.start(); + } + + getBuffer().tryEnsureCapacity((int)input.getFrameLength()); + + paused = false; + running = true; + thread.start(); + return true; + } + + /** + * Mets l'écriture en pause, si la lecture est en temps réel les données sont perdues + * @return vrai si l'écriture était lancée + */ + public boolean pause(){ + if(!thread.isAlive()||paused) + return false; + + if(line != null) + line.stop(); + + paused = true; + return true; + } + + /** + * Stoppe l'écriture dans le buffer, ferme le port d'entrée si la lecture était en temps réel. + * @return + */ + public boolean interrupt(){ + if(!thread.isAlive()) + return false; + + thread.interrupt(); + running = false; + return true; + } + + private void clean(){ + if(line != null){ + line.stop(); + line.close(); + } + + buffer.lastSampleAdded(); + System.out.println("Fin de l'enregistrement, à vous les studios!!!"); + } + + public boolean isAlive(){ + return (!thread.isAlive()) || paused; + } + + @Override + public void run() { + final int NBR_CHANNEL = input.getFormat().getChannels(); + final int NBR_BYTES = input.getFormat().getSampleSizeInBits()/8; + + while(running){ + + try { + + + byte[] data = new byte[NBR_CHANNEL*NBR_BYTES]; + + //On récupère dans data une frame du signal tant que l'on peut, ensuite, + // pour chaque buffer de sortie on recompose le float pour chaque buffer de la channel correspondante + while(input.read(data,0, NBR_CHANNEL*NBR_BYTES)!=-1 && data!=null && !paused && running){ + + buffer.add(floatFromByteArray(data, channel*NBR_BYTES,NBR_BYTES)); + + try { + Thread.sleep(0); + } catch (InterruptedException e) { + clean(); + return; + } + + } + + if(input.getFrameLength()!= AudioSystem.NOT_SPECIFIED && input.getFrameLength() == buffer.size()) { + clean(); + return; + } + + } catch (IOException e) { + e.printStackTrace(); + } + + do{ + try { + Thread.sleep(300); + } catch (InterruptedException e) { + clean(); + return; + } + }while(paused&&!running); + + + } + } + + /** + * Renvoie la valeur du flaot correspondant à une séquence d'octet + * @param b tableau d'octets + * @param offset indice du premier octet à traiter + * @param nbByte nombre d'octet formant le float + * @return la valeur en float de la séquence d'octet + */ + private static float floatFromByteArray(byte[] b, int offset, int nbByte){ + + if(nbByte>4||nbByte<1||nbByte>offset+b.length){ + System.out.println("Erreur de décodage byte à byte"); + return 0; + } + + switch(nbByte){ + case 1: + return (b[offset] & 0xFF); + case 2: + return (b[offset] & 0xFF) | ((b[1+offset]) << 8); + case 3: + return (b[offset] & 0xFF) | ((b[1+offset] & 0xFF) << 8) | ((b[2+offset]) << 16); + case 4: + return (b[offset] & 0xFF) | ((b[1+offset] & 0xFF) << 8) | ((b[2+offset] & 0xFF) << 16) | (b[3+offset] << 24); + + } + + return 0; + } + + + /** + * Renvoie le buffer alimenté + * @return Le buffer + */ + public TemporalBuffer getBuffer() { + return buffer; + } + + public void setBuffer(TemporalBuffer buffer){ + this.buffer = buffer; + buffer.setName(name); + buffer.setType(Float.class); + buffer.invalidateProcessedSamples(); + } + + /** + * Renvoie le flux d'entrée + * @return AudioInputStream qui alimente les buffers + */ + public AudioInputStream getInput() { + return input; + } + + /** + * Renvoie le numéro de la chaîne du flux d'entrée utilisée pour alimenté le buffer + * @return Numéro de la chaîne (première chaîne = 0) + */ + public int getChannel() { + return channel; + } + + /** + * Renvoie l'état du processus + * @return Vrai si le processus est lancé + */ + public boolean isRunning(){ + return thread.isAlive(); + } + + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } +} diff --git a/src/processing/BufferPlayer.java b/src/processing/BufferPlayer.java new file mode 100644 index 0000000..dbc7bfa --- /dev/null +++ b/src/processing/BufferPlayer.java @@ -0,0 +1,201 @@ +package processing; + +import generictools.Instant; +import gui.PlayerControl; +import processing.buffer.StorageIterator; +import processing.buffer.TemporalBuffer; + +import javax.sound.sampled.*; + +/** + * Created by gaby on 16/04/14. + */ +public class BufferPlayer implements Runnable{ + TemporalBuffer buffer; + SourceDataLine line; + int currentFrame = 0; + + StorageIterator currentSample; + + boolean muted = false; + float volume = 1; + + int iTest = 0; + + Thread thread = new Thread(this); + boolean running = false; + final float CLOCK_PERIOD = .05f; + + public BufferPlayer(TemporalBuffer buffer) { + this.buffer = buffer; + DataLine.Info info = new DataLine.Info(SourceDataLine.class, new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, buffer.getSampleRate(),16,1,2,44100, false), (int)(CLOCK_PERIOD*44100*2)); + Mixer m = AudioSystem.getMixer(AudioSystem.getMixerInfo()[0]); + + for(Line.Info i : m.getSourceLineInfo()){ + System.out.println(i); + } + + + try { + line = (SourceDataLine)AudioSystem.getLine(info); + line.open(); + } catch (LineUnavailableException e) { + e.printStackTrace(); + } + + currentSample = buffer.getIterator(); + } + + /** + * Démarre le lecteur de buffer + * @return Renvoie vrai si il n'était pas lancé + */ + public boolean play(){ + + if(thread.isAlive()) + return false; + + line.flush(); + if(!line.isActive()) + line.start(); + + thread = new Thread(this, "BufferPlayer"); + thread.setPriority(Thread.MAX_PRIORITY); + running = true; + thread.start(); + return true; + } + + /** + * Mets en pause le lecteur + * @return + */ + public boolean pause(){ + running = false; + thread.interrupt(); + return true; + } + + /** + * Modifie la frame actuelle + * @param frame + */ + public void setFrame(Instant frame){ + currentFrame = frame.mapToIndex(buffer); + currentSample = buffer.getIterator(frame.mapToIndex(buffer)); + PlayerControl.instance().frameChanged(frame); + } + + private void clean(){ + running = false; + line.stop(); + } + + public static byte [] float2ByteArray (float value) + { + + int v = Math.round(value); + return new byte[]{ + (byte) (v & 0xFF), + (byte) ((v >>> 8) & 0xFF) + }; + } + + public Instant getCurrentFrame(){ + return Instant.fromIndex(currentFrame, buffer); + } + + /** + * Retourne l'état muet du lecteur + * @return Renvoie vrai si muet + */ + public boolean isMuted() { + return muted; + } + + /** + * Modifie l'état muet du lecteur + * @param muted + */ + public void setMuted(boolean muted) { + this.muted = muted; + } + + public float getVolume() { + return volume; + } + + public void setVolume(float volume) { + if(volume==0) + setMuted(true); + else + setMuted(false); + + this.volume = volume; + } + + /** + * When an object implementing interface Runnable is used + * to create a thread, starting the thread causes the object's + * run method to be called in that separately executing + * thread. + *

+ * The general contract of the method run is that it may + * take any action whatsoever. + * + * @see Thread#run() + */ + @Override + public void run() { + boolean catchBack = false; + while(!thread.interrupted()&&running) { + long time = System.currentTimeMillis(); + if (currentFrame >= ProcessControl.instance().getMainInput().getBuffer().size()) { + PlayerControl.instance().stop(); + return; + } + int lastI = (int) (buffer.getSampleRate() * CLOCK_PERIOD); + + + float k = 1; + if (!muted && currentSample.hasNext()) { + if (catchBack) { + k = 1.5f; + } + + for (int i = 0; i < lastI*k; i++) { + if (currentSample.hasNext()) + line.write(float2ByteArray(currentSample.next() * volume), 0, 2); + else { + byte[] nullBytes = new byte[(int)(lastI*k- i) * 2]; + line.write(nullBytes, 0, nullBytes.length); + break; + } + + } + } + + currentFrame += lastI*k; + + + PlayerControl.instance().frameChanged(Instant.fromIndex(currentFrame, buffer)); + + time-=System.currentTimeMillis(); + + try { + if((int)(CLOCK_PERIOD*1000)+time>0) { + thread.sleep((int) (CLOCK_PERIOD * 1000) + time); + catchBack = false; + } + else { + thread.sleep(0); + catchBack = true; + } + } catch (InterruptedException e) { + clean(); + return; + } + } + clean(); + } +} diff --git a/src/processing/PostProcessScript.java b/src/processing/PostProcessScript.java new file mode 100644 index 0000000..361121e --- /dev/null +++ b/src/processing/PostProcessScript.java @@ -0,0 +1,218 @@ +package processing; + +import conteneurs.Note; +import generictools.Instant; +import generictools.Strings; +import processing.buffer.NoteBuffer; +import processing.properties.FloatProperty; + +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.ArrayList; + +/**Created by gaby on 03/06/14. + * Script qui sert à l'exportation vers Lilypond. Regroupe tous les algo de post-traitement nécéssaires à cette exportation. */ +public class PostProcessScript implements Runnable{ + + private NoteBuffer buffer; + private float tolerance = 0.08f; + private float progress = 0; + Runnable callBack; + + Thread thread = null; + private static PostProcessScript script = null; + + private PostProcessScript(NoteBuffer buffer, Runnable callBack){ + this.buffer = buffer; + this.callBack = callBack; + } + + /** + * Lance le script + * @param buffer + * @param callBack + */ + public static void launchScript(NoteBuffer buffer, Runnable callBack){ + interrupt(); + PostProcessScript script = new PostProcessScript(buffer, callBack); + script.thread = new Thread(script); + script.thread.start(); + script = null; + } + + public static void interrupt(){ + if(script!=null && script.thread!=null && script.thread.isAlive()) + script.thread.interrupt(); + script = null; + } + + @Override + public void run() { + unifyTempo(); + +// System.out.println("NOTES:"); +// +// for (int i = 0; i < buffer.getNotesNbr(); i++) { +// System.out.println("\t" + buffer.getNote(i).getNoteName() + " (" + buffer.getNote(i).getUnifiedDuration() + ")"); +// } + + JFileChooser jfc = new JFileChooser(); + jfc.setDialogTitle("Choix du dossier de sortie"); + jfc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int ret = jfc.showOpenDialog(null); + if(ret != JFileChooser.APPROVE_OPTION) + return; + + File file = new File(jfc.getSelectedFile().getPath()+"/exportLilypond.txt"); + System.out.println(jfc.getSelectedFile().getPath()+"/exportLilypond.txt"); + try { + if(!file.exists()) + file.createNewFile(); + FileWriter fileWriter = new FileWriter(file); + fileWriter.write(toLilyPond()); + fileWriter.close(); + } catch (IOException e) { + e.printStackTrace(); + return; + } + + if(callBack!=null) + try { + callBack.run(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private String toLilyPond(){ + String r = "<< {"; + for (int i = 0; i < buffer.getNotesNbr(); i++) { + r+=buffer.getNote(i).getEnglishNoteName() + buffer.getNote(i).getUnifiedDuration() + " "; + } + r+="} >>"; + return r; + } + + /** Cherche le tempo du morceau en se basant sur les écarts entre les notes. + * A partir de la, il donne a chaque note une duree (ronde, blanche noire, croche, etc) et permet de trouver le rythme */ + private void unifyTempo(){ + ArrayList listEcarts = calculeEcarts(bufferSortOut(buffer)); + ArrayList EcartsReference = new ArrayList<>(); + ArrayList nEcartParType = new ArrayList<>(); + + + //Cr�ation des cat�gories de r�f�rence, tri�es par ordre croissant + + EcartsReference.add(listEcarts.get(0)); + nEcartParType.add(1); + for (int i = 1; i < listEcarts.size(); i++) { + float currentEcart = listEcarts.get(i); + boolean newCategoryNeeded = true; + for (int j = 0; j < EcartsReference.size(); j++) { + //Si on est dans la marge de tolerance d�finie en param�tre, on red�finit la r�f�rence gr�ce � une moyenne pond�r�e + if(currentEcart>((1-tolerance)*EcartsReference.get(j)) && currentEcart<((1+tolerance)*EcartsReference.get(j))){ + EcartsReference.set(j, ( (nEcartParType.get(j)*EcartsReference.get(j))+ currentEcart) / (nEcartParType.get(j)+1) ); + nEcartParType.set(j, nEcartParType.get(j)+1); + newCategoryNeeded = false; + } + } + //Sinon cr�ation d'une nouvelle cat�gorie si aucune existante ne convient + if(newCategoryNeeded){ + int j = 0; + while(j à une ronde + ArrayList UnifiedEcarts = new ArrayList<>(EcartsReference.size()); + int indexEcartMostUsed = 0; + int maxUsed = 0; + for (int i = 0; i < nEcartParType.size(); i++) { + if( nEcartParType.get(i)> maxUsed){ + maxUsed = nEcartParType.get(i); + indexEcartMostUsed = i; + } + } + for (int i = 0; i < nEcartParType.size(); i++) { + UnifiedEcarts.add("0"); + } + + //Cas où on se retrouve avec le max plus grand qu'une ronde + while(indexEcartMostUsed < EcartsReference.size()-1 && EcartsReference.get(indexEcartMostUsed)*(4+tolerance)>EcartsReference.get(EcartsReference.size()-1)){ + indexEcartMostUsed++; + } + + + + + + + //Donne à chaque note une durée unified + for (int i = 0; i < buffer.getNotesNbr(); i++) { + double fac = Math.log(EcartsReference.get(indexEcartMostUsed)*4/ buffer.getNote(i).getDuration())/Math.log(2); + buffer.getNote(i).setUnifiedDuration(String.valueOf(Math.round(Math.pow(2, (int) fac))) + (fac % 1 < 0.5 ? "." : "")); + } + + + System.out.println("ECARTS:"); + + for (int i = 0; i < UnifiedEcarts.size(); i++) { + System.out.println("\t" + EcartsReference.get(i) + " (" + UnifiedEcarts.get(i) + ")"); + } + } + + /** Trie les instants de d�part des notes dans l'ordre croissant + * @param buffer le buffer dont les instants sont � trier */ + private ArrayList bufferSortOut(NoteBuffer buffer){ + ArrayList tmp = new ArrayList<>(); + ArrayList res = new ArrayList<>(); + // On r�cup�re tous les instans de d�part + for (int i = 0; i < buffer.getNotesNbr(); i++) { + tmp.add(buffer.getNote(i).getStart()); + } + + //Tri par ordre croissant + res.add(tmp.get(0)); + for (int i = 1; i < tmp.size(); i++) { + int j = 0; + while(j calculeEcarts(ArrayList instantList){ + ArrayList res = new ArrayList<>(); + for (int i = 1; i < instantList.size(); i++) { + res.add(instantList.get(i).substract(instantList.get(i - 1))); + } + return res; + } + + private boolean updateProgress(float progress){ + try { + Thread.sleep(0); + } catch (InterruptedException e) { + return true; + } + this.progress = progress; + return false; + } + + public static float getProgress(){ + if(script == null) + return -1; + return script.progress; + } +} diff --git a/src/processing/ProcessControl.java b/src/processing/ProcessControl.java new file mode 100644 index 0000000..311eaf4 --- /dev/null +++ b/src/processing/ProcessControl.java @@ -0,0 +1,429 @@ +package processing; + + +import conteneurs.NoteEventList; +import conteneurs.Spectre; +import generictools.Instant; +import generictools.Pair; +import gui.PlayerControl; +import gui.ProcessSumUp; +import gui.viewer_state.ViewerState; +import processing.buffer.Buffer; +import processing.buffer.NoteBuffer; +import processing.buffer.TemporalBuffer; +import processing.processes.*; +import processing.processes.FreqEdgeDetector; +import processing.processes.Process; + +import java.util.ArrayList; + +/** + * Classe centrale pour le traitement et l'affichage: + * + * Elle est chargé non seulement de lancer, controler et stopper le traitement dans sa globalité, + * mais elle contient aussi le thread d'affichage qui déclenche, au maximum 10 fois par secondes + * la mise à jour des graphs quand leur buffer a été modifié. + */ +public class ProcessControl implements Runnable { + + private final ArrayList> processes = new ArrayList<>(); + private final ArrayList> inputs= new ArrayList<>(); + private int mainInput=-1; + private final ArrayList buffers = new ArrayList<>(); + + private final ArrayList viewerStates = new ArrayList<>(); + + private ProcessSumUp processSumUp; + private long timeMs=0; + + private final Thread thread = new Thread(this, "GUI Thread"); + + + static private final ProcessControl p = new ProcessControl(); + + /** + * Retourne l'instance du singleton de ProcessControl + * @return Singleton de ProcessControl + */ + static public ProcessControl instance(){return p;} + + private ProcessControl(){ + init(); + } + + + /** + * Initialise les processus + */ + public void init(){ + mainInput = addInput(AudioInputToBuffer.fromMicro()); + addProcess(new FFT(getMainInput().getBuffer(), 50)); + addProcess(new FreqEdgeDetector((TemporalBuffer)processes.get(0).first().output(),15,12)); + addProcess(new FreqEdgeToEvent((TemporalBuffer)processes.get(1).first().output(), 7, .97f)); + addProcess(new EventToNote((TemporalBuffer)processes.get(2).first().output())); + thread.setPriority(Thread.MIN_PRIORITY); + thread.start(); + + } + + /** + * Démarre le controleur: lance le thread d'interface et ceux des différents process et input... + */ + public void start(){ + for(Pair p: processes) + p.first().start(); + + record(); + } + + /** + * Lance l'enregistrement seul: l'état des process n'est pas modifié, seul les inputs sont démarrés + */ + public void record(){ + for(Pair b: inputs) + b.first().start(); + } + + /** + * Mets en pause les process et les inputs + */ + public void pause(){ + for(Pair p: processes) + p.first().pause(); + for(Pair b: inputs) + b.first().pause(); + + } + + /** + * Stoppe l'enregistrement seul: l'état des process n'est pas modifié, seul les inputs sont mis en pause + */ + public void stopRecord(){ + for(Pair b: inputs) + b.first().interrupt(); + } + + /** + * Stoppe tout les les process et les inputs + */ + public void stop(){ + stopRecord(); + for(Pair p: processes) + p.first().interrupt(); + } + + /** + * Tue tous les process, les inputs et le thread d'interface graphique + */ + public void kill(){ + + stop(); + PostProcessScript.interrupt(); + PlayerControl.instance().stop(); + thread.interrupt(); + } + + /** + * Réinitialise le controleur à sont état initial + */ + public void reset(){ + + stop(); + + + /* + inputs.clear(); + processes.clear(); + buffers.clear(); + + viewerStates.clear(); + + init(); + + PlayerControl.instance().setBuffer(getMainInput().getBuffer()); + */ + + for (Buffer b: buffers) + b.clear(); + + PlayerControl.instance().setFrame(Instant.fromIndex(0,44100)); + + } + + /** + * Démarre le post-processing (l'exportation vers lilypond + */ + public void launchPostProcess(){ + System.out.println("POST PROCESS!!!!!"); + stop(); + + PostProcessScript.launchScript((NoteBuffer) processes.get(3).first().output(), new Runnable() { + @Override + public void run() { + System.out.println("PostProcess finished"); + } + }); + } + + /** + * @return Vrai si il existe au moins un process ou input qui est toujours en traitement + */ + public boolean isRunning(){ + for(Pair p: processes) + if(!p.first().isRunning()) + return false; + for(Pair b: inputs) + if(!b.first().isRunning()) + return false; + return true; + } + + /** + * @return Vrai si tout les process sont en attente: les inputs bloquent le traitement + */ + public boolean isWaiting(){ + for(Pair p: processes) + if(!p.first().isWaiting()) + return false; + + return true; + } + + /** + * Renvoie un process utilisé + * @param id identifiant du process à récuperer + * @return le process si il a été trouvé, null sinon. + */ + public Process getProcess(int id){ + if(id>=processes.size()||id<0) + return null; + return processes.get(id).first(); + } + + /** + * Renvoie le process portant le nom en argument + * @param name nom du process à récuperer + * @return le process si il a été trouvé, null sinon. + */ + public Process getProcess(String name){ + for(Pair p:processes){ + if(p.first().name().equals(name)) + return p.first(); + } + return null; + } + + /** + * Renvoie le nombre de processus + * @return + */ + public int getProcessNbr(){ + return processes.size(); + } + + /** + * Renvoie l'index du buffer associé à un process + * @param processID Index correspondant au process + * @return L"index du buffer + */ + public Integer getProcessBufferID(int processID) { + if(processID>=processes.size()||processID<0) + return null; + return processes.get(processID).second(); + + } + + + /** + * Renvoie un input + * @param id identifiant de l'input à récuperer + * @return l'input si il a été trouvé, null sinon. + */ + public AudioInputToBuffer getInput(int id){ + if(id>=inputs.size()||id<0) + return null; + return inputs.get(id).first(); + } + + /** + * Renvoie le nombre d'inputs + * @return + */ + public int getInputNbr() { + return inputs.size(); + } + + /** + * Renvoie l'index du buffer associé à un input + * @param inputID Index correspondant à l'input + * @return L"index du buffer + */ + public Integer getInputBufferID(int inputID) { + if(inputID>=inputs.size()||inputID<0) + return null; + return inputs.get(inputID).second(); + + } + + /** + * Renvoie l'input principal + * @return + */ + public AudioInputToBuffer getMainInput() { + return inputs.get(mainInput).first(); + } + + public int getMainInputID(){ + return mainInput; + } + + + public int getBufferNbr(){ + return buffers.size(); + } + + /** + * Renvoie un buffer + * @param id identifiant du buffer à récuperer + * @return le buffer si il a été trouvé, null sinon. + */ + public Buffer getBuffer(int id){ + if(id>=buffers.size()||id<0) + return null; + return buffers.get(id); + } + + /** + * Renvoie l'index d'un buffer + * @param b buffer à chercher + * @return index de la première occurence ( et normalement la seule) du buffer dans la liste + */ + public int getBufferIndex(Buffer b){ + return buffers.indexOf(b); + } + + + /** + * Ajout d'un process à la liste des processus, son buffer de sortie est enregistrer et ajouté à la liste des buffers + * @param p Le process à ajouter + * @return l'index sur lequel il a été ajouté + */ + int addProcess(Process p){ + for(Pair i:processes) + if(i.first()==p) + return -1; + + processes.add(new Pair<>(p, addBuffer(p.output()))); + + updateProcessesView(); + + return processes.size()-1; + } + + /** + * Ajout d'un input à la liste des processus, son buffer de sortie est enregistrer et ajouté à la liste des buffers + * @param p L'input à ajouter + * @return l'index sur lequel il a été ajouté + */ + int addInput(AudioInputToBuffer p){ + for(Pair i:inputs) + if(i.first()==p) + return -1; + + + inputs.add(new Pair<>(p, addBuffer(p.getBuffer())) ); + + updateProcessesView(); + + return inputs.size()-1; + } + + private int addBuffer(Buffer b){ + buffers.add(b); + return buffers.size()-1; + } + + /** + * Modifie une input, en reconnectant les vues et les process qui utilisaient la précédante + * @param inputID index de l'input à remplacer + * @param newInput nouvelle input + * @return Vrai si l'opération c'est bien déroulée + */ + public boolean setInput(int inputID, AudioInputToBuffer newInput){ + if(newInput==null) + return false; + for(Pair i:inputs) + if(i.first()==newInput) + return false; + + if(inputID>= inputs.size()) + return false; + + boolean inputStarted = inputs.get(inputID).first().isAlive(); + inputs.get(inputID).first().interrupt(); + + newInput.setBuffer(inputs.get(inputID).first().getBuffer()); + + + + inputs.get(inputID).setFirst(newInput); + + + updateProcessesView(); + + return true; + } + + /** + * Enregistre la fenetre récapitulative des traitements pour pouvoir déclancher sa mise à jour + * @param pV fenêtre récapitulative + */ + public void setProcessesView(ProcessSumUp pV) { + this.processSumUp = pV; + processSumUp.updateView(); + } + + /** + * Déclanche la mise à jour de la fenêtre récapitulative des traitements + */ + private void updateProcessesView(){ + if(processSumUp !=null) + processSumUp.updateView(); + } + + /** + * Enregistre un viewerState pour qu'il soit pris en compte dans le thread d'interface + * @param viewerState ViewerState à enregistrer + * @return Vrai si il n'était pas déjà ajouté + */ + public boolean registerViewerState(ViewerState viewerState){ + if(viewerStates.contains(viewerState)) + return false; + + viewerStates.add(viewerState); + viewerState.invalidate(); + return true; + } + + public void run() { + while (!Thread.interrupted()) { + + for (ViewerState b : viewerStates) + if (b.isVisible()) + b.applyState(); + + if (processSumUp != null) { + timeMs = System.currentTimeMillis() - timeMs; + processSumUp.update(1f / timeMs * 1000f); + timeMs = System.currentTimeMillis(); + } + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + return; + } + + } + } +} diff --git a/src/processing/SpectreFilter.java b/src/processing/SpectreFilter.java new file mode 100644 index 0000000..591de0f --- /dev/null +++ b/src/processing/SpectreFilter.java @@ -0,0 +1,102 @@ +package processing; + +import conteneurs.Gamme; +import conteneurs.Spectre; + +/** + * Created by gaby on 27/04/14. + */ +public abstract class SpectreFilter { + + protected SpectreFilter previousFilter = null; + protected float filterIntensity = 1; + protected boolean autoIntensity = true; + + public SpectreFilter(){ + init(); + } + + protected void init(){} + + /** + * Renvoie l'amplitude filtré en fonction de l'indice de la fréquence dans le spectre et du spectre + * Cette méthode peut être hérité pour définir un filtre propre à chaque fréquence + * @param idFreq l'index associé à la fréquence que l'on veut filtrer + * @param spectre spectre d'entrée + * @return amplitude filtrée + */ + protected abstract float filter(int idFreq, Spectre spectre); + + + /** + * Renvoie l'amplitude filtré en fonction de l'indice de la fréquence dans le spectre et du spectre + * @param idFreq l'index associé à la fréquence que l'on veut filtrer + * @param spectre spectre du signal + * @return l'amplitude de la fréquence filtré à travers la chaine de filtre + */ + public float filterAmpli(int idFreq, Spectre spectre){ + if(previousFilter==null) + return autoIntensity?filterIntensity*filter(idFreq,spectre)+(1-filterIntensity)*spectre.getAmplitude(idFreq)/2:filter(idFreq,spectre); + + return filter(idFreq, previousFilter.filterSpectre(spectre)); + } + + + /** + * Renvoie le spectre filtré à travers la chaine de filtre du premier maillon jusqu'à celui-ci + * @param spectre spectre d'entrée + * @return spectre filtré à travers la chaine + */ + public Spectre filterSpectre(Spectre spectre){ + Spectre r = new Spectre(spectre.getGamme()); + + Gamme gamme = spectre.getGamme(); + if(previousFilter!=null) + spectre = previousFilter.filterSpectre(spectre); + + for (int i = 0; i < gamme.gammeSize(); i++) + r.setAmplitudeAt(i, autoIntensity?filterIntensity*filter(i,spectre)+(1-filterIntensity)*spectre.getAmplitude(i)/2:filter(i,spectre)); + + return r; + } + + /** + * Chaine un filtre à celui-ci, le filtre chainer sera executé après ce filtre + * @param filter le filtre à ajouter à la chaine de traitement + * @return la chaine de filtre + */ + public SpectreFilter chainFilter(SpectreFilter filter){ + filter.previousFilter = this; + return filter; + } + + + /** + * Coupe la chaine à ce filtre il est maintenant le premier filtre à être executé lors du calcul de la chaine + * @return Vrai si il n'était pas déjà le premier maillon + */ + public boolean unchainFilter(){ + if(previousFilter==null) + return false; + + previousFilter = null; + return true; + } + + /** + * Renvoie le maillon précédant dans la chaine + * @return + */ + public SpectreFilter getPreviousFilter(){ + return previousFilter; + } + + public float getFilterIntensity() { + return filterIntensity; + } + + public SpectreFilter setIntensity(float filterIntensity) { + this.filterIntensity = filterIntensity; + return this; + } +} diff --git a/src/processing/SpectreFilterFactory.java b/src/processing/SpectreFilterFactory.java new file mode 100644 index 0000000..9665ddf --- /dev/null +++ b/src/processing/SpectreFilterFactory.java @@ -0,0 +1,25 @@ +package processing; + +import conteneurs.Spectre; + +/** + * Created by gaby on 29/04/14. + */ +public class SpectreFilterFactory { + static public SpectreFilter hearingCorrection(){ + return new SpectreFilter() { + @Override + protected void init() { + autoIntensity = false; + } + + @Override + protected float filter(int idFreq, Spectre spectre) { + float f = spectre.getGamme().getFreq(idFreq)/1000f; + float e = f-3.3f; + return Spectre.dBToAmpli(spectre.getdBAmplitude(idFreq)-filterIntensity*(3.64f*(float)Math.pow(f,-.08f)-6.5f*(float)Math.exp(-.06*e*e)+(float)Math.pow(10f,-3f*f*f*f*f))); + } + }; + } + +} diff --git a/src/processing/buffer/Buffer.java b/src/processing/buffer/Buffer.java new file mode 100644 index 0000000..574cba9 --- /dev/null +++ b/src/processing/buffer/Buffer.java @@ -0,0 +1,345 @@ +package processing.buffer; + +import conteneurs.ObservableObject; +import conteneurs.ObservableObjectEvent; +import processing.processes.Process; + +import javax.swing.*; +import java.util.ArrayList; + +public class Buffer { + + private final ArrayList bufferListeners; + private final ArrayList> usedBy; + final private Storage.Type arrayType; + private String name = ""; + private Class type = null; + private boolean itemObservable = false; + private Process.ProcessingOrder processingOrder = Process.ProcessingOrder.NO_ORDER; + + private Storage samples; + private final StorageArrayList updateIDs; + + /** + * Construit une instance de buffer selon un modéle de stockage + * @param arrayType Modèle de stockage + */ + public Buffer(Storage.Type arrayType) { + switch(arrayType){ + case ArrayList: + samples = new StorageArrayList<>(); + break; + } + + updateIDs = new StorageArrayList<>(); + + bufferListeners = new ArrayList<>(); + usedBy = new ArrayList<>(); + this.arrayType = arrayType; + } + + private void emitBufferEvent(BufferEvent event) { + + for(Process p: usedBy) + p.addBufferEvent(event); + + SwingUtilities.invokeLater(() -> { + for(BufferEventListener p: bufferListeners) + p.bufferEvent(event); + }); + + } + private void emitBufferEvent(BufferEvent.Type type, int sampleID) { + emitBufferEvent(new BufferEvent(type, sampleID, this)); + + } + + + /** + * Connecte ce buffer à un listener qu'il fournit, de manière à propager les BufferEvent jusqu'à cet objet + * @param p objet à connecter + * @return Vrai si l'opération à fontionner (si le process n'était pas déjà ajouté) + */ + public boolean addBufferListener(BufferEventListener p){ + if(bufferListeners.contains(p)) + return false; + + bufferListeners.add(p); + return true; + } + + /** + * Déconnecte ce buffer d'un listener + * @param p listener à déconnecter + * @return Vrai si l'opération à fontionner (si le listener était dans la liste) + */ + public boolean removeBufferListener(BufferEventListener p){ + int id = bufferListeners.indexOf(p); + if(id == -1) + return false; + + bufferListeners.remove(id); + return true; + } + + /** + * Connecte ce buffer à une entrée d'un algo + * @param p Algo à connecte + * @return Vrai si l'opération c'est bien passée (si il n'avait pas déjà été appelée) + */ + public boolean connectProcess(Process p){ + if(usedBy.contains(p)) + return false; + + usedBy.add(p); + return true; + } + + public boolean disconnectProcess(Process p){ + if(!usedBy.contains(p)) + return false; + + usedBy.remove(p); + return true; + } + + /** + * Retourne le type de stockage de ce buffer + * @return type de stockage + */ + public Storage.Type getArrayType(){ + return arrayType; + } + + /** + * Retourne l'indice de version associé à un échantillon. + * @param id index de l'échantillon + * @return indice de version + */ + public Integer getUpdateID(int id) { + return updateIDs.get(id); + } + + /** + * Surcharge de set(int,T,int) mais avec un indice de version = 1 + * @param index indice de l'échantillon à modifier ou ajouter + * @param value valeur de l'échantillon + * @return la valeur de l'échantillon précédant /!\ Peut retourner null si aucun n'échantillon ne correspondait à l'index /!\ + */ + public T set(int index, T value){ return set(index, value, null);} + + + /** + * Modifie un échantillon ou l'ajoute à au stockage si index ne correspond à aucun élément, complète alors avec des échantillons = null. + * De plus, updateId est associé à l'échantillon en tant qu'indice de version + * @param index indice de l'échantillon à modifier ou ajouter + * @param value valeur de l'échantillon + * @param updateId indice de version à associer + * @return la valeur de l'échantillon précédant /!\ Peut retourner null si aucun n'échantillon ne correspondait à l'index /!\ + */ + public T set(int index, T value, Integer updateId){ + + T previous = samples.set( index, value); + updateIDs.set(index, updateId); + + if(itemObservable) + ((ObservableObject)value).addListener( + new ObservableObject.SignedListener() { + @Override + public void observableOjectEvent(ObservableObjectEvent e) { + emitBufferEvent(BufferEvent.Type.SAMPLE_CHANGED, index); + } + + @Override + public String signature() { + return this.hashCode() + ":" + index; + } + }); + + if(previous==null) { + emitBufferEvent(BufferEvent.Type.SAMPLE_ADDED, index); + return null; + }else{ + if(value==null) + emitBufferEvent(BufferEvent.Type.SAMPLE_DELETED, index); + else + emitBufferEvent(BufferEvent.Type.SAMPLE_CHANGED, index); + + if(itemObservable) + ((ObservableObject)previous).removeSignedListener(this.hashCode()+":"+index); + } + + return previous; + + } + + /** + * Surcharge de addNote(T) mais avec un indice de version = 1 + * @param value valeur de l'échantillon à ajouter + * @return vrai si l'opération c'est bien passé (mais peut-elle mal se passer?)... + */ + public boolean add(T value){ + return add(value,0); + } + + /** + * Ajoute un élément après le dernier élément du stockage et lui associe un indice de version + * @param value valeur de l'échantillon à ajouter + * @param updateId indice de version à associer + * @return vrai si l'opération c'est bien passé (mais peut-elle rater?)... + */ + boolean add(T value, int updateId){ + if(!samples.add(value)) + return false; + + updateIDs.add(updateId); + emitBufferEvent(BufferEvent.Type.SAMPLE_ADDED, samples.size() - 1); + + if(itemObservable) + ((ObservableObject)value).addListener(new ObservableObject.Listener() { + @Override + public void observableOjectEvent(ObservableObjectEvent e) { + emitBufferEvent(BufferEvent.Type.SAMPLE_CHANGED, samples.size()-1); + } + }); + + return true; + } + + /** + * Renvoie la valeur d'un échantillon + * @param id index de l'échantillon + * @return valeur de l'échantillon /!\ Peut revoyer null si aucun échantillon ne correspond à l'index /!\ + */ + public T get(int id){ + return samples.get(id); + } + + /** + * Supprime un échantillon du buffer + * @param id index de l'échantillon à supprimer + * @return la valeur de l'échantillon supprimer /!\ Peut renvoyer null si aucun échantillon ne correspondait à cet index /!\ + */ + public T remove(int id){ + + T sampleDeleted = samples.remove(id); + updateIDs.remove(id); + + if(sampleDeleted!=null) { + emitBufferEvent(BufferEvent.Type.SAMPLE_DELETED, id); + if(itemObservable) + ((ObservableObject)sampleDeleted).removeSignedListener(this.hashCode() + ":" + id); + } + + return sampleDeleted; + } + + /** + * Retourne le nombre maximum d'élément de tableau (dernier index +1) + * @return la taille de l'espace de stockage + */ + public int size(){ + return samples.size(); + } + + /** + * Vérifie si le buffer est vide où non + * @return Vrai si il est vide + */ + public boolean isEmpty(){ + return size()==0; + } + + public boolean isCorrectIndex(int id){ + return get(id)!=null; + } + + /** + * Retourne la liste des indexs associés à des éléments dans le Storage: aucun de ces indexs ne retournera null par get(index) + * * @return la liste des indexs de tout les éléments du Storage + */ + public ArrayList getSamplesIndex(){ + return samples.getSamplesIndex(); + } + + + public StorageIterator getIterator(){ + return samples.getIterator(); + } + + /** + * Génère un iterateur pointant initialement sur l'élément de la liste correspondant à l'index en argument + * @param id index de l'élément sur lequel pointe initialement l'iterateur + * @return Un iterateur sur ce stockage + */ + + public StorageIterator getIterator(int id){ + return samples.getIterator(id); + } + + public void lastSampleAdded(){ + emitBufferEvent(BufferEvent.Type.INTERRUPT_PROCESS,-1); + } + + public void clear(){ + switch(arrayType){ + case ArrayList: + samples = new StorageArrayList<>(); + break; + } + + emitBufferEvent(BufferEvent.Type.INVALIDATE,-1); + } + + public void invalidateProcessedSamples(){ + emitBufferEvent(BufferEvent.Type.INVALIDATE,-1); + } + + public ArrayList> getUsedBy() { + return usedBy; + } + + /** + * Réserve directement un espace sans modifier la taille du tableau (si la daforme si prête) + * @param capacity capacité maximale; + */ + public void tryEnsureCapacity(int capacity){ + samples.tryEnsureCapacity(capacity); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Class getType() { + if(type==null&&!samples.isEmpty()) + return samples.get(getSamplesIndex().get(0)).getClass(); + + return type; + } + + public void setType(Class type) { + this.type = type; + Class[] classes = type.getClasses(); + for (int i = 0; i < classes.length; i++) + if (classes[i].equals(ObservableObject.class)) { + itemObservable = true; + return; + } + + itemObservable = false; + } + + + public Process.ProcessingOrder getProcessingOrder() { + return processingOrder; + } + + public void setProcessingOrder(Process.ProcessingOrder processingOrder) { + this.processingOrder = processingOrder; + } +} \ No newline at end of file diff --git a/src/processing/buffer/BufferEvent.java b/src/processing/buffer/BufferEvent.java new file mode 100644 index 0000000..42f9224 --- /dev/null +++ b/src/processing/buffer/BufferEvent.java @@ -0,0 +1,78 @@ +package processing.buffer; + + +/** + * Caractérise l'évennement envoyé par le buffer + */ + +public class BufferEvent { + private final int sampleID; + private final Type type; + private final Buffer bufferSource; + + + + public BufferEvent (Type eventType, int sampleID, Buffer bufferSource) { + this.sampleID = sampleID; + type = eventType; + this.bufferSource = bufferSource; + } + + /** Renvoie le type d'évenement */ + public Type getType () { + return type; + } + + /** Renvoie l'identifiant associé a l'échantillon en attribut */ + public int getSampleID () { + return sampleID; + } + + /** Renvoie le buffer source d'ou provient l'évenement */ + public Buffer getSource () { + return bufferSource; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof BufferEvent && bufferSource == ((BufferEvent) obj).getSource(); + + } + + public String toString(){ + String r = ""; + + switch(type){ + case SAMPLE_ADDED: + r+="Sample Added"; + break; + case SAMPLE_CHANGED: + r+="Sample Changed"; + break; + case SAMPLE_DELETED: + r+="Sample Deleted"; + break; + case INVALIDATE: + r+= "Invalidate processed samples"; + break; + case INTERRUPT_PROCESS: + r+="Interrupt process"; + break; + } + + r+= ": " + sampleID; + + return r; + } + + /** Définit les différents évemenements qui peuvent apparaitre dans un buffer */ + public enum Type { + SAMPLE_ADDED, + SAMPLE_CHANGED, + SAMPLE_DELETED, + INTERRUPT_PROCESS, + INVALIDATE + } + +} + diff --git a/src/processing/buffer/BufferEventListener.java b/src/processing/buffer/BufferEventListener.java new file mode 100644 index 0000000..7ffe462 --- /dev/null +++ b/src/processing/buffer/BufferEventListener.java @@ -0,0 +1,8 @@ +package processing.buffer; + +/** + * Created by gaby on 02/04/14. + */ +public interface BufferEventListener { + public void bufferEvent(BufferEvent e); +} diff --git a/src/processing/buffer/NoteBuffer.java b/src/processing/buffer/NoteBuffer.java new file mode 100644 index 0000000..66a2756 --- /dev/null +++ b/src/processing/buffer/NoteBuffer.java @@ -0,0 +1,107 @@ +package processing.buffer; + +import conteneurs.*; +import generictools.Instant; + +import java.util.ArrayList; + +/** Created by gaby on 02/06/14. + * Buffer qui sert à stocker des notes */ +public class NoteBuffer extends TemporalBuffer implements ObservableObject.Listener{ + + ArrayList notes = new ArrayList<>(); + + public NoteBuffer(Storage.Type arrayType, int sampleRate, Instant initialFrame) { + super(arrayType, sampleRate, initialFrame); + } + + /** Permet d'ajouter une note au buffer de notes + * @param note la note à ajouter + * @return l'identifiant lié a l'ajout de la note */ + public int addNote(Note note){ + int id = notes.size(); + notes.add(note); + + note.setBuffer(this); + note.setBufferID(id); + + note.addListener(this); + NoteID noteID = new NoteID(note); + + for (int i = note.getStart().mapToIndex(this); i <= note.getEnd().mapToIndex(this); i++) + add(i, noteID); + + return id; + } + + /** Permet de retirer une note au buffer + * @param bufferID l'identifiant de la note (pour le buffer) + * @return true si ça a fonctionné, false sinon */ + public boolean removeNote(int bufferID){ + Note n = getNote(bufferID); + if(n==null) + return false; + + for (int i = n.getStart().mapToIndex(this); i <= n.getEnd().mapToIndex(this); i++) { + get(i).remove(get(i).getIdById(n.getNoteID())); + } + + n.removeListener(this); + + return true; + } + + /** Permet d'ajouter une note à un instant particulier, caractérisé par son temps + * @param timeID le numero de l'instant + * @param noteID l'identifiant de la note */ + public void add(int timeID, NoteID noteID){ + if(get(timeID)==null) + set(timeID, new NoteList()); + get(timeID).add(noteID); + } + + public Note getNote(int noteID){ + if(noteID<0 || noteID>notes.size()) + return null; + + return notes.get(noteID); + } + + + + @Override + public void observableOjectEvent(ObservableObjectEvent e) { + if(e.getEmmiter() instanceof Note){ + Note note = (Note)e.getEmmiter(); + + int id = note.getStart().mapToIndex(this); + while(isCorrectIndex(id) && id<= note.getEnd().mapToIndex(this) && get(id).getNoteById(note.getNoteID())==null){ + get(id).add(note); + id++; + } + + id = note.getStart().mapToIndex(this)-1; + while(isCorrectIndex(id) && get(id).getNoteById(note.getNoteID())!=null){ + get(id).remove(note); + id--; + } + + id = note.getEnd().mapToIndex(this); + while(isCorrectIndex(id) && get(id).getNoteById(note.getNoteID())==null){ + get(id).add(note); + id--; + } + + id = note.getEnd().mapToIndex(this)+1; + while(isCorrectIndex(id) && get(id).getNoteById(note.getNoteID())!=null){ + get(id).remove(note); + id++; + } + } + } + + /** Renvoie le nombre de notes presentes dans le buffer */ + public int getNotesNbr(){ + return notes.size(); + } +} \ No newline at end of file diff --git a/src/processing/buffer/Storage.java b/src/processing/buffer/Storage.java new file mode 100644 index 0000000..c7dd833 --- /dev/null +++ b/src/processing/buffer/Storage.java @@ -0,0 +1,77 @@ +package processing.buffer; + +import java.util.ArrayList; + + +public interface Storage { + + public enum Type{ + ArrayList + } + + /** + * Retourne l'élément d'index id, null si aucun n'y est associé + * @param id index de l'élément + * @return l'élément /!\ Peut renvoyer null si aucun élément ne correspond à l'index /!\ + */ + public T get(int id); + + /** + * Modifie un élément à un index donné ou l'ajoute si aucun élément ne correspond à l'index + * @param id index de l'élément à modifier ou ajouter + * @param value valeur de l'élément + * @return la valeur de l'élément précédant /!\ Peut renvoyer null si aucun élément ne correspondait à cet index /!\ + */ + public T set(int id, T value); + + /** + * Ajoute une élément en fin de liste + * @param value valeur de l'élément à ajouter + * @return Vrai si l'opération a réussie (mais peut-elle rater?)... + */ + public boolean add(T value); + + /** + * Supprime un élément de la liste + * @param id index de l'élément à supprimer + * @return la valeur de l'élément supprimer /!\ Peut renvoyer null si aucun élément ne correspondait à cet index /!\ + */ + public T remove(int id); + + /** + * Retourne le nombre maximum d'élément de tableau (dernier index +1) + * @return la taille de l'espace de stockage + */ + public int size(); + + /** + * Vérifie si tout les échantillons du stockage sont null + * @return Vrai si size() = 0 ou si aucun échantillon n'est stocké dans l'espace disponible + */ + public boolean isEmpty(); + + /** + * Retourne la liste des indexs associés à des éléments dans le Storage: aucun de ces indexs ne retournera null par get(index) + * * @return la liste des indexs de tout les éléments du Storage + */ + public ArrayList getSamplesIndex(); + + /** + * Génère un iterateur pointant initialement sur le première élément de la liste + * @return Un iterateur sur ce stockage + */ + public StorageIterator getIterator(); + + /** + * Génère un iterateur pointant initialement sur l'élément de la liste correspondant à l'index en argument + * @param initialIndex index de l'élément sur lequel pointe initialement l'iterateur + * @return Un iterateur sur ce stockage + */ + public StorageIterator getIterator(int initialIndex); + + /** + * Réserve directement un espace sans modifier la taille du tableau (si la forme si prête) + * @param capacity capacité maximale; + */ + public void tryEnsureCapacity(int capacity); +} diff --git a/src/processing/buffer/StorageArrayList.java b/src/processing/buffer/StorageArrayList.java new file mode 100644 index 0000000..d637737 --- /dev/null +++ b/src/processing/buffer/StorageArrayList.java @@ -0,0 +1,169 @@ +package processing.buffer; + + +import java.util.ArrayList; + +/** + * Classe "adaptateur" entre les classiques ArrayList et les instances de Storage utilisable par Buffer. + * Certaines méthodes de ArrayList ont été redéfinie, nottament get renvoie null si il n'y as pas d'éléments demandé (il ne provoque pas d'exeption). + * @param Le type des éléments contenu dans ce StorageArrayList + */ + +public class StorageArrayList extends ArrayList implements Storage { + + private int lastIndex=-1; + int idIterator=-1; + + public StorageArrayList(){ + super(); + } + + /** + * Redéfinition de get(int) de ArrayList. La principale différence est de renvoyer null plutot que de lancer une exeption + * @param index index de l'élément désiré + * @param silent si vrai le rapport d'exeption ne sera pas affiché + * @return l'élément correspondant à l'index, ou null si aucun n'y était associé + */ + public T get(int index, boolean silent){ + + if(index < 0 || index >=size()) + return null; + + return super.get(index); + + } + + public T get(int index){ + if(index < 0 || index >=size()) + return null; + + return super.get(index); + } + + /** + * Redéfintion de set(int,T) de ArrayList. La principale différence est que l'index donné n'est pas obligé d'être < size(). + * La méthode s'occupera, si c'est le cas de compléter par des éléments null les indexs entre size()-1 et index + * @param index index de l'élément à modifier + * @param value nouvelle valeur de cet élément + * @return la valeur de l'élément précédant (null si aucun ne s'y trouvait). + */ + public T set(int index, T value){ + T previous = get(index); + + if(value==null) + return remove(index); + + if(previous==null) { + ensureSize(index + 1); + lastIndex = index; + } + + super.set(index, value); + + return previous; + } + + /** + * Redéfintion de addNote(T) de ArrayList. Ajoute un élément après le dernier: à dernier index +1; + * @param value valeur de l'élément à ajouter + * @return Vrai si l'opération c'est bien passé (mais peut-elle rater?)... + */ + public boolean add(T value){ + + set(lastIndex+1,value); + + return true; + } + + /** + * Assure que la taille minimum de la liste est newSize (en complétant avec des éléments null) + * @param newSize la nouvelle taille minimum de la liste + */ + void ensureSize(int newSize){ + ensureCapacity(newSize); + + while(super.size()-1); + return r; + } + + + public int size(){ return lastIndex+1;} + + public boolean isEmpty(){ return lastIndex==-1; } + + public ArrayList getSamplesIndex(){ + + ArrayList r = new ArrayList<>(); + r.ensureCapacity(lastIndex); + + for (int i = 0; i < size(); i++) + if(get(i)!=null) + r.add(i); + + r.trimToSize(); + + return r; + } + + + /** + * Génère un iterateur pointant initialement sur le première élément de la liste + * + * @return Un iterateur sur ce stockage + */ + @Override + public StorageIterator getIterator() { + return getIterator(0); + } + + /** + * Génère un iterateur pointant initialement sur l'élément de la liste correspondant à l'index en argument + * + * @param initialIndex index de l'élément sur lequel pointe initialement l'iterateur + * @return Un iterateur sur ce stockage + */ + @Override + public StorageIterator getIterator(final int initialIndex) { + return new StorageIterator() { + private int currentID = initialIndex; + @Override + public T next() { + if(currentID>=size()) + return null; + + return get(currentID++); + } + + @Override + public boolean hasNext() { + return currentID < size(); + + } + }; + } + + /** + * Réserve directement un espace sans modifier la taille du tableau (si la forme si prête) + * + * @param capacity capacité maximale; + */ + @Override + public void tryEnsureCapacity(int capacity) { + ensureCapacity(capacity); + } +} diff --git a/src/processing/buffer/StorageIterator.java b/src/processing/buffer/StorageIterator.java new file mode 100644 index 0000000..f4018f5 --- /dev/null +++ b/src/processing/buffer/StorageIterator.java @@ -0,0 +1,20 @@ +package processing.buffer; + +/** + * Created by gaby on 31/03/14. + */ +public interface StorageIterator { + /** + * Renvoie la valeur de l'élément suivant, l'iterator ce décale alors à cet élément + * @return + */ + public T next(); + + /** + * Teste si il reste un élément disponible après l'élément actuel + * @return Vrai si on peut appeler next()... + */ + public boolean hasNext(); + + +} diff --git a/src/processing/buffer/TemporalBuffer.java b/src/processing/buffer/TemporalBuffer.java new file mode 100644 index 0000000..de20102 --- /dev/null +++ b/src/processing/buffer/TemporalBuffer.java @@ -0,0 +1,64 @@ +package processing.buffer; + +import generictools.Instant; + +/** + * Created by gaby on 27/03/14. + */ +public class TemporalBuffer extends Buffer { + private int sampleRate = 0; + private Instant originInstant =Instant.fromTime(0); + + + /** + * Constructeur du buffer temporel + * @param arrayType type de stockage + * @param sampleRate fréquence d'échantillonage en Hz + */ + public TemporalBuffer(Storage.Type arrayType, int sampleRate) { + super(arrayType); + this.sampleRate = sampleRate; + } + + public TemporalBuffer(Storage.Type arrayType, int sampleRate, Instant originInstant) { + this(arrayType, sampleRate); + this.originInstant = originInstant; + } + + /** + * Renvoie la fréquence d'échantillonage + * @return Fréquence d'échatillonage en Hz + */ + + public int getSampleRate() { + return sampleRate; + } + + /** + * Renvoie la valeur de l'instant pour l'index 0 + * @return Instant de l'origine des indexs + */ + public Instant getOriginInstant() { + return originInstant; + } + + /** + * Renvoie la valeur de l'échantillon à un instant donné + * @param time Temps en ms + * @return Valeur de l'échantillon + */ + public T get(float time){ + return get(Instant.fromTime(time).mapToIndex(this)); + } + + /** + * Renvoie la valeur de l'échantillon à un instant donné + * @param instant Instant + * @return Valeur de l'échantillon + */ + public T get(Instant instant){ + return get(instant.mapToIndex(this)); + } + + +} diff --git a/src/processing/processes/EventToNote.java b/src/processing/processes/EventToNote.java new file mode 100644 index 0000000..f8254a2 --- /dev/null +++ b/src/processing/processes/EventToNote.java @@ -0,0 +1,141 @@ +package processing.processes; + +import conteneurs.Note; +import conteneurs.NoteEvent; +import conteneurs.NoteEventList; +import conteneurs.NoteList; +import generictools.Instant; +import processing.buffer.NoteBuffer; +import processing.buffer.Storage; +import processing.buffer.TemporalBuffer; +import processing.properties.FloatProperty; + +import java.util.ArrayList; + +/** + * Created by gaby on 03/06/14. + */ +public class EventToNote extends IndexedProcess{ + + ArrayList stillAlive = new ArrayList<>(); + + FloatProperty minDuration; + + public EventToNote( TemporalBuffer input) { + super("Event to note", input, new NoteBuffer(Storage.Type.ArrayList, input.getSampleRate(), input.getOriginInstant())); + minDuration = new FloatProperty("Durée minimum (ms)", 100, 0, 2000); + propertyManager().addProperty(minDuration); + } + + /** + * À redéfinir: Retourne le résultat du calcul à placer à l'index outputID + * + * @param outputID index de l'échantillon que l'on veut calculer + * @return Résultat du calcul + */ + @Override + protected NoteList compute(Instant outputID) { + NoteEventList list = input().get(outputID); + NoteList result = new NoteList(); + + for (int i = 0; i < list.size(); i++) { + NoteEvent e = list.get(i); + + if(e.getEventType()== NoteEvent.Type.APPARITION){ + if(aliveNoteByFreq(e.getNote().getIdNote())==null) { + //Si une apparition est détectée et la note n'est pas active + Note n = new Note(e.getNote().getIdNote(), outputID, outputID.translated(1)); + output().addNote(n); + stillAlive.add(n); + System.out.print(""); + + } + }else{ + + Note noteAlive = aliveNoteByFreq(e.getNote().getIdNote()); + if(noteAlive!=null) { + //Si une disparition est détectée et la note est active + if(noteAlive.getDuration()>minDuration.getValue()) + stillAlive.remove(aliveNoteIdByFreq(e.getNote().getIdNote())); + } + } + } + + for (Note n: stillAlive) + n.setEnd(outputID.translated(1)); + + + return result; + } + + protected Note aliveNoteByFreq(int idFreq){ + for(Note n:stillAlive) + if(n.getNoteID() == idFreq) + return n; + return null; + } + + protected int aliveNoteIdByFreq(int idFreq){ + for (int id = 0; id < stillAlive.size(); id++) + if(stillAlive.get(id).getNoteID() == idFreq) + return id; + + return -1; + } + + /** + * À redéfinir: Renvoie la liste des index dépendant de l'échantillon d'index inputID. + * + * @param inputBufferID index du buffer de l'échantillon + * @param inputID index de l'échantillon qui est utilisé par ... + * @return La liste des indexs de sortie qui utilise inputID pour leur calcul. + */ + @Override + protected ArrayList idUsedBy(int inputBufferID, int inputID) { + ArrayList r = new ArrayList<>(); + r.add(Instant.fromIndex(inputID, this.input())); + return r; + } + + /** + * À redéfinir: Renvoie, pour chaque buffer d'entrée la liste des index nécessaire au calcul de l'échantillon d'index outputID. + * + * @param outputID index de l'échantillon que l'on veut calculer + * @return Un tableau de même taille que getInputs(), et qui contient les liste des échantillons de chaque buffer de getInputs() nécessaire au calcul. + */ + @Override + protected ArrayList[] idNeededFor(int outputID) { + ArrayList r = new ArrayList<>(); + ArrayList[] ret = new ArrayList[1]; + r.add(Instant.fromIndex(outputID, output())); + ret[0]=r; + return ret; + } + + /** + * Renvoie le type du buffer de sortie + * + * @return La class des échantillones du buffer de sortie + */ + @Override + protected Class getType() { + return NoteList.class; + } + protected ProcessingOrder getProcessingOrder(){ + return ProcessingOrder.ASCENDING_ORDER; + } + + public TemporalBuffer input(){ + return (TemporalBuffer)super.input(); + } + + public NoteBuffer output(){ + return (NoteBuffer)super.output(); + } + + @Override + protected void clean() { + stillAlive.clear(); + super.clean(); + } +} \ No newline at end of file diff --git a/src/processing/processes/FFT.java b/src/processing/processes/FFT.java new file mode 100644 index 0000000..692cc30 --- /dev/null +++ b/src/processing/processes/FFT.java @@ -0,0 +1,230 @@ +package processing.processes; + +import conteneurs.NoteGamme; +import conteneurs.RegularGamme; +import conteneurs.Spectre; +import generictools.Complex; +import generictools.Instant; +import generictools.Pair; +import generictools.WindowFunction; +import gui.graphs.SpectreView; +import processing.buffer.Storage; +import processing.buffer.TemporalBuffer; +import processing.properties.ChoiceProperty; +import processing.properties.IntProperty; +import processing.properties.Property; + +import java.awt.*; +import java.util.ArrayList; + + /** + * Algorithme de FFT: Calcule la transformée de Fourier d'un signal temporel et renvoie le spectre correspondant + */ + + public class FFT extends IndexedProcess { + + private IntProperty nFFT_Max; + private ChoiceProperty window; + + private int nFFt; + private final int freq_echant; + + private SpectreView.Type grapheType; + private Complex expOddFFT; + private ArrayList selectedIndex; + private RegularGamme regGammeFFT; + + + public FFT(TemporalBuffer temporalSignal, int sampleRate) { + super("FFT",temporalSignal, new TemporalBuffer(Storage.Type.ArrayList, sampleRate)); + + nFFT_Max = new IntProperty("nFFT_Max", 8192*2*2); + nFFT_Max.setMin(1); + window = new ChoiceProperty<>("Window Type", createWindows(), Window.Type.class, 4); + + + propertyManager().addProperty(nFFT_Max); + propertyManager().addProperty(window); + + freq_echant = temporalSignal.getSampleRate(); + regGammeFFT = new RegularGamme(0f, nFFT_Max.getValue(), (float)freq_echant/nFFT_Max.getValue()); + setGrapheType(SpectreView.Type.NOTES); + + nFFT_Max.addListener(new Property.Listener() { + @Override + public void propertyChange(Property source, Integer previousValue) { + window.forceChoices(createWindows()); + regGammeFFT = new RegularGamme(0f, nFFT_Max.getValue(), (float)freq_echant/nFFT_Max.getValue()); + setGrapheType(SpectreView.Type.NOTES); + } + }); + + this.expOddFFT = new Complex(0,0); + } + + private ArrayList> createWindows(){ + ArrayList> choices = new ArrayList<>(); + choices.add(new Pair<>(new WindowFunction(WindowFunction.Type.RECTANGLE, nFFT_Max.getValue(), input()), "RECTANGLE")); + choices.add(new Pair<>(new WindowFunction(WindowFunction.Type.HANN, nFFT_Max.getValue(), input()), "HANN")); + choices.add(new Pair<>(new WindowFunction(WindowFunction.Type.HAMMING, nFFT_Max.getValue(), input()), "HAMMING")); + choices.add(new Pair<>(new WindowFunction(WindowFunction.Type.TRIANGLE, nFFT_Max.getValue(), input()), "TRIANGLE")); + choices.add(new Pair<>(new WindowFunction(WindowFunction.Type.BLACKMAN, nFFT_Max.getValue(), input()), "BLACKMAN")); + return choices; + } + + /** Suivant le type d'affichage, on n'a pas besoin de calculer toutes les fréquences. + * grapheType permet d'indiquer les fréquences utiles à calculer. + * @param idOutput l'échantillon sur lequel on base la FFT + * @return le spectre sur la gammeConsidérée + */ + protected Spectre compute (Instant idOutput){ + this.nFFt = nFFT_Max.getValue(); + //long time = System.currentTimeMillis(); + window.getValue().setStartID(idOutput.mapToIndex(input())); + Spectre res = new Spectre(new RegularGamme(0, nFFT_Max.getValue(), (float)freq_echant/nFFT_Max.getValue())); + int divFactor, currentSelectId; + float freq, ampliFreq; + + for (int i = 0; i < selectedIndex.size(); i++) { + currentSelectId = selectedIndex.get(i); + freq = regGammeFFT.getFreq(currentSelectId); + divFactor = divideFactor(freq); + // Si changement de coefficient, on doit changer le nombre de points sur lequels on calcule la FFT + nFFt = nFFT_Max.getValue() / divFactor; + window.getValue().setDivideFactor(divFactor); + + this.expOddFFT = Complex.ComplexFromExp(1,(float)(-2*Math.PI*currentSelectId)/nFFT_Max.getValue()); + //On calcule la FFT pour cette fréquence uniquement: sélection uniquement des fréquences correspondant à des notes + ampliFreq = calculFFT(idOutput.mapToIndex(input()), 1).getMod(); + res.setAmplitudeAt(currentSelectId, ampliFreq); + } + + if(this.grapheType == SpectreView.Type.NOTES) { + res.castToGamme(NoteGamme.gamme()); + } + + //time = System.currentTimeMillis() - time; + //System.out.println("FFT done in : "+ time + " ms"); + + return res; + } + + /** Calcule la FFT d'un signal contenu dans un Buffer en entrée + * @param startID: indice de départ dans le Buffer pour le calcul de la FFT + * @param step: le "pas" actuel, qui va doubler à chaque récursion + * @return le tableau de complexes qui correspondent aux fréquences */ + private Complex calculFFT(int startID, int step) { + if (step != (this.nFFt)) { + Complex exp = new Complex(expOddFFT); + expOddFFT.simpleSquare(); + Complex oddFFT = calculFFT(startID + step, step * 2); + + expOddFFT.setToEquals(exp); + expOddFFT.simpleSquare(); + Complex evenFFT = calculFFT(startID, step * 2); + return evenFFT.simpleSum(oddFFT.simpleMult(exp)); + } else { + return new Complex(this.window.getValue().getValue(startID), 0); + } + } + + + protected ArrayList idUsedBy(int inputID, int inputBufferID) { + ArrayList r = new ArrayList<>(); + if(inputBufferID!=0 || inputID<0){ + return r; + } + for (int i = 0; i < this.nFFt; i++) { + r.add(Instant.fromIndex(inputID+i,input())); + } + return r; + } + + protected ArrayList[] idNeededFor (int outputID) { + ArrayList[] r = new ArrayList[1]; + r[0] = new ArrayList<>(); + + r[0].add(Instant.fromIndex(outputID, output())); + + return r; + } + public int setNfft(int nPoints){ + int nextPow = 1; + while( nextPow < nPoints){ + nextPow = nextPow*2; + } + this.nFFt = nextPow; + return this.nFFt; + } + public int getnFFt(){ + return this.nFFt; + } + + + /** Renvoie le type du buffer de sortie * + * @return La class des échantillones du buffer de sortie */ + @Override + public Class getType() { + return Spectre.class; + } + + @Override + protected ProcessingOrder getProcessingOrder() { + return ProcessingOrder.ASCENDING_ORDER; + } + + /** Modifie le type de graphe et calcule les index correspondant à un certain nFFt. nFFt dépend de la fréquence. + * @param grapheType le type de graphique */ + public void setGrapheType(SpectreView.Type grapheType) { + this.grapheType = grapheType; + + // Cas où le graphe est sous forme de NOTES + if(this.grapheType == SpectreView.Type.NOTES) { + int noteGammeSize = NoteGamme.gammeStatSize(); + float freq; + + this.selectedIndex = new ArrayList<>(noteGammeSize); + int index; + for (int i = 0; i < noteGammeSize; i++) { + freq = NoteGamme.getStatFreq(i); + ArrayList neighboorIndex = regGammeFFT.getIndex(freq); + + //Recherche de l'indice le plus proche + index = (int)neighboorIndex.get(0); + if (neighboorIndex.size()>1 && (float) neighboorIndex.get(2) > 0.5) { + index = (int)neighboorIndex.get(1); + } + selectedIndex.add(index); + } + + // Cas où le graphe est sous forme de REGULAR_FREQ + } else if (this.grapheType == SpectreView.Type.REGULAR_FREQ){ + this.selectedIndex = new ArrayList(this.nFFt); + for (int i = 0; i < this.nFFt; i++) { + selectedIndex.add(i); + } + } + } + + /** Renvoie le facteur de division de nFFT (optimisation de nFFt pour obtenir une bonne résolution dans les basses fréquences, + * et une bonne rapidité de calcul dans les hautes fréquences + * @param freq la fréquence considéré + * @return le facteur par lequel on divise nFFt */ + private int divideFactor(float freq){ + if(freq < 200){ + return 1; + } else if (freq < 1000){ + return 2; + } else if (freq < 5000) { + return 8; + } else { + return 16; + } + } + + @Override + protected float getSampleNbrPerIteration() { + return nFFT_Max.getValue(); + } + } + diff --git a/src/processing/processes/FreqEdgeDetector.java b/src/processing/processes/FreqEdgeDetector.java new file mode 100644 index 0000000..695068e --- /dev/null +++ b/src/processing/processes/FreqEdgeDetector.java @@ -0,0 +1,227 @@ +package processing.processes; + +import conteneurs.Spectre; +import generictools.Instant; +import processing.buffer.Storage; +import processing.buffer.TemporalBuffer; +import processing.properties.FloatProperty; +import processing.properties.IntProperty; +import processing.properties.Property; + +import java.util.ArrayList; + +/** + * Created by Gabriel on 20/05/2014. + * Fait un calcul différentiel sur les spectres pour détecter les apparitions/disparitions de notes + * Notre algo utilise une approximation du filtre de Canny (dérivée de Gaussienne) + */ +public class FreqEdgeDetector extends IndexedProcess { + + private ArrayList coefs; + + private FloatProperty sigma; + private IntProperty nPoints; + private IntProperty nPointsPasseBas; + + public FreqEdgeDetector(TemporalBuffer input, int maxNombrePoints, int maxNombreDePointPasseBas) { + super("Frequencies Edge Detector", input, new TemporalBuffer(Storage.Type.ArrayList, input.getSampleRate(), Instant.fromIndex(maxNombrePoints+maxNombreDePointPasseBas,input))); + sigma = new FloatProperty("sigma", 100f); + sigma.setMin(0.00001f); + nPoints = new IntProperty("nPoints", maxNombrePoints); + nPoints.setMin(1); + nPoints.setMax(50); + nPoints.addListener(new Property.Listener() { + @Override + public void propertyChange(Property source, Integer previousValue) { + coefs = FreqEdgeDetector.calcCoef(sigma.getValue(), nPoints.getValue()); + } + }); + sigma.addListener(new Property.Listener() { + @Override + public void propertyChange(Property source, Float previousValue) { + coefs = FreqEdgeDetector.calcCoef(sigma.getValue(), nPoints.getValue()); + } + }); + + nPointsPasseBas = new IntProperty("nPoints Passe Bas", maxNombreDePointPasseBas); + nPointsPasseBas.setMin(1); + nPointsPasseBas.setMax(50); + coefs = FreqEdgeDetector.calcCoef(sigma.getValue(), nPoints.getValue()); + + propertyManager().addProperty(sigma); + propertyManager().addProperty(nPoints); + propertyManager().addProperty(nPointsPasseBas); + } + + /** Algorithme de calcul qui applique le masque définit dans le constructeur */ + @Override + protected Spectre compute(Instant outputID) { + int id = outputID.mapToIndex(output()); + + Spectre r = new Spectre(((Spectre)input().get(id)).getGamme()); + int iMax = r.getGamme().gammeSize(); +// for (int i = 0; i < iMax; i++) { +// float ampl = coefs.get(0)* ((Spectre)input().get(id)).getAmplitude(i); +// for(int k = 1; k < this.coefs.size(); k++){ +// Spectre sApres = (Spectre)input().get(id+k); +// Spectre sAvant = (Spectre)input().get(id-k); +// if(sApres == null){ +// sApres = new Spectre(r.getGamme()); +// } +// if(sAvant == null){ +// sAvant = new Spectre(r.getGamme()); +// } +// ampl += ( coefs.get(k)*sApres.getAmplitude(i) ) - ( coefs.get(k)*sAvant.getAmplitude(i) ); +// } +// r.setAmplitudeAt(i, ampl); +// } + + for(int k = 1; k < this.coefs.size(); k++){ + Spectre sApres =passeBas(id+k); + Spectre sAvant = passeBas(id-k); + if(sApres == null){ + sApres = new Spectre(r.getGamme()); + } + if(sAvant == null){ + sAvant = new Spectre(r.getGamme()); + } + for (int i = 0; i < iMax; i++) { + float ampl = ( coefs.get(k)*sApres.getAmplitude(i) ) - ( coefs.get(k)*sAvant.getAmplitude(i) ); + r.setAmplitudeAt(i, r.getAmplitude(i)+ampl); + } + } + + return r.noiseFilter(); + } + + + /** Réalise une moyenne sur des échantillons successifs -> filtre passe-bas pour lisser le bruit + * @param inputID l'indice de l'échantillon de le buffer + * @return le Spectre moyenné */ + private Spectre passeBas(int inputID){ + + if(input().get(inputID)==null) + return null; + + Spectre r = new Spectre( ((Spectre)input().get(inputID)).getGamme()); + int nPoint = 0; + + for(int i=-nPointsPasseBas.getValue(); i0 que l'on veut. Au total il y en aura (nPoints-1)*2 + 1 + * @return la liste des coefficients */ + public static ArrayList calcCoef( double sigma, int nPoints){ + ArrayList coefs = new ArrayList(); + double step = 4.0*sigma/(double)nPoints; + double newCoeff = 1; + double sum = 0; + for (int i = 0; i < nPoints ; i++) { + newCoeff = (step*i)*Math.exp((-1*step*step*i*i)/(2*sigma*sigma)); + coefs.add((float)newCoeff); + sum += newCoeff; + } + + for (int i = 0; i < coefs.size(); i++) { + coefs.set(i, coefs.get(i)/(float)sum); + } + return coefs; + } + + + + /** Permet de changer les paramètres du masque à afficher et donc de changer les coefficients en conséquence + * @param sigma le nouveau coefficient d'échelle + * @param newNPoint le nouveau nombre de coefficients positifs */ + public void setCoefs(float sigma, int newNPoint) { + this.coefs = FreqEdgeDetector.calcCoef(sigma, newNPoint); + } + + public void setSigma(float sigma) { + this.sigma.setValue(sigma); + setCoefs(this.sigma.getValue(), this.nPoints.getValue()); + } + + public void setnPoints(int nPoints) { + this.nPoints.setValue(nPoints); + setCoefs(this.sigma.getValue(), this.nPoints.getValue()); + } + + /** + * À redéfinir: Renvoie la liste des index dépendant de l'échantillon d'index inputID. + * @param inputID index de l'échantillon qui est utilisé par ... + * @param inputBufferID + * @return La liste des indexs de sortie qui utilise inputID pour leur calcul. + */ + @Override + protected ArrayList idUsedBy(int inputID, int inputBufferID) { + if(inputID!=0) + return new ArrayList(); + + ArrayList r = new ArrayList<>((nPoints.getValue()+nPointsPasseBas.getValue())*2+1); + int outputID = Instant.fromIndex(inputID, input()).mapToIndex(output()); + + r.add(Instant.fromIndex(inputBufferID, input())); + for(int k = 1; k < (nPoints.getValue()+nPointsPasseBas.getValue()); k++){ + r.add(0, Instant.fromIndex(outputID - k,output())); + r.add(Instant.fromIndex(outputID + k, output())); + } + + return r; + } + + /** À redéfinir: Renvoie, pour chaque buffer d'entrée la liste des index nécessaire au calcul de l'échantillon d'index outputID. + * @param outputID index de l'échantillon que l'on veut calculer + * @return Un tableau de même taille que getInputs(), et qui contient les liste des échantillons de chaque buffer de getInputs() nécessaire au calcul. + */ + @Override + protected ArrayList[] idNeededFor(int outputID) { + ArrayList[] t = new ArrayList[1]; + ArrayList r = new ArrayList<>((nPoints.getMax()+nPointsPasseBas.getMax())*2+1); + + t[0] = r; + if(outputID<0) + return t; + + int inputID = Instant.fromIndex(outputID, output()).mapToIndex(input()); + r.add(Instant.fromIndex(inputID, input())); + for(int k = 1; k < (nPoints.getValue()+nPointsPasseBas.getValue()); k++){ + r.add(0, Instant.fromIndex(inputID - k, input())); + r.add(Instant.fromIndex(inputID + k, input())); + } + return t; + } + + /** Renvoie le type du buffer de sortie + * @return La class des échantillones du buffer de sortie */ + @Override + protected Class getType() { + return Spectre.class; + } + + @Override + protected ProcessingOrder getProcessingOrder() { + return ProcessingOrder.ASCENDING_ORDER; + } + + @Override + protected float getSampleNbrPerIteration() { + return nPoints.getValue()*2 + nPointsPasseBas.getValue()*2 +1; + } +} + diff --git a/src/processing/processes/FreqEdgeToEvent.java b/src/processing/processes/FreqEdgeToEvent.java new file mode 100644 index 0000000..d39930d --- /dev/null +++ b/src/processing/processes/FreqEdgeToEvent.java @@ -0,0 +1,163 @@ +package processing.processes; + +import conteneurs.*; +import generictools.Instant; +import javafx.collections.ObservableList; +import processing.buffer.Storage; +import processing.buffer.TemporalBuffer; +import processing.properties.FloatProperty;import processing.properties.IntProperty; + +import java.util.ArrayList; + +/** +* Created by Gabriel on 21/05/2014. + * Permet de passer de la détection des contours aux evenements, donc apparition et disparition de notes */ +public class FreqEdgeToEvent extends IndexedProcess { + + IntProperty nPointRegroup; + FloatProperty cutCoef; + + + public FreqEdgeToEvent(TemporalBuffer input, int nPointRegroupMax, float cutCoef) { + super("FreqEdgeToEvent", input, new TemporalBuffer(Storage.Type.ArrayList, input.getSampleRate(), Instant.fromIndex(0, input))); + nPointRegroup = new IntProperty("nPoint regroupement", nPointRegroupMax); + nPointRegroup.setMax(50); + nPointRegroup.setMin(0); + + + this.cutCoef = new FloatProperty("cut Coef", cutCoef); + this.cutCoef.setMax(1); + this.cutCoef.setMin(0); + + + propertyManager().addProperty(nPointRegroup); + propertyManager().addProperty(this.cutCoef); + } + + @Override + protected NoteEventList compute(Instant outputID) { + + NoteEventList listeEvent = new NoteEventList(); + int id = outputID.mapToIndex(output()); + Spectre r = maxRegroup(id); + if(r == null) + return new NoteEventList(); + + + // Si jamais ce n'est pas un spectre de notes, on le transforme pour pouvoir travailler dessus + if(!(r.getGamme() instanceof NoteGamme)){ + r.castToGamme(NoteGamme.gamme()); + } + + ArrayList ampliEdges = r.getAmplitudes(); + Gamme frequencies = r.getGamme(); + float currentAmpli; + for (int i = 1; i < frequencies.gammeSize(); i++) { + currentAmpli = ampliEdges.get(i); + //Cas d'une disparation brutale de note + Profil profil = new Profil(); + profil.setAmplitude(Math.abs(currentAmpli), 0); + + if (currentAmpli < -200000f) { + listeEvent.add(NoteEvent.disparitionEvent(new NoteProfiled(i, profil), -currentAmpli)); + } else if (currentAmpli > 300000f) { + listeEvent.add(NoteEvent.apparitionEvent(new NoteProfiled(i, profil), currentAmpli)); + } + + } + + return listeEvent; + } + + /** Supprime les maximums qui sont trop proches suivant une certaine marge de tolérance définie par cutCoeff + * @param inputID l'indice du buffer d'entrée */ + private Spectre maxRegroup(int inputID){ + if(input().get(inputID)==null) + return null; + + Spectre currentSpectre = ((Spectre)input().get(inputID)).copy(); + if(currentSpectre==null) + return null; + + + for(int i=0; i<=nPointRegroup.getValue(); i++){ + Spectre previousSpectre = input().get(inputID-i)!=null?(Spectre)input().get(inputID-i):new Spectre( ((Spectre)input().get(inputID)).getGamme()); + Spectre nextSpectre = input().get(inputID+i)!=null?(Spectre)input().get(inputID+i):new Spectre( ((Spectre)input().get(inputID)).getGamme()); + + for (int j = 1; j < currentSpectre.getGamme().gammeSize(); j++) { + float currentAmpli = currentSpectre.getAmplitude(j); + float previousAmpli = (previousSpectre.getAmplitude(j)*currentAmpli)>0?Math.abs(previousSpectre.getAmplitude(j)):0; + float nextAmpli = (nextSpectre.getAmplitude(j)*currentAmpli)>0?Math.abs(nextSpectre.getAmplitude(j)):-1; + + currentAmpli = Math.abs(currentAmpli); + if(currentAmpli == 0) + continue; + + if(previousAmpli>currentAmpli/cutCoef.getValue() || nextAmpli>currentAmpli/cutCoef.getValue()) + currentSpectre.setAmplitudeAt(j, 0); + } + } + return currentSpectre; + } + /** Renvoie le rapport entre 2 float */ + private float rapport(float f1, float f2){ + return Math.min(f1,f2)/Math.max(f1,f2); + } + + /** + * À redéfinir: Renvoie la liste des index dépendant de l'échantillon d'index inputID. + * @param inputID index de l'échantillon qui est utilisé par ... + * @param inputBufferID + * @return La liste des indexs de sortie qui utilise inputID pour leur calcul. + */ + @Override + protected ArrayList idUsedBy(int inputBufferID, int inputID) { + if(inputID!=0) + return new ArrayList(); + + ArrayList r = new ArrayList<>((nPointRegroup.getValue())*2+1); + int outputID = Instant.fromIndex(inputID, input()).mapToIndex(output()); + + r.add(Instant.fromIndex(inputBufferID, input())); + for(int k = 1; k < (nPointRegroup.getValue()); k++){ + r.add(0, Instant.fromIndex(outputID - k,output())); + r.add(Instant.fromIndex(outputID + k, output())); + } + + return r; + } + + /** À redéfinir: Renvoie, pour chaque buffer d'entrée la liste des index nécessaire au calcul de l'échantillon d'index outputID. + * @param outputID index de l'échantillon que l'on veut calculer + * @return Un tableau de même taille que getInputs(), et qui contient les liste des échantillons de chaque buffer de getInputs() nécessaire au calcul. + */ + @Override + protected ArrayList[] idNeededFor(int outputID) { + ArrayList[] t = new ArrayList[1]; + ArrayList r = new ArrayList<>((nPointRegroup.getValue())*2+1); + + t[0] = r; + if(outputID<0) + return t; + + int inputID = Instant.fromIndex(outputID, output()).mapToIndex(input()); + r.add(Instant.fromIndex(inputID, input())); + for(int k = 1; k < (nPointRegroup.getValue()); k++){ + r.add(0, Instant.fromIndex(inputID - k, input())); + r.add(Instant.fromIndex(inputID + k, input())); + } + return t; + } + + /** Renvoie le type du buffer de sortie + * @return La class des échantillones du buffer de sortie */ + @Override + protected Class getType() { + return NoteEventList.class; + } + + @Override + protected ProcessingOrder getProcessingOrder() { + return ProcessingOrder.ASCENDING_ORDER; + } +} \ No newline at end of file diff --git a/src/processing/processes/IndexedProcess.java b/src/processing/processes/IndexedProcess.java new file mode 100644 index 0000000..b62d271 --- /dev/null +++ b/src/processing/processes/IndexedProcess.java @@ -0,0 +1,135 @@ +package processing.processes; + +import generictools.Instant; +import processing.buffer.Buffer; +import processing.buffer.BufferEvent; +import processing.buffer.TemporalBuffer; + +import java.util.ArrayList; + +public abstract class IndexedProcess extends Process { + + public IndexedProcess (String name, Buffer[] inputs, Buffer output) { + super(name, inputs, output); + } + public IndexedProcess(String name, Buffer input, Buffer output) { + super(name,input, output); + } + + protected BufferEventProcessor initBufferEventProcessor(){ + + switch (getProcessingOrder()){ + case ASCENDING_ORDER: + return new BufferEventProcessor() { + + int[] lastInputIndex = {}; + + + @Override + public void processBufferEvent(BufferEvent e, int updateID) { + //Test de la possibilité du calcul sur l'échantillon en sortie suivant + + for (int i = 0; i < lastInputIndex.length; i++) { + if(lastInputIndex[i]>inputs()[i].size()-1) + return; + } + + output().add(compute(Instant.fromIndex(output().size(), output()))); + + + ArrayList[] idNeeded= idNeededFor(output().size()); + lastInputIndex = new int[idNeeded.length]; + for (int i = 0; i < idNeeded.length; i++) + lastInputIndex[i] = idNeeded[i].get(idNeeded[i].size()-1).mapToIndex(inputs()[i]); + + } + }; + } + + return new BufferEventProcessor() { + @Override + public void processBufferEvent(BufferEvent e, int eventUpdateID) { + int bufferID = 0; + for(int i =1; i=0 && isComputable(iOutput) && (outputUpdateID==null || eventUpdateID > outputUpdateID || (eventUpdateID < 0 && outputUpdateID > 0))) { + output().set(iOutput, compute(i), updateID); + updateID++; + } + } + } + }; + } + + private boolean isComputable(int outputID){ + + ArrayList[] needed = idNeededFor(outputID); + + if(needed.length==0) + return false; + + for (int i = 0; i < needed.length; i++) { + for (Instant j : needed[i]) + if (inputs[i].get(j.mapToIndex(inputs()[i])) == null) + return false; + } + + + + return true; + } + + /** + * À redéfinir: Retourne le résultat du calcul à placer à l'index outputID + * @param outputID index de l'échantillon que l'on veut calculer + * @return Résultat du calcul + */ + protected abstract T compute (Instant outputID); + + /** + * À redéfinir: Renvoie la liste des index dépendant de l'échantillon d'index inputID. + * @param inputBufferID index du buffer de l'échantillon + * @param inputID index de l'échantillon qui est utilisé par ... + * @return La liste des indexs de sortie qui utilise inputID pour leur calcul. + */ + protected abstract ArrayList idUsedBy (int inputBufferID, int inputID); + + /** + * À redéfinir: Renvoie, pour chaque buffer d'entrée la liste des index nécessaire au calcul de l'échantillon d'index outputID. + * @param outputID index de l'échantillon que l'on veut calculer + * @return Un tableau de même taille que getInputs(), et qui contient les liste des échantillons de chaque buffer de getInputs() nécessaire au calcul. + */ + protected abstract ArrayList[] idNeededFor (int outputID); + + + public TemporalBuffer input(){ + return (TemporalBuffer)super.input(); + } + + + public TemporalBuffer[] inputs(){ + TemporalBuffer[] r = new TemporalBuffer[super.inputs().length]; + for (int i = 0; i < super.inputs().length; i++) { + r[i] = (TemporalBuffer)super.inputs()[i]; + } + return r; + } + + public TemporalBuffer output(){ + return (TemporalBuffer)super.output(); + } + + +} \ No newline at end of file diff --git a/src/processing/processes/Process.java b/src/processing/processes/Process.java new file mode 100644 index 0000000..4955abd --- /dev/null +++ b/src/processing/processes/Process.java @@ -0,0 +1,494 @@ +package processing.processes; + + +import generictools.Pair; +import processing.buffer.Buffer; +import processing.buffer.BufferEvent; +import processing.properties.PropertiesManager; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.stream.Collectors; + +public abstract class Process implements Runnable { + final Buffer[] inputs; + private boolean[] inputsInterupted; + final Buffer output; + + private boolean interuptAfter = false, paused = false, waiting = true; + + private final String name; + private PropertiesManager properties; + private ArrayList listeners = new ArrayList<>(); + + private LinkedList > bufferEvents; + int updateID = 0; + private Thread thread; + + private BufferEventProcessor bProcessor; + + private boolean boxDisplayed = false; + + public enum ProcessingOrder { + NO_ORDER, + ASCENDING_ORDER + } + + public enum ProcessingState { + PROCESSING, + WAITING, + PAUSED, + STOPPED + } + + public Process(String name, Buffer[] inputs, Buffer output) { + this.inputs = inputs; + + this.output = output; + this.name = name; + + init(); + } + public Process(String name, Buffer input, Buffer output) { + inputs = new Buffer[1]; + inputs[0] = input; + + this.output = output; + this.name = name; + + init(); + } + + public void init(){ + bufferEvents = new LinkedList<>(); + thread = new Thread(this); + + output.setName(name); + output.setType(getType()); + + switch (getProcessingOrder()){ + case ASCENDING_ORDER: + output.setProcessingOrder(ProcessingOrder.ASCENDING_ORDER); + for (int i = 0; i < inputs.length; i++) { + if(inputs[i].getProcessingOrder()!=ProcessingOrder.ASCENDING_ORDER) { + output.setProcessingOrder(ProcessingOrder.NO_ORDER); + break; + } + } + break; + } + + inputsInterupted = new boolean[inputs.length]; + + for(int i=0; i output() { + return output; + } + + public String name(){ + return name; + } + + public void setInput(int inputID, Buffer newInput){ + + inputs[inputID].disconnectProcess(this); + newInput.connectProcess(this); + inputs[inputID] = newInput; + + + + } + + public void addBufferEvent(BufferEvent e){ + + if(e.getType()== BufferEvent.Type.SAMPLE_ADDED) { + bufferEvents.addLast(new Pair<>(e, updateID)); + emitProcessEvent(ProcessEvent.Type.PROGRESS_CHANGED); + }else { + if (e.getType() == BufferEvent.Type.INTERRUPT_PROCESS) { + int bufferID = 0; + for (int i = 1; i < inputs.length; i++) { + if (inputs[i] == e.getSource()) + bufferID = i; + } + inputsInterupted[bufferID] = true; + + if (isAllInputInterrupted()) { + interuptAfter = true; + emitProcessEvent(ProcessEvent.Type.INTERRUPT_AFTER); + } + } else if (e.getType() == BufferEvent.Type.INVALIDATE) { + boolean b = pause(); + bufferEvents.clear(); + if (b) + start(); + } else { + + for (int i = 0; i < bufferEvents.size(); i++) + if (bufferEvents.get(i).first().getSource() == e.getSource()) { + bufferEvents.remove(i); + break; + } + + bufferEvents.addFirst(new Pair<>(e, updateID)); + emitProcessEvent(ProcessEvent.Type.PROGRESS_CHANGED); + } + } + } + + public void run() { + while (true) { + propertyManager().lockProperty(); + while (!bufferEvents.isEmpty()&&!paused) { + setWaiting(false); + Pair event = bufferEvents.pollFirst(); + if(event==null) + continue; + bProcessor.processBufferEvent(event.first(), event.second()); + + + emitProcessEvent(ProcessEvent.Type.PROGRESS_CHANGED); + try { + Thread.sleep(0); + } catch (InterruptedException e) { + clean(); + return; + } + } + + setWaiting(true); + + if(interuptAfter){ + clean(); + return; + } + + try { + propertyManager().unlockProperty(); + Thread.sleep(50); + propertyManager().lockProperty(); + } catch (InterruptedException e) { + clean(); + return; + } + + while(paused) + try { + Thread.sleep(500); + } catch (InterruptedException e) { + clean(); + return; + } + } + } + + protected void clean(){ + properties.unlockProperty(); + pause(); + output.lastSampleAdded(); + } + + /** + * Démarre le traitement + * @return Renvoie vrai si le traitement n'était pas déjà démarré + */ + + public boolean start(){ + if(thread.isAlive()) { + if(paused) { + paused = false; + return true; + } + + return false; + } + + interuptAfter = false; + paused = false; + + thread = new Thread(this); + thread.setName(name); + thread.start(); + + emitProcessEvent(ProcessEvent.Type.PAUSED_STATE_CHANGED); + + return true; + } + + + /** + * Interompt le traitement + * @return Renvoie vrai si le traitement était lancé + */ + public boolean interrupt(){ + if(!thread.isAlive()) + return false; + + thread.interrupt(); + emitProcessEvent(ProcessEvent.Type.PAUSED_STATE_CHANGED); + return true; + } + + + /** + * Interompt le traitement une fois que tout les événements ont été traités + * @return Renvoie vrai si le traitement était lancé + */ + + + public boolean stopAfterEventsClean(){ + if(!thread.isAlive()) + return false; + + emitProcessEvent(ProcessEvent.Type.INTERRUPT_AFTER); + interuptAfter = true; + return true; + } + + /** + * Mais en pause le traitement sans supprimer le thread + * @return Renvoie vrai si le traitement était lancé + */ + + + public boolean pause(){ + if(!thread.isAlive()) + return false; + + paused = true; + emitProcessEvent(ProcessEvent.Type.PAUSED_STATE_CHANGED); + return true; + } + + + public void recomputeAll(){ + output.clear(); + interrupt(); + properties.unlockProperty(); + bufferEvents.clear(); + bProcessor = initBufferEventProcessor(); + for(Buffer input: inputs()){ + ArrayList indexes = input.getSamplesIndex(); + for(Integer index:indexes) + addBufferEvent(new BufferEvent(BufferEvent.Type.SAMPLE_ADDED, index, input)); + } + start(); + + for(Process p:output().getUsedBy()) + p.recomputeAll(); + } + + public boolean isAllInputInterrupted(){ + for (boolean anInputsInterupted : inputsInterupted) { + if (!anInputsInterupted) + return false; + } + return true; + } + + private void setWaiting(boolean waiting){ + if(this.waiting == waiting) + return; + + this.waiting = waiting; + emitProcessEvent(ProcessEvent.Type.WAITING_STATE); + } + + /** + * Renvoie le nom du processus + * @return Le nom + */ + public String getName() { + return name; + } + + /** + * Renvoie le buffer de sortie + * @return buffer + */ + public Buffer getOutput() { + return output; + } + + /** + * Renvoie un buffer d'entrée + * @param index index du buffer d'entrée + * @return Le buffer correspondant /!\ Peut renvoyer null si l'index n'est pas correct /!\ + */ + public Buffer getInputs(int index) { + if(index>=inputs.length || index<1) + return null; + + return inputs[index]; + } + + + /** + * Renvoie le nombre de buffer d'entrée + * @return Nombre d'inputs + */ + public int getInputsNbr(){ + return inputs.length; + } + + /** + * Renvoie la liste des buffersEvent non traités + * @return liste des buffersEvent + */ + public ArrayList getBufferEventsQueue() { + ArrayList r = new ArrayList<>(bufferEvents.size()); + r.addAll(bufferEvents.stream().map(Pair::first).collect(Collectors.toList())); + + return r; + } + + /** + * Renvoie les propriétés associées à cet algorythme + * @return le propertiesManager du process + */ + public PropertiesManager propertyManager() { + return properties; + } + + /** + * Renvoie l'état du processus + * @return Vrai si tout l'algo est en attente d'événement sur les canaux d'entrées + */ + public boolean isWaiting() { + return waiting; + } + + /** + * Renvoie l'état du processus + * @return Vrai si l'algo est en pause + */ + public boolean isPaused() { + return paused; + } + + /** + * Renvoie l'état du processus + * @return Vrai si le processus est lancé + */ + public boolean isRunning(){ + return thread.isAlive(); + } + + + /** + * Renvoie la valeur de la progression du process + * @return la progression: 0 -> aucun échantillon traité; 1 -> tous les échantillons ont été traités + */ + public float getProgress(){ + if(bufferEvents.size() == 0) + return 0; + double n = getSampleNbrPerIteration(); + return 1-(float)Math.log(n)/(float)Math.log(bufferEvents.size()+n-1); + } + + protected float getSampleNbrPerIteration(){return 10;} + + /** + * Renvoie le type du buffer de sortie + * @return La class des échantillones du buffer de sortie + */ + protected abstract Class getType(); + + protected abstract ProcessingOrder getProcessingOrder(); + + + protected interface BufferEventProcessor { + void processBufferEvent(BufferEvent e, int updateID); + } + + protected abstract BufferEventProcessor initBufferEventProcessor(); + + public interface Listener{ + public void processEvent(ProcessEvent event); + } + + /** + * Connecte ce process à un listener qu'il fournit, de manière à propager les BufferEvent jusqu'à cet objet + * @param p objet à connecter + * @return Vrai si l'opération à fontionner (si le listener n'était pas déjà ajouté) + */ + public boolean addListener(Listener p){ + if(listeners.contains(p)) + return false; + + listeners.add(p); + return true; + } + + /** + * Déconnecte ce process d'un listener + * @param p listener à déconnecter + * @return Vrai si l'opération à fontionner (si le listener était dans la liste) + */ + public boolean removeListener(Listener p){ + int id = listeners.indexOf(p); + if(id == -1) + return false; + + listeners.remove(id); + return true; + } + + private void emitProcessEvent(ProcessEvent event) { + + SwingUtilities.invokeLater(() -> { + for (Listener p : listeners) + p.processEvent(event); + }); + + } + + private void emitProcessEvent(ProcessEvent.Type type) { + emitProcessEvent(ProcessEvent.eventFrom(type, this)); + + } + /** + * Indique si la box du buffer est affichée dans le ProcessSumUp ou non. + * @return true si elle est affichée, false sinon. + */ + public boolean isBoxDisplayed() { + return boxDisplayed; + } + + /** + * Change l'état d'affichage de la box du Buffer dans le ProcessSumUp. N'appeler que lorsque le système crée la Box. + * @param boxDisplayed L'état : true si on l'affiche, false si on l'enlève. + */ + public void setBoxDisplayed(boolean boxDisplayed) { + this.boxDisplayed = boxDisplayed; + } +} \ No newline at end of file diff --git a/src/processing/processes/ProcessEvent.java b/src/processing/processes/ProcessEvent.java new file mode 100644 index 0000000..da129e0 --- /dev/null +++ b/src/processing/processes/ProcessEvent.java @@ -0,0 +1,48 @@ +package processing.processes; + +/** + * Created by gaby on 25/05/14. + * Caractérise les différents evenements qui peuvent intervenir sur les processus qui tournent */ +public class ProcessEvent { + + //Les différents types d'évènements + public enum Type{ + PAUSED_STATE_CHANGED, + WAITING_STATE, + PROGRESS_CHANGED, + NAME_CHANGED, + INTERRUPT_AFTER + } + private Type type; + private String name; + private boolean isPaused; + private boolean isWaiting; + + public ProcessEvent(Type type, String name, boolean isPaused, boolean isWaiting) { + this.type = type; + this.name = name; + this.isPaused = isPaused; + this.isWaiting = isWaiting; + } + + /** Renvoie l'évenement lié au process donné en paramètre, en fonction du type donné en paramètre */ + public static ProcessEvent eventFrom(Type type, Process process){ + return new ProcessEvent(type, process.getName(), process.isPaused(), process.isWaiting()); + } + + public Type getType() { + return type; + } + + public String getName() { + return name; + } + + public boolean isPaused() { + return isPaused; + } + + public boolean isWaiting() { + return isWaiting; + } +} diff --git a/src/processing/properties/BooleanProperty.java b/src/processing/properties/BooleanProperty.java new file mode 100644 index 0000000..7647866 --- /dev/null +++ b/src/processing/properties/BooleanProperty.java @@ -0,0 +1,49 @@ +package processing.properties; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Control; + +import java.util.ArrayList; + +/** + * Created by gaby on 27/05/14. + */ +public class BooleanProperty extends Property { + ArrayList nodes = new ArrayList<>(); + + public BooleanProperty(String name, boolean value) { + super(name, Boolean.class, value); + } + + public BooleanProperty(String name){ + this(name, false); + } + + /** + * Retourne l'élément javaFX de modification de la propriété + * + * @return L'élément graphique lié à la propriété + */ + @Override + public Control createEditNode() { + CheckBox node = new CheckBox(); + node.setOnAction(actionEvent -> { + if(node.isSelected()!=getValue()) + nodeValueModified(node.isSelected()); + }); + node.setSelected(getValue()); + return node; + } + + /** + * Appeler lorsque la proriété est modifier par autre chose que le l'élément graphique, + * il faut donc mettre à jour se dernier + */ + @Override + protected void syncNodesValue() { + for(CheckBox c: nodes) + c.setSelected(getApparentValue()); + } +} diff --git a/src/processing/properties/ChoiceProperty.java b/src/processing/properties/ChoiceProperty.java new file mode 100644 index 0000000..f7e64ca --- /dev/null +++ b/src/processing/properties/ChoiceProperty.java @@ -0,0 +1,131 @@ +package processing.properties; + +import generictools.Pair; +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Control; + +import java.awt.image.ComponentColorModel; +import java.util.ArrayList; + +/** + * Created by gaby on 27/05/14. + */ +public class ChoiceProperty extends Property{ + ArrayList> choices; + private int currentID = 0; + ArrayList> comboBoxs = new ArrayList<>(); + + public ChoiceProperty(String name, ArrayList> choices, Class type, int defaultIndex) { + super(name, type, choices.get(defaultIndex).first()); + currentID = defaultIndex; + this.choices = choices; + } + + public ChoiceProperty(String name, ArrayList> choices, Class type) { + this(name, choices, type, 0); + } + + /** Modifie la valeur associée à chaque choix possible + * @param choices l'ensemble des choix possibles sous forme d'ArrayList de Pair */ + public void forceChoices(ArrayList> choices){ + if(choices.size()==0) + return; + + if(getSelectedItemIndex()>=choices.size()) + setSelectedItem(0); + + this.choices = choices; + + Platform.runLater(new Runnable() { + @Override + public void run() { + for(ComboBox c: comboBoxs){ + c.getItems().clear(); + for(Pair choice : choices) + c.getItems().add(choice.second()); + } + } + }); + + setSelectedItem(getSelectedItemIndex()); + } + + /** Renvoie l'indice de l'élément qui est selectionné en ce moment */ + public int getSelectedItemIndex(){ + return currentID; + } + + /** Renvoie le nom de l'élément sélectionné en ce moment */ + public String getSelectedItemName(){ + return choices.get(currentID).second(); + } + + /** Redéfinit la valeur actuelle + * @param item le nouvel élément + * @return vrai si ça a fonctionné, faux sinon */ + public boolean setValue(T item){ + if(item == getValue()) + return false; + + int id = 0; + for(id=0; id(item,item.toString())); + + return setSelectedItem(id); + } + + /** Rétrocontrole sur l'interface + * @param index l'indice de l'item sélectionné + * @return vrai si ça a fonctionné, faux sinon */ + public boolean setSelectedItem(int index){ + if(index<0 || index >= choices.size()) + return false; + + currentID = index; + return super.setValue(choices.get(index).first()); + } + + + /** + * Retourne l'élément javaFX de modification de la propriété + * + * @return L'élément graphique lié à la propriété + */ + @Override + public Control createEditNode() { + + ComboBox comboBox = new ComboBox<>(); + + for(Pair c : choices) + comboBox.getItems().add(c.second()); + + comboBox.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent actionEvent) { + if(comboBox.getSelectionModel().getSelectedIndex() != getSelectedItemIndex()) + setSelectedItem(comboBox.getSelectionModel().getSelectedIndex()); + } + }); + comboBox.getSelectionModel().select(getSelectedItemIndex()); + comboBoxs.add(comboBox); + return comboBox; + } + + /** + * Appeler lorsque la proriété est modifier par autre chose que le l'élément graphique, + * il faut donc mettre à jour se dernier + */ + @Override + protected void syncNodesValue() { + for(ComboBox c: comboBoxs) + c.getSelectionModel().select(getSelectedItemIndex()); + } +} diff --git a/src/processing/properties/FloatProperty.java b/src/processing/properties/FloatProperty.java new file mode 100644 index 0000000..9274b07 --- /dev/null +++ b/src/processing/properties/FloatProperty.java @@ -0,0 +1,126 @@ +package processing.properties; + +import com.sun.org.apache.bcel.internal.generic.FLOAD; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.Control; +import javafx.scene.control.TextField; + +import java.util.ArrayList; + +/** + * Created by gaby on 27/05/14. + */ +public class FloatProperty extends Property { + + ArrayList textFields = new ArrayList<>(); + float min = Float.MIN_VALUE; + float max = Float.MAX_VALUE; + + public FloatProperty(String name, float value){ + super(name, Float.class, value); + } + + + public FloatProperty(String name) { + this(name, 0); + } + + + public FloatProperty(String name, float value, float min, float max){ + this(name, value); + setMin(min); + setMax(max); + } + + /** + * Modifie la valeur de la propriété en s'assurant qu'elle respecte les conditions de minimum et de maximum + * @param value la nouvelle valeur + * @return Renvoie vrai si l'opération s'est bien déroulé (si la valeur est dans l'intervale autorisé) + */ + public boolean setValue(Float value){ + if(value>=min && value<=max) + return super.setValue(value); + + return false; + } + + /** + * Modifie la valeur maximale de l'intervale autorisé et modifie la valeur min si l'intervale est incorrect + * @param max nouvelle valeur max + */ + public void setMax(float max){ + this.max = max; + if(min>max) + min = max; + } + + /** + * Modifie la valeur minimale de l'intervale autorisé et modifie la valeur max si l'intervale est incorrect + * @param min nouvelle valeur min + */ + public void setMin(float min){ + this.min = min; + if(min>max) + max = min; + } + + public float getMin() { + return min; + } + + public float getMax() { + return max; + } + + /** + * Retourne l'élément javaFX de modification de la propriété + * + * @return L'élément graphique lié à la propriété + */ + @Override + public Control createEditNode() { + TextField textField = new TextField(); + textField.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent actionEvent) { + float v = 0; + try{ + v = Float.parseFloat(textField.getText()); + }catch (NumberFormatException e){ + textField.setText(getValue().toString()); + return; + } + + if(v==getValue()) + return; + + if(vmax){ + textField.setText(String.valueOf(max)); + nodeValueModified(max); + return; + } + + nodeValueModified(v); + } + }); + textFields.add(textField); + textField.setText(getValue().toString()); + return textField; + } + + /** + * Appeler lorsque la proriété est modifier par autre chose que le l'élément graphique, + * il faut donc mettre à jour se dernier + */ + @Override + protected void syncNodesValue() { + for(TextField t: textFields) + t.setText(getApparentValue().toString()); + } +} diff --git a/src/processing/properties/IntProperty.java b/src/processing/properties/IntProperty.java new file mode 100644 index 0000000..73cd4bd --- /dev/null +++ b/src/processing/properties/IntProperty.java @@ -0,0 +1,129 @@ +package processing.properties; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.Control; +import javafx.scene.control.TextField; + +import java.util.ArrayList; + +/** + * Created by gaby on 27/05/14. + */ +public class IntProperty extends Property { + ArrayList textFields = new ArrayList<>(); + int min = Integer.MIN_VALUE; + int max = Integer.MAX_VALUE; + + + public IntProperty(String name, int value){ + super(name, Float.class, value); + } + + public IntProperty(String name) { + this(name, 0); + } + + + public IntProperty(String name, int value, int min, int max){ + this(name); + setMin(min); + setMax(max); + setValue(value); + } + + /** + * Modifie la valeur de la propriété en s'assurant qu'elle respecte les conditions de minimum et de maximum + * @param value la nouvelle valeur + * @return Renvoie vrai si l'opération s'est bien déroulé (si la valeur est dans l'intervale autorisé) + */ + public boolean setValue(Integer value){ + if(value>=min && value<=max) + return super.setValue(value); + + return false; + } + + /** + * Modifie la valeur maximale de l'intervale autorisé et modifie la valeur min si l'intervale est incorrect + * @param max nouvelle valeur max + */ + public void setMax(int max){ + this.max = max; + if(min>max) + min = max; + if(getValue()>max) + setValue(max); + } + + /** + * Modifie la valeur minimale de l'intervale autorisé et modifie la valeur max si l'intervale est incorrect + * @param min nouvelle valeur min + */ + public void setMin(int min){ + this.min = min; + if(min>max) + max = min; + if(getValue()() { + @Override + public void handle(ActionEvent actionEvent) { + int v = 0; + try{ + v = Integer.parseInt(textField.getText()); + }catch (NumberFormatException e){ + textField.setText(getValue().toString()); + return; + } + + if(v==getValue()) + return; + + if(vmax){ + textField.setText(String.valueOf(max)); + nodeValueModified(max); + return; + } + + nodeValueModified(v); + } + }); + textFields.add(textField); + textField.setText(getValue().toString()); + return textField; + } + + /** + * Appeler lorsque la proriété est modifier par autre chose que le l'élément graphique, + * il faut donc mettre à jour se dernier + */ + @Override + protected void syncNodesValue() { + for(TextField t: textFields) + t.setText(getApparentValue().toString()); + } +} diff --git a/src/processing/properties/PropertiesManager.java b/src/processing/properties/PropertiesManager.java new file mode 100644 index 0000000..b349dfb --- /dev/null +++ b/src/processing/properties/PropertiesManager.java @@ -0,0 +1,81 @@ +package processing.properties; + +import java.util.ArrayList; + +/** + * Created by gaby on 03/04/14. + * Gestionnaire des propriétés. Permet de controler toutes les propriétés associées à chaque process. + */ +public class PropertiesManager { + + private final ArrayList> properties; + + public PropertiesManager(){ + properties = new ArrayList<>(); + } + + /** Permet d'ajouter une propriété */ + public int addProperty(Property property){ + for (Property property1 : properties) { + if (property.getName() == property1.getName()) + return -1; + } + properties.add(property); + + return properties.size()-1; + } + + + /** Récupère une propriété grâce à son identifiant */ + public Property getProperty(int id){ + if(id >= properties.size() || id < 0) + return null; + + return properties.get(id); + } + + /** Renvoie la propriété donnée identifiée par son noim + * @param name le nom de la propriété + * @return la propriété */ + public Property getProperty(String name){ + return getProperty(getPropertyIndex(name)); + } + + public int getPropertyIndex(Property property){ + return properties.indexOf(property); + } + + /** Renvoie l'indice de la propriété grâce à son nom + * @param name le nom de la propriété + * @return l'indice de la propriété */ + public int getPropertyIndex(String name){ + for (int i = 0; i < properties.size(); i++) { + if(name.equals(properties.get(i).getName())) + return i; + } + return -1; + } + + /** Renvoie le nombre de propriétés ajoutées */ + public int getNbProperties() { + return properties.size(); + } + + /** Permet de bloquer une propriété: la rend non modifiable pendant qu'un process l'utilise */ + public void lockProperty(){ + for (int i = 0; i < properties.size(); i++) { + properties.get(i).lock(); + } + } + + /** Permet de débloquer une propriété : la rend modifiable dès qu'elle n'est plus utilisée */ + public boolean unlockProperty(){ + boolean r = false; + for (int i = 0; i < properties.size(); i++) { + if(properties.get(i).unlock()) + r = true; + } + return r; + } + +} diff --git a/src/processing/properties/Property.java b/src/processing/properties/Property.java new file mode 100644 index 0000000..a9b8077 --- /dev/null +++ b/src/processing/properties/Property.java @@ -0,0 +1,168 @@ +package processing.properties; + +import javafx.application.Platform; +import javafx.scene.control.Control; + +import java.util.ArrayList; + +/** + * Created by gaby on 03/04/14. + */ +public abstract class Property { + private final String name; + private T value; + + private boolean locked = false; + private T temporaryValue; + + private final Class type; + + private final ArrayList> listeners; + + public Property(String name, Class type, T value) { + this.name = name; + this.type = type; + this.value = value; + + listeners = new ArrayList<>(); + } + + /** + * Récupère la valeur de la propriété + * @return le valeur de la propriété + */ + public final T getValue() { + return value; + } + + public final T getApparentValue(){ + if(locked) + return temporaryValue; + return value; + } + + /** + * Modifie la valeur de la propriété + * @param value la nouvelle valeur + * @return Renvoie vrai si l'opération s'est bien déroulé (mais peut-elle mal se passer?) + */ + public boolean setValue(final T value) { + nodeValueModified(value); + return true; + } + + /** + * Méthode à appeler pour gerer la modification de la valeur de la propriété en interne + * @param value la nouvelle valeur + */ + protected final void nodeValueModified(final T value){ + if(locked) + temporaryValue = value; + else + changeRealValue(value); + + Platform.runLater(new Runnable() { + @Override + public void run() { + syncNodesValue(); + } + }); + } + + private final void changeRealValue(final T value){ + T previousValue = this.value; + this.value = value; + emitChangeEvent(previousValue); + } + + + //-------------------------- Manage Event Listener ----------------------- + + private void emitChangeEvent(T previousValue){ + for(Listener p: listeners) + p.propertyChange(this,previousValue); + } + + /** + * Ajoute un listener à cette propriété, lorsquelle sera modifié, le listener sera prévenu + * @param listener le listener à ajouter + * @return Vrai si l'opération d'ajout s'est bien déroulé (si le listener n'avais pasété déjà ajouté) + */ + public final boolean addListener(Listener listener){ + if(listeners.contains(listener)) + return false; + + listeners.add(listener); + return true; + } + + /** + * Supprime le listener de la liste des listeners liés à cette propriété + * @param listener le listener à supprimer + * @return Renvoie vrai si le listener existait dans la liste et à bien été supprimer + */ + public final boolean removeListener(Listener listener){ + if(!listeners.contains(listener)) + return false; + + listeners.remove(listener); + return true; + } + + + public interface Listener { + public void propertyChange(Property source, T previousValue); + } + + + //-------------------------- Manage Type ------------------------------- + + /** + * Retourne le type de la propriété (equivalent T) + * @return Le type de la proprriété + */ + public final Class getType() { + return type; + } + + + /** + * @return le nom de la propriété + */ + public final String getName() { + return name; + } + + /** + * Retourne l'élément javaFX de modification de la propriété + * @return L'élément graphique lié à la propriété + */ + public abstract Control createEditNode(); + + /** + * Appeler lorsque la proriété est modifier par autre chose que le l'élément graphique, + * il faut donc mettre à jour se dernier + */ + protected abstract void syncNodesValue(); + + + public void lock(){ + if(locked) + unlock(); + temporaryValue = value; + locked = true; + } + public boolean unlock(){ + if(temporaryValue==value) + return false; + + changeRealValue(temporaryValue); + temporaryValue = null; + locked = false; + return true; + } + + public boolean isLocked() { + return locked; + } +} diff --git a/src/processing/properties/StringProperty.java b/src/processing/properties/StringProperty.java new file mode 100644 index 0000000..1f01d41 --- /dev/null +++ b/src/processing/properties/StringProperty.java @@ -0,0 +1,56 @@ +package processing.properties; + +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.TextField; +import javafx.scene.text.Text; + +import java.util.ArrayList; + +/** + * Created by gaby on 27/05/14. + */ +public class StringProperty extends Property { + ArrayList textFields = new ArrayList<>(); + + public StringProperty(String name, String value) { + super(name, String.class, value); + } + + public StringProperty(String name){ + this(name, ""); + } + + /** + * Retourne l'élément javaFX de modification de la propriété + * + * @return L'élément graphique lié à la propriété + */ + @Override + public Control createEditNode() { + TextField textField = new TextField(getValue()); + textField.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent actionEvent) { + if(getValue()!=textField.getText()) + nodeValueModified(textField.getText()); + + } + }); + textFields.add(textField); + return textField; + } + + /** + * Appeler lorsque la proriété est modifier par autre chose que le l'élément graphique, + * il faut donc mettre à jour se dernier + */ + @Override + protected void syncNodesValue() { + for(TextField t:textFields) + t.setText(getApparentValue()); + } +} \ No newline at end of file