/******************************************************************************* * SDR Trunk * Copyright (C) 2014,2015 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 ******************************************************************************/ package spectrum; import java.awt.Color; import java.awt.Dimension; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.util.Arrays; import javax.swing.JPanel; 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 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; 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 * * @param zoom level, 0 - 5. */ public void setZoom( int zoom ) { assert( 0 <= zoom && zoom <= 5 ); 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 ) { assert( 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 ); } } }