Adds airspy USB tuner support and updates the spectral display

component.

-Airspy supported with 10 and 2.5 MSps sample rates.  Additional
decimation may be added later.

-Spectral display updated to include smoothing and filtering options.

Note: the decimated output of a tuner channel from the airspy is
48,076.9 samples per second, versus the normal 48 kHz decimated rate.
This is 1.0015 times the normal rate and doesn't appear to impact the
decoders or audio processing.  Therefore, no fractional resampling has
been added.
This commit is contained in:
denny
2015-10-30 20:49:22 -04:00
parent 5a91630996
commit 70e62ebb35
80 changed files with 4781 additions and 1014 deletions

View File

@@ -20,7 +20,6 @@ import settings.Setting;
import settings.SettingChangeListener;
import settings.SettingsManager;
import alias.Alias;
import audio.AudioEvent.Type;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;

View File

@@ -1,6 +1,25 @@
package audio;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;

View File

@@ -18,7 +18,6 @@ import audio.squelch.SquelchState;
import controller.channel.ChannelEvent;
import controller.channel.IChannelEventListener;
import dsp.filter.Filters;
import dsp.filter.dc.AveragingDCRemovalFilter_RB;
import dsp.filter.dc.DCRemovalFilter_RB;
import dsp.filter.fir.real.RealFIRFilter_R_R;
@@ -73,7 +72,7 @@ public class AudioModule extends Module implements IAudioPacketProvider,
if( mRemoveDC )
{
mDCRemovalFilter = new AveragingDCRemovalFilter_RB( DC_REMOVAL_RATIO );
mDCRemovalFilter = new DCRemovalFilter_RB( DC_REMOVAL_RATIO );
mDCRemovalFilter.setListener( this );
}
}

View File

@@ -17,10 +17,10 @@
******************************************************************************/
package audio.inverted;
import util.Oscillator;
import dsp.filter.FilterFactory;
import dsp.filter.FloatFIRFilter;
import dsp.filter.Window.WindowType;
import dsp.mixer.Oscillator;
/**
* Based on the AudioType applied, produces normal, muted, or audio inverted

View File

@@ -19,9 +19,9 @@ package audio.inverted;
import audio.InversionFrequency;
import sample.real.RealSampleListener;
import util.Oscillator;
import dsp.filter.Filters;
import dsp.filter.FloatFIRFilter;
import dsp.mixer.Oscillator;
/**
* Applies audio inversion (or un-inversion) to a stream of float audio samples

View File

@@ -4,7 +4,6 @@ import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
import org.slf4j.Logger;

View File

@@ -10,7 +10,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@ import dsp.filter.Window.WindowType;
import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
import dsp.filter.halfband.complex.HalfBandFilter_CB_CB;
public class ComplexPrimeCICDecimate
public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
{
public static int[] PRIMES = { 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,
59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,
@@ -163,6 +163,7 @@ public class ComplexPrimeCICDecimate
/**
* Primary input method for receiving sample arrays composed as I,Q,I,Q, etc.
*/
@Override
public void receive( ComplexBuffer buffer )
{
if( mFirstDecimatingStage != null )
@@ -343,9 +344,8 @@ public class ComplexPrimeCICDecimate
/**
* Output adapter - applies gain correction, applies cleanup filter, casts
* double-valued samples into float-valued complex sample and sends the
* result to the registered listener.
* Output adapter - applies gain correction and cleanup filter and broadcast
* to registered listener.
*/
public class Output implements ComplexSampleListener
{

View File

@@ -17,18 +17,16 @@
******************************************************************************/
package dsp.filter.dc;
public abstract class DCRemovalFilter
public class DCRemovalFilter
{
private float mAverage;
private float mRatio;
protected float mAverage;
protected float mRatio;
public DCRemovalFilter( float ratio )
{
mRatio = ratio;
}
public abstract void dispose();
public void reset()
{
mAverage = 0.0f;
@@ -36,8 +34,8 @@ public abstract class DCRemovalFilter
public float filter( float sample )
{
mAverage += mRatio * ( sample - mAverage );
return sample - mAverage;
float filtered = sample - mAverage;
mAverage += mRatio * filtered;
return filtered;
}
}

View File

@@ -18,17 +18,18 @@
package dsp.filter.dc;
import sample.Listener;
import sample.real.RealBuffer;
import sample.complex.ComplexBuffer;
public class AveragingDCRemovalFilter_RB extends DCRemovalFilter_RB
implements Listener<RealBuffer>
public class DCRemovalFilter_CB implements Listener<ComplexBuffer>
{
private float mAverage;
private float mRatio;
public AveragingDCRemovalFilter_RB( float ratio )
private Listener<ComplexBuffer> mListener;
private DCRemovalFilter mIFilter;
private DCRemovalFilter mQFilter;
public DCRemovalFilter_CB( float ratio )
{
mRatio = ratio;
mIFilter = new DCRemovalFilter( ratio );
mQFilter = new DCRemovalFilter( ratio );
}
public void dispose()
@@ -36,31 +37,35 @@ public class AveragingDCRemovalFilter_RB extends DCRemovalFilter_RB
mListener = null;
}
public void reset()
public float[] filter( float[] samples )
{
mAverage = 0.0f;
}
for( int x = 0; x < samples.length; x += 2 )
{
samples[ x ] = mIFilter.filter( samples[ x ] );
samples[ x + 1 ] = mQFilter.filter( samples[ x + 1 ] );
}
public float filter( float sample )
{
mAverage += mRatio * ( sample - mAverage );
return samples;
}
return sample - mAverage;
public ComplexBuffer filter( ComplexBuffer buffer )
{
filter( buffer.getSamples() );
return buffer;
}
@Override
public void receive( RealBuffer buffer )
public void receive( ComplexBuffer buffer )
{
if( mListener != null )
{
float[] samples = buffer.getSamples();
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
mListener.receive( buffer );
mListener.receive( filter( buffer ) );
}
}
public void setListener( Listener<ComplexBuffer> listener )
{
mListener = listener;
}
}

View File

@@ -3,13 +3,14 @@ package dsp.filter.dc;
import sample.Listener;
import sample.real.RealBuffer;
public abstract class DCRemovalFilter_RB implements Listener<RealBuffer>
public class DCRemovalFilter_RB extends DCRemovalFilter
implements Listener<RealBuffer>
{
protected Listener<RealBuffer> mListener;
public DCRemovalFilter_RB()
public DCRemovalFilter_RB( float ratio )
{
super( ratio );
}
public void setListener( Listener<RealBuffer> listener )
@@ -27,5 +28,34 @@ public abstract class DCRemovalFilter_RB implements Listener<RealBuffer>
mListener = null;
}
public abstract void reset();
public float[] filter( float[] samples )
{
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return samples;
}
public RealBuffer filter( RealBuffer buffer )
{
float[] samples = buffer.getSamples();
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return buffer;
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
}

View File

@@ -28,7 +28,6 @@ public class DCRemovalFilter_R_R extends DCRemovalFilter implements RealSampleLi
super( ratio );
}
@Override
public void dispose()
{
mListener = null;

View File

@@ -1,12 +1,14 @@
package dsp.filter.dc;
import sample.Listener;
import sample.real.RealBuffer;
public class IIRSinglePoleDCRemovalFilter_RB extends DCRemovalFilter_RB
public class IIRSinglePoleDCRemovalFilter_RB implements Listener<RealBuffer>
{
private float mAlpha;
private float mPreviousInput = 0.0f;
private float mPreviousOutput = 0.0f;
private Listener<RealBuffer> mListener;
/**
* IIR single-pole DC removal filter, as described by J M de Freitas in
@@ -24,6 +26,14 @@ public class IIRSinglePoleDCRemovalFilter_RB extends DCRemovalFilter_RB
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
public RealBuffer filter( RealBuffer buffer )
{
float[] samples = buffer.getSamples();
@@ -31,11 +41,8 @@ public class IIRSinglePoleDCRemovalFilter_RB extends DCRemovalFilter_RB
{
samples[ x ] = filter( samples[ x ] );
}
if( mListener != null )
{
mListener.receive( buffer );
}
return buffer;
}
public float filter( float sample )
@@ -49,10 +56,14 @@ public class IIRSinglePoleDCRemovalFilter_RB extends DCRemovalFilter_RB
return currentOutput;
}
@Override
public void reset()
{
mPreviousInput = 0.0f;
mPreviousOutput = 0.0f;
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
}

View File

@@ -10,10 +10,21 @@ public class HalfBandFilter_RB_RB extends HalfBandFilter implements Listener<Rea
private Listener<RealBuffer> mListener;
/**
* Halfband filter for real buffers with optional decimation by two.
*
* @param coefficients
* @param gain
* @param decimate
* If this filter is used in non-decimating mode, the filter will overwrite
* the inbound sample buffer with the filtered samples. This will cause
* issues if the buffers are being used across several processes are sharing
* the buffer, since this filter will modify the contents of the object, so
* consider making a copy of the buffer prior to feeding the buffer to this
* filter.
*
* If used in decimating mode, a new (half-length) buffer will be created
* for the filtered samples.
*
* @param coefficients - filter kernel
* @param gain - gain to apply to the outputs
* @param decimate - true for decimate by 2, or false for no decimation
*/
public HalfBandFilter_RB_RB( float[] coefficients, float gain, boolean decimate )
{
@@ -28,12 +39,18 @@ public class HalfBandFilter_RB_RB extends HalfBandFilter implements Listener<Rea
mListener = null;
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
public RealBuffer filter( RealBuffer buffer )
{
if( mDecimate )
{
float[] samples = buffer.getSamples();
@@ -72,7 +89,7 @@ public class HalfBandFilter_RB_RB extends HalfBandFilter implements Listener<Rea
mDecimateFlag = !mDecimateFlag;
}
mListener.receive( new RealBuffer( decimated ) );
return new RealBuffer( decimated );
}
else
{
@@ -84,9 +101,8 @@ public class HalfBandFilter_RB_RB extends HalfBandFilter implements Listener<Rea
samples[ x ] = filter( samples[ x ] );
}
mListener.receive( buffer );
return buffer;
}
}
}
public void setListener( Listener<RealBuffer> listener )

View File

@@ -0,0 +1,281 @@
package dsp.filter.hilbert;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.complex.ComplexBuffer;
import sample.real.RealBuffer;
import dsp.filter.Filters;
public class HilbertTransform implements Listener<RealBuffer>
{
private static final Logger mLog = LoggerFactory.getLogger( HilbertTransform.class );
private float[] mHilbertFilter;
private Listener<ComplexBuffer> mListener;
private float[] mBuffer;
private int mBufferSize;
private int mBufferPointer;
private int[][] mIndexMap;
private int mMapHeight;
private int mCenterTapIndex;
private float mCenterCoefficent;
private boolean mInvertFlag = false;
/**
* Hilbert transform filter used for converting real-valued samples into
* complex valued samples using frequency translation (FS/4) and a half-band
* filter.
*
* This filter uses a circular sample buffer and a pre-calculated
* index map to correctly map the filter coefficients to the circular delay
* buffer contents as each new sample is added to the buffer and processed.
*
* Half-band filter coefficients used in this filter must be of length N
* where (N + 1) is a multiple of 4. This filter uses a pre-defined half
* band filter with 47 coefficients.
*
* This transform process is described in Understanding Digital Signal
* Processing, Lyons, 3e, 2011, sections 13.1.2 and 13.1.3 (p 674-678) and
* implemented as described in Section 13.37.1 and 13.37.2 (p 802-804)
*/
public HilbertTransform()
{
convertHalfBandToHilbert( Filters.HALF_BAND_FILTER_47T.getCoefficients() );
mBufferSize = mHilbertFilter.length + 1;
mBuffer = new float[ mBufferSize ];
mCenterCoefficent = mHilbertFilter[ mHilbertFilter.length / 2 ];
generateIndexMap( mHilbertFilter.length );
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
/**
* Sets the listener to receive filtered complex buffers.
*/
public void setListener( Listener<ComplexBuffer> listener )
{
mListener = listener;
}
/**
* Applies Hilbert transform to the real sample buffer and returns a
* complex sample buffer
*/
public ComplexBuffer filter( RealBuffer buffer )
{
return new ComplexBuffer( filter( buffer.getSamples() ) );
}
/**
* Filters the continuous real-sample stream by treating the stream as a
* set of interleaved values and applying two hilbert filters against each
* interleaved set (I & Q). Frequency translation has already been
* pre-applied to the hilbert filter coefficients.
*
* Filtering of the inphase component is a simple multiply by the center
* coefficient (0.5) since all of the other coefficients are zero-valued.
*
* Filtering of the quadrature component uses a folded FIR structure and
* expects the hilbert coefficients to be symmetric with all negative valued
* coefficients below the center coefficient and all positive-valued coefficients
* above the center coefficient. So, convolution only applies the filter
* coefficient against the difference of the two samples.
*
* ( coefficient * value1 ) + (-coefficient * value2 ) = coefficient * ( value1 - value2 )
*
* Performs frequency translation by FS/2 on the final filtered values by
* applying a sequence of 1,-1 (sign change) to each I/Q output sample.
*/
public float[] filter( float[] samples )
{
float accumulator;
for( int y = 0; y < samples.length; y +=2 )
{
insert( samples[ y ] );
insert( samples[ y + 1 ] );
accumulator = 0.0f;
int index = mBufferPointer / 2;
for( int x = 0; x < mHilbertFilter.length / 2; x += 2 )
{
accumulator += mHilbertFilter[ x ] *
( mBuffer[ mIndexMap[ index ][ x + 1] ] -
mBuffer[ mIndexMap[ index ][ x ] ] );
}
//Perform FS/2 frequency translation on the final filtered values
if( mInvertFlag )
{
//inphase
samples[ y ] = -( mCenterCoefficent *
mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ] );
//quadrature
samples[ y + 1 ] = -accumulator;
}
else
{
//inphase
samples[ y ] = mCenterCoefficent *
mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ];
//quadrature
samples[ y + 1 ] = accumulator;
}
mInvertFlag = !mInvertFlag;
}
return samples;
}
/**
* Inserts the sample into the circular buffer, overwriting the oldest value
*/
private void insert( float sample )
{
mBuffer[ mBufferPointer++ ] = sample;
mBufferPointer = mBufferPointer % mBufferSize;
}
/**
* Creates an index map to support efficient lookup of sample indexes from
* the circular buffer.
*
* The first index of the map corresponds to the current buffer pointer
* setting.
*
* The second index is structured in sample pairs to align with
* the hilbert filter coefficients. For example, the first iteration of
* the filter would use filter coefficient 0 and the index map values 0 and
* 1 would be pointing to samples 46 and 0.
*
* The final element of each index set is the index of the center sample.
*/
private void generateIndexMap( int size )
{
mMapHeight = size / 2 + 1;
int mapWidth = mMapHeight + 1;
mIndexMap = new int[ mMapHeight ][ mapWidth ];
//Setup the first row with buffer pointer index 0 as a starting point
for( int x = 0; x < mapWidth - 1; x += 2 )
{
mIndexMap[ 0 ][ x ] = size - 1 - x;
mIndexMap[ 0 ][ x + 1 ] = x;
}
//Place the center index at the end of the array
mCenterTapIndex = mapWidth - 1;
mIndexMap[ 0 ][ mCenterTapIndex ] = size / 2;
//For each subsequent row, increment the previous row's value by 2,
//wrapping as needed, to keep the values between 0 and size - 1
for( int x = 1; x < mMapHeight; x++ )
{
for( int y = 0; y < mapWidth; y++ )
{
mIndexMap[ x ][ y ] = mIndexMap[ x - 1 ][ y ] + 2;
if( mIndexMap[ x ][ y ] >= size )
{
mIndexMap[ x ][ y ] -= size + 1;
//Special handling for center tap wrap around
if( y == mCenterTapIndex && mIndexMap[ x ][ y ] < 0 )
{
mIndexMap[ x ][ y ] = size;
}
}
}
}
}
/**
* Logs the index map contents
*/
public void logIndexMap()
{
for( int[] indexSet: mIndexMap )
{
mLog.debug( "Row:" + Arrays.toString( indexSet ) );
}
}
/**
* Converts the half-band filter coefficients for use as hilbert transform
* filter coefficients. Sets all even-numbered coefficients left of center
* to negative and all even-numbered coefficients right of center to positive
* which applies a FS/4 frequency translation to the coefficients.
*
* Note: even though the coefficients above the center coefficient are being
* defined, they are unused during the convolution process since we're using
* a folded FIR structure to exploit the symmetric nature of the half-band
* filter coefficients. The polarity difference between the upper and lower
* halves is accounted for in the filter() method.
*/
private void convertHalfBandToHilbert( float[] coefficients )
{
mHilbertFilter = new float[ coefficients.length ];
int middle = coefficients.length / 2;
for( int x = 0; x < coefficients.length; x++ )
{
if( x < middle )
{
mHilbertFilter[ x ] = -Math.abs( coefficients[ x ] );
}
else if( x > middle )
{
mHilbertFilter[ x ] = Math.abs( coefficients[ x ] );
}
else
{
mHilbertFilter[ x ] = coefficients[ x ];
}
}
}
}

