/******************************************************************************* * sdrtrunk * Copyright (C) 2014-2017 Dennis Sheirer * * 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 spectrum; import dsp.filter.smoothing.GaussianSmoothingFilter; import dsp.filter.smoothing.NoSmoothingFilter; import dsp.filter.smoothing.RectangularSmoothingFilter; import dsp.filter.smoothing.SmoothingFilter; import dsp.filter.smoothing.SmoothingFilter.SmoothingType; import dsp.filter.smoothing.TriangularSmoothingFilter; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import settings.ColorSetting; import settings.ColorSetting.ColorSettingName; import settings.Setting; import settings.SettingChangeListener; import settings.SettingsManager; import javax.swing.*; import java.awt.*; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.util.Arrays; public class SpectrumPanel extends JPanel implements DFTResultsListener, SettingChangeListener, SpectralDisplayAdjuster { private static final long serialVersionUID = 1L; private final static Logger mLog = LoggerFactory.getLogger(SpectrumPanel.class); private static final RenderingHints RENDERING_HINTS = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); static { RENDERING_HINTS.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); } private Color mColorSpectrumBackground; private Color mColorSpectrumGradientTop; private Color mColorSpectrumGradientBottom; private Color mColorSpectrumLine; //Defines the panel inset along the bottom for frequency display private float mSpectrumInset = 20.0f; //Current DFT output bins in dB private float[] mDisplayFFTBins = new float[1]; //Averaging across multiple DFT result sets private int mAveraging = 4; //Smoothing across bins in the same DFT result set private SmoothingFilter mSmoothingFilter = new GaussianSmoothingFilter(); //Reference dB value set according to the source sample size private float mDBScale; private int mZoom = 0; private int mZoomWindowOffset = 0; ; private SettingsManager mSettingsManager; public SpectrumPanel(SettingsManager settingsManager) { mSettingsManager = settingsManager; if(mSettingsManager != null) { mSettingsManager.addListener(this); } setSampleSize(16.0); mSmoothingFilter.setPointSize(SmoothingFilter.SMOOTHING_DEFAULT); getColors(); setAveraging(mAveraging); } public void dispose() { if(mSettingsManager != null) { mSettingsManager.removeListener(this); } mSettingsManager = null; } /** * DFTResultsListener interface for receiving the processed data * to display */ public void receive(float[] currentFFTBins) { //Prevent arrays of NaN values from being rendered. The first few //DFT result sets on startup will contain NaN values if(Float.isInfinite(currentFFTBins[0]) || Float.isNaN(currentFFTBins[0])) { currentFFTBins = new float[currentFFTBins.length]; } //Construct and/or resize our DFT results variables if(mDisplayFFTBins == null || mDisplayFFTBins.length != currentFFTBins.length) { mDisplayFFTBins = currentFFTBins; } //Apply smoothing across the bins of the DFT results float[] smoothedBins = mSmoothingFilter.filter(currentFFTBins); //Apply averaging over multiple DFT output frames if(mAveraging > 1) { float gain = 1.0f / (float)mAveraging; for(int x = 0; x < mDisplayFFTBins.length; x++) { mDisplayFFTBins[x] += (smoothedBins[x] - mDisplayFFTBins[x]) * gain; } } else { mDisplayFFTBins = smoothedBins; } repaint(); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D graphics = (Graphics2D)g; graphics.setBackground(mColorSpectrumBackground); graphics.setRenderingHints(RENDERING_HINTS); drawSpectrum(graphics); } /** * Draws the current fft spectrum with a line and a gradient fill. */ private void drawSpectrum(Graphics2D graphics) { Dimension size = getSize(); //Draw the background Rectangle background = new Rectangle(0, 0, size.width, size.height); graphics.setColor(mColorSpectrumBackground); graphics.draw(background); graphics.fill(background); //Define the gradient GradientPaint gradient = new GradientPaint(0, (getSize().height - mSpectrumInset) / 2, mColorSpectrumGradientTop, 0, getSize().height, mColorSpectrumGradientBottom); graphics.setBackground(mColorSpectrumBackground); GeneralPath spectrumShape = new GeneralPath(); //Start at the lower right inset point spectrumShape.moveTo(size.getWidth(), size.getHeight() - mSpectrumInset); //Draw to the lower left spectrumShape.lineTo(0, size.getHeight() - mSpectrumInset); float[] bins = getBins(); //If we have FFT data to display ... if(bins != null) { float insideHeight = size.height - mSpectrumInset; float scalor = insideHeight / -mDBScale; /* Calculate based on bin size - 1, since bin 0 is rendered at zero * and the last bin is rendered at the width */ float binSize = (float)size.width / ((float)(bins.length)); for(int x = 0; x < bins.length; x++) { float height; height = bins[x] * scalor; if(height > insideHeight) { height = insideHeight; } if(height < 0) { height = 0; } float xAxis = (float)x * binSize; spectrumShape.lineTo(xAxis, height); } } //Otherwise show an empty spectrum else { //Draw Left Size graphics.setPaint(gradient); spectrumShape.lineTo(0, size.getHeight() - mSpectrumInset); //Draw Middle spectrumShape.lineTo(size.getWidth(), size.getHeight() - mSpectrumInset); } //Draw Right Side spectrumShape.lineTo(size.getWidth(), size.getHeight() - mSpectrumInset); graphics.setPaint(gradient); graphics.draw(spectrumShape); graphics.fill(spectrumShape); graphics.setPaint(mColorSpectrumLine); //Draw the bottom line under the spectrum graphics.draw(new Line2D.Float(0, size.height - mSpectrumInset, size.width, size.height - mSpectrumInset)); } /** * Sets the current zoom level * * 0 No Zoom * 1 2x Zoom * 2 4x Zoom * 3 8x Zoom * 4 16x Zoom * 5 32x Zoom * 6 64x Zoom * * @param zoom level, 0 - 6. */ public void setZoom(int zoom) { Validate.isTrue(0 <= zoom && zoom <= 6, "Unrecognized Zoom Level: " + zoom); mZoom = zoom; } /** * Sets the offset to define which subset of zoomed bins to display * * @param offset */ public void setZoomWindowOffset(int offset) { mZoomWindowOffset = offset; } /** * Sets the source sample size in bits which defines the lowest dB value to * display in the spectrum * * @param sampleSize in bits */ public void setSampleSize(double sampleSize) { Validate.isTrue(2.0 <= sampleSize && sampleSize <= 32.0); mDBScale = (float)(20.0 * Math.log10(Math.pow(2.0, sampleSize - 1))); } /** * Defines the number of DFT result sets to average across */ public void setAveraging(int size) { mAveraging = size; } /** * DFT result sets averaging value */ public int getAveraging() { return mAveraging; } /** * Clears the spectral display */ public void clearSpectrum() { mDisplayFFTBins = null; repaint(); } private void getColors() { mColorSpectrumBackground = getColor(ColorSettingName.SPECTRUM_BACKGROUND); mColorSpectrumGradientBottom = getColor(ColorSettingName.SPECTRUM_GRADIENT_BOTTOM); mColorSpectrumGradientTop = getColor(ColorSettingName.SPECTRUM_GRADIENT_TOP); mColorSpectrumLine = getColor(ColorSettingName.SPECTRUM_LINE); } /** * Retrieves the current setting for the named color setting */ private Color getColor(ColorSettingName name) { ColorSetting setting = mSettingsManager.getColorSetting(name); return setting.getColor(); } /** * Listener interface to receive setting change notifications */ @Override public void settingChanged(Setting setting) { if(setting instanceof ColorSetting) { ColorSetting colorSetting = (ColorSetting)setting; switch(((ColorSetting)setting).getColorSettingName()) { case SPECTRUM_BACKGROUND: mColorSpectrumBackground = colorSetting.getColor(); break; case SPECTRUM_GRADIENT_BOTTOM: mColorSpectrumGradientBottom = colorSetting.getColor(); break; case SPECTRUM_GRADIENT_TOP: mColorSpectrumGradientTop = colorSetting.getColor(); break; case SPECTRUM_LINE: mColorSpectrumLine = colorSetting.getColor(); break; default: break; } } } private int getZoomMultiplier() { return (int)Math.pow(2.0, mZoom); } /** * Returns the DFT result bins, or a zoomed and offset version of the bins * when the display is zoomed. */ private float[] getBins() { if(mZoom == 0) { return mDisplayFFTBins; } else { int length = mDisplayFFTBins.length / getZoomMultiplier(); int offset = mZoomWindowOffset; if((offset + length) >= mDisplayFFTBins.length) { offset = mDisplayFFTBins.length - length; } if(offset < 0) { offset = 0; } return Arrays.copyOfRange(mDisplayFFTBins, offset, offset + length); } } @Override public void settingDeleted(Setting setting) { /* not implemented */ } /** * Returns the current inter-bin smoothing setting */ @Override public int getSmoothing() { return mSmoothingFilter.getPointSize(); } /** * Sets the bin smoothing value to define how wide the smoothing window is * when averaging across DFT bins */ @Override public void setSmoothing(int smoothing) { mSmoothingFilter.setPointSize(smoothing); } /** * Returns the current smoothing filter type */ @Override public SmoothingType getSmoothingType() { return mSmoothingFilter.getSmoothingType(); } /** * Sets the smoothing filter type */ @Override public void setSmoothingType(SmoothingType type) { if(mSmoothingFilter.getSmoothingType() != type) { int pointSize = getSmoothing(); synchronized(mSmoothingFilter) { switch(type) { case GAUSSIAN: mSmoothingFilter = new GaussianSmoothingFilter(); break; case RECTANGLE: mSmoothingFilter = new RectangularSmoothingFilter(); break; case TRIANGLE: mSmoothingFilter = new TriangularSmoothingFilter(); break; case NONE: default: mSmoothingFilter = new NoSmoothingFilter(); break; } } setSmoothing(pointSize); } } }