Updates pull request from Roman Tsypuk to convert the build system from

ant to gradle.  Removes linux and windows application launch scripts and
produces an executable jar with all resources included.
This commit is contained in:
Denny
2017-12-17 07:28:03 -05:00
parent a1535c4157
commit 9316599dec
981 changed files with 8368 additions and 9049 deletions

View File

@@ -0,0 +1,268 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 io.github.dsheirer.spectrum;
import io.github.dsheirer.settings.ColorSetting;
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
import io.github.dsheirer.settings.Setting;
import io.github.dsheirer.settings.SettingChangeListener;
import io.github.dsheirer.settings.SettingsManager;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
public class ChannelOverlayPanel extends JPanel implements SettingChangeListener
{
private static final long serialVersionUID = 1L;
private static DecimalFormat sFORMAT = new DecimalFormat( "000.0" );
private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.0" );
private double mBandwidth = 6000.0d;
private Point mCursorLocation = new Point( 0, 0 );
private boolean mCursorVisible = false;
/**
* Colors used by this component
*/
private Color mColorSpectrumBackground;
private Color mColorSpectrumCursor;
private Color mColorSpectrumLine;
//Defines the offset at the bottom of the spectral display to account for
//the frequency labels
private float mSpectrumInset = 20.0f;
private SettingsManager mSettingsManager;
/**
* Translucent overlay panel for displaying channel configurations,
* processing channels, selected channels, frequency labels and lines, and
* a cursor with a frequency readout.
*/
public ChannelOverlayPanel( SettingsManager settingsManager )
{
mSettingsManager = settingsManager;
if( mSettingsManager != null )
{
mSettingsManager.addListener( this );
}
//Set the background transparent, so the spectrum display can be seen
setOpaque( false );
//Fetch color settings from settings manager
setColors();
}
public void dispose()
{
if( mSettingsManager != null )
{
mSettingsManager.removeListener( this );
}
mSettingsManager = null;
}
public void setCursorLocation( Point point )
{
mCursorLocation = point;
repaint();
}
public void setCursorVisible( boolean visible )
{
mCursorVisible = visible;
repaint();
}
/**
* Fetches the color settings from the settings manager
*/
private void setColors()
{
mColorSpectrumCursor = getColor( ColorSettingName.SPECTRUM_CURSOR );
mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE );
mColorSpectrumBackground =
getColor( ColorSettingName.SPECTRUM_BACKGROUND );
}
/**
* 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 = mSettingsManager.getColorSetting( name );
return setting.getColor();
}
/**
* Monitors for setting changes. Colors can be changed by external actions
* and will automatically update in this class
*/
@Override
public void settingChanged( Setting setting )
{
if( setting instanceof ColorSetting )
{
ColorSetting colorSetting = (ColorSetting)setting;
switch( colorSetting.getColorSettingName() )
{
case SPECTRUM_BACKGROUND:
mColorSpectrumBackground = colorSetting.getColor();
break;
case SPECTRUM_CURSOR:
mColorSpectrumCursor = colorSetting.getColor();
break;
case SPECTRUM_LINE:
mColorSpectrumLine = colorSetting.getColor();
break;
default:
break;
}
}
}
/**
* Renders the channel configs, lines, labels, and cursor
*/
@Override
public void paintComponent( Graphics g )
{
super.paintComponent( g );
Graphics2D graphics = (Graphics2D) g;
graphics.setBackground( mColorSpectrumBackground );
RenderingHints renderHints =
new RenderingHints( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON );
renderHints.put( RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY );
graphics.setRenderingHints( renderHints );
drawFrequencies( graphics );
drawCursor( graphics );
}
/**
* Draws a cursor on the panel, whenever the mouse is hovering over the
* panel
*/
private void drawCursor( Graphics2D graphics )
{
if( mCursorVisible )
{
drawFrequencyLine( graphics,
mCursorLocation.x,
mColorSpectrumCursor );
double value = mBandwidth *
( mCursorLocation.getX() / getSize().getWidth() );
String frequency = sCURSOR_FORMAT.format( value );
graphics.drawString( frequency ,
mCursorLocation.x + 5,
mCursorLocation.y );
}
}
/**
* Draws the frequency lines and labels every 10kHz
*/
private void drawFrequencies( Graphics2D graphics )
{
drawFrequencyLine( graphics, getAxisFromFrequency( 1000 ), Color.LIGHT_GRAY );
drawFrequencyLineAndLabel( graphics, 2000 );
drawFrequencyLine( graphics, getAxisFromFrequency( 3000 ), Color.LIGHT_GRAY );
drawFrequencyLineAndLabel( graphics, 4000 );
drawFrequencyLine( graphics, getAxisFromFrequency( 5000 ), Color.LIGHT_GRAY );
// drawFrequencyLineAndLabel( graphics, 6000 );
// drawFrequencyLine( graphics, getAxisFromFrequency( 7000 ), Color.LIGHT_GRAY );
// drawFrequencyLineAndLabel( graphics, 8000 );
// drawFrequencyLine( graphics, getAxisFromFrequency( 9000 ), Color.LIGHT_GRAY );
// drawFrequencyLineAndLabel( graphics, 10000 );
// drawFrequencyLine( graphics, getAxisFromFrequency( 11000 ), Color.LIGHT_GRAY );
}
private float getAxisFromFrequency( long frequency )
{
return (float)( getSize().getWidth() * ( (double)frequency / mBandwidth ) );
}
/**
* Draws a vertical line and a corresponding frequency label at the bottom
*/
private void drawFrequencyLineAndLabel( Graphics2D graphics, long frequency )
{
float xAxis = (float)( getSize().getWidth() *
( (double)frequency / mBandwidth ) );
drawFrequencyLine( graphics, xAxis, mColorSpectrumLine );
graphics.setColor( mColorSpectrumLine );
drawFrequencyLabel( graphics, xAxis, frequency );
}
/**
* Draws a vertical line at the xaxis
*/
private void drawFrequencyLine( Graphics2D graphics, float xaxis, Color color )
{
graphics.setColor( color );
graphics.draw( new Line2D.Float( xaxis, 0,
xaxis, (float)(getSize().getHeight()) - mSpectrumInset ) );
}
/**
* Draws a frequency label at the x-axis position, at the bottom of the panel
*/
private void drawFrequencyLabel( Graphics2D graphics,
float xaxis,
long frequency )
{
String label = sFORMAT.format( (float)frequency );
FontMetrics fontMetrics = graphics.getFontMetrics( this.getFont() );
Rectangle2D rect = fontMetrics.getStringBounds( label, graphics );
float offset = (float)rect.getWidth() / 2;
graphics.drawString( label, xaxis - offset,
(float)getSize().getHeight() - ( mSpectrumInset * 0.2f ) );
}
public void settingDeleted( Setting setting ) {}
}

View File