View File

@@ -0,0 +1,281 @@
package dsp.filter.hilbert;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* ----------------------------------------------------------------------------
* Half-band filter coefficients retrieved October 2015 from:
* https://github.com/airspy/host/libairspy/src/filters.h
*
* Copyright (C) 2014, Youssef Touil <youssef@airspy.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.complex.ComplexBuffer;
import sample.real.RealBuffer;
public class InterpolatingHilbertTransform implements Listener<RealBuffer>
{
private static final Logger mLog = LoggerFactory.getLogger( InterpolatingHilbertTransform.class );
private static final float[] HALF_BAND_FILTER =
{
-0.000998606272947510f, 0.0f, 0.001695637278417295f, 0.0f,
-0.003054430179754289f, 0.0f, 0.005055504379767936f, 0.0f,
-0.007901319195893647f, 0.0f, 0.011873357051047719f, 0.0f,
-0.017411159379930066f, 0.0f, 0.025304817427568772f, 0.0f,
-0.037225225204559217f, 0.0f, 0.057533286997004301f, 0.0f,
-0.102327462004259350f, 0.0f, 0.317034472508947400f, 0.5f,
0.317034472508947400f, 0.0f, -0.102327462004259350f, 0.0f,
0.057533286997004301f, 0.0f, -0.037225225204559217f, 0.0f,
0.025304817427568772f, 0.0f, -0.017411159379930066f, 0.0f,
0.011873357051047719f, 0.0f, -0.007901319195893647f, 0.0f,
0.005055504379767936f, 0.0f, -0.003054430179754289f, 0.0f,
0.001695637278417295f, 0.0f, -0.000998606272947510f
};
private static final float GAIN = 2.0f;
private Listener<ComplexBuffer> mListener;
private float[] mBuffer;
private int mBufferSize;
private int mBufferPointer;
private int mGroupDelayedInphaseTapPointIndex;
private float[] mCoefficients;
private int[][] mIndexMap;
/**
* Hilbert transform filter used for converting real-valued samples into
* complex valued samples using frequency translation (FS/4) and a half-band
* filter.
*
* This filter uses a circular sample buffer and a pre-calculated
* index map to correctly map the filter coefficients to the circular delay
* buffer contents as each new sample is added to the buffer and processed.
*
* Half-band filter coefficients used in this filter must be of length N
* where (N + 1) is a multiple of 4. This filter uses a pre-defined half
* band filter with 47 coefficients.
*
* This transform process is described in Understanding Digital Signal
* Processing, Lyons, 3e, 2011, sections 13.1.2 and 13.1.3 (p 674-678) and
* implemented as described in Section 13.37.1 and 13.37.2 (p 802-804)
*/
public InterpolatingHilbertTransform()
{
mBufferSize = HALF_BAND_FILTER.length;
mBuffer = new float[ mBufferSize ];
createHilbertCoefficients();
generateIndexMap( mBufferSize );
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
/**
* Sets the listener to receive filtered complex buffers.
*/
public void setListener( Listener<ComplexBuffer> listener )
{
mListener = listener;
}
/**
* Applies Hilbert transform to the real sample buffer and returns a
* complex sample buffer
*/
public ComplexBuffer filter( RealBuffer buffer )
{
return new ComplexBuffer( filter( buffer.getSamples() ) );
}
/**
* Applies a hilbert transform to an array of real samples and returns an
* array twice as long containing I,Q,I,Q ... complex samples
*/
public float[] filter( float[] samples )
{
float[] filtered = new float[ samples.length * 2 ];
float accumulator;
for( int y = 0; y < samples.length; y++ )
{
insert( samples[ y ] );
accumulator = 0.0f;
for( int x = 0; x < mCoefficients.length; x++ )
{
accumulator += mCoefficients[ x ] *
mBuffer[ mIndexMap[ mBufferPointer ][ x ] ] ;
}
int index = y * 2;
/* We use the buffered sample at the half group delay point as the
* Inphase value and the accumulator as the quadrature value */
filtered[ index ] = mBuffer[ mIndexMap[ mBufferPointer ]
[ mGroupDelayedInphaseTapPointIndex ] ];
filtered[ index + 1 ] = accumulator;
}
return filtered;
}
/**
* Inserts the sample into the circular buffer, overwriting the oldest value
*/
private void insert( float sample )
{
mBuffer[ mBufferPointer ] = sample;
mBufferPointer++;
mBufferPointer = mBufferPointer % mBufferSize;
}
/**
* Creates an N x (N + 1 / 2) index map enabling quick access to the
* circular buffer samples to support convolution.
*
* As the buffer pointer shifts right, as each sample is added, we use the
* index map pointed to by the buffer pointer, so that the filter coefficients
* are pointing to the correct circular buffer samples.
*
* We add an extra index to each index map with a value that points to the
* current index of the middle sample in the buffer, so that we can retrieve
* that value as the half-group delayed sample that forms the inphase
* component.
*/
private void generateIndexMap( int size )
{
int mapWidth = ( ( size + 1 ) / 2 ) + 1;
int center = ( size - 1 ) / 2;
//Set center tap index for coefficients array
mGroupDelayedInphaseTapPointIndex = center + 1;
mIndexMap = new int[ size ][ mapWidth ];
//Setup the first row with buffer pointer index 0 as a starting point
for( int x = 0; x < mapWidth - 1; x++ )
{
mIndexMap[ 0 ][ x ] = size - 1 - ( x * 2 );
}
//Place the group delayed in-phase sample tap point in last element of
//the first row
mIndexMap[ 0 ][ mGroupDelayedInphaseTapPointIndex ] = center;
//For each subsequent row, increment the previous row's value by 1,
//subtracting size as needed, to keep the values between 0 and size - 1
for( int x = 1; x < size; x++ )
{
for( int y = 0; y < mapWidth; y++ )
{
mIndexMap[ x ][ y ] = mIndexMap[ x - 1 ][ y ] + 1;
if( mIndexMap[ x ][ y ] >= size )
{
mIndexMap[ x ][ y ] -= size;
}
}
}
}
public void logIndexMap()
{
for( int[] indexSet: mIndexMap )
{
mLog.debug( "Row:" + Arrays.toString( indexSet ) );
}
}
/**
* Converts the half-band filter coefficients into a packed set of hilbert
* coefficients by removing all of the odd index, zero-valued coefficients
* and the center 0.5 coefficient. Then, all coefficients left of the
* original center are made negative and all coefficients right of the
* original center are made positive. These coefficient sign modifications
* effectively incorporate a frequency translation by FS/4 into the hilbert
* coefficients. A gain of 2.0 is applied to the coefficients to offset the
* gain of 0.5 induced by the filter.
*/
private void createHilbertCoefficients()
{
int half = ( HALF_BAND_FILTER.length + 1 ) / 2;
int middle = half / 2;
mCoefficients = new float[ half ];
/* Pack all of the even numbered HB coefficients into our coefficient
* array and pre-apply a gain of two to offset the loss in the filter */
for( int x = 0; x < half; x++ )
{
mCoefficients[ x ] = HALF_BAND_FILTER[ x * 2 ] * GAIN;
/* Make all of the left of center coefficients negative */
if( x < middle && mCoefficients[ x ] > 0.0f )
{
mCoefficients[ x ] = -mCoefficients[ x ];
}
/* Make all of the right of center coefficients positive */
else if( x >= middle && mCoefficients[ x ] < 0.0f )
{
mCoefficients[ x ] = -mCoefficients[ x ];
}
}
}
}

View File

@@ -0,0 +1,39 @@
package dsp.filter.smoothing;
import java.util.HashMap;
import java.util.Map;
public class GaussianSmoothingFilter extends SmoothingFilter
{
private static Map<Integer,float[]> mMap = new HashMap<>();
static
{
mMap.put( 3, new float[] { 0.1842153217f,0.6315693566f,0.1842153217f } );
mMap.put( 5, new float[] { 0.0646709835f,0.2446878423f,0.3812823483f,0.2446878423f,0.0646709835f } );
mMap.put( 7, new float[] { 0.0355966134f,0.1103628062f,0.2176061504f,0.27286886f,0.2176061504f,0.1103628062f,0.0355966134f } );
mMap.put( 9, new float[] { 0.0237625774f,0.061955345f,0.1228439993f,0.185233293f,0.2124095706f,0.185233293f,0.1228439993f,0.061955345f,0.0237625774f } );
mMap.put( 11, new float[] { 0.0175873706f,0.040124091f,0.0762094006f,0.120506627f,0.1586398759f,0.1738652697f,0.1586398759f,0.120506627f,0.0762094006f,0.040124091f,0.0175873706f } );
mMap.put( 13, new float[] { 0.0138646514f,0.0285342951f,0.0515030495f,0.0815279159f,0.1131845993f,0.137808478f,0.1471540217f,0.137808478f,0.1131845993f,0.0815279159f,0.0515030495f,0.0285342951f,0.0138646514f } );
mMap.put( 15, new float[] { 0.011400054f,0.0216349783f,0.0372047506f,0.0579739252f,0.0818576722f,0.1047317717f,0.1214199044f,0.1275538873f,0.1214199044f,0.1047317717f,0.0818576722f,0.0579739252f,0.0372047506f,0.0216349783f,0.011400054f } );
mMap.put( 17, new float[] { 0.0096581767f,0.0171732764f,0.0282802625f,0.0431306476f,0.0609201251f,0.07969074f,0.0965444243f,0.1083225312f,0.1125596326f,0.1083225312f,0.0965444243f,0.07969074f,0.0609201251f,0.0431306476f,0.0282802625f,0.0171732764f,0.0096581767f } );
mMap.put( 19, new float[] { 0.0083665107f,0.0141035977f,0.0223580953f,0.0333318211f,0.0467307368f,0.0616120364f,0.0763919939f,0.0890736966f,0.097672084f,0.1007188551f,0.097672084f,0.0890736966f,0.0763919939f,0.0616120364f,0.0467307368f,0.0333318211f,0.0223580953f,0.0141035977f,0.0083665107f } );
mMap.put( 21, new float[] { 0.0073728906f,0.0118884085f,0.0182292596f,0.0265811467f,0.0368585098f,0.0486027908f,0.0609458331f,0.0726751947f,0.0824114898f,0.0888686744f,0.091131604f,0.0888686744f,0.0824114898f,0.0726751947f,0.0609458331f,0.0486027908f,0.0368585098f,0.0265811467f,0.0182292596f,0.0118884085f,0.0073728906f } );
mMap.put( 23, new float[] { 0.0065861356f,0.010228416f,0.0152327635f,0.0217541246f,0.0297918261f,0.0391241787f,0.0492703835f,0.0595003129f,0.0689041144f,0.0765180086f,0.0814844543f,0.0832105633f,0.0814844543f,0.0765180086f,0.0689041144f,0.0595003129f,0.0492703835f,0.0391241787f,0.0297918261f,0.0217541246f,0.0152327635f,0.010228416f,0.0065861356f } );
mMap.put( 25, new float[] { 0.0059484873f,0.0089460248f,0.0129850288f,0.0181905149f,0.0245944051f,0.0320934871f,0.0404191102f,0.0491298959f,0.0576360481f,0.0652577058f,0.0713113508f,0.07520986f,0.076556162f,0.07520986f,0.0713113508f,0.0652577058f,0.0576360481f,0.0491298959f,0.0404191102f,0.0320934871f,0.0245944051f,0.0181905149f,0.0129850288f,0.0089460248f,0.0059484873f } );
mMap.put( 27, new float[] { 0.0054216802f,0.007930248f,0.0112519401f,0.0154865908f,0.0206762609f,0.0267778655f,0.0336409056f,0.0409965368f,0.0484634619f,0.0555737199f,0.0618176187f,0.0667026128f,0.0698170011f,0.0708871156f,0.0698170011f,0.0667026128f,0.0618176187f,0.0555737199f,0.0484634619f,0.0409965368f,0.0336409056f,0.0267778655f,0.0206762609f,0.0154865908f,0.0112519401f,0.007930248f,0.0054216802f } );
mMap.put( 29, new float[] { 0.0049794093f,0.0071086836f,0.0098843442f,0.013386093f,0.0176566017f,0.022683377f,0.0283828243f,0.0345900198f,0.0410575774f,0.0474660547f,0.0534466215f,0.0586144488f,0.0626089499f,0.0651351564f,0.0659996766f,0.0651351564f,0.0626089499f,0.0586144488f,0.0534466215f,0.0474660547f,0.0410575774f,0.0345900198f,0.0283828243f,0.022683377f,0.0176566017f,0.013386093f,0.0098843442f,0.0071086836f,0.0049794093f } );
mMap.put( 31, new float[] { 0.0046030224f,0.0064323659f,0.0087836656f,0.0117208269f,0.0152833344f,0.0194740097f,0.0242476721f,0.0295027244f,0.0350777394f,0.0407547727f,0.0462703471f,0.0513339215f,0.0556523529f,0.0589576307f,0.0610342929f,0.0617426429f,0.0610342929f,0.0589576307f,0.0556523529f,0.0513339215f,0.0462703471f,0.0407547727f,0.0350777394f,0.0295027244f,0.0242476721f,0.0194740097f,0.0152833344f,0.0117208269f,0.0087836656f,0.0064323659f,0.0046030224f } );
}
public GaussianSmoothingFilter()
{
super( mMap, 3 );
}
@Override
public SmoothingType getSmoothingType()
{
return SmoothingType.GAUSSIAN;
}
}

View File

@@ -0,0 +1,31 @@
package dsp.filter.smoothing;
public class NoSmoothingFilter extends SmoothingFilter
{
public NoSmoothingFilter()
{
super( null, 3 );
}
public float[] filter( float[] data )
{
return data;
}
public static float[] getCoefficients( int points )
{
return null;
}
public int getPointSize()
{
return SMOOTHING_DEFAULT;
}
@Override
public SmoothingType getSmoothingType()
{
return SmoothingType.NONE;
}
}

View File

@@ -0,0 +1,49 @@
package dsp.filter.smoothing;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class RectangularSmoothingFilter extends SmoothingFilter
{
private static Map<Integer,float[]> mMap = new HashMap<>();
static
{
mMap.put( 3, getCoefficients( 3 ) );
mMap.put( 5, getCoefficients( 5 ) );
mMap.put( 7, getCoefficients( 7 ) );
mMap.put( 9, getCoefficients( 9 ) );
mMap.put( 11, getCoefficients( 11 ) );
mMap.put( 13, getCoefficients( 13 ) );
mMap.put( 15, getCoefficients( 15 ) );
mMap.put( 17, getCoefficients( 17 ) );
mMap.put( 19, getCoefficients( 19 ) );
mMap.put( 21, getCoefficients( 21 ) );
mMap.put( 23, getCoefficients( 23 ) );
mMap.put( 25, getCoefficients( 25 ) );
mMap.put( 27, getCoefficients( 27 ) );
mMap.put( 29, getCoefficients( 29 ) );
mMap.put( 31, getCoefficients( 31 ) );
}
public RectangularSmoothingFilter()
{
super( mMap, 3 );
}
public static float[] getCoefficients( int points )
{
float[] coefficients = new float[ points ];
Arrays.fill( coefficients, 1.0f / (float)points );
return coefficients;
}
@Override
public SmoothingType getSmoothingType()
{
return SmoothingType.RECTANGLE;
}
}

View File

@@ -0,0 +1,97 @@
package dsp.filter.smoothing;
import java.util.Map;
import java.util.Set;
public abstract class SmoothingFilter
{
public static final int SMOOTHING_MINIMUM = 3;
public static final int SMOOTHING_MAXIMUM = 29;
public static final int SMOOTHING_DEFAULT = 9;
public enum SmoothingType
{
NONE,
RECTANGLE,
TRIANGLE,
GAUSSIAN;
}
private Map<Integer,float[]> mCoefficientMap;
private float[] mCoefficients;
private float[] mNewCoefficients;
private boolean mNewCoefficientsAvailable;
public SmoothingFilter( Map<Integer,float[]> coefficients, int index )
{
mCoefficientMap = coefficients;
if( mCoefficientMap != null )
{
mCoefficients = mCoefficientMap.get( index );
}
}
public abstract SmoothingType getSmoothingType();
public Set<Integer> getPointSizeList()
{
return mCoefficientMap.keySet();
}
public int getPointSize()
{
return mCoefficients.length;
}
public void setPointSize( int pointSize )
{
if( mCoefficientMap != null )
{
if( mCoefficientMap.containsKey( pointSize ) )
{
mNewCoefficients = mCoefficientMap.get( pointSize );
mNewCoefficientsAvailable = true;
}
else
{
throw new IllegalArgumentException( "Point size [" + pointSize +
"] is not valid in the current coefficient map" );
}
}
}
public float[] filter( float[] data )
{
if( mNewCoefficientsAvailable )
{
mCoefficients = mNewCoefficients;
mNewCoefficientsAvailable = false;
}
int middle = mCoefficients.length / 2;
float[] filtered = new float[ data.length ];
int toCopy = middle;
System.arraycopy( data, 0, filtered, 0, toCopy );
System.arraycopy( data, data.length - toCopy, filtered, filtered.length - toCopy, toCopy );
float accumulator;
for( int x = 0; x < data.length - mCoefficients.length + 1; x++ )
{
accumulator = 0.0f;
for( int y = 0; y < mCoefficients.length; y++ )
{
accumulator += data[ x + y ] * mCoefficients[ y ];
}
filtered[ x + middle ] = accumulator;
}
return filtered;
}
}

View File

@@ -0,0 +1,38 @@
package dsp.filter.smoothing;
import java.util.HashMap;
import java.util.Map;
public class TriangularSmoothingFilter extends SmoothingFilter
{
private static Map<Integer,float[]> mMap = new HashMap<>();
static
{
mMap.put( 3, new float[]{ 0.25f,0.5f,0.25f } );
mMap.put( 5, new float[]{ 0.1111111111f,0.2222222222f,0.3333333333f,0.2222222222f,0.1111111111f } );
mMap.put( 7, new float[]{ 0.0625f,0.125f,0.1875f,0.25f,0.1875f,0.125f,0.0625f } );
mMap.put( 9, new float[]{ 0.04f,0.08f,0.12f,0.16f,0.2f,0.16f,0.12f,0.08f,0.04f } );
mMap.put( 11, new float[]{ 0.0277777778f,0.0555555556f,0.0833333333f,0.1111111111f,0.1388888889f,0.1666666667f,0.1388888889f,0.1111111111f,0.0833333333f,0.0555555556f,0.0277777778f } );
mMap.put( 13, new float[]{ 0.0204081633f,0.0408163265f,0.0612244898f,0.0816326531f,0.1020408163f,0.1224489796f,0.1428571429f,0.1224489796f,0.1020408163f,0.0816326531f,0.0612244898f,0.0408163265f,0.0204081633f } );
mMap.put( 15, new float[]{ 0.015625f,0.03125f,0.046875f,0.0625f,0.078125f,0.09375f,0.109375f,0.125f,0.109375f,0.09375f,0.078125f,0.0625f,0.046875f,0.03125f,0.015625f } );
mMap.put( 17, new float[]{ 0.012345679f,0.024691358f,0.037037037f,0.049382716f,0.0617283951f,0.0740740741f,0.0864197531f,0.0987654321f,0.1111111111f,0.0987654321f,0.0864197531f,0.0740740741f,0.0617283951f,0.049382716f,0.037037037f,0.024691358f,0.012345679f } );
mMap.put( 19, new float[]{ 0.01f,0.02f,0.03f,0.04f,0.05f,0.06f,0.07f,0.08f,0.09f,0.1f,0.09f,0.08f,0.07f,0.06f,0.05f,0.04f,0.03f,0.02f,0.01f } );
mMap.put( 21, new float[]{ 0.0082644628f,0.0165289256f,0.0247933884f,0.0330578512f,0.041322314f,0.0495867769f,0.0578512397f,0.0661157025f,0.0743801653f,0.0826446281f,0.0909090909f,0.0826446281f,0.0743801653f,0.0661157025f,0.0578512397f,0.0495867769f,0.041322314f,0.0330578512f,0.0247933884f,0.0165289256f,0.0082644628f } );
mMap.put( 23, new float[]{ 0.0069444444f,0.0138888889f,0.0208333333f,0.0277777778f,0.0347222222f,0.0416666667f,0.0486111111f,0.0555555556f,0.0625f,0.0694444444f,0.0763888889f,0.0833333333f,0.0763888889f,0.0694444444f,0.0625f,0.0555555556f,0.0486111111f,0.0416666667f,0.0347222222f,0.0277777778f,0.0208333333f,0.0138888889f,0.0069444444f } );
mMap.put( 25, new float[]{ 0.0059171598f,0.0118343195f,0.0177514793f,0.0236686391f,0.0295857988f,0.0355029586f,0.0414201183f,0.0473372781f,0.0532544379f,0.0591715976f,0.0650887574f,0.0710059172f,0.0769230769f,0.0710059172f,0.0650887574f,0.0591715976f,0.0532544379f,0.0473372781f,0.0414201183f,0.0355029586f,0.0295857988f,0.0236686391f,0.0177514793f,0.0118343195f,0.0059171598f } );
mMap.put( 27, new float[]{ 0.0051020408f,0.0102040816f,0.0153061224f,0.0204081633f,0.0255102041f,0.0306122449f,0.0357142857f,0.0408163265f,0.0459183673f,0.0510204082f,0.056122449f,0.0612244898f,0.0663265306f,0.0714285714f,0.0663265306f,0.0612244898f,0.056122449f,0.0510204082f,0.0459183673f,0.0408163265f,0.0357142857f,0.0306122449f,0.0255102041f,0.0204081633f,0.0153061224f,0.0102040816f,0.0051020408f } );
mMap.put( 29, new float[]{ 0.0044444444f,0.0088888889f,0.0133333333f,0.0177777778f,0.0222222222f,0.0266666667f,0.0311111111f,0.0355555556f,0.04f,0.0444444444f,0.0488888889f,0.0533333333f,0.0577777778f,0.0622222222f,0.0666666667f,0.0622222222f,0.0577777778f,0.0533333333f,0.0488888889f,0.0444444444f,0.04f,0.0355555556f,0.0311111111f,0.0266666667f,0.0222222222f,0.0177777778f,0.0133333333f,0.0088888889f,0.0044444444f } );
}
public TriangularSmoothingFilter()
{
super( mMap, 3 );
}
@Override
public SmoothingType getSmoothingType()
{
return SmoothingType.TRIANGLE;
}
}

View File

@@ -15,7 +15,7 @@
* 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 util;
package dsp.mixer;
import sample.complex.Complex;

View File

@@ -6,11 +6,9 @@ import instrument.tap.TapGroup;
import java.util.ArrayList;
import java.util.List;
import source.Source;
import source.tuner.TunerChannelSource;
import source.tuner.frequency.FrequencyCorrectionControl;
import module.Module;
import module.ProcessingChain;
import source.Source;
public class InstrumentableProcessingChain extends ProcessingChain implements Instrumentable
{

View File

@@ -27,7 +27,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import message.IMessageListener;
import message.IMessageProvider;
import message.Message;
import module.decode.Decoder;
import module.decode.event.CallEvent;
import module.decode.event.ICallEventListener;
import module.decode.event.ICallEventProvider;
@@ -42,6 +41,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Broadcaster;
import sample.Buffer;
import sample.Listener;
import sample.complex.ComplexBuffer;
import sample.complex.IComplexBufferListener;

View File

@@ -19,7 +19,6 @@ package module.decode.ltrnet;
import module.decode.DecoderType;
import module.decode.event.CallEvent;
import module.decode.ltrstandard.LTRStandardMessage;
import alias.Alias;
import alias.AliasList;

View File

@@ -40,7 +40,6 @@ import sample.real.RealBuffer;
import alias.AliasList;
import bits.MessageFramer;
import bits.SyncPattern;
import controller.channel.Channel.ChannelType;
import dsp.filter.FilterFactory;
import dsp.filter.Filters;
import dsp.filter.Window.WindowType;

View File

@@ -21,7 +21,6 @@ import java.awt.EventQueue;
import javax.swing.JLabel;
import module.decode.p25.P25Decoder.Modulation;
import module.decode.state.ChangedAttribute;
import module.decode.state.DecoderPanel;
import net.miginfocom.swing.MigLayout;

View File

@@ -36,7 +36,6 @@ import sample.complex.ComplexBufferToStreamConverter;
import sample.complex.IComplexBufferListener;
import source.tuner.frequency.FrequencyCorrectionControl;
import alias.AliasList;
import controller.channel.Channel.ChannelType;
import dsp.filter.FilterFactory;
import dsp.filter.Window.WindowType;
import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;

View File

@@ -135,7 +135,7 @@ public class P25AudioModule extends Module implements Listener<Message>,
library = (AudioConversionLibrary)temp.newInstance();
if( ( library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 ) ||
library.getMajorVersion() > 0 )
library.getMajorVersion() >= 1 )
{
mAudioConverter = library.getAudioConverter( IMBE_CODEC,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );

View File

@@ -30,10 +30,11 @@ import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.miginfocom.swing.MigLayout;
import playlist.PlaylistManager;
import settings.ColorSetting.ColorSettingName;
import settings.ColorSettingMenuItem;

View File

@@ -7,20 +7,14 @@ import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import module.decode.Decoder;
import module.decode.DecoderFactory;
import module.decode.event.ActivitySummaryFrame;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;

View File

@@ -33,7 +33,6 @@ import source.tuner.frequency.FrequencyCorrectionControl;
import source.tuner.frequency.IFrequencyCorrectionController;
import dsp.filter.FilterFactory;
import dsp.filter.Window.WindowType;
import dsp.filter.dc.AveragingDCRemovalFilter_RB;
import dsp.filter.dc.DCRemovalFilter_RB;
import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
import dsp.fm.FMDemodulator_CB;
@@ -102,7 +101,7 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene
if( mRemoveDC )
{
mDCRemovalFilter = new AveragingDCRemovalFilter_RB( DC_REMOVAL_RATIO );
mDCRemovalFilter = new DCRemovalFilter_RB( DC_REMOVAL_RATIO );
mUnfilteredBufferBroadcaster.addListener( mDCRemovalFilter );
}
}

View File

@@ -21,13 +21,13 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import controller.channel.Channel;
import module.Module;
import module.log.MessageEventLogger.Type;
import module.log.config.EventLogConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
public class EventLogManager

View File

@@ -32,6 +32,7 @@ import module.Module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Buffer;
import sample.ConversionUtils;
import sample.Listener;
import sample.complex.ComplexBuffer;
@@ -163,7 +164,7 @@ public class ComplexBufferWaveRecorder extends Module
{
try
{
ComplexBuffer buffer = mBuffers.poll();
Buffer buffer = mBuffers.poll();
while( buffer != null )
{

View File

@@ -30,9 +30,6 @@ import java.util.regex.Pattern;
import javax.sound.sampled.AudioFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WaveWriter implements AutoCloseable
{
// private final static Logger mLog = LoggerFactory.getLogger( WaveWriter.class );

24
src/sample/Buffer.java Normal file
View File

@@ -0,0 +1,24 @@
package sample;
public class Buffer
{
protected float[] mSamples;
public Buffer( float[] samples )
{
mSamples = samples;
}
public float[] getSamples()
{
return mSamples;
}
/**
* Cleanup method to nullify all data and references
*/
public void dispose()
{
mSamples = null;
}
}

View File

@@ -4,7 +4,6 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import sample.complex.ComplexBuffer;
import sample.real.RealBuffer;
/**
@@ -57,7 +56,7 @@ public class ConversionUtils
* Converts the float samples in a complex buffer to a little endian 16-bit
* buffer
*/
public static ByteBuffer convertToSigned16BitSamples( ComplexBuffer buffer )
public static ByteBuffer convertToSigned16BitSamples( Buffer buffer )
{
return convertToSigned16BitSamples( buffer.getSamples() );
}

View File

@@ -17,7 +17,7 @@
******************************************************************************/
package sample.adapter;
public class ByteSampleAdapter extends SampleAdapter
public class ByteSampleAdapter implements ISampleAdapter
{
private final static float[] LOOKUP_VALUES;
@@ -33,8 +33,6 @@ public class ByteSampleAdapter extends SampleAdapter
{
LOOKUP_VALUES[ x ] = (float)( x - 127 ) / 128.0f;
}
//
// LOOKUP_VALUES[ 128 ] = 0.0000001f;
}
@Override

View File

@@ -25,7 +25,7 @@ import source.mixer.MixerChannel;
/**
* Converts 16-bit little endian byte data into an array of floats
*/
public class ChannelShortAdapter extends SampleAdapter
public class ChannelShortAdapter implements ISampleAdapter
{
private ShortToFloatMap mMap = new ShortToFloatMap();
private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN;

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,7 +21,7 @@ package sample.adapter;
* Adapter to convert byte data received from a data stream into float[]
* samples
*/
public abstract class SampleAdapter
public interface ISampleAdapter
{
public abstract float[] convert( byte[] samples );
}

View File

@@ -23,7 +23,7 @@ import java.nio.ByteOrder;
/**
* Converts 16-bit little endian byte data into an array of floats
*/
public class ShortAdapter extends SampleAdapter
public class ShortAdapter implements ISampleAdapter
{
private ShortToFloatMap mMap = new ShortToFloatMap();
private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN;

View File

@@ -1,21 +1,17 @@
package sample.complex;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ComplexBuffer
import sample.Buffer;
public class ComplexBuffer extends Buffer
{
private float[] mSamples;
/**
* Wrapper around float array containing interleaved I/Q samples
*/
public ComplexBuffer( float[] samples )
{
mSamples = samples;
super( samples );
}
/**
@@ -27,35 +23,4 @@ public class ComplexBuffer
return new ComplexBuffer( copy );
}
/**
* Interleaved I/Q float sample array
*/
public float[] getSamples()
{
return mSamples;
}
/**
* Converts this sample buffer into a list of complex samples
*/
public List<Complex> getComplexSamples()
{
List<Complex> samples = new ArrayList<Complex>();
for( int x = 0; x < mSamples.length; x += 2 )
{
samples.add( new Complex( mSamples[ x ], mSamples[ x + 1 ] ) );
}
return samples;
}
/**
* Cleanup method to nullify all data and references
*/
public void dispose()
{
mSamples = null;
}
}

View File

@@ -2,31 +2,18 @@ package sample.real;
import java.util.Arrays;
public class RealBuffer
{
private float[] mSamples;
import sample.Buffer;
public class RealBuffer extends Buffer
{
/**
* Wrapper around float array containing float samples
* Wrapper around float array containing real float samples
*/
public RealBuffer( float[] samples )
{
mSamples = samples;
super( samples );
}
/**
* Package private to allow subclass construction and subsequent definition
* of the sample data
*/
RealBuffer()
{
}
protected void setSamples( float[] samples )
{
mSamples = samples;
}
/**
* Creates a deep copy of the buffer
*/
@@ -36,20 +23,4 @@ public class RealBuffer
return new RealBuffer( copy );
}
/**
* Sample data
*/
public float[] getSamples()
{
return mSamples;
}
/**
* Cleanup method to nullify all data and references
*/
public void dispose()
{
mSamples = null;
}
}

View File

@@ -31,6 +31,7 @@ import source.recording.RecordingConfiguration;
import source.tuner.TunerConfiguration;
import source.tuner.TunerConfigurationAssignment;
import source.tuner.TunerType;
import source.tuner.airspy.AirspyTunerConfiguration;
import source.tuner.fcd.proV1.FCD1TunerConfiguration;
import source.tuner.fcd.proplusV2.FCD2TunerConfiguration;
import source.tuner.hackrf.HackRFTunerConfiguration;
@@ -40,7 +41,8 @@ import source.tuner.rtl.r820t.R820TTunerConfiguration;
/**
* Support for persisting via JAXB all configured SDRTrunk settings
*/
@XmlSeeAlso( { ColorSetting.class,
@XmlSeeAlso( { AirspyTunerConfiguration.class,
ColorSetting.class,
DefaultIcon.class,
E4KTunerConfiguration.class,
FCD1TunerConfiguration.class,

View File

@@ -50,6 +50,7 @@ import source.recording.RecordingConfiguration;
import source.tuner.TunerConfiguration;
import source.tuner.TunerConfigurationAssignment;
import source.tuner.TunerType;
import source.tuner.airspy.AirspyTunerConfiguration;
import source.tuner.fcd.proV1.FCD1TunerConfiguration;
import source.tuner.fcd.proplusV2.FCD2TunerConfiguration;
import source.tuner.hackrf.HackRFTunerConfiguration;
@@ -573,6 +574,14 @@ public class SettingsManager
{
switch( type )
{
case AIRSPY_R820T:
AirspyTunerConfiguration airspyConfig =
new AirspyTunerConfiguration( name );
getSettings().addTunerConfiguration( airspyConfig );
save();
return airspyConfig;
case ELONICS_E4000:
E4KTunerConfiguration e4KConfig =
new E4KTunerConfiguration( name );

View File

@@ -28,7 +28,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.adapter.SampleAdapter;
import sample.adapter.ISampleAdapter;
import sample.complex.ComplexBuffer;
import source.SourceException;
@@ -45,7 +45,7 @@ public class ComplexMixer
private AudioFormat mAudioFormat;
private String mName;
private int mBytesPerFrame = 0;
private SampleAdapter mSampleAdapter;
private ISampleAdapter mSampleAdapter;
private Listener<ComplexBuffer> mListener;
/**
@@ -68,7 +68,7 @@ public class ComplexMixer
public ComplexMixer( TargetDataLine targetDataLine,
AudioFormat format,
String name,
SampleAdapter sampleAdapter,
ISampleAdapter sampleAdapter,
Listener<ComplexBuffer> listener )
{
mTargetDataLine = targetDataLine;

View File

@@ -31,7 +31,7 @@ import org.slf4j.LoggerFactory;
import dsp.gain.AutomaticGainControl;
import sample.Listener;
import sample.adapter.SampleAdapter;
import sample.adapter.ISampleAdapter;
import sample.real.RealBuffer;
import source.RealSource;
import source.SourceException;
@@ -49,7 +49,7 @@ public class RealMixerSource extends RealSource
private AudioFormat mAudioFormat;
private String mDisplayName;
private int mBytesPerFrame = 0;
private SampleAdapter mSampleAdapter;
private ISampleAdapter mSampleAdapter;
private AutomaticGainControl mAGC = new AutomaticGainControl();
CopyOnWriteArrayList<Listener<RealBuffer>> mSampleListeners =
@@ -74,7 +74,7 @@ public class RealMixerSource extends RealSource
public RealMixerSource( TargetDataLine targetDataLine,
AudioFormat format,
String name,
SampleAdapter sampleAdapter )
ISampleAdapter sampleAdapter )
{
super( name );

View File

@@ -25,7 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.adapter.SampleAdapter;
import sample.adapter.ISampleAdapter;
import sample.complex.ComplexBuffer;
import sample.complex.ComplexSampleListListener;
import source.mixer.ComplexMixer;
@@ -48,7 +48,7 @@ public abstract class MixerTuner extends Tuner implements Listener<ComplexBuffer
public MixerTuner( String name,
MixerTunerType mixerTunerType,
TargetDataLine targetDataLine,
SampleAdapter sampleAdapter )
ISampleAdapter sampleAdapter )
{
super( name );
@@ -63,7 +63,7 @@ public abstract class MixerTuner extends Tuner implements Listener<ComplexBuffer
public MixerTuner( String name,
MixerTunerDataLine mixerTuner,
SampleAdapter sampleAdapter )
ISampleAdapter sampleAdapter )
{
this( name,
mixerTuner.getMixerTunerType(),

View File

@@ -124,6 +124,11 @@ public abstract class Tuner implements FrequencyChangeBroadcaster,
* @return
*/
public abstract int getSampleRate();
/**
* Sample size in bits
*/
public abstract double getSampleSize();
/**
* Tuner Local Oscillator Frequency

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Buffer;
import sample.Listener;
import sample.complex.Complex;
import sample.complex.ComplexBuffer;
@@ -36,12 +37,12 @@ import source.SourceException;
import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeEvent.Attribute;
import source.tuner.frequency.FrequencyChangeListener;
import util.Oscillator;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
import dsp.filter.FilterFactory;
import dsp.filter.Window.WindowType;
import dsp.filter.cic.ComplexPrimeCICDecimate;
import dsp.mixer.Oscillator;
public class TunerChannelSource extends ComplexSource
implements FrequencyChangeListener,
@@ -292,7 +293,7 @@ public class TunerChannelSource extends ComplexSource
{
mBuffer.drainTo( sampleBuffers, 4 );
for( ComplexBuffer buffer: sampleBuffers )
for( Buffer buffer: sampleBuffers )
{
/* Check to see if we've been shutdown */
if( mDispose.get() )

View File

@@ -21,6 +21,7 @@ import javax.usb.UsbDeviceDescriptor;
public enum TunerClass
{
AIRSPY( TunerType.AIRSPY_R820T, "1D50", "60A1", "Airspy.com", "Airspy" ),
GENERIC_2832( TunerType.RTL2832_VARIOUS, "0BDA", "2832", "RTL2832", "SDR" ),
GENERIC_2838( TunerType.RTL2832_VARIOUS, "0BDA", "2838", "RTL2832", "SDR" ),
COMPRO_VIDEOMATE_U620F( TunerType.ELONICS_E4000, "185B", "0620", "Compro", "Videomate U620F" ),
@@ -180,6 +181,10 @@ public enum TunerClass
{
retVal = HACKRF_ONE;
}
else if( productID == 24737 ) //60A1
{
retVal = AIRSPY;
}
break;
case 8013: //1F4D
if( productID == 47107 ) //B803

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,6 +35,8 @@ import source.SourceException;
import source.config.SourceConfigTuner;
import source.config.SourceConfiguration;
import source.mixer.MixerManager;
import source.tuner.airspy.AirspyTuner;
import source.tuner.airspy.AirspyTunerController;
import source.tuner.fcd.FCDTuner;
import source.tuner.fcd.proV1.FCD1TunerController;
import source.tuner.fcd.proplusV2.FCD2TunerController;
@@ -208,6 +210,8 @@ public class TunerManager
switch( tunerClass )
{
case AIRSPY:
return initAirspyTuner( device, descriptor );
case ETTUS_USRP_B100:
return initEttusB100Tuner( device, descriptor );
case FUNCUBE_DONGLE_PRO:
@@ -250,6 +254,38 @@ public class TunerManager
return new TunerInitStatus( null, "Unknown Device" );
}
private TunerInitStatus initAirspyTuner( Device device,
DeviceDescriptor descriptor )
{
try
{
AirspyTunerController airspyController =
new AirspyTunerController( device, mResourceManager.getThreadPoolManager() );
airspyController.init();
AirspyTuner tuner = new AirspyTuner( airspyController );
TunerConfiguration config = getTunerConfiguration( tuner );
if( config != null )
{
tuner.apply( config );
}
return new TunerInitStatus( tuner, "LOADED" );
}
catch( SourceException se )
{
mLog.error( "couldn't construct Airspy controller/tuner", se );
return new TunerInitStatus( null,
"error constructing Airspy tuner controller" );
}
}
private TunerInitStatus initEttusB100Tuner( Device device,
DeviceDescriptor descriptor )

View File

@@ -19,6 +19,7 @@ package source.tuner;
public enum TunerType
{
AIRSPY_R820T( "R820T" ),
ELONICS_E4000( "E4000" ),
ETTUS_WBX( "WBX" ),
ETTUS_VARIOUS( "Ettus Tuner" ),

View File

@@ -0,0 +1,159 @@
package source.tuner.airspy;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import source.tuner.airspy.AirspyTunerController.BoardID;
public class AirspyDeviceInformation
{
private final static Logger mLog =
LoggerFactory.getLogger( AirspyDeviceInformation.class );
private BoardID mBoardID = BoardID.UNKNOWN;
private String mFirmwareVersion = "Unknown";
private String mPartNumber = "Unknown";
private String mSerialNumber = "Unknown";
/**
* Airspy Device Information - provides parsing and access to device
* information components.
*/
public AirspyDeviceInformation()
{
}
public void setBoardID( int boardID )
{
mBoardID = BoardID.fromValue( boardID );
}
public BoardID getBoardID()
{
return mBoardID;
}
public void setVersion( byte[] data )
{
if( data != null && data.length > 0 )
{
/* Truncate the version string if we have a line break - ASCII 10 */
for( int x = 0; x < data.length; x++ )
{
if( data[ x ] == (byte)10 )
{
mFirmwareVersion = new String( Arrays.copyOf( data, x ) );
mFirmwareVersion = mFirmwareVersion.replace( "AirSpy NOS ", "" );
return;
}
}
mFirmwareVersion = new String( data );
mFirmwareVersion = mFirmwareVersion.replace( "AirSpy NOS ", "" );
}
else
{
mLog.error( "Error setting airspy version byte data - null:" +
( data == null ? " TRUE" : " FALSE length:" + data.length ) );
}
}
public String getVersion()
{
return mFirmwareVersion;
}
public void setPartAndSerialNumber( byte[] data )
{
if( data != null && data.length == 24 )
{
//Note: values are byte-reversed (big-endian) 32-bit chunks
StringBuilder part = new StringBuilder();
part.append( format( data[ 3 ] ) );
part.append( format( data[ 2 ] ) );
part.append( format( data[ 1 ] ) );
part.append( format( data[ 0 ] ) );
part.append( format( data[ 7 ] ) );
part.append( format( data[ 6 ] ) );
part.append( format( data[ 5 ] ) );
part.append( format( data[ 4 ] ) );
mPartNumber = part.toString();
StringBuilder serial = new StringBuilder();
//Note: current airspy library only exposes 64-bits of serial number
//address space, but appears to have room for 128-bits. We won't
//add the first 64 bits if it's zeros
if( !( data[ 8 ] == 0 && data[ 9 ] == 0 && data[ 10 ] == 0 && data[ 11 ] == 0 ) )
{
serial.append( format( data[ 11 ] ) );
serial.append( format( data[ 10 ] ) );
serial.append( format( data[ 9 ] ) );
serial.append( format( data[ 8 ] ) );
serial.append( "-" );
}
if( !( data[ 12 ] == 0 && data[ 13 ] == 0 && data[ 14 ] == 0 && data[ 15 ] == 0 ) )
{
serial.append( format( data[ 15 ] ) );
serial.append( format( data[ 14 ] ) );
serial.append( format( data[ 13 ] ) );
serial.append( format( data[ 12 ] ) );
serial.append( "-" );
}
serial.append( format( data[ 19 ] ) );
serial.append( format( data[ 18 ] ) );
serial.append( format( data[ 17 ] ) );
serial.append( format( data[ 16 ] ) );
serial.append( "-" );
serial.append( format( data[ 23 ] ) );
serial.append( format( data[ 22 ] ) );
serial.append( format( data[ 21 ] ) );
serial.append( format( data[ 20 ] ) );
mSerialNumber = serial.toString();
}
else
{
mLog.error( "Error setting airspy part and serial byte data - null:" +
( data == null ? " TRUE" : " FALSE length:" + data.length ) );
}
}
private String format( byte value )
{
return String.format( "%02X", value );
}
public String getSerialNumber()
{
return mSerialNumber;
}
public String getPartNumber()
{
return mPartNumber;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append( "Airspy Device Information\n" );
sb.append( "Board: " );
sb.append( getBoardID().getLabel() );
sb.append( "\nPart Number: " );
sb.append( getPartNumber() );
sb.append( "\nSerial Number: " );
sb.append( getSerialNumber() );
sb.append( "\nFirmware: " );
sb.append( getVersion() );
return sb.toString();
}
}

View File

@@ -0,0 +1,77 @@
package source.tuner.airspy;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
import javax.swing.JLabel;
import javax.swing.JPanel;
import net.miginfocom.swing.MigLayout;
public class AirspyInformationPanel extends JPanel
{
private static final long serialVersionUID = 1L;
private AirspyTunerController mController;
/**
* Displays Airspy firmware, serial number and part identifier
*/
public AirspyInformationPanel( AirspyTunerController controller )
{
mController = controller;
initGUI();
}
private void initGUI()
{
setLayout( new MigLayout( "fill,wrap 2",
"[right,grow][grow]",
"[][][][][grow]" ) );
AirspyDeviceInformation info = mController.getDeviceInfo();
add( new JLabel( "Serial:" ) );
add( new JLabel( info.getSerialNumber() ) );
add( new JLabel( "Firmware:" ) );
//Split the firmware on spaces and use first value -- expose full
//string in a tool tip
String[] firmware = info.getVersion().split( " " );
JLabel firmwareLabel;
if( firmware.length > 1 )
{
firmwareLabel = new JLabel( firmware[ 0 ] );
}
else
{
firmwareLabel = new JLabel( info.getVersion() );
}
firmwareLabel.setToolTipText( info.getVersion() );
add( firmwareLabel );
add( new JLabel( "Part:" ) );
add( new JLabel( info.getPartNumber() ) );
add( new JLabel( "Board ID:" ) );
add( new JLabel( info.getBoardID().getLabel() ) );
}
}

View File

@@ -0,0 +1,149 @@
package source.tuner.airspy;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.adapter.ISampleAdapter;
public class AirspySampleAdapter implements ISampleAdapter
{
private final static Logger mLog = LoggerFactory.getLogger( AirspySampleAdapter.class );
private static final float SCALE_SIGNED_12_BIT_TO_FLOAT = 1.0f / 2048.0f;
private boolean mSamplePacking = false;
/**
* Adapter to translate byte buffers received from the airspy tuner into
* float buffers for processing.
*/
public AirspySampleAdapter()
{
}
/**
* Sample packing places two 12-bit samples into 3 bytes when enabled or
* places two 12-bit samples into 4 bytes when disabled.
*
* @param enabled
*/
public void setSamplePacking( boolean enabled )
{
mSamplePacking = enabled;
}
@Override
public float[] convert( byte[] samples )
{
if( mSamplePacking )
{
return convertPacked( samples );
}
else
{
return convertUnpacked( samples );
}
}
/**
* Converts the byte array containing unsigned 12-bit short values into
* signed float values in the range -1 to 1;
*
* @param data - byte array of unsigned 16-bit values
* @return converted float values
*/
private float[] convertUnpacked( byte[] data )
{
float[] samples = new float[ data.length / 2 ];
int pointer = 0;
for( int x = 0; x < data.length; x += 2 )
{
samples[ pointer++ ] = scale( ( data[ x ] & 0xFF ) |
( data[ x + 1 ] << 8 ) );
}
return samples;
}
/**
* Converts every 3 bytes containing a pair of 12-bit unsigned values into
* a pair of float values in the range -1 to 1;
*
* @param data - byte array of unsigned 12-bit values
* @return converted float values
*/
private float[] convertPacked( byte[] data1 )
{
byte[] data = new byte[ data1.length ];
//Convert big-endian to little-endian
for( int x = 0; x < data1.length; x += 4 )
{
data[ x ] = data1[ x + 3 ];
data[ x + 1 ] = data1[ x + 2 ];
data[ x + 2 ] = data1[ x + 1 ];
data[ x + 3 ] = data1[ x ];
}
int count = (int)( (float)data.length / 1.5f );
/* Ensure we have an even number of samples */
if( count % 2 == 1 )
{
count--;
}
int bytes = (int)((float)count * 1.5f);
float[] samples = new float[ count ];
int pointer = 0;
int first;
int second;
for( int x = 0; x < bytes; x += 3 )
{
first = ( ( data[ x ] << 4 ) & 0xFF0 ) |
( ( data[ x + 1 ] >> 4 ) & 0xF );
samples[ pointer++ ] = scale( first );
second = ( ( data[ x + 1 ] << 8 ) & 0xF00 ) |
( data[ x + 2 ] & 0xFF );
samples[ pointer++ ] = scale( second );
}
return samples;
}
/**
* Converts unsigned 12-bit values to signed 12-bit values and then scales
* the signed value to a signed float value in range: -1.0 : +1.0
*/
public static float scale( int value )
{
return (float)(( value & 0xFFF ) - 2048 ) * SCALE_SIGNED_12_BIT_TO_FLOAT;
}
}

View File

@@ -0,0 +1,168 @@
package source.tuner.airspy;
import java.util.concurrent.RejectedExecutionException;
import javax.swing.JPanel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.complex.ComplexBuffer;
import source.SourceException;
import source.tuner.Tuner;
import source.tuner.TunerChannel;
import source.tuner.TunerChannelSource;
import source.tuner.TunerClass;
import source.tuner.TunerConfiguration;
import source.tuner.TunerType;
import source.tuner.airspy.AirspyTunerController.BoardID;
import controller.ResourceManager;
import controller.ThreadPoolManager;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
public class AirspyTuner extends Tuner
{
private final static Logger mLog = LoggerFactory.getLogger( AirspyTuner.class );
AirspyTunerController mController;
public AirspyTuner( AirspyTunerController controller )
{
super( "Airspy" );
mController = controller;
/* Register for frequency/sample rate changes so that we can rebroadcast
* to any registered listeners */
mController.addListener( this );
}
@Override
public JPanel getEditor( ResourceManager resourceManager )
{
return new AirspyTunerEditorPanel( this, resourceManager );
}
public AirspyTunerController getController()
{
return mController;
}
@Override
public void apply( TunerConfiguration config ) throws SourceException
{
mController.apply( config );
}
@Override
public String getUniqueID()
{
try
{
return mController.getDeviceInfo().getSerialNumber();
}
catch( Exception e )
{
mLog.error( "error gettting serial number", e );
}
return BoardID.AIRSPY.getLabel();
}
@Override
public TunerClass getTunerClass()
{
return TunerClass.AIRSPY;
}
@Override
public TunerType getTunerType()
{
return TunerType.AIRSPY_R820T;
}
@Override
public int getSampleRate()
{
int rate = 0;
try
{
rate = mController.getCurrentSampleRate();
}
catch ( SourceException e )
{
mLog.debug( "Error while getting sample rate from controller", e );
}
return rate;
}
@Override
public double getSampleSize()
{
return 12.0;
}
@Override
public long getFrequency() throws SourceException
{
return mController.getFrequency();
}
@Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
TunerChannel channel ) throws RejectedExecutionException, SourceException
{
//TODO: this channel has a decimated sample rate of:
// 10.0 MSps = 10,000,000 / 208 = 48076.923
// 5.0 MSps = 5,000,000 / 104 = 48076.923
// 2.5 MSps = 2,500,000 / 52 = 48076.923
//Consider implementing a fractional resampler to get a correct 48 kHz
//output sample rate
return mController.getChannel( threadPoolManager, this, channel );
}
@Override
public void releaseChannel( TunerChannelSource source )
{
/* Unregister for receiving samples */
removeListener( (Listener<ComplexBuffer>)source );
/* Tell the controller to release the channel and cleanup */
if( source != null )
{
mController.releaseChannel( source );
}
}
@Override
public void addListener( Listener<ComplexBuffer> listener )
{
mController.addListener( listener );
}
@Override
public void removeListener( Listener<ComplexBuffer> listener )
{
mController.removeListener( listener );
}
}

View File

@@ -0,0 +1,119 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package source.tuner.airspy;
import javax.xml.bind.annotation.XmlAttribute;
import source.tuner.TunerConfiguration;
import source.tuner.TunerType;
public class AirspyTunerConfiguration extends TunerConfiguration
{
private int mSampleRate = AirspyTunerController.DEFAULT_SAMPLE_RATE.getRate();
private int mIFGain = AirspyTunerController.IF_GAIN_DEFAULT;
private int mMixerGain = AirspyTunerController.MIXER_GAIN_DEFAULT;
private int mLNAGain = AirspyTunerController.LNA_GAIN_DEFAULT;
private boolean mMixerAGC = false;
private boolean mLNAAGC = false;
/**
* Default constructor for JAXB
*/
public AirspyTunerConfiguration()
{
this( "Default" );
}
public AirspyTunerConfiguration( String name )
{
super( name );
}
@Override
public TunerType getTunerType()
{
return TunerType.AIRSPY_R820T;
}
@XmlAttribute( name = "sample_rate" )
public int getSampleRate()
{
return mSampleRate;
}
public void setSampleRate( int sampleRate )
{
mSampleRate = sampleRate;
}
@XmlAttribute( name = "if_gain" )
public int getIFGain()
{
return mIFGain;
}
public void setIFGain( int gain )
{
mIFGain = gain;
}
@XmlAttribute( name = "mixer_gain" )
public int getMixerGain()
{
return mMixerGain;
}
public void setMixerGain( int gain )
{
mMixerGain = gain;
}
@XmlAttribute( name = "lna_gain" )
public int getLNAGain()
{
return mLNAGain;
}
public void setLNAGain( int gain )
{
mLNAGain = gain;
}
@XmlAttribute( name = "mixer_agc" )
public boolean isMixerAGC()
{
return mMixerAGC;
}
public void setMixerAGC( boolean enabled )
{
mMixerAGC = enabled;
}
@XmlAttribute( name = "lna_agc" )
public boolean isLNAAGC()
{
return mLNAAGC;
}
public void setLNAAGC( boolean enabled )
{
mLNAAGC = enabled;
}
}

View File

@@ -0,0 +1,587 @@
/*******************************************************************************
* 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 source.tuner.airspy;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.usb.UsbException;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.usb4java.LibUsbException;
import source.SourceException;
import source.tuner.TunerConfiguration;
import source.tuner.TunerConfigurationAssignment;
import source.tuner.TunerType;
import controller.ResourceManager;
public class AirspyTunerConfigurationPanel extends JPanel
{
private final static Logger mLog =
LoggerFactory.getLogger( AirspyTunerConfigurationPanel.class );
private static final long serialVersionUID = 1L;
private ResourceManager mResourceManager;
private AirspyTunerController mController;
private AirspyTunerConfiguration mSelectedConfig;
private JButton mNewConfiguration;
private JButton mDeleteConfiguration;
private JComboBox<AirspyTunerConfiguration> mComboConfigurations;
private JTextField mName;
private JComboBox<AirspySampleRate> mSampleRateCombo;
private JComboBox mDecimationCombo;
private JLabel mEffectiveRate;
private JLabel mIFGainLabel;
private JSlider mIFGain;
private JLabel mLNAGainLabel;
private JSlider mLNAGain;
private JLabel mMixerGainLabel;
private JSlider mMixerGain;
private JCheckBox mMixerAGC;
private JCheckBox mLNAAGC;
public AirspyTunerConfigurationPanel( ResourceManager resourceManager,
AirspyTunerController controller )
{
mResourceManager = resourceManager;
mController = controller;
init();
}
/**
* Initializes the gui components
*/
private void init()
{
setLayout( new MigLayout( "fill,wrap 4", "[left][grow][grow][right]",
"[][][][][][][grow]" ) );
/**
* Tuner configuration selector
*/
mComboConfigurations = new JComboBox<AirspyTunerConfiguration>();
mComboConfigurations.setModel( getModel() );
/* Determine which tuner configuration should be selected/displayed */
TunerConfigurationAssignment savedConfig = null;
String serial = null;
serial = mController.getDeviceInfo().getSerialNumber();
if( serial != null )
{
savedConfig = mResourceManager
.getSettingsManager().getSelectedTunerConfiguration(
TunerType.AIRSPY_R820T, serial );
}
if( savedConfig != null )
{
mSelectedConfig = getNamedConfiguration(
savedConfig.getTunerConfigurationName() );
}
/* If we couldn't determine the saved/selected config, use the first one */
if( mSelectedConfig == null )
{
mSelectedConfig = mComboConfigurations.getItemAt( 0 );
//Store this config as the default for this tuner at this address
if( serial != null )
{
mResourceManager.getSettingsManager()
.setSelectedTunerConfiguration( TunerType.AIRSPY_R820T,
serial, mSelectedConfig );
}
}
mComboConfigurations.setSelectedItem( mSelectedConfig );
mComboConfigurations.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
AirspyTunerConfiguration selected =
(AirspyTunerConfiguration)mComboConfigurations
.getItemAt( mComboConfigurations.getSelectedIndex() );
if( selected != null )
{
update( selected );
}
}
});
add( new JLabel( "Config:" ) );
add( mComboConfigurations, "span,growx,push" );
add( new JSeparator(), "span,growx,push" );
/**
* Tuner Configuration Name
*/
mName = new JTextField();
mName.setText( mSelectedConfig.getName() );
mName.addFocusListener( new FocusListener()
{
@Override
public void focusLost( FocusEvent e )
{
mSelectedConfig.setName( mName.getText() );
save();
}
@Override
public void focusGained( FocusEvent e ) {}
} );
add( new JLabel( "Name:" ) );
add( mName, "span,growx,push" );
/**
* Sample Rate
*/
add( new JLabel( "Sample Rate:" ) );
List<AirspySampleRate> rates = mController.getSampleRates();
mSampleRateCombo = new JComboBox<AirspySampleRate>(
new DefaultComboBoxModel<AirspySampleRate>( rates.toArray(
new AirspySampleRate[ rates.size() ] ) ) );
mSampleRateCombo.setSelectedItem( mController.getSampleRate() );
mSampleRateCombo.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
AirspySampleRate rate = (AirspySampleRate)mSampleRateCombo.getSelectedItem();
try
{
mController.setSampleRate( rate );
mSelectedConfig.setSampleRate( rate.getRate() );
save();
mEffectiveRate.setText( rate.getLabel() );
}
catch ( LibUsbException | UsbException e1 )
{
JOptionPane.showMessageDialog( AirspyTunerConfigurationPanel.this,
"Couldn't set sample rate to " + rate.getLabel() );
mLog.error( "Error setting airspy sample rate", e1 );
}
}
} );
add( mSampleRateCombo, "span 3,growx" );
/**
* Decimation
*/
add( new JLabel( "Decimation:" ) );
mDecimationCombo = new JComboBox();
add( mDecimationCombo, "span 3,growx" );
mDecimationCombo.setEnabled( false );
/**
* Effective Rate
*/
add( new JLabel( "Effective Rate:" ) );
mEffectiveRate = new JLabel( "10,000,000" );
add( mEffectiveRate, "span 3,growx" );
add( new JSeparator(), "span,growx,push" );
/**
* IF/VGA Gain
*/
add( new JLabel( "IF Gain" ) );
add( new JLabel( "" ), "span 2" );
mIFGainLabel = new JLabel( String.valueOf( mSelectedConfig.getIFGain() ) );
add( mIFGainLabel, "wrap" );
mIFGain = new JSlider( JSlider.HORIZONTAL,
AirspyTunerController.IF_GAIN_MIN,
AirspyTunerController.IF_GAIN_MAX,
mSelectedConfig.getIFGain() );
mIFGain.setMajorTickSpacing( 1 );
mIFGain.setPaintTicks( true );
mIFGain.addChangeListener( new ChangeListener()
{
@Override
public void stateChanged( ChangeEvent event )
{
int gain = mIFGain.getValue();
try
{
mController.setIFGain( gain );
mSelectedConfig.setIFGain( gain );
save();
mIFGainLabel.setText( String.valueOf( gain ) );
}
catch ( Exception e )
{
mLog.error( "Couldn't set airspy IF gain to:" + gain, e );
JOptionPane.showMessageDialog( mIFGain,
"Couldn't set IF gain value to " + gain );
mIFGain.setValue( mSelectedConfig.getIFGain() );
}
}
} );
add( mIFGain, "span,grow" );
add( new JLabel( "Mixer Gain" ) );
mMixerAGC = new JCheckBox( "AGC" );
mMixerAGC.setSelected( mSelectedConfig.isMixerAGC() );
mMixerAGC.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
try
{
mController.setMixerAGC( !mSelectedConfig.isMixerAGC() );
mSelectedConfig.setMixerAGC( !mSelectedConfig.isMixerAGC() );
save();
}
catch ( Exception e1 )
{
mLog.error( "Error setting Mixer AGC Enabled:" + !mSelectedConfig.isMixerAGC() );
mMixerAGC.setSelected( mSelectedConfig.isMixerAGC() );
}
}
} );
add( mMixerAGC, "span 2,center" );
mMixerGainLabel = new JLabel( String.valueOf( mSelectedConfig.getMixerGain() ) );
add( mMixerGainLabel, "wrap" );
mMixerGain = new JSlider( JSlider.HORIZONTAL,
AirspyTunerController.MIXER_GAIN_MIN,
AirspyTunerController.MIXER_GAIN_MAX,
mSelectedConfig.getMixerGain() );
mMixerGain.setMajorTickSpacing( 1 );
mMixerGain.setPaintTicks( true );
mMixerGain.addChangeListener( new ChangeListener()
{
@Override
public void stateChanged( ChangeEvent event )
{
int gain = mMixerGain.getValue();
try
{
mController.setMixerGain( gain );
mSelectedConfig.setMixerGain( gain );
save();
mMixerGainLabel.setText( String.valueOf( gain ) );
}
catch ( Exception e )
{
mLog.error( "Couldn't set airspy Mixer gain to:" + gain, e );
JOptionPane.showMessageDialog( mIFGain, "Couldn't set Mixer gain value to " + gain );
mMixerGain.setValue( mSelectedConfig.getMixerGain() );
}
}
} );
add( mMixerGain, "span,grow" );
add( new JLabel( "LNA Gain" ) );
mLNAAGC = new JCheckBox( "AGC" );
mLNAAGC.setSelected( mSelectedConfig.isLNAAGC() );
mLNAAGC.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
try
{
mController.setLNAAGC( !mSelectedConfig.isLNAAGC() );
mSelectedConfig.setLNAAGC( !mSelectedConfig.isLNAAGC() );
save();
}
catch ( Exception e1 )
{
mLog.error( "Error setting LNA AGC Enabled:" + !mSelectedConfig.isLNAAGC() );
mLNAAGC.setSelected( mSelectedConfig.isLNAAGC() );
}
}
} );
add( mLNAAGC, "span 2,center" );
mLNAGainLabel = new JLabel( String.valueOf( mSelectedConfig.getLNAGain() ) );
add( mLNAGainLabel, "wrap" );
mLNAGain = new JSlider( JSlider.HORIZONTAL,
AirspyTunerController.LNA_GAIN_MIN,
AirspyTunerController.LNA_GAIN_MAX,
mSelectedConfig.getLNAGain() );
mLNAGain.setMajorTickSpacing( 1 );
mLNAGain.setPaintTicks( true );
mLNAGain.addChangeListener( new ChangeListener()
{
@Override
public void stateChanged( ChangeEvent event )
{
int gain = mLNAGain.getValue();
try
{
mController.setLNAGain( gain );
mSelectedConfig.setLNAGain( gain );
save();
mLNAGainLabel.setText( String.valueOf( gain ) );
}
catch ( Exception e )
{
mLog.error( "Couldn't set airspy LNA gain to:" + gain, e );
JOptionPane.showMessageDialog( mIFGain, "Couldn't set LNA gain value to " + gain );
mLNAGain.setValue( mSelectedConfig.getLNAGain() );
}
}
} );
add( mLNAGain, "span,grow" );
/**
* Create a new configuration
*/
mNewConfiguration = new JButton( "New" );
mNewConfiguration.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
TunerConfiguration config =
mResourceManager.getSettingsManager()
.addNewTunerConfiguration(
TunerType.AIRSPY_R820T,
"New Configuration" );
mComboConfigurations.setModel( getModel() );
mComboConfigurations.setSelectedItem( config );
repaint();
}
} );
add( mNewConfiguration, "span 2,growx,push" );
/**
* Delete the currently selected configuration
*/
mDeleteConfiguration = new JButton( "Delete" );
mDeleteConfiguration.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
AirspyTunerConfiguration selected =
(AirspyTunerConfiguration)mComboConfigurations
.getItemAt( mComboConfigurations.getSelectedIndex() );
if( selected != null )
{
int n = JOptionPane.showConfirmDialog(
AirspyTunerConfigurationPanel.this,
"Are you sure you want to delete '"
+ selected.getName() + "'?",
"Are you sure?",
JOptionPane.YES_NO_OPTION );
if( n == JOptionPane.YES_OPTION )
{
mResourceManager.getSettingsManager()
.deleteTunerConfiguration( selected );
mComboConfigurations.setModel( getModel() );
mComboConfigurations.setSelectedIndex( 0 );
repaint();
}
}
}
} );
add( mDeleteConfiguration, "span 2,growx,push" );
}
/**
* Updates gui controls with the values from the tuner configuration
* @param config - tuner configuration
*/
private void update( AirspyTunerConfiguration config )
{
mSelectedConfig = config;
try
{
mController.apply( config );
mName.setText( config.getName() );
AirspySampleRate rate = mController.getSampleRate(
mSelectedConfig.getSampleRate() );
if( rate != null )
{
mSampleRateCombo.setSelectedItem( rate );
}
//TODO: set decimation and effective rate here
mIFGain.setValue( mSelectedConfig.getIFGain() );
mMixerGain.setValue( mSelectedConfig.getMixerGain() );
mLNAGain.setValue( mSelectedConfig.getLNAGain() );
mMixerAGC.setSelected( mSelectedConfig.isMixerAGC() );
mLNAAGC.setSelected( mSelectedConfig.isLNAAGC() );
mResourceManager.getSettingsManager().setSelectedTunerConfiguration(
TunerType.AIRSPY_R820T, mController.getDeviceInfo().getSerialNumber(), config );
save();
}
catch ( /*UsbException |*/ SourceException e1 )
{
JOptionPane.showMessageDialog(
AirspyTunerConfigurationPanel.this,
"Airspy Tuner Controller - couldn't "
+ "apply the tuner configuration settings - " +
e1.getLocalizedMessage() );
mLog.error( "Airspy Tuner Controller - couldn't apply "
+ "config [" + config.getName() + "]", e1 );
}
}
/**
* Constructs a combo box model for the tuner configuration combo component
* by assembling a list of the tuner configurations appropriate for this
* specific tuner
*/
private ComboBoxModel<AirspyTunerConfiguration> getModel()
{
ArrayList<TunerConfiguration> configs =
mResourceManager.getSettingsManager()
.getTunerConfigurations( TunerType.AIRSPY_R820T );
DefaultComboBoxModel<AirspyTunerConfiguration> model =
new DefaultComboBoxModel<AirspyTunerConfiguration>();
for( TunerConfiguration config: configs )
{
model.addElement( (AirspyTunerConfiguration)config );
}
return model;
}
/**
* Retrieves a specific (named) tuner configuration
*/
private AirspyTunerConfiguration getNamedConfiguration( String name )
{
ArrayList<TunerConfiguration> configs =
mResourceManager.getSettingsManager()
.getTunerConfigurations( TunerType.AIRSPY_R820T );
for( TunerConfiguration config: configs )
{
if( config.getName().contentEquals( name ) )
{
return (AirspyTunerConfiguration)config;
}
}
return null;
}
/**
* Saves current tuner configuration settings
*/
private void save()
{
mResourceManager.getSettingsManager().save();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,113 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package source.tuner.airspy;
import gui.control.JFrequencyControl;
import java.awt.Color;
import javax.swing.JLabel;
import javax.swing.JPanel;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import source.SourceException;
import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeEvent.Attribute;
import source.tuner.frequency.FrequencyChangeListener;
import com.jidesoft.swing.JideTabbedPane;
import controller.ResourceManager;
public class AirspyTunerEditorPanel extends JPanel
implements FrequencyChangeListener
{
private static final long serialVersionUID = 1L;
private final static Logger mLog =
LoggerFactory.getLogger( AirspyTunerEditorPanel.class );
private JFrequencyControl mFrequencyControl;
private AirspyTuner mTuner;
private AirspyTunerController mController;
private ResourceManager mResourceManager;
public AirspyTunerEditorPanel( AirspyTuner tuner,
ResourceManager resourceManager )
{
mTuner = tuner;
mController = mTuner.getController();
mResourceManager = resourceManager;
initGUI();
}
private void initGUI()
{
setLayout( new MigLayout( "fill,wrap 2",
"[right,grow][grow]",
"[][][][][][grow]" ) );
add( new JLabel( "Airspy Tuner" ), "span,align center" );
mFrequencyControl = new JFrequencyControl();
mFrequencyControl.addListener( this );
/* Add frequency control as frequency change listener. This creates a
* feedback loop, so the control does not rebroadcast the event */
mTuner.addListener( mFrequencyControl );
mFrequencyControl.setFrequency( mController.getFrequency(), false );
add( mFrequencyControl, "span,align center" );
JideTabbedPane tabs = new JideTabbedPane();
tabs.setFont( this.getFont() );
tabs.setForeground( Color.BLACK );
add( tabs, "span,grow,push" );
tabs.add( "Config", new AirspyTunerConfigurationPanel(
mResourceManager, mTuner.getController() ) );
tabs.add( "Info", new AirspyInformationPanel( mTuner.getController() ) );
}
@Override
public void frequencyChanged( FrequencyChangeEvent event )
{
if( event.getAttribute() == Attribute.FREQUENCY )
{
try
{
mController.setFrequency( event.getValue().longValue() );
}
catch ( SourceException e )
{
mLog.error( "error setting frequency [" +
event.getValue().longValue() + "]", e );
}
}
}
}

View File

@@ -86,6 +86,12 @@ public class B100Tuner extends Tuner
return 0;
}
@Override
public double getSampleSize()
{
return 16.0;
}
@Override
public long getFrequency() throws SourceException
{

View File

@@ -99,6 +99,12 @@ public class FCDTuner extends MixerTuner
return (int)mMixerTunerType.getAudioFormat().getSampleRate();
}
@Override
public double getSampleSize()
{
return 16.0;
}
@Override
public long getFrequency() throws SourceException
{

View File

@@ -20,7 +20,6 @@ package source.tuner.hackrf;
import java.util.concurrent.RejectedExecutionException;
import javax.swing.JPanel;
import javax.usb.UsbException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,7 +34,6 @@ import source.tuner.TunerClass;
import source.tuner.TunerConfiguration;
import source.tuner.TunerType;
import source.tuner.hackrf.HackRFTunerController.BoardID;
import source.tuner.hackrf.HackRFTunerController.Serial;
import controller.ResourceManager;
import controller.ThreadPoolManager;
@@ -53,23 +51,6 @@ public class HackRFTuner extends Tuner
/* Register for frequency/sample rate changes */
mController.addListener( this );
try
{
BoardID id = mController.getBoardID();
super.setName( id.getLabel() );
String version = mController.getFirmwareVersion();
mLog.info( "HackRF firmware: " + version );
Serial serial = mController.getSerial();
mLog.info( "HackRF Part ID: " + serial.getPartID() );
mLog.info( "HackRF Serial Number: " + serial.getSerialNumber() );
}
catch( UsbException e )
{
mLog.error( "couldn't read HackRF board id", e );
}
}
public void dispose()
@@ -112,6 +93,12 @@ public class HackRFTuner extends Tuner
return mController.getSampleRate();
}
@Override
public double getSampleSize()
{
return 8.0;
}
@Override
public long getFrequency() throws SourceException
{

View File

@@ -142,8 +142,21 @@ public class HackRFTunerController extends TunerController
{
mDeviceHandle = new DeviceHandle();
LibUsb.open( mDevice, mDeviceHandle );
int result = LibUsb.open( mDevice, mDeviceHandle );
if( result != 0 )
{
if( result == LibUsb.ERROR_ACCESS )
{
mLog.error( "Unable to access HackRF - insufficient permissions. "
+ "If you are running a Linux OS, have you installed the "
+ "hackRF rules file in \\etc\\udev\\rules.d ??" );
}
throw new SourceException( "Couldn't open hackrf device - " +
LibUsb.strError( result ) );
}
try
{
claimInterface();
@@ -298,7 +311,7 @@ public class HackRFTunerController extends TunerController
*/
public long getTunedFrequency() throws SourceException
{
return 0;
return mFrequencyController.getTunedFrequency();
}
@Override

View File

@@ -127,6 +127,12 @@ public class RTL2832Tuner extends Tuner
+ "sample rate", e );
}
}
@Override
public double getSampleSize()
{
return 8.0;
}
@Override
public long getFrequency() throws SourceException

