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: * - scaleFactor: the scale factor of each pixel of the sound wave representation. The bigger the
f@0: * scale factor, the lower the resolution. a scale factor ranges from 1, highest resolution supported by
f@0: * the {@code SoundWave} instance, to the max scale factor supported by the backing {@code SoundWave} model - {@code int}
f@0: * - cursorPos: the position of the cursor - {@code int}
f@0: * - mouseDragSelection: the range of the selection on this track as the user drags the mouse - {@code SelectionRange}
f@0: * - the preferred size - {@code Dimension}
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: }