Initial code check in continued

This commit is contained in:
dennis.sheirer
2014-04-26 19:48:24 +00:00
parent 960a6f9952
commit 3dfedcf8d1
474 changed files with 76613 additions and 0 deletions

4
src/spectrum/.directory Normal file
View File

@@ -0,0 +1,4 @@
[Dolphin]
Timestamp=2014,2,4,19,35,46
Version=3
ViewMode=1

View File

@@ -0,0 +1,546 @@
/*******************************************************************************
* 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.util.Arrays;
import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import log.Log;
import sample.Listener;
import source.Source;
import source.Source.SampleType;
import source.tuner.FrequencyChangeListener;
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 ArrayBlockingQueue<Float[]> mQueue =
new ArrayBlockingQueue<Float[]>( 200 );
private ScheduledExecutorService mScheduler =
Executors.newScheduledThreadPool(1);
private FFTWidth mFFTWidth = FFTWidth.FFT04096;
private FFTWidth mNewFFTWidth = FFTWidth.FFT04096;
private double[] mWindow;
private WindowType mWindowType = Window.WindowType.HANNING;
private FloatFFT_1D mFFT = new FloatFFT_1D( mFFTWidth.getWidth() );
private int mFrameRate;
private int mSampleRate;
private float mOverlapPerFrame;
private float mOverlapResidual;
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 );
setFrameRate( 20 );
}
public void dispose()
{
try
{
clearScheduler();
}
catch ( InterruptedException e )
{
Log.error( "DFTProcessor - exception shutting down processor - " +
e.getLocalizedMessage() );
}
mScheduler.shutdown();
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,
mFFTWidth.getWidth() * 2 );
}
else
{
mWindow = Window.getWindow( mWindowType,
mFFTWidth.getWidth() );
}
}
/**
* 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 Source.SampleType getSampleType()
{
return mSampleType;
}
/**
* Queues an FFT size change request. The scheduled executor will apply
* the change when it runs.
*/
public void setFFTSize( FFTWidth width )
{
mNewFFTWidth = width;
}
public FFTWidth getFFTWidth()
{
return mFFTWidth;
}
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 );
}
else
{
mFrameRate = framesPerSecond;
calculateOverlap();
/**
* 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" );
}
/**
* 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 );
}
}
/**
* Shuts down the scheduler and blocks up to 100 milliseconds until all
* tasks have completed.
*/
private void clearScheduler() throws InterruptedException
{
mScheduler.shutdown();
mScheduler.awaitTermination( 100, TimeUnit.MILLISECONDS );
}
public int getCalculationsPerSecond()
{
return mFrameRate;
}
/**
* Places the sample into a transfer queue for future processing.
*/
@Override
public void receive( Float[] samples )
{
if( !mQueue.offer( samples ) )
{
Log.error( "DFTProcessor - queue is full, purging queue, samples[" + samples + "]" );
mQueue.clear();
mQueue.offer( samples );
}
}
private void getNextBuffer()
{
mCurrentBuffer = null;
long start = System.currentTimeMillis();
try
{
mCurrentBuffer = mQueue.take();
}
catch ( InterruptedException e )
{
mCurrentBuffer = null;
}
mCurrentBufferPointer = 0;
}
private float[] getSamples()
{
int remaining;
if( mSampleType == SampleType.COMPLEX )
{
remaining = mFFTWidth.getWidth() * 2;
}
else
{
remaining = mFFTWidth.getWidth();
}
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 )
{
int previousFramePointer;
int length;
if( mSampleType == SampleType.COMPLEX )
{
length = overlap * 2;
previousFramePointer = mPreviousFrame.length - length;
}
else
{
length = overlap;
previousFramePointer = (int)(mPreviousFrame.length / 2) - length;
}
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 );
}
}
while( mRunning.get() && remaining > 0 )
{
if( mCurrentBuffer == null ||
mCurrentBufferPointer == mCurrentBuffer.length )
{
getNextBuffer();
}
int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer;
if( samplesAvailable >= remaining )
{
while( remaining > 0 )
{
currentFrame[ currentFramePointer ] =
(float)mCurrentBuffer[ mCurrentBufferPointer ];
currentFramePointer++;
mCurrentBufferPointer++;
remaining --;
}
mCurrentBufferPointer += remaining;
remaining = 0;
}
else
{
while( samplesAvailable > 0 )
{
currentFrame[ currentFramePointer ] =
(float)mCurrentBuffer[ mCurrentBufferPointer ];
currentFramePointer++;
mCurrentBufferPointer++;
samplesAvailable--;
remaining--;
}
}
}
/* If frames are overlapping, store current frame to use next time */
if( mOverlapPerFrame > 0 )
{
mPreviousFrame = null;
mPreviousFrame = Arrays.copyOf( currentFrame, currentFrame.length );
}
return currentFrame;
}
private void calculate()
{
float[] samples = getSamples();
Window.apply( mWindow, samples );
if( mSampleType == SampleType.FLOAT )
{
mFFT.realForwardFull( 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();
}
int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer;
if( samplesAvailable >= samplesToPurge )
{
mCurrentBufferPointer += samplesToPurge;
samplesToPurge = 0;
}
else
{
samplesToPurge -= samplesAvailable;
mCurrentBufferPointer = mCurrentBuffer.length;
}
}
}
/**
* Takes a calculated DFT results set, reformats the data, and sends it
* out to all registered listeners.
*/
private void dispatch( 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 ];
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 );
}
}
results = null;
}
@Override
public void addListener( DFTResultsListener listener )
{
mListeners.add( listener );
}
@Override
public void removeListener( DFTResultsListener listener )
{
mListeners.remove( listener );
}
private class DFTCalculationTask implements Runnable
{
@Override
public void run()
{
/* Only run if we're not currently running */
if( mRunning.compareAndSet( false, true ) )
{
checkFFTSize();
calculate();
mRunning.set( false );
}
else
{
Log.error( "DFTProcessor - calculation already in progress"
+ " - skipping this iteration" );
}
}
}
/**
* 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( mNewFFTWidth.getWidth() != mFFTWidth.getWidth() )
{
mFFTWidth = mNewFFTWidth;
calculateOverlap();
mWindow = Window.getWindow( WindowType.HANNING,
( mFFTWidth.getWidth() * 2 ) );
mPreviousFrame = new float[ mFFTWidth.getWidth() * 2 ];
mFFT = new FloatFFT_1D( mFFTWidth.getWidth() );
}
}
public void clearBuffer()
{
mQueue.clear();
}
@Override
public void frequencyChanged( int frequency, int bandwidth )
{
mSampleRate = bandwidth;
calculateOverlap();
}
/**
* 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()
{
mOverlapPerFrame = 0.0f;
mOverlapResidual = 0.0f;
int requiredSampleRate = mFFTWidth.getWidth() * mFrameRate;
int overlap = requiredSampleRate - mSampleRate;
mOverlapPerFrame = (float)overlap / (float)mFrameRate;
}
}

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 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 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 spectrum;
public enum FFTWidth
{
FFT00512( 512 ),
FFT01024( 1024 ),
FFT02048( 2048 ),
FFT04096( 4096 ),
FFT08192( 8192 ),
FFT16384( 16384 ),
FFT32768( 32768 );
private int mWidth;
private FFTWidth( int width )
{
mWidth = width;
}
public int getWidth()
{
return mWidth;
}
public String getLabel()
{
return String.valueOf( mWidth );
}
}

View File

@@ -0,0 +1,590 @@
/*******************************************************************************
* 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.Font;
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 java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.JPanel;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting;
import settings.SettingChangeListener;
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;
public class OverlayPanel extends JPanel
implements ChannelListener,
FrequencyChangeListener,
SettingChangeListener
{
private static final long serialVersionUID = 1L;
private static DecimalFormat sFORMAT = new DecimalFormat( "000.000" );
private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" );
private int mFrequency = 0;
private int mBandwidth = 0;
private Point mCursorLocation = new Point( 0, 0 );
private boolean mCursorVisible = false;
/**
* Colors used by this component
*/
private Color mColorChannelConfig;
private Color mColorChannelConfigProcessing;
private Color mColorChannelConfigSelected;
private Color mColorSpectrumBackground;
private Color mColorSpectrumCursor;
private Color mColorSpectrumLine;
//All channels
private ArrayList<Channel> mChannels = new ArrayList<Channel>();
//Currently visible/displayable channels
private CopyOnWriteArrayList<Channel> mVisibleChannels =
new CopyOnWriteArrayList<Channel>();
//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 OverlayPanel( 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()
{
mChannels.clear();
mVisibleChannels.clear();
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()
{
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 =
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 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;
}
}
}
/**
* 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 );
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 = sCURSOR_FORMAT.format(
getFrequencyFromAxis( mCursorLocation.getX() ) / 1000000.0D );
graphics.drawString( frequency ,
mCursorLocation.x + 5,
mCursorLocation.y );
}
}
/**
* Draws the frequency lines and labels every 10kHz
*/
private void drawFrequencies( Graphics2D graphics )
{
int minFrequency = getMinFrequency();
int maxFrequency = getMaxFrequency();
//TODO: the increment should be changeable according to the overall bandwidth
// so that when we move to higher sample rates, the display reacts accordingly
int frequency;
if( mBandwidth < 200000 )
{
//Start with the first diplayable frequency (ie whole 10kHz increment)
frequency = minFrequency - ( minFrequency % 10000 );
frequency += 10000;
//Draw a line and label at each 10kHz interval
while( frequency < maxFrequency )
{
drawFrequencyLineAndLabel( graphics, frequency );
frequency += 10000;
}
}
else
{
//Start with the first diplayable frequency (ie whole 100kHz increment)
frequency = minFrequency - ( minFrequency % 100000 );
frequency += 100000;
//Draw a line and label at each 10kHz interval
while( frequency < maxFrequency )
{
drawFrequencyLineAndLabel( graphics, frequency );
frequency += 100000;
}
}
}
/**
* Draws a vertical line and a corresponding frequency label at the bottom
*/
private void drawFrequencyLineAndLabel( Graphics2D graphics, int frequency )
{
float xAxis = getAxisFromFrequency( frequency );
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 ) );
}
/**
* Returns the x-axis value corresponding to the frequency
*/
public float getAxisFromFrequency( int frequency )
{
float canvasMiddle = (float)getSize().getWidth() / 2;
//Determine frequency offset from middle
int frequencyOffset = mFrequency - frequency;
//Determine ratio of offset to bandwidth
float ratio = (float)frequencyOffset / (float)mBandwidth;
//Calculate offset against the total width
float xOffset = (float)getSize().getWidth() * ratio;
//Apply the offset against the canvas middle
return canvasMiddle - xOffset;
}
/**
* Returns the frequency corresponding to the x-axis value
*/
public int getFrequencyFromAxis( double xAxis )
{
float width = (float)getSize().getWidth();
double offset = xAxis / width;
return getMinFrequency() + (int)( mBandwidth * offset );
}
/**
* Draws a frequency label at the x-axis position, at the bottom of the panel
*/
private void drawFrequencyLabel( Graphics2D graphics,
float xaxis,
int frequency )
{
String label = sFORMAT.format( (float)frequency / 1000000.0f );
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 ) );
}
/**
* Draws visible channel configs as translucent shaded frequency regions
*/
private void drawChannels( Graphics2D graphics )
{
for( Channel channel: mVisibleChannels )
{
//Choose the correct background color to use
if( channel.getSelected() )
{
graphics.setColor( mColorChannelConfigSelected );
}
else if( channel.isProcessing() )
{
graphics.setColor( mColorChannelConfigProcessing );
}
else
{
graphics.setColor( mColorChannelConfig );
}
TunerChannel tunerChannel = channel.getTunerChannel();
if( tunerChannel != null )
{
float xAxis = getAxisFromFrequency(
(int)( tunerChannel.getFrequency() ) );
float width = (float)( tunerChannel.getBandwidth() ) /
(float)mBandwidth * (float)getSize().getWidth();
Rectangle2D.Float box =
new Rectangle2D.Float( xAxis - ( width / 2 ),
0,
width,
(float)( 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
float yAxis = 0;
//Draw the system label and adjust the y-axis position
yAxis += drawLabel( graphics,
channel.getSystem().getName(),
this.getFont(),
xAxis,
yAxis,
width );
//Draw the site label and adjust the y-axis position
yAxis += drawLabel( graphics,
channel.getSite().getName(),
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 );
}
}
}
/**
* 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 float drawLabel( Graphics2D graphics, String text, Font font,
float x, float baseY, float maxWidth )
{
FontMetrics fontMetrics = graphics.getFontMetrics( font );
Rectangle2D label = fontMetrics.getStringBounds( text, graphics );
float offset = (float)label.getWidth() / 2;
float y = baseY + (float)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 ) )
{
label.setRect( x - ( maxWidth / 2 ),
y - label.getHeight(),
maxWidth,
label.getHeight() ); //* 2
graphics.setClip( label );
graphics.drawString( text, x - ( maxWidth / 2 ), y );
graphics.setClip( null );
}
else
{
graphics.drawString( text, x - offset, y );
}
return (float)label.getHeight();
}
/**
* Frequency change event handler
*/
@Override
public void frequencyChanged( int frequency, int bandwidth )
{
mBandwidth = bandwidth;
mFrequency = frequency;
/**
* Reset the visible channel configs list
*/
mVisibleChannels.clear();
int minimum = getMinFrequency();
int maximum = getMaxFrequency();
for( Channel channel: mChannels )
{
if( channel.isWithin( minimum, maximum ) )
{
mVisibleChannels.add( channel );
}
}
}
/**
* Channel change event handler
*/
@Override
@SuppressWarnings( "incomplete-switch" )
public void occurred( Channel channel, ChannelEvent component )
{
switch( component )
{
case CHANNEL_ADDED:
if( !mChannels.contains( channel ) )
{
mChannels.add( channel );
}
if( channel.isWithin( getMinFrequency(), getMaxFrequency() ) &&
!mVisibleChannels.contains( channel ) )
{
mVisibleChannels.add( channel );
}
break;
case CHANNEL_DELETED:
mChannels.remove( channel );
mVisibleChannels.remove( channel );
break;
case PROCESSING_STOPPED:
if( channel.getChannelType() == ChannelType.TRAFFIC )
{
mChannels.remove( channel );
mVisibleChannels.remove( channel );
}
break;
}
repaint();
}
/**
* Currently displayed minimum frequency
*/
private int getMinFrequency()
{
return mFrequency - ( mBandwidth / 2 );
}
/**
* Currently displayed maximum frequency
*/
private int getMaxFrequency()
{
return mFrequency + ( mBandwidth / 2 );
}
/**
* Returns a list of channel configs that contain the frequency within their
* min/max frequency settings.
*/
public ArrayList<Channel> getChannelsAtFrequency( int 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 )
{
// TODO Auto-generated method stub
}
}

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 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,785 @@
/*******************************************************************************
* 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.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
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.MouseListener;
import java.awt.event.MouseMotionListener;
import java.text.DecimalFormat;
import java.util.ArrayList;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JLayeredPane;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import log.Log;
import net.miginfocom.swing.MigLayout;
import sample.Listener;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.ColorSettingMenuItem;
import settings.Setting;
import settings.SettingChangeListener;
import settings.SettingsManager;
import source.Source.SampleType;
import source.SourceException;
import source.tuner.FrequencyChangeListener;
import source.tuner.Tuner;
import source.tuner.TunerSelectionListener;
import com.jidesoft.swing.JideSplitPane;
import controller.ResourceManager;
import controller.channel.Channel;
import controller.channel.Channel.ChannelEvent;
import controller.channel.ChannelListener;
import decode.DecoderType;
import dsp.filter.Window.WindowType;
/**
* Spectral Display Panel - comprises a DFTProcessor coupled with a waterfall
* display and a spectrum display, with an overlay of frequency data and channel
* data.
*
* Processes float or complex sample data and displays the frequency content
* in the spectral and waterfall displays.
*/
public class SpectralDisplayPanel extends JPanel
implements ChannelListener,
Listener<Float[]>,
FrequencyChangeListener,
SettingChangeListener,
TunerSelectionListener
{
private static final long serialVersionUID = 1L;
private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" );
/**
* Colors used by this component
*/
private Color mColorChannelConfig;
private Color mColorChannelConfigProcessing;
private Color mColorChannelConfigSelected;
private Color mColorSpectrumBackground;
private Color mColorSpectrumCursor;
private Color mColorSpectrumGradientBottom;
private Color mColorSpectrumGradientTop;
private Color mColorSpectrumLine;
private JLayeredPane mLayeredPanel;
private SpectrumPanel mSpectrumPanel;
private WaterfallPanel mWaterfallPanel;
private OverlayPanel mChannelPanel;
private DFTProcessor mDFTProcessor;
private ResourceManager mResourceManager;
private Tuner mTuner;
public SpectralDisplayPanel( ResourceManager resourceManager )
{
mResourceManager = resourceManager;
setColors();
init();
}
public void dispose()
{
/* De-register from receiving samples when the window closes */
clearTuner();
mResourceManager.getChannelManager().removeListener( this );
mDFTProcessor.dispose();
mDFTProcessor = null;
mSpectrumPanel.dispose();
mSpectrumPanel = null;
mWaterfallPanel.dispose();
mWaterfallPanel = null;
mChannelPanel.dispose();
mChannelPanel = null;
mResourceManager = null;
mTuner = null;
}
/**
* 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 );
mColorSpectrumGradientBottom =
getColor( ColorSettingName.SPECTRUM_GRADIENT_BOTTOM );
mColorSpectrumGradientTop =
getColor( ColorSettingName.SPECTRUM_GRADIENT_TOP );
}
/**
* 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();
}
/**
* Stores the named color setting
*/
private void setColor( ColorSettingName name, Color color, int translucency )
{
Color adjustedColor = ColorSetting.getTranslucent( color, translucency );
mResourceManager.getSettingsManager()
.setColorSetting( name, adjustedColor );
}
/**
* 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( mChannelPanel != null )
{
mChannelPanel.settingChanged( setting );
}
if( mWaterfallPanel != null )
{
mWaterfallPanel.settingChanged( setting );
}
if( mSpectrumPanel != null )
{
mSpectrumPanel.settingChanged( setting );
}
if( setting instanceof ColorSetting )
{
ColorSetting colorSetting = (ColorSetting)setting;
switch( ((ColorSetting)setting).getColorSettingName() )
{
case CHANNEL_CONFIG:
mColorChannelConfig = colorSetting.getColor();
break;
case CHANNEL_CONFIG_PROCESSING:
mColorChannelConfigProcessing = colorSetting.getColor();
break;
case CHANNEL_CONFIG_SELECTED:
mColorChannelConfigSelected = colorSetting.getColor();
break;
case SPECTRUM_BACKGROUND:
mColorSpectrumBackground = colorSetting.getColor();
break;
case SPECTRUM_CURSOR:
mColorSpectrumCursor = colorSetting.getColor();
break;
case SPECTRUM_GRADIENT_BOTTOM:
mColorSpectrumGradientBottom = colorSetting.getColor();
break;
case SPECTRUM_GRADIENT_TOP:
mColorSpectrumGradientTop = colorSetting.getColor();
break;
case SPECTRUM_LINE:
mColorSpectrumLine = colorSetting.getColor();
break;
}
}
}
/**
* 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]") );
//Register for any future settings (ie color) changes
mResourceManager.getSettingsManager().addListener( this );
/**
* 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();
//Create the spectrum panel
mSpectrumPanel = new SpectrumPanel( mResourceManager );
//Create the channel panel
mChannelPanel = new OverlayPanel( mResourceManager );
mChannelPanel.addMouseListener( mouser );
mChannelPanel.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 );
//Create the waterfall
mWaterfallPanel = new WaterfallPanel( mResourceManager );
mWaterfallPanel.addMouseListener( mouser );
mWaterfallPanel.addMouseMotionListener( 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 );
add( splitPane, "grow" );
/**
* Setup DFTProcessor to process samples and register the waterfall and
* spectrum panel to receive the processed dft results
*/
mDFTProcessor = new DFTProcessor( SampleType.COMPLEX );
mDFTProcessor.addListener( (DFTResultsListener)mSpectrumPanel );
mDFTProcessor.addListener( (DFTResultsListener)mWaterfallPanel );
}
/**
* Receives frequency change events -- primarily from tuner components.
*/
@Override
public void frequencyChanged( int frequency, int bandwidth )
{
mChannelPanel.frequencyChanged( frequency, bandwidth );
mDFTProcessor.frequencyChanged( frequency, bandwidth );
}
/**
* Channel config change events
*/
@Override
public void occurred( Channel config, ChannelEvent component )
{
mChannelPanel.occurred( config, component );
}
@Override
public void receive( Float[] samples )
{
mDFTProcessor.receive( samples );
}
/**
* Tuner selected events
*/
@Override
public void tunerSelected( Tuner tuner )
{
clearTuner();
mDFTProcessor.clearBuffer();
mTuner = tuner;
if( mTuner != null )
{
//Register to receive frequency change events
mTuner.addListener( (FrequencyChangeListener)this );
//Register the dft processor to receive samples from the tuner
mTuner.addListener( (Listener<Float[]>)mDFTProcessor );
//Fire a frequency change event so that everyone can init
try
{
frequencyChanged( mTuner.getFrequency(),
mTuner.getSampleRate() );
}
catch ( SourceException e )
{
Log.info( "DFTProcessor - exception during new tuner setup - "
+ "couldn't get frequency from the tuner" );
}
}
}
/**
* Tuner de-selection cleanup method
*/
public void clearTuner()
{
if( mTuner != null )
{
//Deregister for frequency change events from the tuner
mTuner.removeListener( (FrequencyChangeListener)this );
//Deregister the dft processor from receiving samples
mTuner.removeListener( (Listener<Float[]>)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() );
mChannelPanel.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() == mChannelPanel )
{
mChannelPanel.setCursorLocation( event.getPoint() );
}
else
{
mWaterfallPanel.setCursorLocation( event.getPoint() );
mWaterfallPanel.setCursorFrequency(
mChannelPanel.getFrequencyFromAxis( event.getPoint().x ) );
}
}
@Override
public void mouseEntered( MouseEvent e )
{
if( e.getComponent() == mChannelPanel )
{
mChannelPanel.setCursorVisible( true );
}
else
{
mWaterfallPanel.setCursorVisible( true );
}
}
@Override
public void mouseExited( MouseEvent e )
{
mChannelPanel.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" ) );
}
int frequency =
mChannelPanel.getFrequencyFromAxis( event.getX() );
if( event.getComponent() == mChannelPanel )
{
ArrayList<Channel> channels =
mChannelPanel.getChannelsAtFrequency( frequency );
for( Channel channel: channels )
{
JMenu menu = channel.getContextMenu();
if( menu != null )
{
contextMenu.add( menu );
}
}
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.getAvailableDecoders() )
{
decoderMenu.add( new DecoderItem( mResourceManager,
frequency,
type ) );
}
frequencyMenu.add( decoderMenu );
contextMenu.add( frequencyMenu );
contextMenu.add( new JSeparator() );
/**
* Color Menus
*/
JMenu colorMenu = new JMenu( "Color" );
SettingsManager sm = mResourceManager.getSettingsManager();
colorMenu.add( new ColorSettingMenuItem( sm,
ColorSettingName.CHANNEL_CONFIG ) );
colorMenu.add( new ColorSettingMenuItem( sm,
ColorSettingName.CHANNEL_CONFIG_PROCESSING ) );
colorMenu.add( new ColorSettingMenuItem( sm,
ColorSettingName.CHANNEL_CONFIG_SELECTED ) );
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 );
/**
* 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 )
{
if( event.getComponent() == mChannelPanel )
{
contextMenu.show( mChannelPanel,
event.getX(),
event.getY() );
}
else
{
contextMenu.show( mWaterfallPanel,
event.getX(),
event.getY() );
}
}
}
}
@Override
public void mousePressed( MouseEvent e ) {}
@Override
public void mouseReleased( MouseEvent e ) {}
}
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 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
* processing with the selected decoder
*/
public class DecoderItem extends JMenuItem
{
private static final long serialVersionUID = 1L;
private ResourceManager mResourceManager;
private int mFrequency;
private DecoderType mDecoder;
public DecoderItem( ResourceManager manager, int frequency, DecoderType type )
{
super( type.getDisplayString() );
mResourceManager = manager;
mFrequency = frequency;
mDecoder = type;
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
mResourceManager.getController()
.createChannel( mFrequency, mDecoder );
}
} );
}
}
@Override
public void settingDeleted( Setting setting ) {}
}

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 spectrum;
import java.awt.EventQueue;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import javax.swing.JFrame;
import net.miginfocom.swing.MigLayout;
import source.tuner.Tuner;
import controller.ResourceManager;
public class SpectrumFrame extends JFrame implements WindowListener
{
private static final long serialVersionUID = 1L;
private ResourceManager mResourceManager;
private SpectralDisplayPanel mSpectralDisplayPanel;
public SpectrumFrame( ResourceManager resourceManager, Tuner tuner )
{
mResourceManager = resourceManager;
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( resourceManager );
mSpectralDisplayPanel.tunerSelected( tuner );
add( mSpectralDisplayPanel, "grow" );
/* Register spectral display on channel manager to get channel updates */
mResourceManager.getChannelManager().addListener( mSpectralDisplayPanel );
/* 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,257 @@
/*******************************************************************************
* 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.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import javax.swing.JPanel;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting;
import settings.SettingChangeListener;
import buffer.FloatArrayCircularAveragingBuffer;
import controller.ResourceManager;
public class SpectrumPanel extends JPanel
implements DFTResultsListener,
SettingChangeListener
{
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 );
/**
* 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 Color mColorSpectrumBackground;
private Color mColorSpectrumGradientTop;
private Color mColorSpectrumGradientBottom;
private Color mColorSpectrumLine;
private float mSpectrumInset = 20.0f;
private ResourceManager mResourceManager;
public SpectrumPanel( ResourceManager resourceManager )
{
mResourceManager = resourceManager;
setColors();
}
public void dispose()
{
mResourceManager = null;
}
private void setColors()
{
mColorSpectrumBackground =
getColor( ColorSettingName.SPECTRUM_BACKGROUND );
mColorSpectrumGradientBottom =
getColor( ColorSettingName.SPECTRUM_GRADIENT_BOTTOM );
mColorSpectrumGradientTop =
getColor( ColorSettingName.SPECTRUM_GRADIENT_TOP );
mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE );
}
private Color getColor( ColorSettingName name )
{
ColorSetting setting =
mResourceManager.getSettingsManager().getColorSetting( name );
return setting.getColor();
}
@Override
public void settingChanged( Setting setting )
{
if( setting instanceof ColorSetting )
{
ColorSetting colorSetting = (ColorSetting)setting;
switch( ( (ColorSetting) setting ).getColorSettingName() )
{
case SPECTRUM_BACKGROUND:
mColorSpectrumBackground = colorSetting.getColor();
break;
case SPECTRUM_GRADIENT_BOTTOM:
mColorSpectrumGradientBottom = colorSetting.getColor();
break;
case SPECTRUM_GRADIENT_TOP:
mColorSpectrumGradientTop = colorSetting.getColor();
break;
case SPECTRUM_LINE:
mColorSpectrumLine = colorSetting.getColor();
break;
}
}
}
/**
* DFTResultsListener interface method - for receiving the processed data
* to display
*/
public void receive( float[] currentFFTBins )
{
//Construct and/or resize our fft results variables
if( mDisplayFFTBins == null ||
mDisplayFFTBins.length != currentFFTBins.length )
{
mDisplayFFTBins = new float[ currentFFTBins.length ];
}
/**
* Store the new FFT bins in the buffer and get the average of the FFT
* bin buffer contents into the display fft bins variable
*/
mDisplayFFTBins = mFFTAveragingBuffer.get( currentFFTBins );
repaint();
}
@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 );
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,
0,
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 );
//If we have FFT data to display ...
if( mDisplayFFTBins != null )
{
float insideHeight = size.height - mSpectrumInset;
float insideWidth = size.width;
float binSize = insideWidth / ( mDisplayFFTBins.length - 1 );
float heightMultiplier = (float)( insideHeight * .15 );
float spectrumMiddle = insideHeight * .23f;
for( int x = 0; x < mDisplayFFTBins.length; x++ )
{
float height = spectrumMiddle - ( mDisplayFFTBins[ x ] * heightMultiplier );
if( height > size.getHeight() - mSpectrumInset )
{
height = (float)size.getHeight() - mSpectrumInset;
}
spectrumShape.lineTo( binSize * x, 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 ) );
}
@Override
public void settingDeleted( Setting setting ) {}
}

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 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,287 @@
/*******************************************************************************
* 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.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Line2D;
import java.awt.image.ColorModel;
import java.awt.image.MemoryImageSource;
import java.text.DecimalFormat;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JPanel;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting;
import settings.SettingChangeListener;
import buffer.FloatAveragingBuffer;
import controller.ResourceManager;
/**
* Produces a JPanel filled with a waterfall image derived from a continuous
* stream of received Discrete Fourier Transform (DFT) results from the
* DFTProcessor class.
*/
public class WaterfallPanel extends JPanel implements DFTResultsListener,
Pausable,
SettingChangeListener
{
private static final long serialVersionUID = 1L;
private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" );
private static final String sSPECTRUM_CURSOR = "spectrum_cursor";
private static final String sPAUSED = "PAUSED";
private byte[] mPixels;
private int mFFTSize = 1024;
private int mImageHeight = 700;
private MemoryImageSource mMemoryImageSource;
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 Point mCursorLocation = new Point( 0, 0 );
private boolean mCursorVisible = false;
private int mCursorFrequency = 0;
private AtomicBoolean mPaused = new AtomicBoolean();
private ResourceManager mResourceManager;
private FloatAveragingBuffer mIntraBinAveragingBuffer = new FloatAveragingBuffer( 9 );
public WaterfallPanel( ResourceManager resourceManager )
{
super();
mResourceManager = resourceManager;
mColorSpectrumCursor = getColor( ColorSettingName.SPECTRUM_CURSOR );
reset();
}
public void dispose()
{
mResourceManager = null;
mMemoryImageSource = null;
}
private void reset()
{
mPixels = new byte[ mFFTSize * mImageHeight ];
mMemoryImageSource = new MemoryImageSource( mFFTSize,
mImageHeight,
mColorModel,
mPixels,
0,
mFFTSize );
mMemoryImageSource.setAnimated( true );
mWaterfallImage = createImage( mMemoryImageSource );
}
/**
* Pausable interface - pauses updates to the screen
*/
public void setPaused( boolean paused )
{
mPaused.set( paused );
repaint();
}
public boolean isPaused()
{
return mPaused.get();
}
/**
* 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;
if( ( (ColorSetting) setting ).getColorSettingName() ==
ColorSettingName.SPECTRUM_CURSOR )
{
mColorSpectrumCursor = colorSetting.getColor();
}
}
}
public void setCursorLocation( Point point )
{
mCursorLocation = point;
repaint();
}
public void setCursorFrequency( int frequency )
{
mCursorFrequency = frequency;
}
public void setCursorVisible( boolean visible )
{
mCursorVisible = visible;
repaint();
}
public void paintComponent( Graphics g )
{
g.drawImage( mWaterfallImage, 0, 0, getWidth(), mImageHeight, this );
Graphics2D graphics = (Graphics2D) g;
if( mCursorVisible )
{
graphics.setColor( mColorSpectrumCursor );
graphics.draw( new Line2D.Float( mCursorLocation.x,
0,
mCursorLocation.x,
(float)(getSize().getHeight() ) ) );
String frequency = sCURSOR_FORMAT.format( mCursorFrequency / 1000000.0D );
graphics.drawString( frequency ,
mCursorLocation.x + 5,
mCursorLocation.y );
}
if( mPaused.get() )
{
graphics.drawString( sPAUSED, 20, 20 );
}
graphics.dispose();
}
@Override
public void receive( float[] update )
{
//If our FFT size changes, reset our pixel map and image source
if( mFFTSize != update.length )
{
mFFTSize = update.length;
reset();
}
//Move the pixels down a row to make room for the new results
System.arraycopy( mPixels, 0,
mPixels, mFFTSize, mPixels.length - mFFTSize );
//Convert the power spectrum bin values into a byte (0-255)
//value to use with the color mapping
double averagingTotal = 0.0;
for( int x = 0; x < update.length; 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 ) );
}
//Update the average value
mAverage = (float) averagingTotal / update.length;
if( !mPaused.get() )
{
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
if( mMemoryImageSource != null )
{
mMemoryImageSource.newPixels( mPixels, mColorModel, 0, mFFTSize );
}
}
} );
}
}
public class Handler implements MouseListener
{
@Override
public void mouseClicked( MouseEvent e )
{
//Display context menu to reset the display reset();
}
public void mousePressed( MouseEvent e ) {}
public void mouseReleased( MouseEvent e ) {}
public void mouseEntered( MouseEvent e ) {}
public void mouseExited( MouseEvent e ){}
}
@Override
public void settingDeleted( Setting setting ) {}
}