Channel Spectral Display

- Adds Channel Spectrum tab to display FFT of the audio output of the currently selected channel
 - Upgrades to JTransforms 3.0 FFT library
 - Adds three new context menus to main and channel spectral displays:
   -- Baseline - affects the vertical rendering baseline of the FFT
   -- Amplification - affects the vertical height of each FFT bin
   -- Averaging - affects how many FFTs are averaged to produce the display
This commit is contained in:
dennis.sheirer
2014-09-06 14:58:05 +00:00
parent 5ddf538e2a
commit 4e0fd9b7b0
44 changed files with 1986 additions and 549 deletions

View File

@@ -0,0 +1,264 @@
/*******************************************************************************
* 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 spectrum;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import javax.swing.JPanel;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting;
import settings.SettingChangeListener;
import controller.ResourceManager;
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 ResourceManager mResourceManager;
/**
* Translucent overlay panel for displaying channel configurations,
* processing channels, selected channels, frequency labels and lines, and
* a cursor with a frequency readout.
*/
public ChannelOverlayPanel( ResourceManager resourceManager )
{
mResourceManager = resourceManager;
//Set the background transparent, so the spectrum display can be seen
setOpaque( false );
//Fetch color settings from settings manager
setColors();
}
public void dispose()
{
mResourceManager = 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 =
mResourceManager.getSettingsManager()
.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;
}
}
}
/**
* 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,520 @@
package spectrum;
import java.awt.Component;
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.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JLayeredPane;
import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
import sample.Listener;
import settings.ColorSetting.ColorSettingName;
import settings.ColorSettingMenuItem;
import settings.Setting;
import settings.SettingChangeListener;
import settings.SettingsManager;
import source.Source.SampleType;
import spectrum.converter.DFTResultsConverter;
import spectrum.converter.RealDecibelConverter;
import spectrum.menu.AmplificationItem;
import spectrum.menu.AveragingItem;
import spectrum.menu.BaselineItem;
import spectrum.menu.FFTWidthItem;
import spectrum.menu.FFTWindowTypeItem;
import spectrum.menu.FrameRateItem;
import controller.ResourceManager;
import controller.channel.Channel;
import controller.channel.ChannelEvent;
import controller.channel.ChannelEventListener;
import dsp.filter.Filters;
import dsp.filter.FloatHalfBandFilter;
import dsp.filter.Window.WindowType;
public class ChannelSpectrumPanel extends JPanel
implements ChannelEventListener,
Listener<Float>,
SettingChangeListener,
SpectralDisplayAdjuster
{
private static final long serialVersionUID = 1L;
private static final String CHANNEL_SPECTRUM_AVERAGING_SIZE =
"channel_spectrum_averaging_size";
private ResourceManager mResourceManager;
private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.FLOAT );
private DFTResultsConverter mDFTConverter = new RealDecibelConverter();
private JLayeredPane mLayeredPane;
private SpectrumPanel mSpectrumPanel;
private ChannelOverlayPanel mOverlayPanel;
private Channel mCurrentChannel;
private int mSpectrumAveragingSize;
private int mSampleBufferSize = 2400;
private Float[] mSamples = new Float[ mSampleBufferSize ];
private int mSamplePointer = 0;
private DecimatingSampleAssembler mDecimatingSampleAssembler;
private AtomicBoolean mEnabled = new AtomicBoolean();
public ChannelSpectrumPanel( ResourceManager resourceManager )
{
mResourceManager = resourceManager;
mResourceManager.getSettingsManager().addListener( this );
mSpectrumPanel = new SpectrumPanel( mResourceManager );
mOverlayPanel = new ChannelOverlayPanel( mResourceManager );
mSpectrumAveragingSize = mResourceManager.getSettingsManager()
.getIntegerSetting( CHANNEL_SPECTRUM_AVERAGING_SIZE, 10 ).getValue();
mSpectrumPanel.setAveraging( mSpectrumAveragingSize );
mDFTProcessor.addConverter( mDFTConverter );
mDFTConverter.addListener( mSpectrumPanel );
mDecimatingSampleAssembler = new DecimatingSampleAssembler( mDFTProcessor );
/* Set the DFTProcessor to the decimated 24kHz sample rate */
mDFTProcessor.frequencyChanged( 0, 24000 );
initGui();
}
public void dispose()
{
setEnabled( false );
mDFTProcessor.dispose();
mResourceManager = 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 CHANGE_SELECTED:
if( !event.getChannel().isSelected() )
{
stop();
mCurrentChannel = null;
}
else
{
mCurrentChannel = event.getChannel();
if( mEnabled.get() )
{
start();
}
}
if( event.getChannel().isSelected() &&
mCurrentChannel != event.getChannel() )
{
stop();
mCurrentChannel = event.getChannel();
start();
}
break;
case CHANNEL_DELETED:
if( event.getChannel() == mCurrentChannel )
{
if( mEnabled.get() )
{
stop();
}
mCurrentChannel = null;
}
break;
case CHANGE_ENABLED:
if( event.getChannel() == mCurrentChannel &&
!event.getChannel().isProcessing() )
{
stop();
mCurrentChannel = null;
}
break;
}
}
private void start()
{
if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.isProcessing() )
{
mCurrentChannel.getProcessingChain().addFloatListener( this );
mDFTProcessor.start();
}
}
private void stop()
{
if( mCurrentChannel != null && mCurrentChannel.isProcessing() )
{
mCurrentChannel.getProcessingChain().removeFloatListener( 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( Float sample )
{
mDecimatingSampleAssembler.receive( sample );
}
/**
* 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" );
SettingsManager sm = mResourceManager.getSettingsManager();
colorMenu.add( new ColorSettingMenuItem( sm,
ColorSettingName.SPECTRUM_CURSOR ) );
colorMenu.add( new ColorSettingMenuItem( sm,
ColorSettingName.SPECTRUM_LINE ) );
colorMenu.add( new ColorSettingMenuItem( sm,
ColorSettingName.SPECTRUM_BACKGROUND ) );
colorMenu.add( new ColorSettingMenuItem( sm,
ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ) );
colorMenu.add( new ColorSettingMenuItem( sm,
ColorSettingName.SPECTRUM_GRADIENT_TOP ) );
contextMenu.add( colorMenu );
/**
* Display items: fft and frame rate
*/
JMenu displayMenu = new JMenu( "Display" );
contextMenu.add( displayMenu );
/**
* Amplification menu
*/
JMenu amplificationMenu = new JMenu( "Amplification" );
amplificationMenu.add(
new AmplificationItem( ChannelSpectrumPanel.this, 50 ) );
displayMenu.add( amplificationMenu );
/**
* Averaging menu
*/
JMenu averagingMenu = new JMenu( "Averaging" );
averagingMenu.add(
new AveragingItem( ChannelSpectrumPanel.this, 4 ) );
displayMenu.add( averagingMenu );
/**
* Baseline menu
*/
JMenu baselineMenu = new JMenu( "Baseline" );
baselineMenu.add(
new BaselineItem( ChannelSpectrumPanel.this, 50 ) );
displayMenu.add( baselineMenu );
/**
* FFT width
*/
JMenu fftWidthMenu = new JMenu( "FFT Width" );
displayMenu.add( fftWidthMenu );
for( FFTWidth width: FFTWidth.values() )
{
fftWidthMenu.add( new FFTWidthItem( 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 ) );
}
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 getAmplification()
{
return mSpectrumPanel.getAmplification();
}
@Override
public void setAmplification( int amplification )
{
mSpectrumPanel.setAmplification( amplification );
}
@Override
public int getAveraging()
{
return mSpectrumPanel.getAveraging();
}
@Override
public void setAveraging( int averaging )
{
mSpectrumPanel.setAveraging( averaging );
}
@Override
public int getBaseline()
{
return mSpectrumPanel.getBaseline();
}
@Override
public void setBaseline( int baseline )
{
mSpectrumPanel.setBaseline( baseline );
}
public class DecimatingSampleAssembler implements Listener<Float>
{
private FloatHalfBandFilter mDecimationFilter = new FloatHalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 );
private SampleAssembler mSampleAssembler;
public DecimatingSampleAssembler( Listener<Float[]> listener )
{
mSampleAssembler = new SampleAssembler( listener );
mDecimationFilter.setListener( mSampleAssembler );
}
@Override
public void receive( Float sample )
{
mDecimationFilter.receive( sample );
}
}
public class SampleAssembler implements Listener<Float>
{
private Listener<Float[]> mListener;
public SampleAssembler( Listener<Float[]> listener )
{
mListener = listener;
}
@Override
public void receive( Float sample )
{
mSamples[ mSamplePointer++ ] = sample;
if( mSamplePointer >= mSamples.length )
{
if( mEnabled.get() )
{
mListener.receive( Arrays.copyOf( mSamples,
mSamples.length ) );
}
mSamplePointer = 0;
if( mSamples.length != mSampleBufferSize )
{
mSamples = new Float[ mSampleBufferSize ];
}
}
}
}
}

View File

@@ -27,24 +27,26 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import log.Log;
import org.jtransforms.fft.FloatFFT_1D;
import sample.Listener;
import source.Source;
import source.Source.SampleType;
import source.tuner.FrequencyChangeListener;
import spectrum.converter.DFTResultsConverter;
import dsp.filter.Window;
import dsp.filter.Window.WindowType;
import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D;
/**
* 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<Float[]>,
DFTResultsProvider,
FrequencyChangeListener
{
private CopyOnWriteArrayList<DFTResultsListener> mListeners =
new CopyOnWriteArrayList<DFTResultsListener>();
private CopyOnWriteArrayList<DFTResultsConverter> mListeners =
new CopyOnWriteArrayList<DFTResultsConverter>();
private ArrayBlockingQueue<Float[]> mQueue =
new ArrayBlockingQueue<Float[]>( 200 );
@@ -56,14 +58,16 @@ public class DFTProcessor implements Listener<Float[]>,
private FFTWidth mNewFFTWidth = FFTWidth.FFT04096;
private double[] mWindow;
private WindowType mWindowType = Window.WindowType.HANNING;
private WindowType mWindowType = Window.WindowType.BLACKMAN;
private FloatFFT_1D mFFT = new FloatFFT_1D( mFFTWidth.getWidth() );
private int mFrameRate;
private int mSampleRate;
private float mOverlapPerFrame;
private float mOverlapResidual;
private int mFFTFloatsPerFrame;
private float mNewFloatsPerFrame;
private float mNewFloatsPerFrameToConsume;
private float mNewFloatResidual;
private float[] mPreviousFrame = new float[ 8192 ];
private Float[] mCurrentBuffer;
@@ -81,17 +85,7 @@ public class DFTProcessor implements Listener<Float[]>,
public void dispose()
{
try
{
clearScheduler();
}
catch ( InterruptedException e )
{
Log.error( "DFTProcessor - exception shutting down processor - " +
e.getLocalizedMessage() );
}
mScheduler.shutdown();
stop();
mListeners.clear();
mQueue.clear();
@@ -165,54 +159,56 @@ public class DFTProcessor implements Listener<Float[]>,
+ "more than 1000 times per second -- requested setting:"
+ framesPerSecond );
}
else
{
mFrameRate = framesPerSecond;
calculateOverlap();
mFrameRate = framesPerSecond;
/**
* Shutdown the scheduler and clear out any remaining tasks
*/
try
{
clearScheduler();
}
catch ( InterruptedException e )
{
Log.error( "DFTProcessor - exception while awaiting shutdown of "
+ "calculation scheduler for reset" );
}
calculateConsumptionRate();
/**
* Reset the scheduler
*/
mScheduler = Executors.newScheduledThreadPool(1);
/**
* Schedule the DFT to run calculations at a fixed rate
*/
int initialDelay = 0;
int period = (int)( 1000 / mFrameRate );
TimeUnit unit = TimeUnit.MILLISECONDS;
mScheduler.scheduleAtFixedRate( new DFTCalculationTask(),
initialDelay, period, unit );
}
restart();
}
/**
* Shuts down the scheduler and blocks up to 100 milliseconds until all
* tasks have completed.
*/
private void clearScheduler() throws InterruptedException
public void start()
{
mScheduler.shutdown();
/**
* Reset the scheduler
*/
mScheduler = Executors.newScheduledThreadPool(1);
mScheduler.awaitTermination( 100, TimeUnit.MILLISECONDS );
/**
* Schedule the DFT to run calculations at a fixed rate
*/
int initialDelay = 0;
int period = (int)( 1000 / mFrameRate );
TimeUnit unit = TimeUnit.MILLISECONDS;
mScheduler.scheduleAtFixedRate( new DFTCalculationTask(),
initialDelay, period, unit );
}
public void stop()
{
/**
* Shutdown the scheduler and clear out any remaining tasks
*/
try
{
mScheduler.shutdown();
mScheduler.awaitTermination( 100, TimeUnit.MILLISECONDS );
}
catch ( InterruptedException e )
{
Log.error( "DFTProcessor - exception while awaiting shutdown of "
+ "calculation scheduler for reset" );
}
}
public void restart()
{
stop();
start();
}
public int getCalculationsPerSecond()
{
return mFrameRate;
@@ -226,7 +222,10 @@ public class DFTProcessor implements Listener<Float[]>,
{
if( !mQueue.offer( samples ) )
{
Log.error( "DFTProcessor - queue is full, purging queue, samples[" + samples + "]" );
Log.error( "DFTProcessor - [" + mSampleType.toString()
+ "]queue is full, purging queue, "
+ "samples[" + samples + "]" );
mQueue.clear();
mQueue.offer( samples );
}
@@ -236,8 +235,6 @@ public class DFTProcessor implements Listener<Float[]>,
{
mCurrentBuffer = null;
long start = System.currentTimeMillis();
try
{
mCurrentBuffer = mQueue.take();
@@ -252,107 +249,66 @@ public class DFTProcessor implements Listener<Float[]>,
private float[] getSamples()
{
int remaining;
if( mSampleType == SampleType.COMPLEX )
{
remaining = mFFTWidth.getWidth() * 2;
}
else
{
remaining = mFFTWidth.getWidth();
}
int remaining = (int)mFFTFloatsPerFrame;
float[] currentFrame = new float[ remaining ];
float[] currentFrame = new float[ mFFTWidth.getWidth() * 2 ];
int currentFramePointer = 0;
/* Get the integer component of the overlap, leaving the fractional
* residual for the next frame */
float requiredOverlap = mOverlapPerFrame + mOverlapResidual;
int overlap = (int)requiredOverlap;
mOverlapResidual = requiredOverlap - overlap;
/* If positive overlap, we have to re-use samples from previous frame */
if( overlap > 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 previousFramePointer;
int length;
int previousFloatsRequired = remaining - newFloatsToConsumeThisFrame;
if( mSampleType == SampleType.COMPLEX )
{
length = overlap * 2;
previousFramePointer = mPreviousFrame.length - length;
}
else
{
length = overlap;
previousFramePointer = (int)(mPreviousFrame.length / 2) - length;
}
System.arraycopy( mPreviousFrame,
mPreviousFrame.length - previousFloatsRequired,
currentFrame,
currentFramePointer,
previousFloatsRequired );
remaining -= previousFloatsRequired;
currentFramePointer += previousFloatsRequired;
}
System.arraycopy( mPreviousFrame, previousFramePointer,
currentFrame, currentFramePointer, length );
remaining -= length;
currentFramePointer += length;
}
/* If negative overlap, we have to throw away samples */
else
{
if( mSampleType == SampleType.COMPLEX )
{
purge( -( overlap * 2 ) );
}
else
{
purge( -overlap );
}
}
/* Fill the rest of the buffer with new samples */
while( mRunning.get() && remaining > 0 )
{
if( mCurrentBuffer == null ||
mCurrentBufferPointer == mCurrentBuffer.length )
mCurrentBufferPointer >= mCurrentBuffer.length )
{
getNextBuffer();
}
int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer;
if( samplesAvailable >= remaining )
while( remaining > 0 && samplesAvailable > 0 )
{
while( remaining > 0 )
{
currentFrame[ currentFramePointer ] =
(float)mCurrentBuffer[ mCurrentBufferPointer ];
currentFramePointer++;
mCurrentBufferPointer++;
remaining --;
}
mCurrentBufferPointer += remaining;
remaining = 0;
}
else
{
while( samplesAvailable > 0 )
{
currentFrame[ currentFramePointer ] =
(float)mCurrentBuffer[ mCurrentBufferPointer ];
currentFrame[ currentFramePointer++ ] =
(float)mCurrentBuffer[ mCurrentBufferPointer++ ];
currentFramePointer++;
mCurrentBufferPointer++;
samplesAvailable--;
remaining--;
}
samplesAvailable--;
remaining--;
newFloatsToConsumeThisFrame--;
}
}
/* If frames are overlapping, store current frame to use next time */
if( mOverlapPerFrame > 0 )
/* 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 = null;
mPreviousFrame = Arrays.copyOf( currentFrame, currentFrame.length );
}
@@ -367,7 +323,7 @@ public class DFTProcessor implements Listener<Float[]>,
if( mSampleType == SampleType.FLOAT )
{
mFFT.realForwardFull( samples );
mFFT.realForward( samples );
}
else
{
@@ -389,7 +345,7 @@ public class DFTProcessor implements Listener<Float[]>,
while( mRunning.get() && samplesToPurge > 0 )
{
if( mCurrentBuffer == null ||
mCurrentBufferPointer == mCurrentBuffer.length )
mCurrentBufferPointer >= mCurrentBuffer.length )
{
getNextBuffer();
}
@@ -415,56 +371,20 @@ public class DFTProcessor implements Listener<Float[]>,
*/
private void dispatch( float[] results )
{
float dc = Math.abs( results[ 0 ] );
Iterator<DFTResultsConverter> it = mListeners.iterator();
/* Ensure we have power in the DC power bin (bin 0), otherwise don't
* dispatch the results */
if( dc != 0 )
while( it.hasNext() )
{
float[] processed = new float[ results.length / 2 ];
int half = results.length / 2;
int quarter = results.length / 4;
for( int x = 2; x < half; x += 2 )
{
processed[ quarter + ( x / 2 ) ] =
(float)( Math.log10(
(double)( Math.abs( results[ x ] ) /
dc ) ) );
}
for( int x = 0; x < quarter - 1; x++ )
{
processed[ x ] =
(float)( Math.log10(
(double)( Math.abs( results[ half + ( ( x + 1 ) * 2 ) ] ) /
dc ) ) );
}
//Real bin n/2 -1
processed[ quarter - 1 ] = (float)( Math.log10(
(double)( Math.abs( results[ 1 ] ) / dc ) ) );
Iterator<DFTResultsListener> it = mListeners.iterator();
while( it.hasNext() )
{
it.next().receive( processed );
}
it.next().receive( results );
}
results = null;
}
@Override
public void addListener( DFTResultsListener listener )
public void addConverter( DFTResultsConverter listener )
{
mListeners.add( listener );
}
@Override
public void removeListener( DFTResultsListener listener )
public void removeConverter( DFTResultsConverter listener )
{
mListeners.remove( listener );
}
@@ -483,11 +403,6 @@ public class DFTProcessor implements Listener<Float[]>,
mRunning.set( false );
}
else
{
Log.error( "DFTProcessor - calculation already in progress"
+ " - skipping this iteration" );
}
}
}
@@ -502,12 +417,18 @@ public class DFTProcessor implements Listener<Float[]>,
{
mFFTWidth = mNewFFTWidth;
calculateOverlap();
calculateConsumptionRate();
mWindow = Window.getWindow( WindowType.HANNING,
( mFFTWidth.getWidth() * 2 ) );
mPreviousFrame = new float[ mFFTWidth.getWidth() * 2 ];
setWindowType( mWindowType );
if( mSampleType == SampleType.COMPLEX )
{
mPreviousFrame = new float[ mFFTWidth.getWidth() * 2 ];
}
else
{
mPreviousFrame = new float[ mFFTWidth.getWidth() ];
}
mFFT = new FloatFFT_1D( mFFTWidth.getWidth() );
}
@@ -523,24 +444,30 @@ public class DFTProcessor implements Listener<Float[]>,
{
mSampleRate = bandwidth;
calculateOverlap();
calculateConsumptionRate();
}
/**
* Positive overlap means we have to re-use samples from the previous
* frame
*
* Negative overlap means we have to throw away samples
*/
private void calculateOverlap()
private void calculateConsumptionRate()
{
mOverlapPerFrame = 0.0f;
mOverlapResidual = 0.0f;
mNewFloatResidual = 0.0f;
int requiredSampleRate = mFFTWidth.getWidth() * mFrameRate;
mNewFloatsPerFrame = ( (float)mSampleRate / (float)mFrameRate ) *
( mSampleType == SampleType.COMPLEX ? 2.0f : 1.0f );
int overlap = requiredSampleRate - mSampleRate;
mFFTFloatsPerFrame = ( mSampleType == SampleType.COMPLEX ?
mFFTWidth.getWidth() * 2 :
mFFTWidth.getWidth() );
mOverlapPerFrame = (float)overlap / (float)mFrameRate;
if( mFFTFloatsPerFrame < mNewFloatsPerFrame )
{
mNewFloatsPerFrameToConsume = mFFTFloatsPerFrame;
}
else
{
mNewFloatsPerFrameToConsume = mNewFloatsPerFrame;
}
}
}

