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