org.rdv.viz.spectrum.SpectrumAnalyzerPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.rdv.viz.spectrum.SpectrumAnalyzerPanel.java

Source

/*
 * RDV
 * Real-time Data Viewer
 * http://rdv.googlecode.com/
 * 
 * Copyright (c) 2008 Palta Software
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * 
 * $URL$
 * $Revision$
 * $Date$
 * $Author$
 */

package org.rdv.viz.spectrum;

import java.util.Arrays;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import javax.swing.SwingUtilities;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.xy.XYSeriesCollection;
import org.rdv.viz.chart.ChartPanel;

import edu.emory.mathcs.jtransforms.fft.DoubleFFT_1D;

/**
 * A panel for displaying a spectrum analyzer.
 * 
 * @author Jason P. Hanley
 * 
 */
public class SpectrumAnalyzerPanel extends ChartPanel {

    /** serialization version identifier */
    private static final long serialVersionUID = -4965300280813579540L;

    /** the sample rate of the data */
    private double sampleRate;

    /** the number of samples to use as input */
    private int numberOfSamples;

    /** the window function to use on the data */
    private WindowFunction windowFunction;

    /** the size of the segments used to FFT */
    private int segmentSize;

    /** the overlap of the segments */
    private int overlap;

    /** the data */
    private double[] inputData;

    /** the offset to the data */
    private int oldestDataIndex;

    /** the window function coefficients */
    private double[] window;

    /** the FFT class */
    private DoubleFFT_1D fft;

    /** the FFT executor */
    private Executor fftExecutor;

    /** the chart */
    private JFreeChart chart;

    /** the XY series to hold the chart data */
    private SpectrumXYSeries xySeries;

    /** the sample rate property */
    public static final String SAMPLE_RATE_PROPERTY = "sampleRate";

    /** the number of samples property */
    public static final String NUMBER_OF_SAMPLES_PROPERTY = "numberOfSamples";

    /** the window function property */
    public static final String WINDOW_FUNCTION_PROPERTY = "windowFunction";

    /** the segment size property */
    public static final String SEGMENT_SIZE_PROPERTY = "segmentSize";

    /** the overlap property */
    public static final String OVERLAP_PROPERTY = "overlap";

    /**
     * Creates a spectrum analyzer panel.
     */
    public SpectrumAnalyzerPanel() {
        super(null, true);

        sampleRate = 256;
        numberOfSamples = 256;
        windowFunction = WindowFunction.Hamming;
        segmentSize = 256;
        overlap = 0;

        inputData = new double[numberOfSamples];
        oldestDataIndex = 0;

        createWindow();

        fft = new DoubleFFT_1D(segmentSize);

        fftExecutor = Executors.newSingleThreadExecutor();

        initChart();
    }

    /**
     * Initializes the chart.
     */
    private void initChart() {
        xySeries = new SpectrumXYSeries("", false, false);

        XYSeriesCollection xySeriesCollection = new XYSeriesCollection();
        xySeriesCollection.addSeries(xySeries);

        chart = ChartFactory.createXYLineChart(null, "Frequency (Hz)", null, xySeriesCollection,
                PlotOrientation.VERTICAL, false, true, false);
        chart.setAntiAlias(false);

        XYPlot xyPlot = (XYPlot) chart.getPlot();
        NumberAxis xAxis = (NumberAxis) xyPlot.getDomainAxis();
        xAxis.setRange(0, sampleRate / 2);

        setChart(chart);
    }

    /**
     * Gets the sample rate.
     * 
     * @return  the sample rate
     */
    public double getSampleRate() {
        return sampleRate;
    }

    /**
     * Sets the sample rate. The sample rate must be positive.
     * 
     * @param sampleRate  the new sample rate
     */
    public void setSampleRate(double sampleRate) {
        if (sampleRate <= 0) {
            throw new IllegalArgumentException("Sample rate must be positive.");
        }

        if (this.sampleRate == sampleRate) {
            return;
        }

        double oldSampleRate = this.sampleRate;
        this.sampleRate = sampleRate;

        firePropertyChange(SAMPLE_RATE_PROPERTY, oldSampleRate, sampleRate);

        // set the x-axis range
        XYPlot xyPlot = (XYPlot) chart.getPlot();
        NumberAxis xAxis = (NumberAxis) xyPlot.getDomainAxis();
        xAxis.setRange(0, sampleRate / 2);

        plotSpectrum();
    }