View File

@@ -40,12 +40,12 @@ import source.tuner.FrequencyChangeListener;
import source.tuner.TunerChannel;
import controller.ResourceManager;
import controller.channel.Channel;
import controller.channel.Channel.ChannelEvent;
import controller.channel.Channel.ChannelType;
import controller.channel.ChannelListener;
import controller.channel.ChannelEvent;
import controller.channel.ChannelEventListener;
public class OverlayPanel extends JPanel
implements ChannelListener,
implements ChannelEventListener,
FrequencyChangeListener,
SettingChangeListener
{
@@ -362,7 +362,7 @@ public class OverlayPanel extends JPanel
for( Channel channel: mVisibleChannels )
{
//Choose the correct background color to use
if( channel.getSelected() )
if( channel.isSelected() )
{
graphics.setColor( mColorChannelConfigSelected );
}
@@ -510,9 +510,11 @@ public class OverlayPanel extends JPanel
*/
@Override
@SuppressWarnings( "incomplete-switch" )
public void occurred( Channel channel, ChannelEvent component )
public void channelChanged( ChannelEvent event )
{
switch( component )
Channel channel = event.getChannel();
switch( event.getEvent() )
{
case CHANNEL_ADDED:
if( !mChannels.contains( channel ) )

View File

@@ -0,0 +1,55 @@
package spectrum;
public interface SpectralDisplayAdjuster
{
/**
* Gets the amplification multiplier that is applied against the FFT bin
* results when displaying the value in the spectral display.
*
* @return amplification value ( 1 - 100 )
*/
public int getAmplification();
/**
* Sets the amplification multiplier that is applied against the FFT bin
* results when displaying the value in the spectral display.
*
* @param amplification value ( 1 - 100 )
*/
public void setAmplification( int amplification );
/**
* 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();
/**
* Sets the averaging value indicating the number of FFT results to average
* when producing an FFT output result.
* @param averaging value ( 2 - 50 )
*/
public void setAveraging( int averaging );
/**
* Gets the baseline used when plotting the FFT bin results in the FFT
* spectral display. This value determines the vertical position within the
* spectral display upon which the amplified values are added and then
* plotted.
*
* @return averaging value ( 1 - 50 )
*/
public int getBaseline();
/**
* Sets the baseline used when plotting the FFT bin results in the FFT
* spectral display. This value determines the vertical position within the
* spectral display upon which the amplified values are added and then
* plotted.
*
* @param baseline value ( 1 - 50 )
*/
public void setBaseline( int baseline );
}

View File

@@ -29,6 +29,7 @@ import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.text.DecimalFormat;
import java.text.Format;
import java.util.ArrayList;
import javax.swing.JCheckBoxMenuItem;
@@ -55,13 +56,21 @@ import source.SourceException;
import source.tuner.FrequencyChangeListener;
import source.tuner.Tuner;
import source.tuner.TunerSelectionListener;
import spectrum.converter.ComplexDecibelConverter;
import spectrum.converter.DFTResultsConverter;
import spectrum.menu.AmplificationItem;
import spectrum.menu.AveragingItem;
import spectrum.menu.BaselineItem;
import spectrum.menu.FFTWidthItem;
import spectrum.menu.FFTWindowTypeItem;
import spectrum.menu.FrameRateItem;
import com.jidesoft.swing.JideSplitPane;
import controller.ResourceManager;
import controller.channel.Channel;
import controller.channel.Channel.ChannelEvent;
import controller.channel.ChannelListener;
import controller.channel.ChannelEvent;
import controller.channel.ChannelEventListener;
import decode.DecoderType;
import dsp.filter.Window.WindowType;
@@ -74,7 +83,7 @@ import dsp.filter.Window.WindowType;
* in the spectral and waterfall displays.
*/
public class SpectralDisplayPanel extends JPanel
implements ChannelListener,
implements ChannelEventListener,
Listener<Float[]>,
FrequencyChangeListener,
SettingChangeListener,
@@ -98,8 +107,9 @@ public class SpectralDisplayPanel extends JPanel
private JLayeredPane mLayeredPanel;
private SpectrumPanel mSpectrumPanel;
private WaterfallPanel mWaterfallPanel;
private OverlayPanel mChannelPanel;
private OverlayPanel mOverlayPanel;
private DFTProcessor mDFTProcessor;
private DFTResultsConverter mDFTConverter;
private ResourceManager mResourceManager;
private Tuner mTuner;
@@ -122,14 +132,17 @@ public class SpectralDisplayPanel extends JPanel
mDFTProcessor.dispose();
mDFTProcessor = null;
mDFTConverter.dispose();
mDFTConverter = null;
mSpectrumPanel.dispose();
mSpectrumPanel = null;
mWaterfallPanel.dispose();
mWaterfallPanel = null;
mChannelPanel.dispose();
mChannelPanel = null;
mOverlayPanel.dispose();
mOverlayPanel = null;
mResourceManager = null;
@@ -193,9 +206,9 @@ public class SpectralDisplayPanel extends JPanel
@Override
public void settingChanged( Setting setting )
{
if( mChannelPanel != null )
if( mOverlayPanel != null )
{
mChannelPanel.settingChanged( setting );
mOverlayPanel.settingChanged( setting );
}
if( mWaterfallPanel != null )
@@ -274,14 +287,14 @@ public class SpectralDisplayPanel extends JPanel
//Create the spectrum panel
mSpectrumPanel = new SpectrumPanel( mResourceManager );
//Create the channel panel
mChannelPanel = new OverlayPanel( mResourceManager );
mChannelPanel.addMouseListener( mouser );
mChannelPanel.addMouseMotionListener( mouser );
//Create the overlay panel
mOverlayPanel = new OverlayPanel( mResourceManager );
mOverlayPanel.addMouseListener( mouser );
mOverlayPanel.addMouseMotionListener( mouser );
//Add the spectrum and channel panels to the layered panel
mLayeredPanel.add( mSpectrumPanel, new Integer( 0 ), 0 );
mLayeredPanel.add( mChannelPanel, new Integer( 1 ), 0 );
mLayeredPanel.add( mOverlayPanel, new Integer( 1 ), 0 );
//Create the waterfall
mWaterfallPanel = new WaterfallPanel( mResourceManager );
@@ -311,8 +324,11 @@ public class SpectralDisplayPanel extends JPanel
* spectrum panel to receive the processed dft results
*/
mDFTProcessor = new DFTProcessor( SampleType.COMPLEX );
mDFTProcessor.addListener( (DFTResultsListener)mSpectrumPanel );
mDFTProcessor.addListener( (DFTResultsListener)mWaterfallPanel );
mDFTConverter = new ComplexDecibelConverter();
mDFTProcessor.addConverter( mDFTConverter );
mDFTConverter.addListener( (DFTResultsListener)mSpectrumPanel );
mDFTConverter.addListener( (DFTResultsListener)mWaterfallPanel );
}
/**
@@ -321,17 +337,17 @@ public class SpectralDisplayPanel extends JPanel
@Override
public void frequencyChanged( long frequency, int bandwidth )
{
mChannelPanel.frequencyChanged( frequency, bandwidth );
mOverlayPanel.frequencyChanged( frequency, bandwidth );
mDFTProcessor.frequencyChanged( frequency, bandwidth );
}
/**
* Channel config change events
* Channel change events
*/
@Override
public void occurred( Channel config, ChannelEvent component )
public void channelChanged( ChannelEvent event )
{
mChannelPanel.occurred( config, component );
mOverlayPanel.channelChanged( event );
}
@Override
@@ -402,7 +418,7 @@ public class SpectralDisplayPanel extends JPanel
Component c = e.getComponent();
mSpectrumPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() );
mChannelPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() );
mOverlayPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() );
}
@Override
@@ -432,24 +448,24 @@ public class SpectralDisplayPanel extends JPanel
private void update( MouseEvent event )
{
if( event.getComponent() == mChannelPanel )
if( event.getComponent() == mOverlayPanel )
{
mChannelPanel.setCursorLocation( event.getPoint() );
mOverlayPanel.setCursorLocation( event.getPoint() );
}
else
{
mWaterfallPanel.setCursorLocation( event.getPoint() );
mWaterfallPanel.setCursorFrequency(
mChannelPanel.getFrequencyFromAxis( event.getPoint().x ) );
mOverlayPanel.getFrequencyFromAxis( event.getPoint().x ) );
}
}
@Override
public void mouseEntered( MouseEvent e )
{
if( e.getComponent() == mChannelPanel )
if( e.getComponent() == mOverlayPanel )
{
mChannelPanel.setCursorVisible( true );
mOverlayPanel.setCursorVisible( true );
}
else
{
@@ -460,7 +476,7 @@ public class SpectralDisplayPanel extends JPanel
@Override
public void mouseExited( MouseEvent e )
{
mChannelPanel.setCursorVisible( false );
mOverlayPanel.setCursorVisible( false );
mWaterfallPanel.setCursorVisible( false );
}
@@ -480,12 +496,12 @@ public class SpectralDisplayPanel extends JPanel
}
long frequency =
mChannelPanel.getFrequencyFromAxis( event.getX() );
mOverlayPanel.getFrequencyFromAxis( event.getX() );
if( event.getComponent() == mChannelPanel )
if( event.getComponent() == mOverlayPanel )
{
ArrayList<Channel> channels =
mChannelPanel.getChannelsAtFrequency( frequency );
mOverlayPanel.getChannelsAtFrequency( frequency );
for( Channel channel: channels )
{
@@ -562,6 +578,44 @@ public class SpectralDisplayPanel extends JPanel
JMenu displayMenu = new JMenu( "Display" );
contextMenu.add( displayMenu );
if( event.getComponent() == mWaterfallPanel )
{
/**
* Baseline menu
*/
JMenu baselineMenu = new JMenu( "Baseline" );
baselineMenu.add(
new BaselineItem( mWaterfallPanel, 50 ) );
displayMenu.add( baselineMenu );
}
else
{
/**
* Amplification menu
*/
JMenu amplificationMenu = new JMenu( "Amplify" );
amplificationMenu.add(
new AmplificationItem( mSpectrumPanel, 50 ) );
displayMenu.add( amplificationMenu );
/**
* Averaging menu
*/
JMenu averagingMenu = new JMenu( "Average" );
averagingMenu.add(
new AveragingItem( mSpectrumPanel, 4 ) );
displayMenu.add( averagingMenu );
/**
* Baseline menu
*/
JMenu baselineMenu = new JMenu( "Baseline" );
baselineMenu.add(
new BaselineItem( mSpectrumPanel, 50 ) );
displayMenu.add( baselineMenu );
}
/**
* FFT width
*/
@@ -602,9 +656,9 @@ public class SpectralDisplayPanel extends JPanel
if( contextMenu != null )
{
if( event.getComponent() == mChannelPanel )
if( event.getComponent() == mOverlayPanel )
{
contextMenu.show( mChannelPanel,
contextMenu.show( mOverlayPanel,
event.getX(),
event.getY() );
}
@@ -658,95 +712,6 @@ public class SpectralDisplayPanel extends JPanel
}
}
public class FFTWidthItem extends JCheckBoxMenuItem
{
private static final long serialVersionUID = 1L;
private DFTProcessor mDFTProcessor;
private FFTWidth mFFTWidth;
public FFTWidthItem( DFTProcessor processor, FFTWidth width )
{
super( width.getLabel() );
mDFTProcessor = processor;
mFFTWidth = width;
if( processor.getFFTWidth() == mFFTWidth )
{
setSelected( true );
}
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent arg0 )
{
mDFTProcessor.setFFTSize( mFFTWidth );
}
} );
}
}
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 );
}
} );
}
}
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 );
}
} );
}
}
/**
* Context menu item to provide one-click access to starting a channel

View File

@@ -29,33 +29,42 @@ import java.awt.geom.Line2D;
import javax.swing.JPanel;
import log.Log;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.IntegerSetting;
import settings.Setting;
import settings.SettingChangeListener;
import buffer.FloatArrayCircularAveragingBuffer;
import buffer.FloatAveragingBuffer;
import controller.ResourceManager;
public class SpectrumPanel extends JPanel
implements DFTResultsListener,
SettingChangeListener
SettingChangeListener,
SpectralDisplayAdjuster
{
private static final long serialVersionUID = 1L;
/* Set display bins size to 1, so that we're guaranteed a reset to the
* correct width once the first sample set arrives */
private float[] mDisplayFFTBins = new float[ 1 ];
private FloatArrayCircularAveragingBuffer mFFTAveragingBuffer =
new FloatArrayCircularAveragingBuffer( 4 );
private FloatArrayCircularAveragingBuffer mFFTAveragingBuffer;
private FloatAveragingBuffer mScaleBuffer = new FloatAveragingBuffer( 20 );
private int mAmplification = 50;
private int mAveraging = 4;
private int mBaseline = 50;
/**
* Spectral Display Color Settings
*/
private static final String sSPECTRUM_BACKGROUND = "spectrum_background";
private static final String sSPECTRUM_GRADIENT_TOP = "spectrum_gradient_top";
private static final String sSPECTRUM_GRADIENT_BOTTOM = "spectrum_gradient_bottom";
private static final String sSPECTRUM_LINE = "spectrum_line";
private static final int sSPECTRUM_TRANSLUCENCY = 128;
private static final String SPECTRUM_BACKGROUND = "spectrum_background";
private static final String SPECTRUM_GRADIENT_TOP = "spectrum_gradient_top";
private static final String SPECTRUM_GRADIENT_BOTTOM = "spectrum_gradient_bottom";
private static final String SPECTRUM_LINE = "spectrum_line";
private static final String SPECTRUM_AVERAGING_SIZE = "spectrum_averaging_size";
private static final int SPECTRUM_TRANSLUCENCY = 128;
private Color mColorSpectrumBackground;
private Color mColorSpectrumGradientTop;
@@ -63,14 +72,16 @@ public class SpectrumPanel extends JPanel
private Color mColorSpectrumLine;
private float mSpectrumInset = 20.0f;
private ResourceManager mResourceManager;
public SpectrumPanel( ResourceManager resourceManager )
{
mResourceManager = resourceManager;
setColors();
getSettings();
setAveraging( mAveraging );
}
public void dispose()
@@ -78,7 +89,50 @@ public class SpectrumPanel extends JPanel
mResourceManager = null;
}
private void setColors()
public void setAveraging( int size )
{
mAveraging = size;
mResourceManager.getSettingsManager().setIntegerSetting(
SPECTRUM_AVERAGING_SIZE, mAveraging );
mFFTAveragingBuffer =
new FloatArrayCircularAveragingBuffer( mAveraging );
}
public int getAveraging()
{
return mAveraging;
}
public void setAmplification( int amplification )
{
mAmplification = amplification;
}
public int getAmplification()
{
return mAmplification;
}
public void setBaseline( int baseline )
{
mBaseline = baseline;
}
public int getBaseline()
{
return mBaseline;
}
public void clearSpectrum()
{
mDisplayFFTBins = null;
repaint();
}
private void getSettings()
{
mColorSpectrumBackground =
getColor( ColorSettingName.SPECTRUM_BACKGROUND );
@@ -90,6 +144,16 @@ public class SpectrumPanel extends JPanel
getColor( ColorSettingName.SPECTRUM_GRADIENT_TOP );
mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE );
mAveraging = getIntegerSetting( "spectrum_averaging_size", 4 );
}
private int getIntegerSetting( String name, int defaultValue )
{
IntegerSetting setting = mResourceManager.getSettingsManager()
.getIntegerSetting( name, defaultValue );
return setting.getValue();
}
private Color getColor( ColorSettingName name )
@@ -203,25 +267,45 @@ public class SpectrumPanel extends JPanel
//If we have FFT data to display ...
if( mDisplayFFTBins != null )
{
int binCount = mDisplayFFTBins.length - 1;
float amplify = mAmplification / 50.0f;
float insideHeight = size.height - mSpectrumInset;
float baseline = ( insideHeight * ( 50.0f / (float)mBaseline ) );
/* Use the DC component as a reference to scale values to panel height */
float scale = mScaleBuffer.get( ( insideHeight * 0.75f ) /
mDisplayFFTBins[ binCount ] );
float insideWidth = size.width;
float binSize = insideWidth / ( mDisplayFFTBins.length - 1 );
float heightMultiplier = (float)( insideHeight * .15 );
float binSize = insideWidth / ( binCount );
float spectrumMiddle = insideHeight * .23f;
for( int x = 0; x < mDisplayFFTBins.length; x++ )
for( int x = 0; x < binCount; x++ )
{
float height = spectrumMiddle - ( mDisplayFFTBins[ x ] * heightMultiplier );
/**
* Height is an inverse value reflecting the drawable spectral
* display vertical axis between the inset value at the bottom
* and the top of the display. Values range from:
*
* top = 0
* bottom = insideHeight = panel height - mSpectrumInset
*/
float height = baseline - ( mDisplayFFTBins[ x ] * scale * amplify );
if( height > size.getHeight() - mSpectrumInset )
if( height > insideHeight )
{
height = (float)size.getHeight() - mSpectrumInset;
height = insideHeight;
}
spectrumShape.lineTo( binSize * x, height );
if( height < 0 )
{
height = 0;
}
spectrumShape.lineTo( ( x * binSize ), height );
}
}
//Otherwise show an empty spectrum

