f@0: /* f@0: Cross-Modal DAW Prototype - Prototype of a simple Cross-Modal Digital Audio Workstation. f@0: f@0: Copyright (C) 2015 Queen Mary University of London (http://depic.eecs.qmul.ac.uk/) f@0: f@0: This program is free software: you can redistribute it and/or modify f@0: it under the terms of the GNU General Public License as published by f@0: the Free Software Foundation, either version 3 of the License, or f@0: (at your option) any later version. f@0: f@0: This program is distributed in the hope that it will be useful, f@0: but WITHOUT ANY WARRANTY; without even the implied warranty of f@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the f@0: GNU General Public License for more details. f@0: f@0: You should have received a copy of the GNU General Public License f@0: along with this program. If not, see . f@0: */ f@0: package uk.ac.qmul.eecs.depic.daw.gui; f@0: f@0: import java.awt.Color; f@0: import java.awt.Dimension; f@0: import java.awt.Graphics; f@0: import java.awt.Graphics2D; f@0: import java.awt.Point; f@0: import java.awt.Rectangle; f@0: import java.awt.event.ActionEvent; f@0: import java.awt.event.ActionListener; f@0: import java.awt.geom.Line2D; f@0: import java.awt.geom.Point2D; f@0: f@0: import javax.swing.JPanel; f@0: import javax.swing.Timer; f@0: import javax.swing.event.ChangeEvent; f@0: import javax.swing.event.ChangeListener; f@0: f@0: import uk.ac.qmul.eecs.depic.daw.Automation; f@0: import uk.ac.qmul.eecs.depic.daw.Chunk; f@0: import uk.ac.qmul.eecs.depic.daw.Daw; f@0: import uk.ac.qmul.eecs.depic.daw.Selection; f@0: import uk.ac.qmul.eecs.depic.daw.SoundType; f@0: import uk.ac.qmul.eecs.depic.daw.SoundWave; f@0: import uk.ac.qmul.eecs.depic.daw.SoundWaveEvent; f@0: import uk.ac.qmul.eecs.depic.daw.SoundWaveListener; f@0: import uk.ac.qmul.eecs.depic.daw.Wave; f@0: import uk.ac.qmul.eecs.depic.daw.haptics.HapticViewPort; f@0: import uk.ac.qmul.eecs.depic.patterns.Sequence; f@0: import uk.ac.qmul.eecs.depic.patterns.SequenceMapping; f@0: f@0: /** f@1: * An audio track widget. f@0: * f@0: * f@0: * f@0: * This class has the following bound properties : f@0: * f@0: * f@0: * f@0: * f@0: */ f@0: public class AudioTrack extends JPanel implements SoundWaveListener { f@0: private static final long serialVersionUID = 1L; f@0: f@0: public static final int MAX_TRACK_HEIGHT = 130; f@0: public static final int MAX_TRACK_WIDTH = Integer.MAX_VALUE; f@0: f@0: public static final Color WAVE_COLOR = new Color(7,47,140); f@0: public static final Color CURSOR_COLOR = new Color(44,47,56); f@0: public static final Color SELECTION_COLOR = new Color(218,218,240); f@0: public static final Color OVERLAY_BG_COLOR = new Color(222,222,235); f@0: public static final Color VIEW_PORT_COLOR = Color.GREEN; f@0: f@0: /* these fields are used by friend classes * f@0: * they're final to protect them from being changed */ f@0: final AudioTrackInput trackInteraction; f@0: final AudioTrackSonification trackSonification; f@0: f@0: private SequenceGraph automationGraph; f@0: private SequenceGraph peakMeterGraph; f@0: private final CursorUpdaterOnPlay clipInteraction; f@0: protected SoundWave soundWave; f@0: f@0: private HapticViewPort hapticViewPort; f@0: f@0: private Selection currentSelection; f@0: private int scaleFactor; // bound property f@0: private int cursorPos; // bound property f@0: private float secondsPerPixel; f@0: private boolean showHapticViewPort; f@0: boolean showAutomationSound; f@0: boolean showPeakLevelSound; f@0: private boolean showDbWave; f@0: f@0: public AudioTrack(SoundWave soundWave){ f@0: if(soundWave == null) f@0: throw new IllegalArgumentException("soundWave cannot be null"); f@0: f@0: this.soundWave = soundWave; f@0: soundWave.addSoundWaveListener(this); f@0: f@0: f@0: setBackground(Color.WHITE); f@0: f@0: scaleFactor = 1; f@0: currentSelection = new Selection(0,scaleFactor); f@0: f@0: /* set up sequence graphs */ f@0: ChangeListener sequenceGraphListener = new ChangeListener(){ f@0: @Override f@0: public void stateChanged(ChangeEvent e) { f@0: repaint(); f@0: } f@0: }; f@0: f@0: automationGraph = new SequenceGraph(Color.WHITE, getSize()); f@0: automationGraph.addChangeListener(sequenceGraphListener); f@0: peakMeterGraph = new SequenceGraph(Color.GRAY, getSize()); f@0: peakMeterGraph.addChangeListener(sequenceGraphListener); f@0: f@0: clipInteraction = new CursorUpdaterOnPlay(); f@0: trackInteraction = new AudioTrackInput(this); f@0: trackSonification = new AudioTrackSonification(this); f@0: hapticViewPort = new HapticViewPort(1); f@0: f@0: setMaximumSize(new Dimension(MAX_TRACK_WIDTH,MAX_TRACK_HEIGHT)); f@0: //setMinimumSize(new Dimension(100,TRACK_HEIGHT)); f@0: setPreferredSize(new Dimension(0,MAX_TRACK_HEIGHT)); f@0: f@0: this.setFocusable(true); f@0: f@0: addPropertyChangeListener(trackSonification); f@0: addMouseListener(trackInteraction); f@0: addMouseMotionListener(trackInteraction); f@0: addKeyListener(trackInteraction); f@0: } f@0: f@0: @Override f@0: public void update(SoundWaveEvent evt) { // FIXME fare file closed f@0: String evtType = evt.getType(); f@0: if(SoundWaveEvent.OPEN.equals(evtType)) { f@0: SoundWave wave = evt.getSource(); f@0: hapticViewPort.setTrackSize(wave.getChunkNum()); // FIXME check f@0: _setScaleFactor(wave.getScaleFactor()); f@0: setCursorPos(0); f@0: } else if(SoundWaveEvent.SCALE_FACTOR_CHANGED.equals(evtType)){ f@0: _setScaleFactor((Integer)evt.getArgs()); f@0: }else if (SoundWaveEvent.SELECTION_CHANGED.equals(evtType)){ f@0: Selection oldSelection = currentSelection; f@0: currentSelection = (Selection)evt.getArgs(); f@0: /* update the mouse interaction object accordingly */ f@0: trackInteraction.setMouseSelection(currentSelection.getStart(), currentSelection.getEnd()); f@0: firePropertyChange("mouseDragSelection",oldSelection,currentSelection); f@0: repaint(); f@0: f@0: }else if(SoundWaveEvent.POSITION_CHANGED.equals(evtType)){ f@0: /* update the position according to the Selection object returned by evt.getArgs() * f@0: * the selection is open. The Selection's scale factor is also taken into account * f@0: * when calculating the new position. In particular the ratio between the * f@0: * selction's scale factor and this object current scale factor */ f@0: setCursorPos((Integer)evt.getArgs()); f@0: f@0: f@0: }else if(SoundWaveEvent.START.equals(evtType) || f@0: SoundWaveEvent.STOP.equals(evtType) || f@0: SoundWaveEvent.PAUSE.equals(evtType)){ f@0: clipInteraction.update(evt); f@0: }else if (SoundWaveEvent.AUTOMATION_CHANGED.equals(evtType)){ f@0: Automation automation = (Automation)evt.getArgs(); f@0: f@0: /* if it's an automation different from NONE, paint the overlay: make the bg darker */ f@0: if(Automation.NONE_AUTOMATION.equals(automation)){ f@0: setBackground(Color.WHITE); f@0: automationGraph.removeChangeListener(trackSonification); f@0: }else{ f@0: setBackground(OVERLAY_BG_COLOR); f@0: } f@0: f@0: /* resize automation graph, if currently showing any */ f@0: automationGraph.setMillisecPerPixel(evt.getSource().getMillisecPerChunk()); f@0: automationGraph.setColor(automation.getColor()); f@0: automationGraph.setSize(getSize()); f@0: automationGraph.setSequence(automation); f@0: automationGraph.addChangeListener(trackSonification); f@0: f@0: }else if(SoundWaveEvent.CLOSE.equals(evtType) || f@0: SoundWaveEvent.CUT.equals(evtType) || f@0: SoundWaveEvent.PASTE.equals(evtType) || f@0: SoundWaveEvent.INSERT.equals(evtType)){ f@0: hapticViewPort.setTrackSize(evt.getSource().getChunkNum()); f@0: repaint(); f@0: }else if(SoundWaveEvent.PEAK_METER.equals(evtType)){ f@0: f@0: peakMeterGraph.setMillisecPerPixel(evt.getSource().getMillisecPerChunk()); f@0: peakMeterGraph.setSize(getSize()); f@0: /* listens to the changes, so it will repaint after setSequence */ f@0: peakMeterGraph.setSequence((Sequence)evt.getArgs()); f@0: f@0: } f@0: } f@0: f@0: @Override f@0: public void paintComponent(Graphics g){ f@0: super.paintComponent(g); f@0: Graphics2D g2 = (Graphics2D)g; f@0: Color oldColor = g2.getColor(); f@0: f@0: /* get height and width. needed for painting */ f@0: int height = getHeight(); f@0: int width = getWidth(); f@0: f@0: /* paint the selection grey background*/ f@0: Selection mouseSelection = trackInteraction.getMouseSelection(); f@0: if(!mouseSelection.isOpen()){ f@0: g2.setColor(SELECTION_COLOR); f@0: g2.fillRect(mouseSelection.getStart(), 0, Math.abs(mouseSelection.getStart()-mouseSelection.getEnd()), height); f@0: } f@0: f@0: /* paint the central line */ f@0: g2.setColor(CURSOR_COLOR); f@0: g2.draw(new Line2D.Float(0f, height/2,width,height/2)); f@0: f@0: /* paint the sound wave, if any */ f@0: if(soundWave != null ){ f@0: Wave wave = showDbWave ? soundWave.getDbWave() : soundWave; f@0: g2.setColor(WAVE_COLOR); f@0: int horizontalPixel = 0; f@0: for(int i=0; i getSoundWave().getMaxScaleFactor() ){ f@0: Daw.getSoundEngineFactory().getSharedSonification().play(SoundType.ERROR); f@0: return; f@0: } f@0: f@0: int oldScaleFactor = scaleFactor; f@0: scaleFactor = factor; f@0: f@0: secondsPerPixel = soundWave.getMillisecPerChunk()/1000; f@0: f@0: /* update haptic view port */ f@0: hapticViewPort.setScaleFactor(scaleFactor); f@0: f@0: /* resize the selection after zooming in/out */ f@0: Selection oldSelection = currentSelection; f@0: int start = (int) (oldSelection.getStart() * Math.pow(2,oldScaleFactor - factor)); f@0: if(oldSelection.isOpen()) f@0: currentSelection = new Selection(start,factor); f@0: else{ f@0: int end = (int) (oldSelection.getEnd() * Math.pow(2,oldScaleFactor - factor)); f@0: currentSelection = new Selection(start,end,factor); f@0: } f@0: f@0: /* if the selection is open, it will be painted as a one-pixel selection */ f@0: trackInteraction.setMouseSelection(currentSelection.getStart(), currentSelection.getEnd()); f@0: f@0: /* make listeners aware the mouse selection has changed */ f@0: firePropertyChange("mouseDragSelection",oldSelection,currentSelection); f@0: f@0: setPreferredSize(new Dimension(soundWave.getChunkNum()+30,MAX_TRACK_HEIGHT)); f@0: revalidate(); f@0: f@0: setCursorPos(soundWave.getCurrentChunkPosition()); f@0: /* update the sequences */ f@0: automationGraph.setSize(getSize()); f@0: automationGraph.setMillisecPerPixel(AudioTrack.this.soundWave.getMillisecPerChunk()); f@0: automationGraph.updateSequence(); f@0: f@0: peakMeterGraph.setSize(getSize()); f@0: peakMeterGraph.setMillisecPerPixel(AudioTrack.this.soundWave.getMillisecPerChunk()); f@0: peakMeterGraph.updateSequence(); f@0: f@0: repaint(); f@0: firePropertyChange("scaleFactor", oldScaleFactor, scaleFactor); f@0: } f@0: f@0: public Selection getSelection(){ f@0: return currentSelection; f@0: } f@0: f@0: /** f@0: * Doesn't change the cursor position of the underlying soundwave f@0: * @param position f@0: */ f@0: public void setCursorPos(int position) { f@0: int oldCursorPos = cursorPos; f@0: cursorPos = position; f@0: f@0: repaint(); f@0: scrollRectToVisible(new Rectangle(new Point(position,0))); f@0: firePropertyChange("cursorPos", oldCursorPos, position); f@0: } f@0: f@0: public int getCursorPos(){ f@0: return cursorPos; f@0: } f@0: f@0: public float getSecondsPerPixel(){ f@0: return secondsPerPixel; f@0: } f@0: f@0: public SequenceGraph getAutomationGraph(){ f@0: return automationGraph; f@0: } f@0: f@0: public SequenceGraph getPeakLevelGraph(){ f@0: return peakMeterGraph; f@0: } f@0: f@0: /** f@0: * Makes it available to {@code Rule} class to notify listeners after changing f@0: * the mouseDragSelection. f@0: * f@0: * @param property the programmatic name of the property that was changed f@0: * @param oldValue the old value of the property f@0: * @param newValue the new value of the property f@0: */ f@0: @Override f@0: protected void firePropertyChange(String property, Object oldValue, Object newValue){ f@0: super.firePropertyChange(property, oldValue, newValue); f@0: } f@0: f@0: public HapticViewPort getHapticViewPort(){ f@0: return hapticViewPort; f@0: } f@0: f@0: public void showHapticViewPort(boolean show){ f@0: showHapticViewPort = show; f@0: repaint(); f@0: } f@0: f@0: public void showAutomationSound(boolean show){ f@0: showAutomationSound = show; f@0: f@0: SequenceMapping sonificationSeqMapping = Daw.getSoundEngineFactory().getSharedSonification() f@0: .getSequenceMapping(SoundType.AUTOMATION); f@0: if(show){ f@0: sonificationSeqMapping.renderCurveAt( f@0: soundWave.getSequence(), f@0: soundWave.getMillisecPerChunk() * cursorPos, f@0: SequenceMapping.DURATION_INF); f@0: }else{ f@0: sonificationSeqMapping.renderCurveAt(null, 0.0f, SequenceMapping.DURATION_STOP); f@0: } f@0: f@0: } f@0: f@0: public void showPeakLevelSound(boolean show){ f@0: showPeakLevelSound = show; f@0: f@0: SequenceMapping sonificationSeqMapping = Daw.getSoundEngineFactory().getSharedSonification() f@0: .getSequenceMapping(SoundType.PEAK_LEVEL); f@0: if(show){ f@0: sonificationSeqMapping.renderCurveAt( f@0: soundWave.getDbWave().getSequence(), f@0: soundWave.getMillisecPerChunk() * cursorPos, f@0: SequenceMapping.DURATION_INF); f@0: }else{ f@0: sonificationSeqMapping.renderCurveAt(null, 0.0f, SequenceMapping.DURATION_STOP); f@0: } f@0: } f@0: f@0: public void showDbWave(boolean show){ f@0: showDbWave = show; f@0: repaint(); f@0: } f@0: f@0: f@0: public int normToWidthconvert(float f){ f@0: return hapticViewPort.getPosition(f); f@0: } f@0: /** f@0: * f@0: * @param s a value sample value ranging from Short.MIN_VALUE to Short.MAX_VALUE f@0: * @param height the height of the graphic component where the track is to be drawn f@0: * @return the Y value in the graphic component resulted from mapping the sample to the graphic component height f@0: */ f@0: public int normToHeightConvert(float f){ f@0: /* in a JPanel pixel y coordinate increases from top to bottom, whereas the * f@0: * in the signed short PCM format y value increased from bottom to top * f@0: * so the sign of the value is reversed to match it with the JPanel space */ f@0: int height = getHeight(); f@0: return height - (int)( f *height); f@0: } f@0: f@0: /** f@0: * Returns a point in the {@code Automation} coordinates - x = length in millis, f@0: * y = range of the automation - corresponding to the point on {@code track} f@0: * which has coordinates {@code (mouseX, mouseY) } f@0: * f@0: * f@0: * @param track f@0: * @param mouseX the {@code x} coordinate of the point on {@code track} f@0: * @param mouseY the {@code y} coordinate of the point on {@code track} f@0: * @return the point in the {@code Automation} coordinates corresponding to (mouseX,mouseY) f@0: * or {@code null} if mouseX is bigger than the automation length (in chunks of the track's {@code SoundWave}) f@0: * or mouseY is bigger than the track's height, or if either is lower than zero. f@0: * f@0: */ f@0: public static Point2D.Float getAutomationCoord(AudioTrack track, int mouseX, int mouseY ){ f@0: SoundWave wave = track.getSoundWave(); f@0: Automation autom = wave.getParametersControl().getCurrentAutomation(); f@0: f@0: /* get height and width of the track panel */ f@0: float height = track.getSize().height; f@0: f@0: float width = track.getSoundWave().getChunkNum(); f@0: f@0: if(mouseX >= width || mouseY > height || mouseX < 0 || mouseY < 0 ){ f@0: return null; f@0: } f@0: f@0: /* calculate the automation x (a.k.a. position over time) from * f@0: * the mouse click according following relation: * f@0: * mouse x / width of the panel = automation x position / length of sample */ f@0: float automationX = (float)(autom.getLen()*(mouseX/width)); f@0: /* calculate the automation y (a.k.a. value int he parameter range) from * f@0: * the mouse click according following relation: * f@0: * mouse y / height of the panel = automation y position / value range * f@0: * since in swing y has 0 at the top the y value is reversed height-mouseY */ f@0: float automationY = (float)(autom.getRange().lenght() *((height-mouseY)/height)); f@0: return new Point2D.Float(automationX,automationY+autom.getRange().getStart()); f@0: } f@0: f@0: f@0: f@0: private class CursorUpdaterOnPlay implements ActionListener { f@0: private static final int REFRESH_DELAY = 50; f@0: Timer timer = new Timer(REFRESH_DELAY,this);; f@0: f@0: public void update(SoundWaveEvent evt) { f@0: if(SoundWaveEvent.START.equals(evt.getType())){ f@0: timer.start(); f@0: }else if(SoundWaveEvent.STOP.equals(evt.getType()) || f@0: SoundWaveEvent.PAUSE.equals(evt.getType())){ f@0: timer.stop(); f@0: } f@0: } f@0: f@0: @Override f@0: public void actionPerformed(ActionEvent e) { f@0: int cursorPos = Math.round(soundWave.getTransportControl().getPlayPosition()/soundWave.getMillisecPerChunk()); f@0: setCursorPos(cursorPos); f@0: } f@0: } // class ClipInteraction f@0: f@0: }