    /**
     * Gets the number of samples. This is amount of data that will be used in the
     * analysis.
     * 
     * @return  the number of samples
     */
    public int getNumberOfSamples() {
        return numberOfSamples;
    }

    /**
     * Sets the number of samples. The number of samples must be greater then 1.
     * If the segment size is greater then the number of samples it will be set to
     * the next lower (or equal) power of 2. The overlap also may be reduced
     * because of this.
     * 
     * @param numberOfSamples  the new number of samples
     */
    public void setNumberOfSamples(int numberOfSamples) {
        if (numberOfSamples <= 1) {
            throw new IllegalArgumentException("Number of samples must be greater then 1.");
        }

        if (this.numberOfSamples == numberOfSamples) {
            return;
        }

        double[] newInputData = new double[numberOfSamples];
        int offset = numberOfSamples < this.numberOfSamples ? this.numberOfSamples - numberOfSamples : 0;
        for (int i = 0; i < Math.min(this.numberOfSamples, numberOfSamples); i++) {
            int index = (oldestDataIndex + offset + i) % this.numberOfSamples;
            newInputData[i] = inputData[index];
        }
        inputData = newInputData;
        oldestDataIndex = Math.min(this.numberOfSamples, numberOfSamples) % numberOfSamples;

        int oldNumberOfSamples = this.numberOfSamples;
        this.numberOfSamples = numberOfSamples;

        firePropertyChange(NUMBER_OF_SAMPLES_PROPERTY, oldNumberOfSamples, numberOfSamples);

        // reduce the segment size if needed
        if (segmentSize > numberOfSamples) {
            setSegmentSize(numberOfSamples, false);
        }

        plotSpectrum();
    }

    /**
     * Gets the window function.
     * 
     * @return  the window function
     */
    public WindowFunction getWindowFunction() {
        return windowFunction;
    }

    /**
     * Sets the window function.
     * 
     * @param windowFunction  the new window function
     */
    public void setWindowFunction(WindowFunction windowFunction) {
        if (windowFunction == null) {
            windowFunction = WindowFunction.Rectangular;
        }

        if (this.windowFunction == windowFunction) {
            return;
        }

        WindowFunction oldWindowFunction = this.windowFunction;
        this.windowFunction = windowFunction;

        firePropertyChange(WINDOW_FUNCTION_PROPERTY, oldWindowFunction, windowFunction);

        createWindow();

        plotSpectrum();
    }

    /**
     * Gets the segment size. This is the size of the data that will be FFT'ed.
     * 
     * @return  the segment size
     */
    public int getSegmentSize() {
        return segmentSize;
    }

    /**
     * Sets the segment size. The segment size must at least 2, less then or equal
     * to the number of samples and a power of 2. If the overlap is greater then
     * or equal to the segment size it will be reduced to 1 less then the segment
     * size.
     * 
     * @param segmentSize  the new segment size
     */
    public void setSegmentSize(int segmentSize) {
        setSegmentSize(segmentSize, true);
    }

    /**
     * Sets the segment size and optionally redraws the chart. The segment size
     * must at least 2, less then or equal to the number of samples and a power of
     * 2. If the overlap is greater then or equal to the segment size it will be
     * reduced to 1 less then the segment size.
     * 
     * @param segmentSize  the new segment size
     * @param redraw       if true, redraw the chart, otherwise don't
     */
    private void setSegmentSize(int segmentSize, boolean redraw) {
        if (segmentSize <= 1 || segmentSize > numberOfSamples) {
            throw new IllegalArgumentException(
                    "Segment size must be greater then 1 and less then or equal to the number of samples.");
        }

        if (this.segmentSize == segmentSize) {
            return;
        }

        int oldSegmentSize = this.segmentSize;
        this.segmentSize = segmentSize;

        createWindow();

        fft = new DoubleFFT_1D(segmentSize);

        firePropertyChange(SEGMENT_SIZE_PROPERTY, oldSegmentSize, segmentSize);

        // reduce the overlap if needed
        if (overlap >= segmentSize) {
            setOverlap(segmentSize - 1, false);
        }

        if (redraw) {
            plotSpectrum();
        }
    }

