From 7455077cfa2c8e19509b658144134f82c4288761 Mon Sep 17 00:00:00 2001 From: denny Date: Wed, 11 Nov 2015 20:07:37 -0500 Subject: [PATCH] Spectrum/Waterfall display zoom feature - work in progress (issue #33) Implements zoom in/out by mouse scroll wheel and by right-click context menu. Still have to add ability to drag spectrum left/right when zoomed in. --- src/gui/SDRTrunk.java | 1 + src/spectrum/ChannelSpectrumPanel.java | 6 +- src/spectrum/DFTProcessor.java | 36 +- src/spectrum/{FFTWidth.java => DFTSize.java} | 6 +- src/spectrum/IDFTWidthChangeProcessor.java | 7 + src/spectrum/OverlayPanel.java | 93 ++++- src/spectrum/SpectralDisplayPanel.java | 275 +++++++++------ src/spectrum/SpectrumPanel.java | 321 +++++++++++------- src/spectrum/WaterfallPanel.java | 65 +++- .../converter/ComplexDecibelConverter.java | 5 +- .../converter/RealDecibelConverter.java | 4 +- .../{FFTWidthItem.java => DFTSizeItem.java} | 20 +- 12 files changed, 559 insertions(+), 280 deletions(-) rename src/spectrum/{FFTWidth.java => DFTSize.java} (94%) create mode 100644 src/spectrum/IDFTWidthChangeProcessor.java rename src/spectrum/menu/{FFTWidthItem.java => DFTSizeItem.java} (52%) diff --git a/src/gui/SDRTrunk.java b/src/gui/SDRTrunk.java index d4a55a49..afd740db 100644 --- a/src/gui/SDRTrunk.java +++ b/src/gui/SDRTrunk.java @@ -38,6 +38,7 @@ import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; +import javax.swing.JScrollPane; import javax.swing.JSeparator; import net.miginfocom.swing.MigLayout; diff --git a/src/spectrum/ChannelSpectrumPanel.java b/src/spectrum/ChannelSpectrumPanel.java index c78ddad1..5becccf8 100644 --- a/src/spectrum/ChannelSpectrumPanel.java +++ b/src/spectrum/ChannelSpectrumPanel.java @@ -35,7 +35,7 @@ import source.tuner.frequency.FrequencyChangeEvent.Attribute; import spectrum.converter.DFTResultsConverter; import spectrum.converter.RealDecibelConverter; import spectrum.menu.AveragingItem; -import spectrum.menu.FFTWidthItem; +import spectrum.menu.DFTSizeItem; import spectrum.menu.FFTWindowTypeItem; import spectrum.menu.FrameRateItem; import spectrum.menu.SmoothingItem; @@ -377,9 +377,9 @@ public class ChannelSpectrumPanel extends JPanel JMenu fftWidthMenu = new JMenu( "FFT Width" ); displayMenu.add( fftWidthMenu ); - for( FFTWidth width: FFTWidth.values() ) + for( DFTSize width: DFTSize.values() ) { - fftWidthMenu.add( new FFTWidthItem( mDFTProcessor, width ) ); + fftWidthMenu.add( new DFTSizeItem( mDFTProcessor, width ) ); } /** diff --git a/src/spectrum/DFTProcessor.java b/src/spectrum/DFTProcessor.java index 063df756..4a47d240 100644 --- a/src/spectrum/DFTProcessor.java +++ b/src/spectrum/DFTProcessor.java @@ -45,7 +45,9 @@ import dsp.filter.Window.WindowType; * Processes both complex samples or float samples and dispatches a float array * of DFT results, using configurable fft size and output dispatch timelines. */ -public class DFTProcessor implements Listener, FrequencyChangeListener +public class DFTProcessor implements Listener, + FrequencyChangeListener, + IDFTWidthChangeProcessor { private final static Logger mLog = LoggerFactory.getLogger( DFTProcessor.class ); @@ -59,8 +61,8 @@ public class DFTProcessor implements Listener, FrequencyChangeLis private ScheduledExecutorService mScheduler = Executors .newScheduledThreadPool( 1, new NamingThreadFactory( "spectrum dft" ) ); - private FFTWidth mFFTWidth = FFTWidth.FFT04096; - private FFTWidth mNewFFTWidth = FFTWidth.FFT04096; + private DFTSize mDFTSize = DFTSize.FFT04096; + private DFTSize mNewDFTSize = DFTSize.FFT04096; private double[] mWindow; @@ -68,7 +70,7 @@ public class DFTProcessor implements Listener, FrequencyChangeLis * with minimal bin leakage/smearing */ private WindowType mWindowType = Window.WindowType.HANNING; - private FloatFFT_1D mFFT = new FloatFFT_1D( mFFTWidth.getWidth() ); + private FloatFFT_1D mFFT = new FloatFFT_1D( mDFTSize.getSize() ); private int mFrameRate; private int mSampleRate; @@ -112,12 +114,12 @@ public class DFTProcessor implements Listener, FrequencyChangeLis if( mSampleType == SampleType.COMPLEX ) { mWindow = Window.getWindow( mWindowType, - mFFTWidth.getWidth() * 2 ); + mDFTSize.getSize() * 2 ); } else { mWindow = Window.getWindow( mWindowType, - mFFTWidth.getWidth() ); + mDFTSize.getSize() ); } } @@ -140,14 +142,14 @@ public class DFTProcessor implements Listener, FrequencyChangeLis * Queues an FFT size change request. The scheduled executor will apply * the change when it runs. */ - public void setFFTSize( FFTWidth width ) + public void setDFTSize( DFTSize size ) { - mNewFFTWidth = width; + mNewDFTSize = size; } - public FFTWidth getFFTWidth() + public DFTSize getDFTSize() { - return mFFTWidth; + return mDFTSize; } public int getFrameRate() @@ -444,9 +446,9 @@ public class DFTProcessor implements Listener, FrequencyChangeLis */ private void checkFFTSize() { - if( mNewFFTWidth.getWidth() != mFFTWidth.getWidth() ) + if( mNewDFTSize.getSize() != mDFTSize.getSize() ) { - mFFTWidth = mNewFFTWidth; + mDFTSize = mNewDFTSize; calculateConsumptionRate(); @@ -454,14 +456,14 @@ public class DFTProcessor implements Listener, FrequencyChangeLis if( mSampleType == SampleType.COMPLEX ) { - mPreviousFrame = new float[ mFFTWidth.getWidth() * 2 ]; + mPreviousFrame = new float[ mDFTSize.getSize() * 2 ]; } else { - mPreviousFrame = new float[ mFFTWidth.getWidth() ]; + mPreviousFrame = new float[ mDFTSize.getSize() ]; } - mFFT = new FloatFFT_1D( mFFTWidth.getWidth() ); + mFFT = new FloatFFT_1D( mDFTSize.getSize() ); } } @@ -497,7 +499,7 @@ public class DFTProcessor implements Listener, FrequencyChangeLis ( mSampleType == SampleType.COMPLEX ? 2.0f : 1.0f ); mFFTFloatsPerFrame = ( mSampleType == SampleType.COMPLEX ? - mFFTWidth.getWidth() * 2 : - mFFTWidth.getWidth() ); + mDFTSize.getSize() * 2 : + mDFTSize.getSize() ); } } diff --git a/src/spectrum/FFTWidth.java b/src/spectrum/DFTSize.java similarity index 94% rename from src/spectrum/FFTWidth.java rename to src/spectrum/DFTSize.java index 17e52cdd..eabfa977 100644 --- a/src/spectrum/FFTWidth.java +++ b/src/spectrum/DFTSize.java @@ -17,7 +17,7 @@ ******************************************************************************/ package spectrum; -public enum FFTWidth +public enum DFTSize { FFT00512( 512 ), FFT01024( 1024 ), @@ -29,12 +29,12 @@ public enum FFTWidth private int mWidth; - private FFTWidth( int width ) + private DFTSize( int width ) { mWidth = width; } - public int getWidth() + public int getSize() { return mWidth; } diff --git a/src/spectrum/IDFTWidthChangeProcessor.java b/src/spectrum/IDFTWidthChangeProcessor.java new file mode 100644 index 00000000..29795333 --- /dev/null +++ b/src/spectrum/IDFTWidthChangeProcessor.java @@ -0,0 +1,7 @@ +package spectrum; + +public interface IDFTWidthChangeProcessor +{ + public void setDFTSize( DFTSize size ); + public DFTSize getDFTSize(); +} diff --git a/src/spectrum/OverlayPanel.java b/src/spectrum/OverlayPanel.java index 0d91979b..1469043f 100644 --- a/src/spectrum/OverlayPanel.java +++ b/src/spectrum/OverlayPanel.java @@ -67,6 +67,10 @@ public class OverlayPanel extends JPanel private Point mCursorLocation = new Point( 0, 0 ); private boolean mCursorVisible = false; + private DFTSize mDFTSize = DFTSize.FFT04096; + private int mZoom = 0; + private int mDFTZoomWindowOffset = 0; + /** * Colors used by this component */ @@ -118,6 +122,11 @@ public class OverlayPanel extends JPanel mResourceManager = null; } + public void setDFTSize( DFTSize size ) + { + mDFTSize = size; + } + public ChannelDisplay getChannelDisplay() { return mChannelDisplay; @@ -142,6 +151,25 @@ public class OverlayPanel extends JPanel repaint(); } + /** + * Sets the current zoom level (2^zoom) + * + * 0 No Zoom + * 1 2x Zoom + * 2 4x Zoom + * 3 8x Zoom + * 4 16x Zoom + * 5 32x Zoom + * + * @param zoom level, 0 - 5. + * @param offset into the FFT bins for start zoom window + */ + public void setZoom( int zoom, int offset ) + { + mZoom = zoom; + mDFTZoomWindowOffset = offset; + } + /** * Fetches the color settings from the settings manager */ @@ -237,7 +265,7 @@ public class OverlayPanel extends JPanel drawChannels( graphics ); drawCursor( graphics ); } - + /** * Draws a cursor on the panel, whenever the mouse is hovering over the * panel @@ -256,6 +284,12 @@ public class OverlayPanel extends JPanel graphics.drawString( frequency , mCursorLocation.x + 5, mCursorLocation.y ); + + if( mZoom != 0 ) + { + graphics.drawString( "Zoom: " + (int)Math.pow( 2.0, mZoom ) + "x", + mCursorLocation.x + 17, mCursorLocation.y + 11 ); + } } } @@ -264,8 +298,8 @@ public class OverlayPanel extends JPanel */ private void drawFrequencies( Graphics2D graphics ) { - long minFrequency = getMinFrequency(); - long maxFrequency = getMaxFrequency(); + long minFrequency = getMinDisplayFrequency(); + long maxFrequency = getMaxDisplayFrequency(); int major = mLabelSizeMonitor.getMajorTickIncrement( graphics ); int minor = mLabelSizeMonitor.getMinorTickIncrement( graphics ); @@ -365,21 +399,18 @@ public class OverlayPanel extends JPanel /** * Returns the x-axis value corresponding to the frequency */ - public double getAxisFromFrequency( long frequency ) + private double getAxisFromFrequency( long frequency ) { - double canvasMiddle = getSize().getWidth() / 2.0d; - //Determine frequency offset from middle - double frequencyOffset = mFrequency - frequency; + double frequencyOffset = frequency - getMinDisplayFrequency(); //Determine ratio of offset to bandwidth - double ratio = frequencyOffset / (double)mBandwidth; + double ratio = frequencyOffset / (double)getDisplayBandwidth(); //Calculate offset against the total width - double xOffset = getSize().getWidth() * ratio; - - //Apply the offset against the canvas middle - return canvasMiddle - xOffset; + double xOffset = (double)getSize().getWidth() * ratio; + + return xOffset; } /** @@ -448,7 +479,7 @@ public class OverlayPanel extends JPanel double xAxis = getAxisFromFrequency( tunerChannel.getFrequency() ); double width = (double)( tunerChannel.getBandwidth() ) / - (double)mBandwidth * getSize().getWidth(); + (double)getDisplayBandwidth() * getSize().getWidth(); Rectangle2D.Double box = new Rectangle2D.Double( xAxis - ( width / 2.0d ), @@ -646,11 +677,16 @@ public class OverlayPanel extends JPanel repaint(); } + + public int getBandwidth() + { + return mBandwidth; + } /** * Currently displayed minimum frequency */ - private long getMinFrequency() + public long getMinFrequency() { return mFrequency - ( mBandwidth / 2 ); } @@ -658,11 +694,38 @@ public class OverlayPanel extends JPanel /** * Currently displayed maximum frequency */ - private long getMaxFrequency() + public long getMaxFrequency() { return mFrequency + ( mBandwidth / 2 ); } + + public boolean containsFrequency( long frequency ) + { + return Math.abs( mFrequency - frequency ) <= ( mBandwidth / 2 ); + } + + private long getMinDisplayFrequency() + { + double bandwidthPerBin = (double)mBandwidth / (double)( mDFTSize.getSize() - 1 ); + return getMinFrequency() + (int)( mDFTZoomWindowOffset * bandwidthPerBin ); + } + + private long getMaxDisplayFrequency() + { + return getMinDisplayFrequency() + getDisplayBandwidth(); + } + + private int getDisplayBandwidth() + { + if( mZoom != 0 ) + { + return mBandwidth / (int)Math.pow( 2.0, mZoom ); + } + + return mBandwidth; + } + /** * Returns a list of channel configs that contain the frequency within their * min/max frequency settings. diff --git a/src/spectrum/SpectralDisplayPanel.java b/src/spectrum/SpectralDisplayPanel.java index f78ed1e6..5ba4ffc9 100644 --- a/src/spectrum/SpectralDisplayPanel.java +++ b/src/spectrum/SpectralDisplayPanel.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * 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 @@ -17,7 +17,6 @@ ******************************************************************************/ package spectrum; -import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.EventQueue; @@ -28,18 +27,27 @@ import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; import java.text.DecimalFormat; import java.util.ArrayList; +import java.util.Hashtable; import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComponent; +import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; import javax.swing.JSeparator; +import javax.swing.JSlider; import javax.swing.JSplitPane; import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import module.decode.DecoderType; import net.miginfocom.swing.MigLayout; @@ -50,7 +58,6 @@ import org.slf4j.LoggerFactory; import sample.Listener; import sample.SampleType; import sample.complex.ComplexBuffer; -import settings.ColorSetting; import settings.ColorSetting.ColorSettingName; import settings.ColorSettingMenuItem; import settings.Setting; @@ -66,7 +73,7 @@ import spectrum.OverlayPanel.ChannelDisplay; import spectrum.converter.ComplexDecibelConverter; import spectrum.converter.DFTResultsConverter; import spectrum.menu.AveragingItem; -import spectrum.menu.FFTWidthItem; +import spectrum.menu.DFTSizeItem; import spectrum.menu.FFTWindowTypeItem; import spectrum.menu.FrameRateItem; import spectrum.menu.SmoothingItem; @@ -94,27 +101,25 @@ public class SpectralDisplayPanel extends JPanel implements ChannelEventListener, Listener, FrequencyChangeListener, + IDFTWidthChangeProcessor, SettingChangeListener, TunerSelectionListener { private static final long serialVersionUID = 1L; + private final static Logger mLog = LoggerFactory.getLogger( SpectralDisplayPanel.class ); + public static final int NO_ZOOM = 0; + public static final int MAX_ZOOM = 6; + private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" ); - /** - * Colors used by this component - */ - private Color mColorChannelConfig; - private Color mColorChannelConfigProcessing; - private Color mColorChannelConfigSelected; - private Color mColorSpectrumBackground; - private Color mColorSpectrumCursor; - private Color mColorSpectrumGradientBottom; - private Color mColorSpectrumGradientTop; - private Color mColorSpectrumLine; - + private DFTSize mDFTSize = DFTSize.FFT04096; + private int mZoom = 0; + private int mDFTZoomWindowOffset = 0; + + private JScrollPane mScrollPane; private JLayeredPane mLayeredPanel; private SpectrumPanel mSpectrumPanel; private WaterfallPanel mWaterfallPanel; @@ -128,8 +133,6 @@ public class SpectralDisplayPanel extends JPanel { mResourceManager = resourceManager; - setColors(); - init(); } @@ -159,56 +162,84 @@ public class SpectralDisplayPanel extends JPanel mTuner = null; } - + /** - * Fetches the color settings from the settings manager + * Queues an FFT size change request. The scheduled executor will apply + * the change when it runs. */ - private void setColors() + public void setDFTSize( DFTSize size ) { - mColorChannelConfig = getColor( ColorSettingName.CHANNEL_CONFIG ); + mDFTProcessor.setDFTSize( size ); + mOverlayPanel.setDFTSize( size ); + mDFTSize = size; - mColorChannelConfigProcessing = - getColor( ColorSettingName.CHANNEL_CONFIG_PROCESSING ); - - mColorChannelConfigSelected = - getColor( ColorSettingName.CHANNEL_CONFIG_SELECTED ); - - mColorSpectrumCursor = getColor( ColorSettingName.SPECTRUM_CURSOR ); - - mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE ); - - mColorSpectrumBackground = - getColor( ColorSettingName.SPECTRUM_BACKGROUND ); - - mColorSpectrumGradientBottom = - getColor( ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ); - - mColorSpectrumGradientTop = - getColor( ColorSettingName.SPECTRUM_GRADIENT_TOP ); + setZoom( 0, 0, 0 ); + } + + @Override + public DFTSize getDFTSize() + { + return mDFTSize; } - /** - * Fetches a named color setting from the settings manager. If the setting - * doesn't exist, creates the setting using the defaultColor - */ - private Color getColor( ColorSettingName name ) - { - ColorSetting setting = mResourceManager.getSettingsManager() - .getColorSetting( name ); - - return setting.getColor(); - } + public int getZoom() + { + return mZoom; + } + + /** + * Sets the current zoom level (2^zoom) + * + * 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 - 5. + * @param frequency under the mouse to maintain while zooming + * @param xAxisOffset where to maintain the frequency under the mouse + */ + public void setZoom( int zoom, long frequency, double windowOffset ) + { + if( zoom != mZoom ) + { + mZoom = zoom; + + double zoomTotal = Math.pow( 2.0, mZoom ); - /** - * Stores the named color setting - */ - private void setColor( ColorSettingName name, Color color, int translucency ) - { - Color adjustedColor = ColorSetting.getTranslucent( color, translucency ); - - mResourceManager.getSettingsManager() - .setColorSetting( name, adjustedColor ); - } + double overallOffset = getBinOffset( frequency ); + double windowBinOffset = ( (double)mDFTSize.getSize() / + (double)zoomTotal ) * + windowOffset; + + mDFTZoomWindowOffset = (int)( overallOffset - windowBinOffset ); + + mSpectrumPanel.setZoom( mZoom, mDFTZoomWindowOffset ); + mOverlayPanel.setZoom( mZoom, mDFTZoomWindowOffset ); + mWaterfallPanel.setZoom( mZoom, mDFTZoomWindowOffset ); + } + } + + /** + * Calculates the overall offset of the frequency from the current minimum + * frequency in terms of total FFT width + * @param frequency + * @return + */ + private double getBinOffset( long frequency ) + { + if( mOverlayPanel.containsFrequency( frequency ) ) + { + return (double)mDFTSize.getSize() * + ( (double)( frequency - mOverlayPanel.getMinFrequency() ) / + (double)mOverlayPanel.getBandwidth() ); + } + + return 0.0; + } /** * Monitors for setting changes. Colors can be changed by external actions @@ -231,41 +262,6 @@ public class SpectralDisplayPanel extends JPanel { mSpectrumPanel.settingChanged( setting ); } - - if( setting instanceof ColorSetting ) - { - ColorSetting colorSetting = (ColorSetting)setting; - - switch( ((ColorSetting)setting).getColorSettingName() ) - { - case CHANNEL_CONFIG: - mColorChannelConfig = colorSetting.getColor(); - break; - case CHANNEL_CONFIG_PROCESSING: - mColorChannelConfigProcessing = colorSetting.getColor(); - break; - case CHANNEL_CONFIG_SELECTED: - mColorChannelConfigSelected = colorSetting.getColor(); - break; - case SPECTRUM_BACKGROUND: - mColorSpectrumBackground = colorSetting.getColor(); - break; - case SPECTRUM_CURSOR: - mColorSpectrumCursor = 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; - } - } } /** @@ -304,6 +300,7 @@ public class SpectralDisplayPanel extends JPanel mOverlayPanel = new OverlayPanel( mResourceManager ); mOverlayPanel.addMouseListener( mouser ); mOverlayPanel.addMouseMotionListener( mouser ); + mOverlayPanel.addMouseWheelListener( mouser ); //Add the spectrum and channel panels to the layered panel mLayeredPanel.add( mSpectrumPanel, new Integer( 0 ), 0 ); @@ -313,6 +310,7 @@ public class SpectralDisplayPanel extends JPanel mWaterfallPanel = new WaterfallPanel( mResourceManager ); mWaterfallPanel.addMouseListener( mouser ); mWaterfallPanel.addMouseMotionListener( mouser ); + mWaterfallPanel.addMouseWheelListener( mouser ); /* Attempt to set a 50/50 split preferred size for the split pane */ double totalHeight = mLayeredPanel.getPreferredSize().getHeight() + @@ -330,7 +328,9 @@ public class SpectralDisplayPanel extends JPanel splitPane.add( mLayeredPanel ); splitPane.add( mWaterfallPanel ); - add( splitPane, "grow" ); + mScrollPane = new JScrollPane( splitPane ); + + add( mScrollPane, "grow" ); /** * Setup DFTProcessor to process samples and register the waterfall and @@ -451,8 +451,30 @@ public class SpectralDisplayPanel extends JPanel /** * Mouse event handler for the channel panel. */ - public class MouseEventProcessor implements MouseMotionListener, MouseListener + public class MouseEventProcessor implements MouseMotionListener, + MouseListener, + MouseWheelListener { + @Override + public void mouseWheelMoved( MouseWheelEvent e ) + { + int zoom = mZoom - e.getWheelRotation(); + + if( zoom < NO_ZOOM ) + { + zoom = NO_ZOOM; + } + else if( zoom > MAX_ZOOM ) + { + zoom = MAX_ZOOM; + } + + long frequency = mOverlayPanel.getFrequencyFromAxis( e.getX() ); + + double windowOffset = (double)e.getX() / (double)getWidth(); + + setZoom( zoom, frequency, windowOffset ); + } @Override public void mouseMoved( MouseEvent event ) @@ -631,9 +653,9 @@ public class SpectralDisplayPanel extends JPanel JMenu fftWidthMenu = new JMenu( "FFT Width" ); displayMenu.add( fftWidthMenu ); - for( FFTWidth width: FFTWidth.values() ) + for( DFTSize width: DFTSize.values() ) { - fftWidthMenu.add( new FFTWidthItem( mDFTProcessor, width ) ); + fftWidthMenu.add( new DFTSizeItem( SpectralDisplayPanel.this, width ) ); } /** @@ -683,6 +705,18 @@ public class SpectralDisplayPanel extends JPanel displayMenu.add( smoothingMenu ); } + /* + * Zoom menu + */ + JMenuItem zoomMenu = new JMenu( "Zoom" ); + + double windowOffset = (double)event.getX() / (double)getWidth(); + + zoomMenu.add( new ZoomItem( frequency, windowOffset ) ); + + contextMenu.add( zoomMenu ); + + if( contextMenu != null ) { if( event.getComponent() == mOverlayPanel ) @@ -741,6 +775,47 @@ public class SpectralDisplayPanel extends JPanel } } + public class ZoomItem extends JSlider + { + private static final long serialVersionUID = 1L; + + private long mFrequency; + private double mWindowOffset; + + public ZoomItem( long frequency, double windowOffset ) + { + super( NO_ZOOM, MAX_ZOOM, mZoom ); + + mFrequency = frequency; + mWindowOffset = windowOffset; + + Hashtable labels = new Hashtable<>(); + labels.put( new Integer( 0 ), new JLabel( "1x" ) ); + labels.put( new Integer( 1 ), new JLabel( "2x" ) ); + labels.put( new Integer( 2 ), new JLabel( "4x" ) ); + labels.put( new Integer( 3 ), new JLabel( "8x" ) ); + labels.put( new Integer( 4 ), new JLabel( "16x" ) ); + labels.put( new Integer( 5 ), new JLabel( "32x" ) ); + labels.put( new Integer( 6 ), new JLabel( "64x" ) ); + + setLabelTable( labels ); + + setMajorTickSpacing( 1 ); + setMinorTickSpacing( 1 ); + setPaintTicks( true ); + setPaintLabels( true ); + + this.addChangeListener( new ChangeListener() + { + @Override + public void stateChanged( ChangeEvent e ) + { + setZoom( getValue(), mFrequency, mWindowOffset ); + } + } ); + } + } + public class ChannelDisplayItem extends JCheckBoxMenuItem { private static final long serialVersionUID = 1L; diff --git a/src/spectrum/SpectrumPanel.java b/src/spectrum/SpectrumPanel.java index 28201d1f..3ce5a131 100644 --- a/src/spectrum/SpectrumPanel.java +++ b/src/spectrum/SpectrumPanel.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * 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 @@ -51,40 +51,42 @@ public class SpectrumPanel extends JPanel SpectralDisplayAdjuster { private static final long serialVersionUID = 1L; + private final static Logger mLog = LoggerFactory.getLogger( SpectrumPanel.class ); - /* Set display bins size to 1, so that we're guaranteed a reset to the - * correct width once the first sample set arrives */ - private float[] mDisplayFFTBins = new float[ 1 ]; - private int mAveraging = 4; - - private float mDBScale; - - /** - * Spectral Display Color Settings - */ - private static final String SPECTRUM_BACKGROUND = "spectrum_background"; - private static final String SPECTRUM_GRADIENT_TOP = "spectrum_gradient_top"; - private static final String SPECTRUM_GRADIENT_BOTTOM = "spectrum_gradient_bottom"; - private static final String SPECTRUM_LINE = "spectrum_line"; - private static final String SPECTRUM_AVERAGING_SIZE = "spectrum_averaging_size"; - private static final int SPECTRUM_TRANSLUCENCY = 128; - private static final RenderingHints RENDERING_HINTS = new RenderingHints( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); - - + //Color settings + private static final String SPECTRUM_BACKGROUND = "spectrum_background"; + private static final String SPECTRUM_GRADIENT_TOP = "spectrum_gradient_top"; + private static final String SPECTRUM_GRADIENT_BOTTOM = "spectrum_gradient_bottom"; + private static final String SPECTRUM_LINE = "spectrum_line"; + 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 mZoomBinOffset = 0;; private ResourceManager mResourceManager; @@ -109,104 +111,30 @@ public class SpectrumPanel extends JPanel mResourceManager = null; } - public void setSampleSize( double sampleSize ) - { - assert( 2.0 <= sampleSize && sampleSize <= 32.0 ); - - mDBScale = (float)( 20.0 * Math.log10( 1 / Math.pow( 2.0, sampleSize - 1 ) ) ); - } - - public void setAveraging( int size ) - { - mAveraging = size; - } - - public int getAveraging() - { - return mAveraging; - } - - public void clearSpectrum() - { - mDisplayFFTBins = null; - - repaint(); - } - - private void getSettings() - { - mColorSpectrumBackground = - getColor( ColorSettingName.SPECTRUM_BACKGROUND ); - - mColorSpectrumGradientBottom = - getColor( ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ); - - mColorSpectrumGradientTop = - getColor( ColorSettingName.SPECTRUM_GRADIENT_TOP ); - - mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE ); - - mAveraging = 4; - } - - private Color getColor( ColorSettingName name ) - { - ColorSetting setting = - mResourceManager.getSettingsManager().getColorSetting( name ); - - return setting.getColor(); - } - - @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; - } - } - } - - /** - * DFTResultsListener interface method - for receiving the processed data + * 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 fft results variables + //Construct and/or resize our DFT results variables if( mDisplayFFTBins == null || mDisplayFFTBins.length != currentFFTBins.length ) { mDisplayFFTBins = currentFFTBins; } - //Smooth across the bins + //Apply smoothing across the bins of the DFT results float[] smoothedBins = mSmoothingFilter.filter( currentFFTBins ); - //Average bins over multiple frames + //Apply averaging over multiple DFT output frames if( mAveraging > 1 ) { float gain = 1.0f / (float)mAveraging; @@ -221,7 +149,6 @@ public class SpectrumPanel extends JPanel mDisplayFFTBins = smoothedBins; } - repaint(); } @@ -270,17 +197,19 @@ public class SpectrumPanel extends JPanel //Draw to the lower left spectrumShape.lineTo( 0, size.getHeight() - mSpectrumInset ); + + float[] bins = getBins(); //If we have FFT data to display ... - if( mDisplayFFTBins != null ) + if( bins != null ) { float insideHeight = size.height - mSpectrumInset; - float scalor = insideHeight / mDBScale; + float scalor = insideHeight / -mDBScale; float insideWidth = size.width; - int binCount = mDisplayFFTBins.length; + int binCount = bins.length; float binSize = insideWidth / ( binCount ); @@ -288,24 +217,17 @@ public class SpectrumPanel extends JPanel { float height; - if( mDisplayFFTBins[ x ] > 0 ) + height = bins[ x ] * scalor; + + if( height > insideHeight ) + { + height = insideHeight; + } + + if( height < 0 ) { height = 0; } - else - { - height = mDisplayFFTBins[ x ] * scalor; - - if( height > insideHeight ) - { - height = insideHeight; - } - - if( height < 0 ) - { - height = 0; - } - } spectrumShape.lineTo( ( x * binSize ), height ); } @@ -338,27 +260,188 @@ public class SpectrumPanel extends JPanel 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, int offset ) + { + assert( 0 <= zoom && zoom <= 5 ); + + mZoom = zoom; + + setZoomBinOffset( offset ); + } + + /** + * Sets the offset to define which subset of zoomed bins to display + * + * @param offset + */ + public void setZoomBinOffset( int offset ) + { + mZoomBinOffset = 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 getSettings() + { + 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 = + mResourceManager.getSettingsManager().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 float[] getBins() + { + if( mZoom == 0 ) + { + return mDisplayFFTBins; + } + else + { + int zoom = (int)Math.pow( 2.0, mZoom ); + + int length = mDisplayFFTBins.length / zoom; + + int offset = mZoomBinOffset; + + if( ( offset + length ) > mDisplayFFTBins.length ) + { + offset = mDisplayFFTBins.length - length; + } + + 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 ) { diff --git a/src/spectrum/WaterfallPanel.java b/src/spectrum/WaterfallPanel.java index 9d815fe3..4b491c40 100644 --- a/src/spectrum/WaterfallPanel.java +++ b/src/spectrum/WaterfallPanel.java @@ -60,7 +60,7 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, private static final String PAUSED = "PAUSED"; private byte[] mPixels; - private int mFFTSize = 1024; + private int mDFTSize = 4096; private int mImageHeight = 700; private MemoryImageSource mMemoryImageSource; private ColorModel mColorModel = WaterfallColorModel.getDefaultColorModel(); @@ -71,6 +71,8 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, private boolean mCursorVisible = false; private long mCursorFrequency = 0; private AtomicBoolean mPaused = new AtomicBoolean(); + private int mZoom = 0; + private int mDFTZoomOffset = 0; private ResourceManager mResourceManager; @@ -93,14 +95,14 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, private void reset() { - mPixels = new byte[ mFFTSize * mImageHeight ]; + mPixels = new byte[ mDFTSize * mImageHeight ]; - mMemoryImageSource = new MemoryImageSource( mFFTSize, + mMemoryImageSource = new MemoryImageSource( mDFTSize, mImageHeight, mColorModel, mPixels, 0, - mFFTSize ); + mDFTSize ); mMemoryImageSource.setAnimated( true ); @@ -122,6 +124,25 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, return mPaused.get(); } + /** + * Sets the current zoom level (2^zoom) + * + * 0 No Zoom + * 1 2x Zoom + * 2 4x Zoom + * 3 8x Zoom + * 4 16x Zoom + * 5 32x Zoom + * + * @param zoom level, 0 - 5. + * @param offset into the DFT bins for display + */ + public void setZoom( int zoom, int offset ) + { + mZoom = zoom; + mDFTZoomOffset = offset; + } + /** * Fetches a named color setting from the settings manager. If the setting * doesn't exist, creates the setting using the defaultColor @@ -171,10 +192,31 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, repaint(); } + + private int getPixelOffset( int multiplier ) + { + if( mZoom != 0 ) + { + return -(int)( (double)getWidth() * multiplier / + (double)mDFTSize * + (double)( mDFTZoomOffset ) ); + } + + return 0; + } public void paintComponent( Graphics g ) { - g.drawImage( mWaterfallImage, 0, 0, getWidth(), mImageHeight, this ); + int multiplier = (int)Math.pow( 2.0, mZoom ); + + int offset = getPixelOffset( multiplier ); + + g.drawImage( mWaterfallImage, + offset, + 0, + getWidth() * multiplier, + mImageHeight, + this ); Graphics2D graphics = (Graphics2D) g; @@ -194,6 +236,11 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, mCursorLocation.x + 5, mCursorLocation.y ); + if( mZoom != 0 ) + { + graphics.drawString( "Zoom: " + (int)Math.pow( 2.0, mZoom ) + "x", + mCursorLocation.x + 17, mCursorLocation.y + 11 ); + } } if( mPaused.get() ) @@ -208,16 +255,16 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, public void receive( float[] update ) { //If our FFT size changes, reset our pixel map and image source - if( mFFTSize != update.length ) + if( mDFTSize != update.length ) { - mFFTSize = update.length; + mDFTSize = update.length; reset(); } //Move the pixels down a row to make room for the new results System.arraycopy( mPixels, 0, - mPixels, mFFTSize, mPixels.length - mFFTSize ); + mPixels, mDFTSize, mPixels.length - mDFTSize ); /** * Find the average value and scale the display to it @@ -260,7 +307,7 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener, { if( mMemoryImageSource != null ) { - mMemoryImageSource.newPixels( mPixels, mColorModel, 0, mFFTSize ); + mMemoryImageSource.newPixels( mPixels, mColorModel, 0, mDFTSize ); } } } ); diff --git a/src/spectrum/converter/ComplexDecibelConverter.java b/src/spectrum/converter/ComplexDecibelConverter.java index eac3e0f0..cb59cc67 100644 --- a/src/spectrum/converter/ComplexDecibelConverter.java +++ b/src/spectrum/converter/ComplexDecibelConverter.java @@ -78,7 +78,8 @@ public class ComplexDecibelConverter extends DFTResultsConverter { int halfResults = results.length / 2; - float dftScalor = 1.0f / (float)( halfResults ^ 2 ); +// float dftBinSizeScalor = 1.0f / (float)Math.pow( halfResults, 2.0 ); + float dftBinSizeScalor = 1.0f / (float)halfResults; float[] processed = new float[ halfResults ]; @@ -91,7 +92,7 @@ public class ComplexDecibelConverter extends DFTResultsConverter //Convert the scaled value to decibels. float decibels = 10.0f * (float)Math.log10( ( ( results[ x ] * results[ x ] ) + - ( results[ x + 1 ] * results[ x + 1 ] ) ) * dftScalor ); + ( results[ x + 1 ] * results[ x + 1 ] ) ) * dftBinSizeScalor ); // We have to swap the upper and lower halves of the JTransforms // DFT results for correct display diff --git a/src/spectrum/converter/RealDecibelConverter.java b/src/spectrum/converter/RealDecibelConverter.java index 68b5b252..ae620b50 100644 --- a/src/spectrum/converter/RealDecibelConverter.java +++ b/src/spectrum/converter/RealDecibelConverter.java @@ -21,7 +21,7 @@ public class RealDecibelConverter extends DFTResultsConverter @Override public void receive( float[] results ) { - float dftScalor = 1.0f / (float)( results.length ^ 2 ); + float dftBinSizeScalor = 1.0f / (float)results.length; float[] processed = new float[ results.length / 4 ]; @@ -33,7 +33,7 @@ public class RealDecibelConverter extends DFTResultsConverter processed[ x ] = 20.0f * (float)Math.log10( ( ( results[ index ] * results[ index ] ) + - ( results[ index + 1 ] * results[ index + 1 ] ) ) * dftScalor ); + ( results[ index + 1 ] * results[ index + 1 ] ) ) * dftBinSizeScalor ); } dispatch( processed ); diff --git a/src/spectrum/menu/FFTWidthItem.java b/src/spectrum/menu/DFTSizeItem.java similarity index 52% rename from src/spectrum/menu/FFTWidthItem.java rename to src/spectrum/menu/DFTSizeItem.java index 4185b33d..cb8a6aa3 100644 --- a/src/spectrum/menu/FFTWidthItem.java +++ b/src/spectrum/menu/DFTSizeItem.java @@ -5,24 +5,24 @@ import java.awt.event.ActionListener; import javax.swing.JCheckBoxMenuItem; -import spectrum.DFTProcessor; -import spectrum.FFTWidth; +import spectrum.DFTSize; +import spectrum.IDFTWidthChangeProcessor; -public class FFTWidthItem extends JCheckBoxMenuItem +public class DFTSizeItem extends JCheckBoxMenuItem { private static final long serialVersionUID = 1L; - private DFTProcessor mDFTProcessor; - private FFTWidth mFFTWidth; + private IDFTWidthChangeProcessor mDFTProcessor; + private DFTSize mDFTSize; - public FFTWidthItem( DFTProcessor processor, FFTWidth width ) + public DFTSizeItem( IDFTWidthChangeProcessor processor, DFTSize size ) { - super( width.getLabel() ); + super( size.getLabel() ); mDFTProcessor = processor; - mFFTWidth = width; + mDFTSize = size; - if( processor.getFFTWidth() == mFFTWidth ) + if( processor.getDFTSize() == mDFTSize ) { setSelected( true ); } @@ -32,7 +32,7 @@ public class FFTWidthItem extends JCheckBoxMenuItem @Override public void actionPerformed( ActionEvent arg0 ) { - mDFTProcessor.setFFTSize( mFFTWidth ); + mDFTProcessor.setDFTSize( mDFTSize ); } } ); }