View File

@@ -13,8 +13,12 @@ import javax.swing.JLayeredPane;
import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.miginfocom.swing.MigLayout;
import sample.Listener;
import sample.SampleType;
@@ -31,10 +35,11 @@ import source.tuner.frequency.FrequencyChangeEvent.Attribute;
import spectrum.converter.DFTResultsConverter;
import spectrum.converter.RealDecibelConverter;
import spectrum.menu.AveragingItem;
import spectrum.menu.DBScaleItem;
import spectrum.menu.FFTWidthItem;
import spectrum.menu.FFTWindowTypeItem;
import spectrum.menu.FrameRateItem;
import spectrum.menu.SmoothingItem;
import spectrum.menu.SmoothingTypeItem;
import controller.ResourceManager;
import controller.channel.Channel;
import controller.channel.ChannelEvent;
@@ -42,6 +47,9 @@ import controller.channel.ChannelEventListener;
import dsp.filter.Filters;
import dsp.filter.FloatHalfBandFilter;
import dsp.filter.Window.WindowType;
import dsp.filter.halfband.real.HalfBandFilter_RB_RB;
import dsp.filter.hilbert.HilbertTransform;
import dsp.filter.smoothing.SmoothingFilter.SmoothingType;
public class ChannelSpectrumPanel extends JPanel
implements ChannelEventListener,
@@ -49,9 +57,9 @@ public class ChannelSpectrumPanel extends JPanel
SettingChangeListener,
SpectralDisplayAdjuster
{
private static final long serialVersionUID = 1L;
private static final String CHANNEL_SPECTRUM_AVERAGING_SIZE =
"channel_spectrum_averaging_size";
private static final Logger mLog = LoggerFactory.getLogger( ChannelSpectrumPanel.class );
private static final long serialVersionUID = 1L;
private ResourceManager mResourceManager;
private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.REAL );
@@ -62,13 +70,13 @@ public class ChannelSpectrumPanel extends JPanel
private Channel mCurrentChannel;
private int mSpectrumAveragingSize;
private int mSampleBufferSize = 2400;
private float[] mSamples = new float[ mSampleBufferSize ];
private int mSamplePointer = 0;
private DecimatingSampleAssembler mDecimatingSampleAssembler;
private HalfBandFilter_RB_RB mDecimatingFilter = new HalfBandFilter_RB_RB(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true );
private AtomicBoolean mEnabled = new AtomicBoolean();
public ChannelSpectrumPanel( ResourceManager resourceManager )
@@ -76,20 +84,12 @@ public class ChannelSpectrumPanel extends JPanel
mResourceManager = resourceManager;
mResourceManager.getSettingsManager().addListener( this );
mSpectrumPanel = new SpectrumPanel( mResourceManager );
mSpectrumPanel.setAveraging( 1 );
mOverlayPanel = new ChannelOverlayPanel( mResourceManager );
mSpectrumAveragingSize = 10;
// 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(
new FrequencyChangeEvent( Attribute.SAMPLE_RATE, 24000 ) );
@@ -254,10 +254,11 @@ public class ChannelSpectrumPanel extends JPanel
@Override
public void receive( RealBuffer buffer )
{
for( float sample: buffer.getSamples() )
{
mDecimatingSampleAssembler.receive( sample );
}
RealBuffer decimated = mDecimatingFilter.filter( buffer );
//Hack: we're placing real samples in a complex buffer that the DFT
//processor is expecting.
mDFTProcessor.receive( new ComplexBuffer( decimated.getSamples() ) );
}
/**
@@ -367,17 +368,9 @@ public class ChannelSpectrumPanel extends JPanel
*/
JMenu averagingMenu = new JMenu( "Averaging" );
averagingMenu.add(
new AveragingItem( ChannelSpectrumPanel.this, 4 ) );
new AveragingItem( ChannelSpectrumPanel.this, 2 ) );
displayMenu.add( averagingMenu );
/**
* Baseline menu
*/
JMenu baselineMenu = new JMenu( "Baseline" );
baselineMenu.add(
new DBScaleItem( ChannelSpectrumPanel.this, 50 ) );
displayMenu.add( baselineMenu );
/**
* FFT width
*/
@@ -416,6 +409,24 @@ public class ChannelSpectrumPanel extends JPanel
new FFTWindowTypeItem( mDFTProcessor, type ) );
}
/**
* Smoothing menu
*/
JMenu smoothingMenu = new JMenu( "Smoothing" );
if( mSpectrumPanel.getSmoothingType() != SmoothingType.NONE )
{
smoothingMenu.add( new SmoothingItem( ChannelSpectrumPanel.this, 5 ) );
smoothingMenu.add( new JSeparator() );
}
smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.GAUSSIAN ) );
smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.TRIANGLE ) );
smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.RECTANGLE ) );
smoothingMenu.add( new SmoothingTypeItem( ChannelSpectrumPanel.this, SmoothingType.NONE ) );
displayMenu.add( smoothingMenu );
if( contextMenu != null )
{
contextMenu.show( mOverlayPanel,
@@ -443,37 +454,35 @@ public class ChannelSpectrumPanel extends JPanel
mSpectrumPanel.setAveraging( averaging );
}
@Override
public int getDBScale()
{
return mSpectrumPanel.getDBScale();
}
@Override
public void setDBScale( int baseline )
{
mSpectrumPanel.setDBScale( baseline );
}
public class DecimatingSampleAssembler
public void setSampleSize( double sampleSize )
{
private FloatHalfBandFilter mDecimationFilter = new FloatHalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002f );
private SampleAssembler mSampleAssembler;
public DecimatingSampleAssembler( Listener<ComplexBuffer> listener )
{
mSampleAssembler = new SampleAssembler( listener );
mDecimationFilter.setListener( mSampleAssembler );
}
public void receive( float sample )
{
mDecimationFilter.receive( sample );
}
mSpectrumPanel.setSampleSize( sampleSize );
}
@Override
public int getSmoothing()
{
return mSpectrumPanel.getSmoothing();
}
@Override
public void setSmoothing( int smoothing )
{
mSpectrumPanel.setSmoothing( smoothing );
}
@Override
public SmoothingType getSmoothingType()
{
return mSpectrumPanel.getSmoothingType();
}
@Override
public void setSmoothingType( SmoothingType type )
{
mSpectrumPanel.setSmoothingType( type );
}
public class SampleAssembler implements RealSampleListener
{
private Listener<ComplexBuffer> mListener;

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,13 +30,14 @@ import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import controller.NamingThreadFactory;
import sample.Buffer;
import sample.Listener;
import sample.SampleType;
import sample.complex.ComplexBuffer;
import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeListener;
import spectrum.converter.DFTResultsConverter;
import controller.NamingThreadFactory;
import dsp.filter.Window;
import dsp.filter.Window.WindowType;
@@ -44,8 +45,7 @@ import dsp.filter.Window.WindowType;
* Processes both complex samples or float samples and dispatches a float array
* of DFT results, using configurable fft size and output dispatch timelines.
*/
public class DFTProcessor implements Listener<ComplexBuffer>,
FrequencyChangeListener
public class DFTProcessor implements Listener<ComplexBuffer>, FrequencyChangeListener
{
private final static Logger mLog =
LoggerFactory.getLogger( DFTProcessor.class );
@@ -54,7 +54,7 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
new CopyOnWriteArrayList<DFTResultsConverter>();
private ArrayBlockingQueue<ComplexBuffer> mQueue =
new ArrayBlockingQueue<ComplexBuffer>( 200 );
new ArrayBlockingQueue<ComplexBuffer>( 200 );
private ScheduledExecutorService mScheduler = Executors
.newScheduledThreadPool( 1, new NamingThreadFactory( "spectrum dft" ) );
@@ -74,7 +74,6 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
private int mSampleRate;
private int mFFTFloatsPerFrame;
private float mNewFloatsPerFrame;
private float mNewFloatsPerFrameToConsume;
private float mNewFloatResidual;
private float[] mPreviousFrame = new float[ 8192 ];
@@ -231,7 +230,7 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
if( !mQueue.offer( sampleBuffer ) )
{
mLog.error( "DFTProcessor - [" + mSampleType.toString()
+ "]queue is full, purging queue, "
+ "] queue is full, purging queue, "
+ "samples[" + sampleBuffer + "]" );
mQueue.clear();
@@ -245,7 +244,7 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
try
{
ComplexBuffer buffer = mQueue.take();
Buffer buffer = mQueue.take();
mCurrentBuffer = buffer.getSamples();
}
catch ( InterruptedException e )
@@ -500,14 +499,5 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
mFFTFloatsPerFrame = ( mSampleType == SampleType.COMPLEX ?
mFFTWidth.getWidth() * 2 :
mFFTWidth.getWidth() );
if( mFFTFloatsPerFrame < mNewFloatsPerFrame )
{
mNewFloatsPerFrameToConsume = mFFTFloatsPerFrame;
}
else
{
mNewFloatsPerFrameToConsume = mNewFloatsPerFrame;
}
}
}

View File

@@ -1,35 +1,30 @@
package spectrum;
import dsp.filter.smoothing.SmoothingFilter.SmoothingType;
public interface SpectralDisplayAdjuster
{
/**
/**
* Gets the averaging value indicating the number of FFT results that are
* averaged to produce each FFT results output.
*
* @return averaging value ( 2 - 50 )
*/
public int getAveraging();
/**
* 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 dBm scale used when plotting the FFT bin results in the FFT
* spectral display. This value determines the smallest displayed power
* spectrum value that is shown at the bottom of the display.
* Sets the smoothing filter averaging window width.
*
* @return averaging value ( 1 - 50 )
* Valid values are odd in the range 3 - 29
*/
public int getDBScale();
public int getSmoothing();
public void setSmoothing( int smoothing );
/**
* Sets the dBm scale from 0 (top of display) to the scale value (bottom).
*
* @param baseline value ( -160 to -30 dBm )
* Sets the type of smoothing filter to use.
*/
public void setDBScale( int baseline );
public SmoothingType getSmoothingType();
public void setSmoothingType( SmoothingType type );
}

View File

@@ -66,10 +66,11 @@ import spectrum.OverlayPanel.ChannelDisplay;
import spectrum.converter.ComplexDecibelConverter;
import spectrum.converter.DFTResultsConverter;
import spectrum.menu.AveragingItem;
import spectrum.menu.DBScaleItem;
import spectrum.menu.FFTWidthItem;
import spectrum.menu.FFTWindowTypeItem;
import spectrum.menu.FrameRateItem;
import spectrum.menu.SmoothingItem;
import spectrum.menu.SmoothingTypeItem;
import com.jidesoft.swing.JideSplitPane;
@@ -79,6 +80,7 @@ import controller.channel.ChannelEvent;
import controller.channel.ChannelEventListener;
import controller.channel.ChannelUtils;
import dsp.filter.Window.WindowType;
import dsp.filter.smoothing.SmoothingFilter.SmoothingType;
/**
* Spectral Display Panel - comprises a DFTProcessor coupled with a waterfall
@@ -387,6 +389,9 @@ public class SpectralDisplayPanel extends JPanel
//Register the dft processor to receive samples from the tuner
mTuner.addListener( (Listener<ComplexBuffer>)mDFTProcessor );
mDFTConverter.setSampleSize( mTuner.getSampleSize() );
mSpectrumPanel.setSampleSize( mTuner.getSampleSize() );
//Fire frequency and sample rate change events so that everyone
//can init
try
@@ -597,14 +602,6 @@ public class SpectralDisplayPanel extends JPanel
if( event.getComponent() != mWaterfallPanel )
{
/**
* DB Scale menu
*/
JMenu dbScaleMenu = new JMenu( "Scale: 0 to dBm" );
dbScaleMenu.add(
new DBScaleItem( mSpectrumPanel, -100 ) );
displayMenu.add( dbScaleMenu );
/**
* Averaging menu
*/
@@ -666,6 +663,26 @@ public class SpectralDisplayPanel extends JPanel
fftWindowType.add(
new FFTWindowTypeItem( mDFTProcessor, type ) );
}
if( event.getComponent() != mWaterfallPanel )
{
/**
* Smoothing menu
*/
JMenu smoothingMenu = new JMenu( "Smoothing" );
if( mSpectrumPanel.getSmoothingType() != SmoothingType.NONE )
{
smoothingMenu.add( new SmoothingItem( mSpectrumPanel, 5 ) );
smoothingMenu.add( new JSeparator() );
}
smoothingMenu.add( new SmoothingTypeItem( mSpectrumPanel, SmoothingType.GAUSSIAN ) );
smoothingMenu.add( new SmoothingTypeItem( mSpectrumPanel, SmoothingType.TRIANGLE ) );
smoothingMenu.add( new SmoothingTypeItem( mSpectrumPanel, SmoothingType.RECTANGLE ) );
smoothingMenu.add( new SmoothingTypeItem( mSpectrumPanel, SmoothingType.NONE ) );
displayMenu.add( smoothingMenu );
}
if( contextMenu != null )
{

View File

@@ -36,8 +36,13 @@ import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting;
import settings.SettingChangeListener;
import buffer.FloatArrayCircularAveragingBuffer;
import controller.ResourceManager;
import dsp.filter.smoothing.GaussianSmoothingFilter;
import dsp.filter.smoothing.NoSmoothingFilter;
import dsp.filter.smoothing.RectangularSmoothingFilter;
import dsp.filter.smoothing.SmoothingFilter;
import dsp.filter.smoothing.SmoothingFilter.SmoothingType;
import dsp.filter.smoothing.TriangularSmoothingFilter;
public class SpectrumPanel extends JPanel
implements DFTResultsListener,
@@ -51,9 +56,9 @@ public class SpectrumPanel extends JPanel
/* 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;
private int mAveraging = 4;
private int mDBScale = -100; //dBm
private float mDBScale;
/**
* Spectral Display Color Settings
@@ -65,6 +70,11 @@ public class SpectrumPanel extends JPanel
private static final String SPECTRUM_AVERAGING_SIZE = "spectrum_averaging_size";
private static final int SPECTRUM_TRANSLUCENCY = 128;
private static final RenderingHints RENDERING_HINTS =
new RenderingHints( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON );
private Color mColorSpectrumBackground;
private Color mColorSpectrumGradientTop;
@@ -73,11 +83,20 @@ public class SpectrumPanel extends JPanel
private float mSpectrumInset = 20.0f;
private SmoothingFilter mSmoothingFilter = new GaussianSmoothingFilter();
private ResourceManager mResourceManager;
public SpectrumPanel( ResourceManager resourceManager )
{
mResourceManager = resourceManager;
RENDERING_HINTS.put( RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY );
setSampleSize( 16.0 );
mSmoothingFilter.setPointSize( SmoothingFilter.SMOOTHING_DEFAULT );
getSettings();
@@ -89,12 +108,16 @@ public class SpectrumPanel extends JPanel
mResourceManager = null;
}
public void setSampleSize( double sampleSize )
{
assert( 2.0 <= sampleSize && sampleSize <= 32.0 );
mDBScale = -(float)( 20.0 * Math.log10( Math.pow( 2.0, sampleSize ) ) );
}
public void setAveraging( int size )
{
mAveraging = size;
mFFTAveragingBuffer =
new FloatArrayCircularAveragingBuffer( mAveraging );
}
public int getAveraging()
@@ -102,19 +125,6 @@ public class SpectrumPanel extends JPanel
return mAveraging;
}
/**
* Sets the lowest decibel scale value to show on the display
*/
public void setDBScale( int dbScale )
{
mDBScale = dbScale;
}
public int getDBScale()
{
return mDBScale;
}
public void clearSpectrum()
{
mDisplayFFTBins = null;
@@ -180,18 +190,36 @@ public class SpectrumPanel extends JPanel
*/
public void receive( float[] currentFFTBins )
{
if( Float.isInfinite( currentFFTBins[ 0 ] ) || Float.isNaN( currentFFTBins[ 0 ] ) )
{
currentFFTBins = new float[ currentFFTBins.length ];
}
//Construct and/or resize our fft results variables
if( mDisplayFFTBins == null ||
mDisplayFFTBins.length != currentFFTBins.length )
{
mDisplayFFTBins = new float[ currentFFTBins.length ];
mDisplayFFTBins = currentFFTBins;
}
/**
* 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 );
//Smooth across the bins
float[] smoothedBins = mSmoothingFilter.filter( currentFFTBins );
//Average bins over multiple frames
if( mAveraging > 1 )
{
float gain = 1.0f / (float)mAveraging;
for( int x = 0; x < mDisplayFFTBins.length; x++ )
{
mDisplayFFTBins[ x ] += ( smoothedBins[ x ] - mDisplayFFTBins[ x ] ) * gain;
}
}
else
{
mDisplayFFTBins = smoothedBins;
}
repaint();
}
@@ -204,14 +232,8 @@ public class SpectrumPanel extends JPanel
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 );
graphics.setRenderingHints( RENDERING_HINTS );
drawSpectrum( graphics );
}
@@ -231,7 +253,7 @@ public class SpectrumPanel extends JPanel
//Define the gradient
GradientPaint gradient = new GradientPaint( 0,
0,
getSize().height / 4,
mColorSpectrumGradientTop,
0,
getSize().height,
@@ -253,7 +275,7 @@ public class SpectrumPanel extends JPanel
{
float insideHeight = size.height - mSpectrumInset;
float scalor = insideHeight / ( -mDBScale );
float scalor = insideHeight / mDBScale;
float insideWidth = size.width;
@@ -271,7 +293,7 @@ public class SpectrumPanel extends JPanel
}
else
{
height = (float)( Math.abs( mDisplayFFTBins[ x ] ) * scalor );
height = mDisplayFFTBins[ x ] * scalor;
if( height > insideHeight )
{
@@ -316,5 +338,54 @@ public class SpectrumPanel extends JPanel
}
@Override
public void settingDeleted( Setting setting ) {}
public void settingDeleted( Setting setting ) { /* not implemented */ }
@Override
public int getSmoothing()
{
return mSmoothingFilter.getPointSize();
}
@Override
public void setSmoothing( int smoothing )
{
mSmoothingFilter.setPointSize( smoothing );
}
@Override
public SmoothingType getSmoothingType()
{
return mSmoothingFilter.getSmoothingType();
}
@Override
public void setSmoothingType( SmoothingType type )
{
if( mSmoothingFilter.getSmoothingType() != type )
{
int pointSize = getSmoothing();
synchronized ( mSmoothingFilter )
{
switch( type )
{
case GAUSSIAN:
mSmoothingFilter = new GaussianSmoothingFilter();
break;
case RECTANGLE:
mSmoothingFilter = new RectangularSmoothingFilter();
break;
case TRIANGLE:
mSmoothingFilter = new TriangularSmoothingFilter();
break;
case NONE:
default:
mSmoothingFilter = new NoSmoothingFilter();
break;
}
}
setSmoothing( pointSize );
}
}
}

View File

@@ -1,3 +1,20 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package spectrum.converter;
import org.slf4j.Logger;
@@ -6,52 +23,60 @@ import org.slf4j.LoggerFactory;
public class ComplexDecibelConverter extends DFTResultsConverter
{
private final static Logger mLog =
LoggerFactory.getLogger( DFTResultsConverter.class );
LoggerFactory.getLogger( ComplexDecibelConverter.class );
private float mDynamicRangeReference = (float)Math.pow( 2.0, 16.0 );
/**
* 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.
* calculation into the power spectrum in decibels, normalized to the
* sample bit depth.
*/
public ComplexDecibelConverter()
{
}
/**
* Specifies the bit depth to establish the maximum dynamic range. All
* FFT bin values will be scaled according to this value.
*/
@Override
public void setSampleSize( double size )
{
assert( 2.0 <= size && size <= 32.0 );
mDynamicRangeReference = (float)Math.pow( 2.0d, size );
}
@Override
public void receive( float[] results )
{
float[] processed = new float[ results.length / 2 ];
/* 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 < results.length; x += 2 )
{
float magnitude = (float)Math.sqrt( results[ x ] *
results[ x ] + results[ x + 1 ] * results[ x + 1 ] );
/* Place upper half of magnitudes in lower half of results, vice-versa */
//Calculate the magnitude squared value from each bin's real and
//imaginary value, scale it to the available dynamic range and
//convert to dB.
float normalizedMagnitude = 10.0f * (float)Math.log10(
( ( results[ x ] * results[ x ] ) +
( results[ x + 1 ] * results[ x + 1 ] ) ) /
mDynamicRangeReference );
// We have to swap the upper and lower halves of the JTransforms
// DFT results for correct display
if( x / 2 >= half )
{
processed[ x / 2 - half ] = magnitude;
processed[ x / 2 - half ] = normalizedMagnitude;
}
else
{
processed[ x / 2 + half ] = magnitude;
processed[ x / 2 + half ] = normalizedMagnitude;
}
}
/* Use the DC component as the largest magnitude value to keep the
* display from jumping when a strong bursting signal exists */
float largestMagnitude = processed[ half ];
if( largestMagnitude > 0 )
{
/* Convert magnitudes to normized power spectrum dBm */
for( int i = 0; i < processed.length; i++ )
{
processed[ i ] = 20.0f *
(float)Math.log10( processed[ i ] / largestMagnitude );
}
dispatch( processed );
}
dispatch( processed );
}
}

