Resolves issue with correct display and scaling of decibel values in

both the spectral and waterfall displays.

Resolves issue with gain loss in HilbertTransform.
This commit is contained in:
denny
2015-11-08 08:17:34 -05:00
parent 70e62ebb35
commit f0dc9452c8
10 changed files with 214 additions and 83 deletions

View File

@@ -9,7 +9,7 @@
an alias identifier having the lowest priority.
- Audio recording by call with new 'Non-Recordable' alias action
- (planned)Added Ettus UHD support for B100 with WBX tuner
- (planned)Added Airspy tuner support
- Added Airspy tuner support
0.1.5 20150612 Added initial P25 Decoder support with integrated audio decoding
using the JMBE library

View File

@@ -146,8 +146,7 @@ public class HilbertTransform implements Listener<RealBuffer>
if( mInvertFlag )
{
//inphase
samples[ y ] = -( mCenterCoefficent *
mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ] );
samples[ y ] = -( mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ] );
//quadrature
samples[ y + 1 ] = -accumulator;
@@ -155,8 +154,7 @@ public class HilbertTransform implements Listener<RealBuffer>
else
{
//inphase
samples[ y ] = mCenterCoefficent *
mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ];
samples[ y ] = mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ];
//quadrature
samples[ y + 1 ] = accumulator;
@@ -255,6 +253,9 @@ public class HilbertTransform implements Listener<RealBuffer>
* 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.
*
* Note: we apply a 2.0 gain to the coefficients to compensate for the loss
* of splitting the signal via two filters.
*/
private void convertHalfBandToHilbert( float[] coefficients )
{
@@ -266,15 +267,15 @@ public class HilbertTransform implements Listener<RealBuffer>
{
if( x < middle )
{
mHilbertFilter[ x ] = -Math.abs( coefficients[ x ] );
mHilbertFilter[ x ] = 2.0f * -Math.abs( coefficients[ x ] );
}
else if( x > middle )
{
mHilbertFilter[ x ] = Math.abs( coefficients[ x ] );
mHilbertFilter[ x ] = 2.0f * Math.abs( coefficients[ x ] );
}
else
{
mHilbertFilter[ x ] = coefficients[ x ];
mHilbertFilter[ x ] = 2.0f * coefficients[ x ];
}
}
}

View File

@@ -17,10 +17,21 @@
******************************************************************************/
package dsp.mixer;
import java.util.Arrays;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dsp.filter.Filters;
import dsp.filter.hilbert.HilbertTransform;
import sample.complex.Complex;
public class Oscillator
{
private final static Logger mLog =
LoggerFactory.getLogger( Oscillator.class );
private double mFrequency;
private double mSampleRate;
@@ -105,4 +116,109 @@ public class Oscillator
{
return mCurrentAngle.real();
}
public static void main( String[] args )
{
Oscillator o = new Oscillator( 1536, 4096 );
o.rotate();
float[] samples = new float[ 8192 ];
for( int x = 0; x < 8192; x++ )
{
samples[ x ] = o.inphase();
o.rotate();
}
logLargest( "Real: ", samples, false );
HilbertTransform ht = new HilbertTransform();
float[] transformed = ht.filter( samples );
logLargest( "Hilbert: ", transformed, true );
FloatFFT_1D fft = new FloatFFT_1D( 4096 );
fft.complexForward( transformed );
logLargest( "DFT:", transformed, true );
}
private static void logLargest( String label, float[] samples, boolean complex )
{
float largestI = 0.0f;
int indexLargestI = 0;
float largestQ = 0.0f;
int indexLargestQ = 0;
float smallestI = 0.0f;
int indexsmallestI = 0;
float smallestQ = 0.0f;
int indexsmallestQ = 0;
for( int x = 0; x < samples.length; x++ )
{
if( complex )
{
if( x % 2 == 0 )
{
if( samples[ x ] > largestI )
{
largestI = samples[ x ];
indexLargestI = x;
}
if( samples[ x ] < smallestI )
{
smallestI = samples[ x ];
indexsmallestI = x;
}
}
else
{
if( samples[ x ] > largestQ )
{
largestQ = samples[ x ];
indexLargestQ = x;
}
if( samples[ x ] < smallestQ )
{
smallestQ = samples[ x ];
indexsmallestQ = x;
}
}
}
else
{
if( samples[ x ] > largestI )
{
largestI = samples[ x ];
indexLargestI = x;
}
if( samples[ x ] < smallestI )
{
smallestI = samples[ x ];
indexsmallestI = x;
}
}
}
if( complex )
{
mLog.debug( label +
" largestI (" + indexLargestI + "): " + largestI +
" smallestI (" + indexsmallestI + "): " + smallestI +
" largestQ (" + indexLargestQ + "): " + largestQ +
" smallestQ (" + indexsmallestQ + "): " + smallestQ );
}
else
{
mLog.debug( label +
" largest (" + indexLargestI + "): " + largestI +
" smallest (" + indexsmallestI + "): " + smallestI );
}
}
}

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 source.tuner.airspy;
import java.util.concurrent.RejectedExecutionException;
@@ -20,23 +37,6 @@ 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 );

View File

@@ -389,7 +389,6 @@ 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

View File

