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. an alias identifier having the lowest priority.
- Audio recording by call with new 'Non-Recordable' alias action - Audio recording by call with new 'Non-Recordable' alias action
- (planned)Added Ettus UHD support for B100 with WBX tuner - (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 0.1.5 20150612 Added initial P25 Decoder support with integrated audio decoding
using the JMBE library using the JMBE library

View File

@@ -146,8 +146,7 @@ public class HilbertTransform implements Listener<RealBuffer>
if( mInvertFlag ) if( mInvertFlag )
{ {
//inphase //inphase
samples[ y ] = -( mCenterCoefficent * samples[ y ] = -( mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ] );
mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ] );
//quadrature //quadrature
samples[ y + 1 ] = -accumulator; samples[ y + 1 ] = -accumulator;
@@ -155,8 +154,7 @@ public class HilbertTransform implements Listener<RealBuffer>
else else
{ {
//inphase //inphase
samples[ y ] = mCenterCoefficent * samples[ y ] = mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ];
mBuffer[ mIndexMap[ index ][ mCenterTapIndex ] ];
//quadrature //quadrature
samples[ y + 1 ] = accumulator; 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 * a folded FIR structure to exploit the symmetric nature of the half-band
* filter coefficients. The polarity difference between the upper and lower * filter coefficients. The polarity difference between the upper and lower
* halves is accounted for in the filter() method. * 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 ) private void convertHalfBandToHilbert( float[] coefficients )
{ {
@@ -266,15 +267,15 @@ public class HilbertTransform implements Listener<RealBuffer>
{ {
if( x < middle ) if( x < middle )
{ {
mHilbertFilter[ x ] = -Math.abs( coefficients[ x ] ); mHilbertFilter[ x ] = 2.0f * -Math.abs( coefficients[ x ] );
} }
else if( x > middle ) else if( x > middle )
{ {
mHilbertFilter[ x ] = Math.abs( coefficients[ x ] ); mHilbertFilter[ x ] = 2.0f * Math.abs( coefficients[ x ] );
} }
else else
{ {
mHilbertFilter[ x ] = coefficients[ x ]; mHilbertFilter[ x ] = 2.0f * coefficients[ x ];
} }
} }
} }

View File

@@ -17,10 +17,21 @@
******************************************************************************/ ******************************************************************************/
package dsp.mixer; 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; import sample.complex.Complex;
public class Oscillator public class Oscillator
{ {
private final static Logger mLog =
LoggerFactory.getLogger( Oscillator.class );
private double mFrequency; private double mFrequency;
private double mSampleRate; private double mSampleRate;
@@ -105,4 +116,109 @@ public class Oscillator
{ {
return mCurrentAngle.real(); 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; package source.tuner.airspy;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
@@ -20,23 +37,6 @@ import source.tuner.airspy.AirspyTunerController.BoardID;
import controller.ResourceManager; import controller.ResourceManager;
import controller.ThreadPoolManager; 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 public class AirspyTuner extends Tuner
{ {
private final static Logger mLog = LoggerFactory.getLogger( AirspyTuner.class ); 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 //Register the dft processor to receive samples from the tuner
mTuner.addListener( (Listener<ComplexBuffer>)mDFTProcessor ); mTuner.addListener( (Listener<ComplexBuffer>)mDFTProcessor );
mDFTConverter.setSampleSize( mTuner.getSampleSize() );
mSpectrumPanel.setSampleSize( mTuner.getSampleSize() ); mSpectrumPanel.setSampleSize( mTuner.getSampleSize() );
//Fire frequency and sample rate change events so that everyone //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.RenderingHints;
import java.awt.geom.GeneralPath; import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D; import java.awt.geom.Line2D;
import java.util.Arrays;
import javax.swing.JPanel; import javax.swing.JPanel;
@@ -111,8 +112,8 @@ public class SpectrumPanel extends JPanel
public void setSampleSize( double sampleSize ) public void setSampleSize( double sampleSize )
{ {
assert( 2.0 <= sampleSize && sampleSize <= 32.0 ); 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 ) public void setAveraging( int size )

View File

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

View File

@@ -20,13 +20,50 @@ package spectrum.converter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 public class ComplexDecibelConverter extends DFTResultsConverter
{ {
private final static Logger mLog = private final static Logger mLog =
LoggerFactory.getLogger( ComplexDecibelConverter.class ); LoggerFactory.getLogger( ComplexDecibelConverter.class );
private float mDynamicRangeReference = (float)Math.pow( 2.0, 16.0 );
/** /**
* Converts the output of the JTransforms FloatFFT_1D.complexForward() * Converts the output of the JTransforms FloatFFT_1D.complexForward()
* calculation into the power spectrum in decibels, normalized to the * 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 @Override
public void receive( float[] results ) public void receive( float[] results )
{ {
float[] processed = new float[ results.length / 2 ]; int halfResults = results.length / 2;
int half = processed.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 ) for( int x = 0; x < results.length; x += 2 )
{ {
//Calculate the magnitude squared value from each bin's real and //Calculate the magnitude squared (power) value from each bin's real
//imaginary value, scale it to the available dynamic range and //and imaginary value and scale it to the DFT bin size squared.
//convert to dB. //Convert the scaled value to decibels.
float normalizedMagnitude = 10.0f * (float)Math.log10( float decibels = 10.0f * (float)Math.log10(
( ( results[ x ] * results[ x ] ) + ( ( results[ x ] * results[ x ] ) +
( results[ x + 1 ] * results[ x + 1 ] ) ) / ( results[ x + 1 ] * results[ x + 1 ] ) ) * dftScalor );
mDynamicRangeReference );
// We have to swap the upper and lower halves of the JTransforms // We have to swap the upper and lower halves of the JTransforms
// DFT results for correct display // DFT results for correct display
if( x / 2 >= half ) if( x / 2 >= middle )
{ {
processed[ x / 2 - half ] = normalizedMagnitude; processed[ x / 2 - middle ] = decibels;
} }
else 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() public void dispose()
{ {
mListeners.clear(); mListeners.clear();

View File

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