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

}