package apes.views; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridLayout; import java.nio.ByteBuffer; import java.util.HashSet; import java.util.Set; import javax.swing.JPanel; import apes.controllers.ChannelController; import apes.lib.SampleHelper; import apes.models.Config; import apes.models.InternalFormat; import apes.models.Player; /** * View for all channel graphs. * * @author Simon Holm * @author Johan Andersson (johandy@student.chalmers.se) */ public class ChannelView extends JPanel implements Runnable { /** * Contains a list of all graphs in this channel. */ private Set<ChannelView.Graph> graphs; /** * The player. */ private Player player; /** * The internal format */ private InternalFormat internalFormat; /** * The center sample of the channels */ private int centerSample; /** * The number of visible samples in each channel */ private int visibleSamples; /** * The position of the mouse in the x-axis */ private int mousePosX; /** * The channel controller. */ private ChannelController channelController; /** * A graphs width. */ private int graphWidth; /** * A graphs height. */ private int graphHeight; /** * The layout to use. */ private GridLayout layout; /** * The sample rate. */ private int sampleRate; /** * List of possible time units. */ private static enum Unit { SAMPLES, MILLISECONDS, SECONDS, MINUTES }; /** * Some graph colors. */ private Color colorRuler, colorPlay, colorSelection, colorLine, colorGraph, colorBackground, colorDots, colorStatus; /** * Creates a new <code>ChannelView</code> instance. * * @param channelController The channel controller. * @param player The player. */ public ChannelView(InternalFormat internalFormat, ChannelController channelController, Player player) { // Set layout. layout = new GridLayout(0, 1); layout.setVgap(10); setLayout(layout); // Set some instance variables. this.internalFormat = internalFormat; this.sampleRate = internalFormat.getSampleRate(); this.channelController = channelController; this.player = player; graphs = new HashSet<ChannelView.Graph>(); // Set start zoom. int numSamples = internalFormat.getSampleAmount(); setZoom(numSamples); setCenter(numSamples / 2); // Set configuration options. Config config = Config.getInstance(); graphWidth = config.getIntOption("graph_width"); graphHeight = config.getIntOption("graph_height"); colorRuler = Color.decode(config.getOption("color_ruler")); colorPlay = Color.decode(config.getOption("color_play")); colorSelection = Color.decode(config.getOption("color_selection")); colorLine = Color.decode(config.getOption("color_lines")); colorGraph = Color.decode(config.getOption("color_graph")); colorBackground = Color.decode(config.getOption("color_background")); colorDots = Color.decode(config.getOption("color_dots")); colorStatus = Color.decode(config.getOption("color_status")); new Thread(this).start(); } /** * Adds a channel graph to this view. * * @param channel The channel. */ public void addChannel(int channel) { // Add one more row to the layout. layout.setRows(layout.getRows() + 1); // Create a new graph. ChannelView.Graph graph = this.new Graph(this, channel); // Add to set of graphs. graphs.add(graph); // Add to this panel. add(graph); } /** * Updates the view. */ public void updateInternalFormat() { for(ChannelView.Graph graph : graphs) { graph.updateGraph(); } } /** * Returns the graph width. * * @return The graph width. */ public int getGraphWidth() { return graphWidth; } /** * Returns the graph height. * * @return The graph height. */ public int getGraphHeight() { return graphHeight; } /** * Run in a thread. */ public void run() { while(true) { this.repaint(); try { Thread.sleep(17); // 60 fps } catch(InterruptedException e) { e.printStackTrace(); } } } /** * Transform a position in a channel to pixels in the graph based on samples. * * @param samples The position in the channel in samples. * @return -1 if the sample is outside the graph otherwise where in the graph. */ public int samplesToPixels(int samples) { int firstVisibleSample = getFirstVisibleSample(); int lastVisibelSample = getLastVisibleSample(); if(samples < firstVisibleSample || samples > lastVisibelSample) return -1; int properSamples = samples - firstVisibleSample; float ratio = (float)properSamples / visibleSamples; return Math.round(ratio * graphWidth); } /** * Transform a position in a channel to pixels in the graph based on * milliseconds. * * @param milliseconds The position in the channel in milliseconds. * @return -1 if the time is outside the graph otherwise where in the graph. */ public int millisecondsToPixels(int milliseconds) { int samples = SampleHelper.millisecondsToSamples(sampleRate, milliseconds); return samplesToPixels(samples); } /** * Transform a position in a channel to pixels in the graph based on seconds. * * @param seconds The position in the channel in seconds. * @return -1 if the time is outside the graph otherwise where in the graph. */ public int secondsToPixels(int seconds) { int samples = SampleHelper.secondsToSamples(sampleRate, seconds); return samplesToPixels(samples); } /** * Transform a position in a channel to pixels in the graph based on minutes. * * @param minutes The position in the channel in seconds. * @return -1 if the time is outside the graph otherwise where in the graph. */ public int minutesToPixels(int minutes) { int samples = SampleHelper.millisecondsToSamples(sampleRate, minutes); return samplesToPixels(samples); } /** * Transform a number of pixels to samples in the channel. * * @param pixels How many pixels in the graph in the x-axis * @return The absolute samples in the channel, -1 if outside the graph. */ public int pixelsToSamples(int pixels) { if(pixels < 0 || pixels > graphWidth) { return -1; } int firstVisibleSample = getFirstVisibleSample(); float samplesPerPixel = (float)visibleSamples / graphWidth; int samples = Math.round((pixels * samplesPerPixel) + firstVisibleSample); return samples; } /** * Transform a number of pixels to milliseconds in the channel. Observ that * the numbers are rounded down. * * @param pixels How many pixels in the graph in the x-axis * @return The millisecnods in the channel, -1 if outside the graph. */ public int pixelsToMilliseconds(int pixels) { int samples = pixelsToSamples(pixels); return SampleHelper.samplesToMilliseconds(sampleRate, samples); } /** * Transform a number of pixels to milliseconds in the channel. Observ that * the numbers are rounded down. * * @param pixels How many pixels in the graph in the x-axis * @return The seconds in the channel, -1 if outside the graph. */ public int pixelsToSeconds(int pixels) { int samples = pixelsToSamples(pixels); return SampleHelper.samplesToSeconds(sampleRate, samples); } /** * Transform a number of pixels to milliseconds in the channel. Observ that * the numbers are rounded down. * * @param pixels How many pixels in the graph in the x-axis * @return The minutes in the channel, -1 if outside the graph. */ public int pixelsToMinutes(int pixels) { int samples = pixelsToSamples(pixels); return SampleHelper.samplesToMinutes(sampleRate, samples); } /** * Set the x position of the mouse. * * @param mousePosX The x position. */ public void setMousePosX(int mousePosX) { this.mousePosX = mousePosX; } /** * Is called when the player is updated. Then each graph should be updated. */ public void updatePlayer() { for(ChannelView.Graph graph : graphs) { graph.repaint(); } } /** * Returns the level of zoom. * * @return Number of samples visible. */ public int getZoom() { return visibleSamples; } /** * Sets the level of zoom. * * @param samples The number of samples to be viewed in the view. */ public void setZoom(int samples) { visibleSamples = samples; } /** * Sets the center of the view. * * @param sample The sample that should be in the center of the view. */ public void setCenter(int sample) { centerSample = sample; } /** * Returns the center position. * * @return The center position. */ public int getCenter() { return centerSample; } /** * Returns the first visible sample. * * @return The first visible sample. */ public int getFirstVisibleSample() { return centerSample - (visibleSamples / 2); } /** * Returns the last visible sample. * * @return The last visible sample. */ public int getLastVisibleSample() { return centerSample + (visibleSamples / 2); } /** * This is a graph over a channel. * * @author Simon Holm * @author Johan Andersson (johandy@student.chalmers.se) */ public class Graph extends JPanel { /** * The channel. */ private int channel; /** * Contains all sample amplitudes that should be drawn in the graph. */ private int[] samples; /** * How many samples are there per pixel. */ private float samplesPerPixel; /** * The view that this graph is placed on. */ private ChannelView channelView; /** * A graphics object. */ private Graphics2D g2; /** * The current time unit. */ private Unit timeUnit; /** * Creates a new <code>ChannelGraph</code> instance. * * @param channelView The view that this graph is placed on. * @param channel The channel. */ public Graph(ChannelView channelView, int channel) { // Set graph size. Dimension size = new Dimension(graphWidth, graphHeight); setPreferredSize(size); setSize(size); // Set events for the graph. addMouseListener(channelController); addMouseMotionListener(channelController); addMouseWheelListener(channelController); this.channelView = channelView; this.channel = channel; updateGraph(); } /** * Returns the view that this graph is placed on. * * @return The view. */ public ChannelView getChannelView() { return channelView; } /** * Draws the view. * * @param g A graphics object. */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); g2 = (Graphics2D)g; drawBackground(); drawGraph(); drawLines(); // Do before markers. drawSelection(); drawMarkers(); drawPlayMarker(); drawRuler(); drawStatus(); } /** * Draws a status indication on the graph telling where the mouse is placed. * The time is in seconds or milliseconds if zoomed in. */ private void drawStatus() { int time = getTime(mousePosX); ; String unit = getUnit(); g2.setColor(colorStatus); g2.drawString("( " + time + " " + unit + " )", 3, graphHeight - 3); } /** * Draws a ruler with time marks. The time is in seconds or milliseconds if * zoomed in. */ private void drawRuler() { g2.setColor(colorRuler); int rulerWidth = 3; g2.fillRect(0, 0, graphWidth - 1, rulerWidth); for(int i = 0; i < graphWidth; i += graphWidth / 10) { g2.drawLine(i, 0, i, rulerWidth + 3); int time = getTime(i); g2.drawString("" + time, i, 20); } } /** * Draws the player marker. */ private void drawPlayMarker() { int player = getMarkPlayer(); if(player == 0) { player++; } g2.setColor(colorPlay); g2.drawLine(player, 0, player, graphHeight); } /** * Draws the selection. */ private void drawSelection() { int start = getMarkStart(); int stop = getMarkStop(); if(start >= 0 && stop > 0) { g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f)); g2.setColor(colorSelection); g2.fillRect(start, 0, stop - start, graphHeight); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); } } /** * Draws the selection markers (start and end of selection). */ private void drawMarkers() { int start = getMarkStart(); int stop = getMarkStop(); if(start != stop) { if(start >= 0) { g2.drawLine(start + 1, 0, start + 1, graphHeight); } if(stop > 0) { g2.drawLine(stop - 1, 0, stop - 1, graphHeight); } } } /** * Draws some lines. */ private void drawLines() { g2.setColor(colorLine); g2.drawLine(0, graphHeight / 2, graphWidth, graphHeight / 2); g2.drawLine(0, graphHeight, graphWidth, graphHeight); g2.drawLine(0, 0, graphWidth, 0); } /** * Draws the actual graph. */ private void drawGraph() { // If the player cursor is at the end of the screen. Then go to // the next page. int start = getFirstVisibleSample(); int stop = getLastVisibleSample(); int currentSample = player.getCurrentSample(); if(visibleSamples > 100000 && currentSample > stop) { setCenter(visibleSamples + start + visibleSamples / 2); updateGraph(); } // Repaint it. g2.setColor(colorGraph); int half = graphHeight / 2; if(samplesPerPixel < 1) { float jump = (float)graphWidth / samples.length; int pixel = 0; for(int i = 1; i < samples.length; i++) { int x1 = pixel; int x2 = Math.round(pixel + jump); int y1 = half + samples[i - 1]; int y2 = half + samples[i]; g2.drawLine(x1, y1, x2, y2); if(graphWidth / samples.length > 5) { g2.setColor(colorDots); g2.fillOval(x2 - 3, y2 - 3, 6, 6); g2.setColor(colorGraph); } pixel += Math.round(jump); } } else { for(int i = 0; i < graphWidth; i++) { int y1 = half + samples[i]; int y2 = half - samples[i]; g2.drawLine(i, y1, i, y2); } } } /** * Draws a background. */ private void drawBackground() { g2.setBackground(colorBackground); g2.clearRect(0, 0, graphWidth - 1, graphHeight - 1); } /** * Returns an appropriate time depending on the zoom. The * <code>timeUnit</code> variable is also set to the correct unit. * * @param pixels The pixel position. * @return The time. */ private int getTime(int pixels) { int time = -1; int diff = SampleHelper.samplesToMilliseconds(sampleRate, visibleSamples); // If diff is larger than five minutes. if(diff > 5 * 1000 * 60) { time = pixelsToMinutes(pixels); timeUnit = Unit.MINUTES; } // If diff is larger than one minute. else if(diff > 10 * 1000) { time = pixelsToSeconds(pixels); timeUnit = Unit.SECONDS; } // If diff is larger than one tenth of a second. else if(diff > 100) { time = pixelsToMilliseconds(pixels); timeUnit = Unit.MILLISECONDS; } else { time = pixelsToSamples(pixels); timeUnit = Unit.SAMPLES; } return time; } /** * Returns the current unit as a string. * * @return The unit. */ private String getUnit() { String[] units = { "smp", "ms", "s", "min" }; return units[timeUnit.ordinal()]; } /** * Recalculates the view and repaints it. */ public void updateGraph() { int amount = internalFormat.getSampleAmount(); if(visibleSamples > amount) { visibleSamples = amount; centerSample = visibleSamples / 2; } samplesPerPixel = visibleSamples / graphWidth; // If there are less samples per pixel than 1. Or if there are // equally many samples as there are pixels. if(samplesPerPixel <= 1) { int firstVisibleSample = getFirstVisibleSample(); ByteBuffer bytes = ByteBuffer.wrap(internalFormat.getSamples(firstVisibleSample, firstVisibleSample + visibleSamples)); samples = new int[visibleSamples]; for(int i = 0; i < samples.length; i++) { int index = (int)(internalFormat.samplesToBytes(i) + channel * internalFormat.bytesPerSample); switch (internalFormat.bytesPerSample) { case 2: samples[i] = bytes.getShort(index); break; case 4: samples[i] = bytes.getInt(index); break; default: System.err.println("BAD BYTES PER SAMPLE IN CHANNEL VIEW WHILE UPDATING GRAPH"); System.exit(1); } } } // If there are more samples per pixel than 1. else { samples = new int[graphWidth]; int jump = Math.round((float)visibleSamples / graphWidth); int firstVisibleSample = getFirstVisibleSample(); for(int i = 0; i < samples.length; i++) { int start = firstVisibleSample + (i * jump); int sample = internalFormat.getAverageAmplitude(channel, start, jump); samples[i] = sample; } } applyLowPassFilter(samples, 0.1f); // Set min and max amplitude. int maxAmp = Short.MIN_VALUE; int minAmp = Short.MAX_VALUE; for(int i = 0; i < samples.length; i++) { int amplitude = samples[i]; if(amplitude > maxAmp) { maxAmp = amplitude; } if(amplitude < minAmp) { minAmp = amplitude; } } // Fix sample amplitudes so that they are in correct scale. // double heightScale = (double)( (float)( graphHeight / 2 ) / ( // maxAmp - minAmp ) ); float scale = ((float)graphHeight / 2) / (Math.abs(minAmp) + Math.abs(maxAmp)); for(int i = 0; i < samples.length; i++) { samples[i] = Math.round((samples[i] * scale)); } repaint(); } /** * TODO: Comment Low pass filter * * @param samples an <code>int</code> value * @param alpha a <code>float</code> value * @return an <code>int[]</code> value */ private int[] applyLowPassFilter(int[] samples, float alpha) { float[] tmp = new float[samples.length]; tmp[0] = samples[0]; for(int i = 1; i < samples.length; ++i) tmp[i] = alpha * samples[i] + (1 - alpha) * tmp[i - 1]; for(int i = 0; i < samples.length; ++i) samples[i] = Math.round(tmp[i]); return samples; } /** * Returns the start mark position in pixels. * * @return The start mark position. */ private int getMarkStart() { return samplesToPixels(player.getStart()); } /** * Returns the stop mark position in pixels. * * @return The stop mark position. */ private int getMarkStop() { return samplesToPixels(player.getStop()); } /** * Returns the player mark position in pixels. * * @return The player mark position. */ private int getMarkPlayer() { return samplesToPixels(player.getCurrentSample()); } } }