@@ -26,6 +26,7 @@ import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.util.Arrays;
import javax.swing.JPanel;
@@ -111,8 +112,8 @@ public class SpectrumPanel extends JPanel
public void setSampleSize( double sampleSize )
{
assert( 2.0 <= sampleSize && sampleSize <= 32.0 );
mDBScale = -(float)( 20.0 * Math.log10( Math.pow( 2.0, sampleSize ) ) );
mDBScale = (float)( 20.0 * Math.log10( 1 / Math.pow( 2.0, sampleSize - 1 ) ) );
}
public void setAveraging( int size )

View File

@@ -33,6 +33,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JPanel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting;
@@ -48,7 +51,10 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
Pausable,
SettingChangeListener
{
private static final long serialVersionUID = 1L;
private final static Logger mLog =
LoggerFactory.getLogger( WaterfallPanel.class );
private static final long serialVersionUID = 1L;
private static DecimalFormat CURSOR_FORMAT = new DecimalFormat( "0.00000" );
private static final String SPECTRUM_CURSOR = "spectrum_cursor";
private static final String PAUSED = "PAUSED";
@@ -202,9 +208,9 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
public void receive( float[] update )
{
//If our FFT size changes, reset our pixel map and image source
if( mFFTSize != update.length - 1 )
if( mFFTSize != update.length )
{
mFFTSize = update.length - 1;
mFFTSize = update.length;
reset();
}

View File

@@ -20,13 +20,50 @@ package spectrum.converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converts complex DFT output to scaled dB values with a maximum amplitude of
* 0 dB and all values scaled to the minimum dB value which is:
*
* Max: 0dB
* Min: 20 * log10( 1.0 / ( 2.0 ^ ( bit depth - 1) ) = scaling factor
*
* Notes:
*
* The magnitude of each DFT output bin must be scaled by the number of DFT
* points to normalize the output to a range of ( 0 to 1.0 ). DFT bin magnitude
* can be calculated as:
*
* 1) sqrt( bin[ x ] * bin[ x ] + bin[ x + 1 ] * bin[ x + 1] ) / DFTSize or,
* 2) ( bin[ x ] * bin[ x ] + bin[ x + 1 ] * bin[ x + 1] ) / ( DFTSize * DFTSize )
*
* Convert each scaled DFT bin magnitude to decibels using this formula:
*
* 20 * log10( scaledBinValue )
*
* Max Value: 20 * log10( 1.0 ) = 0 dB
*
* When plotting, these dB bin values should be scaled according to the maximum
* dynamic range supported by the source. Maximum dynamic range for bit depth
* is calculated as:
*
* 20 * log10( 1 / ( 2 ^ ( bit depth - 1 ) )
*
* A source that provides 12-bit samples can produce values in the range of
* ( 0 to 4096 ) or -2048 to 2047. The smallest observable value would then
* be 1 / 2048.
*
* So, the smallest decibel value of a source producing 12 bit samples is:
*
* 20 * log10( 1 / 2048 ) = -66.23 dB
*
* and the dynamic range is: (-66.23 to 0.0 dB)
*
*/
public class ComplexDecibelConverter extends DFTResultsConverter
{
private final static Logger mLog =
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 the power spectrum in decibels, normalized to the
@@ -36,44 +73,35 @@ public class ComplexDecibelConverter extends DFTResultsConverter
{
}
/**
* 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 ];
int half = processed.length / 2;
int halfResults = results.length / 2;
float dftScalor = 1.0f / (float)( halfResults ^ 2 );
float[] processed = new float[ halfResults ];
int middle = processed.length / 2;
for( int x = 0; x < results.length; x += 2 )
{
//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(
//Calculate the magnitude squared (power) value from each bin's real
//and imaginary value and scale it to the DFT bin size squared.
//Convert the scaled value to decibels.
float decibels = 10.0f * (float)Math.log10(
( ( results[ x ] * results[ x ] ) +
( results[ x + 1 ] * results[ x + 1 ] ) ) /
mDynamicRangeReference );
( results[ x + 1 ] * results[ x + 1 ] ) ) * dftScalor );
// We have to swap the upper and lower halves of the JTransforms
// DFT results for correct display
if( x / 2 >= half )
if( x / 2 >= middle )
{
processed[ x / 2 - half ] = normalizedMagnitude;
processed[ x / 2 - middle ] = decibels;
}
else
{
processed[ x / 2 + half ] = normalizedMagnitude;
processed[ x / 2 + middle ] = decibels;
}
}

View File

@@ -19,12 +19,6 @@ public abstract class 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()
{
mListeners.clear();

View File

@@ -7,33 +7,22 @@ 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
* in Lyons, Understanding Digital Signal Processing, 3e, page 141.
*
* Note: this is only calculating the lower half of the spectrum
*/
@Override
public void receive( float[] results )
{
float dftScalor = 1.0f / (float)( results.length ^ 2 );
float[] processed = new float[ results.length / 4 ];
int index = 0;
@@ -42,12 +31,9 @@ public class RealDecibelConverter extends DFTResultsConverter
{
index = x * 2;
float normalizedMagnitude = 10.0f * (float)Math.log10(
processed[ x ] = 20.0f * (float)Math.log10(
( ( results[ index ] * results[ index ] ) +
( results[ index + 1 ] * results[ index + 1 ] ) ) /
mDynamicRangeReference );
processed[ x ] = normalizedMagnitude;
( results[ index + 1 ] * results[ index + 1 ] ) ) * dftScalor );
}
dispatch( processed );