View File

@@ -47,7 +47,8 @@ import controller.ResourceManager;
*/
public class WaterfallPanel extends JPanel implements DFTResultsListener,
Pausable,
SettingChangeListener
SettingChangeListener,
SpectralDisplayAdjuster
{
private static final long serialVersionUID = 1L;
private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" );
@@ -61,11 +62,7 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
private ColorModel mColorModel = WaterfallColorModel.getDefaultColorModel();
private Color mColorSpectrumCursor;
private Image mWaterfallImage;
float mAverage = 0.0f;
double mAlpha = 0.95d;
int mScalor = 85;
int temp = 0;
private int mBaseline = 50;
private Point mCursorLocation = new Point( 0, 0 );
private boolean mCursorVisible = false;
@@ -108,7 +105,7 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
mWaterfallImage = createImage( mMemoryImageSource );
}
/**
* Pausable interface - pauses updates to the screen
*/
@@ -210,9 +207,9 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
public void receive( float[] update )
{
//If our FFT size changes, reset our pixel map and image source
if( mFFTSize != update.length )
if( mFFTSize != update.length - 1 )
{
mFFTSize = update.length;
mFFTSize = update.length - 1;
reset();
}
@@ -221,37 +218,41 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
System.arraycopy( mPixels, 0,
mPixels, mFFTSize, mPixels.length - mFFTSize );
//Convert the power spectrum bin values into a byte (0-255)
//value to use with the color mapping
float baseline = mBaseline / 50.0f;
double averagingTotal = 0.0;
/**
* Scale the power spectrum bin values into a byte range (0-255) value
* using the last bin as the scaling factor
*/
float scale = 256.0f / update[ update.length - 1 ];
double sum = 0.0d;
for( int x = 0; x < update.length; x++ )
for( int x = 0; x < update.length - 1; x++ )
{
float value = update[ x ];
averagingTotal += value;
//Average the values toward zero
float pixel = value - mAverage;
//If we're below the average, make the negative value a positive
if( pixel < 0 )
{
// pixel = -pixel;
pixel = 0.0f;
}
//Scale the value
pixel *= mScalor;
// mPixels[ x ] = (byte)pixel;
mPixels[ x ] = (byte)( mIntraBinAveragingBuffer.get( pixel ) );
sum += update[ x ];
}
//Update the average value
mAverage = (float) averagingTotal / update.length;
float average = (float)( sum / (double)update.length - 1 );
for( int x = 0; x < update.length - 1; x++ )
{
float value = ( update[ x ] - average ) * scale * baseline;
if( value < 0 )
{
mPixels[ x ] = 0;
}
else if( value > 255 )
{
mPixels[ x ] = (byte)255;
}
else
{
mPixels[ x ] = (byte)value;
}
}
if( !mPaused.get() )
{
EventQueue.invokeLater( new Runnable()
@@ -284,4 +285,42 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
@Override
public void settingDeleted( Setting setting ) {}
@Override
public void setBaseline( int baseline )
{
mBaseline = baseline;
}
@Override
public int getBaseline()
{
return mBaseline;
}
@Override
public int getAmplification()
{
/* Not Implemented */
return 0;
}
@Override
public void setAmplification( int amplification )
{
/* Not Implemented */
}
@Override
public int getAveraging()
{
/* Not Implemented */
return 0;
}
@Override
public void setAveraging( int averaging )
{
/* Not Implemented */
}
}

View File

@@ -0,0 +1,62 @@
package spectrum.converter;
public class ComplexDecibelConverter extends DFTResultsConverter
{
/**
* Converts the output of the JTransforms FloatFFT_1D.complexForward()
* calculation into a normalized power spectrum in decibels, per description
* in Lyons, Understanding Digital Signal Processing, 3e, page 141.
*/
@Override
public void receive( float[] results )
{
float dc = Math.abs( results[ 0 ] );
/* Ensure we have power in the DC power bin (bin 0), otherwise don't
* dispatch the results */
if( dc > 0 )
{
float[] processed = new float[ results.length / 2 + 1 ];
/**
* Output from JTransforms 3.0 has the lower and upper halves of the
* spectrum swapped.
*/
int half = processed.length / 2;
for( int x = 0; x < processed.length - 2; x ++ )
{
int index = ( x + 1 ) * 2;
double magnitude2 = ( ( results[ index ] * results[ index ] ) +
( results[ index + 1 ] * results[ index + 1 ] ) );
if( x < half )
{
processed[ x + half ] = 10.0f *
(float)( Math.log10( magnitude2 / dc ) );
}
else
{
processed[ x - half ] = 10.0f *
(float)( Math.log10( magnitude2 / dc ) );
}
}
/**
* Place scaled dc component in last bin of processed array as
* scaling reference
*/
processed[ results.length / 2 ] = processed[ half ];
/**
* Repair the erroneous DC component bin by using the average of
* the neighboring bins
*/
processed[ half ] = ( processed[ half - 1 ] +
processed[ half + 1 ] / 2.0f );
dispatch( processed );
}
}
}

View File

@@ -0,0 +1,46 @@
package spectrum.converter;
import java.util.concurrent.CopyOnWriteArrayList;
import spectrum.DFTResultsListener;
import spectrum.DFTResultsProvider;
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,38 @@
package spectrum.converter;
public class RealDecibelConverter extends DFTResultsConverter
{
/**
* 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.
*/
@Override
public void receive( float[] results )
{
float dc = Math.abs( results[ 0 ] );
/* Ensure we have power in the DC power bin (bin 0), otherwise don't
* dispatch the results */
if( dc != 0 )
{
/* Note: we're only processing 1/4 of the output samples */
float[] processed = new float[ results.length / 4 + 1 ];
for( int x = 0; x < processed.length - 2; x ++ )
{
int index = ( x + 1 ) * 2;
double magnitude = ( ( results[ index ] * results[ index ] ) +
( results[ index + 1 ] * results[ index + 1 ] ) );
processed[ x ] = 10.0f *
(float)( Math.log10( magnitude / dc ) );
}
processed[ processed.length - 1 ] = processed[ 0 ];
dispatch( processed );
}
}
}

View File

@@ -0,0 +1,57 @@
package spectrum.menu;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import spectrum.SpectralDisplayAdjuster;
public class AmplificationItem extends JSlider implements ChangeListener
{
private static final long serialVersionUID = 1L;
private SpectralDisplayAdjuster mAdjuster;
private int mDefaultValue;
public AmplificationItem( SpectralDisplayAdjuster adjuster, int defaultValue )
{
super( JSlider.HORIZONTAL, 1, 100, adjuster.getAmplification() );
mDefaultValue = defaultValue;
mAdjuster = adjuster;
setMajorTickSpacing( 10 );
setMinorTickSpacing( 5 );
setPaintTicks( true );
addChangeListener( this );
addMouseListener( new MouseListener()
{
@Override
public void mouseClicked( MouseEvent event )
{
if( event.getClickCount() == 2 )
{
AmplificationItem.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.setAmplification( value );
}
}

View File

@@ -0,0 +1,57 @@
package spectrum.menu;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import spectrum.SpectralDisplayAdjuster;
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, 2, 50, adjuster.getAveraging() );
mDefaultValue = defaultValue;
mAdjuster = adjuster;
setMajorTickSpacing( 10 );
setMinorTickSpacing( 5 );
setPaintTicks( 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,57 @@
package spectrum.menu;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import spectrum.SpectralDisplayAdjuster;
public class BaselineItem extends JSlider implements ChangeListener
{
private static final long serialVersionUID = 1L;
private SpectralDisplayAdjuster mAdjuster;
private int mDefaultValue;
public BaselineItem( SpectralDisplayAdjuster adjuster, int defaultValue )
{
super( JSlider.HORIZONTAL, 1, 100, adjuster.getBaseline() );
mDefaultValue = defaultValue;
mAdjuster = adjuster;
setMajorTickSpacing( 10 );
setMinorTickSpacing( 5 );
setPaintTicks( true );
addChangeListener( this );
addMouseListener( new MouseListener()
{
@Override
public void mouseClicked( MouseEvent event )
{
if( event.getClickCount() == 2 )
{
BaselineItem.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.setBaseline( value );
}
}

View File

@@ -0,0 +1,39 @@
package spectrum.menu;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JCheckBoxMenuItem;
import spectrum.DFTProcessor;
import spectrum.FFTWidth;
public class FFTWidthItem extends JCheckBoxMenuItem
{
private static final long serialVersionUID = 1L;
private DFTProcessor mDFTProcessor;
private FFTWidth mFFTWidth;
public FFTWidthItem( DFTProcessor processor, FFTWidth width )
{
super( width.getLabel() );
mDFTProcessor = processor;
mFFTWidth = width;
if( processor.getFFTWidth() == mFFTWidth )
{
setSelected( true );
}
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent arg0 )
{
mDFTProcessor.setFFTSize( mFFTWidth );
}
} );
}
}

View File

@@ -0,0 +1,40 @@
package spectrum.menu;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JCheckBoxMenuItem;
import spectrum.DFTProcessor;
import dsp.filter.Window.WindowType;
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,39 @@
package spectrum.menu;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JCheckBoxMenuItem;
import spectrum.DFTProcessor;
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 );
}
} );
}
}