@@ -0,0 +1,484 @@
package io.github.dsheirer.spectrum;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.ChannelEventListener;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.SampleType;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.real.RealBuffer;
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
import io.github.dsheirer.settings.ColorSettingMenuItem;
import io.github.dsheirer.settings.Setting;
import io.github.dsheirer.settings.SettingChangeListener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
import io.github.dsheirer.spectrum.converter.DFTResultsConverter;
import io.github.dsheirer.spectrum.converter.RealDecibelConverter;
import io.github.dsheirer.spectrum.menu.AveragingItem;
import io.github.dsheirer.spectrum.menu.DFTSizeItem;
import io.github.dsheirer.spectrum.menu.FFTWindowTypeItem;
import io.github.dsheirer.spectrum.menu.FrameRateItem;
import io.github.dsheirer.spectrum.menu.SmoothingItem;
import io.github.dsheirer.spectrum.menu.SmoothingTypeItem;
import net.miginfocom.swing.MigLayout;
import io.github.dsheirer.dsp.filter.Filters;
import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB;
import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType;
import io.github.dsheirer.module.ProcessingChain;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.concurrent.atomic.AtomicBoolean;
public class ChannelSpectrumPanel extends JPanel
implements ChannelEventListener,
Listener<RealBuffer>,
SettingChangeListener,
SpectralDisplayAdjuster
{
private static final long serialVersionUID = 1L;
private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.REAL );
private DFTResultsConverter mDFTConverter = new RealDecibelConverter();
private JLayeredPane mLayeredPane;
private SpectrumPanel mSpectrumPanel;
private ChannelOverlayPanel mOverlayPanel;
private Channel mCurrentChannel;
private int mSampleBufferSize = 2400;
private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true );
private AtomicBoolean mEnabled = new AtomicBoolean();
private SettingsManager mSettingsManager;
private ChannelProcessingManager mChannelProcessingManager;
public ChannelSpectrumPanel( SettingsManager settingsManager,
ChannelProcessingManager channelProcessingManager )
{
mSettingsManager = settingsManager;
mChannelProcessingManager = channelProcessingManager;
if( mSettingsManager != null )
{
mSettingsManager.addListener( this );
}
mSpectrumPanel = new SpectrumPanel( mSettingsManager );
mSpectrumPanel.setAveraging( 1 );
mOverlayPanel = new ChannelOverlayPanel( mSettingsManager );
mDFTProcessor.addConverter( mDFTConverter );
mDFTConverter.addListener( mSpectrumPanel );
/* Set the DFTProcessor to the decimated 24kHz sample rate */
mDFTProcessor.frequencyChanged(
new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, 24000 ) );
initGui();
}
public void dispose()
{
setEnabled( false );
mDFTProcessor.dispose();
if( mSettingsManager != null )
{
mSettingsManager.removeListener( this );
}
mSettingsManager = null;
mCurrentChannel = null;
mDFTProcessor = null;
mSpectrumPanel = null;
}
public void setFrameRate( int framesPerSecond )
{
mSampleBufferSize = (int)( 48000 / framesPerSecond );
mDFTProcessor.setFrameRate( framesPerSecond );
}
private void initGui()
{
setLayout( new MigLayout( "insets 0 0 0 0 ",
"[grow,fill]",
"[grow,fill]") );
mLayeredPane = new JLayeredPane();
mLayeredPane.addComponentListener( new ResizeListener() );
MouseEventProcessor mouser = new MouseEventProcessor();
mOverlayPanel.addMouseListener( mouser );
mOverlayPanel.addMouseMotionListener( mouser );
mLayeredPane.add( mSpectrumPanel, new Integer( 0 ), 0 );
mLayeredPane.add( mOverlayPanel, new Integer( 1 ), 0 );
add( mLayeredPane );
}
public void setEnabled( boolean enabled )
{
if( enabled && mEnabled.compareAndSet( false, true ) )
{
start();
}
else if( !enabled && mEnabled.compareAndSet( true, false ) )
{
stop();
}
}
@Override
@SuppressWarnings( "incomplete-switch" )
public void channelChanged( ChannelEvent event )
{
switch( event.getEvent() )
{
case NOTIFICATION_SELECTION_CHANGE:
//ChannelSelectionManager ensures that only 1 channel can be
//selected and any previously selected channel will be first
//deselected before we get a new selection event
if( event.getChannel().isSelected() )
{
if( mCurrentChannel != null )
{
stop();
mCurrentChannel = null;
}
mCurrentChannel = event.getChannel();
if( mEnabled.get() )
{
start();
}
}
else
{
stop();
mCurrentChannel = null;
}
break;
case NOTIFICATION_PROCESSING_STOP:
if( event.getChannel() == mCurrentChannel )
{
if( mEnabled.get() )
{
stop();
}
mCurrentChannel = null;
}
break;
}
}
private void start()
{
if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.getEnabled() )
{
ProcessingChain processingChain = mChannelProcessingManager
.getProcessingChain( mCurrentChannel );
if( processingChain != null )
{
processingChain.addRealBufferListener( this );
mDFTProcessor.start();
}
}
}
private void stop()
{
if( mCurrentChannel != null && mCurrentChannel.getEnabled() )
{
ProcessingChain processingChain = mChannelProcessingManager
.getProcessingChain( mCurrentChannel );
if( processingChain != null )
{
processingChain.removeRealBufferListener( this );
}
}
mDFTProcessor.stop();
mSpectrumPanel.clearSpectrum();
}
@Override
public void settingChanged( Setting setting )
{
if( mSpectrumPanel != null )
{
mSpectrumPanel.settingChanged( setting );
}
if( mOverlayPanel != null )
{
mOverlayPanel.settingChanged( setting );
}
}
@Override
public void settingDeleted( Setting setting )
{
if( mSpectrumPanel != null )
{
mSpectrumPanel.settingDeleted( setting );
}
if( mOverlayPanel != null )
{
mOverlayPanel.settingDeleted( setting );
}
}
@Override
public void receive( RealBuffer buffer )
{
RealBuffer decimated = mDecimatingFilter.filter( buffer );
//Hack: we're placing real samples in a complex buffer that the DFT
//processor is expecting.
mDFTProcessor.receive( new ComplexBuffer( decimated.getSamples() ) );
}
/**
* Monitors the sizing of the layered pane and resizes the spectrum and
* channel panels whenever the layered pane is resized
*/
public class ResizeListener implements ComponentListener
{
@Override
public void componentResized( ComponentEvent e )
{
Component c = e.getComponent();
mSpectrumPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() );
mOverlayPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() );
}
@Override
public void componentHidden( ComponentEvent arg0 ) {}
@Override
public void componentMoved( ComponentEvent arg0 ) {}
@Override
public void componentShown( ComponentEvent arg0 ) {}
}
/**
* Mouse event handler for the channel panel.
*/
public class MouseEventProcessor implements MouseMotionListener, MouseListener
{
@Override
public void mouseMoved( MouseEvent event )
{
update( event );
}
@Override
public void mouseDragged( MouseEvent event )
{
update( event );
}
private void update( MouseEvent event )
{
if( event.getComponent() == mOverlayPanel )
{
mOverlayPanel.setCursorLocation( event.getPoint() );
}
}
@Override
public void mouseEntered( MouseEvent e )
{
if( e.getComponent() == mOverlayPanel )
{
mOverlayPanel.setCursorVisible( true );
}
}
@Override
public void mouseExited( MouseEvent e )
{
mOverlayPanel.setCursorVisible( false );
}
/**
* Displays the context menu.
*/
@Override
public void mouseClicked( MouseEvent event )
{
if( SwingUtilities.isRightMouseButton( event ) )
{
JPopupMenu contextMenu = new JPopupMenu();
/**
* Color Menus
*/
JMenu colorMenu = new JMenu( "Color" );
colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
ColorSettingName.SPECTRUM_CURSOR ) );
colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
ColorSettingName.SPECTRUM_LINE ) );
colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
ColorSettingName.SPECTRUM_BACKGROUND ) );
colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ) );
colorMenu.add( new ColorSettingMenuItem( mSettingsManager,
ColorSettingName.SPECTRUM_GRADIENT_TOP ) );
contextMenu.add( colorMenu );
/**
* Display items: fft and frame rate
*/
JMenu displayMenu = new JMenu( "Display" );
contextMenu.add( displayMenu );
/**
* Averaging menu
*/
JMenu averagingMenu = new JMenu( "Averaging" );
averagingMenu.add(
new AveragingItem( ChannelSpectrumPanel.this, 2 ) );
displayMenu.add( averagingMenu );
/**
* FFT width
*/
JMenu fftWidthMenu = new JMenu( "FFT Width" );
displayMenu.add( fftWidthMenu );
for( DFTSize width: DFTSize.values() )
{
fftWidthMenu.add( new DFTSizeItem( mDFTProcessor, width ) );
}
/**
* DFT Processor Frame Rate
*/
JMenu frameRateMenu = new JMenu( "Frame Rate" );
displayMenu.add( frameRateMenu );
frameRateMenu.add( new FrameRateItem( mDFTProcessor, 14 ) );
frameRateMenu.add( new FrameRateItem( mDFTProcessor, 16 ) );
frameRateMenu.add( new FrameRateItem( mDFTProcessor, 18 ) );
frameRateMenu.add( new FrameRateItem( mDFTProcessor, 20 ) );
frameRateMenu.add( new FrameRateItem( mDFTProcessor, 25 ) );
frameRateMenu.add( new FrameRateItem( mDFTProcessor, 30 ) );
frameRateMenu.add( new FrameRateItem( mDFTProcessor, 40 ) );
frameRateMenu.add( new FrameRateItem( mDFTProcessor, 50 ) );
/**
* FFT Window Type
*/
JMenu fftWindowType = new JMenu( "Window Type" );
displayMenu.add( fftWindowType );
for( WindowType type: WindowType.values() )
{
fftWindowType.add(
new FFTWindowTypeItem( mDFTProcessor, type ) );
}
/**
* Smoothing menu
*/
JMenu smoothingMenu = new JMenu( "Smoothing" );
if( mSpectrumPanel.getSmoothingType() != SmoothingType.NONE )
{
smoothingMenu.add( new SmoothingItem( ChannelSpectrumPanel.this, 5 ) );
smoothingMenu.add( new JSeparator() );
}
smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.GAUSSIAN ) );
smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.TRIANGLE ) );
smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.RECTANGLE ) );
smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.NONE ) );
displayMenu.add( smoothingMenu );
if( contextMenu != null )
{
contextMenu.show( mOverlayPanel,
event.getX(),
event.getY() );
}
}
}
@Override
public void mousePressed( MouseEvent e ) {}
@Override
public void mouseReleased( MouseEvent e ) {}
}
@Override
public int getAveraging()
{
return mSpectrumPanel.getAveraging();
}
@Override
public void setAveraging( int averaging )
{
mSpectrumPanel.setAveraging( averaging );
}
public void setSampleSize( double sampleSize )
{
mSpectrumPanel.setSampleSize( sampleSize );
}
@Override
public int getSmoothing()
{
return mSpectrumPanel.getSmoothing();
}
@Override
public void setSmoothing( int smoothing )
{
mSpectrumPanel.setSmoothing( smoothing );
}
@Override
public SmoothingType getSmoothingType()
{
return mSpectrumPanel.getSmoothingType();
}
@Override
public void setSmoothingType( SmoothingType type )
{
mSpectrumPanel.setSmoothingType( type );
}
}

View File

@@ -0,0 +1,482 @@
/*******************************************************************************
* 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 io.github.dsheirer.spectrum;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Buffer;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.SampleType;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
import io.github.dsheirer.spectrum.converter.DFTResultsConverter;
import io.github.dsheirer.util.ThreadPool;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.dsheirer.dsp.filter.Window;
import io.github.dsheirer.dsp.filter.Window.WindowType;
import java.util.Arrays;
import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 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<ComplexBuffer>, IFrequencyChangeProcessor, IDFTWidthChangeProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(DFTProcessor.class);
private CopyOnWriteArrayList<DFTResultsConverter> mListeners = new CopyOnWriteArrayList<DFTResultsConverter>();
//Fixed size sample queue. We set the size at 12 based on the highest
//sample rate tuner (10 MHz) producing 153 frames per second and the lowest
//DFT processing frame rate of 12 frames per second. This means that the
//maximum size needed is 153 / 12 = 11 frames per DFT processing iteration
//so we set it at 12.
private BlockingQueue<ComplexBuffer> mQueue = new ArrayBlockingQueue<>(12);
private ScheduledFuture<?> mProcessorTaskHandle;
public static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate";
private DFTSize mDFTSize = DFTSize.FFT04096;
private DFTSize mNewDFTSize = DFTSize.FFT04096;
private double[] mWindow;
/* The Cosine and Hanning windows seem to offer the best spectral display
* with minimal bin leakage/smearing */
private WindowType mWindowType = Window.WindowType.HANNING;
private FloatFFT_1D mFFT = new FloatFFT_1D(mDFTSize.getSize());
private int mFrameRate;
private int mSampleRate;
private int mFFTFloatsPerFrame;
private float mNewFloatsPerFrame;
private float mNewFloatResidual;
private float[] mPreviousFrame = new float[8192];
private float[] mCurrentBuffer;
private int mCurrentBufferPointer = 0;
private SampleType mSampleType;
private AtomicBoolean mRunning = new AtomicBoolean();
public DFTProcessor(SampleType sampleType)
{
setSampleType(sampleType);
mFrameRate = SystemProperties.getInstance().get(FRAME_RATE_PROPERTY, 20);
calculateConsumptionRate();
start();
}
public void dispose()
{
stop();
mListeners.clear();
mQueue.clear();
mWindow = null;
mCurrentBuffer = null;
}
public WindowType getWindowType()
{
return mWindowType;
}
public void setWindowType(WindowType windowType)
{
mWindowType = windowType;
if(mSampleType == SampleType.COMPLEX)
{
mWindow = Window.getWindow(mWindowType, mDFTSize.getSize() * 2);
}
else
{
mWindow = Window.getWindow(mWindowType, mDFTSize.getSize());
}
}
/**
* Sets the processor mode to Float or Complex, depending on the sample
* types that will be delivered for processing
*/
public void setSampleType(SampleType type)
{
mSampleType = type;
setWindowType(mWindowType);
}
public SampleType getSampleType()
{
return mSampleType;
}
/**
* Queues an FFT size change request. The scheduled executor will apply
* the change when it runs.
*/
public void setDFTSize(DFTSize size)
{
mNewDFTSize = size;
}
public DFTSize getDFTSize()
{
return mDFTSize;
}
public int getFrameRate()
{
return mFrameRate;
}
public void setFrameRate(int framesPerSecond)
{
//TODO: make sure frame rate & sample rate sample requirement doesn't
//expect overlap greater than the previous frame length
if(framesPerSecond < 1 || framesPerSecond > 1000)
{
throw new IllegalArgumentException("DFTProcessor cannot run "
+ "more than 1000 times per second -- requested setting:"
+ framesPerSecond);
}
mFrameRate = framesPerSecond;
SystemProperties.getInstance().set(FRAME_RATE_PROPERTY, mFrameRate);
calculateConsumptionRate();
restart();
}
public void start()
{
//Schedule the DFT to run calculations at a fixed rate
int initialDelay = 0;
int period = (int) (1000 / mFrameRate);
mProcessorTaskHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(new DFTCalculationTask(), initialDelay, period,
TimeUnit.MILLISECONDS);
}
public void stop()
{
//Cancel running DFT calculation task
if(mProcessorTaskHandle != null)
{
mProcessorTaskHandle.cancel(true);
}
}
public void restart()
{
stop();
start();
}
public int getCalculationsPerSecond()
{
return mFrameRate;
}
/**
* Places the sample into a transfer queue for future processing.
*/
@Override
public void receive(ComplexBuffer sampleBuffer)
{
mQueue.offer(sampleBuffer);
}
private void getNextBuffer()
{
mCurrentBuffer = null;
try
{
Buffer buffer = mQueue.take();
mCurrentBuffer = buffer.getSamples();
}
catch(InterruptedException e)
{
mCurrentBuffer = null;
}
mCurrentBufferPointer = 0;
}
private float[] getSamples()
{
int remaining = (int) mFFTFloatsPerFrame;
float[] currentFrame = new float[remaining];
int currentFramePointer = 0;
float integralFloatsToConsume = mNewFloatsPerFrame + mNewFloatResidual;
int newFloatsToConsumeThisFrame = (int) integralFloatsToConsume;
mNewFloatResidual = integralFloatsToConsume - newFloatsToConsumeThisFrame;
/* If the number of required floats for the fft is greater than the
* consumption rate per frame, we have to reach into the previous
* frame to makeup the difference. */
if(newFloatsToConsumeThisFrame < remaining)
{
int previousFloatsRequired = remaining - newFloatsToConsumeThisFrame;
System.arraycopy(mPreviousFrame,
mPreviousFrame.length - previousFloatsRequired,
currentFrame,
currentFramePointer,
previousFloatsRequired);
remaining -= previousFloatsRequired;
currentFramePointer += previousFloatsRequired;
}
/* Fill the rest of the buffer with new samples */
while(mRunning.get() && remaining > 0)
{
if(mCurrentBuffer == null ||
mCurrentBufferPointer >= mCurrentBuffer.length)
{
getNextBuffer();
}
/* If we don't have new samples to use, send the current frame with
* the remaining values as zero */
if(mCurrentBuffer == null)
{
remaining = 0;
}
else
{
int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer;
while(remaining > 0 && samplesAvailable > 0)
{
currentFrame[currentFramePointer++] =
(float) mCurrentBuffer[mCurrentBufferPointer++];
samplesAvailable--;
remaining--;
newFloatsToConsumeThisFrame--;
}
}
}
/* If the incoming float rate is greater than the fft consumption rate,
* then we have to purge some floats, otherwise, store the previous
* frame, because we have overlapping frames */
if(newFloatsToConsumeThisFrame > 0)
{
purge(newFloatsToConsumeThisFrame);
}
else
{
mPreviousFrame = Arrays.copyOf(currentFrame, currentFrame.length);
}
return currentFrame;
}
private void calculate()
{
float[] samples = getSamples();
Window.apply(mWindow, samples);
if(mSampleType == SampleType.REAL)
{
mFFT.realForward(samples);
}
else
{
mFFT.complexForward(samples);
}
dispatch(samples);
}
private void purge(int samplesToPurge)
{
if(samplesToPurge <= 0)
{
throw new IllegalArgumentException("DFTProcessor - cannot purge "
+ "negative sample amount");
}
while(mRunning.get() && samplesToPurge > 0)
{
if(mCurrentBuffer == null ||
mCurrentBufferPointer >= mCurrentBuffer.length)
{
getNextBuffer();
}
if(mCurrentBuffer != null)
{
int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer;
if(samplesAvailable >= samplesToPurge)
{
mCurrentBufferPointer += samplesToPurge;
samplesToPurge = 0;
}
else
{
samplesToPurge -= samplesAvailable;
mCurrentBufferPointer = mCurrentBuffer.length;
}
}
else
{
samplesToPurge = 0;
}
}
}
/**
* Takes a calculated DFT results set, reformats the data, and sends it
* out to all registered listeners.
*/
private void dispatch(float[] results)
{
Iterator<DFTResultsConverter> it = mListeners.iterator();
while(it.hasNext())
{
it.next().receive(results);
}
}
public void addConverter(DFTResultsConverter listener)
{
mListeners.add(listener);
}
public void removeConverter(DFTResultsConverter listener)
{
mListeners.remove(listener);
}
private class DFTCalculationTask implements Runnable
{
@Override
public void run()
{
try
{
/* Only run if we're not currently running */
if(mRunning.compareAndSet(false, true))
{
checkFFTSize();
calculate();
mRunning.set(false);
}
}
catch(Exception e)
{
mLog.error("error during dft processor calculation task", e);
}
}
}
/**
* Checks for a queued FFT width change request and applies it. This
* method will only be accessed by the scheduled executor that gains
* access to run a calculate method, thus providing thread safety.
*/
private void checkFFTSize()
{
if(mNewDFTSize.getSize() != mDFTSize.getSize())
{
mDFTSize = mNewDFTSize;
calculateConsumptionRate();
setWindowType(mWindowType);
if(mSampleType == SampleType.COMPLEX)
{
mPreviousFrame = new float[mDFTSize.getSize() * 2];
}
else
{
mPreviousFrame = new float[mDFTSize.getSize()];
}
mFFT = new FloatFFT_1D(mDFTSize.getSize());
}
}
public void clearBuffer()
{
mQueue.clear();
}
@Override
public void frequencyChanged(FrequencyChangeEvent event)
{
switch(event.getEvent())
{
case NOTIFICATION_SAMPLE_RATE_CHANGE:
mSampleRate = event.getValue().intValue();
calculateConsumptionRate();
break;
default:
break;
}
}
/**
*
*/
private void calculateConsumptionRate()
{
mNewFloatResidual = 0.0f;
mNewFloatsPerFrame = ((float) mSampleRate / (float) mFrameRate) *
(mSampleType == SampleType.COMPLEX ? 2.0f : 1.0f);
mFFTFloatsPerFrame = (mSampleType == SampleType.COMPLEX ?
mDFTSize.getSize() * 2 :
mDFTSize.getSize());
}
}

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 io.github.dsheirer.spectrum;
/**
* Interface for passing the output of DFT processing
*/
public interface DFTResultsListener
{
public void receive( float[] results );
}