View File

@@ -18,6 +18,12 @@ public abstract class DFTResultsConverter
public DFTResultsConverter()
{
}
/**
* Specifies the sample size in bits to establish the maximum dynamic range.
* All FFT bin values will be scaled according to this value.
*/
public abstract void setSampleSize( double size );
public void dispose()
{

View File

@@ -1,7 +1,31 @@
package spectrum.converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RealDecibelConverter extends DFTResultsConverter
{
private static final Logger mLog = LoggerFactory.getLogger( RealDecibelConverter.class );
private float mDynamicRangeReference;
public RealDecibelConverter()
{
setSampleSize( 24.0 );
}
/**
* Specifies the bit depth to establish the maximum dynamic range. All
* FFT bin values will be scaled according to this value.
*/
@Override
public void setSampleSize( double size )
{
assert( 2.0 <= size && size <= 32.0 );
mDynamicRangeReference = (float)Math.pow( 2.0d, size );
}
/**
* Converts the output of the JTransforms FloatFFT_1D.realForward()
* calculation into a normalized power spectrum in decibels, per description
@@ -10,29 +34,22 @@ public class RealDecibelConverter extends DFTResultsConverter
@Override
public void receive( float[] results )
{
float dc = Math.abs( results[ 0 ] );
float[] processed = new float[ results.length / 4 ];
/* Ensure we have power in the DC power bin (bin 0), otherwise don't
* dispatch the results */
if( dc != 0 )
int index = 0;
for( int x = 0; x < processed.length; x ++ )
{
/* Note: we're only processing 1/4 of the output samples */
float[] processed = new float[ results.length / 4 + 1 ];
index = x * 2;
for( int x = 0; x < processed.length - 2; x ++ )
{
int index = ( x + 1 ) * 2;
float normalizedMagnitude = 10.0f * (float)Math.log10(
( ( results[ index ] * results[ index ] ) +
( results[ index + 1 ] * results[ index + 1 ] ) ) /
mDynamicRangeReference );
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 );
processed[ x ] = normalizedMagnitude;
}
dispatch( processed );
}
}