    /**
     * Gets the overlap. The overlap is the number of points shared by each
     * successive segment.
     * 
     * @return
     */
    public int getOverlap() {
        return overlap;
    }

    /**
     * Sets the overlap. The overlap must be less then the segment size. If the
     * overlap is less then 0, it will be set to half the segment size.
     * 
     * @param overlap  the new overlap
     */
    public void setOverlap(int overlap) {
        setOverlap(overlap, true);
    }

    /**
     * Sets the overlap and optionally redraws the chart. The overlap must be less
     * then the segment size. If the overlap is less then 0, it will be set to
     * half the segment size.
     * 
     * @param overlap  the new overlap
     * @param redraw   if true, redraw the chart, otherwise don't
     */
    private void setOverlap(int overlap, boolean redraw) {
        if (overlap < 0) {
            overlap = segmentSize / 2;
        }

        if (overlap >= segmentSize) {
            throw new IllegalArgumentException("Overlap must be less then the segment size.");
        }

        if (this.overlap == overlap) {
            return;
        }

        int oldOverlap = this.overlap;
        this.overlap = overlap;

        firePropertyChange(OVERLAP_PROPERTY, oldOverlap, overlap);

        plotSpectrum();
    }

    /**
     * Adds data. This will not trigger a replot. Call
     * {@link #finishedAddingData()} when finished adding data.
     * 
     * @param data  the data point to add
     * @see         #finishedAddingData()
     */
    public void addData(double data) {
        inputData[oldestDataIndex++] = data;
        if (oldestDataIndex == numberOfSamples) {
            oldestDataIndex = 0;
        }
    }

    /**
     * Called when finished adding data and replots the chart.
     * 
     * @see  #addData(double)
     */
    public void finishedAddingData() {
        plotSpectrum();
    }

    /**
     * Adds data and replots the chart.
     * 
     * @param data        the new data
     * @param startIndex  the start index for the data
     * @param endIndex    the end index for the data
     */
    public void addData(float[] data, int startIndex, int endIndex) {
        // if there is too much data, only add the most recent
        if (endIndex - startIndex > numberOfSamples) {
            startIndex = endIndex - numberOfSamples;
        }

        // add the data overwritting the oldest data
        for (int i = startIndex; i <= endIndex; i++) {
            inputData[oldestDataIndex++] = data[i];
            if (oldestDataIndex == numberOfSamples) {
                oldestDataIndex = 0;
            }
        }

        plotSpectrum();
    }

    /**
     * Adds data and replots the chart.
     * 
     * @param data        the new data
     * @param startIndex  the start index for the data
     * @param endIndex    the end index for the data
     */
    public void addData(double[] data, int startIndex, int endIndex) {
        // if there is too much data, only add the most recent
        if (endIndex - startIndex > numberOfSamples) {
            startIndex = endIndex - numberOfSamples;
        }

        // add the data overwritting the oldest data
        for (int i = startIndex; i <= endIndex; i++) {
            inputData[oldestDataIndex++] = data[i];
            if (oldestDataIndex == numberOfSamples) {
                oldestDataIndex = 0;
            }
        }

        plotSpectrum();
    }

    /**
     * Clears the data.
     */
    public void clearData() {
        Arrays.fill(inputData, 0);
        oldestDataIndex = 0;

        xySeries.clear();
    }

    /**
     * Gets the data as an array with a 0 offset.
     * 
     * @return  the data
     */
    private double[] getData() {
        double[] array = new double[numberOfSamples];
        for (int i = 0; i < numberOfSamples; i++) {
            int index = (oldestDataIndex + i) % numberOfSamples;
            array[i] = inputData[index];
        }
        return array;
    }