View File

@@ -0,0 +1,25 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 io.github.dsheirer.spectrum;
public interface DFTResultsProvider
{
public void addListener( DFTResultsListener listener );
public void removeListener( DFTResultsListener listener );
}

View File

@@ -0,0 +1,46 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 io.github.dsheirer.spectrum;
public enum DFTSize
{
FFT00512( 512 ),
FFT01024( 1024 ),
FFT02048( 2048 ),
FFT04096( 4096 ),
FFT08192( 8192 ),
FFT16384( 16384 ),
FFT32768( 32768 );
private int mWidth;
private DFTSize( int width )
{
mWidth = width;
}
public int getSize()
{
return mWidth;
}
public String getLabel()
{
return String.valueOf( mWidth );
}
}

View File

@@ -0,0 +1,7 @@
package io.github.dsheirer.spectrum;
public interface IDFTWidthChangeProcessor
{
public void setDFTSize( DFTSize size );
public DFTSize getDFTSize();
}

View File

@@ -0,0 +1,913 @@
/*******************************************************************************
* 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 io.github.dsheirer.spectrum;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.ChannelEventListener;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.settings.ColorSetting;
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
import io.github.dsheirer.settings.Setting;
import io.github.dsheirer.settings.SettingChangeListener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.tuner.TunerChannel;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequencyChangeProcessor,
SettingChangeListener
{
private static final long serialVersionUID = 1L;
private final static Logger mLog = LoggerFactory.getLogger(OverlayPanel.class);
private final DecimalFormat PPM_FORMATTER = new DecimalFormat( "#.0" );
private final static RenderingHints RENDERING_HINTS = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
static
{
RENDERING_HINTS.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
}
private final static BasicStroke DASHED_STROKE = new BasicStroke(0.8f, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER, 5.0f, new float[]{2.0f, 4.0f}, 0.0f);
private static DecimalFormat CURSOR_FORMAT = new DecimalFormat("000.00000");
private long mFrequency = 0;
private int mBandwidth = 0;
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
*/
private Color mColorChannelConfig;
private Color mColorChannelConfigProcessing;
private Color mColorChannelConfigSelected;
private Color mColorSpectrumBackground;
private Color mColorSpectrumCursor;
private Color mColorSpectrumLine;
//Currently visible/displayable channels
private CopyOnWriteArrayList<Channel> mVisibleChannels = new CopyOnWriteArrayList<Channel>();
private ChannelDisplay mChannelDisplay = ChannelDisplay.ALL;
//Defines the offset at the bottom of the spectral display to account for
//the frequency labels
private double mSpectrumInset = 20.0d;
private LabelSizeManager mLabelSizeMonitor = new LabelSizeManager();
private SettingsManager mSettingsManager;
private ChannelModel mChannelModel;
/**
* Translucent overlay panel for displaying channel configurations,
* processing channels, selected channels, frequency labels and lines, and
* a cursor with a frequency readout.
*/
public OverlayPanel(SettingsManager settingsManager, ChannelModel channelModel)
{
mSettingsManager = settingsManager;
if(mSettingsManager != null)
{
mSettingsManager.addListener(this);
}
mChannelModel = channelModel;
if(mChannelModel != null)
{
mChannelModel.addListener(this);
}
addComponentListener(mLabelSizeMonitor);
//Set the background transparent, so the spectrum display can be seen
setOpaque(false);
//Fetch color settings from settings manager
setColors();
}
public void dispose()
{
if(mChannelModel != null)
{
mChannelModel.removeListener(this);
}
mChannelModel = null;
mVisibleChannels.clear();
if(mSettingsManager != null)
{
mSettingsManager.removeListener(this);
}
mSettingsManager = null;
}
/**
* Sets/changes the DFT bin size
*/
public void setDFTSize(DFTSize size)
{
mDFTSize = size;
}
public ChannelDisplay getChannelDisplay()
{
return mChannelDisplay;
}
public void setChannelDisplay(ChannelDisplay display)
{
mChannelDisplay = display;
}
public void setCursorLocation(Point point)
{
mCursorLocation = point;
repaint();
}
public void setCursorVisible(boolean visible)
{
mCursorVisible = visible;
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
* 6 64x Zoom
*
* @param zoom level, 0 - 6.
*/
public void setZoom(int zoom)
{
mZoom = zoom;
mLabelSizeMonitor.update();
}
public void setZoomWindowOffset(int offset)
{
mDFTZoomWindowOffset = offset;
}
/**
* Fetches the color settings from the settings manager
*/
private void setColors()
{
mColorChannelConfig = getColor(ColorSettingName.CHANNEL_CONFIG);
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);
}
/**
* 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 = mSettingsManager.getColorSetting(name);
return setting.getColor();
}
/**
* Monitors for setting changes. Colors can be changed by external actions
* and will automatically update in this class
*/
@Override
public void settingChanged(Setting setting)
{
if(setting instanceof ColorSetting)
{
ColorSetting colorSetting = (ColorSetting)setting;
switch(colorSetting.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_LINE:
mColorSpectrumLine = colorSetting.getColor();
break;
default:
break;
}
}
}
/**
* Renders the channel configs, lines, labels, and cursor
*/
@Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D graphics = (Graphics2D)g;
graphics.setBackground(mColorSpectrumBackground);
graphics.setRenderingHints(RENDERING_HINTS);
drawFrequencies(graphics);
drawChannels(graphics);
drawCursor(graphics);
}
/**
* Draws a cursor on the panel, whenever the mouse is hovering over the
* panel
*/
private void drawCursor(Graphics2D graphics)
{
if(mCursorVisible)
{
drawFrequencyLine(graphics, mCursorLocation.x, mColorSpectrumCursor);
String frequency = CURSOR_FORMAT.format(getFrequencyFromAxis(mCursorLocation.getX()) / 1E6D);
FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont());
Rectangle2D rect = fontMetrics.getStringBounds(frequency, graphics);
if(mCursorLocation.y > rect.getHeight())
{
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);
}
}
}
/**
* Draws the frequency lines and labels every 10kHz
*/
private void drawFrequencies(Graphics2D graphics)
{
Stroke currentStroke = graphics.getStroke();
long minFrequency = getMinDisplayFrequency();
long maxFrequency = getMaxDisplayFrequency();
//Frequency increments for label and tick spacing
int label = mLabelSizeMonitor.getLabelIncrement(graphics);
int major = mLabelSizeMonitor.getMajorTickIncrement(graphics);
int minor = mLabelSizeMonitor.getMinorTickIncrement(graphics);
//Avoid divide by zero error
if(minor == 0)
{
minor = 1;
}
//Adjust the start frequency to a multiple of the minor tick spacing
long frequency = minFrequency - (minFrequency % minor);
while(frequency < maxFrequency)
{
if(frequency % label == 0)
{
drawFrequencyLineAndLabel(graphics, frequency);
}
else if(frequency % major == 0)
{
drawTickLine(graphics, frequency, true);
}
else
{
drawTickLine(graphics, frequency, false);
}
frequency += minor;
}
}
/**
* Draws a vertical line and a corresponding frequency label at the bottom
*/
private void drawFrequencyLineAndLabel(Graphics2D graphics, long frequency)
{
double xAxis = getAxisFromFrequency(frequency);
drawFrequencyLine(graphics, xAxis, mColorSpectrumLine);
drawTickLine(graphics, frequency, false);
graphics.setColor(mColorSpectrumLine);
drawFrequencyLabel(graphics, xAxis, frequency);
}
/**
* Draws a vertical line at the xaxis
*/
private void drawTickLine(Graphics2D graphics, long frequency, boolean major)
{
graphics.setColor(mColorSpectrumLine);
double xAxis = getAxisFromFrequency(frequency);
double start = getSize().getHeight() - mSpectrumInset;
double end = start + (major ? 9.0d : 3.0d);
graphics.draw(new Line2D.Double(xAxis, start, xAxis, end));
}
/**
* Draws a vertical line at the xaxis
*/
private void drawFrequencyLine(Graphics2D graphics, double xaxis, Color color)
{
graphics.setColor(color);
graphics.draw(new Line2D.Double(xaxis, 0.0d, xaxis, getSize().getHeight() - mSpectrumInset));
}
/**
* Draws a vertical line at the xaxis
*/
private void drawChannelCenterLine(Graphics2D graphics, double xaxis)
{
double height = getSize().getHeight() - mSpectrumInset;
graphics.setColor(Color.LIGHT_GRAY);
graphics.draw(new Line2D.Double(xaxis, height * 0.65d, xaxis, height - 1.0d));
}
/**
* Draws the Automatic Frequency Control (AFC) channel center offset
*/
private void drawAFC(Graphics2D graphics, double frequencyAxis, double errorAxis, double bandwidth,
int correction, long frequency)
{
double height = getSize().getHeight() - mSpectrumInset;
double verticalAxisTop = height * 0.88d;
double verticalAxisBottom = height * 0.98d;
double halfBandwidth = bandwidth / 2.0;
double errorEdgeStart = errorAxis - halfBandwidth;
double errorEdgeStop = errorAxis + halfBandwidth;
graphics.setColor(Color.YELLOW);
//Horizontal line connecting frequency and error line
graphics.draw(new Line2D.Double(errorEdgeStart, verticalAxisBottom, errorEdgeStop, verticalAxisBottom));
//Vertical band edge lines
graphics.draw(new Line2D.Double(errorEdgeStart, verticalAxisTop, errorEdgeStart, verticalAxisBottom));
graphics.draw(new Line2D.Double(errorEdgeStop, verticalAxisTop, errorEdgeStop, verticalAxisBottom));
double ppm = (double)correction / ((double)frequency / 1E6d);
String label = "PPM " + PPM_FORMATTER.format(ppm) ;
FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont());
Rectangle2D rect = fontMetrics.getStringBounds(label, graphics);
//Only render the correction value label if the spacing is large enough
if(rect.getWidth() <= bandwidth && rect.getHeight() * 5 <= height)
{
graphics.drawString(label, (float)(errorEdgeStart + 1.0), (float)(verticalAxisBottom - 2.0));
}
}
/**
* Returns the x-axis value corresponding to the frequency
*/
private double getAxisFromFrequency(long frequency)
{
double screenWidth = (double)getSize().getWidth();
double pixelsPerBin = screenWidth / (double)mDFTSize.getSize();
double pixelOffsetToMinDisplayFrequency = pixelsPerBin * 2.0d;
//Calculate frequency offset from the min frequency
double frequencyOffset = (double)(frequency - getMinDisplayFrequency());
//Determine ratio of frequency offset to overall bandwidth
double ratio = frequencyOffset / (double)getDisplayBandwidth();
//Apply the ratio to the screen width minus 1 bin width
double screenOffset = screenWidth * ratio;
return pixelOffsetToMinDisplayFrequency + screenOffset;
}
/**
* Returns the frequency corresponding to the x-axis value using the current
* zoom level.
*/
public long getFrequencyFromAxis(double xAxis)
{
double width = getSize().getWidth();
double offset = xAxis / width;
long frequency = getMinDisplayFrequency() + Math.round((double)getDisplayBandwidth() * offset);
if(frequency > (getMaxFrequency()))
{
frequency = getMaxFrequency();
}
return frequency;
}
/**
* Draws a frequency label at the x-axis position, at the bottom of the panel
*/
private void drawFrequencyLabel(Graphics2D graphics, double xaxis, long frequency)
{
String label = mLabelSizeMonitor.format(frequency);
FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont());
Rectangle2D rect = fontMetrics.getStringBounds(label, graphics);
float xOffset = (float)rect.getWidth() / 2;
graphics.drawString(label, (float)(xaxis - xOffset), (float)(getSize().getHeight() - 2.0f));
}
/**
* Draws visible channel configs as translucent shaded frequency regions
*/
private void drawChannels(Graphics2D graphics)
{
for(Channel channel : mVisibleChannels)
{
if(mChannelDisplay == ChannelDisplay.ALL ||
(mChannelDisplay == ChannelDisplay.ENABLED && channel.getEnabled()))
{
//Choose the correct background color to use
if(channel.isSelected())
{
graphics.setColor(mColorChannelConfigSelected);
}
else if(channel.getEnabled())
{
graphics.setColor(mColorChannelConfigProcessing);
}
else
{
graphics.setColor(mColorChannelConfig);
}
TunerChannel tunerChannel = channel.getTunerChannel();
if(tunerChannel != null)
{
double xAxis = getAxisFromFrequency(tunerChannel.getFrequency());
double width = (double)(tunerChannel.getBandwidth()) / (double)getDisplayBandwidth() * getSize().getWidth();
Rectangle2D.Double box =
new Rectangle2D.Double(xAxis - (width / 2.0d), 0.0d, width, getSize().getHeight() - mSpectrumInset);
//Fill the box with the correct color
graphics.fill(box);
graphics.draw(box);
//Change to the line color to render the channel name, etc.
graphics.setColor(mColorSpectrumLine);
//Draw the labels starting at yAxis position 0
double yAxis = 0;
//Draw the system label and adjust the y-axis position
String system = channel.hasSystem() ? channel.getSystem() : " ";
yAxis += drawLabel(graphics, system, this.getFont(), xAxis, yAxis, width);
//Draw the site label and adjust the y-axis position
String site = channel.hasSite() ? channel.getSite() : " ";
yAxis += drawLabel(graphics, site, this.getFont(), xAxis, yAxis, width);
//Draw the channel label and adjust the y-axis position
yAxis += drawLabel(graphics, channel.getName(), this.getFont(), xAxis, yAxis, width);
//Draw the decoder label
drawLabel(graphics, channel.getDecodeConfiguration().getDecoderType().getShortDisplayString(),
this.getFont(), xAxis, yAxis, width);
long frequency = tunerChannel.getFrequency();
double frequencyAxis = getAxisFromFrequency(frequency);
drawChannelCenterLine(graphics, frequencyAxis);
/* Draw Automatic Frequency Control line */
int correction = channel.getChannelFrequencyCorrection();
if(correction != 0)
{
long error = frequency + correction;
drawAFC(graphics, frequencyAxis, getAxisFromFrequency(error), width, correction,
tunerChannel.getFrequency());
}
}
}
}
}
/**
* Draws a textual label at the x/y position, clipping the end of the text
* to fit within the maxwidth value.
*
* @return height of the drawn label
*/
private double drawLabel(Graphics2D graphics, String text, Font font, double x, double baseY, double maxWidth)
{
FontMetrics fontMetrics = graphics.getFontMetrics(font);
Rectangle2D label = fontMetrics.getStringBounds(text, graphics);
double offset = label.getWidth() / 2.0d;
double y = baseY + label.getHeight();
/**
* If the label is wider than the max width, left justify the text and
* clip the end of it
*/
if(offset > (maxWidth / 2.0d))
{
label.setRect(x - (maxWidth / 2.0d), y - label.getHeight(), maxWidth, label.getHeight());
graphics.setClip(label);
graphics.drawString(text, (float)(x - (maxWidth / 2.0d)), (float)y);
graphics.setClip(null);
}
else
{
graphics.drawString(text, (float)(x - offset), (float)y);
}
return label.getHeight();
}
/**
* Frequency change event handler
*/
@Override
public void frequencyChanged(FrequencyChangeEvent event)
{
switch(event.getEvent())
{
case NOTIFICATION_SAMPLE_RATE_CHANGE:
mBandwidth = event.getValue().intValue();
mLabelSizeMonitor.update();
break;
case NOTIFICATION_FREQUENCY_CHANGE:
mFrequency = event.getValue().longValue();
mLabelSizeMonitor.update();
break;
default:
break;
}
/**
* Reset the visible channels list
*/
mVisibleChannels.clear();
mVisibleChannels.addAll(mChannelModel.getChannelsInFrequencyRange(getMinFrequency(), getMaxFrequency()));
}
/**
* Channel change event handler
*/
@Override
@SuppressWarnings("incomplete-switch")
public void channelChanged(ChannelEvent event)
{
Channel channel = event.getChannel();
switch(event.getEvent())
{
case NOTIFICATION_ADD:
case NOTIFICATION_PROCESSING_START:
if(!mVisibleChannels.contains(channel) && channel.isWithin(getMinFrequency(), getMaxFrequency()))
{
mVisibleChannels.add(channel);
}
break;
case NOTIFICATION_DELETE:
mVisibleChannels.remove(channel);
break;
case NOTIFICATION_PROCESSING_STOP:
if(channel.getChannelType() == ChannelType.TRAFFIC)
{
mVisibleChannels.remove(channel);
}
break;
case NOTIFICATION_CONFIGURATION_CHANGE:
if(mVisibleChannels.contains(channel) && !channel.isWithin(getMinFrequency(), getMaxFrequency()))
{
mVisibleChannels.remove(channel);
}
if(!mVisibleChannels.contains(channel) && channel.isWithin(getMinFrequency(), getMaxFrequency()))
{
mVisibleChannels.add(channel);
}
break;
default:
break;
}
repaint();
}
public int getBandwidth()
{
return mBandwidth;
}
/**
* Currently displayed minimum frequency
*/
public long getMinFrequency()
{
return mFrequency - (mBandwidth / 2);
}
/**
* Currently displayed maximum frequency
*/
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());
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.
*/
public ArrayList<Channel> getChannelsAtFrequency(long frequency)
{
ArrayList<Channel> configs = new ArrayList<Channel>();
for(Channel config : mVisibleChannels)
{
TunerChannel channel = config.getTunerChannel();
if(channel != null && channel.getMinFrequency() <= frequency && channel.getMaxFrequency() >= frequency)
{
configs.add(config);
}
}
return configs;
}
@Override
public void settingDeleted(Setting setting)
{ /* not implemented */ }
/**
* Calculates correct spacing and format for frequency labels and major/minor
* tick lines based on current frequency, bandwidth, zoom and screen size.
*/
public class LabelSizeManager implements ComponentListener
{
private static final double LABEL_FILL_THRESHOLD = 0.5d;
private DecimalFormat mFrequencyFormat = new DecimalFormat("0.0");
private boolean mUpdateRequired = true;
private int mLabelIncrement = 1;
private int mMajorTickIncrement = 1;
private int mMinorTickIncrement = 1;
public String format(long frequency)
{
return mFrequencyFormat.format((double)frequency / 1E6D);
}
private void setPrecision(int precision)
{
if(precision < 1)
{
precision = 1;
}
if(precision > 5)
{
precision = 5;
}
mFrequencyFormat.setMinimumFractionDigits(precision);
mFrequencyFormat.setMaximumFractionDigits(precision);
}
private void update(Graphics2D graphics)
{
if(mUpdateRequired)
{
//Set maximum precision as a starting point
setPrecision(5);
FontMetrics fontMetrics =
graphics.getFontMetrics(OverlayPanel.this.getFont());
int maxLabelWidth = fontMetrics.stringWidth(format(getMaxDisplayFrequency()));
double maxLabels = ((double)OverlayPanel.this.getWidth() * LABEL_FILL_THRESHOLD) / (double)maxLabelWidth;
//Calculate the next smallest base 10 value for the major increment
int power = (int)Math.log10((double)getDisplayBandwidth() / maxLabels);
//Set the number of decimal places to display in frequency labels
int precision = 5 - power;
int start = (int)Math.pow(10.0, power + 1);
int minimum = (int)Math.pow(10.0, power);
int labelIncrement = start;
while(((double)getDisplayBandwidth() / (double)labelIncrement) < maxLabels && labelIncrement >= minimum)
{
labelIncrement /= 2;
precision++;
}
if(labelIncrement == minimum)
{
precision = 5 - power;
}
setPrecision(precision);
mLabelIncrement = labelIncrement;
mMajorTickIncrement = labelIncrement / 2;
mMinorTickIncrement = labelIncrement / 10;
mUpdateRequired = false;
}
}
/**
* Forces the display to update the label and frequency display
* calculations
*/
public void update()
{
mUpdateRequired = true;
}
public int getMajorTickIncrement(Graphics2D graphics)
{
//Check to see if a calculation update is scheduled
update(graphics);
return mMajorTickIncrement;
}
public int getMinorTickIncrement(Graphics2D graphics)
{
return mMinorTickIncrement;
}
public int getLabelIncrement(Graphics2D graphics)
{
return mLabelIncrement;
}
@Override
public void componentResized(ComponentEvent arg0)
{
update();
}
public void componentHidden(ComponentEvent arg0)
{
}
public void componentMoved(ComponentEvent arg0)
{
}
public void componentShown(ComponentEvent arg0)
{
}
}
public enum ChannelDisplay
{
ALL, ENABLED, NONE;
}
}

