// This file is part of Penn TotalRecall <http://memory.psych.upenn.edu/TotalRecall>.
//
// TotalRecall 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, version 3 only.
//
// TotalRecall 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 TotalRecall. If not, see <http://www.gnu.org/licenses/>.
package components.waveform;
import info.GUIConstants;
import info.MyColors;
import info.MyShapes;
import info.SysInfo;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.DecimalFormat;
import javax.swing.JComponent;
import javax.swing.Timer;
import javax.swing.plaf.ComponentUI;
import components.MyFrame;
import components.annotations.Annotation;
import components.annotations.AnnotationDisplay;
import components.waveform.WaveformBuffer.WaveformChunk;
import control.CurAudio;
import edu.upenn.psych.memory.precisionplayer.PrecisionPlayer;
/**
* This WaveformDisplay is totally autonomous except for changes of zoom factor.
*
* Keep in mind that events other than the repaint timer going off can cause repaints.
*
* @author Yuvi Masory
*/
public class WaveformDisplay extends JComponent {
private final DecimalFormat secFormat = new DecimalFormat("0.000s");
private final int REFRESH_DELAY = 20; //people prefer 20 over 30
private Timer refreshTimer;
private int pixelsPerSecond;
private volatile boolean chunkInProgress;
private static volatile int progressBarXPos;
private long refreshFrame;
private int refreshWidth;
private int refreshHeight;
private WaveformChunk previousRefreshChunk;
private WaveformChunk curRefreshChunk;
private WaveformChunk nextRefreshChunk;
private static WaveformDisplay instance;
private WaveformDisplay() {
setOpaque(true);
setBackground(MyColors.waveformBackground);
setUI(new ComponentUI() {}); //a little bit of magic so the JComponent will draw the background color without subclassing to a JPanel
pixelsPerSecond = GUIConstants.zoomlessPixelsPerSecond;
refreshFrame = -1;
addMouseListener(new MouseAdapter(){
@Override
public void mousePressed(MouseEvent e) {
MyFrame.getInstance().requestFocusInWindow();
}
});
if(SysInfo.sys.mouseMode) {
addMouseListener(new WaveformMouseAdapter(this));
addMouseMotionListener(new WaveformMouseAdapter(this));
}
}
public static WaveformDisplay getInstance() {
if (instance == null) {
instance = new WaveformDisplay();
}
return instance;
}
public static int height() {
return instance.getHeight();
}
public static void zoomX(boolean in) {
if(in) {
instance.pixelsPerSecond += GUIConstants.xZoomAmount;
}
else {
if(instance.pixelsPerSecond >= GUIConstants.xZoomAmount + 1) {
instance.pixelsPerSecond -= GUIConstants.xZoomAmount;
}
}
}
public void startRefreshes() {
ActionListener refresher = new RefreshListener();
refreshTimer = new Timer(REFRESH_DELAY, refresher);
refreshTimer.start();
}
public void stopRefreshes() {
if(refreshTimer != null) {
refreshTimer.stop();
curRefreshChunk = null;
previousRefreshChunk = null;
nextRefreshChunk = null;
repaint();
}
}
@Override
public void update(Graphics g) {
paint(g);
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g); //just so the default background color is painted
if(refreshTimer == null || curRefreshChunk == null || refreshTimer.isRunning() == false) {
//draw reference line
g.setColor(MyColors.waveformReferenceLineColor);
g.drawLine(0, getHeight()/2, getWidth() - 1, getHeight()/2);
//draw bottom border
g.setColor(MyColors.unfocusedColor);
g.drawLine(0, getHeight() - 1, getWidth() - 1, getHeight() - 1);
return;
}
chunkInProgress = false;
//draw buffered waveform image
int curChunkXPos = frameToComponentX(CurAudio.firstFrameOfChunk(curRefreshChunk.getNum()));
g.drawImage(curRefreshChunk.getImage(), curChunkXPos, 0, null);
if(previousRefreshChunk != null) {
g.drawImage(previousRefreshChunk.getImage(), curChunkXPos - curRefreshChunk.getImage().getWidth(null), 0, null);
}
else {
if(curRefreshChunk.getNum() != 0) {
chunkInProgress = true;
}
}
if(nextRefreshChunk != null) {
g.drawImage(nextRefreshChunk.getImage(), curChunkXPos + curRefreshChunk.getImage().getWidth(null), 0, null);
}
else {
if(curRefreshChunk.getNum() != CurAudio.lastChunkNum()) {
chunkInProgress = true;
}
}
Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHints(MyShapes.getRenderingHints());
//draw current time
g2d.drawString(secFormat.format(CurAudio.getMaster().framesToSec(refreshFrame)), 10, 20);
//draw annotations
Annotation[] anns = AnnotationDisplay.getAnnotationsInOrder();
for(int i = 0; i < anns.length; i++) {
double time = anns[i].getTime();
int xPos = frameToComponentX(CurAudio.getMaster().millisToFrames(time));
if(xPos < 0) {
continue;
}
if(xPos > refreshWidth) {
break;
}
String text = anns[i].getText();
g2d.setColor(MyColors.annotationLineColor);
g2d.drawLine(xPos, 0, xPos, getHeight() - 1);
g2d.setColor(MyColors.annotationTextColor);
g2d.drawString(text, xPos + 5, 40);
}
//find progress bar position
progressBarXPos = frameToComponentX(refreshFrame);
if(progressBarXPos < 0) {
System.err.println("bad val " + progressBarXPos + "/" + (getWidth() - 1));
}
else if(progressBarXPos > getWidth() - 1) {
if(refreshWidth == getWidth()) {
if(SysInfo.sys.interpolateFrames == false || Math.abs(refreshFrame - CurAudio.getMaster().durationInFrames()) > CurAudio.getMaster().secondsToFrames(SysInfo.sys.interplationToleratedErrorZoneInSec)) {
System.err.println("bad val " + progressBarXPos + "/" + (getWidth() - 1));
}
}
progressBarXPos = getWidth() - 1;
}
//accent selected annotation
boolean foundOverlap = false;
if(CurAudio.getPlayer().getStatus() != PrecisionPlayer.Status.PLAYING) {
for(int i = 0; i < anns.length; i++) {
int annX = WaveformDisplay.frameToDisplayXPixel(CurAudio.getMaster().millisToFrames(anns[i].getTime()));
if(progressBarXPos == annX) {
foundOverlap = true;
g2d.setPaintMode();
g2d.setColor(MyColors.annotationAccentColor);
g2d.drawLine(progressBarXPos, 0, progressBarXPos, refreshHeight - 1);
int[] xCoordinates = {progressBarXPos - 20, progressBarXPos - 1, progressBarXPos + 2, progressBarXPos + 20};
int[] yCoordinates = {0, 20, 20, 0};
g2d.fillPolygon(xCoordinates, yCoordinates, xCoordinates.length);
yCoordinates = new int[] {refreshHeight - 1, refreshHeight - 21, refreshHeight - 21, refreshHeight - 1};
g2d.fillPolygon(xCoordinates, yCoordinates, xCoordinates.length);
break;
}
}
}
//draw progress bar
if(foundOverlap == false) {
Stroke originalStroke = g2d.getStroke();
g2d.setStroke(MyShapes.getProgressBarStroke());
g2d.setXORMode(MyColors.waveformBackground);
g2d.setColor(MyColors.progressBarColor);
g2d.drawLine(progressBarXPos, 0, progressBarXPos, getHeight() - 1);
g2d.setPaintMode();
g2d.setStroke(originalStroke);
}
//draw bottom border
g2d.setColor(MyColors.unfocusedColor);
g2d.drawLine(0, getHeight() - 1, getWidth() - 1, getHeight() - 1);
}
private int frameToComponentX(long frame) {
int absoluteX = absoluteX(frame);
int absoluteCurX = absoluteX(refreshFrame);
int offset = refreshWidth/2 - absoluteCurX;
if(offset > 0) { //first half window of audio is adjusted
offset = 0;
}
else { //last half window of audio is adjusted
int absoluteLength = -1 * (int)Math.ceil(GUIConstants.zoomlessPixelsPerSecond * CurAudio.getMaster().durationInSeconds());
if((-absoluteLength) <= refreshWidth) {
offset = 0;
}
else {
offset = Math.max(offset, absoluteLength + refreshWidth);
}
}
return absoluteX + offset;
}
private int absoluteX(long frame) {
return (int) (GUIConstants.zoomlessPixelsPerSecond * CurAudio.getMaster().framesToSec(frame));
}
public static int frameToAbsoluteXPixel(long frame) {
if(CurAudio.audioOpen()) {
return instance.absoluteX(frame);
}
throw new IllegalStateException("audio not open");
}
public static int frameToDisplayXPixel(long frame) {
if(CurAudio.audioOpen()) {
return instance.frameToComponentX(frame);
}
throw new IllegalStateException("audio not open");
}
public static int displayXPixelToFrame(int xPix) {
if(CurAudio.audioOpen()) {
return (int) (instance.refreshFrame + (xPix - progressBarXPos) * ((1./GUIConstants.zoomlessPixelsPerSecond) * CurAudio.getMaster().frameRate()));
}
throw new IllegalStateException("audio not open");
}
public static int getProgressBarXPos() {
return progressBarXPos;
}
//one RefreshListener per file, guaranteed
protected final class RefreshListener implements ActionListener {
private final long maxFramesError;
private final long lastFrame;
private long bufferedFrame;
private int bufferedWidth;
private int bufferedHeight;
private int bufferedNumAnns;
private boolean wasPlaying;
private long lastTime;
protected RefreshListener() {
lastFrame = CurAudio.getMaster().durationInFrames() - 1;
maxFramesError = CurAudio.getMaster().secondsToFrames(1) / GUIConstants.zoomlessPixelsPerSecond * SysInfo.sys.maxInterpolatedPixels;
bufferedFrame = -1;
bufferedWidth = -1;
bufferedHeight = -1;
bufferedNumAnns = -1;
wasPlaying = false;
lastTime = 0;
}
public final void actionPerformed(ActionEvent evt) {
long realRefreshFrame = CurAudio.getAudioProgress();
refreshWidth = getWidth();
refreshHeight = getHeight();
int chunkNum = CurAudio.lookupChunkNum(realRefreshFrame);
int numAnns = AnnotationDisplay.getNumAnnotations();
boolean isPlaying = CurAudio.getPlayer().getStatus() == PrecisionPlayer.Status.PLAYING;
if(SysInfo.sys.interpolateFrames) {
long curTime;
if(SysInfo.sys.nanoInterplation) {
curTime = System.nanoTime();
}
else {
curTime = System.currentTimeMillis();
}
if(isPlaying && wasPlaying) {
long changeMillis = curTime - lastTime;
if(SysInfo.sys.nanoInterplation) {
refreshFrame += CurAudio.getMaster().nanosToFrames(changeMillis);
}
else {
refreshFrame += CurAudio.getMaster().millisToFrames(changeMillis);
}
if(refreshFrame > lastFrame) {
refreshFrame = lastFrame;
}
if(Math.abs(refreshFrame - realRefreshFrame) > maxFramesError) {
if(SysInfo.sys.interpolateFrames == false || Math.abs(refreshFrame - lastFrame) > CurAudio.getMaster().secondsToFrames(SysInfo.sys.interplationToleratedErrorZoneInSec)) {
System.err.println("interpolation error greater than " + SysInfo.sys.maxInterpolatedPixels + " pixels: " + Math.abs(refreshFrame - realRefreshFrame) + " (frames)");
refreshFrame = realRefreshFrame;
}
}
}
else {
refreshFrame = realRefreshFrame;
}
lastTime = curTime;
}
else {
refreshFrame = realRefreshFrame;
}
if(chunkInProgress == false && refreshFrame == bufferedFrame && bufferedWidth == refreshWidth && bufferedHeight == refreshHeight && bufferedNumAnns == numAnns) {
return;
}
WaveformChunk[] chunks = WaveformBuffer.getWaveformChunks();
if(chunks == null) { //occurs only while WaveformBuffer's constructor is being run
return;
}
if(chunks[chunkNum] == null) {
return;
}
curRefreshChunk = chunks[chunkNum];
if(chunkNum > 0) {
previousRefreshChunk = chunks[chunkNum - 1];
}
if(chunkNum < chunks.length - 1) {
nextRefreshChunk = chunks[chunkNum + 1];
}
wasPlaying = isPlaying;
bufferedFrame = realRefreshFrame;
bufferedWidth = refreshWidth;
bufferedHeight = curRefreshChunk.getImage().getHeight(null);
bufferedNumAnns = AnnotationDisplay.getNumAnnotations();
repaint();
}
};
}