    /**
     * Plots the power spectrum. This does the analysis in a seperate thread and
     * when done adds updates the chart on the EDT.
     */
    private void plotSpectrum() {
        final double[] x = getData();

        fftExecutor.execute(new Runnable() {
            public void run() {
                final double[] X = psdWelch(fft, x, numberOfSamples, window, segmentSize, overlap);

                try {
                    SwingUtilities.invokeAndWait(new Runnable() {
                        public void run() {
                            updateChart(X, sampleRate, segmentSize);
                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Calculate the PSD using Whelch's Method.
     * 
     * @param fft              the FFT class
     * @param x                the data
     * @param numberOfSamples  the number of samples to use
     * @param window           the window coefficients
     * @param segmentSize      the size of the segments
     * @param overlap          the overlap of the segments
     * @return                 the PSD
     */
    private static double[] psdWelch(DoubleFFT_1D fft, double x[], int numberOfSamples, double window[],
            int segmentSize, int overlap) {
        // the final PSD aray
        double X[] = new double[segmentSize / 2 + 1];

        // the data array's for the segments
        double xx[] = new double[segmentSize];
        double XX[] = new double[segmentSize / 2 + 1];

        // loop through each segment and calculate their PSD
        int segments = 0;
        int offset = 0;
        while (offset + segmentSize <= numberOfSamples) {
            // fill the data array
            for (int i = 0; i < segmentSize; i++) {
                xx[i] = x[offset + i] * window[i];
            }

            // compute the FFT
            fft.realForward(xx);

            // calculate the magnitude
            for (int i = 0; i <= segmentSize / 2; i++) {
                // get the data out according to the FFT's data format
                double re = (i == segmentSize / 2) ? xx[1] : xx[2 * i];
                double im = (i == 0 || i == segmentSize / 2) ? 0 : xx[2 * i + 1];
                double mag = Math.sqrt(Math.pow(re, 2) + Math.pow(im, 2));

                XX[i] = mag / segmentSize;
                XX[i] = Math.pow(XX[i], 2);
                if (i != 0 || i != segmentSize)
                    XX[i] = 2 * XX[i];

                // add to the final PSD
                X[i] += XX[i];
            }

            segments++;
            offset += segmentSize - overlap;
        }

        // compute the average of the segments PSD's
        for (int i = 0; i <= segmentSize / 2; i++) {
            X[i] = X[i] / segments;
        }

        return X;
    }

    /**
     * Update the chart with new data.
     * 
     * @param X            the new PSD data
     * @param sampleRate   the sample rate
     * @param segmentSize  the segment size
     */
    private void updateChart(double[] X, double sampleRate, int segmentSize) {
        xySeries.clear(false);

        double period = sampleRate / segmentSize;
        double frequency = 0;
        for (int i = 0; i < X.length; i++) {
            xySeries.add(frequency, X[i], false);
            frequency += period;
        }

        xySeries.fireSeriesChanged();
    }

    /**
     * Create the window by calculating the window coefficients.
     */
    private void createWindow() {
        window = new double[segmentSize];

        switch (windowFunction) {
        case Bartlett:
            bartlett();
            break;
        case Blackman:
            blackman();
            break;
        case Hamming:
            hamming();
            break;
        case Hann:
            hann();
            break;
        case Rectangular:
            rectangular();
            break;
        }
    }

    /**
     * Create a Bartlett window.
     */
    private void bartlett() {
        int N = window.length;
        window[0] = 0;
        for (int n = 1; n < N - 1; n++) {
            if (n < N / 2) {
                window[n] = 2 * n / (N - 1);
            } else {
                window[n] = 2 - (2 * n / (N - 1));
            }
        }
        window[N - 1] = 0;
    }

    /**
     * Create a Blackman window.
     */
    private void blackman() {
        int N = window.length;
        double pi = Math.PI;
        for (int n = 0; n < N; n++) {
            window[n] = 0.42 - (0.5 * Math.cos(2 * pi * n / (N - 1))) + (0.08 * Math.cos(4 * pi * n / (N - 1)));
        }
    }

    /**
     * Create a Hamming window.
     */
    private void hamming() {
        int N = window.length;
        double pi = Math.PI;
        for (int n = 0; n < window.length; n++) {
            window[n] = 0.54 - (0.46 * Math.cos(2 * pi * n / (N - 1)));
        }
    }

    /**
     * Create a Hann window.
     */
    private void hann() {
        int N = window.length;
        double pi = Math.PI;
        for (int n = 0; n < window.length; n++) {
            window[n] = 0.5 - (0.5 * Math.cos(2 * pi * n / (N - 1)));
        }
    }

    /**
     * Create a rectangular window. This is the same as no window.
     */
    private void rectangular() {
        for (int n = 0; n < window.length; n++) {
            window[n] = 1;
        }
    }

}