View File

@@ -0,0 +1,29 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 io.github.dsheirer.spectrum;
/**
* Interface to add pause action to moving displays like the spectrum and
* waterfall panels
*/
public interface Pausable
{
public void setPaused( boolean paused );
public boolean isPaused();
}

View File

@@ -0,0 +1,30 @@
package io.github.dsheirer.spectrum;
import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType;
public interface SpectralDisplayAdjuster
{
/**
* Gets the averaging value indicating the number of FFT results that are
* averaged to produce each FFT results output.
*
* @return averaging value ( 2 - 50 )
*/
public int getAveraging();
public void setAveraging( int averaging );
/**
* Sets the smoothing filter averaging window width.
*
* Valid values are odd in the range 3 - 29
*/
public int getSmoothing();
public void setSmoothing( int smoothing );
/**
* Sets the type of smoothing filter to use.
*/
public SmoothingType getSmoothingType();
public void setSmoothingType( SmoothingType type );
}

View File

@@ -0,0 +1,946 @@
/*******************************************************************************
* 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 io.github.dsheirer.spectrum;
import com.jidesoft.swing.JideSplitPane;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.ChannelUtils;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.SampleType;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
import io.github.dsheirer.settings.ColorSettingMenuItem;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.tuner.Tuner;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
import io.github.dsheirer.spectrum.OverlayPanel.ChannelDisplay;
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
import io.github.dsheirer.spectrum.converter.DFTResultsConverter;
import io.github.dsheirer.spectrum.menu.AveragingItem;
import io.github.dsheirer.spectrum.menu.DFTSizeItem;
import io.github.dsheirer.spectrum.menu.FFTWindowTypeItem;
import io.github.dsheirer.spectrum.menu.FrameRateItem;
import io.github.dsheirer.spectrum.menu.SmoothingItem;
import io.github.dsheirer.spectrum.menu.SmoothingTypeItem;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType;
import io.github.dsheirer.module.decode.DecoderType;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.MouseInputAdapter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Hashtable;
public class SpectralDisplayPanel extends JPanel implements Listener<ComplexBuffer>, IFrequencyChangeProcessor, IDFTWidthChangeProcessor
{
private static final long serialVersionUID = 1L;
private final static Logger mLog = LoggerFactory.getLogger(SpectralDisplayPanel.class);
private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat("000.00000");
public static final String FFT_SIZE_PROPERTY = "spectral.display.dft.size";
public static final int NO_ZOOM = 0;
public static final int MAX_ZOOM = 6;
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;
private OverlayPanel mOverlayPanel;
private DFTProcessor mDFTProcessor;
private DFTResultsConverter mDFTConverter;
private ChannelModel mChannelModel;
private ChannelProcessingManager mChannelProcessingManager;
private SettingsManager mSettingsManager;
private Tuner mTuner;
/**
* Spectral Display Panel provides a frequency component display with a
* historical waterfall display and a transparent overlay to show frequency,
* cursor and channel information.
*
* Mouse scrolling and zooming are supported and the waterfall display can
* be paused.
*
* Complex sample buffers are processed by a DFTProcessor and the output of
* the DFT is translated to decibels for display in the spectrum and
* waterfall components.
*/
public SpectralDisplayPanel(ChannelModel channelModel,
ChannelProcessingManager channelProcessingManager,
SettingsManager settingsManager)
{
mChannelModel = channelModel;
mChannelProcessingManager = channelProcessingManager;
mSettingsManager = settingsManager;
mSpectrumPanel = new SpectrumPanel(mSettingsManager);
mOverlayPanel = new OverlayPanel(mSettingsManager, mChannelModel);
mWaterfallPanel = new WaterfallPanel(mSettingsManager);
init();
loadSettings();
}
private void loadSettings()
{
SystemProperties properties = SystemProperties.getInstance();
String rawSize = properties.get(FFT_SIZE_PROPERTY, DFTSize.FFT04096.name());
DFTSize size = null;
if(rawSize != null)
{
try
{
size = DFTSize.valueOf(rawSize);
}
catch(Exception e)
{
//Do nothing
}
}
if(size == null)
{
size = DFTSize.FFT04096;
}
setDFTSize(size, false);
}
public void dispose()
{
/* De-register from receiving samples when the window closes */
clearTuner();
mSettingsManager = null;
mDFTProcessor.dispose();
mDFTProcessor = null;
mDFTConverter.dispose();
mDFTConverter = null;
mSpectrumPanel.dispose();
mSpectrumPanel = null;
mWaterfallPanel.dispose();
mWaterfallPanel = null;
mOverlayPanel.dispose();
mOverlayPanel = null;
mTuner = null;
}
/**
* Queues an FFT size change request. The scheduled executor will apply
* the change when it runs.
*/
public void setDFTSize(DFTSize size, boolean save)
{
mDFTProcessor.setDFTSize(size);
mOverlayPanel.setDFTSize(size);
mDFTSize = size;
if(save)
{
SystemProperties.getInstance().set(FFT_SIZE_PROPERTY, size.name());
}
setZoom(0, 0, 0);
}
public void setDFTSize(DFTSize size)
{
setDFTSize(size, true);
}
@Override
public DFTSize getDFTSize()
{
return mDFTSize;
}
public int getZoom()
{
return mZoom;
}
/**
* Sets the current zoom level which will be 2 to the power of zoom (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 - 6.
* @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 < NO_ZOOM)
{
zoom = NO_ZOOM;
}
else if(zoom > MAX_ZOOM)
{
zoom = MAX_ZOOM;
}
if(zoom != mZoom)
{
mZoom = zoom;
//Calculate the bin offset that would place the reference frequency
//at the left edge of the zoom window.
double binOffsetToFrequency = getBinOffset(frequency);
//Calculate the bin offset into the newly sized zoom window that
//would place the frequency in the same proportional window location
//that it was in the previous zoom size
double windowBinOffset = (double) getZoomWindowSizeInBins() * windowOffset;
//Set the overall offset to place the reference frequency in the
//same location in the newly zoomed window
double offset = binOffsetToFrequency - windowBinOffset;
mSpectrumPanel.setZoom(mZoom);
mOverlayPanel.setZoom(mZoom);
mWaterfallPanel.setZoom(mZoom);
setZoomWindowOffset(offset);
}
}
/**
* Sets the offset (in DFT bins) to the first bin that the zoom window displays
*/
public void setZoomWindowOffset(double offset)
{
if(offset < 0)
{
offset = 0;
}
if(offset > (mDFTSize.getSize() - getZoomWindowSizeInBins()))
{
offset = mDFTSize.getSize() - getZoomWindowSizeInBins();
}
mDFTZoomWindowOffset = (int) offset;
mSpectrumPanel.setZoomWindowOffset(mDFTZoomWindowOffset);
mOverlayPanel.setZoomWindowOffset(mDFTZoomWindowOffset);
mWaterfallPanel.setZoomWindowOffset(mDFTZoomWindowOffset);
}
/**
* Calculates the size of the current zoom window in DFT bins
*/
private int getZoomWindowSizeInBins()
{
return mDFTSize.getSize() / getZoomMultiplier();
}
public int getZoomMultiplier()
{
return (int) Math.pow(2.0, mZoom);
}
/**
* 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)
{
double offset = 0.0;
if(mOverlayPanel.containsFrequency(frequency))
{
offset = (double) mDFTSize.getSize() *
((double) (frequency - mOverlayPanel.getMinFrequency()) /
(double) mOverlayPanel.getBandwidth());
}
return offset;
}
/**
* Overrides JComponent method to return false, since we have overlapping
* panels with the spectrum and channel panels
*/
public boolean isOptimizedDrawingEnabled()
{
return false;
}
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0", "[grow]", "[grow]"));
/**
* The layered pane holds the overlapping spectrum and channel panels
* and manages the sizing of each panel with the resize listener
*/
mLayeredPanel = new JLayeredPane();
mLayeredPanel.addComponentListener(new ResizeListener());
/**
* Create a mouse adapter to handle mouse events over the spectrum
* and waterfall panels
*/
MouseEventProcessor mouser = new MouseEventProcessor();
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);
mLayeredPanel.add(mOverlayPanel, new Integer(1), 0);
//Create the waterfall
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() +
mWaterfallPanel.getPreferredSize().getHeight();
mLayeredPanel.setPreferredSize(new Dimension((int) mLayeredPanel
.getPreferredSize().getWidth(), (int) (totalHeight / 2.0d)));
mWaterfallPanel.setPreferredSize(new Dimension((int) mWaterfallPanel
.getPreferredSize().getWidth(), (int) (totalHeight / 2.0d)));
//Create the split pane to hold the layered pane and the waterfall
JideSplitPane splitPane = new JideSplitPane(JSplitPane.VERTICAL_SPLIT);
splitPane.setDividerSize(5);
splitPane.add(mLayeredPanel);
splitPane.add(mWaterfallPanel);
mScrollPane = new JScrollPane(splitPane);
add(mScrollPane, "grow");
/**
* Setup DFTProcessor to process samples and register the waterfall and
* spectrum panel to receive the processed dft results
*/
mDFTProcessor = new DFTProcessor(SampleType.COMPLEX);
mDFTConverter = new ComplexDecibelConverter();
mDFTProcessor.addConverter(mDFTConverter);
mDFTConverter.addListener((DFTResultsListener) mSpectrumPanel);
mDFTConverter.addListener((DFTResultsListener) mWaterfallPanel);
}
/**
* Receives frequency change events -- primarily from tuner components.
*/
public void frequencyChanged(FrequencyChangeEvent event)
{
mOverlayPanel.frequencyChanged(event);
mDFTProcessor.frequencyChanged(event);
}
/**
* Complex sample buffer receive method
*/
@Override
public void receive(ComplexBuffer sampleBuffer)
{
mDFTProcessor.receive(sampleBuffer);
}
/**
* Responds to tuner event by deregistering from the current
* complex sample buffer source and registering with the tuner argument.
*/
public void showTuner(Tuner tuner)
{
clearTuner();
mDFTProcessor.clearBuffer();
mTuner = tuner;
if(mTuner != null)
{
//Register to receive frequency change events
mTuner.getTunerController().addListener(this);
//Register the dft processor to receive samples from the tuner
mTuner.addListener((Listener<ComplexBuffer>) mDFTProcessor);
mSpectrumPanel.setSampleSize(mTuner.getSampleSize());
//Fire frequency and sample rate change events so that the spectrum
//and overlay panels can synchronize
frequencyChanged(new FrequencyChangeEvent(
Event.NOTIFICATION_FREQUENCY_CHANGE,
mTuner.getTunerController().getFrequency()));
frequencyChanged(new FrequencyChangeEvent(
Event.NOTIFICATION_SAMPLE_RATE_CHANGE,
mTuner.getTunerController().getSampleRate()));
}
}
/**
* Tuner de-selection cleanup method
*/
public void clearTuner()
{
if(mTuner != null)
{
//Deregister for frequency change events from the tuner
mTuner.getTunerController().removeListener(this);
//Deregister the dft processor from receiving samples
mTuner.removeListener((Listener<ComplexBuffer>) mDFTProcessor);
mTuner = null;
}
}
/**
* Monitors the sizing of the layered pane and resizes the spectrum and
* channel panels whenever the layered pane is resized
*/
public class ResizeListener implements ComponentListener
{
@Override
public void componentResized(ComponentEvent e)
{
Component c = e.getComponent();
mSpectrumPanel.setBounds(0, 0, c.getWidth(), c.getHeight());
mOverlayPanel.setBounds(0, 0, c.getWidth(), c.getHeight());
}
@Override
public void componentHidden(ComponentEvent arg0)
{
}
@Override
public void componentMoved(ComponentEvent arg0)
{
}
@Override
public void componentShown(ComponentEvent arg0)
{
}
}
/**
* Mouse event handler for the spectral display panel.
*/
public class MouseEventProcessor extends MouseInputAdapter
{
private int mDFTZoomWindowOffsetAtDragStart = 0;
private int mDragStartX = 0;
private double mPixelsPerBin;
public MouseEventProcessor()
{
}
@Override
public void mouseWheelMoved(MouseWheelEvent e)
{
int zoom = mZoom - e.getWheelRotation();
long frequency = mOverlayPanel.getFrequencyFromAxis(e.getX());
double windowOffset = (double) e.getX() / (double) getWidth();
setZoom(zoom, frequency, windowOffset);
}
@Override
public void mouseMoved(MouseEvent event)
{
update(event);
}
@Override
public void mouseDragged(MouseEvent event)
{
update(event);
int dragDistance = mDragStartX - event.getX();
double binDistance = (double) dragDistance / mPixelsPerBin;
int offset = (int) (mDFTZoomWindowOffsetAtDragStart + binDistance);
if(offset < 0)
{
offset = 0;
}
int maxOffset = mDFTSize.getSize() - (mDFTSize.getSize() / getZoomMultiplier());
if(offset > maxOffset)
{
offset = maxOffset;
}
setZoomWindowOffset(offset);
}
@Override
public void mousePressed(MouseEvent e)
{
mDragStartX = e.getX();
mDFTZoomWindowOffsetAtDragStart = mDFTZoomWindowOffset;
mPixelsPerBin = (double) getWidth() /
((double) (mDFTSize.getSize()) /
(double) getZoomMultiplier());
}
/**
* Updates the cursor display while the mouse is performing actions
*/
private void update(MouseEvent event)
{
if(event.getComponent() == mOverlayPanel)
{
mOverlayPanel.setCursorLocation(event.getPoint());
}
else
{
mWaterfallPanel.setCursorLocation(event.getPoint());
mWaterfallPanel.setCursorFrequency(
mOverlayPanel.getFrequencyFromAxis(event.getPoint().x));
}
}
@Override
public void mouseEntered(MouseEvent e)
{
if(e.getComponent() == mOverlayPanel)
{
mOverlayPanel.setCursorVisible(true);
}
else
{
mWaterfallPanel.setCursorVisible(true);
}
}
@Override
public void mouseExited(MouseEvent e)
{
mOverlayPanel.setCursorVisible(false);
mWaterfallPanel.setCursorVisible(false);
}
/**
* Displays the context menu.
*/
@Override
public void mouseClicked(MouseEvent event)
{
if(SwingUtilities.isRightMouseButton(event))
{
JPopupMenu contextMenu = new JPopupMenu();
if(event.getComponent() == mWaterfallPanel)
{
contextMenu.add(new PauseItem(mWaterfallPanel, "Pause"));
}
long frequency =
mOverlayPanel.getFrequencyFromAxis(event.getX());
if(event.getComponent() == mOverlayPanel)
{
ArrayList<Channel> channels =
mOverlayPanel.getChannelsAtFrequency(frequency);
for(Channel channel : channels)
{
JMenu channelMenu = ChannelUtils.getContextMenu(mChannelModel,
mChannelProcessingManager, channel,
SpectralDisplayPanel.this);
if(channelMenu != null)
{
contextMenu.add(channelMenu);
}
}
if(!channels.isEmpty())
{
contextMenu.add(new JSeparator());
}
}
// JMenu frequencyMenu = new JMenu(
// sCURSOR_FORMAT.format((float) frequency / 1000000.0f));
//
// JMenu decoderMenu = new JMenu("Add Decoder");
//
// for(DecoderType type : DecoderType.getPrimaryDecoders())
// {
// decoderMenu.add(new DecoderItem(type, frequency));
// }
//
// frequencyMenu.add(decoderMenu);
//
// contextMenu.add(frequencyMenu);
//
// contextMenu.add(new JSeparator());
/**
* Color Menus
*/
JMenu colorMenu = new JMenu("Color");
colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
ColorSettingName.CHANNEL_CONFIG));
colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
ColorSettingName.CHANNEL_CONFIG_PROCESSING));
colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
ColorSettingName.CHANNEL_CONFIG_SELECTED));
colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
ColorSettingName.SPECTRUM_CURSOR));
colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
ColorSettingName.SPECTRUM_LINE));
colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
ColorSettingName.SPECTRUM_BACKGROUND));
colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
ColorSettingName.SPECTRUM_GRADIENT_BOTTOM));
colorMenu.add(new ColorSettingMenuItem(mSettingsManager,
ColorSettingName.SPECTRUM_GRADIENT_TOP));
contextMenu.add(colorMenu);
/**
* Display items: fft and frame rate
*/
JMenu displayMenu = new JMenu("Display");
contextMenu.add(displayMenu);
if(event.getComponent() != mWaterfallPanel)
{
/**
* Averaging menu
*/
JMenu averagingMenu = new JMenu("Averaging");
averagingMenu.add(
new AveragingItem(mSpectrumPanel, 4));
displayMenu.add(averagingMenu);
/**
* Channel Display setting menu
*/
JMenu channelDisplayMenu = new JMenu("Channel");
channelDisplayMenu.add(new ChannelDisplayItem(
mOverlayPanel, ChannelDisplay.ALL));
channelDisplayMenu.add(new ChannelDisplayItem(
mOverlayPanel, ChannelDisplay.ENABLED));
channelDisplayMenu.add(new ChannelDisplayItem(
mOverlayPanel, ChannelDisplay.NONE));
displayMenu.add(channelDisplayMenu);
}
/**
* FFT width
*/
JMenu fftWidthMenu = new JMenu("FFT Width");
displayMenu.add(fftWidthMenu);
for(DFTSize width : DFTSize.values())
{
fftWidthMenu.add(new DFTSizeItem(SpectralDisplayPanel.this, width));
}
/**
* DFT Processor Frame Rate
*/
JMenu frameRateMenu = new JMenu("Frame Rate");
displayMenu.add(frameRateMenu);
frameRateMenu.add(new FrameRateItem(mDFTProcessor, 14));
frameRateMenu.add(new FrameRateItem(mDFTProcessor, 16));
frameRateMenu.add(new FrameRateItem(mDFTProcessor, 18));
frameRateMenu.add(new FrameRateItem(mDFTProcessor, 20));
frameRateMenu.add(new FrameRateItem(mDFTProcessor, 25));
frameRateMenu.add(new FrameRateItem(mDFTProcessor, 30));
frameRateMenu.add(new FrameRateItem(mDFTProcessor, 40));
frameRateMenu.add(new FrameRateItem(mDFTProcessor, 50));
/**
* FFT Window Type
*/
JMenu fftWindowType = new JMenu("Window Type");
displayMenu.add(fftWindowType);
for(WindowType type : WindowType.values())
{
fftWindowType.add(
new FFTWindowTypeItem(mDFTProcessor, type));
}
if(event.getComponent() != mWaterfallPanel)
{
/**
* Smoothing menu
*/
JMenu smoothingMenu = new JMenu("Smoothing");
if(mSpectrumPanel.getSmoothingType() != SmoothingType.NONE)
{
smoothingMenu.add(new SmoothingItem(mSpectrumPanel, 5));
smoothingMenu.add(new JSeparator());
}
smoothingMenu.add(new SmoothingTypeItem(mSpectrumPanel, SmoothingType.GAUSSIAN));
smoothingMenu.add(new SmoothingTypeItem(mSpectrumPanel, SmoothingType.TRIANGLE));
smoothingMenu.add(new SmoothingTypeItem(mSpectrumPanel, SmoothingType.RECTANGLE));
smoothingMenu.add(new SmoothingTypeItem(mSpectrumPanel, SmoothingType.NONE));
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)
{
contextMenu.show(mOverlayPanel,
event.getX(),
event.getY());
}
else
{
contextMenu.show(mWaterfallPanel,
event.getX(),
event.getY());
}
}
}
}
}
public class PauseItem extends JCheckBoxMenuItem
{
private static final long serialVersionUID = 1L;
private Pausable mPausable;
public PauseItem(Pausable pausable, String label)
{
super(label);
final boolean paused = pausable.isPaused();
setSelected(paused);
mPausable = pausable;
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
mPausable.setPaused(!paused);
}
});
}
});
}
}
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<Integer,JComponent> 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;
private OverlayPanel mOverlayPanel;
private ChannelDisplay mChannelDisplay;
public ChannelDisplayItem(OverlayPanel panel, ChannelDisplay display)
{
super(display.name());
mOverlayPanel = panel;
mChannelDisplay = display;
setSelected(mOverlayPanel.getChannelDisplay() == mChannelDisplay);
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
mOverlayPanel.setChannelDisplay(mChannelDisplay);
}
});
}
});
}
}
/**
* Context menu item to provide one-click access to starting a channel
* processing with the selected decoder
*/
public class DecoderItem extends JMenuItem
{
private static final long serialVersionUID = 1L;
private ChannelModel mChannelModel;
private long mFrequency;
private DecoderType mDecoder;
public DecoderItem(DecoderType type, long frequency)
{
super(type.getDisplayString());
mFrequency = frequency;
mDecoder = type;
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mChannelModel.createChannel(mDecoder, mFrequency);
}
});
}
}
}