View File

@@ -18,14 +18,15 @@ public class AveragingItem extends JSlider implements ChangeListener
public AveragingItem( SpectralDisplayAdjuster adjuster, int defaultValue )
{
super( JSlider.HORIZONTAL, 2, 50, adjuster.getAveraging() );
super( JSlider.HORIZONTAL, 1, 20, adjuster.getAveraging() );
mDefaultValue = defaultValue;
mAdjuster = adjuster;
setMajorTickSpacing( 10 );
setMinorTickSpacing( 5 );
setMajorTickSpacing( 5 );
setMinorTickSpacing( 1 );
setPaintTicks( true );
setPaintLabels( true );
addChangeListener( this );

View File

@@ -2,34 +2,50 @@ package spectrum.menu;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Hashtable;
import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import spectrum.SpectralDisplayAdjuster;
import dsp.filter.smoothing.SmoothingFilter;
public class DBScaleItem extends JSlider implements ChangeListener
public class SmoothingItem extends JSlider implements ChangeListener
{
private static final long serialVersionUID = 1L;
private SpectralDisplayAdjuster mAdjuster;
private int mDefaultValue;
public DBScaleItem( SpectralDisplayAdjuster adjuster, int defaultValue )
public SmoothingItem( SpectralDisplayAdjuster adjuster, int defaultValue )
{
super( JSlider.HORIZONTAL, -160, -30, adjuster.getDBScale() );
mDefaultValue = defaultValue;
super( JSlider.HORIZONTAL,
SmoothingFilter.SMOOTHING_MINIMUM,
SmoothingFilter.SMOOTHING_MAXIMUM,
adjuster.getSmoothing() );
mDefaultValue = defaultValue;
mAdjuster = adjuster;
setMajorTickSpacing( 40 );
setMinorTickSpacing( 10 );
setSnapToTicks( true );
setMajorTickSpacing( 6 );
setMinorTickSpacing( 2 );
setPaintTicks( true );
setPaintLabels( true );
addChangeListener( this );
Hashtable<Integer,JLabel> labels = new Hashtable<>();
labels.put( new Integer( 3 ), new JLabel( "3" ) );
labels.put( new Integer( 9 ), new JLabel( "9" ) );
labels.put( new Integer( 15 ), new JLabel( "15" ) );
labels.put( new Integer( 21 ), new JLabel( "21" ) );
labels.put( new Integer( 27 ), new JLabel( "27" ) );
setLabelTable( labels );
addChangeListener( this );
addMouseListener( new MouseListener()
{
@Override
@@ -37,7 +53,7 @@ public class DBScaleItem extends JSlider implements ChangeListener
{
if( event.getClickCount() == 2 )
{
DBScaleItem.this.setValue( mDefaultValue );
SmoothingItem.this.setValue( mDefaultValue );
}
}
@@ -52,7 +68,10 @@ public class DBScaleItem extends JSlider implements ChangeListener
public void stateChanged( ChangeEvent event )
{
int value = ((JSlider)event.getSource()).getValue();
mAdjuster.setDBScale( value );
if( value % 2 == 1 )
{
mAdjuster.setSmoothing( value );
}
}
}

View File

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