View File

@@ -0,0 +1,84 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 io.github.dsheirer.spectrum;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.tuner.Tuner;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
public class SpectrumFrame extends JFrame implements WindowListener
{
private static final long serialVersionUID = 1L;
private SpectralDisplayPanel mSpectralDisplayPanel;
public SpectrumFrame( ChannelModel channelModel,
ChannelProcessingManager channelProcessingManager,
SettingsManager settingsManager,
Tuner tuner )
{
setTitle( "SDRTRunk [" + tuner.getName() + "]" );
setBounds( 100, 100, 1280, 600 );
setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
setLayout( new MigLayout( "insets 0 0 0 0", "[grow]", "[grow]") );
mSpectralDisplayPanel = new SpectralDisplayPanel( channelModel,
channelProcessingManager, settingsManager );
mSpectralDisplayPanel.showTuner( tuner );
add( mSpectralDisplayPanel, "grow" );
/* Register a shutdown listener */
this.addWindowListener( this );
EventQueue.invokeLater( new Runnable()
{
public void run()
{
setVisible( true );
}
} );
}
@Override
public void windowClosed( WindowEvent arg0 )
{
mSpectralDisplayPanel.dispose();
}
@Override
public void windowActivated( WindowEvent arg0 ) {}
@Override
public void windowClosing( WindowEvent arg0 ) {}
@Override
public void windowDeactivated( WindowEvent arg0 ) {}
@Override
public void windowDeiconified( WindowEvent arg0 ) {}
@Override
public void windowIconified( WindowEvent arg0 ) {}
@Override
public void windowOpened( WindowEvent arg0 ) {}
}

View File

@@ -0,0 +1,483 @@
/*******************************************************************************
* 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 io.github.dsheirer.spectrum;
import io.github.dsheirer.settings.ColorSetting;
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
import io.github.dsheirer.settings.Setting;
import io.github.dsheirer.settings.SettingChangeListener;
import io.github.dsheirer.settings.SettingsManager;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.dsheirer.dsp.filter.smoothing.GaussianSmoothingFilter;
import io.github.dsheirer.dsp.filter.smoothing.NoSmoothingFilter;
import io.github.dsheirer.dsp.filter.smoothing.RectangularSmoothingFilter;
import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter;
import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType;
import io.github.dsheirer.dsp.filter.smoothing.TriangularSmoothingFilter;
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);
}
}
}

View File

@@ -0,0 +1,94 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 io.github.dsheirer.spectrum;
import java.awt.image.IndexColorModel;
public class WaterfallColorModel
{
public static IndexColorModel getDefaultColorModel()
{
int bitDepth = 8;
int indexSize = 256;
byte[] red = new byte[ indexSize ];
byte[] green = new byte[ indexSize ];
byte[] blue = new byte[ indexSize ];
//Background noise
for( int x = 0; x < 16; x++ ) //Blue
{
red[ x ] = (byte)0;
green[ x ] = (byte)0;
blue[ x ] = (byte)127;
}
for( int x = 16; x < 32; x++ ) //Blue
{
red[ x ] = (byte)0;
green[ x ] = (byte)0;
blue[ x ] = (byte)191;
}
int r = 0;
int g = 0;
int b = 191;
for( int x = 32; x < 60; x++ )
{
red[ x ] = (byte)r;
r += 9;
green[ x ] = (byte)g;
g += 9;
blue[ x ] = (byte)b;
b -= 6;
}
r = 255;
g = 255;
b = 0;
for( int x = 60; x < 188; x++ ) //Yellow
{
red[ x ] = (byte)r;
green[ x ] = (byte)g;
g -= 2;
blue[ x ] = (byte)b;
}
for( int x = 188; x < 256; x++ ) //Red
{
red[ x ] = (byte)255;
green[ x ] = (byte)0;
blue[ x ] = (byte)0;
}
return new IndexColorModel( bitDepth, indexSize, red, green, blue );
}
}

View File

@@ -0,0 +1,431 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.spectrum;
import io.github.dsheirer.settings.ColorSetting;
import io.github.dsheirer.settings.ColorSetting.ColorSettingName;
import io.github.dsheirer.settings.Setting;
import io.github.dsheirer.settings.SettingChangeListener;
import io.github.dsheirer.settings.SettingsManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Line2D;
import java.awt.image.ColorModel;
import java.awt.image.MemoryImageSource;
import java.text.DecimalFormat;
public class WaterfallPanel extends JPanel implements DFTResultsListener,
Pausable,
SettingChangeListener
{
private static final long serialVersionUID = 1L;
private final static Logger mLog =
LoggerFactory.getLogger( WaterfallPanel.class );
private static DecimalFormat CURSOR_FORMAT = new DecimalFormat( "0.00000" );
private static final String PAUSED = "PAUSED";
private byte[] mPixels;
private byte[] mPausedPixels;
private int mDFTSize = 4096;
private int mImageHeight = 700;
private MemoryImageSource mMemoryImageSource;
private ColorModel mColorModel = WaterfallColorModel.getDefaultColorModel();
private Color mColorSpectrumCursor;
private Image mWaterfallImage;
private Point mCursorLocation = new Point( 0, 0 );
private boolean mCursorVisible = false;
private long mCursorFrequency = 0;
private boolean mPaused = false;
private int mZoom = 0;
private int mDFTZoomWindowOffset = 0;
private SettingsManager mSettingsManager;
/**
* Displays a scrolling window of multiple DFT frequency bin outputs over
* time. Maps DFT frequency bin decibel values into a 256 bucket color map
* for display.
*
* @param settingsManager
*/
public WaterfallPanel( SettingsManager settingsManager )
{
super();
mSettingsManager = settingsManager;
mSettingsManager.addListener( this );
mColorSpectrumCursor = getColor( ColorSettingName.SPECTRUM_CURSOR );
reset();
}
/**
* Prepares this instance for disposal
*/
public void dispose()
{
if( mSettingsManager != null )
{
mSettingsManager.removeListener( this );
}
mSettingsManager = null;
mMemoryImageSource = null;
}
/**
* Resets the memory image source and byte backing array when the DFT point
* size has changed
*/
private void reset()
{
mPixels = new byte[ mDFTSize * mImageHeight ];
mMemoryImageSource = new MemoryImageSource( mDFTSize,
mImageHeight,
mColorModel,
mPixels,
0,
mDFTSize );
mMemoryImageSource.setAnimated( true );
mWaterfallImage = createImage( mMemoryImageSource );
repaint();
}
/**
* Pausable interface - pauses updates to the waterfall
*/
public void setPaused( boolean paused )
{
if( paused )
{
mPausedPixels = mPixels.clone();
}
mPaused = paused;
repaint();
}
/**
* Returns current pause state
* @return true if paused, false otherwise
*/
public boolean isPaused()
{
return mPaused;
}
/**
* 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 - 6.
* @param offset into the DFT bins for display
*/
public void setZoom( int zoom )
{
mZoom = zoom;
}
/**
* Multiplier for the current zoom level
*/
private int getZoomMultiplier()
{
return (int)Math.pow( 2.0, mZoom );
}
/**
* Sets the zoom window offset from zero
* @param offset in DFT bins
*/
public void setZoomWindowOffset( int offset )
{
mDFTZoomWindowOffset = offset;
}
/**
* 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 = mSettingsManager.getColorSetting( name );
return setting.getColor();
}
/**
* Monitors for setting changes. Colors can be changed by external actions
* and will automatically update in this class
*/
@Override
public void settingChanged( Setting setting )
{
if( setting instanceof ColorSetting )
{
ColorSetting colorSetting = (ColorSetting)setting;
if( ( (ColorSetting) setting ).getColorSettingName() ==
ColorSettingName.SPECTRUM_CURSOR )
{
mColorSpectrumCursor = colorSetting.getColor();
}
}
}
@Override
public void settingDeleted( Setting setting ) { /* Not implemented */ }
/**
* Sets the display location of the cursor. Cursor location monitoring is
* handled external to this class.
*
* @param point
*/
public void setCursorLocation( Point point )
{
mCursorLocation = point;
repaint();
}
/**
* Sets the current cursor display frequency. Cursor location frequency
* monitoring is handled external to this class.
*
* @param frequency
*/
public void setCursorFrequency( long frequency )
{
mCursorFrequency = frequency;
}
/**
* Toggles the visibility of the cursor
* @param visible
*/
public void setCursorVisible( boolean visible )
{
mCursorVisible = visible;
repaint();
}
/**
* Calculates the x-axis pixel offset from zero where to start rendering the
* waterfall image
*
* @param multiplier - current zoom multiplier
* @return x-axis pixel offset
*/
private double getPixelOffset( int multiplier )
{
double offset = 0;
if( mZoom != 0 )
{
double binPixelWidth = getBinPixelWidth( multiplier );
offset = -binPixelWidth * (double)( mDFTZoomWindowOffset );
}
return offset;
}
private double getBinPixelWidth( int multiplier )
{
return ( (double)getWidth() * (double)multiplier ) / (double)mDFTSize;
}
/**
* Renders the screen at each refresh
*/
public void paintComponent( Graphics g )
{
super.paintComponent( g );
int multiplier = getZoomMultiplier();
double binPixelWidth = getBinPixelWidth( multiplier );
int offset = (int)( getPixelOffset( multiplier ) - binPixelWidth );
g.drawImage( mWaterfallImage,
offset,
0,
( getWidth() * multiplier ) + (int)binPixelWidth,
mImageHeight,
this );
Graphics2D graphics = (Graphics2D) g;
graphics.setColor( mColorSpectrumCursor );
if( mCursorVisible )
{
graphics.draw( new Line2D.Float( mCursorLocation.x,
0,
mCursorLocation.x,
(float)(getSize().getHeight() ) ) );
String frequency = CURSOR_FORMAT.format( mCursorFrequency / 1000000.0D );
graphics.drawString( frequency ,
mCursorLocation.x + 5,
mCursorLocation.y );
}
if( mPaused )
{
graphics.drawString( PAUSED, 20, 20 );
}
paintZoomIndicator( graphics );
graphics.dispose();
}
/**
* When zoom level is greater than zero, paints a small indicator at the
* bottom center of the screen showing the location of the zoom window
* within the overall DFT results window
*/
private void paintZoomIndicator( Graphics2D graphics )
{
if( mZoom != 0 )
{
int width = getWidth() / 4;
int x = ( getWidth() / 2 ) - ( width / 2 );
//Draw the outer window
graphics.drawRect( x, getHeight() - 12, width, 10 );
int zoomWidth = width / getZoomMultiplier();
int windowOffset = 0;
if( mDFTZoomWindowOffset != 0 )
{
windowOffset = (int)( ( (double)mDFTZoomWindowOffset /
(double)mDFTSize ) * width );
}
//Draw the zoom window
graphics.fillRect( x + windowOffset, getHeight() - 12, zoomWidth, 10 );
//Draw the zoom text
graphics.drawString( "Zoom: " + getZoomMultiplier() + "x",
x + width + 3, getHeight() - 2 );
}
}
/**
* Implements the DFT results listener interface method. This is the
* primary method for receiving new frequency bin results.
*/
@Override
public void receive( float[] update )
{
//If our FFT size changes, reset our pixel map and image source
if( mDFTSize != update.length )
{
mDFTSize = update.length;
reset();
}
//Move the pixels down a row to make room for the new results
System.arraycopy( mPixels, 0,
mPixels, mDFTSize, mPixels.length - mDFTSize );
/**
* Find the average value and scale the display to it
*/
double sum = 0.0d;
for( int x = 0; x < update.length - 1; x++ )
{
sum += update[ x ];
}
float average = (float)( sum / (double)update.length - 1 );
float scale = 256.0f / average;
for( int x = 0; x < update.length - 1; x++ )
{
float value = ( average - update[ x ] ) * scale;
if( value < 0 )
{
mPixels[ x ] = 0;
}
else if( value > 255 )
{
mPixels[ x ] = (byte)255;
}
else
{
mPixels[ x ] = (byte)value;
}
}
//Task the swing event thread to update the display
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
if( mMemoryImageSource != null )
{
if( mPaused )
{
mMemoryImageSource.newPixels( mPausedPixels, mColorModel, 0, mDFTSize );
}
else
{
mMemoryImageSource.newPixels( mPixels, mColorModel, 0, mDFTSize );
}
}
}
} );
}
}

View File

@@ -0,0 +1,109 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 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 <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.spectrum.converter;
/**
* Converts complex DFT output to scaled dB values with a maximum amplitude of
* 0 dB and all values scaled to the minimum dB value which is:
*
* Max: 0dB
* Min: 20 * log10( 1.0 / ( 2.0 ^ ( bit depth - 1) ) = scaling factor
*
* Notes:
*
* The magnitude of each DFT output bin must be scaled by the number of DFT
* points to normalize the output to a range of ( 0 to 1.0 ). DFT bin magnitude
* can be calculated as:
*
* 1) sqrt( bin[ x ] * bin[ x ] + bin[ x + 1 ] * bin[ x + 1] ) / DFTSize or,
* 2) ( bin[ x ] * bin[ x ] + bin[ x + 1 ] * bin[ x + 1] ) / ( DFTSize * DFTSize )
*
* Convert each scaled DFT bin magnitude to decibels using this formula:
*
* 20 * log10( scaledBinValue )
*
* Max Value: 20 * log10( 1.0 ) = 0 dB
*
* When plotting, these dB bin values should be scaled according to the maximum
* dynamic range supported by the source. Maximum dynamic range for bit depth
* is calculated as:
*
* 20 * log10( 1 / ( 2 ^ ( bit depth - 1 ) )
*
* A source that provides 12-bit samples can produce values in the range of
* ( 0 to 4096 ) or -2048 to 2047. The smallest observable value would then
* be 1 / 2048.
*
* So, the smallest decibel value of a source producing 12 bit samples is:
*
* 20 * log10( 1 / 2048 ) = -66.23 dB
*
* and the dynamic range is: (-66.23 to 0.0 dB)
*
*/
public class ComplexDecibelConverter extends DFTResultsConverter
{
/**
* Converts the output of the JTransforms FloatFFT_1D.complexForward()
* calculation into the power spectrum in decibels, normalized to the
* sample bit depth.
*/
public ComplexDecibelConverter()
{
}
@Override
public void receive( float[] results )
{
int halfResults = results.length / 2;
// float dftBinSizeScalor = 1.0f / (float)Math.pow( halfResults, 2.0 );
float dftBinSizeScalor = 1.0f / (float)halfResults;
float[] processed = new float[ halfResults ];
int middle = processed.length / 2;
for( int x = 0; x < results.length; x += 2 )
{
//Calculate the magnitude squared (power) value from each bin's real
//and imaginary value and scale it to the DFT bin size squared.
//Convert the scaled value to decibels.
float decibels = 10.0f * (float)Math.log10(
( ( results[ x ] * results[ x ] ) +
( results[ x + 1 ] * results[ x + 1 ] ) ) * dftBinSizeScalor );
// We have to swap the upper and lower halves of the JTransforms
// DFT results for correct display
int index = x / 2;
if( index >= middle )
{
processed[ index - middle ] = decibels;
}
else
{
processed[ index + middle ] = decibels;
}
}
dispatch( processed );
}
}

View File

@@ -0,0 +1,46 @@
package io.github.dsheirer.spectrum.converter;
import io.github.dsheirer.spectrum.DFTResultsListener;
import io.github.dsheirer.spectrum.DFTResultsProvider;
import java.util.concurrent.CopyOnWriteArrayList;
public abstract class DFTResultsConverter
implements DFTResultsListener, DFTResultsProvider
{
private CopyOnWriteArrayList<DFTResultsListener> mListeners =
new CopyOnWriteArrayList<DFTResultsListener>();
/**
* DFT Results Converter - for converting the output of the JTransforms
* FFT library real and complex forward results
*/
public DFTResultsConverter()
{
}
public void dispose()
{
mListeners.clear();
}
@Override
public void addListener( DFTResultsListener listener )
{
mListeners.add( listener );
}
@Override
public void removeListener( DFTResultsListener listener )
{
mListeners.remove( listener );
}
protected void dispatch( float[] results )
{
for( DFTResultsListener listener: mListeners )
{
listener.receive( results );
}
}
}

View File

@@ -0,0 +1,41 @@
package io.github.dsheirer.spectrum.converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RealDecibelConverter extends DFTResultsConverter
{
private static final Logger mLog = LoggerFactory.getLogger( RealDecibelConverter.class );
public RealDecibelConverter()
{
}
/**
* Converts the output of the JTransforms FloatFFT_1D.realForward()
* calculation into a normalized power spectrum in decibels, per description
* in Lyons, Understanding Digital Signal Processing, 3e, page 141.
*
* Note: this is only calculating the lower half of the spectrum
*/
@Override
public void receive( float[] results )
{
float dftBinSizeScalor = 1.0f / (float)results.length;
float[] processed = new float[ results.length / 4 ];
int index = 0;
for( int x = 0; x < processed.length; x ++ )
{
index = x * 2;
processed[ x ] = 20.0f * (float)Math.log10(
( ( results[ index ] * results[ index ] ) +
( results[ index + 1 ] * results[ index + 1 ] ) ) * dftBinSizeScalor );
}
dispatch( processed );
}
}

View File

@@ -0,0 +1,57 @@
package io.github.dsheirer.spectrum.menu;
import io.github.dsheirer.spectrum.SpectralDisplayAdjuster;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class AveragingItem extends JSlider implements ChangeListener
{
private static final long serialVersionUID = 1L;
private SpectralDisplayAdjuster mAdjuster;
private int mDefaultValue;
public AveragingItem( SpectralDisplayAdjuster adjuster, int defaultValue )
{
super( JSlider.HORIZONTAL, 1, 20, adjuster.getAveraging() );
mDefaultValue = defaultValue;
mAdjuster = adjuster;
setMajorTickSpacing( 5 );
setMinorTickSpacing( 1 );
setPaintTicks( true );
setPaintLabels( true );
addChangeListener( this );
addMouseListener( new MouseListener()
{
@Override
public void mouseClicked( MouseEvent event )
{
if( event.getClickCount() == 2 )
{
AveragingItem.this.setValue( mDefaultValue );
}
}
public void mouseReleased( MouseEvent arg0 ) {}
public void mousePressed( MouseEvent arg0 ) {}
public void mouseExited( MouseEvent arg0 ) {}
public void mouseEntered( MouseEvent arg0 ) {}
} );
}
@Override
public void stateChanged( ChangeEvent event )
{
int value = ((JSlider)event.getSource()).getValue();
mAdjuster.setAveraging( value );
}
}

View File

@@ -0,0 +1,38 @@
package io.github.dsheirer.spectrum.menu;
import io.github.dsheirer.spectrum.DFTSize;
import io.github.dsheirer.spectrum.IDFTWidthChangeProcessor;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class DFTSizeItem extends JCheckBoxMenuItem
{
private static final long serialVersionUID = 1L;
private IDFTWidthChangeProcessor mDFTProcessor;
private DFTSize mDFTSize;
public DFTSizeItem( IDFTWidthChangeProcessor processor, DFTSize size )
{
super( size.getLabel() );
mDFTProcessor = processor;
mDFTSize = size;
if( processor.getDFTSize() == mDFTSize )
{
setSelected( true );
}
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent arg0 )
{
mDFTProcessor.setDFTSize( mDFTSize );
}
} );
}
}

View File

@@ -0,0 +1,39 @@
package io.github.dsheirer.spectrum.menu;
import io.github.dsheirer.spectrum.DFTProcessor;
import io.github.dsheirer.dsp.filter.Window.WindowType;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class FFTWindowTypeItem extends JCheckBoxMenuItem
{
private static final long serialVersionUID = 1L;
private DFTProcessor mDFTProcessor;
private WindowType mWindowType;
public FFTWindowTypeItem( DFTProcessor processor, WindowType windowType )
{
super( windowType.toString() );
mDFTProcessor = processor;
mWindowType = windowType;
if( processor.getWindowType() == mWindowType )
{
setSelected( true );
}
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent arg0 )
{
mDFTProcessor.setWindowType( mWindowType );
}
} );
}
}

View File

@@ -0,0 +1,38 @@
package io.github.dsheirer.spectrum.menu;
import io.github.dsheirer.spectrum.DFTProcessor;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class FrameRateItem extends JCheckBoxMenuItem
{
private static final long serialVersionUID = 1L;
private DFTProcessor mDFTProcessor;
private int mFrameRate;
public FrameRateItem( DFTProcessor processor, int frameRate )
{
super( String.valueOf( frameRate ) );
mDFTProcessor = processor;
mFrameRate = frameRate;
if( processor.getFrameRate() == mFrameRate )
{
setSelected( true );
}
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent arg0 )
{
mDFTProcessor.setFrameRate( mFrameRate );
}
} );
}
}

View File

@@ -0,0 +1,75 @@
package io.github.dsheirer.spectrum.menu;
import io.github.dsheirer.spectrum.SpectralDisplayAdjuster;
import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Hashtable;
public class SmoothingItem extends JSlider implements ChangeListener
{
private static final long serialVersionUID = 1L;
private SpectralDisplayAdjuster mAdjuster;
private int mDefaultValue;
public SmoothingItem( SpectralDisplayAdjuster adjuster, int defaultValue )
{
super( JSlider.HORIZONTAL,
SmoothingFilter.SMOOTHING_MINIMUM,
SmoothingFilter.SMOOTHING_MAXIMUM,
adjuster.getSmoothing() );
mDefaultValue = defaultValue;
mAdjuster = adjuster;
setSnapToTicks( true );
setMajorTickSpacing( 6 );
setMinorTickSpacing( 2 );
setPaintTicks( true );
setPaintLabels( true );
Hashtable<Integer,JLabel> labels = new Hashtable<>();
labels.put( new Integer( 3 ), new JLabel( "3" ) );
labels.put( new Integer( 9 ), new JLabel( "9" ) );
labels.put( new Integer( 15 ), new JLabel( "15" ) );
labels.put( new Integer( 21 ), new JLabel( "21" ) );
labels.put( new Integer( 27 ), new JLabel( "27" ) );
setLabelTable( labels );
addChangeListener( this );
addMouseListener( new MouseListener()
{
@Override
public void mouseClicked( MouseEvent event )
{
if( event.getClickCount() == 2 )
{
SmoothingItem.this.setValue( mDefaultValue );
}
}
public void mouseReleased( MouseEvent arg0 ) {}
public void mousePressed( MouseEvent arg0 ) {}
public void mouseExited( MouseEvent arg0 ) {}
public void mouseEntered( MouseEvent arg0 ) {}
} );
}
@Override
public void stateChanged( ChangeEvent event )
{
int value = ((JSlider)event.getSource()).getValue();
if( value % 2 == 1 )
{
mAdjuster.setSmoothing( value );
}
}
}

View File

@@ -0,0 +1,35 @@
package io.github.dsheirer.spectrum.menu;
import io.github.dsheirer.spectrum.SpectralDisplayAdjuster;
import io.github.dsheirer.dsp.filter.smoothing.SmoothingFilter.SmoothingType;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class SmoothingTypeItem extends JCheckBoxMenuItem
{
private static final long serialVersionUID = 1L;
private SpectralDisplayAdjuster mAdjuster;
private SmoothingType mSmoothingType;
public SmoothingTypeItem( SpectralDisplayAdjuster adjuster, SmoothingType type )
{
super( type.name() );
mAdjuster = adjuster;
mSmoothingType = type;
setSelected( mAdjuster.getSmoothingType() == type );
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
mAdjuster.setSmoothingType( mSmoothingType );
}
} );
}
}